fix(chat): Campaign CH user-gate round 1 — jump-in-air edge, portal cue cadence, wrap/prefix/color fixes
The user tested Campaign CH's CODE-COMPLETE build live and reported ten defects (docs/plans/2026-08-09-chat-parity-campaign.md, "User gate — round 1"). Items A-G are fixed here; the remaining three (extra chat windows on 1/2/3/4, resize working in only one corner, transparency/ artifacts) are out of scope for a fix and filed as slice CH6. A. Jump-in-air refusal never fired live: the jump block only ever evaluated input.Jump inside the grounded-charge or already-charging branches. PlayerMovementController now detects the press RISING EDGE while airborne and reports WeenieError.NotGrounded once per press, leaving the grounded charge/fire path untouched. B. ChatVM's invented "[System] " prefix is dropped — retail prints system text bare. [Popup] is unchanged (AP-175). C. SpewBoxController's color is now the user-pinned exact value (1, 1, 0.247, 1), the same bright yellow as an incoming Tell. Register row AP-178 updated: color CLOSES, size/position/font stay open per the user's live report that they still differ. D. Closes #329: PortalTunnelPresentation now emits the portal wait cue unconditionally on every rotation-segment boundary, matching gmSmartBoxUI::UseTime's decompiled else-arm exactly instead of gating on a 5-second hold local transits never reached. PortalWaitNotice Controller now renders it in the same pinned yellow as item C. Register row AP-150 retired. E. Closes #362: new ClientCommandResponses.cs parses and renders the four previously-unhandled inbound GameEvents (ChannelIndex, ChannelList, AvailableHouses, AllegianceInfoResponse), each ported line-for-line from the named-retail decomp's inbound handlers. Register row TS-70 retired. F. ChatWindowController.WrapText now splits on embedded '\n'/'\r\n' first, then word-wraps each segment independently — server text like /help's reply no longer collapses onto one line. G. The chat input field's right edge no longer holds a fixed absolute pixel position across a window resize; Bind now upgrades it to retail edge-mode 1 (UiLayoutPolicy) or the AnchorEdges.Right stretch fallback so it tracks the window's client width instead of overflowing past a narrower resize. Full Release suite: 12,247 passed / 4 skipped / 0 failed (baseline 12,221/4/0 + 26 new tests across items A, E, F, G). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d1c1368a5e
commit
47e40900f3
17 changed files with 1428 additions and 81 deletions
|
|
@ -238,6 +238,38 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
c.Input.SpriteResolve = resolve;
|
||||
c.Input.OnSubmit = text => ChatCommandRouter.Submit(text, vm, busProvider(), c._activeChannel);
|
||||
|
||||
// Campaign CH user-gate round 1 (item G): the imported field's right
|
||||
// edge otherwise holds a FIXED absolute pixel position across a
|
||||
// window resize — retail edge-mode 0's "frozen at current" fallback
|
||||
// (UiLayoutPolicy.ApplyFar), or the AnchorEdges default (Left|Top,
|
||||
// no stretch) when this field imported without a LayoutPolicy at
|
||||
// all. ReflowInputRow below only repositions Left/Width at bind
|
||||
// time and on channel change; nothing re-runs it on a plain window
|
||||
// RESIZE, so shrinking the window below its authored width left the
|
||||
// input's right edge frozen past the new, narrower client area —
|
||||
// the reported overflow. Retail edge-mode 1 on a FAR edge
|
||||
// ("originalEdge + parentDelta", UiLayoutPolicy.ApplyFar) keeps a
|
||||
// CONSTANT MARGIN from the parent's right edge instead, so the
|
||||
// field's right edge now tracks every resize, not just
|
||||
// bind/channel-change moments; the compatibility AnchorEdges.Right
|
||||
// stretch is the equivalent programmatic-widget fallback. Only the
|
||||
// right-edge behavior changes — Left/Top/Bottom stay whatever the
|
||||
// DAT authored (or the AnchorEdges default).
|
||||
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 ────────
|
||||
// The factory now builds the Type-11 track element (0x10000012) as a UiScrollbar
|
||||
// directly. Find it, bind it in place — no remove/add needed.
|
||||
|
|
@ -508,9 +540,40 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
/// </summary>
|
||||
public static IEnumerable<string> WrapText(string text, float maxW, Func<string, float> measure)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || maxW <= 0f || measure(text) <= maxW)
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
yield return text ?? string.Empty;
|
||||
yield return string.Empty;
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Campaign CH user-gate round 1 (item F): server text (e.g. /help's
|
||||
// reply) carries embedded '\n's. This function used to hand the
|
||||
// WHOLE blob — newlines and all — to the single early-out below,
|
||||
// rendering multi-line text as one UiText.Line with literal newline
|
||||
// characters in it instead of one rendered line per segment. Split
|
||||
// on '\n' FIRST (normalizing "\r\n"/bare "\r" the same way), then
|
||||
// word-wrap each segment independently; the early-out is now scoped
|
||||
// to one already-newline-free segment, so it only ever collapses a
|
||||
// single-segment text to one line, never a multi-line one.
|
||||
string normalized = text.Replace("\r\n", "\n").Replace('\r', '\n');
|
||||
foreach (string segment in normalized.Split('\n'))
|
||||
{
|
||||
foreach (string frag in WrapSingleLine(segment, maxW, measure))
|
||||
yield return frag;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Greedy word-wrap for a single, already newline-free line. Split out of
|
||||
/// <see cref="WrapText"/> (Campaign CH user-gate round 1, item F) so the
|
||||
/// multi-segment split there can call this once per '\n'-delimited
|
||||
/// segment without re-deriving the per-line wrap algorithm.
|
||||
/// </summary>
|
||||
private static IEnumerable<string> WrapSingleLine(string text, float maxW, Func<string, float> measure)
|
||||
{
|
||||
if (text.Length == 0 || maxW <= 0f || measure(text) <= maxW)
|
||||
{
|
||||
yield return text;
|
||||
yield break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,16 @@ namespace AcDream.App.UI;
|
|||
/// </summary>
|
||||
internal sealed class PortalWaitNoticeController : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Register row AP-150/AP-178: CH user-gate round 1 (2026-08-09) PINNED
|
||||
/// this — the user confirmed live, side-by-side against retail, that
|
||||
/// this notice renders in the same bright yellow as an incoming Tell
|
||||
/// (<c>0x81C4C8</c>, <c>RetailChatColorTable.Yellow</c> =
|
||||
/// <c>(1, 1, 0.247, 1)</c>), not white. Same exact value as the
|
||||
/// SpewBox's pinned colour (<see cref="AcDream.App.UI.SpewBoxController"/>).
|
||||
/// </summary>
|
||||
private static readonly Vector4 RetailWaitCueColor = new(1f, 1f, 0.247f, 1f);
|
||||
|
||||
private readonly UiRoot _root;
|
||||
private readonly UiText _text;
|
||||
private UiText.Line[] _lines = [];
|
||||
|
|
@ -32,7 +42,7 @@ internal sealed class PortalWaitNoticeController : IDisposable
|
|||
OneLine = true,
|
||||
ClickThrough = true,
|
||||
ZOrder = int.MaxValue,
|
||||
DefaultColor = Vector4.One,
|
||||
DefaultColor = RetailWaitCueColor,
|
||||
Visible = false,
|
||||
};
|
||||
_text.LinesProvider = () => _lines;
|
||||
|
|
@ -49,7 +59,7 @@ internal sealed class PortalWaitNoticeController : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
_lines = [new UiText.Line(message, Vector4.One)];
|
||||
_lines = [new UiText.Line(message, RetailWaitCueColor)];
|
||||
_text.Visible = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -106,23 +106,22 @@ internal sealed class SpewBoxController : IDisposable
|
|||
private const float SpewBoxHeight = 72f;
|
||||
|
||||
/// <summary>
|
||||
/// Register row AP-178 (colour): retail's authored colour for THIS
|
||||
/// element remains unresolved — the LayoutDesc dump (see class remarks)
|
||||
/// found only two direct-state properties on the SpewBox element/ListBox
|
||||
/// (a bool at <c>0x3B</c> and the <c>MaxConcurrentItems</c> integer at
|
||||
/// <c>0x10000028</c>); no colour property surfaced in that direct-state
|
||||
/// dump, and the per-<c>UIStateId</c> <c>States</c> dictionary (hover/
|
||||
/// pressed/etc. variants, which could carry it) was not walked this
|
||||
/// pass. The chat colour table's <c>0x1A</c> entry
|
||||
/// (<c>colorBrightRed</c>) is explicitly NOT this — retail's own
|
||||
/// Register row AP-178 (colour): CH user-gate round 1 (2026-08-09)
|
||||
/// PINNED this — the user confirmed live, side-by-side against retail,
|
||||
/// that the on-screen SpewBox text is the same bright yellow as an
|
||||
/// incoming Tell (<c>0x81C4C8</c>, <c>RetailChatColorTable.Yellow</c> =
|
||||
/// <c>(1, 1, 0.247, 1)</c>). The chat colour table's <c>0x1A</c> entry
|
||||
/// (<c>colorBrightRed</c>) is still explicitly NOT this — retail's own
|
||||
/// <c>BuildChatColorLookupTable</c> writes to <c>ChatInterface::m_chatLog</c>,
|
||||
/// a completely different element tree the SpewBox never touches
|
||||
/// (research doc §3.2.3). This warm-yellow placeholder follows the
|
||||
/// user's own recollection of the retail SpewBox's colour (unconfirmed
|
||||
/// by any decompiled or DAT-authored source) rather than an arbitrary
|
||||
/// choice.
|
||||
/// (research doc §3.2.3); the LayoutDesc dump (see class remarks) also
|
||||
/// never surfaced a colour property for this element. The exact retail
|
||||
/// value simply happens to coincide with the Tell colour, per the user's
|
||||
/// live observation. SIZE/POSITION/FONT remain OPEN — the user reports
|
||||
/// all three still differ from retail; user gate round 1: differs,
|
||||
/// iterating.
|
||||
/// </summary>
|
||||
private static readonly Vector4 SpewBoxColor = new(1f, 1f, 0.4f, 1f);
|
||||
private static readonly Vector4 SpewBoxColor = new(1f, 1f, 0.247f, 1f);
|
||||
|
||||
private readonly UiRoot _root;
|
||||
private readonly UiText _text;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue