]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Turnstiles (#36313)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Thu, 24 Apr 2025 11:39:40 +0000 (07:39 -0400)
committerGitHub <noreply@github.com>
Thu, 24 Apr 2025 11:39:40 +0000 (13:39 +0200)
* construction rotation fix

* Turnstiles

* renaming

* review-slarticodefast-1

* mild attempts to fix (sorry sloth)

* move some more shit

* Remove engine dependency

* grid agnostic

* remove debug string

* fix json

* Update Content.Shared/Movement/Pulling/Systems/PullingSystem.cs

Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
* Update Content.Shared/Movement/Pulling/Systems/PullingSystem.cs

Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
* remove pass delay for mispredict reasons.

* most minor of changes

* Give directional indicator on examine

---------

Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
15 files changed:
Content.Client/Doors/TurnstileSystem.cs [new file with mode: 0644]
Content.Server/Doors/Systems/TurnstileSystem.cs [new file with mode: 0644]
Content.Shared/Doors/Components/TurnstileComponent.cs [new file with mode: 0644]
Content.Shared/Doors/Systems/SharedTurnstileSystem.cs [new file with mode: 0644]
Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
Resources/Locale/en-US/doors/components/turnstile.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Structures/Doors/turnstile.yml [new file with mode: 0644]
Resources/Prototypes/Recipes/Construction/Graphs/structures/turnstile.yml [new file with mode: 0644]
Resources/Prototypes/Recipes/Construction/structures.yml
Resources/Textures/Structures/Doors/turnstile.rsi/arrow.png [new file with mode: 0644]
Resources/Textures/Structures/Doors/turnstile.rsi/deny.png [new file with mode: 0644]
Resources/Textures/Structures/Doors/turnstile.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Structures/Doors/turnstile.rsi/operate.png [new file with mode: 0644]
Resources/Textures/Structures/Doors/turnstile.rsi/turnstile.png [new file with mode: 0644]
Resources/Textures/Structures/Doors/turnstile.rsi/turnstile_map.png [new file with mode: 0644]

diff --git a/Content.Client/Doors/TurnstileSystem.cs b/Content.Client/Doors/TurnstileSystem.cs
new file mode 100644 (file)
index 0000000..6e76ce6
--- /dev/null
@@ -0,0 +1,75 @@
+using Content.Shared.Doors.Components;
+using Content.Shared.Doors.Systems;
+using Content.Shared.Examine;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Doors;
+
+/// <inheritdoc/>
+public sealed class TurnstileSystem : SharedTurnstileSystem
+{
+    [Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
+
+    private static EntProtoId _examineArrow = "TurnstileArrow";
+
+    private const string AnimationKey = "Turnstile";
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<TurnstileComponent, AnimationCompletedEvent>(OnAnimationCompleted);
+        SubscribeLocalEvent<TurnstileComponent, ExaminedEvent>(OnExamined);
+    }
+
+    private void OnAnimationCompleted(Entity<TurnstileComponent> ent, ref AnimationCompletedEvent args)
+    {
+        if (args.Key != AnimationKey)
+            return;
+
+        if (!TryComp<SpriteComponent>(ent, out var sprite))
+            return;
+        sprite.LayerSetState(TurnstileVisualLayers.Base, new RSI.StateId(ent.Comp.DefaultState));
+    }
+
+    private void OnExamined(Entity<TurnstileComponent> ent, ref ExaminedEvent args)
+    {
+        Spawn(_examineArrow, new EntityCoordinates(ent, 0, 0));
+    }
+
+    protected override void PlayAnimation(EntityUid uid, string stateId)
+    {
+        if (!TryComp<AnimationPlayerComponent>(uid, out var animation) || !TryComp<SpriteComponent>(uid, out var sprite))
+            return;
+        var ent = (uid, animation);
+
+        if (_animationPlayer.HasRunningAnimation(animation, AnimationKey))
+            _animationPlayer.Stop(ent, AnimationKey);
+
+        if (sprite.BaseRSI == null || !sprite.BaseRSI.TryGetState(stateId, out var state))
+            return;
+        var animLength = state.AnimationLength;
+
+        var anim = new Animation
+        {
+            AnimationTracks =
+            {
+                new AnimationTrackSpriteFlick
+                {
+                    LayerKey = TurnstileVisualLayers.Base,
+                    KeyFrames =
+                    {
+                        new AnimationTrackSpriteFlick.KeyFrame(state.StateId, 0f),
+                    },
+                },
+            },
+            Length = TimeSpan.FromSeconds(animLength),
+        };
+
+        _animationPlayer.Play(ent, anim, AnimationKey);
+    }
+}
diff --git a/Content.Server/Doors/Systems/TurnstileSystem.cs b/Content.Server/Doors/Systems/TurnstileSystem.cs
new file mode 100644 (file)
index 0000000..687a970
--- /dev/null
@@ -0,0 +1,6 @@
+using Content.Shared.Doors.Systems;
+
+namespace Content.Server.Doors.Systems;
+
+/// <inheritdoc/>
+public sealed class TurnstileSystem : SharedTurnstileSystem;
diff --git a/Content.Shared/Doors/Components/TurnstileComponent.cs b/Content.Shared/Doors/Components/TurnstileComponent.cs
new file mode 100644 (file)
index 0000000..5f19dd3
--- /dev/null
@@ -0,0 +1,71 @@
+using Content.Shared.Doors.Systems;
+using Content.Shared.Whitelist;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Doors.Components;
+
+/// <summary>
+/// This is used for a condition door that allows entry only through a single side.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
+[Access(typeof(SharedTurnstileSystem))]
+public sealed partial class TurnstileComponent : Component
+{
+    /// <summary>
+    /// A whitelist of the things this turnstile can choose to block or let through.
+    /// Things not in this whitelist will be ignored by default.
+    /// </summary>
+    [DataField]
+    public EntityWhitelist? ProcessWhitelist;
+
+    /// <summary>
+    /// The next time at which the resist message can show.
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
+    public TimeSpan NextResistTime;
+
+    /// <summary>
+    /// Maintained hashset of entities currently passing through the turnstile.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public HashSet<EntityUid> CollideExceptions = new();
+
+    /// <summary>
+    /// default state of the turnstile sprite.
+    /// </summary>
+    [DataField]
+    public string DefaultState = "turnstile";
+
+    /// <summary>
+    /// animation state of the turnstile spinning.
+    /// </summary>
+    [DataField]
+    public string SpinState = "operate";
+
+    /// <summary>
+    /// animation state of the turnstile denying entry.
+    /// </summary>
+    [DataField]
+    public string DenyState = "deny";
+
+    /// <summary>
+    /// Sound to play when the turnstile admits a mob through.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier? TurnSound = new SoundPathSpecifier("/Audio/Items/ratchet.ogg");
+
+    /// <summary>
+    /// Sound to play when the turnstile denies entry
+    /// </summary>
+    [DataField]
+    public SoundSpecifier? DenySound = new SoundPathSpecifier("/Audio/Machines/airlock_deny.ogg");
+}
+
+[Serializable, NetSerializable]
+public enum TurnstileVisualLayers : byte
+{
+    Base
+}
diff --git a/Content.Shared/Doors/Systems/SharedTurnstileSystem.cs b/Content.Shared/Doors/Systems/SharedTurnstileSystem.cs
new file mode 100644 (file)
index 0000000..0af6b91
--- /dev/null
@@ -0,0 +1,131 @@
+using Content.Shared.Access.Systems;
+using Content.Shared.Doors.Components;
+using Content.Shared.Movement.Pulling.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Whitelist;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Physics.Events;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Doors.Systems;
+
+/// <summary>
+/// This handles logic and interactions related to <see cref="TurnstileComponent"/>
+/// </summary>
+public abstract partial class SharedTurnstileSystem : EntitySystem
+{
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly AccessReaderSystem _accessReader = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
+    [Dependency] private readonly PullingSystem _pulling = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<TurnstileComponent, PreventCollideEvent>(OnPreventCollide);
+        SubscribeLocalEvent<TurnstileComponent, StartCollideEvent>(OnStartCollide);
+        SubscribeLocalEvent<TurnstileComponent, EndCollideEvent>(OnEndCollide);
+    }
+
+    private void OnPreventCollide(Entity<TurnstileComponent> ent, ref PreventCollideEvent args)
+    {
+        if (args.Cancelled || !args.OurFixture.Hard || !args.OtherFixture.Hard)
+            return;
+
+        if (ent.Comp.CollideExceptions.Contains(args.OtherEntity))
+        {
+            args.Cancelled = true;
+            return;
+        }
+
+        // We need to add this in here too for chain pulls
+        if (_pulling.GetPuller(args.OtherEntity) is { } puller && ent.Comp.CollideExceptions.Contains(puller))
+        {
+            ent.Comp.CollideExceptions.Add(args.OtherEntity);
+            Dirty(ent);
+            args.Cancelled = true;
+            return;
+        }
+
+        // unblockables go through for free.
+        if (_entityWhitelist.IsWhitelistFail(ent.Comp.ProcessWhitelist, args.OtherEntity))
+        {
+            args.Cancelled = true;
+            return;
+        }
+
+        if (CanPassDirection(ent, args.OtherEntity))
+        {
+            if (!_accessReader.IsAllowed(args.OtherEntity, ent))
+                return;
+
+            ent.Comp.CollideExceptions.Add(args.OtherEntity);
+            if (_pulling.GetPulling(args.OtherEntity) is { } uid)
+                ent.Comp.CollideExceptions.Add(uid);
+
+            args.Cancelled = true;
+            Dirty(ent);
+        }
+        else
+        {
+            if (_timing.CurTime >= ent.Comp.NextResistTime)
+            {
+                _popup.PopupClient(Loc.GetString("turnstile-component-popup-resist", ("turnstile", ent.Owner)), ent, args.OtherEntity);
+                ent.Comp.NextResistTime = _timing.CurTime + TimeSpan.FromSeconds(0.1);
+                Dirty(ent);
+            }
+        }
+    }
+
+    private void OnStartCollide(Entity<TurnstileComponent> ent, ref StartCollideEvent args)
+    {
+        if (!ent.Comp.CollideExceptions.Contains(args.OtherEntity))
+        {
+            if (CanPassDirection(ent, args.OtherEntity))
+            {
+                if (!_accessReader.IsAllowed(args.OtherEntity, ent))
+                {
+                    _audio.PlayPredicted(ent.Comp.DenySound, ent, args.OtherEntity);
+                    PlayAnimation(ent, ent.Comp.DenyState);
+                }
+            }
+
+            return;
+        }
+        // if they passed through:
+        PlayAnimation(ent, ent.Comp.SpinState);
+        _audio.PlayPredicted(ent.Comp.TurnSound, ent, args.OtherEntity);
+    }
+
+    private void OnEndCollide(Entity<TurnstileComponent> ent, ref EndCollideEvent args)
+    {
+        if (!args.OurFixture.Hard)
+        {
+            ent.Comp.CollideExceptions.Remove(args.OtherEntity);
+            Dirty(ent);
+        }
+    }
+
+    protected bool CanPassDirection(Entity<TurnstileComponent> ent, EntityUid other)
+    {
+        var xform = Transform(ent);
+        var otherXform = Transform(other);
+
+        var (pos, rot) = _transform.GetWorldPositionRotation(xform);
+        var otherPos = _transform.GetWorldPosition(otherXform);
+
+        var approachAngle = (pos - otherPos).ToAngle();
+        var rotateAngle = rot.ToWorldVec().ToAngle();
+
+        var dif = Math.Min(Math.Abs(approachAngle.Theta - rotateAngle.Theta), Math.Abs(rotateAngle.Theta - approachAngle.Theta));
+        return dif < Math.PI / 4;
+    }
+
+    protected virtual void PlayAnimation(EntityUid uid, string stateId)
+    {
+
+    }
+}
index 6ac0460f8eeb4500cf5d457097748e7c195bb951..369225df2de6fc2b1dee9afb232288f0f7897693 100644 (file)
@@ -341,6 +341,16 @@ public sealed class PullingSystem : EntitySystem
         return Resolve(puller, ref component, false) && component.Pulling != null;
     }
 
