- The game's cheater detection killed the hero ("Hase!") whenever its
protected stat mirror was stale; the mirror is now refreshed from the hero
whenever it is unlocked, stream checks never punish, loadHero no longer
sets the cheater flag (docs/anticheat.md).
- saveHero wrote the items of weapon sets 1-5 only: its loop runs over all 7
sets, so sets 6-7 survive a world loading the hero from the hero file.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
202 lines
10 KiB
Markdown
202 lines
10 KiB
Markdown
# 7 weapon sets and 7 combat-art slots
|
|
|
|
Goal (user request 2026-10-14): 7 weapon sets and 7 combat-art slots instead
|
|
of 5 each; skills on 1-7, weapon sets on F1-F7. Inventories are saved as JSON
|
|
in place of the binary block; vanilla saves are migrated on load. Saves do not
|
|
need to stay loadable by the original game. Status: inventory data for
|
|
slots 6-7 (`src/mods/skillslots.cpp`) and the JSON save
|
|
(`src/mods/inventory_save.cpp`) done and tested (vanilla save migrated,
|
|
JSON save reloads unchanged); taskbar with 7 + 7 slots (`src/mods/skillbar.cpp`), unlock levels
|
|
45 / 65 and keys 1-7 / F1-F7 done; tested in-game 2026-10-15 (drag & drop, casting,
|
|
equipping set 6 changes the stats, save / reload).
|
|
|
|
## Where the slots live
|
|
|
|
### `cInventoryEntry` (one per player, no vtable)
|
|
|
|
All 32 inventories are one block from `cInventoryManager::Instance`
|
|
(`0x5deb80`, 32 x 0x1180 + 8 bytes at `*0xad23c8`); `GetInventory`
|
|
(`0x5dec50`) hardcodes the stride 0x1180. An inventory's index is therefore
|
|
`(inventory - block) / 0x1180`.
|
|
|
|
| Offset | Size | Content |
|
|
|---|---|---|
|
|
| +0x18 | 0x100 x 0xc | item grid |
|
|
| +0xc18 | 5 x 0x18 | weapon sets: 2 hands x (item id, 2 dwords) |
|
|
| +0xc90 | 5 x 2 x 0x14 | art slots: 2 variants (bit 2 of +0x1162, maybe mounted) x {u16 type 2/3/4, u16, id, 3 dwords} |
|
|
| +0xd58 | 2 x u16 | selected slot: [0] weapon set, [1] art slot |
|
|
| +0xd5c | 0x400 | (next field, unrelated) |
|
|
| +0x115c / +0x115e | u16 | grid width 0x14 / height 8 |
|
|
| +0x1160 | u16 | player index |
|
|
| +0x1162 | u16 | flags |
|
|
|
|
The three slot arrays are packed together, so there is no room for slots
|
|
6 and 7 in the object.
|
|
|
|
Methods (names in `tools/names.txt`): `GetWeaponSetItem` `0x5d86c0`,
|
|
`SetWeaponSetItem` `0x5d86f0`, `SetWeaponSet` `0x5d8930`,
|
|
`TakeWeaponSetItem` `0x5d8be0`, `GetWeaponSetItemPos` `0x5d8d00`,
|
|
`GetArtSlotType` `0x5d93e0`, `GetArtSlotRef` `0x5d9420`, `SetArtSlot`
|
|
`0x5d9480`, `ClearArtSlot` `0x5d9680`, `SelectSlot` `0x5d9820`,
|
|
`GetSelectedSlot` `0x5d9b60`, `SetArtVariant` `0x5d9200`; loops over the 5
|
|
weapon sets in `Clear` `0x5d7d30`, `ReplaceItemId` `0x5d80e0`,
|
|
`FixInventory` `0x5de8a0` and the load path of `Serialize`.
|
|
|
|
Accesses outside the class found so far: none to these offsets (searched
|
|
`+0xc18..+0xca0`, `+0xd58/+0xd5a`, `(x+0x102)*0xc`, `(x+0xa1)*0x14`,
|
|
`(x*3+0x183)*8`). The compiler also spells addresses in other ways, so the
|
|
list is a lower bound until the callers of the accessors are checked.
|
|
|
|
### Save format (`cInventoryEntry::Serialize` `0x5dd9f0`)
|
|
|
|
`(stream, write)`; stream vf04 read, vf08 write, vf14 position. Raw blocks in
|
|
this order, no version field:
|
|
|
|
flags +0x1162 (2), +0x1164 (2), +0x1166 (2), width (2), height (2),
|
|
+0xd5c (0x400), grid (0xc00), **weapon sets (0x78)**, **art slots (0xc8)**,
|
|
**selected (4)**, +0x1168 (4), quest items (count + 8 each), scrolls
|
|
(count + 4 each).
|
|
|
|
Returns the number of bytes processed. Callers:
|
|
|
|
- hero save / load: `ItemDataMgr::saveHero` (`0x6046f0`: `0x604a26`,
|
|
`0x604a7d`) and `ItemDataMgr::loadHero` (`0x605900`: `0x605cee`);
|
|
- multiplayer: `0x5db670` and `0x5df1f0` serialize into a `cFile_ram` and
|
|
send it as a network event; must keep the binary format;
|
|
- save games (GAME*.PAK, "AMS"): `cInventoryManager::Serialize` (`0x5def60`,
|
|
thiscall (stream, write, x)), all 32 inventories behind 0xdeadc0de markers;
|
|
called from `0x61ae26` / `0x61c0a8`.
|
|
|
|
### Hero file chunks (`saveHero` / `loadHero`)
|
|
|
|
0x100-byte header (magic "AMH" at +0x?, chunk count), then a table of
|
|
(type, offset, size) entries, 12 bytes each, 0xc0 bytes = 16 entries, written
|
|
last (`saveHero` seeks back to 0x100). `loadHero` seeks to each chunk and
|
|
switches on its type; unknown types appear to be skipped (to confirm).
|
|
|
|
| Type | Content |
|
|
|---|---|
|
|
| 0xc0 | hero record (0x5ac) |
|
|
| 0xc1 | the hero's inventory (`Serialize`, inventory index = hero id) |
|
|
| 0xc6 | inventory #0x11 (`Serialize`; what it is: not checked) |
|
|
| 0xc2, 0xc3 (0x22c), 0xc4 (0x40), 0xcc, 0xce, 199, 200 | other hero data |
|
|
| 0xca / 0xcb / 0xcd | variants of 0xc1 / 0xc6 / 0xcc with a preceding `0x6076a0` step |
|
|
|
|
Decision (2026-10-14): the inventory chunks are written as JSON (new chunk
|
|
type) instead of 0xc1/0xc6; reading 0xc1/0xc6 remains as the migration from
|
|
vanilla saves. The multiplayer `cFile_ram` path keeps the binary format.
|
|
|
|
### Slot count by level
|
|
|
|
`SlotCountForLevel` (`0x57f750`): table at `0x95503c`, (level, count) u16
|
|
pairs: 0 -> 1, 2 -> 2, 8 -> 3, 16 -> 4, 30 -> 5. Used by the taskbar
|
|
(`+0x15c`).
|
|
|
|
### Taskbar (`cUI_Taskbar2`)
|
|
|
|
Slot controls (0x7c-byte `UI_STATIC` controls, created in the layout
|
|
function `0x6e4ba0`): weapon sets at +0x164 (5), art slots at +0x178 (5);
|
|
selection highlight written into the controls (+0x54..+0x60, +0x78) from
|
|
`0x9ed058`. The object's fields are packed behind the arrays (+0x18c ...).
|
|
|
|
Layout in the 1024 frame, slot width 0x42 (66 px), y = 0xf:
|
|
|
|
- weapon sets on the left, growing leftwards from the centre:
|
|
x = 0x18a + (i - n) * 0x42 (n = active slots);
|
|
- art slots on the right, growing rightwards: x = 0x280 + i * 0x42;
|
|
- `SetSlotCount` (`0x6dffa0`, clamps n to 1..5): shows the first n controls
|
|
of both rows, moves the weapon row, re-selects if the selection is >= n,
|
|
and rebuilds the decorative fill tiles (`UI_STATIC`, random variants from
|
|
`0x9eb0b0` / `0x9eb104` / `0x9eb1ac` / `0x9eb200`) between the rows and
|
|
the centre (left from 0x16a - n * 0x42, right from 0x23a + n * 0x40).
|
|
|
|
With 7 slots the rows reach x = -68 on the left and 1102 on the right, past
|
|
the 1024 frame (66-78 px each side). In the widened view the centred bar has
|
|
room for that (16:9: 1365 wide, 170 px each side). Decision 2026-10-14: keep
|
|
the original spacing; 4:3 is not a target (cut off there).
|
|
|
|
### Art slots without selection (2026-10-15)
|
|
|
|
Decision: art slots are no longer selected. Slot 0 (leftmost) is always the
|
|
right mouse button's art; left click stays the normal attack. Keys 0-6 cast
|
|
slots 0-6 directly (`src/mods/skillkeys.cpp`): key 0 presses the right
|
|
button, keys 1-6 select their slot for as long as the key is held and press
|
|
the right button. Without a held skill key the selection returns to slot 0
|
|
(engine tick). `cUI_Taskbar2::SelectSkillSlot` turns every other request
|
|
(clicks on the bar, wheel, COMBINE_SLOTS, slot count check) into slot 0;
|
|
slots on the bar are only filled by drag & drop. Weapon sets are still
|
|
selected (F1-F7, clicks).
|
|
|
|
How a slot is drawn (`RenderArtSlots` `0x6e31e0`): each slot is a
|
|
`UI_STATIC` control drawn by its vf14; the function swaps the control's atlas
|
|
entry and rect, draws, and restores them. An art has 3 icons (indices into
|
|
`g_uiAtlasEntries` `0x9eaee0`, 0x54 each): ready + selected slot uses the
|
|
third, otherwise the first; while recharging, the slot is drawn twice, split
|
|
vertically by the ready fraction (first icon above, second below). Icons
|
|
0x276-0x278 replace them when the art cannot be used mounted and
|
|
`0x54fa90` holds (fill from creature +0x4e8 / +0x4ec). Over the icon a frame
|
|
is drawn: `g_uiTaskbarArtFrameSelected` `0x9f4be4` / `g_uiTaskbarArtFrame`
|
|
`0x9f7f14`, `0x9f5d9c` as a drop target, `0x9f696c` with creature flag
|
|
0x2000. `UpdateSlotHighlight` (`0x6df9a0`) sets the slot backgrounds
|
|
(`0x9ed084` selected / `0x9ed030`). Patched (`kTaskbarSkillSelectionLook`):
|
|
frame and background test "slot 0" instead of "selected slot", and every
|
|
ready art uses its third icon. Slots 1-6 are moved right by half a slot
|
|
(0x21) after the layout, and the decorative strip right of the centre starts
|
|
that much further right (`add ecx, 23Ah` at `0x6e01cc`).
|
|
|
|
Weapon set clicks (2026-10-15): with an empty cursor, the taskbar's mouse
|
|
handling jumps to `0x6e10d8` (`je` at `0x6e0ca7`; taskbar in esi, inventory
|
|
in ebp, set at [esp+0B8h], hand at [esp+0BCh]), which takes the set's weapon
|
|
onto the cursor (`TakeWeaponSetItem`) and re-selects the selected set. That
|
|
jump now goes through `WeaponSetClickThunk`: a click on a set that is not
|
|
selected only selects it (exit `0x6e124d`, al = 1); dragging starts from the
|
|
selected set only.
|
|
|
|
Negative x (2026-10-15): with 7 slots the leftmost weapon set (set 1, F1)
|
|
lies at x = -68..-2 of the layout. The taskbar's own hit test accepts it,
|
|
but every place that asks the slot controls (`vf1c`, signed compares) passes
|
|
the position as `and reg, 0FFFFh`, so -43 became 65493 and the set could
|
|
not be clicked. `kTaskbarSignedHitTests` sign-extends them (slot lookups
|
|
`0x6dfcfe`/`0x6dfd5e`, the click loops `0x6e1a0c`/`0x6e1a5c`, drop and
|
|
drag `0x6e1e8e`.. `0x6e20f2`).
|
|
|
|
The two 5 x 0xbc blocks at +0x258 / +0x604 are the belt (3D item views), not
|
|
slots. Still to map: rendering of the slot contents (icons, cooldowns,
|
|
`Render` `0x6e3ac0`), drag & drop onto slots (`HandleEvent` mouse branch,
|
|
`0x5d9480` / `0x5d9680` calls), and whether the UI manager routes mouse
|
|
events outside 0..1024 to the bar.
|
|
|
|
### Network
|
|
|
|
Slot changes send network events 299 (`SetWeaponSetItem`), 300
|
|
(`SetWeaponSet`), 0x129 (`SetArtSlot`), 0x12a (`ClearArtSlot`), 0x130
|
|
(`SelectSlot`), 0x123 (`SetArtVariant`) with the slot index. A server or
|
|
client running the original only knows 5 slots: slots 6-7 are single player
|
|
only (or disabled in multiplayer).
|
|
|
|
## Plan
|
|
|
|
1. Slots 6-7 in a DLL table per inventory index (2 weapon sets, 2 x 2 art
|
|
slot variants); the original object keeps slots 1-5 unchanged.
|
|
2. Hook the accessors above: index >= 5 goes to the table. Extend the
|
|
5-slot loops (`Clear`, `ReplaceItemId`, `FixInventory`, load fix-up).
|
|
3. Save: `saveHero` writes the inventories as a JSON chunk (versioned)
|
|
instead of 0xc1 / 0xc6; `loadHero` reads the JSON chunk, or migrates a
|
|
vanilla 0xc1 / 0xc6 chunk through the original reader (slots 6-7 empty).
|
|
4. Slot count table: 7 entries; slot 6 at level 45, slot 7 at level 65
|
|
(agreed 2026-10-14).
|
|
5. Taskbar: 7 controls each (arrays inside the object are fixed at 5: own
|
|
table for the extra controls), layout, rendering, hit tests, drag & drop.
|
|
6. Keys: 0-6 cast art slots (since 2026-10-15; before: 1-7 select and
|
|
cast), F1-F7 select weapon sets.
|
|
|
|
### Hero files: items of sets 6-7 (2026-10-25)
|
|
|
|
Reported: the Gladiator lost the weapons of sets 6/7 on Silber. saveHero
|
|
collects the items it writes (chunk 0xc2) from the weapon sets with
|
|
`GetWeaponSetItem` over sets 0..4 (`cmp esi, 5` at `0x605046`), so sets 6-7
|
|
kept ids of items that were never in the hero file; a world that takes its
|
|
hero from the hero file (Silber without its own hero inventory) had nothing
|
|
there. The loop bound is patched to 7 (inventory_save.cpp); loadHero's id
|
|
replacement already covers the shadow slots. Confirmed 2026-10-25.
|