diff --git a/src/db/getOrPopulateMarkets.ts b/src/db/getOrPopulateMarkets.ts deleted file mode 100644 index 4b501d6..0000000 --- a/src/db/getOrPopulateMarkets.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { findMarkets } from '../features/status/actions/findMarkets' -import { queryMarkets } from '../features/status/actions/queryMarkets' -import { apiFactory } from '../features/status/apiFactory' -import { WaypointEntity } from '../features/status/waypoint.entity' -import { log } from '../logging/configure-logging' -import { getEntityManager } from '../orm' - -export const getOrPopulateMarkets = async ( - api: ReturnType, - resetDate: string, - systemSymbol: string, -): Promise => { - const em = getEntityManager() - const data = await em.findAll(WaypointEntity, { where: { systemSymbol, resetDate } }) - if (data.length) return data - log.warn('populate-data', 'Populating market data') - const markets = await findMarkets(api.systems, systemSymbol) - const marketData = await queryMarkets(api.systems, markets) - const newData = marketData.map((data) => { - const market = markets.find((m) => m.symbol === data.symbol)! - return new WaypointEntity( - resetDate, - market.systemSymbol, - market.symbol, - data.imports.map((d) => d.symbol), - data.exports.map((d) => d.symbol), - data.exchange.map((d) => d.symbol), - data.tradeGoods, - market.x, - market.y, - ) - }) - await Promise.all(newData.map((m) => em.persist(m))) - await em.flush() - return newData -} diff --git a/src/features/actors/mining-drone.ts b/src/features/actors/mining-drone.ts index 3bdbecf..5548e37 100644 --- a/src/features/actors/mining-drone.ts +++ b/src/features/actors/mining-drone.ts @@ -15,7 +15,7 @@ export const miningDroneActorFactory = ( miningLocation: IWaypoint, keep: TradeSymbol[], ) => - decisionMaker(miningDrone, agent, act, async (ship: ShipEntity, agent: AgentEntity) => { + decisionMaker(miningDrone, false, agent, act, async (ship: ShipEntity, agent: AgentEntity) => { await act.refuelShip(ship) await act.jettisonUnwanted(miningDrone, keep) if (ship.nav.waypointSymbol !== miningLocation.symbol) { diff --git a/src/features/actors/shuttle.ts b/src/features/actors/shuttle.ts index 625b0fe..b2d67e6 100644 --- a/src/features/actors/shuttle.ts +++ b/src/features/actors/shuttle.ts @@ -14,7 +14,7 @@ export const shuttleActorFactory = ( miningLocation: IWaypoint, ships: ShipEntity[], sell: TradeSymbol[], -) => decisionMaker(shuttle, agent, act, shuttleLogicFactory(act, markets, miningLocation, ships, sell)) +) => decisionMaker(shuttle, true, agent, act, shuttleLogicFactory(act, markets, miningLocation, ships, sell)) export const shuttleLogicFactory = ( diff --git a/src/features/ship/ship.entity.ts b/src/features/ship/ship.entity.ts index c41a18e..729fe1b 100644 --- a/src/features/ship/ship.entity.ts +++ b/src/features/ship/ship.entity.ts @@ -87,6 +87,7 @@ export class ShipEntity { } get label() { - return `${this.registration.role} (${this.symbol})` + const suffix = this.symbol.match(/-(.+$)/)?.[1] ?? '' + return `${this.registration.role}-${suffix}` } } diff --git a/src/features/status/actions/getActor.ts b/src/features/status/actions/getActor.ts index 29ea53a..3b0c17c 100644 --- a/src/features/status/actions/getActor.ts +++ b/src/features/status/actions/getActor.ts @@ -8,7 +8,8 @@ import { getEntityManager } from '../../../orm' import { ShipActionType, ShipEntity } from '../../ship/ship.entity' import { AgentEntity } from '../agent.entity' import { apiFactory } from '../apiFactory' -import { writeCredits, writeExtraction, writeMarketTransaction, writeShipyardTransaction } from '../influxWrite' +import { writeCredits, writeExtraction, writeMyMarketTransaction, writeShipyardTransaction } from '../influxWrite' +import { updateWaypoint } from '../systemScan' import { getClosest } from '../utils/getClosest' import { shipArriving, shipCooldownRemaining } from '../utils/getCurrentFlightTime' import { getSellLocations } from '../utils/getSellLocations' @@ -80,7 +81,7 @@ export const getActor = async (agent: AgentEntity, api: ReturnType { + const em = getEntityManager() + const waypoint = await em.findOneOrFail(WaypointEntity, { symbol: ship.nav.waypointSymbol, resetDate: agent.resetDate }) + await updateWaypoint(waypoint, api) + } + const orbitShip = async (ship: ShipEntity) => { const { data: { @@ -260,7 +267,7 @@ export const getActor = async (agent: AgentEntity, api: ReturnType { - const shipyardForType = shipyards.find((x) => x.shipyard?.shipTypes.map((s) => s.type).includes(shipType)) - invariant(shipyardForType, `Expected to find a shipyard with ${shipType}`) - const shipyard = shipyards.find((x) => x.symbol === shipyardForType.symbol) + const purchaseShip = async (buyer: ShipEntity, shipType: ShipType, waypoints: WaypointEntity[], ships: ShipEntity[]) => { + const shipyard = waypoints.find((x) => x.shipyard?.shipTypes.map((s) => s.type).includes(shipType)) invariant(shipyard, `Expected to find a waypoint for the ${shipType} shipyard`) - if (buyer.nav.route.destination.symbol !== shipyardForType.symbol) { - await navigateShip(buyer, shipyard, markets) + if (buyer.nav.route.destination.symbol !== shipyard.symbol) { + await navigateShip(buyer, shipyard, waypoints) return } @@ -364,5 +363,6 @@ export const getActor = async (agent: AgentEntity, api: ReturnType { } export const decisionMaker = async ( ship: ShipEntity, + shouldUpdateWaypoint: boolean, agent: AgentEntity, act: Awaited>, decisions: (ship: ShipEntity, agent: AgentEntity) => void, @@ -33,6 +34,10 @@ export const decisionMaker = async ( try { const { seconds, distance } = shipArriving(ship) if (seconds <= 0) { + if (shouldUpdateWaypoint) { + await act.updateCurrentWaypoint(ship) + } + await decisions(ship, agent) } else { log.info('ship', `${ship.label} is not yet in position. Waiting for arrival ${distance}`) diff --git a/src/features/status/influxWrite.ts b/src/features/status/influxWrite.ts index 66ad810..529c81b 100644 --- a/src/features/status/influxWrite.ts +++ b/src/features/status/influxWrite.ts @@ -23,9 +23,21 @@ export const writePoint = ( fields, resetDate, agentSymbol, - }: { measurementName: string; tags: K[]; fields: K[]; resetDate: string; agentSymbol: string }, + timestamp = new Date().toISOString(), + }: { + measurementName: string + tags: K[] + fields: K[] + resetDate: string + agentSymbol: string + timestamp?: string + }, ) => { const point = new Point(measurementName) + if (timestamp) { + const date = new Date(timestamp) + point.timestamp(date) + } Object.entries(lodash.pick(object, tags)).forEach(([key, value]) => { point.tag(key, value as string) }) @@ -37,14 +49,8 @@ export const writePoint = ( influxWrite().writePoint(point) } -export const writeMarketTransaction = (resetDate: string, transaction: MarketTransaction, agent: Agent) => { - writePoint(transaction, { - measurementName: 'market-transaction', - tags: ['waypointSymbol', 'shipSymbol', 'tradeSymbol', 'type'], - fields: ['units', 'pricePerUnit', 'totalPrice'], - resetDate, - agentSymbol: agent.symbol, - }) +export const writeMyMarketTransaction = (resetDate: string, transaction: MarketTransaction, agent: Agent) => { + writeMarketTransaction(transaction, resetDate, agent.symbol) writeCredits(agent, resetDate) } @@ -85,3 +91,14 @@ export const writeShipyardTransaction = (resetDate: string, transaction: Shipyar }) writeCredits(agent, resetDate) } + +export function writeMarketTransaction(transaction: MarketTransaction, resetDate: string, agentSymbol: string) { + writePoint(transaction, { + measurementName: 'market-transaction', + tags: ['waypointSymbol', 'shipSymbol', 'tradeSymbol', 'type'], + fields: ['units', 'pricePerUnit', 'totalPrice'], + resetDate, + agentSymbol, + timestamp: transaction.timestamp, + }) +} diff --git a/src/features/status/startup.ts b/src/features/status/startup.ts index 58a8371..dbc981b 100644 --- a/src/features/status/startup.ts +++ b/src/features/status/startup.ts @@ -1,6 +1,4 @@ -import lodash from 'lodash' import { DefaultApiFactory, TradeSymbol } from '../../../api' -import { getOrPopulateMarkets } from '../../db/getOrPopulateMarkets' import { updateShips } from '../../db/updateShips' import { invariant } from '../../invariant' import { log } from '../../logging/configure-logging' @@ -10,7 +8,7 @@ import { ShipEntity } from '../ship/ship.entity' import { getActor } from './actions/getActor' import { getAgent } from './actions/getAgent' import { decisionMaker } from './decisionMaker' -import { updateWaypoint } from './updateWaypoint' +import { systemScan } from './systemScan' export type Position = { x: number; y: number } @@ -31,7 +29,7 @@ export async function startup() { const systemSymbol = commandShip.nav.systemSymbol - const { markets, shipyards } = await initSystem(api, resetDate, systemSymbol) + const waypoints = await systemScan(systemSymbol, resetDate, api) const { data: { @@ -39,13 +37,13 @@ export async function startup() { }, } = await api.systems.getSystemWaypoints(commandShip.nav.systemSymbol, undefined, 20, 'ENGINEERED_ASTEROID') - const miningDronesToPurchase = 15 + const miningDronesToPurchase = 20 const shuttlesToPurchase = 1 const keep: TradeSymbol[] = ['IRON_ORE', 'COPPER_ORE', 'ALUMINUM_ORE'] - const shuttleLogic = shuttleLogicFactory(act, markets, engineeredAsteroid, ships, keep) + const shuttleLogic = shuttleLogicFactory(act, waypoints, engineeredAsteroid, ships, keep) - await decisionMaker(commandShip, agent, act, async (ship: ShipEntity) => { + await decisionMaker(commandShip, true, agent, act, async (ship: ShipEntity) => { if (!agent.contract || agent.contract.fulfilled) { await act.getOrAcceptContract(ship) return @@ -59,57 +57,30 @@ export async function startup() { const miningDrones = ships.filter((s) => s.frame.symbol === 'FRAME_DRONE') // TODO: don't hardcode the price if (miningDrones.length < miningDronesToPurchase && (agent.data?.credits ?? 0) > 50000) { - await act.purchaseShip(commandShip, 'SHIP_MINING_DRONE', shipyards, markets, ships) + await act.purchaseShip(commandShip, 'SHIP_MINING_DRONE', waypoints, ships) return } else { const idleDrones = miningDrones.filter((s) => !s.isCommanded) idleDrones.forEach((drone) => { log.warn('command', `Spawning worker for ${drone.label}`) drone.isCommanded = true - miningDroneActorFactory(drone, agent, act, markets, engineeredAsteroid, keep) + miningDroneActorFactory(drone, agent, act, waypoints, engineeredAsteroid, keep) }) } const shuttles = ships.filter((s) => s.frame.symbol === 'FRAME_SHUTTLE') if (shuttles.length < shuttlesToPurchase) { - await act.purchaseShip(commandShip, 'SHIP_LIGHT_SHUTTLE', shipyards, markets, ships) + await act.purchaseShip(commandShip, 'SHIP_LIGHT_SHUTTLE', waypoints, ships) return } else { const idleShuttles = shuttles.filter((s) => !s.isCommanded) idleShuttles.forEach((ship) => { log.warn('command', `Spawning worker for ${ship.label}`) ship.isCommanded = true - shuttleActorFactory(ship, agent, act, markets, engineeredAsteroid, ships, keep) + shuttleActorFactory(ship, agent, act, waypoints, engineeredAsteroid, ships, keep) }) } await shuttleLogic(commandShip, agent) }) } - -const initSystem = async (api: Awaited>['api'], resetDate: string, systemSymbol: string) => { - const { - data: { data: shipyardWaypoints, meta }, - //@ts-expect-error because it is wrong - } = await api.systems.getSystemWaypoints(systemSymbol, undefined, 20, undefined, { traits: ['SHIPYARD'] }) - invariant(meta.total < 21, 'Expected less than 21 shipyards') - - const markets = await getOrPopulateMarkets(api, resetDate, systemSymbol) - - const shipyards = await Promise.all( - shipyardWaypoints.map(async (waypoint) => { - const { - data: { data: shipyard }, - } = await api.systems.getShipyard(systemSymbol, waypoint.symbol) - const data = lodash.omit(shipyard, 'transactions', 'symbol') - const result = await updateWaypoint( - resetDate, - waypoint.symbol, - { modificationsFee: data.modificationsFee, shipTypes: data.shipTypes }, - data.ships, - ) - return result - }), - ) - return { markets, shipyards } -} diff --git a/src/features/status/systemScan.ts b/src/features/status/systemScan.ts new file mode 100644 index 0000000..c86ef1b --- /dev/null +++ b/src/features/status/systemScan.ts @@ -0,0 +1,90 @@ +import { WaypointTraitSymbol } from '../../../api' +import { invariant } from '../../invariant' +import { getEntityManager } from '../../orm' +import { getAgent } from './actions/getAgent' +import { writeMarketTransaction } from './influxWrite' +import { WaypointEntity } from './waypoint.entity' + +async function getAllWaypoints(api: Awaited>['api'], systemSymbol: string) { + const { + data: { data: waypoints, meta }, + } = await api.systems.getSystemWaypoints(systemSymbol, 1, 20) + if (meta.total > meta.page * meta.limit) { + const allWaypoints = await Promise.all( + Array.from({ length: Math.ceil(meta.total / meta.limit) - 1 }, (_, i) => api.systems.getSystemWaypoints(systemSymbol, i + 2, 20)), + ) + return waypoints.concat(allWaypoints.flatMap((r) => r.data.data)) + } +} + +export async function updateWaypoint(waypoint: WaypointEntity, api: Awaited>['api']) { + if (waypoint.traits.includes(WaypointTraitSymbol.Marketplace)) { + const { + data: { data: market }, + } = await api.systems.getMarket(waypoint.systemSymbol, waypoint.symbol) + + waypoint.imports = market.imports.map((i) => i.symbol) + waypoint.exports = market.exports.map((e) => e.symbol) + waypoint.exchange = market.exchange.map((e) => e.symbol) + if (market.tradeGoods) { + waypoint.tradeGoods = market.tradeGoods + } + if (market.transactions) { + market.transactions.forEach((t) => { + writeMarketTransaction(t, waypoint.resetDate, t.shipSymbol) + }) + } + } + + if (waypoint.traits.includes(WaypointTraitSymbol.Shipyard)) { + const { + data: { data: shipyard }, + } = await api.systems.getShipyard(waypoint.systemSymbol, waypoint.symbol) + waypoint.shipyard = { + modificationsFee: shipyard.modificationsFee, + shipTypes: shipyard.shipTypes, + } + if (shipyard.ships) waypoint.ships = shipyard.ships + } +} + +export async function systemScan( + systemSymbol: string, + resetDate: string, + api: Awaited>['api'], +): Promise { + const waypoints = await getAllWaypoints(api, systemSymbol) + invariant(waypoints?.length, 'Expected to find waypoints') + const em = getEntityManager() + await em.upsertMany( + WaypointEntity, + waypoints.map( + ({ isUnderConstruction, traits, type, x, y, faction, modifiers, symbol }) => + new WaypointEntity({ + resetDate, + symbol, + systemSymbol, + x, + y, + isUnderConstruction, + traits: traits.map((t) => t.symbol), + faction: faction?.symbol, + type, + modifiers: modifiers?.map((m) => m.symbol) ?? [], + }), + ), + ) + + const entities = await em.find(WaypointEntity, { resetDate, systemSymbol }) + + await Promise.all( + entities.map(async (waypoint) => { + await updateWaypoint(waypoint, api) + em.persist(waypoint) + }), + ) + + await em.flush() + + return entities +} diff --git a/src/features/status/updateWaypoint.ts b/src/features/status/updateWaypoint.ts deleted file mode 100644 index 40d2b71..0000000 --- a/src/features/status/updateWaypoint.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Shipyard } from '../../../api' -import { getEntityManager } from '../../orm' -import { WaypointEntity } from './waypoint.entity' - -export const updateWaypoint = async ( - resetDate: string, - symbol: string, - shipyard: Pick | undefined, - ships: Shipyard['ships'] | undefined, -): Promise => { - const em = getEntityManager().fork() - const waypoint = await em.findOneOrFail(WaypointEntity, { resetDate, symbol }) - waypoint.shipyard = shipyard - if (ships) waypoint.ships = ships - await em.persistAndFlush(waypoint) - return waypoint -} diff --git a/src/features/status/waypoint.entity.ts b/src/features/status/waypoint.entity.ts index 0c4043c..ac861fa 100644 --- a/src/features/status/waypoint.entity.ts +++ b/src/features/status/waypoint.entity.ts @@ -1,19 +1,16 @@ -import { Entity, PrimaryKey, Property } from '@mikro-orm/core' +import { Entity, PrimaryKey, PrimaryKeyProp, Property } from '@mikro-orm/core' import { MarketTradeGood, Shipyard, TradeSymbol } from '../../../api' @Entity({ tableName: 'waypoint' }) export class WaypointEntity { - @PrimaryKey({ autoincrement: true }) - id!: number - - @Property() + @PrimaryKey() resetDate: string - @Property() - systemSymbol: string + @PrimaryKey() + symbol: string @Property() - symbol: string + systemSymbol: string @Property() x: number @@ -22,13 +19,13 @@ export class WaypointEntity { y: number @Property() - imports: TradeSymbol[] + imports: TradeSymbol[] = [] @Property() - exports: TradeSymbol[] + exports: TradeSymbol[] = [] @Property() - exchange: TradeSymbol[] + exchange: TradeSymbol[] = [] @Property({ type: 'json' }) tradeGoods: MarketTradeGood[] | undefined @@ -37,27 +34,46 @@ export class WaypointEntity { shipyard: undefined | Pick @Property({ type: 'json' }) - ships: undefined | Shipyard['ships'] - - constructor( - resetDate: string, - systemSymbol: string, - symbol: string, - imports: TradeSymbol[], - exports: TradeSymbol[], - exchange: TradeSymbol[], - tradeGoods: MarketTradeGood[] | undefined, - x: number, - y: number, - ) { - this.resetDate = resetDate - this.symbol = symbol - this.systemSymbol = systemSymbol - this.x = x - this.y = y - this.imports = imports - this.exports = exports - this.exchange = exchange - this.tradeGoods = tradeGoods + ships: undefined | Shipyard['ships']; + + [PrimaryKeyProp]?: ['resetDate', 'symbol'] + + @Property() + isUnderConstruction: boolean + + @Property() + traits: string[] + + @Property() + faction: string | undefined + + @Property() + type: string + + @Property() + modifiers: string[] + + constructor(values: { + resetDate: string + symbol: string + systemSymbol: string + x: number + y: number + isUnderConstruction: boolean + traits: string[] + faction: string | undefined + type: string + modifiers: string[] + }) { + this.resetDate = values.resetDate + this.symbol = values.symbol + this.systemSymbol = values.systemSymbol + this.x = values.x + this.y = values.y + this.isUnderConstruction = values.isUnderConstruction + this.traits = values.traits + this.faction = values.faction + this.type = values.type + this.modifiers = values.modifiers } } diff --git a/src/migrations/.snapshot-spacetraders.json b/src/migrations/.snapshot-spacetraders.json index ef2f6a5..6a9902e 100644 --- a/src/migrations/.snapshot-spacetraders.json +++ b/src/migrations/.snapshot-spacetraders.json @@ -288,15 +288,6 @@ }, { "columns": { - "id": { - "name": "id", - "type": "serial", - "unsigned": true, - "autoincrement": true, - "primary": true, - "nullable": false, - "mappedType": "integer" - }, "reset_date": { "name": "reset_date", "type": "varchar(255)", @@ -306,8 +297,8 @@ "nullable": false, "mappedType": "string" }, - "system_symbol": { - "name": "system_symbol", + "symbol": { + "name": "symbol", "type": "varchar(255)", "unsigned": false, "autoincrement": false, @@ -315,8 +306,8 @@ "nullable": false, "mappedType": "string" }, - "symbol": { - "name": "symbol", + "system_symbol": { + "name": "system_symbol", "type": "varchar(255)", "unsigned": false, "autoincrement": false, @@ -395,6 +386,51 @@ "primary": false, "nullable": true, "mappedType": "json" + }, + "is_under_construction": { + "name": "is_under_construction", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "boolean" + }, + "traits": { + "name": "traits", + "type": "text[]", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "array" + }, + "faction": { + "name": "faction", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "string" + }, + "type": { + "name": "type", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "string" + }, + "modifiers": { + "name": "modifiers", + "type": "text[]", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "array" } }, "name": "waypoint", @@ -403,9 +439,10 @@ { "keyName": "waypoint_pkey", "columnNames": [ - "id" + "reset_date", + "symbol" ], - "composite": false, + "composite": true, "constraint": true, "primary": true, "unique": true diff --git a/src/migrations/Migration20240330073122.ts b/src/migrations/Migration20240330073122.ts new file mode 100644 index 0000000..bf9a94d --- /dev/null +++ b/src/migrations/Migration20240330073122.ts @@ -0,0 +1,26 @@ +import { Migration } from '@mikro-orm/migrations' + +export class Migration20240330073122 extends Migration { + async up(): Promise { + this.addSql('truncate table "waypoint"') + this.addSql('alter table "waypoint" drop constraint "waypoint_pkey";') + this.addSql('alter table "waypoint" drop column "id";') + + this.addSql( + 'alter table "waypoint" add column "is_under_construction" boolean not null, add column "traits" text[] not null, add column "faction" varchar(255) null, add column "type" varchar(255) not null, add column "modifiers" text[] not null;', + ) + this.addSql('alter table "waypoint" add constraint "waypoint_pkey" primary key ("reset_date", "symbol");') + } + + async down(): Promise { + this.addSql('alter table "waypoint" drop constraint "waypoint_pkey";') + this.addSql('alter table "waypoint" drop column "is_under_construction";') + this.addSql('alter table "waypoint" drop column "traits";') + this.addSql('alter table "waypoint" drop column "faction";') + this.addSql('alter table "waypoint" drop column "type";') + this.addSql('alter table "waypoint" drop column "modifiers";') + + this.addSql('alter table "waypoint" add column "id" serial not null;') + this.addSql('alter table "waypoint" add constraint "waypoint_pkey" primary key ("id");') + } +}