rcd refactor (#15172)

Co-authored-by: deltanedas <@deltanedas:kde.org>
This commit is contained in:
deltanedas
2023-05-01 13:46:59 +00:00
committed by GitHub
parent 10e3635a7b
commit ddc2785110
14 changed files with 495 additions and 355 deletions

View 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))

View 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)
{

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View 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>

View File

@@ -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

View File

@@ -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";
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View 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.
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.

View 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

View File

@@ -272,6 +272,9 @@
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
@@ -296,8 +299,19 @@
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