mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-18 14:07:53 +03:00
<!-- 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)
377 lines
13 KiB
C#
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;
|
|
}
|