Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stocks trading #2103

Merged
merged 20 commits into from
Nov 6, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<cartridges:PriceHistoryTable
xmlns="https://spacestation14.io"
xmlns:cartridges="clr-namespace:Content.Client.DeltaV.CartridgeLoader.Cartridges"
Orientation="Vertical"
HorizontalExpand="True"
Margin="0,5,0,0">

<!-- Header -->
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc stock-trading-price-history}"
HorizontalExpand="True"
StyleClasses="LabelSubText" />
</BoxContainer>

<!-- Price history panel -->
<PanelContainer Name="HistoryPanel"
HorizontalExpand="True"
Margin="0,2,0,0">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
HorizontalAlignment="Center">
<GridContainer Name="PriceGrid" Columns="5" />
</BoxContainer>
</PanelContainer>
</cartridges:PriceHistoryTable>
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;

namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;

[GenerateTypedNameReferences]
public sealed partial class PriceHistoryTable : BoxContainer
{
public PriceHistoryTable()
{
RobustXamlLoader.Load(this);

// Create the stylebox here so we can use the colors from StockTradingUi
var styleBox = new StyleBoxFlat
{
BackgroundColor = StockTradingUiFragment.PriceBackgroundColor,
ContentMarginLeftOverride = 6,
ContentMarginRightOverride = 6,
ContentMarginTopOverride = 4,
ContentMarginBottomOverride = 4,
BorderColor = StockTradingUiFragment.BorderColor,
BorderThickness = new Thickness(1),
};

HistoryPanel.PanelOverride = styleBox;
}

public void Update(List<float> priceHistory)
{
PriceGrid.RemoveAllChildren();

// Take last 5 prices
var lastFivePrices = priceHistory.TakeLast(5).ToList();

for (var i = 0; i < lastFivePrices.Count; i++)
{
var price = lastFivePrices[i];
var previousPrice = i > 0 ? lastFivePrices[i - 1] : price;
var priceChange = ((price - previousPrice) / previousPrice) * 100;

var entryContainer = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
MinWidth = 80,
HorizontalAlignment = HAlignment.Center,
};

var priceLabel = new Label
{
Text = $"${price:F2}",
HorizontalAlignment = HAlignment.Center,
};

var changeLabel = new Label
{
Text = $"{(priceChange >= 0 ? "+" : "")}{priceChange:F2}%",
HorizontalAlignment = HAlignment.Center,
StyleClasses = { "LabelSubText" },
Modulate = priceChange switch
{
> 0 => StockTradingUiFragment.PositiveColor,
< 0 => StockTradingUiFragment.NegativeColor,
_ => StockTradingUiFragment.NeutralColor,
}
};

entryContainer.AddChild(priceLabel);
entryContainer.AddChild(changeLabel);
PriceGrid.AddChild(entryContainer);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Robust.Client.UserInterface;
using Content.Client.UserInterface.Fragments;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;

namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;

public sealed partial class StockTradingUi : UIFragment
{
private StockTradingUiFragment? _fragment;

public override Control GetUIFragmentRoot()
{
return _fragment!;
}

public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
{
_fragment = new StockTradingUiFragment();

_fragment.OnBuyButtonPressed += (company, amount) =>
{
SendStockTradingUiMessage(StockTradingUiAction.Buy, company, amount, userInterface);
};
_fragment.OnSellButtonPressed += (company, amount) =>
{
SendStockTradingUiMessage(StockTradingUiAction.Sell, company, amount, userInterface);
};
}

public override void UpdateState(BoundUserInterfaceState state)
{
if (state is StockTradingUiState cast)
{
_fragment?.UpdateState(cast);
}
}

private static void SendStockTradingUiMessage(StockTradingUiAction action, int company, float amount, BoundUserInterface userInterface)
{
var newsMessage = new StockTradingUiMessageEvent(action, company, amount);
var message = new CartridgeUiMessage(newsMessage);
userInterface.SendMessage(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<cartridges:StockTradingUiFragment
xmlns="https://spacestation14.io"
xmlns:cartridges="clr-namespace:Content.Client.DeltaV.CartridgeLoader.Cartridges"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Margin="5"
VerticalExpand="True">

<!-- A parent container to hold the balance label and main content -->
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<!-- Header section with balance -->
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0,0,5,0">
<Label Text="{Loc stock-trading-title}"
HorizontalExpand="True"
HorizontalAlignment="Left" />
</BoxContainer>
<Label Name="Balance"
Text="{Loc stock-trading-balance}"
HorizontalAlignment="Right" />
</BoxContainer>
</PanelContainer>

<!-- Horizontal line under header -->
<customControls:HSeparator Margin="5 3 5 5"/>

<!-- Main content -->
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<Label Name="NoEntries"
Text="{Loc stock-trading-no-entries}"
HorizontalExpand="True"
HorizontalAlignment="Center"
Visible="False" />
<ScrollContainer HorizontalExpand="True"
VerticalExpand="True"
Margin="0,5,0,0">
<BoxContainer Name="Entries"
Orientation="Vertical"
VerticalAlignment="Top"
HorizontalExpand="True" />
</ScrollContainer>
</BoxContainer>
</BoxContainer>
</cartridges:StockTradingUiFragment>
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
using System.Linq;
using Content.Client.Administration.UI.CustomControls;
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;

namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;

[GenerateTypedNameReferences]
public sealed partial class StockTradingUiFragment : BoxContainer
{
private readonly Dictionary<int, CompanyEntry> _companyEntries = new();

// Event handlers for the parent UI
public event Action<int, float>? OnBuyButtonPressed;
public event Action<int, float>? OnSellButtonPressed;

// Define colors
public static readonly Color PositiveColor = Color.FromHex("#00ff00"); // Green
public static readonly Color NegativeColor = Color.FromHex("#ff0000"); // Red
public static readonly Color NeutralColor = Color.FromHex("#ffffff"); // White
public static readonly Color BackgroundColor = Color.FromHex("#25252a"); // Dark grey
public static readonly Color PriceBackgroundColor = Color.FromHex("#1a1a1a"); // Darker grey
public static readonly Color BorderColor = Color.FromHex("#404040"); // Light grey

public StockTradingUiFragment()
{
RobustXamlLoader.Load(this);
}

public void UpdateState(StockTradingUiState state)
{
NoEntries.Visible = state.Entries.Count == 0;
Balance.Text = Loc.GetString("stock-trading-balance", ("balance", state.Balance));

// Clear all existing entries
foreach (var entry in _companyEntries.Values)
{
entry.Container.RemoveAllChildren();
}
_companyEntries.Clear();
Entries.RemoveAllChildren();

// Add new entries
for (var i = 0; i < state.Entries.Count; i++)
{
var company = state.Entries[i];
var entry = new CompanyEntry(i, company.LocalizedDisplayName, OnBuyButtonPressed, OnSellButtonPressed);
_companyEntries[i] = entry;
Entries.AddChild(entry.Container);

var ownedStocks = state.OwnedStocks.GetValueOrDefault(i, 0);
entry.Update(company, ownedStocks);
}
}

private sealed class CompanyEntry
{
public readonly BoxContainer Container;
private readonly Label _nameLabel;
private readonly Label _priceLabel;
private readonly Label _changeLabel;
private readonly Button _sellButton;
private readonly Button _buyButton;
private readonly Label _sharesLabel;
private readonly LineEdit _amountEdit;
private readonly PriceHistoryTable _priceHistory;

public CompanyEntry(int companyIndex,
string displayName,
Action<int, float>? onBuyPressed,
Action<int, float>? onSellPressed)
{
Container = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
Margin = new Thickness(0, 0, 0, 2),
};

// Company info panel
var companyPanel = new PanelContainer();

var mainContent = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
Margin = new Thickness(8),
};

// Top row with company name and price info
var topRow = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
};

_nameLabel = new Label
{
HorizontalExpand = true,
Text = displayName,
};

// Create a panel for price and change
var pricePanel = new PanelContainer
{
HorizontalAlignment = HAlignment.Right,
};

// Style the price panel
var priceStyleBox = new StyleBoxFlat
{
BackgroundColor = BackgroundColor,
ContentMarginLeftOverride = 8,
ContentMarginRightOverride = 8,
ContentMarginTopOverride = 4,
ContentMarginBottomOverride = 4,
BorderColor = BorderColor,
BorderThickness = new Thickness(1),
};

pricePanel.PanelOverride = priceStyleBox;

// Container for price and change labels
var priceContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
};

_priceLabel = new Label();

_changeLabel = new Label
{
HorizontalAlignment = HAlignment.Right,
Modulate = NeutralColor,
Margin = new Thickness(15, 0, 0, 0),
};

priceContainer.AddChild(_priceLabel);
priceContainer.AddChild(_changeLabel);
pricePanel.AddChild(priceContainer);

topRow.AddChild(_nameLabel);
topRow.AddChild(pricePanel);

// Add the top row
mainContent.AddChild(topRow);

// Add the price history table between top and bottom rows
_priceHistory = new PriceHistoryTable();
mainContent.AddChild(_priceHistory);

// Trading controls (bottom row)
var bottomRow = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
Margin = new Thickness(0, 5, 0, 0),
};

_sharesLabel = new Label
{
Text = Loc.GetString("stock-trading-owned-shares"),
MinWidth = 100,
};

_amountEdit = new LineEdit
{
PlaceHolder = Loc.GetString("stock-trading-amount-placeholder"),
HorizontalExpand = true,
MinWidth = 80,
};

var buttonContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalAlignment = HAlignment.Right,
MinWidth = 140,
};

_buyButton = new Button
{
Text = Loc.GetString("stock-trading-buy-button"),
MinWidth = 65,
Margin = new Thickness(3, 0, 3, 0),
};

_sellButton = new Button
{
Text = Loc.GetString("stock-trading-sell-button"),
MinWidth = 65,
};

buttonContainer.AddChild(_buyButton);
buttonContainer.AddChild(_sellButton);

bottomRow.AddChild(_sharesLabel);
bottomRow.AddChild(_amountEdit);
bottomRow.AddChild(buttonContainer);

// Add the bottom row last
mainContent.AddChild(bottomRow);

companyPanel.AddChild(mainContent);
Container.AddChild(companyPanel);

// Add horizontal separator after the panel
var separator = new HSeparator
{
Margin = new Thickness(5, 3, 5, 5),
};
Container.AddChild(separator);

// Button click events
_buyButton.OnPressed += _ =>
{
if (float.TryParse(_amountEdit.Text, out var amount) && amount > 0)
onBuyPressed?.Invoke(companyIndex, amount);
};

_sellButton.OnPressed += _ =>
{
if (float.TryParse(_amountEdit.Text, out var amount) && amount > 0)
onSellPressed?.Invoke(companyIndex, amount);
};

// There has to be a better way of doing this
_amountEdit.OnTextChanged += args =>
{
var newText = string.Concat(args.Text.Where(char.IsDigit));
if (newText != args.Text)
_amountEdit.Text = newText;
};
}

public void Update(StockCompanyStruct company, int ownedStocks)
{
_nameLabel.Text = company.LocalizedDisplayName;
_priceLabel.Text = $"${company.CurrentPrice:F2}";
_sharesLabel.Text = Loc.GetString("stock-trading-owned-shares", ("shares", ownedStocks));

var priceChange = 0f;
if (company.PriceHistory is { Count: > 0 })
{
var previousPrice = company.PriceHistory[^1];
priceChange = (company.CurrentPrice - previousPrice) / previousPrice * 100;
}

_changeLabel.Text = $"{(priceChange >= 0 ? "+" : "")}{priceChange:F2}%";

// Update color based on price change
_changeLabel.Modulate = priceChange switch
{
> 0 => PositiveColor,
< 0 => NegativeColor,
_ => NeutralColor,
};

// Update the price history table if not null
if (company.PriceHistory != null)
_priceHistory.Update(company.PriceHistory);

// Disable sell button if no shares owned
_sellButton.Disabled = ownedStocks <= 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Numerics;
using Content.Server.DeltaV.Cargo.Systems;
using Content.Server.DeltaV.CartridgeLoader.Cartridges;
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Timing;

namespace Content.Server.DeltaV.Cargo.Components;

[RegisterComponent, AutoGenerateComponentPause]
[Access(typeof(StockMarketSystem), typeof(StockTradingCartridgeSystem))]
public sealed partial class StationStockMarketComponent : Component
{
/// <summary>
/// The list of companies you can invest in
/// </summary>
[DataField]
public List<StockCompanyStruct> Companies = [];

/// <summary>
/// The list of shares owned by the station
/// </summary>
[DataField]
public Dictionary<int, int> StockOwnership = new();

/// <summary>
/// The interval at which the stock market updates
/// </summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(600); // 10 minutes

/// <summary>
/// The <see cref="IGameTiming.CurTime"/> timespan of next update.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextUpdate = TimeSpan.Zero;

/// <summary>
/// The sound to play after selling or buying stocks
/// </summary>
[DataField]
public SoundSpecifier ConfirmSound = new SoundPathSpecifier("/Audio/Effects/Cargo/ping.ogg");

/// <summary>
/// The sound to play if the don't have access to buy or sell stocks
/// </summary>
[DataField]
public SoundSpecifier DenySound = new SoundPathSpecifier("/Audio/Effects/Cargo/buzz_sigh.ogg");

// These work well as presets but can be changed in the yaml
[DataField]
public List<MarketChange> MarketChanges =
[
new() { Chance = 0.86f, Range = new Vector2(-0.05f, 0.05f) }, // Minor
new() { Chance = 0.10f, Range = new Vector2(-0.3f, 0.2f) }, // Moderate
new() { Chance = 0.03f, Range = new Vector2(-0.5f, 1.5f) }, // Major
new() { Chance = 0.01f, Range = new Vector2(-0.9f, 4.0f) }, // Catastrophic
];
}

[DataDefinition]
public sealed partial class MarketChange
{
[DataField(required: true)]
public float Chance;

[DataField(required: true)]
public Vector2 Range;
}
135 changes: 135 additions & 0 deletions Content.Server/DeltaV/Cargo/StocksCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using Content.Server.Administration;
using Content.Server.DeltaV.Cargo.Components;
using Content.Server.DeltaV.Cargo.Systems;
using Content.Shared.Administration;
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Shared.Console;

namespace Content.Server.DeltaV.Cargo;

[AdminCommand(AdminFlags.Fun)]
public sealed class ChangeStocksPriceCommand : IConsoleCommand
{
public string Command => "changestocksprice";
public string Description => Loc.GetString("cmd-changestocksprice-desc");
public string Help => Loc.GetString("cmd-changestocksprice-help", ("command", Command));

[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;

public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
shell.WriteLine(Loc.GetString("shell-wrong-arguments-number"));
return;
}

if (!int.TryParse(args[0], out var companyIndex))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
return;
}

if (!float.TryParse(args[1], out var newPrice))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
return;
}

EntityUid? targetStation = null;
if (args.Length > 2)
{
if (!EntityUid.TryParse(args[2], out var station))
{
shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number"));
return;
}
targetStation = station;
}

var stockMarket = _entitySystemManager.GetEntitySystem<StockMarketSystem>();
var query = _entityManager.EntityQueryEnumerator<StationStockMarketComponent>();

while (query.MoveNext(out var uid, out var comp))
{
// Skip if we're looking for a specific station and this isn't it
if (targetStation != null && uid != targetStation)
continue;

if (stockMarket.TryChangeStocksPrice(uid, comp, newPrice, companyIndex))
{
shell.WriteLine(Loc.GetString("shell-command-success"));
return;
}

shell.WriteLine(Loc.GetString("cmd-changestocksprice-invalid-company"));
return;
}

shell.WriteLine(targetStation != null
? Loc.GetString("cmd-changestocksprice-invalid-station")
: Loc.GetString("cmd-changestocksprice-no-stations"));
}
}

