refactor/player-modules #45
Loading…
Reference in a new issue
No description provided.
Delete branch "refactor/player-modules"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
`world.ex` was 3,770 lines and roughly two thirds of it never touched the world at all -- it read collision and the cache, both `:persistent_term`, and returned a player. That is the code that has to move when players own processes, so it moves now, while the tick is still serial and the diff is provably behaviour-preserving. Five modules under `World.Player`, split by what each is handed: Dispatch a decoded request -> a player. No world state, and it cannot be given any. Encode a settled player -> the bytes it is told. Reads `TickSnapshot` and a three-field context. Advance one player's clocks, interaction passes and step. Effects the few operations that reach anything shared. Modal the interface in front of them, needed by two of the above. `World.Social.Requests` takes the other 690 lines: friends, ignores, private chat, presence and Friends Chat, which are the requests whose effect lands on somebody *else* and so could not go with `Dispatch`. The number worth having is in `Effects.shared/0`: **two fields**, `ground_items` and `loc_changes`, are the whole of what a player's own tick writes outside itself. Today both are safe by construction because the tick threads state through players in index order; they are exactly what stops being safe when players run inside a barrier, and the list is short enough to make atomic. `Advance.world/0` states the two read-only lookups on top of that, both of which `TickSnapshot` already answers. Also here, because both halves of the split needed them: * `World.Scene` -- view distance, read by the NPC activity radius and by the per-viewer add set, which are no longer in one module. * `RoutePlan` gains `path_for/2`, `approach/2` and `object_route/4`, so every route in the project is computed in one place rather than the world keeping a near-duplicate of the planner's fallbacks. * `ZoneUpdate.event/0`, which was `World.zone_event/0` and is produced and consumed by modules that no longer share a file. No behaviour change: 2,203 tests as before, Dialyzer clean, Credo unchanged, and the tick measures 41.71ms at 200 walking players against 42.16ms recorded for the same harness.The two things a player's own tick wrote outside itself are the two things that stop being safe the moment players run inside a barrier, because both were safe only by index-ordered threading. They go opposite ways, and which way is decided by the read side rather than the write side. **The floor becomes a table with a compare-and-swap claim.** `:ets.take/2` is the obvious primitive and it is wrong here: a take can be partial, so removing the row and re-inserting the remainder opens a window in which the pile does not exist, and every observer in it is wrong — a viewer's sync emits a delete and re-adds next tick, another taker is told "it's gone" about a pile holding seven, and a crash between the two destroys the remainder silently. So both halves are guarded on the amount the caller read: taken == before select_delete the row goes, if it still says `before` taken < before select_replace the row drops to `before - taken` `1` for the winner, `0` for a stale read, which is what "Too late - it's gone!" is made of. `Effects.take/5` now claims the floor **before** filling the backpack; the old order was correct only while the tick was serial, and under a barrier it mints items. **A door becomes a report.** A player queues the intent and `World.Doors` turns it, in slot order, after every player has moved. A door is contended but read by almost nobody, so serialising the write is cheaper than making it atomic and `loc_changes` stays a plain diff. The cost is stated in `swing_door/6`: collision turns after movement, so a player advanced later in the same tick no longer walks through the doorway their neighbour just opened — a property concurrency removes anyway, one tick wide, and no test depended on it. Reads do not touch the table. `GroundItems.snapshot/0` is taken once a tick and every viewer diffs the same floor, which is the propagation guarantee rather than an optimisation: a live per-viewer read would let two viewers of one pile disagree inside a tick and would copy every row out once per viewer per tick. Because the snapshot is taken after the phase where takes happen, a pile removed on tick N is absent from tick N's snapshot, so every viewer who was told about it is told it is gone on that same tick. `Effects.shared/0` is read-only now — `tick`, and nothing else. A player's own tick changes exactly two things: itself, and the floor. Coverage for what the change is for: stale-amount refusal leaves the pile untouched, 32 concurrent takers of one pile produce exactly one winner, 20 concurrent partial takers conserve every unit, and a snapshot does not move when the floor does. Three propagation tests were missing and are here — the loser of the existing race test was never checked despite having been told about that pile, a bystander is told when somebody else takes it, and a capped take reaches a bystander as a restack with no delete. 2,219 tests, Dialyzer clean, Credo at baseline, and the tick measures 41.52ms at 200 walking players against 42.04ms for the same harness without this change.