Files
wwdpublic/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
SimpleStation14 825ca8772d Mirror: Trading Outpost now has half buy-only and half sell-only pallets (#191)
## Mirror of PR #25955: [Trading Outpost now has half buy-only and half
sell-only
pallets](https://github.com/space-wizards/space-station-14/pull/25955)
from <img src="https://avatars.githubusercontent.com/u/10567778?v=4"
alt="space-wizards" width="22"/>
[space-wizards](https://github.com/space-wizards)/[space-station-14](https://github.com/space-wizards/space-station-14)

###### `c0bbbc33c19eafcc8defaa7f1ec2df42e485b435`

PR opened by <img
src="https://avatars.githubusercontent.com/u/1471082?v=4" width="16"/><a
href="https://github.com/wafehling"> wafehling</a> at 2024-03-10
02:47:02 UTC

---

PR changed 8 files with 226 additions and 123 deletions.

The PR had the following labels:
- Changes: Sprites
- Status: Needs Review


---

<details open="true"><summary><h1>Original Body</h1></summary>

> ## About the PR
> I added two new entity types, a buy and sell only cargo pallet, and
added them to the trading outpost in place of the old pallets.
> 
> ## Why / Balance
> Since the day this was added, every single shift, at some point I'll
hear someone in cargo yelling about how **"You sold stuff I just
bought!"**
> 
> This seemed like the easiest way to fix the problem.
> 
> ## Technical details
> Added a variable to the cargo pallet component for labeling which type
it is, added two new varieties of the cargo pallet to the yml, and
adjusted all the calls for GetCargoPallets() to have boolean variables
for buyOnly and sellOnly to tell the function what kind of call you're
looking for.
> 
> If you don't give it any specifics it'll still treat any pallet as a
two-way one, but this function is only called 2 or 3 times and I've
adjusted them all and commented the function itself for anyone looking
to change/add it in the future.
> 
> The old pallet is still in the game and is set to "both" by default,
so it should work just fine for backwards compatibility.
> 
> ## Media
>
![image](https://github.com/space-wizards/space-station-14/assets/1471082/38cdf849-3e05-4f98-957f-65a719c2280d)
>
![image](https://github.com/space-wizards/space-station-14/assets/1471082/56e471e9-b45e-4332-bd55-7220566542ab)
>
![image](https://github.com/space-wizards/space-station-14/assets/1471082/a5e8e651-d301-420a-b2d7-cc4cf07a50af)
>
![image](https://github.com/space-wizards/space-station-14/assets/1471082/fdf2cf4c-fba8-46c9-9a30-7313a2204b5e)
>
![image](https://github.com/space-wizards/space-station-14/assets/1471082/3811edd0-bb32-44cf-b884-e0a9f4bf16fa)
> 
> 
> - [X] I have added screenshots/videos to this PR showcasing its
changes ingame, **or** this PR does not require an ingame showcase
> 
> ## Breaking changes
> 
> Shouldn't break anything, I've left the old pallets fully in, and
updated them to work with the new GetCargoPallets() function just like
they used to. Not sure if/where you'd still need them, but better safe
than sorry.
> 
> **Changelog**
> 🆑
> - tweak: The trading outpost now has dedicated buy-only and sell-only
pallets. No more accidentally selling orders you just bought. Cargonians
rejoice!
> 


</details>

Co-authored-by: SimpleStation14 <Unknown>
2024-06-16 11:39:17 -04:00

453 lines
15 KiB
C#

using System.Linq;
using Content.Server.Cargo.Components;
using Content.Server.GameTicking.Events;
using Content.Server.Shuttles.Components;
using Content.Server.Station.Systems;
using Content.Shared.Stacks;
using Content.Shared.Cargo;
using Content.Shared.Cargo.BUI;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Events;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Shuttles.Components;
using Content.Shared.Tiles;
using Content.Shared.Whitelist;
using Robust.Server.Maps;
using Robust.Shared.Map;
using Robust.Shared.Random;
using Robust.Shared.Audio;
using Robust.Shared.Physics.Components;
using Robust.Shared.Utility;
using Robust.Shared.Configuration;
namespace Content.Server.Cargo.Systems;
public sealed partial class CargoSystem
{
/*
* Handles cargo shuttle / trade mechanics.
*/
[Dependency] private readonly IConfigurationManager _confMan = default!;
public MapId? CargoMap { get; private set; }
private static readonly SoundPathSpecifier ApproveSound = new("/Audio/Effects/Cargo/ping.ogg");
private void InitializeShuttle()
{
SubscribeLocalEvent<TradeStationComponent, GridSplitEvent>(OnTradeSplit);
SubscribeLocalEvent<CargoShuttleConsoleComponent, ComponentStartup>(OnCargoShuttleConsoleStartup);
SubscribeLocalEvent<CargoPalletConsoleComponent, CargoPalletSellMessage>(OnPalletSale);
SubscribeLocalEvent<CargoPalletConsoleComponent, CargoPalletAppraiseMessage>(OnPalletAppraise);
SubscribeLocalEvent<CargoPalletConsoleComponent, BoundUIOpenedEvent>(OnPalletUIOpen);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
SubscribeLocalEvent<StationInitializedEvent>(OnStationInitialize);
Subs.CVar(_cfgManager, CCVars.GridFill, SetGridFill);
}
private void SetGridFill(bool obj)
{
if (obj)
{
SetupTradePost();
}
}
#region Console
private void UpdateCargoShuttleConsoles(EntityUid shuttleUid, CargoShuttleComponent _)
{
// Update pilot consoles that are already open.
_console.RefreshDroneConsoles();
// Update order consoles.
var shuttleConsoleQuery = AllEntityQuery<CargoShuttleConsoleComponent>();
while (shuttleConsoleQuery.MoveNext(out var uid, out var _))
{
var stationUid = _station.GetOwningStation(uid);
if (stationUid != shuttleUid)
continue;
UpdateShuttleState(uid, stationUid);
}
}
private void UpdatePalletConsoleInterface(EntityUid uid)
{
var bui = _uiSystem.GetUi(uid, CargoPalletConsoleUiKey.Sale);
if (Transform(uid).GridUid is not EntityUid gridUid)
{
_uiSystem.SetUiState(bui,
new CargoPalletConsoleInterfaceState(0, 0, false));
return;
}
GetPalletGoods(gridUid, out var toSell, out var amount);
_uiSystem.SetUiState(bui,
new CargoPalletConsoleInterfaceState((int) amount, toSell.Count, true));
}
private void OnPalletUIOpen(EntityUid uid, CargoPalletConsoleComponent component, BoundUIOpenedEvent args)
{
var player = args.Session.AttachedEntity;
if (player == null)
return;
UpdatePalletConsoleInterface(uid);
}
/// <summary>
/// Ok so this is just the same thing as opening the UI, its a refresh button.
/// I know this would probably feel better if it were like predicted and dynamic as pallet contents change
/// However.
/// I dont want it to explode if cargo uses a conveyor to move 8000 pineapple slices or whatever, they are
/// known for their entity spam i wouldnt put it past them
/// </summary>
private void OnPalletAppraise(EntityUid uid, CargoPalletConsoleComponent component, CargoPalletAppraiseMessage args)
{
var player = args.Session.AttachedEntity;
if (player == null)
return;
UpdatePalletConsoleInterface(uid);
}
private void OnCargoShuttleConsoleStartup(EntityUid uid, CargoShuttleConsoleComponent component, ComponentStartup args)
{
var station = _station.GetOwningStation(uid);
UpdateShuttleState(uid, station);
}
private void UpdateShuttleState(EntityUid uid, EntityUid? station = null)
{
TryComp<StationCargoOrderDatabaseComponent>(station, out var orderDatabase);
TryComp<CargoShuttleComponent>(orderDatabase?.Shuttle, out var shuttle);
var orders = GetProjectedOrders(station ?? EntityUid.Invalid, orderDatabase, shuttle);
var shuttleName = orderDatabase?.Shuttle != null ? MetaData(orderDatabase.Shuttle.Value).EntityName : string.Empty;
if (_uiSystem.TryGetUi(uid, CargoConsoleUiKey.Shuttle, out var bui))
_uiSystem.SetUiState(bui, new CargoShuttleConsoleBoundUserInterfaceState(
station != null ? MetaData(station.Value).EntityName : Loc.GetString("cargo-shuttle-console-station-unknown"),
string.IsNullOrEmpty(shuttleName) ? Loc.GetString("cargo-shuttle-console-shuttle-not-found") : shuttleName,
orders
));
}
#endregion
private void OnTradeSplit(EntityUid uid, TradeStationComponent component, ref GridSplitEvent args)
{
// If the trade station gets bombed it's still a trade station.
foreach (var gridUid in args.NewGrids)
{
EnsureComp<TradeStationComponent>(gridUid);
}
}
#region Shuttle
/// <summary>
/// Returns the orders that can fit on the cargo shuttle.
/// </summary>
private List<CargoOrderData> GetProjectedOrders(
EntityUid shuttleUid,
StationCargoOrderDatabaseComponent? component = null,
CargoShuttleComponent? shuttle = null)
{
var orders = new List<CargoOrderData>();
if (component == null || shuttle == null || component.Orders.Count == 0)
return orders;
var spaceRemaining = GetCargoSpace(shuttleUid);
for (var i = 0; i < component.Orders.Count && spaceRemaining > 0; i++)
{
var order = component.Orders[i];
if (order.Approved)
{
var numToShip = order.OrderQuantity - order.NumDispatched;
if (numToShip > spaceRemaining)
{
// We won't be able to fit the whole order on, so make one
// which represents the space we do have left:
var reducedOrder = new CargoOrderData(order.OrderId,
order.ProductId, order.Price, spaceRemaining, order.Requester, order.Reason);
orders.Add(reducedOrder);
}
else
{
orders.Add(order);
}
spaceRemaining -= numToShip;
}
}
return orders;
}
/// <summary>
/// Get the amount of space the cargo shuttle can fit for orders.
/// </summary>
private int GetCargoSpace(EntityUid gridUid)
{
var space = GetCargoPallets(gridUid, BuySellType.Buy).Count;
return space;
}
/// GetCargoPallets(gridUid, BuySellType.Sell) to return only Sell pads
/// GetCargoPallets(gridUid, BuySellType.Buy) to return only Buy pads
private List<(EntityUid Entity, CargoPalletComponent Component, TransformComponent PalletXform)> GetCargoPallets(EntityUid gridUid, BuySellType requestType = BuySellType.All)
{
_pads.Clear();
var query = AllEntityQuery<CargoPalletComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var comp, out var compXform))
{
if (compXform.ParentUid != gridUid ||
!compXform.Anchored)
{
continue;
}
if ((requestType & comp.PalletType) == 0)
{
continue;
}
_pads.Add((uid, comp, compXform));
}
return _pads;
}
private List<(EntityUid Entity, CargoPalletComponent Component, TransformComponent Transform)>
GetFreeCargoPallets(EntityUid gridUid,
List<(EntityUid Entity, CargoPalletComponent Component, TransformComponent Transform)> pallets)
{
_setEnts.Clear();
List<(EntityUid Entity, CargoPalletComponent Component, TransformComponent Transform)> outList = new();
foreach (var pallet in pallets)
{
var aabb = _lookup.GetAABBNoContainer(pallet.Entity, pallet.Transform.LocalPosition, pallet.Transform.LocalRotation);
if (_lookup.AnyLocalEntitiesIntersecting(gridUid, aabb, LookupFlags.Dynamic))
continue;
outList.Add(pallet);
}
return outList;
}
#endregion
#region Station
private bool SellPallets(EntityUid gridUid, EntityUid? station, out double amount)
{
station ??= _station.GetOwningStation(gridUid);
GetPalletGoods(gridUid, out var toSell, out amount);
Log.Debug($"Cargo sold {toSell.Count} entities for {amount}");
if (toSell.Count == 0)
return false;
if (station != null)
{
var ev = new EntitySoldEvent(station.Value, toSell);
RaiseLocalEvent(ref ev);
}
foreach (var ent in toSell)
{
Del(ent);
}
return true;
}
private void GetPalletGoods(EntityUid gridUid, out HashSet<EntityUid> toSell, out double amount)
{
amount = 0;
toSell = new HashSet<EntityUid>();
foreach (var (palletUid, _, _) in GetCargoPallets(gridUid, BuySellType.Sell))
{
// Containers should already get the sell price of their children so can skip those.
_setEnts.Clear();
_lookup.GetEntitiesIntersecting(palletUid, _setEnts,
LookupFlags.Dynamic | LookupFlags.Sundries);
foreach (var ent in _setEnts)
{
// Dont sell:
// - anything already being sold
// - anything anchored (e.g. light fixtures)
// - anything blacklisted (e.g. players).
if (toSell.Contains(ent) ||
_xformQuery.TryGetComponent(ent, out var xform) &&
(xform.Anchored || !CanSell(ent, xform)))
{
continue;
}
if (_blacklistQuery.HasComponent(ent))
continue;
var price = _pricing.GetPrice(ent);
if (price == 0)
continue;
toSell.Add(ent);
amount += price;
}
}
}
private bool CanSell(EntityUid uid, TransformComponent xform)
{
if (_mobQuery.HasComponent(uid))
{
return false;
}
var complete = IsBountyComplete(uid, (EntityUid?) null, out var bountyEntities);
// Recursively check for mobs at any point.
var children = xform.ChildEnumerator;
while (children.MoveNext(out var child))
{
if (complete && bountyEntities.Contains(child))
continue;
if (!CanSell(child, _xformQuery.GetComponent(child)))
return false;
}
return true;
}
private void OnPalletSale(EntityUid uid, CargoPalletConsoleComponent component, CargoPalletSellMessage args)
{
var player = args.Session.AttachedEntity;
if (player == null)
return;
var bui = _uiSystem.GetUi(uid, CargoPalletConsoleUiKey.Sale);
var xform = Transform(uid);
if (xform.GridUid is not EntityUid gridUid)
{
_uiSystem.SetUiState(bui,
new CargoPalletConsoleInterfaceState(0, 0, false));
return;
}
if (!SellPallets(gridUid, null, out var price))
return;
var stackPrototype = _protoMan.Index<StackPrototype>(component.CashType);
_stack.Spawn((int) price, stackPrototype, xform.Coordinates);
_audio.PlayPvs(ApproveSound, uid);
UpdatePalletConsoleInterface(uid);
}
#endregion
private void OnRoundRestart(RoundRestartCleanupEvent ev)
{
Reset();
CleanupTradeStation();
}
private void OnStationInitialize(StationInitializedEvent args)
{
if (!HasComp<StationCargoOrderDatabaseComponent>(args.Station)) // No cargo, L
return;
if (_cfgManager.GetCVar(CCVars.GridFill) && _confMan.GetCVar(CargoCVars.CreateCargoMap))
SetupTradePost();
}
private void CleanupTradeStation()
{
if (CargoMap == null || !_mapManager.MapExists(CargoMap.Value))
{
CargoMap = null;
DebugTools.Assert(!EntityQuery<CargoShuttleComponent>().Any());
return;
}
_mapManager.DeleteMap(CargoMap.Value);
CargoMap = null;
}
private void SetupTradePost()
{
if (CargoMap != null && _mapManager.MapExists(CargoMap.Value))
{
return;
}
// It gets mapinit which is okay... buuutt we still want it paused to avoid power draining.
CargoMap = _mapManager.CreateMap();
var options = new MapLoadOptions
{
LoadMap = true,
};
_mapLoader.TryLoad((MapId) CargoMap, "/Maps/Shuttles/trading_outpost.yml", out var rootUids, options); // Oh boy oh boy, hardcoded paths!
// If this fails to load for whatever reason, cargo is fucked
if (rootUids == null || !rootUids.Any())
return;
foreach (var grid in rootUids)
{
EnsureComp<ProtectedGridComponent>(grid);
EnsureComp<TradeStationComponent>(grid);
var shuttleComponent = EnsureComp<ShuttleComponent>(grid);
shuttleComponent.AngularDamping = 10000;
shuttleComponent.LinearDamping = 10000;
Dirty(shuttleComponent);
}
var mapUid = _mapManager.GetMapEntityId(CargoMap.Value);
var ftl = EnsureComp<FTLDestinationComponent>(_mapManager.GetMapEntityId(CargoMap.Value));
ftl.Whitelist = new EntityWhitelist()
{
Components =
[
_factory.GetComponentName(typeof(CargoShuttleComponent))
]
};
_metaSystem.SetEntityName(mapUid, $"Automated Trade Station {_random.Next(1000):000}");
_console.RefreshShuttleConsoles();
}
}
/// <summary>
/// Event broadcast raised by-ref before it is sold and
/// deleted but after the price has been calculated.
/// </summary>
[ByRefEvent]
public readonly record struct EntitySoldEvent(EntityUid Station, HashSet<EntityUid> Sold);