fix(client): restore retail interaction parity
All checks were successful
CI / linux-portable (push) Successful in 3m27s
CI / windows-gate (push) Successful in 6m42s
CI / release (push) Successful in 2m12s

Harden keyboard and camera routing, inventory and vendor interactions, chat/emotes, relog portal flow, and paperdoll rendering. Add retail research, connected gate coverage, and release-gate validation.
This commit is contained in:
Erik 2026-08-26 20:45:11 +02:00
parent 0c699240e0
commit f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions

View file

@ -96,7 +96,8 @@ internal sealed record InteractionRetainedUiDependencies(
Func<AcDream.Core.World.DerethDateTime.Calendar> CurrentCalendar,
AcDream.App.Rendering.Packs.RenderPackCatalogSource? RenderPackCatalog = null,
Func<AcDream.App.Rendering.Packs.RenderPackDiagnosticsSnapshot>?
RenderPackDiagnostics = null)
RenderPackDiagnostics = null,
string? ScreenshotsDirectory = null)
{
public RuntimeActionState Actions => Runtime.ActionOwner;
@ -388,9 +389,15 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
container,
placement,
amount),
sendStackableMerge: (source, target, amount) =>
session.CurrentSession?.SendStackableMerge(source, target, amount),
requestExternalContainer: guid =>
{
d.Inventory.ExternalContainers.RequestOpen(guid);
ClientObject? container = d.Inventory.Objects.Get(guid);
bool isCorpse = container is not null
&& ((PublicWeenieFlags)(container.PublicWeenieBitfield ?? 0u)
& PublicWeenieFlags.Corpse) != 0;
d.Inventory.ExternalContainers.RequestOpen(guid, isCorpse);
},
requestUse: selection.RequestUse,
// Slice 6.3: ItemInteractionController.TryBuy owns the
@ -663,16 +670,20 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
CharacterKey: () => d.Settings.ActiveToonKey,
ScreenSize: () => (d.Window.Size.X, d.Window.Size.Y));
void ProbeLog(string message) => d.Log("[UI-PROBE] " + message);
FrameScreenshotController? screenshots = null;
if (d.Options.UiProbeEnabled
&& d.Options.AutomationArtifactDirectory is { } artifactDirectory)
{
screenshots = new FrameScreenshotController(
d.BackbufferReader,
Path.Combine(artifactDirectory, "screenshots"),
ProbeLog,
d.RenderPackDiagnostics);
}
string screenshotDirectory =
d.Options.UiProbeEnabled
&& d.Options.AutomationArtifactDirectory is { } artifactDirectory
? Path.Combine(artifactDirectory, "screenshots")
: !string.IsNullOrWhiteSpace(d.ScreenshotsDirectory)
? d.ScreenshotsDirectory
: Path.Combine(
Path.GetDirectoryName(d.KeyBindingsFilePath)!,
"screenshots");
var screenshots = new FrameScreenshotController(
d.BackbufferReader,
screenshotDirectory,
ProbeLog,
d.RenderPackDiagnostics);
checkpoint(InteractionRetainedUiCompositionPoint.UiProbeCreated);
var assets = new RetailUiAssets(
@ -1157,11 +1168,10 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
late.GameRuntime.CharacterSelectionConfirmDelete,
late.GameRuntime.CharacterSelectionRestore,
late.GameRuntime.CharacterSelectionCancel,
// Campaign LA gate round 2 finding 1: the SAME
// window-close path GameplayInputCommandController's
// Escape fallback uses (IGameplayWindowCommands.Close
// /GameplayWindowCommands wrap this same d.Window.Close
// delegate) — no separate exit path.
// Campaign LA gate round 2 finding 1: the character
// selection screen's Exit button uses the ordinary host
// close path. Gameplay Escape is independent: retail
// clears selection or toggles the Gameplay Options page.
d.Window.Close),
// Campaign CC slice CC4: same late-bound generation-capturing
// seam as CharacterSelection above. RequestExit here is a
@ -1203,7 +1213,24 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
RandomizeAppearance: late.GameRuntime.CharacterCreationRandomizeAppearance,
RandomizeClothing: late.GameRuntime.CharacterCreationRandomizeClothing,
GetSkillScore: chargenSkillScoreResolver.Resolve,
OpenOnStart: d.Options.OpenCharacterCreationOnStart));
OpenOnStart: d.Options.OpenCharacterCreationOnStart),
CaptureScreenshot: () =>
{
if (screenshots.TryRequestRetailScreenshot(
out string path,
out string error))
{
d.Communication.AddText(
$"Screenshot saved to {path}",
RetailLogTextType.ClientLocal);
}
else
{
d.Communication.AddText(
$"Screenshot failed: {error}",
RetailLogTextType.ClientLocal);
}
});
RetailUiRuntime runtime = lease.Mount(
() => RetailUiRuntime.CreateUninitialized(bindings));
checkpoint(InteractionRetainedUiCompositionPoint.UiRuntimeMounted);

