The user watched far terrain visibly assemble after portal space exits.
The reveal gate was NOT missing a hold — Slice E's hold mechanism is
correct and already in place. The hold was measuring the wrong domain:
it opened at a hardcoded 3x3 landblock neighbourhood (~192 m) while the
visible world extends to the fog end (~2,189 m at the shipped High
preset, inside a 2,304 m Far window). An 11.4:1 ratio.
Retail's equivalent ratio is 1:1 BY CONSTRUCTION. `LScape` owns one
`mid_width x mid_width` array of `CLandBlock*` (`LScape::SetMidRadius`
@0x00504C00, `LScape::update_block` @0x005063A0), `mid_radius` is
assigned directly from the user's `Render.LandscapeDrawDistance`
preference (`SmartBox::SetRegion` @0x004531F0; values
`Render_LandscapeDrawDistance_Values` @0x007CA988 = {3,5,8,11,15,25},
default 8 — both byte-verified against the PDB-paired 2013 binary), and
that same square is simultaneously the prefetched set
(`LScape::PreFetchCells` @0x00505660), the drawn set (`block_draw_list`
over the same array), and the set the simulation blocks on
(`CellManager::blocking_for_cells`). There is no retail configuration in
which the client streams farther than it gates, because there is only
one number.
So the fix derives rather than duplicates. Four coupled parts, which is
why this is one commit and not four — D1 without D2 hangs the client and
D2 without D1 is dead code:
D1 `WorldRevealReadinessBarrier` takes a live `Func<StreamingRevealWindow>`
and stops being static: outdoor requires `FarRadius`, indoor still 0
(retail's `CEnvCell::PreFetchCells` @0x0052D1E0 arm). Read per
evaluation, never captured — the radii are runtime mutable through
Settings, and retail's answer to a mid-hold radius change is to reset,
re-radius, and re-arm the blocking prefetch at the NEW value
(`SmartBox::set_mid_radius` @0x00453180). `OutdoorNeighborhoodRadius`
is deleted; there is no constant left to drift.
D2 `StreamingController.IsRenderNeighborhoodResident` becomes tiered,
because acdream's loaded landscape is: inside `NearRadius`,
`IsNearTier && IsRenderReady`; out to `FarRadius`, `IsRenderReady` only.
Without this the fix cannot work at all — nothing outside the Near ring
is ever promoted, so any radius above `NearRadius` was unsatisfiable and
would have held the reveal forever. Proof obligation P1 (a Far-tier
landblock genuinely satisfies `IsRenderReady`) is now a test driven
through the real `PublicationKind.Far` pipeline against a real
`LandblockSpawnAdapter`, not an inference.
D7 `RuntimeWorldTransitState.AcknowledgeDestinationReadiness` re-derived
`indoor ? 0 : 1` and failed `invalid-readiness-shape` on any other
value, so changing the radius alone would have looked like "the fix
hangs the client". It is now a SHAPE invariant (`indoor => 0`,
`outdoor => >= 1`). Runtime does not own the graphical host's streaming
configuration and must not learn it; plumbing App radii into Runtime to
preserve the strict equality is exactly the assert-a-mechanism-that-does-
not-exist failure C5b was built to stop. Both non-graphical producers
keep emitting their centre-ring token and stay legal, annotated in place.
D6 `PhysicsEngine.IsNeighborhoodTerrainResident` rebuilt a full-map
`HashSet` on every call, every frame of every hold. At radius 1 that was
invisible; at radius 12 (625 ring members) it violates Slice I1's
0 B/resolve standard. Now an engine-owned scratch set, cleared in place;
measured at 0 bytes over 1,000 warmed radius-12 queries.
Also: the destination reservation opens at exactly the gate's radius and
reopens on the same generation when the radius changes mid-hold (retail
has one square for both, and no concept of prioritising an inner ring
differently). Composite warmup deliberately stays `NearRadius`-scoped —
the composite domain is entity-scoped and Far builds carry no entities,
so widening it would walk the outer window to warm nothing.
`ACDREAM_PROBE_REVEAL_RADIUS` is a measurement probe in a diagnostic
owner (CLAUDE.md rule 5) so the connected route can be run A/B on one
binary; it is NOT a user-facing prefetch knob, since a low setting would
reintroduce the decoupling this slice exists to close.
Register: AD-2 amended with the derived window, the two-tier split, and
the four new retail anchors. AP-149 FILED for the residual this does not
close — the outer ring accepts terrain-only publication where retail
requires LandBlockInfo and every building EnvCell, so a distant building
can still pop in at Far-ring distances. Do not let a later closeout
claim parity.
Docs: `ACDREAM_STREAM_RADIUS`'s CLAUDE.md description was wrong on every
clause (the default is unset, not 2; it forces `NearRadius`; it is
silently discarded by any Settings save) — corrected, since that is the
file every session reads. `reference_two_tier_streaming.md` corrected in
four ways, including "Far tier = terrain only": Far also publishes
terrain COLLISION, which is precisely what makes this fix viable.
#280's issue text had the right conclusion from a wrong premise (it
names a view-distance setting acdream does not have) — corrected, and
the missing Viewing Distance option filed separately as #326, with #327
(DDD progress readout) and #328 (hardcoded 5000 f far plane vs retail's
byte-verified 4000) filed alongside.
Expect LONGER holds and the "In Portal Space - Please Wait..." cue on
recalls MORE often. That is convergence toward retail, not away from it:
retail emits the byte-identical string for the whole duration of a
blocked prefetch and polls at 5 s intervals. The failure condition is
non-convergence, not duration.
Gates: Release build 0 errors. Complete suite 11,178 passed / 4 skipped
/ 0 failed, against a re-measured 11,142 / 4 / 0 baseline at 9ee9c1a1 —
+36, reconciled exactly as 36 new tests (App +23, Runtime +10, Core +3),
zero deleted, zero newly skipped. Nine discriminating tests
sabotage-verified in both directions. The connected/visual gate is
batched into C5's matrix; its recipe, its three positive artifacts, and
its required recall leg are written into the campaign plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
617 lines
20 KiB
C#
617 lines
20 KiB
C#
using AcDream.Runtime;
|
|
using AcDream.Runtime.World;
|
|
|
|
namespace AcDream.App.Streaming;
|
|
|
|
/// <summary>
|
|
/// Generation-scoped destination-work seam. The reveal coordinator is the
|
|
/// graphical host for Runtime's canonical login/portal lifetime; the
|
|
/// streaming scheduler only reserves capacity for the exact generation and
|
|
/// destination it is given.
|
|
/// </summary>
|
|
internal interface IWorldRevealStreamingScheduler
|
|
{
|
|
void BeginDestinationReservation(
|
|
long revealGeneration,
|
|
uint destinationCell,
|
|
int requiredRenderRadius);
|
|
|
|
void EndDestinationReservation(long revealGeneration);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generation-scoped renderer-resource profile used only while the normal
|
|
/// world viewport is withheld for a login or portal destination.
|
|
/// </summary>
|
|
internal interface IWorldRevealRenderResourceScheduler
|
|
{
|
|
void BeginDestinationReveal(long revealGeneration);
|
|
void EndDestinationReveal(long revealGeneration);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Graphical-host adapter for one canonical Runtime login/portal reveal
|
|
/// lifetime. App owns render/collision readiness and destination resource
|
|
/// reservations; Runtime owns generation, destination, materialization,
|
|
/// simulation availability, completion, and cancellation.
|
|
/// </summary>
|
|
internal sealed class WorldRevealCoordinator
|
|
{
|
|
private sealed class HostProjection(
|
|
RuntimeWorldHostProjectionToken token,
|
|
int requiredRenderRadius,
|
|
in WorldGenerationQuiescenceEdge quiescenceEdge)
|
|
{
|
|
public RuntimeWorldHostProjectionToken Token { get; } = token;
|
|
|
|
/// <summary>
|
|
/// The radius this projection's streaming reservation is currently
|
|
/// open at. Mutable because the quality preset can be changed
|
|
/// mid-hold; retail re-arms the blocking prefetch at the new radius
|
|
/// (<c>SmartBox::set_mid_radius</c> @0x00453180) rather than finishing
|
|
/// the old hold at the old one.
|
|
/// </summary>
|
|
public int RequiredRenderRadius { get; set; } = requiredRenderRadius;
|
|
public WorldGenerationQuiescenceEdge QuiescenceEdge { get; } =
|
|
quiescenceEdge;
|
|
public bool QuiescenceCommitted { get; set; }
|
|
public bool StreamingRegistered { get; set; }
|
|
public bool RenderResourcesRegistered { get; set; }
|
|
public bool SimulationReleaseProjected { get; set; }
|
|
public bool StreamingReleased { get; set; }
|
|
public bool RenderResourcesReleased { get; set; }
|
|
}
|
|
|
|
private readonly RuntimeWorldTransitState _transit;
|
|
private readonly WorldRevealReadinessBarrier _readiness;
|
|
private readonly WorldGenerationQuiescence? _quiescence;
|
|
private readonly IWorldRevealStreamingScheduler? _streaming;
|
|
private readonly IWorldRevealRenderResourceScheduler? _renderResources;
|
|
private readonly List<HostProjection> _hostProjections = [];
|
|
private bool _hostRetryActive;
|
|
private bool _hostRetryRequested;
|
|
|
|
public WorldRevealCoordinator(
|
|
RuntimeWorldTransitState transit,
|
|
Func<StreamingRevealWindow> revealWindow,
|
|
Func<uint, int, int, bool> isRenderNeighborhoodReady,
|
|
Func<uint, bool> isSpawnCellReady,
|
|
Func<uint, int, bool> isTerrainNeighborhoodReady,
|
|
Func<bool> areCompositeTexturesReady,
|
|
Action<uint, int> prepareCompositeTextures,
|
|
Action invalidateCompositeTextures,
|
|
Func<uint, bool> isSpawnClaimUnhydratable,
|
|
WorldGenerationQuiescence? quiescence = null,
|
|
IWorldRevealStreamingScheduler? streaming = null,
|
|
IWorldRevealRenderResourceScheduler? renderResources = null)
|
|
{
|
|
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
|
|
_readiness = new WorldRevealReadinessBarrier(
|
|
revealWindow,
|
|
isRenderNeighborhoodReady,
|
|
isSpawnCellReady,
|
|
isTerrainNeighborhoodReady,
|
|
areCompositeTexturesReady,
|
|
prepareCompositeTextures,
|
|
invalidateCompositeTextures,
|
|
isSpawnClaimUnhydratable);
|
|
_quiescence = quiescence;
|
|
_streaming = streaming;
|
|
_renderResources = renderResources;
|
|
}
|
|
|
|
public RuntimePortalSnapshot Snapshot => _transit.Snapshot;
|
|
public int PortalMaterializationCount =>
|
|
_transit.Snapshot.PortalMaterializationCount;
|
|
public bool WaitCueShown => _transit.Snapshot.WaitCueShown;
|
|
public RuntimeWorldTransitOwnershipSnapshot Ownership =>
|
|
_transit.Ownership;
|
|
|
|
public long BeginLogin(uint destinationCell)
|
|
{
|
|
if (destinationCell == 0u)
|
|
throw new ArgumentOutOfRangeException(nameof(destinationCell));
|
|
|
|
WithdrawHostForReplacement();
|
|
WorldGenerationQuiescenceEdge quiescenceEdge =
|
|
_quiescence?.CaptureBegin() ?? default;
|
|
_readiness.Begin();
|
|
long generation = _transit.BeginLoginReveal(destinationCell);
|
|
BeginHostLifetime(
|
|
generation,
|
|
destinationCell,
|
|
quiescenceEdge);
|
|
return generation;
|
|
}
|
|
|
|
public bool TryBeginPortal(
|
|
ushort teleportSequence,
|
|
uint destinationCell,
|
|
out long generation)
|
|
{
|
|
if (destinationCell == 0u)
|
|
throw new ArgumentOutOfRangeException(nameof(destinationCell));
|
|
if (!_transit.CanBeginPortalReveal(
|
|
teleportSequence,
|
|
destinationCell))
|
|
{
|
|
generation = 0;
|
|
return false;
|
|
}
|
|
|
|
WithdrawHostForReplacement();
|
|
WorldGenerationQuiescenceEdge quiescenceEdge =
|
|
_quiescence?.CaptureBegin() ?? default;
|
|
if (!_transit.TryBeginPortalReveal(
|
|
teleportSequence,
|
|
destinationCell,
|
|
out generation))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_readiness.Begin();
|
|
BeginHostLifetime(
|
|
generation,
|
|
destinationCell,
|
|
quiescenceEdge);
|
|
return true;
|
|
}
|
|
|
|
private void BeginHostLifetime(
|
|
long generation,
|
|
uint destinationCell,
|
|
in WorldGenerationQuiescenceEdge quiescenceEdge)
|
|
{
|
|
if (!_transit.TryRegisterHostProjection(
|
|
generation,
|
|
destinationCell,
|
|
out RuntimeWorldHostProjectionToken token))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Runtime rejected the exact reveal host projection.");
|
|
}
|
|
|
|
_hostProjections.Add(new HostProjection(
|
|
token,
|
|
_readiness.RequiredRenderRadius(destinationCell),
|
|
quiescenceEdge));
|
|
RetryPendingHostWork();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advances render-thread preparation and publishes the resulting
|
|
/// canonical readiness snapshot to lifecycle diagnostics.
|
|
/// </summary>
|
|
public WorldRevealReadinessSnapshot PrepareAndEvaluate(uint destinationCell)
|
|
{
|
|
_readiness.Prepare(destinationCell);
|
|
return Evaluate(destinationCell);
|
|
}
|
|
|
|
public WorldRevealReadinessSnapshot Evaluate(uint destinationCell)
|
|
{
|
|
RetryPendingHostWork();
|
|
WorldRevealReadinessSnapshot snapshot = _readiness.Evaluate(destinationCell);
|
|
ReconcileDestinationReservationRadius(snapshot);
|
|
RuntimePortalSnapshot portal = _transit.Snapshot;
|
|
if (portal.Generation != 0)
|
|
{
|
|
_transit.AcknowledgeDestinationReadiness(
|
|
new RuntimeDestinationReadiness(
|
|
portal.Generation,
|
|
snapshot.DestinationCell,
|
|
snapshot.IsIndoor,
|
|
snapshot.IsUnhydratable,
|
|
snapshot.RequiredRenderRadius,
|
|
snapshot.IsRenderNeighborhoodReady,
|
|
snapshot.AreCompositeTexturesReady,
|
|
snapshot.IsCollisionReady));
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Commits the accepted destination and resumes its hidden world
|
|
/// simulation while the private portal viewport remains in front.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Retail <c>SmartBox::UseTime @ 0x00455410</c> resumes
|
|
/// <c>CObjectMaint</c>/<c>CPhysics</c> before
|
|
/// <c>gmSmartBoxUI::EndTeleportAnimation @ 0x004D65A0</c> enters
|
|
/// <c>TAS_TUNNEL_CONTINUE</c>. The normal world viewport is not restored
|
|
/// until the later <c>TUNNEL_FADE_OUT -> WORLD_FADE_IN</c> edge.
|
|
/// </remarks>
|
|
public bool CanPlacePortalDestination(
|
|
long generation,
|
|
ushort teleportSequence,
|
|
uint destinationCell) =>
|
|
_transit.CanPlacePortalDestination(
|
|
generation,
|
|
teleportSequence,
|
|
destinationCell);
|
|
|
|
public bool ObserveMaterialized(
|
|
long generation,
|
|
ushort teleportSequence,
|
|
uint destinationCell)
|
|
{
|
|
bool acknowledged = _transit.AcknowledgePortalMaterialized(
|
|
generation,
|
|
teleportSequence,
|
|
destinationCell);
|
|
RetryPendingHostWork();
|
|
return acknowledged;
|
|
}
|
|
|
|
public void ObserveWorldViewportVisible()
|
|
{
|
|
RetryPendingHostWork();
|
|
long generation = _transit.Snapshot.Generation;
|
|
if (generation != 0)
|
|
_transit.AcknowledgeWorldViewportVisible(generation);
|
|
}
|
|
|
|
public bool ObserveWait(TimeSpan elapsed)
|
|
{
|
|
long generation = _transit.Snapshot.Generation;
|
|
return generation != 0
|
|
&& _transit.ObserveWait(generation, elapsed);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases the destination streaming/resource reservation at retail's
|
|
/// TunnelFadeOut -> WorldFadeIn viewport swap without completing the
|
|
/// one-second WorldFadeIn/LoginComplete tail. World simulation already
|
|
/// resumed when the accepted destination materialized.
|
|
/// </summary>
|
|
public void RevealWorldViewport()
|
|
{
|
|
HostProjection? host = FindCurrentHostProjection();
|
|
if (host is null)
|
|
return;
|
|
|
|
_transit.RequireDestinationReservationRelease(host.Token);
|
|
RetryPendingHostWork();
|
|
}
|
|
|
|
public void Complete()
|
|
{
|
|
long generation = _transit.Snapshot.Generation;
|
|
if (generation == 0)
|
|
return;
|
|
|
|
_transit.Complete(generation);
|
|
RetryPendingHostWork();
|
|
}
|
|
|
|
public void Cancel()
|
|
{
|
|
long generation = _transit.Snapshot.Generation;
|
|
if (generation == 0)
|
|
return;
|
|
|
|
_transit.Cancel(generation);
|
|
RetryPendingHostWork();
|
|
}
|
|
|
|
public void ResetSession()
|
|
{
|
|
ResetHostSession();
|
|
_transit.ResetSession();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retires only App streaming/presentation acknowledgements. The shared
|
|
/// Runtime generation-reset transaction clears canonical transit state
|
|
/// after this host boundary has converged.
|
|
/// </summary>
|
|
internal void ResetHostSession()
|
|
{
|
|
long generation = _transit.Snapshot.Generation;
|
|
if (generation != 0)
|
|
_transit.Cancel(generation);
|
|
RetryPendingHostWork();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resumes the exact unfinished graphical-host suffix. Completed callback
|
|
/// stages never replay; a thrown callback leaves its Runtime
|
|
/// acknowledgement pending for the next call.
|
|
/// </summary>
|
|
internal void RetryPendingHostWork()
|
|
{
|
|
_hostRetryRequested = true;
|
|
if (_hostRetryActive)
|
|
return;
|
|
|
|
_hostRetryActive = true;
|
|
try
|
|
{
|
|
do
|
|
{
|
|
_hostRetryRequested = false;
|
|
int index = 0;
|
|
while (index < _hostProjections.Count)
|
|
{
|
|
HostProjection host = _hostProjections[index];
|
|
DrainHostProjection(host);
|
|
if (index < _hostProjections.Count
|
|
&& ReferenceEquals(
|
|
_hostProjections[index],
|
|
host))
|
|
{
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
while (_hostRetryRequested);
|
|
}
|
|
finally
|
|
{
|
|
_hostRetryActive = false;
|
|
}
|
|
}
|
|
|
|
private void DrainHostProjection(HostProjection host)
|
|
{
|
|
if (!TryGetHostProjection(host, out var projection))
|
|
return;
|
|
|
|
RuntimeWorldHostAcknowledgementStage pending =
|
|
projection.PendingAcknowledgements;
|
|
if ((pending & RuntimeWorldHostAcknowledgementStage
|
|
.ProjectionRegistered) != 0)
|
|
{
|
|
CompleteHostRegistration(host);
|
|
}
|
|
|
|
if (!TryGetHostProjection(host, out projection))
|
|
return;
|
|
pending = projection.PendingAcknowledgements;
|
|
if ((pending & RuntimeWorldHostAcknowledgementStage
|
|
.SimulationReleaseProjected) != 0)
|
|
{
|
|
CompleteSimulationRelease(host);
|
|
}
|
|
|
|
if (!TryGetHostProjection(host, out projection))
|
|
return;
|
|
pending = projection.PendingAcknowledgements;
|
|
if ((pending & RuntimeWorldHostAcknowledgementStage
|
|
.DestinationReservationReleased) != 0)
|
|
{
|
|
CompleteDestinationReservationRelease(host);
|
|
}
|
|
|
|
if (!TryGetHostProjection(host, out projection))
|
|
return;
|
|
if ((projection.PendingAcknowledgements
|
|
& RuntimeWorldHostAcknowledgementStage.TerminalProjected)
|
|
== 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Acknowledge(
|
|
host,
|
|
RuntimeWorldHostAcknowledgementStage.TerminalProjected);
|
|
RemoveHostProjection(host);
|
|
}
|
|
|
|
private void CompleteHostRegistration(HostProjection host)
|
|
{
|
|
if (!host.QuiescenceCommitted)
|
|
{
|
|
_quiescence?.CommitBegin(host.QuiescenceEdge);
|
|
host.QuiescenceCommitted = true;
|
|
}
|
|
if (!IsStagePending(
|
|
host,
|
|
RuntimeWorldHostAcknowledgementStage
|
|
.ProjectionRegistered))
|
|
{
|
|
return;
|
|
}
|
|
if (!host.StreamingRegistered)
|
|
{
|
|
_streaming?.BeginDestinationReservation(
|
|
host.Token.Generation,
|
|
host.Token.DestinationCell,
|
|
host.RequiredRenderRadius);
|
|
host.StreamingRegistered = true;
|
|
}
|
|
if (!IsStagePending(
|
|
host,
|
|
RuntimeWorldHostAcknowledgementStage
|
|
.ProjectionRegistered))
|
|
{
|
|
return;
|
|
}
|
|
if (!host.RenderResourcesRegistered)
|
|
{
|
|
_renderResources?.BeginDestinationReveal(
|
|
host.Token.Generation);
|
|
host.RenderResourcesRegistered = true;
|
|
}
|
|
if (!IsStagePending(
|
|
host,
|
|
RuntimeWorldHostAcknowledgementStage
|
|
.ProjectionRegistered))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Acknowledge(
|
|
host,
|
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered);
|
|
}
|
|
|
|
private void CompleteSimulationRelease(HostProjection host)
|
|
{
|
|
if (!host.SimulationReleaseProjected)
|
|
{
|
|
_quiescence?.ObserveReleased();
|
|
host.SimulationReleaseProjected = true;
|
|
}
|
|
if (!IsStagePending(
|
|
host,
|
|
RuntimeWorldHostAcknowledgementStage
|
|
.SimulationReleaseProjected))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Acknowledge(
|
|
host,
|
|
RuntimeWorldHostAcknowledgementStage
|
|
.SimulationReleaseProjected);
|
|
}
|
|
|
|
private void CompleteDestinationReservationRelease(HostProjection host)
|
|
{
|
|
if (!host.StreamingReleased)
|
|
{
|
|
if (host.StreamingRegistered)
|
|
{
|
|
_streaming?.EndDestinationReservation(
|
|
host.Token.Generation);
|
|
}
|
|
host.StreamingReleased = true;
|
|
}
|
|
if (!IsStagePending(
|
|
host,
|
|
RuntimeWorldHostAcknowledgementStage
|
|
.DestinationReservationReleased))
|
|
{
|
|
return;
|
|
}
|
|
if (!host.RenderResourcesReleased)
|
|
{
|
|
if (host.RenderResourcesRegistered)
|
|
{
|
|
_renderResources?.EndDestinationReveal(
|
|
host.Token.Generation);
|
|
}
|
|
host.RenderResourcesReleased = true;
|
|
}
|
|
if (!IsStagePending(
|
|
host,
|
|
RuntimeWorldHostAcknowledgementStage
|
|
.DestinationReservationReleased))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Acknowledge(
|
|
host,
|
|
RuntimeWorldHostAcknowledgementStage
|
|
.DestinationReservationReleased);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Keeps the open destination reservation on the same square the gate is
|
|
/// measuring after a mid-hold quality change. Retail's
|
|
/// <c>SmartBox::set_mid_radius</c> @0x00453180 resets the cell manager,
|
|
/// re-radiuses the landscape, and re-arms the blocking prefetch at the NEW
|
|
/// value; acdream's equivalent is to close and reopen the reservation on
|
|
/// the same reveal generation. <c>EndDestinationReservation</c> already
|
|
/// refuses a generation mismatch, so the reopen is generation-safe.
|
|
/// </summary>
|
|
private void ReconcileDestinationReservationRadius(
|
|
in WorldRevealReadinessSnapshot snapshot)
|
|
{
|
|
if (_streaming is null || !snapshot.HasDestination)
|
|
return;
|
|
|
|
HostProjection? host = FindCurrentHostProjection();
|
|
if (host is null
|
|
|| !host.StreamingRegistered
|
|
|| host.StreamingReleased
|
|
|| host.Token.DestinationCell != snapshot.DestinationCell
|
|
|| host.RequiredRenderRadius == snapshot.RequiredRenderRadius)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_streaming.EndDestinationReservation(host.Token.Generation);
|
|
_streaming.BeginDestinationReservation(
|
|
host.Token.Generation,
|
|
host.Token.DestinationCell,
|
|
snapshot.RequiredRenderRadius);
|
|
host.RequiredRenderRadius = snapshot.RequiredRenderRadius;
|
|
}
|
|
|
|
private void WithdrawHostForReplacement()
|
|
{
|
|
HostProjection? host = FindCurrentHostProjection();
|
|
if (host is null)
|
|
return;
|
|
|
|
if (!_transit.BeginHostProjectionSupersession(host.Token))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Runtime rejected reveal-host supersession.");
|
|
}
|
|
RetryPendingHostWork();
|
|
}
|
|
|
|
private HostProjection? FindCurrentHostProjection()
|
|
{
|
|
long generation = _transit.Snapshot.Generation;
|
|
for (int i = 0; i < _hostProjections.Count; i++)
|
|
{
|
|
HostProjection candidate = _hostProjections[i];
|
|
if (candidate.Token.Generation == generation)
|
|
return candidate;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private bool TryGetHostProjection(
|
|
HostProjection host,
|
|
out RuntimeWorldHostProjectionSnapshot projection)
|
|
{
|
|
if (_transit.TryGetHostProjection(host.Token, out projection))
|
|
return true;
|
|
|
|
RemoveHostProjection(host);
|
|
return false;
|
|
}
|
|
|
|
private bool IsStagePending(
|
|
HostProjection host,
|
|
RuntimeWorldHostAcknowledgementStage stage) =>
|
|
_transit.TryGetHostProjection(
|
|
host.Token,
|
|
out RuntimeWorldHostProjectionSnapshot projection)
|
|
&& (projection.PendingAcknowledgements & stage) != 0;
|
|
|
|
private void RemoveHostProjection(HostProjection host)
|
|
{
|
|
for (int i = 0; i < _hostProjections.Count; i++)
|
|
{
|
|
if (!ReferenceEquals(_hostProjections[i], host))
|
|
continue;
|
|
|
|
_hostProjections.RemoveAt(i);
|
|
return;
|
|
}
|
|
}
|
|
|
|
private void Acknowledge(
|
|
HostProjection host,
|
|
RuntimeWorldHostAcknowledgementStage stage)
|
|
{
|
|
if (!_transit.AcknowledgeHostProjection(
|
|
new RuntimeWorldHostAcknowledgement(
|
|
host.Token,
|
|
stage)))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Runtime rejected reveal host acknowledgement {stage} "
|
|
+ $"for generation {host.Token.Generation}.");
|
|
}
|
|
}
|
|
}
|