Move parts of SpriteComponent to SpriteSystem (#5602)

* Partial sprite component ECS

* release notes

* tests

* Why

* SetSnapCardinals

* NoRotation

* DirectionOverride

* This is why I love distinct overrides that take in object

* LayerSetData

* ISerializationHooks continue to haunt me

* Relocate SetShader

* LayerSetSprite

* LayerSetTexture

* yipeeeee

* LayerSetRsi

* Remove GetFallbackState

* LayerSet Scale,Rotation,Color,Visible

* Fix LayerSetRsi

* LayerSetOffset

* LayerSetDirOffset

* Add overrides that take in a Layer

* LayerSetAnimationTime

* LayerSetRenderingStrategy

* Reduce Resolves, Add Layer.Index

* Access

* Try fix NREs

* Asserts

* LayerGetState

* Cleanup

* Merge helper partial classes

* partial rendering

* GetLayerDirectionCount

* Cache local bounds

* RenderLayer

* RefreshCachedState

* RoundToCardinalAngle

* Fix the pr

* Fix debug assert

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
This commit is contained in:
Leon Friedrich
2025-05-10 21:56:53 +10:00
committed by GitHub
parent d24854d94f
commit 15d5b9aa02
24 changed files with 2675 additions and 1284 deletions

View File

@@ -48,6 +48,7 @@ END TEMPLATE-->
### Other
* The yaml prototype id serialiser now provides better feedback when trying to validate an id for a prototype kind that has been ignored via `IPrototypeManager.RegisterIgnore()`
* Several SpriteComponent methods have been marked as obsolete, and should be replaced with new methods in SpriteSystem.
### Internal

View File

@@ -2,6 +2,7 @@ using System.Numerics;
using Robust.Client.GameObjects;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
@@ -9,25 +10,7 @@ namespace Robust.Client.ComponentTrees;
public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent, SpriteComponent>
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpriteComponent, QueueSpriteTreeUpdateEvent>(OnQueueUpdate);
}
private void OnQueueUpdate(EntityUid uid, SpriteComponent component, ref QueueSpriteTreeUpdateEvent args)
=> QueueTreeUpdate(uid, component, args.Xform);
// TODO remove this when finally ECSing sprite components
[ByRefEvent]
internal readonly struct QueueSpriteTreeUpdateEvent
{
public readonly TransformComponent Xform;
public QueueSpriteTreeUpdateEvent(TransformComponent xform)
{
Xform = xform;
}
}
[Dependency] private readonly SpriteSystem _sprite = default!;
#region Component Tree Overrides
protected override bool DoFrameUpdate => true;
@@ -36,6 +19,11 @@ public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent,
protected override int InitialCapacity => 1024;
protected override Box2 ExtractAabb(in ComponentTreeEntry<SpriteComponent> entry, Vector2 pos, Angle rot)
=> entry.Component.CalculateRotatedBoundingBox(pos, rot, default).CalcBoundingBox();
{
// TODO SPRITE optimize this
// Because the just take the BB of the rotated BB, I'mt pretty sure we do a lot of unnecessary maths.
return _sprite.CalculateBounds((entry.Uid, entry.Component), pos, rot, default).CalcBoundingBox();
}
#endregion
}

View File

@@ -1,9 +1,11 @@
using Robust.Shared.GameObjects;
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects
{
[Obsolete]
public partial interface IRenderableComponent : IComponent
{
int DrawDepth { get; set; }

View File

@@ -1,6 +1,5 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Graphics;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Maths;

View File

@@ -0,0 +1,149 @@
using System;
using System.Linq;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Robust.Client.GameObjects;
// This partial class contains code related to updating a sprites bounding boxes and its position in the sprite tree.
public sealed partial class SpriteSystem
{
/// <summary>
/// Get a sprite's local bounding box. The returned bounds do factor in the sprite's scale but not the rotation or
/// offset.
/// </summary>
public Box2 GetLocalBounds(Entity<SpriteComponent> sprite)
{
if (!sprite.Comp.BoundsDirty)
{
DebugTools.Assert(sprite.Comp.Layers.All(x => !x.BoundsDirty || !x.Drawn));
return sprite.Comp._bounds;
}
var bounds = new Box2();
foreach (var layer in sprite.Comp.Layers)
{
if (layer.Drawn)
bounds = bounds.Union(GetLocalBounds(layer));
}
sprite.Comp._bounds = bounds;
sprite.Comp.BoundsDirty = false;
return sprite.Comp._bounds;
}
/// <summary>
/// Get a layer's local bounding box relative to its owning sprite. Unlike the sprite variant of this method, this
/// does account for the layer's rotation and offset.
/// </summary>
public Box2 GetLocalBounds(Layer layer)
{
if (!layer.BoundsDirty)
return layer.Bounds;
layer.Bounds = CalculateLocalBounds(layer);
layer.BoundsDirty = false;
return layer.Bounds;
}
internal Box2 CalculateLocalBounds(Layer layer)
{
if (layer.Blank || layer.CopyToShaderParameters == null)
return Box2.Empty;
var textureSize = (Vector2) layer.PixelSize / EyeManager.PixelsPerMeter;
var longestSide = MathF.Max(textureSize.X, textureSize.Y);
var longestRotatedSide = Math.Max(longestSide, (textureSize.X + textureSize.Y) / MathF.Sqrt(2));
Vector2 size;
var sprite = layer.Owner.Comp;
// If this layer has any form of arbitrary rotation, return a bounding box big enough to cover
// any possible rotation.
if (layer._rotation != 0)
{
size = new Vector2(longestRotatedSide, longestRotatedSide);
return Box2.CenteredAround(layer.Offset, size * layer._scale);
}
var snapToCardinals = sprite.SnapCardinals;
if (sprite.GranularLayersRendering && layer.RenderingStrategy != LayerRenderingStrategy.UseSpriteStrategy)
{
snapToCardinals = layer.RenderingStrategy == LayerRenderingStrategy.SnapToCardinals;
}
if (snapToCardinals)
{
// Snapping to cardinals only makes sense for 1-directional layers/sprites
DebugTools.Assert(layer._actualState == null || layer._actualState.RsiDirections == RsiDirectionType.Dir1);
// We won't know the actual direction it snaps to, so we ahve to assume the box is given by the longest side.
size = new Vector2(longestSide, longestSide);
return Box2.CenteredAround(layer.Offset, size * layer._scale);
}
// Build the bounding box based on how many directions the sprite has
size = (layer._actualState?.RsiDirections) switch
{
RsiDirectionType.Dir4 => new Vector2(longestSide, longestSide),
RsiDirectionType.Dir8 => new Vector2(longestRotatedSide, longestRotatedSide),
_ => textureSize
};
return Box2.CenteredAround(layer.Offset, size * layer._scale);
}
/// <summary>
/// Gets a sprite's bounding box in world coordinates.
/// </summary>
public Box2Rotated CalculateBounds(Entity<SpriteComponent> sprite, Vector2 worldPos, Angle worldRot, Angle eyeRot)
{
// fast check for invisible sprites
if (!sprite.Comp.Visible || sprite.Comp.Layers.Count == 0)
return new Box2Rotated(new Box2(worldPos, worldPos), Angle.Zero, worldPos);
// We need to modify world rotation so that it lies between 0 and 2pi.
// This matters for 4 or 8 directional sprites deciding which quadrant (octant?) they lie in.
// the 0->2pi convention is set by the sprite-rendering code that selects the layers.
// See RenderInternal().
worldRot = worldRot.Reduced();
if (worldRot.Theta < 0)
worldRot = new Angle(worldRot.Theta + Math.Tau);
// Next, what we do is take the box2 and apply the sprite's transform, and then the entity's transform. We
// could do this via Matrix3.TransformBox, but that only yields bounding boxes. So instead we manually
// transform our box by the combination of these matrices:
var finalRotation = sprite.Comp.NoRotation
? sprite.Comp.Rotation - eyeRot
: sprite.Comp.Rotation + worldRot;
var bounds = GetLocalBounds(sprite);
// slightly faster path if offset == 0 (true for 99.9% of sprites)
if (sprite.Comp.Offset == Vector2.Zero)
return new Box2Rotated(bounds.Translated(worldPos), finalRotation, worldPos);
var adjustedOffset = sprite.Comp.NoRotation
? (-eyeRot).RotateVec(sprite.Comp.Offset)
: worldRot.RotateVec(sprite.Comp.Offset);
var position = adjustedOffset + worldPos;
return new Box2Rotated(bounds.Translated(position), finalRotation, position);
}
private void DirtyBounds(Entity<SpriteComponent> sprite)
{
sprite.Comp.BoundsDirty = true;
foreach (var layer in sprite.Comp.Layers)
{
layer.BoundsDirty = true;
}
}
}

View File

@@ -1,4 +1,8 @@
using System.Linq;
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
@@ -41,4 +45,66 @@ public sealed partial class SpriteSystem
layer.AnimationTimeLeft = (float) -(time % state.TotalDelay);
layer.AnimationFrame = 0;
}
public void CopySprite(Entity<SpriteComponent?> source, Entity<SpriteComponent?> target)
{
if (!Resolve(source.Owner, ref source.Comp))
return;
if (!Resolve(target.Owner, ref target.Comp))
return;
target.Comp._baseRsi = source.Comp._baseRsi;
target.Comp._bounds = source.Comp._bounds;
target.Comp._visible = source.Comp._visible;
target.Comp.color = source.Comp.color;
target.Comp.offset = source.Comp.offset;
target.Comp.rotation = source.Comp.rotation;
target.Comp.scale = source.Comp.scale;
target.Comp.LocalMatrix = Matrix3Helpers.CreateTransform(
in target.Comp.offset,
in target.Comp.rotation,
in target
.Comp.scale);
target.Comp.drawDepth = source.Comp.drawDepth;
target.Comp.NoRotation = source.Comp.NoRotation;
target.Comp.DirectionOverride = source.Comp.DirectionOverride;
target.Comp.EnableDirectionOverride = source.Comp.EnableDirectionOverride;
target.Comp.Layers = new List<SpriteComponent.Layer>(source.Comp.Layers.Count);
foreach (var otherLayer in source.Comp.Layers)
{
var layer = new SpriteComponent.Layer(otherLayer, target.Comp);
layer.Index = target.Comp.Layers.Count;
layer.Owner = target!;
target.Comp.Layers.Add(layer);
}
target.Comp.IsInert = source.Comp.IsInert;
target.Comp.LayerMap = source.Comp.LayerMap.ShallowClone();
target.Comp.PostShader = source.Comp.PostShader is {Mutable: true}
? source.Comp.PostShader.Duplicate()
: source.Comp.PostShader;
target.Comp.RenderOrder = source.Comp.RenderOrder;
target.Comp.GranularLayersRendering = source.Comp.GranularLayersRendering;
DirtyBounds(target!);
_tree.QueueTreeUpdate(target!);
}
/// <summary>
/// Adds a sprite to a queue that will update <see cref="SpriteComponent.IsInert"/> next frame.
/// </summary>
public void QueueUpdateIsInert(Entity<SpriteComponent> sprite)
{
if (sprite.Comp._inertUpdateQueued)
return;
sprite.Comp._inertUpdateQueued = true;
_inertUpdateQueue.Enqueue(sprite);
}
[Obsolete("Use QueueUpdateIsInert")]
public void QueueUpdateInert(EntityUid uid, SpriteComponent sprite) => QueueUpdateIsInert(new (uid, sprite));
}

