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.
ProtectedautoOpt-out for auto-deriving keywords from the host's display name
(via NamedMixin) and short description (via VisibleMixin).
Default true — most hosts want "a brass thermometer" to fold
into ['brass', 'thermometer'] automatically. Set to false
in template data when you want hand-curated keywords only
(e.g., a "scroll of resurrection" where you want just 'scroll',
not ['scroll', 'of', 'resurrection'] — though "of" would be
dropped as a stop word anyway, "resurrection" wouldn't).
ProtecteddetailsHierarchical detail map. Host-internal storage; external callers
go through getDetail / setDetail / removeDetail.
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.
ProtectedillustrationProtectedlongProtected OptionalprimaryAuthored primary keyword. When set, getPrimaryKeyword() returns
this value (after fail-soft validation against the live keyword
pool). Persistent — author-set via template data:.
Hydrator routes through setPrimaryKeyword (the Phase 1 dispatch
prefers a set<Field> method), so an authored-but-invalid value
in a template is logged + dropped at clone time rather than
silently sitting in the slot waiting to confuse a renderer.
ProtectedshortReadonlystuffRuntime ID for this object (generated using nanoid). This is NOT the MongoDB _id - it's a runtime identifier.
ProtecteduserStatic_StaticcommandVisible is target-shape only — no verb contributions. See the
mixin docstring for why look.yaml belongs on Perceiver's
self bucket, not on Visible's target-side buckets.
StaticfieldField-marshaller binding. mass round-trips via the kg-bound
QuantityMarshaller; the runtime accessor pair stays strict on
Quantity<'kg'>. Authoring-shape coercion (mass: heavy,
mass: "12000 g", bare numeric) lives in the marshaller's
fromStored and only runs on the persistence path.
StaticinstructionInstruction-field applier roster. details is consumed by
applyDetails (Phase 2 of PersistentHydrator). The
declarative YAML shape (a plain object keyed by detail name)
differs from the runtime Map<DetailId, Detail> shape, so
the applier sits between them — it owns the conversion. The
applier runs AFTER the persistent bracket-assign in Phase 1
(which would otherwise leave this.details as a plain object
and break getDetailEntries); the applier resets the Map up
front to undo that.
StaticmarkupMarkup-augmenter contribution. The wrapper-style augmenter
narrows the host to Detailed at runtime, then runs the
existing wrapDetailKeywords regex pass. Picked up by
VisibleMixin.getMarkupLong(viewer) via the prototype-chain
walker — non-Detailed hosts never see this augmenter; hosts
with Detailed-but-empty detail maps no-op cheaply inside the
helper.
StaticpersistentStaticsubscribableLive-query subscribable fields. The descriptor's
dependsOnFields defaults to ['details'] (descriptor name =
source field name), so FieldChangedEvent { field: 'details' }
from setDetail / removeDetail triggers re-projection
automatically. The ShadowChangedEvent entry covers future
visible-detail shadows that override projected entries
without firing a field change.
One descriptor carries both projection layers: read
enumerates the alias-grouped top-level entries (flat mode);
perDetailRead extracts a single entry's slice for focus-
mode subscriptions.
ProtectedkeywordsHost-internal accessor pair (Pattern D). External callers go
through getKeywords() / setKeywords(). The private setter
still fires when the Hydrator bracket-assigns
target['keywords'] = data['keywords'] — bracket access bypasses
TS visibility, so the normalization invariant runs during
hydration.
The getter returns the derived keyword pool: authored
keywords plus, when autoDeriveKeywords is true, tokenized
words from the host's display name (NamedMixin) and short
description (VisibleMixin). Authored entries always lead so an
exact-keyword match outranks a tokenized match (see
scope-walk.scoreCandidate). Internal callers that need the
raw authored set go through _keywords directly.
ProtectedmassPrivileged 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.
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.
Declarative applier — wires the template YAML's details:
map into the runtime Map<DetailId, Detail>. Phase 1 of the
hydrator bracket-assigned the plain YAML object onto
this.details, breaking the Map shape; the first thing we
do is reset it. Then walk the entries — each shaped either
legacy ({ keywords?, description: string }) or new
({ keywords?, vision?, hearing?, smell?, touch?, taste? })
— and call setDetail for each. Aliases are [key, ...keywords]
with duplicates squashed.
Mixed-shape entries (legacy description AND any per-sense
slot key in the same entry) throw — authors pick one shape per
entry. Malformed entries (no recognized shape, non-object
payload) are skipped with a warn. Nested children via the
details: sub-key are deferred to a future revision; v1 is
flat.
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.
Get all detail IDs recursively.
Optionalparent: stringGet a detail's per-sense slot value.
Overload-friendly runtime dispatch:
getDetail(id) — returns vision slot.getDetail(id, sense) where sense is a SenseChannel
literal ('vision' | 'hearing' | 'smell' | 'touch' | 'taste')
— returns that slot.getDetail(id, sense, parent) — sense + nested parent.getDetail(id, parent) where the second arg is NOT a known
sense channel — treats it as legacy parent and returns the
vision slot at the nested path.OptionalsenseOrParent: stringOptionalparent: stringAlias-grouped enumeration of top-level (or parent-scoped)
details. Walks the DetailMap at the requested level grouping
keys by Detail-object identity, so aliases (multiple keys
pointing at the same Detail object) bundle into one entry.
hasChildren reflects whether the entry's Detail has its own
nested DetailMap.
Optionalparent: stringLook up the single alias-grouped entry whose Detail covers
key. Supports the same dotted-path resolution that
getDetail uses. Returns null when no detail exists at the
requested key.
Get all detail IDs at level.
Optionalparent: 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.
Get the long description with fallback to short, then default.
Affordance-annotated long description — see the interface
docstring for the augmenter pipeline contract. Calls
Mml.augment with the host (this), the supplied viewer,
and the per-call opts (the senses build threads
opts.filter through here for verb-specific sense filtering).
Every contributing mixin's augmenters run in
parent-first → child-last order.
Optionalopts: AugmentOptsOptionaldetailKey: stringSelf-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).
Read the primary keyword for this Stuff. Returns the authored
value when it appears in the current derived keyword pool;
otherwise the last derived-pool entry; otherwise
undefined.
Last-pool-entry (rather than first) is the better default for
English modifier-noun phrases. Derived-pool ordering is
authored keywords first, then tokenized name (NamedMixin),
then tokenized shortDescription (VisibleMixin). For a Named
"Oak Door" the tokens land in order ['oak', 'door']; for
'a brass thermometer' the tokens land ['brass', 'thermometer']. In both cases the head noun is the trailing
token — what a player would naturally type to refer to the
thing — and what look <X> click-affordances should send.
Authors who need a non-trailing keyword pin it explicitly via
setPrimaryKeyword(...). The substrate default is just a
sensible last-resort.
Intentionally does NOT call setPrimaryKeyword from the getter
— the setter is a separate event surface from rendering.
Resolve 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.
Get the short description with fallback.
Read 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.
Get 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.
Membership test for a single detail id at the given level. Returns true iff the detail exists and ANY sense slot is populated (cheaper than walking the slot map externally).
Optionalparent: stringCheck 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.
Terminal onDestruct no-op. Exists so subclasses and mixins
overriding onDestruct can call super.onDestruct() without
the cast-to-optional-callable dance — the chain is guaranteed
to bottom out here. StuffApi.destruct invokes the hook via
the optional-method dispatcher in api/stuff.ts; that path
still works (always finds a function on the prototype).
Override (not extend with super) at any layer that wants
cleanup; chain to super.onDestruct() from the override so
intermediate layers in a mixin chain run too.
Invoked by StuffApi.destruct (and forceDestruct) while
the target is still live, after canDestruct passes and before
shadow-detach + destroy(). Witness — the return value is
ignored (it cannot veto; canDestruct is the veto seam). Override
to release resources/listeners and chain super.onDestruct()
so mixin layers run.
Remove detail(s).
Optionalparent: 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).
Set detail(s) — supports multiple IDs (aliases). Accepts both
the legacy string-description shape and the new per-sense slot
map shape:
setDetail(ids, "A brass handle.") → populates vision slot.
setDetail(ids, { vision: "...", touch: "..." }) → per-slot.
All IDs in a single call share one Detail object (alias semantics). Separate calls always produce separate Detail objects, even when slot values match.
Per-field invariants: the slot-map shape must populate at least one channel (empty maps throw); empty-string slot values are rejected. A legacy string-description shape accepts empty string (preserves prior behavior for tests / edge cases).
Optionalparent: stringReplace the keyword list. Equivalent to this.keywords = keywords,
kept for symmetry with the addKeyword/removeKeyword API.
Strict 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: stringAuthor-set the primary keyword. Stores the normalized value
unconditionally; pool-membership is a cross-field invariant
(the pool depends on shortDescription via VisibleMixin and
name via NamedMixin) and the Hydrator's Phase 1 dispatch
makes no ordering guarantee across mixins. Validating in the
setter would (and did) silently drop authored values when this
mixin's setter ran before the others contributing to the pool.
The getter (getPrimaryKeyword) does the lookup: if the
stored value is in the current pool, return it; otherwise fall
back to pool[0]. That keeps authored intent honored
regardless of hydration order, and a runtime caller passing a
bogus value gets the same silent override behavior the getter
already implements for any out-of-pool entry.
Passing undefined clears the explicit override; subsequent
getPrimaryKeyword() calls fall back to the derived-pool head.
Stamp 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).
Set 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.
Static__Composition-time check: hosts composing SurfacedMixin MUST
also compose ContainableMixin so the lazy getResting walk
has an environment to iterate. Dispatched once per concrete
class by StuffApi.register.
StaticcleanupFramework cleanup (R2.4 collection-symmetric). When a
Containable destructs, unhook it from its container's
contents set via the canonical chokepoint so onMoved /
onContainableRemoved witnesses fire. Discovered by the
dispatcher in StuffApi.#destructCore via the
MixinApi.queryMixins walk + own-static filter.
The Container-side cleanup (most-derived) for a
Container+Containable composition fires BEFORE this — it
evacuates contents while _container is still set, then
this hook completes the unhook for the destructing item's
own membership in its outer container.
Per-Detail Material overrides — flat map from detailKey to the Material's templatePath. Stored as a plain
Record(not aMap) so default JSON serialization handles it without a marshaller.