Brodgar client

Gob sprites: ModSprite, its mods, and variable materials

How a resource-drawn object becomes render-tree nodes: the ModSprite a ResDrawable builds, the Mod pipeline that assembles its parts, which GAttribs take part in it and when it rebuilds, and lib/vmat, the served attribute that dresses a model’s tagged parts in materials the server chose.

The sprite

What Where
Who builds it ResDrawable’s constructor: Sprite.create(owner, res, sdt) — the resource’s own Sprite.Factory (its code layer, Resource.getcode) first, then Sprite.factories in order, else a bare Sprite. ModSprite.fact is the @Resource.PublishedCode.Builtin(name = "mod") factory and answers for any resource with a FastMesh.MeshRes or a RenderLink.Res layer
The gob it belongs to ModSprite.gob = owner.fcontext(Gob.class, false), resolved through the owner’s context chain: a render-linked sub-sprite’s owner is ModSprite.RenderLinks, whose context falls through to the main sprite’s owner, so it finds the same gob; null for an owner with no gob in its chain (an item icon), and then the attribute half below is off
What it draws ModSprite.parts, a RenderTree.Node[] rebuilt by update() and re-added to every slot in ModSprite.slots through RUtils.readd (the previous parts restored on a Loading)
The per-part record ModSprite.Part: obj (the node — a FastMesh.ResourceMesh for a mesh layer, whose info.rdat is the layer’s key/value tags), wraps (a LinkedList<NodeWrap>, the first applied innermost), state (Pipe.Ops composed once), dynstate (per-frame suppliers, a RUtils.StateTickNode), info (metadata). Part.make() composes them in that order
A material on a part Material implements Pipe.Op (so a NodeWrap): apply(Pipe) puts its states and dynstates; apply(RenderTree.Node) wraps the node in the dynamic states unlocked and then the static ones locked — each a Pipe.Op.Wrapping, whose added(slot) does slot.ostate(op) and, when locked, slot.lockstate(). A MeshRes carries its own Material.Res as mat, and ModSprite.Meshes.operate adds each mesh as new Part(mr.m, mr.mat.get()) — the material is the part’s first wrap

The Mod pipeline

What Where
The contract ModSprite.Mod: operate(Cons), order() (default 0), age(), decdata(Message)
Three sources of mods ModSprite.modifiers(Cons): the resource’s (ResData.mods, built once per Resource by every RMod in ModSprite.rmods — the @RMod.Global classes, Meshes.$res, RenderLinks.$res, Animation.$res, Poser.$res — plus the resource’s own RMod code), then the instance’s (imods, what SMods installed at construction), then the gob’s (omods)
The gob’s mods ModSprite.omods(buf, gob): every value of Gob.attr that instanceof Mod. So a GAttrib that implements Mod takes part in the sprite of the gob it sits on, with no registration beyond Gob.setattr
The order Cons.process() pops the lowest order() first, one at a time, and each operate sees the Cons.parts the earlier ones built. Poser is -1000, the mesh and link mods 0, VarMats 100, Animation 1000, Poser.Applier 1010 — so a wrap that must go under the animation and pose morphs goes in between
Adding to a part in flight Cons.parts is the live collection: a later mod mutates part.wraps/part.state/part.dynstate in place, addFirst on wraps making it the innermost
The current build ModSprite.curcons() — a ThreadLocal set for the duration of Cons.process(), so a mod can find the Cons it is being run in from anywhere below it (eqpoint during construction uses it)

When it rebuilds, and by what test

What Where
The trigger ModSprite.tick(dt) compares gob.updateseq against its own lastupd and calls attrupdate() when they differ. Gob.updated() bumps updateseq; OCache.GobInfo.apply calls it once after every batch of deltas and once on removal, MapView.Plob.move when the placement ghost moves
The test attrupdate() re-collects getomods() and calls update() only when Arrays.equals over the Mod[] fails — an identity comparison per element. A GAttrib mutated in place is the same instance and rebuilds nothing; a new instance under the same key is a different array and rebuilds everything. Gob.setattr replaces the instance and dispose()s the old one, which is what a re-sent server attribute does
What is retried tick catches Loading only, leaving lastupd behind so the next tick tries again, and attrupdate restores the previous omods on it. Anything else operate throws leaves tick — into Gob.ctick’s caller on the UI thread
update(Message) the Sprite.CUpd half: decdata (each imods/ResData.mods entry gets a chance to consume the state bytes, else flags = decflags(sdt), the bit mask Meshes/RenderLinks/Animation/Poser gate their layers by id) then update()

lib/vmat: variable materials

Served code (get-code lib/vmat, version 39 at the time of the adoption under src/haven/res/lib/vmat/, published-code.md). A model whose mesh layers carry a vm=<n> tag is drawn in whatever material the server names for slot n, so one cupboard resource is every wood.

What Where
The tag FastMesh.MeshRes.rdat.get("vm") on the ResourceMesh’s info — the slot number as a string; a mesh without one is not variable
The attribute VarMats extends GAttrib implements ModSprite.Mod, abstract on varmat(int id), order() 100. operate(Cons) walks cons.parts, and for every FastMesh.ResourceMesh whose tag names a slot with a material does part.wraps.addFirst(new VarWrap.Applier(mat, mid)) — innermost, under the mesh’s own mat
The server’s dressing AttrMats extends VarMats: mats, a Map<Integer, Material> keyed 0.. in wire order. AttrMats.decode(Resource.Resolver, Message) reads pairs of (resource id, int8 material id) until the message ends, resolving each to Material.Res.get() — a Material.ResMaterial, which carries res (the Resource) and id (the mat2 layer’s) — and numbers them by position, not by the mesh tag. AttrMats.parse(Gob, Message) is the resource’s GAttrib.Parser (>objdelta), reached from OCache.$resattr on an OD_RESATTR delta: it gob.setattrs a fresh AttrMats every time the server sends the attribute
Where it sits in Gob.attr under VarMats.class: Gob.attrclass keys the map on the class directly under GAttrib, so gob.getattr(VarMats.class) finds it and getattr(AttrMats.class) does too (getattr walks up to the same key and then tests isInstance)
The wrap VarWrap extends Pipe.Op.Wrapping, locked, carrying mid; VarWrap.Applier implements NodeWrap mints one per apply. A locked slot refuses every later ostate on it (RenderTree.TreeSlot.lockstate), so nothing above the wrap can change the states the material set
VarSprite extends ModSprite with nothing added — the resource’s >spr entry, so a lib/vmat user is a ModSprite by declaration

Gotchas

See also