View File

@@ -1,11 +1,153 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
// This partial class contains various public helper methods, including methods for extracting textures/icons from
// sprite specifiers and entity prototypes.
public sealed partial class SpriteSystem
{
private readonly Dictionary<string, IRsiStateLike> _cachedPrototypeIcons = new();
public Texture Frame0(EntityPrototype prototype)
{
return GetPrototypeIcon(prototype).Default;
}
public Texture Frame0(SpriteSpecifier specifier)
{
return RsiStateLike(specifier).Default;
}
public IRsiStateLike RsiStateLike(SpriteSpecifier specifier)
{
switch (specifier)
{
case SpriteSpecifier.Texture tex:
return GetTexture(tex);
case SpriteSpecifier.Rsi rsi:
return GetState(rsi);
case SpriteSpecifier.EntityPrototype prototypeIcon:
return GetPrototypeIcon(prototypeIcon.EntityPrototypeId);
default:
throw new NotSupportedException();
}
}
public Texture GetIcon(IconComponent icon)
{
return GetState(icon.Icon).Frame0;
}
/// <summary>
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
/// This method caches the result based on the prototype identifier.
/// </summary>
public IRsiStateLike GetPrototypeIcon(string prototype)
{
// Check if this prototype has been cached before, and if so return the result.
if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult))
return cachedResult;
if (!_proto.TryIndex<EntityPrototype>(prototype, out var entityPrototype))
{
// The specified prototype doesn't exist, return the fallback "error" sprite.
_sawmill.Error("Failed to load PrototypeIcon {0}", prototype);
return GetFallbackState();
}
// Generate the icon and cache it in case it's ever needed again.
var result = GetPrototypeIcon(entityPrototype);
_cachedPrototypeIcons[prototype] = result;
return result;
}
/// <summary>
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
/// This method does NOT cache the result.
/// </summary>
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
{
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
if (prototype.Components.TryGetValue("Icon", out var compData)
&& compData.Component is IconComponent icon)
{
return GetIcon(icon);
}
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
if (!prototype.Components.ContainsKey("Sprite"))
{
return GetFallbackState();
}
// Finally, we use spawn a dummy entity to get its icon.
var dummy = Spawn(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
var result = spriteComponent.Icon ?? GetFallbackState();
Del(dummy);
return result;
}
[Pure]
public RSI.State GetFallbackState()
{
return _resourceCache.GetFallback<RSIResource>().RSI["error"];
}
public Texture GetFallbackTexture()
{
return _resourceCache.GetFallback<TextureResource>().Texture;
}
[Pure]
public RSI.State GetState(SpriteSpecifier.Rsi rsiSpecifier)
{
if (_resourceCache.TryGetResource<RSIResource>(
TextureRoot / rsiSpecifier.RsiPath,
out var theRsi) &&
theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
{
return state;
}
_sawmill.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath);
return GetFallbackState();
}
public Texture GetTexture(SpriteSpecifier.Texture texSpecifier)
{
return _resourceCache
.GetResource<TextureResource>(TextureRoot / texSpecifier.TexturePath)
.Texture;
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (!args.TryGetModified<EntityPrototype>(out var modified))
return;
// Remove all changed prototypes from the cache, if they're there.
foreach (var prototype in modified)
{
// Let's be lazy and not regenerate them until something needs them again.
_cachedPrototypeIcons.Remove(prototype);
}
}
/// <summary>
/// Gets an entity's sprite position in world terms.
/// </summary>

View File

@@ -0,0 +1,257 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Robust.Client.GameObjects;
// This partial class contains various public methods for managing a sprite's layers.
// This setter methods for modifying a layer's properties are in a separate file.
public sealed partial class SpriteSystem
{
public bool LayerExists(Entity<SpriteComponent?> sprite, int index)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return false;
return index > 0 && index < sprite.Comp.Layers.Count;
}
public bool TryGetLayer(
Entity<SpriteComponent?> sprite,
int index,
[NotNullWhen(true)] out Layer? layer,
bool logMissing)
{
layer = null;
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
if (index >= 0 && index < sprite.Comp.Layers.Count)
{
layer = sprite.Comp.Layers[index];
DebugTools.AssertEqual(layer.Owner, sprite!);
DebugTools.AssertEqual(layer.Index, index);
return true;
}
if (logMissing)
Log.Error($"Layer index '{index}' on entity {ToPrettyString(sprite)} does not exist! Trace:\n{Environment.StackTrace}");
return false;
}
public bool RemoveLayer(Entity<SpriteComponent?> sprite, int index, bool logMissing = true)
{
return RemoveLayer(sprite.Owner, index, out _, logMissing);
}
public bool RemoveLayer(
Entity<SpriteComponent?> sprite,
int index,
[NotNullWhen(true)] out Layer? layer,
bool logMissing = true)
{
layer = null;
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
if (!TryGetLayer(sprite, index, out layer, logMissing))
return false;
sprite.Comp.Layers.RemoveAt(index);
foreach (var otherLayer in sprite.Comp.Layers[index..])
{
otherLayer.Index--;
}
// TODO SPRITE track inverse-mapping?
foreach (var (key, value) in sprite.Comp.LayerMap)
{
if (value == index)
sprite.Comp.LayerMap.Remove(key);
else if (value > index)
{
sprite.Comp.LayerMap[key]--;
}
}
layer.Owner = default;
layer.Index = -1;
#if DEBUG
foreach (var otherLayer in sprite.Comp.Layers)
{
DebugTools.AssertEqual(otherLayer, sprite.Comp.Layers[otherLayer.Index]);
}
#endif
sprite.Comp.BoundsDirty = true;
_tree.QueueTreeUpdate(sprite!);
QueueUpdateIsInert(sprite!);
return true;
}
#region AddLayer
/// <summary>
/// Add the given sprite layer. If an index is specified, this will insert the layer with the given index, resulting
/// in all other layers being reshuffled.
/// </summary>
public int AddLayer(Entity<SpriteComponent?> sprite, Layer layer, int? index = null)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
{
layer.Index = -1;
layer.Owner = default;
return -1;
}
layer.Owner = sprite!;
if (index is { } i && i != sprite.Comp.Layers.Count)
{
foreach (var otherLayer in sprite.Comp.Layers[i..])
{
otherLayer.Index++;
}
// TODO SPRITE track inverse-mapping?
sprite.Comp.Layers.Insert(i, layer);
layer.Index = i;
foreach (var (key, value) in sprite.Comp.LayerMap)
{
if (value >= i)
sprite.Comp.LayerMap[key]++;
}
}
else
{
layer.Index = sprite.Comp.Layers.Count;
sprite.Comp.Layers.Add(layer);
}
#if DEBUG
foreach (var otherLayer in sprite.Comp.Layers)
{
DebugTools.AssertEqual(otherLayer, sprite.Comp.Layers[otherLayer.Index]);
}
#endif
if (!layer.Blank)
{
layer.BoundsDirty = true;
sprite.Comp.BoundsDirty = true;
_tree.QueueTreeUpdate(sprite!);
QueueUpdateIsInert(sprite!);
}
return layer.Index;
}
/// <summary>
/// Add a layer corresponding to the given RSI state.
/// </summary>
/// <param name="sprite">The sprite</param>
/// <param name="stateId">The RSI state</param>
/// <param name="rsi">The RSI to use. If not specified, it will default to using <see cref="SpriteComponent.BaseRSI"/></param>
/// <param name="index">The layer index to use for the new sprite.</param>
/// <returns></returns>
public int AddRsiLayer(Entity<SpriteComponent?> sprite, RSI.StateId stateId, RSI? rsi = null, int? index = null)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
var layer = AddBlankLayer(sprite!, index);
if (rsi != null)
LayerSetRsi(layer, rsi, stateId);
else
LayerSetRsiState(layer, stateId);
return layer.Index;
}
/// <summary>
/// Add a layer corresponding to the given RSI state.
/// </summary>
/// <param name="sprite">The sprite</param>
/// <param name="state">The RSI state</param>
/// <param name="path">The path to the RSI.</param>
/// <param name="index">The layer index to use for the new sprite.</param>
/// <returns></returns>
public int AddRsiLayer(Entity<SpriteComponent?> sprite, RSI.StateId state, ResPath path, int? index = null)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
if (!_resourceCache.TryGetResource<RSIResource>(TextureRoot / path, out var res))
Log.Error($"Unable to load RSI '{path}'. Trace:\n{Environment.StackTrace}");
if (path.Extension != "rsi")
Log.Error($"Expected rsi path but got '{path}'?");
return AddRsiLayer(sprite, state, res?.RSI, index);
}
public int AddTextureLayer(Entity<SpriteComponent?> sprite, ResPath path, int? index = null)
{
if (_resourceCache.TryGetResource<TextureResource>(TextureRoot / path, out var texture))
return AddTextureLayer(sprite, texture?.Texture, index);
if (path.Extension == "rsi")
Log.Error($"Expected texture but got rsi '{path}', did you mean 'sprite:' instead of 'texture:'?");
Log.Error($"Unable to load texture '{path}'. Trace:\n{Environment.StackTrace}");
return AddTextureLayer(sprite, texture?.Texture, index);
}
public int AddTextureLayer(Entity<SpriteComponent?> sprite, Texture? texture, int? index = null)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
var layer = new Layer {Texture = texture};
return AddLayer(sprite, layer, index);
}
public int AddLayer(Entity<SpriteComponent?> sprite, SpriteSpecifier specifier, int? newIndex = null)
{
return specifier switch
{
SpriteSpecifier.Texture tex => AddTextureLayer(sprite, tex.TexturePath, newIndex),
SpriteSpecifier.Rsi rsi => AddRsiLayer(sprite, rsi.RsiState, rsi.RsiPath, newIndex),
_ => throw new NotImplementedException()
};
}
/// <summary>
/// Add a new sprite layer and populate it using the provided layer data.
/// </summary>
public int AddLayer(Entity<SpriteComponent?> sprite, PrototypeLayerData layerDatum, int? index)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
var layer = AddBlankLayer(sprite!, index);
LayerSetData(layer, layerDatum);
return layer.Index;
}
/// <summary>
/// Add a blank sprite layer.
/// </summary>
public Layer AddBlankLayer(Entity<SpriteComponent> sprite, int? index = null)
{
var layer = new Layer();
AddLayer(sprite!, layer, index);
return layer;
}
#endregion
}

View File

@@ -0,0 +1,150 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics.RSI;
using static Robust.Client.GameObjects.SpriteComponent;
using static Robust.Client.Graphics.RSI;
namespace Robust.Client.GameObjects;
// This partial class contains various public methods for reading a layer's properties
public sealed partial class SpriteSystem
{
#region RsiState
/// <summary>
/// Get the RSI state being used by the current layer. Note that the return value may be an invalid state. E.g.,
/// this might be a texture layer that does not use RSIs.
/// </summary>
public StateId LayerGetRsiState(Entity<SpriteComponent?> sprite, int index)
{
if (TryGetLayer(sprite, index, out var layer, true))
return layer.StateId;
return StateId.Invalid;
}
/// <summary>
/// Get the RSI state being used by the current layer. Note that the return value may be an invalid state. E.g.,
/// this might be a texture layer that does not use RSIs.
/// </summary>
public StateId LayerGetRsiState(Entity<SpriteComponent?> sprite, string key, StateId state)
{
if (TryGetLayer(sprite, key, out var layer, true))
return layer.StateId;
return StateId.Invalid;
}
/// <summary>
/// Get the RSI state being used by the current layer. Note that the return value may be an invalid state. E.g.,
/// this might be a texture layer that does not use RSIs.
/// </summary>
public StateId LayerGetRsiState(Entity<SpriteComponent?> sprite, Enum key, StateId state)
{
if (TryGetLayer(sprite, key, out var layer, true))
return layer.StateId;
return StateId.Invalid;
}
#endregion
#region RsiState
/// <summary>
/// Returns the RSI being used by the layer to resolve it's RSI state. If the layer does not specify an RSI, this
/// will just be the base RSI of the owning sprite (<see cref="SpriteComponent.BaseRSI"/>).
/// </summary>
public RSI? LayerGetEffectiveRsi(Entity<SpriteComponent?> sprite, int index)
{
TryGetLayer(sprite, index, out var layer, true);
return layer?.ActualRsi;
}
/// <summary>
/// Returns the RSI being used by the layer to resolve it's RSI state. If the layer does not specify an RSI, this
/// will just be the base RSI of the owning sprite (<see cref="SpriteComponent.BaseRSI"/>).
/// </summary>
public RSI? LayerGetEffectiveRsi(Entity<SpriteComponent?> sprite, string key, StateId state)
{
TryGetLayer(sprite, key, out var layer, true);
return layer?.ActualRsi;
}
/// <summary>
/// Returns the RSI being used by the layer to resolve it's RSI state. If the layer does not specify an RSI, this
/// will just be the base RSI of the owning sprite (<see cref="SpriteComponent.BaseRSI"/>).
/// </summary>
public RSI? LayerGetEffectiveRsi(Entity<SpriteComponent?> sprite, Enum key, StateId state)
{
TryGetLayer(sprite, key, out var layer, true);
return layer?.ActualRsi;
}
#endregion
#region Directions
public RsiDirectionType LayerGetDirections(Entity<SpriteComponent?> sprite, int index)
{
return TryGetLayer(sprite, index, out var layer, true)
? LayerGetDirections(layer)
: RsiDirectionType.Dir1;
}
public RsiDirectionType LayerGetDirections(Entity<SpriteComponent?> sprite, Enum key)
{
return TryGetLayer(sprite, key, out var layer, true)
? LayerGetDirections(layer)
: RsiDirectionType.Dir1;
}
public RsiDirectionType LayerGetDirections(Entity<SpriteComponent?> sprite, string key)
{
return TryGetLayer(sprite, key, out var layer, true)
? LayerGetDirections(layer)
: RsiDirectionType.Dir1;
}
public RsiDirectionType LayerGetDirections(Layer layer)
{
if (!layer.StateId.IsValid)
return RsiDirectionType.Dir1;
// Pull texture from RSI state instead.
if (layer.ActualRsi is not {} rsi || !rsi.TryGetState(layer.StateId, out var state))
return RsiDirectionType.Dir1;
return state.RsiDirections;
}
public int LayerGetDirectionCount(Entity<SpriteComponent?> sprite, int index)
{
return TryGetLayer(sprite, index, out var layer, true) ? LayerGetDirectionCount(layer) : 1;
}
public int LayerGetDirectionCount(Entity<SpriteComponent?> sprite, Enum key)
{
return TryGetLayer(sprite, key, out var layer, true) ? LayerGetDirectionCount(layer) : 1;
}
public int LayerGetDirectionCount(Entity<SpriteComponent?> sprite, string key)
{
return TryGetLayer(sprite, key, out var layer, true) ? LayerGetDirectionCount(layer) : 1;
}
public int LayerGetDirectionCount(Layer layer)
{
return LayerGetDirections(layer) switch
{
RsiDirectionType.Dir1 => 1,
RsiDirectionType.Dir4 => 4,
RsiDirectionType.Dir8 => 8,
_ => throw new ArgumentOutOfRangeException()
};
}
#endregion
}

View File

@@ -0,0 +1,301 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Robust.Client.GameObjects;
// This partial class contains various public methods for manipulating layer mappings.
public sealed partial class SpriteSystem
{
/// <summary>
/// Map an enum to a layer index.
/// </summary>
public void LayerMapSet(Entity<SpriteComponent?> sprite, Enum key, int index)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (index < 0 || index >= sprite.Comp.Layers.Count)
throw new ArgumentOutOfRangeException(nameof(index));
sprite.Comp.LayerMap[key] = index;
}
/// <summary>
/// Map string to a layer index. If possible, it is preferred to use an enum key.
/// string keys mainly exist to make it easier to define custom layer keys in yaml.
/// </summary>
public void LayerMapSet(Entity<SpriteComponent?> sprite, string key, int index)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (index < 0 || index >= sprite.Comp.Layers.Count)
throw new ArgumentOutOfRangeException(nameof(index));
sprite.Comp.LayerMap[key] = index;
}
/// <summary>
/// Map an enum to a layer index.
/// </summary>
public void LayerMapAdd(Entity<SpriteComponent?> sprite, Enum key, int index)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (index < 0 || index >= sprite.Comp.Layers.Count)
throw new ArgumentOutOfRangeException(nameof(index));
sprite.Comp.LayerMap.Add(key, index);
}
/// <summary>
/// Map a string to a layer index. If possible, it is preferred to use an enum key.
/// string keys mainly exist to make it easier to define custom layer keys in yaml.
/// </summary>
public void LayerMapAdd(Entity<SpriteComponent?> sprite, string key, int index)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (index < 0 || index >= sprite.Comp.Layers.Count)
throw new ArgumentOutOfRangeException(nameof(index));
sprite.Comp.LayerMap.Add(key, index);
}
/// <summary>
/// Remove an enum mapping.
/// </summary>
public bool LayerMapRemove(Entity<SpriteComponent?> sprite, Enum key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return false;
return sprite.Comp.LayerMap.Remove(key);
}
/// <summary>
/// Remove a string mapping.
/// </summary>
public bool LayerMapRemove(Entity<SpriteComponent?> sprite, string key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return false;
return sprite.Comp.LayerMap.Remove(key);
}
/// <summary>
/// Remove an enum mapping.
/// </summary>
public bool LayerMapRemove(Entity<SpriteComponent?> sprite, Enum key, out int index)
{
if (_query.Resolve(sprite.Owner, ref sprite.Comp))
return sprite.Comp.LayerMap.Remove(key, out index);
index = 0;
return false;
}
/// <summary>
/// Remove a string mapping.
/// </summary>
public bool LayerMapRemove(Entity<SpriteComponent?> sprite, string key, out int index)
{
if (_query.Resolve(sprite.Owner, ref sprite.Comp))
return sprite.Comp.LayerMap.Remove(key, out index);
index = 0;
return false;
}
/// <summary>
/// Attempt to resolve an enum mapping.
/// </summary>
public bool LayerMapTryGet(Entity<SpriteComponent?> sprite, Enum key, out int index, bool logMissing)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
{
index = 0;
return false;
}
if (sprite.Comp.LayerMap.TryGetValue(key, out index))
return true;
if (logMissing)
Log.Error($"Layer with key '{key}' does not exist on entity {ToPrettyString(sprite)}! Trace:\n{Environment.StackTrace}");
return false;
}
/// <summary>
/// Attempt to resolve a string mapping.
/// </summary>
public bool LayerMapTryGet(Entity<SpriteComponent?> sprite, string key, out int index, bool logMissing)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
{
index = 0;
return false;
}
if (sprite.Comp.LayerMap.TryGetValue(key, out index))
return true;
if (logMissing)
Log.Error($"Layer with key '{key}' does not exist on entity {ToPrettyString(sprite)}! Trace:\n{Environment.StackTrace}");
return false;
}
/// <summary>
/// Attempt to resolve an enum mapping.
/// </summary>
public bool TryGetLayer(Entity<SpriteComponent?> sprite, Enum key, [NotNullWhen(true)] out Layer? layer, bool logMissing)
{
layer = null;
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
return LayerMapTryGet(sprite, key, out var index, logMissing)
&& TryGetLayer(sprite, index, out layer, logMissing);
}
/// <summary>
/// Attempt to resolve a string mapping.
/// </summary>
public bool TryGetLayer(Entity<SpriteComponent?> sprite, string key, [NotNullWhen(true)] out Layer? layer, bool logMissing)
{
layer = null;
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
return LayerMapTryGet(sprite, key, out var index, logMissing)
&& TryGetLayer(sprite, index, out layer, logMissing);
}
public int LayerMapGet(Entity<SpriteComponent?> sprite, Enum key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
return sprite.Comp.LayerMap[key];
}
public int LayerMapGet(Entity<SpriteComponent?> sprite, string key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
return sprite.Comp.LayerMap[key];
}
public bool LayerExists(Entity<SpriteComponent?> sprite, string key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return false;
return sprite.Comp.LayerMap.TryGetValue(key, out var index)
&& LayerExists(sprite, index);
}
public bool LayerExists(Entity<SpriteComponent?> sprite, Enum key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return false;
return sprite.Comp.LayerMap.TryGetValue(key, out var index)
&& LayerExists(sprite, index);
}
/// <summary>
/// Create a new blank layer and map the given key to it.
/// </summary>
public int LayerMapReserve(Entity<SpriteComponent?> sprite, Enum key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
if (LayerExists(sprite, key))
throw new Exception("Layer already exists");
var layer = AddBlankLayer(sprite!);
LayerMapSet(sprite, key, layer.Index);
return layer.Index;
}
/// <summary>
/// A create a new blank layer and map the given key to it. If possible, it is preferred to use an enum key.
/// string keys mainly exist to make it easier to define custom layer keys in yaml.
/// </summary>
public int LayerMapReserve(Entity<SpriteComponent?> sprite, string key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
if (LayerExists(sprite, key))
throw new Exception("Layer already exists");
var layer = AddBlankLayer(sprite!);
LayerMapSet(sprite, key, layer.Index);
return layer.Index;
}
public bool RemoveLayer(Entity<SpriteComponent?> sprite, string key, bool logMissing = true)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
if (!LayerMapTryGet(sprite, key, out var index, logMissing))
return false;
return RemoveLayer(sprite, index, logMissing);
}
public bool RemoveLayer(Entity<SpriteComponent?> sprite, Enum key, bool logMissing = true)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
if (!LayerMapTryGet(sprite, key, out var index, logMissing))
return false;
return RemoveLayer(sprite, index, logMissing);
}
public bool RemoveLayer(
Entity<SpriteComponent?> sprite,
string key,
[NotNullWhen(true)] out Layer? layer,
bool logMissing = true)
{
layer = null;
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
if (!LayerMapTryGet(sprite, key, out var index, logMissing))
return false;
return RemoveLayer(sprite, index, out layer, logMissing);
}
public bool RemoveLayer(
Entity<SpriteComponent?> sprite,
Enum key,
[NotNullWhen(true)] out Layer? layer,
bool logMissing = true)
{
layer = null;
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
if (!LayerMapTryGet(sprite, key, out var index, logMissing))
return false;
return RemoveLayer(sprite, index, out layer, logMissing);
}
}

View File

