| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647 |
- 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.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 Robust.Shared.Timing;
- using System.Diagnostics.CodeAnalysis;
- using System.Linq;
- namespace Content.Shared.RCD.Systems;
- [Virtual]
- public class RCDSystem : EntitySystem
- {
- [Dependency] private readonly IGameTiming _timing = default!;
- [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 _charges = default!;
- [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
- [Dependency] private readonly SharedInteractionSystem _interaction = default!;
- [Dependency] private readonly SharedPopupSystem _popup = default!;
- [Dependency] private readonly TurfSystem _turf = 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 _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, AfterInteractEvent>(OnAfterInteract);
- SubscribeLocalEvent<RCDComponent, RCDDoAfterEvent>(OnDoAfter);
- SubscribeLocalEvent<RCDComponent, DoAfterAttemptEvent<RCDDoAfterEvent>>(OnDoAfterAttempt);
- SubscribeLocalEvent<RCDComponent, RCDSystemMessage>(OnRCDSystemMessage);
- SubscribeNetworkEvent<RCDConstructionGhostRotationEvent>(OnRCDconstructionGhostRotationEvent);
- }
- #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);
- }
- private void OnExamine(EntityUid uid, RCDComponent component, ExaminedEvent args)
- {
- if (!args.IsInDetailsRange)
- return;
- // Update cached prototype if required
- UpdateCachedPrototype(uid, component);
- var msg = Loc.GetString("rcd-component-examine-mode-details", ("mode", Loc.GetString(component.CachedPrototype.SetName)));
- if (component.CachedPrototype.Mode == RcdMode.ConstructTile || component.CachedPrototype.Mode == RcdMode.ConstructObject)
- {
- var name = Loc.GetString(component.CachedPrototype.SetName);
- if (component.CachedPrototype.Prototype != null &&
- _protoManager.TryIndex(component.CachedPrototype.Prototype, out var proto))
- name = proto.Name;
- msg = Loc.GetString("rcd-component-examine-build-details", ("name", name));
- }
- args.PushMarkup(msg);
- }
- private void OnAfterInteract(EntityUid uid, RCDComponent component, AfterInteractEvent args)
- {
- if (args.Handled || !args.CanReach)
- return;
- var user = args.User;
- var location = args.ClickLocation;
- // Initial validity checks
- if (!location.IsValid(EntityManager))
- return;
- if (!TryGetMapGridData(location, out var mapGridData))
- {
- _popup.PopupClient(Loc.GetString("rcd-component-no-valid-grid"), uid, user);
- return;
- }
- 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.ConstructionDirection, component.ProtoId, cost, EntityManager.GetNetEntity(effect));
- 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);
- if (!TryGetMapGridData(location, out var mapGridData))
- {
- args.Cancel();
- return;
- }
- if (!IsRCDOperationStillValid(uid, component, mapGridData.Value, args.Event.Target, args.Event.User))
- args.Cancel();
- }
- 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;
- args.Handled = true;
- var location = GetCoordinates(args.Location);
- if (!TryGetMapGridData(location, out var mapGridData))
- return;
- // Ensure the RCD operation is still valid
- if (!IsRCDOperationStillValid(uid, component, mapGridData.Value, args.Target, args.User))
- return;
- // Finalize the operation
- FinalizeRCDOperation(uid, component, mapGridData.Value, args.Direction, args.Target, args.User);
- // Play audio and consume charges
- _audio.PlayPredicted(component.SuccessSound, uid, args.User);
- _charges.UseCharges(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 == 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;
- // Return whether the operation location is valid
- switch (component.CachedPrototype.Mode)
- {
- 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)
- {
- // Continue if no collision is possible
- if (!fixture.Hard || 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, Direction direction, 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 = direction.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;
- case RcdMode.Deconstruct:
- if (target == null)
- {
- // 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");
- }
- 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
- public bool TryGetMapGridData(EntityCoordinates location, [NotNullWhen(true)] out MapGridData? mapGridData)
- {
- mapGridData = null;
- var gridUid = location.GetGridUid(EntityManager);
- if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
- {
- location = location.AlignWithClosestGridTile(1.75f, EntityManager);
- gridUid = location.GetGridUid(EntityManager);
- // Check if we got a grid ID the second time round
- if (!TryComp(gridUid, out mapGrid))
- return false;
- }
- 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 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));
- }
- 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(required: true)]
- public NetCoordinates Location { get; private set; } = default!;
- [DataField]
- public Direction Direction { get; private set; } = default!;
- [DataField]
- public ProtoId<RCDPrototype> StartingProtoId { get; private set; } = default!;
- [DataField]
- public int Cost { get; private set; } = 1;
- [DataField("fx")]
- public NetEntity? Effect { get; private set; } = null;
- private RCDDoAfterEvent() { }
- public RCDDoAfterEvent(NetCoordinates location, Direction direction, ProtoId<RCDPrototype> startingProtoId, int cost, NetEntity? effect = null)
- {
- Location = location;
- Direction = direction;
- StartingProtoId = startingProtoId;
- Cost = cost;
- Effect = effect;
- }
- public override DoAfterEvent Clone() => this;
- }
|