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.
Protected_Persistent declarative-content field. _coords holds the
YAML-shape input {x, y, z}; the runtime tuple lives on
CartesianCoordinatesMixin.coordinates ([x, y, z]). The setter
bridges them — see setCoords.
Per declarative-content-slate § coords on CartesianLocation,
feedback_property_vs_instruction_fields (property field
shape: storage IS the value), and ref-shapes.md § Field
naming (backing slot is _coords, public surface is
getCoords / setCoords).
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.
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).
ProtectedcontentsThe contained items. Read access goes through getContents();
mutation goes through addContainable / removeContainable,
which only Containable.setContainer may legitimately invoke.
ProtectedcoordinatesProtecteddetailsHierarchical detail map. Host-internal storage; external callers
go through getDetail / setDetail / removeDetail.
ProtectedexitsExplicit exit map. Derived exits (cartesian adjacency, vessel 'out')
are NOT stored here — they are synthesized lazily by the zone and by
ExitableVessel.getExit() respectively.
Host-internal storage; external callers use getExits() /
getExit(direction).
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.
Protected 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.
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 consumed by applyPopulates. The YAML data
is an array of templatePath strings; Phase 2 dispatches by
source-template singleton-shape and moves the resulting
instance into self.
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.
ProtectedmassInstall a forward/back exit pair in one call. Both sides share the
same Door reference when one is supplied, so opening from either
side flips a single state.
Reciprocity: the opposite direction is inferred from
NavigationApi.invertDirection(direction) for cardinal directions.
For non-cardinal labels ('office', 'portal', vessel-specific
names) the caller MUST supply opts.opposite — there is no
structural inverse to infer. Passing opts.opposite for a cardinal
direction overrides the inferred inverse.
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.
Tightened cardinal rule per zone-architecture-slate § The cardinal-only-intra-zone exit invariant:
ZoneApi.resolveZoneForPath
(walks template ancestry in Mongo, no need to load the
destination room as a Stuff). Zones materialize lazily —
first reference triggers a clone, subsequent references hit
the singleton cache.Result: a freely-authored CartesianZone can have semantic exits
(portal, office) that cross into a SphericalZone or another
CartesianZone, but never inside its own grid.
The override is async because zone resolution may clone the
zone Stuff on first reference. The base interface already
carries Promise<boolean> so the override is type-safe.
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.
Declarative-content applier. The instruction field is consumed
here: each ExitInstruction is translated into an explicit Exit (or
addBidirectionalExit for cardinal / explicit-bidirectional
entries). Destinations and doors lazy-clone via
StuffApi.singleton, so a depended-on Location is materialized
on first need; further singleton(x) calls during a cascade
hit the registered proxy and short-circuit cycle scenarios.
Per-direction idempotency: a matching existing exit (same destination, same door) is a no-op; a mismatch throws with a diagnostic naming both seed paths.
Phase 2 applier. See class docstring for dispatch semantics.
Class resolution goes through StuffApi.loadClassByPath — the
existing public class-loading Api surface. The Template lookup
is a separate Template.findByPath call so we have tpl.class
to feed into loadClassByPath.
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.
Cube-cell ceiling height: cellSize. Floor-to-ceiling vertical
extent. null falls through identically to getVolume.
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: stringMerged lookup:
ExitableVessel overrides for 'out')CartesianZone returns anything)All non-null doors on this location's obvious exits.
Explicit exits only. For tooling / persistence.
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.
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: stringExits displayed by look — explicit ∪ derived, filtered by !hidden.
Zones that synthesize cardinal-derived exits (CartesianZone) opt in
via Zone.hasDerivedAdjacency(); zones without adjacency derivation
(SphericalZone) skip the iteration entirely so we don't waste 10
always-undefined deriveExit calls per query.
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: stringRead 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.
Get the short description with fallback.
Effective receiving-surface area in m² used by VisionModality.lightAt
to convert accumulated lumens to lux: the walk divides the total
flux at this room by getSizeScale(). Derived from the linear
cell extent by squaring it — cellSize: 3 → 9 m² floor area.
Larger rooms read dimmer for the same total flux. The fallback
covers transient test state where the room hasn't been added to
a zone yet.
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.
Cube-cell volume: cellSize³. The atmospheric-fill reservation
(n = PV/RT math operates against this scalar). Returns null
when the room hasn't been associated with a zone yet.
OptionaldetailKey: stringNarrowed override: a CartesianLocation lives in a CartesianZone
by CartesianZone.addLocation's rejection of non-Cartesian
locations. The cast happens once here, at the boundary; every
caller within (or with a typed reference to) CartesianLocation
gets the narrowed type for free. If a CartesianLocation ever
landed in a non-Cartesian zone, getCellSize would be undefined
and any optional-call would short-circuit — defensive but
documented.
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.
Mutual-exit verification: wires inverse pointers for any outbound
exit whose destination is already loaded, or marks the exit blocked
if the destination's topology doesn't match. Defers anything whose
destination hasn't been loaded yet — the destination's own load
(or the next traversal) will rerun the check. See
ExitableMixin.verifyOutboundExits.
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: stringSetter with side effect: stores the value AND registers this
location with its resolved CartesianZone via
addLocation(this, x, y, z). Idempotent on the happy path
(same coords → no-op); throws with a conflict diagnostic when
already at different coords. The zone resolution uses
getZone() — at hydrate time, Stuff.zone has been stamped
(see lifecycle: zone stamp runs before hydrate).
Pattern: setter with side effects, parallel to
SphericalLocation.setFocus and Window.setAttachedHosts.
Per declarative-content-slate § coords on CartesianLocation.
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: stringOptionaldetailKey: stringOptionaldetailKey: 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: stringOptionaldetailKey: 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.
Override 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.
Mutual-exit invariant check, run at Location load (via
postRegister) and on traversal as a fallback. Walks ONLY the
_pendingVerify set — exits the addExit-time triage flagged as
"could be wired but isn't yet."
For each pending exit:
blocked = true with a logged warning. Either way,
evict.Idempotent. Zone-derived cartesian exits are synthesized per-call and inherently mutual by grid adjacency — they aren't tracked.
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