@@ -0,0 +1,605 @@
using System;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
using static Robust.Client.Graphics.RSI;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Robust.Client.GameObjects;
// This partial class contains various public methods for modifying a layer's properties.
public sealed partial class SpriteSystem
{
#region SetData
public void LayerSetData(Entity<SpriteComponent?> sprite, int index, PrototypeLayerData data)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetData(layer, data);
}
public void LayerSetData(Entity<SpriteComponent?> sprite, string key, PrototypeLayerData data)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetData(layer, data);
}
public void LayerSetData(Entity<SpriteComponent?> sprite, Enum key, PrototypeLayerData data)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetData(layer, data);
}
public void LayerSetData(Layer layer, PrototypeLayerData data)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
// TODO SPRITE ECS
layer._parent.LayerSetData(layer, data);
}
#endregion
#region SpriteSpecifier
public void LayerSetSprite(Entity<SpriteComponent?> sprite, int index, SpriteSpecifier specifier)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetSprite(layer, specifier);
}
public void LayerSetSprite(Entity<SpriteComponent?> sprite, string key, SpriteSpecifier specifier)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetSprite(layer, specifier);
}
public void LayerSetSprite(Entity<SpriteComponent?> sprite, Enum key, SpriteSpecifier specifier)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetSprite(layer, specifier);
}
public void LayerSetSprite(Layer layer, SpriteSpecifier specifier)
{
switch (specifier)
{
case SpriteSpecifier.Texture tex:
LayerSetTexture(layer, tex.TexturePath);
break;
case SpriteSpecifier.Rsi rsi:
LayerSetRsi(layer, rsi.RsiPath, rsi.RsiState);
break;
default:
throw new NotImplementedException();
}
}
#endregion
#region Texture
public void LayerSetTexture(Entity<SpriteComponent?> sprite, int index, Texture? texture)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetTexture(layer, texture);
}
public void LayerSetTexture(Entity<SpriteComponent?> sprite, string key, Texture? texture)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetTexture(layer, texture);
}
public void LayerSetTexture(Entity<SpriteComponent?> sprite, Enum key, Texture? texture)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetTexture(layer, texture);
}
public void LayerSetTexture(Layer layer, Texture? texture)
{
LayerSetRsiState(layer, StateId.Invalid, refresh: true);
layer.Texture = texture;
}
public void LayerSetTexture(Entity<SpriteComponent?> sprite, int index, ResPath path)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetTexture(layer, path);
}
public void LayerSetTexture(Entity<SpriteComponent?> sprite, string key, ResPath path)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetTexture(layer, path);
}
public void LayerSetTexture(Entity<SpriteComponent?> sprite, Enum key, ResPath path)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetTexture(layer, path);
}
private void LayerSetTexture(Layer layer, ResPath path)
{
if (!_resourceCache.TryGetResource<TextureResource>(TextureRoot / path, out var texture))
{
if (path.Extension == "rsi")
Log.Error($"Expected texture but got rsi '{path}', did you mean 'sprite:' instead of 'texture:'?");
Log.Error($"Unable to load texture '{path}'. Trace:\n{Environment.StackTrace}");
}
LayerSetTexture(layer, texture?.Texture);
}
#endregion
#region RsiState
public void LayerSetRsiState(Entity<SpriteComponent?> sprite, int index, StateId state)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetRsiState(layer, state);
}
public void LayerSetRsiState(Entity<SpriteComponent?> sprite, string key, StateId state)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRsiState(layer, state);
}
public void LayerSetRsiState(Entity<SpriteComponent?> sprite, Enum key, StateId state)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRsiState(layer, state);
}
public void LayerSetRsiState(Layer layer, StateId state, bool refresh = false)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (layer.StateId == state && !refresh)
return;
layer.StateId = state;
RefreshCachedState(layer, true, null);
_tree.QueueTreeUpdate(layer.Owner);
QueueUpdateIsInert(layer.Owner);
layer.BoundsDirty = true;
layer.Owner.Comp.BoundsDirty = true;
}
#endregion
#region Rsi
public void LayerSetRsi(Entity<SpriteComponent?> sprite, int index, RSI? rsi, StateId? state = null)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetRsi(layer, rsi, state);
}
public void LayerSetRsi(Entity<SpriteComponent?> sprite, string key, RSI? rsi, StateId? state = null)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRsi(layer, rsi, state);
}
public void LayerSetRsi(Entity<SpriteComponent?> sprite, Enum key, RSI? rsi, StateId? state = null)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRsi(layer, rsi, state);
}
public void LayerSetRsi(Layer layer, RSI? rsi, StateId? state = null)
{
layer._rsi = rsi;
LayerSetRsiState(layer, state ?? layer.StateId, refresh: true);
}
public void LayerSetRsi(Entity<SpriteComponent?> sprite, int index, ResPath rsi, StateId? state = null)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetRsi(layer, rsi, state);
}
public void LayerSetRsi(Entity<SpriteComponent?> sprite, string key, ResPath rsi, StateId? state = null)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRsi(layer, rsi, state);
}
public void LayerSetRsi(Entity<SpriteComponent?> sprite, Enum key, ResPath rsi, StateId? state = null)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRsi(layer, rsi, state);
}
public void LayerSetRsi(Layer layer, ResPath rsi, StateId? state = null)
{
if (!_resourceCache.TryGetResource<RSIResource>(TextureRoot / rsi, out var res))
Log.Error($"Unable to load RSI '{rsi}' for entity {ToPrettyString(layer.Owner)}. Trace:\n{Environment.StackTrace}");
LayerSetRsi(layer, res?.RSI, state);
}
#endregion
#region Scale
public void LayerSetScale(Entity<SpriteComponent?> sprite, int index, Vector2 value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetScale(layer, value);
}
public void LayerSetScale(Entity<SpriteComponent?> sprite, string key, Vector2 value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetScale(layer, value);
}
public void LayerSetScale(Entity<SpriteComponent?> sprite, Enum key, Vector2 value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetScale(layer, value);
}
public void LayerSetScale(Layer layer, Vector2 value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (layer._scale.EqualsApprox(value))
return;
if (!ValidateScale(layer.Owner, value))
return;
layer._scale = value;
layer.UpdateLocalMatrix();
_tree.QueueTreeUpdate(layer.Owner);
layer.BoundsDirty = true;
layer.Owner.Comp.BoundsDirty = true;
}
#endregion
#region Rotation
public void LayerSetRotation(Entity<SpriteComponent?> sprite, int index, Angle value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetRotation(layer, value);
}
public void LayerSetRotation(Entity<SpriteComponent?> sprite, string key, Angle value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRotation(layer, value);
}
public void LayerSetRotation(Entity<SpriteComponent?> sprite, Enum key, Angle value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRotation(layer, value);
}
public void LayerSetRotation(Layer layer, Angle value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (layer._rotation.EqualsApprox(value))
return;
layer._rotation = value;
layer.UpdateLocalMatrix();
_tree.QueueTreeUpdate(layer.Owner);
layer.BoundsDirty = true;
layer.Owner.Comp.BoundsDirty = true;
}
#endregion
#region Offset
public void LayerSetOffset(Entity<SpriteComponent?> sprite, int index, Vector2 value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetOffset(layer, value);
}
public void LayerSetOffset(Entity<SpriteComponent?> sprite, string key, Vector2 value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetOffset(layer, value);
}
public void LayerSetOffset(Entity<SpriteComponent?> sprite, Enum key, Vector2 value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetOffset(layer, value);
}
public void LayerSetOffset(Layer layer, Vector2 value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (layer._offset.EqualsApprox(value))
return;
layer._offset = value;
layer.UpdateLocalMatrix();
_tree.QueueTreeUpdate(layer.Owner);
layer.BoundsDirty = true;
layer.Owner.Comp.BoundsDirty = true;
}
#endregion
#region Visible
public void LayerSetVisible(Entity<SpriteComponent?> sprite, int index, bool value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetVisible(layer, value);
}
public void LayerSetVisible(Entity<SpriteComponent?> sprite, string key, bool value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetVisible(layer, value);
}
public void LayerSetVisible(Entity<SpriteComponent?> sprite, Enum key, bool value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetVisible(layer, value);
}
public void LayerSetVisible(Layer layer, bool value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (layer._visible == value)
return;
layer._visible = value;
QueueUpdateIsInert(layer.Owner);
_tree.QueueTreeUpdate(layer.Owner);
layer.Owner.Comp.BoundsDirty = true;
}
#endregion
#region Color
public void LayerSetColor(Entity<SpriteComponent?> sprite, int index, Color value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetColor(layer, value);
}
public void LayerSetColor(Entity<SpriteComponent?> sprite, string key, Color value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetColor(layer, value);
}
public void LayerSetColor(Entity<SpriteComponent?> sprite, Enum key, Color value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetColor(layer, value);
}
public void LayerSetColor(Layer layer, Color value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
layer.Color = value;
}
#endregion
#region DirOffset
public void LayerSetDirOffset(Entity<SpriteComponent?> sprite, int index, DirectionOffset value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetDirOffset(layer, value);
}
public void LayerSetDirOffset(Entity<SpriteComponent?> sprite, string key, DirectionOffset value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetDirOffset(layer, value);
}
public void LayerSetDirOffset(Entity<SpriteComponent?> sprite, Enum key, DirectionOffset value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetDirOffset(layer, value);
}
public void LayerSetDirOffset(Layer layer, DirectionOffset value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
layer.DirOffset = value;
}
#endregion
#region AnimationTime
public void LayerSetAnimationTime(Entity<SpriteComponent?> sprite, int index, float value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetAnimationTime(layer, value);
}
public void LayerSetAnimationTime(Entity<SpriteComponent?> sprite, string key, float value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetAnimationTime(layer, value);
}
public void LayerSetAnimationTime(Entity<SpriteComponent?> sprite, Enum key, float value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetAnimationTime(layer, value);
}
public void LayerSetAnimationTime(Layer layer, float value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (!layer.StateId.IsValid)
return;
if (layer.ActualRsi is not { } rsi)
return;
var state = rsi[layer.StateId];
if (value > layer.AnimationTime)
{
// Handle advancing differently from going backwards.
layer.AnimationTimeLeft -= (value - layer.AnimationTime);
}
else
{
// Going backwards we re-calculate from zero.
// Definitely possible to optimize this for going backwards but I'm too lazy to figure that out.
layer.AnimationTimeLeft = -value + state.GetDelay(0);
layer.AnimationFrame = 0;
}
layer.AnimationTime = value;
layer.AdvanceFrameAnimation(state);
layer.SetAnimationTime(value);
}
#endregion
#region AutoAnimated
public void LayerSetAutoAnimated(Entity<SpriteComponent?> sprite, int index, bool value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetAutoAnimated(layer, value);
}
public void LayerSetAutoAnimated(Entity<SpriteComponent?> sprite, string key, bool value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetAutoAnimated(layer, value);
}
public void LayerSetAutoAnimated(Entity<SpriteComponent?> sprite, Enum key, bool value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetAutoAnimated(layer, value);
}
public void LayerSetAutoAnimated(Layer layer, bool value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (layer._autoAnimated == value)
return;
layer._autoAnimated = value;
QueueUpdateIsInert(layer.Owner);
}
#endregion
#region LayerSetRenderingStrategy
public void LayerSetRenderingStrategy(Entity<SpriteComponent?> sprite, int index, LayerRenderingStrategy value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetRenderingStrategy(layer, value);
}
public void LayerSetRenderingStrategy(Entity<SpriteComponent?> sprite, string key, LayerRenderingStrategy value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRenderingStrategy(layer, value);
}
public void LayerSetRenderingStrategy(Entity<SpriteComponent?> sprite, Enum key, LayerRenderingStrategy value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRenderingStrategy(layer, value);
}
public void LayerSetRenderingStrategy(Layer layer, LayerRenderingStrategy value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
layer.RenderingStrategy = value;
layer.BoundsDirty = true;
layer.Owner.Comp.BoundsDirty = true;
_tree.QueueTreeUpdate(layer.Owner);
}
#endregion
/// <summary>
/// Refreshes an RSI layer's cached RSI state.
/// </summary>
private void RefreshCachedState(Layer layer, bool logErrors, RSI.State? fallback)
{
if (!layer.StateId.IsValid)
{
layer._actualState = null;
}
else if (layer.ActualRsi is not { } rsi)
{
layer._actualState = fallback ?? GetFallbackState();
if (logErrors)
Log.Error(
$"{ToPrettyString(layer.Owner)} has no RSI to pull new state from! Trace:\n{Environment.StackTrace}");
}
else if (!rsi.TryGetState(layer.StateId, out layer._actualState))
{
layer._actualState = fallback ?? GetFallbackState();
if (logErrors)
Log.Error(
$"{ToPrettyString(layer.Owner)}'s state '{layer.StateId}' does not exist in RSI {rsi.Path}. Trace:\n{Environment.StackTrace}");
}
layer.AnimationFrame = 0;
layer.AnimationTime = 0;
layer.AnimationTimeLeft = layer._actualState?.GetDelay(0) ?? 0f;
}
}

