refactor(runtime): own world environment state
This commit is contained in:
parent
b972f539f7
commit
902076c0a4
27 changed files with 886 additions and 295 deletions
|
|
@ -188,6 +188,10 @@ public sealed class RuntimeTraceRecorder : IRuntimeEventObserver
|
|||
$"{checkpoint.Actions.Magic.Revision}:" +
|
||||
$"{checkpoint.Actions.Magic.LastRequestedSpellId: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}:" +
|
||||
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady}")));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.World;
|
||||
|
||||
namespace AcDream.Runtime;
|
||||
|
||||
|
|
@ -121,6 +122,7 @@ public readonly record struct RuntimeStateCheckpoint(
|
|||
int ChatCount,
|
||||
RuntimeActionSnapshot Actions,
|
||||
RuntimeMovementSnapshot Movement,
|
||||
RuntimeWorldEnvironmentSnapshot Environment,
|
||||
RuntimePortalSnapshot Portal);
|
||||
|
||||
public interface IGameRuntimeView
|
||||
|
|
@ -147,6 +149,8 @@ public interface IGameRuntimeView
|
|||
|
||||
IRuntimeMovementView Movement { get; }
|
||||
|
||||
IRuntimeWorldEnvironmentView Environment { get; }
|
||||
|
||||
IRuntimePortalView Portal { get; }
|
||||
|
||||
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}";
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue