Commit Graph

8 Commits

Author SHA1 Message Date
43bf73bae5 docs(modules): R15 the optional tier with BetterChat, R16 rewards grant the right to redeem
R16 changes a design rather than adding to it. The reward action no longer calls
GiveKit; it grants the permission that GATES a kit, and the player redeems it
themselves in game. Kits already has exactly this model built in - every kit
carries a RequiredPermission, GiveKit's own path checks it, the in-game menu
renders an ungated kit as locked rather than hiding it, and GetKitInfo returns
the permission so the module can read which kits are gated.

What that removes is most of the hard part.

The offline-grant problem disappears. GiveKit needed a connected BasePlayer, so
an event firing at 2am rewarded only whoever was online; an entitlement waits.
That CLOSES the open question section 3 carried - no pending-grant queue, no
second at-most-once store.

It is the same machinery as R2 rather than a second mechanism: a reward becomes
a permission grant authored by the site and mirrored into Oxide, which phase 7
already builds. One permission authority, one drift story, one audit trail.

reversible: 'ledger' becomes honest where a direct grant could only ever be
'none'. revert revokes the permission, removing one that is not there is a
success, and it is idempotent by construction. A player who redeemed before the
revert keeps the items, and that is correct: the ledgered resource is the GRANT,
so reverting withdraws the entitlement rather than the consumption.

cost() counts grants and is exactly knowable before dispatch, which removes the
whole declare-the-maximum-because-you-cannot-know class of problem from chapter
5 section 4. And the idempotency key largely stops mattering: chapter 5 draws
the line itself - a key is for a write whose repetition would be a second
EFFECT, and a permission grant is a SET.

The earlier section arguing a kit grant can only be reversible: none is
rewritten rather than deleted, with the correction stated: it was right about a
direct grant, and the reusable part is that the action had been declared around
the wrong noun. What the event makes is not loot, it is an entitlement.

One design note carried into the option source: a kit with an EMPTY
RequiredPermission is open to everybody, so granting a permission for it rewards
nobody. The dropdown must surface which kits are gated and refuse or warn on one
that is not.

R15 opens an optional-integration tier, with BetterChat (LaserHydra, 5.2.15,
MIT, Universal) as the first member, for leaderboard-earned chat titles. Its
integration point is a PULL - API_RegisterThirdPartyTitle registers a callback
BetterChat invokes per player - so a title is a pure function of state we
already hold, with nothing written into it and no drift to reconcile. The trap
is chapter 4's rule applied to somebody else's callback: that getter runs
synchronously on the chat path and must be a cheap in-memory lookup, never a
socket call. Its API_AddGroup and API_SetGroupField pair naturally with R2's
site-authored groups, but that direction is a push and would need R2's drift
posture, so it is a phase 17 decision rather than a given.

Section 3 is now empty. Clans-in-the-base-set was confirmed complementary, and
the offline-grant question was dissolved rather than answered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-15 12:11:11 -05:00
5dcb8088ad docs(modules): R7-R14 close the audit, and the phase list roughly doubles
Every question the contract audit raised was answered the same day. Eight new
decisions of record, and the schedule goes from twelve phases to twenty.

R7 the notifications and engagement set ships in v1 - streams, triggers,
audiences, seeds, announce leg, post hook - as ONE phase because they are a
matched set in the template rather than three independent gaps. The three rules
it lives on: ceiling is required and is not a ladder (a staff ceiling does not
permit owner, because one person for a cheat-detection event is the player it
was detected on); subjectKey must name a declared variable or every subject
shares undefined; and a rule group is offered once, so a rule appended to an
existing group reaches fresh installs only.

R8 multi-server from the start - the dry run's finding 2 taken at face value.
R12 per-wipe detail plus all-time rollups, with the truncation as a runtime
route and never a schema statement, since the fragment replays every boot.
R14 /rust on all three tiers, chosen deliberately because prefixes share one
namespace with core's and the collision probe cannot see core's root-mounted
endpoints. R13 two extension slots. R11 a small read-only slash command set,
with ephemerality fixed at the deferral so a refusal must defer ephemeral.

R9 is the one that split in two. The map IMAGE is static content on the game
host regenerated only on a wipe - the ch.3 2b case, so request/reply, one in
flight, two stages, its own derivation version, and no import on boot - while
everything moving on it is live state down the ordinary read path. Every layer
is an operator switch, and that is a security boundary rather than a preference:
public player positions in Rust locate players and let anyone infer base
positions. Default is monuments and world events public, players and bases
admin-only. This is the ONLY asset-bridge work in scope; item icons and the
2,590 skin ids stay out of v1.

R10 the Android app is in this workstream, deciding its screens from
/api/v1/public/modules capabilities, with each leg trailing the website surface
it consumes by one phase. Records the two endpoints that are not this and are
easy to confuse with it: /api/v1/public/status is site mode plus a version
block, and /api/health is an internal liveness probe.

Section 7 now carries the phase each previously-unplanned element lands in, and
the honest cost: twelve phases, from reading the chapters that describe the game
bridge and treating the module as the thin part when the kit says in its first
paragraph that the module is most of the work.
2026-09-15 12:01:44 -05:00
a4544ba5f2 docs(modules): audit the plan against the whole contract, not just the game-facing chapters
The first draft of the phase list was built from chapters 3-5 and under-planned
the website module by a wide margin. The template registers eight of the ten
non-event registrations; the plan covered three.

Unplanned and now recorded: registerExtension, registerNotificationStreams,
registerEventTriggers, registerAudiences, registerEngagementSeeds,
registerAnnounceLeg, registerPostHook, registerSlashCommands - with triggers,
audiences and seeds being a matched set rather than three independent gaps.
Also unplanned: purge.sql, mount prefix choice, the swagger fragment and its
staleness check, the vite alias/shim mechanism the kit calls the highest-risk
mechanical detail in the system, checkImports, and the entire packaging and
release path - there was no packaging phase at all.

On the client: declareModuleSlot, which the kit says is needed the moment a game
has anything like a guild, so it belongs in the Teams phase; registerFeatureProvider;
and the UI kit discipline that has caught this project twice already (PublicLayout
needs shell, PageHeader silently drops unknown props).

Beyond the module: the asset bridge, which Rust's item icons and 2,590 skin ids
fit exactly; the .profile landing page, owed whenever repos are added and three
just were; runicgateway.com, for which a second game is a headline change; and
the Android app, which feature-detects capabilities and must render a site whose
module it has never heard of.

Section 8 carries the eight questions the plan cannot answer for itself.
2026-09-15 11:45:04 -05:00
839e5d05a8 docs(modules): R6 names the base mod set, and reading Clans settles R5 the other way
The three repos exist and are named: Module-Rust, Rust-Link, Rust-Plugins. Notes
that a repository name is not a module id - Module-uo ships id `uo`, this ships
`rust`, and 2.1 requires id to equal the directory core loads it from.

R6 - the required base set is Kits 4.4.9, Clans 0.2.10 and PopupNotifications
0.2.1, all k1lly0u, all MIT, each with a direct .cs download, so phase 0's
install step is three curls rather than manual retrieval. Clans is listed as a
Universal plugin, written against Covalence, which is why its API takes IPlayer
rather than BasePlayer.

Kits is a good dependency: 23 HookMethods, including GiveKit for the reward
action, GetKitNames/GetAllKits for the authoring form's OPTION SOURCE so an
operator picks a kit from the live server instead of typing an identifier, and
per-player uses and cooldown for eligibility. It also raises OnKitRedeemed,
which the bridge can report as an ordinary event whoever triggered it.

Two traps in GiveKit, both found by reading it rather than reasoning about it.
It returns null on a failure path - `if (!player) return null` - and null is
Oxide's idiom for "no opinion", so the reflex of treating null as fine reports a
reward as delivered when there was no player to deliver it to. Success is the
literal true and a refusal is a message string, which drops straight into
chapter 5's `error`. And GiveKit takes a BasePlayer, so there is no offline
grant: an event rewarding participants at 2am rewards only whoever is online.
Phase 8 chooses between accepting online-only and keeping a persisted
pending-grant queue, which is a second at-most-once store and is not free.

