refactor(runtime): own world environment state
This commit is contained in:
parent
b972f539f7
commit
902076c0a4
27 changed files with 886 additions and 295 deletions
|
|
@ -89,7 +89,7 @@ void __fastcall FUN_00501990(uint *param_1)
|
||||||
|
|
||||||
| Symbol | Bytes | Role |
|
| Symbol | Bytes | Role |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `0x6a42fdb2` | 1,782,399,410 | LCG multiplier (AC-specific, not a named-PRNG constant). |
|
| `0x6a42fdb2` | 1,782,775,218 | LCG multiplier (AC-specific, not a named-PRNG constant). |
|
||||||
| `-0x7541e9ae` | `0x8ABE1652` unsigned | LCG increment. |
|
| `-0x7541e9ae` | `0x8ABE1652` unsigned | LCG increment. |
|
||||||
| `_DAT_0079920c` | ~4.29497e9f (2^32) | Standard signed-int→unsigned-float fixup constant. |
|
| `_DAT_0079920c` | ~4.29497e9f (2^32) | Standard signed-int→unsigned-float fixup constant. |
|
||||||
| `_DAT_007c6f10` | (see §2.4) | Scale factor; interpreted as `1.0 / 2^32 ≈ 2.32830644e-10f`. Same constant is reused by all deterministic AC LCG picks (e.g. `FUN_00504060` at `chunk_00500000.c:4042` for hash-chain lookup). |
|
| `_DAT_007c6f10` | (see §2.4) | Scale factor; interpreted as `1.0 / 2^32 ≈ 2.32830644e-10f`. Same constant is reused by all deterministic AC LCG picks (e.g. `FUN_00504060` at `chunk_00500000.c:4042` for hash-chain lookup). |
|
||||||
|
|
|
||||||
|
|
@ -879,6 +879,7 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
d.Communication,
|
d.Communication,
|
||||||
d.Actions,
|
d.Actions,
|
||||||
d.PlayerController,
|
d.PlayerController,
|
||||||
|
d.WorldEnvironment.Runtime,
|
||||||
worldReveal,
|
worldReveal,
|
||||||
d.UpdateClock,
|
d.UpdateClock,
|
||||||
live.SelectionInteractions);
|
live.SelectionInteractions);
|
||||||
|
|
|
||||||
|
|
@ -116,8 +116,7 @@ public sealed class GameWindow :
|
||||||
_renderResourceLifetime = new();
|
_renderResourceLifetime = new();
|
||||||
private readonly AcDream.App.Rendering.GlConstructionCleanupLedger
|
private readonly AcDream.App.Rendering.GlConstructionCleanupLedger
|
||||||
_glConstructionCleanup = new();
|
_glConstructionCleanup = new();
|
||||||
private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment =
|
private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment;
|
||||||
new(Console.WriteLine);
|
|
||||||
private readonly GameWindowLifetime _lifetime = new();
|
private readonly GameWindowLifetime _lifetime = new();
|
||||||
private readonly DisplayFramePacingController _displayFramePacing;
|
private readonly DisplayFramePacingController _displayFramePacing;
|
||||||
private readonly RuntimeSettingsController _runtimeSettings;
|
private readonly RuntimeSettingsController _runtimeSettings;
|
||||||
|
|
@ -566,6 +565,13 @@ public sealed class GameWindow :
|
||||||
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null)
|
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null)
|
||||||
{
|
{
|
||||||
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
||||||
|
_worldEnvironment = new AcDream.App.World.WorldEnvironmentController(
|
||||||
|
new AcDream.Runtime.World.RuntimeWorldEnvironmentState(
|
||||||
|
log: Console.WriteLine,
|
||||||
|
timeSyncDiagnostic:
|
||||||
|
options.DumpSky ? Console.WriteLine : null),
|
||||||
|
options.ForcedDayGroupIndex,
|
||||||
|
Console.WriteLine);
|
||||||
_runtimeInventory = new RuntimeInventoryState(_runtimeEntityObjects);
|
_runtimeInventory = new RuntimeInventoryState(_runtimeEntityObjects);
|
||||||
_runtimeCharacter = new RuntimeCharacterState();
|
_runtimeCharacter = new RuntimeCharacterState();
|
||||||
_runtimeActions = new RuntimeActionState(
|
_runtimeActions = new RuntimeActionState(
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ internal sealed class RuntimeRenderFrameTitleFactsSource : IRenderFrameTitleFact
|
||||||
public RenderFrameTitleFacts Capture() => new(
|
public RenderFrameTitleFacts Capture() => new(
|
||||||
_world.Entities.Count,
|
_world.Entities.Count,
|
||||||
_animations.Count,
|
_animations.Count,
|
||||||
DerethDateTime.ToCalendar(_time.NowTicks),
|
_time.CurrentCalendar,
|
||||||
_time.DayFraction);
|
_time.DayFraction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
using AcDream.Runtime.Gameplay;
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
|
using AcDream.Runtime.World;
|
||||||
using AcDream.UI.Abstractions;
|
using AcDream.UI.Abstractions;
|
||||||
|
|
||||||
namespace AcDream.App.Runtime;
|
namespace AcDream.App.Runtime;
|
||||||
|
|
@ -38,6 +39,7 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
RuntimeCommunicationState communication,
|
RuntimeCommunicationState communication,
|
||||||
RuntimeActionState actions,
|
RuntimeActionState actions,
|
||||||
RuntimeLocalPlayerMovementState movement,
|
RuntimeLocalPlayerMovementState movement,
|
||||||
|
RuntimeWorldEnvironmentState environment,
|
||||||
WorldRevealCoordinator worldReveal,
|
WorldRevealCoordinator worldReveal,
|
||||||
IGameRuntimeClock clock,
|
IGameRuntimeClock clock,
|
||||||
SelectionInteractionController selection)
|
SelectionInteractionController selection)
|
||||||
|
|
@ -51,6 +53,7 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
communication,
|
communication,
|
||||||
actions,
|
actions,
|
||||||
movement,
|
movement,
|
||||||
|
environment,
|
||||||
worldReveal,
|
worldReveal,
|
||||||
clock);
|
clock);
|
||||||
entityObjects.BindEventContext(
|
entityObjects.BindEventContext(
|
||||||
|
|
@ -84,6 +87,7 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
public IRuntimeChatView Chat => _view.Chat;
|
public IRuntimeChatView Chat => _view.Chat;
|
||||||
public IRuntimeActionView Actions => _view.Actions;
|
public IRuntimeActionView Actions => _view.Actions;
|
||||||
public IRuntimeMovementView Movement => _view.Movement;
|
public IRuntimeMovementView Movement => _view.Movement;
|
||||||
|
public IRuntimeWorldEnvironmentView Environment => _view.Environment;
|
||||||
public IRuntimePortalView Portal => _view.Portal;
|
public IRuntimePortalView Portal => _view.Portal;
|
||||||
|
|
||||||
public IRuntimeSessionCommands Session => _commands;
|
public IRuntimeSessionCommands Session => _commands;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
using AcDream.Runtime.Gameplay;
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
|
using AcDream.Runtime.World;
|
||||||
|
|
||||||
namespace AcDream.App.Runtime;
|
namespace AcDream.App.Runtime;
|
||||||
|
|
||||||
|
|
@ -27,6 +28,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
private readonly IRuntimeChatView _chatView;
|
private readonly IRuntimeChatView _chatView;
|
||||||
private readonly IRuntimeActionView _actionView;
|
private readonly IRuntimeActionView _actionView;
|
||||||
private readonly IRuntimeMovementView _movementView;
|
private readonly IRuntimeMovementView _movementView;
|
||||||
|
private readonly IRuntimeWorldEnvironmentView _environmentView;
|
||||||
private readonly PortalView _portalView;
|
private readonly PortalView _portalView;
|
||||||
private bool _active = true;
|
private bool _active = true;
|
||||||
|
|
||||||
|
|
@ -39,6 +41,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
RuntimeCommunicationState communication,
|
RuntimeCommunicationState communication,
|
||||||
RuntimeActionState actions,
|
RuntimeActionState actions,
|
||||||
RuntimeLocalPlayerMovementState movement,
|
RuntimeLocalPlayerMovementState movement,
|
||||||
|
RuntimeWorldEnvironmentState environment,
|
||||||
WorldRevealCoordinator worldReveal,
|
WorldRevealCoordinator worldReveal,
|
||||||
IGameRuntimeClock clock)
|
IGameRuntimeClock clock)
|
||||||
{
|
{
|
||||||
|
|
@ -61,6 +64,8 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
actions ?? throw new ArgumentNullException(nameof(actions))).View;
|
actions ?? throw new ArgumentNullException(nameof(actions))).View;
|
||||||
_movementView = (
|
_movementView = (
|
||||||
movement ?? throw new ArgumentNullException(nameof(movement))).View;
|
movement ?? throw new ArgumentNullException(nameof(movement))).View;
|
||||||
|
_environmentView = environment
|
||||||
|
?? throw new ArgumentNullException(nameof(environment));
|
||||||
_portalView = new PortalView(
|
_portalView = new PortalView(
|
||||||
worldReveal ?? throw new ArgumentNullException(nameof(worldReveal)));
|
worldReveal ?? throw new ArgumentNullException(nameof(worldReveal)));
|
||||||
}
|
}
|
||||||
|
|
@ -102,6 +107,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
public IRuntimeChatView Chat => _chatView;
|
public IRuntimeChatView Chat => _chatView;
|
||||||
public IRuntimeActionView Actions => _actionView;
|
public IRuntimeActionView Actions => _actionView;
|
||||||
public IRuntimeMovementView Movement => _movementView;
|
public IRuntimeMovementView Movement => _movementView;
|
||||||
|
public IRuntimeWorldEnvironmentView Environment => _environmentView;
|
||||||
public IRuntimePortalView Portal => _portalView;
|
public IRuntimePortalView Portal => _portalView;
|
||||||
|
|
||||||
public RuntimeStateCheckpoint CaptureCheckpoint() =>
|
public RuntimeStateCheckpoint CaptureCheckpoint() =>
|
||||||
|
|
@ -120,6 +126,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
_chatView.Count,
|
_chatView.Count,
|
||||||
_actionView.Snapshot,
|
_actionView.Snapshot,
|
||||||
_movementView.Snapshot,
|
_movementView.Snapshot,
|
||||||
|
_environmentView.Snapshot,
|
||||||
_portalView.Snapshot);
|
_portalView.Snapshot);
|
||||||
|
|
||||||
internal void Deactivate() => _active = false;
|
internal void Deactivate() => _active = false;
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ public sealed record RuntimeOptions(
|
||||||
bool DevTools,
|
bool DevTools,
|
||||||
bool UncappedRendering,
|
bool UncappedRendering,
|
||||||
bool DumpMoveTruth,
|
bool DumpMoveTruth,
|
||||||
|
bool DumpSky,
|
||||||
bool NoAudio,
|
bool NoAudio,
|
||||||
bool EnableSkyPesDebug,
|
bool EnableSkyPesDebug,
|
||||||
int HidePartIndex,
|
int HidePartIndex,
|
||||||
|
|
@ -51,6 +52,7 @@ public sealed record RuntimeOptions(
|
||||||
bool UiProbeDump,
|
bool UiProbeDump,
|
||||||
string? UiProbeScript,
|
string? UiProbeScript,
|
||||||
string? AutomationArtifactDirectory,
|
string? AutomationArtifactDirectory,
|
||||||
|
int? ForcedDayGroupIndex,
|
||||||
float FogStartMultiplier,
|
float FogStartMultiplier,
|
||||||
float FogEndMultiplier,
|
float FogEndMultiplier,
|
||||||
ResidencyBudgetOptions ResidencyBudgets,
|
ResidencyBudgetOptions ResidencyBudgets,
|
||||||
|
|
@ -91,6 +93,7 @@ public sealed record RuntimeOptions(
|
||||||
// sole way to measure truly uncapped renderer throughput.
|
// sole way to measure truly uncapped renderer throughput.
|
||||||
UncappedRendering: IsExactlyOne(env("ACDREAM_UNCAPPED_RENDER")),
|
UncappedRendering: IsExactlyOne(env("ACDREAM_UNCAPPED_RENDER")),
|
||||||
DumpMoveTruth: IsExactlyOne(env("ACDREAM_DUMP_MOVE_TRUTH")),
|
DumpMoveTruth: IsExactlyOne(env("ACDREAM_DUMP_MOVE_TRUTH")),
|
||||||
|
DumpSky: IsExactlyOne(env("ACDREAM_DUMP_SKY")),
|
||||||
NoAudio: IsExactlyOne(env("ACDREAM_NO_AUDIO")),
|
NoAudio: IsExactlyOne(env("ACDREAM_NO_AUDIO")),
|
||||||
EnableSkyPesDebug: IsExactlyOne(env("ACDREAM_ENABLE_SKY_PES")),
|
EnableSkyPesDebug: IsExactlyOne(env("ACDREAM_ENABLE_SKY_PES")),
|
||||||
HidePartIndex: TryParseInt(env("ACDREAM_HIDE_PART")) ?? -1,
|
HidePartIndex: TryParseInt(env("ACDREAM_HIDE_PART")) ?? -1,
|
||||||
|
|
@ -110,6 +113,8 @@ public sealed record RuntimeOptions(
|
||||||
UiProbeScript: NullIfEmpty(env("ACDREAM_UI_PROBE_SCRIPT")),
|
UiProbeScript: NullIfEmpty(env("ACDREAM_UI_PROBE_SCRIPT")),
|
||||||
AutomationArtifactDirectory:
|
AutomationArtifactDirectory:
|
||||||
NullIfEmpty(env("ACDREAM_AUTOMATION_ARTIFACT_DIR")),
|
NullIfEmpty(env("ACDREAM_AUTOMATION_ARTIFACT_DIR")),
|
||||||
|
ForcedDayGroupIndex:
|
||||||
|
TryParseNonNegativeInt(env("ACDREAM_DAY_GROUP")),
|
||||||
FogStartMultiplier: TryParseFloat(env("ACDREAM_FOG_START_MULT")) ?? 0.7f,
|
FogStartMultiplier: TryParseFloat(env("ACDREAM_FOG_START_MULT")) ?? 0.7f,
|
||||||
FogEndMultiplier: TryParseFloat(env("ACDREAM_FOG_END_MULT")) ?? 0.95f,
|
FogEndMultiplier: TryParseFloat(env("ACDREAM_FOG_END_MULT")) ?? 0.95f,
|
||||||
ResidencyBudgets: ResidencyBudgetOptions.Parse(env),
|
ResidencyBudgets: ResidencyBudgetOptions.Parse(env),
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,59 @@
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.World;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
namespace AcDream.App.World;
|
namespace AcDream.App.World;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Owns the live Dereth clock, selected retail day group, weather state, and
|
/// Graphical DAT/presentation adapter over the one
|
||||||
/// server environment overrides as one coherent environment lifetime.
|
/// <see cref="RuntimeWorldEnvironmentState"/> owner. Runtime owns clock,
|
||||||
|
/// weather, day selection, and server overrides; App retains raw sky DAT
|
||||||
|
/// objects because they are renderer inputs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class WorldEnvironmentController : IWorldSceneSkyStateSource
|
internal sealed class WorldEnvironmentController : IWorldSceneSkyStateSource
|
||||||
{
|
{
|
||||||
private static readonly WeatherKind[] DebugWeatherKinds =
|
|
||||||
[
|
|
||||||
WeatherKind.Clear,
|
|
||||||
WeatherKind.Overcast,
|
|
||||||
WeatherKind.Rain,
|
|
||||||
WeatherKind.Snow,
|
|
||||||
WeatherKind.Storm,
|
|
||||||
];
|
|
||||||
|
|
||||||
private readonly Action<string> _log;
|
private readonly Action<string> _log;
|
||||||
|
private readonly int? _forcedDayGroupIndex;
|
||||||
private LoadedSkyDesc? _loadedSkyDesc;
|
private LoadedSkyDesc? _loadedSkyDesc;
|
||||||
private long _loadedSkyDayIndex = long.MinValue;
|
|
||||||
private int _timeDebugStep;
|
|
||||||
private int _weatherDebugStep;
|
|
||||||
private bool _initializationClaimed;
|
|
||||||
|
|
||||||
public WorldEnvironmentController(Action<string>? log = null)
|
public WorldEnvironmentController(Action<string>? log = null)
|
||||||
: this(
|
: this(
|
||||||
new WorldTimeService(SkyStateProvider.Default()),
|
new RuntimeWorldEnvironmentState(log: log),
|
||||||
new WeatherSystem(),
|
forcedDayGroupIndex: null,
|
||||||
log)
|
log)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
internal WorldEnvironmentController(
|
internal WorldEnvironmentController(
|
||||||
WorldTimeService worldTime,
|
RuntimeWorldEnvironmentState runtime,
|
||||||
WeatherSystem weather,
|
int? forcedDayGroupIndex = null,
|
||||||
Action<string>? log = null)
|
Action<string>? log = null)
|
||||||
{
|
{
|
||||||
WorldTime = worldTime ?? throw new ArgumentNullException(nameof(worldTime));
|
Runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
Weather = weather ?? throw new ArgumentNullException(nameof(weather));
|
_forcedDayGroupIndex = forcedDayGroupIndex;
|
||||||
_log = log ?? (_ => { });
|
_log = log ?? (_ => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
public WorldTimeService WorldTime { get; }
|
public RuntimeWorldEnvironmentState Runtime { get; }
|
||||||
|
public WorldTimeService WorldTime => Runtime.WorldTime;
|
||||||
|
public WeatherSystem Weather => Runtime.Weather;
|
||||||
|
|
||||||
public WeatherSystem Weather { get; }
|
public DayGroupData? ActiveDayGroup
|
||||||
|
{
|
||||||
public DayGroupData? ActiveDayGroup { get; private set; }
|
get
|
||||||
|
{
|
||||||
|
int index = Runtime.ActiveDayGroupIndex;
|
||||||
|
return _loadedSkyDesc is not null
|
||||||
|
&& index >= 0
|
||||||
|
&& index < _loadedSkyDesc.DayGroups.Count
|
||||||
|
? _loadedSkyDesc.DayGroups[index]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public float DayFraction => (float)WorldTime.DayFraction;
|
public float DayFraction => (float)WorldTime.DayFraction;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads the Region DAT environment and seeds the pre-session clock exactly
|
|
||||||
/// as the former <c>GameWindow.OnLoad</c> body did.
|
|
||||||
/// </summary>
|
|
||||||
public void Initialize(Region region)
|
public void Initialize(Region region)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(region);
|
ArgumentNullException.ThrowIfNull(region);
|
||||||
|
|
@ -68,182 +66,52 @@ internal sealed class WorldEnvironmentController : IWorldSceneSkyStateSource
|
||||||
LoadedSkyDesc? loadedSkyDesc,
|
LoadedSkyDesc? loadedSkyDesc,
|
||||||
double? zeroTimeOfYear)
|
double? zeroTimeOfYear)
|
||||||
{
|
{
|
||||||
if (_initializationClaimed)
|
if (Runtime.IsInitialized)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"The world environment is a one-shot GameWindow lifetime owner.");
|
"The world environment is a one-shot Runtime lifetime owner.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_initializationClaimed = true;
|
|
||||||
_loadedSkyDesc = loadedSkyDesc;
|
|
||||||
_loadedSkyDayIndex = long.MinValue;
|
|
||||||
ActiveDayGroup = null;
|
|
||||||
|
|
||||||
// Region GameTime is authoritative when present. A Region without it
|
|
||||||
// gets the documented offline fallback rather than inheriting mutable
|
|
||||||
// process-global state from an earlier window or test lifetime.
|
|
||||||
double origin = zeroTimeOfYear
|
double origin = zeroTimeOfYear
|
||||||
?? DerethDateTime.DayFractionOriginOffsetTicks;
|
?? DerethDateTime.DayFractionOriginOffsetTicks;
|
||||||
DerethDateTime.SetOriginOffsetFromDat(origin);
|
|
||||||
|
|
||||||
if (_loadedSkyDesc is not null)
|
|
||||||
{
|
|
||||||
// SkyDesc.TickSize is retail's next-sky-update throttle, not the
|
|
||||||
// rate of PortalYearTicks. ACE advances the server clock at one
|
|
||||||
// tick per real second, so client extrapolation must remain 1.0.
|
|
||||||
WorldTime.TickSize = 1.0;
|
|
||||||
|
|
||||||
if (zeroTimeOfYear.HasValue)
|
if (zeroTimeOfYear.HasValue)
|
||||||
{
|
{
|
||||||
_log(
|
_log(
|
||||||
$"sky: GameTime ZeroTimeOfYear={zeroTimeOfYear.Value} " +
|
$"sky: GameTime ZeroTimeOfYear={zeroTimeOfYear.Value} "
|
||||||
$"(was default {DerethDateTime.DayFractionOriginOffsetTicks})");
|
+ $"(was default {DerethDateTime.DayFractionOriginOffsetTicks})");
|
||||||
}
|
}
|
||||||
|
|
||||||
_log(
|
RuntimeWorldDayGroupDefinition[] groups =
|
||||||
$"sky: loaded Region 0x13000000 — {_loadedSkyDesc.DayGroups.Count} day groups, " +
|
loadedSkyDesc?.DayGroups.Select(
|
||||||
$"SkyDesc.TickSize={_loadedSkyDesc.TickSize} (throttle, not rate), " +
|
group => new RuntimeWorldDayGroupDefinition(
|
||||||
$"LightTickSize={_loadedSkyDesc.LightTickSize}");
|
group.Name,
|
||||||
|
group.ChanceOfOccur,
|
||||||
// The initial roll intentionally precedes the offline noon seed,
|
group.SkyObjects.Count,
|
||||||
// preserving the accepted OnLoad order. The first server sync
|
|
||||||
// below will select the authoritative day.
|
|
||||||
RefreshSkyForCurrentDay();
|
|
||||||
}
|
|
||||||
|
|
||||||
WorldTime.SyncFromServer(DerethDateTime.DayTicks / 16.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SynchronizeFromServer(double ticks)
|
|
||||||
{
|
|
||||||
WorldTime.SyncFromServer(ticks);
|
|
||||||
RefreshSkyForCurrentDay();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Environment packet bridge researched from
|
|
||||||
/// <c>CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20</c>. The current
|
|
||||||
/// fog approximation and missing centered UI-sound playback remain
|
|
||||||
/// explicitly registered as TS-55 and TS-54 respectively.
|
|
||||||
/// </summary>
|
|
||||||
public void ApplyAdminEnvirons(uint environChangeType)
|
|
||||||
{
|
|
||||||
if (environChangeType <= 0x06u)
|
|
||||||
{
|
|
||||||
Weather.Override = (EnvironOverride)environChangeType;
|
|
||||||
_log(
|
|
||||||
$"live: AdminEnvirons fog override = " +
|
|
||||||
$"{(EnvironOverride)environChangeType}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string name = environChangeType switch
|
|
||||||
{
|
|
||||||
0x65u => "RoarSound",
|
|
||||||
0x66u => "BellSound",
|
|
||||||
0x67u => "Chant1Sound",
|
|
||||||
0x68u => "Chant2Sound",
|
|
||||||
0x69u => "DarkWhispers1Sound",
|
|
||||||
0x6Au => "DarkWhispers2Sound",
|
|
||||||
0x6Bu => "DarkLaughSound",
|
|
||||||
0x6Cu => "DarkWindSound",
|
|
||||||
0x6Du => "DarkSpeechSound",
|
|
||||||
0x6Eu => "DrumsSound",
|
|
||||||
0x6Fu => "GhostSpeakSound",
|
|
||||||
0x70u => "BreathingSound",
|
|
||||||
0x71u => "HowlSound",
|
|
||||||
0x72u => "LostSoulsSound",
|
|
||||||
0x75u => "SquealSound",
|
|
||||||
0x76u => "Thunder1Sound",
|
|
||||||
0x77u => "Thunder2Sound",
|
|
||||||
0x78u => "Thunder3Sound",
|
|
||||||
0x79u => "Thunder4Sound",
|
|
||||||
0x7Au => "Thunder5Sound",
|
|
||||||
0x7Bu => "Thunder6Sound",
|
|
||||||
_ => $"Unknown(0x{environChangeType:X2})",
|
|
||||||
};
|
|
||||||
|
|
||||||
_log(
|
|
||||||
$"live: AdminEnvirons sound cue = {name} " +
|
|
||||||
$"(0x{environChangeType:X2}) — audio binding pending");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Selects the active DAT day group when the authoritative Dereth day
|
|
||||||
/// changes. The picker itself is the retail-verbatim
|
|
||||||
/// <c>SkyDesc::CalcPresentDayGroup @ 0x00500E10</c> port (older-build
|
|
||||||
/// cross-reference <c>FUN_00501990</c>).
|
|
||||||
/// </summary>
|
|
||||||
public void RefreshSkyForCurrentDay()
|
|
||||||
{
|
|
||||||
if (_loadedSkyDesc is null || _loadedSkyDesc.DayGroups.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
double ticks = WorldTime.NowTicks;
|
|
||||||
int absYear = DerethDateTime.AbsoluteYear(ticks);
|
|
||||||
int dayOfYear = DerethDateTime.DayOfYear(ticks);
|
|
||||||
int daysPerYear = DerethDateTime.DaysInAMonth
|
|
||||||
* DerethDateTime.MonthsInAYear;
|
|
||||||
long dayIndex = (long)absYear * 360 + dayOfYear;
|
|
||||||
|
|
||||||
int idx = _loadedSkyDesc.SelectDayGroupIndex(
|
|
||||||
absYear,
|
|
||||||
daysPerYear,
|
|
||||||
dayOfYear);
|
|
||||||
DayGroupData? group = idx >= 0 && idx < _loadedSkyDesc.DayGroups.Count
|
|
||||||
? _loadedSkyDesc.DayGroups[idx]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
bool dayChanged = dayIndex != _loadedSkyDayIndex;
|
|
||||||
bool groupChanged = !ReferenceEquals(group, ActiveDayGroup);
|
|
||||||
if (!dayChanged && !groupChanged)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_loadedSkyDayIndex = dayIndex;
|
|
||||||
ActiveDayGroup = group;
|
|
||||||
|
|
||||||
if (group is null || group.SkyTimes.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
WorldTime.SetProvider(
|
|
||||||
new SkyStateProvider(
|
new SkyStateProvider(
|
||||||
group.SkyTimes.Select(s => s.Keyframe).ToList()));
|
group.SkyTimes.Select(value => value.Keyframe).ToList())))
|
||||||
Weather.SetKindFromDayGroupName(group.Name);
|
.ToArray()
|
||||||
|
?? [];
|
||||||
|
|
||||||
_log(
|
var definition = new RuntimeWorldEnvironmentDefinition(
|
||||||
$"sky: PY{absYear} day{dayOfYear} → DayGroup[{idx}] \"{group.Name}\" " +
|
origin,
|
||||||
$"(Chance={group.ChanceOfOccur:F2}, {group.SkyObjects.Count} objects, " +
|
loadedSkyDesc?.TickSize ?? 1.0,
|
||||||
$"{group.SkyTimes.Count} keyframes, weather={Weather.Kind})");
|
loadedSkyDesc?.LightTickSize ?? 1.0,
|
||||||
|
groups,
|
||||||
|
_forcedDayGroupIndex);
|
||||||
|
|
||||||
|
Runtime.Initialize(definition);
|
||||||
|
_loadedSkyDesc = loadedSkyDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CycleTimeOfDay()
|
public void SynchronizeFromServer(double ticks) =>
|
||||||
{
|
Runtime.SynchronizeFromServer(ticks);
|
||||||
_timeDebugStep = (_timeDebugStep + 1) % 5;
|
|
||||||
float? selection = _timeDebugStep switch
|
|
||||||
{
|
|
||||||
0 => null,
|
|
||||||
1 => 0.0f,
|
|
||||||
2 => 0.25f,
|
|
||||||
3 => 0.5f,
|
|
||||||
4 => 0.75f,
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (selection.HasValue)
|
public void ApplyAdminEnvirons(uint environChangeType) =>
|
||||||
{
|
_ = Runtime.ApplyAdminEnvirons(environChangeType);
|
||||||
WorldTime.SetDebugTime(selection.Value);
|
|
||||||
return $"Time override = {selection.Value:F2}";
|
|
||||||
}
|
|
||||||
|
|
||||||
WorldTime.ClearDebugTime();
|
public void RefreshSkyForCurrentDay() => Runtime.RefreshDayGroup();
|
||||||
return "Time override cleared";
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CycleWeather()
|
public string CycleTimeOfDay() => Runtime.CycleTimeOfDay();
|
||||||
{
|
|
||||||
_weatherDebugStep = (_weatherDebugStep + 1) % DebugWeatherKinds.Length;
|
public string CycleWeather() => Runtime.CycleWeather();
|
||||||
WeatherKind kind = DebugWeatherKinds[_weatherDebugStep];
|
|
||||||
Weather.ForceWeather(kind);
|
|
||||||
return $"Weather = {kind}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
103
src/AcDream.Core/World/DerethCalendar.cs
Normal file
103
src/AcDream.Core/World/DerethCalendar.cs
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
namespace AcDream.Core.World;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instance-scoped Dereth calendar projection. Retail stores the Region
|
||||||
|
/// <c>GameTime.ZeroTimeOfYear</c> beside the active world rather than in a
|
||||||
|
/// process-global calendar. Keeping that origin on this object lets multiple
|
||||||
|
/// Runtime sessions use different Region definitions without corrupting one
|
||||||
|
/// another.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DerethCalendar
|
||||||
|
{
|
||||||
|
public DerethCalendar(
|
||||||
|
double originOffsetTicks = DerethDateTime.DayFractionOriginOffsetTicks)
|
||||||
|
{
|
||||||
|
SetOriginOffset(originOffsetTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double OriginOffsetTicks { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs the Region DAT origin for this world lifetime.
|
||||||
|
/// </summary>
|
||||||
|
public void SetOriginOffset(double originOffsetTicks)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(originOffsetTicks))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(originOffsetTicks));
|
||||||
|
|
||||||
|
OriginOffsetTicks = originOffsetTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double DayFraction(double ticks)
|
||||||
|
{
|
||||||
|
double shifted = Shift(ticks);
|
||||||
|
double rem = shifted
|
||||||
|
- Math.Floor(shifted / DerethDateTime.DayTicks)
|
||||||
|
* DerethDateTime.DayTicks;
|
||||||
|
return rem / DerethDateTime.DayTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DerethDateTime.HourName CurrentHour(double ticks)
|
||||||
|
{
|
||||||
|
int slot = (int)Math.Floor(
|
||||||
|
DayFraction(ticks) * DerethDateTime.HoursInADay);
|
||||||
|
slot = Math.Clamp(slot, 0, DerethDateTime.HoursInADay - 1);
|
||||||
|
return (DerethDateTime.HourName)slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDaytime(double ticks)
|
||||||
|
{
|
||||||
|
int hour = (int)CurrentHour(ticks);
|
||||||
|
return hour >= (int)DerethDateTime.HourName.Dawnsong
|
||||||
|
&& hour <= (int)DerethDateTime.HourName.WarmtideAndHalf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DerethDateTime.Calendar ToCalendar(double ticks)
|
||||||
|
{
|
||||||
|
double shifted = Shift(ticks);
|
||||||
|
int relativeYear = (int)(shifted / DerethDateTime.YearTicks);
|
||||||
|
double withinYear =
|
||||||
|
shifted - relativeYear * DerethDateTime.YearTicks;
|
||||||
|
int month = Math.Min(
|
||||||
|
DerethDateTime.MonthsInAYear - 1,
|
||||||
|
(int)(withinYear / DerethDateTime.MonthTicks));
|
||||||
|
double withinMonth =
|
||||||
|
withinYear - month * DerethDateTime.MonthTicks;
|
||||||
|
int day = Math.Min(
|
||||||
|
DerethDateTime.DaysInAMonth,
|
||||||
|
(int)(withinMonth / DerethDateTime.DayTicks) + 1);
|
||||||
|
|
||||||
|
return new DerethDateTime.Calendar(
|
||||||
|
relativeYear + DerethDateTime.ZeroYear,
|
||||||
|
(DerethDateTime.MonthName)month,
|
||||||
|
day,
|
||||||
|
CurrentHour(ticks));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Year(double ticks) =>
|
||||||
|
(int)(Shift(ticks) / DerethDateTime.YearTicks);
|
||||||
|
|
||||||
|
public int AbsoluteYear(double ticks) =>
|
||||||
|
Year(ticks) + DerethDateTime.ZeroYear;
|
||||||
|
|
||||||
|
public int DayOfYear(double ticks)
|
||||||
|
{
|
||||||
|
double shifted = Shift(ticks);
|
||||||
|
int year = (int)(shifted / DerethDateTime.YearTicks);
|
||||||
|
double withinYear =
|
||||||
|
shifted - year * DerethDateTime.YearTicks;
|
||||||
|
return Math.Clamp(
|
||||||
|
(int)(withinYear / DerethDateTime.DayTicks),
|
||||||
|
0,
|
||||||
|
DerethDateTime.DaysInAMonth
|
||||||
|
* DerethDateTime.MonthsInAYear
|
||||||
|
- 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double Shift(double ticks)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(ticks))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(ticks));
|
||||||
|
return Math.Max(0d, ticks) + OriginOffsetTicks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -123,18 +123,17 @@ public static class DerethDateTime
|
||||||
/// Morntide-and-Half". This is a fallback — retail reads
|
/// Morntide-and-Half". This is a fallback — retail reads
|
||||||
/// <c>GameTime.ZeroTimeOfYear</c> from the Region dat (verified
|
/// <c>GameTime.ZeroTimeOfYear</c> from the Region dat (verified
|
||||||
/// <c>3600</c> in Dereth, 2026-04-23 live dump) and uses that as
|
/// <c>3600</c> in Dereth, 2026-04-23 live dump) and uses that as
|
||||||
/// the additive offset. We override <see cref="OriginOffsetTicks"/>
|
/// the additive offset. Production installs that value on its
|
||||||
/// once the dat loads; this constant is only for offline tests.
|
/// instance <see cref="DerethCalendar"/>; this constant remains the
|
||||||
|
/// offline/static-helper default.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const double DayFractionOriginOffsetTicks = (7.0 / 16.0) * DayTicks; // 3333.75
|
public const double DayFractionOriginOffsetTicks = (7.0 / 16.0) * DayTicks; // 3333.75
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Additive tick offset applied before every calendar extraction
|
/// Compatibility origin used by the pure static helpers. Production
|
||||||
/// (DayFraction / Year / DayOfYear / AbsoluteYear). Populated from
|
/// worlds use an instance <see cref="DerethCalendar"/> carrying their
|
||||||
/// the Region dat's <c>GameTime.ZeroTimeOfYear</c> via
|
/// Region DAT origin, so multiple sessions never share mutable calendar
|
||||||
/// <see cref="SetOriginOffsetFromDat"/> at Region load. Defaults to
|
/// state.
|
||||||
/// <see cref="DayFractionOriginOffsetTicks"/> for offline tests and
|
|
||||||
/// for the boot window before the dat parses.
|
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Live Dereth dat value: <c>3600</c>. Retail's
|
/// Live Dereth dat value: <c>3600</c>. Retail's
|
||||||
|
|
@ -148,17 +147,7 @@ public static class DerethDateTime
|
||||||
/// corrected and broke DG selection; reverted in the same commit.)
|
/// corrected and broke DG selection; reverted in the same commit.)
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static double OriginOffsetTicks { get; private set; } = DayFractionOriginOffsetTicks;
|
public const double OriginOffsetTicks = DayFractionOriginOffsetTicks;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adopt the Region dat's <c>GameTime.ZeroTimeOfYear</c> as the
|
|
||||||
/// calendar-extraction offset. Idempotent; safe to call on every
|
|
||||||
/// Region reload (though in practice Dereth is the only region).
|
|
||||||
/// </summary>
|
|
||||||
public static void SetOriginOffsetFromDat(double zeroTimeOfYear)
|
|
||||||
{
|
|
||||||
OriginOffsetTicks = zeroTimeOfYear;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Day fraction [0, 1): 0 = Darktide (midnight), 0.5 =
|
/// Day fraction [0, 1): 0 = Darktide (midnight), 0.5 =
|
||||||
|
|
|
||||||
40
src/AcDream.Core/World/SkyDayGroupSelector.cs
Normal file
40
src/AcDream.Core/World/SkyDayGroupSelector.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
namespace AcDream.Core.World;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retail-verbatim day-group selection from
|
||||||
|
/// <c>SkyDesc::CalcPresentDayGroup @ 0x00500E10</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static class SkyDayGroupSelector
|
||||||
|
{
|
||||||
|
public static int SelectIndex(
|
||||||
|
int dayGroupCount,
|
||||||
|
int absoluteYear,
|
||||||
|
int daysPerYear,
|
||||||
|
int dayOfYear,
|
||||||
|
int? forcedIndex = null)
|
||||||
|
{
|
||||||
|
if (dayGroupCount <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (forcedIndex is >= 0 && forcedIndex < dayGroupCount)
|
||||||
|
return forcedIndex.Value;
|
||||||
|
|
||||||
|
if (dayGroupCount == 1)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int seed = unchecked(absoluteYear * daysPerYear + dayOfYear);
|
||||||
|
int mixed = unchecked(
|
||||||
|
seed * 0x6A42FDB2 + unchecked((int)0x8ABE1652));
|
||||||
|
|
||||||
|
float hash = mixed;
|
||||||
|
if (mixed < 0)
|
||||||
|
hash += 4294967296.0f;
|
||||||
|
|
||||||
|
const float inverseTwoTo32 = 1.0f / 4294967296.0f;
|
||||||
|
int index = (int)MathF.Floor(
|
||||||
|
(float)dayGroupCount * hash * inverseTwoTo32);
|
||||||
|
|
||||||
|
// Retail snaps the floating-point upper-edge overflow to zero.
|
||||||
|
return index < 0 || index >= dayGroupCount ? 0 : index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -221,48 +221,22 @@ public sealed class LoadedSkyDesc
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int SelectDayGroupIndex(int year, int secondsPerDay, int dayOfYear)
|
public int SelectDayGroupIndex(int year, int secondsPerDay, int dayOfYear)
|
||||||
{
|
{
|
||||||
if (DayGroups.Count == 0) return 0;
|
|
||||||
|
|
||||||
// Env-var override has absolute priority.
|
// Env-var override has absolute priority.
|
||||||
var env = System.Environment.GetEnvironmentVariable("ACDREAM_DAY_GROUP");
|
var env = System.Environment.GetEnvironmentVariable("ACDREAM_DAY_GROUP");
|
||||||
|
int? forcedIndex = null;
|
||||||
if (int.TryParse(env, System.Globalization.NumberStyles.Integer,
|
if (int.TryParse(env, System.Globalization.NumberStyles.Integer,
|
||||||
System.Globalization.CultureInfo.InvariantCulture, out var forced)
|
System.Globalization.CultureInfo.InvariantCulture, out var forced)
|
||||||
&& forced >= 0 && forced < DayGroups.Count)
|
&& forced >= 0 && forced < DayGroups.Count)
|
||||||
{
|
{
|
||||||
return forced;
|
forcedIndex = forced;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DayGroups.Count == 1) return 0;
|
return SkyDayGroupSelector.SelectIndex(
|
||||||
|
DayGroups.Count,
|
||||||
// --- Retail SkyDesc::CalcPresentDayGroup @ 0x00500E10 ---
|
year,
|
||||||
|
secondsPerDay,
|
||||||
// Step 1: deterministic per-day seed.
|
dayOfYear,
|
||||||
int seed = unchecked(year * secondsPerDay + dayOfYear);
|
forcedIndex);
|
||||||
|
|
||||||
// Step 2: 32-bit signed LCG (retail uses x86 silent wrap; force
|
|
||||||
// unchecked in C#). `0x8ABE1652` is stored as `-0x7541E9AE` in the
|
|
||||||
// decompile — same bit pattern.
|
|
||||||
int mixed = unchecked(seed * 0x6A42FDB2 + unchecked((int)0x8ABE1652));
|
|
||||||
|
|
||||||
// Step 3: signed-int → float with +2^32 fixup for negative values
|
|
||||||
// (retail x87 converts `int` to `float` and then adds
|
|
||||||
// `_DAT_0079920c` ≈ 4294967296.0f when `iVar4 < 0`).
|
|
||||||
float hashF = (float)mixed;
|
|
||||||
if (mixed < 0) hashF += 4294967296.0f;
|
|
||||||
|
|
||||||
// Step 4: scale to [0, dayGroupCount). `_DAT_007c6f10` is
|
|
||||||
// `1.0f / 2^32` per reuse pattern (see research doc §2.4).
|
|
||||||
const float kInv2Pow32 = 1.0f / 4294967296.0f;
|
|
||||||
float countF = (float)DayGroups.Count;
|
|
||||||
int index = (int)System.MathF.Floor(countF * hashF * kInv2Pow32);
|
|
||||||
|
|
||||||
// Step 5: safety clamp for float rounding at the upper edge.
|
|
||||||
// Retail does `if (count <= index) index = 0;`. Using the same
|
|
||||||
// "snap to 0 on overflow" instead of clamping to count-1.
|
|
||||||
if (index >= DayGroups.Count) index = 0;
|
|
||||||
if (index < 0) index = 0;
|
|
||||||
|
|
||||||
return index;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -372,7 +372,9 @@ public sealed class WorldTimeService
|
||||||
{
|
{
|
||||||
private SkyStateProvider _sky;
|
private SkyStateProvider _sky;
|
||||||
private double _lastSyncedTicks;
|
private double _lastSyncedTicks;
|
||||||
private DateTime _lastSyncedWallClockUtc = DateTime.UtcNow;
|
private DateTimeOffset _lastSyncedWallClockUtc;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
private readonly Action<string>? _synchronizationDiagnostic;
|
||||||
|
|
||||||
private float? _debugDayFractionOverride;
|
private float? _debugDayFractionOverride;
|
||||||
|
|
||||||
|
|
@ -384,10 +386,25 @@ public sealed class WorldTimeService
|
||||||
public double TickSize { get; set; } = 1.0;
|
public double TickSize { get; set; } = 1.0;
|
||||||
|
|
||||||
public WorldTimeService(SkyStateProvider sky)
|
public WorldTimeService(SkyStateProvider sky)
|
||||||
|
: this(sky, new DerethCalendar(), TimeProvider.System)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldTimeService(
|
||||||
|
SkyStateProvider sky,
|
||||||
|
DerethCalendar calendar,
|
||||||
|
TimeProvider? timeProvider = null,
|
||||||
|
Action<string>? synchronizationDiagnostic = null)
|
||||||
{
|
{
|
||||||
_sky = sky ?? throw new ArgumentNullException(nameof(sky));
|
_sky = sky ?? throw new ArgumentNullException(nameof(sky));
|
||||||
|
Calendar = calendar ?? throw new ArgumentNullException(nameof(calendar));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
_synchronizationDiagnostic = synchronizationDiagnostic;
|
||||||
|
_lastSyncedWallClockUtc = _timeProvider.GetUtcNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DerethCalendar Calendar { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hot-swap the keyframe source — typically called once at world-load
|
/// Hot-swap the keyframe source — typically called once at world-load
|
||||||
/// time after the Region dat has been parsed by <see cref="SkyDescLoader"/>.
|
/// time after the Region dat has been parsed by <see cref="SkyDescLoader"/>.
|
||||||
|
|
@ -404,14 +421,14 @@ public sealed class WorldTimeService
|
||||||
public void SyncFromServer(double serverTicks)
|
public void SyncFromServer(double serverTicks)
|
||||||
{
|
{
|
||||||
_lastSyncedTicks = serverTicks;
|
_lastSyncedTicks = serverTicks;
|
||||||
_lastSyncedWallClockUtc = System.DateTime.UtcNow;
|
_lastSyncedWallClockUtc = _timeProvider.GetUtcNow();
|
||||||
_debugDayFractionOverride = null;
|
_debugDayFractionOverride = null;
|
||||||
|
|
||||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1")
|
if (_synchronizationDiagnostic is not null)
|
||||||
{
|
{
|
||||||
var df = DerethDateTime.DayFraction(serverTicks);
|
var df = Calendar.DayFraction(serverTicks);
|
||||||
var cal = DerethDateTime.ToCalendar(serverTicks);
|
var cal = Calendar.ToCalendar(serverTicks);
|
||||||
System.Console.WriteLine(
|
_synchronizationDiagnostic(
|
||||||
$"[sky-dump] SyncFromServer: ticks={serverTicks:F1} dayFraction={df:F4} " +
|
$"[sky-dump] SyncFromServer: ticks={serverTicks:F1} dayFraction={df:F4} " +
|
||||||
$"calendar=PY{cal.Year} {cal.Month} {cal.Day} {cal.Hour}");
|
$"calendar=PY{cal.Year} {cal.Month} {cal.Day} {cal.Hour}");
|
||||||
}
|
}
|
||||||
|
|
@ -430,14 +447,17 @@ public sealed class WorldTimeService
|
||||||
public void ClearDebugTime() => _debugDayFractionOverride = null;
|
public void ClearDebugTime() => _debugDayFractionOverride = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Current ticks at <see cref="DateTime.UtcNow"/>, advanced from the
|
/// Current ticks at the injected <see cref="TimeProvider"/>'s UTC time,
|
||||||
/// last sync by real-time elapsed seconds times <see cref="TickSize"/>.
|
/// advanced from the last sync by real-time elapsed seconds times
|
||||||
|
/// <see cref="TickSize"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double NowTicks
|
public double NowTicks
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
double elapsed = (DateTime.UtcNow - _lastSyncedWallClockUtc).TotalSeconds;
|
double elapsed =
|
||||||
|
(_timeProvider.GetUtcNow() - _lastSyncedWallClockUtc)
|
||||||
|
.TotalSeconds;
|
||||||
return _lastSyncedTicks + elapsed * TickSize;
|
return _lastSyncedTicks + elapsed * TickSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -449,7 +469,7 @@ public sealed class WorldTimeService
|
||||||
{
|
{
|
||||||
if (_debugDayFractionOverride.HasValue)
|
if (_debugDayFractionOverride.HasValue)
|
||||||
return _debugDayFractionOverride.Value;
|
return _debugDayFractionOverride.Value;
|
||||||
return DerethDateTime.DayFraction(NowTicks);
|
return Calendar.DayFraction(NowTicks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -461,7 +481,7 @@ public sealed class WorldTimeService
|
||||||
SkyStateProvider.SunDirectionFromKeyframe(CurrentSky);
|
SkyStateProvider.SunDirectionFromKeyframe(CurrentSky);
|
||||||
|
|
||||||
public DerethDateTime.Calendar CurrentCalendar =>
|
public DerethDateTime.Calendar CurrentCalendar =>
|
||||||
DerethDateTime.ToCalendar(NowTicks);
|
Calendar.ToCalendar(NowTicks);
|
||||||
|
|
||||||
public bool IsDaytime => DerethDateTime.IsDaytime(NowTicks);
|
public bool IsDaytime => Calendar.IsDaytime(NowTicks);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,10 @@ public sealed class RuntimeTraceRecorder : IRuntimeEventObserver
|
||||||
$"{checkpoint.Actions.Magic.Revision}:" +
|
$"{checkpoint.Actions.Magic.Revision}:" +
|
||||||
$"{checkpoint.Actions.Magic.LastRequestedSpellId:X8}:" +
|
$"{checkpoint.Actions.Magic.LastRequestedSpellId:X8}:" +
|
||||||
$"{checkpoint.Actions.Magic.LastRequestedTargetId:X8};" +
|
$"{checkpoint.Actions.Magic.LastRequestedTargetId:X8};" +
|
||||||
|
$"environment={checkpoint.Environment.Revision}:" +
|
||||||
|
$"{checkpoint.Environment.ActiveDayGroupIndex}:" +
|
||||||
|
$"{(int)checkpoint.Environment.Weather}:" +
|
||||||
|
$"{(int)checkpoint.Environment.EnvironOverride};" +
|
||||||
$"portal={checkpoint.Portal.Generation}:{(int)checkpoint.Portal.Kind}:" +
|
$"portal={checkpoint.Portal.Generation}:{(int)checkpoint.Portal.Kind}:" +
|
||||||
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady}")));
|
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady}")));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Runtime.World;
|
||||||
|
|
||||||
namespace AcDream.Runtime;
|
namespace AcDream.Runtime;
|
||||||
|
|
||||||
|
|
@ -121,6 +122,7 @@ public readonly record struct RuntimeStateCheckpoint(
|
||||||
int ChatCount,
|
int ChatCount,
|
||||||
RuntimeActionSnapshot Actions,
|
RuntimeActionSnapshot Actions,
|
||||||
RuntimeMovementSnapshot Movement,
|
RuntimeMovementSnapshot Movement,
|
||||||
|
RuntimeWorldEnvironmentSnapshot Environment,
|
||||||
RuntimePortalSnapshot Portal);
|
RuntimePortalSnapshot Portal);
|
||||||
|
|
||||||
public interface IGameRuntimeView
|
public interface IGameRuntimeView
|
||||||
|
|
@ -147,6 +149,8 @@ public interface IGameRuntimeView
|
||||||
|
|
||||||
IRuntimeMovementView Movement { get; }
|
IRuntimeMovementView Movement { get; }
|
||||||
|
|
||||||
|
IRuntimeWorldEnvironmentView Environment { get; }
|
||||||
|
|
||||||
IRuntimePortalView Portal { get; }
|
IRuntimePortalView Portal { get; }
|
||||||
|
|
||||||
RuntimeStateCheckpoint CaptureCheckpoint();
|
RuntimeStateCheckpoint CaptureCheckpoint();
|
||||||
|
|
|
||||||
313
src/AcDream.Runtime/World/RuntimeWorldEnvironmentState.cs
Normal file
313
src/AcDream.Runtime/World/RuntimeWorldEnvironmentState.cs
Normal file
|
|
@ -0,0 +1,313 @@
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.World;
|
||||||
|
|
||||||
|
public sealed record RuntimeWorldDayGroupDefinition(
|
||||||
|
string Name,
|
||||||
|
float ChanceOfOccur,
|
||||||
|
int SkyObjectCount,
|
||||||
|
SkyStateProvider Sky)
|
||||||
|
{
|
||||||
|
public string Name { get; } = Name ?? string.Empty;
|
||||||
|
public SkyStateProvider Sky { get; } =
|
||||||
|
Sky ?? throw new ArgumentNullException(nameof(Sky));
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class RuntimeWorldEnvironmentDefinition
|
||||||
|
{
|
||||||
|
private readonly RuntimeWorldDayGroupDefinition[] _dayGroups;
|
||||||
|
|
||||||
|
public RuntimeWorldEnvironmentDefinition(
|
||||||
|
double originOffsetTicks,
|
||||||
|
double sourceTickSize,
|
||||||
|
double lightTickSize,
|
||||||
|
IEnumerable<RuntimeWorldDayGroupDefinition>? dayGroups,
|
||||||
|
int? forcedDayGroupIndex = null)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(originOffsetTicks))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(originOffsetTicks));
|
||||||
|
if (!double.IsFinite(sourceTickSize))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(sourceTickSize));
|
||||||
|
if (!double.IsFinite(lightTickSize))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(lightTickSize));
|
||||||
|
|
||||||
|
OriginOffsetTicks = originOffsetTicks;
|
||||||
|
SourceTickSize = sourceTickSize;
|
||||||
|
LightTickSize = lightTickSize;
|
||||||
|
_dayGroups = dayGroups?.ToArray() ?? [];
|
||||||
|
if (forcedDayGroupIndex is < 0
|
||||||
|
|| forcedDayGroupIndex >= _dayGroups.Length)
|
||||||
|
{
|
||||||
|
forcedDayGroupIndex = null;
|
||||||
|
}
|
||||||
|
ForcedDayGroupIndex = forcedDayGroupIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double OriginOffsetTicks { get; }
|
||||||
|
public double SourceTickSize { get; }
|
||||||
|
public double LightTickSize { get; }
|
||||||
|
public IReadOnlyList<RuntimeWorldDayGroupDefinition> DayGroups => _dayGroups;
|
||||||
|
public int? ForcedDayGroupIndex { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RuntimeEnvironmentEffectKind
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
FogOverride,
|
||||||
|
SoundCue,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RuntimeEnvironmentSoundCue
|
||||||
|
{
|
||||||
|
Roar = 0x65,
|
||||||
|
Bell = 0x66,
|
||||||
|
Chant1 = 0x67,
|
||||||
|
Chant2 = 0x68,
|
||||||
|
DarkWhispers1 = 0x69,
|
||||||
|
DarkWhispers2 = 0x6A,
|
||||||
|
DarkLaugh = 0x6B,
|
||||||
|
DarkWind = 0x6C,
|
||||||
|
DarkSpeech = 0x6D,
|
||||||
|
Drums = 0x6E,
|
||||||
|
GhostSpeak = 0x6F,
|
||||||
|
Breathing = 0x70,
|
||||||
|
Howl = 0x71,
|
||||||
|
LostSouls = 0x72,
|
||||||
|
Squeal = 0x75,
|
||||||
|
Thunder1 = 0x76,
|
||||||
|
Thunder2 = 0x77,
|
||||||
|
Thunder3 = 0x78,
|
||||||
|
Thunder4 = 0x79,
|
||||||
|
Thunder5 = 0x7A,
|
||||||
|
Thunder6 = 0x7B,
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct RuntimeEnvironmentEffect(
|
||||||
|
RuntimeEnvironmentEffectKind Kind,
|
||||||
|
uint RawValue,
|
||||||
|
EnvironOverride FogOverride = EnvironOverride.None,
|
||||||
|
RuntimeEnvironmentSoundCue? SoundCue = null);
|
||||||
|
|
||||||
|
public readonly record struct RuntimeWorldEnvironmentSnapshot(
|
||||||
|
long Revision,
|
||||||
|
bool IsInitialized,
|
||||||
|
int ActiveDayGroupIndex,
|
||||||
|
long ActiveDayIndex,
|
||||||
|
WeatherKind Weather,
|
||||||
|
EnvironOverride EnvironOverride);
|
||||||
|
|
||||||
|
public interface IRuntimeWorldEnvironmentView
|
||||||
|
{
|
||||||
|
RuntimeWorldEnvironmentSnapshot Snapshot { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instance-scoped owner of the retail world clock, deterministic day-group
|
||||||
|
/// choice, weather label, and AdminEnvirons state. Raw DAT objects and sky
|
||||||
|
/// drawing remain graphical-host projections.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RuntimeWorldEnvironmentState
|
||||||
|
: IRuntimeWorldEnvironmentView
|
||||||
|
{
|
||||||
|
private static readonly WeatherKind[] DebugWeatherKinds =
|
||||||
|
[
|
||||||
|
WeatherKind.Clear,
|
||||||
|
WeatherKind.Overcast,
|
||||||
|
WeatherKind.Rain,
|
||||||
|
WeatherKind.Snow,
|
||||||
|
WeatherKind.Storm,
|
||||||
|
];
|
||||||
|
|
||||||
|
private readonly Action<string> _log;
|
||||||
|
private RuntimeWorldEnvironmentDefinition? _definition;
|
||||||
|
private long _activeDayIndex = long.MinValue;
|
||||||
|
private int _timeDebugStep;
|
||||||
|
private int _weatherDebugStep;
|
||||||
|
private long _revision;
|
||||||
|
|
||||||
|
public RuntimeWorldEnvironmentState(
|
||||||
|
TimeProvider? timeProvider = null,
|
||||||
|
Action<string>? log = null,
|
||||||
|
Action<string>? timeSyncDiagnostic = null)
|
||||||
|
{
|
||||||
|
WorldTime = new WorldTimeService(
|
||||||
|
SkyStateProvider.Default(),
|
||||||
|
new DerethCalendar(),
|
||||||
|
timeProvider,
|
||||||
|
timeSyncDiagnostic);
|
||||||
|
Weather = new WeatherSystem();
|
||||||
|
_log = log ?? (_ => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldTimeService WorldTime { get; }
|
||||||
|
public WeatherSystem Weather { get; }
|
||||||
|
public int ActiveDayGroupIndex { get; private set; } = -1;
|
||||||
|
public bool IsInitialized => _definition is not null;
|
||||||
|
|
||||||
|
public RuntimeWorldEnvironmentSnapshot Snapshot => new(
|
||||||
|
_revision,
|
||||||
|
IsInitialized,
|
||||||
|
ActiveDayGroupIndex,
|
||||||
|
_activeDayIndex,
|
||||||
|
Weather.Kind,
|
||||||
|
Weather.Override);
|
||||||
|
|
||||||
|
public void Initialize(RuntimeWorldEnvironmentDefinition definition)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(definition);
|
||||||
|
if (_definition is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The world environment is a one-shot Runtime lifetime owner.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_definition = definition;
|
||||||
|
WorldTime.Calendar.SetOriginOffset(definition.OriginOffsetTicks);
|
||||||
|
WorldTime.TickSize = 1.0;
|
||||||
|
ActiveDayGroupIndex = -1;
|
||||||
|
_activeDayIndex = long.MinValue;
|
||||||
|
_revision++;
|
||||||
|
|
||||||
|
if (definition.DayGroups.Count > 0)
|
||||||
|
{
|
||||||
|
_log(
|
||||||
|
$"sky: loaded Region 0x13000000 — {definition.DayGroups.Count} day groups, "
|
||||||
|
+ $"SkyDesc.TickSize={definition.SourceTickSize} (throttle, not rate), "
|
||||||
|
+ $"LightTickSize={definition.LightTickSize}");
|
||||||
|
RefreshDayGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve the accepted retail-port initialization order: select the
|
||||||
|
// initial group, then seed the offline clock at noon.
|
||||||
|
WorldTime.SyncFromServer(DerethDateTime.DayTicks / 16.0);
|
||||||
|
_revision++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SynchronizeFromServer(double ticks)
|
||||||
|
{
|
||||||
|
WorldTime.SyncFromServer(ticks);
|
||||||
|
_revision++;
|
||||||
|
if (IsInitialized)
|
||||||
|
RefreshDayGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Port of the state distinction in
|
||||||
|
/// <c>CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20</c>.
|
||||||
|
/// Graphical audio playback consumes the returned typed sound cue.
|
||||||
|
/// </summary>
|
||||||
|
public RuntimeEnvironmentEffect ApplyAdminEnvirons(uint changeType)
|
||||||
|
{
|
||||||
|
if (changeType <= 0x06u)
|
||||||
|
{
|
||||||
|
var value = (EnvironOverride)changeType;
|
||||||
|
Weather.Override = value;
|
||||||
|
_revision++;
|
||||||
|
_log($"live: AdminEnvirons fog override = {value}");
|
||||||
|
return new RuntimeEnvironmentEffect(
|
||||||
|
RuntimeEnvironmentEffectKind.FogOverride,
|
||||||
|
changeType,
|
||||||
|
value);
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeEnvironmentSoundCue? cue =
|
||||||
|
Enum.IsDefined(typeof(RuntimeEnvironmentSoundCue), (int)changeType)
|
||||||
|
? (RuntimeEnvironmentSoundCue)(int)changeType
|
||||||
|
: null;
|
||||||
|
if (cue is null)
|
||||||
|
{
|
||||||
|
_log(
|
||||||
|
$"live: AdminEnvirons sound cue = Unknown(0x{changeType:X2}) "
|
||||||
|
+ $"(0x{changeType:X2}) — audio binding pending");
|
||||||
|
return new RuntimeEnvironmentEffect(
|
||||||
|
RuntimeEnvironmentEffectKind.Unknown,
|
||||||
|
changeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log(
|
||||||
|
$"live: AdminEnvirons sound cue = {cue}Sound "
|
||||||
|
+ $"(0x{changeType:X2}) — audio binding pending");
|
||||||
|
return new RuntimeEnvironmentEffect(
|
||||||
|
RuntimeEnvironmentEffectKind.SoundCue,
|
||||||
|
changeType,
|
||||||
|
SoundCue: cue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Selects the ordered day group with the exact
|
||||||
|
/// <c>SkyDesc::CalcPresentDayGroup @ 0x00500E10</c> integer/float
|
||||||
|
/// algorithm.
|
||||||
|
/// </summary>
|
||||||
|
public void RefreshDayGroup()
|
||||||
|
{
|
||||||
|
RuntimeWorldEnvironmentDefinition? definition = _definition;
|
||||||
|
if (definition is null)
|
||||||
|
return;
|
||||||
|
if (definition.DayGroups.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
double ticks = WorldTime.NowTicks;
|
||||||
|
DerethCalendar calendar = WorldTime.Calendar;
|
||||||
|
int absoluteYear = calendar.AbsoluteYear(ticks);
|
||||||
|
int dayOfYear = calendar.DayOfYear(ticks);
|
||||||
|
int daysPerYear =
|
||||||
|
DerethDateTime.DaysInAMonth * DerethDateTime.MonthsInAYear;
|
||||||
|
long dayIndex = (long)absoluteYear * daysPerYear + dayOfYear;
|
||||||
|
int index = SkyDayGroupSelector.SelectIndex(
|
||||||
|
definition.DayGroups.Count,
|
||||||
|
absoluteYear,
|
||||||
|
daysPerYear,
|
||||||
|
dayOfYear,
|
||||||
|
definition.ForcedDayGroupIndex);
|
||||||
|
|
||||||
|
if (dayIndex == _activeDayIndex && index == ActiveDayGroupIndex)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_activeDayIndex = dayIndex;
|
||||||
|
ActiveDayGroupIndex = index;
|
||||||
|
RuntimeWorldDayGroupDefinition group = definition.DayGroups[index];
|
||||||
|
WorldTime.SetProvider(group.Sky);
|
||||||
|
Weather.SetKindFromDayGroupName(group.Name);
|
||||||
|
_revision++;
|
||||||
|
|
||||||
|
_log(
|
||||||
|
$"sky: PY{absoluteYear} day{dayOfYear} → DayGroup[{index}] \"{group.Name}\" "
|
||||||
|
+ $"(Chance={group.ChanceOfOccur:F2}, {group.SkyObjectCount} objects, "
|
||||||
|
+ $"{group.Sky.KeyframeCount} keyframes, weather={Weather.Kind})");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CycleTimeOfDay()
|
||||||
|
{
|
||||||
|
_timeDebugStep = (_timeDebugStep + 1) % 5;
|
||||||
|
float? selection = _timeDebugStep switch
|
||||||
|
{
|
||||||
|
0 => null,
|
||||||
|
1 => 0.0f,
|
||||||
|
2 => 0.25f,
|
||||||
|
3 => 0.5f,
|
||||||
|
4 => 0.75f,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
_revision++;
|
||||||
|
if (selection.HasValue)
|
||||||
|
{
|
||||||
|
WorldTime.SetDebugTime(selection.Value);
|
||||||
|
return $"Time override = {selection.Value:F2}";
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldTime.ClearDebugTime();
|
||||||
|
return "Time override cleared";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CycleWeather()
|
||||||
|
{
|
||||||
|
_weatherDebugStep =
|
||||||
|
(_weatherDebugStep + 1) % DebugWeatherKinds.Length;
|
||||||
|
WeatherKind kind = DebugWeatherKinds[_weatherDebugStep];
|
||||||
|
Weather.ForceWeather(kind);
|
||||||
|
_revision++;
|
||||||
|
return $"Weather = {kind}";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -236,6 +236,8 @@ public sealed class InteractionUiRuntimeSourcesTests
|
||||||
public IRuntimeChatView Chat => null!;
|
public IRuntimeChatView Chat => null!;
|
||||||
public IRuntimeActionView Actions => null!;
|
public IRuntimeActionView Actions => null!;
|
||||||
public IRuntimeMovementView Movement => null!;
|
public IRuntimeMovementView Movement => null!;
|
||||||
|
public AcDream.Runtime.World.IRuntimeWorldEnvironmentView Environment =>
|
||||||
|
null!;
|
||||||
public IRuntimePortalView Portal => null!;
|
public IRuntimePortalView Portal => null!;
|
||||||
public IRuntimeSessionCommands Session => null!;
|
public IRuntimeSessionCommands Session => null!;
|
||||||
public IRuntimeSelectionCommands Selection => null!;
|
public IRuntimeSelectionCommands Selection => null!;
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,8 @@ public sealed class GameplayInputCommandControllerTests
|
||||||
public IRuntimeChatView Chat => throw new NotSupportedException();
|
public IRuntimeChatView Chat => throw new NotSupportedException();
|
||||||
public IRuntimeActionView Actions => throw new NotSupportedException();
|
public IRuntimeActionView Actions => throw new NotSupportedException();
|
||||||
public IRuntimeMovementView Movement => 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 IRuntimePortalView Portal => throw new NotSupportedException();
|
||||||
public RuntimeStateCheckpoint CaptureCheckpoint() =>
|
public RuntimeStateCheckpoint CaptureCheckpoint() =>
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"WorldRenderComposition.cs"));
|
"WorldRenderComposition.cs"));
|
||||||
|
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment =",
|
"private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment;",
|
||||||
source,
|
source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
using AcDream.Runtime.Gameplay;
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
|
using AcDream.Runtime.World;
|
||||||
using AcDream.UI.Abstractions;
|
using AcDream.UI.Abstractions;
|
||||||
using AcDream.UI.Abstractions.Input;
|
using AcDream.UI.Abstractions.Input;
|
||||||
|
|
||||||
|
|
@ -783,6 +784,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
_combatModeBinding =
|
_combatModeBinding =
|
||||||
CombatModeOperations.BindOwned(CombatMode);
|
CombatModeOperations.BindOwned(CombatMode);
|
||||||
MovementState = new RuntimeLocalPlayerMovementState();
|
MovementState = new RuntimeLocalPlayerMovementState();
|
||||||
|
Environment = new RuntimeWorldEnvironmentState();
|
||||||
MovementInput = new DispatcherMovementInputSource(MovementState);
|
MovementInput = new DispatcherMovementInputSource(MovementState);
|
||||||
GameplayInput = new GameplayInputFrameController(
|
GameplayInput = new GameplayInputFrameController(
|
||||||
dispatcher: null,
|
dispatcher: null,
|
||||||
|
|
@ -833,6 +835,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
Communication,
|
Communication,
|
||||||
Actions,
|
Actions,
|
||||||
MovementState,
|
MovementState,
|
||||||
|
Environment,
|
||||||
WorldReveal,
|
WorldReveal,
|
||||||
Clock,
|
Clock,
|
||||||
selectionController);
|
selectionController);
|
||||||
|
|
@ -853,6 +856,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
public RuntimeActionState Actions { get; }
|
public RuntimeActionState Actions { get; }
|
||||||
public SelectionState Selection => Actions.Selection;
|
public SelectionState Selection => Actions.Selection;
|
||||||
public RuntimeLocalPlayerMovementState MovementState { get; }
|
public RuntimeLocalPlayerMovementState MovementState { get; }
|
||||||
|
public RuntimeWorldEnvironmentState Environment { get; }
|
||||||
public DispatcherMovementInputSource MovementInput { get; }
|
public DispatcherMovementInputSource MovementInput { get; }
|
||||||
public GameplayInputFrameController GameplayInput { get; }
|
public GameplayInputFrameController GameplayInput { get; }
|
||||||
public RecordingCombatModeOperations CombatMode { get; }
|
public RecordingCombatModeOperations CombatMode { get; }
|
||||||
|
|
|
||||||
|
|
@ -338,6 +338,26 @@ public sealed class RuntimeOptionsTests
|
||||||
Assert.Equal(0.95f, invalid.FogEndMultiplier);
|
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]
|
[Fact]
|
||||||
public void DiagnosticFlags_RespectExactValueOne()
|
public void DiagnosticFlags_RespectExactValueOne()
|
||||||
{
|
{
|
||||||
|
|
@ -346,6 +366,7 @@ public sealed class RuntimeOptionsTests
|
||||||
["ACDREAM_DEVTOOLS"] = "1",
|
["ACDREAM_DEVTOOLS"] = "1",
|
||||||
["ACDREAM_UNCAPPED_RENDER"] = "1",
|
["ACDREAM_UNCAPPED_RENDER"] = "1",
|
||||||
["ACDREAM_DUMP_MOVE_TRUTH"] = "1",
|
["ACDREAM_DUMP_MOVE_TRUTH"] = "1",
|
||||||
|
["ACDREAM_DUMP_SKY"] = "1",
|
||||||
["ACDREAM_NO_AUDIO"] = "1",
|
["ACDREAM_NO_AUDIO"] = "1",
|
||||||
["ACDREAM_ENABLE_SKY_PES"] = "1",
|
["ACDREAM_ENABLE_SKY_PES"] = "1",
|
||||||
["ACDREAM_DUMP_SCENERY_Z"] = "1",
|
["ACDREAM_DUMP_SCENERY_Z"] = "1",
|
||||||
|
|
@ -354,6 +375,7 @@ public sealed class RuntimeOptionsTests
|
||||||
Assert.True(allOn.DevTools);
|
Assert.True(allOn.DevTools);
|
||||||
Assert.True(allOn.UncappedRendering);
|
Assert.True(allOn.UncappedRendering);
|
||||||
Assert.True(allOn.DumpMoveTruth);
|
Assert.True(allOn.DumpMoveTruth);
|
||||||
|
Assert.True(allOn.DumpSky);
|
||||||
Assert.True(allOn.NoAudio);
|
Assert.True(allOn.NoAudio);
|
||||||
Assert.True(allOn.EnableSkyPesDebug);
|
Assert.True(allOn.EnableSkyPesDebug);
|
||||||
Assert.True(allOn.DumpSceneryZ);
|
Assert.True(allOn.DumpSceneryZ);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.World;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.World;
|
namespace AcDream.App.Tests.World;
|
||||||
|
|
||||||
|
|
@ -57,25 +58,23 @@ public sealed class WorldEnvironmentControllerTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Initialize_WithoutGameTimeRestoresDocumentedFallbackOrigin()
|
public void Initialize_WithoutGameTimeUsesInstanceFallbackOrigin()
|
||||||
{
|
{
|
||||||
double previous = DerethDateTime.OriginOffsetTicks;
|
var first = CreateController([]);
|
||||||
try
|
var second = CreateController([]);
|
||||||
{
|
|
||||||
DerethDateTime.SetOriginOffsetFromDat(3600.0);
|
|
||||||
var controller = 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(3600.0, first.WorldTime.Calendar.OriginOffsetTicks);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
DerethDateTime.DayFractionOriginOffsetTicks,
|
DerethDateTime.DayFractionOriginOffsetTicks,
|
||||||
DerethDateTime.OriginOffsetTicks);
|
second.WorldTime.Calendar.OriginOffsetTicks);
|
||||||
Assert.InRange(controller.DayFraction, 0.499f, 0.501f);
|
Assert.InRange(second.DayFraction, 0.499f, 0.501f);
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
DerethDateTime.SetOriginOffsetFromDat(previous);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -143,8 +142,8 @@ public sealed class WorldEnvironmentControllerTests
|
||||||
|
|
||||||
private static WorldEnvironmentController CreateController(List<string> log) =>
|
private static WorldEnvironmentController CreateController(List<string> log) =>
|
||||||
new(
|
new(
|
||||||
new WorldTimeService(SkyStateProvider.Default()),
|
new RuntimeWorldEnvironmentState(log: log.Add),
|
||||||
new WeatherSystem(),
|
forcedDayGroupIndex: null,
|
||||||
log.Add);
|
log.Add);
|
||||||
|
|
||||||
private static LoadedSkyDesc SingleGroupSky(string name, float begin)
|
private static LoadedSkyDesc SingleGroupSky(string name, float begin)
|
||||||
|
|
@ -180,8 +179,8 @@ public sealed class WorldEnvironmentControllerTests
|
||||||
line.Contains("→ DayGroup[", StringComparison.Ordinal);
|
line.Contains("→ DayGroup[", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
[CollectionDefinition(Name, DisableParallelization = true)]
|
[CollectionDefinition(Name)]
|
||||||
public sealed class WorldEnvironmentControllerCollection
|
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
|
// 291,411,660 - 3600 = 291,408,060
|
||||||
// Expected output: PY 116 (= ZeroYear 10 + relative 106), Seedsow,
|
// Expected output: PY 116 (= ZeroYear 10 + relative 106), Seedsow,
|
||||||
// day 24 1-indexed.
|
// day 24 1-indexed.
|
||||||
DerethDateTime.SetOriginOffsetFromDat(3600.0);
|
var calendar = new DerethCalendar(3600.0);
|
||||||
try
|
var cal = calendar.ToCalendar(291_408_060.0);
|
||||||
{
|
|
||||||
var cal = DerethDateTime.ToCalendar(291_408_060.0);
|
|
||||||
Assert.Equal(DerethDateTime.ZeroYear + 106, cal.Year);
|
Assert.Equal(DerethDateTime.ZeroYear + 106, cal.Year);
|
||||||
Assert.Equal(DerethDateTime.MonthName.Seedsow, cal.Month);
|
Assert.Equal(DerethDateTime.MonthName.Seedsow, cal.Month);
|
||||||
Assert.Equal(24, cal.Day);
|
Assert.Equal(24, cal.Day);
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
DerethDateTime.SetOriginOffsetFromDat(DerethDateTime.DayFractionOriginOffsetTicks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,39 @@ namespace AcDream.Core.Tests.World;
|
||||||
|
|
||||||
public sealed class SkyDescLoaderTests
|
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>
|
/// <summary>
|
||||||
/// Hand-build a Region with a minimal sky descriptor to feed the
|
/// Hand-build a Region with a minimal sky descriptor to feed the
|
||||||
/// loader without needing real dat bytes. The LoadFromRegion
|
/// 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
|
// 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.
|
// → 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 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;
|
while (syncTick < 0) syncTick += DerethDateTime.DayTicks;
|
||||||
|
|
||||||
var service = new WorldTimeService(SkyStateProvider.Default());
|
|
||||||
service.SyncFromServer(syncTick);
|
service.SyncFromServer(syncTick);
|
||||||
service.SetDebugTime(0.5f);
|
service.SetDebugTime(0.5f);
|
||||||
service.ClearDebugTime();
|
service.ClearDebugTime();
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,7 @@ public sealed class GameRuntimeContractTests
|
||||||
InteractionSourceObjectId: 0u,
|
InteractionSourceObjectId: 0u,
|
||||||
InteractionTransactions: default),
|
InteractionTransactions: default),
|
||||||
default,
|
default,
|
||||||
|
default,
|
||||||
default);
|
default);
|
||||||
|
|
||||||
recorder.AddCheckpoint(stamp, checkpoint);
|
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