Compare commits
1 Commits
c583ddb77f
...
docs/andro
| Author | SHA1 | Date | |
|---|---|---|---|
| f6a22734d3 |
@@ -22,7 +22,6 @@ ci/ cross-cutting CI/quality notes
|
|||||||
| [SHARD_VISIBILITY.md](website/SHARD_VISIBILITY.md) | Who sees which shard data — the admin-configurable audience framework |
|
| [SHARD_VISIBILITY.md](website/SHARD_VISIBILITY.md) | Who sees which shard data — the admin-configurable audience framework |
|
||||||
| [SPAWN_ATLAS.md](website/SPAWN_ATLAS.md) | The bestiary / spawn atlas: what the shard contains, parsed from its own ServUO tree |
|
| [SPAWN_ATLAS.md](website/SPAWN_ATLAS.md) | The bestiary / spawn atlas: what the shard contains, parsed from its own ServUO tree |
|
||||||
| [CLILOCS.md](website/CLILOCS.md) | UO's id → name table: converting one from your client so items have names |
|
| [CLILOCS.md](website/CLILOCS.md) | UO's id → name table: converting one from your client so items have names |
|
||||||
| [UOFIDDLER.md](website/UOFIDDLER.md) | **Operator runbook** — step-by-step extraction from your own UO client (cliloc table, creature art) |
|
|
||||||
| [MARKETPLACE.md](website/MARKETPLACE.md) | The player-vendor index: how it is gathered, what it costs, how to tune it |
|
| [MARKETPLACE.md](website/MARKETPLACE.md) | The player-vendor index: how it is gathered, what it costs, how to tune it |
|
||||||
| [website-README.md](website/website-README.md) | Snapshot of the website repo's README (setup/run reference) |
|
| [website-README.md](website/website-README.md) | Snapshot of the website repo's README (setup/run reference) |
|
||||||
| [PROJECT_TREE.md](website/PROJECT_TREE.md) | Auto-generated snapshot of the repo's tracked file layout |
|
| [PROJECT_TREE.md](website/PROJECT_TREE.md) | Auto-generated snapshot of the repo's tracked file layout |
|
||||||
|
|||||||
@@ -913,6 +913,10 @@ push, and Play (M6–M8) follow the designed app.
|
|||||||
`/public/shard/features` simply `404`s, and each consumer below falls back to exactly today's
|
`/public/shard/features` simply `404`s, and each consumer below falls back to exactly today's
|
||||||
behavior. The app declares no protocol version and never talks to the sidecar.
|
behavior. The app declares no protocol version and never talks to the sidecar.
|
||||||
|
|
||||||
|
**Both parts are built and in review:** Part 1 `RunicGateway/Android-app#30`, Part 2 (stacked on
|
||||||
|
it) `#31`. 336 unit tests pass and lint is clean on both; the on-device five-rung walk below is
|
||||||
|
the remaining gate, and it is what the cutover is actually waiting on.
|
||||||
|
|
||||||
- **Part 1 — the visibility rules + the read-model adds.** The security-shaped half, reviewed on
|
- **Part 1 — the visibility rules + the read-model adds.** The security-shaped half, reviewed on
|
||||||
its own:
|
its own:
|
||||||
- `GET /public/shard/features` → `{ level, features[] }`: the features **this caller** may reach.
|
- `GET /public/shard/features` → `{ level, features[] }`: the features **this caller** may reach.
|
||||||
@@ -970,22 +974,14 @@ push, and Play (M6–M8) follow the designed app.
|
|||||||
(`atlas`). Note the path: `/public/atlas`, **not** `/public/shard` — the atlas is static shard
|
(`atlas`). Note the path: `/public/atlas`, **not** `/public/shard` — the atlas is static shard
|
||||||
*content*, not live shard *state*, and unlike `/shard/*` it **is** `siteMode`-gated like
|
*content*, not live shard *state*, and unlike `/shard/*` it **is** `siteMode`-gated like
|
||||||
`/posts` and `/wiki`, so a site in maintenance mode withholds it independently of the sidecar.
|
`/posts` and `/wiki`, so a site in maintenance mode withholds it independently of the sidecar.
|
||||||
Three traps. Two are units/naming, from `v3.md` §6.3: respawn delays are **seconds**
|
Two units/naming traps from `v3.md` §6.3: respawn delays are **seconds** throughout, and
|
||||||
throughout, and `points` is a *count* on the search route while `spawners` is the *list* on
|
`points` is a *count* on the search route while `spawners` is the *list* on the detail route.
|
||||||
the detail route. The third is a **shape**: `places` is a list of
|
|
||||||
`{facet, label, spawners, maxAlive}` **objects**, not of place-name strings — it is the
|
|
||||||
aggregate the screen exists to show ("Shrines, Isamu-Jima, Yew"), it arrives only on the
|
|
||||||
detail route, and typing it `List<String>` makes that whole route fail to decode while the
|
|
||||||
request itself returns `200`.
|
|
||||||
- **Verification** — the five-rung walk (`anonymous`, `logged_in`, `player`, `staff`, `admin`)
|
- **Verification** — the five-rung walk (`anonymous`, `logged_in`, `player`, `staff`, `admin`)
|
||||||
against a local website on the cutover branch, per
|
against a local website on the cutover branch, per
|
||||||
[`../link/v3.md`](../link/v3.md) §11 and the shard-visibility smoke harness; plus one pass with
|
[`../link/v3.md`](../link/v3.md) §11 and the shard-visibility smoke harness; plus one pass with
|
||||||
**every feature disabled** in Admin → Shard Visibility, confirming the app *hides* each surface
|
**every feature disabled** in Admin → Shard Visibility, confirming the app *hides* each surface
|
||||||
instead of erroring on it. Unit tests cover the menu filter (role × feature set), the
|
instead of erroring on it. Unit tests cover the menu filter (role × feature set), the
|
||||||
`404`/`403`/`503` mapping, and DTO decode for each new shape. **Decode tests must feed real
|
`404`/`403`/`503` mapping, and DTO decode for each new shape.
|
||||||
captured JSON**, not DTOs built in Kotlin: the fakes under `data/api/fake/` construct objects
|
|
||||||
directly, so they can never catch a wire/type mismatch — which is how the `places` shape above
|
|
||||||
shipped past a green suite.
|
|
||||||
- **Excluded**, in the same class as M10's exclusions: the admin *configuration* panels — Shard
|
- **Excluded**, in the same class as M10's exclusions: the admin *configuration* panels — Shard
|
||||||
Visibility, Spawn Atlas and Cliloc import — alongside the hero/CMS block editor, Discord-bot
|
Visibility, Spawn Atlas and Cliloc import — alongside the hero/CMS block editor, Discord-bot
|
||||||
config, uo-link config and OAuth-provider setup.
|
config, uo-link config and OAuth-provider setup.
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ android-app/
|
|||||||
│ │ │ │ │ │ │ ├── PlayerShardDto.kt
|
│ │ │ │ │ │ │ ├── PlayerShardDto.kt
|
||||||
│ │ │ │ │ │ │ ├── PostDto.kt
|
│ │ │ │ │ │ │ ├── PostDto.kt
|
||||||
│ │ │ │ │ │ │ ├── PublicDto.kt
|
│ │ │ │ │ │ │ ├── PublicDto.kt
|
||||||
│ │ │ │ │ │ │ ├── ShardContentDto.kt
|
|
||||||
│ │ │ │ │ │ │ ├── ShardDto.kt
|
│ │ │ │ │ │ │ ├── ShardDto.kt
|
||||||
│ │ │ │ │ │ │ ├── SsoDto.kt
|
│ │ │ │ │ │ │ ├── SsoDto.kt
|
||||||
│ │ │ │ │ │ │ └── WikiDto.kt
|
│ │ │ │ │ │ │ └── WikiDto.kt
|
||||||
@@ -107,7 +106,6 @@ android-app/
|
|||||||
│ │ │ │ │ ├── NotificationsRepository.kt
|
│ │ │ │ │ ├── NotificationsRepository.kt
|
||||||
│ │ │ │ │ ├── PlayerShardRepository.kt
|
│ │ │ │ │ ├── PlayerShardRepository.kt
|
||||||
│ │ │ │ │ ├── SettingsRepository.kt
|
│ │ │ │ │ ├── SettingsRepository.kt
|
||||||
│ │ │ │ │ ├── ShardFeaturesRepository.kt
|
|
||||||
│ │ │ │ │ ├── ShardRepository.kt
|
│ │ │ │ │ ├── ShardRepository.kt
|
||||||
│ │ │ │ │ └── WikiRepository.kt
|
│ │ │ │ │ └── WikiRepository.kt
|
||||||
│ │ │ │ ├── di/
|
│ │ │ │ ├── di/
|
||||||
@@ -173,8 +171,6 @@ android-app/
|
|||||||
│ │ │ │ │ ├── session/
|
│ │ │ │ │ ├── session/
|
||||||
│ │ │ │ │ │ └── SessionViewModel.kt
|
│ │ │ │ │ │ └── SessionViewModel.kt
|
||||||
│ │ │ │ │ ├── shard/
|
│ │ │ │ │ ├── shard/
|
||||||
│ │ │ │ │ │ ├── AtlasScreen.kt
|
|
||||||
│ │ │ │ │ │ ├── AtlasViewModel.kt
|
|
||||||
│ │ │ │ │ │ ├── ChampsScreen.kt
|
│ │ │ │ │ │ ├── ChampsScreen.kt
|
||||||
│ │ │ │ │ │ ├── ChampsViewModel.kt
|
│ │ │ │ │ │ ├── ChampsViewModel.kt
|
||||||
│ │ │ │ │ │ ├── FrameFields.kt
|
│ │ │ │ │ │ ├── FrameFields.kt
|
||||||
@@ -184,13 +180,7 @@ android-app/
|
|||||||
│ │ │ │ │ │ ├── GuildsViewModel.kt
|
│ │ │ │ │ │ ├── GuildsViewModel.kt
|
||||||
│ │ │ │ │ │ ├── HousesScreen.kt
|
│ │ │ │ │ │ ├── HousesScreen.kt
|
||||||
│ │ │ │ │ │ ├── HousesViewModel.kt
|
│ │ │ │ │ │ ├── HousesViewModel.kt
|
||||||
│ │ │ │ │ │ ├── LeaderboardsScreen.kt
|
|
||||||
│ │ │ │ │ │ ├── LeaderboardsViewModel.kt
|
|
||||||
│ │ │ │ │ │ ├── LiveBoard.kt
|
│ │ │ │ │ │ ├── LiveBoard.kt
|
||||||
│ │ │ │ │ │ ├── MarketScreen.kt
|
|
||||||
│ │ │ │ │ │ ├── MarketViewModel.kt
|
|
||||||
│ │ │ │ │ │ ├── RulesScreen.kt
|
|
||||||
│ │ │ │ │ │ ├── RulesViewModel.kt
|
|
||||||
│ │ │ │ │ │ ├── ShardComponents.kt
|
│ │ │ │ │ │ ├── ShardComponents.kt
|
||||||
│ │ │ │ │ │ ├── ShardEventText.kt
|
│ │ │ │ │ │ ├── ShardEventText.kt
|
||||||
│ │ │ │ │ │ ├── ShardScreen.kt
|
│ │ │ │ │ │ ├── ShardScreen.kt
|
||||||
@@ -294,7 +284,6 @@ android-app/
|
|||||||
│ │ │ │ │ ├── PlayerShardDtoTest.kt
|
│ │ │ │ │ ├── PlayerShardDtoTest.kt
|
||||||
│ │ │ │ │ ├── PublicDtoTest.kt
|
│ │ │ │ │ ├── PublicDtoTest.kt
|
||||||
│ │ │ │ │ ├── ShardBoardDtoTest.kt
|
│ │ │ │ │ ├── ShardBoardDtoTest.kt
|
||||||
│ │ │ │ │ ├── ShardContentDtoTest.kt
|
|
||||||
│ │ │ │ │ ├── ShardDtoTest.kt
|
│ │ │ │ │ ├── ShardDtoTest.kt
|
||||||
│ │ │ │ │ ├── SsoDtoTest.kt
|
│ │ │ │ │ ├── SsoDtoTest.kt
|
||||||
│ │ │ │ │ └── WikiDtoTest.kt
|
│ │ │ │ │ └── WikiDtoTest.kt
|
||||||
@@ -305,8 +294,7 @@ android-app/
|
|||||||
│ │ │ │ └── FakeShardStream.kt
|
│ │ │ │ └── FakeShardStream.kt
|
||||||
│ │ │ └── repository/
|
│ │ │ └── repository/
|
||||||
│ │ │ ├── AccountTrustedDevicesTest.kt
|
│ │ │ ├── AccountTrustedDevicesTest.kt
|
||||||
│ │ │ ├── ConnectionVersionGuardTest.kt
|
│ │ │ └── ConnectionVersionGuardTest.kt
|
||||||
│ │ │ └── ShardFeaturesRepositoryTest.kt
|
|
||||||
│ │ ├── ui/
|
│ │ ├── ui/
|
||||||
│ │ │ ├── admin/
|
│ │ │ ├── admin/
|
||||||
│ │ │ │ ├── AdminContentViewModelTest.kt
|
│ │ │ │ ├── AdminContentViewModelTest.kt
|
||||||
@@ -316,8 +304,7 @@ android-app/
|
|||||||
│ │ │ ├── contact/
|
│ │ │ ├── contact/
|
||||||
│ │ │ │ └── ContactViewModelTest.kt
|
│ │ │ │ └── ContactViewModelTest.kt
|
||||||
│ │ │ ├── navigation/
|
│ │ │ ├── navigation/
|
||||||
│ │ │ │ ├── MenuAccessTest.kt
|
│ │ │ │ └── MenuAccessTest.kt
|
||||||
│ │ │ │ └── MenuFeatureGatingTest.kt
|
|
||||||
│ │ │ ├── notifications/
|
│ │ │ ├── notifications/
|
||||||
│ │ │ │ └── NotificationRoutingTest.kt
|
│ │ │ │ └── NotificationRoutingTest.kt
|
||||||
│ │ │ ├── player/
|
│ │ │ ├── player/
|
||||||
@@ -328,8 +315,6 @@ android-app/
|
|||||||
│ │ │ │ ├── FrameFieldsTest.kt
|
│ │ │ │ ├── FrameFieldsTest.kt
|
||||||
│ │ │ │ ├── LiveBoardTest.kt
|
│ │ │ │ ├── LiveBoardTest.kt
|
||||||
│ │ │ │ ├── ShardBoardViewModelTest.kt
|
│ │ │ │ ├── ShardBoardViewModelTest.kt
|
||||||
│ │ │ │ ├── ShardContentHelpersTest.kt
|
|
||||||
│ │ │ │ ├── ShardContentViewModelTest.kt
|
|
||||||
│ │ │ │ └── ShardEventTextTest.kt
|
│ │ │ │ └── ShardEventTextTest.kt
|
||||||
│ │ │ ├── theme/
|
│ │ │ ├── theme/
|
||||||
│ │ │ │ └── BrandColorTest.kt
|
│ │ │ │ └── BrandColorTest.kt
|
||||||
|
|||||||
@@ -1,376 +0,0 @@
|
|||||||
# Runic Gateway Installer — plan
|
|
||||||
|
|
||||||
Status: **planning**. No installer code exists yet. This document is the design of record; it
|
|
||||||
supersedes the informal overview it grew out of, which described a ServUO integration that does not
|
|
||||||
match how `servuo-plugins` actually ships (see [Corrections](#corrections-to-the-original-overview)).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Purpose
|
|
||||||
|
|
||||||
Take a stock ServUO installation and configure it for Runic Gateway with minimal manual steps, while
|
|
||||||
keeping the components separated and independently maintainable.
|
|
||||||
|
|
||||||
The installer handles environment detection, ServUO overlay deployment, the optional stock-file
|
|
||||||
patch tier, uo-link installation and service registration, version tracking, diagnostics, and
|
|
||||||
updates from Gitea releases.
|
|
||||||
|
|
||||||
**It is a deployment tool, not a hosted bootstrapper.** There is no `curl | bash`, no installer
|
|
||||||
service, and no hosted bootstrap script. Artifacts are downloaded from a Gitea release page and run.
|
|
||||||
|
|
||||||
**It does not replace ServUO startup behavior.** ServUO keeps running through its existing
|
|
||||||
release/start scripts. The installer never writes a launcher.
|
|
||||||
|
|
||||||
### Decisions locked
|
|
||||||
|
|
||||||
| Question | Decision |
|
|
||||||
|---|---|
|
|
||||||
| Audience | **Public** — any ServUO operator, not just shards we run |
|
|
||||||
| Code signing | **Unsigned.** `SHA256SUMS` is the trust anchor; SmartScreen/Gatekeeper warnings are expected and documented, as with most self-hosted tooling |
|
|
||||||
| Language | **Rust** — single static binary per OS, reuses the cross-compile pattern already proven in `link/.gitea/workflows/release.yml` |
|
|
||||||
| Plugin source | **Release tarball artifact** — no git and no Gitea credentials on the shard host |
|
|
||||||
| Token handoff | **Print token + prefilled admin URL** at the end of the run |
|
|
||||||
| Repo | **New repo**, `RunicGateway/installer`. It deploys *both* other components, so living inside `link/` would invert the dependency |
|
|
||||||
| ServUO version | **Warn and skip.** Patches are verified against stock 57.4 only; on anything else the base install proceeds and the patch tier is skipped with a warning. Forks are the norm in a public audience — refusing outright would block most operators |
|
|
||||||
| Uninstall | **Never touches the ServUO tree.** Removes uo-link and its service entry, then *prints* the overlay files to delete and the patch hunks to revert. Reverting is the operator's call |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Corrections to the original overview
|
|
||||||
|
|
||||||
These are not wording nits — each one changes what the installer has to do.
|
|
||||||
|
|
||||||
### 2.1 There is no `RunicGateway.dll` and no `Plugins/` directory
|
|
||||||
|
|
||||||
The plugin ships as **C# source** and ServUO compiles it at boot. The real deployable is
|
|
||||||
`servuo-plugins/overlay/`, which mirrors the server root:
|
|
||||||
|
|
||||||
```
|
|
||||||
overlay/
|
|
||||||
├── Config/Bridge.cfg
|
|
||||||
└── Scripts/
|
|
||||||
├── Scripts.csproj # Phase 0 — whole-file overwrite of a stock file
|
|
||||||
└── Custom/Bridge/*.cs # 22 files
|
|
||||||
```
|
|
||||||
|
|
||||||
So the plugin step is a hash-compare file sync, not a DLL drop — mechanically easier than the
|
|
||||||
overview assumed. The sting is that **a successful copy does not mean a working bridge.** Per
|
|
||||||
`link/SHARD_PREREQS.md`, `ScriptCompiler.Compile()` shells out to `dotnet build`, prints the output,
|
|
||||||
**ignores the exit code**, and reloads the existing `Scripts.dll`. A broken script build is
|
|
||||||
invisible: the shard boots clean on stale code. Diagnostics must therefore verify *post-boot* state,
|
|
||||||
never treat "files copied" as success.
|
|
||||||
|
|
||||||
### 2.2 Stock ServUO files *are* modified — by an optional tier
|
|
||||||
|
|
||||||
`servuo-plugins/patches/` holds unified diffs against stock ServUO 57.4, plus two `.cs` files that
|
|
||||||
can only be copied *after* their patch lands (they reference symbols the patch introduces):
|
|
||||||
|
|
||||||
| Patch | Target | Companion file | Rebuild required |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `playervendor-sale-eventsink.patch` | `Server/EventSink.cs` | `BridgeVendorSale.cs` | **Core** — `dotnet build ServUO.sln`; the dynamic script build is not enough |
|
|
||||||
| `playervendor-sale-gump.patch` | `Scripts/Gumps/PlayerVendorGumps.cs` | (same unit as above) | script build |
|
|
||||||
| `commandlogging-event.patch` | `Scripts/Commands/Logging.cs` | `BridgeModerationAudit.cs` | script build |
|
|
||||||
|
|
||||||
Plus `overlay/Scripts/Scripts.csproj`, which overwrites a stock file (Phase 0 — it fixes the silent
|
|
||||||
ServUO build bug above).
|
|
||||||
|
|
||||||
This is the hardest part of the installer. `git apply` against a hand-modified shard will fail, and
|
|
||||||
most real shards are hand-modified. Therefore:
|
|
||||||
|
|
||||||
- The patch tier is **opt-in and skippable**. The base install must complete without it.
|
|
||||||
- Always dry-run (`git apply --check`) before applying, and report per-patch.
|
|
||||||
- When skipped or failed, say plainly what is lost: **no `vendor.sale` events, no in-game moderation
|
|
||||||
audit forwarding**.
|
|
||||||
- The `EventSink.cs` patch must warn loudly that a **core solution rebuild** is required, not just a
|
|
||||||
shard restart.
|
|
||||||
- On any ServUO version other than stock **57.4**, skip the whole tier with a warning and continue
|
|
||||||
with the base install. Do not attempt to apply unverified diffs to an unknown tree.
|
|
||||||
- Record applied patches in `install.json`, **and cache the applied `.patch` files** next to it
|
|
||||||
(`/etc/runicgateway/patches/`, `%ProgramData%\RunicGateway\patches\`). Re-runs stay idempotent,
|
|
||||||
and uninstall can print the exact hunks offline long after the release tarball is gone (§5,
|
|
||||||
Phase 4).
|
|
||||||
|
|
||||||
### 2.3 Config paths collide with what the sidecar actually reads
|
|
||||||
|
|
||||||
The sidecar reads `$UOLINK_CONFIG`, else `sidecar.toml` in the **working directory**
|
|
||||||
(`link/sidecar/src/config.rs`), with keys `[shard].bind`, `[web].bind`, `[web].auth_token`,
|
|
||||||
`[store].path`. The overview proposed a `config.toml` with `[updates]`, `[link]`, `[servuo]` — keys
|
|
||||||
the sidecar cannot read.
|
|
||||||
|
|
||||||
Two files, two owners:
|
|
||||||
|
|
||||||
| File | Owner | Contents |
|
|
||||||
|---|---|---|
|
|
||||||
| `/etc/runicgateway/sidecar.toml` | uo-link | The sidecar's own schema, unchanged. Service sets `UOLINK_CONFIG` to this path |
|
|
||||||
| `/etc/runicgateway/install.json` | installer | Deployed versions, file hashes, applied patches, ServUO path, timestamps |
|
|
||||||
|
|
||||||
**Working-directory trap:** the sidecar writes both `sidecar.toml` and `uo-link.db` relative to CWD.
|
|
||||||
Under `C:\Program Files\` that fails or silently lands in VirtualStore. The service definitions must
|
|
||||||
pin `UOLINK_CONFIG` and `UOLINK_DB_PATH` explicitly:
|
|
||||||
|
|
||||||
- Linux: config `/etc/runicgateway/sidecar.toml`, db `/var/lib/runicgateway/uo-link.db`, dedicated
|
|
||||||
service user
|
|
||||||
- Windows: binary under `%ProgramFiles%\RunicGateway\`, **data under `%ProgramData%\RunicGateway\`**
|
|
||||||
|
|
||||||
### 2.4 The token handoff was missing entirely
|
|
||||||
|
|
||||||
The whole point is the website reaching the sidecar, and today that is manual and undocumented in
|
|
||||||
the install flow: the sidecar generates a token on first run and logs it, then a human pastes base
|
|
||||||
URL, WS URL, token, and protocol version into Admin → Shard, where it is AES-GCM encrypted and
|
|
||||||
becomes write-only. This is the largest "I installed it and nothing happened" failure mode.
|
|
||||||
|
|
||||||
The installer closes it by printing a copy-paste block at the end of a successful run — see §6.
|
|
||||||
|
|
||||||
### 2.5 `deploy.ps1` cannot be the cross-platform deployer
|
|
||||||
|
|
||||||
It is PowerShell-only; a Linux ServUO host running .NET typically has no `pwsh`. It also hard-throws
|
|
||||||
when the ServUO process is running — correct behavior, and the installer must inherit it (detect and
|
|
||||||
refuse, rather than corrupt a live `Scripts.dll`). The installer reimplements the sync natively; it
|
|
||||||
is a short hash-compare-and-copy that never deletes.
|
|
||||||
|
|
||||||
`deploy.ps1` **stays** in `servuo-plugins` as the developer-facing tool. The installer is for
|
|
||||||
operators.
|
|
||||||
|
|
||||||
### 2.6 Prerequisites the overview assumed away
|
|
||||||
|
|
||||||
- **`servuo-plugins` has no release workflow.** Only `link` does. "Pull latest repository" is
|
|
||||||
replaced by a release tarball, which has to be built first (Phase 0).
|
|
||||||
- **arm64 is not buildable today.** `link/release.yml` cross-compiles only
|
|
||||||
`x86_64-unknown-linux-gnu` and `x86_64-pc-windows-gnu`. An arm64 `.deb` needs another cross
|
|
||||||
toolchain.
|
|
||||||
- **The compat matrix has no home.** `PROTOCOL_VERSION` currently lives only in
|
|
||||||
`link/sidecar/src/main.rs`. The sidecar publishes it via `X-UOLink-Version` and `/health`, and the
|
|
||||||
website stores an expected value — but the *plugin's* protocol version is not queryable before
|
|
||||||
boot. See §7.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Distribution model
|
|
||||||
|
|
||||||
Components are published as Gitea release artifacts. Operators download from the release page
|
|
||||||
(browser, `curl`/`wget`, or `scp` to the server) and run the binary.
|
|
||||||
|
|
||||||
```
|
|
||||||
Runic Gateway Installer v1.0.0
|
|
||||||
├── runicgateway-installer-windows-x86_64.exe
|
|
||||||
├── runicgateway-installer-linux-x86_64
|
|
||||||
└── SHA256SUMS
|
|
||||||
|
|
||||||
uo-link v3.x.y (existing release, extended)
|
|
||||||
├── uo-link-sidecar-windows-x86_64.exe
|
|
||||||
├── uo-link-sidecar-linux-x86_64
|
|
||||||
├── runicgateway-link_<ver>_amd64.deb (Phase 5)
|
|
||||||
└── SHA256SUMS
|
|
||||||
|
|
||||||
servuo-plugins v<ver> (new release, Phase 0)
|
|
||||||
├── runicgateway-overlay-<ver>.tar.gz # overlay/ + patches/ + manifest.json
|
|
||||||
└── SHA256SUMS
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scp runicgateway-installer-linux-x86_64 user@server:/tmp/
|
|
||||||
chmod +x runicgateway-installer-linux-x86_64
|
|
||||||
sudo ./runicgateway-installer-linux-x86_64
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unsigned-binary posture
|
|
||||||
|
|
||||||
Because releases are unsigned, trust is anchored on checksums and the operator's own verification.
|
|
||||||
The docs must state this up front rather than let users discover it as a scary dialog:
|
|
||||||
|
|
||||||
- Every release publishes `SHA256SUMS`; the install docs lead with the verification command for both
|
|
||||||
OSes.
|
|
||||||
- Windows will show a SmartScreen "unrecognized app" prompt. Documented, with the exact click path.
|
|
||||||
- The installer verifies the SHA256 of everything **it** downloads (overlay tarball, sidecar binary)
|
|
||||||
against the release's `SHA256SUMS` and refuses on mismatch. Self-verification is not optional just
|
|
||||||
because the installer itself is unsigned.
|
|
||||||
- Revisit signing if it ever becomes affordable; the release layout should not have to change.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Component architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
Runic Gateway Installer (Rust, one binary per OS)
|
|
||||||
│
|
|
||||||
┌───────────────┴────────────────┐
|
|
||||||
▼ ▼
|
|
||||||
ServUO integration uo-link
|
|
||||||
│ │
|
|
||||||
┌────────┴────────┐ ┌────────┴────────┐
|
|
||||||
▼ ▼ ▼ ▼
|
|
||||||
overlay sync patch tier (opt-in) binary install service registration
|
|
||||||
(never deletes) (git apply + guard) + config + data (systemd / Windows SCM)
|
|
||||||
```
|
|
||||||
|
|
||||||
Each component keeps its own lifecycle. ServUO's existing startup process is untouched.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Phases
|
|
||||||
|
|
||||||
### Phase 0 — prerequisites (no installer code)
|
|
||||||
|
|
||||||
Repo work that must land before an installer can exist.
|
|
||||||
|
|
||||||
1. **`servuo-plugins`: add `.gitea/workflows/release.yml`.** Retarget the release *engine* half of
|
|
||||||
`link/release.yml` (its header comment explicitly anticipates this — the plan/release steps
|
|
||||||
consume only `{version, changelog, artifacts}`). The adapter half produces
|
|
||||||
`runicgateway-overlay-<ver>.tar.gz` containing `overlay/`, `patches/`, and a `manifest.json`
|
|
||||||
(version, commit, per-file SHA256, declared protocol version, minimum ServUO version).
|
|
||||||
2. **`link`: make the sidecar installable.** Confirm/settle default data paths, and add a way to
|
|
||||||
read back config non-interactively (e.g. `--print-config` emitting JSON: bind addresses, token,
|
|
||||||
protocol version, db path) so the installer does not have to scrape logs for the token.
|
|
||||||
3. **Decide and document the compat matrix format** (§7).
|
|
||||||
4. **`docs`: this file, plus `docs/installer/INSTALL.md`** (the operator-facing guide) once the
|
|
||||||
shape is settled.
|
|
||||||
|
|
||||||
### Phase 1 — installer core
|
|
||||||
|
|
||||||
- ServUO root detection and validation (`ServUO.exe`, `Scripts/`, `Config/`), with version detection
|
|
||||||
and an explicit refusal when the ServUO process is running.
|
|
||||||
- Overlay sync: fetch tarball → verify SHA256 → hash-compare against the server tree → add/change,
|
|
||||||
**never delete**. Port of `deploy.ps1` semantics including its `-Verify` dry run (`--verify`).
|
|
||||||
- Write `install.json`: component, version, source commit, per-file hashes, applied patches,
|
|
||||||
timestamp.
|
|
||||||
- Idempotent re-runs; a second run with no upstream change reports "unchanged" and writes nothing.
|
|
||||||
|
|
||||||
### Phase 2 — uo-link install and service
|
|
||||||
|
|
||||||
- Linux: binary → `/usr/bin/runicgateway-link`, config → `/etc/runicgateway/sidecar.toml`, db →
|
|
||||||
`/var/lib/runicgateway/`, systemd unit with a dedicated user, `enable` + `start`.
|
|
||||||
- Windows: `%ProgramFiles%\RunicGateway\`, data in `%ProgramData%\RunicGateway\`, service
|
|
||||||
registration with automatic start and restart-on-failure.
|
|
||||||
- Both: `UOLINK_CONFIG` and `UOLINK_DB_PATH` pinned in the service definition (§2.3).
|
|
||||||
- Token surfacing (§6).
|
|
||||||
|
|
||||||
### Phase 3 — patch tier (opt-in)
|
|
||||||
|
|
||||||
Everything in §2.2. Detect applicability, dry-run, apply, record, warn about the core rebuild, and
|
|
||||||
degrade loudly rather than silently.
|
|
||||||
|
|
||||||
### Phase 4 — diagnostics and updates
|
|
||||||
|
|
||||||
`runicgateway doctor` — the command that makes the whole thing supportable:
|
|
||||||
|
|
||||||
```
|
|
||||||
✓ ServUO found /opt/ServUO (57.4)
|
|
||||||
✓ Overlay in sync 23 files, all hashes match install.json
|
|
||||||
⚠ Patch tier 1 of 3 applied — vendor.sale unavailable
|
|
||||||
✓ uo-link installed 3.0.1
|
|
||||||
✓ Service running, enabled
|
|
||||||
✓ Sidecar reachable 127.0.0.1:8080 /health ok
|
|
||||||
✓ Protocol sidecar 3 = overlay manifest 3
|
|
||||||
✗ Shard connected no shard has dialed in since boot
|
|
||||||
```
|
|
||||||
|
|
||||||
The last check matters most: it is the only thing that distinguishes "files copied" from "the bridge
|
|
||||||
actually works" (§2.1).
|
|
||||||
|
|
||||||
`runicgateway update` — asymmetric by component, deliberately:
|
|
||||||
|
|
||||||
- **uo-link**: query the Gitea releases API → compare versions → download → verify checksum →
|
|
||||||
replace binary → restart service.
|
|
||||||
- **plugin overlay**: download the newer overlay tarball → verify → re-sync → record commit → tell
|
|
||||||
the operator ServUO must restart (the installer does not restart the shard).
|
|
||||||
|
|
||||||
`runicgateway uninstall` — **removes only what it exclusively owns, and never edits the ServUO
|
|
||||||
tree.** The installer cannot know what the operator has changed in those files since deployment, so
|
|
||||||
a clever automatic revert risks silently eating their work. It removes and it reports:
|
|
||||||
|
|
||||||
| Action | Scope |
|
|
||||||
|---|---|
|
|
||||||
| Removed | uo-link binary, its service entry (systemd unit / Windows service), `install.json` and the cached patch set |
|
|
||||||
| Kept | `sidecar.toml` and `uo-link.db` (config and history survive; `--purge` to drop them) |
|
|
||||||
| **Printed, not done** | Every overlay file deployed into the ServUO tree, listed by path, for the operator to delete |
|
|
||||||
| **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs`, `Logging.cs`, rendered from the cached `.patch` files, for the operator to revert by hand |
|
|
||||||
|
|
||||||
The printed report is also written to a file, so it survives the terminal scrollback of a long
|
|
||||||
uninstall.
|
|
||||||
|
|
||||||
### Phase 5 — packaging polish
|
|
||||||
|
|
||||||
`.deb` packaging, Windows MSI, arm64 cross build, and optional automated backup before upgrade.
|
|
||||||
Deliberately last: v1 can register services directly (`sc create` / a written systemd unit) and ship
|
|
||||||
plain binaries. Nothing in Phases 1–4 should have to change to add these.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Token handoff (the end of a successful run)
|
|
||||||
|
|
||||||
```
|
|
||||||
Runic Gateway is installed.
|
|
||||||
|
|
||||||
One manual step remains — connect the website to this sidecar:
|
|
||||||
|
|
||||||
Base URL http://<this-host>:8080
|
|
||||||
WebSocket URL ws://<this-host>:8080/ws
|
|
||||||
Protocol version 3
|
|
||||||
Auth token 4f9c... (also in /etc/runicgateway/sidecar.toml)
|
|
||||||
|
|
||||||
Paste these into Admin → Shard on your Runic Gateway site:
|
|
||||||
https://<your-site>/admin/shard
|
|
||||||
|
|
||||||
The token is write-only once saved — the site will never show it back to you.
|
|
||||||
```
|
|
||||||
|
|
||||||
The installer prompts for the site URL only to build that link; it never contacts the website. A
|
|
||||||
future "installer registers itself with the website" flow (claim code + authenticated endpoint) is
|
|
||||||
explicitly **out of scope** — it is real backend work in a security-sensitive area and can be added
|
|
||||||
later without changing anything here.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Version tracking and the compat matrix
|
|
||||||
|
|
||||||
Three components version independently, bound by a protocol contract:
|
|
||||||
|
|
||||||
- **sidecar** — `PROTOCOL_VERSION` in `link/sidecar/src/main.rs`, exposed on `/health` and as
|
|
||||||
`X-UOLink-Version` on every response; a mismatch is rejected `409`.
|
|
||||||
- **website** — stores an expected protocol version in `uoLinkConfig` (admin-managed).
|
|
||||||
- **plugin overlay** — has no queryable version before ServUO boots. The overlay release
|
|
||||||
`manifest.json` declares it, and `install.json` records what was deployed. `doctor` compares the
|
|
||||||
recorded overlay protocol version against the sidecar's live one.
|
|
||||||
|
|
||||||
**Open risk:** the v3 cutover is mid-flight — protocol work landed on `edge` branches with the
|
|
||||||
`edge → main` cutover still open across four repos. Until that lands, `main` and `edge` disagree
|
|
||||||
about `PROTOCOL_VERSION`, so the installer must not hardcode a version anywhere; it reads what the
|
|
||||||
artifacts declare. See `docs/link/v3.md`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Open questions
|
|
||||||
|
|
||||||
1. **Windows service mechanism** — `sc create` against the plain console binary (simplest, works
|
|
||||||
today), a bundled WinSW/NSSM shim, or a native `--service` mode in the sidecar using the
|
|
||||||
`windows-service` crate (cleanest, but changes `link`). Recommendation: `sc create` for v1,
|
|
||||||
revisit if restart semantics prove inadequate.
|
|
||||||
2. **Does the installer manage ServUO stop/start?** Currently it refuses while ServUO runs and tells
|
|
||||||
the operator to restart afterward. Offering to stop/start would be friendlier but means owning
|
|
||||||
another shard's process lifecycle, and the shard's own start scripts vary.
|
|
||||||
3. **Co-location assumption** — the shard dials out to the sidecar on loopback `127.0.0.1:7788`, so
|
|
||||||
sidecar and ServUO must share a host. Should the installer support installing only uo-link on a
|
|
||||||
different host, or hard-assume co-location?
|
|
||||||
4. **Branch targeting for the new repo** — `link`, `website`, `servuo-plugins` and `docs` are
|
|
||||||
mid-cutover between `edge` and `main`. The installer repo starts clean on `main`; the Phase 0
|
|
||||||
`servuo-plugins` release workflow needs a target branch decision.
|
|
||||||
|
|
||||||
Resolved and moved into §1 / §2.2 / §5: uninstall scope, and minimum ServUO version.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Administrator experience
|
|
||||||
|
|
||||||
Before:
|
|
||||||
|
|
||||||
```
|
|
||||||
find plugins → copy files → edit ServUO → download bridge → start bridge
|
|
||||||
→ configure startup → find the token → troubleshoot paths
|
|
||||||
```
|
|
||||||
|
|
||||||
After:
|
|
||||||
|
|
||||||
```
|
|
||||||
download artifact → verify checksum → run installer → select ServUO directory
|
|
||||||
→ install components → paste 4 values into Admin → Shard → start ServUO normally
|
|
||||||
```
|
|
||||||
@@ -18,7 +18,6 @@ link/
|
|||||||
│ ├── scripts/
|
│ ├── scripts/
|
||||||
│ │ └── gen_tree.py
|
│ │ └── gen_tree.py
|
||||||
│ ├── workflows/
|
│ ├── workflows/
|
||||||
│ │ ├── pr-checks.yml
|
|
||||||
│ │ ├── release.yml
|
│ │ ├── release.yml
|
||||||
│ │ ├── sonarqube.yml
|
│ │ ├── sonarqube.yml
|
||||||
│ │ └── sync-project-tree.yml
|
│ │ └── sync-project-tree.yml
|
||||||
|
|||||||
36
link/v3.md
36
link/v3.md
@@ -350,11 +350,8 @@ Sidecar — `store.rs`: singleton `ruleset(id CHECK(id=1), rev, json, updated_t)
|
|||||||
`main.rs`: new arm in the board-projection match; `web.rs`: `GET /ruleset` served from the store, so
|
`main.rs`: new arm in the board-projection match; `web.rs`: `GET /ruleset` served from the store, so
|
||||||
it answers during a shard outage (`PROTOCOL_2.md` §12.2).
|
it answers during a shard outage (`PROTOCOL_2.md` §12.2).
|
||||||
|
|
||||||
Website — `uoLinkClient.getRuleset()`; `uoLinkSocket.backfill()` (object-shaped, so it cannot use the
|
Website — `uoLinkClient.getRuleset()`; `uoLinkSocket.backfill()` (object-shaped, so follow the
|
||||||
array-only `snapshot()` helper — but it **must still go through `shardIngest.ingest()`**, as
|
`getPresence()` block's explicit form, not the array-only `snapshot()` helper); `shardIngest.js` →
|
||||||
`ingestEach` does, rather than calling `shardState.setRuleset` directly: the two arrival orders have
|
|
||||||
to produce the same stored frame, and a direct call quietly made backfill a second writer that
|
|
||||||
skipped the normalization below); `shardIngest.js` →
|
|
||||||
`shardState.setRuleset`, **not** in `LOGGED_KINDS` (it re-arrives every reconnect and `server.hello`
|
`shardState.setRuleset`, **not** in `LOGGED_KINDS` (it re-arrives every reconnect and `server.hello`
|
||||||
already marks those); `KIND_FEATURE['world.ruleset'] = 'ruleset'`; `shard_ruleset` singleton table
|
already marks those); `KIND_FEATURE['world.ruleset'] = 'ruleset'`; `shard_ruleset` singleton table
|
||||||
(`rev`, `expansion`, `payload JSON`, `t`); `GET /public/shard/ruleset` behind
|
(`rev`, `expansion`, `payload JSON`, `t`); `GET /public/shard/ruleset` behind
|
||||||
@@ -363,17 +360,6 @@ already marks those); `KIND_FEATURE['world.ruleset'] = 'ruleset'`; `shard_rulese
|
|||||||
Client — NEW `routes/public/Rules.jsx` at `/site/rules`, alongside
|
Client — NEW `routes/public/Rules.jsx` at `/site/rules`, alongside
|
||||||
`/site/champs|guilds|governors|houses`; live via `useShardFeed({ filter: new Set(['world.ruleset']) })`.
|
`/site/champs|guilds|governors|houses`; live via `useShardFeed({ filter: new Set(['world.ruleset']) })`.
|
||||||
|
|
||||||
**The `shard` field falls back to the instance's own name.** ServUO ships `Server.cfg` with
|
|
||||||
`Name=My Shard`, so an operator who never edited it publishes that verbatim — which is the shard
|
|
||||||
saying *unnamed*, not naming anything, and the rules page then reads "My Shard" under a header
|
|
||||||
carrying the real one. `shardIngest` substitutes `settings.getInstanceName()` (the admin-editable
|
|
||||||
site title, else `BRAND_NAME` — the same resolution `getPublic().brand.name` uses, so one install
|
|
||||||
never shows two names) when `shard` is absent, blank, or exactly the stock default, matched
|
|
||||||
case-insensitively and trim-tolerantly but only as a **whole** value: a shard genuinely called
|
|
||||||
*"My Shard Reborn"* has named itself and keeps it. Applied at **ingest**, not on read, because the
|
|
||||||
ruleset is also broadcast live — the same object goes to the SSE fan-out, so a read-time
|
|
||||||
substitution would be undone by the next reconnect's frame.
|
|
||||||
|
|
||||||
### 5.4 Risk
|
### 5.4 Risk
|
||||||
|
|
||||||
Perf is nil (~3 KB per connect). The only real risk is publishing a secret, mitigated by the explicit
|
Perf is nil (~3 KB per connect). The only real risk is publishing a secret, mitigated by the explicit
|
||||||
@@ -652,15 +638,6 @@ Client — NEW `routes/public/Leaderboards.jsx` at `/site/leaderboards`; a "Loya
|
|||||||
added to `components/CharacterSheet.jsx`, one edit serving both `PlayerCharacter.jsx` and
|
added to `components/CharacterSheet.jsx`, one edit serving both `PlayerCharacter.jsx` and
|
||||||
`AdminCharacter.jsx`.
|
`AdminCharacter.jsx`.
|
||||||
|
|
||||||
**An unscored board still renders a row.** Most systems on a young shard have `top: []`, and a page
|
|
||||||
of blank cards reads as broken rather than as new — so a board with no entries shows a single
|
|
||||||
placeholder bearing the **instance's own name** with an em dash where a score goes, above the
|
|
||||||
existing "nobody has earned points here yet" line. It is deliberately **not** shaped like an entry —
|
|
||||||
no rank, no medal, no bar, muted — because a placeholder that looked like a real standing would be a
|
|
||||||
fabricated one; the first real entry replaces it outright. Purely presentational: the API keeps
|
|
||||||
sending an empty `top`, so no consumer ever receives an invented row. Web and app render it the same
|
|
||||||
way (`Leaderboards.jsx`, `LeaderboardsScreen.kt`).
|
|
||||||
|
|
||||||
### 7.5 What the run against a real shard changed
|
### 7.5 What the run against a real shard changed
|
||||||
|
|
||||||
The plan above was written from reading `PointsSystem.cs`. Booting the actual shard (ServUO 57.4, a
|
The plan above was written from reading `PointsSystem.cs`. Booting the actual shard (ServUO 57.4, a
|
||||||
@@ -984,9 +961,12 @@ request rides the same authenticated OkHttp client as every other call, so an ap
|
|||||||
the same audience rung as the same account on the web; and every shard DTO in the app is
|
the same audience rung as the same account on the web; and every shard DTO in the app is
|
||||||
nullable-with-defaults, so field projection strips fields without a deserialization failure.
|
nullable-with-defaults, so field projection strips fields without a deserialization failure.
|
||||||
|
|
||||||
Scoped as **M11 in [`../android/PLAN.md`](../android/PLAN.md) §9**, two PRs (the visibility rules +
|
Scoped as **M11 in [`../android/PLAN.md`](../android/PLAN.md) §9**, two PRs: the visibility rules +
|
||||||
read-model adds, then the four screens). `edge` → `main` is held until both land, so web and app
|
read-model adds ([Android-app #30](https://gitea.whitlocktech.com/RunicGateway/Android-app/pulls/30))
|
||||||
surface the same shard on the same day. Neither PR is coupled to the merge order — on a pre-v3 website
|
and the four screens ([#31](https://gitea.whitlocktech.com/RunicGateway/Android-app/pulls/31), stacked
|
||||||
|
on it). Both are **built and in review**; the on-device five-rung walk (§11) against a website on the
|
||||||
|
cutover branch is the remaining gate. `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` `404`s and the app falls back to today's behavior — so
|
every new route and `/public/shard/features` `404`s and the app falls back to today's behavior — so
|
||||||
holding the cutover is a schedule decision, not a technical dependency.
|
holding the cutover is a schedule decision, not a technical dependency.
|
||||||
|
|
||||||
|
|||||||
@@ -44,11 +44,6 @@ they did before the table existed.
|
|||||||
|
|
||||||
## Converting
|
## Converting
|
||||||
|
|
||||||
> **Step-by-step operator instructions — where to get UOFiddler, where your
|
|
||||||
> client files are, and how to verify the import — are in
|
|
||||||
> [`UOFIDDLER.md`](UOFIDDLER.md).** This section covers the formats and the
|
|
||||||
> reasoning behind them.
|
|
||||||
|
|
||||||
Either format below is accepted; the site sniffs which one it was handed.
|
Either format below is accepted; the site sniffs which one it was handed.
|
||||||
|
|
||||||
| Format | Fidelity | Notes |
|
| Format | Fidelity | Notes |
|
||||||
@@ -82,16 +77,8 @@ dotnet run -- "<UOFiddler>/Ultima.dll" "<UO client>/Cliloc.enu" /srv/uo-data/cli
|
|||||||
dotnet run -- "<UOFiddler>/Ultima.dll" "<UO client>/Cliloc.enu" /srv/uo-data/clilocs.tsv --tsv
|
dotnet run -- "<UOFiddler>/Ultima.dll" "<UO client>/Cliloc.enu" /srv/uo-data/clilocs.tsv --tsv
|
||||||
```
|
```
|
||||||
|
|
||||||
A UOFiddler GUI export works too, but **not unmodified**: its Cliloc tab writes
|
A UOFiddler GUI export works equally well — anything producing one of the two
|
||||||
`Number;Text;Flag` — three columns, the flag *last* — and the parser reads
|
shapes above is fine.
|
||||||
`number<separator>text`, so the trailing field is absorbed into the name and
|
|
||||||
every item renders as `quarter staff;0`. Stripping it is one `sed`, given in
|
|
||||||
[`UOFIDDLER.md`](UOFIDDLER.md) §Route B.
|
|
||||||
|
|
||||||
The parser already tolerates `number,flag,text`, with the flag in the *middle*.
|
|
||||||
It is not extended to cover the trailing form because a final `;0` is
|
|
||||||
indistinguishable from a name that genuinely ends that way — a heuristic there
|
|
||||||
would corrupt real names to save the operator one command.
|
|
||||||
|
|
||||||
## Shard-added and shard-edited items
|
## Shard-added and shard-edited items
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ website/
|
|||||||
│ │ │ ├── heroLayout.js
|
│ │ │ ├── heroLayout.js
|
||||||
│ │ │ ├── shardEvents.js
|
│ │ │ ├── shardEvents.js
|
||||||
│ │ │ ├── useAsync.js
|
│ │ │ ├── useAsync.js
|
||||||
│ │ │ ├── useShardFeatures.js
|
|
||||||
│ │ │ └── useShardFeed.js
|
│ │ │ └── useShardFeed.js
|
||||||
│ │ ├── routes/
|
│ │ ├── routes/
|
||||||
│ │ │ ├── admin/
|
│ │ │ ├── admin/
|
||||||
@@ -191,8 +190,6 @@ website/
|
|||||||
│ │ │ │ │ ├── SettingsAdmin.jsx
|
│ │ │ │ │ ├── SettingsAdmin.jsx
|
||||||
│ │ │ │ │ ├── ShardAdmin.jsx
|
│ │ │ │ │ ├── ShardAdmin.jsx
|
||||||
│ │ │ │ │ ├── ShardOps.jsx
|
│ │ │ │ │ ├── ShardOps.jsx
|
||||||
│ │ │ │ │ ├── ShardVisibility.jsx
|
|
||||||
│ │ │ │ │ ├── SpawnAtlas.jsx
|
|
||||||
│ │ │ │ │ ├── UserDetail.jsx
|
│ │ │ │ │ ├── UserDetail.jsx
|
||||||
│ │ │ │ │ ├── UserEditor.jsx
|
│ │ │ │ │ ├── UserEditor.jsx
|
||||||
│ │ │ │ │ ├── UsersAdmin.jsx
|
│ │ │ │ │ ├── UsersAdmin.jsx
|
||||||
@@ -216,23 +213,17 @@ website/
|
|||||||
│ │ │ │ └── ResetPassword.jsx
|
│ │ │ │ └── ResetPassword.jsx
|
||||||
│ │ │ ├── public/
|
│ │ │ ├── public/
|
||||||
│ │ │ │ ├── About.jsx
|
│ │ │ │ ├── About.jsx
|
||||||
│ │ │ │ ├── Atlas.jsx
|
|
||||||
│ │ │ │ ├── AtlasCreature.jsx
|
|
||||||
│ │ │ │ ├── ChampSpawns.jsx
|
│ │ │ │ ├── ChampSpawns.jsx
|
||||||
│ │ │ │ ├── CmsPage.jsx
|
│ │ │ │ ├── CmsPage.jsx
|
||||||
│ │ │ │ ├── FiveOnFriday.jsx
|
│ │ │ │ ├── FiveOnFriday.jsx
|
||||||
│ │ │ │ ├── Governors.jsx
|
│ │ │ │ ├── Governors.jsx
|
||||||
│ │ │ │ ├── Guilds.jsx
|
│ │ │ │ ├── Guilds.jsx
|
||||||
│ │ │ │ ├── Houses.jsx
|
│ │ │ │ ├── Houses.jsx
|
||||||
│ │ │ │ ├── Leaderboards.jsx
|
|
||||||
│ │ │ │ ├── Maintenance.jsx
|
│ │ │ │ ├── Maintenance.jsx
|
||||||
│ │ │ │ ├── Market.jsx
|
|
||||||
│ │ │ │ ├── MarketVendor.jsx
|
|
||||||
│ │ │ │ ├── News.jsx
|
│ │ │ │ ├── News.jsx
|
||||||
│ │ │ │ ├── Newsletter.jsx
|
│ │ │ │ ├── Newsletter.jsx
|
||||||
│ │ │ │ ├── NewsletterIssue.jsx
|
│ │ │ │ ├── NewsletterIssue.jsx
|
||||||
│ │ │ │ ├── Portal.jsx
|
│ │ │ │ ├── Portal.jsx
|
||||||
│ │ │ │ ├── Rules.jsx
|
|
||||||
│ │ │ │ ├── Screenshots.jsx
|
│ │ │ │ ├── Screenshots.jsx
|
||||||
│ │ │ │ ├── Shard.jsx
|
│ │ │ │ ├── Shard.jsx
|
||||||
│ │ │ │ ├── ShardActivity.jsx
|
│ │ │ │ ├── ShardActivity.jsx
|
||||||
@@ -266,12 +257,9 @@ website/
|
|||||||
│ └── sonar-test-reporter.mjs
|
│ └── sonar-test-reporter.mjs
|
||||||
├── server/
|
├── server/
|
||||||
│ ├── db/
|
│ ├── db/
|
||||||
│ │ ├── data/
|
|
||||||
│ │ │ └── spawnAtlas.art.example.json
|
|
||||||
│ │ ├── schema.sql
|
│ │ ├── schema.sql
|
||||||
│ │ └── seed.js
|
│ │ └── seed.js
|
||||||
│ ├── scripts/
|
│ ├── scripts/
|
||||||
│ │ ├── importSpawnAtlas.js
|
|
||||||
│ │ └── routeManifest.js
|
│ │ └── routeManifest.js
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── auth/
|
│ │ ├── auth/
|
||||||
@@ -377,27 +365,15 @@ website/
|
|||||||
│ │ │ ├── settings/
|
│ │ │ ├── settings/
|
||||||
│ │ │ │ ├── settings.db.js
|
│ │ │ │ ├── settings.db.js
|
||||||
│ │ │ │ └── settings.model.js
|
│ │ │ │ └── settings.model.js
|
||||||
│ │ │ ├── shardAtlas/
|
|
||||||
│ │ │ │ ├── shardAtlas.db.js
|
|
||||||
│ │ │ │ └── shardAtlas.model.js
|
|
||||||
│ │ │ ├── shardClilocs/
|
|
||||||
│ │ │ │ ├── shardClilocs.db.js
|
|
||||||
│ │ │ │ └── shardClilocs.model.js
|
|
||||||
│ │ │ ├── shardEvents/
|
│ │ │ ├── shardEvents/
|
||||||
│ │ │ │ ├── shardEvents.db.js
|
│ │ │ │ ├── shardEvents.db.js
|
||||||
│ │ │ │ └── shardEvents.model.js
|
│ │ │ │ └── shardEvents.model.js
|
||||||
│ │ │ ├── shardLinks/
|
│ │ │ ├── shardLinks/
|
||||||
│ │ │ │ ├── shardLinks.db.js
|
│ │ │ │ ├── shardLinks.db.js
|
||||||
│ │ │ │ └── shardLinks.model.js
|
│ │ │ │ └── shardLinks.model.js
|
||||||
│ │ │ ├── shardMarket/
|
|
||||||
│ │ │ │ ├── shardMarket.db.js
|
|
||||||
│ │ │ │ └── shardMarket.model.js
|
|
||||||
│ │ │ ├── shardState/
|
│ │ │ ├── shardState/
|
||||||
│ │ │ │ ├── shardState.db.js
|
│ │ │ │ ├── shardState.db.js
|
||||||
│ │ │ │ └── shardState.model.js
|
│ │ │ │ └── shardState.model.js
|
||||||
│ │ │ ├── shardVisibility/
|
|
||||||
│ │ │ │ ├── shardVisibility.db.js
|
|
||||||
│ │ │ │ └── shardVisibility.model.js
|
|
||||||
│ │ │ ├── trustedDevices/
|
│ │ │ ├── trustedDevices/
|
||||||
│ │ │ │ ├── trustedDevices.db.js
|
│ │ │ │ ├── trustedDevices.db.js
|
||||||
│ │ │ │ └── trustedDevices.model.js
|
│ │ │ │ └── trustedDevices.model.js
|
||||||
@@ -422,14 +398,12 @@ website/
|
|||||||
│ │ │ │ │ ├── account.router.js
|
│ │ │ │ │ ├── account.router.js
|
||||||
│ │ │ │ │ ├── activity.router.js
|
│ │ │ │ │ ├── activity.router.js
|
||||||
│ │ │ │ │ ├── admin.controller.js
|
│ │ │ │ │ ├── admin.controller.js
|
||||||
|
│ │ │ │ │ ├── admin.routes.js
|
||||||
│ │ │ │ │ ├── authProviders.controller.js
|
│ │ │ │ │ ├── authProviders.controller.js
|
||||||
│ │ │ │ │ ├── authProviders.router.js
|
│ │ │ │ │ ├── authProviders.router.js
|
||||||
│ │ │ │ │ ├── botActivity.controller.js
|
│ │ │ │ │ ├── botActivity.controller.js
|
||||||
│ │ │ │ │ ├── botActivity.router.js
|
│ │ │ │ │ ├── botActivity.router.js
|
||||||
│ │ │ │ │ ├── dashboard.router.js
|
|
||||||
│ │ │ │ │ ├── discordBot.controller.js
|
│ │ │ │ │ ├── discordBot.controller.js
|
||||||
│ │ │ │ │ ├── discordBot.router.js
|
|
||||||
│ │ │ │ │ ├── email.router.js
|
|
||||||
│ │ │ │ │ ├── emailConfig.controller.js
|
│ │ │ │ │ ├── emailConfig.controller.js
|
||||||
│ │ │ │ │ ├── imageUpload.js
|
│ │ │ │ │ ├── imageUpload.js
|
||||||
│ │ │ │ │ ├── index.js
|
│ │ │ │ │ ├── index.js
|
||||||
@@ -440,25 +414,16 @@ website/
|
|||||||
│ │ │ │ │ ├── pages.controller.js
|
│ │ │ │ │ ├── pages.controller.js
|
||||||
│ │ │ │ │ ├── pages.router.js
|
│ │ │ │ │ ├── pages.router.js
|
||||||
│ │ │ │ │ ├── posts.router.js
|
│ │ │ │ │ ├── posts.router.js
|
||||||
│ │ │ │ │ ├── settings.router.js
|
|
||||||
│ │ │ │ │ ├── shard.router.js
|
|
||||||
│ │ │ │ │ ├── shardAtlas.controller.js
|
|
||||||
│ │ │ │ │ ├── shardClilocs.controller.js
|
|
||||||
│ │ │ │ │ ├── shardOps.controller.js
|
│ │ │ │ │ ├── shardOps.controller.js
|
||||||
│ │ │ │ │ ├── shardVisibility.controller.js
|
|
||||||
│ │ │ │ │ ├── uoLink.controller.js
|
│ │ │ │ │ ├── uoLink.controller.js
|
||||||
│ │ │ │ │ ├── uoLink.router.js
|
|
||||||
│ │ │ │ │ ├── uploads.router.js
|
│ │ │ │ │ ├── uploads.router.js
|
||||||
│ │ │ │ │ ├── users.router.js
|
│ │ │ │ │ ├── users.router.js
|
||||||
│ │ │ │ │ ├── usersShard.controller.js
|
│ │ │ │ │ ├── usersShard.controller.js
|
||||||
│ │ │ │ │ └── wiki.router.js
|
│ │ │ │ │ └── wiki.router.js
|
||||||
│ │ │ │ ├── auth/
|
│ │ │ │ ├── auth/
|
||||||
│ │ │ │ │ ├── auth.controller.js
|
│ │ │ │ │ ├── auth.controller.js
|
||||||
│ │ │ │ │ ├── index.js
|
│ │ │ │ │ ├── auth.routes.js
|
||||||
│ │ │ │ │ ├── invite.controller.js
|
│ │ │ │ │ ├── invite.controller.js
|
||||||
│ │ │ │ │ ├── invite.router.js
|
|
||||||
│ │ │ │ │ ├── login.router.js
|
|
||||||
│ │ │ │ │ ├── loginGuards.js
|
|
||||||
│ │ │ │ │ ├── me.routes.js
|
│ │ │ │ │ ├── me.routes.js
|
||||||
│ │ │ │ │ ├── mobile.controller.js
|
│ │ │ │ │ ├── mobile.controller.js
|
||||||
│ │ │ │ │ ├── mobile.routes.js
|
│ │ │ │ │ ├── mobile.routes.js
|
||||||
@@ -466,10 +431,7 @@ website/
|
|||||||
│ │ │ │ │ ├── mobileSso.routes.js
|
│ │ │ │ │ ├── mobileSso.routes.js
|
||||||
│ │ │ │ │ ├── notifications.controller.js
|
│ │ │ │ │ ├── notifications.controller.js
|
||||||
│ │ │ │ │ ├── notifications.routes.js
|
│ │ │ │ │ ├── notifications.routes.js
|
||||||
│ │ │ │ │ ├── password.router.js
|
|
||||||
│ │ │ │ │ ├── passwordReset.controller.js
|
│ │ │ │ │ ├── passwordReset.controller.js
|
||||||
│ │ │ │ │ ├── register.router.js
|
|
||||||
│ │ │ │ │ ├── session.router.js
|
|
||||||
│ │ │ │ │ ├── sso.controller.js
|
│ │ │ │ │ ├── sso.controller.js
|
||||||
│ │ │ │ │ ├── sso.routes.js
|
│ │ │ │ │ ├── sso.routes.js
|
||||||
│ │ │ │ │ └── trustDevice.helper.js
|
│ │ │ │ │ └── trustDevice.helper.js
|
||||||
@@ -477,23 +439,13 @@ website/
|
|||||||
│ │ │ │ │ ├── internal.controller.js
|
│ │ │ │ │ ├── internal.controller.js
|
||||||
│ │ │ │ │ └── internal.routes.js
|
│ │ │ │ │ └── internal.routes.js
|
||||||
│ │ │ │ ├── player/
|
│ │ │ │ ├── player/
|
||||||
│ │ │ │ │ ├── account.router.js
|
|
||||||
│ │ │ │ │ ├── appeals.controller.js
|
│ │ │ │ │ ├── appeals.controller.js
|
||||||
│ │ │ │ │ ├── appeals.router.js
|
│ │ │ │ │ ├── player.routes.js
|
||||||
│ │ │ │ │ ├── index.js
|
│ │ │ │ │ └── shard.controller.js
|
||||||
│ │ │ │ │ ├── shard.controller.js
|
|
||||||
│ │ │ │ │ └── shard.router.js
|
|
||||||
│ │ │ │ ├── public/
|
│ │ │ │ ├── public/
|
||||||
│ │ │ │ │ ├── atlas.controller.js
|
|
||||||
│ │ │ │ │ ├── atlas.router.js
|
|
||||||
│ │ │ │ │ ├── index.js
|
|
||||||
│ │ │ │ │ ├── pages.router.js
|
|
||||||
│ │ │ │ │ ├── posts.router.js
|
|
||||||
│ │ │ │ │ ├── public.controller.js
|
│ │ │ │ │ ├── public.controller.js
|
||||||
│ │ │ │ │ ├── shard.controller.js
|
│ │ │ │ │ ├── public.routes.js
|
||||||
│ │ │ │ │ ├── shard.router.js
|
│ │ │ │ │ └── shard.controller.js
|
||||||
│ │ │ │ │ ├── site.router.js
|
|
||||||
│ │ │ │ │ └── wiki.router.js
|
|
||||||
│ │ │ │ └── v1.router.js
|
│ │ │ │ └── v1.router.js
|
||||||
│ │ │ ├── api.router.js
|
│ │ │ ├── api.router.js
|
||||||
│ │ │ ├── cspReport.controller.js
|
│ │ │ ├── cspReport.controller.js
|
||||||
@@ -503,8 +455,6 @@ website/
|
|||||||
│ │ │ ├── auth.js
|
│ │ │ ├── auth.js
|
||||||
│ │ │ ├── botInternalClient.js
|
│ │ │ ├── botInternalClient.js
|
||||||
│ │ │ ├── botInternalKey.js
|
│ │ │ ├── botInternalKey.js
|
||||||
│ │ │ ├── clilocParse.js
|
|
||||||
│ │ │ ├── clilocSource.js
|
|
||||||
│ │ │ ├── db.js
|
│ │ │ ├── db.js
|
||||||
│ │ │ ├── logger.js
|
│ │ │ ├── logger.js
|
||||||
│ │ │ ├── mailer.js
|
│ │ │ ├── mailer.js
|
||||||
@@ -515,9 +465,6 @@ website/
|
|||||||
│ │ │ ├── shardBroadcast.js
|
│ │ │ ├── shardBroadcast.js
|
||||||
│ │ │ ├── shardIngest.js
|
│ │ │ ├── shardIngest.js
|
||||||
│ │ │ ├── shardSales.js
|
│ │ │ ├── shardSales.js
|
||||||
│ │ │ ├── shardVisibility.js
|
|
||||||
│ │ │ ├── spawnAtlasParse.js
|
|
||||||
│ │ │ ├── spawnAtlasSource.js
|
|
||||||
│ │ │ ├── totp.js
|
│ │ │ ├── totp.js
|
||||||
│ │ │ ├── trustProxy.js
|
│ │ │ ├── trustProxy.js
|
||||||
│ │ │ ├── uoLinkClient.js
|
│ │ │ ├── uoLinkClient.js
|
||||||
@@ -536,14 +483,11 @@ website/
|
|||||||
│ │ ├── appeals.pure.test.js
|
│ │ ├── appeals.pure.test.js
|
||||||
│ │ ├── appeals.test.js
|
│ │ ├── appeals.test.js
|
||||||
│ │ ├── appLinks.test.js
|
│ │ ├── appLinks.test.js
|
||||||
│ │ ├── atlasController.test.js
|
|
||||||
│ │ ├── authController.test.js
|
│ │ ├── authController.test.js
|
||||||
│ │ ├── authMe.test.js
|
│ │ ├── authMe.test.js
|
||||||
│ │ ├── authTrustedDevice.test.js
|
│ │ ├── authTrustedDevice.test.js
|
||||||
│ │ ├── botInternalKey.test.js
|
│ │ ├── botInternalKey.test.js
|
||||||
│ │ ├── botScore.test.js
|
│ │ ├── botScore.test.js
|
||||||
│ │ ├── clilocParse.test.js
|
|
||||||
│ │ ├── clilocSource.test.js
|
|
||||||
│ │ ├── csp.test.js
|
│ │ ├── csp.test.js
|
||||||
│ │ ├── emailConfig.model.test.js
|
│ │ ├── emailConfig.model.test.js
|
||||||
│ │ ├── honeypot.test.js
|
│ │ ├── honeypot.test.js
|
||||||
@@ -577,32 +521,17 @@ website/
|
|||||||
│ │ ├── secretBox.test.js
|
│ │ ├── secretBox.test.js
|
||||||
│ │ ├── selfTrustedDevices.test.js
|
│ │ ├── selfTrustedDevices.test.js
|
||||||
│ │ ├── session.test.js
|
│ │ ├── session.test.js
|
||||||
│ │ ├── shardBroadcast.visibility.test.js
|
|
||||||
│ │ ├── shardControllerPublic.test.js
|
│ │ ├── shardControllerPublic.test.js
|
||||||
│ │ ├── shardIngest.champsPages.test.js
|
│ │ ├── shardIngest.champsPages.test.js
|
||||||
│ │ ├── shardIngest.market.test.js
|
|
||||||
│ │ ├── shardIngest.points.test.js
|
|
||||||
│ │ ├── shardIngest.protocol2.test.js
|
│ │ ├── shardIngest.protocol2.test.js
|
||||||
│ │ ├── shardIngest.ruleset.test.js
|
|
||||||
│ │ ├── shardMarket.model.test.js
|
|
||||||
│ │ ├── shardState.governorTerms.test.js
|
│ │ ├── shardState.governorTerms.test.js
|
||||||
│ │ ├── shardState.model.test.js
|
│ │ ├── shardState.model.test.js
|
||||||
│ │ ├── shardVisibility.test.js
|
|
||||||
│ │ ├── spawnAtlas.parse.test.js
|
|
||||||
│ │ ├── spawnAtlas.source.test.js
|
|
||||||
│ │ ├── ssoCallback.test.js
|
│ │ ├── ssoCallback.test.js
|
||||||
│ │ ├── ssoState.test.js
|
│ │ ├── ssoState.test.js
|
||||||
│ │ ├── ssoTrustedDevice.test.js
|
|
||||||
│ │ ├── totp.test.js
|
│ │ ├── totp.test.js
|
||||||
│ │ ├── trustedDevices.test.js
|
│ │ ├── trustedDevices.test.js
|
||||||
│ │ ├── trustProxy.test.js
|
│ │ ├── trustProxy.test.js
|
||||||
│ │ ├── uoLinkClient.test.js
|
|
||||||
│ │ └── usernamePolicy.test.js
|
│ │ └── usernamePolicy.test.js
|
||||||
│ ├── tools/
|
|
||||||
│ │ └── cliloc-export/
|
|
||||||
│ │ ├── clilocexport.csproj
|
|
||||||
│ │ ├── Program.cs
|
|
||||||
│ │ └── README.md
|
|
||||||
│ ├── .env.example
|
│ ├── .env.example
|
||||||
│ ├── package-lock.json
|
│ ├── package-lock.json
|
||||||
│ ├── package.json
|
│ ├── package.json
|
||||||
|
|||||||
@@ -223,8 +223,7 @@ The atlas is fully functional as text. `shard_spawn_creatures.art` is nullable
|
|||||||
and is NULL on every fresh import; pages render without images, which is the
|
and is NULL on every fresh import; pages render without images, which is the
|
||||||
normal and supported state, not a degraded one.
|
normal and supported state, not a degraded one.
|
||||||
|
|
||||||
An operator who wants art — step-by-step, with the UOFiddler side spelled out, in
|
An operator who wants art:
|
||||||
[`UOFIDDLER.md`](UOFIDDLER.md) §Part 2:
|
|
||||||
|
|
||||||
1. Extracts it from **their own** client files (UOFiddler, ClassicUO tooling, or
|
1. Extracts it from **their own** client files (UOFiddler, ClassicUO tooling, or
|
||||||
any art extractor).
|
any art extractor).
|
||||||
|
|||||||
@@ -1,282 +0,0 @@
|
|||||||
# Extracting from your own UO client (UOFiddler)
|
|
||||||
|
|
||||||
**Audience:** the shard operator, once, at setup time.
|
|
||||||
**Related:** [`CLILOCS.md`](CLILOCS.md) (why the cliloc conversion is unavoidable),
|
|
||||||
[`SPAWN_ATLAS.md`](SPAWN_ATLAS.md) (where creature art fits).
|
|
||||||
|
|
||||||
Two features read data that **only exists inside a UO client**, and a UO client's
|
|
||||||
files are EA's, not ours to redistribute. So neither this repo nor any image we
|
|
||||||
publish can ship them — the operator extracts from **their own** client, once,
|
|
||||||
and points the site at the result.
|
|
||||||
|
|
||||||
| Feature | What it needs | Required? | Without it |
|
|
||||||
|---|---|---|---|
|
|
||||||
| **Item / title names** ([`CLILOCS.md`](CLILOCS.md)) | `Cliloc.enu`, converted | No | Names render as raw ids — `id 1023721` instead of *quarter staff* |
|
|
||||||
| **Creature art** ([`SPAWN_ATLAS.md`](SPAWN_ATLAS.md)) | Sprites from `.mul`/`.uop` | No | Atlas pages render as text, which is the normal state |
|
|
||||||
|
|
||||||
**Both are optional and neither is load-bearing.** A shard that never does any of
|
|
||||||
this is fully supported. Do part one and skip part two if art is not worth your
|
|
||||||
time — they share only the tool.
|
|
||||||
|
|
||||||
Everything you extract stays **outside the repository**: the converted cliloc
|
|
||||||
file lives at a path you choose, and `spawnAtlas.art.json` plus `server/uploads/`
|
|
||||||
are gitignored, so none of it can be committed by accident.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 0 — Get UOFiddler
|
|
||||||
|
|
||||||
[UOFiddler](https://github.com/polserver/UOFiddler) is the community client-file
|
|
||||||
editor. We use it because its `Ultima.dll` already contains the cliloc
|
|
||||||
decompressor, maintained by people who do this for a living.
|
|
||||||
|
|
||||||
1. Download the latest release zip from
|
|
||||||
<https://github.com/polserver/UOFiddler/releases/latest> — one asset, named
|
|
||||||
`UOFiddler-<version>.zip` (4.22.2 is ~2 MB).
|
|
||||||
2. Extract it. The zip contains a single top-level folder, and the two files that
|
|
||||||
matter are at **its root**:
|
|
||||||
|
|
||||||
```
|
|
||||||
UOFiddler-4.22.2/
|
|
||||||
Ultima.dll ← the decompressor (Part 1 needs this path)
|
|
||||||
UoFiddler.exe ← the GUI (Part 2 needs this)
|
|
||||||
plugins/
|
|
||||||
…
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Runtime:** UOFiddler 4.22.2 is built for **.NET 10**. Running `UoFiddler.exe`
|
|
||||||
needs the .NET 10 **Desktop** Runtime (Windows only); loading `Ultima.dll` from
|
|
||||||
the converter in Part 1 needs the .NET 10 runtime. Install from
|
|
||||||
<https://dotnet.microsoft.com/download/dotnet/10.0>.
|
|
||||||
|
|
||||||
### Finding your client files
|
|
||||||
|
|
||||||
The cliloc file is in your **UO client installation directory**, not in your
|
|
||||||
ServUO tree — the shard server has no copy of it. Look for `Cliloc.enu` (English;
|
|
||||||
the other seven are `chs`, `cht`, `deu`, `esp`, `fra`, `jpn`, `kor`) beside
|
|
||||||
`art.mul` / `artLegacyMUL.uop`. The EA Classic Client's default location is:
|
|
||||||
|
|
||||||
```
|
|
||||||
C:\Program Files (x86)\Electronic Arts\Ultima Online Classic\
|
|
||||||
```
|
|
||||||
|
|
||||||
**If your shard distributes its own patched client to players, use that copy.**
|
|
||||||
Any cliloc edits you shipped to players are then already in the base table and
|
|
||||||
you need no overlay for them (see [`CLILOCS.md`](CLILOCS.md) §Shard-added and
|
|
||||||
shard-edited items).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 1 — Convert the cliloc table
|
|
||||||
|
|
||||||
**Goal:** turn the client's compressed `Cliloc.enu` into a file the site can
|
|
||||||
read, and point the site at it.
|
|
||||||
|
|
||||||
The site cannot read `Cliloc.enu` directly. Every modern client compresses it
|
|
||||||
(the "Mythic" container), and so does ServUO's own bundled `Ultima.StringList` —
|
|
||||||
which is why the shard cannot supply names on our behalf either. The full
|
|
||||||
reasoning is in [`CLILOCS.md`](CLILOCS.md) §Why the operator has to convert the
|
|
||||||
file; this section is just the procedure.
|
|
||||||
|
|
||||||
Two routes. **The bundled tool is the recommended one** — the GUI export needs a
|
|
||||||
fixup step, described below.
|
|
||||||
|
|
||||||
### Route A — the bundled converter (recommended)
|
|
||||||
|
|
||||||
Needs a .NET SDK (any version 8 or newer — the project targets `net8.0` and rolls
|
|
||||||
forward, so whatever you have works) **plus** the .NET 10 runtime from Part 0,
|
|
||||||
which is what actually loads `Ultima.dll`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd website/server/tools/cliloc-export
|
|
||||||
dotnet build -c Release
|
|
||||||
|
|
||||||
# plain binary — recommended, exact
|
|
||||||
dotnet run -c Release -- \
|
|
||||||
"/path/to/UOFiddler-4.22.2/Ultima.dll" \
|
|
||||||
"/path/to/UO client/Cliloc.enu" \
|
|
||||||
/srv/uo-data/clilocs.plain
|
|
||||||
|
|
||||||
# or tab-delimited text, if you want to eyeball or hand-edit it
|
|
||||||
dotnet run -c Release -- \
|
|
||||||
"/path/to/UOFiddler-4.22.2/Ultima.dll" \
|
|
||||||
"/path/to/UO client/Cliloc.enu" \
|
|
||||||
/srv/uo-data/clilocs.tsv --tsv
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected output for a stock English client:
|
|
||||||
|
|
||||||
```
|
|
||||||
wrote 123490 entries to /srv/uo-data/clilocs.plain (maxTextBytes=12150, skippedOversize=0)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Sanity-check that number.** A stock `Cliloc.enu` is ~123,000 entries. A few
|
|
||||||
hundred means it read something else and you should not ship the result. The
|
|
||||||
tool exits non-zero and says `no entries were written — is that a cliloc file?`
|
|
||||||
when it gets nothing at all.
|
|
||||||
|
|
||||||
The conversion runs on whatever machine has the client (usually Windows), and the
|
|
||||||
site reads the output wherever it runs — so **copy the output file to the server**
|
|
||||||
if those are different machines. It is a single self-contained file (~5 MB); the
|
|
||||||
`--tsv` form is larger but diff-able.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Errors you may hit</summary>
|
|
||||||
|
|
||||||
| Message | Cause |
|
|
||||||
|---|---|
|
|
||||||
| `Ultima.StringList not found — is that really UOFiddler's Ultima.dll?` | First argument points at some other `Ultima.dll` (ServUO ships one too — it is **not** the same assembly and cannot do this) |
|
|
||||||
| `You must install .NET to run this application` | Missing the .NET 10 runtime from Part 0 step 3 |
|
|
||||||
| `Unexpected Ultima.StringList API` | UOFiddler older than 4.21 |
|
|
||||||
| `usage: clilocexport …` | Fewer than three arguments |
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Route B — the UOFiddler GUI
|
|
||||||
|
|
||||||
Use this if you would rather not install a .NET SDK. **It needs one extra step**,
|
|
||||||
so do not skip the fixup.
|
|
||||||
|
|
||||||
1. Launch `UoFiddler.exe` and point it at your client directory when it asks
|
|
||||||
(or **Options → Path Settings**).
|
|
||||||
2. Open the **Cliloc** tab and use its **export to CSV** action.
|
|
||||||
3. It writes `CliLoc.csv` to UOFiddler's configured output path, in **three**
|
|
||||||
columns with a header row:
|
|
||||||
|
|
||||||
```
|
|
||||||
Number;Text;Flag
|
|
||||||
1023721;quarter staff;0
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Strip the trailing flag column.** The site's text parser reads
|
|
||||||
`number<TAB|,|;>text`, so that third field is otherwise absorbed into the name
|
|
||||||
and every item on the site renders as `quarter staff;0`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sed -E 's/;[0-9]+$//' CliLoc.csv > clilocs.csv
|
|
||||||
```
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Get-Content CliLoc.csv |
|
|
||||||
ForEach-Object { $_ -replace ';\d+$','' } |
|
|
||||||
Set-Content -Encoding utf8 clilocs.csv
|
|
||||||
```
|
|
||||||
|
|
||||||
The header row needs no removal — a line whose first field is not an integer
|
|
||||||
is skipped. Blank entries (`1005008;`) survive the fixup correctly and are
|
|
||||||
dropped at import, as intended.
|
|
||||||
|
|
||||||
5. Copy `clilocs.csv` to the server.
|
|
||||||
|
|
||||||
**Why the fixup is not just done for us:** the parser already handles
|
|
||||||
`number,flag,text` — the flag in the *middle*, which is what several exports
|
|
||||||
emit. UOFiddler puts it at the *end*, where it is indistinguishable from a name
|
|
||||||
that genuinely ends in `;0`. One `sed` on the operator's side beats a parser
|
|
||||||
heuristic that would corrupt real names.
|
|
||||||
|
|
||||||
### Point the site at it
|
|
||||||
|
|
||||||
Two ways, the setting winning over the environment:
|
|
||||||
|
|
||||||
| Where | How |
|
|
||||||
|---|---|
|
|
||||||
| **Admin → Shard → cliloc path** | Takes effect on the next refresh, no redeploy |
|
|
||||||
| `UO_CLIENT_PATH` env var | The deploy-time default |
|
|
||||||
|
|
||||||
The value may be **the file itself or a directory to search** — both are natural
|
|
||||||
answers to "where is it", and overlays are picked up either way.
|
|
||||||
|
|
||||||
Setting the path deliberately does **not** import as a side effect. Click
|
|
||||||
**Import** (or `POST /api/v1/admin/shard/clilocs/import`) to load it.
|
|
||||||
|
|
||||||
### Verify
|
|
||||||
|
|
||||||
`GET /api/v1/admin/shard/clilocs`, or the Admin → Shard panel, reports what each
|
|
||||||
source contributed:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"sources": [
|
|
||||||
{ "label": "clilocs.plain", "kind": "base", "entries": 123490, "added": 123490, "overrode": 0 }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Roughly **67,500 rows stored** from a stock table is correct — about half a
|
|
||||||
cliloc table is empty strings for ids the client reserves and never uses.
|
|
||||||
|
|
||||||
Then load any character sheet with equipment: items should show names rather than
|
|
||||||
`id 1023721`.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>What a refusal means</summary>
|
|
||||||
|
|
||||||
A bad file answers `200` with a `status` and a named reason, not a `500` — you
|
|
||||||
need to be told *which file* to fix.
|
|
||||||
|
|
||||||
| `code` | Meaning |
|
|
||||||
|---|---|
|
|
||||||
| `COMPRESSED` | You pointed at the raw client `Cliloc.enu`. Convert it — this whole page. |
|
|
||||||
| `TRUNCATED` | Half-copied file. Re-copy; the loaded table is untouched. |
|
|
||||||
| `EMPTY` | A text source with no parseable rows — the file is named in the reason. |
|
|
||||||
| `status: needsReview` + `missingSources` | A previously-loaded source has vanished (unmounted volume? deliberate deletion?). Nothing changes until you re-import with `{ "approve": true }`. |
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Custom items — do *not* re-export for these
|
|
||||||
|
|
||||||
Shard-added items carry ids no client table has. Drop a small delimited file in a
|
|
||||||
`custom/` directory beside the base file and re-import:
|
|
||||||
|
|
||||||
```
|
|
||||||
/srv/uo-data/
|
|
||||||
clilocs.plain ← base, from this guide
|
|
||||||
custom/
|
|
||||||
01-uomysticmoon.tsv ← your additions and overrides
|
|
||||||
```
|
|
||||||
|
|
||||||
Files are read in sorted order and **later sources win**, so an overlay both adds
|
|
||||||
new ids and overrides stock ones you have re-purposed. **Adding one item never
|
|
||||||
means re-exporting a 5 MB client file.** Details in [`CLILOCS.md`](CLILOCS.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 2 — Creature art for the spawn atlas (optional)
|
|
||||||
|
|
||||||
**Goal:** put sprites on atlas pages. Purely cosmetic — the atlas is fully
|
|
||||||
functional as text, and `art` is NULL on every fresh import.
|
|
||||||
|
|
||||||
**This project ships no art and no art-extraction tooling, and never will.**
|
|
||||||
|
|
||||||
1. In `UoFiddler.exe` (paths configured as in Route B step 1), open the
|
|
||||||
**Animations** tab for creature sprites — or **Items** for object art — find
|
|
||||||
the creature, and export as PNG. Right-click an entry for its export options,
|
|
||||||
or use the tab's *Export All* action for a batch. (4.22.2 added an export
|
|
||||||
option to the Animation tab's thumbnail list, which is the convenient one
|
|
||||||
here.)
|
|
||||||
2. Put the images under `server/uploads/atlas/`.
|
|
||||||
3. Copy `server/db/data/spawnAtlas.art.example.json` to `spawnAtlas.art.json` in
|
|
||||||
the same directory and map creature slugs to file names:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"lizardman": "lizardman.png",
|
|
||||||
"orc": "orc.png"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Keys are the slugs the atlas API reports**, derived from the type names in
|
|
||||||
your own shard's `Spawns/*.xml` — read them off the atlas rather than guessing.
|
|
||||||
A creature with no entry renders without art, which is the default.
|
|
||||||
|
|
||||||
4. Restart, or `npm run atlas:import -- --force`.
|
|
||||||
|
|
||||||
The art map is re-read on every atlas refresh, so adding one image is an edit plus
|
|
||||||
a refresh. Both `spawnAtlas.art.json` and `server/uploads/` are gitignored.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Licensing, briefly
|
|
||||||
|
|
||||||
UO's strings and sprites are EA's. Extracting from **your own** client for
|
|
||||||
**your own** shard is the arrangement here; redistributing the extracted files is
|
|
||||||
not something this project does or can advise on. That is the whole reason this
|
|
||||||
page exists instead of a download link.
|
|
||||||
Reference in New Issue
Block a user