docs(rust): plan the six step-3 redesigns in one document
PLAN_REDESIGNS.md plans PLAN_FIXES §6 step 3 in detail, before any code: the permission manager, the event step editor and kit weekend, zones and domes, map marker types, the 23 title conditions, and NPCs. Checked on both rigs first: the public owner lookup for permissions, the mission hooks both frameworks already raise, and ZoneDomes' null-player throw. Eleven questions for the org lead (§10) become D188 onward. Refs RunicGateway/Module-Rust#21 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -428,6 +428,8 @@ in Rust-Plugins, Rust-Link and Module-Rust, and the three cut over to `main` tog
|
||||
3. **The redesigns**, each planned in detail before code and walked on both frameworks: the permission manager
|
||||
(§4.1), the event step editor with the kit-weekend template (§4.2, §4.3), zones and domes (§4.4), the map's
|
||||
marker types (§4.5), the chat title conditions (§4.6), and the NPC research (§4.7).
|
||||
*Planned 2026-09-27:* all six are in one document, [`PLAN_REDESIGNS.md`](PLAN_REDESIGNS.md), as the org
|
||||
lead chose. Its eleven questions (§10) are answered before any code.
|
||||
4. **The cutover**, once the re-walk (§8) passes on Oxide and Carbon.
|
||||
|
||||
## 7. Helper plugins (D168)
|
||||
@@ -464,7 +466,9 @@ The four questions this plan first asked were answered on 2026-09-26: the gate (
|
||||
(D171) and the title conditions (D172) — then quests (D173), the title rules (D174) and admin-owned title names
|
||||
(D175).
|
||||
|
||||
Left for the detailed plans of §6 step 3, none of them blocking the fixes:
|
||||
Left for the detailed plans of §6 step 3, none of them blocking the fixes. Each is answered, or turned into a
|
||||
question, in [`PLAN_REDESIGNS.md`](PLAN_REDESIGNS.md): 1 by a framework hook (its §0.4), 2 as Q9, 3 on the rig
|
||||
(§3.2), and 4 as Q10.
|
||||
|
||||
1. How a completed mission is detected — the spike in §4.6 (D173).
|
||||
2. Whether the plugin or the site filters monument types (§4.5).
|
||||
|
||||
527
modules/rust/PLAN_REDESIGNS.md
Normal file
527
modules/rust/PLAN_REDESIGNS.md
Normal file
@@ -0,0 +1,527 @@
|
||||
# `module-rust` — the redesigns, planned
|
||||
|
||||
**Status:** plan, written 2026-09-27, **waiting for the org lead's answers to §10**. It is step 3 of
|
||||
[`PLAN_FIXES.md`](PLAN_FIXES.md) §6: the six changes the org lead decided during the first player walk,
|
||||
each "planned in detail before code". The org lead asked for all six in one plan (2026-09-27), so they can
|
||||
be read together. No code has been written for any of them.
|
||||
|
||||
This is a companion to [`PLAN_FIXES.md`](PLAN_FIXES.md) and [`PLAN.md`](PLAN.md). Where they disagree, this
|
||||
document is later and wins. The answers to §10 become decisions of record from **D188** onward, continuing
|
||||
PLAN_FIXES' numbering.
|
||||
|
||||
| § | Redesign | PLAN_FIXES | Where the work lands |
|
||||
|---|---|---|---|
|
||||
| 1 | The permission manager | §4.1, D160–D163 | Rust-Plugins, Module-Rust |
|
||||
| 2 | The event step editor and the kit weekend | §4.2, §4.3, D164, U-3–U-6, F11 | **core** (website `main`), Module-Rust, Rust-Plugins |
|
||||
| 3 | Zones: ZoneManager's options, and a dome | §4.4, D166, D167 | Rust-Plugins (bridge and helper), Module-Rust |
|
||||
| 4 | The live map's marker types | §4.5, D165 | Module-Rust |
|
||||
| 5 | Chat titles: twenty-three conditions | §4.6, D172–D175 | Rust-Plugins, Module-Rust |
|
||||
| 6 | NPCs | §4.7 | Rust-Plugins, Module-Rust |
|
||||
|
||||
**Protocol.** Every wire change here joins **protocol 13**, which is still unreleased on `edge` in all three
|
||||
repos. [`PLAN_FIXES.md`](PLAN_FIXES.md) §5 already lists the inventory verb, ZoneManager's flags, zone
|
||||
options and the new tally fields under it, so this adds no bump (§7).
|
||||
|
||||
**Core.** §2 is mostly core's editor, which is game-agnostic and serves Module-uo too. Its contract
|
||||
additions are **MODULE_API 1.11.0 → 1.12.0**, additions only (§8). The same proof website#209 ran for 1.11.0
|
||||
applies: Module-uo's frozen-manifest job and its suites, run against the new core.
|
||||
|
||||
---
|
||||
|
||||
## 0. What was checked before writing
|
||||
|
||||
Each of these was a question PLAN_FIXES left open or a claim this plan depends on. All were checked on
|
||||
2026-09-27 against the two rigs (`rust-oxide`, `rust-carbon`) or the code. The rig checks used a read-only
|
||||
probe plugin that wrote one data file. It was unloaded and deleted afterwards on both rigs.
|
||||
|
||||
| # | Question | Answer | Where it lands |
|
||||
|---|---|---|---|
|
||||
| 0.1 | Can the bridge learn which plugin registered a permission through public API? | **Yes, on both frameworks.** `permission.PermissionExists(name, owner)` takes the owning plugin (Oxide: `Plugin`; Carbon: `BaseHookable`). Asking it of every loaded plugin named an owner for all 85 permissions on Oxide (`RustCore` owns `oxide.*`) and all but 30 on Carbon, in 3.3 ms and 1.8 ms. **ZoneManager owns all 70 `zonemanager.*` names, `zonemanager.ignoreflag.nokits` included**, which uMod PermissionsManager files under Kits. The 30 Carbon names without a plugin owner are `adminmodule.*`, which belong to Carbon's built-in AdminModule (a Carbon *module*, not a plugin). | §1.2 |
|
||||
| 0.2 | Can the bridge list every user in the store? | **No.** Neither framework has a public "all users" call (`GetUsers` and `GetAllUsers` do not exist on either). The complete holder list comes from `GetPermissionUsers` for each permission plus `GetUsersInGroup` for each group. That took 1.7 ms and 2.2 ms on the rigs' tiny stores, but each call walks every user, so on a real store the cost is permissions × users. | §1.2 (sliced) |
|
||||
| 0.3 | Do groups carry anything the site does not model? | **Parents.** `GetGroupParent` answers on both. The site's `rust_perm_groups` has no parent column. | §1.4 |
|
||||
| 0.4 | Is there a framework hook for a finished Rust mission (D173)? | **Yes: option 1 of D173's spike.** Oxide's patched `Assembly-CSharp.dll` raises `OnMissionSucceeded`, `OnMissionFailed`, `OnMissionStarted` and `OnMissionAssigned`, and `Carbon.Hooks.Oxide.dll` patches the same four names onto `BaseMission`. Rust's own methods are `BaseMission.MissionSuccess(MissionInstance, BasePlayer assignee)` and `MissionFailed(MissionInstance, BasePlayer, MissionFailReason, bool)`. The catalogues [`HOOKS.md`](HOOKS.md) and [`CARBON.md`](CARBON.md) predate them. The exact hook arguments are confirmed when a person finishes a mission (§5.7). | §5.7 |
|
||||
| 0.5 | Is ZoneDomes' API safe with a null player (PLAN_FIXES §4.4, §7)? | **No.** `AddNewDome` and `RemoveExistingDome` end with `player.ChatMessage(...)` and no null check. With a null player the dome is made and saved, and then the call throws. On boot, ZoneDomes also drops every dome whose zone does not exist yet, and the bridge's temporary zones are re-created after that. | §3.2 |
|
||||
| 0.6 | Can ZoneManager's flag list be read at run time? | **Only from inside.** The list is `ZoneManager.ZoneFlags.NameToIndex`, a public static on a nested type. No hook or `Call` method returns it. The zone helper (`RunicGatewayZones.cs`) already references ZoneManager's types, and the bridge may not (R2, D168). | §3.1 |
|
||||
| 0.7 | What does the map actually show? | **85 markers on the rigs' map (world 3000, seed 1234): 31 substations in four prefab kinds, 7 caves in six, 7 train-tunnel entrances in four, and 5 water wells in three.** The label (`Substation`) groups the variants. Rust also classifies monuments itself (`MonumentInfo`'s `MonumentType`: Cave, WaterWell, Lake, Mountain, Radtown, Building, Town, Airport, Lighthouse). | §4 |
|
||||
| 0.8 | Can the site see how much of a kit players have used? | **One player at a time.** `kits.list` already returns each kit's `max` (MaximumUses) and `cooldown`, and the module ignores `cooldown`. Per-player use is read only during a credit settle (`GetPlayerKitUses`). | §2.6 |
|
||||
| 0.9 | Where do the Ultima Online examples a Rust admin sees come from (U-3)? | **Core's own actions.** `core.lease` gives `uo.rate.skillgain` as its example, and the announcement actions talk about Britain and Cove. | §2.1 |
|
||||
| 0.10 | What free NPC plugins exist (§4.7)? | **HumanNPC** (Remod/RFC1920) spawns NPCs with a name and a Kits kit (`SpawnHumanNPC(position, rotation, name, clone)`, `SetHumanNPCInfo`), but it **requires two more plugins, Pathfinding and Waypoints**, and its NPCs run its own AI, not Rust's. **NPC Loadouts** (VisEntities) dresses Rust's own NPCs from configuration, for every NPC of a type rather than one spawn. The bridge's `rust.npc.place` already spawns Rust's own scientists, and a scientist is a `BasePlayer`, so it has an inventory and a `displayName`. | §6 |
|
||||
|
||||
---
|
||||
|
||||
## 1. The permission manager (§4.1; D160–D163)
|
||||
|
||||
### 1.1 In one paragraph
|
||||
|
||||
Today the site pushes the set it authored, and the plugin reports "foreign" holders only for the names the
|
||||
site claims (D31). After this change, **the plugin reads the whole store and the site keeps all of it**. The
|
||||
first read of a server imports everything (D160). After that, every difference between the game and the site
|
||||
is either something the site changed, which it pushes as it does now, or something changed in the game. An
|
||||
in-game change is settled by the server's policy: auto-adopt, adopt, or revoke (D161). The screen is rebuilt on
|
||||
PermissionsManager's flow (D162), and every subject is shown with its linked account and in-game name
|
||||
(D163).
|
||||
|
||||
### 1.2 The inventory (plugin, protocol 13)
|
||||
|
||||
A new request/reply verb, **`perm.inventory`**, returns the whole store in the same shape on both frameworks:
|
||||
|
||||
- **`permissions`**: every registered name with its **`owner`**, the plugin that registered it, found with
|
||||
`PermissionExists(name, plugin)` over `plugins.GetAll()` (§0.1). A name no plugin owns is sent with no
|
||||
owner. On Carbon those are the built-in modules' names, and the site groups them by prefix under
|
||||
"Carbon modules".
|
||||
- **`groups`**: name, title, rank, **parent**, and the permissions the group carries directly
|
||||
(`GetGroupPermissions(name, false)`; inherited permissions are the parent's, not the group's).
|
||||
- **`users`**: for each Steam id holding anything, the permissions held directly, the groups (**except
|
||||
`default`**, which every connected player is in by framework rule and which the site treats as "everyone"),
|
||||
and the last name the store recorded. The name comes from the store's own `id(name)` entry.
|
||||
- **`leased`**: the group permissions an event lease is holding. The site excludes them from the
|
||||
in-game-change classification the way the foreign scan excludes them today (PLAN.md §27.3).
|
||||
|
||||
**Cost.** §0.2 means the read is permissions × users. It is **built in slices on the main thread**, one group
|
||||
of permissions per tick, the same drain the sync already uses (`PermOpsPerStep`). It is then **sent in pages**
|
||||
below 768 KiB, the way `map.fetch` stays under the sidecar's 1 MiB line cap. The site asks for page 0 and
|
||||
follows `more`. A snapshot is taken once and paged from memory, so a change between pages cannot tear it. The
|
||||
build walk measures a large store: the plan's ceiling is 20,000 users × 500 permissions under 2 s of drain,
|
||||
with no single tick over 20 ms.
|
||||
|
||||
**What leaves the wire.** The sync's `managed` list and the plugin's foreign scan (`ScanPermission`,
|
||||
`ScanGroup`) are replaced by the inventory. Protocol 13 is unreleased, so they are removed rather than kept
|
||||
beside it. `perm.sync` groups gain **`parent`**, applied with `SetGroupParent` on both frameworks.
|
||||
|
||||
### 1.3 Who can hold a permission on the site
|
||||
|
||||
Grants and memberships are keyed by **website user** today (D28), and adopt refuses a Steam id with no account
|
||||
("there is nobody to author this against"). D160 makes the site the owner of **every** holder, including the
|
||||
many who never link. So the site gains **Steam-keyed holders**:
|
||||
|
||||
- `rust_perm_steam_grants (steam_id, permission, scope, source, …)` and
|
||||
`rust_perm_steam_members (group_name, steam_id, scope, …)`, beside the existing user-keyed tables.
|
||||
`buildDesired` unions both, and they can overlap; the existing `seenGrant` de-duplication already handles
|
||||
that.
|
||||
- **The import writes Steam-keyed rows, even for a linked player.** They are an exact copy of what the game
|
||||
holds. A user-keyed row would reach every Steam account the person has linked (D28), which would widen what
|
||||
the game held. **Q1.**
|
||||
- **A toggle on the screen for a linked player writes a user-keyed row** (it reaches all their accounts, as
|
||||
today). A toggle for an unlinked player writes a Steam-keyed row.
|
||||
- Linking or unlinking an account moves nothing. A Steam-keyed row stays about that game account, and the
|
||||
screen shows it under the person's name once they link.
|
||||
|
||||
### 1.4 Groups across servers
|
||||
|
||||
A group is one name fleet-wide today (`rust_perm_groups.name` is the primary key), with one scope, and
|
||||
**everything it carries or contains is the same on every server in that scope**. Real servers disagree: each
|
||||
has its own `default` and `admin`, and a `vip` on one server can carry different permissions from a `vip` on
|
||||
another. The import has to keep both. The proposal (**Q2**):
|
||||
|
||||
- **The name stays fleet-wide, and what the group carries and contains becomes per-server.**
|
||||
`rust_perm_group_permissions` gains `scope`, like grants have (D29). The member tables are scoped the same
|
||||
way. The import writes rows scoped to the server it read.
|
||||
- **The group's own scope** is the server it was first imported from. When a second server has the same name,
|
||||
the scope widens to `*`. Every server in scope then has the group, which may mean an empty group on a server
|
||||
that never had one. That is harmless, and it is what the site would push today anyway.
|
||||
- `rust_perm_groups` gains **`parent`**. Title, rank and parent stay one value per name. The import takes the
|
||||
first server's values and reports a disagreement between servers on the screen; it does not pick silently.
|
||||
- **Built-in groups** (`default` and `admin` on both frameworks, plus `moderator` on Carbon) are imported and
|
||||
editable, and **never retired**: the site does not send `RetireGroup` for them. `default`'s membership is
|
||||
never modelled (§1.2).
|
||||
|
||||
### 1.5 The sync, rebuilt on three sets
|
||||
|
||||
`rust_perm_pushed`'s own comment names the three sets. Until now the plugin could only compute the third one
|
||||
for the names the site claimed. The inventory gives the site all three:
|
||||
|
||||
| In the game | Pushed (the site put it there) | Desired (the site authors it) | Means |
|
||||
|---|---|---|---|
|
||||
| yes | no | no | **added in the game** |
|
||||
| no | yes | yes | **removed in the game** |
|
||||
| no | no | yes | to apply (today's path) |
|
||||
| — | yes | no | to retire (today's path) |
|
||||
| yes | no | yes | landed by someone else; recorded as pushed |
|
||||
|
||||
One sync becomes: **inventory, then classify, then apply the policy (§1.6), then push** the desired set and the
|
||||
retirements with the existing `perm.sync`. D32's shape is unchanged: whole set, one verb, the plugin
|
||||
reconciles. Triggers are unchanged too. The drift hooks still mark the server dirty, so an in-game change is
|
||||
settled within one tick (30 s) plus the read.
|
||||
|
||||
**The first import (D160).** A server with no completed inventory yet (new column
|
||||
`rust_perm_sync.imported_at`) takes everything present as site-owned, whatever its policy, and records it
|
||||
as pushed. Policies apply only to changes after that. **This includes existing installs.** The first inventory
|
||||
after the upgrade imports every current hand edit, including the drift rows now waiting for an answer, and
|
||||
clears those rows. That follows D160 as written; **Q11** confirms it.
|
||||
|
||||
### 1.6 The policy (D161)
|
||||
|
||||
`rust_servers.perm_policy`, one of `auto-adopt` (the default), `adopt`, or `revoke`, set on the permission
|
||||
screen's server header.
|
||||
|
||||
| Policy | Added in the game | Removed in the game |
|
||||
|---|---|---|
|
||||
| **auto-adopt** | written as the site's own: a Steam-keyed row (or a scoped group row) with `source: 'adopted'`, scoped to that server, and recorded as pushed | the site's row is deleted, **when that row is scoped to that server alone** |
|
||||
| **adopt** | a row on the "needs a person" list, with Adopt and Revoke (today's drift answers) | a row on the list, with "Accept the removal" and "Put it back" |
|
||||
| **revoke** | queued as a revocation and removed at this sync | pushed back at this sync (nothing new: the desired set re-applies it) |
|
||||
|
||||
**A removal that touches a fleet-wide row is held for a person under every policy** (**Q3**). A user grant
|
||||
scoped `*`, or a group permission scoped `*`, that disappears from one server cannot be adopted without
|
||||
deciding whether it should still hold everywhere else. Auto-adopt deleting it would take it off every server,
|
||||
and splitting it into per-server rows would silently stop it reaching the next server added. So the list says
|
||||
"removed on `rust-carbon`, granted on all servers" and offers "remove everywhere", "keep everywhere except
|
||||
here" (which rewrites the row into per-server rows) and "put it back".
|
||||
|
||||
### 1.7 The screen (D162, D163, U-1)
|
||||
|
||||
Following PermissionsManager 2.1.2, the reference screenshots in the workspace's `perms-screenshots/`:
|
||||
|
||||
- **A server picker first.** PermissionsManager works on one server, and the site holds several. Everything
|
||||
below is for the chosen server. A row scoped `*` shows an "all servers" badge, and changing it asks whether
|
||||
to change it everywhere or on this server alone.
|
||||
- **Players ⇄ Groups**, as two tabs. The players list shows the server's holders from the inventory first,
|
||||
then a search over every player seen on that server and every linked account. Each row
|
||||
shows the **in-game name**, the **linked website account** if there is one, and the Steam id when the
|
||||
server has no cached name (D163).
|
||||
- **A subject, then its plugins.** The plugin buttons come from the inventory's `owner` (§0.1), never from
|
||||
the name's prefix. A button shows a count of what the subject holds from that plugin.
|
||||
- **A plugin's permissions, with Granted / Revoked**, plus Grant all and Revoke all. **Each toggle carries its
|
||||
own state on this server** (U-1): *granted*; *through group `vip`* (read-only, with a link to the group);
|
||||
*waiting for their first connection* (a member the store cannot place yet); *not registered here* (the
|
||||
plugin is not loaded, formerly "unresolved"); or *did not land* (D85).
|
||||
- **A player's groups** (add, remove, Remove all). **A group's players** (Remove all), and its title, rank and
|
||||
parent.
|
||||
- **The server header:** sync state, the policy, and the "needs a person" list (§1.6).
|
||||
- **`zonemanager.ignoreflag.*`** (the question PLAN_FIXES §4.1 left open, **Q4**): 69 of ZoneManager's 70
|
||||
permissions exempt a player from one zone flag each. Under ZoneManager's button they are shown as one
|
||||
collapsed section, "Exempt from zone flags", labelled with the flag names (No kits, No build, PvP god…).
|
||||
The seventieth, `zonemanager.zone`, is listed normally.
|
||||
|
||||
The existing account-first view (what one website account holds across the fleet) stays, on the account's own
|
||||
page, because that is where the events' grants (D84) and the chat styles (phase 17) are read.
|
||||
|
||||
### 1.8 The walk
|
||||
|
||||
On both rigs: install on a server with hand-made state (a `vip` group with a parent, two direct grants, one to
|
||||
an unlinked Steam id), and check that the import shows it exactly. Then an `oxide.grant` / `c.grant` under each
|
||||
of the three policies. Then a revoke in the game of a server-scoped grant and of a fleet-wide one (held for a
|
||||
person). Then Grant all and Revoke all. The plugin buttons must be by owner: `nokits` is under ZoneManager. The
|
||||
large-store measurement is in §1.2.
|
||||
|
||||
---
|
||||
|
||||
## 2. The event step editor and the kit weekend (§4.2, §4.3; U-3–U-6, F11; D164)
|
||||
|
||||
Most of this is **core's editor** (`EventEditor.jsx`), so it lands on website `main`, stays game-agnostic,
|
||||
and must leave Module-uo exactly as it is.
|
||||
|
||||
### 2.1 A step becomes the thing it does (U-3, U-4, F11)
|
||||
|
||||
- **No type tags.** `ParamField` stops printing ` · {type}` and the bare `*`. A required field says so in
|
||||
words only when it is empty at save time.
|
||||
- **Each type gets its control:** a toggle for `boolean`, a number field with the declared range beside it for
|
||||
`int` and `float`, a date picker for `datetime`, and a text box only for `string` without a source.
|
||||
- **`core.lease` renders the chosen lease's own form.** The lease is picked by its **label**. The step then
|
||||
shows the value as the lease's type (a toggle for a boolean lease, a number with the lease's `min`–`max`
|
||||
beside it, or a dropdown when the lease declares `values`), **the target as a picker over the lease's own
|
||||
`target.source`** (F11, never built: today the editor reads only `param.source`), and the minutes with the
|
||||
lease's `maxDurationMs` shown beside them.
|
||||
- **Examples.** Core's own examples become game-neutral (§0.9). A lease may declare an optional `example` for
|
||||
its value (**MODULE_API 1.12.0**, §8), and `target.example` already exists, so the examples on a lease step
|
||||
are that lease's.
|
||||
|
||||
### 2.2 A phase that holds a timed step (U-6)
|
||||
|
||||
A phase with no advance rule ends when its steps finish, and teardown gives a lease back at once. So the
|
||||
"10 minutes" of a lease lasted one second. The fix is to make the duration visible:
|
||||
|
||||
- A param may declare **`duration: 'minutes'`** (**MODULE_API 1.12.0**). `core.lease`'s `minutes` is one,
|
||||
and so are `rust.zone.open`'s `minutes` and the other modules' equivalents.
|
||||
- When a timed step is added to a phase that has **no advance rule**, the editor gives the phase
|
||||
**`advance: { after: '<the longest duration in the phase>m' }`**, visibly, and the author may change it.
|
||||
- A phase whose advance rule ends before its longest timed step says so under the phase: "This phase ends
|
||||
after 5 minutes; the lease is given back then, not after 10."
|
||||
|
||||
### 2.3 Start with an unpublished edit (U-5)
|
||||
|
||||
When the definition has a saved draft newer than its published version, **Start** asks: "Publish and start",
|
||||
or "Start version *N* (the published one)". Core client only. `runs.create` already pins the published
|
||||
version.
|
||||
|
||||
### 2.4 Popups (§4.3)
|
||||
|
||||
`rust.announce`'s `delivery` already has two values from an option source. With two or three choices the
|
||||
editor draws them as a segmented control rather than a dropdown. Popups for **other** player-facing messages
|
||||
(a zone entered, a reward earned) are **Q6**. The zone's enter message is ZoneManager's own chat line, so a
|
||||
popup there means the bridge sending it on ZoneManager's `OnEnterZone` instead.
|
||||
|
||||
### 2.5 The kit weekend as a template (D164)
|
||||
|
||||
There is no template concept anywhere today. The proposal (**Q5**) is **`api.registerEventTemplates`**
|
||||
(**MODULE_API 1.12.0**): a module declares a named skeleton (phases, steps, params filled in, and a short list
|
||||
of the inputs the author supplies). The editor's "New event" screen offers the skeletons, asks for their
|
||||
inputs, and opens an ordinary draft. Nothing about a template survives into the definition, so it is purely an
|
||||
authoring shortcut and the runner never sees it.
|
||||
|
||||
Module-Rust registers **"Kit weekend"**. Its inputs are a server, a kit and a duration, and it makes one phase
|
||||
with `advance: { after }` equal to the duration and two steps:
|
||||
|
||||
1. `core.lease` → `rust.group.permission`, target `<server>/default/<the kit's permission>`, value `true`,
|
||||
for the duration. The kit becomes visible and redeemable for everyone.
|
||||
2. `rust.kit.entitle` → that kit, **every linked player on the server**, one use each (D103: withdrawn on
|
||||
revert). This recipient mode does not exist yet. Today's modes are all about a run's scoring ("everyone who
|
||||
scored", top N, and so on), so Module-Rust adds **`linked`**.
|
||||
|
||||
### 2.6 The kit warning (D164)
|
||||
|
||||
"N linked players have already used this kit" needs a bulk read the plugin does not have (§0.8). So there is a
|
||||
new verb, **`kits.uses`**: `{kit, steamIds[]}` in, and `{max, cooldown, exhausted, onCooldown}` out (counts,
|
||||
not a list). It is main thread, capped at the site's linked-player count for that server (the same bound as
|
||||
the sync's).
|
||||
|
||||
The warning reaches the core editor through an **optional `note` on an option row** (**MODULE_API 1.12.0**).
|
||||
When the chosen option carries one, the editor shows it under the field. `rust.options.grouppermissions`
|
||||
attaches the note to a `kits.*` target: "Kit `vip`: 2 uses, 24 h cooldown — 9 of 41 linked players have used
|
||||
it up. The weekend's extra use (step 2) is what lets them take it again."
|
||||
|
||||
### 2.7 The walk
|
||||
|
||||
Author a kit weekend from the template on each rig; a lease step with the phase's advance removed (the warning
|
||||
shows); a draft edited after publishing (Start asks); a boolean lease (a toggle); and a Module-uo event opened
|
||||
in the same editor, which must be unchanged apart from the controls.
|
||||
|
||||
---
|
||||
|
||||
## 3. Zones: ZoneManager's options, and a dome (§4.4; D166, D167)
|
||||
|
||||
### 3.1 The options (D166)
|
||||
|
||||
- **Read from the installed ZoneManager, by the helper.** §0.6 means only code that references ZoneManager's
|
||||
types can read the flag list, and D168 puts that in the helper, not the bridge. `RunicGatewayZones.cs` gains
|
||||
a hook, `RgZoneOptions()`, returning ZoneManager's version and `ZoneFlags.NameToIndex`'s names. The bridge
|
||||
calls it and adds **`zoneManager: { version, flags[] }`** to hello. **Without the helper** the site offers
|
||||
only a name, radius and duration, and says why, the same way D182 degrades.
|
||||
- **Settings offered:** the enter and leave messages, radiation, comfort, temperature, safe zone and
|
||||
permission. The bridge keeps control of the id, location, radius and size, and the rest (rotation, parent,
|
||||
eject spawns) stays out, because each one changes what the bridge believes the zone is.
|
||||
- **Wire:** `world.zone` gains `flags` (names) and `settings` (key → value). The plugin checks every flag
|
||||
against the live list and every setting against the allowlist, and refuses an unknown one with a new reason,
|
||||
`bad-option`. It passes them to `CreateOrUpdateTemporaryZone` as ZoneManager's own key/value arguments,
|
||||
stores them on the zone's registry entry, and re-applies them in `RecreateZones` after a restart or a
|
||||
ZoneManager reload.
|
||||
- **The form groups the flags** from one module file (Vehicles, Combat, Building, Loot, NPCs, Communication,
|
||||
Survival). A flag the file does not know (a newer ZoneManager) goes under **Other**, so it is never hidden.
|
||||
**Presets** fill the flags in one click. **Q8** chooses them; the proposal is *Arena* (PvP only, no building,
|
||||
no decay, no suicide, vehicles kept out), *Safe zone* (no PvP, no PvE damage, no building) and *No building*.
|
||||
- **Exemptions** are `zonemanager.ignoreflag.<flag>` permissions, managed on the permission screen (§1.7). The
|
||||
zone form links there.
|
||||
- **Flying** has no ZoneManager flag (PLAN_FIXES §4.4) and is not in this step.
|
||||
|
||||
### 3.2 The dome (D167) — and why this plan proposes drawing it ourselves
|
||||
|
||||
§0.5 found two problems. ZoneDomes' API throws without a player, and its boot path drops every dome of a
|
||||
temporary zone. ZoneDomes itself is only this, for each dome: create `SphereEntity` from one of five prefabs,
|
||||
set `currentRadius` and `lerpRadius` to twice the zone's radius, `enableSaving = false`, and `Spawn()`
|
||||
(`stack` copies to make it darker).
|
||||
|
||||
**Q7:** the proposal is that **the bridge draws the dome itself**, from the same prefabs, as part of the zone's
|
||||
own registry entry. It is made with the zone, re-made with it in `RecreateZones`, and killed with it at expiry
|
||||
or teardown. There is no data file that outlives the zone, and there is no boot-order race. It **reverses
|
||||
D167's dependency** (ZoneDomes would no longer be needed) and keeps D167's intent: the zone step gains "show a
|
||||
dome". The alternative keeps ZoneDomes and patches its message call in the helper, which is a Harmony patch
|
||||
on a third plugin to work around a one-line bug.
|
||||
|
||||
**The default type and stack** (PLAN_FIXES §9 question 3) are chosen by looking at each on the rig, in daylight
|
||||
and at night, during the build walk. The form offers type and stack, with that default.
|
||||
|
||||
### 3.3 The walk
|
||||
|
||||
A zone with the *Arena* preset on each rig: the flags hold for a player without the exemption and not for one
|
||||
with it; a restart and a ZoneManager reload keep the flags and the dome; expiry removes the dome; and no
|
||||
ZoneDomes install is needed.
|
||||
|
||||
---
|
||||
|
||||
## 4. The live map's marker types (§4.5; D165)
|
||||
|
||||
- **The site filters (Q9).** The map meta already carries each monument's `kind` and `label`. The toggle is by
|
||||
**label**, because that is what staff read and it groups the variants (31 substations, four prefabs, one
|
||||
"Substation"). No wire change is needed.
|
||||
- **The default** is a module list of the minor labels, **off** unless turned on: Substation, Underground
|
||||
Cave, Train Tunnel, Water Well, Wild Swamp, Jungle Swamp, Ice Lake, Jungle Ruin, Fishing Village (not the
|
||||
large one), Large Barn and Abandoned Supermarket, as the rig's map labels them, plus the gas station, mining outpost
|
||||
and ranch under whatever labels the game gives them (read at build time; they are not on the rig's map). **Every label not on
|
||||
the list is on**, so a monument Facepunch adds next month appears rather than disappears.
|
||||
- **Where:** `Visibility.jsx`'s map section, under the *Monuments & world events* layer: a list of the labels
|
||||
on that server's current map with a switch each, a fleet default, and a per-server override. That is the
|
||||
same machinery the four layers already use (D114). The filter is applied where the map meta is served, so a
|
||||
hidden marker is not sent at all.
|
||||
- **The alternative** is the game's own `MonumentType` (§0.7), sent by the plugin. It is authoritative, but it
|
||||
is a wire change, and nothing yet shows that substations are a type of their own.
|
||||
|
||||
---
|
||||
|
||||
## 5. Chat titles: twenty-three conditions (§4.6; D172–D175)
|
||||
|
||||
### 5.1 What is counted, and where
|
||||
|
||||
Everything rides on `player.tally` (rule 2: aggregates, never a frame per event). The **gathering** conditions
|
||||
need **no plugin change**: `rust_gather_totals` already keeps every resource by shortname from all three
|
||||
sources (D159), so the site sums named lists.
|
||||
|
||||
| Condition (title) | Counted from | Tally field (new unless noted) | Site |
|
||||
|---|---|---|---|
|
||||
| Animal kills (Beastbane) | `OnEntityDeath`, victim `BaseNpc` | `animalKills` | column |
|
||||
| NPC kills (Exterminator) | `OnEntityDeath`, victim a human NPC | `npcKills` (exists, and keeps meaning *all* NPC kills) | `npc_kills − animal_kills` |
|
||||
| Players killed (Reaper) | `player.death` (exists) | — | `kills` |
|
||||
| Bow / Melee / Blade / Revolver kills (Fletcher, Brawler, Duelist, Outlaw) | every credited kill's weapon shortname, same source as the killfeed | `weaponKills: { shortname: n }` | new table, classified by lists (§5.3) |
|
||||
| Headshots (Crownbreaker) | the killing blow's `HitInfo.isHeadshot`, any victim | `headshots` | column |
|
||||
| PvP / PvE kill distance (Longshot, Marksman) | the killfeed's distance | `bestPvpM`, `bestPveM` (this interval's maximum) | *best* columns, `GREATEST` at ingest |
|
||||
| APC / helicopter kills (Tankbreaker, Rotorbane) | `OnEntityDeath`, victim `BradleyAPC`, `PatrolHelicopter`, `CH47Helicopter` | `apcKills`, `heliKills` | columns (§5.4) |
|
||||
| Wood / ore / plants (Timberwright, Prospector, Greenhand) | exists (D159) | — | sums of named resource lists |
|
||||
| Clothes / weapons crafted (Seamster, Armorer) | `OnItemCraftFinished`, `item.info.category` | `crafted: { category: n }` | columns for attire and weapon |
|
||||
| Structures built (Mason) | `OnEntityBuilt`, a `BuildingBlock` | `built` | column (not `structures`, which is raids) |
|
||||
| Structures repaired (Tinkerer) | `OnStructureRepair` | `repaired` | column |
|
||||
| Players healed (Mender) | `OnHealingItemUse` on another player, `OnPlayerRevive` | `healed` | column |
|
||||
| Rockets fired (Artillerist) | `OnRocketLaunched` | `rockets` | column |
|
||||
| Explosives thrown (Demolitionist) | `OnExplosiveThrown`, `OnExplosiveDropped` | `explosives` | column |
|
||||
| Quests completed (Wayfarer) | `OnMissionSucceeded` (§0.4) | `missions` | column |
|
||||
|
||||
**Three of these hooks can veto** (`OnStructureRepair`, `OnHealingItemUse`, `OnPlayerRevive`), so they are
|
||||
declared `void` (PROTOCOL.md §8.7). All nine new hooks join `ExpectedHooks`, so a framework that stops raising
|
||||
one is visible on the servers page.
|
||||
|
||||
### 5.2 NPC kills, without changing a public number
|
||||
|
||||
Splitting `npcKills` in two would change what the public leaderboard's *NPC kills* column means, and it would
|
||||
change it mid-wipe. So `npcKills` keeps counting every NPC and animal. `animalKills` counts the animals again
|
||||
separately, and the human-NPC count is the difference. Nothing already on a page moves.
|
||||
|
||||
### 5.3 The weapon lists live on the site
|
||||
|
||||
D174 wants the weapon lists "in one place, so a Rust update adds a weapon by editing a list". The plugin sends
|
||||
every credited kill by **weapon shortname**, and the site keeps `rust_weapon_kills (server, wipe, steam,
|
||||
weapon, n)`, shaped like `rust_gather_totals`. The bow, melee, blade and revolver lists are one module file,
|
||||
**applied when the standings are read**. Editing a list corrects the whole wipe, and adding a Rust weapon needs
|
||||
no plugin release. Melee is a list too, because the site cannot ask `BaseMelee`.
|
||||
|
||||
### 5.4 Who killed the helicopter
|
||||
|
||||
D174: the killing blow. A patrol helicopter usually dies by crashing, and its death `HitInfo` may carry no
|
||||
player. The build walk shoots one down on the rig. If the killing blow has no player, the credit goes to **the
|
||||
last player who damaged it within 60 s**, tracked only for those three vehicle types. That is a stated default,
|
||||
and the walk decides whether it is needed.
|
||||
|
||||
### 5.5 Titles an admin can rename (D175)
|
||||
|
||||
- `rust_title_categories (stat PK, text, updated_by, updated_at)` holds the admin's title for a category.
|
||||
The shipped defaults (§4.6's table) stay in the module.
|
||||
- `rust_title_rules.text` may be **empty, meaning "use the category's title"**. That is what makes a rename
|
||||
reach existing rules. D175 also says "new rules open with the category's current title filled in". If it
|
||||
were filled in as the rule's own text, the rename would never reach that rule. So the form shows the current
|
||||
category title **as a placeholder** and saves nothing unless the admin types.
|
||||
- `stat` takes the 23 category ids (they fit `VARCHAR(16)`). The standings query for a *best* stat is a `MAX`.
|
||||
- The titles screen gains a "Category titles" section: 23 rows, each with the default, the admin's text, and
|
||||
Reset.
|
||||
- **The new stats feed titles only.** The public leaderboard keeps its columns in this step. Adding any of them
|
||||
there is a later, separate choice.
|
||||
|
||||
### 5.6 Missions (D173)
|
||||
|
||||
`OnMissionSucceeded` increments `missions` for the assignee. A failed or abandoned mission is never counted.
|
||||
The hook's exact arguments are confirmed the first time a person finishes a mission on the rig, which is also
|
||||
D173's pass condition: a mission taken from an NPC and finished moves Wayfarer by exactly one, and an abandoned
|
||||
one does not move it. Should the hook disappear on a framework, D173's option 2 is known to work: the player's
|
||||
`acceptedMissions` carries each instance's `status` (`Completed`, `Failed`, …, §0.4).
|
||||
|
||||
### 5.7 The walk
|
||||
|
||||
Every condition earned once on each rig (PLAN_FIXES §8): kill an animal and a scientist with a bow, a machete
|
||||
and a revolver, one of them by headshot; shoot a player from range (this needs the second player); craft a
|
||||
shirt and a weapon; build and repair a wall; bandage and revive another player; fire a rocket; throw a grenade;
|
||||
destroy an APC; shoot down a helicopter; finish a mission and abandon one. Each count is checked in the tally
|
||||
frame and on the titles screen.
|
||||
|
||||
---
|
||||
|
||||
## 6. NPCs (§4.7)
|
||||
|
||||
**Recommendation (Q10): extend `rust.npc.place` rather than depend on an NPC plugin.** HumanNPC needs two more
|
||||
plugins and replaces Rust's AI. NPC Loadouts cannot dress one spawn differently from the next (§0.10). The
|
||||
bridge already spawns Rust's own scientists, and they are players underneath:
|
||||
|
||||
- **`kit`**: a Kits kit. After spawning, the bridge empties the NPC's inventory and gives it the kit through
|
||||
Kits' API (`GiveKit`). Kits is already a required plugin (PLAN.md R6).
|
||||
- **`name`**: the NPC's `displayName`, which already flows into the killfeed (`attackerName`) and on to F2's
|
||||
labels.
|
||||
|
||||
**A spike decides it before any site work:** on each rig, does a scientist given a kit fight with the kit's
|
||||
weapon, or fall back to its prefab loadout? And does the name reach the killfeed? If the AI ignores the kit,
|
||||
this section comes back to the org lead with the HumanNPC route costed.
|
||||
|
||||
---
|
||||
|
||||
## 7. Protocol 13, what this adds
|
||||
|
||||
All of these are additions to the unreleased protocol 13. PROTOCOL.md §19 gains a section for each.
|
||||
|
||||
| Change | Direction | § |
|
||||
|---|---|---|
|
||||
| `perm.inventory` / its paged reply (owners, groups with parents, users, leased) | site → game | 1.2 |
|
||||
| `perm.sync`: groups carry `parent`; `managed` and the foreign report removed | site → game | 1.2 |
|
||||
| `kits.uses` / its reply | site → game | 2.6 |
|
||||
| hello: `zoneManager: { version, flags[] }` (from the helper) | game → site | 3.1 |
|
||||
| `world.zone`: `flags`, `settings`, `dome: { type, stack }`; reason `bad-option` | site → game | 3.1, 3.2 |
|
||||
| `world.place` for an NPC (the verb behind `rust.npc.place`): `kit`, `name` | site → game | 6 |
|
||||
| `player.tally`: `animalKills`, `weaponKills`, `headshots`, `bestPvpM`, `bestPveM`, `apcKills`, `heliKills`, `crafted`, `built`, `repaired`, `healed`, `rockets`, `explosives`, `missions` | game → site | 5.1 |
|
||||
|
||||
## 8. MODULE_API 1.12.0 (core, website `main`)
|
||||
|
||||
Additions only, so Module-uo, declared `^1.10.0`, is unaffected, and that is proven the way 1.11.0 was.
|
||||
|
||||
- `registerEventTemplates` (§2.5).
|
||||
- A param's optional `duration: 'minutes'` (§2.2).
|
||||
- A lease's optional value `example` (§2.1).
|
||||
- An option row's optional `note` (§2.6).
|
||||
|
||||
## 9. Order of building
|
||||
|
||||
Each item is built on `edge`, walked on both rigs, and PR'd with its spec, like steps 1 and 2:
|
||||
|
||||
1. **The permission manager** (§1). It is the largest, and §3's exemptions point at it.
|
||||
2. **Chat titles** (§5), including the mission hook.
|
||||
3. **Zones and the dome** (§3).
|
||||
4. **The map's marker types** (§4).
|
||||
5. **NPCs** (§6), spike first.
|
||||
6. **The step editor and the kit weekend** (§2). It is core's, so it can be built alongside any of the above.
|
||||
It goes out as its own website PR with the Module-uo proof.
|
||||
|
||||
Then PLAN_FIXES §8's re-walk on Oxide and Carbon, and the cutover.
|
||||
|
||||
## 10. Questions for the org lead
|
||||
|
||||
Each has a recommendation. The answers become D188 onward.
|
||||
|
||||
1. **Q1: import rows for a linked player.** *Recommended:* the import writes Steam-keyed rows even when the
|
||||
Steam id is linked, so it copies the game exactly; a toggle made on the site for a linked player is a user
|
||||
grant, reaching all their accounts (D28). *Alternative:* the import writes user rows for linked players,
|
||||
which widens a holding to the person's other Steam accounts.
|
||||
2. **Q2: groups across servers.** *Recommended:* one name fleet-wide, with what it carries and who is in it
|
||||
scoped per server (§1.4). *Alternative:* groups become per-server, which would give `vip` on each server
|
||||
its own row and its own screen.
|
||||
3. **Q3: an in-game removal of a fleet-wide row.** *Recommended:* held for a person under every policy, with
|
||||
"remove everywhere / keep everywhere except here / put it back" (§1.6). *Alternative:* auto-adopt narrows
|
||||
it to "every server except this one" by itself.
|
||||
4. **Q4: `zonemanager.ignoreflag.*` on the screen.** *Recommended:* one collapsed "Exempt from zone flags"
|
||||
section under ZoneManager, labelled by flag name (§1.7).
|
||||
5. **Q5: templates.** *Recommended:* a core `registerEventTemplates` (MODULE_API 1.12.0) that any module can
|
||||
use, with the kit weekend as the first (§2.5). *Alternative:* a Rust-only "Kit weekend" button on the Rust
|
||||
admin pages that creates the draft.
|
||||
6. **Q6: popups beyond announcements.** *Recommended:* not in this step; `rust.announce` gets the Chat/Popup
|
||||
control. *Alternative:* the bridge sends a zone's enter message and "reward earned" as popups when
|
||||
PopupNotifications is present.
|
||||
7. **Q7: the dome.** *Recommended:* the bridge draws it itself, which reverses D167's dependency on ZoneDomes
|
||||
and keeps its intent (§3.2). *Alternative:* keep ZoneDomes and patch its message call from the helper.
|
||||
8. **Q8: zone presets.** *Proposed:* Arena, Safe zone, No building (§3.1). Add, drop or rename.
|
||||
9. **Q9: who filters the map markers.** *Recommended:* the site, by label, with a default-off list of minor
|
||||
labels (§4). *Alternative:* the plugin sends Rust's `MonumentType` and the default follows it.
|
||||
10. **Q10: NPCs.** *Recommended:* extend `rust.npc.place` with a kit and a name, after a spike (§6).
|
||||
*Alternative:* depend on HumanNPC, which brings Pathfinding and Waypoints.
|
||||
11. **Q11: the first import on an existing install.** *Recommended (D160 as written):* the first inventory
|
||||
after the upgrade adopts everything present, including hand edits that are waiting for an answer today
|
||||
(§1.5). *Alternative:* existing drift rows stay on the "needs a person" list and the import skips them.
|
||||
@@ -24,7 +24,8 @@ differ.
|
||||
for actually building `module-rust`. Everything else in this directory is copied from uMod; those two
|
||||
are written by this project and are where the phases, the settled decisions and the local test rig are
|
||||
recorded. [`PLAN_FIXES.md`](PLAN_FIXES.md) is its companion: the first player walk's fixes and decisions
|
||||
D159–D175, all to land before the Module-Rust cutover.
|
||||
D159–D187, all to land before the Module-Rust cutover. [`PLAN_REDESIGNS.md`](PLAN_REDESIGNS.md) plans that
|
||||
document's six redesigns (its §6 step 3) in detail.
|
||||
|
||||
> **This is a mirror, not a specification we own.** uMod is upstream and wins any disagreement; the
|
||||
> point of copying it is availability and grep-ability, not authority. Nothing here may be cited as a
|
||||
|
||||
Reference in New Issue
Block a user