acdream/src/AcDream.App/Rendering/GameWindowLifetime.cs
Erik 6dc7ba51ee feat(physics): C4 route 4b-3 — remote teleport + cell-less through the canonical placement
Flips the last remote classification (SetPosition: teleport-advanced and
cell-less) onto 4b-1's RuntimeRemotePlacementDriveController, runs retail's
teleport_hook before the placement, and deletes the legacy remote-teleport
machinery. Contract: docs/research/2026-08-04-c4-route-4b-3-contract.md.

Retail: MoveOrTeleport @0x00516330's branch @0x00516386 -> teleport_hook
@0x005163EF -> SetFlags(0x1012) @0x00516414 -> SetPosition @0x00516420 ->
return 1 @0x00516438. The hook @0x00514ED0 runs BEFORE the placement and
regardless of its outcome. Retail places this branch unconditionally, at any
distance and any contact state (arg4 is read only @0x0051638E, after the
branch) — which is what retires AP-137's cell-less enqueue-vs-place delta.

D1 — the classifier's cell-less input is now the PRE-merge committed cell.
Retail's predicate is `this_1->cell == 0`, the BODY's own cell at
MoveOrTeleport entry (this_1 is assigned from this @0x00516334). acdream fed
the POST-merge canonical.FullCellId, which RefreshSnapshot ->
RefreshDerivedState -> SetFullCell has already stamped with the accepted wire
cell; a zero wire cell fails validation into RejectedData first. The shipped
remote cell-less predicate was therefore dead code, not merely different from
remotePlacementRequired. Threaded via a builder overload; route 1's overload
is untouched. The graphical !IsSpatiallyVisible arm of
projectionRequiresTeleportHook is deleted — a presentation predicate with no
retail analogue that fired the teleport machinery on a routine hot path.

Deleted: RemoteTeleportController (605), RemoteTeleportPlacement (85),
RemoteShadowPlacementSynchronizer (49), their 1,709 lines of tests, the
remotePlacementRequired predicate, the TeleportHookRequired plumbing, the
legacy pre-operation ConstrainTo fallback, and the player arm's legacy
!IsGrounded fallback. Net -2,030 lines.

Structural fix (two independent Opus reviews, round 1 FAIL/FAIL): three of the
four MAJORs were one defect — OnPosition carried two parallel inline copies of
the routing tail (player-guid, NPC-guid) that had drifted. Extracted
RunRemoteArmTail (3 call sites) and ApplyWireAirborneLeftoverBookkeeping (2),
both branches now share one implementation.

  A1  ToConstraintArm mapped AirborneSnap -> AirborneNoOperation, so the NPC
      arm armed ConstrainTo ZERO times for an out-of-contact wire-grounded
      creature — a regression this slice introduced while closing a
      structurally identical hole. Now maps to NearInterpolate; switch made
      total with a throwing default proven unreachable.
  R1  D2's write-nothing shape existed on the player arm only; NPC packets
      fell through and wrote the body. Retail makes no player/NPC distinction.
  R2  report_collision_end(this,1) @0x00514F31 was bound to
      ShadowObjects.Suspend, a port of a DIFFERENT retail function
      (remove_shadows_from_cells) that teleport_hook never calls. Now routes
      to RuntimeCollisionReportingState.LeaveWorld, which wraps the private
      ForceEnd in an admission-blocking transaction so a DoCollisionEnd
      callback cannot recreate the contact table.
  R3/A2 A teleported NPC synthesized ServerVelocity from the teleport distance
      (~1,000+ m/s) and planned a run cycle from it. Both the install and
      RemoteServerControlledVelocityCycle.Apply now gate on !isTeleportRoute.

BISECT HAZARD — A1's fix is correct only BECAUSE R1 landed. AirborneSnap is
reachable wire-airborne on the NPC arm only while D2's shape is missing there.
Reverting R1 alone silently inverts A1 into the opposite divergence: arming
where retail returns 0. Revert both or neither.

Also in the velocity hunk: the NPC block's two !IsPlayerGuid(update.Guid)
guards were dropped when it was wrapped in `if (!isTeleportRoute)`. Safe — all
five exit paths of the enclosing IsPlayerGuid block return, so the predicate is
unconditionally false below it — but it was unremarked by both reviews.

Register: AP-137 REWRITTEN (not deleted) to the surviving acdream-only
divergences — null classification during the login window and Rejected*
through UnroutedCatchUp keep a row. AD-42's RemoteTeleportController citation
retired; AP-136/AP-138 writer lists corrected to the two surviving non-Position
rebucket writers; AP-138 gains the teleport arm as a second producer of the
visible-without-collision residual (retirement path remains #309). AP-135 is
untouched and its two airborne bookkeeping writes are preserved on both arms.
AP-131 does not retire; #276 does not close.

Proof obligation 1: ParkCollisionResidents' overlap throw stays unreachable —
the teleport arm adds packets to the same TryBeginExclusiveAuthoredPlacement
one-operation-per-key machinery the far arm uses, opens no new operation shape,
and every DeferredCell outcome cancels synchronously with
restoreCancelledPark: true. The guarded property remains
HasOldPrefixPlacementDebt's stall, not a throw (4b-1's B2 caveat stands).

Correction to an earlier claim: LiveEntityPresentationController's
_activePlacementOwners was NOT write-never at HEAD —
remotePlacementRequired -> BeginPlacement -> Begin -> BeginAuthoritativePlacement
was a live writer chain. It becomes write-never BECAUSE this slice deletes that
chain, which is why deleting the dead half is behaviour-preserving.

Probe: ACDREAM_PROBE_REMOTE_TELEPORT=1 emits one [remote-teleport] line per
routed arm (guid, cause, hook-ran, placement status). TEMPORARY, strip with the
probe family.

Carried, disclosed not fixed: no dedicated bidirectional collision-partner test
for R2 (the wiring, not LeaveWorld itself, is what lacks coverage); the
stress test's teleport step drives hand-written field assignments rather than
the canonical arm; the per-packet runTeleportHook closure allocation (network
path, not the resolve path Slice I's 0 B discipline governs — file before
route 5 adds a fourth call site). B2: IRuntimeCollisionReportObserver has zero
production implementations, so retail's bidirectional DoCollisionEnd half still
reaches no gameplay consumer — this fix closes the wrong-function binding, not
that nobody listens.

Complete Release suite MEASURED at 11,013 passed / 4 skipped / 0 failed
(baseline 11,027/4/0; net -14 = ~33 deleted test cases against ~19 added).
Neither known flake fired (#302 PortalProjectionTests GC-allocation, #308
NakEmissionTests wall-clock).

STILL OWED: the two-client connected gate, which MUST use an NPC/creature
teleport target. Both round-1 MAJORs lived on the NPC arm and the velocity
cycle early-returns for 0x50xxxxxx guids, so a player target structurally
cannot observe A1, A2, or R3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:00:10 +02:00

588 lines
23 KiB
C#

using AcDream.App.Audio;
using AcDream.App.Combat;
using AcDream.App.Composition;
using AcDream.App.Diagnostics;
using AcDream.App.Input;
using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Sky;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Rendering.Wb;
using AcDream.App.Settings;
using AcDream.App.Spells;
using AcDream.App.Streaming;
using AcDream.App.UI;
using AcDream.App.World;
using AcDream.Content;
using AcDream.Core.Audio;
using AcDream.Core.Physics;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session;
using AcDream.UI.Abstractions.Input;
using DatReaderWriter;
using Silk.NET.Input;
namespace AcDream.App.Rendering;
internal enum GameWindowLifetimeStatus
{
Active,
RetryableIncomplete,
Complete,
CompleteWithCleanupFailures,
AbandonedIncomplete,
}
internal sealed record GameWindowLifetimeReport(
GameWindowLifetimeStatus Status,
string? BlockedStage,
IReadOnlyList<ResourceShutdownCleanupFailure> CleanupFailures,
Exception? Error)
{
public bool IsTerminal => Status is
GameWindowLifetimeStatus.Complete
or GameWindowLifetimeStatus.CompleteWithCleanupFailures
or GameWindowLifetimeStatus.AbandonedIncomplete;
}
internal sealed record IngressShutdownRoots(
HostQuiescenceGate HostQuiescence,
LiveCombatModeCommandSlot CombatCommands,
RuntimeDiagnosticCommandSlot DiagnosticCommands,
RetainedUiGameplayBinding? RetainedGameplay,
GameplayInputActionRouter? GameplayActions,
CameraPointerInputController? CameraPointer,
InputDispatcher? Dispatcher,
SilkMouseSource? MouseSource,
SilkKeyboardSource? KeyboardSource,
RetailUiRuntimeLease RetailUi,
// Keeps failed physical UI bindings alive through native-window release.
UiHost? RetainedUiHost,
GameRuntime Runtime,
RuntimeSettingsController Settings,
DispatcherMovementInputSource MovementInput,
DispatcherCameraInputSource CameraInput,
SilkWindowCallbackBinding? WindowCallbacks);
internal sealed record FrameShutdownRoots(
IDisposable? FrameGraphPublication,
FrameRootRuntimeBindings? FrameBindings,
SessionPlayerRuntimeBindings? SessionBindings,
InteractionUiLateBindings? InteractionBindings);
internal sealed record LiveShutdownRoots(
CameraPointerInputController? CameraPointer,
RetailUiRuntimeLease RetailUi,
MagicRuntime? Magic,
ItemInteractionController? ItemInteraction,
ExternalContainerLifecycleController? ExternalContainers,
LandblockStreamer? Streamer,
EquippedChildRenderController? EquippedChildren,
LiveEntityRuntime? LiveEntities,
GameRuntime Runtime,
IDisposable RuntimeHostLease,
RenderSceneShadowRuntime? RenderSceneShadow,
LivePresentationRuntimeBindings? PresentationBindings,
DeferredEntityEffectAdvanceSource EffectAdvance,
EntityEffectController? EntityEffects,
AnimationHookRegistrationSet? HookRegistrations,
LiveEntityLightController? LiveLights,
LiveEntityPresentationController? LivePresentation,
AnimationHookFrameQueue? AnimationHookFrames,
EntityEffectPoseRegistry EffectPoses,
OpenAlAudioEngine? Audio);
internal sealed record RenderShutdownRoots(
GpuFrameFlightController? FrameFlights,
IGpuDevice? GpuDevice,
LocalPlayerTeleportController? LocalTeleport,
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
PaperdollViewportRenderer? Paperdoll,
CreatureAppraisalViewportRenderer? CreatureAppraisal,
WbDrawDispatcher? DrawDispatcher,
EnvCellRenderer? EnvironmentCells,
PortalDepthMaskRenderer? PortalDepthMask,
ClipFrame? ClipFrame,
SkyRenderer? Sky,
ParticleRenderer? Particles,
TextureCache? Textures,
WbMeshAdapter? MeshAdapter,
TerrainModernRenderer? Terrain,
SceneLightingUboBinding? SceneLighting,
DebugLineRenderer? DebugLines,
TextRenderer? TextRenderer,
BitmapFont? DebugFont,
DisplayFramePacingController FramePacing,
FrameProfiler FrameProfiler,
GameRenderResourceLifetime DedicatedResources,
ResourceConstructionCleanupLedger ConstructionCleanup);
internal sealed record PlatformShutdownRoots(
IDatReaderWriter? Dats,
IPreparedAssetSource? PreparedAssets,
IInputContext? Input,
GameWindowGraphics? Graphics);
internal sealed record GameWindowShutdownRoots(
IngressShutdownRoots Ingress,
FrameShutdownRoots Frame,
LiveShutdownRoots Live,
RenderShutdownRoots Render,
PlatformShutdownRoots Platform);
/// <summary>
/// Sole process-window lifetime owner. Runtime code receives no service lookup
/// through this type; it retains only teardown roots and the immutable terminal
/// report.
/// </summary>
internal sealed class GameWindowLifetime
{
private readonly Func<ResourceShutdownTransaction>? _injectedTransactionFactory;
private GameWindowShutdownRoots? _roots;
private ResourceShutdownTransaction? _transaction;
private IDisposable? _nativeWindow;
private bool _shutdownRootsPublished;
private bool _nativeReleaseAttempted;
private bool _completing;
private GameWindowLifetimeReport _report = new(
GameWindowLifetimeStatus.Active,
null,
[],
null);
public GameWindowLifetime()
{
}
internal GameWindowLifetime(Func<ResourceShutdownTransaction> transactionFactory)
{
_injectedTransactionFactory = transactionFactory
?? throw new ArgumentNullException(nameof(transactionFactory));
}
public GameWindowLifetimeReport Report => _report;
public bool HasShutdownRoots =>
_shutdownRootsPublished || _injectedTransactionFactory is not null;
internal bool RetainsShutdownGraph => _roots is not null || _transaction is not null;
public void PublishNativeWindow(IDisposable nativeWindow)
{
ArgumentNullException.ThrowIfNull(nativeWindow);
if (_nativeReleaseAttempted || _nativeWindow is not null)
throw new InvalidOperationException("A native window is already lifetime-owned.");
_nativeWindow = nativeWindow;
}
public void PublishShutdownRoots(GameWindowShutdownRoots roots)
{
ArgumentNullException.ThrowIfNull(roots);
if (_transaction is not null || _report.Status != GameWindowLifetimeStatus.Active)
throw new InvalidOperationException("Shutdown roots cannot change after completion starts.");
if (_roots is not null && !ReferenceEquals(_roots, roots))
throw new InvalidOperationException("Shutdown roots are already published.");
_roots = roots;
_shutdownRootsPublished = true;
}
public GameWindowLifetimeReport TryComplete()
{
if (_report.IsTerminal || _completing)
return _report;
EnsureTransaction();
_completing = true;
try
{
try
{
_transaction!.CompleteOrThrow();
}
catch (Exception error)
{
_report = new GameWindowLifetimeReport(
GameWindowLifetimeStatus.RetryableIncomplete,
_transaction!.CurrentStageName,
_transaction.CleanupFailures,
error);
return _report;
}
GameWindowLifetimeStatus status = _transaction!.CleanupFailures.Count == 0
? GameWindowLifetimeStatus.Complete
: GameWindowLifetimeStatus.CompleteWithCleanupFailures;
_report = new GameWindowLifetimeReport(
status,
null,
_transaction.CleanupFailures,
null);
return _report;
}
finally
{
_completing = false;
}
}
public GameWindowLifetimeReport CompleteAndReleaseNativeWindow()
{
if (_completing)
return _report;
if (!_report.IsTerminal)
TryComplete();
if (_report.Status == GameWindowLifetimeStatus.RetryableIncomplete)
{
AbandonRetainedRootsAfterTerminalFailure();
Exception? nativeFailure = ReleaseNativeWindow();
Exception terminalError = nativeFailure is null
? _report.Error ?? new InvalidOperationException(
"Shutdown did not converge before native fallback.")
: new AggregateException(
"Shutdown and native fallback both failed.",
_report.Error ?? new InvalidOperationException(
"Shutdown did not converge before native fallback."),
nativeFailure);
_report = new GameWindowLifetimeReport(
GameWindowLifetimeStatus.AbandonedIncomplete,
_report.BlockedStage,
_report.CleanupFailures,
terminalError);
return _report;
}
if (_report.IsTerminal && !_nativeReleaseAttempted)
{
Exception? nativeFailure = ReleaseNativeWindow();
if (nativeFailure is not null)
{
_report = new GameWindowLifetimeReport(
GameWindowLifetimeStatus.AbandonedIncomplete,
"native window",
_report.CleanupFailures,
nativeFailure);
}
else
{
ReleaseCompletedRoots();
}
}
return _report;
}
private void EnsureTransaction()
{
if (_transaction is not null)
return;
_transaction = _injectedTransactionFactory?.Invoke()
?? GameWindowShutdownManifest.Create(
_roots ?? throw new InvalidOperationException(
"Shutdown roots must publish before completion starts."));
}
private Exception? ReleaseNativeWindow()
{
if (_nativeReleaseAttempted)
return null;
_nativeReleaseAttempted = true;
try
{
_nativeWindow?.Dispose();
_nativeWindow = null;
return null;
}
catch (Exception error)
{
return error;
}
}
private void ReleaseCompletedRoots()
{
// The native event source is now gone, so even reportable physical-edge
// failures can no longer call back. Terminal failures deliberately do
// not take this path: their exact owner graph stays rooted for safety
// and diagnosis.
_transaction = null;
_roots = null;
}
private void AbandonRetainedRootsAfterTerminalFailure()
{
RetailUiRuntimeLease? retainedUi = _roots?.Live.RetailUi;
if (retainedUi?.HasDisposalFailure == true)
retainedUi.AbandonAfterTerminalFailure();
}
}
internal static class GameWindowShutdownManifest
{
public static ResourceShutdownTransaction Create(GameWindowShutdownRoots roots)
{
ArgumentNullException.ThrowIfNull(roots);
IngressShutdownRoots ingress = roots.Ingress;
FrameShutdownRoots frame = roots.Frame;
LiveShutdownRoots live = roots.Live;
RenderShutdownRoots render = roots.Render;
PlatformShutdownRoots platform = roots.Platform;
return new ResourceShutdownTransaction(
new ResourceShutdownStage("host and session barriers",
[
Hard("host quiescence", ingress.HostQuiescence.StopAccepting),
Hard("combat command slot", ingress.CombatCommands.Deactivate),
Hard("diagnostic command slot", ingress.DiagnosticCommands.Deactivate),
Hard("retained gameplay", () => ingress.RetainedGameplay?.Deactivate()),
Hard("gameplay actions", () => ingress.GameplayActions?.Deactivate()),
Hard("camera pointer", () => ingress.CameraPointer?.Deactivate()),
Hard("dispatcher", () => ingress.Dispatcher?.Deactivate()),
Hard("mouse source", () => ingress.MouseSource?.Deactivate()),
Hard("keyboard source", () => ingress.KeyboardSource?.Deactivate()),
Hard("retained UI input", ingress.RetailUi.QuiesceInput),
Hard("game runtime session", ingress.Runtime.StopSession),
]),
new ResourceShutdownStage("physical ingress cleanup",
[
Soft("settings view model", () => ingress.Settings.UnbindViewModel()),
Soft("retained gameplay", () => DisposeRetainedGameplay(ingress.RetainedGameplay)),
Soft("gameplay actions", () => DisposeGameplayActions(ingress.GameplayActions)),
Soft("retained UI input", ingress.RetailUi.DeactivateInput),
Soft("camera pointer", () => DisposeCameraPointer(ingress.CameraPointer)),
Soft("dispatcher", () => DisposeDispatcher(ingress)),
Soft("mouse source", () => DisposeMouseSource(ingress.MouseSource)),
Soft("keyboard source", () => DisposeKeyboardSource(ingress.KeyboardSource)),
Soft("native window callbacks", () => DisposeWindowCallbacks(ingress.WindowCallbacks)),
]),
new ResourceShutdownStage("frame borrowers",
[
Hard("world frame composition", () => frame.FrameGraphPublication?.Dispose()),
Hard("frame-root bindings", () => frame.FrameBindings?.Dispose()),
Hard("session/player bindings", () => frame.SessionBindings?.Dispose()),
]),
new ResourceShutdownStage("session dependents",
[
Hard("interaction/UI late bindings", () => frame.InteractionBindings?.Dispose()),
Hard("mouse capture", () => live.CameraPointer?.ReleaseMouseLookAfterSessionRetirement()),
Hard("retail UI", () => DisposeRetailUi(live.RetailUi)),
Hard("magic runtime", () => live.Magic?.Dispose()),
Hard("item interaction", () => live.ItemInteraction?.Dispose()),
Hard("external containers", () => live.ExternalContainers?.Dispose()),
Hard("streamer", () => live.Streamer?.Dispose()),
Hard("equipped children", () => live.EquippedChildren?.Dispose()),
]),
new ResourceShutdownStage("live entities",
[
Hard("live entity runtime", () => live.LiveEntities?.Clear()),
Hard(
"shadow render scene",
() => live.RenderSceneShadow?.Dispose()),
]),
new ResourceShutdownStage("effect dispatch edges",
[
Hard("live-presentation bindings", () => live.PresentationBindings?.Dispose()),
Hard("entity-effect advance source", () =>
{
if (live.EntityEffects is { } effects)
live.EffectAdvance.Unbind(effects);
live.EffectAdvance.Deactivate();
}),
Hard("animation-hook registrations", () => DisposeHookRegistrations(live.HookRegistrations)),
]),
new ResourceShutdownStage("live entity dependents",
[
Hard("live lights", () => live.LiveLights?.Dispose()),
Hard("live presentation", () => live.LivePresentation?.Dispose()),
Hard("effect network state", () =>
{
live.EntityEffects?.ClearNetworkState();
live.AnimationHookFrames?.Clear();
live.EffectPoses.Clear();
}),
Hard("audio", () => DisposeAudio(live.Audio)),
]),
new ResourceShutdownStage("submitted GPU work",
[
Hard("frame flight drain", () => render.FrameFlights?.WaitForSubmittedWork()),
]),
new ResourceShutdownStage("render frontends",
[
Hard("portal tunnel", () =>
{
render.LocalTeleport?.Dispose();
render.PortalTunnelFallback.ReleaseFallback();
}),
Hard("paperdoll viewport", () => render.Paperdoll?.Dispose()),
Hard(
"creature appraisal viewport",
() => render.CreatureAppraisal?.Dispose()),
Hard("mesh draw dispatcher", () => render.DrawDispatcher?.Dispose()),
Hard("environment cells", () => render.EnvironmentCells?.Dispose()),
Hard("portal depth mask", () => render.PortalDepthMask?.Dispose()),
Hard("clip frame", () => render.ClipFrame?.Dispose()),
Hard("sky", () => render.Sky?.Dispose()),
Hard("particles", () => render.Particles?.Dispose()),
]),
new ResourceShutdownStage("game runtime root",
[
Hard("graphical runtime host lease", live.RuntimeHostLease.Dispose),
Hard("game runtime", () => DisposeGameRuntime(live.Runtime)),
]),
new ResourceShutdownStage("shared texture owners",
[
Hard("texture cache", () => render.Textures?.Dispose()),
]),
new ResourceShutdownStage("mesh adapter",
[
Hard("WB mesh adapter", () => render.MeshAdapter?.Dispose()),
]),
new ResourceShutdownStage("remaining render owners",
[
Hard("terrain", () => render.Terrain?.Dispose()),
Hard("scene lighting", () => render.SceneLighting?.Dispose()),
Hard("debug lines", () => render.DebugLines?.Dispose()),
Hard("text renderer", () => render.TextRenderer?.Dispose()),
Hard("debug font", () => render.DebugFont?.Dispose()),
Hard("frame pacing", render.FramePacing.Dispose),
Hard("frame profiler", render.FrameProfiler.Dispose),
Hard("GPU device (RHI)", () => render.GpuDevice?.Dispose()),
]),
new ResourceShutdownStage("dedicated render resources",
[
Hard("terrain atlas", render.DedicatedResources.ReleaseTerrainAtlas),
]),
new ResourceShutdownStage("failed render construction cleanup",
[
Hard("resource construction ledger", render.ConstructionCleanup.Dispose),
]),
new ResourceShutdownStage("frame flight owner",
[
Hard("frame flights", () => render.FrameFlights?.Dispose()),
]),
new ResourceShutdownStage("content mappings",
[
Hard("prepared asset source", () => platform.PreparedAssets?.Dispose()),
Hard("DAT collection", () => platform.Dats?.Dispose()),
]),
new ResourceShutdownStage("input context",
[
Hard("input context", () => platform.Input?.Dispose()),
]),
new ResourceShutdownStage("graphics API context",
[
Hard("graphics API", () => platform.Graphics?.Dispose()),
]));
}
private static ResourceShutdownOperation Hard(string name, Action action) =>
new(name, action);
private static ResourceShutdownOperation Soft(string name, Action action) =>
new(name, action, ResourceShutdownOperationPolicy.ReportAndContinue);
private static void DisposeGameRuntime(GameRuntime runtime)
{
runtime.Dispose();
GameRuntimeOwnershipSnapshot ownership = runtime.CaptureOwnership();
if (!ownership.IsConverged)
{
throw new InvalidOperationException(
"The canonical game runtime did not converge after disposal.");
}
}
private static void DisposeRetainedGameplay(RetainedUiGameplayBinding? binding)
{
if (binding is null)
return;
binding.Dispose();
if (!binding.IsDisposalComplete)
throw new InvalidOperationException("Retained gameplay callback removal remains pending.");
}
private static void DisposeGameplayActions(GameplayInputActionRouter? actions)
{
if (actions is null)
return;
actions.Dispose();
if (!actions.IsDisposalComplete)
throw new InvalidOperationException("Gameplay action callback removal remains pending.");
}
private static void DisposeCameraPointer(CameraPointerInputController? pointer)
{
if (pointer is null)
return;
pointer.Dispose();
if (!pointer.IsDisposalComplete)
throw new InvalidOperationException("Camera pointer callback removal remains pending.");
}
private static void DisposeDispatcher(IngressShutdownRoots roots)
{
InputDispatcher? dispatcher = roots.Dispatcher;
if (dispatcher is null)
return;
roots.MovementInput.Unbind(dispatcher);
roots.CameraInput.Unbind(dispatcher);
dispatcher.Dispose();
if (!dispatcher.IsDisposalComplete)
throw new InvalidOperationException("Input dispatcher source removal remains pending.");
}
private static void DisposeMouseSource(SilkMouseSource? source)
{
if (source is null)
return;
source.Dispose();
if (!source.IsDisposalComplete)
throw new InvalidOperationException("Mouse source callback removal remains pending.");
}
private static void DisposeKeyboardSource(SilkKeyboardSource? source)
{
if (source is null)
return;
source.Dispose();
if (!source.IsDisposalComplete)
throw new InvalidOperationException("Keyboard source callback removal remains pending.");
}
private static void DisposeWindowCallbacks(SilkWindowCallbackBinding? binding)
{
if (binding is null)
return;
binding.Dispose();
if (!binding.IsDisposalComplete)
throw new NativeWindowCallbackCleanupDeferredException();
}
private static void DisposeRetailUi(RetailUiRuntimeLease lease)
{
lease.Dispose();
if (!lease.IsDisposalComplete)
throw new InvalidOperationException("The retained UI ownership lease did not complete disposal.");
}
private static void DisposeHookRegistrations(AnimationHookRegistrationSet? registrations)
{
if (registrations is null)
return;
registrations.Dispose();
if (!registrations.IsCleanupComplete)
throw new InvalidOperationException("Animation-hook registration cleanup remains pending.");
}
private static void DisposeAudio(OpenAlAudioEngine? engine)
{
if (engine is null)
return;
engine.Dispose();
if (!engine.IsDisposalComplete)
throw new InvalidOperationException("OpenAL native-resource cleanup remains pending.");
}
}