refactor(app): compose interaction and retained UI startup
This commit is contained in:
parent
f663b04a54
commit
aa6ffa5176
15 changed files with 2157 additions and 467 deletions
|
|
@ -19,7 +19,8 @@ public sealed class GameWindow :
|
|||
IGameWindowHostInputCameraPublication,
|
||||
IGameWindowContentEffectsAudioPublication,
|
||||
IGameWindowSettingsDevToolsPublication,
|
||||
IGameWindowWorldRenderPublication
|
||||
IGameWindowWorldRenderPublication,
|
||||
IGameWindowInteractionRetainedUiPublication
|
||||
{
|
||||
private static double ClientTimerNow() =>
|
||||
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
|
|
@ -337,8 +338,12 @@ public sealed class GameWindow :
|
|||
public readonly AcDream.Core.Spells.Spellbook SpellBook = null!;
|
||||
public readonly AcDream.Core.Items.ClientObjectTable Objects = new();
|
||||
/// <summary>Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).</summary>
|
||||
public IReadOnlyList<AcDream.Core.Items.ShortcutEntry> Shortcuts { get; private set; }
|
||||
= System.Array.Empty<AcDream.Core.Items.ShortcutEntry>();
|
||||
private readonly ShortcutSnapshotState _shortcutSnapshots = new();
|
||||
public IReadOnlyList<AcDream.Core.Items.ShortcutEntry> Shortcuts
|
||||
{
|
||||
get => _shortcutSnapshots.Items;
|
||||
private set => _shortcutSnapshots.Items = value;
|
||||
}
|
||||
// Issue #5 — caches CreatureProfile.{Stamina, Mana, *Max} from
|
||||
// PlayerDescription so the Vitals HUD can render those bars.
|
||||
// Issue #6 — wired to SpellBook so GetMaxApprox folds enchantment
|
||||
|
|
@ -356,6 +361,8 @@ public sealed class GameWindow :
|
|||
private AcDream.App.UI.UiHost? _uiHost;
|
||||
private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime;
|
||||
private readonly AcDream.App.UI.RetailUiRuntimeLease _retailUiLease = new();
|
||||
private InteractionUiLateBindings? _interactionUiLateBindings;
|
||||
private readonly DeferredRenderFrameDiagnosticsSource _uiFrameDiagnostics = new();
|
||||
private readonly AcDream.App.Combat.CombatAttackOperationsSlot
|
||||
_combatAttackOperations = new();
|
||||
private readonly AcDream.App.Combat.CombatFeedbackSlot
|
||||
|
|
@ -427,9 +434,7 @@ public sealed class GameWindow :
|
|||
}
|
||||
// Retail Default_CharacterOption (acclient.h:3434). PlayerDescription
|
||||
// replaces this before any in-world drag can normally occur.
|
||||
private AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1
|
||||
_characterOptions1 =
|
||||
AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1.Default;
|
||||
private readonly PlayerCharacterOptionsState _characterOptions = new();
|
||||
private readonly AcDream.App.Physics.LocalPlayerShadowState _localPlayerShadow = new();
|
||||
|
||||
private readonly AcDream.App.Input.LocalPlayerSkillState _localPlayerSkills = new();
|
||||
|
|
@ -882,6 +887,43 @@ public sealed class GameWindow :
|
|||
SamplerCache value) =>
|
||||
PublishCompositionOwner(ref _samplerCache, value, "sampler cache");
|
||||
|
||||
void IGameWindowInteractionRetainedUiPublication.PublishInteractionRetainedUi(
|
||||
InteractionRetainedUiResult result)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
if (_combatAttackController is not null
|
||||
|| _combatTargetController is not null
|
||||
|| _externalContainerLifecycle is not null
|
||||
|| _itemInteractionController is not null
|
||||
|| _interactionUiLateBindings is not null
|
||||
|| _uiHost is not null
|
||||
|| _retailUiRuntime is not null
|
||||
|| _retailChatVm is not null
|
||||
|| _characterSheetProvider is not null
|
||||
|| _magicRuntime is not null
|
||||
|| _frameScreenshots is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The GameWindow composition shell already owns interaction/UI state.");
|
||||
}
|
||||
|
||||
_combatAttackController = result.CombatAttack;
|
||||
_combatTargetController = result.CombatTarget;
|
||||
_externalContainerLifecycle = result.ExternalContainerLifecycle;
|
||||
_itemInteractionController = result.ItemInteraction;
|
||||
_interactionUiLateBindings = result.LateBindings;
|
||||
if (result.RetainedUi is { } retained)
|
||||
{
|
||||
_uiHost = retained.Host;
|
||||
_retailUiRuntime = retained.Runtime;
|
||||
_vitalsVm ??= retained.Vitals;
|
||||
_retailChatVm = retained.Chat;
|
||||
_characterSheetProvider = retained.CharacterSheet;
|
||||
_magicRuntime = retained.Magic;
|
||||
_frameScreenshots = retained.Screenshots;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PublishCompositionOwner<T>(
|
||||
ref T? destination,
|
||||
T value,
|
||||
|
|
@ -1011,6 +1053,9 @@ public sealed class GameWindow :
|
|||
_framebufferResize,
|
||||
Console.WriteLine),
|
||||
this).Compose(platform, hostInputCamera, contentEffectsAudio);
|
||||
Action<string>? compositionToast = settingsDevTools.DevTools is { } composedDevTools
|
||||
? text => composedDevTools.Debug.AddToast(text)
|
||||
: null;
|
||||
|
||||
const uint initialCenterLandblockId = 0xA9B4FFFFu;
|
||||
WorldRenderResult worldRender = new WorldRenderCompositionPhase(
|
||||
|
|
@ -1029,367 +1074,57 @@ public sealed class GameWindow :
|
|||
$"loading world view centered on " +
|
||||
$"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}");
|
||||
|
||||
// Retail ClientCombatSystem attack-request owner. This exists independently
|
||||
// of the retained UI so keyboard combat keeps the same press/hold/release
|
||||
// semantics even when the retail renderer is disabled.
|
||||
_combatAttackController = new AcDream.App.Combat.CombatAttackController(
|
||||
Combat,
|
||||
_combatAttackOperations);
|
||||
_combatTargetController = new AcDream.App.Combat.CombatTargetController(
|
||||
Combat,
|
||||
_selection,
|
||||
autoTarget: () => _runtimeSettings.Gameplay.AutoTarget,
|
||||
selectClosestTarget: () => SelectClosestCombatTarget(showToast: false));
|
||||
_externalContainerLifecycle = new AcDream.App.World.ExternalContainerLifecycleController(
|
||||
_externalContainers,
|
||||
Objects,
|
||||
guid => LiveSession?.SendNoLongerViewingContents(guid));
|
||||
|
||||
AcDream.App.Spells.MagicCatalog magicCatalog = _magicCatalog!;
|
||||
_itemInteractionController = new AcDream.App.UI.ItemInteractionController(
|
||||
Objects,
|
||||
playerGuid: () => _playerServerGuid,
|
||||
// ItemHolder::UseObject is the common policy owner, but world
|
||||
// activation still has to pass through the application-level
|
||||
// use adapter. It owns speculative turn/move, the close-range
|
||||
// deferred send, and ACE's far-range MoveTo callback. Calling
|
||||
// WorldSession directly here made keyboard R approach a corpse
|
||||
// without completing the same open transaction as double-click.
|
||||
sendUse: SendUse,
|
||||
sendExamine: g => LiveSession?.SendAppraise(g),
|
||||
sendUseWithTarget: (source, target) => LiveSession?.SendUseWithTarget(source, target),
|
||||
sendWield: (item, mask) => LiveSession?.SendGetAndWieldItem(item, mask),
|
||||
sendDrop: item => LiveSession?.SendDropItem(item),
|
||||
sendGive: (target, item, amount) =>
|
||||
LiveSession?.SendGiveObject(target, item, amount),
|
||||
dragOnPlayerOpensSecureTrade: () =>
|
||||
(_characterOptions1
|
||||
& AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1
|
||||
.DragItemOnPlayerOpensSecureTrade) != 0,
|
||||
toast: text => _debugVm?.AddToast(text),
|
||||
readyForInventoryRequest: () => _liveSessionHost?.IsInWorld == true,
|
||||
playerOnGround: GetDebugPlayerOnGround,
|
||||
inNonCombatMode: () => Combat.CurrentMode
|
||||
== AcDream.Core.Combat.CombatMode.NonCombat,
|
||||
combatState: Combat,
|
||||
sendChangeCombatMode: mode =>
|
||||
LiveSession?.SendChangeCombatMode(mode),
|
||||
isComponentPack: magicCatalog.IsComponentPack,
|
||||
placeInBackpack: SendPickUp,
|
||||
backpackContainerId: () =>
|
||||
_retailUiRuntime?.InventoryPanelController?.CurrentOpenContainerId
|
||||
?? _playerServerGuid,
|
||||
// Retail ItemHolder::DetermineUseResult treats children of the
|
||||
// current ground object as loot. Keep this late-bound because
|
||||
// ViewContents can replace/close the external container while
|
||||
// the retained controller remains alive.
|
||||
groundObjectId: () => _externalContainers.CurrentContainerId,
|
||||
sendSplitToWorld: (item, amount) =>
|
||||
LiveSession?.SendStackableSplitTo3D(item, amount),
|
||||
selectedObjectId: () => _selection.SelectedObjectId ?? 0u,
|
||||
stackSplitQuantity: _stackSplitQuantity,
|
||||
systemMessage: text => Chat.OnSystemMessage(text, 0x1Au),
|
||||
sendPutItemInContainer: (item, container, placement) =>
|
||||
LiveSession?.SendPutItemInContainer(item, container, placement),
|
||||
sendSplitToContainer: (item, container, placement, amount) =>
|
||||
LiveSession?.SendStackableSplitToContainer(
|
||||
item, container, placement, amount),
|
||||
requestExternalContainer: guid => _externalContainers.RequestOpen(guid));
|
||||
InteractionRetainedUiResult interactionUi =
|
||||
new InteractionRetainedUiCompositionPhase(
|
||||
new InteractionRetainedUiDependencies(
|
||||
_options,
|
||||
platform.Graphics,
|
||||
_window!,
|
||||
platform.Input,
|
||||
shadersDir,
|
||||
contentEffectsAudio.Dats,
|
||||
_datLock,
|
||||
worldRender.Foundation.TextureCache,
|
||||
worldRender.Foundation.DebugFont,
|
||||
_hostQuiescence,
|
||||
_retainedInputCapture,
|
||||
hostInputCamera.InputDispatcher,
|
||||
_runtimeSettings,
|
||||
Combat,
|
||||
_combatAttackOperations,
|
||||
_selection,
|
||||
_externalContainers,
|
||||
Objects,
|
||||
contentEffectsAudio.MagicCatalog,
|
||||
SpellBook,
|
||||
Chat,
|
||||
LocalPlayer,
|
||||
ItemMana,
|
||||
_stackSplitQuantity,
|
||||
_uiRegistry,
|
||||
_liveCombatModeCommands,
|
||||
_localPlayerIdentity,
|
||||
_playerControllerSlot,
|
||||
_localPlayerMode,
|
||||
_characterOptions,
|
||||
_shortcutSnapshots,
|
||||
viewPlane => new SelectionCameraSource(
|
||||
hostInputCamera.CameraController,
|
||||
_window!,
|
||||
viewPlane),
|
||||
_uiFrameDiagnostics,
|
||||
settingsDevTools.DevTools?.Vitals,
|
||||
compositionToast,
|
||||
ClientTimerNow,
|
||||
Console.WriteLine),
|
||||
_retailUiLease,
|
||||
this).Compose(
|
||||
hostInputCamera,
|
||||
contentEffectsAudio,
|
||||
settingsDevTools,
|
||||
worldRender);
|
||||
|
||||
AcDream.App.World.DeferredLiveEntityParentAcceptance? parentAcceptance = null;
|
||||
|
||||
// Phase D.2b retail-look retained UI (ACDREAM_RETAIL_UI=1).
|
||||
if (_options.RetailUi)
|
||||
{
|
||||
_vitalsVm ??= new AcDream.UI.Abstractions.Panels.Vitals.VitalsVM(Combat, LocalPlayer);
|
||||
_uiHost = _retailUiLease.AcquireHost(
|
||||
() => new AcDream.App.UI.UiHost(
|
||||
_gl,
|
||||
shadersDir,
|
||||
_debugFont,
|
||||
_hostQuiescence));
|
||||
_retainedInputCapture.Root = _uiHost.Root;
|
||||
_uiHost.Root.UiLocked = _runtimeSettings.Gameplay.LockUI;
|
||||
var cursorFeedbackController = new AcDream.App.UI.CursorFeedbackController(
|
||||
_itemInteractionController,
|
||||
// Retail UpdateCursorState (0x00564630) keys target-mode
|
||||
// valid/invalid off the SmartBox found object — the world
|
||||
// entity under the cursor, self included.
|
||||
worldTargetProvider: () => PickWorldGuidAtCursor(includeSelf: true) ?? 0u,
|
||||
combatModeProvider: () => Combat.CurrentMode);
|
||||
var retailCursorManager = new RetailCursorManager(_dats!, _datLock);
|
||||
_characterSheetProvider = new AcDream.App.UI.Layout.CharacterSheetProvider(
|
||||
Objects, LocalPlayer,
|
||||
playerGuid: () => _playerServerGuid,
|
||||
activeToonName: () => _runtimeSettings.ActiveToonKey,
|
||||
fallbackSheet: AcDream.App.Studio.SampleData.SampleCharacter,
|
||||
canSendRaise: () => _liveSessionHost?.IsInWorld == true,
|
||||
sendRaiseAttribute: (statId, cost) => LiveSession?.SendRaiseAttribute(statId, cost),
|
||||
sendRaiseVital: (statId, cost) => LiveSession?.SendRaiseVital(statId, cost),
|
||||
sendRaiseSkill: (statId, cost) => LiveSession?.SendRaiseSkill(statId, cost),
|
||||
sendTrainSkill: (statId, credits) => LiveSession?.SendTrainSkill(statId, credits));
|
||||
_magicRuntime = AcDream.App.Spells.MagicRuntime.Create(
|
||||
magicCatalog,
|
||||
SpellBook,
|
||||
Objects,
|
||||
selectedObject: () => _selection.SelectedObjectId,
|
||||
localPlayerId: () => _playerServerGuid,
|
||||
// SpellFormula::Randomize hashes the canonical account spelling
|
||||
// delivered by CharacterList.
|
||||
accountName: () => LiveSession?.Characters?.AccountName
|
||||
?? _options.LiveUser
|
||||
?? string.Empty,
|
||||
stopCompletely: _combatAttackOperations.PrepareAttackRequest,
|
||||
sendUntargeted: spellId => LiveSession?.SendCastUntargetedSpell(spellId),
|
||||
sendTargeted: (target, spellId) => LiveSession?.SendCastTargetedSpell(target, spellId),
|
||||
displayMessage: text => Chat.OnSystemMessage(text, 0x1Au),
|
||||
incrementBusy: () => _itemInteractionController.IncrementBusyCount(),
|
||||
canSend: () => _liveSessionHost?.IsInWorld == true);
|
||||
// Feed Silk input to the UiRoot tree so windows drag / close / select.
|
||||
// UiRoot consumes UI events; the game InputDispatcher (subscribed to the
|
||||
// same devices) is gated off via WantCaptureMouse/Keyboard above when the
|
||||
// pointer is over a widget — no double-handling.
|
||||
foreach (var m in _input!.Mice) _uiHost.WireMouse(m);
|
||||
foreach (var kb in _input!.Keyboards) _uiHost.WireKeyboard(kb);
|
||||
|
||||
var cache = _textureCache!;
|
||||
(uint, int, int) ResolveChrome(uint id)
|
||||
{
|
||||
uint t = cache.GetOrUploadRenderSurface(id, out int w, out int h);
|
||||
return (t, w, h);
|
||||
}
|
||||
|
||||
// Phase D.5.1 — icon composer for the toolbar shortcut slots.
|
||||
// Constructed once here so the closure below can capture it; needs
|
||||
// the same cache reference that ResolveChrome uses above.
|
||||
var iconComposer = new AcDream.App.UI.IconComposer(_dats!, cache);
|
||||
|
||||
// Phase D.2b — optional retail stylesheet. controls.ini lives under
|
||||
// the AC install (ACDREAM_AC_DIR); absent → source-verified fallback.
|
||||
var controls = _options.AcDir is { } acDir
|
||||
? AcDream.App.UI.ControlsIni.Load(System.IO.Path.Combine(acDir, "controls", "controls.ini"))
|
||||
: AcDream.App.UI.ControlsIni.Parse(string.Empty);
|
||||
// Phase D.2b — retail dat-font for the vitals numbers (Font 0x40000000,
|
||||
// Latin-1, 16px, outline atlas). Passed into the importer so the meter
|
||||
// number overlay renders through the dat-font two-pass blit; falls back to
|
||||
// the debug font only if it fails to load. Under _datLock like other reads.
|
||||
AcDream.App.UI.UiDatFont? vitalsDatFont;
|
||||
lock (_datLock)
|
||||
vitalsDatFont = AcDream.App.UI.UiDatFont.Load(_dats!, _textureCache!);
|
||||
var datFontCache = new System.Collections.Concurrent.ConcurrentDictionary<uint, AcDream.App.UI.UiDatFont?>();
|
||||
if (vitalsDatFont is not null)
|
||||
datFontCache.TryAdd(AcDream.App.UI.UiDatFont.DefaultFontId, vitalsDatFont);
|
||||
AcDream.App.UI.UiDatFont? ResolveDatFont(uint fontDid)
|
||||
=> datFontCache.GetOrAdd(fontDid, id =>
|
||||
{
|
||||
lock (_datLock)
|
||||
return AcDream.App.UI.UiDatFont.Load(_dats!, _textureCache!, id);
|
||||
});
|
||||
Console.WriteLine(vitalsDatFont is not null
|
||||
? "[D.2b] vitals dat-font 0x40000000 loaded for numeric overlay."
|
||||
: "[D.2b] vitals dat-font 0x40000000 unavailable — falling back to debug font.");
|
||||
|
||||
_uiHost.Root.Width = _window!.Size.X;
|
||||
_uiHost.Root.Height = _window.Size.Y;
|
||||
|
||||
var radarSnapshotProvider = new AcDream.App.UI.Layout.RadarSnapshotProvider(
|
||||
Objects,
|
||||
() => _visibleEntitiesByServerGuid,
|
||||
() => LastSpawns,
|
||||
playerGuid: () => _playerServerGuid,
|
||||
playerYawRadians: () => _playerController?.Yaw ?? 0f,
|
||||
playerCellId: () => _playerController?.CellId ?? 0u,
|
||||
selectedGuid: () => _selection.SelectedObjectId,
|
||||
coordinatesOnRadar: () => _runtimeSettings.Gameplay.CoordinatesOnRadar,
|
||||
uiLocked: () => _runtimeSettings.Gameplay.LockUI,
|
||||
playerEntities: () => _entitiesByServerGuid,
|
||||
spatialQuery: () => _worldState);
|
||||
var retailChatVm = new AcDream.UI.Abstractions.Panels.Chat.ChatVM(Chat, displayLimit: 200);
|
||||
_retailChatVm = retailChatVm;
|
||||
AcDream.UI.Abstractions.Panels.Settings.SettingsStore? layoutStore =
|
||||
_runtimeSettings.LayoutStore;
|
||||
AcDream.App.UI.RetailUiPersistenceBindings? persistence = layoutStore is null
|
||||
? null
|
||||
: new AcDream.App.UI.RetailUiPersistenceBindings(
|
||||
layoutStore,
|
||||
CharacterKey: () => _runtimeSettings.ActiveToonKey,
|
||||
ScreenSize: () => (_window.Size.X, _window.Size.Y));
|
||||
void UiProbeLog(string message) => Console.WriteLine("[UI-PROBE] " + message);
|
||||
|
||||
if (_options.UiProbeEnabled
|
||||
&& _options.AutomationArtifactDirectory is { } artifactDirectory)
|
||||
{
|
||||
_frameScreenshots = new AcDream.App.Diagnostics.FrameScreenshotController(
|
||||
_gl!,
|
||||
System.IO.Path.Combine(artifactDirectory, "screenshots"),
|
||||
UiProbeLog);
|
||||
_worldLifecycleAutomation =
|
||||
new AcDream.App.Diagnostics.WorldLifecycleAutomationController(
|
||||
() => _worldReveal?.Snapshot ?? default,
|
||||
() => _worldReveal?.PortalMaterializationCount ?? 0,
|
||||
CaptureWorldLifecycleResourceSnapshot,
|
||||
_frameScreenshots,
|
||||
artifactDirectory,
|
||||
UiProbeLog);
|
||||
}
|
||||
|
||||
var retailUiBindings = new AcDream.App.UI.RetailUiRuntimeBindings(
|
||||
Host: _uiHost,
|
||||
Assets: new AcDream.App.UI.RetailUiAssets(
|
||||
_dats!, _datLock, ResolveChrome, ResolveDatFont,
|
||||
vitalsDatFont, _debugFont, controls, iconComposer),
|
||||
Vitals: new AcDream.App.UI.VitalsRuntimeBindings(_vitalsVm),
|
||||
Chat: new AcDream.App.UI.ChatRuntimeBindings(
|
||||
retailChatVm,
|
||||
() => _liveSessionHost?.Commands
|
||||
?? AcDream.UI.Abstractions.NullCommandBus.Instance),
|
||||
Radar: new AcDream.App.UI.RadarRuntimeBindings(
|
||||
radarSnapshotProvider.BuildSnapshot,
|
||||
_selection,
|
||||
_runtimeSettings.SetUiLocked),
|
||||
Combat: new AcDream.App.UI.CombatRuntimeBindings(
|
||||
Combat,
|
||||
_combatAttackController,
|
||||
() => _runtimeSettings.Gameplay,
|
||||
_runtimeSettings.SetCombatGameplay),
|
||||
Magic: new AcDream.App.UI.MagicRuntimeBindings(
|
||||
SpellBook,
|
||||
_magicRuntime.Casting,
|
||||
Objects,
|
||||
() => _playerServerGuid,
|
||||
magicCatalog.Components,
|
||||
(type, icon, under, over, effects) =>
|
||||
iconComposer.GetIcon(type, icon, under, over, effects),
|
||||
(type, icon, under, over, effects) =>
|
||||
iconComposer.GetDragIcon(type, icon, under, over, effects),
|
||||
iconComposer.GetSpellIcon,
|
||||
iconComposer.GetSpellComponentIcon,
|
||||
_selection,
|
||||
magicCatalog.GetSpellLevel,
|
||||
guid => _selection.Select(
|
||||
guid, AcDream.Core.Selection.SelectionChangeSource.Inventory),
|
||||
UseItemByGuid,
|
||||
(tab, position, spellId) =>
|
||||
LiveSession?.SendAddSpellFavorite(spellId, position, tab),
|
||||
(tab, spellId) =>
|
||||
LiveSession?.SendRemoveSpellFavorite(spellId, tab),
|
||||
filters => LiveSession?.SendSpellbookFilter(filters),
|
||||
spellId => LiveSession?.SendRemoveSpell(spellId),
|
||||
(componentId, amount) =>
|
||||
LiveSession?.SendSetDesiredComponentLevel(componentId, amount),
|
||||
ClientTimerNow),
|
||||
JumpPowerbar: new AcDream.App.UI.JumpPowerbarRuntimeBindings(
|
||||
() => _playerController?.JumpCharge ?? default),
|
||||
Fps: new AcDream.App.UI.FpsRuntimeBindings(
|
||||
() => _renderFrameDiagnostics?.Snapshot.Fps ?? 60.0,
|
||||
// Retail displays either the user degrade bias or the live
|
||||
// distance-driven multiplier. acdream does not yet implement
|
||||
// adaptive distance degradation (tracked by TS-15), so its
|
||||
// effective multiplier is exactly 1.0.
|
||||
() => 1.0,
|
||||
() => _runtimeSettings.DisplayPreview.ShowFps),
|
||||
VividTarget: new AcDream.App.UI.Layout.VividTargetRuntimeBindings(
|
||||
_selection,
|
||||
() => _playerServerGuid,
|
||||
() => _runtimeSettings.Gameplay.VividTargetingIndicator,
|
||||
ResolveVividTargetInfo,
|
||||
GetSelectionCamera),
|
||||
Indicators: new AcDream.App.UI.IndicatorRuntimeBindings(
|
||||
SpellBook,
|
||||
Objects,
|
||||
() => _playerServerGuid,
|
||||
() => LocalPlayer.GetAttribute(
|
||||
AcDream.Core.Player.LocalPlayerState.AttributeKind.Strength)
|
||||
is { } strength ? (int?)strength.Current : null,
|
||||
() => LiveSession?.LinkStatus
|
||||
?? AcDream.Core.Net.LinkStatusSnapshot.Disconnected,
|
||||
ClientTimerNow,
|
||||
() => LiveSession?.RequestLinkStatusPing(),
|
||||
() => _window?.Close()),
|
||||
Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings(
|
||||
Objects,
|
||||
() => Shortcuts,
|
||||
(type, icon, under, over, effects) => iconComposer.GetIcon(type, icon, under, over, effects),
|
||||
(type, icon, under, over, effects) => iconComposer.GetDragIcon(type, icon, under, over, effects),
|
||||
UseItemByGuid,
|
||||
Combat,
|
||||
ItemMana,
|
||||
_liveCombatModeCommands.Toggle,
|
||||
_itemInteractionController,
|
||||
entry => LiveSession?.SendAddShortcut(entry),
|
||||
index => LiveSession?.SendRemoveShortcut(index),
|
||||
_selection,
|
||||
handler => Combat.HealthChanged += handler,
|
||||
handler => Combat.HealthChanged -= handler,
|
||||
IsHealthBarTarget,
|
||||
guid => Objects.Get(guid)?.GetAppropriateName(),
|
||||
Combat.GetHealthPercent,
|
||||
Combat.HasHealth,
|
||||
guid => (uint)(Objects.Get(guid)?.StackSize ?? 0),
|
||||
guid => LiveSession?.SendQueryHealth(guid),
|
||||
guid => LiveSession?.SendQueryItemMana(guid),
|
||||
() => _playerServerGuid,
|
||||
(item, container, placement) =>
|
||||
LiveSession?.SendPutItemInContainer(item, container, placement)),
|
||||
Character: new AcDream.App.UI.CharacterRuntimeBindings(_characterSheetProvider),
|
||||
Inventory: new AcDream.App.UI.InventoryRuntimeBindings(
|
||||
Objects,
|
||||
() => _playerServerGuid,
|
||||
(type, icon, under, over, effects) => iconComposer.GetIcon(type, icon, under, over, effects),
|
||||
(type, icon, under, over, effects) => iconComposer.GetDragIcon(type, icon, under, over, effects),
|
||||
() => LocalPlayer.GetAttribute(
|
||||
AcDream.Core.Player.LocalPlayerState.AttributeKind.Strength)
|
||||
is { } strength ? (int?)strength.Current : null,
|
||||
guid => LiveSession?.SendUse(guid),
|
||||
(item, container, placement) =>
|
||||
LiveSession?.SendPutItemInContainer(item, container, placement),
|
||||
(item, container, placement, amount) =>
|
||||
LiveSession?.SendStackableSplitToContainer(
|
||||
item, container, placement, amount),
|
||||
(source, target, amount) =>
|
||||
LiveSession?.SendStackableMerge(source, target, amount),
|
||||
_itemInteractionController,
|
||||
_selection),
|
||||
ExternalContainer: new AcDream.App.UI.ExternalContainerRuntimeBindings(
|
||||
_externalContainers,
|
||||
Objects,
|
||||
(type, icon, under, over, effects) =>
|
||||
iconComposer.GetIcon(type, icon, under, over, effects),
|
||||
(type, icon, under, over, effects) =>
|
||||
iconComposer.GetDragIcon(type, icon, under, over, effects),
|
||||
_itemInteractionController,
|
||||
_selection,
|
||||
guid => LiveSession?.SendUse(guid),
|
||||
(item, container, placement) =>
|
||||
LiveSession?.SendPutItemInContainer(item, container, placement),
|
||||
(item, container, placement, amount) =>
|
||||
LiveSession?.SendStackableSplitToContainer(
|
||||
item, container, placement, amount),
|
||||
IsWithinExternalContainerUseRange),
|
||||
Cursor: new AcDream.App.UI.RetailUiCursorBindings(
|
||||
cursorFeedbackController,
|
||||
retailCursorManager),
|
||||
Confirmations: new AcDream.App.UI.ConfirmationRuntimeBindings(
|
||||
(type, context, accepted) =>
|
||||
LiveSession?.SendConfirmationResponse(type, context, accepted)),
|
||||
StackSplitQuantity: _stackSplitQuantity,
|
||||
Plugins: _uiRegistry,
|
||||
Persistence: persistence,
|
||||
Probe: new AcDream.App.UI.RetailUiProbeBindings(
|
||||
_options.UiProbeEnabled,
|
||||
_options.UiProbeScript,
|
||||
_options.UiProbeDump,
|
||||
UiProbeLog,
|
||||
action => _inputDispatcher?.TryInvokeAutomationAction(action) == true,
|
||||
(action, held) =>
|
||||
_inputDispatcher?.TrySetAutomationActionHeld(action, held) == true,
|
||||
_worldLifecycleAutomation));
|
||||
_retailUiRuntime = _retailUiLease.Mount(
|
||||
() => AcDream.App.UI.RetailUiRuntime.CreateUninitialized(
|
||||
retailUiBindings));
|
||||
|
||||
}
|
||||
|
||||
// Phase N.4 Task 12: construct LandblockSpawnAdapter under the feature flag
|
||||
// and rebuild _worldState so it threads the adapter in. _worldState starts
|
||||
// as an unadorned GpuWorldState (field initializer); here we replace it with
|
||||
|
|
@ -1687,19 +1422,12 @@ public sealed class GameWindow :
|
|||
_liveEntities,
|
||||
Objects,
|
||||
_retailSelectionScene,
|
||||
() => _playerServerGuid,
|
||||
() =>
|
||||
{
|
||||
var camera = GetSelectionCamera();
|
||||
return new AcDream.App.Interaction.SelectionCameraSnapshot(
|
||||
camera.View,
|
||||
camera.Projection,
|
||||
camera.Viewport);
|
||||
},
|
||||
() => _localPlayerIdentity.ServerGuid,
|
||||
interactionUi.LateBindings.SelectionCamera.Snapshot,
|
||||
() => new System.Numerics.Vector2(
|
||||
_pointerPosition.X,
|
||||
_pointerPosition.Y),
|
||||
() => _playerController is { } player
|
||||
() => _playerControllerSlot.Controller is { } player
|
||||
? new AcDream.App.Interaction.PlayerInteractionPose(
|
||||
player.CellId,
|
||||
player.Position)
|
||||
|
|
@ -1707,11 +1435,11 @@ public sealed class GameWindow :
|
|||
_liveEntityMotionBindings.GetSetupCylinder,
|
||||
setupId =>
|
||||
{
|
||||
if (_dats is null)
|
||||
return null;
|
||||
lock (_datLock)
|
||||
{
|
||||
if (!_dats.TryGet<DatReaderWriter.DBObjs.Setup>(setupId, out var setup)
|
||||
if (!contentEffectsAudio.Dats.TryGet<DatReaderWriter.DBObjs.Setup>(
|
||||
setupId,
|
||||
out var setup)
|
||||
|| setup.SelectionSphere is not { } sphere)
|
||||
{
|
||||
return null;
|
||||
|
|
@ -1719,6 +1447,22 @@ public sealed class GameWindow :
|
|||
return (sphere.Origin, sphere.Radius);
|
||||
}
|
||||
});
|
||||
var radarSnapshotProvider = new AcDream.App.UI.Layout.RadarSnapshotProvider(
|
||||
Objects,
|
||||
() => _liveEntities.WorldEntities,
|
||||
() => _liveEntities.Snapshots,
|
||||
playerGuid: () => _localPlayerIdentity.ServerGuid,
|
||||
playerYawRadians: () => _playerControllerSlot.Controller?.Yaw ?? 0f,
|
||||
playerCellId: () => _playerControllerSlot.Controller?.CellId ?? 0u,
|
||||
selectedGuid: () => _selection.SelectedObjectId,
|
||||
coordinatesOnRadar: () =>
|
||||
_runtimeSettings.Gameplay.CoordinatesOnRadar,
|
||||
uiLocked: () => _runtimeSettings.Gameplay.LockUI,
|
||||
playerEntities: () => _liveEntities.MaterializedWorldEntities,
|
||||
spatialQuery: () => _worldState);
|
||||
interactionUi.LateBindings.AdoptLateOwnerBinding(
|
||||
"radar snapshot",
|
||||
interactionUi.LateBindings.Radar.Bind(radarSnapshotProvider));
|
||||
if (_itemInteractionController is { } itemInteractions)
|
||||
{
|
||||
_selectionInteractions ??= new AcDream.App.Interaction.SelectionInteractionController(
|
||||
|
|
@ -1726,12 +1470,17 @@ public sealed class GameWindow :
|
|||
_worldSelectionQuery,
|
||||
itemInteractions,
|
||||
new AcDream.App.Interaction.WorldSessionSelectionInteractionTransport(
|
||||
() => LiveSession),
|
||||
() => interactionUi.LateBindings.Session.CurrentSession),
|
||||
new AcDream.App.Interaction.PlayerInteractionMovementSink(
|
||||
() => _playerController,
|
||||
() => _playerControllerSlot.Controller,
|
||||
_playerApproachCompletions),
|
||||
text => _debugVm?.AddToast(text),
|
||||
compositionToast,
|
||||
_playerApproachCompletions);
|
||||
interactionUi.LateBindings.AdoptLateOwnerBinding(
|
||||
"world selection",
|
||||
interactionUi.LateBindings.Selection.Bind(
|
||||
_worldSelectionQuery,
|
||||
_selectionInteractions));
|
||||
}
|
||||
if (_uiHost is { } retainedHost
|
||||
&& _selectionInteractions is { } selectionInteractions)
|
||||
|
|
@ -1898,6 +1647,9 @@ public sealed class GameWindow :
|
|||
resourceDiagnostics);
|
||||
_devToolsComposition?.LateBindings.FrameDiagnostics.Bind(
|
||||
_renderFrameDiagnostics);
|
||||
interactionUi.LateBindings.AdoptLateOwnerBinding(
|
||||
"render-frame diagnostics",
|
||||
_uiFrameDiagnostics.BindOwned(_renderFrameDiagnostics));
|
||||
|
||||
// Apply radii from the same immutable quality snapshot used for the
|
||||
// window's MSAA, terrain anisotropy, and dispatcher A2C setup.
|
||||
|
|
@ -2031,6 +1783,9 @@ public sealed class GameWindow :
|
|||
sealedDungeonCells,
|
||||
Console.WriteLine);
|
||||
_liveSessionController = new AcDream.App.Net.LiveSessionController();
|
||||
interactionUi.LateBindings.AdoptLateOwnerBinding(
|
||||
"live session",
|
||||
interactionUi.LateBindings.Session.Bind(_liveSessionController));
|
||||
var localPhysicsTimestamps =
|
||||
new AcDream.App.World.LiveSessionLocalPhysicsTimestampPublisher(
|
||||
_localPlayerIdentity,
|
||||
|
|
@ -2320,6 +2075,10 @@ public sealed class GameWindow :
|
|||
new AcDream.App.Streaming.LocalPlayerTeleportPresentation(
|
||||
portalTunnel)));
|
||||
_localPlayerTeleportSink.Bind(_localPlayerTeleport);
|
||||
interactionUi.LateBindings.AdoptLateOwnerBinding(
|
||||
"selection view plane",
|
||||
interactionUi.LateBindings.SelectionViewPlane.Bind(
|
||||
_localPlayerTeleport));
|
||||
var teleportRenderState =
|
||||
new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(
|
||||
_localPlayerTeleport);
|
||||
|
|
@ -2489,6 +2248,24 @@ public sealed class GameWindow :
|
|||
worldScenePasses,
|
||||
_renderRange,
|
||||
worldSceneDiagnostics);
|
||||
if (_frameScreenshots is { } screenshots
|
||||
&& _options.AutomationArtifactDirectory is { } artifactDirectory)
|
||||
{
|
||||
void UiProbeLog(string message) =>
|
||||
Console.WriteLine("[UI-PROBE] " + message);
|
||||
_worldLifecycleAutomation =
|
||||
new AcDream.App.Diagnostics.WorldLifecycleAutomationController(
|
||||
() => _worldReveal?.Snapshot ?? default,
|
||||
() => _worldReveal?.PortalMaterializationCount ?? 0,
|
||||
CaptureWorldLifecycleResourceSnapshot,
|
||||
screenshots,
|
||||
artifactDirectory,
|
||||
UiProbeLog);
|
||||
interactionUi.LateBindings.AdoptLateOwnerBinding(
|
||||
"world lifecycle automation",
|
||||
interactionUi.LateBindings.Automation.Bind(
|
||||
_worldLifecycleAutomation));
|
||||
}
|
||||
AcDream.App.Rendering.IRetainedGameplayUiFrame? retainedGameplayUi =
|
||||
_options.RetailUi && _retailUiRuntime is not null
|
||||
? new AcDream.App.Rendering.RetainedGameplayUiFrame(
|
||||
|
|
@ -2738,8 +2515,7 @@ public sealed class GameWindow :
|
|||
Chat.ResetSessionIdentity();
|
||||
AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = 0u;
|
||||
_runtimeSettings.ResetActiveCharacterKey();
|
||||
_characterOptions1 =
|
||||
AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1.Default;
|
||||
_characterOptions.Reset();
|
||||
_localPlayerSkills.ResetSession();
|
||||
_liveEntityNetworkUpdates?.ResetSessionState();
|
||||
_liveEntityHydration?.ResetSessionState();
|
||||
|
|
@ -2834,7 +2610,7 @@ public sealed class GameWindow :
|
|||
OnConfirmationDone: done =>
|
||||
_retailUiRuntime?.HandleConfirmationDone(done),
|
||||
OnCharacterOptions: (options1, _) =>
|
||||
_characterOptions1 =
|
||||
_characterOptions.Options =
|
||||
(AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1)
|
||||
options1,
|
||||
ClientTime: ClientTimerNow);
|
||||
|
|
@ -2998,9 +2774,6 @@ public sealed class GameWindow :
|
|||
// EXPECTED-DIFF: local sidestep pacing now matches how remotes have
|
||||
// always played (w6-cutover-map.md R3).
|
||||
|
||||
private bool GetDebugPlayerOnGround() =>
|
||||
_playerMode && _playerController is not null && !_playerController.IsAirborne;
|
||||
|
||||
private void SyncToolbarWindowButtons()
|
||||
{
|
||||
_retailUiRuntime?.SyncToolbarWindowButtons();
|
||||
|
|
@ -3023,62 +2796,6 @@ public sealed class GameWindow :
|
|||
private void OnFramebufferResize(Silk.NET.Maths.Vector2D<int> newSize)
|
||||
=> _framebufferResize.Resize(newSize);
|
||||
|
||||
/// <summary>
|
||||
/// Item-target-mode world pick at the current cursor. The renderer supplies the
|
||||
/// exact visible CPhysicsPart equivalents and RetailWorldPicker performs
|
||||
/// retail's drawing-sphere broadphase followed by flat visual-polygon
|
||||
/// intersection. <paramref name="includeSelf"/> allows item target-use to
|
||||
/// pick the local player while plain selection excludes it.
|
||||
/// </summary>
|
||||
private uint? PickWorldGuidAtCursor(bool includeSelf)
|
||||
=> _selectionInteractions?.PickAtCursor(includeSelf)
|
||||
?? _worldSelectionQuery?.PickAtCursor(includeSelf);
|
||||
|
||||
// Contained/toolbar activation is not a world-selection interaction: it
|
||||
// sends directly without speculative TurnToObject/MoveToObject.
|
||||
private void UseItemByGuid(uint guid)
|
||||
{
|
||||
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||
if (_liveSessionHost?.IsInWorld != true || session is null)
|
||||
return;
|
||||
uint sequence = session.NextGameActionSequence();
|
||||
session.SendGameAction(
|
||||
AcDream.Core.Net.Messages.InteractRequests.BuildUse(sequence, guid));
|
||||
Console.WriteLine($"[D.5.1] toolbar use-item guid=0x{guid:X8} seq={sequence}");
|
||||
}
|
||||
|
||||
private bool IsWithinExternalContainerUseRange(uint targetGuid)
|
||||
=> _worldSelectionQuery?.IsWithinExternalContainerUseRange(targetGuid) ?? true;
|
||||
|
||||
private void SendUse(uint guid)
|
||||
=> _selectionInteractions?.SendUse(guid);
|
||||
|
||||
private void SendPickUp(uint itemGuid, uint destinationContainerId, int placement)
|
||||
=> _selectionInteractions?.SendPickup(itemGuid, destinationContainerId, placement);
|
||||
|
||||
private uint? SelectClosestCombatTarget(bool showToast)
|
||||
=> _selectionInteractions?.SelectClosestCombatTarget(showToast);
|
||||
|
||||
private bool IsHealthBarTarget(uint guid)
|
||||
=> _worldSelectionQuery?.ShouldShowHealth(guid) == true;
|
||||
|
||||
private (System.Numerics.Matrix4x4 View,
|
||||
System.Numerics.Matrix4x4 Projection,
|
||||
System.Numerics.Vector2 Viewport) GetSelectionCamera()
|
||||
{
|
||||
if (_cameraController is null || _window is null)
|
||||
return (System.Numerics.Matrix4x4.Identity,
|
||||
System.Numerics.Matrix4x4.Identity,
|
||||
System.Numerics.Vector2.Zero);
|
||||
var camera = _localPlayerTeleport?.ApplyViewPlane(_cameraController.Active)
|
||||
?? _cameraController.Active;
|
||||
return (camera.View, camera.Projection,
|
||||
new System.Numerics.Vector2(_window.Size.X, _window.Size.Y));
|
||||
}
|
||||
|
||||
private AcDream.App.UI.Layout.VividTargetInfo? ResolveVividTargetInfo(uint guid)
|
||||
=> _worldSelectionQuery?.ResolveVividTargetInfo(guid);
|
||||
|
||||
// #107 (2026-06-10): memoized "this indoor spawn claim can never hydrate"
|
||||
// check against the dat's LandBlockInfo.NumCells. Used by the auto-entry
|
||||
// hold so a garbage claim doesn't stall login forever; the Resolve-head
|
||||
|
|
@ -3260,6 +2977,14 @@ public sealed class GameWindow :
|
|||
]),
|
||||
new ResourceShutdownStage("session dependents",
|
||||
[
|
||||
new("interaction/UI late bindings", () =>
|
||||
{
|
||||
InteractionUiLateBindings? bindings = _interactionUiLateBindings;
|
||||
if (bindings is null)
|
||||
return;
|
||||
bindings.Dispose();
|
||||
_interactionUiLateBindings = null;
|
||||
}),
|
||||
new("mouse capture", () =>
|
||||
{
|
||||
AcDream.App.Input.CameraPointerInputController? pointer =
|
||||
|
|
@ -3277,7 +3002,6 @@ public sealed class GameWindow :
|
|||
throw new InvalidOperationException(
|
||||
"The retained UI ownership lease did not complete disposal.");
|
||||
_retailUiRuntime = null;
|
||||
_retainedInputCapture.Root = null;
|
||||
_uiHost = null;
|
||||
}),
|
||||
new("combat target", () =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue