fix(ui,runtime): OP4 re-review residuals R1-R4 (coordinator pass) — OP4 CLOSED
R1: the timestamp prefix moves from ChatLog.Append (which stamped the
stored BODY, rendering 'Alice says, "13:05:09 hi"') to ChatVM's display
composition — FormatTimestampPrefix(entry.Received) prepends the COMPOSED
line, matching retail's separate-leading-string model (fprintf("%ls%ls",
ts, text) @0x00563e5b; AddTextToScroll receives composed lines). The
prefix renders entry.Received in LOCAL time (retail strftime), invariant
literal colons. The ten defect-pinning test cases across
ChatLogTests/RuntimeCommunicationStateTests are rewritten to pin the
corrected contract (stored bodies stay clean; the composed line carries
the stamp outside the quotes — ChatVMTests).
R2: open option-bearing panels converge on every PlayerDescription seed:
OptionPage.ReloadFromLive (per-row live re-read + gating re-eval, NO
AfterApply flush — the seed just cleared the dirty module),
OptionsPanelController.OnServerOptionsSeeded (active page),
CombatUiController.OnServerOptionsSeeded (SyncControls), wired through
RuntimeSettingsController.ServerOptionsSeeded from the same factory hook
LockUI already uses. Retail cannot reach this state (its panels close
across login); the adaptation exists because retained panels survive the
session boundary — documented at the seam.
R3: tests drive the refresh widget push (model AND checkbox converge) and
ReloadFromLive's no-flush contract. R4: AP-196 addendum names the
headless AutoRepeatAttack false->true effective-default flip and the
characterOptions escape hatch.
Full Release suite: 13,083 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e71e5a9614
commit
ac0304dcf0
13 changed files with 216 additions and 114 deletions
|
|
@ -873,6 +873,15 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
RetailUiRuntime runtime = lease.Mount(
|
||||
() => RetailUiRuntime.CreateUninitialized(bindings));
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.UiRuntimeMounted);
|
||||
// OP4 re-review R2: open option-bearing panels converge on every
|
||||
// PlayerDescription seed (login + reconnect), closing the
|
||||
// stale-rows/stale-baseline window a retained panel left open
|
||||
// across the session boundary would otherwise hold.
|
||||
d.Settings.ServerOptionsSeeded = () =>
|
||||
{
|
||||
runtime.OptionsPanelController?.OnServerOptionsSeeded();
|
||||
runtime.CombatUiController?.OnServerOptionsSeeded();
|
||||
};
|
||||
inventoryContainer = late.InventoryContainer.Bind(runtime);
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.InventoryContainerBound);
|
||||
|
||||
|
|
|
|||
|
|
@ -338,6 +338,10 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
_interaction.Settings.SyncChatFromServerOptions(options2);
|
||||
_interaction.Settings.SetUiLocked(
|
||||
_domain.Character.Options.GetOptionBit(CharacterOptionId.LockUI));
|
||||
// OP4 re-review R2: open option-bearing panels re-read live
|
||||
// bits at every seed (login + reconnect) — see
|
||||
// RuntimeSettingsController.ServerOptionsSeeded.
|
||||
_interaction.Settings.NotifyServerOptionsSeeded();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -633,6 +633,21 @@ internal sealed class RuntimeSettingsController :
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OP4 re-review R2 (2026-08-11): assigned by the retained-UI
|
||||
/// composition; fired (via <see cref="NotifyServerOptionsSeeded"/>) from
|
||||
/// the same PlayerDescription seed hook that drives
|
||||
/// <see cref="SyncChatFromServerOptions"/>/<c>SetUiLocked</c>, so OPEN
|
||||
/// option-bearing panels (the Options panel's active page, the Combat
|
||||
/// panel's three LEDs) re-read live bits at every seed — login AND
|
||||
/// reconnect — instead of holding stale rows and a stale undo baseline
|
||||
/// until their next show.
|
||||
/// </summary>
|
||||
public Action? ServerOptionsSeeded { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ServerOptionsSeeded"/>
|
||||
public void NotifyServerOptionsSeeded() => ServerOptionsSeeded?.Invoke();
|
||||
|
||||
private void SaveCharacter(CharacterSettings character)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -200,6 +200,12 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
|
||||
private void OnAttackStateChanged() => SyncControls();
|
||||
|
||||
/// <summary>OP4 re-review R2: a fresh PlayerDescription seed landed —
|
||||
/// the three option LEDs re-read the live bits so an OPEN combat panel
|
||||
/// converges with the Character tab instead of waiting for the next
|
||||
/// show/mode/attack-state change.</summary>
|
||||
public void OnServerOptionsSeeded() => SyncControls();
|
||||
|
||||
private void SyncControls()
|
||||
{
|
||||
_powerControl.SetScalarPosition(_attacks.DesiredPower);
|
||||
|
|
|
|||
|
|
@ -494,6 +494,25 @@ public sealed class OptionPage
|
|||
/// (re)opening): applies + commits, same as <see cref="Apply"/>.</summary>
|
||||
public void OnShown() => Apply();
|
||||
|
||||
/// <summary>
|
||||
/// OP4 re-review R2 (2026-08-11): a fresh <c>PlayerDescription</c> seed
|
||||
/// replaced the live option words while this page may be VISIBLE —
|
||||
/// re-read every row's (current, saved) from the live source WITHOUT
|
||||
/// <see cref="Apply"/>'s flush (the seed just cleared the dirty module;
|
||||
/// there is nothing to flush, and an <see cref="AfterApply"/> publication
|
||||
/// here would be spurious). Retail cannot reach this state — its panels
|
||||
/// are closed during login/reconnect — so this adaptation exists only
|
||||
/// because acdream's retained panels survive the session boundary; the
|
||||
/// stale (current, saved) it clears would otherwise let Reset restore
|
||||
/// pre-reconnect values over the new character's server truth.
|
||||
/// </summary>
|
||||
public void ReloadFromLive()
|
||||
{
|
||||
foreach (IOptionRow row in _rows)
|
||||
row.SaveCurrentValue();
|
||||
OnOptionChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary><c>PlayerOptionPage::OnVisibilityChanged(false)</c> — the page
|
||||
/// became hidden (a tab switch away, or the window closing): reverts
|
||||
/// uncommitted edits, same as <see cref="Reset"/>.</summary>
|
||||
|
|
|
|||
|
|
@ -310,6 +310,16 @@ public sealed class OptionsPanelController : IRetainedPanelController
|
|||
page.OnShown();
|
||||
}
|
||||
|
||||
/// <summary>OP4 re-review R2: a fresh PlayerDescription seed landed —
|
||||
/// re-read the ACTIVE page's rows from live state (flush-free; see
|
||||
/// <see cref="OptionPage.ReloadFromLive"/>). Hidden pages re-read on
|
||||
/// their next <see cref="OptionPage.OnShown"/> as always.</summary>
|
||||
public void OnServerOptionsSeeded()
|
||||
{
|
||||
if (_pages.TryGetValue(_tabPanel.ActivePageElementId, out OptionPage? page))
|
||||
page.ReloadFromLive();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
|
|
|||
|
|
@ -435,14 +435,14 @@ public sealed class ChatLog
|
|||
|
||||
private void Append(ChatEntry entry)
|
||||
{
|
||||
if (DisplayTimestampsSource?.Invoke() == true)
|
||||
{
|
||||
entry = entry with
|
||||
{
|
||||
Text = FormatTimestampPrefix() + entry.Text,
|
||||
};
|
||||
}
|
||||
|
||||
// OP4 re-review R1 (2026-08-11): the timestamp prefix does NOT touch
|
||||
// entry.Text here. Retail composes the display line FIRST and carries
|
||||
// the timestamp as a SEPARATE leading string at display time
|
||||
// (AddTextToScroll @0x00563C50 receives already-composed lines;
|
||||
// fprintf("%ls%ls\n", ts, text) @0x00563e5b) — prefixing the raw body
|
||||
// put the stamp INSIDE the quotes of composed kinds
|
||||
// ('Alice says, "13:05:09 hi"'). ChatVM's display composition applies
|
||||
// FormatTimestampPrefix(entry.Received) around FormatEntry instead.
|
||||
_buffer.Enqueue(entry);
|
||||
while (_buffer.Count > _maxEntries)
|
||||
_buffer.TryDequeue(out _);
|
||||
|
|
@ -465,8 +465,9 @@ public sealed class ChatLog
|
|||
/// <c>GenericQualitiesData::InqString(m_pPlayerOptionsData, 1, ...)</c>
|
||||
/// — see register row AP-197.
|
||||
/// </summary>
|
||||
private static string FormatTimestampPrefix() =>
|
||||
DateTime.Now.ToString(@"H\:mm\:ss ", CultureInfo.InvariantCulture);
|
||||
public static string FormatTimestampPrefix(DateTime receivedUtc) =>
|
||||
receivedUtc.ToLocalTime().ToString(
|
||||
@"H\:mm\:ss ", CultureInfo.InvariantCulture);
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -236,10 +236,16 @@ public sealed class ChatVM : IDisposable
|
|||
int count = snap.Length - start;
|
||||
if (count <= 0) return Array.Empty<string>();
|
||||
|
||||
// OP4 re-review R1: read the option once per snapshot so every line
|
||||
// in one frame renders consistently.
|
||||
bool timestamps = _log.DisplayTimestampsSource?.Invoke() == true;
|
||||
var lines = new string[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
lines[i] = FormatEntry(snap[start + i]);
|
||||
var entry = snap[start + i];
|
||||
lines[i] = timestamps
|
||||
? ChatLog.FormatTimestampPrefix(entry.Received) + FormatEntry(entry)
|
||||
: FormatEntry(entry);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
|
@ -338,12 +344,20 @@ public sealed class ChatVM : IDisposable
|
|||
int count = snap.Length - start;
|
||||
if (count <= 0) return Array.Empty<FormattedLine>();
|
||||
|
||||
// OP4 re-review R1: retail prepends the timestamp to the COMPOSED
|
||||
// display line (a separate leading string — fprintf("%ls%ls", ts,
|
||||
// text) @0x00563e5b), never to the message body, so tells/says render
|
||||
// '13:05:09 Alice says, "hi"' and not 'Alice says, "13:05:09 hi"'.
|
||||
bool timestamps = _log.DisplayTimestampsSource?.Invoke() == true;
|
||||
var lines = new FormattedLine[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entry = snap[start + i];
|
||||
string text = FormatEntry(entry);
|
||||
if (timestamps)
|
||||
text = ChatLog.FormatTimestampPrefix(entry.Received) + text;
|
||||
lines[i] = new FormattedLine(
|
||||
Text: FormatEntry(entry),
|
||||
Text: text,
|
||||
Kind: entry.Kind,
|
||||
CombatKind: entry.CombatKind,
|
||||
LogTextType: entry.LogTextType);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue