namespace AcDream.App.Streaming;
///
/// 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,
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.
///
internal sealed class WorldRevealReadinessBarrier
{
internal const int OutdoorNeighborhoodRadius = 1;
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 isRenderNeighborhoodReady,
Func isSpawnCellReady,
Func isTerrainNeighborhoodReady,
Func areCompositeTexturesReady,
Action prepareCompositeTextures,
Action invalidateCompositeTextures,
Func isSpawnClaimUnhydratable)
{
_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 after all required static
/// meshes for the destination neighborhood have been published.
///
public void Prepare(uint destinationCell)
{
if (destinationCell == 0 || _isSpawnClaimUnhydratable(destinationCell))
return;
int radius = RequiredRenderRadius(destinationCell);
if (_isRenderNeighborhoodReady(destinationCell, radius))
_prepareCompositeTextures(destinationCell, radius);
}
///
/// 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);
int radius = RequiredRenderRadius(destinationCell);
if (_isSpawnClaimUnhydratable(destinationCell))
{
return new WorldRevealReadinessSnapshot(
destinationCell,
isIndoor,
IsUnhydratable: true,
radius,
IsRenderNeighborhoodReady: false,
AreCompositeTexturesReady: false,
IsCollisionReady: false);
}
bool renderReady = _isRenderNeighborhoodReady(destinationCell, radius);
bool compositesReady = renderReady && _areCompositeTexturesReady();
bool collisionReady = renderReady && compositesReady
&& (isIndoor
? _isSpawnCellReady(destinationCell)
: _isTerrainNeighborhoodReady(destinationCell, radius));
return new WorldRevealReadinessSnapshot(
destinationCell,
isIndoor,
IsUnhydratable: false,
radius,
renderReady,
compositesReady,
collisionReady);
}
internal static int RequiredRenderRadius(uint destinationCell) =>
IsIndoor(destinationCell) ? 0 : OutdoorNeighborhoodRadius;
private static bool IsIndoor(uint cellId) => (cellId & 0xFFFFu) >= 0x0100u;
}