Predict dumping (#32394)

* Predict dumping

- This got soaped really fucking hard.
- Dumping is predicted, this required disposals to be predicte.d
- Disposals required mailing (because it's tightly coupled), and a smidge of other content systems.
- I also had to fix a compnetworkgenerator issue at the same time so it wouldn't mispredict.

* Fix a bunch of stuff

* nasty merge

* Some reviews

* Some more reviews while I stash

* Fix merge

* Fix merge

* Half of review

* Review

* re(h)f

* lizards

* feexes

* feex
This commit is contained in:
metalgearsloth
2025-04-19 16:20:40 +10:00
committed by GitHub
parent f1f431e720
commit 63dfd21b14
140 changed files with 1655 additions and 1858 deletions

View File

@@ -0,0 +1,25 @@
using Content.Client.Configurable.UI;
using Content.Shared.Configurable;
namespace Content.Client.Configurable;
public sealed class ConfigurationSystem : SharedConfigurationSystem
{
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ConfigurationComponent, AfterAutoHandleStateEvent>(OnConfigurationState);
}
private void OnConfigurationState(Entity<ConfigurationComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (_uiSystem.TryGetOpenUi<ConfigurationBoundUserInterface>(ent.Owner,
ConfigurationComponent.ConfigurationUiKey.Key,
out var bui))
{
bui.Refresh(ent);
}
}
}

View File

@@ -1,6 +1,8 @@
using System.Text.RegularExpressions;
using Robust.Client.GameObjects;
using System.Numerics;
using System.Text.RegularExpressions;
using Content.Shared.Configurable;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using static Content.Shared.Configurable.ConfigurationComponent;
namespace Content.Client.Configurable.UI
@@ -19,16 +21,53 @@ namespace Content.Client.Configurable.UI
base.Open();
_menu = this.CreateWindow<ConfigurationMenu>();
_menu.OnConfiguration += SendConfiguration;
if (EntMan.TryGetComponent(Owner, out ConfigurationComponent? component))
Refresh((Owner, component));
}
protected override void UpdateState(BoundUserInterfaceState state)
public void Refresh(Entity<ConfigurationComponent> entity)
{
base.UpdateState(state);
if (state is not ConfigurationBoundUserInterfaceState configurationState)
if (_menu == null)
return;
_menu?.Populate(configurationState);
_menu.Column.Children.Clear();
_menu.Inputs.Clear();
foreach (var field in entity.Comp.Config)
{
var label = new Label
{
Margin = new Thickness(0, 0, 8, 0),
Name = field.Key,
Text = field.Key + ":",
VerticalAlignment = Control.VAlignment.Center,
HorizontalExpand = true,
SizeFlagsStretchRatio = .2f,
MinSize = new Vector2(60, 0)
};
var input = new LineEdit
{
Name = field.Key + "-input",
Text = field.Value ?? "",
IsValid = _menu.Validate,
HorizontalExpand = true,
SizeFlagsStretchRatio = .8f
};
_menu.Inputs.Add((field.Key, input));
var row = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal
};
ConfigurationMenu.CopyProperties(_menu.Row, row);
row.AddChild(label);
row.AddChild(input);
_menu.Column.AddChild(row);
}
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)

View File

@@ -1,12 +1,8 @@
using System.Collections.Generic;
using System.Numerics;
using System.Numerics;
using System.Text.RegularExpressions;
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.Configurable.ConfigurationComponent;
using static Robust.Client.UserInterface.Controls.BaseButton;
using static Robust.Client.UserInterface.Controls.BoxContainer;
@@ -14,10 +10,10 @@ namespace Content.Client.Configurable.UI
{
public sealed class ConfigurationMenu : DefaultWindow
{
private readonly BoxContainer _column;
private readonly BoxContainer _row;
public readonly BoxContainer Column;
public readonly BoxContainer Row;
private readonly List<(string name, LineEdit input)> _inputs;
public readonly List<(string name, LineEdit input)> Inputs;
[ViewVariables]
public Regex? Validation { get; internal set; }
@@ -28,7 +24,7 @@ namespace Content.Client.Configurable.UI
{
MinSize = SetSize = new Vector2(300, 250);
_inputs = new List<(string name, LineEdit input)>();
Inputs = new List<(string name, LineEdit input)>();
Title = Loc.GetString("configuration-menu-device-title");
@@ -39,14 +35,14 @@ namespace Content.Client.Configurable.UI
HorizontalExpand = true
};
_column = new BoxContainer
Column = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
Margin = new Thickness(8),
SeparationOverride = 16,
};
_row = new BoxContainer
Row = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
SeparationOverride = 16,
@@ -69,61 +65,20 @@ namespace Content.Client.Configurable.UI
ModulateSelfOverride = Color.FromHex("#202025")
};
outerColumn.AddChild(_column);
outerColumn.AddChild(Column);
baseContainer.AddChild(outerColumn);
baseContainer.AddChild(confirmButton);
Contents.AddChild(baseContainer);
}
public void Populate(ConfigurationBoundUserInterfaceState state)
{
_column.Children.Clear();
_inputs.Clear();
foreach (var field in state.Config)
{
var label = new Label
{
Margin = new Thickness(0, 0, 8, 0),
Name = field.Key,
Text = field.Key + ":",
VerticalAlignment = VAlignment.Center,
HorizontalExpand = true,
SizeFlagsStretchRatio = .2f,
MinSize = new Vector2(60, 0)
};
var input = new LineEdit
{
Name = field.Key + "-input",
Text = field.Value ?? "",
IsValid = Validate,
HorizontalExpand = true,
SizeFlagsStretchRatio = .8f
};
_inputs.Add((field.Key, input));
var row = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal
};
CopyProperties(_row, row);
row.AddChild(label);
row.AddChild(input);
_column.AddChild(row);
}
}
private void OnConfirm(ButtonEventArgs args)
{
var config = GenerateDictionary(_inputs, "Text");
var config = GenerateDictionary(Inputs, "Text");
OnConfiguration?.Invoke(config);
Close();
}
private bool Validate(string value)
public bool Validate(string value)
{
return Validation?.IsMatch(value) != false;
}
@@ -140,7 +95,7 @@ namespace Content.Client.Configurable.UI
return dictionary;
}
private static void CopyProperties<T>(T from, T to) where T : Control
public static void CopyProperties<T>(T from, T to) where T : Control
{
foreach (var property in from.AllAttachedProperties)
{

View File

@@ -13,7 +13,7 @@ namespace Content.Client.Decals
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly SpriteSystem _sprites = default!;
private DecalOverlay _overlay = default!;
private DecalOverlay? _overlay;
private HashSet<uint> _removedUids = new();
private readonly List<Vector2i> _removedChunks = new();
@@ -31,6 +31,9 @@ namespace Content.Client.Decals
public void ToggleOverlay()
{
if (_overlay == null)
return;
if (_overlayManager.HasOverlay<DecalOverlay>())
{
_overlayManager.RemoveOverlay(_overlay);
@@ -44,6 +47,10 @@ namespace Content.Client.Decals
public override void Shutdown()
{
base.Shutdown();
if (_overlay == null)
return;
_overlayManager.RemoveOverlay(_overlay);
}

View File

@@ -0,0 +1,8 @@
using Content.Shared.DeviceNetwork.Systems;
namespace Content.Client.DeviceNetwork.Systems;
public sealed class DeviceNetworkSystem : SharedDeviceNetworkSystem
{
}

View File

@@ -1,9 +0,0 @@
using Content.Shared.Disposal.Components;
namespace Content.Client.Disposal;
[RegisterComponent]
public sealed partial class DisposalUnitComponent : SharedDisposalUnitComponent
{
}

View File

@@ -0,0 +1,79 @@
using Content.Client.Disposal.Unit;
using Content.Client.Power.EntitySystems;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Disposal.Mailing;
public sealed class MailingUnitBoundUserInterface : BoundUserInterface
{
[ViewVariables]
public MailingUnitWindow? MailingUnitWindow;
public MailingUnitBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
private void ButtonPressed(DisposalUnitComponent.UiButton button)
{
SendMessage(new DisposalUnitComponent.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.
}
private void TargetSelected(ItemList.ItemListSelectedEventArgs args)
{
var item = args.ItemList[args.ItemIndex];
SendMessage(new TargetSelectedMessage(item.Text));
}
protected override void Open()
{
base.Open();
MailingUnitWindow = this.CreateWindow<MailingUnitWindow>();
MailingUnitWindow.OpenCenteredRight();
MailingUnitWindow.Eject.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Eject);
MailingUnitWindow.Engage.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Engage);
MailingUnitWindow.Power.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Power);
MailingUnitWindow.TargetListContainer.OnItemSelected += TargetSelected;
if (EntMan.TryGetComponent(Owner, out MailingUnitComponent? component))
Refresh((Owner, component));
}
public void Refresh(Entity<MailingUnitComponent> entity)
{
if (MailingUnitWindow == null)
return;
// TODO: This should be decoupled from disposals
if (EntMan.TryGetComponent(entity.Owner, out DisposalUnitComponent? disposals))
{
var disposalSystem = EntMan.System<DisposalUnitSystem>();
var disposalState = disposalSystem.GetState(Owner, disposals);
var fullPressure = disposalSystem.EstimatedFullPressure(Owner, disposals);
MailingUnitWindow.UnitState.Text = Loc.GetString($"disposal-unit-state-{disposalState}");
MailingUnitWindow.FullPressure = fullPressure;
MailingUnitWindow.PressureBar.UpdatePressure(fullPressure);
MailingUnitWindow.Power.Pressed = EntMan.System<PowerReceiverSystem>().IsPowered(Owner);
MailingUnitWindow.Engage.Pressed = disposals.Engaged;
}
MailingUnitWindow.Title = Loc.GetString("ui-mailing-unit-window-title", ("tag", entity.Comp.Tag ?? " "));
//UnitTag.Text = state.Tag;
MailingUnitWindow.Target.Text = entity.Comp.Target;
MailingUnitWindow.TargetListContainer.Clear();
foreach (var target in entity.Comp.TargetList)
{
MailingUnitWindow.TargetListContainer.AddItem(target);
}
}
}

View File

@@ -0,0 +1,22 @@
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Mailing;
namespace Content.Client.Disposal.Mailing;
public sealed class MailingUnitSystem : SharedMailingUnitSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MailingUnitComponent, AfterAutoHandleStateEvent>(OnMailingState);
}
private void OnMailingState(Entity<MailingUnitComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UserInterfaceSystem.TryGetOpenUi<MailingUnitBoundUserInterface>(ent.Owner, MailingUnitUiKey.Key, out var bui))
{
bui.Refresh(ent);
}
}
}

View File

@@ -1,12 +1,14 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.Disposal.UI"
MinSize="300 400"
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:disposal="clr-namespace:Content.Client.Disposal"
MinSize="300 400"
SetSize="300 400"
Resizable="False">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" SeparationOverride="8">
<Label Text="{Loc 'ui-mailing-unit-target-label'}" />
<Label Name="Target"
Access="Public"
Text="" />
</BoxContainer>
<ItemList Name="TargetListContainer"
@@ -18,14 +20,15 @@
</ItemList>
<BoxContainer Orientation="Horizontal" SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-state'}" />
<Label Name="UnitState"
<Label Name="UnitState" Access="Public"
Text="{Loc 'ui-disposal-unit-label-status'}" />
</BoxContainer>
<Control MinSize="0 5" />
<BoxContainer Orientation="Horizontal"
SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-pressure'}" />
<ui:PressureBar Name="PressureBar"
<disposal:PressureBar Name="PressureBar"
Access="Public"
MinSize="190 20"
HorizontalAlignment="Right"
MinValue="0"
@@ -50,4 +53,4 @@
StyleClasses="OpenLeft" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>
</controls:FancyWindow>

View File

@@ -0,0 +1,27 @@
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Disposal.Mailing
{
/// <summary>
/// Client-side UI used to control a <see cref="Shared.Disposal.Components.MailingUnitComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class MailingUnitWindow : FancyWindow
{
public TimeSpan FullPressure;
public MailingUnitWindow()
{
RobustXamlLoader.Load(this);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
}
}
}

View File

