This guide documents the prototype’s location and keypad-door feature. The feature is hosted by DoorFeaturePlugin; the runtime itself depends on BC map observations and action ports rather than constructing sockets or emitting packets.
The separate web administration workspace
provides map-linked stored location, door, access-group and membership editors.
It has no BC socket or live role authority. Web saves do not open or close
doors; dynamic group principals still use the runtime’s current BC roles.
Door appearance item/group restrictions and conditional rule exceptions use
searchable selectors from the same pinned BC catalog as the appearance set
builder. Unlike that builder, filters include assets that cannot be applied by
the bot: matching observed appearance only requires their group/name identifiers.
Empty BC item names are valid exact references and appear as (Default) in the
editor.
Existing unknown selections are shown and preserved, not silently dropped.
Required items/groups are presence requirements, not an allowlist that overrides
blocks. Only a conditional rule’s own unlessItems/unlessGroups exempt its
matching block; unconditional blocked items/groups still deny access.
The workspace’s linked inspector brings door groups, membership grants,
overlapping locations and trigger counts together, with prefilled group/grant
creation. Stored-reference health findings link to the responsible editor.
These summaries never simulate or claim live BC authorization. The searchable
map legend focuses records, and duplication creates a disabled draft with a new
identity required; it does not clone access groups or membership grants.
The visual layout editor appends reviewed stored-map revisions, preserving
effects and private cell metadata; it does not apply a live map. Integrated
analysis shares the offline analyzer’s exact regions, findings and walkthrough,
with candidate review handed to ordinary disabled location/door drafts.
Deeper health checks flag catalog/style mismatches, wall/out-of-map placement,
shared triggers and empty occupancy regions; overlapping enabled locations are
notices because overlap can be intentional. These are stored-geometry checks,
not BC movement or permission evidence.
Doors embedded in walls are expected: door placement never produces a wall
overlap finding, and point/trigger checks exempt catalog WallPath openings.
Out-of-map coordinates and incompatible styles remain warnings. Pre-save impact
review identifies changed cells intersecting locations, doors, triggers and
inside regions. Analyzed exact regions can be merged and handed to an existing
location’s region or door’s insideRegion as a geometry-only replacement
draft, preserving access rules and all other settings.
insideRegion.Door map updates require the connected bot account to be a BC room admin. The client rejects updates before dispatch when that permission is missing.
Door auto-open, occupant-aware close/recheck, and keypad notification delays use replaceable keyed deadlines. Cancelled or superseded callbacks cannot apply stale door changes. Timer callbacks report failures with safe error names through the door action-error/debug path; timers are unref’d and all are cancelled on runtime disposal. A timer firing does not imply the map mutation was acknowledged or observed.
Location transitions reflect server-observed positions. The runtime publishes initial occupancy from room snapshots/reconnects, emits exits when a character leaves a point/region, disconnects, or configuration removes/disables the location, and emits corresponding entries after configuration changes. Features must not treat an entry event as permanent access.
/bot help, !help, and /bot door help show a shared permission-filtered
catalog. Door opening/code guidance is player-visible; CRUD, group, and access
management descriptors are omitted unless the requester is a current room admin.
The DoorAdminService independently checks room-admin authorization before
returning its command guide or processing any management request.
All door help, including administration help, is omitted when the current map
configuration has no enabled door. The command paths remain registered for
administrators configuring or re-enabling doors.
The shared MapLocationEventBus carries the location’s stable key and geometry
shape (point or region) on entry and exit. Consumers should check each
member’s current location and must remove access on locationLeft; do not
interpret room-global command registration or the bot’s own tile as a player’s
location.
Entry events distinguish position_update, member_join, initial_sync, and
configuration_change causes. Consumers that deliver entry effects should
ignore initial occupancy and configuration-only recalculations unless their
policy explicitly requires them. The
location entry messages feature uses this
metadata to send configured private messages only for observed player entries.
Raw positions use a different core contract, PositionObservations. The
supervisor publishes validated coordinates from full room sync, map-data sync,
and full-character sync sources. The bounded cache retains only one five-second
observation per member; departure, room/connection changes, or a full-roster
replacement invalidate entries. Room-properties-only syncs never refresh their
timestamps. Position observations do not grant access, emit location transitions,
or trigger door actions.
Guarded door-object updates retain the rest of the current map, including unrelated
sign CellData, and existing room settings. Dispatch or a room-admin acknowledgement
does not optimistically change the observed snapshot; only inbound room sync does.
Stored sign validation and creation-template behavior are described in the
world map guide.
classDiagram
direction LR
class DoorFeaturePlugin {
+start() Promise~void~
+stop() Promise~void~
-runtime: VeratownMapRuntime
-admin: DoorAdminService
}
class VeratownMapRuntime {
+seedCharacters(characters)
+observeRoom(room, source)
+observePosition(memberNumber, position)
+handleMessage(message)
+handleBotCommand(memberNumber, commandLine)
+replaceConfiguration(configuration)
}
class DoorCommandSuite {
+register() Dispose
}
class DoorAdminCommandSuite {
+register() Dispose
}
class DoorAdminService {
+execute(actor, command) Promise~string~
}
class MapRepository {
+loadRuntimeConfiguration(roomKey)
+watchRuntimeConfiguration(roomKey, reload, onError)
+validated CRUD and membership access
}
class MongoMapRepository {
+initialize() additive nonunique indexes
+normalize and validate reads/writes
}
class SetMapObjectAction {
+execute(position, style, type) Promise~ActionOutcome~
}
class MapTeleportAction {
+teleport(roomKey, locationKey) Promise~ActionOutcome~
+stop() Promise~void~
}
class PositionObservations {
<<read-only core port>>
+getLatestObservation(memberNumber)
+subscribe(listener) Dispose
}
class MapPositionObservationBus {
<<bounded core cache>>
+publish(observation)
+forget(memberNumber)
+clear()
}
class PositionObservationSession {
<<optional transport lifecycle>>
+start() Promise~void~
+stop() Promise~void~
}
class MapPositionChannel {
<<map-owned action adapter>>
+current()
+subscribe(listener) Dispose
}
class BcClient {
+mapPosition
+roomObserved
+roomUpdateResponse
}
class ActionCoordinator
class ActionJournal
DoorFeaturePlugin --> DoorCommandSuite : owns
DoorFeaturePlugin --> DoorAdminCommandSuite : owns
DoorFeaturePlugin --> VeratownMapRuntime : owns
DoorFeaturePlugin --> DoorAdminService : owns
DoorFeaturePlugin --> MapRepository : loads/watches
MongoMapRepository ..|> MapRepository
MongoMapRepository --> MongoDB : collections and change streams
DoorFeaturePlugin --> BcClient : subscribes/unsubscribes
DoorCommandSuite --> VeratownMapRuntime : open/close/code
DoorAdminCommandSuite --> DoorAdminService : CRUD/access/group
VeratownMapRuntime --> SetMapObjectAction : desired tile update
SetMapObjectAction --> BcClient : ChatRoomAdmin
SetMapObjectAction --> ActionCoordinator
MapTeleportAction --> BcClient : updateOwnMapPosition
MapTeleportAction --> MapPositionChannel
MapPositionChannel --> PositionObservations
MapPositionObservationBus ..|> PositionObservations
DoorFeaturePlugin --> MapPositionObservationBus : publish BC observations
PositionObservationSession --> MapPositionObservationBus : import/export live observations
MapTeleportAction --> ActionCoordinator
MapTeleportAction --> MapRepository
ActionCoordinator --> ActionJournal
DoorAdminService depends only on the MapRepository port. MongoMapRepository
owns all MongoDB collection operations for map configuration, applies bounded
Zod contracts to reads and writes, and initializes non-unique access indexes
before startup loads configuration. Legacy String/ObjectId _id values, missing
or null main-room scope, null group keys, nullable door geometry, declared
membership grant metadata, and the legacy Description field are normalized
without rewriting identities or changing main-room fallback behavior. Invalid
documents fail with bounded collection/path errors rather than being converted
to successful defaults. New writes are validated before persistence.
Location data is a generic bounded object for feature-specific metadata;
legacy data: null is normalized as absent, while object payloads (including
nested values) are preserved, including Location Entry Behavior’s entryBehavior
metadata. Missing
data is also valid. Non-object top-level payloads remain invalid.
Locations and door insideRegion values accept the existing inclusive rectangle
form or an exact tile region:
{
"kind": "tiles",
"rows": [
{ "y": 10, "spans": [[20, 25]] },
{ "y": 11, "spans": [[20, 25]] },
{ "y": 12, "spans": [[20, 22]] }
]
}
Tile regions support gaps, holes and disconnected components. Coordinates are
integers from 0 through 39; rows and spans must be sorted and canonical, with
adjacent spans merged. A one-tile region is valid. Stored legacy rectangles are
not rewritten. The shared core/tile-geometry containment is used by location
transitions, door occupant checks and helper task-location checks. Bounds shown
in admin output are summaries only, not membership.
Location region membership takes precedence over optional x/y coordinates.
Those coordinates remain an explicit point anchor or destination; a region-only
location is not teleportable. Region membership never implies walkability,
permissions or a route.
Room admins can enter a bounded region JSON value during guided
world location create or world location edit; answer none to clear a
location region. Door administrators can use
door update <key> insideRegion <JSON>, with JSON null retaining a nullable
value and none removing the field. Large regions should use the reviewed
offline manifest workflow rather than chat.
npm run analyze:bc-map -- --input <map-file> --apply --output <new-directory>
also writes region-candidates.json and an exact-cell region-candidates.svg.
After reviewing exact overlays, edit the candidate manifest with a room key and
stable location/door mappings, then preview the import:
npm run import:map-regions -- \
--manifest /path/to/region-candidates.json \
--source-map /path/to/map-export.txt
The importer is dry-run by default and selects only the fixed database named by
RUNTIME_TARGET. It verifies the catalog revision and source-map fingerprint,
rejects ambiguous/missing target keys, validates every resulting document and
prints tile counts, bounds summaries and disconnected component counts. Add
--apply only after review; the importer changes only the mapped region or
insideRegion and updatedAt fields. The fingerprint proves source-map
configuration provenance, not the current live BC room. No permission, door
policy or location state is inferred.
Contract normalization is separately available through
npm run migrate:mongodb-contracts. It is dry-run by default, requires
--apply for writes, preserves identifiers and grant metadata, and never
deletes documents. Review and approve the selected runtime target before apply;
the rollout does not imply database validators or unique indexes.
The offline draft review GUI
can prepare new definitions with source-based tile defaults and a 2500 ms
duration, optional hidden-door tile pairs, and map-selected keypadTiles and
autoOpenTiles. These settings are imported as configuration only; trigger
authorization and server-observed door state remain the runtime’s authority.
stateDiagram-v2
[*] --> LockedOrUnknown
LockedOrUnknown --> AwaitCode: keypad entry (all users)
AwaitCode --> OpenRequested: valid code and authorized membership
AwaitCode --> AwaitCode: invalid code / no access
OpenRequested --> OpenAcknowledged: BC ChatRoomUpdateResponse = Updated
OpenRequested --> OpenObserved: matching room map sync
OpenRequested --> OpenUnconfirmed: rejected / timed out / action failed
OpenAcknowledged --> OpenTimer: duration > 0
OpenObserved --> OpenTimer: duration > 0
OpenAcknowledged --> ManuallyOpen: duration = 0
OpenObserved --> ManuallyOpen: duration = 0
OpenTimer --> OccupancyCheck: timer expires
OccupancyCheck --> CloseDeferred: non-bot occupant in insideRegion
CloseDeferred --> OccupancyCheck: recheck delay elapses
OccupancyCheck --> CloseRequested: region empty or no region configured
ManuallyOpen --> CloseRequested: admin close
CloseRequested --> LockedOrUnknown: update acknowledged/observed
CloseRequested --> OpenUnconfirmed: close rejected / timed out
OpenUnconfirmed --> LockedOrUnknown: later locked tile observed
OpenUnconfirmed --> OpenObserved: later open tile observed
The timer starts only after the map action returns acknowledged, observed, or already_satisfied. acknowledged means BC returned Updated; it is not proof of the resulting map state. observed means a sync contained the exact expected BC object ID. A timeout starts no timer and makes no success claim.
Door documents store doorX/doorY, lockedTile/unlockedTile, optional lockedTileType/unlockedTileType, and unlockDurationMs. Style/type pairs are resolved against the pinned BC map catalog to an object ID before editing the encoded 40×40 map. If a style is unique, its type can be inferred; ambiguous styles require an explicit type. New Blank locked tiles default to WallDecoration in the admin create command. Blank in legacy documents is resolved during bulk setup from the existing map object at that door coordinate.
Only exact style/type pairs present in the BC catalog can be written. Tile strings are not guessed from labels or names. Map-state checks compare numeric BC object IDs, so duplicate styles in different types do not look equivalent.
Keypad entry only prompts for a code, including for administrators, group
members, and whitelisted users. Opening requires a nonempty submitted code
matching code or an entry in codes on any applicable group the sender
belongs to. Multiple memberships and multiple groups with the same code are
supported. Missing codes, missing groups, expired memberships, or codes for
another user’s group deny access; administrators have no keypad bypass.
Static groups use active membership records (including shared groupKey
membership). room_admin and room_whitelist principals use current BC roles,
not stored membership grants. Accepted unlocks change the shared room map tile,
not a per-player pass.
Groups have optional accessScope: "door" | "room" (omitted means "door").
Room scope applies to every door in the same configured room, including doors
added later. Startup, before loading runtime configuration, and reviewed
map-draft import with --apply insert these missing built-in records into
keypadGroupDefinitions:
| Management door key | Group name | Principal | Scope | Default code |
|---|---|---|---|---|
__room__ |
room_admin |
room_admin |
room |
knock |
__room__ |
room_whitelist |
room_whitelist |
room |
knock |
__room__ is a management namespace, not a map door. These are ordinary
database-managed group records: seeding does not overwrite operator edits.
Deleting a default group removes its access immediately, but the missing
default will be recreated on the next startup or applied draft import.
To persistently disable its code, clear code and codes instead. Changes
reload through the existing configuration watch; deleting a physical door
does not delete these room groups. No per-door migration is needed.
Manage the groups using the existing administrator commands, for example:
/bot door group list
/bot door group info __room__ room_admin
/bot door group update __room__ room_admin code <replacement>
/bot door group update __room__ room_admin codes []
/bot door group update __room__ room_admin code ""
/bot door group create __room__ <name> <code> principal=room_admin scope=room
principalType updates accept static, room_admin, or room_whitelist;
accessScope updates accept door or room. Group list/info include scope and
code-presence metadata but never code values. Dynamic group membership is live:
manual grants are rejected and group members explains the role instead of
presenting static records as authority. access check reports applicable
code-bearing groups, not an automatic unlock.
Distinct auto-open tiles retain their existing automatic behavior; if the same coordinate is configured as both a keypad and an auto-open tile for a door, keypad code gating takes precedence. Door administrators can optionally restrict player-triggered openings to a list of exact BC appearance item references:
/bot door update <doorKey> blockedAppearanceItems '[{"group":"ItemPelvis","name":"Diaper"}]'
/bot door update <doorKey> blockedAppearanceGroups '["ItemPelvis","ItemArms"]'
/bot door update <doorKey> blockedAppearanceRules '[{"blockedItems":[{"group":"ItemPelvis","name":"Penis"}],"unlessItems":[{"group":"ItemPelvis","name":"ChastityCage"},{"group":"ItemLegs","name":"ChastityBelt"}]}]'
/bot door update <doorKey> requiredAppearanceItems '[{"group":"ItemHead","name":"Blindfold"}]'
/bot door update <doorKey> requiredAppearanceGroups '["ItemLegs"]'
/bot door update <doorKey> blockedAppearanceItems none
/bot door update <doorKey> blockedAppearanceRules none
For example, this blocks players wearing ItemPelvis/Diaper; use the exact BC
appearance group and name to configure any other wearable item. Blocked
appearance groups match any worn item in that BC group (for example,
ItemPelvis). Required appearance items and groups form an allow condition: if
either list is configured, the player must wear at least one listed exact item
or an item in one of the listed groups. A blocked item/group always takes
precedence over the required-item condition. Each list accepts up to 32 entries;
use none to clear each setting. Matching uses the latest appearance observed
from BC. If that appearance is unavailable, a configured policy fails closed.
For conditional exceptions, configure blockedAppearanceRules as a JSON array
of at most 16 rules. Each rule has blockedItems and/or blockedGroups plus
optional unlessItems and/or unlessGroups; each list accepts up to 32 entries.
Within a rule, any blocked match denies access unless any exception in that same
rule matches. Rules are independent, so an exception never cancels another
matching rule or the existing unconditional blocked-item/group filters. In the
example, matching the configured ItemPelvis/Penis item blocks access unless
either exact cage or belt item is also present, including when both are worn.
Replace example group/name values with the exact BC appearance references. Use
/bot door info <doorKey> to inspect configured rules. Existing required-item
and required-group conditions still apply independently.
These rules apply both to keypad code submissions and to the player whose entry
triggers an auto-open tile, and are checked again when the auto-open delay
expires. They do not prevent passage through a door opened by someone else.
Administrator door open/door close commands remain explicit management
overrides, not player access.
The admin workspace also provides optional Additional access conditions and
Conditional block builders. Each is a flat group of 1–16 comparisons with
all (AND) or any (OR); nested groups are intentionally not supported.
Conditions use equals, at_least, or at_most with typed string, number, or
boolean values. Leaving both groups unconfigured preserves the existing door
policy exactly.
Supported door facts include roomAdmin, roomWhitelisted,
memberOf:<groupName>, validAccessCode, wears:<Group>:<Name>, and
hasAppearanceGroup:<Group>, activePose:<PoseName>, arousalProgress,
playerTitle, reputation:<Type>, itemExpression:<Group>:<Name>,
itemLocked:<Group>:<Name>, itemRemovalTimeRemainingMs:<Group>:<Name>,
hasOwner, hasLover, and credits. arousalProgress is the latest
BC-observed progress value from 0 to 100. Reputation is unknown if the type is
absent from the latest full character sync. Appearance-state facts require the
exact worn item; lock identity is not exposed, and zero removal time means the
BC timer elapsed—not that the item was removed. credits is the member’s
current available Veratown balance, excluding reserved credits; it is not BC
account Money. Missing or failed observations remain unknown and deny a condition.
memberOf uses current active memberships and live room roles for dynamic
groups. validAccessCode is true only after a keypad code matches an applicable
group the player belongs to; it is false for automatic opening. Thus, requiring
a valid code also prevents auto-open. The existing keypad requirement for a
matching code plus group membership is never replaced by an access condition. See the shared
condition facts and rules reference
for fact types, evidence sources, unknown handling, and admin-web suggestions.
For example, require both active membership in trusted and a worn blindfold,
while blocking room administrators:
{
"accessConditions": {
"mode": "all",
"conditions": [
{ "variable": "memberOf:trusted", "operator": "equals", "value": true },
{ "variable": "wears:ItemHead:Blindfold", "operator": "equals", "value": true }
]
},
"blockConditions": {
"mode": "any",
"conditions": [{ "variable": "roomAdmin", "operator": "equals", "value": true }]
}
}
An access group is an additional gate: its result must be true. A matching
conditional block denies access; unavailable evidence in either configured
group fails closed. These conditions apply to keypad and auto-open attempts,
including the recheck when the auto-open delay expires. Existing room-admin
door open/door close commands remain explicit overrides. If neither
condition group is configured, legacy door policy remains in effect; a
configured access group that does not match denies access.
The guided controls include add/remove/reorder operations, searchable fact suggestions, inline type validation, and a sample-value preview. The preview uses only values supplied in the editor and is not evidence of live BC state. Appearance predicates use the latest BC-observed appearance; if that evidence is missing, a required condition cannot match and a block condition denies.
The command suites are:
DoorCommandSuite: !door open <key> [durationMs], !door close <key>, and !code <code>; the corresponding native forms are /bot door ... and /bot code ....DoorAdminCommandSuite: door create|update|delete|list|info, door group ..., and door access ..., available as !door ... or /bot door ....WorldAdminCommandSuite: world location ..., world map ..., and
world config ... guided CRUD commands for room admins. world map edit
appends map metadata (Type/Fog) revisions in veratownMap, not the live BC room.
world map save snapshots the synchronized live map, including temporarily open
door objects; edits/restores preserve history and loads select the highest revision.
Local writes and map/room change streams refresh the missing-room creation
template; rejoining an existing room does not apply it. See
world administration for revision and backup commands.!door help or /bot door help; /bot help lists registered root commands.Admin operations return their service result by private Whisper. Debug mode (DOOR_DEBUG=true) records positions, access decisions, tile styles/types/IDs, update responses, timer scheduling/firing, and close deferrals. Codes and message bodies are not logged.
MapTeleportAction is a separate journaled operation for the bot’s own position. It accepts only an enabled point location in the configured room and sends through BcClient’s queue. With sharing disabled, it retains the existing local observation path. With sharing enabled, a fresh matching supervisor observation may satisfy the action; the channel never uses an undated local snapshot as initial evidence, and a fresh shared non-match takes priority over conflicting local state. Stop/disconnect cancels the wait; movement already dispatched remains timed_out_unobserved unless matching BC state was observed. It does not model walking, exits, capacity, or cross-room travel.