Local Material Silo (#36492)

* 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>
This commit is contained in:
Nemanja
2025-04-18 19:43:17 -04:00
committed by GitHub
parent 667bda28df
commit f8ff7aee92
28 changed files with 842 additions and 46 deletions

View File

@@ -127,12 +127,17 @@
HorizontalExpand="True" HorizontalExpand="True"
Orientation="Vertical"> Orientation="Vertical">
<Label Text="{Loc 'lathe-menu-materials-title'}" Margin="5 5 5 5" HorizontalAlignment="Center"/> <Label Text="{Loc 'lathe-menu-materials-title'}" Margin="5 5 5 5" HorizontalAlignment="Center"/>
<BoxContainer <PanelContainer VerticalExpand="True">
Orientation="Vertical" <PanelContainer.PanelOverride>
VerticalExpand="True" <gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
HorizontalExpand="True"> </PanelContainer.PanelOverride>
<ui:MaterialStorageControl Name="MaterialsList" SizeFlagsStretchRatio="8"/> <BoxContainer
</BoxContainer> Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True">
<ui:MaterialStorageControl Name="MaterialsList" SizeFlagsStretchRatio="8"/>
</BoxContainer>
</PanelContainer>
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>

View File

@@ -0,0 +1,6 @@
using Content.Shared.Materials.OreSilo;
namespace Content.Client.Materials;
/// <inheritdoc/>
public sealed class OreSiloSystem : SharedOreSiloSystem;

View File

@@ -2,7 +2,10 @@
SizeFlagsStretchRatio="8" SizeFlagsStretchRatio="8"
HorizontalExpand="True" HorizontalExpand="True"
VerticalExpand="True"> VerticalExpand="True">
<BoxContainer Name="MaterialList" Orientation="Vertical"> <BoxContainer Orientation="Vertical" VerticalExpand="True">
<Label Name="NoMatsLabel" Text="{Loc 'lathe-menu-no-materials-message'}" Align="Center"/> <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> </BoxContainer>
</ScrollContainer> </ScrollContainer>

View File

@@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using Content.Shared.Materials; using Content.Shared.Materials;
using Content.Shared.Materials.OreSilo;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
@@ -15,6 +16,7 @@ namespace Content.Client.Materials.UI;
public sealed partial class MaterialStorageControl : ScrollContainer public sealed partial class MaterialStorageControl : ScrollContainer
{ {
[Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!;
private readonly MaterialStorageSystem _materialStorage;
private EntityUid? _owner; private EntityUid? _owner;
@@ -24,6 +26,8 @@ public sealed partial class MaterialStorageControl : ScrollContainer
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this); IoCManager.InjectDependencies(this);
_materialStorage = _entityManager.System<MaterialStorageSystem>();
} }
public void SetOwner(EntityUid owner) public void SetOwner(EntityUid owner)
@@ -45,7 +49,8 @@ public sealed partial class MaterialStorageControl : ScrollContainer
} }
var canEject = materialStorage.CanEjectStoredMaterials; var canEject = materialStorage.CanEjectStoredMaterials;
var mats = materialStorage.Storage; var mats = _materialStorage.GetStoredMaterials((_owner.Value, materialStorage));
if (_currentMaterials.Equals(mats)) if (_currentMaterials.Equals(mats))
return; return;
@@ -89,5 +94,6 @@ public sealed partial class MaterialStorageControl : ScrollContainer
_currentMaterials = mats; _currentMaterials = mats;
NoMatsLabel.Visible = MaterialList.ChildCount == 1; NoMatsLabel.Visible = MaterialList.ChildCount == 1;
SiloLinkedLabel.Visible = _entityManager.TryGetComponent<OreSiloClientComponent>(_owner.Value, out var client) && client.Silo != null;
} }
} }

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -102,14 +102,18 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
if (!base.TryInsertMaterialEntity(user, toInsert, receiver, storage, material, composition)) if (!base.TryInsertMaterialEntity(user, toInsert, receiver, storage, material, composition))
return false; return false;
_audio.PlayPvs(storage.InsertingSound, receiver); _audio.PlayPvs(storage.InsertingSound, receiver);
_popup.PopupEntity(Loc.GetString("machine-insert-item", ("user", user), ("machine", receiver), _popup.PopupEntity(Loc.GetString("machine-insert-item",
("item", toInsert)), receiver); ("user", user),
("machine", receiver),
("item", toInsert)),
receiver);
QueueDel(toInsert); QueueDel(toInsert);
// Logging // Logging
TryComp<StackComponent>(toInsert, out var stack); TryComp<StackComponent>(toInsert, out var stack);
var count = stack?.Count ?? 1; 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}"); $"{ToPrettyString(user):player} inserted {count} {ToPrettyString(toInsert):inserted} into {ToPrettyString(receiver):receiver}");
return true; return true;
} }

