Predict vending machine UI (#33412)

This commit is contained in:
metalgearsloth
2025-03-02 13:47:52 +11:00
committed by GitHub
parent ba1504d0d6
commit a8ebcac5c9
15 changed files with 542 additions and 318 deletions

View File

@@ -0,0 +1,5 @@
using Content.Shared.Advertise.Systems;
namespace Content.Client.Advertise.Systems;
public sealed class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem;

View File

@@ -96,9 +96,12 @@ public class ListContainer : Control
{ {
ListContainerButton control = new(data[0], 0); ListContainerButton control = new(data[0], 0);
GenerateItem?.Invoke(data[0], control); GenerateItem?.Invoke(data[0], control);
// Yes this AddChild is necessary for reasons (get proper style or whatever?)
// without it the DesiredSize may be different to the final DesiredSize.
AddChild(control);
control.Measure(Vector2Helpers.Infinity); control.Measure(Vector2Helpers.Infinity);
_itemHeight = control.DesiredSize.Y; _itemHeight = control.DesiredSize.Y;
control.Dispose(); control.Orphan();
} }
// Ensure buttons are re-generated. // Ensure buttons are re-generated.
@@ -384,6 +387,7 @@ public sealed class ListContainerButton : ContainerButton, IEntityControl
public ListContainerButton(ListData data, int index) public ListContainerButton(ListData data, int index)
{ {
AddStyleClass(StyleClassButton);
Data = data; Data = data;
Index = index; Index = index;
// AddChild(Background = new PanelContainer // AddChild(Background = new PanelContainer

View File

@@ -16,4 +16,9 @@ public sealed partial class VendingMachineItem : BoxContainer
NameLabel.Text = text; NameLabel.Text = text;
} }
public void SetText(string text)
{
NameLabel.Text = text;
}
} }

View File

@@ -1,3 +1,4 @@
using System.Linq;
using System.Numerics; using System.Numerics;
using Content.Shared.VendingMachines; using Content.Shared.VendingMachines;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
@@ -19,11 +20,16 @@ namespace Content.Client.VendingMachines.UI
[Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!;
private readonly Dictionary<EntProtoId, EntityUid> _dummies = []; 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 event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
public VendingMachineMenu() public VendingMachineMenu()
{ {
MinSize = SetSize = new Vector2(250, 150); MinSize = SetSize = new Vector2(250, 150);
@@ -68,18 +74,23 @@ namespace Content.Client.VendingMachines.UI
if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text }) if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text })
return; return;
button.AddChild(new VendingMachineItem(protoID, text)); var item = new VendingMachineItem(protoID, text);
_listItems[protoID] = (button, item);
button.ToolTip = text; button.AddChild(item);
button.StyleBoxOverride = _styleBox; button.AddStyleClass("ButtonSquare");
button.Disabled = !_enabled || _amounts[protoID] == 0;
} }
/// <summary> /// <summary>
/// Populates the list of available items on the vending machine interface /// Populates the list of available items on the vending machine interface
/// and sets icons based on their prototypes /// and sets icons based on their prototypes
/// </summary> /// </summary>
public void Populate(List<VendingMachineInventoryEntry> inventory) public void Populate(List<VendingMachineInventoryEntry> inventory, bool enabled)
{ {
_enabled = enabled;
_listItems.Clear();
_amounts.Clear();
if (inventory.Count == 0 && VendingContents.Visible) if (inventory.Count == 0 && VendingContents.Visible)
{ {
SearchBar.Visible = false; SearchBar.Visible = false;
@@ -109,7 +120,10 @@ namespace Content.Client.VendingMachines.UI
var entry = inventory[i]; var entry = inventory[i];
if (!_prototypeManager.TryIndex(entry.ID, out var prototype)) if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
{
_amounts[entry.ID] = 0;
continue; continue;
}
if (!_dummies.TryGetValue(entry.ID, out var dummy)) if (!_dummies.TryGetValue(entry.ID, out var dummy))
{ {
@@ -119,11 +133,15 @@ namespace Content.Client.VendingMachines.UI
var itemName = Identity.Name(dummy, _entityManager); var itemName = Identity.Name(dummy, _entityManager);
var itemText = $"{itemName} [{entry.Amount}]"; var itemText = $"{itemName} [{entry.Amount}]";
_amounts[entry.ID] = entry.Amount;
if (itemText.Length > longestEntry.Length) if (itemText.Length > longestEntry.Length)
longestEntry = itemText; longestEntry = itemText;
listData.Add(new VendorItemsListData(prototype.ID, itemText, i)); listData.Add(new VendorItemsListData(prototype.ID, i)
{
ItemText = itemText,
});
} }
VendingContents.PopulateList(listData); VendingContents.PopulateList(listData);
@@ -131,12 +149,43 @@ namespace Content.Client.VendingMachines.UI
SetSizeAfterUpdate(longestEntry.Length, inventory.Count); SetSizeAfterUpdate(longestEntry.Length, inventory.Count);
} }
/// <summary>
/// Updates text entries for vending data in place without modifying the list controls.
/// </summary>
public void UpdateAmounts(List<VendingMachineInventoryEntry> cachedInventory, bool enabled)
{
_enabled = enabled;
foreach (var proto in _dummies.Keys)
{
if (!_listItems.TryGetValue(proto, out var button))
continue;
var dummy = _dummies[proto];
var amount = cachedInventory.First(o => o.ID == proto).Amount;
// Could be better? Problem is all inventory entries get squashed.
var text = GetItemText(dummy, amount);
button.Item.SetText(text);
button.Button.Disabled = !enabled || amount == 0;
}
}
private string GetItemText(EntityUid dummy, uint amount)
{
var itemName = Identity.Name(dummy, _entityManager);
return $"{itemName} [{amount}]";
}
private void SetSizeAfterUpdate(int longestEntryLength, int contentCount) private void SetSizeAfterUpdate(int longestEntryLength, int contentCount)
{ {
SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400), SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400),
Math.Clamp(contentCount * 50, 150, 350)); Math.Clamp(contentCount * 50, 150, 350));
} }
} }
}
public record VendorItemsListData(EntProtoId ItemProtoID, string ItemText, int ItemIndex) : ListData; public record VendorItemsListData(EntProtoId ItemProtoID, int ItemIndex) : ListData
{
public string ItemText = string.Empty;
}
}

