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

+2
View File
@@ -38,6 +38,7 @@ add_library(sacred_re SHARED
src/game/engine/items.cpp src/game/engine/items.cpp
src/game/engine/stream.cpp src/game/engine/stream.cpp
src/game/ui/texts.cpp src/game/ui/texts.cpp
src/game/engine/art.cpp
src/mods/borderless.cpp src/mods/borderless.cpp
src/mods/widescreen.cpp src/mods/widescreen.cpp
src/mods/hud.cpp src/mods/hud.cpp
@@ -58,6 +59,7 @@ add_library(sacred_re SHARED
src/mods/combos.cpp src/mods/combos.cpp
src/mods/aurahits.cpp src/mods/aurahits.cpp
src/mods/chainlightning.cpp src/mods/chainlightning.cpp
src/mods/arttooltip.cpp
src/mods/escape.cpp src/mods/escape.cpp
src/mods/recordcache.cpp src/mods/recordcache.cpp
src/mods/megamap.cpp src/mods/megamap.cpp
+118
View File
@@ -0,0 +1,118 @@
# What improves an art (mods/arttooltip.cpp)
Request of 2026-10-18: "edit ALL skill tooltips, to show which attributes and
abilities improve them in which way".
Follow-ups the same day: arts as objects carrying their effects, name and
tooltip, no art-id checks in code; a dense block, one line per Fertigkeit or
attribute ("Stärke -> Schaden +12%"); only skills and attributes that act on
the art; unlearned skills (of the hero's class) with 0% and greyed out;
lines sorted by impact. Then: only "Regenerationszeit" (faster
regeneration rates are shown as the time they save), no attribute flavor
texts; one line per effect (an attribute with two effects gets two lines,
each sorted by its own size); a wider tooltip so the lines don't wrap.
## Design
- `game::Art` (game/engine/art.{h,cpp}): built on demand by `FindArt(id)`
from the game's definitions (name text SpellDef / CombatMoveDef +4, launch
type, spell or weapon damage values, regeneration group).
`Improvements(creature)` gives every (skill or attribute, stat, percent)
acting on the art for that hero; `Tooltip` = the game's text, the mods'
filters (`AddTooltipFilter`, e.g. Blitzschlag's values), then the block.
- Art ids appear only in data: the game's per-skill art lists and three
exceptions (Energieblitze regenerates as magic; 0x3fa and 0x38 take weapon
damage factors), both in art.cpp.
- Weapon skills count only with their weapon in hand (the game's checks
0x54a3e0 / 0x54a220 / 0x54a2f0 / 0x54a2e0 / 0x54a110, weapon class); only
the skills of the weapon in hand are listed. Their second curve (SP) is the attack speed
(+0x3c; the skill text labels it with text 0x44c, the same "speed" text the
magic schools use for +0x3e).
- mods/arttooltip.cpp hooks ArtTooltip and hands the text to the art.
- Width: tooltips with flag 0x80 wrap in cUI_Tooltip::FormatText
(`0x6e6630`): the first space after 45 characters of a line becomes a line
break (`cmp ecx, 5Ah` on the line's bytes at `0x6e687d`); the layout
(`0x6e7730`) then sizes the window to the longest line + 8. Colour codes
(`\cAARRGGBB`, 10 characters) counted there; the mod replaces the check so
they don't, and texts containing "Verbessert durch:" wrap after 56.
## Sources
Texts: `scripts/us/global.res` holds the German texts (despite the folder):
`"SZ"`, then 16-byte entries {key hash, offset, 0, length}, the UTF-16 text at
offset + 4. Skill names are game text `0x24b7 + skill id` (character panel
`0x6abc8d`); attribute names `UI_ATTRIB_STRENGTH`, `_RESISTANCE`,
`_GESCHICK`, `_PHYREG`, `_MAGREG`, `_CHARISMA`.
Skill ids (0x565f30's switch, names in file order): 1 Himmelsmagie,
2 Waffenkunde, 3 Stangenwaffen, 4 Schwertkunde, 5 Axtkampf,
6 Zwei-Waffen-Kampf, 7 Fernkampf, 8 Wendigkeit, 9 Parade, 0xa Konstitution,
0xb Rüstung, 0xc Meditation, 0xd Klingenkampf, 0xe Magiekunde, 0xf
Feuermagie, 0x10 Wassermagie, 0x11 Erdmagie, 0x12 Luftmagie, 0x13 Mondmagie,
0x14 Vampirismus, 0x15 Handel, 0x16 Reiten, 0x17 Entwaffnung, 0x18
Faustkampf, 0x19 Konzentration, 0x1a Ballistik, 0x1b Fallenkunst, 0x1c
Blutrausch, 0x1d Waffentechnik, 0x1e Zweihandwaffen, 0x1f Zwergenkunde, 0x20
Höllenmacht, 0x21 Schmiedekunst.
Balance values (globals `0xad2400..`, parsed as `key=value` around `0x5eb4xx`;
no Balance.txt ships, so the defaults stand). Skill curves are 12-byte
{start `<k>off`, half `<k>_s`, limit `<k>_w`} at `0xad2450 + 12 n`: WK
Waffenkunde, MK Magiekunde, FK/SK/AK/KK/STK/BK/FEK weapon skills (AW / SP),
HM/FM/EM/WM/LM/MM schools, BA, BL, KO Konzentration, Med Meditation, W, HR,
HP, P, R (resistances), then the price tables; HOM `0xad368c`, WT `0xad3698`.
Value(level) = 0 below 1, else start + 2 (limit − start)(1 − 1/((level − 1)/half + 1))
percent (DiminishingCurve `0x565c30`).
## How the game applies them
cCreatureArts::Recalculate (`0x579560`), then ApplySkill (`0x565f30`) per
skill row with level = bought + item levels:
- Attributes (+0x10 u16[6]) = invested (+0x1c) + base (+0x4a) + base ×
(level − 1) / 10, plus items. Hero level +0x56.
- Damage factors, 4 types (physical, fire, magic, poison):
weapon / combat arts +0x96 + +0xa6, spells +0x96 + +0xb6 (ArtTooltip
multiplies spell values by these; arts 0x3fa and 0x38 by the combat ones).
+0x96 = 1 + items. Attribute shares with F = (156 − BalStatOff) × level / 156
+ BalStatOff + 9 and invested points p, base b, item points q:
- +0xa6 physical: (b F/10 + p + q) of Stärke, or Geschick when
HoldsRangedWeapon (`0x54a110`), / BalCharPD
- +0xaa fire: (2 p + b F/10 + q) of Stärke and Geschick / BalCharFD
- +0xae magic: (b F/10 + p + q) of Mentale Regeneration / BalCharMD
- +0xb2 poison: (2 p + b F/10 + q) of Widerstand and Charisma / BalCharGD
- +0xb6..+0xc2 spells: (b F/10 + p) of Mentale Regeneration / BalCharMD
- Waffenkunde adds its % to +0xa6..+0xb2, Magiekunde to +0xb6..+0xc2.
- Regeneration rates: +0xee magic = (1 + items + Meditation %) × (1 +
Mentale Regeneration / 100), +0xf2 combat = (1 + items) × (1 + Physische
Regeneration / 100). Creature type 8 (Zwerg) gets +0xee = +0xf2.
- Regeneration time: ApplySkillToArts (`0x565c80`) divides each listed art's
cooldown total by 1 + value / 100. With the attack flag it also adds curve
FEK..SP (`0xad2504`) to +0x3e while the art is the selected art slot. Lists
(art ids) per skill:
- Himmelsmagie (HM, attack): d f 11 12 13 14 15 16 3fb
- Feuermagie (FM, attack): 5 b 17 18; Wassermagie (WM): 1b 4 22 23;
Erdmagie (EM): 2 10 19 1a; Luftmagie (LM): 1 1c 1d 21;
Mondmagie (MM): 3 7 24..2a
- Vampirismus (KO): 3ff 401 403 405 40e..411 408 2b 406 407 409..40d
- Konzentration (KO): 3e8..3fa 3fc 421..434 41e 3fe 400 402 404 412..416
443 446 444 449 44e 44a 44d 44b 44f 44c 436 437 438 43a 43b 43c 441
- Ballistik (BA, attack) and Fallenkunst (KO): 417..41d
- Blutrausch (BL, attack): 3fe 2b 408 406 407 409..40d
- Waffentechnik (WT, attack) and Zwergenkunde (KO): 43f 43e 43d 37..3b
- Höllenmacht (HOM): 3c..43
- The weapon skills (Stangenwaffen, Schwertkunde, Axtkampf, ...) raise the
attack value (+0xe6, AW curve) and attack speed (+0x3c, SP curve) while
their weapon is held; listed for weapon arts with that weapon in hand.
- The held weapon's damage per type (physical, fire, magic, poison) is at
cCreatureArts +0x76 + 4 t (max) and +0x86 + 4 t (min); the damage range
functions (0x563e90 .. 0x564160) multiply them by +0x96 + +0xa6 + 4 t. The
attributes' damage lines are listed only for the types the weapon deals.
Measured 2026-10-18 (diag log): +0xa6..+0xb2 and +0xb6 equal the sums of the
listed shares exactly (e.g. 5.422 = Waffenkunde 300.96% + Stärke 241.21%),
Physische Regeneration matches +0xf2 / (1 + items).
Art definitions: ArtSpellDef (arts.h) as ArtTooltip finds the spell values
(own SpellDef, or the one CombatMoveDef::LinkedSpell `0x4d1c50` names);
damage values 4 × (base, per level) s16 at SpellDef +0x42.
+339
View File
@@ -0,0 +1,339 @@
#include "game/engine/art.h"
#include <algorithm>
#include <cmath>
#include <map>
#include <memory>
#include "game/engine/orders.h"
#include "game/engine/skills.h"
#include "game/ui/texts.h"
namespace game {
namespace {
// The game's per-skill art lists (cCreatureArts::ApplySkill 0x565f30 passes
// them to ApplySkillToArts 0x565c80): the regeneration time of each listed art
// is divided by 1 + curve / 100; skills with the speed flag also add curve
// FEK..SP to the execution speed +0x3e of the selected art. Ballistik raises
// the damage of its traps too (ArtTooltip's damage factor for them).
struct LinkedStat {
ArtStat stat;
uint32_t curve;
};
struct SkillArts {
uint8_t skill;
std::vector<LinkedStat> stats;
std::vector<uint16_t> arts;
};
const std::vector<SkillArts>& SkillArtLists() {
using S = ArtStat;
auto school = [](uint32_t curve) {
return std::vector<LinkedStat>{{S::RegenerationTime, curve}, {S::ExecutionSpeed, kCurveExecutionSpeed}};
};
static const std::vector<SkillArts> lists = {
{kHimmelsmagie, school(kCurveHimmelsmagie), {0x0d, 0x0f, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x3fb}},
{kFeuermagie, school(kCurveFeuermagie), {0x05, 0x0b, 0x17, 0x18}},
{kWassermagie, school(kCurveWassermagie), {0x1b, 0x04, 0x22, 0x23}},
{kErdmagie, school(kCurveErdmagie), {0x02, 0x10, 0x19, 0x1a}},
{kLuftmagie, school(kCurveLuftmagie), {0x01, 0x1c, 0x1d, 0x21}},
{kMondmagie, school(kCurveMondmagie), {0x03, 0x07, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a}},
{kVampirismus,
{{S::RegenerationTime, kCurveKonzentration}},
{0x3ff, 0x401, 0x403, 0x405, 0x40e, 0x40f, 0x410, 0x411, 0x408, 0x2b, 0x406, 0x407, 0x409, 0x40a, 0x40b,
0x40c, 0x40d}},
{kKonzentration,
{{S::RegenerationTime, kCurveKonzentration}},
{0x3e8, 0x3e9, 0x3ea, 0x3eb, 0x3ec, 0x3ed, 0x3ee, 0x3ef, 0x3f0, 0x3f1, 0x3f2, 0x3f3, 0x3f4, 0x3f5, 0x3f6,
0x3f7, 0x3f8, 0x3f9, 0x3fa, 0x3fc, 0x421, 0x422, 0x423, 0x424, 0x425, 0x426, 0x427, 0x428, 0x429, 0x42a,
0x42b, 0x42c, 0x42d, 0x42e, 0x42f, 0x430, 0x431, 0x432, 0x433, 0x434, 0x41e, 0x3fe, 0x400, 0x402, 0x404,
0x412, 0x413, 0x414, 0x415, 0x416, 0x443, 0x446, 0x444, 0x449, 0x44e, 0x44a, 0x44d, 0x44b, 0x44f, 0x44c,
0x436, 0x437, 0x438, 0x43a, 0x43b, 0x43c, 0x441}},
{kBallistik,
{{S::RegenerationTime, kCurveBallistik},
{S::ExecutionSpeed, kCurveExecutionSpeed},
{S::Damage, kCurveBallistik}},
{0x417, 0x418, 0x419, 0x41a, 0x41b, 0x41c, 0x41d}},
{kFallenkunst, {{S::RegenerationTime, kCurveKonzentration}}, {0x417, 0x418, 0x419, 0x41a, 0x41b, 0x41c, 0x41d}},
{kBlutrausch, school(kCurveBlutrausch), {0x3fe, 0x2b, 0x408, 0x406, 0x407, 0x409, 0x40a, 0x40b, 0x40c, 0x40d}},
{kWaffentechnik, school(kCurveWaffentechnik), {0x43f, 0x43e, 0x43d, 0x37, 0x38, 0x39, 0x3a, 0x3b}},
{kZwergenkunde,
{{S::RegenerationTime, kCurveKonzentration}},
{0x43f, 0x43e, 0x43d, 0x37, 0x38, 0x39, 0x3a, 0x3b}},
{kHoellenmacht, {{S::RegenerationTime, kCurveHoellenmacht}}, {0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43}},
};
return lists;
}
// Arts the game treats unlike their definitions say: Energieblitze counts down
// at the magic rate (+0xee) although it is a combat move (0x562998,
// ArtTooltip); 0x3fa and 0x38 take the weapon's damage factors although they
// have spell values (ArtTooltip).
constexpr uint16_t kMagicRegenerationMoves[] = {kArtEnergieblitze};
constexpr uint16_t kWeaponDamageSpells[] = {0x3fa, 0x38};
template <size_t N>
bool Contains(const uint16_t (&ids)[N], uint16_t id) {
return std::find(std::begin(ids), std::end(ids), id) != std::end(ids);
}
// cCreature weapon checks (plain thiscall, as ApplySkill calls them).
bool CreatureCheck(void* creature, uint32_t address) {
using Fn = bool(__fastcall*)(void*, void*);
return reinterpret_cast<Fn>(address)(creature, nullptr);
}
bool HoldsTwoWeapons(void* creature) {
return CreatureCheck(creature, 0x0054a2e0); // weapon class 8
}
constexpr size_t kCreatureSecondHand = 0x1d8;
constexpr int kWeaponClassBareHands = 0;
constexpr int kWeaponClassBlade = 10;
// The weapon skills: attack value (+0xe6 += AW curve %) and attack speed
// (+0x3c += SP curve, the next curve) while the matching weapon is held
// (ApplySkill cases 3..7, 0xd, 0x18).
struct WeaponSkill {
uint8_t skill;
uint32_t curve;
bool (*applies)(void* creature);
};
const WeaponSkill kWeaponSkills[] = {
{kStangenwaffen, kCurveStangenwaffenValue,
[](void* c) { return CreatureCheck(c, 0x0054a3e0) && !HoldsTwoWeapons(c); }},
{kSchwertkunde, kCurveSchwertkundeValue,
[](void* c) { return CreatureCheck(c, 0x0054a220) && !HoldsTwoWeapons(c); }},
{kAxtkampf, kCurveAxtkampfValue, [](void* c) { return CreatureCheck(c, 0x0054a2f0) && !HoldsTwoWeapons(c); }},
{kZweiWaffenKampf, kCurveZweiWaffenValue,
[](void* c) {
return HoldsTwoWeapons(c) ||
(WeaponClass(c) == kWeaponClassBareHands &&
*reinterpret_cast<const uint32_t*>(static_cast<uint8_t*>(c) + kCreatureSecondHand) != 0);
}},
{kFernkampf, kCurveFernkampfValue, [](void* c) { return HoldsRangedWeapon(c); }},
// The game also counts a blade in the second hand; not covered here.
{kKlingenkampf, kCurveKlingenkampfValue, [](void* c) { return WeaponClass(c) == kWeaponClassBlade; }},
{kFaustkampf, kCurveFaustkampfValue, [](void* c) { return WeaponClass(c) == kWeaponClassBareHands; }},
};
// The held weapon's damage per type (physical, fire, magic, poison):
// cCreatureArts +0x76 + 4 t max, +0x86 + 4 t min (floats; the damage range
// functions 0x563e90 .. 0x564160 multiply them by +0x96 + +0xa6 + 4 t).
constexpr size_t kArtsWeaponDamageMax = 0x76;
constexpr size_t kArtsWeaponDamageMin = 0x86;
bool WeaponDealsDamage(const uint8_t* arts, int damageType) {
auto value = [&](size_t offset) { return *reinterpret_cast<const float*>(arts + offset + 4 * damageType); };
return value(kArtsWeaponDamageMax) > 0.0f || value(kArtsWeaponDamageMin) > 0.0f;
}
// Recalculate (0x579560) gives creature type 8 (Zwerg) the combat rate for
// magic arts too.
constexpr size_t kCreatureType = 0x10;
constexpr uint32_t kTypeZwerg = 8;
// The attributes' shares of the damage factors (Recalculate), in percent:
// (base * F / 10 + invested * w [+ item points]) / BalChar*D with F = (156 -
// BalStatOff) * level / 156 + BalStatOff + 9.
double AttributeDamage(const uint8_t* arts, int attribute, int damageType, double investedWeight, bool items) {
const float off = BalanceValue(kBalStatOff);
const double f = (156.0 - off) * HeroLevel(arts) / 156.0 + off + 9.0;
double value = AttributeBase(arts, attribute) * f * 0.1 + investedWeight * arts[kArtsInvested + attribute];
if (items) {
value += AttributeFromItems(arts, attribute);
}
return 100.0 * value / BalanceValue(kBalCharDamage[damageType]);
}
double RegenerationTimePercent(float bonus) {
return -100.0 * (1.0 - 1.0 / (1.0 + bonus / 100.0));
}
bool Offensive(LaunchType launch) {
return launch != LaunchType::SelfOrAlly && launch != LaunchType::Corpse && launch != LaunchType::Unknown;
}
// Damage values of a spell definition: 4 types x (base, per level), s16 at +0x42.
bool HasDamageValues(const uint8_t* spell) {
const auto* values = reinterpret_cast<const int16_t*>(spell + 0x42);
return std::any_of(values, values + 8, [](int16_t v) { return v != 0; });
}
const wchar_t* StatName(ArtStat stat) {
switch (stat) {
case ArtStat::Damage:
return L"Schaden";
case ArtStat::FireDamage:
return L"Feuerschaden";
case ArtStat::MagicDamage:
return L"Magieschaden";
case ArtStat::PoisonDamage:
return L"Giftschaden";
case ArtStat::AttackValue:
return L"Angriffswert";
case ArtStat::AttackSpeed:
return L"Angriffstempo";
case ArtStat::ExecutionSpeed:
return L"Ausführungstempo";
case ArtStat::RegenerationTime:
return L"Regenerationszeit";
}
return L"";
}
// Tooltip colours (ARGB, "\c" codes as the game's tooltips use them).
constexpr const wchar_t* kHeaderColour = L"\\cFFFFFF80";
constexpr const wchar_t* kTextColour = L"\\cFFFFFFFF";
constexpr const wchar_t* kInactiveColour = L"\\cFFAFAFAF";
std::wstring ImproverName(const Improver& by) {
return by.kind == Improver::Skill ? SkillName(by.id) : AttributeName(by.id);
}
std::map<uint16_t, std::vector<Art::TooltipFilter>>& Filters() {
static std::map<uint16_t, std::vector<Art::TooltipFilter>> filters;
return filters;
}
} // namespace
Art::Art(uint16_t id) : id_(id) {
const uint8_t* spell = FindSpellDef(id);
const uint8_t* def = spell ? spell : FindCombatMoveDef(id);
nameText_ = def ? *reinterpret_cast<const uint32_t*>(def + 4) : 0; // SpellDef / CombatMoveDef +4 (0x4d24d0, 0x4d1b40)
launch_ = ArtLaunchType(id);
const uint8_t* values = ArtSpellDef(id);
spellValues_ = values && !Contains(kWeaponDamageSpells, id);
magicRegeneration_ = spell || Contains(kMagicRegenerationMoves, id);
damage_ = Offensive(launch_) && (!values || HasDamageValues(values));
for (const SkillArts& list : SkillArtLists()) {
if (std::find(list.arts.begin(), list.arts.end(), id) == list.arts.end()) {
continue;
}
for (const LinkedStat& linked : list.stats) {
if (linked.stat != ArtStat::Damage || damage_) {
links_.push_back({list.skill, linked.curve, linked.stat});
}
}
}
}
std::wstring Art::Name() const {
return nameText_ ? GameText(nameText_) : std::wstring();
}
std::vector<Improvement> Art::Improvements(void* creature) const {
auto* arts = static_cast<uint8_t*>(creature) + kCreatureArts;
const uint8_t* classSkills = ClassSkills(arts);
std::vector<Improvement> out;
auto add = [&](Improver by, ArtStat stat, double percent) {
out.push_back({by, stat, static_cast<float>(percent)});
};
auto skill = [&](uint8_t id, uint32_t curve, ArtStat stat) {
const unsigned level = SkillLevel(arts, id);
const bool ofClass =
level > 0 || (classSkills && std::find(classSkills, classSkills + kClassSkills, id) != classSkills + kClassSkills);
if (!ofClass) {
return;
}
const float value = CurveValue(curve, level);
add({Improver::Skill, id}, stat, stat == ArtStat::RegenerationTime ? RegenerationTimePercent(value) : value);
};
// A faster regeneration rate (+x %) as the time it saves.
auto rate = [&](Improver by, double percent) {
add(by, ArtStat::RegenerationTime, RegenerationTimePercent(static_cast<float>(percent)));
};
auto attribute = [&](uint8_t id, ArtStat stat, double percent) { add({Improver::Attribute, id}, stat, percent); };
if (damage_ && spellValues_) {
// Spell values times +0x96 + +0xb6: Magiekunde, Mentale Regeneration.
skill(kMagiekunde, kCurveMagiekunde, ArtStat::Damage);
attribute(kMentaleRegeneration, ArtStat::Damage, AttributeDamage(arts, kMentaleRegeneration, 2, 1.0, false));
} else if (damage_) {
// Weapon damage times +0x96 + +0xa6: Waffenkunde for all types; the
// attributes for the types the held weapon deals.
skill(kWaffenkunde, kCurveWaffenkunde, ArtStat::Damage);
if (WeaponDealsDamage(arts, 0)) {
const uint8_t physical = HoldsRangedWeapon(creature) ? kGeschick : kStaerke;
attribute(physical, ArtStat::Damage, AttributeDamage(arts, physical, 0, 1.0, true));
}
if (WeaponDealsDamage(arts, 1)) {
attribute(kStaerke, ArtStat::FireDamage, AttributeDamage(arts, kStaerke, 1, 2.0, true));
attribute(kGeschick, ArtStat::FireDamage, AttributeDamage(arts, kGeschick, 1, 2.0, true));
}
if (WeaponDealsDamage(arts, 2)) {
attribute(kMentaleRegeneration, ArtStat::MagicDamage,
AttributeDamage(arts, kMentaleRegeneration, 2, 1.0, true));
}
if (WeaponDealsDamage(arts, 3)) {
attribute(kWiderstand, ArtStat::PoisonDamage, AttributeDamage(arts, kWiderstand, 3, 2.0, true));
attribute(kCharisma, ArtStat::PoisonDamage, AttributeDamage(arts, kCharisma, 3, 2.0, true));
}
// Only the skills of the weapon in hand.
for (const WeaponSkill& weapon : kWeaponSkills) {
if (weapon.applies(creature)) {
skill(weapon.skill, weapon.curve, ArtStat::AttackValue);
skill(weapon.skill, weapon.curve + kCurveSpeedOffset, ArtStat::AttackSpeed);
}
}
skill(kWendigkeit, kCurveWendigkeitValue, ArtStat::AttackValue);
}
for (const SkillLink& link : links_) {
skill(link.skill, link.curve, link.stat);
}
// Rates: +0xee = (1 + Meditation %) * (1 + Mentale Regeneration / 100),
// +0xf2 = 1 + Physische Regeneration / 100.
const bool zwerg = *reinterpret_cast<const uint32_t*>(static_cast<uint8_t*>(creature) + kCreatureType) == kTypeZwerg;
if (magicRegeneration_ && !zwerg) {
skill(kMeditation, kCurveMeditation, ArtStat::RegenerationTime); // a rate, divides the time too
rate({Improver::Attribute, kMentaleRegeneration}, AttributeValue(arts, kMentaleRegeneration));
} else {
rate({Improver::Attribute, kPhysischeRegeneration}, AttributeValue(arts, kPhysischeRegeneration));
}
return out;
}
std::wstring Art::Tooltip(void* creature, std::wstring gameText) const {
const auto filters = Filters().find(id_);
if (filters != Filters().end()) {
for (TooltipFilter filter : filters->second) {
filter(*this, creature, gameText);
}
}
std::vector<Improvement> improvements = Improvements(creature);
if (improvements.empty()) {
return gameText;
}
// One line per effect, biggest first; an improver with several effects
// appears once per effect.
std::stable_sort(improvements.begin(), improvements.end(), [](const Improvement& a, const Improvement& b) {
return std::fabs(a.percent) > std::fabs(b.percent);
});
std::wstring text = gameText + L"\n\n" + kHeaderColour + kImprovedByHeader;
for (const Improvement& i : improvements) {
// Greyed out while it changes nothing (not learned).
const long value = std::lround(i.percent);
text += L"\n";
text += value != 0 ? kTextColour : kInactiveColour;
text += ImproverName(i.by) + L" -> " + StatName(i.stat);
text += (value < 0 ? L" " : L" +") + std::to_wstring(value) + L"%";
}
return text + kTextColour;
}
const Art* FindArt(uint16_t id) {
static std::map<uint16_t, std::unique_ptr<Art>> arts;
auto it = arts.find(id);
if (it == arts.end()) {
std::unique_ptr<Art> art;
if (FindSpellDef(id) || FindCombatMoveDef(id)) {
art.reset(new Art(id));
}
it = arts.emplace(id, std::move(art)).first;
}
return it->second.get();
}
void AddTooltipFilter(uint16_t id, Art::TooltipFilter filter) {
Filters()[id].push_back(filter);
}
} // namespace game
+91
View File
@@ -0,0 +1,91 @@
#pragma once
// Combat arts as objects: name, what improves them and by how much, and their
// tooltip (docs/art-tooltip.md).
#include <cstdint>
#include <string>
#include <vector>
#include "game/engine/arts.h"
namespace game {
// The heading of the block Art::Tooltip adds; also marks a text as an art tooltip.
constexpr const wchar_t* kImprovedByHeader = L"Verbessert durch:";
// What about an art can be improved.
enum class ArtStat : uint8_t {
Damage, // all damage of a spell; the physical part of a weapon hit
FireDamage,
MagicDamage,
PoisonDamage,
AttackValue,
AttackSpeed,
ExecutionSpeed, // cast / execution speed while the art is selected (+0x3e)
RegenerationTime, // also faster regeneration rates, as the shorter time
};
// A skill or attribute that improves an art.
struct Improver {
enum Kind : uint8_t { Skill, Attribute } kind;
uint8_t id; // game::Skill / game::Attribute
bool operator==(const Improver& other) const { return kind == other.kind && id == other.id; }
};
// One stat an improver changes, in percent (regeneration time: negative); 0
// for a skill of the hero's class that it has not learned.
struct Improvement {
Improver by;
ArtStat stat;
float percent;
};
class Art {
public:
// Changes a mod makes to an art's tooltip text (the game's description and
// values), e.g. values the mod changed.
using TooltipFilter = void (*)(const Art& art, void* creature, std::wstring& text);
uint16_t Id() const { return id_; }
std::wstring Name() const;
LaunchType Launch() const { return launch_; }
bool DealsDamage() const { return damage_; }
// What improves the art for `creature`: the attributes and the skills of
// its class that act on this art, with their current effect.
std::vector<Improvement> Improvements(void* creature) const;
// The full tooltip: `gameText` (cCreatureArts::ArtTooltip's) with the
// mods' filters applied, then kImprovedByHeader with one line per effect
// of a skill or attribute, biggest first (greyed out while it changes
// nothing).
std::wstring Tooltip(void* creature, std::wstring gameText) const;
private:
friend const Art* FindArt(uint16_t id);
explicit Art(uint16_t id);
// A skill that improves this art beyond the general rules (the game's
// per-skill art lists): its curve and the stat.
struct SkillLink {
uint8_t skill;
uint32_t curve;
ArtStat stat;
};
uint16_t id_;
uint32_t nameText_ = 0;
LaunchType launch_ = LaunchType::Unknown;
bool spellValues_ = false; // damage from spell values (+0xb6 factors), else the weapon's (+0xa6)
bool magicRegeneration_ = false; // counts down at the magic rate (+0xee)
bool damage_ = false;
std::vector<SkillLink> links_;
};
// The art `id`, or null if the game has no definition for it.
const Art* FindArt(uint16_t id);
// Registers a mod's tooltip change for art `id`.
void AddTooltipFilter(uint16_t id, Art::TooltipFilter filter);
} // namespace game
+24 -3
View File
@@ -40,6 +40,22 @@ inline LaunchType ArtLaunchType(uint16_t id) {
return LaunchType::Unknown; return LaunchType::Unknown;
} }
// The spell definition an art's damage values come from: its own, else the
// spell its combat move names (0x4d1c50 on the CombatMoveDef, plain ret), as
// cCreatureArts::ArtTooltip looks it up. Null for pure combat moves.
inline const uint8_t* ArtSpellDef(uint16_t id) {
if (const uint8_t* spell = FindSpellDef(id)) {
return spell;
}
const uint8_t* move = FindCombatMoveDef(id);
if (!move) {
return nullptr;
}
using LinkFn = uint16_t(__fastcall*)(const void*, void*);
const uint16_t linked = reinterpret_cast<LinkFn>(0x004d1c50)(move, nullptr);
return linked ? FindSpellDef(linked) : nullptr;
}
// cCreature::ResolveArtId (0x5521e0, (ref, 0) as the taskbar calls it): the // cCreature::ResolveArtId (0x5521e0, (ref, 0) as the taskbar calls it): the
// art id of an art slot's reference (cInventoryEntry::GetArtSlotRef) for this // art id of an art slot's reference (cInventoryEntry::GetArtSlotRef) for this
// creature. // creature.
@@ -137,6 +153,13 @@ inline uint8_t* FindCooldownEntry(uint8_t* arts, uint16_t art) {
return begin + index * kCooldownSize; return begin + index * kCooldownSize;
} }
// Whether cooldown entry `entry` counts down at the magic rate (+0xee): magic
// group, or Energieblitze.
inline bool EntryAtMagicRate(const uint8_t* entry) {
return *reinterpret_cast<const uint32_t*>(entry + kCooldownGroup) == kRegenGroupMagic ||
*reinterpret_cast<const uint16_t*>(entry + kCooldownArt) == kArtEnergieblitze;
}
struct ArtCooldown { struct ArtCooldown {
float remaining = 0.0f; // in the game's units float remaining = 0.0f; // in the game's units
float total = 0.0f; float total = 0.0f;
@@ -148,10 +171,8 @@ inline ArtCooldown GetArtCooldown(void* creature, uint16_t art) {
if (!entry) { if (!entry) {
return {}; return {};
} }
const bool magic = *reinterpret_cast<uint32_t*>(entry + kCooldownGroup) == kRegenGroupMagic ||
*reinterpret_cast<uint16_t*>(entry + kCooldownArt) == kArtEnergieblitze;
const float perSecond = const float perSecond =
Float(entry, kCooldownSpeed) * Float(arts, magic ? kArtsRegenRateMagic : kArtsRegenRateCombat); Float(entry, kCooldownSpeed) * Float(arts, EntryAtMagicRate(entry) ? kArtsRegenRateMagic : kArtsRegenRateCombat);
const float remaining = Float(entry, kCooldownRemaining); const float remaining = Float(entry, kCooldownRemaining);
return {remaining, Float(entry, kCooldownTotal), perSecond > 0.0f ? remaining / perSecond : 0.0f}; return {remaining, Float(entry, kCooldownTotal), perSecond > 0.0f ? remaining / perSecond : 0.0f};
} }
+191
View File
@@ -0,0 +1,191 @@
#pragma once
// A creature's attributes and skills (Fertigkeiten) in cCreatureArts (creature
// +0x3a8). See docs/art-tooltip.md for what each of them improves.
#include <cstddef>
#include <cstdint>
#include <string>
#include "game/ui/texts.h"
namespace game {
// Attributes, in the character panel's order.
enum Attribute : uint8_t {
kStaerke = 0,
kWiderstand = 1,
kGeschick = 2,
kPhysischeRegeneration = 3,
kMentaleRegeneration = 4,
kCharisma = 5,
};
constexpr int kAttributeCount = 6;
// The attribute's bare name: the texts with key hashes 0x440470a.. in
// global.res ("Stärke" .. "Charisma"), looked up by hash as GetTextByKey does
// (bit 31). The UI_ATTRIB_* key texts also explain the attribute; their first
// line is the fallback.
constexpr uint32_t kAttributeNameHash = 0x0440470a;
inline std::wstring AttributeName(uint8_t attribute) {
static constexpr const char* kTextKeys[kAttributeCount] = {
"UI_ATTRIB_STRENGTH", "UI_ATTRIB_RESISTANCE", "UI_ATTRIB_GESCHICK",
"UI_ATTRIB_PHYREG", "UI_ATTRIB_MAGREG", "UI_ATTRIB_CHARISMA",
};
if (attribute >= kAttributeCount) {
return {};
}
std::wstring name = GameText(0x80000000 | (kAttributeNameHash + attribute));
if (name.empty()) {
name = GameTextByKey(kTextKeys[attribute]);
name = name.substr(0, name.find_first_of(L"\n:"));
}
return name;
}
// cCreatureArts fields. cCreatureArts::Recalculate (0x579560) sets each shown
// attribute to invested + base + base * (level - 1) / 10, then adds items.
constexpr size_t kArtsAttributes = 0x10; // u16[6]: shown values
constexpr size_t kArtsInvested = 0x1c; // u8[6]: attribute points invested
constexpr int kFreeAttributesIndex = 7; // u8 +0x23: free points
constexpr size_t kArtsSkillIds = 0x24; // u8[8]: skill id by row (0: empty)
constexpr size_t kArtsSkillLevels = 0x2c; // u8[8]: bought levels
constexpr size_t kArtsSkillBonus = 0x34; // u8[8]: levels from items
constexpr size_t kArtsSkillChoices = 0x44; // u16: new skills still to choose
constexpr size_t kArtsCreatureType = 0x8; // u32: the creature (hero class) type
constexpr size_t kArtsAttributeBase = 0x4a; // u16[6]: the class's base values
constexpr size_t kArtsLevel = 0x56; // u16: hero level
constexpr int kSkillRows = 8;
inline uint16_t AttributeValue(const uint8_t* arts, int attribute) {
return reinterpret_cast<const uint16_t*>(arts + kArtsAttributes)[attribute];
}
inline uint16_t AttributeBase(const uint8_t* arts, int attribute) {
return reinterpret_cast<const uint16_t*>(arts + kArtsAttributeBase)[attribute];
}
inline uint16_t HeroLevel(const uint8_t* arts) {
return *reinterpret_cast<const uint16_t*>(arts + kArtsLevel);
}
// The part of an attribute's shown value that comes from items.
inline int AttributeFromItems(const uint8_t* arts, int attribute) {
const int base = AttributeBase(arts, attribute);
const int grown = arts[kArtsInvested + attribute] + base + base * (HeroLevel(arts) - 1) / 10;
return AttributeValue(arts, attribute) - grown;
}
// Skill ids (cCreatureArts::ApplySkill 0x565f30 switches on them).
enum Skill : uint8_t {
kHimmelsmagie = 0x01,
kWaffenkunde = 0x02,
kStangenwaffen = 0x03,
kSchwertkunde = 0x04,
kAxtkampf = 0x05,
kZweiWaffenKampf = 0x06,
kFernkampf = 0x07,
kWendigkeit = 0x08,
kMeditation = 0x0c,
kKlingenkampf = 0x0d,
kMagiekunde = 0x0e,
kFeuermagie = 0x0f,
kWassermagie = 0x10,
kErdmagie = 0x11,
kLuftmagie = 0x12,
kMondmagie = 0x13,
kVampirismus = 0x14,
kFaustkampf = 0x18,
kKonzentration = 0x19,
kBallistik = 0x1a,
kFallenkunst = 0x1b,
kBlutrausch = 0x1c,
kWaffentechnik = 0x1d,
kZwergenkunde = 0x1f,
kHoellenmacht = 0x20,
};
// The level of skill `skill` (bought + from items, as ApplySkill uses it), 0
// if the creature has not learned it.
inline unsigned SkillLevel(const uint8_t* arts, uint8_t skill) {
for (int row = 0; row < kSkillRows; ++row) {
if (arts[kArtsSkillIds + row] == skill) {
return arts[kArtsSkillLevels + row] + arts[kArtsSkillBonus + row];
}
}
return 0;
}
// The name the character panel shows (game text 0x24b7 + id, 0x6abc8d).
inline std::wstring SkillName(uint8_t skill) {
return GameText(0x24b7 + skill);
}
// The skills of the creature's class: 16 ids at +0x16 of its type record
// (0x43ac50 on the type table *0xaa9564, (type), ret 4; read by
// cCreatureArts::ChoosableSkills 0x580f30). Null without a record.
constexpr int kClassSkills = 16;
inline const uint8_t* ClassSkills(const uint8_t* arts) {
using Fn = const uint8_t*(__fastcall*)(void*, void*, uint32_t);
void* table = *reinterpret_cast<void**>(0x00aa9564);
const uint8_t* record =
table ? reinterpret_cast<Fn>(0x0043ac50)(table, nullptr, *reinterpret_cast<const uint32_t*>(arts + kArtsCreatureType))
: nullptr;
return record ? record + 0x16 : nullptr;
}
// A skill curve of the balance settings (globals parsed from "key=value"
// around 0x5eb4xx: "<k>off" start, "<k>_s" half, "<k>_w" limit): 0 below level
// 1, else start + 2 (limit - start) (1 - 1 / ((level - 1) / half + 1)), in
// percent (DiminishingCurve 0x565c30).
struct SkillCurve {
float start;
float half;
float limit;
};
inline float CurveValue(uint32_t curveAddress, unsigned level) {
const auto& curve = *reinterpret_cast<const SkillCurve*>(curveAddress);
if (level < 1) {
return 0.0f;
}
const float t = 1.0f - 1.0f / ((static_cast<float>(level) - 1.0f) / curve.half + 1.0f);
return 2.0f * (curve.limit - curve.start) * t + curve.start;
}
constexpr uint32_t kCurveWaffenkunde = 0x00ad2450; // WK
constexpr uint32_t kCurveMagiekunde = 0x00ad245c; // MK
constexpr uint32_t kCurveFaustkampfValue = 0x00ad2468; // FK..AW, then ..SP
constexpr uint32_t kCurveSchwertkundeValue = 0x00ad2480; // SK
constexpr uint32_t kCurveAxtkampfValue = 0x00ad2498; // AK
constexpr uint32_t kCurveKlingenkampfValue = 0x00ad24b0; // KK
constexpr uint32_t kCurveStangenwaffenValue = 0x00ad24c8; // STK
constexpr uint32_t kCurveZweiWaffenValue = 0x00ad24e0; // BK
constexpr uint32_t kCurveFernkampfValue = 0x00ad24f8; // FEK
constexpr uint32_t kCurveExecutionSpeed = 0x00ad2504; // FEK..SP: also the magic schools' speed bonus
constexpr uint32_t kCurveHimmelsmagie = 0x00ad2510; // HM
constexpr uint32_t kCurveFeuermagie = 0x00ad251c; // FM
constexpr uint32_t kCurveErdmagie = 0x00ad2528; // EM
constexpr uint32_t kCurveWassermagie = 0x00ad2534; // WM
constexpr uint32_t kCurveLuftmagie = 0x00ad2540; // LM
constexpr uint32_t kCurveMondmagie = 0x00ad254c; // MM
constexpr uint32_t kCurveBallistik = 0x00ad2558; // BA
constexpr uint32_t kCurveBlutrausch = 0x00ad2564; // BL
constexpr uint32_t kCurveKonzentration = 0x00ad2570; // KO
constexpr uint32_t kCurveMeditation = 0x00ad257c; // Med
constexpr uint32_t kCurveWendigkeitValue = 0x00ad2588; // W..AW
constexpr uint32_t kCurveHoellenmacht = 0x00ad368c; // HOM
constexpr uint32_t kCurveWaffentechnik = 0x00ad3698; // WT
// Weapon skills have two curves in a row: attack value (AW), then speed (SP).
constexpr uint32_t kCurveSpeedOffset = 0xc;
// Balance settings for the attributes' share of the damage factors
// (Recalculate): BalStatOff and the divisors BalCharPD / FD / MD / GD.
constexpr uint32_t kBalStatOff = 0x00ad242c;
constexpr uint32_t kBalCharDamage[4] = {0x00ad2430, 0x00ad2434, 0x00ad2438, 0x00ad243c};
inline float BalanceValue(uint32_t address) {
return *reinterpret_cast<const float*>(address);
}
// cCreature::HoldsRangedWeapon (0x54a110, plain ret): Recalculate takes
// Geschick instead of Stärke for the physical damage then.
inline bool HoldsRangedWeapon(void* creature) {
using Fn = bool(__fastcall*)(void*, void*);
return reinterpret_cast<Fn>(0x0054a110)(creature, nullptr);
}
} // namespace game
+23
View File
@@ -126,6 +126,29 @@ std::wstring GameText(uint32_t id) {
: std::wstring(); : std::wstring();
} }
std::wstring GameTextByKey(const char* key) {
// The key's hash (0x80e010, cdecl), looked up "by key" as GetTextByKey does.
using HashFn = uint32_t(__cdecl*)(const char*);
const uint32_t hash = reinterpret_cast<HashFn>(0x0080e010)(key);
return hash ? GameText(hash | 0x80000000) : std::wstring();
}
std::wstring WideStringText(const void* string) {
const auto* s = static_cast<const GameString*>(string);
return s && s->begin && s->end > s->begin ? std::wstring(s->begin, s->end) : std::wstring();
}
void SetWideString(void* string, const std::wstring& value) {
auto* s = static_cast<GameString*>(string);
if (s->begin) {
s->end = s->begin;
*const_cast<wchar_t*>(s->begin) = L'\0';
}
// The game's wide string append (0x592ef0, thiscall (first, last, unused), ret 0xc).
using AppendFn = void*(__fastcall*)(void*, void*, const wchar_t*, const wchar_t*, uint32_t);
reinterpret_cast<AppendFn>(0x00592ef0)(string, nullptr, value.data(), value.data() + value.size(), 0);
}
const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id) { const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id) {
if (id >= kFirstOwnText && id < 0x80000000) { if (id >= kFirstOwnText && id < 0x80000000) {
std::lock_guard guard(g_lock); std::lock_guard guard(g_lock);
+9
View File
@@ -23,4 +23,13 @@ void ReplaceInTexts(const std::wstring& from, const std::wstring& to);
// The game's text `id` without our changes (empty if there is none). // The game's text `id` without our changes (empty if there is none).
std::wstring GameText(uint32_t id); std::wstring GameText(uint32_t id);
// The game's text under key `key` (TextManager::GetTextByKey 0x672fd0), e.g.
// "UI_ATTRIB_STRENGTH"; empty if there is none.
std::wstring GameTextByKey(const char* key);
// A game wide string ({begin, end, capacity end}, e.g. a tooltip's text) as
// text, and replaced by `value` (grown with the game's allocator).
std::wstring WideStringText(const void* string);
void SetWideString(void* string, const std::wstring& value);
} // namespace game } // namespace game
+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 <windows.h>
#include "game/engine/art.h"
#include "game/engine/arts.h" #include "game/engine/arts.h"
#include "game/engine/fx.h" #include "game/engine/fx.h"
#include "game/engine/objects.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 // 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 // two numbers, hits per chain and chains, in both the current level's section
// and the next level's (skill book). cCreatureArts::ArtTooltip (0x589270, // and the next level's (skill book). cCreatureArts::ArtTooltip (0x589270,
// thiscall on creature +0x3a8, (art, text, flag), ret 0xc) builds the text // mods/arttooltip.cpp, game::Art::Tooltip) builds the text; sections start with the level
// into a wide string {begin, end, capacity}; sections start with the level
// line, values are "Name: value" lines separated by '\n' (measured for art // line, values are "Name: value" lines separated by '\n' (measured for art
// 0x21 at level 5: "...|Aktuelle Stufe: 5|Regeneration: 3.5s|Schaden: ...| // 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). // 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* kNextLevelLine = L"Nächste erlernte Stufe:";
constexpr const wchar_t* kCountLine = L"Anzahl:"; 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 Rewrite(const std::wstring& text, unsigned currentLevel) {
std::wstring out; std::wstring out;
unsigned level = currentLevel; unsigned level = currentLevel;
@@ -431,24 +415,13 @@ std::wstring Rewrite(const std::wstring& text, unsigned currentLevel) {
return out; return out;
} }
void __fastcall Arts_Tooltip(void* self, void* edx, uint32_t art, void* text, uint32_t flag); void RewriteTooltip(const game::Art& art, void* creature, std::wstring& text) {
RE_HOOK(0x00589270, Arts_Tooltip); text = Rewrite(text, ArtLevel(creature, art.Id()));
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);
}
} }
struct TooltipRegistration {
TooltipRegistration() { game::AddTooltipFilter(kArtBlitzschlag, &RewriteTooltip); }
} g_tooltipRegistration;
} // namespace } // namespace
} // namespace mods::chainlightning } // namespace mods::chainlightning
+9 -8
View File
@@ -29,6 +29,7 @@
#include "game/engine/arts.h" #include "game/engine/arts.h"
#include "game/engine/objects.h" #include "game/engine/objects.h"
#include "game/engine/skills.h"
#include "game/ui/events.h" #include "game/ui/events.h"
#include "game/ui/text.h" #include "game/ui/text.h"
#include "mods/hud.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 kMinusButtons = 0x8ac; // unused by the game
constexpr size_t kButtonSize = 0xbc; constexpr size_t kButtonSize = 0xbc;
constexpr int kRows = 8; constexpr int kRows = 8;
constexpr int kAttributeRows = 6; constexpr int kAttributeRows = game::kAttributeCount;
// cUI_Button (vtable 0x895308). // cUI_Button (vtable 0x895308).
constexpr size_t kControlFlags = 0x10; 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). // The − sits left of the value column (values end at the + at x 232).
constexpr int kMinusX = 184; constexpr int kMinusX = 184;
// cCreatureArts fields (see the top). // cCreatureArts fields (see the top, game/engine/skills.h).
constexpr size_t kAttributes = 0x1c; using game::kFreeAttributesIndex;
constexpr int kFreeAttributesIndex = 7; constexpr size_t kAttributes = game::kArtsInvested;
constexpr size_t kSkillIds = 0x24; constexpr size_t kSkillIds = game::kArtsSkillIds;
constexpr size_t kSkillLevels = 0x2c; constexpr size_t kSkillLevels = game::kArtsSkillLevels;
constexpr size_t kSkillBonus = 0x34; constexpr size_t kSkillBonus = game::kArtsSkillBonus;
constexpr size_t kSkillChoices = 0x44; constexpr size_t kSkillChoices = game::kArtsSkillChoices;
using AddAttributeFn = void(__fastcall*)(void* arts, void*, int index, int delta); using AddAttributeFn = void(__fastcall*)(void* arts, void*, int index, int delta);
using AddSkillLevelsFn = void(__fastcall*)(void* arts, void*, int row, int delta, int spendPoints); using AddSkillLevelsFn = void(__fastcall*)(void* arts, void*, int row, int delta, int spendPoints);
+9
View File
@@ -292,3 +292,12 @@
0x00605900 ItemDataMgr::loadHero 0x00605900 ItemDataMgr::loadHero
0x00672fd0 TextManager::GetTextByKey # (key string): id = hash 0x80e010 0x00672fd0 TextManager::GetTextByKey # (key string): id = hash 0x80e010
0x0080e010 TextKeyHash # h = (h*113 + toupper(c)) % 999999991, & 0x7fffffff 0x0080e010 TextKeyHash # h = (h*113 + toupper(c)) % 999999991, & 0x7fffffff
0x00565f30 cCreatureArts::ApplySkill # (skill, level, describe out, ...), ret 0x18: skill effects, or their text
0x00565c80 cCreatureArts::ApplySkillToArts # (arts[], start, level, half, limit, attack, out, nameText), ret 0x20
0x0054a110 cCreature::HoldsRangedWeapon # Geschick instead of Stärke for physical damage
0x004d1c50 CombatMoveDef::LinkedSpell # spell id whose values the combat move uses, 0 if none
0x0043ac50 TypeTable::CreatureRecord # (type), ret 4: class record (+0x16: 16 skill ids)
0x006e6630 cUI_Tooltip::FormatText # (char), ret 4: copies +0x115c to +0x2168, wraps at 45 chars with flag 0x80
0x006e6ae0 cUI_Tooltip::SetText # (textId, text*, fixedWidth), ret 0xc
0x006e7730 cUI_Tooltip::Layout # measures the text, width = longest line + 8 unless flag 0x100
0x0075ac70 cUI_Manager::ShowTooltip # (x, y, text, ?, flags, show)