View File

@@ -0,0 +1,183 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Client.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Maths;
using static Robust.Client.GameObjects.SpriteComponent;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.GameObjects;
// This partial class contains code related to actually rendering sprites.
public sealed partial class SpriteSystem
{
public void RenderSprite(
Entity<SpriteComponent> sprite,
DrawingHandleWorld drawingHandle,
Angle eyeRotation,
Angle worldRotation,
Vector2 worldPosition)
{
RenderSprite(sprite,
drawingHandle,
eyeRotation,
worldRotation,
worldPosition,
sprite.Comp.EnableDirectionOverride ? sprite.Comp.DirectionOverride : null);
}
public void RenderSprite(
Entity<SpriteComponent> sprite,
DrawingHandleWorld drawingHandle,
Angle eyeRotation,
Angle worldRotation,
Vector2 worldPosition,
Direction? overrideDirection)
{
// TODO SPRITE RENDERING
// Add fast path for simple sprites.
// I.e., when a sprite is modified, check if it is "simple". If it is. cache texture information in a struct
// and use a fast path here.
// E.g., simple 1-directional, 1-layer sprites can basically become a direct texture draw call. (most in game items).
// Similarly, 1-directional multi-layer sprites can become a sequence of direct draw calls (most in game walls).
if (!sprite.Comp.IsInert)
_queuedFrameUpdate.Add(sprite);
var angle = worldRotation + eyeRotation; // angle on-screen. Used to decide the direction of 4/8 directional RSIs
angle = angle.Reduced().FlipPositive(); // Reduce the angles to fix math shenanigans
var cardinal = Angle.Zero;
// If we have a 1-directional sprite then snap it to try and always face it south if applicable.
if (sprite.Comp is {NoRotation: false, SnapCardinals: true})
cardinal = angle.RoundToCardinalAngle();
// worldRotation + eyeRotation should be the angle of the entity on-screen. If no-rot is enabled this is just set to zero.
// However, at some point later the eye-matrix is applied separately, so we subtract -eye rotation for now:
var entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, sprite.Comp.NoRotation ? -eyeRotation : worldRotation - cardinal);
var spriteMatrix = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix);
// Fast path for when all sprites use the same transform matrix
if (!sprite.Comp.GranularLayersRendering)
{
foreach (var layer in sprite.Comp.Layers)
{
RenderLayer(layer, drawingHandle, ref spriteMatrix, angle, overrideDirection);
}
return;
}
//Default rendering (NoRotation = false)
entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, worldRotation);
var transformDefault = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix);
//Snap to cardinals
entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, worldRotation - angle.RoundToCardinalAngle());
var transformSnap = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix);
//No rotation
entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, -eyeRotation);
var transformNoRot = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix);
foreach (var layer in sprite.Comp.Layers)
{
switch (layer.RenderingStrategy)
{
case LayerRenderingStrategy.UseSpriteStrategy:
RenderLayer(layer, drawingHandle, ref spriteMatrix, angle, overrideDirection);
break;
case LayerRenderingStrategy.Default:
RenderLayer(layer, drawingHandle, ref transformDefault, angle, overrideDirection);
break;
case LayerRenderingStrategy.NoRotation:
RenderLayer(layer, drawingHandle, ref transformNoRot, angle, overrideDirection);
break;
case LayerRenderingStrategy.SnapToCardinals:
RenderLayer(layer, drawingHandle, ref transformSnap, angle, overrideDirection);
break;
default:
Log.Error($"Tried to render a layer with unknown rendering stragegy: {layer.RenderingStrategy}");
break;
}
}
}
/// <summary>
/// Render a layer. This assumes that the input angle is between 0 and 2pi.
/// </summary>
private void RenderLayer(Layer layer, DrawingHandleWorld drawingHandle, ref Matrix3x2 spriteMatrix, Angle angle, Direction? overrideDirection)
{
if (!layer.Visible || layer.Blank)
return;
var state = layer._actualState;
var dir = state == null ? RsiDirection.South : Layer.GetDirection(state.RsiDirections, angle);
// Set the drawing transform for this layer
layer.GetLayerDrawMatrix(dir, out var layerMatrix, layer.Owner.Comp.NoRotation);
// The direction used to draw the sprite can differ from the one that the angle would naively suggest,
// due to direction overrides or offsets.
if (overrideDirection != null && state != null)
dir = overrideDirection.Value.Convert(state.RsiDirections);
dir = dir.OffsetRsiDir(layer.DirOffset);
var texture = state?.GetFrame(dir, layer.AnimationFrame) ?? layer.Texture ?? GetFallbackTexture();
// TODO SPRITE
// Refactor shader-param-layers to a separate layer type after layers are split into types & collections.
// I.e., separate Layer -> RsiLayer, TextureLayer, LayerCollection, SpriteLayer, and ShaderLayer
if (layer.CopyToShaderParameters != null)
{
HandleShaderLayer(layer, texture, layer.CopyToShaderParameters);
return;
}
// Set the drawing transform for this layer
var transformMatrix = Matrix3x2.Multiply(layerMatrix, spriteMatrix);
drawingHandle.SetTransform(in transformMatrix);
if (layer.Shader != null)
drawingHandle.UseShader(layer.Shader);
var layerColor = layer.Owner.Comp.color * layer.Color;
var textureSize = texture.Size / (float) EyeManager.PixelsPerMeter;
var quad = Box2.FromDimensions(textureSize / -2, textureSize);
drawingHandle.DrawTextureRectRegion(texture, quad, layerColor);
if (layer.Shader != null)
drawingHandle.UseShader(null);
}
/// <summary>
/// Handle a a "fake layer" that just exists to modify the parameters of a shader being used by some other
/// layer.
/// </summary>
private void HandleShaderLayer(Layer layer, Texture texture, CopyToShaderParameters @params)
{
// Multiple atrocities to god being committed right here.
var otherLayerIdx = layer._parent.LayerMap[@params.LayerKey!];
var otherLayer = layer._parent.Layers[otherLayerIdx];
if (otherLayer.Shader is not { } shader)
return;
if (!shader.Mutable)
otherLayer.Shader = shader = shader.Duplicate();
var clydeTexture = Clyde.RenderHandle.ExtractTexture(texture, null, out var csr);
if (@params.ParameterTexture is { } paramTexture)
shader.SetParameter(paramTexture, clydeTexture);
if (@params.ParameterUV is not { } paramUV)
return;
var sr = Clyde.RenderHandle.WorldTextureBoundsToUV(clydeTexture, csr);
var uv = new Vector4(sr.Left, sr.Bottom, sr.Right, sr.Top);
shader.SetParameter(paramUV, uv);
}
}

