# Development

<h1 id="development-guide">Development Guide</h1>
<p>This page covers the project&rsquo;s core philosophy, testing requirements, and the concurrency model governing all shared state. Read this before touching <code>pkg/game/</code>, <code>pkg/session/</code>, or <code>pkg/combat/</code>.</p>
<hr>
<h2 id="the-prime-directive">The Prime Directive</h2>
<p><strong>Behavioral fidelity to the original 1997–2010 Dark Pawns C codebase takes precedence over idiomatic Go.</strong></p>
<p>This means:</p>
<ul>
<li>Combat formulas, THAC0 tables, damage multipliers, and experience penalties must match <code>fight.c</code> exactly.</li>
<li>Command output strings (room descriptions, combat messages) must match original output character-for-character where possible.</li>
<li>Pronoun substitution (<code>$n</code>, <code>$N</code>, <code>$e</code>, <code>$E</code>, <code>$s</code>, <code>$S</code>, <code>$m</code>, <code>$M</code>) follows ROM <code>act()</code> conventions.</li>
<li>Area files (<code>.wld</code>, <code>.mob</code>, <code>.obj</code>, <code>.zon</code>, <code>.shp</code>) are loaded without modification.</li>
</ul>
<p>When in doubt, the original C behavior is the spec. The Go implementation is a port, not a redesign.</p>
<hr>
<h2 id="testing-verification-checklist">Testing Verification Checklist</h2>
<p>Before submitting any change, verify the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 1. Compile — zero warnings, zero errors</span>
</span></span><span style="display:flex;"><span>go build ./...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2. Vet — catches common correctness bugs</span>
</span></span><span style="display:flex;"><span>go vet ./...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 3. Tests — must pass with no race conditions</span>
</span></span><span style="display:flex;"><span>go test -race ./...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 4. Lint — golangci-lint with project config</span>
</span></span><span style="display:flex;"><span>golangci-lint run ./...
</span></span></code></pre></div><p>All four must pass clean. The test suite includes:</p>
<ul>
<li><code>pkg/session/websocket_e2e_test.go</code> — full login → command → state-update end-to-end</li>
<li><code>pkg/session/session_login_test.go</code> — auth flow, character creation state machine</li>
<li><code>pkg/combat/</code> — damage formula correctness, THAC0 verification</li>
<li><code>pkg/game/</code> — world parser, mob instance, zone resets</li>
<li><code>pkg/scripting/</code> — Lua sandbox, API surface</li>
</ul>
<hr>
<h2 id="lock-ordering-hierarchy">Lock Ordering Hierarchy</h2>
<p>This project uses a strict top-down lock acquisition order to prevent deadlocks. <strong>394 lock acquisitions across 57 functions</strong> depend on this ordering being correct.</p>
<p>Audited <code>2026-05-07</code> by BRENDA69. No violations found at time of audit.</p>
<h3 id="acquire-locks-from-top-1-to-bottom-14-only">Acquire locks from top (1) to bottom (14) only:</h3>
<table>
  <thead>
      <tr>
          <th>#</th>
          <th>Lock</th>
          <th>Protects</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td><code>World.mu</code></td>
          <td>Top-level game state: rooms, mobs, objects</td>
      </tr>
      <tr>
          <td>2</td>
          <td><code>World.gossipMu</code></td>
          <td>Gossip channel history</td>
      </tr>
      <tr>
          <td>3</td>
          <td><code>World.weatherMu</code></td>
          <td>Weather state</td>
      </tr>
      <tr>
          <td>4</td>
          <td><code>World.mailWriteMu</code></td>
          <td>Mail persistence</td>
      </tr>
      <tr>
          <td>5</td>
          <td><code>Clan.mu</code></td>
          <td>Clan membership, ranks</td>
      </tr>
      <tr>
          <td>6</td>
          <td><code>Player.mu</code></td>
          <td>Player stats, gold, exp, position</td>
      </tr>
      <tr>
          <td>7</td>
          <td><code>Equipment.mu</code></td>
          <td>Equipped item slots</td>
      </tr>
      <tr>
          <td>8</td>
          <td><code>Inventory.mu</code></td>
          <td>Carried item list</td>
      </tr>
      <tr>
          <td>9</td>
          <td><code>MobInstance.mu</code></td>
          <td>Mob state, HP, position</td>
      </tr>
      <tr>
          <td>10</td>
          <td><code>Spawner.mu</code></td>
          <td>Zone reset scheduling</td>
      </tr>
      <tr>
          <td>11</td>
          <td><code>BoardState.mu</code></td>
          <td>Bulletin board messages</td>
      </tr>
      <tr>
          <td>12</td>
          <td><code>Shop.mu</code></td>
          <td>Shop inventory, pricing</td>
      </tr>
      <tr>
          <td>13</td>
          <td><code>ZoneDispatcher.mu</code></td>
          <td>Zone command routing</td>
      </tr>
      <tr>
          <td>14</td>
          <td><code>logWriterMu</code></td>
          <td>Log file writes (functionally independent)</td>
      </tr>
  </tbody>
</table>
<h3 id="locks-outside-the-pkggame-hierarchy-always-outermost-first">Locks outside the pkg/game hierarchy (always outermost-first):</h3>
<table>
  <thead>
      <tr>
          <th>Lock</th>
          <th>Package</th>
          <th>Protects</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Manager.mu</code></td>
          <td><code>pkg/session</code></td>
          <td>Active sessions map</td>
      </tr>
      <tr>
          <td><code>CombatEngine.mu</code></td>
          <td><code>pkg/combat</code></td>
          <td>Combat pairs map (2s tick)</td>
      </tr>
      <tr>
          <td><code>scripting.Engine.mu</code></td>
          <td><code>pkg/scripting</code></td>
          <td>Lua VM (serialized, one script at a time)</td>
      </tr>
  </tbody>
</table>
<h3 id="rules">Rules</h3>
<ul>
<li><strong>Never</strong> hold a lower-numbered lock while acquiring a higher-numbered one.</li>
<li><strong>Same-level locks</strong> (e.g., two <code>Player.mu</code> on different players): always acquire in consistent order by <code>Name</code> or <code>ID</code> to prevent ABBA deadlocks.</li>
<li><strong>Never</strong> upgrade <code>RLock → Lock</code> without releasing first.</li>
<li><code>World.mu</code> is always the outermost lock in <code>pkg/game</code>. Never call World methods that re-acquire <code>World.mu</code> while holding <code>Player.mu</code>, <code>MobInstance.mu</code>, or <code>Clan.mu</code>.</li>
<li>Use <code>defer Unlock()</code> for simple critical sections. Use explicit <code>Unlock()</code> for multi-lock or conditional-unlock patterns.</li>
</ul>
<h3 id="verified-safe-nested-patterns">Verified safe nested patterns</h3>
<pre tabindex="0"><code>World.mu → MobInstance.mu          (save.go — deserialization)
Player.mu → Equipment.mu           (death.go — death cleanup)
Clan.mu → Player.mu                (item_transfer.go — gold transfer)
World.mu.RLock → Player/Mob.mu     (party.go — group handling)
</code></pre><hr>
<h2 id="package-overview">Package Overview</h2>
<table>
  <thead>
      <tr>
          <th>Package</th>
          <th>Role</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>cmd/server</code></td>
          <td>Entry point; wires all dependencies in init order</td>
      </tr>
      <tr>
          <td><code>pkg/session</code></td>
          <td>WebSocket lifecycle, login, command dispatch, agent variable push</td>
      </tr>
      <tr>
          <td><code>pkg/game</code></td>
          <td>World state, rooms, mobs, objects, players, zone resets</td>
      </tr>
      <tr>
          <td><code>pkg/combat</code></td>
          <td>2-second combat tick, damage formulas, THAC0</td>
      </tr>
      <tr>
          <td><code>pkg/scripting</code></td>
          <td>Sandboxed Lua VM (gopher-lua), ~60 registered API functions</td>
      </tr>
      <tr>
          <td><code>pkg/parser</code></td>
          <td>ROM area file parser (<code>.wld</code>, <code>.mob</code>, <code>.obj</code>, <code>.zon</code>)</td>
      </tr>
      <tr>
          <td><code>pkg/command</code></td>
          <td>Command registry, skill handlers</td>
      </tr>
      <tr>
          <td><code>pkg/db</code></td>
          <td>Postgres persistence (characters, agent keys, decision logs)</td>
      </tr>
      <tr>
          <td><code>pkg/events</code></td>
          <td>Timer-based event queue + in-process pub/sub bus</td>
      </tr>
      <tr>
          <td><code>pkg/agent</code></td>
          <td>Agent variable subscription, dirty-tracking, push flush</td>
      </tr>
      <tr>
          <td><code>pkg/auth</code></td>
          <td>JWT generation/validation, IP rate limiter</td>
      </tr>
      <tr>
          <td><code>pkg/telnet</code></td>
          <td>Raw TCP listener with IAC negotiation</td>
      </tr>
      <tr>
          <td><code>pkg/audit</code></td>
          <td>Security and admin event logging</td>
      </tr>
      <tr>
          <td><code>pkg/metrics</code></td>
          <td>Prometheus exposition endpoint</td>
      </tr>
      <tr>
          <td><code>pkg/moderation</code></td>
          <td>Mute, ban, word filter, spam detection</td>
      </tr>
      <tr>
          <td><code>pkg/admin</code></td>
          <td>Admin API router and control panels</td>
      </tr>
      <tr>
          <td><code>pkg/common</code></td>
          <td>Shared types and constants across packages</td>
      </tr>
      <tr>
          <td><code>pkg/dreaming</code></td>
          <td>Memory graph, narrative consolidation, valence computation</td>
      </tr>
      <tr>
          <td><code>pkg/engine</code></td>
          <td>Game loop orchestrator (heartbeat, ticks, pulses)</td>
      </tr>
      <tr>
          <td><code>pkg/optimization</code></td>
          <td>Performance profiling and optimization utilities</td>
      </tr>
      <tr>
          <td><code>pkg/privacy</code></td>
          <td>PII hashing and fail-closed filter</td>
      </tr>
      <tr>
          <td><code>pkg/secrets</code></td>
          <td>Secret management and encryption</td>
      </tr>
      <tr>
          <td><code>pkg/spells</code></td>
          <td>Spell system and casting logic</td>
      </tr>
      <tr>
          <td><code>pkg/storage</code></td>
          <td>Storage abstraction layer</td>
      </tr>
      <tr>
          <td><code>pkg/validation</code></td>
          <td>Input validation and sanitization</td>
      </tr>
  </tbody>
