diff --git a/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs b/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs index 36d3878a..7ba5dde7 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Numerics; +using AcDream.Core.Rendering.Wb; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; @@ -125,14 +126,7 @@ public sealed class EnvCellLandblockBuildBuilder uint environmentId, ushort cellStructure, IReadOnlyList surfaces) - { - var hash = 17L; - hash = hash * 31 + (int)environmentId; - hash = hash * 31 + cellStructure; - foreach (var surface in surfaces) - hash = hash * 31 + surface; - return (ulong)hash | 0x2_0000_0000UL; - } + => EnvCellGeometryIdentity.Compute(environmentId, cellStructure, surfaces); private static WbBoundingBox ComputeLocalBounds(CellStruct cellStruct) { diff --git a/src/AcDream.Core/Rendering/Wb/EnvCellGeometryIdentity.cs b/src/AcDream.Core/Rendering/Wb/EnvCellGeometryIdentity.cs new file mode 100644 index 00000000..f03ba64d --- /dev/null +++ b/src/AcDream.Core/Rendering/Wb/EnvCellGeometryIdentity.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace AcDream.Core.Rendering.Wb; + +/// +/// Computes WorldBuilder's content identity for an EnvCell shell. +/// Equal environment, cell-structure, and ordered surface tuples share one +/// prepared geometry payload even when they occur under different cell IDs. +/// +public static class EnvCellGeometryIdentity +{ + /// + /// Source: WorldBuilder EnvCellRenderManager.GetEnvCellGeomId, + /// lines 94-103. Bit 33 separates content identities from per-cell IDs, + /// whose synthetic geometry requests use bit 32. + /// + public static ulong Compute( + uint environmentId, + ushort cellStructure, + IReadOnlyList surfaces) + { + long hash = 17; + hash = unchecked(hash * 31 + environmentId); + hash = unchecked(hash * 31 + cellStructure); + for (int i = 0; i < surfaces.Count; i++) + hash = unchecked(hash * 31 + surfaces[i]); + return (ulong)hash | 0x2_0000_0000UL; + } +} diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/EnvCellGeometryIdentityTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/EnvCellGeometryIdentityTests.cs new file mode 100644 index 00000000..ebb4c10a --- /dev/null +++ b/tests/AcDream.Core.Tests/Rendering/Wb/EnvCellGeometryIdentityTests.cs @@ -0,0 +1,42 @@ +using AcDream.Core.Rendering.Wb; + +namespace AcDream.Core.Tests.Rendering.Wb; + +public sealed class EnvCellGeometryIdentityTests +{ + [Fact] + public void Compute_EmptySurfaces_MatchesExtractedWorldBuilderValue() + { + Assert.Equal( + 0x2_0000_47D6UL, + EnvCellGeometryIdentity.Compute(0x42, 7, Array.Empty())); + } + + [Fact] + public void Compute_OrderedSurfaces_MatchesExtractedWorldBuilderValue() + { + Assert.Equal( + 0x2_20A7_A46CUL, + EnvCellGeometryIdentity.Compute(0x42, 7, new ushort[] { 1, 2, 3 })); + } + + [Fact] + public void Compute_SurfaceOrderIsPartOfIdentity() + { + var forward = EnvCellGeometryIdentity.Compute(1, 1, new ushort[] { 10, 20 }); + var reverse = EnvCellGeometryIdentity.Compute(1, 1, new ushort[] { 20, 10 }); + + Assert.NotEqual(forward, reverse); + } + + [Fact] + public void Compute_AlwaysUsesDeduplicatedGeometryNamespace() + { + var id = EnvCellGeometryIdentity.Compute( + uint.MaxValue, + ushort.MaxValue, + new ushort[] { ushort.MaxValue, ushort.MaxValue }); + + Assert.NotEqual(0UL, id & 0x2_0000_0000UL); + } +}