R5 is settled far more firmly, and the other way round from how it was argued.
For Teams the Clans plugin is WORSE than first-party, not richer: it publishes
fifteen HookMethods and every one is a mutation, with no read API at all - no
GetClan, no GetClanMembers, no GetAllClans - and it raises exactly three hooks,
none of them a membership transition. It cannot answer any of core's three
provider questions from its published surface, while first-party clans answer
all three. So first-party is PERMANENT for the provider, not a first step. What
the plugin actually adds is alliances and clan/alliance chat - richer in
features, not in roster data - which is what phase 9 surfaces beside the
provider rather than under it. Reading its own data files is recorded as
deliberately not taken: that is another plugin's private storage, not an API.

Two questions left open in 3, neither blocking: whether Clans being in the base
set while the provider reads first-party is the intended reading (an
interpretation, not something stated), and the offline-grant choice, deferred to
phase 8 on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-15 11:40:10 -05:00
7d589ff5f2 docs(modules): R4 the installer grows a game dimension, R5 Teams come from first-party clans
R4 - Rust reaches an operator through the shipped installer behind
--game servuo|rust, not a second binary and not a shared-core refactor. The
shape of the work is set by where the coupling already is: service.rs and
main.rs mention ServUO zero times, while install.rs, doctor.rs, overlay.rs and
tier.rs carry nearly all of it, plus the bundle manifest where OverlayComponent
and ServUoCompat name the game in the schema itself. So the game payload becomes
a variant and ServUoCompat a per-game compat block - a schema change on the
published bundles branch, which is the part to design before touching code. The
Rust payload is much simpler than the UO one (no source tree, no patches, no
patch tier - one .cs into oxide/plugins) and what it gains instead is an Oxide
prerequisite check, which is doctor's shape rather than a new concept.

R5 - Teams come from Rust's FIRST-PARTY clans; the uMod Clans plugin is the
richer optional tier in phase 9. Three distinctions that are easy to collapse
and expensive to get wrong: the seven first-party clan hooks are all "no return
behavior", which is exactly what a read-only bridge wants; Rust's first-party
TEAMS are a different system entirely (twelve mostly-vetoable hooks, the
transient squad rather than the persistent organisation) and are not what core's
Team provider should be fed; and the uMod Clans plugin's API is not in our
mirror at all, since HOOKS.md is the game's 477 hooks and a plugin's API is its
own documentation.

The gap R5 has to design around: the seven hooks carry created, disbanded,
added, kicked and left, but no promote or leader-changed event. So
getTeamLeaders reads leadership off LocalClan at snapshot time, which makes
phase 6 partly snapshot-driven where the dry run predicted fully event-driven.
Recorded as a correction to that document rather than silently.

Also records the start.bat fix on the rig: it now updates D:\rust rather than
C:\rustserver, the original is kept at start.bat.bak, and the appmanifest
already present at the same buildid means the first corrected run is a delta
rather than a 5.9 GB re-download.

Phases are now 0-11; the installer is phase 10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-15 11:32:43 -05:00
a5881d5a55 docs(modules): the module-rust plan, its decisions of record and its rig
The dry run designed this module on paper and deliberately did not build it.
This is the document that builds it: eleven phases, three new repos, and the
three decisions the org lead settled on 2026-09-15.

R1 identity is an in-game link code for v1. There is still no
registerAuthProvider at MODULE_API_VERSION 1.10.0, so "Sign in with Steam" is
not reachable from a module. What changed since the dry run is the stakes, not
the options: the dry run rated this survivable because the module only read,
and R2/R3 make the site the author of who may do what and the thing that hands
out loot. A weak link is now a privilege-escalation path.

R2 site-authored permissions mirror into Oxide's own permission store, so every
third-party plugin honours them with no adapter and a wipe stops being a
data-loss event for permissions. This is a direction the Integration Kit has no
chapter for - not the read path, not a ledgered one-shot, but continuously
reconciled state where the website is authoritative. Its nearest relative is the
Team provider inverted. Whether that deserves a sixth chapter is phase 10's
question.

