Shared chest tabs, rune stacks and rune master, Q pickup, Kettenblitz tooltip and range

- Shared chest: tabs "Truhe"/"Gemeinsam" as a column left of the grid
  (game tab sheet), grids placed every tick, persistent across heroes
- Inventory tab texts capitalized (RegisterText / CapitalizeText)
- Combat-art runes stack like potions (itemstack, renamed from potionstack)
- Rune master: any rune for any number of runes, price by rune count
  (4 runes = 1,000, x10 per missing rune), bought rune goes into the
  backpack, Buy disabled without runes, gold or room, last rune remembered
  per hero (saved), the chosen rune's name below its icon, dropping a
  stack gives up one rune
- Keys: the old key handling no longer takes over after the save screen
  (text field focus only counts while its dialog is shown)
- Esc crash: tooltips forgotten when the game destroys them
- Pick up (Q) sends the pickup order directly; search reach doubled
- Kettenblitz: tooltip shows hits per chain, chains, cast and jump range;
  cast range 900 (longer ray segments), jumps half of that

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-17 03:40:42 +02:00
1 parent 3d9d864c25
commit 6ca96d54b4
29 files changed
+2006 -171

No files matched your search

+24
View File
@@ -67,3 +67,27 @@ ray reaches it, otherwise the visible ray just stops. So the cast range is
(also the first hop of extra chains), instead of the game's jump search radius
(squared 100000, about 316). It is the one place to make the range dynamic
later. Jumps do not check the ground in between.
## Tooltip (2026-10-17)
Request: the tooltip should match the mod. The game's text (cCreatureArts::ArtTooltip
0x589270, measured for art 0x21 at level 5) ends each level section with
"Anzahl: X", the game's own hit count. The mod replaces that line with
"Treffer je Kette" (3 + L/5), "Ketten" (1 + L/10) and "Sprungweite" (the cast
range), with L = the hero's level (base + bonus) in the "Aktuelle Stufe" section
and L + 1 in "Nächste erlernte Stufe". The string is rewritten with the game's
append (0x592ef0), so the game's allocator owns the buffer.
## Range +50 % (2026-10-17)
Request: Kettenblitz range about 50 % larger. The ray's 20 segments are
bounded by the caster's segment id array (+0x263, 20 dwords), so the segments
get longer instead: 30 -> 45, cast and jump range 600 -> 900. Patched in the
art code: `fsub [30.0]` 0x526db6 and `fcomp [30.0]` 0x526e0b (redirected to
our 45.0; 0x88ef18 is shared by 72 instructions), `mov eax,30` 0x526e23 (the
segment length given to each segment object) and `push 30.0f` 0x526f3d (the
step of the ray point).
Follow-up (2026-10-17): jumps at 900 felt too far. The cast keeps 900; jumps
use `JumpRange()` = half the cast range (450). The tooltip shows both
("Reichweite", "Sprungweite").
+7
View File
@@ -37,3 +37,10 @@ every player hero a cEvent_creature 0x10c (ends the input lock) and passes
answers below 100000 to the quest script (0x46bd80). Esc does the same with
answer 0xffffffff: the dialog closes, nothing is chosen. (Hiding the tooltip
alone left the input lock; measured with a write watch on its flags.)
## Crash fix (2026-10-17)
Esc crashed in `CloseDialog` on a tooltip pointer whose object had been
destroyed (its vtable was already the base class's; the hide call jumped to
garbage). The HUD layer's tooltip list (`g_tooltips`, hud.cpp) kept destroyed
tooltips; `cUI_Tooltip::~cUI_Tooltip` (`0x6e6560`) is hooked now and drops them.
+28 -2
View File
@@ -1,4 +1,4 @@
# Potion counts and potion stacks
# Item stacks (potions, combat-art runes)
## Requests (2026-10-17)
@@ -44,7 +44,7 @@ Not seen yet: a sale, a right-click drink.
- `src/mods/potioncounts.cpp`: counts per kind (stacks added up), drawn
bottom-right on each potion slot after `RenderPotionSlots`.
- `src/mods/potionstack.cpp`: stack counts by item id (with the item type),
- `src/mods/itemstack.cpp`: stack counts by item id (with the item type),
saved as `"count"` on the grid cell in the JSON inventories
(`inventory_save.cpp`); entry pours into stacks with room on the next
tick; `Sort` stacks the hero's potions first; a drink from a stack of more
@@ -67,3 +67,29 @@ JSON path already (stack sizes survived a reload). Stacking now applies to all
inventories below 0x12 (players and the chest); trader stock (0x12 and up)
never stacks. Selling a stack to a trader still has to be handled (pay
count x price, the trader keeps singles).
## Runes (request 2026-10-17: "also allow stacking for combat arts")
- Measured (sort button log of a Rucksack with runes): runes are item class
0x1c (type table +0x2e), 1x1. Two runes of type 0x1517 serialize the same
past the object part (id, flags, world position differ), so the type alone
identifies a rune.
- The game's own `ItemIsConsumable` (`0x43f5f0`, class 9 or 0x1c) now decides
what stacks (was: `ItemPotionKind`). The module was renamed
`src/mods/itemstack.cpp`.
- Reading a rune goes through `cCreature::UseItem` like a potion (class 0x1c
is one of the three classes it uses), so a stack loses one per read.
## Rune master (request 2026-10-17: one rune per drop)
(See docs/rune-master.md for the window.) `cUI_MasterLotto::HandleEvent`
(`0x6d54e0`, read): a left press over one of
its four slots (controls at +0x154 + i * 0x114) puts the slot's old rune on
the cursor and, if the cursor carries a rune (descriptor at cursor +0x68:
{u16 kind 1, u16 class 0x1c, u32 id, ...}), copies the descriptor into the
slot (+0x24c + i * 0x114) and clears the cursor. `src/mods/runetrade.cpp`
(owns the event hook; hud.cpp centres it through `hud::HandleCentred`): for a
stack, a single copy (`game::CloneItem`, serializer round trip) goes into the
slot, the stack keeps count - 1 and stays on the cursor; if the slot held a
rune (it comes onto the cursor), the rest goes to the backpack instead, and
without room the click does nothing.
+54
View File
@@ -0,0 +1,54 @@
# Rune master (`src/mods/runetrade.cpp`)
## Requests (2026-10-17)
1. Dropping a rune stack into the rune master takes one rune off the stack.
2. Buying: runes can always be exchanged directly for a chosen rune; the
number of runes given lowers the price (decided: 4 runes 1,000, each
missing rune x10: 3 runes 10,000, 2 runes 100,000, 1 rune 1,000,000). The
rune to buy can always be chosen; Buy works only with at least one rune
and enough gold. The last rune bought is remembered (decided: per hero,
saved).
## The original window (`cUI_MasterLotto`, read)
| Offset | What |
|---|---|
| +0x154 + i * 0x114 | slot control i (0..3), hit test vf1c(x, y) |
| +0x24c + i * 0x114 | slot i's item descriptor {u16 kind 1, u16 class 0x1c, u32 id, ...} |
| +0x5a4 / +0x620 | arrows: next / previous rune (`doLottoSelect` mode 1 / 0) |
| +0x69c | the bought rune's control; +0x794 its descriptor, +0x798 id, +0x79c type |
| +0x7b4 | the chosen rune (item type) |
| +0x7c8 | Buy button |
- Prices: u32 table at `0xad2aac` by rune count; the game fills 2..4 from
its settings (`0x5efc7b`); read by the render, the button state
(`0x6d4390`, `0x6d3df0`) and the pay helper `0x6d3ce0(pay)` (gold at
hero +0x3ee, paid with `cCreatureArts::AddGold` `0x57fc40`).
- Buy (`0x6d42a0`): 2 runes give a random rune, 3 a random one of the hero's
class, 4 the chosen one. Buying needed 2 runes (`cmp dx, 2` at `0x6d3e20`,
`0x6d4544`, `0x6d4a13`).
- `0x6d3d50` after slot changes: with 4 runes shows the arrows and keeps the
choice (`doLottoSelect(hero type, 2)`, index in the global `0x17e81d0`),
otherwise hides them and clears +0x7b4.
## Mod
- Owns the window's event hook (hud.cpp centres it via `hud::HandleCentred`).
- Prices: `kPrices` written into the table (on game start and with each
window event); the three `cmp dx, 2` become 1.
- `0x6d3d50` replaced: arrows always shown (unless a bought rune waits), a
rune always chosen; when none is chosen (window opened) the hero's last
bought rune is chosen by stepping `doLottoSelect` forward to it.
- Buy press handled here (`CanBuy`): at least one rune, a chosen rune, gold
(`0x6d3ce0(0)`) and room in the backpack (a stack of that rune with room,
or a free cell). Buying pays (`0x6d3ce0(1)`), destroys the slot runes and
puts the rune straight into the backpack (follow-up 2026-10-17: a bought
rune taken from the window vanished; the window's waiting slot +0x794 /
+0x798 is no longer used). The rune is remembered for the hero.
- The Buy button looks disabled whenever `CanBuy` fails: the render sets its
colours each frame with `cUI_Button::SetColours` (`0x6d5d90`), hooked to
hand it the disabled set (`0x9f0290`, text `0x9f028c`).
- Remembered rune: `"hero": {"lastRune": type}` in the hero's inventory JSON
(`inventory_save::RegisterHeroData`); cleared when the session ends.
- Stack drops: see docs/item-stacks.md.
+71
View File
@@ -0,0 +1,71 @@
# Shared chest
## Request (2026-10-17)
The chest gets a second tab that belongs to no single hero: a chest shared by
all characters. Decided: stored in one file in the save folder
(`save\shared_chest.json`), two tabs ("Truhe" / "Gemeinsam"), same size as the
chest (20 x 10). Potions stack in it like in every player inventory
(docs/item-stacks.md). Follow-up (same day): text tabs were barely visible;
use the inventory's Rucksack / Kampfkünste / Combos header look with its
selected state, stacked as a column from the grid's top-left. The shared chest
was empty after loading another character (in-game saves don't call saveHero).
## Measured / read
- The chest window `cUI_Chest` (constructor ends `0x6ac899`): +0x158 =
inventory 0x11 (the hero's chest), grid 20 x 10 from (window x, 20). Render
`0x6ac8b0` (vf14) draws +0x158 with `DrawItems`; HandleEvent `0x6acc70`
(vf10) does hit test, fit, place, swap and take via +0x158. Only Shift+click
uses the index: `cInventoryManager::MoveItem` `0x5dec80` (from, to, id, ?,
ret 0x10) from 0x11 to the hero.
- Chest saving: inventory 0x11 is chunk 0xc6 of the hero file and part of
save games, both through the JSON inventories already (diagnostic log).
- Items in hero files (`ItemDataMgr::saveHero` `0x6046f0`, chunk 0xc2): the
ids of both inventories, equipment, weapon sets; per item its type (+0x10)
and the object's serializer `vf1c(stream, 1, 0)` (ret 0xc).
- Loading (`loadHero` `0x605900`, chunk 0xc2): `cObjectManager::Create(type,
0, 0, 0)` `0x5fb590`, `vf1c(stream, 0, version)` (version = header byte,
0x1b), then the saved id (read into +0xc) is replaced by the new one
(`ReplaceItemId`), flags +0x14: clear 0x200000, 0x30 if bit 2, 0x10; +0x39
= owner hero id.
- `cEngine::~cEngine` `0x60d5e0` (plain ret) runs before the object manager is
destroyed: `game::OnGameEnd`.
## Mod (`src/mods/sharedchest.cpp`)
- An inventory of our own (`cInventoryEntry::Construct` `0x5d7a10`, owner
0x11 so it counts as a player inventory).
- Tabs: a `cUI_TabSheet` (constructor `0x728ed0`, `AddTab` `0x7290d0`) built
right after the chest window (`cUI_Chest::cUI_Chest` `0x6ac7f0`), the same
control as the inventory's Rucksack / Kampfkünste / Combos headers, so the
selected / hover look is the game's. Its layout (`0x729210`, headers in a row
above the sheet) is followed by ours: a column left of the grid's top-left
corner. Labels "Truhe" / "Gemeinsam" through `game::RegisterText`
(`src/game/ui/texts.cpp`, an override of `GetText` `0x672f70`). Switching
(`cUI_TabControl::SetActive` `0x728af0`) sets window +0x158 to inventory
0x11 or the shared one (origin and size copied from the chest). Pages are
empty controls.
- The tab sheet is added to the window's control list (`cUI_Window2::AddControl`
`0x727120`); only those controls are drawn and get events (a parent alone
isn't enough).
- Grid place: the constructor sets inventory 0x11 to (window x, 20), 20 x 10
once, but loads overwrite it (the game's inventory read `0x5df09b` resets
some inventories to 0/0; the JSON inventories restore the saved place), so
the chest drew at the wrong spot after some loads. Both tabs' grids are put
back to the constructor's values every tick and on each tab switch.
- `MoveItem` from/to 0x11 while the shared tab is open moves from/to it.
- File: `{"format": "sacred_re.shared_chest", "version": 1, "items": [{cell,
w, h, type, data (hex of vf1c), count}]}`; written after each successful
`saveHero` and after each save-game write (in-game saves don't call saveHero;
via a temp file), read on the first engine tick with a hero after a session
start (`game::OnGameStart`, cEngine::cEngine `0x60ac70`) or a save-game read
with a hero; the grid is cleared when the session ends.
## Open / to check
- Whether save games (PAK) also store the shared chest's item objects (they
live in the object manager outside the 32 inventories).
- loadHero's extra fix-up for some item classes (`0x5beaa0` / `0x5d3a20`,
item classes 5, 6, 8, 0xd, 0x11..0x19, 0x1b, 0x1d) is not repeated.
- Multiplayer: not supported.
+2 -2
View File
@@ -119,7 +119,7 @@ in-game texts/tooltips that name the old keys):
| Key | Action | Temporary key (sacred_re.ini) |
|---|---|---|
| W | action slot 3 (`cEngine::UseActionSlot`, like Q/E/R/B/Space) | G (`[keys] ActionSlot3`) |
| A | pick up items nearby (creature event 0x113) | V (`[keys] PickUp`) |
| A | pick up items nearby (creature event 0x113) | Q (`[keys] PickUp`, 2026-10-17; was V) |
| S | save game window (`UI_WND_SAVEGAME`) | K (`[keys] SaveGame`) |
| D | unbound | - |
@@ -133,4 +133,4 @@ Asked 2026-10-17 (after the potion counts on the bottom bar,
stack in one inventory cell, up to 20 per stack (a full stack starts a new
one). Drag, sell and drop move the whole stack, as with any other item. The
counts on the potion slots then add up the stacks. In progress, see
docs/potion-stacks.md (open: sale price per stack, splitting on the ground).
docs/item-stacks.md (open: sale price per stack, splitting on the ground).