]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict vending machine UI (#33412)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Sun, 2 Mar 2025 02:47:52 +0000 (13:47 +1100)
committerGitHub <noreply@github.com>
Sun, 2 Mar 2025 02:47:52 +0000 (13:47 +1100)
15 files changed:
Content.Client/Advertise/Systems/SpeakOnUIClosedSystem.cs [new file with mode: 0644]
Content.Client/UserInterface/Controls/ListContainer.cs
Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs
Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
Content.Client/VendingMachines/VendingMachineSystem.cs
Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs
Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs
Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs
Content.Server/VendingMachines/VendingMachineSystem.cs
Content.Shared/Advertise/Components/SpeakOnUIClosedComponent.cs [moved from Content.Server/Advertise/Components/SpeakOnUIClosedComponent.cs with 80% similarity]
Content.Shared/Advertise/Systems/SharedSpeakOnUIClosedSystem.cs [new file with mode: 0644]
Content.Shared/VendingMachines/SharedVendingMachineSystem.Restock.cs
Content.Shared/VendingMachines/SharedVendingMachineSystem.cs
Content.Shared/VendingMachines/VendingMachineComponent.cs

diff --git a/Content.Client/Advertise/Systems/SpeakOnUIClosedSystem.cs b/Content.Client/Advertise/Systems/SpeakOnUIClosedSystem.cs
new file mode 100644 (file)
index 0000000..4e82ec4
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.Advertise.Systems;
+
+namespace Content.Client.Advertise.Systems;
+
+public sealed class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem;
index e1b3b948f045add3739040bdc3cfb1363ff8a6d0..0ee0a67af0a3f67dbbf41df39feded52581a2a40 100644 (file)
@@ -96,9 +96,12 @@ public class ListContainer : Control
         {
             ListContainerButton control = new(data[0], 0);
             GenerateItem?.Invoke(data[0], control);
+            // Yes this AddChild is necessary for reasons (get proper style or whatever?)
+            // without it the DesiredSize may be different to the final DesiredSize.
+            AddChild(control);
             control.Measure(Vector2Helpers.Infinity);
             _itemHeight = control.DesiredSize.Y;
-            control.Dispose();
+            control.Orphan();
         }
 
         // Ensure buttons are re-generated.
@@ -384,6 +387,7 @@ public sealed class ListContainerButton : ContainerButton, IEntityControl
 
     public ListContainerButton(ListData data, int index)
     {
+        AddStyleClass(StyleClassButton);
         Data = data;
         Index = index;
         // AddChild(Background = new PanelContainer
index a7212934fd81d7d4af14c5c348dbab02573fb20e..0f0564c5960ea02b76abcbb77d36179961ce9484 100644 (file)
@@ -16,4 +16,9 @@ public sealed partial class VendingMachineItem : BoxContainer
 
         NameLabel.Text = text;
     }
+
+    public void SetText(string text)
+    {
+        NameLabel.Text = text;
+    }
 }
index ee7a0e41faec7899eda373f9c62be46b68301408..899a0208cb0b71f9849f7cfd4db55c828c7e6365 100644 (file)
@@ -1,3 +1,4 @@
+using System.Linq;
 using System.Numerics;
 using Content.Shared.VendingMachines;
 using Robust.Client.AutoGenerated;
@@ -19,10 +20,15 @@ namespace Content.Client.VendingMachines.UI
         [Dependency] private readonly IEntityManager _entityManager = default!;
 
         private readonly Dictionary<EntProtoId, EntityUid> _dummies = [];
+        private readonly Dictionary<EntProtoId, (ListContainerButton Button, VendingMachineItem Item)> _listItems = new();
+        private readonly Dictionary<EntProtoId, uint> _amounts = new();
 
-        public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
+        /// <summary>
+        /// Whether the vending machine is able to be interacted with or not.
+        /// </summary>
+        private bool _enabled;
 
-        private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
+        public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
 
         public VendingMachineMenu()
         {
@@ -68,18 +74,23 @@ namespace Content.Client.VendingMachines.UI
             if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text })
                 return;
 
-            button.AddChild(new VendingMachineItem(protoID, text));
-
-            button.ToolTip = text;
-            button.StyleBoxOverride = _styleBox;
+            var item = new VendingMachineItem(protoID, text);
+            _listItems[protoID] = (button, item);
+            button.AddChild(item);
+            button.AddStyleClass("ButtonSquare");
+            button.Disabled = !_enabled || _amounts[protoID] == 0;
         }
 
         /// <summary>
         /// Populates the list of available items on the vending machine interface
         /// and sets icons based on their prototypes
         /// </summary>
-        public void Populate(List<VendingMachineInventoryEntry> inventory)
+        public void Populate(List<VendingMachineInventoryEntry> inventory, bool enabled)
         {
+            _enabled = enabled;
+            _listItems.Clear();
+            _amounts.Clear();
+
             if (inventory.Count == 0 && VendingContents.Visible)
             {
                 SearchBar.Visible = false;
@@ -109,7 +120,10 @@ namespace Content.Client.VendingMachines.UI
                 var entry = inventory[i];
 
                 if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
+                {
+                    _amounts[entry.ID] = 0;
                     continue;
+                }
 
                 if (!_dummies.TryGetValue(entry.ID, out var dummy))
                 {
@@ -119,11 +133,15 @@ namespace Content.Client.VendingMachines.UI
 
                 var itemName = Identity.Name(dummy, _entityManager);
                 var itemText = $"{itemName} [{entry.Amount}]";
+                _amounts[entry.ID] = entry.Amount;
 
                 if (itemText.Length > longestEntry.Length)
                     longestEntry = itemText;
 
-                listData.Add(new VendorItemsListData(prototype.ID, itemText, i));
+                listData.Add(new VendorItemsListData(prototype.ID, i)
+                {
+                    ItemText = itemText,
+                });
             }
 
             VendingContents.PopulateList(listData);
@@ -131,12 +149,43 @@ namespace Content.Client.VendingMachines.UI
             SetSizeAfterUpdate(longestEntry.Length, inventory.Count);
         }
 
+        /// <summary>
+        /// Updates text entries for vending data in place without modifying the list controls.
+        /// </summary>
+        public void UpdateAmounts(List<VendingMachineInventoryEntry> cachedInventory, bool enabled)
+        {
+            _enabled = enabled;
+
+            foreach (var proto in _dummies.Keys)
+            {
+                if (!_listItems.TryGetValue(proto, out var button))
+                    continue;
+
+                var dummy = _dummies[proto];
+                var amount = cachedInventory.First(o => o.ID == proto).Amount;
+                // Could be better? Problem is all inventory entries get squashed.
+                var text = GetItemText(dummy, amount);
+
+                button.Item.SetText(text);
+                button.Button.Disabled = !enabled || amount == 0;
+            }
+        }
+
+        private string GetItemText(EntityUid dummy, uint amount)
+        {
+            var itemName = Identity.Name(dummy, _entityManager);
+            return $"{itemName} [{amount}]";
+        }
+
         private void SetSizeAfterUpdate(int longestEntryLength, int contentCount)
         {
             SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400),
                 Math.Clamp(contentCount * 50, 150, 350));
         }
     }
-}
 
-public record VendorItemsListData(EntProtoId ItemProtoID, string ItemText, int ItemIndex) : ListData;
+    public record VendorItemsListData(EntProtoId ItemProtoID, int ItemIndex) : ListData
+    {
+        public string ItemText = string.Empty;
+    }
+}
index 052bdacb89e051bfc870f3294404761c6aaa832d..874808158d8331ce53545385f4226defcd4ad7ae 100644 (file)
@@ -31,10 +31,21 @@ namespace Content.Client.VendingMachines
 
         public void Refresh()
         {
+            var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
+
             var system = EntMan.System<VendingMachineSystem>();
             _cachedInventory = system.GetAllInventory(Owner);
 
-            _menu?.Populate(_cachedInventory);
+            _menu?.Populate(_cachedInventory, enabled);
+        }
+
+        public void UpdateAmounts()
+        {
+            var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
+
+            var system = EntMan.System<VendingMachineSystem>();
+            _cachedInventory = system.GetAllInventory(Owner);
+            _menu?.UpdateAmounts(_cachedInventory, enabled);
         }
 
         private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
@@ -53,7 +64,7 @@ namespace Content.Client.VendingMachines
             if (selectedItem == null)
                 return;
 
-            SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
+            SendPredictedMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
         }
 
         protected override void Dispose(bool disposing)
index 1b1dde2b67ef5bf0e7d7b2cda6f75b97caee2a9c..130296c8a12113f8ef870249043bdf94e7eb9c28 100644 (file)
@@ -1,6 +1,8 @@
+using System.Linq;
 using Content.Shared.VendingMachines;
 using Robust.Client.Animations;
 using Robust.Client.GameObjects;
+using Robust.Shared.GameStates;
 
 namespace Content.Client.VendingMachines;
 
@@ -8,7 +10,6 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
 {
     [Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
     [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
-    [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
 
     public override void Initialize()
     {
@@ -16,14 +17,69 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
 
         SubscribeLocalEvent<VendingMachineComponent, AppearanceChangeEvent>(OnAppearanceChange);
         SubscribeLocalEvent<VendingMachineComponent, AnimationCompletedEvent>(OnAnimationCompleted);
-        SubscribeLocalEvent<VendingMachineComponent, AfterAutoHandleStateEvent>(OnVendingAfterState);
+        SubscribeLocalEvent<VendingMachineComponent, ComponentHandleState>(OnVendingHandleState);
     }
 
-    private void OnVendingAfterState(EntityUid uid, VendingMachineComponent component, ref AfterAutoHandleStateEvent args)
+    private void OnVendingHandleState(Entity<VendingMachineComponent> entity, ref ComponentHandleState args)
     {
-        if (_uiSystem.TryGetOpenUi<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui))
+        if (args.Current is not VendingMachineComponentState state)
+            return;
+
+        var uid = entity.Owner;
+        var component = entity.Comp;
+
+        component.Contraband = state.Contraband;
+        component.EjectEnd = state.EjectEnd;
+        component.DenyEnd = state.DenyEnd;
+        component.DispenseOnHitEnd = state.DispenseOnHitEnd;
+
+        // If all we did was update amounts then we can leave BUI buttons in place.
+        var fullUiUpdate = !component.Inventory.Keys.SequenceEqual(state.Inventory.Keys) ||
+                           !component.EmaggedInventory.Keys.SequenceEqual(state.EmaggedInventory.Keys) ||
+                           !component.ContrabandInventory.Keys.SequenceEqual(state.ContrabandInventory.Keys);
+
+        component.Inventory.Clear();
+        component.EmaggedInventory.Clear();
+        component.ContrabandInventory.Clear();
+
+        foreach (var entry in state.Inventory)
+        {
+            component.Inventory.Add(entry.Key, new(entry.Value));
+        }
+
+        foreach (var entry in state.EmaggedInventory)
+        {
+            component.EmaggedInventory.Add(entry.Key, new(entry.Value));
+        }
+
+        foreach (var entry in state.ContrabandInventory)
+        {
+            component.ContrabandInventory.Add(entry.Key, new(entry.Value));
+        }
+
+        if (UISystem.TryGetOpenUi<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui))
+        {
+            if (fullUiUpdate)
+            {
+                bui.Refresh();
+            }
+            else
+            {
+                bui.UpdateAmounts();
+            }
+        }
+    }
+
+    protected override void UpdateUI(Entity<VendingMachineComponent?> entity)
+    {
+        if (!Resolve(entity, ref entity.Comp))
+            return;
+
+        if (UISystem.TryGetOpenUi<VendingMachineBoundUserInterface>(entity.Owner,
+                VendingMachineUiKey.Key,
+                out var bui))
         {
-            bui.Refresh();
+            bui.UpdateAmounts();
         }
     }
 
@@ -70,13 +126,13 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
                 if (component.LoopDenyAnimation)
                     SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.DenyState, sprite);
                 else
-                    PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, component.DenyDelay, sprite);
+                    PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, (float)component.DenyDelay.TotalSeconds, sprite);
 
                 SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
                 break;
 
             case VendingMachineVisualState.Eject:
-                PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, component.EjectDelay, sprite);
+                PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, (float)component.EjectDelay.TotalSeconds, sprite);
                 SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
                 break;
 
index a0a709e5faddf064b2ea17d635ad52724476f925..3fca640d4af77b5b9832f7e1431f5b7de74d59b1 100644 (file)
@@ -1,13 +1,13 @@
-using Content.Server.Advertise.Components;
 using Content.Server.Chat.Systems;
-using Content.Shared.Dataset;
+using Content.Shared.Advertise.Components;
+using Content.Shared.Advertise.Systems;
+using Content.Shared.UserInterface;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
-using ActivatableUIComponent = Content.Shared.UserInterface.ActivatableUIComponent;
 
-namespace Content.Server.Advertise;
+namespace Content.Server.Advertise.EntitySystems;
 
-public sealed partial class SpeakOnUIClosedSystem : EntitySystem
+public sealed partial class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem
 {
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -46,13 +46,4 @@ public sealed partial class SpeakOnUIClosedSystem : EntitySystem
         entity.Comp.Flag = false;
         return true;
     }
