Skills, auto-attack, aura and chain lightning rework, Esc, cache fix

- Skill bar: no art selection; slot 0 is the right-click art, keys 0-6 cast
  slots directly; cooldown labels in exact seconds; slot 0 set apart.
- Arts regenerate on their own (casting no longer refills the group).
- Auto-attack after targeting or casting at a hostile creature, at the speed
  of a held button.
- Auras can be recast while active (renew, no second visual).
- Blicke wie Klingen hits every enemy in range, physical, with life leech.
- Blitzschlag steers the game's own chain: 3 + level/5 hits, 1 + level/10
  chains, re-hits a lone enemy.
- Weapon sets: click selects, drag only from the selected set; the leftmost
  set is clickable (signed hit tests).
- Esc closes open windows before opening the game menu.
- HUD: standalone buttons and texts are centred.
- Record cache sized for widened views (town lag while walking).
- Shared game code: orders, fx events, text drawing, cooldowns, effects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-16 00:11:38 +02:00
1 parent f721d2a486
commit 1fd89524d3
41 files changed
+2347 -214

No files matched your search

+194 -15
View File
@@ -12,6 +12,15 @@
// 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.
//
// 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.
@@ -19,14 +28,21 @@
#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;
@@ -39,10 +55,36 @@ 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;
@@ -53,6 +95,12 @@ void Startup() {
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);
}
@@ -66,39 +114,143 @@ void Select(uint32_t function, unsigned slot) {
}
}
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;
}
// 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);
}
}
bool HasSkillSlot(const void* taskbar, unsigned slot) {
const auto* p = static_cast<const uint8_t*>(taskbar);
constexpr size_t kSlotCount = 0x15c; // u16: slots shown, from SetSlotCount
return slot < kSlots && slot < *reinterpret_cast<const uint16_t*>(p + kSlotCount) &&
reinterpret_cast<void* const*>(p + kSkillSlots)[slot] != nullptr;
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);
}
// Copy of the first 5 controls of each row into the old arrays.
void MirrorOldArrays(void* 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).
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) || !ref) {
continue;
}
const uint16_t art = game::ResolveArtId(hero, ref);
const game::ArtCooldown cooldown = game::GetArtCooldown(hero, art);
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) {
@@ -122,7 +274,7 @@ 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);
mods::skillbar::AfterLayout(self);
}
// cUI_Taskbar2::SetSlotCount (0x6dffa0): shows the first n slots of each row.
@@ -133,6 +285,33 @@ void __fastcall UiTaskbar_SetSlotCount(void* self, void* edx, uint32_t count) {
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) {