]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Departmental Economy (#36445)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Sun, 13 Apr 2025 13:22:36 +0000 (09:22 -0400)
committerGitHub <noreply@github.com>
Sun, 13 Apr 2025 13:22:36 +0000 (15:22 +0200)
* Cargo Accounts, Request Consoles, and lock boxes

* Funding Allocation Computer

* final changes

* test fix

* remove dumb code

* ScarKy0 review

* first cour

* second cour

* Update machines.yml

* review

---------

Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: Milon <milonpl.git@proton.me>
62 files changed:
Content.Client/Cargo/BUI/CargoOrderConsoleBoundUserInterface.cs
Content.Client/Cargo/BUI/FundingAllocationConsoleBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Cargo/UI/CargoConsoleMenu.xaml
Content.Client/Cargo/UI/CargoConsoleMenu.xaml.cs
Content.Client/Cargo/UI/CargoOrderRow.xaml
Content.Client/Cargo/UI/CargoProductRow.xaml
Content.Client/Cargo/UI/FundingAllocationMenu.xaml [new file with mode: 0644]
Content.Client/Cargo/UI/FundingAllocationMenu.xaml.cs [new file with mode: 0644]
Content.Server/Cargo/Components/CargoPalletComponent.cs
Content.Server/Cargo/Components/CargoPalletConsoleComponent.cs
Content.Server/Cargo/Components/StationBankAccountComponent.cs [deleted file]
Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs
Content.Server/Cargo/Systems/CargoSystem.Bounty.cs
Content.Server/Cargo/Systems/CargoSystem.Funds.cs [new file with mode: 0644]
Content.Server/Cargo/Systems/CargoSystem.Orders.cs
Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
Content.Server/Cargo/Systems/CargoSystem.Telepad.cs
Content.Server/Cargo/Systems/CargoSystem.cs
Content.Server/Delivery/DeliverySystem.cs
Content.Server/Stack/StackSystem.cs
Content.Server/Station/Systems/StationSystem.cs
Content.Server/StationEvents/Components/CargoGiftsRuleComponent.cs
Content.Server/StationEvents/Events/CargoGiftsRule.cs
Content.Shared/Cargo/BUI/CargoConsoleInterfaceState.cs
Content.Shared/Cargo/Components/BankClientComponent.cs [deleted file]
Content.Shared/Cargo/Components/CargoOrderConsoleComponent.cs
Content.Shared/Cargo/Components/FundingAllocationConsoleComponent.cs [new file with mode: 0644]
Content.Shared/Cargo/Components/OverrideSellComponent.cs [new file with mode: 0644]
Content.Shared/Cargo/Components/StationBankAccountComponent.cs [new file with mode: 0644]
Content.Shared/Cargo/Prototypes/CargoAccountPrototype.cs [new file with mode: 0644]
Content.Shared/Cargo/SharedCargoSystem.cs
Resources/Locale/en-US/cargo/cargo-accounts.ftl [new file with mode: 0644]
Resources/Locale/en-US/cargo/cargo-console-component.ftl
Resources/Prototypes/Catalog/Cargo/cargo_lockbox.yml [new file with mode: 0644]
Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
Resources/Prototypes/Catalog/cargo_accounts.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml
Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
Resources/Prototypes/Entities/Objects/Misc/paper.yml
Resources/Prototypes/Entities/Stations/base.yml
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml
Resources/Prototypes/SoundCollections/machines.yml [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/allocate.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/meta.json
Resources/Textures/Structures/Machines/computers.rsi/request-eng.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/request-med.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/request-sci.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/request-sec.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/request-srv.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/transfer.png [new file with mode: 0644]
Resources/Textures/Structures/Storage/Crates/lockbox.rsi/base.png [new file with mode: 0644]
Resources/Textures/Structures/Storage/Crates/lockbox.rsi/closed.png [new file with mode: 0644]
Resources/Textures/Structures/Storage/Crates/lockbox.rsi/icon.png [new file with mode: 0644]
Resources/Textures/Structures/Storage/Crates/lockbox.rsi/locked.png [new file with mode: 0644]
Resources/Textures/Structures/Storage/Crates/lockbox.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Structures/Storage/Crates/lockbox.rsi/open.png [new file with mode: 0644]
Resources/Textures/Structures/Storage/Crates/lockbox.rsi/overlay-closed.png [new file with mode: 0644]
Resources/Textures/Structures/Storage/Crates/lockbox.rsi/overlay.png [new file with mode: 0644]
Resources/Textures/Structures/Storage/Crates/lockbox.rsi/sparking.png [new file with mode: 0644]
Resources/Textures/Structures/Storage/Crates/lockbox.rsi/unlocked.png [new file with mode: 0644]
Resources/Textures/Structures/Storage/Crates/lockbox.rsi/welded.png [new file with mode: 0644]

index 0be3ebd97f810c009397534e447880b63af7e17b..52846cefdb9388214cb4363242a8cdefa4c49633 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Shared.Cargo;
 using Content.Client.Cargo.UI;
 using Content.Shared.Cargo.BUI;
+using Content.Shared.Cargo.Components;
 using Content.Shared.Cargo.Events;
 using Content.Shared.Cargo.Prototypes;
 using Content.Shared.IdentityManagement;
@@ -14,6 +15,8 @@ namespace Content.Client.Cargo.BUI
 {
     public sealed class CargoOrderConsoleBoundUserInterface : BoundUserInterface
     {
+        private readonly SharedCargoSystem _cargoSystem;
+
         [ViewVariables]
         private CargoConsoleMenu? _menu;
 
@@ -43,6 +46,7 @@ namespace Content.Client.Cargo.BUI
 
         public CargoOrderConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
         {
+            _cargoSystem = EntMan.System<SharedCargoSystem>();
         }
 
         protected override void Open()
@@ -57,7 +61,7 @@ namespace Content.Client.Cargo.BUI
 
             string orderRequester;
 
-            if (EntMan.TryGetComponent<MetaDataComponent>(localPlayer, out var metadata))
+            if (EntMan.EntityExists(localPlayer))
                 orderRequester = Identity.Name(localPlayer.Value, EntMan);
             else
                 orderRequester = string.Empty;
@@ -96,41 +100,54 @@ namespace Content.Client.Cargo.BUI
                 }
             };
 
+            _menu.OnAccountAction += (account, amount) =>
+            {
+                SendMessage(new CargoConsoleWithdrawFundsMessage(account, amount));
+            };
+
+            _menu.OnToggleUnboundedLimit += _ =>
+            {
+                SendMessage(new CargoConsoleToggleLimitMessage());
+            };
+
             _menu.OpenCentered();
         }
 
         private void Populate(List<CargoOrderData> orders)
         {
-            if (_menu == null) return;
+            if (_menu == null)
+                return;
 
             _menu.PopulateProducts();
             _menu.PopulateCategories();
             _menu.PopulateOrders(orders);
+            _menu.PopulateAccountActions();
         }
 
         protected override void UpdateState(BoundUserInterfaceState state)
         {
             base.UpdateState(state);
 
-            if (state is not CargoConsoleInterfaceState cState)
+            if (state is not CargoConsoleInterfaceState cState || !EntMan.TryGetComponent<CargoOrderConsoleComponent>(Owner, out var orderConsole))
                 return;
+            var station = EntMan.GetEntity(cState.Station);
 
             OrderCapacity = cState.Capacity;
             OrderCount = cState.Count;
-            BankBalance = cState.Balance;
+            BankBalance = _cargoSystem.GetBalanceFromAccount(station, orderConsole.Account);
 
             AccountName = cState.Name;
 
+            _menu?.UpdateStation(station);
             Populate(cState.Orders);
-            _menu?.UpdateCargoCapacity(OrderCount, OrderCapacity);
-            _menu?.UpdateBankData(AccountName, BankBalance);
         }
 
         protected override void Dispose(bool disposing)
         {
             base.Dispose(disposing);
 
-            if (!disposing) return;
+            if (!disposing)
+                return;
 
             _menu?.Dispose();
             _orderMenu?.Dispose();
@@ -170,8 +187,6 @@ namespace Content.Client.Cargo.BUI
                 return;
 
             SendMessage(new CargoConsoleApproveOrderMessage(row.Order.OrderId));
-            // Most of the UI isn't predicted anyway so.
-            // _menu?.UpdateCargoCapacity(OrderCount + row.Order.Amount, OrderCapacity);
         }
     }
 }
diff --git a/Content.Client/Cargo/BUI/FundingAllocationConsoleBoundUserInterface.cs b/Content.Client/Cargo/BUI/FundingAllocationConsoleBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..eb65be4
--- /dev/null
@@ -0,0 +1,35 @@
+using Content.Client.Cargo.UI;
+using Content.Shared.Cargo.Components;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Cargo.BUI;
+
+[UsedImplicitly]
+public sealed class FundingAllocationConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
+{
+    [ViewVariables]
+    private FundingAllocationMenu? _menu;
+
+    protected override void Open()
+    {
+        base.Open();
+
+        _menu = this.CreateWindow<FundingAllocationMenu>();
+
+        _menu.OnSavePressed += d =>
+        {
+            SendMessage(new SetFundingAllocationBuiMessage(d));
+        };
+    }
+
+    protected override void UpdateState(BoundUserInterfaceState message)
+    {
+        base.UpdateState(message);
+
+        if (message is not FundingAllocationConsoleBuiState state)
+            return;
+
+        _menu?.Update(state);
+    }
+}
index dd7a9f110cab7b99e8bdd48e912610038ff7ea99..72d8cf7daf05c8741bd32ed9b6072f02d6671175 100644 (file)
@@ -3,66 +3,83 @@
                            xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
                            SetSize="600 600"
                            MinSize="600 600">
-    <BoxContainer Orientation="Vertical" Margin="5 0 5 0">
+    <BoxContainer Orientation="Vertical" Margin="15 5 15 10">
         <BoxContainer Orientation="Horizontal">
             <Label Text="{Loc 'cargo-console-menu-account-name-label'}"
                    StyleClasses="LabelKeyText" />
-            <Label Name="AccountNameLabel"
+            <RichTextLabel Name="AccountNameLabel"
                    Text="{Loc 'cargo-console-menu-account-name-none-text'}" />
         </BoxContainer>
         <BoxContainer Orientation="Horizontal">
             <Label Text="{Loc 'cargo-console-menu-points-label'}"
                    StyleClasses="LabelKeyText" />
-            <Label Name="PointsLabel"
+            <RichTextLabel Name="PointsLabel"
                    Text="$0" />
         </BoxContainer>
-        <BoxContainer Orientation="Horizontal">
-            <Label Text="{Loc 'cargo-console-menu-order-capacity-label'}"
-                   StyleClasses="LabelKeyText" />
-            <Label Name="ShuttleCapacityLabel"
-                   Text="0/20" />
-        </BoxContainer>
-        <BoxContainer Orientation="Horizontal">
-            <OptionButton Name="Categories"
-                          Prefix="{Loc 'cargo-console-menu-categories-label'}"
-                          HorizontalExpand="True" />
-            <LineEdit Name="SearchBar"
-                      PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
-                      HorizontalExpand="True" />
-        </BoxContainer>
-        <ScrollContainer HorizontalExpand="True"
-                         VerticalExpand="True"
-                         SizeFlagsStretchRatio="6">
-            <BoxContainer Name="Products"
-                          Orientation="Vertical"
-                          HorizontalExpand="True"
-                          VerticalExpand="True">
-                <!-- Products get added here by code -->
-            </BoxContainer>
-        </ScrollContainer>
-        <PanelContainer VerticalExpand="True"
-                        SizeFlagsStretchRatio="6">
-            <PanelContainer.PanelOverride>
-                <gfx:StyleBoxFlat BackgroundColor="#000000" />
-            </PanelContainer.PanelOverride>
-            <ScrollContainer VerticalExpand="True">
-                <BoxContainer Orientation="Vertical">
-                    <Label Text="{Loc 'cargo-console-menu-requests-label'}" />
-                    <BoxContainer Name="Requests"
-                                  Orientation="Vertical"
-                                  VerticalExpand="True">
-                        <!-- Requests are added here by code -->
-                    </BoxContainer>
-                    <Label Text="{Loc 'cargo-console-menu-orders-label'}" />
-                    <BoxContainer Name="Orders"
+        <Control MinHeight="10"/>
+        <TabContainer Name="TabContainer" VerticalExpand="True">
+            <BoxContainer Orientation="Vertical" VerticalExpand="True">
+                <BoxContainer Orientation="Horizontal">
+                    <OptionButton Name="Categories"
+                                  Prefix="{Loc 'cargo-console-menu-categories-label'}"
+                                  HorizontalExpand="True" />
+                    <LineEdit Name="SearchBar"
+                              PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
+                              HorizontalExpand="True" />
+                </BoxContainer>
+                <Control MinHeight="5"/>
+                <ScrollContainer HorizontalExpand="True"
+                                 VerticalExpand="True"
+                                 SizeFlagsStretchRatio="2">
+                    <BoxContainer Name="Products"
                                   Orientation="Vertical"
-                                  StyleClasses="transparentItemList"
+                                  HorizontalExpand="True"
                                   VerticalExpand="True">
-                        <!-- Orders are added here by code -->
+                        <!-- Products get added here by code -->
                     </BoxContainer>
+                </ScrollContainer>
+                <Control MinHeight="5"/>
+                <PanelContainer VerticalExpand="True"
+                                SizeFlagsStretchRatio="1">
+                    <PanelContainer.PanelOverride>
+                        <gfx:StyleBoxFlat BackgroundColor="#000000" />
+                    </PanelContainer.PanelOverride>
+                    <ScrollContainer VerticalExpand="True">
+                        <BoxContainer Orientation="Vertical" Margin="5">
+                            <Label Text="{Loc 'cargo-console-menu-requests-label'}" />
+                            <BoxContainer Name="Requests"
+                                          Orientation="Vertical"
+                                          VerticalExpand="True">
+                                <!-- Requests are added here by code -->
+                            </BoxContainer>
+                        </BoxContainer>
+                    </ScrollContainer>
+                </PanelContainer>
+            </BoxContainer>
+            <!-- Funds tab -->
+            <BoxContainer Orientation="Vertical" Margin="15">
+                <BoxContainer Orientation="Horizontal">
+                    <RichTextLabel Name="TransferLimitLabel" Margin="0 0 15 0"/>
+                    <RichTextLabel Name="UnlimitedNotifier" Text="{Loc 'cargo-console-menu-account-action-transfer-limit-unlimited-notifier'}"/>
+                </BoxContainer>
+                <BoxContainer Orientation="Horizontal">
+                    <RichTextLabel Text="{Loc 'cargo-console-menu-account-action-select'}" Margin="0 0 10 0"/>
+                    <OptionButton Name="ActionOptions"/>
                 </BoxContainer>
-            </ScrollContainer>
-        </PanelContainer>
-        <TextureButton VerticalExpand="True" />
+                <Control MinHeight="5"/>
+                <BoxContainer Orientation="Horizontal">
+                    <RichTextLabel Name="AmountText" Text="{ Loc 'cargo-console-menu-account-action-amount'}"/>
+                    <SpinBox Name="TransferSpinBox" MinWidth="100" Value="10"/>
+                </BoxContainer>
+                <Control MinHeight="15"/>
+                <BoxContainer HorizontalAlignment="Center">
+                    <Button Name="AccountActionButton" Text="{ Loc 'cargo-console-menu-account-action-button'}" MinHeight="45" MinWidth="120"/>
+                </BoxContainer>
+                <Control VerticalExpand="True"/>
+                <BoxContainer VerticalAlignment="Bottom" HorizontalAlignment="Center">
+                    <Button Name="AccountLimitToggleButton" Text="{ Loc 'cargo-console-menu-toggle-account-lock-button'}" MinHeight="45" MinWidth="120"/>
+                </BoxContainer>
+            </BoxContainer>
+        </TabContainer>
     </BoxContainer>
 </controls:FancyWindow>
index e60335bc45c3c9b4e7bd454f9c529cb30f03a68e..b837d59855f23c79907ea2a77698cfef5330e492 100644 (file)
@@ -1,4 +1,5 @@
 using System.Linq;
+using Content.Client.Cargo.Systems;
 using Content.Client.UserInterface.Controls;
 using Content.Shared.Cargo;
 using Content.Shared.Cargo.Components;
@@ -8,6 +9,7 @@ using Robust.Client.GameObjects;
 using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.XAML;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
 using static Robust.Client.UserInterface.Controls.BaseButton;
 
 namespace Content.Client.Cargo.UI
@@ -15,30 +17,83 @@ namespace Content.Client.Cargo.UI
     [GenerateTypedNameReferences]
     public sealed partial class CargoConsoleMenu : FancyWindow
     {
-        private IEntityManager _entityManager;
-        private IPrototypeManager _protoManager;
-        private SpriteSystem _spriteSystem;
+        [Dependency] private readonly IGameTiming _timing = default!;
+
+        private readonly IEntityManager _entityManager;
+        private readonly IPrototypeManager _protoManager;
+        private readonly CargoSystem _cargoSystem;
+        private readonly SpriteSystem _spriteSystem;
         private EntityUid _owner;
+        private EntityUid? _station;
+
+        private readonly EntityQuery<CargoOrderConsoleComponent> _orderConsoleQuery;
+        private readonly EntityQuery<StationBankAccountComponent> _bankQuery;
 
         public event Action<ButtonEventArgs>? OnItemSelected;
         public event Action<ButtonEventArgs>? OnOrderApproved;
         public event Action<ButtonEventArgs>? OnOrderCanceled;
 
+        public event Action<ProtoId<CargoAccountPrototype>?, int>? OnAccountAction;
+
+        public event Action<ButtonEventArgs>? OnToggleUnboundedLimit;
+
         private readonly List<string> _categoryStrings = new();
         private string? _category;
 
         public CargoConsoleMenu(EntityUid owner, IEntityManager entMan, IPrototypeManager protoManager, SpriteSystem spriteSystem)
         {
             RobustXamlLoader.Load(this);
+            IoCManager.InjectDependencies(this);
             _entityManager = entMan;
             _protoManager = protoManager;
+            _cargoSystem = entMan.System<CargoSystem>();
             _spriteSystem = spriteSystem;
             _owner = owner;
 
-            Title = Loc.GetString("cargo-console-menu-title");
+            _orderConsoleQuery = _entityManager.GetEntityQuery<CargoOrderConsoleComponent>();
+            _bankQuery = _entityManager.GetEntityQuery<StationBankAccountComponent>();
+
+            Title = entMan.GetComponent<MetaDataComponent>(owner).EntityName;
 
             SearchBar.OnTextChanged += OnSearchBarTextChanged;
             Categories.OnItemSelected += OnCategoryItemSelected;
+
+            if (entMan.TryGetComponent<CargoOrderConsoleComponent>(owner, out var orderConsole))
+            {
+                var accountProto = _protoManager.Index(orderConsole.Account);
+                AccountNameLabel.Text = Loc.GetString("cargo-console-menu-account-name-format",
+                    ("color", accountProto.Color),
+                    ("name", Loc.GetString(accountProto.Name)),
+                    ("code", Loc.GetString(accountProto.Code)));
+            }
+
+            TabContainer.SetTabTitle(0, Loc.GetString("cargo-console-menu-tab-title-orders"));
+            TabContainer.SetTabTitle(1, Loc.GetString("cargo-console-menu-tab-title-funds"));
+
+            ActionOptions.OnItemSelected += idx =>
+            {
+                ActionOptions.SelectId(idx.Id);
+            };
+
+            TransferSpinBox.IsValid = val =>
+            {
+                if (!_entityManager.TryGetComponent<CargoOrderConsoleComponent>(owner, out var console) ||
+                    !_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank))
+                    return true;
+
+                return val >= 0 && val <= (int) (console.TransferLimit * bank.Accounts[console.Account]);
+            };
+
+            AccountActionButton.OnPressed += _ =>
+            {
+                var account = (ProtoId<CargoAccountPrototype>?) ActionOptions.SelectedMetadata;
+                OnAccountAction?.Invoke(account, TransferSpinBox.Value);
+            };
+
+            AccountLimitToggleButton.OnPressed += a =>
+            {
+                OnToggleUnboundedLimit?.Invoke(a);
+            };
         }
 
         private void OnCategoryItemSelected(OptionButton.ItemSelectedEventArgs args)
@@ -144,11 +199,13 @@ namespace Content.Client.Cargo.UI
         /// </summary>
         public void PopulateOrders(IEnumerable<CargoOrderData> orders)
         {
-            Orders.DisposeAllChildren();
             Requests.DisposeAllChildren();
 
             foreach (var order in orders)
             {
+                if (order.Approved)
+                    continue;
+
                 var product = _protoManager.Index<EntityPrototype>(order.ProductId);
                 var productName = product.Name;
 
@@ -164,35 +221,67 @@ namespace Content.Client.Cargo.UI
                             ("orderAmount", order.OrderQuantity),
                             ("orderRequester", order.Requester))
                     },
-                    Description = {Text = Loc.GetString("cargo-console-menu-order-reason-description",
-                                                        ("reason", order.Reason))}
+                    Description =
+                    {
+                        Text = Loc.GetString("cargo-console-menu-order-reason-description",
+                                                        ("reason", order.Reason))
+                    }
                 };
                 row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(args); };
-                if (order.Approved)
-                {
-                    row.Approve.Visible = false;
-                    row.Cancel.Visible = false;
-                    Orders.AddChild(row);
-                }
-                else
-                {
-                    // TODO: Disable based on access.
-                    row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(args); };
-                    Requests.AddChild(row);
-                }
+
+                // TODO: Disable based on access.
+                row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(args); };
+                Requests.AddChild(row);
             }
         }
 
-        public void UpdateCargoCapacity(int count, int capacity)
+        public void PopulateAccountActions()
         {
-            // TODO: Rename + Loc.
-            ShuttleCapacityLabel.Text = $"{count}/{capacity}";
+            if (!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank) ||
+                !_entityManager.TryGetComponent<CargoOrderConsoleComponent>(_owner, out var console))
+                return;
+
+            var i = 0;
+            ActionOptions.Clear();
+            ActionOptions.AddItem(Loc.GetString("cargo-console-menu-account-action-option-withdraw"), i);
+            i++;
+            foreach (var account in bank.Accounts.Keys)
+            {
+                if (account == console.Account)
+                    continue;
+                var accountProto = _protoManager.Index(account);
+                ActionOptions.AddItem(Loc.GetString("cargo-console-menu-account-action-option-transfer",
+                    ("code", Loc.GetString(accountProto.Code))),
+                    i);
+                ActionOptions.SetItemMetadata(i, account);
+                i++;
+            }
         }
 
-        public void UpdateBankData(string name, int points)
+        public void UpdateStation(EntityUid station)
         {
-            AccountNameLabel.Text = name;
-            PointsLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", points.ToString()));
+            _station = station;
+        }
+
+        protected override void FrameUpdate(FrameEventArgs args)
+        {
+            base.FrameUpdate(args);
+
+            if (!_bankQuery.TryComp(_station, out var bankAccount) ||
+                !_orderConsoleQuery.TryComp(_owner, out var orderConsole))
+            {
+                return;
+            }
+
+            var balance = _cargoSystem.GetBalanceFromAccount((_station.Value, bankAccount), orderConsole.Account);
+            PointsLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", balance));
+            TransferLimitLabel.Text = Loc.GetString("cargo-console-menu-account-action-transfer-limit",
+                ("limit", (int) (balance * orderConsole.TransferLimit)));
+
+            UnlimitedNotifier.Visible = orderConsole.TransferUnbounded;
+            AccountActionButton.Disabled = TransferSpinBox.Value <= 0 ||
+                                           TransferSpinBox.Value > bankAccount.Accounts[orderConsole.Account] * orderConsole.TransferLimit ||
+                                           _timing.CurTime < orderConsole.NextAccountActionTime;
         }
     }
 }
index 1d636a1a292e6df004ac2921532d333b3303bd64..22bd2291aefda51c8a2a475a03aed1ed43a8f487 100644 (file)
@@ -1,11 +1,13 @@
 <PanelContainer xmlns="https://spacestation14.io"
-                HorizontalExpand="True">
+                HorizontalExpand="True"
+                Margin="0 1">
     <BoxContainer Orientation="Horizontal"
                   HorizontalExpand="True">
         <TextureRect Name="Icon"
                      Access="Public"
                      MinSize="32 32"
                      RectClipContent="True" />
+        <Control MinWidth="5"/>
         <BoxContainer Orientation="Vertical"
                       HorizontalExpand="True"
                       VerticalExpand="True">
         <Button Name="Approve"
                 Access="Public"
                 Text="{Loc 'cargo-console-menu-cargo-order-row-approve-button'}"
-                StyleClasses="LabelSubText" />
+                StyleClasses="OpenRight" />
         <Button Name="Cancel"
                 Access="Public"
                 Text="{Loc 'cargo-console-menu-cargo-order-row-cancel-button'}"
-                StyleClasses="LabelSubText" />
+                StyleClasses="OpenLeft" />
     </BoxContainer>
 </PanelContainer>
index b47a1e8db226aea0637280eed3de9adb11cdfd7d..4ae7a76e1d4265d12820e30a170d9670e19d1eef 100644 (file)
@@ -4,7 +4,8 @@
             ToolTip=""
             Access="Public"
             HorizontalExpand="True"
-            VerticalExpand="True" />
+            VerticalExpand="True"
+            StyleClasses="OpenBoth"/>
     <BoxContainer Orientation="Horizontal"
                   HorizontalExpand="True">
         <TextureRect Name="Icon"
@@ -18,7 +19,8 @@
             <Label Name="PointCost"
                    Access="Public"
                    MinSize="52 32"
-                   Align="Right" />
+                   Align="Right"
+                   Margin="0 0 5 0"/>
         </PanelContainer>
     </BoxContainer>
 </PanelContainer>
diff --git a/Content.Client/Cargo/UI/FundingAllocationMenu.xaml b/Content.Client/Cargo/UI/FundingAllocationMenu.xaml
new file mode 100644 (file)
index 0000000..0686ea7
--- /dev/null
@@ -0,0 +1,29 @@
+<controls:FancyWindow xmlns="https://spacestation14.io"
+                      xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+                      xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+                      Title="{Loc 'cargo-funding-alloc-console-menu-title'}"
+                      SetSize="680 310"
+                      MinSize="680 310">
+    <BoxContainer Orientation="Vertical"
+                  VerticalExpand="True"
+                  HorizontalExpand="True"
+                  Margin="10 5 10 10">
+        <Label Name="HelpLabel" HorizontalAlignment="Center" StyleClasses="LabelSubText"/>
+        <Control MinHeight="10"/>
+        <PanelContainer VerticalExpand="True" HorizontalExpand="True" VerticalAlignment="Top" MaxHeight="250">
+            <PanelContainer.PanelOverride>
+                <graphics:StyleBoxFlat BackgroundColor="#1B1B1E"/>
+            </PanelContainer.PanelOverride>
+            <controls:TableContainer Name="EntriesContainer" Columns="4" HorizontalExpand="True" VerticalExpand="True" Margin="5 0">
+                <RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-account'}" HorizontalAlignment="Center"/>
+                <RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-code'}" HorizontalAlignment="Center"/>
+                <RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-balance'}" HorizontalAlignment="Center"/>
+                <RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-cut'}" HorizontalAlignment="Center"/>
+            </controls:TableContainer>
+        </PanelContainer>
+        <BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5 0">
+            <Button Name="SaveButton" Text="{Loc 'cargo-funding-alloc-console-button-save'}"  Disabled="True"/>
+            <RichTextLabel Name="SaveAlertLabel" HorizontalExpand="True" HorizontalAlignment="Right" Visible="False"/>
+        </BoxContainer>
+    </BoxContainer>
+</controls:FancyWindow>
diff --git a/Content.Client/Cargo/UI/FundingAllocationMenu.xaml.cs b/Content.Client/Cargo/UI/FundingAllocationMenu.xaml.cs
new file mode 100644 (file)
index 0000000..d605d4c
--- /dev/null
@@ -0,0 +1,169 @@
+using System.Linq;
+using Content.Client.Message;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Cargo.Components;
+using Content.Shared.Cargo.Prototypes;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Cargo.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class FundingAllocationMenu : FancyWindow
+{
+    [Dependency] private readonly IEntityManager _entityManager = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+    private readonly EntityQuery<StationBankAccountComponent> _bankQuery;
+
+    public event Action<Dictionary<ProtoId<CargoAccountPrototype>, int>>? OnSavePressed;
+
+    private EntityUid? _station;
+
+    private readonly HashSet<Control> _addedControls = new();
+    private readonly List<SpinBox> _spinBoxes = new();
+    private readonly Dictionary<ProtoId<CargoAccountPrototype>, RichTextLabel> _balanceLabels = new();
+
+    public FundingAllocationMenu()
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+
+        _bankQuery = _entityManager.GetEntityQuery<StationBankAccountComponent>();
+
+        SaveButton.OnPressed += _ =>
+        {
+            if (!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank))
+                return;
+            var accounts = bank.Accounts.Keys.OrderBy(p => p.Id).ToList();
+            var dicts = new Dictionary<ProtoId<CargoAccountPrototype>, int>();
+            for (var i = 0; i< accounts.Count; i++)
+            {
+                dicts.Add(accounts[i], _spinBoxes[i].Value);
+            }
+
+            OnSavePressed?.Invoke(dicts);
+            SaveButton.Disabled = true;
+        };
+
+        BuildEntries();
+    }
+
+    private void BuildEntries()
+    {
+        if (!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank))
+            return;
+        HelpLabel.Text = Loc.GetString("cargo-funding-alloc-console-label-help",
+            ("percent", (int) (bank.PrimaryCut * 100)));
+
+        foreach (var ctrl in _addedControls)
+        {
+            ctrl.Orphan();
+        }
+
+        _addedControls.Clear();
+        _spinBoxes.Clear();
+        _balanceLabels.Clear();
+
+        var accounts = bank.Accounts.ToList().OrderBy(p => p.Key);
+        foreach (var (account, balance) in accounts)
+        {
+            var accountProto = _prototypeManager.Index(account);
+
+            var accountNameLabel = new RichTextLabel
+            {
+                Modulate = accountProto.Color,
+                Margin = new Thickness(0, 0, 10, 0)
+            };
+            accountNameLabel.SetMarkup($"[bold]{Loc.GetString(accountProto.Name)}[/bold]");
+            EntriesContainer.AddChild(accountNameLabel);
+
+            var codeLabel = new RichTextLabel
+            {
+                Text = $"[font=\"Monospace\"]{Loc.GetString(accountProto.Code)}[/font]",
+                HorizontalAlignment = HAlignment.Center,
+                Margin = new Thickness(5, 0),
+            };
+            EntriesContainer.AddChild(codeLabel);
+
+            var balanceLabel = new RichTextLabel
+            {
+                Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", balance)),
+                HorizontalExpand = true,
+                HorizontalAlignment = HAlignment.Center,
+                Margin = new Thickness(5, 0),
+            };
+            EntriesContainer.AddChild(balanceLabel);
+
+            var box = new SpinBox
+            {
+                HorizontalAlignment = HAlignment.Center,
+                HorizontalExpand = true,
+                Value = (int) (bank.RevenueDistribution[account] * 100),
+                IsValid = val => val is >= 0 and <= 100,
+            };
+            box.ValueChanged += _ => UpdateButtonDisabled();
+            EntriesContainer.AddChild(box);
+
+            _spinBoxes.Add(box);
+            _balanceLabels.Add(account, balanceLabel);
+            _addedControls.Add(accountNameLabel);
+            _addedControls.Add(codeLabel);
+            _addedControls.Add(balanceLabel);
+            _addedControls.Add(box);
+        }
+    }
+
+    private void UpdateButtonDisabled()
+    {
+        if (!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank))
+            return;
+
+        var sum = _spinBoxes.Sum(s => s.Value);
+        var incorrectSum = sum != 100;
+
+        var differs = false;
+        var accounts = bank.Accounts.Keys.OrderBy(p => p.Id).ToList();
+        for (var i = 0; i < accounts.Count; i++)
+        {
+            var percent = _spinBoxes[i].Value;
+            if (percent != (int) Math.Round(bank.RevenueDistribution[accounts[i]] * 100))
+            {
+                differs = true;
+                break;
+            }
+        }
+
+        SaveButton.Disabled = !differs || incorrectSum;
+
+        var diff = sum - 100;
+        SaveAlertLabel.Visible = incorrectSum;
+        SaveAlertLabel.SetMarkup(Loc.GetString("cargo-funding-alloc-console-label-save-fail",
+            ("pos", Math.Sign(diff)),
+            ("val", Math.Abs(diff))));
+    }
+
+    public void Update(FundingAllocationConsoleBuiState state)
+    {
+        _station = _entityManager.GetEntity(state.Station);
+        BuildEntries();
+        UpdateButtonDisabled();
+    }
+
+    protected override void FrameUpdate(FrameEventArgs args)
+    {
+        base.FrameUpdate(args);
+
+        if (!_bankQuery.TryComp(_station, out var bank))
+            return;
+
+        foreach (var (account, label) in _balanceLabels)
+        {
+            label.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", bank.Accounts[account]));
+        }
+    }
+}
index cdfd0a3874f49b8fd679766ce4696806636240e4..d9827ee3bfc2d9958e0b3306cf645f74fd59f4c2 100644 (file)
@@ -1,6 +1,4 @@
 namespace Content.Server.Cargo.Components;
-using Content.Shared.Actions;
-using Robust.Shared.Serialization.TypeSerializers.Implementations;
 
 /// <summary>
 /// Any entities intersecting when a shuttle is recalled will be sold.
index 6092ea0c3edf50abc97aa397ce2c62eb243b809a..da82c7e54606f6c364448ed3eb46c1e564b20e2f 100644 (file)
@@ -6,8 +6,4 @@ namespace Content.Server.Cargo.Components;
 
 [RegisterComponent]
 [Access(typeof(CargoSystem))]
-public sealed partial class CargoPalletConsoleComponent : Component
-{
-    [ViewVariables(VVAccess.ReadWrite), DataField("cashType", customTypeSerializer:typeof(PrototypeIdSerializer<StackPrototype>))]
-    public string CashType = "Credit";
-}
+public sealed partial class CargoPalletConsoleComponent : Component;
diff --git a/Content.Server/Cargo/Components/StationBankAccountComponent.cs b/Content.Server/Cargo/Components/StationBankAccountComponent.cs
deleted file mode 100644 (file)
index fe9be19..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-using Content.Shared.Cargo;
-
-namespace Content.Server.Cargo.Components;
-
-/// <summary>
-/// Added to the abstract representation of a station to track its money.
-/// </summary>
-[RegisterComponent, Access(typeof(SharedCargoSystem))]
-public sealed partial class StationBankAccountComponent : Component
-{
-    [ViewVariables(VVAccess.ReadWrite), DataField("balance")]
-    public int Balance = 2000;
-
-    /// <summary>
-    /// How much the bank balance goes up per second, every Delay period. Rounded down when multiplied.
-    /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("increasePerSecond")]
-    public int IncreasePerSecond = 1;
-}
index 2e3b2c211573937589d8e4d1fd8b2ae2461de92f..36db3b83f2dbf0cdac518b6bbe9f506ed48a92db 100644 (file)
@@ -1,9 +1,9 @@
+using System.Linq;
 using Content.Server.Station.Components;
 using Content.Shared.Cargo;
 using Content.Shared.Cargo.Components;
 using Content.Shared.Cargo.Prototypes;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 
 namespace Content.Server.Cargo.Components;
 
@@ -16,15 +16,19 @@ public sealed partial class StationCargoOrderDatabaseComponent : Component
     /// <summary>
     /// Maximum amount of orders a station is allowed, approved or not.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("capacity")]
+    [DataField]
     public int Capacity = 20;
 
-    [ViewVariables(VVAccess.ReadWrite), DataField("orders")]
-    public List<CargoOrderData> Orders = new();
+    [ViewVariables]
+    public IEnumerable<CargoOrderData> AllOrders => Orders.SelectMany(p => p.Value);
+
+    [DataField]
+    public Dictionary<ProtoId<CargoAccountPrototype>, List<CargoOrderData>> Orders = new();
 
     /// <summary>
     /// Used to determine unique order IDs
     /// </summary>
+    [ViewVariables]
     public int NumOrdersCreated;
 
     // TODO: Can probably dump this
index c938c6fa50be4560720bf2bd9f90d4f2d9e4a1a3..9a6acbc6ef0bf5b8d4762bbc80d554ae6f371733 100644 (file)
@@ -27,7 +27,6 @@ public sealed partial class CargoSystem
     [Dependency] private readonly ContainerSystem _container = default!;
     [Dependency] private readonly NameIdentifierSystem _nameIdentifier = default!;
     [Dependency] private readonly EntityWhitelistSystem _whitelistSys = default!;
-    [Dependency] private readonly IGameTiming _gameTiming = default!;
 
     [ValidatePrototypeId<NameIdentifierGroupPrototype>]
     private const string BountyNameIdentifierGroup = "Bounty";
@@ -472,7 +471,7 @@ public sealed partial class CargoSystem
                     skipped
                         ? CargoBountyHistoryData.BountyResult.Skipped
                         : CargoBountyHistoryData.BountyResult.Completed,
-                    _gameTiming.CurTime,
+                    _timing.CurTime,
                     actorName));
                 ent.Comp.Bounties.RemoveAt(i);
                 return true;
diff --git a/Content.Server/Cargo/Systems/CargoSystem.Funds.cs b/Content.Server/Cargo/Systems/CargoSystem.Funds.cs
new file mode 100644 (file)
index 0000000..f167697
--- /dev/null
@@ -0,0 +1,144 @@
+using System.Linq;
+using Content.Shared.Cargo.Components;
+using Content.Shared.Database;
+using Content.Shared.Emag.Systems;
+using Content.Shared.IdentityManagement;
+using Content.Shared.UserInterface;
+
+namespace Content.Server.Cargo.Systems;
+
+public sealed partial class CargoSystem
+{
+    public void InitializeFunds()
+    {
+        SubscribeLocalEvent<CargoOrderConsoleComponent, CargoConsoleWithdrawFundsMessage>(OnWithdrawFunds);
+        SubscribeLocalEvent<CargoOrderConsoleComponent, CargoConsoleToggleLimitMessage>(OnToggleLimit);
+        SubscribeLocalEvent<FundingAllocationConsoleComponent, SetFundingAllocationBuiMessage>(OnSetFundingAllocation);
+        SubscribeLocalEvent<FundingAllocationConsoleComponent, BeforeActivatableUIOpenEvent>(OnFundAllocationBuiOpen);
+    }
+
+    private void OnWithdrawFunds(Entity<CargoOrderConsoleComponent> ent, ref CargoConsoleWithdrawFundsMessage args)
+    {
+        if (_station.GetOwningStation(ent) is not { } station ||
+            !TryComp<StationBankAccountComponent>(station, out var bank))
+            return;
+
+        if (args.Account == ent.Comp.Account ||
+            args.Amount <= 0 ||
+            args.Amount > GetBalanceFromAccount((station, bank), ent.Comp.Account) * ent.Comp.TransferLimit)
+            return;
+
+        if (_timing.CurTime < ent.Comp.NextAccountActionTime)
+            return;
+
+        if (!_accessReaderSystem.IsAllowed(args.Actor, ent))
+        {
+            ConsolePopup(args.Actor, Loc.GetString("cargo-console-order-not-allowed"));
+            PlayDenySound(ent, ent.Comp);
+            return;
+        }
+
+        ent.Comp.NextAccountActionTime = _timing.CurTime + ent.Comp.AccountActionDelay;
+        Dirty(ent);
+        UpdateBankAccount((station, bank), -args.Amount, CreateAccountDistribution(ent.Comp.Account, bank));
+        _audio.PlayPvs(ApproveSound, ent);
+
+        var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(ent, args.Actor);
+        RaiseLocalEvent(tryGetIdentityShortInfoEvent);
+
+        var ourAccount = _protoMan.Index(ent.Comp.Account);
+        if (args.Account == null)
+        {
+            var stackPrototype = _protoMan.Index(ent.Comp.CashType);
+            _stack.Spawn(args.Amount, stackPrototype, Transform(ent).Coordinates);
+
+            if (!_emag.CheckFlag(ent, EmagType.Interaction))
+            {
+                var msg = Loc.GetString("cargo-console-fund-withdraw-broadcast",
+                    ("name", tryGetIdentityShortInfoEvent.Title ?? Loc.GetString("cargo-console-fund-transfer-user-unknown")),
+                    ("amount", args.Amount),
+                    ("name1", Loc.GetString(ourAccount.Name)),
+                    ("code1", Loc.GetString(ourAccount.Code)));
+                _radio.SendRadioMessage(ent, msg, ourAccount.RadioChannel, ent, escapeMarkup: false);
+            }
+        }
+        else
+        {
+            var otherAccount = _protoMan.Index(args.Account.Value);
+            UpdateBankAccount((station, bank), args.Amount, CreateAccountDistribution(args.Account.Value, bank));
+
+            if (!_emag.CheckFlag(ent, EmagType.Interaction))
+            {
+                var msg = Loc.GetString("cargo-console-fund-transfer-broadcast",
+                    ("name", tryGetIdentityShortInfoEvent.Title ?? Loc.GetString("cargo-console-fund-transfer-user-unknown")),
+                    ("amount", args.Amount),
+                    ("name1", Loc.GetString(ourAccount.Name)),
+                    ("code1", Loc.GetString(ourAccount.Code)),
+                    ("name2", Loc.GetString(otherAccount.Name)),
+                    ("code2", Loc.GetString(otherAccount.Code)));
+                _radio.SendRadioMessage(ent, msg, ourAccount.RadioChannel, ent, escapeMarkup: false);
+                _radio.SendRadioMessage(ent, msg, otherAccount.RadioChannel, ent, escapeMarkup: false);
+            }
+        }
+    }
+
+    private void OnToggleLimit(Entity<CargoOrderConsoleComponent> ent, ref CargoConsoleToggleLimitMessage args)
+    {
+        if (!_accessReaderSystem.FindAccessTags(args.Actor).Intersect(ent.Comp.RemoveLimitAccess).Any())
+        {
+            ConsolePopup(args.Actor, Loc.GetString("cargo-console-order-not-allowed"));
+            PlayDenySound(ent, ent.Comp);
+            return;
+        }
+
+        _audio.PlayPvs(ent.Comp.ToggleLimitSound, ent);
+        ent.Comp.TransferUnbounded = !ent.Comp.TransferUnbounded;
+        Dirty(ent);
+    }
+
+
+    private void OnSetFundingAllocation(Entity<FundingAllocationConsoleComponent> ent, ref SetFundingAllocationBuiMessage args)
+    {
+        if (_station.GetOwningStation(ent) is not { } station ||
+            !TryComp<StationBankAccountComponent>(station, out var bank))
+            return;
+
+        if (args.Percents.Count != bank.RevenueDistribution.Count)
+            return;
+
+        var differs = false;
+        foreach (var (account, percent) in args.Percents)
+        {
+            if (percent != (int) Math.Round(bank.RevenueDistribution[account] * 100))
+            {
+                differs = true;
+                break;
+            }
+        }
+
+        if (!differs)
+            return;
+
+        if (args.Percents.Values.Sum() != 100)
+            return;
+
+        bank.RevenueDistribution.Clear();
+        foreach (var (account, percent )in args.Percents)
+        {
+            bank.RevenueDistribution.Add(account, percent / 100.0);
+        }
+        Dirty(station, bank);
+
+        _audio.PlayPvs(ent.Comp.SetDistributionSound, ent);
+        _adminLogger.Add(
+            LogType.Action,
+            LogImpact.Medium,
+            $"{ToPrettyString(args.Actor):player} set station {ToPrettyString(station)} fund distribution: {string.Join(',', bank.RevenueDistribution.Select(p => $"{p.Key}: {p.Value}").ToList())}");
+    }
+
+    private void OnFundAllocationBuiOpen(Entity<FundingAllocationConsoleComponent> ent, ref BeforeActivatableUIOpenEvent args)
+    {
+        if (_station.GetOwningStation(ent) is { } station)
+            _uiSystem.SetUiState(ent.Owner, FundingAllocationConsoleUiKey.Key, new FundingAllocationConsoleBuiState(GetNetEntity(station)));
+    }
+}
index ee6526c3210673608a2a31f68464ad501b4fcd5b..39b3f144ff55a6c7622176ed6345d90730fa34f3 100644 (file)
@@ -12,6 +12,7 @@ using Content.Shared.IdentityManagement;
 using Content.Shared.Interaction;
 using Content.Shared.Labels.Components;
 using Content.Shared.Paper;
+using JetBrains.Annotations;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
@@ -23,16 +24,6 @@ namespace Content.Server.Cargo.Systems
         [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
         [Dependency] private readonly EmagSystem _emag = default!;
 
-        /// <summary>
-        /// How much time to wait (in seconds) before increasing bank accounts balance.
-        /// </summary>
-        private const int Delay = 10;
-
-        /// <summary>
-        /// Keeps track of how much time has elapsed since last balance increase.
-        /// </summary>
-        private float _timer;
-
         private void InitializeConsole()
         {
             SubscribeLocalEvent<CargoOrderConsoleComponent, CargoConsoleAddOrderMessage>(OnAddOrderMessage);
@@ -41,9 +32,7 @@ namespace Content.Server.Cargo.Systems
             SubscribeLocalEvent<CargoOrderConsoleComponent, BoundUIOpenedEvent>(OnOrderUIOpened);
             SubscribeLocalEvent<CargoOrderConsoleComponent, ComponentInit>(OnInit);
             SubscribeLocalEvent<CargoOrderConsoleComponent, InteractUsingEvent>(OnInteractUsing);
-            SubscribeLocalEvent<CargoOrderConsoleComponent, BankBalanceUpdatedEvent>(OnOrderBalanceUpdated);
             SubscribeLocalEvent<CargoOrderConsoleComponent, GotEmaggedEvent>(OnEmagged);
-            Reset();
         }
 
         private void OnInteractUsing(EntityUid uid, CargoOrderConsoleComponent component, ref InteractUsingEvent args)
@@ -61,8 +50,8 @@ namespace Content.Server.Cargo.Systems
             if (!TryComp(stationUid, out StationBankAccountComponent? bank))
                 return;
 
-            _audio.PlayPvs(component.ConfirmSound, uid);
-            UpdateBankAccount((stationUid.Value, bank), (int) price);
+            _audio.PlayPvs(ApproveSound, uid);
+            UpdateBankAccount((stationUid.Value, bank), (int) price, CreateAccountDistribution(component.Account, bank));
             QueueDel(args.Used);
             args.Handled = true;
         }
@@ -73,11 +62,6 @@ namespace Content.Server.Cargo.Systems
             UpdateOrderState(uid, station);
         }
 
-        private void Reset()
-        {
-            _timer = 0;
-        }
-
         private void OnEmagged(Entity<CargoOrderConsoleComponent> ent, ref GotEmaggedEvent args)
         {
             if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
@@ -89,31 +73,17 @@ namespace Content.Server.Cargo.Systems
             args.Handled = true;
         }
 
-        private void UpdateConsole(float frameTime)
+        private void UpdateConsole()
         {
-            _timer += frameTime;
-
-            // TODO: Doesn't work with serialization and shouldn't just be updating every delay
-            // client can just interp this just fine on its own.
-            while (_timer > Delay)
+            var stationQuery = EntityQueryEnumerator<StationBankAccountComponent>();
+            while (stationQuery.MoveNext(out var uid, out var bank))
             {
-                _timer -= Delay;
-
-                var stationQuery = EntityQueryEnumerator<StationBankAccountComponent>();
-                while (stationQuery.MoveNext(out var uid, out var bank))
-                {
-                    var balanceToAdd = bank.IncreasePerSecond * Delay;
-                    UpdateBankAccount((uid, bank), balanceToAdd);
-                }
-
-                var query = EntityQueryEnumerator<CargoOrderConsoleComponent>();
-                while (query.MoveNext(out var uid, out var _))
-                {
-                    if (!_uiSystem.IsUiOpen(uid, CargoConsoleUiKey.Orders)) continue;
+                if (_timing.CurTime < bank.NextIncomeTime)
+                    continue;
+                bank.NextIncomeTime += bank.IncomeDelay;
 
-                    var station = _station.GetOwningStation(uid);
-                    UpdateOrderState(uid, station);
-                }
+                var balanceToAdd = (int) Math.Round(bank.IncreasePerSecond * bank.IncomeDelay.TotalSeconds);
+                UpdateBankAccount((uid, bank), balanceToAdd, bank.RevenueDistribution);
             }
         }
 
@@ -144,7 +114,7 @@ namespace Content.Server.Cargo.Systems
             }
 
             // Find our order again. It might have been dispatched or approved already
-            var order = orderDatabase.Orders.Find(order => args.OrderId == order.OrderId && !order.Approved);
+            var order = orderDatabase.Orders[component.Account].Find(order => args.OrderId == order.OrderId && !order.Approved);
             if (order == null)
             {
                 return;
@@ -158,7 +128,7 @@ namespace Content.Server.Cargo.Systems
                 return;
             }
 
-            var amount = GetOutstandingOrderCount(orderDatabase);
+            var amount = GetOutstandingOrderCount(orderDatabase, component.Account);
             var capacity = orderDatabase.Capacity;
 
             // Too many orders, avoid them getting spammed in the UI.
@@ -180,9 +150,10 @@ namespace Content.Server.Cargo.Systems
             }
 
             var cost = order.Price * order.OrderQuantity;
+            var accountBalance = GetBalanceFromAccount((station.Value, bank), component.Account);
 
             // Not enough balance
-            if (cost > bank.Balance)
+            if (cost > accountBalance)
             {
                 ConsolePopup(args.Actor, Loc.GetString("cargo-console-insufficient-funds", ("cost", cost)));
                 PlayDenySound(uid, component);
@@ -195,7 +166,7 @@ namespace Content.Server.Cargo.Systems
 
             if (!ev.Handled)
             {
-                ev.FulfillmentEntity = TryFulfillOrder((station.Value, stationData), order, orderDatabase);
+                ev.FulfillmentEntity = TryFulfillOrder((station.Value, stationData), component.Account, order, orderDatabase);
 
                 if (ev.FulfillmentEntity == null)
                 {
@@ -206,7 +177,7 @@ namespace Content.Server.Cargo.Systems
             }
 
             order.Approved = true;
-            _audio.PlayPvs(component.ConfirmSound, uid);
+            _audio.PlayPvs(ApproveSound, uid);
 
             if (!_emag.CheckFlag(uid, EmagType.Interaction))
             {
@@ -220,20 +191,23 @@ namespace Content.Server.Cargo.Systems
                     ("approver", order.Approver ?? string.Empty),
                     ("cost", cost));
                 _radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false);
+                if (CargoOrderConsoleComponent.BaseAnnouncementChannel != component.AnnouncementChannel)
+                    _radio.SendRadioMessage(uid, message, CargoOrderConsoleComponent.BaseAnnouncementChannel, uid, escapeMarkup: false);
             }
 
             ConsolePopup(args.Actor, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(ev.FulfillmentEntity.Value).EntityName)));
 
             // Log order approval
-            _adminLogger.Add(LogType.Action, LogImpact.Low,
-                $"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] with balance at {bank.Balance}");
+            _adminLogger.Add(LogType.Action,
+                LogImpact.Low,
+                $"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] on account {component.Account} with balance at {accountBalance}");
 
-            orderDatabase.Orders.Remove(order);
-            UpdateBankAccount((station.Value, bank), -cost);
+            orderDatabase.Orders[component.Account].Remove(order);
+            UpdateBankAccount((station.Value, bank), -cost, CreateAccountDistribution(component.Account, bank));
             UpdateOrders(station.Value);
         }
 
-        private EntityUid? TryFulfillOrder(Entity<StationDataComponent> stationData, CargoOrderData order, StationCargoOrderDatabaseComponent orderDatabase)
+        private EntityUid? TryFulfillOrder(Entity<StationDataComponent> stationData, ProtoId<CargoAccountPrototype> account, CargoOrderData order, StationCargoOrderDatabaseComponent orderDatabase)
         {
             // No slots at the trade station
             _listEnts.Clear();
@@ -253,7 +227,7 @@ namespace Content.Server.Cargo.Systems
                     {
                         var coordinates = new EntityCoordinates(trade, pad.Transform.LocalPosition);
 
-                        if (FulfillOrder(order, coordinates, orderDatabase.PrinterOutput))
+                        if (FulfillOrder(order, account, coordinates, orderDatabase.PrinterOutput))
                         {
                             tradeDestination = trade;
                             order.NumDispatched++;
@@ -288,7 +262,7 @@ namespace Content.Server.Cargo.Systems
             if (!TryGetOrderDatabase(station, out var orderDatabase))
                 return;
 
-            RemoveOrder(station.Value, args.OrderId, orderDatabase);
+            RemoveOrder(station.Value, component.Account, args.OrderId, orderDatabase);
         }
 
         private void OnAddOrderMessage(EntityUid uid, CargoOrderConsoleComponent component, CargoConsoleAddOrderMessage args)
@@ -315,14 +289,15 @@ namespace Content.Server.Cargo.Systems
 
             var data = GetOrderData(args, product, GenerateOrderId(orderDatabase));
 
-            if (!TryAddOrder(stationUid.Value, data, orderDatabase))
+            if (!TryAddOrder(stationUid.Value, component.Account, data, orderDatabase))
             {
                 PlayDenySound(uid, component);
                 return;
             }
 
             // Log order addition
-            _adminLogger.Add(LogType.Action, LogImpact.Low,
+            _adminLogger.Add(LogType.Action,
+                LogImpact.Low,
                 $"{ToPrettyString(player):user} added order [orderId:{data.OrderId}, quantity:{data.OrderQuantity}, product:{data.ProductId}, requester:{data.Requester}, reason:{data.Reason}]");
 
         }
@@ -335,29 +310,24 @@ namespace Content.Server.Cargo.Systems
 
         #endregion
 
-
-        private void OnOrderBalanceUpdated(Entity<CargoOrderConsoleComponent> ent, ref BankBalanceUpdatedEvent args)
+        private void UpdateOrderState(EntityUid consoleUid, EntityUid? station)
         {
-            if (!_uiSystem.IsUiOpen(ent.Owner, CargoConsoleUiKey.Orders))
+            if (!TryComp<CargoOrderConsoleComponent>(consoleUid, out var console))
                 return;
 
-            UpdateOrderState(ent, args.Station);
-        }
-
-        private void UpdateOrderState(EntityUid consoleUid, EntityUid? station)
-        {
-            if (station == null ||
-                !TryComp<StationCargoOrderDatabaseComponent>(station, out var orderDatabase) ||
-                !TryComp<StationBankAccountComponent>(station, out var bankAccount)) return;
+            if (!TryComp<StationCargoOrderDatabaseComponent>(station, out var orderDatabase))
+                return;
 
             if (_uiSystem.HasUi(consoleUid, CargoConsoleUiKey.Orders))
             {
-                _uiSystem.SetUiState(consoleUid, CargoConsoleUiKey.Orders, new CargoConsoleInterfaceState(
+                _uiSystem.SetUiState(consoleUid,
+                    CargoConsoleUiKey.Orders,
+                    new CargoConsoleInterfaceState(
                     MetaData(station.Value).EntityName,
-                    GetOutstandingOrderCount(orderDatabase),
+                    GetOutstandingOrderCount(orderDatabase, console.Account),
                     orderDatabase.Capacity,
-                    bankAccount.Balance,
-                    orderDatabase.Orders
+                    GetNetEntity(station.Value),
+                    orderDatabase.Orders[console.Account]
                 ));
             }
         }
@@ -377,11 +347,11 @@ namespace Content.Server.Cargo.Systems
             return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Name, cargoProduct.Cost, args.Amount, args.Requester, args.Reason);
         }
 
-        public static int GetOutstandingOrderCount(StationCargoOrderDatabaseComponent component)
+        public static int GetOutstandingOrderCount(StationCargoOrderDatabaseComponent component, ProtoId<CargoAccountPrototype> account)
         {
             var amount = 0;
 
-            foreach (var order in component.Orders)
+            foreach (var order in component.Orders[account])
             {
                 if (!order.Approved)
                     continue;
@@ -430,6 +400,7 @@ namespace Content.Server.Cargo.Systems
             string description,
             string dest,
             StationCargoOrderDatabaseComponent component,
+            ProtoId<CargoAccountPrototype> account,
             Entity<StationDataComponent> stationData
         )
         {
@@ -443,16 +414,17 @@ namespace Content.Server.Cargo.Systems
             order.Approved = true;
 
             // Log order addition
-            _adminLogger.Add(LogType.Action, LogImpact.Low,
+            _adminLogger.Add(LogType.Action,
+                LogImpact.Low,
                 $"AddAndApproveOrder {description} added order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}]");
 
             // Add it to the list
-            return TryAddOrder(dbUid, order, component) && TryFulfillOrder(stationData, order, component).HasValue;
+            return TryAddOrder(dbUid, account, order, component) && TryFulfillOrder(stationData, account, order, component).HasValue;
         }
 
-        private bool TryAddOrder(EntityUid dbUid, CargoOrderData data, StationCargoOrderDatabaseComponent component)
+        private bool TryAddOrder(EntityUid dbUid, ProtoId<CargoAccountPrototype> account, CargoOrderData data, StationCargoOrderDatabaseComponent component)
         {
-            component.Orders.Add(data);
+            component.Orders[account].Add(data);
             UpdateOrders(dbUid);
             return true;
         }
@@ -464,12 +436,12 @@ namespace Content.Server.Cargo.Systems
             return ++orderDB.NumOrdersCreated;
         }
 
-        public void RemoveOrder(EntityUid dbUid, int index, StationCargoOrderDatabaseComponent orderDB)
+        public void RemoveOrder(EntityUid dbUid, ProtoId<CargoAccountPrototype> account, int index, StationCargoOrderDatabaseComponent orderDB)
         {
-            var sequenceIdx = orderDB.Orders.FindIndex(order => order.OrderId == index);
+            var sequenceIdx = orderDB.Orders[account].FindIndex(order => order.OrderId == index);
             if (sequenceIdx != -1)
             {
-                orderDB.Orders.RemoveAt(sequenceIdx);
+                orderDB.Orders[account].RemoveAt(sequenceIdx);
             }
             UpdateOrders(dbUid);
         }
@@ -482,22 +454,22 @@ namespace Content.Server.Cargo.Systems
             component.Orders.Clear();
         }
 
-        private static bool PopFrontOrder(StationCargoOrderDatabaseComponent orderDB, [NotNullWhen(true)] out CargoOrderData? orderOut)
+        private static bool PopFrontOrder(StationCargoOrderDatabaseComponent orderDB, ProtoId<CargoAccountPrototype> account, [NotNullWhen(true)] out CargoOrderData? orderOut)
         {
-            var orderIdx = orderDB.Orders.FindIndex(order => order.Approved);
+            var orderIdx = orderDB.Orders[account].FindIndex(order => order.Approved);
             if (orderIdx == -1)
             {
                 orderOut = null;
                 return false;
             }
 
-            orderOut = orderDB.Orders[orderIdx];
+            orderOut = orderDB.Orders[account][orderIdx];
             orderOut.NumDispatched++;
 
             if (orderOut.NumDispatched >= orderOut.OrderQuantity)
             {
                 // Order is complete. Remove from the queue.
-                orderDB.Orders.RemoveAt(orderIdx);
+                orderDB.Orders[account].RemoveAt(orderIdx);
             }
             return true;
         }
@@ -505,18 +477,19 @@ namespace Content.Server.Cargo.Systems
         /// <summary>
         /// Tries to fulfill the next outstanding order.
         /// </summary>
-        private bool FulfillNextOrder(StationCargoOrderDatabaseComponent orderDB, EntityCoordinates spawn, string? paperProto)
+        [PublicAPI]
+        private bool FulfillNextOrder(StationCargoOrderDatabaseComponent orderDB, ProtoId<CargoAccountPrototype> account, EntityCoordinates spawn, string? paperProto)
         {
-            if (!PopFrontOrder(orderDB, out var order))
+            if (!PopFrontOrder(orderDB, account, out var order))
                 return false;
 
-            return FulfillOrder(order, spawn, paperProto);
+            return FulfillOrder(order, account, spawn, paperProto);
         }
 
         /// <summary>
         /// Fulfills the specified cargo order and spawns paper attached to it.
         /// </summary>
-        private bool FulfillOrder(CargoOrderData order, EntityCoordinates spawn, string? paperProto)
+        private bool FulfillOrder(CargoOrderData order, ProtoId<CargoAccountPrototype> account, EntityCoordinates spawn, string? paperProto)
         {
             // Create the item itself
             var item = Spawn(order.ProductId, spawn);
@@ -532,14 +505,18 @@ namespace Content.Server.Cargo.Systems
                 var val = Loc.GetString("cargo-console-paper-print-name", ("orderNumber", order.OrderId));
                 _metaSystem.SetEntityName(printed, val);
 
-                _paperSystem.SetContent((printed, paper), Loc.GetString(
+                var accountProto = _protoMan.Index(account);
+                _paperSystem.SetContent((printed, paper),
+                    Loc.GetString(
                         "cargo-console-paper-print-text",
                         ("orderNumber", order.OrderId),
                         ("itemName", MetaData(item).EntityName),
                         ("orderQuantity", order.OrderQuantity),
                         ("requester", order.Requester),
-                        ("reason", order.Reason),
-                        ("approver", order.Approver ?? string.Empty)));
+                        ("reason", string.IsNullOrWhiteSpace(order.Reason) ? Loc.GetString("cargo-console-paper-reason-default") : order.Reason),
+                        ("account", Loc.GetString(accountProto.Name)),
+                        ("accountcode", Loc.GetString(accountProto.Code)),
+                        ("approver", string.IsNullOrWhiteSpace(order.Approver) ? Loc.GetString("cargo-console-paper-approver-default") : order.Approver)));
 
                 // attempt to attach the label to the item
                 if (TryComp<PaperLabelComponent>(item, out var label))
index 9365b22ad9c1365687759f753f86b361aef33f46..84319ab79332c6fe2d75c34919a0537f8cf95943 100644 (file)
@@ -1,13 +1,13 @@
+using System.Linq;
 using Content.Server.Cargo.Components;
-using Content.Shared.Stacks;
 using Content.Shared.Cargo;
 using Content.Shared.Cargo.BUI;
 using Content.Shared.Cargo.Components;
 using Content.Shared.Cargo.Events;
-using Content.Shared.GameTicking;
-using Robust.Shared.Map;
-using Robust.Shared.Random;
+using Content.Shared.Cargo.Prototypes;
+using JetBrains.Annotations;
 using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
 
 namespace Content.Server.Cargo.Systems;
 
@@ -28,12 +28,11 @@ public sealed partial class CargoSystem
         SubscribeLocalEvent<CargoPalletConsoleComponent, CargoPalletSellMessage>(OnPalletSale);
         SubscribeLocalEvent<CargoPalletConsoleComponent, CargoPalletAppraiseMessage>(OnPalletAppraise);
         SubscribeLocalEvent<CargoPalletConsoleComponent, BoundUIOpenedEvent>(OnPalletUIOpen);
-
-        SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
     }
 
     #region Console
 
+    [PublicAPI]
     private void UpdateCargoShuttleConsoles(EntityUid shuttleUid, CargoShuttleComponent _)
     {
         // Update pilot consoles that are already open.
@@ -54,15 +53,18 @@ public sealed partial class CargoSystem
 
     private void UpdatePalletConsoleInterface(EntityUid uid)
     {
-        if (Transform(uid).GridUid is not EntityUid gridUid)
+        if (Transform(uid).GridUid is not { } gridUid)
         {
-            _uiSystem.SetUiState(uid, CargoPalletConsoleUiKey.Sale,
-            new CargoPalletConsoleInterfaceState(0, 0, false));
+            _uiSystem.SetUiState(uid,
+                CargoPalletConsoleUiKey.Sale,
+                new CargoPalletConsoleInterfaceState(0, 0, false));
             return;
         }
-        GetPalletGoods(gridUid, out var toSell, out var amount);
-        _uiSystem.SetUiState(uid, CargoPalletConsoleUiKey.Sale,
-            new CargoPalletConsoleInterfaceState((int) amount, toSell.Count, true));
+        GetPalletGoods(gridUid, out var toSell, out var goods);
+        var totalAmount = goods.Sum(t => t.Item3);
+        _uiSystem.SetUiState(uid,
+            CargoPalletConsoleUiKey.Sale,
+            new CargoPalletConsoleInterfaceState((int) totalAmount, toSell.Count, true));
     }
 
     private void OnPalletUIOpen(EntityUid uid, CargoPalletConsoleComponent component, BoundUIOpenedEvent args)
@@ -98,11 +100,15 @@ public sealed partial class CargoSystem
         var shuttleName = orderDatabase?.Shuttle != null ? MetaData(orderDatabase.Shuttle.Value).EntityName : string.Empty;
 
         if (_uiSystem.HasUi(uid, CargoConsoleUiKey.Shuttle))
-            _uiSystem.SetUiState(uid, CargoConsoleUiKey.Shuttle, new CargoShuttleConsoleBoundUserInterfaceState(
+        {
+            _uiSystem.SetUiState(uid,
+                CargoConsoleUiKey.Shuttle,
+                new CargoShuttleConsoleBoundUserInterfaceState(
                 station != null ? MetaData(station.Value).EntityName : Loc.GetString("cargo-shuttle-console-station-unknown"),
                 string.IsNullOrEmpty(shuttleName) ? Loc.GetString("cargo-shuttle-console-shuttle-not-found") : shuttleName,
                 orders
             ));
+        }
     }
 
     #endregion
@@ -132,9 +138,10 @@ public sealed partial class CargoSystem
             return orders;
 
         var spaceRemaining = GetCargoSpace(shuttleUid);
-        for (var i = 0; i < component.Orders.Count && spaceRemaining > 0; i++)
+        var allOrders = component.AllOrders.ToList();
+        for (var i = 0; i < allOrders.Count && spaceRemaining > 0; i++)
         {
-            var order = component.Orders[i];
+            var order = allOrders[i];
             if (order.Approved)
             {
                 var numToShip = order.OrderQuantity - order.NumDispatched;
@@ -142,8 +149,14 @@ public sealed partial class CargoSystem
                 {
                     // We won't be able to fit the whole order on, so make one
                     // which represents the space we do have left:
-                    var reducedOrder = new CargoOrderData(order.OrderId,
-                            order.ProductId, order.ProductName, order.Price, spaceRemaining, order.Requester, order.Reason);
+                    var reducedOrder = new CargoOrderData(
+                        order.OrderId,
+                        order.ProductId,
+                        order.ProductName,
+                        order.Price,
+                        spaceRemaining,
+                        order.Requester,
+                        order.Reason);
                     orders.Add(reducedOrder);
                 }
                 else
@@ -219,16 +232,13 @@ public sealed partial class CargoSystem
 
     #region Station
 
-    private bool SellPallets(EntityUid gridUid, out double amount)
+    private bool SellPallets(EntityUid gridUid, out HashSet<(EntityUid, OverrideSellComponent?, double)> goods)
     {
-        GetPalletGoods(gridUid, out var toSell, out amount);
-
-        Log.Debug($"Cargo sold {toSell.Count} entities for {amount}");
+        GetPalletGoods(gridUid, out var toSell, out goods);
 
         if (toSell.Count == 0)
             return false;
 
-
         var ev = new EntitySoldEvent(toSell);
         RaiseLocalEvent(ref ev);
 
@@ -240,9 +250,9 @@ public sealed partial class CargoSystem
         return true;
     }
 
-    private void GetPalletGoods(EntityUid gridUid, out HashSet<EntityUid> toSell, out double amount)
+    private void GetPalletGoods(EntityUid gridUid, out HashSet<EntityUid> toSell,  out HashSet<(EntityUid, OverrideSellComponent?, double)> goods)
     {
-        amount = 0;
+        goods = new HashSet<(EntityUid, OverrideSellComponent?, double)>();
         toSell = new HashSet<EntityUid>();
 
         foreach (var (palletUid, _, _) in GetCargoPallets(gridUid, BuySellType.Sell))
@@ -250,7 +260,9 @@ public sealed partial class CargoSystem
             // Containers should already get the sell price of their children so can skip those.
             _setEnts.Clear();
 
-            _lookup.GetEntitiesIntersecting(palletUid, _setEnts,
+            _lookup.GetEntitiesIntersecting(
+                palletUid,
+                _setEnts,
                 LookupFlags.Dynamic | LookupFlags.Sundries);
 
             foreach (var ent in _setEnts)
@@ -273,7 +285,7 @@ public sealed partial class CargoSystem
                 if (price == 0)
                     continue;
                 toSell.Add(ent);
-                amount += price;
+                goods.Add((ent, CompOrNull<OverrideSellComponent>(ent), price));
             }
         }
     }
@@ -305,28 +317,49 @@ public sealed partial class CargoSystem
     {
         var xform = Transform(uid);
 
-        if (xform.GridUid is not EntityUid gridUid)
+        if (_station.GetOwningStation(uid) is not { } station ||
+            !TryComp<StationBankAccountComponent>(station, out var bankAccount))
         {
-            _uiSystem.SetUiState(uid, CargoPalletConsoleUiKey.Sale,
-            new CargoPalletConsoleInterfaceState(0, 0, false));
             return;
         }
 
-        if (!SellPallets(gridUid, out var price))
+        if (xform.GridUid is not { } gridUid)
+        {
+            _uiSystem.SetUiState(uid,
+                CargoPalletConsoleUiKey.Sale,
+                new CargoPalletConsoleInterfaceState(0, 0, false));
+            return;
+        }
+
+        if (!SellPallets(gridUid, out var goods))
             return;
 
-        var stackPrototype = _protoMan.Index<StackPrototype>(component.CashType);
-        _stack.Spawn((int) price, stackPrototype, xform.Coordinates);
+        var baseDistribution = CreateAccountDistribution(bankAccount.PrimaryAccount, bankAccount, bankAccount.PrimaryCut);
+        foreach (var (_, sellComponent, value) in goods)
+        {
+            Dictionary<ProtoId<CargoAccountPrototype>, double> distribution;
+            if (sellComponent != null)
+            {
+                distribution = new Dictionary<ProtoId<CargoAccountPrototype>, double>()
+                {
+                    { sellComponent.OverrideAccount, bankAccount.PrimaryCut },
+                    { bankAccount.PrimaryAccount, 1.0 - bankAccount.PrimaryCut },
+                };
+            }
+            else
+            {
+                distribution = baseDistribution;
+            }
+
+            UpdateBankAccount((station, bankAccount), (int) Math.Round(value), distribution, false);
+        }
+
+        Dirty(station, bankAccount);
         _audio.PlayPvs(ApproveSound, uid);
         UpdatePalletConsoleInterface(uid);
     }
 
     #endregion
-
-    private void OnRoundRestart(RoundRestartCleanupEvent ev)
-    {
-        Reset();
-    }
 }
 
 /// <summary>
index 41fbf12a0c6033cc7786ce0898b37227fe2e1d95..25fb514db6dbdb700271a13b43095e6149460453 100644 (file)
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Content.Server.Cargo.Components;
 using Content.Server.Power.Components;
@@ -40,9 +41,8 @@ public sealed partial class CargoSystem
                 continue;
 
             // todo cannot be fucking asked to figure out device linking rn but this shouldn't just default to the first port.
-            if (!TryComp<DeviceLinkSinkComponent>(uid, out var sinkComponent) ||
-                sinkComponent.LinkedSources.FirstOrNull() is not { } console ||
-                console != args.OrderConsole.Owner)
+            if (!TryGetLinkedConsole((uid, tele), out var console) ||
+                console.Value.Owner != args.OrderConsole.Owner)
                 continue;
 
             for (var i = 0; i < args.Order.OrderQuantity; i++)
@@ -56,10 +56,26 @@ public sealed partial class CargoSystem
         }
     }
 
+    private bool TryGetLinkedConsole(Entity<CargoTelepadComponent> ent,
+        [NotNullWhen(true)] out Entity<CargoOrderConsoleComponent>? console)
+    {
+        console = null;
+        if (!TryComp<DeviceLinkSinkComponent>(ent, out var sinkComponent) ||
+            sinkComponent.LinkedSources.FirstOrNull() is not { } linked)
+            return false;
+
+        if (!TryComp<CargoOrderConsoleComponent>(linked, out var consoleComp))
+            return false;
+
+        console = (linked, consoleComp);
+        return true;
+    }
+
+
     private void UpdateTelepad(float frameTime)
     {
-        var query = EntityQueryEnumerator<CargoTelepadComponent>();
-        while (query.MoveNext(out var uid, out var comp))
+        var query = EntityQueryEnumerator<CargoTelepadComponent, TransformComponent>();
+        while (query.MoveNext(out var uid, out var comp, out var xform))
         {
             // Don't EntityQuery for it as it's not required.
             TryComp<AppearanceComponent>(uid, out var appearance);
@@ -82,15 +98,14 @@ public sealed partial class CargoSystem
                 continue;
             }
 
-            if (comp.CurrentOrders.Count == 0)
+            if (comp.CurrentOrders.Count == 0 || !TryGetLinkedConsole((uid, comp), out var console))
             {
                 comp.Accumulator += comp.Delay;
                 continue;
             }
 
-            var xform = Transform(uid);
             var currentOrder = comp.CurrentOrders.First();
-            if (FulfillOrder(currentOrder, xform.Coordinates, comp.PrinterOutput))
+            if (FulfillOrder(currentOrder, console.Value.Comp.Account, xform.Coordinates, comp.PrinterOutput))
             {
                 _audio.PlayPvs(_audio.ResolveSound(comp.TeleportSound), uid, AudioParams.Default.WithVolume(-8f));
 
@@ -128,9 +143,12 @@ public sealed partial class CargoSystem
             !TryComp<StationDataComponent>(station, out var data))
             return;
 
+        if (!TryGetLinkedConsole(ent, out var console))
+            return;
+
         foreach (var order in ent.Comp.CurrentOrders)
         {
-            TryFulfillOrder((station, data), order, db);
+            TryFulfillOrder((station, data), console.Value.Comp.Account, order, db);
         }
     }
 
index 1b776b8bd097a0de378e7c5df9ad0b5943aa8bb3..450f292c9d5e4ddab9c87b012ee6a2bd46ba2bc3 100644 (file)
@@ -9,6 +9,7 @@ using Content.Shared.Administration.Logs;
 using Content.Server.Radio.EntitySystems;
 using Content.Shared.Cargo;
 using Content.Shared.Cargo.Components;
+using Content.Shared.Cargo.Prototypes;
 using Content.Shared.Containers.ItemSlots;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Paper;
@@ -65,36 +66,46 @@ public sealed partial class CargoSystem : SharedCargoSystem
         InitializeShuttle();
         InitializeTelepad();
         InitializeBounty();
+        InitializeFunds();
     }
 
     public override void Update(float frameTime)
     {
         base.Update(frameTime);
-        UpdateConsole(frameTime);
+        UpdateConsole();
         UpdateTelepad(frameTime);
         UpdateBounty();
     }
 
+    /// <summary>
+    /// Adds or removes funds from the <see cref="StationBankAccountComponent"/>.
+    /// </summary>
+    /// <param name="ent">The station.</param>
+    /// <param name="balanceAdded">The amount of funds to add or remove.</param>
+    /// <param name="accountDistribution">The distribution between individual <see cref="CargoAccountPrototype"/>.</param>
+    /// <param name="dirty">Whether to mark the bank accoujnt component as dirty.</param>
     [PublicAPI]
-    public void UpdateBankAccount(Entity<StationBankAccountComponent?> ent, int balanceAdded)
+    public void UpdateBankAccount(
+        Entity<StationBankAccountComponent?> ent,
+        int balanceAdded,
+        Dictionary<ProtoId<CargoAccountPrototype>, double> accountDistribution,
+        bool dirty = true)
     {
         if (!Resolve(ent, ref ent.Comp))
             return;
 
-        ent.Comp.Balance += balanceAdded;
+        foreach (var (account, percent) in accountDistribution)
+        {
+            var accountBalancedAdded = (int) Math.Round(percent * balanceAdded);
+            ent.Comp.Accounts[account] += accountBalancedAdded;
+        }
 
-        var ev = new BankBalanceUpdatedEvent(ent, ent.Comp.Balance);
+        var ev = new BankBalanceUpdatedEvent(ent, ent.Comp.Accounts);
+        RaiseLocalEvent(ent, ref ev, true);
 
-        var query = EntityQueryEnumerator<BankClientComponent, TransformComponent>();
-        while (query.MoveNext(out var client, out var comp, out var xform))
-        {
-            var station = _station.GetOwningStation(client, xform);
-            if (station != ent)
-                continue;
+        if (!dirty)
+            return;
 
-            comp.Balance = ent.Comp.Balance;
-            Dirty(client, comp);
-            RaiseLocalEvent(client, ref ev);
-        }
+        Dirty(ent);
     }
 }
