Every on-screen element joins a parent-child tree the moment it's constructed, without a single explicit addChild() call in most widget code. But when a click actually happens, that tree is never walked — a completely separate set of flat lists answers the question instead.
Every widget is a CIntObject. Almost none of them call addChild() — and yet every one ends up in exactly the right place in the tree.
A CIntObject constructor checks one flag on the engine, ENGINE->captureChildren, and if it's set, silently attaches itself to whatever object sits at the front of ENGINE->createdObj — a stack of “objects currently under construction.” The OBJECT_CONSTRUCTION macro is what pushes onto that stack: it instantiates a scope-guard, ObjectConstruction, whose constructor does createdObj.push_front(this) and flips captureChildren to true, and whose destructor pops it back off. Write OBJECT_CONSTRUCTION at the top of a window's constructor, then construct a dozen buttons and labels normally with new, and every one of them silently becomes a child of that window — no this->addChild(button) anywhere in sight. It's the retained-mode tree VCMI actually runs on, built with immediate-mode ergonomics.
Once in the tree, four calls cascade from parent to children: activate(), deactivate(), show(), showAll(). Each child gets a per-call permission bitmask, recActions (ACTIVATE / DEACTIVATE / UPDATE / SHOWALL / SHARE_POS), checked before the parent forwards the call — which is what disable()’s own comment warns about: it force-deactivates and zeroes recActions to NO_ACTIONS, so a later enable() restores every flag to ALL_ACTIONS, not just the ones a caller had selectively cleared before disabling — the round trip isn't guaranteed to be symmetric if something upstream had already narrowed the mask.
The parent-child edges exist for lifecycle cascade. Input dispatch reads from a second, unrelated structure entirely.
used bitmask, it calls EventDispatcher::activateElement(), which push_fronts it onto that event's own list (lclickable, hoverable, motioninterested, a dozen more). dispatchMouseLeftButtonPressed() then just walks lclickable front-to-back, calling each candidate's own position-based receiveEvent(), until one matches. Window W is never asked, and Button A's and Button B's place in the tree plays no role at all — the tree answers “who cascades with whom,” the lists answer “who's listening right now,” and they're built and maintained completely independently of each other.GameEngine::mainLoop() is four calls, forever. Nothing about it is more complicated than it looks.
pulls raw SDL events off the queue
under one lock: engineUser->onUpdate(), handleEvents() (turns queued input into the dispatch calls above), windows().simpleRedraw()
the finished frame reaches the actual display
sleeps just enough to hold a constant FPS
windows() — WindowHandler — is its own small stack on top of all this: pushWindow()/popWindows() track which modal is on top, topWindow<T>() is how code asks what that is, and WindowBase::close() throws outright if anything but the current top tries to close itself — only the topmost window is ever allowed to leave first.Everything above exists to serve one object that could, from the server's point of view, just as easily not be a person at all.
Both the human and Nullkiller sit behind the identical root class, CGameInterface — the atlas's Adventure & Battle AI doc already traced AIGateway : CAdventureAI : CGlobalAI : CGameInterface for Nullkiller; CPlayerInterface derives from that same CGameInterface directly, one hop closer to the root. The server calls the same virtual methods either way — hero level-up, tile revealed, a blocking dialog to answer — and has no idea which one it's talking to. Everything this document describes, the widget tree, the event lists, the frame loop, is entirely downstream of CPlayerInterface's implementation of that interface being “show a human something and wait.” Nullkiller implements the exact same calls by deciding on its own, in another thread, without a screen.
The Systems Map's founding claim is that the client reads a local cache of server-authoritative state. Not everything on the client is that.
PlayerLocalState holds which hero is selected, which heroes are marked asleep, each hero's currently plotted path, which spellbook page and school tab were open last — its own header calls it “potentially serializeable state of a local player,” meaning it can be written into that player's own save data so a reload doesn't forget it, but none of it is CGameState, none of it travels in a network pack, and the server has no concept of a “sleeping” hero at all — that's purely a note this client left itself. It's the third category the founding diagram's two arrows don't quite cover: not a write, not a read of the broadcast cache, just memory this particular window into the game keeps for itself.