acdream/src/AcDream.App/Update/LiveObjectFrameController.cs
Erik fec0d94148 refactor(settings): own two-phase runtime settings
Move pre-window loading, startup application, live settings mutation, toon context, quality reapply, and SettingsVM loans behind one RuntimeSettingsController. Preserve retail command behavior, ordered target publication, draft semantics, and retryable failure convergence while removing duplicate GameWindow state and feature bodies.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 13:30:22 +02:00

245 lines
10 KiB
C#

using System.Numerics;
using AcDream.App.Input;
using AcDream.App.Interaction;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Settings;
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Core.Rendering;
using AcDream.Core.Vfx;
using AcDream.Core.World;
using AcDream.UI.Abstractions.Panels.Settings;
namespace AcDream.App.Update;
internal interface ILiveObjectFramePhase
{
void Tick(float deltaSeconds);
}
internal interface ILiveSessionFramePhase
{
void Tick();
}
internal interface IPostNetworkCommandFramePhase
{
void RunPostNetworkCommandPhase();
}
internal interface ILiveSpatialReconcilePhase
{
void Reconcile();
}
internal interface IParticleRangeSource
{
float RangeMultiplier { get; }
}
internal sealed class SettingsParticleRangeSource : IParticleRangeSource
{
private readonly IRuntimeSettingsPreviewSource _settings;
public SettingsParticleRangeSource(IRuntimeSettingsPreviewSource settings)
{
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
}
public float RangeMultiplier =>
_settings.DisplayPreview.ParticleRange == ParticleRange.Extended
? ParticleVisibilityController.ExtendedRangeMultiplier
: 1f;
}
/// <summary>
/// Owns the shared effect-manager tail that follows ordinary and static
/// object worksets in acdream's registered TS-50/TS-51 adaptation.
/// </summary>
internal sealed class LiveEffectFrameController
{
private readonly TranslucencyFadeManager _translucencyFades;
private readonly AnimationHookFrameQueue _animationHooks;
private readonly EntityEffectController _entityEffects;
private readonly ParticleHookSink _particleSink;
private readonly LiveEntityLightController _lights;
private readonly ParticleVisibilityController _particleVisibility;
private readonly ParticleSystem _particles;
private readonly PhysicsScriptRunner _scripts;
private readonly IPhysicsScriptTimeSource _scriptTime;
private readonly IParticleRangeSource _particleRange;
public LiveEffectFrameController(
TranslucencyFadeManager translucencyFades,
AnimationHookFrameQueue animationHooks,
EntityEffectController entityEffects,
ParticleHookSink particleSink,
LiveEntityLightController lights,
ParticleVisibilityController particleVisibility,
ParticleSystem particles,
PhysicsScriptRunner scripts,
IPhysicsScriptTimeSource scriptTime,
IParticleRangeSource particleRange)
{
_translucencyFades = translucencyFades
?? throw new ArgumentNullException(nameof(translucencyFades));
_animationHooks = animationHooks ?? throw new ArgumentNullException(nameof(animationHooks));
_entityEffects = entityEffects ?? throw new ArgumentNullException(nameof(entityEffects));
_particleSink = particleSink ?? throw new ArgumentNullException(nameof(particleSink));
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
_particleVisibility = particleVisibility
?? throw new ArgumentNullException(nameof(particleVisibility));
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
_scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
_scriptTime = scriptTime ?? throw new ArgumentNullException(nameof(scriptTime));
_particleRange = particleRange ?? throw new ArgumentNullException(nameof(particleRange));
}
public void Tick(float deltaSeconds)
{
// Retail's ordinary-object UpdatePositionInternal @ 0x00512C30 calls
// CPhysicsObj::process_hooks @ 0x00511550 before its manager tail.
// acdream currently keeps non-AnimationDone hooks at this deferred
// shared boundary under TS-50; this extraction must not silently
// claim that adaptation is gone.
_translucencyFades.AdvanceAll(deltaSeconds);
_animationHooks.Drain();
_entityEffects.RefreshLiveOwnerPoses();
_particleSink.RefreshAttachedEmitters();
_lights.Refresh();
_particleVisibility.Apply(_particles, _particleRange.RangeMultiplier);
// Retail CPhysicsObj::UpdateObjectInternal @ 0x005156B0 advances the
// ordinary-object Particle manager before Script. acdream's shared
// once-per-host tail and its static-order difference remain TS-51.
_particles.Tick(deltaSeconds);
_scripts.Tick(_scriptTime.CurrentScriptTime);
}
}
/// <summary>
/// Owns the complete pre-network local/ordinary/static live-object phase.
/// </summary>
internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
{
private readonly RetailInboundEventDispatcher _inboundEvents;
private readonly RetailLocalPlayerFrameController _localPlayerFrame;
private readonly SelectionInteractionController? _selectionInteractions;
private readonly LiveEntityRuntime _liveEntities;
private readonly ILocalPlayerIdentitySource _localPlayer;
private readonly LiveWorldOriginState _origin;
private readonly LiveEntityAnimationScheduler _animations;
private readonly RetailStaticAnimatingObjectScheduler _staticAnimations;
private readonly LiveEntityAnimationPresenter _animationPresenter;
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState>
_animatedEntities;
private readonly EquippedChildRenderController _equippedChildren;
private readonly LiveEffectFrameController _effects;
public LiveObjectFrameController(
RetailInboundEventDispatcher inboundEvents,
RetailLocalPlayerFrameController localPlayerFrame,
SelectionInteractionController? selectionInteractions,
LiveEntityRuntime liveEntities,
ILocalPlayerIdentitySource localPlayer,
LiveWorldOriginState origin,
LiveEntityAnimationScheduler animations,
RetailStaticAnimatingObjectScheduler staticAnimations,
LiveEntityAnimationPresenter animationPresenter,
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animatedEntities,
EquippedChildRenderController equippedChildren,
LiveEffectFrameController effects)
{
_inboundEvents = inboundEvents ?? throw new ArgumentNullException(nameof(inboundEvents));
_localPlayerFrame = localPlayerFrame
?? throw new ArgumentNullException(nameof(localPlayerFrame));
_selectionInteractions = selectionInteractions;
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
_localPlayer = localPlayer ?? throw new ArgumentNullException(nameof(localPlayer));
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
_animations = animations ?? throw new ArgumentNullException(nameof(animations));
_staticAnimations = staticAnimations
?? throw new ArgumentNullException(nameof(staticAnimations));
_animationPresenter = animationPresenter
?? throw new ArgumentNullException(nameof(animationPresenter));
_animatedEntities = animatedEntities
?? throw new ArgumentNullException(nameof(animatedEntities));
_equippedChildren = equippedChildren
?? throw new ArgumentNullException(nameof(equippedChildren));
_effects = effects ?? throw new ArgumentNullException(nameof(effects));
}
public void Tick(float deltaSeconds) =>
_inboundEvents.Run(
this,
deltaSeconds,
static (controller, elapsed) => controller.TickCore(elapsed));
private void TickCore(float deltaSeconds)
{
_localPlayerFrame.AdvanceBeforeNetwork(deltaSeconds);
_selectionInteractions?.DrainOutbound();
Vector3? playerPosition =
_liveEntities.MaterializedWorldEntities.TryGetValue(
_localPlayer.ServerGuid,
out WorldEntity? player)
? player.Position
: null;
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules =
_animations.Tick(
deltaSeconds,
playerPosition,
_localPlayerFrame.HiddenPartPoseDirty,
_origin.CenterX,
_origin.CenterY,
_animationPresenter.PrepareAnimation);
// CPhysics::UseTime @ 0x00509950 walks ordinary objects before the
// distinct static_animating_objects workset.
_staticAnimations.Tick(deltaSeconds);
if (_animatedEntities.Count > 0)
_animationPresenter.Present(schedules);
_equippedChildren.Tick();
// Retail CPhysicsObj::animate_static_object @ 0x00513DF0 runs
// Script -> Particle -> process_hooks. acdream intentionally retains
// the TS-51 static-order difference here: ProcessHooks precedes the
// shared once-per-host Particle -> Script tail below.
_staticAnimations.ProcessHooks();
_effects.Tick(deltaSeconds);
}
}
/// <summary>
/// Re-composes spatial derivatives after authoritative root mutations without
/// advancing animation, physics, effects, particles, scripts, or fades.
/// </summary>
internal sealed class LiveSpatialPresentationReconciler : ILiveSpatialReconcilePhase
{
private readonly EntityEffectController _entityEffects;
private readonly EquippedChildRenderController _equippedChildren;
private readonly ParticleHookSink _particleSink;
private readonly LiveEntityLightController _lights;
public LiveSpatialPresentationReconciler(
EntityEffectController entityEffects,
EquippedChildRenderController equippedChildren,
ParticleHookSink particleSink,
LiveEntityLightController lights)
{
_entityEffects = entityEffects ?? throw new ArgumentNullException(nameof(entityEffects));
_equippedChildren = equippedChildren
?? throw new ArgumentNullException(nameof(equippedChildren));
_particleSink = particleSink ?? throw new ArgumentNullException(nameof(particleSink));
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
}
public void Reconcile()
{
_entityEffects.RefreshLiveOwnerPoses();
_equippedChildren.Tick();
_particleSink.RefreshAttachedEmitters();
_lights.Refresh();
}
}