[AdminCommand(AdminFlags.Fun)]
public sealed class AddStocksCompanyCommand : IConsoleCommand
{
public string Command => "addstockscompany";
public string Description => Loc.GetString("cmd-addstockscompany-desc");
public string Help => Loc.GetString("cmd-addstockscompany-help", ("command", Command));

[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;

public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
shell.WriteLine(Loc.GetString("shell-wrong-arguments-number"));
return;
}

if (!float.TryParse(args[1], out var basePrice))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
return;
}

EntityUid? targetStation = null;
if (args.Length > 2)
{
if (!EntityUid.TryParse(args[2], out var station))
{
shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number"));
return;
}
targetStation = station;
}

var displayName = args[0];
var stockMarket = _entitySystemManager.GetEntitySystem<StockMarketSystem>();
var query = _entityManager.EntityQueryEnumerator<StationStockMarketComponent>();

while (query.MoveNext(out var uid, out var comp))
{
// Skip if we're looking for a specific station and this isn't it
if (targetStation != null && uid != targetStation)
continue;

if (stockMarket.TryAddCompany(uid, comp, basePrice, displayName))
{
shell.WriteLine(Loc.GetString("shell-command-success"));
return;
}

shell.WriteLine(Loc.GetString("cmd-addstockscompany-failure"));
return;
}

shell.WriteLine(targetStation != null
? Loc.GetString("cmd-addstockscompany-invalid-station")
: Loc.GetString("cmd-addstockscompany-no-stations"));
}
}
385 changes: 385 additions & 0 deletions Content.Server/DeltaV/Cargo/Systems/StockMarketSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,385 @@
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
if (success)
{
_audio.PlayEntity(
stockMarket.ConfirmSound,
Filter.Empty().AddInRange(_transform.GetMapCoordinates(loader, xform), 0.05f),
loader,
true,
AudioParams.Default.WithMaxDistance(0.05f)
);
}
}
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Content.Server.DeltaV.CartridgeLoader.Cartridges;

[RegisterComponent, Access(typeof(StockTradingCartridgeSystem))]
public sealed partial class StockTradingCartridgeComponent : Component
{
/// <summary>
/// Station entity to keep track of
/// </summary>
[DataField]
public EntityUid? Station;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System.Linq;
using Content.Server.Cargo.Components;
using Content.Server.DeltaV.Cargo.Components;
using Content.Server.DeltaV.Cargo.Systems;
using Content.Server.Station.Systems;
using Content.Server.CartridgeLoader;
using Content.Shared.Cargo.Components;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;

namespace Content.Server.DeltaV.CartridgeLoader.Cartridges;

public sealed class StockTradingCartridgeSystem : EntitySystem
{
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!;
[Dependency] private readonly StationSystem _station = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<StockTradingCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<StockMarketUpdatedEvent>(OnStockMarketUpdated);
SubscribeLocalEvent<StationStockMarketComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<StockTradingCartridgeComponent, BankBalanceUpdatedEvent>(OnBalanceUpdated);
}

private void OnBalanceUpdated(Entity<StockTradingCartridgeComponent> ent, ref BankBalanceUpdatedEvent args)
{
UpdateAllCartridges(args.Station);
}

private void OnUiReady(Entity<StockTradingCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
{
UpdateUI(ent, args.Loader);
}

private void OnStockMarketUpdated(StockMarketUpdatedEvent args)
{
UpdateAllCartridges(args.Station);
}

private void OnMapInit(Entity<StationStockMarketComponent> ent, ref MapInitEvent args)
{
// Initialize price history for each company
for (var i = 0; i < ent.Comp.Companies.Count; i++)
{
var company = ent.Comp.Companies[i];

// Create initial price history using base price
company.PriceHistory = new List<float>();
for (var j = 0; j < 5; j++)
{
company.PriceHistory.Add(company.BasePrice);
}

ent.Comp.Companies[i] = company;
}

if (_station.GetOwningStation(ent.Owner) is { } station)
UpdateAllCartridges(station);
}

private void UpdateAllCartridges(EntityUid station)
{
var query = EntityQueryEnumerator<StockTradingCartridgeComponent, CartridgeComponent>();
while (query.MoveNext(out var uid, out var comp, out var cartridge))
{
if (cartridge.LoaderUid is not { } loader || comp.Station != station)
continue;
UpdateUI((uid, comp), loader);
}
}

private void UpdateUI(Entity<StockTradingCartridgeComponent> ent, EntityUid loader)
{
if (_station.GetOwningStation(loader) is { } station)
ent.Comp.Station = station;

if (!TryComp<StationStockMarketComponent>(ent.Comp.Station, out var stockMarket) ||
!TryComp<StationBankAccountComponent>(ent.Comp.Station, out var bankAccount))
return;

// Convert company data to UI state format
var entries = stockMarket.Companies.Select(company => new StockCompanyStruct(
displayName: company.LocalizedDisplayName,
currentPrice: company.CurrentPrice,
basePrice: company.BasePrice,
priceHistory: company.PriceHistory))
.ToList();

// Send the UI state with balance and owned stocks
var state = new StockTradingUiState(
entries: entries,
ownedStocks: stockMarket.StockOwnership,
balance: bankAccount.Balance
);

_cartridgeLoader.UpdateCartridgeUiState(loader, state);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Robust.Shared.Serialization;

namespace Content.Shared.CartridgeLoader.Cartridges;

[Serializable, NetSerializable]
public sealed class StockTradingUiMessageEvent(StockTradingUiAction action, int companyIndex, float amount)
: CartridgeMessageEvent
{
public readonly StockTradingUiAction Action = action;
public readonly int CompanyIndex = companyIndex;
public readonly float Amount = amount;
}

[Serializable, NetSerializable]
public enum StockTradingUiAction
{
Buy,
Sell,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Robust.Shared.Serialization;

namespace Content.Shared.CartridgeLoader.Cartridges;

[Serializable, NetSerializable]
public sealed class StockTradingUiState(
List<StockCompanyStruct> entries,
Dictionary<int, int> ownedStocks,
float balance)
: BoundUserInterfaceState
{
public readonly List<StockCompanyStruct> Entries = entries;
public readonly Dictionary<int, int> OwnedStocks = ownedStocks;
public readonly float Balance = balance;
}

// No structure, zero fucks given
[DataDefinition, Serializable]
public partial struct StockCompanyStruct
{
/// <summary>
/// The displayed name of the company shown in the UI.
/// </summary>
[DataField(required: true)]
public LocId? DisplayName;

// Used for runtime-added companies that don't have a localization entry
private string? _displayName;

/// <summary>
/// Gets or sets the display name, using either the localized or direct string value
/// </summary>
[Access(Other = AccessPermissions.ReadWriteExecute)]
public string LocalizedDisplayName
{
get => _displayName ?? Loc.GetString(DisplayName ?? string.Empty);
set => _displayName = value;
}

/// <summary>
/// The current price of the company's stock
/// </summary>
[DataField(required: true)]
public float CurrentPrice;

/// <summary>
/// The base price of the company's stock
/// </summary>
[DataField(required: true)]
public float BasePrice;

/// <summary>
/// The price history of the company's stock
/// </summary>
[DataField]
public List<float>? PriceHistory;

public StockCompanyStruct(string displayName, float currentPrice, float basePrice, List<float>? priceHistory)
{
DisplayName = displayName;
_displayName = null;
CurrentPrice = currentPrice;
BasePrice = basePrice;
PriceHistory = priceHistory ?? [];
}
}
6 changes: 6 additions & 0 deletions Resources/Locale/en-US/deltav/cargo/stocks-comapnies.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Company names used for stocks trading
stock-trading-company-nanotrasen = Nanotrasen [NT]
stock-trading-company-gorlex = Gorlex [GRX]
stock-trading-company-interdyne = Interdyne Pharmaceuticals [INTP]
stock-trading-company-fishinc = Fish Inc. [FIN]
stock-trading-company-donk = Donk Co. [DONK]
13 changes: 13 additions & 0 deletions Resources/Locale/en-US/deltav/cargo/stocks-commands.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# changestockprice command
cmd-changestocksprice-desc = Changes a company's stock price to the specified number.
cmd-changestocksprice-help = changestockprice <Company index> <New price> [Station UID]
cmd-changestocksprice-invalid-company = Failed to execute command! Invalid company index or the new price exceeds the allowed limit.
cmd-changestocksprice-invalid-station = No stock market found for specified station
cmd-changestocksprice-no-stations = No stations with stock markets found
# addstockscompany command
cmd-addstockscompany-desc = Adds a new company to the stocks market.
cmd-addstockscompany-help = addstockscompany <Display name> <Base price> [Station UID]
cmd-addstockscompany-failure = Failed to add company to the stock market.
cmd-addstockscompany-invalid-station = No stock market found for specified station
cmd-addstockscompany-no-stations = No stations with stock markets found
39 changes: 33 additions & 6 deletions Resources/Locale/en-US/deltav/cartridge-loader/cartridges.ftl
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## CrimeAssist

# General
crime-assist-program-name = Crime Assist
crime-assist-yes-button = Yes
crime-assist-no-button = No
@@ -6,6 +9,14 @@ crime-assist-crimetype-misdemeanour = Misdemeanour
crime-assist-crimetype-felony = Felony
crime-assist-crimetype-capital = Capital
crime-assist-crime-innocent = No crime was committed
crime-assist-mainmenu = Welcome to Crime Assist!
crime-assist-sophont-explanation = A sophont is described as any entity with the capacity to display the following attributes:
• [bold]Sapience[/bold]: the entity possesses basic logic and problem-solving skills, or at a minimum some level of significant intelligence.
• [bold]Sentience[/bold]: the entity has the capacity to process an emotion or lack thereof, or at a minimum the ability to recognise its own pain.
• [bold]Self-awareness[/bold]: the entity is capable of altering its behaviour in a reasonable fashion as a result of stimuli, or at a minimum is capable of recognising its own sapience and sentience.
Any sophont is considered a legal person, regardless of origin or prior cognitive status. Much like any other intelligent organic, a sophont may press charges against crew and be tried for crimes.
# Crimes
crime-assist-crime-animalcruelty = Code 101: Animal Cruelty
crime-assist-crime-theft = Code 102: Theft
crime-assist-crime-trespass = Code 110: Trespass
@@ -32,7 +43,8 @@ crime-assist-crime-decorporealisation = Code 305: Decorporealisation
crime-assist-crime-kidnapping = Code 309: Kidnapping
crime-assist-crime-sedition = Code 311: Sedition
crime-assist-crime-sexualharassment = Code 314: Sexual Harassment
crime-assist-mainmenu = Welcome to Crime Assist!
# Questions
crime-assist-question-isitterrorism = Did the suspect hold hostages, cause many deaths or major destruction to force compliance from the crew?
crime-assist-question-wassomeoneattacked = Was an entity attacked?
crime-assist-question-wasitsophont = Was the victim in question a sophont?
@@ -59,6 +71,8 @@ crime-assist-question-happenincourt = Was the suspect a nuisance in court?
crime-assist-question-duringactiveinvestigation = Was the suspect a nuisance during an active investigation, and hindered the investigation as a result?
crime-assist-question-tocommandstaff = Did the suspect overthrow or compromise a lawfully established Chain of Command, or attempt to do so?
crime-assist-question-wasitcommanditself = Was a command staff or department head abusing authority over another sophont?
# Crime details
crime-assist-crimedetail-innocent = Crime could not be determined. Use your best judgement to resolve the situation.
crime-assist-crimedetail-animalcruelty = To inflict unnecessary suffering on a sapient being with malicious intent.
crime-assist-crimedetail-theft = To unlawfully take property or items without consent.
@@ -86,6 +100,8 @@ crime-assist-crimedetail-decorporealisation = To unlawfully, maliciously, and pe
crime-assist-crimedetail-kidnapping = To unlawfully confine or restrict the free movement of a sophont against their will.
crime-assist-crimedetail-sedition = To act to overthrow a lawfully established Chain of Command or governing body without lawful or legitimate cause.
crime-assist-crimedetail-sexualharassment = To sexually harass, attempt to coerce into sexual relations, or effect unwanted sexual contact with an unwilling sophont.
# Punishments
crime-assist-crimepunishment-innocent = No punishment may be necessary
crime-assist-crimepunishment-animalcruelty = Punishment: 3 minutes
crime-assist-crimepunishment-theft = Punishment: 2 minutes
@@ -113,12 +129,10 @@ crime-assist-crimepunishment-decorporealisation = Punishment: Capital
crime-assist-crimepunishment-kidnapping = Punishment: Capital
crime-assist-crimepunishment-sedition = Punishment: Capital
crime-assist-crimepunishment-sexualharassment = Punishment: Capital
crime-assist-sophont-explanation = A sophont is described as any entity with the capacity to display the following attributes:
• [bold]Sapience[/bold]: the entity possesses basic logic and problem-solving skills, or at a minimum some level of significant intelligence.
• [bold]Sentience[/bold]: the entity has the capacity to process an emotion or lack thereof, or at a minimum the ability to recognise its own pain.
• [bold]Self-awareness[/bold]: the entity is capable of altering its behaviour in a reasonable fashion as a result of stimuli, or at a minimum is capable of recognising its own sapience and sentience.
Any sophont is considered a legal person, regardless of origin or prior cognitive status. Much like any other intelligent organic, a sophont may press charges against crew and be tried for crimes.
## MailMetrics

# General
mail-metrics-program-name = MailMetrics
mail-metrics-header = Income from Mail Deliveries
mail-metrics-opened = Earnings (Opened)
@@ -131,3 +145,16 @@ mail-metrics-money-header = Spesos
mail-metrics-total = Total
mail-metrics-progress = {$opened} out of {$total} packages opened!
mail-metrics-progress-percent = Success rate: {$successRate}%
## StockTrading

# General
stock-trading-program-name = StockTrading
stock-trading-title = Intergalactic Stock Market
stock-trading-balance = Balance: {$balance} credits
stock-trading-no-entries = No entries
stock-trading-owned-shares = Owned: {$shares}
stock-trading-buy-button = Buy
stock-trading-sell-button = Sell
stock-trading-amount-placeholder = Amount
stock-trading-price-history = Price History
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
- id: BoxPDACargo
- id: QuartermasterIDCard
- id: ClothingShoesBootsWinterLogisticsOfficer
- id: StockTradingCartridge
- id: LunchboxCommandFilledRandom
prob: 0.3

Original file line number Diff line number Diff line change
@@ -59,3 +59,27 @@
icon:
sprite: Nyanotrasen/Objects/Specific/Mail/mail.rsi
state: icon

- type: entity
parent: BaseItem
id: StockTradingCartridge
name: StockTrading cartridge
description: A cartridge that tracks the intergalactic stock market.
components:
- type: Sprite
sprite: DeltaV/Objects/Devices/cartridge.rsi
state: cart-stonk
- type: Icon
sprite: DeltaV/Objects/Devices/cartridge.rsi
state: cart-mail
- type: UIFragment
ui: !type:StockTradingUi
- type: StockTradingCartridge
- type: Cartridge
programName: stock-trading-program-name
icon:
sprite: DeltaV/Misc/program_icons.rsi
state: stock_trading
- type: BankClient
- type: AccessReader # This is so that we can restrict who can buy stocks
access: [["Orders"]]
21 changes: 21 additions & 0 deletions Resources/Prototypes/DeltaV/Entities/Stations/base.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
- type: entity
id: BaseStationStockMarket
abstract: true
components:
- type: StationStockMarket
companies:
- displayName: stock-trading-company-nanotrasen
basePrice: 100
currentPrice: 100
- displayName: stock-trading-company-gorlex
basePrice: 75
currentPrice: 75
- displayName: stock-trading-company-interdyne
basePrice: 300
currentPrice: 300
- displayName: stock-trading-company-fishinc
basePrice: 25
currentPrice: 25
- displayName: stock-trading-company-donk
basePrice: 90
currentPrice: 90
18 changes: 16 additions & 2 deletions Resources/Prototypes/Entities/Objects/Devices/pda.yml
Original file line number Diff line number Diff line change
@@ -392,12 +392,13 @@
accentVColor: "#a23e3e"
- type: Icon
state: pda-qm
- type: CartridgeLoader # DeltaV - MailMetrics courier tracker
- type: CartridgeLoader # DeltaV
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NewsReaderCartridge
- MailMetricsCartridge
- MailMetricsCartridge # DeltaV - MailMetrics courier tracker
- StockTradingCartridge # DeltaV - StockTrading

- type: entity
parent: BasePDA
@@ -412,6 +413,12 @@
borderColor: "#e39751"
- type: Icon
state: pda-cargo
- type: CartridgeLoader # DeltaV
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NewsReaderCartridge
- StockTradingCartridge # DeltaV - StockTrading

- type: entity
parent: BasePDA
@@ -792,6 +799,7 @@
- NotekeeperCartridge
- NewsReaderCartridge
- LogProbeCartridge
- StockTradingCartridge # Delta-V

- type: entity
parent: CentcomPDA
@@ -1016,6 +1024,12 @@
borderColor: "#3f3f74"
- type: Icon
state: pda-reporter
- type: CartridgeLoader # DeltaV
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NewsReaderCartridge
- StockTradingCartridge # DeltaV - StockTrading

- type: entity
parent: BasePDA
1 change: 1 addition & 0 deletions Resources/Prototypes/Entities/Stations/nanotrasen.yml
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@
- BaseStationAllEventsEligible
- BaseStationNanotrasen
- BaseStationMail # Nyano component, required for station mail to function
- BaseStationStockMarket # DeltaV
categories: [ HideSpawnMenu ]
components:
- type: Transform
14 changes: 14 additions & 0 deletions Resources/Textures/DeltaV/Misc/program_icons.rsi/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": 1,
"license": "CC0-1.0",
"copyright": "stock_trading made by Malice",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "stock_trading"
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -15,6 +15,9 @@
},
{
"name": "cart-psi"
},
{
"name": "cart-stonk"
}
]
}