1037 lines
40 KiB
C#
1037 lines
40 KiB
C#
using Content.Shared.Administration.Logs;
|
|
using Content.Shared.Charges.Systems;
|
|
using Content.Shared.Construction;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.DoAfter;
|
|
using Content.Shared.Examine;
|
|
using Content.Shared.Hands.EntitySystems;
|
|
using Content.Shared.Interaction;
|
|
using Content.Shared.Maps;
|
|
using Content.Shared.Physics;
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.RCD.Components;
|
|
using Content.Shared.Tag;
|
|
using Content.Shared.Tiles;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Network;
|
|
using Robust.Shared.Physics;
|
|
using Robust.Shared.Physics.Collision.Shapes;
|
|
using Robust.Shared.Physics.Dynamics;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Serialization;
|
|
using System.Linq;
|
|
using Content.Shared.Atmos.EntitySystems; //DeltaV - RPD
|
|
using Content.Shared.Atmos.Components; //DeltaV - RPD
|
|
using Content.Shared.Hands.Components; //DeltaV - RPD
|
|
using System.Numerics; //DeltaV - RPD
|
|
using Content.Shared.Verbs; //DeltaV - RPD
|
|
using Robust.Shared.Utility; //DeltaV - RPD
|
|
using Content.Shared.NodeContainer; //DeltaV - RPD
|
|
using Content.Shared.Atmos; //DeltaV - RPD
|
|
|
|
namespace Content.Shared.RCD.Systems;
|
|
|
|
public sealed class RCDSystem : EntitySystem
|
|
{
|
|
[Dependency] private readonly INetManager _net = default!;
|
|
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
|
[Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
|
|
[Dependency] private readonly FloorTileSystem _floors = default!;
|
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
|
[Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
|
|
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
|
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
|
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
|
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
|
[Dependency] private readonly TurfSystem _turf = default!;
|
|
[Dependency] private readonly TileSystem _tile = default!;
|
|
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
|
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
|
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
|
|
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
|
[Dependency] private readonly TagSystem _tags = default!;
|
|
[Dependency] private readonly SharedAtmosPipeLayersSystem _pipeLayersSystem = default!; //DeltaV - RPD
|
|
[Dependency] private readonly IEntityManager _entityManager = default!; //DeltaV - RPD
|
|
[Dependency] private readonly PipeRestrictOverlapSystem _pipeOverlap = default!; //DeltaV - RPD
|
|
|
|
private readonly int _instantConstructionDelay = 0;
|
|
private readonly EntProtoId _instantConstructionFx = "EffectRCDConstruct0";
|
|
private readonly ProtoId<RCDPrototype> _deconstructTileProto = "DeconstructTile";
|
|
private readonly ProtoId<RCDPrototype> _deconstructLatticeProto = "DeconstructLattice";
|
|
private static readonly ProtoId<TagPrototype> CatwalkTag = "Catwalk";
|
|
|
|
private HashSet<EntityUid> _intersectingEntities = new();
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
SubscribeLocalEvent<RCDComponent, MapInitEvent>(OnMapInit);
|
|
SubscribeLocalEvent<RCDComponent, ComponentStartup>(OnStartup); //DeltaV - RPD
|
|
SubscribeLocalEvent<RCDComponent, ExaminedEvent>(OnExamine);
|
|
SubscribeLocalEvent<RCDComponent, AfterInteractEvent>(OnAfterInteract);
|
|
SubscribeLocalEvent<RCDComponent, RCDDoAfterEvent>(OnDoAfter);
|
|
SubscribeLocalEvent<RCDComponent, DoAfterAttemptEvent<RCDDoAfterEvent>>(OnDoAfterAttempt);
|
|
SubscribeLocalEvent<RCDComponent, RCDSystemMessage>(OnRCDSystemMessage);
|
|
SubscribeNetworkEvent<RCDConstructionGhostRotationEvent>(OnRCDconstructionGhostRotationEvent);
|
|
SubscribeNetworkEvent<RCDConstructionGhostFlipEvent>(OnRCDConstructionGhostFlipEvent); //DeltaV - RPD
|
|
SubscribeNetworkEvent<RPDEyeRotationEvent>(OnRPDEyeRotationEvent); //DeltaV - RPD
|
|
SubscribeLocalEvent<RCDComponent, GetVerbsEvent<UtilityVerb>>(OnGetUtilityVerb); //DeltaV - RPD
|
|
SubscribeLocalEvent<RCDComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerb); //DeltaV - RPD
|
|
}
|
|
|
|
#region Event handling
|
|
|
|
private void OnMapInit(EntityUid uid, RCDComponent component, MapInitEvent args)
|
|
{
|
|
// On map startup, set the RCD to its first available recipe
|
|
if (component.AvailablePrototypes.Count > 0)
|
|
{
|
|
//DeltaV - RPD Begin
|
|
if (component.IsRpd)
|
|
component.ProtoId = "PipeStraight";
|
|
else
|
|
component.ProtoId = component.AvailablePrototypes.ElementAt(0);
|
|
//DeltaV - RPD End
|
|
Dirty(uid, component);
|
|
|
|
return;
|
|
}
|
|
|
|
// The RCD has no valid recipes somehow? Get rid of it
|
|
QueueDel(uid);
|
|
}
|
|
|
|
//DeltaV - RPD Begin
|
|
private void OnStartup(EntityUid uid, RCDComponent component, ComponentStartup args)
|
|
{
|
|
UpdateCachedPrototype(uid, component);
|
|
Dirty(uid, component);
|
|
|
|
return;
|
|
}
|
|
//DeltaV - RPD End
|
|
|
|
private void OnRCDSystemMessage(EntityUid uid, RCDComponent component, RCDSystemMessage args)
|
|
{
|
|
// Exit if the RCD doesn't actually know the supplied prototype
|
|
if (!component.AvailablePrototypes.Contains(args.ProtoId))
|
|
return;
|
|
|
|
if (!_protoManager.Resolve<RCDPrototype>(args.ProtoId, out var prototype))
|
|
return;
|
|
|
|
// Set the current RCD prototype to the one supplied
|
|
component.ProtoId = args.ProtoId;
|
|
UpdateCachedPrototype(uid, component); //DeltaV - RPD
|
|
|
|
_adminLogger.Add(LogType.RCD, LogImpact.Low, $"{args.Actor} set RCD mode to: {prototype.Mode} : {prototype.Prototype}");
|
|
|
|
Dirty(uid, component);
|
|
}
|
|
|
|
private void OnExamine(EntityUid uid, RCDComponent component, ExaminedEvent args)
|
|
{
|
|
if (!args.IsInDetailsRange)
|
|
return;
|
|
|
|
UpdateCachedPrototype(uid, component); //DeltaV - RPD
|
|
var prototype = component.CachedPrototype; //DeltaV - RPD
|
|
|
|
var msg = Loc.GetString("rcd-component-examine-mode-details", ("mode", Loc.GetString(prototype.SetName)));
|
|
|
|
if (prototype.Mode == RcdMode.ConstructTile || prototype.Mode == RcdMode.ConstructObject)
|
|
{
|
|
var name = Loc.GetString(prototype.SetName);
|
|
|
|
if (prototype.Prototype != null &&
|
|
_protoManager.TryIndex(prototype.Prototype, out var proto)) // don't use Resolve because this can be a tile
|
|
name = proto.Name;
|
|
|
|
msg = Loc.GetString("rcd-component-examine-build-details", ("name", name));
|
|
}
|
|
|
|
args.PushMarkup(msg);
|
|
//DeltaV - RPD Begin
|
|
if (component.IsRpd)
|
|
{
|
|
var modeLoc = $"rcd-rpd-mode-{component.CurrentMode.ToString().ToLowerInvariant()}";
|
|
args.PushMarkup(Loc.GetString("rcd-component-examine-rpd-mode", ("mode", Loc.GetString(modeLoc))));
|
|
}
|
|
}
|
|
|
|
private void OnRPDEyeRotationEvent(RPDEyeRotationEvent ev, EntitySessionEventArgs session)
|
|
{
|
|
var uid = GetEntity(ev.NetEntity);
|
|
|
|
if (session.SenderSession.AttachedEntity is not { } player)
|
|
return;
|
|
|
|
if (_hands.GetActiveItem(player) != uid)
|
|
return;
|
|
|
|
if (!TryComp<RCDComponent>(uid, out var rcd))
|
|
return;
|
|
|
|
// Update the layer if different
|
|
if (rcd.LastKnownEyeRotation != ev.EyeRotation)
|
|
{
|
|
rcd.LastKnownEyeRotation = ev.EyeRotation;
|
|
}
|
|
}
|
|
|
|
private void OnGetUtilityVerb(EntityUid uid, RCDComponent component, GetVerbsEvent<UtilityVerb> args)
|
|
{
|
|
if (!args.CanAccess || !args.CanInteract || !component.IsRpd)
|
|
return;
|
|
|
|
var verb = new UtilityVerb
|
|
{
|
|
Act = () => SwitchPipeMode(uid, component, args.User),
|
|
Text = Loc.GetString("rcd-verb-switch-mode"),
|
|
Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
|
|
Impact = LogImpact.Low
|
|
};
|
|
|
|
args.Verbs.Add(verb);
|
|
}
|
|
|
|
private void OnGetAlternativeVerb(EntityUid uid, RCDComponent component, GetVerbsEvent<AlternativeVerb> args)
|
|
{
|
|
if (!args.CanAccess || !args.CanInteract || !component.IsRpd || !args.Using.HasValue)
|
|
return;
|
|
|
|
// Only show when alt-clicking the RPD itself (args.Using is the held item)
|
|
if (args.Using.Value != uid)
|
|
return;
|
|
|
|
var verb = new AlternativeVerb
|
|
{
|
|
Act = () => SwitchPipeMode(uid, component, args.User),
|
|
Text = Loc.GetString("rcd-verb-switch-mode"),
|
|
Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
|
|
Impact = LogImpact.Low
|
|
};
|
|
|
|
args.Verbs.Add(verb);
|
|
}
|
|
//DeltaV - RPD End
|
|
private void OnAfterInteract(EntityUid uid, RCDComponent component, AfterInteractEvent args)
|
|
{
|
|
if (args.Handled || !args.CanReach)
|
|
return;
|
|
|
|
var user = args.User;
|
|
var location = args.ClickLocation;
|
|
|
|
var prototype = component.CachedPrototype; //DeltaV - RPD
|
|
|
|
// Initial validity checks
|
|
if (!location.IsValid(EntityManager))
|
|
return;
|
|
|
|
var gridUid = _transform.GetGrid(location);
|
|
|
|
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
|
|
{
|
|
_popup.PopupClient(Loc.GetString("rcd-component-no-valid-grid"), uid, user);
|
|
return;
|
|
}
|
|
var tile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location);
|
|
var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location);
|
|
//DeltaV - RPD Begin
|
|
var layer = AtmosPipeLayer.Primary;
|
|
if (component.IsRpd && prototype.HasLayers)
|
|
{
|
|
var tileSize = mapGrid.TileSize;
|
|
var tileCenter = new Vector2(tile.X + tileSize / 2, tile.Y + tileSize / 2);
|
|
var mouseCoordsDiff = args.ClickLocation.Position - tileCenter - new Vector2(0.5f, 0.5f);
|
|
var mouseDeadzoneRadius = 0.25f;
|
|
|
|
switch (component.CurrentMode)
|
|
{
|
|
case RpdMode.Primary:
|
|
layer = AtmosPipeLayer.Primary;
|
|
break;
|
|
|
|
case RpdMode.Secondary:
|
|
layer = AtmosPipeLayer.Secondary;
|
|
break;
|
|
|
|
case RpdMode.Tertiary:
|
|
layer = AtmosPipeLayer.Tertiary;
|
|
break;
|
|
|
|
case RpdMode.Free:
|
|
// Only use mouse direction in Free mode
|
|
if (mouseCoordsDiff.Length() > mouseDeadzoneRadius && component.LastKnownEyeRotation.HasValue)
|
|
{
|
|
var gridRotation = _transform.GetWorldRotation(gridUid.Value);
|
|
var angle = new Angle(mouseCoordsDiff);
|
|
var eyeRotation = new Angle(component.LastKnownEyeRotation.Value);
|
|
var direction = (angle + eyeRotation + gridRotation + Math.PI / 2).GetCardinalDir();
|
|
|
|
layer = (direction == Direction.North || direction == Direction.East)
|
|
? AtmosPipeLayer.Secondary
|
|
: AtmosPipeLayer.Tertiary;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
//DeltaV - RPD End
|
|
|
|
if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, component.ConstructionDirection, args.Target, args.User))
|
|
return;
|
|
|
|
if (!_net.IsServer)
|
|
return;
|
|
|
|
// Get the starting cost, delay, and effect from the prototype
|
|
var cost = prototype.Cost;
|
|
var delay = prototype.Delay;
|
|
var effectPrototype = prototype.Effect;
|
|
|
|
#region: Operation modifiers
|
|
|
|
// Deconstruction modifiers
|
|
switch (prototype.Mode)
|
|
{
|
|
case RcdMode.Deconstruct:
|
|
|
|
// Deconstructing an object
|
|
if (args.Target != null)
|
|
{
|
|
if (TryComp<RCDDeconstructableComponent>(args.Target, out var destructible))
|
|
{
|
|
cost = destructible.Cost;
|
|
delay = destructible.Delay;
|
|
effectPrototype = destructible.Effect;
|
|
}
|
|
}
|
|
|
|
// Deconstructing a tile
|
|
else
|
|
{
|
|
var deconstructedTile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location);
|
|
var protoName = !_turf.IsSpace(deconstructedTile) ? _deconstructTileProto : _deconstructLatticeProto;
|
|
|
|
if (_protoManager.Resolve(protoName, out var deconProto))
|
|
{
|
|
cost = deconProto.Cost;
|
|
delay = deconProto.Delay;
|
|
effectPrototype = deconProto.Effect;
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case RcdMode.ConstructTile:
|
|
|
|
// If replacing a tile, make the construction instant
|
|
var contructedTile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location);
|
|
|
|
if (!contructedTile.Tile.IsEmpty)
|
|
{
|
|
delay = _instantConstructionDelay;
|
|
effectPrototype = _instantConstructionFx;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
#endregion
|
|
|
|
// Try to start the do after
|
|
var effect = Spawn(effectPrototype, location);
|
|
var ev = new RCDDoAfterEvent(GetNetCoordinates(location), component.ConstructionDirection, component.ProtoId, cost, layer, GetNetEntity(effect)); //DeltaV - RPD
|
|
|
|
var doAfterArgs = new DoAfterArgs(EntityManager, user, delay, ev, uid, target: args.Target, used: uid)
|
|
{
|
|
BreakOnDamage = true,
|
|
BreakOnHandChange = true,
|
|
BreakOnMove = true,
|
|
AttemptFrequency = AttemptFrequency.EveryTick,
|
|
CancelDuplicate = false,
|
|
BlockDuplicate = false
|
|
};
|
|
|
|
args.Handled = true;
|
|
|
|
if (!_doAfter.TryStartDoAfter(doAfterArgs))
|
|
QueueDel(effect);
|
|
}
|
|
|
|
private void OnDoAfterAttempt(EntityUid uid, RCDComponent component, DoAfterAttemptEvent<RCDDoAfterEvent> args)
|
|
{
|
|
if (args.Event?.DoAfter?.Args == null)
|
|
return;
|
|
|
|
// Exit if the RCD prototype has changed
|
|
if (component.ProtoId != args.Event.StartingProtoId)
|
|
{
|
|
args.Cancel();
|
|
return;
|
|
}
|
|
|
|
// Ensure the RCD operation is still valid
|
|
var location = GetCoordinates(args.Event.Location);
|
|
|
|
var gridUid = _transform.GetGrid(location);
|
|
|
|
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
|
|
{
|
|
args.Cancel();
|
|
return;
|
|
}
|
|
|
|
|
|
var tile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location);
|
|
var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location);
|
|
|
|
if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, args.Event.Direction, args.Event.Target, args.Event.User))
|
|
args.Cancel();
|
|
}
|
|
|
|
private void OnDoAfter(EntityUid uid, RCDComponent component, RCDDoAfterEvent args)
|
|
{
|
|
if (args.Cancelled)
|
|
{
|
|
// Delete the effect entity if the do-after was cancelled (server-side only)
|
|
if (_net.IsServer)
|
|
QueueDel(GetEntity(args.Effect));
|
|
return;
|
|
}
|
|
|
|
if (args.Handled)
|
|
return;
|
|
|
|
args.Handled = true;
|
|
|
|
var location = GetCoordinates(args.Location);
|
|
|
|
var gridUid = _transform.GetGrid(location);
|
|
|
|
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
|
|
return;
|
|
|
|
var tile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location);
|
|
var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location);
|
|
|
|
// Ensure the RCD operation is still valid
|
|
if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, args.Direction, args.Target, args.User))
|
|
return;
|
|
|
|
// Finalize the operation (this should handle prediction properly)
|
|
FinalizeRCDOperation(uid, component, gridUid.Value, mapGrid, tile, position, args.Direction, args.Target, args.User, args.Layer);
|
|
|
|
// Play audio and consume charges
|
|
_audio.PlayPredicted(component.SuccessSound, uid, args.User);
|
|
_sharedCharges.AddCharges(uid, -args.Cost);
|
|
}
|
|
|
|
private void OnRCDconstructionGhostRotationEvent(RCDConstructionGhostRotationEvent ev, EntitySessionEventArgs session)
|
|
{
|
|
var uid = GetEntity(ev.NetEntity);
|
|
|
|
// Determine if player that send the message is carrying the specified RCD in their active hand
|
|
if (session.SenderSession.AttachedEntity is not { } player)
|
|
return;
|
|
|
|
if (_hands.GetActiveItem(player) != uid)
|
|
return;
|
|
|
|
if (!TryComp<RCDComponent>(uid, out var rcd))
|
|
return;
|
|
|
|
// Update the construction direction
|
|
rcd.ConstructionDirection = ev.Direction;
|
|
Dirty(uid, rcd);
|
|
}
|
|
//DeltaV - RPD Begin
|
|
private void OnRCDConstructionGhostFlipEvent(RCDConstructionGhostFlipEvent ev, EntitySessionEventArgs session)
|
|
{
|
|
var uid = GetEntity(ev.NetEntity);
|
|
|
|
if (session.SenderSession.AttachedEntity is not { } player)
|
|
return;
|
|
|
|
if (_hands.GetActiveItem(player) != uid)
|
|
return;
|
|
|
|
if (!TryComp<RCDComponent>(uid, out var rcd))
|
|
return;
|
|
|
|
rcd.UseMirrorPrototype = ev.UseMirrorPrototype;
|
|
Dirty(uid, rcd);
|
|
}
|
|
|
|
private void SwitchPipeMode(EntityUid uid, RCDComponent component, EntityUid? user = null)
|
|
{
|
|
if (!component.IsRpd)
|
|
return;
|
|
|
|
// Cycle through modes
|
|
component.CurrentMode = component.CurrentMode switch
|
|
{
|
|
RpdMode.Primary => RpdMode.Secondary,
|
|
RpdMode.Secondary => RpdMode.Tertiary,
|
|
RpdMode.Tertiary => RpdMode.Free,
|
|
RpdMode.Free => RpdMode.Primary,
|
|
_ => RpdMode.Free
|
|
};
|
|
|
|
Dirty(uid, component);
|
|
|
|
if (user != null)
|
|
_audio.PlayPredicted(component.SoundSwitchMode, uid, user.Value);
|
|
}
|
|
//DeltaV - RPD End
|
|
#endregion
|
|
|
|
#region Entity construction/deconstruction rule checks
|
|
|
|
public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, EntityUid? target, EntityUid user, bool popMsgs = true)
|
|
{
|
|
return IsRCDOperationStillValid(uid, component, gridUid, mapGrid, tile, position, component.ConstructionDirection, target, user, popMsgs);
|
|
}
|
|
|
|
public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, Direction direction, EntityUid? target, EntityUid user, bool popMsgs = true)
|
|
{
|
|
// Update cached prototype if required
|
|
UpdateCachedPrototype(uid, component); //DeltaV - RPD
|
|
|
|
var prototype = component.CachedPrototype; //DeltaV - RPD
|
|
|
|
// Check that the RCD has enough ammo to get the job done
|
|
var charges = _sharedCharges.GetCurrentCharges(uid);
|
|
|
|
// Both of these were messages were suppose to be predicted, but HasInsufficientCharges wasn't being checked on the client for some reason?
|
|
if (charges == 0)
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-no-ammo-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
|
|
if (prototype.Cost > charges)
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-insufficient-ammo-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
|
|
// DeltaV - RPD Begin
|
|
bool unobstructed;
|
|
var worldPos = _mapSystem.GridTileToWorld(gridUid, mapGrid, position);
|
|
|
|
if (component.IsRpd)
|
|
{
|
|
unobstructed = _interaction.InRangeUnobstructed(user, worldPos,
|
|
predicate: (entity) => !HasComp<PipeRestrictOverlapComponent>(entity),
|
|
popup: popMsgs);
|
|
}
|
|
else
|
|
{
|
|
unobstructed = (target == null)
|
|
? _interaction.InRangeUnobstructed(user, worldPos, popup: popMsgs)
|
|
: _interaction.InRangeUnobstructed(user, target.Value, popup: popMsgs);
|
|
}
|
|
// DeltaV - RPD End
|
|
|
|
if (!unobstructed)
|
|
return false;
|
|
|
|
// DeltaV - RPD Begin
|
|
var layer = AtmosPipeLayer.Primary;
|
|
if (component.IsRpd)
|
|
{
|
|
layer = GetLayerFromState(component, gridUid, mapGrid, tile, worldPos.Position);
|
|
}
|
|
// DeltaV - RPD End
|
|
|
|
// Return whether the operation location is valid
|
|
switch (prototype.Mode)
|
|
{
|
|
case RcdMode.ConstructTile:
|
|
case RcdMode.ConstructObject:
|
|
return IsConstructionLocationValid(uid, component, gridUid, mapGrid, tile, position, direction, user, layer, popMsgs); //DeltaV - RPD
|
|
case RcdMode.Deconstruct:
|
|
return IsDeconstructionStillValid(uid, component, tile, target, user, popMsgs); //DeltaV - RPD
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// DeltaV - RPD Begin
|
|
private AtmosPipeLayer GetLayerFromState(RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2 clickPos)
|
|
{
|
|
if (!component.IsRpd || component.CachedPrototype == null || !component.CachedPrototype.HasLayers)
|
|
return AtmosPipeLayer.Primary;
|
|
|
|
var tileSize = mapGrid.TileSize;
|
|
var tileCenter = new Vector2(tile.X + tileSize / 2f, tile.Y + tileSize / 2f);
|
|
var mouseCoordsDiff = clickPos - tileCenter;
|
|
var mouseDeadzoneRadius = 0.25f;
|
|
|
|
switch (component.CurrentMode)
|
|
{
|
|
case RpdMode.Primary: return AtmosPipeLayer.Primary;
|
|
case RpdMode.Secondary: return AtmosPipeLayer.Secondary;
|
|
case RpdMode.Tertiary: return AtmosPipeLayer.Tertiary;
|
|
case RpdMode.Free:
|
|
if (mouseCoordsDiff.Length() > mouseDeadzoneRadius && component.LastKnownEyeRotation.HasValue)
|
|
{
|
|
var gridRotation = _transform.GetWorldRotation(gridUid);
|
|
var angle = new Angle(mouseCoordsDiff);
|
|
var eyeRotation = new Angle(component.LastKnownEyeRotation.Value);
|
|
var direction = (angle + eyeRotation + gridRotation + Math.PI / 2).GetCardinalDir();
|
|
return (direction == Direction.North || direction == Direction.East) ? AtmosPipeLayer.Secondary : AtmosPipeLayer.Tertiary;
|
|
}
|
|
return AtmosPipeLayer.Primary;
|
|
default: return AtmosPipeLayer.Primary;
|
|
}
|
|
}
|
|
|
|
// DeltaV - RPD End
|
|
|
|
private bool IsConstructionLocationValid(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, Direction direction, EntityUid user, AtmosPipeLayer layer, bool popMsgs = true)
|
|
{
|
|
// Update cached prototype if required
|
|
UpdateCachedPrototype(uid, component); //DeltaV - RPD
|
|
|
|
var prototype = component.CachedPrototype; //DeltaV - RPD
|
|
|
|
// Check rule: Must build on empty tile
|
|
if (prototype.ConstructionRules.Contains(RcdConstructionRule.MustBuildOnEmptyTile) && !tile.Tile.IsEmpty)
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-must-build-on-empty-tile-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Check rule: Must build on non-empty tile
|
|
if (!prototype.ConstructionRules.Contains(RcdConstructionRule.CanBuildOnEmptyTile) && tile.Tile.IsEmpty)
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-on-empty-tile-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Check rule: Must place on subfloor
|
|
if (prototype.ConstructionRules.Contains(RcdConstructionRule.MustBuildOnSubfloor) && !_turf.GetContentTileDefinition(tile).IsSubFloor)
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-must-build-on-subfloor-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Tile specific rules
|
|
if (prototype.Mode == RcdMode.ConstructTile)
|
|
{
|
|
// Check rule: Tile placement is valid
|
|
if (!_floors.CanPlaceTile(gridUid, mapGrid, tile.GridIndices, out var reason))
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(reason, uid, user);
|
|
|
|
return false;
|
|
}
|
|
|
|
var tileDef = _turf.GetContentTileDefinition(tile);
|
|
|
|
// Check rule: Respect baseTurf and baseWhitelist
|
|
if (prototype.Prototype != null && _tileDefMan.TryGetDefinition(prototype.Prototype, out var replacementDef))
|
|
{
|
|
var replacementContentDef = (ContentTileDefinition) replacementDef;
|
|
|
|
if (replacementContentDef.BaseTurf != tileDef.ID && !replacementContentDef.BaseWhitelist.Contains(tileDef.ID))
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-on-empty-tile-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check rule: Tiles can't be identical
|
|
if (tileDef.ID == prototype.Prototype)
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-identical-tile"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Ensure that all construction rules shared between tiles and object are checked before exiting here
|
|
return true;
|
|
}
|
|
|
|
// Entity specific rules
|
|
|
|
// Check rule: The tile is unoccupied
|
|
var isWindow = prototype.ConstructionRules.Contains(RcdConstructionRule.IsWindow);
|
|
var isCatwalk = prototype.ConstructionRules.Contains(RcdConstructionRule.IsCatwalk);
|
|
|
|
_intersectingEntities.Clear();
|
|
_lookup.GetLocalEntitiesIntersecting(gridUid, position, _intersectingEntities, -0.05f, LookupFlags.Uncontained);
|
|
|
|
foreach (var ent in _intersectingEntities)
|
|
{
|
|
// If the entity is the exact same prototype as what we are trying to build, then block it.
|
|
// This is to prevent spamming objects on the same tile (e.g. lights)
|
|
if (prototype.Prototype != null && MetaData(ent).EntityPrototype?.ID == prototype.Prototype)
|
|
{
|
|
// DeltaV - RPD Begin
|
|
var isIdentical = true;
|
|
|
|
if (component.IsRpd)
|
|
{
|
|
// A pipe is only "identical" if it exists on the same layer we are trying to build on.
|
|
// We check the nodes of the existing entity to find its layer.
|
|
if (TryComp<NodeContainerComponent>(ent, out var nodeContainer))
|
|
{
|
|
var existingLayerMatch = false;
|
|
foreach (var node in nodeContainer.Nodes.Values)
|
|
{
|
|
if (node is IPipeNode pipeNode && pipeNode.Layer == layer)
|
|
{
|
|
existingLayerMatch = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no nodes match the target layer, it's not identical (we can build here!)
|
|
if (!existingLayerMatch)
|
|
isIdentical = false;
|
|
}
|
|
}
|
|
|
|
else if (prototype.AllowMultiDirection)
|
|
{
|
|
var entDirection = Transform(ent).LocalRotation.GetCardinalDir();
|
|
if (entDirection != direction)
|
|
isIdentical = false;
|
|
}
|
|
|
|
if (isIdentical)
|
|
{
|
|
if (popMsgs) _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-identical-entity"), uid, user);
|
|
return false;
|
|
}
|
|
// DeltaV - RPD End
|
|
}
|
|
|
|
if (isWindow && HasComp<SharedCanBuildWindowOnTopComponent>(ent))
|
|
continue;
|
|
|
|
if (isCatwalk && _tags.HasTag(ent, CatwalkTag))
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-on-occupied-tile-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
|
|
// DeltaV - RPD Begin
|
|
if (component.IsRpd && HasComp<PipeRestrictOverlapComponent>(ent))
|
|
{
|
|
continue;
|
|
}
|
|
// DeltaV - RPD End
|
|
|
|
if (prototype.CollisionMask != CollisionGroup.None && TryComp<FixturesComponent>(ent, out var fixtures))
|
|
{
|
|
foreach (var fixture in fixtures.Fixtures.Values)
|
|
{
|
|
// Continue if no collision is possible
|
|
if (!fixture.Hard || fixture.CollisionLayer <= 0 || (fixture.CollisionLayer & (int) prototype.CollisionMask) == 0)
|
|
continue;
|
|
|
|
// Continue if our custom collision bounds are not intersected
|
|
if (prototype.CollisionPolygon != null &&
|
|
!DoesCustomBoundsIntersectWithFixture(prototype.CollisionPolygon, component.ConstructionTransform, ent, fixture))
|
|
continue;
|
|
|
|
// Collision was detected
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-on-occupied-tile-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool IsDeconstructionStillValid(EntityUid uid, RCDComponent component, TileRef tile, EntityUid? target, EntityUid user, bool popMsgs = true) //DeltaV - RPD
|
|
{
|
|
// Attempt to deconstruct a floor tile
|
|
if (target == null)
|
|
{
|
|
//DeltaV - RPD Begin
|
|
if (component.IsRpd)
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
//DeltaV - RPD End
|
|
// The tile is empty
|
|
if (tile.Tile.IsEmpty)
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-nothing-to-deconstruct-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
|
|
// The tile has a structure sitting on it
|
|
if (_turf.IsTileBlocked(tile, CollisionGroup.MobMask))
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-tile-obstructed-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
|
|
// The tile cannot be destroyed
|
|
var tileDef = _turf.GetContentTileDefinition(tile);
|
|
|
|
if (tileDef.Indestructible)
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-tile-indestructible-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Attempt to deconstruct an object
|
|
else
|
|
{
|
|
//DeltaV - RPD Begin
|
|
// The object is not in the RPD whitelist
|
|
if (!TryComp<RCDDeconstructableComponent>(target, out var deconstructible) || !deconstructible.RpdDeconstructable && component.IsRpd)
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
//DeltaV - RPD End
|
|
|
|
// The object is not in the whitelist
|
|
if (!deconstructible.Deconstructable) //DeltaV - RPD
|
|
{
|
|
if (popMsgs)
|
|
_popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user);
|
|
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Entity construction/deconstruction
|
|
|
|
private void FinalizeRCDOperation(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, Direction direction, EntityUid? target, EntityUid user, AtmosPipeLayer layer) // DeltaV - RPD
|
|
{
|
|
if (!_net.IsServer)
|
|
return;
|
|
|
|
var prototype = component.CachedPrototype; //DeltaV - RPD
|
|
|
|
if (prototype.Prototype == null)
|
|
return;
|
|
|
|
switch (prototype.Mode)
|
|
{
|
|
case RcdMode.ConstructTile:
|
|
if (!_tileDefMan.TryGetDefinition(prototype.Prototype, out var tileDef))
|
|
return;
|
|
|
|
_tile.ReplaceTile(tile, (ContentTileDefinition) tileDef, gridUid, mapGrid);
|
|
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to set grid: {gridUid} {position} to {prototype.Prototype}");
|
|
break;
|
|
|
|
case RcdMode.ConstructObject:
|
|
//DeltaV - RPD Begin
|
|
var proto = (component.UseMirrorPrototype && !string.IsNullOrEmpty(prototype.MirrorPrototype))
|
|
? prototype.MirrorPrototype
|
|
: prototype.Prototype;
|
|
|
|
if (component.IsRpd && prototype.HasLayers)
|
|
{
|
|
if (_protoManager.TryIndex<EntityPrototype>(proto, out var entityProto) &&
|
|
entityProto.TryGetComponent<AtmosPipeLayersComponent>(out var atmosPipeLayers, _entityManager.ComponentFactory) &&
|
|
_pipeLayersSystem.TryGetAlternativePrototype(atmosPipeLayers, layer, out var newProtoId))
|
|
{
|
|
proto = newProtoId;
|
|
}
|
|
}
|
|
|
|
// Calculate rotation before spawn
|
|
var rotation = prototype.Rotation switch
|
|
{
|
|
RcdRotation.Fixed => Angle.Zero,
|
|
RcdRotation.Camera => Transform(uid).LocalRotation,
|
|
RcdRotation.User => direction.ToAngle(),
|
|
_ => Angle.Zero // Fallback
|
|
};
|
|
|
|
// For RPD's, if overlapping existing pipe, replace the pipe
|
|
if (component.IsRpd)
|
|
{
|
|
// We need to know what the pipe *would* look like to check for overlaps
|
|
if (_protoManager.TryIndex<EntityPrototype>(proto, out var pipeProto) &&
|
|
pipeProto.TryGetComponent<NodeContainerComponent>(out var nodeContainer, _entityManager.ComponentFactory))
|
|
{
|
|
// Check every node in the prototype to see if it overlaps something on the grid
|
|
foreach (var node in nodeContainer.Nodes.Values)
|
|
{
|
|
if (node is IPipeNode pipeNode)
|
|
{
|
|
var proposed = new PipeRestrictOverlapSystem.ProposedPipe(
|
|
pipeNode.Direction,
|
|
layer,
|
|
rotation
|
|
);
|
|
|
|
// If there is a conflict, delete the old pipe first
|
|
var conflict = _pipeOverlap.CheckIfWouldConflict(gridUid, position, proposed);
|
|
if (Exists(conflict) && HasComp<RCDDeconstructableComponent>(conflict))
|
|
{
|
|
_adminLogger.Add(LogType.RCD, LogImpact.Medium,
|
|
$"{ToPrettyString(user):user} RPD replaced {ToPrettyString(conflict.Value)} at {position}");
|
|
Del(conflict.Value);
|
|
_audio.PlayPvs(component.SuccessSound, uid);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var entityCoords = _mapSystem.GridTileToLocal(gridUid, mapGrid, position);
|
|
var mapCoords = new MapCoordinates(entityCoords.ToMapPos(EntityManager, _transform), entityCoords.GetMapId(EntityManager));
|
|
|
|
var ent = Spawn(proto, mapCoords, rotation: rotation);
|
|
//DeltaV - RPD End
|
|
switch (prototype.Rotation)
|
|
{
|
|
case RcdRotation.Fixed:
|
|
Transform(ent).LocalRotation = Angle.Zero;
|
|
break;
|
|
case RcdRotation.Camera:
|
|
Transform(ent).LocalRotation = Transform(uid).LocalRotation;
|
|
break;
|
|
case RcdRotation.User:
|
|
Transform(ent).LocalRotation = direction.ToAngle();
|
|
break;
|
|
}
|
|
|
|
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to spawn {ToPrettyString(ent)} at {position} on grid {gridUid}");
|
|
break;
|
|
|
|
case RcdMode.Deconstruct:
|
|
|
|
if (target == null)
|
|
{
|
|
// Deconstruct tile, don't drop tile as item
|
|
if (_tile.DeconstructTile(tile, spawnItem: false))
|
|
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to set grid: {gridUid} tile: {position} open to space");
|
|
}
|
|
else
|
|
{
|
|
// Deconstruct object
|
|
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to delete {ToPrettyString(target):target}");
|
|
QueueDel(target);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Utility functions
|
|
|
|
private bool DoesCustomBoundsIntersectWithFixture(PolygonShape boundingPolygon, Transform boundingTransform, EntityUid fixtureOwner, Fixture fixture)
|
|
{
|
|
var entXformComp = Transform(fixtureOwner);
|
|
var entXform = new Transform(new(), entXformComp.LocalRotation);
|
|
|
|
return boundingPolygon.ComputeAABB(boundingTransform, 0).Intersects(fixture.Shape.ComputeAABB(entXform, 0));
|
|
}
|
|
//DeltaV - RPD Begin
|
|
public void UpdateCachedPrototype(EntityUid uid, RCDComponent component)
|
|
{
|
|
if (component.ProtoId.Id != component.CachedPrototype?.Prototype ||
|
|
(component.CachedPrototype?.MirrorPrototype != null &&
|
|
component.ProtoId.Id != component.CachedPrototype?.MirrorPrototype))
|
|
{
|
|
component.CachedPrototype = _protoManager.Index(component.ProtoId);
|
|
}
|
|
}
|
|
|
|
public RpdMode GetCurrentRpdMode(EntityUid uid, RCDComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return RpdMode.Free; // default to Free mode
|
|
|
|
return component.CurrentMode;
|
|
}
|
|
//DeltaV - RPD End
|
|
#endregion
|
|
}
|
|
|
|
[Serializable, NetSerializable]
|
|
public sealed partial class RCDDoAfterEvent : DoAfterEvent
|
|
{
|
|
[DataField(required: true)]
|
|
public NetCoordinates Location { get; private set; }
|
|
|
|
[DataField]
|
|
public Direction Direction { get; private set; }
|
|
|
|
[DataField]
|
|
public ProtoId<RCDPrototype> StartingProtoId { get; private set; }
|
|
|
|
[DataField]
|
|
public int Cost { get; private set; } = 1;
|
|
|
|
[DataField("fx")]
|
|
public NetEntity? Effect { get; private set; }
|
|
|
|
[DataField] //DeltaV - RPD
|
|
public AtmosPipeLayer Layer { get; private set; } = AtmosPipeLayer.Primary; //DeltaV - RPD
|
|
|
|
private RCDDoAfterEvent() { }
|
|
|
|
public RCDDoAfterEvent(NetCoordinates location, Direction direction, ProtoId<RCDPrototype> startingProtoId, int cost, AtmosPipeLayer layer, NetEntity? effect = null) //DeltaV - RPD
|
|
{
|
|
Location = location;
|
|
Direction = direction;
|
|
StartingProtoId = startingProtoId;
|
|
Cost = cost;
|
|
Layer = layer; //DeltaV - RPD
|
|
Effect = effect;
|
|
}
|
|
|
|
public override DoAfterEvent Clone()
|
|
{
|
|
return this;
|
|
}
|
|
}
|