]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Local Material Silo (#36492)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Fri, 18 Apr 2025 23:43:17 +0000 (19:43 -0400)
committerGitHub <noreply@github.com>
Fri, 18 Apr 2025 23:43:17 +0000 (09:43 +1000)
* Material Silo

* fix board, fix copyright

* a bit of review.... for the vibe....

* a tiny bit of review

* 4 spaced

* sloths no good very tiny nitpick

* fix ui flickers

* oops

* slightly lower range

* Sloth Review

---------

Co-authored-by: ScarKy0 <scarky0@onet.eu>
28 files changed:
Content.Client/Lathe/UI/LatheMenu.xaml
Content.Client/Materials/OreSiloSystem.cs [new file with mode: 0644]
Content.Client/Materials/UI/MaterialStorageControl.xaml
Content.Client/Materials/UI/MaterialStorageControl.xaml.cs
Content.Client/Materials/UI/OreSiloBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Materials/UI/OreSiloMenu.xaml [new file with mode: 0644]
Content.Client/Materials/UI/OreSiloMenu.xaml.cs [new file with mode: 0644]
Content.Server/Materials/MaterialStorageSystem.cs
Content.Server/Materials/OreSiloSystem.cs [new file with mode: 0644]
Content.Server/Pinpointer/NavMapSystem.cs
Content.Shared/Materials/MaterialStorageComponent.cs
Content.Shared/Materials/OreSilo/OreSiloClientComponent.cs [new file with mode: 0644]
Content.Shared/Materials/OreSilo/OreSiloComponent.cs [new file with mode: 0644]
Content.Shared/Materials/OreSilo/SharedOreSiloSystem.cs [new file with mode: 0644]
Content.Shared/Materials/SharedMaterialStorageSystem.cs
Resources/Locale/en-US/lathe/ui/lathe-menu.ftl
Resources/Locale/en-US/materials/silo.ftl [new file with mode: 0644]
Resources/Prototypes/Catalog/Cargo/cargo_materials.yml
Resources/Prototypes/Catalog/Fills/Crates/materials.yml
Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml
Resources/Prototypes/Entities/Structures/Machines/flatpacker.yml
Resources/Prototypes/Entities/Structures/Machines/lathe.yml
Resources/Prototypes/Entities/Structures/Machines/silo.yml [new file with mode: 0644]
Resources/Textures/Structures/Machines/silo.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Structures/Machines/silo.rsi/overlay_active.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/silo.rsi/silo.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/silo.rsi/silo_active.png [new file with mode: 0644]

index d5e35731487acee18303f8c05adab9bf8e94502e..28b79254c02f73147540189bce9ef91d3f963781 100644 (file)
             HorizontalExpand="True"
             Orientation="Vertical">
             <Label Text="{Loc 'lathe-menu-materials-title'}" Margin="5 5 5 5" HorizontalAlignment="Center"/>
-            <BoxContainer
-                Orientation="Vertical"
-                VerticalExpand="True"
-                HorizontalExpand="True">
-                <ui:MaterialStorageControl Name="MaterialsList" SizeFlagsStretchRatio="8"/>
-            </BoxContainer>
+            <PanelContainer VerticalExpand="True">
+                <PanelContainer.PanelOverride>
+                    <gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
+                </PanelContainer.PanelOverride>
+                <BoxContainer
+                    Orientation="Vertical"
+                    VerticalExpand="True"
+                    HorizontalExpand="True">
+                    <ui:MaterialStorageControl Name="MaterialsList" SizeFlagsStretchRatio="8"/>
+                </BoxContainer>
+            </PanelContainer>
         </BoxContainer>
         </BoxContainer>
 
diff --git a/Content.Client/Materials/OreSiloSystem.cs b/Content.Client/Materials/OreSiloSystem.cs
new file mode 100644 (file)
index 0000000..076a546
--- /dev/null
@@ -0,0 +1,6 @@
+using Content.Shared.Materials.OreSilo;
+
+namespace Content.Client.Materials;
+
+/// <inheritdoc/>
+public sealed class OreSiloSystem : SharedOreSiloSystem;
index 2be0f40aa51a4eafe4925817ec50650327d49869..d7503a61f3b990fe7c6f7215696add953cd22597 100644 (file)
@@ -2,7 +2,10 @@
               SizeFlagsStretchRatio="8"
               HorizontalExpand="True"
               VerticalExpand="True">
-    <BoxContainer Name="MaterialList" Orientation="Vertical">
-        <Label Name="NoMatsLabel" Text="{Loc 'lathe-menu-no-materials-message'}" Align="Center"/>
+    <BoxContainer Orientation="Vertical" VerticalExpand="True">
+        <BoxContainer Name="MaterialList" Orientation="Vertical" VerticalExpand="True">
+            <Label Name="NoMatsLabel" Text="{Loc 'lathe-menu-no-materials-message'}" HorizontalAlignment="Center" VerticalAlignment="Center" VerticalExpand="True"/>
+        </BoxContainer>
+        <Label Name="SiloLinkedLabel" Text="{Loc 'lathe-menu-silo-linked-message'}" StyleClasses="LabelSubText" Visible="False" HorizontalAlignment="Center"/>
     </BoxContainer>
 </ScrollContainer>
index 3cf1792c144c50311eee2aba0d21efbd83c7f5bd..fd698d890fc4671aa77fd68b45854c513e5a2cb2 100644 (file)
@@ -1,5 +1,6 @@
 using System.Linq;
 using Content.Shared.Materials;
+using Content.Shared.Materials.OreSilo;
 using Robust.Client.AutoGenerated;
 using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.XAML;
@@ -15,6 +16,7 @@ namespace Content.Client.Materials.UI;
 public sealed partial class MaterialStorageControl : ScrollContainer
 {
     [Dependency] private readonly IEntityManager _entityManager = default!;
+    private readonly MaterialStorageSystem _materialStorage;
 
     private EntityUid? _owner;
 
@@ -24,6 +26,8 @@ public sealed partial class MaterialStorageControl : ScrollContainer
     {
         RobustXamlLoader.Load(this);
         IoCManager.InjectDependencies(this);
+
+        _materialStorage = _entityManager.System<MaterialStorageSystem>();
     }
 
     public void SetOwner(EntityUid owner)
@@ -45,7 +49,8 @@ public sealed partial class MaterialStorageControl : ScrollContainer
         }
 
         var canEject = materialStorage.CanEjectStoredMaterials;
-        var mats = materialStorage.Storage;
+        var mats = _materialStorage.GetStoredMaterials((_owner.Value, materialStorage));
+
         if (_currentMaterials.Equals(mats))
             return;
 
@@ -89,5 +94,6 @@ public sealed partial class MaterialStorageControl : ScrollContainer
 
         _currentMaterials = mats;
         NoMatsLabel.Visible = MaterialList.ChildCount == 1;
+        SiloLinkedLabel.Visible = _entityManager.TryGetComponent<OreSiloClientComponent>(_owner.Value, out var client) && client.Silo != null;
     }
 }
