mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-18 14:07:53 +03:00
<!-- Please read these guidelines before opening your PR: https://docs.spacestation14.io/en/getting-started/pr-guideline --> <!-- The text between the arrows are comments - they will not be visible on your PR. --> ## About the PR <!-- What did you change in this PR? --> EMP has been changed to target APC-powered-devices (most electrical devices) as well as batteries to disable them. This means EMP can interfere with autolathes, airlocks, atmos devices, substations and SMES. The power draw of a single EMP grenade now cuts out a substation, and the disabling effect prevents further recharge until it subsides. EMP duration now also stacks, which creates a novel way to quietly black out the station by attacking engineering SMES with 3 EMP grenades (6tc EMP bundle) to black out the station for 3 minutes. Edit, here's a detailed changelog of the PR, Functionality: - EMP disable has been generalised to kill and prevent further function of every device/battery by interrupting recharge - As a result of the above, some hard coded interactions have been culled - EMP disable duration now stacks with multiple EMP blasts - EMP is now capable of draining from gravity generators - The Charger system has been slightly reworked to facilitate communication between batteries and chargers Results: - EMP grenade can disable basically every powered machine, most notably doors - EMP grenade has had its power drain upped to 2.7MW, which is slightly more than a substation and 1/3 a SMES - EMP grenade can now instantly kill substations - EMP grenade can now instantly kill gravity generators - 3 EMP grenades (6tc) can be used to kill SMES and disable recharge for 3 minutes with no evidence on the power monitor. ## Why / Balance <!-- Why was it changed? Link any discussions or issues here. Please discuss how this would affect game balance. --> EMP at 2tc has a relatively low value-proposition when compared to C4 which is also 2tc. While EMP can probably black out one (or two if you're lucky) APCs and can be used as a defensive option against Stun/Lasers. C4 can be used to cut wires, substations, SMES, generators, doors, reinforced walls, people and the list probably continues. New EMP can be used to soft-bomb station power in an explosion that isn't globally alarming (salv boom). Targeting the captain's office directly may let you crowbar in and steal the locker but it leaves ephemeral evidence in the form of everything electrical shimmering blue. Opting to bomb substations blacks out a wider area, providing several degrees of separation from your target. That is to say, new EMP grenade favours map knowledge and rewards better stealth. ## Technical details <!-- If this is a code change, summarize at high level how your new code works. This makes it easier to review. --> - `C.S/.../EmpSystem.cs` uses TryComp to turn on/off charging for `C.S/Power/Components/PowerNetworkBatteryComponent` - `C.S/Power/EntitySystems/PowerReceiverSystem.cs` listens to `EmpPulseEvent` to turn off. Requests to turn back on are additionally intercepted by `EmpSystem.cs` and cancelled. - `C.S/.../GravityGeneratorSystem.cs` listens to `EmpPulseEvent` and converts energy consumption to a normalised charge - `C.S/Power/EntitySystems/ApcSystem.cs` no longer toggles its breaker, but still listens to `EmpPulseEvent` for updating visuals. - `C.S/Power/EntitySystems/ChargerSystem.cs` was refactored to add a `ChargingComponent` flag to power cells instead of `ActiveCharger` on itself. Battery and Charger communicate through this flag. Listens to `EmpPulseEvent` for updating its state machine. New `ChargerUpdateStatusEvent` allows batteries to update the charger's status. - `C.S/Power/EntitySystems/BatterySystem.cs` can now be disabled, and checks for disabling before updating its charge. Raises `ChargerUpdateStatusEvent` when hearing `EmpDisabledRemoved` to tell its charger to start charging again. - `C.S/Power/PowerWireAction.cs` checks for `EmpDisabledComponent` before turning power back on. - `C.S/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs` and `C.S/VendingMachines/VendingMachineSystem.cs` had redundant `EmpPulseEvent` listeners culled. - `Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml` buffed EMP grenade. ## Media <!-- PRs which make ingame changes (adding clothing, items, new features, etc) are required to have media attached that showcase the changes. Small fixes/refactors are exempt. Any media may be used in SS14 progress reports, with clear credit given. If you're unsure whether your PR will require media, ask a maintainer. Check the box below to confirm that you have in fact seen this (put an X in the brackets, like [X]): --> https://www.youtube.com/embed/rSVph6OIg1s?si=8o4bx9Vx16B6usuu - outdated video demonstrating changes on a wizden map https://www.youtube.com/embed/B3iPhLcfs-0?si=trB1HY2ccjMf96Bj - electrical anomaly crit with updated emp - [x] I have added screenshots/videos to this PR showcasing its changes ingame, **or** this PR does not require an ingame showcase **Changelog** <!-- Make players aware of new features and changes that could affect how they play the game by adding a Changelog entry. Please read the Changelog guidelines located at: https://docs.spacestation14.io/en/getting-started/pr-guideline#changelog --> 🆑 - tweak: EMP Grenades can now disable basically any electrical device, and stack in disable duration. --------- Signed-off-by: VMSolidus <evilexecutive@gmail.com> Co-authored-by: VMSolidus <evilexecutive@gmail.com>
839 lines
31 KiB
C#
839 lines
31 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using Content.Server.Administration.Logs;
|
|
using Content.Server.Atmos.EntitySystems;
|
|
using Content.Server.Disposal.Tube;
|
|
using Content.Server.Disposal.Tube.Components;
|
|
using Content.Server.Disposal.Unit.Components;
|
|
using Content.Server.Popups;
|
|
using Content.Server.Power.Components;
|
|
using Content.Server.Power.EntitySystems;
|
|
using Content.Shared.ActionBlocker;
|
|
using Content.Shared.Atmos;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.Destructible;
|
|
using Content.Shared.Disposal;
|
|
using Content.Shared.Disposal.Components;
|
|
using Content.Shared.DoAfter;
|
|
using Content.Shared.DragDrop;
|
|
using Content.Shared.Emag.Systems;
|
|
using Content.Shared.Hands.Components;
|
|
using Content.Shared.Hands.EntitySystems;
|
|
using Content.Shared.IdentityManagement;
|
|
using Content.Shared.Interaction;
|
|
using Content.Shared.Item;
|
|
using Content.Shared.Movement.Events;
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Throwing;
|
|
using Content.Shared.Verbs;
|
|
using Robust.Server.Audio;
|
|
using Robust.Server.GameObjects;
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.GameStates;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Physics.Components;
|
|
using Robust.Shared.Physics.Events;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Server.Disposal.Unit.EntitySystems;
|
|
|
|
public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
|
{
|
|
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
|
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
|
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
|
|
[Dependency] private readonly AppearanceSystem _appearance = default!;
|
|
[Dependency] private readonly AtmosphereSystem _atmosSystem = default!;
|
|
[Dependency] private readonly AudioSystem _audioSystem = default!;
|
|
[Dependency] private readonly DisposalTubeSystem _disposalTubeSystem = default!;
|
|
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
|
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
|
[Dependency] private readonly PowerReceiverSystem _power = default!;
|
|
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
|
|
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
|
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
|
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
|
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
SubscribeLocalEvent<DisposalUnitComponent, ComponentGetState>(OnGetState);
|
|
SubscribeLocalEvent<DisposalUnitComponent, PreventCollideEvent>(OnPreventCollide);
|
|
SubscribeLocalEvent<DisposalUnitComponent, CanDropTargetEvent>(OnCanDragDropOn);
|
|
SubscribeLocalEvent<DisposalUnitComponent, GotEmaggedEvent>(OnEmagged);
|
|
|
|
// Shouldn't need re-anchoring.
|
|
SubscribeLocalEvent<DisposalUnitComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
|
// TODO: Predict me when hands predicted
|
|
SubscribeLocalEvent<DisposalUnitComponent, ContainerRelayMovementEntityEvent>(OnMovement);
|
|
SubscribeLocalEvent<DisposalUnitComponent, PowerChangedEvent>(OnPowerChange);
|
|
SubscribeLocalEvent<DisposalUnitComponent, ComponentInit>(OnDisposalInit);
|
|
|
|
SubscribeLocalEvent<DisposalUnitComponent, ThrowHitByEvent>(OnThrowCollide);
|
|
|
|
SubscribeLocalEvent<DisposalUnitComponent, ActivateInWorldEvent>(OnActivate);
|
|
SubscribeLocalEvent<DisposalUnitComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
|
|
SubscribeLocalEvent<DisposalUnitComponent, DragDropTargetEvent>(OnDragDropOn);
|
|
SubscribeLocalEvent<DisposalUnitComponent, DestructionEventArgs>(OnDestruction);
|
|
|
|
SubscribeLocalEvent<DisposalUnitComponent, GetVerbsEvent<InteractionVerb>>(AddInsertVerb);
|
|
SubscribeLocalEvent<DisposalUnitComponent, GetVerbsEvent<AlternativeVerb>>(AddDisposalAltVerbs);
|
|
SubscribeLocalEvent<DisposalUnitComponent, GetVerbsEvent<Verb>>(AddClimbInsideVerb);
|
|
|
|
SubscribeLocalEvent<DisposalUnitComponent, DisposalDoAfterEvent>(OnDoAfter);
|
|
|
|
SubscribeLocalEvent<DisposalUnitComponent, SharedDisposalUnitComponent.UiButtonPressedMessage>(OnUiButtonPressed);
|
|
}
|
|
|
|
private void OnGetState(EntityUid uid, DisposalUnitComponent component, ref ComponentGetState args)
|
|
{
|
|
args.State = new DisposalUnitComponentState(
|
|
component.FlushSound,
|
|
component.State,
|
|
component.NextPressurized,
|
|
component.AutomaticEngageTime,
|
|
component.NextFlush,
|
|
component.Powered,
|
|
component.Engaged,
|
|
GetNetEntityList(component.RecentlyEjected));
|
|
}
|
|
|
|
private void AddDisposalAltVerbs(EntityUid uid, SharedDisposalUnitComponent component, GetVerbsEvent<AlternativeVerb> args)
|
|
{
|
|
if (!args.CanAccess || !args.CanInteract)
|
|
return;
|
|
|
|
// Behavior for if the disposals bin has items in it
|
|
if (component.Container.ContainedEntities.Count > 0)
|
|
{
|
|
// Verbs to flush the unit
|
|
AlternativeVerb flushVerb = new()
|
|
{
|
|
Act = () => ManualEngage(uid, component),
|
|
Text = Loc.GetString("disposal-flush-verb-get-data-text"),
|
|
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png")),
|
|
Priority = 1,
|
|
};
|
|
args.Verbs.Add(flushVerb);
|
|
|
|
// Verb to eject the contents
|
|
AlternativeVerb ejectVerb = new()
|
|
{
|
|
Act = () => TryEjectContents(uid, component),
|
|
Category = VerbCategory.Eject,
|
|
Text = Loc.GetString("disposal-eject-verb-get-data-text")
|
|
};
|
|
args.Verbs.Add(ejectVerb);
|
|
}
|
|
}
|
|
|
|
private void AddClimbInsideVerb(EntityUid uid, SharedDisposalUnitComponent component, GetVerbsEvent<Verb> args)
|
|
{
|
|
// This is not an interaction, activation, or alternative verb type because unfortunately most users are
|
|
// unwilling to accept that this is where they belong and don't want to accidentally climb inside.
|
|
if (!args.CanAccess ||
|
|
!args.CanInteract ||
|
|
component.Container.ContainedEntities.Contains(args.User) ||
|
|
!_actionBlockerSystem.CanMove(args.User))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Add verb to climb inside of the unit,
|
|
Verb verb = new()
|
|
{
|
|
Act = () => TryInsert(uid, args.User, args.User),
|
|
DoContactInteraction = true,
|
|
Text = Loc.GetString("disposal-self-insert-verb-get-data-text")
|
|
};
|
|
// TODO VERB ICON
|
|
// TODO VERB CATEGORY
|
|
// create a verb category for "enter"?
|
|
// See also, medical scanner. Also maybe add verbs for entering lockers/body bags?
|
|
args.Verbs.Add(verb);
|
|
}
|
|
|
|
private void AddInsertVerb(EntityUid uid, SharedDisposalUnitComponent component, GetVerbsEvent<InteractionVerb> args)
|
|
{
|
|
if (!args.CanAccess || !args.CanInteract || args.Hands == null || args.Using == null)
|
|
return;
|
|
|
|
if (!_actionBlockerSystem.CanDrop(args.User))
|
|
return;
|
|
|
|
if (!CanInsert(uid, component, args.Using.Value))
|
|
return;
|
|
|
|
InteractionVerb insertVerb = new()
|
|
{
|
|
Text = Name(args.Using.Value),
|
|
Category = VerbCategory.Insert,
|
|
Act = () =>
|
|
{
|
|
_handsSystem.TryDropIntoContainer(args.User, args.Using.Value, component.Container, checkActionBlocker: false, args.Hands);
|
|
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User):player} inserted {ToPrettyString(args.Using.Value)} into {ToPrettyString(uid)}");
|
|
AfterInsert(uid, component, args.Using.Value, args.User);
|
|
}
|
|
};
|
|
|
|
args.Verbs.Add(insertVerb);
|
|
}
|
|
|
|
private void OnDoAfter(EntityUid uid, SharedDisposalUnitComponent component, DoAfterEvent args)
|
|
{
|
|
if (args.Handled || args.Cancelled || args.Args.Target == null || args.Args.Used == null)
|
|
return;
|
|
|
|
AfterInsert(uid, component, args.Args.Target.Value, args.Args.User, doInsert: true);
|
|
|
|
args.Handled = true;
|
|
}
|
|
|
|
public override void DoInsertDisposalUnit(EntityUid uid, EntityUid toInsert, EntityUid user, SharedDisposalUnitComponent? disposal = null)
|
|
{
|
|
if (!ResolveDisposals(uid, ref disposal))
|
|
return;
|
|
|
|
if (!_containerSystem.Insert(toInsert, disposal.Container))
|
|
return;
|
|
|
|
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):player} inserted {ToPrettyString(toInsert)} into {ToPrettyString(uid)}");
|
|
AfterInsert(uid, disposal, toInsert, user);
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
|
|
var query = EntityQueryEnumerator<DisposalUnitComponent, MetaDataComponent>();
|
|
while (query.MoveNext(out var uid, out var unit, out var metadata))
|
|
{
|
|
Update(uid, unit, metadata, frameTime);
|
|
}
|
|
}
|
|
|
|
#region UI Handlers
|
|
private void OnUiButtonPressed(EntityUid uid, SharedDisposalUnitComponent component, SharedDisposalUnitComponent.UiButtonPressedMessage args)
|
|
{
|
|
if (args.Session.AttachedEntity is not { Valid: true } player)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (args.Button)
|
|
{
|
|
case SharedDisposalUnitComponent.UiButton.Eject:
|
|
TryEjectContents(uid, component);
|
|
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):player} hit eject button on {ToPrettyString(uid)}");
|
|
break;
|
|
case SharedDisposalUnitComponent.UiButton.Engage:
|
|
ToggleEngage(uid, component);
|
|
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):player} hit flush button on {ToPrettyString(uid)}, it's now {(component.Engaged ? "on" : "off")}");
|
|
break;
|
|
case SharedDisposalUnitComponent.UiButton.Power:
|
|
_power.TryTogglePower(uid, user: args.Session.AttachedEntity);
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException($"{ToPrettyString(player):player} attempted to hit a nonexistant button on {ToPrettyString(uid)}");
|
|
}
|
|
}
|
|
|
|
public void ToggleEngage(EntityUid uid, SharedDisposalUnitComponent component)
|
|
{
|
|
component.Engaged ^= true;
|
|
|
|
if (component.Engaged)
|
|
{
|
|
ManualEngage(uid, component);
|
|
}
|
|
else
|
|
{
|
|
Disengage(uid, component);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Eventbus Handlers
|
|
|
|
private void OnActivate(EntityUid uid, SharedDisposalUnitComponent component, ActivateInWorldEvent args)
|
|
{
|
|
if (!TryComp(args.User, out ActorComponent? actor))
|
|
{
|
|
return;
|
|
}
|
|
|
|
args.Handled = true;
|
|
_ui.TryOpen(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key, actor.PlayerSession);
|
|
}
|
|
|
|
private void OnAfterInteractUsing(EntityUid uid, SharedDisposalUnitComponent component, AfterInteractUsingEvent args)
|
|
{
|
|
if (args.Handled || !args.CanReach)
|
|
return;
|
|
|
|
if (!HasComp<HandsComponent>(args.User))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CanInsert(uid, component, args.Used) || !_handsSystem.TryDropIntoContainer(args.User, args.Used, component.Container))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User):player} inserted {ToPrettyString(args.Used)} into {ToPrettyString(uid)}");
|
|
AfterInsert(uid, component, args.Used, args.User);
|
|
args.Handled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Thrown items have a chance of bouncing off the unit and not going in.
|
|
/// </summary>
|
|
private void OnThrowCollide(EntityUid uid, SharedDisposalUnitComponent component, ThrowHitByEvent args)
|
|
{
|
|
var canInsert = CanInsert(uid, component, args.Thrown);
|
|
var randDouble = _robustRandom.NextDouble();
|
|
|
|
if (!canInsert)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (randDouble > 0.75)
|
|
{
|
|
_audioSystem.PlayPvs(component.MissSound, uid);
|
|
|
|
_popupSystem.PopupEntity(Loc.GetString("disposal-unit-thrown-missed"), uid);
|
|
return;
|
|
}
|
|
|
|
var inserted = _containerSystem.Insert(args.Thrown, component.Container);
|
|
|
|
if (!inserted)
|
|
{
|
|
throw new InvalidOperationException("Container insertion failed but CanInsert returned true");
|
|
}
|
|
|
|
if (args.Component.Thrower != null)
|
|
_adminLogger.Add(LogType.Landed, LogImpact.Low, $"{ToPrettyString(args.Thrown)} thrown by {ToPrettyString(args.Component.Thrower.Value):player} landed in {ToPrettyString(uid)}");
|
|
|
|
AfterInsert(uid, component, args.Thrown);
|
|
}
|
|
|
|
private void OnDisposalInit(EntityUid uid, SharedDisposalUnitComponent component, ComponentInit args)
|
|
{
|
|
component.Container = _containerSystem.EnsureContainer<Container>(uid, SharedDisposalUnitComponent.ContainerId);
|
|
|
|
UpdateInterface(uid, component, component.Powered);
|
|
}
|
|
|
|
private void OnPowerChange(EntityUid uid, SharedDisposalUnitComponent component, ref PowerChangedEvent args)
|
|
{
|
|
if (!component.Running || args.Powered == component.Powered)
|
|
return;
|
|
|
|
component.Powered = args.Powered;
|
|
UpdateVisualState(uid, component);
|
|
UpdateInterface(uid, component, args.Powered);
|
|
|
|
if (!args.Powered)
|
|
{
|
|
component.NextFlush = null;
|
|
Dirty(component);
|
|
return;
|
|
}
|
|
|
|
if (component.Engaged && !TryFlush(uid, component))
|
|
{
|
|
QueueAutomaticEngage(uid, component);
|
|
}
|
|
}
|
|
|
|
// TODO: This should just use the same thing as entity storage?
|
|
private void OnMovement(EntityUid uid, SharedDisposalUnitComponent component, ref ContainerRelayMovementEntityEvent args)
|
|
{
|
|
var currentTime = GameTiming.CurTime;
|
|
|
|
if (!TryComp(args.Entity, out HandsComponent? hands) ||
|
|
hands.Count == 0 ||
|
|
currentTime < component.LastExitAttempt + ExitAttemptDelay)
|
|
{
|
|
return;
|
|
}
|
|
|
|
component.LastExitAttempt = currentTime;
|
|
Remove(uid, component, args.Entity);
|
|
}
|
|
|
|
private void OnAnchorChanged(EntityUid uid, SharedDisposalUnitComponent component, ref AnchorStateChangedEvent args)
|
|
{
|
|
if (Terminating(uid))
|
|
return;
|
|
|
|
UpdateVisualState(uid, component);
|
|
if (!args.Anchored)
|
|
TryEjectContents(uid, component);
|
|
}
|
|
|
|
private void OnDestruction(EntityUid uid, SharedDisposalUnitComponent component, DestructionEventArgs args)
|
|
{
|
|
TryEjectContents(uid, component);
|
|
}
|
|
|
|
private void OnDragDropOn(EntityUid uid, SharedDisposalUnitComponent component, ref DragDropTargetEvent args)
|
|
{
|
|
args.Handled = TryInsert(uid, args.Dragged, args.User);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void UpdateState(EntityUid uid, DisposalsPressureState state, SharedDisposalUnitComponent component, MetaDataComponent metadata)
|
|
{
|
|
if (component.State == state)
|
|
return;
|
|
|
|
component.State = state;
|
|
UpdateVisualState(uid, component);
|
|
UpdateInterface(uid, component, component.Powered);
|
|
Dirty(component, metadata);
|
|
|
|
if (state == DisposalsPressureState.Ready)
|
|
{
|
|
component.NextPressurized = TimeSpan.Zero;
|
|
|
|
// Manually engaged
|
|
if (component.Engaged)
|
|
{
|
|
component.NextFlush = GameTiming.CurTime + component.ManualFlushTime;
|
|
}
|
|
else if (component.Container.ContainedEntities.Count > 0)
|
|
{
|
|
component.NextFlush = GameTiming.CurTime + component.AutomaticEngageTime;
|
|
}
|
|
else
|
|
{
|
|
component.NextFlush = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Work out if we can stop updating this disposals component i.e. full pressure and nothing colliding.
|
|
/// </summary>
|
|
private void Update(EntityUid uid, SharedDisposalUnitComponent component, MetaDataComponent metadata, float frameTime)
|
|
{
|
|
var state = GetState(uid, component, metadata);
|
|
|
|
// Pressurizing, just check if we need a state update.
|
|
if (component.NextPressurized > GameTiming.CurTime)
|
|
{
|
|
UpdateState(uid, state, component, metadata);
|
|
return;
|
|
}
|
|
|
|
if (component.NextFlush != null)
|
|
{
|
|
if (component.NextFlush.Value < GameTiming.CurTime)
|
|
{
|
|
TryFlush(uid, component);
|
|
}
|
|
}
|
|
|
|
UpdateState(uid, state, component, metadata);
|
|
|
|
Box2? disposalsBounds = null;
|
|
var count = component.RecentlyEjected.Count;
|
|
|
|
if (count > 0)
|
|
{
|
|
if (!HasComp<PhysicsComponent>(uid))
|
|
{
|
|
component.RecentlyEjected.Clear();
|
|
}
|
|
else
|
|
{
|
|
disposalsBounds = _lookup.GetWorldAABB(uid);
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < component.RecentlyEjected.Count; i++)
|
|
{
|
|
var ejectedId = component.RecentlyEjected[i];
|
|
if (HasComp<PhysicsComponent>(ejectedId))
|
|
{
|
|
// TODO: We need to use a specific collision method (which sloth hasn't coded yet) for actual bounds overlaps.
|
|
// TODO: Come do this sloth :^)
|
|
// Check for itemcomp as we won't just block the disposal unit "sleeping" for something it can't collide with anyway.
|
|
if (!HasComp<ItemComponent>(ejectedId)
|
|
&& _lookup.GetWorldAABB(ejectedId).Intersects(disposalsBounds!.Value))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
component.RecentlyEjected.RemoveAt(i);
|
|
i--;
|
|
}
|
|
}
|
|
|
|
if (count != component.RecentlyEjected.Count)
|
|
Dirty(component, metadata);
|
|
}
|
|
|
|
public bool TryInsert(EntityUid unitId, EntityUid toInsertId, EntityUid? userId, DisposalUnitComponent? unit = null)
|
|
{
|
|
if (!Resolve(unitId, ref unit))
|
|
return false;
|
|
|
|
if (userId.HasValue && !HasComp<HandsComponent>(userId) && toInsertId != userId) // Mobs like mouse can Jump inside even with no hands
|
|
{
|
|
_popupSystem.PopupEntity(Loc.GetString("disposal-unit-no-hands"), userId.Value, userId.Value, PopupType.SmallCaution);
|
|
return false;
|
|
}
|
|
|
|
if (!CanInsert(unitId, unit, toInsertId))
|
|
return false;
|
|
|
|
bool insertingSelf = userId == toInsertId;
|
|
|
|
var delay = insertingSelf ? unit.EntryDelay : unit.DraggedEntryDelay;
|
|
|
|
if (userId != null && !insertingSelf)
|
|
_popupSystem.PopupEntity(Loc.GetString("disposal-unit-being-inserted", ("user", Identity.Entity((EntityUid) userId, EntityManager))), toInsertId, toInsertId, PopupType.Large);
|
|
|
|
if (delay <= 0 || userId == null)
|
|
{
|
|
AfterInsert(unitId, unit, toInsertId, userId, doInsert: true);
|
|
return true;
|
|
}
|
|
|
|
// Can't check if our target AND disposals moves currently so we'll just check target.
|
|
// if you really want to check if disposals moves then add a predicate.
|
|
var doAfterArgs = new DoAfterArgs(EntityManager, userId.Value, delay, new DisposalDoAfterEvent(), unitId, target: toInsertId, used: unitId)
|
|
{
|
|
BreakOnDamage = true,
|
|
BreakOnTargetMove = true,
|
|
BreakOnUserMove = true,
|
|
NeedHand = false
|
|
};
|
|
|
|
_doAfterSystem.TryStartDoAfter(doAfterArgs);
|
|
return true;
|
|
}
|
|
|
|
|
|
public bool TryFlush(EntityUid uid, SharedDisposalUnitComponent component)
|
|
{
|
|
if (!CanFlush(uid, component))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (component.NextFlush != null)
|
|
component.NextFlush = component.NextFlush.Value + component.AutomaticEngageTime;
|
|
|
|
var beforeFlushArgs = new BeforeDisposalFlushEvent();
|
|
RaiseLocalEvent(uid, beforeFlushArgs);
|
|
|
|
if (beforeFlushArgs.Cancelled)
|
|
{
|
|
Disengage(uid, component);
|
|
return false;
|
|
}
|
|
|
|
var xform = Transform(uid);
|
|
if (!TryComp(xform.GridUid, out MapGridComponent? grid))
|
|
return false;
|
|
|
|
var coords = xform.Coordinates;
|
|
var entry = grid.GetLocal(coords)
|
|
.FirstOrDefault(HasComp<DisposalEntryComponent>);
|
|
|
|
if (entry == default || component is not DisposalUnitComponent sDisposals)
|
|
{
|
|
component.Engaged = false;
|
|
Dirty(uid, component);
|
|
return false;
|
|
}
|
|
|
|
HandleAir(uid, sDisposals, xform);
|
|
|
|
_disposalTubeSystem.TryInsert(entry, sDisposals, beforeFlushArgs.Tags);
|
|
|
|
component.NextPressurized = GameTiming.CurTime;
|
|
if (!component.DisablePressure)
|
|
component.NextPressurized += TimeSpan.FromSeconds(1f / PressurePerSecond);
|
|
|
|
component.Engaged = false;
|
|
// stop queuing NOW
|
|
component.NextFlush = null;
|
|
|
|
UpdateVisualState(uid, component, true);
|
|
UpdateInterface(uid, component, component.Powered);
|
|
|
|
Dirty(uid, component);
|
|
|
|
return true;
|
|
}
|
|
|
|
private void HandleAir(EntityUid uid, DisposalUnitComponent component, TransformComponent xform)
|
|
{
|
|
var air = component.Air;
|
|
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, xform));
|
|
|
|
if (_atmosSystem.GetTileMixture(xform.GridUid, xform.MapUid, indices, true) is { Temperature: > 0f } environment)
|
|
{
|
|
var transferMoles = 0.1f * (0.25f * Atmospherics.OneAtmosphere * 1.01f - air.Pressure) * air.Volume / (environment.Temperature * Atmospherics.R);
|
|
|
|
component.Air = environment.Remove(transferMoles);
|
|
}
|
|
}
|
|
|
|
public void UpdateInterface(EntityUid uid, SharedDisposalUnitComponent component, bool powered)
|
|
{
|
|
var compState = GetState(uid, component);
|
|
var stateString = Loc.GetString($"disposal-unit-state-{compState}");
|
|
var state = new SharedDisposalUnitComponent.DisposalUnitBoundUserInterfaceState(Name(uid), stateString, EstimatedFullPressure(uid, component), powered, component.Engaged);
|
|
_ui.TrySetUiState(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key, state);
|
|
|
|
var stateUpdatedEvent = new DisposalUnitUIStateUpdatedEvent(state);
|
|
RaiseLocalEvent(uid, stateUpdatedEvent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the estimated time when the disposal unit will be back to full pressure.
|
|
/// </summary>
|
|
private TimeSpan EstimatedFullPressure(EntityUid uid, SharedDisposalUnitComponent component)
|
|
{
|
|
if (component.NextPressurized < GameTiming.CurTime)
|
|
return TimeSpan.Zero;
|
|
|
|
return component.NextPressurized;
|
|
}
|
|
|
|
public void UpdateVisualState(EntityUid uid, SharedDisposalUnitComponent component, bool flush = false)
|
|
{
|
|
if (!TryComp(uid, out AppearanceComponent? appearance))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Transform(uid).Anchored)
|
|
{
|
|
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.UnAnchored, appearance);
|
|
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Handle, SharedDisposalUnitComponent.HandleState.Normal, appearance);
|
|
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Light, SharedDisposalUnitComponent.LightStates.Off, appearance);
|
|
return;
|
|
}
|
|
|
|
var state = GetState(uid, component);
|
|
|
|
switch (state)
|
|
{
|
|
case DisposalsPressureState.Flushed:
|
|
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.OverlayFlushing, appearance);
|
|
break;
|
|
case DisposalsPressureState.Pressurizing:
|
|
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.OverlayCharging, appearance);
|
|
break;
|
|
case DisposalsPressureState.Ready:
|
|
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Anchored, appearance);
|
|
break;
|
|
}
|
|
|
|
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Handle, component.Engaged
|
|
? SharedDisposalUnitComponent.HandleState.Engaged
|
|
: SharedDisposalUnitComponent.HandleState.Normal, appearance);
|
|
|
|
if (!component.Powered)
|
|
{
|
|
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Light, SharedDisposalUnitComponent.LightStates.Off, appearance);
|
|
return;
|
|
}
|
|
|
|
var lightState = SharedDisposalUnitComponent.LightStates.Off;
|
|
|
|
if (component.Container.ContainedEntities.Count > 0)
|
|
{
|
|
lightState |= SharedDisposalUnitComponent.LightStates.Full;
|
|
}
|
|
|
|
if (state is DisposalsPressureState.Pressurizing or DisposalsPressureState.Flushed)
|
|
{
|
|
lightState |= SharedDisposalUnitComponent.LightStates.Charging;
|
|
}
|
|
else
|
|
{
|
|
lightState |= SharedDisposalUnitComponent.LightStates.Ready;
|
|
}
|
|
|
|
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Light, lightState, appearance);
|
|
}
|
|
|
|
public void Remove(EntityUid uid, SharedDisposalUnitComponent component, EntityUid toRemove)
|
|
{
|
|
_containerSystem.Remove(toRemove, component.Container);
|
|
|
|
if (component.Container.ContainedEntities.Count == 0)
|
|
{
|
|
// If not manually engaged then reset the flushing entirely.
|
|
if (!component.Engaged)
|
|
{
|
|
component.NextFlush = null;
|
|
}
|
|
}
|
|
|
|
if (!component.RecentlyEjected.Contains(toRemove))
|
|
component.RecentlyEjected.Add(toRemove);
|
|
|
|
UpdateVisualState(uid, component);
|
|
Dirty(uid, component);
|
|
}
|
|
|
|
public bool CanFlush(EntityUid unit, SharedDisposalUnitComponent component)
|
|
{
|
|
return GetState(unit, component) == DisposalsPressureState.Ready
|
|
&& component.Powered
|
|
&& Comp<TransformComponent>(unit).Anchored;
|
|
}
|
|
|
|
public void ManualEngage(EntityUid uid, SharedDisposalUnitComponent component, MetaDataComponent? metadata = null)
|
|
{
|
|
component.Engaged = true;
|
|
UpdateVisualState(uid, component);
|
|
UpdateInterface(uid, component, component.Powered);
|
|
Dirty(uid, component);
|
|
|
|
if (!CanFlush(uid, component))
|
|
return;
|
|
|
|
if (!Resolve(uid, ref metadata))
|
|
return;
|
|
|
|
var pauseTime = Metadata.GetPauseTime(uid, metadata);
|
|
var nextEngage = GameTiming.CurTime - pauseTime + component.ManualFlushTime;
|
|
component.NextFlush = TimeSpan.FromSeconds(Math.Min((component.NextFlush ?? TimeSpan.MaxValue).TotalSeconds, nextEngage.TotalSeconds));
|
|
}
|
|
|
|
public void Disengage(EntityUid uid, SharedDisposalUnitComponent component)
|
|
{
|
|
component.Engaged = false;
|
|
|
|
if (component.Container.ContainedEntities.Count == 0)
|
|
{
|
|
component.NextFlush = null;
|
|
}
|
|
|
|
UpdateVisualState(uid, component);
|
|
UpdateInterface(uid, component, component.Powered);
|
|
Dirty(uid, component);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove all entities currently in the disposal unit.
|
|
/// </summary>
|
|
public void TryEjectContents(EntityUid uid, SharedDisposalUnitComponent component)
|
|
{
|
|
foreach (var entity in component.Container.ContainedEntities.ToArray())
|
|
{
|
|
Remove(uid, component, entity);
|
|
}
|
|
|
|
if (!component.Engaged)
|
|
{
|
|
component.NextFlush = null;
|
|
Dirty(uid, component);
|
|
}
|
|
}
|
|
|
|
public override bool HasDisposals(EntityUid? uid)
|
|
{
|
|
return HasComp<DisposalUnitComponent>(uid);
|
|
}
|
|
|
|
public override bool ResolveDisposals(EntityUid uid, [NotNullWhen(true)] ref SharedDisposalUnitComponent? component)
|
|
{
|
|
if (component != null)
|
|
return true;
|
|
|
|
TryComp<DisposalUnitComponent>(uid, out var storage);
|
|
component = storage;
|
|
return component != null;
|
|
}
|
|
|
|
public override bool CanInsert(EntityUid uid, SharedDisposalUnitComponent component, EntityUid entity)
|
|
{
|
|
if (!base.CanInsert(uid, component, entity))
|
|
return false;
|
|
|
|
return _containerSystem.CanInsert(entity, component.Container);
|
|
}
|
|
|
|
/// <summary>
|
|
/// If something is inserted (or the likes) then we'll queue up an automatic flush in the future.
|
|
/// </summary>
|
|
public void QueueAutomaticEngage(EntityUid uid, SharedDisposalUnitComponent component, MetaDataComponent? metadata = null)
|
|
{
|
|
if (component.Deleted || !component.AutomaticEngage || !component.Powered && component.Container.ContainedEntities.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var pauseTime = Metadata.GetPauseTime(uid, metadata);
|
|
var automaticTime = GameTiming.CurTime + component.AutomaticEngageTime - pauseTime;
|
|
var flushTime = TimeSpan.FromSeconds(Math.Min((component.NextFlush ?? TimeSpan.MaxValue).TotalSeconds, automaticTime.TotalSeconds));
|
|
|
|
component.NextFlush = flushTime;
|
|
Dirty(component);
|
|
}
|
|
|
|
public void AfterInsert(EntityUid uid, SharedDisposalUnitComponent component, EntityUid inserted, EntityUid? user = null, bool doInsert = false)
|
|
{
|
|
_audioSystem.PlayPvs(component.InsertSound, uid);
|
|
|
|
if (doInsert && !_containerSystem.Insert(inserted, component.Container))
|
|
return;
|
|
|
|
if (user != inserted && user != null)
|
|
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user.Value):player} inserted {ToPrettyString(inserted)} into {ToPrettyString(uid)}");
|
|
|
|
QueueAutomaticEngage(uid, component);
|
|
|
|
if (TryComp(inserted, out ActorComponent? actor))
|
|
{
|
|
_ui.TryClose(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key, actor.PlayerSession);
|
|
}
|
|
|
|
// Maybe do pullable instead? Eh still fine.
|
|
Joints.RecursiveClearJoints(inserted);
|
|
UpdateVisualState(uid, component);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sent before the disposal unit flushes it's contents.
|
|
/// Allows adding tags for sorting and preventing the disposal unit from flushing.
|
|
/// </summary>
|
|
public sealed class DisposalUnitUIStateUpdatedEvent : EntityEventArgs
|
|
{
|
|
public SharedDisposalUnitComponent.DisposalUnitBoundUserInterfaceState State;
|
|
|
|
public DisposalUnitUIStateUpdatedEvent(SharedDisposalUnitComponent.DisposalUnitBoundUserInterfaceState state)
|
|
{
|
|
State = state;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sent before the disposal unit flushes it's contents.
|
|
/// Allows adding tags for sorting and preventing the disposal unit from flushing.
|
|
/// </summary>
|
|
public sealed class BeforeDisposalFlushEvent : CancellableEntityEventArgs
|
|
{
|
|
public readonly List<string> Tags = new();
|
|
}
|