using AcDream.Core.Audio;
using AcDream.App.Rendering;
using AcDream.Core.World;
using AcDream.Runtime.World;
using DatReaderWriter.DBObjs;
namespace AcDream.App.World;
///
/// Graphical DAT/presentation adapter over the one
/// owner. Runtime owns clock,
/// weather, day selection, and server overrides; App retains raw sky DAT
/// objects because they are renderer inputs.
///
internal sealed class WorldEnvironmentController : IWorldSceneSkyStateSource
{
private readonly Action _log;
private readonly int? _forcedDayGroupIndex;
private LoadedSkyDesc? _loadedSkyDesc;
public WorldEnvironmentController(Action? log = null)
: this(
new RuntimeWorldEnvironmentState(log: log),
forcedDayGroupIndex: null,
log)
{
}
internal WorldEnvironmentController(
RuntimeWorldEnvironmentState runtime,
int? forcedDayGroupIndex = null,
Action? log = null,
float? pinnedDayFraction = null)
{
Runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_forcedDayGroupIndex = forcedDayGroupIndex;
_log = log ?? (_ => { });
// Campaign V slice V7 instrument determinism. Applied here rather than
// per generation because the Runtime environment owner and its clock are
// session-scoped: one write outlives every teleport and reveal.
Runtime.WorldTime.PinnedDayFraction = pinnedDayFraction;
if (pinnedDayFraction.HasValue)
_log($"sky: world time PINNED at day fraction {pinnedDayFraction.Value:F4}");
}
public RuntimeWorldEnvironmentState Runtime { get; }
public WorldTimeService WorldTime => Runtime.WorldTime;
public WeatherSystem Weather => Runtime.Weather;
public DayGroupData? ActiveDayGroup
{
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 void Initialize(Region region)
{
ArgumentNullException.ThrowIfNull(region);
Initialize(
SkyDescLoader.LoadFromRegion(region),
region.GameTime?.ZeroTimeOfYear);
}
internal void Initialize(
LoadedSkyDesc? loadedSkyDesc,
double? zeroTimeOfYear)
{
if (Runtime.IsInitialized)
{
throw new InvalidOperationException(
"The world environment is a one-shot Runtime lifetime owner.");
}
double origin = zeroTimeOfYear
?? DerethDateTime.DayFractionOriginOffsetTicks;
if (zeroTimeOfYear.HasValue)
{
_log(
$"sky: GameTime ZeroTimeOfYear={zeroTimeOfYear.Value} "
+ $"(was default {DerethDateTime.DayFractionOriginOffsetTicks})");
}
RuntimeWorldDayGroupDefinition[] groups =
loadedSkyDesc?.DayGroups.Select(
group => new RuntimeWorldDayGroupDefinition(
group.Name,
group.ChanceOfOccur,
group.SkyObjects.Count,
new SkyStateProvider(
group.SkyTimes.Select(value => value.Keyframe).ToList())))
.ToArray()
?? [];
var definition = new RuntimeWorldEnvironmentDefinition(
origin,
loadedSkyDesc?.TickSize ?? 1.0,
loadedSkyDesc?.LightTickSize ?? 1.0,
groups,
_forcedDayGroupIndex);
Runtime.Initialize(definition);
_loadedSkyDesc = loadedSkyDesc;
}
public void SynchronizeFromServer(double ticks) =>
Runtime.SynchronizeFromServer(ticks);
///
/// Interface-sound sink for retail's AdminEnvirons stingers. Retail's
/// CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20 plays each
/// of codes 0x65..0x7B from centre through the UI sound bank; the
/// code-to-slot table is . Null (silent)
/// when audio is unavailable.
///
public Action? EnvironSoundSink { get; set; }
public void ApplyAdminEnvirons(uint environChangeType)
{
RuntimeEnvironmentEffect effect = Runtime.ApplyAdminEnvirons(environChangeType);
if (effect.Kind is RuntimeEnvironmentEffectKind.SoundCue)
EnvironSoundSink?.Invoke(environChangeType);
}
public void RefreshSkyForCurrentDay() => Runtime.RefreshDayGroup();
public string CycleTimeOfDay() => Runtime.CycleTimeOfDay();
public string CycleWeather() => Runtime.CycleWeather();
}