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; }