--- /dev/null
+using Content.Shared.Advertise.Systems;
+
+namespace Content.Client.Advertise.Systems;
+
+public sealed class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem;
{
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.
public ListContainerButton(ListData data, int index)
{
+ AddStyleClass(StyleClassButton);
Data = data;
Index = index;
// AddChild(Background = new PanelContainer
NameLabel.Text = text;
}
+
+ public void SetText(string text)
+ {
+ NameLabel.Text = text;
+ }
}
+using System.Linq;
using System.Numerics;
using Content.Shared.VendingMachines;
using Robust.Client.AutoGenerated;
[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()
{
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;
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))
{
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);
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;
+ }
+}
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)
if (selectedItem == null)
return;
- SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
+ SendPredictedMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
}
protected override void Dispose(bool disposing)
+using System.Linq;
using Content.Shared.VendingMachines;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
+using Robust.Shared.GameStates;
namespace Content.Client.VendingMachines;
{
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
- [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
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();
}
}
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;
-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!;
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;
- }
}
-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;
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;
SubscribeLocalEvent<SpaceVillainArcadeComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<SpaceVillainArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpenSV);
- SubscribeLocalEvent<SpaceVillainArcadeComponent, SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
+ SubscribeLocalEvent<SpaceVillainArcadeComponent, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
SubscribeLocalEvent<SpaceVillainArcadeComponent, PowerChangedEvent>(OnSVillainPower);
}
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;
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;
}
}
if (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered)
return;
- _uiSystem.CloseUi(uid, SpaceVillainArcadeUiKey.Key);
+ _uiSystem.CloseUi(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key);
}
}
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;
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;
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;
SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt);
- Subs.BuiEvents<VendingMachineComponent>(VendingMachineUiKey.Key, subs =>
- {
- subs.Event<VendingMachineEjectMessage>(OnInventoryEjectMessage);
- });
-
SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense);
SubscribeLocalEvent<VendingMachineComponent, RestockDoAfterEvent>(OnDoAfter);
if (HasComp<ApcPowerReceiverComponent>(uid))
{
- TryUpdateVisualState(uid, component);
+ TryUpdateVisualState((uid, component));
}
}
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)
if (!args.DamageIncreased && component.Broken)
{
component.Broken = false;
- TryUpdateVisualState(uid, component);
+ TryUpdateVisualState((uid, component));
return;
}
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);
}
}
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>
}
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))
{
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);
}
}
}
RestockInventoryFromPrototype(uid, vendComponent);
Dirty(uid, vendComponent);
- TryUpdateVisualState(uid, vendComponent);
+ TryUpdateVisualState((uid, vendComponent));
}
private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args)
+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>
/// <summary>
/// State variable only used if <see cref="RequireFlag"/> is true. Set with <see cref="SpeakOnUIClosedSystem.TrySetFlag"/>.
/// </summary>
- [DataField]
+ [DataField, AutoNetworkedField]
public bool Flag;
}
--- /dev/null
+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;
+ }
+}
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,
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)
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)
{
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;
/// 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.
/// 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;
/// <summary>
/// When true, will forcefully throw any object it dispenses
/// </summary>
- [DataField("speedLimiter")]
+ [DataField]
public bool CanShoot = false;
public bool ThrowNextItem = false;
/// 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>
/// 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")
{
/// <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");
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.
/// <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
/// 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>
/// 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>
ID = id;
Amount = amount;
}
+
+ public VendingMachineInventoryEntry(VendingMachineInventoryEntry entry)
+ {
+ Type = entry.Type;
+ ID = entry.ID;
+ Amount = entry.Amount;
+ }
}
[Serializable, NetSerializable]
}
[Serializable, NetSerializable]
- public enum VendingMachineVisuals
+ public enum VendingMachineVisuals : byte
{
VisualState
}
[Serializable, NetSerializable]
- public enum VendingMachineVisualState
+ public enum VendingMachineVisualState : byte
{
Normal,
Off,
{
};
+
+ [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;
+ }
}