Commit Graph

23 Commits

Author SHA1 Message Date
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
d712b0c1e7 Merge pull request 'feat(champ): stream champion-spawn state to the sidecar board' (#3) from feature/champ-spawns into main
Reviewed-on: UOM/link#3
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-14 16:47:12 +00:00
4295c18124 Merge branch 'main' into feature/champ-spawns 2026-07-14 16:46:55 +00: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
46f8b2bb4e Merge pull request 'feat(admin): staff write plane + help-page queue' (#1) from feature/admin-controls into main
Reviewed-on: UOM/link#1
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-13 08:39:43 +00: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