⚓ Seaworth Games

server/CGameHandler

Where Requests Land

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.

The shape of the hub

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.

Where a request actually lands

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.

CGameHandler::handleReceivedPack() PackageReceived, echoed immediately isBlockedByQueries(&pack, pack.player)? yes → PackageApplied(false), stop here no ApplyGhNetPackVisitor applier(*this); pack.visit(applier); the double-dispatch this atlas's Wire Protocol doc already traced one visitXxx() per pack type, in server/NetPacksServer.cpp visitEndTurn throwIfWrongPlayer, throwIfPlayerNotActive visitDismissHero throwIfWrongOwner, throwIfPlayerNotActive visitMoveHero throwIfWrongOwner, throwIfPlayerNotActive each check throws ExceptionNotAllowedAction; each success calls the matching gh.*() method catch (ExceptionNotAllowedAction&) → result = false PackageApplied, either way
Rejection is an exception, not a bool, on purpose: 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.

What it doesn't delegate

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:

guards & contact

is the destination guarded, or owned by an enemy who's still off-limits under The New Turn's simultaneous-turn rules (turnOrder->isContactAllowed())

→

layer & cost

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

→

doMove()

pushes its own CHeroMovementQuery onto queries, sends the TryMoveHero result pack, then calls onHeroLeave() on whatever the hero is leaving

→

the landing tile

visitObjectOnTile() or objectVisited() — called directly, not handed to a processor

A separate 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: if settings["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())->human and movementMode == EMovementMode::STANDARD gate 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.