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: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ read whole; each is one layer.
| [docs/data-layer.md](docs/data-layer.md) | `src/data/` — the runtime bundle, the updater, the lexicon, stat normalization and matching. |
| [docs/updater.md](docs/updater.md) | `src/update/` and `packaging/` — how a copy arrives and how it replaces itself: the install flavours, the swap, `latest.json`, and the Windows installer. |
| [docs/item-layer.md](docs/item-layer.md) | `src/item/` — parse, resolve, derive, range matching, and the plan rules every strategy shares. Where most pricing judgement lives. |
| [docs/strategy-unique.md](docs/strategy-unique.md), [strategy-map.md](docs/strategy-map.md), [strategy-gem.md](docs/strategy-gem.md) | One per search strategy that has more to say than the shared rules: uniques (including unidentified), maps (with charts and Valdo maps), gems. |
| [docs/strategy-unique.md](docs/strategy-unique.md), [strategy-map.md](docs/strategy-map.md), [strategy-gem.md](docs/strategy-gem.md), [strategy-logbook.md](docs/strategy-logbook.md) | One per search strategy that has more to say than the shared rules: uniques (including unidentified), maps (with charts and Valdo maps), gems, expedition logbooks (the one item that is up to three items at once). |
| [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. |
Expand Down
14 changes: 9 additions & 5 deletions docs/data-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,15 @@ downloaded at runtime from **[JIRPOS/PathOfPriceCheck-Data](https://github.com/J
lets the client's own words be sent as the id the site wants), and the property and item-class
tables are keyed the other way round, printed label to key, so a translated one replaces the
English outright.
**One entry here is not the client's wording at all**: `Term::SanctumEffectPrefix`, the `Has `
a sanctum boon or affliction's *stat* is worded with, where the item prints the name alone
under a `Minor Boons:` label. It lives here because a translated bundle translates the stat
along with everything else and the lookup is by exact wording, so it is per-language in the
same way the rest of this table is.
**Three entries here are not the client's wording at all**: `Term::SanctumEffectPrefix`, the
`Has ` a sanctum boon or affliction's *stat* is worded with, where the item prints the name
alone under a `Minor Boons:` label; and `LogbookFactionPrefix` / `LogbookAreaPrefix`, the
`Has Logbook Faction: ` and `Has Logbook Area: ` an Expedition Logbook's destination is
searched under, where the item prints the two as bare names in a block of their own. They live
here because a translated bundle translates the stat along with everything else and the lookup
is by exact wording, so they are per-language in the same way the rest of this table is — and
because keeping the join in the lexicon is what keeps a compiled-in list of the four expedition
factions out of the parser. See [strategy-logbook.md](strategy-logbook.md).
**An empty entry never matches**, deliberately: `starts_with("")` is true of every line, and
`ModType::Explicit` has no generation word of its own.
`parse_item` and `looks_like_item` **take a lexicon and have no default**. The language is
Expand Down
5 changes: 5 additions & 0 deletions docs/item-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,4 +471,9 @@ bundle, and only the third and fourth encode pricing judgement.
- **`item/plan`'s map strategy** (`plan_map`, `plan_chart`, `add_map_pseudo`) is
[strategy-map.md](strategy-map.md) — maps, charts and Valdo maps, and the one strategy that
searches on none of an item's affixes.
- **`item/plan`'s logbook strategy** (`plan_logbook`, `group_logbook_mods`) is
[strategy-logbook.md](strategy-logbook.md) — the one item that is up to three items at once, and
the only plan whose rows are **alternatives**: `SearchPlan::choices` and `StatFilter::choice`,
one group per destination, exactly one live. `select_choice` is the whole of the exclusion and
`trade::build_query` reads nothing but `enabled`, as it does for `hidden`.
- **`item/plan`'s gem strategy** (`plan_gem`) is [strategy-gem.md](strategy-gem.md).
120 changes: 120 additions & 0 deletions docs/strategy-logbook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# The logbook strategy

<!-- Developer notes for PathOfPriceCheck. Loaded on demand; see ../CLAUDE.md for the map. -->

What every strategy shares — bounds, options, merged stats, the hidden section — is in
[item-layer.md](item-layer.md). This is the one item in the game that is **up to three items at
once**, and everything below follows from that.

An Expedition Logbook lists up to three **destinations**. Each names an area, the faction whose
land it is, and the two or three implicits that apply there. The player takes the book to Dannig
and travels to **exactly one** of them, and the faction is what decides what that is worth — so
one logbook has up to three prices, and a query asking for all three destinations at once prices
the single copy in the league that leads to that exact trio.

## Parsing (`item/parse`)

A destination is a section of its own and is recognised by **shape, not by a table of factions**
(`is_logbook_destination`). There are four factions today and the areas run to dozens; both are
game data a league can add to, and neither is a vocabulary this layer could keep honest. What is
matched instead is the two leading lines being **bare names** — no digit anywhere, no `Label:`
colon, no mod-type suffix, not an info line — which nothing else a logbook prints in a section of
its own is.

Two checks and both are load-bearing. The **leading pair** is what keeps the block of affixes a
magic or rare logbook prints *below* its destinations out, because an affix opens with its roll.
And the
**tail has to read as modifiers** (`looks_like_mods`), which keeps a stray pair of prose lines out
and is the half that still holds with Advanced Mod Descriptions on, where each implicit gains an
info line above it. The number of implicits is deliberately not fixed: the captures show two and
three.

The implicits stay in `Item::mods`, and `LogbookArea::mods` holds their indices. Which implicit
belongs to which destination is the whole of what a logbook is priced on and nothing about the
modifiers themselves says it — the rare capture grants "increased number of Explosives" from two
different destinations at two different rolls. The area and the faction go nowhere near the mod
list, where they used to arrive as six unmatchable lines.

**"Take this item to Dannig" needed a usage needle.** A logbook is gear, so prose needs a positive
signal before it stops being read as a modifier, and that sentence opens with no click
instruction — the same shape a chart's "Take this item to Valerie" already had a needle for.

## The join (`item/plan`, `logbook_stat`)

The faction and the area are searched as **`pseudo.*` stats**, and nothing else in the game is
searched this way: the item prints the two as bare names and the site words them as
`Has Logbook Faction: Druids of the Broken Circle` and `Has Logbook Area: Scrublands`. So the join
is by exact stat wording, through two lexicon terms (`Term::LogbookFactionPrefix`,
`LogbookAreaPrefix`) — the same shape and the same argument as `SanctumEffectPrefix`, which is
also why the four faction names are nowhere in this codebase. A name the bundle has no stat for is
a note, as an unknown sanctum boon or beast species is: the area list grows with the league.

## What is searched

- **One destination at a time.** `SearchPlan::choices` is one group per destination and
`StatFilter::choice` files each row under one; `select_choice` keeps exactly one group live.
The **faction is the choice itself** — `choice_primary`, drawn as the alternative's own row and
never a second time as a tickable one, because it is the single filter that follows entirely
from which destination was picked. Everything else in the group is offered unticked: where it
goes, because a buyer picking a faction is rarely picking an area with it, and what it grants
there, because an implicit is one of two or three numbers that came with the area rather than
something anybody chose.
- **The faction is asked on presence, never on a count.** The pseudo stat does take a value — how
many destinations belong to that faction — and it is not what decides the price: a logbook with
two Druids destinations is still bought for a Druids run, and bounding it drops every
single-destination copy of the same thing.
- **The first destination is live by default**, and it is the *first* on purpose. Nothing here can
rank the four factions, the ranking changes with the league and with what the player is farming,
and a default dressed up as an answer would be read as one. The panel puts them in the game's own
order and the choice is one click.
- **A destination's implicit is a floor and never a ceiling** (`group_logbook_mods`). Trade indexes
an item's implicits as one total per stat and all three destinations feed the same total — the
rare capture's two Explosives rolls, 14% and 16%, are indexed as 30%. A floor still matches under
that, since the total can only exceed one destination's own roll; a ceiling seeded from one
destination's roll asks the other two not to exist. Which side is the floor is the stat's own
`better`, the same question `to_filter` asks of an open bound.
- **The area level**, `map_filters.area_level`, a floor and ticked. Unlike a map's tier, a chart's
area level or a sanctum's floor — all exact, because a different number there is a different
product — a higher logbook area level is strictly more of the same one, and a buyer at 80 takes
an 83.
- **The item level**, offered. It bounds what the affixes can be crafted to, which is a question
about crafting the book rather than about running it.
- **Quantity, rarity and pack size**, offered, and all three **unticked on purpose — decided, not
deferred.** They are the same three properties a map is searched on and come off the same
`map_filters` keys, and this is the one place a logbook deliberately parts company with the map
strategy: a map's quantity and pack size are the whole of what it is run for and are ticked,
where a logbook's are a second-order bonus on top of the artifacts, which the *destination*
decides. They also only exist on a magic or rare book, so ticking them would make the same
logbook searched two different ways depending on whether it had rolled affixes at all. A
secondary reason to leave them alone: nobody has measured that the site indexes them for this
category, and a filter it accepts and indexes nothing under empties the search exactly as
`heist_max_escape_routes` does. Unticked is the state that cannot do that either way.
- **The type is sent only where the bundle resolved the base.** The category is the whole search on
its own — one base type is filed under `logbook` — so the type says nothing it does not, and a
magic logbook's printed line is "Buffered Expedition Logbook", which as a type matches nothing
and reads as nobody selling one.

## What is not

**The affixes the book prints below its destinations** are the map argument and get the map's
answer, which is what the maintainer asked for: they apply wherever it goes, a logbook is
`craftable` and a currency redoes them, and a query naming them finds the one copy in the league
that rolled that set. So they are `hidden` — offered under the section at the foot of the list
rather than dropped on the floor — and, as a map's are, not notes either.

**Split is not parsed, not filtered and not mentioned.** It is a flag line like Corrupted and the
site has a filter for it; neither is wanted here. (The flag itself is read by `parse_flags` for
every item in the game and has been since long before this, which is a different thing from the
strategy having an opinion about it.)

## Measured

The rare capture, searched as the plan builds it — category `logbook`, type `Expedition Logbook`,
`map_filters.area_level` at min 80, `pseudo.pseudo_logbook_faction_druids` on presence, plus the
three ordinary booleans — returned **983 listings** in Allflame. So the category, the type beside
it, the area level and the faction pseudo are all indexed for this category and none of them is a
filter the site accepts and answers with nothing.

`map_iiq`, `map_packsize` and `map_iir` are **not** measured, and are not waiting on a
measurement either: they are unticked because a logbook is not bought for them, and a measurement
would only decide whether a row the search does not send is safe to send.
9 changes: 9 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ base line says, and `ITEM::SeafloorRidges` is what trade files the chart under
internal id, with the `chart` discriminator and no display name on it at all, which is the whole
reason `chart_area_key` exists.

The **logbook** entries are the only `pseudo.*` records in the slice that are looked up by
wording: two faction stats and five area stats, plus the seven destination implicits and the five
affixes a rare logbook prints below them. Two of those five affixes are there and still do not
resolve, which is a data-side gap rather than a slicing one — `+#% Monster Chaos Resistance` and
`+#% Monster Elemental Resistances` are published with the sign *inside* the matcher, and
`placeholder_form` replaces the sign along with the digits, so nothing the clipboard prints can
reach them. 34 of the bundle's 15,148 matchers are shaped that way. The pricing case asserts three
hidden affixes for that reason and becomes five the day it is fixed.

The slice's four `GEM::` records need a bundle from `data-20260807.23` or later, which is the
release that keys gems on the name the game prints. The transfigured one
(`Raise Zombie of Falling`) is the whole point of that field and is the record to check after
Expand Down
25 changes: 22 additions & 3 deletions docs/trade-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,10 +356,11 @@ with ImGui about which presses can do anything.

**What a strategy leaves out is a collapsed section at the foot of the list**, not nothing.
`StatFilter::hidden` and `NumericFilter::hidden` are the flag and `draw_hidden_header` the row that
opens it. Three strategies set it on a modifier they match and then decide the item is not bought
opens it. Four strategies set it on a modifier they match and then decide the item is not bought
for: **a map's affixes**, re-rollable with one Chaos Orb and answered by the single copy in the
league that rolled that set; **a beast's monster modifiers**, which are not affixes; and **an
ultimatum's hazards** other than the two that scale the stake. **Sockets and links below five** are
league that rolled that set; **a beast's monster modifiers**, which are not affixes; **an
ultimatum's hazards** other than the two that scale the stake; and **a logbook's own affixes**,
on the map argument exactly. **Sockets and links below five** are
the numeric case and the same argument. Every one of those is occasionally the whole question, and
before this there was no way to ask it short of the trade site itself. Numerics come first behind
the disclosure as they do in front of it, so a row does not change position depending on which of
Expand All @@ -374,6 +375,24 @@ something it deliberately did not attempt. And `merge_same_stat` **never folds a
or a modifier the strategy left out would end up inside the total of one it did not, with the
shown row's tick sending both.

**A set of rows the search sends one of is a different thing entirely**, and the list draws it
differently: `SearchPlan::choices` and `StatFilter::choice`, drawn by `draw_choice_row` as a
**radio button** at the head of the list, ahead of the numerics as well as the modifiers. An
Expedition Logbook is the case — up to three destinations, exactly one of which the player
travels to — so those rows are not three questions to answer independently but one question with
three answers, and three checkboxes would invite ticking two and searching for a logbook that
goes to both. **The alternative's own row is its primary filter** rather than a heading over one: a logbook
destination's faction is exactly what picking that destination asks for, so a tickable row
repeating it underneath said the same thing twice and offered to untick what the radio button had
just decided. It is the one line in the list drawn **bold**, being what a reader scans a logbook
for. The **chosen alternative shows the rest of its group** indented under it — the area and the
implicits, offered unticked; the others show their label and where they lead and nothing else,
since expanding all three would bury the choice under nine rows nobody has picked. Clicking
an unchosen one is `SearchPlan::select_choice`, which is the only thing that ticks or unticks a
grouped row. `build_query` again knows nothing about any of it: the other groups are simply
unticked. `merge_same_stat` grows a third divide for the same reason it has the `hidden` one —
two destinations can share a faction or grant one stat, and their total belongs to neither.

**Collapsed for every price check**, held on `App` rather than in ImGui's storage, which is keyed
by id and would carry an open section from one item to the next. Six map affixes open by default
would bury the two rows that actually price the map, which is the same argument that hid them.
Expand Down
Loading