fix(ui): night-round review — F11/F13/F14/F15 one-liners
F11: logs when a Map tab town-marker's template resolves to something other than a UiButton — that path previously silently skipped the TooltipText write with no diagnostic, leaving a mounted-but-empty tooltip popup indistinguishable from "no template configured". F13: RetailSkillFormula.FormatFormula now reads Attribute1Multiplier/ Attribute2Multiplier/AdditiveBonus/Divisor through the SAME unsigned reinterpretation TryCalculate already uses (this class's own doc comment already stated the invariant; FormatFormula just didn't follow it). A high-bit-set value would previously both mis-gate hasAttr1/ hasAttr2 and print a negative number, out of sync with what TryCalculate actually computes with for the same formula. Added regression tests, empirically verified to fail without the fix. F14: documented the RefreshHouseMarker gap rather than guessing at the byte-decode — Position::get_outside_cell_id @0x004527b0 is itself BN-mangled (its `(eax_2 - eax_2) & objcell_id` return is the same decompiler-obscures-a-real-conditional artifact class this round hit elsewhere) and depends on LandDefs::adjust_to_outside, a genuinely larger port than this round's other findings. HousePosition is wired () => null in production today (ISSUES #413's remaining scope), so this method is currently unreachable; left a TODO citing the retail call chain for whenever that lands. F15: fixed RefreshCoordinatesAndPlayerMarker's gate to AND-on-both- present, matching gmMapUI::Update @0x004a2078's exact `if (m_pCoordinateText != 0 && m_pPlayerLocationIcon != 0)` condition. The prior `_coordinateText is null && _playerIcon is null` check only skipped when BOTH were absent (proceeding whenever EITHER was present), letting coordinate text and the player marker update independently instead of as the single gated unit retail treats them as. Added a regression test (player-icon template resolution failure must also skip the coordinate-text write), empirically verified to fail without the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c403f57815
commit
0b0c7aa485
4 changed files with 172 additions and 18 deletions
|
|
@ -74,6 +74,46 @@ public sealed class RetailSkillFormulaTests
|
|||
Assert.Equal(uint.MaxValue, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F13 (night-round review): <see cref="RetailSkillFormula.FormatFormula"/>
|
||||
/// previously read the raw signed <c>int</c> fields directly instead of
|
||||
/// the SAME unsigned reinterpretation <see cref="RetailSkillFormula.TryCalculate"/>
|
||||
/// uses. A multiplier whose stored bit pattern has the high bit set
|
||||
/// would read as a small negative number here (failing the
|
||||
/// <c>>= 1</c> hasAttr gate, or printing a negative multiplier) instead
|
||||
/// of the huge unsigned value <c>TryCalculate</c> actually computes
|
||||
/// with for the SAME formula.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FormatFormula_MultiplierIsReinterpretedAsRetailUnsignedWord()
|
||||
{
|
||||
SkillFormula formula = Formula(w: 0, x: -1, y: 0, z: 1);
|
||||
formula.Attribute1 = AttributeId.Strength;
|
||||
|
||||
string? text = RetailSkillFormula.FormatFormula(formula);
|
||||
|
||||
Assert.NotNull(text);
|
||||
Assert.Contains(uint.MaxValue.ToString(), text);
|
||||
Assert.DoesNotContain("-1", text);
|
||||
}
|
||||
|
||||
/// <summary>Same reinterpretation, but for the divisor and additive-bonus
|
||||
/// suffixes rather than the multiplier — both must read unsigned too.</summary>
|
||||
[Fact]
|
||||
public void FormatFormula_DivisorAndAdditiveBonusAreReinterpretedAsRetailUnsignedWords()
|
||||
{
|
||||
SkillFormula formula = Formula(w: -1, x: 1, y: 0, z: unchecked((uint)-2));
|
||||
formula.Attribute1 = AttributeId.Strength;
|
||||
|
||||
string? text = RetailSkillFormula.FormatFormula(formula);
|
||||
|
||||
Assert.NotNull(text);
|
||||
Assert.Contains($"/ {unchecked((uint)-2)}", text);
|
||||
Assert.Contains($"+{uint.MaxValue}", text);
|
||||
Assert.DoesNotContain("-1", text);
|
||||
Assert.DoesNotContain("-2", text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LiveResolverLooksUpTheDatFormulaAndTreatsMissingAttributesAsZero()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -229,6 +229,49 @@ public sealed class MapPageControllerTests
|
|||
Assert.False(playerIcon!.Visible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Refresh_PlayerIconTemplateResolutionFails_CoordinateTextStaysEmptyToo()
|
||||
{
|
||||
// F15 (night-round review): gmMapUI::Update @0x004a2078's gate is
|
||||
// `if (m_pCoordinateText != 0 && m_pPlayerLocationIcon != 0)` — BOTH
|
||||
// widgets present, not "at least one". A player-icon template
|
||||
// resolution failure (leaving _playerIcon null, e.g. a future DAT
|
||||
// regression) must skip the coordinate-text write too, not just the
|
||||
// marker placement — the OLD `_coordinateText is null &&
|
||||
// _playerIcon is null` gate only skipped when BOTH were absent, so
|
||||
// it would have written coordinate text here even with a missing
|
||||
// player icon.
|
||||
const uint cellId = 0x11CE0001u; // outdoor: TryFromCell succeeds.
|
||||
Assert.True(RadarCoordinates.TryFromCell(cellId, out _));
|
||||
|
||||
ElementInfo rootInfo = FixtureLoader.LoadMapHouseHostInfos();
|
||||
ImportedLayout layout = FixtureLoader.LoadMapHouseHost();
|
||||
var callbacks = new MapHousePanelController.Callbacks(
|
||||
Toggle: () => { },
|
||||
Map: new MapPageController.Bindings(
|
||||
CurrentCalendar: static () => default,
|
||||
PlayerCellId: () => cellId,
|
||||
HousePosition: static () => (CreateObject.ServerPosition?)null,
|
||||
// Simulates the player icon's own standalone template
|
||||
// resolution failing (ResolveSwallowedIcon's own null path)
|
||||
// while every other swallowed-icon/town-marker resolution
|
||||
// still succeeds normally.
|
||||
TemplateResolver: (_, e) => e == MapPageController.PlayerIconId
|
||||
? null
|
||||
: new UiText { Width = 10f, Height = 10f, DatElementId = e }),
|
||||
House: new HousePageController.Bindings(Lines: static () => Array.Empty<string>()));
|
||||
|
||||
MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, callbacks);
|
||||
Assert.NotNull(controller);
|
||||
|
||||
Assert.Null(
|
||||
UiElement.FindDescendant(controller!.Root, MapPageController.PlayerIconId));
|
||||
|
||||
var coordinateText = Assert.IsType<UiText>(
|
||||
UiElement.FindDescendant(controller.Root, MapPageController.CoordinateTextId));
|
||||
Assert.Empty(coordinateText.LinesProvider());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_HouseMarker_NullPosition_StaysHidden()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue