refactor(app): compose session and player startup
Move streaming, live-session, hydration, local-player, combat, and teleport construction behind the typed Phase-7 boundary. Add exact-owner runtime bindings and focused spawn-claim classification so partial startup rolls back without retaining old session targets while preserving the accepted construction and frame dependencies. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
3573da12e1
commit
7771c07fb6
31 changed files with 1759 additions and 532 deletions
|
|
@ -27,13 +27,14 @@ What does NOT go here:
|
||||||
## Current queue — 2026-07-22
|
## Current queue — 2026-07-22
|
||||||
|
|
||||||
- **Active structural work:** `GameWindow` decomposition. Slices 1–7 and Slice
|
- **Active structural work:** `GameWindow` decomposition. Slices 1–7 and Slice
|
||||||
8 checkpoints A–H are complete; I.1–I.6 now own the executable ordered
|
8 checkpoints A–H are complete; I.1–I.7 now own the executable ordered
|
||||||
startup path through retained UI and live presentation, including the
|
startup path through retained UI and live presentation, including the
|
||||||
canonical runtime/world, selection/radar, landblock publishers, and
|
canonical runtime/world, selection/radar, landblock publishers, and
|
||||||
portal/sky/particle resources.
|
portal/sky/particle resources.
|
||||||
`GameWindow.OnUpdate` and `GameWindow.OnRender` are typed orchestration
|
I.7 now owns streaming/session/hydration/local-player/teleport startup and
|
||||||
handoffs and the class is 2,840 raw lines. I.7–I.9 plus the remaining
|
every late edge explicitly. `GameWindow.OnUpdate` and `GameWindow.OnRender`
|
||||||
shutdown/closeout
|
are typed orchestration handoffs and the class is 2,479 raw lines. I.8–I.9
|
||||||
|
plus the remaining shutdown/closeout
|
||||||
cleanup is active in
|
cleanup is active in
|
||||||
[`docs/architecture/code-structure.md`](architecture/code-structure.md).
|
[`docs/architecture/code-structure.md`](architecture/code-structure.md).
|
||||||
This is the behavior-preserving prerequisite before new M4 feature bodies.
|
This is the behavior-preserving prerequisite before new M4 feature bodies.
|
||||||
|
|
|
||||||
|
|
@ -731,11 +731,14 @@ retained UI, live presentation, and landblock publication into ordered
|
||||||
composition phases. Typed exact-owner sources bridge later session,
|
composition phases. Typed exact-owner sources bridge later session,
|
||||||
selection, radar, view-plane, diagnostics, inventory, hydration, and automation
|
selection, radar, view-plane, diagnostics, inventory, hydration, and automation
|
||||||
owners without callbacks to the window. The old item-use and selection wrapper
|
owners without callbacks to the window. The old item-use and selection wrapper
|
||||||
methods are deleted. `GameWindow` is 2,840 raw lines after I.6. I.7–L remain
|
methods are deleted. I.7 now owns the complete streaming/session/hydration/
|
||||||
active, with session, streaming, and hydration composition next. The App gate
|
local-player/teleport construction phase and every late edge through named
|
||||||
passes 3,407 tests / 3 intentional skips and the complete Release suite passes
|
exact-owner tokens; the former 432-line inline body and spawn-claim memo are
|
||||||
7,779 tests / 5 intentional skips. The clean solution build retains only the
|
gone. `GameWindow` is 2,479 raw lines after I.7. I.8–L remain active, with
|
||||||
17 test-project warnings tracked by #228; the I.6 behavior, architecture, and
|
atomic frame-root publication and terminal session start next. The App gate
|
||||||
|
passes 3,420 tests / 3 intentional skips and the complete Release suite passes
|
||||||
|
7,792 tests / 5 intentional skips. The clean solution build retains only the
|
||||||
|
17 test-project warnings tracked by #228; the I.7 behavior, architecture, and
|
||||||
adversarial reviews are clean.
|
adversarial reviews are clean.
|
||||||
|
|
||||||
### 4.4 Exit criteria
|
### 4.4 Exit criteria
|
||||||
|
|
|
||||||
|
|
@ -97,15 +97,17 @@ runtime-target binding without replay, and one current/toon settings owner.
|
||||||
`GameWindow` is 3,663 lines / 162 fields / 37 methods at G. Checkpoint H adds
|
`GameWindow` is 3,663 lines / 162 fields / 37 methods at G. Checkpoint H adds
|
||||||
sole terrain-atlas and sky-shader roots, one retained Host/runtime lease, an
|
sole terrain-atlas and sky-shader roots, one retained Host/runtime lease, an
|
||||||
atomic update/render frame-root pair, prepared portal fallback/transfer, and
|
atomic update/render frame-root pair, prepared portal fallback/transfer, and
|
||||||
checked retryable GL leaf construction and state mutation. Checkpoint I.1–I.6
|
checked retryable GL leaf construction and state mutation. Checkpoint I.1–I.7
|
||||||
now provide the executable ordered startup pipeline through retained UI and
|
now provide the executable ordered startup pipeline through retained UI and
|
||||||
live presentation. The canonical live runtime/world, presentation/effect
|
live presentation. The canonical live runtime/world, presentation/effect
|
||||||
controllers, selection/radar, landblock publishers, and portal/sky/particle
|
controllers, selection/radar, landblock publishers, and portal/sky/particle
|
||||||
resources have transactional phase ownership and exact-owner late bindings.
|
resources have transactional phase ownership and exact-owner late bindings.
|
||||||
`GameWindow` is now 2,840 raw lines. I.7–I.9, shutdown, canonical soak
|
I.7 adds the complete streaming/session/hydration/local-player/teleport phase,
|
||||||
snapshots, and closeout checkpoints J–L remain. The App gate passes 3,407 tests
|
including explicit reversible ownership for every late edge. `GameWindow` is
|
||||||
/ 3 intentional skips, the complete Release suite passes 7,779 tests / 5
|
now 2,479 raw lines. I.8–I.9, shutdown, canonical soak snapshots, and closeout
|
||||||
skips, and all three I.6 corrected-diff reviews are clean.
|
checkpoints J–L remain. The App gate passes 3,420 tests / 3 intentional skips,
|
||||||
|
the complete Release suite passes 7,792 tests / 5 skips, and all three I.7
|
||||||
|
corrected-diff reviews are clean.
|
||||||
|
|
||||||
This is a behavior-preserving structural program. Severe regressions still get
|
This is a behavior-preserving structural program. Severe regressions still get
|
||||||
root-cause fixes in separate commits; ordinary feature work resumes with M4
|
root-cause fixes in separate commits; ordinary feature work resumes with M4
|
||||||
|
|
|
||||||
|
|
@ -53,15 +53,17 @@ startup application, and inert late target binding. `GameWindow` is 3,663
|
||||||
lines / 162 fields / 37 methods at G. Checkpoint H adds sole terrain/sky
|
lines / 162 fields / 37 methods at G. Checkpoint H adds sole terrain/sky
|
||||||
resource roots, one retained Host/runtime lease, an atomic update/render root
|
resource roots, one retained Host/runtime lease, an atomic update/render root
|
||||||
pair, a prepared portal fallback/transfer transaction, and checked retryable GL
|
pair, a prepared portal fallback/transfer transaction, and checked retryable GL
|
||||||
leaf construction. Checkpoint I.1–I.6 now provide the executable ordered
|
leaf construction. Checkpoint I.1–I.7 now provide the executable ordered
|
||||||
startup pipeline through retained UI and live presentation. The canonical live
|
startup pipeline through retained UI and live presentation. The canonical live
|
||||||
runtime/world, presentation/effect controllers, selection/radar, landblock
|
runtime/world, presentation/effect controllers, selection/radar, landblock
|
||||||
publishers, and portal/sky/particle resources have transactional phase
|
publishers, and portal/sky/particle resources have transactional phase
|
||||||
ownership and exact-owner late bindings. `GameWindow` is 2,840 raw lines.
|
ownership and exact-owner late bindings. I.7 now owns the complete streaming,
|
||||||
I.7–I.9 and checkpoints J–L remain active. Issue #232 tracks process-residency
|
session, hydration, local-player, combat, and teleport startup graph, including
|
||||||
variance in the soak without loosening its leak threshold. The App gate passes
|
named reversible late edges. `GameWindow` is 2,479 raw lines. I.8–I.9 and
|
||||||
3,407 tests / 3 skips, the complete Release suite passes 7,779 tests / 5 skips,
|
checkpoints J–L remain active. Issue #232 tracks process-residency variance in
|
||||||
and all three I.6 corrected-diff reviews are clean.
|
the soak without loosening its leak threshold. The App gate passes 3,420 tests
|
||||||
|
/ 3 skips, the complete Release suite passes 7,792 tests / 5 skips, and all
|
||||||
|
three I.7 corrected-diff reviews are clean.
|
||||||
|
|
||||||
Carried:
|
Carried:
|
||||||
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus
|
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus
|
||||||
|
|
|
||||||
|
|
@ -281,9 +281,16 @@ ledger only after the complete pipeline is live.
|
||||||
under the same ownership contract. `GameWindow.cs` is 2,840 raw lines; the
|
under the same ownership contract. `GameWindow.cs` is 2,840 raw lines; the
|
||||||
App gate passes 3,407 tests / 3 intentional skips and the complete Release
|
App gate passes 3,407 tests / 3 intentional skips and the complete Release
|
||||||
suite passes 7,779 tests / 5 intentional skips. I.7 session, streaming, and
|
suite passes 7,779 tests / 5 intentional skips. I.7 session, streaming, and
|
||||||
hydration composition is the active cut; its ownership, rollback, and gate
|
hydration composition is complete; its ownership, rollback, and gate
|
||||||
contract is
|
contract is
|
||||||
[`2026-07-22-gamewindow-slice-8-checkpoint-i7-session-player.md`](2026-07-22-gamewindow-slice-8-checkpoint-i7-session-player.md).
|
[`2026-07-22-gamewindow-slice-8-checkpoint-i7-session-player.md`](2026-07-22-gamewindow-slice-8-checkpoint-i7-session-player.md).
|
||||||
|
- I.7 removes the former 432-line Phase-7 body. Streaming, session, hydration,
|
||||||
|
local-player, combat, and teleport startup now compose through one typed
|
||||||
|
result; all late edges have named reversible exact-owner tokens. The
|
||||||
|
spawn-claim DAT range memo has a focused owner. `GameWindow.cs` is 2,479 raw
|
||||||
|
lines. App Release passes 3,420 tests / 3 skips; the complete Release suite
|
||||||
|
passes 7,792 tests / 5 skips with the same 17 warnings tracked by #228. I.8
|
||||||
|
frame-root publication and terminal session start is the active cut.
|
||||||
Checkpoint I remains active until I.9 closes the complete nine-phase pipeline.
|
Checkpoint I remains active until I.9 closes the complete nine-phase pipeline.
|
||||||
|
|
||||||
## 6. Automated acceptance
|
## 6. Automated acceptance
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# GameWindow Slice 8 — Checkpoint I.7 Session and Player Composition
|
# GameWindow Slice 8 — Checkpoint I.7 Session and Player Composition
|
||||||
|
|
||||||
**Status:** Active
|
**Status:** Complete 2026-07-22
|
||||||
|
|
||||||
**Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`
|
**Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`
|
||||||
|
|
||||||
|
|
@ -183,3 +183,20 @@ edge has explicit reversible ownership, failure and retry gates pass, the old
|
||||||
window body is gone, documentation/memory report the measured class size and
|
window body is gone, documentation/memory report the measured class size and
|
||||||
test totals, and the commit is independently buildable. I.8 then composes the
|
test totals, and the commit is independently buildable. I.8 then composes the
|
||||||
update/render roots and terminal session start over the typed result graph.
|
update/render roots and terminal session start over the typed result graph.
|
||||||
|
|
||||||
|
## 8. Completion evidence
|
||||||
|
|
||||||
|
- `SessionPlayerCompositionPhase` is the production Phase-7 path. The former
|
||||||
|
432-line radius/streaming/session/hydration/player/teleport block is absent
|
||||||
|
from `GameWindow`.
|
||||||
|
- `SessionPlayerRuntimeBindings` plus exact-owner tokens cover every Phase-7
|
||||||
|
late edge. Focused tests cover reverse retry, rebind, and stale-token safety.
|
||||||
|
- `DatSpawnClaimHydrationClassifier` owns the former window memo and exact
|
||||||
|
indoor-cell boundaries, including missing/empty DAT cases and reset.
|
||||||
|
- `GameWindow.cs` is 2,479 raw lines, down 13,244 lines (84.2%) from the
|
||||||
|
15,723-line campaign baseline.
|
||||||
|
- App Release: 3,420 passed / 3 intentional skips. Complete Release suite:
|
||||||
|
7,792 passed / 5 intentional skips. Clean solution build: 0 errors and the
|
||||||
|
17 pre-existing test warnings tracked by #228.
|
||||||
|
- Behavior/order, architecture/ownership, and adversarial rollback reviews are
|
||||||
|
clean. No gameplay mechanism changed and no divergence row was introduced.
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ audit.
|
||||||
- [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots
|
- [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots
|
||||||
explicit single ownership and transfer seams.
|
explicit single ownership and transfer seams.
|
||||||
- [ ] I — group `OnLoad` into small ordered, fakeable composition phases with
|
- [ ] I — group `OnLoad` into small ordered, fakeable composition phases with
|
||||||
transactional partial-acquisition rollback. Active detailed plan:
|
transactional partial-acquisition rollback. I.1–I.7 are complete; I.8
|
||||||
|
frame-root publication and terminal session start is active. Detailed plan:
|
||||||
[`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md).
|
[`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md).
|
||||||
- [ ] J — move the exact retryable shutdown manifest to a focused lifetime
|
- [ ] J — move the exact retryable shutdown manifest to a focused lifetime
|
||||||
owner and prove all partial-load/reentrant/retry paths.
|
owner and prove all partial-load/reentrant/retry paths.
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,22 @@ internal sealed class CombatAttackOperationsSlot : ICombatAttackOperations
|
||||||
_owner = owner;
|
_owner = owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindOwned(ICombatAttackOperations owner)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(owner);
|
||||||
|
if (_owner is not null)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Combat attack operations are already bound.");
|
||||||
|
_owner = owner;
|
||||||
|
return new Binding(this, owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unbind(ICombatAttackOperations expected)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_owner, expected))
|
||||||
|
_owner = null;
|
||||||
|
}
|
||||||
|
|
||||||
public bool CanStartAttack() => _owner?.CanStartAttack() == true;
|
public bool CanStartAttack() => _owner?.CanStartAttack() == true;
|
||||||
public void PrepareAttackRequest() => _owner?.PrepareAttackRequest();
|
public void PrepareAttackRequest() => _owner?.PrepareAttackRequest();
|
||||||
public bool SendAttack(AttackHeight height, float power) =>
|
public bool SendAttack(AttackHeight height, float power) =>
|
||||||
|
|
@ -75,6 +91,23 @@ internal sealed class CombatAttackOperationsSlot : ICombatAttackOperations
|
||||||
public bool IsDualWield => _owner?.IsDualWield == true;
|
public bool IsDualWield => _owner?.IsDualWield == true;
|
||||||
public bool PlayerReadyForAttack => _owner?.PlayerReadyForAttack == true;
|
public bool PlayerReadyForAttack => _owner?.PlayerReadyForAttack == true;
|
||||||
public bool AutoRepeatAttack => _owner?.AutoRepeatAttack == true;
|
public bool AutoRepeatAttack => _owner?.AutoRepeatAttack == true;
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private CombatAttackOperationsSlot? _slot;
|
||||||
|
private readonly ICombatAttackOperations _expected;
|
||||||
|
|
||||||
|
public Binding(
|
||||||
|
CombatAttackOperationsSlot slot,
|
||||||
|
ICombatAttackOperations expected)
|
||||||
|
{
|
||||||
|
_slot = slot;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _slot, null)?.Unbind(_expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
749
src/AcDream.App/Composition/SessionPlayerComposition.cs
Normal file
749
src/AcDream.App/Composition/SessionPlayerComposition.cs
Normal file
|
|
@ -0,0 +1,749 @@
|
||||||
|
using AcDream.App.Combat;
|
||||||
|
using AcDream.App.Input;
|
||||||
|
using AcDream.App.Interaction;
|
||||||
|
using AcDream.App.Net;
|
||||||
|
using AcDream.App.Physics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Settings;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
|
using AcDream.App.Update;
|
||||||
|
using AcDream.App.World;
|
||||||
|
using AcDream.Core.Combat;
|
||||||
|
using AcDream.Core.Items;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Core.Physics.Motion;
|
||||||
|
using AcDream.Core.Plugins;
|
||||||
|
using AcDream.Core.Rendering;
|
||||||
|
using AcDream.Core.Selection;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
using Silk.NET.Windowing;
|
||||||
|
|
||||||
|
namespace AcDream.App.Composition;
|
||||||
|
|
||||||
|
internal sealed record SessionPlayerDependencies(
|
||||||
|
RuntimeOptions Options,
|
||||||
|
IWindow Window,
|
||||||
|
object DatLock,
|
||||||
|
RuntimeSettingsController Settings,
|
||||||
|
SettingsDevToolsResult SettingsDevTools,
|
||||||
|
PhysicsEngine PhysicsEngine,
|
||||||
|
PhysicsDataCache PhysicsDataCache,
|
||||||
|
WorldGameState WorldGameState,
|
||||||
|
WorldEvents WorldEvents,
|
||||||
|
SelectionState Selection,
|
||||||
|
ClientObjectTable Objects,
|
||||||
|
EntityClassificationCache ClassificationCache,
|
||||||
|
LiveEntityRuntimeSlot RuntimeSlot,
|
||||||
|
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> AnimatedEntities,
|
||||||
|
RemoteMovementObservationTracker RemoteMovementObservations,
|
||||||
|
RemotePhysicsUpdater RemotePhysicsUpdater,
|
||||||
|
RemoteInboundMotionDispatcher RemoteInboundMotion,
|
||||||
|
RetailInboundEventDispatcher InboundEntityEvents,
|
||||||
|
DeferredLiveEntityMotionRuntimeBindings MotionBindings,
|
||||||
|
LocalPlayerIdentityState PlayerIdentity,
|
||||||
|
LocalPlayerControllerSlot PlayerController,
|
||||||
|
LocalPlayerPhysicsHostSlot PlayerHost,
|
||||||
|
LocalPlayerModeState PlayerMode,
|
||||||
|
ChaseCameraInputState ChaseCameraInput,
|
||||||
|
LocalPlayerOutboundController PlayerOutbound,
|
||||||
|
DeferredLocalPlayerTeleportNetworkSink TeleportSink,
|
||||||
|
LiveWorldOriginState WorldOrigin,
|
||||||
|
WorldRenderRangeState RenderRange,
|
||||||
|
LocalPlayerShadowState PlayerShadow,
|
||||||
|
LocalPlayerSkillState PlayerSkills,
|
||||||
|
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,
|
||||||
|
CombatState Combat,
|
||||||
|
CombatFeedbackSlot CombatFeedback,
|
||||||
|
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
|
||||||
|
Action<string> Log);
|
||||||
|
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
ResultPublished,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class SessionPlayerCompositionPhase
|
||||||
|
: ISessionPlayerCompositionPhase<
|
||||||
|
HostInputCameraResult,
|
||||||
|
ContentEffectsAudioResult,
|
||||||
|
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,
|
||||||
|
WorldRenderResult world,
|
||||||
|
InteractionRetainedUiResult interaction,
|
||||||
|
LivePresentationResult live)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(host);
|
||||||
|
ArgumentNullException.ThrowIfNull(content);
|
||||||
|
ArgumentNullException.ThrowIfNull(world);
|
||||||
|
ArgumentNullException.ThrowIfNull(interaction);
|
||||||
|
ArgumentNullException.ThrowIfNull(live);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
var landblockBuildFactory = new LandblockBuildFactory(
|
||||||
|
content.Dats,
|
||||||
|
d.DatLock,
|
||||||
|
world.TerrainBuild.HeightTable,
|
||||||
|
d.PhysicsDataCache,
|
||||||
|
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,
|
||||||
|
drainCompletions: streamerLease.Resource.DrainCompletions,
|
||||||
|
state: live.WorldState,
|
||||||
|
nearRadius: nearRadius,
|
||||||
|
farRadius: farRadius,
|
||||||
|
presentationPipeline: live.LandblockPipeline,
|
||||||
|
clearPendingLoads: streamerLease.Resource.ClearPendingLoads);
|
||||||
|
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 worldReveal = new WorldRevealCoordinator(
|
||||||
|
streaming.IsRenderNeighborhoodResident,
|
||||||
|
d.PhysicsEngine.IsSpawnCellReady,
|
||||||
|
d.PhysicsEngine.IsNeighborhoodTerrainResident,
|
||||||
|
() => live.DrawDispatcher.CompositeTexturesReady,
|
||||||
|
(destinationCell, radius) => live.DrawDispatcher.PrepareCompositeTextures(
|
||||||
|
live.WorldState.Entities,
|
||||||
|
live.WorldState.FlatViewGeneration,
|
||||||
|
destinationCell,
|
||||||
|
radius),
|
||||||
|
live.DrawDispatcher.InvalidateCompositeWarmupReadiness,
|
||||||
|
spawnClaimClassifier.IsUnhydratable,
|
||||||
|
d.Log);
|
||||||
|
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,
|
||||||
|
d.PhysicsDataCache,
|
||||||
|
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);
|
||||||
|
|
||||||
|
var liveSessionLease = scope.Acquire(
|
||||||
|
"live-session controller",
|
||||||
|
static () => new LiveSessionController(),
|
||||||
|
static value => value.Dispose());
|
||||||
|
LiveSessionController liveSession = liveSessionLease.Resource;
|
||||||
|
bindings.Adopt(
|
||||||
|
"retained-UI live session",
|
||||||
|
interaction.LateBindings.Session.Bind(liveSession));
|
||||||
|
var localPhysicsTimestamps =
|
||||||
|
new LiveSessionLocalPhysicsTimestampPublisher(
|
||||||
|
d.PlayerIdentity,
|
||||||
|
liveSession);
|
||||||
|
Fault(SessionPlayerCompositionPoint.LiveSessionCreated);
|
||||||
|
|
||||||
|
var teardown = new LiveEntityRuntimeTeardownController(
|
||||||
|
live.LiveEntities,
|
||||||
|
live.Presentation,
|
||||||
|
live.EntityEffects,
|
||||||
|
live.RemoteTeleport,
|
||||||
|
live.SelectionInteractions,
|
||||||
|
d.Selection,
|
||||||
|
d.AnimatedEntities,
|
||||||
|
d.RemoteMovementObservations,
|
||||||
|
d.TranslucencyFades,
|
||||||
|
live.ProjectionWithdrawal,
|
||||||
|
live.EquippedChildren,
|
||||||
|
d.PhysicsEngine.ShadowObjects,
|
||||||
|
live.Lights,
|
||||||
|
d.ClassificationCache,
|
||||||
|
d.PlayerIdentity);
|
||||||
|
var deletion = new LiveEntityDeletionController(
|
||||||
|
live.LiveEntities,
|
||||||
|
d.Objects,
|
||||||
|
teardown,
|
||||||
|
d.PlayerIdentity,
|
||||||
|
d.Options.DumpLiveSpawns ? d.Log : null);
|
||||||
|
var hydration = new LiveEntityHydrationController(
|
||||||
|
live.LiveEntities,
|
||||||
|
d.Objects,
|
||||||
|
d.DatLock,
|
||||||
|
projectionMaterializer,
|
||||||
|
new LiveEntityRelationshipProjection(live.EquippedChildren),
|
||||||
|
new LiveEntityReadyPublisher(
|
||||||
|
live.LiveEntities,
|
||||||
|
live.EntityEffects,
|
||||||
|
live.Presentation),
|
||||||
|
originCoordinator,
|
||||||
|
networkUpdateBridge,
|
||||||
|
localPhysicsTimestamps,
|
||||||
|
d.PlayerIdentity,
|
||||||
|
deletion,
|
||||||
|
d.Options.DumpLiveSpawns ? d.Log : null);
|
||||||
|
bindings.Adopt(
|
||||||
|
"landblock-loaded hydration",
|
||||||
|
live.LandblockLoaded.Bind(hydration));
|
||||||
|
Fault(SessionPlayerCompositionPoint.HydrationCreated);
|
||||||
|
|
||||||
|
var networkUpdates = new LiveEntityNetworkUpdateController(
|
||||||
|
live.LiveEntities,
|
||||||
|
d.Objects,
|
||||||
|
hydration,
|
||||||
|
live.EntityEffects,
|
||||||
|
live.Presentation,
|
||||||
|
live.Lights,
|
||||||
|
live.EquippedChildren,
|
||||||
|
live.ProjectileController,
|
||||||
|
live.RemoteTeleport,
|
||||||
|
d.AnimatedEntities,
|
||||||
|
new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(d.RuntimeSlot),
|
||||||
|
d.RemoteMovementObservations,
|
||||||
|
d.RemotePhysicsUpdater,
|
||||||
|
d.RemoteInboundMotion,
|
||||||
|
live.MotionRuntime,
|
||||||
|
d.PhysicsEngine,
|
||||||
|
content.Dats,
|
||||||
|
content.AnimationLoader,
|
||||||
|
interaction.CombatTarget,
|
||||||
|
d.WorldOrigin,
|
||||||
|
d.TeleportSink,
|
||||||
|
d.PlayerController,
|
||||||
|
d.PlayerOutbound,
|
||||||
|
d.PlayerHost,
|
||||||
|
d.PlayerIdentity,
|
||||||
|
d.UpdateClock,
|
||||||
|
liveSession,
|
||||||
|
localPhysicsTimestamps.Publish,
|
||||||
|
d.MovementDiagnostics);
|
||||||
|
var liveness = new LiveEntityLivenessController(
|
||||||
|
live.LiveEntities,
|
||||||
|
d.PlayerIdentity,
|
||||||
|
deletion);
|
||||||
|
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.Combat,
|
||||||
|
new CombatAttackTargetSource(
|
||||||
|
d.Selection,
|
||||||
|
live.LiveEntities,
|
||||||
|
d.Objects,
|
||||||
|
d.PlayerIdentity),
|
||||||
|
d.Settings,
|
||||||
|
d.PlayerController,
|
||||||
|
d.PlayerOutbound,
|
||||||
|
liveSession,
|
||||||
|
liveSession,
|
||||||
|
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,
|
||||||
|
liveSession,
|
||||||
|
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,
|
||||||
|
liveSession,
|
||||||
|
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);
|
||||||
|
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,
|
||||||
|
liveSession);
|
||||||
|
var localPlayerFrame = new RetailLocalPlayerFrameController(
|
||||||
|
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);
|
||||||
|
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,
|
||||||
|
d.PhysicsDataCache,
|
||||||
|
d.AnimatedEntities,
|
||||||
|
localPlayerAnimation,
|
||||||
|
localPlayerShadow,
|
||||||
|
d.PlayerApproachCompletions,
|
||||||
|
gameplayInput,
|
||||||
|
liveSession,
|
||||||
|
d.MovementDiagnostics,
|
||||||
|
d.PlayerSkills,
|
||||||
|
d.ViewportAspect);
|
||||||
|
if (d.SettingsDevTools.DevTools is { } devTools)
|
||||||
|
{
|
||||||
|
bindings.Adopt(
|
||||||
|
"developer player mode",
|
||||||
|
devTools.LateBindings.PlayerModeCommands.BindOwned(playerMode));
|
||||||
|
bindings.Adopt(
|
||||||
|
"developer command bus",
|
||||||
|
devTools.CommandBus.BindOwned(liveSession));
|
||||||
|
}
|
||||||
|
var playerModeAutoEntry = new PlayerModeAutoEntry(
|
||||||
|
new LivePlayerModeAutoEntryContext(
|
||||||
|
liveSession,
|
||||||
|
live.LiveEntities,
|
||||||
|
d.PlayerIdentity,
|
||||||
|
worldReveal,
|
||||||
|
d.PlayerMode,
|
||||||
|
playerMode));
|
||||||
|
playerMode.BindAutoEntry(playerModeAutoEntry);
|
||||||
|
Fault(SessionPlayerCompositionPoint.PlayerModeBound);
|
||||||
|
|
||||||
|
LocalPlayerTeleportController localTeleport =
|
||||||
|
d.PortalTunnelFallback.Transfer(
|
||||||
|
portalTunnel => new LocalPlayerTeleportController(
|
||||||
|
new LiveLocalPlayerTeleportAuthority(
|
||||||
|
live.LiveEntities,
|
||||||
|
d.PlayerIdentity),
|
||||||
|
gameplayInput,
|
||||||
|
playerMode,
|
||||||
|
new LocalPlayerTeleportStreamingOperations(
|
||||||
|
d.WorldOrigin,
|
||||||
|
streamingOriginRecenter,
|
||||||
|
streaming,
|
||||||
|
sealedDungeonCells),
|
||||||
|
worldReveal,
|
||||||
|
new LocalPlayerTeleportPlacement(
|
||||||
|
d.PhysicsEngine,
|
||||||
|
live.LiveEntities,
|
||||||
|
d.PlayerIdentity,
|
||||||
|
d.PlayerController,
|
||||||
|
d.PlayerHost,
|
||||||
|
d.ChaseCameraInput,
|
||||||
|
d.WorldOrigin,
|
||||||
|
liveSpatialReconciler),
|
||||||
|
new LocalPlayerTeleportSession(liveSession),
|
||||||
|
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);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
live.RuntimeBindings.Adopt(
|
||||||
|
"live runtime component teardown",
|
||||||
|
componentLifecycleBinding);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
componentLifecycleBinding.Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
bindings);
|
||||||
|
_publication.PublishSessionPlayer(result);
|
||||||
|
|
||||||
|
streamerLease.Transfer();
|
||||||
|
liveSessionLease.Transfer();
|
||||||
|
teleportLease.Transfer();
|
||||||
|
bindingsLease.Transfer();
|
||||||
|
Fault(SessionPlayerCompositionPoint.ResultPublished);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Fault(SessionPlayerCompositionPoint point) =>
|
||||||
|
_faultInjection?.Invoke(point);
|
||||||
|
}
|
||||||
95
src/AcDream.App/Composition/SessionPlayerRuntimeBindings.cs
Normal file
95
src/AcDream.App/Composition/SessionPlayerRuntimeBindings.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Composition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Named Phase-7 external edges. Releases run in reverse order; completed
|
||||||
|
/// edges are removed immediately so an incomplete cleanup retries only the
|
||||||
|
/// remaining work.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class SessionPlayerRuntimeBindings : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<(string Name, IDisposable Binding)> _bindings = [];
|
||||||
|
private bool _deactivationStarted;
|
||||||
|
|
||||||
|
public void Adopt(string name, IDisposable binding)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||||
|
ArgumentNullException.ThrowIfNull(binding);
|
||||||
|
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
|
||||||
|
_bindings.Add((name, binding));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BindEntityReady(
|
||||||
|
EquippedChildRenderController source,
|
||||||
|
Action<LiveEntityReadyCandidate> handler)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
ArgumentNullException.ThrowIfNull(handler);
|
||||||
|
source.EntityReady += handler;
|
||||||
|
Adopt(
|
||||||
|
"equipped-child entity ready",
|
||||||
|
new DelegateBinding(() => source.EntityReady -= handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BindAppearanceApplied(
|
||||||
|
LiveEntityHydrationController source,
|
||||||
|
Action<uint> handler)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
ArgumentNullException.ThrowIfNull(handler);
|
||||||
|
source.AppearanceApplied += handler;
|
||||||
|
Adopt(
|
||||||
|
"live appearance applied",
|
||||||
|
new DelegateBinding(() => source.AppearanceApplied -= handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_deactivationStarted && _bindings.Count == 0)
|
||||||
|
return;
|
||||||
|
_deactivationStarted = true;
|
||||||
|
|
||||||
|
List<Exception>? failures = null;
|
||||||
|
for (int i = _bindings.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
(string name, IDisposable binding) = _bindings[i];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
binding.Dispose();
|
||||||
|
_bindings.RemoveAt(i);
|
||||||
|
}
|
||||||
|
catch (Exception failure)
|
||||||
|
{
|
||||||
|
(failures ??= []).Add(new InvalidOperationException(
|
||||||
|
$"Session/player binding '{name}' did not detach.",
|
||||||
|
failure));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures is not null)
|
||||||
|
{
|
||||||
|
throw new AggregateException(
|
||||||
|
"Session/player binding cleanup remains incomplete.",
|
||||||
|
failures);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DelegateBinding : IDisposable
|
||||||
|
{
|
||||||
|
private Action? _release;
|
||||||
|
|
||||||
|
public DelegateBinding(Action release) =>
|
||||||
|
_release = release ?? throw new ArgumentNullException(nameof(release));
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Action? release = _release;
|
||||||
|
if (release is null)
|
||||||
|
return;
|
||||||
|
release();
|
||||||
|
_release = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -245,6 +245,20 @@ internal sealed class CameraPointerInputController
|
||||||
_gameplayFrame = gameplayFrame;
|
_gameplayFrame = gameplayFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindGameplayFrameOwned(
|
||||||
|
GameplayInputFrameController gameplayFrame)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(gameplayFrame);
|
||||||
|
if (_gameplayFrame is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The raw pointer owner already has a gameplay frame.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_gameplayFrame = gameplayFrame;
|
||||||
|
return new GameplayFrameBinding(this, gameplayFrame);
|
||||||
|
}
|
||||||
|
|
||||||
public void UnbindGameplayFrame(GameplayInputFrameController gameplayFrame)
|
public void UnbindGameplayFrame(GameplayInputFrameController gameplayFrame)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(gameplayFrame);
|
ArgumentNullException.ThrowIfNull(gameplayFrame);
|
||||||
|
|
@ -252,6 +266,24 @@ internal sealed class CameraPointerInputController
|
||||||
_gameplayFrame = null;
|
_gameplayFrame = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class GameplayFrameBinding : IDisposable
|
||||||
|
{
|
||||||
|
private CameraPointerInputController? _owner;
|
||||||
|
private readonly GameplayInputFrameController _expected;
|
||||||
|
|
||||||
|
public GameplayFrameBinding(
|
||||||
|
CameraPointerInputController owner,
|
||||||
|
GameplayInputFrameController expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?
|
||||||
|
.UnbindGameplayFrame(_expected);
|
||||||
|
}
|
||||||
|
|
||||||
public void HandleFocusChanged(bool focused)
|
public void HandleFocusChanged(bool focused)
|
||||||
{
|
{
|
||||||
if (!focused)
|
if (!focused)
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,20 @@ internal sealed class DevToolsCommandBusSource : IDevToolsCommandBusSource
|
||||||
_session = session;
|
_session = session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindOwned(LiveSessionController session)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(session);
|
||||||
|
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||||
|
if (_session is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Developer command-bus authority is already bound.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_session = session;
|
||||||
|
return new Binding(this, session);
|
||||||
|
}
|
||||||
|
|
||||||
public void Unbind(LiveSessionController session)
|
public void Unbind(LiveSessionController session)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(session);
|
ArgumentNullException.ThrowIfNull(session);
|
||||||
|
|
@ -211,6 +225,23 @@ internal sealed class DevToolsCommandBusSource : IDevToolsCommandBusSource
|
||||||
_deactivated = true;
|
_deactivated = true;
|
||||||
_session = null;
|
_session = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private DevToolsCommandBusSource? _owner;
|
||||||
|
private readonly LiveSessionController _expected;
|
||||||
|
|
||||||
|
public Binding(
|
||||||
|
DevToolsCommandBusSource owner,
|
||||||
|
LiveSessionController expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Typed panel operations used by menu and input presentation.</summary>
|
/// <summary>Typed panel operations used by menu and input presentation.</summary>
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,20 @@ internal sealed class DeferredDevToolsPlayerModeCommands
|
||||||
_target = target;
|
_target = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindOwned(IDevToolsPlayerModeTarget target)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
|
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||||
|
if (_target is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Developer player-mode commands are already bound.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_target = target;
|
||||||
|
return new Binding(this, target);
|
||||||
|
}
|
||||||
|
|
||||||
public void Unbind(IDevToolsPlayerModeTarget target)
|
public void Unbind(IDevToolsPlayerModeTarget target)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(target);
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
|
|
@ -185,6 +199,23 @@ internal sealed class DeferredDevToolsPlayerModeCommands
|
||||||
if (!_deactivated)
|
if (!_deactivated)
|
||||||
_target?.ToggleFlyOrChase();
|
_target?.ToggleFlyOrChase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private DeferredDevToolsPlayerModeCommands? _owner;
|
||||||
|
private readonly IDevToolsPlayerModeTarget _expected;
|
||||||
|
|
||||||
|
public Binding(
|
||||||
|
DeferredDevToolsPlayerModeCommands owner,
|
||||||
|
IDevToolsPlayerModeTarget expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal interface IDevToolsRuntimeFacts
|
internal interface IDevToolsRuntimeFacts
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ public sealed class GameWindow :
|
||||||
IGameWindowSettingsDevToolsPublication,
|
IGameWindowSettingsDevToolsPublication,
|
||||||
IGameWindowWorldRenderPublication,
|
IGameWindowWorldRenderPublication,
|
||||||
IGameWindowInteractionRetainedUiPublication,
|
IGameWindowInteractionRetainedUiPublication,
|
||||||
IGameWindowLivePresentationPublication
|
IGameWindowLivePresentationPublication,
|
||||||
|
IGameWindowSessionPlayerPublication
|
||||||
{
|
{
|
||||||
private static double ClientTimerNow() =>
|
private static double ClientTimerNow() =>
|
||||||
System.Diagnostics.Stopwatch.GetTimestamp()
|
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||||
|
|
@ -99,6 +100,7 @@ public sealed class GameWindow :
|
||||||
private AcDream.App.Rendering.RenderFrameDiagnosticsController?
|
private AcDream.App.Rendering.RenderFrameDiagnosticsController?
|
||||||
_renderFrameDiagnostics;
|
_renderFrameDiagnostics;
|
||||||
private LivePresentationRuntimeBindings? _livePresentationBindings;
|
private LivePresentationRuntimeBindings? _livePresentationBindings;
|
||||||
|
private SessionPlayerRuntimeBindings? _sessionPlayerBindings;
|
||||||
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
|
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
|
||||||
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
|
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
|
||||||
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
||||||
|
|
@ -123,22 +125,14 @@ public sealed class GameWindow :
|
||||||
private AcDream.App.Streaming.StreamingOriginRecenterCoordinator?
|
private AcDream.App.Streaming.StreamingOriginRecenterCoordinator?
|
||||||
_streamingOriginRecenter;
|
_streamingOriginRecenter;
|
||||||
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
|
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
|
||||||
|
private AcDream.App.Streaming.DatSpawnClaimHydrationClassifier?
|
||||||
|
_spawnClaimHydration;
|
||||||
private readonly AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink
|
private readonly AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink
|
||||||
_localPlayerTeleportSink = new();
|
_localPlayerTeleportSink = new();
|
||||||
private AcDream.App.Streaming.LocalPlayerTeleportController?
|
private AcDream.App.Streaming.LocalPlayerTeleportController?
|
||||||
_localPlayerTeleport;
|
_localPlayerTeleport;
|
||||||
private readonly AcDream.App.Rendering.WorldRenderRangeState _renderRange =
|
private readonly AcDream.App.Rendering.WorldRenderRangeState _renderRange =
|
||||||
new(nearRadius: 4, farRadius: 12);
|
new(nearRadius: 4, farRadius: 12);
|
||||||
private int _nearRadius
|
|
||||||
{
|
|
||||||
get => _renderRange.NearRadius;
|
|
||||||
set => _renderRange.NearRadius = value;
|
|
||||||
}
|
|
||||||
private int _farRadius
|
|
||||||
{
|
|
||||||
get => _renderRange.FarRadius;
|
|
||||||
set => _renderRange.FarRadius = value;
|
|
||||||
}
|
|
||||||
// Phase B.3: physics engine — populated from the streaming pipeline.
|
// Phase B.3: physics engine — populated from the streaming pipeline.
|
||||||
private readonly AcDream.Core.Physics.PhysicsEngine _physicsEngine = new();
|
private readonly AcDream.Core.Physics.PhysicsEngine _physicsEngine = new();
|
||||||
|
|
||||||
|
|
@ -170,7 +164,7 @@ public sealed class GameWindow :
|
||||||
_liveEntityProjectionWithdrawal;
|
_liveEntityProjectionWithdrawal;
|
||||||
private AcDream.App.World.LiveEntityHydrationController? _liveEntityHydration;
|
private AcDream.App.World.LiveEntityHydrationController? _liveEntityHydration;
|
||||||
private AcDream.App.Physics.LiveEntityNetworkUpdateController? _liveEntityNetworkUpdates;
|
private AcDream.App.Physics.LiveEntityNetworkUpdateController? _liveEntityNetworkUpdates;
|
||||||
private AcDream.App.Net.LiveEntitySessionController _liveEntitySessionEvents = null!;
|
private AcDream.App.Net.LiveEntitySessionController? _liveEntitySessionEvents;
|
||||||
private readonly AcDream.App.Physics.DeferredLiveEntityMotionRuntimeBindings
|
private readonly AcDream.App.Physics.DeferredLiveEntityMotionRuntimeBindings
|
||||||
_liveEntityMotionBindings = new();
|
_liveEntityMotionBindings = new();
|
||||||
|
|
||||||
|
|
@ -996,6 +990,51 @@ public sealed class GameWindow :
|
||||||
_livePresentationBindings = result.RuntimeBindings;
|
_livePresentationBindings = result.RuntimeBindings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IGameWindowSessionPlayerPublication.PublishSessionPlayer(
|
||||||
|
SessionPlayerResult result)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(result);
|
||||||
|
if (_streamer is not null
|
||||||
|
|| _streamingController is not null
|
||||||
|
|| _streamingOriginRecenter is not null
|
||||||
|
|| _worldReveal is not null
|
||||||
|
|| _spawnClaimHydration is not null
|
||||||
|
|| _liveSessionController is not null
|
||||||
|
|| _liveEntityHydration is not null
|
||||||
|
|| _liveEntityNetworkUpdates is not null
|
||||||
|
|| _liveEntityLiveness is not null
|
||||||
|
|| _liveEntitySessionEvents is not null
|
||||||
|
|| _gameplayInputFrame is not null
|
||||||
|
|| _localPlayerAnimation is not null
|
||||||
|
|| _localPlayerShadowSynchronizer is not null
|
||||||
|
|| _playerModeController is not null
|
||||||
|
|| _playerModeAutoEntry is not null
|
||||||
|
|| _localPlayerTeleport is not null
|
||||||
|
|| _sessionPlayerBindings is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The GameWindow composition shell already owns session/player state.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_streamer = result.Streamer;
|
||||||
|
_streamingController = result.Streaming;
|
||||||
|
_streamingOriginRecenter = result.StreamingOriginRecenter;
|
||||||
|
_worldReveal = result.WorldReveal;
|
||||||
|
_spawnClaimHydration = result.SpawnClaimHydration;
|
||||||
|
_liveSessionController = result.LiveSession;
|
||||||
|
_liveEntityHydration = result.Hydration;
|
||||||
|
_liveEntityNetworkUpdates = result.NetworkUpdates;
|
||||||
|
_liveEntityLiveness = result.Liveness;
|
||||||
|
_liveEntitySessionEvents = result.SessionEvents;
|
||||||
|
_gameplayInputFrame = result.GameplayInput;
|
||||||
|
_localPlayerAnimation = result.LocalPlayerAnimation;
|
||||||
|
_localPlayerShadowSynchronizer = result.LocalPlayerShadow;
|
||||||
|
_playerModeController = result.PlayerMode;
|
||||||
|
_playerModeAutoEntry = result.PlayerModeAutoEntry;
|
||||||
|
_localPlayerTeleport = result.LocalTeleport;
|
||||||
|
_sessionPlayerBindings = result.RuntimeBindings;
|
||||||
|
}
|
||||||
|
|
||||||
private static void PublishCompositionOwner<T>(
|
private static void PublishCompositionOwner<T>(
|
||||||
ref T? destination,
|
ref T? destination,
|
||||||
T value,
|
T value,
|
||||||
|
|
@ -1139,9 +1178,6 @@ public sealed class GameWindow :
|
||||||
Console.WriteLine),
|
Console.WriteLine),
|
||||||
this).Compose(platform, contentEffectsAudio, settingsDevTools);
|
this).Compose(platform, contentEffectsAudio, settingsDevTools);
|
||||||
string shadersDir = worldRender.Foundation.ShadersDirectory;
|
string shadersDir = worldRender.Foundation.ShadersDirectory;
|
||||||
TerrainAtlas terrainAtlas = worldRender.Foundation.TerrainAtlas;
|
|
||||||
int centerX = worldRender.TerrainBuild.InitialCenterX;
|
|
||||||
int centerY = worldRender.TerrainBuild.InitialCenterY;
|
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"loading world view centered on " +
|
$"loading world view centered on " +
|
||||||
$"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}");
|
$"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}");
|
||||||
|
|
@ -1243,441 +1279,64 @@ public sealed class GameWindow :
|
||||||
worldRender,
|
worldRender,
|
||||||
interactionUi);
|
interactionUi);
|
||||||
|
|
||||||
// Apply radii from the same immutable quality snapshot used for the
|
SessionPlayerResult sessionPlayer =
|
||||||
// window's MSAA, terrain anisotropy, and dispatcher A2C setup.
|
new SessionPlayerCompositionPhase(
|
||||||
// Legacy ACDREAM_STREAM_RADIUS is still honoured for backward-compat.
|
new SessionPlayerDependencies(
|
||||||
_nearRadius = _runtimeSettings.ResolvedQuality.NearRadius;
|
_options,
|
||||||
_farRadius = _runtimeSettings.ResolvedQuality.FarRadius;
|
_window!,
|
||||||
|
_datLock,
|
||||||
// Legacy override: ACDREAM_STREAM_RADIUS acts as nearRadius and
|
_runtimeSettings,
|
||||||
// ensures farRadius >= streamRadius. Parsed once into
|
settingsDevTools,
|
||||||
// RuntimeOptions.LegacyStreamRadius (null when unset or invalid).
|
|
||||||
if (_options.LegacyStreamRadius is { } sr)
|
|
||||||
{
|
|
||||||
_nearRadius = sr;
|
|
||||||
_farRadius = System.Math.Max(sr, _farRadius);
|
|
||||||
}
|
|
||||||
Console.WriteLine(
|
|
||||||
$"streaming: nearRadius={_nearRadius} (window={2 * _nearRadius + 1}x{2 * _nearRadius + 1})" +
|
|
||||||
$" farRadius={_farRadius} (window={2 * _farRadius + 1}x{2 * _farRadius + 1})");
|
|
||||||
|
|
||||||
// Phase A.5 T11+: the streamer now runs on a dedicated worker thread.
|
|
||||||
// loadLandblock acquires _datLock (T10) before touching DatCollection.
|
|
||||||
// buildMeshOrNull (T12) receives the already-loaded LoadedLandblock so
|
|
||||||
// it can call LandblockMesh.Build without a dat read — _heightTable and
|
|
||||||
// _blendCtx are read-only after init, _surfaceCache is ConcurrentDictionary (T9).
|
|
||||||
var landblockBuildFactory = new AcDream.App.Streaming.LandblockBuildFactory(
|
|
||||||
_dats!,
|
|
||||||
_datLock,
|
|
||||||
_heightTable!,
|
|
||||||
_physicsDataCache,
|
|
||||||
_options.DumpSceneryZ);
|
|
||||||
_streamer = AcDream.App.Streaming.LandblockStreamer.CreateForRequests(
|
|
||||||
loadLandblock: landblockBuildFactory.Build,
|
|
||||||
buildMeshOrNull: (id, lb) =>
|
|
||||||
{
|
|
||||||
if (lb is null || _heightTable is null || _blendCtx is null || _surfaceCache is null)
|
|
||||||
return null;
|
|
||||||
uint lbX = (id >> 24) & 0xFFu;
|
|
||||||
uint lbY = (id >> 16) & 0xFFu;
|
|
||||||
// _surfaceCache is ConcurrentDictionary (T9) — safe from worker thread.
|
|
||||||
// _heightTable and _blendCtx are read-only after initialization.
|
|
||||||
// lb.Heightmap is the pre-loaded LandBlock; no dat read needed here.
|
|
||||||
return AcDream.Core.Terrain.LandblockMesh.Build(
|
|
||||||
lb.Heightmap, lbX, lbY, _heightTable, _blendCtx, _surfaceCache);
|
|
||||||
});
|
|
||||||
_streamer.Start();
|
|
||||||
|
|
||||||
_streamingController = new AcDream.App.Streaming.StreamingController(
|
|
||||||
enqueueLoad: (id, kind, generation) => _streamer.EnqueueLoad(
|
|
||||||
new AcDream.App.Streaming.LandblockBuildRequest(
|
|
||||||
id,
|
|
||||||
kind,
|
|
||||||
generation,
|
|
||||||
new AcDream.App.Streaming.LandblockBuildOrigin(
|
|
||||||
_liveCenterX,
|
|
||||||
_liveCenterY))),
|
|
||||||
enqueueUnload: (id, generation) => _streamer.EnqueueUnload(id, generation),
|
|
||||||
drainCompletions: _streamer.DrainCompletions,
|
|
||||||
state: _worldState,
|
|
||||||
nearRadius: _nearRadius,
|
|
||||||
farRadius: _farRadius,
|
|
||||||
clearPendingLoads: _streamer.ClearPendingLoads,
|
|
||||||
// Retained-object recovery runs after each
|
|
||||||
// (dungeon-exit expand or Far→Near promote). ACE won't re-send the
|
|
||||||
// objects it still considers known.
|
|
||||||
presentationPipeline: _landblockPresentationPipeline!);
|
|
||||||
_streamingOriginRecenter =
|
|
||||||
new AcDream.App.Streaming.StreamingOriginRecenterCoordinator(
|
|
||||||
_streamingController,
|
|
||||||
_liveWorldOrigin);
|
|
||||||
// A.5 T22.5: apply max-completions from resolved quality.
|
|
||||||
_streamingController.MaxCompletionsPerFrame =
|
|
||||||
_runtimeSettings.ResolvedQuality.MaxCompletionsPerFrame;
|
|
||||||
_runtimeSettings.BindRuntimeTargets(
|
|
||||||
new RuntimeSettingsTargets(
|
|
||||||
new SilkRuntimeDisplayWindowTarget(_window!),
|
|
||||||
_wbDrawDispatcher!,
|
|
||||||
terrainAtlas,
|
|
||||||
_streamingController,
|
|
||||||
_renderRange,
|
|
||||||
_uiHost?.Root,
|
|
||||||
Console.WriteLine));
|
|
||||||
_worldReveal = new AcDream.App.Streaming.WorldRevealCoordinator(
|
|
||||||
isRenderNeighborhoodReady: _streamingController.IsRenderNeighborhoodResident,
|
|
||||||
isSpawnCellReady: _physicsEngine.IsSpawnCellReady,
|
|
||||||
isTerrainNeighborhoodReady: _physicsEngine.IsNeighborhoodTerrainResident,
|
|
||||||
areCompositeTexturesReady: () => _wbDrawDispatcher?.CompositeTexturesReady == true,
|
|
||||||
prepareCompositeTextures: (destinationCell, radius) =>
|
|
||||||
_wbDrawDispatcher?.PrepareCompositeTextures(
|
|
||||||
_worldState.Entities,
|
|
||||||
_worldState.FlatViewGeneration,
|
|
||||||
destinationCell,
|
|
||||||
radius),
|
|
||||||
invalidateCompositeTextures: () =>
|
|
||||||
_wbDrawDispatcher?.InvalidateCompositeWarmupReadiness(),
|
|
||||||
isSpawnClaimUnhydratable: IsSpawnClaimUnhydratable,
|
|
||||||
log: message => Console.WriteLine(message));
|
|
||||||
|
|
||||||
var networkUpdateBridge =
|
|
||||||
new AcDream.App.World.DeferredLiveEntityNetworkUpdateSink();
|
|
||||||
var sealedDungeonCells =
|
|
||||||
new AcDream.App.Streaming.DatSealedDungeonCellClassifier(
|
|
||||||
_dats!,
|
|
||||||
_datLock);
|
|
||||||
var projectionMaterializer = new DatLiveEntityProjectionMaterializer(
|
|
||||||
_options,
|
|
||||||
_dats!,
|
|
||||||
_liveEntities!,
|
|
||||||
_physicsDataCache,
|
|
||||||
_animLoader!,
|
|
||||||
_wbEntitySpawnAdapter!,
|
|
||||||
_textureCache!,
|
|
||||||
_classificationCache,
|
|
||||||
_effectPoses,
|
|
||||||
_equippedChildRenderer!,
|
|
||||||
_worldGameState,
|
|
||||||
_worldEvents,
|
|
||||||
_physicsEngine.ShadowObjects,
|
|
||||||
_liveEntityCollisionBuilder!,
|
|
||||||
_projectileController!,
|
|
||||||
_animationPresenter,
|
|
||||||
_staticAnimationScheduler!,
|
|
||||||
_liveWorldOrigin,
|
|
||||||
_updateFrameClock);
|
|
||||||
var originCoordinator =
|
|
||||||
new AcDream.App.World.LiveEntityWorldOriginCoordinator(
|
|
||||||
_liveWorldOrigin,
|
|
||||||
_streamingController,
|
|
||||||
_worldState,
|
|
||||||
_worldReveal,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
sealedDungeonCells,
|
|
||||||
Console.WriteLine);
|
|
||||||
_liveSessionController = new AcDream.App.Net.LiveSessionController();
|
|
||||||
interactionUi.LateBindings.AdoptLateOwnerBinding(
|
|
||||||
"live session",
|
|
||||||
interactionUi.LateBindings.Session.Bind(_liveSessionController));
|
|
||||||
var localPhysicsTimestamps =
|
|
||||||
new AcDream.App.World.LiveSessionLocalPhysicsTimestampPublisher(
|
|
||||||
_localPlayerIdentity,
|
|
||||||
_liveSessionController);
|
|
||||||
var liveEntityTeardown =
|
|
||||||
new AcDream.App.World.LiveEntityRuntimeTeardownController(
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
_liveEntityPresentation!,
|
|
||||||
_entityEffects!,
|
|
||||||
_remoteTeleportController!,
|
|
||||||
_selectionInteractions,
|
|
||||||
_selection,
|
|
||||||
_animatedEntities,
|
|
||||||
_remoteMovementObservations,
|
|
||||||
_translucencyFades,
|
|
||||||
_liveEntityProjectionWithdrawal!,
|
|
||||||
_equippedChildRenderer!,
|
|
||||||
_physicsEngine.ShadowObjects,
|
|
||||||
_liveEntityLights!,
|
|
||||||
_classificationCache,
|
|
||||||
_localPlayerIdentity);
|
|
||||||
livePresentation.ComponentLifecycle.Bind(liveEntityTeardown);
|
|
||||||
var liveEntityDeletion =
|
|
||||||
new AcDream.App.World.LiveEntityDeletionController(
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
Objects,
|
|
||||||
liveEntityTeardown,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
|
||||||
_liveEntityHydration = new AcDream.App.World.LiveEntityHydrationController(
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
Objects,
|
|
||||||
_datLock,
|
|
||||||
projectionMaterializer,
|
|
||||||
new AcDream.App.World.LiveEntityRelationshipProjection(
|
|
||||||
livePresentation.EquippedChildren),
|
|
||||||
new AcDream.App.World.LiveEntityReadyPublisher(
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
_entityEffects!,
|
|
||||||
_liveEntityPresentation!),
|
|
||||||
originCoordinator,
|
|
||||||
networkUpdateBridge,
|
|
||||||
localPhysicsTimestamps,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
liveEntityDeletion,
|
|
||||||
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
|
||||||
livePresentation.RuntimeBindings.Adopt(
|
|
||||||
"landblock-loaded hydration",
|
|
||||||
livePresentation.LandblockLoaded.Bind(_liveEntityHydration));
|
|
||||||
_liveEntityNetworkUpdates =
|
|
||||||
new AcDream.App.Physics.LiveEntityNetworkUpdateController(
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
Objects,
|
|
||||||
_liveEntityHydration,
|
|
||||||
_entityEffects!,
|
|
||||||
_liveEntityPresentation!,
|
|
||||||
_liveEntityLights!,
|
|
||||||
_equippedChildRenderer!,
|
|
||||||
_projectileController!,
|
|
||||||
_remoteTeleportController!,
|
|
||||||
_animatedEntities,
|
|
||||||
new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(
|
|
||||||
_liveEntityRuntimeSlot),
|
|
||||||
_remoteMovementObservations,
|
|
||||||
_remotePhysicsUpdater,
|
|
||||||
_remoteInboundMotion,
|
|
||||||
livePresentation.MotionRuntime,
|
|
||||||
_physicsEngine,
|
|
||||||
_dats!,
|
|
||||||
_animLoader!,
|
|
||||||
_combatTargetController,
|
|
||||||
_liveWorldOrigin,
|
|
||||||
_localPlayerTeleportSink,
|
|
||||||
_playerControllerSlot,
|
|
||||||
_localPlayerOutbound,
|
|
||||||
_playerHostSlot,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
_updateFrameClock,
|
|
||||||
_liveSessionController,
|
|
||||||
localPhysicsTimestamps.Publish,
|
|
||||||
_movementTruthDiagnostics);
|
|
||||||
_liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController(
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
liveEntityDeletion);
|
|
||||||
_liveEntitySessionEvents = new AcDream.App.Net.LiveEntitySessionController(
|
|
||||||
_inboundEntityEvents,
|
|
||||||
_liveEntityHydration,
|
|
||||||
_liveEntityNetworkUpdates,
|
|
||||||
_localPlayerTeleportSink,
|
|
||||||
_entityEffects!);
|
|
||||||
livePresentation.ParentAcceptance.Bind(
|
|
||||||
_liveEntityHydration.TryAcceptParentForProjection);
|
|
||||||
networkUpdateBridge.Bind(_liveEntityNetworkUpdates);
|
|
||||||
livePresentation.EquippedChildren.EntityReady += candidate =>
|
|
||||||
_liveEntityHydration.OnEntityReady(candidate);
|
|
||||||
_liveEntityHydration.AppearanceApplied += guid =>
|
|
||||||
{
|
|
||||||
if (guid == _playerServerGuid)
|
|
||||||
_paperdollFramePresenter?.MarkDirty();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Phase 4.7: optional live-mode startup. Connect to the ACE server,
|
|
||||||
// enter the world as the first character on the account, and stream
|
|
||||||
// CreateObject messages into _worldGameState as they arrive. Entirely
|
|
||||||
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
|
|
||||||
_liveWorldOrigin.SetPlaceholder(centerX, centerY);
|
|
||||||
_combatAttackOperations.Bind(
|
|
||||||
new AcDream.App.Combat.LiveCombatAttackOperations(
|
|
||||||
Combat,
|
|
||||||
new AcDream.App.Combat.CombatAttackTargetSource(
|
|
||||||
_selection,
|
|
||||||
_liveEntities!,
|
|
||||||
Objects,
|
|
||||||
_localPlayerIdentity),
|
|
||||||
_runtimeSettings,
|
|
||||||
_playerControllerSlot,
|
|
||||||
_localPlayerOutbound,
|
|
||||||
_liveSessionController,
|
|
||||||
_liveSessionController,
|
|
||||||
_combatFeedback));
|
|
||||||
AcDream.App.Input.MouseLookController? mouseLookController =
|
|
||||||
_mouseSource is not null && _mouseLookCursor is not null
|
|
||||||
? new AcDream.App.Input.MouseLookController(
|
|
||||||
_mouseSource,
|
|
||||||
_pointerPosition,
|
|
||||||
_localPlayerMode,
|
|
||||||
_playerControllerSlot,
|
|
||||||
_cameraController!,
|
|
||||||
_chaseCameraInput,
|
|
||||||
_movementInput,
|
|
||||||
_localPlayerOutbound,
|
|
||||||
_liveSessionController,
|
|
||||||
_mouseLookCursor,
|
|
||||||
new AcDream.App.Input.EnvironmentInputMonotonicClock())
|
|
||||||
: null;
|
|
||||||
_gameplayInputFrame = new AcDream.App.Input.GameplayInputFrameController(
|
|
||||||
_inputDispatcher,
|
|
||||||
_movementInput,
|
|
||||||
mouseLookController,
|
|
||||||
new AcDream.App.Input.CombatAttackInputFrameAdapter(
|
|
||||||
_combatAttackController!));
|
|
||||||
_cameraPointerInput?.BindGameplayFrame(_gameplayInputFrame);
|
|
||||||
var streamingFrame = new AcDream.App.Streaming.StreamingFrameController(
|
|
||||||
_options.LiveMode,
|
|
||||||
_localPlayerMode,
|
|
||||||
_playerControllerSlot,
|
|
||||||
_liveSessionController,
|
|
||||||
_liveWorldOrigin,
|
|
||||||
_liveEntityNetworkUpdates!,
|
|
||||||
new AcDream.App.Streaming.FlyCameraStreamingObserverSource(
|
|
||||||
_cameraController!),
|
|
||||||
new AcDream.App.Streaming.PhysicsStreamingDungeonCellSource(
|
|
||||||
_physicsEngine),
|
|
||||||
_streamingOriginRecenter!,
|
|
||||||
_streamingController!,
|
|
||||||
new AcDream.App.Streaming.LiveProjectionRescueRebucketter(
|
|
||||||
_worldState,
|
|
||||||
livePresentation.LiveEntities));
|
|
||||||
var liveEffectFrame = new AcDream.App.Update.LiveEffectFrameController(
|
|
||||||
_translucencyFades,
|
|
||||||
_animationHookFrames!,
|
|
||||||
_entityEffects!,
|
|
||||||
_particleSink!,
|
|
||||||
_liveEntityLights!,
|
|
||||||
_particleVisibility,
|
|
||||||
_particleSystem!,
|
|
||||||
_scriptRunner!,
|
|
||||||
_updateFrameClock,
|
|
||||||
new AcDream.App.Update.SettingsParticleRangeSource(
|
|
||||||
_runtimeSettings));
|
|
||||||
var liveSpatialReconciler =
|
|
||||||
new AcDream.App.Update.LiveSpatialPresentationReconciler(
|
|
||||||
_entityEffects!,
|
|
||||||
_equippedChildRenderer!,
|
|
||||||
_particleSink!,
|
|
||||||
_liveEntityLights!);
|
|
||||||
_localPlayerAnimation =
|
|
||||||
new AcDream.App.Input.LocalPlayerAnimationController(
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
_animatedEntities,
|
|
||||||
_animationPresenter,
|
|
||||||
_animationHookFrames!);
|
|
||||||
_localPlayerShadowSynchronizer =
|
|
||||||
new AcDream.App.Physics.LocalPlayerShadowSynchronizer(
|
|
||||||
_physicsEngine,
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
_liveWorldOrigin,
|
|
||||||
_localPlayerShadow);
|
|
||||||
var localPlayerProjection =
|
|
||||||
new AcDream.App.Input.LocalPlayerProjectionController(
|
|
||||||
new AcDream.App.Input.LiveLocalPlayerProjectionRuntime(
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
_liveWorldOrigin,
|
|
||||||
_localPlayerShadowSynchronizer));
|
|
||||||
var localPlayerFrameRuntime =
|
|
||||||
new AcDream.App.Input.LiveLocalPlayerFrameRuntime(
|
|
||||||
_cameraController!,
|
|
||||||
_localPlayerMode,
|
|
||||||
_playerControllerSlot,
|
|
||||||
_chaseCameraInput,
|
|
||||||
_movementInput,
|
|
||||||
_inputCapture,
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
_playerHostSlot,
|
|
||||||
localPlayerProjection,
|
|
||||||
_localPlayerOutbound,
|
|
||||||
_liveSessionController);
|
|
||||||
var localPlayerFrame =
|
|
||||||
new AcDream.App.Input.RetailLocalPlayerFrameController(
|
|
||||||
localPlayerFrameRuntime,
|
|
||||||
_movementInput);
|
|
||||||
var liveObjectFrame = new AcDream.App.Update.LiveObjectFrameController(
|
|
||||||
_inboundEntityEvents,
|
|
||||||
localPlayerFrame,
|
|
||||||
_selectionInteractions,
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
_liveWorldOrigin,
|
|
||||||
_liveAnimationScheduler,
|
|
||||||
livePresentation.StaticAnimationScheduler,
|
|
||||||
_animationPresenter,
|
|
||||||
_animatedEntities,
|
|
||||||
_equippedChildRenderer!,
|
|
||||||
liveEffectFrame);
|
|
||||||
_playerModeController = new AcDream.App.Input.PlayerModeController(
|
|
||||||
_localPlayerMode,
|
|
||||||
_playerControllerSlot,
|
|
||||||
_playerHostSlot,
|
|
||||||
_chaseCameraInput,
|
|
||||||
_cameraController!,
|
|
||||||
_physicsEngine,
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
_liveWorldOrigin,
|
|
||||||
_liveEntityMotionBindings,
|
|
||||||
_dats!,
|
|
||||||
_datLock,
|
|
||||||
_physicsDataCache,
|
|
||||||
_animatedEntities,
|
|
||||||
_localPlayerAnimation,
|
|
||||||
_localPlayerShadowSynchronizer,
|
|
||||||
_playerApproachCompletions,
|
|
||||||
_gameplayInputFrame,
|
|
||||||
_liveSessionController,
|
|
||||||
_movementTruthDiagnostics,
|
|
||||||
_localPlayerSkills,
|
|
||||||
_viewportAspect);
|
|
||||||
_devToolsComposition?.LateBindings.PlayerModeCommands.Bind(
|
|
||||||
_playerModeController);
|
|
||||||
_devToolsCommandBus?.Bind(_liveSessionController);
|
|
||||||
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
|
|
||||||
new AcDream.App.Input.LivePlayerModeAutoEntryContext(
|
|
||||||
_liveSessionController,
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
_worldReveal
|
|
||||||
?? throw new InvalidOperationException(
|
|
||||||
"World reveal was not composed before player auto-entry."),
|
|
||||||
_localPlayerMode,
|
|
||||||
_playerModeController));
|
|
||||||
_playerModeController.BindAutoEntry(_playerModeAutoEntry);
|
|
||||||
_localPlayerTeleport = _portalTunnelFallback.Transfer(
|
|
||||||
portalTunnel => new AcDream.App.Streaming.LocalPlayerTeleportController(
|
|
||||||
new AcDream.App.Streaming.LiveLocalPlayerTeleportAuthority(
|
|
||||||
livePresentation.LiveEntities,
|
|
||||||
_localPlayerIdentity),
|
|
||||||
_gameplayInputFrame,
|
|
||||||
_playerModeController,
|
|
||||||
new AcDream.App.Streaming.LocalPlayerTeleportStreamingOperations(
|
|
||||||
_liveWorldOrigin,
|
|
||||||
_streamingOriginRecenter!,
|
|
||||||
_streamingController!,
|
|
||||||
sealedDungeonCells),
|
|
||||||
_worldReveal,
|
|
||||||
new AcDream.App.Streaming.LocalPlayerTeleportPlacement(
|
|
||||||
_physicsEngine,
|
_physicsEngine,
|
||||||
livePresentation.LiveEntities,
|
_physicsDataCache,
|
||||||
|
_worldGameState,
|
||||||
|
_worldEvents,
|
||||||
|
_selection,
|
||||||
|
Objects,
|
||||||
|
_classificationCache,
|
||||||
|
_liveEntityRuntimeSlot,
|
||||||
|
_animatedEntities,
|
||||||
|
_remoteMovementObservations,
|
||||||
|
_remotePhysicsUpdater,
|
||||||
|
_remoteInboundMotion,
|
||||||
|
_inboundEntityEvents,
|
||||||
|
_liveEntityMotionBindings,
|
||||||
_localPlayerIdentity,
|
_localPlayerIdentity,
|
||||||
_playerControllerSlot,
|
_playerControllerSlot,
|
||||||
_playerHostSlot,
|
_playerHostSlot,
|
||||||
|
_localPlayerMode,
|
||||||
_chaseCameraInput,
|
_chaseCameraInput,
|
||||||
|
_localPlayerOutbound,
|
||||||
|
_localPlayerTeleportSink,
|
||||||
_liveWorldOrigin,
|
_liveWorldOrigin,
|
||||||
liveSpatialReconciler),
|
_renderRange,
|
||||||
new AcDream.App.Streaming.LocalPlayerTeleportSession(
|
_localPlayerShadow,
|
||||||
_liveSessionController),
|
_localPlayerSkills,
|
||||||
new AcDream.App.Streaming.LocalPlayerTeleportPresentation(
|
_viewportAspect,
|
||||||
portalTunnel)));
|
_playerApproachCompletions,
|
||||||
_localPlayerTeleportSink.Bind(_localPlayerTeleport);
|
_pointerPosition,
|
||||||
interactionUi.LateBindings.AdoptLateOwnerBinding(
|
_movementInput,
|
||||||
"selection view plane",
|
_inputCapture,
|
||||||
interactionUi.LateBindings.SelectionViewPlane.Bind(
|
_particleVisibility,
|
||||||
_localPlayerTeleport));
|
_translucencyFades,
|
||||||
|
_effectPoses,
|
||||||
|
_updateFrameClock,
|
||||||
|
_movementTruthDiagnostics,
|
||||||
|
_combatAttackOperations,
|
||||||
|
Combat,
|
||||||
|
_combatFeedback,
|
||||||
|
_portalTunnelFallback,
|
||||||
|
Console.WriteLine),
|
||||||
|
this).Compose(
|
||||||
|
hostInputCamera,
|
||||||
|
contentEffectsAudio,
|
||||||
|
worldRender,
|
||||||
|
interactionUi,
|
||||||
|
livePresentation);
|
||||||
|
|
||||||
var teleportRenderState =
|
var teleportRenderState =
|
||||||
new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(
|
new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(
|
||||||
_localPlayerTeleport);
|
sessionPlayer.LocalTeleport);
|
||||||
var renderLoginState = new AcDream.App.Rendering.RenderLoginStateSource(
|
var renderLoginState = new AcDream.App.Rendering.RenderLoginStateSource(
|
||||||
_options.LiveMode,
|
_options.LiveMode,
|
||||||
_localPlayerMode);
|
_localPlayerMode);
|
||||||
|
|
@ -1743,7 +1402,7 @@ public sealed class GameWindow :
|
||||||
new AcDream.App.Rendering.WorldRenderFrameBuilder(
|
new AcDream.App.Rendering.WorldRenderFrameBuilder(
|
||||||
new AcDream.App.Rendering.RuntimeWorldFrameCameraSource(
|
new AcDream.App.Rendering.RuntimeWorldFrameCameraSource(
|
||||||
_cameraController!,
|
_cameraController!,
|
||||||
_localPlayerTeleport),
|
sessionPlayer.LocalTeleport),
|
||||||
new AcDream.App.Rendering.RuntimeWorldFrameVisibilityPreparation(
|
new AcDream.App.Rendering.RuntimeWorldFrameVisibilityPreparation(
|
||||||
_retailSelectionScene,
|
_retailSelectionScene,
|
||||||
_particleVisibility,
|
_particleVisibility,
|
||||||
|
|
@ -1876,7 +1535,7 @@ public sealed class GameWindow :
|
||||||
var privatePresentation =
|
var privatePresentation =
|
||||||
new AcDream.App.Rendering.PrivatePresentationRenderer(
|
new AcDream.App.Rendering.PrivatePresentationRenderer(
|
||||||
new AcDream.App.Rendering.LocalPlayerPortalViewport(
|
new AcDream.App.Rendering.LocalPlayerPortalViewport(
|
||||||
_localPlayerTeleport!,
|
sessionPlayer.LocalTeleport,
|
||||||
_cameraController!),
|
_cameraController!),
|
||||||
renderFrameResources,
|
renderFrameResources,
|
||||||
_paperdollFramePresenter,
|
_paperdollFramePresenter,
|
||||||
|
|
@ -1899,19 +1558,19 @@ public sealed class GameWindow :
|
||||||
_devToolsFramePresenter
|
_devToolsFramePresenter
|
||||||
?? AcDream.App.Rendering.NullRenderFrameFailureRecovery.Instance);
|
?? AcDream.App.Rendering.NullRenderFrameFailureRecovery.Instance);
|
||||||
var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
|
var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
|
||||||
liveObjectFrame,
|
sessionPlayer.LiveObjectFrame,
|
||||||
_worldState,
|
_worldState,
|
||||||
_liveSessionController,
|
sessionPlayer.LiveSession,
|
||||||
localPlayerFrame,
|
sessionPlayer.LocalPlayerFrame,
|
||||||
liveSpatialReconciler);
|
sessionPlayer.LiveSpatialReconciler);
|
||||||
var cameraFrame = new AcDream.App.Rendering.CameraFrameController(
|
var cameraFrame = new AcDream.App.Rendering.CameraFrameController(
|
||||||
_cameraController!,
|
_cameraController!,
|
||||||
_inputCapture,
|
_inputCapture,
|
||||||
_cameraInput,
|
_cameraInput,
|
||||||
localPlayerFrameRuntime,
|
sessionPlayer.LocalPlayerFrameRuntime,
|
||||||
_chaseCameraInput,
|
_chaseCameraInput,
|
||||||
localPlayerFrame,
|
sessionPlayer.LocalPlayerFrame,
|
||||||
liveSpatialReconciler,
|
sessionPlayer.LiveSpatialReconciler,
|
||||||
new AcDream.App.Combat.CombatCameraTargetSource(
|
new AcDream.App.Combat.CombatCameraTargetSource(
|
||||||
_runtimeSettings,
|
_runtimeSettings,
|
||||||
Combat,
|
Combat,
|
||||||
|
|
@ -1923,15 +1582,15 @@ public sealed class GameWindow :
|
||||||
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
|
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
|
||||||
_updateFrameClock,
|
_updateFrameClock,
|
||||||
new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!),
|
new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!),
|
||||||
streamingFrame,
|
sessionPlayer.StreamingFrame,
|
||||||
_gameplayInputFrame,
|
sessionPlayer.GameplayInput,
|
||||||
liveFrameCoordinator,
|
liveFrameCoordinator,
|
||||||
new AcDream.App.Update.LiveEntityLivenessFramePhase(
|
new AcDream.App.Update.LiveEntityLivenessFramePhase(
|
||||||
_liveEntityLiveness,
|
sessionPlayer.Liveness,
|
||||||
new AcDream.App.Update.StopwatchClientMonotonicTimeSource()),
|
new AcDream.App.Update.StopwatchClientMonotonicTimeSource()),
|
||||||
_localPlayerTeleport,
|
sessionPlayer.LocalTeleport,
|
||||||
new AcDream.App.Update.PlayerModeAutoEntryFramePhase(
|
new AcDream.App.Update.PlayerModeAutoEntryFramePhase(
|
||||||
_playerModeAutoEntry),
|
sessionPlayer.PlayerModeAutoEntry),
|
||||||
cameraFrame);
|
cameraFrame);
|
||||||
_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);
|
_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);
|
||||||
_liveSessionHost = CreateLiveSessionHost();
|
_liveSessionHost = CreateLiveSessionHost();
|
||||||
|
|
@ -1988,7 +1647,7 @@ public sealed class GameWindow :
|
||||||
runtimeDiagnostics,
|
runtimeDiagnostics,
|
||||||
new AcDream.App.Input.PlayerModeGameplayCommands(
|
new AcDream.App.Input.PlayerModeGameplayCommands(
|
||||||
_localPlayerMode,
|
_localPlayerMode,
|
||||||
_playerModeController),
|
sessionPlayer.PlayerMode),
|
||||||
new AcDream.App.Input.ItemTargetModeCommands(
|
new AcDream.App.Input.ItemTargetModeCommands(
|
||||||
_itemInteractionController!),
|
_itemInteractionController!),
|
||||||
new AcDream.App.Input.GameplayCameraModeCommands(
|
new AcDream.App.Input.GameplayCameraModeCommands(
|
||||||
|
|
@ -1996,7 +1655,7 @@ public sealed class GameWindow :
|
||||||
combatCommand,
|
combatCommand,
|
||||||
new AcDream.App.Input.GameplayWindowCommands(_window!.Close));
|
new AcDream.App.Input.GameplayWindowCommands(_window!.Close));
|
||||||
var targets = new AcDream.App.Input.RuntimeGameplayInputPriorityTargets(
|
var targets = new AcDream.App.Input.RuntimeGameplayInputPriorityTargets(
|
||||||
_gameplayInputFrame!,
|
sessionPlayer.GameplayInput,
|
||||||
pointer,
|
pointer,
|
||||||
_retailUiRuntime,
|
_retailUiRuntime,
|
||||||
_selectionInteractions,
|
_selectionInteractions,
|
||||||
|
|
@ -2095,7 +1754,7 @@ public sealed class GameWindow :
|
||||||
private void ResetSessionPlayerPresentation()
|
private void ResetSessionPlayerPresentation()
|
||||||
{
|
{
|
||||||
_playerModeController?.ResetSession();
|
_playerModeController?.ResetSession();
|
||||||
_spawnClaimRangeMemo = null;
|
_spawnClaimHydration?.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResetSessionIdentity()
|
private void ResetSessionIdentity()
|
||||||
|
|
@ -2157,7 +1816,10 @@ public sealed class GameWindow :
|
||||||
|
|
||||||
return new AcDream.App.Net.LiveSessionEventRouter(
|
return new AcDream.App.Net.LiveSessionEventRouter(
|
||||||
session,
|
session,
|
||||||
_liveEntitySessionEvents.CreateSink(),
|
(_liveEntitySessionEvents
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"Live entity session routing was not composed."))
|
||||||
|
.CreateSink(),
|
||||||
new AcDream.App.Net.LiveEnvironmentSessionSink(
|
new AcDream.App.Net.LiveEnvironmentSessionSink(
|
||||||
_worldEnvironment.ApplyAdminEnvirons,
|
_worldEnvironment.ApplyAdminEnvirons,
|
||||||
_worldEnvironment.SynchronizeFromServer),
|
_worldEnvironment.SynchronizeFromServer),
|
||||||
|
|
@ -2392,34 +2054,6 @@ public sealed class GameWindow :
|
||||||
private void OnFramebufferResize(Silk.NET.Maths.Vector2D<int> newSize)
|
private void OnFramebufferResize(Silk.NET.Maths.Vector2D<int> newSize)
|
||||||
=> _framebufferResize.Resize(newSize);
|
=> _framebufferResize.Resize(newSize);
|
||||||
|
|
||||||
// #107 (2026-06-10): memoized "this indoor spawn claim can never hydrate"
|
|
||||||
// check against the dat's LandBlockInfo.NumCells. Used by the auto-entry
|
|
||||||
// hold so a garbage claim doesn't stall login forever; the Resolve-head
|
|
||||||
// safety net demotes it loudly once entry proceeds.
|
|
||||||
private (uint Claim, bool Unhydratable)? _spawnClaimRangeMemo;
|
|
||||||
|
|
||||||
private bool IsSpawnClaimUnhydratable(uint claim)
|
|
||||||
{
|
|
||||||
if ((claim & 0xFFFFu) < 0x0100u) return false;
|
|
||||||
if (_spawnClaimRangeMemo is { } m && m.Claim == claim) return m.Unhydratable;
|
|
||||||
|
|
||||||
bool unhydratable = false;
|
|
||||||
if (_dats is not null)
|
|
||||||
{
|
|
||||||
DatReaderWriter.DBObjs.LandBlockInfo? lbInfo;
|
|
||||||
lock (_datLock)
|
|
||||||
{
|
|
||||||
lbInfo = _dats.Get<DatReaderWriter.DBObjs.LandBlockInfo>(
|
|
||||||
(claim & 0xFFFF0000u) | 0xFFFEu);
|
|
||||||
}
|
|
||||||
uint low = claim & 0xFFFFu;
|
|
||||||
unhydratable = lbInfo is null || lbInfo.NumCells == 0
|
|
||||||
|| low >= 0x0100u + lbInfo.NumCells;
|
|
||||||
}
|
|
||||||
_spawnClaimRangeMemo = (claim, unhydratable);
|
|
||||||
return unhydratable;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnClosing()
|
private void OnClosing()
|
||||||
{
|
{
|
||||||
_hostQuiescence.StopAccepting();
|
_hostQuiescence.StopAccepting();
|
||||||
|
|
@ -2565,11 +2199,18 @@ public sealed class GameWindow :
|
||||||
// the later render-frontend stage still disposes the GL owners.
|
// the later render-frontend stage still disposes the GL owners.
|
||||||
new ResourceShutdownStage("frame borrowers",
|
new ResourceShutdownStage("frame borrowers",
|
||||||
[
|
[
|
||||||
new("runtime settings targets", _runtimeSettings.UnbindRuntimeTargets),
|
|
||||||
new("world frame composition", () =>
|
new("world frame composition", () =>
|
||||||
{
|
{
|
||||||
_frameGraphs.Withdraw();
|
_frameGraphs.Withdraw();
|
||||||
}),
|
}),
|
||||||
|
new("session/player bindings", () =>
|
||||||
|
{
|
||||||
|
SessionPlayerRuntimeBindings? bindings = _sessionPlayerBindings;
|
||||||
|
if (bindings is null)
|
||||||
|
return;
|
||||||
|
bindings.Dispose();
|
||||||
|
_sessionPlayerBindings = null;
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
new ResourceShutdownStage("session dependents",
|
new ResourceShutdownStage("session dependents",
|
||||||
[
|
[
|
||||||
|
|
@ -2587,8 +2228,6 @@ public sealed class GameWindow :
|
||||||
_cameraPointerInput;
|
_cameraPointerInput;
|
||||||
if (pointer is null) return;
|
if (pointer is null) return;
|
||||||
pointer.ReleaseMouseLookAfterSessionRetirement();
|
pointer.ReleaseMouseLookAfterSessionRetirement();
|
||||||
if (_gameplayInputFrame is { } gameplayFrame)
|
|
||||||
pointer.UnbindGameplayFrame(gameplayFrame);
|
|
||||||
_cameraPointerInput = null;
|
_cameraPointerInput = null;
|
||||||
}),
|
}),
|
||||||
new("retail UI", () =>
|
new("retail UI", () =>
|
||||||
|
|
|
||||||
|
|
@ -246,8 +246,38 @@ internal sealed class RuntimeSettingsController :
|
||||||
_runtimeTargets = targets;
|
_runtimeTargets = targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindRuntimeTargetsOwned(IRuntimeSettingsTargets targets)
|
||||||
|
{
|
||||||
|
BindRuntimeTargets(targets);
|
||||||
|
return new RuntimeTargetBinding(this, targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnbindRuntimeTargets(IRuntimeSettingsTargets expected)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_runtimeTargets, expected))
|
||||||
|
_runtimeTargets = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void UnbindRuntimeTargets() => _runtimeTargets = null;
|
public void UnbindRuntimeTargets() => _runtimeTargets = null;
|
||||||
|
|
||||||
|
private sealed class RuntimeTargetBinding : IDisposable
|
||||||
|
{
|
||||||
|
private RuntimeSettingsController? _owner;
|
||||||
|
private readonly IRuntimeSettingsTargets _expected;
|
||||||
|
|
||||||
|
public RuntimeTargetBinding(
|
||||||
|
RuntimeSettingsController owner,
|
||||||
|
IRuntimeSettingsTargets expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?
|
||||||
|
.UnbindRuntimeTargets(_expected);
|
||||||
|
}
|
||||||
|
|
||||||
public SettingsVM CreateViewModel(
|
public SettingsVM CreateViewModel(
|
||||||
KeyBindings persistedBindings,
|
KeyBindings persistedBindings,
|
||||||
InputDispatcher dispatcher,
|
InputDispatcher dispatcher,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
using AcDream.Content;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
|
namespace AcDream.App.Streaming;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Memoized range check for indoor spawn claims. Claims below the indoor-cell
|
||||||
|
/// range are always hydratable; indoor claims are validated against the exact
|
||||||
|
/// landblock's authored cell count.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class DatSpawnClaimHydrationClassifier
|
||||||
|
{
|
||||||
|
private readonly Func<uint, LandBlockInfo?> _lookup;
|
||||||
|
private (uint Claim, bool Unhydratable)? _memo;
|
||||||
|
|
||||||
|
public DatSpawnClaimHydrationClassifier(
|
||||||
|
IDatReaderWriter dats,
|
||||||
|
object datLock)
|
||||||
|
: this(CreateLookup(dats, datLock))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal DatSpawnClaimHydrationClassifier(
|
||||||
|
Func<uint, LandBlockInfo?> lookup) =>
|
||||||
|
_lookup = lookup ?? throw new ArgumentNullException(nameof(lookup));
|
||||||
|
|
||||||
|
public bool IsUnhydratable(uint claim)
|
||||||
|
{
|
||||||
|
uint low = claim & 0xFFFFu;
|
||||||
|
if (low < 0x0100u)
|
||||||
|
return false;
|
||||||
|
if (_memo is { } memo && memo.Claim == claim)
|
||||||
|
return memo.Unhydratable;
|
||||||
|
|
||||||
|
LandBlockInfo? info = _lookup((claim & 0xFFFF0000u) | 0xFFFEu);
|
||||||
|
bool unhydratable = info is null
|
||||||
|
|| info.NumCells == 0
|
||||||
|
|| low >= 0x0100u + info.NumCells;
|
||||||
|
_memo = (claim, unhydratable);
|
||||||
|
return unhydratable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset() => _memo = null;
|
||||||
|
|
||||||
|
private static Func<uint, LandBlockInfo?> CreateLookup(
|
||||||
|
IDatReaderWriter dats,
|
||||||
|
object datLock)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(dats);
|
||||||
|
ArgumentNullException.ThrowIfNull(datLock);
|
||||||
|
return id =>
|
||||||
|
{
|
||||||
|
lock (datLock)
|
||||||
|
return dats.Get<LandBlockInfo>(id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,17 @@ internal sealed class DeferredLocalPlayerTeleportNetworkSink
|
||||||
throw new InvalidOperationException("The local teleport sink is already bound.");
|
throw new InvalidOperationException("The local teleport sink is already bound.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindOwned(ILocalPlayerTeleportNetworkSink inner)
|
||||||
|
{
|
||||||
|
Bind(inner);
|
||||||
|
return new Binding(this, inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unbind(ILocalPlayerTeleportNetworkSink expected)
|
||||||
|
{
|
||||||
|
_ = Interlocked.CompareExchange(ref _inner, null, expected);
|
||||||
|
}
|
||||||
|
|
||||||
public void OnTeleportStarted(uint sequence) => Required().OnTeleportStarted(sequence);
|
public void OnTeleportStarted(uint sequence) => Required().OnTeleportStarted(sequence);
|
||||||
|
|
||||||
public void OfferDestination(
|
public void OfferDestination(
|
||||||
|
|
@ -53,6 +64,23 @@ internal sealed class DeferredLocalPlayerTeleportNetworkSink
|
||||||
private ILocalPlayerTeleportNetworkSink Required() =>
|
private ILocalPlayerTeleportNetworkSink Required() =>
|
||||||
_inner ?? throw new InvalidOperationException(
|
_inner ?? throw new InvalidOperationException(
|
||||||
"The local teleport sink was used before composition completed.");
|
"The local teleport sink was used before composition completed.");
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private DeferredLocalPlayerTeleportNetworkSink? _owner;
|
||||||
|
private readonly ILocalPlayerTeleportNetworkSink _expected;
|
||||||
|
|
||||||
|
public Binding(
|
||||||
|
DeferredLocalPlayerTeleportNetworkSink owner,
|
||||||
|
ILocalPlayerTeleportNetworkSink expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal interface ILocalPlayerTeleportInputLifetime
|
internal interface ILocalPlayerTeleportInputLifetime
|
||||||
|
|
|
||||||
|
|
@ -103,10 +103,38 @@ internal sealed class DeferredLiveEntityNetworkUpdateSink : ILiveEntityNetworkUp
|
||||||
throw new InvalidOperationException("The live-entity network sink is already bound.");
|
throw new InvalidOperationException("The live-entity network sink is already bound.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindOwned(ILiveEntityNetworkUpdateSink inner)
|
||||||
|
{
|
||||||
|
Bind(inner);
|
||||||
|
return new Binding(this, inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unbind(ILiveEntityNetworkUpdateSink expected)
|
||||||
|
{
|
||||||
|
_ = Interlocked.CompareExchange(ref _inner, null, expected);
|
||||||
|
}
|
||||||
|
|
||||||
public void ApplySameGeneration(SameGenerationCreateObjectEvents events) =>
|
public void ApplySameGeneration(SameGenerationCreateObjectEvents events) =>
|
||||||
(_inner ?? throw new InvalidOperationException(
|
(_inner ?? throw new InvalidOperationException(
|
||||||
"The live-entity network sink must be bound before a session starts."))
|
"The live-entity network sink must be bound before a session starts."))
|
||||||
.ApplySameGeneration(events);
|
.ApplySameGeneration(events);
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private DeferredLiveEntityNetworkUpdateSink? _owner;
|
||||||
|
private readonly ILiveEntityNetworkUpdateSink _expected;
|
||||||
|
|
||||||
|
public Binding(
|
||||||
|
DeferredLiveEntityNetworkUpdateSink owner,
|
||||||
|
ILiveEntityNetworkUpdateSink expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class DelegateLiveEntityNetworkUpdateSink(
|
internal sealed class DelegateLiveEntityNetworkUpdateSink(
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,37 @@ internal sealed class DeferredLiveEntityParentAcceptance
|
||||||
throw new InvalidOperationException("Live parent acceptance is already bound.");
|
throw new InvalidOperationException("Live parent acceptance is already bound.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindOwned(Func<ParentEvent.Parsed, bool> accept)
|
||||||
|
{
|
||||||
|
Bind(accept);
|
||||||
|
return new Binding(this, accept);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unbind(Func<ParentEvent.Parsed, bool> expected)
|
||||||
|
{
|
||||||
|
_ = Interlocked.CompareExchange(ref _accept, null, expected);
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryAccept(ParentEvent.Parsed update) =>
|
public bool TryAccept(ParentEvent.Parsed update) =>
|
||||||
(_accept ?? throw new InvalidOperationException(
|
(_accept ?? throw new InvalidOperationException(
|
||||||
"Live parent acceptance must be bound before a session starts."))(update);
|
"Live parent acceptance must be bound before a session starts."))(update);
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private DeferredLiveEntityParentAcceptance? _owner;
|
||||||
|
private readonly Func<ParentEvent.Parsed, bool> _expected;
|
||||||
|
|
||||||
|
public Binding(
|
||||||
|
DeferredLiveEntityParentAcceptance owner,
|
||||||
|
Func<ParentEvent.Parsed, bool> expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
|
internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,17 @@ internal sealed class DeferredLiveEntityRuntimeComponentLifecycle :
|
||||||
throw new InvalidOperationException("The live-entity runtime component lifecycle is already bound.");
|
throw new InvalidOperationException("The live-entity runtime component lifecycle is already bound.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindOwned(ILiveEntityRuntimeComponentLifecycle lifecycle)
|
||||||
|
{
|
||||||
|
Bind(lifecycle);
|
||||||
|
return new Binding(this, lifecycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unbind(ILiveEntityRuntimeComponentLifecycle expected)
|
||||||
|
{
|
||||||
|
_ = Interlocked.CompareExchange(ref _inner, null, expected);
|
||||||
|
}
|
||||||
|
|
||||||
public void TearDown(LiveEntityRecord record)
|
public void TearDown(LiveEntityRecord record)
|
||||||
{
|
{
|
||||||
ILiveEntityRuntimeComponentLifecycle lifecycle = Volatile.Read(ref _inner)
|
ILiveEntityRuntimeComponentLifecycle lifecycle = Volatile.Read(ref _inner)
|
||||||
|
|
@ -159,6 +170,23 @@ internal sealed class DeferredLiveEntityRuntimeComponentLifecycle :
|
||||||
"The live-entity runtime component lifecycle has not been bound.");
|
"The live-entity runtime component lifecycle has not been bound.");
|
||||||
lifecycle.TearDown(record);
|
lifecycle.TearDown(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private DeferredLiveEntityRuntimeComponentLifecycle? _owner;
|
||||||
|
private readonly ILiveEntityRuntimeComponentLifecycle _expected;
|
||||||
|
|
||||||
|
public Binding(
|
||||||
|
DeferredLiveEntityRuntimeComponentLifecycle owner,
|
||||||
|
ILiveEntityRuntimeComponentLifecycle expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Delegate adapter used by the App composition root.</summary>
|
/// <summary>Delegate adapter used by the App composition root.</summary>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,23 @@ public sealed class LiveCombatAttackOperationsTests
|
||||||
Assert.Throws<InvalidOperationException>(() => slot.Bind(second));
|
Assert.Throws<InvalidOperationException>(() => slot.Bind(second));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OwnedBindingReleasesOnlyItsExactOwnerAndAllowsRebind()
|
||||||
|
{
|
||||||
|
var slot = new CombatAttackOperationsSlot();
|
||||||
|
var first = new FakeOperations { CanStartValue = false };
|
||||||
|
var second = new FakeOperations { CanStartValue = true };
|
||||||
|
|
||||||
|
IDisposable firstBinding = slot.BindOwned(first);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => slot.BindOwned(second));
|
||||||
|
|
||||||
|
firstBinding.Dispose();
|
||||||
|
using IDisposable secondBinding = slot.BindOwned(second);
|
||||||
|
firstBinding.Dispose();
|
||||||
|
|
||||||
|
Assert.True(slot.CanStartAttack());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanStartRequiresLiveWorldBeforeTargetResolution()
|
public void CanStartRequiresLiveWorldBeforeTargetResolution()
|
||||||
{
|
{
|
||||||
|
|
@ -137,7 +154,8 @@ public sealed class LiveCombatAttackOperationsTests
|
||||||
|
|
||||||
private sealed class FakeOperations : ICombatAttackOperations
|
private sealed class FakeOperations : ICombatAttackOperations
|
||||||
{
|
{
|
||||||
public bool CanStartAttack() => true;
|
public bool CanStartValue { get; init; } = true;
|
||||||
|
public bool CanStartAttack() => CanStartValue;
|
||||||
public void PrepareAttackRequest()
|
public void PrepareAttackRequest()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
using AcDream.App.Composition;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Composition;
|
||||||
|
|
||||||
|
public sealed class SessionPlayerCompositionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RuntimeBindingsReleaseInReverseAndRetryOnlyFailedEdges()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var bindings = new SessionPlayerRuntimeBindings();
|
||||||
|
bindings.Adopt("first", new RetryBinding("first", calls, 0));
|
||||||
|
bindings.Adopt("second", new RetryBinding("second", calls, 1));
|
||||||
|
|
||||||
|
Assert.Throws<AggregateException>(bindings.Dispose);
|
||||||
|
Assert.Equal(["second", "first"], calls);
|
||||||
|
|
||||||
|
bindings.Dispose();
|
||||||
|
bindings.Dispose();
|
||||||
|
|
||||||
|
Assert.Equal(["second", "first", "second"], calls);
|
||||||
|
Assert.Throws<ObjectDisposedException>(() =>
|
||||||
|
bindings.Adopt("late", new RetryBinding("late", calls, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SpawnClaimClassifierPreservesIndoorBoundaryMemoAndReset()
|
||||||
|
{
|
||||||
|
int lookups = 0;
|
||||||
|
uint lastDid = 0;
|
||||||
|
var info = new LandBlockInfo { NumCells = 2 };
|
||||||
|
var classifier = new DatSpawnClaimHydrationClassifier(did =>
|
||||||
|
{
|
||||||
|
lookups++;
|
||||||
|
lastDid = did;
|
||||||
|
return info;
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.False(classifier.IsUnhydratable(0x123400FFu));
|
||||||
|
Assert.Equal(0, lookups);
|
||||||
|
Assert.False(classifier.IsUnhydratable(0x12340100u));
|
||||||
|
Assert.Equal(0x1234FFFEu, lastDid);
|
||||||
|
Assert.False(classifier.IsUnhydratable(0x12340100u));
|
||||||
|
Assert.Equal(1, lookups);
|
||||||
|
Assert.False(classifier.IsUnhydratable(0x12340101u));
|
||||||
|
Assert.True(classifier.IsUnhydratable(0x12340102u));
|
||||||
|
|
||||||
|
classifier.Reset();
|
||||||
|
Assert.True(classifier.IsUnhydratable(0x12340102u));
|
||||||
|
Assert.Equal(4, lookups);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MissingOrEmptyLandblockMakesIndoorClaimUnhydratable()
|
||||||
|
{
|
||||||
|
var missing = new DatSpawnClaimHydrationClassifier(_ => null);
|
||||||
|
var empty = new DatSpawnClaimHydrationClassifier(
|
||||||
|
_ => new LandBlockInfo { NumCells = 0 });
|
||||||
|
|
||||||
|
Assert.True(missing.IsUnhydratable(0x12340100u));
|
||||||
|
Assert.True(empty.IsUnhydratable(0x12340100u));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GameWindowUsesSessionPhaseAndContainsNoPhaseSevenBody()
|
||||||
|
{
|
||||||
|
string root = FindRepoRoot();
|
||||||
|
string window = File.ReadAllText(Path.Combine(
|
||||||
|
root,
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Rendering",
|
||||||
|
"GameWindow.cs"));
|
||||||
|
string phase = File.ReadAllText(Path.Combine(
|
||||||
|
root,
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"SessionPlayerComposition.cs"));
|
||||||
|
|
||||||
|
Assert.Contains("new SessionPlayerCompositionPhase(", window);
|
||||||
|
Assert.DoesNotContain("LandblockStreamer.CreateForRequests(", window);
|
||||||
|
Assert.DoesNotContain("new AcDream.App.Net.LiveSessionController()", window);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"new AcDream.App.Streaming.LocalPlayerTeleportController(",
|
||||||
|
window);
|
||||||
|
Assert.DoesNotContain("IsSpawnClaimUnhydratable", window);
|
||||||
|
Assert.Contains("LandblockStreamer.CreateForRequests(", phase);
|
||||||
|
Assert.Contains("new LiveSessionController()", phase);
|
||||||
|
Assert.Contains("new LocalPlayerTeleportController(", phase);
|
||||||
|
Assert.Contains("new DatSpawnClaimHydrationClassifier(", phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProductionPhaseStartsStreamerBeforeSessionAndTransfersPortalLast()
|
||||||
|
{
|
||||||
|
string phase = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"SessionPlayerComposition.cs"));
|
||||||
|
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
phase,
|
||||||
|
"LandblockStreamer.CreateForRequests(",
|
||||||
|
"streamerLease.Resource.Start();",
|
||||||
|
"new StreamingController(",
|
||||||
|
"new WorldRevealCoordinator(",
|
||||||
|
"new LiveSessionController()",
|
||||||
|
"new LiveEntityHydrationController(",
|
||||||
|
"new LiveEntityNetworkUpdateController(",
|
||||||
|
"new GameplayInputFrameController(",
|
||||||
|
"new PlayerModeController(",
|
||||||
|
"d.PortalTunnelFallback.Transfer(",
|
||||||
|
"d.TeleportSink.BindOwned(localTeleport)",
|
||||||
|
"_publication.PublishSessionPlayer(result);");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RetryBinding(
|
||||||
|
string name,
|
||||||
|
List<string> calls,
|
||||||
|
int failures) : IDisposable
|
||||||
|
{
|
||||||
|
private int _failures = failures;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
calls.Add(name);
|
||||||
|
if (_failures-- > 0)
|
||||||
|
throw new InvalidOperationException("retry");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertAppearsInOrder(string source, params string[] values)
|
||||||
|
{
|
||||||
|
int cursor = 0;
|
||||||
|
foreach (string value in values)
|
||||||
|
{
|
||||||
|
int found = source.IndexOf(value, cursor, StringComparison.Ordinal);
|
||||||
|
Assert.True(found >= 0, $"Missing expected source fragment: {value}");
|
||||||
|
cursor = found + value.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FindRepoRoot()
|
||||||
|
{
|
||||||
|
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||||
|
while (directory is not null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||||
|
return directory.FullName;
|
||||||
|
directory = directory.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using AcDream.App.Diagnostics;
|
using AcDream.App.Diagnostics;
|
||||||
|
using AcDream.App.Net;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.UI.Abstractions;
|
using AcDream.UI.Abstractions;
|
||||||
|
|
||||||
|
|
@ -8,6 +9,24 @@ namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
public sealed class DevToolsFramePresenterTests
|
public sealed class DevToolsFramePresenterTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CommandBusOwnedBindingReleasesExactlyAndAllowsRebind()
|
||||||
|
{
|
||||||
|
var source = new DevToolsCommandBusSource();
|
||||||
|
using var first = new LiveSessionController();
|
||||||
|
using var second = new LiveSessionController();
|
||||||
|
|
||||||
|
IDisposable firstBinding = source.BindOwned(first);
|
||||||
|
Assert.Same(first.Commands, source.Current);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => source.BindOwned(second));
|
||||||
|
|
||||||
|
firstBinding.Dispose();
|
||||||
|
using IDisposable secondBinding = source.BindOwned(second);
|
||||||
|
firstBinding.Dispose();
|
||||||
|
|
||||||
|
Assert.Same(second.Commands, source.Current);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Frame_PreservesBeginThenMenuPanelsAndDrawDataOrder()
|
public void Frame_PreservesBeginThenMenuPanelsAndDrawDataOrder()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,25 @@ public sealed class DevToolsRuntimeSourcesTests
|
||||||
Assert.Throws<ObjectDisposedException>(() => source.Bind(first));
|
Assert.Throws<ObjectDisposedException>(() => source.Bind(first));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlayerModeOwnedBindingReleasesExactlyAndAllowsRebind()
|
||||||
|
{
|
||||||
|
var source = new DeferredDevToolsPlayerModeCommands();
|
||||||
|
var first = new PlayerModeTarget();
|
||||||
|
var second = new PlayerModeTarget();
|
||||||
|
|
||||||
|
IDisposable firstBinding = source.BindOwned(first);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => source.BindOwned(second));
|
||||||
|
|
||||||
|
firstBinding.Dispose();
|
||||||
|
using IDisposable secondBinding = source.BindOwned(second);
|
||||||
|
firstBinding.Dispose();
|
||||||
|
source.ToggleFlyOrChase();
|
||||||
|
|
||||||
|
Assert.Equal(0, first.ToggleCalls);
|
||||||
|
Assert.Equal(1, second.ToggleCalls);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void WorldCountSlotCannotFreezeOrReleaseTheWrongWorld()
|
public void WorldCountSlotCannotFreezeOrReleaseTheWrongWorld()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -39,14 +39,24 @@ public sealed class GameWindowRenderLeafCompositionTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Composition_transfers_portal_tunnel_before_constructing_frame_borrowers()
|
public void Composition_transfers_portal_tunnel_before_constructing_frame_borrowers()
|
||||||
{
|
{
|
||||||
|
string phase = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"SessionPlayerComposition.cs"));
|
||||||
string source = GameWindowSource();
|
string source = GameWindowSource();
|
||||||
|
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
phase,
|
||||||
|
"d.PortalTunnelFallback.Transfer(",
|
||||||
|
"new LocalPlayerTeleportController(",
|
||||||
|
"new LocalPlayerTeleportPresentation(portalTunnel)",
|
||||||
|
"d.TeleportSink.BindOwned(localTeleport)",
|
||||||
|
"_publication.PublishSessionPlayer(result);");
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
source,
|
source,
|
||||||
"_portalTunnelFallback.Transfer(",
|
"new SessionPlayerCompositionPhase(",
|
||||||
"new AcDream.App.Streaming.LocalPlayerTeleportController(",
|
|
||||||
"new AcDream.App.Streaming.LocalPlayerTeleportPresentation(",
|
|
||||||
"_localPlayerTeleportSink.Bind(_localPlayerTeleport);",
|
|
||||||
"new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(",
|
"new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(",
|
||||||
"new AcDream.App.Rendering.RenderFrameResourceController(",
|
"new AcDream.App.Rendering.RenderFrameResourceController(",
|
||||||
"new AcDream.App.Rendering.PrivatePresentationRenderer(",
|
"new AcDream.App.Rendering.PrivatePresentationRenderer(",
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"private AcDream.App.Net.LiveSessionHost");
|
"private AcDream.App.Net.LiveSessionHost");
|
||||||
string body = MethodBody(
|
string body = MethodBody(
|
||||||
"private void OnFramebufferResize(",
|
"private void OnFramebufferResize(",
|
||||||
"private (uint Claim, bool Unhydratable)? _spawnClaimRangeMemo;");
|
"private void OnClosing()");
|
||||||
|
|
||||||
Assert.Contains("=> _framebufferResize.Resize(newSize);", body, StringComparison.Ordinal);
|
Assert.Contains("=> _framebufferResize.Resize(newSize);", body, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("Viewport(", body, StringComparison.Ordinal);
|
Assert.DoesNotContain("Viewport(", body, StringComparison.Ordinal);
|
||||||
|
|
@ -294,8 +294,18 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"new SettingsDevToolsCompositionPhase(",
|
"new SettingsDevToolsCompositionPhase(",
|
||||||
"this).Compose(platform, hostInputCamera, contentEffectsAudio);",
|
"this).Compose(platform, hostInputCamera, contentEffectsAudio);",
|
||||||
"new WorldRenderCompositionPhase(",
|
"new WorldRenderCompositionPhase(",
|
||||||
"_runtimeSettings.BindRuntimeTargets(",
|
"new SessionPlayerCompositionPhase(",
|
||||||
"_liveSessionHost.Start(_options)");
|
"_liveSessionHost.Start(_options)");
|
||||||
|
string sessionPhase = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"SessionPlayerComposition.cs"));
|
||||||
|
Assert.Contains(
|
||||||
|
"d.Settings.BindRuntimeTargetsOwned(settingsTargets)",
|
||||||
|
sessionPhase,
|
||||||
|
StringComparison.Ordinal);
|
||||||
string settingsPhase = File.ReadAllText(Path.Combine(
|
string settingsPhase = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
"src",
|
"src",
|
||||||
|
|
@ -320,7 +330,8 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
shutdown,
|
shutdown,
|
||||||
"_runtimeSettings.UnbindViewModel()",
|
"_runtimeSettings.UnbindViewModel()",
|
||||||
"_runtimeSettings.UnbindRuntimeTargets",
|
"new ResourceShutdownStage(\"frame borrowers\"",
|
||||||
|
"bindings.Dispose();",
|
||||||
"_retailUiLease.Dispose()",
|
"_retailUiLease.Dispose()",
|
||||||
"_streamer?.Dispose()",
|
"_streamer?.Dispose()",
|
||||||
"_wbDrawDispatcher?.Dispose()",
|
"_wbDrawDispatcher?.Dispose()",
|
||||||
|
|
@ -425,7 +436,6 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"pointer.Dispose();",
|
"pointer.Dispose();",
|
||||||
"new ResourceShutdownStage(\"session dependents\"",
|
"new ResourceShutdownStage(\"session dependents\"",
|
||||||
"pointer.ReleaseMouseLookAfterSessionRetirement();",
|
"pointer.ReleaseMouseLookAfterSessionRetirement();",
|
||||||
"pointer.UnbindGameplayFrame(gameplayFrame);",
|
|
||||||
"_cameraPointerInput = null;");
|
"_cameraPointerInput = null;");
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
dispose,
|
dispose,
|
||||||
|
|
@ -472,6 +482,12 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"Composition",
|
"Composition",
|
||||||
"WorldRenderComposition.cs"));
|
"WorldRenderComposition.cs"));
|
||||||
string livePhase = LivePresentationSource();
|
string livePhase = LivePresentationSource();
|
||||||
|
string sessionPhase = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"SessionPlayerComposition.cs"));
|
||||||
|
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
livePhase,
|
livePhase,
|
||||||
|
|
@ -483,8 +499,13 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
load,
|
load,
|
||||||
"new WorldRenderCompositionPhase(",
|
"new WorldRenderCompositionPhase(",
|
||||||
"new LivePresentationCompositionPhase(",
|
"new LivePresentationCompositionPhase(",
|
||||||
"_portalTunnelFallback.Transfer(",
|
"new SessionPlayerCompositionPhase(",
|
||||||
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);");
|
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);");
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
sessionPhase,
|
||||||
|
"d.PortalTunnelFallback.Transfer(",
|
||||||
|
"new LocalPlayerTeleportController(",
|
||||||
|
"_publication.PublishSessionPlayer(result);");
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
worldPhase,
|
worldPhase,
|
||||||
"lifetime.AcquireTerrainAtlas(",
|
"lifetime.AcquireTerrainAtlas(",
|
||||||
|
|
|
||||||
|
|
@ -524,6 +524,27 @@ public sealed class RuntimeSettingsControllerTests
|
||||||
Assert.Equal(1, second.UiLockCalls);
|
Assert.Equal(1, second.UiLockCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OwnedRuntimeTargetsReleaseExactlyAndAllowRebind()
|
||||||
|
{
|
||||||
|
var events = new List<string>();
|
||||||
|
RuntimeSettingsController controller = CreateController(events: events);
|
||||||
|
var first = new FakeRuntimeTargets(events);
|
||||||
|
var second = new FakeRuntimeTargets(events);
|
||||||
|
|
||||||
|
IDisposable firstBinding = controller.BindRuntimeTargetsOwned(first);
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
controller.BindRuntimeTargetsOwned(second));
|
||||||
|
|
||||||
|
firstBinding.Dispose();
|
||||||
|
using IDisposable secondBinding = controller.BindRuntimeTargetsOwned(second);
|
||||||
|
firstBinding.Dispose();
|
||||||
|
controller.SetUiLocked(true);
|
||||||
|
|
||||||
|
Assert.Equal(0, first.UiLockCalls);
|
||||||
|
Assert.Equal(1, second.UiLockCalls);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DisplayPersistenceFailureDoesNotPublishStateOrTargets()
|
public void DisplayPersistenceFailureDoesNotPublishStateOrTargets()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,25 @@ namespace AcDream.App.Tests.Streaming;
|
||||||
|
|
||||||
public sealed class LocalPlayerTeleportControllerTests
|
public sealed class LocalPlayerTeleportControllerTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void DeferredNetworkOwnedBindingReleasesExactlyAndAllowsRebind()
|
||||||
|
{
|
||||||
|
var deferred = new DeferredLocalPlayerTeleportNetworkSink();
|
||||||
|
var first = new FakeNetworkSink();
|
||||||
|
var second = new FakeNetworkSink();
|
||||||
|
|
||||||
|
IDisposable firstBinding = deferred.BindOwned(first);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => deferred.BindOwned(second));
|
||||||
|
|
||||||
|
firstBinding.Dispose();
|
||||||
|
using IDisposable secondBinding = deferred.BindOwned(second);
|
||||||
|
firstBinding.Dispose();
|
||||||
|
deferred.OnTeleportStarted(9);
|
||||||
|
|
||||||
|
Assert.Empty(first.Starts);
|
||||||
|
Assert.Equal([9u], second.Starts);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DestinationBeforeStart_IsReplayedOnceAfterPresentationActivates()
|
public void DestinationBeforeStart_IsReplayedOnceAfterPresentationActivates()
|
||||||
{
|
{
|
||||||
|
|
@ -684,6 +703,20 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class FakeNetworkSink : ILocalPlayerTeleportNetworkSink
|
||||||
|
{
|
||||||
|
public List<uint> Starts { get; } = [];
|
||||||
|
public void OnTeleportStarted(uint sequence) => Starts.Add(sequence);
|
||||||
|
public void OfferDestination(
|
||||||
|
WorldSession.EntityPositionUpdate update,
|
||||||
|
bool teleportTimestampAdvanced)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public void ResetSession()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class FakePresentation : ILocalPlayerTeleportPresentation
|
private sealed class FakePresentation : ILocalPlayerTeleportPresentation
|
||||||
{
|
{
|
||||||
private readonly List<string> _order;
|
private readonly List<string> _order;
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,29 @@ public sealed class DeferredLiveEntityRuntimeComponentLifecycleTests
|
||||||
Assert.Throws<ArgumentException>(() => bridge.Bind(bridge));
|
Assert.Throws<ArgumentException>(() => bridge.Bind(bridge));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OwnedBindingReleasesExactlyAndAllowsRebind()
|
||||||
|
{
|
||||||
|
var bridge = new DeferredLiveEntityRuntimeComponentLifecycle();
|
||||||
|
int firstCalls = 0;
|
||||||
|
int secondCalls = 0;
|
||||||
|
var first = new DelegateLiveEntityRuntimeComponentLifecycle(
|
||||||
|
_ => firstCalls++);
|
||||||
|
var second = new DelegateLiveEntityRuntimeComponentLifecycle(
|
||||||
|
_ => secondCalls++);
|
||||||
|
|
||||||
|
IDisposable firstBinding = bridge.BindOwned(first);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => bridge.BindOwned(second));
|
||||||
|
|
||||||
|
firstBinding.Dispose();
|
||||||
|
using IDisposable secondBinding = bridge.BindOwned(second);
|
||||||
|
firstBinding.Dispose();
|
||||||
|
bridge.TearDown(CreateRecord());
|
||||||
|
|
||||||
|
Assert.Equal(0, firstCalls);
|
||||||
|
Assert.Equal(1, secondCalls);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ConcurrentBind_HasOneWinnerAndPublishesThatOwner()
|
public async Task ConcurrentBind_HasOneWinnerAndPublishesThatOwner()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,24 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
Assert.Throws<InvalidOperationException>(() => bridge.Bind(_ => false));
|
Assert.Throws<InvalidOperationException>(() => bridge.Bind(_ => false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParentAcceptanceOwnedBindingReleasesExactlyAndAllowsRebind()
|
||||||
|
{
|
||||||
|
var bridge = new DeferredLiveEntityParentAcceptance();
|
||||||
|
Func<ParentEvent.Parsed, bool> first = _ => true;
|
||||||
|
Func<ParentEvent.Parsed, bool> second = _ => false;
|
||||||
|
|
||||||
|
IDisposable firstBinding = bridge.BindOwned(first);
|
||||||
|
Assert.True(bridge.TryAccept(default));
|
||||||
|
Assert.Throws<InvalidOperationException>(() => bridge.BindOwned(second));
|
||||||
|
|
||||||
|
firstBinding.Dispose();
|
||||||
|
using IDisposable secondBinding = bridge.BindOwned(second);
|
||||||
|
firstBinding.Dispose();
|
||||||
|
|
||||||
|
Assert.False(bridge.TryAccept(default));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FreshCreate_PublishesCanonicalOrderAndReadyAfterMaterialization()
|
public void FreshCreate_PublishesCanonicalOrderAndReadyAfterMaterialization()
|
||||||
{
|
{
|
||||||
|
|
@ -1580,6 +1598,25 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
deferred.Bind(new RecordingNetworkSink()));
|
deferred.Bind(new RecordingNetworkSink()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeferredNetworkOwnedBindingReleasesExactlyAndAllowsRebind()
|
||||||
|
{
|
||||||
|
var deferred = new DeferredLiveEntityNetworkUpdateSink();
|
||||||
|
var first = new RecordingNetworkSink();
|
||||||
|
var second = new RecordingNetworkSink();
|
||||||
|
|
||||||
|
IDisposable firstBinding = deferred.BindOwned(first);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => deferred.BindOwned(second));
|
||||||
|
|
||||||
|
firstBinding.Dispose();
|
||||||
|
using IDisposable secondBinding = deferred.BindOwned(second);
|
||||||
|
firstBinding.Dispose();
|
||||||
|
deferred.ApplySameGeneration(default);
|
||||||
|
|
||||||
|
Assert.Equal(0, first.ApplyCount);
|
||||||
|
Assert.Equal(1, second.ApplyCount);
|
||||||
|
}
|
||||||
|
|
||||||
private const uint Guid = 0x70000001u;
|
private const uint Guid = 0x70000001u;
|
||||||
private const uint Cell = 0x01010001u;
|
private const uint Cell = 0x01010001u;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -343,9 +343,15 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Composition",
|
"Composition",
|
||||||
"LivePresentationComposition.cs"));
|
"LivePresentationComposition.cs"));
|
||||||
|
string sessionPlayer = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"SessionPlayerComposition.cs"));
|
||||||
|
|
||||||
Assert.Contains("new AcDream.App.Update.LiveObjectFrameController(", source);
|
Assert.Contains("new LiveObjectFrameController(", sessionPlayer);
|
||||||
Assert.Contains("new AcDream.App.Update.LiveSpatialPresentationReconciler(", source);
|
Assert.Contains("new LiveSpatialPresentationReconciler(", sessionPlayer);
|
||||||
Assert.Contains("new AcDream.App.World.RetailLiveFrameCoordinator(", source);
|
Assert.Contains("new AcDream.App.World.RetailLiveFrameCoordinator(", source);
|
||||||
Assert.DoesNotContain("AdvanceLiveObjectRuntime", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("AdvanceLiveObjectRuntime", source, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("ReconcileLiveObjectSpatialPresentation", source,
|
Assert.DoesNotContain("ReconcileLiveObjectSpatialPresentation", source,
|
||||||
|
|
@ -363,8 +369,14 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Rendering",
|
"Rendering",
|
||||||
"GameWindow.cs"));
|
"GameWindow.cs"));
|
||||||
|
string sessionPlayer = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"SessionPlayerComposition.cs"));
|
||||||
|
|
||||||
Assert.Contains("new AcDream.App.Streaming.StreamingFrameController(", source);
|
Assert.Contains("new StreamingFrameController(", sessionPlayer);
|
||||||
Assert.DoesNotContain("_streamingFrame", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("_streamingFrame", source, StringComparison.Ordinal);
|
||||||
Assert.Equal(1, CountOccurrences(
|
Assert.Equal(1, CountOccurrences(
|
||||||
source,
|
source,
|
||||||
|
|
@ -385,8 +397,14 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Rendering",
|
"Rendering",
|
||||||
"GameWindow.cs"));
|
"GameWindow.cs"));
|
||||||
|
string sessionPlayer = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"SessionPlayerComposition.cs"));
|
||||||
|
|
||||||
Assert.Contains("new AcDream.App.Input.GameplayInputFrameController(", source);
|
Assert.Contains("new GameplayInputFrameController(", sessionPlayer);
|
||||||
Assert.DoesNotContain("_gameplayInputFrame!.Tick", source,
|
Assert.DoesNotContain("_gameplayInputFrame!.Tick", source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("_inputDispatcher?.Tick()", source,
|
Assert.DoesNotContain("_inputDispatcher?.Tick()", source,
|
||||||
|
|
@ -468,10 +486,16 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Physics",
|
"Physics",
|
||||||
"LiveEntityNetworkUpdateController.cs"));
|
"LiveEntityNetworkUpdateController.cs"));
|
||||||
|
string sessionPlayer = File.ReadAllText(Path.Combine(
|
||||||
|
root,
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"SessionPlayerComposition.cs"));
|
||||||
|
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"new AcDream.App.Streaming.LocalPlayerTeleportController(",
|
"new LocalPlayerTeleportController(",
|
||||||
source,
|
sessionPlayer,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("_localPlayerTeleport!.Tick", source,
|
Assert.DoesNotContain("_localPlayerTeleport!.Tick", source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
|
@ -491,7 +515,7 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
source,
|
source,
|
||||||
"new AcDream.App.Update.LiveEntityLivenessFramePhase(",
|
"new AcDream.App.Update.LiveEntityLivenessFramePhase(",
|
||||||
"_localPlayerTeleport,",
|
"sessionPlayer.LocalTeleport,",
|
||||||
"new AcDream.App.Update.PlayerModeAutoEntryFramePhase(",
|
"new AcDream.App.Update.PlayerModeAutoEntryFramePhase(",
|
||||||
"cameraFrame);");
|
"cameraFrame);");
|
||||||
}
|
}
|
||||||
|
|
@ -799,7 +823,7 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains("d.WorldOrigin.CellLocalForSeed", livePresentation,
|
Assert.Contains("d.WorldOrigin.CellLocalForSeed", livePresentation,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains("_liveEntitySessionEvents.CreateSink()", source,
|
Assert.Contains("Live entity session routing was not composed.", source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue