Dependency Graph and Data Flow¶
How tightly the legacy systems are coupled, and therefore where the new module boundaries have to be cut.
1. Load-time dependencies¶
The order in sh_init.lua is not arbitrary — each stage is required by the next. This is the
coupling that D-08 describes.
graph TD
CFG[sh_config.lua<br/>tuning values]
ENUM[sh_enumerations.lua]
LIB[libraries/<br/>16 files]
META[metatables/<br/>player · item · entity]
HOOKS[hooks/]
PLUG["GM:LoadPlugins()"]
ITEMS["GM:LoadItems()"]
JOBS[sh_jobs.lua]
DERMA[derma/]
ENUM --> CFG --> LIB --> META --> HOOKS --> PLUG --> ITEMS --> JOBS --> DERMA
ITEMS -. "GetPlugin('packaging')<br/>GetPlugin('vehicles')" .-> PLUG
JOBS -. "GetPlugin('officials')<br/>GetPlugin('hunger')" .-> PLUG
JOBS -. "CATEGORY_* constants" .-> ITEMS
classDef bad fill:#f8d7da,stroke:#c00
class PLUG,ITEMS,JOBS bad
The dotted edges are the problem. items/base/packaging.lua calls GetPlugin("packaging")
at include time and stores the result in a local. sh_jobs.lua wraps whole job
definitions in if GetPlugin("officials"). Move LoadPlugins() one line later and the game
boots into a subtly broken state with no error.
Resolution: modules declare dependencies; the loader topologically sorts and fails loudly. Nothing resolves a dependency by calling a global at file scope. Architecture/02_MODULE_MODEL.md
2. Runtime coupling between systems¶
graph LR
subgraph Core
PLAYER[Player record<br/>ply.cider]
INV[Inventory]
ITEM[Item registry]
ENT[Entity ownership]
TEAM[Teams]
CMD[Commands]
LOG[Logging]
end
subgraph Plugins
OFF[officials]
DOORS[doors]
HUNGER[hunger]
STAM[stamina]
VEH[vehicles]
PACK[packaging]
end
CMD --> PLAYER & INV & ENT & TEAM
INV --> ITEM
INV --> PLAYER
ITEM --> INV
ITEM --> PLAYER
ITEM --> PACK
ITEM --> VEH
TEAM --> ITEM
TEAM --> PLAYER
ENT --> TEAM
ENT --> PLAYER
OFF --> PLAYER
DOORS --> ENT
HUNGER --> PLAYER
STAM --> PLAYER
PLAYER --> LOG
Note the cycles:
Inventory ⇄ Item.cider.inventory.updatecallsitem:onUpdate, and item callbacks callcider.inventory.update. Mutual recursion is possible and is only prevented by convention.Item → Plugin → Item.items/base/vehicle.luadelegates to thevehiclesplugin, which manufactures vehicle items.- Everything →
ply.cider. The player table is a global mutable bag that eleven systems write to directly. There is no owner and no invariant.
That last point is the central structural failure. ply.cider._Money is written by the
economy, the manufacturing command, the door tax, the contraband loop and item callbacks —
none of which coordinate. Any rule about money ("never below zero") has to be re-asserted at
every write site, which is exactly why it lives inside GiveMoney and why anything that
bypasses GiveMoney breaks it.
Resolution: state has exactly one owning module, mutated through that module's service interface. Other modules observe via events. Cross-module reads go through an interface, never through a shared table.
3. Data flow: a player buying a door¶
Traced end to end, because it touches almost everything and shows the shape of a typical operation.
sequenceDiagram
participant C as Client
participant D as concommand "cider"
participant CMD as command registry
participant P as Player record
participant E as Entity ownership
participant DP as doors plugin
participant L as Log
participant N as Network
C->>D: cider door buy
D->>CMD: dispatch("door", ["buy"])
CMD->>CMD: _Initialized? args? access flag "b"?
CMD->>CMD: hook PlayerCanUseCommand
CMD->>DP: hook PlayerCanOwnDoor
DP-->>CMD: allowed
CMD->>P: CanAfford(150)
CMD->>P: GiveMoney(-150)
P->>N: usermessage MoneyAlert
CMD->>P: GiveDoor(entity)
CMD->>E: setOwnerPlayer(entity, player)
E->>N: NWString cider_ownerName
E->>N: DTInt(3) |= OBJ_LOCKED
CMD->>L: Log(EVENT_COMMAND, ...)
DP->>DP: SaveData() → doors/<map>.txt
P-->>N: _Money resent every second regardless
Six systems, four hook points, three separate network mechanisms, and two persistence
backends — for one purchase. Note also that nothing is transactional: if setOwnerPlayer
fails after GiveMoney(-150), the money is gone and the door is not owned.
Resolution: the new equivalent is one
[Rpc.Host]call into aPropertyService, which validates, performs the whole operation, and publishes aDoorPurchasedevent. Money and ownership change together or not at all.
4. Where to cut¶
Reading the coupling above, the module boundaries fall out naturally. They follow the legacy plugin split closely (10_PLUGINS.md) because that split was already right.
| Module | Owns | Depends on |
|---|---|---|
| Core | Logging, config, events, services, persistence, time | nothing |
| Character | The character record, permissions, blacklists, lifecycle | Core |
| Items | Definitions, the registry, instances | Core |
| Inventory | Containers, capacity, transfers, world items | Core, Items |
| Interaction | The timed-interaction primitive, verbs | Core |
| Ownership | Entity ownership, access lists, property groups | Core, Character |
| Economy | Money, salary, payday, tax, manufacturing | Core, Character, Items |
| Jobs | Groups, gangs, teams, governance | Core, Character |
| Needs | The need framework; hunger, stamina, thirst as data | Core, Character |
| Crime | Warrants, arrest, detention mechanisms | Core, Character |
| Law | Policy: who may police whom | Core, Crime, Jobs |
| Chat | Channels, routing, proximity | Core, Character |
| Commands | Registry, dispatch, help | Core |
| World | Doors, containers, lights, switches as components | Core, Ownership, Interaction |
| UI | Panels, HUD, ESP registration | Core (+ reads others' interfaces) |
Two dependency rules make this hold:
- No cycles. The table above is a DAG. If a new feature seems to need a cycle, the shared concept belongs in a lower module or the communication should be an event.
- Policy depends on mechanism, never the reverse.
CrimeprovidesArrest;Lawdecides who may call it.Crimemust not knowLawexists — exactly as the legacy metatable/officialssplit worked.
5. The single most important structural lesson¶
Every serious problem in this document reduces to one cause: ply.cider is a shared
mutable table with no owner.
It is why money rules have to be re-asserted at each write site, why saving mutates game
state, why the save schema is implicit, why _Misc exists, and why eleven systems are
coupled to one another through a data structure rather than an interface.
The new design's first rule follows directly: every piece of state has exactly one owning module, and everyone else asks.