From a8ebcac5c9e36765671b742a4f587a80cfadf4f3 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sun, 2 Mar 2025 13:47:52 +1100 Subject: [PATCH] Predict vending machine UI (#33412) --- .../Systems/SpeakOnUIClosedSystem.cs | 5 + .../UserInterface/Controls/ListContainer.cs | 6 +- .../UI/VendingMachineItem.xaml.cs | 5 + .../UI/VendingMachineMenu.xaml.cs | 69 ++++- .../VendingMachineBoundUserInterface.cs | 15 +- .../VendingMachines/VendingMachineSystem.cs | 70 ++++- .../EntitySystems/SpeakOnUIClosedSystem.cs | 19 +- .../Arcade/BlockGame/BlockGameArcadeSystem.cs | 6 +- .../SpaceVillainArcadeSystem.cs | 26 +- .../VendingMachines/VendingMachineSystem.cs | 244 +--------------- .../Components/SpeakOnUIClosedComponent.cs | 8 +- .../Systems/SharedSpeakOnUIClosedSystem.cs | 16 ++ .../SharedVendingMachineSystem.Restock.cs | 2 +- .../SharedVendingMachineSystem.cs | 269 +++++++++++++++++- .../VendingMachineComponent.cs | 100 +++++-- 15 files changed, 542 insertions(+), 318 deletions(-) create mode 100644 Content.Client/Advertise/Systems/SpeakOnUIClosedSystem.cs rename {Content.Server => Content.Shared}/Advertise/Components/SpeakOnUIClosedComponent.cs (80%) create mode 100644 Content.Shared/Advertise/Systems/SharedSpeakOnUIClosedSystem.cs diff --git a/Content.Client/Advertise/Systems/SpeakOnUIClosedSystem.cs b/Content.Client/Advertise/Systems/SpeakOnUIClosedSystem.cs new file mode 100644 index 0000000000..4e82ec4d00 --- /dev/null +++ b/Content.Client/Advertise/Systems/SpeakOnUIClosedSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Advertise.Systems; + +namespace Content.Client.Advertise.Systems; + +public sealed class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem; diff --git a/Content.Client/UserInterface/Controls/ListContainer.cs b/Content.Client/UserInterface/Controls/ListContainer.cs index e1b3b948f0..0ee0a67af0 100644 --- a/Content.Client/UserInterface/Controls/ListContainer.cs +++ b/Content.Client/UserInterface/Controls/ListContainer.cs @@ -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 diff --git a/Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs b/Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs index a7212934fd..0f0564c596 100644 --- a/Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs +++ b/Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs @@ -16,4 +16,9 @@ public sealed partial class VendingMachineItem : BoxContainer NameLabel.Text = text; } + + public void SetText(string text) + { + NameLabel.Text = text; + } } diff --git a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs index ee7a0e41fa..899a0208cb 100644 --- a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs +++ b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Numerics; using Content.Shared.VendingMachines; using Robust.Client.AutoGenerated; @@ -19,11 +20,16 @@ namespace Content.Client.VendingMachines.UI [Dependency] private readonly IEntityManager _entityManager = default!; private readonly Dictionary _dummies = []; + private readonly Dictionary _listItems = new(); + private readonly Dictionary _amounts = new(); + + /// + /// Whether the vending machine is able to be interacted with or not. + /// + private bool _enabled; public event Action? OnItemSelected; - private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) }; - public VendingMachineMenu() { MinSize = SetSize = new Vector2(250, 150); @@ -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; } /// /// Populates the list of available items on the vending machine interface /// and sets icons based on their prototypes /// - public void Populate(List inventory) + public void Populate(List 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); } + /// + /// Updates text entries for vending data in place without modifying the list controls. + /// + public void UpdateAmounts(List 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; + } +} diff --git a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs index 052bdacb89..874808158d 100644 --- a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs +++ b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs @@ -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(); _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(); + _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) diff --git a/Content.Client/VendingMachines/VendingMachineSystem.cs b/Content.Client/VendingMachines/VendingMachineSystem.cs index 1b1dde2b67..130296c8a1 100644 --- a/Content.Client/VendingMachines/VendingMachineSystem.cs +++ b/Content.Client/VendingMachines/VendingMachineSystem.cs @@ -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(OnAppearanceChange); SubscribeLocalEvent(OnAnimationCompleted); - SubscribeLocalEvent(OnVendingAfterState); + SubscribeLocalEvent(OnVendingHandleState); } - private void OnVendingAfterState(EntityUid uid, VendingMachineComponent component, ref AfterAutoHandleStateEvent args) + private void OnVendingHandleState(Entity entity, ref ComponentHandleState args) { - if (_uiSystem.TryGetOpenUi(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) { - bui.Refresh(); + 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(uid, VendingMachineUiKey.Key, out var bui)) + { + if (fullUiUpdate) + { + bui.Refresh(); + } + else + { + bui.UpdateAmounts(); + } + } + } + + protected override void UpdateUI(Entity entity) + { + if (!Resolve(entity, ref entity.Comp)) + return; + + if (UISystem.TryGetOpenUi(entity.Owner, + VendingMachineUiKey.Key, + out var bui)) + { + 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; diff --git a/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs b/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs index a0a709e5fa..3fca640d4a 100644 --- a/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs +++ b/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs @@ -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 entity, bool value = true) - { - if (!Resolve(entity, ref entity.Comp)) - return false; - - entity.Comp.Flag = value; - return true; - } } diff --git a/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs b/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs index b0bf389509..a0e52e9b48 100644 --- a/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs +++ b/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs @@ -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; diff --git a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs index b359a13bd1..2070ab8bfe 100644 --- a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs +++ b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs @@ -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(OnComponentInit); SubscribeLocalEvent(OnAfterUIOpenSV); - SubscribeLocalEvent(OnSVPlayerAction); + SubscribeLocalEvent(OnSVPlayerAction); SubscribeLocalEvent(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(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(uid, out var power) && power.Powered) return; - _uiSystem.CloseUi(uid, SpaceVillainArcadeUiKey.Key); + _uiSystem.CloseUi(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key); } } diff --git a/Content.Server/VendingMachines/VendingMachineSystem.cs b/Content.Server/VendingMachines/VendingMachineSystem.cs index c2ea6dd1ac..e061398114 100644 --- a/Content.Server/VendingMachines/VendingMachineSystem.cs +++ b/Content.Server/VendingMachines/VendingMachineSystem.cs @@ -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(OnActivatableUIOpenAttempt); - Subs.BuiEvents(VendingMachineUiKey.Key, subs => - { - subs.Event(OnInventoryEjectMessage); - }); - SubscribeLocalEvent(OnSelfDispense); SubscribeLocalEvent(OnDoAfter); @@ -91,7 +73,7 @@ namespace Content.Server.VendingMachines if (HasComp(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); - } - - /// - /// Checks if the user is authorized to use this vending machine - /// - /// - /// Entity trying to use the vending machine - /// - public bool IsAuthorized(EntityUid uid, EntityUid sender, VendingMachineComponent? vendComponent = null) - { - if (!Resolve(uid, ref vendComponent)) - return false; - - if (!TryComp(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; - } - - /// - /// 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. - /// - /// - /// The type of inventory the item is from - /// The prototype ID of the item - /// Whether the item should be thrown in a random direction after ejection - /// - 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); - } - - /// - /// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true - /// - /// - /// Entity that is trying to use the vending machine - /// The type of inventory the item is from - /// The prototype ID of the item - /// - 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); - } - } - - /// - /// Tries to update the visuals of the component based on its current state. - /// - 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); - } - /// /// Ejects a random item from the available stock. Will do nothing if the vending machine is empty. /// @@ -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(); - 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(); 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) diff --git a/Content.Server/Advertise/Components/SpeakOnUIClosedComponent.cs b/Content.Shared/Advertise/Components/SpeakOnUIClosedComponent.cs similarity index 80% rename from Content.Server/Advertise/Components/SpeakOnUIClosedComponent.cs rename to Content.Shared/Advertise/Components/SpeakOnUIClosedComponent.cs index 99d0080d7f..1f9672dc7f 100644 --- a/Content.Server/Advertise/Components/SpeakOnUIClosedComponent.cs +++ b/Content.Shared/Advertise/Components/SpeakOnUIClosedComponent.cs @@ -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; /// /// Causes the entity to speak using the Chat system when its ActivatableUI is closed, optionally /// requiring that a Flag be set as well. /// -[RegisterComponent, Access(typeof(SpeakOnUIClosedSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedSpeakOnUIClosedSystem))] public sealed partial class SpeakOnUIClosedComponent : Component { /// @@ -31,6 +33,6 @@ public sealed partial class SpeakOnUIClosedComponent : Component /// /// State variable only used if is true. Set with . /// - [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 index 0000000000..2574e9d499 --- /dev/null +++ b/Content.Shared/Advertise/Systems/SharedSpeakOnUIClosedSystem.cs @@ -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 entity, bool value = true) + { + if (!Resolve(entity, ref entity.Comp)) + return false; + + entity.Comp.Flag = value; + Dirty(entity); + return true; + } +} diff --git a/Content.Shared/VendingMachines/SharedVendingMachineSystem.Restock.cs b/Content.Shared/VendingMachines/SharedVendingMachineSystem.Restock.cs index f8d00f56f0..de57189bc7 100644 --- a/Content.Shared/VendingMachines/SharedVendingMachineSystem.Restock.cs +++ b/Content.Shared/VendingMachines/SharedVendingMachineSystem.Restock.cs @@ -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, diff --git a/Content.Shared/VendingMachines/SharedVendingMachineSystem.cs b/Content.Shared/VendingMachines/SharedVendingMachineSystem.cs index c4f3eede2d..d44e00c599 100644 --- a/Content.Shared/VendingMachines/SharedVendingMachineSystem.cs +++ b/Content.Shared/VendingMachines/SharedVendingMachineSystem.cs @@ -1,34 +1,143 @@ 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(OnVendingGetState); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnAfterInteract); + + Subs.BuiEvents(VendingMachineUiKey.Key, subs => + { + subs.Event(OnInventoryEjectMessage); + }); + } + + private void OnVendingGetState(Entity entity, ref ComponentGetState args) + { + var component = entity.Comp; + + var inventory = new Dictionary(); + var emaggedInventory = new Dictionary(); + var contrabandInventory = new Dictionary(); + + 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(); + 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 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) { } + + /// + /// Checks if the user is authorized to use this vending machine + /// + /// + /// Entity trying to use the vending machine + /// + public bool IsAuthorized(EntityUid uid, EntityUid sender, VendingMachineComponent? vendComponent = null) + { + if (!Resolve(uid, ref vendComponent)) + return false; + + if (!TryComp(uid, out var accessReader)) + return true; + + if (_accessReader.IsAllowed(sender, uid, accessReader) || HasComp(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(uid)) + return component.EmaggedInventory.GetValueOrDefault(entryId); + + if (type == InventoryType.Contraband && component.Contraband) + return component.ContrabandInventory.GetValueOrDefault(entryId); + + return component.Inventory.GetValueOrDefault(entryId); + } + + /// + /// 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. + /// + /// + /// The type of inventory the item is from + /// The prototype ID of the item + /// Whether the item should be thrown in a random direction after ejection + /// + 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 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 entity) { } + + /// + /// Tries to update the visuals of the component based on its current state. + /// + public void TryUpdateVisualState(Entity 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); + } + + /// + /// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true + /// + /// + /// Entity that is trying to use the vending machine + /// The type of inventory the item is from + /// The prototype ID of the item + /// + 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) { diff --git a/Content.Shared/VendingMachines/VendingMachineComponent.cs b/Content.Shared/VendingMachines/VendingMachineComponent.cs index f3fe3a1ecd..cbd59dbfaa 100644 --- a/Content.Shared/VendingMachines/VendingMachineComponent.cs +++ b/Content.Shared/VendingMachines/VendingMachineComponent.cs @@ -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 { /// /// PrototypeID for the vending machine's inventory, see /// + // 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), 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. /// [DataField] - public float DenyDelay = 2.0f; + public TimeSpan DenyDelay = TimeSpan.FromSeconds(2); /// /// 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. /// [DataField] - public float EjectDelay = 1.2f; + public TimeSpan EjectDelay = TimeSpan.FromSeconds(1.2); - [DataField, AutoNetworkedField] + [DataField] public Dictionary Inventory = new(); - [DataField, AutoNetworkedField] + [DataField] public Dictionary EmaggedInventory = new(); - [DataField, AutoNetworkedField] + [DataField] public Dictionary ContrabandInventory = new(); - [DataField, AutoNetworkedField] + /// + /// If true then unlocks the + /// + [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 /// /// When true, will forcefully throw any object it dispenses /// - [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. /// - [DataField("dispenseOnHitChance")] + [DataField] public float? DispenseOnHitChance; /// /// The minimum amount of damage that must be done per hit to have a chance /// of dispensing an item. /// - [DataField("dispenseOnHitThreshold")] + [DataField] public float? DispenseOnHitThreshold; /// @@ -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. /// - [DataField("dispenseOnHitCooldown")] - public float? DispenseOnHitCooldown = 1.0f; + [DataField] + public TimeSpan? DispenseOnHitCooldown = TimeSpan.FromSeconds(1.0); /// /// Sound that plays when ejecting an item /// - [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 /// /// Sound that plays when an item can't be ejected /// - [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; - /// /// 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 /// /// While disabled by EMP it randomly ejects items /// - [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 /// - [DataField("offState")] + [DataField] public string? OffState; /// /// RSI state for the screen of the vending machine /// Will be displayed on the layer /// - [DataField("screenState")] + [DataField] public string? ScreenState; /// /// RSI state for the vending machine's normal state. Usually a looping animation. /// Will be displayed on the layer /// - [DataField("normalState")] + [DataField] public string? NormalState; /// /// RSI state for the vending machine's eject animation. /// Will be displayed on the layer /// - [DataField("ejectState")] + [DataField] public string? EjectState; /// @@ -160,14 +173,14 @@ namespace Content.Shared.VendingMachines /// or looped depending on how is set. /// Will be displayed on the layer /// - [DataField("denyState")] + [DataField] public string? DenyState; /// /// RSI state for when the vending machine is unpowered. /// Will be displayed on the layer /// - [DataField("brokenState")] + [DataField] public string? BrokenState; /// @@ -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 Inventory = new(); + + public Dictionary EmaggedInventory = new(); + + public Dictionary ContrabandInventory = new(); + + public bool Contraband; + + public TimeSpan? EjectEnd; + + public TimeSpan? DenyEnd; + + public TimeSpan? DispenseOnHitEnd; + } }