View File

@@ -31,10 +31,21 @@ namespace Content.Client.VendingMachines
public void Refresh() public void Refresh()
{ {
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
var system = EntMan.System<VendingMachineSystem>(); var system = EntMan.System<VendingMachineSystem>();
_cachedInventory = system.GetAllInventory(Owner); _cachedInventory = system.GetAllInventory(Owner);
_menu?.Populate(_cachedInventory); _menu?.Populate(_cachedInventory, enabled);
}
public void UpdateAmounts()
{
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
var system = EntMan.System<VendingMachineSystem>();
_cachedInventory = system.GetAllInventory(Owner);
_menu?.UpdateAmounts(_cachedInventory, enabled);
} }
private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data) private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
@@ -53,7 +64,7 @@ namespace Content.Client.VendingMachines
if (selectedItem == null) if (selectedItem == null)
return; return;
SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID)); SendPredictedMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)

View File

@@ -1,6 +1,8 @@
using System.Linq;
using Content.Shared.VendingMachines; using Content.Shared.VendingMachines;
using Robust.Client.Animations; using Robust.Client.Animations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.GameStates;
namespace Content.Client.VendingMachines; namespace Content.Client.VendingMachines;
@@ -8,7 +10,6 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
{ {
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!; [Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -16,14 +17,69 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
SubscribeLocalEvent<VendingMachineComponent, AppearanceChangeEvent>(OnAppearanceChange); SubscribeLocalEvent<VendingMachineComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<VendingMachineComponent, AnimationCompletedEvent>(OnAnimationCompleted); SubscribeLocalEvent<VendingMachineComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<VendingMachineComponent, AfterAutoHandleStateEvent>(OnVendingAfterState); SubscribeLocalEvent<VendingMachineComponent, ComponentHandleState>(OnVendingHandleState);
} }
private void OnVendingAfterState(EntityUid uid, VendingMachineComponent component, ref AfterAutoHandleStateEvent args) private void OnVendingHandleState(Entity<VendingMachineComponent> entity, ref ComponentHandleState args)
{ {
if (_uiSystem.TryGetOpenUi<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui)) if (args.Current is not VendingMachineComponentState state)
return;
var uid = entity.Owner;
var component = entity.Comp;
component.Contraband = state.Contraband;
component.EjectEnd = state.EjectEnd;
component.DenyEnd = state.DenyEnd;
component.DispenseOnHitEnd = state.DispenseOnHitEnd;
// If all we did was update amounts then we can leave BUI buttons in place.
var fullUiUpdate = !component.Inventory.Keys.SequenceEqual(state.Inventory.Keys) ||
!component.EmaggedInventory.Keys.SequenceEqual(state.EmaggedInventory.Keys) ||
!component.ContrabandInventory.Keys.SequenceEqual(state.ContrabandInventory.Keys);
component.Inventory.Clear();
component.EmaggedInventory.Clear();
component.ContrabandInventory.Clear();
foreach (var entry in state.Inventory)
{ {
bui.Refresh(); component.Inventory.Add(entry.Key, new(entry.Value));
}
foreach (var entry in state.EmaggedInventory)
{
component.EmaggedInventory.Add(entry.Key, new(entry.Value));
}
foreach (var entry in state.ContrabandInventory)
{
component.ContrabandInventory.Add(entry.Key, new(entry.Value));
}
if (UISystem.TryGetOpenUi<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui))
{
if (fullUiUpdate)
{
bui.Refresh();
}
else
{
bui.UpdateAmounts();
}
}
}
protected override void UpdateUI(Entity<VendingMachineComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return;
if (UISystem.TryGetOpenUi<VendingMachineBoundUserInterface>(entity.Owner,
VendingMachineUiKey.Key,
out var bui))
{
bui.UpdateAmounts();
} }
} }
@@ -70,13 +126,13 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
if (component.LoopDenyAnimation) if (component.LoopDenyAnimation)
SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.DenyState, sprite); SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.DenyState, sprite);
else else
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, component.DenyDelay, sprite); PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, (float)component.DenyDelay.TotalSeconds, sprite);
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite); SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
break; break;
case VendingMachineVisualState.Eject: case VendingMachineVisualState.Eject:
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, component.EjectDelay, sprite); PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, (float)component.EjectDelay.TotalSeconds, sprite);
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite); SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
break; break;

View File

@@ -1,13 +1,13 @@
using Content.Server.Advertise.Components;
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Shared.Dataset; using Content.Shared.Advertise.Components;
using Content.Shared.Advertise.Systems;
using Content.Shared.UserInterface;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using ActivatableUIComponent = Content.Shared.UserInterface.ActivatableUIComponent;
namespace Content.Server.Advertise; namespace Content.Server.Advertise.EntitySystems;
public sealed partial class SpeakOnUIClosedSystem : EntitySystem public sealed partial class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem
{ {
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -46,13 +46,4 @@ public sealed partial class SpeakOnUIClosedSystem : EntitySystem
entity.Comp.Flag = false; entity.Comp.Flag = false;
return true; return true;
} }
public bool TrySetFlag(Entity<SpeakOnUIClosedComponent?> entity, bool value = true)
{
if (!Resolve(entity, ref entity.Comp))
return false;
entity.Comp.Flag = value;
return true;
}
} }

View File

@@ -1,11 +1,9 @@
using Content.Server.Power.Components;
using Content.Shared.UserInterface; using Content.Shared.UserInterface;
using Content.Server.Advertise; using Content.Server.Advertise.EntitySystems;
using Content.Server.Advertise.Components; using Content.Shared.Advertise.Components;
using Content.Shared.Arcade; using Content.Shared.Arcade;
using Content.Shared.Power; using Content.Shared.Power;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Player;
namespace Content.Server.Arcade.BlockGame; namespace Content.Server.Arcade.BlockGame;

View File

