]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Implement SmartFridge functionality (#38648)
authorpathetic meowmeow <uhhadd@gmail.com>
Mon, 21 Jul 2025 03:21:28 +0000 (23:21 -0400)
committerGitHub <noreply@github.com>
Mon, 21 Jul 2025 03:21:28 +0000 (23:21 -0400)
* Add SmartFridge

* my nit so pick

* my access so expanded and my whitelist so both

* list -> hashset

17 files changed:
Content.Client/SmartFridge/SmartFridgeBoundUserInterface.cs [new file with mode: 0644]
Content.Client/SmartFridge/SmartFridgeItem.xaml [new file with mode: 0644]
Content.Client/SmartFridge/SmartFridgeItem.xaml.cs [new file with mode: 0644]
Content.Client/SmartFridge/SmartFridgeMenu.xaml [new file with mode: 0644]
Content.Client/SmartFridge/SmartFridgeMenu.xaml.cs [new file with mode: 0644]
Content.Client/SmartFridge/SmartFridgeUISystem.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/SmartFridge/SmartFridgeInteractionTest.cs [new file with mode: 0644]
Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs
Content.Shared/Placeable/PlaceableSurfaceSystem.cs
Content.Shared/SmartFridge/SmartFridgeComponent.cs [new file with mode: 0644]
Content.Shared/SmartFridge/SmartFridgeSystem.cs [new file with mode: 0644]
Content.Shared/Storage/Components/DumpableComponent.cs
Content.Shared/Storage/EntitySystems/DumpableSystem.cs
Resources/Locale/en-US/smartfridge/smartfridge.ftl [new file with mode: 0644]
Resources/Locale/en-US/storage/components/dumpable-component.ftl
Resources/Prototypes/Entities/Structures/Machines/smartfridge.yml
Resources/Prototypes/SoundCollections/machines.yml

diff --git a/Content.Client/SmartFridge/SmartFridgeBoundUserInterface.cs b/Content.Client/SmartFridge/SmartFridgeBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..60d8ea4
--- /dev/null
@@ -0,0 +1,43 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.SmartFridge;
+using Robust.Client.UserInterface;
+using Robust.Shared.Input;
+
+namespace Content.Client.SmartFridge;
+
+public sealed class SmartFridgeBoundUserInterface : BoundUserInterface
+{
+    private SmartFridgeMenu? _menu;
+
+    public SmartFridgeBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+    {
+    }
+
+    protected override void Open()
+    {
+        base.Open();
+
+        _menu = this.CreateWindow<SmartFridgeMenu>();
+        _menu.OnItemSelected += OnItemSelected;
+        Refresh();
+    }
+
+    public void Refresh()
+    {
+        if (_menu is not {} menu || !EntMan.TryGetComponent(Owner, out SmartFridgeComponent? fridge))
+            return;
+
+        menu.SetFlavorText(Loc.GetString(fridge.FlavorText));
+        menu.Populate((Owner, fridge));
+    }
+
+    private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
+    {
+        if (args.Function != EngineKeyFunctions.UIClick)
+            return;
+
+        if (data is not SmartFridgeListData entry)
+            return;
+        SendPredictedMessage(new SmartFridgeDispenseItemMessage(entry.Entry));
+    }
+}
diff --git a/Content.Client/SmartFridge/SmartFridgeItem.xaml b/Content.Client/SmartFridge/SmartFridgeItem.xaml
new file mode 100644 (file)
index 0000000..3960d7c
--- /dev/null
@@ -0,0 +1,16 @@
+<BoxContainer xmlns="https://spacestation14.io"
+            Orientation="Horizontal"
+            HorizontalExpand="True"
+            SeparationOverride="4">
+    <SpriteView
+            Name="EntityView"
+            Margin="4 0 0 0"
+            HorizontalAlignment="Center"
+            VerticalAlignment="Center"
+            MinSize="32 32"
+            />
+    <Label Name="NameLabel"
+            SizeFlagsStretchRatio="3"
+            HorizontalExpand="True"
+            ClipText="True"/>
+</BoxContainer>
diff --git a/Content.Client/SmartFridge/SmartFridgeItem.xaml.cs b/Content.Client/SmartFridge/SmartFridgeItem.xaml.cs
new file mode 100644 (file)
index 0000000..c69d2e7
--- /dev/null
@@ -0,0 +1,18 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.SmartFridge;
+
+[GenerateTypedNameReferences]
+public sealed partial class SmartFridgeItem : BoxContainer
+{
+    public SmartFridgeItem(EntityUid uid, string text)
+    {
+        RobustXamlLoader.Load(this);
+
+        EntityView.SetEntity(uid);
+        NameLabel.Text = text;
+    }
+}
diff --git a/Content.Client/SmartFridge/SmartFridgeMenu.xaml b/Content.Client/SmartFridge/SmartFridgeMenu.xaml
new file mode 100644 (file)
index 0000000..73cb0f3
--- /dev/null
@@ -0,0 +1,24 @@
+<controls:FancyWindow
+    xmlns="https://spacestation14.io"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+    xmlns:co="clr-namespace:Content.Client.UserInterface.Controls"
+    MinHeight="450"
+    MinWidth="350"
+    Title="{Loc 'smart-fridge-component-title'}">
+    <BoxContainer Name="MainContainer" Orientation="Vertical">
+        <LineEdit Name="SearchBar" PlaceHolder="{Loc 'smart-fridge-component-search-filter'}" HorizontalExpand="True"  Margin ="4 4"/>
+        <co:SearchListContainer Name="VendingContents" VerticalExpand="True" Margin="4 4"/>
+         <!-- Footer -->
+        <BoxContainer Orientation="Vertical">
+            <PanelContainer StyleClasses="LowDivider" />
+            <BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
+                <Label Name="LeftFlavorLabel" StyleClasses="WindowFooterText" />
+                <Label Text="{Loc 'vending-machine-flavor-right'}" StyleClasses="WindowFooterText"
+                        HorizontalAlignment="Right" HorizontalExpand="True"  Margin="0 0 5 0" />
+                <TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
+                        VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
+            </BoxContainer>
+        </BoxContainer>
+    </BoxContainer>
+</controls:FancyWindow>
diff --git a/Content.Client/SmartFridge/SmartFridgeMenu.xaml.cs b/Content.Client/SmartFridge/SmartFridgeMenu.xaml.cs
new file mode 100644 (file)
index 0000000..c896e7f
--- /dev/null
@@ -0,0 +1,81 @@
+using System.Linq;
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.SmartFridge;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.XAML;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.SmartFridge;
+
+public record SmartFridgeListData(EntityUid Representative, SmartFridgeEntry Entry, int Amount) : ListData;
+
+[GenerateTypedNameReferences]
+public sealed partial class SmartFridgeMenu : FancyWindow
+{
+    [Dependency] private readonly IEntityManager _entityManager = default!;
+
+    public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
+
+    private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
+
+    public SmartFridgeMenu()
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+
+        VendingContents.SearchBar = SearchBar;
+        VendingContents.DataFilterCondition += DataFilterCondition;
+        VendingContents.GenerateItem += GenerateButton;
+        VendingContents.ItemKeyBindDown += (args, data) => OnItemSelected?.Invoke(args, data);
+    }
+
+    private bool DataFilterCondition(string filter, ListData data)
+    {
+        if (data is not SmartFridgeListData entry)
+            return false;
+
+        if (string.IsNullOrEmpty(filter))
+            return true;
+
+        return entry.Entry.Name.Contains(filter, StringComparison.CurrentCultureIgnoreCase);
+    }
+
+    private void GenerateButton(ListData data, ListContainerButton button)
+    {
+        if (data is not SmartFridgeListData entry)
+            return;
+
+        var label = Loc.GetString("smart-fridge-list-item", ("item", entry.Entry.Name), ("amount", entry.Amount));
+        button.AddChild(new SmartFridgeItem(entry.Representative, label));
+
+        button.ToolTip = label;
+        button.StyleBoxOverride = _styleBox;
+    }
+
+    public void Populate(Entity<SmartFridgeComponent> ent)
+    {
+        var listData = new List<ListData>();
+
+        foreach (var item in ent.Comp.Entries)
+        {
+            if (!ent.Comp.ContainedEntries.TryGetValue(item, out var items) || items.Count == 0)
+            {
+                listData.Add(new SmartFridgeListData(EntityUid.Invalid, item, 0));
+            }
+            else
+            {
+                var representative = _entityManager.GetEntity(items.First());
+                listData.Add(new SmartFridgeListData(representative, item, items.Count));
+            }
+        }
+
+        VendingContents.PopulateList(listData);
+    }
+
+    public void SetFlavorText(string flavor)
+    {
+        LeftFlavorLabel.Text = flavor;
+    }
+}
diff --git a/Content.Client/SmartFridge/SmartFridgeUISystem.cs b/Content.Client/SmartFridge/SmartFridgeUISystem.cs
new file mode 100644 (file)
index 0000000..4068c27
--- /dev/null
@@ -0,0 +1,24 @@
+using Content.Shared.SmartFridge;
+using Robust.Shared.Analyzers;
+
+namespace Content.Client.SmartFridge;
+
+public sealed class SmartFridgeUISystem : EntitySystem
+{
+    [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<SmartFridgeComponent, AfterAutoHandleStateEvent>(OnSmartFridgeAfterState);
+    }
+
+    private void OnSmartFridgeAfterState(Entity<SmartFridgeComponent> ent, ref AfterAutoHandleStateEvent args)
+    {
+        if (!_uiSystem.TryGetOpenUi<SmartFridgeBoundUserInterface>(ent.Owner, SmartFridgeUiKey.Key, out var bui))
+            return;
+
+        bui.Refresh();
+    }
+}
diff --git a/Content.IntegrationTests/Tests/SmartFridge/SmartFridgeInteractionTest.cs b/Content.IntegrationTests/Tests/SmartFridge/SmartFridgeInteractionTest.cs
new file mode 100644 (file)
index 0000000..aa2ebef
--- /dev/null
@@ -0,0 +1,111 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.SmartFridge;
+
+namespace Content.IntegrationTests.Tests.SmartFridge;
+
+public sealed class SmartFridgeInteractionTest : InteractionTest
+{
+    private const string SmartFridgeProtoId = "SmartFridge";
+    private const string SampleItemProtoId = "FoodAmbrosiaVulgaris";
+    private const string SampleDumpableAndInsertableId = "PillCanisterSomething";
+    private const int SampleDumpableCount = 5;
+    private const string SampleDumpableId = "ChemBagSomething";
+
+    [TestPrototypes]
+    private const string TestPrototypes = $@"
+- type: entity
+  parent: PillCanister
+  id: {SampleDumpableAndInsertableId}
+  components:
+  - type: StorageFill
+    contents:
+    - id: PillCopper
+      amount: 5
+
+- type: entity
+  parent: ChemBag
+  id: {SampleDumpableId}
+  components:
+  - type: StorageFill
+    contents:
+    - id: PillCopper
+      amount: 5
+";
+
+    [Test]
+    public async Task InsertAndDispenseItemTest()
+    {
+        await PlaceInHands(SampleItemProtoId);
+
+        await SpawnTarget(SmartFridgeProtoId);
+        var fridge = SEntMan.GetEntity(Target.Value);
+        var component = SEntMan.GetComponent<SmartFridgeComponent>(fridge);
+
+        await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
+        await RunTicks(1);
+
+        // smartfridge spawns with nothing
+        Assert.That(component.Entries, Is.Empty);
+        await InteractUsing(SampleItemProtoId);
+
+        // smartfridge now has items
+        Assert.That(component.Entries, Is.Not.Empty);
+        Assert.That(component.ContainedEntries[component.Entries[0]], Is.Not.Empty);
+
+        // open the UI
+        await Activate();
+        Assert.That(IsUiOpen(SmartFridgeUiKey.Key));
+
+        // dispense an item
+        await SendBui(SmartFridgeUiKey.Key, new SmartFridgeDispenseItemMessage(component.Entries[0]));
+
+        // assert that the listing is still there
+        Assert.That(component.Entries, Is.Not.Empty);
+        // but empty
+        Assert.That(component.ContainedEntries[component.Entries[0]], Is.Empty);
+
+        // and that the thing we dispensed is actually around
+        await AssertEntityLookup(
+            ("APCBasic", 1),
+            (SampleItemProtoId, 1)
+        );
+    }
+
+    [Test]
+    public async Task InsertDumpableInsertableItemTest()
+    {
+        await PlaceInHands(SampleItemProtoId);
+
+        await SpawnTarget(SmartFridgeProtoId);
+        var fridge = SEntMan.GetEntity(Target.Value);
+        var component = SEntMan.GetComponent<SmartFridgeComponent>(fridge);
+
+        await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
+        await RunTicks(1);
+
+        await InteractUsing(SampleDumpableAndInsertableId);
+
+        // smartfridge now has one item only
+        Assert.That(component.Entries, Is.Not.Empty);
+        Assert.That(component.ContainedEntries[component.Entries[0]].Count, Is.EqualTo(1));
+    }
+
+    [Test]
+    public async Task InsertDumpableItemTest()
+    {
+        await PlaceInHands(SampleItemProtoId);
+
+        await SpawnTarget(SmartFridgeProtoId);
+        var fridge = SEntMan.GetEntity(Target.Value);
+        var component = SEntMan.GetComponent<SmartFridgeComponent>(fridge);
+
+        await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
+        await RunTicks(1);
+
+        await InteractUsing(SampleDumpableId);
+
+        // smartfridge now has N items
+        Assert.That(component.Entries, Is.Not.Empty);
+        Assert.That(component.ContainedEntries[component.Entries[0]].Count, Is.EqualTo(SampleDumpableCount));
+    }
+}
index e92552ef6d48c6a8f634a288ac0cd24db62f4e8b..bdf8b5ba07e50f6c060b1a0b984d756712e0fe74 100644 (file)
@@ -19,6 +19,7 @@ using Content.Shared.Movement.Events;
 using Content.Shared.Popups;
 using Content.Shared.Power;
 using Content.Shared.Power.EntitySystems;
+using Content.Shared.Storage.Components;
 using Content.Shared.Throwing;
 using Content.Shared.Verbs;
 using Content.Shared.Whitelist;
@@ -90,6 +91,9 @@ public abstract class SharedDisposalUnitSystem : EntitySystem
         SubscribeLocalEvent<DisposalUnitComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
         SubscribeLocalEvent<DisposalUnitComponent, DragDropTargetEvent>(OnDragDropOn);
         SubscribeLocalEvent<DisposalUnitComponent, ContainerRelayMovementEntityEvent>(OnMovement);
+
+        SubscribeLocalEvent<DisposalUnitComponent, GetDumpableVerbEvent>(OnGetDumpableVerb);
+        SubscribeLocalEvent<DisposalUnitComponent, DumpEvent>(OnDump);
     }
 
     private void AddDisposalAltVerbs(Entity<DisposalUnitComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
@@ -785,4 +789,23 @@ public abstract class SharedDisposalUnitSystem : EntitySystem
         // See also, medical scanner. Also maybe add verbs for entering lockers/body bags?
         args.Verbs.Add(verb);
     }
+
+    private void OnGetDumpableVerb(Entity<DisposalUnitComponent> ent, ref GetDumpableVerbEvent args)
+    {
+        args.Verb = Loc.GetString("dump-disposal-verb-name", ("unit", ent));
+    }
+
+    private void OnDump(Entity<DisposalUnitComponent> ent, ref DumpEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        args.Handled = true;
+        args.PlaySound = true;
+
+        foreach (var entity in args.DumpQueue)
+        {
+            DoInsertDisposalUnit(ent, entity, args.User);
+        }
+    }
 }
index c332064ea38eae767f8d35a11b7b89a820b40524..251043a60d1d1a5bada7c0a9d2f420792e518584 100644 (file)
@@ -3,11 +3,13 @@ using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Interaction;
 using Content.Shared.Storage;
 using Content.Shared.Storage.Components;
+using Robust.Shared.Random;
 
 namespace Content.Shared.Placeable;
 
 public sealed class PlaceableSurfaceSystem : EntitySystem
 {
+    [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
     [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
 
@@ -19,6 +21,8 @@ public sealed class PlaceableSurfaceSystem : EntitySystem
         SubscribeLocalEvent<PlaceableSurfaceComponent, StorageInteractUsingAttemptEvent>(OnStorageInteractUsingAttempt);
         SubscribeLocalEvent<PlaceableSurfaceComponent, StorageAfterOpenEvent>(OnStorageAfterOpen);
         SubscribeLocalEvent<PlaceableSurfaceComponent, StorageAfterCloseEvent>(OnStorageAfterClose);
+        SubscribeLocalEvent<PlaceableSurfaceComponent, GetDumpableVerbEvent>(OnGetDumpableVerb);
+        SubscribeLocalEvent<PlaceableSurfaceComponent, DumpEvent>(OnDump);
     }
 
     public void SetPlaceable(EntityUid uid, bool isPlaceable, PlaceableSurfaceComponent? surface = null)
@@ -87,4 +91,25 @@ public sealed class PlaceableSurfaceSystem : EntitySystem
     {
         SetPlaceable(ent.Owner, false, ent.Comp);
     }
+
+    private void OnGetDumpableVerb(Entity<PlaceableSurfaceComponent> ent, ref GetDumpableVerbEvent args)
+    {
+        args.Verb = Loc.GetString("dump-placeable-verb-name", ("surface", ent));
+    }
+
+    private void OnDump(Entity<PlaceableSurfaceComponent> ent, ref DumpEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        args.Handled = true;
+        args.PlaySound = true;
+
+        var (targetPos, targetRot) = _transformSystem.GetWorldPositionRotation(ent);
+
+        foreach (var entity in args.DumpQueue)
+        {
+            _transformSystem.SetWorldPositionRotation(entity, targetPos + _random.NextVector2Box() / 4, targetRot);
+        }
+    }
 }
diff --git a/Content.Shared/SmartFridge/SmartFridgeComponent.cs b/Content.Shared/SmartFridge/SmartFridgeComponent.cs
new file mode 100644 (file)
index 0000000..db132c5
--- /dev/null
@@ -0,0 +1,99 @@
+using Content.Shared.Whitelist;
+using Robust.Shared.Analyzers;
+using Robust.Shared.Audio;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.SmartFridge;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+[Access(typeof(SmartFridgeSystem))]
+public sealed partial class SmartFridgeComponent : Component
+{
+    /// <summary>
+    /// The container ID that this SmartFridge stores its inventory in
+    /// </summary>
+    [DataField]
+    public string Container = "smart_fridge_inventory";
+
+    /// <summary>
+    /// Whitelist for what entities can be inserted
+    /// </summary>
+    [DataField]
+    public EntityWhitelist? Whitelist;
+
+    /// <summary>
+    /// Blacklist for what entities can be inserted
+    /// </summary>
+    [DataField]
+    public EntityWhitelist? Blacklist;
+
+    /// <summary>
+    /// The sound played on inserting an item into the fridge
+    /// </summary>
+    [DataField]
+    public SoundSpecifier? InsertSound = new SoundCollectionSpecifier("MachineInsert");
+
+    /// <summary>
+    /// A list of entries to display in the UI
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public List<SmartFridgeEntry> Entries = new();
+
+    /// <summary>
+    /// A mapping of smart fridge entries to the actual contained contents
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    [Access(typeof(SmartFridgeSystem), Other = AccessPermissions.ReadExecute)]
+    public Dictionary<SmartFridgeEntry, HashSet<NetEntity>> ContainedEntries = new();
+
+    /// <summary>
+    /// The flavour text displayed at the bottom of the SmartFridge's UI
+    /// </summary>
+    [DataField]
+    public LocId FlavorText = "smart-fridge-request-generic";
+
+    /// <summary>
+    /// Sound that plays when ejecting an item
+    /// </summary>
+    [DataField]
+    public SoundSpecifier SoundVend = new SoundCollectionSpecifier("VendingDispense")
+    {
+        Params = new AudioParams
+        {
+            Volume = -4f,
+            Variation = 0.15f
+        }
+    };
+
+    /// <summary>
+    /// Sound that plays when an item can't be ejected
+    /// </summary>
+    [DataField]
+    public SoundSpecifier SoundDeny = new SoundCollectionSpecifier("VendingDeny");
+}
+
+[Serializable, NetSerializable, DataRecord]
+public record struct SmartFridgeEntry
+{
+    public string Name;
+
+    public SmartFridgeEntry(string name)
+    {
+        Name = name;
+    }
+}
+
+[Serializable, NetSerializable]
+public enum SmartFridgeUiKey : byte
+{
+    Key,
+}
+
+[Serializable, NetSerializable]
+public sealed class SmartFridgeDispenseItemMessage(SmartFridgeEntry entry) : BoundUserInterfaceMessage
+{
+    public SmartFridgeEntry Entry = entry;
+}
diff --git a/Content.Shared/SmartFridge/SmartFridgeSystem.cs b/Content.Shared/SmartFridge/SmartFridgeSystem.cs
new file mode 100644 (file)
index 0000000..1341f6c
--- /dev/null
@@ -0,0 +1,157 @@
+using Content.Shared.Access.Components;
+using Content.Shared.Access.Systems;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.Storage.Components;
+using Content.Shared.Whitelist;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.SmartFridge;
+
+public sealed class SmartFridgeSystem : EntitySystem
+{
+    [Dependency] private readonly AccessReaderSystem _accessReader = default!;
+    [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedContainerSystem _container = default!;
+    [Dependency] private readonly SharedHandsSystem _hands = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<SmartFridgeComponent, InteractUsingEvent>(OnInteractUsing);
+        SubscribeLocalEvent<SmartFridgeComponent, EntRemovedFromContainerMessage>(OnItemRemoved);
+
+        SubscribeLocalEvent<SmartFridgeComponent, GetDumpableVerbEvent>(OnGetDumpableVerb);
+        SubscribeLocalEvent<SmartFridgeComponent, DumpEvent>(OnDump);
+
+        Subs.BuiEvents<SmartFridgeComponent>(SmartFridgeUiKey.Key,
+            sub =>
+            {
+                sub.Event<SmartFridgeDispenseItemMessage>(OnDispenseItem);
+            });
+    }
+
+    private bool DoInsert(Entity<SmartFridgeComponent> ent, EntityUid user, IEnumerable<EntityUid> usedItems, bool playSound)
+    {
+        if (!_container.TryGetContainer(ent, ent.Comp.Container, out var container))
+            return false;
+
+        if (!Allowed(ent, user))
+            return true;
+
+        bool anyInserted = false;
+        foreach (var used in usedItems)
+        {
+            if (!_whitelist.CheckBoth(used, ent.Comp.Blacklist, ent.Comp.Whitelist))
+                continue;
+            anyInserted = true;
+
+            _container.Insert(used, container);
+            var key = new SmartFridgeEntry(Identity.Name(used, EntityManager));
+            if (!ent.Comp.Entries.Contains(key))
+                ent.Comp.Entries.Add(key);
+
+            ent.Comp.ContainedEntries.TryAdd(key, new());
+            var entries = ent.Comp.ContainedEntries[key];
+            if (!entries.Contains(GetNetEntity(used)))
+                entries.Add(GetNetEntity(used));
+
+            Dirty(ent);
+        }
+
+        if (anyInserted && playSound)
+        {
+            _audio.PlayPredicted(ent.Comp.InsertSound, ent, user);
+        }
+
+        return anyInserted;
+    }
+
+    private void OnInteractUsing(Entity<SmartFridgeComponent> ent, ref InteractUsingEvent args)
+    {
+        if (!_hands.CanDrop(args.User, args.Used))
+            return;
+
+        args.Handled = DoInsert(ent, args.User, [args.Used], true);
+    }
+
+    private void OnItemRemoved(Entity<SmartFridgeComponent> ent, ref EntRemovedFromContainerMessage args)
+    {
+        var key = new SmartFridgeEntry(Identity.Name(args.Entity, EntityManager));
+
+        if (ent.Comp.ContainedEntries.TryGetValue(key, out var contained))
+        {
+            contained.Remove(GetNetEntity(args.Entity));
+        }
+
+        Dirty(ent);
+    }
+
+    private bool Allowed(Entity<SmartFridgeComponent> machine, EntityUid user)
+    {
+        if (_accessReader.IsAllowed(user, machine))
+            return true;
+
+        _popup.PopupPredicted(Loc.GetString("smart-fridge-component-try-eject-access-denied"), machine, user);
+        _audio.PlayPredicted(machine.Comp.SoundDeny, machine, user);
+        return false;
+    }
+
+    private void OnDispenseItem(Entity<SmartFridgeComponent> ent, ref SmartFridgeDispenseItemMessage args)
+    {
+        if (!_timing.IsFirstTimePredicted)
+            return;
+
+        if (!Allowed(ent, args.Actor))
+            return;
+
+        if (!ent.Comp.ContainedEntries.TryGetValue(args.Entry, out var contained))
+        {
+            _audio.PlayPredicted(ent.Comp.SoundDeny, ent, args.Actor);
+            _popup.PopupPredicted(Loc.GetString("smart-fridge-component-try-eject-unknown-entry"), ent, args.Actor);
+            return;
+        }
+
+        foreach (var item in contained)
+        {
+            if (!_container.TryRemoveFromContainer(GetEntity(item)))
+                continue;
+
+            _audio.PlayPredicted(ent.Comp.SoundVend, ent, args.Actor);
+            contained.Remove(item);
+            Dirty(ent);
+            return;
+        }
+
+        _audio.PlayPredicted(ent.Comp.SoundDeny, ent, args.Actor);
+        _popup.PopupPredicted(Loc.GetString("smart-fridge-component-try-eject-out-of-stock"), ent, args.Actor);
+    }
+
+    private void OnGetDumpableVerb(Entity<SmartFridgeComponent> ent, ref GetDumpableVerbEvent args)
+    {
+        if (_accessReader.IsAllowed(args.User, ent))
+        {
+            args.Verb = Loc.GetString("dump-smartfridge-verb-name", ("unit", ent));
+        }
+    }
+
+    private void OnDump(Entity<SmartFridgeComponent> ent, ref DumpEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        args.Handled = true;
+        args.PlaySound = true;
+
+        DoInsert(ent, args.User, args.DumpQueue, false);
+    }
+}
index ecf5f17ea99464fb0207e951590a0a4b6bdfe4d6..0c751df2a3b788f0f66bfb6d11ffd2d3ef008a7c 100644 (file)
@@ -33,3 +33,15 @@ public sealed partial class DumpableComponent : Component
     [DataField("multiplier"), AutoNetworkedField]
     public float Multiplier = 1.0f;
 }
+
+/// <summary>
+/// Event raised on Dumpable entities to get the verb for dumping
+/// </summary>
+[ByRefEvent]
+public record struct GetDumpableVerbEvent(EntityUid User, string? Verb);
+
+/// <summary>
+/// Event raised on Dumpable entities to complete the dump
+/// </summary>
+[ByRefEvent]
+public record struct DumpEvent(Queue<EntityUid> DumpQueue, EntityUid User, bool PlaySound, bool Handled);
index d0ad27eee5a3da5a52c02a3158c66951a99d18a8..6c0cc2d65672c01086ec1abc3935e8fffd4da27d 100644 (file)
@@ -1,11 +1,7 @@
 using System.Linq;
-using Content.Shared.Disposal;
-using Content.Shared.Disposal.Components;
-using Content.Shared.Disposal.Unit;
 using Content.Shared.DoAfter;
 using Content.Shared.Interaction;
 using Content.Shared.Item;
-using Content.Shared.Placeable;
 using Content.Shared.Storage.Components;
 using Content.Shared.Verbs;
 using Robust.Shared.Audio.Systems;
@@ -21,7 +17,6 @@ public sealed class DumpableSystem : EntitySystem
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
-    [Dependency] private readonly SharedDisposalUnitSystem _disposalUnitSystem = default!;
     [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
     [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
 
@@ -39,10 +34,12 @@ public sealed class DumpableSystem : EntitySystem
 
     private void OnAfterInteract(EntityUid uid, DumpableComponent component, AfterInteractEvent args)
     {
-        if (!args.CanReach || args.Handled)
+        if (!args.CanReach || args.Handled || args.Target is not { } target)
             return;
 
-        if (!HasComp<DisposalUnitComponent>(args.Target) && !HasComp<PlaceableSurfaceComponent>(args.Target))
+        var evt = new GetDumpableVerbEvent(args.User, null);
+        RaiseLocalEvent(target, ref evt);
+        if (evt.Verb is null)
             return;
 
         if (!TryComp<StorageComponent>(uid, out var storage))
@@ -51,7 +48,7 @@ public sealed class DumpableSystem : EntitySystem
         if (!storage.Container.ContainedEntities.Any())
             return;
 
-        StartDoAfter(uid, args.Target.Value, args.User, component);
+        StartDoAfter(uid, target, args.User, component);
         args.Handled = true;
     }
 
@@ -83,33 +80,22 @@ public sealed class DumpableSystem : EntitySystem
         if (!TryComp<StorageComponent>(uid, out var storage) || !storage.Container.ContainedEntities.Any())
             return;
 
-        if (HasComp<DisposalUnitComponent>(args.Target))
-        {
-            UtilityVerb verb = new()
-            {
-                Act = () =>
-                {
-                    StartDoAfter(uid, args.Target, args.User, dumpable);
-                },
-                Text = Loc.GetString("dump-disposal-verb-name", ("unit", args.Target)),
-                IconEntity = GetNetEntity(uid)
-            };
-            args.Verbs.Add(verb);
-        }
+        var evt = new GetDumpableVerbEvent(args.User, null);
+        RaiseLocalEvent(args.Target, ref evt);
 
-        if (HasComp<PlaceableSurfaceComponent>(args.Target))
+        if (evt.Verb is not { } verbText)
+            return;
+
+        UtilityVerb verb = new()
         {
-            UtilityVerb verb = new()
+            Act = () =>
             {
-                Act = () =>
-                {
-                    StartDoAfter(uid, args.Target, args.User, dumpable);
-                },
-                Text = Loc.GetString("dump-placeable-verb-name", ("surface", args.Target)),
-                IconEntity = GetNetEntity(uid)
-            };
-            args.Verbs.Add(verb);
-        }
+                StartDoAfter(uid, args.Target, args.User, dumpable);
+            },
+            Text = verbText,
+            IconEntity = GetNetEntity(uid)
+        };
+        args.Verbs.Add(verb);
     }
 
     private void StartDoAfter(EntityUid storageUid, EntityUid targetUid, EntityUid userUid, DumpableComponent dumpable)
@@ -141,34 +127,15 @@ public sealed class DumpableSystem : EntitySystem
 
     private void OnDoAfter(EntityUid uid, DumpableComponent component, DumpableDoAfterEvent args)
     {
-        if (args.Handled || args.Cancelled || !TryComp<StorageComponent>(uid, out var storage) || storage.Container.ContainedEntities.Count == 0)
+        if (args.Handled || args.Cancelled || !TryComp<StorageComponent>(uid, out var storage) || storage.Container.ContainedEntities.Count == 0 || args.Args.Target is not { } target)
             return;
 
         var dumpQueue = new Queue<EntityUid>(storage.Container.ContainedEntities);
 
-        var dumped = false;
+        var evt = new DumpEvent(dumpQueue, args.Args.User, false, false);
+        RaiseLocalEvent(target, ref evt);
 
-        if (HasComp<DisposalUnitComponent>(args.Args.Target))
-        {
-            dumped = true;
-
-            foreach (var entity in dumpQueue)
-            {
-                _disposalUnitSystem.DoInsertDisposalUnit(args.Args.Target.Value, entity, args.Args.User);
-            }
-        }
-        else if (HasComp<PlaceableSurfaceComponent>(args.Args.Target))
-        {
-            dumped = true;
-
-            var (targetPos, targetRot) = _transformSystem.GetWorldPositionRotation(args.Args.Target.Value);
-
-            foreach (var entity in dumpQueue)
-            {
-                _transformSystem.SetWorldPositionRotation(entity, targetPos + _random.NextVector2Box() / 4, targetRot);
-            }
-        }
-        else
+        if (!evt.Handled)
         {
             var targetPos = _transformSystem.GetWorldPosition(uid);
 
@@ -177,9 +144,11 @@ public sealed class DumpableSystem : EntitySystem
                 var transform = Transform(entity);
                 _transformSystem.SetWorldPositionRotation(entity, targetPos + _random.NextVector2Box() / 4, _random.NextAngle(), transform);
             }
+
+            return;
         }
 
-        if (dumped)
+        if (evt.PlaySound)
         {
             _audio.PlayPredicted(component.DumpSound, uid, args.User);
         }
diff --git a/Resources/Locale/en-US/smartfridge/smartfridge.ftl b/Resources/Locale/en-US/smartfridge/smartfridge.ftl
new file mode 100644 (file)
index 0000000..e796036
--- /dev/null
@@ -0,0 +1,8 @@
+smart-fridge-component-try-eject-unknown-entry = Invalid selection!
+smart-fridge-component-try-eject-out-of-stock = Out of stock!
+smart-fridge-component-try-eject-access-denied = Access denied!
+smart-fridge-component-search-filter = Search...
+smart-fridge-component-title = SmartFridge
+smart-fridge-list-item = {$item} [{$amount}]
+smart-fridge-request-generic = All sales final
+smart-fridge-request-chemistry = Request refills from chemistry
index 732a45a3b6530dda7134e50653a4a7adb4e30026..867b461f1fb0f704f20d5c077f2283b4921a8007 100644 (file)
@@ -1,3 +1,4 @@
 dump-verb-name = Dump out on ground
 dump-disposal-verb-name = Dump out into {$unit}
 dump-placeable-verb-name = Dump out onto {$surface}
+dump-smartfridge-verb-name = Restock into {$unit}
index 8ea6fee938e2be7ae61b321b70fe3e20bc3b2375..788febc01410338e58302a2d2c2ec93011eb9336 100644 (file)
     - state: smartfridge_door
       map: ["enum.StorageVisualLayers.Door"]
       shader: unshaded
-  - type: EntityStorageVisuals
-    stateBaseClosed: smartfridge
-    stateDoorOpen: smartfridge_open
-    stateDoorClosed: smartfridge_door
   - type: PointLight
     radius: 1.5
     energy: 1.6
     color: "#9dc5c9"
-  - type: EntityStorage
-    isCollidableWhenOpen: true
-    closeSound:
-      path: /Audio/Machines/windoor_open.ogg
-      params:
-        volume: -3
-    openSound:
-      path: /Audio/Machines/windoor_open.ogg
-      params:
-        volume: -3
   - type: ContainerContainer
     containers:
-      entity_storage: !type:Container
+      smart_fridge_inventory: !type:Container
+  - type: LitOnPowered
+  - type: ApcPowerReceiver
+    powerLoad: 200
+  - type: ExtensionCableReceiver
+  - type: SmartFridge
+    whitelist:
+      components:
+      - FitsInDispenser
+      - Pill
+      - Produce
+      - Seed
+      tags:
+      - PillCanister
+      - Bottle
+      - Syringe
+      - ChemDispensable
+  - type: ActivatableUI
+    key: enum.SmartFridgeUiKey.Key
+  - type: ActivatableUIRequiresPower
+  - type: UserInterface
+    interfaces:
+      enum.SmartFridgeUiKey.Key:
+        type: SmartFridgeBoundUserInterface
+  - type: AccessReader
   - type: UseDelay
     delay: 1
   - type: AntiRottingContainer
         Blunt: 5
     soundHit:
       collection: MetalThud
+  - type: ExplosionResistance
+    damageCoefficient: 0.1
+
+- type: entity
+  parent: SmartFridge
+  id: SmartFridgeMedical
+  suffix: Medical
+  components:
+  - type: SmartFridge
+    flavorText: smart-fridge-request-chemistry
+  - type: AccessReader
+    access: [["Medical"]]
index 3c567ab7b33620d4585d22bbdb63e4531b9c825d..45768bfa88c9a3d3436ee961180c143c316213e2 100644 (file)
   id: CargoBeep
   files:
   - /Audio/Effects/Cargo/beep.ogg
+
+- type: soundCollection
+  id: VendingDispense
+  files:
+  - /Audio/Machines/machine_vend.ogg
+
+- type: soundCollection
+  id: VendingDeny
+  files:
+  - /Audio/Machines/custom_deny.ogg
+
+- type: soundCollection
+  id: MachineInsert
+  files:
+  - /Audio/Weapons/Guns/MagIn/revolver_magin.ogg