Clean up vending machines and port their visualizer (#10465)

This commit is contained in:
Andreas Kämper
2022-08-31 14:12:09 +02:00
committed by GitHub
parent 6b0e03e0d7
commit 42f3155c85
62 changed files with 873 additions and 850 deletions

View File

@@ -1,62 +1,87 @@
using System;
using System.Collections.Generic;
using Content.Shared.VendingMachines;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using static Content.Shared.VendingMachines.SharedVendingMachineComponent;
namespace Content.Client.VendingMachines.UI
{
[GenerateTypedNameReferences]
public sealed partial class VendingMachineMenu : DefaultWindow
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private VendingMachineBoundUserInterface Owner { get; }
public event Action<ItemList.ItemListSelectedEventArgs>? OnItemSelected;
private List<VendingMachineInventoryEntry> _cachedInventory = new();
public VendingMachineMenu(VendingMachineBoundUserInterface owner)
public VendingMachineMenu()
{
IoCManager.InjectDependencies(this);
MinSize = SetSize = (250, 150);
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
Owner = owner;
VendingContents.OnItemSelected += ItemSelected;
VendingContents.OnItemSelected += args =>
{
OnItemSelected?.Invoke(args);
};
}
/// <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)
{
VendingContents.Clear();
_cachedInventory = inventory;
var longestEntry = "";
foreach (VendingMachineInventoryEntry entry in inventory)
if (inventory.Count == 0)
{
var itemName = _prototypeManager.Index<EntityPrototype>(entry.ID).Name;
VendingContents.Clear();
var outOfStockText = Loc.GetString("vending-machine-component-try-eject-out-of-stock");
VendingContents.AddItem(outOfStockText);
SetSizeAfterUpdate(outOfStockText.Length);
return;
}
while (inventory.Count != VendingContents.Count)
{
if (inventory.Count > VendingContents.Count)
VendingContents.AddItem(string.Empty);
else
VendingContents.RemoveAt(VendingContents.Count - 1);
}
var longestEntry = string.Empty;
var spriteSystem = EntitySystem.Get<SpriteSystem>();
for (var i = 0; i < inventory.Count; i++)
{
var entry = inventory[i];
var vendingItem = VendingContents[i];
vendingItem.Text = string.Empty;
vendingItem.Icon = null;
var itemName = entry.ID;
Texture? icon = null;
if (_prototypeManager.TryIndex<EntityPrototype>(entry.ID, out var prototype))
{
itemName = prototype.Name;
icon = spriteSystem.GetPrototypeIcon(prototype).Default;
}
if (itemName.Length > longestEntry.Length)
longestEntry = itemName;
Texture? icon = null;
if(_prototypeManager.TryIndex(entry.ID, out EntityPrototype? prototype))
icon = SpriteComponent.GetPrototypeIcon(prototype, _resourceCache).Default;
VendingContents.AddItem($"{itemName} [{entry.Amount}]", icon);
vendingItem.Text = $"{itemName} [{entry.Amount}]";
vendingItem.Icon = icon;
}
SetSize = (Math.Clamp((longestEntry.Length + 2) * 12, 250, 300),
Math.Clamp(VendingContents.Count * 50, 150, 350));
SetSizeAfterUpdate(longestEntry.Length);
}
public void ItemSelected(ItemList.ItemListSelectedEventArgs args)
private void SetSizeAfterUpdate(int longestEntryLength)
{
Owner.Eject(_cachedInventory[args.ItemIndex].Type, _cachedInventory[args.ItemIndex].ID);
SetSize = (Math.Clamp((longestEntryLength + 2) * 12, 250, 300),
Math.Clamp(VendingContents.Count * 50, 150, 350));
}
}
}

View File

@@ -1,241 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Shared.VendingMachines;
using JetBrains.Annotations;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using static Content.Shared.VendingMachines.SharedVendingMachineComponent;
namespace Content.Client.VendingMachines.UI
{
[UsedImplicitly]
public sealed class VendingMachineVisualizer : AppearanceVisualizer, ISerializationHooks
{
// TODO: Should default to off or broken if damaged
//
// TODO: The length of these animations is supposed to be dictated
// by the vending machine's pack prototype's `AnimationDuration`
// but we have no good way of passing that data from the server
// to the client at the moment. Rework Visualizers?
private Dictionary<string, bool> _baseStates = new();
private static readonly Dictionary<string, VendingMachineVisualLayers> LayerMap =
new()
{
{"off", VendingMachineVisualLayers.Unlit},
{"screen", VendingMachineVisualLayers.Screen},
{"normal", VendingMachineVisualLayers.Base},
{"normal-unshaded", VendingMachineVisualLayers.BaseUnshaded},
{"eject", VendingMachineVisualLayers.Base},
{"eject-unshaded", VendingMachineVisualLayers.BaseUnshaded},
{"deny", VendingMachineVisualLayers.Base},
{"deny-unshaded", VendingMachineVisualLayers.BaseUnshaded},
{"broken", VendingMachineVisualLayers.Unlit},
};
[DataField("screen")]
private bool _screen;
[DataField("normal")]
private bool _normal;
[DataField("normalUnshaded")]
private bool _normalUnshaded;
[DataField("eject")]
private bool _eject;
[DataField("ejectUnshaded")]
private bool _ejectUnshaded;
[DataField("deny")]
private bool _deny;
[DataField("denyUnshaded")]
private bool _denyUnshaded;
[DataField("broken")]
private bool _broken;
[DataField("brokenUnshaded")]
private bool _brokenUnshaded;
private readonly Dictionary<string, Animation> _animations = new();
void ISerializationHooks.AfterDeserialization()
{
// Used a dictionary so the yaml can adhere to the style-guide and the texture states can be clear
var states = new Dictionary<string, bool>
{
{"off", true},
{"screen", _screen},
{"normal", _normal},
{"normal-unshaded", _normalUnshaded},
{"eject", _eject},
{"eject-unshaded", _ejectUnshaded},
{"deny", _deny},
{"deny-unshaded", _denyUnshaded},
{"broken", _broken},
{"broken-unshaded", _brokenUnshaded},
};
_baseStates = states;
if (_baseStates["deny"])
{
InitializeAnimation("deny");
}
if (_baseStates["deny-unshaded"])
{
InitializeAnimation("deny-unshaded", true);
}
if (_baseStates["eject"])
{
InitializeAnimation("eject");
}
if (_baseStates["eject-unshaded"])
{
InitializeAnimation("eject-unshaded", true);
}
}
private void InitializeAnimation(string key, bool unshaded = false)
{
_animations.Add(key, new Animation {Length = TimeSpan.FromSeconds(1.2f)});
var flick = new AnimationTrackSpriteFlick();
_animations[key].AnimationTracks.Add(flick);
flick.LayerKey = unshaded ? VendingMachineVisualLayers.BaseUnshaded : VendingMachineVisualLayers.Base;
flick.KeyFrames.Add(new AnimationTrackSpriteFlick.KeyFrame(key, 0f));
}
[Obsolete("Subscribe to your component being initialised instead.")]
public override void InitializeEntity(EntityUid entity)
{
base.InitializeEntity(entity);
IoCManager.Resolve<IEntityManager>().EnsureComponent<AnimationPlayerComponent>(entity);
}
private void HideLayers(ISpriteComponent spriteComponent)
{
foreach (var layer in spriteComponent.AllLayers)
{
layer.Visible = false;
}
spriteComponent.LayerSetVisible(VendingMachineVisualLayers.Unlit, true);
}
[Obsolete("Subscribe to AppearanceChangeEvent instead.")]
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var entMan = IoCManager.Resolve<IEntityManager>();
var sprite = entMan.GetComponent<SpriteComponent>(component.Owner);
// TODO when moving to a system visualizer, re work how this is done
// Currently this only gets called during init, so unless it NEEEDS to be configurable, just make this party of the entity prototype.
if (component.TryGetData(VendingMachineVisuals.Inventory, out string? invId) &&
IoCManager.Resolve<IPrototypeManager>().TryIndex(invId, out VendingMachineInventoryPrototype? prototype) &&
IoCManager.Resolve<IResourceCache>().TryGetResource<RSIResource>(
SharedSpriteComponent.TextureRoot / $"Structures/Machines/VendingMachines/{prototype.SpriteName}.rsi", out var res))
{
sprite.BaseRSI = res.RSI;
}
var animPlayer = entMan.GetComponent<AnimationPlayerComponent>(component.Owner);
if (!component.TryGetData(VendingMachineVisuals.VisualState, out VendingMachineVisualState state))
{
state = VendingMachineVisualState.Normal;
}
// Hide last state
HideLayers(sprite);
ActivateState(sprite, "off");
switch (state)
{
case VendingMachineVisualState.Normal:
ActivateState(sprite, "screen");
ActivateState(sprite, "normal-unshaded");
ActivateState(sprite, "normal");
break;
case VendingMachineVisualState.Off:
break;
case VendingMachineVisualState.Broken:
ActivateState(sprite, "broken-unshaded");
ActivateState(sprite, "broken");
break;
case VendingMachineVisualState.Deny:
ActivateState(sprite, "screen");
ActivateAnimation(sprite, animPlayer, "deny-unshaded");
ActivateAnimation(sprite, animPlayer, "deny");
break;
case VendingMachineVisualState.Eject:
ActivateState(sprite, "screen");
ActivateAnimation(sprite, animPlayer, "eject-unshaded");
ActivateAnimation(sprite, animPlayer, "eject");
break;
default:
throw new ArgumentOutOfRangeException();
}
}
// Helper methods just to avoid all of that hard-to-read-indented code
private void ActivateState(ISpriteComponent spriteComponent, string stateId)
{
// No state for it on the rsi :(
if (!_baseStates[stateId])
{
return;
}
var stateLayer = LayerMap[stateId];
spriteComponent.LayerSetVisible(stateLayer, true);
spriteComponent.LayerSetState(stateLayer, stateId);
}
private void ActivateAnimation(ISpriteComponent spriteComponent, AnimationPlayerComponent animationPlayer, string key)
{
if (!_animations.TryGetValue(key, out var animation))
{
return;
}
if (!animationPlayer.HasRunningAnimation(key))
{
spriteComponent.LayerSetVisible(LayerMap[key], true);
animationPlayer.Play(animation, key);
}
}
public enum VendingMachineVisualLayers : byte
{
// Off / Broken. The other layers will overlay this if the machine is on.
Unlit,
// Normal / Deny / Eject
Base,
BaseUnshaded,
// Screens that are persistent (where the machine is not off or broken)
Screen,
}
}
}

View File

@@ -1,22 +1,20 @@
using Content.Client.VendingMachines.UI;
using Content.Shared.VendingMachines;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables;
using static Content.Shared.VendingMachines.SharedVendingMachineComponent;
using Robust.Client.UserInterface.Controls;
using System.Linq;
namespace Content.Client.VendingMachines
{
public sealed class VendingMachineBoundUserInterface : BoundUserInterface
{
[ViewVariables] private VendingMachineMenu? _menu;
[ViewVariables]
private VendingMachineMenu? _menu;
public SharedVendingMachineComponent? VendingMachine { get; private set; }
private List<VendingMachineInventoryEntry> _cachedInventory = new();
public VendingMachineBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
SendMessage(new InventorySyncRequestMessage());
}
protected override void Open()
@@ -24,33 +22,43 @@ namespace Content.Client.VendingMachines
base.Open();
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent(Owner.Owner, out SharedVendingMachineComponent? vendingMachine))
{
return;
}
var vendingMachineSys = EntitySystem.Get<VendingMachineSystem>();
VendingMachine = vendingMachine;
_cachedInventory = vendingMachineSys.GetAllInventory(Owner.Owner);
_menu = new VendingMachineMenu(this) {Title = entMan.GetComponent<MetaDataComponent>(Owner.Owner).EntityName};
_menu.Populate(VendingMachine.AllInventory);
_menu = new VendingMachineMenu {Title = entMan.GetComponent<MetaDataComponent>(Owner.Owner).EntityName};
_menu.OnClose += Close;
_menu.OnItemSelected += OnItemSelected;
_menu.Populate(_cachedInventory);
_menu.OpenCentered();
}
public void Eject(InventoryType type, string id)
protected override void UpdateState(BoundUserInterfaceState state)
{
SendMessage(new VendingMachineEjectMessage(type, id));
base.UpdateState(state);
if (state is not VendingMachineInterfaceState newState)
return;
_cachedInventory = newState.Inventory;
_menu?.Populate(_cachedInventory);
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
{
switch (message)
{
case VendingMachineInventoryMessage msg:
_menu?.Populate(msg.Inventory);
break;
}
if (_cachedInventory == null || _cachedInventory.Count == 0)
return;
var selectedItem = _cachedInventory.ElementAtOrDefault(args.ItemIndex);
if (selectedItem == null)
return;
SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
}
protected override void Dispose(bool disposing)
@@ -59,7 +67,12 @@ namespace Content.Client.VendingMachines
if (!disposing)
return;
_menu?.Dispose();
if (_menu == null)
return;
_menu.OnItemSelected -= OnItemSelected;
_menu.OnClose -= Close;
_menu.Dispose();
}
}
}

View File

@@ -4,7 +4,57 @@ namespace Content.Client.VendingMachines;
[RegisterComponent]
[ComponentReference(typeof(SharedVendingMachineComponent))]
[Access(typeof(VendingMachineSystem))]
public sealed class VendingMachineComponent : SharedVendingMachineComponent
{
/// <summary>
/// RSI state for when the vending machine is unpowered.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Base"/>
/// </summary>
[DataField("offState")]
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")]
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")]
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")]
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("denyState")]
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")]
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="SharedVendingMachineComponent.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;
}

View File

@@ -1,7 +1,148 @@
using Content.Shared.VendingMachines;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
namespace Content.Client.VendingMachines;
public sealed class VendingMachineSystem : SharedVendingMachineSystem
{
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<VendingMachineComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<VendingMachineComponent, AnimationCompletedEvent>(OnAnimationCompleted);
}
private void OnAnimationCompleted(EntityUid uid, VendingMachineComponent component, AnimationCompletedEvent args)
{
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
UpdateAppearance(uid, VendingMachineVisualState.Normal, component, sprite);
}
private void OnAppearanceChange(EntityUid uid, VendingMachineComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!args.AppearanceData.TryGetValue(VendingMachineVisuals.VisualState, out var visualStateObject) ||
visualStateObject is not VendingMachineVisualState visualState)
{
visualState = VendingMachineVisualState.Normal;
}
UpdateAppearance(uid, visualState, component, args.Sprite);
}
private void UpdateAppearance(EntityUid uid, VendingMachineVisualState visualState, VendingMachineComponent component, SpriteComponent sprite)
{
SetLayerState(VendingMachineVisualLayers.Base, component.OffState, sprite);
switch (visualState)
{
case VendingMachineVisualState.Normal:
SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.NormalState, sprite);
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
break;
case VendingMachineVisualState.Deny:
if (component.LoopDenyAnimation)
SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.DenyState, sprite);
else
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, component.DenyDelay, sprite);
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
break;
case VendingMachineVisualState.Eject:
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, component.EjectDelay, sprite);
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
break;
case VendingMachineVisualState.Broken:
HideLayers(sprite);
SetLayerState(VendingMachineVisualLayers.Base, component.BrokenState, sprite);
break;
case VendingMachineVisualState.Off:
HideLayers(sprite);
break;
}
}
private static void SetLayerState(VendingMachineVisualLayers layer, string? state, SpriteComponent sprite)
{
if (string.IsNullOrEmpty(state))
return;
sprite.LayerSetVisible(layer, true);
sprite.LayerSetAutoAnimated(layer, true);
sprite.LayerSetState(layer, state);
}
private void PlayAnimation(EntityUid uid, VendingMachineVisualLayers layer, string? state, float animationTime, SpriteComponent sprite)
{
if (string.IsNullOrEmpty(state))
return;
if (!_animationPlayer.HasRunningAnimation(uid, state))
{
var animation = GetAnimation(layer, state, animationTime);
sprite.LayerSetVisible(layer, true);
_animationPlayer.Play(uid, animation, state);
}
}
private static Animation GetAnimation(VendingMachineVisualLayers layer, string state, float animationTime)
{
return new Animation
{
Length = TimeSpan.FromSeconds(animationTime),
AnimationTracks =
{
new AnimationTrackSpriteFlick
{
LayerKey = layer,
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(state, 0f)
}
}
}
};
}
private static void HideLayers(SpriteComponent sprite)
{
HideLayer(VendingMachineVisualLayers.BaseUnshaded, sprite);
HideLayer(VendingMachineVisualLayers.Screen, sprite);
}
private static void HideLayer(VendingMachineVisualLayers layer, SpriteComponent sprite)
{
if (!sprite.LayerMapTryGet(layer, out var actualLayer))
return;
sprite.LayerSetVisible(actualLayer, false);
}
}
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
}

View File

@@ -29,7 +29,7 @@ namespace Content.IntegrationTests.Tests
}
catch (UnknownPrototypeException)
{
throw new UnknownPrototypeException($"Unknown prototype {item} on vending inventory {vendorProto.Name}");
throw new UnknownPrototypeException($"Unknown prototype {item} on vending inventory {vendorProto.ID}");
}
}
}

View File

@@ -1,6 +1,4 @@
using Content.Server.VendingMachines;
using Content.Shared.Throwing;
using Robust.Shared.Random;
namespace Content.Server.Destructible.Thresholds.Behaviors
{
@@ -30,17 +28,15 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
!system.EntityManager.TryGetComponent<TransformComponent>(owner, out var xform))
return;
var throwingsys = system.EntityManager.EntitySysManager.GetEntitySystem<ThrowingSystem>();
var totalItems = vendingcomp.AllInventory.Count;
var vendingMachineSystem = EntitySystem.Get<VendingMachineSystem>();
var inventory = vendingMachineSystem.GetAvailableInventory(owner, vendingcomp);
if (inventory.Count <= 0)
return;
var toEject = Math.Min(totalItems * Percent, Max);
var toEject = Math.Min(inventory.Count * Percent, Max);
for (var i = 0; i < toEject; i++)
{
var entity = system.EntityManager.SpawnEntity(system.Random.PickAndTake(vendingcomp.AllInventory).ID, xform.Coordinates);
float range = vendingcomp.NonLimitedEjectRange;
Vector2 direction = new Vector2(system.Random.NextFloat(-range, range), system.Random.NextFloat(-range, range));
throwingsys.TryThrow(entity, direction, vendingcomp.NonLimitedEjectForce);
vendingMachineSystem.EjectRandom(owner, throwItem: true, forceEject: true, vendingcomp);
}
}
}

View File