View file

@ -63,6 +63,7 @@ internal sealed record LivePresentationDependencies(
CellVisibility CellVisibility,
LiveWorldOriginState WorldOrigin,
LocalPlayerIdentityState PlayerIdentity,
ChaseCameraInputState ChaseCameraInput,
PointerPositionState PointerPosition,
PlayerApproachCompletionState PlayerApproachCompletions,
GameRenderResourceLifetime RenderResourceLifetime,
@ -808,7 +809,12 @@ internal sealed class LivePresentationCompositionPhase
d.RetailAlphaQueue,
alphaScratchBudgets.DispatcherBytes,
foundation.TerrainAtlas?.BuildingDetailTexture ?? default,
() => d.Settings.DisplayPreview.BuildingDetailTextures),
() => d.Settings.DisplayPreview.BuildingDetailTextures,
serverGuid => serverGuid != 0u
&& serverGuid == d.PlayerIdentity.ServerGuid
? d.ChaseCameraInput.Retail?.PlayerTranslucency
?? (d.ChaseCameraInput.Legacy?.IsInHead == true ? 1f : 0f)
: 0f),
static value => value.Dispose());
var selectionQuery = new WorldSelectionQuery(
liveEntities,
@ -845,7 +851,11 @@ internal sealed class LivePresentationCompositionPhase
localEntityId =>
d.EffectPoses.TryGetRootPose(localEntityId, out Matrix4x4 childRoot)
? childRoot
: null);
: null,
hasOpenedCorpse:
d.Runtime.InventoryOwner.ExternalContainers.HasCorpseBeenOpened,
combatMode: () => d.Runtime.ActionOwner.Combat.CurrentMode,
isFellow: guid => d.Runtime.Fellowship.TryGetMember(guid, out _));
var radarSnapshotProvider = new RadarSnapshotProvider(
d.EntityObjects.Objects,
liveEntities,
@ -876,7 +886,12 @@ internal sealed class LivePresentationCompositionPhase
() => d.PlayerController.Controller,
d.PlayerApproachCompletions),
d.Toast,
d.PlayerApproachCompletions);
d.PlayerApproachCompletions,
splitStack: guid =>
interaction.RetainedUi?.Runtime.SelectedObjectController?
.FocusSplitStackEntry(guid) ?? false,
fellowshipMembers: () =>
d.Runtime.Fellowship.GetMembers().Select(static member => member.Guid));
selectionInteractionSource.Bind(selectionInteractions);
bindings.Adopt(
"world selection",

View file

@ -1169,6 +1169,7 @@ internal sealed class SessionPlayerCompositionPhase
live.SelectionInteractions),
new LiveSessionWorldRuntime(
content.Dats,
d.DatLock,
content.Audio?.Engine is { } sessionAudioEngine
? new AcDream.App.Audio.WorldAudioSessionGate(
sessionAudioEngine,
@ -1279,14 +1280,10 @@ internal sealed class SessionPlayerCompositionPhase
new RetainedGameplayWindowCommands(
interaction.RetainedUi?.Runtime),
runtimeDiagnostics,
new PlayerModeGameplayCommands(
d.PlayerMode,
playerMode),
new PlayerModeGameplayCommands(playerMode),
new ItemTargetModeCommands(interaction.ItemInteraction),
new GameplayCameraModeCommands(host.CameraController),
gameRuntime,
gameRuntime.Combat,
new GameplayWindowCommands(d.Window.Close),
toggleAudioMute: content.Audio?.Engine is { } audioEngine
? () =>
{
@ -1305,6 +1302,7 @@ internal sealed class SessionPlayerCompositionPhase
gameRuntime,
gameRuntime.Selection,
gameRuntime.MovementCommands,
gameRuntime.CharacterCommands,
commands);
GameplayInputActionRouter gameplayActions =
GameplayInputActionRouter.Create(

View file

@ -18,8 +18,8 @@ namespace AcDream.App.Composition;
/// <c>UiHost</c>/<c>UiRoot</c> tree — D1) instead of a new
/// <c>IPanelRenderer</c> implementation, and its OP9 closeout retired the
/// unrendered ImGui-era SettingsPanel/SettingsVM outright. Keybind remapping
/// is Campaign OP slice OP8's Configure Keyboard screen, persisting to
/// keybinds.json (not retail's <c>.keymap</c> format — register row AP-202).
/// is Campaign OP slice OP8's Configure Keyboard screen, persisting retail
/// <c>*.keymap</c> profiles with <c>keybinds.json</c> as the host-command mirror.
/// </summary>
internal sealed record SettingsDevToolsResult(
AcDream.UI.Abstractions.Settings.QualitySettings ResolvedQuality)