| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542 |
- using System.Numerics;
- using Content.Shared.IconSmoothing;
- using JetBrains.Annotations;
- using Robust.Client.GameObjects;
- using Robust.Shared.Map;
- using Robust.Shared.Map.Components;
- using Robust.Shared.Map.Enumerators;
- using static Robust.Client.GameObjects.SpriteComponent;
- namespace Content.Client.IconSmoothing
- {
- // TODO: just make this set appearance data?
- /// <summary>
- /// Entity system implementing the logic for <see cref="IconSmoothComponent"/>
- /// </summary>
- [UsedImplicitly]
- public sealed partial class IconSmoothSystem : EntitySystem
- {
- [Dependency] private readonly SharedMapSystem _mapSystem = default!;
- private readonly Queue<EntityUid> _dirtyEntities = new();
- private readonly Queue<EntityUid> _anchorChangedEntities = new();
- private int _generation;
- public void SetEnabled(EntityUid uid, bool value, IconSmoothComponent? component = null)
- {
- if (!Resolve(uid, ref component, false) || value == component.Enabled)
- return;
- component.Enabled = value;
- DirtyNeighbours(uid, component);
- }
- public override void Initialize()
- {
- base.Initialize();
- InitializeEdge();
- SubscribeLocalEvent<IconSmoothComponent, AnchorStateChangedEvent>(OnAnchorChanged);
- SubscribeLocalEvent<IconSmoothComponent, ComponentShutdown>(OnShutdown);
- SubscribeLocalEvent<IconSmoothComponent, ComponentStartup>(OnStartup);
- }
- private void OnStartup(EntityUid uid, IconSmoothComponent component, ComponentStartup args)
- {
- var xform = Transform(uid);
- if (xform.Anchored)
- {
- component.LastPosition = TryComp<MapGridComponent>(xform.GridUid, out var grid)
- ? (xform.GridUid.Value, _mapSystem.TileIndicesFor(xform.GridUid.Value, grid, xform.Coordinates))
- : (null, new Vector2i(0, 0));
- DirtyNeighbours(uid, component);
- }
- if (component.Mode != IconSmoothingMode.Corners || !TryComp(uid, out SpriteComponent? sprite))
- return;
- SetCornerLayers(sprite, component);
- if (component.Shader != null)
- {
- sprite.LayerSetShader(CornerLayers.SE, component.Shader);
- sprite.LayerSetShader(CornerLayers.NE, component.Shader);
- sprite.LayerSetShader(CornerLayers.NW, component.Shader);
- sprite.LayerSetShader(CornerLayers.SW, component.Shader);
- }
- }
- public void SetStateBase(EntityUid uid, IconSmoothComponent component, string newState)
- {
- if (!TryComp<SpriteComponent>(uid, out var sprite))
- return;
- component.StateBase = newState;
- SetCornerLayers(sprite, component);
- }
- private void SetCornerLayers(SpriteComponent sprite, IconSmoothComponent component)
- {
- sprite.LayerMapRemove(CornerLayers.SE);
- sprite.LayerMapRemove(CornerLayers.NE);
- sprite.LayerMapRemove(CornerLayers.NW);
- sprite.LayerMapRemove(CornerLayers.SW);
- var state0 = $"{component.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 OnShutdown(EntityUid uid, IconSmoothComponent component, ComponentShutdown args)
- {
- _dirtyEntities.Enqueue(uid);
- DirtyNeighbours(uid, component);
- }
- public override void FrameUpdate(float frameTime)
- {
- base.FrameUpdate(frameTime);
- var xformQuery = GetEntityQuery<TransformComponent>();
- var smoothQuery = GetEntityQuery<IconSmoothComponent>();
- // first process anchor state changes.
- while (_anchorChangedEntities.TryDequeue(out var uid))
- {
- if (!xformQuery.TryGetComponent(uid, out var xform))
- continue;
- if (xform.MapID == MapId.Nullspace)
- {
- // in null-space. Almost certainly because it left PVS. If something ever gets sent to null-space
- // for reasons other than this (or entity deletion), then maybe we still need to update ex-neighbor
- // smoothing here.
- continue;
- }
- DirtyNeighbours(uid, comp: null, xform, smoothQuery);
- }
- // Next, update actual sprites.
- if (_dirtyEntities.Count == 0)
- return;
- _generation += 1;
- var spriteQuery = GetEntityQuery<SpriteComponent>();
- // Performance: This could be spread over multiple updates, or made parallel.
- while (_dirtyEntities.TryDequeue(out var uid))
- {
- CalculateNewSprite(uid, spriteQuery, smoothQuery, xformQuery);
- }
- }
- public void DirtyNeighbours(EntityUid uid, IconSmoothComponent? comp = null, TransformComponent? transform = null, EntityQuery<IconSmoothComponent>? smoothQuery = null)
- {
- smoothQuery ??= GetEntityQuery<IconSmoothComponent>();
- if (!smoothQuery.Value.Resolve(uid, ref comp) || !comp.Running)
- return;
- _dirtyEntities.Enqueue(uid);
- if (!Resolve(uid, ref transform))
- return;
- Vector2i pos;
- EntityUid entityUid;
- if (transform.Anchored && TryComp<MapGridComponent>(transform.GridUid, out var grid))
- {
- entityUid = transform.GridUid.Value;
- pos = _mapSystem.CoordinatesToTile(transform.GridUid.Value, grid, transform.Coordinates);
- }
- else
- {
- // Entity is no longer valid, update around the last position it was at.
- if (comp.LastPosition is not (EntityUid gridId, Vector2i oldPos))
- return;
- if (!TryComp(gridId, out grid))
- return;
- entityUid = gridId;
- pos = oldPos;
- }
- // Yes, we updates ALL smoothing entities surrounding us even if they would never smooth with us.
- DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(1, 0)));
- DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(-1, 0)));
- DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(0, 1)));
- DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(0, -1)));
- if (comp.Mode is IconSmoothingMode.Corners or IconSmoothingMode.NoSprite or IconSmoothingMode.Diagonal)
- {
- DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(1, 1)));
- DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(-1, -1)));
- DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(-1, 1)));
- DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(1, -1)));
- }
- }
- private void DirtyEntities(AnchoredEntitiesEnumerator entities)
- {
- // Instead of doing HasComp -> Enqueue -> TryGetComp, we will just enqueue all entities. Generally when
- // dealing with walls neighboring anchored entities will also be walls, and in those instances that will
- // require one less component fetch/check.
- while (entities.MoveNext(out var entity))
- {
- _dirtyEntities.Enqueue(entity.Value);
- }
- }
- private void OnAnchorChanged(EntityUid uid, IconSmoothComponent component, ref AnchorStateChangedEvent args)
- {
- if (!args.Detaching)
- _anchorChangedEntities.Enqueue(uid);
- }
- private void CalculateNewSprite(EntityUid uid,
- EntityQuery<SpriteComponent> spriteQuery,
- EntityQuery<IconSmoothComponent> smoothQuery,
- EntityQuery<TransformComponent> xformQuery,
- IconSmoothComponent? smooth = null)
- {
- TransformComponent? xform;
- Entity<MapGridComponent>? gridEntity = null;
- // The generation check prevents updating an entity multiple times per tick.
- // As it stands now, it's totally possible for something to get queued twice.
- // Generation on the component is set after an update so we can cull updates that happened this generation.
- if (!smoothQuery.Resolve(uid, ref smooth, false)
- || smooth.Mode == IconSmoothingMode.NoSprite
- || smooth.UpdateGeneration == _generation
- || !smooth.Enabled
- || !smooth.Running)
- {
- if (smooth is { Enabled: true } &&
- TryComp<SmoothEdgeComponent>(uid, out var edge) &&
- xformQuery.TryGetComponent(uid, out xform))
- {
- var directions = DirectionFlag.None;
- if (TryComp(xform.GridUid, out MapGridComponent? grid))
- {
- var gridUid = xform.GridUid.Value;
- var pos = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates);
- gridEntity = (gridUid, grid);
- if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)), smoothQuery))
- directions |= DirectionFlag.North;
- if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)), smoothQuery))
- directions |= DirectionFlag.South;
- if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)), smoothQuery))
- directions |= DirectionFlag.East;
- if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)), smoothQuery))
- directions |= DirectionFlag.West;
- }
- CalculateEdge(uid, directions, component: edge);
- }
- return;
- }
- xform = xformQuery.GetComponent(uid);
- smooth.UpdateGeneration = _generation;
- if (!spriteQuery.TryGetComponent(uid, out var sprite))
- {
- Log.Error($"Encountered a icon-smoothing entity without a sprite: {ToPrettyString(uid)}");
- RemCompDeferred(uid, smooth);
- return;
- }
- var spriteEnt = (uid, sprite);
- if (xform.Anchored)
- {
- if (TryComp(xform.GridUid, out MapGridComponent? grid))
- {
- gridEntity = (xform.GridUid.Value, grid);
- }
- else
- {
- Log.Error($"Failed to calculate IconSmoothComponent sprite in {uid} because grid {xform.GridUid} was missing.");
- return;
- }
- }
- switch (smooth.Mode)
- {
- case IconSmoothingMode.Corners:
- CalculateNewSpriteCorners(gridEntity, smooth, spriteEnt, xform, smoothQuery);
- break;
- case IconSmoothingMode.CardinalFlags:
- CalculateNewSpriteCardinal(gridEntity, smooth, spriteEnt, xform, smoothQuery);
- break;
- case IconSmoothingMode.Diagonal:
- CalculateNewSpriteDiagonal(gridEntity, smooth, spriteEnt, xform, smoothQuery);
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
- private void CalculateNewSpriteDiagonal(Entity<MapGridComponent>? gridEntity, IconSmoothComponent smooth,
- Entity<SpriteComponent> sprite, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
- {
- if (gridEntity == null)
- {
- sprite.Comp.LayerSetState(0, $"{smooth.StateBase}0");
- return;
- }
- var gridUid = gridEntity.Value.Owner;
- var grid = gridEntity.Value.Comp;
- var neighbors = new Vector2[]
- {
- new(1, 0),
- new(1, -1),
- new(0, -1),
- };
- var pos = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates);
- var rotation = xform.LocalRotation;
- var matching = true;
- for (var i = 0; i < neighbors.Length; i++)
- {
- var neighbor = (Vector2i)rotation.RotateVec(neighbors[i]);
- matching = matching && MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos + neighbor), smoothQuery);
- }
- if (matching)
- {
- sprite.Comp.LayerSetState(0, $"{smooth.StateBase}1");
- }
- else
- {
- sprite.Comp.LayerSetState(0, $"{smooth.StateBase}0");
- }
- }
- private void CalculateNewSpriteCardinal(Entity<MapGridComponent>? gridEntity, IconSmoothComponent smooth, Entity<SpriteComponent> sprite, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
- {
- var dirs = CardinalConnectDirs.None;
- if (gridEntity == null)
- {
- sprite.Comp.LayerSetState(0, $"{smooth.StateBase}{(int)dirs}");
- return;
- }
- var gridUid = gridEntity.Value.Owner;
- var grid = gridEntity.Value.Comp;
- var pos = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates);
- if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)), smoothQuery))
- dirs |= CardinalConnectDirs.North;
- if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)), smoothQuery))
- dirs |= CardinalConnectDirs.South;
- if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)), smoothQuery))
- dirs |= CardinalConnectDirs.East;
- if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)), smoothQuery))
- dirs |= CardinalConnectDirs.West;
- sprite.Comp.LayerSetState(0, $"{smooth.StateBase}{(int)dirs}");
- var directions = DirectionFlag.None;
- if ((dirs & CardinalConnectDirs.South) != 0x0)
- directions |= DirectionFlag.South;
- if ((dirs & CardinalConnectDirs.East) != 0x0)
- directions |= DirectionFlag.East;
- if ((dirs & CardinalConnectDirs.North) != 0x0)
- directions |= DirectionFlag.North;
- if ((dirs & CardinalConnectDirs.West) != 0x0)
- directions |= DirectionFlag.West;
- CalculateEdge(sprite, directions, sprite);
- }
- private bool MatchingEntity(IconSmoothComponent smooth, AnchoredEntitiesEnumerator candidates, EntityQuery<IconSmoothComponent> smoothQuery)
- {
- while (candidates.MoveNext(out var entity))
- {
- if (smoothQuery.TryGetComponent(entity, out var other) &&
- other.SmoothKey != null &&
- (other.SmoothKey == smooth.SmoothKey || smooth.AdditionalKeys.Contains(other.SmoothKey)) &&
- other.Enabled)
- {
- return true;
- }
- }
- return false;
- }
- private void CalculateNewSpriteCorners(Entity<MapGridComponent>? gridEntity, IconSmoothComponent smooth, Entity<SpriteComponent> spriteEnt, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
- {
- var (cornerNE, cornerNW, cornerSW, cornerSE) = gridEntity == null
- ? (CornerFill.None, CornerFill.None, CornerFill.None, CornerFill.None)
- : CalculateCornerFill(gridEntity.Value, smooth, xform, smoothQuery);
- // TODO figure out a better way to set multiple sprite layers.
- // This will currently re-calculate the sprite bounding box 4 times.
- // It will also result in 4-8 sprite update events being raised when it only needs to be 1-2.
- // At the very least each event currently only queues a sprite for updating.
- // Oh god sprite component is a mess.
- var sprite = spriteEnt.Comp;
- sprite.LayerSetState(CornerLayers.NE, $"{smooth.StateBase}{(int)cornerNE}");
- sprite.LayerSetState(CornerLayers.SE, $"{smooth.StateBase}{(int)cornerSE}");
- sprite.LayerSetState(CornerLayers.SW, $"{smooth.StateBase}{(int)cornerSW}");
- sprite.LayerSetState(CornerLayers.NW, $"{smooth.StateBase}{(int)cornerNW}");
- var directions = DirectionFlag.None;
- if ((cornerSE & cornerSW) != CornerFill.None)
- directions |= DirectionFlag.South;
- if ((cornerSE & cornerNE) != CornerFill.None)
- directions |= DirectionFlag.East;
- if ((cornerNE & cornerNW) != CornerFill.None)
- directions |= DirectionFlag.North;
- if ((cornerNW & cornerSW) != CornerFill.None)
- directions |= DirectionFlag.West;
- CalculateEdge(spriteEnt, directions, sprite);
- }
- private (CornerFill ne, CornerFill nw, CornerFill sw, CornerFill se) CalculateCornerFill(Entity<MapGridComponent> gridEntity, IconSmoothComponent smooth, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
- {
- var gridUid = gridEntity.Owner;
- var grid = gridEntity.Comp;
- var pos = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates);
- var n = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)), smoothQuery);
- var ne = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.NorthEast)), smoothQuery);
- var e = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)), smoothQuery);
- var se = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.SouthEast)), smoothQuery);
- var s = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)), smoothQuery);
- var sw = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.SouthWest)), smoothQuery);
- var w = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)), smoothQuery);
- var nw = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.NorthWest)), smoothQuery);
- // 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 (xform.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);
- }
- }
- // TODO consider changing this to use DirectionFlags?
- // would require re-labelling all the RSI states.
- [Flags]
- private enum CardinalConnectDirs : byte
- {
- None = 0,
- North = 1,
- South = 2,
- East = 4,
- West = 8
- }
- [Flags]
- private 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,
- }
- private enum CornerLayers : byte
- {
- SE,
- NE,
- NW,
- SW,
- }
- }
- }
|