Brodgar client

Weather, tree sway, overlay plumes and plant sprites

Four seams a client-side setting (fork) draws less at: the weather the server composes into the scene (and the light and weather numbers it arrives with), the sway shader every tree and bush carries, the particle plumes standing at a gob’s overlays, and the sprouts a crop or a forageable clump scatters over its tile.

Weather: where it lives

What Where
The live set Glob.wmap, Map<Indir<Resource>, Object> keyed by the weather’s own resource. A value is either the raw Object[] args the "wth" branch of Glob.blob last put there, or — once read — the instantiated Glob.Weather
The type Glob.Weather, an interface: state() (a Pipe.Op, null by default), update(Object...), remove() (true when it is done), tick(double) (true when it ends itself)
Where one is made Glob.Weather.Factory, published code (@Resource.PublishedCode(name = "wtr", …)) on the resource’s Resource.CodeEntry layer. Glob.weather() instantiates a wmap entry the first time it is read, under synchronized(this), and leaves the Weather in place of the raw args from then on
Composing the scene MapView.updweather(), called every MapView.tick: reads glob.weather(), composes every state() into basic(Glob.Weather.class, …), and adds every Weather that is a RenderTree.Node into MapView.rweather (a Map<RenderTree.Node, RenderTree.Slot>). A node no longer in the list is pulled out of rweather and the tree
Simulating it Glob.ctick(), under synchronized(this): ticks every live Weather in wmap, removing (and, if Disposable, disposing) one whose tick(dt) answers true
The numbers the server sent ⚠️ Not kept. Once an entry is a Weather, a later "wth" hands its args to update(Object...) and nothing holds them after; what wmap answers is the published object, whose fields are its own. One "wth" message is the whole set: an entry it does not name is removed. Fork: Glob.wthargs, a volatile Map<Indir<Resource>, Object[]> of the last message’s resources and args in the order sent, replaced whole by each message. The shapes, as the published code reads them: gfx/fx/clouds (v11) a whole-number scale, four percentages and optionally a drift velocity (two numbers); gfx/fx/rain (v2) the intensity, optionally a wind (three numbers) and optionally that wind’s spread (three more); gfx/fx/snow (v2) the intensity
What the rain and the snow draw Served code (get-code fetches it). gfx/fx/rain (v2, Rain) keeps every drop on the CPU: a point of the ground (MCache.getzp) within 75 * 11 units of MapView.getcc() each way, a velocity of the wind plus a random share of its spread each way, a life of 1.5 s, then four splashes of 0.15 to 0.3 s; it refills a LINES buffer of all of them every frame (lib/vertspr’s DynSprite, LineWidth(1), a blue-white Light.PhongLight, vertex colours from half opaque to none, maskdepth, order 20000), and its first tick fills a whole drop’s life at once. gfx/fx/snow (v2, Snow) makes textured quads (the one material of gfx/fx/snow-1, order 5500, alpha-clipped) 350 units over such a point, moves each one on the CPU under random tumbling forces every tick, drops one found under the ground (each flake checked once a second), and refills its whole vertex and index buffers every frame, up to maxflakes, 50,000
The world’s light the "light" branch of Glob.blob (game-clock.md); MapView.amblight() builds the scene’s DirLight from glob.lightamb/lightdif/lightspc/lightelev/lightang, at prio(100); upstream every tick, taking the old one out of the scene and adding the new. Fork: a light the client previews takes that place while one is set, ahead of the server’s; and a new sun is built only when those five values (MapView.sunkey) move, which the server’s do for the two seconds of each turn (Glob.ticklight)

Fork. Both readers ask the same question by the resource’s own name before touching an entry: Glob.weather() skips a withheld one so it never reaches updweather (and so rweather drops its node on the next composition), and Glob.ctick() skips ticking a withheld one without removing it — a rained-off rain still exists in wmap, ready the moment it stops being withheld. Neither loop needs a second pass: the same per-resource decision answers both “should this draw” and “should this simulate”. A weather is withheld by its setting or because the fork draws it another way — the clouds, the rain or the snow, while the client draws its own — and MapView.tick hands the scene to that drawing right after updweather().

Tree sway: where it lives

What Where
The adopted copy haven.res.lib.svaj, @haven.FromResource(name = "lib/svaj", version = 25): GobSvaj (a GAttrib and a Gob.SetupMod) and Svaj (the Pipe.Op/State it hands out)
The one seam that moved for multi-session GobSvaj.st() translates the shader’s origin by io.brodgar.session.Sessions.offsetfor(gob.glob.map) before comparing it to the cached Svaj — multi-session.md has the why
Where a SetupMod feeds the placement Gob.Placed.Placement, rebuilt every tick from every Gob.setupmods entry’s placestate(), composed into the mods field; Placement.equals compares mods along with the transform and tile state
Applying a changed placement Gob.Placed.autotick(dt) builds a fresh Placement and calls Gob.Placed.update(np) when it differs from the current one

Fork. GobSvaj.placestate() answers null while tree effects are off, changing nothing else in the file — st(), carrying the multi-session fix, is untouched. Placement already treats a null mods term as absent and recomposes every tick regardless of any setting, so a changed answer here is already a changed Placement with no further wiring: the next autotick finds the two unequal and calls update.

