* Fix for the salvage ice labs map. (#26928)
* done
* more work
* Automatic changelog update
* Update Credits (#26938)
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
* Fix cryostorage identifying unknown characters as captain (#26927)
Fixed cryostorage getting captain's record for unknown jobs.
Also localized Unknown job string.
* Automatic changelog update
* Fixed Honkbot/jonkbot honking like crazy, gave honkbot/jonkbot standard idle ai. (#26939)
* Fixed Honkbot/jonkbot honking like crazy, gave honkbot/jonkbot standard idle ai.
* Update Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml
---------
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
* Automatic changelog update
* Bug fix: Force cancellation of RCD constructions if the construction type is changed (#26935)
Force cancellation of RCD constructions if the construction type is changed
* Fix standart -> standard and dressfilled test fail (#26942)
Fix standart -> standard
* Add Ability to stop sound when MobState is Dead (#26905)
* Add stopsWhenEntityDead to sound components
* Convert component
* Review
* Fix dupe sub
---------
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
* Automatic changelog update
* Fix rockets and lasers looking like they have nothing loaded (#26933)
* Automatic changelog update
* You can now see paper on crates (with color!) (#26834)
* Implement changes on not-cooked branch
* Made it work
* Fix update appearance calls
* Fix extra indents, clean-up code, fix tests hopefully
* Fix hammy cagecrate
* Fix messing up the yml, add artifact crate specific labels back in
* Visual Studio hates yml, sad
* Seperate the colors for cargonia
* sorry json
* make label move with artifact door
* Apply suggestion changes
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
* Fix remaining crate offsets, add a few for livestock and graves (why are you labeling graves) and coffin label sprites (why are you labeling coffins??)
---------
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
* Make UtensilSystem and SharpSystem not run AfterInteract if it has already been handled (#25826)
* Make UtensilSystem and SharpSystem not run AfterInteract if it has already been handled
* merge conflicts
---------
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
* Automatic changelog update
* Add two-message overload to PopupPredicted (#26907)
Added two-message overload to PopupPredicted
* Update submodule to 218.0.0 (#26945)
* Autism pins! (#25597)
* hee hee he ha ha
* added gold varients, forgive me for my spritework
* maints loot, copying from past PRs
* Trying to fix RSI
* speedran these sprites in break time, pictures will be later
* Fixed/Tweaked glows
* consensus
* gregregation
* dam copiryte
* oops i forgot to delete 2 fields hope this works
* Automatic changelog update
* Fix database round start date issues (#26838)
How can ONE DATABASE COLUMN have so many cursed issues I don't know, but it certainly pissed off the devil in its previous life.
The start_date column on round entities in the database was added by https://github.com/space-wizards/space-station-14/pull/21153. For some reason, this PR gave the column a nonsensical default value instead of making it nullable. This default value causes the code from #25280 to break. It actually trips an assert though that's not what the original issue report ran into.
This didn't get noticed on wizden servers because we at some point backfilled the start_date column based on the stored admin logs.
So I change the database model to make this column nullable, updated the C# code to match, and made the existing migration set the invalid values to be NULL instead. Cool.
Wait how's SQLite handle in this scenario anyways? Well actually turns out the column was *completely broken* in the first place!
The code for inserting into the round table was copy pasted between SQLite and PostgreSQL, with the only difference being that the SQLite key manually assigned the primary key instead of letting SQLite AUTOINCREMENT it. And then the code to give a start_date value was only added to the PostgreSQL version (which is actually in the base class already). So for SQLite that column's been filled up with the same invalid default the whole time.
Why was the code manually assigning a PK? I checked the SQLite docs for AUTOINCREMENT[1], and the behavior seems appropriate.
I removed the SQLite-specific code path and it just seems to work regardless. The migration just sets the old values to NULL too.
BUT WAIT, THERE'S MORE!
Turns out just doing the migration on SQLite is a pain in the ass! EF Core has to create a new table to apply the nullability change, because SQLite doesn't support proper ALTER COLUMN. This causes the generated SQL commands to be weird and the UPDATE for the migration goes BEFORE the nullability change... I ended up having to make TWO migrations for SQLite. Yay.
Fixes #26800
[1]: https://www.sqlite.org/autoinc.html
* Fix options menu crashing in replays (#26911)
Not having the nullable set properly is annoying but fixing that would probably be a significant amount of work.
* Greyscale color clothing (#26943)
* greyscales color gloves, color jumpsuits, and shoes
* remove popbob
* fix test fails
* Automatic changelog update
* WT550 Buffs + Burst Mode for WT550 & C-20R (#26886)
* Slightly increased WT550 Firerate, drastically reduced recoil, and given it the option to fire in 5 round bursts.
* Given the C-20 a 5 round burst aswell
* Automatic changelog update
* make holoparasites actually holographic (#26862)
it's over
* Automatic changelog update
* Add character sheets to board game crate (#26926)
add character sheets to board game crate
* Automatic changelog update
* Game server admin API (#26880)
* Reapply "Game server api" (#26871)
This reverts commit 3aee197923.
* Rewrite 75% of the code it's good now
* Wield recoil components (#26915)
* WieldRecoilComponents
* WieldRecoilComponents
* Update Content.Shared/Weapons/Ranged/Components/GunWieldBonusComponent.cs
Co-authored-by: Whisper <121047731+QuietlyWhisper@users.noreply.github.com>
* Update Content.Shared/Weapons/Ranged/Components/GunWieldBonusComponent.cs
---------
Co-authored-by: Whisper <121047731+QuietlyWhisper@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
* Clown shoes make you waddle, as God intended (#26338)
* Clown shoes make you waddle, as God intended
* OOPS
* Toned down, client system name fix
* Tidy namespacing for @deltanedas
* Refactor to handle prediction better, etc.
* Resolve PR comments.
* Automatic changelog update
* Use round time instead of server time for criminal history (#26949)
make criminal records computer use round time for history instead of the server time
* Rotate and Offset station CCVar nuke (#26175)
* no content
* add noRot to Europa
* bruh. and this
* yay
* fix
* Update debug.yml
---------
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: superjj18 <gagnonjake@gmail.com>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: GreaseMonk <1354802+GreaseMonk@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Co-authored-by: Verm <32827189+Vermidia@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: Terraspark4941 <terraspark4941@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com>
Co-authored-by: BramvanZijp <56019239+BramvanZijp@users.noreply.github.com>
Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
Co-authored-by: Tyzemol <85772526+Tyzemol@users.noreply.github.com>
Co-authored-by: Froffy025 <78222136+Froffy025@users.noreply.github.com>
Co-authored-by: Whisper <121047731+QuietlyWhisper@users.noreply.github.com>
Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com>
Co-authored-by: ilya.mikheev.coder <imc-ext+github@ilyamikcoder.com>
650 lines
24 KiB
C#
650 lines
24 KiB
C#
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;
|
|
}
|
|
|
|
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 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;
|
|
}
|