Repository-cleanup / DX pass folding three tracker items into one coherent change, verified green end to end (`bin/x test` 49/0, `bin/x selfhost-test` 29/0, `bin/x test-tools` 29/0). #28 — curate & categorise examples/ - 42 flat entries regrouped into intent-revealing subdirs: games/, rendering/, ecs/, events/, networking/, lang/, library/ (was lib/). - chronorift dir-vs-file duplication resolved: the entry file and its import modules now live together under games/chronorift(.ludic). - Every path reference updated repo-wide (test runner, editor-tool drivers, docs/site, design docs). - New examples/README.md indexes the whole set with run commands. - Showcase examples without a self-asserting entry (hello, events, net_rt) now get a compile-only rot guard in `bin/x test`, so nothing here rots silently. #30 — text-diffable golden baseline - The 4 binary selfhost/golden/*.ppm blobs are replaced by a single selfhost/golden/renders.sha256 manifest (SHA-256 per render). Hashes are byte-identical to the old PPMs, so the baseline is unchanged — only its form. - game_case now compares framebuffer hashes; a regression shows as a changed hex line in review, not "binary files differ". - New `bin/x golden` regenerates the manifest deliberately (review with `git diff selfhost/golden/renders.sha256`). #27 — PPM & asset handling - Headless renders now write build/out.ppm, never the repo root; `x app`, `x clean`, messaging and .gitignore updated to match. Nothing is written to the working root any more. - Redundant local Kenney .zip archives removed (the art ships extracted; .gitignore already excludes *.zip). CC0 License.txt files retained. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
29 KiB
Networking, from primitives up — a design doc
Status: N0–N6 all shipped. The whole stack is implemented and self-hosted, and — unlike the original N0/N1 which linked C hosts — every phase now runs as a self-contained pure-Ludic program (no
.c, no foreign host): a built-in loopback transport fills the seam, and eachexamples/net_*.ludicdrives and asserts itself from its ownentry. Seebin/x test(checksnet_echo…net_demo) andexamples/networking/net_demo.ludicfor a full RPC→authority→replicate→reconcile loop. clang remains only as the LLVM-IR assembler/linker (no C is compiled), the floor Rust and Swift stand on.Historical note: N0 + N1 shipped first; N2–N6 were design. Two phases landed as
bin/x testchecks. N0 (transport seam):extern fnnow lowers end to end — a direct@<sym>call plus adeclare, no networking logic in the compiler — so the whole transport is two externs (net_send/net_poll) a host fills. Proven byexamples/networking/net_echo.ludicsending four bytes through the loopback host intests/net_c/loopback.cand polling them back (4 10 20 30 42). N1 (snapshot-to-buffer):world_size()/world_save(buf)/world_load(buf, len)generalizesave()/load()from a file to a caller-owned memory buffer — the same block layout viamemcpy— so the whole ECS world round-trips through bytes. Proven byexamples/networking/net_snapshot.ludic+tests/net_c/snapshot_mod.c(snapshot, mutate, restore →50 7 50). Both are byte-identical when unused, so the offline dividend (§8) holds. This is a companion to EVENTS-DESIGN.md, LIFECYCLE-DESIGN.md, and SCENES-DESIGN.md. Where the events work made Ludic moddable, this proposes making it networked — and it deliberately does not ship a multiplayer framework. Ludic is a language: it exposes the low-level mechanism (transport seam, world snapshot, generated serializers, ownership, a drivable sim) and a thin high-level declarative layer that lowers onto that mechanism, and it leaves the netcode policy (authority, prediction, relevancy) to the developer or a library. §14 lists the open decisions.
1. Thesis
Every networking model dies on one of two problems: determinism or state serialization. Ludic already solves both, almost by accident.
- Determinism is designed in — seeded RNG,
fixed(Q16.16) instead of floats, byte-identical golden renders, and (as of EVENTS-DESIGN EV6) bounded, array-ordered event dispatch. A modded, event-driven Ludic game still replays identically. That is exactly the property lockstep multiplayer needs, and the reason Factorio's heavily-modded multiplayer stays in sync. - State serialization already exists —
save()/load()snapshot the entire ECS World to a byte buffer (selfhost/emit_save.ludic), and the world-table schema built for EVENTS-DESIGN EV2 (prop → field → offset) is exactly the descriptor you serialize against.
So networking is not a new subsystem. It is a fourth lens on the event + world layer — the same layer modding used. And it obeys the same two-altitude rule as everything else in Ludic:
Low-level is freedom; high-level is developer experience; they are the same feature at two altitudes.
@Querieslowers to a query loop,scenelowers to a machine,@Public @OnSpawnlowers toemit. Networking's high-level annotations lower to a transport seam, generated serializers, and a drivable sim — and the primitives stay exposed underneath for anyone the sugar doesn't fit.
The developer writes one simulation, declares what replicates, who owns
each entity, and where each handler runs — and never branches on is_server()
in ordinary code. The compiler lowers the declarations; a networking runtime
(the seam-filler, like rt_* for windowing) supplies the transport and the tick.
2. Two altitudes, one system
| Altitude | Who writes it | Surface |
|---|---|---|
| High-level (DX) | the developer, declaratively | @Sync (field/property/model), @Owned, @Server/@Predicted, directional remote events |
| Lowering | the compiler | per-model serializers, role-guarded dispatch, remote-event send/recv, ownership storage |
| Runtime seam | a networking library (blessed or custom) | binds the socket, sets role, drives the replication tick |
| Low-level (freedom) | power users, when the sugar doesn't fit | net_send/net_poll, world_save/world_load, generated serialize_*/apply_*, owner(), the drivable sim |
Everyone lives at the top row for normal games; the bottom row stays open for
someone building something no framework could express. The split that keeps this a
language and not a framework: annotations and their lowering are the language;
the replication driver and the transport are a library. It is precisely the
events story — @On/emit are the language, the modding system is library code —
applied again.
3. Research digest — the one idea to steal from each
| System | The transferable idea |
|---|---|
| Quake / QuakeWorld | The founding pattern: client-side prediction + server reconciliation, and delta-compressed snapshots against the last acked baseline. Predict locally, correct from the authority. |
| Source (Valve) | Entity interpolation (render remote entities slightly in the past, smoothly) paired with lag compensation (the server rewinds to the shooter's view for hit detection). Interpolation and rewind are two halves of one clock discipline. |
| Unity NGO (GameObject) | NetworkVariable<T> with read/write permissions + OnValueChanged; ownership as OwnerClientId. Also the anti-pattern to avoid: IsServer/IsOwner branching sprinkled through gameplay code. |
| Unity Netcode for Entities (ghosts) | The model Ludic is closest to: replication is a compile-time property of components and fields — [GhostField], [GhostComponent], GhostOwner, and Predicted/Interpolated ghost modes — with serializers generated from the ECS schema. |
| Mirror / FishNet | The community-ergonomic take: SyncVar with change hooks, and clean directional RPCs — Command (client→server) / ClientRpc (server→clients). |
| GGPO / rollback | Save state → predict → on misprediction restore and re-simulate. Its one hard requirement is cheap, complete state snapshot/restore — which Ludic already has in save()/load(). |
| Factorio | Fully deterministic lockstep for heavy mod multiplayer: only inputs cross the wire; the whole sim is reproduced. Proof that determinism (EV6) is the enabler, not a nicety. |
| Photon Quantum | A shipping product that is deterministic-ECS-rollback. Validates the exact combination — ECS + determinism + rollback — Ludic is already positioned for. |
| Roblox | The local/remote split (BindableEvent vs RemoteEvent), server-authority by default, and engine-replicated properties: "some state just replicates, and RPCs are directional events." |
Six footguns the survey warns against, to design out from the start:
- Role branching everywhere.
if (IsServer)scattered through gameplay is the NGO readability tax. Fix: role is a handler annotation (@Server/@Predicted), never a runtime branch in ordinary code. - Float nondeterminism. Lockstep breaks the instant the networked sim touches
f32across platforms. Fix: the determinism contract (§11) — the networked sim staysint/fixed. - Replicating pointers / heap refs. A
ptrfield holds a machine-local address; it cannot cross the wire. Fix: the compiler rejects@Syncon a non-POD-scalar field — a checked guarantee, not a convention. - Sending everything every tick. Fix:
@Syncis opt-in at the field level (only marked fields replicate), plus change-driven dirty tracking (@OnChange, LIFECYCLE LC2) so an unchanged field costs nothing. - Hidden authority. Magic "the server decides" behavior is unclear and
unauditable. Fix: explicit
@Server/@Predicted; unmarked code runs everywhere by definition. - Schema-less snapshots. A raw state blob with no version desyncs silently on a version mismatch. Fix: the world-table schema is the versioned descriptor the serializer is generated against.
4. What Ludic already has
The substrate is unusually complete for an engine that has never networked:
- A deterministic simulation — seeded RNG,
fixedmath, ordered ECS iteration, EV6-bounded event dispatch. Lockstep's precondition. - World snapshot/restore —
save()/load()serialize the whole World (emit_save.ludic); today to a file, trivially retargetable to a memory buffer. Rollback's precondition. - A reflective world table —
ludic_get/set/has/query/register_propand the prop→field→offset schema (EV2/EV2b). The apply-and-serialize substrate. - An event bus with a foreign ABI and POD payloads (EV0). Directional remote events (RPCs) are one flag on this.
- The
rt_*seam pattern — the compiler already emits calls tort_init/rt_poll/rt_presentthat a runtime library fills. Networking's transport and role registers plug into the identical seam.
What is missing is small and named: a transport seam, snapshot-to-buffer, generated per-field serializers, ownership storage, role-guarded dispatch, and a developer-drivable loop. Each is a phase in §13.
5. The low-level primitives (the freedom layer)
Unopinionated, composable, host- or developer-owned. A power user builds any model directly from these; the high-level layer (§6) is sugar over them.
| Primitive | Signature (sketch) | Enables |
|---|---|---|
| Transport seam | extern function net_send(peer: int, buf: pointer, len: int) · extern function net_poll(buf: pointer, cap: int) -> int |
any model; host binds UDP (native) or WebRTC/WebSocket (wasm), or a loopback for tests |
| World snapshot ↔ buffer | world_save(buf: pointer) -> int · world_load(buf: pointer, len: int) |
rollback, replication, join/resync — generalizes save()/load() off the filesystem |
| Generated serializers | serialize_<Model>(e: entity, buf: pointer) -> int · apply_<Model>(e: entity, buf: pointer, len: int) |
per-model, touch only the @Sync fields; emitted from the schema |
| Ownership | owner(e: entity) -> int · set_owner(e: entity, id: int) |
authority checks, per-entity owner metadata (an @L_owner array, like @L_kind) |
| Role registers | is_server() -> bool · is_owner(e: entity) -> bool · local_id() -> int |
the runtime sets these; role-guarded dispatch reads them |
| Drivable sim | tick_fixed() · tick_render() · seed get/set |
a developer-owned loop for prediction/rollback (also: replay, headless tests, AI) |
| Remote-event serde | emit-site serialize + net_send; inbound bytes rebuild + re-emit |
RPCs |
Transport is the one that needs no language work at all — a developer can already
extern fn a socket library and link it, exactly as the windowing layer is linked.
The language's genuine contributions are snapshot-to-buffer, the generated
serializers, ownership storage, and the drivable loop.
# doc-check: skip — the freedom layer, a hand-rolled replication tick
entry {
while running() {
if is_server() {
for (Transform) in query [Transform, Owned] {
let n = serialize_Player(self(), buf) # compiler-generated
net_send(ALL, buf, n) # developer's transport
}
} else {
let n = net_poll(buf, CAP)
if n > 0 { apply_Player(target_of(buf), buf, n) }
}
tick_render(); present()
}
}
This works, but it is deliberately not how most games should be written — it puts serialization and role branching in the developer's face. That is what §6 fixes.
6. The high-level DX layer (the default)
The developer declares what replicates, who owns, and where handlers
run. No serialization, no transport, no is_server() in ordinary code.
6.1 @Sync — what replicates, at three granularities
Replication is opt-in at the field level: a field crosses the wire only when it
is explicitly marked. There is no @NoSync — the surface is purely additive.
Two independent switches, and both must be on for a field to replicate:
- A field is replicable iff it is
@Sync-marked — directly (@Sync hp: int), or via@Sync property P { … }(a shorthand that marks every field ofPreplicable). Only marked fields — never all-by-default. - A component participates in a model iff the model marks it
@Sync(@Sync Transforminside themodel). Participation is decided per model use-site, so the same property syncs in one model and not another.
A field of an entity replicates iff it is replicable AND its component participates in that entity's model.
# doc-check: skip — the three levels
@Sync property Position { x: int, y: int } # every field of Position is replicable
property Health { @Sync hp: int, max: int } # only hp is replicable; max never is
property Transform { @Sync x: int, @Sync y: int, angle: int } # x, y replicable; angle not
@Owned model Player { # entities carry a network owner
@Sync Transform # participates → replicates x, y (not angle)
@Sync Health # participates → replicates hp (not max)
@Sync Position # participates → replicates x, y
}
model Prop { # a non-owned decoration
Transform # not @Sync here → Transform does NOT replicate — the
# "non-synced Transform sometimes" case, for free
}
- Checked, not silent.
@Syncon aptr/non-POD-scalar field is a compile error ("networked fields must be POD scalars" — footgun 3). A model that@Synces a component with zero replicable fields is a compile warning (participation that replicates nothing). - Per-field direction rides the same annotation as an argument, mirroring how
@Queries(these:…, on:…)takes args:@Sync(to: owner) hp: intreplicates a field only to the entity's owner (Unity'sSendToOwner). Default isto: all.
6.2 Roles — where a handler runs
The role is a declarative annotation on the handler, never a runtime branch. Unmarked code is the shared, deterministic simulation and runs everywhere.
| Annotation | Runs where | Meaning |
|---|---|---|
| (none) | everywhere | shared, deterministic simulation |
@Server |
the authority only | server-authoritative logic; clients receive the result via @Sync |
@Predicted |
the owning client (speculatively) and the server (authoritatively) | responsive local control, auto-reconciled against the server |
@Predicted is explicit — the developer opts an owned entity's control handlers
into prediction; the language does not silently predict. The name states the netcode
role (owner-predicts + server-authoritative + reconcile), not the machine, and
matches Unity's GhostMode.Predicted so the concept transfers.
@Interpolated — how a non-owned synced component is smoothed between snapshots on
a remote client — is a presentation concern on the component, kept separate from
these sim-handler roles rather than muddying them.
6.3 Ownership
# doc-check: skip
@Owned model Player { @Sync Transform; @Sync Health } # every Player entity has a network owner
@Owned gives the model an owner slot (the @L_owner array); owner(e) /
set_owner(e, id) read and assign it (the authority assigns). is_owner(e) and
@Predicted dispatch read it. Ownership gates who may write @Sync(to: owner)
fields and who runs @Predicted handlers.
6.4 RPCs are directional remote events
RPCs are the event bus with a direction flag — no new concept:
# doc-check: skip
@ToServer event Fire { dir: int } # client → server (a request)
@ToClients event Boom { x: int, y: int } # server → clients (a broadcast)
@Server @On(Fire) handler DoFire { spawn Bullet { dir: Fire.dir } } # authority handles the request
@On(Boom) handler Vfx { spawn Explosion { x: Boom.x, y: Boom.y } } # every client reacts
@ToServer/@ToClients mark an event remote; the compiler serializes its POD
payload (already flat — EVENTS-DESIGN EV0) and routes it through
the transport seam in the declared direction, re-emitting it on the far side into
the ordinary event dispatch.
6.5 The whole game, high-level
# doc-check: skip — read top to bottom: you always know where each line runs
program Shooter {
@Sync property Position { x: int, y: int }
property Health { @Sync hp: int, max: int }
@Owned model Player { @Sync Position; @Sync Health }
model Bullet { Position }
handler Physics phase FixedUpdate { … } # no tag → shared, identical everywhere
@Predicted handler Move phase Input { … } # owner predicts, server authoritative
@Server handler Death phase Update { … } # authority only; clients get the result via @Sync
@ToServer event Fire { dir: int }
@Server @On(Fire) handler DoFire { spawn Bullet { … } }
}
No is_server(), no net_send, no serializer — yet every line's role is legible,
and every replicated field is explicitly opted in.
7. Lowering summary
Everything above reduces to the §5 primitives, gated so an un-networked build is unchanged:
| High-level | Lowers to |
|---|---|
@Sync field / @Sync C in a model |
a per-model serialize_<M> / apply_<M> over the replicable-and-participating fields, + a sync manifest a runtime reads |
@Sync(to: owner) |
a field tag in the manifest; the serializer branches on owner(e) == peer |
@Owned |
an @L_owner array + owner()/set_owner(), like @L_kind |
@Server / @Predicted handler |
the handler's dispatch wrapped in a role guard the runtime's role register drives (the rt_* seam pattern) |
@ToServer / @ToClients event |
payload serialize + net_send(direction, …) at the emit site; inbound bytes rebuild + re-emit |
world_save/world_load to buffer |
the existing save()/load() snapshot machinery, retargeted from a file handle to a memory buffer |
drivable tick_fixed/tick_render |
the phase runners the compiler already generates for the frame loop, exposed as callables when a game owns its entry loop |
No heap, no hidden runtime beyond the honestly-named transport/role seams a networking library fills — the same relationship windowing already has.
8. The offline dividend
Because these are opt-in-cost annotations — serializers generated, nothing run until a networking runtime is spliced — a build with no runtime is byte-identical to single-player, and every role guard collapses to "run here." You build the game offline, drop in a runtime, and the same annotated code starts replicating. That is Unity's "offline mode adjustable," achieved by the same opt-in-cost invariant the whole event system already holds.
9. The one genuinely hard corner
Determinism holds beautifully for int/fixed simulations, which makes lockstep
and rollback cheap. It breaks for f32 across platforms — so 3D/voxel +
lockstep stays the hard corner (3D wants floats; the Luanti analysis flagged that
fixed saturates at ±32768). No language sleight-of-hand fixes this; the
determinism contract (§11) states it plainly, and a developer choosing lockstep for
a 3D game has to accept it (or choose state replication, §10's other branch, where
per-frame determinism is not required).
10. Two model families, both reachable — neither built in
The language commits to neither; both are library policy over the §5 primitives.
- Deterministic lockstep / rollback — exchange only inputs; reproduce the sim;
on misprediction,
world_loada snapshot and re-tick_fixed. Plays to Ludic's determinism, and GGPO-cheap because snapshot/restore already exists. Best for 2D/integer/fixed games. - State replication — the authority
world_saves (or per-@Syncserializes), delta-encodes against the last acked snapshot per peer, ships the diff; peersapply_*it and interpolate/predict. Heavier, but needed when the sim can't be deterministic (float physics, 3D).
A blessed reference runtime (§13, N6) can ship one of these so @Sync games
work out of the box — the way tests/mod_c/mod.c proved the
event ABI — while the seams stay open for others.
11. The determinism contract (what the language must guarantee)
For a developer to trust lockstep, the language must promise, document, and where possible enforce:
fixed/intmath is bit-identical across platforms. The networked sim must avoidf32(footgun 2). (Enforcement: at least a documented rule; ideally a@Sync/@Server-reachable-code float lint.)- ECS iteration order is stable — query order is declaration/id order, and EV6 already fixes event-dispatch order. No hash-map iteration in the sim path.
- RNG is deterministic from a shared seed —
seed()exists; the seed must be synchronized at session start (library policy) and never re-seeded from wall-clock mid-sim. - Networked components are POD scalars — no
ptr/heap fields cross the wire (footgun 3). Enforced:@Syncon a non-scalar field is a compile error. - Entity ids agree across peers — lockstep gets this free from determinism; replication needs an id-mapping table (library policy).
This contract is the language's real networking responsibility. Most of it is already true; the work is stating and enforcing it, not inventing it.
12. Design principles
- Mechanism in the language, policy in the library. Expose serializers, transport seam, ownership, snapshot, drivable sim. Never bake in authority, prediction, or matchmaking.
- Role is declared, not branched.
@Server/@Predictedon handlers; unmarked code runs everywhere. Nois_server()in ordinary gameplay. - Replication is explicit and opt-in. Only
@Sync-marked fields cross the wire; participation is decided per model. Nothing replicates by surprise. - Opt-in cost. Un-networked builds are byte-identical; the sim runs offline with the same code.
- Determinism is a promise the language keeps. Enforce the POD-scalar rule; document the float/iteration/seed rules; keep the sim reproducible.
- Two altitudes, always. The high-level lowers to primitives that stay callable. The sugar is the default; the freedom layer is never removed.
- Reuse, don't reinvent. Snapshot = generalized
save(); RPC = directionalevent; serializer = generated from the EV2 schema; role seam = thert_*pattern. Networking is the fourth lens, not a parallel stack.
13. Suggested implementation order
Each phase is independently shippable and testable, matching how the repo phases work (and how EVENTS-DESIGN sequenced EV0–EV7).
- N0 — transport seam + loopback. ✅ SHIPPED. The
net_send/net_pollextern seam and a loopback host stub; an echo test. The floor; needed almost no compiler work — just finishingextern fn: a call lowers to a direct@<sym>call and the header emits a matchingdeclare, so any C/Rust/Zig library (a socket, here the loopback) binds through the same seam windowing uses.find_extern(emit_core), the extern branch in emit_expr's call path,emit_extern_decls(emit_head). (examples/networking/net_echo.ludic,tests/net_c/loopback.c→4 10 20 30 42.) - N1 — snapshot-to-buffer. ✅ SHIPPED. Generalized
save()/load()to a memory buffer:world_size()(exact snapshot bytes),world_save(buf) -> int,world_load(buf, len). The same fixed block list (entity count, freelist, alive, kind, vars, per-component@S_/@H_) now feeds a file (fwrite/fread) or a buffer (memcpy over a threaded i64 offset), chosen byg_snap_modein emit_save.ludic; no rt_ hook (the ECS world only). The rollback/replication substrate. (examples/networking/net_snapshot.ludic,tests/net_c/snapshot_mod.c→50 7 50.) - N2 —
@Synccodegen. ✅ SHIPPED. The three-level annotations → generated per-modelserialize_<M>/apply_<M>+ by-kind dispatchers (ludic_serialize/apply/sync_size, and theserialize/apply/sync_sizebuiltins); the POD-scalar compile error and the empty-participation warning. The declarative core. (examples/networking/net_sync.ludic→12 3 4 50 999, emit inselfhost/emit_net.ludic.) - N3 — ownership. ✅ SHIPPED.
@Owned+ the@L_owner_arrarray +owner()/set_owner()/is_owner(); owners are part of the world snapshot. (examples/networking/net_owner.ludic→-1 7 0 1.) - N4 — remote events (RPCs). ✅ SHIPPED.
@ToServer/@ToClientsonevents → payload serialize ([event id][fields]) + directionalnet_send+net_pump()far-side re-emit. (examples/networking/net_rpc.ludic→0 8.) - N5 — roles + drivable sim. ✅ SHIPPED.
@Server/@Predictedrole-guarded dispatch driven by the@L_roleregister (set_role/is_server/local_id); the opt-inentry-owns-the-loop withtick_fixed()/tick_render(). Together these let prediction/rollback be written in developer/library code. (examples/networking/net_roles.ludic→1 102.) - N6 — a blessed reference netcode runtime. ✅ SHIPPED. A Ludic library
(
examples/networking/net_rt.ludic) — server-authoritative state replication over the primitives — plus a full end-to-end demo, proving the seams the way the C mod proved the event ABI, but in pure Ludic over the built-in transport. Library policy, swappable for lockstep+rollback. (examples/networking/net_demo.ludic→5 999 5.) A built-in loopback transport (N0) means all of this needs no foreign code at all.
N0–N2 deliver "state can be declared, serialized, and moved." N3–N4 add ownership and RPCs. N5 unlocks prediction. N6 is a batteries-included default that others can replace. The determinism contract (§11) is cross-cutting — documented from N0, enforced incrementally.
14. Open decisions
- Field direction vocabulary.
@Sync(to: owner)/@Sync(to: all)confirmed in spirit; isto:the right key, and do we also wantto: server(a field only the authority reads)? How does per-field direction interact with@Predicted? - Blessed runtime, or seams only? Events chose "seams + reference mod, bless nothing." Networking's DX may justify shipping one reference runtime (N6). One, or none?
- Authority default. Server-authoritative with
@Predictedopt-in is the safe, Unity-ish default. Confirm, or keep the language authority-neutral and leave even that to the runtime? - Drivable loop shape. Whole-frame
tick()vs thetick_fixed()/tick_render()split; how a developer-ownedentryloop coexists with scenes, thert_*hooks, and the auto-loop (opt-in via presence of anentryblock?). - Snapshot granularity. Full
world_savevs per-@Syncserialize vs a generated delta between two snapshots — which does the language provide, and which is library work? - Float determinism enforcement. A documented rule only, or a real lint that
flags
f32reachable from@Server/@Predicted/@Synccode paths? - Ownership at component granularity. Unity's DOTS allows per-component owner
send-rules. Is
@Ownedper-entity enough, or do we need per-component owners (a real complexity jump)? - Networking substrate for the remote half of EVENTS EV7. This doc's directional remote events (N4) are the local/remote split EVENTS-DESIGN EV7 deferred for "no networking substrate." N4 is that substrate — the two docs meet here.
Companion to EVENTS-DESIGN.md (remote events are directional
events; serializers reuse the EV2 world-table schema; EV7's deferred local/remote
split lands here as N4), LIFECYCLE-DESIGN.md (@OnChange/LC2
is the dirty-tracking primitive for delta replication), and
SCENES-DESIGN.md. Supersedes nothing until the compiler work in
§13 lands.