DRIFTLANDS co-op survival on drifting tides — studio progress

Driftlands — Master Architecture

Document Status

Engine Knowledge Gap Summary

Engine: Unity 6.3 LTS · LLM training covers: ~Unity 6.0 · Post-cutoff: 6.1, 6.2, 6.3.

Risk Domain What changed / why it matters Systems touched
HIGHURP Render GraphCompatibility Mode removed in 6.3; any custom pass must be Render-Graph-native (AddRenderPasses). Mitigated for MVP: the tide tint is a Shader Graph sprite shader reading a global property, not a custom pass (art bible §8.B.3).Tide-Tint (TR-tint-001/003), BB ghost/grid overlay (TR-bb-009)
HIGHNGO 2.x1.x deprecated; NetworkTransform.OnUpdate replaces the Update override; Multiplayer Play Mode 2.0.1 local instances only. The project's own modules/networking.md still shows 1.x-style [ServerRpc] and legacy Input examples — the 2.x RPC attribute surface and NetworkTransform snap thresholds are NEEDS VERIFICATION against the 6000.3 / NGO 2.x docs.Every host-rail system: TR-net-*, TR-combat-002/003/007, TR-harv-001/002, TR-craft-003/008, TR-bb-002/007, TR-ai-001, TR-move-002/004/005
MEDIUMUI ToolkitUSS strict parsing blocks import on invalid selectorsTR-ui-*
MEDIUMAddressables 2.xasync / Awaitable load patterns; release disciplineTR-ldu-001/007
MEDIUM2D content pipelineTile Set assets (6.1), Aseprite importer auto-Tilemapland-template authoring, TR-ldu-002
MEDIUMAwaitable / async scriptingdirectly relevant to the no-yield host-commit rule (TR-net-007)Foundation
LOWInput System, Physics2D (Box2D), Unity Test Framework, [SerializeField] fields-onlystable; note modules/physics.md documents 3D PhysX, not the 2D API this project usesTR-move-001/003, TR-bb-007

Rule: every decision in a HIGH domain in this document is marked ⚠️ NEEDS VERIFICATION and lands as an ADR input, not a settled fact.

Technical Requirements Baseline

Extracted from 13 design docs | 94 rows below (the original "102" was a miscount). Registered 2026-08-28 in docs/architecture/tr-registry.yaml (v2, 111 IDs: these 94 + 17 surfaced by the review — see traceability-index.md). The registry, not this table, is canonical.

Req ID System Requirement Domain
TR-tide-001TideDockedLands(seed, t, config) pure function in an engine-free C# assembly (no UnityEngine refs)Core
TR-tide-002Tidelong UTC seconds, StableHash64 splitmix, explicit FloorDiv/FloorMod; System.Random/GetHashCode/Mathf/float banned in schedule mathDeterminism
TR-tide-003TideHost-authoritative t replicated on join/change; client extrapolation presentation-onlyNetworking
TR-tide-004TideMonotonic clock guard lastKnownGoodT, persistedSave/Load
TR-tide-005TideNextBoundary schedule-one-wake-up transition dispatch, no polling, ±5 sTiming
TR-tide-006TideDock/undock edge events + PreloadLeadTime notice subscribable by LDUEvents
TR-tide-007TideDerived-only API: (docked, progress) pair, cycleIndex, epochIndex, variationSeed, templateForDocking; raw t/φ/W never exposed to UI/renderAPI
TR-tide-008TideamberIntensity/violetIntensity floats; violet held 1.0 through EvictionGraceRendering
TR-tide-009TideTideConfig data-driven, load-validated (EC 15–18), versioned; epoch boundary on N changeConfig
TR-tide-010Tide≤ 50 µs/call avg, 0 bytes alloc/call (hard gates per TD ruling), ≤ 0.1 ms/frame (advisory)Performance
TR-net-001NetcodeNGO 2.x host-and-play; no dedicated server, no host migration, no rollbackNetworking
TR-net-002NetcodeTwo authority rails: owner (movement, dash, CARRY, predicted FX) / host (damage, HP, storage, commits, registries, marks)Networking
TR-net-003NetcodeLate-join full snapshot: seed+config+t, structure registry, SharedStorage, Known* sets, full InstanceMetadata, player statesNetworking
TR-net-004NetcodeDisconnect despawns the body; HP/Downed/bleed-out timer retained session-scoped; rejoin = late joinState
TR-net-005NetcodeSteam transport (package TBD) — ADR after playtestTransport
TR-net-006NetcodeAll host-rail commit RPCs share one ordered arrival channel (or explicit sequence numbers); tiebreak lowest OwnerClientIdNetworking
TR-net-007NetcodeEvery host-rail commit is a single non-yielding synchronous critical section on the host main thread (TD-C1)Concurrency
TR-net-008NetcodeExecution-phase ordering of host commits vs. owner movement writes pinned (TD-C2)Timing
TR-cam-001CameraOrthographic, 64 px/u at 1080p, integer multiples elsewhere; PPU 32, point filterRendering
TR-cam-002CameraSmoothed follow + aim bias; never detaches; hard cut on deposit/respawn; no shakeCore
TR-save-001SaveOne host-owned file; write-temp-then-swap; rolling backupsSave/Load
TR-save-002SavePersisted tables per Rule 2 incl. both host-monotonic instanceId countersSave/Load
TR-save-003SavePer-confirmed-action write-through durability (saveDebounceMs 0)Save/Load
TR-save-004SavesaveFormatVersion/configVersion gate — refuse unknown, never migrate silentlySave/Load
TR-save-005SaveFixed reconcile pipeline invoking each owner's predicate; two-class rule (only HarvestDiff droppable)Save/Load
TR-save-006SaveLoad: full HP, no Downed, position per tide EC1; no merge/import path (schema-negative)Save/Load
TR-move-001MovementInput System package; one action map for KB/M + gamepad; aim vector decoupled, persists on stick releaseInput
TR-move-002MovementOwner-authoritative client-predicted locomotion, 6 u/s, instant accel; no host correction at MVPNetworking
TR-move-003MovementCollision blocks never damages: terrain, structures, bounds; no player/enemy body collisionPhysics 2D
TR-move-004MovementRemotes interpolated never extrapolated (NetworkTransform.OnUpdate path)Networking
TR-move-005MovementClosed teleport-snap list: tide deposit, respawn, BB EC21 correction (observer asymmetry → ADR)Networking
TR-move-006MovementSchema-negative: no MoveSpeedModifier, stamina, or encumbranceCore
TR-inv-001InventoryItemStack = (itemId, count, instanceTag?); CanAdd/Add/Remove/Count; per-item maxStack; tagged stacks never mergeData
TR-inv-002InventoryCarryInventory owner-auth predicted; SharedStorage host-auth, N instances sited by BBNetworking
TR-inv-003InventoryTransfers atomic host transactions in arrival order; duplication/loss invariantConcurrency
TR-inv-004InventoryNo world-item entities; 2 equipment slots outside the bagScope
TR-inv-005InventoryStack-list serialization for Save (CARRY per player, STORAGE per container id)Save/Load
TR-combat-001CombatPlayer state machine (Idle/Move, Dashing, Strike W/A/R, Downed, Dead) + non-blocking Hurt overlayCore
TR-combat-002CombatUnified strike intent (swingId, aimDirection, targetKind, nodeId?) client→host at windup startNetworking
TR-combat-003CombatHost resolves at fixed resolution instant vs. current positions; fixed tolerances; no historyNetworking
TR-combat-004CombatFeedback authority split: predicted (swing anim, whiff) vs host-confirmed (impact, stagger, death)Presentation
TR-combat-005CombatF1–F5 deterministic, AwayFromZero rounding, pure/DI-testable in EditModeCore / Testing
TR-combat-006CombatEnemy interface: EnterTelegraph(attackId); telegraph ≥ 400 ms; stagger meter; max-contribution knockback; interrupt flagAPI
TR-combat-007CombatTelegraph phase is replicated state (NetworkVariable), never local Animator-derivedNetworking
TR-combat-008CombatDash i-frames owner-auth; host wins if dash not yet received (accepted)Networking
TR-combat-009CombatDowned/revive/bleed-out/Dead → home respawn; home heal + stabilize; keep-everythingCore
TR-combat-010CombatTide soft-close evaluates before combat resolution on the same host tickTiming
TR-combat-011CombatSwingLanded/DamageAbsorbed durability events, host-confirmed onlyEvents
TR-combat-012CombatLoad validation: telegraph ≥ 400 ms, swingWindup+swingActive ≥ 0.18, DR < 1.0, HP bands > 0; unarmed 14Config
TR-combat-013CombatHUD data: own/party HP, downed indicator; damage numbers world-spaceUI
TR-ldu-001LDUTide-edge subscription; Addressables async preload at NextBoundary − PreloadLeadTime; activate at φ; despawn+unload at φ+W+EvictionGraceAssets
TR-ldu-002LDUlayout(template, variationSeed) pure: nodes, spawn points, scatter — byte-identical everywhereDeterminism
TR-ldu-003LDUKnownLandTemplates host-auth append-only, seeded at world creation, permanent-class persistedData / Save
TR-ldu-004LDUSole-source load warning; depositPoint + socketNoBuildRadius authored per socketConfig
TR-ldu-005LDUEnemy spawn contract (template, variationSeed, spawnPoints[], scalingContext)API
TR-ldu-006LDUtraversalProfile per template; stripTimeEstimate CI readout (no assert)Testing
TR-ldu-007LDUMemory: home + 1 land + 1 preloading ≤ 2 GB; one atlas per template; per-template MB budget (owner TBD)Performance
TR-harv-001HarvestingNode = host-auth swingsRemaining int, Depleted computed; replicated to clients (representation → ADR 10; NetworkList<NodeState> candidate)Data / Networking
TR-harv-002HarvestingThree-gate resolution at completion tick (Docked, swingsRemaining, CanAdd) + decrement + Add + diff write in one host critical sectionConcurrency
TR-harv-003HarvestingHarvestDiff {(epoch, socket, cycle, nodeId), swingsRemaining} sparse, synchronous writeSave/Load
TR-harv-004HarvestingReferent chosen at windup start (client, host-validated); harvest phase constants independent of combat'sCore
TR-harv-005HarvestingInteger deterministic yield F1; teardown sequenced after last-tick resolutionCore / Timing
TR-harv-006HarvestingLoad validation: monotonic bands, modifier ≤ 2.0, itemId registered, no home-island nodesConfig
TR-harv-007HarvestingHost proximity sanity check on intents; crack stages + yield flyouts host-confirmedNetworking / Presentation
TR-craft-001CraftingRecipeDef schema + 11-row load validation tableData / Config
TR-craft-002CraftingKnownRecipes host-auth group set; union reconcileData / Save
TR-craft-003CraftingcraftLegality five gates; provisional at start, authoritative at commit; consume(CARRY ∪ STORAGE-in-range) + produce + instance + mark in one host critical sectionConcurrency
TR-craft-004CraftingInstanceMetadata side table keyed by host-monotonic instanceId; permanent; orphan/mystery-gear reconcileData / Save
TR-craft-005CraftingGear hook values per tier + per-item override; ceilings validated at Crafting's loadData
TR-craft-006CraftingDurability drain from combat/harvest events; instant repair at stationPresentAtLeastCore
TR-craft-007CraftingEra gate reads KnownLandTemplates; material→source-template mapping in item data (owner unnamed)Data
TR-craft-008Crafting2 s craft beat client-predicted stance; commit host-side; no occupancy lockNetworking
TR-craft-009CraftingSchema-negative: no craft counts; no aggregation-by-crafter affordance; instant swap-in-place equipCore / UI
TR-bb-001Base BuildingStructureDef schema + 12-row load validation (incl. depositPoint ≤ socketNoBuildRadius)Data / Config
TR-bb-002Base BuildingStructureInstance {instanceId, structureId, position} host-auth registry; replication representation → ADRNetworking
TR-bb-003Base BuildingClient-local build mode; ghostSnapResult pure snap; per-frame non-allocating provisional F1Core / Performance
TR-bb-004Base BuildingplacementLegality 8 conjuncts, pure over ITerrainQuery + plain registry + position list; one host critical sectionCore / Concurrency
TR-bb-005Base BuildingdemolishLegality; refund to CARRY only; storage empty-first; inert-relic reconcileCore / Save
TR-bb-006Base BuildingIStationPresenceProvider exposing exactly stationPresentExact / stationPresentAtLeast; storage placement instantiates SharedStorageAPI
TR-bb-007Base BuildingWalls are static 2D colliders blocking players; EC21 owner-rail one-cell correction (write mode → ADR)Physics 2D / Networking
TR-bb-008Base BuildingCI gates bootstrapRatio / perimeterSessions live-recomputed from named constantsTesting
TR-bb-009Base BuildingDashed ghost outline + reach-radius grid overlay within < 100 draw callsRendering
TR-ai-001Enemy AIHost-simulated flat state machine per enemy; clients render replicated transform + telegraph events; no client AINetworking / Core
TR-ai-002Enemy AISeparation steering (movement layer only); aggro/leash/de-aggro heal; shore threshold absolute leashCore
TR-ai-003Enemy AImaxUninterruptibleTelegraphsPerTarget = 1 capCore
TR-ai-004Enemy AIComposition/patrol pure function of (template, variationSeed); no respawn per docking; fully ephemeralDeterminism
TR-ai-005Enemy AIguardDensityByBand monotonic load-check; only EnterTelegraph on the combat boundaryConfig / API
TR-ui-001UI/HUDClosed screen-space HUD list; everything else diegetic; UI Toolkit screens, world-space flyoutsUI
TR-ui-002UI/HUDCo-op affordances: edge ally indicators, storage who-took-what, world-space pingUI
TR-ui-003UI/HUDPer-cause reason strings with precedence, stability, and snap hysteresisUI
TR-ui-004UI/HUDSchema-negative aggregation ban; gamepad nav order + focus, no hover-only; ≥ 4.5:1 contrast; no input gated by animationUI / Accessibility
TR-tint-001Tide-TintOne Shader Graph sprite shader for DriftLayer sprites reading global _TideTint/_TideAmount via Shader.SetGlobal*; never MPBsRendering
TR-tint-002Tide-TintDriftLayer tag taxonomy: combat/character sprites and home structures excluded by constructionRendering
TR-tint-003Tide-TintConsumes tide floats each frame; any future post-process Render-Graph-native (Tier 1)Rendering

System Layer Map

Approved 2026-08-27 (Phase 1).

┌──────────────────────────────────────────────────────────────────────────────┐
│ PRESENTATION   UI/HUD & Co-op Affordances · Tide-Tint (minimal, MVP) ·        │
│                Feedback Presenters (combat / harvest / build / craft VFX+SFX)  │
├──────────────────────────────────────────────────────────────────────────────┤
│ FEATURE        Crafting & Gear Identity · Base Building · Enemy AI            │
├──────────────────────────────────────────────────────────────────────────────┤
│ CORE           Player Movement · Combat · Harvesting · Inventory (Carry +     │
│                SharedStorage) · Land Content (LDU: layout + registries) ·     │
│                Camera                                                          │
├──────────────────────────────────────────────────────────────────────────────┤
│ FOUNDATION     Tide Scheduler (pure) · Netcode Session (rails, HostCommitQueue,│
│                session state) · Save & Persistence · Land Lifecycle (LDU:     │
│                Addressables half) · Input (action map) · Game Data (defs +     │
│                load validation)                                                │
├──────────────────────────────────────────────────────────────────────────────┤
│ PLATFORM       Unity 6.3 LTS · URP 2D (Render Graph) · NGO 2.x ⚠️ + transport │
│                · Input System · Addressables · Physics2D (Box2D) · UI Toolkit  │
└──────────────────────────────────────────────────────────────────────────────┘

Structural decisions (binding)

Assembly layout

Assembly Unity refs Contains
Driftlands.TidenoneTR-tide-001..010 — the pure scheduler (DockedLands, StableHash64, deck, NextBoundary, tint floats)
Driftlands.Datanone*Def records, config records, the load-validation rules (fail-loud), registry lookups
Driftlands.RulesnoneEvery pure formula and legality predicate: combat F1–F5 + player state machine, harvest gates/yield/referent rule, craft legality, BB placement/demolish/FindCorrectionCell/snap, inventory stack ops + transaction check, LDU layout, enemy composition, CI-gate arithmetic. Depends on Driftlands.Tide and Driftlands.Data only.
Driftlands.RuntimeUnity, NGO, Addressables, Input System, Physics2DAdapters and authority: NetworkBehaviours, HostCommitQueue, session state, Save I/O, LandLifecycle, movement, camera, thin ITerrainQuery/registry/physics adapters that feed Rules with plain data
Driftlands.PresentationUnity, URP, UI ToolkitHUD + screens, tint driver, feedback presenters
Driftlands.Tests.EditModeTide, Data, RulesEvery [UNIT] criterion in every GDD — no scene, no NGO
Driftlands.Tests.PlayModeRuntime[INTEGRATION] criteria: Multiplayer Play Mode + Network Simulator

Why this matters: every GDD labels its formula/legality/state-machine criteria [UNIT] and several add a purity mandate (BB AC purity, combat/harvest "pure/DI-testable"). That is only achievable if the rules never reference UnityEngine. The assembly boundary makes the mandate a compile-time fact, not a discipline.

Engine-awareness flags

Layer Module Domain Risk
FoundationNetcodeSessionNGO 2.x RPC attribute surface, transport✅ verified against NGO 2.13.2 source — ADR-0008 (2026-09-11)
FoundationLandLifecycleAddressables 2.x async/releaseMEDIUM
FoundationTideScheduler, SavePersistence, GameData, Inputpure C# / System.IO / Input SystemLOW
CorePlayerMovementNetworkTransform owner-auth, OnUpdate, snap thresholds⚠️ HIGH — NEEDS VERIFICATION
CoreCombat, Harvesting, InventoryNetworkVariable<T>, RPC intents, replication representation⚠️ HIGH
CoreLandContentTilemap / Tile Set (authoring only)MEDIUM
CoreCameraorthographic CameraLOW

Module Ownership

Approved 2026-08-27 (Phase 2).

Foundation

Module Owns Exposes Consumes Engine APIs (risk)
TideSchedulerschedule math, TideConfig, t authority + monotonic guard, TideState (epoch counter + activation t)ITideClock: Evaluate(socket) → (docked, progress), CycleIndex, EpochIndex, VariationSeed, TemplateFor, NextBoundary, TintFloats, IsDockingLive (reconcile predicate); events DockStarted, SoftClose, PreloadDuehost UTC (via Runtime adapter); TideState from Savenone (pure). Runtime adapter replicates t as NetworkVariable<long> ⚠️
NetcodeSessionrail assignment, HostCommitQueue, session-scoped player state (HP/Downed/timer), join/leave flow, late-join snapshot orchestrationIHostCommitQueue.Submit(ICommand) (host executes in arrival order, one critical section each); SessionState per client; IsHost/IsOwnerevery host-rail system's command types; Save write-through hook per committed commandNetworkManager, NetworkBehaviour, RPCs ⚠️ HIGH (2.x attribute surface NEEDS VERIFICATION), transport ⚠️ (post-playtest ADR)
SavePersistencefile format, atomic write, backups, version gate, reconcile orchestrationSave(ISaveable[]), Load() → reconcile pipeline, Persist(table) write-through — dirty-marks inside the critical section; file I/O runs off it (LP review: a synchronous serialize + file swap inside every commit collides with the 16.6 ms budget — ordering + I/O budget are ADR 4's, QQ-11)each owner's ISaveable + reconcile predicateSystem.IO, Application.persistentDataPath (LOW)
LandLifecycleAddressables handles per land (one InstantiateAsync handle per slot), preload/activate/teardown timing, LandNet spawn/despawn, landTemplateBudgetMB = 64 (ADR-0005)ILandLifecycle: LandActivated(LandInstance, root), SoftCloseDeposit, EvictionExpired, LandTornDown; StateOf, InstanceOfITideClock events (host); IPreloadSchedule pull (client)Addressables.InstantiateAsync/ReleaseInstance/LoadResourceLocationsAsync (MEDIUM)
Inputthe project-wide Driftlands.inputactions (Player / Build / UI maps; KeyboardMouse / Gamepad schemes), InputAdapter (one instance via InputSystem.actions, Update @ −1000), build-mode routing, UI capture, bindings.json (ADR-0007)IPlayerInput.Current : InputFrame (Move, Aim, AimIsPointer, PointerWorld, edge/Held/HeldFor per action incl. Revive + Demolish, BuildModeActive, UiCaptured); IRebindServiceInputConfig (input.json)Input System 1.20 (LOW core / MEDIUM project-wide + UI Toolkit direct consumption)
GameDataloading + fail-loud validation of every *Def/config table; registry lookupsIGameData.Items/Recipes/Structures/Enemies/Templates/Configs (read-only)JSON under assets/data/ (per GDDs)file load (LOW)

Core

Module Owns Exposes Consumes Engine APIs
PlayerMovementowner-auth transform, locomotion, aim, teleport-snap list, collision layer configIAimSource.AimVector, IPlayerSnapper.Snap(key, reason, pos) (host) / ISnapSource.Snapped (owner), HostKnownPosition(client)IPlayerInput; blockers from Physics2DNetworkTransform owner-auth ⚠️ HIGH (OnUpdate, interpolation buffer, Teleport() vs interpolated write — NEEDS VERIFICATION), Rigidbody2D/Collider2D (LOW)
CombatCombatState actor table (single writer, keyed by PlayerKey, session-scoped), dual-run PlayerCombatMachine, StrikeResolve/StrikeRevoke/DashStarted/Revive* commands, EnemyAttackResolver → phase 0, HostTickOrchestrator step order (ADR-0015)ICombatState (HostContext.Combat), IPlayerCombatView, ~~IStrikeResolver~~, ~~ICombatActor~~ (HP, state), IEnemyCombat (EnterTelegraph, stagger, knockback, interrupt), events SwingLanded, DamageAbsorbed, PlayerDowned/PlayerDiedRules.Combat formulas; IAimSource; ITideClock.Evaluate at resolution; IGearHooks from Crafting; IHostCommitQueueNetworkVariable<TelegraphPhase> ⚠️, intent RPC ⚠️
Harvestingnode state (swingsRemaining), three-gate resolution command, HarvestDiff write, referent-selection ruleIHarvestResolver, NodeView (replicated swingsRemaining), HarvestDiffTable (ISaveable, droppable)ITideClock, ICarryInventory.CanAdd/Add, LandContent node instances, IHostCommitQueuenode replication: NetworkList<NodeNetRow> on LandNet — ADR-0010 ✅ (LP review: one NetworkList<NodeState> on the land instance is the idiomatic NGO 2.x shape; a NetworkVariable<int> per node — 40/land, ~80 with a preloading land — is rejected as an avoidable footprint)
InventoryItemStack ops, CarryInventory (owner rail), SharedStorage (host rail), transfer commandsICarryInventory, ISharedStorage, IInventoryTx (atomic CanRemove/Remove over a CARRY ∪ STORAGE set, CanAdd/Add)IGameData.Items (maxStack); IHostCommitQueueowner-rail replication of CARRY, CARRY = owner fold over outcomes + CarrySync; SharedStorage = slot-indexed NetworkList<StackNetRow> on StorageNet — ADR-0010 ✅
LandContentlayout(), KnownLandTemplates, socket layout (depositPoint, exclusion), traversalProfile, spawn-contract callIKnownLandTemplates (ISaveable, permanent), ISocketLayout, LandInstance (nodes, spawn points)ITideClock.VariationSeed/TemplateFor; LandLifecycle.LandActivated; IGameData.TemplatesTilemap / Tile Set (MEDIUM — authoring only)
Camerafollow, aim bias, integer zoom, hard cuts—IAimSource; snap eventsCamera orthographic (LOW); Cinemachine not used

Feature

Module Owns Exposes Consumes Engine APIs
Craftingrecipes/legality, KnownRecipes, InstanceMetadata + gear instanceId counter, craft-commit command, durability state, equipICraftService, IGearHooks(instance) → DM/DR/HM, IInstanceMetadata (ISaveable, permanent), IKnownRecipes (ISaveable, union)IInventoryTx, IStationPresenceProvider, IKnownLandTemplates, ITideClock.CycleIndex, combat/harvest durability events, IHostCommitQueuecommit RPC ⚠️
BaseBuildingStructureRegistry + structure instanceId counter, build mode (client), placement/demolish commands, stationPresent*, socket exclusion, EC21 correction triggerIStationPresenceProvider (exactly two predicates), IStructureRegistry (ISaveable, permanent), PlaceStructure/Demolish commandsIInventoryTx, ISocketLayout, ITerrainQuery, PlayerMovement.HostKnownPosition, IHostCommitQueue; instantiates ISharedStoragestatic BoxCollider2D per registry row (ADR-0011); registry = NetworkList<StructureNetRow> on in-scene BaseNet (ADR-0011 ✅); EC21 = owner-side interpolated write on the list Add event (ADR-0009/0011 ✅)
EnemyAIper-enemy host state machines, steering, aggro/leash, composition from seed, telegraph-density capIEnemySpawner.Spawn(template, seed, points, ctx)LandContent spawn contract; IEnemyCombat.EnterTelegraph; host-side player positionshost-only EnemyAI.Tick (step 3) over one pure EnemyBrain.Step; one NetworkObject per enemy — EnemyTransform Server-auth + EnemyNet{Phase, AnimFrame, Archetype, ActorId} (ADR-0012 ✅)

Presentation

Module Owns Exposes Consumes Engine APIs
UIHUDclosed HUD list, screens (recipes, build picker, bag/storage), reason-string display rules, co-op affordances—read-only views from every system (HP, bag, registry copy, tide floats + docked bools, recipe list with reasons)UI Toolkit UIDocument/USS (MEDIUM — strict USS)
TideTintthe DriftLayer sprite shader + global-property driver—ITideClock.TintFloats per frameShader.SetGlobalFloat/Color (LOW); Shader Graph URP 2D (MEDIUM); no custom Render Graph pass at MVP
FeedbackPresenterspredicted-vs-confirmed effect timing (combat, harvest, build thunk, craft beat)—events from Combat/Harvesting/BB/Crafting split by authoritySpriteRenderer, particles, audio (LOW)

Dependency diagram (arrows = depends on)

 UIHUD ──┐   TideTint    FeedbackPresenters
         │       │              │
         ▼       ▼              ▼
   Crafting ──► BaseBuilding    EnemyAI
      │  ▲         │  │           │
      │  └─────────┘  │           │            (Crafting ⇄ BB: peer — Crafting reads
      ▼               ▼           ▼             IStationPresenceProvider; BB reads
   Inventory ◄── Harvesting   Combat ◄──── PlayerMovement    IGameData.Items only)
      │               │          │                 │
      └───────┬───────┴──────────┴────────┬────────┘
              ▼                           ▼
      NetcodeSession (HostCommitQueue)   LandContent ◄── LandLifecycle
              │                           │                  │
              ▼                           ▼                  ▼
      SavePersistence ◄──────────── TideScheduler ◄──── (events)
              ▲
           GameData ── Input ── Camera (leaf; reads Movement aim only)

Every arrow points downward or sideways within a layer; the one lateral peer link (Crafting ⇄ BB) is interface-only, matching the GDDs' "peer both ways" contract. No Foundation module depends on anything above it.

Data Flow

Approved 2026-08-27 (Phase 3).

3.1 Frame update path

HOST frame                                        CLIENT frame
──────────                                        ────────────
Input System ─► PlayerMovement (owner rail)       Input System ─► PlayerMovement (owner rail)
   │ own transform written locally                    │ own transform written locally; NetworkTransform
   │ (host IS an owner for its own player)            │ sends state to host (one RTT stale on arrival)
   ▼                                                  ▼
Combat/Harvest/BB/Craft (owner half):             Combat/Harvest/BB/Craft (owner half):
   issue intents/commands ─► HostCommitQueue          issue intents/commands ─► RPC ⚠️ ─► host queue
   (local submit, same frame)                         predicted FX fire now (stance, whiff, ghost)
   ▼
HostTickOrchestrator (sole NetworkTickSystem.Tick subscriber — ADR-0015): tide Advance ─► CombatState.Tick (bleed-out, enemy phases, revive, home heal) ─► EnemyAI.Tick() (host only) ─► IEnemyCombat.EnterTelegraph → NetworkVariable<EnemyPhase>; EnemyAttackResolver → PendingEnemyDamage ─► Drain;
   │                             attacks whose resolution instant ≤ now → PendingEnemyDamage (ADR-0002)
   ▼
HostCommitQueue.Drain()  ── hooked to NetworkManager.NetworkTickSystem.Tick (ADR-0002; fires before
   │ NetworkVariable delta collection). ⚠️ The stage in which NGO applies incoming RPCs/NetworkTransform
   │ relative to Tick is undocumented — measured by ADR-0002 Verification 1–2, recorded in ADR-0008:
   │   1. SoftClose set (sockets closed since last drain); cancel enemy attacks on those lands
   │   1b. phase 0: apply PendingEnemyDamage (combat AC-32 — enemy first, then player)
   │   2. for each envelope in arrivalSeq order: drop if ResolveTick ≥ SoftCloseTick(socket), else
   │      cmd.Execute(ctx) → CommandResult  ← single synchronous non-yielding block; re-samples every gate
   │   3. release teardown for soft-closed lands (harvesting EC6)
   │   4. flush buffered Broadcast/QueueSpawn in command order; Save.Persist dirty-marks → I/O after Drain
   ▼
Replication out: NetworkVariables + host-confirmed broadcasts  ─────────►  clients apply:
                                                                  NetworkTransform.OnUpdate (interp),
                                                                  NodeView/HP/telegraph vars,
                                                                  confirmed FX (impact, thunk, burst)
   ▼                                                  ▼
TideScheduler.TintFloats ─► TideTint.SetGlobal       same, from replicated t (presentation-only)
Camera.LateUpdate (follow + aim bias)                 same
UIHUD reads views                                     same

Classification: owner-rail writes are local synchronous; intents are RPC (async, ordered channel); host commit execution is synchronous, single slot; replication is NGO shared state; tint/HUD are pull reads. No data flow crosses a thread boundary — Addressables loads are the only async work and they never touch gameplay state until LandActivated fires on the main thread.

3.2 Event / signal path

Event Producer Consumers Mechanism
PreloadDue(socket, template), DockStarted(socket), SoftClose(socket)TideScheduler (host, from NextBoundary wake-up)LandLifecycle, LandContent, Combat (state discard), Harvesting (teardown ordering), TideTint (held violet)C# events on the host; clients receive the consequences via replication, never the event
LandActivated(LandInstance, root)LandLifecyclehost: after Layout.Build → Harvesting.SeedNodeState → LandNet spawn → Append (ADR-0005 order); consumers: Harvesting node views, IEnemySpawner.Spawn. Clients: raised from LandNet.OnNetworkSpawnC# event on each machine; LandNet spawn is the NGO edge
SoftCloseDeposit(socket) at φ+W, EvictionExpired(socket) at +EvictionGraceLandLifecycle (host)ADR-0009 (owner teleport to depositPoint)host C# events
Host-confirmed outcomes: SwingLanded/DamageAbsorbed (ADR-0015), HarvestResolved, StructurePlaced/Demolished, CraftCommitted, PlayerDowned/Diedthe executing command inside HostCommitQueueFeedbackPresenters, UIHUD, Crafting durability (SwingLanded/DamageAbsorbed), Save write-throughOutcomeRpc — one [Rpc(SendTo.ClientsAndHost)] on HostCommitChannel, flushed from drain phase 4 (ADR-0008 §3/§5); may arrive one tick before the same drain's NetworkVariable delta
Predicted-only: swing start, ghost preview, craft stanceowner clientFeedbackPresenters (local only)local C# event; never replicated
Session: ClientJoined, ClientLeftNetcodeSessionlate-join snapshot builder; session-state retention; body despawnNGO NetworkManager callbacks (LOW)

Rule: there is no global event bus. Each producer exposes typed C# events on its interface; presentation subscribes downward; nothing subscribes upward. The HostCommitQueue is the only fan-in point.

3.3 Save / load path

WRITE (host only, per confirmed command):
  cmd.Execute ─► owner table mutates ─► Save.Persist(table)  [dirty-mark only — ADR-0004]
  Drain returns ─► Save.EndOfDrain: serialise dirty tables (main thread, snapshot) ─► background flush:
                   write-temp → fsync / F_FULLFSYNC (macOS) → File.Replace → rotate 3 backups; lastDurableDrain ← N
  (durability is per-drain, broadcast-first; loss bound = wall-clock flushDeadlineMs — ADR-0004 §4)

LOAD (host, before NetworkManager.StartHost):
  Save.Load()
    1. version gate (saveFormatVersion, configVersion) → refuse loudly
    2. clock guard: TideScheduler.Restore(TideState{lastKnownGoodT, epochIndex, epochActivationT})  (future t wins)
    3. TideScheduler derives the cycle purely; the EPOCH is restored, not derived (tide EC14 — ADR-0001)
    4. union reconcile: IKnownLandTemplates, IKnownRecipes   (owners' predicates)
    5. droppable reconcile: HarvestDiffTable via ITideClock validity predicate
    6. permanent reconcile: IStructureRegistry (inert relics), IInstanceMetadata (orphans)
    7. inventories: CARRY per player, SharedStorage per container id (tag-without-row → mystery gear)
    8. player placement: saved position on Absent land → depositPoint; HP full; Downed cleared
  then StartHost → late-joiners receive the reconciled tables (TR-net-003)

Owners implement ISaveable (serialize/deserialize + Reconcile(ctx)); Save owns only ordering and I/O. Both instanceId counters are ISaveable singletons owned by Crafting and BaseBuilding respectively.

3.4 Initialisation order

1. GameData.LoadAndValidate()          — fail-loud; nothing else may start on failure
2. Input (action asset)                — no dependencies
3. Save.Load() [host]                  — needs GameData for reconcile predicates
4. TideScheduler.Restore(TideState)    — needs Save's lastKnownGoodT + epoch state; pure otherwise
5. NetcodeSession.StartHost / StartClient ⚠️  — needs tide t to replicate; HostCommitQueue armed
6. LandLifecycle.Subscribe(tide)       — needs tide + Addressables ready; first evaluation may Preload immediately
7. LandContent / Inventory / Crafting / BaseBuilding registries bound to Save tables
8. Player spawn (Movement, Combat actor, CarryInventory)  — needs session + reconciled CARRY
9. Camera, TideTint, UIHUD, FeedbackPresenters            — presentation last; all pull-only

Clients skip step 3 and receive the tables in the late-join snapshot between steps 5 and 7.

3.5 Key scenario walkthroughs

Strike at a node with a Rusher closing (harvesting Rule 2 / combat Rule 7):

client: Input.Strike ─► Rules.Referent.Choose(aim, nearbyHostiles, nearbyNodes) → targetKind, nodeId?
        Combat.IssueIntent(swingId, aim, targetKind, nodeId?) ─► predicted windup anim (phase constants by kind)
        ─► RPC ⚠️ ─► host queue (arrival-ordered)
host:   at completion tick: SoftClose? → discard/whiff
        targetKind == node  → HarvestResolveCommand: Docked ∧ swingsRemaining>0 ∧ CanAdd → decrement, Add, Persist(HarvestDiff)
        targetKind == enemy → StrikeResolveCommand: Rules.Combat.HitArc(current positions) → F1/F2, stagger, knockback, SwingLanded
        broadcast confirmed outcome ─► presenters (burst / impact) ; rejected → nothing (honest whiff)

Wall placement (BB Rule 6):

client: BuildMode (local) ─► Rules.BB.Snap → ghost; provisional F1 every frame (no alloc) ─► Confirm ─► PlaceStructureCommand RPC ⚠️
host:   queue slot: re-sample all 8 conjuncts via ITerrainQuery + registry + IHostPositions.All() (ADR-0009: raw last-received owner state, ≤ 1 RTT + 1 tick, snap-overridden)
        → IInventoryTx.Remove(cost, CARRY ∪ STORAGE-in-range-of-spot) → registry.Add → Persist → broadcast
clients: BaseNet.Structures list delta (ADR-0011) → ClientRegistry → StructureView (static BoxCollider2D on Structure + sprite, Physics2D.SyncTransforms) ; local player under footprint → Rules.Build.FindCorrectionCell → PlayerMotor.CorrectTo (EC21, interpolated write — ADR-0009)

Dock transition (tide Rule 8/9):

host: NextBoundary wake-up ─► PreloadDue ─► LandLifecycle.LoadAsync (Addressables) ─► cosmetic drift-in
      at φ: DockStarted ─► Layout.Build ─► Harvesting.SeedNodeState ─► spawn LandNet ─► colliders on ─► KnownLandTemplates.Append ─► LandActivated → node views, EnemySpawner (ADR-0005; clients activate on LandNet spawn)
      at φ+W: SoftClose ─► (same tick, first) combat discards ≥φ+W, harvest last-tick completes, SoftCloseDeposit → deposit at depositPoint (ADR-0009); drain step 3 → DrainReleased
      +EvictionGrace: GraceExpired ∧ DrainReleased ─► EvictionExpired (EC11 backstop) ─► despawn LandNet ─► LandTornDown ─► release slot; violet held until then

API Boundaries

Approved 2026-08-27 (Phase 4). C# — these are the contracts programmers implement against. Names are pinned at implementation; shapes are binding.

// ── Foundation ────────────────────────────────────────────────────────────────
public interface ITideClock {                      // Driftlands.Tide — pure; Runtime adapter supplies t
    (bool docked, double progress) Evaluate(int socket);   // pair: progress unreadable when !docked
    long   CycleIndex(int socket);   int  EpochIndex { get; }
    ulong  VariationSeed(int socket, long cycleIndex);
    TemplateId TemplateFor(int socket, long cycleIndex);
    long   NextBoundary();                          // strictly > t, ≤ t + C
    (float amber, float violet) TintFloats(int socket); // held (0,1) during EvictionGrace
    bool  IsDockingLive(int epochIndex, int socket, long cycleIndex); // the reconcile validity predicate the scheduler owns (ADR-0001, TD-ADR C3)
    event Action<int,TemplateId> PreloadDue; event Action<int> DockStarted; event Action<int> SoftClose;
}
// Invariants: no raw t/φ/W/C leaves this interface (tide AC-27); consumers never cache Evaluate() across ticks.

public interface ICommand { ulong OwnerClientId { get; } uint ClientSeq { get; } CommandResult Execute(HostContext ctx); }  // ADR-0002: returns CommandResult; ClientSeq owner-stamped
public readonly record struct QueuedCommand(ICommand Command, uint? ArrivalSeq);   // host-assigned arrival order (BB AC-20b's optional field lives here)
public interface IHostCommitQueue {                // NetcodeSession — the only fan-in (ADR-0002)
    void Enqueue(CommandEnvelope env, ulong ownerClientId);   // host only; wire entry is HostCommitChannel.SubmitRpc — ONE [Rpc(SendTo.Server)] on ONE NetworkObject
    void Drain(HostContext ctx);                    // once per NetworkTickSystem.Tick
    // Guarantee: Drain() runs once per host tick in the pinned phase (TR-net-008), executes commands in
    // arrival order (ArrivalSeq; lowest OwnerClientId only when seq is absent), each Execute() is one
    // synchronous non-yielding block, SoftClose is evaluated before any command on that tick.
    // Callers must: re-sample every gate inside Execute; never hold state across commands; never await.
}
public interface ISaveable { string TableId { get; } SaveClass Class { get; } /* Permanent | Droppable */
    void Serialize(ISaveWriter w); void Deserialize(ISaveReader r); void Reconcile(ReconcileContext ctx); }
public interface IGameData { IReadOnlyDictionary<ItemId,ItemDef> Items { get; } /* Recipes, Structures, Enemies, Templates, Configs */ }
public interface IPlayerInput { InputFrame Current { get; } }   // ADR-0007: one immutable per-frame snapshot (Driftlands.Rules.Input) — edges computed once; edge consumers run in Update only

// ── Core ──────────────────────────────────────────────────────────────────────
public readonly record struct StrikeIntent(uint SwingId, Vector2 Aim, TargetKind Kind, NodeId? Node);
// IStrikeResolver RETIRED by ADR-0015 — StrikeResolveCommand IS the resolver; node-kind intents are routed to HarvestResolveCommand at receipt. StrikeIntent above lives in Driftlands.Data (shared vocabulary, neither system owns it); IntentTick is host-stamped, never in the payload.
public interface ICombatState { /* ADR-0015: host-owned actor table keyed by PlayerKey/EnemyId — Player/Enemy views, HostSeesDashing, MirrorStep, ApplyHit, ApplyEnemyDamage, TryStartRevive/CancelRevive, RevokeSwing */ }
public interface IPlayerCombatView { int Hp { get; } int MaxHp { get; } PlayerState State { get; } float BleedOutRemaining { get; } }   // HUD/presenters — never enemy HP; SwingLanded/DamageAbsorbed are raised from the drain flush only (replaces ICombatActor — ADR-0015)  // two payload records (ADR-0003, TD-ADR C4)
public interface IEnemyCombat { void EnterTelegraph(AttackId id); bool Interruptible { get; } float Meter { get; } EnemyPhase Phase { get; } }   // ADR-0015: Meter is a float (F4 operates on floats); Phase exposed for enemy-ai Rule 6's cap check; EnterTelegraph is the ONLY AI→combat call
public interface IAimSource { Vector2 AimVector { get; } }               // Movement → Combat, BB ghost, Camera — RETAINED by ADR-0007 as a Movement pass-through (AimVector => input.Current.Aim); aim is computed in Rules.Input.AimResolver
public interface IHostPositions { Vector2 HostKnown(ulong clientId); }  // exact for host's own player, one-RTT stale for remotes (BB F1)
public readonly record struct ItemStack(ItemId Id, int Count, InstanceTag? Tag);
public interface ICarryInventory { bool CanAdd(ItemId id, int n); void Add(ItemStack s); bool CanRemove(ItemId id, int n); void Remove(ItemId id, int n); int Count(ItemId id); }
public interface ISharedStorage : ICarryInventory { ContainerId Id { get; } int ContentCount { get; } }
public interface IInventoryTx {                    // atomic over a CARRY ∪ STORAGE-set; host only, inside Execute
    bool CanRemove(IEnumerable<(ItemId, int)> cost, ICarryInventory carry, IReadOnlyList<ISharedStorage> inRange);
    void Remove(...);  bool CanAdd(ICarryInventory target, ItemStack s); }
public interface IKnownLandTemplates : ISaveable { bool Contains(TemplateId t); void Append(TemplateId t); }  // append-only, seeded
public interface ISocketLayout { IReadOnlyList<Socket> Sockets { get; } }  // Socket = (index, position, depositPoint, noBuildRadius)
public interface IEnemySpawner { void Spawn(TemplateId t, ulong variationSeed, IReadOnlyList<SpawnPoint> pts, ScalingContext ctx /* empty at MVP */); }

// ── Feature ───────────────────────────────────────────────────────────────────
public interface IStationPresenceProvider {        // BaseBuilding → Crafting; exactly these two, never a mode arg
    bool StationPresentExact(StationTier tier, Vector2 asker);
    bool StationPresentAtLeast(StationTier tier, Vector2 asker); }   // never cached host-side
public interface IGearHooks { float DamageModifier(InstanceId i); float DamageReduction(InstanceId i); float HarvestModifier(InstanceId i); } // dull → tier baseline
public interface ITerrainQuery { bool IsBuildable(Cell c); bool InPlaceableRegion(Cell c); }  // plain-data adapter; Rules never touch Tilemap
public interface IStructureRegistry : ISaveable { IReadOnlyList<StructureInstance> All { get; } bool AnyCovers(Cell c); }

Boundary invariants (binding):

ADR Audit

Phase 5, 2026-08-27: no ADRs existed. Superseded by /architecture-review 2026-08-28 (architecture-review-2026-08-28.md): 8 ADRs audited — 8/8 carry Engine Compatibility on 6000.3, 0 deprecated-API references, 3 cross-ADR conflicts (C1–C3, amended same day), 13 specialist findings (E1–E13).

Traceability Coverage

2026-08-28: 111 requirements — 64 covered, 28 partial, 19 gaps (Foundation layer: 0 gaps). Full matrix in traceability-index.md; stable IDs in tr-registry.yaml. Note TR-craft-003/004/006/008 (crafting runtime half) are assigned to no row below — see the review's Required ADRs (extend ADR-0010 or add row 16).

Required ADRs

Phase 6, 2026-08-27. ⏳ = the wire half is gated on the friend-group playtest (netcode-session Rule 7); the model half can be drafted as Proposed now. ⚠️ = HIGH-risk engine domain, verification step required before Accepted.

Foundation — must exist before any coding

# ADR Covers Gate
1Tide Scheduler as a pure engine-free assembly — Driftlands.Tide, typed math, TideConfig versioning, host t replication adapter, perf gates (AC-44/45 hard, AC-46 advisory — TD ruling)TR-tide-001..010none
2Host commit model: single ordered HostCommitQueue, non-yielding critical sections, tick-phase ordering (TD-C1 + TD-C2) — pinned execution phase for Drain vs NetworkTransform receive vs owner writes (candidates: FixedUpdate, a NetworkUpdateStage registration, NetworkTickSystem.Tick — must verify NGO 2.x still exposes NetworkUpdateLoop/NetworkUpdateStage on 6000.3; the project reference never checked it); arrival-seq/tiebreak rule; SoftClose-first; enforcement (analyzer or review rule)TR-net-006/007/008, TR-combat-010, TR-harv-002/005, TR-craft-003, TR-bb-004, TR-inv-003none — answerable from NGO NetworkUpdateLoop docs ⚠️ verify
3Assembly + purity boundaries — Tide/Data/Rules/Runtime/Presentation/Tests asmdefs; Rules has no Unity refs; plain-data adapters (ITerrainQuery, IHostPositions); one Runtime-owned UnityInterop conversion utility (Driftlands Vector2/Cell ↔ UnityEngine); shapes for the five referenced-but-undefined types — HostContext, ReconcileContext, Cell, LandInstance, HitEvent — plus one worked concrete command class showing intent-time fields vs Execute-time re-sampled state (LP review, QQ-12); gridCellSize/snap constants pinnedTR-combat-005, TR-bb-003/004, TR-harv-004/005, TR-ai-004, TR-ldu-002, TR-craft-009, TR-move-006none
4Save file format + reconcile pipeline — JSON vs binary, location, write-temp-then-swap, backups, version gate, ISaveable + two-class rule, the 8-step order, both instanceId counters; resolves the TR-net-007 ↔ TR-save-003 tension: Persist dirty-marks synchronously, I/O runs off the critical section (Awaitable/worker) with in-order completion and a stated per-write budget; "confirmed" durability semantics defined (LP review, QQ-11)TR-save-001..006, TR-tide-004, TR-harv-003, TR-craft-002/004, TR-ldu-003, TR-inv-005, TR-bb-005none
5Land lifecycle on Addressables — group/label scheme (one atlas per template + core), preload at NextBoundary − PreloadLeadTime, teardown at +EvictionGrace, release discipline, per-template MB budget owner + number (TD-C4)TR-ldu-001/007, TR-tide-006none (MEDIUM — Addressables 2.x)
6Game data loading + fail-loud validation — assets/data/*.json → Driftlands.Data; every GDD validation table as one boot gate; material→source-template mapping owner (crafting Rule 3b)TR-tide-009, TR-combat-012, TR-harv-006, TR-craft-001/005/007, TR-bb-001, TR-ai-005, TR-ldu-004none
7Input action map — one .inputactions, Player + Build maps, KB/M + gamepad schemes, decoupled aim, rebind persistenceTR-move-001, TR-ui-004 (gamepad nav)none (LOW)

Core — before the relevant system is built

# ADR Covers Gate
8✅ ADR-0008 (2026-09-11, Accepted under the Gate 0 provisional pass) — Netcode wire: transport, RPC shapes, replication representations — Steam transport package; the 2.x RPC attribute surface ⚠️; per-state NetworkVariable-vs-RPC choices (telegraph phase, swingsRemaining, HP, tide t); late-join snapshot mechanics; intent-validation bounds; send rates; relevancy = noneTR-net-001..005, TR-combat-002/003/007, TR-harv-001/007, TR-craft-008, TR-ai-001, TR-tide-003written 2026-09-11; friend playtest = confirmation (revisit clause)
9✅ ADR-0009 (2026-09-11, Accepted under the Gate 0 provisional pass) — Player transform authority — NetworkTransform owner-auth config, OnUpdate interpolation, remoteInterpBuffer, the closed teleport-snap list, EC21 write mode (Teleport() vs interpolated — ~30-min prototype check first, TD-C3)TR-move-002/004/005, TR-combat-008, TR-bb-007written 2026-09-11; §6 interpolation provisional pending the playtest CSV; QQ-02 = its V2
10✅ ADR-0010 (2026-09-11, Accepted under the Gate 0 provisional pass) — Inventory replication — CARRY owner-rail representation + prediction; SharedStorage host-rail representation (component / NetworkObject / replicated record); IInventoryTx implementationTR-inv-001..004, TR-bb-006 (storage instantiation)partially ⏳ (representation rides ADR 8); micro-spike recommended (TD-C5)
11✅ ADR-0011 (2026-09-11, Accepted under the Gate 0 provisional pass) — Structure registry replication + wall colliders — NetworkObject-per-structure vs replicated list; snapshot-vs-in-flight ordering; static Collider2D instantiation; Physics2D layer matrix (no player/enemy body collision)TR-bb-002/007, TR-move-003⏳ rides ADR 8
12✅ ADR-0012 (2026-09-11, Accepted under the Gate 0 provisional pass) — Enemy simulation model — host-only flat state machines, separation steering, transform replication cadence, spawn contract from LandContent, scalingContext reservedTR-ai-001..005, TR-ldu-005partially ⏳ (cadence)

Presentation — before the relevant screen/shader is built

# ADR Covers Gate
13Tide-tint render pipeline (minimal, MVP) — Shader Graph URP 2D sprite shader, global _TideTint/_TideAmount, DriftLayer tag taxonomy with the combat-sprite/home-structure exclusion, no custom Render Graph pass; BB ghost/grid overlay within the draw-call budget (TD-C4)TR-tint-001..003, TR-tide-008, TR-bb-009, TR-cam-001ADR-0013 (Accepted 2026-09-01) — the HIGH-domain verification was executed and it changed the design: both globals must be declared at Scope = Global on the Shader Graph blackboard (review E14; Show In Inspector is not the control)
14UI stack — UI Toolkit for screens, world-space flyouts (UGUI or sprites), USS linting, reason-string precedence/hysteresis contract, aggregation-ban schema checkTR-ui-001..004, TR-combat-013ADR-0014 (Accepted 2026-09-01) — world-space UI resolved to sprites (not UGUI); no uGUI EventSystem anywhere; world-space UI draws from the separate UI atlas at ≈ +2 draw calls (review F1)
15Combat model — owner of ICombatState (HP/state per actor, stagger meters, ApplyDamage, Downed/Dead/revive transitions, PendingEnemyDamage production for ADR-0002 phase 0), StrikeResolveCommand, revive-channel edges; added 2026-08-28 (ADR-0003 TD-ADR C7 — the largest un-owned surface in HostContext)TR-combat-001/003/005/006/008/009/011, TR-ai-003partially ⏳ (telegraph replication rides ADR 8)

Can defer to implementation

Camera follow implementation (TR-cam-002 — a script, not a decision); feedback-presenter timing (TR-combat-004, TR-harv-007 presentation halves); CI-gate test harness (TR-bb-008, TR-ldu-006 — test code).

Coverage: 102/102 requirements assigned. ADRs 1–7 and 13–14 (9 of 14) are writable now; ADRs 8–12 can be drafted Proposed today and finalised after the playtest.

Architecture Principles

Open Questions

ID Summary Priority Resolution path
QQ-01Exact host execution phase for HostCommitQueue.Drain vs NetworkTransform receive vs owner movement write (TD-C2)HighADR 2 — from NGO 2.x NetworkUpdateLoop docs ⚠️
QQ-02Does a one-cell NetworkTransform write survive snap thresholds as an interpolated update (EC21 observer asymmetry, TD-C3)?Closed 2026-09-11ADR-0009 §5 ratified the interpolated write (no receive-side snap threshold in NGO 2.13.2 source); V2 verifies in play
QQ-03NGO 2.x RPC attribute surface ([Rpc(SendTo.…)] vs ServerRpc/ClientRpc) — project reference shows 1.xClosed 2026-09-11ADR-0008: [Rpc(SendTo.…)] verified against the 2.13.2 source; InvokePermission supersedes the deprecated RequireOwnership
QQ-04~~Steam transport package (Facepunch / Heathen / Unity relay)~~ **RETITLED 2026-09-02: "which Steam transport, when we ship" — downgraded High→Low and taken OFF the playtest critical path. Research (2026-09-02) established the load-bearing fact: NGO's NetworkTransport abstract surface is stable across 1.x→2.x** — verified directly against the NGO develop source: exactly 10 abstract members (ServerClientId, Send, PollEvent, StartClient, StartServer, DisconnectRemoteClient, DisconnectLocalClient, GetCurrentRtt, Shutdown, Initialize), IsSupported virtual, and NGO 2.2's OnEarlyUpdate/OnPostLateUpdate are optional additions, not abstract requirements. The anticipated "NGO 2.0 broke custom transports" is not true — the real historical break was NGO 1.0.0-pre.6 (2022), long since absorbed. Therefore the transport is swappable after the playtest without invalidating its findings, because the netcode model (owner-auth movement, host-auth damage, local-only prediction) sits entirely above a pure byte-pipe. Decision: run the playtest on UTP + Unity Relay (first-party, tracks NGO 2.x exactly, free at 50 CCU vs. a 2-player need, no App ID, no router config, ~35 lines behind an ISessionLauncher seam). Relay's extra hop lands RTT naturally in the 30–70 ms target band — record measured GetCurrentRtt during the playtest so a later swap has a baseline. When Steam is revisited: prefer Steamworks.NET (2025.164.1, SDK 1.64, MIT, actively maintained, single native plugin set) over Facepunch, whose community transport has an open unfixed duplicate-assembly conflict (multiplayer-community-contributions#219) that bites exactly the Win64/Posix cross-platform pair this project needs; Heathen's NGO integration is undocumented (KB page 301s away). Budget 2–3 days worst case for a hand-rolled transport — 10 members map near 1:1 onto SteamNetworkingSockets.LowADR-0008 (ship-time, not playtest-time)
QQ-05SharedStorage and structure-registry replication representationMediumADRs 10/11, after ADR 8
QQ-06~~Per-template asset MB budget owner and number~~ RESOLVED by ADR-0005: LandLifecycle owns landTemplateBudgetMB = 64, CI-gated on the Addressables build layout (non-vacuous). Build-overlay draw-call cost RESOLVED by ADR-0013 §8: ghost 0 (home atlas, pre-authored dash frames) + grid 1 (one procedural mesh) = +1 draw call, build mode only. QQ-06 fully closed 2026-09-01.LowADR-0005 + ADR-0013
QQ-07~~Save format: JSON vs binary; file location~~ RESOLVED by ADR-0004: JSON via Newtonsoft behind ISaveWriter/ISaveReader (STJ not in the 6.3 BCL); persistentDataPath/Driftlands/worlds/<worldId>/save.json + 3 rolling backups—ADR-0004
QQ-08~~Material→source-template mapping owner (crafting era gate)~~ RESOLVED by ADR-0006: ItemDef.SourceTemplates in crafting.json, owner Crafting; referential check at validation stage 7; no reverse copy on TemplateDef—ADR-0006
QQ-09~~gridCellSize / snap tolerance (BB OQ6)~~ RESOLVED by ADR-0003: 1 cell = 1 world unit = one 32 px tile at PPU 32—ADR-0003
QQ-10~~Shader Graph 2D sprite shader global-property read verified in URP 6.3 (no Render Graph pass needed)~~ RESOLVED 2026-09-01 by ADR-0013 — verified against live 6.3 docs, and the verification changed the design: the read works only if both properties are declared at Scope = Global on the blackboard (Show In Inspector is a visibility control and does NOT prevent material shadowing — architecture review E14). No Render Graph pass is needed, as assumed.MediumADR-0013
QQ-11~~Save write-through vs non-yielding commit~~ RESOLVED by ADR-0004: dirty-mark in the critical section, serialise at end-of-drain on the main thread, flush on a background thread; durability per-drain, broadcast-first, wall-clock-bounded by flushDeadlineMs—ADR-0004
QQ-12~~Shapes of HostContext, ReconcileContext, Cell, LandInstance, HitEvent; the UnityInterop utility; a worked command example~~ RESOLVED by ADR-0003 (HitEvent became SwingLanded + DamageAbsorbed)—ADR-0003
QQ-13Harvest-node replication: NetworkList<NodeState> per land vs per-node NetworkVariable (LP review 4)Closed 2026-09-11ADR-0010 §4: NetworkList<NodeNetRow> on LandNet, index-aligned with LandInstance.Nodes