+    public EntityUid? GetPuller(EntityUid puller, PullableComponent? component = null)
+    {
+        return !Resolve(puller, ref component, false) ? null : component.Puller;
+    }
+
+    public EntityUid? GetPulling(EntityUid puller, PullerComponent? component = null)
+    {
+        return !Resolve(puller, ref component, false) ? null : component.Pulling;
+    }
+
     private void OnReleasePulledObject(ICommonSession? session)
     {
         if (session?.AttachedEntity is not { Valid: true } player)
diff --git a/Resources/Locale/en-US/doors/components/turnstile.ftl b/Resources/Locale/en-US/doors/components/turnstile.ftl
new file mode 100644 (file)
index 0000000..16dca94
--- /dev/null
@@ -0,0 +1 @@
+turnstile-component-popup-resist = {CAPITALIZE(THE($turnstile))} resists your efforts!
diff --git a/Resources/Prototypes/Entities/Structures/Doors/turnstile.yml b/Resources/Prototypes/Entities/Structures/Doors/turnstile.yml
new file mode 100644 (file)
index 0000000..6d675c5
--- /dev/null
@@ -0,0 +1,77 @@
+- type: entity
+  id: Turnstile
+  parent: BaseStructure
+  name: turnstile
+  description: A mechanical door that permits one-way access and prevents tailgating.
+  components:
+  - type: Sprite
+    sprite: Structures/Doors/turnstile.rsi
+    snapCardinals: true
+    drawdepth: Doors
+    layers:
+    - state: turnstile
+      map: [ "enum.TurnstileVisualLayers.Base" ]
+  - type: AnimationPlayer
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.49,-0.49,0.49,0.49" # same dimensions as a door for tall turnstile, prevents objects being thrown through
+        density: 100
+        mask:
+        - FullTileMask
+        layer:
+        - AirlockLayer
+      fix2:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.50,-0.50,0.50,0.50" # same dimensions as a door for tall turnstile, prevents objects being thrown through
+        hard: false
+        mask:
+        - FullTileMask
+        layer:
+        - AirlockLayer
+  - type: MeleeSound
+    soundGroups:
+      Brute:
+        path:
+          "/Audio/Weapons/smash.ogg"
+  - type: InteractionOutline
+  - type: Turnstile
+    processWhitelist:
+      components:
+      - MobState # no mobs
+      - Pullable # no dragging things in
+  - type: Appearance
+  - type: Damageable
+    damageContainer: Inorganic
+    damageModifierSet: StrongMetallic
+  - type: Destructible
+    thresholds:
+    - trigger:
+        !type:DamageTrigger
+        damage: 500
+      behaviors:
+      - !type:DoActsBehavior
+        acts: ["Destruction"]
+  - type: Construction
+    graph: Turnstile
+    node: turnstile
+
+- # Spawned by the client-side turnstile examine code to indicate the direction to pass through.
+  type: entity
+  id: TurnstileArrow
+  categories: [ HideSpawnMenu ]
+  components:
+  - type: Sprite
+    sprite: Structures/Doors/turnstile.rsi
+    color: "#FFFFFFBB"
+    layers:
+    - state: arrow
+      offset: 0, 0.78125
+  - type: TimedDespawn
+    lifetime: 2
+  - type: Tag
+    tags:
+    - HideContextMenu
diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/structures/turnstile.yml b/Resources/Prototypes/Recipes/Construction/Graphs/structures/turnstile.yml
new file mode 100644 (file)
index 0000000..cc1b8ef
--- /dev/null
@@ -0,0 +1,33 @@
+- type: constructionGraph
+  id: Turnstile
+  start: start
+  graph:
+  - node: start
+    actions:
+    - !type:DeleteEntity { }
+    edges:
+    - to: turnstile
+      completed:
+      - !type:SnapToGrid
+      steps:
+      - material: MetalRod
+        amount: 4
+        doAfter: 6
+      - material: Steel
+        amount: 1
+        doAfter: 2
+
+  - node: turnstile
+    entity: Turnstile
+    edges:
+    - to: start
+      completed:
+      - !type:SpawnPrototype
+        prototype: PartRodMetal1
+        amount: 4
+      - !type:DeleteEntity
+      steps:
+      - tool: Welding
+        doAfter: 4.0
+      - tool: Cutting
+        doAfter: 2.0
index 7602a6cb1168780b016551e0e51bd8f0db432f31..11579ac9baa8a4878f0314989f276aae98b0dcd3 100644 (file)
   conditions:
     - !type:TileNotBlocked
 
+- type: construction
+  name: turnstile
+  id: Turnstile
+  graph: Turnstile
+  startNode: start
+  targetNode: turnstile
+  category: construction-category-structures
+  description: A mechanical door that permits one-way access and prevents tailgating.
+  icon:
+    sprite: Structures/Doors/turnstile.rsi
+    state: turnstile
+  objectType: Structure
+  placementMode: SnapgridCenter
+  canBuildInImpassable: false
+  conditions:
+  - !type:TileNotBlocked
+
 - type: construction
   name: shutter
   id: Shutters
diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/arrow.png b/Resources/Textures/Structures/Doors/turnstile.rsi/arrow.png
new file mode 100644 (file)
index 0000000..6332f6b
Binary files /dev/null and b/Resources/Textures/Structures/Doors/turnstile.rsi/arrow.png differ
diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/deny.png b/Resources/Textures/Structures/Doors/turnstile.rsi/deny.png
new file mode 100644 (file)
index 0000000..4afe444
Binary files /dev/null and b/Resources/Textures/Structures/Doors/turnstile.rsi/deny.png differ
diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/meta.json b/Resources/Textures/Structures/Doors/turnstile.rsi/meta.json
new file mode 100644 (file)
index 0000000..dea5fb8
--- /dev/null
@@ -0,0 +1,65 @@
+{
+    "version": 1,
+    "license": "CC-BY-SA-3.0",
+    "copyright": "Taken from OracleStation at https://github.com/OracleStation/OracleStation/blob/c1046bdd14674dc5a8631074aaa6650770b2937e/icons/obj/turnstile.dmi. Arrow by EmoGarbage404 (github).",
+    "size": {
+        "x": 32,
+        "y": 32
+    },
+    "states": [
+        {
+          "name": "arrow",
+          "delays": [
+            [
+              0.5,
+              0.1,
+              0.1,
+              0.1,
+              0.1,
+              0.1,
+              0.1,
+              0.1,
+              0.1,
+              0.1,
+              0.1
+            ]
+          ]
+        },
+        {
+            "name": "turnstile"
+        },
+        {
+            "name": "turnstile_map",
+            "directions": 4
+        },
+        {
+            "name": "operate",
+            "delays": [
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        },
+        {
+            "name": "deny",
+            "delays": [
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        }
+    ]
+}
diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/operate.png b/Resources/Textures/Structures/Doors/turnstile.rsi/operate.png
new file mode 100644 (file)
index 0000000..3178c29
Binary files /dev/null and b/Resources/Textures/Structures/Doors/turnstile.rsi/operate.png differ
diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile.png b/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile.png
new file mode 100644 (file)
index 0000000..48c8557
Binary files /dev/null and b/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile.png differ
diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile_map.png b/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile_map.png
new file mode 100644 (file)
index 0000000..fb3fcc5
Binary files /dev/null and b/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile_map.png differ