diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 017d3ec..467e465 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,12 @@ jobs: if [ "$files" = "VERSION" ]; then echo "code=false" >> "$GITHUB_OUTPUT" echo "Version-only PR; skipping the build." + elif printf '%s\n' "$files" | grep -qx VERSION; then + # The release workflow bumps VERSION and applies its own `bump` on top of whatever it + # reads, so a hand-edit that reaches master is counted twice and skips a minor. That + # is how 0.5 was lost. A version-only PR is the release's own and passes above. + echo "::error::VERSION is the release workflow's to move. Drop it from this PR." + exit 1 else echo "code=true" >> "$GITHUB_OUTPUT" fi diff --git a/CLAUDE.md b/CLAUDE.md index 14a3973..603912f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,7 @@ read whole; each is one layer. | [docs/trade-layer.md](docs/trade-layer.md) | `src/trade/` — query building, the two-step client, the rate limiter, and how results and the filter list are drawn. | | [docs/ninja.md](docs/ninja.md) | `src/ninja/` — the poe.ninja reference price. | | [docs/exchange.md](docs/exchange.md) | `src/exchange/` — GGG's hourly in-game currency exchange digests. | +| [docs/map-check.md](docs/map-check.md) | **Not built.** The design for 0.7 — mod domains, the map modifier pool, and the bundle and data-layer changes it needs first. Read before touching either for map check. | | [docs/localisation.md](docs/localisation.md) | Reading a translated client vs. translating our own text — two unrelated problems, two settings. | | [docs/external-apis.md](docs/external-apis.md) | The endpoints themselves: trade, poe.ninja, currency exchange, and GGG's rate-limit policy. | | [docs/conventions.md](docs/conventions.md) | Comment style, commit and PR shape, the maintainer alias, which docs are public. | diff --git a/ROADMAP.md b/ROADMAP.md index 0291f4e..b1d079f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -8,14 +8,19 @@ A version that has shipped stays on this page, marked **shipped** and written in so the list reads as the whole road rather than only the part still ahead. **One feature per minor, no feature on a build.** `MAJOR.MINOR` is the [VERSION](VERSION) file and -`BUILD` is the CI run counter, so `0.4.31` → `0.4.32` is a fix and `0.4` → `0.5` is the next thing -on this list. A minor stays open as long as its fixes keep arriving. +`BUILD` is the CI run counter, so `0.6.31` → `0.6.32` is a fix and `0.6` → `0.7` is the next thing +on this list. A minor stays open as long as its fixes keep arriving. The release workflow owns that +file; it is never edited by hand. -**0.7 is the only version whose place is not fixed.** It waits on the +**0.8 is the only version whose place is not fixed.** It waits on the [data repository](https://github.com/JIRPOS/PathOfPriceCheck-Data) rather than on this one, and lands whenever the first localised bundle is ready - if that is early, everything behind it shifts up. +**There is no 0.5.** The release after 0.4 was cut with the minor already moved by hand, so the +bump landed twice and QuickPaste shipped as 0.6. The number is spent; nothing is missing from this +page. + For what is deliberately *not* planned, and why, see [docs/roadmap.md](docs/roadmap.md). ## 0.3 - The application updates itself - **shipped** @@ -70,7 +75,7 @@ could only tick it or leave it. **Does not** - carry an edit onto the next item. Bounds belong to the item in hand. -## 0.5 - QuickPaste - **shipped** +## 0.6 - QuickPaste - **shipped** A hotkey opens a small window at the cursor listing saved snippets - a map regex, a vendor search, a whisper you send twenty times an evening. @@ -92,7 +97,7 @@ search, a whisper you send twenty times an evening. **Might** - grow that keystroke as an option later if it turns out to be wanted. -## 0.6 - Map check +## 0.7 - Map check A hotkey that reads a map's rolled modifiers and tells you which ones you decided you cannot take. @@ -113,13 +118,13 @@ modifier an excluding term hits is *proposed* dangerous, every one a wanted term safe. You confirm; from then on the table is what the tool believes. It imports imperfectly on purpose - the game's search reads a whole item where this reads modifier wordings, and a term written against a printed number is being matched against a placeholder - so it is a head start, -not an answer. The regexes come from the paste list in 0.5. +not an answer. The regexes come from the paste list in 0.6. **Might - switch profile automatically** by watching `LatestClient.log`. Outside the 1.0 promise: it ships if it is cheap and is dropped without argument if it is not. Either way it goes in [PRIVACY.md](PRIVACY.md), as does the verdict table. -## 0.7 - Every language the client speaks +## 0.8 - Every language the client speaks Already built on this side: every word the client prints is read from a table, and Settings has a **Client language** row. Only the data is missing, which is why this version floats. @@ -131,7 +136,7 @@ Already built on this side: every word the client prints is read from a table, a - Ship each language only once it has been checked against a real capture from a client actually set to it. -## 0.8 - This tool's own text, in more than English +## 0.9 - This tool's own text, in more than English Reading a translated client and translating the buttons are two different problems and two settings; the machinery for the second is written and only the tables are missing. @@ -139,7 +144,7 @@ settings; the machinery for the second is written and only the tables are missin **Might** - Latin-script languages first. **This is the one version here that can be dropped**: it is cosmetic, 1.0 does not wait on it, and if it slips past 1.0 the README will say so. -## 0.9.x - The long window +## The long window - the last minor before 1.0 **No features. Fixes only, for as long as it takes**, and longer than the windows between the versions above. diff --git a/docs/map-check.md b/docs/map-check.md new file mode 100644 index 0000000..05db3e2 --- /dev/null +++ b/docs/map-check.md @@ -0,0 +1,288 @@ +# Map check (not built) + + + +**Nothing in this document is built.** It is the design for [ROADMAP.md](../ROADMAP.md)'s 0.7 and +for the two layers underneath it that have to move first — the data bundle and the app's data +layer. Sections become the layer's own documentation as they ship; until then read every sentence +as intent. + +**[roadmap.md](roadmap.md)'s constraints for this version come first**, and this document is +written under them rather than beside them. Where the two disagree, that one wins. + +The feature: a hotkey reads a map's rolled modifiers and says which ones you decided you cannot +take. The verdicts live in a table meant to fill in by being used — rate what the popup shows, on +the spot. Nothing about the feature requires knowing what a map *could* have rolled. + +**The store keys on the stat record, never on the printed line's `placeholder_form`.** A wording is +language-dependent the moment a localised bundle exists, and `find_stat` already refuses to guess +between two records that share one. So resolution happens first and the verdict attaches to what it +resolved to — which also means a modifier printing several stats carries a verdict per stat, since +that is what the store can key. The pool below is grouped by modifier for reading; the store +underneath it is not. + +What a **pool** buys is the one thing that cannot fill in by use: pre-filling. A searchable list of +every modifier that exists, so a user can rate ones they have not met yet, or paste a regex and +have it propose verdicts across the lot. That is the whole reason the bundle grows a new asset, and +it is worth keeping in view — the pool is a convenience for one settings page, not a correctness +requirement anywhere. + +**"A map" means anything that opens in the map device**: ordinary maps, nightmare and Originator +maps, unique maps, charts, expedition logbooks and invitations. That is a scope decision, and the +game's own data groups them the same way — one domain, which is the section below. Maven's +invitations are handed over by Kirac now rather than slotted, and are kept in scope anyway, because +they are still rollable and still carry modifiers a player wants a verdict on. **Vaal side areas +are out**: there is no copy of a side area to read, and reading one off the screen is not something +this project does. + +Every layer built so far starts from the item in hand, so nothing in the bundle describes a pool +except the per-unique dataset, which is built from poewiki rather than from the client. Map check +is the first feature where the client's own data expresses one directly. + +## What the game's data actually says + +Measured against patch **3.29.2.1** / data bundle **20260811.45**. Re-derivable from the data +repo's cached `builder/.work/tables/English/*.json`; the numbers are stated so a later patch can be +compared against them rather than re-argued. + +**Two independent axes.** `Mods.Domain` is the *pool namespace* a modifier is generated from; +`Mods.GenerationType` is *how it arrives* (prefix, suffix, unique, corrupted, enchant, essence…). +Both are needed: a map's affixes are domain 5 + generation 1/2, its Vaal corruption implicits are +domain 5 + generation 5. Four live domains and two live generation types have no name in +[dat-schema](https://github.com/poe-tool-dev/dat-schema), so **the numbers are the identity** and +any display name is an app-side table with a fallback. + +**Domain 5 is `AREA` and it is the map pool.** 407 prefixes + 393 suffixes = 800 rows, which +collapse to **207 distinct wording-sets** once tier variants fold together — and they do fold, +because a verdict is attached to a wording, not to a roll. Of those: + +| | | +| --- | --- | +| `MapCorruptedSideArea*` / `*Map2Tier*` only — a Vaal side area's own mods and legacy map-series rows, neither of which prints on a map item | ~52 | +| `MapUber*`-exclusive — nightmare and Originator only | 59 | +| shared between `MapUber*` and ordinary map mods, so the distinction collapses at wording level | 55 | + +Leaving roughly **155 rateable wordings**, which is small enough that the settings page is a list +with a search box rather than a data-management problem. + +**The other map-adjacent domains.** Charts are **39** (49 prefixes, 32 suffixes), a domain +dat-schema does not name. **12** `LEAGUESTONE` holds legacy map mods that still collide with +domain-5 wordings. **14** `MAP_DEVICE` holds invitation implicits. **11** `ATLAS` holds +atlas-side mods. + +**Domain 5 is the map device.** Every kind of thing that opens in it shares the domain and carries +a tag on its base saying which kind it is. The tags are recorded here because they are what proves +the scope above is the game's own grouping rather than a guess — **they are not used**, and the +pool is deliberately not split by them: + +| item | domain | tags on the base | +| --- | --- | --- | +| ordinary map | 5 | `top_tier_map`, `mid_tier_map`, … | +| Nightmare Map | 5 | `uber_tier_map`, `top_tier_map`, `map_force_6_mods` | +| Maven's Invitation: The Feared | 5 | `maven_map`, `maven_void_map_feared` | +| Writhing Invitation | 5 | `primordial_map` | +| Expedition Logbook | 5 | `expedition_logbook` | +| Coral Reef Chart | 39 | `chart` | + +Domain 5's generation types say the same thing from the other side: 1 and 2 are the affixes, 5 the +Vaal corruption implicits, 23 `EXPEDITION_LOGBOOK`, 36 `MEMORY_ALTAR`, 8 `TEMPEST` (legacy), 3 the +fixed ones including `IsUberMap` and `MapZanaInfluenced`. + +One thing to fix while here: the bundle resolves an invitation to its **`Quest Items`** row +(`Metadata/Items/MapFragments/…/Quest…`, domain 43) while the clipboard prints +`Item Class: Misc Map Items`, which is the *other* row of the same name — domain 5, and the one +that carries the tags. Two bases share each invitation's name and the emit picks the wrong one. +Harmless today; not harmless once the base decides which pool is shown. + +**The three map variants, precisely.** + +- A **Nightmare Map** is a base — `Metadata/Items/Maps/MapKeyNightmareBoss`, tags `uber_tier_map` + and `map_force_6_mods`, implicit `IsUberMap`. +- An **Originator map** is *not* a base. It is an ordinary `Map` carrying the implicit + `MapZanaInfluenced` (`map_zana_influence` → `implicit.stat_2696470877`), and the half-and-half + pool follows from that implicit rather than from the base. +- An **invitation** is domain 5 like everything else, tagged `maven_map` (plus a per-boss tag such + as `maven_void_map_feared`) or `primordial_map` for the Eldritch four, and carries one fixed + implicit. Whether it *also* rolls affixes is not answerable from what the builder fetches, and + with an unpartitioned pool it does not need to be: whatever an invitation prints gets rated like + anything else. An earlier reading of a Normal-rarity capture as "invitations have no pool" was + unsound — that capture shows one invitation that had no affixes, not a base that cannot have + them. + +**A base has exactly one domain.** A flask is `FLASK`, not `ITEM` plus a refinement; the domains +are mutually exclusive namespaces. Only 11 of 82 item classes span two, and always because the +class holds genuinely different things (Jewels covers `BASE_JEWEL` and `AFFLICTION_JEWEL`). + +**Which is why domain buys nothing for gear, and everything for maps.** Granularity inside domain +1 comes from spawn weights against tags, and the class-level tag (`ring`, `flask`) is not even on +the base row — `Iron Ring` has an empty `TagsKeys` and reaches its tag through `InheritsFrom`, +whose tag set lives in the game's metadata files rather than in any dat table. Maps need none of +that because domain 5 is effectively one slot with one pool. + +**And nothing for uniques.** A unique's modifiers are domain 1, the same as a rare's; the +discriminator is generation type 3, except that 276 of the mod rows the unique dataset references +are ordinary generation 1/2 anyway. The unique→mod link is a relation, not a property of the mod, +which is what [UNIQUE-MODS.md](../UNIQUE-MODS.md) exists for. + +## The rule this whole design hangs on + +**The pool describes; it never gates.** It says what can spawn *naturally* on this kind of thing, +which is strictly less than what an item can print. An essence (generation type 11) puts a modifier +on a base whose weights would never have produced it, and so do crafted (domain 9), veiled +(26 / 28) and Harvest. On top of that the pool here is filtered by mod-id convention and is +knowingly imperfect. So: + +- **A modifier the item prints and the pool does not contain is normal, not an error.** It renders + as it does today, it is rateable on the spot like any other, and the verdict sticks — the store + keys on the resolved stat and has no idea whether the pool mentions it. +- The pool may be used to *offer* and to *pre-fill*. It may never be used to reject, to hide a + printed modifier, or to decide that a line failed to parse. + +## What the bundle gains + +One change in [the data repo](https://github.com/JIRPOS/PathOfPriceCheck-Data) — a separate +repository and therefore a separate change set — and one decision that keeps it to an emit. + +**1. One flat pool, deliberately not partitioned per base.** Domain 5 covers every map-device item +at once, and the pool is emitted exactly that way — a single list, not one list per kind of map. +The reason it can be is the shape of the feature: the popup shows only what the item in hand +actually rolled, so a modifier that could never appear on this base never comes up. The settings +list is a rating dictionary, and an entry that goes unrated for a year costs one row. + +**So `Mods.SpawnWeight_TagsKeys` / `SpawnWeight_Values` stay unfetched.** They are the only thing +in the game's data that says which base a modifier can spawn on — a membership list rather than the +probability model the name suggests — but nothing here consumes that. Splitting the list was the +only use, and the list is not split. **Decided, not deferred**: revisit only if something needs to +answer "can this base roll that", which nothing in 0.7 does. + +The one thing borrowed from that idea is **list hygiene, by mod id**: entries whose every mod row +matches `CorruptedSideArea` (a Vaal side area's own modifiers, which never print on anything the +user can copy) or `Map2Tier` (legacy map series) are left out of the emit. Roughly 52 of 207 +wording-sets. This is a naming convention rather than data, it is allowed to be imperfect, and the +cost of a mistake either way is one row in a searchable list. + +**2. Emit what is already fetched.** `Mods.Domain`, `Mods.GenerationType` and +`BaseItemTypes.ModDomain` are all downloaded today and dropped at emit time. + +- **`domain` on base-type records**, straight from `BaseItemTypes.ModDomain`. One integer, exact, + no ambiguity — this is how the app knows a Map is `AREA` and a chart is 39 without a compiled-in + name list. +- **A new optional asset, `en-mod-pools.ndjson`**, plus an fnv1a32 index over each entry's + normalized wordings, built with the same machinery as `en-stats-matcher.index.bin`. Named for + the general case and carrying its `domain` per record, because the settings page below is + pool-agnostic and flasks, abyss jewels and idols are the same shape. Seed it with domain 5 and + domain 39; nothing else, until something asks. +- **`source.mod_pools`** in the manifest, written only when non-zero, so "no data" is + distinguishable from "no pool for this domain" — the same reasoning as `source.exchange_items` + in [data-layer.md](data-layer.md). + +One record is one **wording-set**, not one mod row, because tiers collapse: + +```json +{"domain": 5, "gen": 1, "name": "Hungering", "tiers": 1, + "mods": ["MapUberModDrowningOrbs"], + "stats": [{"ref": "Area contains Drowning Orbs", + "trade": "explicit.stat_25225034", "min": null, "max": null}]} +``` + +`name` is `Mods.Name`, the affix name the client prints with Advanced Mod Descriptions on, and it +is free. `mods` is provenance, for a debug log that has to explain itself. `min`/`max` span the +lowest and highest tier **in displayed units with `dp` applied**, exactly as `unique_mods` already +emits ranges — `Mods.dat` stores hundredths and milliseconds raw and leaving that to the client is +a silent factor of 100. The ranges are not decoration: they are what lets a pasted regex be tested +against a rendered line rather than against a placeholder. + +**Deliberately not done: a domain set on stat records.** It was measured. 643 wordings are +rendered by more than one description block; domain sets separate only 47 of them, 335 overlap and +261 involve a block no mod uses. In the emitted file that is 4 wordings currently reported as +"ambiguous wording, not searched", none of them map mods. It buys too little to be worth a field on +every one of 12,288 records, and it cannot touch the larger class described next. + +## What it cannot fix, and must not be expected to + +791 records carry **more than one trade id inside a single namespace** (249 explicit, 506 crucible, +38 enchant, 27 implicit), and `to_filter` in [plan.cpp](../src/item/plan.cpp) sends `ids.front()`. +No amount of client-side data resolves those: both hashes carry identical text, and the trade +hash is **not** a derivable function of the client stat id — fnv1a32, crc32, djb2, sdbm, Java +hashCode and adler32 were each tested against `map_zana_influence`, over the raw, uppercased and +`stat_`-prefixed forms, and none produce `2696470877`. Only observing a live listing can pin a hash +to a printed line. This is a separate known issue, out of scope here, and named so it is not +rediscovered as a symptom of this work. + +## What the app gains + +**`src/data`.** `en-mod-pools.ndjson` and its index load exactly as the optional datasets already +do, with a `has_mod_pools()` gate mirroring `has_unique_mods()` / `has_unique_bases()` — a bundle +published before this asset existed must keep working, which is most of why the gate exists rather +than a null check. `BaseType` gains `mod_domain`. Neither addition may pull SDL, ImGui, X11 or +libcurl into `ppc_core`; the pool is data and is testable headless. + +**The verdict store.** A table per character profile of stat → verdict, keyed as above and +persisted beside the existing settings. The verdicts are the roadmap's and the roadmap's wording +sticks: **safe**, **dangerous**, **deadly**, and **unrated** as the zero state — not a fourth choice +a user picks but the absence of one, which is why a row can be drawn as unrated and why the table +grows by being used. The file format **parses an optional roll bound from day one and shows no UI +for it**, per [roadmap.md](roadmap.md): accepting both shapes costs one branch now and a format +migration costs every user's file later. It is a **new file on disk**, so it is a change to +[PRIVACY.md](../PRIVACY.md) as much as to the code — that document enumerates every file written, +and it is the one that goes stale silently. Write both in the same change. + +**A pool browser in Settings, built pool-agnostic from the start.** One flat searchable list of +every entry in the pool, rated and unrated together, with the rating control on the row. Two things +make a long list usable and both are cheap: a **search box**, and **rated entries sorted to the +top** so the part the user has an opinion about is the part they see first. + +The page is expected to be **rarely opened**. The table is meant to fill in by playing — rate what +the popup shows you, on the spot — and the settings list exists for the one session where someone +sits down to pre-fill it, usually by pasting a regex. Design accordingly: it is a bulk-editing +tool, not the primary way anything gets rated. + +Parameterise it by domain now rather than hard-coding the map list. The same page serves 246 flask +mods, 511 abyss jewel mods or 552 idol mods later at no extra cost, and retrofitting a hard-coded +list into that is the expensive order. This is the largest independent piece of UI in 0.7 and +depends on no map logic at all. + +**Seeding from a pasted map regex.** [roadmap.md](roadmap.md) settles the shape of this and it is +worth restating only because it is the part most easily got wrong: the pasted string is **PoE's +search syntax, not one regex** — quoted terms, a leading `!` for negation, space-separated terms +ANDed, bare trailing terms (`"!a|b|c" pte`). Tokenize first, hand each *term* to the engine. +Feeding the whole string to one is how the `!` ends up matched literally. And **match against a +rendered wording, never `placeholder_form`**: a term like `\d+ e` was written against printed item +text and can never match a `#`. + +This is where the pool's `min`/`max` earn their place. With a map in hand the printed lines are +right there, but seeding from the whole list needs each entry rendered with something in the +placeholder — and a term that can only match a number is one this cannot honestly resolve, so it +says so rather than guessing. **The import proposes and the user confirms; nothing writes a verdict +the user has not seen.** + +Two smaller semantics are left, and they are the doc's to settle rather than the roadmap's: whether +an entry counts as hit when **any** of its wordings matches (a modifier can print two to four +lines), and whether the affix-name line is in scope, since the in-game search sees it when Advanced +Mod Descriptions is on. + +## Order of work + +Each phase is shippable and reversible on its own, and the first two are the "pre-fire" the feature +sits on. + +1. **Data repo.** Emit `domain` on base records, fix the invitation base-row pick, emit + `en-mod-pools.ndjson` + index + manifest count. No new columns are fetched, so this is an emit + change and a data release. The app ignores assets it does not know, so it ships safely ahead of + any app change — and that property should be confirmed against the current release rather than + assumed. +2. **App plumbing, no visible feature.** Load and gate the pool, add `BaseType::mod_domain`, extend + the test bundle slice per [testing.md](testing.md). Nothing on screen changes. +3. **Settings: the pool browser and the verdict store**, with `PRIVACY.md`. Visible, usable, and + inert without the hotkey — a user can rate modifiers before anything reads the ratings. +4. **The feature.** Hotkey, popup, worst-verdict-leads, rate-on-the-spot, profiles. +5. **Regex import**, last, because it is the part the roadmap marks *might*. + +## Open, and needing an answer rather than a guess + +- **Do atlas-side modifiers print on the map item?** If they do, domain 11's 145 rows belong in the + pool so they can be rated like anything else. This wants a **capture**, not reasoning — the same + rule every number in this project is held to. +- **The two import semantics above** — any-wording-hits, and whether the affix name is matchable. +- **What a profile is keyed on**, if `LatestClient.log` watching is ever built. It is outside the + 1.0 promise and goes in `PRIVACY.md` either way. diff --git a/docs/roadmap.md b/docs/roadmap.md index f6cd5cd..b9d82ab 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -18,8 +18,8 @@ load-bearing and should not be reshuffled casually: is fixed in 0.3.x builds before anything downstream depends on it. Built — it and the Windows installer are [updater.md](updater.md) now, not a plan. - **The paste list precedes the map check** because both need an overlay placed at the cursor, - which does not exist yet, and the paste list is much the simpler first consumer of it. 0.6 also - reads its regexes from what 0.5 stores. + which does not exist yet, and the paste list is much the simpler first consumer of it. 0.7 also + reads its regexes from what 0.6 stores. ## Implementation notes, per planned version @@ -37,7 +37,7 @@ what a strategy leaves out is a **collapsed section at its foot** rather than no its links**, which is a price the tool missed entirely — see [trade-layer.md](trade-layer.md) and [item-layer.md](item-layer.md). -### 0.5, QuickPaste — **built** +### 0.6, QuickPaste — **built** Every constraint below held; the layer is [quickpaste.md](quickpaste.md) now, not a plan. The clipboard owner is a thread with its own `Display` (`clipboard_set_text`), nothing injects @@ -49,11 +49,11 @@ One rule the plan did not have and the code now does: nine slots is a limit on t so storage is unbounded, `enabled` is what competes for a number, and the ceiling is enforced on load as well as in the UI because `config.json` is hand-editable. -### 0.6, map check +### 0.7, map check - Reuse the price-check copy path whole, and the map strategy's existing parse and resolve. → [strategy-map.md](strategy-map.md) -- The verdict store keys on the stat record's id, never `placeholder_form` — 0.7 makes the +- The verdict store keys on the stat record's id, never `placeholder_form` — 0.8 makes the wording language-dependent and `find_stat` already refuses to guess between shared wordings. → [data-layer.md](data-layer.md) - **Verdict per stat, not per roll** — the practice this copies (map regexes) has no notion of a @@ -71,7 +71,7 @@ load as well as in the UI because `config.json` is hand-editable. - The import proposes and the user confirms. Nothing writes a verdict the user has not seen. - The store, the profiles and any `LatestClient.log` read are all `PRIVACY.md` entries. -### 0.7, client languages +### 0.8, client languages - Blocked upstream: `-stats.ndjson`, `-items.ndjson` with `refName`, and `-lexicon.json`. Nothing in the app's schema changes. → [localisation.md](localisation.md) @@ -81,7 +81,7 @@ load as well as in the UI because `config.json` is hand-editable. - `chart_area_key` moves into the bundle. - `fonts.unicode`'s system fallback already covers the scripts Fontin does not. -### 0.8, UI language +### 0.9, UI language - `ui::Msg` plus one table per language, `static_assert` on the length. Droppable. @@ -89,13 +89,13 @@ load as well as in the UI because `config.json` is hand-editable. Known, argued, and unscheduled — except where a planned version claims one, which is marked. -- **A language other than English to actually select.** *(Claimed by 0.7.)* The application side is built; +- **A language other than English to actually select.** *(Claimed by 0.8.)* The application side is built; what is missing is upstream. The data build fetches only the English `stat_descriptions.txt` files and emits one language, so `manifest.json` declares `["en"]` and the Settings row has one entry. A second language needs the build to emit `-stats.ndjson`, `-items.ndjson` with `refName` filled in, and a `-lexicon.json`. Nothing in the app's schema has to change. **Say so in the README rather than letting it be discovered.** -- **Two things still matched on English wordings** *(claimed by 0.7)*, both deliberately left alone rather than +- **Two things still matched on English wordings** *(claimed by 0.8)*, both deliberately left alone rather than guessed at, because getting either wrong is a confident wrong price rather than a failure: `item/derive`'s local-modifier lists (`kLocalDefences` and the weapon wordings) and `item/resolve`'s `kLocalWordings` compare `placeholder_form` of the printed line, and @@ -106,7 +106,7 @@ Known, argued, and unscheduled — except where a planned version claims one, wh localised matcher. **Pin it to a real localised bundle, not to a guess.** `ninja::kKeywords` is the same shape and already reads `ref_name`, which is why it is not on this list. -- **`chart_area_key`'s convention is Latin-only** *(claimed by 0.7)* — it capitalises words and drops apostrophes +- **`chart_area_key`'s convention is Latin-only** *(claimed by 0.8)* — it capitalises words and drops apostrophes to turn a printed area name into the internal id trade files a chart under. That cannot work for Russian, Korean or Thai, and the answer is for the bundle to carry the mapping rather than for the app to keep deriving it. A wrong key already costs breadth and never correctness