Declared address path in the namespace, or null. Sparse —
null is the common case and the resolve-walk falls through it
to a containment ancestor or the spatial zone.
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.
ProtectedcontentsThe contained items. Read access goes through getContents();
mutation goes through addContainable / removeContainable,
which only Containable.setContainer may legitimately invoke.
ProtecteddetailsHierarchical detail map. Host-internal storage; external callers
go through getDetail / setDetail / removeDetail.
ProtectedfixtureFixtures keyed by slot name. Single source of truth — the
Slotted base's slots Map stays empty for Adornable hosts
because occupy / vacate on Adornable are routed through
addFixture / removeFixture.
Runtime-only — fixtures are reconstructed by seed clone hooks
(BoundaryApi.attachExistingBoundary).
ProtectedillustrationProtectedlongProtectednextCounter for synthetic slot names. Instance-local, runtime-only.
ProtectedshortReadonlystuffRuntime ID for this object (generated using nanoid). This is NOT the MongoDB _id - it's a runtime identifier.
Static_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 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 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.
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 — clone each adornment template and attach it as a
fixture. Mirrors applyPopulates, minus the singleton dispatch:
fixtures are per-instance, so every entry is cloned fresh. A
template that doesn't compose AdornmentMixin is a configuration
error (it can't be a fixture) and throws, naming the path.
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.
The host's declared address, or null when none is declared.
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.
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: stringOptionaldetailKey: 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.
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).
OptionaldetailKey: stringGet the short description with fallback.
Effective light-receiving floor area in m², used by
VisionModality.lightAt to convert accumulated lumens to lux.
The base is topology-agnostic and returns 1.0 (m²); concrete
Location subclasses (CartesianLocation, SphericalLocation)
override per their cell geometry. Larger rooms read dimmer for
the same total flux.
Slot universe (Pattern C from slot.md). Derives from the live fixture keying.
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.
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.
Detach from the owning Zone on destruct. Clears locations
membership and any coordinate-keyed indexes the zone maintains
(CartesianZone grid, SphericalZone focus index).
ExitableMixin.onDestruct super-chains here after handling the
exit-side teardown. We chain to super in turn so
AdornableMixin.onDestruct (fixture teardown — wall sconces,
BoundaryAnchors) runs before the chain bottoms out at Stuff
(which has no onDestruct of its own).
Optional_context: unknownRemove primitive. Same lockdown as addContainable. Fires the
matching FieldChangedEvent { field: 'contents' } on a real
removal.
Remove detail(s).
Optionalparent: stringDeclare (or, with null, clear) the host's address.
OptionaldetailKey: stringSet 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: stringOptionaldetailKey: 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