Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,15 @@ derivatives separate "instant" commands from round-based combat resolution.

### Content authoring evolution (not all v1)

1. Hardcoded rooms in C# (bootstrap only, to get the loop working).
2. Data-driven world files (JSON/YAML) loaded at startup β€” natural next step,
also the foundation for in-game building commands later.
3. In-game building commands (`@dig`, `@describe`, etc.) writing to the same
data model.
1. Hardcoded rooms in C# (bootstrap only, to get the loop working) β€” still
how every world's starting content is authored today.
2. Data-driven world files (JSON/YAML) loaded at startup β€” **not yet
implemented**; turned out not to be a prerequisite for stage 3 below.
3. In-game building commands (`dig`/`tunnel`/`describe`) writing directly
to the live `Thing` tree β€” **implemented**, role-gated at
`SecurityRole.MinorBuilder`. See
[ADR-0009](docs/adr/0009-world-building-olc-command-surface.md) and
[docs/world-model.md](docs/world-model.md).

## Command System

Expand Down Expand Up @@ -157,8 +161,9 @@ derivatives separate "instant" commands from round-based combat resolution.
for a plain name (no identity verification) as a placeholder until this
phase lands.
8. **Moderation/admin tooling**: implemented β€” see below.
9. **Procedural frontier generation**, **in-game building/scripting**: later
phases (see Deferred/Open Items).
9. **In-game building** (`dig`/`tunnel`/`describe`): implemented β€” see
Content authoring evolution above. **Procedural frontier generation**
and **soft-code/scripting**: later phases (see Deferred/Open Items).

## Accounts & Auth βœ… implemented

Expand Down Expand Up @@ -204,6 +209,23 @@ Explicitly out of scope for v1, to revisit later:
where β€” undesigned. Tracked as an open item in
[PLAN-0005](docs/plans/0005-security-role-model-and-moderation-commands.md);
the moderation commands themselves are implemented (see above).
- **NPC/item spawning, mob-respawn loops, loot tables**: undesigned. An NPC
killed in combat is removed from the world permanently (no respawn
timer); loot drops aren't implemented at all (see
[docs/combat.md](docs/combat.md)). Surfaced while scoping ADR-0009,
deliberately kept out of it β€” tracked as Slice 10 in
[PLAN-0001](docs/plans/0001-wheelmud-reconciliation-roadmap.md), not yet
a numbered slice in ADR-0001 itself.
- **World persistence loads everything into memory, always**: `ThingRepository`
reconstructs the *entire* stored world on every load β€” fine at
hand-built-hub scale, won't scale to real user counts or a large
procedurally generated world. A persistence-layer/loading-strategy
concern, separable from the `Thing`/`Behavior` domain model itself (see
[docs/persistence.md](docs/persistence.md) Open Items for the full
reasoning). Undesigned β€” revisit once there's a real deployment
approaching actual scale, or Slice 9 makes the world large enough that
eager-loading stops being free. Tracked as Slice 11 in
[PLAN-0001](docs/plans/0001-wheelmud-reconciliation-roadmap.md).
- **Soft-code/scripting engine**: revisit once data/config-driven NPC and room
behavior proves insufficient.
- **Procedural frontier generation algorithm**: choice of generation approach
Expand Down
246 changes: 246 additions & 0 deletions docs/adr/0009-world-building-olc-command-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
# [ADR-0009] World-Building/OLC Command Surface

**Status:** Accepted

**Date:** 2026-07-23

**Decision Makers:** solo (design dive conducted with the user)

## Context

Per [ADR-0001](0001-wheelmud-reconciliation-roadmap.md), this is Slice 4
of the WheelMUD reconciliation roadmap, explicitly bundled with Slice 3
(now `SecurityRole`, [ADR-0005](0005-security-role-model-and-moderation-commands.md))
since it consumes the same mechanism. WheelMUD's own OLC surface is
minimal β€” just `Actions/OLC/Tunnel.cs`, an admin command that wires a
two-way exit between two *already-existing* rooms by id; WheelMUD has no
room-creation command at all (rooms are only ever built in code via
`Core/Creators/`).

`docs/world-model.md`'s "Content Authoring Evolution" section, though,
already names a further stage sharp-mud wants that WheelMUD never had:
in-game building commands (`@dig`, `@describe`) writing directly into the
live world tree, explicitly *not* requiring a data-file format first.
`docs/commands.md`'s V1 Verb List already flags builder/OLC verbs as
excluded pending "the deferred in-game building phase" β€” this ADR is that
phase.