View File

@@ -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);
}
}
}
}

View File

@@ -436,12 +436,12 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
/// to the position of <paramref name="ent"/> from the nearest beacon. /// to the position of <paramref name="ent"/> from the nearest beacon.
/// </summary> /// </summary>
[PublicAPI] [PublicAPI]
public string GetNearestBeaconString(Entity<TransformComponent?> ent) public string GetNearestBeaconString(Entity<TransformComponent?> ent, bool onlyName = false)
{ {
if (!Resolve(ent, ref ent.Comp)) if (!Resolve(ent, ref ent.Comp))
return Loc.GetString("nav-beacon-pos-no-beacons"); return Loc.GetString("nav-beacon-pos-no-beacons");
return GetNearestBeaconString(_transformSystem.GetMapCoordinates(ent, ent.Comp)); return GetNearestBeaconString(_transformSystem.GetMapCoordinates(ent, ent.Comp), onlyName);
} }
/// <summary> /// <summary>
@@ -449,11 +449,14 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
/// to <paramref name="coordinates"/> from the nearest beacon. /// to <paramref name="coordinates"/> from the nearest beacon.
/// </summary> /// </summary>
public string GetNearestBeaconString(MapCoordinates coordinates) public string GetNearestBeaconString(MapCoordinates coordinates, bool onlyName = false)
{ {
if (!TryGetNearestBeacon(coordinates, out var beacon, out var pos)) if (!TryGetNearestBeacon(coordinates, out var beacon, out var pos))
return Loc.GetString("nav-beacon-pos-no-beacons"); return Loc.GetString("nav-beacon-pos-no-beacons");
if (onlyName)
return beacon.Value.Comp.Text!;
var gridOffset = Angle.Zero; var gridOffset = Angle.Zero;
if (_mapManager.TryFindGridAt(pos.Value, out var grid, out _)) if (_mapManager.TryFindGridAt(pos.Value, out var grid, out _))
gridOffset = Transform(grid).LocalRotation; gridOffset = Transform(grid).LocalRotation;

View File

@@ -75,6 +75,24 @@ public enum MaterialStorageVisuals : byte
Inserting 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> /// <summary>
/// event raised on the materialStorage when a material entity is inserted into it. /// event raised on the materialStorage when a material entity is inserted into it.
/// </summary> /// </summary>

View File

@@ -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;
}

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -1,7 +1,6 @@
using System.Linq; using System.Linq;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Interaction.Components; using Content.Shared.Interaction.Components;
using Content.Shared.Mobs;
using Content.Shared.Stacks; using Content.Shared.Stacks;
using Content.Shared.Whitelist; using Content.Shared.Whitelist;
using JetBrains.Annotations; using JetBrains.Annotations;
@@ -58,16 +57,22 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
} }
/// <summary> /// <summary>
/// Gets the volume of a specified material contained in this storage. /// Gets all the materials stored on this entity
/// </summary> /// </summary>
/// <param name="uid"></param> /// <param name="ent"></param>
/// <param name="material"></param> /// <param name="localOnly">Include only materials held "locally", as determined by event subscribers</param>
/// <param name="component"></param> /// <returns></returns>
/// <returns>The volume of the material</returns> public Dictionary<ProtoId<MaterialPrototype>, int> GetStoredMaterials(Entity<MaterialStorageComponent?> ent, bool localOnly = false)
[PublicAPI]
public int GetMaterialAmount(EntityUid uid, MaterialPrototype material, MaterialStorageComponent? component = null)
{ {
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<ProtoId<MaterialPrototype>, int>(ent.Comp.Storage);
var ev = new GetStoredMaterialsEvent((ent, ent.Comp), mats, localOnly);
RaiseLocalEvent(ent, ref ev, true);
return ev.Materials;
} }
/// <summary> /// <summary>
@@ -76,12 +81,27 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
/// <param name="uid"></param> /// <param name="uid"></param>
/// <param name="material"></param> /// <param name="material"></param>
/// <param name="component"></param> /// <param name="component"></param>
/// <param name="localOnly"></param>
/// <returns>The volume of the material</returns> /// <returns>The volume of the material</returns>
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);
}
/// <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>
public int GetMaterialAmount(EntityUid uid, string material, MaterialStorageComponent? component = null, bool localOnly = false)
{ {
if (!Resolve(uid, ref component)) if (!Resolve(uid, ref component))
return 0; //you have nothing return 0; //you have nothing
return component.Storage.GetValueOrDefault(material, 0); return GetStoredMaterials((uid, component), localOnly).GetValueOrDefault(material, 0);
} }
/// <summary> /// <summary>
@@ -89,26 +109,43 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
/// </summary> /// </summary>
/// <param name="uid"></param> /// <param name="uid"></param>
/// <param name="component"></param> /// <param name="component"></param>
/// <param name="localOnly"></param>
/// <returns>The volume of all materials in the storage</returns> /// <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)) if (!Resolve(uid, ref component))
return 0; 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> /// <summary>
/// Tests if a specific amount of volume will fit in the storage. /// Tests if a specific amount of volume will fit in the storage.
/// </summary> /// </summary>
/// <param name="uid"></param> /// <param name="uid"></param>
/// <param name="volume"></param> /// <param name="volume"></param>
/// <param name="component"></param> /// <param name="component"></param>
/// <param name="localOnly"></param>
/// <returns>If the specified volume will fit</returns> /// <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)) if (!Resolve(uid, ref component))
return false; 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> /// <summary>
@@ -118,8 +155,9 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
/// <param name="materialId"></param> /// <param name="materialId"></param>
/// <param name="volume"></param> /// <param name="volume"></param>
/// <param name="component"></param> /// <param name="component"></param>
/// <param name="localOnly"></param>
/// <returns>If the amount can be changed</returns> /// <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)) if (!Resolve(uid, ref component))
return false; return false;
@@ -127,10 +165,10 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
if (!CanTakeVolume(uid, volume, component)) if (!CanTakeVolume(uid, volume, component))
return false; return false;
if (component.MaterialWhiteList == null ? false : !component.MaterialWhiteList.Contains(materialId)) if (!IsMaterialWhitelisted((uid, component), materialId))
return false; return false;
var amount = component.Storage.GetValueOrDefault(materialId); var amount = GetMaterialAmount(uid, materialId, component, localOnly);
return amount + volume >= 0; return amount + volume >= 0;
} }
@@ -140,14 +178,24 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
/// <param name="entity"></param> /// <param name="entity"></param>
/// <param name="materials"></param> /// <param name="materials"></param>
/// <returns>If the amount can be changed</returns> /// <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)) if (!Resolve(entity, ref entity.Comp))
return false; 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) 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; return false;
} }
@@ -163,16 +211,27 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
/// <param name="volume"></param> /// <param name="volume"></param>
/// <param name="component"></param> /// <param name="component"></param>
/// <param name="dirty"></param> /// <param name="dirty"></param>
/// <param name="localOnly"></param>
/// <returns>If it was successful</returns> /// <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)) if (!Resolve(uid, ref component))
return false; return false;
if (!CanChangeMaterialAmount(uid, materialId, volume, component))
if (!CanChangeMaterialAmount(uid, materialId, volume, component, localOnly))
return false; 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); 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) if (existing == 0)
component.Storage.Remove(materialId); component.Storage.Remove(materialId);
@@ -191,23 +250,54 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
/// Changes the amount of a specific material in the storage. /// Changes the amount of a specific material in the storage.
/// Still respects the filters in place. /// Still respects the filters in place.
/// </summary> /// </summary>
/// <param name="entity"></param>
/// <param name="materials"></param>
/// <returns>If the amount can be changed</returns> /// <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)
{
return TryChangeMaterialAmount(entity, materials.Select(p => (new ProtoId<MaterialPrototype>(p.Key), p.Value)).ToDictionary(), localOnly);
}
/// <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)) if (!Resolve(entity, ref entity.Comp))
return false; return false;
if (!CanChangeMaterialAmount(entity, materials))
return false;
foreach (var (material, amount) in materials) foreach (var (material, amount) in materials)
{ {
if (!TryChangeMaterialAmount(entity, material, amount, entity.Comp, false)) if (!CanChangeMaterialAmount(entity, material, amount, entity))
return false; 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); Dirty(entity, entity.Comp);
return true; 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="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> /// <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> /// <returns>True if it was successful (enough space etc).</returns>
[PublicAPI]
public bool TrySetMaterialAmount( public bool TrySetMaterialAmount(
EntityUid uid, EntityUid uid,
string materialId, string materialId,
@@ -268,7 +359,7 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
totalVolume += vol * multiplier; totalVolume += vol * multiplier;
} }
if (!CanTakeVolume(receiver, totalVolume, storage)) if (!CanTakeVolume(receiver, totalVolume, storage, localOnly: true))
return false; return false;
foreach (var (mat, vol) in composition.MaterialComposition) foreach (var (mat, vol) in composition.MaterialComposition)

View 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]) *[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-no-materials-message = No materials loaded.
lathe-menu-silo-linked-message = Silo Linked
lathe-menu-fabricating-message = Fabricating... lathe-menu-fabricating-message = Fabricating...
lathe-menu-materials-title = Materials lathe-menu-materials-title = Materials
lathe-menu-queue-title = Build Queue lathe-menu-queue-title = Build Queue

View File

@@ -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)
}

