diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml b/Content.Client/Lathe/UI/LatheMenu.xaml
index 5b21f0bae66..fc5a918b29e 100644
--- a/Content.Client/Lathe/UI/LatheMenu.xaml
+++ b/Content.Client/Lathe/UI/LatheMenu.xaml
@@ -24,8 +24,7 @@
-
+ HorizontalExpand="True" />
-
+ RectClipContent="True" />
@@ -78,8 +76,7 @@
Name="ServerListButton"
Text="{Loc 'lathe-menu-server-list'}"
TextAlign="Center"
- Mode="Press">
-
+ Mode="Press" />
-
+ Text="{Loc 'lathe-menu-fabricating-message'}" />
@@ -107,8 +103,7 @@
Name="NameLabel"
RectClipContent="True"
HorizontalAlignment="Left"
- Margin="130 0 0 0">
-
+ Margin="130 0 0 0" />
@@ -117,8 +112,7 @@
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
- RectClipContent="True">
-
+ RectClipContent="True" />
-
-
-
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Materials/MaterialStorageSystem.cs b/Content.Client/Materials/MaterialStorageSystem.cs
index 365fd8c1ef3..f030dfff0c9 100644
--- a/Content.Client/Materials/MaterialStorageSystem.cs
+++ b/Content.Client/Materials/MaterialStorageSystem.cs
@@ -7,6 +7,7 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
{
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly TransformSystem _transform = default!;
+ [Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
@@ -20,7 +21,7 @@ private void OnAppearanceChange(EntityUid uid, MaterialStorageComponent componen
if (args.Sprite == null)
return;
- if (!args.Sprite.LayerMapTryGet(MaterialStorageVisualLayers.Inserting, out var layer))
+ if (!_sprite.LayerMapTryGet((uid, args.Sprite), MaterialStorageVisualLayers.Inserting, out var layer, false))
return;
if (!_appearance.TryGetData(uid, MaterialStorageVisuals.Inserting, out var inserting, args.Component))
@@ -28,15 +29,15 @@ private void OnAppearanceChange(EntityUid uid, MaterialStorageComponent componen
if (inserting && TryComp(uid, out var insertingComp))
{
- args.Sprite.LayerSetAnimationTime(layer, 0f);
+ _sprite.LayerSetAnimationTime((uid, args.Sprite), layer, 0f);
- args.Sprite.LayerSetVisible(layer, true);
+ _sprite.LayerSetVisible((uid, args.Sprite), layer, true);
if (insertingComp.MaterialColor != null)
- args.Sprite.LayerSetColor(layer, insertingComp.MaterialColor.Value);
+ _sprite.LayerSetColor((uid, args.Sprite), layer, insertingComp.MaterialColor.Value);
}
else
{
- args.Sprite.LayerSetVisible(layer, false);
+ _sprite.LayerSetVisible((uid, args.Sprite), layer, false);
}
}
diff --git a/Content.Client/Materials/OreSilo/OreSiloBoundUserInterface.cs b/Content.Client/Materials/OreSilo/OreSiloBoundUserInterface.cs
new file mode 100644
index 00000000000..31cdbba2ea0
--- /dev/null
+++ b/Content.Client/Materials/OreSilo/OreSiloBoundUserInterface.cs
@@ -0,0 +1,35 @@
+using Content.Client.Materials.UI;
+using Content.Shared.Materials.OreSilo;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Materials.OreSilo;
+
+[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/OreSilo/OreSiloSystem.cs b/Content.Client/Materials/OreSilo/OreSiloSystem.cs
new file mode 100644
index 00000000000..076a546d373
--- /dev/null
+++ b/Content.Client/Materials/OreSilo/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 2be0f40aa51..d7503a61f3b 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 0237b86db76..fd698d890fc 100644
--- a/Content.Client/Materials/UI/MaterialStorageControl.xaml.cs
+++ b/Content.Client/Materials/UI/MaterialStorageControl.xaml.cs
@@ -1,8 +1,10 @@
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;
+using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Materials.UI;
@@ -14,15 +16,18 @@ namespace Content.Client.Materials.UI;
public sealed partial class MaterialStorageControl : ScrollContainer
{
[Dependency] private readonly IEntityManager _entityManager = default!;
+ private readonly MaterialStorageSystem _materialStorage;
private EntityUid? _owner;
- private Dictionary _currentMaterials = new();
+ private Dictionary, int> _currentMaterials = new();
public MaterialStorageControl()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
+
+ _materialStorage = _entityManager.System();
}
public void SetOwner(EntityUid owner)
@@ -44,7 +49,8 @@ protected override void FrameUpdate(FrameEventArgs args)
}
var canEject = materialStorage.CanEjectStoredMaterials;
- var mats = materialStorage.Storage.Select(pair => (pair.Key.Id, pair.Value)).ToDictionary();
+ var mats = _materialStorage.GetStoredMaterials((_owner.Value, materialStorage));
+
if (_currentMaterials.Equals(mats))
return;
@@ -88,5 +94,6 @@ protected override void FrameUpdate(FrameEventArgs args)
_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/OreSiloMenu.xaml b/Content.Client/Materials/UI/OreSiloMenu.xaml
new file mode 100644
index 00000000000..3a8e0ef48a8
--- /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 00000000000..785af4cb680
--- /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.IntegrationTests/Tests/MachineBoardTest.cs b/Content.IntegrationTests/Tests/MachineBoardTest.cs
index b52c2ea6a32..636fa1782fa 100644
--- a/Content.IntegrationTests/Tests/MachineBoardTest.cs
+++ b/Content.IntegrationTests/Tests/MachineBoardTest.cs
@@ -2,9 +2,12 @@
using System.Linq;
using Content.Server.Construction.Components;
using Content.Shared.Construction.Components;
+using Content.Shared.Construction.Prototypes;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
+// Frontier
+
namespace Content.IntegrationTests.Tests;
public sealed class MachineBoardTest
@@ -17,12 +20,11 @@ public sealed class MachineBoardTest
//These have their own construction thing going on here
"MachineParticleAcceleratorEndCapCircuitboard",
"MachineParticleAcceleratorFuelChamberCircuitboard",
- "MachineParticleAcceleratorFuelChamberCircuitboard",
"MachineParticleAcceleratorPowerBoxCircuitboard",
"MachineParticleAcceleratorEmitterStarboardCircuitboard",
"MachineParticleAcceleratorEmitterForeCircuitboard",
"MachineParticleAcceleratorEmitterPortCircuitboard",
- "ParticleAcceleratorComputerCircuitboard"
+ "ParticleAcceleratorComputerCircuitboard",
};
///
@@ -55,7 +57,8 @@ await server.WaitAssertion(() =>
$"Machine board {p.ID}'s corresponding machine has an invalid prototype.");
Assert.That(mProto.TryGetComponent(out var mComp, compFact),
$"Machine board {p.ID}'s corresponding machine {mId} does not have MachineComponent");
- Assert.That(mComp.Board, Is.EqualTo(p.ID),
+ Assert.That(mComp.Board,
+ Is.EqualTo(p.ID),
$"Machine {mId}'s BoardPrototype is not equal to it's corresponding machine board, {p.ID}");
});
}
@@ -95,7 +98,8 @@ await server.WaitAssertion(() =>
$"Computer board \"{p.ID}\"'s corresponding computer has an invalid prototype.");
Assert.That(cProto.TryGetComponent(out var cComp, compFact),
$"Computer board {p.ID}'s corresponding computer \"{cId}\" does not have ComputerComponent");
- Assert.That(cComp.BoardPrototype, Is.EqualTo(p.ID),
+ Assert.That(cComp.BoardPrototype,
+ Is.EqualTo(p.ID),
$"Computer \"{cId}\"'s BoardPrototype is not equal to it's corresponding computer board, \"{p.ID}\"");
});
}
@@ -131,7 +135,8 @@ await server.WaitAssertion(() =>
{
foreach (var component in board.ComponentRequirements.Keys)
{
- Assert.That(entMan.ComponentFactory.TryGetRegistration(component, out _), $"Invalid component requirement {component} specified on machine board entity {p}");
+ Assert.That(entMan.ComponentFactory.TryGetRegistration(component, out _),
+ $"Invalid component requirement {component} specified on machine board entity {p}");
}
});
}
@@ -139,4 +144,111 @@ await server.WaitAssertion(() =>
await pair.CleanReturnAsync();
}
+
+ // Frontier: machine part tests
+ ///
+ /// Invalid stack types for MachineBoard components, should be listed as requirements.
+ ///
+ private readonly HashSet _invalidStackTypes = new()
+ {
+ };
+
+ ///
+ /// Invalid tags for MachineBoard components, should be listed as requirements.
+ ///
+ private readonly HashSet _invalidTags = new()
+ {
+ };
+
+ ///
+ /// Invalid components for MachineBoard components, should be listed as requirements.
+ ///
+ private readonly HashSet _invalidComponents = new()
+ {
+ "PowerCell"
+ };
+
+ ///
+ /// Check machine requirements for miscategorized machine part requirements.
+ ///
+ [Test]
+ public async Task TestValidateBoardMachinePartRequirements()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.ResolveDependency();
+ var protoMan = server.ResolveDependency();
+ var compFact = server.ResolveDependency();
+
+ await server.WaitAssertion(() =>
+ {
+ HashSet machinePartEntities = new();
+ foreach (var p in protoMan.EnumeratePrototypes()
+ .Where(p => !pair.IsTestPrototype(p))
+ .Where(p => !_ignoredPrototypes.Contains(p.ID)))
+ {
+ machinePartEntities.Add(p.StockPartPrototype);
+ }
+
+ Assert.Multiple(() =>
+ {
+ foreach (var p in protoMan.EnumeratePrototypes()
+ .Where(p => !p.Abstract)
+ .Where(p => !pair.IsTestPrototype(p))
+ .Where(p => !_ignoredPrototypes.Contains(p.ID)))
+ {
+ if (!p.TryGetComponent(out var mbc, compFact))
+ continue;
+
+ foreach (var stackReq in mbc.StackRequirements.Keys)
+ {
+ if (_invalidStackTypes.Contains(stackReq))
+ {
+ Assert.Fail(
+ $"Entity {p.ID} has a stackRequirement for {stackReq}, which should be converted into a machine part requirement.");
+ continue;
+ }
+
+ if (!protoMan.TryIndex(stackReq, out var stack))
+ {
+ Assert.Fail(
+ $"Entity {p.ID} has a stackRequirement for {stackReq}, which could not be resolved.");
+ continue;
+ }
+
+ if (machinePartEntities.Contains(stack.Spawn))
+ {
+ Assert.Fail(
+ $"Entity {p.ID} has a stackRequirement for {stackReq}, which is a machine part, and should be in requirements.");
+ continue;
+ }
+ }
+
+ foreach (var tagReq in mbc.TagRequirements.Keys)
+ {
+ if (_invalidTags.Contains(tagReq))
+ {
+ Assert.Fail(
+ $"Entity {p.ID} has a tagRequirement for {tagReq}, which should be converted into a machine part requirement.");
+ continue;
+ }
+ }
+
+ foreach (var compReq in mbc.ComponentRequirements.Keys)
+ {
+ if (_invalidComponents.Contains(compReq))
+ {
+ Assert.Fail(
+ $"Entity {p.ID} has a componentRequirement for {compReq}, which should be converted into a machine part requirement.");
+ continue;
+ }
+ }
+ }
+ });
+ });
+
+ await pair.CleanReturnAsync();
+ }
+ // End Frontier: machine part tests
}
diff --git a/Content.Server/Materials/MaterialStorageSystem.cs b/Content.Server/Materials/MaterialStorageSystem.cs
index 7f8b13b64b1..8bbc367b072 100644
--- a/Content.Server/Materials/MaterialStorageSystem.cs
+++ b/Content.Server/Materials/MaterialStorageSystem.cs
@@ -1,20 +1,22 @@
using System.Linq;
using Content.Server.Administration.Logs;
-using Content.Shared.Materials;
-using Content.Shared.Popups;
-using Content.Shared.Stacks;
-using Content.Server.Storage.Components; // Frontier
-using Content.Server.Cargo.Systems; // Frontier
+using Content.Server.Cargo.Systems;
using Content.Server.Power.Components;
using Content.Server.Stack;
+using Content.Server.Storage.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Construction;
using Content.Shared.Database;
+using Content.Shared.Materials;
+using Content.Shared.Popups;
+using Content.Shared.Stacks;
using JetBrains.Annotations;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
+// Frontier
+
namespace Content.Server.Materials;
///
@@ -54,11 +56,12 @@ private void OnPriceCalculation(EntityUid uid, MaterialStorageComponent componen
{
foreach (var (materialProto, amount) in component.Storage)
{
- if (!_prototypeManager.TryIndex(materialProto, out var material))
+ if (!_prototypeManager.TryIndex(materialProto, out var material))
{
Log.Error("Failed to index material prototype " + materialProto);
continue;
}
+
ev.Price += material.Price * amount;
}
}
@@ -80,14 +83,16 @@ private void OnEjectMessage(EjectMaterialMessage msg, EntitySessionEventArgs arg
if (!_actionBlocker.CanInteract(player, uid))
return;
- if (!component.CanEjectStoredMaterials || !_prototypeManager.TryIndex(msg.Material, out var material))
+ if (!component.CanEjectStoredMaterials ||
+ !_prototypeManager.TryIndex(msg.Material, out var material))
return;
var volume = 0;
if (material.StackEntity != null)
{
- if (!_prototypeManager.Index(material.StackEntity).TryGetComponent(out var composition, EntityManager.ComponentFactory))
+ if (!_prototypeManager.Index(material.StackEntity)
+ .TryGetComponent(out var composition, EntityManager.ComponentFactory))
return;
var volumePerSheet = composition.MaterialComposition.FirstOrDefault(kvp => kvp.Key == msg.Material).Value;
@@ -128,14 +133,18 @@ public override bool TryInsertMaterialEntity(EntityUid user,
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);
- //QueueDel(toInsert); // Frontier
+ _popup.PopupEntity(Loc.GetString("machine-insert-item",
+ ("user", user),
+ ("machine", receiver),
+ ("item", toInsert)),
+ receiver);
+ // QueueDel(toInsert); // Frontier
// 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}");
Del(toInsert); // Frontier: delete immediately, don't queue
return true;
@@ -157,15 +166,25 @@ public override bool TryInsertMaxPossibleMaterialEntity(EntityUid user,
return false;
// Cache old count
var initialCount = TryComp(toInsert, out var stack) ? stack.Count : 1;
- if (!base.TryInsertMaxPossibleMaterialEntity(user, toInsert, receiver, out empty, storage, material, composition))
+ if (!base.TryInsertMaxPossibleMaterialEntity(user,
+ toInsert,
+ receiver,
+ out empty,
+ 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);
// Logging
var newCount = stack?.Count ?? 0;
- _adminLogger.Add(LogType.Action, LogImpact.Low,
+ _adminLogger.Add(LogType.Action,
+ LogImpact.Low,
$"{ToPrettyString(user):player} inserted {initialCount - newCount} item(s) from {ToPrettyString(toInsert):inserted} into {ToPrettyString(receiver):receiver}");
if (empty)
Del(toInsert);
@@ -190,13 +209,16 @@ public List SpawnMultipleFromMaterial(int amount, string material, En
/// 1 biomass = 1 biomass in its stack,
/// but 100 plasma = 1 sheet of plasma, etc.
///
- public List SpawnMultipleFromMaterial(int amount, string material, EntityCoordinates coordinates, out int overflowMaterial)
+ public List SpawnMultipleFromMaterial(int amount,
+ string material,
+ EntityCoordinates coordinates,
+ out int overflowMaterial)
{
overflowMaterial = 0;
if (!_prototypeManager.TryIndex(material, out var stackType))
{
Log.Error("Failed to index material prototype " + material);
- return new List();
+ return [];
}
return SpawnMultipleFromMaterial(amount, stackType, coordinates, out overflowMaterial);
@@ -209,7 +231,9 @@ public List SpawnMultipleFromMaterial(int amount, string material, En
/// but 100 plasma = 1 sheet of plasma, etc.
///
[PublicAPI]
- public List SpawnMultipleFromMaterial(int amount, MaterialPrototype materialProto, EntityCoordinates coordinates)
+ public List SpawnMultipleFromMaterial(int amount,
+ MaterialPrototype materialProto,
+ EntityCoordinates coordinates)
{
return SpawnMultipleFromMaterial(amount, materialProto, coordinates, out _);
}
@@ -220,23 +244,27 @@ public List SpawnMultipleFromMaterial(int amount, MaterialPrototype m
/// 1 biomass = 1 biomass in its stack,
/// but 100 plasma = 1 sheet of plasma, etc.
///
- public List SpawnMultipleFromMaterial(int amount, MaterialPrototype materialProto, EntityCoordinates coordinates, out int overflowMaterial)
+ public List SpawnMultipleFromMaterial(int amount,
+ MaterialPrototype materialProto,
+ EntityCoordinates coordinates,
+ out int overflowMaterial)
{
overflowMaterial = 0;
if (amount <= 0 || materialProto.StackEntity == null)
- return new List();
+ return [];
var entProto = _prototypeManager.Index(materialProto.StackEntity);
- if (!entProto.TryGetComponent(out var composition, EntityManager.ComponentFactory))
- return new List();
+ if (!entProto.TryGetComponent(out var composition,
+ EntityManager.ComponentFactory))
+ return [];
var materialPerStack = composition.MaterialComposition[materialProto.ID];
var amountToSpawn = amount / materialPerStack;
overflowMaterial = amount - amountToSpawn * materialPerStack;
if (amountToSpawn == 0)
- return new List();
+ return [];
return _stackSystem.SpawnMultiple(materialProto.StackEntity, amountToSpawn, coordinates);
}
@@ -259,7 +287,7 @@ public List EjectMaterial(
MaterialStorageComponent? component = null)
{
if (!Resolve(entity, ref component))
- return new List();
+ return [];
coordinates ??= Transform(entity).Coordinates;
@@ -286,7 +314,7 @@ public List EjectAllMaterial(
MaterialStorageComponent? component = null)
{
if (!Resolve(entity, ref component))
- return new List();
+ return [];
coordinates ??= Transform(entity).Coordinates;
diff --git a/Content.Server/Materials/OreSilo/OreSiloSystem.cs b/Content.Server/Materials/OreSilo/OreSiloSystem.cs
new file mode 100644
index 00000000000..332c0c4f053
--- /dev/null
+++ b/Content.Server/Materials/OreSilo/OreSiloSystem.cs
@@ -0,0 +1,123 @@
+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.OreSilo;
+
+///
+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;
+
+ // Don't show clients on the screen if we can't link them.
+ if (!CanTransmitMaterials((ent, ent, xform), client))
+ 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-beaconless", // Frontier: use NF key
+ ("name", name),
+ // ("beacon", beacon), // Frontier
+ ("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);
+ var inRange = CanTransmitMaterials((ent, ent, xform), client);
+
+ var txt = Loc.GetString("ore-silo-ui-itemlist-entry-beaconless",
+ ("name", name),
+ // ("beacon", beacon), // Frontier
+ ("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 d21eff2dff4..4a9129748b9 100644
--- a/Content.Server/Pinpointer/NavMapSystem.cs
+++ b/Content.Server/Pinpointer/NavMapSystem.cs
@@ -452,12 +452,12 @@ public bool TryGetNearestBeacon(MapCoordinates coordinates,
/// 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);
}
///
@@ -465,11 +465,14 @@ public string GetNearestBeaconString(Entity ent)
/// 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 7d8dd5c7495..1911b3de6f0 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 00000000000..ff43c9c05aa
--- /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 00000000000..45c86b18456
--- /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 00000000000..33168db1db3
--- /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.Comp1, ref silo.Comp2))
+ return false;
+
+ if (!_powerReceiver.IsPowered(silo.Owner))
+ return false;
+
+ if (_transform.GetGrid(client) != _transform.GetGrid(silo.Owner))
+ return false;
+
+ if (!_transform.InRange((silo.Owner, silo.Comp2), client, silo.Comp1.Range))
+ return false;
+
+ return true;
+ }
+}
diff --git a/Content.Shared/Materials/SharedMaterialStorageSystem.cs b/Content.Shared/Materials/SharedMaterialStorageSystem.cs
index f309481f2b9..39f6fd4aa91 100644
--- a/Content.Shared/Materials/SharedMaterialStorageSystem.cs
+++ b/Content.Shared/Materials/SharedMaterialStorageSystem.cs
@@ -1,14 +1,13 @@
using System.Linq;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Components;
-using Content.Shared.Mobs;
+using Content.Shared.Research.Components;
using Content.Shared.Stacks;
using Content.Shared.Whitelist;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
-using Content.Shared.Research.Components;
namespace Content.Shared.Materials;
@@ -59,17 +58,37 @@ private void OnMapInit(EntityUid uid, MaterialStorageComponent component, MapIni
_appearance.SetData(uid, MaterialStorageVisuals.Inserting, false);
}
+ ///
+ /// Gets all the materials stored on this entity
+ ///
+ ///
+ /// Include only materials held "locally", as determined by event subscribers
+ ///
+ public Dictionary, int> GetStoredMaterials(Entity 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, int>(ent.Comp.Storage);
+ var ev = new GetStoredMaterialsEvent((ent, ent.Comp), mats, localOnly);
+ RaiseLocalEvent(ent, ref ev, true);
+
+ return ev.Materials;
+ }
+
///
/// Gets the volume of a specified material contained in this storage.
///
///
///
///
+ ///
/// The volume of the material
[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);
}
///
@@ -78,12 +97,13 @@ public int GetMaterialAmount(EntityUid uid, MaterialPrototype material, Material
///
///
///
+ ///
/// The volume of the material
- 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);
}
///
@@ -91,12 +111,13 @@ public int GetMaterialAmount(EntityUid uid, string material, MaterialStorageComp
///
///
///
+ ///
/// 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();
}
///
@@ -105,12 +126,27 @@ public int GetTotalMaterialAmount(EntityUid uid, MaterialStorageComponent? compo
///
///
///
+ ///
/// 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);
}
///
@@ -120,8 +156,9 @@ public bool CanTakeVolume(EntityUid uid, int volume, MaterialStorageComponent? c
///
///
///
+ ///
/// 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;
@@ -129,10 +166,10 @@ public bool CanChangeMaterialAmount(EntityUid uid, string materialId, int volume
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;
}
@@ -142,14 +179,24 @@ public bool CanChangeMaterialAmount(EntityUid uid, string materialId, int volume
///
///
/// 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;
}
@@ -165,16 +212,27 @@ public bool CanChangeMaterialAmount(Entity entity, Di
///
///
///
+ ///
/// 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);
@@ -193,23 +251,54 @@ public bool TryChangeMaterialAmount(EntityUid uid, string materialId, int volume
/// 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)
{
- if (!Resolve(entity, ref entity.Comp))
- return false;
+ return TryChangeMaterialAmount(entity, materials.Select(p => (new ProtoId(p.Key), p.Value)).ToDictionary(), localOnly);
+ }
- if (!CanChangeMaterialAmount(entity, materials))
+ ///
+ /// 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;
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;
}
@@ -223,6 +312,7 @@ public bool TryChangeMaterialAmount(Entity entity, Di
/// 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,
@@ -270,7 +360,7 @@ public virtual bool TryInsertMaterialEntity(EntityUid user,
totalVolume += vol * multiplier;
}
- if (!CanTakeVolume(receiver, totalVolume, storage))
+ if (!CanTakeVolume(receiver, totalVolume, storage, localOnly: true))
return false;
foreach (var (mat, vol) in composition.MaterialComposition)
@@ -318,7 +408,7 @@ public virtual bool TryInsertMaxPossibleMaterialEntity(EntityUid user,
if (HasComp(toInsert))
return false;
- int multiplier;
+ var multiplier = 0;
if (storage.StorageLimit is not null && TryComp(toInsert, out var stack))
{
var availableVolume = (int)storage.StorageLimit - GetTotalMaterialAmount(receiver, storage);
@@ -327,7 +417,8 @@ public virtual bool TryInsertMaxPossibleMaterialEntity(EntityUid user,
{
volumePerSheet += vol;
}
- multiplier = availableVolume / volumePerSheet;
+ if (volumePerSheet != 0)
+ multiplier = availableVolume / volumePerSheet;
if (multiplier >= stack.Count)
{
empty = true;
diff --git a/DEV_RESEARCH_GRAPH.pdn b/DEV_RESEARCH_GRAPH.pdn
index 764df36a19a..9310c3db353 100644
Binary files a/DEV_RESEARCH_GRAPH.pdn and b/DEV_RESEARCH_GRAPH.pdn differ
diff --git a/Resources/Locale/en-US/_Null/research/technologies.ftl b/Resources/Locale/en-US/_Null/research/technologies.ftl
index 373b0726b7f..079791ef9a0 100644
--- a/Resources/Locale/en-US/_Null/research/technologies.ftl
+++ b/Resources/Locale/en-US/_Null/research/technologies.ftl
@@ -145,4 +145,5 @@ research-technology-shuttle-armaments-shields = Weaponized Shield Emitters
research-technology-shuttle-armaments-advanced-shields = Circiabonic Shield Emitters
research-technology-automated-cleaning = Automated Waste Emancipation
research-technology-antique-weapons = Antique Weapon Restoration
-research-technology-ion-weaponry = Advance Ion-Plasmic Field Mechanics
\ No newline at end of file
+research-technology-ion-weaponry = Advanced Ion-Plasmic Field Mechanics
+research-technology-material-silo = Advanced Material Allocation
\ No newline at end of file
diff --git a/Resources/Locale/en-US/materials/silo.ftl b/Resources/Locale/en-US/materials/silo.ftl
new file mode 100644
index 00000000000..89927a99299
--- /dev/null
+++ b/Resources/Locale/en-US/materials/silo.ftl
@@ -0,0 +1,17 @@
+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)
+}
+ore-silo-ui-itemlist-entry-beaconless = {$linked ->
+ [true] {"[Linked] "}
+ *[False] {""}
+} {$name} {$inRange ->
+ [true] {""}
+ *[false] (Out of Range)
+}
diff --git a/Resources/Locale/en-US/materials/units.ftl b/Resources/Locale/en-US/materials/units.ftl
index bff854086b4..b4bd4b3879b 100644
--- a/Resources/Locale/en-US/materials/units.ftl
+++ b/Resources/Locale/en-US/materials/units.ftl
@@ -16,6 +16,8 @@ materials-unit-slab = slab
materials-unit-web = web
# chunks of ore
materials-unit-chunk = chunk
+# bolls of cotton
+materials-unit-boll = boll
# bills of spesos... not very good but they are not (yet?) used for crafting anything
# also the lathe/atm would need bigger denominations to output...
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
index 5e39aac10e9..a5afbffffb8 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
@@ -1026,6 +1026,20 @@
stackRequirements:
Steel: 1
+- type: entity
+ id: MaterialSiloMachineCircuitboard
+ parent: BaseMachineCircuitboard
+ name: material silo machine board
+ components:
+ - type: Sprite
+ state: supply
+ - type: MachineBoard
+ prototype: MachineMaterialSilo
+ requirements: # Frontier
+ MatterBin: 4 # Frontier: stackRequirements