Disposal Units tag their contents + UI comp state (#45613)

* commit

* also flipped

* oops

* fuck it added to the UI

* fix micro annoyance

* Unify components also doesn't work yet

* reuse the existing UI also clean up the UI also applying state headache

* cheat code

* flippy

* More interface changes

* yaml ;p

* private tagger

* betterer tagger

* private unit window

* limit tagger window

* access blocked

* typo

* validate

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
This commit is contained in:
āda 2026-09-22 08:55:54 +00:00 • committed by GitHub
parent b8640a4c58
commit 86e633be6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 226 additions and 120 deletions

View file

@ -8,48 +8,36 @@ namespace Content.Client.Disposal.Tagger
/// Initializes a <see cref="DisposalTaggerWindow"/> and updates it when new server messages are received.
/// </summary>
[UsedImplicitly]
public sealed class DisposalTaggerBoundUserInterface : BoundUserInterface
public sealed class DisposalTaggerBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
private DisposalTaggerWindow? _window;
private const int TagLimit = 30;
public DisposalTaggerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
public const int TagLimit = 30;
protected override void Open()
{
base.Open();
_window = this.CreateWindow<DisposalTaggerWindow>();
_window.OnRouteChanged += NewRoute;
_window.Confirm.OnPressed += _ => AcceptButtonPressed(_window.TagInput.Text);
_window.TagInput.OnTextEntered += args => AcceptButtonPressed(args.Text);
if (EntMan.TryGetComponent<DisposalTaggerComponent>(Owner, out var tagger) &&
tagger.Tag != string.Empty)
{
_window.TagInput.Text = tagger.Tag;
}
Update();
}
private void AcceptButtonPressed(string tag)
private void NewRoute(string route)
{
SendMessage(new DisposalTaggerUiActionMessage(tag, TagLimit));
SendMessage(new DisposalTaggerUiActionMessage(route, TagLimit));
Close();
}
protected override void UpdateState(BoundUserInterfaceState state)
public override void Update()
{
base.UpdateState(state);
base.Update();
if (state is not DisposalTaggerUserInterfaceState cast)
{
if (_window == null || !EntMan.TryGetComponent<DisposalTaggerComponent>(Owner, out var tagger))
return;
}
_window?.UpdateState(cast);
_window.Populate(tagger.Tag, tagger.Editable);
}
}
}

View file

@ -7,13 +7,10 @@
<Control MinSize="0 10" />
<BoxContainer Orientation="Horizontal">
<LineEdit Name="TagInput"
Access="Public"
HorizontalExpand="True"
MinSize="320 0" />
<Control MinSize="10 0" />
<Button Name="Confirm"
Access="Public"
Text="{Loc 'disposal-tagger-window-tag-confirm-button'}" />
<Button Name="Confirm" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View file

@ -14,20 +14,32 @@ public sealed partial class DisposalTaggerWindow : DefaultWindow
{
[Dependency] private IEntityManager _entManager = default!;
private readonly SharedDisposalHolderSystem _disposalHolder;
public event Action<string>? OnRouteChanged;
public DisposalTaggerWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_disposalHolder = _entManager.System<SharedDisposalHolderSystem>();
var disposalHolder = _entManager.System<SharedDisposalHolderSystem>();
TagInput.IsValid = s => disposalHolder.TagIsValid(s) && s.Length <= DisposalTaggerBoundUserInterface.TagLimit;
TagInput.IsValid = tag => _disposalHolder.TagIsValid(tag);
Confirm.OnPressed += _ => NewRoute(TagInput.Text);
TagInput.OnTextEntered += args => NewRoute(args.Text);
}
public void UpdateState(DisposalTaggerUserInterfaceState state)
public void Populate(string route, bool editable)
{
TagInput.Text = state.Tags;
TagInput.Text = route;
TagInput.Editable = editable;
Confirm.Disabled = !editable;
Confirm.Text = Loc.GetString(editable ? "generic-confirm" : "generic-disabled");
}
private void NewRoute(string route)
{
OnRouteChanged?.Invoke(route);
}
}

View file

@ -1,4 +1,6 @@
using Content.Client.Power.EntitySystems;
using Content.Client.UserInterface.Controls;
using Content.Shared.Access.Systems;
using Content.Shared.Disposal.Components;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
@ -6,13 +8,61 @@ using Robust.Client.UserInterface;
namespace Content.Client.Disposal.Unit
{
[UsedImplicitly]
public sealed class DisposalUnitBoundUserInterface : BoundUserInterface
public sealed class DisposalUnitBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private DisposalUnitWindow? _disposalUnitWindow;
public DisposalUnitBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
protected override void Open()
{
base.Open();
_disposalUnitWindow = this.CreateWindow<DisposalUnitWindow>();
_disposalUnitWindow.OpenCenteredRight();
_disposalUnitWindow.SetInfoFromEntity(EntMan, Owner);
_disposalUnitWindow.OnTogglePower += ButtonPressed;
_disposalUnitWindow.OnEject += ButtonPressed;
_disposalUnitWindow.OnEngage += ButtonPressed;
_disposalUnitWindow.OnChangeRouting += OpenRoutingWindow;
Update();
}
public override void Update()
{
base.Update();
if (_disposalUnitWindow == null)
return;
// Update general fields
if (!EntMan.TryGetComponent<DisposalUnitComponent>(Owner, out var component))
return;
var disposalSystem = EntMan.System<DisposalUnitSystem>();
_disposalUnitWindow.Populate(
EntMan.System<PowerReceiverSystem>().IsPowered(Owner),
component.Engaged,
disposalSystem.GetState((Owner, component)),
disposalSystem.EstimatedFullPressure((Owner, component)),
component.PressurePerSecond);
// Update if routing is enabled or accessible
if (!EntMan.TryGetComponent<DisposalTaggerComponent>(Owner, out var tagger))
{
_disposalUnitWindow.PopulateRouting(false, false);
return;
}
var hasAccess = false;
if (PlayerManager.LocalEntity != null)
{
var accessReader = EntMan.System<AccessReaderSystem>();
hasAccess = accessReader.IsAllowed((EntityUid)PlayerManager.LocalEntity, Owner);
}
_disposalUnitWindow.PopulateRouting(tagger.Editable, hasAccess);
}
private void ButtonPressed(DisposalUnitUiButton button)
@ -20,55 +70,9 @@ namespace Content.Client.Disposal.Unit
SendPredictedMessage(new DisposalUnitUiButtonPressedMessage(button));
}
protected override void Open()
private void OpenRoutingWindow()
{
base.Open();
_disposalUnitWindow = this.CreateWindow<DisposalUnitWindow>();
_disposalUnitWindow.OpenCenteredRight();
_disposalUnitWindow.Eject.OnPressed += _ => ButtonPressed(DisposalUnitUiButton.Eject);
_disposalUnitWindow.Engage.OnPressed += _ => ButtonPressed(DisposalUnitUiButton.Engage);
_disposalUnitWindow.Power.OnPressed += _ => ButtonPressed(DisposalUnitUiButton.Power);
if (EntMan.TryGetComponent(Owner, out DisposalUnitComponent? component))
{
Refresh((Owner, component));
}
}
public override void Update()
{
base.Update();
if (EntMan.TryGetComponent(Owner, out DisposalUnitComponent? component))
{
Refresh((Owner, component));
}
}
public void Refresh(Entity<DisposalUnitComponent> entity)
{
if (_disposalUnitWindow == null)
return;
var name = EntMan.GetComponent<MetaDataComponent>(entity.Owner).EntityName;
_disposalUnitWindow.Title = Loc.GetString("ui-disposal-unit-title", ("name", name));
if (!EntMan.TryGetComponent(entity.Owner, out DisposalUnitComponent? disposals))
return;
var disposalUnit = EntMan.System<DisposalUnitSystem>();
var disposalState = disposalUnit.GetState(entity);
var fullPressure = disposalUnit.EstimatedFullPressure((Owner, disposals));
var pressurePerSecond = disposals.PressurePerSecond;
_disposalUnitWindow.UnitState.Text = Loc.GetString($"disposal-unit-state-{disposalState}");
_disposalUnitWindow.FullPressure = disposalUnit.EstimatedFullPressure(entity);
_disposalUnitWindow.PressurePerSecond = entity.Comp.PressurePerSecond;
_disposalUnitWindow.PressureBar.UpdatePressure(fullPressure, pressurePerSecond);
_disposalUnitWindow.Power.Pressed = EntMan.System<PowerReceiverSystem>().IsPowered(Owner);
_disposalUnitWindow.Engage.Pressed = entity.Comp.Engaged;
SendPredictedMessage(new DisposalTaggerOpenUiMessage());
}
}
}

