The keys rewrite the hero's path every cCreature::Update: one segment along the held direction, the facing set to it, sliding along blocked cells. Starting from idle runs the game's own Think steps at once (StartPendingWalk, WalkAction) instead of waiting up to 110 ms, so weapon switches no longer stop the walk. Stops wait 50 ms after the keys are released, and a stop in progress is taken back when keys are held again. Names for movement, stats and text-key functions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
325 lines
13 KiB
C++
325 lines
13 KiB
C++
// 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.
|
|
//
|
|
// Combat-art slots are not selected any more: slot 0 is always the right
|
|
// mouse button's art (the game's "selected" slot), the others are cast with
|
|
// their keys (src/mods/skillkeys.cpp). Selecting another slot is refused
|
|
// unless a skill key asks for it. Slot 0 is drawn with the selected look and
|
|
// set apart from slots 1-6 by a gap; every ready art uses its selected icon.
|
|
//
|
|
// Recharging arts show their remaining cooldown (seconds, one decimal) on the
|
|
// slot, drawn with the game's font after RenderArtSlots.
|
|
// Combos too, from their charge (game::GetComboCooldown).
|
|
//
|
|
// 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 <cwchar>
|
|
|
|
#include "game/addresses.h"
|
|
#include "game/engine/arts.h"
|
|
#include "game/engine/objects.h"
|
|
#include "game/ui/text.h"
|
|
#include "mods/skillslots.h"
|
|
#include "re/hook.h"
|
|
#include "re/log.h"
|
|
#include "re/patch.h"
|
|
|
|
namespace mods::skillbar {
|
|
|
|
void WeaponSetClickThunk();
|
|
|
|
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
|
|
|
|
// cUI_Taskbar2 fields.
|
|
constexpr size_t kInventoryIndex = 0x158; // u16: the hero's inventory
|
|
constexpr size_t kSlotCount = 0x15c; // u16: slots shown, from SetSlotCount
|
|
// UI control: x at +0x24 (relative to the taskbar).
|
|
constexpr size_t kControlX = 0x24;
|
|
|
|
// Gap between slot 0 (right mouse button) and slots 1-6: half a slot.
|
|
constexpr int kRightButtonSlotGap = 0x21;
|
|
constexpr uint32_t kRightStripStart = 0x23a;
|
|
|
|
// Weapon set clicks (WeaponSetClickThunk).
|
|
constexpr uint32_t kWeaponSetEmptyCursorJump = 0x006e0ca7;
|
|
constexpr uint32_t kWeaponSetDrag = 0x006e10d8;
|
|
|
|
// Cooldown labels on the art slots.
|
|
constexpr uint16_t kCooldownFont = 1;
|
|
constexpr uint32_t kCooldownColor = 0xffffffff;
|
|
constexpr uint32_t kCooldownAlign = game::kTextCenter;
|
|
|
|
// (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};
|
|
// A skill key is selecting a slot other than 0 (window thread).
|
|
bool g_keySelects = false;
|
|
|
|
// Where the cooldown labels go: computed on the first frame after the layout
|
|
// (render thread), fixed to the slots from then on.
|
|
game::UiRect g_labelRects[kSlots] = {};
|
|
bool g_labelRectsValid = false;
|
|
|
|
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));
|
|
failed += re::ReplaceAll(kTaskbarSkillSelectionLook);
|
|
failed += re::ReplaceAll(kTaskbarSignedHitTests);
|
|
failed += !re::PatchInstructionDword(kTaskbarRightStripStart, kRightStripStart,
|
|
kRightStripStart + kRightButtonSlotGap);
|
|
failed += !re::RedirectJcc(kWeaponSetEmptyCursorJump, kWeaponSetDrag,
|
|
reinterpret_cast<const void*>(&WeaponSetClickThunk));
|
|
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);
|
|
}
|
|
}
|
|
|
|
uint8_t* HeroInventory() {
|
|
const uint8_t* taskbar = g_taskbar.load();
|
|
return taskbar ? skillslots::Inventory(*reinterpret_cast<const uint16_t*>(taskbar + kInventoryIndex))
|
|
: nullptr;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void SelectSkillSlot(unsigned slot) {
|
|
g_keySelects = slot != 0;
|
|
Select(0x006e17d0, slot); // cUI_Taskbar2::SelectSkillSlot
|
|
g_keySelects = false;
|
|
}
|
|
|
|
void SelectWeaponSet(unsigned slot) {
|
|
Select(0x006e1440, slot); // cUI_Taskbar2::SelectWeaponSet
|
|
}
|
|
|
|
unsigned SelectedSkillSlot() {
|
|
const uint8_t* inventory = HeroInventory();
|
|
return inventory ? reinterpret_cast<const uint16_t*>(inventory + skillslots::kSelected)[1] : 0;
|
|
}
|
|
|
|
bool SkillSlotHasArt(unsigned slot) {
|
|
const uint8_t* taskbar = g_taskbar.load();
|
|
uint8_t* inventory = HeroInventory();
|
|
if (!taskbar || !inventory || slot >= kSlots ||
|
|
slot >= *reinterpret_cast<const uint16_t*>(taskbar + kSlotCount) ||
|
|
!reinterpret_cast<void* const*>(taskbar + kSkillSlots)[slot]) {
|
|
return false;
|
|
}
|
|
using GetArtSlotRefFn = uint32_t(__fastcall*)(void* inventory, void* edx, uint32_t slot);
|
|
return reinterpret_cast<GetArtSlotRefFn>(0x005d9420)(inventory, nullptr, slot) != 0;
|
|
}
|
|
|
|
bool KeySelects() {
|
|
return g_keySelects;
|
|
}
|
|
|
|
void OnTaskbar(void* taskbar) {
|
|
g_taskbar = static_cast<uint8_t*>(taskbar);
|
|
}
|
|
|
|
// After the layout: slots 1-6 move right, away from slot 0; the first 5
|
|
// controls of each row are copied into the old arrays.
|
|
void AfterLayout(void* taskbar) {
|
|
auto* p = static_cast<uint8_t*>(taskbar);
|
|
auto* const* skills = reinterpret_cast<uint8_t* const*>(p + kSkillSlots);
|
|
for (unsigned slot = 1; slot < kSlots; ++slot) {
|
|
if (skills[slot]) {
|
|
*reinterpret_cast<int*>(skills[slot] + kControlX) += kRightButtonSlotGap;
|
|
}
|
|
}
|
|
std::memcpy(p + kTaskbarWeaponSlots, p + kWeaponSlots, kGameSlots * 4);
|
|
std::memcpy(p + kTaskbarSkillSlots, p + kSkillSlots, kGameSlots * 4);
|
|
g_labelRectsValid = false;
|
|
}
|
|
|
|
void UpdateLabelRects(uint8_t* const* skills) {
|
|
for (unsigned slot = 0; slot < kSlots; ++slot) {
|
|
if (skills[slot]) {
|
|
g_labelRects[slot] =
|
|
game::CenterLineVertically(kCooldownFont, game::ControlScreenRect(skills[slot]));
|
|
}
|
|
}
|
|
g_labelRectsValid = true;
|
|
}
|
|
|
|
// The remaining cooldown in seconds (at the hero's current regeneration
|
|
// rate, game::GetArtCooldown), to one decimal, centred on each art slot that
|
|
// is recharging (combat moves and spells: slot types 2 / 3; combos: 4,
|
|
// game::GetComboCooldown).
|
|
void RenderCooldowns(void* taskbar, void* renderer) {
|
|
auto* p = static_cast<uint8_t*>(taskbar);
|
|
uint8_t* inventory = skillslots::Inventory(*reinterpret_cast<const uint16_t*>(p + kInventoryIndex));
|
|
void* hero = game::GetPlayerHero();
|
|
if (!inventory || !hero) {
|
|
return;
|
|
}
|
|
using SlotFn = uint32_t(__fastcall*)(void* inventory, void* edx, uint32_t slot);
|
|
const auto getType = reinterpret_cast<SlotFn>(0x005d93e0); // cInventoryEntry::GetArtSlotType
|
|
const auto getRef = reinterpret_cast<SlotFn>(0x005d9420); // cInventoryEntry::GetArtSlotRef
|
|
auto* const* skills = reinterpret_cast<uint8_t* const*>(p + kSkillSlots);
|
|
if (!g_labelRectsValid) {
|
|
UpdateLabelRects(skills);
|
|
}
|
|
const unsigned shown = *reinterpret_cast<const uint16_t*>(p + kSlotCount);
|
|
for (unsigned slot = 0; slot < kSlots && slot < shown; ++slot) {
|
|
const uint32_t type = getType(inventory, nullptr, slot);
|
|
const uint32_t ref = getRef(inventory, nullptr, slot);
|
|
if (!skills[slot] || (type != 2 && type != 3 && type != game::kArtSlotTypeCombo) || !ref) {
|
|
continue;
|
|
}
|
|
const game::ArtCooldown cooldown = type == game::kArtSlotTypeCombo
|
|
? game::GetComboCooldown(hero, ref)
|
|
: game::GetArtCooldown(hero, game::ResolveArtId(hero, ref));
|
|
if (cooldown.remaining <= game::kArtReadyCooldown) {
|
|
continue; // ready: no label, the game's icon shows it
|
|
}
|
|
wchar_t text[16];
|
|
swprintf_s(text, L"%.1f", cooldown.seconds);
|
|
game::DrawText(renderer, kCooldownFont, g_labelRects[slot], text, kCooldownColor, kCooldownAlign);
|
|
}
|
|
}
|
|
|
|
// A click on a weapon set with an empty cursor (cUI_Taskbar2's mouse handling,
|
|
// `je 0x6e10d8` at 0x6e0ca7) takes the set's weapon onto the cursor, i.e.
|
|
// starts dragging it. Only a click on the selected set does that now; a click
|
|
// on another set just selects it.
|
|
|
|
bool __fastcall SelectInsteadOfDrag(void* taskbar, void*, const uint8_t* inventory, uint32_t set) {
|
|
set &= 0xffff;
|
|
const unsigned selected = reinterpret_cast<const uint16_t*>(inventory + skillslots::kSelected)[0];
|
|
if (set == selected) {
|
|
return false;
|
|
}
|
|
using SelectFn = void(__fastcall*)(void*, void*, uint32_t, int);
|
|
reinterpret_cast<SelectFn>(0x006e1440)(taskbar, nullptr, set, 1); // cUI_Taskbar2::SelectWeaponSet
|
|
return true;
|
|
}
|
|
|
|
// Jumped to instead of 0x6e10d8 (taskbar in esi, inventory in ebp, the set
|
|
// at [esp+0B8h]).
|
|
__declspec(naked) void WeaponSetClickThunk() {
|
|
__asm {
|
|
mov eax, [esp+0B8h]
|
|
push eax
|
|
push ebp
|
|
mov ecx, esi
|
|
call SelectInsteadOfDrag
|
|
test al, al
|
|
jz drag
|
|
mov al, 1
|
|
mov edx, 0x006e124d // handler exit, al = handled
|
|
jmp edx
|
|
drag:
|
|
mov edx, 0x006e10d8 // the drag (kWeaponSetDrag)
|
|
jmp edx
|
|
}
|
|
}
|
|
|
|
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::AfterLayout(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);
|
|
}
|
|
|
|
// cUI_Taskbar2::SelectSkillSlot (0x6e17d0): makes combat-art slot `slot` the
|
|
// right mouse button's art. Clicks on the bar, the mouse wheel, COMBINE_SLOTS
|
|
// and the slot count check all end up here; only a skill key may select a
|
|
// slot other than 0.
|
|
void __fastcall UiTaskbar_SelectSkillSlot(void* self, void* edx, uint32_t slot, int notify);
|
|
RE_HOOK(0x006e17d0, UiTaskbar_SelectSkillSlot);
|
|
void __fastcall UiTaskbar_SelectSkillSlot(void* self, void* edx, uint32_t slot, int notify) {
|
|
if (!mods::skillbar::KeySelects()) {
|
|
slot = 0;
|
|
}
|
|
UiTaskbar_SelectSkillSlot_original(self, edx, slot, notify);
|
|
// It also moves the selected slot background (orange) to the new slot;
|
|
// redo the backgrounds, which mark slot 0 (kTaskbarSkillSelectionLook).
|
|
if (self) {
|
|
using UpdateSlotHighlightFn = void(__fastcall*)(void*, void*, int force);
|
|
reinterpret_cast<UpdateSlotHighlightFn>(0x006df9a0)(self, nullptr, 1);
|
|
}
|
|
}
|
|
|
|
// cUI_Taskbar2::RenderArtSlots (0x6e31e0): then the cooldown labels.
|
|
void __fastcall UiTaskbar_RenderArtSlots(void* self, void* edx, void* renderer);
|
|
RE_HOOK(0x006e31e0, UiTaskbar_RenderArtSlots);
|
|
void __fastcall UiTaskbar_RenderArtSlots(void* self, void* edx, void* renderer) {
|
|
UiTaskbar_RenderArtSlots_original(self, edx, renderer);
|
|
mods::skillbar::RenderCooldowns(self, renderer);
|
|
}
|
|
|
|
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
|