Improved RCDs (#22799)
* Initial radial menu prototyping for the RCD * Radial UI buttons can send messages to the server * Beginning to update RCDSystem * RCD building system in progress * Further updates * Added extra effects, RCDSystem now reads RCD prototype data * Replacing tiles is instant, multiple constructions are allowed, deconstruction is broken * Added extra functionality to RadialContainers plus documentation * Fixed localization of RCD UI strings * Menu opens near cursor, added basic RCD * Avoiding merge conflict * Implemented atomized construction / deconstruction rules * Increased RCD ammo base charges * Moved input context definition to content * Removed obsoleted code * Updates to system * Switch machine and computer frames for electrical cabling * Added construction ghosts * Fixed issue with keybind detection code * Fixed RCD construction ghost mispredications * Code clean up * Updated deconstruction effects * RCDs effects don't rotate * Code clean up * Balancing for ammo counts * Code clean up * Added missing localized strings * More clean up * Made directional window handling more robust * Added documentation to radial menus and made them no longer dependent on Content * Made radial containers more robust * Further robustness to the radial menu * The RCD submenu buttons are only shown when the destination layer has at least one children * Expanded upon deconstructing plus construction balance * Fixed line endings * Updated list of RCD deconstructable entities. Now needs a component to deconstruct instead of a tag * Bug fixes * Revert unnecessary change * Updated RCD strings * Fixed bug * More fixes * Deconstructed tiles/subflooring convert to lattice instead * Fixed failed tests (Linux doesn't like invalid spritespecifer paths) * Fixing merge conflict * Updated airlock assembly * Fixing merge conflict * Fixing merge conflict * More fixing... * Removed erroneous project file change * Fixed string handling issue * Trying to fix merge conflict * Still fixing merge conflicts * Balancing * Hidden RCD construction ghosts when in 'build' mode * Fixing merge conflict * Implemented requested changes (Part 1) * Added more requested changes * Fix for failed test. Removed sussy null suppression * Made requested changes - custom construction ghost system was replaced * Fixing merge conflict * Fixed merge conflict * Fixed bug in RCD construction ghost validation * Fixing merge conflict * Merge conflict fixed * Made required update * Removed lingering RCD deconstruct tag * Fixing merge conflict * Merge conflict fixed * Made requested changes * Bug fixes and balancing * Made string names more consistent * Can no longer stack catwalks
This commit is contained in:
@@ -1,28 +1,35 @@
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Charges.Components;
|
||||
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.Components;
|
||||
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 Content.Shared.Tiles;
|
||||
using Robust.Shared.Audio;
|
||||
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 Robust.Shared.Timing;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Shared.RCD.Systems;
|
||||
|
||||
public sealed class RCDSystem : EntitySystem
|
||||
[Virtual]
|
||||
public class RCDSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
@@ -34,312 +41,599 @@ public sealed class RCDSystem : EntitySystem
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
[Dependency] private readonly TurfSystem _turf = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
|
||||
[Dependency] private readonly TagSystem _tags = default!;
|
||||
|
||||
private readonly int _rcdModeCount = Enum.GetValues(typeof(RcdMode)).Length;
|
||||
private readonly int _instantConstructionDelay = 0;
|
||||
private readonly EntProtoId _instantConstructionFx = "EffectRCDConstruct0";
|
||||
private readonly ProtoId<RCDPrototype> _deconstructTileProto = "DeconstructTile";
|
||||
private readonly ProtoId<RCDPrototype> _deconstructLatticeProto = "DeconstructLattice";
|
||||
|
||||
private HashSet<EntityUid> _intersectingEntities = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RCDComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<RCDComponent, ExaminedEvent>(OnExamine);
|
||||
SubscribeLocalEvent<RCDComponent, UseInHandEvent>(OnUseInHand);
|
||||
SubscribeLocalEvent<RCDComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<RCDComponent, RCDDoAfterEvent>(OnDoAfter);
|
||||
SubscribeLocalEvent<RCDComponent, DoAfterAttemptEvent<RCDDoAfterEvent>>(OnDoAfterAttempt);
|
||||
SubscribeLocalEvent<RCDComponent, RCDSystemMessage>(OnRCDSystemMessage);
|
||||
SubscribeNetworkEvent<RCDConstructionGhostRotationEvent>(OnRCDconstructionGhostRotationEvent);
|
||||
}
|
||||
|
||||
private void OnExamine(EntityUid uid, RCDComponent comp, ExaminedEvent args)
|
||||
#region Event handling
|
||||
|
||||
private void OnMapInit(EntityUid uid, RCDComponent component, MapInitEvent args)
|
||||
{
|
||||
// On init, set the RCD to its first available recipe
|
||||
if (component.AvailablePrototypes.Any())
|
||||
{
|
||||
component.ProtoId = component.AvailablePrototypes.First();
|
||||
UpdateCachedPrototype(uid, component);
|
||||
Dirty(uid, component);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// The RCD has no valid recipes somehow? Get rid of it
|
||||
QueueDel(uid);
|
||||
}
|
||||
|
||||
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.HasIndex(args.ProtoId))
|
||||
return;
|
||||
|
||||
// Set the current RCD prototype to the one supplied
|
||||
component.ProtoId = args.ProtoId;
|
||||
UpdateCachedPrototype(uid, component);
|
||||
Dirty(uid, component);
|
||||
|
||||
if (args.Session.AttachedEntity != null)
|
||||
{
|
||||
// Popup message
|
||||
var msg = (component.CachedPrototype.Prototype != null) ?
|
||||
Loc.GetString("rcd-component-change-build-mode", ("name", Loc.GetString(component.CachedPrototype.SetName))) :
|
||||
Loc.GetString("rcd-component-change-mode", ("mode", Loc.GetString(component.CachedPrototype.SetName)));
|
||||
|
||||
_popup.PopupClient(msg, uid, args.Session.AttachedEntity.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnExamine(EntityUid uid, RCDComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange)
|
||||
return;
|
||||
|
||||
var msg = Loc.GetString("rcd-component-examine-detail", ("mode", comp.Mode));
|
||||
// Update cached prototype if required
|
||||
UpdateCachedPrototype(uid, component);
|
||||
|
||||
var msg = (component.CachedPrototype.Prototype != null) ?
|
||||
Loc.GetString("rcd-component-examine-build-details", ("name", Loc.GetString(component.CachedPrototype.SetName))) :
|
||||
Loc.GetString("rcd-component-examine-mode-details", ("mode", Loc.GetString(component.CachedPrototype.SetName)));
|
||||
|
||||
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)
|
||||
private void OnAfterInteract(EntityUid uid, RCDComponent component, 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
|
||||
|
||||
// Initial validity checks
|
||||
if (!location.IsValid(EntityManager))
|
||||
return;
|
||||
|
||||
var gridId = location.GetGridUid(EntityManager);
|
||||
if (!HasComp<MapGridComponent>(gridId))
|
||||
if (!TryGetMapGridData(location, out var mapGridData))
|
||||
{
|
||||
location = location.AlignWithClosestGridTile();
|
||||
gridId = location.GetGridUid(EntityManager);
|
||||
// Check if fixing it failed / get final grid ID
|
||||
if (!HasComp<MapGridComponent>(gridId))
|
||||
return;
|
||||
_popup.PopupClient(Loc.GetString("rcd-component-no-valid-grid"), uid, user);
|
||||
return;
|
||||
}
|
||||
|
||||
var doAfterArgs = new DoAfterArgs(EntityManager, user, comp.Delay, new RCDDoAfterEvent(GetNetCoordinates(location), comp.Mode), uid, target: args.Target, used: uid)
|
||||
if (!IsRCDOperationStillValid(uid, component, mapGridData.Value, args.Target, args.User))
|
||||
return;
|
||||
|
||||
if (!_net.IsServer)
|
||||
return;
|
||||
|
||||
// Get the starting cost, delay, and effect from the prototype
|
||||
var cost = component.CachedPrototype.Cost;
|
||||
var delay = component.CachedPrototype.Delay;
|
||||
var effectPrototype = component.CachedPrototype.Effect;
|
||||
|
||||
#region: Operation modifiers
|
||||
|
||||
// Deconstruction modifiers
|
||||
switch (component.CachedPrototype.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(mapGridData.Value.GridUid, mapGridData.Value.Component, mapGridData.Value.Location);
|
||||
var protoName = deconstructedTile.IsSpace() ? _deconstructTileProto : _deconstructLatticeProto;
|
||||
|
||||
if (_protoManager.TryIndex(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(mapGridData.Value.GridUid, mapGridData.Value.Component, mapGridData.Value.Location);
|
||||
|
||||
if (!contructedTile.Tile.IsEmpty)
|
||||
{
|
||||
delay = _instantConstructionDelay;
|
||||
effectPrototype = _instantConstructionFx;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Try to start the do after
|
||||
var effect = Spawn(effectPrototype, mapGridData.Value.Location);
|
||||
var ev = new RCDDoAfterEvent(GetNetCoordinates(mapGridData.Value.Location), component.ProtoId, cost, EntityManager.GetNetEntity(effect));
|
||||
|
||||
var doAfterArgs = new DoAfterArgs(EntityManager, user, delay, ev, uid, target: args.Target, used: uid)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
NeedHand = true,
|
||||
BreakOnHandChange = true,
|
||||
BreakOnMove = true,
|
||||
AttemptFrequency = AttemptFrequency.EveryTick
|
||||
AttemptFrequency = AttemptFrequency.EveryTick,
|
||||
CancelDuplicate = false,
|
||||
BlockDuplicate = false
|
||||
};
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
if (_doAfter.TryStartDoAfter(doAfterArgs) && _gameTiming.IsFirstTimePredicted)
|
||||
Spawn("EffectRCDConstruction", location);
|
||||
if (!_doAfter.TryStartDoAfter(doAfterArgs))
|
||||
QueueDel(effect);
|
||||
}
|
||||
|
||||
private void OnDoAfterAttempt(EntityUid uid, RCDComponent comp, DoAfterAttemptEvent<RCDDoAfterEvent> args)
|
||||
private void OnDoAfterAttempt(EntityUid uid, RCDComponent component, DoAfterAttemptEvent<RCDDoAfterEvent> args)
|
||||
{
|
||||
// sus client crash why
|
||||
if (args.Event?.DoAfter?.Args == null)
|
||||
return;
|
||||
|
||||
// Exit if the RCD prototype has changed
|
||||
if (component.ProtoId != args.Event.StartingProtoId)
|
||||
return;
|
||||
|
||||
// Ensure the RCD operation is still valid
|
||||
var location = GetCoordinates(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;
|
||||
}
|
||||
if (!TryGetMapGridData(location, out var mapGridData))
|
||||
return;
|
||||
|
||||
var mapGrid = Comp<MapGridComponent>(gridId.Value);
|
||||
var tile = mapGrid.GetTileRef(location);
|
||||
|
||||
if (!IsRCDStillValid(uid, comp, args.Event.User, args.Event.Target, mapGrid, tile, args.Event.StartingMode))
|
||||
if (!IsRCDOperationStillValid(uid, component, mapGridData.Value, args.Event.Target, args.Event.User))
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnDoAfter(EntityUid uid, RCDComponent comp, RCDDoAfterEvent args)
|
||||
private void OnDoAfter(EntityUid uid, RCDComponent component, RCDDoAfterEvent args)
|
||||
{
|
||||
if (args.Cancelled && _net.IsServer)
|
||||
QueueDel(EntityManager.GetEntity(args.Effect));
|
||||
|
||||
if (args.Handled || args.Cancelled || !_timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
var user = args.User;
|
||||
args.Handled = true;
|
||||
|
||||
var location = GetCoordinates(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;
|
||||
}
|
||||
if (!TryGetMapGridData(location, out var mapGridData))
|
||||
return;
|
||||
|
||||
var mapGrid = Comp<MapGridComponent>(gridId.Value);
|
||||
var tile = mapGrid.GetTileRef(location);
|
||||
var snapPos = mapGrid.TileIndicesFor(location);
|
||||
// Ensure the RCD operation is still valid
|
||||
if (!IsRCDOperationStillValid(uid, component, mapGridData.Value, args.Target, args.User))
|
||||
return;
|
||||
|
||||
// I love that this uses entirely separate code to construction and tile placement!!!
|
||||
// Finalize the operation
|
||||
FinalizeRCDOperation(uid, component, mapGridData.Value, args.Target, args.User);
|
||||
|
||||
switch (comp.Mode)
|
||||
{
|
||||
//Floor mode just needs the tile to be a space tile (subFloor)
|
||||
case RcdMode.Floors:
|
||||
if (!_floors.CanPlaceTile(gridId.Value, mapGrid, out var reason))
|
||||
{
|
||||
_popup.PopupClient(reason, user, user);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
// Play audio and consume charges
|
||||
_audio.PlayPredicted(component.SuccessSound, uid, args.User);
|
||||
_charges.UseCharges(uid, args.Cost);
|
||||
}
|
||||
|
||||
private bool IsRCDStillValid(EntityUid uid, RCDComponent comp, EntityUid user, EntityUid? target, MapGridComponent mapGrid, TileRef tile, RcdMode startingMode)
|
||||
private void OnRCDconstructionGhostRotationEvent(RCDConstructionGhostRotationEvent ev, EntitySessionEventArgs session)
|
||||
{
|
||||
//Less expensive checks first. Failing those ones, we need to check that the tile isn't obstructed.
|
||||
if (comp.Mode != startingMode)
|
||||
return false;
|
||||
var uid = GetEntity(ev.NetEntity);
|
||||
|
||||
var unobstructed = target == null
|
||||
? _interaction.InRangeUnobstructed(user, mapGrid.GridTileToWorld(tile.GridIndices), popup: true)
|
||||
: _interaction.InRangeUnobstructed(user, target.Value, popup: true);
|
||||
// Determine if player that send the message is carrying the specified RCD in their active hand
|
||||
if (session.SenderSession.AttachedEntity == null)
|
||||
return;
|
||||
|
||||
if (!TryComp<HandsComponent>(session.SenderSession.AttachedEntity, out var hands) ||
|
||||
uid != hands.ActiveHand?.HeldEntity)
|
||||
return;
|
||||
|
||||
if (!TryComp<RCDComponent>(uid, out var rcd))
|
||||
return;
|
||||
|
||||
// Update the construction direction
|
||||
rcd.ConstructionDirection = ev.Direction;
|
||||
Dirty(uid, rcd);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Entity construction/deconstruction rule checks
|
||||
|
||||
public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, MapGridData mapGridData, EntityUid? target, EntityUid user, bool popMsgs = true)
|
||||
{
|
||||
// Update cached prototype if required
|
||||
UpdateCachedPrototype(uid, component);
|
||||
|
||||
// Check that the RCD has enough ammo to get the job done
|
||||
TryComp<LimitedChargesComponent>(uid, out var charges);
|
||||
|
||||
// Both of these were messages were suppose to be predicted, but HasInsufficientCharges wasn't being checked on the client for some reason?
|
||||
if (_charges.IsEmpty(uid, charges))
|
||||
{
|
||||
if (popMsgs)
|
||||
_popup.PopupClient(Loc.GetString("rcd-component-no-ammo-message"), uid, user);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_charges.HasInsufficientCharges(uid, component.CachedPrototype.Cost, charges))
|
||||
{
|
||||
if (popMsgs)
|
||||
_popup.PopupClient(Loc.GetString("rcd-component-insufficient-ammo-message"), uid, user);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exit if the target / target location is obstructed
|
||||
var unobstructed = (target == null)
|
||||
? _interaction.InRangeUnobstructed(user, _mapSystem.GridTileToWorld(mapGridData.GridUid, mapGridData.Component, mapGridData.Position), popup: popMsgs)
|
||||
: _interaction.InRangeUnobstructed(user, target.Value, popup: popMsgs);
|
||||
|
||||
if (!unobstructed)
|
||||
return false;
|
||||
|
||||
switch (comp.Mode)
|
||||
// Return whether the operation location is valid
|
||||
switch (component.CachedPrototype.Mode)
|
||||
{
|
||||
//Floor mode just needs the tile to be a space tile (subFloor)
|
||||
case RcdMode.Floors:
|
||||
if (!tile.Tile.IsEmpty)
|
||||
case RcdMode.ConstructTile: return IsConstructionLocationValid(uid, component, mapGridData, user, popMsgs);
|
||||
case RcdMode.ConstructObject: return IsConstructionLocationValid(uid, component, mapGridData, user, popMsgs);
|
||||
case RcdMode.Deconstruct: return IsDeconstructionStillValid(uid, component, mapGridData, target, user, popMsgs);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsConstructionLocationValid(EntityUid uid, RCDComponent component, MapGridData mapGridData, EntityUid user, bool popMsgs = true)
|
||||
{
|
||||
// Check rule: Must build on empty tile
|
||||
if (component.CachedPrototype.ConstructionRules.Contains(RcdConstructionRule.MustBuildOnEmptyTile) && !mapGridData.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 (!component.CachedPrototype.ConstructionRules.Contains(RcdConstructionRule.CanBuildOnEmptyTile) && mapGridData.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 (component.CachedPrototype.ConstructionRules.Contains(RcdConstructionRule.MustBuildOnSubfloor) && !mapGridData.Tile.Tile.GetContentTileDefinition().IsSubFloor)
|
||||
{
|
||||
if (popMsgs)
|
||||
_popup.PopupClient(Loc.GetString("rcd-component-must-build-on-subfloor-message"), uid, user);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tile specific rules
|
||||
if (component.CachedPrototype.Mode == RcdMode.ConstructTile)
|
||||
{
|
||||
// Check rule: Tile placement is valid
|
||||
if (!_floors.CanPlaceTile(mapGridData.GridUid, mapGridData.Component, out var reason))
|
||||
{
|
||||
if (popMsgs)
|
||||
_popup.PopupClient(reason, uid, user);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check rule: Tiles can't be identical
|
||||
if (mapGridData.Tile.Tile.GetContentTileDefinition().ID == component.CachedPrototype.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 = component.CachedPrototype.ConstructionRules.Contains(RcdConstructionRule.IsWindow);
|
||||
var isCatwalk = component.CachedPrototype.ConstructionRules.Contains(RcdConstructionRule.IsCatwalk);
|
||||
|
||||
_intersectingEntities.Clear();
|
||||
_lookup.GetLocalEntitiesIntersecting(mapGridData.GridUid, mapGridData.Position, _intersectingEntities, -0.05f, LookupFlags.Uncontained);
|
||||
|
||||
foreach (var ent in _intersectingEntities)
|
||||
{
|
||||
if (isWindow && HasComp<SharedCanBuildWindowOnTopComponent>(ent))
|
||||
continue;
|
||||
|
||||
if (isCatwalk && _tags.HasTag(ent, "Catwalk"))
|
||||
{
|
||||
if (popMsgs)
|
||||
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-on-occupied-tile-message"), uid, user);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (component.CachedPrototype.CollisionMask != CollisionGroup.None && TryComp<FixturesComponent>(ent, out var fixtures))
|
||||
{
|
||||
foreach (var fixture in fixtures.Fixtures.Values)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-floor-tile-not-empty-message"), uid, user);
|
||||
// Continue if no collision is possible
|
||||
if (fixture.CollisionLayer <= 0 || (fixture.CollisionLayer & (int) component.CachedPrototype.CollisionMask) == 0)
|
||||
continue;
|
||||
|
||||
// Continue if our custom collision bounds are not intersected
|
||||
if (component.CachedPrototype.CollisionPolygon != null &&
|
||||
!DoesCustomBoundsIntersectWithFixture(component.CachedPrototype.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, MapGridData mapGridData, EntityUid? target, EntityUid user, bool popMsgs = true)
|
||||
{
|
||||
// Attempt to deconstruct a floor tile
|
||||
if (target == null)
|
||||
{
|
||||
// The tile is empty
|
||||
if (mapGridData.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(mapGridData.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 = (ContentTileDefinition) _tileDefMan[mapGridData.Tile.Tile.TypeId];
|
||||
|
||||
if (tileDef.Indestructible)
|
||||
{
|
||||
if (popMsgs)
|
||||
_popup.PopupClient(Loc.GetString("rcd-component-tile-indestructible-message"), uid, user);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to deconstruct an object
|
||||
else
|
||||
{
|
||||
// The object is not in the whitelist
|
||||
if (!TryComp<RCDDeconstructableComponent>(target, out var deconstructible) || !deconstructible.Deconstructable)
|
||||
{
|
||||
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, MapGridData mapGridData, EntityUid? target, EntityUid user)
|
||||
{
|
||||
if (!_net.IsServer)
|
||||
return;
|
||||
|
||||
if (component.CachedPrototype.Prototype == null)
|
||||
return;
|
||||
|
||||
switch (component.CachedPrototype.Mode)
|
||||
{
|
||||
case RcdMode.ConstructTile:
|
||||
_mapSystem.SetTile(mapGridData.GridUid, mapGridData.Component, mapGridData.Position, new Tile(_tileDefMan[component.CachedPrototype.Prototype].TileId));
|
||||
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to set grid: {mapGridData.GridUid} {mapGridData.Position} to {component.CachedPrototype.Prototype}");
|
||||
break;
|
||||
|
||||
case RcdMode.ConstructObject:
|
||||
var ent = Spawn(component.CachedPrototype.Prototype, _mapSystem.GridTileToLocal(mapGridData.GridUid, mapGridData.Component, mapGridData.Position));
|
||||
|
||||
switch (component.CachedPrototype.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 = component.ConstructionDirection.ToAngle();
|
||||
break;
|
||||
}
|
||||
|
||||
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to spawn {ToPrettyString(ent)} at {mapGridData.Position} on grid {mapGridData.GridUid}");
|
||||
break;
|
||||
|
||||
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...
|
||||
if (target == null)
|
||||
{
|
||||
// the turf is blocked
|
||||
if (IsTileBlocked(tile))
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("rcd-component-tile-obstructed-message"), uid, user);
|
||||
return false;
|
||||
}
|
||||
// the turf can't be destroyed (planet probably)
|
||||
var tileDef = (ContentTileDefinition) _tileDefMan[tile.Tile.TypeId];
|
||||
if (tileDef.Indestructible)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("rcd-component-tile-indestructible-message"), uid, user);
|
||||
return false;
|
||||
}
|
||||
// Deconstruct tile (either converts the tile to lattice, or removes lattice)
|
||||
var tile = (mapGridData.Tile.Tile.GetContentTileDefinition().ID != "Lattice") ? new Tile(_tileDefMan["Lattice"].TileId) : Tile.Empty;
|
||||
_mapSystem.SetTile(mapGridData.GridUid, mapGridData.Component, mapGridData.Position, tile);
|
||||
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to set grid: {mapGridData.GridUid} tile: {mapGridData.Position} open to space");
|
||||
}
|
||||
//They tried to decon a non-turf but it's not in the whitelist
|
||||
else if (!_tag.HasTag(target.Value, "RCDDeconstructWhitelist"))
|
||||
else
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user);
|
||||
return false;
|
||||
// Deconstruct object
|
||||
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to delete {ToPrettyString(target):target}");
|
||||
QueueDel(target);
|
||||
}
|
||||
|
||||
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!
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void NextMode(EntityUid uid, RCDComponent comp, EntityUid user)
|
||||
#endregion
|
||||
|
||||
#region Utility functions
|
||||
|
||||
public bool TryGetMapGridData(EntityCoordinates location, [NotNullWhen(true)] out MapGridData? mapGridData)
|
||||
{
|
||||
_audio.PlayPredicted(comp.SwapModeSound, uid, user);
|
||||
mapGridData = null;
|
||||
var gridUid = location.GetGridUid(EntityManager);
|
||||
|
||||
var mode = (int) comp.Mode;
|
||||
mode = ++mode % _rcdModeCount;
|
||||
comp.Mode = (RcdMode) mode;
|
||||
Dirty(uid, comp);
|
||||
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
|
||||
{
|
||||
location = location.AlignWithClosestGridTile(1.75f, EntityManager);
|
||||
gridUid = location.GetGridUid(EntityManager);
|
||||
|
||||
var msg = Loc.GetString("rcd-component-change-mode", ("mode", comp.Mode.ToString()));
|
||||
_popup.PopupClient(msg, uid, user);
|
||||
// Check if we got a grid ID the second time round
|
||||
if (!TryComp(gridUid, out mapGrid))
|
||||
return false;
|
||||
}
|
||||
|
||||
gridUid = mapGrid.Owner;
|
||||
|
||||
var tile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location);
|
||||
var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location);
|
||||
mapGridData = new MapGridData(gridUid.Value, mapGrid, location, tile, position);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsTileBlocked(TileRef tile)
|
||||
private bool DoesCustomBoundsIntersectWithFixture(PolygonShape boundingPolygon, Transform boundingTransform, EntityUid fixtureOwner, Fixture fixture)
|
||||
{
|
||||
return _turf.IsTileBlocked(tile, CollisionGroup.MobMask);
|
||||
var entXformComp = Transform(fixtureOwner);
|
||||
var entXform = new Transform(new(), entXformComp.LocalRotation);
|
||||
|
||||
return boundingPolygon.ComputeAABB(boundingTransform, 0).Intersects(fixture.Shape.ComputeAABB(entXform, 0));
|
||||
}
|
||||
|
||||
public void UpdateCachedPrototype(EntityUid uid, RCDComponent component)
|
||||
{
|
||||
if (component.ProtoId.Id != component.CachedPrototype?.Prototype)
|
||||
component.CachedPrototype = _protoManager.Index(component.ProtoId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public struct MapGridData
|
||||
{
|
||||
public EntityUid GridUid;
|
||||
public MapGridComponent Component;
|
||||
public EntityCoordinates Location;
|
||||
public TileRef Tile;
|
||||
public Vector2i Position;
|
||||
|
||||
public MapGridData(EntityUid gridUid, MapGridComponent component, EntityCoordinates location, TileRef tile, Vector2i position)
|
||||
{
|
||||
GridUid = gridUid;
|
||||
Component = component;
|
||||
Location = location;
|
||||
Tile = tile;
|
||||
Position = position;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class RCDDoAfterEvent : DoAfterEvent
|
||||
{
|
||||
[DataField("location", required: true)]
|
||||
public NetCoordinates Location = default!;
|
||||
[DataField(required: true)]
|
||||
public NetCoordinates Location { get; private set; } = default!;
|
||||
|
||||
[DataField("startingMode", required: true)]
|
||||
public RcdMode StartingMode = default!;
|
||||
[DataField]
|
||||
public ProtoId<RCDPrototype> StartingProtoId { get; private set; } = default!;
|
||||
|
||||
private RCDDoAfterEvent()
|
||||
{
|
||||
}
|
||||
[DataField]
|
||||
public int Cost { get; private set; } = 1;
|
||||
|
||||
public RCDDoAfterEvent(NetCoordinates location, RcdMode startingMode)
|
||||
[DataField("fx")]
|
||||
public NetEntity? Effect { get; private set; } = null;
|
||||
|
||||
private RCDDoAfterEvent() { }
|
||||
|
||||
public RCDDoAfterEvent(NetCoordinates location, ProtoId<RCDPrototype> startingProtoId, int cost, NetEntity? effect = null)
|
||||
{
|
||||
Location = location;
|
||||
StartingMode = startingMode;
|
||||
StartingProtoId = startingProtoId;
|
||||
Cost = cost;
|
||||
Effect = effect;
|
||||
}
|
||||
|
||||
public override DoAfterEvent Clone() => this;
|
||||
|
||||
Reference in New Issue
Block a user