using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
using Robust.Shared.Utility;
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;
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))
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)
{
+++ /dev/null
-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;
- }
-}
+++ /dev/null
-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;
- }
-}
+++ /dev/null
-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;
- }
- }
-}
+++ /dev/null
-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);
- }
- }
-}
/// 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>
--- /dev/null
+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
--- /dev/null
+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";
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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;
+}
-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.
### 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
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!
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