merge: sky/weather/lighting overhaul branch (Opus agent, 7 commits, +27 tests)
Ships full retail-faithful sky-object rendering, 5-kind weather with deterministic per-day roll + storm lightning, dynamic-lighting shader UBO with retail hard-cutoff semantics, per-entity torch LightSource registration via Setup.Lights, ParticleRenderer for rain/snow, and TimeSync handshake wiring. F7 / F10 debug keys for time/weather cycling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
48b5e1f1b1
31 changed files with 3057 additions and 129 deletions
119
tests/AcDream.Core.Tests/Lighting/LightingHookSinkTests.cs
Normal file
119
tests/AcDream.Core.Tests/Lighting/LightingHookSinkTests.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Lighting;
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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(8f, light.Range);
|
||||
Assert.Equal(0.8f, light.Intensity);
|
||||
Assert.Equal(new Vector3(101, 202, 303), light.WorldPosition);
|
||||
Assert.InRange(light.ColorLinear.X, 0.99f, 1.01f);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
121
tests/AcDream.Core.Tests/Lighting/SceneLightingUboTests.cs
Normal file
121
tests/AcDream.Core.Tests/Lighting/SceneLightingUboTests.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.Core.Lighting;
|
||||
using AcDream.Core.World;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Lighting;
|
||||
|
||||
public sealed class SceneLightingUboTests
|
||||
{
|
||||
[Fact]
|
||||
public void UboLight_StructSize_Is64Bytes()
|
||||
{
|
||||
// std140 mandates 4× vec4 = 64 bytes. If this drifts the shader
|
||||
// will read garbage.
|
||||
Assert.Equal(64, Marshal.SizeOf<UboLight>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SceneLightingUbo_StructSize_MatchesConstant()
|
||||
{
|
||||
Assert.Equal(SceneLightingUbo.SizeInBytes, Marshal.SizeOf<SceneLightingUbo>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_PacksActiveLightsIntoSlotsInOrder()
|
||||
{
|
||||
var lights = new LightManager();
|
||||
lights.Register(new LightSource
|
||||
{
|
||||
Kind = LightKind.Point,
|
||||
WorldPosition = new Vector3(1, 2, 3),
|
||||
ColorLinear = new Vector3(1f, 0.5f, 0.25f),
|
||||
Intensity = 0.8f,
|
||||
Range = 6f,
|
||||
});
|
||||
lights.Tick(Vector3.Zero);
|
||||
|
||||
var atmo = new AtmosphereSnapshot(
|
||||
Kind: WeatherKind.Clear,
|
||||
Intensity: 1f,
|
||||
FogColor: new Vector3(0.7f, 0.8f, 0.9f),
|
||||
FogStart: 100f,
|
||||
FogEnd: 400f,
|
||||
FogMode: FogMode.Linear,
|
||||
LightningFlash: 0f,
|
||||
Override: EnvironOverride.None);
|
||||
|
||||
var ubo = SceneLightingUbo.Build(lights, in atmo, new Vector3(10, 20, 30), 0.5f);
|
||||
|
||||
// Light 0 is the slot we populated.
|
||||
Assert.Equal(1f, ubo.Light0.PosAndKind.X);
|
||||
Assert.Equal(2f, ubo.Light0.PosAndKind.Y);
|
||||
Assert.Equal(3f, ubo.Light0.PosAndKind.Z);
|
||||
Assert.Equal((float)(int)LightKind.Point, ubo.Light0.PosAndKind.W);
|
||||
Assert.Equal(6f, ubo.Light0.DirAndRange.W);
|
||||
Assert.Equal(0.8f, ubo.Light0.ColorAndIntensity.W);
|
||||
|
||||
// Unused slots should be zero-packed.
|
||||
Assert.Equal(0f, ubo.Light1.DirAndRange.W);
|
||||
|
||||
// Active count lives in uCellAmbient.w.
|
||||
Assert.Equal(1f, ubo.CellAmbient.W);
|
||||
|
||||
// Fog params passed through.
|
||||
Assert.Equal(100f, ubo.FogParams.X);
|
||||
Assert.Equal(400f, ubo.FogParams.Y);
|
||||
Assert.Equal(0f, ubo.FogParams.Z); // no flash
|
||||
Assert.Equal((float)(int)FogMode.Linear, ubo.FogParams.W);
|
||||
|
||||
// Camera + day fraction.
|
||||
Assert.Equal(10f, ubo.CameraAndTime.X);
|
||||
Assert.Equal(0.5f, ubo.CameraAndTime.W);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_ClampsAtEightLights()
|
||||
{
|
||||
var lights = new LightManager();
|
||||
// Register 20; the active list caps at 8.
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
lights.Register(new LightSource
|
||||
{
|
||||
Kind = LightKind.Point,
|
||||
WorldPosition = new Vector3(i, 0, 0),
|
||||
Range = 200f, // all in range
|
||||
});
|
||||
}
|
||||
lights.Tick(Vector3.Zero);
|
||||
|
||||
var atmo = new AtmosphereSnapshot(
|
||||
WeatherKind.Clear, 1f, Vector3.Zero, 0, 0, FogMode.Off, 0f, EnvironOverride.None);
|
||||
var ubo = SceneLightingUbo.Build(lights, in atmo, Vector3.Zero, 0f);
|
||||
|
||||
// Slot 7 populated (8th light), active count = 8.
|
||||
Assert.Equal(8f, ubo.CellAmbient.W);
|
||||
Assert.NotEqual(0f, ubo.Light7.DirAndRange.W);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithSun_SlotZeroIsDirectional()
|
||||
{
|
||||
var lights = new LightManager();
|
||||
lights.Sun = new LightSource
|
||||
{
|
||||
Kind = LightKind.Directional,
|
||||
WorldForward = new Vector3(0, 0, -1),
|
||||
ColorLinear = new Vector3(1f, 0.9f, 0.8f),
|
||||
Intensity = 1.2f,
|
||||
};
|
||||
lights.Tick(Vector3.Zero);
|
||||
|
||||
var atmo = new AtmosphereSnapshot(
|
||||
WeatherKind.Clear, 1f, Vector3.Zero, 0, 0, FogMode.Off, 0f, EnvironOverride.None);
|
||||
var ubo = SceneLightingUbo.Build(lights, in atmo, Vector3.Zero, 0f);
|
||||
|
||||
Assert.Equal((float)(int)LightKind.Directional, ubo.Light0.PosAndKind.W);
|
||||
Assert.Equal(1.2f, ubo.Light0.ColorAndIntensity.W);
|
||||
}
|
||||
}
|
||||
136
tests/AcDream.Core.Tests/World/SkyDescLoaderTests.cs
Normal file
136
tests/AcDream.Core.Tests/World/SkyDescLoaderTests.cs
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.World;
|
||||
|
||||
public sealed class SkyDescLoaderTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Hand-build a Region with a minimal sky descriptor to feed the
|
||||
/// loader without needing real dat bytes. The LoadFromRegion
|
||||
/// separator exists precisely for this — keeps the parsing logic
|
||||
/// testable independent of DatCollection.
|
||||
/// </summary>
|
||||
private static Region MakeRegion(float dirBright, byte rBgrOrder)
|
||||
{
|
||||
var region = new Region();
|
||||
region.PartsMask = PartsMask.HasSkyInfo;
|
||||
|
||||
var sky = new SkyDesc
|
||||
{
|
||||
TickSize = 1.0,
|
||||
LightTickSize = 2.0,
|
||||
};
|
||||
|
||||
var dg = new DayGroup
|
||||
{
|
||||
ChanceOfOccur = 1.0f,
|
||||
};
|
||||
|
||||
var time = new SkyTimeOfDay
|
||||
{
|
||||
Begin = 0.5f,
|
||||
DirBright = dirBright,
|
||||
DirHeading = 180f,
|
||||
DirPitch = 70f,
|
||||
DirColor = new ColorARGB { Blue = 0, Green = 0, Red = rBgrOrder, Alpha = 255 },
|
||||
AmbBright = 0.4f,
|
||||
AmbColor = new ColorARGB { Blue = 100, Green = 100, Red = 100, Alpha = 255 },
|
||||
MinWorldFog = 120f,
|
||||
MaxWorldFog = 400f,
|
||||
WorldFogColor = new ColorARGB { Blue = 50, Green = 50, Red = 50, Alpha = 255 },
|
||||
WorldFog = 1, // Linear
|
||||
};
|
||||
|
||||
dg.SkyTime.Add(time);
|
||||
sky.DayGroups.Add(dg);
|
||||
region.SkyInfo = sky;
|
||||
|
||||
return region;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromRegion_ConvertsFogFields()
|
||||
{
|
||||
var region = MakeRegion(dirBright: 1.5f, rBgrOrder: 200);
|
||||
var loaded = SkyDescLoader.LoadFromRegion(region);
|
||||
|
||||
Assert.NotNull(loaded);
|
||||
Assert.Equal(1.0, loaded!.TickSize);
|
||||
Assert.Single(loaded.DayGroups);
|
||||
var grp = loaded.DayGroups[0];
|
||||
Assert.Single(grp.SkyTimes);
|
||||
|
||||
var kf = grp.SkyTimes[0].Keyframe;
|
||||
Assert.Equal(120f, kf.FogStart);
|
||||
Assert.Equal(400f, kf.FogEnd);
|
||||
Assert.Equal(FogMode.Linear, kf.FogMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromRegion_SunColor_IsPrepreMultipliedByBrightness()
|
||||
{
|
||||
var region = MakeRegion(dirBright: 1.5f, rBgrOrder: 200);
|
||||
var loaded = SkyDescLoader.LoadFromRegion(region);
|
||||
Assert.NotNull(loaded);
|
||||
|
||||
var kf = loaded!.DayGroups[0].SkyTimes[0].Keyframe;
|
||||
// R was 200/255 ≈ 0.784, times dirBright 1.5 = 1.176
|
||||
Assert.InRange(kf.SunColor.X, 1.17f, 1.19f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromRegion_NoSkyInfo_ReturnsNull()
|
||||
{
|
||||
var region = new Region { PartsMask = 0 };
|
||||
Assert.Null(SkyDescLoader.LoadFromRegion(region));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDefaultProvider_FromDatKeyframes_SupportsInterpolation()
|
||||
{
|
||||
var region = MakeRegion(dirBright: 1.0f, rBgrOrder: 255);
|
||||
var loaded = SkyDescLoader.LoadFromRegion(region)!;
|
||||
var provider = loaded.BuildDefaultProvider();
|
||||
|
||||
// Exactly one keyframe: interpolation at any t returns it.
|
||||
var s = provider.Interpolate(0.1f);
|
||||
Assert.InRange(s.SunColor.X, 0.99f, 1.01f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkyObjectData_IsVisible_HandlesWrap()
|
||||
{
|
||||
var obj = new SkyObjectData
|
||||
{
|
||||
BeginTime = 0.9f, // wraps across midnight
|
||||
EndTime = 0.1f,
|
||||
};
|
||||
|
||||
Assert.True(obj.IsVisible(0.95f)); // near end of day
|
||||
Assert.True(obj.IsVisible(0.05f)); // just after midnight
|
||||
Assert.False(obj.IsVisible(0.5f)); // mid-day (not visible)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkyObjectData_CurrentAngle_LerpsAcrossWindow()
|
||||
{
|
||||
var obj = new SkyObjectData
|
||||
{
|
||||
BeginTime = 0.25f,
|
||||
EndTime = 0.75f,
|
||||
BeginAngle = 0f,
|
||||
EndAngle = 180f,
|
||||
};
|
||||
|
||||
// Middle of the window → 90°.
|
||||
Assert.Equal(90f, obj.CurrentAngle(0.5f), precision: 2);
|
||||
// At begin → begin angle.
|
||||
Assert.Equal(0f, obj.CurrentAngle(0.25f), precision: 2);
|
||||
}
|
||||
}
|
||||
102
tests/AcDream.Core.Tests/World/WeatherSystemTests.cs
Normal file
102
tests/AcDream.Core.Tests/World/WeatherSystemTests.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.World;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.World;
|
||||
|
||||
public sealed class WeatherSystemTests
|
||||
{
|
||||
[Fact]
|
||||
public void Roll_Deterministic_ForSameDayIndex()
|
||||
{
|
||||
var a = new WeatherSystem();
|
||||
var b = new WeatherSystem();
|
||||
|
||||
for (int d = 0; d < 100; d++)
|
||||
{
|
||||
a.Tick(0, d, 100f); // big dt to finish any transition
|
||||
b.Tick(0, d, 100f);
|
||||
Assert.Equal(a.Kind, b.Kind);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Roll_WeightsDominatedByClear()
|
||||
{
|
||||
// Clear should cover ~60% of the distribution. Sample many days
|
||||
// and check the clear fraction is in a reasonable band.
|
||||
var sys = new WeatherSystem();
|
||||
int clear = 0;
|
||||
for (int d = 0; d < 1000; d++)
|
||||
{
|
||||
sys.Tick(0, d, 100f);
|
||||
if (sys.Kind == WeatherKind.Clear) clear++;
|
||||
}
|
||||
double frac = clear / 1000.0;
|
||||
Assert.InRange(frac, 0.45, 0.75);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Transition_EasesAcrossTenSeconds()
|
||||
{
|
||||
// Force Storm, then Clear, sample snapshot fog distance mid-transition.
|
||||
var sys = new WeatherSystem();
|
||||
sys.ForceWeather(WeatherKind.Storm);
|
||||
sys.Tick(0, 1, 100f); // finalize
|
||||
|
||||
var kf = SkyStateProvider.Default().Interpolate(0.5f);
|
||||
var stormFog = sys.Snapshot(in kf);
|
||||
Assert.Equal(WeatherKind.Storm, stormFog.Kind);
|
||||
|
||||
// Snapshot should have a small fog end (storm fog is dense).
|
||||
Assert.True(stormFog.FogEnd < 120f, $"storm fog end too large: {stormFog.FogEnd}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironOverride_ForcesTintedFog()
|
||||
{
|
||||
var sys = new WeatherSystem();
|
||||
sys.Override = EnvironOverride.RedFog;
|
||||
|
||||
var kf = SkyStateProvider.Default().Interpolate(0.5f);
|
||||
var snap = sys.Snapshot(in kf);
|
||||
|
||||
Assert.Equal(EnvironOverride.RedFog, snap.Override);
|
||||
// Red override means the R channel dominates.
|
||||
Assert.True(snap.FogColor.X > snap.FogColor.Y);
|
||||
Assert.True(snap.FogColor.X > snap.FogColor.Z);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Flash_DecaysOverTime()
|
||||
{
|
||||
var sys = new WeatherSystem();
|
||||
sys.TriggerFlash();
|
||||
|
||||
var kf = SkyStateProvider.Default().Interpolate(0.5f);
|
||||
var imm = sys.Snapshot(in kf);
|
||||
Assert.True(imm.LightningFlash > 0.9f);
|
||||
|
||||
// After 1 second the flash should be mostly decayed.
|
||||
sys.Tick(0, 0, 1.0f);
|
||||
var later = sys.Snapshot(in kf);
|
||||
Assert.True(later.LightningFlash < 0.1f,
|
||||
$"lightning flash didn't decay: {later.LightningFlash}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Snapshot_ClearKind_PassesThroughKeyframeFog()
|
||||
{
|
||||
var sys = new WeatherSystem();
|
||||
sys.ForceWeather(WeatherKind.Clear);
|
||||
sys.Tick(0, 0, 100f); // finish transition
|
||||
|
||||
var kf = SkyStateProvider.Default().Interpolate(0.5f);
|
||||
var snap = sys.Snapshot(in kf);
|
||||
|
||||
// Clear passes the keyframe's fog color + distances through.
|
||||
Assert.Equal(kf.FogStart, snap.FogStart, precision: 2);
|
||||
Assert.Equal(kf.FogEnd, snap.FogEnd, precision: 2);
|
||||
}
|
||||
}
|
||||
58
tests/AcDream.Core.Tests/World/WorldTimeDebugTests.cs
Normal file
58
tests/AcDream.Core.Tests/World/WorldTimeDebugTests.cs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
using System;
|
||||
using AcDream.Core.World;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.World;
|
||||
|
||||
public sealed class WorldTimeDebugTests
|
||||
{
|
||||
[Fact]
|
||||
public void SetDebugTime_OverridesDayFraction()
|
||||
{
|
||||
var service = new WorldTimeService(SkyStateProvider.Default());
|
||||
service.SyncFromServer(0); // midnight
|
||||
|
||||
service.SetDebugTime(0.5f); // force noon
|
||||
Assert.InRange(service.DayFraction, 0.499, 0.501);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearDebugTime_RestoresServerTime()
|
||||
{
|
||||
var service = new WorldTimeService(SkyStateProvider.Default());
|
||||
service.SyncFromServer(DerethDateTime.DayTicks * 0.25); // dawn
|
||||
service.SetDebugTime(0.5f);
|
||||
service.ClearDebugTime();
|
||||
|
||||
Assert.InRange(service.DayFraction, 0.24, 0.26);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SyncFromServer_ClearsDebugOverride()
|
||||
{
|
||||
var service = new WorldTimeService(SkyStateProvider.Default());
|
||||
service.SetDebugTime(0.75f);
|
||||
service.SyncFromServer(0); // midnight — this should clear the override
|
||||
|
||||
Assert.InRange(service.DayFraction, 0.0, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProvider_AcceptsNewKeyframes()
|
||||
{
|
||||
var service = new WorldTimeService(SkyStateProvider.Default());
|
||||
var custom = new SkyStateProvider(new[]
|
||||
{
|
||||
new SkyKeyframe(
|
||||
Begin: 0f,
|
||||
SunHeadingDeg: 0f,
|
||||
SunPitchDeg: 90f,
|
||||
SunColor: System.Numerics.Vector3.One,
|
||||
AmbientColor: System.Numerics.Vector3.One,
|
||||
FogColor: System.Numerics.Vector3.Zero,
|
||||
FogDensity: 0f),
|
||||
});
|
||||
service.SetProvider(custom);
|
||||
Assert.Equal(1, custom.KeyframeCount);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue