PLAN.md gains a "Phase 3 as built" section covering the decisions the plan had
left open, and §2.2, §2.2.1 and §7.0 are brought in line with what shipped:
* The engine is fully native. §2.2.1 wrote rung 1 as "apply verbatim with
git apply", but §1 chose the release tarball so there would be no git on the
shard host, and rung 2 needs a native applier anyway. Rung 1 keeps its
stronger verdict and shares rung 2's write path. On the real files this is
not academic — the shipped patches are CRLF and two of their three targets
are LF, so git apply refuses patches the installer places correctly.
* §7.0 documents `patch_tier` in the overlay manifest. Which patches form one
unit, which companion follows which, whether a core rebuild is needed and
what declining costs are not derivable from a diff, so the release declares
them and adding a patch regenerates metadata rather than an installer.
* §2.2 gains the pre-image cache and the widened patch cache, and §2.2.1 gains
the second, per-feature level of the all-or-nothing rule.
INSTALL.md's illustrated tier output is replaced with the real thing, the
status banner now says `install` is complete, §3's path tables list
patches/originals/, and Appendix A2 names the line-ending trap that makes
git apply refuse a patch whose region is visibly untouched.
Refs: RunicGateway/installer#6, RunicGateway/servuo-plugins#10
Co-Authored-By: Claude <noreply@anthropic.com>
Phases 1 and 2 now live on the installer repo's `edge` branch, so PLAN.md's
status, the config-path section, and the operator guide all move with them.
PLAN.md
- Status: phases 1 and 2 built. The `edge -> main` cutover now follows Phase 3
rather than Phase 2, because INSTALL.md §4 describes the patch tier as part of
the run and a release that answers "not implemented" to all of it is the same
half-capable binary that kept Phase 1 off `main`.
- §2.3: the service definition always pins the config path, but only Linux pins
the database. On Windows config and data share a directory, so the sidecar's
own anchoring rule already lands it correctly — and `sc.exe` offers no
per-service environment, only a machine-wide one that every process inherits
and that outlives an uninstall.
- New "Phase 2 as built" section: the virtual service account, the config
lockdown and why its two halves straddle registration, `--verify` running no
part of the sidecar half, the protocol check against the installed binary,
`RUNICGATEWAY_STATE_DIR` relocating the binary and suppressing service
registration, degrading to a printed recipe with no root/LocalSystem fallback,
and the token never entering install.json.
- §8 question 1 (Windows service mechanism) resolved: `sc create`, as
recommended — plus the service identity the recommendation did not anticipate.
INSTALL.md
- Status banner: what is built, and that the patch tier is the remaining gap.
- §2: the illustrated run matches the sidecar block the binary actually prints.
- §3: a table of how each platform pins config and database, the dedicated
service account on both, and the fact that sidecar.toml's permissions are
restricted because it holds the auth token.
- Appendix A4: the Windows recipe now matches what the installer does —
`--config` in binPath (single-quoted so PowerShell keeps the inner quotes),
`obj=` for the virtual account, the icacls lockdown before and grants after,
and no machine-wide environment variables.
- Troubleshooting: a row for a run that could not register a service, and one
for a service that starts and immediately stops.
Co-Authored-By: Claude <noreply@anthropic.com>
PLAN.md moves from "Phase 0 complete, no code exists" to "Phase 1 built, on
edge", and the Phase 1 section gains an "As built" block in the same shape as
the Phase 0 entries — covering the decisions that were not already settled by
the design: why the crate lands on `edge` instead of `main`, why the library
target is not named after the binary (Windows UAC installer detection makes
`cargo test` unrunnable under that name), the dependency choices that follow
from the MinGW cross-build, path-based rather than name-based shard-running
detection, reading ServUO's version from Server/AssemblyInfo.cs, and the two
rules the smoke test corrected — install.json recording a state rather than the
run's verb, and the Bridge.cfg keep comparing against the last hash deployed
rather than the last hash seen.
INSTALL.md gains the same status note and one troubleshooting row: Windows
elevates the binary on launch because its file name contains "install", which
is expected and needs no action beyond running from an elevated shell.
Co-Authored-By: Claude <noreply@anthropic.com>
The patch tier refused on a whole-file hash mismatch, which is the wrong
question: the three patches touch three small regions of three large files, so
an operator who edited Logging.cs somewhere else entirely was handed a manual
patch job they did not need. Hand-modified shards are the norm, so that refusal
covered most of the audience.
Replace the single hash test with a four-rung ladder (PLAN §2.2.1), cheapest and
safest first:
0 post-patch text already present -> no-op, keeps re-runs idempotent
1 whole file matches the pre-image -> apply verbatim
2 file differs, patched region is still byte-identical -> apply at the
matched offset
3 anything else -> do not touch the file; print the hunk to apply by hand
Rung 2 needs no new metadata: a unified diff already carries the stock text of
the region it edits (context lines plus the '-' lines). Guardrails keep it from
becoming a fuzzy apply -- exact match with only CRLF/trailing-whitespace
normalisation, exactly one occurrence or it fails, line numbers advisory only,
and all-or-nothing per patch file so a half-patched EventSink.cs cannot happen.
install.json records which rung applied each patch, and doctor and uninstall
report it.
This retires the blanket 57.4-only version gate, so PLAN gains §2.2.2 to draw
the line the ladder does not: content matching is a mechanical guarantee about
where text lands, not a support commitment. 57.4 stays the only supported
version. A non-57.4 tree may attempt the tier, but unsupported, untested and not
guaranteed -- behind a loud banner, a prompt defaulted to no, and its own
--patches-unsupported-servuo flag, because a bare --patches can be hit by
accident in a copied script. The unsupported marker persists into install.json,
every later doctor run, and the uninstall report.
INSTALL.md gets the operator-facing half: a block-quoted warning naming the
silent-script-build failure mode, the updated prerequisite row, prompts and flag
table, and a sample run showing all three outcomes.
Co-Authored-By: Claude <noreply@anthropic.com>
docs#87 landed; the row it added still said 'in review'. The status table is
the first thing anyone reads in this file, so a stale row there is worse than
no row.
Closes the last Phase 0 item. INSTALL.md is written before the installer
binary on purpose: everything it installs is already released (0.1-0.3), so
the guide is not speculation about a tool that might exist - it is the
specification of what the run asks, where it writes, what it prints, and what
the operator does next.
It is useful today. Appendix A is the same deployment done by hand - bundle
fetch, tarball verify and overlay copy, the optional patch tier,
--print-config provisioning, systemd unit / sc create - composed from the
released artifacts' actual contents and the sidecar's config and CLI source.
That appendix doubles as Phase 1's acceptance test.
Writing it settled four things the plan had left implicit, now recorded in
PLAN.md:
- The installer does not install itself; day-two commands run from the
downloaded binary.
- The flag surface: --servuo, --patches/--no-patches, --host, --site-url and
--yes join the --verify/--bundle/--purge the plan already named, so every
prompt has a non-interactive equivalent.
- A modified Config/Bridge.cfg is reported, not overwritten - one deliberate
deviation from deploy.ps1, whose overwrite-on-hash-differs rule is right for
a developer and would silently revert an operator's whole shard config on
update. install.json's recorded hashes are what make the distinction
possible.
- Remote-website deployments: widen [web] bind, firewall it to the site's
address, front it with TLS or a VPN off a trusted network. [shard] bind
stays on loopback because that socket carries commands into the game.
Also adds the missing installer/ section to the docs index, and refreshes two
stale examples in PLAN.md (overlay file count, sidecar version).
Co-Authored-By: Claude <noreply@anthropic.com>
Documentation half of installer Phase 0.3. Code PRs: RunicGateway/installer#3,
RunicGateway/link#25, RunicGateway/servuo-plugins#9.
Phase 0 item 3 asked for a compose job, a nightly cron, and a dispatch step in
each component. All three landed, and building them settled questions §7 had
left open — so those sections are corrected rather than appended to.
Status: Phase 0 is now all but complete. 0.2 is merged and released as link
v1.1.0; 0.3 is in review; 0.4 (INSTALL.md) is the remaining item, and the shape
it was waiting on has settled.
Phase 0 item 3 gains an "As built" subsection matching items 1 and 2, recording
what was chosen rather than inherited: why gate 1 reads the sidecar's protocol
from source at the release tag instead of asking the binary via --print-config
(it only answers for v1.1.0+, and --bundle <tag> must be able to recompose an
older pair); why gate 2 records the hash CI computed itself and separately
checks for assets absent from SHA256SUMS; why release metadata is read
anonymously; why an unrecognized asset name is a hard failure; and why a run
that changes nothing writes nothing.
§7.1 is corrected in two places. The manifest shape now shows link.assets as a
map keyed by platform — the single sha256 this section sketched could only ever
have described one of the two binaries link publishes — plus schema/generated
and the servuo block. And it gains a subsection naming where bundles are
published: committed under bundles/ in the installer repo, NOT one Gitea release
per bundle, because that repo's own releases are the installer binaries and
/releases/latest returns whichever release is newest regardless of kind.
§7.2 records that the dispatch steps now exist, and that a failed dispatch is a
warning rather than a failed release — which is what keeps the installer repo's
token out of the components' hard requirements.
§7.3 records that the releasable-commit rule must also exclude merge commits,
whose subject quotes the real one: without that, merging a docs: branch whose
title mentions a fix: would re-dispatch a declining release workflow nightly.
Co-Authored-By: Claude <noreply@anthropic.com>
Phase 0.2 landed in link#24: the sidecar gained a four-flag CLI, --print-config,
and config-anchored data paths. Three sections of the installer plan asserted
facts that change as a result, so they are corrected rather than appended to.
- Status table: 0.1 merged (servuo-plugins#7/#8, overlay v0.1.1 released), 0.2 in
review, repo bootstrap merged. 0.3 (bundle CI) is next and now unblocked — both
components it composes exist.
- Phase 0 item 2 gains an "As built" subsection matching item 1's: why four
hand-rolled flags rather than a parsing crate, why --print-config provisions
instead of only reporting, why config_created/token_generated exist, and why no
platform data directories are compiled into the binary.
- §2.3 (working-directory trap): half-closed in the sidecar — a relative
[store].path now anchors to the config file's directory — while the service
definitions still pin both env vars, and why that is not redundant.
- §2.4 (token handoff): the installer reads the handoff block out of one
--print-config call and never parses the log, which is not a contract.
- §5 Phase 2 / Phase 4, §6: the ordering that follows (print-config before service
registration), which doctor rows the CLI answers, that only the host is
substituted into the printed URLs because web.bind is often 0.0.0.0, and that the
printed token must not reach a log or support bundle.
- link/INTEGRATION.md §1: how to read the token back, replacing "the sidecar logs
it" with the supported command and its output.
Co-Authored-By: Claude <noreply@anthropic.com>
Tracks what actually landed while starting the installer plan, and corrects
the parts of the plan that the work proved wrong or stale.
Progress:
A Phase 0 status table at the top, so the plan says where it is rather
than needing a reader to reconstruct it from PR links.
Phase 0 item 1 (§5) now records the release workflow as built, including
its three deviations from link's copy — structural gates instead of build
gates, no bump commit and therefore no push to main, and overlay.toml as
the home for the declared protocol version. Plus the fixed tarball prefix
and why: the installer would otherwise have to parse the version it is
trying to read.
New §7.0 documents the overlay manifest as generated, and states plainly the
two things about it that carry weight: `protocol` is hand-maintained and has
to be (nothing in CI can derive it, which is exactly why §7.1's gate 1 has
something to compare), and `files` is what lets `doctor` distinguish
"operator edited a deployed file" from "the overlay moved on".
Corrections:
§2.6 the plugin's protocol version now has a home (overlay.toml), and
servuo-plugins now has a release workflow.
§7.2 the dispatch step is deliberately deferred to Phase 0 item 3.
§7.4 no longer "open risk" — the v3 cutover merged. The rule it motivated
(never hardcode a protocol version) is restated as permanent rather
than as a workaround for a mid-flight cutover.
§8 open question 4 (branch targeting) resolved: servuo-plugins#6 merged,
main == edge, everything targets main.
Version examples in §3, §5 and §7.1 said uo-link v3.x.y / 3.0.1, conflating
the release version with the protocol version. link is actually at v0.3.0 —
the two are independent, and the bundle names release versions, so an example
implying they track each other is actively misleading. Now uses the real
values (link 0.3.0, overlay 0.1.0, 30 overlay files).
Co-Authored-By: Claude <noreply@anthropic.com>
The installer needs CI that reacts when a component publishes a release. Adds
that as section 7, folded into version tracking because the bundle IS the compat
matrix -- which closes the "where does the compat matrix live" gap section 7
previously left open.
- 7.1 Bundle manifest: CI publishes an exact, protocol-checked combination of
component versions; the installer resolves against it at run time and
--bundle <tag> pins one. A link release regenerates JSON and leaves the
installer binary untouched, so operators don't re-download the installer for a
sidecar patch and the repo doesn't accumulate releases with identical code.
Two compose-time gates: sidecar PROTOCOL_VERSION must equal the overlay
manifest's declared version, and every asset's SHA256 must match.
- 7.2 Triggers: each component's release job POSTs to the installer's
workflow-dispatch endpoint (link's release.yml already declares
workflow_dispatch and already holds a write:repository token), plus a nightly
cron so a missed dispatch self-heals. repository_dispatch avoided -- support
is uncertain on this Gitea version.
- 7.3 Stale overlay: dispatch, don't wait. Components self-release on merge to
their own main, so the release normally already exists. If main is ahead with
*releasable* commits (docs:/chore: correctly cut nothing), fire that repo's
workflow, compose from what exists now, warn loudly, and let the nightly fold
in the result. Dispatching another repo's workflow is fine -- it still runs
its own gates -- but polling it is not, since Gitea's dispatch endpoint
returns no run handle.
Bundle CI becomes a Phase 0 deliverable, since Phase 1 resolves what to install
from the bundle. `update` now moves between checked combinations rather than two
independently-latest artifacts.
Co-Authored-By: Claude <noreply@anthropic.com>
Design of record for a deployment tool that takes a stock ServUO install and
configures it for Runic Gateway. Supersedes the informal overview it grew from,
which described a ServUO integration that does not match how servuo-plugins
actually ships.
Corrections that change the design:
- There is no RunicGateway.dll and no Plugins/ dir. The plugin ships as C#
source compiled by ServUO at boot, so the step is a hash-compare sync of
overlay/ -- but a successful copy does not mean a working bridge, because
ScriptCompiler.Compile() ignores dotnet build's exit code and reloads the
stale Scripts.dll.
- Stock ServUO files ARE modified, by three diffs in patches/. Made an opt-in,
skippable tier: git apply against a hand-modified shard will fail, and the
EventSink.cs patch needs a full core solution rebuild.
- Config paths collided with what the sidecar actually reads. Split ownership:
sidecar.toml stays the sidecar's schema, install.json is the installer's.
Service definitions pin UOLINK_CONFIG and UOLINK_DB_PATH, since the sidecar
writes relative to CWD and would land in VirtualStore under Program Files.
- The token handoff was missing entirely -- the largest "installed it and
nothing happened" failure mode.
- deploy.ps1 cannot be the cross-platform deployer; it stays the developer tool.
Decisions: public audience, unsigned binaries anchored on SHA256SUMS, Rust,
release-tarball plugin distribution, printed token handoff, new installer repo,
warn-and-skip on non-57.4 ServUO, and an uninstall that never edits the shard
tree -- it prints the files to delete and the hunks to revert.
Phases 0-5, with Phase 0 (a release workflow for servuo-plugins, which has
none today) gating everything else. Two open questions remain in section 8.
Co-Authored-By: Claude <noreply@anthropic.com>
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
CLILOCS.md and SPAWN_ATLAS.md each explain WHY the operator has to supply
something out of their own client, but neither says how. UOFIDDLER.md is the
missing procedure: where to get UOFiddler, which two files in the zip matter,
which runtime it needs, where Cliloc.enu actually lives, the conversion, how to
point the site at the result, and how to confirm it took.
Verified end to end on a stock Windows box: UOFiddler 4.22.2 (Ultima.dll is
net10.0), .NET SDK 9.0.312 building the net8.0 converter, RollForward carrying
it onto runtime 10.0.8, and the site's own parser reading the output back.
Corrects one claim while doing it. CLILOCS.md said a UOFiddler GUI export
"works equally well"; it does not. Its Cliloc tab writes `Number;Text;Flag` --
three columns, flag LAST -- and parseClilocText splits on the first separator
only, so the flag is absorbed into the name and every item renders as
`quarter staff;0`. The parser already handles `number,flag,text` with the flag
in the middle, but a trailing `;0` is indistinguishable from a name that
genuinely ends that way, so this stays a documented `sed` on the operator's
side rather than a heuristic that would corrupt real names.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
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>
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>