Per-Detail Material overrides — flat map from detailKey to the
Material's templatePath. Stored as a plain Record (not a
Map) so default JSON serialization handles it without a
marshaller.
Path to the bulk default Material singleton. Resolved lazily on
each getMaterial() call so HMR replacement is observed
immediately.
Protected_Runtime-only auxiliary support pointer — Pattern B live ref.
Holds a direct reference to the supporting Surfaced host
(null when no support). NOT in persistentFields; resets to
null on hydrate. R2.3 self-heal in getRestingOn clears the
slot if the supporter has been destructed.
Pattern B chosen over Pattern A templatePath stamping because non-singleton surfaces (e.g., multiple identical tables in a dining hall) can't be addressed unambiguously by templatePath. The cross-restart loss is small — items reappear in their container, just without the on-surface precision.
ProtectedcontentsThe contained items. Read access goes through getContents();
mutation goes through addContainable / removeContainable,
which only Containable.setContainer may legitimately invoke.
ProtectedenvironmentLive reference to the container. NOT a persistent field —
cross-Stuff references would round-trip badly through the
Hydrator's reflection. The container relationship is rebuilt
at clone time via the applyContainer instruction-field path
(see static instructionFields above) or by direct
ContainmentApi.move calls after hydration.
Auxiliary restingOn is different — it's a Pattern A
path-string (_restingOnPath) that DOES persist; see static
persistentFields below.
Short locomotion-mode name (or path) the cart's passage needs —
wheeled (ground medium) by default. The terrain gate asks
LocomotionApi.exitAllowsMode(exit, this); wheeled is admitted
by any media: ['ground'] exit and refused by vertical/water/air.
LocomotionApi.modeOf accepts either name or path form.
ReadonlystuffRuntime ID for this object (generated using nanoid). This is NOT the MongoDB _id - it's a runtime identifier.
Static_StaticcommandCommand provider for inventory management commands
StaticfieldField-marshaller bindings for the five Quantity-typed bulk
fields. The per-detail maps round-trip via standard JSON
(Quantity.toJSON / fromJSON) — Record<string, Quantity<U>>
serializes natively without a map marshaller.
StaticinstructionInstruction field — declarative spawn target. Consumed by
Phase 2 of the Hydrator. There is NO paired getContainer(path)
declaration accessor; the live getContainer() ref is the
only runtime getter.
StaticpersistentStaticsubscribableLive-query subscribable fields.
bulkMaterial — flat. The bulk default Material as a
MaterialSummary wire shape, or null when no bulk
material is set. dependsOnFields defaults to
['bulkMaterial'] (descriptor name = source field name).mass — flat. Quantity in kg as { value, unit: 'kg' }.
dependsOnFields defaults to ['mass'].detailMaterial — focused-detail only (no flat read).
Descriptor name (detailMaterial) does NOT match the
setter's discriminator (detailMaterials, plural), so we
declare dependsOnFields explicitly.Shadow lifecycle entries cover future material shadows that change the rendered value without firing a field change.
ProtecteddraftProtectedhandednessProtectedmassProtectedtransmissionAccessor pair owns the per-field invariant (the project rule);
setTransmissionFactor delegates here so the Hydrator's Phase-1
dispatch and in-process callers share one validation point.
Privileged setter for the auxiliary restingOn pointer.
Reachable only from ContainmentApi.move /
ContainmentApi.placeOn. Pass null to clear.
Stores the supporting Surfaced ref directly (Pattern B); runtime-only — see the field declaration's JSDoc for the persistence rationale.
State-mutation primitive. Locked down — only callable from
Containable.setContainer. Use ContainmentApi.move(item, container) from application code.
Fires FieldChangedEvent { field: 'contents' } after a real
addition so the MQL subscription substrate's dependency index
picks up containment-shape changes for the contents descriptor.
The substrate matches on (KIND, 'field', 'contents') only —
oldValue / newValue are inspected by the diff pass via
re-projection of the host, not by the index, so the count
delta carried here is informational (debugging / future
coarse-grain optimizations) rather than load-bearing.
Phase 2 applier — see the interface docstring for semantics.
Compare-and-move idempotency: no-op when the current container's
templatePath matches the declared path; otherwise resolve the
target via StuffApi.singleton and ContainmentApi.move into
it. The singleton-target invariant is enforced at template-save
time by TemplateApi.validateSingletonContainerTarget.
Destroy this object.
Locked down by @CallSecurity(ApiOnly) — only callers under
mud/api/ (in practice, StuffApi.destruct) may invoke it.
@Unshadowable because the unregistration path must always run;
a shadow that intercepts and skips it would leak the object into
the registry forever. @Final because subclass overrides would
defeat the same invariant — the loader hook throws
FinalViolationError at import time on any subclass redefinition.
Subclass cleanup belongs on the optional onDestruct() witness
(consulted by StuffApi.destruct while the target is still live);
refusal logic belongs on canDestruct(). This terminal destroy()
is the unshadowable mark-and-unregister step only.
OptionaldetailKey: stringResolve the host's authored biome, or null when no override.
Floor-to-ceiling vertical extent, in m. CartesianLocation
returns cellSize; SphericalLocation returns the inscribed
cube's side (2r/√3). Default is null.
Snapshot of contained items as an array.
Walk the containment tree depth-first pre-order, starting
from this Container's immediate contents, and return every
Containable encountered. Used by MQL's :I transform and
any controller that wants the full nested inventory.
OptionaldetailKey: stringOptionaldetailKey: stringRead the recency timestamp. Read by the residency sweep — which
calls it on the raw target (via RAW_TARGET) so the sweep's own
introspection never counts as a touch.
OptionaldetailKey: stringLocomotion mode the cart's passage needs (default wheeled).
Self-presentation — the casual-register render string for this object, the answer to "what does this Stuff call itself?" Three- step resolution:
Named.name if present and non-empty — the object's
proper name ("Alice", "Excalibur", "Town Square").Visible.shortDescription if present and non-empty —
the object's visual identity ("a heavy oak door").For a Globbable stack (quantity !== 1) the count folds in as
an affix — "30 coins" — pluralized via GrammarApi.pluralize
(which honors host-side getPluralForm() overrides for
irregulars). Named takes precedence over Visible so a
Named-with-description renders by its proper name; code that
needs the formal register calls getFullName() when typed as
Named.
Viewer-blind by design. This is the shared baseline every
Stuff exposes; the viewer-aware naming step (recognition /
identification — see
docs/subsystems/belief.md) composes on top
of it. Left shadowable (NOT @Final) so masking / disguise
effects can override the rendered identity via a method shadow.
Build the composable Mml fragment for this object's display name —
the Mml sibling of getPresentation. Mml.ref (and so every
<item> / <name> / … identity tag) renders this, not a raw
string, so a name joins the compose chain as a fragment like
everything else. The label is the already-resolved, viewer-aware
name (recognition runs in the render layer and hands it in).
Return null for the plain default — Mml.ref then wraps the label
in Mml.text, which escapes it exactly once, so player-authored
names / status decoration are safe by construction and the fragment
is never re-escaped downstream. Override to build a richer fragment
(a TPA terminal wraps its name in <color> to tint by state). The
plain-string getPresentation stays the surface for non-prose
consumers (logs, context.note, MQL scalars).
OptionaldetailKey: stringResolve the auxiliary restingOn pointer. Pattern B live ref;
R2.3 self-heal clears the slot if the supporting surface has
been destructed since the last set.
Returns null when no support OR the supporter has been
destructed. The caller can't tell the two apart from the
return value; that's deliberate — absence of support is the
same observable as a stale ref.
Walk the container chain to the topmost non-null environment.
Returns null when already at the root.
Containment is acyclic by construction (a Container can't
contain its own ancestor — setContainer's atomic update is
the chokepoint), so the loop is bounded by the depth of the
world's nesting.
Resolve the temperature at this scope (optionally narrowed by a
detailKey). Routes through BiomeApi.resolveTemperatureFor,
which walks innermost-container-outward then biome / zone /
universe.
OptionaldetailKey: stringRead seam. Instance method, but unwraps via ProxyApi.unwrap
before reaching the # slot — this inside an instance method
called through the proxy is the proxy, and the # slot lives
on the raw target.
Atmospheric-bearing volume of this scope, in m³. Concrete
subclasses derive from their topology (CartesianLocation from
cube cellSize³, SphericalLocation from (4/3)πr³). The
default is null — a scope with no derivable volume.
OptionaldetailKey: stringGet the spatial zone. Unwraps via RAW_TARGET because the
# slot lives on the raw target only and this inside an
instance method called through the proxy is the proxy.
Check if this object has been destroyed.
@Unshadowable: the destroyed-state read is a framework invariant —
any shadow that lied about it would let consumers touch a torn-down
Stuff. @Final: subclasses overriding this would defeat the same
invariant; the loader hook throws FinalViolationError at import
time on any subclass that redefines it.
Remove primitive. Same lockdown as addContainable. Fires the
matching FieldChangedEvent { field: 'contents' } on a real
removal.
OptionaldetailKey: stringState-mutation chokepoint. Reachable only from
ContainmentApi.move; cross-Container contents mutation must
not be subclass-extensible (@Final) or shadow-bypassable
(@Unshadowable).
Atomic across three updates: detach from the old container,
attach to the new, update the field. null argument is the
detach case; the policy rejects calls from anywhere other than
ContainmentApi, so setContainer(null) outside the Api
throws — the legitimate detach is ContainmentApi.move(item, null).
OptionaldetailKey: stringOptionaldetailKey: stringStrict on Quantity<'kg'>. Callers holding a raw number wrap
via Quantity.of(n, 'kg') at the call site; tag / alt-unit
authoring is the marshaller's job, not a runtime API concern.
OptionaldetailKey: stringOptionaldetailKey: stringOverride the temperature at the room/vessel scope or at a
specific detailKey. null clears the override (the next read
falls through to the chain).
OptionaldetailKey: stringStamp this Stuff's templatePath and re-key the
byTemplatePath index so future findByTemplatePath lookups
see the new path. No-op when path matches the current value.
Locked down by @CallSecurity(ApiOnly) because flipping a
Stuff's identity post-clone would break FromTemplate
policies and any caller-side caching of template-path
identity. @Final @Unshadowable because the index update has
to run for every successful set — a subclass override that
forgot the index call (or a shadow that intercepted) would
silently desync byTemplatePath.
Unwraps via ProxyApi.unwrap so the #-slot access lands on
the raw target (see comment on #templatePath above).
OptionaldetailKey: stringSet the spatial zone. Gated by FromSpatialZone — only the
SpatialZone class and its subclasses (CartesianZone,
SphericalZone) may call this through the proxy. The
addLocation / removeLocation chokepoints on the zone side
are the legitimate callers; everyone else is rejected.
Clone-time seeding from StuffApi.#cloneInner doesn't go
through this method — it uses the caller-allowlisted
_stampZone seam below.
@Final @Unshadowable because the index of substrate
invariants that consult getZone() (containment's
cross-zone gate, Mobile.traverse, MQL scope walks) trusts
the slot's value; a subclass override or shadow that lied
about it could break those invariants. No legitimate
subclass needs to extend this anyway — the only legitimate
write paths are the SpatialZone chokepoints and clone-time.
Get a string representation of this object (for debugging).
Refresh the recency timestamp to now. Timestamp-fixed (no caller-supplied value). Called on the raw target by the security gate on every successful dispatch (Phase 2) and by the residency presence walk.
Staticcleanup
Constructor - generates unique runtime ID.
Subclass constructors should call super() and then initialize their fields. Use field initializers for default values where possible.
IMPORTANT: Direct
new SomeStuff()is rejected — every Stuff must be created viaStuffApi.create(() => new YourClass())orStuffApi.clone(...). The construction sentinel above guarantees this; rawnewoutside the Api layer throws here.Constructor-body method calls bypass the Proxy (the Proxy is installed AFTER the constructor returns). Initialize fields here; do NOT invoke methods that carry