diff --git a/Content.Client/Materials/UI/OreSiloBoundUserInterface.cs b/Content.Client/Materials/UI/OreSiloBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..32eed56
--- /dev/null
@@ -0,0 +1,34 @@
+using Content.Shared.Materials.OreSilo;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Materials.UI;
+
+[UsedImplicitly]
+public sealed class OreSiloBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
+{
+    [ViewVariables]
+    private OreSiloMenu? _menu;
+
+    protected override void Open()
+    {
+        base.Open();
+
+        _menu = this.CreateWindow<OreSiloMenu>();
+        _menu.SetEntity(Owner);
+
+        _menu.OnClientEntryPressed += netEnt =>
+        {
+            SendPredictedMessage(new ToggleOreSiloClientMessage(netEnt));
+        };
+    }
+
+    protected override void UpdateState(BoundUserInterfaceState state)
+    {
+        base.UpdateState(state);
+
+        if (state is not OreSiloBuiState msg)
+            return;
+        _menu?.Update(msg);
+    }
+}
diff --git a/Content.Client/Materials/UI/OreSiloMenu.xaml b/Content.Client/Materials/UI/OreSiloMenu.xaml
new file mode 100644 (file)
index 0000000..3a8e0ef
--- /dev/null
@@ -0,0 +1,42 @@
+<controls:FancyWindow xmlns="https://spacestation14.io"
+                     xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+                     xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+                     xmlns:ui="clr-namespace:Content.Client.Materials.UI"
+                     Title="{Loc 'ore-silo-ui-title'}"
+                     MinSize="400 260"
+                     SetSize="400 460">
+    <BoxContainer Orientation="Vertical"
+                  HorizontalExpand="True"
+                  VerticalExpand="True"
+                  Margin="10 10 10 10">
+        <BoxContainer VerticalExpand="True"
+                      HorizontalExpand="True"
+                      Orientation="Vertical"
+                      SizeFlagsStretchRatio="3">
+            <Label Text="{Loc 'ore-silo-ui-label-clients'}" Margin="5 5 5 5" HorizontalAlignment="Center" StyleClasses="LabelKeyText"/>
+            <PanelContainer VerticalExpand="True">
+                <PanelContainer.PanelOverride>
+                    <graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
+                </PanelContainer.PanelOverride>
+                <ItemList Name="ClientList" SelectMode="Button" VerticalExpand="True"/>
+            </PanelContainer>
+        </BoxContainer>
+        <BoxContainer VerticalExpand="True"
+                      HorizontalExpand="True"
+                      Orientation="Vertical"
+                      SizeFlagsStretchRatio="2">
+            <Label Text="{Loc 'ore-silo-ui-label-mats'}" Margin="5 5 5 5" HorizontalAlignment="Center" StyleClasses="LabelKeyText"/>
+            <PanelContainer VerticalExpand="True">
+                <PanelContainer.PanelOverride>
+                    <graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
+                </PanelContainer.PanelOverride>
+                <BoxContainer
+                    Orientation="Vertical"
+                    VerticalExpand="True"
+                    HorizontalExpand="True">
+                    <ui:MaterialStorageControl Name="Materials"/>
+                </BoxContainer>
+            </PanelContainer>
+        </BoxContainer>
+    </BoxContainer>
+</controls:FancyWindow>
diff --git a/Content.Client/Materials/UI/OreSiloMenu.xaml.cs b/Content.Client/Materials/UI/OreSiloMenu.xaml.cs
new file mode 100644 (file)
index 0000000..69d1fa3
--- /dev/null
@@ -0,0 +1,64 @@
+using System.Linq;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Materials.OreSilo;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Materials.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class OreSiloMenu : FancyWindow
+{
+    public event Action<NetEntity>? OnClientEntryPressed;
+
+    public OreSiloMenu()
+    {
+        RobustXamlLoader.Load(this);
+
+        ClientList.OnItemSelected += args =>
+        {
+            var item = ClientList[args.ItemIndex];
+            // a little bit of null suppression makes me feel great! :-)
+            OnClientEntryPressed?.Invoke((NetEntity) item.Metadata!);
+        };
+    }
+
+    public void SetEntity(EntityUid uid)
+    {
+        Materials.SetOwner(uid);
+    }
+
+    public void Update(OreSiloBuiState state)
+    {
+        var items = new List<ItemList.Item>();
+        var orderedClients = state.Clients.OrderBy(t => t.Item3).ThenBy(t => t.Item1.Id);
+        foreach (var (ent, _, _) in orderedClients)
+        {
+            items.Add(new ItemList.Item(ClientList)
+            {
+                Metadata = ent
+            });
+        }
+
+        ClientList.SetItems(items,
+            (item1, item2) =>
+            {
+                var ent1 = (NetEntity) item1.Metadata!;
+                var ent2 = (NetEntity) item2.Metadata!;
+                return ent1.CompareTo(ent2);
+            });
+
+        var entTextDict = state.Clients.Select(t => (t.Item1, t.Item2)).ToDictionary();
+        using var enumerator = ClientList.GetEnumerator();
+        while (enumerator.MoveNext())
+        {
+            if (enumerator.Current.Metadata is not NetEntity ent)
+                continue;
+
+            if (entTextDict.TryGetValue(ent, out var text))
+                enumerator.Current.Text = text;
+        }
+    }
+}
+
index 8c50d5fe9af524f503ae0795260e8cdcffa8ffbd..3a462dd4d5e64ee0e8b85e8045671bda6e3d3b79 100644 (file)
@@ -102,14 +102,18 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
         if (!base.TryInsertMaterialEntity(user, toInsert, receiver, storage, material, composition))
             return false;
         _audio.PlayPvs(storage.InsertingSound, receiver);
