Architecture Overview¶
This document is the map. It defines the layer diagram every other Architecture/ document
assumes, states the one dependency rule that keeps the codebase from becoming
ply.cider
again, and lists the engine surface the rest of this folder relies on — verified, not assumed.
If you read one page before writing code, read this one.
1. The layers¶
graph TD
ENGINE["S&box engine<br/>GameObject · Component · Scene · GameResource · Networking"]
subgraph Core["Applejack.Core — depends on nothing but the engine"]
LOG[Logging]
CFG[Configuration]
EVT[Event bus]
SVC[Service registry]
PER[Persistence]
MOD[Module lifecycle]
end
subgraph Modules["Gameplay modules — depend on Core, and on each other only by declared dependency"]
CHAR[Character]
ITEMS[Items]
INV[Inventory]
INTER[Interaction]
OWN[Ownership]
ECON[Economy]
JOBS[Jobs]
NEEDS[Needs]
CRIME[Crime]
LAW[Law]
CHAT[Chat]
CMD[Commands]
WORLD[World]
end
UI["UI — reads other modules' interfaces, owned by none of them"]
ENGINE --> Core
Core --> Modules
Modules --> UI
Core -.-> UI
classDef core fill:#dbe9ff,stroke:#3366cc
classDef mod fill:#e6ffed,stroke:#2c8a3d
class LOG,CFG,EVT,SVC,PER,MOD core
class CHAR,ITEMS,INV,INTER,OWN,ECON,JOBS,NEEDS,CRIME,LAW,CHAT,CMD,WORLD mod
Four layers, three rules:
- Engine is S&box itself. Nothing in this project wraps it —
00_CONSTITUTION.md §3 forbids a
compatibility layer over
GameObject,Component,SceneorGameResource. - Core (
Applejack.Core) owns the cross-cutting concerns every module needs — logging, configuration, the event bus, the service registry, persistence, and the module lifecycle itself. Core depends on the engine and nothing else in this project. - Modules are gameplay: Character, Items, Inventory, Interaction, Ownership, Economy, Jobs, Needs, Crime, Law, Chat, Commands, World. Each depends on Core and on whichever other modules it explicitly declares — never sideways through a shared table, never downward into another module's concrete types. The full dependency table, and the reasoning for where each boundary was cut, is in Legacy/13_DEPENDENCY_GRAPH.md §4.
- UI sits on top, reads modules through their public interfaces and events, and is owned by none of them. See 11_UI_ARCHITECTURE.md.
2. The dependency rule¶
Every piece of state has exactly one owning module. Everyone else asks. (Legacy/13_DEPENDENCY_GRAPH.md §5)
Concretely, enforced at review and largely at compile time (ADR-0002):
- A module's concrete types are
internal. Other modules see only what it registers as a service interface, publishes as an event, or exposes as aGameResource. - The module dependency graph is a DAG.
CrimeprovidesArrest;Lawdecides who may call it;Crimedoes not knowLawexists. If a new feature seems to need a cycle, the shared concept belongs in Core, or the communication should be an event. - Nothing resolves a dependency by touching a global at file scope. See D-08 for what that cost the original.
3. Module lifecycle¶
Every module is one type deriving from GameModule, declaring its dependencies, resolved in
topological order at boot:
Configure → RegisterServices → Initialize → Ready → Shutdown
A missing dependency or a cycle is a hard startup failure naming the modules involved — never
a silent misordering. Full treatment, including the GameModule contract and what belongs in
each lifecycle stage, is in 02_MODULE_MODEL.md.
4. Engine baseline¶
All documentation in this folder is written against S&box as of July 2026 and cites the
specific engine feature it relies on. This table is the index; each Architecture/ document
goes into the detail relevant to it.
| Concern | Engine feature | Verified against | Used in |
|---|---|---|---|
| Component lifecycle | OnAwake → OnStart → OnUpdate / OnFixedUpdate → OnDestroy, plus OnEnabled / OnDisabled |
Component Interfaces, Sandbox.Component |
Every module |
| Making an object networked | GameObject.NetworkSpawn() |
Networking & Multiplayer | 07_NETWORKING.md |
| State replication | [Sync], [Sync(SyncFlags.FromHost)] |
Sync Properties, Networked Objects | 07_NETWORKING.md |
| Remote calls | [Rpc.Broadcast], [Rpc.Host], [Rpc.Owner] |
Networking & Multiplayer, above | 07_NETWORKING.md |
| Data-driven content | GameResource, [GameResource], [Property] |
Custom Assets | 09_ITEM_FRAMEWORK.md |
| Local persistence | FileSystem.Data.WriteJson / ReadJson |
File System | 06_PERSISTENCE.md |
| Remote persistence | Http.RequestAsync |
HTTP Requests | 06_PERSISTENCE.md |
| Reusable/shared code | Libraries | Libraries | ADR-0002 |
| Automated tests | UnitTests/ at project root, auto-detected, generates an MSTest project |
Standards/03_TESTING_STANDARDS.md §2 | Every module |
Two engine facts shape the architecture broadly enough to state here rather than repeat in every document:
- Serialisation is get/set properties only.
GameResourceand the save envelope both serialise properties with both a getter and a setter; a public field is silently skipped. Every persisted or inspector-exposed type is written as properties, never fields. [Sync]is permissive by default;[Sync(SyncFlags.FromHost)]is the safe one. The shorter attribute name is the one that lets the owning client write. Anything a player could profit from forging isFromHost, always — see ADR-0005.
If an engine API changes in a way that invalidates a fact on this page, that is a documentation bug — the whole point of pinning a baseline date is so a drift is visible rather than silently wrong.
5. How to read the rest of this folder¶
| Document | Answers |
|---|---|
| 01_PROJECT_LAYOUT.md | Where does a new file go? |
| 02_MODULE_MODEL.md | What is a module, exactly, and how does it boot? |
| 03_SERVICE_REGISTRY.md | How does one module call another? |
| 04_EVENT_BUS.md | How does one module observe another without depending on it? |
| 05_CONFIGURATION.md | How does an operator change a number without recompiling? |
| 06_PERSISTENCE.md | How is anything saved, and how does it survive a schema change? |
| 07_NETWORKING.md | What may a client tell the host, and who is trusted? |
| 08_DATA_MODEL.md | What is a Character, an Item, a Container? |
| 09_ITEM_FRAMEWORK.md | How is an item defined, and how does it get a behaviour? |
| 10_INTERACTION_FRAMEWORK.md | How does a timed player action work, generically? |
| 11_UI_ARCHITECTURE.md | How does a panel get built and registered? |
| 12_LOGGING_AND_DIAGNOSTICS.md | How does an operator find out what happened? |
| 13_ERROR_HANDLING.md | Exception or result? When does the game refuse to boot? |
| 14_NEEDS_FRAMEWORK.md | What need decays over time, and how fast? |
| 15_COMMANDS_FRAMEWORK.md | How does a chat-typed player action get registered and gated? |
| 16_JOBS_FRAMEWORK.md | What is the group→gang→team hierarchy, and how does a character join a job? |
| 17_CRIME_FRAMEWORK.md | How does a warrant get issued, and how does an arrest work? |
| 18_LAW_FRAMEWORK.md | What is the ten-laws noticeboard, and why is it deliberately inert? |
| 19_CHAT_FRAMEWORK.md | How does a chat message get routed to its recipients? |
| 20_WORLD_LOCKS_AND_CONTAINERS.md | How do locks, seals, and a second container type work? |
| 21_EXTENSION_SURFACE.md | What counts as an extension interface, and has it been stable long enough to revisit ADR-0002? |
| 22_PLAYER_PRESENCE_AND_ADMIN_TOOLS.md | How does an admin see, move, and moderate connected players? |
| 23_UI_DESIGN_SYSTEM.md | What visual language and component patterns should every panel share? |
| 24_PLAYER_MENU.md | How does the player-facing tabbed menu get built and registered? |
Each document carries real C# signatures, not policy statements — see 00_CONSTITUTION.md §1 for why that distinction matters. Where a document's design departs from legacy behaviour, it cites the ADR or defect that justifies the departure.