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