diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml b/Content.Client/Lathe/UI/LatheMenu.xaml
index d5e3573148..28b79254c0 100644
--- a/Content.Client/Lathe/UI/LatheMenu.xaml
+++ b/Content.Client/Lathe/UI/LatheMenu.xaml
@@ -127,12 +127,17 @@
HorizontalExpand="True"
Orientation="Vertical">
-
-
-
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Materials/OreSiloSystem.cs b/Content.Client/Materials/OreSiloSystem.cs
new file mode 100644
index 0000000000..076a546d37
--- /dev/null
+++ b/Content.Client/Materials/OreSiloSystem.cs
@@ -0,0 +1,6 @@
+using Content.Shared.Materials.OreSilo;
+
+namespace Content.Client.Materials;
+
+///
+public sealed class OreSiloSystem : SharedOreSiloSystem;
diff --git a/Content.Client/Materials/UI/MaterialStorageControl.xaml b/Content.Client/Materials/UI/MaterialStorageControl.xaml
index 2be0f40aa5..d7503a61f3 100644
--- a/Content.Client/Materials/UI/MaterialStorageControl.xaml
+++ b/Content.Client/Materials/UI/MaterialStorageControl.xaml
@@ -2,7 +2,10 @@
SizeFlagsStretchRatio="8"
HorizontalExpand="True"
VerticalExpand="True">
-
-
+
+
+
+
+
diff --git a/Content.Client/Materials/UI/MaterialStorageControl.xaml.cs b/Content.Client/Materials/UI/MaterialStorageControl.xaml.cs
index 3cf1792c14..fd698d890f 100644
--- a/Content.Client/Materials/UI/MaterialStorageControl.xaml.cs
+++ b/Content.Client/Materials/UI/MaterialStorageControl.xaml.cs
@@ -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();
}
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(_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
index 0000000000..32eed56d80
--- /dev/null
+++ b/Content.Client/Materials/UI/OreSiloBoundUserInterface.cs
@@ -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();
+ _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
index 0000000000..3a8e0ef48a
--- /dev/null
+++ b/Content.Client/Materials/UI/OreSiloMenu.xaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Materials/UI/OreSiloMenu.xaml.cs b/Content.Client/Materials/UI/OreSiloMenu.xaml.cs
new file mode 100644
index 0000000000..69d1fa328e
--- /dev/null
+++ b/Content.Client/Materials/UI/OreSiloMenu.xaml.cs
@@ -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? 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();
+ 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;
+ }
+ }
+}
+
diff --git a/Content.Server/Materials/MaterialStorageSystem.cs b/Content.Server/Materials/MaterialStorageSystem.cs
index 8c50d5fe9a..3a462dd4d5 100644
--- a/Content.Server/Materials/MaterialStorageSystem.cs
+++ b/Content.Server/Materials/MaterialStorageSystem.cs
@@ -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(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
index 0000000000..87e6db16cf
--- /dev/null
+++ b/Content.Server/Materials/OreSiloSystem.cs
@@ -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;
+
+///
+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> _clientLookup = new();
+ private readonly HashSet<(NetEntity, string, string)> _clientInformation = new();
+ private readonly HashSet _silosToAdd = new();
+ private readonly HashSet _silosToRemove = new();
+
+ protected override void UpdateOreSiloUi(Entity 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();
+ while (actorQuery.MoveNext(out _, out var actorComp, out var actorXform))
+ {
+ _silosToAdd.Clear();
+ _silosToRemove.Clear();
+
+ var clientQuery = EntityQueryEnumerator();
+ 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);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Pinpointer/NavMapSystem.cs b/Content.Server/Pinpointer/NavMapSystem.cs
index da9ab20a39..3ab0e8eff1 100644
--- a/Content.Server/Pinpointer/NavMapSystem.cs
+++ b/Content.Server/Pinpointer/NavMapSystem.cs
@@ -436,12 +436,12 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
/// to the position of from the nearest beacon.
///
[PublicAPI]
- public string GetNearestBeaconString(Entity ent)
+ public string GetNearestBeaconString(Entity 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);
}
///
@@ -449,11 +449,14 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
/// to from the nearest beacon.
///
- 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;
diff --git a/Content.Shared/Materials/MaterialStorageComponent.cs b/Content.Shared/Materials/MaterialStorageComponent.cs
index 7d8dd5c749..1911b3de6f 100644
--- a/Content.Shared/Materials/MaterialStorageComponent.cs
+++ b/Content.Shared/Materials/MaterialStorageComponent.cs
@@ -75,6 +75,24 @@ public enum MaterialStorageVisuals : byte
Inserting
}
+///
+/// Collects all the materials stored on a
+///
+/// The entity holding all these materials
+/// A dictionary of all materials held
+/// An optional specifier. Non-local sources (silo, etc.) should not add materials when this is false.
+[ByRefEvent]
+public readonly record struct GetStoredMaterialsEvent(Entity Entity, Dictionary, int> Materials, bool LocalOnly);
+
+///
+/// After using materials, removes them from storage.
+///
+/// The entity that held the materials and is being used up
+/// A dictionary of the difference of materials left.
+/// An optional specifier. Non-local sources (silo, etc.) should not consume materials when this is false.
+[ByRefEvent]
+public readonly record struct ConsumeStoredMaterialsEvent(Entity Entity, Dictionary, int> Materials, bool LocalOnly);
+
///
/// event raised on the materialStorage when a material entity is inserted into it.
///
diff --git a/Content.Shared/Materials/OreSilo/OreSiloClientComponent.cs b/Content.Shared/Materials/OreSilo/OreSiloClientComponent.cs
new file mode 100644
index 0000000000..ff43c9c05a
--- /dev/null
+++ b/Content.Shared/Materials/OreSilo/OreSiloClientComponent.cs
@@ -0,0 +1,18 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Materials.OreSilo;
+
+///
+/// An entity with that interfaces with an .
+/// Used for tracking the connected silo.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedOreSiloSystem))]
+public sealed partial class OreSiloClientComponent : Component
+{
+ ///
+ /// The silo that this client pulls materials from.
+ ///
+ [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
index 0000000000..45c86b1845
--- /dev/null
+++ b/Content.Shared/Materials/OreSilo/OreSiloComponent.cs
@@ -0,0 +1,55 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Materials.OreSilo;
+
+///
+/// Provides additional materials to linked clients across long distances.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedOreSiloSystem))]
+public sealed partial class OreSiloComponent : Component
+{
+ ///
+ /// The that are connected to this silo.
+ ///
+ [DataField, AutoNetworkedField]
+ public HashSet Clients = new();
+
+ ///
+ /// The maximum distance you can be to the silo and still receive transmission.
+ ///
+ ///
+ /// Default value should be big enough to span a single large department.
+ ///
+ [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
index 0000000000..729a710065
--- /dev/null
+++ b/Content.Shared/Materials/OreSilo/SharedOreSiloSystem.cs
@@ -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 _clientQuery;
+
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnToggleOreSiloClient);
+ SubscribeLocalEvent(OnSiloShutdown);
+ Subs.BuiEvents(OreSiloUiKey.Key,
+ subs =>
+ {
+ subs.Event(OnBoundUIOpened);
+ });
+
+
+ SubscribeLocalEvent(OnGetStoredMaterials);
+ SubscribeLocalEvent(OnConsumeStoredMaterials);
+ SubscribeLocalEvent(OnClientShutdown);
+
+ _clientQuery = GetEntityQuery();
+ }
+
+ private void OnToggleOreSiloClient(Entity 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();
+ 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 ent, ref BoundUIOpenedEvent args)
+ {
+ UpdateOreSiloUi(ent);
+ }
+
+ private void OnSiloShutdown(Entity 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 ent)
+ {
+
+ }
+
+ private void OnGetStoredMaterials(Entity 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 ent, ref ConsumeStoredMaterialsEvent args)
+ {
+ if (args.LocalOnly)
+ return;
+
+ if (ent.Comp.Silo is not { } silo || !TryComp(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 ent, ref ComponentShutdown args)
+ {
+ if (!TryComp(ent.Comp.Silo, out var silo))
+ return;
+
+ silo.Clients.Remove(ent);
+ Dirty(ent.Comp.Silo.Value, silo);
+ UpdateOreSiloUi((ent.Comp.Silo.Value, silo));
+ }
+
+ ///
+ /// Checks if a given client fulfills the criteria to link/receive materials from an ore silo.
+ ///
+ [PublicAPI]
+ public bool CanTransmitMaterials(Entity 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;
+ }
+}
diff --git a/Content.Shared/Materials/SharedMaterialStorageSystem.cs b/Content.Shared/Materials/SharedMaterialStorageSystem.cs
index 2544aacadd..2fc03dd997 100644
--- a/Content.Shared/Materials/SharedMaterialStorageSystem.cs
+++ b/Content.Shared/Materials/SharedMaterialStorageSystem.cs
@@ -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;
@@ -58,16 +57,22 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
}
///
- /// Gets the volume of a specified material contained in this storage.
+ /// Gets all the materials stored on this entity
///
- ///
- ///
- ///
- /// The volume of the material
- [PublicAPI]
- public int GetMaterialAmount(EntityUid uid, MaterialPrototype material, MaterialStorageComponent? component = null)
+ ///
+ /// Include only materials held "locally", as determined by event subscribers
+ ///
+ public Dictionary, int> GetStoredMaterials(Entity ent, bool localOnly = false)
{
- return GetMaterialAmount(uid, material.ID, component);
+ if (!Resolve(ent, ref ent.Comp, false))
+ return new();
+
+ // clone so we don't modify by accident.
+ var mats = new Dictionary, int>(ent.Comp.Storage);
+ var ev = new GetStoredMaterialsEvent((ent, ent.Comp), mats, localOnly);
+ RaiseLocalEvent(ent, ref ev, true);
+
+ return ev.Materials;
}
///
@@ -76,12 +81,27 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
///
///
///
+ ///
/// The volume of the material
- public int GetMaterialAmount(EntityUid uid, string material, MaterialStorageComponent? component = null)
+ [PublicAPI]
+ public int GetMaterialAmount(EntityUid uid, MaterialPrototype material, MaterialStorageComponent? component = null, bool localOnly = false)
+ {
+ return GetMaterialAmount(uid, material.ID, component, localOnly);
+ }
+
+ ///
+ /// Gets the volume of a specified material contained in this storage.
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// The volume of the material
+ 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);
}
///
@@ -89,26 +109,43 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
///
///
///
+ ///
/// The volume of all materials in the storage
- 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.
///
/// Tests if a specific amount of volume will fit in the storage.
///
///
///
///
+ ///
/// If the specified volume will fit
- 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;
+ }
+
+ ///
+ /// Checks if a certain material prototype is supported by this entity.
+ ///
+ public bool IsMaterialWhitelisted(Entity ent, ProtoId material)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return false;
+
+ if (ent.Comp.MaterialWhiteList == null)
+ return true;
+
+ return ent.Comp.MaterialWhiteList.Contains(material);
}
///
@@ -118,8 +155,9 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
///
///
///
+ ///
/// If the amount can be changed
- 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
///
///
/// If the amount can be changed
- public bool CanChangeMaterialAmount(Entity entity, Dictionary materials)
+ ///
+ public bool CanChangeMaterialAmount(Entity entity, Dictionary 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
///
///
///
+ ///
/// If it was successful
- 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.
///
- ///
- ///
/// If the amount can be changed
- public bool TryChangeMaterialAmount(Entity entity, Dictionary materials)
+ public bool TryChangeMaterialAmount(Entity entity, Dictionary materials, bool localOnly = false)
+ {
+ return TryChangeMaterialAmount(entity, materials.Select(p => (new ProtoId(p.Key), p.Value)).ToDictionary(), localOnly);
+ }
+
+ ///
+ /// Changes the amount of a specific material in the storage.
+ /// Still respects the filters in place.
+ ///
+ /// If the amount can be changed
+ public bool TryChangeMaterialAmount(
+ Entity entity,
+ Dictionary, int> materials,
+ bool localOnly = false)
{
if (!Resolve(entity, ref entity.Comp))
return false;
- if (!CanChangeMaterialAmount(entity, materials))
- 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
/// The stored material volume to set the storage to.
/// The storage component on . Resolved automatically if not given.
/// True if it was successful (enough space etc).
+ [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)
diff --git a/Resources/Locale/en-US/lathe/ui/lathe-menu.ftl b/Resources/Locale/en-US/lathe/ui/lathe-menu.ftl
index 907baf4ffc..076a70447c 100644
--- a/Resources/Locale/en-US/lathe/ui/lathe-menu.ftl
+++ b/Resources/Locale/en-US/lathe/ui/lathe-menu.ftl
@@ -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
index 0000000000..32d7de8b2e
--- /dev/null
+++ b/Resources/Locale/en-US/materials/silo.ftl
@@ -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)
+}
diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_materials.yml b/Resources/Prototypes/Catalog/Cargo/cargo_materials.yml
index 097a11185b..35830900a8 100644
--- a/Resources/Prototypes/Catalog/Cargo/cargo_materials.yml
+++ b/Resources/Prototypes/Catalog/Cargo/cargo_materials.yml
@@ -98,6 +98,16 @@
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:
diff --git a/Resources/Prototypes/Catalog/Fills/Crates/materials.yml b/Resources/Prototypes/Catalog/Fills/Crates/materials.yml
index f6eb9a17af..80c6311f13 100644
--- a/Resources/Prototypes/Catalog/Fills/Crates/materials.yml
+++ b/Resources/Prototypes/Catalog/Fills/Crates/materials.yml
@@ -153,6 +153,22 @@
# 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
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
index 812b495ee9..48af55da3f 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
@@ -1005,6 +1005,19 @@
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
diff --git a/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml b/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml
index 1761f176da..6ef8f7262f 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml
@@ -270,6 +270,7 @@
- Sheet
materialWhiteList:
- Plasma
+ - type: OreSiloClient
- type: Fixtures
fixtures:
fix1:
diff --git a/Resources/Prototypes/Entities/Structures/Machines/flatpacker.yml b/Resources/Prototypes/Entities/Structures/Machines/flatpacker.yml
index 78f1504003..f7f1bcba1b 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/flatpacker.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/flatpacker.yml
@@ -44,6 +44,7 @@
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: AmbientSound
enabled: false
volume: 5
diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
index 0d008ed1e1..3b4b9cf5d2 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
@@ -112,6 +112,7 @@
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: Lathe
idleState: icon
runningState: building
@@ -184,6 +185,7 @@
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: Lathe
idleState: icon
runningState: building
@@ -273,6 +275,7 @@
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: RequireProjectileTarget
- type: entity
@@ -321,6 +324,7 @@
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: GuideHelp
guides:
- Robotics
@@ -408,6 +412,7 @@
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: LatheAnnouncing
channels: [Security]
@@ -443,6 +448,7 @@
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: entity
id: MedicalTechFab
@@ -480,6 +486,7 @@
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
index 0000000000..31bfe42572
--- /dev/null
+++ b/Resources/Prototypes/Entities/Structures/Machines/silo.yml
@@ -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
index 0000000000..aa78616897
--- /dev/null
+++ b/Resources/Textures/Structures/Machines/silo.rsi/meta.json
@@ -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
index 0000000000..0a06e45676
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
index 0000000000..45fe37ac5f
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
index 0000000000..f25218b2d2
Binary files /dev/null and b/Resources/Textures/Structures/Machines/silo.rsi/silo_active.png differ