From ae97e5a9ab84b931be4ade163ae240c6a37c957b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 16:22:09 +0000 Subject: [PATCH 1/2] Engines: stop treating Ironcore as the default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ironcore is one of seven supported engines, but the repo still read as though it were the one MONx is for — and in two places that was not just prose. The fallbacks: - `default_profile()` returned Ironcore. It now returns TFS, and the comment says why: TFS is the shape the other XML engines are variants of and the most conservative of them, so a wrong guess under-declares and rides along as unknown attributes rather than dropping data. - `detection()` was worse than a named fallback. `detect` sorts stably, so when nothing scored, `best` was whichever profile sat first in `ALL` — table order dressed as evidence, which is what made every unrecognisable corpus come back "Ironcore". It now names the default profile explicitly when the top score is zero; `confident: false` already said it was a guess. `ALL` and the frontend's `ENGINES` are reordered to match, and neither order means anything any more. - The frontend's `engineInfo()` fell back to Ironcore independently. Now TFS, pointing at the backend as the reason the two must agree. The UI: - The `✦ (Ironcore)` marker on boolean and numeric flags is gone. It was wrong on `isboss`, which TFS and TVP both read, and redundant everywhere else — the section already filters flags by `engine.boolFlags`, so a flag that renders is one this engine has. The `ironcore` row field goes with it; the `MAGIC_EFFECTS` one stays, since ids 81-104 really are only in Ironcore's table. - Pacifist & Events rendered the two Ironcore pacifist strings on every engine. That half now gates on `engine.pacifist` and the labels drop the "(Ironcore)" they no longer need to carry. The prose: AGENTS.md no longer says Ironcore is the default profile, ENGINES.md's table no longer calls it "The default" (and its comparison tables lead with TFS, cells moved with the headers), and the manifest description, issue template, fixture README and run-monx skill are engine-neutral. Statements about what Ironcore actually does are left alone — those are documentation of a supported engine, not framing. Gates: bun run i18n / build / catalog / commands, bun test (161 pass), cargo check, and probe_monster --mutate --lint --crud against all seven engine fixtures, each still detecting itself. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WpsR8Uq4Cuu53LoqEqGLPA --- .claude/skills/run-monx/SKILL.md | 2 +- .claude/skills/run-monx/driver.ps1 | 5 ++-- .github/ISSUE_TEMPLATE.md | 2 +- AGENTS.md | 24 ++++++++---------- ENGINES.md | 39 ++++++++++++++++------------- README.md | 2 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 6 ++--- src-tauri/examples/probe_monster.rs | 5 ++++ src-tauri/fixtures/README.md | 4 +-- src-tauri/src/catalog.rs | 2 +- src-tauri/src/engine/detect.rs | 17 +++++++++---- src-tauri/src/engine/mod.rs | 17 +++++++------ src-tauri/src/engine/profiles.rs | 16 ++++++++++-- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 2 +- src/catalog.ts | 22 ++++++++-------- src/customeffects.ts | 8 +++--- src/engine.ts | 14 ++++++++--- src/fixtures.ts | 10 ++++---- src/locales/en.ts | 2 ++ src/locales/pl.ts | 12 +++++---- src/locales/pt.ts | 11 ++++---- src/prefs.ts | 5 ++-- src/sections/Combat.tsx | 18 ++++++------- src/sections/PacifistEvents.tsx | 25 +++++++++++++----- src/settings.ts | 2 +- 28 files changed, 164 insertions(+), 114 deletions(-) diff --git a/.claude/skills/run-monx/SKILL.md b/.claude/skills/run-monx/SKILL.md index 9fc3475..c8cb203 100644 --- a/.claude/skills/run-monx/SKILL.md +++ b/.claude/skills/run-monx/SKILL.md @@ -1,6 +1,6 @@ --- name: run-monx -description: Build, run, and drive MONx (the Tauri desktop Ironcore monster XML editor). Use when asked to start MONx, build the exe, screenshot its UI, open a workspace in it, exercise the editor, or verify a Rust backend change (monster.rs/otb.rs/items.rs/dat.rs) via the probe CLI examples. +description: Build, run, and drive MONx (the Tauri desktop OpenTibia monster editor). Use when asked to start MONx, build the exe, screenshot its UI, open a workspace in it, exercise the editor, or verify a Rust backend change (monster.rs/otb.rs/items.rs/dat.rs) via the probe CLI examples. --- MONx is a Windows Tauri 2 desktop app (React/TS frontend + Rust backend in `src-tauri/`). diff --git a/.claude/skills/run-monx/driver.ps1 b/.claude/skills/run-monx/driver.ps1 index e8524d8..c3ce327 100644 --- a/.claude/skills/run-monx/driver.ps1 +++ b/.claude/skills/run-monx/driver.ps1 @@ -36,8 +36,9 @@ param( # The default fixture workspace. `assets/` holds one folder per engine — # Ironcore, Canary, BlackTek, Nostalrius, TVP — each with monsters/, items/, - # client/ and spells/; Ironcore is the default profile, so it is the default - # here. Pass another engine's folder to `start`/`open` to test against it. + # client/ and spells/. Ironcore's is the default here only because it is the + # fullest of them, not because MONx favours that engine. Pass another + # engine's folder to `start`/`open` to test against it. [string]$Fixture = (Resolve-Path "$PSScriptRoot\..\..\..\assets\Ironcore" -ErrorAction SilentlyContinue), # Fill the four slots through the folder pickers instead of taking the diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 86a7077..10d5cab 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -6,7 +6,7 @@ 1. 2. -**Which server?** (Ironcore / TFS / TheVioletProject / Nostalrius / Canary / BlackTek) +**Which server?** (TFS / Ironcore / TheVioletProject / Nostalrius / Canary / CrystalServer / BlackTek) **MONx version** (titlebar, or `package.json`): diff --git a/AGENTS.md b/AGENTS.md index fa7a42f..12f4cea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,7 @@ path it runs against the committed fixtures, so a fresh clone can check itself b `assets/` is populated: ```sh -cargo run --release --example probe_monster # fixtures/engines/ironcore/monsters +cargo run --release --example probe_monster # fixtures/engines/ironcore/monsters — the fullest fixture, not a default engine cargo run --release --example probe_monster -- fixtures/engines/tvp/monster --engine tvp --mutate ``` @@ -53,10 +53,10 @@ Those are a smoke test, not coverage — see `src-tauri/fixtures/README.md`. The point at a server's own tree: ```sh -cargo run --release --example probe_monster -- ../assets/Ironcore/monsters # round-trip -cargo run --release --example probe_monster -- ../assets/Ironcore/monsters --canonical --mutate -cargo run --release --example probe_monster -- ../assets/Ironcore/monsters --lint -cargo run --release --example probe_monster -- ../assets/Ironcore/monsters --crud +cargo run --release --example probe_monster -- ../assets/TFS/monster # round-trip +cargo run --release --example probe_monster -- ../assets/TFS/monster --canonical --mutate +cargo run --release --example probe_monster -- ../assets/TFS/monster --lint +cargo run --release --example probe_monster -- ../assets/TFS/monster --crud ``` `--mutate` is the one that proves the writer is driven by the model rather than copying bytes: it edits several fields in every file, writes, re-reads, and checks the document that comes back is the one that went in. It also budgets the diff — a handful of field edits that rewrite more than 12 lines fail, because the writer is meant to splice. A change that *inserts or removes* a node moves every line under it and can never meet that budget, so it belongs in a pass of its own (see `voice_extras_survive`). Add `--verbose` to any of them to list every finding, `--items ` to point loot resolution at a database that is not the monsters folder's sibling, and `--bands` to print the corpus's balance bands. @@ -70,7 +70,7 @@ cargo run --release --example probe_monster -- data/monster \ `--format json|sarif` implies `--lint` and writes the report to `--out `, or to stdout with the human summary moved to stderr so the stream stays pipeable. `--fail-on` takes a **set** of severities, not a threshold — the three are not a ranking, and `silent` is the one worth failing a build on even where warnings are tolerated. `--fail-on any` is all three; without the flag the lint pass never fails the run, which is what keeps `--lint` usable on a corpus that has lived with three hundred warnings for years. -`--engine ` picks the profile (`ironcore`, `tfs`, `tvp`, `nostalrius`, `canary`, `crystal`, `blacktek`); without it the corpus is sniffed exactly as the Landing dialog sniffs it, and the guess is printed. **Run every gate against all seven engines' own corpora when touching the reader, writer or a profile** — an over-declared `known_attrs` drops data, and `--mutate` is the only thing that catches it: +`--engine ` picks the profile (`tfs`, `ironcore`, `tvp`, `nostalrius`, `canary`, `crystal`, `blacktek`); without it the corpus is sniffed exactly as the Landing dialog sniffs it, and the guess is printed. **Run every gate against all seven engines' own corpora when touching the reader, writer or a profile** — an over-declared `known_attrs` drops data, and `--mutate` is the only thing that catches it: ```sh cargo run --release --example probe_monster -- ../assets/TVP/monster --engine tvp --mutate @@ -256,16 +256,14 @@ assets/ fixture workspaces, one folder per engine: each has ## Domain knowledge -**Do not infer behaviour from upstream TFS.** Ironcore diverges in ways that matter constantly: per-spell cooldowns, extra flags, the pacifist system, `force` on summons, `corpseactionid`, `masterEffect`. - -MONx also opens **TheForgottenServer 1.x, TheVioletProject, Nostalrius, Canary/OTServBR, CrystalServer and BlackTek** corpora. Everything below describes Ironcore, which is the default profile; what the other six do differently lives in `engine/profiles.rs` and is summarised in [ENGINES.md](ENGINES.md). Four consequences worth knowing before touching anything: +**Do not infer one engine's behaviour from another's.** MONx opens **TheForgottenServer 1.x, Ironcore, TheVioletProject, Nostalrius, Canary/OTServBR, CrystalServer and BlackTek** corpora, and they diverge in ways that matter constantly — per-spell cooldowns, extra flags, the pacifist system, `force` on summons, `corpseactionid`, `masterEffect` are Ironcore's; every other engine has its own list. None of them is the engine MONx is *for*: what each one does lives in `engine/profiles.rs` and is summarised in [ENGINES.md](ENGINES.md). Four consequences worth knowing before touching anything: - **The reader, writer and linter all take a `&'static EngineProfile`.** There is one `MonsterDoc` for all seven engines — a superset — and the profile decides which parts the reader populates and the writer emits. Never hard-code a spelling like `raceid` or `CONST_ME_*`; ask the profile. - **Two formats, one model.** Canary and BlackTek define monsters as Lua tables, not XML. `Parsed` is an enum with an XML body and a Lua body; `read_bytes`/`write_bytes` dispatch on `profile.format`. Everything above the document layer — `MonsterDoc`, the lints, the editor — is shared, and should stay that way. -- **A corpus can be a tree.** Only Ironcore is flat. A monster's key is its path relative to the monsters folder (`monsters/demon.xml`), matching its `file=` in `monsters.xml`. -- **A lint the engine has no rule for is suppressed, not reported.** `silent` severity is only worth anything if it means the server really would say nothing; firing Ironcore's rules at a TVP corpus inverts that. Per-engine suppressions live on the profile. +- **A corpus can be a tree.** Only Ironcore's is flat. A monster's key is its path relative to the monsters folder (`monsters/demon.xml`), matching its `file=` in `monsters.xml`. +- **A lint the engine has no rule for is suppressed, not reported.** `silent` severity is only worth anything if it means the server really would say nothing; firing one engine's rules at another's corpus inverts that. Per-engine suppressions live on the profile. -The format was originally specified in `MONSTER_EDITOR_REFERENCE.md` and the product in `DESIGN.md`; both were derived from the server's own source and have since been removed from the repo. The `§n` markers throughout the code cite them. What they said now lives in the code, and that is where to look — or to add to: +The format was originally specified in `MONSTER_EDITOR_REFERENCE.md` and the product in `DESIGN.md`; both were derived from Ironcore's own source, back when that was the only engine MONx read, and have since been removed from the repo. The `§n` markers throughout the code cite them. What they said now lives in the code, and that is where to look — or to add to: - `catalog.rs` / `catalog.ts` — the enum tables (flags, damage and condition types, races, skulls, `CONST_ME_*`, `CONST_ANI_*`, built-in spells), each citing its section. The two are hand-kept mirrors and **`bun run catalog` is what keeps them honest** — add a value to one side only and nothing breaks, no build fails and no probe notices: the linter quietly stops calling it unknown while the picker still will not offer it, which is the "renders as nothing, deleted on the next click" case above. The check compares wire-exact names and numbers only; labels, colours, notes and group headings are UI-only and diverge on purpose. Where the two sides model the same fact differently — the unreachable shoot effects are a Rust exclusion list and a TS row flag — the check knows, and the comments in `scripts/check-catalog.mjs` say why for each. - `customeffects.ts` / `engine/custom.rs` — the escape hatch for the tables above. Every effect table is read out of a shipped server's source, which is what makes it trustworthy and what makes it wrong for anyone who modified their own. A user declares the extras (Preferences → Custom effects, `monx.customEffects`), the picker appends them to the engine's list, and the linter stops calling them unknown. **The probes deliberately pass `CustomEffects::default()`** — a gate a setting can quieten is not a gate. Effects that are neither shipped nor declared still show in the picker as off-catalogue rather than reading as `(none)`, because a value the editor renders as "nothing" is a value the next click deletes. @@ -277,7 +275,7 @@ The format was originally specified in `MONSTER_EDITOR_REFERENCE.md` and the pro Four rules that come up constantly: - **Round-trip is sacred.** Unknown attributes and comments are preserved verbatim; nothing is reordered or normalised on save. A value the engine would clamp gets linted, not silently rewritten. -- **Exact casing on the wire.** `raceid`, `maxSummons`, `actionId`, and upper-case `CONST_ME_*` / `CONST_ANI_*` — under Ironcore. Which spelling is the live one is a property of the engine: TFS reads `raceId` and names effects `firearea`, so both come from the profile rather than a literal. +- **Exact casing on the wire.** Which spelling is the live one is a property of the engine, never a literal: Ironcore reads `raceid` and upper-case `CONST_ME_*` / `CONST_ANI_*`, TFS reads `raceId` and names effects `firearea`. Both come from the profile. - **MONx never invents item ids.** A loot id the items database cannot resolve is a lint (`loot.unknown-id`), not something to create. The check is against the database, not the OTB — most engines ship no OTB, and it is skipped entirely when no database was loaded. - **`silent` is the loudest severity, not the quietest.** It marks the findings the server never reports at all — a spell the loader drops without a word. Those are the ones a human cannot discover any other way, so the UI gives them their own icon and hue rather than burying them under errors. diff --git a/ENGINES.md b/ENGINES.md index f111c60..c32ea9e 100644 --- a/ENGINES.md +++ b/ENGINES.md @@ -41,8 +41,8 @@ dead on Canary. | Key | Server | Format | Shape | |-----|--------|--------|-------| -| `ironcore` | Ironcore | XML | The default. `raceid`, `species`, the pacifist system, `CONST_ME_*` effects | | `tfs` | TheForgottenServer 1.x | XML | `raceId` + ``, short-name effects, no pacifist system | +| `ironcore` | Ironcore | XML | `raceid`, `species`, the pacifist system, `CONST_ME_*` effects | | `tvp` | TheVioletProject | XML | 7.x: ``, `delay=`, melee skill progression | | `nostalrius` | Nostalrius | XML | 7.x: melee on ``, no spell interval, `count=` conditions | | `canary` | Canary / OTServBR | Lua | Bestiary + bosstiary, factions, `COMBAT_*` damage types | @@ -64,7 +64,7 @@ codebase in half. ### A corpus can be a tree -Only Ironcore is flat. Everywhere else a monster's key is its path relative to +Only Ironcore's corpus is flat. Everywhere else a monster's key is its path relative to the monsters folder (`monsters/demon.xml`), matching its `file=` in `monsters.xml`. @@ -79,13 +79,13 @@ The profile has ~50 fields. These are the ones that bite. ### Identity -| | ironcore | tfs | tvp | nostalrius | canary | crystal | blacktek | +| | tfs | ironcore | tvp | nostalrius | canary | crystal | blacktek | |---|---|---|---|---|---|---|---| -| Race id attribute | `raceid` | `raceId` | — | — | — | — | — | -| `species=` | ✓ | — | — | — | — | — | — | -| Bestiary | — | ✓ | — | — | ✓ | ✓ | — | +| Race id attribute | `raceId` | `raceid` | — | — | — | — | — | +| `species=` | — | ✓ | — | — | — | — | — | +| Bestiary | ✓ | — | — | — | ✓ | ✓ | — | | Registry (`monsters.xml`) | ✓ | ✓ | ✓ | ✓ | — | — | — | -| Recursive corpus | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Recursive corpus | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ | | Extension | `.xml` | `.xml` | `.xml` | `.xml` | `.lua` | `.lua` | `.lua` | Ironcore's `species=` is **author metadata and nothing else** — `monsters.cpp` @@ -97,7 +97,7 @@ in detection. ### Spells -| | ironcore | tfs | tvp | nostalrius | canary | crystal | blacktek | +| | tfs | ironcore | tvp | nostalrius | canary | crystal | blacktek | |---|---|---|---|---|---|---|---| | Cadence | `interval` | `interval` | `interval`/`delay` | chance only | `interval` | `interval` | `interval` | | Melee from | spell block | spell block | spell block | `` node | spell block | spell block | spell block | @@ -121,13 +121,13 @@ why they are enums and not booleans: ### Flags and resistances -| | ironcore | tfs | tvp | nostalrius | canary | crystal | blacktek | +| | tfs | ironcore | tvp | nostalrius | canary | crystal | blacktek | |---|---|---|---|---|---|---|---| -| Pacifist system | ✓ | — | — | — | — | — | — | +| Pacifist system | — | ✓ | — | — | — | — | — | | `canpushcreatures` override | always | always | **never** | always | never | never | **only when unset** | | Clamps `health` to max | ✓ | ✓ | ✓ | **—** | ✓ | ✓ | ✓ | -| Effect naming | `CONST_ME_*` | short | short | short | `CONST_ME_*` | `CONST_ME_*` | `CONST_ME_*` | -| Effect matching | **case-sensitive** | lowercased | lowercased | lowercased | lowercased | lowercased | lowercased | +| Effect naming | short | `CONST_ME_*` | short | short | `CONST_ME_*` | `CONST_ME_*` | `CONST_ME_*` | +| Effect matching | lowercased | **case-sensitive** | lowercased | lowercased | lowercased | lowercased | lowercased | `PushableOverride` needed three states rather than a bool because BlackTek added a condition the C++ engines do not have: the override applies **only @@ -141,11 +141,11 @@ Ironcore-only finding. ### Loot and summons -| | ironcore | tfs | tvp | nostalrius | canary | crystal | blacktek | +| | tfs | ironcore | tvp | nostalrius | canary | crystal | blacktek | |---|---|---|---|---|---|---|---| | Loot inside a wrapper node | ✓ | ✓ | ✓ | **—** | — | — | — | | Loader validates loot ids | ✓ | ✓ | ✓ | **—** | ✓ | ✓ | ✓ | -| `countmax` ceiling | ✓ | — | — | — | — | — | — | +| `countmax` ceiling | — | ✓ | — | — | — | — | — | | Summon `interval` | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | | Summon `delay` | — | — | ✓ | — | — | — | — | | Summon cap key | `max` | `max` | `max` | `max` | **`count`** | **`count`** | `max` | @@ -163,13 +163,16 @@ existing file is always mirrored rather than reshaped, because > A lint the engine has no rule for is **suppressed, not reported**. `silent` severity is only worth anything if it means the server really would -say nothing. Firing Ironcore's rules at a TVP corpus inverts that: it turns -"your server will not tell you about this" into noise. +say nothing. Firing one engine's rules at another's corpus inverts that: it +turns "your server will not tell you about this" into noise. Each profile carries `suppressed_lints`, and an entry ending in `.` suppresses the whole prefix: -- **Ironcore** — nothing suppressed. It is the reference engine. +- **Ironcore** — nothing suppressed, because every rule in `lint.rs` was + written against a real Ironcore consequence. That is a fact about where the + rules came from, not a ranking: a rule with no Ironcore counterpart would be + suppressed there like anywhere else. - **TFS, TVP, Nostalrius** — the pacifist rules, since only Ironcore has that system. TVP and Nostalrius additionally drop `flag.staticattack-over-100`. - **Canary, Crystal, BlackTek** — `registry.` and (BlackTek) `raceid.` @@ -216,7 +219,7 @@ Nothing else catches it: ```sh cd src-tauri -cargo run --release --example probe_monster -- ../assets/Ironcore/monsters --mutate +cargo run --release --example probe_monster -- ../assets/TFS/monster --mutate ``` **Run every gate against all seven engines' own corpora when touching the diff --git a/README.md b/README.md index c1c0195..3540fa4 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ -A monster editor for OpenTibia servers — Ironcore, TheForgottenServer, TheVioletProject, Nostalrius, Canary/OTServBR, CrystalServer and BlackTek. Open a workspace, pick a monster, edit, save. +A monster editor for OpenTibia servers — TheForgottenServer, Ironcore, TheVioletProject, Nostalrius, Canary/OTServBR, CrystalServer and BlackTek. Open a workspace, pick a monster, edit, save. A workspace is up to four folders: the server's `monster/` folder, its `items/` folder, a client folder and optionally `spells/`. **Only the monsters folder is required** — the rest fill in what they can. Outfits, corpses and loot render as real sprites because the client assets are loaded alongside the monsters, from either a `.spr`/`.dat` pair or a modern 12.x+ asset bundle. diff --git a/package.json b/package.json index 8f47dc7..e1f63ae 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "monx", "private": true, - "version": "0.1.131", + "version": "0.1.132", "license": "MIT", "type": "module", "packageManager": "bun@1.3.14", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 90ea2a2..7413989 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1803,7 +1803,7 @@ dependencies = [ [[package]] name = "monx" -version = "0.1.131" +version = "0.1.132" dependencies = [ "byteorder", "image", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9e6b676..f08ff15 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "monx" -version = "0.1.131" -description = "MONx — Ironcore monster XML editor" -authors = ["Ironcore"] +version = "0.1.132" +description = "MONx — OpenTibia monster editor" +authors = ["Coldensjo"] edition = "2021" license = "MIT" diff --git a/src-tauri/examples/probe_monster.rs b/src-tauri/examples/probe_monster.rs index d52c6e5..ff2bc24 100644 --- a/src-tauri/examples/probe_monster.rs +++ b/src-tauri/examples/probe_monster.rs @@ -90,6 +90,11 @@ fn main() { // The committed fixtures, so a fresh clone can run the gates without // first populating the (gitignored) `assets/` workspaces. They are a // smoke test, not coverage — see fixtures/README.md. + // + // Ironcore's is the one a bare run picks not because that engine is + // special but because its fixture is the fullest: it is the only corpus + // here with the pacifist voices, so `voice_extras_survive` has + // something to exercise. Name the folder to run any of the other six. .unwrap_or_else(|| PathBuf::from("fixtures/engines/ironcore/monsters")); // The CI surface. `--format` is what makes this usable as a check in a // datapack repository: a machine document on stdout (or in a file), and an diff --git a/src-tauri/fixtures/README.md b/src-tauri/fixtures/README.md index b61ad15..30ad75f 100644 --- a/src-tauri/fixtures/README.md +++ b/src-tauri/fixtures/README.md @@ -13,7 +13,7 @@ engines/ one tiny workspace per engine (below) ```sh cd src-tauri -cargo run --release --example probe_monster # defaults to engines/ironcore/monsters +cargo run --release --example probe_monster # engines/ironcore/monsters — the fullest fixture, not a default engine cargo run --release --example probe_monster -- fixtures/engines/tvp/monster --engine tvp --mutate cargo run --release --example probe_lua -- fixtures/engines/canary/monster ``` @@ -22,7 +22,7 @@ Each engine gets a **workspace root** — `engines//` — with the monst folder inside it, named the way that server names it (`monsters` for Ironcore, `monster` for the rest). The nesting is not decoration: probes resolve a `spells/` sibling from the monsters folder's parent, so a flatter layout has -every engine inherit `fixtures/spells/spells.xml`, which is Ironcore's. That +every engine inherit `fixtures/spells/spells.xml`, which is an Ironcore file. That made Canary's `combat`/`COMBAT_ICEDAMAGE` spells lint as unknown names — an artifact of the layout, not a finding. diff --git a/src-tauri/src/catalog.rs b/src-tauri/src/catalog.rs index 4692679..dbe6ce7 100644 --- a/src-tauri/src/catalog.rs +++ b/src-tauri/src/catalog.rs @@ -140,7 +140,7 @@ pub fn immunity_combat_type(name: &str) -> Option<&'static str> { // ---------- §11 Elements ---------- -/// Ironcore's eleven. This is the *default* table, not the authority — ask +/// Ironcore's eleven. It is one engine's table, not the authority — ask /// `EngineProfile::is_element_attr`, which is what every caller now does. It /// deliberately has no `agonyPercent`: that is CrystalServer's twelfth type and /// lives on `ELEMENTS_CRYSTAL`, so listing it here would tell an Ironcore diff --git a/src-tauri/src/engine/detect.rs b/src-tauri/src/engine/detect.rs index d8fac6a..3261d6f 100644 --- a/src-tauri/src/engine/detect.rs +++ b/src-tauri/src/engine/detect.rs @@ -342,7 +342,7 @@ fn contains(haystack: &[u8], needle: &[u8]) -> bool { #[serde(rename_all = "camelCase")] pub struct EngineDetection { pub candidates: Vec, - /// Best guess, always populated (falls back to Ironcore). + /// Best guess, always populated (falls back to the default profile). pub best: String, /// False when nothing scored, or when the top two are close enough that /// picking silently would be a guess dressed as a fact. @@ -354,12 +354,19 @@ const MIN_SCORE: i32 = 40; const MIN_LEAD: i32 = 25; pub fn detection(candidates: Vec) -> EngineDetection { - let best = candidates - .first() - .map(|c| c.key.clone()) - .unwrap_or_else(|| "ironcore".to_string()); let top = candidates.first().map(|c| c.score).unwrap_or(0); let second = candidates.get(1).map(|c| c.score).unwrap_or(0); + // A corpus that scored nothing has not named an engine, and neither should + // this. `detect` sorts stably, so the head of an all-zero list is whichever + // profile happens to sit first in `ALL` — table order dressed as evidence, + // and for years that meant every unrecognisable corpus was called Ironcore. + // Name the default profile instead, and let `confident: false` say it is a + // fallback rather than a reading. + let best = if top > 0 { + candidates.first().map(|c| c.key.clone()).unwrap_or_default() + } else { + super::default_profile().key.to_string() + }; EngineDetection { confident: top >= MIN_SCORE && top - second >= MIN_LEAD, best, diff --git a/src-tauri/src/engine/mod.rs b/src-tauri/src/engine/mod.rs index 9f0a63e..9ce20b9 100644 --- a/src-tauri/src/engine/mod.rs +++ b/src-tauri/src/engine/mod.rs @@ -1,13 +1,14 @@ //! Engine profiles — which server's rules a workspace is being edited under. //! -//! MONx was written against Ironcore. TheForgottenServer, TheVioletProject and -//! Nostalrius read the same-*looking* monster XML and do materially different -//! things with it: TFS spells the bestiary id `raceId` where Ironcore spells it -//! `raceid`, TVP has no `hostile` flag at all, Nostalrius keeps melee on the -//! `` container instead of in a spell block, and all three name magic -//! effects `firearea` where Ironcore names them `CONST_ME_FIREAREA`. +//! Ironcore, TheForgottenServer, TheVioletProject and Nostalrius read the +//! same-*looking* monster XML and do materially different things with it: TFS +//! spells the bestiary id `raceId` where Ironcore spells it `raceid`, TVP has no +//! `hostile` flag at all, Nostalrius keeps melee on the `` container +//! instead of in a spell block, and Ironcore names magic effects +//! `CONST_ME_FIREAREA` where the other three name them `firearea`. No engine +//! here is the one MONx is *for*; each is a profile like the rest. //! -//! Every table here was read out of that engine's own `monsters.cpp` and +//! Every table here was read out of that server's own `monsters.cpp` and //! `tools.cpp` (kept under `sources/` while this was written) rather than //! inferred from upstream docs — the whole reason this module exists is that //! the engines disagree in ways no document records. @@ -50,7 +51,7 @@ //! [`detect`] depends on none of it — it takes corpus bytes and returns scored //! candidates, naming engines by key string only. //! -//! Everything is re-exported here, so the paths callers use — `engine::IRONCORE`, +//! Everything is re-exported here, so the paths callers use — `engine::TFS`, //! `engine::EngineProfile`, `engine::detect` — are the same as when this was one //! file. Submodules reach each other through `use super::*`. diff --git a/src-tauri/src/engine/profiles.rs b/src-tauri/src/engine/profiles.rs index 61cd673..df6aed0 100644 --- a/src-tauri/src/engine/profiles.rs +++ b/src-tauri/src/engine/profiles.rs @@ -572,9 +572,14 @@ pub static CRYSTAL: EngineProfile = EngineProfile { ], }; +/// Every profile, in the order they are offered to the user: the four XML +/// engines, then the three Lua ones. The order carries no ranking — the head of +/// this list is not a default, and `detect` deliberately does not let the order +/// break a tie (see `detection`). `default_profile` is the one place a fallback +/// engine is named. pub static ALL: &[&EngineProfile] = &[ - &IRONCORE, &TFS, + &IRONCORE, &TVP, &NOSTALRIUS, &CANARY, @@ -587,7 +592,14 @@ pub fn by_key(key: &str) -> Option<&'static EngineProfile> { } /// The profile to fall back to when nothing is specified and nothing detects. +/// +/// TFS 1.x, and not because MONx has a home engine — it does not. It is the +/// shape the other XML engines are variants of, and the most conservative of +/// them: it declares fewer extras than Ironcore or TVP, so anything it does not +/// name rides along as an unknown attribute. A profile that under-declares +/// makes MONx quiet rather than destructive (see the module docs), which is the +/// failure mode to pick when the engine is a guess. pub fn default_profile() -> &'static EngineProfile { - &IRONCORE + &TFS } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 4c71886..3c621f4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "MONx", - "version": "0.1.131", + "version": "0.1.132", "identifier": "com.ironcore.monx", "build": { "beforeDevCommand": "bun run dev", diff --git a/src/App.tsx b/src/App.tsx index 49d6b2f..41dbdec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,7 +33,7 @@ export interface Toast { const THEMES = ['lined', 'light', 'dark'] as const; type Theme = (typeof THEMES)[number]; -/** "…/Ironcore/data/monster" → "Ironcore". Falls back to the folder itself. */ +/** "…/MyServer/data/monster" → "MyServer". Falls back to the folder itself. */ export function workspaceLabel(monstersPath: string): string { const parts = monstersPath.split(/[\\/]/).filter(Boolean); return parts[parts.length - 3] ?? parts[parts.length - 1] ?? monstersPath; diff --git a/src/catalog.ts b/src/catalog.ts index dcf6a23..019572f 100644 --- a/src/catalog.ts +++ b/src/catalog.ts @@ -537,7 +537,6 @@ export interface BooleanFlag { /** Loader default when the flag is absent. */ default: boolean; group: 'behaviour' | 'push' | 'terrain' | 'pacifist'; - ironcore?: boolean; note?: string; } @@ -548,25 +547,25 @@ export const BOOLEAN_FLAGS: BooleanFlag[] = [ { key: 'convinceable', label: 'Convinceable', default: false, group: 'behaviour', note: 'Needs a non-zero mana cost.' }, { key: 'illusionable', label: 'Illusionable', default: false, group: 'behaviour' }, { key: 'challengeable', label: 'Challengeable', default: true, group: 'behaviour', note: 'exeta res can force a target switch.' }, - { key: 'isboss', label: 'Boss', default: false, group: 'behaviour', ironcore: true, note: 'Metadata only — read from Lua by killboss.lua to broadcast the kill.' }, + { key: 'isboss', label: 'Boss', default: false, group: 'behaviour', note: 'Metadata only — read from Lua by killboss.lua to broadcast the kill.' }, { key: 'ignorespawnblock', label: 'Ignore spawn block', default: false, group: 'behaviour' }, { key: 'hidehealth', label: 'Hide health bar', default: false, group: 'behaviour' }, - { key: 'whiteskullonattack', label: 'White skull on attack', default: false, group: 'behaviour', ironcore: true, note: 'Damaging it PvP-flags the player.' }, + { key: 'whiteskullonattack', label: 'White skull on attack', default: false, group: 'behaviour', note: 'Damaging it PvP-flags the player.' }, { key: 'pushable', label: 'Pushable by players', default: true, group: 'push', note: 'Forced off at load when "pushes creatures" is set.' }, { key: 'canpushitems', label: 'Pushes items', default: false, group: 'push' }, { key: 'canpushcreatures', label: 'Pushes creatures', default: false, group: 'push', note: 'Implies pushable = 0.' }, - { key: 'canpushplayers', label: 'Pushes players', default: false, group: 'push', ironcore: true }, - { key: 'cannotmove', label: 'Cannot move', default: false, group: 'push', ironcore: true, note: 'Never takes a step — distinct from speed 0.' }, - { key: 'corpseunmovable', label: 'Corpse unmovable', default: false, group: 'push', ironcore: true }, + { key: 'canpushplayers', label: 'Pushes players', default: false, group: 'push' }, + { key: 'cannotmove', label: 'Cannot move', default: false, group: 'push', note: 'Never takes a step — distinct from speed 0.' }, + { key: 'corpseunmovable', label: 'Corpse unmovable', default: false, group: 'push' }, { key: 'canwalkonenergy', label: 'Walks on energy', default: true, group: 'terrain' }, { key: 'canwalkonfire', label: 'Walks on fire', default: true, group: 'terrain' }, { key: 'canwalkonpoison', label: 'Walks on poison', default: true, group: 'terrain' }, - { key: 'pacifist', label: 'Pacifist', default: false, group: 'pacifist', ironcore: true, note: 'Dormant until attacked. Forces hostile = 0 at load.' }, - { key: 'deaggroonkill', label: 'De-aggro on kill', default: false, group: 'pacifist', ironcore: true, note: 'Pacifist only.' }, - { key: 'singletarget', label: 'Single target', default: false, group: 'pacifist', ironcore: true, note: 'Pacifist only — only actual attackers count as opponents.' }, + { key: 'pacifist', label: 'Pacifist', default: false, group: 'pacifist', note: 'Dormant until attacked. Forces hostile = 0 at load.' }, + { key: 'deaggroonkill', label: 'De-aggro on kill', default: false, group: 'pacifist', note: 'Pacifist only.' }, + { key: 'singletarget', label: 'Single target', default: false, group: 'pacifist', note: 'Pacifist only — only actual attackers count as opponents.' }, ]; export interface NumericFlag { @@ -578,7 +577,6 @@ export interface NumericFlag { min: number; max: number; group: 'behaviour' | 'push' | 'terrain' | 'pacifist'; - ironcore?: boolean; note?: string; } @@ -588,14 +586,14 @@ export const NUMERIC_FLAGS: NumericFlag[] = [ { key: 'runonhealth', label: 'Flee below HP', default: 0, corpusDefault: null, min: 0, max: 100000, group: 'behaviour' }, { key: 'lightlevel', label: 'Light level', default: 0, corpusDefault: null, min: 0, max: 255, group: 'behaviour' }, { key: 'lightcolor', label: 'Light colour', default: 0, corpusDefault: null, min: 0, max: 255, group: 'behaviour' }, - { key: 'leashradius', label: 'Leash radius', default: 0, corpusDefault: null, min: 0, max: 50, group: 'pacifist', ironcore: true, note: 'Pacifist only — returns home beyond this, and tightens the dormant despawn radius.' }, + { key: 'leashradius', label: 'Leash radius', default: 0, corpusDefault: null, min: 0, max: 50, group: 'pacifist', note: 'Pacifist only — returns home beyond this, and tightens the dormant despawn radius.' }, ]; export const FLAG_GROUP_LABEL: Record = { behaviour: 'Behaviour', push: 'Pushing and movement', terrain: 'Terrain', - pacifist: 'Pacifist (Ironcore)' + pacifist: 'Pacifist' }; // ---------- lookups ---------- diff --git a/src/customeffects.ts b/src/customeffects.ts index e5cb992..350b428 100644 --- a/src/customeffects.ts +++ b/src/customeffects.ts @@ -3,14 +3,14 @@ // The engine tables in `engine.ts` and `engine/tables.rs` are each read out of a real // server's source, which is what makes them worth trusting — and exactly why // they cannot cover the effect somebody added to their own fork last Tuesday. -// Without this, magic effect 105 on a modified Ironcore is invisible in the -// picker, indistinguishable from no effect at all, and linted as "dropped" by a -// loader that handles it perfectly well. +// Without this, the magic effect somebody added to their own build is invisible +// in the picker, indistinguishable from no effect at all, and linted as +// "dropped" by a loader that handles it perfectly well. // // So the catalogue the editor offers is the engine's table *plus* whatever the // user declares here. Declarations never replace a stock entry: they are the // user's word about their own server, which is why they live in settings rather -// than beside `ME_IRONCORE`. +// than beside the engine tables. import { invoke } from '@tauri-apps/api/core'; import type { EffectEntry } from './catalog'; diff --git a/src/engine.ts b/src/engine.ts index b66b51b..9d4db4c 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -9,7 +9,7 @@ import { MAGIC_EFFECTS, SHOOT_EFFECTS, type EffectEntry } from './catalog'; -export type EngineKey = 'ironcore' | 'tfs' | 'tvp' | 'nostalrius' | 'canary' | 'crystal' | 'blacktek'; +export type EngineKey = 'tfs' | 'ironcore' | 'tvp' | 'nostalrius' | 'canary' | 'crystal' | 'blacktek'; /** How effect values are spelled. Ironcore matches `CONST_ME_*` * case-sensitively; everyone else lower-cases a short name before lookup. */ @@ -392,11 +392,17 @@ const BLACKTEK: EngineInfo = { shootEffects: SHOOT_EFFECTS }; -export const ENGINES: EngineInfo[] = [IRONCORE, TFS, TVP, NOSTALRIUS, CANARY, CRYSTAL, BLACKTEK]; +/** Same order as `engine/profiles.rs` `ALL`, which is the order the Landing + * picker offers them in. It carries no ranking — the first entry is not a + * default and detection does not let the order break a tie. */ +export const ENGINES: EngineInfo[] = [TFS, IRONCORE, TVP, NOSTALRIUS, CANARY, CRYSTAL, BLACKTEK]; const BY_KEY = new Map(ENGINES.map(e => [e.key, e])); -/** Falls back to Ironcore, which is what the backend does too. */ +/** Falls back to TFS — `engine/profiles.rs` `default_profile()` and the reason + * it is that one rather than any other. Keep the two sides agreeing: a + * frontend that assumes a different engine to the backend mislabels every + * field on screen while the file on disk is read under other rules. */ export function engineInfo(key: string | null | undefined): EngineInfo { - return BY_KEY.get((key ?? '') as EngineKey) ?? IRONCORE; + return BY_KEY.get((key ?? '') as EngineKey) ?? TFS; } diff --git a/src/fixtures.ts b/src/fixtures.ts index 8b58301..b7dfe00 100644 --- a/src/fixtures.ts +++ b/src/fixtures.ts @@ -249,9 +249,9 @@ export const FIXTURE_DEMON: MonsterDoc = { export const FIXTURE_WORKSPACE: WorkspaceInfo = { paths: { - monsters: 'C:\\Ironcore\\data\\monsters', - items: 'C:\\Ironcore\\data\\items', - client: 'C:\\Ironcore\\client', + monsters: 'C:\\MyServer\\data\\monsters', + items: 'C:\\MyServer\\data\\items', + client: 'C:\\MyServer\\client', spells: null, engine: 'ironcore' }, @@ -263,8 +263,8 @@ export const FIXTURE_WORKSPACE: WorkspaceInfo = { orphanCount: 11, itemCount: 11863, otbVersion: 'OTB 2.7.2', - sprPath: 'C:\\Ironcore\\client\\Tibia.spr', - datPath: 'C:\\Ironcore\\client\\Tibia.dat', + sprPath: 'C:\\MyServer\\client\\Tibia.spr', + datPath: 'C:\\MyServer\\client\\Tibia.dat', spriteCount: 10313, transparent: false, enhancedAnimations: false, diff --git a/src/locales/en.ts b/src/locales/en.ts index 12b0dd0..7d01817 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -29,6 +29,8 @@ const en: Record = { '{{count}} silent-data-loss issue_other': '{{count}} silent-data-loss issues', '{{count}} lint_one': '{{count}} lint', '{{count}} lint_other': '{{count}} lints', + '{{count}} event_one': '{{count}} event', + '{{count}} event_other': '{{count}} events', 'Fix all ({{count}})_one': 'Fix all ({{count}})', 'Fix all ({{count}})_other': 'Fix all ({{count}})', 'Fixed {{count}} lint_one': 'Fixed {{count}} lint', diff --git a/src/locales/pl.ts b/src/locales/pl.ts index 2d55a35..0878606 100644 --- a/src/locales/pl.ts +++ b/src/locales/pl.ts @@ -29,6 +29,9 @@ const pl: Record = { '{{count}} lint_one': '{{count}} uwaga', '{{count}} lint_few': '{{count}} uwagi', '{{count}} lint_many': '{{count}} uwag', + '{{count}} event_one': '{{count}} zdarzenie', + '{{count}} event_few': '{{count}} zdarzenia', + '{{count}} event_many': '{{count}} zdarzeń', 'Fix all ({{count}})_one': 'Napraw wszystko ({{count}})', 'Fix all ({{count}})_few': 'Napraw wszystko ({{count}})', 'Fix all ({{count}})_many': 'Napraw wszystko ({{count}})', @@ -1087,8 +1090,7 @@ const pl: Record = { Behaviour: 'Zachowanie', 'Pushing and movement': 'Popychanie i ruch', Terrain: 'Teren', - 'Pacifist (Ironcore)': 'Pacyfista (Ironcore)', - '{{note}} (Ironcore)': '{{note}} (Ironcore)', + Pacifist: 'Pacyfista', '“Pushes creatures” forces “pushable by players” off at load — the value written here will not survive.': '„Popycha stworzenia” wymusza wyłączenie „popychalny przez graczy” przy wczytaniu — zapisana tu wartość nie przetrwa.', 'Target change': 'Zmiana celu', @@ -1112,8 +1114,8 @@ const pl: Record = { 'Pancerz redukuje trafienia wręcz i fizyczne; obrona liczy się tylko przy trafieniach, które ją sprawdzają, czyli wręcz.', Armor: 'Pancerz', Defense: 'Obrona', - 'Show pacifist system (Ironcore)': 'Pokaż system pacyfisty (Ironcore)', - 'Hide pacifist system (Ironcore)': 'Ukryj system pacyfisty (Ironcore)', + 'Show pacifist system': 'Pokaż system pacyfisty', + 'Hide pacifist system': 'Ukryj system pacyfisty', 'A dormant monster that only fights back once struck. The sub-flags do nothing without Pacifist.': 'Uśpiony potwór, który walczy dopiero po uderzeniu. Podflagi nic nie robią bez Pacyfisty.', 'Pacifist forces {{flag}} to 0 during load — writing both as 1 will not survive.': @@ -1235,7 +1237,7 @@ const pl: Record = { // --- Pacifist & events ------------------------------------------------------ '{{pacifist}} pacifist · {{events}} events': '{{pacifist}} pacyfisty · {{events}} zdarzeń', - 'Pacifist lines (Ironcore)': 'Kwestie pacyfisty (Ironcore)', + 'Pacifist lines': 'Kwestie pacyfisty', 'Said once when it wakes, and when it walks past its leash radius. Neither is part of the random voice pool.': 'Wypowiadane raz przy przebudzeniu oraz przy wyjściu poza promień smyczy. Żadne nie należy do losowej puli kwestii.', 'Only spoken by a pacifist monster — turn Pacifist on in Combat, or these never fire.': diff --git a/src/locales/pt.ts b/src/locales/pt.ts index 8956ebf..7ef2cb9 100644 --- a/src/locales/pt.ts +++ b/src/locales/pt.ts @@ -25,6 +25,8 @@ const pt: Record = { '{{count}} silent-data-loss issue_other': '{{count}} perdas silenciosas de dados', '{{count}} lint_one': '{{count}} apontamento', '{{count}} lint_other': '{{count}} apontamentos', + '{{count}} event_one': '{{count}} evento', + '{{count}} event_other': '{{count}} eventos', 'Fix all ({{count}})_one': 'Corrigir tudo ({{count}})', 'Fix all ({{count}})_other': 'Corrigir tudo ({{count}})', 'Fixed {{count}} lint_one': '{{count}} apontamento corrigido', @@ -993,8 +995,7 @@ const pt: Record = { Behaviour: 'Comportamento', 'Pushing and movement': 'Empurrar e movimento', Terrain: 'Terreno', - 'Pacifist (Ironcore)': 'Pacifista (Ironcore)', - '{{note}} (Ironcore)': '{{note}} (Ironcore)', + Pacifist: 'Pacifista', '“Pushes creatures” forces “pushable by players” off at load — the value written here will not survive.': '“Empurra criaturas” força “empurrável por jogadores” a ficar desligado no carregamento — o valor gravado aqui não sobrevive.', 'Target change': 'Troca de alvo', @@ -1019,8 +1020,8 @@ const pt: Record = { 'A armadura reduz golpes corpo a corpo e físicos; a defesa só é consultada em golpes que a verificam, ou seja, corpo a corpo.', Armor: 'Armadura', Defense: 'Defesa', - 'Show pacifist system (Ironcore)': 'Mostrar sistema pacifista (Ironcore)', - 'Hide pacifist system (Ironcore)': 'Ocultar sistema pacifista (Ironcore)', + 'Show pacifist system': 'Mostrar sistema pacifista', + 'Hide pacifist system': 'Ocultar sistema pacifista', 'A dormant monster that only fights back once struck. The sub-flags do nothing without Pacifist.': 'Um monstro dormente que só revida depois de ser atingido. As sub-flags não fazem nada sem o Pacifista.', 'Pacifist forces {{flag}} to 0 during load — writing both as 1 will not survive.': @@ -1142,7 +1143,7 @@ const pt: Record = { // --- Pacifist & events ------------------------------------------------------------------------------------------------ '{{pacifist}} pacifist · {{events}} events': '{{pacifist}} pacifista · {{events}} eventos', - 'Pacifist lines (Ironcore)': 'Falas de pacifista (Ironcore)', + 'Pacifist lines': 'Falas de pacifista', 'Said once when it wakes, and when it walks past its leash radius. Neither is part of the random voice pool.': 'Dito uma vez ao acordar, e ao ultrapassar o raio da coleira. Nenhum dos dois faz parte do conjunto aleatório de falas.', 'Only spoken by a pacifist monster — turn Pacifist on in Combat, or these never fire.': diff --git a/src/prefs.ts b/src/prefs.ts index d2ee983..e814fe1 100644 --- a/src/prefs.ts +++ b/src/prefs.ts @@ -17,8 +17,9 @@ export interface Prefs { visibleSections: SectionId[]; } -/** Pacifist & Events: two Ironcore strings and the creaturescript list, both - * empty on most monsters, so the tab stays out of the way until asked for. */ +/** Pacifist & Events: the two pacifist strings and the creaturescript list, + * both empty on most monsters, so the tab stays out of the way until asked + * for. */ const HIDDEN_BY_DEFAULT: readonly SectionId[] = ['events']; /** Engine-specific sections. They are gated by the active engine anyway, so a diff --git a/src/sections/Combat.tsx b/src/sections/Combat.tsx index 61eb359..e2795d5 100644 --- a/src/sections/Combat.tsx +++ b/src/sections/Combat.tsx @@ -56,15 +56,14 @@ export function Combat({ doc, patch, lintAt, readOnly, collapsed, onToggle }: Pr
{BOOLEAN_FLAGS.filter(f => f.group === group && engine.boolFlags.includes(f.key)).map(f => { const value = boolFlag(f.key, f.default); + // No "this one is an extra" marker on the label: the filter + // above already said so by not rendering the flags this + // engine lacks. return ( - {/* The pacifist system is Ironcore-only; every other loader reports its - flags as unknown and ignores them. */} + {/* Only Ironcore has the pacifist system; every other loader reports its + flags as unknown and ignores them, so `engine.pacifist` is what says + whose corpus this is and the labels need not repeat it. */} {engine.pacifist && (
{showPacifist && (