Re-organize all projects (#4166)

This commit is contained in:
DrSmugleaf
2021-06-09 22:19:39 +02:00
committed by GitHub
parent 9f50e4061b
commit ff1a2d97ea
1773 changed files with 5258 additions and 5508 deletions

View File

@@ -0,0 +1,788 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Anchor;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Connections;
using Content.Server.Disposal.Tube.Components;
using Content.Server.Disposal.Unit.Components;
using Content.Server.DoAfter;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Body.Components;
using Content.Shared.Configurable;
using Content.Shared.Disposal.Components;
using Content.Shared.DragDrop;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.Disposal.Mailing
{
[RegisterComponent]
[ComponentReference(typeof(SharedDisposalMailingUnitComponent))]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(IInteractUsing))]
public class DisposalMailingUnitComponent : SharedDisposalMailingUnitComponent, IInteractHand, IActivate, IInteractUsing, IDragDropOn
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
private const string HolderPrototypeId = "DisposalHolder";
/// <summary>
/// The delay for an entity trying to move out of this unit.
/// </summary>
private static readonly TimeSpan ExitAttemptDelay = TimeSpan.FromSeconds(0.5);
/// <summary>
/// Last time that an entity tried to exit this disposal unit.
/// </summary>
[ViewVariables]
private TimeSpan _lastExitAttempt;
public static readonly Regex TagRegex = new("^[a-zA-Z0-9, ]*$", RegexOptions.Compiled);
/// <summary>
/// The current pressure of this disposal unit.
/// Prevents it from flushing if it is not equal to or bigger than 1.
/// </summary>
[ViewVariables]
[DataField("pressure")]
private float _pressure = 1f;
private bool _engaged;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("autoEngageTime")]
private readonly TimeSpan _automaticEngageTime = TimeSpan.FromSeconds(30);
[ViewVariables(VVAccess.ReadWrite)]
[DataField("flushDelay")]
private readonly TimeSpan _flushDelay = TimeSpan.FromSeconds(3);
[ViewVariables(VVAccess.ReadWrite)]
[DataField("entryDelay")]
private float _entryDelay = 0.5f;
/// <summary>
/// Token used to cancel the automatic engage of a disposal unit
/// after an entity enters it.
/// </summary>
private CancellationTokenSource? _automaticEngageToken;
/// <summary>
/// Container of entities inside this disposal unit.
/// </summary>
[ViewVariables]
private Container _container = default!;
[ViewVariables]
private WiredNetworkConnection? _connection;
[ViewVariables] public IReadOnlyList<IEntity> ContainedEntities => _container.ContainedEntities;
[ViewVariables]
private readonly List<string> _targetList = new();
[ViewVariables]
private string? _target;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("Tag")]
private string _tag = string.Empty;
[ViewVariables]
public bool Powered =>
!Owner.TryGetComponent(out PowerReceiverComponent? receiver) ||
receiver.Powered;
[ViewVariables]
private PressureState State => _pressure >= 1 ? PressureState.Ready : PressureState.Pressurizing;
[ViewVariables(VVAccess.ReadWrite)]
private bool Engaged
{
get => _engaged;
set
{
var oldEngaged = _engaged;
_engaged = value;
if (oldEngaged == value)
{
return;
}
UpdateVisualState();
}
}
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(DisposalMailingUnitUiKey.Key);
/// <summary>
/// Store the translated state.
/// </summary>
private (PressureState State, string Localized) _locState;
public override bool CanInsert(IEntity entity)
{
if (!Anchored)
{
return false;
}
if (!entity.TryGetComponent(out IPhysBody? physics) ||
!physics.CanCollide)
{
return false;
}
if (!entity.HasComponent<ItemComponent>() &&
!entity.HasComponent<IBody>())
{
return false;
}
return _container.CanInsert(entity);
}
private void TryQueueEngage()
{
if (!Powered && ContainedEntities.Count == 0)
{
return;
}
_automaticEngageToken = new CancellationTokenSource();
Timer.Spawn(_automaticEngageTime, () =>
{
if (!TryFlush())
{
TryQueueEngage();
}
}, _automaticEngageToken.Token);
}
private void AfterInsert(IEntity entity)
{
TryQueueEngage();
if (entity.TryGetComponent(out ActorComponent? actor))
{
UserInterface?.Close(actor.PlayerSession);
}
UpdateVisualState();
}
public async Task<bool> TryInsert(IEntity entity, IEntity? user = default)
{
if (!CanInsert(entity))
return false;
if (user != null && _entryDelay > 0f)
{
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
var doAfterArgs = new DoAfterEventArgs(user, _entryDelay, default, Owner)
{
BreakOnDamage = true,
BreakOnStun = true,
BreakOnTargetMove = true,
BreakOnUserMove = true,
NeedHand = false,
};
var result = await doAfterSystem.DoAfter(doAfterArgs);
if (result == DoAfterStatus.Cancelled)
return false;
}
if (!_container.Insert(entity))
return false;
AfterInsert(entity);
return true;
}
private bool TryDrop(IEntity user, IEntity entity)
{
if (!user.TryGetComponent(out HandsComponent? hands))
{
return false;
}
if (!CanInsert(entity) || !hands.Drop(entity, _container))
{
return false;
}
AfterInsert(entity);
return true;
}
private void Remove(IEntity entity)
{
_container.Remove(entity);
if (ContainedEntities.Count == 0)
{
_automaticEngageToken?.Cancel();
_automaticEngageToken = null;
}
UpdateVisualState();
}
private bool CanFlush()
{
return _pressure >= 1 && Powered && Anchored;
}
private void ToggleEngage()
{
Engaged ^= true;
if (Engaged && CanFlush())
{
Timer.Spawn(_flushDelay, () => TryFlush());
}
}
public bool TryFlush()
{
if (!CanFlush())
{
return false;
}
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
var coords = Owner.Transform.Coordinates;
var entry = grid.GetLocal(coords)
.FirstOrDefault(entity => Owner.EntityManager.ComponentManager.HasComponent<DisposalEntryComponent>(entity));
if (entry == default)
{
return false;
}
var entryComponent = Owner.EntityManager.ComponentManager.GetComponent<DisposalEntryComponent>(entry);
var entities = _container.ContainedEntities.ToList();
foreach (var entity in _container.ContainedEntities.ToList())
{
_container.Remove(entity);
}
if (_target == null)
{
return false;
}
var holder = CreateTaggedHolder(entities, _target);
entryComponent.TryInsert(holder);
_automaticEngageToken?.Cancel();
_automaticEngageToken = null;
_pressure = 0;
Engaged = false;
UpdateVisualState(true);
UpdateInterface();
if (_connection != null)
{
var data = new Dictionary<string, string>
{
{ NetworkUtils.COMMAND, NET_CMD_SENT },
{ NET_SRC, _tag },
{ NET_TARGET, _target }
};
_connection.Broadcast(_connection.Frequency, data);
}
return true;
}
private DisposalHolderComponent CreateTaggedHolder(IReadOnlyCollection<IEntity> entities, string tag)
{
var holder = Owner.EntityManager.SpawnEntity(HolderPrototypeId, Owner.Transform.MapPosition);
var holderComponent = holder.GetComponent<DisposalHolderComponent>();
holderComponent.Tags.Add(tag);
holderComponent.Tags.Add(TAGS_MAIL);
foreach (var entity in entities)
{
holderComponent.TryInsert(entity);
}
return holderComponent;
}
private void UpdateTargetList()
{
_targetList.Clear();
var payload = new Dictionary<string, string>
{
{ NetworkUtils.COMMAND, NET_CMD_REQUEST }
};
_connection?.Broadcast(_connection.Frequency, payload);
}
private void TryEjectContents()
{
foreach (var entity in _container.ContainedEntities.ToArray())
{
Remove(entity);
}
}
private void TogglePower()
{
if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver))
{
return;
}
receiver.PowerDisabled = !receiver.PowerDisabled;
UpdateInterface();
}
private void UpdateInterface()
{
string stateString;
stateString = Loc.GetString($"{State}");
var state = new DisposalUnitBoundUserInterfaceState(Owner.Name, stateString, _pressure, Powered, Engaged);
UserInterface?.SetState(state);
}
private bool PlayerCanUse(IEntity? player)
{
if (player == null)
{
return false;
}
if (!ActionBlockerSystem.CanInteract(player) ||
!ActionBlockerSystem.CanUse(player))
{
return false;
}
return true;
}
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (obj.Session.AttachedEntity == null)
{
return;
}
if (!PlayerCanUse(obj.Session.AttachedEntity))
{
return;
}
if (obj.Message is UiButtonPressedMessage buttonMessage)
{
switch (buttonMessage.Button)
{
case UiButton.Eject:
TryEjectContents();
break;
case UiButton.Engage:
ToggleEngage();
break;
case UiButton.Power:
TogglePower();
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
break;
default:
throw new ArgumentOutOfRangeException();
}
}
if (obj.Message is UiTargetUpdateMessage tagMessage && TagRegex.IsMatch(tagMessage.Target ?? string.Empty))
{
_target = tagMessage.Target;
}
}
private void OnConfigUpdate(Dictionary<string, string> config)
{
if (config.TryGetValue("Tag", out var tag))
_tag = tag;
}
public void UpdateVisualState()
{
UpdateVisualState(false);
}
private void UpdateVisualState(bool flush)
{
if (!Owner.TryGetComponent(out AppearanceComponent? appearance))
{
return;
}
if (!Anchored)
{
appearance.SetData(Visuals.VisualState, VisualState.UnAnchored);
appearance.SetData(Visuals.Handle, HandleState.Normal);
appearance.SetData(Visuals.Light, LightState.Off);
return;
}
else if (_pressure < 1)
{
appearance.SetData(Visuals.VisualState, VisualState.Charging);
}
else
{
appearance.SetData(Visuals.VisualState, VisualState.Anchored);
}
appearance.SetData(Visuals.Handle, Engaged
? HandleState.Engaged
: HandleState.Normal);
if (!Powered)
{
appearance.SetData(Visuals.Light, LightState.Off);
return;
}
if (flush)
{
appearance.SetData(Visuals.VisualState, VisualState.Flushing);
appearance.SetData(Visuals.Light, LightState.Off);
return;
}
if (ContainedEntities.Count > 0)
{
appearance.SetData(Visuals.Light, LightState.Full);
return;
}
appearance.SetData(Visuals.Light, _pressure < 1
? LightState.Charging
: LightState.Ready);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!Powered)
{
return;
}
var oldPressure = _pressure;
_pressure = _pressure + frameTime > 1
? 1
: _pressure + 0.05f * frameTime;
if (oldPressure < 1 && _pressure >= 1)
{
UpdateVisualState();
if (Engaged)
{
TryFlush();
}
}
if (_pressure < 1.0f || oldPressure < 1.0f && _pressure >= 1.0f)
{
UpdateInterface();
}
}
private void PowerStateChanged(PowerChangedMessage args)
{
if (!args.Powered)
{
_automaticEngageToken?.Cancel();
_automaticEngageToken = null;
}
UpdateVisualState();
if (Engaged && !TryFlush())
{
TryQueueEngage();
}
}
public override void Initialize()
{
base.Initialize();
_container = ContainerHelpers.EnsureContainer<Container>(Owner, Name);
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
}
_connection = new WiredNetworkConnection(OnReceiveNetMessage, false, Owner);
UpdateInterface();
}
protected override void Startup()
{
base.Startup();
if(!Owner.HasComponent<AnchorableComponent>())
{
Logger.WarningS("VitalComponentMissing", $"Disposal unit {Owner.Uid} is missing an anchorable component");
}
UpdateTargetList();
UpdateVisualState();
UpdateInterface();
}
public override void OnRemove()
{
if (_container != null)
{
foreach (var entity in _container.ContainedEntities.ToArray())
{
_container.ForceRemove(entity);
}
}
UserInterface?.CloseAll();
_automaticEngageToken?.Cancel();
_automaticEngageToken = null;
_container = null!;
_connection!.Close();
base.OnRemove();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case SharedConfigurationComponent.ConfigUpdatedComponentMessage msg:
OnConfigUpdate(msg.Config);
break;
case RelayMovementEntityMessage msg:
if (!msg.Entity.TryGetComponent(out HandsComponent? hands) ||
hands.Count == 0 ||
_gameTiming.CurTime < _lastExitAttempt + ExitAttemptDelay)
{
break;
}
_lastExitAttempt = _gameTiming.CurTime;
Remove(msg.Entity);
break;
case PowerChangedMessage powerChanged:
PowerStateChanged(powerChanged);
break;
}
}
private void OnReceiveNetMessage(int frequency, string sender, IReadOnlyDictionary<string, string> payload, object _, bool broadcast)
{
if (payload.TryGetValue(NetworkUtils.COMMAND, out var command) && Powered)
{
if (command == NET_CMD_RESPONSE && payload.TryGetValue(NET_TAG, out var tag))
{
_targetList.Add(tag);
UpdateInterface();
}
if (command == NET_CMD_REQUEST)
{
if (_tag == "" || !Powered)
return;
var data = new Dictionary<string, string>
{
{NetworkUtils.COMMAND, NET_CMD_RESPONSE},
{NET_TAG, _tag}
};
_connection?.Send(frequency, sender, data);
}
}
}
private bool IsValidInteraction(ITargetedInteractEventArgs eventArgs)
{
if (!ActionBlockerSystem.CanInteract(eventArgs.User))
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("You can't do that!"));
return false;
}
if (eventArgs.User.IsInContainer())
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("You can't reach there!"));
return false;
}
// This popup message doesn't appear on clicks, even when code was seperate. Unsure why.
if (!eventArgs.User.HasComponent<IHandsComponent>())
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("You have no hands!"));
return false;
}
return true;
}
bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs)
{
if (eventArgs.User == null)
{
return false;
}
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return false;
}
// Duplicated code here, not sure how else to get actor inside to make UserInterface happy.
if (IsValidInteraction(eventArgs))
{
UpdateTargetList();
UpdateInterface();
UserInterface?.Open(actor.PlayerSession);
return true;
}
return false;
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (IsValidInteraction(eventArgs))
{
UserInterface?.Open(actor.PlayerSession);
}
return;
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
return TryDrop(eventArgs.User, eventArgs.Using);
}
public override bool CanDragDropOn(DragDropEvent eventArgs)
{
return CanInsert(eventArgs.Dragged);
}
public override bool DragDropOn(DragDropEvent eventArgs)
{
_ = TryInsert(eventArgs.Dragged, eventArgs.User);
return true;
}
[Verb]
private sealed class SelfInsertVerb : Verb<DisposalMailingUnitComponent>
{
protected override void GetData(IEntity user, DisposalMailingUnitComponent component, VerbData data)
{
data.Visibility = VerbVisibility.Invisible;
if (!ActionBlockerSystem.CanInteract(user) ||
component.ContainedEntities.Contains(user))
{
return;
}
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("Jump inside");
}
protected override void Activate(IEntity user, DisposalMailingUnitComponent component)
{
_ = component.TryInsert(user, user);
}
}
[Verb]
private sealed class FlushVerb : Verb<DisposalMailingUnitComponent>
{
protected override void GetData(IEntity user, DisposalMailingUnitComponent component, VerbData data)
{
data.Visibility = VerbVisibility.Invisible;
if (!ActionBlockerSystem.CanInteract(user) ||
component.ContainedEntities.Contains(user))
{
return;
}
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("Flush");
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
}
protected override void Activate(IEntity user, DisposalMailingUnitComponent component)
{
component.Engaged = true;
component.TryFlush();
}
}
}
}