View File

@@ -0,0 +1,166 @@
using System;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects;
// This partial class contains various public methods for setting sprite component data.
public sealed partial class SpriteSystem
{
private bool ValidateScale(Entity<SpriteComponent> sprite, Vector2 scale)
{
if (!(MathF.Abs(scale.X) < 0.005f) && !(MathF.Abs(scale.Y) < 0.005f))
return true;
// Scales of ~0.0025 or lower can lead to singular matrices due to rounding errors.
Log.Error(
$"Attempted to set layer sprite scale to very small values. Entity: {ToPrettyString(sprite)}. Scale: {scale}");
return false;
}
#region Transform
public void SetScale(Entity<SpriteComponent?> sprite, Vector2 value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (!ValidateScale(sprite!, value))
return;
sprite.Comp._bounds = sprite.Comp._bounds.Scale(value / sprite.Comp.scale);
sprite.Comp.scale = value;
sprite.Comp.LocalMatrix = Matrix3Helpers.CreateTransform(
in sprite.Comp.offset,
in sprite.Comp.rotation,
in sprite.Comp.scale);
}
public void SetRotation(Entity<SpriteComponent?> sprite, Angle value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
sprite.Comp.rotation = value;
sprite.Comp.LocalMatrix = Matrix3Helpers.CreateTransform(
in sprite.Comp.offset,
in sprite.Comp.rotation,
in sprite.Comp.scale);
}
public void SetOffset(Entity<SpriteComponent?> sprite, Vector2 value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
sprite.Comp.offset = value;
sprite.Comp.LocalMatrix = Matrix3Helpers.CreateTransform(
in sprite.Comp.offset,
in sprite.Comp.rotation,
in sprite.Comp.scale);
}
#endregion
public void SetVisible(Entity<SpriteComponent?> sprite, bool value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (sprite.Comp.Visible == value)
return;
sprite.Comp._visible = value;
_tree.QueueTreeUpdate(sprite!);
}
public void SetDrawDepth(Entity<SpriteComponent?> sprite, int value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
sprite.Comp.drawDepth = value;
}
public void SetColor(Entity<SpriteComponent?> sprite, Color value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
sprite.Comp.color = value;
}
/// <summary>
/// Modify a sprites base RSI. This is the RSI that is used by any RSI layers that do not specify their own.
/// Note that changing the base RSI may result in existing layers having an invalid state. This will not log errors
/// under the assumption that the states of each layers will be updated after the base RSI has changed.
/// </summary>
public void SetBaseRsi(Entity<SpriteComponent?> sprite, RSI? value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (value == sprite.Comp._baseRsi)
return;
sprite.Comp._baseRsi = value;
if (value == null)
return;
var fallback = GetFallbackState();
for (var i = 0; i < sprite.Comp.Layers.Count; i++)
{
var layer = sprite.Comp.Layers[i];
if (!layer.State.IsValid || layer.RSI != null)
continue;
RefreshCachedState(layer, logErrors: false, fallback);
if (value.TryGetState(layer.State, out var state))
{
layer.AnimationTimeLeft = state.GetDelay(0);
}
else
{
Log.Error($"Layer {i} no longer has state '{layer.State}' due to base RSI change. Trace:\n{Environment.StackTrace}");
layer.Texture = null;
}
}
}
public void SetContainerOccluded(Entity<SpriteComponent?> sprite, bool value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
sprite.Comp._containerOccluded = value;
_tree.QueueTreeUpdate(sprite!);
}
public void SetSnapCardinals(Entity<SpriteComponent?> sprite, bool value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (value == sprite.Comp._snapCardinals)
return;
sprite.Comp._snapCardinals = value;
_tree.QueueTreeUpdate(sprite!);
DirtyBounds(sprite!);
}
public void SetGranularLayersRendering(Entity<SpriteComponent?> sprite, bool value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (value == sprite.Comp.GranularLayersRendering)
return;
sprite.Comp.GranularLayersRendering = value;
_tree.QueueTreeUpdate(sprite!);
DirtyBounds(sprite!);
}
}

View File

@@ -1,137 +0,0 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
public sealed partial class SpriteSystem
{
private readonly Dictionary<string, IRsiStateLike> _cachedPrototypeIcons = new();
public Texture Frame0(EntityPrototype prototype)
{
return GetPrototypeIcon(prototype).Default;
}
public Texture Frame0(SpriteSpecifier specifier)
{
return RsiStateLike(specifier).Default;
}
public IRsiStateLike RsiStateLike(SpriteSpecifier specifier)
{
switch (specifier)
{
case SpriteSpecifier.Texture tex:
return tex.GetTexture(_resourceCache);
case SpriteSpecifier.Rsi rsi:
return GetState(rsi);
case SpriteSpecifier.EntityPrototype prototypeIcon:
return GetPrototypeIcon(prototypeIcon.EntityPrototypeId);
default:
throw new NotSupportedException();
}
}
public Texture GetIcon(IconComponent icon)
{
return GetState(icon.Icon).Frame0;
}
/// <summary>
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
/// This method caches the result based on the prototype identifier.
/// </summary>
public IRsiStateLike GetPrototypeIcon(string prototype)
{
// Check if this prototype has been cached before, and if so return the result.
if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult))
return cachedResult;
if (!_proto.TryIndex<EntityPrototype>(prototype, out var entityPrototype))
{
// The specified prototype doesn't exist, return the fallback "error" sprite.
_sawmill.Error("Failed to load PrototypeIcon {0}", prototype);
return GetFallbackState();
}
// Generate the icon and cache it in case it's ever needed again.
var result = GetPrototypeIcon(entityPrototype);
_cachedPrototypeIcons[prototype] = result;
return result;
}
/// <summary>
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
/// This method does NOT cache the result.
/// </summary>
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
{
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
if (prototype.Components.TryGetValue("Icon", out var compData)
&& compData.Component is IconComponent icon)
{
return GetIcon(icon);
}
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
if (!prototype.Components.ContainsKey("Sprite"))
{
return GetFallbackState();
}
// Finally, we use spawn a dummy entity to get its icon.
var dummy = Spawn(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
var result = spriteComponent.Icon ?? GetFallbackState();
Del(dummy);
return result;
}
[Pure]
public RSI.State GetFallbackState()
{
return _resourceCache.GetFallback<RSIResource>().RSI["error"];
}
[Pure]
public RSI.State GetState(SpriteSpecifier.Rsi rsiSpecifier)
{
if (_resourceCache.TryGetResource<RSIResource>(
SpriteSpecifierSerializer.TextureRoot / rsiSpecifier.RsiPath,
out var theRsi) &&
theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
{
return state;
}
_sawmill.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath);
return GetFallbackState();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (!args.TryGetModified<EntityPrototype>(out var modified))
return;
// Remove all changed prototypes from the cache, if they're there.
foreach (var prototype in modified)
{
// Let's be lazy and not regenerate them until something needs them again.
_cachedPrototypeIcons.Remove(prototype);
}
}
}

