acdream/src/AcDream.Core/Rendering/Wb/SceneryHelpers.cs
Erik 16bc10c99d feat(O-T2): extract pure stateless helpers to AcDream.Core.Rendering.Wb
Verbatim copy of 5 WorldBuilder files into src/AcDream.Core/Rendering/Wb/:
- TextureHelpers.cs (pixel-format decoders, Chorizite Lib)
- SceneryHelpers.cs (scenery transforms, Chorizite Lib)
- TerrainUtils.cs, TerrainEntry.cs, CellSplitDirection.cs (WB.Shared Landscape)

Namespace migrated from WorldBuilder.* / Chorizite.OpenGLSDLBackend.Lib
to AcDream.Core.Rendering.Wb per O-D11. [MemoryPackable] stripped from
TerrainEntry per O-D10 (we don't serialize the struct).

Updated 3 source files + 1 test file to import from the new namespace.

Verbatim discipline (O-D1): only namespace + MemoryPack attribute changed.
All algorithm bodies byte-identical to upstream.

Note: TextureHelpers omits IsAlphaFormat() and GetCompressedLayerSize()
because those reference Chorizite.Core.Render.Enums.TextureFormat, a type
that has no path into AcDream.Core without adding an unwanted NuGet dep.
Neither method is called from Core or the test suite; the omission is safe.

Verified on main checkout: dotnet build green (0 errors), dotnet test
green — Failed: 8, Passed: 1147, Skipped: 0, Total: 1155 (baseline maintained).
TextureDecodeConformanceTests (9/9) pass byte-for-byte after namespace swap.
AcDream.Core project alone builds green in this worktree (App-layer failures
are pre-existing, blocked by empty WB submodule, addressed in Tasks 3+4).

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:13:26 +02:00

107 lines
4.4 KiB
C#

using DatReaderWriter.Types;
using System;
using System.Numerics;
namespace AcDream.Core.Rendering.Wb {
/// <summary>
/// Stateless utility methods for deterministic scenery placement.
/// </summary>
public static class SceneryHelpers {
/// <summary>
/// Displaces a scenery object into a pseudo-randomized location
/// </summary>
public static Vector3 Displace(ObjectDesc obj, uint ix, uint iy, uint iq) {
float x;
float y;
float z = obj.BaseLoc.Origin.Z;
var loc = obj.BaseLoc;
if (obj.DisplaceX <= 0)
x = loc.Origin.X;
else
x = (float)(unchecked((uint)(1813693831u * iy - (iq + 45773u) * (1360117743u * iy * ix + 1888038839u) - 1109124029u * ix))
* 2.3283064e-10 * obj.DisplaceX + loc.Origin.X);
if (obj.DisplaceY <= 0)
y = loc.Origin.Y;
else
y = (float)(unchecked((uint)(1813693831u * iy - (iq + 72719u) * (1360117743u * iy * ix + 1888038839u) - 1109124029u * ix))
* 2.3283064e-10 * obj.DisplaceY + loc.Origin.Y);
var quadrantVal = unchecked((uint)(1813693831u * iy - ix * (1870387557u * iy + 1109124029u) - 402451965u)) * 2.3283064e-10f;
if (quadrantVal >= 0.75) return new Vector3(y, -x, z);
if (quadrantVal >= 0.5) return new Vector3(-x, -y, z);
if (quadrantVal >= 0.25) return new Vector3(-y, x, z);
return new Vector3(x, y, z);
}
/// <summary>
/// Returns the scale for a scenery object
/// </summary>
public static float ScaleObj(ObjectDesc obj, uint x, uint y, uint k) {
var minScale = obj.MinScale;
var maxScale = obj.MaxScale;
if (minScale == maxScale)
return maxScale;
return (float)(Math.Pow(maxScale / minScale,
unchecked((uint)(1813693831u * y - (k + 32593u) * (1360117743u * y * x + 1888038839u) - 1109124029u * x)) * 2.3283064e-10) * minScale);
}
/// <summary>
/// Returns the rotation for a scenery object as a Quaternion.
/// </summary>
public static Quaternion RotateObj(ObjectDesc obj, uint x, uint y, uint k, Vector3 loc) {
var baseOrientation = new Quaternion(obj.BaseLoc.Orientation.X, obj.BaseLoc.Orientation.Y, obj.BaseLoc.Orientation.Z, obj.BaseLoc.Orientation.W);
if (obj.MaxRotation <= 0.0f)
return baseOrientation;
var degrees = (float)(unchecked((uint)(1813693831u * y - (k + 63127u) * (1360117743u * y * x + 1888038839u) - 1109124029u * x))
* 2.3283064e-10 * obj.MaxRotation);
return SetHeading(baseOrientation, degrees);
}
/// <summary>
/// Aligns an object to a plane normal, returning Quaternion.
/// </summary>
public static Quaternion ObjAlign(ObjectDesc obj, Vector3 normal, float z, Vector3 loc) {
var baseOrientation = new Quaternion(obj.BaseLoc.Orientation.X, obj.BaseLoc.Orientation.Y, obj.BaseLoc.Orientation.Z, obj.BaseLoc.Orientation.W);
var negNormal = -normal;
var headingDeg = (450.0f - (MathF.Atan2(negNormal.Y, negNormal.X) * 180f / MathF.PI)) % 360f;
return SetHeading(baseOrientation, headingDeg);
}
public static Quaternion SetHeading(Quaternion orientation, float degrees) {
var rads = degrees * MathF.PI / 180f;
var matrix = Matrix4x4.CreateFromQuaternion(orientation);
var heading = new Vector3(MathF.Sin(rads), MathF.Cos(rads), matrix.M23 + matrix.M13);
var normal = Vector3.Normalize(heading);
// Avoid attempting to rotate if normal is too small
if (normal.LengthSquared() < 0.0001f)
return orientation;
var headingDeg = MathF.Atan2(normal.Y, normal.X) * 180f / MathF.PI;
var zDeg = 450.0f - headingDeg;
var zRot = -(zDeg % 360.0f) * MathF.PI / 180f;
var xRot = MathF.Asin(normal.Z);
return Quaternion.CreateFromYawPitchRoll(xRot, 0, zRot);
}
/// <summary>
/// Returns true if floor slope is within bounds for this object
/// </summary>
public static bool CheckSlope(ObjectDesc obj, float zNormal) {
return zNormal >= obj.MinSlope && zNormal <= obj.MaxSlope;
}
}
}