index 8d2052733eb026d4c65232a2286511547901019a..91dff3b855becdf5bcc3989ca0096a96a6a8c7c5 100644 (file)
@@ -1,7 +1,7 @@
-using Content.Server.Cargo.Components;
 using Content.Server.Cargo.Systems;
 using Content.Server.Station.Systems;
 using Content.Server.StationRecords.Systems;
+using Content.Shared.Cargo.Components;
 using Content.Shared.Delivery;
 using Content.Shared.FingerprintReader;
 using Content.Shared.Labels.EntitySystems;
@@ -73,7 +73,10 @@ public sealed partial class DeliverySystem : SharedDeliverySystem
         if (!TryComp<StationBankAccountComponent>(ent.Comp.RecipientStation, out var account))
             return;
 
-        _cargo.UpdateBankAccount((ent.Comp.RecipientStation.Value, account), ent.Comp.SpesoReward);
+        _cargo.UpdateBankAccount(
+            (ent.Comp.RecipientStation.Value, account),
+            ent.Comp.SpesoReward,
+            _cargo.CreateAccountDistribution(account.PrimaryAccount, account, account.PrimaryCut));
     }
 
     public override void Update(float frameTime)
index bc1800ffd1eaf0508e21eee24002ba685ade0c7c..61153be401078a06d530cb58ceae596c98f76b58 100644 (file)
@@ -86,7 +86,7 @@ namespace Content.Server.Stack
         public EntityUid Spawn(int amount, StackPrototype prototype, EntityCoordinates spawnPosition)
         {
             // Set the output result parameter to the new stack entity...
-            var entity = Spawn(prototype.Spawn, spawnPosition);
+            var entity = SpawnAtPosition(prototype.Spawn, spawnPosition);
             var stack = Comp<StackComponent>(entity);
 
             // And finally, set the correct amount!
index a61adb78abe49d8e93fe5298d6f1778c0ac02025..d80e2d08d1c37ff9d622b73e9a6406c30d2f6ad5 100644 (file)
@@ -8,6 +8,7 @@ using Content.Shared.Station;
 using Content.Shared.Station.Components;
 using JetBrains.Annotations;
 using Robust.Server.GameObjects;
+using Robust.Server.GameStates;
 using Robust.Server.Player;
 using Robust.Shared.Collections;
 using Robust.Shared.Configuration;
@@ -34,6 +35,7 @@ public sealed class StationSystem : EntitySystem
     [Dependency] private readonly SharedTransformSystem _transform = default!;
     [Dependency] private readonly MetaDataSystem _metaData = default!;
     [Dependency] private readonly MapSystem _map = default!;
+    [Dependency] private readonly PvsOverrideSystem _pvsOverride = default!;
 
     private ISawmill _sawmill = default!;
 
@@ -97,7 +99,7 @@ public sealed class StationSystem : EntitySystem
         var metaData = MetaData(uid);
         RaiseLocalEvent(new StationInitializedEvent(uid));
         _sawmill.Info($"Set up station {metaData.EntityName} ({uid}).");
-
+        _pvsOverride.AddGlobalOverride(uid);
     }
 
     private void OnStationDeleted(EntityUid uid, StationDataComponent component, ComponentShutdown args)
index ab5d722a737b056393400abfab85672bc0bb053a..01d67f61cfa18cbaf1ee1b20f90a08930a2b2a21 100644 (file)
@@ -34,6 +34,12 @@ public sealed partial class CargoGiftsRuleComponent : Component
     [DataField, ViewVariables(VVAccess.ReadWrite)]
     public LocId Dest = "cargo-gift-default-dest";
 
+    /// <summary>
+    /// Account the gifts are deposited into
+    /// </summary>
+    [DataField]
+    public ProtoId<CargoAccountPrototype> Account = "Cargo";
+
     /// <summary>
     /// Cargo that you would like gifted to the station, with the quantity for each
     /// Use Ids from cargoProduct Prototypes
index ff2d1ca63117c3564ad01d6b958fe66967c3444f..f8a718a536ccbafa7ca1c45937870a55b1ed6424 100644 (file)
@@ -53,7 +53,7 @@ public sealed class CargoGiftsRule : StationEventSystem<CargoGiftsRuleComponent>
         }
 
         // Add some presents
-        var outstanding = CargoSystem.GetOutstandingOrderCount(cargoDb);
+        var outstanding = CargoSystem.GetOutstandingOrderCount(cargoDb, component.Account);
         while (outstanding < cargoDb.Capacity - component.OrderSpaceToLeave && component.Gifts.Count > 0)
         {
             // I wish there was a nice way to pop this
@@ -72,6 +72,7 @@ public sealed class CargoGiftsRule : StationEventSystem<CargoGiftsRuleComponent>
                     Loc.GetString(component.Description),
                     Loc.GetString(component.Dest),
                     cargoDb,
+                    component.Account,
                     (station.Value, stationData)
             ))
             {
index a1e61772cd605c0d9732bdc05b91376829084e87..7084477f246ddf3e261822931524bb22d2583f7d 100644 (file)
@@ -8,15 +8,15 @@ public sealed class CargoConsoleInterfaceState : BoundUserInterfaceState
     public string Name;
     public int Count;
     public int Capacity;
-    public int Balance;
+    public NetEntity Station;
     public List<CargoOrderData> Orders;
 
-    public CargoConsoleInterfaceState(string name, int count, int capacity, int balance, List<CargoOrderData> orders)
+    public CargoConsoleInterfaceState(string name, int count, int capacity, NetEntity station, List<CargoOrderData> orders)
     {
         Name = name;
         Count = count;
         Capacity = capacity;
-        Balance = balance;
+        Station = station;
         Orders = orders;
     }
-}
\ No newline at end of file
+}
diff --git a/Content.Shared/Cargo/Components/BankClientComponent.cs b/Content.Shared/Cargo/Components/BankClientComponent.cs
deleted file mode 100644 (file)
index a2bf804..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-using Robust.Shared.GameStates;
-
-namespace Content.Shared.Cargo.Components;
-
-/// <summary>
-/// Makes an entity a client of the station's bank account.
-/// When its balance changes it will have <see cref="BankBalanceUpdatedEvent"/> raised on it.
-/// Other systems can then use this for logic or to update ui states.
-/// </summary>
-[RegisterComponent, NetworkedComponent, Access(typeof(SharedCargoSystem))]
-[AutoGenerateComponentState]
-public sealed partial class BankClientComponent : Component
-{
-    /// <summary>
-    /// The balance updated for the last station this entity was a part of.
-    /// </summary>
-    [DataField, AutoNetworkedField]
-    public int Balance;
-}
-
-/// <summary>
-/// Raised on an entity with <see cref="BankClientComponent"/> when the bank's balance is updated.
-/// </summary>
-[ByRefEvent]
-public readonly record struct BankBalanceUpdatedEvent(EntityUid Station, int Balance);
index 873e9bb7b9da3e45e9d1bb9cd3051ce85670004b..90abfe8bfa39829eda2bea4be0fff701e3a81768 100644 (file)
+using Content.Shared.Access;
 using Content.Shared.Cargo.Prototypes;
 using Robust.Shared.Audio;
 using Robust.Shared.GameStates;
 using Content.Shared.Radio;
+using Content.Shared.Stacks;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Shared.Cargo.Components;
 
 /// <summary>
 /// Handles sending order requests to cargo. Doesn't handle orders themselves via shuttle or telepads.
 /// </summary>
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
+[Access(typeof(SharedCargoSystem))]
 public sealed partial class CargoOrderConsoleComponent : Component
 {
-    [DataField("soundError")] public SoundSpecifier ErrorSound =
-        new SoundPathSpecifier("/Audio/Effects/Cargo/buzz_sigh.ogg");
+    /// <summary>
+    /// The account that this console pulls from for ordering.
+    /// </summary>
+    [DataField]
+    public ProtoId<CargoAccountPrototype> Account = "Cargo";
+
+    [DataField]
+    public SoundSpecifier ErrorSound = new SoundCollectionSpecifier("CargoError");
+
+    /// <summary>
+    /// Sound made when <see cref="TransferUnbounded"/> is toggled.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier ToggleLimitSound = new SoundCollectionSpecifier("CargoToggleLimit");
+
+    /// <summary>
+    /// If true, account transfers have no limit and a lower cooldown.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool TransferUnbounded;
+
+    [ViewVariables]
+    public float TransferLimit => TransferUnbounded ? 1 : BaseTransferLimit;
+
+    /// <summary>
+    /// The maximum percent of total funds that can be transferred or withdrawn in one action.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float BaseTransferLimit = 0.20f;
+
+    /// <summary>
+    /// The time at which account actions can be performed again.
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
+    public TimeSpan NextAccountActionTime;
+
+    [ViewVariables]
+    public TimeSpan AccountActionDelay => TransferUnbounded ? UnboundedAccountActionDelay : BaseAccountActionDelay;
+
+    /// <summary>
+    /// The minimum time between account actions when <see cref="TransferUnbounded"/> is false
+    /// </summary>
+    [DataField]
+    public TimeSpan BaseAccountActionDelay = TimeSpan.FromMinutes(1);
 
-    [DataField("soundConfirm")]
-    public SoundSpecifier ConfirmSound = new SoundPathSpecifier("/Audio/Effects/Cargo/ping.ogg");
+    /// <summary>
+    /// The minimum time between account actions when <see cref="TransferUnbounded"/> is true
+    /// </summary>
+    [DataField]
+    public TimeSpan UnboundedAccountActionDelay = TimeSpan.FromSeconds(10);
+
+    /// <summary>
+    /// The stack representing cash dispensed on withdrawals.
+    /// </summary>
+    [DataField]
+    public ProtoId<StackPrototype> CashType = "Credit";
 
     /// <summary>
     /// All of the <see cref="CargoProductPrototype.Group"/>s that are supported.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField, AutoNetworkedField]
     public List<string> AllowedGroups = new() { "market" };
 
+    /// <summary>
+    /// Access needed to toggle the limit on this console.
+    /// </summary>
+    [DataField]
+    public HashSet<ProtoId<AccessLevelPrototype>> RemoveLimitAccess = new();
+
     /// <summary>
     /// Radio channel on which order approval announcements are transmitted
     /// </summary>
     [DataField, ViewVariables(VVAccess.ReadWrite)]
     public ProtoId<RadioChannelPrototype> AnnouncementChannel = "Supply";
+
+    /// <summary>
+    /// Secondary radio channel which always receives order announcements.
+    /// </summary>
+    public static readonly ProtoId<RadioChannelPrototype> BaseAnnouncementChannel = "Supply";
+}
+
+/// <summary>
+/// Withdraw funds from an account
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class CargoConsoleWithdrawFundsMessage : BoundUserInterfaceMessage
+{
+    public ProtoId<CargoAccountPrototype>? Account;
+    public int Amount;
+
+    public CargoConsoleWithdrawFundsMessage(ProtoId<CargoAccountPrototype>? account, int amount)
+    {
+        Account = account;
+        Amount = amount;
+    }
 }
 
+/// <summary>
+/// Toggle the limit on withdrawals and transfers.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class CargoConsoleToggleLimitMessage : BoundUserInterfaceMessage;
diff --git a/Content.Shared/Cargo/Components/FundingAllocationConsoleComponent.cs b/Content.Shared/Cargo/Components/FundingAllocationConsoleComponent.cs
new file mode 100644 (file)
index 0000000..65fd09a
--- /dev/null
@@ -0,0 +1,49 @@
+using Content.Shared.Cargo.Prototypes;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Cargo.Components;
+
+/// <summary>
+/// A console that manipulates the distribution of revenue on the station.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedCargoSystem))]
+public sealed partial class FundingAllocationConsoleComponent : Component
+{
+    /// <summary>
+    /// Sound played when the budget distribution is set.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier SetDistributionSound = new SoundCollectionSpecifier("CargoPing");
+}
+
+[Serializable, NetSerializable]
+public sealed class SetFundingAllocationBuiMessage : BoundUserInterfaceMessage
+{
+    public Dictionary<ProtoId<CargoAccountPrototype>, int> Percents;
+
+    public SetFundingAllocationBuiMessage(Dictionary<ProtoId<CargoAccountPrototype>, int> percents)
+    {
+        Percents = percents;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class FundingAllocationConsoleBuiState : BoundUserInterfaceState
+{
+    public NetEntity Station;
+
+    public FundingAllocationConsoleBuiState(NetEntity station)
+    {
+        Station = station;
+    }
+}
+
+[Serializable, NetSerializable]
+public enum FundingAllocationConsoleUiKey : byte
+{
+    Key
+}
diff --git a/Content.Shared/Cargo/Components/OverrideSellComponent.cs b/Content.Shared/Cargo/Components/OverrideSellComponent.cs
new file mode 100644 (file)
index 0000000..7d798c9
--- /dev/null
@@ -0,0 +1,17 @@
+using Content.Shared.Cargo.Prototypes;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Cargo.Components;
+
+/// <summary>
+/// Makes a sellable object portion out its value to a specified department rather than the station default
+/// </summary>
+[RegisterComponent]
+public sealed partial class OverrideSellComponent : Component
+{
+    /// <summary>
+    /// The account that will receive the primary funds from this being sold.
+    /// </summary>
+    [DataField(required: true)]
+    public ProtoId<CargoAccountPrototype> OverrideAccount;
+}
diff --git a/Content.Shared/Cargo/Components/StationBankAccountComponent.cs b/Content.Shared/Cargo/Components/StationBankAccountComponent.cs
new file mode 100644 (file)
index 0000000..e320ef8
--- /dev/null
@@ -0,0 +1,77 @@
+using Content.Shared.Cargo.Prototypes;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Cargo.Components;
+
+/// <summary>
+/// Added to the abstract representation of a station to track its money.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedCargoSystem)), AutoGenerateComponentPause, AutoGenerateComponentState]
+public sealed partial class StationBankAccountComponent : Component
+{
+    /// <summary>
+    /// The account that receives funds by default
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public ProtoId<CargoAccountPrototype> PrimaryAccount = "Cargo";
+
+    /// <summary>
+    /// When giving funds to a particular account, the proportion of funds they should receive compared to remaining accounts.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public double PrimaryCut = 0.75;
+
+    /// <summary>
+    /// A dictionary corresponding to the money held by each cargo account.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public Dictionary<ProtoId<CargoAccountPrototype>, int> Accounts = new()
+    {
+        { "Cargo",       2000 },
+        { "Engineering", 1000 },
+        { "Medical",     1000 },
+        { "Science",     1000 },
+        { "Security",    1000 },
+        { "Service",     1000 },
+    };
+
+    /// <summary>
+    /// A baseline distribution used for income and dispersing leftovers after sale.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public Dictionary<ProtoId<CargoAccountPrototype>, double> RevenueDistribution = new()
+    {
+        { "Cargo",       0.00 },
+        { "Engineering", 0.25 },
+        { "Medical",     0.30 },
+        { "Science",     0.15 },
+        { "Security",    0.20 },
+        { "Service",     0.10 },
+    };
+
+    /// <summary>
+    /// How much the bank balance goes up per second, every Delay period. Rounded down when multiplied.
+    /// </summary>
+    [DataField]
+    public int IncreasePerSecond = 2;
+
+    /// <summary>
+    /// The time at which the station will receive its next deposit of passive income
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
+    public TimeSpan NextIncomeTime;
+
+    /// <summary>
+    /// How much time to wait (in seconds) before increasing bank accounts balance.
+    /// </summary>
+    [DataField]
+    public TimeSpan IncomeDelay = TimeSpan.FromSeconds(50);
+}
+
+/// <summary>
+/// Broadcast and raised on station ent whenever its balance is updated.
+/// </summary>
+[ByRefEvent]
+public readonly record struct BankBalanceUpdatedEvent(EntityUid Station, Dictionary<ProtoId<CargoAccountPrototype>, int> Balance);
diff --git a/Content.Shared/Cargo/Prototypes/CargoAccountPrototype.cs b/Content.Shared/Cargo/Prototypes/CargoAccountPrototype.cs
new file mode 100644 (file)
index 0000000..185c8f9
--- /dev/null
@@ -0,0 +1,39 @@
+using Content.Shared.Radio;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Cargo.Prototypes;
+
+/// <summary>
+/// This is a prototype for a single account that stores money on StationBankAccountComponent
+/// </summary>
+[Prototype]
+public sealed partial class CargoAccountPrototype : IPrototype
+{
+    /// <inheritdoc/>
+    [IdDataField]
+    public string ID { get; } = default!;
+
+    /// <summary>
+    /// Full IC name of the account.
+    /// </summary>
+    [DataField]
+    public LocId Name;
+
+    /// <summary>
+    /// A shortened code used to refer to the account in UIs
+    /// </summary>
+    [DataField]
+    public LocId Code;
+
+    /// <summary>
+    /// Color corresponding to the account.
+    /// </summary>
+    [DataField]
+    public Color Color;
+
+    /// <summary>
+    /// Channel used for announcing transactions.
+    /// </summary>
+    [DataField]
+    public ProtoId<RadioChannelPrototype> RadioChannel;
+}
index 5351932cdf67e3515e10f4029a3d76f7a1df4d00..e2ef2348603565ab34059e86cb03dcbf62451a1b 100644 (file)
@@ -1,7 +1,54 @@
+using System.Linq;
+using Content.Shared.Cargo.Components;
+using Content.Shared.Cargo.Prototypes;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Cargo;
 
+public abstract class SharedCargoSystem : EntitySystem
+{
+    /// <summary>
+    /// For a given station, retrieves the balance in a specific account.
+    /// </summary>
+    public int GetBalanceFromAccount(Entity<StationBankAccountComponent?> station, ProtoId<CargoAccountPrototype> account)
+    {
+        if (!Resolve(station, ref station.Comp))
+            return 0;
+
+        return station.Comp.Accounts.GetValueOrDefault(account);
+    }
+
+    /// <summary>
+    /// For a station, creates a distribution between one "primary" account and the other accounts.
+    /// The primary account receives the majority cut specified, with the remaining accounts getting cuts
+    /// distributed through the remaining amount, based on <see cref="StationBankAccountComponent.RevenueDistribution"/>
+    /// </summary>
+    public Dictionary<ProtoId<CargoAccountPrototype>, double> CreateAccountDistribution(
+        ProtoId<CargoAccountPrototype> primary,
+        StationBankAccountComponent stationBank,
+        double primaryCut = 1.0)
+    {
+        var distribution = new Dictionary<ProtoId<CargoAccountPrototype>, double>
+        {
+            { primary, primaryCut }
+        };
+        var remaining = 1.0 - primaryCut;
+
+        var allAccountPercentages = new Dictionary<ProtoId<CargoAccountPrototype>, double>(stationBank.RevenueDistribution);
+        allAccountPercentages.Remove(primary);
+        var weightsSum = allAccountPercentages.Values.Sum();
+
+        foreach (var (account, percentage) in allAccountPercentages)
+        {
+            var adjustedPercentage = percentage / weightsSum;
+
+            distribution.Add(account, remaining * adjustedPercentage);
+        }
+        return distribution;
+    }
+}
+
 [NetSerializable, Serializable]
 public enum CargoConsoleUiKey : byte
 {
@@ -17,8 +64,6 @@ public enum CargoPalletConsoleUiKey : byte
     Sale
 }
 
-public abstract class SharedCargoSystem : EntitySystem {}
-
 [Serializable, NetSerializable]
 public enum CargoTelepadState : byte
 {
diff --git a/Resources/Locale/en-US/cargo/cargo-accounts.ftl b/Resources/Locale/en-US/cargo/cargo-accounts.ftl
new file mode 100644 (file)
index 0000000..fbad9cd
--- /dev/null
@@ -0,0 +1,17 @@
+cargo-account-cargo-name = Station Supply Budget
+cargo-account-cargo-code = SUP
+
+cargo-account-engineering-name = Maintenance Savings
+cargo-account-engineering-code = ENG
+
+cargo-account-medical-name = Crew Healthcare Fund
+cargo-account-medical-code = MED
+
+cargo-account-science-name = Interstellar Development Funding
+cargo-account-science-code = RND
+
+cargo-account-security-name = Station Defense Reserves
+cargo-account-security-code = SEC
+
+cargo-account-service-name = Collective Service Holdings
+cargo-account-service-code = SRV
index 7114924c02602a75b104066744633941cd983cc2..b7467771d95bd0ed90360e0f563868bad6da4c59 100644 (file)
@@ -1,10 +1,11 @@
 ## UI
 cargo-console-menu-title = Cargo request console
-cargo-console-menu-account-name-label = Account name:{" "}
+cargo-console-menu-account-name-label = Account:{" "}
 cargo-console-menu-account-name-none-text = None
+cargo-console-menu-account-name-format = [bold][color={$color}]{$name}[/color][/bold] [font="Monospace"]\[{$code}\][/font]
 cargo-console-menu-shuttle-name-label = Shuttle name:{" "}
 cargo-console-menu-shuttle-name-none-text = None
-cargo-console-menu-points-label = Spesos:{" "}
+cargo-console-menu-points-label = Balance:{" "}
 cargo-console-menu-points-amount = ${$amount}
 cargo-console-menu-shuttle-status-label = Shuttle status:{" "}
 cargo-console-menu-shuttle-status-away-text = Away
@@ -20,6 +21,16 @@ cargo-console-menu-populate-categories-all-text = All
 cargo-console-menu-populate-orders-cargo-order-row-product-name-text = {$productName} (x{$orderAmount}) by {$orderRequester}
 cargo-console-menu-cargo-order-row-approve-button = Approve
 cargo-console-menu-cargo-order-row-cancel-button = Cancel
+cargo-console-menu-tab-title-orders = Orders
+cargo-console-menu-tab-title-funds = Transfers
+cargo-console-menu-account-action-transfer-limit = [bold]Transfer Limit:[/bold] ${$limit}
+cargo-console-menu-account-action-transfer-limit-unlimited-notifier = [color=gold](Unlimited)[/color]
+cargo-console-menu-account-action-select = [bold]Account Action:[/bold]
+cargo-console-menu-account-action-amount = [bold]Amount:[/bold] $
+cargo-console-menu-account-action-button = Transfer
+cargo-console-menu-toggle-account-lock-button = Toggle Transfer Limit
+cargo-console-menu-account-action-option-withdraw = Withdraw Cash
+cargo-console-menu-account-action-option-transfer = Transfer Funds to {$code}
 
 # Orders
 cargo-console-order-not-allowed = Access not allowed
@@ -31,15 +42,21 @@ cargo-console-insufficient-funds = Insufficient funds (require {$cost})
 cargo-console-unfulfilled = No room to fulfill order
 cargo-console-trade-station = Sent to {$destination}
 cargo-console-unlock-approved-order-broadcast = [bold]{$productName} x{$orderAmount}[/bold], which cost [bold]{$cost}[/bold], was approved by [bold]{$approver}[/bold]
+cargo-console-fund-withdraw-broadcast = [bold]{$name} withdrew {$amount} spesos from {$name1} \[{$code1}\]
+cargo-console-fund-transfer-broadcast = [bold]{$name} transferred {$amount} spesos from {$name1} \[{$code1}\] to {$name2} \[{$code2}\][/bold]
+cargo-console-fund-transfer-user-unknown = Unknown
 
+cargo-console-paper-reason-default = None
+cargo-console-paper-approver-default = Self
 cargo-console-paper-print-name = Order #{$orderNumber}
-cargo-console-paper-print-text =
-    Order #{$orderNumber}
-    Item: {$itemName}
-    Quantity: {$orderQuantity}
-    Requested by: {$requester}
-    Reason: {$reason}
-    Approved by: {$approver}
+cargo-console-paper-print-text = [head=2]Order #{$orderNumber}[/head]
+    {"[bold]Item:[/bold]"} {$itemName} (x{$orderQuantity})
+    {"[bold]Requested by:[/bold]"} {$requester}
+
+    {"[head=3]Order Information[/head]"}
+    {"[bold]Payer[/bold]:"} {$account} [font="Monospace"]\[{$accountcode}\][/font]
+    {"[bold]Approved by:[/bold]"} {$approver}
+    {"[bold]Reason:[/bold]"} {$reason}
 
 # Cargo shuttle console
 cargo-shuttle-console-menu-title = Cargo shuttle console
@@ -47,3 +64,17 @@ cargo-shuttle-console-station-unknown = Unknown
 cargo-shuttle-console-shuttle-not-found = Not found
 cargo-shuttle-console-organics = Detected organic lifeforms on the shuttle
 cargo-no-shuttle = No cargo shuttle found!
+
+# Funding allocation console
+cargo-funding-alloc-console-menu-title = Funding Allocation Console
+cargo-funding-alloc-console-label-account = [bold]Account[/bold]
+cargo-funding-alloc-console-label-code = [bold] Code [/bold]
+cargo-funding-alloc-console-label-balance = [bold] Balance [/bold]
+cargo-funding-alloc-console-label-cut = [bold] Revenue Division (%) [/bold]
+
+cargo-funding-alloc-console-label-help = Cargo receives {$percent}% of all profits. The rest is split as specified below:
+cargo-funding-alloc-console-button-save = Save Changes
+cargo-funding-alloc-console-label-save-fail = [bold]Revenue Divisions Invalid![/bold] [color=red]({$pos ->
+    [1] +
+    *[-1] -
+}{$val}%)[/color]
diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_lockbox.yml b/Resources/Prototypes/Catalog/Cargo/cargo_lockbox.yml
new file mode 100644 (file)
index 0000000..92e8e8e
--- /dev/null
@@ -0,0 +1,49 @@
+- type: cargoProduct
+  id: CrateLockBoxEngineering
+  icon:
+    sprite: Structures/Storage/Crates/lockbox.rsi
+    state: icon
+  product: CrateLockBoxEngineering
+  cost: 100
+  category: cargoproduct-category-name-engineering
+  group: market
+
+- type: cargoProduct
+  id: CrateLockBoxMedical
+  icon:
+    sprite: Structures/Storage/Crates/lockbox.rsi
+    state: icon
+  product: CrateLockBoxMedical
+  cost: 100
+  category: cargoproduct-category-name-medical
+  group: market
+
+- type: cargoProduct
+  id: CrateLockBoxScience
+  icon:
+    sprite: Structures/Storage/Crates/lockbox.rsi
+    state: icon
+  product: CrateLockBoxScience
+  cost: 100
+  category: cargoproduct-category-name-science
+  group: market
+
+- type: cargoProduct
+  id: CrateLockBoxSecurity
+  icon:
+    sprite: Structures/Storage/Crates/lockbox.rsi
+    state: icon
+  product: CrateLockBoxSecurity
+  cost: 100
+  category: cargoproduct-category-name-security
+  group: market
+
+- type: cargoProduct
+  id: CrateLockBoxService
+  icon:
+    sprite: Structures/Storage/Crates/lockbox.rsi
+    state: icon
+  product: CrateLockBoxService
+  cost: 100
+  category: cargoproduct-category-name-service
+  group: market
index b63d661ae9818c8d79d55a709f503e405f05f8bf..bf49e30923f8235e5b7fb2ec8904d0b7b51614f2 100644 (file)
     - id: DoorRemoteService
     - id: HoPIDCard
     - id: IDComputerCircuitboard
+    - id: FundingAllocationComputerCircuitboard
+    - id: CargoRequestServiceComputerCircuitboard
     - id: RubberStampApproved
     - id: RubberStampDenied
     - id: RubberStampHop
     - id: ClothingHandsGlovesColorYellow
     - id: ClothingHeadsetAltEngineering
     - id: DoorRemoteEngineering
+    - id: CargoRequestEngineeringComputerCircuitboard
     - id: RCD
     - id: RCDAmmo
     - id: RubberStampCE
     - id: HandheldCrewMonitor
     - id: Hypospray
     - id: MedicalTechFabCircuitboard
+    - id: CargoRequestMedicalComputerCircuitboard
     - id: MedkitFilled
     - id: RubberStampCMO
     - id: MedTekCartridge
     - id: HandTeleporter
     - id: ProtolatheMachineCircuitboard
     - id: ResearchComputerCircuitboard
+    - id: CargoRequestScienceComputerCircuitboard
     - id: RubberStampRd
 
 # Hardsuit table, used for suit storage as well
     - id: HoloprojectorSecurity
     - id: RubberStampHos
     - id: SecurityTechFabCircuitboard
+    - id: CargoRequestSecurityComputerCircuitboard
     - id: WeaponDisabler
     - id: WantedListCartridge
     - id: DrinkHosFlask
diff --git a/Resources/Prototypes/Catalog/cargo_accounts.yml b/Resources/Prototypes/Catalog/cargo_accounts.yml
new file mode 100644 (file)
index 0000000..7ddc07e
--- /dev/null
@@ -0,0 +1,42 @@
+- type: cargoAccount
+  id: Cargo
+  name: cargo-account-cargo-name
+  code: cargo-account-cargo-code
+  color: "#b48b57"
+  radioChannel: Supply
+
+- type: cargoAccount
+  id: Engineering
+  name: cargo-account-engineering-name
+  code: cargo-account-engineering-code
+  color: "#ff733c"
+  radioChannel: Engineering
+
+- type: cargoAccount
+  id: Medical
+  name: cargo-account-medical-name
+  code: cargo-account-medical-code
+  color: "#57b8f0"
+  radioChannel: Medical
+
+- type: cargoAccount
+  id: Science
+  name: cargo-account-science-name
+  code: cargo-account-science-code
+  color: "#cd7ccd"
+  radioChannel: Science
+
+- type: cargoAccount
+  id: Security
+  name: cargo-account-security-name
+  code: cargo-account-security-code
+  color: "#ff4242"
+  radioChannel: Security
+
+- type: cargoAccount
+  id: Service
+  name: cargo-account-service-name
+  code: cargo-account-service-code
+  color: "#539c00"
+  radioChannel: Service
+
index e964ae3b519d425d8cfde96bfa45b71f7911e97b..03fc16407cc7076d5d4888e3124efd76eaa7874d 100644 (file)
@@ -77,7 +77,6 @@
   - type: RadarConsole
     followEntity: true
   - type: CargoOrderConsole
-  - type: BankClient
   - type: CrewMonitoringConsole
   - type: GeneralStationRecordConsole
     canDeleteEntries: true
index c7281173c4eeb159855f1f4ad06ca89eef707bcd..69734a5c5789800ec2e26f28edd8c14d62bfe403 100644 (file)
     - type: StaticPrice
       price: 750
 
+- type: entity
+  parent: BaseComputerCircuitboard
+  id: CargoRequestEngineeringComputerCircuitboard
+  name: engineering request computer board
+  description: A computer printed circuit board for an engineering request computer.
+  components:
+  - type: Sprite
+    state: cpu_engineering
+  - type: ComputerBoard
+    prototype: ComputerCargoOrdersEngineering
+  - type: StaticPrice
+    price: 750
+
+- type: entity
+  parent: BaseComputerCircuitboard
+  id: CargoRequestMedicalComputerCircuitboard
+  name: medical request computer board
+  description: A computer printed circuit board for a medical request computer.
+  components:
+  - type: Sprite
+    state: cpu_medical
+  - type: ComputerBoard
+    prototype: ComputerCargoOrdersMedical
+  - type: StaticPrice
+    price: 750
+
+- type: entity
+  parent: BaseComputerCircuitboard
+  id: CargoRequestScienceComputerCircuitboard
+  name: science request computer board
+  description: A computer printed circuit board for a science request computer.
+  components:
+  - type: Sprite
+    state: cpu_science
+  - type: ComputerBoard
+    prototype: ComputerCargoOrdersScience
+  - type: StaticPrice
+    price: 750
+
+- type: entity
+  parent: BaseComputerCircuitboard
+  id: CargoRequestSecurityComputerCircuitboard
+  name: security request computer board
+  description: A computer printed circuit board for a security request computer.
+  components:
+  - type: Sprite
+    state: cpu_security
+  - type: ComputerBoard
+    prototype: ComputerCargoOrdersSecurity
+  - type: StaticPrice
+    price: 750
+
+- type: entity
+  parent: BaseComputerCircuitboard
+  id: CargoRequestServiceComputerCircuitboard
+  name: service request computer board
+  description: A computer printed circuit board for a service request computer.
+  components:
+  - type: Sprite
+    state: cpu_service
+  - type: ComputerBoard
+    prototype: ComputerCargoOrdersService
+  - type: StaticPrice
+    price: 750
+
+- type: entity
+  parent: BaseComputerCircuitboard
+  id: FundingAllocationComputerCircuitboard
+  name: funding allocation computer board
+  description: A computer printed circuit board for a funding allocation card console.
+  components:
+  - type: Sprite
+    state: cpu_command
+  - type: ComputerBoard
+    prototype: ComputerFundingAllocation
+  - type: StaticPrice
+    price: 750
+
 - type: entity
   parent: BaseComputerCircuitboard
   id: CargoSaleComputerCircuitboard
index de4e38cc7a20f1f6cbed6c3c721302893cf52072..7974d70a27aaa4fe777f05f67e449a0980d17a13 100644 (file)
           tags:
           - Write
   - type: CargoOrderConsole
-  - type: BankClient
+    removeLimitAccess: [ "Quartermaster" ]
   - type: ActivatableUI
     verbText: qm-clipboard-computer-verb-text
     key: enum.CargoConsoleUiKey.Orders
index 5490dcc5745b540d519962a61840b1af7ba6bcdc..6221757a2de0e9db8e8cab4d5f5e3cea7fc90bea 100644 (file)
   components:
     - type: StationBankAccount
     - type: StationCargoOrderDatabase
+      orders:
+        Cargo: [ ]
+        Engineering: [ ]
+        Medical: [ ]
+        Science: [ ]
+        Security: [ ]
+        Service: [ ]
     - type: StationCargoBountyDatabase
 
 - type: entity
index 3a1684bbc7fb3fab2a45fe42112896b3e37ec045..4ee4c0de0a03d6f803aa9e98a20073ca7ed7a144 100644 (file)
     - map: ["computerLayerScreen"]
       state: request
     - map: ["computerLayerKeys"]
-      state: tech_key
+      state: generic_keys
     - map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
       state: generic_panel_open
   - type: CargoOrderConsole
-  - type: BankClient
+    removeLimitAccess: [ "Quartermaster" ]
   - type: ActiveRadio
     channels:
     - Supply
     guides:
     - Cargo
 
+# Request console variants.
+- type: entity
+  id: ComputerCargoOrdersEngineering
+  parent: ComputerCargoOrders
+  name: engineering request computer
+  description: Used by the engineering department to order supplies.
+  components:
+  - type: Sprite
+    layers:
+    - map: ["computerLayerBody"]
+      state: computer
+    - map: ["computerLayerKeyboard"]
+      state: generic_keyboard
+    - map: ["computerLayerScreen"]
+      state: request-eng
+    - map: ["computerLayerKeys"]
+      state: generic_keys
+    - map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
+      state: generic_panel_open
+  - type: CargoOrderConsole
+    account: Engineering
+    announcementChannel: Engineering
+    removeLimitAccess: [ "ChiefEngineer" ]
+  - type: ActiveRadio
+    channels:
+    - Engineering
+  - type: Computer
+    board: CargoRequestEngineeringComputerCircuitboard
+  - type: PointLight
+    color: "#c9c042"
+  - type: AccessReader
+    access: [["Engineering"]]
+
+- type: entity
+  id: ComputerCargoOrdersMedical
+  parent: ComputerCargoOrders
+  name: medical request computer
+  description: Used by the medical department to order supplies.
+  components:
+  - type: Sprite
+    layers:
+    - map: ["computerLayerBody"]
+      state: computer
+    - map: ["computerLayerKeyboard"]
+      state: generic_keyboard
+    - map: ["computerLayerScreen"]
+      state: request-med
+    - map: ["computerLayerKeys"]
+      state: generic_keys
+    - map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
+      state: generic_panel_open
+  - type: CargoOrderConsole
+    account: Medical
+    announcementChannel: Medical
+    removeLimitAccess: [ "ChiefMedicalOfficer" ]
+  - type: ActiveRadio
+    channels:
+    - Medical
+  - type: Computer
+    board: CargoRequestMedicalComputerCircuitboard
+  - type: PointLight
+    color: "#41e0fc"
+  - type: AccessReader
+    access: [["Medical"]]
+
+- type: entity
+  id: ComputerCargoOrdersScience
+  parent: ComputerCargoOrders
+  name: science request computer
+  description: Used by the science department to order supplies.
+  components:
+  - type: Sprite
+    layers:
+    - map: ["computerLayerBody"]
+      state: computer
+    - map: ["computerLayerKeyboard"]
+      state: generic_keyboard
+    - map: ["computerLayerScreen"]
+      state: request-sci
+    - map: ["computerLayerKeys"]
+      state: generic_keys
+    - map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
+      state: generic_panel_open
+  - type: CargoOrderConsole
+    account: Science
+    announcementChannel: Science
+    removeLimitAccess: [ "ResearchDirector" ]
+  - type: ActiveRadio
+    channels:
+    - Science
+  - type: Computer
+    board: CargoRequestScienceComputerCircuitboard
+  - type: PointLight
+    color: "#b53ca1"
+  - type: AccessReader
+    access: [["Research"]]
+
+- type: entity
+  id: ComputerCargoOrdersSecurity
+  parent: ComputerCargoOrders
+  name: security request computer
+  description: Used by the security department to order supplies.
+  components:
+  - type: Sprite
+    layers:
+    - map: ["computerLayerBody"]
+      state: computer
+    - map: ["computerLayerKeyboard"]
+      state: generic_keyboard
+    - map: ["computerLayerScreen"]
+      state: request-sec
+    - map: ["computerLayerKeys"]
+      state: generic_keys
+    - map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
+      state: generic_panel_open
+  - type: CargoOrderConsole
+    account: Security
+    announcementChannel: Security
+    removeLimitAccess: [ "HeadOfSecurity" ]
+  - type: ActiveRadio
+    channels:
+    - Security
+  - type: Computer
+    board: CargoRequestSecurityComputerCircuitboard
+  - type: PointLight
+    color: "#d11d00"
+  - type: AccessReader
+    access: [["Security"]]
+
+- type: entity
+  id: ComputerCargoOrdersService
+  parent: ComputerCargoOrders
+  name: service request computer
+  description: Used by the service department to order supplies.
+  components:
+  - type: Sprite
+    layers:
+    - map: ["computerLayerBody"]
+      state: computer
+    - map: ["computerLayerKeyboard"]
+      state: generic_keyboard
+    - map: ["computerLayerScreen"]
+      state: request-srv
+    - map: ["computerLayerKeys"]
+      state: generic_keys
+    - map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
+      state: generic_panel_open
+  - type: CargoOrderConsole
+    account: Service
+    announcementChannel: Service
+    removeLimitAccess: [ "HeadOfPersonnel" ]
+  - type: ActiveRadio
+    channels:
+    - Service
+  - type: Computer
+    board: CargoRequestServiceComputerCircuitboard
+  - type: PointLight
+    color: "#afe837"
+  - type: AccessReader
+    access: [["Service"]]
+
 - type: entity
   id: ComputerCargoBounty
   parent: BaseComputerAiAccess
     - CargoBounties
     - Cargo
 
+- type: entity
+  parent: BaseComputerAiAccess
+  id: ComputerFundingAllocation
+  name: funding allocation computer
+  description: Terminal for controlling the distribution of funds and pay to departments.
+  components:
+  - type: Sprite
+    layers:
+    - map: ["computerLayerBody"]
+      state: computer
+    - map: ["computerLayerKeyboard"]
+      state: generic_keyboard
+    - map: ["computerLayerScreen"]
+      state: allocate # ALLOCATION !!!
+    - map: ["computerLayerKeys"]
+      state: generic_keys
+    - map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
+      state: generic_panel_open
+  - type: FundingAllocationConsole
+  - type: ActivatableUI
+    key: enum.FundingAllocationConsoleUiKey.Key
+  - type: ActivatableUIRequiresAccess
+  - type: AccessReader
+    access: [["HeadOfPersonnel"]]
+  - type: UserInterface
+    interfaces:
+      enum.FundingAllocationConsoleUiKey.Key:
+        type: FundingAllocationConsoleBoundUserInterface
+      enum.WiresUiKey.Key:
+        type: WiresBoundUserInterface
+  - type: Computer
+    board: FundingAllocationComputerCircuitboard
+  - type: PointLight
+    radius: 1.5
+    energy: 1.6
+    color: "#3c5eb5"
+
 - type: entity
   parent: BaseComputerAiAccess
   id: ComputerCloningConsole
     - map: ["computerLayerKeyboard"]
       state: generic_keyboard
     - map: ["computerLayerScreen"]
-      state: request
+      state: transfer
     - map: ["computerLayerKeys"]
-      state: tech_key
+      state: generic_keys
     - map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
       state: generic_panel_open
   - type: Anchorable
index 74b3fca79f977e23c3237b8100339d64e9c7f6ed..a12ff6773ea8f95520b26b30cd9eecb61b1ca586 100644 (file)
   - type: StaticPrice
     price: 75
 
+- type: entity
+  id: CrateBaseLockBox
+  parent: CrateBaseSecure
+  name: lock box
+  description: "A secure lock box. Funds from its sale will be distributed back to the department. Just remember: Cargo always takes a cut."
+  abstract: true
+  components:
+  - type: Sprite
+    sprite: Structures/Storage/Crates/lockbox.rsi
+  - type: GenericVisualizer
+    visuals:
+      enum.PaperLabelVisuals.HasLabel:
+        enum.PaperLabelVisuals.Layer:
+          True: { visible: true }
+          False: { visible: false }
+      enum.PaperLabelVisuals.LabelType:
+        enum.PaperLabelVisuals.Layer:
+          Paper: { state: paper }
+          Bounty: { state: bounty }
+          CaptainsPaper: { state: captains_paper }
+          Invoice: { state: invoice }
+      enum.StorageVisuals.Open:
+        lid_overlay:
+          True: { visible: false }
+          False: { visible: true }
+  - type: OverrideSell
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.3,-0.4,0.3,0.19"
+        density: 50
+        mask:
+        - CrateMask #this is so they can go under plastic flaps
+        layer:
+        - MachineLayer
+
+- type: entity
+  id: CrateLockBoxEngineering
+  parent: CrateBaseLockBox
+  name: engineering lock box
+  components:
+  - type: Sprite
+    layers:
+    - state: base
+    - state: overlay
+      color: "#ad8c27"
+    - state: closed
+      map: ["enum.StorageVisualLayers.Door"]
+    - state: overlay-closed
+      color: "#ad8c27"
+      map: [ lid_overlay ]
+    - state: welded
+      visible: false
+      map: ["enum.WeldableLayers.BaseWelded"]
+    - state: locked
+      map: ["enum.LockVisualLayers.Lock"]
+      shader: unshaded
+    - state: paper
+      sprite: Structures/Storage/Crates/labels.rsi
+      offset: "-0.46875,0.03125"
+      map: ["enum.PaperLabelVisuals.Layer"]
+  - type: OverrideSell
+    overrideAccount: Engineering
+  - type: AccessReader
+    access: [["Engineering"]]
+
+- type: entity
+  id: CrateLockBoxMedical
+  parent: CrateBaseLockBox
+  name: medical lock box
+  components:
+  - type: Sprite
+    layers:
+    - state: base
+    - state: overlay
+      color: "#92c7e8"
+    - state: closed
+      map: ["enum.StorageVisualLayers.Door"]
+    - state: overlay-closed
+      color: "#92c7e8"
+      map: [ lid_overlay ]
+    - state: welded
+      visible: false
+      map: ["enum.WeldableLayers.BaseWelded"]
+    - state: locked
+      map: ["enum.LockVisualLayers.Lock"]
+      shader: unshaded
+    - state: paper
+      sprite: Structures/Storage/Crates/labels.rsi
+      offset: "-0.46875,0.03125"
+      map: ["enum.PaperLabelVisuals.Layer"]
+  - type: OverrideSell
+    overrideAccount: Medical
+  - type: AccessReader
+    access: [["Medical"]]
+
+- type: entity
+  id: CrateLockBoxScience
+  parent: CrateBaseLockBox
+  name: science lock box
+  components:
+  - type: Sprite
+    layers:
+    - state: base
+    - state: overlay
+      color: "#ba4bf0"
+    - state: closed
+      map: ["enum.StorageVisualLayers.Door"]
+    - state: overlay-closed
+      color: "#ba4bf0"
+      map: [ lid_overlay ]
+    - state: welded
+      visible: false
+      map: ["enum.WeldableLayers.BaseWelded"]
+    - state: locked
+      map: ["enum.LockVisualLayers.Lock"]
+      shader: unshaded
+    - state: paper
+      sprite: Structures/Storage/Crates/labels.rsi
+      offset: "-0.46875,0.03125"
+      map: ["enum.PaperLabelVisuals.Layer"]
+  - type: OverrideSell
+    overrideAccount: Science
+  - type: AccessReader
+    access: [["Research"]]
+
+- type: entity
+  id: CrateLockBoxSecurity
+  parent: CrateBaseLockBox
+  name: security lock box
+  components:
+  - type: Sprite
+    layers:
+    - state: base
+    - state: overlay
+      color: "#c12d30"
+    - state: closed
+      map: ["enum.StorageVisualLayers.Door"]
+    - state: overlay-closed
+      color: "#c12d30"
+      map: [ lid_overlay ]
+    - state: welded
+      visible: false
+      map: ["enum.WeldableLayers.BaseWelded"]
+    - state: locked
+      map: ["enum.LockVisualLayers.Lock"]
+      shader: unshaded
+    - state: paper
+      sprite: Structures/Storage/Crates/labels.rsi
+      offset: "-0.46875,0.03125"
+      map: ["enum.PaperLabelVisuals.Layer"]
+  - type: OverrideSell
+    overrideAccount: Security
+  - type: AccessReader
+    access: [["Security"]]
+
+- type: entity
+  id: CrateLockBoxService
+  parent: CrateBaseLockBox
+  name: service lock box
+  components:
+  - type: Sprite
+    layers:
+    - state: base
+    - state: overlay
+      color: "#53b723"
+    - state: closed
+      map: ["enum.StorageVisualLayers.Door"]
+    - state: overlay-closed
+      color: "#53b723"
+      map: [ lid_overlay ]
+    - state: welded
+      visible: false
+      map: ["enum.WeldableLayers.BaseWelded"]
+    - state: locked
+      map: ["enum.LockVisualLayers.Lock"]
+      shader: unshaded
+    - state: paper
+      sprite: Structures/Storage/Crates/labels.rsi
+      offset: "-0.46875,0.03125"
+      map: ["enum.PaperLabelVisuals.Layer"]
+  - type: OverrideSell
+    overrideAccount: Service
+  - type: AccessReader
+    access: [["Service"]]
+
 - type: entity
   parent: CrateGeneric
   id: CratePirate
diff --git a/Resources/Prototypes/SoundCollections/machines.yml b/Resources/Prototypes/SoundCollections/machines.yml
new file mode 100644 (file)
index 0000000..7848da0
--- /dev/null
@@ -0,0 +1,14 @@
+- type: soundCollection
+  id: CargoPing
+  files:
+  - /Audio/Effects/Cargo/ping.ogg
+
+- type: soundCollection
+  id: CargoError
+  files:
+  - /Audio/Effects/Cargo/buzz_sigh.ogg
+
+- type: soundCollection
+  id: CargoToggleLimit
+  files:
+  - /Audio/Machines/quickbeep.ogg
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/allocate.png b/Resources/Textures/Structures/Machines/computers.rsi/allocate.png
new file mode 100644 (file)
index 0000000..6743d29
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/allocate.png differ
index e0127936dd1cf752072de98d1084e419df1f6a20..0502bd36619d25770cd757fdda11f97b61e77154 100644 (file)
@@ -1,7 +1,7 @@
 {
   "version": 1,
   "license": "CC-BY-SA-3.0",
-  "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/bd6873fd4dd6a61d7e46f1d75cd4d90f64c40894. comm_syndie made by Veritius, based on comm. generic_panel_open made by Errant, commit https://github.com/space-wizards/space-station-14/pull/32273, comms_wizard and wizard_key by ScarKy0",
+  "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/bd6873fd4dd6a61d7e46f1d75cd4d90f64c40894. comm_syndie made by Veritius, based on comm. generic_panel_open made by Errant, commit https://github.com/space-wizards/space-station-14/pull/32273, comms_wizard and wizard_key by ScarKy0, request- variants transfer made by EmoGarbage404 (github)",
   "size": {
     "x": 32,
     "y": 32
                 ]
             ]
         },
+        {
+            "name": "allocate",
+            "directions": 4,
+            "delays": [
+                [
+                    1.0,
+                    1.0,
+                    1.0
+                ],
+                [
+                    1.0,
+                    1.0,
+                    1.0
+                ],
+                [
+                    1.0,
+                    1.0,
+                    1.0
+                ],
+                [
+                    1.0,
+                    1.0,
+                    1.0
+                ]
+            ]
+        },
         {
             "name": "area_atmos",
             "directions": 4,
                 ]
             ]
         },
+        {
+            "name": "request-eng",
+            "directions": 4,
+            "delays": [
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ]
+            ]
+        },
+        {
+            "name": "request-med",
+            "directions": 4,
+            "delays": [
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ]
+            ]
+        },
+        {
+            "name": "request-sci",
+            "directions": 4,
+            "delays": [
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ]
+            ]
+        },
+        {
+            "name": "request-sec",
+            "directions": 4,
+            "delays": [
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ]
+            ]
+        },
+        {
+            "name": "request-srv",
+            "directions": 4,
+            "delays": [
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ],
+                [
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3,
+                    0.3
+                ]
+            ]
+        },
         {
             "name": "robot",
             "directions": 4
             "name": "telesci_key_off",
             "directions": 4
         },
+        {
+            "name": "transfer",
+            "directions": 4,
+            "delays": [
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ],
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ],
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ],
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        },
         {
             "name": "turbinecomp",
             "directions": 4
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/request-eng.png b/Resources/Textures/Structures/Machines/computers.rsi/request-eng.png
new file mode 100644 (file)
index 0000000..8453112
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/request-eng.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/request-med.png b/Resources/Textures/Structures/Machines/computers.rsi/request-med.png
new file mode 100644 (file)
index 0000000..29ee175
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/request-med.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/request-sci.png b/Resources/Textures/Structures/Machines/computers.rsi/request-sci.png
new file mode 100644 (file)
index 0000000..c395cfc
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/request-sci.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/request-sec.png b/Resources/Textures/Structures/Machines/computers.rsi/request-sec.png
new file mode 100644 (file)
index 0000000..1a2c8e0
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/request-sec.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/request-srv.png b/Resources/Textures/Structures/Machines/computers.rsi/request-srv.png
new file mode 100644 (file)
index 0000000..46468ec
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/request-srv.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/transfer.png b/Resources/Textures/Structures/Machines/computers.rsi/transfer.png
new file mode 100644 (file)
index 0000000..86bb376
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/transfer.png differ
diff --git a/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/base.png b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/base.png
new file mode 100644 (file)
index 0000000..1088abb
Binary files /dev/null and b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/base.png differ
diff --git a/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/closed.png b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/closed.png
new file mode 100644 (file)
index 0000000..c081d66
Binary files /dev/null and b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/closed.png differ
diff --git a/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/icon.png b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/icon.png
new file mode 100644 (file)
index 0000000..53bfb09
Binary files /dev/null and b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/icon.png differ
diff --git a/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/locked.png b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/locked.png
new file mode 100644 (file)
index 0000000..747da48
Binary files /dev/null and b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/locked.png differ
diff --git a/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/meta.json b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/meta.json
new file mode 100644 (file)
index 0000000..6d33e62
--- /dev/null
@@ -0,0 +1,51 @@
+{
+  "version": 1,
+  "license": "CC-BY-SA-3.0",
+  "copyright": "Created by EmoGarbage404 (github) for Space Station 14.",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "states": [
+    {
+      "name": "icon"
+    },
+    {
+      "name": "base"
+    },
+    {
+      "name": "closed"
+    },
+    {
+      "name": "overlay"
+    },
+    {
+      "name": "overlay-closed"
+    },
+    {
+      "name": "open"
+    },
+    {
+      "name": "welded"
+    },
+    {
+      "name": "locked"
+    },
+    {
+      "name": "unlocked"
+    },
+    {
+      "name": "sparking",
+      "delays": [
+        [
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1
+        ]
+      ]
+    }
+  ]
+}
diff --git a/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/open.png b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/open.png
new file mode 100644 (file)
index 0000000..1c14d5d
Binary files /dev/null and b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/open.png differ
diff --git a/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/overlay-closed.png b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/overlay-closed.png
new file mode 100644 (file)
index 0000000..7f8745c
Binary files /dev/null and b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/overlay-closed.png differ
diff --git a/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/overlay.png b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/overlay.png
new file mode 100644 (file)
index 0000000..6a0495b
Binary files /dev/null and b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/overlay.png differ
diff --git a/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/sparking.png b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/sparking.png
new file mode 100644 (file)
index 0000000..c31e3a1
Binary files /dev/null and b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/sparking.png differ
diff --git a/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/unlocked.png b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/unlocked.png
new file mode 100644 (file)
index 0000000..11715f9
Binary files /dev/null and b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/unlocked.png differ
diff --git a/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/welded.png b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/welded.png
new file mode 100644 (file)
index 0000000..e8ae93c
Binary files /dev/null and b/Resources/Textures/Structures/Storage/Crates/lockbox.rsi/welded.png differ