Make vending machine restocks predicted (and its sound not spammable) (#38609)

* feat: make vending machine restocks predicted

* refactor: VendingMachineRestockComponent cleanup

* refactor: minor simplification

* revert: refactor: minor simplification; load bearing IsFirstTimePredicted

lol second guessed myself

* chore: unneeded VendingMachineSystem dep

* Update Content.Shared/VendingMachines/VendingMachineComponent.cs

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
This commit is contained in:
Perry Fraser
2025-09-11 14:27:54 -04:00
committed by GitHub
parent 77eca4a570
commit fbf65b7f74
5 changed files with 77 additions and 70 deletions

View File

@@ -8,20 +8,14 @@ using Content.Server.Vocalization.Systems;
using Content.Shared.Cargo; using Content.Shared.Cargo;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Destructible; using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.Emp; using Content.Shared.Emp;
using Content.Shared.IdentityManagement;
using Content.Shared.Popups;
using Content.Shared.Power; using Content.Shared.Power;
using Content.Shared.Throwing; 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.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.VendingMachines namespace Content.Server.VendingMachines
{ {
@@ -30,7 +24,6 @@ namespace Content.Server.VendingMachines
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = 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!;
private const float WallVendEjectDistanceFromWall = 1f; private const float WallVendEjectDistanceFromWall = 1f;
@@ -46,11 +39,8 @@ namespace Content.Server.VendingMachines
SubscribeLocalEvent<VendingMachineComponent, TryVocalizeEvent>(OnTryVocalize); SubscribeLocalEvent<VendingMachineComponent, TryVocalizeEvent>(OnTryVocalize);
SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt); SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt);
SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense); SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense);
SubscribeLocalEvent<VendingMachineComponent, RestockDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<VendingMachineRestockComponent, PriceCalculationEvent>(OnPriceCalculation); SubscribeLocalEvent<VendingMachineRestockComponent, PriceCalculationEvent>(OnPriceCalculation);
} }
@@ -133,30 +123,6 @@ namespace Content.Server.VendingMachines
EjectRandom(uid, throwItem: true, forceEject: false, component); EjectRandom(uid, throwItem: true, forceEject: false, component);
} }
private void OnDoAfter(EntityUid uid, VendingMachineComponent component, DoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Used == null)
return;
if (!TryComp<VendingMachineRestockComponent>(args.Args.Used, out var restockComponent))
{
Log.Error($"{ToPrettyString(args.Args.User)} tried to restock {ToPrettyString(uid)} with {ToPrettyString(args.Args.Used.Value)} which did not have a VendingMachineRestockComponent.");
return;
}
TryRestockInventory(uid, component);
Popup.PopupEntity(Loc.GetString("vending-machine-restock-done-self", ("target", uid)), args.Args.User, args.Args.User, PopupType.Medium);
var othersFilter = Filter.PvsExcept(args.Args.User);
Popup.PopupEntity(Loc.GetString("vending-machine-restock-done-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", uid)), args.Args.User, othersFilter, true, PopupType.Medium);
Audio.PlayPvs(restockComponent.SoundRestockDone, uid, AudioParams.Default.WithVolume(-2f).WithVariation(0.2f));
Del(args.Args.Used.Value);
args.Handled = true;
}
/// <summary> /// <summary>
/// Sets the <see cref="VendingMachineComponent.CanShoot"/> property of the vending machine. /// Sets the <see cref="VendingMachineComponent.CanShoot"/> property of the vending machine.
/// </summary> /// </summary>
@@ -259,7 +225,7 @@ namespace Content.Server.VendingMachines
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 += (5 * comp.EjectDelay); comp.NextEmpEject += (5 * comp.EjectDelay);
@@ -267,17 +233,6 @@ namespace Content.Server.VendingMachines
} }
} }
public void TryRestockInventory(EntityUid uid, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
RestockInventoryFromPrototype(uid, vendComponent);
Dirty(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)
{ {
List<double> priceSets = new(); List<double> priceSets = new();
@@ -308,7 +263,7 @@ namespace Content.Server.VendingMachines
{ {
args.Affected = true; args.Affected = true;
args.Disabled = true; args.Disabled = true;
component.NextEmpEject = _timing.CurTime; component.NextEmpEject = Timing.CurTime;
} }
} }

View File

@@ -3,7 +3,6 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Wires; using Content.Shared.Wires;
using Robust.Shared.Audio;
namespace Content.Shared.VendingMachines; namespace Content.Shared.VendingMachines;
@@ -46,6 +45,17 @@ public abstract partial class SharedVendingMachineSystem
return true; return true;
} }
public void TryRestockInventory(EntityUid uid, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
RestockInventoryFromPrototype(uid, vendComponent);
Dirty(uid, vendComponent);
TryUpdateVisualState((uid, vendComponent));
}
private void OnAfterInteract(EntityUid uid, VendingMachineRestockComponent component, AfterInteractEvent args) private void OnAfterInteract(EntityUid uid, VendingMachineRestockComponent component, AfterInteractEvent args)
{ {
if (args.Target is not { } target || !args.CanReach || args.Handled) if (args.Target is not { } target || !args.CanReach || args.Handled)
@@ -62,8 +72,13 @@ 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,
target: target, used: uid) args.User,
component.RestockDelay,
new RestockDoAfterEvent(),
target,
target: target,
used: uid)
{ {
BreakOnMove = true, BreakOnMove = true,
BreakOnDamage = true, BreakOnDamage = true,
@@ -74,13 +89,48 @@ public abstract partial class SharedVendingMachineSystem
return; return;
var selfMessage = Loc.GetString("vending-machine-restock-start-self", ("target", target)); var selfMessage = Loc.GetString("vending-machine-restock-start-self", ("target", target));
var othersMessage = Loc.GetString("vending-machine-restock-start-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", target)); var othersMessage = Loc.GetString("vending-machine-restock-start-others",
Popup.PopupPredicted(selfMessage, ("user", Identity.Entity(args.User, EntityManager)),
othersMessage, ("target", target));
uid, Popup.PopupPredicted(selfMessage, othersMessage, target, args.User, PopupType.Medium);
args.User,
PopupType.Medium);
Audio.PlayPredicted(component.SoundRestockStart, uid, args.User);
if (!Timing.IsFirstTimePredicted)
return;
Audio.Stop(machineComponent.RestockStream);
machineComponent.RestockStream = Audio.PlayPredicted(component.SoundRestockStart, target, args.User)?.Entity;
}
private void OnRestockDoAfter(Entity<VendingMachineComponent> ent, ref RestockDoAfterEvent args)
{
if (args.Cancelled)
{
// Future predicted ticks can clobber the RestockStream with null while not stopping anything
if (Timing.IsFirstTimePredicted)
ent.Comp.RestockStream = Audio.Stop(ent.Comp.RestockStream);
return;
}
if (args.Handled || args.Used == null)
return;
if (!TryComp<VendingMachineRestockComponent>(args.Used, out var restockComponent))
{
Log.Error($"{ToPrettyString(args.User)} tried to restock {ToPrettyString(ent)} with {ToPrettyString(args.Used.Value)} which did not have a VendingMachineRestockComponent.");
return;
}
TryRestockInventory(ent, ent.Comp);
var userMessage = Loc.GetString("vending-machine-restock-done-self", ("target", ent));
var othersMessage = Loc.GetString("vending-machine-restock-done-others",
("user", Identity.Entity(args.User, EntityManager)),
("target", ent));
Popup.PopupPredicted(userMessage, othersMessage, ent, args.User, PopupType.Medium);
Audio.PlayPredicted(restockComponent.SoundRestockDone, ent, args.User);
PredictedQueueDel(args.Used.Value);
} }
} }

View File

@@ -41,6 +41,7 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
SubscribeLocalEvent<VendingMachineComponent, ComponentGetState>(OnVendingGetState); SubscribeLocalEvent<VendingMachineComponent, ComponentGetState>(OnVendingGetState);
SubscribeLocalEvent<VendingMachineComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<VendingMachineComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged); SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<VendingMachineComponent, RestockDoAfterEvent>(OnRestockDoAfter);
SubscribeLocalEvent<VendingMachineRestockComponent, AfterInteractEvent>(OnAfterInteract); SubscribeLocalEvent<VendingMachineRestockComponent, AfterInteractEvent>(OnAfterInteract);

View File

@@ -139,6 +139,12 @@ namespace Content.Shared.VendingMachines
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextEmpEject = TimeSpan.Zero; public TimeSpan NextEmpEject = TimeSpan.Zero;
/// <summary>
/// Audio entity used during restock in case the doafter gets canceled.
/// </summary>
[DataField]
public EntityUid? RestockStream;
#region Client Visuals #region Client Visuals
/// <summary> /// <summary>
/// RSI state for when the vending machine is unpowered. /// RSI state for when the vending machine is unpowered.

View File

@@ -12,23 +12,20 @@ public sealed partial class VendingMachineRestockComponent : Component
/// <summary> /// <summary>
/// The time (in seconds) that it takes to restock a machine. /// The time (in seconds) that it takes to restock a machine.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField]
[DataField("restockDelay")]
public TimeSpan RestockDelay = TimeSpan.FromSeconds(5.0f); public TimeSpan RestockDelay = TimeSpan.FromSeconds(5.0f);
/// <summary> /// <summary>
/// What sort of machine inventory does this restock? /// What sort of machine inventory does this restock?
/// This is checked against the VendingMachineComponent's pack value. /// This is checked against the VendingMachineComponent's pack value.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField(customTypeSerializer: typeof(PrototypeIdHashSetSerializer<VendingMachineInventoryPrototype>))]
[DataField("canRestock", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<VendingMachineInventoryPrototype>))] public HashSet<string> CanRestock = [];
public HashSet<string> CanRestock = new();
/// <summary> /// <summary>
/// Sound that plays when starting to restock a machine. /// Sound that plays when starting to restock a machine.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField]
[DataField("soundRestockStart")]
public SoundSpecifier SoundRestockStart = new SoundPathSpecifier("/Audio/Machines/vending_restock_start.ogg") public SoundSpecifier SoundRestockStart = new SoundPathSpecifier("/Audio/Machines/vending_restock_start.ogg")
{ {
Params = new AudioParams Params = new AudioParams
@@ -41,12 +38,10 @@ public sealed partial class VendingMachineRestockComponent : Component
/// <summary> /// <summary>
/// Sound that plays when finished restocking a machine. /// Sound that plays when finished restocking a machine.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField]
[DataField("soundRestockDone")] public SoundSpecifier SoundRestockDone = new SoundPathSpecifier("/Audio/Machines/vending_restock_done.ogg",
public SoundSpecifier SoundRestockDone = new SoundPathSpecifier("/Audio/Machines/vending_restock_done.ogg"); AudioParams.Default.WithVolume(-2f).WithVariation(0.2f));
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed partial class RestockDoAfterEvent : SimpleDoAfterEvent public sealed partial class RestockDoAfterEvent : SimpleDoAfterEvent;
{
}