⚓ Seaworth Games

client/mapView

One Renderer, Many Contexts

This atlas's Widget Tree doc covered the framework a map view lives inside. This is what's inside it: one renderer that never knows whether it's drawing a normal frame, a hero mid-step, a fading object, or the puzzle screen — because it never asks. It asks a context object instead, and something else decides which context is listening.

A view in the tree

MapView is an ordinary widget first. Everything in this doc happens underneath that.

class MapView : public BasicMapView, and class BasicMapView : public CIntObject — confirmed directly in MapView.h. It's built, activated, and shown the exact way Widget Tree already traced for any other widget; a second subclass, PuzzleMapView, reuses the identical base for the downscaled obelisk-puzzle screen. Three collaborators sit behind that one widget: MapViewModel is nothing but camera state (tile size, view center, viewport dimensions, current level — converting between map coordinates and screen pixels); MapViewCache holds the composited tile surfaces so a static frame doesn't get redrawn from scratch; and MapViewController is where the actual behavior lives.

One renderer, many contexts

MapRenderer composites a tile from up to eight named layers — terrain, river, road, border, fog, objects, overlay, path — and every one of those layer renderers takes the same single argument to find out what to draw: an IMapRendererContext&.

MapRenderer · 8 layer sub-renderers terrain, river, road, border, fog, objects, overlay, path every renderTile() takes IMapRendererContext& IMapRendererContext isVisible(), objectImageOffset(), objectTransparency(), objectImageIndex(), overlayText(), viewTransitionProgress()… one implementation is context — swapped, not branched on AdventureContext the ordinary frame MovingContext a hero mid-step FadingContext object appearing / vanishing TransitionContext teleport WorldViewContext downscaled overview SpellViewContext View Earth / View Air PuzzleMapContext obelisk puzzle screen MapViewController::activateAdventureContext() / activateWorldViewContext() / activateSpellViewContext() / activatePuzzleMapContext() swap which context the renderer sees. The renderer's own code never branches on which one is active.
Seven concrete IMapRendererContext implementations exist; exactly one is active at a time, held by MapViewController in a plain context pointer. Asking for an object's transparency means one thing under FadingContext and another under AdventureContext, but MapRenderer never has an if for it — it just calls context.objectTransparency(id, coordinates) and draws whatever comes back. The same shape this atlas keeps finding under different names: swap the object answering the questions, not the code asking them.

A blocking animation

A hero's single step, the one thing Where Requests Land found CGameHandler handling inline, lands here as MapViewController::onHeroMoved() — and it doesn't just start an animation. It blocks on one.

swap in MovingContext

movementContext = make_shared<MapRendererAdventureMovingContext>(*state), tracking tileFrom/tileDest/progress

→

animationWait.setBusy()

a ConditionalWait, flipped busy the instant the animation starts

→

hasOngoingAnimations()

the calling thread checks this and, if busy, calls waitForOngoingAnimations() — a real block, not a poll

→

tick() advances progress

each frame nudges progress until the step completes, then the wait is released

Movement time itself is asymmetric by design: settings["adventure"]["heroMoveTime"] for the player's own hero, "enemyMoveTime" for anyone else's — and if either is configured at or below one, the step is instant, no context swap, no wait, straight to addObject() at the new tile.

This is the same instinct Wire Protocol found in battle — BattleStacksController starting an attack animation against a stack's pre-hit HP, so damage and motion don't land in the same instant — except here the wait is explicit and named. Whatever thread is driving pack processing genuinely pauses at waitForOngoingAnimations() until the walk finishes on screen; the next pack in the queue simply doesn't get processed until this one has visibly happened.

What the renderer doesn't own

Fog of war and "whose hero is this" look like rendering concerns. Neither is decided here.

MapRendererBaseContext::isVisible(coordinates) — the method every layer renderer calls before drawing anything on a tile — is one line: GAME->interface()->cb->isVisible(coordinates). That's Asking the Game's read-side callback, itself just a local cache kept in sync by the FoWChange packs Wire Protocol already catalogued. The renderer doesn't track fog; it asks. isActiveHero() is the same shape one level down — it checks GAME->interface()->localState->getCurrentHero(), the client-only, never-broadcast memory Widget Tree's PlayerLocalState finding already named. Both questions a tile-drawing routine needs answered were already answered elsewhere in this atlas; nothing in mapView/ re-derives them.

One older piece sits underneath all of this: CMapHandler (lowercase file, an earlier naming generation than its PascalCase siblings) does nothing but fan a hero-move or object-visibility event out to a list of IMapObjectObserver* — MapViewController is one such observer, registered once and left to react. It's a plain broadcaster, not a second copy of any logic covered above.