Protected_Whether a wheeled hauled cart may pass through. Default true;
authors set false on stairs / stiles / narrow doors that admit
walking but not wheels (the residue the media gate can't express).
ProtectedblockedPermanently blocked regardless of door state.
ProtecteddirectionProtectedhiddenHidden exits are skipped by getObviousExits() (and therefore by look).
Protected OptionalinverseCounterpart Exit on the destination side, when this exit is part
of a bidirectional pair. undefined for one-way exits, vessel-
synthesized 'out' exits, or pairs that haven't been wired (lazy-
load case).
ProtectedmediaLocomotion media this exit admits (e.g. ['ground'],
['water', 'ground']). The exit admits any LocomotionMode whose
getMedium() is in this list. Default [] is treated by
allowsMode as legacy walk-only — preserves backcompat for exits
authored before the medium refactor. Authors group modes by medium
to keep declarations terse: a normal corridor's ['ground'] admits
every ground-locomotion mode without enumerating each verb.
ProtectedmessageCustom arrival text (destination peers). null → use default.
ProtectedmessageCustom departure text (source peers). null → use default.
ProtectedmuffledMuffled exits: movement/speech through here is suppressed for sensors. Reserved for later phases.
ProtectednoIf true, followers/mounts don't chain through this exit. Reserved for later phases.
ProtectedoneIntentionally one-way. Mutual-exit verification skips this Exit when checking that the destination has a back-pointing inverse.
ProtectedsourceReadonlystuffRuntime ID for this object (generated using nanoid). This is NOT the MongoDB _id - it's a runtime identifier.
StaticsubscribableUniversal live-query subscribable fields — fields every Stuff
exposes regardless of mixin composition. Currently just
displayName, a derived render that delegates to
Stuff.getPresentation (Named's name or Visible's
shortDescription, falling through to the baked-in 'something').
Declared here rather than in a substrate-private synthetic
table because every Stuff genuinely owns the concept — there is
no "what if this Stuff has no displayable identity?" case. The
descriptor uses dependsOnFields to declare the leaf source
fields it depends on (name, shortDescription); the substrate
installs precise (FieldChangedEvent, 'field', dep) index
entries automatically. Shadow lifecycle support rides on
ShadowChangedEvent in changes (declared-but-unfired until
the shadow subsystem wires it).
Mixin layers above Stuff add their own subscribableFields for
mixin-owned state; the substrate's prototype-chain walk
hasOwnProperty-checks at every level and unions the
descriptors.
Future universal renders (pronoun, articleName, etc.) land
here too. Mixin-gated renders go on the mixin that owns the
gate.
ProtecteddestinationHost-internal getter.
Resolution order:
_destination (live ref) — present for Pattern B and for
Pattern-C-with-direct-live-ref constructor calls._destinationPath resolved via StuffApi.findByTemplatePath
— re-resolved every call, no cache.Throws if no destination is set, or if the path is set but the
target zone isn't loaded yet. Callers in the latter case must
await exit.resolveDestination() to fault in the zone.
Setter accepts a live Container. Stamps _destinationPath from
the target's templatePath if available (Pattern C), else stores
the ref in _destination (Pattern B within-session). NEVER
caches a previous path-resolution into the live-ref slot — that
was the rule the ref-shapes design dropped.
Both slots are written symmetrically: whichever branch the new destination takes, the other slot is cleared so the getter's resolution order can never see stale conflicting state.
ProtecteddoorHost-internal accessor pair (Pattern D). External callers go
through getDoor() / setDoor(). Setting this property updates
the door's attachedTo back-reference: the previous door (if any)
drops this Exit, the new door (if any) gains it. null clears.
True iff a mode named modeName is admitted by this exit. Resolution:
media → legacy default; admits only 'walk'. Preserves
pre-refactor behavior for exits constructed without an explicit
medium set.media → resolve the mode singleton via
LocomotionApi.modeOf(modeName). If unloaded, reject (matches
the strict-resolution contract elsewhere in the substrate). If
resolved, admit iff the mode's getMedium() is in media.Passthrough modes (ride / drive) have medium === null; they're
never admitted by an exit's media list directly — the conveyance
host's mode is what gets gated. Player flow always routes through
LocomotionControllerBase, which substitutes the host's mode at
the exit-gate call site (see LocomotionControllerBase.execute).
Residency veto: an exit whose source room is alive stays resident — else the sweep would cull an exit out from under a live room (R2.1 owned). An exit of a destroyed room is cullable.
Can mover traverse this exit right now?
Returns { ok: false, reason, gate } on the first failing gate
(blocked / closed door / mode rejection); { ok: true } otherwise.
mode is the short name of the actor's intended LocomotionMode
(e.g. 'walk', 'climb'). When supplied, this method consults
allowsMode(mode). When omitted (pre-locomotion callers, admin
tools), the mode gate is skipped — backcompat-additive.
Optionalmode: stringDestroy 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.
Return the destination's templatePath. Returns null only when
the Exit was constructed with a live-ref destination that itself
has no templatePath (the within-session Pattern B case).
Used by the mutual-exit verifier to identify the destination
by path.
Read 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.
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).
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.
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.
Cleanup hook fired by StuffApi.destruct(this).
attachedTo set
(Phase 4 wires that field — pre-Phase-4 the lookup is a
no-op).Resolve the destination, awaiting a singleton clone when the
target zone hasn't been loaded yet. Use this from async paths
(MobileMixin.traverse) that need to fault in the zone before
the cross-boundary move.
No runtime cache — every call re-reads via
StuffApi.singleton(path) (or returns the within-session live
ref if one was supplied via setDestination). The cache slot
the ref-shapes design dropped used to silently turn path
resolutions into stale Pattern-B refs across hot-reload churn.
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 idea (for debugging). Subclasses should override to provide more specific information.
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.
Base class for incorporeal game objects with identity but no physical presence.