View file

@ -1,21 +1,25 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:disposal="clr-namespace:Content.Client.Disposal"
MinSize="300 140"
SetSize="300 160"
Resizable="False">
SetSize="300 160"
Resizable="False">
<BoxContainer Orientation="Vertical" Margin="10">
<BoxContainer Orientation="Horizontal" SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-state'}" />
<Label Name="UnitState" Access="Public"
<Label Name="UnitState"
Text="{Loc 'ui-disposal-unit-label-status'}" />
<Label Name="UnitStateUnpowered"
Visible="False"
Text="{Loc 'disposal-unit-state-Unpowered'}" />
<Control HorizontalExpand="True"/>
<controls:SwitchButton Name="Power"
OnStateText="" OffStateText=""/>
</BoxContainer>
<Control MinSize="0 5" />
<BoxContainer Orientation="Horizontal"
SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-pressure'}" />
<disposal:PressureBar Name="PressureBar"
Access="Public"
MinSize="190 20"
HorizontalAlignment="Right"
MinValue="0"
@ -26,19 +30,15 @@
<Control MinSize="0 10" />
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Button Name="Engage"
Access="Public"
Text="{Loc 'ui-disposal-unit-button-flush'}"
StyleClasses="OpenRight"
ToggleMode="True" />
<Button Name="Eject"
Access="Public"
Text="{Loc 'ui-disposal-unit-button-eject'}"
StyleClasses="OpenBoth" />
<Button Name="Power"
Access="Public"
Text="{Loc 'ui-disposal-unit-button-power'}"
StyleClasses="OpenLeft"
ToggleMode="True" />
<Button Name="Routing"
Text="{Loc 'ui-disposal-unit-button-routing'}"
StyleClasses="OpenLeft" />
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View file

@ -1,4 +1,5 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Disposal.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
@ -11,18 +12,79 @@ namespace Content.Client.Disposal.Unit
[GenerateTypedNameReferences]
public sealed partial class DisposalUnitWindow : FancyWindow
{
public TimeSpan FullPressure;
public float PressurePerSecond;
// Values for managing the pressure bar
private TimeSpan _fullPressure;
private float _pressurePerSecond;
public event Action<DisposalUnitUiButton>? OnTogglePower;
public event Action<DisposalUnitUiButton>? OnEject;
public event Action<DisposalUnitUiButton>? OnEngage;
public event Action? OnChangeRouting;
public DisposalUnitWindow()
{
RobustXamlLoader.Load(this);
Power.OnPressed += args => TogglePower(args.Button.Pressed);
Eject.OnPressed += _ => BeginEject();
Engage.OnPressed += _ => BeginEngage();
Routing.OnPressed += _ => ChangeRouting();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure, PressurePerSecond);
PressureBar.UpdatePressure(_fullPressure, _pressurePerSecond);
}
public void Populate(bool powered, bool engaged, DisposalsPressureState disposalState, TimeSpan fullPressure, float pressurePerSecond)
{
ToggleUnitStateText(powered);
UnitState.Text = Loc.GetString($"disposal-unit-state-{disposalState}");
_fullPressure = fullPressure;
_pressurePerSecond = pressurePerSecond;
Power.Pressed = powered;
Engage.Pressed = engaged;
}
public void PopulateRouting(bool editable, bool accessible)
{
Routing.Visible = editable;
Routing.Disabled = !accessible;
Routing.ToolTip = accessible ? null : Loc.GetString("generic-no-access");
}
/// <summary>
/// This trick is used to hide power being unpredicted so the UI feels responsive.
/// </summary>
private void ToggleUnitStateText(bool powered)
{
UnitState.Visible = powered;
UnitStateUnpowered.Visible = !powered;
}
private void TogglePower(bool powered)
{
ToggleUnitStateText(powered);
OnTogglePower?.Invoke(DisposalUnitUiButton.Power);
}
private void BeginEject()
{
OnEject?.Invoke(DisposalUnitUiButton.Eject);
}
private void BeginEngage()
{
OnEngage?.Invoke(DisposalUnitUiButton.Engage);
}
private void ChangeRouting()
{
OnChangeRouting?.Invoke();
}
}
}

