VendingMachine cleanup and small refactor (#44839)

* just cleanup

* remove initialize
This commit is contained in:
AndrewFenriz 2026-07-22 02:57:02 +03:00 • committed by GitHub
parent ae9e947d8a
commit aa64ee8b77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 723 additions and 760 deletions

View file

@ -1,4 +1,3 @@
using System.Linq;
using System.Numerics;
using Content.Client.Stylesheets;
using Content.Shared.VendingMachines;
@ -10,186 +9,184 @@ using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
using Robust.Client.UserInterface;
using Content.Client.UserInterface.Controls;
using Content.Shared.IdentityManagement;
using Robust.Client.Graphics;
using Robust.Shared.Utility;
namespace Content.Client.VendingMachines.UI
namespace Content.Client.VendingMachines.UI;
[GenerateTypedNameReferences]
public sealed partial class VendingMachineMenu : FancyWindow
{
[GenerateTypedNameReferences]
public sealed partial class VendingMachineMenu : FancyWindow
[Dependency] private IPrototypeManager _prototypeManager = default!;
[Dependency] private 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();
/// <summary>
/// Whether the vending machine is able to be interacted with or not.
/// </summary>
private bool _enabled;
public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
public VendingMachineMenu()
{
[Dependency] private IPrototypeManager _prototypeManager = default!;
[Dependency] private IEntityManager _entityManager = default!;
MinSize = SetSize = new Vector2(250, 150);
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
private readonly Dictionary<EntProtoId, EntityUid> _dummies = [];
private readonly Dictionary<EntProtoId, (ListContainerButton Button, VendingMachineItem Item)> _listItems = new();
private readonly Dictionary<EntProtoId, uint> _amounts = new();
VendingContents.SearchBar = SearchBar;
VendingContents.DataFilterCondition += DataFilterCondition;
VendingContents.GenerateItem += GenerateButton;
VendingContents.ItemKeyBindDown += (args, data) => OnItemSelected?.Invoke(args, data);
}
/// <summary>
/// Whether the vending machine is able to be interacted with or not.
/// </summary>
private bool _enabled;
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
// Don't clean up dummies during disposal or we'll just have to spawn them again
if (!disposing)
return;
public VendingMachineMenu()
// Delete any dummy items we spawned
foreach (var entity in _dummies.Values)
{
MinSize = SetSize = new Vector2(250, 150);
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
VendingContents.SearchBar = SearchBar;
VendingContents.DataFilterCondition += DataFilterCondition;
VendingContents.GenerateItem += GenerateButton;
VendingContents.ItemKeyBindDown += (args, data) => OnItemSelected?.Invoke(args, data);
_entityManager.QueueDeleteEntity(entity);
}
_dummies.Clear();
}
protected override void Dispose(bool disposing)
private bool DataFilterCondition(string filter, ListData data)
{
if (data is not VendorItemsListData { ItemText: var text })
return false;
if (string.IsNullOrEmpty(filter))
return true;
return text.Contains(filter, StringComparison.CurrentCultureIgnoreCase);
}
private void GenerateButton(ListData data, ListContainerButton button)
{
if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text })
return;
var item = new VendingMachineItem(protoID, text);
_listItems[protoID] = (button, item);
button.AddChild(item);
button.AddStyleClass(StyleClass.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, bool enabled)
{
_enabled = enabled;
_listItems.Clear();
_amounts.Clear();
if (inventory.Count == 0 && VendingContents.Visible)
{
base.Dispose(disposing);
SearchBar.Visible = false;
VendingContents.Visible = false;
// Don't clean up dummies during disposal or we'll just have to spawn them again
if (!disposing)
return;
// Delete any dummy items we spawned
foreach (var entity in _dummies.Values)
var outOfStockLabel = new Label
{
_entityManager.QueueDeleteEntity(entity);
}
_dummies.Clear();
Text = Loc.GetString("vending-machine-component-try-eject-out-of-stock"),
Margin = new Thickness(4, 4),
HorizontalExpand = true,
VerticalAlignment = VAlignment.Stretch,
HorizontalAlignment = HAlignment.Center
};
MainContainer.AddChild(outOfStockLabel);
SetSizeAfterUpdate(outOfStockLabel.Text.Length, 0);
return;
}
private bool DataFilterCondition(string filter, ListData data)
var longestEntry = string.Empty;
var listData = new List<VendorItemsListData>();
for (var i = 0; i < inventory.Count; i++)
{
if (data is not VendorItemsListData { ItemText: var text })
return false;
var entry = inventory[i];
if (string.IsNullOrEmpty(filter))
return true;
return text.Contains(filter, StringComparison.CurrentCultureIgnoreCase);
}
private void GenerateButton(ListData data, ListContainerButton button)
{
if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text })
return;
var item = new VendingMachineItem(protoID, text);
_listItems[protoID] = (button, item);
button.AddChild(item);
button.AddStyleClass(StyleClass.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, bool enabled)
{
_enabled = enabled;
_listItems.Clear();
_amounts.Clear();
if (inventory.Count == 0 && VendingContents.Visible)
if (!_prototypeManager.Resolve(entry.ID, out var prototype))
{
SearchBar.Visible = false;
VendingContents.Visible = false;
var outOfStockLabel = new Label()
{
Text = Loc.GetString("vending-machine-component-try-eject-out-of-stock"),
Margin = new Thickness(4, 4),
HorizontalExpand = true,
VerticalAlignment = VAlignment.Stretch,
HorizontalAlignment = HAlignment.Center
};
MainContainer.AddChild(outOfStockLabel);
SetSizeAfterUpdate(outOfStockLabel.Text.Length, 0);
return;
_amounts[entry.ID] = 0;
continue;
}
var longestEntry = string.Empty;
var listData = new List<VendorItemsListData>();
for (var i = 0; i < inventory.Count; i++)
if (!_dummies.TryGetValue(entry.ID, out var dummy))
{
var entry = inventory[i];
if (!_prototypeManager.Resolve(entry.ID, out var prototype))
{
_amounts[entry.ID] = 0;
continue;
}
if (!_dummies.TryGetValue(entry.ID, out var dummy))
{
dummy = _entityManager.Spawn(entry.ID);
_dummies.Add(entry.ID, 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, i)
{
ItemText = itemText,
});
dummy = _entityManager.Spawn(entry.ID);
_dummies.Add(entry.ID, dummy);
}
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];
if (!cachedInventory.TryFirstOrDefault(o => o.ID == proto, out var entry))
continue;
var amount = entry.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}]";
var itemText = $"{itemName} [{entry.Amount}]";
_amounts[entry.ID] = entry.Amount;
if (itemText.Length > longestEntry.Length)
longestEntry = itemText;
listData.Add(new VendorItemsListData(prototype.ID, i)
{
ItemText = itemText,
});
}
private void SetSizeAfterUpdate(int longestEntryLength, int contentCount)
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)
{
SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400),
Math.Clamp(contentCount * 50, 150, 350));
if (!_listItems.TryGetValue(proto, out var button))
continue;
var dummy = _dummies[proto];
if (!cachedInventory.TryFirstOrDefault(o => o.ID == proto, out var entry))
continue;
var amount = entry.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;
}
}
public record VendorItemsListData(EntProtoId ItemProtoID, int ItemIndex) : ListData
private string GetItemText(EntityUid dummy, uint amount)
{
public string ItemText = string.Empty;
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, int ItemIndex) : ListData
{
public string ItemText = string.Empty;
}

View file

@ -5,80 +5,75 @@ using Robust.Client.UserInterface;
using Robust.Shared.Input;
using System.Linq;
namespace Content.Client.VendingMachines
namespace Content.Client.VendingMachines;
public sealed class VendingMachineBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
public sealed class VendingMachineBoundUserInterface : BoundUserInterface
[ViewVariables]
private VendingMachineMenu? _menu;
[ViewVariables]
private List<VendingMachineInventoryEntry> _cachedInventory = new();
protected override void Open()
{
[ViewVariables]
private VendingMachineMenu? _menu;
base.Open();
[ViewVariables]
private List<VendingMachineInventoryEntry> _cachedInventory = new();
_menu = this.CreateWindowCenteredLeft<VendingMachineMenu>();
_menu.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
_menu.OnItemSelected += OnItemSelected;
Refresh();
}
public VendingMachineBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
public void Refresh()
{
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
protected override void Open()
{
base.Open();
var system = EntMan.System<VendingMachineSystem>();
_cachedInventory = system.GetAllInventory(Owner);
_menu = this.CreateWindowCenteredLeft<VendingMachineMenu>();
_menu.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
_menu.OnItemSelected += OnItemSelected;
Refresh();
}
_menu?.Populate(_cachedInventory, enabled);
}
public void Refresh()
{
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
public void UpdateAmounts()
{
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
var system = EntMan.System<VendingMachineSystem>();
_cachedInventory = system.GetAllInventory(Owner);
var system = EntMan.System<VendingMachineSystem>();
_cachedInventory = system.GetAllInventory(Owner);
_menu?.UpdateAmounts(_cachedInventory, enabled);
}
_menu?.Populate(_cachedInventory, enabled);
}
private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
{
if (args.Function != EngineKeyFunctions.UIClick)
return;
public void UpdateAmounts()
{
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
if (data is not VendorItemsListData { ItemIndex: var itemIndex })
return;
var system = EntMan.System<VendingMachineSystem>();
_cachedInventory = system.GetAllInventory(Owner);
_menu?.UpdateAmounts(_cachedInventory, enabled);
}
if (_cachedInventory.Count == 0)
return;
private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
{
if (args.Function != EngineKeyFunctions.UIClick)
return;
var selectedItem = _cachedInventory.ElementAtOrDefault(itemIndex);
if (data is not VendorItemsListData { ItemIndex: var itemIndex })
return;
if (selectedItem == null)
return;
if (_cachedInventory.Count == 0)
return;
SendPredictedMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
}
var selectedItem = _cachedInventory.ElementAtOrDefault(itemIndex);
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
if (selectedItem == null)
return;
if (_menu == null)
return;
SendPredictedMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
if (_menu == null)
return;
_menu.OnItemSelected -= OnItemSelected;
_menu.OnClose -= Close;
_menu.Dispose();
}
_menu.OnItemSelected -= OnItemSelected;
_menu.OnClose -= Close;
_menu.Dispose();
}
}

View file

@ -12,15 +12,7 @@ public sealed partial class VendingMachineSystem : SharedVendingMachineSystem
[Dependency] private SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<VendingMachineComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<VendingMachineComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<VendingMachineComponent, ComponentHandleState>(OnVendingHandleState);
}
[SubscribeLocalEvent]
private void OnVendingHandleState(Entity<VendingMachineComponent> entity, ref ComponentHandleState args)
{
if (args.Current is not VendingMachineComponentState state)
@ -85,6 +77,7 @@ public sealed partial class VendingMachineSystem : SharedVendingMachineSystem
}
}
[SubscribeLocalEvent]
private void OnAnimationCompleted(EntityUid uid, VendingMachineComponent component, AnimationCompletedEvent args)
{
if (!TryComp<SpriteComponent>(uid, out var sprite))
@ -99,6 +92,7 @@ public sealed partial class VendingMachineSystem : SharedVendingMachineSystem
UpdateAppearance(uid, visualState, component, sprite);
}
[SubscribeLocalEvent]
private void OnAppearanceChange(EntityUid uid, VendingMachineComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)

View file

@ -14,7 +14,6 @@ using Content.Shared.Storage.EntitySystems;
using Content.Shared.VendingMachines;
using Content.Shared.Wires;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests

View file

