Commit Graph

43 Commits

Author SHA1 Message Date
1444c77413 docs(link): the shard-name fallback, the atlas places shape, and two traps
Records what the live Protocol 3.0 smoke test (ServUO + sidecar + website +
AVD) turned up, so none of it has to be rediscovered.

v3.md §5.3 — the ruleset `shard` field now falls back to the instance's own
name when the shard publishes ServUO's stock "My Shard", why that is done at
ingest rather than on read (the frame is also broadcast live), and why the
backfill snapshot must go through the dispatcher instead of writing state
directly: a direct call made it a second writer that skipped the
normalization.

v3.md §7.4 — an unscored board renders a placeholder row rather than a blank
card, and why it is deliberately not shaped like a real entry.

PLAN.md §9 M11 — `places` is a list of {facet,label,spawners,maxAlive} OBJECTS,
not of place-name strings, and typing it `List<String>` makes the whole detail
route fail to decode while the request itself returns 200. Adds the rule that
came out of it: decode tests must feed real captured JSON, because the fakes
build DTOs in Kotlin and can never catch a wire mismatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP
2026-08-01 00:58:53 -05:00
5a091157d6 docs(android): scope M11 — Protocol 3.0 shard parity for the app
The v3 work added four shard features and an admin-configurable visibility
framework the Android client knows nothing about. v3.md §10 deferred the app
side as a follow-up; re-examining it before the cutover found the gap is wider
than nav hiding:

  - no consumer for any of ruleset / leaderboards / market / atlas,
  - no `points` block on the character sheet (§7.3),
  - no cliloc-resolved item names (§8.6), and
  - shard nav gated on session role alone, so an admin who disables a feature
    or raises its audience leaves the app rendering entries that 404/403 into a
    generic error where the web client hides them.

Scoped as PLAN.md §9 M11 in two PRs (the visibility rules + read-model adds,
then the four screens), with the traps a real shard exposes recorded inline:
uncapped `maxPoints: 0`, cliloc-named boards with a null `nameString`, skill
caps in tenths, the required market staleness banner, the market stream being
off by default, atlas delays in seconds, and `points`-count vs `spawners`-list.

edge → main is held until both land so web and app surface the same shard on
the same day. Neither PR is coupled to the merge order — on a pre-v3 website
every new route and /public/shard/features 404s and the app falls back to
today's behavior — so holding the cutover is a schedule decision, not a
technical dependency.

Also records two things verified as already correct, so they are not
re-derived: the app's SSE request rides the authenticated client (same audience
rung as the same account on web), and every shard DTO is nullable-with-defaults
(field projection cannot cause a decode failure).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-30 00:46:16 -05:00
32def88c4e docs(link): fill in the cutover PR numbers
The order-6 row was written before the seven PRs existed. Same follow-up as the
cliloc row got.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-29 18:09:50 -05:00
71207cef16 docs(link): the Protocol 3.0 cutover (v3.md order 6)
INTEGRATION.md was written for the window that just closed -- it told integrators
the version had NOT been bumped yet and that a sidecar on `edge` reports 2 while
already carrying v3 kinds. That guidance is now wrong in the direction that
matters, so the version section states 3 (header, /health, ws.hello, the 409
example and the §8 worked example) and replaces the "until then" paragraph with
what a v2 integration actually has to do to upgrade: change the constant it
sends, and nothing else, because nothing that existed in v2 changed shape.

v3.md gains §4.1 for what the bump touches and, more importantly, WHY the
website's boot migration is gated on a marker row: schema.sql is re-run on every
boot and uo_link_config.protocol is admin-editable, so an ungated UPDATE would
silently un-pin an operator running an older sidecar. That is the one piece of
the cutover a reader could not infer from the code being one constant.

Progress tables: 5b done, 6 in review.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-29 18:04:00 -05:00
6ce60a82c3 docs(link): the player-vendor marketplace (Protocol 3.0 §8)
Documents order 5b across the four repos, and records what building it changed
about §8 as designed.

- NEW website/MARKETPLACE.md — the operator guide: what the pages must say out
  loud and why, the privacy contract (the player's in-game Vendor Search toggle
  wins, and no admin setting overrides it), the Bridge.cfg knobs and how they
  trade against each other, and the measured sweep costs.
- INTEGRATION.md — catalog entry for vendor.listing / vendor.listing.remove with
  its six consumer gotchas, and the GET /market REST section (the sidecar's only
  paged read, and why it orders by serial rather than shop name).
- BACKEND_DESIGN.md — shard_vendors / shard_vendor_items, the routes, and the
  marketplace search as the only rate-limited public read.
- SHARD_VISIBILITY.md — why the market's fields default to Everyone (the in-game
  gump already shows exactly that set), why location is one setting covering
  four things, and why hiding the owner name without the owner id achieves
  nothing.
- PLAN.md — the amortized round-robin as the one sweep pattern the bridge did not
  previously have, and an update to §7's cliloc note: pushing name resolution to
  the plugin was never an option, because ServUO cannot read a modern client's
  compressed cliloc files either.
- v3.md §8.8 — the four things the build settled differently, chief among them
  that §8.1's FLAT location payload would have made Part A's pre-wired
  market.location rule inert, exactly like the characterName miss one part
  earlier.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-29 09:52:14 -05:00
ee0c146d7a docs(link): cliloc overlays for shard-added and shard-edited items
Shards edit items and add new ones, carrying cliloc ids no stock client table
has. The cliloc table now reads a SET of sources rather than one file — a base
plus every operator-maintained overlay under `custom/`, hash-gated together and
re-read on every boot, exactly as §6 reads the ServUO tree. Matching docs for
website #115.

website/CLILOCS.md gains a "Shard-added and shard-edited items" section: the
directory layout, merge precedence, the per-source breakdown an operator uses
to confirm an overlay took effect, and why `custom/` is a convention we chose
rather than one discovered from the shard — ServUO has no server-side notion of
a custom cliloc, they live in the patched client a shard distributes and
nothing in the tree declares them. Scale recorded: the live shard's script tree
references 16,434 cliloc ids and only 37 are absent from stock, which is why
this is an overlay and not a second table.

"Why there is no staged-approval flow" is replaced by "Two ways a refresh is
refused", because the set brings back the hazard a single file did not have. A
corrupt source fails the parse loudly; a source that has VANISHED parses
perfectly and imports a table quietly missing everything it contributed. That
is the same ambiguity §6 stages a facet removal for, so it is staged here too
(`needsReview`, `{approve:true}` to accept) — as a flag rather than §6's
approve/reject pair, because the atlas stores a pending decision SO THAT
approving re-parses, and here nothing is stored.

Two more traps recorded in §8.6 (now five), both found by running a shard-style
overlay rather than another stock-table fixture:

- Tidying punctuation unconditionally corrupts real names — a custom
  "Runic Gateway Sigil (v2)" rendered as "(v2". Stripping leftover brackets is
  right after a placeholder is removed and wrong otherwise, the same condition
  the `%` rule already had.
- Source labels must be forward-slashed and root-relative or the same directory
  fingerprints differently on Windows and Linux and every boot looks like a
  change — the identical bug §6 records.

BACKEND_DESIGN.md: the source set, the two refusals, and the updated admin
route notes (`approve`, `missingSources`, overlays found beside a file path).

Documentation only. Every claim was written after the behaviour was observed:
the counts come from the live shard's script tree, and the refusal/approve flow
and per-source breakdown are copied from real runs in website #115.

PROJECT_TREE.md files are deliberately untouched — CI-generated by the
sync-project-tree workflow.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-29 06:46:43 -05:00
e3aabf9e3e docs(link): fill in the cliloc PR numbers, correct the cliloc file count
The progress table and CLILOCS.md carried #TBD placeholders until the PRs
existed; they now point at website #115 and docs #70.

Also corrects "all four Cliloc.* files" to eight (chs, cht, deu, enu, esp,
fra, jpn, kor) in both v3.md §8.6 and CLILOCS.md — the compression marker was
checked against every one of them, and understating the sample weakens the
claim it supports.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-29 04:25:06 -05:00
be9f5019fa docs(link): the cliloc table, and why §8.6's recommendation was not implementable
Protocol 3.0 §8.6 resolved as its own website-only change, landing ahead of
§8 so the marketplace ships with real item names. Matching documentation for
website #TBD.

NEW website/CLILOCS.md — operator-facing: why the conversion step exists, how
to convert, how to configure the path, the refresh contract, what gets stored
and how names are applied.

link/v3.md §8.6 rewritten. Two things in the original recommendation turned
out to be wrong, and both are recorded because the reasoning generalises:

1. The committed db/data/clilocs.json artifact predates the Part C
   corrections (§6.1) and violates both — no committed snapshot of derived
   content, and nothing EA-derived ever shipped. UO's strings are EA's,
   exactly as the creature sprites are.
2. "scripts/buildClilocs.js reads the UO client's Cliloc.enu" is not
   possible. EVERY current client ships its cliloc files compressed (first
   DWORD's high byte 0x8E, the Mythic container); the plain layout is what
   those files looked like before that change, and parsing one as the other
   does not fail cleanly — it yields ~19k records with negative ids, 1,722
   distinct keys out of 19,508, one 62 KB "string", and a truncation
   somewhere in the middle. ServUO's own Ultima.StringList cannot read it
   either, so VendorSearch.GetItemName is already inert on such a shard and
   the work could not be pushed to the plugin.

That second point also retires an open question in §8.2: the warning never to
call GetItemName in the market sweep costs us nothing we could otherwise have
had, because the in-game Vendor Search gump has the same gap.

Three traps found by building it are recorded: StringList.SaveStringList
RE-COMPRESSES on save (its output is byte-identical to its compressed input,
because its purpose is round-tripping a file back into the client); trimming a
text line before splitting silently drops the ~half of a table that is empty
strings; and Number('') is 0, not NaN.

Also updated:
- Progress and §9 sequencing tables: order 5 split into 5a (this, website
  only) and 5b (the four-repo wire change).
- website/BACKEND_DESIGN.md — shard_clilocs / shard_cliloc_meta, the three
  admin routes, and why there is no staged-approval flow and no public route.
- link/INTEGRATION.md — the char.profile field note now says explicitly not to
  expect the shard to resolve clilocs, and points at CLILOCS.md.
- §10 documentation obligations list CLILOCS.md.

Documentation only. Every claim was written after the corresponding behaviour
was observed running: the compressed-format finding and the parse failures
come from the real client files on this machine, and the counts (123,490
parsed → 67,496 stored) and timings from importing them into the live MariaDB.

PROJECT_TREE.md files are deliberately untouched — they are CI-generated by
the sync-project-tree workflow and say so in their header.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-29 04:22:05 -05:00
8e857a9c8d docs(link): points.board, the leaderboards API, and what a real shard changed
Protocol 3.0 §7 lands across servuo-plugins, link and website; this is the
matching documentation.

INTEGRATION.md
  - points.board in the event catalog: one frame per system, never a delta, no
    points.remove (the shard's system set is fixed at startup). Four gotchas
    called out, all of them things a consumer gets wrong by default:
      1. maxPoints: 0 means UNCAPPED, not "zero allowed" — and on a real shard
         it is the COMMON case, so anything rendering points/maxPoints must
         special-case it or divide by zero on the happy path.
      2. nameString is usually null, with a cliloc in nameNumber — so
         humanising the system key is the primary display path, not a fallback.
      3. players counts players actually holding points, not table size: ten of
         the ~25 systems keep a zero-point row per character ever created, so
         the raw count would report the shard's whole census.
      4. Entries carry serial + name only, never acct/webId.
  - The char.profile `points` block, and why `rank` is absent by default.
  - GET /points and /points/:system, including why 404 (never published) and
    200-with-empty-top (published, nobody scored) are different answers.

v3.md
  - B/2 marked done in both the progress table and §9.
  - NEW §7.5, "what the run against a real shard changed" — the same record §6.1
    and §6.2 keep. Four corrections the plan could not have anticipated from
    reading PointsSystem.cs, the sharpest being that (long)double.MaxValue is an
    unchecked conversion yielding long.MinValue, which published
    "maxPoints": -9223372036854775808 on the first live sweep. Also records that
    GetEntry/GetPoints mutate the world on AutoAdd systems and so cannot be used
    in a read model, and the one deliberate deviation from §7.4: the visibility
    field rule must key on the wire's `name`, not the descriptive
    `characterName`, or it is silently inert.

BACKEND_DESIGN.md — shard_points_boards (including why the top-N list stays in
the payload and why listing orders by COALESCE(name, system)), plus the two new
public routes.

PLAN.md — 3.0 phasing brought current: the spawn atlas and points.board added to
what has shipped, and the Points* keys noted in the config-key paragraph.

PROJECT_TREE.md files are deliberately untouched — they are CI-generated and say
so.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-28 21:05:04 -05:00
be7e1a69ce docs(website): the spawn atlas API, the admin panel, and the delay-unit trap
Docs half of website #113 (Protocol 3.0 Part C, second website PR).

Carries the atlas rewrite that missed #67: that PR merged before the "derive
from the tree on every boot" commit was pushed, so `edge` currently describes
the build/import-artifact design that was rejected in review, not what shipped
in website #112. It lands here.

New in SPAWN_ATLAS.md: the six public routes and five admin ones, and three
behaviours that read as bugs unless they are written down — an unreadable tree
answers 200 with status "unavailable" rather than 500 (refresh reports outcomes
so boot is never blocked by a bad tree, and the contract is preserved at the
API), setting the ServUO path deliberately does not import, and `points` is a
count while `spawners` is the list.

Also the delay-unit trap: XmlSpawner stores MinDelay/MaxDelay in minutes OR
seconds per record, decided by that record's own DelayInSec flag, so a `5` is
five minutes on one spawner and five seconds on the next. Both are plausible
respawn times, which is what makes it silent. 170 of 6,455 stock spawners are
second-flagged. And PARSER_VERSION, which exists because hashing the tree alone
would strand an install whose maps never change on whatever an older parser
derived.

BACKEND_DESIGN.md gains the routes, the router-map entry, and the parser-version
rule. v3.md marks Part C done and records in 6.3 what the API half found.

api-route-inventory.json refreshed from the live manifest — it had drifted to
200 routes before this PR (real count was 204) and is now 215.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP
2026-07-28 19:51:50 -05:00
ff1c2064a5 docs(website): the atlas reads the shard's tree on every boot, not a snapshot
Follows the redesign in website #112. Two decisions from the original §6 were
rejected in review and replaced; the docs now describe what was actually built.

**The committed artifact is gone.** A shard's maps change over its life, so a
snapshot in the repo silently drifts from the world players actually see. The
ServUO tree is the single source of truth and the atlas is re-derived on every
server boot, hash-gated so an unchanged tree costs one read pass and no write.

**Nothing may name a facet.** The first implementation carried a lookup table of
the six stock UO facets. A shard may add facets, replace them outright, or rename
them when its maps are updated, and a built-in list mishandles all three
silently. Reconciliation is now by matching against the facet set discovered from
the shard's own data, with an unmatched name keeping its own rather than being
forced into a wrong bucket.

## Changes

- **`website/SPAWN_ATLAS.md`** rewritten: the two ideas that shape the design,
  how to configure the tree path, the boot flow as a decision tree, the
  approve/reject flow, and the code layout. The artwork policy is unchanged and
  still explicit — no art ever ships, operators extract their own from their own
  client files.
- **`link/v3.md` §6.1 (new)** records the two rejected decisions plus the two
  boot-path contracts. The old "what real data changed" list becomes §6.2. §6's
  now-superseded passages — the artifact bullet, the payload budget, the operator
  re-run story — are marked rather than deleted, so the reasoning stays legible.
- **`website/BACKEND_DESIGN.md`** documents `shard_atlas_pending` and the two
  contracts that make it safe: a facet removal is staged for a human, and the
  boot refresh can never block startup.

The two contracts are the part worth reviewing. Losing a facet is
indistinguishable at boot from a half-copied or mid-update tree, so it is staged
rather than applied; and no failure mode of the atlas — missing path, unreadable
mount, malformed file, database error — is allowed to stop the site coming up.

---

- [x] AI-assisted: written with **Claude Code** (Claude Opus 5), reviewed before opening.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP
2026-07-28 16:44:39 -05:00
3fb3f63f25 docs(website): record the spawn atlas pipeline and what real data changed
Protocol 3.0 order 3 (Part C), docs half of website #112. Part C is website-only
— no plugin, no sidecar, no new kinds, no wire change.

## New: website/SPAWN_ATLAS.md

The operator-facing reference: the build/import split and why it exists (build
needs a ServUO tree, import does not, and the container has the artifact but not
the tree), the re-run story, the artifact format, the placement transform, and
the three quirks in the source data that are silent when unhandled.

Also documents the artwork policy explicitly: **the project ships no creature
art and no extraction tooling.** Sprites live in the operator's own client
.mul/.uop files and are theirs, not ours to redistribute. `art` is nullable and
NULL on every fresh import; an operator who wants art extracts it themselves into
the gitignored uploads/atlas/ and maps slugs in a gitignored art map. Text-only
is the normal, supported state — not a degraded one.

## New: v3.md §6.1 — what the build against real data changed

Six corrections, kept as a diff rather than edited into §6 in place, because
each is a trap the next person would otherwise re-enter:

1. **Six facets, not thirteen.** Eodon.xml and the other named-area files carry
   TerMur/Trammel points; the facet comes from each record's `<Map>`.
2. **The XML dependency call resolved: hand-rolled, zero deps.** §6 left
   fast-xml-parser vs a tokenizer open.
3. **Facet names disagree between sources** — Locations says `Ter Mur`, `<Map>`
   says `TerMur`. Unreconciled the landmark fallback never fires there and every
   unregioned Ter Mur/Tokuno spawn silently reads "Wilderness".
4. **Spawn type tokens carry XmlSpawner directives** (`Fairy,{RND,4,8}`,
   `alchemist/z/-50`). Taken literally they invent creatures that do not exist
   and split real ones in two. 71 of 845 affected; 800 remain after stripping.
5. **The artifact is 1.41 MB, not "well under 1 MB"** — down from 4.40 MB via
   three encodings. Getting under 1 MB would mean dropping the spawner name.
6. **DELETE, not TRUNCATE** — TRUNCATE is DDL in MariaDB and implicitly commits,
   which would defeat the all-or-nothing reload the design asked for.

§6 also now records that Part C ships as two website PRs: the parsing half is
where the correctness risk lives and should not be reviewed inside a 10k-line
diff alongside routes and React.

## BACKEND_DESIGN.md

The seven atlas tables, the import-owned contract, the four column choices that
are traps (`spawn_range`/`grp` reserved words, DELETE vs TRUNCATE, explicit point
ids, plain INDEX not FULLTEXT), and the distinction between the configured
champion roster and the live champ.update feed.

PROJECT_TREE.md is left alone — it is auto-generated by the sync-project-tree
workflow.

---

- [x] AI-assisted: written with **Claude Code** (Claude Opus 5), reviewed before opening.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP
2026-07-28 16:13:30 -05:00
09467c67b0 docs(link): link the world.ruleset PRs from the v3 progress table
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-28 14:41:54 -05:00
b0a2207c6a docs(link): record world.ruleset and mark Protocol 3.0 progress
Protocol 3.0 order 2 (v3.md §5) is built across all four repos; this is its
documentation half, plus the running progress record the plan was missing.

v3.md
  - A progress table at the top and a State column on §9's sequencing table, so
    "what has landed" is answerable without reading four git logs. Part A (order
    1) and world.ruleset (order 2) are marked done; the spawn atlas is next.
  - §5 gains the implementation notes worth keeping, chiefly: where a system's
    on/off state is DERIVED rather than configured, read the system's own static
    instead of inventing a .cfg key (Shadowguard has no Enabled key — it's the
    TOL expansion gate; Factions is `!ViceVsVirtueSystem.Enabled` by
    construction in stock ServUO). Also that the plugin CAN be compile-verified
    despite the "no standalone build" caveat, and how.

INTEGRATION.md
  - The world.ruleset catalog entry and GET /ruleset, with the two things
    consumers get wrong: caps are in TENTHS (1000 = 100.0), and `connect` exists
    only if the operator set Bridge.PublicConnectAddress — the shard's real
    listen address is never published.
  - §2 now says plainly that v3 has NOT been bumped yet and what that means:
    sidecars on `edge` report 2 while already carrying some v3 kinds, so do not
    infer feature availability from the version during this window.

PROTOCOL_2.md §10.4
  - The deferred "which PvP system does this shard run?" is answered (VvV on,
    Factions off — and mutually exclusive by construction), and world.systems is
    marked superseded by world.ruleset, which carries the systems block it asked
    for. No orphan kind is left behind.

BACKEND_DESIGN.md — the shard_ruleset table (why it is stored whole rather than
normalized, and why no row means null rather than {}) and the public route.

PROJECT_TREE.md is deliberately untouched: sync-project-tree regenerates it on
push to main, so it updates itself at the v3 cutover.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-28 14:39:53 -05:00
35ad440bad docs(shard): record the REST projection gap the Part A smoke test found
The live five-rung smoke test of the visibility framework found that Part
A enforced it on the SSE path and on /guilds + /governors, but not on the
remaining public REST reads - so one event was projected live and served
verbatim from stored history.

link/v3.md gains 3.6.1 with the full list (the anonymous acct/webId leak
on /feed, the flattened ownerAcct on /idoc, the dead `houses` field
rules, /feed ignoring live config, the empty-allowlist fall-through, and
the Date-to-{} projection bug), plus the rule it leaves behind: a read
path that returns shard data and does not project is a bug, and every new
Part B/C surface must gate its kind set on live config rather than on
PUBLIC_KINDS.

3.5 also corrected: the table is NOT seeded on boot. An absent row means
"use the compiled default", which keeps the defaults in one place instead
of duplicating them into a seeder that could drift.

BACKEND_DESIGN.md 6.5 records the same as a security contract: rule 1
locks a field by meaning rather than spelling; PUBLIC_KINDS is a
module-load constant and must not answer per-caller questions;
projectFeature walks arrays and plain objects only.

SHARD_VISIBILITY.md gets the admin-facing version - that stored history
answers the same way the live stream does, and that turning live updates
off stops the push, not the reading.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-28 10:52:13 -05:00
6622afe4bd docs(link): add the Protocol 3.0 design
Surveys the live ServUO tree against everything the bridge already
surfaces and records the full gap list (17 items), then specs the four
features scoped for 3.0.

3.0 has three scope areas:

- A: the visibility framework. Admin-configurable, per-feature and
  per-field audience control over all ten shard-derived surfaces (the
  four new ones plus the six that already ship), on an
  anonymous -> logged_in -> player -> staff -> admin ladder. Every
  default reproduces today's behavior, so the retrofit is a no-op until
  an admin changes something. Two rules an admin cannot override: acct
  and webId are admin-only always, and an unmapped event kind is never
  broadcast below admin. This also fixes a verified leak - guild leader
  acct/webId are readable today on the anonymous /public/shard/guilds.
- B: three new wire streams - world.ruleset, points.board, and
  vendor.listing/vendor.listing.remove.
- C: the spawn atlas, built from static ServUO data files with no wire
  involvement.

Visibility lives entirely on the website; the sidecar stays a dumb
forwarder that defines no access parameters and advertises no
capabilities.

PROTOCOL_VERSION goes 2 -> 3 once, at the end: every part PRs into an
edge branch per repo, and the coordinated edge -> main merge is the
cutover. A schema migration moves uo_link_config.protocol so operators
don't have to.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-28 09:28:04 -05:00
runic-docs-bot
7f9d63308b docs(tree): sync link/PROJECT_TREE.md from RunicGateway/link@7e4177c [skip ci] 2026-07-22 21:20:56 +00:00
3ae4d1c3db docs(tree): add per-repo PROJECT_TREE snapshots + index them
Add auto-generated project-tree snapshots for the website, link, and
Android-app repos under docs/<repo>/PROJECT_TREE.md, and link them from
the README (adding a previously-missing android/ section). These files
are maintained going forward by the sync-project-tree CI workflow in each
source repo, which opens a PR here whenever the tracked layout changes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 16:14:52 -05:00
Claude
5880bc9f1e docs: fix cross-refs and scrub machine paths
- Repoint doc-to-doc references from the old docs/ prefix to the co-located
  sibling filenames (docs live under link/ here now).
- Replace the personal ServUO checkout path (C:\Users\...\servuo) with a
  <servuo> placeholder throughout.
2026-07-18 02:32:49 -05:00
Claude
69c8afa7bc docs: add index README and source README snapshots 2026-07-17 23:57:50 -05:00
2d04b4808b feat(protocol2): Town Cryer news-gump integration (§16, Protocol 2.1)
Website news articles now land in the modern Town Cryer News gump
(TownCryerSystem.NewsEntries), separate from the scrolling-crier lines.

Overlay BridgeNews (new): news.add / news.remove insert/remove a
TownCryerNewsEntry directly in the public NewsEntries list (no stock edit),
tracking our own id->entry map so stock uo.com news is left intact. Title,
HTML body, image, and URL are all supported (the stock gumps already branch on
TextDefinition.Number, so string content renders). On add the article title is
also proclaimed via GlobalTownCrierEntryList (announce defaults on; set
announce:false to suppress). Config caps: NewsMaxTitleLength/BodyLength/
External, NewsAnnounceDurationSec.

Sidecar: POST /news (add/replace, id-correlated), DELETE /news/{id}; news table
stores each article as its news.add command; on shard server.hello the sidecar
replays the stored set with announce:false (the shard rebuilds NewsEntries each
boot and does not persist ours, so the website is the source of truth).

Docs: PROTOCOL_2 §16 (design + verified), INTEGRATION.md /news endpoints.

Verified live: sidecar cargo check clean; overlay compiles in the full ServUO
Scripts tree (0 errors); booted shard + sidecar and exercised add/replace/
remove/error paths and the reconnect replay end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:27:13 -05:00
fdd67bff69 docs(protocol2): add Town Cryer news-gump integration design (§16)
Adds §16: sync website news articles into the modern Town Cryer News gump
(TownCryerSystem.NewsEntries), distinct from the Protocol 1.0 scrolling-crier
lines (GlobalTownCrierEntryList). Grounded in the shard's Town Cryer source.

Key findings / decisions:
- NewsEntries is a public mutable List and TownCryerNewsEntry's ctor is public,
  and the display gumps already branch on Title/Body .Number>0 (cliloc) vs
  string (AddLabelCropped / AddHtml with HTML support). So website content
  needs NO gump changes and NO stock patch — the overlay inserts/removes
  directly and tracks its own entries, leaving stock uo.com news intact.
  (Refines the pasted guidance, which proposed adding methods to the stock
  TownCryerSystem.cs = a patch.)
- Ties the two surfaces together: full article -> news gump; crier "says" just
  the title via the existing GlobalTownCrierEntryList path.
- news.add/news.remove verbs (id-correlated, idempotent), POST /news +
  DELETE /news/{id}; website is source of truth, re-synced on shard reconnect
  since NewsEntries isn't persisted across reboot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:15:24 -05:00
e34606dccc docs(protocol2): record live smoke-test results
Booted ServUO + the real sidecar (protocol 2, plugin connected) and exercised
every Protocol 2.0 endpoint end-to-end. PROTOCOL_2.md §15 records the results:

- Part A: account.create 200 + link; duplicate 409; per-IP cap enforced at the
  shard's real AccountsPerIp=3 (4th from one IP -> 429); loopback IP -> 400
  (fail-closed); unlink 200 then lookup 404.
- Part B: /houses (28, full data), /governors (9 cities), /guilds ([]),
  /online (count 0, headless), /char titles block present.

Not exercised (needs a live UO client): presence.online with players,
region.enter, real-time guild.join, char.vitals. Also documents the
Scripts.dll boot-recompile lock quirk (build offline with the server stopped).
World save left untouched; test accounts did not persist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 10:23:07 -05:00
2d579a0b4d feat(protocol2): titles in char.profile (Part B ph.4)
Overlay BridgeProfile: char.profile gains a titles block (selected index,
fameKarma, skill, and the raw reward-title list) read from PlayerMobile's
public title accessors. No new stream, no sidecar change — it rides the
existing char.profile served by GET /char. Reward entries may be a cliloc
number as a string or a literal; resolve numeric ones website-side like item
names.

Docs: INTEGRATION.md char.profile titles field; PROTOCOL_2 ph.4 built. Part B
phase 5 (Factions/VvV) remains deferred by owner decision.

Verified: overlay compiles in the full ServUO Scripts tree (0 errors, 0
warnings). Live run pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 10:12:01 -05:00
bba7cc7714 feat(protocol2): house registry board (Part B ph.3)
Overlay BridgeHousing (new): a diff sweep over BaseHouse.AllHouses ->
house.update / house.remove (owner, region, location, decay level, co-owners,
friends, placement price), complementing the existing house.decay transition
feed. HousingSweepSeconds (300s); wired into [bridge reload|sweepnow|status.
Stock ServUO has no "for sale" flag, so this is an owner->houses registry;
price is the placement value, not a listing.

Sidecar: houses board table with upsert/delete/all; main routes house.update/
remove into it; GET /houses served from the store.

Docs: INTEGRATION.md house.* events + /houses endpoint; PROTOCOL_2 ph.3 built.

Verified: sidecar cargo check clean; overlay compiles in the full ServUO
Scripts tree (0 errors, 0 warnings). Live run pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 08:05:47 -05:00
fba337e530 feat(protocol2): presence stream — online population + region transitions (Part B ph.2)
Overlay BridgePresence (new):
- presence.online sweep over online PlayerMobiles: total plus per-facet and
  per-region counts, emitted only when the population changes.
- region.enter real-time from EventSink.OnEnterRegion (player-filtered), the
  cheap location signal PLAN.md prefers over Movement.
- PresenceSweepSeconds (30s); wired into [bridge reload|sweepnow|status.

Sidecar:
- GET /online serves the latest presence.online snapshot from the event store
  (survives restart); population time series via /history?kind=presence.online.

Docs: INTEGRATION.md presence events + /online endpoint; PROTOCOL_2 ph.2 built.

Verified: sidecar cargo check clean; overlay compiles in the full ServUO
Scripts tree (0 errors, 0 warnings). Live run pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 08:02:05 -05:00
9183bf748f feat(protocol2): guild and town-governor world-state streams (Part B ph.1)
Adds the first Part B streams from docs/PROTOCOL_2.md: guild rosters and
town governors ("mayors"), both outbound diff-board sweeps mirroring the
existing champ board.

Overlay:
- BridgeSocial (new): guild sweep+diff over BaseGuild.List -> guild.update /
  guild.remove (full-state upsert; disband detected via Disbanded), plus a
  real-time guild.join from EventSink.JoinGuild. (EventSink.CreateGuild is only
  the load-time factory, so creation is derived sidecar-side from a first-seen
  id, as champs do.)
- BridgeGovernance (new): city sweep over CityLoyaltySystem.Cities -> city.update
  (governor / governor-elect / election phase), gated on CityLoyaltySystem.Enabled.
- BridgeJson.Actor: shared serial/name/acct/webId/player writer used by both.
- BridgeConfig: GuildSweepSeconds (60s), CitySweepSeconds (300s).
- BridgeBoot: both wired into [bridge reload|sweepnow|status.

Sidecar:
- store: guilds + governors board tables with upsert/delete/all.
- main: route guild.update/remove and city.update into the boards.
- web: GET /guilds, GET /governors served from the store (snapshot-companion
  rule, so a fresh page or a restarted sidecar hydrates without the shard).

Docs: INTEGRATION.md event catalog (guild.*, city.update) + board endpoints;
PROTOCOL_2.md Part B phase 1 marked built.

Verified: sidecar cargo check clean; overlay compiles in the full ServUO
Scripts tree (0 errors, 0 warnings). Live end-to-end run still pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 07:54:11 -05:00
d01a49103f feat(protocol2): website account provisioning & unlinking (Part A)
Adds the account-provisioning plane from docs/PROTOCOL_2.md Part A: the
website can create game accounts and unlink them, gated by a shard-wide
signup mode. The existing [link flow is unchanged.

Overlay:
- BridgeConfig: SignupMode (website|game|hybrid, default hybrid; unrecognized
  falls back to game), AccountCreateEnabled (mode-following default),
  RequireIpForCreate, name/password caps, and a boot warning when the core
  Accounts.AutoCreateAccounts setting contradicts the mode.
- BridgeAccounts (new): account.create (mode gate, actor required, char-safety
  mirrored from AccountHandler, collision check, per-IP cap via CanCreate/
  LogAccess with fail-closed missing/loopback IP, create + WebsiteUserId link,
  account.audit; password never logged or echoed) and account.unlink (Owner
  floor via BridgeAdmin.Protected, clears the tag).
- BridgeAccountLink: in-game [unlink command, emits account.unlinked.
- BridgeAdmin: Protected / ResolveTargetAccount promoted to public for reuse.

Sidecar:
- POST /accounts/create, DELETE /link/:account, respond_account status mapping
  (409 collision / 429 ip cap / 403 disabled|protected / 404 not-linked / 400).
- store.record_unlink drops the mirrored link row.
- PROTOCOL_VERSION -> 2 (outbound events additive; new endpoints need v2).

Docs: INTEGRATION.md protocol bump, account.* events, endpoints, 409/429;
PROTOCOL_2.md Part A marked built.

Verified: sidecar cargo check clean; overlay compiles in the full ServUO
Scripts tree (0 errors, 0 warnings). Live end-to-end run still pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 07:42:06 -05:00
f8e9ee8cfc feat(champ): stream champion-spawn state to the sidecar board
Champion spawns have no ServUO EventSink, so add a fourth polled stream
(BridgeChamps) modeled on BridgeSweeps: enumerate every spawn each tick,
fold to a small record, and emit champ.update only on change. No core
patch — every field used is public.

Covers all three families via a `category` field:
  - champion: ChampionSpawn (type/level/kills/boss/cooldown ETA)
  - mini:     MiniChamp (type/level; auto-restarts, no kill counter)
  - sea:      BaseSeaChampion (a High Seas world-boss mobile, alive only
              while summoned; removed via champ.remove when slain)

Status folds to active/cooldown/dormant. A (re)connection clears the diff
cache so the next sweep re-emits the full board, rebuilding a sidecar that
restarted on its own. Transient entries leave via champ.remove.

Sidecar: a `champs` current-state table (one row per serial) fed by
champ.update (upsert) and champ.remove (delete), exposed at GET /champs as
the live board. New ChampSweepSeconds config (default 10s), wired into
[bridge reload/sweepnow/status. Documented in docs/INTEGRATION.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
2026-07-14 05:46:21 -05:00
b210675f35 docs(admin): shipped scope is Phase 1 + Phase 2; Phase 3 not planned
Records the owner decision to stop after the help-page queue: Phase 3
(mute/notes/teleport/save) will not be built. The Tier-B candidates remain
catalogued in §3 for the record.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
2026-07-13 02:58:04 -05:00
c9cc8ead1a feat(pages): help-page (support) queue — stream, snapshot, respond, close
Phase 2 of docs/ADMIN_CONTROLS.md: surface the in-game help-page queue to the
website.

- BridgePages.cs: the queue has no EventSink, so it is polled (PageSweepSeconds,
  default 5s) and diffed, keyed by sender serial (one page per player) ->
  page.new / page.updated / page.closed. Inbound pages.snapshot -> pages.list;
  page.respond delivers a staff reply to the player (online: a gump now; offline:
  queued for next login; shows as "Staff") and can close; page.close removes it.
- BridgeConfig/Bridge.cfg: PageSweepSeconds. BridgeBoot: reload re-arms the poll,
  status reports it.
- sidecar/src/web.rs: GET /pages, POST /pages/{id}/respond, POST /pages/{id}/close.
- INTEGRATION.md: page events (§4) and endpoints (§6).
- tools/scaffolding/BridgePageProbe.cs: gated headless verification.

Verified live (probe-seeded tickets): snapshot returns the queue, the poll emits
page.new for both and page.closed on removal, respond -> 200, close removes the
page, unknown page -> 404.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
2026-07-13 02:54:19 -05:00
5990a2be3a feat(admin): forward in-game moderation to the website (bidirectional audit)
Phase C / §5.5: so the site's moderation log is complete regardless of origin,
in-game uses of the write-plane verbs are forwarded as admin.audit
(origin:"in-game").

- patches/commandlogging-event.patch: adds CommandLogging.OnWrite, raised in
  WriteLine before the m_Enabled guard so it fires even when file logging is
  off. Scripts-layer file -> dynamic build, no core rebuild.
- patches/BridgeModerationAudit.cs: subscriber. Taps OnWrite for resolved
  ban/kick (parsing the target from the log line) and EventSink.Command for
  [bcast. Lives in patches/ (not overlay/) because it references OnWrite,
  which only exists post-patch — same rule as BridgeVendorSale.cs.
- tools/scaffolding/BridgeAuditProbe.cs: gated headless verification.

Verified live: a genuine [bcast plus simulated ban/kick log lines produced
admin.audit frames with origin=in-game, actor, and the target parsed
(seed_010); a non-moderation line was correctly ignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
2026-07-13 02:12:30 -05:00
a9bb5e8641 feat(admin): sidecar REST routes for the write plane
Phase 1 (sidecar side): POST /admin/{kick,ban,unban,broadcast} forward to the
shard, correlated on a fresh reqId, with an admin-specific status mapping —
unknown target -> 404, protected target / plane-disabled -> 403, missing actor
/ bad body -> 400. actor is required and checked up front. Documents the
endpoints and the admin.audit event in INTEGRATION.md.

Verified end-to-end (real sidecar + booted shard): 200 on success, 403 on the
Owner floor, 404 unknown target, 400 missing actor, 401 no token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
2026-07-13 02:00:17 -05:00
aa329215d8 feat(admin): plugin write plane — admin.kick/ban/unban/broadcast
Phase 1 (plugin side) of docs/ADMIN_CONTROLS.md: a staff write plane so the
website can moderate the live shard.

- BridgeAdmin.cs: inbound admin.kick, admin.ban (timed + indefinite),
  admin.unban, admin.broadcast. Each requires an `actor`, refuses targets at
  or above AdminAccessFloor (default CoOwner — Owner-only shield), replies
  admin.ok/admin.error with the reqId echoed, and emits an admin.audit
  (origin=web) broadcast. Attribution is web:<actor> in the console log and
  the ban BanDealer tag. Kicking enumerates NetState.Instances so a
  character-select session is caught too.
- BridgeConfig/Bridge.cfg: AdminWriteEnabled (default OFF — opt-in),
  AdminAccessFloor, broadcast/reason length caps, ban duration clamp.
- tools/stub_sidecar_admin.ps1: live smoke-test harness; *.log gitignored.

Verified: compiles clean against ServUO (0 err/warn); live run on the seeded
shard confirms all four verbs, the audit stream, timed-ban fields, and the
Owner-floor refusal, with no exceptions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
2026-07-13 01:55:11 -05:00
52206a73d2 docs: website integration guide
docs/INTEGRATION.md is the API reference for building the front end against the
sidecar: base URL, auth (Bearer / X-Api-Key / ?token=), protocol versioning, the
rich /health, the WebSocket live feed with a full event catalog, every REST query
and command (char/roster/vendors/link/towncrier/history/economy), the status-code
table, a worked character-page example, and current caveats. Payloads are the real
shapes captured during testing. Linked from the top-level README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 20:47:09 -05:00
ee425f95d9 Phase 7: PlayerVendorSale core event + subscriber
The one non-drop-in piece. Player-vendor purchases raise no EventSink, so the
sale is invisible to subscription. Two git-format core patches add a
PlayerVendorSale event and raise it at the committed sale in
PlayerVendorBuyGump.OnResponse (right after HoldGold +=), where buyer, vendor
owner, item, price, and commission are all in scope. The subscriber
BridgeVendorSale emits vendor.sale.

All three are a coupled unit. The subscriber references PlayerVendorSaleEventArgs,
which does not exist until the EventSink patch is applied, so it lives in patches/
not overlay/ -- shipping it in overlay would break the build on any unpatched
install. patches/README.md documents applying the unit; both patches verified
with git apply --check against stock ServUO 57.4. This is the first phase that
rebuilds the core (ServUO.exe), not just Scripts.dll.

vendor.sale carries buyer and vendor-owner accounts, both present and distinct,
which is the pair that flags gold-laundering when they match -- richer than the
ownerless NPC ValidVendor* events, and on a committed sale rather than a
validation stage.

Verified with a probe firing the event on real seeded-vendor data: vendor.sale
emitted with buyerAcct=seed_001, ownerAcct=seed_000, Longsword, price 69819. The
probe proves the event, args, subscriber, and payload; the literal gump call site
firing on a real purchase needs a live buyer with a NetState and is confirmed by
an in-game buy. Evidence in docs/PLAN.md §17.

This completes every phase on the ServUO side. Phases 0-6 are drop-in (overlay/);
7 is patches/. Cheat signals are folded into existing streams (fastwalk, audit,
vendor.sale), not a separate phase. Remaining work is the Rust sidecar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:46:07 -05:00
db7c0adbf0 Phase 6: town-crier news (website -> game)
BridgeTownCrier handles inbound towncrier.add / towncrier.remove, pushing
website-published news into GlobalTownCrierEntryList so every town crier
announces it until it expires. Both run on the Core thread (required: AddEntry
mutates a shared list and the criers send packets). An id maps to the created
TownCrierEntry so a later remove can pull it, and re-adding an id replaces the
prior entry.

Caps are enforced before touching the shared list -- line count, line length,
active-entry count, duration -- as defense in depth on top of the loopback trust
boundary: a buggy or compromised sidecar still cannot flood the criers or pin a
message forever. Config: Bridge.TownCrierMax{Lines,LineLength,Active,DurationSec}.

Verified with a sending stub and a probe that logs the actual crier list. Replies
and game state agree: add n1 -> towncrier.ok and the entry appears with the exact
lines; add n2 (8 lines over the cap of 6) -> towncrier.error and never enters the
list; remove n1 -> towncrier.ok and the entry is gone; remove unknown ->
towncrier.error. Evidence in docs/PLAN.md §16.

Adds BridgeJson.GetStringList for JSON string arrays, tools/stub_sidecar_crier.ps1,
and tools/scaffolding/BridgeCrierProbe.cs. This closes the pure-plugin inbound
work; only the PlayerVendorSale core edit (Phase 7) remains on the ServUO side.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 11:56:22 -05:00
5eecdea2fc Phase 5: [link account linking
BridgeAccountLink ties a game account to a website account. [link mints a
one-time, 5-minute code from an unambiguous alphabet (no O/0/I/1), holds it in a
Core-thread dict keyed to the account, and emits link.request. The website
relays the code back through the sidecar as link.confirm; the shard validates,
writes the WebsiteUserId account tag, and replies link.ok. A bad or expired code
gets link.error.

The tag persists to accounts.xml in ServUO's standard <tags> format, read by
LoadTags at boot, so a link survives restarts with no new persistence layer.
mob.login now carries webId when the account is linked, so the sidecar can
attribute a session to a site user without a lookup.

Safeguards: one-time codes; only the newest code per account is valid; per-account
30s rate limit against code spam; a 1-minute purge bounds the code table; the
websiteUserId is trusted only because the socket is loopback-only. The tag reaches
memory on confirm but disk only on the next save — a hard crash between loses it,
and the player just re-runs [link.

Verified end to end with a smart stub that reads the emitted code and confirms
it: link.request -> link.confirm -> link.ok, a bad code -> link.error, and the
tag observed in accounts.xml after a save. Evidence in docs/PLAN.md §15.

The [link command body is exposed as RequestLink(Mobile) so it can be driven in
tests without a client. Adds tools/stub_sidecar_link.ps1 and
tools/scaffolding/BridgeLinkProbe.cs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 11:47:17 -05:00
edb1fdcbe2 Phase 4: character-profile request/response
BridgeProfile builds the read-models the website consumes; BridgeRequests
registers the inbound handlers. The sidecar asks, the shard answers on the Core
thread (inbound lines are marshaled through Timer.DelayCall before a handler
runs), so all of these read live world state safely.

  - char.request: resolve by serial, or by account + slot, and reply with a full
    profile (stats, all trained skills, worn equipment with flattened AOS mods,
    resists). Works for offline characters since a logged-off mobile stays
    resident until Delete.
  - account.roster: light per-character summary, offline chars included.
  - vendor.snapshot: every player vendor owned by an account, with held gold and
    priced listings.

Each request may carry a reqId the reply echoes so the sidecar can correlate.
An unresolvable request gets a bridge.error reply rather than silence, so the
website can show a real failure instead of hanging.

Verified against the real world with a sending stub: all five requests answered,
both char lookup paths (account+slot and serial) returning the identical profile,
vendor.snapshot returning seed_000's two vendors and 80 listings, and the bad
account returning bridge.error. Two real-data findings noted in docs/PLAN.md §14:
a GM character can have skill base > cap (the website must not assume otherwise),
and the mod-flattening path still wants a genuinely kitted character to exercise
against real suffix gear.

Adds tools/stub_sidecar_request.ps1 (sends requests) and a hardened
tools/stub_sidecar.ps1 (survives reaping/rebind).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 11:34:14 -05:00
07e109994d Phase 3: polled sweeps (vitals, house decay, economy)
BridgeSweeps runs three repeating Core-thread timers for the state that has no
EventSink. Cost measured in Phase 1 is why they can run on the main thread: a
full pass of all three is well under a millisecond at the seeded scale.

  - Vitals: online players only. Small and volatile; the sidecar diffs snapshots.
    Offline characters do not move, so they are served on demand as full profiles
    instead, not swept.
  - House decay: emits only on a level transition. A silent baseline on
    ServerStarted records every house's current stage, so a restart does not
    re-announce them. Payload carries from/to, coords, nested ban location,
    region, sign name, owner serial+account, and built/refreshed timestamps, all
    null-guarded.
  - Economy supply: periodic sum of every account's currency as a snapshot. The
    level; AccountGoldChange and the vendor events are the flow.

All three re-arm on `[bridge reload`; `[bridge sweepnow` runs one of each on
demand; `[bridge status` reports sweep counters. Sweeps skip emitting while the
sidecar is disconnected, since their state is perishable and re-emitted next
tick anyway (unlike events, which queue through an outage).

Verified on the seeded world with 8s intervals: baseline recorded 29 houses
silently, a probe bumped one Somewhat->Fairly, and the next sweep emitted exactly
one house.decay and none for the other 28. Economy emitted a supply snapshot per
interval. Vitals emitted nothing, correctly, since all seeded characters are
offline. Evidence in docs/PLAN.md §13.

Adds tools/scaffolding/BridgeSweepProbe.cs (never deployed) to force a decay
transition on demand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:59:21 -05:00
637dedc45e Phase 2: cheap event streams
BridgeEvents subscribes the streams selected for tracking, economy, and cheat
detection: Login/Logout/AccountLogin, AccountGoldChange, ValidVendorPurchase/
Sell, PlacePlayerVendor, SkillGain, FameChange, KarmaChange, QuestComplete,
PlayerDeath, PlayerMurdered, OnKilledBy, FastWalk, OnPropertyChanged, Command,
and Before/AfterWorldSave.

Every handler runs on the Core thread inside the path that raised it, so each is
wrapped to never throw, does only Emit (which enqueues and returns), and never
mutates the args. Three of these are veto hooks and are read strictly:
AccountLogin (Accepted/RejectReason, and a plaintext Password we never emit),
FastWalk (Blocked), and the login decision path generally.

Testing on the live shard found that SkillGain fires for NPCs, hard: the first
boot emitted 115 skill.gain events in four seconds, all spawned creatures
grinding Meditation, zero players. That is the general rule here — most "player"
events also fire for NPCs — so SkillGain, FameChange, KarmaChange, and OnKilledBy
all filter to players on the Core thread before the socket. Gold, fame, karma,
and the save boundaries were fired through their real code paths and observed at
the stub sidecar; gold.change round-trips the platinum->gold conversion and
persists across restarts. Evidence in docs/PLAN.md §12.

Adds tools/scaffolding/BridgeEventProbe.cs (never deployed) which triggers those
events through real world mutations rather than synthetic Invoke calls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:46:41 -05:00
35c85d542a Phase 1: loopback transport to the sidecar
BridgeLink owns a TcpClient to 127.0.0.1 and nothing else touches it. Emit() is
called from the Core thread; it enqueues onto a bounded drop-oldest queue and
returns. A link thread drains the queue and reconnects with backoff; a reader
thread parses inbound lines and marshals each to the Core thread via
Timer.DelayCall. An absent, slow, or wedged sidecar therefore cannot stall the
shard, which is the property the rest of the bridge depends on.

Outbound JSON is written by hand into a StringBuilder because it runs on the
Core thread for every event and the measured budget assumes that cost. Inbound
uses JavaScriptSerializer: commands arrive at human rates, so correctness beats
speed, and parsing happens off the Core thread anyway. That needs a
System.Web.Extensions reference.

server.hello is emitted per connection rather than once at ServerStarted. A
sidecar that restarts independently would otherwise never learn which shard it
is attached to. It carries a bootId, stable across reconnects and fresh on every
shard restart, so the sidecar can tell "I reconnected" from "the shard
restarted" and keep or discard its cache accordingly.

Two defects found by testing and fixed before commit:

  - Backoff ceiling was 30s, so a sidecar restart cost up to half a minute of
    buffering on a loopback socket. Now 5s.
  - A stale reader could kill a fresh connection: reader.Join(1s) can time out,
    and the old thread's finally block then set the shared _dead flag, possibly
    tearing down the connection that had replaced it. Connections now carry an
    epoch and a reader only marks dead the one it owned.

Acceptance evidence recorded in docs/PLAN.md §11: boots with no sidecar, buffers
through the outage and drains on connect, round-trips ping/pong on the Core
thread, survives unknown kinds and malformed JSON, and reconnects unattended.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 05:03:03 -05:00
c87ca88d86 Phase 0: fix runtime script compilation
ScriptCompiler.Compile() runs `dotnet build Scripts/Scripts.csproj -c Release`
with no Platform, so MSBuild defaults to AnyCPU. Scripts.csproj gated both
OutputPath and DefineConstants on Configuration|Platform == Release|x64, so
under the server's own build the DLL landed in Scripts/bin/Release/ (while the
core loads Scripts.dll from the base directory) and TRACE;NEWTIMERS;ServUO went
undefined (XmlSpawner compiled its non-ServUO branches).

Compile() also never checks the build's exit code before Assembly.LoadFrom, so
the failure was silent and the stale DLL reloaded. Runtime script compilation
had had no effect since 2026-05-30.

Condition both property groups on Configuration alone. Server.csproj is left
alone: nothing under Server/ uses those symbols, and giving it OutputPath=..\
would make the boot-time build try to overwrite the running ServUO.exe.

Verified end-to-end: a plain boot now logs "Core: Compiling scripts... / Build
succeeded." and loads 206208 items, 42771 mobiles.

Also adds the implementation plan, the measured performance budget, the test
scaffolding used to produce it (seeder + probe, both default-off), and the
record of shard repairs that had to precede any of this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 04:34:18 -05:00