View file

@ -7,13 +7,14 @@ namespace Content.Shared.Disposal.Components;
/// <summary>
/// Entities that pass through disposal tubes with this component can be marked with a tag.
/// Entities flushed from a disposal unit with this component can be marked with a tag.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(DisposalTaggerSystem))]
public sealed partial class DisposalTaggerComponent : Component
{
/// <summary>
/// Tag to apply to passing entities.
/// Tag to apply to passing or flushing entities.
/// </summary>
[DataField, AutoNetworkedField]
public string Tag = string.Empty;
@ -23,6 +24,12 @@ public sealed partial class DisposalTaggerComponent : Component
/// </summary>
[DataField]
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
/// <summary>
/// If false editing the tag is disabled.
/// </summary>
[DataField, AutoNetworkedField]
public bool Editable = true;
}
/// <summary>
@ -40,18 +47,10 @@ public sealed class DisposalTaggerUiActionMessage : BoundUserInterfaceMessage
}
/// <summary>
/// Sends tag data to disposal tagger UIs.
/// A message to open the disposal tagger UI sent from a separate UI.
/// </summary>
[Serializable, NetSerializable]
public sealed class DisposalTaggerUserInterfaceState : BoundUserInterfaceState
{
public readonly string Tags;
public DisposalTaggerUserInterfaceState(string tags)
{
Tags = tags;
}
}
public sealed class DisposalTaggerOpenUiMessage : BoundUserInterfaceMessage;
/// <summary>
/// Key for opening disposal tagger UIs.

View file

