diff --git a/src/main/basedbot/fleet-strategies/destruct-all-strategy.ts b/src/main/basedbot/fleet-strategies/destruct-all-strategy.ts new file mode 100644 index 00000000..29ed0b2c --- /dev/null +++ b/src/main/basedbot/fleet-strategies/destruct-all-strategy.ts @@ -0,0 +1,21 @@ +import { Game } from '@staratlas/sage' + +import { createDestructStrategy, destructConfig } from '../fsm/destruct' +import { Player } from '../lib/sage/state/user-account' +import { WorldMap } from '../lib/sage/state/world-map' + +import { nameMapMatcher } from './name-map-matcher' +import { makeStrategyMap, StrategyConfig } from './strategy-config' + +export const destructAllStrategy = ( + worldMap: WorldMap, + player: Player, + game: Game, +): StrategyConfig => { + return { + match: nameMapMatcher( + createDestructStrategy(destructConfig({ worldMap }), player, game), + ), + map: makeStrategyMap(), + } +} diff --git a/src/main/basedbot/fleet-strategies/get-fleet-strategy.ts b/src/main/basedbot/fleet-strategies/get-fleet-strategy.ts index c505acfc..bdddf5ba 100644 --- a/src/main/basedbot/fleet-strategies/get-fleet-strategy.ts +++ b/src/main/basedbot/fleet-strategies/get-fleet-strategy.ts @@ -4,6 +4,7 @@ import { Player } from '../lib/sage/state/user-account' import { WorldMap } from '../lib/sage/state/world-map' import { atlasnetLuStrategy } from './atlasnet-lu-strategy' +import { destructAllStrategy } from './destruct-all-strategy' import { disbandAllStrategy } from './disband-all-strategy' import { mainnetLuStrategy } from './mainnet-lu-strategy' import { StrategyConfig } from './strategy-config' @@ -20,16 +21,16 @@ export const getFleetStrategy = ( return mainnetLuStrategy(map, player, game) case 'CgHvzwGbwWv3CwLTvEgeqSKeD8EwMdTfiiCG3dFrKVVC': // return atlasnetFcStrategy(150)(map, player, game, 'mud') - return disbandAllStrategy(map, player, game) + return destructAllStrategy(map, player, game) case '9KBrgWVjsmdZ3YEjcsa3wrbbJREgZgS7vDbgoz2aHaNm': // return atlasnetFcStrategy(15)(map, player, game, 'ustur') - return disbandAllStrategy(map, player, game) + return destructAllStrategy(map, player, game) case 'FUwHSqujzcPD44SDZYJXuk73NbkEyYQwcLMioHhpjbx2': // return atlasnetFcStrategy(2)(map, player, game, 'oni') - return disbandAllStrategy(map, player, game) + return destructAllStrategy(map, player, game) case '34ghznSJCYEMrS1aC55UYZZUuxfuurA9441aKnigmYyz': // return atlasnetQtStrategy(1)(map, player, game, 'le.local') - return disbandAllStrategy(map, player, game) + return destructAllStrategy(map, player, game) default: throw new Error('Unknown strategy') } diff --git a/src/main/basedbot/fsm/destruct.ts b/src/main/basedbot/fsm/destruct.ts new file mode 100644 index 00000000..f9abddcc --- /dev/null +++ b/src/main/basedbot/fsm/destruct.ts @@ -0,0 +1,176 @@ +import { Game } from '@staratlas/sage' +import dayjs from 'dayjs' + +import { now } from '../../../dayjs' +import { logger } from '../../../logger' +import { disbandFleet } from '../lib/sage/act/disband-fleet' +import { dock } from '../lib/sage/act/dock' +import { endMine } from '../lib/sage/act/end-mine' +import { endMove } from '../lib/sage/act/end-move' +import { selfDestruct } from '../lib/sage/act/self-destruct' +import { stopSubwarp } from '../lib/sage/act/stop-subwarp' +import { undock } from '../lib/sage/act/undock' +import { starbaseByCoordinates } from '../lib/sage/state/starbase-by-coordinates' +import { Player } from '../lib/sage/state/user-account' +import { FleetInfo } from '../lib/sage/state/user-fleets' +import { mineableByCoordinates, WorldMap } from '../lib/sage/state/world-map' +import { getName } from '../lib/sage/util' + +import { DisbandConfig } from './configs/disband-config' +import { Strategy } from './strategy' + +// eslint-disable-next-line complexity +const transition = async ( + fleetInfo: FleetInfo, + player: Player, + game: Game, + config: DestructConfig, +): Promise => { + const currentStarbase = await starbaseByCoordinates(fleetInfo.location) + const { fleetName, location } = fleetInfo + const homeBase = player.homeCoordinates + const isAtHomeBase = homeBase.equals(location) + + switch (fleetInfo.fleetState.type) { + case 'Idle': { + logger.info( + `${fleetName} is idle at ${fleetInfo.fleetState.data.sector} [Starbase: ${currentStarbase ? getName(currentStarbase) : 'N/A'}]`, + ) + + if (isAtHomeBase) { + logger.info(`${fleetName} is at home base, docking to disband`) + + return dock(fleetInfo, location, player, game) + } + + return selfDestruct(fleetInfo, player, game) + } + case 'StarbaseLoadingBay': { + logger.info( + `${fleetInfo.fleetName} is in the loading bay at ${getName(fleetInfo.fleetState.data.starbase)}`, + ) + + if (isAtHomeBase) { + logger.info( + `${fleetInfo.fleetName} is at home base, disbanding...`, + ) + + return disbandFleet( + player, + game, + player.homeStarbase, + fleetInfo, + ) + } + logger.info( + `${fleetInfo.fleetName} is at ${location}, undocking...`, + ) + + return undock(fleetInfo.fleet, fleetInfo.location, player, game) + } + case 'MoveWarp': { + const { fromSector, toSector, warpFinish } = + fleetInfo.fleetState.data + + if (!homeBase.equals(toSector)) { + logger.info(`Stopping fleet ${fleetInfo.fleetName}`) + + return endMove(fleetInfo, player, game) + } + + if (warpFinish.isBefore(now())) { + logger.info( + `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + ) + } else { + logger.info( + `${fleetInfo.fleetName} warping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(warpFinish.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + ) + } + break + } + case 'MoveSubwarp': { + const { fromSector, toSector, arrivalTime } = + fleetInfo.fleetState.data + + if (!homeBase.equals(toSector)) { + logger.info(`Stopping fleet ${fleetInfo.fleetName}`) + + return stopSubwarp(fleetInfo, player, game) + } + + if (arrivalTime.isBefore(now())) { + logger.info( + `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + ) + } else { + logger.info( + `${fleetInfo.fleetName} subwarping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + ) + } + break + } + case 'MineAsteroid': { + const { mineItem, end, amountMined } = fleetInfo.fleetState.data + + if (end.isBefore(now())) { + logger.info( + `${fleetInfo.fleetName} has finished mining ${getName(mineItem)} for ${amountMined}`, + ) + } + + logger.info( + `${fleetInfo.fleetName} mining ${getName(mineItem)} for ${amountMined}. Ending...`, + ) + const resource = mineableByCoordinates( + config.worldMap, + fleetInfo.location, + ) + .values() + .next().value + + return endMine(fleetInfo, player, game, resource) + } + case 'Respawn': { + const { destructionTime, ETA } = fleetInfo.fleetState.data + + if (ETA.isBefore(now())) { + logger.info(`${fleetInfo.fleetName} has respawned`) + } else { + logger.info( + `${fleetInfo.fleetName} respawning at ${fleetInfo.fleetState.data.sector}. ETA: ${dayjs.duration(ETA.diff(now())).humanize(false)}. Destruction time: ${destructionTime}`, + ) + } + break + } + default: + logger.info( + `${fleetInfo.fleetName} is ${fleetInfo.fleetState.type}`, + ) + + return Promise.resolve() + } +} + +export type DestructConfig = { + worldMap: WorldMap +} + +export const destructConfig = ( + config: Partial & { + worldMap: WorldMap + }, +): DestructConfig => ({ + worldMap: config.worldMap, +}) + +export const createDestructStrategy = ( + config: DestructConfig, + player: Player, + game: Game, +): Strategy => { + return { + apply: (fleetInfo: FleetInfo): Promise => + transition(fleetInfo, player, game, config), + } +} diff --git a/src/main/basedbot/fsm/disband.ts b/src/main/basedbot/fsm/disband.ts index df5821a8..0fca98be 100644 --- a/src/main/basedbot/fsm/disband.ts +++ b/src/main/basedbot/fsm/disband.ts @@ -9,6 +9,7 @@ import { dock } from '../lib/sage/act/dock' import { endMine } from '../lib/sage/act/end-mine' import { endMove } from '../lib/sage/act/end-move' import { move } from '../lib/sage/act/move' +import { selfDestruct } from '../lib/sage/act/self-destruct' import { stopSubwarp } from '../lib/sage/act/stop-subwarp' import { undock } from '../lib/sage/act/undock' import { starbaseByCoordinates } from '../lib/sage/state/starbase-by-coordinates' @@ -44,7 +45,7 @@ const transition = async ( `${fleetName} is out of fuel and not at a starbase, need self destruction`, ) - return Promise.resolve() + return selfDestruct(fleetInfo, player, game) } if (isAtHomeBase) { logger.info(`${fleetName} is at home base, docking to disband`) diff --git a/src/main/basedbot/fsm/mine.ts b/src/main/basedbot/fsm/mine.ts index c571b4df..d2ef80d3 100644 --- a/src/main/basedbot/fsm/mine.ts +++ b/src/main/basedbot/fsm/mine.ts @@ -8,6 +8,7 @@ import { endMine } from '../lib/sage/act/end-mine' import { loadCargo } from '../lib/sage/act/load-cargo' import { mine } from '../lib/sage/act/mine' import { move } from '../lib/sage/act/move' +import { selfDestruct } from '../lib/sage/act/self-destruct' import { undock } from '../lib/sage/act/undock' import { unloadAllCargo } from '../lib/sage/act/unload-all-cargo' import { starbaseByCoordinates } from '../lib/sage/state/starbase-by-coordinates' @@ -71,7 +72,7 @@ const transition = async ( `${fleetName} is out of fuel and not at a starbase, need self destruction`, ) - return Promise.resolve() + return selfDestruct(fleetInfo, player, game) } if (isAtHomeBase) { logger.info(`${fleetName} is at home base`) diff --git a/src/main/basedbot/fsm/transport.ts b/src/main/basedbot/fsm/transport.ts index e96671cc..43136485 100644 --- a/src/main/basedbot/fsm/transport.ts +++ b/src/main/basedbot/fsm/transport.ts @@ -8,6 +8,7 @@ import { getTokenBalance } from '../basedbot' import { dock } from '../lib/sage/act/dock' import { loadCargo } from '../lib/sage/act/load-cargo' import { move, WarpMode } from '../lib/sage/act/move' +import { selfDestruct } from '../lib/sage/act/self-destruct' import { undock } from '../lib/sage/act/undock' import { getHold, unloadCargo } from '../lib/sage/act/unload-cargo' import { starbaseByCoordinates } from '../lib/sage/state/starbase-by-coordinates' @@ -63,7 +64,7 @@ const transition = async ( `${fleetName} is out of fuel and not at a starbase, need self destruction`, ) - return Promise.resolve() + return selfDestruct(fleetInfo, player, game) } if (isSameBase) { logger.warn( diff --git a/src/main/basedbot/lib/sage/act/exit-respawn.ts b/src/main/basedbot/lib/sage/act/exit-respawn.ts index 0bab4780..bc71b570 100644 --- a/src/main/basedbot/lib/sage/act/exit-respawn.ts +++ b/src/main/basedbot/lib/sage/act/exit-respawn.ts @@ -1,14 +1,22 @@ -import { ixReturnsToIxs } from '@staratlas/data-source' +import { getAssociatedTokenAddressSync } from '@solana/spl-token' +import { PublicKey } from '@solana/web3.js' +import { InstructionReturn, ixReturnsToIxs } from '@staratlas/data-source' import { Game, Starbase } from '@staratlas/sage' import { logger } from '../../../../../logger' +import { connection } from '../../../../../service/sol' import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx' import { programs } from '../../programs' import { exitRespawnIx } from '../ix/exit-respawn' +import { forceDropFleetCargoIx } from '../ix/force-drop-fleet-cargo' +import { getCargoStatsDefinition } from '../state/cargo-stats-definition' +import { getCargoType } from '../state/cargo-types' import { getStarbasePlayer } from '../state/starbase-player' import { Player } from '../state/user-account' import { FleetInfo } from '../state/user-fleets' +import { getFleetCargoHold } from './load-cargo' + export const exitRespawn = async ( fleetInfo: FleetInfo, starbase: Starbase, @@ -25,15 +33,45 @@ export const exitRespawn = async ( const starbasePlayer = await getStarbasePlayer(player, starbase, programs) - const ix = exitRespawnIx( - fleetInfo, - player, - game, - starbase, - starbasePlayer, - programs, - ) - const instructions = await ixReturnsToIxs(ix, player.signer) + const [cargoStatsDefinition] = await getCargoStatsDefinition() + + const ixs: Array = [] + + const cargoMints = player.cargoTypes.map((ct) => ct.data.mint) + + for (const key of cargoMints) { + const mint = new PublicKey(key) + const cargoType = getCargoType(player.cargoTypes, game, mint) - await sendAndConfirmInstructions(instructions) + const cargoPod = getFleetCargoHold(mint, game, fleetInfo) + const tokenFrom = getAssociatedTokenAddressSync(mint, cargoPod, true) + + const accountInfo = await connection.getAccountInfo(tokenFrom) + + if (accountInfo) { + ixs.push( + forceDropFleetCargoIx( + fleetInfo, + game, + cargoStatsDefinition, + cargoPod, + cargoType.key, + tokenFrom, + mint, + programs, + ), + ) + } + } + ixs.push( + exitRespawnIx( + fleetInfo, + player, + game, + starbase, + starbasePlayer, + programs, + ), + ) + await ixReturnsToIxs(ixs, player.signer).then(sendAndConfirmInstructions) } diff --git a/src/main/basedbot/lib/sage/act/load-cargo.ts b/src/main/basedbot/lib/sage/act/load-cargo.ts index f12e802a..3b5ee5b7 100644 --- a/src/main/basedbot/lib/sage/act/load-cargo.ts +++ b/src/main/basedbot/lib/sage/act/load-cargo.ts @@ -22,7 +22,7 @@ import { Player } from '../state/user-account' import { FleetInfo } from '../state/user-fleets' import { getName } from '../util' -export const getHold = ( +export const getFleetCargoHold = ( mint: PublicKey, game: Game, fleetInfo: FleetInfo, @@ -47,7 +47,7 @@ export const loadCargo = async ( ): Promise => { const starbase = await starbaseByCoordinates(fleetInfo.location) - const hold = getHold(mint, game, fleetInfo) + const hold = getFleetCargoHold(mint, game, fleetInfo) if (!starbase) { throw new Error(`No starbase found at ${fleetInfo.location}`) diff --git a/src/main/basedbot/lib/sage/act/self-destruct.ts b/src/main/basedbot/lib/sage/act/self-destruct.ts new file mode 100644 index 00000000..808d93f4 --- /dev/null +++ b/src/main/basedbot/lib/sage/act/self-destruct.ts @@ -0,0 +1,34 @@ +import { getAssociatedTokenAddressSync } from '@solana/spl-token' +import { ixReturnsToIxs } from '@staratlas/data-source' +import { Game } from '@staratlas/sage' + +import { logger } from '../../../../../logger' +import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx' +import { programs } from '../../programs' +import { idleToRespawnIx } from '../ix/idle-to-respawn' +import { Player } from '../state/user-account' +import { FleetInfo } from '../state/user-fleets' + +export const selfDestruct = async ( + fleetInfo: FleetInfo, + player: Player, + game: Game, +): Promise => { + const { fleet } = fleetInfo + + // TODO: Also support self-destruct for mining fleets + if (!fleet.state.Idle) { + logger.warn('Only Idle Fleets can self destruct') + + return + } + const atlasTokenFrom = getAssociatedTokenAddressSync( + game.data.mints.atlas, + player.signer.publicKey(), + ) + + await ixReturnsToIxs( + idleToRespawnIx(player, game, fleet, atlasTokenFrom, programs), + player.signer, + ).then(sendAndConfirmInstructions) +} diff --git a/src/main/basedbot/lib/sage/ix/force-drop-fleet-cargo.ts b/src/main/basedbot/lib/sage/ix/force-drop-fleet-cargo.ts new file mode 100644 index 00000000..8f8e46e5 --- /dev/null +++ b/src/main/basedbot/lib/sage/ix/force-drop-fleet-cargo.ts @@ -0,0 +1,29 @@ +import { PublicKey } from '@solana/web3.js' +import { CargoStatsDefinition } from '@staratlas/cargo' +import { InstructionReturn } from '@staratlas/data-source' +import { Fleet, Game } from '@staratlas/sage' + +import { StarAtlasPrograms } from '../../programs' +import { FleetInfo } from '../state/user-fleets' + +export const forceDropFleetCargoIx = ( + fleetInfo: FleetInfo, + game: Game, + cargoStatsDefinition: CargoStatsDefinition, + cargoPod: PublicKey, + cargoType: PublicKey, + tokenFrom: PublicKey, + tokenMint: PublicKey, + programs: StarAtlasPrograms, +): InstructionReturn => + Fleet.forceDropFleetCargo( + programs.sage, + programs.cargo, + fleetInfo.fleet.key, + cargoPod, + cargoType, + cargoStatsDefinition.key, + game.key, + tokenFrom, + tokenMint, + ) diff --git a/src/main/basedbot/lib/sage/ix/idle-to-respawn.ts b/src/main/basedbot/lib/sage/ix/idle-to-respawn.ts new file mode 100644 index 00000000..2bfafea1 --- /dev/null +++ b/src/main/basedbot/lib/sage/ix/idle-to-respawn.ts @@ -0,0 +1,26 @@ +import { PublicKey } from '@solana/web3.js' +import { InstructionReturn } from '@staratlas/data-source' +import { Fleet, Game } from '@staratlas/sage' + +import { StarAtlasPrograms } from '../../programs' +import { Player } from '../state/user-account' + +export const idleToRespawnIx = ( + player: Player, + game: Game, + fleet: Fleet, + atlasTokenFrom: PublicKey, + programs: StarAtlasPrograms, +): InstructionReturn => + Fleet.idleToRespawn( + programs.sage, + player.signer, + player.profile.key, + player.profileFaction.key, + fleet.key, + atlasTokenFrom, + game.data.vaults.atlas, + game.data.gameState, + game.key, + { keyIndex: 0 }, + )