veratown-prototype

Veratown Map And Doors

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.

Responsibilities

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.

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.

UML Map

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.

Region Geometry

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.

Door State Flow

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.

Tile Definitions

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.

Access And Commands

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.

Additional AND/OR access and block conditions

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:

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.

Current Limits