Files
wwdpublic/Content.Server/Cargo/Systems/StockMarketSystem.cs
sleepyyapril 885ee5a831 Wizmerge for Station AI (#1351)
<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description

the adding AI is now up to y'all because i'm not touching loadout code
for name datasets, but it shouldn't be too bad from here

---------

Signed-off-by: sleepyyapril <123355664+sleepyyapril@users.noreply.github.com>
Signed-off-by: SolStar <44028047+ewokswagger@users.noreply.github.com>
Signed-off-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: themias <89101928+themias@users.noreply.github.com>
Co-authored-by: Verm <32827189+Vermidia@users.noreply.github.com>
Co-authored-by: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Co-authored-by: Sphiral <145869023+SphiraI@users.noreply.github.com>
Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com>
Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: Alzore <140123969+Blackern5000@users.noreply.github.com>
Co-authored-by: ravage <142820619+ravage123321@users.noreply.github.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: Intoxicating-Innocence <188202277+Intoxicating-Innocence@users.noreply.github.com>
Co-authored-by: Saphire <lattice@saphi.re>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: CaasGit <87243814+CaasGit@users.noreply.github.com>
Co-authored-by: BramvanZijp <56019239+BramvanZijp@users.noreply.github.com>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: NakataRin <45946146+NakataRin@users.noreply.github.com>
Co-authored-by: Kara <lunarautomaton6@gmail.com>
Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com>
Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>
Co-authored-by: Doomsdrayk <robotdoughnut@comcast.net>
Co-authored-by: Brandon Hu <103440971+Brandon-Huu@users.noreply.github.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
Co-authored-by: Julian Giebel <juliangiebel@live.de>
Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Co-authored-by: Repo <47093363+Titian3@users.noreply.github.com>
Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com>
Co-authored-by: icekot8 <93311212+icekot8@users.noreply.github.com>
Co-authored-by: AJCM-git <60196617+AJCM-git@users.noreply.github.com>
Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Co-authored-by: no <165581243+pissdemon@users.noreply.github.com>
Co-authored-by: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com>
Co-authored-by: osjarw <62134478+osjarw@users.noreply.github.com>
Co-authored-by: Simon <63975668+Simyon264@users.noreply.github.com>
Co-authored-by: TGRCDev <tgrc@tgrc.dev>
Co-authored-by: Milon <milonpl.git@proton.me>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
Co-authored-by: Fildrance <fildrance@gmail.com>
Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Co-authored-by: chavonadelal <156101927+chavonadelal@users.noreply.github.com>
Co-authored-by: SolStar <44028047+ewokswagger@users.noreply.github.com>
Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: ArchRBX <5040911+ArchRBX@users.noreply.github.com>
Co-authored-by: archrbx <punk.gear5260@fastmail.com>
Co-authored-by: Radezolid <snappednexus@gmail.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: EmoGarbage404 <retron404@gmail.com>
Co-authored-by: MilenVolf <63782763+MilenVolf@users.noreply.github.com>
Co-authored-by: Velcroboy <107660393+IamVelcroboy@users.noreply.github.com>
Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
Co-authored-by: neuPanda <chriseparton@gmail.com>
Co-authored-by: neuPanda <spainman0@yahoo.com>
Co-authored-by: Dvir <39403717+dvir001@users.noreply.github.com>
Co-authored-by: Whatstone <whatston3@gmail.com>
Co-authored-by: VideoKompany <135313844+VlaDOS1408@users.noreply.github.com>

(cherry picked from commit 93ed70acfeda357133a701f637d3faeec02749bb)
2025-01-14 00:13:42 +03:00

377 lines
13 KiB
C#

using Content.Server.Access.Systems;
using Content.Server.Administration.Logs;
using Content.Server.Cargo.Components;
using Content.Server.Cargo.Systems;
using Content.Server.DeltaV.Cargo.Components;
using Content.Server.DeltaV.CartridgeLoader.Cartridges;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.Database;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.DeltaV.Cargo.Systems;
/// <summary>
/// This handles the stock market updates
/// </summary>
public sealed class StockMarketSystem : EntitySystem
{
[Dependency] private readonly AccessReaderSystem _accessSystem = default!;
[Dependency] private readonly CargoSystem _cargo = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ILogManager _log = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IdCardSystem _idCardSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private ISawmill _sawmill = default!;
private const float MaxPrice = 262144; // 1/64 of max safe integer
public override void Initialize()
{
base.Initialize();
_sawmill = _log.GetSawmill("admin.stock_market");
SubscribeLocalEvent<StockTradingCartridgeComponent, CartridgeMessageEvent>(OnStockTradingMessage);
}
public override void Update(float frameTime)
{
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<StationStockMarketComponent>();
while (query.MoveNext(out var uid, out var component))
{
if (curTime < component.NextUpdate)
continue;
component.NextUpdate = curTime + component.UpdateInterval;
UpdateStockPrices(uid, component);
}
}
private void OnStockTradingMessage(Entity<StockTradingCartridgeComponent> ent, ref CartridgeMessageEvent args)
{
if (args is not StockTradingUiMessageEvent message)
return;
var companyIndex = message.CompanyIndex;
var amount = (int)message.Amount;
var station = ent.Comp.Station;
var loader = GetEntity(args.LoaderUid);
var xform = Transform(loader);
// Ensure station and stock market components are valid
if (station == null || !TryComp<StationStockMarketComponent>(station, out var stockMarket))
return;
// Validate company index
if (companyIndex < 0 || companyIndex >= stockMarket.Companies.Count)
return;
if (!TryComp<AccessReaderComponent>(ent.Owner, out var access))
return;
// Attempt to retrieve ID card from loader
IdCardComponent? idCard = null;
if (_idCardSystem.TryGetIdCard(loader, out var pdaId))
idCard = pdaId;
// Play deny sound and exit if access is not allowed
if (idCard == null || !_accessSystem.IsAllowed(pdaId.Owner, ent.Owner, access))
{
_audio.PlayEntity(
stockMarket.DenySound,
Filter.Empty().AddInRange(_transform.GetMapCoordinates(loader, xform), 0.05f),
loader,
true,
AudioParams.Default.WithMaxDistance(0.05f)
);
return;
}
try
{
var company = stockMarket.Companies[companyIndex];
// Attempt to buy or sell stocks based on the action
bool success;
switch (message.Action)
{
case StockTradingUiAction.Buy:
_adminLogger.Add(LogType.Action,
LogImpact.Medium,
$"{ToPrettyString(loader)} attempting to buy {amount} stocks of {company.LocalizedDisplayName}");
success = TryBuyStocks(station.Value, stockMarket, companyIndex, amount);
break;
case StockTradingUiAction.Sell:
_adminLogger.Add(LogType.Action,
LogImpact.Medium,
$"{ToPrettyString(loader)} attempting to sell {amount} stocks of {company.LocalizedDisplayName}");
success = TrySellStocks(station.Value, stockMarket, companyIndex, amount);
break;
default:
throw new ArgumentOutOfRangeException();
}
// Play confirmation sound if the transaction was successful
_audio.PlayEntity(success ? stockMarket.ConfirmSound : stockMarket.DenySound, loader, args.Actor);
}
finally
{
// Raise the event to update the UI regardless of outcome
var ev = new StockMarketUpdatedEvent(station.Value);
RaiseLocalEvent(ev);
}
}
private bool TryBuyStocks(
EntityUid station,
StationStockMarketComponent stockMarket,
int companyIndex,
int amount)
{
if (amount <= 0 || companyIndex < 0 || companyIndex >= stockMarket.Companies.Count)
return false;
// Check if the station has a bank account
if (!TryComp<StationBankAccountComponent>(station, out var bank))
return false;
var company = stockMarket.Companies[companyIndex];
var totalValue = (int)Math.Round(company.CurrentPrice * amount);
// See if we can afford it
if (bank.Balance < totalValue)
return false;
if (!stockMarket.StockOwnership.TryGetValue(companyIndex, out var currentOwned))
currentOwned = 0;
// Update the bank account
_cargo.UpdateBankAccount(station, bank, -totalValue);
stockMarket.StockOwnership[companyIndex] = currentOwned + amount;
// Log the transaction
_adminLogger.Add(LogType.Action,
LogImpact.Medium,
$"[StockMarket] Bought {amount} stocks of {company.LocalizedDisplayName} at {company.CurrentPrice:F2} credits each (Total: {totalValue})");
return true;
}
private bool TrySellStocks(
EntityUid station,
StationStockMarketComponent stockMarket,
int companyIndex,
int amount)
{
if (amount <= 0 || companyIndex < 0 || companyIndex >= stockMarket.Companies.Count)
return false;
// Check if the station has a bank account
if (!TryComp<StationBankAccountComponent>(station, out var bank))
return false;
if (!stockMarket.StockOwnership.TryGetValue(companyIndex, out var currentOwned) || currentOwned < amount)
return false;
var company = stockMarket.Companies[companyIndex];
var totalValue = (int)Math.Round(company.CurrentPrice * amount);
// Update stock ownership
var newAmount = currentOwned - amount;
if (newAmount > 0)
stockMarket.StockOwnership[companyIndex] = newAmount;
else
stockMarket.StockOwnership.Remove(companyIndex);
// Update the bank account
_cargo.UpdateBankAccount(station, bank, totalValue);
// Log the transaction
_adminLogger.Add(LogType.Action,
LogImpact.Medium,
$"[StockMarket] Sold {amount} stocks of {company.LocalizedDisplayName} at {company.CurrentPrice:F2} credits each (Total: {totalValue})");
return true;
}
private void UpdateStockPrices(EntityUid station, StationStockMarketComponent stockMarket)
{
for (var i = 0; i < stockMarket.Companies.Count; i++)
{
var company = stockMarket.Companies[i];
var changeType = DetermineMarketChange(stockMarket.MarketChanges);
var multiplier = CalculatePriceMultiplier(changeType);
UpdatePriceHistory(company);
// Update price with multiplier
var oldPrice = company.CurrentPrice;
company.CurrentPrice *= (1 + multiplier);
// Ensure price doesn't go below minimum threshold
company.CurrentPrice = MathF.Max(company.CurrentPrice, company.BasePrice * 0.1f);
// Ensure price doesn't go above maximum threshold
company.CurrentPrice = MathF.Min(company.CurrentPrice, MaxPrice);
stockMarket.Companies[i] = company;
// Calculate the percentage change
var percentChange = (company.CurrentPrice - oldPrice) / oldPrice * 100;
// Raise the event
var ev = new StockMarketUpdatedEvent(station);
RaiseLocalEvent(ev);
// Log it
_adminLogger.Add(LogType.Action,
LogImpact.Medium,
$"[StockMarket] Company '{company.LocalizedDisplayName}' price updated by {percentChange:+0.00;-0.00}% from {oldPrice:0.00} to {company.CurrentPrice:0.00}");
}
}
/// <summary>
/// Attempts to change the price for a specific company
/// </summary>
/// <returns>True if the operation was successful, false otherwise</returns>
public bool TryChangeStocksPrice(EntityUid station,
StationStockMarketComponent stockMarket,
float newPrice,
int companyIndex)
{
// Check if it exceeds the max price
if (newPrice > MaxPrice)
{
_sawmill.Error($"New price cannot be greater than {MaxPrice}.");
return false;
}
if (companyIndex < 0 || companyIndex >= stockMarket.Companies.Count)
return false;
var company = stockMarket.Companies[companyIndex];
UpdatePriceHistory(company);
company.CurrentPrice = MathF.Max(newPrice, company.BasePrice * 0.1f);
stockMarket.Companies[companyIndex] = company;
var ev = new StockMarketUpdatedEvent(station);
RaiseLocalEvent(ev);
return true;
}
/// <summary>
/// Attempts to add a new company to the station
/// </summary>
/// <returns>False if the company already exists, true otherwise</returns>
public bool TryAddCompany(EntityUid station,
StationStockMarketComponent stockMarket,
float basePrice,
string displayName)
{
// Create a new company struct with the specified parameters
var company = new StockCompanyStruct
{
LocalizedDisplayName = displayName, // Assume there's no Loc for it
BasePrice = basePrice,
CurrentPrice = basePrice,
PriceHistory = [],
};
stockMarket.Companies.Add(company);
UpdatePriceHistory(company);
var ev = new StockMarketUpdatedEvent(station);
RaiseLocalEvent(ev);
return true;
}
/// <summary>
/// Attempts to add a new company to the station using the StockCompanyStruct
/// </summary>
/// <returns>False if the company already exists, true otherwise</returns>
public bool TryAddCompany(EntityUid station,
StationStockMarketComponent stockMarket,
StockCompanyStruct company)
{
// Add the new company to the dictionary
stockMarket.Companies.Add(company);
// Make sure it has a price history
UpdatePriceHistory(company);
var ev = new StockMarketUpdatedEvent(station);
RaiseLocalEvent(ev);
return true;
}
private static void UpdatePriceHistory(StockCompanyStruct company)
{
// Create if null
company.PriceHistory ??= [];
// Make sure it has at least 5 entries
while (company.PriceHistory.Count < 5)
{
company.PriceHistory.Add(company.BasePrice);
}
// Store previous price in history
company.PriceHistory.Add(company.CurrentPrice);
if (company.PriceHistory.Count > 5) // Keep last 5 prices
company.PriceHistory.RemoveAt(1); // Always keep the base price
}
private MarketChange DetermineMarketChange(List<MarketChange> marketChanges)
{
var roll = _random.NextFloat();
var cumulative = 0f;
foreach (var change in marketChanges)
{
cumulative += change.Chance;
if (roll <= cumulative)
return change;
}
return marketChanges[0]; // Default to first (usually minor) change if we somehow exceed 100%
}
private float CalculatePriceMultiplier(MarketChange change)
{
// Using Box-Muller transform for normal distribution
var u1 = _random.NextFloat();
var u2 = _random.NextFloat();
var randStdNormal = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2);
// Scale and shift the result to our desired range
var range = change.Range.Y - change.Range.X;
var mean = (change.Range.Y + change.Range.X) / 2;
var stdDev = range / 6.0f; // 99.7% of values within range
var result = (float)(mean + (stdDev * randStdNormal));
return Math.Clamp(result, change.Range.X, change.Range.Y);
}
}
public sealed class StockMarketUpdatedEvent(EntityUid station) : EntityEventArgs
{
public EntityUid Station = station;
}