Verified by direct research (see `docs/plans/0009-world-building-olc-command-surface.md`
for exact file/method citations): rooms today only ever get created once,
at boot, in a ruleset's `IWorldBuilder.Build()` (e.g.
`src/SharpMud.Ruleset.Basic/BasicWorldBuilder.cs`), which also contains the
only existing "wire a two-way exit" logic (`Connect(world, a, b,
direction)` β€” two `Thing`s, one `ExitBehavior` each, one per direction,
using the existing `Direction.Opposite()` extension). There is no
runtime/in-game equivalent, and no player-facing short id for a room today
β€” just an internal `ThingId` (`Guid`), never surfaced to a user.
`SecurityRole.MinorBuilder`/`FullBuilder` (added in ADR-0005, accumulation
already implemented) exist and are unused, doc-commented as reserved for
this slice.

## Decision Drivers

- ADR-0001 already scoped this as "small once #3 exists" β€” the mechanism
(role-gated commands via `RegisterWithRole`, admin-command dependency
shape via constructor-injected `IThingRepository`) is fully reusable
from Slice 3, nothing new needed there.
- `world-model.md` already commits sharp-mud to going further than
WheelMUD's Tunnel-only OLC (room creation, not just room linking) β€”
reconciling only the narrower WheelMUD scope would leave `commands.md`'s
own flagged gap half-closed.
- No new `Thing` subtype (`design-decisions.md` rule 2) β€” a "room" is
still a `Thing` + `RoomBehavior`, built exactly like
`BasicWorldBuilder.CreateRoom` does today, just at runtime instead of at
boot.
- `CommandParser` splits on whitespace only (`src/SharpMud.Engine/Commands/CommandParser.cs`)
β€” no quoted-string support exists anywhere in the command pipeline
today. Any multi-word argument (a room name) has to be "rest of the
line," which rules out a command needing two independent free-text
fields (e.g. name *and* description) in one line.
- Rooms have no player-facing identifier other than `Thing.Name` β€” a
targeting scheme has to be built on that (name-based lookup) or a wholly
new id concept invented; introducing a new persisted id purely for this
slice is unjustified scope given name-based lookup already works and
nothing else in the codebase uses non-`Guid` ids.

## Considered Options

**Scope (what this slice builds):**

1. **Tunnel-only** β€” one command, linking two pre-existing rooms by name,
matching WheelMUD's actual OLC surface exactly.
2. **Dig + tunnel** β€” add room *creation* (`dig`, wiring a new room to the
current one) alongside a `tunnel`-only command for linking two
pre-existing rooms, closing `world-model.md`'s stated gap in full.

**Room targeting:**

1. **By exact `Thing.Name`** β€” reuse the field players already see;
ambiguous/duplicate names rejected with a clear error.
2. **New short numeric/slug room id** (WheelMUD/ROM-`vnum`-style) β€” a new
stable identifier, its own admin command to list rooms with their ids,
and a new persisted field.

## Decision Outcome

Chosen: **"2 β€” dig + tunnel"** for scope, **"1 β€” by exact `Thing.Name`"**
for targeting.

Scope: WheelMUD's Tunnel-only OLC solves a narrower problem than the one
sharp-mud has already committed to solving (`world-model.md`'s stage-3
authoring evolution). Reconciling only the WheelMUD-faithful subset would
require redesigning this again immediately to add room creation β€” better
to build both now, in the same slice, while the security-role mechanism
and admin-command dependency shape are already fresh from Slice 3.

Targeting: researched ROM/Diku's OLC convention directly as prior art
(numeric `vnum` addressing, block-allocated per area, exits set
per-direction with no automatic two-way mirroring β€” a builder sets both
sides manually). That scheme solves a problem sharp-mud doesn't have
(multiple builders editing separate area files, needing collision-free
ids across files) β€” sharp-mud has one live, in-memory world tree, so a
new id concept would be solving for a constraint that isn't real here.
Name-based lookup costs nothing new; a `vnum`-equivalent would require
designing and persisting an id field, plus a command to discover it,
for a benefit that doesn't apply to this codebase's actual shape.

**Mechanism, in full:**

- Three new commands, all gated `SecurityRole.MinorBuilder`, registered
via `RegisterWithRole` (mirrors Slice 3's `AdminCommands` shape exactly)
in a new `BuilderCommands.RegisterAll(registry, repository)`:
- **`dig <direction> <new room name>`** β€” creates a new `Thing` +
`RoomBehavior` (empty `Description`, filled in afterward via
`describe`), attached as a child of `ctx.CurrentRoom`'s parent (the
same area/container `ctx.CurrentRoom` itself lives under β€” a dug room
is a sibling of the room it's dug from, not a child of it; exits are
children of rooms, rooms are children of an area), then wires a
bidirectional exit between `ctx.CurrentRoom` and the new room in the
given direction (and its `Direction.Opposite()` back).
- **`tunnel <direction> <existing room name>`** β€” looks up an existing
room by exact `Name` among `world.AllWithBehavior<RoomBehavior>()`;
rejects with a clear message if zero or more than one match. Wires
the same bidirectional exit as `dig`, between `ctx.CurrentRoom` and
the found room.
- **`describe <text>`** β€” sets `ctx.CurrentRoom.Description` to the
rest-of-line text. Not itself in WheelMUD's Tunnel-only OLC, but a
direct, unavoidable consequence of choosing option 2 above (`dig`
creates a room with no description; without a way to set one
afterward, every dug room is permanently blank) and matches
`world-model.md`'s own `@describe` naming.
- All three take `IThingRepository` via constructor injection (same
shape as Slice 3's six repository-needing admin commands), and after
mutating the tree, walk `ctx.CurrentRoom` up via `.Parent` to the
root `Thing` (no `Parent`) and call `repository.SaveTreeAsync(root,
ct)` β€” not `SaveTreeAsync(ctx.CurrentRoom, ct)`, because `dig`'s new
room and its own reverse exit live outside `ctx.CurrentRoom`'s own
subtree (siblings, not descendants); only a save rooted above both
rooms captures everything that changed in one call.
- The exit-wiring logic (`Connect`-equivalent: two `Thing`s, one
`ExitBehavior` each, `Direction`/`Direction.Opposite()`) is extracted
from `BasicWorldBuilder` into a small shared helper in
`SharpMud.Engine` (all the types involved β€” `Thing`, `Direction`,
`ExitBehavior`, `IWorld` β€” already live in `Engine`), and
`BasicWorldBuilder.Connect` is updated to call it instead of keeping its
own copy β€” a direct, justified dedup enabled by writing the same logic
a second time in this slice, not a separate unrelated refactor.
- No delete/undo command (`undig`, room removal) β€” not in WheelMUD's
Tunnel either, and removal safety (what happens to players/items/exits
currently inside a removed room) is a genuinely different problem,
deliberately deferred rather than folded in here.

### Positive Consequences

- Closes `commands.md`'s explicitly-flagged builder/OLC gap in full, not
just the narrower WheelMUD-equivalent subset.
- `SecurityRole.MinorBuilder`/`FullBuilder` get their first real consumer,
as ADR-0005 anticipated.
- Reuses Slice 3's registration/dependency/save patterns exactly β€” no new
architectural shape introduced.
- The `Connect`-logic dedup between `BasicWorldBuilder` and the new
commands means there's exactly one place that knows how to wire a
two-way exit, not two copies that can drift.

### Negative Consequences

- No room-removal/undo path β€” a `dig` mistake is permanent (fixable only
by leaving the room in place, unconnected, or a manual DB edit); accepted
as a smaller, separable problem, not a reason to hold up this slice.
- Name-based room lookup means two rooms sharing a name are ambiguous to
`tunnel` (rejected with an error, not silently picking one) β€” acceptable
given no naming-uniqueness constraint exists elsewhere in the world
model today, and enforcing one now would be a separate, broader change.
- `describe` only touches the room the builder is currently standing in β€”
there's no `describe <room name>` for a room elsewhere; consistent with
WheelMUD's Tunnel needing no such remote-targeting either, but a real
limitation if a builder wants to batch-describe several dug rooms
before walking between them.
- **Explicitly does not include NPC/item spawning** β€” surfaced during this
ADR's own scoping discussion: sharp-mud today has no mob-respawn loop or
loot-table mechanism at all (`docs/combat.md` β€” NPC death permanently
removes it from the world, no timer brings it back; loot drops are
"not implemented, blocked on the item system"). This maps to WheelMUD's
`Clone`/`Spawn` admin actions, already explicitly deferred by
[PLAN-0005](../plans/0005-security-role-model-and-moderation-commands.md)
pending "item/NPC creation tooling" β€” a genuinely separate decision
(spawn timing, loot table shape, whether `Clone`/`Spawn` are even the
right shape for a tick-driven respawn vs. a one-shot admin placement)
deliberately left for its own future slice rather than folded in here,
per the user's explicit call during this ADR's design dive.

## Pros and Cons of the Options

### Scope option 1: Tunnel-only

- Good, because it's the smallest possible slice, most faithful to
WheelMUD's actual OLC surface.
- Bad, because it doesn't close `world-model.md`'s already-stated gap
(room creation) β€” a second design pass would be needed almost
immediately after.

### Scope option 2: Dig + tunnel (chosen)

- Good, because it closes the real, already-documented gap in one pass
while the Slice 3 mechanism is fresh.
- Bad, because it's more surface than WheelMUD's own OLC ever had β€” not a
1:1 reconciliation, a deliberate extension past it.

### Targeting option 1: By exact `Thing.Name` (chosen)

- Good, because it needs no new concept, field, or command β€” the name
players already see is the identifier.
- Bad, because duplicate room names are ambiguous (rejected, not silently
resolved).

### Targeting option 2: New short id (vnum-style)

- Good, because it's what WheelMUD/ROM-family MUDs actually do, and scales
better if the world ever has many same-named rooms.
- Bad, because it solves a multi-builder/multi-area-file collision problem
sharp-mud doesn't have (one live world tree, not separate area files),
and requires a new persisted field plus a discovery command with no
other consumer today.

## Links

- [ADR-0001](0001-wheelmud-reconciliation-roadmap.md) β€” WheelMUD
Reconciliation Roadmap (this is Slice 4, bundled with Slice 3).
- [ADR-0005](0005-security-role-model-and-moderation-commands.md) β€” the
`RegisterWithRole`/`SecurityRole` mechanism this slice consumes
unchanged; `MinorBuilder`/`FullBuilder` were added there specifically
for this slice.
- [PLAN-0009](../plans/0009-world-building-olc-command-surface.md) β€”
execution plan for this decision.
- `docs/world-model.md` β€” "Content Authoring Evolution" stage this ADR
implements; also where frontier/procedural generation (out of scope,
Slice 9) is separately tracked.
- `docs/commands.md` β€” the V1 Verb List's builder/OLC exclusion this ADR
resolves.
- `src/SharpMud.Ruleset.Basic/BasicWorldBuilder.cs` β€” existing
boot-time room/exit creation pattern this slice's runtime commands
mirror (and whose `Connect` logic moves into `Engine`, shared).
- WheelMUD `Actions/OLC/Tunnel.cs` β€” source consulted; adopted the
two-way-exit-by-direction shape, explicitly extended past it (room
creation) per the Decision Outcome above.
- ROM/Diku-family `redit`/vnum OLC convention β€” researched as prior art
for room targeting, explicitly not adopted (see Decision Outcome).
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ the mechanics: numbering, status, and the index.
| [0006](0006-nuget-package-distribution.md) | NuGet Package Distribution + Sample-Based Ruleset Extraction | Accepted |
| [0007](0007-narrow-meta-package-scope.md) | Narrow the `SharpMud` Meta-Package to Engine + Hosting + Persistence | Accepted |
| [0008](0008-ruleset-scaffolding-tier.md) | A Reusable RPG Scaffolding Tier Between `Engine` and Concrete Rulesets | Accepted |
| [0009](0009-world-building-olc-command-surface.md) | World-Building/OLC Command Surface | Accepted |
14 changes: 11 additions & 3 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,17 @@ actually implemented as of the inventory/items build-order phase:
- **Character**: `score`/`stats` (display derived stats β€” see
[character.md](character.md)) is **not implemented yet**.
- **Meta** βœ…: `help`, `quit`.
- **Builder/OLC verbs** (`@dig`, `@describe`, etc.) explicitly excluded β€”
those belong to the deferred in-game building phase (see
[world-model.md](world-model.md)). **Moderation/admin verbs** βœ…
- **Builder/OLC verbs** βœ… (`dig <direction> <name>`, `tunnel <direction>
<existing room name>`, `describe <text>`) β€” role-gated at
`SecurityRole.MinorBuilder` via `ICommandRegistry.RegisterWithRole`, same
mechanism as the moderation verbs below. `dig` creates a new room and
wires a two-way exit to it; `tunnel` wires the same kind of exit between
the current room and an already-existing room found by exact name;
`describe` sets the current room's description. No NPC/item spawning,
no room deletion β€” see
[ADR-0009](adr/0009-world-building-olc-command-surface.md) and
[PLAN-0009](plans/0009-world-building-olc-command-surface.md).
**Moderation/admin verbs** βœ…
(`boot`/`mute`/`unmute`/`announce`/`ban`/`unban`/`rolegrant`/
`rolerevoke`) are role-gated via `ICommandRegistry.RegisterWithRole` β€”
see [ADR-0005](adr/0005-security-role-model-and-moderation-commands.md)
Expand Down
Loading