Skip to content

feat(save-editor): edit what a merchant sells and how much ore he has - #84

Open
dh0er wants to merge 6 commits into
mainfrom
claude/editor-handler-verkaufsdaten-2c3925
Open

feat(save-editor): edit what a merchant sells and how much ore he has#84
dh0er wants to merge 6 commits into
mainfrom
claude/editor-handler-verkaufsdaten-2c3925

Conversation

@dh0er

@dh0er dh0er commented Aug 12, 2026

Copy link
Copy Markdown
Owner

A merchant's shop is not his inventory. It lives in one global array
(m_Traders) keyed by his unique name and holds two maps: the live stock and
the baseline he restocks toward. His ore sits in that same map as an ordinary
line, because ore is the colony's currency and what he holds is what he can pay
with.

Adds a Handel sub-tab between Inventar and Dialogwissen, and the core
commands behind it.

Core

private.traders.list / .detail, plus three edits:

  • setStock — a bare i32 at the tail of its map entry, so it batches with peers.
  • addItem / removeItem — structural splices, classified in
    may_invalidate_caller_ordinals and carries_caller_ordinal so the core's
    ordering rule keeps them apart from index-addressed peers. The app mirrors
    that classification, which is what decides how a save is split into writes.

Two things the data forces:

  • Rows are addressed by array index, never by name. Two shipped rows are
    named None, are byte-identically long and hold the same ore, so a name
    lookup refuses rather than guesses.
  • A sold-out item is deleted from the map, not left at zero. setStock
    therefore refuses a line that does not exist and points at addItem instead
    of reporting success for a write it cannot do.

Ore is optional: three merchants carry no ore line at all, which reads as
null rather than 0 so the UI does not claim they are broke.

private.characters.list gains isTrader, joined in the core so the character
list can badge merchants without a second query.

App

Stock is browsed through the inventory's own category sidebar, sorted by the
localized name. Queued additions and removals appear as banners above the list
— the shared PendingStructuralRow, extracted from the inventory — because a
row drawn among the saved lines would claim a state the save does not have.

A segmented switch selects one of the two maps. That also settles which map the
ore field belongs to: the ore card appears only for the live stock, where the
number IS the purchasing power, and its line leaves the list so it is not shown
twice.

Verified against a real save

Editing ore in m_Items reaches the game, and the load-time regeneration is
additive: Fisk stored 12 → 777 in game (+765); after setting 40000 → 40765
in game, the same +765. So the value is the merchant's liquidity, and it is not
re-derived from m_DefaultItems — the failure mode the faction-hostility work
warned about does not apply here.

Also verified on a copy of a real save: a batched setStock pair, an insert, a
removal, that add+remove is byte-identical to a no-op, and that neighbouring
merchants are untouched.

Not included

Prices. The factor lives in AngelScript (8 merchant types × 18 item tags) and
in the difficulty settings, not in the savegame.

Tests

435 Rust (incl. an integration suite that runs against the embedded start save,
so it needs no game install), 557 Flutter, flutter analyze clean.

🤖 Generated with Claude Code


Note

Medium Risk
Changes savegame write paths and batching for trader map splices; mistakes could corrupt merchant data, but behavior mirrors existing inventory ordering rules and is covered by extensive Rust/Flutter tests.

Overview
Adds a Handel / Trade sub-tab on the character screen so merchants can be edited outside their personal inventory: live stock, restock baseline, ore (purchasing power), and add/remove stock lines.

The Rust core exposes private.traders.list / .detail and three writes — setStock (in-place count), addItem / removeItem (map splices). Traders are keyed by array index (duplicate None rows); sold-out lines are removed, not zeroed. private.characters.list now includes isTrader for list badges.

The Flutter app loads traders, queues edits with stable pending keys, and mirrors core save batching (structural trader ops split from batched setStock). UI reuses inventory-style category browsing and shared PendingStructuralRow banners for queued adds/removals; editing is gated when per-difficulty stock exists or the core is read-only.

Reviewed by Cursor Bugbot for commit 8274c5b. Bugbot is set up for automated code reviews on this repo. Configure here.

dh0er and others added 2 commits August 11, 2026 23:33
A merchant's shop is not his inventory. It lives in one global array
(m_Traders) keyed by his unique name, and holds two maps: the live stock
and the baseline he restocks toward. His ore sits in that same map as an
ordinary line, because ore is the currency and what he holds is what he
can pay with.