-        _popup.PopupEntity(Loc.GetString("machine-insert-item", ("user", user), ("machine", receiver),
-            ("item", toInsert)), receiver);
+        _popup.PopupEntity(Loc.GetString("machine-insert-item",
+                ("user", user),
+                ("machine", receiver),
+                ("item", toInsert)),
+            receiver);
         QueueDel(toInsert);
 
         // Logging
         TryComp<StackComponent>(toInsert, out var stack);
         var count = stack?.Count ?? 1;
-        _adminLogger.Add(LogType.Action, LogImpact.Low,
+        _adminLogger.Add(LogType.Action,
+            LogImpact.Low,
             $"{ToPrettyString(user):player} inserted {count} {ToPrettyString(toInsert):inserted} into {ToPrettyString(receiver):receiver}");
         return true;
     }
diff --git a/Content.Server/Materials/OreSiloSystem.cs b/Content.Server/Materials/OreSiloSystem.cs
new file mode 100644 (file)
index 0000000..87e6db1
--- /dev/null
@@ -0,0 +1,119 @@
+using Content.Server.Pinpointer;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Materials.OreSilo;
+using Robust.Server.GameStates;
+using Robust.Shared.Player;
+
+namespace Content.Server.Materials;
+
+/// <inheritdoc/>
+public sealed class OreSiloSystem : SharedOreSiloSystem
+{
+    [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
+    [Dependency] private readonly NavMapSystem _navMap = default!;
+    [Dependency] private readonly PvsOverrideSystem _pvsOverride = default!;
+    [Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
+
+    private const float OreSiloPreloadRangeSquared = 225f; // ~1 screen
+
+    private readonly HashSet<Entity<OreSiloClientComponent>> _clientLookup = new();
+    private readonly HashSet<(NetEntity, string, string)> _clientInformation = new();
+    private readonly HashSet<EntityUid> _silosToAdd = new();
+    private readonly HashSet<EntityUid> _silosToRemove = new();
+
+    protected override void UpdateOreSiloUi(Entity<OreSiloComponent> ent)
+    {
+        if (!_userInterface.IsUiOpen(ent.Owner, OreSiloUiKey.Key))
+            return;
+        _clientLookup.Clear();
+        _clientInformation.Clear();
+
+        var xform = Transform(ent);
+
+        // Sneakily uses override with TComponent parameter
+        _entityLookup.GetEntitiesInRange(xform.Coordinates, ent.Comp.Range, _clientLookup);
+
+        foreach (var client in _clientLookup)
+        {
+            // don't show already-linked clients.
+            if (client.Comp.Silo is not null)
+                continue;
+
+            var netEnt = GetNetEntity(client);
+            var name = Identity.Name(client, EntityManager);
+            var beacon = _navMap.GetNearestBeaconString(client.Owner, onlyName: true);
+
+            var txt = Loc.GetString("ore-silo-ui-itemlist-entry",
+                ("name", name),
+                ("beacon", beacon),
+                ("linked", ent.Comp.Clients.Contains(client)),
+                ("inRange", true));
+
+            _clientInformation.Add((netEnt, txt, beacon));
+        }
+
+        // Get all clients of this silo, including those out of range.
+        foreach (var client in ent.Comp.Clients)
+        {
+            var netEnt = GetNetEntity(client);
+            var name = Identity.Name(client, EntityManager);
+            var beacon = _navMap.GetNearestBeaconString(client, onlyName: true);
+            var inRange = CanTransmitMaterials((ent, ent), client);
+
+            var txt = Loc.GetString("ore-silo-ui-itemlist-entry",
+                ("name", name),
+                ("beacon", beacon),
+                ("linked", ent.Comp.Clients.Contains(client)),
+                ("inRange", inRange));
+
+            _clientInformation.Add((netEnt, txt, beacon));
+        }
+
+        _userInterface.SetUiState(ent.Owner, OreSiloUiKey.Key, new OreSiloBuiState(_clientInformation));
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        // Solving an annoying problem: we need to send the silo to people who are near the silo so that
+        // Things don't start wildly mispredicting. We do this as cheaply as possible via grid-based local-pos checks.
+        // Sloth okay-ed this in the interim until a better solution comes around.
+
+        var actorQuery = EntityQueryEnumerator<ActorComponent, TransformComponent>();
+        while (actorQuery.MoveNext(out _, out var actorComp, out var actorXform))
+        {
+            _silosToAdd.Clear();
+            _silosToRemove.Clear();
+
+            var clientQuery = EntityQueryEnumerator<OreSiloClientComponent, TransformComponent>();
+            while (clientQuery.MoveNext(out _, out var clientComp, out var clientXform))
+            {
+                if (clientComp.Silo == null)
+                    continue;
+
+                // We limit it to same-grid checks only for peak perf
+                if (actorXform.GridUid != clientXform.GridUid)
+                    continue;
+
+                if ((actorXform.LocalPosition - clientXform.LocalPosition).LengthSquared() <= OreSiloPreloadRangeSquared)
+                {
+                    _silosToAdd.Add(clientComp.Silo.Value);
+                }
+                else
+                {
+                    _silosToRemove.Add(clientComp.Silo.Value);
+                }
+            }
+
+            foreach (var toRemove in _silosToRemove)
+            {
+                _pvsOverride.RemoveSessionOverride(toRemove, actorComp.PlayerSession);
+            }
+            foreach (var toAdd in _silosToAdd)
+            {
+                _pvsOverride.AddSessionOverride(toAdd, actorComp.PlayerSession);
+            }
+        }
+    }
+}
index da9ab20a39abc7315beb6ddebf69859cc1cdca7f..3ab0e8eff1a42d3aea6143b9a78367015d7e6d5d 100644 (file)
@@ -436,12 +436,12 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
     /// to the position of <paramref name="ent"/> from the nearest beacon.
     /// </summary>
     [PublicAPI]
-    public string GetNearestBeaconString(Entity<TransformComponent?> ent)
+    public string GetNearestBeaconString(Entity<TransformComponent?> ent, bool onlyName = false)
     {
         if (!Resolve(ent, ref ent.Comp))
             return Loc.GetString("nav-beacon-pos-no-beacons");
 
-        return GetNearestBeaconString(_transformSystem.GetMapCoordinates(ent, ent.Comp));
+        return GetNearestBeaconString(_transformSystem.GetMapCoordinates(ent, ent.Comp), onlyName);
     }
 
     /// <summary>
@@ -449,11 +449,14 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
     /// to <paramref name="coordinates"/> from the nearest beacon.
     /// </summary>
 
-    public string GetNearestBeaconString(MapCoordinates coordinates)
+    public string GetNearestBeaconString(MapCoordinates coordinates, bool onlyName = false)
     {
         if (!TryGetNearestBeacon(coordinates, out var beacon, out var pos))
             return Loc.GetString("nav-beacon-pos-no-beacons");
 
+        if (onlyName)
+            return beacon.Value.Comp.Text!;
+
         var gridOffset = Angle.Zero;
         if (_mapManager.TryFindGridAt(pos.Value, out var grid, out _))
             gridOffset = Transform(grid).LocalRotation;
index 7d8dd5c7495f17535efd0c4812100af7d7d56aec..1911b3de6f081086e01f03f61e2b45c8fd873e1b 100644 (file)
@@ -75,6 +75,24 @@ public enum MaterialStorageVisuals : byte
     Inserting
 }
 
+/// <summary>
+/// Collects all the materials stored on a <see cref="MaterialStorageComponent"/>
+/// </summary>
+/// <param name="Entity">The entity holding all these materials</param>
+/// <param name="Materials">A dictionary of all materials held</param>
+/// <param name="LocalOnly">An optional specifier. Non-local sources (silo, etc.) should not add materials when this is false.</param>
+[ByRefEvent]
+public readonly record struct GetStoredMaterialsEvent(Entity<MaterialStorageComponent> Entity, Dictionary<ProtoId<MaterialPrototype>, int> Materials, bool LocalOnly);
+
+/// <summary>
+/// After using materials, removes them from storage.
+/// </summary>
+/// <param name="Entity">The entity that held the materials and is being used up</param>
+/// <param name="Materials">A dictionary of the difference of materials left.</param>
+/// <param name="LocalOnly">An optional specifier. Non-local sources (silo, etc.) should not consume materials when this is false.</param>
+[ByRefEvent]
+public readonly record struct ConsumeStoredMaterialsEvent(Entity<MaterialStorageComponent> Entity, Dictionary<ProtoId<MaterialPrototype>, int> Materials, bool LocalOnly);
+
 /// <summary>
 /// event raised on the materialStorage when a material entity is inserted into it.
 /// </summary>
diff --git a/Content.Shared/Materials/OreSilo/OreSiloClientComponent.cs b/Content.Shared/Materials/OreSilo/OreSiloClientComponent.cs
new file mode 100644 (file)
index 0000000..ff43c9c
--- /dev/null
@@ -0,0 +1,18 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Materials.OreSilo;
+
+/// <summary>
+/// An entity with <see cref="MaterialStorageComponent"/> that interfaces with an <see cref="OreSiloComponent"/>.
+/// Used for tracking the connected silo.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedOreSiloSystem))]
+public sealed partial class OreSiloClientComponent : Component
+{
+    /// <summary>
+    /// The silo that this client pulls materials from.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public EntityUid? Silo;
+}
diff --git a/Content.Shared/Materials/OreSilo/OreSiloComponent.cs b/Content.Shared/Materials/OreSilo/OreSiloComponent.cs
new file mode 100644 (file)
index 0000000..45c86b1
--- /dev/null
@@ -0,0 +1,55 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Materials.OreSilo;
+
+/// <summary>
+/// Provides additional materials to linked clients across long distances.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedOreSiloSystem))]
+public sealed partial class OreSiloComponent : Component
+{
+    /// <summary>
+    /// The <see cref="OreSiloClientComponent"/> that are connected to this silo.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public HashSet<EntityUid> Clients = new();
+
+    /// <summary>
+    /// The maximum distance you can be to the silo and still receive transmission.
+    /// </summary>
+    /// <remarks>
+    /// Default value should be big enough to span a single large department.
+    /// </remarks>
+    [DataField, AutoNetworkedField]
+    public float Range = 20f;
+}
+
+[Serializable, NetSerializable]
+public sealed class OreSiloBuiState : BoundUserInterfaceState
+{
+    public readonly HashSet<(NetEntity, string, string)> Clients;
+
+    public OreSiloBuiState(HashSet<(NetEntity, string, string)> clients)
+    {
+        Clients = clients;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class ToggleOreSiloClientMessage : BoundUserInterfaceMessage
+{
+    public readonly NetEntity Client;
+
+    public ToggleOreSiloClientMessage(NetEntity client)
+    {
+        Client = client;
+    }
+}
+
+[Serializable, NetSerializable]
+public enum OreSiloUiKey : byte
+{
+    Key
+}
diff --git a/Content.Shared/Materials/OreSilo/SharedOreSiloSystem.cs b/Content.Shared/Materials/OreSilo/SharedOreSiloSystem.cs
new file mode 100644 (file)
index 0000000..729a710
--- /dev/null
@@ -0,0 +1,168 @@
+using Content.Shared.Power.EntitySystems;
+using JetBrains.Annotations;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Materials.OreSilo;
+
+public abstract class SharedOreSiloSystem : EntitySystem
+{
+    [Dependency] private readonly SharedMaterialStorageSystem _materialStorage = default!;
+    [Dependency] private readonly SharedPowerReceiverSystem _powerReceiver = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+    private EntityQuery<OreSiloClientComponent> _clientQuery;
+
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<OreSiloComponent, ToggleOreSiloClientMessage>(OnToggleOreSiloClient);
+        SubscribeLocalEvent<OreSiloComponent, ComponentShutdown>(OnSiloShutdown);
+        Subs.BuiEvents<OreSiloComponent>(OreSiloUiKey.Key,
+            subs =>
+        {
+            subs.Event<BoundUIOpenedEvent>(OnBoundUIOpened);
+        });
+
+
+        SubscribeLocalEvent<OreSiloClientComponent, GetStoredMaterialsEvent>(OnGetStoredMaterials);
+        SubscribeLocalEvent<OreSiloClientComponent, ConsumeStoredMaterialsEvent>(OnConsumeStoredMaterials);
+        SubscribeLocalEvent<OreSiloClientComponent, ComponentShutdown>(OnClientShutdown);
+
+        _clientQuery = GetEntityQuery<OreSiloClientComponent>();
+    }
+
+    private void OnToggleOreSiloClient(Entity<OreSiloComponent> ent, ref ToggleOreSiloClientMessage args)
+    {
+        var client = GetEntity(args.Client);
+
+        if (!_clientQuery.TryComp(client, out var clientComp))
+            return;
+
+        if (ent.Comp.Clients.Contains(client)) // remove client
+        {
+            clientComp.Silo = null;
+            Dirty(client, clientComp);
+            ent.Comp.Clients.Remove(client);
+            Dirty(ent);
+
+            UpdateOreSiloUi(ent);
+        }
+        else // add client
+        {
+            if (!CanTransmitMaterials((ent, ent), client))
+                return;
+
+            var clientMats = _materialStorage.GetStoredMaterials(client, true);
+            var inverseMats = new Dictionary<string, int>();
+            foreach (var (mat, amount) in clientMats)
+            {
+                inverseMats.Add(mat, -amount);
+            }
+            _materialStorage.TryChangeMaterialAmount(client, inverseMats, localOnly: true);
+            _materialStorage.TryChangeMaterialAmount(ent.Owner, clientMats);
+
+            ent.Comp.Clients.Add(client);
+            Dirty(ent);
+            clientComp.Silo = ent;
+            Dirty(client, clientComp);
+
+            UpdateOreSiloUi(ent);
+        }
+    }
+
+    private void OnBoundUIOpened(Entity<OreSiloComponent> ent, ref BoundUIOpenedEvent args)
+    {
+        UpdateOreSiloUi(ent);
+    }
+
+    private void OnSiloShutdown(Entity<OreSiloComponent> ent, ref ComponentShutdown args)
+    {
+        foreach (var client in ent.Comp.Clients)
+        {
+            if (!_clientQuery.TryComp(client, out var comp))
+                continue;
+
+            comp.Silo = null;
+            Dirty(client, comp);
+        }
+    }
+
+    protected virtual void UpdateOreSiloUi(Entity<OreSiloComponent> ent)
+    {
+
+    }
+
+    private void OnGetStoredMaterials(Entity<OreSiloClientComponent> ent, ref GetStoredMaterialsEvent args)
+    {
+        if (args.LocalOnly)
+            return;
+
+        if (ent.Comp.Silo is not { } silo)
+            return;
+
+        if (!CanTransmitMaterials(silo, ent))
+            return;
+
+        var materials = _materialStorage.GetStoredMaterials(silo);
+
+        foreach (var (mat, amount) in materials)
+        {
+            // Don't supply materials that they don't usually have access to.
+            if (!_materialStorage.IsMaterialWhitelisted((args.Entity, args.Entity), mat))
+                continue;
+
+            var existing = args.Materials.GetOrNew(mat);
+            args.Materials[mat] = existing + amount;
+        }
+    }
+
+    private void OnConsumeStoredMaterials(Entity<OreSiloClientComponent> ent, ref ConsumeStoredMaterialsEvent args)
+    {
+        if (args.LocalOnly)
+            return;
+
+        if (ent.Comp.Silo is not { } silo || !TryComp<MaterialStorageComponent>(silo, out var materialStorage))
+            return;
+
+        if (!CanTransmitMaterials(silo, ent))
+            return;
+
+        foreach (var (mat, amount) in args.Materials)
+        {
+            if (!_materialStorage.TryChangeMaterialAmount(silo, mat, amount, materialStorage))
+                continue;
+            args.Materials[mat] = 0;
+        }
+    }
+
+    private void OnClientShutdown(Entity<OreSiloClientComponent> ent, ref ComponentShutdown args)
+    {
+        if (!TryComp<OreSiloComponent>(ent.Comp.Silo, out var silo))
+            return;
+
+        silo.Clients.Remove(ent);
+        Dirty(ent.Comp.Silo.Value, silo);
+        UpdateOreSiloUi((ent.Comp.Silo.Value, silo));
+    }
+
+    /// <summary>
+    /// Checks if a given client fulfills the criteria to link/receive materials from an ore silo.
+    /// </summary>
+    [PublicAPI]
+    public bool CanTransmitMaterials(Entity<OreSiloComponent?> silo, EntityUid client)
+    {
+        if (!Resolve(silo, ref silo.Comp))
+            return false;
+
+        if (!_powerReceiver.IsPowered(silo.Owner))
+            return false;
+
+        if (_transform.GetGrid(client) != _transform.GetGrid(silo.Owner))
+            return false;
+
+        if (!_transform.InRange(silo.Owner, client, silo.Comp.Range))
+            return false;
+
+        return true;
+    }
+}
index 2544aacadd957167206373e129150a552c935ab6..2fc03dd997e1faaf2ed25e53eb01e314fa88cb47 100644 (file)
@@ -1,7 +1,6 @@
 using System.Linq;
 using Content.Shared.Interaction;
 using Content.Shared.Interaction.Components;
-using Content.Shared.Mobs;
 using Content.Shared.Stacks;
 using Content.Shared.Whitelist;
 using JetBrains.Annotations;
@@ -57,17 +56,37 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
         _appearance.SetData(uid, MaterialStorageVisuals.Inserting, false);
     }
 
+    /// <summary>
+    /// Gets all the materials stored on this entity
+    /// </summary>
+    /// <param name="ent"></param>
+    /// <param name="localOnly">Include only materials held "locally", as determined by event subscribers</param>
+    /// <returns></returns>
+    public Dictionary<ProtoId<MaterialPrototype>, int> GetStoredMaterials(Entity<MaterialStorageComponent?> ent, bool localOnly = false)
+    {
+        if (!Resolve(ent, ref ent.Comp, false))
+            return new();
+
+        // clone so we don't modify by accident.
+        var mats = new Dictionary<ProtoId<MaterialPrototype>, int>(ent.Comp.Storage);
+        var ev = new GetStoredMaterialsEvent((ent, ent.Comp), mats, localOnly);
+        RaiseLocalEvent(ent, ref ev, true);
+
+        return ev.Materials;
+    }
+
     /// <summary>
     /// Gets the volume of a specified material contained in this storage.
     /// </summary>
     /// <param name="uid"></param>
     /// <param name="material"></param>
     /// <param name="component"></param>
+    /// <param name="localOnly"></param>
     /// <returns>The volume of the material</returns>
     [PublicAPI]
-    public int GetMaterialAmount(EntityUid uid, MaterialPrototype material, MaterialStorageComponent? component = null)
+    public int GetMaterialAmount(EntityUid uid, MaterialPrototype material, MaterialStorageComponent? component = null, bool localOnly = false)
     {
-        return GetMaterialAmount(uid, material.ID, component);
+        return GetMaterialAmount(uid, material.ID, component, localOnly);
     }
 
     /// <summary>
@@ -76,12 +95,13 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
     /// <param name="uid"></param>
     /// <param name="material"></param>
     /// <param name="component"></param>
+    /// <param name="localOnly"></param>
     /// <returns>The volume of the material</returns>
-    public int GetMaterialAmount(EntityUid uid, string material, MaterialStorageComponent? component = null)
+    public int GetMaterialAmount(EntityUid uid, string material, MaterialStorageComponent? component = null, bool localOnly = false)
     {
         if (!Resolve(uid, ref component))
             return 0; //you have nothing
-        return component.Storage.GetValueOrDefault(material, 0);
+        return GetStoredMaterials((uid, component), localOnly).GetValueOrDefault(material, 0);
     }
 
     /// <summary>
@@ -89,26 +109,43 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
     /// </summary>
     /// <param name="uid"></param>
     /// <param name="component"></param>
+    /// <param name="localOnly"></param>
     /// <returns>The volume of all materials in the storage</returns>
-    public int GetTotalMaterialAmount(EntityUid uid, MaterialStorageComponent? component = null)
+    public int GetTotalMaterialAmount(EntityUid uid, MaterialStorageComponent? component = null, bool localOnly = false)
     {
         if (!Resolve(uid, ref component))
             return 0;
-        return component.Storage.Values.Sum();
+        return GetStoredMaterials((uid, component), localOnly).Values.Sum();
     }
 
+    // TODO: Revisit this if we ever decide to do things with storage limits. As it stands, the feature is unused.
     /// <summary>
     /// Tests if a specific amount of volume will fit in the storage.
     /// </summary>
     /// <param name="uid"></param>
     /// <param name="volume"></param>
     /// <param name="component"></param>
+    /// <param name="localOnly"></param>
     /// <returns>If the specified volume will fit</returns>
-    public bool CanTakeVolume(EntityUid uid, int volume, MaterialStorageComponent? component = null)
+    public bool CanTakeVolume(EntityUid uid, int volume, MaterialStorageComponent? component = null, bool localOnly = false)
     {
         if (!Resolve(uid, ref component))
             return false;
-        return component.StorageLimit == null || GetTotalMaterialAmount(uid, component) + volume <= component.StorageLimit;
+        return component.StorageLimit == null || GetTotalMaterialAmount(uid, component, true) + volume <= component.StorageLimit;
+    }
+
+    /// <summary>
+    /// Checks if a certain material prototype is supported by this entity.
+    /// </summary>
+    public bool IsMaterialWhitelisted(Entity<MaterialStorageComponent?> ent, ProtoId<MaterialPrototype> material)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return false;
+
+        if (ent.Comp.MaterialWhiteList == null)
+            return true;
+
+        return ent.Comp.MaterialWhiteList.Contains(material);
     }
 
     /// <summary>
@@ -118,8 +155,9 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
     /// <param name="materialId"></param>
     /// <param name="volume"></param>
     /// <param name="component"></param>
+    /// <param name="localOnly"></param>
     /// <returns>If the amount can be changed</returns>
-    public bool CanChangeMaterialAmount(EntityUid uid, string materialId, int volume, MaterialStorageComponent? component = null)
+    public bool CanChangeMaterialAmount(EntityUid uid, string materialId, int volume, MaterialStorageComponent? component = null, bool localOnly = false)
     {
         if (!Resolve(uid, ref component))
             return false;
@@ -127,10 +165,10 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
         if (!CanTakeVolume(uid, volume, component))
             return false;
 
-        if (component.MaterialWhiteList == null ? false : !component.MaterialWhiteList.Contains(materialId))
+        if (!IsMaterialWhitelisted((uid, component), materialId))
             return false;
 
-        var amount = component.Storage.GetValueOrDefault(materialId);
+        var amount = GetMaterialAmount(uid, materialId, component, localOnly);
         return amount + volume >= 0;
     }
 
@@ -140,14 +178,24 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
     /// <param name="entity"></param>
     /// <param name="materials"></param>
     /// <returns>If the amount can be changed</returns>
-    public bool CanChangeMaterialAmount(Entity<MaterialStorageComponent?> entity, Dictionary<string,int> materials)
+    /// <param name="localOnly"></param>
+    public bool CanChangeMaterialAmount(Entity<MaterialStorageComponent?> entity, Dictionary<string,int> materials, bool localOnly = false)
     {
         if (!Resolve(entity, ref entity.Comp))
             return false;
 
+        var inVolume = materials.Values.Sum();
+        var stored = GetStoredMaterials((entity, entity.Comp), localOnly);
+
+        if (!CanTakeVolume(entity, inVolume, entity.Comp))
+            return false;
+
         foreach (var (material, amount) in materials)
         {
-            if (!CanChangeMaterialAmount(entity, material, amount, entity.Comp))
+            if (!IsMaterialWhitelisted(entity, material))
+                return false;
+
+            if (stored.GetValueOrDefault(material) + amount < 0)
                 return false;
         }
 
@@ -163,16 +211,27 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
     /// <param name="volume"></param>
     /// <param name="component"></param>
     /// <param name="dirty"></param>
+    /// <param name="localOnly"></param>
     /// <returns>If it was successful</returns>
-    public bool TryChangeMaterialAmount(EntityUid uid, string materialId, int volume, MaterialStorageComponent? component = null, bool dirty = true)
+    public bool TryChangeMaterialAmount(EntityUid uid, string materialId, int volume, MaterialStorageComponent? component = null, bool dirty = true, bool localOnly = false)
     {
         if (!Resolve(uid, ref component))
             return false;
-        if (!CanChangeMaterialAmount(uid, materialId, volume, component))
+
+        if (!CanChangeMaterialAmount(uid, materialId, volume, component, localOnly))
             return false;
 
+        var changeEv = new ConsumeStoredMaterialsEvent((uid, component), new() {{materialId, volume}}, localOnly);
+        RaiseLocalEvent(uid, ref changeEv);
+        var remaining = changeEv.Materials.Values.First();
+
         var existing = component.Storage.GetOrNew(materialId);
-        existing += volume;
+
+        var localUpperLimit = component.StorageLimit == null ? int.MaxValue : component.StorageLimit.Value - existing;
+        var localLowerLimit = -existing;
+        var localChange = Math.Clamp(remaining, localLowerLimit, localUpperLimit);
+
+        existing += localChange;
 
         if (existing == 0)
             component.Storage.Remove(materialId);
@@ -191,23 +250,54 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
     /// Changes the amount of a specific material in the storage.
     /// Still respects the filters in place.
     /// </summary>
-    /// <param name="entity"></param>
-    /// <param name="materials"></param>
     /// <returns>If the amount can be changed</returns>
-    public bool TryChangeMaterialAmount(Entity<MaterialStorageComponent?> entity, Dictionary<string,int> materials)
+    public bool TryChangeMaterialAmount(Entity<MaterialStorageComponent?> entity, Dictionary<string, int> materials, bool localOnly = false)
     {
-        if (!Resolve(entity, ref entity.Comp))
-            return false;
+        return TryChangeMaterialAmount(entity, materials.Select(p => (new ProtoId<MaterialPrototype>(p.Key), p.Value)).ToDictionary(), localOnly);
+    }
 
-        if (!CanChangeMaterialAmount(entity, materials))
+    /// <summary>
+    /// Changes the amount of a specific material in the storage.
+    /// Still respects the filters in place.
+    /// </summary>
+    /// <returns>If the amount can be changed</returns>
+    public bool TryChangeMaterialAmount(
+        Entity<MaterialStorageComponent?> entity,
+        Dictionary<ProtoId<MaterialPrototype>, int> materials,
+        bool localOnly = false)
+    {
+        if (!Resolve(entity, ref entity.Comp))
             return false;
 
         foreach (var (material, amount) in materials)
         {
-            if (!TryChangeMaterialAmount(entity, material, amount, entity.Comp, false))
+            if (!CanChangeMaterialAmount(entity, material, amount, entity))
                 return false;
         }
 
+        var changeEv = new ConsumeStoredMaterialsEvent((entity, entity.Comp), materials, localOnly);
+        RaiseLocalEvent(entity, ref changeEv);
+
+        foreach (var (material, remaining) in changeEv.Materials)
+        {
+            var existing = entity.Comp.Storage.GetOrNew(material);
+
+            var localUpperLimit = entity.Comp.StorageLimit == null ? int.MaxValue : entity.Comp.StorageLimit.Value - existing;
+            var localLowerLimit = -existing;
+            var localChange = Math.Clamp(remaining, localLowerLimit, localUpperLimit);
+
+            existing += localChange;
+
+            if (existing == 0)
+                entity.Comp.Storage.Remove(material);
+            else
+                entity.Comp.Storage[material] = existing;
+
+        }
+
+        var ev = new MaterialAmountChangedEvent();
+        RaiseLocalEvent(entity, ref ev);
+
         Dirty(entity, entity.Comp);
         return true;
     }
@@ -221,6 +311,7 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
     /// <param name="volume">The stored material volume to set the storage to.</param>
     /// <param name="component">The storage component on <paramref name="uid"/>. Resolved automatically if not given.</param>
     /// <returns>True if it was successful (enough space etc).</returns>
+    [PublicAPI]
     public bool TrySetMaterialAmount(
         EntityUid uid,
         string materialId,
@@ -268,7 +359,7 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
             totalVolume += vol * multiplier;
         }
 
-        if (!CanTakeVolume(receiver, totalVolume, storage))
+        if (!CanTakeVolume(receiver, totalVolume, storage, localOnly: true))
             return false;
 
         foreach (var (mat, vol) in composition.MaterialComposition)
index 907baf4ffc29501d061306923fa15053764a5f00..076a70447cbf131093f9199960a5132bb9169958 100644 (file)
@@ -25,6 +25,7 @@ lathe-menu-material-amount-missing = { $amount ->
     *[other] {NATURALFIXED($amount, 2)} {MAKEPLURAL($unit)} of {$material} ([color=red]{NATURALFIXED($missingAmount, 2)} {MAKEPLURAL($unit)} missing[/color])
 }
 lathe-menu-no-materials-message = No materials loaded.
+lathe-menu-silo-linked-message = Silo Linked
 lathe-menu-fabricating-message = Fabricating...
 lathe-menu-materials-title = Materials
 lathe-menu-queue-title = Build Queue
diff --git a/Resources/Locale/en-US/materials/silo.ftl b/Resources/Locale/en-US/materials/silo.ftl
new file mode 100644 (file)
index 0000000..32d7de8
--- /dev/null
@@ -0,0 +1,10 @@
+ore-silo-ui-title = Material Silo
+ore-silo-ui-label-clients = Machines
+ore-silo-ui-label-mats = Materials
+ore-silo-ui-itemlist-entry = {$linked ->
+    [true] {"[Linked] "}
+    *[False] {""}
+} {$name} ({$beacon}) {$inRange ->
+    [true] {""}
+    *[false] (Out of Range)
+}
index 097a11185bea97a2d8ac8dc74cdcfc5be190702e..35830900a8757e97133d5d8868042cbeaf3da0cf 100644 (file)
   category: cargoproduct-category-name-materials
   group: market
 
+- type: cargoProduct
+  id: MaterialSilo
+  icon:
+    sprite: Structures/Machines/silo.rsi
+    state: silo
+  product: CrateMaterialSilo
+  cost: 5000
+  category: cargoproduct-category-name-materials
+  group: market
+
 - type: cargoProduct
   id: MaterialFuelTank
   icon:
index f6eb9a17af7e3521ada8f2fb12297ce597b8aaf7..80c6311f1321c36cc517100ff44cc2459e76667a 100644 (file)
           # for some reason, the selector here adds 1 to whatever value it generates,
           # so this is actually 2-4
 
+- type: entity
+  id: CrateMaterialSilo
+  parent: CrateGenericSteel
+  name: material silo crate
+  description: A package including all the materials to create a material silo.
+  components:
+  - type: StorageFill
+    contents:
+    - id: MaterialSiloMachineCircuitboard
+    - id: SheetSteel1
+      amount: 5
+    - id: MatterBinStockPart
+      amount: 4
+    - id: CableApcStack1
+      amount: 2
+
 - type: entity
   id: CrateMaterialBasicResource
   parent: CrateGenericSteel
index 812b495ee96c51263def81d7cdff6a9a127b5193..48af55da3fa801b53ac3685300b9355fbdda510b 100644 (file)
       Manipulator: 1
       Steel: 1
 
+- type: entity
+  id: MaterialSiloMachineCircuitboard
+  parent: BaseMachineCircuitboard
+  name: material silo machine board
+  components:
+  - type: Sprite
+    state: supply
+  - type: MachineBoard
+    prototype: MachineMaterialSilo
+    stackRequirements:
+      MatterBin: 4
+      Cable: 1
+
 - type: entity
   id: OreProcessorMachineCircuitboard
   parent: BaseMachineCircuitboard
index 1761f176da79264ed4f3e0186b1f4ded7d61c239..6ef8f7262f648f752b4f6313ffb2577157522a99 100644 (file)
         - Sheet
     materialWhiteList:
     - Plasma
+  - type: OreSiloClient
   - type: Fixtures
     fixtures:
       fix1:
index 78f150400306ac45951eb9631691325275f4da5b..f7f1bcba1b1c337e5a79091df820aa047bbb8e8d 100644 (file)
@@ -44,6 +44,7 @@
       - Sheet
       - RawMaterial
       - Ingot
+  - type: OreSiloClient
   - type: AmbientSound
     enabled: false
     volume: 5
index 0d008ed1e175bb3b8f9294585c8982bca6c9f66f..3b4b9cf5d2f64580aa48f94cbd90ed781aad7e5c 100644 (file)
         - Sheet
         - RawMaterial
         - Ingot
+  - type: OreSiloClient
   - type: Lathe
     idleState: icon
     runningState: building
         - Sheet
         - RawMaterial
         - Ingot
+  - type: OreSiloClient
   - type: Lathe
     idleState: icon
     runningState: building
         - Sheet
         - RawMaterial
         - Ingot
+  - type: OreSiloClient
   - type: RequireProjectileTarget
 
 - type: entity
       - Sheet
       - RawMaterial
       - Ingot
+  - type: OreSiloClient
   - type: GuideHelp
     guides:
     - Robotics
         - Sheet
         - RawMaterial
         - Ingot
+  - type: OreSiloClient
   - type: LatheAnnouncing
     channels: [Security]
 
           - Sheet
           - RawMaterial
           - Ingot
+    - type: OreSiloClient
 
 - type: entity
   id: MedicalTechFab
     board: MedicalTechFabCircuitboard
   - type: StealTarget
     stealGroup: MedicalTechFabCircuitboard
+  - type: OreSiloClient
   - type: LatheAnnouncing
     channels: [Medical]
 
diff --git a/Resources/Prototypes/Entities/Structures/Machines/silo.yml b/Resources/Prototypes/Entities/Structures/Machines/silo.yml
new file mode 100644 (file)
index 0000000..31bfe42
--- /dev/null
@@ -0,0 +1,61 @@
+- type: entity
+  id: MachineMaterialSilo
+  parent: [ BaseMachinePowered, ConstructibleMachine ]
+  name: material silo
+  description: An advanced machine, capable of using bluespace technology to transmit materials to nearby machines.
+  components:
+  - type: Sprite
+    sprite: Structures/Machines/silo.rsi
+    layers:
+    - state: silo
+      map: [ "base" ]
+  - type: Appearance
+  - type: GenericVisualizer
+    visuals:
+      enum.PowerDeviceVisuals.Powered:
+        base:
+          True: { state: silo_active }
+          False: { state: silo }
+  - type: OreSilo
+  - type: MaterialStorage
+    whitelist:
+      tags:
+      - Sheet
+      - Ingot
+  - type: ActivatableUI
+    key: enum.OreSiloUiKey.Key
+  - type: ActivatableUIRequiresPower
+  - type: UserInterface
+    interfaces:
+      enum.OreSiloUiKey.Key:
+        type: OreSiloBoundUserInterface
+  - type: Machine
+    board: MaterialSiloMachineCircuitboard
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.4,-0.4,0.4,0.4"
+        density: 190
+        mask:
+        - MachineMask
+        layer:
+        - MachineLayer
+  - type: Destructible
+    thresholds:
+    - trigger:
+        !type:DamageTrigger
+        damage: 300
+      behaviors:
+      - !type:PlaySoundBehavior
+        sound:
+          collection: MetalBreak
+      - !type:ChangeConstructionNodeBehavior
+        node: machineFrame
+      - !type:DoActsBehavior
+        acts: ["Destruction"]
+  - type: WiresVisuals
+  - type: WiresPanel
+  - type: StaticPrice
+    price: 1500
diff --git a/Resources/Textures/Structures/Machines/silo.rsi/meta.json b/Resources/Textures/Structures/Machines/silo.rsi/meta.json
new file mode 100644 (file)
index 0000000..aa78616
--- /dev/null
@@ -0,0 +1,40 @@
+{
+    "version": 1,
+    "license": "CC-BY-SA-3.0",
+    "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/d74b67828394a9842578279a6b8ab2955bb08216. Created by MrDoomBringer (github)",
+    "size": {
+        "x": 32,
+        "y": 32
+    },
+    "states": [
+        {
+            "name": "silo"
+        },
+        {
+            "name": "silo_active",
+            "delays": [
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        },
+        {
+            "name": "overlay_active",
+            "delays": [
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        }
+    ]
+}
diff --git a/Resources/Textures/Structures/Machines/silo.rsi/overlay_active.png b/Resources/Textures/Structures/Machines/silo.rsi/overlay_active.png
new file mode 100644 (file)
index 0000000..0a06e45
Binary files /dev/null and b/Resources/Textures/Structures/Machines/silo.rsi/overlay_active.png differ
diff --git a/Resources/Textures/Structures/Machines/silo.rsi/silo.png b/Resources/Textures/Structures/Machines/silo.rsi/silo.png
new file mode 100644 (file)
index 0000000..45fe37a
Binary files /dev/null and b/Resources/Textures/Structures/Machines/silo.rsi/silo.png differ
diff --git a/Resources/Textures/Structures/Machines/silo.rsi/silo_active.png b/Resources/Textures/Structures/Machines/silo.rsi/silo_active.png
new file mode 100644 (file)
index 0000000..f25218b
Binary files /dev/null and b/Resources/Textures/Structures/Machines/silo.rsi/silo_active.png differ