fix(ui): night-round review — F3/F4/F7 cast-button tooltip strings
F3: TS-85 had claimed the plain-spell branch's three SetTooltip format strings were "genuine gmNoticeHandler vtable SLOTS" and unrecoverable from the decomp dump. That was itself the artifact — Binary Ninja's pseudo-C rendering of PStringBase::sprintf's second argument as "&gmSpellcastingUI::`vftable'.RecvNotice_XXX" was a spurious symbol match, not the true operand. A direct capstone disassembly of the raw bytes at gmSpellcastingUI::UpdateCastButtonTooltip @0x004c6a30's four call sites (0x4c6e48/0x4c6ea4/0x4c6f18/0x4c6f5d) resolves the actual pushed literals: "CAST %hs" @0x7b63a4 (untargeted/self-cast, and targeted+compatible with " on %s" @0x7b6464 appended), "You must select an appropriate target for %hs" @0x7b6348 (incompatible target), "You must select a target for %hs" @0x7b63b8 (no target). %hs is the spell's own name throughout. Added RuntimeSpellCastState.EvaluateCastGate (SpellCastGate: NoTarget- Needed/TargetCompatible/TargetIncompatible/NoTargetSelected/Unknown), refactoring IsTargetReady to use it, and wired SpellcastingUiController.ComputeSpellCastState to the four-state tooltip text, replacing the bare-spell-name fallback. F4: the endowment branch's "USE the %s" (and both select-target strings) vararg is NOT the bare item name — retail composes "%s (%hs)" @0x7b64d8 (item name, spell name) once at @0x004c6bb6-ef and reuses it for all three format strings, byte-confirmed by all three sprintf call sites (0x4c6c7f/0x4c6ca4/0x4c6d46) reading the identical stack slot. Added ComposeEndowmentName and wired it in place of the bare item name. F7: added test coverage for the two genuinely NEW disabled states (needs-target, needs-appropriate-target) neither branch had any coverage for before, plus the enabled untargeted/targeted-compatible states and both endowment-branch composed-name cases. Corrected the register's TS-85 row (the "cannot be recovered" claim and the endowment operand claim) with the byte-decoded findings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
353ae3bb0c
commit
4a24614fd1
4 changed files with 344 additions and 30 deletions
|
|
@ -50,18 +50,34 @@ public sealed class RuntimeSpellCastState
|
|||
public uint? LastRequestedTargetId { get; private set; }
|
||||
public event Action? StateChanged;
|
||||
|
||||
public bool IsTargetReady(uint spellId)
|
||||
public bool IsTargetReady(uint spellId) =>
|
||||
EvaluateCastGate(spellId)
|
||||
is SpellCastGate.NoTargetNeeded or SpellCastGate.TargetCompatible;
|
||||
|
||||
/// <summary>
|
||||
/// <c>gmSpellcastingUI::UpdateCastButtonTooltip @0x004c6a30</c>'s
|
||||
/// plain-spell branch (<c>m_endowmentItemID == 0</c>) gate, split out
|
||||
/// (night-round review F3) so the tooltip presenter can distinguish
|
||||
/// retail's four states rather than just the enabled/disabled boolean
|
||||
/// <see cref="IsTargetReady"/> collapses them to. Byte-decoded call
|
||||
/// sites: the untargeted/self-cast branch at <c>0x4c6f35</c>, the
|
||||
/// targeted-and-compatible branch at <c>0x4c6e57</c>
|
||||
/// (<c>ClientMagicSystem::ObjectCompatibleWithSpell @0x567c30</c>), and
|
||||
/// the two disabled tails at <c>0x4c6f04</c> (incompatible) and
|
||||
/// <c>0x4c6e2b</c> (nothing selected).
|
||||
/// </summary>
|
||||
public SpellCastGate EvaluateCastGate(uint spellId)
|
||||
{
|
||||
if (!_spellbook.Knows(spellId)
|
||||
|| !_spellbook.TryGetMetadata(spellId, out SpellMetadata spell))
|
||||
return false;
|
||||
return SpellCastGate.Unknown;
|
||||
if (spell.IsSelfTargeted || spell.IsUntargeted || spell.TargetMask == 0u)
|
||||
return true;
|
||||
return _selection.SelectedObjectId is uint target and not 0u
|
||||
&& _operations.IsTargetCompatible(
|
||||
target,
|
||||
spell,
|
||||
showMessage: false);
|
||||
return SpellCastGate.NoTargetNeeded;
|
||||
if (_selection.SelectedObjectId is not (uint target and not 0u))
|
||||
return SpellCastGate.NoTargetSelected;
|
||||
return _operations.IsTargetCompatible(target, spell, showMessage: false)
|
||||
? SpellCastGate.TargetCompatible
|
||||
: SpellCastGate.TargetIncompatible;
|
||||
}
|
||||
|
||||
public CastRequestResult Cast(uint spellId)
|
||||
|
|
@ -159,3 +175,27 @@ public enum CastRequestResult
|
|||
MissingComponents,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The four retail cast-button states <c>gmSpellcastingUI::
|
||||
/// UpdateCastButtonTooltip @0x004c6a30</c>'s plain-spell branch
|
||||
/// distinguishes — see <see cref="RuntimeSpellCastState.EvaluateCastGate"/>.
|
||||
/// </summary>
|
||||
public enum SpellCastGate
|
||||
{
|
||||
/// <summary>Spell metadata is missing / not known.</summary>
|
||||
Unknown,
|
||||
/// <summary>Untargeted, self-targeted, or no target mask — always
|
||||
/// castable. Retail: <c>"CAST %hs"</c> @0x7b63a4.</summary>
|
||||
NoTargetNeeded,
|
||||
/// <summary>A target is selected and compatible. Retail: <c>"CAST
|
||||
/// %hs"</c> @0x7b63a4, then <c>" on %s"</c> @0x7b6464 appended with
|
||||
/// the target's name.</summary>
|
||||
TargetCompatible,
|
||||
/// <summary>A target is selected but incompatible. Retail: <c>"You
|
||||
/// must select an appropriate target for %hs"</c> @0x7b6348.</summary>
|
||||
TargetIncompatible,
|
||||
/// <summary>No target is selected. Retail: <c>"You must select a
|
||||
/// target for %hs"</c> @0x7b63b8.</summary>
|
||||
NoTargetSelected,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue