refactor(runtime): own world environment state
This commit is contained in:
parent
b972f539f7
commit
902076c0a4
27 changed files with 886 additions and 295 deletions
|
|
@ -236,6 +236,8 @@ public sealed class InteractionUiRuntimeSourcesTests
|
|||
public IRuntimeChatView Chat => null!;
|
||||
public IRuntimeActionView Actions => null!;
|
||||
public IRuntimeMovementView Movement => null!;
|
||||
public AcDream.Runtime.World.IRuntimeWorldEnvironmentView Environment =>
|
||||
null!;
|
||||
public IRuntimePortalView Portal => null!;
|
||||
public IRuntimeSessionCommands Session => null!;
|
||||
public IRuntimeSelectionCommands Selection => null!;
|
||||
|
|
|
|||
|
|
@ -209,6 +209,8 @@ public sealed class GameplayInputCommandControllerTests
|
|||
public IRuntimeChatView Chat => throw new NotSupportedException();
|
||||
public IRuntimeActionView Actions => throw new NotSupportedException();
|
||||
public IRuntimeMovementView Movement => throw new NotSupportedException();
|
||||
public AcDream.Runtime.World.IRuntimeWorldEnvironmentView Environment =>
|
||||
throw new NotSupportedException();
|
||||
public IRuntimePortalView Portal => throw new NotSupportedException();
|
||||
public RuntimeStateCheckpoint CaptureCheckpoint() =>
|
||||
throw new NotSupportedException();
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"WorldRenderComposition.cs"));
|
||||
|
||||
Assert.Contains(
|
||||
"private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment =",
|
||||
"private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment;",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ using AcDream.Runtime;
|
|||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Session;
|
||||
using AcDream.Runtime.World;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
|
|
@ -783,6 +784,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
_combatModeBinding =
|
||||
CombatModeOperations.BindOwned(CombatMode);
|
||||
MovementState = new RuntimeLocalPlayerMovementState();
|
||||
Environment = new RuntimeWorldEnvironmentState();
|
||||
MovementInput = new DispatcherMovementInputSource(MovementState);
|
||||
GameplayInput = new GameplayInputFrameController(
|
||||
dispatcher: null,
|
||||
|
|
@ -833,6 +835,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
Communication,
|
||||
Actions,
|
||||
MovementState,
|
||||
Environment,
|
||||
WorldReveal,
|
||||
Clock,
|
||||
selectionController);
|
||||
|
|
@ -853,6 +856,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
public RuntimeActionState Actions { get; }
|
||||
public SelectionState Selection => Actions.Selection;
|
||||
public RuntimeLocalPlayerMovementState MovementState { get; }
|
||||
public RuntimeWorldEnvironmentState Environment { get; }
|
||||
public DispatcherMovementInputSource MovementInput { get; }
|
||||
public GameplayInputFrameController GameplayInput { get; }
|
||||
public RecordingCombatModeOperations CombatMode { get; }
|
||||
|
|
|
|||
|
|
@ -338,6 +338,26 @@ public sealed class RuntimeOptionsTests
|
|||
Assert.Equal(0.95f, invalid.FogEndMultiplier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DayGroupOverride_IsReadOnceIntoTypedOptions()
|
||||
{
|
||||
Assert.Null(
|
||||
RuntimeOptions.Parse(
|
||||
AnyDatDir,
|
||||
EmptyEnv()).ForcedDayGroupIndex);
|
||||
Assert.Null(
|
||||
RuntimeOptions.Parse(
|
||||
AnyDatDir,
|
||||
Env(new() { ["ACDREAM_DAY_GROUP"] = "-1" }))
|
||||
.ForcedDayGroupIndex);
|
||||
Assert.Equal(
|
||||
7,
|
||||
RuntimeOptions.Parse(
|
||||
AnyDatDir,
|
||||
Env(new() { ["ACDREAM_DAY_GROUP"] = "7" }))
|
||||
.ForcedDayGroupIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiagnosticFlags_RespectExactValueOne()
|
||||
{
|
||||
|
|
@ -346,6 +366,7 @@ public sealed class RuntimeOptionsTests
|
|||
["ACDREAM_DEVTOOLS"] = "1",
|
||||
["ACDREAM_UNCAPPED_RENDER"] = "1",
|
||||
["ACDREAM_DUMP_MOVE_TRUTH"] = "1",
|
||||
["ACDREAM_DUMP_SKY"] = "1",
|
||||
["ACDREAM_NO_AUDIO"] = "1",
|
||||
["ACDREAM_ENABLE_SKY_PES"] = "1",
|
||||
["ACDREAM_DUMP_SCENERY_Z"] = "1",
|
||||
|
|
@ -354,6 +375,7 @@ public sealed class RuntimeOptionsTests
|
|||
Assert.True(allOn.DevTools);
|
||||
Assert.True(allOn.UncappedRendering);
|
||||
Assert.True(allOn.DumpMoveTruth);
|
||||
Assert.True(allOn.DumpSky);
|
||||
Assert.True(allOn.NoAudio);
|
||||
Assert.True(allOn.EnableSkyPesDebug);
|
||||
Assert.True(allOn.DumpSceneryZ);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.World;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
||||
|
|
@ -57,25 +58,23 @@ public sealed class WorldEnvironmentControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_WithoutGameTimeRestoresDocumentedFallbackOrigin()
|
||||
public void Initialize_WithoutGameTimeUsesInstanceFallbackOrigin()
|
||||
{
|
||||
double previous = DerethDateTime.OriginOffsetTicks;
|
||||
try
|
||||
{
|
||||
DerethDateTime.SetOriginOffsetFromDat(3600.0);
|
||||
var controller = CreateController([]);
|
||||
var first = CreateController([]);
|
||||
var second = CreateController([]);
|
||||
|
||||
controller.Initialize(SingleGroupSky("Sunny", begin: 0f), zeroTimeOfYear: null);
|
||||
first.Initialize(
|
||||
SingleGroupSky("Sunny", begin: 0f),
|
||||
zeroTimeOfYear: 3600.0);
|
||||
second.Initialize(
|
||||
SingleGroupSky("Sunny", begin: 0f),
|
||||
zeroTimeOfYear: null);
|
||||
|
||||
Assert.Equal(
|
||||
DerethDateTime.DayFractionOriginOffsetTicks,
|
||||
DerethDateTime.OriginOffsetTicks);
|
||||
Assert.InRange(controller.DayFraction, 0.499f, 0.501f);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DerethDateTime.SetOriginOffsetFromDat(previous);
|
||||
}
|
||||
Assert.Equal(3600.0, first.WorldTime.Calendar.OriginOffsetTicks);
|
||||
Assert.Equal(
|
||||
DerethDateTime.DayFractionOriginOffsetTicks,
|
||||
second.WorldTime.Calendar.OriginOffsetTicks);
|
||||
Assert.InRange(second.DayFraction, 0.499f, 0.501f);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -143,8 +142,8 @@ public sealed class WorldEnvironmentControllerTests
|
|||
|
||||
private static WorldEnvironmentController CreateController(List<string> log) =>
|
||||
new(
|
||||
new WorldTimeService(SkyStateProvider.Default()),
|
||||
new WeatherSystem(),
|
||||
new RuntimeWorldEnvironmentState(log: log.Add),
|
||||
forcedDayGroupIndex: null,
|
||||
log.Add);
|
||||
|
||||
private static LoadedSkyDesc SingleGroupSky(string name, float begin)
|
||||
|
|
@ -180,8 +179,8 @@ public sealed class WorldEnvironmentControllerTests
|
|||
line.Contains("→ DayGroup[", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[CollectionDefinition(Name, DisableParallelization = true)]
|
||||
[CollectionDefinition(Name)]
|
||||
public sealed class WorldEnvironmentControllerCollection
|
||||
{
|
||||
public const string Name = "World environment process-global calendar";
|
||||
public const string Name = "World environment";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,17 +115,10 @@ public sealed class DerethDateTimeTests
|
|||
// 291,411,660 - 3600 = 291,408,060
|
||||
// Expected output: PY 116 (= ZeroYear 10 + relative 106), Seedsow,
|
||||
// day 24 1-indexed.
|
||||
DerethDateTime.SetOriginOffsetFromDat(3600.0);
|
||||
try
|
||||
{
|
||||
var cal = DerethDateTime.ToCalendar(291_408_060.0);
|
||||
Assert.Equal(DerethDateTime.ZeroYear + 106, cal.Year);
|
||||
Assert.Equal(DerethDateTime.MonthName.Seedsow, cal.Month);
|
||||
Assert.Equal(24, cal.Day);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DerethDateTime.SetOriginOffsetFromDat(DerethDateTime.DayFractionOriginOffsetTicks);
|
||||
}
|
||||
var calendar = new DerethCalendar(3600.0);
|
||||
var cal = calendar.ToCalendar(291_408_060.0);
|
||||
Assert.Equal(DerethDateTime.ZeroYear + 106, cal.Year);
|
||||
Assert.Equal(DerethDateTime.MonthName.Seedsow, cal.Month);
|
||||
Assert.Equal(24, cal.Day);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,39 @@ namespace AcDream.Core.Tests.World;
|
|||
|
||||
public sealed class SkyDescLoaderTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(20, 10, 0, 16)]
|
||||
[InlineData(20, 10, 83, 5)]
|
||||
[InlineData(20, 116, 83, 18)]
|
||||
[InlineData(20, 127, 359, 4)]
|
||||
public void DayGroupSelector_MatchesNamedRetailGoldenValues(
|
||||
int count,
|
||||
int absoluteYear,
|
||||
int dayOfYear,
|
||||
int expected)
|
||||
{
|
||||
Assert.Equal(
|
||||
expected,
|
||||
SkyDayGroupSelector.SelectIndex(
|
||||
count,
|
||||
absoluteYear,
|
||||
daysPerYear: 360,
|
||||
dayOfYear));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DayGroupSelector_ForcedIndexIsExplicitInput()
|
||||
{
|
||||
Assert.Equal(
|
||||
7,
|
||||
SkyDayGroupSelector.SelectIndex(
|
||||
20,
|
||||
absoluteYear: 10,
|
||||
daysPerYear: 360,
|
||||
dayOfYear: 0,
|
||||
forcedIndex: 7));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hand-build a Region with a minimal sky descriptor to feed the
|
||||
/// loader without needing real dat bytes. The LoadFromRegion
|
||||
|
|
|
|||
|
|
@ -29,10 +29,11 @@ public sealed class WorldTimeDebugTests
|
|||
// fraction 1/16: solve (t + 7/16*D) mod D = 1/16*D
|
||||
// → t = (1/16 - 7/16) * D mod D = -6/16 * D mod D = 10/16 * D.
|
||||
double targetFraction = 1.0 / 16.0; // Darktide-and-Half
|
||||
double syncTick = targetFraction * DerethDateTime.DayTicks - DerethDateTime.OriginOffsetTicks;
|
||||
var service = new WorldTimeService(SkyStateProvider.Default());
|
||||
double syncTick = targetFraction * DerethDateTime.DayTicks
|
||||
- service.Calendar.OriginOffsetTicks;
|
||||
while (syncTick < 0) syncTick += DerethDateTime.DayTicks;
|
||||
|
||||
var service = new WorldTimeService(SkyStateProvider.Default());
|
||||
service.SyncFromServer(syncTick);
|
||||
service.SetDebugTime(0.5f);
|
||||
service.ClearDebugTime();
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ public sealed class GameRuntimeContractTests
|
|||
InteractionSourceObjectId: 0u,
|
||||
InteractionTransactions: default),
|
||||
default,
|
||||
default,
|
||||
default);
|
||||
|
||||
recorder.AddCheckpoint(stamp, checkpoint);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.World;
|
||||
|
||||
namespace AcDream.Runtime.Tests.World;
|
||||
|
||||
public sealed class RuntimeWorldEnvironmentStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void Initialize_OwnsRetailDayGroupAndOfflineNoon()
|
||||
{
|
||||
List<string> log = [];
|
||||
var state = new RuntimeWorldEnvironmentState(
|
||||
new ManualTimeProvider(),
|
||||
log.Add);
|
||||
|
||||
state.Initialize(Definition(
|
||||
DerethDateTime.DayFractionOriginOffsetTicks,
|
||||
("Sunny", Sky(0f))));
|
||||
|
||||
Assert.True(state.IsInitialized);
|
||||
Assert.Equal(0, state.ActiveDayGroupIndex);
|
||||
Assert.Equal(WeatherKind.Clear, state.Weather.Kind);
|
||||
Assert.InRange(state.WorldTime.DayFraction, 0.499, 0.501);
|
||||
Assert.Contains(
|
||||
log,
|
||||
value => value.Contains(
|
||||
"SkyDesc.TickSize=0.8",
|
||||
StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TwoInstances_IsolateCalendarClockWeatherAndDebugState()
|
||||
{
|
||||
var time = new ManualTimeProvider();
|
||||
var first = new RuntimeWorldEnvironmentState(time);
|
||||
var second = new RuntimeWorldEnvironmentState(time);
|
||||
first.Initialize(Definition(3600.0, ("Sunny", Sky(0f))));
|
||||
second.Initialize(Definition(
|
||||
DerethDateTime.DayFractionOriginOffsetTicks,
|
||||
("Cloudy", Sky(0f))));
|
||||
|
||||
first.SynchronizeFromServer(0.0);
|
||||
second.SynchronizeFromServer(0.0);
|
||||
_ = first.ApplyAdminEnvirons(0x01u);
|
||||
_ = first.CycleTimeOfDay();
|
||||
|
||||
Assert.Equal(3600.0, first.WorldTime.Calendar.OriginOffsetTicks);
|
||||
Assert.Equal(
|
||||
DerethDateTime.DayFractionOriginOffsetTicks,
|
||||
second.WorldTime.Calendar.OriginOffsetTicks);
|
||||
Assert.Equal(0f, first.WorldTime.DayFraction);
|
||||
Assert.InRange(
|
||||
second.WorldTime.DayFraction,
|
||||
7.0 / 16.0 - 0.001,
|
||||
7.0 / 16.0 + 0.001);
|
||||
Assert.Equal(EnvironOverride.RedFog, first.Weather.Override);
|
||||
Assert.Equal(EnvironOverride.None, second.Weather.Override);
|
||||
Assert.Equal(WeatherKind.Clear, first.Weather.Kind);
|
||||
Assert.Equal(WeatherKind.Overcast, second.Weather.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SynchronizeFromServer_ClearsDebugOverrideAndAdvancesClock()
|
||||
{
|
||||
var time = new ManualTimeProvider();
|
||||
var state = new RuntimeWorldEnvironmentState(time);
|
||||
state.Initialize(Definition(
|
||||
DerethDateTime.DayFractionOriginOffsetTicks,
|
||||
("Sunny", Sky(0f))));
|
||||
_ = state.CycleTimeOfDay();
|
||||
|
||||
state.SynchronizeFromServer(0.0);
|
||||
time.Advance(TimeSpan.FromSeconds(12));
|
||||
|
||||
Assert.Equal(12.0, state.WorldTime.NowTicks, 5);
|
||||
Assert.NotEqual(0.0, state.WorldTime.DayFraction);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x00u, EnvironOverride.None)]
|
||||
[InlineData(0x01u, EnvironOverride.RedFog)]
|
||||
[InlineData(0x06u, EnvironOverride.BlackFog2)]
|
||||
public void ApplyAdminEnvirons_FogIsCanonicalRuntimeState(
|
||||
uint raw,
|
||||
EnvironOverride expected)
|
||||
{
|
||||
var state = Initialized();
|
||||
|
||||
RuntimeEnvironmentEffect effect = state.ApplyAdminEnvirons(raw);
|
||||
|
||||
Assert.Equal(RuntimeEnvironmentEffectKind.FogOverride, effect.Kind);
|
||||
Assert.Equal(expected, effect.FogOverride);
|
||||
Assert.Equal(expected, state.Snapshot.EnvironOverride);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyAdminEnvirons_SoundDoesNotMutateFogState()
|
||||
{
|
||||
var state = Initialized();
|
||||
_ = state.ApplyAdminEnvirons(0x04u);
|
||||
|
||||
RuntimeEnvironmentEffect effect = state.ApplyAdminEnvirons(0x78u);
|
||||
|
||||
Assert.Equal(RuntimeEnvironmentEffectKind.SoundCue, effect.Kind);
|
||||
Assert.Equal(RuntimeEnvironmentSoundCue.Thunder3, effect.SoundCue);
|
||||
Assert.Equal(EnvironOverride.GreenFog, state.Weather.Override);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForcedDayGroup_IsDefinitionScoped()
|
||||
{
|
||||
var state = new RuntimeWorldEnvironmentState(
|
||||
new ManualTimeProvider());
|
||||
state.Initialize(new RuntimeWorldEnvironmentDefinition(
|
||||
DerethDateTime.DayFractionOriginOffsetTicks,
|
||||
sourceTickSize: 0.8,
|
||||
lightTickSize: 0.2,
|
||||
[
|
||||
Group("Sunny", Sky(0f)),
|
||||
Group("Cloudy", Sky(0f)),
|
||||
],
|
||||
forcedDayGroupIndex: 1));
|
||||
|
||||
Assert.Equal(1, state.ActiveDayGroupIndex);
|
||||
Assert.Equal(WeatherKind.Overcast, state.Weather.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_IsOneShot()
|
||||
{
|
||||
var state = Initialized();
|
||||
|
||||
InvalidOperationException error =
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => state.Initialize(Definition(
|
||||
3600.0,
|
||||
("Cloudy", Sky(0f)))));
|
||||
|
||||
Assert.Contains("one-shot", error.Message, StringComparison.Ordinal);
|
||||
Assert.Equal(0, state.ActiveDayGroupIndex);
|
||||
}
|
||||
|
||||
private static RuntimeWorldEnvironmentState Initialized()
|
||||
{
|
||||
var state = new RuntimeWorldEnvironmentState(
|
||||
new ManualTimeProvider());
|
||||
state.Initialize(Definition(
|
||||
DerethDateTime.DayFractionOriginOffsetTicks,
|
||||
("Sunny", Sky(0f))));
|
||||
return state;
|
||||
}
|
||||
|
||||
private static RuntimeWorldEnvironmentDefinition Definition(
|
||||
double origin,
|
||||
params (string Name, SkyStateProvider Sky)[] groups) =>
|
||||
new(
|
||||
origin,
|
||||
sourceTickSize: 0.8,
|
||||
lightTickSize: 0.2,
|
||||
groups.Select(value => Group(value.Name, value.Sky)));
|
||||
|
||||
private static RuntimeWorldDayGroupDefinition Group(
|
||||
string name,
|
||||
SkyStateProvider sky) =>
|
||||
new(
|
||||
name,
|
||||
ChanceOfOccur: 100f,
|
||||
SkyObjectCount: 1,
|
||||
sky);
|
||||
|
||||
private static SkyStateProvider Sky(float begin) =>
|
||||
new(
|
||||
[
|
||||
new SkyKeyframe(
|
||||
Begin: begin,
|
||||
SunHeadingDeg: 0f,
|
||||
SunPitchDeg: 90f,
|
||||
DirColor: Vector3.One,
|
||||
DirBright: 1f,
|
||||
AmbColor: Vector3.One,
|
||||
AmbBright: 1f,
|
||||
FogColor: Vector3.Zero,
|
||||
FogDensity: 0f),
|
||||
]);
|
||||
|
||||
private sealed class ManualTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now =
|
||||
new(2026, 7, 26, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
|
||||
public void Advance(TimeSpan elapsed) => _now += elapsed;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue