1299 lines
54 KiB
C#
1299 lines
54 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,
|
|
/// <summary>Campaign LA slice LA1: the shared per-session status-event
|
|
/// writer, no-op when <see cref="RuntimeOptions.StatusFilePath"/> was
|
|
/// not configured.</summary>
|
|
SessionStatusWriter StatusWriter)
|
|
{
|
|
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();
|
|
// CH3 (2026-08-09): constructed here (rather than at its original
|
|
// site further down, alongside LiveSessionAppSource) purely so
|
|
// RuntimeSettingsTargets can publish SetSingleCharacterOption
|
|
// through the SAME stable command surface the retained UI uses —
|
|
// LiveSessionCommandSurface has no dependencies of its own, so
|
|
// hoisting its construction is inert; the later site now reuses
|
|
// this instance instead of constructing a second one.
|
|
var liveSessionCommands = new LiveSessionCommandSurface();
|
|
var settingsTargets = new RuntimeSettingsTargets(
|
|
new SilkRuntimeDisplayWindowTarget(d.Window),
|
|
live.DrawDispatcher,
|
|
foundation.TerrainAtlas,
|
|
streaming,
|
|
d.RenderRange,
|
|
interaction.RetainedUi?.Host.Root,
|
|
liveSessionCommands,
|
|
// CH6c: null when no retained UI exists (e.g. a no-window host) — the
|
|
// Chat tab's opacity sliders then apply through NullRuntimeChatOpacityTarget.
|
|
chatOpacity: interaction.RetainedUi?.Runtime.WindowOpacity,
|
|
// #389 blast MUST-FIX 2: the live Field-of-View apply target —
|
|
// see RuntimeSettingsTargets' cameras ctor doc.
|
|
cameras: host.CameraController,
|
|
log: d.Log,
|
|
// OP6: null on a no-audio/headless host — ApplyAudio then
|
|
// silently no-ops, same shape as chatOpacity above.
|
|
audio: content.Audio?.Engine);
|
|
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,
|
|
liveSessionCommands,
|
|
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,
|
|
// CH3 (2026-08-09): threaded through from ComposeCore, where it was
|
|
// constructed early (alongside RuntimeSettingsTargets) instead of at
|
|
// its original site below — see the construction-site comment.
|
|
LiveSessionCommandSurface liveSessionCommands,
|
|
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;
|
|
// liveSessionCommands arrives as a parameter — constructed in
|
|
// ComposeCore alongside RuntimeSettingsTargets (CH3, 2026-08-09) and
|
|
// threaded through, not reconstructed here.
|
|
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),
|
|
// D7 Group-C re-point (Campaign OP OP4): server bit,
|
|
// not the client-local GameplaySettings record — see
|
|
// CharacterOptionCombatSettingsSource's doc comment.
|
|
new CharacterOptionCombatSettingsSource(d.Character.Options),
|
|
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,
|
|
d.StatusWriter,
|
|
d.Options.SessionId ?? "app",
|
|
d.Options.LoginCommands,
|
|
d.Options.LoginCommandDelayMs);
|
|
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,
|
|
d.Options.LiveCharacterSelector,
|
|
AwaitCharacterSelection:
|
|
d.Options.LiveCharacterSelector is null));
|
|
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.AddText(text, 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),
|
|
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);
|
|
}
|