diff --git a/Robust.Client/GameObjects/ClientComponentFactory.cs b/Robust.Client/GameObjects/ClientComponentFactory.cs index bee27f6d57..4776b29da6 100644 --- a/Robust.Client/GameObjects/ClientComponentFactory.cs +++ b/Robust.Client/GameObjects/ClientComponentFactory.cs @@ -44,11 +44,6 @@ namespace Robust.Client.GameObjects Register(); RegisterReference(); - RegisterReference(); - - Register(); - RegisterReference(); - RegisterReference(); Register(); RegisterReference(); diff --git a/Robust.Client/GameObjects/Components/Input/ClickableComponent.cs b/Robust.Client/GameObjects/Components/Input/ClickableComponent.cs deleted file mode 100644 index ca75df4c6b..0000000000 --- a/Robust.Client/GameObjects/Components/Input/ClickableComponent.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Robust.Client.Graphics.Shaders; -using Robust.Client.Interfaces.GameObjects; -using Robust.Client.Interfaces.GameObjects.Components; -using Robust.Shared.GameObjects; -using Robust.Shared.GameObjects.Components; -using Robust.Shared.Input; -using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Map; -using Robust.Shared.Maths; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; -using Robust.Shared.ViewVariables; - -namespace Robust.Client.GameObjects -{ - // Notice: Most actual logic for clicking is done by the game screen. - public class ClickableComponent : Component, IClientClickableComponent - { - private Box2? _localBounds; - - public override string Name => "Clickable"; - public override uint? NetID => NetIDs.CLICKABLE; - - [ViewVariables] - public Box2? LocalBounds - { - get => _localBounds; - set => _localBounds = value; - } - - /// - public override void ExposeData(ObjectSerializer serializer) - { - serializer.DataField(ref _localBounds, "bounds", null); - } - - /// - public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) - { - if (curState is ClickableComponentState state) - { - _localBounds = state.LocalBounds; - } - } - - /// - //TODO: This needs to accept MapPosition, not a Vector2 - public bool CheckClick(Vector2 worldPos, out int drawdepth) - { - if (LocalBounds.HasValue) - { - var worldBounds = LocalBounds.Value.Translated(Owner.Transform.WorldPosition); - if (!worldBounds.Contains(worldPos)) - { - drawdepth = default; - return false; - } - } - - if (Owner.TryGetComponent(out ISpriteComponent sprite) && !sprite.Visible) - { - drawdepth = default; - return false; - } - - var component = Owner.GetComponent(); - drawdepth = component.DrawDepth; - return true; - } - } -} diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index bf49092fc2..8c64d0abb3 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -27,7 +27,7 @@ using Robust.Shared.ViewVariables; namespace Robust.Client.GameObjects { - public sealed class SpriteComponent : SharedSpriteComponent, ISpriteComponent, IClickTargetComponent, + public sealed class SpriteComponent : SharedSpriteComponent, ISpriteComponent, IComponentDebug { private bool _visible = true; @@ -159,8 +159,7 @@ namespace Robust.Client.GameObjects [Dependency] private readonly IPrototypeManager prototypes = default!; [Dependency] private readonly IReflectionManager reflectionManager = default!; - [ViewVariables(VVAccess.ReadWrite)] - public uint RenderOrder { get; set; } + [ViewVariables(VVAccess.ReadWrite)] public uint RenderOrder { get; set; } // TODO: this should absolutely not be static. private static ShaderInstance? _defaultShader; @@ -175,8 +174,7 @@ namespace Robust.Client.GameObjects const string LayerSerializationCache = "spritelayer"; const string LayerMapSerializationCache = "spritelayermap"; - [ViewVariables(VVAccess.ReadWrite)] - public bool IsInert { get; private set; } + [ViewVariables(VVAccess.ReadWrite)] public bool IsInert { get; private set; } /// public void LayerMapSet(object key, int layer) @@ -232,7 +230,7 @@ namespace Robust.Client.GameObjects public int AddBlankLayer(int? newIndex = null) { - var layer = new Layer {Visible = false}; + var layer = new Layer(this) {Visible = false}; return AddLayer(layer, newIndex); } @@ -247,8 +245,11 @@ namespace Robust.Client.GameObjects { if (texturePath.Extension == "rsi") { - Logger.ErrorS(LogCategory, "Expected texture but got rsi '{0}', did you mean 'sprite:' instead of 'texture:'?", texturePath); + Logger.ErrorS(LogCategory, + "Expected texture but got rsi '{0}', did you mean 'sprite:' instead of 'texture:'?", + texturePath); } + Logger.ErrorS(LogCategory, "Unable to load texture '{0}'. Trace:\n{1}", texturePath, Environment.StackTrace); } @@ -258,13 +259,13 @@ namespace Robust.Client.GameObjects public int AddLayer(Texture? texture, int? newIndex = null) { - var layer = new Layer {Texture = texture}; + var layer = new Layer(this) {Texture = texture}; return AddLayer(layer, newIndex); } public int AddLayer(RSI.StateId stateId, int? newIndex = null) { - var layer = new Layer {State = stateId}; + var layer = new Layer(this) {State = stateId}; if (BaseRSI != null && BaseRSI.TryGetState(stateId, out var state)) { layer.AnimationTimeLeft = state.GetDelay(0); @@ -310,7 +311,7 @@ namespace Robust.Client.GameObjects public int AddLayer(RSI.StateId stateId, RSI? rsi, int? newIndex = null) { - var layer = new Layer {State = stateId, RSI = rsi}; + var layer = new Layer(this) {State = stateId, RSI = rsi}; if (rsi != null && rsi.TryGetState(stateId, out var state)) { layer.AnimationTimeLeft = state.GetDelay(0); @@ -501,10 +502,7 @@ namespace Robust.Client.GameObjects } var theLayer = Layers[layer]; - theLayer.State = default; - theLayer.Texture = texture; - - UpdateIsInert(); + theLayer.SetTexture(texture); } public void LayerSetTexture(object layerKey, Texture texture) @@ -535,8 +533,11 @@ namespace Robust.Client.GameObjects { if (texturePath.Extension == "rsi") { - Logger.ErrorS(LogCategory, "Expected texture but got rsi '{0}', did you mean 'sprite:' instead of 'texture:'?", texturePath); + Logger.ErrorS(LogCategory, + "Expected texture but got rsi '{0}', did you mean 'sprite:' instead of 'texture:'?", + texturePath); } + Logger.ErrorS(LogCategory, "Unable to load texture '{0}'. Trace:\n{1}", texturePath, Environment.StackTrace); } @@ -566,34 +567,7 @@ namespace Robust.Client.GameObjects } var theLayer = Layers[layer]; - if (theLayer.State == stateId) - { - return; - } - - theLayer.State = stateId; - RSI.State? state; - var rsi = theLayer.RSI ?? BaseRSI; - if (rsi == null) - { - state = GetFallbackState(); - Logger.ErrorS(LogCategory, "No RSI to pull new state from! Trace:\n{0}", Environment.StackTrace); - } - else - { - if (!rsi.TryGetState(stateId, out state)) - { - state = GetFallbackState(); - Logger.ErrorS(LogCategory, "State '{0}' does not exist in RSI. Trace:\n{1}", stateId, - Environment.StackTrace); - } - } - - theLayer.AnimationFrame = 0; - theLayer.AnimationTime = 0; - theLayer.AnimationTimeLeft = state.GetDelay(0); - - UpdateIsInert(); + theLayer.SetState(stateId); } public void LayerSetState(object layerKey, RSI.StateId stateId) @@ -697,34 +671,7 @@ namespace Robust.Client.GameObjects } var theLayer = Layers[layer]; - theLayer.RSI = rsi; - if (!theLayer.State.IsValid) - { - return; - } - - // Gotta do this because somebody might use null as argument (totally valid). - var actualRsi = theLayer.RSI ?? BaseRSI; - if (actualRsi == null) - { - Logger.ErrorS(LogCategory, "No RSI to pull new state from! Trace:\n{0}", Environment.StackTrace); - theLayer.Texture = null; - } - else - { - if (actualRsi.TryGetState(theLayer.State, out var state)) - { - theLayer.AnimationTimeLeft = state.GetDelay(0); - } - else - { - Logger.ErrorS(LogCategory, "State '{0}' does not exist in set RSI. Trace:\n{1}", theLayer.State, - Environment.StackTrace); - theLayer.Texture = null; - } - } - - UpdateIsInert(); + theLayer.SetRsi(rsi); } public void LayerSetRSI(object layerKey, RSI rsi) @@ -831,10 +778,7 @@ namespace Robust.Client.GameObjects return; } - var theLayer = Layers[layer]; - theLayer.Visible = visible; - - UpdateIsInert(); + Layers[layer].SetVisible(visible); } public void LayerSetVisible(object layerKey, bool visible) @@ -903,47 +847,21 @@ namespace Robust.Client.GameObjects { if (Layers.Count <= layer) { - Logger.ErrorS(LogCategory, "Layer with index '{0}' does not exist, cannot set animation time! Trace:\n{1}", + Logger.ErrorS(LogCategory, + "Layer with index '{0}' does not exist, cannot set animation time! Trace:\n{1}", layer, Environment.StackTrace); return; } - var theLayer = Layers[layer]; - if (!theLayer.State.IsValid) - { - return; - } - - var theLayerRSI = theLayer.RSI ?? BaseRSI; - if (theLayerRSI == null) - { - return; - } - - var state = theLayerRSI[theLayer.State]; - if (animationTime > theLayer.AnimationTime) - { - // Handle advancing differently from going backwards. - theLayer.AnimationTimeLeft -= (animationTime - theLayer.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. - theLayer.AnimationTimeLeft = -animationTime + state.GetDelay(0); - theLayer.AnimationFrame = 0; - } - - theLayer.AnimationTime = animationTime; - // After setting timing data correctly, run advance to get to the correct frame. - _advanceFrameAnimation(theLayer, state); + Layers[layer].SetAnimationTime(animationTime); } public void LayerSetAnimationTime(object layerKey, float animationTime) { if (!LayerMapTryGet(layerKey, out var layer)) { - Logger.ErrorS(LogCategory, "Layer with key '{0}' does not exist, cannot set animation time! Trace:\n{1}", + Logger.ErrorS(LogCategory, + "Layer with key '{0}' does not exist, cannot set animation time! Trace:\n{1}", layerKey, Environment.StackTrace); return; } @@ -955,15 +873,13 @@ namespace Robust.Client.GameObjects { if (Layers.Count <= layer) { - Logger.ErrorS(LogCategory, "Layer with index '{0}' does not exist, cannot set auto animated! Trace:\n{1}", + Logger.ErrorS(LogCategory, + "Layer with index '{0}' does not exist, cannot set auto animated! Trace:\n{1}", layer, Environment.StackTrace); return; } - var theLayer = Layers[layer]; - theLayer.AutoAnimated = autoAnimated; - - UpdateIsInert(); + Layers[layer].SetAutoAnimated(autoAnimated); } public void LayerSetAutoAnimated(object layerKey, bool autoAnimated) @@ -994,26 +910,21 @@ namespace Robust.Client.GameObjects public RSI? LayerGetActualRSI(int layer) { - if (Layers.Count <= layer) - { - throw new ArgumentOutOfRangeException(nameof(layer), $"Layer '{layer}' does not exist."); - } - - var theLayer = Layers[layer]; - return BaseRSI ?? theLayer.RSI; + return this[layer].ActualRsi; } public RSI? LayerGetActualRSI(object layerKey) { - if (!LayerMapTryGet(layerKey, out var layer)) - { - throw new KeyNotFoundException($"Layer '{layerKey}' does not exist."); - } - - return LayerGetActualRSI(layer); + return this[layerKey].ActualRsi; } - internal void Render(DrawingHandleWorld drawingHandle, in Matrix3 worldTransform, Angle worldRotation, Direction? overrideDirection=null) + public ISpriteLayer this[int layer] => Layers[layer]; + public ISpriteLayer this[Index layer] => Layers[layer]; + public ISpriteLayer this[object layerKey] => this[LayerMap[layerKey]]; + public IEnumerable AllLayers => Layers; + + internal void Render(DrawingHandleWorld drawingHandle, in Matrix3 worldTransform, Angle worldRotation, + Direction? overrideDirection = null) { var angle = Rotation; if (Directional) @@ -1052,6 +963,7 @@ namespace Robust.Client.GameObjects } // TODO: Implement layer-specific rotation and scale. + // Oh and when you do update Layer.LocalToLayer so content doesn't break. var texture = layer.Texture; @@ -1064,26 +976,7 @@ namespace Robust.Client.GameObjects state = GetFallbackState(); } - RSI.State.Direction layerSpecificDir; - if (state.Directions == RSI.State.DirectionType.Dir1) - { - layerSpecificDir = RSI.State.Direction.South; - } - else - { - RSI.State.Direction dir; - if (overrideDirection != null) - { - dir = overrideDirection.Value.Convert(state.Directions); - } - else - { - dir = GetDir(state.Directions, worldRotation); - } - - layerSpecificDir = OffsetRsiDir(dir, layer.DirOffset); - } - + var layerSpecificDir = layer.EffectiveDirection(state, worldRotation, overrideDirection); texture = state.GetFrame(layerSpecificDir, layer.AnimationFrame); } @@ -1111,7 +1004,8 @@ namespace Robust.Client.GameObjects serializer.DataFieldCached(ref scale, "scale", Vector2.One); serializer.DataFieldCached(ref rotation, "rotation", Angle.Zero); serializer.DataFieldCached(ref offset, "offset", Vector2.Zero); - serializer.DataFieldCached(ref drawDepth, "drawdepth", DrawDepthTag.Default, WithFormat.Constants()); + serializer.DataFieldCached(ref drawDepth, "drawdepth", DrawDepthTag.Default, + WithFormat.Constants()); serializer.DataFieldCached(ref color, "color", Color.White); serializer.DataFieldCached(ref _directional, "directional", true); serializer.DataFieldCached(ref _visible, "visible", true); @@ -1185,7 +1079,7 @@ namespace Robust.Client.GameObjects foreach (var layerDatum in layerData) { var anyTextureAttempted = false; - var layer = new Layer(); + var layer = new Layer(this); if (!string.IsNullOrWhiteSpace(layerDatum.RsiPath)) { var path = TextureRoot / layerDatum.RsiPath; @@ -1380,7 +1274,7 @@ namespace Robust.Client.GameObjects for (var i = 0; i < thestate.Layers.Count; i++) { var netlayer = thestate.Layers[i]; - var layer = new Layer + var layer = new Layer(this) { // These are easy so do them here. Scale = netlayer.Scale, @@ -1467,33 +1361,23 @@ namespace Robust.Client.GameObjects case DirectionOffset.None: return dir; case DirectionOffset.Clockwise: - switch (dir) + return dir switch { - case RSI.State.Direction.North: - return RSI.State.Direction.East; - case RSI.State.Direction.East: - return RSI.State.Direction.South; - case RSI.State.Direction.South: - return RSI.State.Direction.West; - case RSI.State.Direction.West: - return RSI.State.Direction.North; - default: - throw new NotImplementedException(); - } + RSI.State.Direction.North => RSI.State.Direction.East, + RSI.State.Direction.East => RSI.State.Direction.South, + RSI.State.Direction.South => RSI.State.Direction.West, + RSI.State.Direction.West => RSI.State.Direction.North, + _ => throw new NotImplementedException() + }; case DirectionOffset.CounterClockwise: - switch (dir) + return dir switch { - case RSI.State.Direction.North: - return RSI.State.Direction.West; - case RSI.State.Direction.East: - return RSI.State.Direction.North; - case RSI.State.Direction.South: - return RSI.State.Direction.East; - case RSI.State.Direction.West: - return RSI.State.Direction.South; - default: - throw new NotImplementedException(); - } + RSI.State.Direction.North => RSI.State.Direction.West, + RSI.State.Direction.East => RSI.State.Direction.North, + RSI.State.Direction.South => RSI.State.Direction.East, + RSI.State.Direction.West => RSI.State.Direction.South, + _ => throw new NotImplementedException() + }; case DirectionOffset.Flip: switch (dir) { @@ -1541,7 +1425,7 @@ namespace Robust.Client.GameObjects /// /// Enum to "offset" a cardinal direction. /// - public enum DirectionOffset + public enum DirectionOffset : byte { /// /// No offset. @@ -1564,8 +1448,10 @@ namespace Robust.Client.GameObjects Flip = 3, } - private class Layer + private class Layer : ISpriteLayer { + private readonly SpriteComponent _parent; + public ShaderInstance? Shader; public Texture? Texture; @@ -1574,23 +1460,22 @@ namespace Robust.Client.GameObjects public float AnimationTimeLeft; public float AnimationTime; public int AnimationFrame; - public Vector2 Scale = Vector2.One; - public Angle Rotation; + public Vector2 Scale { get; set; } = Vector2.One; + public Angle Rotation { get; set; } public bool Visible = true; - public Color Color = Color.White; - public DirectionOffset DirOffset; + public Color Color { get; set; } = Color.White; public bool AutoAnimated = true; + public DirectionOffset DirOffset { get; set; } + public RSI? ActualRsi => RSI ?? _parent.BaseRSI; - public Layer() + public Layer(SpriteComponent parent) { - Visible = true; - Scale = Vector2.One; - Color = Color.White; - AutoAnimated = true; + _parent = parent; } public Layer(Layer toClone) { + _parent = toClone._parent; Shader = toClone.Shader; Texture = toClone.Texture; RSI = toClone.RSI; @@ -1605,6 +1490,195 @@ namespace Robust.Client.GameObjects DirOffset = toClone.DirOffset; AutoAnimated = toClone.AutoAnimated; } + + RSI? ISpriteLayer.Rsi { get => RSI; set => SetRsi(value); } + RSI.StateId ISpriteLayer.RsiState { get => State; set => SetState(value); } + Texture? ISpriteLayer.Texture { get => Texture; set => SetTexture(value); } + + bool ISpriteLayer.Visible + { + get => Visible; + set => SetVisible(value); + } + + float ISpriteLayer.AnimationTime + { + get => AnimationTime; + set => SetAnimationTime(value); + } + + int ISpriteLayer.AnimationFrame => AnimationFrame; + + bool ISpriteLayer.AutoAnimated + { + get => AutoAnimated; + set => SetAutoAnimated(value); + } + + public RSI.State.Direction EffectiveDirection(Angle worldRotation) + { + if (State == default) + { + return default; + } + + var rsi = ActualRsi; + if (rsi == null) + { + return default; + } + + var state = rsi[State]; + + return EffectiveDirection(state, worldRotation, null); + } + + public Vector2 LocalToLayer(Vector2 localPos) + { + // TODO: scale & rotation for layers is currently unimplemented. + return localPos; + } + + public RSI.State.Direction EffectiveDirection(RSI.State state, Angle worldRotation, + Direction? overrideDirection) + { + if (state.Directions == RSI.State.DirectionType.Dir1) + { + return RSI.State.Direction.South; + } + else + { + RSI.State.Direction dir; + if (overrideDirection != null) + { + dir = overrideDirection.Value.Convert(state.Directions); + } + else + { + dir = _parent.GetDir(state.Directions, worldRotation); + } + + return OffsetRsiDir(dir, DirOffset); + } + } + + public void SetAnimationTime(float animationTime) + { + if (!State.IsValid) + { + return; + } + + var theLayerRSI = ActualRsi; + if (theLayerRSI == null) + { + return; + } + + var state = theLayerRSI[State]; + if (animationTime > AnimationTime) + { + // Handle advancing differently from going backwards. + AnimationTimeLeft -= (animationTime - 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. + AnimationTimeLeft = -animationTime + state.GetDelay(0); + AnimationFrame = 0; + } + + AnimationTime = animationTime; + // After setting timing data correctly, run advance to get to the correct frame. + _advanceFrameAnimation(this, state); + } + + public void SetAutoAnimated(bool value) + { + AutoAnimated = value; + + _parent.UpdateIsInert(); + } + + public void SetVisible(bool value) + { + Visible = value; + + _parent.UpdateIsInert(); + } + + public void SetRsi(RSI? rsi) + { + RSI = rsi; + if (!State.IsValid) + { + return; + } + + // Gotta do this because somebody might use null as argument (totally valid). + var actualRsi = ActualRsi; + if (actualRsi == null) + { + Logger.ErrorS(LogCategory, "No RSI to pull new state from! Trace:\n{0}", Environment.StackTrace); + Texture = null; + } + else + { + if (actualRsi.TryGetState(State, out var state)) + { + AnimationTimeLeft = state.GetDelay(0); + } + else + { + Logger.ErrorS(LogCategory, "State '{0}' does not exist in set RSI. Trace:\n{1}", State, + Environment.StackTrace); + Texture = null; + } + } + + _parent.UpdateIsInert(); + } + + public void SetState(RSI.StateId stateId) + { + if (State == stateId) + { + return; + } + + State = stateId; + RSI.State? state; + var rsi = ActualRsi; + if (rsi == null) + { + state = _parent.GetFallbackState(); + Logger.ErrorS(LogCategory, "No RSI to pull new state from! Trace:\n{0}", Environment.StackTrace); + } + else + { + if (!rsi.TryGetState(stateId, out state)) + { + state = _parent.GetFallbackState(); + Logger.ErrorS(LogCategory, "State '{0}' does not exist in RSI. Trace:\n{1}", stateId, + Environment.StackTrace); + } + } + + AnimationFrame = 0; + AnimationTime = 0; + AnimationTimeLeft = state.GetDelay(0); + + _parent.UpdateIsInert(); + } + + public void SetTexture(Texture? texture) + { + State = default; + Texture = texture; + + _parent.UpdateIsInert(); + } } void IAnimationProperties.SetAnimatableProperty(string name, object value) @@ -1618,7 +1692,7 @@ namespace Robust.Client.GameObjects var delimiter = name.IndexOf("/", 6, StringComparison.Ordinal); var indexString = name.Substring(6, delimiter - 6); var index = int.Parse(indexString, CultureInfo.InvariantCulture); - var layerProp = name.Substring(delimiter+1); + var layerProp = name.Substring(delimiter + 1); switch (layerProp) { diff --git a/Robust.Client/Graphics/Clyde/Clyde.Textures.cs b/Robust.Client/Graphics/Clyde/Clyde.Textures.cs index b3c0bca852..cde5318c46 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Textures.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Textures.cs @@ -258,6 +258,15 @@ namespace Robust.Client.Graphics.Clyde TextureId = id; _clyde = clyde; } + + public override string ToString() + { + if (_clyde._loadedTextures.TryGetValue(TextureId, out var loaded) && loaded.Name != null) + { + return $"ClydeTexture: {loaded.Name} ({TextureId})"; + } + return $"ClydeTexture: ({TextureId})"; + } } public Texture GetStockTexture(ClydeStockTexture stockTexture) diff --git a/Robust.Client/Interfaces/GameObjects/Components/IClientClickableComponent.cs b/Robust.Client/Interfaces/GameObjects/Components/IClientClickableComponent.cs deleted file mode 100644 index 9c58ab2988..0000000000 --- a/Robust.Client/Interfaces/GameObjects/Components/IClientClickableComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Robust.Shared.Interfaces.GameObjects.Components; -using Robust.Shared.Maths; - -namespace Robust.Client.Interfaces.GameObjects.Components -{ - public interface IClientClickableComponent : IClickableComponent - { - /// - /// Used to check whether a click worked. - /// - /// The world position that was clicked. - /// - /// The draw depth for the sprite that captured the click. - /// - /// True if the click worked, false otherwise. - bool CheckClick(Vector2 worldPos, out int drawdepth); - } -} diff --git a/Robust.Client/Interfaces/GameObjects/Components/ISpriteComponent.cs b/Robust.Client/Interfaces/GameObjects/Components/ISpriteComponent.cs index 0b96d5f995..6b170a5cac 100644 --- a/Robust.Client/Interfaces/GameObjects/Components/ISpriteComponent.cs +++ b/Robust.Client/Interfaces/GameObjects/Components/ISpriteComponent.cs @@ -1,9 +1,9 @@ using System; +using System.Collections.Generic; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Graphics.Shaders; using Robust.Shared.Animations; -using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Maths; using Robust.Shared.Utility; @@ -174,7 +174,9 @@ namespace Robust.Client.Interfaces.GameObjects.Components void LayerSetVisible(int layer, bool visible); void LayerSetVisible(object layerKey, bool visible); void LayerSetColor(int layer, Color color); + void LayerSetColor(object layerKey, Color color); + // Yes, I realize how silly it is to reference an enum in the concrete implementation. // I don't care. void LayerSetDirOffset(int layer, SpriteComponent.DirectionOffset offset); @@ -196,5 +198,36 @@ namespace Robust.Client.Interfaces.GameObjects.Components /// Get the RSI used by a layer. /// RSI? LayerGetActualRSI(object layerKey); + + ISpriteLayer this[int layer] { get; } + ISpriteLayer this[Index layer] { get; } + ISpriteLayer this[object layerKey] { get; } + + IEnumerable AllLayers { get; } + } + + public interface ISpriteLayer + { + SpriteComponent.DirectionOffset DirOffset { get; set; } + + RSI? Rsi { get; set; } + RSI.StateId RsiState { get; set; } + RSI? ActualRsi { get; } + + Texture? Texture { get; set; } + + Angle Rotation { get; set; } + Vector2 Scale { get; set; } + + bool Visible { get; set; } + Color Color { get; set; } + + float AnimationTime { get; set; } + int AnimationFrame { get; } + bool AutoAnimated { get; set; } + + RSI.State.Direction EffectiveDirection(Angle worldRotation); + + Vector2 LocalToLayer(Vector2 localPos); } } diff --git a/Robust.Client/Interfaces/GameObjects/IClickTargetComponent.cs b/Robust.Client/Interfaces/GameObjects/IClickTargetComponent.cs deleted file mode 100644 index d74a18931f..0000000000 --- a/Robust.Client/Interfaces/GameObjects/IClickTargetComponent.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Robust.Shared.GameObjects; -using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.Maths; -using Robust.Shared.Map; - -namespace Robust.Client.Interfaces.GameObjects -{ - /// - /// A component that can be clicked on. Handles whether a coordinate is a valid place to click us on. - /// - public interface IClickTargetComponent : IComponent - { - int DrawDepth { get; } - } -} diff --git a/Robust.Client/Interfaces/ResourceManagement/IResourceCache.cs b/Robust.Client/Interfaces/ResourceManagement/IResourceCache.cs index e2b1fe9f65..811c36d1b1 100644 --- a/Robust.Client/Interfaces/ResourceManagement/IResourceCache.cs +++ b/Robust.Client/Interfaces/ResourceManagement/IResourceCache.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Robust.Client.ResourceManagement; using Robust.Shared.Interfaces.Resources; @@ -36,9 +37,15 @@ namespace Robust.Client.Interfaces.ResourceManagement where T : BaseResource, new(); IEnumerable> GetAllResources() where T : BaseResource, new(); + + // Resource load callbacks so content can hook stuff like click maps. + event Action OnRawTextureLoaded; + event Action OnRsiLoaded; } internal interface IResourceCacheInternal : IResourceCache, IResourceManagerInternal { + void TextureLoaded(TextureLoadedEventArgs eventArgs); + void RsiLoaded(RsiLoadedEventArgs eventArgs); } } diff --git a/Robust.Client/ResourceManagement/ResourceCache.cs b/Robust.Client/ResourceManagement/ResourceCache.cs index 86140a14a3..09df7e4d6f 100644 --- a/Robust.Client/ResourceManagement/ResourceCache.cs +++ b/Robust.Client/ResourceManagement/ResourceCache.cs @@ -151,6 +151,9 @@ namespace Robust.Client.ResourceManagement return GetTypeDict().Select(p => new KeyValuePair(p.Key, (T) p.Value)); } + public event Action? OnRawTextureLoaded; + public event Action? OnRsiLoaded; + #region IDisposable Members private bool disposed = false; @@ -197,5 +200,15 @@ namespace Robust.Client.ResourceManagement return ret; } + + public void TextureLoaded(TextureLoadedEventArgs eventArgs) + { + OnRawTextureLoaded?.Invoke(eventArgs); + } + + public void RsiLoaded(RsiLoadedEventArgs eventArgs) + { + OnRsiLoaded?.Invoke(eventArgs); + } } } diff --git a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs index 33717c61cb..7ad759fd3c 100644 --- a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs +++ b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs @@ -70,12 +70,14 @@ namespace Robust.Client.ResourceManagement // Ok schema validated just fine. var manifestJson = JObject.Parse(manifestContents); - var toAtlas = new List<(Image src, Texture[][] output, int[][] indices, int totalFrameCount)>(); + var toAtlas = new List<(Image src, Texture[][] output, int[][] indices, Vector2i[][] offsets, int totalFrameCount)>(); var metaData = ParseMetaData(manifestJson); var frameSize = metaData.Size; var rsi = new RSI(frameSize); + var callbackOffsets = new Dictionary(); + // Do every state. foreach (var stateObject in metaData.States) { @@ -95,84 +97,92 @@ namespace Robust.Client.ResourceManagement var (foldedDelays, foldedIndices) = FoldDelays(stateObject.Delays); var textures = new Texture[foldedIndices.Length][]; + var callbackOffset = new Vector2i[foldedIndices.Length][]; for (var i = 0; i < textures.Length; i++) { textures[i] = new Texture[foldedIndices[0].Length]; + callbackOffset[i] = new Vector2i[foldedIndices[0].Length]; } var state = new RSI.State(frameSize, stateObject.StateId, stateObject.DirType, foldedDelays, textures); rsi.AddState(state); - toAtlas.Add((image, textures, foldedIndices, frameCount)); + toAtlas.Add((image, textures, foldedIndices, callbackOffset, frameCount)); + callbackOffsets[stateObject.StateId] = callbackOffset; } // Poorly hacked in texture atlas support here. + var totalFrameCount = toAtlas.Sum(p => p.totalFrameCount); + + // Generate atlas. + var dimensionX = (int) MathF.Ceiling(MathF.Sqrt(totalFrameCount)); + var dimensionY = (int) MathF.Ceiling((float) totalFrameCount / dimensionX); + + using var sheet = new Image(dimensionX * frameSize.X, dimensionY * frameSize.Y); + + var sheetIndex = 0; + foreach (var (src, _, _, _, frameCount) in toAtlas) { - var totalFrameCount = toAtlas.Sum(p => p.totalFrameCount); - - // Generate atlas. - var dimensionX = (int) MathF.Ceiling(MathF.Sqrt(totalFrameCount)); - var dimensionY = (int) MathF.Ceiling((float) totalFrameCount / dimensionX); - - using var sheet = new Image(dimensionX * frameSize.X, dimensionY * frameSize.Y); - - var sheetIndex = 0; - foreach (var (src, _, _, frameCount) in toAtlas) + // Blit all the frames over. + for (var i = 0; i < frameCount; i++) { - // Blit all the frames over. - for (var i = 0; i < frameCount; i++) - { - var srcWidth = (src.Width / frameSize.X); - var srcColumn = i % srcWidth; - var srcRow = i / srcWidth; - var srcPos = (srcColumn * frameSize.X, srcRow * frameSize.Y); + var srcWidth = (src.Width / frameSize.X); + var srcColumn = i % srcWidth; + var srcRow = i / srcWidth; + var srcPos = (srcColumn * frameSize.X, srcRow * frameSize.Y); - var sheetColumn = (sheetIndex + i) % dimensionX; - var sheetRow = (sheetIndex + i) / dimensionX; - var sheetPos = (sheetColumn * frameSize.X, sheetRow * frameSize.Y); + var sheetColumn = (sheetIndex + i) % dimensionX; + var sheetRow = (sheetIndex + i) / dimensionX; + var sheetPos = (sheetColumn * frameSize.X, sheetRow * frameSize.Y); - var srcBox = UIBox2i.FromDimensions(srcPos, frameSize); + var srcBox = UIBox2i.FromDimensions(srcPos, frameSize); - src.Blit(srcBox, sheet, sheetPos); - } - - sheetIndex += frameCount; + src.Blit(srcBox, sheet, sheetPos); } - // Load atlas. - var texture = Texture.LoadFromImage(sheet, path.ToString()); - - var sheetOffset = 0; - foreach (var (src, output, indices, frameCount) in toAtlas) - { - for (var i = 0; i < indices.Length; i++) - { - var dirIndices = indices[i]; - var dirOutput = output[i]; - - for (var j = 0; j < dirIndices.Length; j++) - { - var index = sheetOffset + dirIndices[j]; - - var sheetColumn = index % dimensionX; - var sheetRow = index / dimensionX; - var sheetPos = (sheetColumn * frameSize.X, sheetRow * frameSize.Y); - - dirOutput[j] = new AtlasTexture(texture, UIBox2.FromDimensions(sheetPos, frameSize)); - } - } - - sheetOffset += frameCount; - } + sheetIndex += frameCount; } - foreach (var (image, _, _, _) in toAtlas) + // Load atlas. + var texture = Texture.LoadFromImage(sheet, path.ToString()); + + var sheetOffset = 0; + foreach (var (_, output, indices, offsets, frameCount) in toAtlas) + { + for (var i = 0; i < indices.Length; i++) + { + var dirIndices = indices[i]; + var dirOutput = output[i]; + var dirOffsets = offsets[i]; + + for (var j = 0; j < dirIndices.Length; j++) + { + var index = sheetOffset + dirIndices[j]; + + var sheetColumn = index % dimensionX; + var sheetRow = index / dimensionX; + var sheetPos = (sheetColumn * frameSize.X, sheetRow * frameSize.Y); + + dirOffsets[j] = sheetPos; + dirOutput[j] = new AtlasTexture(texture, UIBox2.FromDimensions(sheetPos, frameSize)); + } + } + + sheetOffset += frameCount; + } + + foreach (var (image, _, _, _, _) in toAtlas) { image.Dispose(); } RSI = rsi; + + if (cache is IResourceCacheInternal cacheInternal) + { + cacheInternal.RsiLoaded(new RsiLoadedEventArgs(path, this, sheet, callbackOffsets)); + } } /// diff --git a/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs index e9bfb191b0..345d23d7be 100644 --- a/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs +++ b/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs @@ -6,12 +6,16 @@ using Robust.Client.Interfaces.ResourceManagement; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Utility; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; using YamlDotNet.RepresentationModel; namespace Robust.Client.ResourceManagement { public class TextureResource : BaseResource { + public const float ClickThreshold = 0.25f; + public override ResourcePath? Fallback => new ResourcePath("/Textures/noSprite.png"); public Texture Texture { get; private set; } = default!; @@ -29,7 +33,14 @@ namespace Robust.Client.ResourceManagement var manager = IoCManager.Resolve(); - Texture = manager.LoadTextureFromPNGStream(stream, path.ToString(), loadParameters); + using var image = Image.Load(stream); + + Texture = manager.LoadTextureFromImage(image, path.ToString(), loadParameters); + + if (cache is IResourceCacheInternal cacheInternal) + { + cacheInternal.TextureLoaded(new TextureLoadedEventArgs(path, image, this)); + } } private static TextureLoadParameters? _tryLoadTextureParameters(IResourceCache cache, ResourcePath path) diff --git a/Robust.Client/ResourceManagement/RsiLoadedEventArgs.cs b/Robust.Client/ResourceManagement/RsiLoadedEventArgs.cs new file mode 100644 index 0000000000..a22fe934cd --- /dev/null +++ b/Robust.Client/ResourceManagement/RsiLoadedEventArgs.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Robust.Client.Graphics; +using Robust.Shared.Maths; +using Robust.Shared.Utility; +using SixLabors.ImageSharp; + +namespace Robust.Client.ResourceManagement +{ + public readonly struct RsiLoadedEventArgs + { + internal RsiLoadedEventArgs(ResourcePath path, RSIResource resource, Image atlas, Dictionary atlasOffsets) + { + Path = path; + Resource = resource; + Atlas = atlas; + AtlasOffsets = atlasOffsets; + } + + public ResourcePath Path { get; } + public RSIResource Resource { get; } + public Image Atlas { get; } + public Dictionary AtlasOffsets { get; } + } +} diff --git a/Robust.Client/ResourceManagement/TextureLoadedEventArgs.cs b/Robust.Client/ResourceManagement/TextureLoadedEventArgs.cs new file mode 100644 index 0000000000..cb8a8cdaff --- /dev/null +++ b/Robust.Client/ResourceManagement/TextureLoadedEventArgs.cs @@ -0,0 +1,19 @@ +using Robust.Shared.Utility; +using SixLabors.ImageSharp; + +namespace Robust.Client.ResourceManagement +{ + public readonly struct TextureLoadedEventArgs + { + internal TextureLoadedEventArgs(ResourcePath path, Image image, TextureResource resource) + { + Path = path; + Image = image; + Resource = resource; + } + + public ResourcePath Path { get; } + public Image Image { get; } + public TextureResource Resource { get; } + } +} diff --git a/Robust.Server/GameObjects/Components/ClickableComponent.cs b/Robust.Server/GameObjects/Components/ClickableComponent.cs deleted file mode 100644 index a4f6d6657f..0000000000 --- a/Robust.Server/GameObjects/Components/ClickableComponent.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Robust.Shared.GameObjects; -using Robust.Shared.GameObjects.Components; -using Robust.Shared.Interfaces.GameObjects.Components; -using Robust.Shared.Maths; -using Robust.Shared.Serialization; - -namespace Robust.Server.GameObjects -{ - public class ClickableComponent : Component, IClickableComponent - { - private Box2? _localBounds; - - public Box2? LocalBounds - { - get => _localBounds; - set => _localBounds = value; - } - - public override string Name => "Clickable"; - public override uint? NetID => NetIDs.CLICKABLE; - - /// - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(ref _localBounds, "bounds", null); - } - - /// - public override ComponentState GetComponentState() - { - return new ClickableComponentState(_localBounds); - } - } -} diff --git a/Robust.Server/GameObjects/ServerComponentFactory.cs b/Robust.Server/GameObjects/ServerComponentFactory.cs index 87c115a91b..c8d4567066 100644 --- a/Robust.Server/GameObjects/ServerComponentFactory.cs +++ b/Robust.Server/GameObjects/ServerComponentFactory.cs @@ -45,9 +45,6 @@ namespace Robust.Server.GameObjects Register(); Register(); - Register(); - RegisterReference(); - Register(); RegisterReference(); diff --git a/Robust.Shared/Interfaces/GameObjects/Components/IClickableComponent.cs b/Robust.Shared/Interfaces/GameObjects/Components/IClickableComponent.cs deleted file mode 100644 index 82f6408f0f..0000000000 --- a/Robust.Shared/Interfaces/GameObjects/Components/IClickableComponent.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Robust.Shared.Interfaces.GameObjects.Components -{ - public interface IClickableComponent : IComponent { } -} diff --git a/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs b/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs index eac0581b00..f05769147f 100644 --- a/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs +++ b/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs @@ -45,7 +45,6 @@ namespace Robust.UnitTesting.Shared.Prototypes Assert.That(prototype.Name, Is.EqualTo("Wall Light")); Assert.That(prototype.ID, Is.EqualTo(id)); Assert.That(prototype.Components, Contains.Key("Transform")); - Assert.That(prototype.Components, Contains.Key("Clickable")); Assert.That(prototype.Components, Contains.Key("Sprite")); Assert.That(prototype.Components, Contains.Key("PointLight")); }); @@ -118,7 +117,6 @@ namespace Robust.UnitTesting.Shared.Prototypes name: Wall Light components: - type: Transform - - type: Clickable - type: Sprite - type: PointLight startState: Off