diff --git a/Content.Client/Bank/BUI/BankATMMenuBoundUserInterface.cs b/Content.Client/Bank/BUI/BankATMMenuBoundUserInterface.cs
new file mode 100644
index 00000000000000..455388fd4ef3d5
--- /dev/null
+++ b/Content.Client/Bank/BUI/BankATMMenuBoundUserInterface.cs
@@ -0,0 +1,58 @@
+using Content.Client.Bank.UI;
+using Content.Shared.Bank.BUI;
+using Content.Shared.Bank.Events;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Cargo.BUI;
+
+public sealed class BankATMMenuBoundUserInterface : BoundUserInterface
+{
+ private BankATMMenu? _menu;
+
+ public BankATMMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) {}
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = new BankATMMenu();
+ _menu.WithdrawRequest += OnWithdraw;
+ _menu.DepositRequest += OnDeposit;
+ _menu.OnClose += Close;
+ _menu.OpenCentered();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (disposing)
+ {
+ _menu?.Dispose();
+ }
+ }
+
+ private void OnWithdraw()
+ {
+ if (_menu?.Amount is not int amount)
+ return;
+
+ SendMessage(new BankWithdrawMessage(amount));
+ }
+
+ private void OnDeposit()
+ {
+ SendMessage(new BankDepositMessage());
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not BankATMMenuInterfaceState bankState)
+ return;
+
+ _menu?.SetEnabled(bankState.Enabled);
+ _menu?.SetBalance(bankState.Balance);
+ _menu?.SetDeposit(bankState.Deposit);
+ }
+}
diff --git a/Content.Client/Bank/BUI/StationBankATMMenuBoundUserInterface.cs b/Content.Client/Bank/BUI/StationBankATMMenuBoundUserInterface.cs
new file mode 100644
index 00000000000000..028e5db36e6ef2
--- /dev/null
+++ b/Content.Client/Bank/BUI/StationBankATMMenuBoundUserInterface.cs
@@ -0,0 +1,63 @@
+using Content.Client.Bank.UI;
+using Content.Shared.Bank.BUI;
+using Content.Shared.Bank.Events;
+using Robust.Client.GameObjects;
+using Content.Shared.Access.Systems;
+
+namespace Content.Client.Cargo.BUI;
+
+public sealed class StationBankATMMenuBoundUserInterface : BoundUserInterface
+{
+ private StationBankATMMenu? _menu;
+
+ public StationBankATMMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) {}
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = new StationBankATMMenu();
+ _menu.WithdrawRequest += OnWithdraw;
+ _menu.DepositRequest += OnDeposit;
+ _menu.OnClose += Close;
+ _menu.PopulateReasons();
+ _menu.OpenCentered();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (disposing)
+ {
+ _menu?.Dispose();
+ }
+ }
+
+ private void OnWithdraw()
+ {
+ if (_menu?.Amount is not int amount)
+ return;
+
+ SendMessage(new StationBankWithdrawMessage(amount, _menu.Reason, _menu.Description));
+ }
+
+ private void OnDeposit()
+ {
+ if (_menu?.Amount is not int amount)
+ return;
+
+ SendMessage(new StationBankDepositMessage(amount, _menu.Reason, _menu.Description));
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not StationBankATMMenuInterfaceState bankState)
+ return;
+
+ _menu?.SetEnabled(bankState.Enabled);
+ _menu?.SetBalance(bankState.Balance);
+ _menu?.SetDeposit(bankState.Deposit);
+ }
+}
diff --git a/Content.Client/Bank/BUI/WithdrawlBankATMMenuBoundUserInterface.cs b/Content.Client/Bank/BUI/WithdrawlBankATMMenuBoundUserInterface.cs
new file mode 100644
index 00000000000000..e59c5325861a7f
--- /dev/null
+++ b/Content.Client/Bank/BUI/WithdrawlBankATMMenuBoundUserInterface.cs
@@ -0,0 +1,51 @@
+using Content.Client.Bank.UI;
+using Content.Shared.Bank.BUI;
+using Content.Shared.Bank.Events;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Cargo.BUI;
+
+public sealed class WithdrawBankATMMenuBoundUserInterface : BoundUserInterface
+{
+ private WithdrawBankATMMenu? _menu;
+
+ public WithdrawBankATMMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) {}
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = new WithdrawBankATMMenu();
+ _menu.WithdrawRequest += OnWithdraw;
+ _menu.OnClose += Close;
+ _menu.OpenCentered();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (disposing)
+ {
+ _menu?.Dispose();
+ }
+ }
+
+ private void OnWithdraw()
+ {
+ if (_menu?.Amount is not int amount)
+ return;
+
+ SendMessage(new BankWithdrawMessage(amount));
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not BankATMMenuInterfaceState bankState)
+ return;
+
+ _menu?.SetEnabled(bankState.Enabled);
+ _menu?.SetBalance(bankState.Balance);
+ }
+}
diff --git a/Content.Client/Bank/UI/BankATMMenu.xaml b/Content.Client/Bank/UI/BankATMMenu.xaml
new file mode 100644
index 00000000000000..ff91ed98a8f4f2
--- /dev/null
+++ b/Content.Client/Bank/UI/BankATMMenu.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Bank/UI/BankATMMenu.xaml.cs b/Content.Client/Bank/UI/BankATMMenu.xaml.cs
new file mode 100644
index 00000000000000..4e62faef384db0
--- /dev/null
+++ b/Content.Client/Bank/UI/BankATMMenu.xaml.cs
@@ -0,0 +1,56 @@
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Bank.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class BankATMMenu : FancyWindow
+{
+ public Action? WithdrawRequest;
+ public Action? DepositRequest;
+ public int Amount;
+ public BankATMMenu()
+ {
+ RobustXamlLoader.Load(this);
+ WithdrawButton.OnPressed += OnWithdrawPressed;
+ DepositButton.OnPressed += OnDepositPressed;
+ Title = Loc.GetString("bank-atm-menu-title");
+ WithdrawEdit.OnTextChanged += OnAmountChanged;
+ }
+
+ public void SetBalance(int amount)
+ {
+ BalanceLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", amount.ToString()));
+ }
+
+ public void SetDeposit(int amount)
+ {
+ DepositButton.Disabled = amount <= 0;
+ DepositLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", amount.ToString()));
+ }
+
+ public void SetEnabled(bool enabled)
+ {
+ WithdrawButton.Disabled = !enabled;
+ }
+
+ private void OnWithdrawPressed(BaseButton.ButtonEventArgs obj)
+ {
+ WithdrawRequest?.Invoke();
+ }
+
+ private void OnDepositPressed(BaseButton.ButtonEventArgs obj)
+ {
+ DepositRequest?.Invoke();
+ }
+
+ private void OnAmountChanged(LineEdit.LineEditEventArgs args)
+ {
+ if (int.TryParse(args.Text, out var amount))
+ {
+ Amount = amount;
+ }
+ }
+}
diff --git a/Content.Client/Bank/UI/StationBankATMMenu.xaml b/Content.Client/Bank/UI/StationBankATMMenu.xaml
new file mode 100644
index 00000000000000..60f1fe069547cd
--- /dev/null
+++ b/Content.Client/Bank/UI/StationBankATMMenu.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Bank/UI/StationBankATMMenu.xaml.cs b/Content.Client/Bank/UI/StationBankATMMenu.xaml.cs
new file mode 100644
index 00000000000000..0ddd66325a6f59
--- /dev/null
+++ b/Content.Client/Bank/UI/StationBankATMMenu.xaml.cs
@@ -0,0 +1,95 @@
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Bank.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class StationBankATMMenu : FancyWindow
+{
+ public Action? WithdrawRequest;
+ public Action? DepositRequest;
+ public int Amount;
+ private readonly List _reasonStrings = new();
+ public string? Reason;
+ public string? Description;
+ public StationBankATMMenu()
+ {
+ RobustXamlLoader.Load(this);
+ WithdrawButton.OnPressed += OnWithdrawPressed;
+ DepositButton.OnPressed += OnDepositPressed;
+ Title = Loc.GetString("station-bank-atm-menu-title");
+ WithdrawEdit.OnTextChanged += OnAmountChanged;
+ Reasons.OnItemSelected += OnReasonSelected;
+ AmountDescription.OnTextChanged += OnDescChanged;
+ }
+
+ private void SetReasonText(int id)
+ {
+ Reason = id == 0 ? null : _reasonStrings[id];
+ Reasons.SelectId(id);
+ }
+ private void OnReasonSelected(OptionButton.ItemSelectedEventArgs args)
+ {
+ SetReasonText(args.Id);
+ }
+ public void SetBalance(int amount)
+ {
+ BalanceLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", amount.ToString()));
+ }
+
+ public void SetDeposit(int amount)
+ {
+ DepositButton.Disabled = amount <= 0;
+ DepositLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", amount.ToString()));
+ }
+
+ public void SetEnabled(bool enabled)
+ {
+ WithdrawButton.Disabled = !enabled;
+ DepositButton.Disabled = !enabled;
+ }
+
+ private void OnWithdrawPressed(BaseButton.ButtonEventArgs obj)
+ {
+ WithdrawRequest?.Invoke();
+ }
+
+ private void OnDepositPressed(BaseButton.ButtonEventArgs obj)
+ {
+ DepositRequest?.Invoke();
+ }
+
+ private void OnAmountChanged(LineEdit.LineEditEventArgs args)
+ {
+ if (int.TryParse(args.Text, out var amount))
+ {
+ Amount = amount;
+ }
+ }
+
+ private void OnDescChanged(LineEdit.LineEditEventArgs args)
+ {
+ Description = args.Text;
+ }
+
+ public void PopulateReasons()
+ {
+ _reasonStrings.Clear();
+ Reasons.Clear();
+ //todo: think of a better way/place to store the petty cash reason strings. this is mostly for rp, and a little bit of admin qol
+ _reasonStrings.Add("default");
+ _reasonStrings.Add("payroll");
+ _reasonStrings.Add("workorder");
+ _reasonStrings.Add("supplies");
+ _reasonStrings.Add("bounty");
+ _reasonStrings.Add("other");
+ Reasons.AddItem(Loc.GetString("station-bank-required"), 0);
+ Reasons.AddItem(Loc.GetString("station-bank-payroll"), 1);
+ Reasons.AddItem(Loc.GetString("station-bank-workorder"), 2);
+ Reasons.AddItem(Loc.GetString("station-bank-supplies"), 3);
+ Reasons.AddItem(Loc.GetString("station-bank-bounty"), 4);
+ Reasons.AddItem(Loc.GetString("station-bank-other"), 5);
+ }
+}
diff --git a/Content.Client/Bank/UI/WithdrawBankATMMenu.cs b/Content.Client/Bank/UI/WithdrawBankATMMenu.cs
new file mode 100644
index 00000000000000..1811e5d1992217
--- /dev/null
+++ b/Content.Client/Bank/UI/WithdrawBankATMMenu.cs
@@ -0,0 +1,44 @@
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Bank.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class WithdrawBankATMMenu : FancyWindow
+{
+ public Action? WithdrawRequest;
+ public Action? DepositRequest;
+ public int Amount;
+ public WithdrawBankATMMenu()
+ {
+ RobustXamlLoader.Load(this);
+ WithdrawButton.OnPressed += OnWithdrawPressed;
+ Title = Loc.GetString("bank-atm-menu-title");
+ WithdrawEdit.OnTextChanged += OnAmountChanged;
+ }
+
+ public void SetBalance(int amount)
+ {
+ BalanceLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", amount.ToString()));
+ }
+
+ public void SetEnabled(bool enabled)
+ {
+ WithdrawButton.Disabled = !enabled;
+ }
+
+ private void OnWithdrawPressed(BaseButton.ButtonEventArgs obj)
+ {
+ WithdrawRequest?.Invoke();
+ }
+
+ private void OnAmountChanged(LineEdit.LineEditEventArgs args)
+ {
+ if (int.TryParse(args.Text, out var amount))
+ {
+ Amount = amount;
+ }
+ }
+}
diff --git a/Content.Client/Bank/UI/WithdrawBankATMMenu.xaml b/Content.Client/Bank/UI/WithdrawBankATMMenu.xaml
new file mode 100644
index 00000000000000..ffa0bf38c8d6b3
--- /dev/null
+++ b/Content.Client/Bank/UI/WithdrawBankATMMenu.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Shipyard/UI/ShipyardRulesPopup.xaml b/Content.Client/Shipyard/UI/ShipyardRulesPopup.xaml
new file mode 100644
index 00000000000000..55867cd2e0a7d7
--- /dev/null
+++ b/Content.Client/Shipyard/UI/ShipyardRulesPopup.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Shipyard/UI/ShipyardRulesPopup.xaml.cs b/Content.Client/Shipyard/UI/ShipyardRulesPopup.xaml.cs
new file mode 100644
index 00000000000000..d908ebc5c36dd3
--- /dev/null
+++ b/Content.Client/Shipyard/UI/ShipyardRulesPopup.xaml.cs
@@ -0,0 +1,28 @@
+using Content.Client.UserInterface.Controls;
+using Content.Client.Shipyard.BUI;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+using static Robust.Client.UserInterface.Controls.BaseButton;
+using Content.Client.UserInterface.Systems.Actions.Controls;
+
+namespace Content.Client.Shipyard.UI
+{
+ [GenerateTypedNameReferences]
+ sealed partial class ShipyardRulesPopup : FancyWindow
+ {
+ private readonly ShipyardConsoleBoundUserInterface _menu;
+
+ public ShipyardRulesPopup(ShipyardConsoleBoundUserInterface owner)
+ {
+ RobustXamlLoader.Load(this);
+ _menu = owner;
+ Title = Loc.GetString("shipyard-console-menu-title");
+ AcceptButton.OnPressed += OnRulesAccept;
+ }
+
+ private void OnRulesAccept(ButtonEventArgs args)
+ {
+ Close();
+ }
+ }
+}
diff --git a/Content.Client/Shipyard/UI/VesselRow.xaml b/Content.Client/Shipyard/UI/VesselRow.xaml
new file mode 100644
index 00000000000000..3ae32f2e195bc2
--- /dev/null
+++ b/Content.Client/Shipyard/UI/VesselRow.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Shipyard/UI/VesselRow.xaml.cs b/Content.Client/Shipyard/UI/VesselRow.xaml.cs
new file mode 100644
index 00000000000000..ed0bcee386bfbf
--- /dev/null
+++ b/Content.Client/Shipyard/UI/VesselRow.xaml.cs
@@ -0,0 +1,17 @@
+using Content.Shared.Shipyard;
+using Content.Shared.Shipyard.Prototypes;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Shipyard.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class VesselRow : PanelContainer
+{
+ public VesselPrototype? Vessel;
+ public VesselRow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
diff --git a/Content.Server/Shipyard/Commands/PurchaseShuttleCommand.cs b/Content.Server/Shipyard/Commands/PurchaseShuttleCommand.cs
new file mode 100644
index 00000000000000..6dca2bcc1f63a7
--- /dev/null
+++ b/Content.Server/Shipyard/Commands/PurchaseShuttleCommand.cs
@@ -0,0 +1,45 @@
+using Content.Server.Administration;
+using Content.Server.Maps;
+using Content.Server.Shipyard.Systems;
+using Content.Shared.Administration;
+using Robust.Shared.Console;
+
+namespace Content.Server.Shipyard.Commands;
+
+///
+/// Purchases a shuttle and docks it to a station.
+///
+[AdminCommand(AdminFlags.Fun)]
+public sealed class PurchaseShuttleCommand : IConsoleCommand
+{
+ [Dependency] private readonly IEntitySystemManager _entityManager = default!;
+ public string Command => "purchaseshuttle";
+ public string Description => Loc.GetString("shipyard-commands-purchase-desc");
+ public string Help => $"{Command} ";
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (!int.TryParse(args[0], out var stationId))
+ {
+ shell.WriteError($"{args[0]} is not a valid integer.");
+ return;
+ }
+
+ var shuttlePath = args[1];
+ var system = _entityManager.GetEntitySystem();
+ var station = new EntityUid(stationId);
+ system.TryPurchaseShuttle(station, shuttlePath, out _);
+ }
+
+ public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
+ {
+ switch (args.Length)
+ {
+ case 1:
+ return CompletionResult.FromHint(Loc.GetString("station-id"));
+ case 2:
+ return CompletionResult.FromHint(Loc.GetString("cmd-hint-savemap-path"));
+ }
+
+ return CompletionResult.Empty;
+ }
+}
diff --git a/Content.Server/Shipyard/Systems/ShipyardSystem.Consoles.cs b/Content.Server/Shipyard/Systems/ShipyardSystem.Consoles.cs
index 55c86ab4603f34..7b9e4d7d86a9bb 100644
--- a/Content.Server/Shipyard/Systems/ShipyardSystem.Consoles.cs
+++ b/Content.Server/Shipyard/Systems/ShipyardSystem.Consoles.cs
@@ -12,7 +12,7 @@
using Content.Shared.Shipyard;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
-using Robust.Shared.Players;
+using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Content.Shared.Radio;
using System.Linq;
@@ -36,6 +36,7 @@
using Content.Server.Shuttles.Components;
using Content.Server.Station.Components;
using System.Text.RegularExpressions;
+using Robust.Shared.Audio.Systems;
namespace Content.Server.Shipyard.Systems;
diff --git a/Content.Server/_NF/Bank/ATMSystem.cs b/Content.Server/_NF/Bank/ATMSystem.cs
new file mode 100644
index 00000000000000..e1b8f7f0d0c881
--- /dev/null
+++ b/Content.Server/_NF/Bank/ATMSystem.cs
@@ -0,0 +1,265 @@
+using Content.Server.Popups;
+using Content.Server.Stack;
+using Content.Shared.Bank;
+using Content.Shared.Bank.BUI;
+using Content.Shared.Bank.Components;
+using Content.Shared.Bank.Events;
+using Content.Shared.Coordinates;
+using Content.Shared.Stacks;
+using Robust.Server.GameObjects;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Player;
+using System.Linq;
+using Content.Server.Administration.Logs;
+using Content.Server.Cargo.Components;
+using Content.Shared.Database;
+using Robust.Shared.Audio.Systems;
+
+namespace Content.Server.Bank;
+
+public sealed partial class BankSystem
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly StackSystem _stackSystem = default!;
+ [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
+ [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
+ [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+
+ private void InitializeATM()
+ {
+ SubscribeLocalEvent(OnWithdraw);
+ SubscribeLocalEvent(OnDeposit);
+ SubscribeLocalEvent(OnATMUIOpen);
+ SubscribeLocalEvent(OnCashSlotChanged);
+ SubscribeLocalEvent(OnCashSlotChanged);
+ }
+
+ private void OnWithdraw(EntityUid uid, BankATMComponent component, BankWithdrawMessage args)
+ {
+
+ if (args.Session.AttachedEntity is not { Valid : true } player)
+ return;
+
+ // to keep the window stateful
+ GetInsertedCashAmount(component, out var deposit);
+ if (!_uiSystem.TryGetUi(uid, args.UiKey, out var bui))
+ {
+ return;
+ }
+
+ // check for a bank account
+ if (!TryComp(player, out var bank))
+ {
+ _log.Info($"{player} has no bank account");
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-no-bank"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new BankATMMenuInterfaceState(0, false, deposit));
+ return;
+ }
+
+ // check for sufficient funds
+ if (bank.Balance < args.Amount)
+ {
+ ConsolePopup(args.Session, Loc.GetString("bank-insufficient-funds"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new BankATMMenuInterfaceState(bank.Balance, true, deposit));
+ return;
+ }
+
+ // try to actually withdraw from the bank. Validation happens on the banking system but we still indicate error.
+ if (!TryBankWithdraw(player, args.Amount))
+ {
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-transaction-denied"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new BankATMMenuInterfaceState(bank.Balance, true, deposit));
+ return;
+ }
+
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-withdraw-successful"));
+ PlayConfirmSound(uid, component);
+ _adminLogger.Add(LogType.ATMUsage, LogImpact.Low, $"{ToPrettyString(player):actor} withdrew {args.Amount} from {ToPrettyString(component.Owner)}");
+
+ //spawn the cash stack of whatever cash type the ATM is configured to.
+ var stackPrototype = _prototypeManager.Index(component.CashType);
+ _stackSystem.Spawn(args.Amount, stackPrototype, uid.ToCoordinates());
+
+ _uiSystem.SetUiState(bui,
+ new BankATMMenuInterfaceState(bank.Balance, true, deposit));
+ }
+
+ private void OnDeposit(EntityUid uid, BankATMComponent component, BankDepositMessage args)
+ {
+ if (args.Session.AttachedEntity is not { Valid: true } player)
+ return;
+
+ // gets the money inside a cashslot of an ATM.
+ // Dynamically knows what kind of cash to look for according to BankATMComponent
+ GetInsertedCashAmount(component, out var deposit);
+
+ var bui = _uiSystem.GetUi(component.Owner, args.UiKey);
+
+ // make sure the user actually has a bank
+ if (!TryComp(player, out var bank))
+ {
+ _log.Info($"{player} has no bank account");
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-no-bank"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new BankATMMenuInterfaceState(0, false, deposit));
+ return;
+ }
+
+ // validating the cash slot was setup correctly in the yaml
+ if (component.CashSlot.ContainerSlot is not BaseContainer cashSlot)
+ {
+ _log.Info($"ATM has no cash slot");
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-no-bank"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new BankATMMenuInterfaceState(0, false, deposit));
+ return;
+ }
+
+ // validate stack prototypes
+ if (!TryComp(component.CashSlot.ContainerSlot.ContainedEntity, out var stackComponent) ||
+ stackComponent.StackTypeId == null)
+ {
+ _log.Info($"ATM cash slot contains bad stack prototype");
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-wrong-cash"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new BankATMMenuInterfaceState(0, false, deposit));
+ return;
+ }
+
+ // and then check them against the ATM's CashType
+ if (_prototypeManager.Index(component.CashType) != _prototypeManager.Index(stackComponent.StackTypeId))
+ {
+ _log.Info($"{stackComponent.StackTypeId} is not {component.CashType}");
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-wrong-cash"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new BankATMMenuInterfaceState(0, false, deposit));
+ return;
+ }
+
+ if (BankATMMenuUiKey.BlackMarket == (BankATMMenuUiKey) args.UiKey)
+ {
+ var tax = (int) (deposit * 0.30f);
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out _, out var comp))
+ {
+ _cargo.DeductFunds(comp, -tax);
+ }
+
+ deposit -= tax;
+ }
+
+ // try to deposit the inserted cash into a player's bank acount. Validation happens on the banking system but we still indicate error.
+ if (!TryBankDeposit(player, deposit))
+ {
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-transaction-denied"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new BankATMMenuInterfaceState(bank.Balance, true, deposit));
+ return;
+ }
+
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-deposit-successful"));
+ PlayConfirmSound(uid, component);
+ _adminLogger.Add(LogType.ATMUsage, LogImpact.Low, $"{ToPrettyString(player):actor} deposited {deposit} into {ToPrettyString(component.Owner)}");
+
+ // yeet and delete the stack in the cash slot after success
+ _containerSystem.CleanContainer(cashSlot);
+ _uiSystem.SetUiState(bui,
+ new BankATMMenuInterfaceState(bank.Balance, true, 0));
+ return;
+ }
+
+ private void OnCashSlotChanged(EntityUid uid, BankATMComponent component, ContainerModifiedMessage args)
+ {
+ var bankUi = _uiSystem.GetUiOrNull(uid, BankATMMenuUiKey.ATM) ?? _uiSystem.GetUiOrNull(uid, BankATMMenuUiKey.BlackMarket);
+
+ var uiUser = bankUi!.SubscribedSessions.FirstOrDefault();
+ GetInsertedCashAmount(component, out var deposit);
+
+ if (uiUser?.AttachedEntity is not { Valid: true } player)
+ {
+ return;
+ }
+
+ if (!TryComp(player, out var bank))
+ {
+ return;
+ }
+
+ if (component.CashSlot.ContainerSlot?.ContainedEntity is not { Valid : true } cash)
+ {
+ _uiSystem.SetUiState(bankUi,
+ new BankATMMenuInterfaceState(bank.Balance, true, 0));
+ }
+
+ _uiSystem.SetUiState(bankUi,
+ new BankATMMenuInterfaceState(bank.Balance, true, deposit));
+ }
+
+ private void OnATMUIOpen(EntityUid uid, BankATMComponent component, BoundUIOpenedEvent args)
+ {
+ var player = args.Session.AttachedEntity;
+
+ if (player == null)
+ return;
+
+ GetInsertedCashAmount(component, out var deposit);
+ var bui = _uiSystem.GetUi(component.Owner, args.UiKey);
+
+ if (!TryComp(player, out var bank))
+ {
+ _log.Info($"{player} has no bank account");
+ _uiSystem.SetUiState(bui,
+ new BankATMMenuInterfaceState(0, false, deposit));
+ return;
+ }
+
+ _uiSystem.SetUiState(bui,
+ new BankATMMenuInterfaceState(bank.Balance, true, deposit));
+ }
+
+ private void GetInsertedCashAmount(BankATMComponent component, out int amount)
+ {
+ amount = 0;
+ var cashEntity = component.CashSlot.ContainerSlot?.ContainedEntity;
+
+ if (!TryComp(cashEntity, out var cashStack) ||
+ cashStack.StackTypeId != component.CashType)
+ {
+ return;
+ }
+
+ amount = cashStack.Count;
+ return;
+ }
+
+ private void PlayDenySound(EntityUid uid, BankATMComponent component)
+ {
+ _audio.PlayPvs(_audio.GetSound(component.ErrorSound), uid);
+ }
+
+ private void PlayConfirmSound(EntityUid uid, BankATMComponent component)
+ {
+ _audio.PlayPvs(_audio.GetSound(component.ConfirmSound), uid);
+ }
+
+ private void ConsolePopup(ICommonSession session, string text)
+ {
+ if (session.AttachedEntity is { Valid: true } player)
+ _popup.PopupEntity(text, player);
+ }
+}
diff --git a/Content.Server/_NF/Bank/BankSystem.cs b/Content.Server/_NF/Bank/BankSystem.cs
new file mode 100644
index 00000000000000..72a7051c5e114a
--- /dev/null
+++ b/Content.Server/_NF/Bank/BankSystem.cs
@@ -0,0 +1,163 @@
+using System.Threading;
+using Content.Server.Database;
+using Content.Server.Preferences.Managers;
+using Content.Server.GameTicking;
+using Content.Shared.Bank.Components;
+using Content.Shared.Preferences;
+using Robust.Shared.GameStates;
+using Robust.Shared.Network;
+using Content.Server.Cargo.Components;
+
+namespace Content.Server.Bank;
+
+public sealed partial class BankSystem : EntitySystem
+{
+ [Dependency] private readonly IServerPreferencesManager _prefsManager = default!;
+ [Dependency] private readonly IServerDbManager _dbManager = default!;
+
+ private ISawmill _log = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _log = Logger.GetSawmill("bank");
+ SubscribeLocalEvent(OnPlayerSpawn);
+ SubscribeLocalEvent(OnBankAccountChanged);
+ SubscribeLocalEvent(OnPlayerLobbyJoin);
+ InitializeATM();
+ InitializeStationATM();
+ }
+
+ // attaches the bank component directly on to the player's mob. Could be attached to something else on the player later.
+ // we may have to change this later depending on mind rework.
+ // then again, maybe the bank account should stay attached to the mob
+ private void OnPlayerSpawn (PlayerSpawnCompleteEvent args)
+ {
+ var mobUid = args.Mob;
+ var bank = EnsureComp(mobUid);
+ bank.Balance = args.Profile.BankBalance;
+ Dirty(bank);
+ }
+
+ // To ensure that bank account data gets saved, we are going to update the db every time the component changes
+ // I at first wanted to try to reduce database calls, however notafet suggested I just do it every time the account changes
+ // TODO: stop it from running 5 times every time
+ private void OnBankAccountChanged(EntityUid mobUid, BankAccountComponent bank, ref ComponentGetState args)
+ {
+ var user = args.Player?.UserId;
+
+ if (user == null || args.Player?.AttachedEntity != mobUid)
+ {
+ return;
+ }
+
+ var prefs = _prefsManager.GetPreferences((NetUserId) user);
+ var character = prefs.SelectedCharacter;
+ var index = prefs.IndexOfCharacter(character);
+
+ if (character is not HumanoidCharacterProfile profile)
+ {
+ return;
+ }
+
+ var newProfile = new HumanoidCharacterProfile(
+ profile.Name,
+ profile.FlavorText,
+ profile.Species,
+ profile.Age,
+ profile.Sex,
+ profile.Gender,
+ bank.Balance,
+ profile.Appearance,
+ profile.Clothing,
+ profile.Backpack,
+ profile.JobPriorities,
+ profile.PreferenceUnavailable,
+ profile.AntagPreferences,
+ profile.TraitPreferences);
+
+ args.State = new BankAccountComponentState
+ {
+ Balance = bank.Balance,
+ };
+
+ _dbManager.SaveCharacterSlotAsync((NetUserId) user, newProfile, index);
+ _log.Info($"Character {profile.Name} saved");
+ }
+
+ ///
+ /// Attempts to remove money from a character's bank account. This should always be used instead of attempting to modify the bankaccountcomponent directly
+ ///
+ /// The UID that the bank account is attached to, typically the player controlled mob
+ /// The integer amount of which to decrease the bank account
+ /// true if the transaction was successful, false if it was not
+ public bool TryBankWithdraw(EntityUid mobUid, int amount)
+ {
+ if (amount <= 0)
+ {
+ _log.Info($"{amount} is invalid");
+ return false;
+ }
+
+ if (!TryComp(mobUid, out var bank))
+ {
+ _log.Info($"{mobUid} has no bank account");
+ return false;
+ }
+
+ if (bank.Balance < amount)
+ {
+ _log.Info($"{mobUid} has insufficient funds");
+ return false;
+ }
+
+ bank.Balance -= amount;
+ _log.Info($"{mobUid} withdrew {amount}");
+ Dirty(bank);
+ return true;
+ }
+
+ ///
+ /// Attempts to add money to a character's bank account. This should always be used instead of attempting to modify the bankaccountcomponent directly
+ ///
+ /// The UID that the bank account is connected to, typically the player controlled mob
+ /// The integer amount of which to increase the bank account
+ /// true if the transaction was successful, false if it was not
+ public bool TryBankDeposit(EntityUid mobUid, int amount)
+ {
+ if (amount <= 0)
+ {
+ _log.Info($"{amount} is invalid");
+ return false;
+ }
+
+ if (!TryComp(mobUid, out var bank))
+ {
+ _log.Info($"{mobUid} has no bank account");
+ return false;
+ }
+
+ bank.Balance += amount;
+ _log.Info($"{mobUid} deposited {amount}");
+ Dirty(bank);
+ return true;
+ }
+
+ ///
+ /// ok so this is incredibly fucking cursed, and really shouldnt be calling LoadData
+ /// However
+ /// as of writing, the preferences system caches all player character data at the time of client connection.
+ /// This is causing some bad bahavior where the cache is becoming outdated after character data is getting saved to the db
+ /// and there is no method right now to invalidate and refresh this cache to ensure we get accurate bank data from the database,
+ /// resulting in respawns/round restarts populating the bank account component with an outdated cache and then re-saving that
+ /// bad cached data into the db.
+ /// effectively a gigantic money exploit.
+ /// So, this will have to stay cursed until I can find another way to refresh the character cache
+ /// or the db gods themselves come up to smite me from below, whichever comes first
+ ///
+ private void OnPlayerLobbyJoin (PlayerJoinedLobbyEvent args)
+ {
+ var cts = new CancellationToken();
+ _prefsManager.LoadData(args.PlayerSession, cts);
+ }
+}
diff --git a/Content.Server/_NF/Bank/StationATMSystem.cs b/Content.Server/_NF/Bank/StationATMSystem.cs
new file mode 100644
index 00000000000000..458aa80dfbe737
--- /dev/null
+++ b/Content.Server/_NF/Bank/StationATMSystem.cs
@@ -0,0 +1,297 @@
+using Content.Shared.Bank;
+using Content.Shared.Bank.Components;
+using Content.Shared.Bank.Events;
+using Content.Shared.Coordinates;
+using Content.Shared.Stacks;
+using Content.Server.Station.Systems;
+using Content.Server.Cargo.Systems;
+using Content.Server.Cargo.Components;
+using Content.Shared.Bank.BUI;
+using Content.Shared.Access.Systems;
+using Content.Shared.Database;
+using Robust.Shared.Containers;
+using System.Linq;
+
+namespace Content.Server.Bank;
+
+public sealed partial class BankSystem
+{
+ [Dependency] private readonly StationSystem _station = default!;
+ [Dependency] private readonly CargoSystem _cargo = default!;
+ [Dependency] private readonly AccessReaderSystem _access = default!;
+
+ private void InitializeStationATM()
+ {
+ SubscribeLocalEvent(OnWithdraw);
+ SubscribeLocalEvent(OnDeposit);
+ SubscribeLocalEvent(OnATMUIOpen);
+ SubscribeLocalEvent(OnCashSlotChanged);
+ SubscribeLocalEvent(OnCashSlotChanged);
+ }
+
+ private void OnWithdraw(EntityUid uid, StationBankATMComponent component, StationBankWithdrawMessage args)
+ {
+
+ if (args.Session.AttachedEntity is not { Valid: true } player)
+ return;
+
+ // to keep the window stateful
+ var bui = _uiSystem.GetUi(component.Owner, BankATMMenuUiKey.ATM);
+ var station = _station.GetOwningStation(uid);
+ // check for a bank account
+
+ GetInsertedCashAmount(component, out var deposit);
+
+ if (!TryComp(station, out var stationBank))
+ {
+ _log.Info($"station {station} has no bank account");
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-no-bank"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(0, false, deposit));
+ return;
+ }
+
+ if (!_access.IsAllowed(player, uid))
+ {
+ _log.Info($"{player} tried to access stationo bank account");
+ ConsolePopup(args.Session, Loc.GetString("station-bank-unauthorized"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(stationBank.Balance, false, deposit));
+ return;
+ }
+
+ if (args.Description == null || args.Reason == null)
+ {
+ ConsolePopup(args.Session, Loc.GetString("station-bank-requires-reason"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(stationBank.Balance, _access.IsAllowed(player, uid), deposit));
+ return;
+ }
+
+ // check for sufficient funds
+ if (stationBank.Balance < args.Amount || args.Amount < 0)
+ {
+ ConsolePopup(args.Session, Loc.GetString("bank-insufficient-funds"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(stationBank.Balance, _access.IsAllowed(player, uid), deposit));
+ return;
+ }
+
+ _cargo.DeductFunds(stationBank, args.Amount);
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-withdraw-successful"));
+ PlayConfirmSound(uid, component);
+ _log.Info($"{args.Session.UserId} {args.Session.Name} withdrew {args.Amount}, '{args.Reason}': {args.Description}");
+
+ _adminLogger.Add(LogType.ATMUsage, LogImpact.Low, $"{ToPrettyString(player):actor} withdrew {args.Amount} from station bank account. '{args.Reason}': {args.Description}");
+ //spawn the cash stack of whatever cash type the ATM is configured to.
+ var stackPrototype = _prototypeManager.Index(component.CashType);
+ _stackSystem.Spawn(args.Amount, stackPrototype, uid.ToCoordinates());
+
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(stationBank.Balance, _access.IsAllowed(player, uid), deposit));
+ }
+
+ private void OnDeposit(EntityUid uid, StationBankATMComponent component, StationBankDepositMessage args)
+ {
+ if (args.Session.AttachedEntity is not { Valid: true } player)
+ return;
+
+ // to keep the window stateful
+ var bui = _uiSystem.GetUi(component.Owner, BankATMMenuUiKey.ATM);
+ var station = _station.GetOwningStation(uid);
+ // check for a bank account
+
+ // gets the money inside a cashslot of an ATM.
+ // Dynamically knows what kind of cash to look for according to BankATMComponent
+ GetInsertedCashAmount(component, out var deposit);
+
+ if (!TryComp(station, out var stationBank))
+ {
+ _log.Info($"station {station} has no bank account");
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-no-bank"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(0, false, deposit));
+ return;
+ }
+
+ // validating the cash slot was setup correctly in the yaml
+ if (component.CashSlot.ContainerSlot is not BaseContainer cashSlot)
+ {
+ _log.Info($"ATM has no cash slot");
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-no-bank"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(0, false, deposit));
+ return;
+ }
+
+ if (!_access.IsAllowed(player, uid))
+ {
+ _log.Info($"{player} tried to access stationo bank account");
+ ConsolePopup(args.Session, Loc.GetString("station-bank-unauthorized"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(stationBank.Balance, false, deposit));
+ return;
+ }
+
+ if (args.Description == null || args.Reason == null)
+ {
+ ConsolePopup(args.Session, Loc.GetString("station-bank-requires-reason"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(stationBank.Balance, _access.IsAllowed(player, uid), deposit));
+ return;
+ }
+
+ // validate stack prototypes
+ if (!TryComp(component.CashSlot.ContainerSlot.ContainedEntity, out var stackComponent) ||
+ stackComponent.StackTypeId == null)
+ {
+ _log.Info($"ATM cash slot contains bad stack prototype");
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-wrong-cash"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(0, false, deposit));
+ return;
+ }
+
+ // and then check them against the ATM's CashType
+ if (_prototypeManager.Index(component.CashType) != _prototypeManager.Index(stackComponent.StackTypeId))
+ {
+ _log.Info($"{stackComponent.StackTypeId} is not {component.CashType}");
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-wrong-cash"));
+ PlayDenySound(uid, component);
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(0, false, deposit));
+ return;
+ }
+
+ // try to deposit the inserted cash into a player's bank acount.
+ if (args.Amount <= 0)
+ {
+ _log.Info($"{args.Amount} is invalid");
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-transaction-denied"));
+ PlayDenySound(uid, component);
+ return;
+ }
+
+ if (deposit < args.Amount)
+ {
+ _log.Info($"{args.Amount} is more then {deposit}");
+ ConsolePopup(args.Session, Loc.GetString("bank-insufficient-funds"));
+ PlayDenySound(uid, component);
+ return;
+ }
+
+ _cargo.DeductFunds(stationBank, -args.Amount);
+ ConsolePopup(args.Session, Loc.GetString("bank-atm-menu-deposit-successful"));
+ PlayConfirmSound(uid, component);
+ _log.Info($"{args.Session.UserId} {args.Session.Name} deposited {args.Amount}, '{args.Reason}': {args.Description}");
+
+ _adminLogger.Add(LogType.ATMUsage, LogImpact.Low, $"{ToPrettyString(player):actor} deposited {args.Amount} to station bank account. '{args.Reason}': {args.Description}");
+
+ SetInsertedCashAmount(component, args.Amount, out int leftAmount, out bool empty);
+
+ // yeet and delete the stack in the cash slot after success if its worth 0
+ if (empty)
+ _containerSystem.CleanContainer(cashSlot);
+
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(stationBank.Balance, _access.IsAllowed(player, uid), leftAmount));
+ }
+
+ private void OnCashSlotChanged(EntityUid uid, StationBankATMComponent component, ContainerModifiedMessage args)
+ {
+ GetInsertedCashAmount(component, out var deposit);
+ var bui = _uiSystem.GetUi(component.Owner, BankATMMenuUiKey.ATM);
+ var station = _station.GetOwningStation(uid);
+
+ if (!TryComp(station, out var bank))
+ {
+ return;
+ }
+
+ if (component.CashSlot.ContainerSlot?.ContainedEntity is not { Valid: true } cash)
+ {
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(bank.Balance, true, 0));
+ }
+
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(bank.Balance, true, deposit));
+ }
+
+ private void OnATMUIOpen(EntityUid uid, StationBankATMComponent component, BoundUIOpenedEvent args)
+ {
+ if (args.Session.AttachedEntity is not { Valid : true } player)
+ return;
+
+ GetInsertedCashAmount(component, out var deposit);
+ var bui = _uiSystem.GetUi(component.Owner, BankATMMenuUiKey.ATM);
+ var station = _station.GetOwningStation(uid);
+
+ if (!TryComp(station, out var stationBank))
+ {
+ _log.Info($"{station} has no bank account");
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(0, false, deposit));
+ return;
+ }
+
+ _uiSystem.SetUiState(bui,
+ new StationBankATMMenuInterfaceState(stationBank.Balance, _access.IsAllowed(player, uid), deposit));
+ }
+
+ private void GetInsertedCashAmount(StationBankATMComponent component, out int amount)
+ {
+ amount = 0;
+ var cashEntity = component.CashSlot.ContainerSlot?.ContainedEntity;
+
+ if (!TryComp(cashEntity, out var cashStack) ||
+ cashStack.StackTypeId != component.CashType)
+ {
+ return;
+ }
+
+ amount = cashStack.Count;
+ return;
+ }
+
+ private void SetInsertedCashAmount(StationBankATMComponent component, int amount, out int leftAmount, out bool empty)
+ {
+ leftAmount = 0;
+ empty = false;
+ var cashEntity = component.CashSlot.ContainerSlot?.ContainedEntity;
+
+ if (!TryComp(cashEntity, out var cashStack) ||
+ cashStack.StackTypeId != component.CashType)
+ {
+ return;
+ }
+
+ int newAmount = cashStack.Count;
+ cashStack.Count = newAmount - amount;
+ leftAmount = cashStack.Count;
+
+ if (cashStack.Count <= 0)
+ empty = true;
+
+ return;
+ }
+
+ private void PlayDenySound(EntityUid uid, StationBankATMComponent component)
+ {
+ _audio.PlayPvs(_audio.GetSound(component.ErrorSound), uid);
+ }
+
+ private void PlayConfirmSound(EntityUid uid, StationBankATMComponent component)
+ {
+ _audio.PlayPvs(_audio.GetSound(component.ConfirmSound), uid);
+ }
+}
diff --git a/Content.Shared/Shipyard/Events/ShipyardConsolePurchaseMessage.cs b/Content.Shared/Shipyard/Events/ShipyardConsolePurchaseMessage.cs
new file mode 100644
index 00000000000000..dd2a6e7649e8de
--- /dev/null
+++ b/Content.Shared/Shipyard/Events/ShipyardConsolePurchaseMessage.cs
@@ -0,0 +1,17 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shipyard.Events;
+
+///
+/// Purchase a Vessel from the console
+///
+[Serializable, NetSerializable]
+public sealed class ShipyardConsolePurchaseMessage : BoundUserInterfaceMessage
+{
+ public string Vessel; //vessel prototype ID
+
+ public ShipyardConsolePurchaseMessage(string vessel)
+ {
+ Vessel = vessel;
+ }
+}
diff --git a/Content.Shared/Shipyard/Events/ShipyardConsoleSellMessage.cs b/Content.Shared/Shipyard/Events/ShipyardConsoleSellMessage.cs
new file mode 100644
index 00000000000000..f5218ba797b0aa
--- /dev/null
+++ b/Content.Shared/Shipyard/Events/ShipyardConsoleSellMessage.cs
@@ -0,0 +1,14 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shipyard.Events;
+
+///
+/// Sell a Vessel from the console. The button holds no info and is doing a validation check for a deed client side, but we will still check on the server.
+///
+[Serializable, NetSerializable]
+public sealed class ShipyardConsoleSellMessage : BoundUserInterfaceMessage
+{
+ public ShipyardConsoleSellMessage()
+ {
+ }
+}
diff --git a/Content.Shared/Shipyard/Prototypes/VesselPrototype.cs b/Content.Shared/Shipyard/Prototypes/VesselPrototype.cs
new file mode 100644
index 00000000000000..c025c9feacc4cf
--- /dev/null
+++ b/Content.Shared/Shipyard/Prototypes/VesselPrototype.cs
@@ -0,0 +1,54 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Shipyard.Prototypes;
+
+[Prototype("vessel")]
+public sealed class VesselPrototype : IPrototype
+{
+ [IdDataField]
+ public string ID { get; } = default!;
+
+ ///
+ /// Vessel name.
+ ///
+ [DataField("name")] public string Name = string.Empty;
+
+ ///
+ /// Short description of the vessel.
+ ///
+ [DataField("description")] public string Description = string.Empty;
+
+ ///
+ /// The price of the vessel
+ ///
+ [DataField("price", required: true)]
+ public int Price;
+
+ ///
+ /// The category of the product. (e.g. Small, Medium, Large, Emergency, Special etc.)
+ ///
+ [DataField("category")]
+ public string Category = string.Empty;
+
+ ///
+ /// The group of the product. (e.g. Civilian, Syndicate, Contraband etc.)
+ ///
+ [DataField("group")]
+ public string Group = string.Empty;
+
+ /// Frontier - Add this field for the MapChecker script.
+ ///
+ /// The MapChecker override group for this vessel.
+ ///
+ [DataField("mapchecker_group_override")]
+ public string MapcheckerGroup = string.Empty;
+
+ ///
+ /// Relative directory path to the given shuttle, i.e. `/Maps/Shuttles/yourshittle.yml`
+ ///
+ [DataField("shuttlePath", required: true)]
+ public ResPath ShuttlePath = default!;
+}
diff --git a/Content.Shared/_NF/Bank/BUI/BankATMMenuInterfaceState.cs b/Content.Shared/_NF/Bank/BUI/BankATMMenuInterfaceState.cs
new file mode 100644
index 00000000000000..5da333d5fe225f
--- /dev/null
+++ b/Content.Shared/_NF/Bank/BUI/BankATMMenuInterfaceState.cs
@@ -0,0 +1,29 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Bank.BUI;
+
+[NetSerializable, Serializable]
+public sealed class BankATMMenuInterfaceState : BoundUserInterfaceState
+{
+ ///
+ /// bank balance of the character using the atm
+ ///
+ public int Balance;
+
+ ///
+ /// are the buttons enabled
+ ///
+ public bool Enabled;
+
+ ///
+ /// how much cash is inserted
+ ///
+ public int Deposit;
+
+ public BankATMMenuInterfaceState(int balance, bool enabled, int deposit)
+ {
+ Balance = balance;
+ Enabled = enabled;
+ Deposit = deposit;
+ }
+}
diff --git a/Content.Shared/_NF/Bank/BUI/StationBankATMMenuInterfaceState.cs b/Content.Shared/_NF/Bank/BUI/StationBankATMMenuInterfaceState.cs
new file mode 100644
index 00000000000000..a5dde050bc7b68
--- /dev/null
+++ b/Content.Shared/_NF/Bank/BUI/StationBankATMMenuInterfaceState.cs
@@ -0,0 +1,29 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Bank.BUI;
+
+[NetSerializable, Serializable]
+public sealed class StationBankATMMenuInterfaceState : BoundUserInterfaceState
+{
+ ///
+ /// bank balance of the character using the atm
+ ///
+ public int Balance;
+
+ ///
+ /// are the buttons enabled
+ ///
+ public bool Enabled;
+
+ ///
+ /// how much cash is inserted
+ ///
+ public int Deposit;
+
+ public StationBankATMMenuInterfaceState(int balance, bool enabled, int deposit)
+ {
+ Balance = balance;
+ Enabled = enabled;
+ Deposit = deposit;
+ }
+}
diff --git a/Content.Shared/_NF/Bank/Components/BankATMComponent.cs b/Content.Shared/_NF/Bank/Components/BankATMComponent.cs
new file mode 100644
index 00000000000000..3ed566c171504a
--- /dev/null
+++ b/Content.Shared/_NF/Bank/Components/BankATMComponent.cs
@@ -0,0 +1,28 @@
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Stacks;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Bank.Components;
+
+[RegisterComponent, NetworkedComponent]
+
+public sealed partial class BankATMComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite), DataField("cashType", customTypeSerializer:typeof(PrototypeIdSerializer))]
+ public string CashType = "Credit";
+
+ public static string CashSlotSlotId = "bank-ATM-cashSlot";
+
+ [DataField("bank-ATM-cashSlot")]
+ public ItemSlot CashSlot = new();
+
+ [DataField("soundError")]
+ public SoundSpecifier ErrorSound =
+ new SoundPathSpecifier("/Audio/Effects/Cargo/buzz_sigh.ogg");
+
+ [DataField("soundConfirm")]
+ public SoundSpecifier ConfirmSound =
+ new SoundPathSpecifier("/Audio/Effects/Cargo/ping.ogg");
+}
diff --git a/Content.Shared/_NF/Bank/Components/BankAccountComponent.cs b/Content.Shared/_NF/Bank/Components/BankAccountComponent.cs
new file mode 100644
index 00000000000000..d029ab277c4a8a
--- /dev/null
+++ b/Content.Shared/_NF/Bank/Components/BankAccountComponent.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Bank.Components;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class BankAccountComponent : Component
+{
+ [DataField("balance")]
+ public int Balance;
+}
+[Serializable, NetSerializable]
+public sealed partial class BankAccountComponentState : ComponentState
+{
+ public int Balance;
+}
diff --git a/Content.Shared/_NF/Bank/Components/MarketModifierComponent.cs b/Content.Shared/_NF/Bank/Components/MarketModifierComponent.cs
new file mode 100644
index 00000000000000..502bc185239f41
--- /dev/null
+++ b/Content.Shared/_NF/Bank/Components/MarketModifierComponent.cs
@@ -0,0 +1,16 @@
+namespace Content.Shared.Bank.Components;
+
+///
+/// This is used for applying a pricing modifier to things like vending machines.
+/// It's used to ensure that a purchased product costs more than it is actually worth.
+/// The float is applied to the StaticPrice component in the various systems that utilize it.
+///
+[RegisterComponent]
+public sealed partial class MarketModifierComponent : Component
+{
+ ///
+ /// The amount to multiply a Static Price by
+ ///
+ [DataField("mod", required: true)]
+ public float Mod;
+}
diff --git a/Content.Shared/_NF/Bank/Components/StationBankATMComponent.cs b/Content.Shared/_NF/Bank/Components/StationBankATMComponent.cs
new file mode 100644
index 00000000000000..28be34cb689ccc
--- /dev/null
+++ b/Content.Shared/_NF/Bank/Components/StationBankATMComponent.cs
@@ -0,0 +1,28 @@
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Stacks;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Bank.Components;
+
+[RegisterComponent, NetworkedComponent]
+
+public sealed partial class StationBankATMComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite), DataField("cashType", customTypeSerializer:typeof(PrototypeIdSerializer))]
+ public string CashType = "Credit";
+
+ public static string CashSlotSlotId = "station-bank-ATM-cashSlot";
+
+ [DataField("station-bank-ATM-cashSlot")]
+ public ItemSlot CashSlot = new();
+
+ [DataField("soundError")]
+ public SoundSpecifier ErrorSound =
+ new SoundPathSpecifier("/Audio/Effects/Cargo/buzz_sigh.ogg");
+
+ [DataField("soundConfirm")]
+ public SoundSpecifier ConfirmSound =
+ new SoundPathSpecifier("/Audio/Effects/Cargo/ping.ogg");
+}
diff --git a/Content.Shared/_NF/Bank/Events/BankDepositMessage.cs b/Content.Shared/_NF/Bank/Events/BankDepositMessage.cs
new file mode 100644
index 00000000000000..02b500d97b6f5b
--- /dev/null
+++ b/Content.Shared/_NF/Bank/Events/BankDepositMessage.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Bank.Events;
+
+///
+/// Raised on a client bank deposit
+///
+[Serializable, NetSerializable]
+
+public sealed class BankDepositMessage : BoundUserInterfaceMessage
+{
+ // an empty message because we dont really want clients to be able to send funny ints to deposit
+ public BankDepositMessage()
+ {
+ }
+}
diff --git a/Content.Shared/_NF/Bank/Events/BankWithdrawMessage.cs b/Content.Shared/_NF/Bank/Events/BankWithdrawMessage.cs
new file mode 100644
index 00000000000000..9b4b76bfd16e3c
--- /dev/null
+++ b/Content.Shared/_NF/Bank/Events/BankWithdrawMessage.cs
@@ -0,0 +1,19 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Bank.Events;
+
+///
+/// Raised on a client bank withdrawl
+///
+[Serializable, NetSerializable]
+
+public sealed class BankWithdrawMessage : BoundUserInterfaceMessage
+{
+ //amount to withdraw. validation is happening server side but we still need client input from a text field.
+ public int Amount;
+
+ public BankWithdrawMessage(int amount)
+ {
+ Amount = amount;
+ }
+}
diff --git a/Content.Shared/_NF/Bank/Events/StationBankDepositMessage.cs b/Content.Shared/_NF/Bank/Events/StationBankDepositMessage.cs
new file mode 100644
index 00000000000000..ddbc074449d4eb
--- /dev/null
+++ b/Content.Shared/_NF/Bank/Events/StationBankDepositMessage.cs
@@ -0,0 +1,22 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Bank.Events;
+
+///
+/// Raised on a client bank deposit
+///
+[Serializable, NetSerializable]
+
+public sealed class StationBankDepositMessage : BoundUserInterfaceMessage
+{
+ //amount to deposit. validation is happening server side but we still need client input from a text field.
+ public int Amount;
+ public string? Reason;
+ public string? Description;
+ public StationBankDepositMessage(int amount, string? reason, string? description)
+ {
+ Amount = amount;
+ Reason = reason;
+ Description = description;
+ }
+}
diff --git a/Content.Shared/_NF/Bank/Events/StationBankWithdrawMessage.cs b/Content.Shared/_NF/Bank/Events/StationBankWithdrawMessage.cs
new file mode 100644
index 00000000000000..22cac8ab6563e8
--- /dev/null
+++ b/Content.Shared/_NF/Bank/Events/StationBankWithdrawMessage.cs
@@ -0,0 +1,22 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Bank.Events;
+
+///
+/// Raised on a client bank withdrawl
+///
+[Serializable, NetSerializable]
+
+public sealed class StationBankWithdrawMessage : BoundUserInterfaceMessage
+{
+ //amount to withdraw. validation is happening server side but we still need client input from a text field.
+ public int Amount;
+ public string? Reason;
+ public string? Description;
+ public StationBankWithdrawMessage(int amount, string? reason, string? description)
+ {
+ Amount = amount;
+ Reason = reason;
+ Description = description;
+ }
+}
diff --git a/Content.Shared/_NF/Bank/SharedBankSystem.cs b/Content.Shared/_NF/Bank/SharedBankSystem.cs
new file mode 100644
index 00000000000000..051fe8dd3d591b
--- /dev/null
+++ b/Content.Shared/_NF/Bank/SharedBankSystem.cs
@@ -0,0 +1,59 @@
+using Content.Shared.Bank.Components;
+using Content.Shared.Containers.ItemSlots;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Bank;
+
+[NetSerializable, Serializable]
+public enum BankATMMenuUiKey : byte
+{
+ ATM,
+ BlackMarket
+}
+
+public sealed partial class SharedBankSystem : EntitySystem
+{
+ [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnHandleState);
+ SubscribeLocalEvent(OnComponentInit);
+ SubscribeLocalEvent(OnComponentRemove);
+ SubscribeLocalEvent(OnComponentInit);
+ SubscribeLocalEvent(OnComponentRemove);
+ }
+
+ private void OnComponentInit(EntityUid uid, BankATMComponent component, ComponentInit args)
+ {
+ _itemSlotsSystem.AddItemSlot(uid, BankATMComponent.CashSlotSlotId, component.CashSlot);
+ }
+
+ private void OnComponentRemove(EntityUid uid, BankATMComponent component, ComponentRemove args)
+ {
+ _itemSlotsSystem.RemoveItemSlot(uid, component.CashSlot);
+ }
+
+ private void OnComponentInit(EntityUid uid, StationBankATMComponent component, ComponentInit args)
+ {
+ _itemSlotsSystem.AddItemSlot(uid, StationBankATMComponent.CashSlotSlotId, component.CashSlot);
+ }
+
+ private void OnComponentRemove(EntityUid uid, StationBankATMComponent component, ComponentRemove args)
+ {
+ _itemSlotsSystem.RemoveItemSlot(uid, component.CashSlot);
+ }
+
+ private void OnHandleState(EntityUid playerUid, BankAccountComponent component, ref ComponentHandleState args)
+ {
+ if (args.Current is not BankAccountComponentState state)
+ {
+ return;
+ }
+
+ component.Balance = state.Balance;
+ }
+}
+
diff --git a/Resources/Locale/en-US/_NF/bank/bank-ATM-component.ftl b/Resources/Locale/en-US/_NF/bank/bank-ATM-component.ftl
new file mode 100644
index 00000000000000..478dbe637618f6
--- /dev/null
+++ b/Resources/Locale/en-US/_NF/bank/bank-ATM-component.ftl
@@ -0,0 +1,25 @@
+## UI
+bank-atm-menu-title = NT Galactic Bank
+bank-atm-menu-balance-label = Account Balance:{" "}
+bank-atm-menu-no-bank = No Bank Account!
+bank-atm-menu-withdraw-button = Withdraw
+bank-atm-menu-deposit-label = Deposit Amount:{" "}
+bank-atm-menu-no-deposit = Empty
+bank-atm-menu-deposit-button = Deposit
+bank-insufficient-funds = Insufficient Funds
+bank-atm-menu-transaction-denied = Transaction Denied
+bank-atm-menu-deposit-successful = Deposit Accepted
+bank-atm-menu-withdraw-successful = Transfer Approved
+bank-atm-menu-wrong-cash = Wrong Currency Type
+station-bank-atm-menu-title = Station Administration
+bank-atm-menu-amount-label = Amount:{" "}
+bank-atm-reason-label = For:{" "}
+bank-atm-description-label = Description:{" "}
+station-bank-payroll = Payroll
+station-bank-workorder = Work Order
+station-bank-supplies = Station Supplies
+station-bank-bounty = Bounty
+station-bank-other = Other
+station-bank-required = {"("}Required{")"}
+station-bank-requires-reason = NT Requires transaction details
+station-bank-unauthorized = Unauthorized!
\ No newline at end of file
diff --git a/Resources/Prototypes/_NF/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/_NF/Entities/Structures/Machines/Computers/computers.yml
index b4ea1018719723..5de82102fefb4a 100644
--- a/Resources/Prototypes/_NF/Entities/Structures/Machines/Computers/computers.yml
+++ b/Resources/Prototypes/_NF/Entities/Structures/Machines/Computers/computers.yml
@@ -200,101 +200,101 @@
- map: ["computerLayerKeys"]
state: blackmarket_key
-- type: entity
- name: cargo sale computer
- suffix: Normal
- parent: BaseStructureComputer
- id: ComputerPalletConsoleNFNormalMarket
- description: Used to sell goods loaded onto cargo pallets
- placement:
- mode: SnapgridCenter
- components:
- - type: MeleeSound
- soundGroups:
- Brute:
- path:
- "/Audio/Effects/glass_hit.ogg"
- - type: Computer
- - type: ApcPowerReceiver
- powerLoad: 200
- - type: ExtensionCableReceiver
- - type: ActivatableUIRequiresPower
- - type: Sprite
- netsync: false
- noRot: true
- sprite: Structures/Machines/computers.rsi
- layers:
- - map: ["computerLayerBody"]
- state: computer
- - map: ["computerLayerKeyboard"]
- state: generic_keyboard
- - map: ["computerLayerScreen"]
- state: request
- - map: ["computerLayerKeys"]
- state: tech_key
- - type: Appearance
- - type: GenericVisualizer
- visuals:
- enum.ComputerVisuals.Powered:
- computerLayerScreen:
- True: { visible: true, shader: unshaded }
- False: { visible: false }
- computerLayerKeys:
- True: { visible: true, shader: unshaded }
- False: { visible: true, shader: shaded }
- - type: LitOnPowered
- - type: PointLight
- radius: 1.5
- energy: 1.6
- color: "#b89f25"
- enabled: false
- mask: /Textures/Effects/LightMasks/cone.png
- autoRot: true
- offset: "0, 0.4" # shine from the top, not bottom of the computer
- castShadows: false
- - type: EmitSoundOnUIOpen
- sound:
- collection: Keyboard
- - type: CargoPalletConsole
- - type: ActivatableUI
- key: enum.CargoPalletConsoleUiKey.Sale
- - type: UserInterface
- interfaces:
- - key: enum.CargoPalletConsoleUiKey.Sale
- type: CargoPalletConsoleBoundUserInterface
- - type: Anchorable
- delay: 999999
- - type: MarketModifier
- mod: 1.00
- - type: Destructible
- thresholds:
- - trigger:
- !type:DamageTrigger
- damage: 0
+# - type: entity
+ # name: cargo sale computer
+ # suffix: Normal
+ # parent: BaseStructureComputer
+ # id: ComputerPalletConsoleNFNormalMarket
+ # description: Used to sell goods loaded onto cargo pallets
+ # placement:
+ # mode: SnapgridCenter
+ # components:
+ # - type: MeleeSound
+ # soundGroups:
+ # Brute:
+ # path:
+ # "/Audio/Effects/glass_hit.ogg"
+ # - type: Computer
+ # - type: ApcPowerReceiver
+ # powerLoad: 200
+ # - type: ExtensionCableReceiver
+ # - type: ActivatableUIRequiresPower
+ # - type: Sprite
+ # netsync: false
+ # noRot: true
+ # sprite: Structures/Machines/computers.rsi
+ # layers:
+ # - map: ["computerLayerBody"]
+ # state: computer
+ # - map: ["computerLayerKeyboard"]
+ # state: generic_keyboard
+ # - map: ["computerLayerScreen"]
+ # state: request
+ # - map: ["computerLayerKeys"]
+ # state: tech_key
+ # - type: Appearance
+ # - type: GenericVisualizer
+ # visuals:
+ # enum.ComputerVisuals.Powered:
+ # computerLayerScreen:
+ # True: { visible: true, shader: unshaded }
+ # False: { visible: false }
+ # computerLayerKeys:
+ # True: { visible: true, shader: unshaded }
+ # False: { visible: true, shader: shaded }
+ # - type: LitOnPowered
+ # - type: PointLight
+ # radius: 1.5
+ # energy: 1.6
+ # color: "#b89f25"
+ # enabled: false
+ # mask: /Textures/Effects/LightMasks/cone.png
+ # autoRot: true
+ # offset: "0, 0.4" # shine from the top, not bottom of the computer
+ # castShadows: false
+ # - type: EmitSoundOnUIOpen
+ # sound:
+ # collection: Keyboard
+ # - type: CargoPalletConsole
+ # - type: ActivatableUI
+ # key: enum.CargoPalletConsoleUiKey.Sale
+ # - type: UserInterface
+ # interfaces:
+ # - key: enum.CargoPalletConsoleUiKey.Sale
+ # type: CargoPalletConsoleBoundUserInterface
+ # - type: Anchorable
+ # delay: 999999
+ # - type: MarketModifier
+ # mod: 1.00
+ # - type: Destructible
+ # thresholds:
+ # - trigger:
+ # !type:DamageTrigger
+ # damage: 0
-- type: entity
- parent: ComputerPalletConsoleNFNormalMarket
- id: ComputerPalletConsoleNFHighMarket
- suffix: High
- description: Used to sell goods loaded onto cargo pallets
- components:
- - type: MarketModifier
- mod: 1.59
+# - type: entity
+ # parent: ComputerPalletConsoleNFNormalMarket
+ # id: ComputerPalletConsoleNFHighMarket
+ # suffix: High
+ # description: Used to sell goods loaded onto cargo pallets
+ # components:
+ # - type: MarketModifier
+ # mod: 1.59
-- type: entity
- parent: ComputerPalletConsoleNFNormalMarket
- id: ComputerPalletConsoleNFLowMarket
- suffix: Low
- description: Used to sell goods loaded onto cargo pallets
- components:
- - type: MarketModifier
- mod: 0.698
+# - type: entity
+ # parent: ComputerPalletConsoleNFNormalMarket
+ # id: ComputerPalletConsoleNFLowMarket
+ # suffix: Low
+ # description: Used to sell goods loaded onto cargo pallets
+ # components:
+ # - type: MarketModifier
+ # mod: 0.698
-- type: entity
- parent: ComputerPalletConsoleNFNormalMarket
- id: ComputerPalletConsoleNFVeryLowMarket
- suffix: VeryLow
- description: Used to sell goods loaded onto cargo pallets
- components:
- - type: MarketModifier
- mod: 0.40
+# - type: entity
+ # parent: ComputerPalletConsoleNFNormalMarket
+ # id: ComputerPalletConsoleNFVeryLowMarket
+ # suffix: VeryLow
+ # description: Used to sell goods loaded onto cargo pallets
+ # components:
+ # - type: MarketModifier
+ # mod: 0.40
diff --git a/Resources/Prototypes/_NF/Entities/Structures/Machines/Computers/mothership-computers.yml b/Resources/Prototypes/_NF/Entities/Structures/Machines/Computers/mothership-computers.yml
deleted file mode 100644
index 07a1a5a83b295b..00000000000000
--- a/Resources/Prototypes/_NF/Entities/Structures/Machines/Computers/mothership-computers.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-# The empress console
-- type: entity
- id: EmpressMothershipComputer
- name: Empress mothership console
- parent: BaseMothershipComputer
- components:
- - type: Sprite
- sprite: _NF/Structures/Machines/computers.rsi
- layers:
- - map: ["computerLayerBody"]
- state: computer
- - map: ["computerLayerKeyboard"]
- state: generic_keyboard
- - map: ["computerLayerScreen"]
- state: shipyard_security
- - map: ["computerLayerKeys"]
- state: telesci_key
- - type: ShipyardListing
- shuttles:
- - Fighter
- - Cleric
- - Rogue