using System.Numerics; using AcDream.App.Rendering.Vfx; using AcDream.Core.Vfx; using AcDream.Core.World; namespace AcDream.App.Rendering; /// /// Owns the optional DAT-archaeology sky-PES experiment. Named retail shows /// GameSky does not consume SkyObject.PesObjectId, so production invokes this /// owner only when the explicit startup diagnostic is enabled. /// internal sealed class SkyPesFrameController { private readonly record struct SkyPesKey( int ObjectIndex, uint PesObjectId, bool PostScene); private readonly PhysicsScriptRunner _scripts; private readonly ParticleHookSink _particles; private readonly EntityEffectPoseRegistry _poses; private readonly EntityEffectController? _effects; private readonly HashSet _active = []; private readonly HashSet _missing = []; public SkyPesFrameController( PhysicsScriptRunner scripts, ParticleHookSink particles, EntityEffectPoseRegistry poses, EntityEffectController? effects) { _scripts = scripts ?? throw new ArgumentNullException(nameof(scripts)); _particles = particles ?? throw new ArgumentNullException(nameof(particles)); _poses = poses ?? throw new ArgumentNullException(nameof(poses)); _effects = effects; } public void Update( float dayFraction, DayGroupData? dayGroup, Vector3 cameraWorldPosition, bool suppressSky) { var seen = new HashSet(); if (!suppressSky && dayGroup is not null) { for (int index = 0; index < dayGroup.SkyObjects.Count; index++) { SkyObjectData skyObject = dayGroup.SkyObjects[index]; if (skyObject.PesObjectId == 0 || !skyObject.IsVisible(dayFraction)) continue; var key = new SkyPesKey( index, skyObject.PesObjectId, skyObject.IsPostScene); seen.Add(key); uint ownerId = EntityId(key); ParticleRenderPass renderPass = skyObject.IsPostScene ? ParticleRenderPass.SkyPostScene : ParticleRenderPass.SkyPreScene; _particles.SetEntityRenderPass(ownerId, renderPass); Vector3 anchor = Anchor(skyObject, cameraWorldPosition); Quaternion rotation = Rotation(skyObject, dayFraction); _poses.Publish( ownerId, Matrix4x4.CreateFromQuaternion(rotation) * Matrix4x4.CreateTranslation(anchor), Array.Empty(), cellId: 0u); if (_active.Contains(key) || _missing.Contains(key)) continue; _effects?.RegisterSyntheticOwner(ownerId); if (_scripts.Play(skyObject.PesObjectId, ownerId, anchor)) { _active.Add(key); } else { _missing.Add(key); _effects?.UnregisterSyntheticOwner(ownerId); _particles.ClearEntityRenderPass(ownerId); _poses.Remove(ownerId); } } } foreach (SkyPesKey key in _active.ToArray()) { if (seen.Contains(key)) continue; uint ownerId = EntityId(key); _scripts.StopAllForEntity(ownerId); _effects?.UnregisterSyntheticOwner(ownerId); _particles.StopAllForEntity(ownerId, fadeOut: true); _poses.Remove(ownerId); _active.Remove(key); } foreach (SkyPesKey key in _missing.ToArray()) { if (!seen.Contains(key)) _missing.Remove(key); } } private static uint EntityId(SkyPesKey key) { uint postScene = key.PostScene ? 0x08000000u : 0u; return 0xF0000000u | postScene | ((uint)key.ObjectIndex & 0x07FFFFFFu); } private static Vector3 Anchor( SkyObjectData skyObject, Vector3 cameraWorldPosition) { if (skyObject.IsWeather && (skyObject.Properties & 0x08u) == 0u) return cameraWorldPosition + new Vector3(0f, 0f, -120f); return cameraWorldPosition; } private static Quaternion Rotation( SkyObjectData skyObject, float dayFraction) { float radians = skyObject.CurrentAngle(dayFraction) * (MathF.PI / 180f); return Quaternion.CreateFromAxisAngle(Vector3.UnitY, -radians); } }