HorizontalExpand="True"
Orientation="Vertical">
<Label Text="{Loc 'lathe-menu-materials-title'}" Margin="5 5 5 5" HorizontalAlignment="Center"/>
- <BoxContainer
- Orientation="Vertical"
- VerticalExpand="True"
- HorizontalExpand="True">
- <ui:MaterialStorageControl Name="MaterialsList" SizeFlagsStretchRatio="8"/>
- </BoxContainer>
+ <PanelContainer VerticalExpand="True">
+ <PanelContainer.PanelOverride>
+ <gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
+ </PanelContainer.PanelOverride>
+ <BoxContainer
+ Orientation="Vertical"
+ VerticalExpand="True"
+ HorizontalExpand="True">
+ <ui:MaterialStorageControl Name="MaterialsList" SizeFlagsStretchRatio="8"/>
+ </BoxContainer>
+ </PanelContainer>
</BoxContainer>
</BoxContainer>
--- /dev/null
+using Content.Shared.Materials.OreSilo;
+
+namespace Content.Client.Materials;
+
+/// <inheritdoc/>
+public sealed class OreSiloSystem : SharedOreSiloSystem;
SizeFlagsStretchRatio="8"
HorizontalExpand="True"
VerticalExpand="True">
- <BoxContainer Name="MaterialList" Orientation="Vertical">
- <Label Name="NoMatsLabel" Text="{Loc 'lathe-menu-no-materials-message'}" Align="Center"/>
+ <BoxContainer Orientation="Vertical" VerticalExpand="True">
+ <BoxContainer Name="MaterialList" Orientation="Vertical" VerticalExpand="True">
+ <Label Name="NoMatsLabel" Text="{Loc 'lathe-menu-no-materials-message'}" HorizontalAlignment="Center" VerticalAlignment="Center" VerticalExpand="True"/>
+ </BoxContainer>
+ <Label Name="SiloLinkedLabel" Text="{Loc 'lathe-menu-silo-linked-message'}" StyleClasses="LabelSubText" Visible="False" HorizontalAlignment="Center"/>
</BoxContainer>
</ScrollContainer>
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;
public sealed partial class MaterialStorageControl : ScrollContainer
{
[Dependency] private readonly IEntityManager _entityManager = default!;
+ private readonly MaterialStorageSystem _materialStorage;
private EntityUid? _owner;
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
+
+ _materialStorage = _entityManager.System<MaterialStorageSystem>();
}
public void SetOwner(EntityUid owner)
}
var canEject = materialStorage.CanEjectStoredMaterials;
- var mats = materialStorage.Storage;
+ var mats = _materialStorage.GetStoredMaterials((_owner.Value, materialStorage));
+
if (_currentMaterials.Equals(mats))
return;
_currentMaterials = mats;
NoMatsLabel.Visible = MaterialList.ChildCount == 1;
+ SiloLinkedLabel.Visible = _entityManager.TryGetComponent<OreSiloClientComponent>(_owner.Value, out var client) && client.Silo != null;
}
}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+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;
+ }
+ }
+}
+
if (!base.TryInsertMaterialEntity(user, toInsert, receiver, storage, material, composition))
return false;
_audio.PlayPvs(storage.InsertingSound, receiver);
- _popup.PopupEntity(Loc.GetString("machine-insert-item", ("user", user), ("machine", receiver),
- ("item", toInsert)), receiver);
+ _popup.PopupEntity(Loc.GetString("machine-insert-item",
+ ("user", user),
+ ("machine", receiver),
+ ("item", toInsert)),
+ receiver);
QueueDel(toInsert);
// Logging
TryComp<StackComponent>(toInsert, out var stack);
var count = stack?.Count ?? 1;
- _adminLogger.Add(LogType.Action, LogImpact.Low,
+ _adminLogger.Add(LogType.Action,
+ LogImpact.Low,
$"{ToPrettyString(user):player} inserted {count} {ToPrettyString(toInsert):inserted} into {ToPrettyString(receiver):receiver}");
return true;
}
--- /dev/null
+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);
+ }
+ }
+ }
+}
/// to the position of <paramref name="ent"/> from the nearest beacon.
/// </summary>
[PublicAPI]
- public string GetNearestBeaconString(Entity<TransformComponent?> ent)
+ public string GetNearestBeaconString(Entity<TransformComponent?> ent, bool onlyName = false)
{
if (!Resolve(ent, ref ent.Comp))
return Loc.GetString("nav-beacon-pos-no-beacons");
- return GetNearestBeaconString(_transformSystem.GetMapCoordinates(ent, ent.Comp));
+ return GetNearestBeaconString(_transformSystem.GetMapCoordinates(ent, ent.Comp), onlyName);
}
/// <summary>
/// to <paramref name="coordinates"/> from the nearest beacon.
/// </summary>
- public string GetNearestBeaconString(MapCoordinates coordinates)
+ public string GetNearestBeaconString(MapCoordinates coordinates, bool onlyName = false)
{
if (!TryGetNearestBeacon(coordinates, out var beacon, out var pos))
return Loc.GetString("nav-beacon-pos-no-beacons");
+ if (onlyName)
+ return beacon.Value.Comp.Text!;
+
var gridOffset = Angle.Zero;
if (_mapManager.TryFindGridAt(pos.Value, out var grid, out _))
gridOffset = Transform(grid).LocalRotation;
Inserting
}
+/// <summary>
+/// Collects all the materials stored on a <see cref="MaterialStorageComponent"/>
+/// </summary>
+/// <param name="Entity">The entity holding all these materials</param>
+/// <param name="Materials">A dictionary of all materials held</param>
+/// <param name="LocalOnly">An optional specifier. Non-local sources (silo, etc.) should not add materials when this is false.</param>
+[ByRefEvent]
+public readonly record struct GetStoredMaterialsEvent(Entity<MaterialStorageComponent> Entity, Dictionary<ProtoId<MaterialPrototype>, int> Materials, bool LocalOnly);
+
+/// <summary>
+/// After using materials, removes them from storage.
+/// </summary>
+/// <param name="Entity">The entity that held the materials and is being used up</param>
+/// <param name="Materials">A dictionary of the difference of materials left.</param>
+/// <param name="LocalOnly">An optional specifier. Non-local sources (silo, etc.) should not consume materials when this is false.</param>
+[ByRefEvent]
+public readonly record struct ConsumeStoredMaterialsEvent(Entity<MaterialStorageComponent> Entity, Dictionary<ProtoId<MaterialPrototype>, int> Materials, bool LocalOnly);
+
/// <summary>
/// event raised on the materialStorage when a material entity is inserted into it.
/// </summary>
--- /dev/null
+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;
+}
--- /dev/null
+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
+}
--- /dev/null
+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;
+ }
+}
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;
_appearance.SetData(uid, MaterialStorageVisuals.Inserting, false);
}
+ /// <summary>
+ /// Gets all the materials stored on this entity
+ /// </summary>
+ /// <param name="ent"></param>
+ /// <param name="localOnly">Include only materials held "locally", as determined by event subscribers</param>
+ /// <returns></returns>
+ public Dictionary<ProtoId<MaterialPrototype>, int> GetStoredMaterials(Entity<MaterialStorageComponent?> ent, bool localOnly = false)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return new();
+
+ // clone so we don't modify by accident.
+ var mats = new Dictionary<ProtoId<MaterialPrototype>, int>(ent.Comp.Storage);
+ var ev = new GetStoredMaterialsEvent((ent, ent.Comp), mats, localOnly);
+ RaiseLocalEvent(ent, ref ev, true);
+
+ return ev.Materials;
+ }
+
/// <summary>
/// Gets the volume of a specified material contained in this storage.
/// </summary>
/// <param name="uid"></param>
/// <param name="material"></param>
/// <param name="component"></param>
+ /// <param name="localOnly"></param>
/// <returns>The volume of the material</returns>
[PublicAPI]
- public int GetMaterialAmount(EntityUid uid, MaterialPrototype material, MaterialStorageComponent? component = null)
+ public int GetMaterialAmount(EntityUid uid, MaterialPrototype material, MaterialStorageComponent? component = null, bool localOnly = false)
{
- return GetMaterialAmount(uid, material.ID, component);
+ return GetMaterialAmount(uid, material.ID, component, localOnly);
}
/// <summary>
/// <param name="uid"></param>
/// <param name="material"></param>
/// <param name="component"></param>
+ /// <param name="localOnly"></param>
/// <returns>The volume of the material</returns>
- public int GetMaterialAmount(EntityUid uid, string material, MaterialStorageComponent? component = null)
+ public int GetMaterialAmount(EntityUid uid, string material, MaterialStorageComponent? component = null, bool localOnly = false)
{
if (!Resolve(uid, ref component))
return 0; //you have nothing
- return component.Storage.GetValueOrDefault(material, 0);
+ return GetStoredMaterials((uid, component), localOnly).GetValueOrDefault(material, 0);
}
/// <summary>
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
+ /// <param name="localOnly"></param>
/// <returns>The volume of all materials in the storage</returns>
- public int GetTotalMaterialAmount(EntityUid uid, MaterialStorageComponent? component = null)
+ public int GetTotalMaterialAmount(EntityUid uid, MaterialStorageComponent? component = null, bool localOnly = false)
{
if (!Resolve(uid, ref component))
return 0;
- return component.Storage.Values.Sum();
+ return GetStoredMaterials((uid, component), localOnly).Values.Sum();
}
+ // TODO: Revisit this if we ever decide to do things with storage limits. As it stands, the feature is unused.
/// <summary>
/// Tests if a specific amount of volume will fit in the storage.
/// </summary>
/// <param name="uid"></param>
/// <param name="volume"></param>
/// <param name="component"></param>
+ /// <param name="localOnly"></param>
/// <returns>If the specified volume will fit</returns>
- public bool CanTakeVolume(EntityUid uid, int volume, MaterialStorageComponent? component = null)
+ public bool CanTakeVolume(EntityUid uid, int volume, MaterialStorageComponent? component = null, bool localOnly = false)
{
if (!Resolve(uid, ref component))
return false;
- return component.StorageLimit == null || GetTotalMaterialAmount(uid, component) + volume <= component.StorageLimit;
+ return component.StorageLimit == null || GetTotalMaterialAmount(uid, component, true) + volume <= component.StorageLimit;
+ }
+
+ /// <summary>
+ /// Checks if a certain material prototype is supported by this entity.
+ /// </summary>
+ public bool IsMaterialWhitelisted(Entity<MaterialStorageComponent?> ent, ProtoId<MaterialPrototype> material)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return false;
+
+ if (ent.Comp.MaterialWhiteList == null)
+ return true;
+
+ return ent.Comp.MaterialWhiteList.Contains(material);
}
/// <summary>
/// <param name="materialId"></param>
/// <param name="volume"></param>
/// <param name="component"></param>
+ /// <param name="localOnly"></param>
/// <returns>If the amount can be changed</returns>
- public bool CanChangeMaterialAmount(EntityUid uid, string materialId, int volume, MaterialStorageComponent? component = null)
+ public bool CanChangeMaterialAmount(EntityUid uid, string materialId, int volume, MaterialStorageComponent? component = null, bool localOnly = false)
{
if (!Resolve(uid, ref component))
return false;
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;
}
/// <param name="entity"></param>
/// <param name="materials"></param>
/// <returns>If the amount can be changed</returns>
- public bool CanChangeMaterialAmount(Entity<MaterialStorageComponent?> entity, Dictionary<string,int> materials)
+ /// <param name="localOnly"></param>
+ public bool CanChangeMaterialAmount(Entity<MaterialStorageComponent?> entity, Dictionary<string,int> materials, bool localOnly = false)
{
if (!Resolve(entity, ref entity.Comp))
return false;
+ var inVolume = materials.Values.Sum();
+ var stored = GetStoredMaterials((entity, entity.Comp), localOnly);
+
+ if (!CanTakeVolume(entity, inVolume, entity.Comp))
+ return false;
+
foreach (var (material, amount) in materials)
{
- if (!CanChangeMaterialAmount(entity, material, amount, entity.Comp))
+ if (!IsMaterialWhitelisted(entity, material))
+ return false;
+
+ if (stored.GetValueOrDefault(material) + amount < 0)
return false;
}
/// <param name="volume"></param>
/// <param name="component"></param>
/// <param name="dirty"></param>
+ /// <param name="localOnly"></param>
/// <returns>If it was successful</returns>
- public bool TryChangeMaterialAmount(EntityUid uid, string materialId, int volume, MaterialStorageComponent? component = null, bool dirty = true)
+ public bool TryChangeMaterialAmount(EntityUid uid, string materialId, int volume, MaterialStorageComponent? component = null, bool dirty = true, bool localOnly = false)
{
if (!Resolve(uid, ref component))
return false;
- if (!CanChangeMaterialAmount(uid, materialId, volume, component))
+
+ if (!CanChangeMaterialAmount(uid, materialId, volume, component, localOnly))
return false;
+ var changeEv = new ConsumeStoredMaterialsEvent((uid, component), new() {{materialId, volume}}, localOnly);
+ RaiseLocalEvent(uid, ref changeEv);
+ var remaining = changeEv.Materials.Values.First();
+
var existing = component.Storage.GetOrNew(materialId);
- existing += volume;
+
+ var localUpperLimit = component.StorageLimit == null ? int.MaxValue : component.StorageLimit.Value - existing;
+ var localLowerLimit = -existing;
+ var localChange = Math.Clamp(remaining, localLowerLimit, localUpperLimit);
+
+ existing += localChange;
if (existing == 0)
component.Storage.Remove(materialId);
/// Changes the amount of a specific material in the storage.
/// Still respects the filters in place.
/// </summary>
- /// <param name="entity"></param>
- /// <param name="materials"></param>
/// <returns>If the amount can be changed</returns>
- public bool TryChangeMaterialAmount(Entity<MaterialStorageComponent?> entity, Dictionary<string,int> materials)
+ public bool TryChangeMaterialAmount(Entity<MaterialStorageComponent?> entity, Dictionary<string, int> materials, bool localOnly = false)
{
- if (!Resolve(entity, ref entity.Comp))
- return false;
+ return TryChangeMaterialAmount(entity, materials.Select(p => (new ProtoId<MaterialPrototype>(p.Key), p.Value)).ToDictionary(), localOnly);
+ }
- if (!CanChangeMaterialAmount(entity, materials))
+ /// <summary>
+ /// Changes the amount of a specific material in the storage.
+ /// Still respects the filters in place.
+ /// </summary>
+ /// <returns>If the amount can be changed</returns>
+ public bool TryChangeMaterialAmount(
+ Entity<MaterialStorageComponent?> entity,
+ Dictionary<ProtoId<MaterialPrototype>, int> materials,
+ bool localOnly = false)
+ {
+ if (!Resolve(entity, ref entity.Comp))
return false;
foreach (var (material, amount) in materials)
{
- if (!TryChangeMaterialAmount(entity, material, amount, entity.Comp, false))
+ if (!CanChangeMaterialAmount(entity, material, amount, entity))
return false;
}
+ var changeEv = new ConsumeStoredMaterialsEvent((entity, entity.Comp), materials, localOnly);
+ RaiseLocalEvent(entity, ref changeEv);
+
+ foreach (var (material, remaining) in changeEv.Materials)
+ {
+ var existing = entity.Comp.Storage.GetOrNew(material);
+
+ var localUpperLimit = entity.Comp.StorageLimit == null ? int.MaxValue : entity.Comp.StorageLimit.Value - existing;
+ var localLowerLimit = -existing;
+ var localChange = Math.Clamp(remaining, localLowerLimit, localUpperLimit);
+
+ existing += localChange;
+
+ if (existing == 0)
+ entity.Comp.Storage.Remove(material);
+ else
+ entity.Comp.Storage[material] = existing;
+
+ }
+
+ var ev = new MaterialAmountChangedEvent();
+ RaiseLocalEvent(entity, ref ev);
+
Dirty(entity, entity.Comp);
return true;
}
/// <param name="volume">The stored material volume to set the storage to.</param>
/// <param name="component">The storage component on <paramref name="uid"/>. Resolved automatically if not given.</param>
/// <returns>True if it was successful (enough space etc).</returns>
+ [PublicAPI]
public bool TrySetMaterialAmount(
EntityUid uid,
string materialId,
totalVolume += vol * multiplier;
}
- if (!CanTakeVolume(receiver, totalVolume, storage))
+ if (!CanTakeVolume(receiver, totalVolume, storage, localOnly: true))
return false;
foreach (var (mat, vol) in composition.MaterialComposition)
*[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
--- /dev/null
+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)
+}
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:
# 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
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
- Sheet
materialWhiteList:
- Plasma
+ - type: OreSiloClient
- type: Fixtures
fixtures:
fix1:
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: AmbientSound
enabled: false
volume: 5
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: Lathe
idleState: icon
runningState: building
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: Lathe
idleState: icon
runningState: building
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: RequireProjectileTarget
- type: entity
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: GuideHelp
guides:
- Robotics
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: LatheAnnouncing
channels: [Security]
- Sheet
- RawMaterial
- Ingot
+ - type: OreSiloClient
- type: entity
id: MedicalTechFab
board: MedicalTechFabCircuitboard
- type: StealTarget
stealGroup: MedicalTechFabCircuitboard
+ - type: OreSiloClient
- type: LatheAnnouncing
channels: [Medical]
--- /dev/null
+- 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
--- /dev/null
+{
+ "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
+ ]
+ ]
+ }
+ ]
+}