-
-    public bool TrySetFlag(Entity<SpeakOnUIClosedComponent?> entity, bool value = true)
-    {
-        if (!Resolve(entity, ref entity.Comp))
-            return false;
-
-        entity.Comp.Flag = value;
-        return true;
-    }
 }
index b0bf3895092860a505bb148c4fcf2a217488364a..a0e52e9b48853197533cc0f7129a543e3b952f64 100644 (file)
@@ -1,11 +1,9 @@
-using Content.Server.Power.Components;
 using Content.Shared.UserInterface;
-using Content.Server.Advertise;
-using Content.Server.Advertise.Components;
+using Content.Server.Advertise.EntitySystems;
+using Content.Shared.Advertise.Components;
 using Content.Shared.Arcade;
 using Content.Shared.Power;
 using Robust.Server.GameObjects;
-using Robust.Shared.Player;
 
 namespace Content.Server.Arcade.BlockGame;
 
index b359a13bd121ec90a3d6b77ab480de12f4980d51..2070ab8bfec6397915713860b2bb18c1daaf7d7f 100644 (file)
@@ -1,9 +1,9 @@
 using Content.Server.Power.Components;
 using Content.Shared.UserInterface;
-using Content.Server.Advertise;
-using Content.Server.Advertise.Components;
+using Content.Server.Advertise.EntitySystems;
+using Content.Shared.Advertise.Components;
+using Content.Shared.Arcade;
 using Content.Shared.Power;
-using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent;
 using Robust.Server.GameObjects;
 using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
@@ -24,7 +24,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
 
         SubscribeLocalEvent<SpaceVillainArcadeComponent, ComponentInit>(OnComponentInit);
         SubscribeLocalEvent<SpaceVillainArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpenSV);
-        SubscribeLocalEvent<SpaceVillainArcadeComponent, SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
+        SubscribeLocalEvent<SpaceVillainArcadeComponent, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
         SubscribeLocalEvent<SpaceVillainArcadeComponent, PowerChangedEvent>(OnSVillainPower);
     }
 
@@ -70,7 +70,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
         component.RewardAmount = new Random().Next(component.RewardMinAmount, component.RewardMaxAmount + 1);
     }
 
-    private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SpaceVillainArcadePlayerActionMessage msg)
+    private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage msg)
     {
         if (component.Game == null)
             return;
@@ -79,22 +79,22 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
 
         switch (msg.PlayerAction)
         {
-            case PlayerAction.Attack:
-            case PlayerAction.Heal:
-            case PlayerAction.Recharge:
+            case SharedSpaceVillainArcadeComponent.PlayerAction.Attack:
+            case SharedSpaceVillainArcadeComponent.PlayerAction.Heal:
+            case SharedSpaceVillainArcadeComponent.PlayerAction.Recharge:
                 component.Game.ExecutePlayerAction(uid, msg.PlayerAction, component);
                 // Any sort of gameplay action counts
                 if (TryComp<SpeakOnUIClosedComponent>(uid, out var speakComponent))
                     _speakOnUIClosed.TrySetFlag((uid, speakComponent));
                 break;
-            case PlayerAction.NewGame:
+            case SharedSpaceVillainArcadeComponent.PlayerAction.NewGame:
                 _audioSystem.PlayPvs(component.NewGameSound, uid, AudioParams.Default.WithVolume(-4f));
 
                 component.Game = new SpaceVillainGame(uid, component, this);
-                _uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
+                _uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
                 break;
-            case PlayerAction.RequestData:
-                _uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
+            case SharedSpaceVillainArcadeComponent.PlayerAction.RequestData:
+                _uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
                 break;
         }
     }
@@ -109,6 +109,6 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
         if (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered)
             return;
 
-        _uiSystem.CloseUi(uid, SpaceVillainArcadeUiKey.Key);
+        _uiSystem.CloseUi(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key);
     }
 }
index c2ea6dd1ac0099ab728262131f6ab1afd8c7fb3b..e061398114b4385cb898fc3e93e54b60bb31531c 100644 (file)
@@ -1,19 +1,12 @@
 using System.Linq;
 using System.Numerics;
-using Content.Server.Advertise;
-using Content.Server.Advertise.Components;
 using Content.Server.Cargo.Systems;
 using Content.Server.Emp;
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
-using Content.Shared.Access.Components;
-using Content.Shared.Access.Systems;
-using Content.Shared.Actions;
 using Content.Shared.Damage;
 using Content.Shared.Destructible;
 using Content.Shared.DoAfter;
-using Content.Shared.Emag.Components;
-using Content.Shared.Emag.Systems;
 using Content.Shared.Emp;
 using Content.Shared.Popups;
 using Content.Shared.Power;
@@ -21,7 +14,6 @@ using Content.Shared.Throwing;
 using Content.Shared.UserInterface;
 using Content.Shared.VendingMachines;
 using Content.Shared.Wall;
-using Robust.Server.GameObjects;
 using Robust.Shared.Audio;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
@@ -32,14 +24,9 @@ namespace Content.Server.VendingMachines
     public sealed class VendingMachineSystem : SharedVendingMachineSystem
     {
         [Dependency] private readonly IRobustRandom _random = default!;
-        [Dependency] private readonly AccessReaderSystem _accessReader = default!;
-        [Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
         [Dependency] private readonly PricingSystem _pricing = default!;
         [Dependency] private readonly ThrowingSystem _throwingSystem = default!;
         [Dependency] private readonly IGameTiming _timing = default!;
-        [Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!;
-        [Dependency] private readonly SharedPointLightSystem _light = default!;
-        [Dependency] private readonly EmagSystem _emag = default!;
 
         private const float WallVendEjectDistanceFromWall = 1f;
 
@@ -55,11 +42,6 @@ namespace Content.Server.VendingMachines
 
             SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt);
 
-            Subs.BuiEvents<VendingMachineComponent>(VendingMachineUiKey.Key, subs =>
-            {
-                subs.Event<VendingMachineEjectMessage>(OnInventoryEjectMessage);
-            });
-
             SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense);
 
             SubscribeLocalEvent<VendingMachineComponent, RestockDoAfterEvent>(OnDoAfter);
@@ -91,7 +73,7 @@ namespace Content.Server.VendingMachines
 
             if (HasComp<ApcPowerReceiverComponent>(uid))
             {
-                TryUpdateVisualState(uid, component);
+                TryUpdateVisualState((uid, component));
             }
         }
 
@@ -101,26 +83,15 @@ namespace Content.Server.VendingMachines
                 args.Cancel();
         }
 