@@ -1,9 +1,10 @@
using Content.Shared.Disposal;
using Content.Shared.Disposal.Unit;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
namespace Content.Client.Disposal.UI;
namespace Content.Client.Disposal;
public sealed class PressureBar : ProgressBar
{

View File

@@ -1,187 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using Content.Shared.DragDrop;
using Content.Shared.Emag.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Animations;
using Robust.Client.Graphics;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Physics.Events;
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
namespace Content.Client.Disposal.Systems;
public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
{
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
private const string AnimationKey = "disposal_unit_animation";
private const string DefaultFlushState = "disposal-flush";
private const string DefaultChargeState = "disposal-charging";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalUnitComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<DisposalUnitComponent, PreventCollideEvent>(OnPreventCollide);
SubscribeLocalEvent<DisposalUnitComponent, CanDropTargetEvent>(OnCanDragDropOn);
SubscribeLocalEvent<DisposalUnitComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<DisposalUnitComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<DisposalUnitComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnHandleState(EntityUid uid, DisposalUnitComponent component, ref ComponentHandleState args)
{
if (args.Current is not DisposalUnitComponentState state)
return;
component.FlushSound = state.FlushSound;
component.State = state.State;
component.NextPressurized = state.NextPressurized;
component.AutomaticEngageTime = state.AutomaticEngageTime;
component.NextFlush = state.NextFlush;
component.Powered = state.Powered;
component.Engaged = state.Engaged;
component.RecentlyEjected.Clear();
component.RecentlyEjected.AddRange(EnsureEntityList<DisposalUnitComponent>(state.RecentlyEjected, uid));
}
public override bool HasDisposals(EntityUid? uid)
{
return HasComp<DisposalUnitComponent>(uid);
}
public override bool ResolveDisposals(EntityUid uid, [NotNullWhen(true)] ref SharedDisposalUnitComponent? component)
{
if (component != null)
return true;
TryComp<DisposalUnitComponent>(uid, out var storage);
component = storage;
return component != null;
}
public override void DoInsertDisposalUnit(EntityUid uid, EntityUid toInsert, EntityUid user, SharedDisposalUnitComponent? disposal = null)
{
return;
}
private void OnComponentInit(EntityUid uid, SharedDisposalUnitComponent sharedDisposalUnit, ComponentInit args)
{
if (!TryComp<SpriteComponent>(uid, out var sprite) || !TryComp<AppearanceComponent>(uid, out var appearance))
return;
UpdateState(uid, sharedDisposalUnit, sprite, appearance);
}
private void OnAppearanceChange(EntityUid uid, SharedDisposalUnitComponent unit, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
UpdateState(uid, unit, args.Sprite, args.Component);
}
/// <summary>
/// Update visuals and tick animation
/// </summary>
private void UpdateState(EntityUid uid, SharedDisposalUnitComponent unit, SpriteComponent sprite, AppearanceComponent appearance)
{
if (!_appearanceSystem.TryGetData<VisualState>(uid, Visuals.VisualState, out var state, appearance))
return;
sprite.LayerSetVisible(DisposalUnitVisualLayers.Unanchored, state == VisualState.UnAnchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.Base, state == VisualState.Anchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFlush, state is VisualState.OverlayFlushing or VisualState.OverlayCharging);
var chargingState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseCharging, out var chargingLayer)
? sprite.LayerGetState(chargingLayer)
: new RSI.StateId(DefaultChargeState);
// This is a transient state so not too worried about replaying in range.
if (state == VisualState.OverlayFlushing)
{
if (!_animationSystem.HasRunningAnimation(uid, AnimationKey))
{
var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.OverlayFlush, out var flushLayer)
? sprite.LayerGetState(flushLayer)
: new RSI.StateId(DefaultFlushState);
// Setup the flush animation to play
var anim = new Animation
{
Length = unit.FlushDelay,
AnimationTracks =
{
new AnimationTrackSpriteFlick
{
LayerKey = DisposalUnitVisualLayers.OverlayFlush,
KeyFrames =
{
// Play the flush animation
new AnimationTrackSpriteFlick.KeyFrame(flushState, 0),
// Return to base state (though, depending on how the unit is
// configured we might get an appearance change event telling
// us to go to charging state)
new AnimationTrackSpriteFlick.KeyFrame(chargingState, (float) unit.FlushDelay.TotalSeconds)
}
},
}
};
if (unit.FlushSound != null)
{
anim.AnimationTracks.Add(
new AnimationTrackPlaySound
{
KeyFrames =
{
new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(unit.FlushSound), 0)
}
});
}
_animationSystem.Play(uid, anim, AnimationKey);
}
}
else if (state == VisualState.OverlayCharging)
sprite.LayerSetState(DisposalUnitVisualLayers.OverlayFlush, chargingState);
else
_animationSystem.Stop(uid, AnimationKey);
if (!_appearanceSystem.TryGetData<HandleState>(uid, Visuals.Handle, out var handleState, appearance))
handleState = HandleState.Normal;
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayEngaged, handleState != HandleState.Normal);
if (!_appearanceSystem.TryGetData<LightStates>(uid, Visuals.Light, out var lightState, appearance))
lightState = LightStates.Off;
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayCharging,
(lightState & LightStates.Charging) != 0);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayReady,
(lightState & LightStates.Ready) != 0);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFull,
(lightState & LightStates.Full) != 0);
}
}
public enum DisposalUnitVisualLayers : byte
{
Unanchored,
Base,
BaseCharging,
OverlayFlush,
OverlayCharging,
OverlayReady,
OverlayFull,
OverlayEngaged
}

