]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
rcd refactor (#15172)
authordeltanedas <39013340+deltanedas@users.noreply.github.com>
Mon, 1 May 2023 13:46:59 +0000 (13:46 +0000)
committerGitHub <noreply@github.com>
Mon, 1 May 2023 13:46:59 +0000 (23:46 +1000)
Co-authored-by: deltanedas <@deltanedas:kde.org>
14 files changed:
Content.Client/Popups/PopupSystem.cs
Content.Server/Popups/PopupSystem.cs
Content.Server/RCD/Components/RCDAmmoComponent.cs [deleted file]
Content.Server/RCD/Components/RCDComponent.cs [deleted file]
Content.Server/RCD/Systems/RCDAmmoSystem.cs [deleted file]
Content.Server/RCD/Systems/RCDSystem.cs [deleted file]
Content.Shared/Popups/SharedPopupSystem.cs
Content.Shared/RCD/Components/RCDAmmoComponent.cs [new file with mode: 0644]
Content.Shared/RCD/Components/RCDComponent.cs [new file with mode: 0644]
Content.Shared/RCD/Systems/RCDAmmoSystem.cs [new file with mode: 0644]
Content.Shared/RCD/Systems/RCDSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/rcd/components/rcd-ammo-component.ftl
Resources/Locale/en-US/rcd/components/rcd-component.ftl
Resources/Prototypes/Entities/Objects/Tools/tools.yml

index 14e456177db08b3c559a48473b5faa278c7a3003..573c45617aeaaa9578d6ec85cc59eacc8558128d 100644 (file)
@@ -10,6 +10,7 @@ using Robust.Shared.Map;
 using Robust.Shared.Player;
 using Robust.Shared.Players;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
 using Robust.Shared.Utility;
 
 namespace Content.Client.Popups
@@ -22,6 +23,7 @@ namespace Content.Client.Popups
         [Dependency] private readonly IPlayerManager _playerManager = default!;
         [Dependency] private readonly IPrototypeManager _prototype = default!;
         [Dependency] private readonly IResourceCache _resource = default!;
+        [Dependency] private readonly IGameTiming _timing = default!;
         [Dependency] private readonly IUserInterfaceManager _uiManager = default!;
 
         public IReadOnlyList<WorldPopupLabel> WorldLabels => _aliveWorldLabels;
@@ -123,6 +125,12 @@ namespace Content.Client.Popups
             PopupEntity(message, uid, type);
         }
 
+        public override void PopupClient(string message, EntityUid uid, EntityUid recipient, PopupType type = PopupType.Small)
+        {
+            if (_timing.IsFirstTimePredicted)
+                PopupEntity(message, uid, recipient);
+        }
+
         public override void PopupEntity(string message, EntityUid uid, PopupType type = PopupType.Small)
         {
             if (!EntityManager.EntityExists(uid))
index ef5cd7ca88f1a3a96917a6a7462d08216e891b9b..9c4b0ae082a5ed5d5176120381b912a225f20259 100644 (file)
@@ -64,6 +64,11 @@ namespace Content.Server.Popups
                 RaiseNetworkEvent(new PopupEntityEvent(message, type, uid), actor.PlayerSession);
         }
 
