The command system maps BC messages to registered command suites. Built-in plug-ins are statically assembled by src/app/start.ts; they are not discovered from the filesystem or loaded as external packages.
src/adapters/bc/command-router-plugin.ts owns the BC message subscription.
BcCommandParser validates targeting, sender, native Hidden commands, and
Chat/Emote/Whisper bang syntax before submitting a normalized BotCommandInput
to the transport-neutral core BotCommandParser. Feature suites register with
the core registry and recheck their own permissions. The command context
projects only the channel (message.Type), not the raw BC payload; submission
handlers that need private message text stay at the BC adapter boundary.
The composition-root room-config-runtime plug-in is a watcher, not a command
suite. It refreshes the missing-room creation template when versioned BC room
settings change. The supervisor separately applies the active revision after
the initial BC room sync, through its journaled room-settings action.
SafetyStopPlugin registers the shared /bot safeword command and delegates
stops to handlers registered by active activities. StagedTablePlugin
registers its private /bot table suite and a stop handler during start();
it also subscribes to observed BC appearance for pending-release reconciliation.
All registrations are disposed during stop(). Staged Table commands reject
public chat, and its game replies, cards, consent, offers, and prompt outcomes
are sent privately. The activity remains disabled unless its room config is
explicitly enabled and adults-only confirmed. Optional /bot table join accept
appearance, /bot table solo start appearance, and /bot table appearance
stop use an explicitly disclosed, revision-pinned itemset sequence and
recoverable release without another socket or journal owner.
The supervisor composes the ABCL feature and registers its command suite with the shared command router. The ABCL suite is an explicit opt-in/opt-out/status allowlist; automatic ABCL triggers are feature-owned and independently require the same consent and observed-region checks as command-triggered work. The separate helper process observes ABCL messages and performs assigned typed tasks, including participant-facing communication. Missing or invalid ABCL configuration leaves the feature inactive without blocking supervisor startup. ABCL configuration and helper-only communication are described in the ABCL feature guide.
Casino entry funding is independent of its game-command gate. The composition
root injects server-observed character-entry and ready subscriptions; the casino
plug-in grants 20 chips per rolling 24 hours, privately reports credits, and
disposes/drains this work on stop before the shared Mongo client closes.
Casino rules/fairness disclosures are also private and available while wagering
is disabled. Gated game commands accept weighted four-reel slots and the full
supplied European roulette layout; roulette accepts multiple bets and refunds
the last active bet before close. Game timers follow per-player slot 10/5/5 and
shared roulette 45/10/5-second pacing, with terminal cooldowns persisted.
Game subcommands are omitted from each sender’s casino help unless the map
runtime’s observed point/region transitions match a configured binding for
that supported game; bindings and casino rules come from room-scoped
prototypeGameConfig, not source-defined location keys. Command dispatch checks
the same policy again. The global help entry
remains the general casino wallet command and does not reveal game subcommands.
Newly observed roulette access also triggers one private command invitation. The composition root supplies the bot’s member identity so it is excluded. Duplicate entries within the same observed occupancy do not repeat the invitation; exit/re-entry or plugin restart permits a new one. It uses the existing journaled whisper API, never opens a round automatically, and drains alongside entry-grant work before casino game shutdown.
The shop is a separate statically wired plug-in. Its command suite requires
observed membership in the operator-created shop region; its service owns
recipient consent, economy reservations, appearance reconciliation, favor
evidence, and recovery. It shares the existing appearance action and economy
service and does not own a socket, transport queue, or second journal.
By default, round-open and closing callbacks use the casino plugin’s existing
private-announcement path, which selects and rechecks observed roulette-region
occupants, excludes the bot, and logs per-recipient failures. The optional
wheel-helper mode is composed in start.ts: casino lifecycle callbacks enqueue
typed helper events, while the helper uses its own CharacterCommunicationApi
and observed-region checks. Only confirmed pre-dispatch unavailability may use
the whisper fallback; ambiguous helper work never produces a second
announcement. Registration/lifecycle order is unchanged.
Location-entry-driven features can use the generic core
LocationEntryFeatureHandler<TAction> to own their event subscription,
concurrent member/location de-duplication, error reporting, and stop-time drain.
It injects event policy, action resolution, and execution rather than depending
on feature or adapter code. The location-messages plug-in uses it to validate
location data and current BC-observed context before sending journaled private
Whispers; see the location entry messages guide.
The location-appearance plug-in independently consumes both entry and exit
transitions: it delays an itemset application, cancels on exit or stop, and
revalidates current geometry and configuration at expiry. See the
delayed location appearance guide.
The emergency-release plug-in is a native-only self-service command: it applies
emergency.release.clothing directly to the requesting member without moving
either character or waiting for a location event; see the
emergency release guide.
classDiagram
direction LR
class BotPlugin {
<<lifecycle contract>>
+id: string
+start()
+stop()
}
class BotPluginHost {
-plugins: BotPlugin[]
-startedPlugins: BotPlugin[]
+start() Promise~void~
+stop() Promise~void~
}
class CommandSuite {
<<registration contract>>
+register() Dispose
}
class BotCommandParser {
<<registry and dispatcher>>
+register(name, description, handler, helpDescriptors?) Dispose
+handleMessage(input: BotCommandInput) Promise~boolean~
}
class BcCommandParser {
<<BC validation adapter>>
+handleMessage(message: BcChatRoomMessage) Promise~boolean~
}
class KeyedDeadlines {
+schedule(key, delayMs, callback)
+cancel(key)
+clear()
+has(key)
}
class KeyedSerialQueue {
+run(key, operation) Promise
+drain() Promise
}
class TrackedTasks {
+track(promise) Promise
+drain() Promise
}
class DisposerStack {
+add(disposers)
+dispose() Promise
}
class CommandAliasResolver {
<<single-pass fallback>>
+resolve(parts, commandPaths, descriptors)
+availableAliases(commandPaths, descriptors)
}
class CommandHelpCatalog {
<<live registration metadata>>
+register(descriptors) Dispose
+snapshot() CommandHelpSnapshot
+validate()
}
class ScopedHelpService {
<<read-only filtered help>>
+request(sender, scope, channel)
}
class HelpContextPort {
<<observed requester context>>
+current(sender, channel)
+isCurrent(context)
}
class CommandRouterPlugin {
<<BC event adapter>>
+start() void
+stop() void
}
class DoorFeaturePlugin
class LocationEntryBehaviorPlugin
class LocationEntryMessagePlugin
class LocationAppearanceTriggerPlugin
class EmergencyReleasePlugin
class LocationEntryFeatureHandler {
<<generic core event handler>>
+start() void
+stop() Promise~void~
}
class WorldAdminPlugin
class RoomConfigRuntimePlugin {
<<composition-root watcher>>
+start()
+stop()
}
class CommunicationPlugin
class PrototypeStatusPlugin
class AppearancePlugin
class CasinoFeaturePlugin
class ShopFeaturePlugin
class AnonymousWhispererPlugin
class TruthDarePlugin
class HotSeatPlugin
class StagedTablePlugin
class DilemmaPlugin
class ShowerFeaturePlugin
class AbclFeaturePlugin
class HelperBotsPlugin
class TrickOrTreatPlugin
class ShopLocationAccess
class CasinoLocationAccess {
<<per-member allowlists>>
+canAccess(member, gameId)
+accessibleGames(member)
}
class MapLocationEventBus {
<<server-observed point/region transitions>>
+locationEntered(member, key, shape)
+locationLeft(member, key, shape)
}
class DoorCommandSuite
class DoorAdminCommandSuite
class CommunicationTestCommandSuite
class PrototypeCommandSuite
class AppearanceCommandSuite
class EmergencyReleaseCommandSuite
class WorldAdminCommandSuite
class WhispererCommandSuite
class WhispererSubmissionHandler
class TruthDareCommandSuite
class HotSeatCommandSuite
class StagedTableCommandSuite
class SafetyStopPlugin
class SafetyStopCommandSuite
class SafetyStopRegistry {
<<shared activity stop boundary>>
+register(activityId, handler) Dispose
+stopActive(reason)
}
class DilemmaCommandSuite
class ShopCommandSuite
class DilemmaService
class ShowerService
class ShopService
class MongoDilemmaRepository
class MongoShopRepository
class HotSeatService
class MongoHotSeatRepository
class EconomyPort {
<<shared application contract>>
+grantOnce(operationId, member, credits)
}
class CharacterAppearanceApi {
<<shared appearance action API>>
+execute(member, mutation)
}
class CharacterAppearanceSequenceApi {
<<ordered shared appearance action API>>
+executeSequence(member, mutations, source, onPieceOutcome?)
}
class ShowerAdminPort {
<<transport-neutral room-admin contract>>
+showSettings()
+updateSettings(settings)
+manageSongs()
}
class SharedAppearanceObservationBus
class BcClient
class ActionCoordinator
class MongoActionJournal
class MongoRoomConfigRepository {
<<world config repository>>
+loadCurrent(roomKey)
+appendRevision(...)
+watch(reload, onError)
}
BotPluginHost o-- BotPlugin : owns lifecycle
RoomConfigRuntimePlugin ..|> BotPlugin
RoomConfigRuntimePlugin --> MongoRoomConfigRepository : watches revisions
DoorFeaturePlugin ..|> BotPlugin
LocationEntryBehaviorPlugin ..|> BotPlugin
WorldAdminPlugin ..|> BotPlugin
CommunicationPlugin ..|> BotPlugin
PrototypeStatusPlugin ..|> BotPlugin
AppearancePlugin ..|> BotPlugin
CasinoFeaturePlugin ..|> BotPlugin
AnonymousWhispererPlugin ..|> BotPlugin
TruthDarePlugin ..|> BotPlugin
HotSeatPlugin ..|> BotPlugin
StagedTablePlugin ..|> BotPlugin
SafetyStopPlugin ..|> BotPlugin
DilemmaPlugin ..|> BotPlugin
ShowerFeaturePlugin ..|> BotPlugin
ShopFeaturePlugin ..|> BotPlugin
AbclFeaturePlugin ..|> BotPlugin
HelperBotsPlugin ..|> BotPlugin
TrickOrTreatPlugin ..|> BotPlugin
CommandRouterPlugin ..|> BotPlugin
DoorCommandSuite ..|> CommandSuite
DoorAdminCommandSuite ..|> CommandSuite
CommunicationTestCommandSuite ..|> CommandSuite
PrototypeCommandSuite ..|> CommandSuite
AppearanceCommandSuite ..|> CommandSuite
WorldAdminCommandSuite ..|> CommandSuite
WhispererCommandSuite ..|> CommandSuite
TruthDareCommandSuite ..|> CommandSuite
HotSeatCommandSuite ..|> CommandSuite
StagedTableCommandSuite ..|> CommandSuite
SafetyStopCommandSuite ..|> CommandSuite
DilemmaCommandSuite ..|> CommandSuite
ShopCommandSuite ..|> CommandSuite
ShowerFeaturePlugin --> ShowerService
ShowerService --> CharacterAppearanceSequenceApi
ShowerService --> MapLocationEventBus
WorldAdminCommandSuite --> ShowerAdminPort
DoorFeaturePlugin --> DoorCommandSuite
DoorFeaturePlugin --> DoorAdminCommandSuite
WorldAdminPlugin --> WorldAdminCommandSuite
CommunicationPlugin --> CommunicationTestCommandSuite
PrototypeStatusPlugin --> PrototypeCommandSuite
AppearancePlugin --> AppearanceCommandSuite
TruthDarePlugin --> TruthDareCommandSuite
TruthDarePlugin --> TruthDareService
TruthDareCommandSuite --> TruthDareService
CasinoFeaturePlugin --> CasinoLocationAccess
CasinoFeaturePlugin --> MapLocationEventBus : observed entry/exit
DoorFeaturePlugin --> MapLocationEventBus : publishes transitions
LocationEntryBehaviorPlugin --> MapLocationEventBus : consumes transitions
LocationEntryMessagePlugin --> MapLocationEventBus : consumes observed entries
LocationAppearanceTriggerPlugin --> MapLocationEventBus : consumes entry/exit
LocationAppearanceTriggerPlugin --> AppearanceItemSetService : delayed set application
EmergencyReleasePlugin --> EmergencyReleaseCommandSuite
EmergencyReleasePlugin --> MapLocationEventBus : target entry observation
EmergencyReleasePlugin --> MapTeleportAction : observed bot teleport
EmergencyReleasePlugin --> AppearanceItemSetService : one-time release set
EmergencyReleasePlugin --> CharacterCommunicationApi : private notice and result
EmergencyReleaseCommandSuite --> BotCommandParser : native-only handler
LocationEntryMessagePlugin --> LocationEntryFeatureHandler
LocationEntryMessagePlugin --> CharacterCommunicationApi : journaled private Whisper
DoorCommandSuite --> BotCommandParser
DoorAdminCommandSuite --> BotCommandParser
CommunicationTestCommandSuite --> BotCommandParser
PrototypeCommandSuite --> BotCommandParser
AppearanceCommandSuite --> BotCommandParser
WorldAdminCommandSuite --> BotCommandParser
TruthDareCommandSuite --> BotCommandParser
TruthDareService --> EconomyPort
TruthDareService --> TruthDareRepository
WorldAdminCommandSuite --> LocationAdministration : injected core API
AnonymousWhispererPlugin --> WhispererCommandSuite
AnonymousWhispererPlugin --> WhispererSubmissionHandler : explicit private input
WhispererCommandSuite --> BotCommandParser
HotSeatPlugin --> HotSeatCommandSuite
HotSeatPlugin --> HotSeatService
HotSeatCommandSuite --> BotCommandParser
StagedTablePlugin --> StagedTableCommandSuite
StagedTablePlugin --> StagedTableService
StagedTablePlugin --> SafetyStopRegistry
StagedTableCommandSuite --> BotCommandParser
SafetyStopPlugin --> SafetyStopCommandSuite
SafetyStopPlugin --> SafetyStopRegistry
SafetyStopCommandSuite --> BotCommandParser
DilemmaPlugin --> DilemmaCommandSuite
DilemmaPlugin --> DilemmaService
DilemmaService --> MongoDilemmaRepository
DilemmaService --> EconomyPort
DilemmaCommandSuite --> DilemmaService
DilemmaCommandSuite --> BotCommandParser
ShopFeaturePlugin --> ShopCommandSuite
ShopFeaturePlugin --> ShopLocationAccess
ShopFeaturePlugin --> MapLocationEventBus : observed entry/exit
ShopFeaturePlugin --> ShopService
ShopService --> MongoShopRepository
ShopService --> EconomyPort
ShopService --> CharacterAppearanceApi
ShopCommandSuite --> BotCommandParser
CommandRouterPlugin --> BcCommandParser : passes BC messages
BcCommandParser --|> BotCommandParser : normalized commands
CommandRouterPlugin --> BcClient : owns message subscription
DilemmaService --> KeyedDeadlines : session expiry
VeratownMapRuntime --> KeyedDeadlines : door and notification timers
ShopService --> KeyedDeadlines : favor deadlines
LocationAppearanceTriggerPlugin --> KeyedDeadlines : entry delay
CasinoGameService --> KeyedSerialQueue : per-session operations
TrickOrTreatPlugin --> TrackedTasks : shutdown drain
CommunicationPlugin --> TrackedTasks : event work
DoorFeaturePlugin --> DisposerStack : reverse cleanup
StagedTablePlugin --> DisposerStack : partial-start rollback
HotSeatPlugin --> DisposerStack : reverse cleanup
ShopFeaturePlugin --> DisposerStack : startup rollback
CasinoFeaturePlugin --> DisposerStack : service shutdown
DilemmaPlugin --> DisposerStack : preserve resolution drain order
TrickOrTreatPlugin --> DisposerStack : event drain before service
WorldAdminPlugin --> DisposerStack : watch cleanup
AbclFeaturePlugin --> TrackedTasks : fence and drain accepted work
HelperBotsPlugin --> TrackedTasks : fence and drain repository work
BotCommandParser --> ActionCoordinator : handlers use journaled actions
BotCommandParser --> ScopedHelpService : global/scoped help
BotCommandParser --> CommandHelpCatalog : registration lifecycle
BotCommandParser --> CommandAliasResolver : fallback after real commands
ScopedHelpService --> CommandHelpCatalog
ScopedHelpService --> HelpContextPort
ScopedHelpService --> CharacterCommunicationApi : private whispers only
ActionCoordinator --> MongoActionJournal
World administration owns guided sessions, game-config watching, and the map/room creation-template watchers; shutdown disposes them before closing MongoDB. All guided controls recheck BC room-admin permissions. Map save/edit/restore append revisions, while only synchronized BC map observations can be saved as live snapshots. See world administration for commands and stored-versus-live semantics.
stateDiagram-v2
[*] --> Constructed
Constructed --> Starting: BotPluginHost.start()
Starting --> Running: all plug-ins started in registration order
Starting --> RollingBack: plug-in start throws
RollingBack --> Stopped: stop started/partially-started plug-ins in reverse order
Running --> Stopping: shutdown signal
Stopping --> Stopped: stop plug-ins in reverse order
Stopped --> [*]
The current static order is doors/map, location-entry messages, delayed location appearance, emergency release, location-entry behavior, world administration, communication/social, prototype status, appearance, Anonymous Whisperer, casino, shop, Truth/Dare, Dilemma, Hot Seat, Trick-or-Treat, helper bots, shower, then command routing. Thus command handlers and event listeners are installed before the BC client starts. Emergency release registers only native /bot emergency-release and applies the configured set to the requesting member. Anonymous Whisperer uses a separate explicit private-submission handler and remains database-disabled by default. Casino starts its approved slots/roulette engine and recovery; shop starts order reconciliation and region-gated commands, while release/favor use remains disabled until adult-policy confirmation. Truth/Dare recovery is enabled following operator-reported qualification and approval on 2026-10-15, while room configuration remains disabled by default. Dilemma registers commands and timer recovery but starts no round without enabled room config. Hot Seat starts recovery and private commands, but disabled room configuration prevents play. Trick-or-Treat listens to the shared map event bus, and only post-start position_update entries can offer a checkpoint; commands and invitations reply privately. Shower is opt-in and accepts matching local BC room updates without additional helper corroboration; its placement after helper bots preserves reverse-order shutdown with live room updates and optional helper observations available during restoration. Shutdown stops plugins in reverse order, draining game work and removing listeners, command registrations, watchers, and timers before closing the BC connection. On startup failure, the host attempts cleanup for the failing plugin and each earlier plugin.
For the complete wiring and data-authority model, see Bot Runtime Architecture.
stateDiagram-v2
[*] --> IncomingMessage
IncomingMessage --> NativeBotPacket: Hidden + ChatRoomBot prefix
IncomingMessage --> BangMessage: Chat/Emote/Whisper + !prefix
IncomingMessage --> Ignored: other message
NativeBotPacket --> TargetCheck: explicit target must be this bot
TargetCheck --> Ignored: another explicit target / invalid sender
TargetCheck --> RegisteredLookup: target omitted or matches bot
BangMessage --> NormalizeWhisper: strip BC wrapper when Whisper
NormalizeWhisper --> TargetCheck: targeted Whisper validation
RegisteredLookup --> LongestPrefixMatch: parse command words
LongestPrefixMatch --> Handler: registered command wins
LongestPrefixMatch --> AliasFallback: no registered command
AliasFallback --> Handler: valid alias target routed once
AliasFallback --> PrivateUnknown: no valid alias
LongestPrefixMatch --> PrivateHelp: help requested
LongestPrefixMatch --> PrivateUnknown: no handler found
Handler --> ActionLayer: feature invokes dedicated action/service
ActionLayer --> PrivateReply: command provides feedback
Handler --> [*]
PrivateHelp --> [*]
PrivateUnknown --> [*]
Ignored --> [*]
The registry supports multiword command paths and chooses the longest registered prefix. For example, door group ... is routed to the admin suite before the parent door handler. Only when no registered path matches does CommandAliasResolver rewrite the leading alias once; real commands always win. Aliases and their targets are validated against the current command-help catalog. Their target handler retains its own permission and availability checks, and scoped help lists an alias only beside a target visible to that requester. Native /bot door group ... becomes a Hidden ChatRoomBot door group ... packet in BC; bang commands remain available through Chat, Emote, and Whisper.
Anonymous Whisperer command and submission input are separate adapters. The
command suite handles opt-in, round start, and accusations. A dedicated
WhispererSubmissionHandler accepts only an explicit direct Whisper with the
game prefix and publish-consent phrase; it never echoes or logs the response.
The generic command trace suppresses option-name fragments for this command,
and the resulting room send uses the redacted communication action.
CommandSuite for BC, an operator entrypoint for CLI, or a future Discord adapter. Each adapter owns its own authorization and input/output mapping; never copy policy rules into adapters.BotPlugin under src/features/<feature>/ for feature lifecycle and BC event subscriptions, or a distinct command-adapter plugin where that lifecycle is clearer.start() and retain the disposer returned by BotCommandParser.register(). Inject shared services; do not construct a second Socket.IO client.src/app/start.ts, with service tests using fake ports and adapter tests for authorization, routing, and cleanup.Command permissions belong in the feature command suite/service, not in the generic parser. The BC adapter handles BC syntax and source normalization; the core registry routes normalized commands and sends private unknown-command replies. Each suite registers reviewed CommandHelpDescriptor metadata with its command disposer. ScopedHelpService resolves the current room-member context, applies explicit public/admin and availability policies fail-closed, and renders only visible capabilities, including operator aliases for visible targets. A feature-wide availability policy hides every descriptor for that feature, including administrative setup help, while disabled; per-capability policies can additionally hide commands such as games unavailable at the requester’s location. /bot help, !help, scoped <feature> help, and marked bare-feature fallbacks share that service; !<alias> help opens help for the canonical target. Unknown and inaccessible scopes use the same neutral private response. Oversized help is paginated on descriptor boundaries with !<scope> help page <n> continuation links; every page recomputes visibility and is rate-limited. Output is suppressed if room, permission, location/configuration revision, or catalog state changes before enqueue. A help listing never replaces command-time authorization.
Administrative and future non-BC interfaces share feature use cases, not transport behavior or permissions:
flowchart LR
BC[BC command suite] -->|room-admin check| UseCases[Feature use-case service]
CLI[Operator CLI] -->|operator invocation guard| UseCases
Discord[Future Discord adapter] -->|guild and role check| UseCases
UseCases --> Repositories[Feature repository ports]
UseCases --> Actions[Feature action ports]
Location entry behavior is edited through the room-scoped world-config service
at main.location-entry-behavior.rules; legacy main.bunny.rules remains a
read-only compatibility fallback unless explicitly edited. Referenced
appearance itemsets are managed through the standard appearance itemset editor.
Timer locks and their expiry properties are part of those generic itemset
definitions and are passed to BC unchanged. Generic itemset applications can
select conditional variants from the target’s latest observed appearance; the
composition root injects the BC prerequisite evaluator, and unknown evidence
does not match. Consumers with fixed-set policies reject variants.