diff --git a/src/AcDream.Core/Physics/LandDefs.cs b/src/AcDream.Core/Physics/LandDefs.cs index a6a61040..8ddbde03 100644 --- a/src/AcDream.Core/Physics/LandDefs.cs +++ b/src/AcDream.Core/Physics/LandDefs.cs @@ -141,6 +141,34 @@ public static class LandDefs return false; } + /// + /// LandDefs::get_block_offset (pc:69189, @0x0043e630): world-meter offset + /// from 's landblock origin to 's. + /// ZeroVector when both ids share a landblock (high words equal). The decomp's + /// block_byte<<3 converts to lcoord (cell) units and the literal + /// ·24 m/cell then nets to (Δlandblock)·192 m per axis. The + /// degenerate arg==0 fallbacks are ported verbatim (dest==0 → raw source + /// id; source==0 → 0 lcoord). The ONLY cross-cell translation in retail physics: + /// a delta of two NAMED cell ids, never an accumulation against a moving center. + /// + public static Vector3 GetBlockOffset(uint source, uint dest) + { + uint srcBlock = source >> 16; + uint dstBlock = dest >> 16; + if (srcBlock == dstBlock) + return Vector3.Zero; + + int srcLx, srcLy; + if (source == 0u) { srcLx = 0; srcLy = 0; } + else { srcLx = (int)((source >> 21) & 0x7f8u); srcLy = (int)((srcBlock & 0xFFu) << 3); } + + int dstLx, dstLy; + if (dest == 0u) { dstLx = (int)source; dstLy = (int)source; } // retail degenerate guard + else { dstLx = (int)((dest >> 21) & 0x7f8u); dstLy = (int)((dstBlock & 0xFFu) << 3); } + + return new Vector3((dstLx - srcLx) * CellLength, (dstLy - srcLy) * CellLength, 0f); + } + // Retail cell_in_range: landcell, envcell, or the block sentinel. private static bool CellLowInRange(uint low) => low is (>= 1u and <= 0x40u) or (>= 0x100u and <= 0xFFFDu) or 0xFFFFu; diff --git a/src/AcDream.Core/Physics/Position.cs b/src/AcDream.Core/Physics/Position.cs new file mode 100644 index 00000000..067ffefa --- /dev/null +++ b/src/AcDream.Core/Physics/Position.cs @@ -0,0 +1,23 @@ +using System.Numerics; + +namespace AcDream.Core.Physics; + +/// +/// Retail Frame (acclient.h:30647). is LOCAL to the +/// owning cell: outdoor → X/Y ∈ [0,192) within the landblock, Z = height; +/// indoor → the EnvCell-relative placement. replaces +/// retail's m_fl2gv 3×3 local→global matrix (equivalent rotation). +/// +public readonly record struct Frame(Vector3 Origin, Quaternion Orientation); + +/// +/// Retail Position (acclient.h:30658): the (which-cell, where-inside) pair. +/// One type for indoor and outdoor. The full 32-bit cell id (high 16 = landblock +/// prefix, low 16 = cell index) plus the cell-local . Neither +/// half is ever reconstructed from a streaming center — see #145 design spec. +/// +public readonly record struct Position(uint ObjCellId, Frame Frame) +{ + public Position(uint objCellId, Vector3 origin, Quaternion orientation) + : this(objCellId, new Frame(origin, orientation)) { } +} diff --git a/src/AcDream.Core/Physics/ShadowShapeBuilder.cs b/src/AcDream.Core/Physics/ShadowShapeBuilder.cs index 4fff4106..449b302c 100644 --- a/src/AcDream.Core/Physics/ShadowShapeBuilder.cs +++ b/src/AcDream.Core/Physics/ShadowShapeBuilder.cs @@ -4,6 +4,7 @@ using System.Numerics; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; +using DatFrame = DatReaderWriter.Types.Frame; namespace AcDream.Core.Physics; @@ -88,9 +89,9 @@ public static class ShadowShapeBuilder uint gfxId = (uint)setup.Parts[i]; if (!hasPhysicsBsp(gfxId)) continue; - Frame partFrame = placementFrame is not null && i < placementFrame.Frames.Count + DatFrame partFrame = placementFrame is not null && i < placementFrame.Frames.Count ? placementFrame.Frames[i] - : new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity }; + : new DatFrame { Origin = Vector3.Zero, Orientation = Quaternion.Identity }; // BSP radius default; caller substitutes the real BoundingSphere.Radius // at registration time when available. Loose-but-safe broadphase value. diff --git a/tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs b/tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs index e911d21d..3508d6fe 100644 --- a/tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs +++ b/tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs @@ -10,6 +10,7 @@ using Xunit; // Alias the DatReaderWriter enum so it doesn't clash with // AcDream.Core.Physics.MotionCommand (which is a static class of uint constants). using DRWMotionCommand = DatReaderWriter.Enums.MotionCommand; +using DatFrame = DatReaderWriter.Types.Frame; namespace AcDream.Core.Tests.Physics; @@ -65,7 +66,7 @@ file static class Fixtures { var pf = new AnimationFrame((uint)numParts); for (int p = 0; p < numParts; p++) - pf.Frames.Add(new Frame { Origin = origin, Orientation = orientation }); + pf.Frames.Add(new DatFrame { Origin = origin, Orientation = orientation }); anim.PartFrames.Add(pf); } return anim; @@ -86,8 +87,8 @@ file static class Fixtures var pf1 = new AnimationFrame((uint)numParts); for (int p = 0; p < numParts; p++) { - pf0.Frames.Add(new Frame { Origin = fromOrigin, Orientation = fromRot }); - pf1.Frames.Add(new Frame { Origin = toOrigin, Orientation = toRot }); + pf0.Frames.Add(new DatFrame { Origin = fromOrigin, Orientation = fromRot }); + pf1.Frames.Add(new DatFrame { Origin = toOrigin, Orientation = toRot }); } anim.PartFrames.Add(pf0); anim.PartFrames.Add(pf1); @@ -396,7 +397,7 @@ public sealed class AnimationSequencerTests { var pf = new AnimationFrame(1); float y = 10f - 5f * f; // 10, 5, 0 - pf.Frames.Add(new Frame { Origin = new Vector3(0, y, 0), Orientation = Quaternion.Identity }); + pf.Frames.Add(new DatFrame { Origin = new Vector3(0, y, 0), Orientation = Quaternion.Identity }); linkAnim.PartFrames.Add(pf); } @@ -593,7 +594,7 @@ public sealed class AnimationSequencerTests for (int f = 0; f < 4; f++) { var pf = new AnimationFrame(1); - pf.Frames.Add(new Frame { Origin = new Vector3(0, 0, f), Orientation = Quaternion.Identity }); + pf.Frames.Add(new DatFrame { Origin = new Vector3(0, 0, f), Orientation = Quaternion.Identity }); anim.PartFrames.Add(pf); } @@ -1006,7 +1007,7 @@ public sealed class AnimationSequencerTests anim.Flags = AnimationFlags.PosFrames; for (int f = 0; f < 4; f++) { - anim.PosFrames.Add(new Frame + anim.PosFrames.Add(new DatFrame { Origin = new Vector3(1f, 0f, 0f), Orientation = Quaternion.Identity, @@ -1153,7 +1154,7 @@ public sealed class AnimationSequencerTests for (int f = 0; f < 10; f++) { var pf = new AnimationFrame(1); - pf.Frames.Add(new Frame { Origin = new Vector3(0, 0, f), Orientation = Quaternion.Identity }); + pf.Frames.Add(new DatFrame { Origin = new Vector3(0, 0, f), Orientation = Quaternion.Identity }); anim.PartFrames.Add(pf); } @@ -1204,7 +1205,7 @@ public sealed class AnimationSequencerTests for (int f = 0; f < 10; f++) { var pf = new AnimationFrame(1); - pf.Frames.Add(new Frame { Origin = new Vector3(0, 0, f), Orientation = Quaternion.Identity }); + pf.Frames.Add(new DatFrame { Origin = new Vector3(0, 0, f), Orientation = Quaternion.Identity }); anim.PartFrames.Add(pf); } diff --git a/tests/AcDream.Core.Tests/Physics/LandDefsBlockOffsetTests.cs b/tests/AcDream.Core.Tests/Physics/LandDefsBlockOffsetTests.cs new file mode 100644 index 00000000..a9a75b35 --- /dev/null +++ b/tests/AcDream.Core.Tests/Physics/LandDefsBlockOffsetTests.cs @@ -0,0 +1,41 @@ +using System.Numerics; +using AcDream.Core.Physics; +using Xunit; + +namespace AcDream.Core.Tests.Physics; + +public class LandDefsBlockOffsetTests +{ + // get_block_offset(src, dst) returns the world-meter offset from src's + // landblock origin to dst's landblock origin. block_byte<<3 = lcoord, *24 m/cell + // → nets to (Δlandblock)·192 m. Same landblock → Zero. Decomp @0x0043e630. + + [Fact] + public void SameLandblock_ReturnsZero() + { + // both 0xC95B + Assert.Equal(Vector3.Zero, LandDefs.GetBlockOffset(0xC95B0001u, 0xC95B0008u)); + } + + [Fact] + public void SouthNeighbour_IsMinus192Y() + { + // 0xC95B (lbY=0x5B) → 0xC95A (lbY=0x5A): one landblock south = (0,-192,0). + // This is the exact #145 case: correct origin for 0xC95A relative to 0xC95B. + Assert.Equal(new Vector3(0f, -192f, 0f), LandDefs.GetBlockOffset(0xC95B0001u, 0xC95A0001u)); + } + + [Fact] + public void EastNeighbour_IsPlus192X() + { + // 0xC95B (lbX=0xC9) → 0xCA5B (lbX=0xCA): one landblock east = (+192,0,0). + Assert.Equal(new Vector3(192f, 0f, 0f), LandDefs.GetBlockOffset(0xC95B0001u, 0xCA5B0001u)); + } + + [Fact] + public void DiagonalNeighbour_CombinesBothAxes() + { + // 0xC95B → 0xCA5C: lbX +1 (+192), lbY +1 (+192). + Assert.Equal(new Vector3(192f, 192f, 0f), LandDefs.GetBlockOffset(0xC95B0001u, 0xCA5C0001u)); + } +} diff --git a/tests/AcDream.Core.Tests/Physics/ShadowShapeBuilderTests.cs b/tests/AcDream.Core.Tests/Physics/ShadowShapeBuilderTests.cs index b1679358..2d94753a 100644 --- a/tests/AcDream.Core.Tests/Physics/ShadowShapeBuilderTests.cs +++ b/tests/AcDream.Core.Tests/Physics/ShadowShapeBuilderTests.cs @@ -6,6 +6,7 @@ using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; using Xunit; +using DatFrame = DatReaderWriter.Types.Frame; namespace AcDream.Core.Tests.Physics; @@ -37,9 +38,9 @@ public class ShadowShapeBuilderTests { Frames = { - new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity }, - new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity }, - new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity } + new DatFrame { Origin = Vector3.Zero, Orientation = Quaternion.Identity }, + new DatFrame { Origin = Vector3.Zero, Orientation = Quaternion.Identity }, + new DatFrame { Origin = Vector3.Zero, Orientation = Quaternion.Identity } } } } diff --git a/tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs b/tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs index 4a5bd3fe..4f832dab 100644 --- a/tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs @@ -7,6 +7,7 @@ using AcDream.Core.World; using DatReaderWriter.Types; using Xunit; using DatPhysicsScript = DatReaderWriter.DBObjs.PhysicsScript; +using DatFrame = DatReaderWriter.Types.Frame; namespace AcDream.Core.Tests.Rendering.Vfx; @@ -140,7 +141,7 @@ public sealed class EntityScriptActivatorTests var hookSink = new ParticleHookSink(system); // Hook offset = (1, 0, 0) in entity-local frame. - var hookOffset = new Frame + var hookOffset = new DatFrame { Origin = new Vector3(1f, 0f, 0f), Orientation = Quaternion.Identity, @@ -194,7 +195,7 @@ public sealed class EntityScriptActivatorTests var system = new ParticleSystem(registry); var hookSink = new ParticleHookSink(system); - var script = BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100u, Offset = new Frame() })); + var script = BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100u, Offset = new DatFrame() })); var table = new Dictionary { [0xAAu] = script }; var runner = new PhysicsScriptRunner( id => table.TryGetValue(id, out var s) ? s : null, @@ -257,7 +258,7 @@ public sealed class EntityScriptActivatorTests var system = new ParticleSystem(registry); var hookSink = new ParticleHookSink(system); - var hookOffset = new Frame { Origin = new Vector3(1f, 0, 0), Orientation = Quaternion.Identity }; + var hookOffset = new DatFrame { Origin = new Vector3(1f, 0, 0), Orientation = Quaternion.Identity }; var script = BuildScript( (0.0, new CreateParticleHook { EmitterInfoId = 100u, Offset = hookOffset, PartIndex = 1 })); var table = new Dictionary { [0xAAu] = script }; @@ -297,7 +298,7 @@ public sealed class EntityScriptActivatorTests var system = new ParticleSystem(registry); var hookSink = new ParticleHookSink(system); - var script = BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100u, Offset = new Frame() })); + var script = BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100u, Offset = new DatFrame() })); var table = new Dictionary { [0xAAu] = script }; var runner = new PhysicsScriptRunner( id => table.TryGetValue(id, out var s) ? s : null, diff --git a/tests/AcDream.Core.Tests/Streaming/GpuWorldStateActivatorTests.cs b/tests/AcDream.Core.Tests/Streaming/GpuWorldStateActivatorTests.cs index e5a2034e..c04a5718 100644 --- a/tests/AcDream.Core.Tests/Streaming/GpuWorldStateActivatorTests.cs +++ b/tests/AcDream.Core.Tests/Streaming/GpuWorldStateActivatorTests.cs @@ -10,6 +10,7 @@ using DatReaderWriter.DBObjs; using DatReaderWriter.Types; using Xunit; using DatPhysicsScript = DatReaderWriter.DBObjs.PhysicsScript; +using DatFrame = DatReaderWriter.Types.Frame; namespace AcDream.Core.Tests.Streaming; @@ -43,7 +44,7 @@ public sealed class GpuWorldStateActivatorTests script.ScriptData.Add(new PhysicsScriptData { StartTime = 0.0, - Hook = new CreateParticleHook { EmitterInfoId = 100u, Offset = new Frame() }, + Hook = new CreateParticleHook { EmitterInfoId = 100u, Offset = new DatFrame() }, }); var table = new Dictionary { [scriptId] = script };