-        private void OnInventoryEjectMessage(EntityUid uid, VendingMachineComponent component, VendingMachineEjectMessage args)
-        {
-            if (!this.IsPowered(uid, EntityManager))
-                return;
-
-            if (args.Actor is not { Valid: true } entity || Deleted(entity))
-                return;
-
-            AuthorizedVend(uid, entity, args.Type, args.ID, component);
-        }
-
         private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, ref PowerChangedEvent args)
         {
-            TryUpdateVisualState(uid, component);
+            TryUpdateVisualState((uid, component));
         }
 
         private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs)
         {
             vendComponent.Broken = true;
-            TryUpdateVisualState(uid, vendComponent);
+            TryUpdateVisualState((uid, vendComponent));
         }
 
         private void OnDamageChanged(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args)
@@ -128,7 +99,7 @@ namespace Content.Server.VendingMachines
             if (!args.DamageIncreased && component.Broken)
             {
                 component.Broken = false;
-                TryUpdateVisualState(uid, component);
+                TryUpdateVisualState((uid, component));
                 return;
             }
 
@@ -139,8 +110,11 @@ namespace Content.Server.VendingMachines
             if (args.DamageIncreased && args.DamageDelta.GetTotal() >= component.DispenseOnHitThreshold &&
                 _random.Prob(component.DispenseOnHitChance.Value))
             {
-                if (component.DispenseOnHitCooldown > 0f)
-                    component.DispenseOnHitCoolingDown = true;
+                if (component.DispenseOnHitCooldown != null)
+                {
+                    component.DispenseOnHitEnd = Timing.CurTime + component.DispenseOnHitCooldown.Value;
+                }
+
                 EjectRandom(uid, throwItem: true, forceEject: true, component);
             }
         }
@@ -199,145 +173,6 @@ namespace Content.Server.VendingMachines
             Dirty(uid, component);
         }
 
-        public void Deny(EntityUid uid, VendingMachineComponent? vendComponent = null)
-        {
-            if (!Resolve(uid, ref vendComponent))
-                return;
-
-            if (vendComponent.Denying)
-                return;
-
-            vendComponent.Denying = true;
-            Audio.PlayPvs(vendComponent.SoundDeny, uid, AudioParams.Default.WithVolume(-2f));
-            TryUpdateVisualState(uid, vendComponent);
-        }
-
-        /// <summary>
-        /// Checks if the user is authorized to use this vending machine
-        /// </summary>
-        /// <param name="uid"></param>
-        /// <param name="sender">Entity trying to use the vending machine</param>
-        /// <param name="vendComponent"></param>
-        public bool IsAuthorized(EntityUid uid, EntityUid sender, VendingMachineComponent? vendComponent = null)
-        {
-            if (!Resolve(uid, ref vendComponent))
-                return false;
-
-            if (!TryComp<AccessReaderComponent>(uid, out var accessReader))
-                return true;
-
-            if (_accessReader.IsAllowed(sender, uid, accessReader))
-                return true;
-
-            Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid);
-            Deny(uid, vendComponent);
-            return false;
-        }
-
-        /// <summary>
-        /// Tries to eject the provided item. Will do nothing if the vending machine is incapable of ejecting, already ejecting
-        /// or the item doesn't exist in its inventory.
-        /// </summary>
-        /// <param name="uid"></param>
-        /// <param name="type">The type of inventory the item is from</param>
-        /// <param name="itemId">The prototype ID of the item</param>
-        /// <param name="throwItem">Whether the item should be thrown in a random direction after ejection</param>
-        /// <param name="vendComponent"></param>
-        public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, VendingMachineComponent? vendComponent = null)
-        {
-            if (!Resolve(uid, ref vendComponent))
-                return;
-
-            if (vendComponent.Ejecting || vendComponent.Broken || !this.IsPowered(uid, EntityManager))
-            {
-                return;
-            }
-
-            var entry = GetEntry(uid, itemId, type, vendComponent);
-
-            if (entry == null)
-            {
-                Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-invalid-item"), uid);
-                Deny(uid, vendComponent);
-                return;
-            }
-
-            if (entry.Amount <= 0)
-            {
-                Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-out-of-stock"), uid);
-                Deny(uid, vendComponent);
-                return;
-            }
-
-            if (string.IsNullOrEmpty(entry.ID))
-                return;
-
-
-            // Start Ejecting, and prevent users from ordering while anim playing
-            vendComponent.Ejecting = true;
-            vendComponent.NextItemToEject = entry.ID;
-            vendComponent.ThrowNextItem = throwItem;
-
-            if (TryComp(uid, out SpeakOnUIClosedComponent? speakComponent))
-                _speakOnUIClosed.TrySetFlag((uid, speakComponent));
-
-            entry.Amount--;
-            Dirty(uid, vendComponent);
-            TryUpdateVisualState(uid, vendComponent);
-            Audio.PlayPvs(vendComponent.SoundVend, uid);
-        }
-
-        /// <summary>
-        /// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true
-        /// </summary>
-        /// <param name="uid"></param>
-        /// <param name="sender">Entity that is trying to use the vending machine</param>
-        /// <param name="type">The type of inventory the item is from</param>
-        /// <param name="itemId">The prototype ID of the item</param>
-        /// <param name="component"></param>
-        public void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component)
-        {
-            if (IsAuthorized(uid, sender, component))
-            {
-                TryEjectVendorItem(uid, type, itemId, component.CanShoot, component);
-            }
-        }
-
-        /// <summary>
-        /// Tries to update the visuals of the component based on its current state.
-        /// </summary>
-        public void TryUpdateVisualState(EntityUid uid, VendingMachineComponent? vendComponent = null)
-        {
-            if (!Resolve(uid, ref vendComponent))
-                return;
-
-            var finalState = VendingMachineVisualState.Normal;
-            if (vendComponent.Broken)
-            {
-                finalState = VendingMachineVisualState.Broken;
-            }
-            else if (vendComponent.Ejecting)
-            {
-                finalState = VendingMachineVisualState.Eject;
-            }
-            else if (vendComponent.Denying)
-            {
-                finalState = VendingMachineVisualState.Deny;
-            }
-            else if (!this.IsPowered(uid, EntityManager))
-            {
-                finalState = VendingMachineVisualState.Off;
-            }
-
-            if (_light.TryGetLight(uid, out var pointlight))
-            {
-                var lightState = finalState != VendingMachineVisualState.Broken && finalState != VendingMachineVisualState.Off;
-                _light.SetEnabled(uid, lightState, pointlight);
-            }
-
-            _appearanceSystem.SetData(uid, VendingMachineVisuals.VisualState, finalState);
-        }
-
         /// <summary>
         /// Ejects a random item from the available stock. Will do nothing if the vending machine is empty.
         /// </summary>
@@ -367,18 +202,18 @@ namespace Content.Server.VendingMachines
             }
             else
             {
-                TryEjectVendorItem(uid, item.Type, item.ID, throwItem, vendComponent);
+                TryEjectVendorItem(uid, item.Type, item.ID, throwItem, user: null, vendComponent: vendComponent);
             }
         }
 