@ -11,8 +11,8 @@ public sealed partial class VendingMachineContrabandWireAction : BaseToggleWireA
public override Color Color { get; set; } = Color.Green;
public override string Name { get; set; } = "wire-name-vending-contraband";
public override object? StatusKey { get; } = ContrabandWireKey.StatusKey;
public override object? TimeoutKey { get; } = ContrabandWireKey.TimeoutKey;
public override object StatusKey => ContrabandWireKey.StatusKey;
public override object TimeoutKey => ContrabandWireKey.TimeoutKey;
public override void Initialize()
{

View file

@ -11,7 +11,7 @@ public sealed partial class VendingMachineEjectItemWireAction : ComponentWireAct
public override Color Color { get; set; } = Color.Red;
public override string Name { get; set; } = "wire-name-vending-eject";
public override object? StatusKey { get; } = EjectWireKey.StatusKey;
public override object StatusKey => EjectWireKey.StatusKey;
public override StatusLightState? GetLightState(Wire wire, VendingMachineComponent comp)
=> comp.CanShoot ? StatusLightState.BlinkingFast : StatusLightState.On;

View file

@ -13,235 +13,226 @@ using Content.Shared.Wall;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.VendingMachines
namespace Content.Server.VendingMachines;
public sealed partial class VendingMachineSystem : SharedVendingMachineSystem
{
public sealed partial class VendingMachineSystem : SharedVendingMachineSystem
[Dependency] private IRobustRandom _random = default!;
[Dependency] private PricingSystem _pricing = default!;
[Dependency] private ThrowingSystem _throwingSystem = default!;
private const float WallVendEjectDistanceFromWall = 1f;
[SubscribeLocalEvent]
private void OnVendingPrice(EntityUid uid, VendingMachineComponent component, ref PriceCalculationEvent args)
{
[Dependency] private IRobustRandom _random = default!;
[Dependency] private PricingSystem _pricing = default!;
[Dependency] private ThrowingSystem _throwingSystem = default!;
var price = 0.0;
private const float WallVendEjectDistanceFromWall = 1f;
public override void Initialize()
foreach (var entry in component.Inventory.Values)
{
base.Initialize();
SubscribeLocalEvent<VendingMachineComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<VendingMachineComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<VendingMachineComponent, PriceCalculationEvent>(OnVendingPrice);
SubscribeLocalEvent<VendingMachineComponent, TryVocalizeEvent>(OnTryVocalize);
SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense);
SubscribeLocalEvent<VendingMachineRestockComponent, PriceCalculationEvent>(OnPriceCalculation);
}
private void OnVendingPrice(EntityUid uid, VendingMachineComponent component, ref PriceCalculationEvent args)
{
var price = 0.0;
foreach (var entry in component.Inventory.Values)
if (!ProtoMan.TryIndex<EntityPrototype>(entry.ID, out var proto))
{
if (!ProtoMan.TryIndex<EntityPrototype>(entry.ID, out var proto))
{
Log.Error($"Unable to find entity prototype {entry.ID} on {ToPrettyString(uid)} vending.");
continue;
}
price += entry.Amount * _pricing.GetEstimatedPrice(proto);
Log.Error($"Unable to find entity prototype {entry.ID} on {ToPrettyString(uid)} vending.");
continue;
}
args.Price += price;
price += entry.Amount * _pricing.GetEstimatedPrice(proto);
}
protected override void OnMapInit(EntityUid uid, VendingMachineComponent component, MapInitEvent args)
{
base.OnMapInit(uid, component, args);
args.Price += price;
}
if (HasComp<ApcPowerReceiverComponent>(uid))
{
TryUpdateVisualState((uid, component));
}
}
protected override void OnMapInit(EntityUid uid, VendingMachineComponent component, MapInitEvent args)
{
base.OnMapInit(uid, component, args);
private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, ref PowerChangedEvent args)
if (HasComp<ApcPowerReceiverComponent>(uid))
{
TryUpdateVisualState((uid, component));
}
}
private void OnDamageChanged(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args)
[SubscribeLocalEvent]
private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, ref PowerChangedEvent args)
{
TryUpdateVisualState((uid, component));
}
[SubscribeLocalEvent]
private void OnDamageChanged(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args)
{
if (!args.DamageIncreased && component.Broken)
{
if (!args.DamageIncreased && component.Broken)
{
component.Broken = false;
Dirty(uid, component);
TryUpdateVisualState((uid, component));
return;
}
if (component.Broken || component.DispenseOnHitCoolingDown ||
component.DispenseOnHitChance == null || args.DamageDelta == null)
return;
if (args.DamageIncreased && args.DamageDelta.GetTotal() >= component.DispenseOnHitThreshold &&
_random.Prob(component.DispenseOnHitChance.Value))
{
if (component.DispenseOnHitCooldown != null)
{
component.DispenseOnHitEnd = Timing.CurTime + component.DispenseOnHitCooldown.Value;
}
EjectRandom(uid, throwItem: true, forceEject: true, component);
}
}
private void OnSelfDispense(EntityUid uid, VendingMachineComponent component, VendingMachineSelfDispenseEvent args)
{
if (args.Handled)
return;
args.Handled = true;
EjectRandom(uid, throwItem: true, forceEject: false, component);
}
/// <summary>
/// Sets the <see cref="VendingMachineComponent.CanShoot"/> property of the vending machine.
/// </summary>
public void SetShooting(EntityUid uid, bool canShoot, VendingMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.CanShoot = canShoot;
}
/// <summary>
/// Sets the <see cref="VendingMachineComponent.Contraband"/> property of the vending machine.
/// </summary>
public void SetContraband(EntityUid uid, bool contraband, VendingMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.Contraband = contraband;
component.Broken = false;
Dirty(uid, component);
TryUpdateVisualState((uid, component));
return;
}
/// <summary>
/// Ejects a random item from the available stock. Will do nothing if the vending machine is empty.
/// </summary>
/// <param name="uid"></param>
/// <param name="throwItem">Whether to throw the item in a random direction after dispensing it.</param>
/// <param name="forceEject">Whether to skip the regular ejection checks and immediately dispense the item without animation.</param>
/// <param name="vendComponent"></param>
public void EjectRandom(EntityUid uid, bool throwItem, bool forceEject = false, VendingMachineComponent? vendComponent = null)
if (component.Broken || component.DispenseOnHitCoolingDown ||
component.DispenseOnHitChance == null || args.DamageDelta == null)
return;
if (args.DamageIncreased && args.DamageDelta.GetTotal() >= component.DispenseOnHitThreshold &&
_random.Prob(component.DispenseOnHitChance.Value))
{
if (!Resolve(uid, ref vendComponent))
return;
var availableItems = GetAvailableInventory(uid, vendComponent);
if (availableItems.Count <= 0)
return;
var item = _random.Pick(availableItems);
if (forceEject)
if (component.DispenseOnHitCooldown != null)
{
vendComponent.NextItemToEject = item.ID;
vendComponent.ThrowNextItem = throwItem;
var entry = GetEntry(uid, item.ID, item.Type, vendComponent);
if (entry != null)
entry.Amount--;
EjectItem(uid, vendComponent, forceEject);
}
else
{
TryEjectVendorItem(uid, item.Type, item.ID, throwItem, user: null, vendComponent: vendComponent);
}
}
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));
if (string.IsNullOrEmpty(vendComponent.NextItemToEject))
{
vendComponent.ThrowNextItem = false;
return;
component.DispenseOnHitEnd = Timing.CurTime + component.DispenseOnHitCooldown.Value;
}
// Default spawn coordinates
var xform = Transform(uid);
var spawnCoordinates = xform.Coordinates;
//Make sure the wallvends spawn outside of the wall.
if (TryComp<WallMountComponent>(uid, out var wallMountComponent))
{
var offset = (wallMountComponent.Direction + xform.LocalRotation - Math.PI / 2).ToVec() * WallVendEjectDistanceFromWall;
spawnCoordinates = spawnCoordinates.Offset(offset);
}
var ent = Spawn(vendComponent.NextItemToEject, spawnCoordinates);
if (vendComponent.ThrowNextItem)
{
var range = vendComponent.NonLimitedEjectRange;
var direction = new Vector2(_random.NextFloat(-range, range), _random.NextFloat(-range, range));
_throwingSystem.TryThrow(ent, direction, vendComponent.NonLimitedEjectForce);
}
vendComponent.NextItemToEject = null;
vendComponent.ThrowNextItem = false;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
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 += (5 * comp.EjectDelay);
}
}
}
private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args)
{
List<double> priceSets = new();
// Find the most expensive inventory and use that as the highest price.
foreach (var vendingInventory in component.CanRestock)
{
double total = 0;
if (ProtoMan.TryIndex(vendingInventory, out VendingMachineInventoryPrototype? inventoryPrototype))
{
foreach (var (item, amount) in inventoryPrototype.StartingInventory)
{
if (ProtoMan.TryIndex(item, out EntityPrototype? entity))
total += _pricing.GetEstimatedPrice(entity) * amount;
}
}
priceSets.Add(total);
}
args.Price += priceSets.Max();
}
private void OnTryVocalize(Entity<VendingMachineComponent> ent, ref TryVocalizeEvent args)
{
args.Cancelled |= ent.Comp.Broken;
EjectRandom(uid, throwItem: true, forceEject: true, component);
}
}
[SubscribeLocalEvent]
private void OnSelfDispense(EntityUid uid, VendingMachineComponent component, VendingMachineSelfDispenseEvent args)
{
if (args.Handled)
return;
args.Handled = true;
EjectRandom(uid, throwItem: true, forceEject: false, component);
}
/// <summary>
/// Sets the <see cref="VendingMachineComponent.CanShoot"/> property of the vending machine.
/// </summary>
public void SetShooting(EntityUid uid, bool canShoot, VendingMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.CanShoot = canShoot;
}
/// <summary>
/// Sets the <see cref="VendingMachineComponent.Contraband"/> property of the vending machine.
/// </summary>
public void SetContraband(EntityUid uid, bool contraband, VendingMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.Contraband = contraband;
Dirty(uid, component);
}
/// <summary>
/// Ejects a random item from the available stock. Will do nothing if the vending machine is empty.
/// </summary>
/// <param name="uid"></param>
/// <param name="throwItem">Whether to throw the item in a random direction after dispensing it.</param>
/// <param name="forceEject">Whether to skip the regular ejection checks and immediately dispense the item without animation.</param>
/// <param name="vendComponent"></param>
public void EjectRandom(EntityUid uid, bool throwItem, bool forceEject = false, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
var availableItems = GetAvailableInventory(uid, vendComponent);
if (availableItems.Count <= 0)
return;
var item = _random.Pick(availableItems);
if (forceEject)
{
vendComponent.NextItemToEject = item.ID;
vendComponent.ThrowNextItem = throwItem;
var entry = GetEntry(uid, item.ID, item.Type, vendComponent);
if (entry != null)
entry.Amount--;
EjectItem(uid, vendComponent, forceEject);
}
else
{
TryEjectVendorItem(uid, item.Type, item.ID, throwItem, user: null, vendComponent: vendComponent);
}
}
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));
if (string.IsNullOrEmpty(vendComponent.NextItemToEject))
{
vendComponent.ThrowNextItem = false;
return;
}
// Default spawn coordinates
var xform = Transform(uid);
var spawnCoordinates = xform.Coordinates;
//Make sure the wallvends spawn outside of the wall.
if (TryComp<WallMountComponent>(uid, out var wallMountComponent))
{
var offset = (wallMountComponent.Direction + xform.LocalRotation - Math.PI / 2).ToVec() * WallVendEjectDistanceFromWall;
spawnCoordinates = spawnCoordinates.Offset(offset);
}
var ent = Spawn(vendComponent.NextItemToEject, spawnCoordinates);
if (vendComponent.ThrowNextItem)
{
var range = vendComponent.NonLimitedEjectRange;
var direction = new Vector2(_random.NextFloat(-range, range), _random.NextFloat(-range, range));
_throwingSystem.TryThrow(ent, direction, vendComponent.NonLimitedEjectForce);
}
vendComponent.NextItemToEject = null;
vendComponent.ThrowNextItem = false;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
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 += (5 * comp.EjectDelay);
}
}
}
[SubscribeLocalEvent]
private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args)
{
List<double> priceSets = new();
// Find the most expensive inventory and use that as the highest price.
foreach (var vendingInventory in component.CanRestock)
{
double total = 0;
if (ProtoMan.TryIndex(vendingInventory, out VendingMachineInventoryPrototype? inventoryPrototype))
{
foreach (var (item, amount) in inventoryPrototype.StartingInventory)
{
if (ProtoMan.TryIndex(item, out EntityPrototype? entity))
total += _pricing.GetEstimatedPrice(entity) * amount;
}
}
priceSets.Add(total);
}
args.Price += priceSets.Max();
}
[SubscribeLocalEvent]
private void OnTryVocalize(Entity<VendingMachineComponent> ent, ref TryVocalizeEvent args)
{
args.Cancelled |= ent.Comp.Broken;
}
}

View file

@ -56,6 +56,7 @@ public abstract partial class SharedVendingMachineSystem
TryUpdateVisualState((uid, vendComponent));
}
[SubscribeLocalEvent]
private void OnAfterInteract(EntityUid uid, VendingMachineRestockComponent component, AfterInteractEvent args)
{
if (args.Target is not { } target || !args.CanReach || args.Handled)
@ -102,6 +103,7 @@ public abstract partial class SharedVendingMachineSystem
machineComponent.RestockStream = Audio.PlayPredicted(component.SoundRestockStart, target, args.User)?.Entity;
}
[SubscribeLocalEvent]
private void OnRestockDoAfter(Entity<VendingMachineComponent> ent, ref RestockDoAfterEvent args)
{
if (args.Cancelled)

View file

@ -8,7 +8,6 @@ using Content.Shared.DoAfter;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Emp;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Power.EntitySystems;
using Content.Shared.UserInterface;
@ -39,15 +38,6 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<VendingMachineComponent, ComponentGetState>(OnVendingGetState);
SubscribeLocalEvent<VendingMachineComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<VendingMachineComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<VendingMachineComponent, RestockDoAfterEvent>(OnRestockDoAfter);
SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt);
SubscribeLocalEvent<VendingMachineComponent, BreakageEventArgs>(OnBreak);
SubscribeLocalEvent<VendingMachineRestockComponent, AfterInteractEvent>(OnAfterInteract);
Subs.BuiEvents<VendingMachineComponent>(VendingMachineUiKey.Key, subs =>
{
@ -55,6 +45,7 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
});
}
[SubscribeLocalEvent]
private void OnVendingGetState(Entity<VendingMachineComponent> entity, ref ComponentGetState args)
{
var component = entity.Comp;
@ -145,11 +136,13 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
AuthorizedVend(entity.Owner, actor, args.Type, args.ID, entity.Comp);
}
[SubscribeLocalEvent]
protected virtual void OnMapInit(EntityUid uid, VendingMachineComponent component, MapInitEvent args)
{
RestockInventoryFromPrototype(uid, component, component.InitialStockQuality);
}
[SubscribeLocalEvent]
private void OnEmpPulse(Entity<VendingMachineComponent> ent, ref EmpPulseEvent args)
{
if (!ent.Comp.Broken && _receiver.IsPowered(ent.Owner))
@ -206,6 +199,7 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
/// <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="user"></param>
/// <param name="vendComponent"></param>
public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, EntityUid? user = null, VendingMachineComponent? vendComponent = null)
{
@ -333,6 +327,7 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
Dirty(uid, component);
}
[SubscribeLocalEvent]
private void OnEmagged(EntityUid uid, VendingMachineComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
@ -429,12 +424,14 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
}
}
[SubscribeLocalEvent]
private void OnActivatableUIOpenAttempt(EntityUid uid, VendingMachineComponent component, ActivatableUIOpenAttemptEvent args)
{
if (component.Broken)
args.Cancel();
}
[SubscribeLocalEvent]
private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs)
{
vendComponent.Broken = true;

View file

@ -5,303 +5,299 @@ using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.VendingMachines
namespace Content.Shared.VendingMachines;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentPause]
public sealed partial class VendingMachineComponent : Component
{
[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;
/// <summary>
/// Used by the server to determine how long the vending machine stays in the "Deny" state.
/// Used by the client to determine how long the deny animation should be played.
/// </summary>
[DataField]
public TimeSpan DenyDelay = TimeSpan.FromSeconds(2);
/// <summary>
/// Used by the server to determine how long the vending machine stays in the "Eject" state.
/// The selected item is dispensed afer this delay.
/// Used by the client to determine how long the deny animation should be played.
/// </summary>
[DataField]
public TimeSpan EjectDelay = TimeSpan.FromSeconds(1.2);
[DataField]
public Dictionary<string, VendingMachineInventoryEntry> Inventory = new();
[DataField]
public Dictionary<string, VendingMachineInventoryEntry> EmaggedInventory = new();
[DataField]
public Dictionary<string, VendingMachineInventoryEntry> ContrabandInventory = new();
/// <summary>
/// If true then unlocks the <see cref="ContrabandInventory"/>
/// </summary>
[DataField]
public bool Contraband;
[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;
[DataField]
public bool Broken;
/// <summary>
/// When true, will forcefully throw any object it dispenses
/// </summary>
[DataField]
public bool CanShoot = false;
public bool ThrowNextItem = false;
/// <summary>
/// The chance that a vending machine will randomly dispense an item on hit.
/// Chance is 0 if null.
/// </summary>
[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]
public float? DispenseOnHitThreshold;
/// <summary>
/// Amount of time in seconds that need to pass before damage can cause a vending machine to eject again.
/// This value is separate to <see cref="VendingMachineComponent.EjectDelay"/> because that value might be
/// 0 for a vending machine for legitimate reasons (no desired delay/no eject animation)
/// and can be circumvented with forced ejections.
/// </summary>
[DataField]
public TimeSpan? DispenseOnHitCooldown = TimeSpan.FromSeconds(1.0);
/// <summary>
/// Sound that plays when ejecting an item
/// </summary>
[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>
/// 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;
/// <summary>
/// Used by the server to determine how long the vending machine stays in the "Deny" state.
/// Used by the client to determine how long the deny animation should be played.
/// </summary>
[DataField]
public TimeSpan DenyDelay = TimeSpan.FromSeconds(2);
/// <summary>
/// Used by the server to determine how long the vending machine stays in the "Eject" state.
/// The selected item is dispensed afer this delay.
/// Used by the client to determine how long the deny animation should be played.
/// </summary>
[DataField]
public TimeSpan EjectDelay = TimeSpan.FromSeconds(1.2);
[DataField]
public Dictionary<string, VendingMachineInventoryEntry> Inventory = new();
[DataField]
public Dictionary<string, VendingMachineInventoryEntry> EmaggedInventory = new();
[DataField]
public Dictionary<string, VendingMachineInventoryEntry> ContrabandInventory = new();
/// <summary>
/// If true then unlocks the <see cref="ContrabandInventory"/>
/// </summary>
[DataField]
public bool Contraband;
[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;
[DataField]
public bool Broken;
/// <summary>
/// When true, will forcefully throw any object it dispenses
/// </summary>
[DataField]
public bool CanShoot = false;
public bool ThrowNextItem = false;
/// <summary>
/// The chance that a vending machine will randomly dispense an item on hit.
/// Chance is 0 if null.
/// </summary>
[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]
public float? DispenseOnHitThreshold;
/// <summary>
/// Amount of time in seconds that need to pass before damage can cause a vending machine to eject again.
/// This value is separate to <see cref="VendingMachineComponent.EjectDelay"/> because that value might be
/// 0 for a vending machine for legitimate reasons (no desired delay/no eject animation)
/// and can be circumvented with forced ejections.
/// </summary>
[DataField]
public TimeSpan? DispenseOnHitCooldown = TimeSpan.FromSeconds(1.0);
/// <summary>
/// Sound that plays when ejecting an item
/// </summary>
[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")
Params = new AudioParams
{
Params = new AudioParams
{
Volume = -4f,
Variation = 0.15f
}
};
/// <summary>
/// Sound that plays when an item can't be ejected
/// </summary>
[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 NonLimitedEjectForce = 7.5f;
public float NonLimitedEjectRange = 5f;
/// <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.
/// If not fully stocked, the stock will have a random value between 0 (inclusive) and max stock (exclusive).
/// </summary>
[DataField]
public float InitialStockQuality = 1.0f;
/// <summary>
/// While disabled by EMP it randomly ejects items
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextEmpEject = TimeSpan.Zero;
/// <summary>
/// Audio entity used during restock in case the doafter gets canceled.
/// </summary>
[DataField]
public EntityUid? RestockStream;
#region Client Visuals
/// <summary>
/// RSI state for when the vending machine is unpowered.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Base"/>
/// </summary>
[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]
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]
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]
public string? EjectState;
/// <summary>
/// RSI state for the vending machine's deny animation. Will either be played once as sprite flick
/// or looped depending on how <see cref="LoopDenyAnimation"/> is set.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
/// </summary>
[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]
public string? BrokenState;
/// <summary>
/// If set to <c>true</c> (default) will loop the animation of the <see cref="DenyState"/> for the duration
/// of <see cref="VendingMachineComponent.DenyDelay"/>. If set to <c>false</c> will play a sprite
/// flick animation for the state and then linger on the final frame until the end of the delay.
/// </summary>
[DataField("loopDeny")]
public bool LoopDenyAnimation = true;
#endregion
}
[Serializable, NetSerializable, DataDefinition]
public sealed partial class VendingMachineInventoryEntry
{
[DataField]
public InventoryType Type;
[DataField]
public string ID;
[DataField]
public uint Amount;
public VendingMachineInventoryEntry(InventoryType type, string id, uint amount)
{
Type = type;
ID = id;
Amount = amount;
Volume = -4f,
Variation = 0.15f
}
public VendingMachineInventoryEntry(VendingMachineInventoryEntry entry)
{
Type = entry.Type;
ID = entry.ID;
Amount = entry.Amount;
}
}
[Serializable, NetSerializable]
public enum InventoryType : byte
{
Regular,
Emagged,
Contraband
}
[Serializable, NetSerializable]
public enum VendingMachineVisuals : byte
{
VisualState
}
[Serializable, NetSerializable]
public enum VendingMachineVisualState : byte
{
Normal,
Off,
Broken,
Eject,
Deny,
}
public enum VendingMachineVisualLayers : byte
{
/// <summary>
/// Off / Broken. The other layers will overlay this if the machine is on.
/// </summary>
Base,
/// <summary>
/// Normal / Deny / Eject
/// </summary>
BaseUnshaded,
/// <summary>
/// Screens that are persistent (where the machine is not off or broken)
/// </summary>
Screen
}
[Serializable, NetSerializable]
public enum ContrabandWireKey : byte
{
StatusKey,
TimeoutKey
}
[Serializable, NetSerializable]
public enum EjectWireKey : byte
{
StatusKey,
}
public sealed partial class VendingMachineSelfDispenseEvent : InstantActionEvent
{
};
[Serializable, NetSerializable]
public sealed class VendingMachineComponentState : ComponentState
/// <summary>
/// Sound that plays when an item can't be ejected
/// </summary>
[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 NonLimitedEjectForce = 7.5f;
public float NonLimitedEjectRange = 5f;
/// <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.
/// If not fully stocked, the stock will have a random value between 0 (inclusive) and max stock (exclusive).
/// </summary>
[DataField]
public float InitialStockQuality = 1.0f;
/// <summary>
/// While disabled by EMP it randomly ejects items
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextEmpEject = TimeSpan.Zero;
/// <summary>
/// Audio entity used during restock in case the doafter gets canceled.
/// </summary>
[DataField]
public EntityUid? RestockStream;
#region Client Visuals
/// <summary>
/// RSI state for when the vending machine is unpowered.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Base"/>
/// </summary>
[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]
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]
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]
public string? EjectState;
/// <summary>
/// RSI state for the vending machine's deny animation. Will either be played once as sprite flick
/// or looped depending on how <see cref="LoopDenyAnimation"/> is set.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
/// </summary>
[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]
public string? BrokenState;
/// <summary>
/// If set to <c>true</c> (default) will loop the animation of the <see cref="DenyState"/> for the duration
/// of <see cref="VendingMachineComponent.DenyDelay"/>. If set to <c>false</c> will play a sprite
/// flick animation for the state and then linger on the final frame until the end of the delay.
/// </summary>
[DataField("loopDeny")]
public bool LoopDenyAnimation = true;
#endregion
}
[Serializable, NetSerializable, DataDefinition]
public sealed partial class VendingMachineInventoryEntry
{
[DataField]
public InventoryType Type;
[DataField]
public string ID;
[DataField]
public uint Amount;
public VendingMachineInventoryEntry(InventoryType type, string id, uint amount)
{
public Dictionary<string, VendingMachineInventoryEntry> Inventory = new();
Type = type;
ID = id;
Amount = amount;
}
public Dictionary<string, VendingMachineInventoryEntry> EmaggedInventory = new();
public Dictionary<string, VendingMachineInventoryEntry> ContrabandInventory = new();
public bool Contraband;
public TimeSpan? EjectEnd;
public TimeSpan? DenyEnd;
public TimeSpan? DispenseOnHitEnd;
public bool Broken;
public VendingMachineInventoryEntry(VendingMachineInventoryEntry entry)
{
Type = entry.Type;
ID = entry.ID;
Amount = entry.Amount;
}
}
[Serializable, NetSerializable]
public enum InventoryType : byte
{
Regular,
Emagged,
Contraband
}
[Serializable, NetSerializable]
public enum VendingMachineVisuals : byte
{
VisualState
}
[Serializable, NetSerializable]
public enum VendingMachineVisualState : byte
{
Normal,
Off,
Broken,
Eject,
Deny
}
public enum VendingMachineVisualLayers : byte
{
/// <summary>
/// Off / Broken. The other layers will overlay this if the machine is on.
/// </summary>
Base,
/// <summary>
/// Normal / Deny / Eject
/// </summary>
BaseUnshaded,
/// <summary>
/// Screens that are persistent (where the machine is not off or broken)
/// </summary>
Screen
}
[Serializable, NetSerializable]
public enum ContrabandWireKey : byte
{
StatusKey,
TimeoutKey
}
[Serializable, NetSerializable]
public enum EjectWireKey : byte
{
StatusKey
}
public sealed partial class VendingMachineSelfDispenseEvent : InstantActionEvent;
[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;
public bool Broken;
}

View file

@ -1,22 +1,16 @@
using Robust.Shared.Serialization;
namespace Content.Shared.VendingMachines
{
[Serializable, NetSerializable]
public sealed class VendingMachineEjectMessage : BoundUserInterfaceMessage
{
public readonly InventoryType Type;
public readonly string ID;
public VendingMachineEjectMessage(InventoryType type, string id)
{
Type = type;
ID = id;
}
}
namespace Content.Shared.VendingMachines;
[Serializable, NetSerializable]
public enum VendingMachineUiKey
{
Key,
}
[Serializable, NetSerializable]
public sealed class VendingMachineEjectMessage(InventoryType type, string id) : BoundUserInterfaceMessage
{
public readonly InventoryType Type = type;
public readonly string ID = id;
}
[Serializable, NetSerializable]
public enum VendingMachineUiKey
{
Key
}

View file

@ -1,23 +1,21 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Shared.VendingMachines
namespace Content.Shared.VendingMachines;
[Prototype]
public sealed partial class VendingMachineInventoryPrototype : IPrototype
{
[Prototype]
public sealed partial class VendingMachineInventoryPrototype : IPrototype
{
[ViewVariables]
[IdDataField]
public string ID { get; private set; } = default!;
[ViewVariables]
[IdDataField]
public string ID { get; private set; } = default!;
[DataField("startingInventory", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<uint, EntityPrototype>))]
public Dictionary<string, uint> StartingInventory { get; private set; } = new();
[DataField(customTypeSerializer:typeof(PrototypeIdDictionarySerializer<uint, EntityPrototype>))]
public Dictionary<string, uint> StartingInventory { get; private set; } = new();
[DataField("emaggedInventory", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<uint, EntityPrototype>))]
public Dictionary<string, uint>? EmaggedInventory { get; private set; }
[DataField(customTypeSerializer:typeof(PrototypeIdDictionarySerializer<uint, EntityPrototype>))]
public Dictionary<string, uint>? EmaggedInventory { get; private set; }
[DataField("contrabandInventory", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<uint, EntityPrototype>))]
public Dictionary<string, uint>? ContrabandInventory { get; private set; }
}
[DataField(customTypeSerializer:typeof(PrototypeIdDictionarySerializer<uint, EntityPrototype>))]
public Dictionary<string, uint>? ContrabandInventory { get; private set; }
}