City: track crashed vehicles in a set, delay and gate AI rescue dispatch (#1694) - #1695
andremmfaria wants to merge 2 commits into
Conversation
…mechanics Organisation::updateMissions ran an orgs x vehicles x rescuers rescue scan on every GameState::update even when nothing had crashed, costing tens of milliseconds on an idle city. GameState now keeps a set of crashed vehicle ids, maintained where the crashed flag changes and rebuilt from the flags in initState so saves are unaffected. The dispatch moves out of updateMissions into Organisation::dispatchRescueCraft, runs only when the set holds a recoverable non-alien wreck, iterates the set instead of the fleet, and shares one claimed-victim set across organisations. Stale entries are dropped on lookup. The dispatch is gated behind a new OpenApoc.NewFeature.RescueCrashedVehicles option (default off). A second new option, CrashingDamagedVehicles, gates whether non-UFO vehicles crash land when damaged below their crash threshold. CrashingGroundVehicles, CrashingDimensionGate and CrashingOutOfFuel now default to off as well, matching vanilla behaviour by default. Fixes OpenApoc#1694
…ds per wreck Each vehicle draws a rescue availability tick when it becomes crashed. The update gate and both dispatch target loops ignore a wreck until that tick, so emergency services do not launch within seconds of a crash and the dispatch scan stays idle for the first minutes after one. Player initiated recovery is unchanged. The tick is not saved, so a wreck loaded from a save is eligible at once. The recovery test zeroes the tick so it keeps measuring throughput. Refs OpenApoc#1694
|
Temporary harness used for the before/after check (not committed). Built as a standalone target next to // Timing: 200 x update(1) on the populated idle city, four legs.
// on_wreck parks one crashed non-alien vehicle inside a building (stays crashed and alive for the
// whole run, one rescuer claims it, every other rescuer keeps looking), on_wreck_waiting is the
// same wreck still inside its 60..600 s dispatch delay.
struct TimingLeg { UString label; bool rescueOption; bool withWreck; bool waitingWreck; };
std::vector<TimingLeg> timingLegs = {
{"off", false, false, false}, {"on", true, false, false},
{"on_wreck", true, true, false}, {"on_wreck_waiting", true, true, true}};
for (auto &leg : timingLegs)
{
config().set("OpenApoc.NewFeature.RescueCrashedVehicles", leg.rescueOption);
auto state = loadTimingState(commonPath, gamestatePath); // loadGame x2, startGame, initState,
// fillPlayerStartingProperty, fillOrgStartingProperty
if (leg.withWreck)
{
auto wreck = state->current_city->placeVehicle(*state, craftType, org, homeBuilding);
wreck->setCrashed(*state);
wreck->rescueAvailableTick =
leg.waitingWreck ? state->gameTime.getTicks() + 600 * TICKS_PER_SECOND : 0;
}
auto start = std::chrono::steady_clock::now();
for (int i = 0; i < iterations; i++)
state->update(1);
auto end = std::chrono::steady_clock::now();
std::cout << "rescue_option=" << leg.label << "\n"
<< "mean_update_ms=" << std::chrono::duration<double, std::milli>(end - start).count() / iterations << "\n"
<< "vehicle_count=" << state->vehicles.size() << "\n"
<< "idle_rescue_craft_count=" << countIdleRescueCraft(state) << "\n";
// wreck legs also print wreck_crashed / wreck_carried / wreck_dead / recover_missions / wreck_eligible_in_s
}
// Gate: one idle rescuer + one crashed victim of a non-player, non-alien org, co-located on an
// open sky tile so dispatch does not depend on flight time. countRecoverVehicleMissions() counts
// RecoverVehicle missions across state->vehicles.
auto off = loadGateState(commonPath, gamestatePath); // loadTimingState + vehicles.clear() + agents.clear()
spawnRescueScenario(off); // placeVehicle rescuer, placeVehicle victim, victim->setCrashed
gGateVictim->rescueAvailableTick = 0; // this leg is about the option, not the delay
config().set("OpenApoc.NewFeature.RescueCrashedVehicles", false);
for (int t = 0; t < 50; t++) off->update(1);
std::cout << "dispatched_off=" << countRecoverVehicleMissions(off) << "\n";
auto on = loadGateState(commonPath, gamestatePath);
spawnRescueScenario(on);
config().set("OpenApoc.NewFeature.RescueCrashedVehicles", true);
int64_t delaySeconds = (gGateVictim->rescueAvailableTick - on->gameTime.getTicks()) / TICKS_PER_SECOND;
std::cout << "delay_s=" << delaySeconds << "\n"; // FAIL unless 60 <= delay_s <= 600
for (int t = 0; t < 50; t++) on->update(1);
std::cout << "dispatched_before_delay=" << countRecoverVehicleMissions(on) << "\n"; // expect 0
while (on->gameTime.getTicks() < gGateVictim->rescueAvailableTick)
on->update(TICKS_PER_SECOND);
for (int t = 0; t < 5; t++) on->update(1);
std::cout << "dispatched_after_delay=" << countRecoverVehicleMissions(on) << "\n"; // expect 1
// Sync (this branch only): crashedVehicles must follow every Vehicle::crashed write site.
auto sync = loadGateState(commonPath, gamestatePath);
auto v = sync->current_city->placeVehicle(*sync, craftType, org, spawnPos, 0.0f);
v->homeBuilding = homeBuilding;
v->setCrashed(*sync, true);
std::cout << "sync_after_crash=" << sync->crashedVehicles.size() << "\n"; // expect 1
v->enterBuilding(*sync, homeBuilding);
std::cout << "sync_after_deliver=" << sync->crashedVehicles.size() << "\n"; // expect 0
v->setCrashed(*sync, true);
v->die(*sync, true);
sync->update(1);
std::cout << "sync_after_die=" << sync->crashedVehicles.size() << "\n"; // expect 0Output with master The pre fix code has no option and no delay, so it dispatches at once in every gate leg ( Output on this branch 4 core box, RelWithDebInfo, FPS, crashed fleet scenario. New Medium campaign under Xvfb 640x480 with llvmpipe, Speed 3 for 15 s so traffic is airborne, back to Speed 1, then the debug hotkey End frames, master then this branch: Each campaign starts at a random base, so the rendered scene and the number of airborne vehicles at the crash differ per run; the harness table is the controlled comparison, the recording is illustration. |
Fixes #1694.
Thanks to @JonnyH for measuring this and pointing it out in the review comment on db04a32: the rescue dispatch added in #1684 turned every
GameState::updateinto an orgs x vehicles x rescuers scan, about 24 ms per update on an idle city on a 5800X3D, whether or not anything had crashed.Root cause
Organisation::updateMissions(called once per organisation per update) collected every idle rescue capable craft, rebuilt the set of already claimed victims from every vehicle's mission list, and then walkedstate.vehiclesup to twice per idle rescuer callingVehicleMission::canRecoverVehicleon each. On a populated city that is hundreds of idle rescuers times hundreds of vehicles times 28 organisations, with a string id compare and a relation lookup per candidate, and it ran even when no vehicle in the city was crashed.perfon the pre fix harness binary puts the time inOrganisation::updateMissions,_Rb_tree_increment,memcmpandOrganisation::getRelationTo, which is that allied rescue loop.Fix
The strategy follows JonnyH's suggestion on Discord: instead of scanning the fleet for wrecks, keep a list of crashed vehicles fed by the state transition, and put the non vanilla mechanics behind
NewFeatureoptions that default to off.GameState::crashedVehiclesis astd::set<UString>of vehicle ids maintained at every site that flipsVehicle::crashed:Vehicle::crash,Vehicle::setCrashed,enterBuilding,enterDimensionGate,dropCarriedVehicle, plus the death note prune incleanUpDeathNote. It is not serialized:initState()rebuilds it from the loaded vehicles, so saves are untouched. Every reader self heals a stale entry (id no longer instate.vehicles, or the flag already false) by erasing it.GameState::updatewalks that set once per update, shared by all organisations. If no eligible non alien wreck exists, nothing else runs. If one does, the claimed victim set is built once and passed to the newOrganisation::dispatchRescueCraft, which iteratescrashedVehiclesinstead ofstate.vehiclesfor both the owned and the allied target loops. The rescue dispatch code itself is the City: dispatch every idle rescue craft per organisation update (#1008) #1684 logic, just moved out ofupdateMissionsand pointed at the set.state.rng. The update gate and both dispatch loops ignore a wreck until that tick, so emergency services do not launch within seconds of a crash and the dispatch pass stays idle for the first minutes after one. Player initiated recovery through the UI is not delayed. The tick is not saved, so a wreck loaded from a save is eligible at once.OpenApoc.NewFeature.RescueCrashedVehicles(organisations dispatch rescue craft) andOpenApoc.NewFeature.CrashingDamagedVehicles(damaged vehicles crash instead of flying on). The existingCrashingGroundVehicles,CrashingDimensionGateandCrashingOutOfFuelare flipped from default on to default off. UFO crashes are unchanged. Both new options are listed in the More Options screen.An earlier variant of this branch kept the fleet scan but gated it behind a "is anything crashed" check. It measured the same on an idle city but fell straight back to the full 25 to 30 ms per update for as long as a single wreck existed in the city, so it was dropped in favour of the crashed vehicle list.
Verification
ctest15/15 on this branch.tests/test_crashed_vehicle_recovery.cppforces the rescue option on and zeroes the per wreck delay on its victims so it keeps comparing turbo against normal cadence recovery throughput rather than measuring the delay.Harness (posted as a comment below, not committed): same populated idle city, 798 vehicles, 28 organisations, 326 idle rescue capable craft, 200 x
update(1), ms per update.Master has neither the option nor the delay, so its last column is simply another full scan. The gate check on this branch dispatches nothing with the option off, nothing before the wreck's delay elapses and exactly one recover mission after it. The set is checked against every crashed flag write site (crash, deliver, die) in the same harness.
FPS recording in the same comment: a new campaign at Speed 1 after the debug hotkey crashes the whole airborne fleet, master versus this branch.