@@ -1,9 +1,9 @@
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Shared.UserInterface; using Content.Shared.UserInterface;
using Content.Server.Advertise; using Content.Server.Advertise.EntitySystems;
using Content.Server.Advertise.Components; using Content.Shared.Advertise.Components;
using Content.Shared.Arcade;
using Content.Shared.Power; using Content.Shared.Power;
using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
@@ -24,7 +24,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
SubscribeLocalEvent<SpaceVillainArcadeComponent, ComponentInit>(OnComponentInit); SubscribeLocalEvent<SpaceVillainArcadeComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<SpaceVillainArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpenSV); SubscribeLocalEvent<SpaceVillainArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpenSV);
SubscribeLocalEvent<SpaceVillainArcadeComponent, SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction); SubscribeLocalEvent<SpaceVillainArcadeComponent, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
SubscribeLocalEvent<SpaceVillainArcadeComponent, PowerChangedEvent>(OnSVillainPower); SubscribeLocalEvent<SpaceVillainArcadeComponent, PowerChangedEvent>(OnSVillainPower);
} }
@@ -70,7 +70,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
component.RewardAmount = new Random().Next(component.RewardMinAmount, component.RewardMaxAmount + 1); component.RewardAmount = new Random().Next(component.RewardMinAmount, component.RewardMaxAmount + 1);
} }
private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SpaceVillainArcadePlayerActionMessage msg) private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage msg)
{ {
if (component.Game == null) if (component.Game == null)
return; return;
@@ -79,22 +79,22 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
switch (msg.PlayerAction) switch (msg.PlayerAction)
{ {
case PlayerAction.Attack: case SharedSpaceVillainArcadeComponent.PlayerAction.Attack:
case PlayerAction.Heal: case SharedSpaceVillainArcadeComponent.PlayerAction.Heal:
case PlayerAction.Recharge: case SharedSpaceVillainArcadeComponent.PlayerAction.Recharge:
component.Game.ExecutePlayerAction(uid, msg.PlayerAction, component); component.Game.ExecutePlayerAction(uid, msg.PlayerAction, component);
// Any sort of gameplay action counts // Any sort of gameplay action counts
if (TryComp<SpeakOnUIClosedComponent>(uid, out var speakComponent)) if (TryComp<SpeakOnUIClosedComponent>(uid, out var speakComponent))
_speakOnUIClosed.TrySetFlag((uid, speakComponent)); _speakOnUIClosed.TrySetFlag((uid, speakComponent));
break; break;
case PlayerAction.NewGame: case SharedSpaceVillainArcadeComponent.PlayerAction.NewGame:
_audioSystem.PlayPvs(component.NewGameSound, uid, AudioParams.Default.WithVolume(-4f)); _audioSystem.PlayPvs(component.NewGameSound, uid, AudioParams.Default.WithVolume(-4f));
component.Game = new SpaceVillainGame(uid, component, this); component.Game = new SpaceVillainGame(uid, component, this);
_uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage()); _uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
break; break;
case PlayerAction.RequestData: case SharedSpaceVillainArcadeComponent.PlayerAction.RequestData:
_uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage()); _uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
break; break;
} }
} }
@@ -109,6 +109,6 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
if (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered) if (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered)
return; return;
_uiSystem.CloseUi(uid, SpaceVillainArcadeUiKey.Key); _uiSystem.CloseUi(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key);
} }
} }

View File

