DRIFTLANDS co-op survival on drifting tides — studio progress

Control Manifest — Driftlands

Manifest Version: 2026-09-11
Engine: Unity 6.3 LTS (6000.3) · installed editor 6000.3.18f1 · URP 17.3.0 2D (Render Graph) · Shader Graph 17.3.0 · Input System 1.20.0 · Addressables 2.9.1 · Newtonsoft 3.2.2 · UI Toolkit · Netcode for GameObjects 2.13.2 · Unity Transport 2.7.3 · Multiplayer Services SDK 2.2.1 (Relay/Authentication/Core) · Multiplayer Tools 2.2.9 — pinned by ADR-0008, verified against the package source in the Gate 0 kit; not yet in UnityProject/Packages/manifest.json (NetcodeSession story 001 adds them and commits packages-lock.json)
Sources: all 15 Accepted ADRs (0001–0015) as amended 2026-09-11 (ADR-0008–0012 each amended ADR-0001/0002/0003/0004/0005/0006/0015 in their own commits) · .claude/docs/technical-preferences.md · docs/registry/architecture.yaml (69 active forbidden patterns, 8 budget entries = the 60 fps / 16.6 ms frame row + 7 per-system slices) · docs/engine-reference/unity/{VERSION,breaking-changes,deprecated-apis,current-best-practices}.md · production/playtests/gate-0-2026-09-11/ (Gate 0 GO)
Generated by: /create-control-manifest
Technical Director Review (TD-MANIFEST): CONCERNS → REVISED 2026-09-11 — C1–C6 applied; the three ADR sentences that contradicted their own ADR (ADR-0008 §4 outcome cap, ADR-0010 CarryFold.Apply arity, ADR-0012 EnemySpawner teardown trigger) were corrected as editorial amendments in the same commit

What this is. A flat rules sheet. ADRs explain why; this tells you what, in a form you can check a diff against. If a rule here and an ADR disagree, the ADR wins — and the disagreement is a bug in this file, so report it.

Stories embed the Manifest Version above. /story-done compares it and flags staleness, so regenerate this whenever an ADR is accepted or amended. Every story written before 2026-09-11 (versions 2026-09-02/03/07) is now stale against this file; the new rules are the NetcodeSession, PlayerMovement, Inventory replication, Structure registry and Enemy simulation blocks plus the amended Host-commit / Combat / Save / GameData / Land rows marked (2026-09-11).


✅ Gate 0 PASSED — confirmed GO 2026-09-11

The two-machine friend playtest ran on 2026-09-11 over Unity Relay (Jeroen host, Eric client, RTT ≈ 150–280 ms): own movement and dash instant, hits landed where seen, zero client warps; verdict GO (production/playtests/gate-0-2026-09-11/README.md, production/gate-checks/gate-0-provisional-pass-2026-09-11.md). ADR-0008–0012 are Accepted without qualification and every epic that routed through them is unblocked (production/epics/index.md). The netcode model (owner-authoritative movement, host-authoritative everything else, local-player prediction only, one ordered channel, host tick) is unchanged and binding. The one playtest observation — occasional remote stutter at that RTT — is a register item (interpolation tuning, ADR-0009 §6: offset 1→2, then SmoothDampening, players win arbitration; never extrapolation), not a blocker.


GLOBAL — applies to every layer

Naming (technical-preferences)

Thing Convention
ClassesPascalCase — PlayerController
Public fields/propertiesPascalCase — MoveSpeed
Private fields_camelCase — _moveSpeed
MethodsPascalCase — TakeDamage()
EventsPascalCase, past tense — HealthChanged, LandDocked
FilesPascalCase matching the class
Scenes/PrefabsPascalCase — HomeIsland.unity, LandTemplate_Marsh.prefab
ConstantsPascalCase or UPPER_SNAKE_CASE for true invariants
Teststest_[system]_[scenario]_[expected] (.claude/rules/test-standards.md)
Wire rows / net behavioursNetRow for an unmanaged NetworkList element (StackNetRow, NodeNetRow, StructureNetRow), Net for the NetworkBehaviour that owns replicated fields (PlayerNet, EnemyNet, StorageNet, BaseNet, SessionNet) (ADR-0008–0012)

Global budgets

Forbidden project-wide

Pinned toolchain facts (verified by compiling, 2026-09-03)

Unity 6000.3.18f1 compiles C# 9.0 against .NET Standard 2.1. Three things ADR text assumes that the compiler refuses:

If an ADR names a shape, compile it before accepting the ADR. ADR-0003 §3 carried all three of the above unnoticed through review, acceptance, and a producer gate.

Pinned NGO 2.13.2 facts (read from the package source 2026-09-11 — (src) in ADR-0008–0012)

These are facts about the installed package, not recommendations. Every one of them changed a design decision; code that assumes the training-data behaviour is wrong.

The 69 registered forbidden patterns

docs/registry/architecture.yaml is canonical. Each name below is greppable; the layer sections expand the ones you are most likely to trip.

unity_refs_in_tide_assembly · static_cache_in_pure_assembly · raw_tide_time_on_consumer_surface · caching_evaluate_across_ticks · iterating_elapsed_cycles · second_authority_clock · presentation_clock_driving_gameplay · mpb_on_driftlayer_sprite · exposed_shader_global_read · non_render_graph_pass · baked_tide_state_variant · ui_event_system_present · host_rail_rpc_outside_channel · execute_on_rpc_receipt · async_or_reentrant_execute · spawn_or_send_inside_execute · save_io_inside_drain · upward_assembly_reference · code_outside_asmdef · conversion_outside_unityinterop · float_output_from_client_evaluated_rules · formula_in_runtime_assembly · reflection_serialize_gameplay_types · dictionary_iteration_into_save · second_droppable_table · unknown_key_preservation · quit_without_flush · silent_default_on_missing_key · hardcoded_gameplay_value · cross_stage_forward_reference · editing_streaming_assets_in_a_build · second_copy_of_source_template_mapping · addressable_network_prefab · land_load_outside_lifecycle · addressables_call_in_validation · vacuous_layout_gate · activation_from_local_clock · legacy_input_manager · input_system_outside_adapter · per_consumer_edge_detection · input_edges_in_fixed_update · scheme_gating_gameplay · aim_persistence_toggle · owner_to_host_fact_outside_channel · multiple_tick_subscribers · client_stamped_intent_tick · clientid_as_row_key · burst_in_rules · movement_speed_modifier_for_downed · distributed_authority_topology · relevancy_filtering · bulk_payload_on_command_channel · owner_written_network_variable · outcome_handler_reads_variable · byte_array_on_command_wire · rigidbody_driven_motion · teleport_outside_snap_path · collision_callbacks_in_movement · host_known_is_not_continuous · carry_on_wire · storage_rows_written_outside_replicator · bag_mutation_without_outcome · registry_and_colliders_never_compared · list_index_on_remove · client_registry_gating_a_commit · client_side_enemy_ai · enemy_save_table · random_in_enemy_rules · collider_queries_in_enemy_runtime

(bold = added 2026-09-11 by ADR-0008–0012)


FOUNDATION LAYER

TideScheduler · SavePersistence · GameData · LandLifecycle · Input · NetcodeSession

Required

Assemblies (ADR-0003)

Tide (ADR-0001) — implemented; see Assets/Driftlands/Tide/

Save (ADR-0004)

GameData (ADR-0006)

Land lifecycle (ADR-0005)

Input (ADR-0007)

NetcodeSession (ADR-0008) (new 2026-09-11)

Forbidden (Foundation)

Guardrails (Foundation)

Budget Value Measured against
Tide evaluation≤ 50 µs avg per combined DockedLands+NextBoundary at N ≤ 8 (hard)Editor profile, stopwatch
Tide allocation0 bytes per warmed call (hard)GC.GetAllocatedBytesForCurrentThread()
Tide per-frame≤ 0.1 ms (advisory, AC-46)tint-float evaluation
BB ghost preview0 B per-frame allocation in Rules.BuildProfiler
Coverage≥ 80 % on Tide/Data/Rulesassembly filter
flushDeadlineMs250 ms — conservative, unmeasured; retune before Alphareal fsync/F_FULLFSYNC on SSD and HDD+AV
Session-end wait≤ 2 × flushDeadlineMsthe only permitted blocking wait
Boot< 100 ms for load + validation of ~17 filesCI
Land preloadPreloadLeadTime 600 s vs ≤ 64 MB LZ4 bundlezero I/O at the φ edge
Per-template memorylandTemplateBudgetMB = 64 (32–128)parsed Addressables build layout; FAIL if the file is absent — never pass vacuously
netcode-session per tick (2026-09-11)≤ 0.2 ms on the host (codec decode + validation + OutcomeRpc flush), excluding the drain's 1 msEditor profile at 50 Hz
Per-command allocation (2026-09-11)0 B in steady state (fixed-buffer envelopes)AllocatingGCMemory constraint plus a control allocation — a meter that has never read non-zero proves nothing (tide story-006)
Bandwidth (2026-09-11)≤ 20 KB/s down / 5 KB/s up per client at 2 players + 8 enemies (expected ≈ 15 / 2)RttLogger-style CSV on both machines
Snapshot build (2026-09-11)≤ 5 ms once per join, main thread, at a drain boundarymeasured in the join story
Snapshot chunk (2026-09-11)≤ 4 096 B (MaxPayloadSize 6 144); a 40 KB fixture round-trips byte-identicalV3
Relay sign-in + allocation (2026-09-11)≈ 1–2 s on a normal link (measured 2026-09-11)title-screen host/join spinner budget

Engine constraints (Foundation)


CORE LAYER

PlayerMovement · Combat · Harvesting · Inventory · LandContent · Camera

Required

Host commit model (ADR-0002, amended 2026-09-11 by ADR-0008/0009/0010/0012)

Combat (ADR-0015, amended 2026-09-11 by ADR-0008/0009/0010/0012)

PlayerMovement (ADR-0009) (new 2026-09-11)

Harvesting (ADR-0015 / ADR-0010 §4, amended 2026-09-11)

Inventory replication (ADR-0010) (new 2026-09-11)

Camera (ADR-0009 §4 / core-camera epic)

Forbidden (Core)

Guardrails (Core)

Budget Value Notes
Host drain≤ 1 ms at MVP load50 mixed commands, Editor profile
Command latency (decided 2026-09-11)RTT + ≤ 1 tick for outcomes (≤ 20 ms at 50 Hz); RTT + ≤ 2 ticks (≤ 40 ms) for stateADR-0008 §2 — the one-tick replication lag; no longer provisional
HostKnown staleness (2026-09-11)≤ 1 RTT + 1 tickADR-0009 §7 V6; RangeSlackUnits 1.0 u ≈ 6 u/s × 0.17 s absorbs it
Combat CPUarc test ≤ ~15 enemies/strike; Tick walks ≤ 5 players + ≤ 15 enemiesStep is struct-in/struct-out, zero alloc
Combat network≤ 3 replicated fields/player (one PlayerActorNet) + Phase/AnimFrame/Archetype/ActorId per enemyno new RPC surface
Movement CPU (2026-09-11)one shape cast per axis per moving body per rendered frame — 2 casts × ≤ 10 bodies ≪ 0.1 ms; Locomotion.Step 0 allocEditor profile; AllocatingGCMemory + control
Movement memory (2026-09-11)RaycastHit2D[8] per mover, nothing else—
PlayerSnapped (2026-09-11)25 B, a few per hour—
StorageReplicator (2026-09-11)≤ storageSlots (40; ≤ 80) comparisons per dirty container per drain; worst case storageSlots × 8 B deltas ≪ 1 KBhost, after drain
Node/storage spawn payload (2026-09-11)≤ 320 B of rows per land/chest spawn message (40 × 8 B), asserted so growth is noticedV4
CarrySync (2026-09-11)≤ 90 B at bagSlots 10; bagSlots ≤ 14 at the 128 B envelopewire-validated in inventory.json
Client fold (2026-09-11)one CarryFold.Apply per bag-touching outcome — µs—

Engine constraints (Core)


FEATURE LAYER

BaseBuilding · Crafting · EnemyAI (section new 2026-09-11 — every Feature epic was Gate-0 blocked before)

Required

Structure registry (ADR-0011)

Crafting (ADR-0008 §5 / ADR-0010 §1 / ADR-0011 §7)

Enemy simulation (ADR-0012)

Forbidden (Feature)

Guardrails (Feature)

