{
  "note": "Zone server netcode numbers (ADR-003, M1-03): tick budget, fixed-rate scheduler, spatial hash, interest (AOI) sets and channel caps, moved here from docs/tech/NETCODE_1000.md so the zone server (services/zone) reads them from data; the doc explains them. The tick rate itself is sim.json → tickHz.",
  "tick": {
    "source": "docs/tech/NETCODE_1000.md → Server tick",
    "rateFrom": "sim.json → tickHz",
    "budgetMs": 50,
    "targetP50Ms": 11,
    "targetP99Ms": 15,
    "systemBudgets": {
      "note": "Per-system budget of one tick on the 8 vCPU reference server; the sum is the p50 target.",
      "vcpu": 8,
      "ms": {
        "ingestInputs": 0.5,
        "movement": 0.6,
        "spatialHash": 0.4,
        "monsterAi": 1.5,
        "combat": 0.8,
        "statuses": 0.4,
        "interest": 3.0,
        "serialize": 2.5,
        "networkSend": 1.0
      }
    }
  },
  "scheduler": {
    "proposed": true,
    "note": "Not in NETCODE_1000.md; starting value set by M1-03, measure in the M1-04 soak. Tick deadlines are absolute (start + n × period), so sleeping never drifts. After a stall the loop runs at most maxTicksPerFrame due ticks back to back (4 ticks = 200 ms of world time, about what the client's 100 ms interpolation delay plus 150 ms extrapolation can hide); any older backlog is dropped, the schedule re-anchors to now and the stall is counted.",
    "maxTicksPerFrame": 4
  },
  "spatialHash": {
    "source": "docs/tech/NETCODE_1000.md → Server tick (spatial hash: 16 m cells, 256 × 256 over 4,096 m)",
    "cellM": 16,
    "grid": [256, 256]
  },
  "interest": {
    "source": "docs/tech/NETCODE_1000.md → Interest management and Bandwidth math",
    "radiusM": 90,
    "townRadiusM": 70,
    "maxSentPerSnapshot": 150
  },
  "interestSet": {
    "proposed": true,
    "note": "Not in NETCODE_1000.md; starting values set by M1-03. leaveMarginM is the hysteresis: an entity enters a client's set inside the interest radius and leaves only beyond radius + margin, so walking along the edge (6.2 m/s = 0.31 m per tick) never flickers. maxSize caps one client's set at 512 entities, the 9-bit AOI table index of the Bandwidth math entity header (9–10 bit); when more qualify, the nearest are kept (ties by entity id).",
    "leaveMarginM": 5,
    "maxSize": 512
  },
  "snapshot": {
    "source": "docs/tech/NETCODE_1000.md → Interest management (rates) and Bandwidth math (bit widths, 32 KB/s hard cap)",
    "hardCapKBps": 32,
    "upstreamTargetKBps": 0.3,
    "sceneTargetsKBps": {
      "note": "Movement share and total per scene from the Bandwidth math table; services/zone bench-snapshot measures the movement share.",
      "quietField": { "visible": 25, "movement": 1.8, "total": 3 },
      "busyHunting": { "visible": 70, "movement": 6, "total": 9 },
      "crowdedTown": { "visible": 150, "movement": 14, "total": 18 }
    },
    "rates": [
      { "band": "near", "maxM": 30, "hz": 20 },
      { "band": "mid", "maxM": 60, "hz": 10 },
      { "band": "far", "maxM": 90, "hz": 4 }
    ],
    "bits": {
      "positionAbsolute": 18,
      "positionDelta": 10,
      "yaw": 8,
      "anim": 6,
      "entityIndex": 9
    },
    "proposed": {
      "proposed": true,
      "note": "Not in NETCODE_1000.md; starting values set by M1-03B. KB = 1,000 bytes (the conservative reading), so one 20 Hz packet may carry 32,000 / 20 = 1,600 bytes including the WebSocket frame header (4 bytes counted). positionQuantumMm: 18 bits must cover the 4,096 m region, 4,096,000 / 2^18 = 15.6 mm rounded up to 16 mm (2^18 × 16 mm = 4,194 m); the 10-bit signed delta then spans ±8.2 m, 26 ticks of running at 6.2 m/s; larger moves fall back to absolute. hpBits: HP as permille of max (0–1,000) in 10 bits until combat lands (M2), MP joins with the combat model. entityIdBits: an entering entity is named by its 16-bit channel entity id (the channel holds 5,000). baselineFrames: the server keeps the last 32 frames per client (1.6 s at 20 Hz, ~96 KB per client); a client whose newest ack is older gets a full snapshot. animStates: the zone's movement-only states until the animation set exists. Relevance multipliers (target, attacker, party, boss, telegraph) wait for those systems.",
      "positionQuantumMm": 16,
      "hpBits": 10,
      "entityIdBits": 16,
      "wsHeaderBytes": 4,
      "baselineFrames": 32,
      "animStates": ["idle", "run"]
    }
  },
  "channel": {
    "source": "docs/tech/NETCODE_1000.md → Channels, and ADR-003 (a channel holds 1,000 players plus about 4,000 monsters)",
    "softCapPlayers": 900,
    "hardCapPlayers": 1000,
    "openNewChannelAtPercent": 85,
    "monsters": 4000
  },
  "server": {
    "proposed": true,
    "note": "Not in NETCODE_1000.md; starting values set by M1-03C for the zone binary's WebSocket endpoint (services/zone src/net). Binds loopback by default (a TLS-terminating gateway fronts wss later). Network threads hand events to the tick through one bounded queue (inboundQueueEvents; network threads wait when it is full, the tick never does) and take snapshots from a per-client pool of sendQueueFrames buffers (when all are in flight the snapshot is skipped and the delta baseline covers the gap). Clients that send nothing for idleTimeoutMs are closed (4002). New clients spawn within spawnSpreadM of the start waypoint's zone centre (world.json). snapshotWorkers (M1-04B): threads that build and hand off the per-client snapshots, the tick thread included; clients sit in stable shards (slot mod workers), each shard runs on one thread against the tick's read-only state and the tick waits for all of them (1 = the single-threaded path). 4 = half of the 8-core dev Mac and of the 8 vCPU reference server, leaving cores for the network threads. Measure in the M1-04 soak.",
    "bindAddress": "127.0.0.1",
    "port": 7310,
    "handshakeTimeoutMs": 5000,
    "idleTimeoutMs": 10000,
    "inboundQueueEvents": 4096,
    "sendQueueFrames": 4,
    "maxClientMessageBytes": 125,
    "maxHandshakeBytes": 4096,
    "spawnSpreadM": 10,
    "statusEveryS": 5,
    "snapshotWorkers": 4
  },
  "loadTest": {
    "source": "docs/tech/NETCODE_1000.md → Load-test plan (M1 and M6 gates)",
    "bots": 1000,
    "rampSteps": [100, 250, 500, 1000],
    "rampMinutes": 10,
    "holdMinutes": { "M1": 10, "M6": 60 },
    "avgDownstreamKBps": 12,
    "passNote": "Pass also needs tick p99 within tick.targetP99Ms, no disconnects and every client (crowded town included) within snapshot.hardCapKBps.",
    "memoryDriftPercent": 5,
    "castEveryS": [2, 6],
    "botMix": {
      "proposed": true,
      "note": "Not in NETCODE_1000.md; starting values set by M1-04 for the bot harness (services/bots). The doc names the behaviours (wander, hunt, crowd the town, cast every 2-6 s, chat) but not their mix. townShare of the bots crowd the town (waypoints within townRadiusM of the start zone centre); the rest walk to a random field zone and wander inside its bounds, pausing pauseS at each waypoint and, once in their zone, casting every castEveryS (standing still for castMs). Protocol v1 has no action bits and no chat message, so casts and chat lines are bot-side timers (counted, not sent) until the protocol carries them. fieldSpawnShare (M1-04B): this share of the bots spawn inside a random hunting zone (players who logged out in the field; only the in-process load-test server places them so) and hunt there from their first snapshot; townShare stays a share of all bots, the rest walk from town to a field. In M1-04 part A every bot started in town and only 12 of ~750 hunters reached a zone 400 m+ away in 80 s. Measure in the M1-04 soak.",
      "townShare": 0.25,
      "townRadiusM": 50,
      "fieldSpawnShare": 0.5,
      "pauseS": [0, 3],
      "castMs": 800,
      "chatEveryS": [15, 45]
    }
  }
}
