acdream/tests/AcDream.Core.Tests/Lighting/LightingHookSinkTests.cs

246 lines
8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Numerics;
using AcDream.Core.Lighting;
using AcDream.Core.Vfx;
using DatReaderWriter.Types;
using Xunit;
namespace AcDream.Core.Tests.Lighting;
public sealed class LightingHookSinkTests
{
[Fact]
public void SetLightHook_FlipsOwnedLights()
{
var mgr = new LightManager();
var sink = new LightingHookSink(mgr, new MutablePoseSource());
var light1 = new LightSource { Kind = LightKind.Point, OwnerId = 42, IsLit = true };
var light2 = new LightSource { Kind = LightKind.Point, OwnerId = 42, IsLit = true };
var other = new LightSource { Kind = LightKind.Point, OwnerId = 99, IsLit = true };
sink.RegisterOwnedLight(light1);
sink.RegisterOwnedLight(light2);
sink.RegisterOwnedLight(other);
var hook = new SetLightHook { LightsOn = false };
sink.OnHook(entityId: 42, entityWorldPosition: Vector3.Zero, hook: hook);
Assert.False(light1.IsLit);
Assert.False(light2.IsLit);
Assert.True(other.IsLit); // owner 99 untouched
}
[Fact]
public void UnregisterOwner_RemovesAllOwnedLights()
{
var mgr = new LightManager();
var sink = new LightingHookSink(mgr, new MutablePoseSource());
sink.RegisterOwnedLight(new LightSource { OwnerId = 7 });
sink.RegisterOwnedLight(new LightSource { OwnerId = 7 });
Assert.Equal(2, mgr.RegisteredCount);
sink.UnregisterOwner(7);
Assert.Equal(0, mgr.RegisteredCount);
}
[Fact]
public void UnrelatedHook_Ignored()
{
var mgr = new LightManager();
var sink = new LightingHookSink(mgr, new MutablePoseSource());
var light = new LightSource { OwnerId = 1, IsLit = true };
sink.RegisterOwnedLight(light);
// Should not crash or change state for non-SetLight hooks.
var noise = new SoundHook();
sink.OnHook(entityId: 1, entityWorldPosition: Vector3.Zero, hook: noise);
Assert.True(light.IsLit);
}
[Fact]
public void RefreshAttachedLights_ComposesLocalFrameWithCurrentRootAndCell()
{
var mgr = new LightManager();
var poses = new MutablePoseSource();
poses.Publish(42u,
Matrix4x4.CreateRotationZ(MathF.PI / 2f)
* Matrix4x4.CreateTranslation(10, 20, 30),
cellId: 0x01010002u);
var sink = new LightingHookSink(mgr, poses);
var light = new LightSource
{
OwnerId = 42u,
LocalPose = Matrix4x4.CreateTranslation(1, 0, 2),
TracksOwnerPose = true,
};
sink.RegisterOwnedLight(light);
sink.RefreshAttachedLights();
Assert.InRange(light.WorldPosition.X, 9.99f, 10.01f);
Assert.InRange(light.WorldPosition.Y, 20.99f, 21.01f);
Assert.InRange(light.WorldPosition.Z, 31.99f, 32.01f);
Assert.Equal(0x01010002u, light.CellId);
}
[Fact]
public void RefreshAttachedLights_DoesNotRecomposeDatStaticLight()
{
var mgr = new LightManager();
var poses = new MutablePoseSource();
poses.Publish(42u, Matrix4x4.CreateTranslation(100, 0, 0));
var sink = new LightingHookSink(mgr, poses);
var light = new LightSource
{
OwnerId = 42u,
WorldPosition = new Vector3(7, 8, 9),
LocalPose = Matrix4x4.CreateTranslation(1, 0, 0),
TracksOwnerPose = false,
};
sink.RegisterOwnedLight(light);
sink.RefreshAttachedLights();
Assert.Equal(new Vector3(7, 8, 9), light.WorldPosition);
Assert.Equal(0, poses.RootPoseQueryCount);
}
[Fact]
public void UnregisterPresentation_PreservesSetLightStateForReregistration()
{
var mgr = new LightManager();
var sink = new LightingHookSink(mgr, new MutablePoseSource());
sink.InitializeOwnerLighting(7u, enabled: true);
sink.SetOwnerLighting(7u, enabled: false);
sink.UnregisterOwner(7u, forgetState: false);
var replacement = new LightSource { OwnerId = 7u };
sink.RegisterOwnedLight(replacement);
Assert.False(replacement.IsLit);
}
}
public sealed class LightInfoLoaderTests
{
[Fact]
public void Load_EmptyLights_ReturnsEmpty()
{
var setup = new DatReaderWriter.DBObjs.Setup();
var result = LightInfoLoader.Load(setup, 1u, Vector3.Zero, Quaternion.Identity);
Assert.Empty(result);
}
[Fact]
public void Load_PointLight_ProducesCorrectSource()
{
var setup = new DatReaderWriter.DBObjs.Setup();
setup.Lights[0] = new LightInfo
{
ViewSpaceLocation = new Frame
{
Origin = new Vector3(1, 2, 3),
Orientation = Quaternion.Identity,
},
Color = new ColorARGB { Red = 255, Green = 200, Blue = 50, Alpha = 255 },
Intensity = 0.8f,
Falloff = 8f,
ConeAngle = 0f, // point
};
var result = LightInfoLoader.Load(setup, ownerId: 77,
entityPosition: new Vector3(100, 200, 300),
entityRotation: Quaternion.Identity);
Assert.Single(result);
var light = result[0];
Assert.Equal(LightKind.Point, light.Kind);
Assert.Equal(77u, light.OwnerId);
Assert.Equal(10.4f, light.Range, 3); // Falloff 8 × static_light_factor 1.3 (calc_point_light 0x00820e24)
Assert.Equal(0.8f, light.Intensity);
Assert.Equal(new Vector3(101, 202, 303), light.WorldPosition);
Assert.Equal(new Vector3(1, 2, 3), light.LocalPose.Translation);
Assert.InRange(light.ColorLinear.X, 0.99f, 1.01f);
}
[Fact]
public void Load_DynamicLight_UsesRetailHardwareRangeFactor()
{
var setup = new DatReaderWriter.DBObjs.Setup();
setup.Lights[0] = new LightInfo
{
ViewSpaceLocation = new Frame { Orientation = Quaternion.Identity },
Color = new ColorARGB { Red = 255, Green = 255, Blue = 255, Alpha = 255 },
Intensity = 1f,
Falloff = 8f,
};
var light = Assert.Single(LightInfoLoader.Load(
setup,
ownerId: 77u,
entityPosition: Vector3.Zero,
entityRotation: Quaternion.Identity,
isDynamic: true));
Assert.True(light.IsDynamic);
Assert.Equal(12f, light.Range, 3);
}
[Fact]
public void Load_NonZeroConeAngle_ProducesSpot()
{
var setup = new DatReaderWriter.DBObjs.Setup();
setup.Lights[0] = new LightInfo
{
ViewSpaceLocation = new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity },
Color = new ColorARGB { Red = 255, Green = 255, Blue = 255, Alpha = 255 },
Intensity = 1f,
Falloff = 5f,
ConeAngle = 0.5f,
};
var result = LightInfoLoader.Load(setup, ownerId: 1, entityPosition: Vector3.Zero, entityRotation: Quaternion.Identity);
Assert.Equal(LightKind.Spot, result[0].Kind);
Assert.Equal(0.5f, result[0].ConeAngle);
}
}
file sealed class MutablePoseSource : IEntityEffectPoseSource, IEntityEffectCellSource
{
private readonly Dictionary<uint, (Matrix4x4 Root, uint Cell)> _poses = new();
public void Publish(uint id, Matrix4x4 root, uint cellId = 0) =>
_poses[id] = (root, cellId);
public int RootPoseQueryCount { get; private set; }
public bool TryGetRootPose(uint localEntityId, out Matrix4x4 rootWorld)
{
RootPoseQueryCount++;
if (_poses.TryGetValue(localEntityId, out var pose))
{
rootWorld = pose.Root;
return true;
}
rootWorld = default;
return false;
}
public bool TryGetPartPose(uint localEntityId, int partIndex, out Matrix4x4 partLocal)
{
partLocal = default;
return false;
}
public bool TryGetCellId(uint localEntityId, out uint cellId)
{
if (_poses.TryGetValue(localEntityId, out var pose))
{
cellId = pose.Cell;
return true;
}
cellId = 0;
return false;
}
}