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:
1 parent
5d80a33e9b
commit
c875d7d6b9
12 files changed
+933
-46
No files matched your search
@@ -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
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user