@@ -1,19 +1,12 @@
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Content.Server.Advertise;
using Content.Server.Advertise.Components;
using Content.Server.Cargo.Systems; using Content.Server.Cargo.Systems;
using Content.Server.Emp; using Content.Server.Emp;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems; using Content.Server.Power.EntitySystems;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Actions;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Destructible; using Content.Shared.Destructible;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Emp; using Content.Shared.Emp;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Power; using Content.Shared.Power;
@@ -21,7 +14,6 @@ using Content.Shared.Throwing;
using Content.Shared.UserInterface; using Content.Shared.UserInterface;
using Content.Shared.VendingMachines; using Content.Shared.VendingMachines;
using Content.Shared.Wall; using Content.Shared.Wall;
using Robust.Server.GameObjects;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
@@ -32,14 +24,9 @@ namespace Content.Server.VendingMachines
public sealed class VendingMachineSystem : SharedVendingMachineSystem public sealed class VendingMachineSystem : SharedVendingMachineSystem
{ {
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly PricingSystem _pricing = default!; [Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly ThrowingSystem _throwingSystem = default!; [Dependency] private readonly ThrowingSystem _throwingSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!;
[Dependency] private readonly SharedPointLightSystem _light = default!;
[Dependency] private readonly EmagSystem _emag = default!;
private const float WallVendEjectDistanceFromWall = 1f; private const float WallVendEjectDistanceFromWall = 1f;
@@ -55,11 +42,6 @@ namespace Content.Server.VendingMachines
SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt); SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt);
Subs.BuiEvents<VendingMachineComponent>(VendingMachineUiKey.Key, subs =>
{
subs.Event<VendingMachineEjectMessage>(OnInventoryEjectMessage);
});
SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense); SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense);
SubscribeLocalEvent<VendingMachineComponent, RestockDoAfterEvent>(OnDoAfter); SubscribeLocalEvent<VendingMachineComponent, RestockDoAfterEvent>(OnDoAfter);
@@ -91,7 +73,7 @@ namespace Content.Server.VendingMachines
if (HasComp<ApcPowerReceiverComponent>(uid)) if (HasComp<ApcPowerReceiverComponent>(uid))
{ {
TryUpdateVisualState(uid, component); TryUpdateVisualState((uid, component));
} }
} }
@@ -101,26 +83,15 @@ namespace Content.Server.VendingMachines
args.Cancel(); args.Cancel();
} }
private void OnInventoryEjectMessage(EntityUid uid, VendingMachineComponent component, VendingMachineEjectMessage args)
{
if (!this.IsPowered(uid, EntityManager))
return;
if (args.Actor is not { Valid: true } entity || Deleted(entity))
return;
AuthorizedVend(uid, entity, args.Type, args.ID, component);
}
private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, ref PowerChangedEvent args) private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, ref PowerChangedEvent args)
{ {
TryUpdateVisualState(uid, component); TryUpdateVisualState((uid, component));
} }
private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs) private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs)
{ {
vendComponent.Broken = true; vendComponent.Broken = true;
TryUpdateVisualState(uid, vendComponent); TryUpdateVisualState((uid, vendComponent));
} }
private void OnDamageChanged(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args) private void OnDamageChanged(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args)
@@ -128,7 +99,7 @@ namespace Content.Server.VendingMachines
if (!args.DamageIncreased && component.Broken) if (!args.DamageIncreased && component.Broken)
{ {
component.Broken = false; component.Broken = false;
TryUpdateVisualState(uid, component); TryUpdateVisualState((uid, component));
return; return;
} }
@@ -139,8 +110,11 @@ namespace Content.Server.VendingMachines
if (args.DamageIncreased && args.DamageDelta.GetTotal() >= component.DispenseOnHitThreshold && if (args.DamageIncreased && args.DamageDelta.GetTotal() >= component.DispenseOnHitThreshold &&
_random.Prob(component.DispenseOnHitChance.Value)) _random.Prob(component.DispenseOnHitChance.Value))
{ {
if (component.DispenseOnHitCooldown > 0f) if (component.DispenseOnHitCooldown != null)
component.DispenseOnHitCoolingDown = true; {
component.DispenseOnHitEnd = Timing.CurTime + component.DispenseOnHitCooldown.Value;
}
EjectRandom(uid, throwItem: true, forceEject: true, component); EjectRandom(uid, throwItem: true, forceEject: true, component);
} }
} }
@@ -199,145 +173,6 @@ namespace Content.Server.VendingMachines
Dirty(uid, component); Dirty(uid, component);
} }
public void Deny(EntityUid uid, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
if (vendComponent.Denying)
return;
vendComponent.Denying = true;
Audio.PlayPvs(vendComponent.SoundDeny, uid, AudioParams.Default.WithVolume(-2f));
TryUpdateVisualState(uid, vendComponent);
}
/// <summary>
/// Checks if the user is authorized to use this vending machine
/// </summary>
/// <param name="uid"></param>
/// <param name="sender">Entity trying to use the vending machine</param>
/// <param name="vendComponent"></param>
public bool IsAuthorized(EntityUid uid, EntityUid sender, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return false;
if (!TryComp<AccessReaderComponent>(uid, out var accessReader))
return true;
if (_accessReader.IsAllowed(sender, uid, accessReader))
return true;
Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid);
Deny(uid, vendComponent);
return false;
}
/// <summary>
/// Tries to eject the provided item. Will do nothing if the vending machine is incapable of ejecting, already ejecting
/// or the item doesn't exist in its inventory.
/// </summary>
/// <param name="uid"></param>
/// <param name="type">The type of inventory the item is from</param>
/// <param name="itemId">The prototype ID of the item</param>
/// <param name="throwItem">Whether the item should be thrown in a random direction after ejection</param>
/// <param name="vendComponent"></param>
public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
if (vendComponent.Ejecting || vendComponent.Broken || !this.IsPowered(uid, EntityManager))
{
return;
}
var entry = GetEntry(uid, itemId, type, vendComponent);
if (entry == null)
{
Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-invalid-item"), uid);
Deny(uid, vendComponent);
return;
}
if (entry.Amount <= 0)
{
Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-out-of-stock"), uid);
Deny(uid, vendComponent);
return;
}
if (string.IsNullOrEmpty(entry.ID))
return;
// Start Ejecting, and prevent users from ordering while anim playing
vendComponent.Ejecting = true;
vendComponent.NextItemToEject = entry.ID;
vendComponent.ThrowNextItem = throwItem;
if (TryComp(uid, out SpeakOnUIClosedComponent? speakComponent))
_speakOnUIClosed.TrySetFlag((uid, speakComponent));
entry.Amount--;
Dirty(uid, vendComponent);
TryUpdateVisualState(uid, vendComponent);
Audio.PlayPvs(vendComponent.SoundVend, uid);
}
/// <summary>
/// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true
/// </summary>
/// <param name="uid"></param>
/// <param name="sender">Entity that is trying to use the vending machine</param>
/// <param name="type">The type of inventory the item is from</param>
/// <param name="itemId">The prototype ID of the item</param>
/// <param name="component"></param>
public void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component)
{
if (IsAuthorized(uid, sender, component))
{
TryEjectVendorItem(uid, type, itemId, component.CanShoot, component);
}
}
/// <summary>
/// Tries to update the visuals of the component based on its current state.
/// </summary>
public void TryUpdateVisualState(EntityUid uid, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
var finalState = VendingMachineVisualState.Normal;
if (vendComponent.Broken)
{
finalState = VendingMachineVisualState.Broken;
}
else if (vendComponent.Ejecting)
{
finalState = VendingMachineVisualState.Eject;
}
else if (vendComponent.Denying)
{
finalState = VendingMachineVisualState.Deny;
}
else if (!this.IsPowered(uid, EntityManager))
{
finalState = VendingMachineVisualState.Off;
}
if (_light.TryGetLight(uid, out var pointlight))
{
var lightState = finalState != VendingMachineVisualState.Broken && finalState != VendingMachineVisualState.Off;
_light.SetEnabled(uid, lightState, pointlight);
}
_appearanceSystem.SetData(uid, VendingMachineVisuals.VisualState, finalState);
}
/// <summary> /// <summary>
/// Ejects a random item from the available stock. Will do nothing if the vending machine is empty. /// Ejects a random item from the available stock. Will do nothing if the vending machine is empty.
/// </summary> /// </summary>
@@ -367,18 +202,18 @@ namespace Content.Server.VendingMachines
} }
else else
{ {
TryEjectVendorItem(uid, item.Type, item.ID, throwItem, vendComponent); TryEjectVendorItem(uid, item.Type, item.ID, throwItem, user: null, vendComponent: vendComponent);
} }
} }
private void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false) protected override void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false)
{ {
if (!Resolve(uid, ref vendComponent)) if (!Resolve(uid, ref vendComponent))
return; return;
// No need to update the visual state because we never changed it during a forced eject // No need to update the visual state because we never changed it during a forced eject
if (!forceEject) if (!forceEject)
TryUpdateVisualState(uid, vendComponent); TryUpdateVisualState((uid, vendComponent));
if (string.IsNullOrEmpty(vendComponent.NextItemToEject)) if (string.IsNullOrEmpty(vendComponent.NextItemToEject))
{ {
@@ -411,68 +246,17 @@ namespace Content.Server.VendingMachines
vendComponent.ThrowNextItem = false; vendComponent.ThrowNextItem = false;
} }
private VendingMachineInventoryEntry? GetEntry(EntityUid uid, string entryId, InventoryType type, VendingMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return null;
if (type == InventoryType.Emagged && _emag.CheckFlag(uid, EmagType.Interaction))
return component.EmaggedInventory.GetValueOrDefault(entryId);
if (type == InventoryType.Contraband && component.Contraband)
return component.ContrabandInventory.GetValueOrDefault(entryId);
return component.Inventory.GetValueOrDefault(entryId);
}
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(frameTime); base.Update(frameTime);
var query = EntityQueryEnumerator<VendingMachineComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.Ejecting)
{
comp.EjectAccumulator += frameTime;
if (comp.EjectAccumulator >= comp.EjectDelay)
{
comp.EjectAccumulator = 0f;
comp.Ejecting = false;
EjectItem(uid, comp);
}
}
if (comp.Denying)
{
comp.DenyAccumulator += frameTime;
if (comp.DenyAccumulator >= comp.DenyDelay)
{
comp.DenyAccumulator = 0f;
comp.Denying = false;
TryUpdateVisualState(uid, comp);
}
}
if (comp.DispenseOnHitCoolingDown)
{
comp.DispenseOnHitAccumulator += frameTime;
if (comp.DispenseOnHitAccumulator >= comp.DispenseOnHitCooldown)
{
comp.DispenseOnHitAccumulator = 0f;
comp.DispenseOnHitCoolingDown = false;
}
}
}
var disabled = EntityQueryEnumerator<EmpDisabledComponent, VendingMachineComponent>(); var disabled = EntityQueryEnumerator<EmpDisabledComponent, VendingMachineComponent>();
while (disabled.MoveNext(out var uid, out _, out var comp)) while (disabled.MoveNext(out var uid, out _, out var comp))
{ {
if (comp.NextEmpEject < _timing.CurTime) if (comp.NextEmpEject < _timing.CurTime)
{ {
EjectRandom(uid, true, false, comp); EjectRandom(uid, true, false, comp);
comp.NextEmpEject += TimeSpan.FromSeconds(5 * comp.EjectDelay); comp.NextEmpEject += (5 * comp.EjectDelay);
} }
} }
} }
@@ -485,7 +269,7 @@ namespace Content.Server.VendingMachines
RestockInventoryFromPrototype(uid, vendComponent); RestockInventoryFromPrototype(uid, vendComponent);
Dirty(uid, vendComponent); Dirty(uid, vendComponent);
TryUpdateVisualState(uid, vendComponent); TryUpdateVisualState((uid, vendComponent));
} }
private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args) private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args)

