using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using AcDream.App.UI; using AcDream.Core.Chat; using AcDream.UI.Abstractions; using AcDream.UI.Abstractions.Panels.Chat; namespace AcDream.App.UI.Layout; /// /// Binds one of retail's four floating chat windows — LayoutDesc /// 0x2100005B, window elements 0x10000505/0x1000050E/ /// 0x1000050F/0x10000510 — to live behavior (Campaign CH /// slice CH6b). All four windows share the SAME LayoutDesc; only the /// window-id attribute (0x1000007E, carried here as /// rather than re-read from the dat, since acdream's /// importer does not resolve LayoutDesc attributes into runtime state) and /// the mounted screen position differ per instance. /// /// /// Shares with /// (the main window) rather than /// duplicating the word-wrap/color-carry algorithm — both are views over the /// SAME transcript (research doc §6.1's "one /// ChatWindowController instance per window" recommendation, generalized to /// two sibling classes because the main and floaty layouts diverge enough /// in their own element sets — talk-focus menu, max/min button, four /// indicator buttons on main; title bar and close button on floaty, /// research doc §2.1 vs §2.2 — to make one shared Bind() unreadable). /// /// /// /// A floaty window has no talk-focus menu (research doc §2.2), so its chat /// entry always sends on — there is no /// authored control to pick another channel from this window. /// /// public sealed class FloatingChatWindowController : IRetainedPanelController { public const uint LayoutId = 0x2100005Bu; // Element ids from floating chat LayoutDesc 0x2100005B (research doc §2.2). private const uint RootId = 0x100004F7u; // window root, 250x108 private const uint TranscriptPanelId = 0x10000010u; private const uint TranscriptId = 0x10000011u; // Type-12 prototype — skipped by factory private const uint TrackId = 0x10000012u; private const uint InputRowId = 0x10000509u; // NOTE: differs from the main window's 0x10000013 private const uint InputId = 0x10000016u; // Type-12 Text + Editable 0x16 -> UiField private const uint SendId = 0x10000019u; private const uint TitleBarId = 0x100004D9u; // gmFloatyChatUI::SetWindowTitle target private const uint CloseButtonId = 0x1000052Au; private bool _disposed; /// The retail window id this instance is bound to (1-4). public int WindowId { get; } public UiElement Root { get; private set; } = null!; public UiText Transcript { get; private set; } = null!; public UiField Input { get; private set; } = null!; public UiScrollbar? Scrollbar { get; private set; } /// Resolved window-root metadata, including DAT size constraints. public ElementInfo DatWindowInfo { get; private set; } = null!; public RetailWindowHandle? WindowHandle { get; private set; } // Same idle-frame caching shape as ChatWindowController — an unchanged // transcript/filter/wrap-width does not re-wrap or re-resolve colors. private IReadOnlyList _cachedTranscriptLines = Array.Empty(); private long _cachedTranscriptRevision = -1; private ulong _cachedFilter; private float _cachedTranscriptWrapWidth = float.NaN; private UiDatFont? _cachedTranscriptDatFont; private BitmapFont? _cachedTranscriptDebugFont; internal int TranscriptLayoutBuildCount { get; private set; } private FloatingChatWindowController(int windowId) { WindowId = windowId; } /// /// Bind an imported floating-chat layout to live behavior. /// /// Retail chat window id, 1 through 4. /// Full tree from an /// call against /// . /// Widget tree from a matching /// call — a FRESH call per window /// instance, since four independent windows need four independent /// widget trees even though they share one imported rootInfo. /// The SAME chat view-model the main window binds — /// one canonical transcript, filtered per window. /// Factory for the live command bus at submit time. /// /// Runtime's canonical per-window filter/open state /// (). Read live on /// every transcript rebuild — never copied — so a filter change is /// visible on the next frame without a separate notification. /// public static FloatingChatWindowController? Bind( int windowId, ElementInfo rootInfo, ImportedLayout layout, ChatVM vm, Func busProvider, ChatWindowState windowFilters, UiDatFont? datFont, BitmapFont? debugFont, Func resolve) { if (windowId < ChatWindowState.MinFloatingWindowId || windowId > ChatWindowState.MaxFloatingWindowId) throw new ArgumentOutOfRangeException(nameof(windowId)); ArgumentNullException.ThrowIfNull(windowFilters); var transcriptPanel = layout.FindElement(TranscriptPanelId); var inputRow = layout.FindElement(InputRowId); var input = layout.FindElement(InputId) as UiField; if (input is null || transcriptPanel is null || inputRow is null) { Console.WriteLine( $"[D.2b] FloatingChatWindowController.Bind(window {windowId}): missing required elements " + $"(input={input is not null}, panel={transcriptPanel is not null}, row={inputRow is not null}) — " + $"floating chat window will not be interactive."); return null; } var window = layout.FindElement(RootId) ?? layout.Root; var c = new FloatingChatWindowController(windowId) { Root = window, DatWindowInfo = FindInfo(rootInfo, RootId) ?? rootInfo, }; // ── Transcript ─────────────────────────────────────────────────── c.Transcript = layout.FindElement(TranscriptId) as UiText ?? throw new InvalidOperationException("floating chat transcript 0x10000011 not built as UiText"); c.Transcript.DatFont = datFont; c.Transcript.Font = debugFont; c.Transcript.Centered = false; c.Transcript.RightAligned = false; c.Transcript.OneLine = false; c.Transcript.Selectable = true; c.Transcript.LinesProvider = () => c.GetTranscriptLines(vm, windowFilters); // ── Input — no talk-focus menu on the floaty layout, so the // channel is always Say (class doc). ───────────────────────────── c.Input = input; c.Input.DatFont = datFont; c.Input.Font = debugFont; c.Input.SpriteResolve = resolve; c.Input.OnSubmit = text => ChatCommandRouter.Submit(text, vm, busProvider(), ChatChannelKind.Say); // Same right-edge-tracks-resize fix as the main window // (ChatWindowController.Bind) — see that method's comment for the // full retail edge-mode citation. if (c.Input.LayoutPolicy is { } inputPolicy) { c.Input.LayoutPolicy = new UiLayoutPolicy( inputPolicy.LeftMode, inputPolicy.TopMode, rightMode: 1u, inputPolicy.BottomMode, inputPolicy.OriginalChild, inputPolicy.OriginalParent); } else { c.Input.Anchors |= AnchorEdges.Right; } // ── Scrollbar — bind the factory-built Type-11 track element ──── if (layout.FindElement(TrackId) is UiScrollbar bar) { bar.Model = c.Transcript.Scroll; bar.SpriteResolve ??= resolve; c.Scrollbar = bar; } // ── Send button ───────────────────────────────────────────────── if (layout.FindElement(SendId) is UiButton sendEl) { sendEl.OnClick = () => c.Input.Submit(); sendEl.Label = "Send"; sendEl.LabelFont = datFont; sendEl.LabelColor = new Vector4(1f, 0.92f, 0.72f, 1f); } // ── Title bar — gmFloatyChatUI::SetWindowTitle @0x004CEAA0 sets a // localized "Chat N" string here; acdream has no LayoutDesc string // table wired for it yet, so this is a hardcoded English label — // the same stopgap the channel menu's item labels already use // (ChatWindowController.ChannelItems). ────────────────────────── if (layout.FindElement(TitleBarId) is UiText titleText) { titleText.DatFont = datFont; titleText.Font = debugFont; titleText.OneLine = true; string title = $"Chat {windowId}"; var titleColor = new Vector4(1f, 0.92f, 0.72f, 1f); titleText.LinesProvider = () => new[] { new UiText.Line(title, titleColor) }; } // ── Close button — gmFloatyChatUI::ListenToElementMessage // @0x004CE330: idMessage==1 (clicked) on 0x1000052A -> SetVisible(false). ── if (layout.FindElement(CloseButtonId) is UiButton closeEl) { closeEl.OnClick = () => c.WindowHandle?.Hide(); } return c; } /// /// Attach the typed outer-frame handle after the controller's imported /// content has been mounted. The close button needs this to hide the /// window (retail's own SetVisible(false)). /// public void AttachWindow(RetailWindowHandle handle) { ArgumentNullException.ThrowIfNull(handle); if (!ReferenceEquals(handle.ContentRoot, Root)) throw new ArgumentException( "Floating chat handle content root does not match the bound layout.", nameof(handle)); if (WindowHandle is not null && !ReferenceEquals(WindowHandle, handle)) throw new InvalidOperationException("Floating chat controller is already attached to another window."); WindowHandle = handle; } private static ElementInfo? FindInfo(ElementInfo node, uint id) { if (node.Id == id) return node; foreach (ElementInfo child in node.Children) { ElementInfo? found = FindInfo(child, id); if (found is not null) return found; } return null; } /// /// Convert the shared ChatVM's detailed lines to this window's filtered, /// wrapped, colored transcript — /// is retail's broadcast half of the display rule /// (ChatInterface::RecvNotice_DisplayFinalStringInfo); the /// explicit-address half is inert today because no production /// ChatEntry carries a target window id yet (register row /// AP-180 — see 's class doc). /// private IReadOnlyList GetTranscriptLines(ChatVM vm, ChatWindowState windowFilters) { float maxW = Transcript.Width - 2f * Transcript.Padding; UiDatFont? datFont = Transcript.DatFont; BitmapFont? debugFont = Transcript.Font; long revision = vm.Revision; ulong filter = windowFilters.GetFilter(WindowId); if (_cachedTranscriptRevision == revision && _cachedFilter == filter && _cachedTranscriptWrapWidth.Equals(maxW) && ReferenceEquals(_cachedTranscriptDatFont, datFont) && ReferenceEquals(_cachedTranscriptDebugFont, debugFont)) { return _cachedTranscriptLines; } var detailed = vm.RecentLinesDetailed(); Func measure = datFont is { } df ? s => df.MeasureWidth(s) : debugFont is { } bf ? s => bf.MeasureWidth(s) : static s => s.Length * 7f; bool Accept(uint logTextType) => windowFilters.ShouldDisplay( WindowId, ChatWindowState.BroadcastTargetWindow, logTextType); var result = ChatTranscriptRenderer.BuildLines( detailed, maxW, measure, Accept, Transcript.DefaultColor); _cachedTranscriptRevision = revision; _cachedFilter = filter; _cachedTranscriptWrapWidth = maxW; _cachedTranscriptDatFont = datFont; _cachedTranscriptDebugFont = debugFont; _cachedTranscriptLines = result; TranscriptLayoutBuildCount++; return result; } public void Dispose() { if (_disposed) return; _disposed = true; } }