Taskbar: 7 weapon sets and 7 combat-art slots

cUI_Taskbar2's slot arrays move behind the (enlarged) object with room
for 7 each; all instructions addressing them, the relative accesses
between the two arrays, the slot loops and the count clamp are patched
from tables in game/addresses.h (belt loops untouched). Slots 6 / 7
unlock at levels 45 / 65; AssignArtToSlot accepts slots up to 6. Slot
keys call the taskbar's selection directly (honouring COMBINE_SLOTS).

Shared code: re::PatchAll / PatchInstructionByte for patch tables,
game/engine/objects.h for object, RTTI and kernel calls, slot-hook
templates in skillslots.cpp.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-15 00:38:34 +02:00
1 parent 998e315d98
commit dbc632750b
12 files changed
+364 -145

No files matched your search

+135
View File
@@ -0,0 +1,135 @@
// 7 weapon-set and 7 combat-art slots on the taskbar.
//
// cUI_Taskbar2 keeps its slot controls in two packed arrays of 5 (+0x164
// weapon sets, +0x178 art slots) and walks them in loops of 5. The arrays move
// to the end of the taskbar object, which is allocated 0x40 bytes larger, with
// room for 7 each; every instruction addressing them, the loops and the slot
// count clamp are patched (tables in game/addresses.h, found by reviewing all
// of 0x6df340-0x6e6000, docs/skill-slots.md). The layout code then creates 7
// controls per row at the original spacing: weapon sets grow leftwards from
// the centre, art slots rightwards.
//
// The old arrays keep a copy of the first 5 controls, so code that was missed
// still sees valid controls.
//
// Also: 7 entries in the slot-count-by-level table (6 at level 45, 7 at 65),
// and cCreature::AssignArtToSlot accepts slots up to 6.
#include "mods/skillbar.h"
#include <atomic>
#include <cstring>
#include "game/addresses.h"
#include "mods/skillslots.h"
#include "re/hook.h"
#include "re/log.h"
#include "re/patch.h"
namespace mods::skillbar {
namespace {
using namespace game::addr;
using mods::skillslots::kGameSlots;
using mods::skillslots::kSlots;
constexpr uint32_t kWeaponSlots = kTaskbarSize; // new arrays behind the old object
constexpr uint32_t kSkillSlots = kWeaponSlots + kSlots * 4;
constexpr uint32_t kNewTaskbarSize = kSkillSlots + kSlots * 4 + 0x8;
constexpr int8_t kOldDistance = static_cast<int8_t>(kTaskbarSkillSlots - kTaskbarWeaponSlots); // 0x14
constexpr int8_t kNewDistance = static_cast<int8_t>(kSkillSlots - kWeaponSlots); // 0x1c
// (level, slots): the game's table plus slots 6 and 7.
constexpr uint16_t kSlotLevels[kSlots] = {0, 2, 8, 16, 30, 45, 65};
std::atomic<uint8_t*> g_taskbar{nullptr};
void Startup() {
int failed = 0;
failed += !re::PatchPushImm32(kTaskbarAllocSizePush, kTaskbarSize, kNewTaskbarSize);
failed += re::PatchAll(kTaskbarWeaponSlotUsers, kTaskbarWeaponSlots, kWeaponSlots);
failed += re::PatchAll(kTaskbarSkillSlotUsers, kTaskbarSkillSlots, kSkillSlots);
failed += re::PatchAll(kTaskbarSlotDistanceDown, static_cast<uint8_t>(-kNewDistance));
failed += re::PatchAll(kTaskbarSlotDistanceUp, static_cast<uint8_t>(kNewDistance));
failed += re::PatchAll(kTaskbarSlotLoopBounds, static_cast<uint8_t>(kSlots));
failed += re::PatchAll(kAssignArtMaxSlot, static_cast<uint8_t>(kSlots - 1));
static_assert(kOldDistance == 0x14);
re::Log("skillbar: %u weapon sets / art slots on the taskbar (%d patches failed)", kSlots, failed);
}
RE_STARTUP(SkillBarStartup, Startup);
using SelectFn = void(__fastcall*)(void* taskbar, void* edx, uint32_t slot, int notify);
void Select(uint32_t function, unsigned slot) {
if (uint8_t* taskbar = g_taskbar.load()) {
reinterpret_cast<SelectFn>(function)(taskbar, nullptr, slot, 1);
}
}
} // namespace
void SelectSkillSlot(unsigned slot) {
Select(0x006e17d0, slot); // cUI_Taskbar2::SelectSkillSlot
}
// With the game option COMBINE_SLOTS (Options::CombineSlots 0x81c020) a
// weapon set also selects the art slot with the same number, as the digit
// keys do (cUI_Taskbar2::HandleEvent).
void SelectWeaponSet(unsigned slot) {
Select(0x006e1440, slot); // cUI_Taskbar2::SelectWeaponSet
using CombineSlotsFn = char(__cdecl*)();
if (reinterpret_cast<CombineSlotsFn>(0x0081c020)()) {
SelectSkillSlot(slot);
}
}
void OnTaskbar(void* taskbar) {
g_taskbar = static_cast<uint8_t*>(taskbar);
}
// Copy of the first 5 controls of each row into the old arrays.
void MirrorOldArrays(void* taskbar) {
auto* p = static_cast<uint8_t*>(taskbar);
std::memcpy(p + kTaskbarWeaponSlots, p + kWeaponSlots, kGameSlots * 4);
std::memcpy(p + kTaskbarSkillSlots, p + kSkillSlots, kGameSlots * 4);
}
uint16_t SlotCountForLevel(unsigned level) {
uint16_t count = 1;
for (unsigned i = 0; i < kSlots; ++i) {
if (level >= kSlotLevels[i]) {
count = static_cast<uint16_t>(i + 1);
}
}
return count;
}
} // namespace mods::skillbar
namespace game {
// cUI_Taskbar2's layout (0x6e4ba0, from the constructor): creates the slot
// controls.
void __fastcall UiTaskbar_Layout(void* self, void* edx);
RE_HOOK(0x006e4ba0, UiTaskbar_Layout);
void __fastcall UiTaskbar_Layout(void* self, void* edx) {
mods::skillbar::OnTaskbar(self);
UiTaskbar_Layout_original(self, edx);
mods::skillbar::MirrorOldArrays(self);
}
// cUI_Taskbar2::SetSlotCount (0x6dffa0): shows the first n slots of each row.
void __fastcall UiTaskbar_SetSlotCount(void* self, void* edx, uint32_t count);
RE_HOOK(0x006dffa0, UiTaskbar_SetSlotCount);
void __fastcall UiTaskbar_SetSlotCount(void* self, void* edx, uint32_t count) {
mods::skillbar::OnTaskbar(self);
UiTaskbar_SetSlotCount_original(self, edx, count);
}
uint32_t __stdcall SlotCountForLevel(uint32_t level);
RE_HOOK(addr::SlotCountForLevel, SlotCountForLevel);
uint32_t __stdcall SlotCountForLevel(uint32_t level) {
return mods::skillbar::SlotCountForLevel(level & 0xffff);
}
} // namespace game