Self / ally arts cast on the hero when pointed at an enemy

Arts with launch type 2 (buffs, auras, heals) are only valid on allies
or without a target; with an enemy under the cursor the game dropped
the order. Orders of a player hero for such arts lose a non-ally target,
which casts them on the hero. The launch type comes from the art
definition tables (game/engine/arts.h, tools/arttable.mjs).

Also: skill keys check the relocated taskbar slots (all 7), Ghidra
data labels (g_ names in names.txt).

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 01:02:43 +02:00
1 parent dbc632750b
commit 30c93fc87e
9 files changed
+187 -10

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
#pragma once
// Combat arts: the definition tables and how they are targeted.
#include <cstdint>
namespace game {
// How an art is launched (byte +0x3c of a g_spellDefs entry, +0x4c of a
// g_combatMoveDefs entry; cCreature::IsValidArtTarget 0x51d680 switches on it).
enum class LaunchType : uint8_t {
Target0 = 0, // aimed (e.g. Feuerball)
Enemy = 1, // aimed at an enemy (attacks, Blitzschlag)
SelfOrAlly = 2, // buffs, auras, heals: an ally, or the caster without a target
Ground = 3, // a point / area, summons
Corpse = 4, // a dead ally (resurrection)
AroundSelf = 5, // centred on the caster
Unknown = 0xff,
};
// SpellDef::Find (0x4d2150) / CombatMoveDef::Find (0x4d1930): the definition
// entry of art `id` (spells: small ids, combat moves: 0x3e8+), or null.
inline const uint8_t* FindSpellDef(uint16_t id) {
using Fn = const uint8_t*(__cdecl*)(uint32_t);
return reinterpret_cast<Fn>(0x004d2150)(id);
}
inline const uint8_t* FindCombatMoveDef(uint16_t id) {
using Fn = const uint8_t*(__cdecl*)(uint32_t);
return reinterpret_cast<Fn>(0x004d1930)(id);
}
// The launch type of art `id`, looked up as cEngine::UpdateCursorPick does.
inline LaunchType ArtLaunchType(uint16_t id) {
if (const uint8_t* spell = FindSpellDef(id)) {
return static_cast<LaunchType>(spell[0x3c]);
}
if (const uint8_t* move = FindCombatMoveDef(id)) {
return static_cast<LaunchType>(move[0x4c]);
}
return LaunchType::Unknown;
}
// cCreature::IsAlly (0x548f20): whether object `id` is an ally of `creature`.
inline bool IsAlly(void* creature, uint32_t id) {
using Fn = char(__fastcall*)(void*, void*, uint32_t);
return reinterpret_cast<Fn>(0x00548f20)(creature, nullptr, id) != 0;
}
} // namespace game
+56
View File
@@ -0,0 +1,56 @@
// Self / ally arts cast on the caster when pointed at an enemy.
//
// Using an art sends the hero a cEvent_creature 0x105 with the object under
// the cursor as target (+0x10, 0 for the ground or the hero itself) and the
// art id (+0x30). Arts with launch type SelfOrAlly (buffs, auras, heals) are
// only valid on allies or without a target (cCreature::IsValidArtTarget,
// 0x51d680); with an enemy under the cursor the game drops the order and
// nothing happens. For a player hero such an order loses its target instead,
// which casts the art on the hero, as a click on the hero does.
#include <cstdint>
#include "game/engine/arts.h"
#include "re/hook.h"
namespace game {
namespace {
constexpr uint32_t kOrderUseArt = 0x105;
constexpr uint32_t kPlayerIds = 0x11; // object ids below this are player heroes
// cEvent_creature fields used here.
constexpr size_t kOrderCommand = 0x4;
constexpr size_t kOrderTarget = 0x10; // object id
constexpr size_t kOrderArt = 0x30; // art id (cCreature::ResolveArtId)
uint32_t& Field(void* event, size_t offset) {
return *reinterpret_cast<uint32_t*>(static_cast<uint8_t*>(event) + offset);
}
void RetargetSelfArt(void* creature, void* event) {
if (!creature || !event || Field(event, kOrderCommand) != kOrderUseArt) {
return;
}
const uint32_t target = Field(event, kOrderTarget);
const uint32_t casterId = *reinterpret_cast<uint32_t*>(static_cast<uint8_t*>(creature) + 0xc);
if (target == 0 || casterId >= kPlayerIds) {
return;
}
if (ArtLaunchType(static_cast<uint16_t>(Field(event, kOrderArt))) == LaunchType::SelfOrAlly &&
!IsAlly(creature, target)) {
Field(event, kOrderTarget) = 0;
}
}
} // namespace
// cEngine::SendCreatureOrder (0x6170d0): orders for the controlled creature.
void __fastcall Engine_SendCreatureOrder(void* self, void* edx, void* creature, void* event);
RE_HOOK(0x006170d0, Engine_SendCreatureOrder);
void __fastcall Engine_SendCreatureOrder(void* self, void* edx, void* creature, void* event) {
RetargetSelfArt(creature, event);
Engine_SendCreatureOrder_original(self, edx, creature, event);
}
} // namespace game
+7
View File
@@ -83,6 +83,13 @@ void SelectWeaponSet(unsigned 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;
}
void OnTaskbar(void* taskbar) {
g_taskbar = static_cast<uint8_t*>(taskbar);
}
+3
View File
@@ -7,4 +7,7 @@ namespace mods::skillbar {
void SelectSkillSlot(unsigned slot);
void SelectWeaponSet(unsigned slot);
// Whether the taskbar shows combat-art slot `slot` (unlocked, control exists).
bool HasSkillSlot(const void* taskbar, unsigned slot);
} // namespace mods::skillbar
+4 -8
View File
@@ -11,6 +11,8 @@
#include "mods/skillkeys.h"
#include "mods/skillbar.h"
#include <cstdint>
#include <mutex>
@@ -21,11 +23,6 @@
namespace mods::skillkeys {
namespace {
// cUI_Taskbar2 fields.
constexpr size_t kTaskbarSlotCount = 0x15c; // uint16: combat-art slots the hero has
constexpr size_t kTaskbarSkillSlots = 0x178; // 5 slot controls (null: none)
constexpr unsigned kSlots = 5;
// The game window, if it has the focus (it does while a skill key is used).
HWND GameWindow() {
HWND window = GetForegroundWindow();
@@ -84,9 +81,7 @@ void OnSlotSelected(void* taskbar, unsigned slot) {
}
const int key = g_state.pendingKey;
g_state.pendingKey = 0;
const auto* bar = static_cast<const uint8_t*>(taskbar);
if (slot >= kSlots || slot >= *reinterpret_cast<const uint16_t*>(bar + kTaskbarSlotCount) ||
!reinterpret_cast<void* const*>(bar + kTaskbarSkillSlots)[slot]) {
if (!mods::skillbar::HasSkillSlot(taskbar, slot)) {
return; // empty slot: nothing was selected
}
HWND window = GameWindow();
@@ -118,3 +113,4 @@ void __fastcall UiTaskbar_SelectSkillSlot(void* self, void* edx, unsigned slot,
}
} // namespace game