Adds private.traders.list/.detail plus three edits: setStock (a bare i32
at the tail of its map entry, so it batches), and addItem/removeItem
(structural splices, so they stand alone in their write and are listed in
both the core guard and the app's splicingPaths).

Two things the data forces:

- Rows are addressed by array index, never by name. Two shipped rows are
  named `None`, are byte-identically long, and hold the same ore, so a
  name lookup has to refuse rather than guess.
- A sold-out item is deleted from the map, not left at zero. setStock
  therefore refuses a line that does not exist and points at addItem
  instead of reporting success for a write it cannot do.

Ore is optional: three merchants carry no ore line at all, which reads as
null rather than zero so the UI does not claim they are broke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… browsed

Queued changes now sit above the list rather than inside it. An addition has
no row yet and a removal's row is about to stop existing, so drawing either
among the saved lines claimed a state the save does not have. Both use the
same banner the inventory uses, extracted into pending_structural_row.dart.

The two stock maps get a segmented switch instead of sitting on one page. That
also settles which map the ore field belongs to: the ore card only appears for
the live stock, where the number IS the merchant's purchasing power, and its
line leaves the list so it is not shown twice. In the restock baseline ore is
an ordinary row, because "purchasing power" means nothing there.

Stock is grouped by item category behind the inventory's own sidebar, sorted
by the localized name with the class id as tiebreak.

Smaller corrections from testing against a real save:

- The ore hint said what ore is for. It now says what the user cannot see
  otherwise: the in-game figure differs, because on load the game adds what
  accrued since the merchant's last trade.
- Class ids followed the show-object-ids setting nowhere; now they do, and are
  dropped entirely when the title already is the id.
- The character list badges a merchant, joined in the core so the list needs
  no second query.
- The price note moved to the top, where it qualifies the ore as much as the
  counts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ac681af. Configure here.

Comment thread apps/save-editor/lib/features/editor/ui/trader_detail.dart
Comment thread crates/gore-save/src/lib.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac681afa10

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/gore-save/src/lib.rs Outdated
Two reports from the PR review.

A count of 0 was accepted and written in place. Sold-out lines are deleted
from the map rather than held at zero — no shipped or played save carries a
zero-valued entry — so that write invented a record the game never produces.
setStock now requires a positive count and names removeItem in the refusal,
which is what "he no longer offers this" actually looks like. addItem already
rejected zero for the same reason. The count field stops submitting it.

The count field also kept the previous line's value. It refreshed only when
the queued count changed, and rows carried no key, so the list reused one
line's field state for whichever line landed at that position after a category
or map switch — and submitting the leftover queued a setStock against the item
now shown. Rows are keyed by item path, and the field now reacts to a changed
saved value as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dh0er

dh0er commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

@codex review

1 similar comment
@dh0er

dh0er commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dca43e6870

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/save-editor/lib/features/editor/ui/trader_detail.dart
Comment thread apps/save-editor/lib/features/editor/ui/trader_detail.dart
Comment thread apps/save-editor/lib/features/editor/domain/trader_models.dart
…line removable

Three reports from the second review pass.

The core badges a merchant by matching lowercased names, while a character's
unique name is the stored knowledge key where one exists — whose casing can
differ from the trader row's. The app compared exactly, so a character the
list badged as a merchant could open on "does not trade". It now folds case
the same way the core does.

Pulling the ore row out of the live-stock list left no way to drop it, and
setStock no longer accepts zero — yet a merchant with no ore line at all is a
state the game itself produces. The ore card carries a delete action now, and
its field is disabled while that removal is queued.

The count field accepted values above i32::MAX, which the core then refused at
save time. It applies the same bound the add-item dialog already applies, so
the refusal happens in the field instead of at the end of a save.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dh0er

dh0er commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2897b09d21

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/gore-save/src/traders.rs Outdated
apply_set_stock already refuses it; apply_add_item only refused a negative,
leaving a Rust caller able to insert the zero-valued entry the module treats
as an invalid state. The JSON parser was the only thing enforcing a positive
count on that path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dh0er

dh0er commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

@codex review

1 similar comment
@dh0er

dh0er commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12a1091446

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/gore-save/src/traders.rs Outdated
Comment thread apps/save-editor/lib/features/editor/ui/trader_detail.dart Outdated
…led stock

private.traders.detail compared trader names exactly, so a caller passing the
uniqueName that private.characters.list returned — the stored knowledge key,
whose casing can differ from the trader row's — got "no trader named" for a
character the same list had marked a merchant. The Dart-side fold did not
cover direct users of the command. The ambiguity check still runs, now on the
folded comparison, so two rows differing only in case are still refused.

A save whose per-difficulty stock is populated now disables editing and says
why. The edits reach only m_Items and m_DefaultItems, so such a save would
have taken a change, reported success, and left that stock standing. Empty in
every save observed so far, which is exactly why it needed saying.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dh0er

dh0er commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8274c5b8be

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +611 to +617
for (final item in pendingAdds) ...[
const SizedBox(height: 8),
_PendingLineRow(
item: item,
tone: PendingTone.add,
onCancel: () => onRevertAdd(map, item.path),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make queued stock changes scrollable

When a user queues enough additions or removals—for example while replacing most of a merchant's stock—every pending banner is inserted as a fixed child above the only Expanded list. _StockSection itself has bounded height and provides no scrolling around these banners, so their combined height eventually causes a vertical RenderFlex overflow, hides the stock browser, and can make later pending changes impossible to reach or cancel. Put the pending rows in a bounded scrollable area or include them in the main list.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant