Flips the last remote classification (SetPosition: teleport-advanced and
cell-less) onto 4b-1's RuntimeRemotePlacementDriveController, runs retail's
teleport_hook before the placement, and deletes the legacy remote-teleport
machinery. Contract: docs/research/2026-08-04-c4-route-4b-3-contract.md.
Retail: MoveOrTeleport @0x00516330's branch @0x00516386 -> teleport_hook
@0x005163EF -> SetFlags(0x1012) @0x00516414 -> SetPosition @0x00516420 ->
return 1 @0x00516438. The hook @0x00514ED0 runs BEFORE the placement and
regardless of its outcome. Retail places this branch unconditionally, at any
distance and any contact state (arg4 is read only @0x0051638E, after the
branch) — which is what retires AP-137's cell-less enqueue-vs-place delta.
D1 — the classifier's cell-less input is now the PRE-merge committed cell.
Retail's predicate is `this_1->cell == 0`, the BODY's own cell at
MoveOrTeleport entry (this_1 is assigned from this @0x00516334). acdream fed
the POST-merge canonical.FullCellId, which RefreshSnapshot ->
RefreshDerivedState -> SetFullCell has already stamped with the accepted wire
cell; a zero wire cell fails validation into RejectedData first. The shipped
remote cell-less predicate was therefore dead code, not merely different from
remotePlacementRequired. Threaded via a builder overload; route 1's overload
is untouched. The graphical !IsSpatiallyVisible arm of
projectionRequiresTeleportHook is deleted — a presentation predicate with no
retail analogue that fired the teleport machinery on a routine hot path.
Deleted: RemoteTeleportController (605), RemoteTeleportPlacement (85),
RemoteShadowPlacementSynchronizer (49), their 1,709 lines of tests, the
remotePlacementRequired predicate, the TeleportHookRequired plumbing, the
legacy pre-operation ConstrainTo fallback, and the player arm's legacy
!IsGrounded fallback. Net -2,030 lines.
Structural fix (two independent Opus reviews, round 1 FAIL/FAIL): three of the
four MAJORs were one defect — OnPosition carried two parallel inline copies of
the routing tail (player-guid, NPC-guid) that had drifted. Extracted
RunRemoteArmTail (3 call sites) and ApplyWireAirborneLeftoverBookkeeping (2),
both branches now share one implementation.
A1 ToConstraintArm mapped AirborneSnap -> AirborneNoOperation, so the NPC
arm armed ConstrainTo ZERO times for an out-of-contact wire-grounded
creature — a regression this slice introduced while closing a
structurally identical hole. Now maps to NearInterpolate; switch made
total with a throwing default proven unreachable.
R1 D2's write-nothing shape existed on the player arm only; NPC packets
fell through and wrote the body. Retail makes no player/NPC distinction.
R2 report_collision_end(this,1) @0x00514F31 was bound to
ShadowObjects.Suspend, a port of a DIFFERENT retail function
(remove_shadows_from_cells) that teleport_hook never calls. Now routes
to RuntimeCollisionReportingState.LeaveWorld, which wraps the private
ForceEnd in an admission-blocking transaction so a DoCollisionEnd
callback cannot recreate the contact table.
R3/A2 A teleported NPC synthesized ServerVelocity from the teleport distance
(~1,000+ m/s) and planned a run cycle from it. Both the install and
RemoteServerControlledVelocityCycle.Apply now gate on !isTeleportRoute.
BISECT HAZARD — A1's fix is correct only BECAUSE R1 landed. AirborneSnap is
reachable wire-airborne on the NPC arm only while D2's shape is missing there.
Reverting R1 alone silently inverts A1 into the opposite divergence: arming
where retail returns 0. Revert both or neither.
Also in the velocity hunk: the NPC block's two !IsPlayerGuid(update.Guid)
guards were dropped when it was wrapped in `if (!isTeleportRoute)`. Safe — all
five exit paths of the enclosing IsPlayerGuid block return, so the predicate is
unconditionally false below it — but it was unremarked by both reviews.
Register: AP-137 REWRITTEN (not deleted) to the surviving acdream-only
divergences — null classification during the login window and Rejected*
through UnroutedCatchUp keep a row. AD-42's RemoteTeleportController citation
retired; AP-136/AP-138 writer lists corrected to the two surviving non-Position
rebucket writers; AP-138 gains the teleport arm as a second producer of the
visible-without-collision residual (retirement path remains #309). AP-135 is
untouched and its two airborne bookkeeping writes are preserved on both arms.
AP-131 does not retire; #276 does not close.
Proof obligation 1: ParkCollisionResidents' overlap throw stays unreachable —
the teleport arm adds packets to the same TryBeginExclusiveAuthoredPlacement
one-operation-per-key machinery the far arm uses, opens no new operation shape,
and every DeferredCell outcome cancels synchronously with
restoreCancelledPark: true. The guarded property remains
HasOldPrefixPlacementDebt's stall, not a throw (4b-1's B2 caveat stands).
Correction to an earlier claim: LiveEntityPresentationController's
_activePlacementOwners was NOT write-never at HEAD —
remotePlacementRequired -> BeginPlacement -> Begin -> BeginAuthoritativePlacement
was a live writer chain. It becomes write-never BECAUSE this slice deletes that
chain, which is why deleting the dead half is behaviour-preserving.
Probe: ACDREAM_PROBE_REMOTE_TELEPORT=1 emits one [remote-teleport] line per
routed arm (guid, cause, hook-ran, placement status). TEMPORARY, strip with the
probe family.
Carried, disclosed not fixed: no dedicated bidirectional collision-partner test
for R2 (the wiring, not LeaveWorld itself, is what lacks coverage); the
stress test's teleport step drives hand-written field assignments rather than
the canonical arm; the per-packet runTeleportHook closure allocation (network
path, not the resolve path Slice I's 0 B discipline governs — file before
route 5 adds a fourth call site). B2: IRuntimeCollisionReportObserver has zero
production implementations, so retail's bidirectional DoCollisionEnd half still
reaches no gameplay consumer — this fix closes the wrong-function binding, not
that nobody listens.
Complete Release suite MEASURED at 11,013 passed / 4 skipped / 0 failed
(baseline 11,027/4/0; net -14 = ~33 deleted test cases against ~19 added).
Neither known flake fired (#302 PortalProjectionTests GC-allocation, #308
NakEmissionTests wall-clock).
STILL OWED: the two-client connected gate, which MUST use an NPC/creature
teleport target. Both round-1 MAJORs lived on the NPC arm and the velocity
cycle early-returns for 0x50xxxxxx guids, so a player target structurally
cannot observe A1, A2, or R3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1145 lines
45 KiB
C#
1145 lines
45 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.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,
|
|
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);
|
|
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);
|
|
// 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 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));
|
|
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));
|
|
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);
|
|
var localPlayerShadow = new LocalPlayerShadowSynchronizer(
|
|
d.PhysicsEngine,
|
|
live.LiveEntities,
|
|
d.PlayerIdentity,
|
|
d.WorldOrigin,
|
|
d.PlayerShadow);
|
|
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(
|
|
d.PhysicsEngine,
|
|
live.LiveEntities,
|
|
d.PlayerIdentity,
|
|
d.PlayerController,
|
|
d.PlayerHost,
|
|
d.ChaseCameraInput,
|
|
d.WorldOrigin,
|
|
liveSpatialReconciler),
|
|
new LocalPlayerTeleportSession(liveSessionSource),
|
|
presentation);
|
|
|
|
LocalPlayerTeleportController CreateLocalTeleportWithTunnel(
|
|
PortalTunnelPresentation portalTunnel) =>
|
|
CreateLocalTeleport(
|
|
new LocalPlayerTeleportPresentation(portalTunnel));
|
|
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, 0x1Au));
|
|
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));
|
|
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);
|
|
}
|