Overlays and plumes: where it lives

What Where
A served overlay arriving OCache.$overlay (a Delta): builds a Gob.Overlay around an OCache.OlSprite (a Sprite.Mill closing over the resource and state bytes) and calls Gob.addol
Its state bytes changing OlSprite.sdt is not final. $overlay on a known overlay id with different bytes: if the sprite is a Sprite.CUpd, update(...) in place and os.sdt rewritten; otherwise a new Gob.Overlay is addoled before the old one is remove(false)d, so ols briefly holds both. A removal of a Sprite.CDel sprite only calls delete(); it stays in ols until Gob.ctick’s ol.tick(dt) answers done. A Gob.Overlay from Sprite.Mill.of carries its bytes in Sprite.Mill.FromRes.sdt; one built from a bare Sprite has sm == null. The CUpd branch is the one place a member’s bytes change with neither an addol nor a remove, so a reader that must hear every change of bytes hooks it there as well as at those two
The overlay object Gob.Overlay: spr (null until init() runs), slots (null until it stands in a render tree — the “in a tree” test), init() (lazily creates the sprite and adds it to gob.slots), remove0()/add0()
Adding one Gob.addol(Overlay, boolean): ol.init(), ol.add0(), ols.add(ol)
Per-tick upkeep Gob.ctick(): for each Overlay in ols whose slots is still null, retries init(); if it is still null afterwards, the loop continues past ol.tick(dt) unless this session is dormant (the rts: comment there states the contract: an overlay in no render tree is not ticked)

Fork. Gob.withheldplume(Sprite) decides by resource name — the gob’s own Drawable.getres() as the owner, the overlay Sprite’s own res as the plume — and answers through the one holder the panel keeps. Overlay.init()’s last line gates its RUtils.multiadd on it: a withheld plume’s slots never leaves null, so Gob.ctick()’s existing retry-and-skip above costs it nothing and it is shown the moment smoke(true) lets a later init() through — no walk needed for that direction. The withhold direction, for a plume already standing in a tree, is Gob.plumes() (defer(this::syncplumes)) and syncplumes(): under the gob’s own monitor, for every Overlay with slots != null and a withheld name, RUtils.multirem(new ArrayList<>(ol.slots)) then ol.slots = null — leaving the Overlay in ols, so gob:overlay() keeps listing it, GobOverlayAdded already fired for it stands, and the server’s own removal of it still works.

Plant sprites: where it lives

What Where
The factories Served code on the crop and forageable resources, reached as the resource’s Sprite.Factory through Resource.getcode(Sprite.Factory.class, false) (Sprite.create asks it first). The factory instance is made once per resource by Resource.CodeEntry.get from the codeentry layer’s arguments and cached there; create(owner, res, sdt) runs once per gob
Field crops lib/plants, adopted at version 11 under src/haven/res/lib/plants/: GrowingPlant holds num (the count, an argument of the entry) and var, the mesh variants grouped by growth stage (MeshRes.id / 10); create reads the stage from the first byte of the state bytes and scatters num parts over the tile at random offsets from owner.mkrandoom()
Trellis crops TrellisPlant, beside it: the same num and stages, its parts lined along the trellis at 11f / num apart, rotated by the gob’s a
Forageable clumps lib/gplant, adopted at version 1 under src/haven/res/lib/gplant/: GaussianPlant holds numl..numh and a radius r; create draws a count in that range and places each part at a gaussian offset, a variant being a mesh layer or a RenderLink.Res made per owner. A forageable drawn as one mesh does not use it
The part CSprite.addpart(xo, yo, a, mat, node): one render node per sprout at an offset and an explicit angle, all under the one CSprite the factory returns
The drawable ResDrawable: res (the Indir<Resource>), rres (resolved in the constructor), sdt (a package-private MessageBuf of the state bytes) and spr, created by Sprite.create in the constructor and aged. OCache.$cres replaces it through Gob.setattr when the resource or the state bytes change, and updates the sprite in place when only the bytes did and the sprite is a Sprite.CUpd

Fork. The three copies scale the count they draw by the panel’s plant amount — a prefix of the full set, since the loop runs to the scaled count and the random sequence is untouched, so a lower setting keeps every drawn sprout where the full field had it; TrellisPlant re-spaces its prefix along the whole trellis. At the default the loop bound is the count itself: upstream’s own code path. A write of crops or forage is followed by Gob.replant() (defer(this::syncplant)) over every gob of every session: a ResDrawable whose rres.getcode(Sprite.Factory.class, false) is an instance of one of the three is replaced by setattr(new ResDrawable(this, rd.res, rd.sdt.clone())) and updated() — the same path a re-sent OD_RES takes, so the old drawable is disposed and the slots swapped by setattr, and the gob’s id, position, name, state bytes and overlays are untouched. A served factory of another version is not an instance of the local class and is left alone, which is right: the setting never reached it.

Gotchas

What is not mapped

The particle simulation inside a plume’s own sprite, and the render backend’s instancing of weather geometry.

See also