@@ -1,7 +1,5 @@
using Content.Server.UserInterface;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.VendingMachines;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -9,9 +7,14 @@ namespace Content.Server.VendingMachines
{
[RegisterComponent]
[ComponentReference(typeof(SharedVendingMachineComponent))]
[Access(typeof(VendingMachineSystem))]
public sealed class VendingMachineComponent : SharedVendingMachineComponent
{
public bool Ejecting;
public bool Denying;
public bool DispenseOnHitCoolingDown;
public string? NextItemToEject;
public bool Broken;
@@ -21,6 +24,8 @@ namespace Content.Server.VendingMachines
[DataField("speedLimiter")]
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.
@@ -28,24 +33,48 @@ namespace Content.Server.VendingMachines
[DataField("dispenseOnHitChance")]
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")]
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="SharedVendingMachineComponent.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("dispenseOnHitCooldown")]
public float? DispenseOnHitCooldown = 1.0f;
/// <summary>
/// Sound that plays when ejecting an item
/// </summary>
[DataField("soundVend")]
// Grabbed from: https://github.com/discordia-space/CEV-Eris/blob/f702afa271136d093ddeb415423240a2ceb212f0/sound/machines/vending_drop.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")]
// 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");
/// <summary>
/// The action available to the player controlling the vending machine
/// </summary>
[DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string? Action = "VendingThrow";
[ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(VendingMachineUiKey.Key);
public float NonLimitedEjectForce = 7.5f;
public float NonLimitedEjectRange = 5f;
public float EjectAccumulator = 0f;
public float DenyAccumulator = 0f;
public float DispenseOnHitAccumulator = 0f;
}
}

View File

@@ -42,7 +42,7 @@ public sealed class VendingMachineEjectItemWireAction : BaseWireAction
{
if (EntityManager.TryGetComponent(wire.Owner, out VendingMachineComponent? vending))
{
vending.CanShoot = true;
_vendingMachineSystem.SetShooting(wire.Owner, true, vending);
}
return true;
@@ -52,7 +52,7 @@ public sealed class VendingMachineEjectItemWireAction : BaseWireAction
{
if (EntityManager.TryGetComponent(wire.Owner, out VendingMachineComponent? vending))
{
vending.CanShoot = false;
_vendingMachineSystem.SetShooting(wire.Owner, false, vending);
}
return true;

View File

@@ -1,7 +1,7 @@
using System.Linq;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.UserInterface;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Actions;
@@ -16,7 +16,6 @@ using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using static Content.Shared.VendingMachines.SharedVendingMachineComponent;
namespace Content.Server.VendingMachines
{
@@ -28,18 +27,23 @@ namespace Content.Server.VendingMachines
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
[Dependency] private readonly SharedActionsSystem _action = default!;
[Dependency] private readonly AudioSystem _audioSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<VendingMachineComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<VendingMachineComponent, InventorySyncRequestMessage>(OnInventoryRequestMessage);
SubscribeLocalEvent<VendingMachineComponent, VendingMachineEjectMessage>(OnInventoryEjectMessage);
SubscribeLocalEvent<VendingMachineComponent, BreakageEventArgs>(OnBreak);
SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<VendingMachineComponent, DamageChangedEvent>(OnDamage);
SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt);
SubscribeLocalEvent<VendingMachineComponent, BoundUIOpenedEvent>(OnBoundUIOpened);
SubscribeLocalEvent<VendingMachineComponent, VendingMachineEjectMessage>(OnInventoryEjectMessage);
SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense);
}
@@ -49,9 +53,9 @@ namespace Content.Server.VendingMachines
var component = (VendingMachineComponent) sharedComponent;
if (TryComp<ApcPowerReceiverComponent>(component.Owner, out var receiver))
if (HasComp<ApcPowerReceiverComponent>(component.Owner))
{
TryUpdateVisualState(uid, null, component);
TryUpdateVisualState(uid, component);
}
if (component.Action != null)
@@ -61,17 +65,22 @@ namespace Content.Server.VendingMachines
}
}
private void OnInventoryRequestMessage(EntityUid uid, VendingMachineComponent component, InventorySyncRequestMessage args)
private void OnActivatableUIOpenAttempt(EntityUid uid, VendingMachineComponent component, ActivatableUIOpenAttemptEvent args)
{
if (!this.IsPowered(uid, EntityManager))
return;
if (component.Broken)
args.Cancel();
}
var inventory = new List<VendingMachineInventoryEntry>(component.Inventory);
private void OnBoundUIOpened(EntityUid uid, VendingMachineComponent component, BoundUIOpenedEvent args)
{
UpdateVendingMachineInterfaceState(component);
}
if (component.Emagged) inventory.AddRange(component.EmaggedInventory);
if (component.Contraband) inventory.AddRange(component.ContrabandInventory);
private void UpdateVendingMachineInterfaceState(VendingMachineComponent component)
{
var state = new VendingMachineInterfaceState(GetAllInventory(component.Owner, component));
component.UserInterface?.SendMessage(new VendingMachineInventoryMessage(inventory));
_userInterfaceSystem.TrySetUiState(component.Owner, VendingMachineUiKey.Key, state);
}
private void OnInventoryEjectMessage(EntityUid uid, VendingMachineComponent component, VendingMachineEjectMessage args)
@@ -87,13 +96,13 @@ namespace Content.Server.VendingMachines
private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, PowerChangedEvent args)
{
TryUpdateVisualState(uid, null, component);
TryUpdateVisualState(uid, component);
}
private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs)
{
vendComponent.Broken = true;
TryUpdateVisualState(uid, VendingMachineVisualState.Broken, vendComponent);
TryUpdateVisualState(uid, vendComponent);
}
private void OnEmagged(EntityUid uid, VendingMachineComponent component, GotEmaggedEvent args)
@@ -107,11 +116,17 @@ namespace Content.Server.VendingMachines
private void OnDamage(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args)
{
if (component.DispenseOnHitChance == null || args.DamageDelta == null)
if (component.Broken || component.DispenseOnHitCoolingDown ||
component.DispenseOnHitChance == null || args.DamageDelta == null)
return;
if (args.DamageDelta.Total >= component.DispenseOnHitThreshold && _random.Prob(component.DispenseOnHitChance.Value))
EjectRandom(uid, true, component);
if (args.DamageIncreased && args.DamageDelta.Total >= component.DispenseOnHitThreshold &&
_random.Prob(component.DispenseOnHitChance.Value))
{
if (component.DispenseOnHitCooldown > 0f)
component.DispenseOnHitCoolingDown = true;
EjectRandom(uid, throwItem: true, forceEject: true, component);
}
}
private void OnSelfDispense(EntityUid uid, VendingMachineComponent component, VendingMachineSelfDispenseEvent args)
@@ -120,7 +135,18 @@ namespace Content.Server.VendingMachines
return;
args.Handled = true;
EjectRandom(uid, true, component);
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;
}
public void Deny(EntityUid uid, VendingMachineComponent? vendComponent = null)
@@ -128,16 +154,18 @@ namespace Content.Server.VendingMachines
if (!Resolve(uid, ref vendComponent))
return;
SoundSystem.Play(vendComponent.SoundDeny.GetSound(), Filter.Pvs(vendComponent.Owner), vendComponent.Owner, AudioParams.Default.WithVolume(-2f));
// Play the Deny animation
TryUpdateVisualState(uid, VendingMachineVisualState.Deny, vendComponent);
//TODO: This duration should be a distinct value specific to the deny animation
vendComponent.Owner.SpawnTimer(vendComponent.AnimationDuration, () =>
{
TryUpdateVisualState(uid, VendingMachineVisualState.Normal, vendComponent);
});
if (vendComponent.Denying)
return;
vendComponent.Denying = true;
_audioSystem.Play(vendComponent.SoundDeny, Filter.Pvs(vendComponent.Owner), vendComponent.Owner, AudioParams.Default.WithVolume(-2f));
TryUpdateVisualState(uid, vendComponent);
}
/// <summary>
/// Checks if the user is authorized to use this vending machine
/// </summary>
/// <param name="sender">Entity trying to use the vending machine</param>
public bool IsAuthorized(EntityUid uid, EntityUid? sender, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent) || sender == null)
@@ -155,6 +183,13 @@ namespace Content.Server.VendingMachines
return true;
}
/// <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="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>
public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
@@ -165,13 +200,7 @@ namespace Content.Server.VendingMachines
return;
}
var entry = type switch
{
InventoryType.Regular => vendComponent.Inventory.Find(x => x.ID == itemId),
InventoryType.Emagged when vendComponent.Emagged => vendComponent.EmaggedInventory.Find(x => x.ID == itemId),
InventoryType.Contraband when vendComponent.Contraband => vendComponent.ContrabandInventory.Find(x => x.ID == itemId),
_ => null
};
var entry = GetEntry(itemId, type, vendComponent);
if (entry == null)
{
@@ -195,24 +224,20 @@ namespace Content.Server.VendingMachines
// Start Ejecting, and prevent users from ordering while anim playing
vendComponent.Ejecting = true;
vendComponent.NextItemToEject = entry.ID;
vendComponent.ThrowNextItem = throwItem;
entry.Amount--;
vendComponent.UserInterface?.SendMessage(new VendingMachineInventoryMessage(vendComponent.AllInventory));
TryUpdateVisualState(uid, VendingMachineVisualState.Eject, vendComponent);
vendComponent.Owner.SpawnTimer(vendComponent.AnimationDuration, () =>
{
vendComponent.Ejecting = false;
TryUpdateVisualState(uid, VendingMachineVisualState.Normal, vendComponent);
var ent = EntityManager.SpawnEntity(entry.ID, transformComp.Coordinates);
if (throwItem)
{
float range = vendComponent.NonLimitedEjectRange;
Vector2 direction = new Vector2(_random.NextFloat(-range, range), _random.NextFloat(-range, range));
_throwingSystem.TryThrow(ent, direction, vendComponent.NonLimitedEjectForce);
}
});
SoundSystem.Play(vendComponent.SoundVend.GetSound(), Filter.Pvs(vendComponent.Owner), vendComponent.Owner, AudioParams.Default.WithVolume(-2f));
UpdateVendingMachineInterfaceState(vendComponent);
TryUpdateVisualState(uid, vendComponent);
_audioSystem.Play(vendComponent.SoundVend, Filter.Pvs(vendComponent.Owner), vendComponent.Owner, AudioParams.Default.WithVolume(-2f));
}
/// <summary>
/// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true
/// </summary>
/// <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>
public void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component)
{
if (IsAuthorized(uid, sender, component))
@@ -221,12 +246,15 @@ namespace Content.Server.VendingMachines
}
}
public void TryUpdateVisualState(EntityUid uid, VendingMachineVisualState? state = VendingMachineVisualState.Normal, VendingMachineComponent? vendComponent = null)
/// <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 = state == null ? VendingMachineVisualState.Normal : state;
var finalState = VendingMachineVisualState.Normal;
if (vendComponent.Broken)
{
finalState = VendingMachineVisualState.Broken;
@@ -235,6 +263,10 @@ namespace Content.Server.VendingMachines
{
finalState = VendingMachineVisualState.Eject;
}
else if (vendComponent.Denying)
{
finalState = VendingMachineVisualState.Deny;
}
else if (!this.IsPowered(uid, EntityManager))
{
finalState = VendingMachineVisualState.Off;
@@ -242,23 +274,121 @@ namespace Content.Server.VendingMachines
if (TryComp<AppearanceComponent>(vendComponent.Owner, out var appearance))
{
appearance.SetData(VendingMachineVisuals.VisualState, finalState);
_appearanceSystem.SetData(uid, VendingMachineVisuals.VisualState, finalState, appearance);
}
}
public void EjectRandom(EntityUid uid, bool throwItem, VendingMachineComponent? vendComponent = null)
/// <summary>
/// Ejects a random item from the available stock. Will do nothing if the vending machine is empty.
/// </summary>
/// <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>
public void EjectRandom(EntityUid uid, bool throwItem, bool forceEject = false, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
var availableItems = vendComponent.AllInventory.Where(x => x.Amount > 0).ToList();
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(item.ID, item.Type, vendComponent);
if (entry != null)
entry.Amount--;
EjectItem(vendComponent, forceEject);
}
else
TryEjectVendorItem(uid, item.Type, item.ID, throwItem, vendComponent);
}
private void EjectItem(VendingMachineComponent vendComponent, bool forceEject = false)
{
// No need to update the visual state because we never changed it during a forced eject
if (!forceEject)
TryUpdateVisualState(vendComponent.Owner, vendComponent);
if (string.IsNullOrEmpty(vendComponent.NextItemToEject))
{
vendComponent.ThrowNextItem = false;
return;
}
var ent = EntityManager.SpawnEntity(vendComponent.NextItemToEject, Transform(vendComponent.Owner).Coordinates);
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;
}
private void DenyItem(VendingMachineComponent vendComponent)
{
TryUpdateVisualState(vendComponent.Owner, vendComponent);
}
private VendingMachineInventoryEntry? GetEntry(string entryId, InventoryType type, VendingMachineComponent component)
{
if (type == InventoryType.Emagged && component.Emagged)
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);
foreach (var comp in EntityQuery<VendingMachineComponent>())
{
if (comp.Ejecting)
{
comp.EjectAccumulator += frameTime;
if (comp.EjectAccumulator >= comp.EjectDelay)
{
comp.EjectAccumulator = 0f;
comp.Ejecting = false;
EjectItem(comp);
}
}
if (comp.Denying)
{
comp.DenyAccumulator += frameTime;
if (comp.DenyAccumulator >= comp.DenyDelay)
{
comp.DenyAccumulator = 0f;
comp.Denying = false;
DenyItem(comp);
}
}
if (comp.DispenseOnHitCoolingDown)
{
comp.DispenseOnHitAccumulator += frameTime;
if (comp.DispenseOnHitAccumulator >= comp.DispenseOnHitCooldown)
{
comp.DispenseOnHitAccumulator = 0f;
comp.DispenseOnHitCoolingDown = false;
}
}
}
}
}
}

View File

@@ -8,85 +8,45 @@ namespace Content.Shared.VendingMachines
[NetworkedComponent()]
public abstract class SharedVendingMachineComponent : Component
{
/// <summary>
/// PrototypeID for the vending machine's inventory, see <see cref="VendingMachineInventoryPrototype"/>
/// </summary>
[DataField("pack", customTypeSerializer: typeof(PrototypeIdSerializer<VendingMachineInventoryPrototype>))]
public string PackPrototypeId = string.Empty;
public TimeSpan AnimationDuration = TimeSpan.Zero;
/// <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("denyDelay")]
public float DenyDelay = 2.0f;
[ViewVariables] public List<VendingMachineInventoryEntry> Inventory = new();
[ViewVariables] public List<VendingMachineInventoryEntry> EmaggedInventory = new();
[ViewVariables] public List<VendingMachineInventoryEntry> ContrabandInventory = new();
/// <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("ejectDelay")]
public float EjectDelay = 1.2f;
public List<VendingMachineInventoryEntry> AllInventory
{
get
{
var inventory = new List<VendingMachineInventoryEntry>(Inventory);
[ViewVariables]
public Dictionary<string, VendingMachineInventoryEntry> Inventory = new();
if (Emagged) inventory.AddRange(EmaggedInventory);
if (Contraband) inventory.AddRange(ContrabandInventory);
[ViewVariables]
public Dictionary<string, VendingMachineInventoryEntry> EmaggedInventory = new();
return inventory;
}
}
[ViewVariables]
public Dictionary<string, VendingMachineInventoryEntry> ContrabandInventory = new();
public bool Emagged;
public bool Contraband;
[Serializable, NetSerializable]
public enum VendingMachineVisuals
{
VisualState,
Inventory,
}
[Serializable, NetSerializable]
public enum VendingMachineVisualState
{
Normal,
Off,
Broken,
Eject,
Deny,
}
[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;
}
}
[Serializable, NetSerializable]
public enum VendingMachineUiKey
{
Key,
}
[Serializable, NetSerializable]
public sealed class InventorySyncRequestMessage : BoundUserInterfaceMessage
{
}
[Serializable, NetSerializable]
public sealed class VendingMachineInventoryMessage : BoundUserInterfaceMessage
{
public readonly List<VendingMachineInventoryEntry> Inventory;
public VendingMachineInventoryMessage(List<VendingMachineInventoryEntry> inventory)
{
Inventory = inventory;
}
}
[Serializable, NetSerializable]
public sealed class VendingMachineInventoryEntry
{
[ViewVariables(VVAccess.ReadWrite)] public InventoryType Type;
[ViewVariables(VVAccess.ReadWrite)]
public InventoryType Type;
[ViewVariables(VVAccess.ReadWrite)]
public string ID;
[ViewVariables(VVAccess.ReadWrite)]
@@ -98,14 +58,6 @@ namespace Content.Shared.VendingMachines
Amount = amount;
}
}
[Serializable, NetSerializable]
public enum VendingMachineWireStatus : byte
{
Power,
Access,
Advertisement,
Limiter
}
[Serializable, NetSerializable]
public enum InventoryType : byte
@@ -114,6 +66,21 @@ namespace Content.Shared.VendingMachines
Emagged,
Contraband
}
[Serializable, NetSerializable]
public enum VendingMachineVisuals
{
VisualState
}
[Serializable, NetSerializable]
public enum VendingMachineVisualState
{
Normal,
Off,
Broken,
Eject,
Deny,
}
[Serializable, NetSerializable]