View File

@@ -98,6 +98,16 @@
category: cargoproduct-category-name-materials category: cargoproduct-category-name-materials
group: market 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 - type: cargoProduct
id: MaterialFuelTank id: MaterialFuelTank
icon: icon:

View File

@@ -153,6 +153,22 @@
# for some reason, the selector here adds 1 to whatever value it generates, # for some reason, the selector here adds 1 to whatever value it generates,
# so this is actually 2-4 # 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 - type: entity
id: CrateMaterialBasicResource id: CrateMaterialBasicResource
parent: CrateGenericSteel parent: CrateGenericSteel

View File

@@ -1005,6 +1005,19 @@
Manipulator: 1 Manipulator: 1
Steel: 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 - type: entity
id: OreProcessorMachineCircuitboard id: OreProcessorMachineCircuitboard
parent: BaseMachineCircuitboard parent: BaseMachineCircuitboard

View File

@@ -270,6 +270,7 @@
- Sheet - Sheet
materialWhiteList: materialWhiteList:
- Plasma - Plasma
- type: OreSiloClient
- type: Fixtures - type: Fixtures
fixtures: fixtures:
fix1: fix1:

View File

@@ -44,6 +44,7 @@
- Sheet - Sheet
- RawMaterial - RawMaterial
- Ingot - Ingot
- type: OreSiloClient
- type: AmbientSound - type: AmbientSound
enabled: false enabled: false
volume: 5 volume: 5

