namespace AcDream.App.Streaming;
///
/// The live landblock window the reveal gate measures. Retail has exactly one
/// landscape square — LScape::mid_radius, assigned directly from the
/// user's Render.LandscapeDrawDistance preference
/// (SmartBox::SetRegion @0x004531F0;
/// Render_LandscapeDrawDistance_Values @0x007CA988 = {3,5,8,11,15,25},
/// default 8) — and that same square is simultaneously the loaded set, the
/// drawn set, and the set LScape::PreFetchCells @0x00505660 blocks on.
/// acdream splits the loaded landscape into a Near tier (full publication) and
/// a Far tier (terrain render + terrain collision), so the reveal window is a
/// pair rather than a single number; is the structural
/// analogue of retail's mid_radius because it bounds everything the
/// user can see (fog end is FarRadius * 192 m * 0.95).
///
internal readonly record struct StreamingRevealWindow(
int NearRadius,
int FarRadius);
///
/// One evaluation of the destination domains guarded by
/// . Keeping the individual facts in
/// the canonical owner lets diagnostics and automation observe readiness
/// without duplicating (and eventually drifting from) the reveal predicate.
///
internal readonly record struct WorldRevealReadinessSnapshot(
uint DestinationCell,
bool IsIndoor,
bool IsUnhydratable,
int RequiredRenderRadius,
int RequiredNearRadius,
bool IsRenderNeighborhoodReady,
bool AreCompositeTexturesReady,
bool IsCollisionReady)
{
public bool HasDestination => DestinationCell != 0u;
public bool IsReady => HasDestination
&& (IsUnhydratable
|| (IsRenderNeighborhoodReady
&& AreCompositeTexturesReady
&& IsCollisionReady));
}
///
/// Joins the render-publication, texture, and collision domains that must be
/// complete before the normal world viewport is revealed. Retail keeps
/// SmartBox::position_update_complete false while
/// CellManager::blocking_for_cells is set
/// (SmartBox::UseTime, 0x00455410). acdream loads those domains
/// asynchronously, so login and portal arrival must share this explicit
/// equivalent of retail's single blocking-cell edge.
///
///
/// #280: the outdoor gate is DERIVED from the live streaming window, never
/// hardcoded. Retail cannot stream farther than it gates because it has only
/// one number (LScape::SetMidRadius @0x00504C00 sets
/// mid_width = radius * 2 + 1 and LScape::update_block
/// @0x005063A0 allocates exactly that array). A fixed reveal radius under a
/// configurable streaming window reintroduces the decoupling the moment the
/// preset changes, which is the whole defect.
///
///
internal sealed class WorldRevealReadinessBarrier
{
private readonly Func _revealWindow;
private readonly Func _isRenderNeighborhoodReady;
private readonly Func _isSpawnCellReady;
private readonly Func _isTerrainNeighborhoodReady;
private readonly Func _areCompositeTexturesReady;
private readonly Action _prepareCompositeTextures;
private readonly Action _invalidateCompositeTextures;
private readonly Func _isSpawnClaimUnhydratable;
public WorldRevealReadinessBarrier(
Func revealWindow,
Func isRenderNeighborhoodReady,
Func isSpawnCellReady,
Func isTerrainNeighborhoodReady,
Func areCompositeTexturesReady,
Action prepareCompositeTextures,
Action invalidateCompositeTextures,
Func isSpawnClaimUnhydratable)
{
_revealWindow = revealWindow
?? throw new ArgumentNullException(nameof(revealWindow));
_isRenderNeighborhoodReady = isRenderNeighborhoodReady
?? throw new ArgumentNullException(nameof(isRenderNeighborhoodReady));
_isSpawnCellReady = isSpawnCellReady
?? throw new ArgumentNullException(nameof(isSpawnCellReady));
_isTerrainNeighborhoodReady = isTerrainNeighborhoodReady
?? throw new ArgumentNullException(nameof(isTerrainNeighborhoodReady));
_areCompositeTexturesReady = areCompositeTexturesReady
?? throw new ArgumentNullException(nameof(areCompositeTexturesReady));
_prepareCompositeTextures = prepareCompositeTextures
?? throw new ArgumentNullException(nameof(prepareCompositeTextures));
_invalidateCompositeTextures = invalidateCompositeTextures
?? throw new ArgumentNullException(nameof(invalidateCompositeTextures));
_isSpawnClaimUnhydratable = isSpawnClaimUnhydratable
?? throw new ArgumentNullException(nameof(isSpawnClaimUnhydratable));
}
///
/// Starts a new reveal lifetime. Texture readiness is destination-scoped;
/// carrying the prior login/portal result across a new destination would
/// open the barrier before that destination's composites were uploaded.
///
public void Begin() => _invalidateCompositeTextures();
///
/// Advances render-thread texture preparation once the static meshes the
/// composite domain covers have been published.
///
///
/// D3: the composite domain is entity-scoped
/// (WbDrawDispatcher.IsCompositeWarmupCandidate filters entities by
/// Chebyshev landblock radius) and Far-tier builds carry no entities at all
/// (LandblockBuildFactory), so the honest composite domain is the
/// NEAR radius. Widening it over Far rings would walk the whole outer
/// window to warm nothing.
///
///
///
/// The TRIGGER is therefore scoped the same way. Pre-#280 the gate and the
/// composite domain were the same radius-1 square, so gating warmup on the
/// whole gate cost nothing. #280 widened the gate to the entire Far window
/// without widening the domain; keeping the old trigger would have pushed
/// every composite upload behind the last outer-ring landblock and made
/// the hold longer than the streaming work alone requires. Warming as soon
/// as the Near sub-window is published restores the overlap. The reveal
/// gate itself is untouched: still requires the
/// full window AND composite readiness.
///
///
public void Prepare(uint destinationCell)
{
if (destinationCell == 0 || _isSpawnClaimUnhydratable(destinationCell))
return;
StreamingRevealWindow required = RequiredWindow(destinationCell);
if (_isRenderNeighborhoodReady(
destinationCell,
required.NearRadius,
required.NearRadius))
{
_prepareCompositeTextures(destinationCell, required.NearRadius);
}
}
///
/// True only when the same destination is drawable and collidable. An
/// impossible indoor claim intentionally crosses the barrier so the
/// existing loud unhydratable-placement path can expose invalid server
/// data. A merely slow hydratable claim never crosses early.
///
public bool IsReady(uint destinationCell)
=> Evaluate(destinationCell).IsReady;
///
/// Evaluates each readiness domain once and returns the complete decision.
/// Consumers must use this snapshot rather than reimplementing the join.
///
public WorldRevealReadinessSnapshot Evaluate(uint destinationCell)
{
if (destinationCell == 0)
return default;
bool isIndoor = IsIndoor(destinationCell);
StreamingRevealWindow required = RequiredWindow(destinationCell);
if (_isSpawnClaimUnhydratable(destinationCell))
{
return new WorldRevealReadinessSnapshot(
destinationCell,
isIndoor,
IsUnhydratable: true,
required.FarRadius,
required.NearRadius,
IsRenderNeighborhoodReady: false,
AreCompositeTexturesReady: false,
IsCollisionReady: false);
}
bool renderReady = _isRenderNeighborhoodReady(
destinationCell,
required.NearRadius,
required.FarRadius);
bool compositesReady = renderReady && _areCompositeTexturesReady();
bool collisionReady = renderReady && compositesReady
&& (isIndoor
? _isSpawnCellReady(destinationCell)
: _isTerrainNeighborhoodReady(
destinationCell,
required.FarRadius));
return new WorldRevealReadinessSnapshot(
destinationCell,
isIndoor,
IsUnhydratable: false,
required.FarRadius,
required.NearRadius,
renderReady,
compositesReady,
collisionReady);
}
///
/// The window this destination must satisfy, read LIVE from the streaming
/// configuration on every call. Retail's answer to a mid-hold radius
/// change is SmartBox::set_mid_radius @0x00453180: reset the cell
/// manager, re-radius the landscape, and re-arm the blocking prefetch at
/// the NEW value — never finish the old hold at the old radius.
///
///
/// Indoor destinations take retail's EnvCell arm
/// (CEnvCell::PreFetchCells @0x0052D1E0), which walks the cell's own
/// visible-cell graph rather than the landscape square, so the outdoor
/// ring is not required and the radius is zero.
///
///
internal StreamingRevealWindow RequiredWindow(uint destinationCell)
{
if (IsIndoor(destinationCell))
return new StreamingRevealWindow(0, 0);
StreamingRevealWindow window = _revealWindow();
int far = Math.Max(0, window.FarRadius);
int near = Math.Clamp(window.NearRadius, 0, far);
return new StreamingRevealWindow(near, far);
}
///
/// The outdoor gate's outer radius — retail's mid_radius. Callers
/// that reserve destination streaming capacity must use exactly this value
/// so the prefetch square and the blocked square stay identical, as they
/// are in retail by construction.
///
internal int RequiredRenderRadius(uint destinationCell) =>
RequiredWindow(destinationCell).FarRadius;
private static bool IsIndoor(uint cellId) => (cellId & 0xFFFFu) >= 0x0100u;
}