@
feat(D.2b): data-driven channel menu chrome + greying + scroll-arrow fix Investigation found the menu popup is fully dat-driven (UIElement_Menu::MakePopup @0x46d310 reads LayoutDesc 0x21000006 elements 0x1000001C/1D/1E — the "stray" top-level elements). Render the popup from the real sprites instead of a flat rect: - panel 0x0600124C, item row 0x0600124E, selected row 0x0600124D; 191x17 rows, 2 cols. - drawing rows as SPRITES also fixes the z-order (a DrawRect bg composited OVER the labels; sprites share the labels submission bucket so text lands on top). - item greying: available channels white, unavailable salmon (colorPink) — static approximation (Say/General/Trade/LFG) with an AvailabilityProvider hook for live TurbineChat state; unavailable items are inert on click. Ports ResetAllTalkFocusMenuButtons. - scroll arrows: both dat sprites point down (export-confirmed); V-flip the top button so it points up. Tabs confirmed to have NO digits in retail (blank gold frames) — acdream already matches. Build + 392 App tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
This commit is contained in:
parent
7094a1c847
commit
bb983ae850
4 changed files with 155 additions and 64 deletions
|
|
@ -50,8 +50,11 @@ public sealed class ChatWindowController
|
|||
private const uint DownSprite = 0x06004C6Cu;
|
||||
|
||||
// Channel menu sprite ids (confirmed in chat element dump).
|
||||
private const uint MenuNormal = 0x06004D65u;
|
||||
private const uint MenuPressed = 0x06004D66u;
|
||||
private const uint MenuNormal = 0x06004D65u; // button face
|
||||
private const uint MenuPressed = 0x06004D66u; // button pressed
|
||||
private const uint MenuPopupBg = 0x0600124Cu; // popup panel fill (element 0x1000001C)
|
||||
private const uint MenuItemRow = 0x0600124Eu; // item row bg (template 0x1000001E)
|
||||
private const uint MenuItemSelected = 0x0600124Du; // active channel row
|
||||
|
||||
// ── Public surface ─────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -225,11 +228,14 @@ public sealed class ChatWindowController
|
|||
Width = menuEl.Width,
|
||||
Height = menuEl.Height,
|
||||
Anchors = menuEl.Anchors,
|
||||
DatFont = datFont,
|
||||
Font = debugFont,
|
||||
SpriteResolve = resolve,
|
||||
NormalSprite = MenuNormal,
|
||||
PressedSprite = MenuPressed,
|
||||
DatFont = datFont,
|
||||
Font = debugFont,
|
||||
SpriteResolve = resolve,
|
||||
NormalSprite = MenuNormal,
|
||||
PressedSprite = MenuPressed,
|
||||
PopupBgSprite = MenuPopupBg,
|
||||
ItemNormalSprite = MenuItemRow,
|
||||
ItemHighlightSprite = MenuItemSelected,
|
||||
};
|
||||
c.Menu.OnChannelChanged = k => c._activeChannel = k;
|
||||
menuParent.RemoveChild(menuEl);
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ namespace AcDream.App.UI;
|
|||
|
||||
/// <summary>
|
||||
/// Chat channel selector — the "Chat" button + its talk-focus popup. Mirrors retail
|
||||
/// <c>gmMainChatUI::InitTalkFocusMenu @0x4cdc50</c>: the button is labelled "Chat";
|
||||
/// clicking opens a TWO-COLUMN popup of 14 talk-focus items (Squelch, Tell to Selected,
|
||||
/// Chat to All, Tell to Fellows, …). Selecting a channel item sets the active outbound
|
||||
/// channel (retail <c>SetTalkFocus</c>; here <see cref="OnChannelChanged"/>). The items
|
||||
/// are code-populated exactly as retail populates them, not a dat-layout port.
|
||||
/// <c>gmMainChatUI::InitTalkFocusMenu @0x4cdc50</c> + <c>UIElement_Menu::MakePopup
|
||||
/// @0x46d310</c>: the button is labelled with the active target; clicking opens a
|
||||
/// TWO-COLUMN popup of 14 talk-focus items on the dat-driven menu chrome (panel +
|
||||
/// per-row + selected-row sprites, 191×17 rows, from LayoutDesc 0x21000006 elements
|
||||
/// 0x1000001C/1D/1E). Items are code-populated exactly as retail populates them.
|
||||
/// Unavailable channels render greyed (retail <c>ResetAllTalkFocusMenuButtons</c> →
|
||||
/// SetState(disabled), <c>colorPink</c>).
|
||||
/// </summary>
|
||||
public sealed class UiChannelMenu : UiElement
|
||||
{
|
||||
|
|
@ -39,21 +41,34 @@ public sealed class UiChannelMenu : UiElement
|
|||
};
|
||||
|
||||
private const int Rows = 7; // items per column
|
||||
private const float ItemH = 16f; // row height
|
||||
private const float ColW = 150f; // column width (fits "Tell to Roleplay Chat")
|
||||
private const float ItemH = 17f; // row height (dat item template 0x1000001E H=17)
|
||||
private const float ColW = 191f; // column width (dat item template W=191)
|
||||
|
||||
/// <summary>The channel the player's typed text currently goes to.</summary>
|
||||
public ChatChannelKind Selected { get; private set; } = ChatChannelKind.Say;
|
||||
public Action<ChatChannelKind>? OnChannelChanged { get; set; }
|
||||
|
||||
/// <summary>Per-channel availability gate (retail greys channels you are not in).
|
||||
/// Defaults to a static approximation; the controller can inject live channel state.</summary>
|
||||
public Func<ChatChannelKind, bool>? AvailabilityProvider { get; set; }
|
||||
|
||||
public UiDatFont? DatFont { get; set; }
|
||||
public AcDream.App.Rendering.BitmapFont? Font { get; set; }
|
||||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
|
||||
// Button face sprites (dat menu element 0x10000014).
|
||||
public uint NormalSprite { get; set; }
|
||||
public uint PressedSprite { get; set; }
|
||||
// Popup chrome sprites (dat menu popup template, layout 0x21000006).
|
||||
public uint PopupBgSprite { get; set; } // 0x0600124C — panel fill (191×2 tiles)
|
||||
public uint ItemNormalSprite { get; set; } // 0x0600124E — a row background (191×17)
|
||||
public uint ItemHighlightSprite { get; set; } // 0x0600124D — the active channel's row
|
||||
|
||||
public Vector4 TextColor { get; set; } = new(1f, 0.92f, 0.72f, 1f);
|
||||
/// <summary>Popup panel fill — the retail talk-focus menu is a warm tan/orange.</summary>
|
||||
public Vector4 PopupColor { get; set; } = new(0.56f, 0.40f, 0.18f, 1f);
|
||||
/// <summary>Available item text (retail white #FFFFFF).</summary>
|
||||
public Vector4 TextColorAvailable { get; set; } = new(1f, 1f, 1f, 1f);
|
||||
/// <summary>Greyed/unavailable item text (retail colorPink salmon, acclient 0x81c528).</summary>
|
||||
public Vector4 TextColorGhosted { get; set; } = new(1f, 0.588f, 0.588f, 1f);
|
||||
|
||||
private bool _open;
|
||||
private static float PopupW => 2 * ColW;
|
||||
|
|
@ -61,8 +76,17 @@ public sealed class UiChannelMenu : UiElement
|
|||
|
||||
public UiChannelMenu() { CapturesPointerDrag = true; }
|
||||
|
||||
/// <summary>True if the channel is currently joinable/visible. Defaults to a static
|
||||
/// approximation matching the common case (Say/General/Trade/LFG); the fellowship +
|
||||
/// allegiance-hierarchy channels need membership state acdream does not yet track
|
||||
/// (deferred → greyed). The controller can override via <see cref="AvailabilityProvider"/>.</summary>
|
||||
private bool IsAvailable(ChatChannelKind ch)
|
||||
=> AvailabilityProvider?.Invoke(ch)
|
||||
?? ch is ChatChannelKind.Say or ChatChannelKind.General
|
||||
or ChatChannelKind.Trade or ChatChannelKind.Lfg;
|
||||
|
||||
/// <summary>The button face label = the active talk target (retail updates the
|
||||
/// "Chat" button to whichever target you pick). "Chat" = Chat-to-All (Say).</summary>
|
||||
/// button to whichever target you pick). "Chat" = Chat-to-All (Say).</summary>
|
||||
private string ButtonText => Selected switch
|
||||
{
|
||||
ChatChannelKind.Say => "Chat",
|
||||
|
|
@ -82,27 +106,40 @@ public sealed class UiChannelMenu : UiElement
|
|||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
// Button face + the active-target label (retail updates this to the chosen target).
|
||||
if (SpriteResolve is { } resolve)
|
||||
var resolve = SpriteResolve;
|
||||
|
||||
// Button face + the active-target label.
|
||||
if (resolve is not null)
|
||||
{
|
||||
var (tex, _, _) = resolve(_open ? PressedSprite : NormalSprite);
|
||||
if (tex != 0) ctx.DrawSprite(tex, 0, 0, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
DrawLabel(ctx, ButtonText, 4f, (Height - LineH()) * 0.5f);
|
||||
DrawLabel(ctx, ButtonText, 4f, (Height - LineH()) * 0.5f, TextColor);
|
||||
|
||||
if (!_open) return;
|
||||
if (!_open || resolve is null) return;
|
||||
|
||||
// Two-column popup opening UPWARD from the button (chat sits at screen bottom).
|
||||
// Force OPAQUE: the menu must read solid even though the chat window is translucent.
|
||||
// Two-column popup opening UPWARD from the button. Force OPAQUE (a menu reads
|
||||
// solid even though the chat window is translucent). Draw the dat row sprites
|
||||
// first, then the labels — both go through the sprite bucket in submission order,
|
||||
// so the labels land on top (a DrawRect bg would composite over the text instead).
|
||||
ctx.PushAlphaAbsolute(1f);
|
||||
try
|
||||
{
|
||||
float top = -PopupH;
|
||||
ctx.DrawRect(0, top, PopupW, PopupH, PopupColor);
|
||||
DrawSprite(ctx, resolve, PopupBgSprite, 0f, top, PopupW, PopupH); // panel base
|
||||
for (int i = 0; i < Items.Length; i++)
|
||||
{
|
||||
int col = i / Rows, row = i % Rows;
|
||||
DrawLabel(ctx, Items[i].Label, 4f + col * ColW, top + row * ItemH);
|
||||
float x = col * ColW, y = top + row * ItemH;
|
||||
bool selected = Items[i].Channel is { } c && c == Selected;
|
||||
DrawSprite(ctx, resolve, selected ? ItemHighlightSprite : ItemNormalSprite, x, y, ColW, ItemH);
|
||||
}
|
||||
for (int i = 0; i < Items.Length; i++)
|
||||
{
|
||||
int col = i / Rows, row = i % Rows;
|
||||
bool avail = Items[i].Channel is { } c && IsAvailable(c);
|
||||
DrawLabel(ctx, Items[i].Label, 4f + col * ColW, top + row * ItemH,
|
||||
avail ? TextColorAvailable : TextColorGhosted);
|
||||
}
|
||||
}
|
||||
finally { ctx.PopAlpha(); }
|
||||
|
|
@ -110,10 +147,20 @@ public sealed class UiChannelMenu : UiElement
|
|||
|
||||
private float LineH() => DatFont?.LineHeight ?? Font?.LineHeight ?? 14f;
|
||||
|
||||
private void DrawLabel(UiRenderContext ctx, string s, float x, float y)
|
||||
private void DrawSprite(UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||
uint id, float x, float y, float w, float h)
|
||||
{
|
||||
if (DatFont is { } df) ctx.DrawStringDat(df, s, x, y, TextColor);
|
||||
else ctx.DrawString(s, x, y, TextColor, Font);
|
||||
if (id == 0) return;
|
||||
var (tex, tw, th) = resolve(id);
|
||||
if (tex == 0 || tw == 0 || th == 0) return;
|
||||
// Tile at native size (the panel fill is 191×2; rows are 191×17 = 1:1).
|
||||
ctx.DrawSprite(tex, x, y, w, h, 0f, 0f, w / tw, h / th, Vector4.One);
|
||||
}
|
||||
|
||||
private void DrawLabel(UiRenderContext ctx, string s, float x, float y, Vector4 color)
|
||||
{
|
||||
if (DatFont is { } df) ctx.DrawStringDat(df, s, x, y, color);
|
||||
else ctx.DrawString(s, x, y, color, Font);
|
||||
}
|
||||
|
||||
protected override bool OnHitTest(float lx, float ly)
|
||||
|
|
@ -130,8 +177,9 @@ public sealed class UiChannelMenu : UiElement
|
|||
int col = lx < ColW ? 0 : 1;
|
||||
int row = (int)((ly + PopupH) / ItemH);
|
||||
int idx = col * Rows + row;
|
||||
// Only pick available channel items (special + greyed items are inert).
|
||||
if (row >= 0 && row < Rows && idx >= 0 && idx < Items.Length
|
||||
&& Items[idx].Channel is { } ch)
|
||||
&& Items[idx].Channel is { } ch && IsAvailable(ch))
|
||||
{
|
||||
Selected = ch;
|
||||
OnChannelChanged?.Invoke(ch);
|
||||
|
|
|
|||
|
|
@ -89,10 +89,11 @@ public sealed class UiChatScrollbar : UiElement
|
|||
// sprite (~16×32) repeats to fill the element height instead of stretch-distorting.
|
||||
DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height);
|
||||
|
||||
// Up button — top ButtonH rows (directional arrow art, drawn 1:1).
|
||||
DrawSprite(ctx, resolve, UpSprite, 0f, 0f, Width, ButtonH);
|
||||
// Up button — top ButtonH rows. The dat up/down arrow sprites both point DOWN
|
||||
// (confirmed by sprite export), so the TOP button is drawn V-FLIPPED to point UP.
|
||||
DrawSpriteFlipV(ctx, resolve, UpSprite, 0f, 0f, Width, ButtonH);
|
||||
|
||||
// Down button — bottom ButtonH rows.
|
||||
// Down button — bottom ButtonH rows (down arrow as-is).
|
||||
DrawSprite(ctx, resolve, DownSprite, 0f, Height - ButtonH, Width, ButtonH);
|
||||
|
||||
// Thumb — only when content overflows the view. Retail 3-slice: top cap +
|
||||
|
|
@ -127,6 +128,17 @@ public sealed class UiChatScrollbar : UiElement
|
|||
ctx.DrawSprite(tex, x, y, w, h, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
/// <summary>Draw a sprite 1:1 but vertically FLIPPED (V0/V1 swapped) — used to point
|
||||
/// the top scroll button's (down-art) arrow upward.</summary>
|
||||
private void DrawSpriteFlipV(UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||
uint id, float x, float y, float w, float h)
|
||||
{
|
||||
if (id == 0 || w <= 0f || h <= 0f) return;
|
||||
var (tex, _, _) = resolve(id);
|
||||
if (tex == 0) return;
|
||||
ctx.DrawSprite(tex, x, y, w, h, 0f, 1f, 1f, 0f, Vector4.One);
|
||||
}
|
||||
|
||||
/// <summary>Draw a sprite TILED to fill the dest rect (UV-repeat at native size on
|
||||
/// both axes — the UI texture is GL_REPEAT-wrapped). A native-width axis gives 1:1.</summary>
|
||||
private void DrawTiled(UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue