#include "app/visibility_phase.h" #include #include #include #include #include "game/sim/visibility.h" namespace sots::app { namespace { using mars::stream::shapes::SaveGame; using mars::stream::shapes::Sys; std::string fmt(const char* f, ...) { char buf[512]; va_list ap; va_start(ap, f); std::vsnprintf(buf, sizeof buf, f, ap); va_end(ap); return std::string(buf); } sim::SystemMasks MasksOf(const Sys& s) { sim::SystemMasks m; m.seen = s.vFlags; m.explored = s.eFlags; m.active = s.aFlags; m.presence = s.fFlags; m.gate = s.gFlags; return m; } // slot -> player handle id, sized to the highest slot the save uses. A slot no player // occupies maps to 0 and is never observed, because no mask bit can be set for it. std::vector SlotToPlayerId(const SaveGame& game) { int maxSlot = -1; for (const auto& e : game.sim.players) maxSlot = std::max(maxSlot, e.player.plyrIdx); if (maxSlot < 0) return {}; std::vector out(static_cast(maxSlot) + 1, 0); for (const auto& e : game.sim.players) { if (!sim::SlotRepresentable(e.player.plyrIdx)) continue; out[static_cast(e.player.plyrIdx)] = e.playerID; } return out; } } // namespace std::int32_t EncounterAtSystem(const SaveGame& game, std::int32_t systemId) { for (const auto& f : game.sim.fleets) { if (f.flt.locID != systemId) continue; if (f.flt.ftEnc == 0) continue; // "no encounter"; the system defence fleet uses it return f.flt.ftEnc; } return sim::kNoEncounter; } VisibilityPhaseResult RunSystemObservedStamp(SaveGame& game) { VisibilityPhaseResult r; int watched = 0; for (auto& e : game.sim.systems) { ++r.systemsVisited; if (e.sys.aFlags != 0) ++watched; const sim::SystemStampResult s = sim::UpdateSystemStamp(e.sys.ltis, e.sys.aFlags, game.sim.frame); if (!s.changed) continue; e.sys.ltis = s.stamp; ++r.systemsTouched; ++r.leafWrites; } r.notes.push_back(fmt("%d of %d system(s) are watched by someone; %d stamp(s) moved to %d", watched, r.systemsVisited, r.leafWrites, game.sim.frame)); if (watched != r.systemsVisited) r.notes.push_back("an unwatched system keeps the turn it was last watched -- the gate " "is the ACTIVE mask, which falls when the last fleet leaves, not the " "sticky one"); return r; } VisibilityPhaseResult RunExploredSweep(SaveGame& game) { VisibilityPhaseResult r; int newlyExplored = 0; for (auto& e : game.sim.systems) { ++r.systemsVisited; const std::int32_t next = sim::ApplyExploredSweep(e.sys.eFlags, e.sys.aFlags); if (next == e.sys.eFlags) continue; newlyExplored += static_cast( sim::NewlyExploredSlots(e.sys.eFlags, e.sys.aFlags).size()); e.sys.eFlags = next; ++r.systemsTouched; ++r.leafWrites; } r.notes.push_back(fmt("%d system(s) newly surveyed, across %d (system, player) pair(s)", r.systemsTouched, newlyExplored)); if (newlyExplored) r.notes.push_back(fmt("the original posts one event per pair, so this phase owes %d " "event(s) it does not post", newlyExplored)); return r; } VisibilityPhaseResult RunObservationRecords(SaveGame& game) { VisibilityPhaseResult r; const std::vector slotToId = SlotToPlayerId(game); int created = 0, refreshed = 0, derivedEncounters = 0; for (auto& e : game.sim.systems) { ++r.systemsVisited; std::vector before; before.reserve(e.sys.nve.size()); for (const auto& n : e.sys.nve) { sim::Observation o; // The wire carries the handle id; the map key is the slot. Recover the slot so // the ordering the container imposes is reproducible. o.playerId = n.ePid; o.playerSlot = -1; for (std::size_t slot = 0; slot < slotToId.size(); ++slot) if (slotToId[slot] == n.ePid) o.playerSlot = static_cast(slot); o.turnSeen = n.ets; o.encounterId = n.eid; before.push_back(o); } const std::int32_t enc = EncounterAtSystem(game, e.sysID); if (enc != sim::kNoEncounter) ++derivedEncounters; const sim::ObservationUpdate up = sim::UpdateObservations( before, MasksOf(e.sys), slotToId, enc, game.sim.frame); created += up.created; refreshed += up.refreshed; // Count the leaves that actually move. A created record adds four (the count and its // three fields); a refreshed one moves only the fields that changed. int moved = 0; if (up.records.size() != e.sys.nve.size()) ++moved; // the count leaf for (std::size_t i = 0; i < up.records.size(); ++i) { const sim::Observation& o = up.records[i]; if (i >= e.sys.nve.size()) { moved += 3; continue; } const auto& old = e.sys.nve[i]; if (old.ePid != o.playerId) ++moved; if (old.ets != o.turnSeen) ++moved; if (old.eid != o.encounterId) ++moved; } if (moved == 0) continue; e.sys.nve.clear(); e.sys.nve.reserve(up.records.size()); for (const auto& o : up.records) { mars::stream::shapes::NveEntry n; n.ePid = o.playerId; n.ets = o.turnSeen; n.eid = o.encounterId; e.sys.nve.push_back(n); } ++r.systemsTouched; r.leafWrites += moved; } r.notes.push_back(fmt("%d record(s) created, %d refreshed, over %d system(s); %d leaf/leaves", created, refreshed, r.systemsTouched, r.leafWrites)); r.notes.push_back(fmt("%d system(s) carry an encounter fleet whose id the record copies -- " "the field the original reads is NOT on the wire, so this derivation " "is a HYPOTHESIS and no save in the corpus can separate it from the " "rule it stands in for", derivedEncounters)); return r; } } // namespace sots::app