2023-05-03 01:38:03 -04:00
using System.Linq ;
2023-07-08 14:08:32 +10:00
using System.Numerics ;
2022-09-15 11:53:17 +10:00
using Content.Server.Cargo.Systems ;
2023-05-07 01:26:04 +10:00
using Content.Server.Emp ;
2022-01-30 22:16:41 -06:00
using Content.Server.Power.Components ;
2022-05-27 10:36:12 +10:00
using Content.Server.Power.EntitySystems ;
2022-06-23 00:52:28 -04:00
using Content.Shared.Damage ;
2022-05-27 10:36:12 +10:00
using Content.Shared.Destructible ;
2023-02-24 19:01:25 -05:00
using Content.Shared.DoAfter ;
2023-05-07 01:26:04 +10:00
using Content.Shared.Emp ;
2023-01-01 18:42:56 -05:00
using Content.Shared.Popups ;
2024-08-25 22:18:42 +10:00
using Content.Shared.Power ;
2022-05-27 10:36:12 +10:00
using Content.Shared.Throwing ;
2024-02-01 11:45:24 +03:00
using Content.Shared.UserInterface ;
2022-01-30 22:16:41 -06:00
using Content.Shared.VendingMachines ;
2024-05-29 02:02:15 +12:00
using Content.Shared.Wall ;
2022-05-27 10:36:12 +10:00
using Robust.Shared.Audio ;
2022-01-30 22:16:41 -06:00
using Robust.Shared.Prototypes ;
using Robust.Shared.Random ;
2023-05-07 01:26:04 +10:00
using Robust.Shared.Timing ;
2022-01-30 22:16:41 -06:00
2022-05-27 10:36:12 +10:00
namespace Content.Server.VendingMachines
2022-01-30 22:16:41 -06:00
{
2022-07-26 12:35:36 +12:00
public sealed class VendingMachineSystem : SharedVendingMachineSystem
2022-01-30 22:16:41 -06:00
{
[Dependency] private readonly IRobustRandom _random = default ! ;
2022-09-15 11:53:17 +10:00
[Dependency] private readonly PricingSystem _pricing = default ! ;
[Dependency] private readonly ThrowingSystem _throwingSystem = default ! ;
2023-05-07 01:26:04 +10:00
[Dependency] private readonly IGameTiming _timing = default ! ;
2022-09-15 11:53:17 +10:00
2024-05-29 02:02:15 +12:00
private const float WallVendEjectDistanceFromWall = 1f ;
2022-01-30 22:16:41 -06:00
public override void Initialize ( )
{
base . Initialize ( ) ;
2022-07-26 12:35:36 +12:00
2022-01-30 22:16:41 -06:00
SubscribeLocalEvent < VendingMachineComponent , PowerChangedEvent > ( OnPowerChanged ) ;
SubscribeLocalEvent < VendingMachineComponent , BreakageEventArgs > ( OnBreak ) ;
2024-06-14 20:13:52 -07:00
SubscribeLocalEvent < VendingMachineComponent , DamageChangedEvent > ( OnDamageChanged ) ;
2022-09-15 11:53:17 +10:00
SubscribeLocalEvent < VendingMachineComponent , PriceCalculationEvent > ( OnVendingPrice ) ;
2023-05-07 01:26:04 +10:00
SubscribeLocalEvent < VendingMachineComponent , EmpPulseEvent > ( OnEmpPulse ) ;
2022-06-23 00:52:28 -04:00
2022-08-31 14:12:09 +02:00
SubscribeLocalEvent < VendingMachineComponent , ActivatableUIOpenAttemptEvent > ( OnActivatableUIOpenAttempt ) ;
2024-01-14 08:18:39 +01:00
2022-06-23 00:52:28 -04:00
SubscribeLocalEvent < VendingMachineComponent , VendingMachineSelfDispenseEvent > ( OnSelfDispense ) ;
2023-01-01 18:42:56 -05:00
2023-04-03 13:13:48 +12:00
SubscribeLocalEvent < VendingMachineComponent , RestockDoAfterEvent > ( OnDoAfter ) ;
2023-05-03 01:38:03 -04:00
SubscribeLocalEvent < VendingMachineRestockComponent , PriceCalculationEvent > ( OnPriceCalculation ) ;
2022-01-30 22:16:41 -06:00
}
2022-09-15 11:53:17 +10:00
private void OnVendingPrice ( EntityUid uid , VendingMachineComponent component , ref PriceCalculationEvent args )
{
var price = 0.0 ;
2023-05-03 01:38:03 -04:00
foreach ( var entry in component . Inventory . Values )
2022-09-15 11:53:17 +10:00
{
2023-05-03 01:38:03 -04:00
if ( ! PrototypeManager . TryIndex < EntityPrototype > ( entry . ID , out var proto ) )
2022-09-15 11:53:17 +10:00
{
2024-03-17 21:30:27 +01:00
Log . Error ( $"Unable to find entity prototype {entry.ID} on {ToPrettyString(uid)} vending." ) ;
2022-09-15 11:53:17 +10:00
continue ;
}
2023-03-24 15:27:55 +11:00
price + = entry . Amount * _pricing . GetEstimatedPrice ( proto ) ;
2022-09-15 11:53:17 +10:00
}
args . Price + = price ;
}
2024-09-23 12:10:22 +10:00
protected override void OnMapInit ( EntityUid uid , VendingMachineComponent component , MapInitEvent args )
2022-01-30 22:16:41 -06:00
{
2024-09-23 12:10:22 +10:00
base . OnMapInit ( uid , component , args ) ;
2022-01-30 22:16:41 -06:00
2023-05-03 01:38:03 -04:00
if ( HasComp < ApcPowerReceiverComponent > ( uid ) )
2022-01-30 22:16:41 -06:00
{
2025-03-02 13:47:52 +11:00
TryUpdateVisualState ( ( uid , component ) ) ;
2022-01-30 22:16:41 -06:00
}
}
2022-08-31 14:12:09 +02:00
private void OnActivatableUIOpenAttempt ( EntityUid uid , VendingMachineComponent component , ActivatableUIOpenAttemptEvent args )
2022-01-30 22:16:41 -06:00
{
2022-08-31 14:12:09 +02:00
if ( component . Broken )
args . Cancel ( ) ;
}
2022-01-30 22:16:41 -06:00
2022-10-15 15:08:15 +11:00
private void OnPowerChanged ( EntityUid uid , VendingMachineComponent component , ref PowerChangedEvent args )
2022-01-30 22:16:41 -06:00
{
2025-03-02 13:47:52 +11:00
TryUpdateVisualState ( ( uid , component ) ) ;
2022-01-30 22:16:41 -06:00
}
private void OnBreak ( EntityUid uid , VendingMachineComponent vendComponent , BreakageEventArgs eventArgs )
{
vendComponent . Broken = true ;
2025-03-02 13:47:52 +11:00
TryUpdateVisualState ( ( uid , vendComponent ) ) ;
2022-01-30 22:16:41 -06:00
}
2024-06-14 20:13:52 -07:00
private void OnDamageChanged ( EntityUid uid , VendingMachineComponent component , DamageChangedEvent args )
2022-06-23 00:52:28 -04:00
{
2024-06-14 20:13:52 -07:00
if ( ! args . DamageIncreased & & component . Broken )
{
component . Broken = false ;
2025-03-02 13:47:52 +11:00
TryUpdateVisualState ( ( uid , component ) ) ;
2024-06-14 20:13:52 -07:00
return ;
}
2022-08-31 14:12:09 +02:00
if ( component . Broken | | component . DispenseOnHitCoolingDown | |
component . DispenseOnHitChance = = null | | args . DamageDelta = = null )
2022-06-23 00:52:28 -04:00
return ;
2024-01-22 02:59:14 +01:00
if ( args . DamageIncreased & & args . DamageDelta . GetTotal ( ) > = component . DispenseOnHitThreshold & &
2022-08-31 14:12:09 +02:00
_random . Prob ( component . DispenseOnHitChance . Value ) )
{
2025-03-02 13:47:52 +11:00
if ( component . DispenseOnHitCooldown ! = null )
{
component . DispenseOnHitEnd = Timing . CurTime + component . DispenseOnHitCooldown . Value ;
}
2022-08-31 14:12:09 +02:00
EjectRandom ( uid , throwItem : true , forceEject : true , component ) ;
}
2022-06-23 00:52:28 -04:00
}
private void OnSelfDispense ( EntityUid uid , VendingMachineComponent component , VendingMachineSelfDispenseEvent args )
{
if ( args . Handled )
return ;
args . Handled = true ;
2022-08-31 14:12:09 +02:00
EjectRandom ( uid , throwItem : true , forceEject : false , component ) ;
}
2023-02-24 19:01:25 -05:00
private void OnDoAfter ( EntityUid uid , VendingMachineComponent component , DoAfterEvent args )
2023-01-01 18:42:56 -05:00
{
2023-02-24 19:01:25 -05:00
if ( args . Handled | | args . Cancelled | | args . Args . Used = = null )
return ;
if ( ! TryComp < VendingMachineRestockComponent > ( args . Args . Used , out var restockComponent ) )
2023-01-01 18:42:56 -05:00
{
2024-03-17 21:30:27 +01:00
Log . Error ( $"{ToPrettyString(args.Args.User)} tried to restock {ToPrettyString(uid)} with {ToPrettyString(args.Args.Used.Value)} which did not have a VendingMachineRestockComponent." ) ;
2023-01-01 18:42:56 -05:00
return ;
}
TryRestockInventory ( uid , component ) ;
2023-05-03 01:38:03 -04:00
Popup . PopupEntity ( Loc . GetString ( "vending-machine-restock-done" , ( "this" , args . Args . Used ) , ( "user" , args . Args . User ) , ( "target" , uid ) ) , args . Args . User , PopupType . Medium ) ;
2023-02-24 19:01:25 -05:00
2023-05-03 01:38:03 -04:00
Audio . PlayPvs ( restockComponent . SoundRestockDone , uid , AudioParams . Default . WithVolume ( - 2f ) . WithVariation ( 0.2f ) ) ;
2023-01-01 18:42:56 -05:00
2023-02-24 19:01:25 -05:00
Del ( args . Args . Used . Value ) ;
2023-01-01 18:42:56 -05:00
2023-02-24 19:01:25 -05:00
args . Handled = true ;
2023-01-01 18:42:56 -05:00
}
2022-08-31 14:12:09 +02:00
/// <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 ;
2022-06-23 00:52:28 -04:00
}
2022-04-17 03:16:02 -04:00
2024-09-25 07:21:24 +02:00
/// <summary>
/// Sets the <see cref="VendingMachineComponent.Contraband"/> property of the vending machine.
/// </summary>
public void SetContraband ( EntityUid uid , bool contraband , VendingMachineComponent ? component = null )
{
if ( ! Resolve ( uid , ref component ) )
return ;
component . Contraband = contraband ;
Dirty ( uid , component ) ;
}
2022-08-31 14:12:09 +02:00
/// <summary>
/// Ejects a random item from the available stock. Will do nothing if the vending machine is empty.
/// </summary>
2023-05-03 01:38:03 -04:00
/// <param name="uid"></param>
2022-08-31 14:12:09 +02:00
/// <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>
2023-05-03 01:38:03 -04:00
/// <param name="vendComponent"></param>
2022-08-31 14:12:09 +02:00
public void EjectRandom ( EntityUid uid , bool throwItem , bool forceEject = false , VendingMachineComponent ? vendComponent = null )
2022-01-30 22:16:41 -06:00
{
if ( ! Resolve ( uid , ref vendComponent ) )
return ;
2022-08-31 14:12:09 +02:00
var availableItems = GetAvailableInventory ( uid , vendComponent ) ;
2022-01-30 22:16:41 -06:00
if ( availableItems . Count < = 0 )
return ;
2022-05-09 19:22:58 -07:00
var item = _random . Pick ( availableItems ) ;
2022-08-31 14:12:09 +02:00
if ( forceEject )
{
vendComponent . NextItemToEject = item . ID ;
vendComponent . ThrowNextItem = throwItem ;
2023-05-03 01:38:03 -04:00
var entry = GetEntry ( uid , item . ID , item . Type , vendComponent ) ;
2022-08-31 14:12:09 +02:00
if ( entry ! = null )
entry . Amount - - ;
2023-05-03 01:38:03 -04:00
EjectItem ( uid , vendComponent , forceEject ) ;
2022-08-31 14:12:09 +02:00
}
else
2023-05-03 01:38:03 -04:00
{
2025-03-02 13:47:52 +11:00
TryEjectVendorItem ( uid , item . Type , item . ID , throwItem , user : null , vendComponent : vendComponent ) ;
2023-05-03 01:38:03 -04:00
}
2022-08-31 14:12:09 +02:00
}
2025-03-02 13:47:52 +11:00
protected override void EjectItem ( EntityUid uid , VendingMachineComponent ? vendComponent = null , bool forceEject = false )
2022-08-31 14:12:09 +02:00
{
2023-05-03 01:38:03 -04:00
if ( ! Resolve ( uid , ref vendComponent ) )
return ;
2022-08-31 14:12:09 +02:00
// No need to update the visual state because we never changed it during a forced eject
if ( ! forceEject )
2025-03-02 13:47:52 +11:00
TryUpdateVisualState ( ( uid , vendComponent ) ) ;
2022-08-31 14:12:09 +02:00
if ( string . IsNullOrEmpty ( vendComponent . NextItemToEject ) )
{
vendComponent . ThrowNextItem = false ;
return ;
}
2022-09-15 11:53:17 +10:00
2024-05-29 02:02:15 +12:00
// Default spawn coordinates
var spawnCoordinates = Transform ( uid ) . Coordinates ;
//Make sure the wallvends spawn outside of the wall.
if ( TryComp < WallMountComponent > ( uid , out var wallMountComponent ) )
{
var offset = wallMountComponent . Direction . ToWorldVec ( ) * WallVendEjectDistanceFromWall ;
spawnCoordinates = spawnCoordinates . Offset ( offset ) ;
}
var ent = Spawn ( vendComponent . NextItemToEject , spawnCoordinates ) ;
2022-08-31 14:12:09 +02:00
if ( vendComponent . ThrowNextItem )
{
var range = vendComponent . NonLimitedEjectRange ;
var direction = new Vector2 ( _random . NextFloat ( - range , range ) , _random . NextFloat ( - range , range ) ) ;
_throwingSystem . TryThrow ( ent , direction , vendComponent . NonLimitedEjectForce ) ;
}
vendComponent . NextItemToEject = null ;
vendComponent . ThrowNextItem = false ;
}
public override void Update ( float frameTime )
{
base . Update ( frameTime ) ;
2023-05-07 01:26:04 +10:00
var disabled = EntityQueryEnumerator < EmpDisabledComponent , VendingMachineComponent > ( ) ;
while ( disabled . MoveNext ( out var uid , out _ , out var comp ) )
{
if ( comp . NextEmpEject < _timing . CurTime )
{
EjectRandom ( uid , true , false , comp ) ;
2025-03-02 13:47:52 +11:00
comp . NextEmpEject + = ( 5 * comp . EjectDelay ) ;
2023-05-07 01:26:04 +10:00
}
}
2022-01-30 22:16:41 -06:00
}
2023-01-01 18:42:56 -05:00
public void TryRestockInventory ( EntityUid uid , VendingMachineComponent ? vendComponent = null )
{
if ( ! Resolve ( uid , ref vendComponent ) )
return ;
RestockInventoryFromPrototype ( uid , vendComponent ) ;
2024-09-23 12:10:22 +10:00
Dirty ( uid , vendComponent ) ;
2025-03-02 13:47:52 +11:00
TryUpdateVisualState ( ( uid , vendComponent ) ) ;
2023-01-01 18:42:56 -05:00
}
2023-05-03 01:38:03 -04:00
private void OnPriceCalculation ( EntityUid uid , VendingMachineRestockComponent component , ref PriceCalculationEvent args )
{
List < double > priceSets = new ( ) ;
// Find the most expensive inventory and use that as the highest price.
foreach ( var vendingInventory in component . CanRestock )
{
double total = 0 ;
if ( PrototypeManager . TryIndex ( vendingInventory , out VendingMachineInventoryPrototype ? inventoryPrototype ) )
{
foreach ( var ( item , amount ) in inventoryPrototype . StartingInventory )
{
if ( PrototypeManager . TryIndex ( item , out EntityPrototype ? entity ) )
total + = _pricing . GetEstimatedPrice ( entity ) * amount ;
}
}
priceSets . Add ( total ) ;
}
args . Price + = priceSets . Max ( ) ;
}
2023-05-07 01:26:04 +10:00
private void OnEmpPulse ( EntityUid uid , VendingMachineComponent component , ref EmpPulseEvent args )
{
if ( ! component . Broken & & this . IsPowered ( uid , EntityManager ) )
{
args . Affected = true ;
args . Disabled = true ;
component . NextEmpEject = _timing . CurTime ;
}
}
2022-01-30 22:16:41 -06:00
}
}