Improved click detection supporting features: (#1133)

* Improved click detection supporting features:

Clickable removed from engine
Texture/RSI load hooks for content
New sprite layer API

* Fix tests.
This commit is contained in:
Pieter-Jan Briers
2020-06-18 02:25:02 +02:00
committed by GitHub
parent 6dba85e6dd
commit af249f80c8
17 changed files with 440 additions and 395 deletions
@@ -44,11 +44,6 @@ namespace Robust.Client.GameObjects
Register<SpriteComponent>();
RegisterReference<SpriteComponent, ISpriteComponent>();
RegisterReference<SpriteComponent, IClickTargetComponent>();
Register<ClickableComponent>();
RegisterReference<ClickableComponent, IClientClickableComponent>();
RegisterReference<ClickableComponent, IClickableComponent>();
Register<ClientOccluderComponent>();
RegisterReference<ClientOccluderComponent, OccluderComponent>();
@@ -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;
}
/// <inheritdoc />
public override void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(ref _localBounds, "bounds", null);
}
/// <inheritdoc />
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
if (curState is ClickableComponentState state)
{
_localBounds = state.LocalBounds;
}
}
/// <inheritdoc />
//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<IClickTargetComponent>();
drawdepth = component.DrawDepth;
return true;
}
}
}
@@ -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; }
/// <inheritdoc />
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<ISpriteLayer> 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<DrawDepthTag>());
serializer.DataFieldCached(ref drawDepth, "drawdepth", DrawDepthTag.Default,
WithFormat.Constants<DrawDepthTag>());
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
/// <summary>
/// Enum to "offset" a cardinal direction.
/// </summary>
public enum DirectionOffset
public enum DirectionOffset : byte
{
/// <summary>
/// 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)
{
@@ -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)
@@ -1,18 +0,0 @@
using Robust.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.Maths;
namespace Robust.Client.Interfaces.GameObjects.Components
{
public interface IClientClickableComponent : IClickableComponent
{
/// <summary>
/// Used to check whether a click worked.
/// </summary>
/// <param name="worldPos">The world position that was clicked.</param>
/// <param name="drawdepth">
/// The draw depth for the sprite that captured the click.
/// </param>
/// <returns>True if the click worked, false otherwise.</returns>
bool CheckClick(Vector2 worldPos, out int drawdepth);
}
}
@@ -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.
/// </summary>
RSI? LayerGetActualRSI(object layerKey);
ISpriteLayer this[int layer] { get; }
ISpriteLayer this[Index layer] { get; }
ISpriteLayer this[object layerKey] { get; }
IEnumerable<ISpriteLayer> 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);
}
}
@@ -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
{
/// <summary>
/// A component that can be clicked on. Handles whether a coordinate is a valid place to click us on.
/// </summary>
public interface IClickTargetComponent : IComponent
{
int DrawDepth { get; }
}
}
@@ -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<KeyValuePair<ResourcePath, T>> GetAllResources<T>() where T : BaseResource, new();
// Resource load callbacks so content can hook stuff like click maps.
event Action<TextureLoadedEventArgs> OnRawTextureLoaded;
event Action<RsiLoadedEventArgs> OnRsiLoaded;
}
internal interface IResourceCacheInternal : IResourceCache, IResourceManagerInternal
{
void TextureLoaded(TextureLoadedEventArgs eventArgs);
void RsiLoaded(RsiLoadedEventArgs eventArgs);
}
}
@@ -151,6 +151,9 @@ namespace Robust.Client.ResourceManagement
return GetTypeDict<T>().Select(p => new KeyValuePair<ResourcePath, T>(p.Key, (T) p.Value));
}
public event Action<TextureLoadedEventArgs>? OnRawTextureLoaded;
public event Action<RsiLoadedEventArgs>? 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);
}
}
}
@@ -70,12 +70,14 @@ namespace Robust.Client.ResourceManagement
// Ok schema validated just fine.
var manifestJson = JObject.Parse(manifestContents);
var toAtlas = new List<(Image<Rgba32> src, Texture[][] output, int[][] indices, int totalFrameCount)>();
var toAtlas = new List<(Image<Rgba32> 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<RSI.StateId, Vector2i[][]>();
// 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<Rgba32>(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<Rgba32>(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));
}
}
/// <summary>
@@ -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<IClyde>();
Texture = manager.LoadTextureFromPNGStream(stream, path.ToString(), loadParameters);
using var image = Image.Load<Rgba32>(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)
@@ -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<RSI.StateId, Vector2i[][]> atlasOffsets)
{
Path = path;
Resource = resource;
Atlas = atlas;
AtlasOffsets = atlasOffsets;
}
public ResourcePath Path { get; }
public RSIResource Resource { get; }
public Image Atlas { get; }
public Dictionary<RSI.StateId, Vector2i[][]> AtlasOffsets { get; }
}
}
@@ -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; }
}
}
@@ -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;
/// <inheritdoc />
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _localBounds, "bounds", null);
}
/// <inheritdoc />
public override ComponentState GetComponentState()
{
return new ClickableComponentState(_localBounds);
}
}
}
@@ -45,9 +45,6 @@ namespace Robust.Server.GameObjects
Register<PhysicsComponent>();
Register<SpriteComponent>();
Register<ClickableComponent>();
RegisterReference<ClickableComponent, IClickableComponent>();
Register<ContainerManagerComponent>();
RegisterReference<ContainerManagerComponent, IContainerManager>();
@@ -1,4 +0,0 @@
namespace Robust.Shared.Interfaces.GameObjects.Components
{
public interface IClickableComponent : IComponent { }
}
@@ -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