View File

@@ -13,10 +13,9 @@ 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.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
@@ -36,24 +35,20 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly SharedTransformSystem _xforms = default!;
[Dependency] private readonly SpriteTreeSystem _tree = default!;
public static readonly ProtoId<ShaderPrototype> UnshadedId = "unshaded";
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
public static readonly ResPath TextureRoot = SpriteSpecifierSerializer.TextureRoot;
/// <summary>
/// Entities that require a sprite frame update.
/// </summary>
private readonly HashSet<EntityUid> _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);
}
private EntityQuery<SpriteComponent> _query;
public override void Initialize()
{
@@ -62,11 +57,11 @@ namespace Robust.Client.GameObjects
UpdatesAfter.Add(typeof(SpriteTreeSystem));
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
SubscribeLocalEvent<SpriteComponent, SpriteUpdateInertEvent>(QueueUpdateInert);
SubscribeLocalEvent<SpriteComponent, ComponentInit>(OnInit);
Subs.CVar(_cfg, CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
_sawmill = _logManager.GetSawmill("sprite");
_query = GetEntityQuery<SpriteComponent>();
}
public bool IsVisible(Layer layer)
@@ -85,18 +80,6 @@ namespace Robust.Client.GameObjects
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;

View File

@@ -318,7 +318,7 @@ namespace Robust.Client.Graphics.Clyde
screenSpriteSize.Y++;
bool exit = false;
if (entry.Sprite.GetScreenTexture)
if (entry.Sprite.GetScreenTexture && entry.Sprite.PostShader != null)
{
FlushRenderQueue();
var tex = CopyScreenTexture(viewport.RenderTarget);
@@ -369,7 +369,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
spriteSystem.Render(entry.Uid, entry.Sprite, _renderHandle.DrawingHandleWorld, eye.Rotation, in entry.WorldRot, in entry.WorldPos);
spriteSystem.RenderSprite(new(entry.Uid, entry.Sprite), _renderHandle.DrawingHandleWorld, eye.Rotation, entry.WorldRot, entry.WorldPos);
if (entry.Sprite.PostShader != null && entityPostRenderTarget != null)
{

View File

@@ -613,6 +613,8 @@ namespace Robust.Client.Graphics.Clyde
EnsureBatchSpaceAvailable(4, GetQuadBatchIndexCount());
EnsureBatchState(texture, true, GetQuadBatchPrimitiveType(), _queuedShader);
// TODO RENDERING
// It's probably better to do this on the GPU.
bl = Vector2.Transform(bl, _currentMatrixModel);
br = Vector2.Transform(br, _currentMatrixModel);
tr = Vector2.Transform(tr, _currentMatrixModel);

View File

@@ -126,7 +126,7 @@ namespace Robust.Client.Placement
sprite.Color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
var rot = args.Viewport.Eye?.Rotation ?? default;
spriteSys.Render(uid.Value, sprite, args.WorldHandle, rot, worldRot, worldPos);
spriteSys.RenderSprite((uid.Value, sprite), args.WorldHandle, rot, worldRot, worldPos);
}
}

View File

@@ -17,6 +17,7 @@ namespace Robust.Client.Utility
/// </summary>
public static class SpriteSpecifierExt
{
[Obsolete("Use SpriteSystem.GetTexture() instead")]
public static Texture GetTexture(this SpriteSpecifier.Texture texSpecifier, IResourceCache cache)
{
return cache
@@ -24,13 +25,14 @@ namespace Robust.Client.Utility
.Texture;
}
[Obsolete("Use SpriteSystem")]
[Obsolete("Use SpriteSystem.GetState() instead")]
public static RSI.State GetState(this SpriteSpecifier.Rsi rsiSpecifier, IResourceCache cache)
{
if (!cache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / rsiSpecifier.RsiPath, out var theRsi))
{
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
Logger.Error("SpriteSpecifier failed to load RSI {0}", rsiSpecifier.RsiPath);
return SpriteComponent.GetFallbackState(cache);
return sys.GetFallbackState();
}
if (theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
@@ -39,21 +41,22 @@ namespace Robust.Client.Utility
}
Logger.Error($"SpriteSpecifier has invalid RSI state '{rsiSpecifier.RsiState}' for RSI: {rsiSpecifier.RsiPath}");
return SpriteComponent.GetFallbackState(cache);
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>().GetFallbackState();
}
[Obsolete("Use SpriteSystem")]
[Obsolete("Use SpriteSystem.Frame0() instead")]
public static Texture Frame0(this SpriteSpecifier specifier)
{
return specifier.RsiStateLike().Default;
}
[Obsolete("Use SpriteSystem.RsiStateLike() instead")]
public static IDirectionalTextureProvider DirFrame0(this SpriteSpecifier specifier)
{
return specifier.RsiStateLike();
}
[Obsolete("Use SpriteSystem")]
[Obsolete("Use SpriteSystem.RsiStateLike() instead")]
public static IRsiStateLike RsiStateLike(this SpriteSpecifier specifier)
{
var resC = IoCManager.Resolve<IResourceCache>();
@@ -67,10 +70,11 @@ namespace Robust.Client.Utility
case SpriteSpecifier.EntityPrototype prototypeIcon:
var protMgr = IoCManager.Resolve<IPrototypeManager>();
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
if (!protMgr.TryIndex<EntityPrototype>(prototypeIcon.EntityPrototypeId, out var prototype))
{
Logger.Error("Failed to load PrototypeIcon {0}", prototypeIcon.EntityPrototypeId);
return SpriteComponent.GetFallbackState(resC);
return sys.GetFallbackState();
}
return SpriteComponent.GetPrototypeIcon(prototype, resC);

View File

@@ -104,6 +104,16 @@ namespace Robust.Shared.Maths
return (Direction) (Math.Floor((ang + CardinalOffset) / CardinalSegment) * 2 % 8);
}
/// <summary>
/// Rounds the angle to the nearest cardinal direction. This behaves similarly to a combination of
/// <see cref="GetCardinalDir"/> and Direction.ToAngle(), however this may return an angle outside of the range
/// returned by those methods (-pi to pi).
/// </summary>
public Angle RoundToCardinalAngle()
{
return new Angle(CardinalSegment * Math.Floor((Theta + CardinalOffset) / CardinalSegment));
}
/// <summary>
/// Rotates the vector counter-clockwise around its origin by the value of Theta.
/// </summary>

View File

@@ -111,6 +111,11 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
component.TreeUpdateQueued = true;
_updateQueue.Enqueue((component, xform));
}
public void QueueTreeUpdate(Entity<TComp> entity, TransformComponent? xform = null)
{
QueueTreeUpdate(entity.Owner, entity.Comp, xform);
}
#endregion
#region Component Management

View File

@@ -80,4 +80,8 @@ public enum LayerRenderingStrategy
SnapToCardinals,
NoRotation,
UseSpriteStrategy
// TODO SPRITE
// Refactor this make the sprites strategy the actual default.
// That way layers have to opt in to having a custom strategy, instead of opt out.
// Also rename default to make it clear that its not actually the default, instead I guess its "WithRotation"?
}

View File

@@ -25,7 +25,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations
ITypeCopier<Rsi>,
ITypeCopier<Texture>
{
// Should probably be in SpriteComponent, but is needed for server to validate paths.
// Should probably be in SpriteSystem, but is needed for server to validate paths.
// So I guess it might as well go here?
public static readonly ResPath TextureRoot = new("/Textures");