Budget Value Notes
EnemyAI.Tick≤ 0.3 ms per tick for 20 enemies, 0 B/tick steady stateEditor profile; Senses over pooled lists; AllocatingGCMemory + control
Enemy movementone sweep per enemy per rendered frame (ADR-0009's cost)—
Enemy network≈ 14 KB/s down for 10 moving enemies at 50 Hz + ≤ 1 KB/s Phase/AnimFrame; AnimFrame ≤ 20 Hz × ~5 Binside ADR-0008 §8's 20 KB/s guardrail
Activation spawn burst≤ ~10 CreateObjectMessages in one tick, each ≤ MaxPayloadSize; message count == spawn countV1; if it ever matters, spread over ticks with no wire change
Telegraph floor≥ 400 ms = ≥ 20 ticks, exact host-sideload-rejected below 400
RegistryReplicatorO(changes) per drainhost, after drain
ClientRegistryO(1) per event; AnyCovers O(rows ≤ ~100) per ghost frametrivial at EC18's scale
Registry wire14 B per placement/demolish delta; ≤ ~1.4 KB scene-sync for the whole base (~100 rows)asserted (V5) so growth past EC18 is noticed
Structure memoryone GameObject + BoxCollider2D per structure per machineAlt D is the recorded swap if it measures
BB ghost preview0 B per-frame allocation in Rules.BuildProfiler (carried from Foundation)

Engine constraints (Feature)


PRESENTATION LAYER

UIHUD · TideTint · FeedbackPresenters · EnemyView

Required

Tide tint (ADR-0013) — the rule that has already been wrong once

UI (ADR-0014)

Feedback presenters and views (ADR-0002 §7 / ADR-0008 §2 / ADR-0012 §5) (new 2026-09-11)

Forbidden (Presentation)

Guardrails (Presentation)

Budget Value How it must be measured
BB build overlay≤ +2 draw calls in the editor (grid ≤ 1 + ghost ≤ 1), build mode only; ghost-0 is a built-player claim, deferredMarginal delta via UnityStats.drawCalls — before vs. after entering build mode with the ghost dashed. Never "total < 100". spritePackerMode = 3 means no atlas exists in Play Mode (ADR-0013 V4, 2026-09-07).
World-space UI≈ +2 draw calls, fixed (UI atlas + shared TMP font atlas)Marginal delta across the full bounded population (4 party rows, 8 flyouts, 4 edge glyphs, 1 ring, 1 ping). The delta must not scale with element count.
ui-worldspace≤ 0.2 ms/framehud.md's own performance AC
ui-panels0.0 msRetained mode, update-on-change. A future need for a measurable slice violates this invariant — flag it, never absorb it.
Tide tint CPU2 SetGlobal* calls/frame, zero allocRides ADR-0001's ≤ 50 µs / 0-byte gate.
SRP batching (V2)GetSRPBatcherCompatibilityCode(DriftLayerSprite) == 0, control ≠ 0Automatable, CI-safe. Never count UnityStats.batches (ADR-0013 V2, 2026-09-07).
Structure sprites (2026-09-11)one SpriteRenderer per StructureView (≤ ~100), home atlasbatching claim is a built-player Frame Debugger check (ADR-0013 V8 rule)
Enemy flipbooks (2026-09-11)one SpriteRenderer per enemy (≤ ~10 per land), frame from AnimFrame — zero per-frame allocationProfiler

Engine constraints (Presentation)


Implementation status (2026-09-11 — per-layer fractions from requirements-traceability.md; suite counts from production/session-state/active.md; Gate 0 from production/playtests/gate-0-2026-09-11/)

Item State
UnityProject/ on 6000.3.18f1✅ ProjectVersion.txt corroborates the CI pin; package pins match every ADR assumption (Input System 1.20.0, Addressables 2.9.1, URP/SG 17.3.0, Newtonsoft)
Assemblies Tide / Data / Rules / Runtime / Presentation / Editor + two test assemblies✅ all exist; every ADR-0003 fence green and mutation-verified; HostContext pinned at 18 members until ADR-0010's story moves it to 19
Foundation epics (bootstrap, tide, save, game-data, land-lifecycle, input, boot-scene)✅ Done except tide story 010 (t replication) and land story 009 (LandNet NGO) — both flipped Blocked → Ready 2026-09-11; the UNITY_LICENSE/hardware items remain human-only
Presentation epics (tide-tint, ui-hud-shell, feedback-presenters, boot-scene)✅ Done — 6/8 full-chain
Core: core-inventory-carry 5/5, core-harvesting 8/8, core-camera✅ Done — the pure halves; the replication halves (ADR-0010) and PlayerMovement (ADR-0009) are unblocked, not yet storied
EditMode / PlayMode suites✅ 971/968/0/3 (3 pre-existing [Explicit] fixture generators) · 112/96/0/16 (16 GPU-guarded skips) — 2026-09-11
Gate 0✅ PASSED — confirmed GO 2026-09-11 (two machines, Relay europe-west4, RTT ≈ 150–280 ms; 0 client warps; own actions instant; hits landed where seen). Register item: remote-stutter interpolation tuning (ADR-0009 §6 order).
NGO 2.13.2 / UTP 2.7.3 / Multiplayer Services 2.2.1 / Tools 2.2.9⏳ decided and source-verified (ADR-0008), not yet in UnityProject/Packages/manifest.json — NetcodeSession story 001 adds them; until then every Net_* fence is unrunnable and Runtime references none of the Unity.Netcode/Services assemblies
ADR-0008–0012✅ all Accepted 2026-09-11 without qualification; every sibling ADR/GDD/quick-spec/registry entry amended in the same commits; /architecture-review owed in a fresh session (never the authoring session)
Epics unblocked 2026-09-11, in the architecture's suggested orderNetcodeSession → PlayerMovement → Inventory replication (core-harvesting/core-inventory second halves) → Combat → Crafting / BaseBuilding / EnemyAI
Review follow-up stories (2026-09-07 review)❌ still open: HarvestConfig → double, NextEvictionInstant onto IPreloadSchedule, spritePackerMode assertion, HostCommitQueue header, E10 banners, ADR-0015 ms budget (see docs/tech-debt-register.md); ReconcileFinding migration ✅ (review-fixes story-001, 2026-09-08)
Register items from Gate 0interpolation tuning (try RemoteInterpBuffer 1→2, then SmoothDampening, players win; measure at the next two-machine session); RttLogger CSV corruption when two instances share one file (kit-only)