-        private void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false)
+        protected override void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false)
         {
             if (!Resolve(uid, ref vendComponent))
                 return;
 
             // No need to update the visual state because we never changed it during a forced eject
             if (!forceEject)
-                TryUpdateVisualState(uid, vendComponent);
+                TryUpdateVisualState((uid, vendComponent));
 
             if (string.IsNullOrEmpty(vendComponent.NextItemToEject))
             {
@@ -411,68 +246,17 @@ namespace Content.Server.VendingMachines
             vendComponent.ThrowNextItem = false;
         }
 
-        private VendingMachineInventoryEntry? GetEntry(EntityUid uid, string entryId, InventoryType type, VendingMachineComponent? component = null)
-        {
-            if (!Resolve(uid, ref component))
-                return null;
-
-            if (type == InventoryType.Emagged && _emag.CheckFlag(uid, EmagType.Interaction))
-                return component.EmaggedInventory.GetValueOrDefault(entryId);
-
-            if (type == InventoryType.Contraband && component.Contraband)
-                return component.ContrabandInventory.GetValueOrDefault(entryId);
-
-            return component.Inventory.GetValueOrDefault(entryId);
-        }
-
         public override void Update(float frameTime)
         {
             base.Update(frameTime);
 
-            var query = EntityQueryEnumerator<VendingMachineComponent>();
-            while (query.MoveNext(out var uid, out var comp))
-            {
-                if (comp.Ejecting)
-                {
-                    comp.EjectAccumulator += frameTime;
-                    if (comp.EjectAccumulator >= comp.EjectDelay)
-                    {
-                        comp.EjectAccumulator = 0f;
-                        comp.Ejecting = false;
-
-                        EjectItem(uid, comp);
-                    }
-                }
-
-                if (comp.Denying)
-                {
-                    comp.DenyAccumulator += frameTime;
-                    if (comp.DenyAccumulator >= comp.DenyDelay)
-                    {
-                        comp.DenyAccumulator = 0f;
-                        comp.Denying = false;
-
-                        TryUpdateVisualState(uid, comp);
-                    }
-                }
-
-                if (comp.DispenseOnHitCoolingDown)
-                {
-                    comp.DispenseOnHitAccumulator += frameTime;
-                    if (comp.DispenseOnHitAccumulator >= comp.DispenseOnHitCooldown)
-                    {
-                        comp.DispenseOnHitAccumulator = 0f;
-                        comp.DispenseOnHitCoolingDown = false;
-                    }
-                }
-            }
             var disabled = EntityQueryEnumerator<EmpDisabledComponent, VendingMachineComponent>();
             while (disabled.MoveNext(out var uid, out _, out var comp))
             {
                 if (comp.NextEmpEject < _timing.CurTime)
                 {
                     EjectRandom(uid, true, false, comp);
-                    comp.NextEmpEject += TimeSpan.FromSeconds(5 * comp.EjectDelay);
+                    comp.NextEmpEject += (5 * comp.EjectDelay);
                 }
             }
         }
@@ -485,7 +269,7 @@ namespace Content.Server.VendingMachines
             RestockInventoryFromPrototype(uid, vendComponent);
 
             Dirty(uid, vendComponent);
-            TryUpdateVisualState(uid, vendComponent);
+            TryUpdateVisualState((uid, vendComponent));
         }
 
         private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args)
similarity index 80%
rename from Content.Server/Advertise/Components/SpeakOnUIClosedComponent.cs
rename to Content.Shared/Advertise/Components/SpeakOnUIClosedComponent.cs
index 99d0080d7f227d6d48b86d257a06c32ad60c8734..1f9672dc7f187f8ed1ed9c709929cfc52df29c5b 100644 (file)
@@ -1,13 +1,15 @@
+using Content.Shared.Advertise.Systems;
 using Content.Shared.Dataset;
+using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 
-namespace Content.Server.Advertise.Components;
+namespace Content.Shared.Advertise.Components;
 
 /// <summary>
 /// Causes the entity to speak using the Chat system when its ActivatableUI is closed, optionally
 /// requiring that a Flag be set as well.
 /// </summary>
-[RegisterComponent, Access(typeof(SpeakOnUIClosedSystem))]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedSpeakOnUIClosedSystem))]
 public sealed partial class SpeakOnUIClosedComponent : Component
 {
     /// <summary>
@@ -31,6 +33,6 @@ public sealed partial class SpeakOnUIClosedComponent : Component
     /// <summary>
     /// State variable only used if <see cref="RequireFlag"/> is true. Set with <see cref="SpeakOnUIClosedSystem.TrySetFlag"/>.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public bool Flag;
 }
diff --git a/Content.Shared/Advertise/Systems/SharedSpeakOnUIClosedSystem.cs b/Content.Shared/Advertise/Systems/SharedSpeakOnUIClosedSystem.cs
new file mode 100644 (file)
index 0000000..2574e9d
--- /dev/null
@@ -0,0 +1,16 @@
+using SpeakOnUIClosedComponent = Content.Shared.Advertise.Components.SpeakOnUIClosedComponent;
+
+namespace Content.Shared.Advertise.Systems;
+
+public abstract class SharedSpeakOnUIClosedSystem : EntitySystem
+{
+    public bool TrySetFlag(Entity<SpeakOnUIClosedComponent?> entity, bool value = true)
+    {
+        if (!Resolve(entity, ref entity.Comp))
+            return false;
+
+        entity.Comp.Flag = value;
+        Dirty(entity);
+        return true;
+    }
+}
index f8d00f56f033fa5a2bb3b0aa132c50fca7e98126..de57189bc76f7b1c64daf6820d5f187fc3cef126 100644 (file)
@@ -67,7 +67,7 @@ public abstract partial class SharedVendingMachineSystem
 
         args.Handled = true;
 
