Refactor disposals to ECS (#4418)
* ECS up disposals Also significantly reduced its CPU usage. * Make update significantly less S L O W * Start units pressurised * Client-side flush lerping * Fix powered not toggling UI * Fix flush button * InteractUsing * Minor optimisations * Fix collisions * Make visual state ECS * Almost done with shared * Most stuff moved * Optimise item sleeping
This commit is contained in:
@@ -1,16 +0,0 @@
|
|||||||
using Content.Shared.Disposal.Components;
|
|
||||||
using Content.Shared.DragDrop;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
|
|
||||||
namespace Content.Client.Disposal.Components
|
|
||||||
{
|
|
||||||
[RegisterComponent]
|
|
||||||
[ComponentReference(typeof(SharedDisposalMailingUnitComponent))]
|
|
||||||
public class DisposalMailingUnitComponent : SharedDisposalMailingUnitComponent
|
|
||||||
{
|
|
||||||
public override bool DragDropOn(DragDropEvent eventArgs)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,16 @@ namespace Content.Client.Disposal.Components
|
|||||||
[ComponentReference(typeof(SharedDisposalUnitComponent))]
|
[ComponentReference(typeof(SharedDisposalUnitComponent))]
|
||||||
public class DisposalUnitComponent : SharedDisposalUnitComponent
|
public class DisposalUnitComponent : SharedDisposalUnitComponent
|
||||||
{
|
{
|
||||||
|
public DisposalUnitBoundUserInterfaceState? UiState;
|
||||||
|
|
||||||
|
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||||
|
{
|
||||||
|
base.HandleComponentState(curState, nextState);
|
||||||
|
if (curState is not DisposalUnitComponentState state) return;
|
||||||
|
|
||||||
|
RecentlyEjected = state.RecentlyEjected;
|
||||||
|
}
|
||||||
|
|
||||||
public override bool DragDropOn(DragDropEvent eventArgs)
|
public override bool DragDropOn(DragDropEvent eventArgs)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
57
Content.Client/Disposal/Systems/DisposalUnitSystem.cs
Normal file
57
Content.Client/Disposal/Systems/DisposalUnitSystem.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Client.Disposal.Components;
|
||||||
|
using Content.Client.Disposal.UI;
|
||||||
|
using Content.Shared.Disposal;
|
||||||
|
using Robust.Client.GameObjects;
|
||||||
|
|
||||||
|
namespace Content.Client.Disposal.Systems
|
||||||
|
{
|
||||||
|
public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||||
|
{
|
||||||
|
public List<DisposalUnitComponent> PressuringDisposals = new();
|
||||||
|
|
||||||
|
public void UpdateActive(DisposalUnitComponent component, bool active)
|
||||||
|
{
|
||||||
|
if (active)
|
||||||
|
{
|
||||||
|
if (!PressuringDisposals.Contains(component))
|
||||||
|
PressuringDisposals.Add(component);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PressuringDisposals.Remove(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void FrameUpdate(float frameTime)
|
||||||
|
{
|
||||||
|
base.FrameUpdate(frameTime);
|
||||||
|
for (var i = PressuringDisposals.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var comp = PressuringDisposals[i];
|
||||||
|
if (!UpdateInterface(comp)) continue;
|
||||||
|
PressuringDisposals.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool UpdateInterface(DisposalUnitComponent component)
|
||||||
|
{
|
||||||
|
if (component.Deleted) return true;
|
||||||
|
|
||||||
|
if (!component.Owner.TryGetComponent(out ClientUserInterfaceComponent? userInterface)) return true;
|
||||||
|
|
||||||
|
var state = component.UiState;
|
||||||
|
if (state == null) return true;
|
||||||
|
|
||||||
|
foreach (var inter in userInterface.Interfaces)
|
||||||
|
{
|
||||||
|
if (inter is DisposalUnitBoundUserInterface disposals)
|
||||||
|
{
|
||||||
|
return disposals.Window?.UpdateState(state) != false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
using JetBrains.Annotations;
|
|
||||||
using Robust.Client.GameObjects;
|
|
||||||
using Robust.Client.UserInterface.Controls;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using static Content.Shared.Disposal.Components.SharedDisposalMailingUnitComponent;
|
|
||||||
|
|
||||||
namespace Content.Client.Disposal.UI
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a <see cref="DisposalMailingUnitWindow"/> and updates it when new server messages are received.
|
|
||||||
/// </summary>
|
|
||||||
[UsedImplicitly]
|
|
||||||
public class DisposalMailingUnitBoundUserInterface : BoundUserInterface
|
|
||||||
{
|
|
||||||
private DisposalMailingUnitWindow? _window;
|
|
||||||
|
|
||||||
public DisposalMailingUnitBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ButtonPressed(UiButton button)
|
|
||||||
{
|
|
||||||
SendMessage(new UiButtonPressedMessage(button));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Open()
|
|
||||||
{
|
|
||||||
base.Open();
|
|
||||||
|
|
||||||
_window = new DisposalMailingUnitWindow();
|
|
||||||
|
|
||||||
_window.OpenCentered();
|
|
||||||
_window.OnClose += Close;
|
|
||||||
|
|
||||||
_window.Eject.OnPressed += _ => ButtonPressed(UiButton.Eject);
|
|
||||||
_window.Engage.OnPressed += _ => ButtonPressed(UiButton.Engage);
|
|
||||||
_window.Power.OnPressed += _ => ButtonPressed(UiButton.Power);
|
|
||||||
_window.TargetListContainer.OnItemSelected += TargetSelected;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected override void UpdateState(BoundUserInterfaceState state)
|
|
||||||
{
|
|
||||||
base.UpdateState(state);
|
|
||||||
|
|
||||||
if (state is not DisposalMailingUnitBoundUserInterfaceState cast)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_window?.UpdateState(cast);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TargetSelected(ItemList.ItemListSelectedEventArgs item)
|
|
||||||
{
|
|
||||||
SendMessage(new UiTargetUpdateMessage(_window?.TargetList[item.ItemIndex]));
|
|
||||||
//(ノ°Д°)ノ︵ ┻━┻
|
|
||||||
if (_window != null) _window.Engage.Disabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
base.Dispose(disposing);
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_window?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Content.Shared.Disposal.Components;
|
|
||||||
using Robust.Client.Graphics;
|
|
||||||
using Robust.Client.UserInterface;
|
|
||||||
using Robust.Client.UserInterface.Controls;
|
|
||||||
using Robust.Client.UserInterface.CustomControls;
|
|
||||||
using Robust.Shared.Localization;
|
|
||||||
using Robust.Shared.Maths;
|
|
||||||
using static Content.Shared.Disposal.Components.SharedDisposalMailingUnitComponent;
|
|
||||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
|
||||||
|
|
||||||
namespace Content.Client.Disposal.UI
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Client-side UI used to control a <see cref="SharedDisposalMailingUnitComponent"/>
|
|
||||||
/// </summary>
|
|
||||||
public class DisposalMailingUnitWindow : SS14Window
|
|
||||||
{
|
|
||||||
private readonly Label _unitState;
|
|
||||||
private readonly ProgressBar _pressureBar;
|
|
||||||
private readonly Label _pressurePercentage;
|
|
||||||
public readonly Button Engage;
|
|
||||||
public readonly Button Eject;
|
|
||||||
public readonly Button Power;
|
|
||||||
|
|
||||||
public readonly ItemList TargetListContainer;
|
|
||||||
public List<string> TargetList;
|
|
||||||
private readonly Label _tagLabel;
|
|
||||||
|
|
||||||
public DisposalMailingUnitWindow()
|
|
||||||
{
|
|
||||||
MinSize = SetSize = (460, 230);
|
|
||||||
TargetList = new List<string>();
|
|
||||||
Contents.AddChild(new BoxContainer
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Horizontal,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new BoxContainer
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Vertical,
|
|
||||||
HorizontalExpand = true,
|
|
||||||
Margin = new Thickness(8, 0),
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new BoxContainer
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Horizontal,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new Label {Text = $"{Loc.GetString("disposal-mailing-unit-window-state-label")} "},
|
|
||||||
new Control {MinSize = (4, 0)},
|
|
||||||
(_unitState = new Label {Text = Loc.GetString("disposal-mailing-unit-window-ready-state")})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new Control {MinSize = (0, 10)},
|
|
||||||
new BoxContainer
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Horizontal,
|
|
||||||
HorizontalExpand = true,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new Label {Text = Loc.GetString("disposal-mailing-unit-pressure-label")},
|
|
||||||
new Control {MinSize = (4, 0)},
|
|
||||||
(_pressureBar = new ProgressBar
|
|
||||||
{
|
|
||||||
MinSize = (100, 20),
|
|
||||||
HorizontalExpand = true,
|
|
||||||
MinValue = 0,
|
|
||||||
MaxValue = 1,
|
|
||||||
Page = 0,
|
|
||||||
Value = 0.5f,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
(_pressurePercentage = new Label())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new Control {MinSize = (0, 10)},
|
|
||||||
new BoxContainer
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Horizontal,
|
|
||||||
HorizontalExpand = true,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new Label {Text = Loc.GetString("disposal-mailing-unit-handle-label")},
|
|
||||||
new Control
|
|
||||||
{
|
|
||||||
MinSize = (4, 0),
|
|
||||||
HorizontalExpand = true
|
|
||||||
},
|
|
||||||
(Engage = new Button
|
|
||||||
{
|
|
||||||
MinSize = (16, 0),
|
|
||||||
Text = Loc.GetString("disposal-mailing-unit-engage-button"),
|
|
||||||
ToggleMode = true,
|
|
||||||
Disabled = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new Control {MinSize = (0, 10)},
|
|
||||||
new BoxContainer
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Horizontal,
|
|
||||||
HorizontalExpand = true,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new Label {Text = Loc.GetString("disposal-mailing-unit-eject-label")},
|
|
||||||
new Control
|
|
||||||
{
|
|
||||||
MinSize = (4, 0),
|
|
||||||
HorizontalExpand = true
|
|
||||||
},
|
|
||||||
(Eject = new Button
|
|
||||||
{
|
|
||||||
MinSize = (16, 0),
|
|
||||||
Text = Loc.GetString("disposal-mailing-unit-eject-button"),
|
|
||||||
//HorizontalAlignment = HAlignment.Right
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new Control {MinSize = (0, 10)},
|
|
||||||
new BoxContainer
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Horizontal,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
(Power = new CheckButton {Text = Loc.GetString("disposal-mailing-unit-power-button")}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new BoxContainer
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Vertical,
|
|
||||||
Margin = new Thickness(12, 0, 8, 0),
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new BoxContainer
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Horizontal,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new Label
|
|
||||||
{
|
|
||||||
Text = Loc.GetString("disposal-mailing-unit-destination-select-label")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new Control {MinSize = new Vector2(0, 8)},
|
|
||||||
new BoxContainer
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Horizontal,
|
|
||||||
VerticalExpand = true,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
(TargetListContainer = new ItemList
|
|
||||||
{
|
|
||||||
SelectMode = ItemList.ItemListSelectMode.Single,
|
|
||||||
HorizontalExpand = true,
|
|
||||||
VerticalExpand = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new PanelContainer
|
|
||||||
{
|
|
||||||
PanelOverride = new StyleBoxFlat
|
|
||||||
{
|
|
||||||
BackgroundColor = Color.FromHex("#ACBDBA")
|
|
||||||
},
|
|
||||||
HorizontalExpand = true,
|
|
||||||
MinSize = new Vector2(0, 1),
|
|
||||||
},
|
|
||||||
new BoxContainer
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Horizontal,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new BoxContainer
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Vertical,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new BoxContainer
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Horizontal,
|
|
||||||
Margin = new Thickness(4, 0, 0, 0),
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new Label
|
|
||||||
{
|
|
||||||
Text = Loc.GetString("disposal-mailing-unit-unit-self-reference")
|
|
||||||
},
|
|
||||||
new Control
|
|
||||||
{
|
|
||||||
MinSize = new Vector2(4, 0)
|
|
||||||
},
|
|
||||||
(_tagLabel = new Label
|
|
||||||
{
|
|
||||||
Text = "-",
|
|
||||||
VerticalAlignment = VAlignment.Bottom
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePressureBar(float pressure)
|
|
||||||
{
|
|
||||||
_pressureBar.Value = pressure;
|
|
||||||
|
|
||||||
var normalized = pressure / _pressureBar.MaxValue;
|
|
||||||
|
|
||||||
const float leftHue = 0.0f; // Red
|
|
||||||
const float middleHue = 0.066f; // Orange
|
|
||||||
const float rightHue = 0.33f; // Green
|
|
||||||
const float saturation = 1.0f; // Uniform saturation
|
|
||||||
const float value = 0.8f; // Uniform value / brightness
|
|
||||||
const float alpha = 1.0f; // Uniform alpha
|
|
||||||
|
|
||||||
// These should add up to 1.0 or your transition won't be smooth
|
|
||||||
const float leftSideSize = 0.5f; // Fraction of _chargeBar lerped from leftHue to middleHue
|
|
||||||
const float rightSideSize = 0.5f; // Fraction of _chargeBar lerped from middleHue to rightHue
|
|
||||||
|
|
||||||
float finalHue;
|
|
||||||
if (normalized <= leftSideSize)
|
|
||||||
{
|
|
||||||
normalized /= leftSideSize; // Adjust range to 0.0 to 1.0
|
|
||||||
finalHue = MathHelper.Lerp(leftHue, middleHue, normalized);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
normalized = (normalized - leftSideSize) / rightSideSize; // Adjust range to 0.0 to 1.0.
|
|
||||||
finalHue = MathHelper.Lerp(middleHue, rightHue, normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if null first to avoid repeatedly creating this.
|
|
||||||
_pressureBar.ForegroundStyleBoxOverride ??= new StyleBoxFlat();
|
|
||||||
|
|
||||||
var foregroundStyleBoxOverride = (StyleBoxFlat) _pressureBar.ForegroundStyleBoxOverride;
|
|
||||||
foregroundStyleBoxOverride.BackgroundColor =
|
|
||||||
Color.FromHsv(new Vector4(finalHue, saturation, value, alpha));
|
|
||||||
|
|
||||||
var percentage = pressure / _pressureBar.MaxValue * 100;
|
|
||||||
_pressurePercentage.Text = $" {percentage:0}%";
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateState(DisposalMailingUnitBoundUserInterfaceState state)
|
|
||||||
{
|
|
||||||
Title = state.UnitName;
|
|
||||||
_unitState.Text = state.UnitState;
|
|
||||||
UpdatePressureBar(state.Pressure);
|
|
||||||
Power.Pressed = state.Powered;
|
|
||||||
Engage.Pressed = state.Engaged;
|
|
||||||
PopulateTargetList(state.Tags);
|
|
||||||
_tagLabel.Text = state.Tag;
|
|
||||||
TargetList = state.Tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PopulateTargetList(List<string> tags)
|
|
||||||
{
|
|
||||||
TargetListContainer.Clear();
|
|
||||||
foreach (var target in tags)
|
|
||||||
{
|
|
||||||
TargetListContainer.AddItem(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using JetBrains.Annotations;
|
using Content.Client.Disposal.Components;
|
||||||
|
using Content.Client.Disposal.Systems;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Robust.Client.GameObjects;
|
using Robust.Client.GameObjects;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
|
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
|
||||||
@@ -11,7 +13,7 @@ namespace Content.Client.Disposal.UI
|
|||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public class DisposalUnitBoundUserInterface : BoundUserInterface
|
public class DisposalUnitBoundUserInterface : BoundUserInterface
|
||||||
{
|
{
|
||||||
private DisposalUnitWindow? _window;
|
public DisposalUnitWindow? Window;
|
||||||
|
|
||||||
public DisposalUnitBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
|
public DisposalUnitBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
|
||||||
{
|
{
|
||||||
@@ -20,20 +22,22 @@ namespace Content.Client.Disposal.UI
|
|||||||
private void ButtonPressed(UiButton button)
|
private void ButtonPressed(UiButton button)
|
||||||
{
|
{
|
||||||
SendMessage(new UiButtonPressedMessage(button));
|
SendMessage(new UiButtonPressedMessage(button));
|
||||||
|
// If we get client-side power stuff then we can predict the button presses but for now we won't as it stuffs
|
||||||
|
// the pressure lerp up.
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Open()
|
protected override void Open()
|
||||||
{
|
{
|
||||||
base.Open();
|
base.Open();
|
||||||
|
|
||||||
_window = new DisposalUnitWindow();
|
Window = new DisposalUnitWindow();
|
||||||
|
|
||||||
_window.OpenCentered();
|
Window.OpenCentered();
|
||||||
_window.OnClose += Close;
|
Window.OnClose += Close;
|
||||||
|
|
||||||
_window.Eject.OnPressed += _ => ButtonPressed(UiButton.Eject);
|
Window.Eject.OnPressed += _ => ButtonPressed(UiButton.Eject);
|
||||||
_window.Engage.OnPressed += _ => ButtonPressed(UiButton.Engage);
|
Window.Engage.OnPressed += _ => ButtonPressed(UiButton.Engage);
|
||||||
_window.Power.OnPressed += _ => ButtonPressed(UiButton.Power);
|
Window.Power.OnPressed += _ => ButtonPressed(UiButton.Power);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateState(BoundUserInterfaceState state)
|
protected override void UpdateState(BoundUserInterfaceState state)
|
||||||
@@ -45,7 +49,13 @@ namespace Content.Client.Disposal.UI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_window?.UpdateState(cast);
|
Window?.UpdateState(cast);
|
||||||
|
|
||||||
|
// Kinda icky but we just want client to handle its own lerping and not flood bandwidth for it.
|
||||||
|
if (!Owner.Owner.TryGetComponent(out DisposalUnitComponent? component)) return;
|
||||||
|
|
||||||
|
component.UiState = cast;
|
||||||
|
EntitySystem.Get<DisposalUnitSystem>().UpdateActive(component, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
@@ -54,7 +64,7 @@ namespace Content.Client.Disposal.UI
|
|||||||
|
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
_window?.Dispose();
|
Window?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
using System;
|
||||||
using Content.Client.Stylesheets;
|
using Content.Client.Stylesheets;
|
||||||
|
using Content.Shared.Disposal;
|
||||||
using Content.Shared.Disposal.Components;
|
using Content.Shared.Disposal.Components;
|
||||||
using Robust.Client.Graphics;
|
using Robust.Client.Graphics;
|
||||||
using Robust.Client.UserInterface;
|
using Robust.Client.UserInterface;
|
||||||
@@ -7,6 +9,7 @@ using Robust.Client.UserInterface.CustomControls;
|
|||||||
using Robust.Shared.IoC;
|
using Robust.Shared.IoC;
|
||||||
using Robust.Shared.Localization;
|
using Robust.Shared.Localization;
|
||||||
using Robust.Shared.Maths;
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
|
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
|
||||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||||
|
|
||||||
@@ -130,13 +133,23 @@ namespace Content.Client.Disposal.UI
|
|||||||
Color.FromHsv(new Vector4(finalHue, saturation, value, alpha));
|
Color.FromHsv(new Vector4(finalHue, saturation, value, alpha));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateState(DisposalUnitBoundUserInterfaceState state)
|
/// <summary>
|
||||||
|
/// Update the interface state for the disposals window.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>true if we should stop updating every frame.</returns>
|
||||||
|
public bool UpdateState(DisposalUnitBoundUserInterfaceState state)
|
||||||
{
|
{
|
||||||
|
var currentTime = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||||
|
var fullTime = state.FullPressureTime;
|
||||||
|
var pressure = (float) Math.Min(1.0f, 1.0f - (fullTime.TotalSeconds - currentTime.TotalSeconds) * SharedDisposalUnitSystem.PressurePerSecond);
|
||||||
|
|
||||||
Title = state.UnitName;
|
Title = state.UnitName;
|
||||||
_unitState.Text = state.UnitState;
|
_unitState.Text = state.UnitState;
|
||||||
UpdatePressureBar(state.Pressure);
|
UpdatePressureBar(pressure);
|
||||||
Power.Pressed = state.Powered;
|
Power.Pressed = state.Powered;
|
||||||
Engage.Pressed = state.Engaged;
|
Engage.Pressed = state.Engaged;
|
||||||
|
|
||||||
|
return !state.Powered || pressure >= 1.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.Disposal.Tube.Components;
|
using Content.Server.Disposal.Tube.Components;
|
||||||
using Content.Server.Disposal.Unit.Components;
|
using Content.Server.Disposal.Unit.Components;
|
||||||
using Content.Server.GameObjects.Components;
|
using Content.Server.Disposal.Unit.EntitySystems;
|
||||||
using Content.Server.Power.Components;
|
using Content.Server.Power.Components;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
@@ -24,7 +24,7 @@ namespace Content.IntegrationTests.Tests.Disposal
|
|||||||
foreach (var entity in entities)
|
foreach (var entity in entities)
|
||||||
{
|
{
|
||||||
var insertTask = unit.TryInsert(entity);
|
var insertTask = unit.TryInsert(entity);
|
||||||
Assert.That(unit.CanInsert(entity), Is.EqualTo(result));
|
Assert.That(EntitySystem.Get<DisposalUnitSystem>().CanInsert(unit, entity), Is.EqualTo(result));
|
||||||
insertTask.ContinueWith(task =>
|
insertTask.ContinueWith(task =>
|
||||||
{
|
{
|
||||||
Assert.That(task.Result, Is.EqualTo(result));
|
Assert.That(task.Result, Is.EqualTo(result));
|
||||||
@@ -56,7 +56,7 @@ namespace Content.IntegrationTests.Tests.Disposal
|
|||||||
Assert.That(unit.ContainedEntities, Is.SupersetOf(entities));
|
Assert.That(unit.ContainedEntities, Is.SupersetOf(entities));
|
||||||
Assert.That(entities.Length, Is.EqualTo(unit.ContainedEntities.Count));
|
Assert.That(entities.Length, Is.EqualTo(unit.ContainedEntities.Count));
|
||||||
|
|
||||||
Assert.That(result, Is.EqualTo(unit.TryFlush()));
|
Assert.That(result, Is.EqualTo(EntitySystem.Get<DisposalUnitSystem>().TryFlush(unit)));
|
||||||
Assert.That(result || entities.Length == 0, Is.EqualTo(unit.ContainedEntities.Count == 0));
|
Assert.That(result || entities.Length == 0, Is.EqualTo(unit.ContainedEntities.Count == 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ namespace Content.IntegrationTests.Tests.Disposal
|
|||||||
// Can't insert, unanchored and unpowered
|
// Can't insert, unanchored and unpowered
|
||||||
var physics = disposalUnit.GetComponent<IPhysBody>();
|
var physics = disposalUnit.GetComponent<IPhysBody>();
|
||||||
physics.BodyType = BodyType.Dynamic;
|
physics.BodyType = BodyType.Dynamic;
|
||||||
Assert.False(unit.Anchored);
|
Assert.False(unit.Owner.Transform.Anchored);
|
||||||
UnitInsertContains(unit, false, human, wrench, disposalUnit, disposalTrunk);
|
UnitInsertContains(unit, false, human, wrench, disposalUnit, disposalTrunk);
|
||||||
|
|
||||||
// Anchor the disposal unit
|
// Anchor the disposal unit
|
||||||
|
|||||||
@@ -1,794 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.Construction.Components;
|
|
||||||
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.Movement;
|
|
||||||
using Content.Shared.Notification;
|
|
||||||
using Content.Shared.Notification.Managers;
|
|
||||||
using Content.Shared.Sound;
|
|
||||||
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;
|
|
||||||
|
|
||||||
[DataField("receivedMessageSound")]
|
|
||||||
private SoundSpecifier _receivedMessageSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
|
|
||||||
|
|
||||||
/// <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 ApcPowerReceiverComponent? 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<SharedBodyComponent>())
|
|
||||||
{
|
|
||||||
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.WaitDoAfter(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 ApcPowerReceiverComponent? 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
var actionBlocker = EntitySystem.Get<ActionBlockerSystem>();
|
|
||||||
|
|
||||||
if (!actionBlocker.CanInteract(player) ||
|
|
||||||
!actionBlocker.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), _receivedMessageSound.GetSound(), 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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 (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(eventArgs.User))
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("disposal-mailing-unit-is-valid-interaction-cannot-interact"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventArgs.User.IsInContainer())
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("disposal-mailing-unit-is-valid-interaction-cannot-reach"));
|
|
||||||
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("disposal-mailing-unit-is-valid-interaction-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 (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user) ||
|
|
||||||
component.ContainedEntities.Contains(user))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.Visibility = VerbVisibility.Visible;
|
|
||||||
data.Text = Loc.GetString("self-insert-verb-get-data-text");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user) ||
|
|
||||||
component.ContainedEntities.Contains(user))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.Visibility = VerbVisibility.Visible;
|
|
||||||
data.Text = Loc.GetString("flush-verb-get-data-text");
|
|
||||||
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Activate(IEntity user, DisposalMailingUnitComponent component)
|
|
||||||
{
|
|
||||||
component.Engaged = true;
|
|
||||||
component.TryFlush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using Robust.Shared.GameObjects;
|
|
||||||
|
|
||||||
namespace Content.Server.Disposal.Mailing
|
|
||||||
{
|
|
||||||
public sealed class DisposalMailingUnitSystem : EntitySystem
|
|
||||||
{
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
SubscribeLocalEvent<DisposalMailingUnitComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void BodyTypeChanged(
|
|
||||||
EntityUid uid,
|
|
||||||
DisposalMailingUnitComponent component,
|
|
||||||
PhysicsBodyTypeChangedEvent args)
|
|
||||||
{
|
|
||||||
component.UpdateVisualState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,8 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.Atmos;
|
using Content.Server.Atmos;
|
||||||
using Content.Server.Atmos.EntitySystems;
|
using Content.Server.Disposal.Unit.EntitySystems;
|
||||||
using Content.Server.Construction.Components;
|
|
||||||
using Content.Server.Disposal.Tube.Components;
|
|
||||||
using Content.Server.DoAfter;
|
using Content.Server.DoAfter;
|
||||||
using Content.Server.Hands.Components;
|
|
||||||
using Content.Server.Power.Components;
|
using Content.Server.Power.Components;
|
||||||
using Content.Server.UserInterface;
|
using Content.Server.UserInterface;
|
||||||
using Content.Shared.ActionBlocker;
|
using Content.Shared.ActionBlocker;
|
||||||
@@ -16,49 +13,27 @@ using Content.Shared.Acts;
|
|||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
using Content.Shared.Disposal.Components;
|
using Content.Shared.Disposal.Components;
|
||||||
using Content.Shared.DragDrop;
|
using Content.Shared.DragDrop;
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Movement;
|
|
||||||
using Content.Shared.Notification.Managers;
|
|
||||||
using Content.Shared.Sound;
|
|
||||||
using Content.Shared.Throwing;
|
|
||||||
using Content.Shared.Verbs;
|
using Content.Shared.Verbs;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Localization;
|
using Robust.Shared.Localization;
|
||||||
using Robust.Shared.Log;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Random;
|
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.Timing;
|
|
||||||
using Robust.Shared.ViewVariables;
|
using Robust.Shared.ViewVariables;
|
||||||
|
|
||||||
namespace Content.Server.Disposal.Unit.Components
|
namespace Content.Server.Disposal.Unit.Components
|
||||||
{
|
{
|
||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
[ComponentReference(typeof(SharedDisposalUnitComponent))]
|
[ComponentReference(typeof(SharedDisposalUnitComponent))]
|
||||||
[ComponentReference(typeof(IActivate))]
|
public class DisposalUnitComponent : SharedDisposalUnitComponent, IGasMixtureHolder, IDestroyAct
|
||||||
[ComponentReference(typeof(IInteractUsing))]
|
|
||||||
public class DisposalUnitComponent : SharedDisposalUnitComponent, IInteractHand, IActivate, IInteractUsing, IThrowCollide, IGasMixtureHolder, IDestroyAct
|
|
||||||
{
|
{
|
||||||
[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>
|
/// <summary>
|
||||||
/// Last time that an entity tried to exit this disposal unit.
|
/// Last time that an entity tried to exit this disposal unit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
private TimeSpan _lastExitAttempt;
|
public TimeSpan LastExitAttempt;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current pressure of this disposal unit.
|
/// The current pressure of this disposal unit.
|
||||||
@@ -66,19 +41,15 @@ namespace Content.Server.Disposal.Unit.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
[DataField("pressure")]
|
[DataField("pressure")]
|
||||||
private float _pressure;
|
public float Pressure = 1f;
|
||||||
|
|
||||||
private bool _engaged;
|
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
[DataField("autoEngageTime")]
|
[DataField("autoEngageTime")]
|
||||||
private readonly TimeSpan _automaticEngageTime = TimeSpan.FromSeconds(30);
|
public readonly TimeSpan _automaticEngageTime = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
[DataField("flushDelay")]
|
[DataField("flushDelay")]
|
||||||
private readonly TimeSpan _flushDelay = TimeSpan.FromSeconds(3);
|
public readonly TimeSpan FlushDelay = TimeSpan.FromSeconds(3);
|
||||||
|
|
||||||
[DataField("clickSound")] private SoundSpecifier _clickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delay from trying to enter disposals ourselves.
|
/// Delay from trying to enter disposals ourselves.
|
||||||
@@ -97,88 +68,33 @@ namespace Content.Server.Disposal.Unit.Components
|
|||||||
/// Token used to cancel the automatic engage of a disposal unit
|
/// Token used to cancel the automatic engage of a disposal unit
|
||||||
/// after an entity enters it.
|
/// after an entity enters it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private CancellationTokenSource? _automaticEngageToken;
|
public CancellationTokenSource? AutomaticEngageToken;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Container of entities inside this disposal unit.
|
/// Container of entities inside this disposal unit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables]
|
[ViewVariables] public Container Container = default!;
|
||||||
private Container _container = default!;
|
|
||||||
|
|
||||||
[ViewVariables] public IReadOnlyList<IEntity> ContainedEntities => _container.ContainedEntities;
|
[ViewVariables] public IReadOnlyList<IEntity> ContainedEntities => Container.ContainedEntities;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
public bool Powered =>
|
public bool Powered =>
|
||||||
!Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) ||
|
!Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) ||
|
||||||
receiver.Powered;
|
receiver.Powered;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables] public PressureState State => Pressure >= 1 ? PressureState.Ready : PressureState.Pressurizing;
|
||||||
private PressureState State => _pressure >= 1 ? PressureState.Ready : PressureState.Pressurizing;
|
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
private bool Engaged
|
public bool Engaged { get; set; }
|
||||||
{
|
|
||||||
get => _engaged;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
var oldEngaged = _engaged;
|
|
||||||
_engaged = value;
|
|
||||||
|
|
||||||
if (oldEngaged == value)
|
[ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(DisposalUnitUiKey.Key);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateVisualState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(DisposalUnitUiKey.Key);
|
|
||||||
|
|
||||||
[DataField("air")]
|
[DataField("air")]
|
||||||
public GasMixture Air { get; set; } = new GasMixture(Atmospherics.CellVolume);
|
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)
|
public async Task<bool> TryInsert(IEntity entity, IEntity? user = default)
|
||||||
{
|
{
|
||||||
if (!CanInsert(entity))
|
if (!EntitySystem.Get<DisposalUnitSystem>().CanInsert(this, entity))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var delay = user == entity ? _entryDelay : _draggedEntryDelay;
|
var delay = user == entity ? _entryDelay : _draggedEntryDelay;
|
||||||
@@ -204,130 +120,14 @@ namespace Content.Server.Disposal.Unit.Components
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_container.Insert(entity))
|
if (!Container.Insert(entity))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
AfterInsert(entity);
|
EntitySystem.Get<DisposalUnitSystem>().AfterInsert(this, entity);
|
||||||
|
|
||||||
return true;
|
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);
|
|
||||||
|
|
||||||
var atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
|
|
||||||
|
|
||||||
if (atmosphereSystem.GetTileMixture(Owner.Transform.Coordinates, true) is {Temperature: > 0} environment)
|
|
||||||
{
|
|
||||||
var transferMoles = 0.1f * (0.05f * Atmospherics.OneAtmosphere * 1.01f - Air.Pressure) * Air.Volume / (environment.Temperature * Atmospherics.R);
|
|
||||||
|
|
||||||
Air = environment.Remove(transferMoles);
|
|
||||||
}
|
|
||||||
|
|
||||||
entryComponent.TryInsert(this);
|
|
||||||
|
|
||||||
_automaticEngageToken?.Cancel();
|
|
||||||
_automaticEngageToken = null;
|
|
||||||
|
|
||||||
_pressure = 0;
|
|
||||||
|
|
||||||
Engaged = false;
|
|
||||||
|
|
||||||
UpdateVisualState(true);
|
|
||||||
UpdateInterface();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void TryEjectContents()
|
|
||||||
{
|
|
||||||
foreach (var entity in _container.ContainedEntities.ToArray())
|
|
||||||
{
|
|
||||||
Remove(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TogglePower()
|
|
||||||
{
|
|
||||||
if (!Owner.TryGetComponent(out ApcPowerReceiverComponent? 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)
|
private bool PlayerCanUse(IEntity? player)
|
||||||
{
|
{
|
||||||
if (player == null)
|
if (player == null)
|
||||||
@@ -346,7 +146,7 @@ namespace Content.Server.Disposal.Unit.Components
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
|
public void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
|
||||||
{
|
{
|
||||||
if (obj.Session.AttachedEntity == null)
|
if (obj.Session.AttachedEntity == null)
|
||||||
{
|
{
|
||||||
@@ -366,259 +166,25 @@ namespace Content.Server.Disposal.Unit.Components
|
|||||||
switch (message.Button)
|
switch (message.Button)
|
||||||
{
|
{
|
||||||
case UiButton.Eject:
|
case UiButton.Eject:
|
||||||
TryEjectContents();
|
EntitySystem.Get<DisposalUnitSystem>().TryEjectContents(this);
|
||||||
break;
|
break;
|
||||||
case UiButton.Engage:
|
case UiButton.Engage:
|
||||||
ToggleEngage();
|
EntitySystem.Get<DisposalUnitSystem>().ToggleEngage(this);
|
||||||
break;
|
break;
|
||||||
case UiButton.Power:
|
case UiButton.Power:
|
||||||
TogglePower();
|
EntitySystem.Get<DisposalUnitSystem>().TogglePower(this);
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _clickSound.GetSound(), Owner, AudioParams.Default.WithVolume(-2f));
|
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException();
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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 {nameof(AnchorableComponent)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateVisualState();
|
|
||||||
UpdateInterface();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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 (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(eventArgs.User))
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("ui-disposal-unit-is-valid-interaction-cannot=interact"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventArgs.User.IsInContainer())
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("ui-disposal-unit-is-valid-interaction-cannot-reach"));
|
|
||||||
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("ui-disposal-unit-is-valid-interaction-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)
|
public override bool CanDragDropOn(DragDropEvent eventArgs)
|
||||||
{
|
{
|
||||||
// Base is redundant given this already calls the base CanInsert
|
// Base is redundant given this already calls the base CanInsert
|
||||||
// If that changes then update this
|
// If that changes then update this
|
||||||
return CanInsert(eventArgs.Dragged);
|
return EntitySystem.Get<DisposalUnitSystem>().CanInsert(this, eventArgs.Dragged);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool DragDropOn(DragDropEvent eventArgs)
|
public override bool DragDropOn(DragDropEvent eventArgs)
|
||||||
@@ -627,18 +193,6 @@ namespace Content.Server.Disposal.Unit.Components
|
|||||||
return true;
|
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]
|
[Verb]
|
||||||
private sealed class SelfInsertVerb : Verb<DisposalUnitComponent>
|
private sealed class SelfInsertVerb : Verb<DisposalUnitComponent>
|
||||||
{
|
{
|
||||||
@@ -682,14 +236,13 @@ namespace Content.Server.Disposal.Unit.Components
|
|||||||
|
|
||||||
protected override void Activate(IEntity user, DisposalUnitComponent component)
|
protected override void Activate(IEntity user, DisposalUnitComponent component)
|
||||||
{
|
{
|
||||||
component.Engaged = true;
|
EntitySystem.Get<DisposalUnitSystem>().Engage(component);
|
||||||
component.TryFlush();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
|
void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
|
||||||
{
|
{
|
||||||
TryEjectContents();
|
EntitySystem.Get<DisposalUnitSystem>().TryEjectContents(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,537 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using Content.Server.Atmos.EntitySystems;
|
||||||
using Content.Server.Disposal.Unit.Components;
|
using Content.Server.Disposal.Unit.Components;
|
||||||
using Content.Server.Construction.Components;
|
using Content.Server.Construction.Components;
|
||||||
|
using Content.Server.Disposal.Tube.Components;
|
||||||
|
using Content.Server.Hands.Components;
|
||||||
|
using Content.Server.Items;
|
||||||
|
using Content.Server.Power.Components;
|
||||||
|
using Content.Shared.ActionBlocker;
|
||||||
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Disposal;
|
||||||
|
using Content.Shared.Disposal.Components;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Movement;
|
||||||
|
using Content.Shared.Notification.Managers;
|
||||||
|
using Content.Shared.Throwing;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Log;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
namespace Content.Server.Disposal.Unit.EntitySystems
|
namespace Content.Server.Disposal.Unit.EntitySystems
|
||||||
{
|
{
|
||||||
public sealed class DisposalUnitSystem : EntitySystem
|
public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||||
{
|
{
|
||||||
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||||
|
|
||||||
|
[Dependency] private readonly AtmosphereSystem _atmosSystem = default!;
|
||||||
|
|
||||||
|
private readonly List<DisposalUnitComponent> _activeDisposals = new();
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
SubscribeLocalEvent<DisposalUnitComponent, AnchoredEvent>(OnAnchored);
|
SubscribeLocalEvent<DisposalUnitComponent, AnchoredEvent>(OnAnchored);
|
||||||
SubscribeLocalEvent<DisposalUnitComponent, UnanchoredEvent>(OnUnanchored);
|
SubscribeLocalEvent<DisposalUnitComponent, UnanchoredEvent>(OnUnanchored);
|
||||||
|
// TODO: Predict me when hands predicted
|
||||||
|
SubscribeLocalEvent<DisposalUnitComponent, RelayMovementEntityEvent>(HandleMovement);
|
||||||
|
SubscribeLocalEvent<DisposalUnitComponent, PowerChangedEvent>(HandlePowerChange);
|
||||||
|
|
||||||
|
// Component lifetime
|
||||||
|
SubscribeLocalEvent<DisposalUnitComponent, ComponentInit>(HandleDisposalInit);
|
||||||
|
SubscribeLocalEvent<DisposalUnitComponent, ComponentShutdown>(HandleDisposalShutdown);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<DisposalUnitComponent, ThrowCollideEvent>(HandleThrowCollide);
|
||||||
|
|
||||||
|
// Interactions
|
||||||
|
SubscribeLocalEvent<DisposalUnitComponent, ActivateInWorldEvent>(HandleActivate);
|
||||||
|
SubscribeLocalEvent<DisposalUnitComponent, InteractHandEvent>(HandleInteractHand);
|
||||||
|
SubscribeLocalEvent<DisposalUnitComponent, InteractUsingEvent>(HandleInteractUsing);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnAnchored(EntityUid uid, DisposalUnitComponent component, AnchoredEvent args)
|
public override void Update(float frameTime)
|
||||||
{
|
{
|
||||||
component.UpdateVisualState();
|
base.Update(frameTime);
|
||||||
|
for (var i = _activeDisposals.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var comp = _activeDisposals[i];
|
||||||
|
if (!Update(comp, frameTime)) continue;
|
||||||
|
_activeDisposals.RemoveAt(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnUnanchored(EntityUid uid, DisposalUnitComponent component, UnanchoredEvent args)
|
#region UI Handlers
|
||||||
|
public void ToggleEngage(DisposalUnitComponent component)
|
||||||
{
|
{
|
||||||
component.UpdateVisualState();
|
component.Engaged ^= true;
|
||||||
component.TryEjectContents();
|
|
||||||
|
if (component.Engaged)
|
||||||
|
{
|
||||||
|
Engage(component);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Disengage(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TogglePower(DisposalUnitComponent component)
|
||||||
|
{
|
||||||
|
if (!ComponentManager.TryGetComponent(component.Owner.Uid, out ApcPowerReceiverComponent? receiver))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
receiver.PowerDisabled = !receiver.PowerDisabled;
|
||||||
|
UpdateInterface(component, receiver.Powered);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Eventbus Handlers
|
||||||
|
private void HandleActivate(EntityUid uid, DisposalUnitComponent component, ActivateInWorldEvent args)
|
||||||
|
{
|
||||||
|
if (!args.User.TryGetComponent(out ActorComponent? actor))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.Handled = true;
|
||||||
|
|
||||||
|
if (IsValidInteraction(args))
|
||||||
|
{
|
||||||
|
component.UserInterface?.Open(actor.PlayerSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleInteractHand(EntityUid uid, DisposalUnitComponent component, InteractHandEvent args)
|
||||||
|
{
|
||||||
|
if (!args.User.TryGetComponent(out ActorComponent? actor)) return;
|
||||||
|
|
||||||
|
// Duplicated code here, not sure how else to get actor inside to make UserInterface happy.
|
||||||
|
|
||||||
|
if (!IsValidInteraction(args)) return;
|
||||||
|
component.UserInterface?.Open(actor.PlayerSession);
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleInteractUsing(EntityUid uid, DisposalUnitComponent component, InteractUsingEvent args)
|
||||||
|
{
|
||||||
|
if (!args.User.TryGetComponent(out HandsComponent? hands))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CanInsert(component, args.Used) || !hands.Drop(args.Used, component.Container))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AfterInsert(component, args.Used);
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown items have a chance of bouncing off the unit and not going in.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleThrowCollide(EntityUid uid, DisposalUnitComponent component, ThrowCollideEvent args)
|
||||||
|
{
|
||||||
|
if (!CanInsert(component, args.Thrown) ||
|
||||||
|
_robustRandom.NextDouble() > 0.75 ||
|
||||||
|
!component.Container.Insert(args.Thrown))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AfterInsert(component, args.Thrown);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDisposalInit(EntityUid uid, DisposalUnitComponent component, ComponentInit args)
|
||||||
|
{
|
||||||
|
component.Container = component.Owner.EnsureContainer<Container>(component.Name);
|
||||||
|
|
||||||
|
if (component.UserInterface != null)
|
||||||
|
{
|
||||||
|
component.UserInterface.OnReceiveMessage += component.OnUiReceiveMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateInterface(component, component.Powered);
|
||||||
|
|
||||||
|
if (!component.Owner.HasComponent<AnchorableComponent>())
|
||||||
|
{
|
||||||
|
Logger.WarningS("VitalComponentMissing", $"Disposal unit {uid} is missing an {nameof(AnchorableComponent)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDisposalShutdown(EntityUid uid, DisposalUnitComponent component, ComponentShutdown args)
|
||||||
|
{
|
||||||
|
foreach (var entity in component.Container.ContainedEntities.ToArray())
|
||||||
|
{
|
||||||
|
component.Container.ForceRemove(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
component.UserInterface?.CloseAll();
|
||||||
|
|
||||||
|
component.AutomaticEngageToken?.Cancel();
|
||||||
|
component.AutomaticEngageToken = null;
|
||||||
|
|
||||||
|
component.Container = null!;
|
||||||
|
_activeDisposals.Remove(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandlePowerChange(EntityUid uid, DisposalUnitComponent component, PowerChangedEvent args)
|
||||||
|
{
|
||||||
|
// TODO: Need to check the other stuff.
|
||||||
|
if (!args.Powered)
|
||||||
|
{
|
||||||
|
component.AutomaticEngageToken?.Cancel();
|
||||||
|
component.AutomaticEngageToken = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleStateChange(component, args.Powered && component.State == SharedDisposalUnitComponent.PressureState.Pressurizing);
|
||||||
|
UpdateVisualState(component);
|
||||||
|
UpdateInterface(component, args.Powered);
|
||||||
|
|
||||||
|
if (component.Engaged && !TryFlush(component))
|
||||||
|
{
|
||||||
|
TryQueueEngage(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add or remove this disposal from the active ones for updating.
|
||||||
|
/// </summary>
|
||||||
|
public void HandleStateChange(DisposalUnitComponent component, bool active)
|
||||||
|
{
|
||||||
|
if (active)
|
||||||
|
{
|
||||||
|
if (!_activeDisposals.Contains(component))
|
||||||
|
_activeDisposals.Add(component);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_activeDisposals.Remove(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleMovement(EntityUid uid, DisposalUnitComponent component, RelayMovementEntityEvent args)
|
||||||
|
{
|
||||||
|
var currentTime = GameTiming.CurTime;
|
||||||
|
|
||||||
|
if (!args.Entity.TryGetComponent(out HandsComponent? hands) ||
|
||||||
|
hands.Count == 0 ||
|
||||||
|
currentTime < component.LastExitAttempt + ExitAttemptDelay)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
component.LastExitAttempt = currentTime;
|
||||||
|
Remove(component, args.Entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAnchored(EntityUid uid, DisposalUnitComponent component, AnchoredEvent args)
|
||||||
|
{
|
||||||
|
UpdateVisualState(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUnanchored(EntityUid uid, DisposalUnitComponent component, UnanchoredEvent args)
|
||||||
|
{
|
||||||
|
UpdateVisualState(component);
|
||||||
|
TryEjectContents(component);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Work out if we can stop updating this disposals component i.e. full pressure and nothing colliding.
|
||||||
|
/// </summary>
|
||||||
|
private bool Update(DisposalUnitComponent component, float frameTime)
|
||||||
|
{
|
||||||
|
var oldPressure = component.Pressure;
|
||||||
|
|
||||||
|
component.Pressure = MathF.Min(1.0f, component.Pressure + PressurePerSecond * frameTime);
|
||||||
|
|
||||||
|
var state = component.State;
|
||||||
|
|
||||||
|
if (oldPressure < 1 && state == SharedDisposalUnitComponent.PressureState.Ready)
|
||||||
|
{
|
||||||
|
UpdateVisualState(component);
|
||||||
|
|
||||||
|
if (component.Engaged)
|
||||||
|
{
|
||||||
|
TryFlush(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box2? disposalsBounds = null;
|
||||||
|
var count = component.RecentlyEjected.Count;
|
||||||
|
|
||||||
|
if (count > 0)
|
||||||
|
{
|
||||||
|
if (!component.Owner.TryGetComponent(out PhysicsComponent? disposalsBody))
|
||||||
|
{
|
||||||
|
component.RecentlyEjected.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
disposalsBounds = disposalsBody.GetWorldAABB();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = component.RecentlyEjected.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var uid = component.RecentlyEjected[i];
|
||||||
|
if (EntityManager.EntityExists(uid) &&
|
||||||
|
ComponentManager.TryGetComponent(uid, out PhysicsComponent? body))
|
||||||
|
{
|
||||||
|
// TODO: We need to use a specific collision method (which sloth hasn't coded yet) for actual bounds overlaps.
|
||||||
|
// Check for itemcomp as we won't just block the disposal unit "sleeping" for something it can't collide with anyway.
|
||||||
|
if (!ComponentManager.HasComponent<ItemComponent>(uid) && body.GetWorldAABB().Intersects(disposalsBounds!.Value)) continue;
|
||||||
|
component.RecentlyEjected.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count != component.RecentlyEjected.Count)
|
||||||
|
component.Dirty();
|
||||||
|
|
||||||
|
return state == SharedDisposalUnitComponent.PressureState.Ready && component.RecentlyEjected.Count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsValidInteraction(ITargetedInteractEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
if (!Get<ActionBlockerSystem>().CanInteract(eventArgs.User))
|
||||||
|
{
|
||||||
|
eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("ui-disposal-unit-is-valid-interaction-cannot=interact"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventArgs.User.IsInContainer())
|
||||||
|
{
|
||||||
|
eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("ui-disposal-unit-is-valid-interaction-cannot-reach"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// This popup message doesn't appear on clicks, even when code was seperate. Unsure why.
|
||||||
|
|
||||||
|
if (!eventArgs.User.HasComponent<IHandsComponent>())
|
||||||
|
{
|
||||||
|
eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("ui-disposal-unit-is-valid-interaction-no-hands"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryFlush(DisposalUnitComponent component)
|
||||||
|
{
|
||||||
|
if (component.Deleted || !CanFlush(component))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var grid = _mapManager.GetGrid(component.Owner.Transform.GridID);
|
||||||
|
var coords = component.Owner.Transform.Coordinates;
|
||||||
|
var entry = grid.GetLocal(coords)
|
||||||
|
.FirstOrDefault(entity => EntityManager.ComponentManager.HasComponent<DisposalEntryComponent>(entity));
|
||||||
|
|
||||||
|
if (entry == default)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var air = component.Air;
|
||||||
|
var entryComponent = EntityManager.ComponentManager.GetComponent<DisposalEntryComponent>(entry);
|
||||||
|
|
||||||
|
if (_atmosSystem.GetTileMixture(component.Owner.Transform.Coordinates, true) is {Temperature: > 0} environment)
|
||||||
|
{
|
||||||
|
var transferMoles = 0.1f * (0.05f * Atmospherics.OneAtmosphere * 1.01f - air.Pressure) * air.Volume / (environment.Temperature * Atmospherics.R);
|
||||||
|
|
||||||
|
component.Air = environment.Remove(transferMoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
entryComponent.TryInsert(component);
|
||||||
|
|
||||||
|
component.AutomaticEngageToken?.Cancel();
|
||||||
|
component.AutomaticEngageToken = null;
|
||||||
|
|
||||||
|
component.Pressure = 0;
|
||||||
|
|
||||||
|
component.Engaged = false;
|
||||||
|
|
||||||
|
HandleStateChange(component, true);
|
||||||
|
UpdateVisualState(component, true);
|
||||||
|
UpdateInterface(component, component.Powered);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateInterface(DisposalUnitComponent component, bool powered)
|
||||||
|
{
|
||||||
|
var stateString = Loc.GetString($"{component.State}");
|
||||||
|
var state = new SharedDisposalUnitComponent.DisposalUnitBoundUserInterfaceState(component.Owner.Name, stateString, EstimatedFullPressure(component), powered, component.Engaged);
|
||||||
|
component.UserInterface?.SetState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TimeSpan EstimatedFullPressure(DisposalUnitComponent component)
|
||||||
|
{
|
||||||
|
if (component.State == SharedDisposalUnitComponent.PressureState.Ready) return TimeSpan.Zero;
|
||||||
|
|
||||||
|
var currentTime = GameTiming.CurTime;
|
||||||
|
var pressure = component.Pressure;
|
||||||
|
|
||||||
|
return TimeSpan.FromSeconds(currentTime.TotalSeconds + (1.0f - pressure) / PressurePerSecond);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateVisualState(DisposalUnitComponent component)
|
||||||
|
{
|
||||||
|
UpdateVisualState(component, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateVisualState(DisposalUnitComponent component, bool flush)
|
||||||
|
{
|
||||||
|
if (!component.Owner.TryGetComponent(out SharedAppearanceComponent? appearance))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!component.Owner.Transform.Anchored)
|
||||||
|
{
|
||||||
|
appearance.SetData(SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.UnAnchored);
|
||||||
|
appearance.SetData(SharedDisposalUnitComponent.Visuals.Handle, SharedDisposalUnitComponent.HandleState.Normal);
|
||||||
|
appearance.SetData(SharedDisposalUnitComponent.Visuals.Light, SharedDisposalUnitComponent.LightState.Off);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appearance.SetData(SharedDisposalUnitComponent.Visuals.VisualState, component.Pressure < 1 ? SharedDisposalUnitComponent.VisualState.Charging : SharedDisposalUnitComponent.VisualState.Anchored);
|
||||||
|
|
||||||
|
appearance.SetData(SharedDisposalUnitComponent.Visuals.Handle, component.Engaged
|
||||||
|
? SharedDisposalUnitComponent.HandleState.Engaged
|
||||||
|
: SharedDisposalUnitComponent.HandleState.Normal);
|
||||||
|
|
||||||
|
if (!component.Powered)
|
||||||
|
{
|
||||||
|
appearance.SetData(SharedDisposalUnitComponent.Visuals.Light, SharedDisposalUnitComponent.LightState.Off);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flush)
|
||||||
|
{
|
||||||
|
appearance.SetData(SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Flushing);
|
||||||
|
appearance.SetData(SharedDisposalUnitComponent.Visuals.Light, SharedDisposalUnitComponent.LightState.Off);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.ContainedEntities.Count > 0)
|
||||||
|
{
|
||||||
|
appearance.SetData(SharedDisposalUnitComponent.Visuals.Light, SharedDisposalUnitComponent.LightState.Full);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appearance.SetData(SharedDisposalUnitComponent.Visuals.Light, component.Pressure < 1
|
||||||
|
? SharedDisposalUnitComponent.LightState.Charging
|
||||||
|
: SharedDisposalUnitComponent.LightState.Ready);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(DisposalUnitComponent component, IEntity entity)
|
||||||
|
{
|
||||||
|
component.Container.Remove(entity);
|
||||||
|
|
||||||
|
if (component.ContainedEntities.Count == 0)
|
||||||
|
{
|
||||||
|
component.AutomaticEngageToken?.Cancel();
|
||||||
|
component.AutomaticEngageToken = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!component.RecentlyEjected.Contains(entity.Uid))
|
||||||
|
component.RecentlyEjected.Add(entity.Uid);
|
||||||
|
|
||||||
|
component.Dirty();
|
||||||
|
HandleStateChange(component, true);
|
||||||
|
UpdateVisualState(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanFlush(DisposalUnitComponent component)
|
||||||
|
{
|
||||||
|
return component.State == SharedDisposalUnitComponent.PressureState.Ready && component.Powered && component.Owner.Transform.Anchored;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Engage(DisposalUnitComponent component)
|
||||||
|
{
|
||||||
|
component.Engaged = true;
|
||||||
|
UpdateVisualState(component);
|
||||||
|
UpdateInterface(component, component.Powered);
|
||||||
|
|
||||||
|
if (CanFlush(component))
|
||||||
|
{
|
||||||
|
component.Owner.SpawnTimer(component.FlushDelay, () => TryFlush(component));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disengage(DisposalUnitComponent component)
|
||||||
|
{
|
||||||
|
component.Engaged = false;
|
||||||
|
UpdateVisualState(component);
|
||||||
|
UpdateInterface(component, component.Powered);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove all entities currently in the disposal unit.
|
||||||
|
/// </summary>
|
||||||
|
public void TryEjectContents(DisposalUnitComponent component)
|
||||||
|
{
|
||||||
|
foreach (var entity in component.Container.ContainedEntities.ToArray())
|
||||||
|
{
|
||||||
|
Remove(component, entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanInsert(SharedDisposalUnitComponent component, IEntity entity)
|
||||||
|
{
|
||||||
|
if (!base.CanInsert(component, entity) || component is not DisposalUnitComponent serverComp)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return serverComp.Container.CanInsert(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If something is inserted (or the likes) then we'll queue up a flush in the future.
|
||||||
|
/// </summary>
|
||||||
|
public void TryQueueEngage(DisposalUnitComponent component)
|
||||||
|
{
|
||||||
|
if (component.Deleted || !component.Powered && component.ContainedEntities.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
component.AutomaticEngageToken = new CancellationTokenSource();
|
||||||
|
|
||||||
|
component.Owner.SpawnTimer(component._automaticEngageTime, () =>
|
||||||
|
{
|
||||||
|
if (!TryFlush(component))
|
||||||
|
{
|
||||||
|
TryQueueEngage(component);
|
||||||
|
}
|
||||||
|
}, component.AutomaticEngageToken.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AfterInsert(DisposalUnitComponent component, IEntity entity)
|
||||||
|
{
|
||||||
|
TryQueueEngage(component);
|
||||||
|
|
||||||
|
if (entity.TryGetComponent(out ActorComponent? actor))
|
||||||
|
{
|
||||||
|
component.UserInterface?.Close(actor.PlayerSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateVisualState(component);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.Serialization;
|
|
||||||
using Robust.Shared.Utility;
|
|
||||||
|
|
||||||
namespace Content.Shared.Disposal.Components
|
|
||||||
{
|
|
||||||
public abstract class SharedDisposalMailingUnitComponent : SharedDisposalUnitComponent
|
|
||||||
{
|
|
||||||
public override string Name => "DisposalMailingUnit";
|
|
||||||
|
|
||||||
public const string TAGS_MAIL = "mail";
|
|
||||||
|
|
||||||
public const string NET_TAG = "tag";
|
|
||||||
public const string NET_SRC = "src";
|
|
||||||
public const string NET_TARGET = "target";
|
|
||||||
public const string NET_CMD_SENT = "mail_sent";
|
|
||||||
public const string NET_CMD_REQUEST = "get_mailer_tag";
|
|
||||||
public const string NET_CMD_RESPONSE = "mailer_tag";
|
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public new enum UiButton : byte
|
|
||||||
{
|
|
||||||
Eject,
|
|
||||||
Engage,
|
|
||||||
Power
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public class DisposalMailingUnitBoundUserInterfaceState : BoundUserInterfaceState, IEquatable<DisposalMailingUnitBoundUserInterfaceState>, ICloneable
|
|
||||||
{
|
|
||||||
public readonly string UnitName;
|
|
||||||
public readonly string UnitState;
|
|
||||||
public readonly float Pressure;
|
|
||||||
public readonly bool Powered;
|
|
||||||
public readonly bool Engaged;
|
|
||||||
public readonly string Tag;
|
|
||||||
public readonly List<string> Tags;
|
|
||||||
public readonly string? Target;
|
|
||||||
|
|
||||||
public DisposalMailingUnitBoundUserInterfaceState(string unitName, string unitState, float pressure, bool powered,
|
|
||||||
bool engaged, string tag, List<string> tags, string? target)
|
|
||||||
{
|
|
||||||
UnitName = unitName;
|
|
||||||
UnitState = unitState;
|
|
||||||
Pressure = pressure;
|
|
||||||
Powered = powered;
|
|
||||||
Engaged = engaged;
|
|
||||||
Tag = tag;
|
|
||||||
Tags = tags;
|
|
||||||
Target = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
public object Clone()
|
|
||||||
{
|
|
||||||
return new DisposalMailingUnitBoundUserInterfaceState(UnitName, UnitState, Pressure, Powered, Engaged, Tag, (List<string>)Tags.Clone(), Target);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(DisposalMailingUnitBoundUserInterfaceState? other)
|
|
||||||
{
|
|
||||||
if (other is null) return false;
|
|
||||||
if (ReferenceEquals(this, other)) return true;
|
|
||||||
return UnitName == other.UnitName &&
|
|
||||||
UnitState == other.UnitState &&
|
|
||||||
Powered == other.Powered &&
|
|
||||||
Engaged == other.Engaged &&
|
|
||||||
Pressure.Equals(other.Pressure) &&
|
|
||||||
Tag == other.Tag &&
|
|
||||||
Target == other.Target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Message data sent from client to server when a mailing unit ui button is pressed.
|
|
||||||
/// </summary>
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public new class UiButtonPressedMessage : BoundUserInterfaceMessage
|
|
||||||
{
|
|
||||||
public readonly UiButton Button;
|
|
||||||
|
|
||||||
public UiButtonPressedMessage(UiButton button)
|
|
||||||
{
|
|
||||||
Button = button;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Message data sent from client to server when the mailing units target is updated.
|
|
||||||
/// </summary>
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public class UiTargetUpdateMessage : BoundUserInterfaceMessage
|
|
||||||
{
|
|
||||||
public readonly string? Target;
|
|
||||||
|
|
||||||
public UiTargetUpdateMessage(string? target)
|
|
||||||
{
|
|
||||||
Target = target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public enum DisposalMailingUnitUiKey
|
|
||||||
{
|
|
||||||
Key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,27 @@
|
|||||||
using System;
|
using System;
|
||||||
using Content.Shared.Body.Components;
|
using System.Collections.Generic;
|
||||||
using Content.Shared.DragDrop;
|
using Content.Shared.DragDrop;
|
||||||
using Content.Shared.Item;
|
|
||||||
using Content.Shared.MobState;
|
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.Physics;
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Players;
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
using Robust.Shared.ViewVariables;
|
|
||||||
|
|
||||||
namespace Content.Shared.Disposal.Components
|
namespace Content.Shared.Disposal.Components
|
||||||
{
|
{
|
||||||
|
[NetworkedComponent]
|
||||||
public abstract class SharedDisposalUnitComponent : Component, IDragDropOn
|
public abstract class SharedDisposalUnitComponent : Component, IDragDropOn
|
||||||
{
|
{
|
||||||
public override string Name => "DisposalUnit";
|
public override string Name => "DisposalUnit";
|
||||||
|
|
||||||
[ViewVariables]
|
// TODO: Could maybe turn the contact off instead far more cheaply as farseer (though not box2d) had support for it?
|
||||||
public bool Anchored =>
|
// Need to suss it out.
|
||||||
!Owner.TryGetComponent(out IPhysBody? physics) ||
|
/// <summary>
|
||||||
physics.BodyType == BodyType.Static;
|
/// We'll track whatever just left disposals so we know what collision we need to ignore until they stop intersecting our BB.
|
||||||
|
/// </summary>
|
||||||
|
public List<EntityUid> RecentlyEjected = new();
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
public enum Visuals
|
public enum Visuals : byte
|
||||||
{
|
{
|
||||||
VisualState,
|
VisualState,
|
||||||
Handle,
|
Handle,
|
||||||
@@ -28,7 +29,7 @@ namespace Content.Shared.Disposal.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
public enum VisualState
|
public enum VisualState : byte
|
||||||
{
|
{
|
||||||
UnAnchored,
|
UnAnchored,
|
||||||
Anchored,
|
Anchored,
|
||||||
@@ -37,14 +38,14 @@ namespace Content.Shared.Disposal.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
public enum HandleState
|
public enum HandleState : byte
|
||||||
{
|
{
|
||||||
Normal,
|
Normal,
|
||||||
Engaged
|
Engaged
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
public enum LightState
|
public enum LightState : byte
|
||||||
{
|
{
|
||||||
Off,
|
Off,
|
||||||
Charging,
|
Charging,
|
||||||
@@ -53,7 +54,7 @@ namespace Content.Shared.Disposal.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
public enum UiButton
|
public enum UiButton : byte
|
||||||
{
|
{
|
||||||
Eject,
|
Eject,
|
||||||
Engage,
|
Engage,
|
||||||
@@ -61,15 +62,26 @@ namespace Content.Shared.Disposal.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
public enum PressureState
|
public enum PressureState : byte
|
||||||
{
|
{
|
||||||
Ready,
|
Ready,
|
||||||
Pressurizing
|
Pressurizing
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Update(float frameTime)
|
public override ComponentState GetComponentState(ICommonSession player)
|
||||||
{
|
{
|
||||||
return;
|
return new DisposalUnitComponentState(RecentlyEjected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
protected sealed class DisposalUnitComponentState : ComponentState
|
||||||
|
{
|
||||||
|
public List<EntityUid> RecentlyEjected;
|
||||||
|
|
||||||
|
public DisposalUnitComponentState(List<EntityUid> uids)
|
||||||
|
{
|
||||||
|
RecentlyEjected = uids;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
@@ -77,16 +89,16 @@ namespace Content.Shared.Disposal.Components
|
|||||||
{
|
{
|
||||||
public readonly string UnitName;
|
public readonly string UnitName;
|
||||||
public readonly string UnitState;
|
public readonly string UnitState;
|
||||||
public readonly float Pressure;
|
public readonly TimeSpan FullPressureTime;
|
||||||
public readonly bool Powered;
|
public readonly bool Powered;
|
||||||
public readonly bool Engaged;
|
public readonly bool Engaged;
|
||||||
|
|
||||||
public DisposalUnitBoundUserInterfaceState(string unitName, string unitState, float pressure, bool powered,
|
public DisposalUnitBoundUserInterfaceState(string unitName, string unitState, TimeSpan fullPressureTime, bool powered,
|
||||||
bool engaged)
|
bool engaged)
|
||||||
{
|
{
|
||||||
UnitName = unitName;
|
UnitName = unitName;
|
||||||
UnitState = unitState;
|
UnitState = unitState;
|
||||||
Pressure = pressure;
|
FullPressureTime = fullPressureTime;
|
||||||
Powered = powered;
|
Powered = powered;
|
||||||
Engaged = engaged;
|
Engaged = engaged;
|
||||||
}
|
}
|
||||||
@@ -99,7 +111,7 @@ namespace Content.Shared.Disposal.Components
|
|||||||
UnitState == other.UnitState &&
|
UnitState == other.UnitState &&
|
||||||
Powered == other.Powered &&
|
Powered == other.Powered &&
|
||||||
Engaged == other.Engaged &&
|
Engaged == other.Engaged &&
|
||||||
Pressure.Equals(other.Pressure);
|
FullPressureTime.Equals(other.FullPressureTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,37 +130,15 @@ namespace Content.Shared.Disposal.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
public enum DisposalUnitUiKey
|
public enum DisposalUnitUiKey : byte
|
||||||
{
|
{
|
||||||
Key
|
Key
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool CanInsert(IEntity entity)
|
// TODO: Unfortunately these aren't really ECS yet so soontm
|
||||||
{
|
|
||||||
if (!Anchored)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// TODO: Probably just need a disposable tag.
|
|
||||||
if (!entity.TryGetComponent(out SharedItemComponent? storable) &&
|
|
||||||
!entity.HasComponent<SharedBodyComponent>())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (!entity.TryGetComponent(out IPhysBody? physics) ||
|
|
||||||
!physics.CanCollide && storable == null)
|
|
||||||
{
|
|
||||||
if (!(entity.TryGetComponent(out IMobStateComponent? damageState) && damageState.IsDead())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual bool CanDragDropOn(DragDropEvent eventArgs)
|
public virtual bool CanDragDropOn(DragDropEvent eventArgs)
|
||||||
{
|
{
|
||||||
return CanInsert(eventArgs.Dragged);
|
return EntitySystem.Get<SharedDisposalUnitSystem>().CanInsert(this, eventArgs.Dragged);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract bool DragDropOn(DragDropEvent eventArgs);
|
public abstract bool DragDropOn(DragDropEvent eventArgs);
|
||||||
|
|||||||
@@ -1,23 +1,73 @@
|
|||||||
using Content.Shared.Disposal.Components;
|
using System;
|
||||||
|
using Content.Shared.Body.Components;
|
||||||
|
using Content.Shared.Disposal.Components;
|
||||||
|
using Content.Shared.Item;
|
||||||
|
using Content.Shared.MobState;
|
||||||
|
using Content.Shared.Throwing;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Physics;
|
||||||
|
using Robust.Shared.Physics.Dynamics;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Shared.Disposal
|
namespace Content.Shared.Disposal
|
||||||
{
|
{
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public sealed class SharedDisposalUnitSystem : EntitySystem
|
public abstract class SharedDisposalUnitSystem : EntitySystem
|
||||||
{
|
{
|
||||||
public override void Update(float frameTime)
|
[Dependency] protected readonly IGameTiming GameTiming = default!;
|
||||||
|
|
||||||
|
protected static TimeSpan ExitAttemptDelay = TimeSpan.FromSeconds(0.5);
|
||||||
|
|
||||||
|
// Percentage
|
||||||
|
public const float PressurePerSecond = 0.05f;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
foreach (var comp in ComponentManager.EntityQuery<SharedDisposalUnitComponent>(true))
|
base.Initialize();
|
||||||
{
|
SubscribeLocalEvent<SharedDisposalUnitComponent, PreventCollideEvent>(HandlePreventCollide);
|
||||||
comp.Update(frameTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var comp in ComponentManager.EntityQuery<SharedDisposalMailingUnitComponent>(true))
|
private void HandlePreventCollide(EntityUid uid, SharedDisposalUnitComponent component, PreventCollideEvent args)
|
||||||
{
|
{
|
||||||
comp.Update(frameTime);
|
var otherBody = args.BodyB.Owner.Uid;
|
||||||
|
|
||||||
|
// Items dropped shouldn't collide but items thrown should
|
||||||
|
if (ComponentManager.HasComponent<SharedItemComponent>(otherBody) &&
|
||||||
|
!ComponentManager.HasComponent<ThrownItemComponent>(otherBody))
|
||||||
|
{
|
||||||
|
args.Cancel();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (component.RecentlyEjected.Contains(otherBody))
|
||||||
|
{
|
||||||
|
args.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool CanInsert(SharedDisposalUnitComponent component, IEntity entity)
|
||||||
|
{
|
||||||
|
if (!component.Owner.Transform.Anchored)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// TODO: Probably just need a disposable tag.
|
||||||
|
if (!entity.TryGetComponent(out SharedItemComponent? storable) &&
|
||||||
|
!entity.HasComponent<SharedBodyComponent>())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!entity.TryGetComponent(out IPhysBody? physics) ||
|
||||||
|
!physics.CanCollide && storable == null)
|
||||||
|
{
|
||||||
|
if (!(entity.TryGetComponent(out IMobStateComponent? damageState) && damageState.IsDead())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ namespace Content.Shared.Interaction
|
|||||||
/// Raised when an entity is activated in the world.
|
/// Raised when an entity is activated in the world.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ActivateInWorldEvent : HandledEntityEventArgs
|
public class ActivateInWorldEvent : HandledEntityEventArgs, ITargetedInteractEventArgs
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Entity that activated the target world entity.
|
/// Entity that activated the target world entity.
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace Content.Shared.Interaction
|
|||||||
/// Raised directed on a target entity when it is interacted with by a user with an empty hand.
|
/// Raised directed on a target entity when it is interacted with by a user with an empty hand.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class InteractHandEvent : HandledEntityEventArgs
|
public class InteractHandEvent : HandledEntityEventArgs, ITargetedInteractEventArgs
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Entity that triggered the interaction.
|
/// Entity that triggered the interaction.
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ namespace Content.Shared.Movement.EntitySystems
|
|||||||
{
|
{
|
||||||
var relayEntityMoveMessage = new RelayMovementEntityMessage(owner);
|
var relayEntityMoveMessage = new RelayMovementEntityMessage(owner);
|
||||||
owner.Transform.Parent!.Owner.SendMessage(owner.Transform, relayEntityMoveMessage);
|
owner.Transform.Parent!.Owner.SendMessage(owner.Transform, relayEntityMoveMessage);
|
||||||
|
|
||||||
|
var relayMoveEvent = new RelayMovementEntityEvent(owner);
|
||||||
|
owner.EntityManager.EventBus.RaiseLocalEvent(owner.Transform.ParentUid, relayMoveEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,4 +15,14 @@ namespace Content.Shared.Movement
|
|||||||
Entity = entity;
|
Entity = entity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class RelayMovementEntityEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
public IEntity Entity { get; }
|
||||||
|
|
||||||
|
public RelayMovementEntityEvent(IEntity entity)
|
||||||
|
{
|
||||||
|
Entity = entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ namespace Content.Shared.Throwing
|
|||||||
{
|
{
|
||||||
// TODO: Just pass in the bodies directly
|
// TODO: Just pass in the bodies directly
|
||||||
var collideMsg = new ThrowCollideEvent(user, thrown.Owner, target.Owner);
|
var collideMsg = new ThrowCollideEvent(user, thrown.Owner, target.Owner);
|
||||||
RaiseLocalEvent(collideMsg);
|
RaiseLocalEvent(target.Owner.Uid, collideMsg);
|
||||||
if (collideMsg.Handled)
|
if (collideMsg.Handled)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -26,10 +26,11 @@
|
|||||||
bounds: "-0.4,-0.25,0.4,0.25"
|
bounds: "-0.4,-0.25,0.4,0.25"
|
||||||
mass: 30
|
mass: 30
|
||||||
mask:
|
mask:
|
||||||
- SmallImpassable
|
- Impassable
|
||||||
layer:
|
layer:
|
||||||
- Opaque
|
- Opaque
|
||||||
- Impassable
|
- SmallImpassable
|
||||||
|
- VaultImpassable
|
||||||
- MobImpassable
|
- MobImpassable
|
||||||
- type: Destructible
|
- type: Destructible
|
||||||
thresholds:
|
thresholds:
|
||||||
@@ -77,20 +78,3 @@
|
|||||||
interfaces:
|
interfaces:
|
||||||
- key: enum.DisposalUnitUiKey.Key
|
- key: enum.DisposalUnitUiKey.Key
|
||||||
type: DisposalUnitBoundUserInterface
|
type: DisposalUnitBoundUserInterface
|
||||||
|
|
||||||
# - type: entity
|
|
||||||
# parent: DisposalUnitBase
|
|
||||||
# id: DisposalMailingUnit
|
|
||||||
# name: disposal mailing unit
|
|
||||||
# components:
|
|
||||||
# - type: Configuration
|
|
||||||
# keys:
|
|
||||||
# - Tag
|
|
||||||
# - type: DisposalMailingUnit
|
|
||||||
# flushTime: 2
|
|
||||||
# - type: UserInterface
|
|
||||||
# interfaces:
|
|
||||||
# - key: enum.DisposalMailingUnitUiKey.Key
|
|
||||||
# type: DisposalMailingUnitBoundUserInterface
|
|
||||||
# - key: enum.ConfigurationUiKey.Key
|
|
||||||
# type: ConfigurationBoundUserInterface
|
|
||||||
|
|||||||
Reference in New Issue
Block a user