using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.ComponentTrees;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Robust.Client.GameObjects
{
///
/// Updates the layer animation for every visible sprite.
///
[UsedImplicitly]
public sealed partial class SpriteSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly Queue _inertUpdateQueue = new();
///
/// Entities that require a sprite frame update.
///
private readonly HashSet _queuedFrameUpdate = new();
private ISawmill _sawmill = default!;
internal void Render(EntityUid uid, SpriteComponent sprite, DrawingHandleWorld drawingHandle, Angle eyeRotation, in Angle worldRotation, in Vector2 worldPosition)
{
if (!sprite.IsInert)
_queuedFrameUpdate.Add(uid);
sprite.RenderInternal(drawingHandle, eyeRotation, worldRotation, worldPosition, sprite.EnableDirectionOverride ? sprite.DirectionOverride : null);
}
public override void Initialize()
{
base.Initialize();
UpdatesAfter.Add(typeof(SpriteTreeSystem));
SubscribeLocalEvent(OnPrototypesReloaded);
SubscribeLocalEvent(QueueUpdateInert);
SubscribeLocalEvent(OnInit);
_cfg.OnValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
_sawmill = _logManager.GetSawmill("sprite");
}
private void OnInit(EntityUid uid, SpriteComponent component, ComponentInit args)
{
// I'm not 100% this is needed, but I CBF with this ATM. Somebody kill server sprite component please.
QueueUpdateInert(uid, component);
}
public override void Shutdown()
{
base.Shutdown();
_cfg.UnsubValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged);
}
private void OnBiasChanged(double value)
{
SpriteComponent.DirectionBias = value;
}
private void QueueUpdateInert(EntityUid uid, SpriteComponent sprite, ref SpriteUpdateInertEvent ev)
=> QueueUpdateInert(uid, sprite);
public void QueueUpdateInert(EntityUid uid, SpriteComponent sprite)
{
if (sprite._inertUpdateQueued)
return;
sprite._inertUpdateQueued = true;
_inertUpdateQueue.Enqueue(sprite);
}
private void DoUpdateIsInert(SpriteComponent component)
{
component._inertUpdateQueued = false;
component.IsInert = true;
foreach (var layer in component.Layers)
{
// Since StateId is a struct, we can't null-check it directly.
if (!layer.State.IsValid || !layer.Visible || !layer.AutoAnimated || layer.Blank)
{
continue;
}
var rsi = layer.RSI ?? component.BaseRSI;
if (rsi == null || !rsi.TryGetState(layer.State, out var state))
{
state = GetFallbackState();
}
if (state.IsAnimated)
{
component.IsInert = false;
break;
}
}
}
///
public override void FrameUpdate(float frameTime)
{
while (_inertUpdateQueue.TryDequeue(out var sprite))
{
DoUpdateIsInert(sprite);
}
var realtime = _timing.RealTime.TotalSeconds;
var spriteQuery = GetEntityQuery();
var syncQuery = GetEntityQuery();
var metaQuery = GetEntityQuery();
foreach (var uid in _queuedFrameUpdate)
{
if (!spriteQuery.TryGetComponent(uid, out var sprite) ||
metaQuery.GetComponent(uid).EntityPaused)
{
continue;
}
if (sprite.IsInert)
continue;
var sync = syncQuery.HasComponent(uid);
foreach (var layer in sprite.Layers)
{
if (!layer.State.IsValid || !layer.Visible || !layer.AutoAnimated)
continue;
var rsi = layer.RSI ?? sprite.BaseRSI;
if (rsi == null || !rsi.TryGetState(layer.State, out var state))
state = GetFallbackState();
if (!state.IsAnimated)
continue;
if (sync)
{
layer.AnimationTime = (float)(realtime % state.TotalDelay);
layer.AnimationTimeLeft = -layer.AnimationTime;
layer.AnimationFrame = 0;
}
else
{
layer.AnimationTime += frameTime;
layer.AnimationTimeLeft -= frameTime;
}
layer.AdvanceFrameAnimation(state);
}
}
_queuedFrameUpdate.Clear();
}
///
/// Force update of the sprite component next frame
///
public void ForceUpdate(EntityUid uid)
{
_queuedFrameUpdate.Add(uid);
}
///
/// Gets the specified frame for this sprite at the specified time.
///
public Texture GetFrame(SpriteSpecifier spriteSpec, TimeSpan curTime)
{
Texture? sprite = null;
switch (spriteSpec)
{
case SpriteSpecifier.Rsi rsi:
var rsiActual = _resourceCache.GetResource(rsi.RsiPath).RSI;
rsiActual.TryGetState(rsi.RsiState, out var state);
var frames = state!.GetFrames(RsiDirection.South);
var delays = state.GetDelays();
var totalDelay = delays.Sum();
var time = curTime.TotalSeconds % totalDelay;
var delaySum = 0f;
for (var i = 0; i < delays.Length; i++)
{
var delay = delays[i];
delaySum += delay;
if (time > delaySum)
continue;
sprite = frames[i];
break;
}
sprite ??= Frame0(spriteSpec);
break;
case SpriteSpecifier.Texture texture:
sprite = texture.GetTexture(_resourceCache);
break;
default:
throw new NotImplementedException();
}
return sprite;
}
}
///
/// This event gets raised before a sprite gets drawn using it's post-shader.
///
public sealed class BeforePostShaderRenderEvent : EntityEventArgs
{
public readonly SpriteComponent Sprite;
public readonly IClydeViewport Viewport;
public BeforePostShaderRenderEvent(SpriteComponent sprite, IClydeViewport viewport)
{
Sprite = sprite;
Viewport = viewport;
}
}
}