diff --git a/Content.Client/AME/UI/AMEControllerBoundUserInterface.cs b/Content.Client/AME/UI/AMEControllerBoundUserInterface.cs index 3765d45fa2..aaeaf538f3 100644 --- a/Content.Client/AME/UI/AMEControllerBoundUserInterface.cs +++ b/Content.Client/AME/UI/AMEControllerBoundUserInterface.cs @@ -19,14 +19,9 @@ namespace Content.Client.AME.UI { base.Open(); - _window = new AMEWindow(); + _window = new AMEWindow(this); _window.OnClose += Close; _window.OpenCentered(); - - _window.EjectButton.OnPressed += _ => ButtonPressed(UiButton.Eject); - _window.ToggleInjection.OnPressed += _ => ButtonPressed(UiButton.ToggleInjection); - _window.IncreaseFuelButton.OnPressed += _ => ButtonPressed(UiButton.IncreaseFuel); - _window.DecreaseFuelButton.OnPressed += _ => ButtonPressed(UiButton.DecreaseFuel); } /// @@ -44,7 +39,7 @@ namespace Content.Client.AME.UI _window?.UpdateState(castState); //Update window state } - private void ButtonPressed(UiButton button, int dispenseIndex = -1) + public void ButtonPressed(UiButton button, int dispenseIndex = -1) { SendMessage(new UiButtonPressedMessage(button)); } diff --git a/Content.Client/AME/UI/AMEWindow.cs b/Content.Client/AME/UI/AMEWindow.cs deleted file mode 100644 index e9104e6f67..0000000000 --- a/Content.Client/AME/UI/AMEWindow.cs +++ /dev/null @@ -1,173 +0,0 @@ -using Content.Client.Stylesheets; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.CustomControls; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using static Content.Shared.AME.SharedAMEControllerComponent; -using static Robust.Client.UserInterface.Controls.BoxContainer; - -namespace Content.Client.AME.UI -{ - public class AMEWindow : SS14Window - { - public Label InjectionStatus { get; set; } - public Button EjectButton { get; set; } - public Button ToggleInjection { get; set; } - public Button IncreaseFuelButton { get; set; } - public Button DecreaseFuelButton { get; set; } - public ProgressBar? FuelMeter { get; set; } - public Label FuelAmount { get; set; } - public Label InjectionAmount { get; set; } - public Label CoreCount { get; set; } - - - public AMEWindow() - { - IoCManager.InjectDependencies(this); - - Title = Loc.GetString("ame-window-title"); - - MinSize = SetSize = (250, 250); - - Contents.AddChild(new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - Children = - { - new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - new Label {Text = Loc.GetString("ame-window-engine-status-label") + " "}, - (InjectionStatus = new Label {Text = Loc.GetString("ame-window-engine-injection-status-not-injecting-label")}) - } - }, - new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - (ToggleInjection = new Button {Text = Loc.GetString("ame-window-toggle-injection-button"), StyleClasses = {StyleBase.ButtonOpenBoth}, Disabled = true}), - } - }, - new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - new Label {Text = Loc.GetString("ame-window-fuel-status-label") + " "}, - (FuelAmount = new Label {Text = Loc.GetString("ame-window-fuel-not-inserted-text")}) - } - }, - new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - (EjectButton = new Button {Text = Loc.GetString("ame-window-eject-button"), StyleClasses = {StyleBase.ButtonOpenBoth}, Disabled = true}), - } - }, - new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - new Label {Text = Loc.GetString("ame-window-injection-amount-label") + " "}, - (InjectionAmount = new Label {Text = "0"}) - } - }, - new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - (IncreaseFuelButton = new Button {Text = Loc.GetString("ame-window-increase-fuel-button"), StyleClasses = {StyleBase.ButtonOpenRight}}), - (DecreaseFuelButton = new Button {Text = Loc.GetString("ame-window-decrease-fuel-button"), StyleClasses = {StyleBase.ButtonOpenLeft}}), - } - }, - new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - new Label { Text = Loc.GetString("ame-window-core-count-label") + " "}, - (CoreCount = new Label { Text = "0"}), - } - } - } - }); - } - - /// - /// This searches recursively through all the children of "parent" - /// and sets the Disabled value of any buttons found to "val" - /// - /// The control which childrens get searched - /// The value to which disabled gets set - private void SetButtonDisabledRecursive(Control parent, bool val) - { - foreach (var child in parent.Children) - { - if (child is Button but) - { - but.Disabled = val; - continue; - } - - if (child.Children != null) - { - SetButtonDisabledRecursive(child, val); - } - } - } - - /// - /// Update the UI state when new state data is received from the server. - /// - /// State data sent by the server. - public void UpdateState(BoundUserInterfaceState state) - { - var castState = (AMEControllerBoundUserInterfaceState) state; - - // Disable all buttons if not powered - if (Contents.Children != null) - { - SetButtonDisabledRecursive(Contents, !castState.HasPower); - EjectButton.Disabled = false; - } - - if (!castState.HasFuelJar) - { - EjectButton.Disabled = true; - ToggleInjection.Disabled = true; - FuelAmount.Text = Loc.GetString("ame-window-fuel-not-inserted-text"); - } - else - { - EjectButton.Disabled = false; - ToggleInjection.Disabled = false; - FuelAmount.Text = $"{castState.FuelAmount}"; - } - - if (!castState.IsMaster) - { - ToggleInjection.Disabled = true; - } - - if (!castState.Injecting) - { - InjectionStatus.Text = Loc.GetString("ame-window-engine-injection-status-not-injecting-label") + " "; - } - else - { - InjectionStatus.Text = Loc.GetString("ame-window-engine-injection-status-injecting-label") + " "; - } - - CoreCount.Text = $"{castState.CoreCount}"; - InjectionAmount.Text = $"{castState.InjectionAmount}"; - } - } -} diff --git a/Content.Client/AME/UI/AMEWindow.xaml b/Content.Client/AME/UI/AMEWindow.xaml new file mode 100644 index 0000000000..bc1e7d9ded --- /dev/null +++ b/Content.Client/AME/UI/AMEWindow.xaml @@ -0,0 +1,46 @@ + + + + + + public void InteractHand(IEntity user, IEntity target) { - if (!Get().CanInteract(user)) + if (!_actionBlockerSystem.CanInteract(user)) return; // all interactions should only happen when in range / unobstructed, so no range check is needed @@ -457,7 +457,7 @@ namespace Content.Server.Interaction /// public void TryUseInteraction(IEntity user, IEntity used) { - if (user != null && used != null && Get().CanUse(user)) + if (user != null && used != null && _actionBlockerSystem.CanUse(user)) { UseInteraction(user, used); } @@ -501,7 +501,7 @@ namespace Content.Server.Interaction /// public bool TryThrowInteraction(IEntity user, IEntity item) { - if (user == null || item == null || !Get().CanThrow(user)) return false; + if (user == null || item == null || !_actionBlockerSystem.CanThrow(user)) return false; ThrownInteraction(user, item); return true; @@ -618,7 +618,7 @@ namespace Content.Server.Interaction /// public bool TryDroppedInteraction(IEntity user, IEntity item, bool intentional) { - if (user == null || item == null || !Get().CanDrop(user)) return false; + if (user == null || item == null || !_actionBlockerSystem.CanDrop(user)) return false; DroppedInteraction(user, item, intentional); return true; @@ -726,7 +726,7 @@ namespace Content.Server.Interaction if (!ValidateInteractAndFace(user, coordinates)) return; - if (!Get().CanAttack(user)) + if (!_actionBlockerSystem.CanAttack(user)) return; IEntity? targetEnt = null; diff --git a/Content.Server/Inventory/Components/InventoryComponent.cs b/Content.Server/Inventory/Components/InventoryComponent.cs index 6ba250d52d..8f1d0d7ad1 100644 --- a/Content.Server/Inventory/Components/InventoryComponent.cs +++ b/Content.Server/Inventory/Components/InventoryComponent.cs @@ -460,7 +460,7 @@ namespace Content.Server.Inventory.Components { if (!HasSlot(slot)) { - throw new InvalidOperationException($"Slow '{slot}' does not exist."); + throw new InvalidOperationException($"Slot '{slot}' does not exist."); } ForceUnequip(slot); diff --git a/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs b/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs index 2f49df1945..5507978b53 100644 --- a/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs +++ b/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs @@ -1,24 +1,9 @@ -using System; -using System.Linq; -using System.Threading.Tasks; using Content.Server.Chemistry.Components; -using Content.Server.Hands.Components; -using Content.Server.Items; -using Content.Server.Power.Components; -using Content.Server.UserInterface; -using Content.Shared.Chemistry.Solution; using Content.Shared.Interaction; using Content.Shared.Kitchen.Components; -using Content.Shared.Notification.Managers; -using Content.Shared.Random.Helpers; using Content.Shared.Sound; -using Content.Shared.Tag; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.GameObjects; -using Robust.Shared.Localization; -using Robust.Shared.Player; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; @@ -32,343 +17,30 @@ namespace Content.Server.Kitchen.Components /// it contained, juice an apple and get "apple juice". /// [RegisterComponent] - [ComponentReference(typeof(IActivate))] - public class ReagentGrinderComponent : SharedReagentGrinderComponent, IActivate, IInteractUsing + public class ReagentGrinderComponent : SharedReagentGrinderComponent { - private AudioSystem _audioSystem = default!; - [ViewVariables] private ContainerSlot _beakerContainer = default!; + [ViewVariables] public ContainerSlot BeakerContainer = default!; /// /// Can be null since we won't always have a beaker in the grinder. /// - [ViewVariables] private SolutionContainerComponent? _heldBeaker = default!; + [ViewVariables] public SolutionContainerComponent? HeldBeaker = default!; /// /// Contains the things that are going to be ground or juiced. /// - [ViewVariables] private Container _chamber = default!; + [ViewVariables] public Container Chamber = default!; - [ViewVariables] private bool ChamberEmpty => _chamber.ContainedEntities.Count <= 0; - [ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null; - [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ReagentGrinderUiKey.Key); - - private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered; - - /// - /// Should the BoundUI be told to update? - /// - private bool _uiDirty = true; /// /// Is the machine actively doing something and can't be used right now? /// - private bool _busy = false; + public bool Busy; //YAML serialization vars - [ViewVariables(VVAccess.ReadWrite)] [DataField("chamberCapacity")] private int _storageCap = 16; - [ViewVariables(VVAccess.ReadWrite)] [DataField("workTime")] private int _workTime = 3500; //3.5 seconds, completely arbitrary for now. + [ViewVariables(VVAccess.ReadWrite)] [DataField("chamberCapacity")] public int StorageCap = 16; + [ViewVariables(VVAccess.ReadWrite)] [DataField("workTime")] public int WorkTime = 3500; //3.5 seconds, completely arbitrary for now. [DataField("clickSound")] private SoundSpecifier _clickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg"); [DataField("grindSound")] private SoundSpecifier _grindSound = new SoundPathSpecifier("/Audio/Machines/blender.ogg"); [DataField("juiceSound")] private SoundSpecifier _juiceSound = new SoundPathSpecifier("/Audio/Machines/juicer.ogg"); - - protected override void Initialize() - { - base.Initialize(); - //A slot for the beaker where the grounds/juices will go. - _beakerContainer = - ContainerHelpers.EnsureContainer(Owner, $"{Name}-reagentContainerContainer"); - - //A container for the things that WILL be ground/juiced. Useful for ejecting them instead of deleting them from the hands of the user. - _chamber = - ContainerHelpers.EnsureContainer(Owner, $"{Name}-entityContainerContainer"); - - if (UserInterface != null) - { - UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage; - } - - _audioSystem = EntitySystem.Get(); - } - - public override void HandleMessage(ComponentMessage message, IComponent? component) - { - base.HandleMessage(message, component); - switch (message) - { - case PowerChangedMessage powerChanged: - OnPowerStateChanged(powerChanged); - break; - } - } - - protected override void OnRemove() - { - base.OnRemove(); - if (UserInterface != null) - { - UserInterface.OnReceiveMessage -= UserInterfaceOnReceiveMessage; - } - } - - private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage message) - { - if(_busy) - { - return; - } - - switch(message.Message) - { - case ReagentGrinderGrindStartMessage msg: - if (!Powered) break; - ClickSound(); - DoWork(message.Session.AttachedEntity!, GrinderProgram.Grind); - break; - - case ReagentGrinderJuiceStartMessage msg: - if (!Powered) break; - ClickSound(); - DoWork(message.Session.AttachedEntity!, GrinderProgram.Juice); - break; - - case ReagentGrinderEjectChamberAllMessage msg: - if(!ChamberEmpty) - { - ClickSound(); - for (var i = _chamber.ContainedEntities.Count - 1; i >= 0; i--) - { - EjectSolid(_chamber.ContainedEntities.ElementAt(i).Uid); - } - } - break; - - case ReagentGrinderEjectChamberContentMessage msg: - if (!ChamberEmpty) - { - EjectSolid(msg.EntityID); - ClickSound(); - _uiDirty = true; - } - break; - - case ReagentGrinderEjectBeakerMessage msg: - ClickSound(); - EjectBeaker(message.Session.AttachedEntity); - //EjectBeaker will dirty the UI for us, we don't have to do it explicitly here. - break; - } - } - - private void OnPowerStateChanged(PowerChangedMessage e) - { - _uiDirty = true; - } - - private void ClickSound() - { - if(_clickSound.TryGetSound(out var sound)) - SoundSystem.Play(Filter.Pvs(Owner), sound, Owner, AudioParams.Default.WithVolume(-2f)); - } - - private void SetAppearance() - { - if (Owner.TryGetComponent(out AppearanceComponent? appearance)) - { - appearance.SetData(ReagentGrinderVisualState.BeakerAttached, HasBeaker); - } - } - - public void OnUpdate() - { - if(_uiDirty) - { - UpdateInterface(); - _uiDirty = false; - } - } - - // This doesn't check for UI dirtiness so handle that when calling this. - private void UpdateInterface() - { - bool canJuice = false; - bool canGrind = false; - if (HasBeaker) - { - foreach (var entity in _chamber.ContainedEntities) - { - if (!canJuice && entity.HasComponent()) canJuice = true; - if (!canGrind && entity.HasTag("Grindable")) canGrind = true; - if (canJuice && canGrind) break; - } - } - - UserInterface?.SetState(new ReagentGrinderInterfaceState - ( - _busy, - HasBeaker, - Powered, - canJuice, - canGrind, - _chamber.ContainedEntities.Select(item => item.Uid).ToArray(), - //Remember the beaker can be null! - _heldBeaker?.Solution.Contents.ToArray() - )); - _uiDirty = false; - } - - private void EjectSolid(EntityUid entityID) - { - if (_busy) - return; - - if (Owner.EntityManager.TryGetEntity(entityID, out var entity)) - { - _chamber.Remove(entity); - - //Give the ejected entity a tiny bit of offset so each one is apparent in case of a big stack, - //but (hopefully) not enough to clip it through a solid (wall). - entity.RandomOffset(0.4f); - } - _uiDirty = true; - } - - /// - /// Tries to eject whatever is in the beaker slot. Puts the item in the user's hands or failing that on top - /// of the grinder. - /// - private void EjectBeaker(IEntity? user) - { - if (!HasBeaker || _heldBeaker == null || _busy) - return; - - var beaker = _beakerContainer.ContainedEntity; - if(beaker is null) - return; - - _beakerContainer.Remove(beaker); - - if (user == null || !user.TryGetComponent(out var hands) || !_heldBeaker.Owner.TryGetComponent(out var item)) - return; - hands.PutInHandOrDrop(item); - - _heldBeaker = null; - _uiDirty = true; - SetAppearance(); - } - - void IActivate.Activate(ActivateEventArgs eventArgs) - { - if (!eventArgs.User.TryGetComponent(out ActorComponent? actor)) - { - return; - } - _uiDirty = true; - UserInterface?.Toggle(actor.PlayerSession); - } - - async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) - { - if (!eventArgs.User.TryGetComponent(out IHandsComponent? hands)) - { - Owner.PopupMessage(eventArgs.User, Loc.GetString("reagent-grinder-component-interact-using-no-hands")); - return true; - } - - IEntity heldEnt = eventArgs.Using; - - //First, check if user is trying to insert a beaker. - //No promise it will be a beaker right now, but whatever. - //Maybe this should whitelist "beaker" in the prototype id of heldEnt? - if(heldEnt.TryGetComponent(out SolutionContainerComponent? beaker) && beaker.Capabilities.HasFlag(SolutionContainerCaps.FitsInDispenser)) - { - _beakerContainer.Insert(heldEnt); - _heldBeaker = beaker; - _uiDirty = true; - //We are done, return. Insert the beaker and exit! - SetAppearance(); - ClickSound(); - return true; - } - - //Next, see if the user is trying to insert something they want to be ground/juiced. - if(!heldEnt.HasTag("Grindable") && !heldEnt.TryGetComponent(out JuiceableComponent? juice)) - { - //Entity did NOT pass the whitelist for grind/juice. - //Wouldn't want the clown grinding up the Captain's ID card now would you? - //Why am I asking you? You're biased. - return false; - } - - //Cap the chamber. Don't want someone putting in 500 entities and ejecting them all at once. - //Maybe I should have done that for the microwave too? - if (_chamber.ContainedEntities.Count >= _storageCap) - { - return false; - } - - if (!_chamber.Insert(heldEnt)) - return false; - - _uiDirty = true; - return true; - } - - /// - /// The wzhzhzh of the grinder. Processes the contents of the grinder and puts the output in the beaker. - /// - /// true for wanting to juice, false for wanting to grind. - private async void DoWork(IEntity user, GrinderProgram program) - { - //Have power, are we busy, chamber has anything to grind, a beaker for the grounds to go? - if(!Powered || _busy || ChamberEmpty || !HasBeaker || _heldBeaker == null) - { - return; - } - - _busy = true; - - UserInterface?.SendMessage(new ReagentGrinderWorkStartedMessage(program)); - switch (program) - { - case GrinderProgram.Grind: - if(_grindSound.TryGetSound(out var grindSound)) - SoundSystem.Play(Filter.Pvs(Owner), grindSound, Owner, AudioParams.Default); - //Get each item inside the chamber and get the reagents it contains. Transfer those reagents to the beaker, given we have one in. - Owner.SpawnTimer(_workTime, (Action) (() => - { - foreach (var item in _chamber.ContainedEntities.ToList()) - { - if (!item.HasTag("Grindable")) continue; - if (!item.TryGetComponent(out var solution)) continue; - if (_heldBeaker.CurrentVolume + solution.CurrentVolume > _heldBeaker.MaxVolume) continue; - _heldBeaker.TryAddSolution(solution.Solution); - solution.RemoveAllSolution(); - item.Delete(); - } - - _busy = false; - _uiDirty = true; - UserInterface?.SendMessage(new ReagentGrinderWorkCompleteMessage()); - })); - break; - - case GrinderProgram.Juice: - if(_juiceSound.TryGetSound(out var juiceSound)) - SoundSystem.Play(Filter.Pvs(Owner), juiceSound, Owner, AudioParams.Default); - Owner.SpawnTimer(_workTime, (Action) (() => - { - foreach (var item in _chamber.ContainedEntities.ToList()) - { - if (!item.TryGetComponent(out var juiceMe)) continue; - if (_heldBeaker.CurrentVolume + juiceMe.JuiceResultSolution.TotalVolume > _heldBeaker.MaxVolume) continue; - _heldBeaker.TryAddSolution(juiceMe.JuiceResultSolution); - item.Delete(); - } - UserInterface?.SendMessage(new ReagentGrinderWorkCompleteMessage()); - _busy = false; - _uiDirty = true; - })); - break; - } - } } } diff --git a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs index 95f3dbe5f8..c58c5b32ee 100644 --- a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs @@ -1,19 +1,314 @@ -using Content.Server.Kitchen.Components; +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Server.Chemistry.Components; +using Content.Server.Hands.Components; +using Content.Server.Items; +using Content.Server.Kitchen.Components; +using Content.Server.Power.Components; +using Content.Server.UserInterface; +using Content.Shared.Chemistry.Solution; +using Content.Shared.Interaction; +using Content.Shared.Kitchen.Components; +using Content.Shared.Notification.Managers; +using Content.Shared.Random.Helpers; +using Content.Shared.Tag; using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Containers; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Player; +using Robust.Shared.Utility; namespace Content.Server.Kitchen.EntitySystems { [UsedImplicitly] internal sealed class ReagentGrinderSystem : EntitySystem { + [Dependency] private readonly IEntityManager _entityManager = default!; + + private Queue _uiUpdateQueue = new (); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent((_, component, _) => EnqueueUiUpdate(component)); + SubscribeLocalEvent(OnInteractHand); + SubscribeLocalEvent(OnInteractUsing); + } + + private void OnInteractUsing(EntityUid uid, ReagentGrinderComponent component, InteractUsingEvent args) + { + if(args.Handled) return; + + if (!args.User.TryGetComponent(out IHandsComponent? hands)) + { + component.Owner.PopupMessage(args.User, Loc.GetString("reagent-grinder-component-interact-using-no-hands")); + args.Handled = true; + return; + } + + IEntity heldEnt = args.Used; + + //First, check if user is trying to insert a beaker. + //No promise it will be a beaker right now, but whatever. + //Maybe this should whitelist "beaker" in the prototype id of heldEnt? + if(heldEnt.TryGetComponent(out SolutionContainerComponent? beaker) && beaker.Capabilities.HasFlag(SolutionContainerCaps.FitsInDispenser)) + { + component.BeakerContainer.Insert(heldEnt); + component.HeldBeaker = beaker; + EnqueueUiUpdate(component); + //We are done, return. Insert the beaker and exit! + if (component.Owner.TryGetComponent(out AppearanceComponent? appearance)) + { + appearance.SetData(SharedReagentGrinderComponent.ReagentGrinderVisualState.BeakerAttached, component.BeakerContainer.ContainedEntity != null); + } + ClickSound(component); + args.Handled = true; + return; + } + + //Next, see if the user is trying to insert something they want to be ground/juiced. + if(!heldEnt.HasTag("Grindable") && !heldEnt.TryGetComponent(out JuiceableComponent? juice)) + { + //Entity did NOT pass the whitelist for grind/juice. + //Wouldn't want the clown grinding up the Captain's ID card now would you? + //Why am I asking you? You're biased. + return; + } + + //Cap the chamber. Don't want someone putting in 500 entities and ejecting them all at once. + //Maybe I should have done that for the microwave too? + if (component.Chamber.ContainedEntities.Count >= component.StorageCap) + { + return; + } + + if (!component.Chamber.Insert(heldEnt)) + { + return; + } + + EnqueueUiUpdate(component); + args.Handled = true; + } + + private void OnInteractHand(EntityUid uid, ReagentGrinderComponent component, InteractHandEvent args) + { + if (args.Handled) return; + + if (!args.User.TryGetComponent(out ActorComponent? actor)) + { + return; + } + EnqueueUiUpdate(component); + component.Owner.GetUIOrNull(SharedReagentGrinderComponent.ReagentGrinderUiKey.Key)?.Toggle(actor.PlayerSession); + args.Handled = true; + } + + private void EnqueueUiUpdate(ReagentGrinderComponent component) + { + if(!_uiUpdateQueue.Contains(component)) _uiUpdateQueue.Enqueue(component); + } + + private void OnComponentInit(EntityUid uid, ReagentGrinderComponent component, ComponentInit args) + { + EnqueueUiUpdate(component); + + //A slot for the beaker where the grounds/juices will go. + component.BeakerContainer = + ContainerHelpers.EnsureContainer(component.Owner, $"{component.Name}-reagentContainerContainer"); + + //A container for the things that WILL be ground/juiced. Useful for ejecting them instead of deleting them from the hands of the user. + component.Chamber = + ContainerHelpers.EnsureContainer(component.Owner, $"{component.Name}-entityContainerContainer"); + + var bui = component.Owner.GetUIOrNull(SharedReagentGrinderComponent.ReagentGrinderUiKey.Key); + if (bui != null) + { + bui.OnReceiveMessage += msg => OnUIMessageReceived(uid, component, msg); + } + } + + private void OnUIMessageReceived(EntityUid uid, ReagentGrinderComponent component, + ServerBoundUserInterfaceMessage message) + { + if(component.Busy) + { + return; + } + + switch(message.Message) + { + case SharedReagentGrinderComponent.ReagentGrinderGrindStartMessage msg: + if (!component.Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || !receiver.Powered) break; + ClickSound(component); + DoWork(component, message.Session.AttachedEntity!, SharedReagentGrinderComponent.GrinderProgram.Grind); + break; + + case SharedReagentGrinderComponent.ReagentGrinderJuiceStartMessage msg: + if (!component.Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver2) || !receiver2.Powered) break; + ClickSound(component); + DoWork(component, message.Session.AttachedEntity!, SharedReagentGrinderComponent.GrinderProgram.Juice); + break; + + case SharedReagentGrinderComponent.ReagentGrinderEjectChamberAllMessage msg: + if(component.Chamber.ContainedEntities.Count > 0) + { + ClickSound(component); + for (var i = component.Chamber.ContainedEntities.Count - 1; i >= 0; i--) + { + var entity = component.Chamber.ContainedEntities[i]; + component.Chamber.Remove(entity); + entity.RandomOffset(0.4f); + } + EnqueueUiUpdate(component); + } + break; + + case SharedReagentGrinderComponent.ReagentGrinderEjectChamberContentMessage msg: + if (component.Chamber.ContainedEntities.TryFirstOrDefault(x => x.Uid == msg.EntityID, out var ent)) + { + component.Chamber.Remove(ent); + ent.RandomOffset(0.4f); + EnqueueUiUpdate(component); + ClickSound(component); + } + break; + + case SharedReagentGrinderComponent.ReagentGrinderEjectBeakerMessage msg: + ClickSound(component); + EjectBeaker(component, message.Session.AttachedEntity); + EnqueueUiUpdate(component); + break; + } + } + public override void Update(float frameTime) { base.Update(frameTime); - foreach (var comp in ComponentManager.EntityQuery(true)) + + while (_uiUpdateQueue.TryDequeue(out var comp)) { - comp.OnUpdate(); + bool canJuice = false; + bool canGrind = false; + if (comp.BeakerContainer.ContainedEntity != null) + { + foreach (var entity in comp.Chamber.ContainedEntities) + { + if (!canJuice && entity.HasComponent()) canJuice = true; + if (!canGrind && entity.HasTag("Grindable")) canGrind = true; + if (canJuice && canGrind) break; + } + } + + comp.Owner.GetUIOrNull(SharedReagentGrinderComponent.ReagentGrinderUiKey.Key)?.SetState(new ReagentGrinderInterfaceState + ( + comp.Busy, + comp.BeakerContainer.ContainedEntity != null, + comp.Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) && receiver.Powered, + canJuice, + canGrind, + comp.Chamber.ContainedEntities.Select(item => item.Uid).ToArray(), + //Remember the beaker can be null! + comp.HeldBeaker?.Solution.Contents.ToArray() + )); } } + + /// + /// Tries to eject whatever is in the beaker slot. Puts the item in the user's hands or failing that on top + /// of the grinder. + /// + private void EjectBeaker(ReagentGrinderComponent component, IEntity? user) + { + if (component.BeakerContainer.ContainedEntity == null || component.HeldBeaker == null || component.Busy) + return; + + var beaker = component.BeakerContainer.ContainedEntity; + if(beaker is null) + return; + + component.BeakerContainer.Remove(beaker); + + if (user == null || !user.TryGetComponent(out var hands) || !component.HeldBeaker.Owner.TryGetComponent(out var item)) + return; + hands.PutInHandOrDrop(item); + + component.HeldBeaker = null; + EnqueueUiUpdate(component); + if (component.Owner.TryGetComponent(out AppearanceComponent? appearance)) + { + appearance.SetData(SharedReagentGrinderComponent.ReagentGrinderVisualState.BeakerAttached, component.BeakerContainer.ContainedEntity != null); + } + } + + /// + /// The wzhzhzh of the grinder. Processes the contents of the grinder and puts the output in the beaker. + /// + /// true for wanting to juice, false for wanting to grind. + private void DoWork(ReagentGrinderComponent component, IEntity user, SharedReagentGrinderComponent.GrinderProgram program) + { + //Have power, are we busy, chamber has anything to grind, a beaker for the grounds to go? + if(!component.Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || !receiver.Powered || component.Busy || component.Chamber.ContainedEntities.Count <= 0 || component.BeakerContainer.ContainedEntity == null || component.HeldBeaker == null) + { + return; + } + + component.Busy = true; + + var bui = component.Owner.GetUIOrNull(SharedReagentGrinderComponent.ReagentGrinderUiKey.Key); + bui?.SendMessage(new SharedReagentGrinderComponent.ReagentGrinderWorkStartedMessage(program)); + switch (program) + { + case SharedReagentGrinderComponent.GrinderProgram.Grind: + SoundSystem.Play(Filter.Pvs(component.Owner), "/Audio/Machines/blender.ogg", component.Owner, AudioParams.Default); + //Get each item inside the chamber and get the reagents it contains. Transfer those reagents to the beaker, given we have one in. + component.Owner.SpawnTimer(component.WorkTime, (Action) (() => + { + foreach (var item in component.Chamber.ContainedEntities.ToList()) + { + if (!item.HasTag("Grindable")) continue; + if (!item.TryGetComponent(out var solution)) continue; + if (component.HeldBeaker.CurrentVolume + solution.CurrentVolume > component.HeldBeaker.MaxVolume) continue; + component.HeldBeaker.TryAddSolution(solution.Solution); + solution.RemoveAllSolution(); + item.Delete(); + } + + component.Busy = false; + EnqueueUiUpdate(component); + bui?.SendMessage(new SharedReagentGrinderComponent.ReagentGrinderWorkCompleteMessage()); + })); + break; + + case SharedReagentGrinderComponent.GrinderProgram.Juice: + SoundSystem.Play(Filter.Pvs(component.Owner), "/Audio/Machines/juicer.ogg", component.Owner, AudioParams.Default); + component.Owner.SpawnTimer(component.WorkTime, (Action) (() => + { + foreach (var item in component.Chamber.ContainedEntities.ToList()) + { + if (!item.TryGetComponent(out var juiceMe)) continue; + if (component.HeldBeaker.CurrentVolume + juiceMe.JuiceResultSolution.TotalVolume > component.HeldBeaker.MaxVolume) continue; + component.HeldBeaker.TryAddSolution(juiceMe.JuiceResultSolution); + item.Delete(); + } + bui?.SendMessage(new SharedReagentGrinderComponent.ReagentGrinderWorkCompleteMessage()); + component.Busy = false; + EnqueueUiUpdate(component); + })); + break; + } + } + + private void ClickSound(ReagentGrinderComponent component) + { + SoundSystem.Play(Filter.Pvs(component.Owner), "/Audio/Machines/machine_switch.ogg", component.Owner, AudioParams.Default.WithVolume(-2f)); + } } } diff --git a/Content.Server/Light/Components/EmergencyLightComponent.cs b/Content.Server/Light/Components/EmergencyLightComponent.cs index 5d7fd2a933..b5df55fb28 100644 --- a/Content.Server/Light/Components/EmergencyLightComponent.cs +++ b/Content.Server/Light/Components/EmergencyLightComponent.cs @@ -142,7 +142,7 @@ namespace Content.Server.Light.Components void IExamine.Examine(FormattedMessage message, bool inDetailsRange) { - message.AddMarkup(Loc.GetString("emergency-light-component-on-examine",("batteryStateText", BatteryStateText[State]))); + message.AddMarkup(Loc.GetString("emergency-light-component-on-examine",("batteryStateText", Loc.GetString(BatteryStateText[State])))); } public enum EmergencyLightState diff --git a/Content.Server/Light/Components/ExpendableLightComponent.cs b/Content.Server/Light/Components/ExpendableLightComponent.cs index 0a834773e1..39671b0e86 100644 --- a/Content.Server/Light/Components/ExpendableLightComponent.cs +++ b/Content.Server/Light/Components/ExpendableLightComponent.cs @@ -56,7 +56,7 @@ namespace Content.Server.Light.Components /// private bool TryActivate() { - if (!Activated) + if (!Activated && CurrentState == ExpendableLightState.BrandNew) { if (Owner.TryGetComponent(out var item)) { diff --git a/Content.Server/Lock/LockComponent.cs b/Content.Server/Lock/LockComponent.cs new file mode 100644 index 0000000000..966548e607 --- /dev/null +++ b/Content.Server/Lock/LockComponent.cs @@ -0,0 +1,64 @@ +using Content.Server.Lock; +using Content.Shared.ActionBlocker; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Helpers; +using Content.Shared.Sound; +using Content.Shared.Verbs; +using Robust.Shared.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.ViewVariables; + +namespace Content.Server.Storage.Components +{ + /// + /// Allows locking/unlocking, with access determined by AccessReader + /// + [RegisterComponent] + public class LockComponent : Component + { + public override string Name => "Lock"; + + [ViewVariables(VVAccess.ReadWrite)] [DataField("locked")] public bool Locked { get; set; } = true; + [ViewVariables(VVAccess.ReadWrite)] [DataField("unlockingSound")] public SoundSpecifier? UnlockSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/door_lock_off.ogg"); + [ViewVariables(VVAccess.ReadWrite)] [DataField("lockingSound")] public SoundSpecifier? LockSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/door_lock_off.ogg"); + + [Verb] + private sealed class ToggleLockVerb : Verb + { + protected override void GetData(IEntity user, LockComponent component, VerbData data) + { + if (!EntitySystem.Get().CanInteract(user) || + component.Owner.TryGetComponent(out EntityStorageComponent? entityStorageComponent) && entityStorageComponent.Open) + { + data.Visibility = VerbVisibility.Invisible; + return; + } + + data.Text = Loc.GetString(component.Locked ? "toggle-lock-verb-unlock" : "toggle-lock-verb-lock"); + } + + protected override void Activate(IEntity user, LockComponent component) + { + // Do checks + if (!EntitySystem.Get().CanInteract(user) || + !user.InRangeUnobstructed(component)) + { + return; + } + + // Call relevant entity system + var lockSystem = user.EntityManager.EntitySysManager.GetEntitySystem(); + var eventData = new ActivateInWorldEvent(user, component.Owner); + if (component.Locked) + { + lockSystem.DoUnlock(component, eventData); + } + else + { + lockSystem.DoLock(component, eventData); + } + } + } + } +} diff --git a/Content.Server/Lock/LockSystem.cs b/Content.Server/Lock/LockSystem.cs new file mode 100644 index 0000000000..063ac1656a --- /dev/null +++ b/Content.Server/Lock/LockSystem.cs @@ -0,0 +1,116 @@ +using Content.Server.Access.Components; +using Content.Server.Storage.Components; +using Content.Shared.Examine; +using Content.Shared.Interaction; +using Content.Shared.Notification.Managers; +using Content.Shared.Storage; +using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Player; + +namespace Content.Server.Lock +{ + /// + /// Handles (un)locking and examining of Lock components + /// + [UsedImplicitly] + public class LockSystem : EntitySystem + { + /// + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnActivated); + SubscribeLocalEvent(OnExamined); + } + + private void OnStartup(EntityUid eUI, LockComponent lockComp, ComponentStartup args) + { + if (lockComp.Owner.TryGetComponent(out AppearanceComponent? appearance)) + { + appearance.SetData(StorageVisuals.CanLock, true); + } + } + + private void OnActivated(EntityUid eUI, LockComponent lockComp, ActivateInWorldEvent args) + { + // Only attempt an unlock by default on Activate + if (lockComp.Locked) + { + DoUnlock(lockComp, args); + } + } + + private void OnExamined(EntityUid eUI, LockComponent lockComp, ExaminedEvent args) + { + args.Message.AddText("\n"); + args.Message.AddText(Loc.GetString(lockComp.Locked + ? "lock-comp-on-examined-is-locked" + : "lock-comp-on-examined-is-unlocked", + ("entityName", lockComp.Owner.Name))); + } + + public void DoLock(LockComponent lockComp, ActivateInWorldEvent args) + { + if (!HasUserAccess(lockComp, args.User)) + { + return; + } + + lockComp.Owner.PopupMessage(args.User, Loc.GetString("lock-comp-do-lock-success", ("entityName",lockComp.Owner.Name))); + lockComp.Locked = true; + if(lockComp.LockSound != null) + { + SoundSystem.Play(Filter.Pvs(lockComp.Owner), lockComp.LockSound.GetSound(), lockComp.Owner, AudioParams.Default.WithVolume(-5)); + } + + if (lockComp.Owner.TryGetComponent(out AppearanceComponent? appearanceComp)) + { + appearanceComp.SetData(StorageVisuals.Locked, true); + } + + args.Handled = true; + } + + public void DoUnlock(LockComponent lockComp, ActivateInWorldEvent args ) + { + if (!HasUserAccess(lockComp, args.User)) + { + return; + } + + lockComp.Owner.PopupMessage(args.User, Loc.GetString("lock-comp-do-unlock-success", ("entityName", lockComp.Owner.Name))); + lockComp.Locked = false; + if(lockComp.UnlockSound != null) + { + SoundSystem.Play(Filter.Pvs(lockComp.Owner), lockComp.UnlockSound.GetSound(), lockComp.Owner, AudioParams.Default.WithVolume(-5)); + } + + if (lockComp.Owner.TryGetComponent(out AppearanceComponent? appearanceComp)) + { + appearanceComp.SetData(StorageVisuals.Locked, false); + } + + // To stop EntityStorageComponent from opening right after the container gets unlocked + args.Handled = true; + } + + private static bool HasUserAccess(LockComponent lockComp, IEntity user) + { + if (lockComp.Owner.TryGetComponent(out AccessReader? reader)) + { + if (!reader.IsAllowed(user)) + { + lockComp.Owner.PopupMessage(user, Loc.GetString("lock-comp-has-user-access-fail")); + return false; + } + } + + return true; + } + } +} diff --git a/Content.Server/Nutrition/Components/FoodContainerComponent.cs b/Content.Server/Nutrition/Components/FoodContainerComponent.cs deleted file mode 100644 index 3854928bca..0000000000 --- a/Content.Server/Nutrition/Components/FoodContainerComponent.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Content.Server.Hands.Components; -using Content.Server.Items; -using Content.Shared.Interaction; -using Content.Shared.Nutrition.Components; -using Robust.Server.GameObjects; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Random; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Utility; -using Robust.Shared.ViewVariables; - -namespace Content.Server.Nutrition.Components -{ - /// - /// This container acts as a master object for things like Pizza, which holds slices. - /// - /// TODO: Perhaps implement putting food back (pizza boxes) but that really isn't mandatory. - /// This doesn't even need to have an actual Container for right now. - [RegisterComponent] - public sealed class FoodContainer : SharedFoodContainerComponent, IUse - { - [Dependency] private readonly IRobustRandom _random = default!; - public override string Name => "FoodContainer"; - - private AppearanceComponent? _appearance; - [DataField("prototypes")] - private Dictionary? _prototypes = default; - [DataField("capacity")] - private uint _capacity = 5; - - public int Capacity => (int)_capacity; - [ViewVariables] - public int Count => _count; - - private int _count = 0; - - protected override void Initialize() - { - base.Initialize(); - Owner.TryGetComponent(out _appearance); - _count = Capacity; - UpdateAppearance(); - - } - - bool IUse.UseEntity(UseEntityEventArgs eventArgs) - { - if (!eventArgs.User.TryGetComponent(out HandsComponent? handsComponent)) - { - return false; - } - - var itemToSpawn = Owner.EntityManager.SpawnEntity(GetRandomPrototype(), Owner.Transform.Coordinates); - handsComponent.PutInHandOrDrop(itemToSpawn.GetComponent()); - _count--; - if (_count < 1) - { - Owner.Delete(); - return false; - } - - return true; - } - - private string? GetRandomPrototype() - { - var defaultProto = _prototypes?.Keys.FirstOrDefault(); - - if (defaultProto == null) - { - return null; - } - - DebugTools.AssertNotNull(_prototypes); - - if (_prototypes!.Count == 1) - { - return defaultProto; - } - - var probResult = _random.Next(0, 100); - var total = 0; - foreach (var item in _prototypes) - { - total += item.Value; - if (probResult < total) - { - return item.Key; - } - } - - return defaultProto; - } - - private void UpdateAppearance() - { - _appearance?.SetData(FoodContainerVisuals.Current, Count); - } - } -} diff --git a/Content.Server/Pointing/EntitySystems/PointingSystem.cs b/Content.Server/Pointing/EntitySystems/PointingSystem.cs index f31155ff11..e855c9270c 100644 --- a/Content.Server/Pointing/EntitySystems/PointingSystem.cs +++ b/Content.Server/Pointing/EntitySystems/PointingSystem.cs @@ -31,6 +31,7 @@ namespace Content.Server.Pointing.EntitySystems [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; private static readonly TimeSpan PointDelay = TimeSpan.FromSeconds(0.5f); @@ -112,7 +113,7 @@ namespace Content.Server.Pointing.EntitySystems return false; } - if (EntitySystem.Get().CanChangeDirection(player)) + if (_actionBlockerSystem.CanChangeDirection(player)) { var diff = coords.ToMapPos(EntityManager) - player.Transform.MapPosition.Position; if (diff.LengthSquared > 0.01f) diff --git a/Content.Server/Power/EntitySystems/BatterySystem.cs b/Content.Server/Power/EntitySystems/BatterySystem.cs index 44126d6810..e97b007861 100644 --- a/Content.Server/Power/EntitySystems/BatterySystem.cs +++ b/Content.Server/Power/EntitySystems/BatterySystem.cs @@ -11,23 +11,25 @@ namespace Content.Server.Power.EntitySystems { base.Initialize(); - SubscribeLocalEvent(PreSync); - SubscribeLocalEvent(PostSync); + SubscribeLocalEvent(PreSync); + SubscribeLocalEvent(PostSync); } - private void PreSync(EntityUid uid, BatteryComponent component, NetworkBatteryPreSync args) + private void PreSync(NetworkBatteryPreSync ev) { - var networkBattery = ComponentManager.GetComponent(uid); - - networkBattery.NetworkBattery.Capacity = component.MaxCharge; - networkBattery.NetworkBattery.CurrentStorage = component.CurrentCharge; + foreach (var (bat, netBat) in ComponentManager.EntityQuery()) + { + netBat.NetworkBattery.Capacity = bat.MaxCharge; + netBat.NetworkBattery.CurrentStorage = bat.CurrentCharge; + } } - private void PostSync(EntityUid uid, BatteryComponent component, NetworkBatteryPostSync args) + private void PostSync(NetworkBatteryPostSync ev) { - var networkBattery = ComponentManager.GetComponent(uid); - - component.CurrentCharge = networkBattery.NetworkBattery.CurrentStorage; + foreach (var (bat, netBat) in ComponentManager.EntityQuery()) + { + bat.CurrentCharge = netBat.NetworkBattery.CurrentStorage; + } } public override void Update(float frameTime) diff --git a/Content.Server/Power/EntitySystems/PowerNetSystem.cs b/Content.Server/Power/EntitySystems/PowerNetSystem.cs index 3b7f81fb25..84b65289b5 100644 --- a/Content.Server/Power/EntitySystems/PowerNetSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerNetSystem.cs @@ -175,19 +175,13 @@ namespace Content.Server.Power.EntitySystems } // Synchronize batteries - foreach (var battery in ComponentManager.EntityQuery()) - { - RaiseLocalEvent(battery.Owner.Uid, new NetworkBatteryPreSync()); - } + RaiseLocalEvent(new NetworkBatteryPreSync()); // Run power solver. _solver.Tick(frameTime, _powerState); // Synchronize batteries, the other way around. - foreach (var battery in ComponentManager.EntityQuery()) - { - RaiseLocalEvent(battery.Owner.Uid, new NetworkBatteryPostSync()); - } + RaiseLocalEvent(new NetworkBatteryPostSync()); // Send events where necessary. { @@ -313,7 +307,7 @@ namespace Content.Server.Power.EntitySystems /// Raised before power network simulation happens, to synchronize battery state from /// components like into . /// - public sealed class NetworkBatteryPreSync : EntityEventArgs + public struct NetworkBatteryPreSync { } @@ -321,7 +315,7 @@ namespace Content.Server.Power.EntitySystems /// Raised after power network simulation happens, to synchronize battery charge changes from /// to components like . /// - public sealed class NetworkBatteryPostSync : EntityEventArgs + public struct NetworkBatteryPostSync { } diff --git a/Content.Server/Storage/Components/EntityStorageComponent.cs b/Content.Server/Storage/Components/EntityStorageComponent.cs index 3ee411fd40..f8b63b6bbd 100644 --- a/Content.Server/Storage/Components/EntityStorageComponent.cs +++ b/Content.Server/Storage/Components/EntityStorageComponent.cs @@ -25,7 +25,6 @@ using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Maths; using Robust.Shared.Physics; -using Robust.Shared.Physics.Broadphase; using Robust.Shared.Player; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Timing; @@ -132,7 +131,8 @@ namespace Content.Server.Storage.Components private bool _beingWelded; [ViewVariables(VVAccess.ReadWrite)] - public bool CanWeldShut { + public bool CanWeldShut + { get => _canWeldShut; set { @@ -161,6 +161,13 @@ namespace Content.Server.Storage.Components public virtual void Activate(ActivateEventArgs eventArgs) { + // HACK until EntityStorageComponent gets refactored to the new ECS system + if (Owner.TryGetComponent(out var @lock) && @lock.Locked) + { + // Do nothing, LockSystem is responsible for handling this case + return; + } + ToggleOpen(eventArgs.User); } @@ -168,7 +175,7 @@ namespace Content.Server.Storage.Components { if (IsWeldedShut) { - if(!silent) Owner.PopupMessage(user, Loc.GetString("entity-storage-component-welded-shut-message")); + if (!silent) Owner.PopupMessage(user, Loc.GetString("entity-storage-component-welded-shut-message")); return false; } return true; @@ -468,7 +475,8 @@ namespace Content.Server.Storage.Components protected virtual void OpenVerbGetData(IEntity user, EntityStorageComponent component, VerbData data) { - if (!EntitySystem.Get().CanInteract(user)) + if (!EntitySystem.Get().CanInteract(user) || + component.Owner.TryGetComponent(out LockComponent? lockComponent) && lockComponent.Locked) // HACK extra check, until EntityStorage gets refactored { data.Visibility = VerbVisibility.Invisible; return; @@ -478,7 +486,7 @@ namespace Content.Server.Storage.Components { data.Visibility = VerbVisibility.Disabled; var verb = Loc.GetString(component.Open ? "open-toggle-verb-close" : "open-toggle-verb-open"); - data.Text = Loc.GetString("open-toggle-verb-welded-shut-message",("verb", verb)); + data.Text = Loc.GetString("open-toggle-verb-welded-shut-message", ("verb", verb)); return; } diff --git a/Content.Server/Storage/Components/SecretStashComponent.cs b/Content.Server/Storage/Components/SecretStashComponent.cs index b202ab19db..30af3865fc 100644 --- a/Content.Server/Storage/Components/SecretStashComponent.cs +++ b/Content.Server/Storage/Components/SecretStashComponent.cs @@ -81,7 +81,7 @@ namespace Content.Server.Storage.Components if (_itemContainer.ContainedEntity == null) return false; - Owner.PopupMessage(user, Loc.GetString("There was something inside {0}!", ("stash", SecretPartName))); + Owner.PopupMessage(user, Loc.GetString("comp-secret-stash-action-get-item-found-something", ("stash", SecretPartName))); if (user.TryGetComponent(out HandsComponent? hands)) { diff --git a/Content.Server/Storage/Components/SecureEntityStorageComponent.cs b/Content.Server/Storage/Components/SecureEntityStorageComponent.cs deleted file mode 100644 index 8c3df45b4a..0000000000 --- a/Content.Server/Storage/Components/SecureEntityStorageComponent.cs +++ /dev/null @@ -1,153 +0,0 @@ -using Content.Server.Access.Components; -using Content.Shared.ActionBlocker; -using Content.Shared.Interaction; -using Content.Shared.Notification.Managers; -using Content.Shared.Sound; -using Content.Shared.Storage; -using Content.Shared.Verbs; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; -using Robust.Shared.GameObjects; -using Robust.Shared.Localization; -using Robust.Shared.Player; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.ViewVariables; - -namespace Content.Server.Storage.Components -{ - [RegisterComponent] - [ComponentReference(typeof(EntityStorageComponent))] - [ComponentReference(typeof(IActivate))] - [ComponentReference(typeof(IStorageComponent))] - public class SecureEntityStorageComponent : EntityStorageComponent - { - public override string Name => "SecureEntityStorage"; - [DataField("locked")] - private bool _locked = true; - - [DataField("unlockSound")] private SoundSpecifier _unlockSound = new SoundPathSpecifier("/Audio/Machines/door_lock_off.ogg"); - [DataField("lockSound")] private SoundSpecifier _lockSound = new SoundPathSpecifier("/Audio/Machines/door_lock_on.ogg"); - - [ViewVariables(VVAccess.ReadWrite)] - public bool Locked - { - get => _locked; - set - { - _locked = value; - - if (Owner.TryGetComponent(out AppearanceComponent? appearance)) - { - appearance.SetData(StorageVisuals.Locked, _locked); - } - } - } - - protected override void Startup() - { - base.Startup(); - - if (Owner.TryGetComponent(out AppearanceComponent? appearance)) - { - appearance.SetData(StorageVisuals.CanLock, true); - } - } - - public override void Activate(ActivateEventArgs eventArgs) - { - if (Locked) - { - DoToggleLock(eventArgs.User); - return; - } - - base.Activate(eventArgs); - } - - public override bool CanOpen(IEntity user, bool silent = false) - { - if (Locked) - { - Owner.PopupMessage(user, "It's locked!"); - return false; - } - return base.CanOpen(user, silent); - } - - protected override void OpenVerbGetData(IEntity user, EntityStorageComponent component, VerbData data) - { - if (Locked) - { - data.Visibility = VerbVisibility.Invisible; - - return; - } - - base.OpenVerbGetData(user, component, data); - } - - private void DoToggleLock(IEntity user) - { - if (Locked) - { - DoUnlock(user); - } - else - { - DoLock(user); - } - } - - private void DoUnlock(IEntity user) - { - if (!CheckAccess(user)) return; - - Locked = false; - if(_unlockSound.TryGetSound(out var unlockSound)) - SoundSystem.Play(Filter.Pvs(Owner), unlockSound, Owner, AudioParams.Default.WithVolume(-5)); - } - - private void DoLock(IEntity user) - { - if (!CheckAccess(user)) return; - - Locked = true; - if(_lockSound.TryGetSound(out var lockSound)) - SoundSystem.Play(Filter.Pvs(Owner), lockSound, Owner, AudioParams.Default.WithVolume(-5)); - } - - private bool CheckAccess(IEntity user) - { - if (Owner.TryGetComponent(out AccessReader? reader)) - { - if (!reader.IsAllowed(user)) - { - Owner.PopupMessage(user, Loc.GetString("secure-entity-storage-component-not-allowed-message")); - return false; - } - } - - return true; - } - - [Verb] - private sealed class ToggleLockVerb : Verb - { - protected override void GetData(IEntity user, SecureEntityStorageComponent component, VerbData data) - { - if (!EntitySystem.Get().CanInteract(user) || component.Open) - { - data.Visibility = VerbVisibility.Invisible; - return; - } - - data.Text = Loc.GetString(component.Locked ? "toggle-lock-verb-unlock" : "toggle-lock-verb-lock"); - } - - protected override void Activate(IEntity user, SecureEntityStorageComponent component) - { - component.DoToggleLock(user); - } - } - } -} diff --git a/Content.Server/Storage/Components/ServerStorageComponent.cs b/Content.Server/Storage/Components/ServerStorageComponent.cs index 9721b5f9e0..14654a38ac 100644 --- a/Content.Server/Storage/Components/ServerStorageComponent.cs +++ b/Content.Server/Storage/Components/ServerStorageComponent.cs @@ -16,6 +16,7 @@ using Content.Shared.Notification; using Content.Shared.Notification.Managers; using Content.Shared.Sound; using Content.Shared.Storage; +using Content.Shared.Whitelist; using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared.Audio; @@ -48,10 +49,16 @@ namespace Content.Server.Storage.Components [DataField("occludesLight")] private bool _occludesLight = true; + [DataField("quickInsert")] - private bool _quickInsert; //Can insert storables by "attacking" them with the storage entity + private bool _quickInsert = false; // Can insert storables by "attacking" them with the storage entity + [DataField("areaInsert")] - private bool _areaInsert; //"Attacking" with the storage entity causes it to insert all nearby storables after a delay + private bool _areaInsert = false; // "Attacking" with the storage entity causes it to insert all nearby storables after a delay + + [DataField("whitelist")] + private EntityWhitelist? _whitelist = null; + private bool _storageInitialCalculated; private int _storageUsed; [DataField("capacity")] @@ -124,6 +131,11 @@ namespace Content.Server.Storage.Components return false; } + if (_whitelist != null && !_whitelist.IsValid(entity)) + { + return false; + } + return true; } diff --git a/Content.Server/Storage/Components/SpawnItemsOnUseComponent.cs b/Content.Server/Storage/Components/SpawnItemsOnUseComponent.cs new file mode 100644 index 0000000000..0956e8a5f3 --- /dev/null +++ b/Content.Server/Storage/Components/SpawnItemsOnUseComponent.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Content.Shared.Sound; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Server.Storage.Components +{ + /// + /// Spawns items when used in hand. + /// + [RegisterComponent] + public class SpawnItemsOnUseComponent : Component + { + public override string Name => "SpawnItemsOnUse"; + + /// + /// The list of entities to spawn, with amounts and orGroups. + /// + /// + [DataField("items", required: true)] + public List Items = new List(); + + /// + /// A sound to play when the items are spawned. For example, gift boxes being unwrapped. + /// + [DataField("sound")] + public SoundSpecifier? Sound = null; + + /// + /// How many uses before the item should delete itself. + /// + /// + [DataField("uses")] + public int Uses = 1; + } +} diff --git a/Content.Server/Storage/Components/StorageFillComponent.cs b/Content.Server/Storage/Components/StorageFillComponent.cs index 788a1727bb..ed56832fb8 100644 --- a/Content.Server/Storage/Components/StorageFillComponent.cs +++ b/Content.Server/Storage/Components/StorageFillComponent.cs @@ -17,9 +17,9 @@ namespace Content.Server.Storage.Components { public override string Name => "StorageFill"; - [DataField("contents")] private List _contents = new(); + [DataField("contents")] private List _contents = new(); - public IReadOnlyList Contents => _contents; + public IReadOnlyList Contents => _contents; void IMapInit.MapInit() { @@ -39,7 +39,6 @@ namespace Content.Server.Storage.Components var alreadySpawnedGroups = new List(); foreach (var storageItem in _contents) { - if (string.IsNullOrEmpty(storageItem.PrototypeId)) continue; if (!string.IsNullOrEmpty(storageItem.GroupId) && alreadySpawnedGroups.Contains(storageItem.GroupId)) continue; @@ -58,50 +57,5 @@ namespace Content.Server.Storage.Components if (!string.IsNullOrEmpty(storageItem.GroupId)) alreadySpawnedGroups.Add(storageItem.GroupId); } } - - [Serializable] - [DataDefinition] - public struct StorageFillEntry : IPopulateDefaultValues - { - [DataField("id", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? PrototypeId; - - [DataField("prob")] public float SpawnProbability; - - /// - /// The probability that an item will spawn. Takes decimal form so 0.05 is 5%, 0.50 is 50% etc. - /// - [DataField("orGroup")] public string GroupId; - - /// - /// orGroup signifies to pick between entities designated with an ID. - /// - /// - /// To define an orGroup in a StorageFill component you - /// need to add it to the entities you want to choose between and - /// add a prob field. In this example there is a 50% chance the storage - /// spawns with Y or Z. - /// - /// - /// - /// - type: StorageFill - /// contents: - /// - name: X - /// - name: Y - /// prob: 0.50 - /// orGroup: YOrZ - /// - name: Z - /// orGroup: YOrZ - /// - /// - /// - [DataField("amount")] public int Amount; - - public void PopulateDefaultValues() - { - Amount = 1; - SpawnProbability = 1; - } - } } } diff --git a/Content.Server/Storage/EntitySpawnEntry.cs b/Content.Server/Storage/EntitySpawnEntry.cs new file mode 100644 index 0000000000..99080980c0 --- /dev/null +++ b/Content.Server/Storage/EntitySpawnEntry.cs @@ -0,0 +1,59 @@ +using System; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Storage +{ + /// + /// Dictates a list of items that can be spawned. + /// + [Serializable] + [DataDefinition] + public struct EntitySpawnEntry : IPopulateDefaultValues + { + [DataField("id", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] + public string PrototypeId; + + /// + /// The probability that an item will spawn. Takes decimal form so 0.05 is 5%, 0.50 is 50% etc. + /// + [DataField("prob")] + public float SpawnProbability; + + /// + /// orGroup signifies to pick between entities designated with an ID. + /// + /// + /// To define an orGroup in a StorageFill component you + /// need to add it to the entities you want to choose between and + /// add a prob field. In this example there is a 50% chance the storage + /// spawns with Y or Z. + /// + /// + /// + /// - type: StorageFill + /// contents: + /// - name: X + /// - name: Y + /// prob: 0.50 + /// orGroup: YOrZ + /// - name: Z + /// orGroup: YOrZ + /// + /// + /// + [DataField("orGroup")] + public string? GroupId; + + [DataField("amount")] + public int Amount; + + public void PopulateDefaultValues() + { + Amount = 1; + SpawnProbability = 1; + } + } +} diff --git a/Content.Server/Storage/ItemCounterSystem.cs b/Content.Server/Storage/EntitySystems/ItemCounterSystem.cs similarity index 96% rename from Content.Server/Storage/ItemCounterSystem.cs rename to Content.Server/Storage/EntitySystems/ItemCounterSystem.cs index 9e79ab984f..cdd53e335c 100644 --- a/Content.Server/Storage/ItemCounterSystem.cs +++ b/Content.Server/Storage/EntitySystems/ItemCounterSystem.cs @@ -5,7 +5,7 @@ using JetBrains.Annotations; using Robust.Shared.Containers; using Robust.Shared.GameObjects; -namespace Content.Server.Storage +namespace Content.Server.Storage.EntitySystems { [UsedImplicitly] public class ItemCounterSystem : SharedItemCounterSystem diff --git a/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs b/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs new file mode 100644 index 0000000000..e694aa237a --- /dev/null +++ b/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using Content.Server.Storage.Components; +using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Player; +using Robust.Shared.Random; + +namespace Content.Server.Storage.EntitySystems +{ + public class SpawnItemsOnUseSystem : EntitySystem + { + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUseInHand); + } + + private void OnUseInHand(EntityUid uid, SpawnItemsOnUseComponent component, UseInHandEvent args) + { + if (args.Handled) + return; + + var owner = EntityManager.GetEntity(uid); + var alreadySpawnedGroups = new List(); + IEntity? entityToPlaceInHands = null; + foreach (var storageItem in component.Items) + { + if (!string.IsNullOrEmpty(storageItem.GroupId) && + alreadySpawnedGroups.Contains(storageItem.GroupId)) continue; + + if (storageItem.SpawnProbability != 1f && + !_random.Prob(storageItem.SpawnProbability)) + { + continue; + } + + for (var i = 0; i < storageItem.Amount; i++) + { + entityToPlaceInHands = EntityManager.SpawnEntity(storageItem.PrototypeId, args.User.Transform.Coordinates); + } + + if (!string.IsNullOrEmpty(storageItem.GroupId)) alreadySpawnedGroups.Add(storageItem.GroupId); + } + + if (component.Sound != null) + SoundSystem.Play(Filter.Pvs(owner), component.Sound.GetSound()); + + component.Uses--; + if (component.Uses == 0) + { + args.Handled = true; + owner.Delete(); + } + + if (entityToPlaceInHands != null + && args.User.TryGetComponent(out var hands)) + { + hands.TryPutInAnyHand(entityToPlaceInHands); + } + } + } +} diff --git a/Content.Server/Storage/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs similarity index 98% rename from Content.Server/Storage/StorageSystem.cs rename to Content.Server/Storage/EntitySystems/StorageSystem.cs index 73cf367254..d80a8e32a9 100644 --- a/Content.Server/Storage/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -6,7 +6,7 @@ using Robust.Server.Player; using Robust.Shared.Containers; using Robust.Shared.GameObjects; -namespace Content.Server.Storage +namespace Content.Server.Storage.EntitySystems { [UsedImplicitly] internal sealed class StorageSystem : EntitySystem @@ -54,7 +54,7 @@ namespace Content.Server.Storage { storageComp.HandleEntityMaybeInserted(message); } - + if (oldParentEntity.TryGetComponent(out var newStorageComp)) { newStorageComp.ContainerUpdateAppearance(message.Container); diff --git a/Content.Shared/Atmos/AtmosDirection.cs b/Content.Shared/Atmos/AtmosDirection.cs index 96023a55e8..5093b5245b 100644 --- a/Content.Shared/Atmos/AtmosDirection.cs +++ b/Content.Shared/Atmos/AtmosDirection.cs @@ -142,6 +142,28 @@ namespace Content.Shared.Atmos { return (direction & other) == other; } + + public static Vector2i CardinalToIntVec(this Direction dir) + { + switch (dir) + { + case Direction.North: + return new Vector2i(0, 1); + case Direction.East: + return new Vector2i(1, 0); + case Direction.South: + return new Vector2i(0, -1); + case Direction.West: + return new Vector2i(-1, 0); + default: + throw new ArgumentException($"Direction dir {dir} is not a cardinal direction", nameof(dir)); + } + } + + public static Vector2i Offset(this Vector2i pos, Direction dir) + { + return pos + dir.CardinalToIntVec(); + } } public sealed class AtmosDirectionFlags { } diff --git a/Content.Shared/Atmos/Atmospherics.cs b/Content.Shared/Atmos/Atmospherics.cs index b2736ebf68..53d34fa40c 100644 --- a/Content.Shared/Atmos/Atmospherics.cs +++ b/Content.Shared/Atmos/Atmospherics.cs @@ -1,6 +1,7 @@ using Robust.Shared.Maths; using Robust.Shared.Serialization; using System; +// ReSharper disable InconsistentNaming namespace Content.Shared.Atmos { diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 3656505a4e..3b2091492c 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -175,6 +175,12 @@ namespace Content.Shared.CCVar * Physics */ + /// + /// When a mob is walking should its X / Y movement be relative to its parent (true) or the map (false). + /// + public static readonly CVarDef RelativeMovement = + CVarDef.Create("physics.relative_movement", true, CVar.ARCHIVE | CVar.REPLICATED); + public static readonly CVarDef TileFrictionModifier = CVarDef.Create("physics.tile_friction", 40.0f); @@ -285,7 +291,6 @@ namespace Content.Shared.CCVar public static readonly CVarDef AtmosGridImpulse = CVarDef.Create("atmos.grid_impulse", false, CVar.SERVERONLY); - /// /// Whether atmos superconduction is enabled. /// @@ -293,10 +298,17 @@ namespace Content.Shared.CCVar public static readonly CVarDef Superconduction = CVarDef.Create("atmos.superconduction", false, CVar.SERVERONLY); + /// + /// Whether excited groups will be processed and created. + /// + public static readonly CVarDef ExcitedGroups = + CVarDef.Create("atmos.excited_groups", true, CVar.SERVERONLY); + /// /// Whether all tiles in an excited group will clear themselves once being exposed to space. /// Similar to , without none of the tile ripping or /// things being thrown around very violently. + /// Needs to be enabled to work. /// public static readonly CVarDef ExcitedGroupsSpaceIsAllConsuming = CVarDef.Create("atmos.excited_groups_space_is_all_consuming", false, CVar.SERVERONLY); diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index 7f50cf0063..0d02792593 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Notification.Managers; using Content.Shared.Physics; using JetBrains.Annotations; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Physics; @@ -17,6 +18,8 @@ namespace Content.Shared.Interaction [UsedImplicitly] public class SharedInteractionSystem : EntitySystem { + [Dependency] private readonly SharedBroadphaseSystem _sharedBroadphaseSystem = default!; + public const float InteractionRange = 2; public const float InteractionRangeSquared = InteractionRange * InteractionRange; @@ -46,7 +49,7 @@ namespace Content.Shared.Interaction predicate ??= _ => false; var ray = new CollisionRay(origin.Position, dir.Normalized, collisionMask); - var rayResults = Get().IntersectRayWithPredicate(origin.MapId, ray, dir.Length, predicate.Invoke, false).ToList(); + var rayResults = _sharedBroadphaseSystem.IntersectRayWithPredicate(origin.MapId, ray, dir.Length, predicate.Invoke, false).ToList(); if (rayResults.Count == 0) return dir.Length; return (rayResults[0].HitPos - origin.Position).Length; @@ -122,7 +125,7 @@ namespace Content.Shared.Interaction predicate ??= _ => false; var ray = new CollisionRay(origin.Position, dir.Normalized, (int) collisionMask); - var rayResults = Get().IntersectRayWithPredicate(origin.MapId, ray, dir.Length, predicate.Invoke, false).ToList(); + var rayResults = _sharedBroadphaseSystem.IntersectRayWithPredicate(origin.MapId, ray, dir.Length, predicate.Invoke, false).ToList(); if (rayResults.Count == 0) return true; diff --git a/Content.Shared/Kitchen/Components/SharedReagentGrinderComponent.cs b/Content.Shared/Kitchen/Components/SharedReagentGrinderComponent.cs index ca1eae6923..0effbe15db 100644 --- a/Content.Shared/Kitchen/Components/SharedReagentGrinderComponent.cs +++ b/Content.Shared/Kitchen/Components/SharedReagentGrinderComponent.cs @@ -51,16 +51,6 @@ namespace Content.Shared.Kitchen.Components } } - [Serializable, NetSerializable] - public class ReagentGrinderVaporizeReagentIndexedMessage : BoundUserInterfaceMessage - { - public Solution.ReagentQuantity ReagentQuantity; - public ReagentGrinderVaporizeReagentIndexedMessage(Solution.ReagentQuantity reagentQuantity) - { - ReagentQuantity = reagentQuantity; - } - } - [Serializable, NetSerializable] public class ReagentGrinderWorkStartedMessage : BoundUserInterfaceMessage { diff --git a/Content.Shared/Movement/SharedMoverController.cs b/Content.Shared/Movement/SharedMoverController.cs index d48d7ff3b9..0c1bc416a9 100644 --- a/Content.Shared/Movement/SharedMoverController.cs +++ b/Content.Shared/Movement/SharedMoverController.cs @@ -1,7 +1,9 @@ using Content.Shared.ActionBlocker; +using Content.Shared.CCVar; using Content.Shared.MobState; using Content.Shared.Movement.Components; using Content.Shared.Pulling.Components; +using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; @@ -9,6 +11,7 @@ using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Physics.Broadphase; using Robust.Shared.Physics.Controllers; +using Robust.Shared.Utility; namespace Content.Shared.Movement { @@ -22,10 +25,14 @@ namespace Content.Shared.Movement private SharedBroadphaseSystem _broadPhaseSystem = default!; + private bool _relativeMovement; + public override void Initialize() { base.Initialize(); _broadPhaseSystem = EntitySystem.Get(); + var configManager = IoCManager.Resolve(); + configManager.OnValueChanged(CCVars.RelativeMovement, value => _relativeMovement = value, true); } /// @@ -39,12 +46,14 @@ namespace Content.Shared.Movement // Target velocity. var total = (walkDir * mover.CurrentWalkSpeed + sprintDir * mover.CurrentSprintSpeed); - if (total != Vector2.Zero) + var worldTotal = _relativeMovement ? new Angle(mover.Owner.Transform.Parent!.WorldRotation.Theta).RotateVec(total) : total; + + if (worldTotal != Vector2.Zero) { - mover.Owner.Transform.LocalRotation = total.GetDir().ToAngle(); + mover.Owner.Transform.WorldRotation = worldTotal.GetDir().ToAngle(); } - physicsComponent.LinearVelocity = total; + physicsComponent.LinearVelocity = worldTotal; } /// @@ -53,7 +62,8 @@ namespace Content.Shared.Movement /// /// /// - protected void HandleMobMovement(IMoverComponent mover, PhysicsComponent physicsComponent, IMobMoverComponent mobMover) + protected void HandleMobMovement(IMoverComponent mover, PhysicsComponent physicsComponent, + IMobMoverComponent mobMover) { // TODO: Look at https://gameworksdocs.nvidia.com/PhysX/4.1/documentation/physxguide/Manual/CharacterControllers.html?highlight=controller as it has some adviceo n kinematic controllersx if (!UseMobMovement(_broadPhaseSystem, physicsComponent, _mapManager)) @@ -74,30 +84,37 @@ namespace Content.Shared.Movement if (!touching) { - transform.LocalRotation = physicsComponent.LinearVelocity.GetDir().ToAngle(); + transform.WorldRotation = physicsComponent.LinearVelocity.GetDir().ToAngle(); return; } } // Regular movement. // Target velocity. + // This is relative to the map / grid we're on. var total = (walkDir * mover.CurrentWalkSpeed + sprintDir * mover.CurrentSprintSpeed); + var worldTotal = _relativeMovement ? + new Angle(transform.Parent!.WorldRotation.Theta).RotateVec(total) : + total; + + DebugTools.Assert(MathHelper.CloseTo(total.Length, worldTotal.Length)); + if (weightless) { - total *= mobMover.WeightlessStrength; + worldTotal *= mobMover.WeightlessStrength; } - if (total != Vector2.Zero) + if (worldTotal != Vector2.Zero) { // This should have its event run during island solver soooo transform.DeferUpdates = true; - transform.LocalRotation = total.GetDir().ToAngle(); + transform.WorldRotation = worldTotal.GetDir().ToAngle(); transform.DeferUpdates = false; HandleFootsteps(mover, mobMover); } - physicsComponent.LinearVelocity = total; + physicsComponent.LinearVelocity = worldTotal; } public static bool UseMobMovement(SharedBroadphaseSystem broadPhaseSystem, PhysicsComponent body, IMapManager mapManager) diff --git a/Content.Shared/Nutrition/Components/SharedDrinkFoodContainerComponent.cs b/Content.Shared/Nutrition/Components/SharedDrinkFoodContainerComponent.cs deleted file mode 100644 index a4fb1d0288..0000000000 --- a/Content.Shared/Nutrition/Components/SharedDrinkFoodContainerComponent.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Robust.Shared.GameObjects; -using Robust.Shared.Serialization; - -namespace Content.Shared.Nutrition.Components -{ - public abstract class SharedFoodContainerComponent : Component - { - } - - [NetSerializable, Serializable] - public enum FoodContainerVisualMode - { - /// - /// Discrete: 50 eggs in a carton -> down to 25, will show 12/12 until it gets below max - /// Rounded: 50 eggs in a carton -> down to 25, will round it to 6 of 12 - /// - Discrete, - Rounded, - } - - [NetSerializable, Serializable] - public enum FoodContainerVisuals - { - Capacity, - Current, - } -} diff --git a/Content.Shared/SubFloor/SubFloorHideComponent.cs b/Content.Shared/SubFloor/SubFloorHideComponent.cs index a76c73473e..93587e6f8c 100644 --- a/Content.Shared/SubFloor/SubFloorHideComponent.cs +++ b/Content.Shared/SubFloor/SubFloorHideComponent.cs @@ -1,4 +1,6 @@ using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.ViewVariables; namespace Content.Shared.SubFloor { @@ -13,5 +15,12 @@ namespace Content.Shared.SubFloor { /// public override string Name => "SubFloorHide"; + + /// + /// This entity needs to be anchored to be hid in the subfloor. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("requireAnchored")] + public bool RequireAnchored { get; set; } = true; } } diff --git a/Content.Shared/SubFloor/SubFloorHideSystem.cs b/Content.Shared/SubFloor/SubFloorHideSystem.cs index 5dd30a86a3..6f3f5bd9f2 100644 --- a/Content.Shared/SubFloor/SubFloorHideSystem.cs +++ b/Content.Shared/SubFloor/SubFloorHideSystem.cs @@ -68,8 +68,7 @@ namespace Content.Shared.SubFloor var transform = ComponentManager.GetComponent(uid); // We do this directly instead of calling UpdateEntity. - if(_mapManager.TryGetGrid(transform.GridID, out var grid)) - UpdateTile(grid, grid.TileIndicesFor(transform.Coordinates)); + UpdateEntity(uid); } private void MapManagerOnTileChanged(object? sender, TileChangedEventArgs e) @@ -113,6 +112,7 @@ namespace Content.Shared.SubFloor private void UpdateEntity(EntityUid uid) { var transform = ComponentManager.GetComponent(uid); + if (!_mapManager.TryGetGrid(transform.GridID, out var grid)) { // Not being on a grid counts as no subfloor, unhide this. @@ -134,6 +134,18 @@ namespace Content.Shared.SubFloor if (subFloorHideEvent.Handled) return; + // This might look weird, but basically we only need to query the SubFloorHide and Transform components + // if we are gonna hide the entity and we require it to be anchored to be hidden. Because getting components + // is "expensive", we have a slow path where we query them, and a fast path where we don't. + if (!subFloor + && ComponentManager.TryGetComponent(uid, out SubFloorHideComponent? subFloorHideComponent) && + subFloorHideComponent.RequireAnchored + && ComponentManager.TryGetComponent(uid, out ITransformComponent? transformComponent)) + { + // If we require the entity to be anchored but it's not, this will set subfloor to true, unhiding it. + subFloor = !transformComponent.Anchored; + } + // Show sprite if (ComponentManager.TryGetComponent(uid, out SharedSpriteComponent? spriteComponent)) { diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 95ed321f80..62bb69b3ae 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1588,3 +1588,28 @@ Entries: - {message: Items with missing inhand sprites no longer show a giant ERROR, type: Fix} id: 282 time: '2021-07-25T10:02:48.0000000+00:00' +- author: Seth + changes: + - {message: Added Makeshift Handcuffs into the construction list, type: Add} + id: 283 + time: '2021-07-25T15:30:42.0000000+00:00' +- author: metalgearsloth + changes: + - {message: Shuttle-relative movement for when we get shuttle rotation., type: Add} + id: 284 + time: '2021-07-25T15:48:22.0000000+00:00' +- author: Seth + changes: + - {message: Added Storage Room to Coats, type: Add} + id: 285 + time: '2021-07-26T05:36:27.0000000+00:00' +- author: Seth + changes: + - {message: Added a hardsuit into research directors locker, type: Add} + id: 286 + time: '2021-07-26T22:14:10.0000000+00:00' +- author: Seth + changes: + - {message: Added bio suits into bio lockers, type: Add} + id: 287 + time: '2021-07-28T17:36:06.0000000+00:00' diff --git a/Resources/Locale/en-US/chat/managers/chat-manager.ftl b/Resources/Locale/en-US/chat/managers/chat-manager.ftl index 02b71db263..01b43a67c6 100644 --- a/Resources/Locale/en-US/chat/managers/chat-manager.ftl +++ b/Resources/Locale/en-US/chat/managers/chat-manager.ftl @@ -13,7 +13,7 @@ chat-manager-sender-announcement-wrap-message = {$sender} Announcement: chat-manager-entity-say-wrap-message = {$entityName} says: "{"{0}"}" chat-manager-entity-me-wrap-message = {$entityName} {"{0}"} chat-manager-send-ooc-wrap-message = OOC: {$playerName}: {"{0}"} -chat-manager-send-ooc-patron-wrap-message = OOC: [color={$patronColor}]{$playerName}[/color]: {"{0}"}: +chat-manager-send-ooc-patron-wrap-message = OOC: [color={$patronColor}]{$playerName}[/color]: {"{0}"} chat-manager-send-dead-chat-wrap-message = {$deadChannelName}: {$playerName}: {"{0}"} chat-manager-send-admin-dead-chat-wrap-message = {$adminChannelName}:({$userName}): {"{0}"} chat-manager-send-admin-chat-wrap-message = {$adminChannelName}: {$playerName}: {"{0}"} @@ -21,4 +21,4 @@ chat-manager-send-admin-announcement-wrap-message = {$adminChannelName}: {"{0}"} chat-manager-send-hook-ooc-wrap-message = OOC: (D){$senderName}: {"{0}"} chat-manager-dead-channel-name = DEAD -chat-manager-admin-channel-name = ADMIN \ No newline at end of file +chat-manager-admin-channel-name = ADMIN diff --git a/Resources/Locale/en-US/lock/lock-component.ftl b/Resources/Locale/en-US/lock/lock-component.ftl new file mode 100644 index 0000000000..f9f975c96e --- /dev/null +++ b/Resources/Locale/en-US/lock/lock-component.ftl @@ -0,0 +1,10 @@ +lock-comp-on-examined-is-locked = The {$entityName} seems to be locked. +lock-comp-on-examined-is-unlocked = The {$entityName} seems to be unlocked. +lock-comp-do-lock-success = You lock the {$entityName}. +lock-comp-do-unlock-success = You unlock the {$entityName}. +lock-comp-has-user-access-fail = Access denied + +## ToggleLockVerb + +toggle-lock-verb-unlock = Unlock +toggle-lock-verb-lock = Lock \ No newline at end of file diff --git a/Resources/Locale/en-US/storage/components/secure-entity-storage-component.ftl b/Resources/Locale/en-US/storage/components/secure-entity-storage-component.ftl deleted file mode 100644 index 02082f2dcc..0000000000 --- a/Resources/Locale/en-US/storage/components/secure-entity-storage-component.ftl +++ /dev/null @@ -1,6 +0,0 @@ -secure-entity-storage-component-not-allowed-message = Access denied - -## ToggleLockVerb - -toggle-lock-verb-unlock = Unlock -toggle-lock-verb-lock = Lock \ No newline at end of file diff --git a/Resources/Maps/saltern.yml b/Resources/Maps/saltern.yml index 2c0fd2c5ac..8ca177e8e3 100644 --- a/Resources/Maps/saltern.yml +++ b/Resources/Maps/saltern.yml @@ -12318,15 +12318,6 @@ entities: uniqueMixes: - volume: 2500 temperature: 293.15 - molesArchived: - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 moles: - 21.824879 - 82.10312 diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/biohazard.yml b/Resources/Prototypes/Catalog/Fills/Lockers/biohazard.yml index b9a6ce5322..79b38ef7cc 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/biohazard.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/biohazard.yml @@ -1,24 +1,59 @@ -# - type: entity -# id: ClosetL3Filled -# suffix: Filled, Generic -# parent: ClosetL3 + - type: entity + id: ClosetL3Filled + suffix: Filled, Generic + parent: ClosetL3 + components: + - type: StorageFill + contents: + - id: ClothingOuterBioGeneral + prob: 1 + - id: ClothingHeadHatHoodBioGeneral + prob: 1 - type: entity id: ClosetL3VirologyFilled suffix: Filled, Virology parent: ClosetL3Virology + components: + - type: StorageFill + contents: + - id: ClothingOuterBioVirology + prob: 1 + - id: ClothingHeadHatHoodBioVirology + prob: 1 - type: entity id: ClosetL3SecurityFilled suffix: Filled, Security parent: ClosetL3Security + components: + - type: StorageFill + contents: + - id: ClothingOuterBioSecurity + prob: 1 + - id: ClothingHeadHatHoodBioSecurity + prob: 1 - type: entity id: ClosetL3JanitorFilled suffix: Filled, Janitor parent: ClosetL3Janitor + components: + - type: StorageFill + contents: + - id: ClothingOuterBioJanitor + prob: 1 + - id: ClothingHeadHatHoodBioJanitor + prob: 1 -# - type: entity -# id: ClosetL3ScienceFilled -# suffix: Filled, Science -# parent: ClosetL3Virology + - type: entity + id: ClosetL3ScienceFilled + suffix: Filled, Science + parent: ClosetL3Virology + components: + - type: StorageFill + contents: + - id: ClothingOuterBioScientist + prob: 1 + - id: ClothingHeadHatHoodBioScientist + prob: 1 diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml index f8df3df691..415db3246d 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml @@ -147,6 +147,10 @@ prob: 1 - id: ClothingHeadsetMedicalScience prob: 1 + - id: ClothingHeadHelmetHardsuitRd + prob: 1 + - id: ClothingOuterHardsuitRd + prob: 1 - id: PlushieSlime prob: 0.1 diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml index b753048f50..d2e13f35e9 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml @@ -72,6 +72,8 @@ prob: 1 - id: ClothingOuterApronBotanist prob: 0.8 + - id: ClothingBeltPlant + prob: 1 - id: TowercapSeeds prob: 1 - id: BananaSeeds diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/coats.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/coats.yml index b6654eec1b..e8662570c4 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/coats.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/coats.yml @@ -8,6 +8,8 @@ sprite: Clothing/OuterClothing/Coats/bomber.rsi - type: Clothing sprite: Clothing/OuterClothing/Coats/bomber.rsi + - type: Storage + capacity: 10 - type: entity parent: ClothingOuterBase @@ -19,6 +21,8 @@ sprite: Clothing/OuterClothing/Coats/detective.rsi - type: Clothing sprite: Clothing/OuterClothing/Coats/detective.rsi + - type: Storage + capacity: 10 - type: entity parent: ClothingOuterBase @@ -30,6 +34,8 @@ sprite: Clothing/OuterClothing/Coats/gentlecoat.rsi - type: Clothing sprite: Clothing/OuterClothing/Coats/gentlecoat.rsi + - type: Storage + capacity: 10 - type: entity parent: ClothingOuterBase @@ -41,6 +47,8 @@ sprite: Clothing/OuterClothing/Coats/hos_trenchcoat.rsi - type: Clothing sprite: Clothing/OuterClothing/Coats/hos_trenchcoat.rsi + - type: Storage + capacity: 10 - type: entity parent: ClothingOuterBase @@ -52,6 +60,8 @@ sprite: Clothing/OuterClothing/Coats/insp_coat.rsi - type: Clothing sprite: Clothing/OuterClothing/Coats/insp_coat.rsi + - type: Storage + capacity: 10 - type: entity parent: ClothingOuterBase @@ -63,6 +73,8 @@ sprite: Clothing/OuterClothing/Coats/jensencoat.rsi - type: Clothing sprite: Clothing/OuterClothing/Coats/jensencoat.rsi + - type: Storage + capacity: 10 - type: entity parent: ClothingOuterBase @@ -74,6 +86,8 @@ sprite: Clothing/OuterClothing/Coats/labcoat.rsi - type: Clothing sprite: Clothing/OuterClothing/Coats/labcoat.rsi + - type: Storage + capacity: 10 - type: entity parent: ClothingOuterBase @@ -85,6 +99,8 @@ sprite: Clothing/OuterClothing/Coats/labcoat_chem.rsi - type: Clothing sprite: Clothing/OuterClothing/Coats/labcoat_chem.rsi + - type: Storage + capacity: 10 - type: entity parent: ClothingOuterBase @@ -96,6 +112,8 @@ sprite: Clothing/OuterClothing/Coats/labcoat_cmo.rsi - type: Clothing sprite: Clothing/OuterClothing/Coats/labcoat_cmo.rsi + - type: Storage + capacity: 10 - type: entity parent: ClothingOuterBase @@ -107,3 +125,5 @@ sprite: Clothing/OuterClothing/Coats/pirate.rsi - type: Clothing sprite: Clothing/OuterClothing/Coats/pirate.rsi + - type: Storage + capacity: 10 diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donut.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donut.yml index 32623c81ea..25b22164db 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donut.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donut.yml @@ -1,8 +1,5 @@ # Base -- type: Tag - id: Donut - - type: entity parent: BaseItem id: FoodDonutBase diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml index 87bfb0cd8c..c5d94a8ded 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml @@ -21,6 +21,9 @@ count: 8 - type: Item size: 8 + - type: Tag + tags: + - Pizza - type: entity parent: FoodPizzaBase @@ -37,6 +40,9 @@ Quantity: 5 - type: Item size: 1 + - type: Tag + tags: + - Pizza # Pizza diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml index b3264ef81e..da91d4b718 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml @@ -8,7 +8,7 @@ - type: entity parent: BaseItem id: FoodBoxDonut - name: donutbox + name: donut box description: Mmm, Donuts. components: - type: Sprite @@ -17,6 +17,9 @@ state: box - type: Storage capacity: 6 + whitelist: + tags: + - Donut - type: Item sprite: Objects/Consumable/Food/Baked/donut.rsi size: 6 @@ -48,7 +51,7 @@ - type: entity parent: BaseItem id: FoodContainerEgg - name: eggbox + name: egg carton description: Don't drop 'em! components: - type: Sprite @@ -57,6 +60,9 @@ state: box-closed - type: Storage capacity: 12 + whitelist: + tags: + - Egg - type: Item sprite: Objects/Consumable/Food/egg.rsi size: 12 @@ -114,6 +120,7 @@ - type: entity parent: FoodContainerEgg id: EggBoxBroken + suffix: Broken components: - type: StorageFill contents: @@ -261,6 +268,9 @@ sprite: Objects/Consumable/Food/Baked/donkpocket.rsi state: box - type: Storage + whitelist: + tags: + - DonkPocket capacity: 6 - type: Item sprite: Objects/Consumable/Food/Baked/donkpocket.rsi diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml index f2896a2ba0..886827dcd8 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml @@ -1,8 +1,5 @@ # Base -- type: Tag - id: Egg - - type: entity parent: BaseItem id: FoodEggBase @@ -66,7 +63,7 @@ - type: Puddle variants: 4 state: egg - + - type: entity name: eggshells parent: BaseItem @@ -86,6 +83,9 @@ reagents: - ReagentId: Egg Quantity: 1 + - type: Tag + tags: + - Egg # Egg diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/rolling_paper.yml b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/rolling_paper.yml index 9a55077392..7308ac01ce 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/rolling_paper.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/rolling_paper.yml @@ -4,10 +4,15 @@ id: PackPaperRolling description: "A pack of thin pieces of paper used to make fine smokeables." components: - # I know but it just works. - - type: FoodContainer - prototypes: - PaperRolling: 100 + - type: Storage + whitelist: + tags: + - RollingPaper + capacity: 16 + - type: StorageFill + contents: + - id: PaperRolling + amount: 8 - type: Sprite sprite: Objects/Consumable/Smokeables/Cigarettes/paper.rsi state: cigpapers @@ -29,6 +34,10 @@ state: cigpaper - type: Item sprite: Objects/Consumable/Smokeables/Cigarettes/paper.rsi + size: 2 + - type: Tag + tags: + - RollingPaper - type: entity id: CigaretteFilter diff --git a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml index 675f7e2642..23229182c1 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml @@ -42,6 +42,9 @@ startUncuffSound: /Audio/Items/Handcuffs/rope_start.ogg endUncuffSound: /Audio/Items/Handcuffs/rope_breakout.ogg startBreakoutSound: /Audio/Items/Handcuffs/rope_takeoff.ogg + - type: Construction + graph: makeshifthandcuffs + node: cuffscable - type: Sprite sprite: Objects/Misc/cablecuffs.rsi diff --git a/Resources/Prototypes/Entities/Objects/Misc/monkeycube.yml b/Resources/Prototypes/Entities/Objects/Misc/monkeycube.yml index 396e7ae7fe..263d026607 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/monkeycube.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/monkeycube.yml @@ -4,9 +4,15 @@ id: MonkeyCubeBox description: Drymate brand monkey cubes. Just add water! components: - - type: FoodContainer - prototypes: - MonkeyCubeWrapper: 100 + - type: Storage + whitelist: + tags: + - MonkeyCube + capacity: 30 + - type: StorageFill + contents: + - id: MonkeyCubeWrapped + amount: 6 - type: Sprite sprite: Objects/Misc/monkeycube.rsi state: box @@ -14,13 +20,18 @@ - type: entity parent: BaseItem name: monkey cube - id: MonkeyCubeWrapper + suffix: Wrapped + id: MonkeyCubeWrapped description: Unwrap this to get a monkey cube. components: - - type: FoodContainer - capacity: 1 - prototypes: - MonkeyCube: 100 + - type: SpawnItemsOnUse + items: + - id: MonkeyCube + sound: + path: /Audio/Effects/unwrap.ogg - type: Sprite sprite: Objects/Misc/monkeycube.rsi state: wrapper + - type: Tag + tags: + - MonkeyCube diff --git a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/sprays.yml b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/sprays.yml index a53113ae72..abc6ffceea 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/sprays.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/sprays.yml @@ -50,7 +50,7 @@ name: pest spray id: PestSpray parent: WeedSpray - description: Objects/Tools/Hydroponics/sprays.rsi + description: It's some pest eliminator spray! Do not inhale! suffix: "Filled" components: - type: Sprite diff --git a/Resources/Prototypes/Entities/Objects/Tools/bucket.yml b/Resources/Prototypes/Entities/Objects/Tools/bucket.yml index 9646bdce26..be03c43997 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/bucket.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/bucket.yml @@ -12,6 +12,7 @@ sprite: Objects/Tools/bucket.rsi state: icon - type: Clothing + size: 100 sprite: Objects/Tools/bucket.rsi Slots: - Helmet diff --git a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml index 4c93eab341..48b667a26d 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml @@ -11,6 +11,7 @@ - key: enum.SharedGasTankUiKey.Key type: GasTankBoundUserInterface - type: Clothing + size: 15 sprite: Objects/Tanks/generic.rsi QuickEquip: false - type: GasTank @@ -80,6 +81,7 @@ volume: 2 temperature: 293.15 - type: Clothing + size: 10 sprite: Objects/Tanks/emergency.rsi Slots: - Pocket @@ -100,6 +102,7 @@ volume: 6 temperature: 293.15 - type: Clothing + size: 10 sprite: Objects/Tanks/emergency_yellow.rsi Slots: - Pocket @@ -118,6 +121,7 @@ volume: 10 temperature: 293.15 - type: Clothing + size: 10 sprite: Objects/Tanks/emergency_double.rsi Slots: - Pocket @@ -157,6 +161,7 @@ volume: 70 temperature: 293.15 - type: Clothing + size: 10 sprite: Objects/Tanks/plasma.rsi Slots: - Belt diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/base.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/base.yml index 1b33400a9f..a8fe9b1a7e 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/base.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/base.yml @@ -4,7 +4,7 @@ abstract: true components: - type: AccessReader - - type: SecureEntityStorage + - type: Lock - type: Sprite netsync: false sprite: Structures/Storage/closet.rsi diff --git a/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml b/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml index dae7bc4c1c..70e4f09b5d 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml @@ -261,7 +261,7 @@ components: - type: AccessReader access: [["Security"]] - - type: SecureEntityStorage + - type: Lock - type: Sprite sprite: Structures/Storage/Crates/sec_gear.rsi layers: @@ -290,7 +290,7 @@ components: - type: AccessReader access: [["Engineering"]] - - type: SecureEntityStorage + - type: Lock - type: Sprite sprite: Structures/Storage/Crates/engicrate_secure.rsi layers: @@ -319,7 +319,7 @@ components: - type: AccessReader access: [["Medical"]] - - type: SecureEntityStorage + - type: Lock - type: Sprite sprite: Structures/Storage/Crates/medicalcrate_secure.rsi layers: @@ -347,7 +347,7 @@ parent: CrateGeneric components: - type: AccessReader - - type: SecureEntityStorage + - type: Lock - type: Sprite sprite: Structures/Storage/Crates/privatecrate_secure.rsi layers: @@ -376,7 +376,7 @@ components: - type: AccessReader access: [["Research"]] - - type: SecureEntityStorage + - type: Lock - type: Sprite sprite: Structures/Storage/Crates/scicrate_secure.rsi layers: @@ -405,7 +405,7 @@ components: - type: AccessReader access: [["Engineering"]] - - type: SecureEntityStorage + - type: Lock - type: Sprite sprite: Structures/Storage/Crates/plasma.rsi layers: @@ -433,7 +433,7 @@ parent: CrateGeneric components: - type: AccessReader - - type: SecureEntityStorage + - type: Lock - type: Sprite sprite: Structures/Storage/Crates/secure.rsi layers: @@ -462,7 +462,7 @@ components: - type: AccessReader access: [["Service"]] - - type: SecureEntityStorage + - type: Lock - type: Sprite sprite: Structures/Storage/Crates/hydro_secure.rsi layers: @@ -491,7 +491,7 @@ components: - type: AccessReader access: [["Security"]] - - type: SecureEntityStorage + - type: Lock - type: Sprite sprite: Structures/Storage/Crates/weapon.rsi layers: diff --git a/Resources/Prototypes/Recipes/Crafting/Graphs/makeshifthandcuffs.yml b/Resources/Prototypes/Recipes/Crafting/Graphs/makeshifthandcuffs.yml new file mode 100644 index 0000000000..08d6d57fb9 --- /dev/null +++ b/Resources/Prototypes/Recipes/Crafting/Graphs/makeshifthandcuffs.yml @@ -0,0 +1,14 @@ +- type: constructionGraph + id: makeshifthandcuffs + start: start + graph: + - node: start + edges: + - to: cuffscable + steps: + - material: Cable + amount: 15 + doAfter: 5 + - node: cuffscable + entity: Cablecuffs + diff --git a/Resources/Prototypes/Recipes/Crafting/makeshifthandcuffs.yml b/Resources/Prototypes/Recipes/Crafting/makeshifthandcuffs.yml new file mode 100644 index 0000000000..b7b0344f11 --- /dev/null +++ b/Resources/Prototypes/Recipes/Crafting/makeshifthandcuffs.yml @@ -0,0 +1,11 @@ +- type: construction + name: makeshift handcuffs + id: makeshifthandcuffs + graph: makeshifthandcuffs + startNode: start + targetNode: cuffscable + category: Utility + description: "Homemade handcuffs crafted from spare cables." + icon: Objects/Misc/cablecuffs.rsi/cuff.png + objectType: Item + diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index 07adc2a9fc..3b4028d5e7 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -58,6 +58,12 @@ - type: Tag id: DoorElectronics +- type: Tag + id: Donut + +- type: Tag + id: Egg + - type: Tag id: ExplosivePassable @@ -84,13 +90,19 @@ - type: Tag id: JawsOfLife - + +- type: Tag + id: MonkeyCube + - type: Tag id: NoSpinOnThrow - type: Tag id: Ore +- type: Tag + id: Pizza + - type: Tag id: PlantAnalyzer @@ -103,6 +115,9 @@ - type: Tag id: Powerdrill +- type: Tag + id: RollingPaper + - type: Tag id: Screwdriver diff --git a/RobustToolbox b/RobustToolbox index 3a86c827ea..e93c0f76a9 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit 3a86c827ea02f46b213f473f6479fbfaed35095e +Subproject commit e93c0f76a9ea5021754b6c9b5f1819e0a46b8f4b diff --git a/SpaceStation14.sln.DotSettings b/SpaceStation14.sln.DotSettings index 5ff7f8d1e5..692912bc33 100644 --- a/SpaceStation14.sln.DotSettings +++ b/SpaceStation14.sln.DotSettings @@ -264,6 +264,7 @@ True True True + True True True True @@ -284,6 +285,7 @@ True True True + True True True True