@ -1,3 +1,4 @@
using Content.Shared.Access.Systems;
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Holder;
using Content.Shared.Disposal.Tube;
@ -12,14 +13,19 @@ namespace Content.Shared.Disposal.Tagger;
/// </summary>
public sealed partial class DisposalTaggerSystem : EntitySystem
{
[Dependency] private AccessReaderSystem _accessReaderSystem = default!;
[Dependency] private SharedDisposalHolderSystem _disposalHolder = default!;
[Dependency] private SharedAudioSystem _audio = default!;
[Dependency] private SharedUserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalTaggerComponent, GetDisposalsNextDirectionEvent>(OnGetTaggerNextDirection, after: new[] { typeof(DisposalTubeSystem) });
Subs.BuiEvents<DisposalTaggerComponent>(DisposalUnitUiKey.Key, subs =>
{
subs.Event<DisposalTaggerOpenUiMessage>(OnOpenUiAction);
});
Subs.BuiEvents<DisposalTaggerComponent>(DisposalTaggerUiKey.Key, subs =>
{
@ -27,11 +33,24 @@ public sealed partial class DisposalTaggerSystem : EntitySystem
});
}
[SubscribeLocalEvent]
private void OnDisposalFlush(Entity<DisposalTaggerComponent> ent, ref BeforeDisposalFlushEvent args)
{
args.Tags.Add(ent.Comp.Tag);
}
[SubscribeLocalEvent(after: [typeof(DisposalTubeSystem)])]
private void OnGetTaggerNextDirection(Entity<DisposalTaggerComponent> ent, ref GetDisposalsNextDirectionEvent args)
{
_disposalHolder.AddTag(args.Holder, ent.Comp.Tag);
}
private void OnOpenUiAction(Entity<DisposalTaggerComponent> ent, ref DisposalTaggerOpenUiMessage args)
{
if (_accessReaderSystem.IsAllowed(args.Actor, ent))
_uiSystem.TryToggleUi(ent.Owner, DisposalTaggerUiKey.Key, args.Actor);
}
/// <summary>
/// Handles UI messages from the client. For things such as button presses
/// which interact with the world and require server action.
@ -39,7 +58,7 @@ public sealed partial class DisposalTaggerSystem : EntitySystem
/// <param name="msg">A user interface message from the client.</param>
private void OnUiAction(Entity<DisposalTaggerComponent> ent, ref DisposalTaggerUiActionMessage msg)
{
if (!Exists(msg.Actor))
if (!Exists(msg.Actor) || !ent.Comp.Editable)
return;
// Check for correct message and ignore maleformed strings

View file

@ -188,12 +188,6 @@ public record DisposalSystemTransitionEvent;
[Serializable, NetSerializable]
public sealed partial class DisposalDoAfterEvent : SimpleDoAfterEvent;
/// <summary>
/// Message sent from the server to the client to update the UI of disposal units.
/// </summary>
[Serializable, NetSerializable]
public sealed class DisposalUnitBoundUserInterfaceState : BoundUserInterfaceState;
/// <summary>
/// Disposal unit pressure states.
/// </summary>

View file

@ -44,12 +44,9 @@ public abstract partial class SharedDisposalUnitSystem
protected void UpdateUI(Entity<DisposalUnitComponent> entity)
{
if (_timing.ApplyingState)
return;
if (_ui.TryGetOpenUi(entity.Owner, DisposalUnitUiKey.Key, out var bui))
{
bui.Update<DisposalUnitBoundUserInterfaceState>();
bui.Update();
}
}

View file

@ -1,3 +1,2 @@
disposal-tagger-window-title = Disposal Tagger
disposal-tagger-window-tag-input-label = Tag:
disposal-tagger-window-tag-confirm-button = Confirm

View file

@ -8,6 +8,7 @@ ui-disposal-unit-label-status = Ready
ui-disposal-unit-button-flush = Flush
ui-disposal-unit-button-eject = Eject Contents
ui-disposal-unit-button-routing = Routing
ui-disposal-unit-button-power = Power
## Verbs
@ -23,6 +24,7 @@ disposal-unit-state-Ready = Ready
# Yes I want it to always say Pressurizing
disposal-unit-state-Flushed = Pressurizing
disposal-unit-state-Pressurizing = Pressurizing
disposal-unit-state-Unpowered = Unpowered
# putting people in
disposal-unit-being-inserted = {CAPITALIZE($user)} is trying to force you into a disposal chute!

View file

@ -19,3 +19,6 @@ generic-seconds = seconds
generic-playtime-title = Playtime
generic-confirm = Confirm
generic-disabled = Disabled
generic-no-access = Insufficient access.

View file

@ -519,3 +519,27 @@
True: { state: signal-router-flipped }
- type: Construction
node: signal_router_flipped
# Specific
- type: entity
parent: DisposalRouter
id: DisposalRouterWaste
suffix: Waste
components:
- type: DisposalRouter
tags:
- waste
- type: Flippable
mirrorEntity: DisposalRouterFlippedWaste
- type: entity
parent: DisposalRouterFlipped
id: DisposalRouterFlippedWaste
suffix: Flipped, Waste
components:
- type: DisposalRouter
tags:
- waste
- type: Flippable
mirrorEntity: DisposalRouterWaste

View file

@ -146,10 +146,16 @@
interfaces:
enum.DisposalUnitUiKey.Key:
type: DisposalUnitBoundUserInterface
enum.DisposalTaggerUiKey.Key:
type: DisposalTaggerBoundUserInterface
- type: Rummageable
table: !type:NestedSelector
tableId: RatKingLoot
- type: RequireProjectileTarget
- type: DisposalTagger
tag: waste
- type: AccessReader
access: [["Engineering"]] # Too easy to break disposals when the tag can be edited by anyone
- type: entity
id: MailingUnit