using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Content.Client.IconSmoothing
{
// TODO: Potential improvements:
// Defer updating of these.
// Get told by somebody to use a loop.
///
/// Makes sprites of other grid-aligned entities like us connect.
///
///
/// The system is based on Baystation12's smoothwalling, and thus will work with those.
/// To use, set base equal to the prefix of the corner states in the sprite base RSI.
/// Any objects with the same key will connect.
///
[RegisterComponent]
public class IconSmoothComponent : Component
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[DataField("mode")]
private IconSmoothingMode _mode = IconSmoothingMode.Corners;
public override string Name => "IconSmooth";
internal ISpriteComponent? Sprite { get; private set; }
private (GridId, Vector2i) _lastPosition;
///
/// We will smooth with other objects with the same key.
///
[DataField("key")]
public string? SmoothKey { get; }
///
/// Prepended to the RSI state.
///
[DataField("base")]
public string StateBase { get; } = string.Empty;
///
/// Mode that controls how the icon should be selected.
///
public IconSmoothingMode Mode => _mode;
///
/// Used by to reduce redundant updates.
///
internal int UpdateGeneration { get; set; }
protected override void Initialize()
{
base.Initialize();
Sprite = _entMan.GetComponent(Owner);
}
///
protected override void Startup()
{
base.Startup();
if (_entMan.GetComponent(Owner).Anchored)
{
// ensures lastposition initial value is populated on spawn. Just calling
// the hook here would cause a dirty event to fire needlessly
UpdateLastPosition();
_entMan.EventBus.RaiseEvent(EventSource.Local, new IconSmoothDirtyEvent(Owner, null, Mode));
}
if (Sprite != null && Mode == IconSmoothingMode.Corners)
{
var state0 = $"{StateBase}0";
Sprite.LayerMapSet(CornerLayers.SE, Sprite.AddLayerState(state0));
Sprite.LayerSetDirOffset(CornerLayers.SE, DirectionOffset.None);
Sprite.LayerMapSet(CornerLayers.NE, Sprite.AddLayerState(state0));
Sprite.LayerSetDirOffset(CornerLayers.NE, DirectionOffset.CounterClockwise);
Sprite.LayerMapSet(CornerLayers.NW, Sprite.AddLayerState(state0));
Sprite.LayerSetDirOffset(CornerLayers.NW, DirectionOffset.Flip);
Sprite.LayerMapSet(CornerLayers.SW, Sprite.AddLayerState(state0));
Sprite.LayerSetDirOffset(CornerLayers.SW, DirectionOffset.Clockwise);
}
}
private void UpdateLastPosition()
{
if (_mapManager.TryGetGrid(_entMan.GetComponent(Owner).GridID, out var grid))
{
_lastPosition = (_entMan.GetComponent(Owner).GridID, grid.TileIndicesFor(_entMan.GetComponent(Owner).Coordinates));
}
else
{
// When this is called during component startup, the transform can end up being with an invalid grid ID.
// In that case, use this.
_lastPosition = (GridId.Invalid, new Vector2i(0, 0));
}
}
internal virtual void CalculateNewSprite()
{
if (!_mapManager.TryGetGrid(_entMan.GetComponent(Owner).GridID, out var grid))
{
Logger.Error($"Failed to calculate IconSmoothComponent sprite in {Owner} because grid {_entMan.GetComponent(Owner).GridID} was missing.");
return;
}
CalculateNewSprite(grid);
}
internal virtual void CalculateNewSprite(IMapGrid grid)
{
switch (Mode)
{
case IconSmoothingMode.Corners:
CalculateNewSpriteCorners(grid);
break;
case IconSmoothingMode.CardinalFlags:
CalculateNewSpriteCardinal(grid);
break;
case IconSmoothingMode.NoSprite:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private void CalculateNewSpriteCardinal(IMapGrid grid)
{
if (!_entMan.GetComponent(Owner).Anchored || Sprite == null)
{
return;
}
var dirs = CardinalConnectDirs.None;
var position = _entMan.GetComponent(Owner).Coordinates;
if (MatchingEntity(grid.GetInDir(position, Direction.North)))
dirs |= CardinalConnectDirs.North;
if (MatchingEntity(grid.GetInDir(position, Direction.South)))
dirs |= CardinalConnectDirs.South;
if (MatchingEntity(grid.GetInDir(position, Direction.East)))
dirs |= CardinalConnectDirs.East;
if (MatchingEntity(grid.GetInDir(position, Direction.West)))
dirs |= CardinalConnectDirs.West;
Sprite.LayerSetState(0, $"{StateBase}{(int) dirs}");
}
private void CalculateNewSpriteCorners(IMapGrid grid)
{
if (Sprite == null)
{
return;
}
var (cornerNE, cornerNW, cornerSW, cornerSE) = CalculateCornerFill(grid);
Sprite.LayerSetState(CornerLayers.NE, $"{StateBase}{(int) cornerNE}");
Sprite.LayerSetState(CornerLayers.SE, $"{StateBase}{(int) cornerSE}");
Sprite.LayerSetState(CornerLayers.SW, $"{StateBase}{(int) cornerSW}");
Sprite.LayerSetState(CornerLayers.NW, $"{StateBase}{(int) cornerNW}");
}
protected (CornerFill ne, CornerFill nw, CornerFill sw, CornerFill se) CalculateCornerFill(IMapGrid grid)
{
if (!_entMan.GetComponent(Owner).Anchored)
{
return (CornerFill.None, CornerFill.None, CornerFill.None, CornerFill.None);
}
var position = _entMan.GetComponent(Owner).Coordinates;
var n = MatchingEntity(grid.GetInDir(position, Direction.North));
var ne = MatchingEntity(grid.GetInDir(position, Direction.NorthEast));
var e = MatchingEntity(grid.GetInDir(position, Direction.East));
var se = MatchingEntity(grid.GetInDir(position, Direction.SouthEast));
var s = MatchingEntity(grid.GetInDir(position, Direction.South));
var sw = MatchingEntity(grid.GetInDir(position, Direction.SouthWest));
var w = MatchingEntity(grid.GetInDir(position, Direction.West));
var nw = MatchingEntity(grid.GetInDir(position, Direction.NorthWest));
// ReSharper disable InconsistentNaming
var cornerNE = CornerFill.None;
var cornerSE = CornerFill.None;
var cornerSW = CornerFill.None;
var cornerNW = CornerFill.None;
// ReSharper restore InconsistentNaming
if (n)
{
cornerNE |= CornerFill.CounterClockwise;
cornerNW |= CornerFill.Clockwise;
}
if (ne)
{
cornerNE |= CornerFill.Diagonal;
}
if (e)
{
cornerNE |= CornerFill.Clockwise;
cornerSE |= CornerFill.CounterClockwise;
}
if (se)
{
cornerSE |= CornerFill.Diagonal;
}
if (s)
{
cornerSE |= CornerFill.Clockwise;
cornerSW |= CornerFill.CounterClockwise;
}
if (sw)
{
cornerSW |= CornerFill.Diagonal;
}
if (w)
{
cornerSW |= CornerFill.Clockwise;
cornerNW |= CornerFill.CounterClockwise;
}
if (nw)
{
cornerNW |= CornerFill.Diagonal;
}
// Local is fine as we already know it's parented to the grid (due to the way anchoring works).
switch (_entMan.GetComponent(Owner).LocalRotation.GetCardinalDir())
{
case Direction.North:
return (cornerSW, cornerSE, cornerNE, cornerNW);
case Direction.West:
return (cornerSE, cornerNE, cornerNW, cornerSW);
case Direction.South:
return (cornerNE, cornerNW, cornerSW, cornerSE);
default:
return (cornerNW, cornerSW, cornerSE, cornerNE);
}
}
///
protected override void Shutdown()
{
base.Shutdown();
if (_entMan.GetComponent(Owner).Anchored)
{
_entMan.EventBus.RaiseEvent(EventSource.Local, new IconSmoothDirtyEvent(Owner, _lastPosition, Mode));
}
}
public void AnchorStateChanged()
{
if (_entMan.GetComponent(Owner).Anchored)
{
_entMan.EventBus.RaiseEvent(EventSource.Local, new IconSmoothDirtyEvent(Owner, _lastPosition, Mode));
UpdateLastPosition();
}
}
[System.Diagnostics.Contracts.Pure]
protected bool MatchingEntity(IEnumerable candidates)
{
foreach (var entity in candidates)
{
if (!_entMan.TryGetComponent(entity, out IconSmoothComponent? other))
{
continue;
}
if (other.SmoothKey == SmoothKey)
{
return true;
}
}
return false;
}
[Flags]
private enum CardinalConnectDirs : byte
{
None = 0,
North = 1,
South = 2,
East = 4,
West = 8
}
[Flags]
public enum CornerFill : byte
{
// These values are pulled from Baystation12.
// I'm too lazy to convert the state names.
None = 0,
// The cardinal tile counter-clockwise of this corner is filled.
CounterClockwise = 1,
// The diagonal tile in the direction of this corner.
Diagonal = 2,
// The cardinal tile clockwise of this corner is filled.
Clockwise = 4,
}
public enum CornerLayers : byte
{
SE,
NE,
NW,
SW,
}
}
///
/// Controls the mode with which icon smoothing is calculated.
///
[PublicAPI]
public enum IconSmoothingMode : byte
{
///
/// Each icon is made up of 4 corners, each of which can get a different state depending on
/// adjacent entities clockwise, counter-clockwise and diagonal with the corner.
///
Corners,
///
/// There are 16 icons, only one of which is used at once.
/// The icon selected is a bit field made up of the cardinal direction flags that have adjacent entities.
///
CardinalFlags,
///
/// Where this component contributes to our neighbors being calculated but we do not update our own sprite.
///
NoSprite,
}
}