Applies the Opus review findings on CH1 (172c6f9a), the exact retail chat
color table. Two blockers plus should-fixes/nits, one commit:
BLOCKER 1 — LegacyChannelChatType.Resolve's channel-bit table was wrong.
Binary Ninja renders retail's `neg esi; sbb esi, esi` idiom (a branchless
select between Channel 0x08 and Channel_Send 0x09) as the trivial pseudo-C
`esi - esi` (always 0), hiding the real values. Corrected by decoding the
raw bytes at the PDB-paired binary: HEAR sbb site VA 0x00570F0A (mask -6 ->
0x08), SEND sbb site VA 0x00570D4F (mask -5 -> 0x09). The generic
admin/audit/sentinel catch-all is Channel/Channel_Send, NOT Abuse (0x0E) —
Abuse is retail's ONLY 0x0E producer (bit 0x0001). The unnamed
FellowBroadcast bit (0x4000000) is hear=Channel(0x08)/send=Fellowship(0x13),
not a flat 0x13. ACE's PDB-sourced Channel enum corroborates. Introduces
`RetailLogTextType`, the 34-value named enum for the wire LogTextType space
(values only, no color — Core stays presentation-free).
BLOCKER 2 — three ChatLog.OnSystemMessage sinks (ChatVM.ShowSystemMessage,
LiveSessionRuntimeFactory's ShowSystemMessage delegate,
HeadlessGameplayOperations.DisplayMessage) were typing ALL
ClientCommandController output 0x1A (bright red), including informational
command output (@version, /loc, friends list, usage lines). Retail types
the great majority of that output 0x00 Default (green) and reserves 0x1A
for genuine refusals/errors. Reverted to 0x00 with a comment noting the
refusal-vs-info split lands with CH2's SpewBox producer rewiring. The five
App composition sites that pass 0x1A for actual refusal text
(InteractionRetainedUiComposition, SessionPlayerComposition) were already
correct and are untouched (aside from converting the literal to the new
enum).
Also: AP-176 divergence-register row for OnWeenieError/OnCombatLine's
single-type approximation of retail's per-code/per-message dispatch; a
carry-forward test for the out-of-range LogTextType color fallback in
ChatWindowController; decomp-confirmed anchors replacing ACE-inferred
citations in CombatChatTranslator and ChatLog.OnPlayerKilled; required
(non-optional) logTextType parameters on OnLocalSpeech/OnTellReceived/
OnCombatLine/OnSelfSent since no production caller relied on a default;
LegacyChannelChatType.Resolve's parameter renamed channelBit -> channelId
with a doc note on multi-bit ids; corrections to the color-table research
doc's §3.3 wire tables; and issue #359 for the pre-existing (not
CH1-introduced) 0x019E PlayerKilled participant-suppression gap retail has
and acdream lacks.
dotnet build clean; full Release suite 11,835 passed / 4 skipped / 0 failed
(11,839 total), up from the CH1 baseline of 11,833/4/0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1261 lines
51 KiB
C#
1261 lines
51 KiB
C#
using AcDream.App.Combat;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Interaction;
|
|
using AcDream.App.Diagnostics;
|
|
using AcDream.App.Net;
|
|
using AcDream.App.Physics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.App.Rendering.Vfx;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Runtime;
|
|
using AcDream.App.Settings;
|
|
using AcDream.App.Audio;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.App.Update;
|
|
using AcDream.App.World;
|
|
using AcDream.Content;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Chat;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
using AcDream.Core.Plugins;
|
|
using AcDream.Core.Rendering;
|
|
using AcDream.Core.Selection;
|
|
using AcDream.Core.Player;
|
|
using AcDream.Core.Social;
|
|
using AcDream.Core.Spells;
|
|
using AcDream.Core.World;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Gameplay;
|
|
using AcDream.Runtime.Session;
|
|
using Silk.NET.Windowing;
|
|
|
|
namespace AcDream.App.Composition;
|
|
|
|
internal sealed record SessionPlayerDependencies(
|
|
RuntimeOptions Options,
|
|
GameRuntime Runtime,
|
|
IWindow Window,
|
|
object DatLock,
|
|
RuntimeSettingsController Settings,
|
|
SettingsDevToolsResult SettingsDevTools,
|
|
WorldEnvironmentController WorldEnvironment,
|
|
WorldSceneDebugState WorldSceneDebugState,
|
|
RuntimeDiagnosticCommandSlot RuntimeDiagnosticCommands,
|
|
LiveCombatModeCommandSlot CombatModeCommands,
|
|
RuntimeCombatModeOperationsSlot CombatModeOperations,
|
|
HostQuiescenceGate HostQuiescence,
|
|
PhysicsEngine PhysicsEngine,
|
|
PhysicsDataCache PhysicsDataCache,
|
|
WorldGameState WorldGameState,
|
|
WorldEvents WorldEvents,
|
|
EntityClassificationCache ClassificationCache,
|
|
LiveEntityRuntimeSlot RuntimeSlot,
|
|
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> AnimatedEntities,
|
|
RemoteMovementObservationTracker RemoteMovementObservations,
|
|
RemotePhysicsUpdater RemotePhysicsUpdater,
|
|
RemoteInboundMotionDispatcher RemoteInboundMotion,
|
|
RetailInboundEventDispatcher InboundEntityEvents,
|
|
DeferredLiveEntityMotionRuntimeBindings MotionBindings,
|
|
LocalPlayerIdentityState PlayerIdentity,
|
|
LocalPlayerPhysicsHostSlot PlayerHost,
|
|
LocalPlayerModeState PlayerMode,
|
|
ChaseCameraInputState ChaseCameraInput,
|
|
LocalPlayerOutboundController PlayerOutbound,
|
|
DeferredLocalPlayerTeleportNetworkSink TeleportSink,
|
|
LiveWorldOriginState WorldOrigin,
|
|
WorldRenderRangeState RenderRange,
|
|
LocalPlayerShadowState PlayerShadow,
|
|
ViewportAspectState ViewportAspect,
|
|
PlayerApproachCompletionState PlayerApproachCompletions,
|
|
PointerPositionState PointerPosition,
|
|
DispatcherMovementInputSource MovementInput,
|
|
IInputCaptureSource InputCapture,
|
|
AcDream.App.Rendering.Vfx.ParticleVisibilityController ParticleVisibility,
|
|
TranslucencyFadeManager TranslucencyFades,
|
|
EntityEffectPoseRegistry EffectPoses,
|
|
UpdateFrameClock UpdateClock,
|
|
MovementTruthDiagnosticController MovementDiagnostics,
|
|
CombatAttackOperationsSlot CombatAttackOperations,
|
|
CombatFeedbackSlot CombatFeedback,
|
|
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
|
|
Action<string> Log)
|
|
{
|
|
public RuntimeActionState Actions => Runtime.ActionOwner;
|
|
|
|
public RuntimeEntityObjectLifetime EntityObjects =>
|
|
Runtime.EntityObjects;
|
|
|
|
public RuntimeLocalPlayerMovementState PlayerController =>
|
|
Runtime.MovementOwner;
|
|
|
|
public RuntimeInventoryState Inventory => Runtime.InventoryOwner;
|
|
|
|
public RuntimeCommunicationState Communication =>
|
|
Runtime.CommunicationOwner;
|
|
|
|
public RuntimeCharacterState Character => Runtime.CharacterOwner;
|
|
}
|
|
|
|
internal sealed record SessionPlayerResult(
|
|
LandblockStreamer Streamer,
|
|
StreamingController Streaming,
|
|
StreamingOriginRecenterCoordinator StreamingOriginRecenter,
|
|
WorldRevealCoordinator WorldReveal,
|
|
DatSpawnClaimHydrationClassifier SpawnClaimHydration,
|
|
LiveSessionController LiveSession,
|
|
LiveEntityHydrationController Hydration,
|
|
LiveEntityNetworkUpdateController NetworkUpdates,
|
|
LiveEntityLivenessController Liveness,
|
|
LiveEntitySessionController SessionEvents,
|
|
GameplayInputFrameController GameplayInput,
|
|
StreamingFrameController StreamingFrame,
|
|
LocalPlayerAnimationController LocalPlayerAnimation,
|
|
LocalPlayerShadowSynchronizer LocalPlayerShadow,
|
|
LiveLocalPlayerFrameRuntime LocalPlayerFrameRuntime,
|
|
RetailLocalPlayerFrameController LocalPlayerFrame,
|
|
LiveSpatialPresentationReconciler LiveSpatialReconciler,
|
|
LiveObjectFrameController LiveObjectFrame,
|
|
PlayerModeController PlayerMode,
|
|
PlayerModeAutoEntry PlayerModeAutoEntry,
|
|
LocalPlayerTeleportController LocalTeleport,
|
|
LiveSessionHost SessionHost,
|
|
RuntimePlacementProjectionRetrySlot PlacementProjectionRetry,
|
|
CurrentGameRuntimeAdapter GameRuntime,
|
|
GameplayInputActionRouter? GameplayActions,
|
|
SessionPlayerRuntimeBindings RuntimeBindings);
|
|
|
|
internal interface IGameWindowSessionPlayerPublication
|
|
{
|
|
void PublishSessionPlayer(SessionPlayerResult result);
|
|
}
|
|
|
|
internal enum SessionPlayerCompositionPoint
|
|
{
|
|
StreamingRadiiResolved,
|
|
StreamerCreated,
|
|
StreamerStarted,
|
|
StreamingCreated,
|
|
RuntimeSettingsBound,
|
|
WorldRevealCreated,
|
|
LiveSessionCreated,
|
|
HydrationCreated,
|
|
LiveEntityGraphBound,
|
|
CombatOperationsBound,
|
|
GameplayInputBound,
|
|
UpdateLeavesCreated,
|
|
PlayerModeBound,
|
|
PortalTransferred,
|
|
TeleportBound,
|
|
SessionHostCreated,
|
|
CommandsBound,
|
|
GameplayActionsAttached,
|
|
ResultPublished,
|
|
}
|
|
|
|
internal sealed class SessionPlayerCompositionPhase
|
|
: ISessionPlayerCompositionPhase<
|
|
HostInputCameraResult,
|
|
ContentEffectsAudioResult,
|
|
SettingsDevToolsResult,
|
|
WorldRenderResult,
|
|
InteractionRetainedUiResult,
|
|
LivePresentationResult,
|
|
SessionPlayerResult>
|
|
{
|
|
private readonly SessionPlayerDependencies _dependencies;
|
|
private readonly IGameWindowSessionPlayerPublication _publication;
|
|
private readonly Action<SessionPlayerCompositionPoint>? _faultInjection;
|
|
|
|
public SessionPlayerCompositionPhase(
|
|
SessionPlayerDependencies dependencies,
|
|
IGameWindowSessionPlayerPublication publication,
|
|
Action<SessionPlayerCompositionPoint>? faultInjection = null)
|
|
{
|
|
_dependencies = dependencies
|
|
?? throw new ArgumentNullException(nameof(dependencies));
|
|
_publication = publication
|
|
?? throw new ArgumentNullException(nameof(publication));
|
|
_faultInjection = faultInjection;
|
|
}
|
|
|
|
public SessionPlayerResult Compose(
|
|
HostInputCameraResult host,
|
|
ContentEffectsAudioResult content,
|
|
SettingsDevToolsResult settings,
|
|
WorldRenderResult world,
|
|
InteractionRetainedUiResult interaction,
|
|
LivePresentationResult live)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(host);
|
|
ArgumentNullException.ThrowIfNull(content);
|
|
ArgumentNullException.ThrowIfNull(settings);
|
|
ArgumentNullException.ThrowIfNull(world);
|
|
ArgumentNullException.ThrowIfNull(interaction);
|
|
ArgumentNullException.ThrowIfNull(live);
|
|
if (!ReferenceEquals(_dependencies.SettingsDevTools, settings))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Session/player dependencies do not match the ordered settings result.");
|
|
}
|
|
|
|
var scope = new CompositionAcquisitionScope();
|
|
SessionPlayerRuntimeBindings? bindings = null;
|
|
bool bindingsOwnedByScope = false;
|
|
try
|
|
{
|
|
SessionPlayerResult result = ComposeCore(
|
|
host,
|
|
content,
|
|
world,
|
|
interaction,
|
|
live,
|
|
scope,
|
|
ref bindings,
|
|
ref bindingsOwnedByScope);
|
|
scope.Complete();
|
|
return result;
|
|
}
|
|
catch (Exception failure)
|
|
{
|
|
if (bindings is not null && !bindingsOwnedByScope)
|
|
{
|
|
scope.Own(
|
|
"session/player runtime bindings",
|
|
bindings,
|
|
static value => value.Dispose());
|
|
bindingsOwnedByScope = true;
|
|
}
|
|
|
|
scope.RollbackAndThrow(failure);
|
|
throw new System.Diagnostics.UnreachableException();
|
|
}
|
|
}
|
|
|
|
private SessionPlayerResult ComposeCore(
|
|
HostInputCameraResult host,
|
|
ContentEffectsAudioResult content,
|
|
WorldRenderResult world,
|
|
InteractionRetainedUiResult interaction,
|
|
LivePresentationResult live,
|
|
CompositionAcquisitionScope scope,
|
|
ref SessionPlayerRuntimeBindings? bindings,
|
|
ref bool bindingsOwnedByScope)
|
|
{
|
|
SessionPlayerDependencies d = _dependencies;
|
|
WorldRenderFoundation foundation = world.Foundation;
|
|
int nearRadius = d.Settings.ResolvedQuality.NearRadius;
|
|
int farRadius = d.Settings.ResolvedQuality.FarRadius;
|
|
if (d.Options.LegacyStreamRadius is { } legacyRadius)
|
|
{
|
|
nearRadius = legacyRadius;
|
|
farRadius = Math.Max(legacyRadius, farRadius);
|
|
}
|
|
d.RenderRange.NearRadius = nearRadius;
|
|
d.RenderRange.FarRadius = farRadius;
|
|
d.Log(
|
|
$"streaming: nearRadius={nearRadius} " +
|
|
$"(window={2 * nearRadius + 1}x{2 * nearRadius + 1}) " +
|
|
$"farRadius={farRadius} " +
|
|
$"(window={2 * farRadius + 1}x{2 * farRadius + 1})");
|
|
Fault(SessionPlayerCompositionPoint.StreamingRadiiResolved);
|
|
|
|
IPreparedCollisionSource preparedCollisions =
|
|
content.PreparedAssets as IPreparedCollisionSource ??
|
|
throw new NotSupportedException(
|
|
"Production prepared assets must expose the matching " +
|
|
"prepared-collision catalog.");
|
|
var landblockBuildFactory = new LandblockBuildFactory(
|
|
content.Dats,
|
|
preparedCollisions,
|
|
d.DatLock,
|
|
world.TerrainBuild.HeightTable,
|
|
d.Options.DumpSceneryZ);
|
|
var streamerLease = scope.Acquire(
|
|
"landblock streamer",
|
|
() => LandblockStreamer.CreateForRequests(
|
|
landblockBuildFactory.Build,
|
|
(id, landblock) =>
|
|
{
|
|
if (landblock is null)
|
|
return null;
|
|
uint x = (id >> 24) & 0xFFu;
|
|
uint y = (id >> 16) & 0xFFu;
|
|
return AcDream.Core.Terrain.LandblockMesh.Build(
|
|
landblock.Heightmap,
|
|
x,
|
|
y,
|
|
world.TerrainBuild.HeightTable,
|
|
world.TerrainBuild.Blending,
|
|
world.TerrainBuild.SurfaceCache);
|
|
}),
|
|
static value => value.Dispose());
|
|
Fault(SessionPlayerCompositionPoint.StreamerCreated);
|
|
streamerLease.Resource.Start();
|
|
Fault(SessionPlayerCompositionPoint.StreamerStarted);
|
|
|
|
var streaming = new StreamingController(
|
|
enqueueLoad: (id, kind, generation) => streamerLease.Resource.EnqueueLoad(
|
|
new LandblockBuildRequest(
|
|
id,
|
|
kind,
|
|
generation,
|
|
new LandblockBuildOrigin(
|
|
d.WorldOrigin.CenterX,
|
|
d.WorldOrigin.CenterY))),
|
|
enqueueUnload: streamerLease.Resource.EnqueueUnload,
|
|
completionSource: streamerLease.Resource,
|
|
state: live.WorldState,
|
|
nearRadius: nearRadius,
|
|
farRadius: farRadius,
|
|
presentationPipeline: live.LandblockPipeline,
|
|
clearPendingLoads: streamerLease.Resource.ClearPendingLoads,
|
|
workBudgetOptions: d.Options.StreamingWorkBudgets);
|
|
var streamingOriginRecenter = new StreamingOriginRecenterCoordinator(
|
|
streaming,
|
|
d.WorldOrigin);
|
|
streaming.MaxCompletionsPerFrame =
|
|
d.Settings.ResolvedQuality.MaxCompletionsPerFrame;
|
|
Fault(SessionPlayerCompositionPoint.StreamingCreated);
|
|
|
|
bindings = new SessionPlayerRuntimeBindings();
|
|
var settingsTargets = new RuntimeSettingsTargets(
|
|
new SilkRuntimeDisplayWindowTarget(d.Window),
|
|
live.DrawDispatcher,
|
|
foundation.TerrainAtlas,
|
|
streaming,
|
|
d.RenderRange,
|
|
interaction.RetainedUi?.Host.Root,
|
|
d.Log);
|
|
bindings.Adopt(
|
|
"runtime settings targets",
|
|
d.Settings.BindRuntimeTargetsOwned(settingsTargets));
|
|
Fault(SessionPlayerCompositionPoint.RuntimeSettingsBound);
|
|
|
|
var spawnClaimClassifier = new DatSpawnClaimHydrationClassifier(
|
|
content.Dats,
|
|
d.DatLock);
|
|
var worldQuiescence = new WorldGenerationQuiescence(
|
|
d.Actions.Selection,
|
|
live.WorldState,
|
|
guid => live.LiveEntities.TryGetProjectionKey(
|
|
guid,
|
|
out AcDream.Runtime.Entities.RuntimeEntityKey key)
|
|
? key
|
|
: null,
|
|
content.Audio?.Engine);
|
|
var compositeWarmupSource =
|
|
new CompositeWarmupEntitySource(live.WorldState);
|
|
// Campaign V slice V6h: composite-texture warmup is a property of the
|
|
// draw dispatcher, and mesh/texture reveal priority of the world upload
|
|
// path. On a backend with neither, the reveal gate's render-resource
|
|
// condition is trivially satisfied — there is nothing to warm — and
|
|
// every other reveal condition (streaming residence, spawn-cell
|
|
// readiness, terrain residence, quiescence) is unchanged.
|
|
WbDrawDispatcher? revealDispatcher = live.DrawDispatcher;
|
|
// #262 (Campaign P P6): the #111 [snap] apparatus was designed as a
|
|
// permanent low-volume diagnostic (one line per login/teleport entry
|
|
// snap) but PhysicsEngine.DiagnosticLog was never wired in production,
|
|
// so the 2026-07-29 Coldeve acceptance log was structurally silent on
|
|
// which Resolve branch the run-on-the-spot login took. Wire it per
|
|
// session composition (a session reset constructs a fresh engine).
|
|
d.PhysicsEngine.DiagnosticLog =
|
|
static line => System.Console.WriteLine(line);
|
|
var revealRenderResources = new WorldRevealRenderResourceScheduler(
|
|
foundation.MeshAdapter is { } revealMeshes
|
|
? revealMeshes.SetDestinationRevealUploadPriority
|
|
: static _ => { },
|
|
foundation.TextureCache.SetDestinationRevealUploadPriority);
|
|
var worldReveal = new WorldRevealCoordinator(
|
|
live.WorldTransit,
|
|
// #280: read the radii LIVE from the streaming controller rather
|
|
// than capturing them here. They are runtime mutable through
|
|
// Settings (StreamingController.ReconfigureRadii), and retail
|
|
// re-arms the blocking prefetch at the new radius on a mid-hold
|
|
// change (SmartBox::set_mid_radius @0x00453180).
|
|
() => StreamingDiagnostics.ApplyRevealRadiusOverride(
|
|
new StreamingRevealWindow(
|
|
streaming.NearRadius,
|
|
streaming.FarRadius)),
|
|
streaming.IsRenderNeighborhoodResident,
|
|
d.PhysicsEngine.IsSpawnCellReady,
|
|
d.PhysicsEngine.IsNeighborhoodTerrainResident,
|
|
() => revealDispatcher?.CompositeTexturesReady ?? true,
|
|
(destinationCell, radius) =>
|
|
{
|
|
if (revealDispatcher is null)
|
|
return;
|
|
compositeWarmupSource.Refresh(destinationCell, radius);
|
|
revealDispatcher.PrepareCompositeTextures(
|
|
compositeWarmupSource.Entities,
|
|
compositeWarmupSource.Generation,
|
|
destinationCell,
|
|
radius);
|
|
},
|
|
() =>
|
|
{
|
|
compositeWarmupSource.Reset();
|
|
revealDispatcher?.InvalidateCompositeWarmupReadiness();
|
|
},
|
|
spawnClaimClassifier.IsUnhydratable,
|
|
worldQuiescence,
|
|
streaming,
|
|
revealRenderResources);
|
|
Fault(SessionPlayerCompositionPoint.WorldRevealCreated);
|
|
|
|
return CompleteSessionPlayer(
|
|
host,
|
|
content,
|
|
world,
|
|
interaction,
|
|
live,
|
|
scope,
|
|
streamerLease,
|
|
streaming,
|
|
streamingOriginRecenter,
|
|
worldReveal,
|
|
spawnClaimClassifier,
|
|
bindings,
|
|
ref bindingsOwnedByScope);
|
|
}
|
|
|
|
private SessionPlayerResult CompleteSessionPlayer(
|
|
HostInputCameraResult host,
|
|
ContentEffectsAudioResult content,
|
|
WorldRenderResult world,
|
|
InteractionRetainedUiResult interaction,
|
|
LivePresentationResult live,
|
|
CompositionAcquisitionScope scope,
|
|
CompositionAcquisitionScope.CompositionAcquisitionLease<LandblockStreamer>
|
|
streamerLease,
|
|
StreamingController streaming,
|
|
StreamingOriginRecenterCoordinator streamingOriginRecenter,
|
|
WorldRevealCoordinator worldReveal,
|
|
DatSpawnClaimHydrationClassifier spawnClaimClassifier,
|
|
SessionPlayerRuntimeBindings bindings,
|
|
ref bool bindingsOwnedByScope)
|
|
{
|
|
SessionPlayerDependencies d = _dependencies;
|
|
var networkUpdateBridge = new DeferredLiveEntityNetworkUpdateSink();
|
|
var sealedDungeonCells = new DatSealedDungeonCellClassifier(
|
|
content.Dats,
|
|
d.DatLock);
|
|
var projectionMaterializer = new DatLiveEntityProjectionMaterializer(
|
|
d.Options,
|
|
content.Dats,
|
|
live.LiveEntities,
|
|
content.CollisionAssets,
|
|
content.AnimationLoader,
|
|
live.EntitySpawnAdapter,
|
|
world.Foundation.TextureCache,
|
|
d.ClassificationCache,
|
|
d.EffectPoses,
|
|
live.EquippedChildren,
|
|
d.WorldGameState,
|
|
d.WorldEvents,
|
|
d.PhysicsEngine.ShadowObjects,
|
|
content.CollisionBuilder,
|
|
live.ProjectileController,
|
|
live.AnimationPresenter,
|
|
live.StaticAnimationScheduler,
|
|
d.WorldOrigin,
|
|
d.UpdateClock,
|
|
d.Runtime.TransitOwner);
|
|
var originCoordinator = new LiveEntityWorldOriginCoordinator(
|
|
d.WorldOrigin,
|
|
streaming,
|
|
live.WorldState,
|
|
worldReveal,
|
|
d.PlayerIdentity,
|
|
sealedDungeonCells,
|
|
d.Log);
|
|
|
|
LiveSessionController liveSession = d.Runtime.Session;
|
|
var liveSessionCommands = new LiveSessionCommandSurface();
|
|
var liveSessionSource = new LiveSessionAppSource(
|
|
liveSession,
|
|
liveSessionCommands);
|
|
bindings.Adopt(
|
|
"retained-UI live session",
|
|
interaction.LateBindings.Session.Bind(liveSessionSource));
|
|
var localPhysicsTimestamps =
|
|
new LiveSessionLocalPhysicsTimestampPublisher(
|
|
d.PlayerIdentity,
|
|
liveSessionSource);
|
|
Fault(SessionPlayerCompositionPoint.LiveSessionCreated);
|
|
|
|
var teardown = new LiveEntityRuntimeTeardownController(
|
|
live.LiveEntities,
|
|
live.Presentation,
|
|
live.EntityEffects,
|
|
live.SelectionInteractions,
|
|
d.Actions.Selection,
|
|
d.AnimatedEntities,
|
|
d.RemoteMovementObservations,
|
|
d.TranslucencyFades,
|
|
live.ProjectionWithdrawal,
|
|
live.EquippedChildren,
|
|
d.PhysicsEngine.ShadowObjects,
|
|
live.Lights,
|
|
d.ClassificationCache,
|
|
d.PlayerIdentity);
|
|
var dormantLiveEntities = new DormantLiveEntityStore();
|
|
var deletion = new LiveEntityDeletionController(
|
|
live.LiveEntities,
|
|
d.EntityObjects,
|
|
teardown,
|
|
d.PlayerIdentity,
|
|
dormantLiveEntities,
|
|
d.Options.DumpLiveSpawns ? d.Log : null);
|
|
// 2026-08-08 vendor-approach root cause: the local player's
|
|
// publication-chain host resolves moveto/sticky targets through
|
|
// RuntimePhysicsState.ResolveObjectTableHost. Bind the graphical
|
|
// host's canonical CObjectMaint::GetObjectA stand-in — the SAME
|
|
// lazy-minimal-host resolver every remote host's GetObjectA uses
|
|
// (LiveEntityMotionRuntimeController.ResolvePhysicsHost, reached
|
|
// through the deferred motion-bindings seam so this closure stays
|
|
// valid across session routes). Without it the local player's
|
|
// MoveToObject against a never-animated NPC/static object never
|
|
// received TargetManager.AddVoyeur's immediate initial snapshot and
|
|
// sat armed-but-inert forever (no nodes, no movement, no natural
|
|
// completion).
|
|
live.LiveEntities.Physics.BindObjectTableHostResolver(
|
|
guid => d.MotionBindings.ResolvePhysicsHost(guid));
|
|
// C3c: the graphical first-entry drive controller — walks every
|
|
// initial-Create residence through its Runtime conductor with the
|
|
// production prepared-collision source, the live movement-skill
|
|
// options, and the truthful local-player activation preparation
|
|
// (authored-cylinder radius/height with the legacy fallbacks, and
|
|
// the shadow disposition read from the exact shadow registry the
|
|
// activation commit validates against).
|
|
IPreparedCollisionSource firstEntryCollision =
|
|
content.PreparedAssets as IPreparedCollisionSource
|
|
?? throw new NotSupportedException(
|
|
"Production prepared assets must expose the matching "
|
|
+ "prepared-collision catalog.");
|
|
// C3c-R1 review F9: the provider runs on EVERY drive pump while the
|
|
// entity's conductor is still yielding, and the Setup cylinder per
|
|
// incarnation is immutable (SourceGfxObjOrSetupId and the spawn
|
|
// record's ObjScale are fixed at Create) — cache the resolved
|
|
// world-entity lookup + GetSetupCylinder per pending incarnation
|
|
// (keyed by the incarnation-unique local id; an unresolved/default
|
|
// result is NOT cached so late hydration still upgrades it). The
|
|
// shadow disposition deliberately stays live: shadow registration
|
|
// can land between pumps and the activation commit validates the
|
|
// disposition against the exact registry.
|
|
uint firstEntryCylinderLocalId = 0u;
|
|
float firstEntryCylinderRadius = 0f;
|
|
float firstEntryCylinderHeight = 0f;
|
|
var firstEntryDrive = new RuntimeFirstEntryDriveController(
|
|
d.EntityObjects,
|
|
d.Runtime.Clock,
|
|
firstEntryCollision,
|
|
() => PlayerMovementConstructionOptions.From(
|
|
d.Runtime.CharacterOwner.MovementSkills.Snapshot),
|
|
record =>
|
|
{
|
|
float radius = 0.48f;
|
|
float height = 1.835f;
|
|
uint localId = record.Key?.LocalEntityId ?? 0u;
|
|
if (localId != 0u && localId == firstEntryCylinderLocalId)
|
|
{
|
|
radius = firstEntryCylinderRadius;
|
|
height = firstEntryCylinderHeight;
|
|
}
|
|
else if (live.LiveEntities.TryGetWorldEntity(
|
|
record.ServerGuid,
|
|
out WorldEntity? playerEntity)
|
|
&& playerEntity is not null)
|
|
{
|
|
(float setupRadius, float setupHeight) =
|
|
d.MotionBindings.GetSetupCylinder(
|
|
record.ServerGuid,
|
|
playerEntity);
|
|
if (setupRadius >= 0.05f)
|
|
{
|
|
radius = setupRadius;
|
|
height = setupHeight;
|
|
if (localId != 0u)
|
|
{
|
|
firstEntryCylinderLocalId = localId;
|
|
firstEntryCylinderRadius = radius;
|
|
firstEntryCylinderHeight = height;
|
|
}
|
|
}
|
|
}
|
|
bool hasAuthoredShadow = record.Key is { } key
|
|
&& d.PhysicsEngine.ShadowObjects.HasLogicalOwner(
|
|
key.LocalEntityId);
|
|
return new RuntimeLocalPlayerPhysicsActivationPreparation(
|
|
radius,
|
|
height,
|
|
hasAuthoredShadow
|
|
? RuntimeLocalPlayerShadowDisposition
|
|
.RegisteredAuthoredPayload
|
|
: RuntimeLocalPlayerShadowDisposition.ProvenShapeless);
|
|
});
|
|
// C4 route 2 (2026-08-03): the graphical accepted-Position drive
|
|
// controller — the Runtime-owned execution seam for a ForcePosition
|
|
// on the already-live local player. Constructed once per session
|
|
// route alongside firstEntryDrive; both share the SAME entity
|
|
// lifetime, collision source, and clock.
|
|
var acceptedPositionDrive = new RuntimeAcceptedPositionDriveController(
|
|
d.EntityObjects,
|
|
d.Runtime.Clock,
|
|
firstEntryCollision,
|
|
d.PlayerOutbound,
|
|
() => d.Runtime.Generation,
|
|
() => d.PlayerIdentity.ServerGuid,
|
|
() => d.PlayerController.Controller,
|
|
() => d.Character.UsePositionFromServer,
|
|
() => liveSessionSource.CurrentSession,
|
|
// C4 route 3: the portal arm's PlayerTeleported port needs the
|
|
// J5.4 autorun latch owner, one level above the raw controller.
|
|
() => d.PlayerController,
|
|
// A2/D-T2.4 review fix (2026-08-05): the SAME idempotent query
|
|
// the Place edge itself uses (WorldRevealCoordinator.
|
|
// CanPlacePortalDestination -> RuntimeWorldTransitState) lets a
|
|
// DeferredCell wake re-validate before reconciling instead of
|
|
// running the ack suffix against a reveal that ended or was
|
|
// superseded while the park sat outstanding.
|
|
isPortalAuthorityCurrent: portal => live.WorldTransit
|
|
.CanPlacePortalDestination(
|
|
portal.RevealGeneration,
|
|
portal.TeleportSequence,
|
|
portal.Projection.DestinationCell));
|
|
// C4 route 4b-2 (2026-08-04): the graphical remote-placement drive
|
|
// controller — route 4b-1's dormant owner, now driven by the remote
|
|
// far snap. Its service window is the graphical host's near-tier
|
|
// residency (GraphicalRemotePlacementServiceWindow's own remarks
|
|
// justify IsNearTier over IsNearTierOrPending). Shares the SAME entity
|
|
// lifetime, clock, and prepared-collision source as the two drive
|
|
// controllers above.
|
|
var remotePlacementDrive = new RuntimeRemotePlacementDriveController(
|
|
d.EntityObjects,
|
|
d.Runtime.Clock,
|
|
firstEntryCollision,
|
|
new GraphicalRemotePlacementServiceWindow(live.WorldState));
|
|
var hydration = new LiveEntityHydrationController(
|
|
live.LiveEntities,
|
|
d.EntityObjects,
|
|
d.DatLock,
|
|
projectionMaterializer,
|
|
new LiveEntityRelationshipProjection(live.EquippedChildren),
|
|
new LiveEntityReadyPublisher(
|
|
live.LiveEntities,
|
|
live.EntityEffects,
|
|
live.Presentation,
|
|
live.RenderSceneShadow?.LiveProjections),
|
|
originCoordinator,
|
|
networkUpdateBridge,
|
|
localPhysicsTimestamps,
|
|
d.PlayerIdentity,
|
|
deletion,
|
|
dormantLiveEntities,
|
|
d.Options.DumpLiveSpawns ? d.Log : null,
|
|
firstEntryDrive,
|
|
acceptedPositionDrive);
|
|
bindings.Adopt(
|
|
"landblock-loaded hydration",
|
|
live.LandblockLoaded.Bind(hydration));
|
|
Fault(SessionPlayerCompositionPoint.HydrationCreated);
|
|
|
|
var worldDropProjection =
|
|
new InventoryWorldDropProjectionController(
|
|
interaction.ItemInteraction,
|
|
d.EntityObjects.Objects,
|
|
live.LiveEntities,
|
|
hydration,
|
|
() => d.UpdateClock.SimulationTimeSeconds);
|
|
bindings.Adopt(
|
|
"inventory world-drop projection",
|
|
worldDropProjection);
|
|
|
|
var networkUpdates = new LiveEntityNetworkUpdateController(
|
|
live.LiveEntities,
|
|
d.EntityObjects.Objects,
|
|
hydration,
|
|
live.EntityEffects,
|
|
live.Presentation,
|
|
live.Lights,
|
|
live.EquippedChildren,
|
|
live.ProjectileController,
|
|
d.AnimatedEntities,
|
|
d.RemoteMovementObservations,
|
|
d.RemotePhysicsUpdater,
|
|
d.RemoteInboundMotion,
|
|
live.MotionRuntime,
|
|
d.PhysicsEngine,
|
|
content.Dats,
|
|
content.AnimationLoader,
|
|
d.Actions.CombatTarget,
|
|
d.WorldOrigin,
|
|
d.TeleportSink,
|
|
d.PlayerController,
|
|
d.PlayerOutbound,
|
|
d.PlayerHost,
|
|
d.PlayerIdentity,
|
|
d.UpdateClock,
|
|
liveSessionSource,
|
|
localPhysicsTimestamps.Publish,
|
|
d.MovementDiagnostics,
|
|
acceptedPositionDrive,
|
|
remotePlacementDrive,
|
|
worldDropProjection);
|
|
var liveness = new LiveEntityLivenessController(
|
|
live.LiveEntities,
|
|
d.PlayerIdentity,
|
|
deletion,
|
|
dormantLiveEntities);
|
|
var sessionEvents = new LiveEntitySessionController(
|
|
d.InboundEntityEvents,
|
|
hydration,
|
|
networkUpdates,
|
|
d.TeleportSink,
|
|
live.EntityEffects);
|
|
bindings.Adopt(
|
|
"live parent acceptance",
|
|
live.ParentAcceptance.BindOwned(
|
|
hydration.TryAcceptParentForProjection));
|
|
bindings.Adopt(
|
|
"same-generation network updates",
|
|
networkUpdateBridge.BindOwned(networkUpdates));
|
|
bindings.BindEntityReady(
|
|
live.EquippedChildren,
|
|
candidate => _ = hydration.OnEntityReady(candidate));
|
|
bindings.BindAppearanceApplied(
|
|
hydration,
|
|
guid =>
|
|
{
|
|
if (guid == d.PlayerIdentity.ServerGuid)
|
|
live.PaperdollPresenter?.MarkDirty();
|
|
});
|
|
Fault(SessionPlayerCompositionPoint.LiveEntityGraphBound);
|
|
|
|
d.WorldOrigin.SetPlaceholder(
|
|
world.TerrainBuild.InitialCenterX,
|
|
world.TerrainBuild.InitialCenterY);
|
|
bindings.Adopt(
|
|
"combat attack operations",
|
|
d.CombatAttackOperations.BindOwned(
|
|
new LiveCombatAttackOperations(
|
|
d.Actions.Combat,
|
|
new CombatAttackTargetSource(
|
|
d.Actions.Selection,
|
|
live.LiveEntities,
|
|
d.EntityObjects.Objects,
|
|
d.PlayerIdentity),
|
|
d.Settings,
|
|
d.PlayerController,
|
|
d.PlayerOutbound,
|
|
liveSessionSource,
|
|
liveSessionSource,
|
|
d.CombatFeedback)));
|
|
Fault(SessionPlayerCompositionPoint.CombatOperationsBound);
|
|
|
|
MouseLookController? mouseLook =
|
|
host.MouseSource is not null && host.MouseLookCursor is not null
|
|
? new MouseLookController(
|
|
host.MouseSource,
|
|
d.PointerPosition,
|
|
d.PlayerMode,
|
|
d.PlayerController,
|
|
host.CameraController,
|
|
d.ChaseCameraInput,
|
|
d.MovementInput,
|
|
d.PlayerOutbound,
|
|
liveSessionSource,
|
|
host.MouseLookCursor,
|
|
new EnvironmentInputMonotonicClock())
|
|
: null;
|
|
var gameplayInput = new GameplayInputFrameController(
|
|
host.InputDispatcher,
|
|
d.MovementInput,
|
|
mouseLook,
|
|
new CombatAttackInputFrameAdapter(interaction.CombatAttack));
|
|
if (host.CameraPointerInput is { } cameraPointer)
|
|
{
|
|
bindings.Adopt(
|
|
"camera pointer gameplay frame",
|
|
cameraPointer.BindGameplayFrameOwned(gameplayInput));
|
|
}
|
|
Fault(SessionPlayerCompositionPoint.GameplayInputBound);
|
|
|
|
var streamingFrame = new StreamingFrameController(
|
|
d.Options.LiveMode,
|
|
d.PlayerMode,
|
|
d.PlayerController,
|
|
liveSessionSource,
|
|
d.WorldOrigin,
|
|
networkUpdates,
|
|
new FlyCameraStreamingObserverSource(host.CameraController),
|
|
new PhysicsStreamingDungeonCellSource(d.PhysicsEngine),
|
|
streamingOriginRecenter,
|
|
streaming,
|
|
new LiveProjectionRescueRebucketter(
|
|
live.WorldState,
|
|
live.LiveEntities));
|
|
// Campaign A slice A5: retail rebuilds the ambient soundscape on every
|
|
// objcell change (24 m) and drains its deadline queue from the frame
|
|
// tick. The terrain words come from the loaded landblocks in the 3x3
|
|
// ring; the listener is the local player's cell and position.
|
|
IAmbientFramePhase? BuildAmbientFrame()
|
|
{
|
|
if (content.Audio?.Ambient is not { } ambient)
|
|
return null;
|
|
|
|
DatReaderWriter.DBObjs.Region? region =
|
|
content.Dats.Get<DatReaderWriter.DBObjs.Region>(0x13000000u);
|
|
if (region is null)
|
|
return null;
|
|
|
|
ambient.InstallRegion(region, LoadTerrainWords);
|
|
return new AmbientFramePhase(
|
|
ambient,
|
|
new LocalPlayerAmbientListenerSource(
|
|
d.PlayerController,
|
|
indoorLandblockLocal: (cellId, cellLocal) =>
|
|
{
|
|
// seen_outside interiors keep the outdoor ambient set
|
|
// (TS-66). The cell's WorldTransform maps its local
|
|
// frame into landblock coordinates, which is the frame
|
|
// the ambient walk centres on.
|
|
var cellStruct = d.PhysicsDataCache.GetCellStruct(cellId);
|
|
if (cellStruct is null || !cellStruct.SeenOutside)
|
|
return null;
|
|
return System.Numerics.Vector3.Transform(
|
|
cellLocal,
|
|
cellStruct.WorldTransform);
|
|
}));
|
|
|
|
ushort[]? LoadTerrainWords(uint landblockId)
|
|
{
|
|
if (!live.WorldState.TryGetLandblock(landblockId, out LoadedLandblock? loaded)
|
|
|| loaded?.Heightmap is not { } heightmap)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// The dat exposes terrain words as TerrainInfo; the gatherer wants
|
|
// the raw words. Converted here rather than in Core so the ambient
|
|
// model stays free of the dat type. This runs only on an objcell
|
|
// change (every 24 m), nine landblocks at a time.
|
|
var words = new ushort[heightmap.Terrain.Length];
|
|
for (int i = 0; i < words.Length; i++)
|
|
words[i] = (ushort)heightmap.Terrain[i];
|
|
return words;
|
|
}
|
|
}
|
|
|
|
var liveEffectFrame = new LiveEffectFrameController(
|
|
d.TranslucencyFades,
|
|
content.AnimationHookFrames,
|
|
live.EntityEffects,
|
|
content.ParticleSink,
|
|
live.Lights,
|
|
d.ParticleVisibility,
|
|
content.ParticleSystem,
|
|
content.ScriptRunner,
|
|
d.UpdateClock,
|
|
new SettingsParticleRangeSource(d.Settings),
|
|
BuildAmbientFrame());
|
|
var liveSpatialReconciler = new LiveSpatialPresentationReconciler(
|
|
live.EntityEffects,
|
|
live.EquippedChildren,
|
|
content.ParticleSink,
|
|
live.Lights,
|
|
live.RenderSceneShadow?.LiveProjections);
|
|
var localPlayerAnimation = new LocalPlayerAnimationController(
|
|
live.LiveEntities,
|
|
d.PlayerIdentity,
|
|
d.AnimatedEntities,
|
|
live.AnimationPresenter,
|
|
content.AnimationHookFrames);
|
|
// AP-145 fix (2026-08-05, #318): the ONE synchronizer instance is
|
|
// now constructed earlier, in LivePresentationComposition, so
|
|
// RuntimePlacementPresentationSink's Place edge and this session's
|
|
// ordinary per-tick movement publish through the exact same
|
|
// publisher / cache rather than two independent instances.
|
|
var localPlayerShadow = live.LocalPlayerShadowSynchronizer;
|
|
var localPlayerProjection = new LocalPlayerProjectionController(
|
|
new LiveLocalPlayerProjectionRuntime(
|
|
live.LiveEntities,
|
|
d.PlayerIdentity,
|
|
d.WorldOrigin,
|
|
localPlayerShadow));
|
|
var localPlayerFrameRuntime = new LiveLocalPlayerFrameRuntime(
|
|
host.CameraController,
|
|
d.PlayerMode,
|
|
d.PlayerController,
|
|
d.ChaseCameraInput,
|
|
d.MovementInput,
|
|
d.InputCapture,
|
|
live.LiveEntities,
|
|
d.PlayerIdentity,
|
|
d.PlayerHost,
|
|
localPlayerProjection,
|
|
d.PlayerOutbound,
|
|
liveSessionSource);
|
|
var localPlayerFrame = new RetailLocalPlayerFrameController(
|
|
d.Runtime,
|
|
localPlayerFrameRuntime,
|
|
d.MovementInput);
|
|
var liveObjectFrame = new LiveObjectFrameController(
|
|
d.InboundEntityEvents,
|
|
localPlayerFrame,
|
|
live.SelectionInteractions,
|
|
live.LiveEntities,
|
|
d.PlayerIdentity,
|
|
d.WorldOrigin,
|
|
live.AnimationScheduler,
|
|
live.StaticAnimationScheduler,
|
|
live.AnimationPresenter,
|
|
d.AnimatedEntities,
|
|
live.EquippedChildren,
|
|
liveEffectFrame,
|
|
live.RenderSceneShadow?.LiveProjections,
|
|
live.RenderSceneShadow?.StaticProjections);
|
|
Fault(SessionPlayerCompositionPoint.UpdateLeavesCreated);
|
|
|
|
var playerMode = new PlayerModeController(
|
|
d.PlayerMode,
|
|
d.PlayerController,
|
|
d.PlayerHost,
|
|
d.ChaseCameraInput,
|
|
host.CameraController,
|
|
d.PhysicsEngine,
|
|
live.LiveEntities,
|
|
d.PlayerIdentity,
|
|
d.WorldOrigin,
|
|
d.MotionBindings,
|
|
content.Dats,
|
|
d.DatLock,
|
|
content.CollisionAssets,
|
|
d.AnimatedEntities,
|
|
localPlayerAnimation,
|
|
localPlayerShadow,
|
|
d.PlayerApproachCompletions,
|
|
gameplayInput,
|
|
liveSessionSource,
|
|
d.MovementDiagnostics,
|
|
d.Character.MovementSkills,
|
|
d.ViewportAspect);
|
|
var playerModeAutoEntry = new PlayerModeAutoEntry(
|
|
new LivePlayerModeAutoEntryContext(
|
|
liveSessionSource,
|
|
live.LiveEntities,
|
|
d.PlayerIdentity,
|
|
worldReveal,
|
|
d.PlayerMode,
|
|
playerMode));
|
|
playerMode.BindAutoEntry(playerModeAutoEntry);
|
|
Fault(SessionPlayerCompositionPoint.PlayerModeBound);
|
|
|
|
// Campaign V slice V6m: the portal tunnel draws on both arms, so the
|
|
// presentation phase always leaves one in the fallback slot and the
|
|
// transfer is unconditional again. V6h's null presentation — the
|
|
// "no tunnel showing" stand-in a backend without a GL context used to
|
|
// drive — is deleted with the backend condition that produced it.
|
|
LocalPlayerTeleportController localTeleport =
|
|
d.PortalTunnelFallback.Transfer(CreateLocalTeleportWithTunnel);
|
|
|
|
LocalPlayerTeleportController CreateLocalTeleport(
|
|
ILocalPlayerTeleportPresentation presentation) =>
|
|
new LocalPlayerTeleportController(
|
|
new LiveLocalPlayerTeleportAuthority(
|
|
live.LiveEntities,
|
|
d.PlayerIdentity),
|
|
gameplayInput,
|
|
playerMode,
|
|
new LocalPlayerTeleportStreamingOperations(
|
|
d.WorldOrigin,
|
|
streamingOriginRecenter,
|
|
streaming,
|
|
sealedDungeonCells),
|
|
live.WorldTransit,
|
|
worldReveal,
|
|
new LocalPlayerTeleportPlacement(
|
|
live.LiveEntities,
|
|
d.PlayerIdentity,
|
|
d.PlayerController,
|
|
d.PlayerHost,
|
|
d.ChaseCameraInput,
|
|
liveSpatialReconciler),
|
|
new LocalPlayerTeleportSession(liveSessionSource),
|
|
presentation,
|
|
// C4 route 3: the portal arm shares route 2's Runtime
|
|
// SetPosition drive controller.
|
|
acceptedPositionDrive);
|
|
|
|
LocalPlayerTeleportController CreateLocalTeleportWithTunnel(
|
|
PortalTunnelPresentation portalTunnel)
|
|
{
|
|
var tunnelPresentation = new LocalPlayerTeleportPresentation(portalTunnel);
|
|
if (content.Audio?.UiSounds is { } portalUiSounds)
|
|
tunnelPresentation.UiSoundSink = sound => portalUiSounds.Play(sound);
|
|
return CreateLocalTeleport(tunnelPresentation);
|
|
}
|
|
// Campaign A slice A4: the AdminEnvirons stingers from
|
|
// Handle_Admin__Environs go through the interface sound bank. The portal
|
|
// enter/exit cues are attached to the tunnel presentation below, which is
|
|
// where retail's teleport-animation boundary plays them.
|
|
if (content.Audio?.UiSounds is { } environUiSounds)
|
|
{
|
|
d.WorldEnvironment.EnvironSoundSink =
|
|
changeType => environUiSounds.PlayEnvironCue(changeType);
|
|
}
|
|
var teleportLease = scope.Own(
|
|
"local-player teleport",
|
|
localTeleport,
|
|
static value => value.Dispose());
|
|
Fault(SessionPlayerCompositionPoint.PortalTransferred);
|
|
bindings.Adopt(
|
|
"local teleport network sink",
|
|
d.TeleportSink.BindOwned(localTeleport));
|
|
bindings.Adopt(
|
|
"selection view plane",
|
|
interaction.LateBindings.SelectionViewPlane.Bind(localTeleport));
|
|
Fault(SessionPlayerCompositionPoint.TeleportBound);
|
|
|
|
IDisposable componentLifecycleBinding =
|
|
live.ComponentLifecycle.BindOwned(teardown);
|
|
IDisposable componentLifecycleAdoption;
|
|
try
|
|
{
|
|
componentLifecycleAdoption = live.RuntimeBindings.AdoptOwned(
|
|
"live runtime component teardown",
|
|
componentLifecycleBinding);
|
|
}
|
|
catch
|
|
{
|
|
componentLifecycleBinding.Dispose();
|
|
throw;
|
|
}
|
|
var componentLifecycleLease = scope.Own(
|
|
"live runtime component teardown adoption",
|
|
componentLifecycleAdoption,
|
|
static value => value.Dispose());
|
|
|
|
AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? vitals =
|
|
interaction.RetainedUi?.Vitals;
|
|
var placementProjectionRetry =
|
|
new RuntimePlacementProjectionRetrySlot(
|
|
() => d.Runtime.Generation);
|
|
var sessionRuntimeFactory = new LiveSessionRuntimeFactory(
|
|
new LiveSessionPlayerRuntime(
|
|
d.PlayerIdentity,
|
|
d.PlayerController,
|
|
d.WorldOrigin),
|
|
new LiveSessionDomainRuntime(
|
|
d.Runtime,
|
|
d.EntityObjects,
|
|
d.Character,
|
|
d.Actions,
|
|
d.Inventory,
|
|
d.Communication),
|
|
new LiveSessionUiRuntime(
|
|
interaction.RetainedUi?.Runtime,
|
|
vitals,
|
|
interaction.RetainedUi?.CharacterSheet,
|
|
interaction.Magic,
|
|
live.PaperdollPresenter),
|
|
new LiveSessionInteractionRuntime(
|
|
d.Settings,
|
|
gameplayInput,
|
|
playerMode,
|
|
playerModeAutoEntry,
|
|
interaction.ItemInteraction,
|
|
interaction.CombatAttack,
|
|
live.SelectionInteractions),
|
|
new LiveSessionWorldRuntime(
|
|
content.Dats,
|
|
live.WorldState,
|
|
live.LiveEntities,
|
|
sessionEvents,
|
|
d.WorldEnvironment,
|
|
d.TeleportSink,
|
|
spawnClaimClassifier,
|
|
live.EquippedChildren,
|
|
live.SelectionScene,
|
|
d.ParticleVisibility,
|
|
d.InboundEntityEvents,
|
|
liveness,
|
|
networkUpdates,
|
|
hydration,
|
|
live.EntityEffects,
|
|
content.AnimationHookFrames,
|
|
live.Presentation,
|
|
d.RemoteMovementObservations,
|
|
live.RenderSceneShadow,
|
|
live.PlacementProjection,
|
|
placementProjectionRetry,
|
|
firstEntryDrive,
|
|
acceptedPositionDrive,
|
|
remotePlacementDrive),
|
|
liveSessionCommands,
|
|
d.Log);
|
|
LiveSessionHost sessionHost = sessionRuntimeFactory.Create(
|
|
liveSession,
|
|
new LiveSessionConnectOptions(
|
|
d.Options.LiveMode,
|
|
d.Options.LiveHost,
|
|
d.Options.LivePort,
|
|
d.Options.LiveUser ?? string.Empty,
|
|
d.Options.LivePass ?? string.Empty));
|
|
Fault(SessionPlayerCompositionPoint.SessionHostCreated);
|
|
|
|
// The ImGui developer-tools debug toast sink was removed at Campaign V
|
|
// slice V11 along with the rest of the ImGui frontend; there is no
|
|
// replacement toast surface yet.
|
|
Action<string>? debugToast = null;
|
|
var combatModeOperations = new LiveCombatModeOperations(
|
|
new LiveSessionCombatModeAuthority(sessionHost),
|
|
new LocalPlayerCombatEquipmentSource(
|
|
d.EntityObjects.Objects,
|
|
d.PlayerIdentity),
|
|
new ItemInteractionCombatModeIntentSink(
|
|
interaction.ItemInteraction));
|
|
bindings.Adopt(
|
|
"runtime combat-mode operations",
|
|
d.CombatModeOperations.BindOwned(combatModeOperations));
|
|
var combatCommand = new RuntimeCombatModeCommandAdapter(
|
|
d.Actions.CombatMode,
|
|
d.Log,
|
|
debugToast,
|
|
text => d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal));
|
|
bindings.Adopt(
|
|
"live combat-mode commands",
|
|
d.CombatModeCommands.BindOwned(combatCommand));
|
|
var gameRuntime = new CurrentGameRuntimeAdapter(
|
|
d.Runtime,
|
|
sessionHost,
|
|
liveSessionCommands,
|
|
live.SelectionInteractions);
|
|
bindings.Adopt("current game runtime adapter", gameRuntime);
|
|
bindings.Adopt(
|
|
"retained-UI game runtime commands",
|
|
interaction.LateBindings.GameRuntime.Bind(gameRuntime, gameRuntime));
|
|
|
|
var nearbyDiagnostics = new NearbyWorldDiagnosticDumper(
|
|
new RuntimeNearbyWorldDiagnosticSource(
|
|
d.PlayerMode,
|
|
d.PlayerController,
|
|
host.CameraController,
|
|
d.WorldOrigin,
|
|
live.WorldState,
|
|
d.PhysicsEngine),
|
|
d.Log);
|
|
var runtimeDiagnostics = new RuntimeDiagnosticCommandController(
|
|
d.WorldEnvironment,
|
|
d.WorldSceneDebugState,
|
|
host.CameraPointerInput,
|
|
nearbyDiagnostics,
|
|
debugToast);
|
|
bindings.Adopt(
|
|
"runtime diagnostic commands",
|
|
d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics));
|
|
Fault(SessionPlayerCompositionPoint.CommandsBound);
|
|
|
|
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
|
GameplayInputActionRouter>? gameplayActionsLease = null;
|
|
if (host.InputDispatcher is { } dispatcher)
|
|
{
|
|
CameraPointerInputController pointer = host.CameraPointerInput
|
|
?? throw new InvalidOperationException(
|
|
"Gameplay action routing requires the composed pointer owner.");
|
|
var commands = new GameplayInputCommandController(
|
|
new RetainedGameplayWindowCommands(
|
|
interaction.RetainedUi?.Runtime),
|
|
new DevToolsGameplayCommands(),
|
|
runtimeDiagnostics,
|
|
new PlayerModeGameplayCommands(
|
|
d.PlayerMode,
|
|
playerMode),
|
|
new ItemTargetModeCommands(interaction.ItemInteraction),
|
|
new GameplayCameraModeCommands(host.CameraController),
|
|
gameRuntime,
|
|
gameRuntime.Combat,
|
|
new GameplayWindowCommands(d.Window.Close),
|
|
toggleAudioMute: content.Audio?.Engine is { } audioEngine
|
|
? () =>
|
|
{
|
|
audioEngine.Muted = !audioEngine.Muted;
|
|
Console.WriteLine(
|
|
audioEngine.Muted
|
|
? "audio: muted (Ctrl+M to unmute)"
|
|
: "audio: unmuted");
|
|
}
|
|
: null);
|
|
var targets = new RuntimeGameplayInputPriorityTargets(
|
|
gameplayInput,
|
|
pointer,
|
|
interaction.RetainedUi?.Runtime,
|
|
live.SelectionInteractions,
|
|
gameRuntime,
|
|
gameRuntime.Selection,
|
|
gameRuntime.MovementCommands,
|
|
commands);
|
|
GameplayInputActionRouter gameplayActions =
|
|
GameplayInputActionRouter.Create(
|
|
dispatcher,
|
|
d.Actions.Combat,
|
|
targets,
|
|
d.HostQuiescence,
|
|
d.Log);
|
|
gameplayActionsLease = scope.Own(
|
|
"gameplay input actions",
|
|
gameplayActions,
|
|
static value => value.Dispose());
|
|
gameplayActions.Attach();
|
|
}
|
|
Fault(SessionPlayerCompositionPoint.GameplayActionsAttached);
|
|
|
|
var bindingsLease = scope.Own(
|
|
"session/player runtime bindings",
|
|
bindings,
|
|
static value => value.Dispose());
|
|
bindingsOwnedByScope = true;
|
|
var result = new SessionPlayerResult(
|
|
streamerLease.Resource,
|
|
streaming,
|
|
streamingOriginRecenter,
|
|
worldReveal,
|
|
spawnClaimClassifier,
|
|
liveSession,
|
|
hydration,
|
|
networkUpdates,
|
|
liveness,
|
|
sessionEvents,
|
|
gameplayInput,
|
|
streamingFrame,
|
|
localPlayerAnimation,
|
|
localPlayerShadow,
|
|
localPlayerFrameRuntime,
|
|
localPlayerFrame,
|
|
liveSpatialReconciler,
|
|
liveObjectFrame,
|
|
playerMode,
|
|
playerModeAutoEntry,
|
|
teleportLease.Resource,
|
|
sessionHost,
|
|
placementProjectionRetry,
|
|
gameRuntime,
|
|
gameplayActionsLease?.Resource,
|
|
bindings);
|
|
_publication.PublishSessionPlayer(result);
|
|
|
|
streamerLease.Transfer();
|
|
teleportLease.Transfer();
|
|
gameplayActionsLease?.Transfer();
|
|
componentLifecycleLease.Transfer();
|
|
bindingsLease.Transfer();
|
|
Fault(SessionPlayerCompositionPoint.ResultPublished);
|
|
return result;
|
|
}
|
|
|
|
private void Fault(SessionPlayerCompositionPoint point) =>
|
|
_faultInjection?.Invoke(point);
|
|
}
|