Roadmap¶
Milestones and their exit criteria. This is the plan for turning Documentation/ into a running, forkable gamemode.
How to read this¶
Every milestone below ends with a working, tested, documented subset — never a broad, partially-working whole. This is 01_VISION.md's explicit non-goal: "Not feature-complete at v1." A milestone is not "done enough"; it meets every line of the Definition of Done or it is still in progress, regardless of how much of it works.
Tags follow Standards/04_GIT_WORKFLOW.md §5 exactly — this document is what each of those tags means, not a separate plan.
Two different senses of "done" appear below, and they are not interchangeable:
| Marker | Means |
|---|---|
| ✅ Complete | Every exit criterion met, including the ones needing a running game. |
| 🟡 Written | Code and tests are on main and meet the design, but no compiler or running client has ever confirmed it. |
| ⚪ Not started | Named and scoped here so the order and dependencies are on record, but no architecture doc, ADR, or code exists yet. |
Milestones 2 through 11 are currently 🟡. Milestones 12 onward are ⚪ — they cover every row
still marked ❌ in Legacy/01_FEATURE_INVENTORY.md's "Not present at all"
table, the same table Milestone 8 used as
its own exit criterion for the content that did have legacy precedent. OfflineTests/
(landed mid-Milestone-8)
compiles this repository's real source as an ordinary .NET class library and runs the real
UnitTests/ suite with dotnet test — so the code genuinely compiles and its logic is
genuinely tested — but no milestone has ever been opened in the real S&box editor, and no
live client has ever connected. See § Verification debt at the foot of
this document, which is a milestone gate in its own right and not a footnote.
Milestone 1 — Documentation ✅¶
Status: complete.
- [x]
Legacy/— behavioural specification of the original, defect list, dependency graph - [x]
Standards/— coding, naming, documentation, testing, git - [x]
ADR/— six accepted decisions covering module architecture, persistence, networking, items, and inventory - [x]
Architecture/— 14 documents, real signatures, engine-verified against S&box (July 2026) - [x]
Guides/and this roadmap
No .sbproj existed at the close of this milestone. That was deliberate — see
00_CONSTITUTION.md §4: documentation and design
precede implementation, not the reverse. Applejack.sbproj arrived with Milestone 2.
Exit criterion met: every Architecture/ document has been reviewed against the ADRs and
the Legacy defect list for consistency, and cites real, verified engine APIs rather than
assumed ones.
Milestone 2 — Core framework — v0.1.0¶
Status: 🟡 written. On main; tag not cut. Everything under Code/Core/ except
Persistence/ (Milestone 3) landed here, plus Applejack.sbproj, UnitTests/ and
.editorconfig. The two placeholder modules that proved boot ordering were deleted in
Milestone 5 once CharacterModule → Items/Inventory → Persistence/Core proved the same
guarantee with real modules.
The spine everything else attaches to. No gameplay yet.
Build:
- Code/Core/Modules/ — GameModule, the topological dependency resolver, the bootstrap file
(02_MODULE_MODEL.md)
- Code/Core/Services/ — IServiceRegistry, IServiceResolver
(03_SERVICE_REGISTRY.md)
- Code/Core/Events/ — IEventBus, IEvent, CancellableEvent
(04_EVENT_BUS.md)
- Code/Core/Logging/ — IGameLog
(12_LOGGING_AND_DIAGNOSTICS.md)
- Code/Core/Configuration/ — IModuleConfigurationContext
(05_CONFIGURATION.md)
- Result / Result<T> (13_ERROR_HANDLING.md)
- The .sbproj, UnitTests/ scaffold, .editorconfig
Exit criteria (in addition to the full Definition of
Done):
- A module with a declared dependency, a missing-dependency case, and a cyclic case each have
a passing/failing regression test per 02_MODULE_MODEL.md
§3.
- An event bus test proves a throwing handler does not block a second handler
(04_EVENT_BUS.md §6).
- Two trivial placeholder modules boot in the correct order, each logging through IGameLog.
- Zero gameplay code exists. If a reviewer finds gameplay logic in this milestone, that is a
scope violation, not a bonus.
Milestone 3 — Persistence — v0.2.0¶
Status: 🟡 written. On main; tag not cut. Code/Core/Persistence/ — both backends, the
migration chain, and PersistenceResult's named Corrupt case. The §6 conformance suite is
written as an abstract base class so any backend can be run through it, but only an
in-memory fixture backend is currently run through it: JsonPersistenceBackend needs
FileSystem.Data and HttpPersistenceBackend needs a live endpoint, so neither can be
exercised under dotnet test. The "passes against both backends" criterion below is
therefore not met — it needs the editor, and is part of the verification
debt.
Build:
- IPersistenceBackend, SaveDocument<T>, DocumentKey
(06_PERSISTENCE.md)
- JsonPersistenceBackend (FileSystem.Data)
- HttpPersistenceBackend, with the REST contract documented for operators
- The migration chain (ISchemaMigration<T>) and the write-through cache pattern
Exit criteria:
- The conformance suite from 06_PERSISTENCE.md
§6 passes against both backends.
- A fixture schema with two versions and one migration round-trips correctly.
- A corrupted local save file produces a named Corrupt result, not a crash.
- Guides/00_GETTING_STARTED.md's "configuring persistence" section is accurate against the
real implementation (guides are load-bearing documentation —
Standards/02_DOCUMENTATION_STANDARDS.md §6).
Milestone 4 — Character — v0.3.0¶
Status: 🟡 written. On main; tag not cut. Account/Character/CharacterId/
AccountModeration, CharacterStore, PermissionSet (flat exact-match, 12 tests),
ConnectionExtensions.GetCharacter(), and CharacterNetworkComponent — the first
[Rpc.Host] surface, walked against the exploit checklist at review. The milestone also
forced an architecture fix: GameModule.RegisterServices now takes an IServiceResolver,
so a module can resolve a DependsOn service at registration time.
The two exit criteria needing a running game — the save/disconnect/reconnect round trip and the two-client multiplayer test — are not met.
Build:
- Account and Character (08_DATA_MODEL.md §1)
- Character lifecycle: creation, spawn, IsInitialised, save, load
- Permission resolution (engine-agnostic rule type, per
Standards/03_TESTING_STANDARDS.md §3)
- The first real [Rpc.Host] surface and its exploit-checklist pass
(07_NETWORKING.md)
Exit criteria:
- A character created, saved, disconnected, and reconnected loads with identical state.
- Permission resolution across player/team/gang is unit-tested per
Standards/03_TESTING_STANDARDS.md §3.
- Multiplayer-tested with two connected clients, per the Definition of Done.
- Every [Rpc.Host] method added this milestone is walked against the exploit
checklist at review.
Milestone 5 — Items and Inventory — v0.4.0¶
Status: 🟡 written. On main; tag not cut. ItemDefinition with multi-level inheritance
resolution and cycle detection, ItemRegistry, the Edible/Equippable/Container
behaviour library (Contraband deferred), ItemInstance/ItemStateBag, the one shared
Inventory implementation behind both holder kinds, InventoryStore, and 19 real item
assets under Assets/Items/ drawn from the legacy source rather than invented. All four
named regression tests (D-03, D-04, D-09, D-10) and the forced-mid-transfer atomicity test
are written.
Two caveats carried forward: the .item JSON shape — specifically the polymorphic
Behaviours discriminator — is an unverified best guess (see
Assets/Items/README.md), and the "verified by actually adding
an item following the guide" criterion needs the editor.
Build:
- ItemDefinition, inheritance resolution, ItemRegistry
(09_ITEM_FRAMEWORK.md)
- The initial behaviour library: Edible, Equippable, Container
- ItemInstance, IInventory, IInventoryHolder
(08_DATA_MODEL.md §2–3)
- Character inventory and one world container type, sharing the one implementation
- A first batch of real item assets (10–20, not all 139) proving the authoring flow
Exit criteria:
- D03_PerItemStackLimitIsEnforced, D04_ItemWithUnresolvableBaseFailsLoudly,
D09_CharacterAndContainerInventoriesShareOneImplementation,
D10_TwoInstancesOfTheSameDefinitionAreDistinguishable all pass — the named regression
tests from Standards/03_TESTING_STANDARDS.md
§3.
- A transfer between character inventory and a world container is atomic under a forced
mid-transfer failure (test with a fake backend that fails on the second write).
- Guides/02_ADDING_AN_ITEM.md is verified by actually adding an item with no code, following
the guide exactly.
Milestone 6 — First playable — v0.5.0¶
Status: 🟡 every build item written; no exit criterion met. This is the current milestone, and the honest summary is that its code is finished and its definition is not — every exit criterion below requires a running client, which nothing in this project has ever had.
Written, on main:
| Build item | Landed as |
|---|---|
Interaction framework |
InteractionRunner (one singleton, keyed by CharacterId), the StillInRange/StillAlive/StillHasItem/TargetUnchanged/NotInterrupted condition library, InteractionComponent |
Minimal Economy |
IEconomyService/EconomyStore, CharacterEconomyComponent |
Minimal Ownership |
Ownership/OwnerRef/AccessGrant/AccessLevel, OwnershipStore |
The RequestPurchase flow |
PropertyStore.PurchaseAsync + DoorComponent.RequestPurchaseDoor, traced end to end in 07_NETWORKING.md §7 |
Minimal Ui |
IHudRegistry/HudRegistry/HudRoot.razor, with MoneyDisplay and InventoryPanel as its first registrants |
| The Guide's example module | Bulletin, built for real and booted from ApplejackBootstrap |
Also closed here: ItemDefinition.CreateUseInteraction and
CharacterInventoryComponent.RequestUseItem, the RPC entry point that finally connects an
item behaviour's built InteractionRequest to IInteractionRunner.TryStart over the wire.
Three deliberate gaps are named rather than hidden — RequestPurchaseDoor's exploit
checklist marks Distance N/A because no player-to-world-position binding exists yet;
IOwnershipService.HasAccess cannot resolve Team/Gang ownership because Jobs does not
exist yet; and whether [Sync] accepts a List<InventorySlotView> directly is unverified.
Each is recorded in the owning module's README.md under "Not responsible for".
The first milestone a person can actually play, however small.
Build:
- Interaction framework (10_INTERACTION_FRAMEWORK.md)
- Minimal Economy — money, a single RequestPurchase-shaped flow (a door, per
07_NETWORKING.md §7)
- Minimal Ownership — the Ownership record, one ownable entity type (doors)
- Minimal Ui — a HUD showing money and a basic inventory panel
(11_UI_ARCHITECTURE.md)
- Guides/01_WRITING_A_MODULE.md's example module, built for real (not just documented)
Exit criteria: - Two players can join a server, pick up an item, buy a door, store something in it, and reconnect without losing state. - Idle bandwidth per player measured near zero, per 07_NETWORKING.md §9 — the direct regression test for D-11. - A short recorded or narrated playtest exists, referenced from the milestone's closing commit.
Milestone 7 — Remaining gameplay¶
Status: 🟡 written. On main; tag not cut. Full Jobs, Needs, Crime/Law, Chat,
Commands, and the rest of World — each landed as its own small commits, per this
milestone's own exit criterion below.
Everything in Legacy/13_DEPENDENCY_GRAPH.md §4
not yet built: full Jobs (group/gang/team, demotion, mutiny), Needs (hunger, stamina,
thirst as configuration), Crime and Law (warrants, arrest, laws), Chat (proximity,
channels), full Commands, the rest of World (containers beyond the first type, locks,
seals).
Exit criteria: each system meets the Definition of Done independently and lands as its
own set of small commits — this milestone is explicitly not "one big merge"; per
00_CONSTITUTION.md §4, Small → Test → Review →
Expand applies within it exactly as it does everywhere else. No version tag is cut until the
whole milestone closes, but nothing here waits for the others to start.
Milestone 8 — Content migration¶
Status: 🟡 written. On main; tag not cut. All 116 real legacy items and 16 jobs
authored as Assets/, every item in Legacy/01_FEATURE_INVENTORY.md given a disposition —
this milestone's own exit criterion below, met.
The 139 legacy items, all jobs, and all recipes, authored as assets per Guides/02_ADDING_AN_ITEM.md — a content task, not an engineering one, deliberately deferred until here per ADR-0004.
Exit criteria: every item in Legacy/01_FEATURE_INVENTORY.md has a disposition — migrated, deliberately dropped (with a one-line reason), or deliberately redesigned (with an ADR or 01_VISION.md §3 citation).
Milestone 9 — Polish and hardening¶
Status: 🟡 partial. On main; tag not cut. The exploit-checklist pass across every
[Rpc.Host] entry point and closing Legacy/'s > **Gap:** markers are both done (zero
remain). Performance review and stress testing are named, not done — both need a live S&box
server connected to real clients, an environment limitation standing since Milestone 2; see
Milestone 10 and Verification debt below.
Performance review (allocation, network bytes, tick cost per
00_CONSTITUTION.md's Definition of Done), stress
testing at realistic player counts, a full exploit-checklist pass across every [Rpc.Host]
entry point accumulated so far, and closing any > **Gap:** markers left in Legacy/.
Milestone 10 — v9.8.0 — Release-prep¶
The repository is public. ADR-0008 records
the decision to go public during this milestone — ahead of the original plan below — in order
to host a public documentation website. v1.0.0 no longer marks "goes public"; that already
happened. It now means the first stable, feature-complete release, independent of visibility.
This milestone still tags v9.8.0, not v1.0.0: going public is a visibility change, not a
claim that everything below is finished — the live-server-gated items are still named, not
faked. See Standards/04_GIT_WORKFLOW.md §5
for the full tag table.
Superseded framing, kept for context. This milestone was originally planned around "Reserving
v1.0.0" — the idea thatv1.0.0marks the repository (then private) going public, and that pre-launch tags use thev9.xrange specifically so they can never be confused with that moment. ADR-0008 changed that plan; thev9.xrange is kept for its own sake (renumbering released tags is worse than an explanation that's evolved), but no longer marks a countdown to public launch.
Exit criteria, split by what this environment can actually verify:
- Done this pass: a way to become an admin at all (CharacterConfiguration's bootstrap
list — nothing could grant a permission before this), /give (closing
Guides/02_ADDING_AN_ITEM.md's own named blocker on spawning
a newly-authored item), the Equippable/Container field documentation gap, a real
Guides/04_SERVER_ADMIN_GUIDE.md for the "stand up and
reconfigure a fork" persona this milestone's exit criterion actually names, and the
nullability warnings-as-errors policy enforced for real in OfflineTests/
(tasks.md #37).
- Also done this pass: a real, functional admin panel (/grant, /revoke, /kick, all
reached through AdminPanel.razor) and dev menu (item/job browser+spawn, module boot-order
status, a live config inspector, a log tail — DevMenuPanel.razor), this codebase's first
interactive UI and its first on-demand-panel pattern
(Architecture/11_UI_ARCHITECTURE.md §7,
Code/Admin/README.md). Explicitly, deliberately deferred rather
than faked: teleport, noclip, and spectate all need a Character↔pawn Transform binding that
is completely absent from this codebase (not partial — zero code anywhere reads or writes a
player's world position), and real ban records/mute enforcement need their own design pass.
Both are named, scoped follow-up work, not silently dropped scope.
- Still blocked on a live S&box server, unchanged by this pass: Milestone 6's playtest exit
criteria (two players, idle-bandwidth measurement, a recorded session) and Milestone 9's
performance/stress-testing halves. Every milestone above is written, not all of them are
closed — see the ✅/🟡 status convention at the top of this document.
- A GitHub Release is not cut for v9.8.0 — per
Standards/04_GIT_WORKFLOW.md §5,
Releases are for major versions, public previews, RCs, and v1.0 itself. The repository
being public as of ADR-0008 doesn't change
that — a Release is still for announcing something to an audience, and v9.8.0 isn't that
moment.
- "Without reading Code/" replaces the earlier, more literal "using only Guides/ and
Assets/" — Guides/00_GETTING_STARTED.md itself routes readers into Architecture//
ADR//Standards/ for orientation, which is fine (documentation, not implementation) but
was in literal tension with the old wording.
Milestone 11 — Player presence & admin tools — v9.9.0¶
Status: 🟡 every build item landed; no exit criterion formally verified. On main; tag not
cut. All six build items below are landed: Applejack.Movement (PawnComponent,
IPawnLocator), DoorComponent's real distance checks, the admin movement commands
(/goto, /bring, /tp, /noclip, /spectate, /unspectate), the admin player-list panel
(PlayerListPanel.razor), Account persistence (AccountStore/IAccountService,
accounts/{steamId}), and real ban records + mute enforcement (AccountModeration/
ModerationStatus, /ban//unban//mute//unmute, enforced in
CharacterNetworkComponent.RequestCreateCharacter and ChatRouter.SendAsync). Every
engine-agnostic rule has a real unit test written; dotnet test now runs to completion
(312/312 passing) — the compile breakage previously flagged here (a concurrent player-menu
workstream, plus a since-found second break from the whitelist/obsolete-API migration leaving
OfflineTests/Shims/ out of sync on [AssetType], SteamId.ValueUnsigned, and Http's
cancellationToken parameter name) is resolved — see Verification debt below. Scoped following
Milestone 10's admin panel/dev menu,
which named
three things it deliberately did not build — teleport/noclip/spectate and real ban records —
both blocked on "a Character↔pawn Transform binding that doesn't exist anywhere in this
codebase" (Code/Admin/README.md). ADR-0007
is that binding's design; Architecture/22_PLAYER_PRESENCE_AND_ADMIN_TOOLS.md
is its concrete shape.
Two things turned out bigger than "add a Transform field" once actually scoped: this codebase
has no physical player presence at all yet — no .scene, no .prefab, empty
StartupScene/MapStartupScene in Applejack.sbproj — so this milestone includes building a
minimal reference player prefab and spawn point, not just the binding layer; and Account has
no persistence of any kind yet — no store, no schema, no service — so real ban records need
that stood up first. Neither has legacy precedent to preserve: a full search of
Documentation/Legacy/ found no admin teleport, no spectate, no admin overview map, and no
server ban/mute mechanism (only a job/item _Blacklist, a different system) — everything here
is new design, which is why it has its own ADR rather than a legacy citation.
Build, each landing as its own independent set of small commits — per this milestone's own
exit criterion below, none blocks another from starting:
- Applejack.Movement — PawnComponent, IPawnLocator, the reference player prefab and one
spawn point (22_PLAYER_PRESENCE_AND_ADMIN_TOOLS.md §1)
- Real distance checks on DoorComponent's purchase/lock RPCs, closing the "Distance: N/A"
gap named since Milestone 6 (§2)
- Admin movement commands — /goto, /bring, /tp, /noclip, /spectate, /unspectate,
each behind its own new permission (admin.teleport, admin.noclip, admin.spectate)
(§3)
- An admin player-list panel — name, live position, distance from the viewing admin; a
sortable list, not a textured minimap (no per-map coordinate convention exists to build one
against yet) (§4)
- Account persistence, standing up for the first time: AccountStore, IAccountService,
the accounts/{steamId} schema
- Real ban records (AccountModeration becomes reason/issuer/timestamps, not a bare enum) and
mute enforcement in ChatRouter.SendAsync, plus /ban, /unban, /mute, /unmute
(§6)
Exit criteria:
- Every build item above meets the full Definition of Done
independently and lands as its own commits — explicitly not one big merge, per
00_CONSTITUTION.md §4, the same rule Milestone 7
held itself to.
- Character gains no engine-typed field — reviewed against ADR-0007's Decision directly.
- Every new [Rpc.Host]-reachable path (teleport/noclip/spectate/ban/mute, all routed through
the existing CommandDispatchComponent.RequestExecuteCommand, per
§3's
refinement over ADR-0007's illustrative sketch) is walked against the exploit checklist.
- The four unverified engine assumptions named in
22_PLAYER_PRESENCE_AND_ADMIN_TOOLS.md §5
are either confirmed or still explicitly named as open — never silently assumed correct.
- Like every milestone since Milestone 6, the parts needing a running client (does the pawn
actually move on someone else's screen, does spectate's camera reparent work, does the
reference prefab even compile as an S&box addon) are not met by this pass and are not
claimed to be — see Verification debt below, unchanged by this milestone.
Milestones 12–18 — Legacy content: everything still marked ❌¶
Status: Milestone 12 🟡 every build item landed, no exit criterion formally verified against a live client; Milestones 13–18 ⚪ not started. Sequenced by dependency and leverage, not by list order — see the sequencing note before each milestone for why it sits where it does. Every item here is dispositioned New in Legacy/01_FEATURE_INVENTORY.md: legacy gave none of it real behaviour to preserve, so this is design work, not a port. One partial exception is named explicitly in Milestone 12 below (Vehicles).
Each milestone follows the same per-system pattern Milestones 7 and 8 already established:
Architecture doc (and an ADR where the decision is genuinely costly to reverse) written before
the module, OfflineTests coverage, content assets, docs sync — landing as its own small
commits per 00_CONSTITUTION.md §4, never one merge
per milestone. The Plugin System work tracked separately in the project's task log (ADR-0010,
Phase 2 — the in-repo loader — complete as of 2026-07-31; Phase 3, Library-assembly plugins,
gated behind an editor spike) does not gate any of this — that work is for third-party
extension without a recompile; everything below is first-party gameplay and ships as an
ordinary module under Code/, the same way Jobs, Crime, and Vehicles already did.
Milestone 12 — Independent extensions — v9.10.0¶
Status: 🟡 every build item landed and merged to main; live-client verification still
open. Built as four genuinely parallel, independently-reviewed worktrees, exactly as the
sequencing note below anticipated — each landed as its own PR, independently verified
(dotnet test run fresh against each branch, diff read in full) before merging, main green
throughout (398/398 passing, 0 warnings, as of the last merge):
- Vehicles: entering, exiting, and driving — PR #9.
- Thirst, Temperature, Disease — PR #7.
- Weapon attachments and jamming — PR #11.
- Target resolution by name (wired to /pm) — PR #8.
Sequencing note: these four items share no state and depend only on modules that already
exist (Needs, Items, Movement, Character). Nothing here waits on anything else in
Milestones 12–18, so this is the one milestone genuinely safe to build as four parallel
worktrees, mirroring the pattern already used for Plugin System Phase 1a/1b/1c.
Build:
- Vehicles: entering, exiting, and driving. The one item on this list with real legacy
precedent for the surrounding feature — ownership, purchase, and the key-as-item design are
already built (ADR-0009). Driving itself has no
legacy precedent either (legacy never had a Transform/pawn binding at all), and ADR-0009
named its own unblock condition explicitly: "Movement has a second real consumer beyond
admin teleport/spectate." Movement (PawnComponent/IPawnLocator) landed in Milestone 11 —
the condition is met. See ADR-0012 (Accepted). This
pass supports exactly one driver seat and one passenger seat per vehicle, not a general
seat-count system — see that ADR's Notes.
- Thirst, Temperature, Disease — extends NeedsConfiguration with three more
NeedDefinition entries, mirroring Hunger's existing pattern
(14_NEEDS_FRAMEWORK.md) exactly. No new module.
- Weapon attachments and jamming — extends the Equippable item behaviour
(09_ITEM_FRAMEWORK.md). No new module.
- Target resolution by name — not on the original list, but cheap and load-bearing: it
currently blocks /warrant, /pm, and offline ban/mute, and will block Milestone 17's
fines/records commands if left for later. A small Character service extension, not a new
module.
Exit criteria:
- Each of the four lands independently and meets the full Definition of Done on its own —
explicitly not one merge, per Milestone 7's own precedent for this phrasing. Met — see
Status above.
- A vehicle can be entered, exited, and driven by its owner or anyone holding an AccessLevel
of Passenger or Drive; every new [Rpc.Host] surface this adds is walked against the
exploit checklist. Code-complete and exploit-checklisted; not yet exercised against a real
client — same live-verification gap as Milestone 11's own admin tools, see Verification
debt below.
- Guides/02_ADDING_AN_ITEM.md documents adding an attachment the same way it documents adding
any other item. Met.
Milestone 13 — Character: creation, multiple characters, biography, attributes, skills — v9.11.0¶
Sequencing note: deliberately front-loaded ahead of Milestones 14–18 even though nothing in Milestone 12 depends on it. Medical (15), Crafting (16), Licences (14), and Criminal Records (17) all want to hang state off "a specific character's" attributes, skills, or biography. Building those four first against today's Account↔Character 1:1 model and retrofitting Character afterward would mean migrating four modules instead of giving one clean surface up front. This is the one milestone in this batch that reshapes an existing module rather than adding a new one, which is why it gets its own ADR rather than an in-place extension.
Build:
- Character creation, and multiple characters per account with selection and deletion —
legacy's own gap was total: "a character is a Steam account"
(Legacy/01_FEATURE_INVENTORY.md). See
ADR-0011 (Proposed) for the
Account↔Character cardinality decision this forces.
- Biography and description fields.
- Attributes and skills, and the first real consumer of them (even if it's just Milestone 16's
crafting success/speed check).
- New architecture doc: 25_CHARACTER_CREATION_AND_PROGRESSION.md, plus updates to
08_DATA_MODEL.md §1.
Exit criteria:
- A player can create, select, and delete a character; every existing system that currently
assumes "the" character for a connection (persistence, the admin panel, CommandContext)
is reviewed and updated to resolve the active character explicitly, not implicitly.
- Character gains no new engine-typed field, matching the discipline
ADR-0007 already set for Movement.
- Save/load round-trips a character with biography, attributes, and skills populated.
Milestone 14 — Economy and property extensions — v9.12.0¶
Status: 🟡 every build item landed and merged to main; live-client verification still
open. Built as four genuinely parallel worktrees, same pattern as Milestone 12 — ADR-0013
(Housing's open decision) resolved first, then all four launched together, each landed as its
own PR, independently verified (dotnet test run fresh against each branch, full diff read)
before merging, main green throughout (581/581 passing, 0 warnings, as of the last merge):
- Housing (property groups) — PR #19,
stacked on the ADR resolution itself,
PR #15.
- Banks — PR #16.
- Vendors / NPC shops — PR #17.
- Licences — PR #18.
Sequencing note: four independent additions to already-built modules (Economy,
Ownership, World, Items, Interaction), safe to build in parallel once Milestone 13 lands
(Licences references Character's new permission/attribute surface; the other three don't
strictly need it but are grouped here for a single milestone of "things the economy and world
can now sell or gate").
Build:
- Banks — legacy built the enabling hook and never the bank
(Legacy/01_FEATURE_INVENTORY.md). New doc
26_BANKING.md.
- Vendors / NPC shops — deliberately scoped as a static, ownerless shop entity reusing
WorldContainer's shape, no AI. Ships here, well before Milestone 18's NPC framework, on
purpose — nothing about a shop actually requires a person behind the counter. New doc
27_VENDORS.md.
- Licences — never existed in legacy at all. New doc 28_LICENCES.md.
- Housing — legacy had nothing beyond door ownership. This is explicitly the "master/slave
door linking → property groups" follow-on Milestone 7's World system already named and
deferred, not a new concept invented from nothing. See
ADR-0013 (Accepted). New doc 29_HOUSING.md.
Exit criteria:
- Each of the four meets the Definition of Done independently and lands as its own commits.
Met — see Status above.
- A player can open an account at a bank, buy from a static vendor, hold a licence that gates
at least one existing check (e.g. carrying a weapon category), and own a multi-room property
as one linked group rather than N independent doors. Code-complete and exploit-checklisted;
not yet exercised against a real client — same live-verification gap as Milestone 12's own
vehicles/needs work, see Verification debt below. Licences gates weapon equip specifically
(CharacterInventoryComponent.RequestUseItem's Equippable flow), since no existing
pickup/possession check existed to hook into instead — see 28_LICENCES.md §4.
Milestone 15 — Medical — v9.13.0¶
Sequencing note: after Milestone 12 so Thirst/Disease exist as real needs axes to attach injury/illness severity to, and after Milestone 13 so severity can optionally scale against attributes. Reuses Crime's existing knockout/incapacitate primitives rather than inventing a second one.
Build: a real Medical system replacing legacy's drugs items + sleep regen
(Legacy/01_FEATURE_INVENTORY.md) — injury
states, treatment items, and a revival/incapacitation path that composes with Crime's
existing Incapacitate primitive rather than duplicating it. New doc
30_MEDICAL_FRAMEWORK.md.
Exit criteria:
- A character can be injured, treated, and revived through the new system end to end.
- The new injury states compose with Crime's existing knockout/incapacitate rather than
introducing a second, competing "is this character down" concept.
Milestone 16 — Crafting — v9.14.0¶
Sequencing note: genuinely unblocked, not just next in line. ItemDefinition.BatchSize and
JobDefinition.CanManufacture were built in Milestone 8 specifically for this and have sat
unused since; the missing "spawn the result at the crafter's position" gap that kept
/manufacture a purchase instead of real crafting is closed by Milestone 11's Movement module.
Build: a real crafting system — recipes, a timed Interaction (reusing the primitive
Interaction already provides, not a new one), and a result that spawns at the crafter's
position via IPawnLocator — replacing /manufacture's "purchase dressed as crafting"
( Legacy/01_FEATURE_INVENTORY.md ). Optionally gated
by Milestone 13's skills, if that lands first. New doc 31_CRAFTING_FRAMEWORK.md.
Exit criteria:
- A player can craft a real recipe through a timed interaction and receive the result in the
world at their own position, cancellable the same way any other Interaction is.
- JobDefinition.CanManufacture gates who may craft what, closing the gap named at Milestone 8.
Milestone 17 — Evidence, fines, criminal records, bail, trials, sentencing — v9.15.0¶
Sequencing note: split into three waves within one milestone, smallest first, because the three pieces are genuinely different sizes. All three need Milestone 12's target-resolution.
Build:
- Wave 1 — Fines and criminal records. Small, additive to Crime, Economy, and
Character's new biography surface. New doc 32_CRIMINAL_JUSTICE_EXTENSIONS.md.
- Wave 2 — Evidence. Needs a real collection/physical-item mechanic on top of Items and
Inventory — an evidence item is not just another Container entry, it needs a chain-of-
custody concept Items doesn't have yet.
- Wave 3 — Bail, trials, sentencing. The heaviest piece in this entire batch: a real
multi-party "court" state machine, where legacy's only precedent is a single fixed 300-second
sentence with no trial at all. See
ADR-0015 (Proposed) for the state-machine shape.
New doc 33_TRIALS_AND_SENTENCING.md.
Exit criteria:
- Each wave meets the Definition of Done independently; Wave 3 does not block Waves 1–2 from
shipping first.
- A fine or criminal record can be issued to a named, currently-connected player using
Milestone 12's target-resolution, not a raw SteamId.
- A full bail → trial → sentencing cycle completes end to end for at least one crime.
Milestone 18 — NPC framework — v9.16.0¶
Sequencing note: last, deliberately. Legacy left only a stub base class — no AI, no pathing, nothing to preserve. This needs S&box pathing/navmesh, which is completely unverified in this codebase, the same category of risk the Verification Debt section already tracks for other engine-facing unknowns. Nothing else in Milestones 12–17 requires it — Milestone 14's Vendors was deliberately scoped as a static entity for exactly this reason. Once this lands, Milestone 14's Vendors may be upgraded to NPC-driven as a pure enhancement, not a rebuild.
Build: a real NPC framework — pathing, basic behaviour, and at least one real consumer
(a patrol NPC, or an NPC-driven Vendor upgrade). See
ADR-0014 (Proposed) for the AI/pathing-approach decision this
needs before any code. New doc 34_NPC_FRAMEWORK.md.
Exit criteria:
- At least one NPC exists in a running scene, paths between two points, and is exercised by a
real gameplay consumer — not a standalone tech demo.
- This is the last row in
Legacy/01_FEATURE_INVENTORY.md's "Not present at all" table
to close. Once it and the remaining Verification Debt items below are both settled, v1.0.0
— "the first stable, feature-complete release" per
Standards/04_GIT_WORKFLOW.md §5 — is
achievable. This roadmap does not pre-declare that tag; it is cut when the criteria are
actually met, not before.
Verification debt¶
Milestones 2 through 6 were written without a compiler. That is a deliberate consequence of
the development environment, not a standards lapse — but it is debt, it compounds, and it was
the single largest risk to this project. OfflineTests/ (Milestone 8) closed the largest
piece of it; what's below reflects that, not the original, fully-uncompiled state.
What OfflineTests/ actually settled: it compiles Code/'s engine-agnostic source
(everything except thin engine-facing Components — see that project's own .csproj for the
exact exclusion list) plus the real UnitTests/ suite into an ordinary .NET class library,
with hand-written shims standing in for the S&box engine surface. dotnet test genuinely
runs — 258/258 passing as of this milestone — and it has already caught one real bug the
uncompiled milestones before it could not have (a lambda-parameter-shadowing typo in
Edible.cs's OnComplete, found and fixed mid-Milestone-8). This means "the project has
never been compiled" and "dotnet test has never been run" are no longer true — they were
true through Milestone 7, and the record here now reflects when that changed.
What still hasn't happened, unchanged by OfflineTests/:
- No S&box editor has ever opened
Applejack.sbproj.OfflineTests/proves the code compiles as ordinary C#; it says nothing about whether it compiles as an S&box addon — engine attributes,GameResourceserialization, and hot reload are all unverified.Assets/Prefabs/PlayerPawn.prefabandAssets/scenes/main.scene(withApplejack.sbproj'sStartupScene/DedicatedServerStartupScenenow pointing at the latter) exist as of this pass specifically to make that first real load testable via dedicated-server tooling on a machine without editor-capable graphics — see Assets/Prefabs/README.md's own FORMAT UNCERTAINTY section for exactly what in those two files is hand-authored-from-public-example rather than confirmed. - No client has ever connected. Every
[Rpc.Host]method has been walked against the exploit checklist by reading, never by exercising over a real connection. OfflineTests/is a stand-in, not the real thing. Delete it once the real editor-generatedUnitTests/project is available and verified on a machine with the editor — see that project's own header comment.dotnet testcompiles and passes again (312/312) as of thefix/whitelist-obsolete-apis-and-spawnbranch. Two separate breaks accumulated and are now both resolved: a concurrent player-menu workstream that addedApplejack.Characters.Ui/Applejack.Commands.Ui/Applejack.Law.Uinamespaces andCode/Ui/PlayerMenuRootComponent.cswithout the matchingOfflineTests.csprojexclusion (fixed by excluding the file, same pattern asDoorComponent.cs); and a second break from the whitelist/obsolete-API migration (db7c83a), which movedCode/to[AssetType],SteamId.ValueUnsigned, andHttp.RequestAsync(..., cancellationToken:)without updatingOfflineTests/Shims/to match —GameResource.cs,SteamId.cs, andHttp.csnow define/name those to compile again. Milestone 11's own unit tests have now actually executed end to end, not just been hand-checked for absence of new errors.ModuleRunner.Shutdown()is now called. The scene-teardown counterpart hook this section previously said was unconfirmed is now confirmed against the real engine source at/home/kyle/Projects/sbox-public:GameObjectSystem(the base class ofApplejackBootstrap) implementsIDisposable(engine/Sandbox.Engine/Scene/GameObjectSystem/GameObjectSystem.cs:11), andScene.ShutdownSystems()(engine/Sandbox.Engine/Scene/Scene/Scene.System.cs:13-32, called fromScene.DestroyInternal()atScene.cs:150, itself called fromScene.Destroy()atScene.cs:132-135) callsDispose()on every registeredGameObjectSystem, wrapped in a per-system try/catch.ApplejackBootstrapnow overridesDispose()to callModuleRunner.Shutdown()beforebase.Dispose()— seeCode/Core/Modules/ApplejackBootstrap.cs's header comment and itsDispose()override for the full citation and ordering rationale.GameObject.SetParent's existence and replication are now confirmed (Milestone 12, ADR-0012's vehicle-driving work), the same check-source-first discipline as theModuleRunner.Shutdown()entry above:GameObject.SetParent(GameObject value, bool keepWorldPosition = true)exists (engine/Sandbox.Engine/Scene/GameObject/GameObject.cs:487-512), acceptsnullto re-parent back to the scene root (value ??= Scene, same file), and — for aNetworkSpawn()'dGameObject— replicates the change to every observing client automatically via an internal[Rpc.Broadcast] Msg_SetParentcall (GameObject.cs:511, handled bySetParentFromNetworkatGameObject.cs:517-536, the RPC itself inGameObject.Network.cs:314-340). No custom[Sync]mirror is needed.GameObject.LocalPosition/LocalRotation(GameObject.LocalTransform.cs:10-30) andTransform.PointToWorld(used throughoutengine/Sandbox.Engine/, e.g.Editor/Gizmos/Hitbox/Hitbox.cs:193) are confirmed too.Code/ Movement/PawnComponent.cs's newEnterVehicle/ExitVehiclemethods use all four. What is not confirmed: runtime behaviour with a second connected client actually watching a reparent happen (interpolation smoothness, whether a client-side prediction/camera system needs its own awareness of the switch) — the API existing and broadcasting is settled, the live-session experience of it is not.- A single suspend-movement bool is confirmed insufficient on its own (same Milestone 12
dig,
PawnComponent.IsInVehicle, mirroringIsNoclipping's open question from Milestone 11): the real stock movement controller,PlayerController, exposesUseInputControls(PlayerController.Input.cs:6), andOnFixedUpdate/DefaultControlsgenuinely gatesInputMove()/InputJump()/ducking behind it (PlayerController.DefaultControls.cs:75) — so a bool does stop new input, confirming the shapeIsNoclippingalready assumed. ButPlayerController.PrePhysicsStep()(PlayerController.cs:185-194) still applies gravity/velocity/ground-friction physics regardless of that flag (onlyIsProxyskips it), so residual velocity is not zeroed by the flag alone. Fully freezing a pawn, if that matters once this is exercised in a real session, needs eitherComponent.Enabled = falseonPlayerControlleritself or an explicit velocity reset —IsInVehicleships anyway (real, useful signal for animation/UI state and for a futurePlayerControllerintegration to read), with this limitation named rather than silently assumed away. Also resolves, by the same finding, whatIsNoclipping's own still-open bullet above was asking:UseInputControlsis the settable mode flag that bullet speculated might exist, though whether it is sufficient for noclip specifically (which additionally needs collision suppressed, not just input) is unconfirmed here and remains that bullet's own open question, not resolved by this one.
Specific known-unverified assumptions, each already flagged at its own site: the .item/
.job asset JSON shape's polymorphic Behaviours discriminator; [Sync] support for a
List<InventorySlotView>; CharacterStore's internal visibility needing
InternalsVisibleTo wiring in a generated .csproj; SteamId construction in tests, and
(Milestone 10) whether SteamId.Value is really a ulong SteamID64 the way
CharacterConfiguration's admin bootstrap and the OfflineTests/Shims/SteamId.cs shim both
assume; and
(Milestone 10) whether Applejack.sbproj's Compiler.WarningsAsErrors: "true" is actually
interpreted as "treat every warning as an error" by the S&box compiler wrapper, or whether
CS1591 (missing XML doc) can even fire given no GenerateDocumentationFile-equivalent key
is visible anywhere in the .sbproj schema; and (Milestone 10's admin panel/dev menu, this
codebase's first interactive UI) Connection.All/Connection.Kick() as the API for finding
and disconnecting a specific player's connection, Input.Pressed(string) as a named-action
per-frame keybind check, onclick="@(() => ...)" as S&box Razor's real click-binding syntax,
and toggling a child GameObject.Enabled as how a panel's visibility is actually shown/hidden
— all four are the most plausible real S&box surface for what they're used for, at the same
confidence level this codebase already extends to Rpc.Caller.SteamId and [Sync] on a
List<T>, none independently confirmed against engine source this pass.
- 2026-07-31 session — closing the character-binding gap, and a new blocker found while
doing it. Investigating a report that "everything except movement/UI" fails on a real
rp_downtown(thieves.rpdowntown3t) session traced the cause toPlayerPawn.prefabnever carryingCharacterNetworkComponentor any of its sibling per-character components (InteractionComponent,CharacterEconomyComponent,CharacterNeedsComponent,CharacterInventoryComponent,CharacterBankAccountComponent) — every module keyed offGameObject.Components.Get<CharacterNetworkComponent>()had nothing to find. All six are now added (Milestone 11'sPlayerPawn.prefab), none carry[Property]fields so the JSON entries needed no new format guesses. Separately,PawnComponent.CharacterIdnever left its prefab default ofGuid.Emptyafter spawn —IPawnLocator'sRegister/Findwas always keyed on a value no[Rpc.Host]caller could ever match — closed via a newOnUpdatepoll-and-rebind onPawnComponentitself (ADR-0007 Addendum,Code/Movement/PawnComponent.cs). - GameObject-reference and Component-reference
[Property]JSON shapes are now confirmed, not guessed: pulledFacepunch/sbox-hc1's currentAssets/prefabs/player/player.prefabandAssets/prefabs/hud.prefabdirectly (gh api/curl, this session). A same-file[Property] GameObjectreference serializes as{"_type": "gameobject", "go": "<the target GameObject's own __guid>"}(player.prefab'sHead/CameraObject/Targetfields, cross- checked against the referenced__guidappearing as a realChildrenentry elsewhere in the same file). A[Property]typed as a specificComponentsubclass serializes as{"_type": "component", "component_id": "<that component's own __guid>", "go": "<the GameObject it lives on>"}(player.prefab'sBody/HealthComponent/Playerfields). Used to wireCharacterSelectRootComponent.SelectPanelandCharacterSelectPanel.CharacterNetworkintoPlayerPawn.prefabthis same session — the character-creation/selection screen a player actually needs to reachRequestCreateCharacternow has a real, scene-placed path. Also confirmsSandbox.ScreenPanelas a sibling component (not a parent/ancestor) alongside aPanelComponent-derived type on one GameObject is the real, current pattern (hud.prefab's root carriesFacepunch.UI.MainHUD+Sandbox.ScreenPanel+Facepunch.UI.DeveloperMenu+ a secondSandbox.ScreenPanel+Facepunch.Crosshair, all as siblings) — resolves this section's own former "toggling a childGameObject.Enabled... none independently confirmed" line for the reference-shape question specifically (visibility-toggle-via-Enableditself is still unconfirmed, a separate question). - New blocker found, not yet fixed:
Code/Ui/HudRoot.razorandCode/Ui/PlayerMenu.razorboth have their actual per-module content-instantiation call (TypeLibrary.Create<Panel>(element.PanelType)/TypeLibrary.Create<Panel>(tab.PanelType)) commented out, each citing "a compiler whitelist violation." Grepping the codebase confirms this isn't cosmetic: everyRegisterHudElement<T>/RegisterMenuTab<T>call site across every module (Code/Economy/EconomyModule.cs,Code/Ui/UiModule.cs's own Info/Changelog/Credits tabs, etc.) is also commented out, soIHudRegistry.RegisteredElementsandIMenuRegistry.RegisteredTabsare unconditionally empty today. Scene-placingHudRoot/PlayerMenuright now would render a real but completely empty shell (aScreenPanelwith zero HUD elements and a nav rail with zero tabs) — not the fix "UI doesn't fully work" needs. Checked~/Projects/sbox-public'sSandbox.Internal.TypeLibrary.Create<T>(Type, object[])(engine/Sandbox.Reflection/TypeLibrary/Instancing.cs:53-67) directly: the engine's own implementation is a plainIsAllowedTypecheck plusActivator.CreateInstance, nothing that obviously bans a whitelisted addon type — but whether the addon-side Roslyn compiler restrictions genuinely block calling this generic method with a runtimeTypeargument from game code (as opposed to the engine's own internal use of it) is a separate, closed-source compiler-analyzer question this public engine mirror can't settle. Not fixed this session — flipping it blind, under this project'sWarningsAsErrors: true, risks a hard compile failure with no local way to verify against the real S&box Roslyn analyzer. Needs either a real editor/compiler session, or a second public sample demonstrating a workingTypeLibrary.Create<Panel>(Type)call from addon (not engine) code before re-enabling. - Deliberately deferred this session:
AdminToolsRootComponent's three panel references (AdminPanel/DevMenuPanel/PlayerListPanel) andPlayerMenuRootComponent.Menu— same confirmed reference shape asCharacterSelectRootComponentabove, could be wired the same way, but lower priority than closing the character-creation path, andHudRoot/PlayerMenuscene-placement was skipped pending theTypeLibrary.Createquestion above. Also deferred: placing map-anchored gameplay entities (DoorComponent/BankComponent/VendorComponent/HousingListingComponent/WorldContainerComponent) intoAssets/scenes/main.scene— no reference-shape blocker for these (none take a GameObject/Component[Property]), purely scoped out of this pass; seeAssets/Prefabs/README.md/Assets/scenes/README.mdfor the current per-file state.
Opening the real S&box editor for the first time still takes precedence over new feature
work, exactly as before: compile, run dotnet test (both the real editor-generated project
and, while it still exists, OfflineTests/), fix what falls out, and only then attempt
Milestone 6's playtest criteria. A green build in the real editor is a prerequisite for the
playtest, not a parallel activity.
What is deliberately not on this roadmap¶
Per 01_VISION.md §5: Garry's Mod addon/save compatibility, DarkRP-style job-spam economies, and single-server-only assumptions anywhere in the design. If a milestone's implementation starts trending toward any of these, that is a signal to stop and re-read 01_VISION.md, not a scope addition to accept quietly.