Skip to content

Commit

Permalink
refactor: rewrite mountFromDelivery to use MultiJob
Browse files Browse the repository at this point in the history
For some reason this also made our smoke test get a lot
further, so I had to add a bunch of mocks to it.

This is all in preparation for making a new mount command
which self-purchases.
  • Loading branch information
eseidel committed Sep 24, 2023
1 parent 7b748d6 commit 9a617ff
Show file tree
Hide file tree
Showing 2 changed files with 278 additions and 135 deletions.
297 changes: 175 additions & 122 deletions packages/cli/lib/behavior/mount_from_delivery.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,25 @@ extension on Ship {
}
}

/// Change mounts on a ship.
Future<DateTime?> advanceMountFromDelivery(
/// Init the change-mounts job.
Future<JobResult> doInitJob(
BehaviorState state,
Api api,
Database db,
CentralCommand centralCommand,
Caches caches,
BehaviorState state,
Ship ship, {
DateTime Function() getNow = defaultGetNow,
}) async {
final toMount = state.mountToAdd;
final hqSystem = caches.agent.headquartersSymbol.systemSymbol;
final hqWaypoints = await caches.waypoints.waypointsInSystem(hqSystem);
final shipyard = assertNotNull(
hqWaypoints.firstWhereOrNull((w) => w.hasShipyard),
'No shipyard in $hqSystem',
const Duration(days: 1),
);
final shipyardSymbol = shipyard.waypointSymbol;

// Re-validate every loop in case resuming from error.
final template = assertNotNull(
centralCommand.templateForShip(ship),
'No template.',
Expand All @@ -50,138 +56,185 @@ Future<DateTime?> advanceMountFromDelivery(
final needed = mountsToAddToShip(ship, template);
jobAssert(needed.isNotEmpty, 'No mounts needed.', const Duration(hours: 1));

// We've already started a change-mount job, continue.
if (toMount != null) {
shipInfo(ship, 'Changing mounts. Mounting $toMount.');
final currentWaypoint =
await caches.waypoints.waypoint(ship.waypointSymbol);
if (!currentWaypoint.hasShipyard) {
shipErr(ship, 'Unexpectedly off course during change mount.');
state.isComplete = true;
return null;
}
final available = centralCommand.unclaimedMountsAt(shipyardSymbol);
// If there is a mount ready for us to claim, claim it?
// Decide on the mount and "claim" it by saving it in our state.
final toClaim = assertNotNull(
_pickMountFromAvailable(available, needed),
'No unclaimed mounts at $shipyardSymbol.',
const Duration(minutes: 10),
);
shipInfo(ship, 'Claiming mount: $toClaim.');
state.mountToAdd = toClaim;
return JobResult.complete();
}

/// Pickup the mount from the delivery ship.
Future<JobResult> doPickupJob(
BehaviorState state,
Api api,
Database db,
CentralCommand centralCommand,
Caches caches,
Ship ship, {
DateTime Function() getNow = defaultGetNow,
}) async {
// TODO(eseidel): Pickup location should be saved in state.
final hqSystem = caches.agent.headquartersSymbol.systemSymbol;
final hqWaypoints = await caches.waypoints.waypointsInSystem(hqSystem);
final shipyard = assertNotNull(
hqWaypoints.firstWhereOrNull((w) => w.hasShipyard),
'No shipyard in $hqSystem',
const Duration(days: 1),
);
final shipyardSymbol = shipyard.waypointSymbol;

final tradeSymbol = tradeSymbolForMountSymbol(toMount);
final deliveryShip = assertNotNull(
centralCommand.getDeliveryShip(ship.shipSymbol, tradeSymbol),
'No delivery ship for $tradeSymbol.',
const Duration(minutes: 10),
if (ship.waypointSymbol != shipyardSymbol) {
final waitUntil = await beingNewRouteAndLog(
api,
ship,
state,
caches.ships,
caches.systems,
caches.routePlanner,
centralCommand,
shipyardSymbol,
);
return JobResult.wait(waitUntil);
}

// We could match the docking status instead.
if (!deliveryShip.isDocked) {
shipErr(ship, 'Delivery ship undocked during change mount, docking it.');
// Terrible hack.
await dockIfNeeded(api, caches.ships, deliveryShip);
}
final tradeSymbol = tradeSymbolForMountSymbol(state.mountToAdd!);
final deliveryShip = assertNotNull(
centralCommand.getDeliveryShip(ship.shipSymbol, tradeSymbol),
'No delivery ship for $tradeSymbol.',
const Duration(minutes: 10),
);

await dockIfNeeded(api, caches.ships, ship);

if (ship.countUnits(tradeSymbol) < 1) {
try {
// Get it from the delivery ship.
await transferCargoAndLog(
api,
caches.ships,
from: deliveryShip,
to: ship,
tradeSymbol: tradeSymbol,
units: 1,
);
} on ApiException catch (e) {
shipErr(ship, 'Failed to transfer mount: $e');
jobAssert(
false,
'Failed to transfer mount.',
const Duration(minutes: 10),
);
}
}
// We could match the docking status instead.
if (!deliveryShip.isDocked) {
shipErr(ship, 'Delivery ship undocked during change mount, docking it.');
// Terrible hack.
await dockIfNeeded(api, caches.ships, deliveryShip);
}

// TODO(eseidel): This should only remove mounts if we absolutely need to.
// This could end up removing mounts before we need to.
final toRemove = mountsToRemoveFromShip(ship, template);
if (toRemove.isNotEmpty) {
// Unmount existing mounts if needed.
for (final mount in toRemove) {
await removeMountAndLog(
api,
db,
caches.agent,
caches.ships,
ship,
mount,
);
}
await dockIfNeeded(api, caches.ships, ship);

if (ship.countUnits(tradeSymbol) < 1) {
try {
// Get it from the delivery ship.
await transferCargoAndLog(
api,
caches.ships,
from: deliveryShip,
to: ship,
tradeSymbol: tradeSymbol,
units: 1,
);
} on ApiException catch (e) {
shipErr(ship, 'Failed to transfer mount: $e');
jobAssert(
false,
'Failed to transfer mount.',
const Duration(minutes: 10),
);
}
}
return JobResult.complete();
}

// 🛸#6 🔧 MOUNT_MINING_LASER_II on ESEIDEL-6 for 3,600c -> 🏦 89,172c
// Mount the new mount.
await installMountAndLog(
api,
db,
caches.agent,
caches.ships,
ship,
toMount,
);
/// Actually change the mounts on the ship.
Future<JobResult> doChangeMounts(
BehaviorState state,
Api api,
Database db,
CentralCommand centralCommand,
Caches caches,
Ship ship, {
DateTime Function() getNow = defaultGetNow,
}) async {
final template = assertNotNull(
centralCommand.templateForShip(ship),
'No template.',
const Duration(hours: 1),
);

// Give the delivery ship our extra mount if we have one.
final extraMounts = ship.mountsInCargo();
if (extraMounts.isNotEmpty) {
// This could send more items than deliveryShip has space for.
for (final cargoItem in extraMounts) {
await transferCargoAndLog(
api,
caches.ships,
from: ship,
to: deliveryShip,
tradeSymbol: cargoItem.tradeSymbol,
units: cargoItem.units,
);
}
// TODO(eseidel): This should only remove mounts if we absolutely need to.
// This could end up removing mounts before we need to.
final toRemove = mountsToRemoveFromShip(ship, template);
if (toRemove.isNotEmpty) {
// Unmount existing mounts if needed.
for (final mount in toRemove) {
await removeMountAndLog(
api,
db,
caches.agent,
caches.ships,
ship,
mount,
);
}

// We're done.
state.isComplete = true;
jobAssert(
false,
'Mounting complete!',
const Duration(hours: 1),
);
return null;
}

final hqSystem = caches.agent.headquartersSymbol.systemSymbol;
final hqWaypoints = await caches.waypoints.waypointsInSystem(hqSystem);
final shipyard = assertNotNull(
hqWaypoints.firstWhereOrNull((w) => w.hasShipyard),
'No shipyard in $hqSystem',
const Duration(days: 1),
// 🛸#6 🔧 MOUNT_MINING_LASER_II on ESEIDEL-6 for 3,600c -> 🏦 89,172c
// Mount the new mount.
await installMountAndLog(
api,
db,
caches.agent,
caches.ships,
ship,
state.mountToAdd!,
);
final shipyardSymbol = shipyard.waypointSymbol;
return JobResult.complete();
}

final available = centralCommand.unclaimedMountsAt(shipyardSymbol);
// If there is a mount ready for us to claim, claim it?
// Decide on the mount and "claim" it by saving it in our state.
final toClaim = assertNotNull(
_pickMountFromAvailable(available, needed),
'No unclaimed mounts at $shipyardSymbol.',
/// Give the delivery ship any extra mounts we have.
Future<JobResult> doGiveExtraMounts(
BehaviorState state,
Api api,
Database db,
CentralCommand centralCommand,
Caches caches,
Ship ship, {
DateTime Function() getNow = defaultGetNow,
}) async {
final tradeSymbol = tradeSymbolForMountSymbol(state.mountToAdd!);
final deliveryShip = assertNotNull(
centralCommand.getDeliveryShip(ship.shipSymbol, tradeSymbol),
'No delivery ship for $tradeSymbol.',
const Duration(minutes: 10),
);
shipInfo(ship, 'Claiming mount: $toClaim.');
state.mountToAdd = toClaim;

// Go to the shipyard.
final waitUntil = beingNewRouteAndLog(
api,
ship,
state,
caches.ships,
caches.systems,
caches.routePlanner,
centralCommand,
shipyardSymbol,
// Give the delivery ship our extra mount if we have one.
final extraMounts = ship.mountsInCargo();
if (extraMounts.isNotEmpty) {
// This could send more items than deliveryShip has space for.
for (final cargoItem in extraMounts) {
await transferCargoAndLog(
api,
caches.ships,
from: ship,
to: deliveryShip,
tradeSymbol: cargoItem.tradeSymbol,
units: cargoItem.units,
);
}
}

// We're done.
state.isComplete = true;
jobAssert(
false,
'Mounting complete!',
const Duration(hours: 1),
);
return waitUntil;
return JobResult.complete();
}

/// Advance the behavior of the given ship.
final advanceMountFromDelivery = const MultiJob('Mount from Delivery', [
doInitJob,
doPickupJob,
doChangeMounts,
doGiveExtraMounts,
]).run;
Loading

0 comments on commit 9a617ff

Please sign in to comment.