View File

@@ -1,4 +1,5 @@
using Robust.Shared.Prototypes;
using System.Linq;
using static Content.Shared.VendingMachines.SharedVendingMachineComponent;
namespace Content.Shared.VendingMachines;
@@ -18,17 +19,43 @@ public abstract class SharedVendingMachineSystem : EntitySystem
if (!_prototypeManager.TryIndex(component.PackPrototypeId, out VendingMachineInventoryPrototype? packPrototype))
return;
MetaData(uid).EntityName = packPrototype.Name;
component.AnimationDuration = TimeSpan.FromSeconds(packPrototype.AnimationDuration);
if (TryComp(component.Owner, out AppearanceComponent? appearance))
appearance.SetData(VendingMachineVisuals.Inventory, component.PackPrototypeId);
AddInventoryFromPrototype(uid, packPrototype.StartingInventory, InventoryType.Regular, component);
AddInventoryFromPrototype(uid, packPrototype.EmaggedInventory, InventoryType.Emagged, component);
AddInventoryFromPrototype(uid, packPrototype.ContrabandInventory, InventoryType.Contraband, component);
}
/// <summary>
/// Returns all of the vending machine's inventory. Only includes emagged and contraband inventories if
/// <see cref="SharedVendingMachineComponent.Emagged"/> and <see cref="SharedVendingMachineComponent.Contraband"/>
/// are <c>true</c> respectively.
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <returns></returns>
public List<VendingMachineInventoryEntry> GetAllInventory(EntityUid uid, SharedVendingMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return new();
var inventory = new List<VendingMachineInventoryEntry>(component.Inventory.Values);
if (component.Emagged)
inventory.AddRange(component.EmaggedInventory.Values);
if (component.Contraband)
inventory.AddRange(component.ContrabandInventory.Values);
return inventory;
}
public List<VendingMachineInventoryEntry> GetAvailableInventory(EntityUid uid, SharedVendingMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return new();
return GetAllInventory(uid, component).Where(_ => _.Amount > 0).ToList();
}
private void AddInventoryFromPrototype(EntityUid uid, Dictionary<string, uint>? entries,
InventoryType type,
SharedVendingMachineComponent? component = null)
@@ -38,26 +65,26 @@ public abstract class SharedVendingMachineSystem : EntitySystem
return;
}
var inventory = new List<VendingMachineInventoryEntry>();
var inventory = new Dictionary<string, VendingMachineInventoryEntry>();
foreach (var (id, amount) in entries)
{
if (_prototypeManager.HasIndex<EntityPrototype>(id))
{
inventory.Add(new VendingMachineInventoryEntry(type, id, amount));
inventory.Add(id, new VendingMachineInventoryEntry(type, id, amount));
}
}
switch (type)
{
case InventoryType.Regular:
component.Inventory.AddRange(inventory);
component.Inventory = inventory;
break;
case InventoryType.Emagged:
component.EmaggedInventory.AddRange(inventory);
component.EmaggedInventory = inventory;
break;
case InventoryType.Contraband:
component.ContrabandInventory.AddRange(inventory);
component.ContrabandInventory = inventory;
break;
}
}

View File

@@ -0,0 +1,33 @@
using Robust.Shared.Serialization;
namespace Content.Shared.VendingMachines
{
[NetSerializable, Serializable]
public sealed class VendingMachineInterfaceState : BoundUserInterfaceState
{
public List<VendingMachineInventoryEntry> Inventory;
public VendingMachineInterfaceState(List<VendingMachineInventoryEntry> inventory)
{
Inventory = inventory;
}
}
[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;
}
}
[Serializable, NetSerializable]
public enum VendingMachineUiKey
{
Key,
}
}

View File

@@ -7,19 +7,9 @@ namespace Content.Shared.VendingMachines
public sealed class VendingMachineInventoryPrototype : IPrototype
{
[ViewVariables]
[IdDataFieldAttribute]
[IdDataField]
public string ID { get; } = default!;
[DataField("name")]
public string Name { get; } = string.Empty;
[DataField("animationDuration")]
public double AnimationDuration { get; }
// TODO make this a proper sprite specifier for yaml linting.
[DataField("spriteName")]
public string SpriteName { get; } = string.Empty;
[DataField("startingInventory")]
public Dictionary<string, uint> StartingInventory { get; } = new();

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: AmmoVendInventory
name: Ammovend
spriteName: ammo
startingInventory:
MagazineBoxCaselessRifle: 3
MagazineBoxCaselessRifleHighVelocity: 3

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: AtmosDrobeInventory
name: AtmosDrobe
spriteName: atmosdrobe
startingInventory:
ClothingBackpackDuffelEngineering: 2
ClothingBackpackSatchelEngineering: 2

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: BarDrobeInventory
name: BarDrobe
spriteName: bardrobe
startingInventory:
ClothingHeadHatTophat: 2
ClothingEyesGlassesBeer: 2

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: BoozeOMatInventory
name: Booze-O-Mat
spriteName: boozeomat
startingInventory:
DrinkGlass: 30 #Kept glasses at top for ease to differentiate from booze.

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: CargoDrobeInventory
name: CargoDrobe
spriteName: cargodrobe
startingInventory:
AppraisalTool: 3
ClothingUniformJumpsuitCargo: 3

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: PTechInventory
name: PTech
spriteName: cart
startingInventory:
PassengerPDA: 5
ClearPDA: 5

View File

@@ -1,8 +1,5 @@
- type: vendingMachineInventory
id: ChangInventory
name: Mr. Chang
animationDuration: 2.1
spriteName: cigs
startingInventory:
FoodCondimentPacketSoy: 5
FoodSnackCookieFortune: 5

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: PietyVendInventory
name: PietyVend
spriteName: chapel
startingInventory:
ClothingUniformJumpsuitChaplain: 2
ClothingUniformJumpskirtChaplain: 2

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: ChefDrobeInventory
name: ChefDrobe
spriteName: chefdrobe
startingInventory:
ClothingHeadsetService: 2
ClothingOuterApronChef: 3

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: ChemDrobeInventory
name: ChemDrobe
spriteName: chemdrobe
startingInventory:
ClothingUniformJumpsuitChemistry: 2
ClothingUniformJumpskirtChemistry: 2

View File

@@ -1,8 +1,5 @@
- type: vendingMachineInventory
id: CigaretteMachineInventory
name: cigarette machine
animationDuration: 2.1
spriteName: cigs
startingInventory:
CigPackGreen: 2
CigPackRed: 2

View File

@@ -1,6 +1,5 @@
- type: vendingMachineInventory
id: ClothesMateInventory
name: ClothesMate
startingInventory:
ClothingBackpack: 5
ClothingBackpackDuffel: 5

View File

@@ -1,8 +1,5 @@
- type: vendingMachineInventory
id: HotDrinksMachineInventory
name: Hot drinks machine
animationDuration: 3.4
spriteName: coffee
startingInventory:
DrinkHotCoffee: 10
DrinkTeacup: 10

View File

@@ -1,8 +1,5 @@
- type: vendingMachineInventory
id: RobustSoftdrinksInventory
name: Robust Softdrinks
animationDuration: 1.1
spriteName: cola
startingInventory:
DrinkColaCan: 2
DrinkEnergyDrinkCan: 2

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: DetDrobeInventory
name: DetDrobe
spriteName: detdrobe
startingInventory:
ClothingUniformJumpsuitDetective: 2
ClothingUniformJumpskirtDetective: 2

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: DinnerwareInventory
name: Dinnerware
spriteName: dinnerware
startingInventory:
ButchCleaver: 1
KitchenKnife: 5

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: DiscountDansInventory
name: Discount Dan's
spriteName: discount
startingInventory:
FoodSnackCheesie: 3
FoodSnackChips: 3

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: DonutInventory
name: Donut
spriteName: donut
startingInventory:
FoodDonutChocolate: 5
FoodDonutApple: 3

View File

@@ -1,4 +1,2 @@
- type: vendingMachineInventory
id: EmptyVendingMachineInventory
name: Empty vending machine
spriteName: empty

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: EngiDrobeInventory
name: EngiDrobe
spriteName: engidrobe
startingInventory:
ClothingBackpackDuffelEngineering: 3
ClothingBackpackSatchelEngineering: 3

View File

@@ -1,8 +1,5 @@
- type: vendingMachineInventory
id: EngiVendInventory
name: Engi-Vend
animationDuration: 2.1
spriteName: engivend
startingInventory:
ClothingEyesGlassesMeson: 4
ClothingHeadHatWelding: 4

View File

@@ -1,8 +1,5 @@
- type: vendingMachineInventory
id: GoodCleanFunInventory
name: Good Clean Fun
animationDuration: 1.3
spriteName: games
startingInventory:
DiceBag: 4
Paper: 8

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: GeneDrobeInventory
name: GeneDrobe
spriteName: genedrobe
startingInventory:
ClothingShoesColorWhite: 2
ClothingOuterCoatLab: 2

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: HyDrobeInventory
name: HyDrobe
spriteName: hydrobe
startingInventory:
ClothingBackpackHydroponics: 2
ClothingBackpackSatchelHydroponics: 2

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: JaniDrobeInventory
name: JaniDrobe
spriteName: janidrobe
startingInventory:
ClothingUniformJumpsuitJanitor: 2
ClothingUniformJumpskirtJanitor: 2

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: LawDrobeInventory
name: LawDrobe
spriteName: lawdrobe
startingInventory:
ClothingUniformJumpsuitLawyerBlue: 1
ClothingUniformJumpsuitLawyerPurple: 1

View File

@@ -1,8 +1,5 @@
- type: vendingMachineInventory
id: MagiVendInventory
name: MagiVend
animationDuration: 1.5
spriteName: magivend
startingInventory:
ClothingHeadHatWizard: 3
ClothingOuterWizard: 3

View File

@@ -1,8 +1,5 @@
- type: vendingMachineInventory
id: NanoMedPlusInventory
name: NanoMed Plus
animationDuration: 1.8
spriteName: medical
startingInventory:
HandheldHealthAnalyzer: 3
Brutepack: 5

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: MediDrobeInventory
name: MediDrobe
spriteName: medidrobe
startingInventory:
ClothingBackpackDuffelMedical: 4
ClothingBackpackMedical: 4

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: NutriMaxInventory
name: NutriMax
spriteName: nutri
startingInventory:
HydroponicsToolSpade: 2
HydroponicsToolMiniHoe: 2

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: RoboDrobeInventory
name: RoboDrobe
spriteName: robodrobe
startingInventory:
ClothingOuterCoatLab: 2
ClothingShoesColorBlack: 2

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: RobotechDeluxeInventory
name: Robotech Deluxe
spriteName: robotics
startingInventory:
#TO DO: add missing prototypes
ClothingOuterCoatLab: 4

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: SalvageEquipmentInventory
name: Salvage Equipment
spriteName: mining
startingInventory:
Crowbar: 2
Pickaxe: 4

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: SciDrobeInventory
name: SciDrobe
spriteName: scidrobe
startingInventory:
ClothingBackpackScience: 3
ClothingBackpackSatchelScience: 3

View File

@@ -1,8 +1,5 @@
- type: vendingMachineInventory
id: SecTechInventory
name: SecTech
animationDuration: 2.8
spriteName: sec
startingInventory:
Handcuffs: 8
GrenadeFlashBang: 4

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: SecDrobeInventory
name: SecDrobe
spriteName: secdrobe
startingInventory:
ClothingBackpackSecurity: 3
ClothingBackpackSatchelSecurity: 3

View File

@@ -1,8 +1,5 @@
- type: vendingMachineInventory
id: MegaSeedServitorInventory
name: MegaSeed Servitor
animationDuration: 1.3
spriteName: seeds
startingInventory:
AloeSeeds: 3
AmbrosiaVulgarisSeeds: 3

View File

@@ -1,4 +1,2 @@
- type: vendingMachineInventory
id: SmartFridgeInventory
name: SmartFridge
spriteName: smartfridge

View File

@@ -1,8 +1,5 @@
- type: vendingMachineInventory
id: GetmoreChocolateCorpInventory
name: Getmore Chocolate Corp
animationDuration: 0.5
spriteName: snack
startingInventory:
FoodSnackRaisins: 3
FoodSnackChocolate: 3

View File

@@ -1,6 +1,4 @@
- type: vendingMachineInventory
id: BodaInventory
name: BODA
spriteName: sovietsoda
startingInventory:
DrinkColaCan: 10 #typically hacked product. Default product is "soda"

View File

@@ -1,15 +1,11 @@
- type: vendingMachineInventory
id: TankDispenserEVAInventory
name: tank dispenser
spriteName: tankdispenser
startingInventory:
OxygenTankFilled: 5
NitrogenTankFilled: 5
- type: vendingMachineInventory
id: TankDispenserEngineeringInventory
name: tank dispenser
spriteName: tankdispenser
startingInventory:
PlasmaTankFilled: 5
OxygenTankFilled: 5

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: AutoDrobeInventory
name: AutoDrobe
spriteName: theater
startingInventory:
ClothingOuterWinterClown: 1
ClothingOuterWinterMime: 1

View File

@@ -1,4 +1,2 @@
- type: vendingMachineInventory
id: VendomatInventory
name: Vendomat
spriteName: vendomat

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: ViroDrobeInventory
name: ViroDrobe
spriteName: virodrobe
startingInventory:
ClothingShoesColorWhite: 2
ClothingOuterCoatLab: 2

View File

@@ -1,7 +1,5 @@
- type: vendingMachineInventory
id: NanoMedInventory
name: NanoMed
spriteName: wallmed
startingInventory:
Brutepack: 5
Ointment: 5

View File

@@ -1,8 +1,5 @@
- type: vendingMachineInventory
id: YouToolInventory
name: YouTool
animationDuration: 1.1
spriteName: youtool
startingInventory:
CableApcStack: 5
Crowbar: 5

View File

@@ -46,6 +46,9 @@
},
{
"name": "off"
},
{
"name": "panel"
}
]
}