Compare commits
4 Commits
cbf322b4d6
...
80c7a9dcd1
| Author | SHA1 | Date | |
|---|---|---|---|
| 80c7a9dcd1 | |||
| b12c6dfde4 | |||
| 98bed8ff3d | |||
| 947e1c1c67 |
@@ -93,6 +93,16 @@ Three properties fall out, and they are the reason this shape is worth its cost:
|
||||
drift, and drift is surfaced to an operator — the same posture a lease's `restore()` takes when it
|
||||
finds a value a human has moved ([kit][kit] ch. 5).
|
||||
|
||||
**Phase 0 exercised all three against the real store and they hold — but it found four rules the
|
||||
push path has to obey (§12.2).** The one that would have cost the most:
|
||||
**`permission.GrantUserPermission` silently no-ops when the permission is not registered.** It
|
||||
returns `void`, throws nothing and logs nothing; the grant simply does not happen. A permission is
|
||||
registered by the plugin that declares it, so **every grant naming an unloaded, renamed or
|
||||
uninstalled plugin's permission disappears without a trace** — and since R2's whole recovery story is
|
||||
"the site re-pushes the full set on connect", a re-push into a server missing one plugin is a silent
|
||||
partial. The push must check `PermissionExists` (or register the name itself) and report the
|
||||
difference as drift rather than assuming a write landed.
|
||||
|
||||
**This is a direction the Integration Kit has no chapter for, and that is a finding.** Chapters 3 and
|
||||
4 are the read path — data leaving the game. Chapter 5 is one-shot commands with a ledger and a
|
||||
teardown. This is neither: it is *continuously reconciled state where the website is authoritative*,
|
||||
@@ -171,12 +181,30 @@ direction worth stating plainly: for Teams, the plugin is *worse* than first-par
|
||||
`Clans` v0.2.10 (k1lly0u, MIT, 2,692 lines) publishes **fifteen `[HookMethod]`s and every one of
|
||||
them is a mutation** — `CreateClan`, `JoinClan`, `LeaveClan`, `KickPlayer`, `PromotePlayer`,
|
||||
`DemotePlayer`, `DisbandClan`, and the eight alliance verbs. **There is no read API whatsoever**: no
|
||||
`GetClan`, no `GetClanOf`, no `GetClanMembers`, no `GetAllClans`. And it raises exactly **three**
|
||||
hooks — `OnClanCreate`, `OnClanChat`, `OnAllianceChat` — **none of which is a membership
|
||||
transition**.
|
||||
`GetClan`, no `GetClanOf`, no `GetClanMembers`, no `GetAllClans`.
|
||||
|
||||
So it cannot answer any of core's three provider questions from its published surface, while
|
||||
first-party clans answer all three. Feeding the Team provider from first-party clans is therefore
|
||||
> **Corrected in phase 0 (§12.3).** This paragraph continued *"it raises exactly **three** hooks —
|
||||
> `OnClanCreate`, `OnClanChat`, `OnAllianceChat` — none of which is a membership transition"*, and
|
||||
> concluded that the plugin cannot answer core's provider questions at all. **That was a grep
|
||||
> artefact and it is wrong.** `Clans` 0.2.10 raises **nine** hooks. The missing six are invisible to
|
||||
> a search for `CallHook("OnClan…` because the name is a `const` at the call site:
|
||||
>
|
||||
> ```csharp
|
||||
> const string HOOK_NAME = "OnClanMemberJoined";
|
||||
> Interface.CallHook(HOOK_NAME, tag, ulong.Parse(joining), RustMemberList);
|
||||
> ```
|
||||
>
|
||||
> They are `OnClanMemberJoined(tag, joining, members)`, `OnClanMemberGone(tag, leaving, members)`,
|
||||
> `OnClanDisbanded(tag, members)`, `OnClanAllianceCreated`, `OnClanAllianceDissolved`, and
|
||||
> `OnClanUpdate(tag)`. The first three **carry the full member list**, so the plugin *can* answer
|
||||
> "who is in this clan" without any read API, and `OnClanUpdate` fires on promote and demote — the
|
||||
> exact transitions first-party lacks.
|
||||
|
||||
**The decision does not change, but its reason does.** First-party remains the Team provider's source
|
||||
because it is the system the *game* maintains and every server has it; the plugin is an optional
|
||||
install that not every shard will run, and feeding a provider from something optional makes Teams
|
||||
conditional on a mod. It is no longer true that the plugin *cannot* answer the questions — only that
|
||||
it should not be the one asked. Feeding the Team provider from first-party clans is still
|
||||
**permanent, not a first step**.
|
||||
|
||||
What the plugin genuinely adds is **alliances and clan/alliance chat** — richer in *features*, not in
|
||||
@@ -263,6 +291,12 @@ kicked and left — but **no promote or leader-changed event**. Core's provider
|
||||
transitions. That makes phase 9 *partly* snapshot-driven where the dry run predicted it would be
|
||||
fully event-driven — a small correction to that document, recorded here rather than silently.
|
||||
|
||||
Phase 0 verified the seven against `agent/hooks.tsv` and the gap is real. It also found that the
|
||||
**Clans plugin closes it** — `OnClanUpdate(tag)` fires on both promote and demote (§12.3). That does
|
||||
not move the provider off first-party, but it does mean phase 17's adapter can offer *event-driven
|
||||
leadership* on servers that run the plugin, over a snapshot baseline on servers that do not. Design
|
||||
phase 9's snapshot so phase 17 can sharpen it rather than replace it.
|
||||
|
||||
### R7 — the notifications and engagement set ships in v1
|
||||
|
||||
**Decided 2026-09-15 (org lead).** `registerNotificationStreams`, `registerEventTriggers`,
|
||||
@@ -519,7 +553,7 @@ someone goes looking for one that is not there.
|
||||
| **`GetPlayerZoneIDsNoAlloc(player, List<string>)`** | the allocation-free variant — **the one to use on any sweep** |
|
||||
| `CreateOrUpdateZone(zoneId, args, position)` | make a zone |
|
||||
| **`CreateOrUpdateTemporaryZone(..., Plugin owner)`** | make a zone *owned by our plugin* |
|
||||
| **`EraseTemporaryZone(Plugin owner, zoneId)`** | remove one, **scoped to the owner** |
|
||||
| **`EraseTemporaryZone(Plugin owner, zoneId)`** | remove one; owner-scoped **only against another plugin's zone**, not against an unowned one (§12.4) |
|
||||
| `GetZoneIDs` / `GetZoneName` / `GetZoneLocation` / `CheckZoneID` | the catalogue, for an option source |
|
||||
|
||||
And nine hooks raised: `OnEnterZone` / `OnExitZone`, `OnEntityEnterZone` / `OnEntityExitZone`,
|
||||
@@ -543,10 +577,30 @@ expensive and less accurate.
|
||||
**`rust.zone.open` moves from the optional tier into the base catalogue, and it can be
|
||||
`reversible: 'ledger'` honestly.** This is the nicer half: `CreateOrUpdateTemporaryZone` takes a
|
||||
**`Plugin owner`** and `EraseTemporaryZone` is **scoped to that owner**, so ZoneManager already has a
|
||||
first-class notion of a zone belonging to the plugin that made it. That is most of the persisted
|
||||
ownership registry [kit][kit] ch. 4 demands — we still keep our own map from core's resource
|
||||
reference to the zone id, but we are not inventing ownership, we are borrowing a concept the plugin
|
||||
already has. And erasing a zone that is gone is a success, which is what `revert` needs.
|
||||
first-class notion of a zone belonging to the plugin that made it. And erasing a zone that is gone is
|
||||
a success, which is what `revert` needs.
|
||||
|
||||
> **Narrowed in phase 0 (§12.4), and this one is a safety correction.** The scoping is real but it is
|
||||
> *one-directional*: it stops us erasing a zone owned by **another plugin**, and does nothing at all
|
||||
> for a zone owned by **nobody**.
|
||||
>
|
||||
> ```csharp
|
||||
> // Only compare zone owner if the owner param is provided so users can remove temporary zones
|
||||
> // without needing to unload the plugin that created them
|
||||
> if (owner && zoneOwner && owner != zoneOwner)
|
||||
> return false;
|
||||
> ```
|
||||
>
|
||||
> `zoneOwner` is null for every *permanent* zone — which is every zone an operator made by hand. So
|
||||
> `EraseTemporaryZone(us, "<operator's zone>")` **deletes it and returns `true`**, indistinguishable
|
||||
> from erasing our own. Observed live: a zone created with `CreateOrUpdateZone` and no owner was
|
||||
> erased by an `EraseTemporaryZone` call from an unrelated plugin.
|
||||
>
|
||||
> So this is **less** of ch. 4's persisted ownership registry than the paragraph above claims. We
|
||||
> still keep our own map from core's resource reference to the zone id, and that map is now
|
||||
> **load-bearing rather than convenient**: phase 12 must refuse to erase any zone id it did not
|
||||
> record creating. ZoneManager will not refuse on our behalf, and the `true` it returns is not
|
||||
> evidence the zone was ours.
|
||||
|
||||
**One trap to design against, and it is chapter 4's rule meeting a chatty hook.** `OnEnterZone` and
|
||||
`OnExitZone` fire on the game thread and a large zone with a busy server produces a great many of
|
||||
@@ -635,22 +689,43 @@ online. The persisted pending-grant queue that question was weighing is not need
|
||||
|
||||
## 4. The test rig
|
||||
|
||||
`D:\rust` on the org lead's workstation. It has been booted, it has a generated world
|
||||
(procedural, seed 1234, size 4000, save v287) and Oxide **2.0.7585** matched to its build, and its
|
||||
Oxide permission store already holds a `default` and an `admin` group with one admin user — which
|
||||
means R2's mechanism can be exercised on day one.
|
||||
`D:\rust` on the org lead's workstation. **Brought current in phase 0 (see §12):** build
|
||||
**25230300**, Oxide **2.0.7716**, a fresh procedural world (seed 1234, size 4000) generated for this
|
||||
wipe, and all four base plugins loaded. Its Oxide permission store holds a `default` and an `admin`
|
||||
group with one admin user, so R2's mechanism was exercised on day one and works.
|
||||
|
||||
Two traps recorded here because both cost time before they were understood:
|
||||
A wipe keeps `server/server1/cfg/`. That directory holds `users.cfg`, and `users.cfg` holds the
|
||||
`ownerid` line — delete the whole identity directory and you silently remove the operator's own
|
||||
ownership along with the map.
|
||||
|
||||
- **`D:\rust\start.bat` updated the wrong directory — fixed 2026-09-15.** It ran
|
||||
`steamcmd +force_install_dir c:\rustserver\ +app_update 258550` and then launched
|
||||
`D:\rust\RustDedicated.exe`. The server that boots had never been updated by its own script, which
|
||||
is why a second, never-booted install exists at `C:\rustserver` and why `D:\rust` is a wipe behind.
|
||||
Now reads `+force_install_dir d:\rust\`; the original is kept at `D:\rust\start.bat.bak`.
|
||||
`D:\rust\steamapps\appmanifest_258550.acf` was already present at the same buildid, so the first
|
||||
corrected run is a delta to the current wipe rather than a 5.9 GB re-download.
|
||||
Three traps recorded here because each cost time before it was understood:
|
||||
|
||||
- **`start.bat` never updated anything — the 2026-09-15 diagnosis was wrong, corrected in phase 0.**
|
||||
The script was read as *"updates `C:\rustserver`, runs `D:\rust`"*, and the fix changed the path to
|
||||
`d:\rust\`. The path was never the problem. **steamcmd requires `+force_install_dir` before
|
||||
`+login`**, and the script had it after:
|
||||
|
||||
```
|
||||
steamcmd.exe +login anonymous +force_install_dir d:\rust\ +app_update 258550 +quit
|
||||
→ Please use force_install_dir before logon!
|
||||
→ Error! App '258550' state is 0x486 after update job.
|
||||
```
|
||||
|
||||
So the flag was discarded, the update ran against steamcmd's own directory, and the job errored out
|
||||
every single time. It updated **no** directory, ever — which is the actual reason `D:\rust` fell a
|
||||
wipe behind, and why `C:\rustserver` sits at the *same* stale buildid rather than a newer one.
|
||||
Now reads `+force_install_dir d:\rust\ +login anonymous +app_update 258550 +quit`; the pristine
|
||||
original is kept at `start.bat.bak` and the path-only fix at `start.bat.broken-order-20260915`.
|
||||
With the ordering right, the run is a delta and takes minutes.
|
||||
|
||||
- **Re-extract Oxide after every `app_update`.** Updating the server and re-installing Oxide together
|
||||
is the standard operator routine, not a discovery — Oxide ships a *patched* `Assembly-CSharp.dll`
|
||||
and a Steam update restores Facepunch's. Recorded here only for the mechanical detail: the update
|
||||
does **not** remove `Oxide.Core.dll` and friends, so a half-done install still *looks* Oxided while
|
||||
loading no plugins and raising no hook. Check the size rather than the directory — on build
|
||||
25230300 vanilla is 9,758,544 bytes and Oxide 2.0.7716's is 9,953,280.
|
||||
- **`C:\oxide_files` is a 2025-04-23 Oxide and must not be copied anywhere.** Oxide ships a patched
|
||||
`Assembly-CSharp.dll`; that bundle's is 6,842,880 bytes against the live 9,780,224, so copying it
|
||||
`Assembly-CSharp.dll`; that bundle's is 6,842,880 bytes against the live 9,953,280, so copying it
|
||||
over a real install is a hard downgrade. `D:\rust` is already correct and needs nothing from it.
|
||||
|
||||
**Rust force-wipes on the first Thursday of the month and Oxide is rebuilt to match**, so "is the
|
||||
@@ -673,7 +748,7 @@ Each phase ends with its findings written down, as every workstream here does.
|
||||
|
||||
| # | Phase | Repos | Done when |
|
||||
|---|---|---|---|
|
||||
| 0 | **The rig.** Update to the current wipe (the script is fixed), confirm the Oxide build still matches, install the base set — Kits, Clans, PopupNotifications, ZoneManager (R6, R17) — prove a console grant reaches a plugin | docs | A current server boots with all four loaded, `oxide.grant` demonstrably gates something, and a test zone reports who is standing in it |
|
||||
| 0 | **The rig.** ✅ **Done 2026-09-15 — as built and findings in §12.** Updated to the current wipe (the script was fixed *again*, properly), Oxide re-laid, base set installed, the grant path proven end to end and both zone transitions observed live with a player connected. **Both criteria met** | docs | A current server boots with all four loaded, `oxide.grant` demonstrably gates something, and a test zone reports who is standing in it |
|
||||
| 1 | **Protocol 1, three skeletons, and every bundle seam at once.** Plugin: bounded drop-oldest queue, one writer thread, tagged reconnect epoch, dial-out. Sidecar: listener, SQLite, always-on token auth, version header, rpc correlation. Module: `id: rust`, `/rust` on all three tiers (R14), `schema.sql` **and `purge.sql`**, the **`extensions`** declaration (§11.3), per-server sidecar tokens through **`ctx.secretBox`** (§11.4), the vite aliases and shims, `checkExternals`, `checkImports`, the swagger fragment and its staleness check, explicit `onBoot`/`onShutdown`, `capabilities` | all 3 + docs | One hello line travels game -> sidecar -> module; killing the sidecar does not stall the game; all five guards green on an untouched skeleton |
|
||||
| 2 | **Packaging and release.** `release.yml`, the install manifest, the `sha256`, the host allowlist — and a real install into a running core from a manifest URL | Module-Rust + docs | An operator installs the empty module from Admin -> Modules and it reaches `started` |
|
||||
| 3 | **The read path.** First hook wave from [`HOOKS.md`](HOOKS.md); events and snapshots distinct at the wire; `wipe_id` **and server id** on every row (R8); all-time rollups (R12); every board re-emitted on connect | all 3 + docs | A restarted sidecar is fully populated within one connection, and a wipe does not erase a player's history |
|
||||
@@ -925,7 +1000,7 @@ cleanly, reads back cleanly, and does nothing at all, and neither core nor revie
|
||||
| `rust.kit.entitle` | `change` | `ledger` | R16 — grants the kit's `RequiredPermission`; revert revokes |
|
||||
| `rust.prefab.place` | `change` | `ledger` | §H's verb; revert kills the entity, and needs the persisted ownership registry ch. 4 describes |
|
||||
| `rust.announce` | `notify` | `none` | via PopupNotifications (R6) — global or targeted |
|
||||
| `rust.zone.open` | `change` | `ledger` | §H's other verb. **Base, not optional, since R17** — `CreateOrUpdateTemporaryZone` takes a `Plugin owner` and `EraseTemporaryZone` is scoped to it, so the undo is real |
|
||||
| `rust.zone.open` | `change` | `ledger` | §H's other verb. **Base, not optional, since R17** — `CreateOrUpdateTemporaryZone` takes a `Plugin owner`, so the undo is real. Our own id map decides what may be erased, not ZoneManager's owner check (§12.4) |
|
||||
|
||||
**Rewards are not a contract member.** `EVENTS.md` deleted a `registerEventRewards` registry because
|
||||
it carried four Ultima Online nouns inside a core signature. A reward here is an ordinary action —
|
||||
@@ -1096,4 +1171,181 @@ So R10's capability probe already has the behaviour the app wants: a Rust module
|
||||
makes the app render a site *without* those screens, rather than one advertising screens that `503`.
|
||||
The app needs no failure handling for this case because core does not expose the failure.
|
||||
|
||||
## 12. Phase 0 as built — the rig, 2026-09-15
|
||||
|
||||
The rig is current, the base set runs, and **both acceptance criteria are met**. Six things were
|
||||
learned that the plan had either wrong or had never asked, and four of them change work in later
|
||||
phases.
|
||||
|
||||
### 12.0 What the rig is now
|
||||
|
||||
| | Before | After |
|
||||
|---|---|---|
|
||||
| Server build | `24613624` (2026-08-13) | **`25230300`** (2026-09-10) |
|
||||
| Oxide | `2.0.7585` | **`2.0.7716`** (`OxideMod/Oxide.Rust`, 2026-09-11) |
|
||||
| World | seed 1234 save v287, previous wipe | regenerated for this wipe; `cfg/` preserved |
|
||||
| `oxide/plugins/` | empty | Kits 4.4.9 · Clans 0.2.10 · Popup Notifications 0.2.1 · Zone Manager 3.1.14 |
|
||||
|
||||
All four compiled and loaded first time on the new build, at exactly the versions R6 and R17 name —
|
||||
pulled fresh from `https://umod.org/plugins/<Name>.cs`, which still serves those versions and needs
|
||||
no Cloudflare workaround. Server protocol `2633.288.1`.
|
||||
|
||||
The Oxide permission store survived the update untouched (`oxide/` is not a Steam depot directory),
|
||||
so `76561198038695917` is still in `default` and `admin`.
|
||||
|
||||
Two instruments were built and are kept in the phase-0 scratchpad rather than committed: a
|
||||
dependency-free **WebSocket RCON driver** (Node's global `WebSocket`, no `ws` package), and
|
||||
**`RGProbe.cs`**, a throwaway Oxide plugin that exposes Oxide's permission API and ZoneManager's
|
||||
by-name API as console commands. The probe is what made §12.2 and §12.4 observable; phase 1's plugin
|
||||
skeleton can start from it.
|
||||
|
||||
> **One thing the RCON driver had to learn.** Oxide tags its own `Puts()` output and its warnings
|
||||
> with the **identifier of the command being run**, so a first-match-wins client reads a plugin's log
|
||||
> line as if it were the reply and discards the real one. It cost two wrong readings before it was
|
||||
> spotted. Collect every frame in a window; do not correlate one reply per identifier.
|
||||
|
||||
### 12.1 The rig's own script was broken in a way the earlier diagnosis missed
|
||||
|
||||
Recorded in §4. In short: `start.bat` put `+force_install_dir` **after** `+login`, steamcmd discarded
|
||||
it, and every update run in the rig's history errored out without updating anything. The 2026-09-15
|
||||
"fix" changed the path and left the order, so it fixed nothing.
|
||||
|
||||
The Oxide re-install in §4 is **not** a finding — pairing a server update with an Oxide re-install is
|
||||
the routine every Rust host already follows, and saying otherwise would be this plan talking down to
|
||||
its own audience. One narrow consequence is still worth carrying to **phase 18**: because
|
||||
`app_update` leaves `Oxide.Core.dll` and the rest in place, a `doctor` check that tests for `oxide/`
|
||||
or for Oxide's assemblies **passes on a server that is mid-routine**. Compare the
|
||||
`Assembly-CSharp.dll` against the Oxide build instead, so `doctor` reports the real state rather than
|
||||
a directory listing.
|
||||
|
||||
### 12.2 Four rules the R2 permission push must obey
|
||||
|
||||
Verified live against the real store, granting and revoking through both the console command and the
|
||||
API:
|
||||
|
||||
1. **`permission.GrantUserPermission` silently no-ops for an unregistered permission.** `void`, no
|
||||
throw, no log. The console `oxide.grant` at least answers `Permission 'x' doesn't exist`; the API
|
||||
path R2 uses says nothing at all. This is the finding with teeth — see R2.
|
||||
2. **A permission exists only because a loaded plugin registered it.** Kits registers `kits.admin`
|
||||
and, dynamically, **every kit's `RequiredPermission`** (`Kits.cs:1225`, `:2895`) — which is what
|
||||
makes R16's entitlement model real. Unload Kits and those names stop existing.
|
||||
3. **`RegisterPermission` warns about a foreign prefix but registers anyway.**
|
||||
`Missing plugin name prefix 'rgprobe' for permission 'someplugin.vip'` is a warning, not a
|
||||
refusal — the permission was created and granted successfully. So the site *can* make a grant
|
||||
stick for a plugin that is not currently loaded, at the cost of a console warning. Whether it
|
||||
*should* is a phase 7 decision; the mechanism exists.
|
||||
4. **A player who has never connected is in no group, but can hold direct grants.** A grant to an
|
||||
unseen SteamID64 works and reads back immediately. Group membership does not exist for them yet,
|
||||
so **anything the site expresses as group membership does not reach a player until their first
|
||||
connection**, while a direct grant does. R16's offline entitlement is safe; a group-shaped
|
||||
entitlement is not.
|
||||
|
||||
Point 4 is the one to carry into phase 7's design: grants and groups have **different reach** for
|
||||
offline players, and the site's model currently treats them as two spellings of the same thing.
|
||||
|
||||
### 12.3 R5's claim about the Clans plugin was a grep artefact
|
||||
|
||||
Corrected in R5. The plugin raises nine hooks, not three, and three of them carry full member lists;
|
||||
the six that were missed are invisible to a literal search because the hook name is a `const` at the
|
||||
call site. The decision stands on a different reason — first-party is what every server has, the
|
||||
plugin is optional — and phase 17 gains event-driven leadership as a sharpening rather than a
|
||||
replacement.
|
||||
|
||||
Two smaller things from the same read, both worth having before phase 9 and 17:
|
||||
|
||||
- **`Clans` raises the same hook name twice per transition**, once Rust-typed
|
||||
(`string, ulong, List<ulong>`) and once Universal-typed (`string, string, List<string>`), plus two
|
||||
deprecated arities. Oxide binds by name **and** arity, and both live forms are arity 3 — so a
|
||||
loosely typed subscriber catches both and double-counts every join and leave. Type the parameters
|
||||
precisely and pick one.
|
||||
- **`Clans` calls `API_RegisterThirdPartyTitle` itself.** R15's BetterChat integration will be the
|
||||
*second* title provider on any server running both, not the first.
|
||||
|
||||
Also confirmed, since the plan rests on it: the first-party set is exactly the **seven** hooks in
|
||||
`agent/hooks.tsv`, all "no return behavior", with no promote and no leader-changed.
|
||||
|
||||
### 12.4 ZoneManager's owner scoping is narrower than R17 assumed
|
||||
|
||||
Corrected in R17. `EraseTemporaryZone(owner, id)` refuses only when the zone has a *different*
|
||||
owner; an **unowned** zone — every permanent zone, including every zone an operator made by hand — is
|
||||
erased by anyone and returns `true`. Phase 12 must gate erasure on its own id map.
|
||||
|
||||
Three more things the source and the live rig agreed on:
|
||||
|
||||
- **ZoneManager's entire API is plain private methods**, no `[HookMethod]` anywhere in 3.1.14 — so
|
||||
`Call()` by name is the only way in, and a typo is silence. Confirmed working live for
|
||||
`CreateOrUpdateZone`, `CreateOrUpdateTemporaryZone`, `EraseTemporaryZone`, `GetZoneIDs` and
|
||||
`GetPlayersInZone`. The three-conventions finding holds: Kits declares `[HookMethod]` (23 of them),
|
||||
ZoneManager declares nothing, BetterChat will use `API_` prefixes.
|
||||
- **`GetPlayersInZone` cannot distinguish an unknown zone from an empty one** — both return an empty
|
||||
list, not null. The participation ledger R17 wants to feed therefore cannot use this call alone to
|
||||
answer "is this zone still there", and must check `GetZoneIDs` separately. This is the same
|
||||
absence-of-an-answer / answer-of-absence trap earlier phases of other workstreams hit.
|
||||
- **NPCs never appear in a zone's player list.** `baseEntity is BasePlayer { IsNpc: false }` routes
|
||||
them to the zone's *entity* list instead. Useful to know before designing a condition that counts
|
||||
"players at the monument" on a server with scientists.
|
||||
|
||||
### 12.5 The criterion is closed, and it revealed two ceilings on the rig
|
||||
|
||||
> `oxide.grant` demonstrably gates something, and a test zone reports who is standing in it
|
||||
|
||||
**Both halves done**, the second with the org lead connected. The zone was created on the player's
|
||||
own position, and ZoneManager reported both transitions live:
|
||||
|
||||
```
|
||||
[probe] ENTER zone=rgtest player=76561198038695917 (whitlocktech)
|
||||
[probe] zone=rgtest occupancy=1 [76561198038695917:whitlocktech]
|
||||
[probe] EXIT zone=rgtest player=76561198038695917 (whitlocktech)
|
||||
```
|
||||
|
||||
The exit was produced by **moving the zone off the player** rather than walking them out —
|
||||
`CreateOrUpdateZone` on an existing id relocates the trigger volume and fires `OnExitZone` as it
|
||||
leaves. Useful for testing presence without choreographing a person.
|
||||
|
||||
So **R17's "presence transitions as events" is verified**, which is the claim the participation
|
||||
ledger and the advance conditions both rest on.
|
||||
|
||||
Two ceilings surfaced on the way, and both constrain later phases:
|
||||
|
||||
**1. No console session can observe a gate.** The standard idiom is
|
||||
`return !player || permission.UserHasPermission(...)` — a command from RCON has no `BasePlayer`, so
|
||||
the console is unconditionally allowed. Anything whose acceptance needs a permission to actually
|
||||
*refuse* somebody needs a client attached.
|
||||
|
||||
**2. An admin account cannot see a refusal either — from most plugins.** The bypass is not uniform,
|
||||
and the difference decides which phases can be demonstrated on the org lead's own account:
|
||||
|
||||
| Plugin | Admin bypass | Demonstrable as owner? |
|
||||
|---|---|---|
|
||||
| Popup Notifications | `player.IsAdmin \|\|` — hard | **No** |
|
||||
| Zone Manager | `authLevel > 0 \|\|` — hard | **No** |
|
||||
| Kits (`RequiredPermission`) | `Configuration.AdminIgnoreRestrictions && IsAdmin(player)`, and Kits' own `IsAdmin` is the `kits.admin` **permission**, not auth level. The shipped default is **`false`** | **Yes** |
|
||||
|
||||
So **phase 13 is demonstrable on this rig as it stands** — R16's entitlement gate applies to a server
|
||||
owner like anyone else. **Phase 7 is not**, if its acceptance is "a grant made on the website gates a
|
||||
third-party plugin in-game" against Popup Notifications or Zone Manager: that needs a **second,
|
||||
non-admin Steam account**. Worth arranging before phase 7 rather than discovering there.
|
||||
|
||||
### 12.6 R18's trees, as they actually look
|
||||
|
||||
The four base plugins wrote their configs on first boot, so R18's two trees can be compared against
|
||||
something real rather than predicted:
|
||||
|
||||
```
|
||||
oxide/config/ Clans.json Kits.json PopupNotifications.json ZoneManager.json
|
||||
oxide/data/ clan_data.json Kits/kits_data.json Kits/player_data.json
|
||||
ZoneManager/zone_data.json
|
||||
oxide.users.data oxide.groups.data oxide.covalence.data oxide.lang.data
|
||||
```
|
||||
|
||||
R18's inventory of `data/` was exactly right. One nuance worth correcting, though: R18 motivates the
|
||||
**recursive** walk with *"plugins nest (`config/<Mod>/x.json` and deeper)"*, and on a fresh install of
|
||||
the base set **`config/` is flat — it is `data/` that nests.** The recursive walk is still correct
|
||||
(other plugins do nest configs), but the nesting the plan cites as its reason is currently visible
|
||||
only in the tree it must never walk.
|
||||
|
||||
And a reason to hold that boundary harder than R18 states: **`oxide/data/` is where Oxide keeps its
|
||||
own permission store** (`oxide.users.data`, `oxide.groups.data`). A config editor that strayed one
|
||||
directory over would be editing R2's mirror underneath itself.
|
||||
|
||||
[kit]: https://gitea.whitlocktech.com/RunicGateway/Integration-kit
|
||||
|
||||
Reference in New Issue
Block a user