diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index b06ec30..2a09dff 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -930,7 +930,7 @@ pulls) is research, not a walk, so it still comes first: answered before code: **written on 2026-10-14 as §14.3**, from D361–D369, and answered as D370–D376. Then the build, in §14.3.11's nine stages (two of them core), §13 first. **Stage 1, §13, was built on 2026-10-15 (§14.3.14)**, with D378 and D379. **Stage 2, core's `retryAfterMs` (MODULE_API 1.15.0), was built on - 2026-10-15 (§14.3.15).** **Stage 3, TruePVE in the zone step, was built on 2026-10-15 (§14.3.16).** + 2026-10-15 (§14.3.15).** **Stage 3, TruePVE in the zone step, was built on 2026-10-15 (§14.3.16)**, and **stage 4, Dynamic PVP, the same day (§14.3.17).** **Then one walk, on Oxide and Carbon, of everything still owed (D331):** §2.7; §11's rows; §12.9; §13's and §14's rows; the in-game rows @@ -2531,6 +2531,9 @@ and see the leftover removed. #### 14.3.3 Dynamic PVP +> **Built on 2026-10-15** (§14.3.17), specified in PROTOCOL.md §20.7. The seed, the zone's shape and the +> public names differ from this text; §14.3.17 says how. + **Hello.** `integrations.dynamicPvp`: `{ loaded, version }`. **The wire.** Two events, `pvp.zone.created` `{ zoneId, event, x, z, radius, durationSec }` (−1 for "as long as @@ -2976,6 +2979,59 @@ plugin's words). Full suites: server 603/603 and every check (imports, bundle, s **Walk rows** are §14.3.2's, at the end walk (D331, D359). +#### 14.3.17 Stage 4, built: Dynamic PVP (2026-10-15) + +Built in Rust-Plugins and Module-Rust, specified in [PROTOCOL.md](../../rust-link/PROTOCOL.md) §20.7 and §17.3. +**No Rust-Link change**: the sidecar stores the two events and forwards `map.live` whole. `PROTOCOL_VERSION` +stays 14. Builds: Rust 25902425, Oxide 2.0.7830, Carbon 2.0.262; the code read is Dynamic PVP 5.2.0, the +version the §14.2 test ran and still the newest on uMod. + +**Three things in §14.3.3 were wrong or unsaid, found in Dynamic PVP's source:** + +1. **The seed comes from Dynamic PVP itself, not from TruePVE's table.** §14.3.3 seeded the layer from + *"ZoneManager's zones whose ids TruePVE maps for Dynamic PVP"*, but nothing in TruePVE's table says whose a + mapping is, and an operator's own numeric zone would be taken for one. Dynamic PVP has an API for exactly + this: `AllDynamicPVPZones()` and `GetEventName(zoneId)`. +2. **A zone is a sphere or a cube.** The plan read a radius from ZoneManager, but cargo's zone (the one that + works out of the box) is a cube, `Size` with no `Radius`. The layer and the line carry `shape`, and a cube's + `radius` is half its longer side across the ground, so the map can still draw a ring. +3. **Cargo's zone moves.** It is parented to the ship, and ZoneManager's `GetZoneLocation` answers where it was + made. The bridge reads the zone's transform each time, through `GetZoneByID` (a `MonoBehaviour`, read as a + `Component`, so the bridge needs none of ZoneManager's types). + +**Settled in the code, and overrulable:** + +1. **Only Dynamic PVP's eleven vanilla events carry their name to the public.** A monument zone is named after + the monument and an operator's event by whatever they typed, so below staff those read *"A PvP zone opened · + G7"*. The map's ring shows the name as written, because the layer has its own audience. +2. **`pvp.zone.deleted` carries `event` too**, which the hook gives, so a staff line says which zone closed. +3. **Nothing is said while the world is not live**, as world events (§20.5): a boot opens every monument zone + at once. Dynamic PVP's own unload is said, because those zones really do close; the lines are staff's. +4. **The `pvp` layer is public by default**, its own `map.layer.pvp.audience`: the game draws the zones for + everyone near one, and they name nobody. +5. **The closing line is not on the public feed's list.** The client asks only for public kinds (its own test + holds it to that), and nothing on the site draws staff-only kinds today, `npc.died` included; the map shows + a zone going, and the API serves the line to staff. +6. **A zone ZoneManager no longer holds is dropped from the set when the map reads it**, since an erase by a + ZoneManager reload raises no Dynamic PVP hook. Dynamic PVP's unload clears the set after its deletes. +7. **Hello is sent again when Dynamic PVP loads or unloads**, and the set is re-seeded on the tick after it loads. +8. **Server settings' plugin line names Dynamic PVP.** + +**Tests.** Module-Rust server: `test/dynamicPvp.test.js`, six new (the public read the grid and never the +position; staff read the position and id; a monument's or an operator's name is staff's; no geometry or off +the map, no grid; another kind untouched; the feed shapes a stored row per viewer); `catalogue.test.js` holds +the two kinds and their classes; `map.test.js` two new (the layer passes through, is empty from an older +plugin, and is absent when set to staff) and the fresh-install defaults. Client: four new in `feed.test.js` +(the opening's words, detail and duration; an unnamed and a monument zone; the closing; every vanilla event has +words, held against the server's list) and one in `mapEvents.test.js` (the ring's tooltip, and the client's +names held against the server's list). Full suites: server 611/611 and every check; client 89/89, the build +and externals. The plugin compiled and loaded on both rigs, `rg.world` read `dynamicPvp=False pvpZones=0` on +each, and both were put back on the protocol-13 bridge. Dynamic PVP itself is not on the rigs; its hooks +are exercised by §14.3.3's walk rows. + +**Walk rows** are §14.3.3's, at the end walk, with one more: hotload the bridge while a zone stands and see +the ring still drawn (the seed). + ### 14.4 Questions for the org lead **Answered on 2026-10-14:** Q17 as proposed, out (D358). **Q18 against the proposal: the end walk waits for diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index ac95e50..a222c6c 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -1809,6 +1809,7 @@ drawing a second time. A new map key makes the file stale, and it is ignored. | `events` | what this site's events placed (§15.2): `kind` (`zone`, `crate`, `npc`), `runId`, `x`, `z`, and a zone's `radius` and `name` or a thing's `prefab` and `key` (the step that placed it). Protocol 13 adds, for one of RunicNPC's NPCs, its `name`, and for a boss `boss: true` and `health` (0–1); and a boss's **adds**, which no step placed, under their boss's run with `add: true` | | `players` | `steamId`, `name`, `x`, `z`, `sleeping`, `online`. Connected players, then offline sleepers | | `bases` | `kind` (`tc`, `vending`), `x`, `z`. **Positions only**: no owner, no authorised list, no shop name | +| `pvp` | Protocol 14 (§20.7): Dynamic PVP's own zones, `zoneId`, `event`, `x`, `z` (where the zone is now), `radius`, `shape` (`sphere`, `cube`). Empty without Dynamic PVP | It also carries `mapKey` and `tookMs`, the plugin's own time for the ask. `playersTruncated` and `basesTruncated` are set when a bound cut a layer short. @@ -3075,3 +3076,68 @@ name over 64 characters, and either one is refused where the server's last hello (a plugin before protocol 14, or never reached) is left to the plugin. `truepve-missing` and `truepve-ruleset` fail the step for good, with the plugin's words. Server settings' plugin line says whether TruePVE is loaded. + +### 20.7 Dynamic PVP's zones — `pvp.zone.created`, `pvp.zone.deleted`, the `pvp` layer (§14.3.3) + +HunterZ's **Dynamic PVP** (5.2.0, uMod; it requires ZoneManager and TruePVE), optional like Economics (D199) +and not in `overlay.toml`. It opens its own PvP zones around a downed patrol helicopter, a Bradley, a locked +crate, an airdrop, the cargo ship and the rest. **The bridge only reports them**: it never makes, maps or +erases a Dynamic PVP zone, and nothing on the site starts one. The design is +[`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §14.3.3; what the §14.2 test found is §14.2.1 there, +and what the build found in Dynamic PVP's own source is §14.3.17. + +**Hello and status.** `integrations` gains `dynamicPvp`: + +```json +"dynamicPvp": { "loaded": true, "version": "5.2.0" } +``` + +Hello is sent again when Dynamic PVP loads or unloads. `rg.world` prints `dynamicPvp=` and `pvpZones=` (the +zones the bridge knows are live). + +**Two events**, from Dynamic PVP's own hooks: + +```json +{"kind":"pvp.zone.created","type":"event","zoneId":"1412203045","event":"Helicopter", + "x":812.4,"z":-340.5,"radius":100,"shape":"sphere","durationSec":600} +{"kind":"pvp.zone.deleted","type":"event","zoneId":"1412203045","event":"Helicopter"} +``` + +- **`pvp.zone.created`** is `OnCreatedDynamicPVP(zoneId, eventName, position, duration)`. `event` is Dynamic + PVP's own name for what opened it: one of its vanilla events (`Bradley`, `Helicopter`, `Satellite`, + `TimedSupply`, `SupplySignal`, `CargoShip`, `HackableCrate`, `ExcavatorIgnition`, `GhostShip`, + `DeepSeaIsland`, `IslandCannon`), a monument's name for a monument zone, or an operator's own event name. + The hook carries no size, so **`radius` and `shape` are read from ZoneManager**: a sphere's `Radius`, or for + a cube (cargo's) half the longer of its `Size` across the ground, `shape: "cube"`. **`x`, `z` are where the + zone is**, from its transform, which for a parented zone (cargo) is the ship. `durationSec` is −1 for a zone + that lasts as long as its event (cargo, a monument's). +- **`pvp.zone.deleted`** is `OnDeletedDynamicPVP(zoneId, eventName)`, for a zone the bridge knew. +- **Neither is sent while the world is not live** (§20.5's rule): a boot or a shutdown opens or closes every + monument zone at once, and that is not news. Dynamic PVP's own unload deletes every zone and is said, as + `deleted` lines. +- Dynamic PVP's ids are numeric (a time stamp, or the entity's net id), never one of the bridge's `rg-…`. +- `OnPlayerEnterPVP` / `OnPlayerExitPVP` are not subscribed: they would say who stands where. + +**The `pvp` layer of `map.live`** (§17.3) is every zone the bridge knows is live and ZoneManager still holds, +with where it is **now** (cargo's moves with the ship), `radius` and `shape`. The bridge keeps the set by the +two hooks, and **seeds it from Dynamic PVP itself** (`AllDynamicPVPZones()`, `GetEventName(zoneId)`) when the +world is ready and when Dynamic PVP loads, because a hotload of the bridge hears no creates. A zone ZoneManager +no longer holds is dropped from the set when the layer is read. + +**Hooks.** `OnCreatedDynamicPVP` and `OnDeletedDynamicPVP` join `rg.hooks`' list. + +**The sidecar** stores and forwards the two frames like every other event, and `map.live` whole; nothing in +it changed. + +**The website (Module-Rust).** + +- **`pvp.zone.created` is a public kind, `pvp.zone.deleted` a staff kind.** Each row is read per viewer by + `dynamicPvp.shapeFor`, as a world event is (§20.5): everyone gets `grid`, `radius`, `shape` and + `durationSec`; **`event` only when it is one of Dynamic PVP's vanilla events**, because a monument's or an + operator's name is the operator's own text; and **`x`, `z`, `zoneId` only for staff**, because the map has its + own audience for where things are (D111). The public line reads *"A PvP zone opened around the downed patrol + helicopter · G7 · for 10 min"* and names nobody. No trigger and no step. +- **The map gains a fifth layer, `pvp`** (*PvP zones*), with its own audience in Admin → Rust visibility + (`map.layer.pvp.audience`), **public by default**: the game shows the zones to everybody near one, and they + name nobody. It is drawn as a red ring with its event's name. +- Server settings' plugin line says whether Dynamic PVP is loaded.