Art tooltips: "Verbessert durch" lists what improves each art

Arts are objects (game::Art) built from the game's definitions: name,
launch type, damage source, regeneration group and the skills' art lists.
The tooltip gets one line per effect of a skill or attribute, sorted by
size; unlearned class skills are greyed out at 0%. Weapon arts list only
the damage types the held weapon deals and the held weapon's skill.
Tooltip line wrap ignores colour codes; art tooltips wrap wider.
Blitzschlag's tooltip rewrite becomes a tooltip filter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-18 23:04:22 +02:00
1 parent 5d80a33e9b
commit c875d7d6b9
12 files changed
+933 -46

No files matched your search

+110
View File
@@ -0,0 +1,110 @@
// Art tooltips name what improves the art (request of 2026-10-18: "edit ALL
// skill tooltips, to show which attributes and abilities improve them in
// which way"; then: arts as objects with their effects, name and tooltip,
// lines like "Damage improved by [Axe Lore, Strength, ...]").
//
// cCreatureArts::ArtTooltip (0x589270, thiscall on creature +0x3a8, (art,
// text, flag), ret 0xc) writes the game's description and values into a wide
// string; game::Art::Tooltip (game/engine/art.cpp) applies the mods' changes
// and adds "Verbessert durch:" with one line per skill or attribute acting on
// the art, one line per effect sorted by size, e.g. "Stärke -> Schaden +12%"
// (unlearned skills greyed out with 0%). Combos (0x200..0x204) get no text
// from the game. Art tooltips wrap at a wider line (below).
#include <cstdint>
#include <cstring>
#include <string>
#include <string_view>
#include "game/engine/art.h"
#include "game/engine/arts.h"
#include "game/ui/texts.h"
#include "re/hook.h"
#include "re/log.h"
#include "re/patch.h"
namespace mods::arttooltip {
namespace {
void __fastcall Arts_Tooltip(void* self, void* edx, uint32_t art, void* text, uint32_t flag);
RE_HOOK(0x00589270, Arts_Tooltip);
void __fastcall Arts_Tooltip(void* self, void* edx, uint32_t art, void* text, uint32_t flag) {
Arts_Tooltip_original(self, edx, art, text, flag);
const std::wstring gameText = game::WideStringText(text);
const game::Art* object = game::FindArt(static_cast<uint16_t>(art));
if (gameText.empty() || !object) {
return;
}
void* creature = static_cast<uint8_t*>(self) - game::kCreatureArts;
game::SetWideString(text, object->Tooltip(creature, gameText));
}
// Line wrap. With flag 0x80 (+0x154) cUI_Tooltip::FormatText (0x6e6630,
// thiscall (char), ret 4; called by SetText 0x6e6ae0) copies the text
// (+0x115c begin, +0x1160 end) and turns the first space after 45 characters
// of a line into a line break (the check `cmp ecx, 5Ah` on the line's bytes at
// 0x6e687d); the width then follows the longest line. Colour codes counted as
// characters there. Now they don't, and art tooltips wrap later, so the
// "Verbessert durch" lines stay whole.
constexpr int kGameWrapChars = 45;
constexpr int kArtWrapChars = 56;
constexpr size_t kTooltipText = 0x115c;
constexpr size_t kColourCodeLength = 10; // "\cAARRGGBB"
int g_wrapChars = kGameWrapChars;
void __fastcall Tooltip_FormatText(void* self, void* edx, char flag);
RE_HOOK(0x006e6630, Tooltip_FormatText);
void __fastcall Tooltip_FormatText(void* self, void* edx, char flag) {
auto* const* text = reinterpret_cast<const wchar_t* const*>(static_cast<uint8_t*>(self) + kTooltipText);
const bool art = text[0] && std::wstring_view(text[0], text[1] - text[0]).find(game::kImprovedByHeader) !=
std::wstring_view::npos;
g_wrapChars = art ? kArtWrapChars : kGameWrapChars;
Tooltip_FormatText_original(self, edx, flag);
g_wrapChars = kGameWrapChars;
}
// Characters of the line [start, current) beyond the wrap width, colour codes not counted.
int __cdecl WrapExcess(const wchar_t* start, const wchar_t* current) {
int chars = 0;
for (const wchar_t* c = start; c < current; ++c, ++chars) {
if (c[0] == L'\\' && c[1] == L'c') {
c += kColourCodeLength - 1;
--chars;
}
}
return chars - g_wrapChars;
}
// Replaces `mov ecx, edi; sub ecx, esi; and ecx, -2; cmp ecx, 5Ah` (line start
// in esi, current character in edi, the character in ax); the `jle` after it
// keeps the line going.
constexpr uint32_t kWrapCheck = 0x006e6876;
__declspec(naked) void WrapCheckThunk() {
__asm {
push eax
push edx
push edi
push esi
call WrapExcess
add esp, 8
mov ecx, eax
pop edx
pop eax
cmp ecx, 0
ret
}
}
void Startup() {
const int32_t rel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(&WrapCheckThunk) - (kWrapCheck + 5));
uint8_t to[10] = {0xe8, 0, 0, 0, 0, 0x90, 0x90, 0x90, 0x90, 0x90};
std::memcpy(to + 1, &rel, 4);
const uint8_t from[10] = {0x8b, 0xcf, 0x2b, 0xce, 0x83, 0xe1, 0xfe, 0x83, 0xf9, 0x5a};
const bool ok = re::ReplaceCode(kWrapCheck, from, to, sizeof(to));
re::Log("arttooltip: tooltip wrap ignores colour codes, art tooltips %d characters wide%s", kArtWrapChars,
ok ? "" : " (patch FAILED)");
}
RE_STARTUP(ArtTooltipStartup, Startup);
} // namespace
} // namespace mods::arttooltip
+8 -35
View File
@@ -34,6 +34,7 @@
#include <windows.h>
#include "game/engine/art.h"
#include "game/engine/arts.h"
#include "game/engine/fx.h"
#include "game/engine/objects.h"
@@ -373,8 +374,7 @@ uint32_t __fastcall NextTarget(void* manager, void*, const int32_t* position, ui
// The tooltip: the game's "Anzahl: X" line (its hit count) becomes the mod's
// two numbers, hits per chain and chains, in both the current level's section
// and the next level's (skill book). cCreatureArts::ArtTooltip (0x589270,
// thiscall on creature +0x3a8, (art, text, flag), ret 0xc) builds the text
// into a wide string {begin, end, capacity}; sections start with the level
// mods/arttooltip.cpp, game::Art::Tooltip) builds the text; sections start with the level
// line, values are "Name: value" lines separated by '\n' (measured for art
// 0x21 at level 5: "...|Aktuelle Stufe: 5|Regeneration: 3.5s|Schaden: ...|
// Anzahl: 7", with flag 1 also "Nächste erlernte Stufe: 6" and its values).
@@ -386,22 +386,6 @@ constexpr const wchar_t* kCurrentLevelLine = L"Aktuelle Stufe:";
constexpr const wchar_t* kNextLevelLine = L"Nächste erlernte Stufe:";
constexpr const wchar_t* kCountLine = L"Anzahl:";
// The game's wide string append (0x592ef0, thiscall (first, last, unused),
// ret 0xc); grows the buffer with the game's allocator.
void AppendText(void* text, const wchar_t* first, const wchar_t* last) {
using Fn = void*(__fastcall*)(void*, void*, const wchar_t*, const wchar_t*, uint32_t);
reinterpret_cast<Fn>(0x00592ef0)(text, nullptr, first, last, 0);
}
void SetText(void* text, const std::wstring& value) {
auto** string = static_cast<wchar_t**>(text);
if (string[0]) {
string[1] = string[0];
*string[0] = L'\0';
}
AppendText(text, value.data(), value.data() + value.size());
}
std::wstring Rewrite(const std::wstring& text, unsigned currentLevel) {
std::wstring out;
unsigned level = currentLevel;
@@ -431,24 +415,13 @@ std::wstring Rewrite(const std::wstring& text, unsigned currentLevel) {
return out;
}
void __fastcall Arts_Tooltip(void* self, void* edx, uint32_t art, void* text, uint32_t flag);
RE_HOOK(0x00589270, Arts_Tooltip);
void __fastcall Arts_Tooltip(void* self, void* edx, uint32_t art, void* text, uint32_t flag) {
Arts_Tooltip_original(self, edx, art, text, flag);
if (!text || static_cast<uint16_t>(art) != kArtBlitzschlag) {
return;
}
auto* const* string = static_cast<wchar_t* const*>(text);
if (!string[0] || string[1] <= string[0]) {
return;
}
void* creature = static_cast<uint8_t*>(self) - game::kCreatureArts;
const std::wstring before(string[0], string[1]);
const std::wstring after = Rewrite(before, ArtLevel(creature, kArtBlitzschlag));
if (after != before) {
SetText(text, after);
}
void RewriteTooltip(const game::Art& art, void* creature, std::wstring& text) {
text = Rewrite(text, ArtLevel(creature, art.Id()));
}
struct TooltipRegistration {
TooltipRegistration() { game::AddTooltipFilter(kArtBlitzschlag, &RewriteTooltip); }
} g_tooltipRegistration;
} // namespace
} // namespace mods::chainlightning
+9 -8
View File
@@ -29,6 +29,7 @@
#include "game/engine/arts.h"
#include "game/engine/objects.h"
#include "game/engine/skills.h"
#include "game/ui/events.h"
#include "game/ui/text.h"
#include "mods/hud.h"
@@ -46,7 +47,7 @@ constexpr size_t kPlusButtons = 0x2cc;
constexpr size_t kMinusButtons = 0x8ac; // unused by the game
constexpr size_t kButtonSize = 0xbc;
constexpr int kRows = 8;
constexpr int kAttributeRows = 6;
constexpr int kAttributeRows = game::kAttributeCount;
// cUI_Button (vtable 0x895308).
constexpr size_t kControlFlags = 0x10;
@@ -85,13 +86,13 @@ constexpr float kIconSize = 16.0f;
// The − sits left of the value column (values end at the + at x 232).
constexpr int kMinusX = 184;
// cCreatureArts fields (see the top).
constexpr size_t kAttributes = 0x1c;
constexpr int kFreeAttributesIndex = 7;
constexpr size_t kSkillIds = 0x24;
constexpr size_t kSkillLevels = 0x2c;
constexpr size_t kSkillBonus = 0x34;
constexpr size_t kSkillChoices = 0x44;
// cCreatureArts fields (see the top, game/engine/skills.h).
using game::kFreeAttributesIndex;
constexpr size_t kAttributes = game::kArtsInvested;
constexpr size_t kSkillIds = game::kArtsSkillIds;
constexpr size_t kSkillLevels = game::kArtsSkillLevels;
constexpr size_t kSkillBonus = game::kArtsSkillBonus;
constexpr size_t kSkillChoices = game::kArtsSkillChoices;
using AddAttributeFn = void(__fastcall*)(void* arts, void*, int index, int delta);
using AddSkillLevelsFn = void(__fastcall*)(void* arts, void*, int row, int delta, int spendPoints);