-        var doAfterArgs = new DoAfterArgs(EntityManager, args.User, (float) component.RestockDelay.TotalSeconds, new RestockDoAfterEvent(), target,
+        var doAfterArgs = new DoAfterArgs(EntityManager, args.User, (float)component.RestockDelay.TotalSeconds, new RestockDoAfterEvent(), target,
             target: target, used: uid)
         {
             BreakOnMove = true,
index c4f3eede2d29ba2f66d40ea773d552a2551947d6..d44e00c599ff92b27522cf6bc0b5a521c7452a49 100644 (file)
 using Content.Shared.Emag.Components;
 using Robust.Shared.Prototypes;
 using System.Linq;
+using Content.Shared.Access.Components;
+using Content.Shared.Access.Systems;
+using Content.Shared.Advertise.Components;
+using Content.Shared.Advertise.Systems;
 using Content.Shared.DoAfter;
 using Content.Shared.Emag.Systems;
 using Content.Shared.Interaction;
 using Content.Shared.Popups;
+using Content.Shared.Power.EntitySystems;
 using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
+using Robust.Shared.GameStates;
 using Robust.Shared.Network;
 using Robust.Shared.Random;
+using Robust.Shared.Timing;
 
 namespace Content.Shared.VendingMachines;
 
 public abstract partial class SharedVendingMachineSystem : EntitySystem
 {
-    [Dependency] private readonly INetManager _net = default!;
+    [Dependency] protected readonly IGameTiming Timing = default!;
+    [Dependency] private   readonly INetManager _net = default!;
     [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
+    [Dependency] private   readonly AccessReaderSystem _accessReader = default!;
+    [Dependency] private   readonly SharedAppearanceSystem _appearanceSystem = default!;
     [Dependency] protected readonly SharedAudioSystem Audio = default!;
-    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private   readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] protected readonly SharedPointLightSystem Light = default!;
+    [Dependency] private   readonly SharedPowerReceiverSystem _receiver = default!;
     [Dependency] protected readonly SharedPopupSystem Popup = default!;
+    [Dependency] private   readonly SharedSpeakOnUIClosedSystem _speakOn = default!;
+    [Dependency] protected readonly SharedUserInterfaceSystem UISystem = default!;
     [Dependency] protected readonly IRobustRandom Randomizer = default!;
     [Dependency] private readonly EmagSystem _emag = default!;
 
     public override void Initialize()
     {
         base.Initialize();
+        SubscribeLocalEvent<VendingMachineComponent, ComponentGetState>(OnVendingGetState);
         SubscribeLocalEvent<VendingMachineComponent, MapInitEvent>(OnMapInit);
         SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged);
 
         SubscribeLocalEvent<VendingMachineRestockComponent, AfterInteractEvent>(OnAfterInteract);
+
+        Subs.BuiEvents<VendingMachineComponent>(VendingMachineUiKey.Key, subs =>
+        {
+            subs.Event<VendingMachineEjectMessage>(OnInventoryEjectMessage);
+        });
+    }
+
+    private void OnVendingGetState(Entity<VendingMachineComponent> entity, ref ComponentGetState args)
+    {
+        var component = entity.Comp;
+
+        var inventory = new Dictionary<string, VendingMachineInventoryEntry>();
+        var emaggedInventory = new Dictionary<string, VendingMachineInventoryEntry>();
+        var contrabandInventory = new Dictionary<string, VendingMachineInventoryEntry>();
+
+        foreach (var weh in component.Inventory)
+        {
+            inventory[weh.Key] = new(weh.Value);
+        }
+
+        foreach (var weh in component.EmaggedInventory)
+        {
+            emaggedInventory[weh.Key] = new(weh.Value);
+        }
+
+        foreach (var weh in component.ContrabandInventory)
+        {
+            contrabandInventory[weh.Key] = new(weh.Value);
+        }
+
+        args.State = new VendingMachineComponentState()
+        {
+            Inventory = inventory,
+            EmaggedInventory = emaggedInventory,
+            ContrabandInventory = contrabandInventory,
+            Contraband = component.Contraband,
+            EjectEnd = component.EjectEnd,
+            DenyEnd = component.DenyEnd,
+            DispenseOnHitEnd = component.DispenseOnHitEnd,
+        };
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        var query = EntityQueryEnumerator<VendingMachineComponent>();
+        var curTime = Timing.CurTime;
+
+        while (query.MoveNext(out var uid, out var comp))
+        {
+            if (comp.Ejecting)
+            {
+                if (curTime > comp.EjectEnd)
+                {
+                    comp.EjectEnd = null;
+                    Dirty(uid, comp);
+
+                    EjectItem(uid, comp);
+                    UpdateUI((uid, comp));
+                }
+            }
+
+            if (comp.Denying)
+            {
+                if (curTime > comp.DenyEnd)
+                {
+                    comp.DenyEnd = null;
+                    Dirty(uid, comp);
+
+                    TryUpdateVisualState((uid, comp));
+                }
+            }
+
+            if (comp.DispenseOnHitCoolingDown)
+            {
+                if (curTime > comp.DispenseOnHitEnd)
+                {
+                    comp.DispenseOnHitEnd = null;
+                    Dirty(uid, comp);
+                }
+            }
+        }
+    }
+
+    private void OnInventoryEjectMessage(Entity<VendingMachineComponent> entity, ref VendingMachineEjectMessage args)
+    {
+        if (!_receiver.IsPowered(entity.Owner) || Deleted(entity))
+            return;
+
+        if (args.Actor is not { Valid: true } actor)
+            return;
+
+        AuthorizedVend(entity.Owner, actor, args.Type, args.ID, entity.Comp);
     }
 
     protected virtual void OnMapInit(EntityUid uid, VendingMachineComponent component, MapInitEvent args)
@@ -36,6 +145,162 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
         RestockInventoryFromPrototype(uid, component, component.InitialStockQuality);
     }
 
+    protected virtual void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false) { }
+
+    /// <summary>
+    /// Checks if the user is authorized to use this vending machine
+    /// </summary>
+    /// <param name="uid"></param>
+    /// <param name="sender">Entity trying to use the vending machine</param>
+    /// <param name="vendComponent"></param>
+    public bool IsAuthorized(EntityUid uid, EntityUid sender, VendingMachineComponent? vendComponent = null)
+    {
+        if (!Resolve(uid, ref vendComponent))
+            return false;
+
+        if (!TryComp<AccessReaderComponent>(uid, out var accessReader))
+            return true;
+
+        if (_accessReader.IsAllowed(sender, uid, accessReader) || HasComp<EmaggedComponent>(uid))
+            return true;
+
+        Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid, sender);
+        Deny((uid, vendComponent), sender);
+        return false;
+    }
+
+    protected VendingMachineInventoryEntry? GetEntry(EntityUid uid, string entryId, InventoryType type, VendingMachineComponent? component = null)
+    {
+        if (!Resolve(uid, ref component))
+            return null;
+
+        if (type == InventoryType.Emagged && HasComp<EmaggedComponent>(uid))
+            return component.EmaggedInventory.GetValueOrDefault(entryId);
+
+        if (type == InventoryType.Contraband && component.Contraband)
+            return component.ContrabandInventory.GetValueOrDefault(entryId);
+
+        return component.Inventory.GetValueOrDefault(entryId);
+    }
+
+    /// <summary>
+    /// Tries to eject the provided item. Will do nothing if the vending machine is incapable of ejecting, already ejecting
+    /// or the item doesn't exist in its inventory.
+    /// </summary>
+    /// <param name="uid"></param>
+    /// <param name="type">The type of inventory the item is from</param>
+    /// <param name="itemId">The prototype ID of the item</param>
+    /// <param name="throwItem">Whether the item should be thrown in a random direction after ejection</param>
+    /// <param name="vendComponent"></param>
+    public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, EntityUid? user = null, VendingMachineComponent? vendComponent = null)
+    {
+        if (!Resolve(uid, ref vendComponent))
+            return;
+
+        if (vendComponent.Ejecting || vendComponent.Broken || !_receiver.IsPowered(uid))
+        {
+            return;
+        }
+
+        var entry = GetEntry(uid, itemId, type, vendComponent);
+
+        if (string.IsNullOrEmpty(entry?.ID))
+        {
+            Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-invalid-item"), uid);
+            Deny((uid, vendComponent));
+            return;
+        }
+
+        if (entry.Amount <= 0)
+        {
+            Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-out-of-stock"), uid);
+            Deny((uid, vendComponent));
+            return;
+        }
+
+        // Start Ejecting, and prevent users from ordering while anim playing
+        vendComponent.EjectEnd = Timing.CurTime + vendComponent.EjectDelay;
+        vendComponent.NextItemToEject = entry.ID;
+        vendComponent.ThrowNextItem = throwItem;
+
+        if (TryComp(uid, out SpeakOnUIClosedComponent? speakComponent))
+            _speakOn.TrySetFlag((uid, speakComponent));
+
+        entry.Amount--;
+        Dirty(uid, vendComponent);
+        UpdateUI((uid, vendComponent));
+        TryUpdateVisualState((uid, vendComponent));
+        Audio.PlayPredicted(vendComponent.SoundVend, uid, user);
+    }
+
+    public void Deny(Entity<VendingMachineComponent?> entity, EntityUid? user = null)
+    {
+        if (!Resolve(entity.Owner, ref entity.Comp))
+            return;
+
+        if (entity.Comp.Denying)
+            return;
+
+        entity.Comp.DenyEnd = Timing.CurTime + entity.Comp.DenyDelay;
+        Audio.PlayPredicted(entity.Comp.SoundDeny, entity.Owner, user, AudioParams.Default.WithVolume(-2f));
+        TryUpdateVisualState(entity);
+        Dirty(entity);
+    }
+
+    protected virtual void UpdateUI(Entity<VendingMachineComponent?> entity) { }
+
+    /// <summary>
+    /// Tries to update the visuals of the component based on its current state.
+    /// </summary>
+    public void TryUpdateVisualState(Entity<VendingMachineComponent?> entity)
+    {
+        if (!Resolve(entity.Owner, ref entity.Comp))
+            return;
+
+        var finalState = VendingMachineVisualState.Normal;
+        if (entity.Comp.Broken)
+        {
+            finalState = VendingMachineVisualState.Broken;
+        }
+        else if (entity.Comp.Ejecting)
+        {
+            finalState = VendingMachineVisualState.Eject;
+        }
+        else if (entity.Comp.Denying)
+        {
+            finalState = VendingMachineVisualState.Deny;
+        }
+        else if (!_receiver.IsPowered(entity.Owner))
+        {
+            finalState = VendingMachineVisualState.Off;
+        }
+
+        // TODO: You know this should really live on the client with netsync off because client knows the state.
+        if (Light.TryGetLight(entity.Owner, out var pointlight))
+        {
+            var lightEnabled = finalState != VendingMachineVisualState.Broken && finalState != VendingMachineVisualState.Off;
+            Light.SetEnabled(entity.Owner, lightEnabled, pointlight);
+        }
+
+        _appearanceSystem.SetData(entity.Owner, VendingMachineVisuals.VisualState, finalState);
+    }
+
+    /// <summary>
+    /// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true
+    /// </summary>
+    /// <param name="uid"></param>
+    /// <param name="sender">Entity that is trying to use the vending machine</param>
+    /// <param name="type">The type of inventory the item is from</param>
+    /// <param name="itemId">The prototype ID of the item</param>
+    /// <param name="component"></param>
+    public void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component)
+    {
+        if (IsAuthorized(uid, sender, component))
+        {
+            TryEjectVendorItem(uid, type, itemId, component.CanShoot, sender, component);
+        }
+    }
+
     public void RestockInventoryFromPrototype(EntityUid uid,
         VendingMachineComponent? component = null, float restockQuality = 1f)
     {
index f3fe3a1ecdba5d8049515fcc197558ded9594290..cbd59dbfaa2c4d1474c39100552b02233128cf1d 100644 (file)
@@ -1,19 +1,19 @@
 using Content.Shared.Actions;
 using Robust.Shared.Audio;
 using Robust.Shared.GameStates;
-using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 
 namespace Content.Shared.VendingMachines
 {
-    [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+    [RegisterComponent, NetworkedComponent, AutoGenerateComponentPause]
     public sealed partial class VendingMachineComponent : Component
     {
         /// <summary>
         /// PrototypeID for the vending machine's inventory, see <see cref="VendingMachineInventoryPrototype"/>
         /// </summary>
+        // Okay so not using ProtoId here is load-bearing because the ProtoId serializer will log errors if the prototype doesn't exist.
         [DataField("pack", customTypeSerializer: typeof(PrototypeIdSerializer<VendingMachineInventoryPrototype>), required: true)]
         public string PackPrototypeId = string.Empty;
 
@@ -22,7 +22,7 @@ namespace Content.Shared.VendingMachines
         /// Used by the client to determine how long the deny animation should be played.
         /// </summary>
         [DataField]
-        public float DenyDelay = 2.0f;
+        public TimeSpan DenyDelay = TimeSpan.FromSeconds(2);
 
         /// <summary>
         /// Used by the server to determine how long the vending machine stays in the "Eject" state.
@@ -30,23 +30,40 @@ namespace Content.Shared.VendingMachines
         /// Used by the client to determine how long the deny animation should be played.
         /// </summary>
         [DataField]
-        public float EjectDelay = 1.2f;
+        public TimeSpan EjectDelay = TimeSpan.FromSeconds(1.2);
 
-        [DataField, AutoNetworkedField]
+        [DataField]
         public Dictionary<string, VendingMachineInventoryEntry> Inventory = new();
 
-        [DataField, AutoNetworkedField]
+        [DataField]
         public Dictionary<string, VendingMachineInventoryEntry> EmaggedInventory = new();
 
-        [DataField, AutoNetworkedField]
+        [DataField]
         public Dictionary<string, VendingMachineInventoryEntry> ContrabandInventory = new();
 
-        [DataField, AutoNetworkedField]
+        /// <summary>
+        /// If true then unlocks the <see cref="ContrabandInventory"/>
+        /// </summary>
+        [DataField]
         public bool Contraband;
 
-        public bool Ejecting;
-        public bool Denying;
-        public bool DispenseOnHitCoolingDown;
+        [ViewVariables]
+        public bool Ejecting => EjectEnd != null;
+
+        [ViewVariables]
+        public bool Denying => DenyEnd != null;
+
+        [ViewVariables]
+        public bool DispenseOnHitCoolingDown => DispenseOnHitEnd != null;
+
+        [DataField, AutoPausedField]
+        public TimeSpan? EjectEnd;
+
+        [DataField, AutoPausedField]
+        public TimeSpan? DenyEnd;
+
+        [DataField]
+        public TimeSpan? DispenseOnHitEnd;
 
         public string? NextItemToEject;
 
@@ -55,7 +72,7 @@ namespace Content.Shared.VendingMachines
         /// <summary>
         /// When true, will forcefully throw any object it dispenses
         /// </summary>
-        [DataField("speedLimiter")]
+        [DataField]
         public bool CanShoot = false;
 
         public bool ThrowNextItem = false;
@@ -64,14 +81,14 @@ namespace Content.Shared.VendingMachines
         ///     The chance that a vending machine will randomly dispense an item on hit.
         ///     Chance is 0 if null.
         /// </summary>
-        [DataField("dispenseOnHitChance")]
+        [DataField]
         public float? DispenseOnHitChance;
 
         /// <summary>
         ///     The minimum amount of damage that must be done per hit to have a chance
         ///     of dispensing an item.
         /// </summary>
-        [DataField("dispenseOnHitThreshold")]
+        [DataField]
         public float? DispenseOnHitThreshold;
 
         /// <summary>
@@ -80,13 +97,13 @@ namespace Content.Shared.VendingMachines
         ///     0 for a vending machine for legitimate reasons (no desired delay/no eject animation)
         ///     and can be circumvented with forced ejections.
         /// </summary>
-        [DataField("dispenseOnHitCooldown")]
-        public float? DispenseOnHitCooldown = 1.0f;
+        [DataField]
+        public TimeSpan? DispenseOnHitCooldown = TimeSpan.FromSeconds(1.0);
 
         /// <summary>
         ///     Sound that plays when ejecting an item
         /// </summary>
-        [DataField("soundVend")]
+        [DataField]
         // Grabbed from: https://github.com/tgstation/tgstation/blob/d34047a5ae911735e35cd44a210953c9563caa22/sound/machines/machine_vend.ogg
         public SoundSpecifier SoundVend = new SoundPathSpecifier("/Audio/Machines/machine_vend.ogg")
         {
@@ -100,7 +117,7 @@ namespace Content.Shared.VendingMachines
         /// <summary>
         ///     Sound that plays when an item can't be ejected
         /// </summary>
-        [DataField("soundDeny")]
+        [DataField]
         // Yoinked from: https://github.com/discordia-space/CEV-Eris/blob/35bbad6764b14e15c03a816e3e89aa1751660ba9/sound/machines/Custom_deny.ogg
         public SoundSpecifier SoundDeny = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
 
@@ -108,10 +125,6 @@ namespace Content.Shared.VendingMachines
 
         public float NonLimitedEjectRange = 5f;
 
-        public float EjectAccumulator = 0f;
-        public float DenyAccumulator = 0f;
-        public float DispenseOnHitAccumulator = 0f;
-
         /// <summary>
         /// The quality of the stock in the vending machine on spawn.
         /// Represents the percentage chance (0.0f = 0%, 1.0f = 100%) each set of items in the machine is fully-stocked.
@@ -123,7 +136,7 @@ namespace Content.Shared.VendingMachines
         /// <summary>
         ///     While disabled by EMP it randomly ejects items
         /// </summary>
-        [DataField("nextEmpEject", customTypeSerializer: typeof(TimeOffsetSerializer))]
+        [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
         public TimeSpan NextEmpEject = TimeSpan.Zero;
 
         #region Client Visuals
@@ -131,28 +144,28 @@ namespace Content.Shared.VendingMachines
         /// RSI state for when the vending machine is unpowered.
         /// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Base"/>
         /// </summary>
-        [DataField("offState")]
+        [DataField]
         public string? OffState;
 
         /// <summary>
         /// RSI state for the screen of the vending machine
         /// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Screen"/>
         /// </summary>
-        [DataField("screenState")]
+        [DataField]
         public string? ScreenState;
 
         /// <summary>
         /// RSI state for the vending machine's normal state. Usually a looping animation.
         /// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
         /// </summary>
-        [DataField("normalState")]
+        [DataField]
         public string? NormalState;
 
         /// <summary>
         /// RSI state for the vending machine's eject animation.
         /// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
         /// </summary>
-        [DataField("ejectState")]
+        [DataField]
         public string? EjectState;
 
         /// <summary>
@@ -160,14 +173,14 @@ namespace Content.Shared.VendingMachines
         /// or looped depending on how <see cref="LoopDenyAnimation"/> is set.
         /// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
         /// </summary>
-        [DataField("denyState")]
+        [DataField]
         public string? DenyState;
 
         /// <summary>
         /// RSI state for when the vending machine is unpowered.
         /// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Base"/>
         /// </summary>
-        [DataField("brokenState")]
+        [DataField]
         public string? BrokenState;
 
         /// <summary>
@@ -195,6 +208,13 @@ namespace Content.Shared.VendingMachines
             ID = id;
             Amount = amount;
         }
+
+        public VendingMachineInventoryEntry(VendingMachineInventoryEntry entry)
+        {
+            Type = entry.Type;
+            ID = entry.ID;
+            Amount = entry.Amount;
+        }
     }
 
     [Serializable, NetSerializable]
@@ -206,13 +226,13 @@ namespace Content.Shared.VendingMachines
     }
 
     [Serializable, NetSerializable]
-    public enum VendingMachineVisuals
+    public enum VendingMachineVisuals : byte
     {
         VisualState
     }
 
     [Serializable, NetSerializable]
-    public enum VendingMachineVisualState
+    public enum VendingMachineVisualState : byte
     {
         Normal,
         Off,
@@ -254,4 +274,22 @@ namespace Content.Shared.VendingMachines
     {
 
     };
+
+    [Serializable, NetSerializable]
+    public sealed class VendingMachineComponentState : ComponentState
+    {
+        public Dictionary<string, VendingMachineInventoryEntry> Inventory = new();
+
+        public Dictionary<string, VendingMachineInventoryEntry> EmaggedInventory = new();
+
+        public Dictionary<string, VendingMachineInventoryEntry> ContrabandInventory = new();
+
+        public bool Contraband;
+
+        public TimeSpan? EjectEnd;
+
+        public TimeSpan? DenyEnd;
+
+        public TimeSpan? DispenseOnHitEnd;
+    }
 }