View File

@@ -1,9 +1,8 @@
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent;
namespace Content.Client.Disposal.UI
namespace Content.Client.Disposal.Tube
{
/// <summary>
/// Initializes a <see cref="DisposalRouterWindow"/> and updates it when new server messages are received.

View File

@@ -1,11 +1,10 @@
using Content.Shared.Disposal.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent;
namespace Content.Client.Disposal.UI
namespace Content.Client.Disposal.Tube
{
/// <summary>
/// Client-side UI used to control a <see cref="SharedDisposalRouterComponent"/>

View File

@@ -1,9 +1,8 @@
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using static Content.Shared.Disposal.Components.SharedDisposalTaggerComponent;
namespace Content.Client.Disposal.UI
namespace Content.Client.Disposal.Tube
{
/// <summary>
/// Initializes a <see cref="DisposalTaggerWindow"/> and updates it when new server messages are received.

View File

@@ -1,11 +1,10 @@
using Content.Shared.Disposal.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using static Content.Shared.Disposal.Components.SharedDisposalTaggerComponent;
namespace Content.Client.Disposal.UI
namespace Content.Client.Disposal.Tube
{
/// <summary>
/// Client-side UI used to control a <see cref="SharedDisposalTaggerComponent"/>

View File

@@ -0,0 +1,8 @@
using Content.Shared.Disposal.Unit;
namespace Content.Client.Disposal.Tube;
public sealed class DisposalTubeSystem : SharedDisposalTubeSystem
{
}

View File

@@ -1,103 +0,0 @@
using Content.Client.Disposal.Systems;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
namespace Content.Client.Disposal.UI
{
/// <summary>
/// Initializes a <see cref="MailingUnitWindow"/> or a <see cref="DisposalUnitWindow"/> and updates it when new server messages are received.
/// </summary>
[UsedImplicitly]
public sealed class DisposalUnitBoundUserInterface : BoundUserInterface
{
// What are you doing here
[ViewVariables]
public MailingUnitWindow? MailingUnitWindow;
[ViewVariables]
public DisposalUnitWindow? DisposalUnitWindow;
public DisposalUnitBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
private void ButtonPressed(UiButton 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.
}
private void TargetSelected(ItemList.ItemListSelectedEventArgs args)
{
var item = args.ItemList[args.ItemIndex];
SendMessage(new TargetSelectedMessage(item.Text));
}
protected override void Open()
{
base.Open();
if (UiKey is MailingUnitUiKey)
{
MailingUnitWindow = new MailingUnitWindow();
MailingUnitWindow.OpenCenteredRight();
MailingUnitWindow.OnClose += Close;
MailingUnitWindow.Eject.OnPressed += _ => ButtonPressed(UiButton.Eject);
MailingUnitWindow.Engage.OnPressed += _ => ButtonPressed(UiButton.Engage);
MailingUnitWindow.Power.OnPressed += _ => ButtonPressed(UiButton.Power);
MailingUnitWindow.TargetListContainer.OnItemSelected += TargetSelected;
}
else if (UiKey is DisposalUnitUiKey)
{
DisposalUnitWindow = new DisposalUnitWindow();
DisposalUnitWindow.OpenCenteredRight();
DisposalUnitWindow.OnClose += Close;
DisposalUnitWindow.Eject.OnPressed += _ => ButtonPressed(UiButton.Eject);
DisposalUnitWindow.Engage.OnPressed += _ => ButtonPressed(UiButton.Engage);
DisposalUnitWindow.Power.OnPressed += _ => ButtonPressed(UiButton.Power);
}
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not MailingUnitBoundUserInterfaceState && state is not DisposalUnitBoundUserInterfaceState)
{
return;
}
switch (state)
{
case MailingUnitBoundUserInterfaceState mailingUnitState:
MailingUnitWindow?.UpdateState(mailingUnitState);
break;
case DisposalUnitBoundUserInterfaceState disposalUnitState:
DisposalUnitWindow?.UpdateState(disposalUnitState);
break;
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
MailingUnitWindow?.Dispose();
DisposalUnitWindow?.Dispose();
}
}
}

View File

@@ -1,43 +0,0 @@
using Content.Shared.Disposal.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
namespace Content.Client.Disposal.UI
{
/// <summary>
/// Client-side UI used to control a <see cref="SharedDisposalUnitComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class DisposalUnitWindow : DefaultWindow
{
public TimeSpan FullPressure;
public DisposalUnitWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
}
/// <summary>
/// Update the interface state for the disposals window.
/// </summary>
/// <returns>true if we should stop updating every frame.</returns>
public void UpdateState(DisposalUnitBoundUserInterfaceState state)
{
Title = state.UnitName;
UnitState.Text = state.UnitState;
Power.Pressed = state.Powered;
Engage.Pressed = state.Engaged;
FullPressure = state.FullPressureTime;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
}
}
}

View File

@@ -1,55 +0,0 @@
using Content.Shared.Disposal;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Disposal.UI
{
/// <summary>
/// Client-side UI used to control a <see cref="MailingUnitComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class MailingUnitWindow : DefaultWindow
{
public TimeSpan FullPressure;
public MailingUnitWindow()
{
RobustXamlLoader.Load(this);
}
/// <summary>
/// Update the interface state for the disposals window.
/// </summary>
/// <returns>true if we should stop updating every frame.</returns>
public bool UpdateState(MailingUnitBoundUserInterfaceState state)
{
var disposalState = state.DisposalState;
Title = Loc.GetString("ui-mailing-unit-window-title", ("tag", state.Tag ?? " "));
UnitState.Text = disposalState.UnitState;
FullPressure = disposalState.FullPressureTime;
var pressureReached = PressureBar.UpdatePressure(disposalState.FullPressureTime);
Power.Pressed = disposalState.Powered;
Engage.Pressed = disposalState.Engaged;
//UnitTag.Text = state.Tag;
Target.Text = state.Target;
TargetListContainer.Clear();
foreach (var target in state.TargetList)
{
TargetListContainer.AddItem(target);
}
return !disposalState.Powered || pressureReached;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
}
}
}

View File

@@ -0,0 +1,63 @@
using Content.Client.Disposal.Mailing;
using Content.Client.Power.EntitySystems;
using Content.Shared.Disposal.Components;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client.Disposal.Unit
{
/// <summary>
/// Initializes a <see cref="MailingUnitWindow"/> or a <see cref="_disposalUnitWindow"/> and updates it when new server messages are received.
/// </summary>
[UsedImplicitly]
public sealed class DisposalUnitBoundUserInterface : BoundUserInterface
{
[ViewVariables] private DisposalUnitWindow? _disposalUnitWindow;
public DisposalUnitBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
private void ButtonPressed(DisposalUnitComponent.UiButton button)
{
SendPredictedMessage(new DisposalUnitComponent.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()
{
base.Open();
_disposalUnitWindow = this.CreateWindow<DisposalUnitWindow>();
_disposalUnitWindow.OpenCenteredRight();
_disposalUnitWindow.Eject.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Eject);
_disposalUnitWindow.Engage.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Engage);
_disposalUnitWindow.Power.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Power);
if (EntMan.TryGetComponent(Owner, out DisposalUnitComponent? component))
{
Refresh((Owner, component));
}
}
public void Refresh(Entity<DisposalUnitComponent> entity)
{
if (_disposalUnitWindow == null)
return;
var disposalSystem = EntMan.System<DisposalUnitSystem>();
_disposalUnitWindow.Title = EntMan.GetComponent<MetaDataComponent>(entity.Owner).EntityName;
var state = disposalSystem.GetState(entity.Owner, entity.Comp);
_disposalUnitWindow.UnitState.Text = Loc.GetString($"disposal-unit-state-{state}");
_disposalUnitWindow.Power.Pressed = EntMan.System<PowerReceiverSystem>().IsPowered(Owner);
_disposalUnitWindow.Engage.Pressed = entity.Comp.Engaged;
_disposalUnitWindow.FullPressure = disposalSystem.EstimatedFullPressure(entity.Owner, entity.Comp);
}
}
}

View File

@@ -0,0 +1,156 @@
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Unit;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Audio.Systems;
namespace Content.Client.Disposal.Unit;
public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
{
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
private const string AnimationKey = "disposal_unit_animation";
private const string DefaultFlushState = "disposal-flush";
private const string DefaultChargeState = "disposal-charging";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalUnitComponent, AfterAutoHandleStateEvent>(OnHandleState);
SubscribeLocalEvent<DisposalUnitComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnHandleState(EntityUid uid, DisposalUnitComponent component, ref AfterAutoHandleStateEvent args)
{
UpdateUI((uid, component));
}
protected override void UpdateUI(Entity<DisposalUnitComponent> entity)
{
if (_uiSystem.TryGetOpenUi<DisposalUnitBoundUserInterface>(entity.Owner, DisposalUnitComponent.DisposalUnitUiKey.Key, out var bui))
{
bui.Refresh(entity);
}
}
protected override void OnDisposalInit(Entity<DisposalUnitComponent> ent, ref ComponentInit args)
{
base.OnDisposalInit(ent, ref args);
if (!TryComp<SpriteComponent>(ent, out var sprite) || !TryComp<AppearanceComponent>(ent, out var appearance))
return;
UpdateState(ent, sprite, appearance);
}
private void OnAppearanceChange(Entity<DisposalUnitComponent> ent, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
UpdateState(ent, args.Sprite, args.Component);
}
/// <summary>
/// Update visuals and tick animation
/// </summary>
private void UpdateState(Entity<DisposalUnitComponent> ent, SpriteComponent sprite, AppearanceComponent appearance)
{
if (!_appearanceSystem.TryGetData<DisposalUnitComponent.VisualState>(ent, DisposalUnitComponent.Visuals.VisualState, out var state, appearance))
return;
sprite.LayerSetVisible(DisposalUnitVisualLayers.Unanchored, state == DisposalUnitComponent.VisualState.UnAnchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.Base, state == DisposalUnitComponent.VisualState.Anchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFlush, state is DisposalUnitComponent.VisualState.OverlayFlushing or DisposalUnitComponent.VisualState.OverlayCharging);
var chargingState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseCharging, out var chargingLayer)
? sprite.LayerGetState(chargingLayer)
: new RSI.StateId(DefaultChargeState);
// This is a transient state so not too worried about replaying in range.
if (state == DisposalUnitComponent.VisualState.OverlayFlushing)
{
if (!_animationSystem.HasRunningAnimation(ent, AnimationKey))
{
var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.OverlayFlush, out var flushLayer)
? sprite.LayerGetState(flushLayer)
: new RSI.StateId(DefaultFlushState);
// Setup the flush animation to play
var anim = new Animation
{
Length = ent.Comp.FlushDelay,
AnimationTracks =
{
new AnimationTrackSpriteFlick
{
LayerKey = DisposalUnitVisualLayers.OverlayFlush,
KeyFrames =
{
// Play the flush animation
new AnimationTrackSpriteFlick.KeyFrame(flushState, 0),
// Return to base state (though, depending on how the unit is
// configured we might get an appearance change event telling
// us to go to charging state)
new AnimationTrackSpriteFlick.KeyFrame(chargingState, (float) ent.Comp.FlushDelay.TotalSeconds)
}
},
}
};
if (ent.Comp.FlushSound != null)
{
anim.AnimationTracks.Add(
new AnimationTrackPlaySound
{
KeyFrames =
{
new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(ent.Comp.FlushSound), 0)
}
});
}
_animationSystem.Play(ent, anim, AnimationKey);
}
}
else if (state == DisposalUnitComponent.VisualState.OverlayCharging)
sprite.LayerSetState(DisposalUnitVisualLayers.OverlayFlush, chargingState);
else
_animationSystem.Stop(ent.Owner, AnimationKey);
if (!_appearanceSystem.TryGetData<DisposalUnitComponent.HandleState>(ent, DisposalUnitComponent.Visuals.Handle, out var handleState, appearance))
handleState = DisposalUnitComponent.HandleState.Normal;
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayEngaged, handleState != DisposalUnitComponent.HandleState.Normal);
if (!_appearanceSystem.TryGetData<DisposalUnitComponent.LightStates>(ent, DisposalUnitComponent.Visuals.Light, out var lightState, appearance))
lightState = DisposalUnitComponent.LightStates.Off;
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayCharging,
(lightState & DisposalUnitComponent.LightStates.Charging) != 0);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayReady,
(lightState & DisposalUnitComponent.LightStates.Ready) != 0);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFull,
(lightState & DisposalUnitComponent.LightStates.Full) != 0);
}
}
public enum DisposalUnitVisualLayers : byte
{
Unanchored,
Base,
BaseCharging,
OverlayFlush,
OverlayCharging,
OverlayReady,
OverlayFull,
OverlayEngaged
}

View File

@@ -1,20 +1,21 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.Disposal.UI"
MinSize="300 140"
SetSize="300 140"
<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">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Vertical" Margin="10">
<BoxContainer Orientation="Horizontal"
SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-state'}" />
<Label Name="UnitState"
<Label Name="UnitState" Access="Public"
Text="{Loc 'ui-disposal-unit-label-status'}" />
</BoxContainer>
<Control MinSize="0 5" />
<BoxContainer Orientation="Horizontal"
SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-pressure'}" />
<ui:PressureBar Name="PressureBar"
<disposal:PressureBar Name="PressureBar"
MinSize="190 20"
HorizontalAlignment="Right"
MinValue="0"
@@ -23,7 +24,7 @@
Value="0.5" />
</BoxContainer>
<Control MinSize="0 10" />
<BoxContainer Orientation="Horizontal">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Button Name="Engage"
Access="Public"
Text="{Loc 'ui-disposal-unit-button-flush'}"
@@ -39,4 +40,4 @@
StyleClasses="OpenLeft" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>
</controls:FancyWindow>

View File

@@ -0,0 +1,28 @@
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Disposal.Unit
{
/// <summary>
/// Client-side UI used to control a <see cref="Shared.Disposal.Components.DisposalUnitComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class DisposalUnitWindow : FancyWindow
{
public TimeSpan FullPressure;
public DisposalUnitWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
}
}
}