View File

@@ -112,6 +112,7 @@
- Sheet - Sheet
- RawMaterial - RawMaterial
- Ingot - Ingot
- type: OreSiloClient
- type: Lathe - type: Lathe
idleState: icon idleState: icon
runningState: building runningState: building
@@ -184,6 +185,7 @@
- Sheet - Sheet
- RawMaterial - RawMaterial
- Ingot - Ingot
- type: OreSiloClient
- type: Lathe - type: Lathe
idleState: icon idleState: icon
runningState: building runningState: building
@@ -273,6 +275,7 @@
- Sheet - Sheet
- RawMaterial - RawMaterial
- Ingot - Ingot
- type: OreSiloClient
- type: RequireProjectileTarget - type: RequireProjectileTarget
- type: entity - type: entity
@@ -321,6 +324,7 @@
- Sheet - Sheet
- RawMaterial - RawMaterial
- Ingot - Ingot
- type: OreSiloClient
- type: GuideHelp - type: GuideHelp
guides: guides:
- Robotics - Robotics
@@ -408,6 +412,7 @@
- Sheet - Sheet
- RawMaterial - RawMaterial
- Ingot - Ingot
- type: OreSiloClient
- type: LatheAnnouncing - type: LatheAnnouncing
channels: [Security] channels: [Security]
@@ -443,6 +448,7 @@
- Sheet - Sheet
- RawMaterial - RawMaterial
- Ingot - Ingot
- type: OreSiloClient
- type: entity - type: entity
id: MedicalTechFab id: MedicalTechFab
@@ -480,6 +486,7 @@
board: MedicalTechFabCircuitboard board: MedicalTechFabCircuitboard
- type: StealTarget - type: StealTarget
stealGroup: MedicalTechFabCircuitboard stealGroup: MedicalTechFabCircuitboard
- type: OreSiloClient
- type: LatheAnnouncing - type: LatheAnnouncing
channels: [Medical] channels: [Medical]

View File

@@ -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

View File

@@ -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
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB