Files
khallaandClaude Opus 5.5 91bbc52cf0 Anti-cheat off, weapon sets 6-7 in hero files
- 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>
2026-10-25 01:33:43 +02:00

10 KiB

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.