View File

@@ -0,0 +1,29 @@
using Robust.Shared.GameObjects;
namespace Content.Server.Disposal.Mailing
{
public sealed class DisposalMailingUnitSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalMailingUnitComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<DisposalMailingUnitComponent, PhysicsBodyTypeChangedEvent>();
}
private static void BodyTypeChanged(
EntityUid uid,
DisposalMailingUnitComponent component,
PhysicsBodyTypeChangedEvent args)
{
component.UpdateVisualState();
}
}
}

View File

@@ -0,0 +1,39 @@
using Content.Server.Disposal.Unit.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Disposal.Tube.Components
{
[RegisterComponent]
[ComponentReference(typeof(IDisposalTubeComponent))]
public class DisposalBendComponent : DisposalTubeComponent
{
[DataField("sideDegrees")]
private int _sideDegrees = -90;
public override string Name => "DisposalBend";
protected override Direction[] ConnectableDirections()
{
var direction = Owner.Transform.LocalRotation;
var side = new Angle(MathHelper.DegreesToRadians(direction.Degrees + _sideDegrees));
return new[] {direction.GetDir(), side.GetDir()};
}
public override Direction NextDirection(DisposalHolderComponent holder)
{
var directions = ConnectableDirections();
var previousTube = holder.PreviousTube;
if (previousTube == null)
{
return directions[0];
}
var previousDirection = DirectionTo(previousTube);
return previousDirection == directions[0] ? directions[1] : directions[0];
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Linq;
using Content.Server.Disposal.Unit.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Random;
namespace Content.Server.Disposal.Tube.Components
{
[RegisterComponent]
[ComponentReference(typeof(IDisposalTubeComponent))]
public class DisposalEntryComponent : DisposalTubeComponent
{
[Dependency] private readonly IRobustRandom _random = default!;
private const string HolderPrototypeId = "DisposalHolder";
public override string Name => "DisposalEntry";
public bool TryInsert(DisposalUnitComponent from)
{
var holder = Owner.EntityManager.SpawnEntity(HolderPrototypeId, Owner.Transform.MapPosition);
var holderComponent = holder.GetComponent<DisposalHolderComponent>();
foreach (var entity in from.ContainedEntities.ToArray())
{
holderComponent.TryInsert(entity);
}
holderComponent.Air.Merge(from.Air);
from.Air.Clear();
return TryInsert(holderComponent);
}
public bool TryInsert(DisposalHolderComponent holder)
{
if (!Contents.Insert(holder.Owner))
{
return false;
}
holder.EnterTube(this);
return true;
}
protected override Direction[] ConnectableDirections()
{
return new[] {Owner.Transform.LocalRotation.GetDir()};
}
/// <summary>
/// Ejects contents when they come from the same direction the entry is facing.
/// </summary>
public override Direction NextDirection(DisposalHolderComponent holder)
{
if (holder.PreviousTube != null && DirectionTo(holder.PreviousTube) == ConnectableDirections()[0])
{
var invalidDirections = new[] { ConnectableDirections()[0], Direction.Invalid };
var directions = Enum.GetValues(typeof(Direction))
.Cast<Direction>().Except(invalidDirections).ToList();
return _random.Pick(directions);
}
return ConnectableDirections()[0];
}
}
}

View File

@@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.Disposal.Unit.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Disposal.Tube.Components
{
[RegisterComponent]
[ComponentReference(typeof(IDisposalTubeComponent))]
public class DisposalJunctionComponent : DisposalTubeComponent
{
[Dependency] private readonly IRobustRandom _random = default!;
/// <summary>
/// The angles to connect to.
/// </summary>
[ViewVariables]
[DataField("degrees")]
private List<Angle> _degrees = new();
public override string Name => "DisposalJunction";
protected override Direction[] ConnectableDirections()
{
var direction = Owner.Transform.LocalRotation;
return _degrees.Select(degree => new Angle(degree.Theta + direction.Theta).GetDir()).ToArray();
}
public override Direction NextDirection(DisposalHolderComponent holder)
{
var next = Owner.Transform.LocalRotation.GetDir();
var directions = ConnectableDirections().Skip(1).ToArray();
if (holder.PreviousTube == null ||
DirectionTo(holder.PreviousTube) == next)
{
return _random.Pick(directions);
}
return next;
}
}
}

View File

@@ -0,0 +1,216 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Text;
using Content.Server.Disposal.Unit.Components;
using Content.Server.Hands.Components;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Verbs;
using Robust.Server.Console;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.ViewVariables;
using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent;
namespace Content.Server.Disposal.Tube.Components
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(IDisposalTubeComponent))]
public class DisposalRouterComponent : DisposalJunctionComponent, IActivate
{
public override string Name => "DisposalRouter";
[ViewVariables]
private readonly HashSet<string> _tags = new();
[ViewVariables]
public bool Anchored =>
!Owner.TryGetComponent(out IPhysBody? physics) ||
physics.BodyType == BodyType.Static;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(DisposalRouterUiKey.Key);
public override Direction NextDirection(DisposalHolderComponent holder)
{
var directions = ConnectableDirections();
if (holder.Tags.Overlaps(_tags))
{
return directions[1];
}
return Owner.Transform.LocalRotation.GetDir();
}
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
}
UpdateUserInterface();
}
/// <summary>
/// Handles ui messages from the client. For things such as button presses
/// which interact with the world and require server action.
/// </summary>
/// <param name="obj">A user interface message from the client.</param>
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (obj.Session.AttachedEntity == null)
{
return;
}
var msg = (UiActionMessage) obj.Message;
if (!PlayerCanUseDisposalTagger(obj.Session))
return;
//Check for correct message and ignore maleformed strings
if (msg.Action == UiAction.Ok && TagRegex.IsMatch(msg.Tags))
{
_tags.Clear();
foreach (var tag in msg.Tags.Split(',', StringSplitOptions.RemoveEmptyEntries))
{
_tags.Add(tag.Trim());
ClickSound();
}
}
}
/// <summary>
/// Checks whether the player entity is able to use the configuration interface of the pipe tagger.
/// </summary>
/// <param name="IPlayerSession">The player session.</param>
/// <returns>Returns true if the entity can use the configuration interface, and false if it cannot.</returns>
private bool PlayerCanUseDisposalTagger(IPlayerSession session)
{
//Need player entity to check if they are still able to use the configuration interface
if (session.AttachedEntity == null)
return false;
if (!Anchored)
return false;
var groupController = IoCManager.Resolve<IConGroupController>();
//Check if player can interact in their current state
if (!groupController.CanAdminMenu(session) && (!ActionBlockerSystem.CanInteract(session.AttachedEntity) || !ActionBlockerSystem.CanUse(session.AttachedEntity)))
return false;
return true;
}
/// <summary>
/// Gets component data to be used to update the user interface client-side.
/// </summary>
/// <returns>Returns a <see cref="DisposalRouterUserInterfaceState"/></returns>
private DisposalRouterUserInterfaceState GetUserInterfaceState()
{
if(_tags.Count <= 0)
{
return new DisposalRouterUserInterfaceState("");
}
var taglist = new StringBuilder();
foreach (var tag in _tags)
{
taglist.Append(tag);
taglist.Append(", ");
}
taglist.Remove(taglist.Length - 2, 2);
return new DisposalRouterUserInterfaceState(taglist.ToString());
}
private void UpdateUserInterface()
{
var state = GetUserInterfaceState();
UserInterface?.SetState(state);
}
private void ClickSound()
{
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
}
/// <summary>
/// Called when you click the owner entity with an empty hand. Opens the UI client-side if possible.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
void IActivate.Activate(ActivateEventArgs args)
{
if (!args.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (!args.User.TryGetComponent(out IHandsComponent? hands))
{
Owner.PopupMessage(args.User, Loc.GetString("You have no hands."));
return;
}
var activeHandEntity = hands.GetActiveHand?.Owner;
if (activeHandEntity == null)
{
OpenUserInterface(actor);
}
}
public override void OnRemove()
{
UserInterface?.CloseAll();
base.OnRemove();
}
private void OpenUserInterface(ActorComponent actor)
{
UpdateUserInterface();
UserInterface?.Open(actor.PlayerSession);
}
[Verb]
public sealed class ConfigureVerb : Verb<DisposalRouterComponent>
{
protected override void GetData(IEntity user, DisposalRouterComponent component, VerbData data)
{
var session = user.PlayerSession();
var groupController = IoCManager.Resolve<IConGroupController>();
if (session == null || !groupController.CanAdminMenu(session))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Open Configuration");
data.IconTexture = "/Textures/Interface/VerbIcons/settings.svg.192dpi.png";
}
protected override void Activate(IEntity user, DisposalRouterComponent component)
{
if (user.TryGetComponent(out ActorComponent? actor))
{
component.OpenUserInterface(actor);
}
}
}
}
}

View File

@@ -0,0 +1,182 @@
#nullable enable
using Content.Server.Disposal.Unit.Components;
using Content.Server.Hands.Components;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Verbs;
using Robust.Server.Console;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.ViewVariables;
using static Content.Shared.Disposal.Components.SharedDisposalTaggerComponent;
namespace Content.Server.Disposal.Tube.Components
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(IDisposalTubeComponent))]
public class DisposalTaggerComponent : DisposalTransitComponent, IActivate
{
public override string Name => "DisposalTagger";
[ViewVariables(VVAccess.ReadWrite)]
private string _tag = "";
[ViewVariables]
public bool Anchored =>
!Owner.TryGetComponent(out PhysicsComponent? physics) ||
physics.BodyType == BodyType.Static;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(DisposalTaggerUiKey.Key);
public override Direction NextDirection(DisposalHolderComponent holder)
{
holder.Tags.Add(_tag);
return base.NextDirection(holder);
}
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
}
UpdateUserInterface();
}
/// <summary>
/// Handles ui messages from the client. For things such as button presses
/// which interact with the world and require server action.
/// </summary>
/// <param name="obj">A user interface message from the client.</param>
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
var msg = (UiActionMessage) obj.Message;
if (!PlayerCanUseDisposalTagger(obj.Session))
return;
//Check for correct message and ignore maleformed strings
if (msg.Action == UiAction.Ok && TagRegex.IsMatch(msg.Tag))
{
_tag = msg.Tag;
ClickSound();
}
}
/// <summary>
/// Checks whether the player entity is able to use the configuration interface of the pipe tagger.
/// </summary>
/// <param name="IPlayerSession">The player entity.</param>
/// <returns>Returns true if the entity can use the configuration interface, and false if it cannot.</returns>
private bool PlayerCanUseDisposalTagger(IPlayerSession session)
{
//Need player entity to check if they are still able to use the configuration interface
if (session.AttachedEntity == null)
return false;
if (!Anchored)
return false;
var groupController = IoCManager.Resolve<IConGroupController>();
//Check if player can interact in their current state
if (!groupController.CanAdminMenu(session) && (!ActionBlockerSystem.CanInteract(session.AttachedEntity) || !ActionBlockerSystem.CanUse(session.AttachedEntity)))
return false;
return true;
}
/// <summary>
/// Gets component data to be used to update the user interface client-side.
/// </summary>
/// <returns>Returns a <see cref="DisposalTaggerUserInterfaceState"/></returns>
private DisposalTaggerUserInterfaceState GetUserInterfaceState()
{
return new(_tag);
}
private void UpdateUserInterface()
{
var state = GetUserInterfaceState();
UserInterface?.SetState(state);
}
private void ClickSound()
{
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
}
/// <summary>
/// Called when you click the owner entity with an empty hand. Opens the UI client-side if possible.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
void IActivate.Activate(ActivateEventArgs args)
{
if (!args.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (!args.User.TryGetComponent(out IHandsComponent? hands))
{
Owner.PopupMessage(args.User, Loc.GetString("You have no hands."));
return;
}
var activeHandEntity = hands.GetActiveHand?.Owner;
if (activeHandEntity == null)
{
OpenUserInterface(actor);
}
}
public override void OnRemove()
{
base.OnRemove();
UserInterface?.CloseAll();
}
[Verb]
public sealed class ConfigureVerb : Verb<DisposalTaggerComponent>
{
protected override void GetData(IEntity user, DisposalTaggerComponent component, VerbData data)
{
var groupController = IoCManager.Resolve<IConGroupController>();
if (!user.TryGetComponent(out ActorComponent? actor) || !groupController.CanAdminMenu(actor.PlayerSession))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Open Configuration");
data.IconTexture = "/Textures/Interface/VerbIcons/settings.svg.192dpi.png";
}
protected override void Activate(IEntity user, DisposalTaggerComponent component)
{
if (user.TryGetComponent(out ActorComponent? actor))
{
component.OpenUserInterface(actor);
}
}
}
private void OpenUserInterface(ActorComponent actor)
{
UpdateUserInterface();
UserInterface?.Open(actor.PlayerSession);
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using Content.Server.Disposal.Unit.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Content.Server.Disposal.Tube.Components
{
// TODO: Different types of tubes eject in random direction with no exit point
[RegisterComponent]
[ComponentReference(typeof(IDisposalTubeComponent))]
public class DisposalTransitComponent : DisposalTubeComponent
{
public override string Name => "DisposalTransit";
protected override Direction[] ConnectableDirections()
{
var rotation = Owner.Transform.LocalRotation;
var opposite = new Angle(rotation.Theta + Math.PI);
return new[] {rotation.GetDir(), opposite.GetDir()};
}
public override Direction NextDirection(DisposalHolderComponent holder)
{
var directions = ConnectableDirections();
var previousTube = holder.PreviousTube;
var forward = directions[0];
if (previousTube == null)
{
return forward;
}
var backward = directions[1];
return DirectionTo(previousTube) == forward ? backward : forward;
}
}
}

View File

@@ -0,0 +1,313 @@
#nullable enable
using System;
using System.Linq;
using Content.Server.Anchor;
using Content.Server.Disposal.Unit.Components;
using Content.Shared.Acts;
using Content.Shared.Disposal.Components;
using Content.Shared.Notification;
using Content.Shared.Verbs;
using Robust.Server.Console;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Content.Server.Disposal.Tube.Components
{
public abstract class DisposalTubeComponent : Component, IDisposalTubeComponent, IBreakAct
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
private static readonly TimeSpan ClangDelay = TimeSpan.FromSeconds(0.5);
private TimeSpan _lastClang;
private bool _connected;
private bool _broken;
[DataField("clangSound")]
private string _clangSound = "/Audio/Effects/clang.ogg";
/// <summary>
/// Container of entities that are currently inside this tube
/// </summary>
[ViewVariables]
public Container Contents { get; private set; } = default!;
[ViewVariables]
private bool Anchored =>
!Owner.TryGetComponent(out PhysicsComponent? physics) ||
physics.BodyType == BodyType.Static;
/// <summary>
/// The directions that this tube can connect to others from
/// </summary>
/// <returns>a new array of the directions</returns>
protected abstract Direction[] ConnectableDirections();
public abstract Direction NextDirection(DisposalHolderComponent holder);
public virtual Vector2 ExitVector(DisposalHolderComponent holder)
{
return NextDirection(holder).ToVec();
}
protected Direction DirectionTo(IDisposalTubeComponent other)
{
return (other.Owner.Transform.WorldPosition - Owner.Transform.WorldPosition).GetDir();
}
public IDisposalTubeComponent? NextTube(DisposalHolderComponent holder)
{
var nextDirection = NextDirection(holder);
var oppositeDirection = new Angle(nextDirection.ToAngle().Theta + Math.PI).GetDir();
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
var position = Owner.Transform.Coordinates;
foreach (var entity in grid.GetInDir(position, nextDirection))
{
if (!Owner.EntityManager.ComponentManager.TryGetComponent(entity, out IDisposalTubeComponent? tube))
{
continue;
}
if (!tube.CanConnect(oppositeDirection, this))
{
continue;
}
if (!CanConnect(nextDirection, tube))
{
continue;
}
return tube;
}
return null;
}
public bool Remove(DisposalHolderComponent holder)
{
var removed = Contents.Remove(holder.Owner);
holder.ExitDisposals();
return removed;
}
public bool TransferTo(DisposalHolderComponent holder, IDisposalTubeComponent to)
{
var position = holder.Owner.Transform.LocalPosition;
if (!to.Contents.Insert(holder.Owner))
{
return false;
}
holder.Owner.Transform.LocalPosition = position;
Contents.Remove(holder.Owner);
holder.EnterTube(to);
return true;
}
// TODO: Make disposal pipes extend the grid
private void Connect()
{
if (_connected || _broken)
{
return;
}
_connected = true;
}
public bool CanConnect(Direction direction, IDisposalTubeComponent with)
{
if (!_connected)
{
return false;
}
if (_broken)
{
return false;
}
if (!ConnectableDirections().Contains(direction))
{
return false;
}
return true;
}
private void Disconnect()
{
if (!_connected)
{
return;
}
_connected = false;
foreach (var entity in Contents.ContainedEntities.ToArray())
{
if (!entity.TryGetComponent(out DisposalHolderComponent? holder))
{
continue;
}
holder.ExitDisposals();
}
}
public void PopupDirections(IEntity entity)
{
var directions = string.Join(", ", ConnectableDirections());
Owner.PopupMessage(entity, Loc.GetString("{0}", directions));
}
private void UpdateVisualState()
{
if (!Owner.TryGetComponent(out AppearanceComponent? appearance))
{
return;
}
var state = _broken
? DisposalTubeVisualState.Broken
: Anchored
? DisposalTubeVisualState.Anchored
: DisposalTubeVisualState.Free;
appearance.SetData(DisposalTubeVisuals.VisualState, state);
}
public void AnchoredChanged()
{
if (!Owner.TryGetComponent(out PhysicsComponent? physics))
{
return;
}
if (physics.BodyType == BodyType.Static)
{
OnAnchor();
}
else
{
OnUnAnchor();
}
}
private void OnAnchor()
{
Connect();
UpdateVisualState();
}
private void OnUnAnchor()
{
Disconnect();
UpdateVisualState();
}
public override void Initialize()
{
base.Initialize();
Contents = ContainerHelpers.EnsureContainer<Container>(Owner, Name);
Owner.EnsureComponent<AnchorableComponent>();
}
protected override void Startup()
{
base.Startup();
Owner.EnsureComponent<PhysicsComponent>(out var physicsComponent);
if (physicsComponent.BodyType != BodyType.Static)
{
return;
}
Connect();
UpdateVisualState();
}
public override void OnRemove()
{
base.OnRemove();
Disconnect();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case RelayMovementEntityMessage _:
if (_gameTiming.CurTime < _lastClang + ClangDelay)
{
break;
}
_lastClang = _gameTiming.CurTime;
SoundSystem.Play(Filter.Pvs(Owner), _clangSound, Owner.Transform.Coordinates);
break;
}
}
void IBreakAct.OnBreak(BreakageEventArgs eventArgs)
{
_broken = true; // TODO: Repair
Disconnect();
UpdateVisualState();
}
[Verb]
private sealed class TubeDirectionsVerb : Verb<IDisposalTubeComponent>
{
protected override void GetData(IEntity user, IDisposalTubeComponent component, VerbData data)
{
data.Text = Loc.GetString("Tube Directions");
data.CategoryData = VerbCategories.Debug;
data.Visibility = VerbVisibility.Invisible;
var groupController = IoCManager.Resolve<IConGroupController>();
if (user.TryGetComponent<ActorComponent>(out var player))
{
if (groupController.CanCommand(player.PlayerSession, "tubeconnections"))
{
data.Visibility = VerbVisibility.Visible;
}
}
}
protected override void Activate(IEntity user, IDisposalTubeComponent component)
{
var groupController = IoCManager.Resolve<IConGroupController>();
if (user.TryGetComponent<ActorComponent>(out var player))
{
if (groupController.CanCommand(player.PlayerSession, "tubeconnections"))
{
component.PopupDirections(user);
}
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
#nullable enable
using Content.Server.Disposal.Unit.Components;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Content.Server.Disposal.Tube.Components
{
public interface IDisposalTubeComponent : IComponent
{
Container Contents { get; }
Direction NextDirection(DisposalHolderComponent holder);
Vector2 ExitVector(DisposalHolderComponent holder);
IDisposalTubeComponent? NextTube(DisposalHolderComponent holder);
bool Remove(DisposalHolderComponent holder);
bool TransferTo(DisposalHolderComponent holder, IDisposalTubeComponent to);
bool CanConnect(Direction direction, IDisposalTubeComponent with);
void PopupDirections(IEntity entity);
}
}

View File

@@ -0,0 +1,30 @@
using Content.Server.Disposal.Tube.Components;
using Robust.Shared.GameObjects;
namespace Content.Server.Disposal.Tube
{
public sealed class DisposalTubeSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalTubeComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<DisposalTubeComponent, PhysicsBodyTypeChangedEvent>();
}
private static void BodyTypeChanged(
EntityUid uid,
DisposalTubeComponent component,
PhysicsBodyTypeChangedEvent args)
{
component.AnchoredChanged();
}
}
}

View File

@@ -0,0 +1,57 @@
#nullable enable
using Content.Server.Administration;
using Content.Server.Disposal.Tube.Components;
using Content.Shared.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Content.Server.Disposal
{
[AdminCommand(AdminFlags.Debug)]
public class TubeConnectionsCommand : IConsoleCommand
{
public string Command => "tubeconnections";
public string Description => Loc.GetString("Shows all the directions that a tube can connect in.");
public string Help => $"Usage: {Command} <entityUid>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player as IPlayerSession;
if (player?.AttachedEntity == null)
{
shell.WriteLine(Loc.GetString("Only players can use this command"));
return;
}
if (args.Length < 1)
{
shell.WriteLine(Help);
return;
}
if (!EntityUid.TryParse(args[0], out var id))
{
shell.WriteLine(Loc.GetString("{0} isn't a valid entity uid", args[0]));
return;
}
var entityManager = IoCManager.Resolve<IEntityManager>();
if (!entityManager.TryGetEntity(id, out var entity))
{
shell.WriteLine(Loc.GetString("No entity exists with uid {0}", id));
return;
}
if (!entity.TryGetComponent(out IDisposalTubeComponent? tube))
{
shell.WriteLine(Loc.GetString("Entity with uid {0} doesn't have a {1} component", id, nameof(IDisposalTubeComponent)));
return;
}
tube.PopupDirections(player.AttachedEntity);
}
}
}

View File

@@ -0,0 +1,191 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.Atmos;
using Content.Server.Disposal.Tube.Components;
using Content.Server.Interfaces;
using Content.Server.Items;
using Content.Shared.Atmos;
using Content.Shared.Body.Components;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Disposal.Unit.Components
{
// TODO: Add gas
[RegisterComponent]
public class DisposalHolderComponent : Component, IGasMixtureHolder
{
public override string Name => "DisposalHolder";
private bool _deletionRequested = false;
private Container _contents = null!;
/// <summary>
/// The total amount of time that it will take for this entity to
/// be pushed to the next tube
/// </summary>
[ViewVariables]
private float StartingTime { get; set; }
/// <summary>
/// Time left until the entity is pushed to the next tube
/// </summary>
[ViewVariables]
private float TimeLeft { get; set; }
[ViewVariables]
public IDisposalTubeComponent? PreviousTube { get; set; }
[ViewVariables]
public IDisposalTubeComponent? CurrentTube { get; private set; }
[ViewVariables]
public IDisposalTubeComponent? NextTube { get; set; }
/// <summary>
/// A list of tags attached to the content, used for sorting
/// </summary>
[ViewVariables]
public HashSet<string> Tags { get; set; } = new();
[ViewVariables]
[DataField("air")]
public GasMixture Air { get; set; } = new GasMixture(Atmospherics.CellVolume);
public override void Initialize()
{
base.Initialize();
_contents = ContainerHelpers.EnsureContainer<Container>(Owner, nameof(DisposalHolderComponent));
}
public override void OnRemove()
{
base.OnRemove();
ExitDisposals();
}
private bool CanInsert(IEntity entity)
{
if (!_contents.CanInsert(entity))
{
return false;
}
return entity.HasComponent<ItemComponent>() ||
entity.HasComponent<IBody>();
}
public bool TryInsert(IEntity entity)
{
if (!CanInsert(entity) || !_contents.Insert(entity))
{
return false;
}
if (entity.TryGetComponent(out IPhysBody? physics))
{
physics.CanCollide = false;
}
return true;
}
public void EnterTube(IDisposalTubeComponent tube)
{
if (CurrentTube != null)
{
PreviousTube = CurrentTube;
}
Owner.Transform.Coordinates = tube.Owner.Transform.Coordinates;
CurrentTube = tube;
NextTube = tube.NextTube(this);
StartingTime = 0.1f;
TimeLeft = 0.1f;
}
public void ExitDisposals()
{
if (_deletionRequested || Deleted)
return;
PreviousTube = null;
CurrentTube = null;
NextTube = null;
StartingTime = 0;
TimeLeft = 0;
foreach (var entity in _contents.ContainedEntities.ToArray())
{
if (entity.TryGetComponent(out IPhysBody? physics))
{
physics.CanCollide = true;
}
_contents.ForceRemove(entity);
if (entity.Transform.Parent == Owner.Transform)
{
entity.Transform.AttachParentToContainerOrGrid();
}
}
if (Owner.Transform.Coordinates.TryGetTileAtmosphere(out var tileAtmos) &&
tileAtmos.Air != null)
{
tileAtmos.AssumeAir(Air);
Air.Clear();
}
// FIXME: This is a workaround for https://github.com/space-wizards/RobustToolbox/issues/1646
Owner.SpawnTimer(TimeSpan.Zero, () => Owner.Delete());
_deletionRequested = true;
}
public void Update(float frameTime)
{
while (frameTime > 0)
{
var time = frameTime;
if (time > TimeLeft)
{
time = TimeLeft;
}
TimeLeft -= time;
frameTime -= time;
if (CurrentTube == null)
{
ExitDisposals();
break;
}
if (TimeLeft > 0)
{
var progress = 1 - TimeLeft / StartingTime;
var origin = CurrentTube.Owner.Transform.WorldPosition;
var destination = CurrentTube.NextDirection(this).ToVec();
var newPosition = destination * progress;
Owner.Transform.WorldPosition = origin + newPosition;
continue;
}
if (NextTube == null || !CurrentTube.TransferTo(this, NextTube))
{
CurrentTube.Remove(this);
break;
}
}
}
}
}

View File

@@ -0,0 +1,691 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Anchor;
using Content.Server.Atmos;
using Content.Server.Disposal.Tube.Components;
using Content.Server.DoAfter;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Hands.Components;
using Content.Server.Interfaces;
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Atmos;
using Content.Shared.Disposal.Components;
using Content.Shared.DragDrop;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Throwing;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Content.Server.Disposal.Unit.Components
{
[RegisterComponent]
[ComponentReference(typeof(SharedDisposalUnitComponent))]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(IInteractUsing))]
public class DisposalUnitComponent : SharedDisposalUnitComponent, IInteractHand, IActivate, IInteractUsing, IThrowCollide, IGasMixtureHolder
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
public override string Name => "DisposalUnit";
/// <summary>
/// The delay for an entity trying to move out of this unit.
/// </summary>
private static readonly TimeSpan ExitAttemptDelay = TimeSpan.FromSeconds(0.5);
/// <summary>
/// Last time that an entity tried to exit this disposal unit.
/// </summary>
[ViewVariables]
private TimeSpan _lastExitAttempt;
/// <summary>
/// The current pressure of this disposal unit.
/// Prevents it from flushing if it is not equal to or bigger than 1.
/// </summary>
[ViewVariables]
[DataField("pressure")]
private float _pressure;
private bool _engaged;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("autoEngageTime")]
private readonly TimeSpan _automaticEngageTime = TimeSpan.FromSeconds(30);
[ViewVariables(VVAccess.ReadWrite)]
[DataField("flushDelay")]
private readonly TimeSpan _flushDelay = TimeSpan.FromSeconds(3);
/// <summary>
/// Delay from trying to enter disposals ourselves.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("entryDelay")]
private float _entryDelay = 0.5f;
/// <summary>
/// Delay from trying to shove someone else into disposals.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
private float _draggedEntryDelay = 0.5f;
/// <summary>
/// Token used to cancel the automatic engage of a disposal unit
/// after an entity enters it.
/// </summary>
private CancellationTokenSource? _automaticEngageToken;
/// <summary>
/// Container of entities inside this disposal unit.
/// </summary>
[ViewVariables]
private Container _container = default!;
[ViewVariables] public IReadOnlyList<IEntity> ContainedEntities => _container.ContainedEntities;
[ViewVariables]
public bool Powered =>
!Owner.TryGetComponent(out PowerReceiverComponent? receiver) ||
receiver.Powered;
[ViewVariables]
private PressureState State => _pressure >= 1 ? PressureState.Ready : PressureState.Pressurizing;
[ViewVariables(VVAccess.ReadWrite)]
private bool Engaged
{
get => _engaged;
set
{
var oldEngaged = _engaged;
_engaged = value;
if (oldEngaged == value)
{
return;
}
UpdateVisualState();
}
}
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(DisposalUnitUiKey.Key);
[DataField("air")]
public GasMixture Air { get; set; } = new GasMixture(Atmospherics.CellVolume);
public override bool CanInsert(IEntity entity)
{
if (!base.CanInsert(entity))
return false;
return _container.CanInsert(entity);
}
private void TryQueueEngage()
{
if (!Powered && ContainedEntities.Count == 0)
{
return;
}
_automaticEngageToken = new CancellationTokenSource();
Owner.SpawnTimer(_automaticEngageTime, () =>
{
if (!TryFlush())
{
TryQueueEngage();
}
}, _automaticEngageToken.Token);
}
private void AfterInsert(IEntity entity)
{
TryQueueEngage();
if (entity.TryGetComponent(out ActorComponent? actor))
{
UserInterface?.Close(actor.PlayerSession);
}
UpdateVisualState();
}
public async Task<bool> TryInsert(IEntity entity, IEntity? user = default)
{
if (!CanInsert(entity))
return false;
var delay = user == entity ? _entryDelay : _draggedEntryDelay;
if (user != null && delay > 0.0f)
{
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
// 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 DoAfterEventArgs(user, delay, default, entity)
{
BreakOnDamage = true,
BreakOnStun = true,
BreakOnTargetMove = true,
BreakOnUserMove = true,
NeedHand = false,
};
var result = await doAfterSystem.DoAfter(doAfterArgs);
if (result == DoAfterStatus.Cancelled)
return false;
}
if (!_container.Insert(entity))
return false;
AfterInsert(entity);
return true;
}
private bool TryDrop(IEntity user, IEntity entity)
{
if (!user.TryGetComponent(out HandsComponent? hands))
{
return false;
}
if (!CanInsert(entity) || !hands.Drop(entity, _container))
{
return false;
}
AfterInsert(entity);
return true;
}
private void Remove(IEntity entity)
{
_container.Remove(entity);
if (ContainedEntities.Count == 0)
{
_automaticEngageToken?.Cancel();
_automaticEngageToken = null;
}
UpdateVisualState();
}
private bool CanFlush()
{
return _pressure >= 1 && Powered && Anchored;
}
private void ToggleEngage()
{
Engaged ^= true;
if (Engaged && CanFlush())
{
Owner.SpawnTimer(_flushDelay, () => TryFlush());
}
}
public bool TryFlush()
{
if (!CanFlush())
{
return false;
}
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
var coords = Owner.Transform.Coordinates;
var entry = grid.GetLocal(coords)
.FirstOrDefault(entity => Owner.EntityManager.ComponentManager.HasComponent<DisposalEntryComponent>(entity));
if (entry == default)
{
return false;
}
var entryComponent = Owner.EntityManager.ComponentManager.GetComponent<DisposalEntryComponent>(entry);
if (Owner.Transform.Coordinates.TryGetTileAtmosphere(out var tileAtmos) &&
tileAtmos.Air != null &&
tileAtmos.Air.Temperature > 0)
{
var tileAir = tileAtmos.Air;
var transferMoles = 0.1f * (0.05f * Atmospherics.OneAtmosphere * 1.01f - Air.Pressure) * Air.Volume / (tileAir.Temperature * Atmospherics.R);
Air = tileAir.Remove(transferMoles);
var atmosSystem = EntitySystem.Get<AtmosphereSystem>();
atmosSystem
.GetGridAtmosphere(Owner.Transform.Coordinates)?
.Invalidate(tileAtmos.GridIndices);
}
entryComponent.TryInsert(this);
_automaticEngageToken?.Cancel();
_automaticEngageToken = null;
_pressure = 0;
Engaged = false;
UpdateVisualState(true);
UpdateInterface();
return true;
}
private void TryEjectContents()
{
foreach (var entity in _container.ContainedEntities.ToArray())
{
Remove(entity);
}
}
private void TogglePower()
{
if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver))
{
return;
}
receiver.PowerDisabled = !receiver.PowerDisabled;
UpdateInterface();
}
private void UpdateInterface()
{
string stateString;
stateString = Loc.GetString($"{State}");
var state = new DisposalUnitBoundUserInterfaceState(Owner.Name, stateString, _pressure, Powered, Engaged);
UserInterface?.SetState(state);
}
private bool PlayerCanUse(IEntity? player)
{
if (player == null)
{
return false;
}
if (!ActionBlockerSystem.CanInteract(player) ||
!ActionBlockerSystem.CanUse(player))
{
return false;
}
return true;
}
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (obj.Session.AttachedEntity == null)
{
return;
}
if (!PlayerCanUse(obj.Session.AttachedEntity))
{
return;
}
if (obj.Message is not UiButtonPressedMessage message)
{
return;
}
switch (message.Button)
{
case UiButton.Eject:
TryEjectContents();
break;
case UiButton.Engage:
ToggleEngage();
break;
case UiButton.Power:
TogglePower();
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
break;
default:
throw new ArgumentOutOfRangeException();
}
}
public void UpdateVisualState()
{
UpdateVisualState(false);
}
private void UpdateVisualState(bool flush)
{
if (!Owner.TryGetComponent(out AppearanceComponent? appearance))
{
return;
}
if (!Anchored)
{
appearance.SetData(Visuals.VisualState, VisualState.UnAnchored);
appearance.SetData(Visuals.Handle, HandleState.Normal);
appearance.SetData(Visuals.Light, LightState.Off);
return;
}
else if (_pressure < 1)
{
appearance.SetData(Visuals.VisualState, VisualState.Charging);
}
else
{
appearance.SetData(Visuals.VisualState, VisualState.Anchored);
}
appearance.SetData(Visuals.Handle, Engaged
? HandleState.Engaged
: HandleState.Normal);
if (!Powered)
{
appearance.SetData(Visuals.Light, LightState.Off);
return;
}
if (flush)
{
appearance.SetData(Visuals.VisualState, VisualState.Flushing);
appearance.SetData(Visuals.Light, LightState.Off);
return;
}
if (ContainedEntities.Count > 0)
{
appearance.SetData(Visuals.Light, LightState.Full);
return;
}
appearance.SetData(Visuals.Light, _pressure < 1
? LightState.Charging
: LightState.Ready);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!Powered)
{
return;
}
var oldPressure = _pressure;
_pressure = _pressure + frameTime > 1
? 1
: _pressure + 0.05f * frameTime;
if (oldPressure < 1 && _pressure >= 1)
{
UpdateVisualState();
if (Engaged)
{
TryFlush();
}
}
// TODO: Ideally we'd just send the start and end and client could lerp as the bandwidth would be way lower
if (_pressure < 1.0f || oldPressure < 1.0f && _pressure >= 1.0f)
{
UpdateInterface();
}
}
private void PowerStateChanged(PowerChangedMessage args)
{
if (!args.Powered)
{
_automaticEngageToken?.Cancel();
_automaticEngageToken = null;
}
UpdateVisualState();
if (Engaged && !TryFlush())
{
TryQueueEngage();
}
}
public override void Initialize()
{
base.Initialize();
_container = ContainerHelpers.EnsureContainer<Container>(Owner, Name);
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
}
UpdateInterface();
}
protected override void Startup()
{
base.Startup();
if(!Owner.HasComponent<AnchorableComponent>())
{
Logger.WarningS("VitalComponentMissing", $"Disposal unit {Owner.Uid} is missing an anchorable component");
}
UpdateVisualState();
UpdateInterface();
}
public override void OnRemove()
{
foreach (var entity in _container.ContainedEntities.ToArray())
{
_container.ForceRemove(entity);
}
UserInterface?.CloseAll();
_automaticEngageToken?.Cancel();
_automaticEngageToken = null;
_container = null!;
base.OnRemove();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case RelayMovementEntityMessage msg:
if (!msg.Entity.TryGetComponent(out HandsComponent? hands) ||
hands.Count == 0 ||
_gameTiming.CurTime < _lastExitAttempt + ExitAttemptDelay)
{
break;
}
_lastExitAttempt = _gameTiming.CurTime;
Remove(msg.Entity);
break;
case PowerChangedMessage powerChanged:
PowerStateChanged(powerChanged);
break;
}
}
bool IsValidInteraction(ITargetedInteractEventArgs eventArgs)
{
if (!ActionBlockerSystem.CanInteract(eventArgs.User))
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("You can't do that!"));
return false;
}
if (eventArgs.User.IsInContainer())
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("You can't reach there!"));
return false;
}
// This popup message doesn't appear on clicks, even when code was seperate. Unsure why.
if (!eventArgs.User.HasComponent<IHandsComponent>())
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("You have no hands!"));
return false;
}
return true;
}
bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return false;
}
// Duplicated code here, not sure how else to get actor inside to make UserInterface happy.
if (IsValidInteraction(eventArgs))
{
UserInterface?.Open(actor.PlayerSession);
return true;
}
return false;
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (IsValidInteraction(eventArgs))
{
UserInterface?.Open(actor.PlayerSession);
}
return;
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
return TryDrop(eventArgs.User, eventArgs.Using);
}
public override bool CanDragDropOn(DragDropEvent eventArgs)
{
// Base is redundant given this already calls the base CanInsert
// If that changes then update this
return CanInsert(eventArgs.Dragged);
}
public override bool DragDropOn(DragDropEvent eventArgs)
{
_ = TryInsert(eventArgs.Dragged, eventArgs.User);
return true;
}
void IThrowCollide.HitBy(ThrowCollideEventArgs eventArgs)
{
if (!CanInsert(eventArgs.Thrown) ||
IoCManager.Resolve<IRobustRandom>().NextDouble() > 0.75 ||
!_container.Insert(eventArgs.Thrown))
{
return;
}
AfterInsert(eventArgs.Thrown);
}
[Verb]
private sealed class SelfInsertVerb : Verb<DisposalUnitComponent>
{
protected override void GetData(IEntity user, DisposalUnitComponent component, VerbData data)
{
data.Visibility = VerbVisibility.Invisible;
if (!ActionBlockerSystem.CanInteract(user) ||
component.ContainedEntities.Contains(user))
{
return;
}
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("Jump inside");
}
protected override void Activate(IEntity user, DisposalUnitComponent component)
{
_ = component.TryInsert(user, user);
}
}
[Verb]
private sealed class FlushVerb : Verb<DisposalUnitComponent>
{
protected override void GetData(IEntity user, DisposalUnitComponent component, VerbData data)
{
data.Visibility = VerbVisibility.Invisible;
if (!ActionBlockerSystem.CanInteract(user) ||
component.ContainedEntities.Contains(user))
{
return;
}
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("Flush");
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
}
protected override void Activate(IEntity user, DisposalUnitComponent component)
{
component.Engaged = true;
component.TryFlush();
}
}
}
}

View File

@@ -0,0 +1,19 @@
using Content.Server.Disposal.Unit.Components;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.Disposal.Unit.EntitySystems
{
[UsedImplicitly]
internal sealed class DisposableSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<DisposalHolderComponent>(true))
{
comp.Update(frameTime);
}
}
}
}

View File

@@ -0,0 +1,30 @@
using Content.Server.Disposal.Unit.Components;
using Robust.Shared.GameObjects;
namespace Content.Server.Disposal.Unit.EntitySystems
{
public sealed class DisposalUnitSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalUnitComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<DisposalUnitComponent, PhysicsBodyTypeChangedEvent>();
}
private static void BodyTypeChanged(
EntityUid uid,
DisposalUnitComponent component,
PhysicsBodyTypeChangedEvent args)
{
component.UpdateVisualState();
}
}
}