R3 the Kits reward action always registers and refuses with a reason in `error`,
rather than vanishing from the form or refusing to boot. Phase 8 must declare
reversible: 'none' honestly - there is no way to un-grant loot a player has
spent - and count cost() per kit actually granted.

The rig is D:\rust, which has been booted and carries a matched Oxide 2.0.7585.
Two traps recorded: its start.bat updates C:\rustserver and launches D:\rust, so
the server that boots has never been updated by its own script; and
C:\oxide_files is a 2025-04-23 Oxide whose bundled Assembly-CSharp.dll would
downgrade a real install.

One question left open: which plugins besides Kits are in the required base set.
oxide/plugins/ is empty, so all of it is a fresh install either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-15 11:12:20 -05:00
4e2c2dc439 docs(modules): mirror the rest of the Oxide ecosystem, and add a machine-readable set
Completes the uMod mirror beyond the Rust hook table, and adds agent/ — the
same facts in TSV/JSONL at ~46% of the tokens.

New prose:
- OXIDE_API.md: the 19 developer pages under umod.org/documentation/api/ —
  plugin structure, hooks, commands, IPlayer, permissions, config, data files,
  database, localization, timers, web requests, dependencies, integration,
  preprocessor directives, security, style guide, CI, review. This is the
  framework our plugin is a guest in, where HOOKS.md is what the game says.
- OPERATING.md: the 6 operator pages — installing Oxide on a server, then
  installing, configuring and permissioning plugins.

New machine-readable set (agent/):
- hooks.tsv    477 rows, ~31% of HOOKS.md
- items.tsv    678 rows, ~80% of DEFINITIONS.md's item table
- skins.tsv    104 rows covering 2,590 skins, ~77%
- api.jsonl    150 code examples, ~47% of the two prose docs
Generated in the same pass as the markdown, so the two cannot drift.

HOOKS.md gains the universal-hook split: 34 of the 477 are uMod's own
Covalence hooks, raised identically on every game uMod supports. Verified
against /documentation/games/universal in the same capture - all 34 are in
the Rust set and the Rust page adds none of its own, so the overlap is exact.
The distinction is architectural: a universal hook is the portable part of
the surface.

Two things worth recording from building it:
- The first skins.tsv was one row per skin and came out 11% LARGER than the
  markdown it replaces. Grouping it one row per item is what made it a
  saving. The token win is real for prose (3.2x on hooks) and small for
  tables that were already dense - agent/README.md says so plainly rather
  than claiming a flat number.
- Signature extraction by brace depth silently captured body lines (a nested
  '}' in OnUserConnected's example ended the block early). It now matches on
  the hook's own name; all 477 rows verified to carry a real signature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-15 10:01:11 -05:00
8d31e497d2 docs(modules): add the Rust Oxide/uMod API reference for module-rust
Mirrors uMod's Rust game API into docs/modules/rust/ so the upcoming
module-rust can be designed against it without a round trip upstream.

- HOOKS.md: all 477 hooks in 20 categories, scraped verbatim from
  umod.org/documentation/games/rust. Each entry keeps its description,
  its return contract, its tags and every C# overload uMod publishes.
  Opens with a category table and a full alphabetical index
  (name -> category -> return contract).
- DEFINITIONS.md: 678 items (short name, item id, display name) and
  2,590 workshop skin ids across 104 items.
- README.md: why the reference exists, how an Oxide hook actually binds
  (by name and arity, by reflection - a typo is silently never called),
  the four return contracts, and how the bridge invariants restate
  against Oxide. Records provenance and the exact refresh procedure:
  the hooks JSON endpoint is not Cloudflare-gated even though the HTML
  page is, and the definitions page has to come out of a browser.

The mirror is reference only - uMod stays upstream and normative, and
nothing here is a Runic Gateway contract. Rust wipes monthly, so both
files carry their capture date (2026-09-15) and a re-capture note.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-15 09:38:43 -05:00