fix: jump-bar 'Height' right-aligned - per-STATE justification (user
gate; the DAT authors it) The raw-property probe settles it exactly as the user reported: the powerbar caption's mode STATES author their OWN justification (0x14 Enum=0x3 = Right on JumpMode/MeleeMode/MissileMode) while the element default stays centered. ElementInfo.HJustify only ever read the effective DEFAULT state, so the earlier "authored Center" conclusion measured the wrong state. The meter's absorbed state-label entry now carries the state's own authored alignment (state 0x14 wins, element-level HJustify as the fallback, ElementReader's same enum mapping), and the caption draw aligns accordingly - 'Height' sits at the bar's right edge, retail's placement. Live vitals labels keep their centered draw. App suite 4,987/3 skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
adf8335675
commit
88cdfdc3c7
4 changed files with 79 additions and 22 deletions
|
|
@ -538,11 +538,13 @@ public static class DatWidgetFactory
|
||||||
|
|
||||||
// The absorbed Type-12 caption child (gmPowerbarUI's 0x10000035)
|
// The absorbed Type-12 caption child (gmPowerbarUI's 0x10000035)
|
||||||
// authors the per-mode caption on its own states — JumpMode
|
// authors the per-mode caption on its own states — JumpMode
|
||||||
// 'Height', MeleeMode 'Power', MissileMode 'Accuracy' (HJustify
|
// 'Height', MeleeMode 'Power', MissileMode 'Accuracy'. Each mode
|
||||||
// Center over the full bar, matching UiMeter's centered label
|
// STATE also authors its own justification (0x14 = 0x3 Right on
|
||||||
// draw). Retail shows it through the meter's PassToChildren state
|
// every powerbar mode; the element default is centered —
|
||||||
// cascade; the absorbed equivalent is a state-label table
|
// installed-DAT probe 2026-08-14, the user's retail gate). Retail
|
||||||
// consulted by TrySetRetailState.
|
// shows it through the meter's PassToChildren state cascade; the
|
||||||
|
// absorbed equivalent is a state-label table consulted by
|
||||||
|
// TrySetRetailState.
|
||||||
foreach (ElementInfo textChild in info.Children.Where(static c => c.Type == 12))
|
foreach (ElementInfo textChild in info.Children.Where(static c => c.Type == 12))
|
||||||
{
|
{
|
||||||
foreach (var (stateId, state) in textChild.States)
|
foreach (var (stateId, state) in textChild.States)
|
||||||
|
|
@ -551,8 +553,27 @@ public static class DatWidgetFactory
|
||||||
|| !state.Properties.Values.TryGetValue(0x17u, out var caption)
|
|| !state.Properties.Values.TryGetValue(0x17u, out var caption)
|
||||||
|| caption.Kind != UiPropertyKind.StringInfo)
|
|| caption.Kind != UiPropertyKind.StringInfo)
|
||||||
continue;
|
continue;
|
||||||
if (stringResolve?.Invoke(caption.StringInfoValue) is { Length: > 0 } text)
|
if (stringResolve?.Invoke(caption.StringInfoValue) is not { Length: > 0 } text)
|
||||||
m.ConfigureStateLabel(stateId, text);
|
continue;
|
||||||
|
// The state's own 0x14 wins; absent → the element-level
|
||||||
|
// justification (ElementReader's same enum mapping).
|
||||||
|
UiMeterLabelAlign align = textChild.HJustify switch
|
||||||
|
{
|
||||||
|
HJustify.Left => UiMeterLabelAlign.Left,
|
||||||
|
HJustify.Right => UiMeterLabelAlign.Right,
|
||||||
|
_ => UiMeterLabelAlign.Center,
|
||||||
|
};
|
||||||
|
if (state.Properties.Values.TryGetValue(0x14u, out var justify)
|
||||||
|
&& justify.Kind == UiPropertyKind.Enum)
|
||||||
|
{
|
||||||
|
align = justify.UnsignedValue switch
|
||||||
|
{
|
||||||
|
0u or 2u => UiMeterLabelAlign.Left,
|
||||||
|
3u or 5u => UiMeterLabelAlign.Right,
|
||||||
|
_ => UiMeterLabelAlign.Center,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
m.ConfigureStateLabel(stateId, text, align);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.UI;
|
namespace AcDream.App.UI;
|
||||||
|
|
||||||
|
/// <summary>Justification of a meter's absorbed state caption (dat property
|
||||||
|
/// 0x14 on the caption child's own state: 0/2 = Left, 3/5 = Right, 1 = Center
|
||||||
|
/// — the same mapping ElementReader applies at element level).</summary>
|
||||||
|
public enum UiMeterLabelAlign : byte { Left = 0, Center = 1, Right = 2 }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A horizontal vital bar (retail HP/Stamina/Mana style): a background rect, a
|
/// A horizontal vital bar (retail HP/Stamina/Mana style): a background rect, a
|
||||||
/// partial-width solid fill, and an optional centered "current/max" numeric
|
/// partial-width solid fill, and an optional centered "current/max" numeric
|
||||||
|
|
@ -17,8 +22,8 @@ namespace AcDream.App.UI;
|
||||||
public sealed class UiMeter : UiElement, IUiDatStateful
|
public sealed class UiMeter : UiElement, IUiDatStateful
|
||||||
{
|
{
|
||||||
private readonly Dictionary<uint, uint> _stateFillSprites = new();
|
private readonly Dictionary<uint, uint> _stateFillSprites = new();
|
||||||
private readonly Dictionary<uint, string> _stateLabels = new();
|
private readonly Dictionary<uint, (string Text, UiMeterLabelAlign Align)> _stateLabels = new();
|
||||||
private string? _activeStateLabel;
|
private (string Text, UiMeterLabelAlign Align)? _activeStateLabel;
|
||||||
|
|
||||||
/// <summary>Dat element id, set by the layout importer so duplicated page copies can be scoped.</summary>
|
/// <summary>Dat element id, set by the layout importer so duplicated page copies can be scoped.</summary>
|
||||||
public uint ElementId { get; set; }
|
public uint ElementId { get; set; }
|
||||||
|
|
@ -76,7 +81,10 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
||||||
|
|
||||||
/// <summary>The caption latched by the active retail state (null when the
|
/// <summary>The caption latched by the active retail state (null when the
|
||||||
/// state authors none). Exposed for tests.</summary>
|
/// state authors none). Exposed for tests.</summary>
|
||||||
internal string? ActiveStateLabel => _activeStateLabel;
|
internal string? ActiveStateLabel => _activeStateLabel?.Text;
|
||||||
|
|
||||||
|
/// <summary>The latched caption's authored justification. Exposed for tests.</summary>
|
||||||
|
internal UiMeterLabelAlign? ActiveStateLabelAlign => _activeStateLabel?.Align;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers a fill sprite carried by a child state of the imported meter.
|
/// Registers a fill sprite carried by a child state of the imported meter.
|
||||||
|
|
@ -95,19 +103,21 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
||||||
/// / MissileMode 'Accuracy'). Retail shows it via the meter's
|
/// / MissileMode 'Accuracy'). Retail shows it via the meter's
|
||||||
/// PassToChildren state cascade; the absorbed equivalent latches it in
|
/// PassToChildren state cascade; the absorbed equivalent latches it in
|
||||||
/// <see cref="TrySetRetailState"/> and draws it when no live
|
/// <see cref="TrySetRetailState"/> and draws it when no live
|
||||||
/// <see cref="Label"/> provider supplies text. The child authors
|
/// <see cref="Label"/> provider supplies text. Each mode STATE authors its
|
||||||
/// HJustify=Center — the same centered draw the label overlay uses.
|
/// own justification (dat property 0x14 — the powerbar modes author 0x3 =
|
||||||
|
/// Right; only the element default is centered), so alignment rides the
|
||||||
|
/// state entry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void ConfigureStateLabel(uint stateId, string text)
|
internal void ConfigureStateLabel(uint stateId, string text, UiMeterLabelAlign align)
|
||||||
{
|
{
|
||||||
if (stateId != 0 && !string.IsNullOrEmpty(text))
|
if (stateId != 0 && !string.IsNullOrEmpty(text))
|
||||||
_stateLabels[stateId] = text;
|
_stateLabels[stateId] = (text, align);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TrySetRetailState(uint stateId)
|
public bool TrySetRetailState(uint stateId)
|
||||||
{
|
{
|
||||||
bool hasFill = _stateFillSprites.TryGetValue(stateId, out uint spriteId);
|
bool hasFill = _stateFillSprites.TryGetValue(stateId, out uint spriteId);
|
||||||
bool hasLabel = _stateLabels.TryGetValue(stateId, out string? caption);
|
bool hasLabel = _stateLabels.TryGetValue(stateId, out var caption);
|
||||||
if (!hasFill && !hasLabel)
|
if (!hasFill && !hasLabel)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
@ -136,6 +146,14 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
||||||
/// grandchild slice/text elements as separate widgets.</summary>
|
/// grandchild slice/text elements as separate widgets.</summary>
|
||||||
public override bool ConsumesDatChildren => true;
|
public override bool ConsumesDatChildren => true;
|
||||||
|
|
||||||
|
private static float AlignX(UiMeterLabelAlign align, float width, float textWidth)
|
||||||
|
=> align switch
|
||||||
|
{
|
||||||
|
UiMeterLabelAlign.Left => 0f,
|
||||||
|
UiMeterLabelAlign.Right => width - textWidth,
|
||||||
|
_ => (width - textWidth) * 0.5f,
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>Clamp <paramref name="pct"/> to [0,1] and return the fill rect
|
/// <summary>Clamp <paramref name="pct"/> to [0,1] and return the fill rect
|
||||||
/// (local px) for a bar of <paramref name="w"/> x <paramref name="h"/>.</summary>
|
/// (local px) for a bar of <paramref name="w"/> x <paramref name="h"/>.</summary>
|
||||||
public static (float x, float y, float w, float h) ComputeFillRect(
|
public static (float x, float y, float w, float h) ComputeFillRect(
|
||||||
|
|
@ -196,16 +214,20 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A live provider (vitals "cur/max") wins; otherwise the active
|
// A live provider (vitals "cur/max") wins and stays centered;
|
||||||
// retail-state caption ('Height'/'Power'/'Accuracy') shows.
|
// otherwise the active retail-state caption shows with its own
|
||||||
string? label = Label() ?? _activeStateLabel;
|
// authored justification (the powerbar modes author Right).
|
||||||
|
string? label = Label();
|
||||||
|
UiMeterLabelAlign align = UiMeterLabelAlign.Center;
|
||||||
|
if (string.IsNullOrEmpty(label) && _activeStateLabel is { } stateLabel)
|
||||||
|
(label, align) = stateLabel;
|
||||||
if (!string.IsNullOrEmpty(label))
|
if (!string.IsNullOrEmpty(label))
|
||||||
{
|
{
|
||||||
if (DatFont is { } datFont)
|
if (DatFont is { } datFont)
|
||||||
{
|
{
|
||||||
// Retail path: centered cur/max via the dat font's two-pass blit.
|
// Retail path: the dat font's two-pass blit.
|
||||||
float tw = datFont.MeasureWidth(label);
|
float tw = datFont.MeasureWidth(label);
|
||||||
float tx = (Width - tw) * 0.5f;
|
float tx = AlignX(align, Width, tw);
|
||||||
float ty = (Height - datFont.LineHeight) * 0.5f;
|
float ty = (Height - datFont.LineHeight) * 0.5f;
|
||||||
ctx.DrawStringDat(datFont, label, tx, ty, LabelColor, Outline, OutlineColor);
|
ctx.DrawStringDat(datFont, label, tx, ty, LabelColor, Outline, OutlineColor);
|
||||||
}
|
}
|
||||||
|
|
@ -213,7 +235,7 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
||||||
{
|
{
|
||||||
// Fallback: debug bitmap font (no dat font available).
|
// Fallback: debug bitmap font (no dat font available).
|
||||||
float tw = font.MeasureWidth(label);
|
float tw = font.MeasureWidth(label);
|
||||||
float tx = (Width - tw) * 0.5f;
|
float tx = AlignX(align, Width, tw);
|
||||||
float ty = (Height - font.LineHeight) * 0.5f;
|
float ty = (Height - font.LineHeight) * 0.5f;
|
||||||
ctx.DrawString(label, tx, ty, LabelColor);
|
ctx.DrawString(label, tx, ty, LabelColor);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -755,6 +755,13 @@ public class DatWidgetFactoryTests
|
||||||
Kind = UiPropertyKind.StringInfo,
|
Kind = UiPropertyKind.StringInfo,
|
||||||
StringInfoValue = new UiStringInfoValue(0, 1, 2, 0, 1, 0),
|
StringInfoValue = new UiStringInfoValue(0, 1, 2, 0, 1, 0),
|
||||||
};
|
};
|
||||||
|
// The mode state's own justification (0x14=0x3 Right — the powerbar
|
||||||
|
// modes all author it; the element default stays centered).
|
||||||
|
jumpText.Properties.Values[0x14u] = new UiPropertyValue
|
||||||
|
{
|
||||||
|
Kind = UiPropertyKind.Enum,
|
||||||
|
UnsignedValue = 0x3u,
|
||||||
|
};
|
||||||
caption.States[0x10000042u] = jumpText;
|
caption.States[0x10000042u] = jumpText;
|
||||||
meter.Children.Add(caption);
|
meter.Children.Add(caption);
|
||||||
|
|
||||||
|
|
@ -764,6 +771,7 @@ public class DatWidgetFactoryTests
|
||||||
Assert.Null(m.ActiveStateLabel);
|
Assert.Null(m.ActiveStateLabel);
|
||||||
Assert.True(m.TrySetRetailState(0x10000042u));
|
Assert.True(m.TrySetRetailState(0x10000042u));
|
||||||
Assert.Equal("Height", m.ActiveStateLabel);
|
Assert.Equal("Height", m.ActiveStateLabel);
|
||||||
|
Assert.Equal(UiMeterLabelAlign.Right, m.ActiveStateLabelAlign);
|
||||||
Assert.Equal(0x06001354u, m.FrontTile);
|
Assert.Equal(0x06001354u, m.FrontTile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,15 @@ public sealed class PowerbarLayoutProbeTests
|
||||||
text = strings.Resolve(p.StringInfoValue) ?? "<unresolved>";
|
text = strings.Resolve(p.StringInfoValue) ?? "<unresolved>";
|
||||||
text = $" text='{text}'";
|
text = $" text='{text}'";
|
||||||
}
|
}
|
||||||
|
string properties = string.Join(
|
||||||
|
" ",
|
||||||
|
state.Properties.Values
|
||||||
|
.OrderBy(static pair => pair.Key)
|
||||||
|
.Select(static pair =>
|
||||||
|
$"0x{pair.Key:X2}:{pair.Value.Kind}=0x{pair.Value.UnsignedValue:X}"));
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"[pbprobe] {indent} state 0x{stateId:X8} '{state.Name}'"
|
$"[pbprobe] {indent} state 0x{stateId:X8} '{state.Name}'"
|
||||||
+ $" passToChildren={state.PassToChildren}{text}");
|
+ $" passToChildren={state.PassToChildren}{text} props[{properties}]");
|
||||||
}
|
}
|
||||||
foreach (ElementInfo child in e.Children)
|
foreach (ElementInfo child in e.Children)
|
||||||
DumpElement(strings, child, depth + 1);
|
DumpElement(strings, child, depth + 1);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue