feat(ui): spellcasting cast-button + character-panel attribute/skill tooltips — gmSpellcastingUI::UpdateCastButtonTooltip @0x004C6A30, AttributeInfoRegion/Attribute2ndInfoRegion/SkillInfoRegion @0x004F1530/0x004F1680/0x004F2140

TS-85 remainder batch (hover/UI overnight round, batch B). Audit found
three of the four listed spellcasting SetTooltip sites (endowment icon,
favorite, submenu) were already correct via UiCatalogSlot's pre-existing
Label-driven GetTooltipText; only the cast button (UiButton, no tooltip
wiring at all) was a real gap. Ports the verified literal states
("Select a spell to cast" / "You have no spells ready to cast" / the
full endowment-item USE-the-%s branch) plus a documented, narrower
fallback (spell name only) for the one sub-branch whose exact wording
sits behind a genuine gmNoticeHandler vtable-slot collision in the
pseudo-C dump rather than the unlabeled-string-pool class the rest of
this batch recovered.

Character panel: new UiClickablePanel.TooltipText seam (same pattern as
UiButton.TooltipText) carries the six hardcoded attribute descriptions
and three pair-shared vitals descriptions (byte-decoded from the retail
string pool) plus skill tooltips composed from the already-DAT-parsed
SkillBase.Description/.Formula — no hand-transcription needed for the
~30+ skill strings. The formula-to-text algorithm itself
(SkillSystem::InqSkillFormula) was recovered by byte-decoding six short
fragments Binary Ninja left completely unlabeled between two
gmSpellcastingUI vtable declarations.

Live-verified against the local ACE server: 34 real skills' composed
tooltips and both reachable cast-button states captured via a temporary
probe (stripped before this commit). Full solution suite green
(14,647 tests, 0 failures) both before and after the probe strip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-17 01:11:22 +02:00
parent c623b57ad3
commit 39c49e140e
8 changed files with 269 additions and 9 deletions

View file

@ -85,6 +85,114 @@ internal static class RetailSkillFormula
_ => result,
};
}
/// <summary>
/// Retail <c>SkillSystem::InqAttributeName @ 0x005c8d90</c> — the six
/// hardcoded attribute display names (matched exactly against
/// <c>DatReaderWriter.Enums.AttributeId</c>'s Strength=1..Self=6
/// numbering, the same table <see cref="ResolveAttribute"/>-style
/// switches elsewhere in this file already assume).
/// </summary>
public static string AttributeName(DatReaderWriter.Enums.AttributeId attribute) => attribute switch
{
DatReaderWriter.Enums.AttributeId.Strength => "Strength",
DatReaderWriter.Enums.AttributeId.Endurance => "Endurance",
DatReaderWriter.Enums.AttributeId.Quickness => "Quickness",
DatReaderWriter.Enums.AttributeId.Coordination => "Coordination",
DatReaderWriter.Enums.AttributeId.Focus => "Focus",
DatReaderWriter.Enums.AttributeId.Self => "Self",
_ => string.Empty,
};
/// <summary>
/// Retail <c>SkillSystem::InqSkillFormula @ 0x005c89b0</c> — builds the
/// human-readable formula line shown in a skill's tooltip, e.g.
/// <c>"( (Strength + Coordination) / 2 )"</c>, <c>"( Quickness )"</c>, or
/// <c>"( (2 x Quickness) )"</c>. Ported byte-for-byte from the retail
/// binary's string pool: the five short literal fragments below
/// (<c>data_7e7930</c> = <c>" )"</c>, <c>data_7e7934</c> = <c>"+%u"</c>,
/// <c>data_7e7940</c> = <c>" + "</c>, <c>data_7e7950</c> = <c>"("</c>,
/// <c>data_7e7954</c> = <c>"( "</c>, <c>data_797584</c> = <c>")"</c>)
/// sit between two vtable declarations in the pseudo-C dump and Binary
/// Ninja's type inference never recognized them as strings, so they show
/// up unlabeled rather than as readable literals — this port decoded
/// their raw bytes directly as narrow ASCII (the function operates
/// exclusively on <c>AC1Legacy::PStringBase&lt;char&gt;</c>, so 1
/// byte/char, not the 2-byte/char wide encoding used elsewhere in this
/// file's neighborhood). <c>" / %u"</c> (divisor) and <c>"(%u x %s)"</c>
/// (multiplier wrap) are plain, directly-visible literals in the same
/// function and needed no such recovery. Returns null when the skill has
/// neither attribute wired (<c>_x &lt; 1 || _attr1 == 0</c> AND the
/// attr2 equivalent), matching <c>InqSkillFormula</c>'s own false
/// return.
/// </summary>
public static string? FormatFormula(SkillFormula formula)
{
ArgumentNullException.ThrowIfNull(formula);
bool hasAttr1 = formula.Attribute1Multiplier >= 1
&& formula.Attribute1 != 0;
bool hasAttr2 = formula.Attribute2Multiplier >= 1
&& formula.Attribute2 != 0;
if (!hasAttr1 && !hasAttr2)
return null;
var text = new System.Text.StringBuilder("( ");
if (hasAttr1 && hasAttr2)
text.Append('(');
if (hasAttr1)
{
string name1 = AttributeName(formula.Attribute1);
text.Append(formula.Attribute1Multiplier <= 1
? name1
: $"({formula.Attribute1Multiplier} x {name1})");
if (hasAttr2)
text.Append(" + ");
}
if (hasAttr2)
{
string name2 = AttributeName(formula.Attribute2);
text.Append(formula.Attribute2Multiplier <= 1
? name2
: $"({formula.Attribute2Multiplier} x {name2})");
}
if (hasAttr1 && hasAttr2)
text.Append(')');
if (formula.Divisor != 1)
text.Append($" / {formula.Divisor}");
if (formula.AdditiveBonus != 0)
text.Append($"+{formula.AdditiveBonus}");
text.Append(" )");
return text.ToString();
}
/// <summary>
/// Retail <c>SkillInfoRegion::GetTooltip @ 0x004f1fe0</c>, called once
/// from <c>SkillInfoRegion::SkillInfoRegion @ 0x004f2140</c>'s
/// <c>UIElement::SetTooltip</c> at 0x004f222f. Composition is exactly
/// <c>"\n" + formula + description</c> — retail concatenates the
/// description directly onto the formula line with NO separator between
/// them (ported verbatim, not "fixed": <c>append_n_chars</c> runs
/// immediately after the formula assignment with no intervening
/// literal). <c>SkillSystem::InqSkillDescription @ 0x005c8770</c> reads
/// <c>SkillBase._description</c> — the same DAT field
/// <see cref="DatReaderWriter.Types.SkillBase.Description"/> already
/// exposes, so no hand-transcription was needed for the ~30+ skill
/// description strings (unlike the six hardcoded attribute
/// descriptions).
/// </summary>
public static string? BuildTooltip(SkillBase skillBase)
{
ArgumentNullException.ThrowIfNull(skillBase);
string? formula = FormatFormula(skillBase.Formula);
string description = skillBase.Description.Value ?? string.Empty;
string tooltip = (formula is null ? string.Empty : "\n" + formula) + description;
return tooltip.Length == 0 ? null : tooltip;
}
}
/// <summary>