View File

@@ -1,13 +1,15 @@
using Content.Shared.Advertise.Systems;
using Content.Shared.Dataset; using Content.Shared.Dataset;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Server.Advertise.Components; namespace Content.Shared.Advertise.Components;
/// <summary> /// <summary>
/// Causes the entity to speak using the Chat system when its ActivatableUI is closed, optionally /// Causes the entity to speak using the Chat system when its ActivatableUI is closed, optionally
/// requiring that a Flag be set as well. /// requiring that a Flag be set as well.
/// </summary> /// </summary>
[RegisterComponent, Access(typeof(SpeakOnUIClosedSystem))] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedSpeakOnUIClosedSystem))]
public sealed partial class SpeakOnUIClosedComponent : Component public sealed partial class SpeakOnUIClosedComponent : Component
{ {
/// <summary> /// <summary>
@@ -31,6 +33,6 @@ public sealed partial class SpeakOnUIClosedComponent : Component
/// <summary> /// <summary>
/// State variable only used if <see cref="RequireFlag"/> is true. Set with <see cref="SpeakOnUIClosedSystem.TrySetFlag"/>. /// State variable only used if <see cref="RequireFlag"/> is true. Set with <see cref="SpeakOnUIClosedSystem.TrySetFlag"/>.
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public bool Flag; public bool Flag;
} }

View File

@@ -0,0 +1,16 @@
using SpeakOnUIClosedComponent = Content.Shared.Advertise.Components.SpeakOnUIClosedComponent;
namespace Content.Shared.Advertise.Systems;
public abstract class SharedSpeakOnUIClosedSystem : EntitySystem
{
public bool TrySetFlag(Entity<SpeakOnUIClosedComponent?> entity, bool value = true)
{
if (!Resolve(entity, ref entity.Comp))
return false;
entity.Comp.Flag = value;
Dirty(entity);
return true;
}
}

View File

@@ -67,7 +67,7 @@ public abstract partial class SharedVendingMachineSystem
args.Handled = true; args.Handled = true;
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, (float) component.RestockDelay.TotalSeconds, new RestockDoAfterEvent(), target, var doAfterArgs = new DoAfterArgs(EntityManager, args.User, (float)component.RestockDelay.TotalSeconds, new RestockDoAfterEvent(), target,
target: target, used: uid) target: target, used: uid)
{ {
BreakOnMove = true, BreakOnMove = true,

View File

@@ -1,34 +1,143 @@
using Content.Shared.Emag.Components; using Content.Shared.Emag.Components;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using System.Linq; using System.Linq;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Advertise.Components;
using Content.Shared.Advertise.Systems;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Content.Shared.Emag.Systems; using Content.Shared.Emag.Systems;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Power.EntitySystems;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Shared.VendingMachines; namespace Content.Shared.VendingMachines;
public abstract partial class SharedVendingMachineSystem : EntitySystem public abstract partial class SharedVendingMachineSystem : EntitySystem
{ {
[Dependency] private readonly INetManager _net = default!; [Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!; [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!; [Dependency] protected readonly SharedAudioSystem Audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] protected readonly SharedPointLightSystem Light = default!;
[Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!; [Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] private readonly SharedSpeakOnUIClosedSystem _speakOn = default!;
[Dependency] protected readonly SharedUserInterfaceSystem UISystem = default!;
[Dependency] protected readonly IRobustRandom Randomizer = default!; [Dependency] protected readonly IRobustRandom Randomizer = default!;
[Dependency] private readonly EmagSystem _emag = default!; [Dependency] private readonly EmagSystem _emag = default!;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<VendingMachineComponent, ComponentGetState>(OnVendingGetState);
SubscribeLocalEvent<VendingMachineComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<VendingMachineComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged); SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<VendingMachineRestockComponent, AfterInteractEvent>(OnAfterInteract); SubscribeLocalEvent<VendingMachineRestockComponent, AfterInteractEvent>(OnAfterInteract);
Subs.BuiEvents<VendingMachineComponent>(VendingMachineUiKey.Key, subs =>
{
subs.Event<VendingMachineEjectMessage>(OnInventoryEjectMessage);
});
}
private void OnVendingGetState(Entity<VendingMachineComponent> entity, ref ComponentGetState args)
{
var component = entity.Comp;
var inventory = new Dictionary<string, VendingMachineInventoryEntry>();
var emaggedInventory = new Dictionary<string, VendingMachineInventoryEntry>();
var contrabandInventory = new Dictionary<string, VendingMachineInventoryEntry>();
foreach (var weh in component.Inventory)
{
inventory[weh.Key] = new(weh.Value);
}
foreach (var weh in component.EmaggedInventory)
{
emaggedInventory[weh.Key] = new(weh.Value);
}
foreach (var weh in component.ContrabandInventory)
{
contrabandInventory[weh.Key] = new(weh.Value);
}
args.State = new VendingMachineComponentState()
{
Inventory = inventory,
EmaggedInventory = emaggedInventory,
ContrabandInventory = contrabandInventory,
Contraband = component.Contraband,
EjectEnd = component.EjectEnd,
DenyEnd = component.DenyEnd,
DispenseOnHitEnd = component.DispenseOnHitEnd,
};
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<VendingMachineComponent>();
var curTime = Timing.CurTime;
while (query.MoveNext(out var uid, out var comp))
{
if (comp.Ejecting)
{
if (curTime > comp.EjectEnd)
{
comp.EjectEnd = null;
Dirty(uid, comp);
EjectItem(uid, comp);
UpdateUI((uid, comp));
}
}
if (comp.Denying)
{
if (curTime > comp.DenyEnd)
{
comp.DenyEnd = null;
Dirty(uid, comp);
TryUpdateVisualState((uid, comp));
}
}
if (comp.DispenseOnHitCoolingDown)
{
if (curTime > comp.DispenseOnHitEnd)
{
comp.DispenseOnHitEnd = null;
Dirty(uid, comp);
}
}
}
}
private void OnInventoryEjectMessage(Entity<VendingMachineComponent> entity, ref VendingMachineEjectMessage args)
{
if (!_receiver.IsPowered(entity.Owner) || Deleted(entity))
return;
if (args.Actor is not { Valid: true } actor)
return;
AuthorizedVend(entity.Owner, actor, args.Type, args.ID, entity.Comp);
} }
protected virtual void OnMapInit(EntityUid uid, VendingMachineComponent component, MapInitEvent args) protected virtual void OnMapInit(EntityUid uid, VendingMachineComponent component, MapInitEvent args)
@@ -36,6 +145,162 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
RestockInventoryFromPrototype(uid, component, component.InitialStockQuality); RestockInventoryFromPrototype(uid, component, component.InitialStockQuality);
} }
protected virtual void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false) { }
/// <summary>
/// Checks if the user is authorized to use this vending machine
/// </summary>
/// <param name="uid"></param>
/// <param name="sender">Entity trying to use the vending machine</param>
/// <param name="vendComponent"></param>
public bool IsAuthorized(EntityUid uid, EntityUid sender, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return false;
if (!TryComp<AccessReaderComponent>(uid, out var accessReader))
return true;
if (_accessReader.IsAllowed(sender, uid, accessReader) || HasComp<EmaggedComponent>(uid))
return true;
Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid, sender);
Deny((uid, vendComponent), sender);
return false;
}
protected VendingMachineInventoryEntry? GetEntry(EntityUid uid, string entryId, InventoryType type, VendingMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return null;
if (type == InventoryType.Emagged && HasComp<EmaggedComponent>(uid))
return component.EmaggedInventory.GetValueOrDefault(entryId);
if (type == InventoryType.Contraband && component.Contraband)
return component.ContrabandInventory.GetValueOrDefault(entryId);
return component.Inventory.GetValueOrDefault(entryId);
}
/// <summary>
/// Tries to eject the provided item. Will do nothing if the vending machine is incapable of ejecting, already ejecting
/// or the item doesn't exist in its inventory.
/// </summary>
/// <param name="uid"></param>
/// <param name="type">The type of inventory the item is from</param>
/// <param name="itemId">The prototype ID of the item</param>
/// <param name="throwItem">Whether the item should be thrown in a random direction after ejection</param>
/// <param name="vendComponent"></param>
public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, EntityUid? user = null, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
if (vendComponent.Ejecting || vendComponent.Broken || !_receiver.IsPowered(uid))
{
return;
}
var entry = GetEntry(uid, itemId, type, vendComponent);
if (string.IsNullOrEmpty(entry?.ID))
{
Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-invalid-item"), uid);
Deny((uid, vendComponent));
return;
}
if (entry.Amount <= 0)
{
Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-out-of-stock"), uid);
Deny((uid, vendComponent));
return;
}
// Start Ejecting, and prevent users from ordering while anim playing
vendComponent.EjectEnd = Timing.CurTime + vendComponent.EjectDelay;
vendComponent.NextItemToEject = entry.ID;
vendComponent.ThrowNextItem = throwItem;
if (TryComp(uid, out SpeakOnUIClosedComponent? speakComponent))
_speakOn.TrySetFlag((uid, speakComponent));
entry.Amount--;
Dirty(uid, vendComponent);
UpdateUI((uid, vendComponent));
TryUpdateVisualState((uid, vendComponent));
Audio.PlayPredicted(vendComponent.SoundVend, uid, user);
}
public void Deny(Entity<VendingMachineComponent?> entity, EntityUid? user = null)
{
if (!Resolve(entity.Owner, ref entity.Comp))
return;
if (entity.Comp.Denying)
return;
entity.Comp.DenyEnd = Timing.CurTime + entity.Comp.DenyDelay;
Audio.PlayPredicted(entity.Comp.SoundDeny, entity.Owner, user, AudioParams.Default.WithVolume(-2f));
TryUpdateVisualState(entity);
Dirty(entity);
}
protected virtual void UpdateUI(Entity<VendingMachineComponent?> entity) { }
/// <summary>
/// Tries to update the visuals of the component based on its current state.
/// </summary>
public void TryUpdateVisualState(Entity<VendingMachineComponent?> entity)
{
if (!Resolve(entity.Owner, ref entity.Comp))
return;
var finalState = VendingMachineVisualState.Normal;
if (entity.Comp.Broken)
{
finalState = VendingMachineVisualState.Broken;
}
else if (entity.Comp.Ejecting)
{
finalState = VendingMachineVisualState.Eject;
}
else if (entity.Comp.Denying)
{
finalState = VendingMachineVisualState.Deny;
}
else if (!_receiver.IsPowered(entity.Owner))
{
finalState = VendingMachineVisualState.Off;
}
// TODO: You know this should really live on the client with netsync off because client knows the state.
if (Light.TryGetLight(entity.Owner, out var pointlight))
{
var lightEnabled = finalState != VendingMachineVisualState.Broken && finalState != VendingMachineVisualState.Off;
Light.SetEnabled(entity.Owner, lightEnabled, pointlight);
}
_appearanceSystem.SetData(entity.Owner, VendingMachineVisuals.VisualState, finalState);
}
/// <summary>
/// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true
/// </summary>
/// <param name="uid"></param>
/// <param name="sender">Entity that is trying to use the vending machine</param>
/// <param name="type">The type of inventory the item is from</param>
/// <param name="itemId">The prototype ID of the item</param>
/// <param name="component"></param>
public void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component)
{
if (IsAuthorized(uid, sender, component))
{
TryEjectVendorItem(uid, type, itemId, component.CanShoot, sender, component);
}
}
public void RestockInventoryFromPrototype(EntityUid uid, public void RestockInventoryFromPrototype(EntityUid uid,
VendingMachineComponent? component = null, float restockQuality = 1f) VendingMachineComponent? component = null, float restockQuality = 1f)
{ {

View File

@@ -1,19 +1,19 @@
using Content.Shared.Actions; using Content.Shared.Actions;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.VendingMachines namespace Content.Shared.VendingMachines
{ {
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] [RegisterComponent, NetworkedComponent, AutoGenerateComponentPause]
public sealed partial class VendingMachineComponent : Component public sealed partial class VendingMachineComponent : Component
{ {
/// <summary> /// <summary>
/// PrototypeID for the vending machine's inventory, see <see cref="VendingMachineInventoryPrototype"/> /// PrototypeID for the vending machine's inventory, see <see cref="VendingMachineInventoryPrototype"/>
/// </summary> /// </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)] [DataField("pack", customTypeSerializer: typeof(PrototypeIdSerializer<VendingMachineInventoryPrototype>), required: true)]
public string PackPrototypeId = string.Empty; public string PackPrototypeId = string.Empty;
@@ -22,7 +22,7 @@ namespace Content.Shared.VendingMachines
/// Used by the client to determine how long the deny animation should be played. /// Used by the client to determine how long the deny animation should be played.
/// </summary> /// </summary>
[DataField] [DataField]
public float DenyDelay = 2.0f; public TimeSpan DenyDelay = TimeSpan.FromSeconds(2);
/// <summary> /// <summary>
/// Used by the server to determine how long the vending machine stays in the "Eject" state. /// Used by the server to determine how long the vending machine stays in the "Eject" state.
@@ -30,23 +30,40 @@ namespace Content.Shared.VendingMachines
/// Used by the client to determine how long the deny animation should be played. /// Used by the client to determine how long the deny animation should be played.
/// </summary> /// </summary>
[DataField] [DataField]
public float EjectDelay = 1.2f; public TimeSpan EjectDelay = TimeSpan.FromSeconds(1.2);
[DataField, AutoNetworkedField] [DataField]
public Dictionary<string, VendingMachineInventoryEntry> Inventory = new(); public Dictionary<string, VendingMachineInventoryEntry> Inventory = new();
[DataField, AutoNetworkedField] [DataField]
public Dictionary<string, VendingMachineInventoryEntry> EmaggedInventory = new(); public Dictionary<string, VendingMachineInventoryEntry> EmaggedInventory = new();
[DataField, AutoNetworkedField] [DataField]
public Dictionary<string, VendingMachineInventoryEntry> ContrabandInventory = new(); public Dictionary<string, VendingMachineInventoryEntry> ContrabandInventory = new();
[DataField, AutoNetworkedField] /// <summary>
/// If true then unlocks the <see cref="ContrabandInventory"/>
/// </summary>
[DataField]
public bool Contraband; public bool Contraband;
public bool Ejecting; [ViewVariables]
public bool Denying; public bool Ejecting => EjectEnd != null;
public bool DispenseOnHitCoolingDown;
[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; public string? NextItemToEject;
@@ -55,7 +72,7 @@ namespace Content.Shared.VendingMachines
/// <summary> /// <summary>
/// When true, will forcefully throw any object it dispenses /// When true, will forcefully throw any object it dispenses
/// </summary> /// </summary>
[DataField("speedLimiter")] [DataField]
public bool CanShoot = false; public bool CanShoot = false;
public bool ThrowNextItem = false; public bool ThrowNextItem = false;
@@ -64,14 +81,14 @@ namespace Content.Shared.VendingMachines
/// The chance that a vending machine will randomly dispense an item on hit. /// The chance that a vending machine will randomly dispense an item on hit.
/// Chance is 0 if null. /// Chance is 0 if null.
/// </summary> /// </summary>
[DataField("dispenseOnHitChance")] [DataField]
public float? DispenseOnHitChance; public float? DispenseOnHitChance;
/// <summary> /// <summary>
/// The minimum amount of damage that must be done per hit to have a chance /// The minimum amount of damage that must be done per hit to have a chance
/// of dispensing an item. /// of dispensing an item.
/// </summary> /// </summary>
[DataField("dispenseOnHitThreshold")] [DataField]
public float? DispenseOnHitThreshold; public float? DispenseOnHitThreshold;
/// <summary> /// <summary>
@@ -80,13 +97,13 @@ namespace Content.Shared.VendingMachines
/// 0 for a vending machine for legitimate reasons (no desired delay/no eject animation) /// 0 for a vending machine for legitimate reasons (no desired delay/no eject animation)
/// and can be circumvented with forced ejections. /// and can be circumvented with forced ejections.
/// </summary> /// </summary>
[DataField("dispenseOnHitCooldown")] [DataField]
public float? DispenseOnHitCooldown = 1.0f; public TimeSpan? DispenseOnHitCooldown = TimeSpan.FromSeconds(1.0);
/// <summary> /// <summary>
/// Sound that plays when ejecting an item /// Sound that plays when ejecting an item
/// </summary> /// </summary>
[DataField("soundVend")] [DataField]
// Grabbed from: https://github.com/tgstation/tgstation/blob/d34047a5ae911735e35cd44a210953c9563caa22/sound/machines/machine_vend.ogg // Grabbed from: https://github.com/tgstation/tgstation/blob/d34047a5ae911735e35cd44a210953c9563caa22/sound/machines/machine_vend.ogg
public SoundSpecifier SoundVend = new SoundPathSpecifier("/Audio/Machines/machine_vend.ogg") public SoundSpecifier SoundVend = new SoundPathSpecifier("/Audio/Machines/machine_vend.ogg")
{ {
@@ -100,7 +117,7 @@ namespace Content.Shared.VendingMachines
/// <summary> /// <summary>
/// Sound that plays when an item can't be ejected /// Sound that plays when an item can't be ejected
/// </summary> /// </summary>
[DataField("soundDeny")] [DataField]
// Yoinked from: https://github.com/discordia-space/CEV-Eris/blob/35bbad6764b14e15c03a816e3e89aa1751660ba9/sound/machines/Custom_deny.ogg // 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 SoundSpecifier SoundDeny = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
@@ -108,10 +125,6 @@ namespace Content.Shared.VendingMachines
public float NonLimitedEjectRange = 5f; public float NonLimitedEjectRange = 5f;
public float EjectAccumulator = 0f;
public float DenyAccumulator = 0f;
public float DispenseOnHitAccumulator = 0f;
/// <summary> /// <summary>
/// The quality of the stock in the vending machine on spawn. /// 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. /// Represents the percentage chance (0.0f = 0%, 1.0f = 100%) each set of items in the machine is fully-stocked.
@@ -123,7 +136,7 @@ namespace Content.Shared.VendingMachines
/// <summary> /// <summary>
/// While disabled by EMP it randomly ejects items /// While disabled by EMP it randomly ejects items
/// </summary> /// </summary>
[DataField("nextEmpEject", customTypeSerializer: typeof(TimeOffsetSerializer))] [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextEmpEject = TimeSpan.Zero; public TimeSpan NextEmpEject = TimeSpan.Zero;
#region Client Visuals #region Client Visuals
@@ -131,28 +144,28 @@ namespace Content.Shared.VendingMachines
/// RSI state for when the vending machine is unpowered. /// RSI state for when the vending machine is unpowered.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Base"/> /// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Base"/>
/// </summary> /// </summary>
[DataField("offState")] [DataField]
public string? OffState; public string? OffState;
/// <summary> /// <summary>
/// RSI state for the screen of the vending machine /// RSI state for the screen of the vending machine
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Screen"/> /// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Screen"/>
/// </summary> /// </summary>
[DataField("screenState")] [DataField]
public string? ScreenState; public string? ScreenState;
/// <summary> /// <summary>
/// RSI state for the vending machine's normal state. Usually a looping animation. /// RSI state for the vending machine's normal state. Usually a looping animation.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/> /// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
/// </summary> /// </summary>
[DataField("normalState")] [DataField]
public string? NormalState; public string? NormalState;
/// <summary> /// <summary>
/// RSI state for the vending machine's eject animation. /// RSI state for the vending machine's eject animation.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/> /// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
/// </summary> /// </summary>
[DataField("ejectState")] [DataField]
public string? EjectState; public string? EjectState;
/// <summary> /// <summary>
@@ -160,14 +173,14 @@ namespace Content.Shared.VendingMachines
/// or looped depending on how <see cref="LoopDenyAnimation"/> is set. /// or looped depending on how <see cref="LoopDenyAnimation"/> is set.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/> /// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
/// </summary> /// </summary>
[DataField("denyState")] [DataField]
public string? DenyState; public string? DenyState;
/// <summary> /// <summary>
/// RSI state for when the vending machine is unpowered. /// RSI state for when the vending machine is unpowered.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Base"/> /// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Base"/>
/// </summary> /// </summary>
[DataField("brokenState")] [DataField]
public string? BrokenState; public string? BrokenState;
/// <summary> /// <summary>
@@ -195,6 +208,13 @@ namespace Content.Shared.VendingMachines
ID = id; ID = id;
Amount = amount; Amount = amount;
} }
public VendingMachineInventoryEntry(VendingMachineInventoryEntry entry)
{
Type = entry.Type;
ID = entry.ID;
Amount = entry.Amount;
}
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
@@ -206,13 +226,13 @@ namespace Content.Shared.VendingMachines
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
public enum VendingMachineVisuals public enum VendingMachineVisuals : byte
{ {
VisualState VisualState
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
public enum VendingMachineVisualState public enum VendingMachineVisualState : byte
{ {
Normal, Normal,
Off, Off,
@@ -254,4 +274,22 @@ namespace Content.Shared.VendingMachines
{ {
}; };
[Serializable, NetSerializable]
public sealed class VendingMachineComponentState : ComponentState
{
public Dictionary<string, VendingMachineInventoryEntry> Inventory = new();
public Dictionary<string, VendingMachineInventoryEntry> EmaggedInventory = new();
public Dictionary<string, VendingMachineInventoryEntry> ContrabandInventory = new();
public bool Contraband;
public TimeSpan? EjectEnd;
public TimeSpan? DenyEnd;
public TimeSpan? DispenseOnHitEnd;
}
} }