Skip to content

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.update calls item:onUpdate, and item callbacks call cider.inventory.update. Mutual recursion is possible and is only prevented by convention.
  • Item → Plugin → Item. items/base/vehicle.lua delegates to the vehicles plugin, 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 a PropertyService, which validates, performs the whole operation, and publishes a DoorPurchased event. 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:

  1. 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.
  2. Policy depends on mechanism, never the reverse. Crime provides Arrest; Law decides who may call it. Crime must not know Law exists — exactly as the legacy metatable/officials split 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.