fix #483: restore far terrain and close OVERHAUL with #484 deferred
Some checks failed
CI / linux-portable (push) Successful in 3m36s
CI / windows-gate (push) Failing after 6m52s
CI / release (push) Has been skipped

This commit is contained in:
Erik 2026-09-06 10:22:51 +02:00
parent e0ba3e7f2c
commit 3ebb120dd2
20 changed files with 700 additions and 100 deletions

View file

@ -75,8 +75,19 @@ public sealed class WalkLandscapeAssembler
RefreshSlotIfWindowed(bx, by);
}
/// <summary>Near-tier retirement retains terrain and its authored bounds.
/// A late building cleanup must not recreate a fully retired block.</summary>
public void ClearBuildings(uint landblockId)
{
(int bx, int by) = BlockCoords(landblockId);
if (!_blocks.TryGetValue((bx, by), out BlockData? data))
return;
data.Buildings = Array.Empty<WalkBuildingFactory.Entry>();
RefreshSlotIfWindowed(bx, by);
}
/// <summary>Landblock retirement (<c>LandblockRenderPublisher
/// .RemoveBuildingRegistry</c>): drop this landblock's data. Safe to call
/// .RemoveTerrain</c>): drop this landblock's data. Safe to call
/// on a landblock that never published (no-op).</summary>
public void RetireLandblock(uint landblockId)
{

View file

@ -3,6 +3,9 @@ using AcDream.Core.World;
namespace AcDream.App.Streaming;
/// <summary>Authored terrain z-slab, independent of near-tier cell/building data.</summary>
public readonly record struct LandblockTerrainBounds(float MaxZ, float MinZ);
/// <summary>
/// Complete CPU-side output of one streaming job. The worker owns this object
/// until it posts the corresponding completion; the render thread then applies
@ -12,7 +15,8 @@ public sealed record LandblockBuild(
LoadedLandblock Landblock,
EnvCellLandblockBuild? EnvCells = null,
LandblockBuildOrigin Origin = default,
LandblockCollisionBuild? Collisions = null)
LandblockCollisionBuild? Collisions = null,
LandblockTerrainBounds TerrainBounds = default)
{
public uint LandblockId => Landblock.LandblockId;
}

View file

@ -132,13 +132,15 @@ public sealed class LandblockBuildFactory
{
var heightmapOnly = _dats.Get<DatReaderWriter.DBObjs.LandBlock>(landblockId);
if (heightmapOnly is null) return null;
(float maxZ, float minZ) = ComputeWalkZSlab(heightmapOnly.Height);
return new AcDream.App.Streaming.LandblockBuild(
new AcDream.Core.World.LoadedLandblock(
landblockId,
heightmapOnly,
System.Array.Empty<AcDream.Core.World.WorldEntity>(),
AcDream.Core.World.PhysicsDatBundle.Empty),
Origin: request.Origin); // far tier: no cells/buildings/entities
Origin: request.Origin,
TerrainBounds: new(maxZ, minZ)); // far tier: no cells/buildings/entities
}
var baseLoaded = AcDream.Core.World.LandblockLoader.Load(_dats, landblockId);
@ -179,7 +181,7 @@ public sealed class LandblockBuildFactory
_heightTable));
var envCellBuild = new AcDream.App.Rendering.Wb.EnvCellLandblockBuildBuilder(landblockId);
// Campaign FW3.1: the walk landscape's per-block z-slab
// (CLandBlock::calc_lighting-adjacent unpack @0x0052f1d0). Every
// (CLandBlock::get_land_limits @0x0052f1d0). Every
// near-tier landblock has outdoor terrain heights regardless of
// whether it has interior cells or buildings, so this is
// unconditional — unlike WalkBuildings (added below, inside
@ -208,7 +210,8 @@ public sealed class LandblockBuildFactory
merged,
physicsDats),
completedEnvCells,
request.Origin);
request.Origin,
TerrainBounds: new(walkMaxZ, walkMinZ));
}
/// <summary>
/// Phase A.1 Task 8: generate scenery (trees, rocks, bushes) for a single
@ -673,7 +676,7 @@ public sealed class LandblockBuildFactory
/// <summary>Campaign FW3.1: retail's per-block z-slab
/// (<c>WalkLandscapeDatBuilder</c>'s port target —
/// <c>CLandBlock::calc_lighting</c>-adjacent unpack @0x0052f1d0):
/// <c>CLandBlock::get_land_limits</c> @0x0052f1d0):
/// <c>max_zval = heightTable[maxByte] + 200</c>,
/// <c>min_zval = heightTable[minByte] - 1</c>, over the landblock's own
/// 81-byte heightmap. Uses <see cref="_heightTable"/> — already a

View file

@ -479,18 +479,18 @@ public sealed class LandblockPresentationPipeline
completed.Heightmap,
Array.Empty<WorldEntity>(),
PhysicsDatBundle.Empty);
LandblockBuild farBuild = new(
farLandblock,
Origin: completedBuild.Origin,
TerrainBounds: completedBuild.TerrainBounds);
transaction = new PublicationTransaction
{
Kind = PublicationKind.Far,
Build = new LandblockBuild(
farLandblock,
Origin: completedBuild.Origin),
Build = farBuild,
MeshData = meshData,
LandblockId = farLandblock.LandblockId,
Cost = LandblockStreamResultCost.Estimate(
new LandblockBuild(
farLandblock,
Origin: completedBuild.Origin),
farBuild,
meshData),
Tier = LandblockStreamTier.Far,
Timing = PublicationTimingProbe.CreateTimings(),
@ -534,7 +534,8 @@ public sealed class LandblockPresentationPipeline
PhysicsDatBundle.Empty);
LandblockBuild farBuild = new(
farLandblock,
Origin: completedBuild.Origin);
Origin: completedBuild.Origin,
TerrainBounds: completedBuild.TerrainBounds);
transaction = new PublicationTransaction
{
Kind = PublicationKind.Far,

View file

@ -103,15 +103,9 @@ public sealed class LandblockRenderPublisher
private readonly Action<EnvCellLandblockBuild>? _prepareEnvCells;
private readonly Action<uint>? _removeEnvCells;
private readonly Dictionary<uint, BuildingRegistry> _buildingRegistries = new();
// Campaign FW3.1: the walk's production world-data siblings of
// _buildingRegistries. Owned here (not injected) exactly like
// _buildingRegistries — no caller constructs a LandblockRenderPublisher
// with pre-existing walk state. Committed/retired at the same points as
// the building registry (see AdvanceCompleteOne / RemoveBuildingRegistry
// below) because both come from the same landblock's BuildingInfo array
// at the same commit. No frame reads these yet (FW3.2 wires the walk
// into the render loop) — publication happens now so FW3.1's
// conformance gate exercises the production commit path.
// The frame walk's sole terrain/building owners. Both publish in the
// retained render suffix; near-layer retirement clears only buildings,
// while full terrain retirement removes the landscape entry as well.
private readonly WalkBuildingRegistry _walkBuildingRegistry = new();
private readonly WalkLandscapeAssembler _walkLandscape = new();
@ -360,21 +354,19 @@ public sealed class LandblockRenderPublisher
_buildingRegistries[registryKey] =
completedBuildings.Registry;
}
// Campaign FW3.1: publish this landblock's walk world data in
// the same step — build.EnvCells carries WalkBuildings/WalkMaxZ
// /WalkMinZ (computed worker-side by LandblockBuildFactory) for
// every near-tier build, even one with zero interior cells
// (the z-slab is unconditional; WalkBuildings is naturally empty
// when LandBlockInfo has none).
// Terrain exists at both tiers; only near builds supply buildings.
// Keep both publications in this retained receipt's existing step.
if (build.EnvCells is { } walkEnvCells)
{
_walkBuildingRegistry.Publish(landblockId, walkEnvCells.WalkBuildings);
_walkLandscape.PublishLandblock(
landblockId,
walkEnvCells.WalkMaxZ,
walkEnvCells.WalkMinZ,
walkEnvCells.WalkBuildings);
}
else
_walkBuildingRegistry.Retire(landblockId);
_walkLandscape.PublishLandblock(
landblockId,
build.TerrainBounds.MaxZ,
build.TerrainBounds.MinZ,
build.EnvCells is { } cells
? cells.WalkBuildings
: Array.Empty<WalkBuildingFactory.Entry>());
publication.BuildingRegistryCommitted = true;
}
else if (publication.EnvCellPublication is { } envCellPublication
@ -421,6 +413,7 @@ public sealed class LandblockRenderPublisher
public void RemoveTerrain(uint landblockId)
{
_removeTerrain(landblockId);
_walkLandscape.RetireLandblock(landblockId);
_terrainRemovalCount++;
}
@ -433,14 +426,9 @@ public sealed class LandblockRenderPublisher
public void RemoveBuildingRegistry(uint landblockId)
{
_buildingRegistries.Remove(landblockId & 0xFFFF0000u);
// Campaign FW3.1: retire the walk's siblings at the SAME retirement
// stage — they were committed together above, so they retire
// together (no new LandblockRetirementStage; folding into the
// existing BuildingRegistry stage keeps the retirement ticket state
// machine unchanged, which is the minimal/additive choice for a
// slice that does no frame wiring yet).
// Demotion retires buildings, not the retained far-tier terrain.
_walkBuildingRegistry.Retire(landblockId);
_walkLandscape.RetireLandblock(landblockId);
_walkLandscape.ClearBuildings(landblockId);
_buildingRegistryRemovalCount++;
}

View file

@ -615,7 +615,10 @@ public sealed class LandblockStreamer : IDisposable, ILandblockCompletionSource
lb.Heightmap,
System.Array.Empty<AcDream.Core.World.WorldEntity>(),
PhysicsDatBundle.Empty);
build = new LandblockBuild(lb, Origin: build.Origin);
build = new LandblockBuild(
lb,
Origin: build.Origin,
TerrainBounds: build.TerrainBounds);
}
PublishResult(new LandblockStreamResult.Loaded(
load.LandblockId, tier, build, mesh, load.Generation));