Every doc in this atlas has pointed at CGameHandler and moved on — "the hub," "the sole writer," gh.battles, gh.turnOrder. It's the thing that holds every specialist this atlas has already covered. But for one mechanic — a hero taking a single step — there's no specialist to hold. CGameHandler just does it, inline, itself.
class CGameHandler : public Environment, public IGameEventCallback — one object, holding every specialist this atlas has already named.
Its member list reads like a table of contents for this atlas's own third pass: std::unique_ptr<BattleProcessor> battles (Battle System), TurnOrderProcessor> turnOrder and NewTurnProcessor> newTurnProcessor and HeroPoolProcessor> heroPool and PlayerMessageProcessor> playerMessages (The New Turn), QueriesProcessor> queries (Wire Protocol's CQuery machinery, and What's On the Map's VisitQuery), plus GameRandomizer> randomizer (Asking the Game's morale/luck streams) and the live shared_ptr<CGameState> gs itself. Nothing here is new; what's new is seeing them all hang off one object.
CGameHandler::handleReceivedPack() is the real function Wire Protocol's visitMakeAction() example was drawn from — here's the whole thing, not just one branch of it.
throwIfWrongPlayer()/throwIfWrongOwner()/throwIfPlayerNotActive() can be called from inside a visitXxx() at any nesting depth, and every one of them unwinds to the exact same catch in handleReceivedPack(). No individual action method needs its own bail-out path for "wrong player" — the validators just throw, and the one place that started the call already knows how to catch it.Battle goes to battles. A new day goes to newTurnProcessor. A hero taking one step goes nowhere — CGameHandler::moveHero() is the specialist.
ApplyGhNetPackVisitor::visitMoveHero() doesn't call gh.moveHero() once. A MoveHero pack carries a whole path — the full route Asking the Game's CCallback::moveHero() already handed to the pathfinder client-side — and the visitor just loops it, calling gh.moveHero(pack.hid, dest, …) once per tile. Every one of those calls is a single, self-contained step, and CGameHandler re-checks everything about it from scratch, inline, right there in the function:
is the destination guarded, or owned by an enemy who's still off-limits under The New Turn's simultaneous-turn rules (turnOrder->isContactAllowed())
a fresh CPathfinderHelper answers embark/disembark, flight, water-walking, and the tile's real movement cost — The Rules of the Road's engine, called for one hex at a time
pushes its own CHeroMovementQuery onto queries, sends the TryMoveHero result pack, then calls onHeroLeave() on whatever the hero is leaving
visitObjectOnTile() or objectVisited() — called directly, not handed to a processor
blockingVisit() lambda inside the same function catches objects that stop a hero mid-route without finishing the move at all — a resource pile or a quest gate on the way to somewhere else gets visited on the spot, and the rest of pack.path is simply never reached.One aside worth naming: ifsettings["general"]["saveBeforeVisit"]is on and a human hero is about to step next to a guard or an object,moveHero()writes an autosave first —gameInfo().getPlayerState(h->getOwner())->humanandmovementMode == EMovementMode::STANDARDgate it so it only fires for a real player's real step, not a script or a teleport. The single-step design above is exactly what makes that possible: there's always a clean, self-contained moment right before the risky part to save from.