+        public override void PopupClient(string message, EntityUid uid, EntityUid recipient, PopupType type = PopupType.Small)
+        {
+            // do nothing duh its for client only
+        }
+
 
         public override void PopupEntity(string message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small)
         {
diff --git a/Content.Server/RCD/Components/RCDAmmoComponent.cs b/Content.Server/RCD/Components/RCDAmmoComponent.cs
deleted file mode 100644 (file)
index 29a0d07..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Content.Server.RCD.Components
-{
-    [RegisterComponent]
-    public sealed class RCDAmmoComponent : Component
-    {
-        //How much ammo we refill
-        [ViewVariables(VVAccess.ReadWrite)] [DataField("refillAmmo")] public int RefillAmmo = 5;
-    }
-}
diff --git a/Content.Server/RCD/Components/RCDComponent.cs b/Content.Server/RCD/Components/RCDComponent.cs
deleted file mode 100644 (file)
index 5375ae7..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-using System.Threading;
-using Robust.Shared.Audio;
-
-namespace Content.Server.RCD.Components
-{
-    public enum RcdMode : byte
-    {
-        Floors,
-        Walls,
-        Airlock,
-        Deconstruct
-    }
-
-    [RegisterComponent]
-    public sealed class RCDComponent : Component
-    {
-        private const int DefaultAmmoCount = 5;
-
-        [ViewVariables(VVAccess.ReadOnly)]
-        [DataField("maxAmmo")] public int MaxAmmo = DefaultAmmoCount;
-
-        [ViewVariables(VVAccess.ReadWrite)] [DataField("delay")]
-        public float Delay = 2f;
-
-        [DataField("swapModeSound")]
-        public SoundSpecifier SwapModeSound = new SoundPathSpecifier("/Audio/Items/genhit.ogg");
-
-        [DataField("successSound")]
-        public SoundSpecifier SuccessSound = new SoundPathSpecifier("/Audio/Items/deconstruct.ogg");
-
-        /// <summary>
-        ///     What mode are we on? Can be floors, walls, deconstruct.
-        /// </summary>
-        [DataField("mode")]
-        public RcdMode Mode = RcdMode.Floors;
-
-        /// <summary>
-        ///     How much "ammo" we have left. You can refill this with RCD ammo.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)] [DataField("ammo")]
-        public int CurrentAmmo = DefaultAmmoCount;
-    }
-}
diff --git a/Content.Server/RCD/Systems/RCDAmmoSystem.cs b/Content.Server/RCD/Systems/RCDAmmoSystem.cs
deleted file mode 100644 (file)
index b6d45e5..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-using Content.Server.RCD.Components;
-using Content.Shared.Examine;
-using Content.Shared.Interaction;
-using Content.Shared.Popups;
-
-namespace Content.Server.RCD.Systems
-{
-    public sealed class RCDAmmoSystem : EntitySystem
-    {
-        public override void Initialize()
-        {
-            base.Initialize();
-            SubscribeLocalEvent<RCDAmmoComponent, ExaminedEvent>(OnExamine);
-            SubscribeLocalEvent<RCDAmmoComponent, AfterInteractEvent>(OnAfterInteract);
-        }
-
-        private void OnExamine(EntityUid uid, RCDAmmoComponent component, ExaminedEvent args)
-        {
-            var examineMessage = Loc.GetString("rcd-ammo-component-on-examine-text", ("ammo", component.RefillAmmo));
-            args.PushText(examineMessage);
-        }
-
-        private void OnAfterInteract(EntityUid uid, RCDAmmoComponent component, AfterInteractEvent args)
-        {
-            if (args.Handled || !args.CanReach)
-                return;
-
-            if (args.Target is not {Valid: true} target ||
-                !EntityManager.TryGetComponent(target, out RCDComponent? rcdComponent))
-                return;
-
-            if (rcdComponent.MaxAmmo - rcdComponent.CurrentAmmo < component.RefillAmmo)
-            {
-                rcdComponent.Owner.PopupMessage(args.User, Loc.GetString("rcd-ammo-component-after-interact-full-text"));
-                args.Handled = true;
-                return;
-            }
-
-            rcdComponent.CurrentAmmo = Math.Min(rcdComponent.MaxAmmo, rcdComponent.CurrentAmmo + component.RefillAmmo);
-            rcdComponent.Owner.PopupMessage(args.User, Loc.GetString("rcd-ammo-component-after-interact-refilled-text"));
-            EntityManager.QueueDeleteEntity(uid);
-
-            args.Handled = true;
-        }
-    }
-}
diff --git a/Content.Server/RCD/Systems/RCDSystem.cs b/Content.Server/RCD/Systems/RCDSystem.cs
deleted file mode 100644 (file)
index 9a2fb2f..0000000
+++ /dev/null
@@ -1,246 +0,0 @@
-using Content.Server.Administration.Logs;
-using Content.Server.Popups;
-using Content.Server.RCD.Components;
-using Content.Shared.Database;
-using Content.Shared.DoAfter;
-using Content.Shared.Examine;
-using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Maps;
-using Content.Shared.Tag;
-using Robust.Shared.Audio;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Player;
-
-namespace Content.Server.RCD.Systems
-{
-    public sealed class RCDSystem : EntitySystem
-    {
-        [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
-        [Dependency] private readonly IMapManager _mapManager = default!;
-
-        [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-        [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
-        [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
-        [Dependency] private readonly PopupSystem _popup = default!;
-        [Dependency] private readonly TagSystem _tagSystem = default!;
-
-        private readonly int RCDModeCount = Enum.GetValues(typeof(RcdMode)).Length;
-
-        public override void Initialize()
-        {
-            base.Initialize();
-            SubscribeLocalEvent<RCDComponent, ExaminedEvent>(OnExamine);
-            SubscribeLocalEvent<RCDComponent, UseInHandEvent>(OnUseInHand);
-            SubscribeLocalEvent<RCDComponent, AfterInteractEvent>(OnAfterInteract);
-        }
-
-        private void OnExamine(EntityUid uid, RCDComponent component, ExaminedEvent args)
-        {
-            var msg = Loc.GetString("rcd-component-examine-detail-count",
-                ("mode", component.Mode), ("ammoCount", component.CurrentAmmo));
-            args.PushMarkup(msg);
-        }
-
-        private void OnUseInHand(EntityUid uid, RCDComponent component, UseInHandEvent args)
-        {
-            if (args.Handled)
-                return;
-
-            NextMode(uid, component, args.User);
-            args.Handled = true;
-        }
-
-        private async void OnAfterInteract(EntityUid uid, RCDComponent rcd, AfterInteractEvent args)
-        {
-            if (args.Handled || !args.CanReach)
-                return;
-
-            if (!args.ClickLocation.IsValid(EntityManager)) return;
-
-            var clickLocationMod = args.ClickLocation;
-            // Initial validity check
-            if (!clickLocationMod.IsValid(EntityManager))
-                return;
-            // Try to fix it (i.e. if clicking on space)
-            // Note: Ideally there'd be a better way, but there isn't right now.
-            var gridIdOpt = clickLocationMod.GetGridUid(EntityManager);
-            if (!(gridIdOpt is EntityUid gridId) || !gridId.IsValid())
-            {
-                clickLocationMod = clickLocationMod.AlignWithClosestGridTile();
-                gridIdOpt = clickLocationMod.GetGridUid(EntityManager);
-                // Check if fixing it failed / get final grid ID
-                if (!(gridIdOpt is EntityUid gridId2) || !gridId2.IsValid())
-                    return;
-                gridId = gridId2;
-            }
-
-            var mapGrid = _mapManager.GetGrid(gridId);
-            var tile = mapGrid.GetTileRef(clickLocationMod);
-            var snapPos = mapGrid.TileIndicesFor(clickLocationMod);
-
-            //No changing mode mid-RCD
-            var startingMode = rcd.Mode;
-            args.Handled = true;
-            var user = args.User;
-
-            //Using an RCD isn't instantaneous
-            var doAfterEventArgs = new DoAfterArgs(user, rcd.Delay, new AwaitedDoAfterEvent(), null, target: args.Target)
-            {
-                BreakOnDamage = true,
-                NeedHand = true,
-                BreakOnHandChange = true,
-                BreakOnUserMove = true,
-                BreakOnTargetMove = true,
-                AttemptFrequency = AttemptFrequency.EveryTick,
-                ExtraCheck = () => IsRCDStillValid(rcd, args, mapGrid, tile, startingMode) //All of the sanity checks are here
-            };
-
-            var result = await _doAfterSystem.WaitDoAfter(doAfterEventArgs);
-
-            if (result == DoAfterStatus.Cancelled)
-                return;
-
-            switch (rcd.Mode)
-            {
-                //Floor mode just needs the tile to be a space tile (subFloor)
-                case RcdMode.Floors:
-                    mapGrid.SetTile(snapPos, new Tile(_tileDefinitionManager["FloorSteel"].TileId));
-                    _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to set grid: {tile.GridUid} {snapPos} to FloorSteel");
-                    break;
-                //We don't want to place a space tile on something that's already a space tile. Let's do the inverse of the last check.
-                case RcdMode.Deconstruct:
-                    if (!tile.IsBlockedTurf(true)) //Delete the turf
-                    {
-                        mapGrid.SetTile(snapPos, Tile.Empty);
-                        _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to set grid: {tile.GridUid} tile: {snapPos} to space");
-                    }
-                    else //Delete what the user targeted
-                    {
-                        if (args.Target is {Valid: true} target)
-                        {
-                            _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to delete {ToPrettyString(target):target}");
-                            QueueDel(target);
-                        }
-                    }
-                    break;
-                //Walls are a special behaviour, and require us to build a new object with a transform rather than setting a grid tile,
-                // thus we early return to avoid the tile set code.
-                case RcdMode.Walls:
-                    var ent = EntityManager.SpawnEntity("WallSolid", mapGrid.GridTileToLocal(snapPos));
-                    Transform(ent).LocalRotation = Angle.Zero; // Walls always need to point south.
-                    _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to spawn {ToPrettyString(ent)} at {snapPos} on grid {mapGrid.Owner}");
-                    break;
-                case RcdMode.Airlock:
-                    var airlock = EntityManager.SpawnEntity("Airlock", mapGrid.GridTileToLocal(snapPos));
-                    Transform(airlock).LocalRotation = Transform(rcd.Owner).LocalRotation; //Now apply icon smoothing.
-                    _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to spawn {ToPrettyString(airlock)} at {snapPos} on grid {mapGrid.Owner}");
-                    break;
-                default:
-                    args.Handled = true;
-                    return; //I don't know why this would happen, but sure I guess. Get out of here invalid state!
-            }
-
-            SoundSystem.Play(rcd.SuccessSound.GetSound(), Filter.Pvs(uid, entityManager: EntityManager), rcd.Owner);
-            rcd.CurrentAmmo--;
-            args.Handled = true;
-        }
-
-        private bool IsRCDStillValid(RCDComponent rcd, AfterInteractEvent eventArgs, MapGridComponent mapGrid, TileRef tile, RcdMode startingMode)
-        {
-            //Less expensive checks first. Failing those ones, we need to check that the tile isn't obstructed.
-            if (rcd.CurrentAmmo <= 0)
-            {
-                _popup.PopupEntity(Loc.GetString("rcd-component-no-ammo-message"), rcd.Owner, eventArgs.User);
-                return false;
-            }
-
-            if (rcd.Mode != startingMode)
-            {
-                return false;
-            }
-
-            var unobstructed = eventArgs.Target == null
-                ? _interactionSystem.InRangeUnobstructed(eventArgs.User, mapGrid.GridTileToWorld(tile.GridIndices), popup: true)
-                : _interactionSystem.InRangeUnobstructed(eventArgs.User, eventArgs.Target.Value, popup: true);
-
-            if (!unobstructed)
-                return false;
-
-            switch (rcd.Mode)
-            {
-                //Floor mode just needs the tile to be a space tile (subFloor)
-                case RcdMode.Floors:
-                    if (!tile.Tile.IsEmpty)
-                    {
-                        _popup.PopupEntity(Loc.GetString("rcd-component-cannot-build-floor-tile-not-empty-message"), rcd.Owner, eventArgs.User);
-                        return false;
-                    }
-
-                    return true;
-                //We don't want to place a space tile on something that's already a space tile. Let's do the inverse of the last check.
-                case RcdMode.Deconstruct:
-                    if (tile.Tile.IsEmpty)
-                    {
-                        return false;
-                    }
-
-                    //They tried to decon a turf but the turf is blocked
-                    if (eventArgs.Target == null && tile.IsBlockedTurf(true))
-                    {
-                        _popup.PopupEntity(Loc.GetString("rcd-component-tile-obstructed-message"), rcd.Owner, eventArgs.User);
-                        return false;
-                    }
-                    //They tried to decon a non-turf but it's not in the whitelist
-                    if (eventArgs.Target != null && !_tagSystem.HasTag(eventArgs.Target.Value, "RCDDeconstructWhitelist"))
-                    {
-                        _popup.PopupEntity(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), rcd.Owner, eventArgs.User);
-                        return false;
-                    }
-
-                    return true;
-                //Walls are a special behaviour, and require us to build a new object with a transform rather than setting a grid tile, thus we early return to avoid the tile set code.
-                case RcdMode.Walls:
-                    if (tile.Tile.IsEmpty)
-                    {
-                        _popup.PopupEntity(Loc.GetString("rcd-component-cannot-build-wall-tile-not-empty-message"), rcd.Owner, eventArgs.User);
-                        return false;
-                    }
-
-                    if (tile.IsBlockedTurf(true))
-                    {
-                        _popup.PopupEntity(Loc.GetString("rcd-component-tile-obstructed-message"), rcd.Owner, eventArgs.User);
-                        return false;
-                    }
-                    return true;
-                case RcdMode.Airlock:
-                    if (tile.Tile.IsEmpty)
-                    {
-                        _popup.PopupEntity(Loc.GetString("rcd-component-cannot-build-airlock-tile-not-empty-message"), rcd.Owner, eventArgs.User);
-                        return false;
-                    }
-                    if (tile.IsBlockedTurf(true))
-                    {
-                        _popup.PopupEntity(Loc.GetString("rcd-component-tile-obstructed-message"), rcd.Owner, eventArgs.User);
-                        return false;
-                    }
-                    return true;
-                default:
-                    return false; //I don't know why this would happen, but sure I guess. Get out of here invalid state!
-            }
-        }
-
-        private void NextMode(EntityUid uid, RCDComponent rcd, EntityUid user)
-        {
-            SoundSystem.Play(rcd.SwapModeSound.GetSound(), Filter.Pvs(uid, entityManager: EntityManager), uid);
-
-            var mode = (int) rcd.Mode;
-            mode = ++mode % RCDModeCount;
-            rcd.Mode = (RcdMode) mode;
-
-            var msg = Loc.GetString("rcd-component-change-mode", ("mode", rcd.Mode.ToString()));
-            _popup.PopupEntity(msg, rcd.Owner, user);
-        }
-    }
-}
index 8ff3e7467c86524032ff44893bd1ebf04b86ee9c..4f3619dbd3c465cd175bb161b9d630a5d71594c0 100644 (file)
@@ -82,6 +82,12 @@ namespace Content.Shared.Popups
         ///     if the filtering has to be more specific than simply PVS range based.
         /// </summary>
         public abstract void PopupEntity(string message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small);
+
+        /// <summary>
+        /// Variant of <see cref="PopupEnity(string, EntityUid, EntityUid, PopupType)"/> that only runs on the client, outside of prediction.
+        /// Useful for shared code that is always ran by both sides to avoid duplicate popups.
+        /// </summary>
+        public abstract void PopupClient(string message, EntityUid uid, EntityUid recipient, PopupType type = PopupType.Small);
     }
 
     /// <summary>
diff --git a/Content.Shared/RCD/Components/RCDAmmoComponent.cs b/Content.Shared/RCD/Components/RCDAmmoComponent.cs
new file mode 100644 (file)
index 0000000..7b1fc00
--- /dev/null
@@ -0,0 +1,18 @@
+using Content.Shared.RCD.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.RCD.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(RCDAmmoSystem))]
+public sealed partial class RCDAmmoComponent : Component
+{
+    /// <summary>
+    /// How many charges are contained in this ammo cartridge.
+    /// Can be partially transferred into an RCD, until it is empty then it gets deleted.
+    /// </summary>
+    [DataField("charges"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+    public int Charges = 5;
+}
+
+// TODO: state??? check if it desyncs
diff --git a/Content.Shared/RCD/Components/RCDComponent.cs b/Content.Shared/RCD/Components/RCDComponent.cs
new file mode 100644 (file)
index 0000000..8e10328
--- /dev/null
@@ -0,0 +1,51 @@
+using Content.Shared.Maps;
+using Content.Shared.RCD.Systems;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.RCD.Components;
+
+public enum RcdMode : byte
+{
+    Floors,
+    Walls,
+    Airlock,
+    Deconstruct
+}
+
+/// <summary>
+/// Main component for the RCD
+/// Optionally uses LimitedChargesComponent.
+/// Charges can be refilled with RCD ammo
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(RCDSystem))]
+public sealed partial class RCDComponent : Component
+{
+    /// <summary>
+    /// Time taken to do an action like placing a wall
+    /// </summary>
+    [DataField("delay"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+    public float Delay = 2f;
+
+    [DataField("swapModeSound")]
+    public SoundSpecifier SwapModeSound = new SoundPathSpecifier("/Audio/Items/genhit.ogg");
+
+    [DataField("successSound")]
+    public SoundSpecifier SuccessSound = new SoundPathSpecifier("/Audio/Items/deconstruct.ogg");
+
+    /// <summary>
+    /// What mode are we on? Can be floors, walls, airlock, deconstruct.
+    /// </summary>
+    [DataField("mode"), AutoNetworkedField]
+    public RcdMode Mode = RcdMode.Floors;
+
+    /// <summary>
+    /// ID of the floor to create when using the floor mode.
+    /// </summary>
+    [DataField("floor", customTypeSerializer: typeof(PrototypeIdSerializer<ContentTileDefinition>))]
+    [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+    public string Floor = "FloorSteel";
+}
diff --git a/Content.Shared/RCD/Systems/RCDAmmoSystem.cs b/Content.Shared/RCD/Systems/RCDAmmoSystem.cs
new file mode 100644 (file)
index 0000000..9481d29
--- /dev/null
@@ -0,0 +1,62 @@
+using Content.Shared.Charges.Components;
+using Content.Shared.Charges.Systems;
+using Content.Shared.Examine;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.RCD.Components;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.RCD.Systems;
+
+public sealed class RCDAmmoSystem : EntitySystem
+{
+    [Dependency] private readonly SharedChargesSystem _charges = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<RCDAmmoComponent, ExaminedEvent>(OnExamine);
+        SubscribeLocalEvent<RCDAmmoComponent, AfterInteractEvent>(OnAfterInteract);
+    }
+
+    private void OnExamine(EntityUid uid, RCDAmmoComponent comp, ExaminedEvent args)
+    {
+        if (!args.IsInDetailsRange)
+            return;
+
+        var examineMessage = Loc.GetString("rcd-ammo-component-on-examine", ("charges", comp.Charges));
+        args.PushText(examineMessage);
+    }
+
+    private void OnAfterInteract(EntityUid uid, RCDAmmoComponent comp, AfterInteractEvent args)
+    {
+        if (args.Handled || !args.CanReach || !_timing.IsFirstTimePredicted)
+            return;
+
+        if (args.Target is not {Valid: true} target ||
+            !HasComp<RCDComponent>(target) ||
+            !TryComp<LimitedChargesComponent>(target, out var charges))
+            return;
+
+        var user = args.User;
+        args.Handled = true;
+        var count = Math.Min(charges.MaxCharges - charges.Charges, comp.Charges);
+        if (count <= 0)
+        {
+            _popup.PopupClient(Loc.GetString("rcd-ammo-component-after-interact-full"), target, user);
+            return;
+        }
+
+        _popup.PopupClient(Loc.GetString("rcd-ammo-component-after-interact-refilled"), target, user);
+        _charges.AddCharges(target, count, charges);
+        comp.Charges -= count;
+        Dirty(comp);
+
+        // prevent having useless ammo with 0 charges
+        if (comp.Charges <= 0)
+            QueueDel(uid);
+    }
+}
diff --git a/Content.Shared/RCD/Systems/RCDSystem.cs b/Content.Shared/RCD/Systems/RCDSystem.cs
new file mode 100644 (file)
index 0000000..9bab9c6
--- /dev/null
@@ -0,0 +1,324 @@
+using Content.Shared.Administration.Logs;
+using Content.Shared.Charges.Components;
+using Content.Shared.Charges.Systems;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
+using Content.Shared.Examine;
+using Content.Shared.Interaction;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Maps;
+using Content.Shared.Physics;
+using Content.Shared.Popups;
+using Content.Shared.RCD.Components;
+using Content.Shared.Tag;
+using Robust.Shared.Audio;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.RCD.Systems;
+
+public sealed class RCDSystem : EntitySystem
+{
+    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedChargesSystem _charges = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private readonly SharedInteractionSystem _interaction = default!;
+    [Dependency] private readonly IMapManager _mapMan = default!;
+    [Dependency] private readonly INetManager _net = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly TagSystem _tag = default!;
+    [Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly TurfSystem _turf = default!;
+
+    private readonly int RcdModeCount = Enum.GetValues(typeof(RcdMode)).Length;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<RCDComponent, ExaminedEvent>(OnExamine);
+        SubscribeLocalEvent<RCDComponent, UseInHandEvent>(OnUseInHand);
+        SubscribeLocalEvent<RCDComponent, AfterInteractEvent>(OnAfterInteract);
+        SubscribeLocalEvent<RCDComponent, RCDDoAfterEvent>(OnDoAfter);
+        SubscribeLocalEvent<RCDComponent, DoAfterAttemptEvent<RCDDoAfterEvent>>(OnDoAfterAttempt);
+    }
+
+    private void OnExamine(EntityUid uid, RCDComponent comp, ExaminedEvent args)
+    {
+        if (!args.IsInDetailsRange)
+            return;
+
+        var msg = Loc.GetString("rcd-component-examine-detail", ("mode", comp.Mode));
+        args.PushMarkup(msg);
+    }
+
+    private void OnUseInHand(EntityUid uid, RCDComponent comp, UseInHandEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        NextMode(uid, comp, args.User);
+        args.Handled = true;
+    }
+
+    private void OnAfterInteract(EntityUid uid, RCDComponent comp, AfterInteractEvent args)
+    {
+        if (args.Handled || !args.CanReach)
+            return;
+
+        var user = args.User;
+
+        TryComp<LimitedChargesComponent>(uid, out var charges);
+        if (_charges.IsEmpty(uid, charges))
+        {
+            _popup.PopupClient(Loc.GetString("rcd-component-no-ammo-message"), uid, user);
+            return;
+        }
+
+        var location = args.ClickLocation;
+        // Initial validity check
+        if (!location.IsValid(EntityManager))
+            return;
+
+        var gridId = location.GetGridUid(EntityManager);
+        if (!HasComp<MapGridComponent>(gridId))
+        {
+            location = location.AlignWithClosestGridTile();
+            gridId = location.GetGridUid(EntityManager);
+            // Check if fixing it failed / get final grid ID
+            if (!HasComp<MapGridComponent>(gridId))
+                return;
+        }
+
+        var doAfterArgs = new DoAfterArgs(user, comp.Delay, new RCDDoAfterEvent(location, comp.Mode), uid, target: args.Target, used: uid)
+        {
+            BreakOnDamage = true,
+            NeedHand = true,
+            BreakOnHandChange = true,
+            BreakOnUserMove = true,
+            BreakOnTargetMove = args.Target != null,
+            AttemptFrequency = AttemptFrequency.EveryTick
+        };
+
+        args.Handled = true;
+        _doAfter.TryStartDoAfter(doAfterArgs);
+    }
+
+    private void OnDoAfterAttempt(EntityUid uid, RCDComponent comp, DoAfterAttemptEvent<RCDDoAfterEvent> args)
+    {
+        // sus client crash why
+        if (args.Event?.DoAfter?.Args == null)
+            return;
+
+        var location = args.Event.Location;
+
+        var gridId = location.GetGridUid(EntityManager);
+        if (!HasComp<MapGridComponent>(gridId))
+        {
+            location = location.AlignWithClosestGridTile();
+            gridId = location.GetGridUid(EntityManager);
+            // Check if fixing it failed / get final grid ID
+            if (!HasComp<MapGridComponent>(gridId))
+                return;
+        }
+
+        var mapGrid = _mapMan.GetGrid(gridId.Value);
+        var tile = mapGrid.GetTileRef(location);
+
+        if (!IsRCDStillValid(uid, comp, args.Event.User, args.Event.Target, mapGrid, tile, args.Event.StartingMode))
+            args.Cancel();
+    }
+
+    private void OnDoAfter(EntityUid uid, RCDComponent comp, RCDDoAfterEvent args)
+    {
+        if (args.Handled || args.Cancelled || !_timing.IsFirstTimePredicted)
+            return;
+
+        var user = args.User;
+        var location = args.Location;
+
+        var gridId = location.GetGridUid(EntityManager);
+        if (!HasComp<MapGridComponent>(gridId))
+        {
+            location = location.AlignWithClosestGridTile();
+            gridId = location.GetGridUid(EntityManager);
+            // Check if fixing it failed / get final grid ID
+            if (!HasComp<MapGridComponent>(gridId))
+                return;
+        }
+
+        var mapGrid = _mapMan.GetGrid(gridId.Value);
+        var tile = mapGrid.GetTileRef(location);
+        var snapPos = mapGrid.TileIndicesFor(location);
+
+        switch (comp.Mode)
+        {
+            //Floor mode just needs the tile to be a space tile (subFloor)
+            case RcdMode.Floors:
+
+                mapGrid.SetTile(snapPos, new Tile(_tileDefMan[comp.Floor].TileId));
+                _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to set grid: {tile.GridUid} {snapPos} to {comp.Floor}");
+                break;
+            //We don't want to place a space tile on something that's already a space tile. Let's do the inverse of the last check.
+            case RcdMode.Deconstruct:
+                if (!IsTileBlocked(tile)) // Delete the turf
+                {
+                    mapGrid.SetTile(snapPos, Tile.Empty);
+                    _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to set grid: {tile.GridUid} tile: {snapPos} to space");
+                }
+                else // Delete the targeted thing
+                {
+                    var target = args.Target!.Value;
+                    _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to delete {ToPrettyString(target):target}");
+                    QueueDel(target);
+                }
+                break;
+            //Walls are a special behaviour, and require us to build a new object with a transform rather than setting a grid tile,
+            // thus we early return to avoid the tile set code.
+            case RcdMode.Walls:
+                // only spawn on the server
+                if (_net.IsServer)
+                {
+                    var ent = Spawn("WallSolid", mapGrid.GridTileToLocal(snapPos));
+                    Transform(ent).LocalRotation = Angle.Zero; // Walls always need to point south.
+                    _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to spawn {ToPrettyString(ent)} at {snapPos} on grid {tile.GridUid}");
+                }
+                break;
+            case RcdMode.Airlock:
+                // only spawn on the server
+                if (_net.IsServer)
+                {
+                    var airlock = Spawn("Airlock", mapGrid.GridTileToLocal(snapPos));
+                    Transform(airlock).LocalRotation = Transform(uid).LocalRotation; //Now apply icon smoothing.
+                    _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to spawn {ToPrettyString(airlock)} at {snapPos} on grid {tile.GridUid}");
+                }
+                break;
+            default:
+                args.Handled = true;
+                return; //I don't know why this would happen, but sure I guess. Get out of here invalid state!
+        }
+
+        _audio.PlayPredicted(comp.SuccessSound, uid, user);
+        _charges.UseCharge(uid);
+        args.Handled = true;
+    }
+
+    private bool IsRCDStillValid(EntityUid uid, RCDComponent comp, EntityUid user, EntityUid? target, MapGridComponent mapGrid, TileRef tile, RcdMode startingMode)
+    {
+        //Less expensive checks first. Failing those ones, we need to check that the tile isn't obstructed.
+        if (comp.Mode != startingMode)
+            return false;
+
+        var unobstructed = target == null
+            ? _interaction.InRangeUnobstructed(user, mapGrid.GridTileToWorld(tile.GridIndices), popup: true)
+            : _interaction.InRangeUnobstructed(user, target.Value, popup: true);
+
+        if (!unobstructed)
+            return false;
+
+        switch (comp.Mode)
+        {
+            //Floor mode just needs the tile to be a space tile (subFloor)
+            case RcdMode.Floors:
+                if (!tile.Tile.IsEmpty)
+                {
+                    _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-floor-tile-not-empty-message"), uid, user);
+                    return false;
+                }
+
+                return true;
+            //We don't want to place a space tile on something that's already a space tile. Let's do the inverse of the last check.
+            case RcdMode.Deconstruct:
+                if (tile.Tile.IsEmpty)
+                    return false;
+
+                //They tried to decon a turf but the turf is blocked
+                if (target == null && IsTileBlocked(tile))
+                {
+                    _popup.PopupClient(Loc.GetString("rcd-component-tile-obstructed-message"), uid, user);
+                    return false;
+                }
+                //They tried to decon a non-turf but it's not in the whitelist
+                if (target != null && !_tag.HasTag(target.Value, "RCDDeconstructWhitelist"))
+                {
+                    _popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user);
+                    return false;
+                }
+
+                return true;
+            //Walls are a special behaviour, and require us to build a new object with a transform rather than setting a grid tile, thus we early return to avoid the tile set code.
+            case RcdMode.Walls:
+                if (tile.Tile.IsEmpty)
+                {
+                    _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-wall-tile-not-empty-message"), uid, user);
+                    return false;
+                }
+
+                if (IsTileBlocked(tile))
+                {
+                    _popup.PopupClient(Loc.GetString("rcd-component-tile-obstructed-message"), uid, user);
+                    return false;
+                }
+                return true;
+            case RcdMode.Airlock:
+                if (tile.Tile.IsEmpty)
+                {
+                    _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-airlock-tile-not-empty-message"), uid, user);
+                    return false;
+                }
+                if (IsTileBlocked(tile))
+                {
+                    _popup.PopupClient(Loc.GetString("rcd-component-tile-obstructed-message"), uid, user);
+                    return false;
+                }
+                return true;
+            default:
+                return false; //I don't know why this would happen, but sure I guess. Get out of here invalid state!
+        }
+    }
+
+    private void NextMode(EntityUid uid, RCDComponent comp, EntityUid user)
+    {
+        _audio.PlayPredicted(comp.SwapModeSound, uid, user);
+
+        var mode = (int) comp.Mode;
+        mode = ++mode % RcdModeCount;
+        comp.Mode = (RcdMode) mode;
+        Dirty(comp);
+
+        var msg = Loc.GetString("rcd-component-change-mode", ("mode", comp.Mode.ToString()));
+        _popup.PopupClient(msg, uid, user);
+    }
+
+    private bool IsTileBlocked(TileRef tile)
+    {
+        return _turf.IsTileBlocked(tile, CollisionGroup.MobMask);
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class RCDDoAfterEvent : DoAfterEvent
+{
+    [DataField("location", required: true)]
+    public readonly EntityCoordinates Location = default!;
+
+    [DataField("startingMode", required: true)]
+    public readonly RcdMode StartingMode = default!;
+
+    private RCDDoAfterEvent()
+    {
+    }
+
+    public RCDDoAfterEvent(EntityCoordinates location, RcdMode startingMode)
+    {
+        Location = location;
+        StartingMode = startingMode;
+    }
+
+    public override DoAfterEvent Clone() => this;
+}
index 705f8fdc5cf4f086d88492b917e41a00a27c43a1..e65a9b314766f40d1160e8ab3a65131c72295dbf 100644 (file)
@@ -1,3 +1,3 @@
-rcd-ammo-component-on-examine-text = It holds {$ammo} charges.
-rcd-ammo-component-after-interact-full-text = The RCD is full!
-rcd-ammo-component-after-interact-refilled-text = You refill the RCD.
\ No newline at end of file
+rcd-ammo-component-on-examine = It holds {$charges} charges.
+rcd-ammo-component-after-interact-full = The RCD is full!
+rcd-ammo-component-after-interact-refilled = You refill the RCD.
index a1671fa7f91c1ecc9d54dce2654bf4996983bccc..00ca3fa262b143e589e7fc9b54be22d64e6992d8 100644 (file)
@@ -2,11 +2,7 @@
 ### UI
 
 # Shown when an RCD is examined in details range
-rcd-component-examine-detail-count = It's currently on {$mode} mode, and holds {$ammoCount ->
-    *[zero] no charges.
-    [one] one charge.
-    [other] {$ammoCount} charges.
-}
+rcd-component-examine-detail = It's currently on {$mode} mode.
 
 ### Interaction Messages
 
@@ -18,4 +14,4 @@ rcd-component-tile-obstructed-message = That tile is obstructed!
 rcd-component-deconstruct-target-not-on-whitelist-message = You can't deconstruct that!
 rcd-component-cannot-build-floor-tile-not-empty-message = You can only build a floor on space!
 rcd-component-cannot-build-wall-tile-not-empty-message = You cannot build a wall on space!
-rcd-component-cannot-build-airlock-tile-not-empty-message = Cannot build an airlock on space!
\ No newline at end of file
+rcd-component-cannot-build-airlock-tile-not-empty-message = Cannot build an airlock on space!
index 9708b7f538a0da20174d289ee289c75dedcbef8f..34cb1cc3689e3933def9965987a95cc0f6e1a86c 100644 (file)
   description: An advanced construction device which can place/remove walls, floors, and airlocks quickly.
   components:
   - type: RCD
+  - type: LimitedCharges
+    maxCharges: 5
+    charges: 5
   - type: UseDelay
     delay: 1.0
   - type: Sprite
   parent: RCD
   suffix: Empty
   components:
-  - type: RCD
-    ammo: 0
+  - type: LimitedCharges
+    maxCharges: 5
+    charges: 0
+
+- type: entity
+  id: RCDExperimental
+  parent: RCD
+  suffix: Admeme
+  name: experimental rcd
+  description: A bluespace-enhanced RCD that regenerates charges passively.
+  components:
+  - type: AutoRecharge
+    rechargeDuration: 5
 
 - type: entity
   name: RCD Ammo