From c875d7d6b98432d21a4e5ecee871642492926cf8 Mon Sep 17 00:00:00 2001 From: Torhan Bartel Date: Sun, 18 Oct 2026 23:04:22 +0200 Subject: [PATCH] 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) --- CMakeLists.txt | 2 + docs/art-tooltip.md | 118 +++++++++++++ src/game/engine/art.cpp | 339 ++++++++++++++++++++++++++++++++++++ src/game/engine/art.h | 91 ++++++++++ src/game/engine/arts.h | 27 ++- src/game/engine/skills.h | 191 ++++++++++++++++++++ src/game/ui/texts.cpp | 23 +++ src/game/ui/texts.h | 9 + src/mods/arttooltip.cpp | 110 ++++++++++++ src/mods/chainlightning.cpp | 43 +---- src/mods/respec.cpp | 17 +- tools/names.txt | 9 + 12 files changed, 933 insertions(+), 46 deletions(-) create mode 100644 docs/art-tooltip.md create mode 100644 src/game/engine/art.cpp create mode 100644 src/game/engine/art.h create mode 100644 src/game/engine/skills.h create mode 100644 src/mods/arttooltip.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e4f961d..7437aa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,7 @@ add_library(sacred_re SHARED src/game/engine/items.cpp src/game/engine/stream.cpp src/game/ui/texts.cpp + src/game/engine/art.cpp src/mods/borderless.cpp src/mods/widescreen.cpp src/mods/hud.cpp @@ -58,6 +59,7 @@ add_library(sacred_re SHARED src/mods/combos.cpp src/mods/aurahits.cpp src/mods/chainlightning.cpp + src/mods/arttooltip.cpp src/mods/escape.cpp src/mods/recordcache.cpp src/mods/megamap.cpp diff --git a/docs/art-tooltip.md b/docs/art-tooltip.md new file mode 100644 index 0000000..54370ca --- /dev/null +++ b/docs/art-tooltip.md @@ -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 `off`, half `_s`, limit `_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. diff --git a/src/game/engine/art.cpp b/src/game/engine/art.cpp new file mode 100644 index 0000000..14b441c --- /dev/null +++ b/src/game/engine/art.cpp @@ -0,0 +1,339 @@ +#include "game/engine/art.h" + +#include +#include +#include +#include + +#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 stats; + std::vector arts; +}; +const std::vector& SkillArtLists() { + using S = ArtStat; + auto school = [](uint32_t curve) { + return std::vector{{S::RegenerationTime, curve}, {S::ExecutionSpeed, kCurveExecutionSpeed}}; + }; + static const std::vector 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 +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(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(static_cast(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(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(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>& Filters() { + static std::map> 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(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 Art::Improvements(void* creature) const { + auto* arts = static_cast(creature) + kCreatureArts; + const uint8_t* classSkills = ClassSkills(arts); + std::vector out; + auto add = [&](Improver by, ArtStat stat, double percent) { + out.push_back({by, stat, static_cast(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(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(static_cast(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 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> arts; + auto it = arts.find(id); + if (it == arts.end()) { + std::unique_ptr 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 diff --git a/src/game/engine/art.h b/src/game/engine/art.h new file mode 100644 index 0000000..43f576d --- /dev/null +++ b/src/game/engine/art.h @@ -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 +#include +#include + +#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 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 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 diff --git a/src/game/engine/arts.h b/src/game/engine/arts.h index 8cad1e4..1531290 100644 --- a/src/game/engine/arts.h +++ b/src/game/engine/arts.h @@ -40,6 +40,22 @@ inline LaunchType ArtLaunchType(uint16_t id) { 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(0x004d1c50)(move, nullptr); + return linked ? FindSpellDef(linked) : nullptr; +} + // cCreature::ResolveArtId (0x5521e0, (ref, 0) as the taskbar calls it): the // art id of an art slot's reference (cInventoryEntry::GetArtSlotRef) for this // creature. @@ -137,6 +153,13 @@ inline uint8_t* FindCooldownEntry(uint8_t* arts, uint16_t art) { 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(entry + kCooldownGroup) == kRegenGroupMagic || + *reinterpret_cast(entry + kCooldownArt) == kArtEnergieblitze; +} + struct ArtCooldown { float remaining = 0.0f; // in the game's units float total = 0.0f; @@ -148,10 +171,8 @@ inline ArtCooldown GetArtCooldown(void* creature, uint16_t art) { if (!entry) { return {}; } - const bool magic = *reinterpret_cast(entry + kCooldownGroup) == kRegenGroupMagic || - *reinterpret_cast(entry + kCooldownArt) == kArtEnergieblitze; 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); return {remaining, Float(entry, kCooldownTotal), perSecond > 0.0f ? remaining / perSecond : 0.0f}; } diff --git a/src/game/engine/skills.h b/src/game/engine/skills.h new file mode 100644 index 0000000..a52af21 --- /dev/null +++ b/src/game/engine/skills.h @@ -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 +#include +#include + +#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(arts + kArtsAttributes)[attribute]; +} +inline uint16_t AttributeBase(const uint8_t* arts, int attribute) { + return reinterpret_cast(arts + kArtsAttributeBase)[attribute]; +} +inline uint16_t HeroLevel(const uint8_t* arts) { + return *reinterpret_cast(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(0x00aa9564); + const uint8_t* record = + table ? reinterpret_cast(0x0043ac50)(table, nullptr, *reinterpret_cast(arts + kArtsCreatureType)) + : nullptr; + return record ? record + 0x16 : nullptr; +} + +// A skill curve of the balance settings (globals parsed from "key=value" +// around 0x5eb4xx: "off" start, "_s" half, "_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(curveAddress); + if (level < 1) { + return 0.0f; + } + const float t = 1.0f - 1.0f / ((static_cast(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(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(0x0054a110)(creature, nullptr); +} + +} // namespace game diff --git a/src/game/ui/texts.cpp b/src/game/ui/texts.cpp index e1c5e4a..7d7f83a 100644 --- a/src/game/ui/texts.cpp +++ b/src/game/ui/texts.cpp @@ -126,6 +126,29 @@ std::wstring GameText(uint32_t id) { : 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(0x0080e010)(key); + return hash ? GameText(hash | 0x80000000) : std::wstring(); +} + +std::wstring WideStringText(const void* string) { + const auto* s = static_cast(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(string); + if (s->begin) { + s->end = s->begin; + *const_cast(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(0x00592ef0)(string, nullptr, value.data(), value.data() + value.size(), 0); +} + const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id) { if (id >= kFirstOwnText && id < 0x80000000) { std::lock_guard guard(g_lock); diff --git a/src/game/ui/texts.h b/src/game/ui/texts.h index d0c5277..b11dac4 100644 --- a/src/game/ui/texts.h +++ b/src/game/ui/texts.h @@ -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). 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 diff --git a/src/mods/arttooltip.cpp b/src/mods/arttooltip.cpp new file mode 100644 index 0000000..6770204 --- /dev/null +++ b/src/mods/arttooltip.cpp @@ -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 +#include +#include +#include + +#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(art)); + if (gameText.empty() || !object) { + return; + } + void* creature = static_cast(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(static_cast(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(reinterpret_cast(&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 diff --git a/src/mods/chainlightning.cpp b/src/mods/chainlightning.cpp index 0fdc6a1..5425037 100644 --- a/src/mods/chainlightning.cpp +++ b/src/mods/chainlightning.cpp @@ -34,6 +34,7 @@ #include +#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(0x00592ef0)(text, nullptr, first, last, 0); -} - -void SetText(void* text, const std::wstring& value) { - auto** string = static_cast(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(art) != kArtBlitzschlag) { - return; - } - auto* const* string = static_cast(text); - if (!string[0] || string[1] <= string[0]) { - return; - } - void* creature = static_cast(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 diff --git a/src/mods/respec.cpp b/src/mods/respec.cpp index 36f8c4d..3fe9014 100644 --- a/src/mods/respec.cpp +++ b/src/mods/respec.cpp @@ -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); diff --git a/tools/names.txt b/tools/names.txt index 39a7143..758dc4f 100644 --- a/tools/names.txt +++ b/tools/names.txt @@ -292,3 +292,12 @@ 0x00605900 ItemDataMgr::loadHero 0x00672fd0 TextManager::GetTextByKey # (key string): id = hash 0x80e010 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)