</table>
<p>See <strong><a href="/docs/development/architecture/">Architecture Reference</a></strong> for the full data-flow diagram and concurrency model.</p>
<hr>
<h2 id="contributing">Contributing</h2>
<ol>
<li>Read the Prime Directive above.</li>
<li>Run the full verification checklist before every commit.</li>
<li>If you touch any lock acquisition, check the hierarchy table and add a comment in <code>locks.go</code> for any new nested pattern.</li>
<li>Combat formula changes require a side-by-side comparison against the original C <code>fight.c</code> logic, documented in your PR.</li>
<li>Lua script API changes must preserve backward compatibility with existing <code>world/lib/scripts/</code> files.</li>
</ol>

## Pages
### Architecture Reference

<h1 id="architecture-reference">Architecture Reference</h1>
<p><strong>Last updated:</strong> 2026-05-16</p>
<p>This page documents the internal architecture of the Dark Pawns Go server, covering the data flow from client connection to command dispatch, the concurrency model, and the package breakdown. The full source is in <code>docs/architecture/ARCHITECTURE.md</code> in the repository.</p>
<hr>
<h2 id="overview">Overview</h2>
<p>Dark Pawns is a Go MUD server faithful to ROM 2.4b / Dark Pawns C source. It reads unmodified <code>.wld</code>, <code>.mob</code>, <code>.obj</code>, and <code>.zon</code> area files via a custom parser, then serves the game over WebSocket (primary) and telnet (legacy). Concurrency is goroutine-per-connection with a shared <code>sync.RWMutex</code> on the world state, a 2-second combat ticker, and serialized Lua scripting via a single <code>gopher-lua</code> VM. The design prioritizes behavioral fidelity to the original C codebase — same command set, same combat formulas, same Lua script API — while replacing the single-threaded C event loop with Go&rsquo;s concurrency primitives.</p>
<hr>
<h2 id="data-flow">Data Flow</h2>
<pre tabindex="0"><code>  Client (Browser/Telnet/Agent)
       │           │            │
   WebSocket    TCP/Telnet    WebSocket
       │           │            │  (is_agent: true, api_key auth)
       ▼           ▼            ▼
  ┌────────┐  ┌──────────────────────────────────┐
  │/ws     │  │ pkg/telnet                       │
  │handler │  │ Listen() → handleConn()          │
  └───┬────┘  │    → manager.NewSession()        │
      │       │    → JSON shim → HandleMessage() │
      │       └──────────────┬───────────────────┘
      │                      │
      ▼                      ▼
  ┌──────────────────────────────────────┐
  │ pkg/session — Manager                │
  │  HandleWebSocket() / NewSession()    │
  │  ┌──────────────┐                    │
  │  │ Session      │                    │
  │  │ readPump()   │──┐                 │
  │  │ writePump()  │◀─┘  (goroutines)   │
  │  └──────┬───────┘                    │
  └─────────┼────────────────────────────┘
            │ handleMessage()
            ▼
  ┌──────────────────────────────────────┐
  │ Command Dispatch (pkg/session)       │
  │  ExecuteCommand()                    │
  │   1. Check mob oncmd scripts         │
  │   2. cmdRegistry.Lookup(cmd)         │
  │   3. handler(session, args)          │
  └──────────────┬───────────────────────┘
                 │
       ┌─────────┼─────────┐
       ▼         ▼         ▼
  ┌────────┐ ┌────────┐ ┌──────────────────┐
  │World   │ │Combat  │ │Scripting (Lua)   │
  │(game)  │ │Engine  │ │  RunScript()     │
  │        │ │2s tick │ │  Serialized VM   │
  └───┬────┘ └───┬────┘ └────────┬─────────┘
      │          │               │
      ▼          ▼               ▼
  ┌──────────────────────────────────────┐
  │ Response Path                        │
  │  session.send ← []byte (JSON)        │
  │  writePump() → WebSocket/Telnet      │
  └──────────────────────────────────────┘
</code></pre><hr>
<h2 id="package-reference">Package Reference</h2>
<h3 id="cmdserver"><code>cmd/server</code></h3>
<p>Entry point. Parses flags (<code>-world</code>, <code>-port</code>, <code>-db</code>, <code>-scripts</code>, <code>-web</code>, <code>-hugo</code>), calls <code>parser.ParseWorld()</code>, constructs <code>game.World</code>, initializes <code>scripting.Engine</code>, connects to Postgres via <code>pkg/db</code>, creates <code>session.Manager</code>, wires callbacks (combat broadcast, death handler, memory hooks, fight scripts, damage tracking), registers HTTP routes (<code>/ws</code>, <code>/health</code>, <code>/metrics</code>, <code>/api/</code>, <code>/admin/</code>), starts zone resets, and serves HTTP (with optional TLS).</p>
<p><strong>Initialization order is strict:</strong></p>
<ol>
<li>Parse world files</li>
<li>Create game world</li>
<li>Init scripting engine (depends on world)</li>
<li>Connect to database (optional, graceful fallback)</li>
<li>Create session manager (depends on world + db)</li>
<li>Register manager hooks</li>
<li>Setup HTTP routes</li>
<li>Start zone reset goroutine</li>
<li>Start HTTP server</li>
<li>Block on signal for shutdown</li>
</ol>
<h3 id="pkgsession"><code>pkg/session</code></h3>
<p>WebSocket connection lifecycle and command dispatch. <code>Manager</code> holds all active sessions in a map keyed by player name, plus references to <code>game.World</code>, <code>combat.CombatEngine</code>, and <code>db.DB</code>. Each WebSocket upgrade spawns two goroutines (<code>readPump</code>, <code>writePump</code>) with a buffered send channel (cap 256).</p>
<p>Handles login (new player creation, bcrypt password verification, DB load/save), agent auth (API key validation), character creation state machine, and command routing. Agent sessions get variable subscription/dirty-tracking for push-based state sync.</p>
<p><strong>Key types:</strong> <code>Manager</code>, <code>Session</code></p>
<h3 id="pkggame"><code>pkg/game</code></h3>
<p>The game world: rooms, mobs (prototypes + instances), objects, zones, players, items on the ground, AI ticker, spawner/zone resets, point update ticker (regen/hunger). Single <code>sync.RWMutex</code> (<code>World.mu</code>) protects top-level world state. <code>SnapshotManager</code> provides lock-free room snapshots via atomic pointer swaps.</p>
<p><code>MobInstance</code> uses mutex-protected getters/setters. Door and shop types live in <code>pkg/game/systems/</code>.</p>
<p><strong>Key types:</strong> <code>World</code>, <code>Player</code>, <code>MobInstance</code>, <code>ObjectInstance</code>, <code>Spawner</code>, <code>SnapshotManager</code>, <code>ZoneDispatcher</code></p>
<h3 id="pkgcombat"><code>pkg/combat</code></h3>
<p>Tick-based combat engine. Runs a 2-second ticker goroutine that snapshots all <code>CombatPair</code>s under write lock, then processes each pair (hit chance → damage → death check → fight scripts) outside the lock. Death handling delegates to game layer via <code>DeathFunc</code> callback. Damage formulas are faithful to Dark Pawns C <code>fight.c</code>.</p>
<p><strong>Key types:</strong> <code>CombatEngine</code>, <code>CombatPair</code>, <code>Combatant</code> (interface)</p>
<h3 id="pkgscripting"><code>pkg/scripting</code></h3>
<p>Lua scripting engine using <code>gopher-lua</code>. Single VM (<code>lua.LState</code>) protected by <code>sync.Mutex</code> — all script execution is serialized. Registers ~60 Lua API functions. Sandboxed: no <code>io</code>, <code>os.execute</code>, <code>dofile</code> (path-restricted), <code>debug</code>, <code>package</code>. 10MB memory limit.</p>
<p>Scripts live at <code>world/lib/scripts/</code> matching original C paths (e.g., <code>mob/144/hisc.lua</code>).</p>
<p><strong>Trigger types:</strong> <code>greet</code> (player enters room), <code>oncmd</code> (before command processing — can intercept), <code>fight</code> (after each combat round), custom (via <code>create_event()</code>)</p>
<h3 id="pkgparser"><code>pkg/parser</code></h3>
<p>Reads original ROM/Dark Pawns area files from disk. <code>ParseWorld(libDir)</code> scans <code>wld/</code>, <code>mob/</code>, <code>obj/</code>, <code>zon/</code> subdirectories and returns a parsed <code>World</code> struct. Parsed data is immutable after boot.</p>
<h3 id="pkgtelnet"><code>pkg/telnet</code></h3>
<p>Raw TCP telnet listener. Accepts connections, negotiates IAC/WILL/DO for ECHO and SGA, then translates line-based input into JSON <code>ClientMessage</code> format and feeds it to <code>session.Manager</code>.</p>
<h3 id="pkgauth"><code>pkg/auth</code></h3>
<p>JWT generation/validation using HMAC-SHA256 with <code>JWT_SECRET</code> env var (24-hour expiry). <code>IPRateLimiter</code> for login rate limiting.</p>
<h3 id="pkgcommand"><code>pkg/command</code></h3>
<p>Command registry and skill handlers. <code>Registry</code> maps command names to <code>Handler</code> functions with aliases, minimum level, and position requirements. Middleware decorator chain: <code>LoggingMiddleware</code>, <code>RateLimitMiddleware</code>.</p>
<h3 id="pkgdb"><code>pkg/db</code></h3>
<p>Postgres persistence. Player records serialized to/from JSON columns. Agent API key validation. Graceful: server runs without DB (no persistence).</p>
<h3 id="pkgevents"><code>pkg/events</code></h3>
<p>Two subsystems:</p>
<ol>
<li><strong>EventQueue</strong> — timer-based priority queue for scheduled Lua <code>create_event()</code> callbacks (100ms per pulse).</li>
<li><strong>InProcessBus</strong> — typed in-process pub/sub bus (<code>MobKilledEvent</code>, <code>PlayerLeveledEvent</code>, <code>RoomEnteredEvent</code>, etc.).</li>
</ol>
<h3 id="pkgstorage"><code>pkg/storage</code></h3>
<p>Optional SQLite persistence backend (WAL mode). Players, world state, narrative memory. <strong>Status:</strong> Implemented but not wired into <code>cmd/server/main.go</code> yet — currently in-memory only.</p>
<hr>
<h2 id="concurrency-model">Concurrency Model</h2>
<h3 id="goroutine-layout">Goroutine Layout</h3>
<table>
  <thead>
      <tr>
          <th>Goroutine</th>
          <th>Count</th>
          <th>Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Per-WebSocket session</td>
          <td>2</td>
          <td><code>readPump</code> + <code>writePump</code></td>
      </tr>
      <tr>
          <td>Per-telnet session</td>
          <td>2</td>
          <td>input reader + output writer</td>
      </tr>
      <tr>
          <td>Combat engine</td>
          <td>1</td>
          <td>2-second tick</td>
      </tr>
      <tr>
          <td>AI ticker</td>
          <td>1</td>
          <td>NPC behavior (game.World)</td>
      </tr>
      <tr>
          <td>Point update ticker</td>
          <td>1</td>
          <td>Regen every 30s</td>
      </tr>
      <tr>
          <td>Zone dispatcher</td>
          <td>1 per zone</td>
          <td>Resets, AI processing</td>
      </tr>
      <tr>
          <td>Lua transit cleanup</td>
          <td>1</td>
          <td>10s ticker, orphaned item cleanup</td>
      </tr>
      <tr>
          <td>Zone resets</td>
          <td>1</td>
          <td>60s periodic check</td>
      </tr>
  </tbody>
</table>
<h3 id="lock-hierarchy">Lock Hierarchy</h3>
<p>See the <a href="/docs/development/">Development Guide</a> for the complete 14-level lock acquisition hierarchy. The short version: acquire from outermost (<code>World.mu</code>) to innermost (<code>logWriterMu</code>), never in reverse.</p>
<hr>
<h2 id="authentication-flow">Authentication Flow</h2>
<pre tabindex="0"><code>WebSocket connect → /ws
  │
  ▼
handleLogin()
  ├─ Agent path: is_agent=true, password=api_key → db.ValidateAgentKey()
  │
  ├─ Returning player: db.GetPlayer() → bcrypt.CompareHashAndPassword()
  │
  ├─ New player: bcrypt.GenerateFromPassword() → db.CreatePlayer()
  │     GiveStartingItems() + GiveStartingSkills()
  │
  └─ Success:
       manager.Register(name, session)
       world.AddPlayer(player)
       auth.GenerateJWT(name, isAgent, agentKeyID)
       sendWelcome(jwt_token)   ← &#34;state&#34; message type
       BroadcastToRoom(&#34;X has arrived.&#34;)
</code></pre><p>JWT tokens are 24-hour HMAC-SHA256, issued on login and sent in the initial <code>&quot;state&quot;</code> message. Agent sessions additionally receive a full variable dump and memory bootstrap immediately after login.</p>
<hr>
<h2 id="command-dispatch">Command Dispatch</h2>
<pre tabindex="0"><code>Input: &#34;kill goblin&#34;
  │
  ▼
Session.handleCommand()
  │  Rate limit check (10 cmd/s token bucket)
  │
  ▼
ExecuteCommand(session, &#34;kill&#34;, [&#34;goblin&#34;])
  │
  ├─ 1. Check mobs in room for oncmd scripts
  │     If script returns TRUE → stop (handled by script)
  │
  ├─ 2. cmdRegistry.Lookup(&#34;kill&#34;)
  │     Returns &#34;hit&#34; entry (registered with alias &#34;kill&#34;)
  │
  ├─ 3. entry.Handler(&amp;commandSession{s}, [&#34;goblin&#34;])
  │     → cmdHit() → find target → combat engine
  │
  └─ 4. If agent session → flush dirty vars
</code></pre><hr>
<h2 id="snapshot-system-lock-free-reads">Snapshot System (Lock-Free Reads)</h2>
<p><code>WorldSnapshot</code> + <code>SnapshotManager</code> provide an atomic pointer-swapped read-only view of world rooms:</p>
<ul>
<li><code>World</code> holds a <code>SnapshotManager</code> with an <code>atomic.Pointer[WorldSnapshot]</code></li>
<li>World writers mutate under <code>World.mu</code> write lock, then call <code>PublishSnapshot()</code></li>
<li><code>PublishSnapshot</code> allocates a new snapshot, copies the rooms map, then atomically stores the pointer (no lock held on swap)</li>
<li>Readers call <code>World.Snapshot()</code> which does a load-free pointer read</li>
</ul>
<p><strong>Status:</strong> Initializing and publishing on boot. Readers (GetRoom, look, movement) still use the mutex path. Transition readers to <code>Snapshot()</code> for zero-lock lookups in performance-critical paths.</p>
<hr>
<h2 id="middleware-pipeline">Middleware Pipeline</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Middleware</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">Handler</span>) <span style="color:#a6e22e">Handler</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">cmdRegistry</span>.<span style="color:#a6e22e">Use</span>(<span style="color:#a6e22e">LoggingMiddleware</span>())
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">cmdRegistry</span>.<span style="color:#a6e22e">Use</span>(<span style="color:#a6e22e">RateLimitMiddleware</span>(<span style="color:#ae81ff">250</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Millisecond</span>))
</span></span></code></pre></div><p><strong>Built-in middleware</strong> (<code>pkg/command/middleware.go</code>):</p>
<ul>
<li><code>LoggingMiddleware</code> — logs command name, duration, and error status</li>
<li><code>RateLimitMiddleware</code> — enforces minimum interval between commands per session</li>
</ul>
<p><strong>Status:</strong> Implemented; not currently wired on the registry (add noise during development).</p>
<hr>
<h2 id="lua-scripting">Lua Scripting</h2>
<h3 id="execution-model">Execution Model</h3>
<ol>
<li>Acquires <code>Engine.mu</code> (serialized — one script at a time)</li>
<li>Marshals context to Lua globals: <code>ch</code> (player), <code>me</code> (mob), <code>obj</code>, <code>room</code>, <code>argument</code></li>
<li><code>DoFile(scriptPath)</code> to load the script</li>
<li><code>PCall(triggerName)</code> to invoke the trigger function</li>
<li>Reads back mutations from <code>ch</code> and <code>me</code> tables</li>
<li>Returns whether the script &ldquo;handled&rdquo; the event</li>
</ol>
<h3 id="object-transfer-protocol">Object Transfer Protocol</h3>
<p><code>objfrom(item, &quot;room&quot;|&quot;char&quot;)</code> removes an item to a transit map. <code>objto(item, &quot;room&quot;|&quot;char&quot;, target)</code> places it. Orphaned items (not placed within 30s) are logged and discarded by the Lua transit cleanup goroutine.</p>