View File

@@ -1,4 +1,3 @@
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Systems;
namespace Content.Client.NetworkConfigurator.Systems;

View File

@@ -3,6 +3,7 @@ using Content.Client.Power.Components;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Examine;
using Content.Shared.Power;
using Robust.Shared.GameStates;
namespace Content.Client.Power.EntitySystems;
@@ -26,9 +27,20 @@ public sealed class PowerReceiverSystem : SharedPowerReceiverSystem
if (args.Current is not ApcPowerReceiverComponentState state)
return;
var powerChanged = component.Powered != state.Powered;
component.Powered = state.Powered;
component.NeedsPower = state.NeedsPower;
component.PowerDisabled = state.PowerDisabled;
// SO client systems can handle it. The main reason for this is we can't guarantee compstate ordering.
if (powerChanged)
RaisePower((uid, component));
}
protected override void RaisePower(Entity<SharedApcPowerReceiverComponent> entity)
{
var ev = new PowerChangedEvent(entity.Comp.Powered, 0f);
RaiseLocalEvent(entity.Owner, ref ev);
}
public override bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component)

View File

@@ -11,9 +11,6 @@ public sealed class StorageFillVisualizerSystem : VisualizerSystem<StorageFillVi
if (args.Sprite == null)
return;
if (!TryComp(uid, out SpriteComponent? sprite))
return;
if (!AppearanceSystem.TryGetData<int>(uid, StorageFillVisuals.FillLevel, out var level, args.Component))
return;