fix: powerbar mode captions - jump 'Height' + combat 'Power'/'Accuracy'
Retail authors ONE caption element per bar with per-mode state strings, switched by a PassToChildren state cascade (gmPowerbarUI:: RecvNotice_BeginPowerbar @0x004DA730 sets 0x10000042 Jump / 0x10000043 Melee / 0x10000044 Missile / 0x10000045 DDD; installed-DAT probe confirmed every string + PassToChildren flag). - Jump bar (user gate): the floaty powerbar's caption child (0x10000035: JumpMode 'Height', authored HJustify=Center over the bar) was dropped by UiMeter's child absorption. The stateful-fill meter build now absorbs it into per-state labels; TrySetRetailState latches the caption and OnDraw shows it when no live Label provider is bound. JumpPowerbarController's existing JumpMode flip now surfaces 'Height' with zero controller changes. The mount gained the string resolver the Build call never passed. - Combat bar (user gate): label 0x10000052 authors 'MeleeCombat' -> 'Power' and 'MissileCombat' -> 'Accuracy'; the controller latched the MELEE string once at bind. CombatUiLabels now resolves both authored strings and OnCombatModeChanged sets the mode's string - switching live when swapping melee <-> missile weapons in combat. Also fixed the mode-state flip target: the states live on the BASIC PANEL (0x1000005C, PassToChildren), not the layout root (Hide/ShowDetail only) - the old _root flip was a silent no-op. New env-gated ACDREAM_PROBE_POWERBAR layout probe (kept, house pattern). App suite 4,987/3 skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4943484eb9
commit
adf8335675
7 changed files with 233 additions and 14 deletions
|
|
@ -116,6 +116,35 @@ public sealed class CombatUiControllerTests
|
|||
Assert.True(powerLabel.RightAligned);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2026-08-14 gate: the power label's authored per-state strings (element
|
||||
/// 0x10000052: 'MeleeCombat' → 'Power', 'MissileCombat' → 'Accuracy' —
|
||||
/// installed-DAT probe) switch with the combat mode, retail's basic-panel
|
||||
/// PassToChildren cascade. Magic mode hides the basic panel, so the label
|
||||
/// keeps whatever mode set it last.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PowerLabel_SwitchesToAccuracyInMissileMode_AndBack()
|
||||
{
|
||||
var combat = new CombatState();
|
||||
using var attacks = CreateAttacks(combat, () => 0d, []);
|
||||
var (layout, _, _, _, _, _, _) = BuildLayout();
|
||||
var options = new FakeOptionBindings();
|
||||
using var controller = CombatUiController.Bind(
|
||||
layout, combat, attacks, options.ToBindings(),
|
||||
Labels, _ => { })!;
|
||||
var powerLabel = Assert.IsType<UiText>(
|
||||
layout.FindElement(CombatUiController.PowerLabelId));
|
||||
|
||||
Assert.Equal("Power", powerLabel.LinesProvider!()[0].Text);
|
||||
|
||||
combat.SetCombatMode(CombatMode.Missile);
|
||||
Assert.Equal("Accuracy", powerLabel.LinesProvider!()[0].Text);
|
||||
|
||||
combat.SetCombatMode(CombatMode.Melee);
|
||||
Assert.Equal("Power", powerLabel.LinesProvider!()[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImportedFixture_ReflowUpdatesCenteredDarkRange()
|
||||
{
|
||||
|
|
@ -136,8 +165,8 @@ public sealed class CombatUiControllerTests
|
|||
}
|
||||
|
||||
private static readonly CombatUiLabels Labels = new(
|
||||
"Speed", "Power", "Repeat Attacks", "Auto Target", "Keep in View",
|
||||
"High", "Medium", "Low");
|
||||
"Speed", "Power", "Accuracy", "Repeat Attacks", "Auto Target",
|
||||
"Keep in View", "High", "Medium", "Low");
|
||||
|
||||
private static RuntimeCombatAttackState CreateAttacks(
|
||||
CombatState combat,
|
||||
|
|
|
|||
|
|
@ -730,6 +730,43 @@ public class DatWidgetFactoryTests
|
|||
Assert.Equal("six", lines[1].Text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2026-08-14 gate: the stateful-fill meter (gmPowerbarUI's shape in
|
||||
/// LayoutDesc 0x21000072) also absorbs its Type-12 caption child
|
||||
/// (0x10000035, per-mode strings 'Height'/'Power'/'Accuracy') into
|
||||
/// per-state labels latched by TrySetRetailState — the absorbed
|
||||
/// equivalent of retail's PassToChildren state cascade.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildMeter_StatefulFill_AbsorbsCaptionChildPerStateStrings()
|
||||
{
|
||||
var meter = new ElementInfo { Id = 0x34, Type = 7, Width = 600, Height = 15 };
|
||||
meter.StateMedia[""] = (0x06004D0Bu, 1);
|
||||
|
||||
var fill = new ElementInfo { Id = 2, Type = 3, Width = 600, Height = 15 };
|
||||
fill.States[0x10000042u] = new UiStateInfo { Id = 0x10000042u, Name = "JumpMode" };
|
||||
fill.StateMedia["JumpMode"] = (0x06001354u, 1);
|
||||
meter.Children.Add(fill);
|
||||
|
||||
var caption = new ElementInfo { Id = 0x35, Type = 12, Width = 600, Height = 15 };
|
||||
var jumpText = new UiStateInfo { Id = 0x10000042u, Name = "JumpMode" };
|
||||
jumpText.Properties.Values[0x17u] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.StringInfo,
|
||||
StringInfoValue = new UiStringInfoValue(0, 1, 2, 0, 1, 0),
|
||||
};
|
||||
caption.States[0x10000042u] = jumpText;
|
||||
meter.Children.Add(caption);
|
||||
|
||||
var m = Assert.IsType<UiMeter>(DatWidgetFactory.Create(
|
||||
meter, NoTex, null, stringResolve: _ => "Height"));
|
||||
|
||||
Assert.Null(m.ActiveStateLabel);
|
||||
Assert.True(m.TrySetRetailState(0x10000042u));
|
||||
Assert.Equal("Height", m.ActiveStateLabel);
|
||||
Assert.Equal(0x06001354u, m.FrontTile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HorizontalScrollbar_PreservesNestedCombatMeterFillSprite()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
using System.IO;
|
||||
using AcDream.App.UI.Layout;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.Options;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// 2026-08-14 gate probe (the ACDREAM_PROBE_LIVE_MOUNT pattern): dumps the
|
||||
/// authored per-state strings of the floaty powerbar layout (0x21000072 —
|
||||
/// gmPowerbarUI's mode states 0x10000042 jump / 0x10000043 melee /
|
||||
/// 0x10000044 missile / 0x10000045 DDD, RecvNotice_BeginPowerbar
|
||||
/// @ 0x004DA730) and the combat panel's Power label (0x21000073 element
|
||||
/// 0x10000052, root states 0x10000003 melee / 0x10000004 missile) from the
|
||||
/// INSTALLED DAT, so the "Height"/"Power"/"Accuracy" fix binds exactly what
|
||||
/// retail authors instead of guessing.
|
||||
/// </summary>
|
||||
public sealed class PowerbarLayoutProbeTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProbePowerbarAuthoredStrings()
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_POWERBAR") != "1")
|
||||
return;
|
||||
|
||||
var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
?? Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
"Documents",
|
||||
"Asheron's Call");
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
var strings = new DatStringResolver(dats);
|
||||
|
||||
Dump(dats, strings, JumpPowerbarController.LayoutId, "floaty-powerbar");
|
||||
Dump(dats, strings, CombatUiController.LayoutId, "combat-panel");
|
||||
}
|
||||
|
||||
private static void Dump(
|
||||
DatCollection dats,
|
||||
DatStringResolver strings,
|
||||
uint layoutId,
|
||||
string label)
|
||||
{
|
||||
ElementInfo? root = LayoutImporter.ImportInfos(dats, layoutId);
|
||||
if (root is null)
|
||||
{
|
||||
Console.WriteLine($"[pbprobe] {label} 0x{layoutId:X8}: import FAILED");
|
||||
return;
|
||||
}
|
||||
Console.WriteLine($"[pbprobe] === {label} 0x{layoutId:X8} ===");
|
||||
DumpElement(strings, root, 0);
|
||||
}
|
||||
|
||||
private static void DumpElement(DatStringResolver strings, ElementInfo e, int depth)
|
||||
{
|
||||
string indent = new(' ', depth * 2);
|
||||
Console.WriteLine(
|
||||
$"[pbprobe] {indent}0x{e.Id:X8} type={e.Type} ({e.X},{e.Y} {e.Width}x{e.Height}) "
|
||||
+ $"defaultState=0x{e.DefaultStateId:X8}('{e.DefaultStateName}') states={e.States.Count} "
|
||||
+ $"hjustify={e.HJustify} fontColor={(e.FontColor is { } fc ? fc.ToString() : "none")}");
|
||||
foreach (var (stateId, state) in e.States)
|
||||
{
|
||||
string text = "";
|
||||
if (state.Properties.Values.TryGetValue(0x17u, out var p)
|
||||
&& p.Kind == UiPropertyKind.StringInfo)
|
||||
{
|
||||
text = strings.Resolve(p.StringInfoValue) ?? "<unresolved>";
|
||||
text = $" text='{text}'";
|
||||
}
|
||||
Console.WriteLine(
|
||||
$"[pbprobe] {indent} state 0x{stateId:X8} '{state.Name}'"
|
||||
+ $" passToChildren={state.PassToChildren}{text}");
|
||||
}
|
||||
foreach (ElementInfo child in e.Children)
|
||||
DumpElement(strings, child, depth + 1);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue