Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/skills/run-overlay/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ launch it, photograph it, and look. `PPC_DEV_ITEM` is what makes that possible w
| `PPC_DEV_OVERLAY=1` | Opens Settings and disables dismiss-on-blur. Required for every dev run. |
| `PPC_DEV_ITEM=<file>` | Opens the price-check panel on a captured clipboard instead. |
| `PPC_DEV_PASTE=1` | Opens the QuickPaste popup at wherever the pointer is, which is the only way to see it without the game. |
| `PPC_DEV_MAP=<file>` | Opens the map check popup on a captured map instead, at the pointer. Takes precedence over `PPC_DEV_ITEM`. |
| `XDG_CONFIG_HOME=<dir>` | Sends `config.json` and the map-check profile tables somewhere scratch. Set it for anything touching map check — otherwise a dev run writes a `Default.json` into the real configuration directory, and a profile with verdicts in it is the only way to photograph a rated popup. |
| `PPC_DEV_IDLE=1` | Keeps the idle status marker up (it otherwise shows only while the game is in front). |
| `PPC_MANAGED=1` | Lets the window manager manage the window — needed if you want it stackable/movable on a real session. |
| `PPC_DEBUG_COPY=1` | Traces the copy timeline to stderr. |
Expand Down
18 changes: 14 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ Pipeline: **hotkey → auto-copy → clipboard → parse → identify → price
The overlay, Settings, the league list, the static game-data layer, the item layer (parse →
resolve → price-relevant numbers → search plan, plus the game-styled tooltip), the trade search,
poe.ninja reference pricing, the in-game currency exchange feed, QuickPaste, the bug reporter (with
the relay it posts to) and the binary updater (with the Windows installer it depends on) are all
**built and tested**.
the relay it posts to), the binary updater (with the Windows installer it depends on) and 0.7's
**map check** — the modifier pool, the per-profile verdict tables, the popup and the search-string
import — are all **built and tested**.
What is not built is [docs/roadmap.md](docs/roadmap.md) — including the fact that a language other
than English cannot yet be selected, because the data build emits only English.
than English cannot yet be selected, because the data build emits only English. Map check's second
"might", switching profile by watching the client log, is **not** on that list: the log never names
the character you selected, so it was closed rather than deferred — see that doc's **Decided
against**, and bring a capture if you reopen it.

Sections of any doc describing an unbuilt layer say so explicitly. Keep them honest.

Expand All @@ -47,7 +51,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/map-check.md](docs/map-check.md) | `src/mapcheck/` and the two screens over it — mod domains, the modifier pool, the per-profile verdict tables, and PoE's item-search syntax as this reads it. |
| [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. |
Expand Down Expand Up @@ -99,6 +103,12 @@ violate one of these on the strength of not having read it.
- **A bug report is sent only by a press, and the dialog shows the whole of it first.** Nothing may
reach the relay that the preview does not draw, and nothing about a report is gathered in the
background. → reporting, PRIVACY.md
- **The verdict store keys on the stat record's `ref`, never on a printed line.** A wording is
language-dependent the moment a localised bundle exists; resolution happens first and the verdict
attaches to what it resolved to. → map-check
- **The mod pool describes; it never gates.** A modifier an item prints and the pool does not
contain is normal — the pool may offer and pre-fill, never reject, hide, or decide that a line
failed to parse. → map-check
- **The clipboard is ours to read *and* to write.** `clipboard_set_text` owns the X selection from
a thread of its own, because a write on X11 is a promise to answer for the text later. →
quickpaste, platform
Expand Down
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ add_library(ppc_core STATIC
src/paths.cpp
src/config.cpp
src/quickpaste.cpp
src/mapcheck/verdict.cpp
src/mapcheck/filter.cpp
src/mapcheck/store.cpp
src/mapcheck/rate.cpp
src/leagues.cpp
src/util/sha256.cpp
src/util/base64.cpp
Expand Down Expand Up @@ -187,6 +191,7 @@ set(APP_SOURCES
src/ui/range_slider.cpp
src/screens/settings_screen.cpp
src/screens/quickpaste_screen.cpp
src/screens/mapcheck_screen.cpp
src/screens/pricecheck_screen.cpp
src/screens/report_screen.cpp
src/screens/item_view.cpp)
Expand Down Expand Up @@ -292,4 +297,6 @@ ppc_add_test(exchange_test)
ppc_add_test(ratelimit_test)
ppc_add_test(track_test)
ppc_add_test(quickpaste_test)
ppc_add_test(mapcheck_test)
ppc_add_test(hotkey_test)
ppc_add_test(report_test)
3 changes: 2 additions & 1 deletion PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ The whole tool works by reading the clipboard, so this is worth being precise ab

| path | what |
|---|---|
| `<config>/config.json` | your settings: league, hotkeys, panel geometry, listing status, result count, filter ranges, client and interface language, panel opacity, whether the status indicator is shown, whether to update automatically - **and your QuickPaste entries, in full**, since they are text you typed for this tool to hold |
| `<config>/config.json` | your settings: league, hotkeys, panel geometry, listing status, result count, filter ranges, client and interface language, panel opacity, whether the status indicator is shown, whether to update automatically, the names of your map-check profiles and which one is in use - **and your QuickPaste entries, in full**, since they are text you typed for this tool to hold |
| `<config>/map-profiles/<name>.json` | one file per map-check profile: which modifiers you marked safe, dangerous or deadly. Written as you rate them, and one empty `Default.json` is created on first run so the feature has somewhere to put a verdict. Nothing here leaves your machine |
| `<config>/cookies.txt` | the cookie jar above |
| `<cache>/data/<version>/` | the downloaded game-data bundle, plus a `current` pointer |
| `<cache>/update/` | a downloaded release of the application, waiting for the restart that applies it. One file, consumed as it is applied; absent whenever no update is pending |
Expand Down
42 changes: 25 additions & 17 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,32 +97,40 @@ 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.7 - Map check
## 0.7 - Map check - **shipped**

A hotkey that reads a map's rolled modifiers and tells you which ones you decided you cannot take.

**Will:**
**Does:**

- Mark each modifier **safe**, **dangerous** or **deadly**, and remember it.
- Lead with the worst verdict on the map.
- Draw unrated modifiers as unrated, with the rating control on the spot. The table fills in by
being used.
- Keep a table per character profile, picked in the popup and sticky until you change it. A new
profile can start as a copy of an existing one.
- Rate a modifier as a modifier, at every roll - the way a map regex does. **Will not** ask you
- Keep a table per profile, picked in the popup or in Settings and remembered until you change
it. A new profile can start as a copy of an existing one.
- Rate a modifier as a modifier, at every roll - the way a map regex does. **Does not** ask you
for a threshold on each of a few hundred mods.

**Might - seed the table from a map regex you already use.** Paste
`"!\d+ e|te of|m resistances$|ents$|r, f|ter e|ll damage$|from$|t reg|s def|h tem" pte` and every
modifier an excluding term hits is *proposed* dangerous, every one a wanted term hits *proposed*
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.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.
- Rate a whole modifier, however many lines it prints, and apply that to every modifier carrying
the same lines. Implicits included.

**Also does - seed the table from a map regex you already use.** Paste a regex from a tool like
poe.re\* (for example
`"!\d+ e|te of|m resistances$|ents$|r, f|ter e|ll damage$|from$|t reg|s def|h tem"`) and every
modifier an excluding term hits is *proposed* deadly, every one a wanted term hits *proposed* safe.
You confirm; from then on the table is what the tool believes. It can import imperfectly - 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 - think of it as a head start. The real
ratings will come from using the tool and fill up pretty quickly. The same syntax narrows the
modifier list in Settings, and a **?** beside the box says what it takes.

\* When building the regex, don't add filters for Quantity&Yield, Map State or Quality. "I don't
want any of these mods" and "I want these mods" with match type "Any".

**Dropped - switch profile automatically** by watching the client log. The log never names the
character you selected, and the three lines that do carry a name (a level-up, a death, a chat
message) name your party as readily as you, so there is nothing to switch on. The profile you
pick is remembered instead, and the verdict table goes in [PRIVACY.md](PRIVACY.md) as promised.

## 0.8 - Every language the client speaks

Expand Down
18 changes: 12 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ it drives have docs of their own and are read separately: [data-layer.md](data-l
[platform.md](platform.md).

Pipeline: **hotkey → auto-copy → clipboard → parse → identify → price → render**. `App` (`src/app.cpp`)
owns the SDL event loop and a `Screen` state machine `{ Hidden, PriceCheck, Settings, QuickPaste }`
— the last of which is the paste list and is [quickpaste.md](quickpaste.md), the only screen that
does not involve the copy path at all. Price-check
owns the SDL event loop and a `Screen` state machine
`{ Hidden, PriceCheck, Settings, QuickPaste, MapCheck }` — QuickPaste is the paste list and is
[quickpaste.md](quickpaste.md), the only screen that does not involve the copy path at all, and
MapCheck is [map-check.md](map-check.md), which shares the copy path *whole* and differs only in
what it opens on the far side of it (`App::copy_target_`). Price-check
hotkey → `simulate_copy()` → wait for the clipboard to be written → parse → show if it's an item.
**Four steps, and they are meant to stay four.** An earlier version grew a pre-copy snapshot, a
byte comparison against it, a latching write detector, `SDL_EVENT_CLIPBOARD_UPDATE` as a third
Expand Down Expand Up @@ -195,7 +197,10 @@ releases even when it finds no game window — an unmatched pair leaks the helpe

`App::place_overlay()` gives each screen its own geometry: Settings is a 640×720 dialog centered over
the game, the paste popup is sized to its own list and placed at the cursor sampled when its hotkey
fired (see [quickpaste.md](quickpaste.md)), price-check is a **full-height panel docked beside the item's own frame** — right of the
fired (see [quickpaste.md](quickpaste.md)), the map check popup is placed the same way but sized to
an item nothing has laid out yet — so it draws at a generous estimate, reports the height it came
to, and the window follows on the next frame, which is one frame either way for a window that has
just appeared — price-check is a **full-height panel docked beside the item's own frame** — right of the
stash if the cursor was in the left half of the game window at hotkey time, left of the inventory if
in the right half (`App::cursor_side()`, sampled before the copy; the user has moved on by the time
the clipboard lands). Panels straddling the middle — vendor, quest rewards — have no correct answer,
Expand Down Expand Up @@ -246,7 +251,7 @@ solved back through.
`Config::status_marker` (Settings → General → Appearance, on by default) turns the marker off
altogether; with it off the idle overlay is never mapped, and nothing else about a check changes.

**Settings is four tabs** — General, Price check, QuickPaste, Application — between a fixed header (the title
**Settings is five tabs** — General, Price check, QuickPaste, Map check, Application — between a fixed header (the title
and the close disc) and a fixed footer (Save). `kTabs` in `settings_screen.cpp` pairs each name with
the function that draws it; `App::settings_tab()` holds which one is open, because the screen is a
free function rebuilt every frame. The strip is buttons, not `ImGui::BeginTabBar`: the game marks
Expand Down Expand Up @@ -355,7 +360,8 @@ and on Linux that is an `org.freedesktop.ScreenSaver` inhibit — reason "Playin
the life of the process, so an application that sits in the tray all day stopped the machine from
sleeping. The game does its own inhibiting; we are a desktop app. `PPC_DEV_OVERLAY=1` opens Settings and disables
dismiss-on-blur for local dev; add `PPC_DEV_ITEM=<file>` to open the price-check panel on a captured
clipboard instead, `PPC_DEV_PASTE=1` to open the paste popup at the pointer, or `PPC_DEV_IDLE=1` to keep the idle status marker up (it otherwise only ever
clipboard instead, `PPC_DEV_MAP=<file>` to open the map check popup on a captured map at the
pointer, `PPC_DEV_PASTE=1` to open the paste popup at the pointer, or `PPC_DEV_IDLE=1` to keep the idle status marker up (it otherwise only ever
appears while the game is the window in front). `PPC_DEV_UPDATE_URL=<url>` points the update check
at a `latest.json` of your own, which is the only way to see its three notice surfaces before a
release publishes one — see [updater.md](updater.md). `PPC_REPORT_URL=<url>` points the bug
Expand Down
34 changes: 34 additions & 0 deletions docs/data-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,40 @@ downloaded at runtime from **[JIRPOS/PathOfPriceCheck-Data](https://github.com/J
bundle-level signal saying whether `BaseType::exchange` means anything, because unlike a whole
missing file an absent boolean cannot tell "no data" from "no". `install` writes it only when
non-zero, since a 0 would claim the opposite of what it means. See the currency-exchange section.
`en-mod-pools.ndjson` and its index are optional in the same way `en-unique-mods.ndjson` is —
see the mod-pool section. `source.mod_pools` is written through beside the other two so the
installed bundle records what the build produced, but it is **not** what gates anything: a
whole missing file says that already, and `has_mod_pools()` reads the index.
- **The mod pools** are the one thing here that does not start from an item in hand.
`en-mod-pools.ndjson` is, per **mod domain**, every modifier that domain can spawn — the whole
set, whether or not anything is holding one. It exists so a modifier can be rated in Settings
before it has ever been rolled; see [map-check.md](map-check.md), which owns the feature.
**The pool describes and never gates.** What it lists is what spawns *naturally*, which is
strictly less than what an item can print: an essence, a craft, a veiled mod or Harvest all put
modifiers on an item whose weights would never have produced them, and the published list is
trimmed by naming conventions besides. A printed modifier no entry covers is normal, renders as
it always did, and is rateable on the spot. Nothing may use a pool to reject a line, hide one,
or decide it failed to parse.
One record — `PoolMod` — is one **wording-set**, not one roll: the tiers of an affix print the
same wordings and a verdict attaches to a wording, so they collapse, and `min`/`max` span the
lowest tier's floor to the highest tier's ceiling in displayed units. `tiers` and `mods` are
provenance for the debug log. A `PoolStat` with no `trade_id` is the ordinary case and not a
gap — the pool is rated, not searched, and a wording trade indexes under two hashes is one the
build refuses to pick between, here as everywhere else.
Two lookups, because the feature needs both directions: `mod_pool(domain)` is the whole pool,
for the settings list, filled by one pass over the file the first time it is asked for (a few
hundred records, and a pool is only ever wanted entire); `find_pool_mods(domain, wording)` goes
the other way, from a wording resolved off an item, through an index keyed on
`"{domain}::{wording}"`. **The domain is part of the key**, because a map and a chart are
separate pools that word 42 modifiers identically, and an answer mixing them would offer a
chart's affix for a map.
**Which domain an item rolls from is `mod_domain_for(base, item_class)`, not
`BaseType::mod_domain`.** The base is asked first and its class answers where it cannot, and
that fallback is not a nicety: trade lists all 491 maps under one entry whose game row is a
*stand-in* sitting with the stackable currency in domain 43, so a map's own record deliberately
states no domain at all and the `Maps` class is what knows the answer is 5. The other way round
would be wrong — a class holding genuinely different things (Jewels covers two domains)
publishes none, and only a base can answer for those.
- **`data/lexicon`** is every word the *client* prints, for one language: the section labels
(`Item Class`, `Rarity`, `Requirements`, `Sockets`, `Note`), the flag lines, the rarity and
influence names, the mod-type suffixes and Advanced Mod Descriptions generation words, the
Expand Down
Loading