The Apply Layer, The New Turn, and Bytes and Types all told the write side of this atlas — how a pack is built, crosses the wire, and lands. This is the other half: the four interfaces AGENTS.md names as how AI and the client ever find out anything at all, entirely apart from that machinery.
Nothing here is one class. It's an inheritance chain, each layer answering a narrower question than the one below it.
IGameInfoCallback is the abstract query surface — roughly forty pure-virtual methods covering tiles, objects, players, teams, teleports. MapInfoCallback fills in the parts that only need a CMap*. CGameInfoCallback is where visibility actually gets enforced: hasAccess(playerId) returns true if the caller has no player (server context, or a spectator) or isn't at war with the player being asked about — canGetFullInfo(obj) is just that check applied to an object's owner. CPlayerSpecificInfoCallback adds no new enforcement, only convenience: the same queries with the PlayerColor argument dropped, implicitly meaning “me.” CNonConstInfoCallback is a sibling of that, not a further layer — the same lookups as non-const pointers, for the one caller (the server itself) that's allowed to look something up and then change it in the same breath.Wire Protocol named the pack types a client sends — MoveHero, EndTurn — without showing where they come from. Here's where.
CCallback is the concrete class both the human client and every AI actually hold: class CCallback final : public CPlayerSpecificInfoCallback, public CBattleCallback, public IGameActionCallback. The first base is everything above; the third, IGameActionCallback, is a completely separate interface family — not a query, a command. Its methods read like a verb list (moveHero, buildBuilding, recruitHero, swapArtifacts…) and every implementation is the same three lines: construct the matching pack, call sendRequest(), return.
moveHero()builds & sendsMoveHero pack(path, layer, h->id, transit); sendRequest(pack); — nothing moreendTurn()builds & sendsEndTurn pack; sendRequest(pack); — nothing moreswapCreatures()builds & sendsArrangeStacks pack(1, p1, p2, s1->id, s2->id, 0); sendRequest(pack);sendRequest() itself, declared on CBattleCallback and inherited from there, does nothing clever — it forwards to IClient::sendRequest(const CPackForServer &, PlayerColor, bool waitTillRealize), the one pure-virtual method that actually owns the network connection this atlas's Wire Protocol doc already traced. Every command on CCallback is a translator: a typed C++ method call in, a CPackForServer out.
A reader who's already met CBattleInfoCallback in this atlas's Battle System doc will collide with a similarly-named class here. They're not the same thing, and the difference is the point.
lib/battle — already covered
The 81KB query engine Battle System already traced — hex reachability, unit state, damage estimation. Pure reads, no notion of sending anything.
lib/battle — a thin wrapper, new to this doc
class CPlayerBattleCallback : public CBattleInfoCallback — scopes the query engine above to one player's view of one active battle. This is what an AI or the client actually holds per fight.
lib/callback — this document, a different job entirely
Not a state reader at all. Keeps a map<BattleID, shared_ptr<CPlayerBattleCallback>> of active fights and sends the action packs — battleMakeUnitAction(), battleMakeSpellAction() — that Battle System's MakeAction swimlane already traced from the receiving end.
The naming lines up once the split is visible: CBattleInfoCallback answers questions about a battle; CPlayerBattleCallback is that answer engine scoped to one viewer; CBattleCallback is the part that acts, and it hands out the second class as the read half of what CCallback exposes for battle. Same word, three jobs, two directories.
This atlas has already met two kinds of scoped randomness — Adventure & Battle AI's RNGStub, which always returns the midpoint inside a hypothetical battle, and The New Turn's one CRandomGenerator per player for the tavern pool. GameRandomizer is neither — it's the real, live RNG the server rolls against, and part of it remembers how unlucky you've been.
Separate seeded streams exist per purpose — heroSkillSeed per hero type, and four more maps keyed by ObjectInstanceID for goodMoraleSeed, badMoraleSeed, goodLuckSeed, badLuckSeed — so a bad luck roll for one stack can never perturb another stack's morale, or anyone else's luck, in the same battle. All of it is serialized (h & goodMoraleSeed, and the rest) as part of CGameState itself, per this atlas's Bytes and Types doc's own framework.
The morale and luck streams aren't plain RNGs. They'reRandomGeneratorWithBias, and its own header comment states the goal outright: “simulate human expectations of random distributions and reduce frustration from ‘bad’ rolls.” Each roll accumulates a bias (RandomizationBias::accumulatedBias) that nudges the next one — at bias 100 a success is guaranteed within100/chancetries no matter how the dice have gone before, while the statistical probability over a large number of rolls stays exactly what the configured chance says. A hero with terrible luck this battle is measurably more likely to get good luck next round — not by cheating the odds, but by remembering that the odds already failed once.