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:
1 parent
3d9d864c25
commit
6ca96d54b4
29 files changed
+2006
-171
No files matched your search
@@ -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").
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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).
|
||||
Reference in new issue
Block a user