From 6649310ccf489afd6e96d7b5106d433013b0482b Mon Sep 17 00:00:00 2001 From: clyf Date: Fri, 30 Jul 2021 16:05:07 -0700 Subject: [PATCH 1/9] Moved MagbootsComponent.cs from Cloning to Clothing (#4399) --- Content.Client/{Cloning => Clothing}/MagbootsComponent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename Content.Client/{Cloning => Clothing}/MagbootsComponent.cs (87%) diff --git a/Content.Client/Cloning/MagbootsComponent.cs b/Content.Client/Clothing/MagbootsComponent.cs similarity index 87% rename from Content.Client/Cloning/MagbootsComponent.cs rename to Content.Client/Clothing/MagbootsComponent.cs index dce398cf52..fb03315318 100644 --- a/Content.Client/Cloning/MagbootsComponent.cs +++ b/Content.Client/Clothing/MagbootsComponent.cs @@ -1,7 +1,7 @@ -using Content.Shared.Clothing; +using Content.Shared.Clothing; using Robust.Shared.GameObjects; -namespace Content.Client.Cloning +namespace Content.Client.Clothing { [RegisterComponent] public sealed class MagbootsComponent : SharedMagbootsComponent From e93d174c1bce30368e3a132592310ff6eec3e403 Mon Sep 17 00:00:00 2001 From: Kara D Date: Fri, 30 Jul 2021 16:19:24 -0700 Subject: [PATCH 2/9] Update submodule. --- RobustToolbox | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RobustToolbox b/RobustToolbox index e93c0f76a9..8fea42ff9a 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit e93c0f76a9ea5021754b6c9b5f1819e0a46b8f4b +Subproject commit 8fea42ff9ac05dfef40f9fec0dfd051ba054dbfa From b92a2ba115b90b47206a29404dea3249fe074b71 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sat, 31 Jul 2021 01:53:40 +0200 Subject: [PATCH 3/9] Remove GraphWalkSolver It's not worth maintaining and if anybody cares the code is in git history. --- Content.Server/Power/Pow3r/GraphWalkSolver.cs | 181 ------------------ Pow3r/Program.Simulation.cs | 2 - 2 files changed, 183 deletions(-) delete mode 100644 Content.Server/Power/Pow3r/GraphWalkSolver.cs diff --git a/Content.Server/Power/Pow3r/GraphWalkSolver.cs b/Content.Server/Power/Pow3r/GraphWalkSolver.cs deleted file mode 100644 index 981aad8120..0000000000 --- a/Content.Server/Power/Pow3r/GraphWalkSolver.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using static Content.Server.Power.Pow3r.PowerState; - -namespace Content.Server.Power.Pow3r -{ - /// - /// Partial implementation of full-graph-walking power solving under pow3r. - /// Concept described at https://hackmd.io/@ss14/lowpower - /// - /// - /// Many features like batteries, cycle detection, join handling, etc... are not implemented at all. - /// Seriously, this implementation barely works. Ah well. - /// is better. - /// - public class GraphWalkSolver : IPowerSolver - { - public void Tick(float frameTime, PowerState state) - { - foreach (var load in state.Loads.Values) - { - load.ReceivingPower = 0; - } - - foreach (var supply in state.Supplies.Values) - { - supply.CurrentSupply = 0; - } - - foreach (var network in state.Networks.Values) - { - // Clear some stuff. - network.LocalDemandMet = 0; - - // Add up demands in network. - network.LocalDemandTotal = network.Loads - .Select(l => state.Loads[l]) - .Where(c => c.Enabled) - .Sum(c => c.DesiredPower); - - // Add up supplies in network. - var availableSupplySum = 0f; - var maxSupplySum = 0f; - foreach (var supplyId in network.Supplies) - { - var supply = state.Supplies[supplyId]; - if (!supply.Enabled) - continue; - - var rampMax = supply.SupplyRampPosition + supply.SupplyRampTolerance; - var effectiveSupply = Math.Min(rampMax, supply.MaxSupply); - supply.EffectiveMaxSupply = effectiveSupply; - availableSupplySum += effectiveSupply; - maxSupplySum += supply.MaxSupply; - } - - network.AvailableSupplyTotal = availableSupplySum; - network.TheoreticalSupplyTotal = maxSupplySum; - } - - // Sort networks by tree height so that suppliers that have less possible loads go FIRST. - // Idea being that a backup generator on a small subnet should do more work - // so that a larger generator that covers more networks can put its power elsewhere. - var sortedByHeight = state.Networks.Values.OrderBy(v => TotalSubLoadCount(state, v)).ToArray(); - - // Go over every network with supply to send power. - foreach (var network in sortedByHeight) - { - // Find all loads recursively, and sum them up. - var subNets = new List(); - var totalDemand = 0f; - GetLoadingNetworksRecursively(state, network, subNets, ref totalDemand); - - if (totalDemand == 0) - continue; - - // Calculate power delivered. - var power = Math.Min(totalDemand, network.AvailableSupplyTotal); - - // Distribute load across supplies in network. - foreach (var supplyId in network.Supplies) - { - var supply = state.Supplies[supplyId]; - if (!supply.Enabled) - continue; - - if (supply.EffectiveMaxSupply != 0) - { - var ratio = supply.EffectiveMaxSupply / network.AvailableSupplyTotal; - - supply.CurrentSupply = ratio * power; - } - else - { - supply.CurrentSupply = 0; - } - - if (supply.MaxSupply != 0) - { - var ratio = supply.MaxSupply / network.TheoreticalSupplyTotal; - - supply.SupplyRampTarget = ratio * totalDemand; - } - else - { - supply.SupplyRampTarget = 0; - } - } - - // Distribute supply across subnet loads. - foreach (var subNet in subNets) - { - var rem = subNet.RemainingDemand; - var ratio = rem / totalDemand; - - subNet.LocalDemandMet += ratio * power; - } - } - - // Distribute power across loads in networks. - foreach (var network in state.Networks.Values) - { - if (network.LocalDemandMet == 0) - continue; - - foreach (var loadId in network.Loads) - { - var load = state.Loads[loadId]; - if (!load.Enabled) - continue; - - var ratio = load.DesiredPower / network.LocalDemandTotal; - load.ReceivingPower = ratio * network.LocalDemandMet; - } - } - - PowerSolverShared.UpdateRampPositions(frameTime, state); - } - - private int TotalSubLoadCount(PowerState state, Network network) - { - // TODO: Cycle detection. - var height = network.Loads.Count; - - foreach (var batteryId in network.BatteriesCharging) - { - var battery = state.Batteries[batteryId]; - if (battery.LinkedNetworkDischarging != default) - { - height += TotalSubLoadCount(state, state.Networks[battery.LinkedNetworkDischarging]); - } - } - - return height; - } - - private void GetLoadingNetworksRecursively( - PowerState state, - Network network, - List networks, - ref float totalDemand) - { - networks.Add(network); - totalDemand += network.LocalDemandTotal - network.LocalDemandMet; - - foreach (var batteryId in network.BatteriesCharging) - { - var battery = state.Batteries[batteryId]; - if (battery.LinkedNetworkDischarging != default) - { - GetLoadingNetworksRecursively( - state, - state.Networks[battery.LinkedNetworkDischarging], - networks, - ref totalDemand); - } - } - } - } -} diff --git a/Pow3r/Program.Simulation.cs b/Pow3r/Program.Simulation.cs index 440e262f83..2c8072e3e8 100644 --- a/Pow3r/Program.Simulation.cs +++ b/Pow3r/Program.Simulation.cs @@ -18,13 +18,11 @@ namespace Pow3r private readonly string[] _solverNames = { - nameof(GraphWalkSolver), nameof(BatteryRampPegSolver), nameof(NoOpSolver) }; private readonly IPowerSolver[] _solvers = { - new GraphWalkSolver(), new BatteryRampPegSolver(), new NoOpSolver() }; From 73e4946e27805b473e70d4949c03b1070e78b4b1 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sat, 31 Jul 2021 02:50:11 +0200 Subject: [PATCH 4/9] Pow3r goes brrr with generational IDs. --- .../Power/EntitySystems/PowerNetSystem.cs | 30 +- Content.Server/Power/Pow3r/PowerState.cs | 309 ++++++++++++++++-- Pow3r/Program.SaveLoad.cs | 11 +- Pow3r/Program.Simulation.cs | 6 - Pow3r/Program.UI.cs | 32 +- 5 files changed, 310 insertions(+), 78 deletions(-) diff --git a/Content.Server/Power/EntitySystems/PowerNetSystem.cs b/Content.Server/Power/EntitySystems/PowerNetSystem.cs index 84b65289b5..d8eee5d111 100644 --- a/Content.Server/Power/EntitySystems/PowerNetSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerNetSystem.cs @@ -19,7 +19,6 @@ namespace Content.Server.Power.EntitySystems private readonly HashSet _powerNetReconnectQueue = new(); private readonly HashSet _apcNetReconnectQueue = new(); - private int _nextId = 1; private readonly BatteryRampPegSolver _solver = new(); public override void Initialize() @@ -50,7 +49,7 @@ namespace Content.Server.Power.EntitySystems private void ApcPowerReceiverShutdown(EntityUid uid, ApcPowerReceiverComponent component, ComponentShutdown args) { - _powerState.Loads.Remove(component.NetworkLoad.Id); + _powerState.Loads.Free(component.NetworkLoad.Id); } private static void ApcPowerReceiverPaused( @@ -68,7 +67,7 @@ namespace Content.Server.Power.EntitySystems private void BatteryShutdown(EntityUid uid, PowerNetworkBatteryComponent component, ComponentShutdown args) { - _powerState.Batteries.Remove(component.NetworkBattery.Id); + _powerState.Batteries.Free(component.NetworkBattery.Id); } private static void BatteryPaused(EntityUid uid, PowerNetworkBatteryComponent component, EntityPausedEvent args) @@ -83,7 +82,7 @@ namespace Content.Server.Power.EntitySystems private void PowerConsumerShutdown(EntityUid uid, PowerConsumerComponent component, ComponentShutdown args) { - _powerState.Loads.Remove(component.NetworkLoad.Id); + _powerState.Loads.Free(component.NetworkLoad.Id); } private static void PowerConsumerPaused(EntityUid uid, PowerConsumerComponent component, EntityPausedEvent args) @@ -98,7 +97,7 @@ namespace Content.Server.Power.EntitySystems private void PowerSupplierShutdown(EntityUid uid, PowerSupplierComponent component, ComponentShutdown args) { - _powerState.Supplies.Remove(component.NetworkSupply.Id); + _powerState.Supplies.Free(component.NetworkSupply.Id); } private static void PowerSupplierPaused(EntityUid uid, PowerSupplierComponent component, EntityPausedEvent args) @@ -113,7 +112,7 @@ namespace Content.Server.Power.EntitySystems public void DestroyPowerNet(PowerNet powerNet) { - _powerState.Networks.Remove(powerNet.NetworkNode.Id); + _powerState.Networks.Free(powerNet.NetworkNode.Id); } public void QueueReconnectPowerNet(PowerNet powerNet) @@ -128,7 +127,7 @@ namespace Content.Server.Power.EntitySystems public void DestroyApcNet(ApcNet apcNet) { - _powerState.Networks.Remove(apcNet.NetworkNode.Id); + _powerState.Networks.Free(apcNet.NetworkNode.Id); } public void QueueReconnectApcNet(ApcNet apcNet) @@ -213,26 +212,22 @@ namespace Content.Server.Power.EntitySystems private void AllocLoad(PowerState.Load load) { - load.Id = AllocId(); - _powerState.Loads.Add(load.Id, load); + _powerState.Loads.Allocate(out load.Id) = load; } private void AllocSupply(PowerState.Supply supply) { - supply.Id = AllocId(); - _powerState.Supplies.Add(supply.Id, supply); + _powerState.Supplies.Allocate(out supply.Id) = supply; } private void AllocBattery(PowerState.Battery battery) { - battery.Id = AllocId(); - _powerState.Batteries.Add(battery.Id, battery); + _powerState.Batteries.Allocate(out battery.Id) = battery; } private void AllocNetwork(PowerState.Network network) { - network.Id = AllocId(); - _powerState.Networks.Add(network.Id, network); + _powerState.Networks.Allocate(out network.Id) = network; } private static void DoReconnectApcNet(ApcNet net) @@ -296,11 +291,6 @@ namespace Content.Server.Power.EntitySystems battery.NetworkBattery.LinkedNetworkDischarging = netNode.Id; } } - - private PowerState.NodeId AllocId() - { - return new(_nextId++); - } } /// diff --git a/Content.Server/Power/Pow3r/PowerState.cs b/Content.Server/Power/Pow3r/PowerState.cs index 276fc785a9..0d220b9259 100644 --- a/Content.Server/Power/Pow3r/PowerState.cs +++ b/Content.Server/Power/Pow3r/PowerState.cs @@ -1,40 +1,51 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Numerics; +using System.Linq; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; +using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Content.Server.Power.Pow3r { public sealed class PowerState { - public const int MaxTickData = 180; - public static readonly JsonSerializerOptions SerializerOptions = new() { IncludeFields = true, Converters = {new NodeIdJsonConverter()} }; - public Dictionary Supplies = new(); - public Dictionary Networks = new(); - public Dictionary Loads = new(); - public Dictionary Batteries = new(); + public GenIdStorage Supplies = new(); + public GenIdStorage Networks = new(); + public GenIdStorage Loads = new(); + public GenIdStorage Batteries = new(); public readonly struct NodeId : IEquatable { - public readonly int Id; + public readonly int Index; + public readonly int Generation; - public NodeId(int id) + public long Combined => (uint) Index | ((long) Generation << 32); + + public NodeId(int index, int generation) { - Id = id; + Index = index; + Generation = generation; + } + + public NodeId(long combined) + { + Index = (int) combined; + Generation = (int) (combined >> 32); } public bool Equals(NodeId other) { - return Id == other.Id; + return Index == other.Index && Generation == other.Generation; } public override bool Equals(object? obj) @@ -44,7 +55,7 @@ namespace Content.Server.Power.Pow3r public override int GetHashCode() { - return Id; + return HashCode.Combine(Index, Generation); } public static bool operator ==(NodeId left, NodeId right) @@ -59,7 +70,261 @@ namespace Content.Server.Power.Pow3r public override string ToString() { - return Id.ToString(); + return $"{Index} (G{Generation})"; + } + } + + public static class GenIdStorage + { + public static GenIdStorage FromEnumerable(IEnumerable<(NodeId, T)> enumerable) + { + return GenIdStorage.FromEnumerable(enumerable); + } + } + + public sealed class GenIdStorage + { + // This is an implementation of "generational index" storage. + // + // The advantage of this storage method is extremely fast, O(1) lookup (way faster than Dictionary). + // Resolving a value in the storage is a single array load and generation compare. Extremely fast. + // Indices can also be cached into temporary + // Disadvantages are that storage cannot be shrunk, and sparse storage is inefficient space wise. + // Also this implementation does not have optimizations necessary to make sparse iteration efficient. + // + // The idea here is that the index type (NodeId in this case) has both an index and a generation. + // The index is an integer index into the storage array, the generation is used to avoid use-after-free. + // + // Empty slots in the array form a linked list of free slots. + // When we allocate a new slot, we pop one link off this linked list and hand out its index + generation. + // + // When we free a node, we bump the generation of the slot and make it the head of the linked list. + // The generation being bumped means that any IDs to this slot will fail to resolve (generation mismatch). + // + + // Index of the next free slot to use when allocating a new one. + // If this is int.MaxValue, + // it basically means "no slot available" and the next allocation call should resize the array storage. + private int _nextFree = int.MaxValue; + private Slot[] _storage; + + public int Count { get; private set; } + + public ref T this[NodeId id] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ref var slot = ref _storage[id.Index]; + if (slot.Generation != id.Generation) + ThrowKeyNotFound(); + + return ref slot.Value; + } + } + + public GenIdStorage() + { + _storage = Array.Empty(); + } + + public static GenIdStorage FromEnumerable(IEnumerable<(NodeId, T)> enumerable) + { + var storage = new GenIdStorage(); + + // Cache enumerable to array to do double enumeration. + var cache = enumerable.ToArray(); + + if (cache.Length == 0) + return storage; + + // Figure out max size necessary and set storage size to that. + var maxSize = cache.Max(tup => tup.Item1.Index) + 1; + storage._storage = new Slot[maxSize]; + + // Fill in slots. + foreach (var (id, value) in cache) + { + DebugTools.Assert(id.Generation != 0, "Generation cannot be 0"); + + ref var slot = ref storage._storage[id.Index]; + DebugTools.Assert(slot.Generation == 0, "Duplicate key index!"); + + slot.Generation = id.Generation; + slot.Value = value; + } + + // Go through empty slots and build the free chain. + var nextFree = int.MaxValue; + for (var i = 0; i < storage._storage.Length; i++) + { + ref var slot = ref storage._storage[i]; + + if (slot.Generation != 0) + // Slot in use. + continue; + + slot.NextSlot = nextFree; + nextFree = i; + } + + storage.Count = cache.Length; + storage._nextFree = nextFree; + + return storage; + } + + public ref T Allocate(out NodeId id) + { + if (_nextFree == int.MaxValue) + ReAllocate(); + + var idx = _nextFree; + ref var slot = ref _storage[idx]; + + Count += 1; + _nextFree = slot.NextSlot; + // NextSlot = -1 indicates filled. + slot.NextSlot = -1; + + id = new NodeId(idx, slot.Generation); + return ref slot.Value; + } + + public void Free(NodeId id) + { + var idx = id.Index; + ref var slot = ref _storage[idx]; + if (slot.Generation != id.Generation) + ThrowKeyNotFound(); + + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + slot.Value = default!; + + Count -= 1; + slot.Generation += 1; + slot.NextSlot = _nextFree; + _nextFree = idx; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ReAllocate() + { + var oldLength = _storage.Length; + var newLength = Math.Max(oldLength, 2) * 2; + + ReAllocateTo(newLength); + } + + private void ReAllocateTo(int newSize) + { + var oldLength = _storage.Length; + DebugTools.Assert(newSize >= oldLength, "Cannot shrink GenIdStorage"); + + Array.Resize(ref _storage, newSize); + + for (var i = oldLength; i < newSize - 1; i++) + { + // Build linked list chain for newly allocated segment. + ref var slot = ref _storage[i]; + slot.NextSlot = i + 1; + // Every slot starts at generation 1. + slot.Generation = 1; + } + + _storage[^1].NextSlot = _nextFree; + + _nextFree = oldLength; + } + + public ValuesCollection Values => new(this); + + private struct Slot + { + // Next link on the free list. if int.MaxValue then this is the tail. + // If negative, this slot is occupied. + public int NextSlot; + // Generation of this slot. + public int Generation; + public T Value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowKeyNotFound() + { + throw new KeyNotFoundException(); + } + + public readonly struct ValuesCollection : IReadOnlyCollection + { + private readonly GenIdStorage _owner; + + public ValuesCollection(GenIdStorage owner) + { + _owner = owner; + } + + public Enumerator GetEnumerator() + { + return new Enumerator(_owner); + } + + public int Count => _owner.Count; + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator + { + // Save the array in the enumerator here to avoid a few pointer dereferences. + private readonly Slot[] _owner; + private int _index; + + public Enumerator(GenIdStorage owner) + { + _owner = owner._storage; + Current = default!; + _index = -1; + } + + public bool MoveNext() + { + while (true) + { + _index += 1; + if (_index >= _owner.Length) + return false; + + ref var slot = ref _owner[_index]; + + if (slot.NextSlot < 0) + { + Current = slot.Value; + return true; + } + } + } + + public void Reset() + { + _index = -1; + } + + object IEnumerator.Current => Current!; + + public T Current { get; private set; } + + public void Dispose() + { + } + } } } @@ -67,12 +332,12 @@ namespace Content.Server.Power.Pow3r { public override NodeId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return new(reader.GetInt32()); + return new NodeId(reader.GetInt64()); } public override void Write(Utf8JsonWriter writer, NodeId value, JsonSerializerOptions options) { - writer.WriteNumberValue(value.Id); + writer.WriteNumberValue(value.Combined); } } @@ -186,22 +451,8 @@ namespace Content.Server.Power.Pow3r // "Supplying" means the network is connected to the OUTPUT port of the battery. [ViewVariables] public List BatteriesDischarging = new(); - // Calculation parameters used by GraphWalkSolver. - // Unused by BatteryRampPegSolver. - [JsonIgnore] public float LocalDemandTotal; - [JsonIgnore] public float LocalDemandMet; - [JsonIgnore] public float GroupDemandTotal; - [JsonIgnore] public float GroupDemandMet; - [ViewVariables] [JsonIgnore] public int Height; [JsonIgnore] public bool HeightTouched; - - // Supply available this tick. - [JsonIgnore] public float AvailableSupplyTotal; - - // Max theoretical supply assuming max ramp. - [JsonIgnore] public float TheoreticalSupplyTotal; - public float RemainingDemand => LocalDemandTotal - LocalDemandMet; } } } diff --git a/Pow3r/Program.SaveLoad.cs b/Pow3r/Program.SaveLoad.cs index 2ffdc8eb4e..5fff64f662 100644 --- a/Pow3r/Program.SaveLoad.cs +++ b/Pow3r/Program.SaveLoad.cs @@ -20,15 +20,14 @@ namespace Pow3r return; _paused = dat.Paused; - _nextId = dat.NextId; _currentSolver = dat.Solver; _state = new PowerState { - Networks = dat.Networks.ToDictionary(n => n.Id, n => n), - Supplies = dat.Supplies.ToDictionary(s => s.Id, s => s), - Loads = dat.Loads.ToDictionary(l => l.Id, l => l), - Batteries = dat.Batteries.ToDictionary(b => b.Id, b => b) + Networks = GenIdStorage.FromEnumerable(dat.Networks.Select(n => (n.Id, n))), + Supplies = GenIdStorage.FromEnumerable(dat.Supplies.Select(s => (s.Id, s))), + Loads = GenIdStorage.FromEnumerable(dat.Loads.Select(l => (l.Id, l))), + Batteries = GenIdStorage.FromEnumerable(dat.Batteries.Select(b => (b.Id, b))) }; _displayLoads = dat.Loads.ToDictionary(n => n.Id, _ => new DisplayLoad()); @@ -44,7 +43,6 @@ namespace Pow3r var data = new DiskDat { Paused = _paused, - NextId = _nextId, Solver = _currentSolver, Loads = _state.Loads.Values.ToList(), @@ -59,7 +57,6 @@ namespace Pow3r private sealed class DiskDat { public bool Paused; - public int NextId; public int Solver; public List Loads; diff --git a/Pow3r/Program.Simulation.cs b/Pow3r/Program.Simulation.cs index 2c8072e3e8..fac2dbc415 100644 --- a/Pow3r/Program.Simulation.cs +++ b/Pow3r/Program.Simulation.cs @@ -10,7 +10,6 @@ namespace Pow3r { private const int MaxTickData = 180; - private int _nextId = 1; private PowerState _state = new(); private Network _linking; private int _tickDataIdx; @@ -33,11 +32,6 @@ namespace Pow3r private readonly Queue _remQueue = new(); private readonly Stopwatch _simStopwatch = new Stopwatch(); - private NodeId AllocId() - { - return new(_nextId++); - } - private void Tick(float frameTime) { if (_paused) diff --git a/Pow3r/Program.UI.cs b/Pow3r/Program.UI.cs index 96f7b90cf8..9a99a52699 100644 --- a/Pow3r/Program.UI.cs +++ b/Pow3r/Program.UI.cs @@ -36,30 +36,30 @@ namespace Pow3r if (Button("Generator")) { - var id = AllocId(); - _state.Supplies.Add(id, new Supply { Id = id }); - _displaySupplies.Add(id, new DisplaySupply()); + var supply = new Supply(); + _state.Supplies.Allocate(out supply.Id) = supply; + _displaySupplies.Add(supply.Id, new DisplaySupply()); } if (Button("Load")) { - var id = AllocId(); - _state.Loads.Add(id, new Load { Id = id }); - _displayLoads.Add(id, new DisplayLoad()); + var load = new Load(); + _state.Loads.Allocate(out load.Id) = load; + _displayLoads.Add(load.Id, new DisplayLoad()); } if (Button("Network")) { - var id = AllocId(); - _state.Networks.Add(id, new Network { Id = id }); - _displayNetworks.Add(id, new DisplayNetwork()); + var network = new Network(); + _state.Networks.Allocate(out network.Id) = network; + _displayNetworks.Add(network.Id, new DisplayNetwork()); } if (Button("Battery")) { - var id = AllocId(); - _state.Batteries.Add(id, new Battery { Id = id }); - _displayBatteries.Add(id, new DisplayBattery()); + var battery = new Battery(); + _state.Batteries.Allocate(out battery.Id) = battery; + _displayBatteries.Add(battery.Id, new DisplayBattery()); } Checkbox("Paused", ref _paused); @@ -355,25 +355,25 @@ namespace Pow3r switch (item) { case Network n: - _state.Networks.Remove(n.Id); + _state.Networks.Free(n.Id); _displayNetworks.Remove(n.Id); reLink = true; break; case Supply s: - _state.Supplies.Remove(s.Id); + _state.Supplies.Free(s.Id); _state.Networks.Values.ForEach(n => n.Supplies.Remove(s.Id)); _displaySupplies.Remove(s.Id); break; case Load l: - _state.Loads.Remove(l.Id); + _state.Loads.Free(l.Id); _state.Networks.Values.ForEach(n => n.Loads.Remove(l.Id)); _displayLoads.Remove(l.Id); break; case Battery b: - _state.Batteries.Remove(b.Id); + _state.Batteries.Free(b.Id); _state.Networks.Values.ForEach(n => n.BatteriesCharging.Remove(b.Id)); _state.Networks.Values.ForEach(n => n.BatteriesDischarging.Remove(b.Id)); _displayBatteries.Remove(b.Id); From 632e72b817c110dcda6fa008ff55d26136753be9 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sat, 31 Jul 2021 03:14:00 +0200 Subject: [PATCH 5/9] Improve hands & pulling (#4389) --- .../Actions/ClientActionsComponent.cs | 38 +--- .../Hands/HandVirtualPullItemStatus.xaml | 3 + .../Hands/HandVirtualPullItemStatus.xaml.cs | 13 ++ Content.Client/Hands/HandsComponent.cs | 127 +----------- Content.Client/Hands/HandsGui.xaml | 6 + .../Hands/{HandsGui.cs => HandsGui.xaml.cs} | 142 ++++++------- Content.Client/Hands/HandsSystem.cs | 80 -------- .../Hands/Systems/HandVirtualPullSystem.cs | 18 ++ Content.Client/Hands/Systems/HandsSystem.cs | 149 ++++++++++++++ Content.Client/Items/ItemStatusMessages.cs | 32 +++ .../Items/Managers/IItemSlotManager.cs | 19 +- .../Items/Managers/ItemSlotManager.cs | 56 +++++- Content.Client/Items/UI/ItemSlotButton.cs | 35 +++- Content.Client/Items/UI/ItemStatusPanel.cs | 40 +++- .../Fluids/Components/SpillableComponent.cs | 1 + .../Hands/Components/HandsComponent.cs | 97 ++------- .../Hands/Systems/HandVirtualPullSystem.cs | 57 ++++++ .../Hands/{ => Systems}/HandsSystem.cs | 136 ++++++++++++- .../Interaction/InteractionSystem.cs | 23 ++- .../Components/HandVirtualPullComponent.cs | 50 +++++ .../Hands/Components/SharedHandsComponent.cs | 186 +++++++----------- Content.Shared/Hands/SharedHandsSystem.cs | 48 ++--- Content.Shared/Interaction/BeforeInteract.cs | 53 +++++ .../{DragDrop => Interaction}/IDropped.cs | 2 +- .../Components/SharedPullableComponent.cs | 40 +--- .../Components/SharedPullerComponent.cs | 29 +-- .../Events}/PullAttemptMessage.cs | 0 .../Pull => Pulling/Events}/PullMessage.cs | 2 +- .../Events}/PullStartedMessage.cs | 0 .../Events}/PullStoppedMessage.cs | 0 .../Pulling/Systems/SharedPullerSystem.cs | 44 +++++ .../{ => Systems}/SharedPullingSystem.cs | 23 +++ .../Entities/Virtual/virtual_pull_item.yml | 8 + 33 files changed, 945 insertions(+), 612 deletions(-) create mode 100644 Content.Client/Hands/HandVirtualPullItemStatus.xaml create mode 100644 Content.Client/Hands/HandVirtualPullItemStatus.xaml.cs create mode 100644 Content.Client/Hands/HandsGui.xaml rename Content.Client/Hands/{HandsGui.cs => HandsGui.xaml.cs} (68%) delete mode 100644 Content.Client/Hands/HandsSystem.cs create mode 100644 Content.Client/Hands/Systems/HandVirtualPullSystem.cs create mode 100644 Content.Client/Hands/Systems/HandsSystem.cs create mode 100644 Content.Client/Items/ItemStatusMessages.cs create mode 100644 Content.Server/Hands/Systems/HandVirtualPullSystem.cs rename Content.Server/Hands/{ => Systems}/HandsSystem.cs (58%) create mode 100644 Content.Shared/Hands/Components/HandVirtualPullComponent.cs create mode 100644 Content.Shared/Interaction/BeforeInteract.cs rename Content.Shared/{DragDrop => Interaction}/IDropped.cs (97%) rename Content.Shared/{Physics/Pull => Pulling/Events}/PullAttemptMessage.cs (100%) rename Content.Shared/{Physics/Pull => Pulling/Events}/PullMessage.cs (87%) rename Content.Shared/{Physics/Pull => Pulling/Events}/PullStartedMessage.cs (100%) rename Content.Shared/{Physics/Pull => Pulling/Events}/PullStoppedMessage.cs (100%) create mode 100644 Content.Shared/Pulling/Systems/SharedPullerSystem.cs rename Content.Shared/Pulling/{ => Systems}/SharedPullingSystem.cs (88%) create mode 100644 Resources/Prototypes/Entities/Virtual/virtual_pull_item.yml diff --git a/Content.Client/Actions/ClientActionsComponent.cs b/Content.Client/Actions/ClientActionsComponent.cs index 6df12b2581..3caab80cc6 100644 --- a/Content.Client/Actions/ClientActionsComponent.cs +++ b/Content.Client/Actions/ClientActionsComponent.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; using Content.Client.Actions.Assignments; using Content.Client.Actions.UI; using Content.Client.Hands; using Content.Client.Inventory; -using Content.Client.Items.UI; +using Content.Client.Items.Managers; using Content.Shared.Actions.Components; using Content.Shared.Actions.Prototypes; using Robust.Client.GameObjects; @@ -26,12 +25,13 @@ namespace Content.Client.Actions public const byte Slots = 10; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IItemSlotManager _itemSlotManager = default!; [ComponentDependency] private readonly HandsComponent? _handsComponent = null; [ComponentDependency] private readonly ClientInventoryComponent? _inventoryComponent = null; private ActionsUI? _ui; - private readonly List _highlightingItemSlots = new(); + private EntityUid _highlightedEntity; /// /// Current assignments for all hotbars / slots for this entity. @@ -225,26 +225,8 @@ namespace Content.Client.Actions { StopHighlightingItemSlots(); - // figure out if it's in hand or inventory and highlight it - foreach (var hand in _handsComponent!.Gui!.Hands) - { - if (hand.HeldItem != item || hand.HandButton == null) continue; - _highlightingItemSlots.Add(hand.HandButton); - hand.HandButton.Highlight(true); - return; - } - - foreach (var (slot, slotItem) in _inventoryComponent!.AllSlots) - { - if (slotItem != item) continue; - foreach (var itemSlotButton in - _inventoryComponent.InterfaceController.GetItemSlotButtons(slot)) - { - _highlightingItemSlots.Add(itemSlotButton); - itemSlotButton.Highlight(true); - } - return; - } + _highlightedEntity = item.Uid; + _itemSlotManager.HighlightEntity(item.Uid); } /// @@ -252,11 +234,11 @@ namespace Content.Client.Actions /// public void StopHighlightingItemSlots() { - foreach (var itemSlot in _highlightingItemSlots) - { - itemSlot.Highlight(false); - } - _highlightingItemSlots.Clear(); + if (_highlightedEntity == default) + return; + + _itemSlotManager.UnHighlightEntity(_highlightedEntity); + _highlightedEntity = default; } public void ToggleActionsMenu() diff --git a/Content.Client/Hands/HandVirtualPullItemStatus.xaml b/Content.Client/Hands/HandVirtualPullItemStatus.xaml new file mode 100644 index 0000000000..9fac1e0d08 --- /dev/null +++ b/Content.Client/Hands/HandVirtualPullItemStatus.xaml @@ -0,0 +1,3 @@ + + diff --git a/Content.Client/Hands/HandVirtualPullItemStatus.xaml.cs b/Content.Client/Hands/HandVirtualPullItemStatus.xaml.cs new file mode 100644 index 0000000000..f4324f6e41 --- /dev/null +++ b/Content.Client/Hands/HandVirtualPullItemStatus.xaml.cs @@ -0,0 +1,13 @@ +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Hands +{ + public sealed class HandVirtualPullItemStatus : Control + { + public HandVirtualPullItemStatus() + { + RobustXamlLoader.Load(this); + } + } +} diff --git a/Content.Client/Hands/HandsComponent.cs b/Content.Client/Hands/HandsComponent.cs index 0299384e33..4276ca1e9c 100644 --- a/Content.Client/Hands/HandsComponent.cs +++ b/Content.Client/Hands/HandsComponent.cs @@ -1,15 +1,8 @@ using System.Collections.Generic; -using Content.Client.Animations; -using Content.Client.HUD; using Content.Shared.Hands.Components; using Content.Shared.Item; using Robust.Shared.Containers; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Network; -using Robust.Shared.Players; -using Robust.Shared.Timing; -using Robust.Shared.ViewVariables; namespace Content.Client.Hands { @@ -18,16 +11,7 @@ namespace Content.Client.Hands [ComponentReference(typeof(SharedHandsComponent))] public class HandsComponent : SharedHandsComponent { - [Dependency] private readonly IGameHud _gameHud = default!; - - [ViewVariables] - public HandsGui? Gui { get; private set; } - - protected override void OnRemove() - { - ClearGui(); - base.OnRemove(); - } + public HandsGui? Gui { get; set; } public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) { @@ -38,94 +22,23 @@ namespace Content.Client.Hands foreach (var handState in state.Hands) { - var newHand = new Hand(handState.Name, handState.Enabled, handState.Location); + var newHand = new Hand(handState.Name, handState.Location); Hands.Add(newHand); } + ActiveHand = state.ActiveHand; UpdateHandContainers(); UpdateHandVisualizer(); - UpdateHandsGuiState(); - } - - public void SettupGui() - { - if (Gui == null) - { - Gui = new HandsGui(); - _gameHud.HandsContainer.AddChild(Gui); - Gui.HandClick += args => OnHandClick(args.HandClicked); - Gui.HandActivate += args => OnActivateInHand(args.HandUsed); - UpdateHandsGuiState(); - } - } - - public void ClearGui() - { - Gui?.Dispose(); - Gui = null; - } - - public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null) - { - base.HandleNetworkMessage(message, netChannel, session); - - switch (message) - { - case PickupAnimationMessage msg: - RunPickupAnimation(msg); - break; - } + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new HandsModifiedMessage { Hands = this }); } public override void HandsModified() { - base.HandsModified(); - UpdateHandContainers(); UpdateHandVisualizer(); - UpdateHandsGuiState(); - } - private void OnHandClick(string handClicked) - { - if (!TryGetHand(handClicked, out var pressedHand)) - return; - - if (!TryGetActiveHand(out var activeHand)) - return; - - var pressedEntity = pressedHand.HeldEntity; - var activeEntity = activeHand.HeldEntity; - - if (pressedHand == activeHand && activeEntity != null) - { - SendNetworkMessage(new UseInHandMsg()); //use item in hand - return; - } - - if (pressedHand != activeHand && pressedEntity == null) - { - SendNetworkMessage(new ClientChangedHandMsg(pressedHand.Name)); //swap hand - return; - } - - if (pressedHand != activeHand && pressedEntity != null && activeEntity != null) - { - SendNetworkMessage(new ClientAttackByInHandMsg(pressedHand.Name)); //use active item on held item - return; - } - - if (pressedHand != activeHand && pressedEntity != null && activeEntity == null) - { - SendNetworkMessage(new MoveItemFromHandMsg(pressedHand.Name)); //move item in hand to active hand - return; - } - } - - private void OnActivateInHand(string handActivated) - { - SendNetworkMessage(new ActivateInHandMsg(handActivated)); + base.HandsModified(); } public void UpdateHandContainers() @@ -149,27 +62,10 @@ namespace Content.Client.Hands appearance.SetData(HandsVisuals.VisualState, GetHandsVisualState()); } - public void UpdateHandsGuiState() - { - Gui?.SetState(GetHandsGuiState()); - } - - private HandsGuiState GetHandsGuiState() - { - var handStates = new List(); - - foreach (var hand in ReadOnlyHands) - { - var handState = new GuiHand(hand.Name, hand.Location, hand.HeldEntity, hand.Enabled); - handStates.Add(handState); - } - return new HandsGuiState(handStates, ActiveHand); - } - private HandsVisualState GetHandsVisualState() { var hands = new List(); - foreach (var hand in ReadOnlyHands) + foreach (var hand in Hands) { if (hand.HeldEntity == null) continue; @@ -182,16 +78,5 @@ namespace Content.Client.Hands } return new(hands); } - - private void RunPickupAnimation(PickupAnimationMessage msg) - { - if (!Owner.EntityManager.TryGetEntity(msg.EntityUid, out var entity)) - return; - - if (!IoCManager.Resolve().IsFirstTimePredicted) - return; - - ReusableAnimations.AnimateEntityPickup(entity, msg.InitialPosition, msg.PickupDirection); - } } } diff --git a/Content.Client/Hands/HandsGui.xaml b/Content.Client/Hands/HandsGui.xaml new file mode 100644 index 0000000000..d39c044821 --- /dev/null +++ b/Content.Client/Hands/HandsGui.xaml @@ -0,0 +1,6 @@ + + + + + + diff --git a/Content.Client/Hands/HandsGui.cs b/Content.Client/Hands/HandsGui.xaml.cs similarity index 68% rename from Content.Client/Hands/HandsGui.cs rename to Content.Client/Hands/HandsGui.xaml.cs index ff0fa47b23..63590882bf 100644 --- a/Content.Client/Hands/HandsGui.cs +++ b/Content.Client/Hands/HandsGui.xaml.cs @@ -1,87 +1,84 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Content.Client.HUD; using Content.Client.Items.Managers; using Content.Client.Items.UI; using Content.Client.Resources; -using Content.Shared; using Content.Shared.CCVar; using Content.Shared.Hands.Components; -using Content.Shared.Input; +using Robust.Client.AutoGenerated; using Robust.Client.Graphics; -using Robust.Client.Player; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Timing; using Robust.Shared.ViewVariables; -using static Robust.Client.UserInterface.Controls.BoxContainer; namespace Content.Client.Hands { - public class HandsGui : Control + [GenerateTypedNameReferences] + public sealed partial class HandsGui : Control { [Dependency] private readonly IResourceCache _resourceCache = default!; [Dependency] private readonly IItemSlotManager _itemSlotManager = default!; [Dependency] private readonly IGameHud _gameHud = default!; [Dependency] private readonly INetConfigurationManager _configManager = default!; + private readonly HandsSystem _handsSystem; + private readonly HandsComponent _handsComponent; + private Texture StorageTexture => _gameHud.GetHudTexture("back.png"); private Texture BlockedTexture => _resourceCache.GetTexture("/Textures/Interface/Inventory/blocked.png"); private ItemStatusPanel StatusPanel { get; } - private BoxContainer HandsContainer { get; } - - [ViewVariables] - public IReadOnlyList Hands => _hands; - private List _hands = new(); + [ViewVariables] private GuiHand[] _hands = Array.Empty(); private string? ActiveHand { get; set; } - public Action? HandClick; //TODO: Move to Eventbus - - public Action? HandActivate; //TODO: Move to Eventbus - - public HandsGui() + public HandsGui(HandsComponent hands, HandsSystem handsSystem) { - IoCManager.InjectDependencies(this); - _configManager.OnValueChanged(CCVars.HudTheme, UpdateHudTheme); + _handsComponent = hands; + _handsSystem = handsSystem; - AddChild(new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - SeparationOverride = 0, - HorizontalAlignment = HAlignment.Center, - Children = - { - new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - Children = - { - (StatusPanel = ItemStatusPanel.FromSide(HandLocation.Middle)), - (HandsContainer = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - HorizontalAlignment = HAlignment.Center - }), - } - }, - } - }); + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + StatusPanel = ItemStatusPanel.FromSide(HandLocation.Middle); + StatusContainer.AddChild(StatusPanel); + StatusPanel.SetPositionFirst(); } - public void SetState(HandsGuiState state) + protected override void EnteredTree() { + base.EnteredTree(); + + _handsSystem.GuiStateUpdated += HandsSystemOnGuiStateUpdated; + _configManager.OnValueChanged(CCVars.HudTheme, UpdateHudTheme); + + HandsSystemOnGuiStateUpdated(); + } + + protected override void ExitedTree() + { + base.ExitedTree(); + + _handsSystem.GuiStateUpdated -= HandsSystemOnGuiStateUpdated; + _configManager.UnsubValueChanged(CCVars.HudTheme, UpdateHudTheme); + } + + private void HandsSystemOnGuiStateUpdated() + { + var state = _handsSystem.GetGuiState(); + ActiveHand = state.ActiveHand; _hands = state.GuiHands; + Array.Sort(_hands, HandOrderComparer.Instance); UpdateGui(); } @@ -97,12 +94,15 @@ namespace Content.Client.Hands var handName = hand.Name; newButton.OnPressed += args => OnHandPressed(args, handName); - newButton.OnStoragePressed += args => OnStoragePressed(handName); - - newButton.Blocked.Visible = !hand.Enabled; + newButton.OnStoragePressed += _ => OnStoragePressed(handName); _itemSlotManager.SetItemSlot(newButton, hand.HeldItem); + + // Show blocked overlay if hand is pulling. + newButton.Blocked.Visible = + hand.HeldItem != null && hand.HeldItem.HasComponent(); } + if (TryGetActiveHand(out var activeHand)) { activeHand.HandButton.SetActiveHand(true); @@ -114,7 +114,7 @@ namespace Content.Client.Hands { if (args.Function == EngineKeyFunctions.UIClick) { - HandClick?.Invoke(new HandClickEventArgs(handName)); + _handsSystem.UIHandClick(_handsComponent, handName); } else if (TryGetHand(handName, out var hand)) { @@ -124,7 +124,7 @@ namespace Content.Client.Hands private void OnStoragePressed(string handName) { - HandActivate?.Invoke(new HandActivateEventArgs(handName)); + _handsSystem.UIHandActivate(handName); } private bool TryGetActiveHand([NotNullWhen(true)] out GuiHand? activeHand) @@ -145,6 +145,7 @@ namespace Content.Client.Hands if (hand.Name == handName) foundHand = hand; } + return foundHand != null; } @@ -153,7 +154,9 @@ namespace Content.Client.Hands base.FrameUpdate(args); foreach (var hand in _hands) + { _itemSlotManager.UpdateCooldown(hand.HandButton, hand.HeldItem); + } } private HandButton MakeHandButton(HandLocation buttonLocation) @@ -173,23 +176,31 @@ namespace Content.Client.Hands UpdateGui(); } - public class HandClickEventArgs + private sealed class HandOrderComparer : IComparer { - public string HandClicked { get; } + public static readonly HandOrderComparer Instance = new(); - public HandClickEventArgs(string handClicked) + public int Compare(GuiHand? x, GuiHand? y) { - HandClicked = handClicked; - } - } + if (ReferenceEquals(x, y)) return 0; + if (ReferenceEquals(null, y)) return 1; + if (ReferenceEquals(null, x)) return -1; - public class HandActivateEventArgs - { - public string HandUsed { get; } + var orderX = Map(x.HandLocation); + var orderY = Map(y.HandLocation); - public HandActivateEventArgs(string handUsed) - { - HandUsed = handUsed; + return orderX.CompareTo(orderY); + + static int Map(HandLocation loc) + { + return loc switch + { + HandLocation.Left => 3, + HandLocation.Middle => 2, + HandLocation.Right => 1, + _ => throw new ArgumentOutOfRangeException(nameof(loc), loc, null) + }; + } } } } @@ -203,7 +214,7 @@ namespace Content.Client.Hands /// The set of hands to be displayed. /// [ViewVariables] - public List GuiHands { get; } = new(); + public GuiHand[] GuiHands { get; } /// /// The name of the currently active hand. @@ -211,7 +222,7 @@ namespace Content.Client.Hands [ViewVariables] public string? ActiveHand { get; } - public HandsGuiState(List guiHands, string? activeHand = null) + public HandsGuiState(GuiHand[] guiHands, string? activeHand = null) { GuiHands = guiHands; ActiveHand = activeHand; @@ -247,18 +258,11 @@ namespace Content.Client.Hands [ViewVariables] public HandButton HandButton { get; set; } = default!; - /// - /// If this hand can be used by the player. - /// - [ViewVariables] - public bool Enabled { get; } - - public GuiHand(string name, HandLocation handLocation, IEntity? heldItem, bool enabled) + public GuiHand(string name, HandLocation handLocation, IEntity? heldItem) { Name = name; HandLocation = handLocation; HeldItem = heldItem; - Enabled = enabled; } } } diff --git a/Content.Client/Hands/HandsSystem.cs b/Content.Client/Hands/HandsSystem.cs deleted file mode 100644 index 4ba3a73c6f..0000000000 --- a/Content.Client/Hands/HandsSystem.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Content.Shared.Hands; -using Content.Shared.Hands.Components; -using Content.Shared.Input; -using Robust.Client.GameObjects; -using Robust.Shared.Containers; -using Robust.Shared.GameObjects; -using Robust.Shared.Input.Binding; -using Robust.Shared.Map; -using Robust.Shared.Players; - -namespace Content.Client.Hands -{ - internal sealed class HandsSystem : SharedHandsSystem - { - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent((_, component, _) => component.SettupGui()); - SubscribeLocalEvent((_, component, _) => component.ClearGui()); - - CommandBinds.Builder - .Bind(ContentKeyFunctions.SwapHands, InputCmdHandler.FromDelegate(SwapHandsPressed)) - .Bind(ContentKeyFunctions.Drop, new PointerInputCmdHandler(DropPressed)) - .Register(); - } - - public override void Shutdown() - { - CommandBinds.Unregister(); - base.Shutdown(); - } - - private void SwapHandsPressed(ICommonSession? session) - { - if (session == null) - return; - - var player = session.AttachedEntity; - - if (player == null) - return; - - if (!player.TryGetComponent(out SharedHandsComponent? hands)) - return; - - if (!hands.TryGetSwapHandsResult(out var nextHand)) - return; - - EntityManager.RaisePredictiveEvent(new RequestSetHandEvent(nextHand)); - } - - private bool DropPressed(ICommonSession? session, EntityCoordinates coords, EntityUid uid) - { - if (session == null) - return false; - - var player = session.AttachedEntity; - - if (player == null) - return false; - - if (!player.TryGetComponent(out SharedHandsComponent? hands)) - return false; - - var activeHand = hands.ActiveHand; - - if (activeHand == null) - return false; - - EntityManager.RaisePredictiveEvent(new RequestDropHeldEntityEvent(activeHand, coords)); - return true; - } - - protected override void HandleContainerModified(EntityUid uid, SharedHandsComponent component, ContainerModifiedMessage args) - { - component.HandsModified(); - } - } -} diff --git a/Content.Client/Hands/Systems/HandVirtualPullSystem.cs b/Content.Client/Hands/Systems/HandVirtualPullSystem.cs new file mode 100644 index 0000000000..0f47efd9dc --- /dev/null +++ b/Content.Client/Hands/Systems/HandVirtualPullSystem.cs @@ -0,0 +1,18 @@ +using Content.Client.Items; +using Content.Shared.Hands.Components; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; + +namespace Content.Client.Hands +{ + [UsedImplicitly] + public sealed class HandVirtualPullSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + Subs.ItemStatus(_ => new HandVirtualPullItemStatus()); + } + } +} diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs new file mode 100644 index 0000000000..5f2e40f2e5 --- /dev/null +++ b/Content.Client/Hands/Systems/HandsSystem.cs @@ -0,0 +1,149 @@ +using System; +using System.Linq; +using Content.Client.Animations; +using Content.Client.HUD; +using Content.Shared.Hands; +using Content.Shared.Hands.Components; +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Client.Player; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Input.Binding; +using Robust.Shared.IoC; +using Robust.Shared.Timing; + +namespace Content.Client.Hands +{ + [UsedImplicitly] + public sealed class HandsSystem : SharedHandsSystem + { + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IGameHud _gameHud = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + + public event Action? GuiStateUpdated; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(HandlePlayerAttached); + SubscribeLocalEvent(HandlePlayerDetached); + SubscribeLocalEvent(HandleCompRemove); + SubscribeLocalEvent(HandleHandsModified); + + SubscribeNetworkEvent(HandlePickupAnimation); + } + + public override void Shutdown() + { + CommandBinds.Unregister(); + base.Shutdown(); + } + + private void HandleHandsModified(HandsModifiedMessage ev) + { + if (ev.Hands.Owner == _playerManager.LocalPlayer?.ControlledEntity) + GuiStateUpdated?.Invoke(); + } + + protected override void HandleContainerModified(EntityUid uid, SharedHandsComponent component, ContainerModifiedMessage args) + { + if (uid == _playerManager.LocalPlayer?.ControlledEntity?.Uid) + GuiStateUpdated?.Invoke(); + } + + private void HandlePickupAnimation(PickupAnimationMessage msg) + { + if (!EntityManager.TryGetEntity(msg.EntityUid, out var entity)) + return; + + if (!_gameTiming.IsFirstTimePredicted) + return; + + ReusableAnimations.AnimateEntityPickup(entity, msg.InitialPosition, msg.PickupDirection); + } + + public HandsGuiState GetGuiState() + { + var player = _playerManager.LocalPlayer?.ControlledEntity; + + if (player == null || !player.TryGetComponent(out HandsComponent? hands)) + return new HandsGuiState(Array.Empty()); + + var states = hands.Hands + .Select(hand => new GuiHand(hand.Name, hand.Location, hand.HeldEntity)) + .ToArray(); + + return new HandsGuiState(states, hands.ActiveHand); + } + + public void UIHandClick(HandsComponent hands, string handName) + { + if (!hands.TryGetHand(handName, out var pressedHand)) + return; + + if (!hands.TryGetActiveHand(out var activeHand)) + return; + + var pressedEntity = pressedHand.HeldEntity; + var activeEntity = activeHand.HeldEntity; + + if (pressedHand == activeHand && activeEntity != null) + { + // use item in hand + // it will always be attack_self() in my heart. + RaiseNetworkEvent(new UseInHandMsg()); + return; + } + + if (pressedHand != activeHand && pressedEntity == null) + { + // change active hand + RaiseNetworkEvent(new RequestSetHandEvent(handName)); + return; + } + + if (pressedHand != activeHand && pressedEntity != null && activeEntity != null) + { + // use active item on held item + RaiseNetworkEvent(new ClientInteractUsingInHandMsg(pressedHand.Name)); + return; + } + + if (pressedHand != activeHand && pressedEntity != null && activeEntity == null) + { + // use active item on held item + RaiseNetworkEvent(new MoveItemFromHandMsg(pressedHand.Name)); + } + } + + public void UIHandActivate(string handName) + { + RaiseNetworkEvent (new ActivateInHandMsg(handName)); + } + + private void HandlePlayerAttached(EntityUid uid, HandsComponent component, PlayerAttachedEvent args) + { + component.Gui = new HandsGui(component, this); + _gameHud.HandsContainer.AddChild(component.Gui); + } + + private static void HandlePlayerDetached(EntityUid uid, HandsComponent component, PlayerDetachedEvent args) + { + ClearGui(component); + } + + private static void HandleCompRemove(EntityUid uid, HandsComponent component, ComponentRemove args) + { + ClearGui(component); + } + + private static void ClearGui(HandsComponent comp) + { + comp.Gui?.Orphan(); + comp.Gui = null; + } + } +} diff --git a/Content.Client/Items/ItemStatusMessages.cs b/Content.Client/Items/ItemStatusMessages.cs new file mode 100644 index 0000000000..93ebaeabe0 --- /dev/null +++ b/Content.Client/Items/ItemStatusMessages.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Robust.Client.UserInterface; +using Robust.Shared.GameObjects; + +namespace Content.Client.Items +{ + public sealed class ItemStatusCollectMessage : EntityEventArgs + { + public List Controls = new(); + } + + public static class ItemStatusRegisterExt + { + /// + /// Register an item status control for a component. + /// + /// The handle from within entity system initialize. + /// A delegate to create the actual control. + /// The type of component for which this control should be made. + public static void ItemStatus( + this EntitySystem.Subscriptions subs, + Func createControl) + where TComp : IComponent + { + subs.SubscribeLocalEvent((uid, _, args) => + { + args.Controls.Add(createControl(uid)); + }); + } + } +} diff --git a/Content.Client/Items/Managers/IItemSlotManager.cs b/Content.Client/Items/Managers/IItemSlotManager.cs index 127eedc1b4..bbebaf55b4 100644 --- a/Content.Client/Items/Managers/IItemSlotManager.cs +++ b/Content.Client/Items/Managers/IItemSlotManager.cs @@ -1,4 +1,5 @@ -using Content.Client.Items.UI; +using System; +using Content.Client.Items.UI; using Robust.Client.UserInterface; using Robust.Shared.GameObjects; @@ -10,5 +11,21 @@ namespace Content.Client.Items.Managers void UpdateCooldown(ItemSlotButton? cooldownTexture, IEntity? entity); bool SetItemSlot(ItemSlotButton button, IEntity? entity); void HoverInSlot(ItemSlotButton button, IEntity? entity, bool fits); + event Action? EntityHighlightedUpdated; + bool IsHighlighted(EntityUid uid); + + /// + /// Highlight all slot controls that contain the specified entity. + /// + /// The UID of the entity to highlight. + /// + void HighlightEntity(EntityUid uid); + + /// + /// Remove highlighting for the specified entity. + /// + /// The UID of the entity to unhighlight. + /// + void UnHighlightEntity(EntityUid uid); } } diff --git a/Content.Client/Items/Managers/ItemSlotManager.cs b/Content.Client/Items/Managers/ItemSlotManager.cs index 5fd3f0fbe0..3435913914 100644 --- a/Content.Client/Items/Managers/ItemSlotManager.cs +++ b/Content.Client/Items/Managers/ItemSlotManager.cs @@ -1,8 +1,11 @@ +using System; +using System.Collections.Generic; using Content.Client.Examine; using Content.Client.Items.UI; using Content.Client.Storage; using Content.Client.Verbs; using Content.Shared.Cooldown; +using Content.Shared.Hands.Components; using Content.Shared.Input; using Robust.Client.GameObjects; using Robust.Client.Graphics; @@ -28,6 +31,11 @@ namespace Content.Client.Items.Managers [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IComponentManager _componentManager = default!; + + private readonly HashSet _highlightEntities = new(); + + public event Action? EntityHighlightedUpdated; public bool SetItemSlot(ItemSlotButton button, IEntity? entity) { @@ -38,13 +46,26 @@ namespace Content.Client.Items.Managers } else { - if (!entity.TryGetComponent(out ISpriteComponent? sprite)) + ISpriteComponent? sprite; + if (entity.TryGetComponent(out HandVirtualPullComponent? virtPull) + && _componentManager.TryGetComponent(virtPull.PulledEntity, out ISpriteComponent pulledSprite)) + { + sprite = pulledSprite; + } + else if (!entity.TryGetComponent(out sprite)) + { return false; + } button.ClearHover(); button.SpriteView.Sprite = sprite; button.StorageButton.Visible = entity.HasComponent(); } + + button.Entity = entity?.Uid ?? default; + + // im lazy + button.UpdateSlotHighlighted(); return true; } @@ -145,5 +166,38 @@ namespace Content.Client.Items.Managers button.HoverSpriteView.Sprite = hoverSprite; } + + public bool IsHighlighted(EntityUid uid) + { + return _highlightEntities.Contains(uid); + } + + public void HighlightEntity(EntityUid uid) + { + if (!_highlightEntities.Add(uid)) + return; + + EntityHighlightedUpdated?.Invoke(new EntitySlotHighlightedEventArgs(uid, true)); + } + + public void UnHighlightEntity(EntityUid uid) + { + if (!_highlightEntities.Remove(uid)) + return; + + EntityHighlightedUpdated?.Invoke(new EntitySlotHighlightedEventArgs(uid, false)); + } + } + + public readonly struct EntitySlotHighlightedEventArgs + { + public EntitySlotHighlightedEventArgs(EntityUid entity, bool newHighlighted) + { + Entity = entity; + NewHighlighted = newHighlighted; + } + + public EntityUid Entity { get; } + public bool NewHighlighted { get; } } } diff --git a/Content.Client/Items/UI/ItemSlotButton.cs b/Content.Client/Items/UI/ItemSlotButton.cs index 8ffbdc5cf8..201fa28f5c 100644 --- a/Content.Client/Items/UI/ItemSlotButton.cs +++ b/Content.Client/Items/UI/ItemSlotButton.cs @@ -1,18 +1,24 @@ using System; using Content.Client.Cooldown; +using Content.Client.Items.Managers; using Content.Client.Stylesheets; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Shared.GameObjects; using Robust.Shared.Input; +using Robust.Shared.IoC; using Robust.Shared.Maths; namespace Content.Client.Items.UI { - public class ItemSlotButton : Control + public class ItemSlotButton : Control, IEntityEventSubscriber { private const string HighlightShader = "SelectionOutlineInrange"; + [Dependency] private readonly IItemSlotManager _itemSlotManager = default!; + + public EntityUid Entity { get; set; } public TextureRect Button { get; } public SpriteView SpriteView { get; } public SpriteView HoverSpriteView { get; } @@ -32,6 +38,8 @@ namespace Content.Client.Items.UI public ItemSlotButton(Texture texture, Texture storageTexture, string textureName) { + IoCManager.InjectDependencies(this); + MinSize = (64, 64); TextureName = textureName; @@ -101,6 +109,31 @@ namespace Content.Client.Items.UI }); } + protected override void EnteredTree() + { + base.EnteredTree(); + + _itemSlotManager.EntityHighlightedUpdated += HandleEntitySlotHighlighted; + UpdateSlotHighlighted(); + } + + protected override void ExitedTree() + { + base.ExitedTree(); + + _itemSlotManager.EntityHighlightedUpdated -= HandleEntitySlotHighlighted; + } + + private void HandleEntitySlotHighlighted(EntitySlotHighlightedEventArgs entitySlotHighlightedEventArgs) + { + UpdateSlotHighlighted(); + } + + public void UpdateSlotHighlighted() + { + Highlight(_itemSlotManager.IsHighlighted(Entity)); + } + public void ClearHover() { if (EntityHover) diff --git a/Content.Client/Items/UI/ItemStatusPanel.cs b/Content.Client/Items/UI/ItemStatusPanel.cs index d880d7ea49..d1dbd714c6 100644 --- a/Content.Client/Items/UI/ItemStatusPanel.cs +++ b/Content.Client/Items/UI/ItemStatusPanel.cs @@ -8,7 +8,9 @@ using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Maths; +using Robust.Shared.Timing; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; using static Content.Client.IoC.StaticIoC; @@ -18,6 +20,8 @@ namespace Content.Client.Items.UI { public class ItemStatusPanel : Control { + [Dependency] private readonly IEntityManager _entityManager = default!; + [ViewVariables] private readonly List<(IItemStatus, Control)> _activeStatusComponents = new(); @@ -33,6 +37,8 @@ namespace Content.Client.Items.UI public ItemStatusPanel(Texture texture, StyleBox.Margin cutout, StyleBox.Margin flat, Label.AlignMode textAlign) { + IoCManager.InjectDependencies(this); + var panel = new StyleBoxTexture { Texture = texture @@ -117,6 +123,13 @@ namespace Content.Client.Items.UI return new ItemStatusPanel(ResC.GetTexture(texture), cutOut, flat, textAlign); } + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + UpdateItemName(); + } + public void Update(IEntity? entity) { if (entity == null) @@ -131,12 +144,29 @@ namespace Content.Client.Items.UI { _entity = entity; BuildNewEntityStatus(); - _itemNameLabel.Text = entity.Name; + + UpdateItemName(); } _panel.Visible = true; } + private void UpdateItemName() + { + if (_entity == null) + return; + + if (_entity.TryGetComponent(out HandVirtualPullComponent? virtualPull) + && _entityManager.TryGetEntity(virtualPull.PulledEntity, out var pulledEnt)) + { + _itemNameLabel.Text = pulledEnt.Name; + } + else + { + _itemNameLabel.Text = _entity.Name; + } + } + private void ClearOldStatus() { _statusContents.RemoveAllChildren(); @@ -162,6 +192,14 @@ namespace Content.Client.Items.UI _activeStatusComponents.Add((statusComponent, control)); } + + var collectMsg = new ItemStatusCollectMessage(); + _entity.EntityManager.EventBus.RaiseLocalEvent(_entity.Uid, collectMsg); + + foreach (var control in collectMsg.Controls) + { + _statusContents.AddChild(control); + } } } } diff --git a/Content.Server/Fluids/Components/SpillableComponent.cs b/Content.Server/Fluids/Components/SpillableComponent.cs index 79e1f267a7..bd5dca6d3d 100644 --- a/Content.Server/Fluids/Components/SpillableComponent.cs +++ b/Content.Server/Fluids/Components/SpillableComponent.cs @@ -2,6 +2,7 @@ using Content.Shared.ActionBlocker; using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Solution.Components; using Content.Shared.DragDrop; +using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Notification.Managers; using Content.Shared.Verbs; diff --git a/Content.Server/Hands/Components/HandsComponent.cs b/Content.Server/Hands/Components/HandsComponent.cs index c3f65060b5..f8b84c9992 100644 --- a/Content.Server/Hands/Components/HandsComponent.cs +++ b/Content.Server/Hands/Components/HandsComponent.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -10,7 +11,6 @@ using Content.Shared.Audio; using Content.Shared.Body.Part; using Content.Shared.Hands.Components; using Content.Shared.Notification.Managers; -using Content.Shared.Physics.Pull; using Content.Shared.Pulling.Components; using Robust.Server.GameObjects; using Robust.Shared.Audio; @@ -19,9 +19,7 @@ using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Map; -using Robust.Shared.Network; using Robust.Shared.Player; -using Robust.Shared.Players; namespace Content.Server.Hands.Components { @@ -35,48 +33,6 @@ namespace Content.Server.Hands.Components int IDisarmedAct.Priority => int.MaxValue; // We want this to be the last disarm act to run. - public override void HandleMessage(ComponentMessage message, IComponent? component) - { - base.HandleMessage(message, component); - - switch (message) - { - case PullAttemptMessage msg: - AttemptPull(msg); - break; - case PullStartedMessage: - StartPulling(); - break; - case PullStoppedMessage: - StopPulling(); - break; - } - } - - public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null) - { - base.HandleNetworkMessage(message, channel, session); - - switch (message) - { - case ClientChangedHandMsg msg: - ActiveHand = msg.HandName; - break; - case ClientAttackByInHandMsg msg: - InteractHandWithActiveHand(msg.HandName); - break; - case UseInHandMsg: - UseActiveHeldEntity(); - break; - case ActivateInHandMsg msg: - ActivateHeldEntity(msg.HandName); - break; - case MoveItemFromHandMsg msg: - TryMoveHeldEntityToActiveHand(msg.HandName); - break; - } - } - protected override void OnHeldEntityRemovedFromHand(IEntity heldEntity, HandState handState) { if (heldEntity.TryGetComponent(out ItemComponent? item)) @@ -141,7 +97,8 @@ namespace Content.Server.Hands.Components if (pickupDirection == initialPosition.ToMapPos(Owner.EntityManager)) return; - SendNetworkMessage(new PickupAnimationMessage(entity.Uid, pickupDirection, initialPosition)); + Owner.EntityManager.EntityNetManager!.SendSystemNetworkMessage( + new PickupAnimationMessage(entity.Uid, pickupDirection, initialPosition)); } #region Pull/Disarm @@ -151,9 +108,17 @@ namespace Content.Server.Hands.Components if (args.Part.PartType != BodyPartType.Hand) return; - var handLocation = ReadOnlyHands.Count == 0 ? HandLocation.Right : HandLocation.Left; //TODO: make hand body part have a handlocation? + // If this annoys you, which it should. + // Ping Smugleaf. + var location = args.Part.Symmetry switch + { + BodyPartSymmetry.None => HandLocation.Middle, + BodyPartSymmetry.Left => HandLocation.Left, + BodyPartSymmetry.Right => HandLocation.Right, + _ => throw new ArgumentOutOfRangeException() + }; - AddHand(args.Slot, handLocation); + AddHand(args.Slot, location); } void IBodyPartRemoved.BodyPartRemoved(BodyPartRemovedEventArgs args) @@ -205,41 +170,13 @@ namespace Content.Server.Hands.Components return pullable.TryStopPull(); } - private void AttemptPull(PullAttemptMessage msg) - { - if (!ReadOnlyHands.Any(hand => hand.Enabled)) - { - msg.Cancelled = true; - } - } - - private void StartPulling() - { - var firstFreeHand = Hands.FirstOrDefault(hand => hand.Enabled); - - if (firstFreeHand == null) - return; - - DisableHand(firstFreeHand); - } - - private void StopPulling() - { - var firstOccupiedHand = Hands.FirstOrDefault(hand => !hand.Enabled); - - if (firstOccupiedHand == null) - return; - - EnableHand(firstOccupiedHand); - } - #endregion #region Old public methods - public IEnumerable HandNames => ReadOnlyHands.Select(h => h.Name); + public IEnumerable HandNames => Hands.Select(h => h.Name); - public int Count => ReadOnlyHands.Count; + public int Count => Hands.Count; /// /// Returns a list of all hand names, with the active hand being first. @@ -249,9 +186,9 @@ namespace Content.Server.Hands.Components if (ActiveHand != null) yield return ActiveHand; - foreach (var hand in ReadOnlyHands) + foreach (var hand in Hands) { - if (hand.Name == ActiveHand || !hand.Enabled) + if (hand.Name == ActiveHand) continue; yield return hand.Name; diff --git a/Content.Server/Hands/Systems/HandVirtualPullSystem.cs b/Content.Server/Hands/Systems/HandVirtualPullSystem.cs new file mode 100644 index 0000000000..f95a4747ca --- /dev/null +++ b/Content.Server/Hands/Systems/HandVirtualPullSystem.cs @@ -0,0 +1,57 @@ +using Content.Server.Pulling; +using Content.Shared.Hands; +using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; + +namespace Content.Server.Hands +{ + [UsedImplicitly] + public sealed class HandVirtualPullSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(HandlePullerDropped); + SubscribeLocalEvent(HandlePullerUnequipped); + + SubscribeLocalEvent(HandleBeforeInteract); + } + + private static void HandleBeforeInteract( + EntityUid uid, + HandVirtualPullComponent component, + BeforeInteractEvent args) + { + // No interactions with a virtual pull, please. + args.Handled = true; + } + + // If the virtual pull gets removed from the hands for any reason, cancel the pull and delete it. + private void HandlePullerUnequipped(EntityUid uid, HandVirtualPullComponent component, UnequippedHandEvent args) + { + MaybeDelete(component, args.User); + } + + private void HandlePullerDropped(EntityUid uid, HandVirtualPullComponent component, DroppedEvent args) + { + MaybeDelete(component, args.User); + } + + private void MaybeDelete(HandVirtualPullComponent comp, IEntity? user) + { + var pulled = comp.PulledEntity; + + if (!ComponentManager.TryGetComponent(pulled, out PullableComponent? pullable)) + return; + + if (pullable.Puller != user) + return; + + pullable.TryStopPull(user); + comp.Owner.QueueDelete(); + } + } +} diff --git a/Content.Server/Hands/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs similarity index 58% rename from Content.Server/Hands/HandsSystem.cs rename to Content.Server/Hands/Systems/HandsSystem.cs index 4a76d07101..7e6adca66c 100644 --- a/Content.Server/Hands/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -13,9 +13,9 @@ using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Input; using Content.Shared.Notification.Managers; +using Content.Shared.Physics.Pull; using JetBrains.Annotations; using Robust.Server.Player; -using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Input.Binding; using Robust.Shared.IoC; @@ -23,6 +23,7 @@ using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Players; +using Robust.Shared.Utility; using static Content.Shared.Inventory.EquipmentSlotDefines; namespace Content.Server.Hands @@ -38,15 +39,126 @@ namespace Content.Server.Hands base.Initialize(); SubscribeLocalEvent(HandleExamined); + SubscribeNetworkEvent(HandleActivateInHand); + SubscribeNetworkEvent(HandleInteractUsingInHand); + SubscribeNetworkEvent(HandleUseInHand); + SubscribeNetworkEvent(HandleMoveItemFromHand); + + SubscribeLocalEvent(HandlePullAttempt); + SubscribeLocalEvent(HandlePullStarted); + SubscribeLocalEvent(HandlePullStopped); CommandBinds.Builder .Bind(ContentKeyFunctions.ActivateItemInHand, InputCmdHandler.FromDelegate(HandleActivateItem)) .Bind(ContentKeyFunctions.ThrowItemInHand, new PointerInputCmdHandler(HandleThrowItem)) .Bind(ContentKeyFunctions.SmartEquipBackpack, InputCmdHandler.FromDelegate(HandleSmartEquipBackpack)) .Bind(ContentKeyFunctions.SmartEquipBelt, InputCmdHandler.FromDelegate(HandleSmartEquipBelt)) + .Bind(ContentKeyFunctions.SwapHands, InputCmdHandler.FromDelegate(SwapHandsPressed, handle: false)) + .Bind(ContentKeyFunctions.Drop, new PointerInputCmdHandler(DropPressed)) .Register(); } + private static void HandlePullAttempt(EntityUid uid, HandsComponent component, PullAttemptMessage args) + { + // Cancel pull if all hands full. + if (component.Hands.All(hand => !hand.IsEmpty)) + args.Cancelled = true; + } + + private void HandlePullStarted(EntityUid uid, HandsComponent component, PullStartedMessage args) + { + foreach (var handName in component.ActivePriorityEnumerable()) + { + var hand = component.GetHand(handName); + if (!hand.IsEmpty) + continue; + + var pos = component.Owner.Transform.Coordinates; + var virtualPull = EntityManager.SpawnEntity("HandVirtualPull", pos); + var virtualPullComp = virtualPull.GetComponent(); + virtualPullComp.PulledEntity = args.Pulled.Owner.Uid; + component.PutEntityIntoHand(hand, virtualPull); + return; + } + + DebugTools.Assert("Unable to find available hand when starting pulling??"); + } + + private void HandlePullStopped(EntityUid uid, HandsComponent component, PullStoppedMessage args) + { + // Try find hand that is doing this pull. + // and clear it. + foreach (var hand in component.Hands) + { + if (hand.HeldEntity == null + || !hand.HeldEntity.TryGetComponent(out HandVirtualPullComponent? virtualPull) + || virtualPull.PulledEntity != args.Pulled.Owner.Uid) + continue; + + hand.HeldEntity.Delete(); + break; + } + } + + private void SwapHandsPressed(ICommonSession? session) + { + var player = session?.AttachedEntity; + + if (player == null) + return; + + if (!player.TryGetComponent(out SharedHandsComponent? hands)) + return; + + if (!hands.TryGetSwapHandsResult(out var nextHand)) + return; + + hands.ActiveHand = nextHand; + } + + private bool DropPressed(ICommonSession? session, EntityCoordinates coords, EntityUid uid) + { + var player = session?.AttachedEntity; + + if (player == null) + return false; + + if (!player.TryGetComponent(out SharedHandsComponent? hands)) + return false; + + var activeHand = hands.ActiveHand; + + if (activeHand == null) + return false; + + hands.TryDropHand(activeHand, coords); + return false; + } + + private void HandleMoveItemFromHand(MoveItemFromHandMsg msg, EntitySessionEventArgs args) + { + if (!TryGetHandsComp(args.SenderSession, out var hands)) + return; + + hands.TryMoveHeldEntityToActiveHand(msg.HandName); + } + + private void HandleUseInHand(UseInHandMsg msg, EntitySessionEventArgs args) + { + if (!TryGetHandsComp(args.SenderSession, out var hands)) + return; + + hands.UseActiveHeldEntity(); + } + + private void HandleInteractUsingInHand(ClientInteractUsingInHandMsg msg, EntitySessionEventArgs args) + { + if (!TryGetHandsComp(args.SenderSession, out var hands)) + return; + + hands.InteractHandWithActiveHand(msg.HandName); + } + public override void Shutdown() { base.Shutdown(); @@ -54,6 +166,14 @@ namespace Content.Server.Hands CommandBinds.Unregister(); } + private void HandleActivateInHand(ActivateInHandMsg msg, EntitySessionEventArgs args) + { + if (!TryGetHandsComp(args.SenderSession, out var hands)) + return; + + hands.ActivateHeldEntity(msg.HandName); + } + protected override void DropAllItemsInHands(IEntity entity, bool doMobChecks = true) { base.DropAllItemsInHands(entity, doMobChecks); @@ -75,25 +195,21 @@ namespace Content.Server.Hands } } - protected override void HandleContainerModified(EntityUid uid, SharedHandsComponent component, ContainerModifiedMessage args) - { - component.Dirty(); - } - - private bool TryGetHandsComp(ICommonSession? session, [NotNullWhen(true)] out SharedHandsComponent? hands) + private static bool TryGetHandsComp( + ICommonSession? session, + [NotNullWhen(true)] out SharedHandsComponent? hands) { hands = default; if (session is not IPlayerSession playerSession) return false; - var playerEnt = playerSession?.AttachedEntity; + var playerEnt = playerSession.AttachedEntity; if (playerEnt == null || !playerEnt.IsValid()) return false; - playerEnt.TryGetComponent(out hands); - return hands != null; + return playerEnt.TryGetComponent(out hands); } private void HandleActivateItem(ICommonSession? session) diff --git a/Content.Server/Interaction/InteractionSystem.cs b/Content.Server/Interaction/InteractionSystem.cs index 61a5ce724e..6908b146b5 100644 --- a/Content.Server/Interaction/InteractionSystem.cs +++ b/Content.Server/Interaction/InteractionSystem.cs @@ -387,6 +387,18 @@ namespace Content.Server.Interaction return false; } + private async Task InteractDoBefore( + IEntity user, + IEntity used, + IEntity? target, + EntityCoordinates clickLocation, + bool canReach) + { + var ev = new BeforeInteractEvent(user, used, target, clickLocation, canReach); + RaiseLocalEvent(used.Uid, ev, false); + return ev.Handled; + } + /// /// Uses a item/object on an entity /// Finds components with the InteractUsing interface and calls their function @@ -397,6 +409,9 @@ namespace Content.Server.Interaction if (!_actionBlockerSystem.CanInteract(user)) return; + if (await InteractDoBefore(user, used, target, clickLocation, true)) + return; + // all interactions should only happen when in range / unobstructed, so no range check is needed var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation); RaiseLocalEvent(target.Uid, interactUsingEvent); @@ -696,6 +711,9 @@ namespace Content.Server.Interaction /// public async Task InteractUsingRanged(IEntity user, IEntity used, IEntity? target, EntityCoordinates clickLocation, bool inRangeUnobstructed) { + if (await InteractDoBefore(user, used, inRangeUnobstructed ? target : null, clickLocation, false)) + return true; + if (target != null) { var rangedMsg = new RangedInteractEvent(user, used, target, clickLocation); @@ -715,10 +733,7 @@ namespace Content.Server.Interaction } } - if (inRangeUnobstructed) - return await InteractDoAfter(user, used, target, clickLocation, false); - else - return await InteractDoAfter(user, used, null, clickLocation, false); + return await InteractDoAfter(user, used, inRangeUnobstructed ? target : null, clickLocation, false); } public void DoAttack(IEntity user, EntityCoordinates coordinates, bool wideAttack, EntityUid targetUid = default) diff --git a/Content.Shared/Hands/Components/HandVirtualPullComponent.cs b/Content.Shared/Hands/Components/HandVirtualPullComponent.cs new file mode 100644 index 0000000000..42f6bd85f3 --- /dev/null +++ b/Content.Shared/Hands/Components/HandVirtualPullComponent.cs @@ -0,0 +1,50 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.Players; +using Robust.Shared.Serialization; + +namespace Content.Shared.Hands.Components +{ + [RegisterComponent] + [NetworkedComponent] + public sealed class HandVirtualPullComponent : Component + { + private EntityUid _pulledEntity; + public override string Name => "HandVirtualPull"; + + public EntityUid PulledEntity + { + get => _pulledEntity; + set + { + _pulledEntity = value; + Dirty(); + } + } + + public override ComponentState GetComponentState(ICommonSession player) + { + return new VirtualPullComponentState(_pulledEntity); + } + + public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) + { + if (curState is not VirtualPullComponentState pullState) + return; + + _pulledEntity = pullState.PulledEntity; + } + + [Serializable, NetSerializable] + public sealed class VirtualPullComponentState : ComponentState + { + public readonly EntityUid PulledEntity; + + public VirtualPullComponentState(EntityUid pulledEntity) + { + PulledEntity = pulledEntity; + } + } + } +} diff --git a/Content.Shared/Hands/Components/SharedHandsComponent.cs b/Content.Shared/Hands/Components/SharedHandsComponent.cs index 15a2ca3d69..9e1978b06f 100644 --- a/Content.Shared/Hands/Components/SharedHandsComponent.cs +++ b/Content.Shared/Hands/Components/SharedHandsComponent.cs @@ -55,11 +55,11 @@ namespace Content.Shared.Hands.Components } } } + private string? _activeHand; [ViewVariables] - public IReadOnlyList ReadOnlyHands => Hands; - protected readonly List Hands = new(); + public readonly List Hands = new(); /// /// The amount of throw impulse per distance the player is from the throw target. @@ -89,7 +89,10 @@ namespace Content.Shared.Hands.Components public virtual void HandsModified() { + // todo axe all this for ECS. Dirty(); + + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new HandsModifiedMessage { Hands = this }); } public void AddHand(string handName, HandLocation handLocation) @@ -100,7 +103,7 @@ namespace Content.Shared.Hands.Components var container = ContainerHelpers.CreateContainer(Owner, handName); container.OccludesLight = false; - Hands.Add(new Hand(handName, true, handLocation, container)); + Hands.Add(new Hand(handName, handLocation, container)); if (ActiveHand == null) ActiveHand = handName; @@ -125,48 +128,55 @@ namespace Content.Shared.Hands.Components Hands.Remove(hand); if (ActiveHand == hand.Name) - ActiveHand = ReadOnlyHands.FirstOrDefault()?.Name; + ActiveHand = Hands.FirstOrDefault()?.Name; HandCountChanged(); HandsModified(); } - public bool HasHand(string handName) - { - foreach (var hand in Hands) - { - if (hand.Name == handName) - return true; - } - return false; - } - - private Hand? GetHand(string handName) - { - foreach (var hand in Hands) - { - if (hand.Name == handName) - return hand; - } - return null; - } - private Hand? GetActiveHand() { if (ActiveHand == null) return null; - return GetHand(ActiveHand); + return GetHandOrNull(ActiveHand); } - protected bool TryGetHand(string handName, [NotNullWhen(true)] out Hand? foundHand) + public bool HasHand(string handName) { - foundHand = GetHand(handName); - return foundHand != null; + return TryGetHand(handName, out _); } - protected bool TryGetActiveHand([NotNullWhen(true)] out Hand? activeHand) + public Hand? GetHandOrNull(string handName) + { + return TryGetHand(handName, out var hand) ? hand : null; + } + + public Hand GetHand(string handName) + { + if (!TryGetHand(handName, out var hand)) + throw new KeyNotFoundException($"Unable to find hand with name {handName}"); + + return hand; + } + + public bool TryGetHand(string handName, [NotNullWhen(true)] out Hand? foundHand) + { + foreach (var hand in Hands) + { + if (hand.Name == handName) + { + foundHand = hand; + return true; + }; + } + + foundHand = null; + return false; + } + + public bool TryGetActiveHand([NotNullWhen(true)] out Hand? activeHand) { activeHand = GetActiveHand(); return activeHand != null; @@ -211,7 +221,7 @@ namespace Content.Shared.Hands.Components public IEnumerable GetAllHeldEntities() { - foreach (var hand in ReadOnlyHands) + foreach (var hand in Hands) { if (hand.HeldEntity != null) yield return hand.HeldEntity; @@ -416,7 +426,7 @@ namespace Content.Shared.Hands.Components /// /// Drops a hands contents to the target location. /// - private void DropHeldEntity(Hand hand, EntityCoordinates targetDropLocation, bool intentionalDrop = true) + public void DropHeldEntity(Hand hand, EntityCoordinates targetDropLocation, bool intentionalDrop = true) { var heldEntity = hand.HeldEntity; @@ -538,16 +548,7 @@ namespace Content.Shared.Hands.Components public bool CanPickupEntityToActiveHand(IEntity entity, bool checkActionBlocker = true) { - if (!TryGetActiveHand(out var hand)) - return false; - - if (checkActionBlocker && !PlayerCanPickup()) - return false; - - if (!CanInsertEntityIntoHand(hand, entity)) - return false; - - return true; + return ActiveHand != null && CanPickupEntity(ActiveHand, entity, checkActionBlocker); } /// @@ -563,10 +564,7 @@ namespace Content.Shared.Hands.Components public bool TryPickupEntityToActiveHand(IEntity entity, bool checkActionBlocker = true) { - if (!TryGetActiveHand(out var hand)) - return false; - - return TryPickupEntity(hand, entity, checkActionBlocker); + return ActiveHand != null && TryPickupEntity(ActiveHand, entity, checkActionBlocker); } /// @@ -574,9 +572,6 @@ namespace Content.Shared.Hands.Components /// protected bool CanInsertEntityIntoHand(Hand hand, IEntity entity) { - if (!hand.Enabled) - return false; - var handContainer = hand.Container; if (handContainer == null) return false; @@ -602,7 +597,7 @@ namespace Content.Shared.Hands.Components /// /// Puts an entity into the player's hand, assumes that the insertion is allowed. /// - private void PutEntityIntoHand(Hand hand, IEntity entity) + public void PutEntityIntoHand(Hand hand, IEntity entity) { var handContainer = hand.Container; if (handContainer == null) @@ -658,7 +653,7 @@ namespace Content.Shared.Hands.Components if (newActiveIndex > finalHandIndex) newActiveIndex = 0; - nextHand = ReadOnlyHands[newActiveIndex].Name; + nextHand = Hands[newActiveIndex].Name; return true; } @@ -752,7 +747,7 @@ namespace Content.Shared.Hands.Components Hand? priorityHand = null; if (priorityHandName != null) - priorityHand = GetHand(priorityHandName); + priorityHand = GetHandOrNull(priorityHandName); return TryPutInAnyHand(entity, priorityHand, checkActionBlocker); } @@ -793,43 +788,16 @@ namespace Content.Shared.Hands.Components protected virtual void DoActivate(IEntity heldEntity) { } protected virtual void HandlePickupAnimation(IEntity entity) { } - - protected void EnableHand(Hand hand) - { - hand.Enabled = true; - Dirty(); - } - - protected void DisableHand(Hand hand) - { - hand.Enabled = false; - DropHeldEntityToFloor(hand, intentionalDrop: false); - Dirty(); - } } - public interface IReadOnlyHand + public class Hand { + [ViewVariables] public string Name { get; } - public bool Enabled { get; } - + [ViewVariables] public HandLocation Location { get; } - public abstract IEntity? HeldEntity { get; } - } - - public class Hand : IReadOnlyHand - { - [ViewVariables] - public string Name { get; set; } - - [ViewVariables] - public bool Enabled { get; set; } - - [ViewVariables] - public HandLocation Location { get; set; } - /// /// The container used to hold the contents of this hand. Nullable because the client must get the containers via , /// which may not be synced with the server when the client hands are created. @@ -840,37 +808,36 @@ namespace Content.Shared.Hands.Components [ViewVariables] public IEntity? HeldEntity => Container?.ContainedEntities?.FirstOrDefault(); - public Hand(string name, bool enabled, HandLocation location, IContainer? container = null) + public bool IsEmpty => HeldEntity == null; + + public Hand(string name, HandLocation location, IContainer? container = null) { Name = name; - Enabled = enabled; Location = location; Container = container; } public HandState ToHandState() { - return new(Name, Location, Enabled); + return new(Name, Location); } } [Serializable, NetSerializable] - public sealed class HandState + public struct HandState { public string Name { get; } public HandLocation Location { get; } - public bool Enabled { get; } - public HandState(string name, HandLocation location, bool enabled) + public HandState(string name, HandLocation location) { Name = name; Location = location; - Enabled = enabled; } } [Serializable, NetSerializable] - public class HandsComponentState : ComponentState + public sealed class HandsComponentState : ComponentState { public HandState[] Hands { get; } public string? ActiveHand { get; } @@ -886,25 +853,20 @@ namespace Content.Shared.Hands.Components /// A message that calls the use interaction on an item in hand, presumed for now the interaction will occur only on the active hand. /// [Serializable, NetSerializable] - public class UseInHandMsg : ComponentMessage + public sealed class UseInHandMsg : EntityEventArgs { - public UseInHandMsg() - { - Directed = true; - } } /// /// A message that calls the activate interaction on the item in the specified hand. /// [Serializable, NetSerializable] - public class ActivateInHandMsg : ComponentMessage + public class ActivateInHandMsg : EntityEventArgs { public string HandName { get; } public ActivateInHandMsg(string handName) { - Directed = true; HandName = handName; } } @@ -913,13 +875,12 @@ namespace Content.Shared.Hands.Components /// Uses the item in the active hand on the item in the specified hand. /// [Serializable, NetSerializable] - public class ClientAttackByInHandMsg : ComponentMessage + public class ClientInteractUsingInHandMsg : EntityEventArgs { public string HandName { get; } - public ClientAttackByInHandMsg(string handName) + public ClientInteractUsingInHandMsg(string handName) { - Directed = true; HandName = handName; } } @@ -928,28 +889,12 @@ namespace Content.Shared.Hands.Components /// Moves an item from one hand to the active hand. /// [Serializable, NetSerializable] - public class MoveItemFromHandMsg : ComponentMessage + public class MoveItemFromHandMsg : EntityEventArgs { public string HandName { get; } public MoveItemFromHandMsg(string handName) { - Directed = true; - HandName = handName; - } - } - - /// - /// Sets the player's active hand to a specified hand. - /// - [Serializable, NetSerializable] - public class ClientChangedHandMsg : ComponentMessage - { - public string HandName { get; } - - public ClientChangedHandMsg(string handName) - { - Directed = true; HandName = handName; } } @@ -975,7 +920,7 @@ namespace Content.Shared.Hands.Components } [Serializable, NetSerializable] - public class PickupAnimationMessage : ComponentMessage + public class PickupAnimationMessage : EntityEventArgs { public EntityUid EntityUid { get; } public EntityCoordinates InitialPosition { get; } @@ -983,10 +928,15 @@ namespace Content.Shared.Hands.Components public PickupAnimationMessage(EntityUid entityUid, Vector2 pickupDirection, EntityCoordinates initialPosition) { - Directed = true; EntityUid = entityUid; PickupDirection = pickupDirection; InitialPosition = initialPosition; } } + + [Serializable, NetSerializable] + public struct HandsModifiedMessage + { + public SharedHandsComponent Hands; + } } diff --git a/Content.Shared/Hands/SharedHandsSystem.cs b/Content.Shared/Hands/SharedHandsSystem.cs index 9a9bdec3d8..96bd96d793 100644 --- a/Content.Shared/Hands/SharedHandsSystem.cs +++ b/Content.Shared/Hands/SharedHandsSystem.cs @@ -1,7 +1,6 @@ using Content.Shared.Hands.Components; using Robust.Shared.Containers; using Robust.Shared.GameObjects; -using Robust.Shared.Map; using Robust.Shared.Serialization; using System; @@ -16,11 +15,7 @@ namespace Content.Shared.Hands SubscribeLocalEvent(HandleContainerModified); SubscribeLocalEvent(HandleContainerModified); - SubscribeLocalEvent(HandleSetHand); - SubscribeNetworkEvent(HandleSetHand); - - SubscribeLocalEvent(HandleDrop); - SubscribeNetworkEvent(HandleDrop); + SubscribeAllEvent(HandleSetHand); } public void DropHandItems(IEntity entity, bool doMobChecks = true) @@ -38,14 +33,16 @@ namespace Content.Shared.Hands eventBus.RaiseLocalEvent(uid, msg); - if (msg.Cancelled) return; + if (msg.Cancelled) + return; if (entity.TryGetContainerMan(out var containerManager)) { var parentMsg = new ContainedEntityDropHandItemsAttemptEvent(uid); eventBus.RaiseLocalEvent(containerManager.Owner.Uid, parentMsg); - if (parentMsg.Cancelled) return; + if (parentMsg.Cancelled) + return; } DropAllItemsInHands(entity, doMobChecks); @@ -55,9 +52,9 @@ namespace Content.Shared.Hands { } - private void HandleSetHand(RequestSetHandEvent msg, EntitySessionEventArgs eventArgs) + private static void HandleSetHand(RequestSetHandEvent msg, EntitySessionEventArgs eventArgs) { - var entity = eventArgs.SenderSession?.AttachedEntity; + var entity = eventArgs.SenderSession.AttachedEntity; if (entity == null || !entity.TryGetComponent(out SharedHandsComponent? hands)) return; @@ -65,17 +62,13 @@ namespace Content.Shared.Hands hands.ActiveHand = msg.HandName; } - private void HandleDrop(RequestDropHeldEntityEvent msg, EntitySessionEventArgs eventArgs) + protected virtual void HandleContainerModified( + EntityUid uid, + SharedHandsComponent component, + ContainerModifiedMessage args) { - var entity = eventArgs.SenderSession?.AttachedEntity; - - if (entity == null || !entity.TryGetComponent(out SharedHandsComponent? hands)) - return; - - hands.TryDropHand(msg.HandName, msg.DropTarget); + component.Dirty(); } - - protected abstract void HandleContainerModified(EntityUid uid, SharedHandsComponent component, ContainerModifiedMessage args); } public sealed class ContainedEntityDropHandItemsAttemptEvent : CancellableEntityEventArgs @@ -103,21 +96,4 @@ namespace Content.Shared.Hands HandName = handName; } } - - [Serializable, NetSerializable] - public class RequestDropHeldEntityEvent : EntityEventArgs - { - /// - /// The hand to drop from. - /// - public string HandName { get; } - - public EntityCoordinates DropTarget { get; } - - public RequestDropHeldEntityEvent(string handName, EntityCoordinates dropTarget) - { - HandName = handName; - DropTarget = dropTarget; - } - } } diff --git a/Content.Shared/Interaction/BeforeInteract.cs b/Content.Shared/Interaction/BeforeInteract.cs new file mode 100644 index 0000000000..2c8bb5ca83 --- /dev/null +++ b/Content.Shared/Interaction/BeforeInteract.cs @@ -0,0 +1,53 @@ +using JetBrains.Annotations; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; + +namespace Content.Shared.Interaction +{ + /// + /// Raised directed on the used object when clicking on another object before an interaction is handled. + /// + [PublicAPI] + public class BeforeInteractEvent : HandledEntityEventArgs + { + /// + /// Entity that triggered the interaction. + /// + public IEntity User { get; } + + /// + /// Entity that the user used to interact. + /// + public IEntity Used { get; } + + /// + /// Entity that was interacted on. This can be null if the attack did not click on an entity. + /// + public IEntity? Target { get; } + + /// + /// Location that the user clicked outside of their interaction range. + /// + public EntityCoordinates ClickLocation { get; } + + /// + /// Is the click location close enough to reach by the player? This does not check for obstructions, just that the target is within + /// reach radius around the user. + /// + public bool CanReach { get; } + + public BeforeInteractEvent( + IEntity user, + IEntity used, + IEntity? target, + EntityCoordinates clickLocation, + bool canReach) + { + User = user; + Used = used; + Target = target; + ClickLocation = clickLocation; + CanReach = canReach; + } + } +} diff --git a/Content.Shared/DragDrop/IDropped.cs b/Content.Shared/Interaction/IDropped.cs similarity index 97% rename from Content.Shared/DragDrop/IDropped.cs rename to Content.Shared/Interaction/IDropped.cs index c09f7fc7df..b1daed20fc 100644 --- a/Content.Shared/DragDrop/IDropped.cs +++ b/Content.Shared/Interaction/IDropped.cs @@ -3,7 +3,7 @@ using JetBrains.Annotations; using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; -namespace Content.Shared.DragDrop +namespace Content.Shared.Interaction { /// /// This interface gives components behavior when they're dropped by a mob. diff --git a/Content.Shared/Pulling/Components/SharedPullableComponent.cs b/Content.Shared/Pulling/Components/SharedPullableComponent.cs index 34890165eb..a25f3915ff 100644 --- a/Content.Shared/Pulling/Components/SharedPullableComponent.cs +++ b/Content.Shared/Pulling/Components/SharedPullableComponent.cs @@ -50,6 +50,8 @@ namespace Content.Shared.Pulling.Components return; } + var eventBus = Owner.EntityManager.EventBus; + // New value. Abandon being pulled by any existing object. if (_puller != null) { @@ -64,10 +66,9 @@ namespace Content.Shared.Pulling.Components { var message = new PullStoppedMessage(oldPullerPhysics, _physics); - oldPuller.SendMessage(null, message); - Owner.SendMessage(null, message); + eventBus.RaiseLocalEvent(oldPuller.Uid, message, broadcast: false); + eventBus.RaiseLocalEvent(Owner.Uid, message); - oldPuller.EntityManager.EventBus.RaiseEvent(EventSource.Local, message); _physics.WakeBody(); } // else-branch warning is handled below @@ -125,14 +126,14 @@ namespace Content.Shared.Pulling.Components var pullAttempt = new PullAttemptMessage(pullerPhysics, _physics); - value.SendMessage(null, pullAttempt); + eventBus.RaiseLocalEvent(value.Uid, pullAttempt, broadcast: false); if (pullAttempt.Cancelled) { return; } - Owner.SendMessage(null, pullAttempt); + eventBus.RaiseLocalEvent(Owner.Uid, pullAttempt); if (pullAttempt.Cancelled) { @@ -147,10 +148,8 @@ namespace Content.Shared.Pulling.Components var message = new PullStartedMessage(PullerPhysics, _physics); - _puller.SendMessage(null, message); - Owner.SendMessage(null, message); - - _puller.EntityManager.EventBus.RaiseEvent(EventSource.Local, message); + eventBus.RaiseLocalEvent(_puller.Uid, message, broadcast: false); + eventBus.RaiseLocalEvent(Owner.Uid, message); var union = PullerPhysics.GetWorldAABB().Union(_physics.GetWorldAABB()); var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f; @@ -335,29 +334,6 @@ namespace Content.Shared.Pulling.Components Puller = entity; } - public override void HandleMessage(ComponentMessage message, IComponent? component) - { - base.HandleMessage(message, component); - - if (message is not PullMessage pullMessage || - pullMessage.Pulled.Owner != Owner) - { - return; - } - - var pulledStatus = Owner.GetComponentOrNull(); - - switch (message) - { - case PullStartedMessage: - pulledStatus?.ShowAlert(AlertType.Pulled); - break; - case PullStoppedMessage: - pulledStatus?.ClearAlert(AlertType.Pulled); - break; - } - } - protected override void OnRemove() { TryStopPull(); diff --git a/Content.Shared/Pulling/Components/SharedPullerComponent.cs b/Content.Shared/Pulling/Components/SharedPullerComponent.cs index 12a539ab12..1579339958 100644 --- a/Content.Shared/Pulling/Components/SharedPullerComponent.cs +++ b/Content.Shared/Pulling/Components/SharedPullerComponent.cs @@ -1,6 +1,4 @@ -using Content.Shared.Alert; -using Content.Shared.Movement.Components; -using Content.Shared.Physics.Pull; +using Content.Shared.Movement.Components; using Robust.Shared.GameObjects; using Component = Robust.Shared.GameObjects.Component; @@ -46,30 +44,5 @@ namespace Content.Shared.Pulling.Components base.OnRemove(); } - - public override void HandleMessage(ComponentMessage message, IComponent? component) - { - base.HandleMessage(message, component); - - if (message is not PullMessage pullMessage || - pullMessage.Puller.Owner != Owner) - { - return; - } - - SharedAlertsComponent? ownerStatus = Owner.GetComponentOrNull(); - - switch (message) - { - case PullStartedMessage msg: - Pulling = msg.Pulled.Owner; - ownerStatus?.ShowAlert(AlertType.Pulling); - break; - case PullStoppedMessage _: - Pulling = null; - ownerStatus?.ClearAlert(AlertType.Pulling); - break; - } - } } } diff --git a/Content.Shared/Physics/Pull/PullAttemptMessage.cs b/Content.Shared/Pulling/Events/PullAttemptMessage.cs similarity index 100% rename from Content.Shared/Physics/Pull/PullAttemptMessage.cs rename to Content.Shared/Pulling/Events/PullAttemptMessage.cs diff --git a/Content.Shared/Physics/Pull/PullMessage.cs b/Content.Shared/Pulling/Events/PullMessage.cs similarity index 87% rename from Content.Shared/Physics/Pull/PullMessage.cs rename to Content.Shared/Pulling/Events/PullMessage.cs index cbdb5af3a1..4376720a03 100644 --- a/Content.Shared/Physics/Pull/PullMessage.cs +++ b/Content.Shared/Pulling/Events/PullMessage.cs @@ -3,7 +3,7 @@ using Robust.Shared.Physics; namespace Content.Shared.Physics.Pull { - public class PullMessage : ComponentMessage + public class PullMessage : EntityEventArgs { public readonly IPhysBody Puller; public readonly IPhysBody Pulled; diff --git a/Content.Shared/Physics/Pull/PullStartedMessage.cs b/Content.Shared/Pulling/Events/PullStartedMessage.cs similarity index 100% rename from Content.Shared/Physics/Pull/PullStartedMessage.cs rename to Content.Shared/Pulling/Events/PullStartedMessage.cs diff --git a/Content.Shared/Physics/Pull/PullStoppedMessage.cs b/Content.Shared/Pulling/Events/PullStoppedMessage.cs similarity index 100% rename from Content.Shared/Physics/Pull/PullStoppedMessage.cs rename to Content.Shared/Pulling/Events/PullStoppedMessage.cs diff --git a/Content.Shared/Pulling/Systems/SharedPullerSystem.cs b/Content.Shared/Pulling/Systems/SharedPullerSystem.cs new file mode 100644 index 0000000000..d05754c851 --- /dev/null +++ b/Content.Shared/Pulling/Systems/SharedPullerSystem.cs @@ -0,0 +1,44 @@ +using Content.Shared.Alert; +using Content.Shared.Physics.Pull; +using Content.Shared.Pulling.Components; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; + +namespace Content.Shared.Pulling +{ + [UsedImplicitly] + public sealed class SharedPullerSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(PullerHandlePullStarted); + SubscribeLocalEvent(PullerHandlePullStopped); + } + + private static void PullerHandlePullStarted( + EntityUid uid, + SharedPullerComponent component, + PullStartedMessage args) + { + if (args.Puller.Owner.Uid != uid) + return; + + if (component.Owner.TryGetComponent(out SharedAlertsComponent? alerts)) + alerts.ShowAlert(AlertType.Pulling); + } + + private static void PullerHandlePullStopped( + EntityUid uid, + SharedPullerComponent component, + PullStoppedMessage args) + { + if (args.Puller.Owner.Uid != uid) + return; + + if (component.Owner.TryGetComponent(out SharedAlertsComponent? alerts)) + alerts.ClearAlert(AlertType.Pulling); + } + } +} diff --git a/Content.Shared/Pulling/SharedPullingSystem.cs b/Content.Shared/Pulling/Systems/SharedPullingSystem.cs similarity index 88% rename from Content.Shared/Pulling/SharedPullingSystem.cs rename to Content.Shared/Pulling/Systems/SharedPullingSystem.cs index 0bb04d19b4..5783ee332d 100644 --- a/Content.Shared/Pulling/SharedPullingSystem.cs +++ b/Content.Shared/Pulling/Systems/SharedPullingSystem.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Content.Shared.Alert; using Content.Shared.GameTicking; using Content.Shared.Input; using Content.Shared.Physics.Pull; @@ -56,12 +57,34 @@ namespace Content.Shared.Pulling SubscribeLocalEvent(PullerMoved); SubscribeLocalEvent(HandleContainerInsert); + SubscribeLocalEvent(PullableHandlePullStarted); + SubscribeLocalEvent(PullableHandlePullStopped); + CommandBinds.Builder .Bind(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(HandleMovePulledObject)) .Bind(ContentKeyFunctions.ReleasePulledObject, InputCmdHandler.FromDelegate(HandleReleasePulledObject)) .Register(); } + // Raise a "you are being pulled" alert if the pulled entity has alerts. + private static void PullableHandlePullStarted(EntityUid uid, SharedPullableComponent component, PullStartedMessage args) + { + if (args.Pulled.Owner.Uid != uid) + return; + + if (component.Owner.TryGetComponent(out SharedAlertsComponent? alerts)) + alerts.ShowAlert(AlertType.Pulled); + } + + private static void PullableHandlePullStopped(EntityUid uid, SharedPullableComponent component, PullStoppedMessage args) + { + if (args.Pulled.Owner.Uid != uid) + return; + + if (component.Owner.TryGetComponent(out SharedAlertsComponent? alerts)) + alerts.ClearAlert(AlertType.Pulled); + } + public override void Update(float frameTime) { base.Update(frameTime); diff --git a/Resources/Prototypes/Entities/Virtual/virtual_pull_item.yml b/Resources/Prototypes/Entities/Virtual/virtual_pull_item.yml new file mode 100644 index 0000000000..aa5b7bdb22 --- /dev/null +++ b/Resources/Prototypes/Entities/Virtual/virtual_pull_item.yml @@ -0,0 +1,8 @@ +# This item is stored in the hand slot while you are pulling something, to represent that and what you are pulling. +- type: entity + id: HandVirtualPull + name: VIRTUAL PULL YOU SHOULD NOT SEE THIS + abstract: true + components: + - type: Item + - type: HandVirtualPull From 881c9c9856ef312d5b8011cfc8d77d82cdc672d3 Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 30 Jul 2021 21:15:03 -0400 Subject: [PATCH 6/9] Automatic changelog update --- Resources/Changelog/Changelog.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 62bb69b3ae..085c58872c 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1613,3 +1613,10 @@ Entries: - {message: Added bio suits into bio lockers, type: Add} id: 287 time: '2021-07-28T17:36:06.0000000+00:00' +- author: PJB3005 + changes: + - {message: Pulling is better integrated with hands now. It properly picks an empty + hand and dropping on the pulling hand stops the pull. You also see what you're + pulling in your hand!, type: Tweak} + id: 288 + time: '2021-07-31T01:14:00.0000000+00:00' From dbc90f202a1ff0a06301ea0fbf44ace31d67bc29 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+leonsfriedrich@users.noreply.github.com> Date: Sat, 31 Jul 2021 14:11:02 +1000 Subject: [PATCH 7/9] Bug fixes for metabolisable reagents (#4385) * HealthChangeMetabolism now scales with ticktime and metabolism rate Both Food and Drink metabolisms scale with ticktime, Now HealthChangeMetabolism also does so. Additionally, 'healthChange' now correctly scales with the metabolism rate, so it's description is now correct. * LiverBehaviour now uses correct frameTime Previously, the liver only metabolised reagants once every second, but incorrectly passes the current frameTime to the metabilism function, not 1 second. * Stomach now only transfers non-empty solutions. Makes debugging bloodstream bugs easier if the stomach is not constantly adding empty solution to it. * Fixed StomachBehaviour using wrong SolutionContainerComponent Stomach was using the first SolutionContainerComponent in the owner of the body, instead of the container in the owner of the mechanism (stomach). As a result, it used to use the BloodStreamComponent.Solution as a "Stomach". * Update StomachBehavior.cs Somach now checks if it still contains a reagant, before transferring it. * Added argument to IMetabolizable.Metabolize() Added availableReagent argument to IMetabolizable.Metabolize(), This ensures that this function does not over-metabolize a reagant, which can happen if tickTime*metabolismRate is larger than the available reagant * Revert "Stomach now only transfers non-empty solutions." This reverts commit 2a51e2d87e6e17ab76b48e5316ce501ec05ac061. * Renamed _updateInterval to _updateIntervalSeconds Also modified doc comment specifying units * Fix spelling of healthChangeAmount Changed from healthChangeAmmount to healthChangeAmount * Fixed comment comment used to mention _updateInterval, which has been renamed _updateIntervalSeconds * Fixed typo in comment * fixed typos: reagant -> reagent Most typos were just in comments. * Make metabolizable classes inherit from DefaultMetabolizable Also involved changing around IMetabolizable * Renamed variables metabolismAmount -> amountMetabolized * Updated Comments in DefaultMetabolizable Makes it clearer why DefaultMetabolizable works as it does, and that other classes depend on it. --- Content.Server/Body/Behavior/LiverBehavior.cs | 18 ++++++++--- .../Body/Behavior/StomachBehavior.cs | 25 ++++++++++++---- .../Chemistry/Metabolism/DefaultDrink.cs | 21 +++++++------ .../Chemistry/Metabolism/DefaultFood.cs | 30 +++++++++---------- .../Metabolism/HealthChangeMetabolism.cs | 28 +++++++++-------- .../Metabolizable/DefaultMetabolizable.cs | 24 +++++++++++---- .../Chemistry/Metabolizable/IMetabolizable.cs | 5 ++-- 7 files changed, 96 insertions(+), 55 deletions(-) diff --git a/Content.Server/Body/Behavior/LiverBehavior.cs b/Content.Server/Body/Behavior/LiverBehavior.cs index 82e12b29ce..95897b320a 100644 --- a/Content.Server/Body/Behavior/LiverBehavior.cs +++ b/Content.Server/Body/Behavior/LiverBehavior.cs @@ -17,6 +17,11 @@ namespace Content.Server.Body.Behavior private float _accumulatedFrameTime; + /// + /// Delay time that determines how often to metabolise blood contents (in seconds). + /// + private float _updateIntervalSeconds = 1.0f; + /// /// Whether the liver is functional. /// @@ -63,13 +68,13 @@ namespace Content.Server.Body.Behavior _accumulatedFrameTime += frameTime; - // Update at most once per second - if (_accumulatedFrameTime < 1) + // Update at most once every _updateIntervalSeconds + if (_accumulatedFrameTime < _updateIntervalSeconds) { return; } - _accumulatedFrameTime -= 1; + _accumulatedFrameTime -= _updateIntervalSeconds; if (!Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream)) { @@ -90,6 +95,10 @@ namespace Content.Server.Body.Behavior continue; } + // How much reagent is available to metabolise? + // This needs to be passed to other functions that have metabolism rate information, such that they don't "overmetabolise" a reagent. + var availableReagent = bloodstream.Solution.Solution.GetReagentQuantity(reagent.ReagentId); + //TODO BODY Check if it's a Toxin. If volume < _toxinTolerance, just remove it. If greater, add damage = volume * _toxinLethality //TODO BODY Check if it has BoozePower > 0. Affect drunkenness, apply damage. Proposed formula (SS13-derived): damage = sqrt(volume) * BoozePower^_alcoholExponent * _alcoholLethality / 10 //TODO BODY Liver failure. @@ -99,8 +108,9 @@ namespace Content.Server.Body.Behavior // Run metabolism code for each reagent foreach (var metabolizable in prototype.Metabolism) { - var reagentDelta = metabolizable.Metabolize(Body.Owner, reagent.ReagentId, frameTime); + var reagentDelta = metabolizable.Metabolize(Body.Owner, reagent.ReagentId, _updateIntervalSeconds, availableReagent); bloodstream.Solution.TryRemoveReagent(reagent.ReagentId, reagentDelta); + availableReagent -= reagentDelta; } } } diff --git a/Content.Server/Body/Behavior/StomachBehavior.cs b/Content.Server/Body/Behavior/StomachBehavior.cs index a6c903efe9..813b8a3a5e 100644 --- a/Content.Server/Body/Behavior/StomachBehavior.cs +++ b/Content.Server/Body/Behavior/StomachBehavior.cs @@ -30,6 +30,8 @@ namespace Content.Server.Body.Behavior /// public override void Update(float frameTime) { + + // Do not metabolise if the organ does not have a body. if (Body == null) { return; @@ -45,7 +47,9 @@ namespace Content.Server.Body.Behavior _accumulatedFrameTime -= 1; - if (!Body.Owner.TryGetComponent(out SolutionContainerComponent? solution) || + // Note that "Owner" should be the organ that has this behaviour/mechanism, and it should have a dedicated + // solution container. "Body.Owner" is something else, and may have more than one solution container. + if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || !Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream)) { return; @@ -61,8 +65,19 @@ namespace Content.Server.Body.Behavior delta.Increment(1); if (delta.Lifetime > _digestionDelay) { - solution.TryRemoveReagent(delta.ReagentId, delta.Quantity); - transferSolution.AddReagent(delta.ReagentId, delta.Quantity); + // This reagent has been in the somach long enough, TRY to transfer it. + // But first, check if the reagent still exists, and how much is left. + // Some poor spessman may have washed down a potassium snack with some water. + if (solution.Solution.ContainsReagent(delta.ReagentId, out ReagentUnit quantity)){ + + if (quantity > delta.Quantity) { + quantity = delta.Quantity; + } + + solution.TryRemoveReagent(delta.ReagentId, quantity); + transferSolution.AddReagent(delta.ReagentId, quantity); + } + _reagentDeltas.Remove(delta); } } @@ -133,10 +148,10 @@ namespace Content.Server.Body.Behavior public bool TryTransferSolution(Solution solution) { - if (Body == null || !CanTransferSolution(solution)) + if (Owner == null || !CanTransferSolution(solution)) return false; - if (!Body.Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent)) + if (!Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent)) { return false; } diff --git a/Content.Server/Chemistry/Metabolism/DefaultDrink.cs b/Content.Server/Chemistry/Metabolism/DefaultDrink.cs index 3b32776c85..255200f103 100644 --- a/Content.Server/Chemistry/Metabolism/DefaultDrink.cs +++ b/Content.Server/Chemistry/Metabolism/DefaultDrink.cs @@ -1,4 +1,4 @@ -using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.Components; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Metabolizable; using Content.Shared.Chemistry.Reagent; @@ -9,28 +9,27 @@ namespace Content.Server.Chemistry.Metabolism { /// /// Default metabolism for drink reagents. Attempts to find a ThirstComponent on the target, - /// and to update it's thirst values. + /// and to update it's thirst values. Inherits metabolisation rate logic from DefaultMetabolizable. /// [DataDefinition] - public class DefaultDrink : IMetabolizable + public class DefaultDrink : DefaultMetabolizable { - //Rate of metabolism in units / second - [DataField("rate")] - public ReagentUnit MetabolismRate { get; set; } = ReagentUnit.New(1); - //How much thirst is satiated when 1u of the reagent is metabolized [DataField("hydrationFactor")] public float HydrationFactor { get; set; } = 30.0f; //Remove reagent at set rate, satiate thirst if a ThirstComponent can be found - ReagentUnit IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) + public override ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent) { - var metabolismAmount = MetabolismRate * tickTime; + // use DefaultMetabolism to determine how much reagent we should metabolize + var amountMetabolized = base.Metabolize(solutionEntity, reagentId, tickTime, availableReagent); + + // If metabolizing entity has a ThirstComponent, hydrate them. if (solutionEntity.TryGetComponent(out ThirstComponent? thirst)) - thirst.UpdateThirst(metabolismAmount.Float() * HydrationFactor); + thirst.UpdateThirst(amountMetabolized.Float() * HydrationFactor); //Return amount of reagent to be removed, remove reagent regardless of ThirstComponent presence - return metabolismAmount; + return amountMetabolized; } } } diff --git a/Content.Server/Chemistry/Metabolism/DefaultFood.cs b/Content.Server/Chemistry/Metabolism/DefaultFood.cs index aeba8ed95d..fafcf58dd1 100644 --- a/Content.Server/Chemistry/Metabolism/DefaultFood.cs +++ b/Content.Server/Chemistry/Metabolism/DefaultFood.cs @@ -1,4 +1,4 @@ -using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.Components; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Metabolizable; using Content.Shared.Chemistry.Reagent; @@ -9,30 +9,30 @@ namespace Content.Server.Chemistry.Metabolism { /// /// Default metabolism for food reagents. Attempts to find a HungerComponent on the target, - /// and to update it's hunger values. + /// and to update it's hunger values. Inherits metabolisation rate logic from DefaultMetabolizable. /// [DataDefinition] - public class DefaultFood : IMetabolizable + public class DefaultFood : DefaultMetabolizable { - /// - /// Rate of metabolism in units / second - /// - [DataField("rate")] public ReagentUnit MetabolismRate { get; private set; } = ReagentUnit.New(1.0); /// /// How much hunger is satiated when 1u of the reagent is metabolized /// [DataField("nutritionFactor")] public float NutritionFactor { get; set; } = 30.0f; - //Remove reagent at set rate, satiate hunger if a HungerComponent can be found - ReagentUnit IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) - { - var metabolismAmount = MetabolismRate * tickTime; - if (solutionEntity.TryGetComponent(out HungerComponent? hunger)) - hunger.UpdateFood(metabolismAmount.Float() * NutritionFactor); - //Return amount of reagent to be removed, remove reagent regardless of HungerComponent presence - return metabolismAmount; + //Remove reagent at set rate, satiate hunger if a HungerComponent can be found + public override ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent) + { + // use DefaultMetabolism to determine how much reagent we should metabolize + var amountMetabolized = base.Metabolize(solutionEntity, reagentId, tickTime, availableReagent); + + // If metabolizing entity has a HungerComponent, feed them. + if (solutionEntity.TryGetComponent(out HungerComponent? hunger)) + hunger.UpdateFood(amountMetabolized.Float() * NutritionFactor); + + //Return amount of reagent to be removed. Reagent is removed regardless of HungerComponent presence + return amountMetabolized; } } } diff --git a/Content.Server/Chemistry/Metabolism/HealthChangeMetabolism.cs b/Content.Server/Chemistry/Metabolism/HealthChangeMetabolism.cs index 45bbff68d7..bbf2534505 100644 --- a/Content.Server/Chemistry/Metabolism/HealthChangeMetabolism.cs +++ b/Content.Server/Chemistry/Metabolism/HealthChangeMetabolism.cs @@ -10,16 +10,11 @@ namespace Content.Server.Chemistry.Metabolism { /// /// Default metabolism for medicine reagents. Attempts to find a DamageableComponent on the target, - /// and to update its damage values. + /// and to update its damage values. Inherits metabolisation rate logic from DefaultMetabolizable. /// [DataDefinition] - public class HealthChangeMetabolism : IMetabolizable + public class HealthChangeMetabolism : DefaultMetabolizable { - /// - /// How much of the reagent should be metabolized each sec. - /// - [DataField("rate")] - public ReagentUnit MetabolismRate { get; set; } = ReagentUnit.New(1); /// /// How much damage is changed when 1u of the reagent is metabolized. @@ -41,14 +36,23 @@ namespace Content.Server.Chemistry.Metabolism /// /// /// + /// Reagent available to be metabolized. /// - ReagentUnit IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) + public override ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent) { + // use DefaultMetabolism to determine how much reagent we should metabolize + var amountMetabolized = base.Metabolize(solutionEntity, reagentId, tickTime, availableReagent); + + // how much does this much reagent heal for + var healthChangeAmount = HealthChange * amountMetabolized.Float(); + if (solutionEntity.TryGetComponent(out IDamageableComponent? health)) { - health.ChangeDamage(DamageType, (int)HealthChange, true); - float decHealthChange = (float) (HealthChange - (int) HealthChange); - _accumulatedHealth += decHealthChange; + // Heal damage by healthChangeAmmount, rounding down to nearest integer + health.ChangeDamage(DamageType, (int) healthChangeAmount, true); + + // Store decimal remainder of healthChangeAmmount in _accumulatedHealth + _accumulatedHealth += (healthChangeAmount - (int) healthChangeAmount); if (_accumulatedHealth >= 1) { @@ -62,7 +66,7 @@ namespace Content.Server.Chemistry.Metabolism _accumulatedHealth += 1; } } - return MetabolismRate; + return amountMetabolized; } } } diff --git a/Content.Shared/Chemistry/Metabolizable/DefaultMetabolizable.cs b/Content.Shared/Chemistry/Metabolizable/DefaultMetabolizable.cs index cbd10016a5..939acdd21a 100644 --- a/Content.Shared/Chemistry/Metabolizable/DefaultMetabolizable.cs +++ b/Content.Shared/Chemistry/Metabolizable/DefaultMetabolizable.cs @@ -1,11 +1,13 @@ -using Content.Shared.Chemistry.Reagent; +using Content.Shared.Chemistry.Reagent; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Shared.Chemistry.Metabolizable { /// - /// Default metabolism for reagents. Metabolizes the reagent with no effects + /// Default metabolization for reagents. Returns the amount of reagents metabolized without applying effects. + /// Metabolizes reagents at a constant rate, limited by how much is available. Other classes are derived from + /// this class, so that they do not need their own metabolization quantity calculation. /// [DataDefinition] public class DefaultMetabolizable : IMetabolizable @@ -13,12 +15,22 @@ namespace Content.Shared.Chemistry.Metabolizable /// /// Rate of metabolism in units / second /// - [DataField("rate")] - public double MetabolismRate { get; set; } = 1; + [DataField("rate")] public ReagentUnit MetabolismRate { get; set; } = ReagentUnit.New(1); - ReagentUnit IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) + public virtual ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent) { - return ReagentUnit.New(MetabolismRate * tickTime); + + // How much reagent should we metabolize + // The default behaviour is to metabolize at a constant rate, independent of the quantity of reagents. + var amountMetabolized = MetabolismRate * tickTime; + + // is that much reagent actually available? + if (availableReagent < amountMetabolized) + { + return availableReagent; + } + + return amountMetabolized; } } } diff --git a/Content.Shared/Chemistry/Metabolizable/IMetabolizable.cs b/Content.Shared/Chemistry/Metabolizable/IMetabolizable.cs index e08e209a0c..f8b2a888cc 100644 --- a/Content.Shared/Chemistry/Metabolizable/IMetabolizable.cs +++ b/Content.Shared/Chemistry/Metabolizable/IMetabolizable.cs @@ -1,4 +1,4 @@ -using Content.Shared.Chemistry.Reagent; +using Content.Shared.Chemistry.Reagent; using Robust.Shared.GameObjects; namespace Content.Shared.Chemistry.Metabolizable @@ -16,7 +16,8 @@ namespace Content.Shared.Chemistry.Metabolizable /// The entity containing the solution. /// The reagent id /// The time since the last metabolism tick in seconds. + /// Reagent available to be metabolized. /// The amount of reagent to be removed. The metabolizing organ should handle removing the reagent. - ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime); + ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent); } } From f93bdcd226e06daa3ae14d15c4cbb0c5b98316fb Mon Sep 17 00:00:00 2001 From: mirrorcult Date: Sat, 31 Jul 2021 01:59:18 -0700 Subject: [PATCH 8/9] Fix serv warnings (#4400) --- .../Chemistry/ReagentEntityReactions/AddToSolutionReaction.cs | 1 - .../Chemistry/ReagentEntityReactions/ExtinguishReaction.cs | 1 - .../Chemistry/ReagentEntityReactions/FlammableReaction.cs | 1 - .../Chemistry/ReagentEntityReactions/WashCreamPieReaction.cs | 1 - Content.Shared/Chemistry/Reagent/ReagentEntityReaction.cs | 2 +- Content.Shared/Sound/SoundSpecifier.cs | 4 +--- 6 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Content.Server/Chemistry/ReagentEntityReactions/AddToSolutionReaction.cs b/Content.Server/Chemistry/ReagentEntityReactions/AddToSolutionReaction.cs index 3218e4f983..f509fa0d56 100644 --- a/Content.Server/Chemistry/ReagentEntityReactions/AddToSolutionReaction.cs +++ b/Content.Server/Chemistry/ReagentEntityReactions/AddToSolutionReaction.cs @@ -10,7 +10,6 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy namespace Content.Server.Chemistry.ReagentEntityReactions { [UsedImplicitly] - [DataDefinition] public class AddToSolutionReaction : ReagentEntityReaction { [DataField("reagents", true, customTypeSerializer:typeof(PrototypeIdHashSetSerializer))] diff --git a/Content.Server/Chemistry/ReagentEntityReactions/ExtinguishReaction.cs b/Content.Server/Chemistry/ReagentEntityReactions/ExtinguishReaction.cs index 9078eef2d1..860df2c423 100644 --- a/Content.Server/Chemistry/ReagentEntityReactions/ExtinguishReaction.cs +++ b/Content.Server/Chemistry/ReagentEntityReactions/ExtinguishReaction.cs @@ -10,7 +10,6 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy namespace Content.Server.Chemistry.ReagentEntityReactions { [UsedImplicitly] - [DataDefinition] public class ExtinguishReaction : ReagentEntityReaction { [DataField("reagents", true, customTypeSerializer:typeof(PrototypeIdHashSetSerializer))] diff --git a/Content.Server/Chemistry/ReagentEntityReactions/FlammableReaction.cs b/Content.Server/Chemistry/ReagentEntityReactions/FlammableReaction.cs index 5242565e0e..8c71a8904d 100644 --- a/Content.Server/Chemistry/ReagentEntityReactions/FlammableReaction.cs +++ b/Content.Server/Chemistry/ReagentEntityReactions/FlammableReaction.cs @@ -10,7 +10,6 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy namespace Content.Server.Chemistry.ReagentEntityReactions { [UsedImplicitly] - [DataDefinition] public class FlammableReaction : ReagentEntityReaction { [DataField("reagents", true, customTypeSerializer:typeof(PrototypeIdHashSetSerializer))] diff --git a/Content.Server/Chemistry/ReagentEntityReactions/WashCreamPieReaction.cs b/Content.Server/Chemistry/ReagentEntityReactions/WashCreamPieReaction.cs index 44774ffcc9..8005384562 100644 --- a/Content.Server/Chemistry/ReagentEntityReactions/WashCreamPieReaction.cs +++ b/Content.Server/Chemistry/ReagentEntityReactions/WashCreamPieReaction.cs @@ -10,7 +10,6 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy namespace Content.Server.Chemistry.ReagentEntityReactions { [UsedImplicitly] - [DataDefinition] public class WashCreamPieReaction : ReagentEntityReaction { [DataField("reagents", true, customTypeSerializer:typeof(PrototypeIdHashSetSerializer))] diff --git a/Content.Shared/Chemistry/Reagent/ReagentEntityReaction.cs b/Content.Shared/Chemistry/Reagent/ReagentEntityReaction.cs index 59f931e871..fe8e081e25 100644 --- a/Content.Shared/Chemistry/Reagent/ReagentEntityReaction.cs +++ b/Content.Shared/Chemistry/Reagent/ReagentEntityReaction.cs @@ -12,7 +12,7 @@ namespace Content.Shared.Chemistry.Reagent Ingestion, } - [DataDefinition] + [ImplicitDataDefinitionForInheritors] public abstract class ReagentEntityReaction { [ViewVariables] diff --git a/Content.Shared/Sound/SoundSpecifier.cs b/Content.Shared/Sound/SoundSpecifier.cs index 616ccf9551..bb74337e8a 100644 --- a/Content.Shared/Sound/SoundSpecifier.cs +++ b/Content.Shared/Sound/SoundSpecifier.cs @@ -8,13 +8,12 @@ using Robust.Shared.Utility; namespace Content.Shared.Sound { - [DataDefinition] + [ImplicitDataDefinitionForInheritors] public abstract class SoundSpecifier { public abstract string GetSound(); } - [DataDefinition] public sealed class SoundPathSpecifier : SoundSpecifier { public const string Node = "path"; @@ -42,7 +41,6 @@ namespace Content.Shared.Sound } } - [DataDefinition] public sealed class SoundCollectionSpecifier : SoundSpecifier { public const string Node = "collection"; From dc18997bf8c859ce3cd59ffde718a768f5c8b967 Mon Sep 17 00:00:00 2001 From: Vera Aguilera Puerto <6766154+Zumorica@users.noreply.github.com> Date: Sat, 31 Jul 2021 12:41:59 +0200 Subject: [PATCH 9/9] Removes LoopingSoundComponent. (#4396) I'm sorry. --- Content.Client/Entry/EntryPoint.cs | 1 - .../{ => Components}/KitchenSpikeComponent.cs | 2 +- .../Kitchen/Components/MicrowaveComponent.cs | 17 +++ .../Kitchen/EntitySystems/MicrowaveSystem.cs | 31 +++++ .../Visualizers/MicrowaveVisualizer.cs | 24 ++-- .../Components/ExpendableLightComponent.cs | 3 +- .../Visualizers/ExpendableLightVisualizer.cs | 36 +++++- Content.Client/Sound/LoopingSoundComponent.cs | 105 ---------------- .../Components/Mobs/ActionsComponentTests.cs | 1 - .../EntitySystems/ReagentGrinderSystem.cs | 3 + .../Components/ExpendableLightComponent.cs | 21 +--- .../LoopingLoopingSoundComponent.cs | 67 ---------- .../SharedExpendableLightComponent.cs | 12 +- .../Sound/SharedLoopingSoundComponent.cs | 116 ------------------ .../Entities/Clothing/Head/base.yml | 1 - .../Entities/Clothing/Head/hardhats.yml | 1 - .../Entities/Clothing/Shoes/specific.yml | 1 - .../Prototypes/Entities/Effects/puddle.yml | 1 - .../Prototypes/Entities/Mobs/NPCs/animals.yml | 1 - .../Objects/Consumable/Food/Baked/bread.yml | 1 - .../Objects/Consumable/Food/Baked/cake.yml | 1 - .../Objects/Consumable/Food/Baked/donut.yml | 1 - .../Objects/Consumable/Food/Baked/misc.yml | 1 - .../Objects/Consumable/Food/Baked/pizza.yml | 1 - .../Consumable/Food/Containers/box.yml | 1 - .../Consumable/Food/Containers/tin.yml | 4 - .../Objects/Consumable/Food/burger.yml | 1 - .../Entities/Objects/Consumable/Food/egg.yml | 1 - .../Objects/Consumable/Food/frozen.yml | 1 - .../Entities/Objects/Consumable/Food/meat.yml | 1 - .../Objects/Consumable/Food/noodles.yml | 1 - .../Objects/Consumable/Food/skewer.yml | 1 - .../Objects/Consumable/Food/snacks.yml | 1 - .../Entities/Objects/Consumable/Food/soup.yml | 1 - .../Objects/Consumable/trash_drinks.yml | 1 - .../Entities/Objects/Devices/pda.yml | 1 - .../Prototypes/Entities/Objects/Fun/skub.yml | 1 - .../Prototypes/Entities/Objects/Fun/toys.yml | 22 +--- .../Entities/Objects/Misc/torch.yml | 1 - .../Entities/Objects/Tools/flare.yml | 1 - .../Entities/Objects/Tools/flashlight.yml | 1 - .../Entities/Objects/Tools/lantern.yml | 2 - .../Objects/Weapons/Melee/pickaxe.yml | 1 - .../Entities/Structures/Dispensers/base.yml | 1 - .../Structures/Machines/microwave.yml | 1 - .../Structures/Machines/reagent_grinder.yml | 1 - .../Structures/Storage/Closets/base.yml | 1 - .../Structures/Storage/Crates/base.yml | 1 - .../Entities/Structures/Storage/morgue.yml | 1 - .../Structures/Wallmounts/lighting.yml | 1 - 50 files changed, 120 insertions(+), 380 deletions(-) rename Content.Client/Kitchen/{ => Components}/KitchenSpikeComponent.cs (88%) create mode 100644 Content.Client/Kitchen/Components/MicrowaveComponent.cs create mode 100644 Content.Client/Kitchen/EntitySystems/MicrowaveSystem.cs delete mode 100644 Content.Client/Sound/LoopingSoundComponent.cs delete mode 100644 Content.Server/Sound/Components/LoopingLoopingSoundComponent.cs delete mode 100644 Content.Shared/Sound/SharedLoopingSoundComponent.cs diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index b975c28673..95a7ec0596 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -81,7 +81,6 @@ namespace Content.Client.Entry factory.RegisterClass(); factory.RegisterClass(); factory.RegisterClass(); - factory.RegisterClass(); factory.RegisterClass(); factory.RegisterClass(); diff --git a/Content.Client/Kitchen/KitchenSpikeComponent.cs b/Content.Client/Kitchen/Components/KitchenSpikeComponent.cs similarity index 88% rename from Content.Client/Kitchen/KitchenSpikeComponent.cs rename to Content.Client/Kitchen/Components/KitchenSpikeComponent.cs index efae8b89e6..58f271d63f 100644 --- a/Content.Client/Kitchen/KitchenSpikeComponent.cs +++ b/Content.Client/Kitchen/Components/KitchenSpikeComponent.cs @@ -2,7 +2,7 @@ using Content.Shared.DragDrop; using Content.Shared.Kitchen.Components; using Robust.Shared.GameObjects; -namespace Content.Client.Kitchen +namespace Content.Client.Kitchen.Components { [RegisterComponent] internal sealed class KitchenSpikeComponent : SharedKitchenSpikeComponent diff --git a/Content.Client/Kitchen/Components/MicrowaveComponent.cs b/Content.Client/Kitchen/Components/MicrowaveComponent.cs new file mode 100644 index 0000000000..862a8b6de0 --- /dev/null +++ b/Content.Client/Kitchen/Components/MicrowaveComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared.Kitchen.Components; +using Content.Shared.Sound; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Client.Kitchen.Components +{ + [RegisterComponent] + public class MicrowaveComponent : SharedMicrowaveComponent + { + public IPlayingAudioStream? PlayingStream { get; set; } + + [DataField("loopingSound")] + public SoundSpecifier LoopingSound = new SoundPathSpecifier("/Audio/Machines/microwave_loop.ogg"); + } +} diff --git a/Content.Client/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Client/Kitchen/EntitySystems/MicrowaveSystem.cs new file mode 100644 index 0000000000..6a18a0feac --- /dev/null +++ b/Content.Client/Kitchen/EntitySystems/MicrowaveSystem.cs @@ -0,0 +1,31 @@ +using System; +using Content.Client.Kitchen.Components; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.Player; + +namespace Content.Client.Kitchen.EntitySystems +{ + public class MicrowaveSystem : EntitySystem + { + public void StartSoundLoop(MicrowaveComponent microwave) + { + StopSoundLoop(microwave); + + microwave.PlayingStream = SoundSystem.Play(Filter.Local(), microwave.LoopingSound.GetSound(), microwave.Owner, + AudioParams.Default.WithAttenuation(1).WithMaxDistance(5).WithLoop(true)); + } + + public void StopSoundLoop(MicrowaveComponent microwave) + { + try + { + microwave.PlayingStream?.Stop(); + } + catch (Exception _) + { + // TODO: HOLY SHIT EXPOSE SOME DISPOSED PROPERTY ON PLAYING STREAM OR SOMETHING. + } + } + } +} diff --git a/Content.Client/Kitchen/Visualizers/MicrowaveVisualizer.cs b/Content.Client/Kitchen/Visualizers/MicrowaveVisualizer.cs index 97d2bf0e7c..c0a3a68e8c 100644 --- a/Content.Client/Kitchen/Visualizers/MicrowaveVisualizer.cs +++ b/Content.Client/Kitchen/Visualizers/MicrowaveVisualizer.cs @@ -1,10 +1,10 @@ -using Content.Client.Sound; +using Content.Client.Kitchen.Components; +using Content.Client.Kitchen.EntitySystems; using Content.Shared.Kitchen.Components; using Content.Shared.Power; -using Content.Shared.Sound; using JetBrains.Annotations; using Robust.Client.GameObjects; -using Robust.Shared.Audio; +using Robust.Shared.GameObjects; using Robust.Shared.Log; namespace Content.Client.Kitchen.Visualizers @@ -17,35 +17,33 @@ namespace Content.Client.Kitchen.Visualizers base.OnChangeData(component); var sprite = component.Owner.GetComponent(); - var loopingSoundComponent = component.Owner.GetComponentOrNull(); + var microwaveComponent = component.Owner.GetComponentOrNull(); if (!component.TryGetData(PowerDeviceVisuals.VisualState, out MicrowaveVisualState state)) { state = MicrowaveVisualState.Idle; } + // The only reason we get the entity system so late is so that tests don't fail... Amazing, huh? switch (state) { case MicrowaveVisualState.Broken: sprite.LayerSetState(MicrowaveVisualizerLayers.BaseUnlit, "mwb"); - loopingSoundComponent?.StopAllSounds(); + if(microwaveComponent != null) + EntitySystem.Get().StopSoundLoop(microwaveComponent); break; case MicrowaveVisualState.Idle: sprite.LayerSetState(MicrowaveVisualizerLayers.Base, "mw"); sprite.LayerSetState(MicrowaveVisualizerLayers.BaseUnlit, "mw_unlit"); - loopingSoundComponent?.StopAllSounds(); + if(microwaveComponent != null) + EntitySystem.Get().StopSoundLoop(microwaveComponent); break; case MicrowaveVisualState.Cooking: sprite.LayerSetState(MicrowaveVisualizerLayers.Base, "mw"); sprite.LayerSetState(MicrowaveVisualizerLayers.BaseUnlit, "mw_running_unlit"); - var audioParams = AudioParams.Default; - audioParams.Loop = true; - var scheduledSound = new ScheduledSound(); - scheduledSound.Filename = "/Audio/Machines/microwave_loop.ogg"; - scheduledSound.AudioParams = audioParams; - loopingSoundComponent?.StopAllSounds(); - loopingSoundComponent?.AddScheduledSound(scheduledSound); + if(microwaveComponent != null) + EntitySystem.Get().StartSoundLoop(microwaveComponent); break; default: diff --git a/Content.Client/Light/Components/ExpendableLightComponent.cs b/Content.Client/Light/Components/ExpendableLightComponent.cs index 5531379084..37eb37ee13 100644 --- a/Content.Client/Light/Components/ExpendableLightComponent.cs +++ b/Content.Client/Light/Components/ExpendableLightComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.Light.Component; +using Robust.Shared.Audio; using Robust.Shared.GameObjects; namespace Content.Client.Light.Components @@ -9,6 +10,6 @@ namespace Content.Client.Light.Components [RegisterComponent] public class ExpendableLightComponent : SharedExpendableLightComponent { - + public IPlayingAudioStream? PlayingStream { get; set; } } } diff --git a/Content.Client/Light/Visualizers/ExpendableLightVisualizer.cs b/Content.Client/Light/Visualizers/ExpendableLightVisualizer.cs index 3a21d11b1a..59e11983e8 100644 --- a/Content.Client/Light/Visualizers/ExpendableLightVisualizer.cs +++ b/Content.Client/Light/Visualizers/ExpendableLightVisualizer.cs @@ -1,7 +1,10 @@ -using Content.Client.Light.Components; +using System; +using Content.Client.Light.Components; using Content.Shared.Light.Component; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Player; namespace Content.Client.Light.Visualizers { @@ -17,7 +20,7 @@ namespace Content.Client.Light.Visualizers return; } - if (component.TryGetData(ExpendableLightVisuals.State, out string lightBehaviourID)) + if (component.TryGetData(ExpendableLightVisuals.Behavior, out string lightBehaviourID)) { if (component.Owner.TryGetComponent(out var lightBehaviour)) { @@ -33,6 +36,35 @@ namespace Content.Client.Light.Visualizers } } } + + void TryStopStream(IPlayingAudioStream? stream) + { + try + { + stream?.Stop(); + } + catch (Exception _) + { + // TODO: HOLY SHIT EXPOSE SOME DISPOSED PROPERTY ON PLAYING STREAM OR SOMETHING. + } + } + + if (component.TryGetData(ExpendableLightVisuals.State, out ExpendableLightState state) + && component.Owner.TryGetComponent(out var expendableLight)) + { + switch (state) + { + case ExpendableLightState.Lit: + TryStopStream(expendableLight.PlayingStream); + expendableLight.PlayingStream = SoundSystem.Play(Filter.Local(), expendableLight.LoopedSound, + expendableLight.Owner, SharedExpendableLightComponent.LoopedSoundParams.WithLoop(true)); + break; + + case ExpendableLightState.Dead: + TryStopStream(expendableLight.PlayingStream); + break; + } + } } } } diff --git a/Content.Client/Sound/LoopingSoundComponent.cs b/Content.Client/Sound/LoopingSoundComponent.cs deleted file mode 100644 index cd2d2e2812..0000000000 --- a/Content.Client/Sound/LoopingSoundComponent.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections.Generic; -using Content.Shared.Physics; -using Content.Shared.Sound; -using Robust.Shared.Audio; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Network; -using Robust.Shared.Player; -using Robust.Shared.Players; -using Robust.Shared.Random; -using Robust.Shared.Serialization.Manager.Attributes; - -namespace Content.Client.Sound -{ - [RegisterComponent] - public class LoopingSoundComponent : SharedLoopingSoundComponent - { - [Dependency] private readonly IRobustRandom _random = default!; - - private readonly Dictionary _audioStreams = new(); - - [DataField("schedules", true)] - private List _scheduledSounds - { - set => value.ForEach(AddScheduledSound); - get => new(); - } - - public override void StopAllSounds() - { - foreach (var kvp in _audioStreams) - { - kvp.Key.Play = false; - kvp.Value.Stop(); - } - _audioStreams.Clear(); - } - - public override void StopScheduledSound(string filename) - { - foreach (var kvp in _audioStreams) - { - if (kvp.Key.Filename != filename) continue; - kvp.Key.Play = false; - kvp.Value.Stop(); - _audioStreams.Remove(kvp.Key); - } - } - - public override void AddScheduledSound(ScheduledSound schedule) - { - Play(schedule); - } - - public void Play(ScheduledSound schedule) - { - if (!schedule.Play) return; - - Owner.SpawnTimer((int) schedule.Delay + (_random.Next((int) schedule.RandomDelay)),() => - { - if (!schedule.Play) return; // We make sure this hasn't changed. - - if (!_audioStreams.ContainsKey(schedule)) - { - _audioStreams.Add(schedule, SoundSystem.Play(Filter.Local(), schedule.Filename, Owner, schedule.AudioParams)!); - } - else - { - _audioStreams[schedule] = SoundSystem.Play(Filter.Local(), schedule.Filename, Owner, schedule.AudioParams)!; - } - - if (schedule.Times == 0) return; - - if (schedule.Times > 0) schedule.Times--; - - Play(schedule); - }); - } - - public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null) - { - base.HandleNetworkMessage(message, channel, session); - switch (message) - { - case ScheduledSoundMessage msg: - AddScheduledSound(msg.Schedule); - break; - - case StopSoundScheduleMessage msg: - StopScheduledSound(msg.Filename); - break; - - case StopAllSoundsMessage _: - StopAllSounds(); - break; - } - } - - protected override void Initialize() - { - base.Initialize(); - SoundSystem.OcclusionCollisionMask = (int) CollisionGroup.Impassable; - } - } -} diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs index 497e735a82..82bdf16910 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs @@ -52,7 +52,6 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs - type: PointLight enabled: false radius: 3 - - type: LoopingSound - type: Appearance visuals: - type: FlashLightVisualizer diff --git a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs index c58c5b32ee..2b110578c1 100644 --- a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs @@ -195,6 +195,9 @@ namespace Content.Server.Kitchen.EntitySystems while (_uiUpdateQueue.TryDequeue(out var comp)) { + if(comp.Deleted) + continue; + bool canJuice = false; bool canGrind = false; if (comp.BeakerContainer.ContainedEntity != null) diff --git a/Content.Server/Light/Components/ExpendableLightComponent.cs b/Content.Server/Light/Components/ExpendableLightComponent.cs index ef74376c01..180e791009 100644 --- a/Content.Server/Light/Components/ExpendableLightComponent.cs +++ b/Content.Server/Light/Components/ExpendableLightComponent.cs @@ -20,8 +20,6 @@ namespace Content.Server.Light.Components [RegisterComponent] public class ExpendableLightComponent : SharedExpendableLightComponent, IUse { - private static readonly AudioParams LoopedSoundParams = new(0, 1, "Master", 62.5f, 1, true, 0.3f); - /// /// Status of light, whether or not it is emitting light. /// @@ -77,18 +75,20 @@ namespace Content.Server.Light.Components private void UpdateVisualizer() { + _appearance?.SetData(ExpendableLightVisuals.State, CurrentState); + switch (CurrentState) { case ExpendableLightState.Lit: - _appearance?.SetData(ExpendableLightVisuals.State, TurnOnBehaviourID); + _appearance?.SetData(ExpendableLightVisuals.Behavior, TurnOnBehaviourID); break; case ExpendableLightState.Fading: - _appearance?.SetData(ExpendableLightVisuals.State, FadeOutBehaviourID); + _appearance?.SetData(ExpendableLightVisuals.Behavior, FadeOutBehaviourID); break; case ExpendableLightState.Dead: - _appearance?.SetData(ExpendableLightVisuals.State, string.Empty); + _appearance?.SetData(ExpendableLightVisuals.Behavior, string.Empty); break; } } @@ -100,12 +100,6 @@ namespace Content.Server.Light.Components switch (CurrentState) { case ExpendableLightState.Lit: - - if (LoopedSound != string.Empty && Owner.TryGetComponent(out var loopingSound)) - { - loopingSound.Play(LoopedSound, LoopedSoundParams); - } - if (LitSound != string.Empty) { SoundSystem.Play(Filter.Pvs(Owner), LitSound, Owner); @@ -131,11 +125,6 @@ namespace Content.Server.Light.Components SoundSystem.Play(Filter.Pvs(Owner), DieSound, Owner); } - if (LoopedSound != string.Empty && Owner.TryGetComponent(out var loopSound)) - { - loopSound.StopAllSounds(); - } - sprite.LayerSetState(0, IconStateSpent); sprite.LayerSetShader(0, "shaded"); sprite.LayerSetVisible(1, false); diff --git a/Content.Server/Sound/Components/LoopingLoopingSoundComponent.cs b/Content.Server/Sound/Components/LoopingLoopingSoundComponent.cs deleted file mode 100644 index 4230e39d8e..0000000000 --- a/Content.Server/Sound/Components/LoopingLoopingSoundComponent.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Content.Shared.Sound; -using Robust.Shared.Audio; -using Robust.Shared.GameObjects; -using Robust.Shared.Network; - -namespace Content.Server.Sound.Components -{ - [RegisterComponent] - public class LoopingLoopingSoundComponent : SharedLoopingSoundComponent - { - /// - /// Stops all sounds. - /// - /// User that will be affected. - public void StopAllSounds(INetChannel? channel) - { - SendNetworkMessage(new StopAllSoundsMessage(), channel); - } - - /// - /// Stops a certain scheduled sound from playing. - /// - /// User that will be affected. - public void StopScheduledSound(string filename, INetChannel? channel) - { - SendNetworkMessage(new StopSoundScheduleMessage() {Filename = filename}, channel); - } - - /// - /// Adds an scheduled sound to be played. - /// - /// User that will be affected. - public void AddScheduledSound(ScheduledSound schedule, INetChannel? channel) - { - SendNetworkMessage(new ScheduledSoundMessage() {Schedule = schedule}, channel); - } - - public override void StopAllSounds() - { - StopAllSounds(null); - } - - public override void StopScheduledSound(string filename) - { - StopScheduledSound(filename, null); - } - - public override void AddScheduledSound(ScheduledSound schedule) - { - AddScheduledSound(schedule, null); - } - - /// - /// Play an audio file following the entity. - /// - /// The resource path to the OGG Vorbis file to play. - /// User that will be affected. - public void Play(string filename, AudioParams? audioParams = null, INetChannel? channel = null) - { - AddScheduledSound(new ScheduledSound() - { - Filename = filename, - AudioParams = audioParams, - }, channel); - } - } -} diff --git a/Content.Shared/Light/Component/SharedExpendableLightComponent.cs b/Content.Shared/Light/Component/SharedExpendableLightComponent.cs index f6f057ea23..9c0e2960b2 100644 --- a/Content.Shared/Light/Component/SharedExpendableLightComponent.cs +++ b/Content.Shared/Light/Component/SharedExpendableLightComponent.cs @@ -1,4 +1,8 @@ using System; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.Players; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; @@ -8,7 +12,8 @@ namespace Content.Shared.Light.Component [Serializable, NetSerializable] public enum ExpendableLightVisuals { - State + State, + Behavior } [Serializable, NetSerializable] @@ -20,8 +25,11 @@ namespace Content.Shared.Light.Component Dead } + [NetworkedComponent] public abstract class SharedExpendableLightComponent: Robust.Shared.GameObjects.Component { + public static readonly AudioParams LoopedSoundParams = new(0, 1, "Master", 62.5f, 1, true, 0.3f); + public sealed override string Name => "ExpendableLight"; [ViewVariables(VVAccess.ReadOnly)] @@ -65,7 +73,7 @@ namespace Content.Shared.Light.Component [ViewVariables] [DataField("loopedSound")] - protected string LoopedSound { get; set; } = string.Empty; + public string LoopedSound { get; set; } = string.Empty; [ViewVariables] [DataField("dieSound")] diff --git a/Content.Shared/Sound/SharedLoopingSoundComponent.cs b/Content.Shared/Sound/SharedLoopingSoundComponent.cs deleted file mode 100644 index b31234d03e..0000000000 --- a/Content.Shared/Sound/SharedLoopingSoundComponent.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using Robust.Shared.Audio; -using Robust.Shared.GameObjects; -using Robust.Shared.GameStates; -using Robust.Shared.Serialization; -using Robust.Shared.Serialization.Manager.Attributes; - -namespace Content.Shared.Sound -{ - [NetworkedComponent()] - public class SharedLoopingSoundComponent : Component - { - public override string Name => "LoopingSound"; - - /// - /// Stops all sounds. - /// - public virtual void StopAllSounds() - {} - - /// - /// Stops a certain scheduled sound from playing. - /// - public virtual void StopScheduledSound(string filename) - {} - - /// - /// Adds an scheduled sound to be played. - /// - public virtual void AddScheduledSound(ScheduledSound scheduledSound) - {} - - /// - /// Play an audio file following the entity. - /// - /// The resource path to the OGG Vorbis file to play. - public void Play(string filename, AudioParams? audioParams = null) - { - AddScheduledSound(new ScheduledSound() - { - Filename = filename, - AudioParams = audioParams, - }); - } - } - - [NetSerializable, Serializable] - public class ScheduledSoundMessage : ComponentMessage - { - public ScheduledSound Schedule = new(); - public ScheduledSoundMessage() - { - Directed = true; - } - } - - [NetSerializable, Serializable] - public class StopSoundScheduleMessage : ComponentMessage - { - public string Filename = string.Empty; - public StopSoundScheduleMessage() - { - Directed = true; - } - } - - [NetSerializable, Serializable] - public class StopAllSoundsMessage : ComponentMessage - { - public StopAllSoundsMessage() - { - Directed = true; - } - } - - [Serializable, NetSerializable] - [DataDefinition] - public class ScheduledSound - { - [DataField("fileName")] - public string Filename = string.Empty; - - /// - /// The parameters to play the sound with. - /// - [DataField("audioparams")] - public AudioParams? AudioParams; - - /// - /// Delay in milliseconds before playing the sound, - /// and delay between repetitions if Times is not 0. - /// - [DataField("delay")] - public uint Delay; - - /// - /// Maximum number of milliseconds to add to the delay randomly. - /// Useful for random ambience noises. Generated value differs from client to client. - /// - [DataField("randomdelay")] - public uint RandomDelay; - - /// - /// How many times to repeat the sound. If it's 0, it will play the sound once. - /// If it's less than 0, it will repeat the sound indefinitely. - /// If it's greater than 0, it will play the sound n+1 times. - /// - [DataField("times")] - public int Times; - - /// - /// Whether the sound will play or not. - /// - public bool Play = true; - } -} diff --git a/Resources/Prototypes/Entities/Clothing/Head/base.yml b/Resources/Prototypes/Entities/Clothing/Head/base.yml index c81acedf5c..ea2aa62164 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/base.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/base.yml @@ -47,7 +47,6 @@ - type: PointLight enabled: false radius: 3 - - type: LoopingSound - type: Appearance visuals: - type: FlashLightVisualizer diff --git a/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml b/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml index e8e403ae9b..ab1d35e31f 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml @@ -15,7 +15,6 @@ - type: PointLight enabled: false radius: 3 - - type: LoopingSound - type: Appearance visuals: - type: FlashLightVisualizer diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml index d9481657b5..8863546e1c 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml @@ -19,7 +19,6 @@ sprite: Clothing/Shoes/Specific/clown.rsi - type: Clothing sprite: Clothing/Shoes/Specific/clown.rsi - - type: LoopingSound - type: FootstepModifier footstepSoundCollection: footstep_clown diff --git a/Resources/Prototypes/Entities/Effects/puddle.yml b/Resources/Prototypes/Entities/Effects/puddle.yml index 300ccbcf6e..6e018f8f0b 100644 --- a/Resources/Prototypes/Entities/Effects/puddle.yml +++ b/Resources/Prototypes/Entities/Effects/puddle.yml @@ -10,7 +10,6 @@ - type: Puddle spill_sound: /Audio/Effects/Fluids/splat.ogg recolor: true - - type: LoopingSound - type: Clickable - type: Slippery - type: Physics diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 4ef1d7ba87..45b1b8e193 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -387,7 +387,6 @@ # Eek! You can eat them alive for now until someone makes something that detects when # a mob is dead or something idk - type: Food - - type: LoopingSound - type: SolutionContainer contents: reagents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml index 5154e389c5..5e08078666 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml @@ -9,7 +9,6 @@ - type: Sprite sprite: Objects/Consumable/Food/Baked/bread.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 20 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml index 5f792c0c8e..0ecb6b5fa6 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml @@ -10,7 +10,6 @@ - type: Sprite sprite: Objects/Consumable/Food/Baked/cake.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 20 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donut.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donut.yml index 25b22164db..5f71cd9431 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donut.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donut.yml @@ -13,7 +13,6 @@ - type: Sprite sprite: Objects/Consumable/Food/Baked/donut.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 5 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/misc.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/misc.yml index d697dd849f..416fcd82ff 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/misc.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/misc.yml @@ -9,7 +9,6 @@ - type: Sprite sprite: Objects/Consumable/Food/Baked/misc.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 5 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml index c5d94a8ded..2a5c2fa711 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml @@ -10,7 +10,6 @@ - type: Sprite sprite: Objects/Consumable/Food/Baked/pizza.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 15 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml index da91d4b718..b80ef6f762 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml @@ -92,7 +92,6 @@ - box11 - box12 # Someday... - # - type: LoopingSound # - type: DamageOnLand # amount: 5 # - type: DamageOtherOnHit diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml index f37b3c2670..fa7bc163fa 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml @@ -78,7 +78,6 @@ reagents: - ReagentId: Nutriment Quantity: 15 - - type: LoopingSound - type: Food trash: FoodTinPeachesTrash @@ -127,7 +126,6 @@ reagents: - ReagentId: Nutriment Quantity: 15 - - type: LoopingSound - type: Food trash: FoodTinPeachesMaintTrash @@ -176,7 +174,6 @@ reagents: - ReagentId: Nutriment Quantity: 15 - - type: LoopingSound - type: Food trash: FoodTinBeansTrash @@ -227,7 +224,6 @@ reagents: - ReagentId: Nutriment Quantity: 15 - - type: LoopingSound - type: Food trash: FoodTinMRETrash diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml index 956cbabd68..7f4dfafec8 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml @@ -10,7 +10,6 @@ - type: Sprite sprite: Objects/Consumable/Food/burger.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 15 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml index 886827dcd8..06c818d1b9 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml @@ -23,7 +23,6 @@ reagents: - ReagentId: Egg Quantity: 5 - - type: LoopingSound - type: DamageOnLand amount: 1 - type: DamageOtherOnHit diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml index 86d65c8845..745d0ec100 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml @@ -9,7 +9,6 @@ - type: Sprite sprite: Objects/Consumable/Food/frozen.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 20 # For sprinkles or something? Idk. contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/meat.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/meat.yml index c04c0e016d..fef5800303 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/meat.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/meat.yml @@ -9,7 +9,6 @@ - type: Sprite sprite: Objects/Consumable/Food/meat.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 20 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/noodles.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/noodles.yml index 5c0f7ebce4..02ee16cf0c 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/noodles.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/noodles.yml @@ -11,7 +11,6 @@ - type: Sprite sprite: Objects/Consumable/Food/noodles.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 20 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/skewer.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/skewer.yml index 704e8a7376..3de439686d 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/skewer.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/skewer.yml @@ -10,7 +10,6 @@ - type: Sprite sprite: Objects/Consumable/Food/skewer.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 20 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml index b3fe15a37c..2fcb644269 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml @@ -9,7 +9,6 @@ - type: Sprite sprite: Objects/Consumable/Food/snacks.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 30 # Room for extra condiments contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml index 95d8810b6b..d7a7ab1de6 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml @@ -5,7 +5,6 @@ components: - type: Food trash: FoodBowlBig - - type: LoopingSound - type: SolutionContainer maxVol: 20 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/trash_drinks.yml b/Resources/Prototypes/Entities/Objects/Consumable/trash_drinks.yml index 25c6f54d7c..eb10d855f8 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/trash_drinks.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/trash_drinks.yml @@ -6,7 +6,6 @@ id: DrinkBottleBaseEmpty description: That's an empty bottle. components: - - type: LoopingSound - type: Sprite state: icon - type: SolutionContainer diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index f52578dce9..cf36a7dfa6 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -28,7 +28,6 @@ interfaces: - key: enum.PDAUiKey.Key type: PDABoundUserInterface - - type: LoopingSound - type: entity parent: BasePDA diff --git a/Resources/Prototypes/Entities/Objects/Fun/skub.yml b/Resources/Prototypes/Entities/Objects/Fun/skub.yml index e4695f5062..6f1e2e6842 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/skub.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/skub.yml @@ -10,7 +10,6 @@ - type: Item sprite: Objects/Misc/skub.rsi - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Items/skub.ogg diff --git a/Resources/Prototypes/Entities/Objects/Fun/toys.yml b/Resources/Prototypes/Entities/Objects/Fun/toys.yml index 0d8babe815..3ab1f035c5 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/toys.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/toys.yml @@ -5,7 +5,7 @@ id: BasePlushie components: - type: EmitSoundOnUse - sound: + sound: collection: ToySqueak - type: EmitSoundOnLand sound: @@ -13,7 +13,6 @@ - type: EmitSoundOnActivate sound: collection: ToySqueak - - type: LoopingSound - type: ItemCooldown - type: UseDelay delay: 1.0 @@ -91,7 +90,6 @@ sprite: Objects/Fun/toys.rsi state: plushie_snake - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Items/Toys/rattle.ogg @@ -108,7 +106,6 @@ sprite: Objects/Fun/toys.rsi state: toy_mouse - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Items/Toys/mousesqueek.ogg @@ -125,7 +122,6 @@ sprite: Objects/Fun/toys.rsi state: plushie_vox - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Voice/Vox/shriek1.ogg @@ -151,7 +147,6 @@ sound: path: /Audio/Items/Toys/helpme.ogg - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Items/Toys/helpme.ogg @@ -170,12 +165,11 @@ - type: Item sprite: Objects/Misc/carvings.rsi - type: EmitSoundOnThrow - sound: + sound: path: /Audio/Items/Toys/hellothere.ogg - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse - sound: + sound: path: /Audio/Items/Toys/hellothere.ogg - type: UseDelay delay: 1.0 @@ -195,9 +189,8 @@ sound: path: /Audio/Items/Toys/thankyou.ogg - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse - sound: + sound: path: /Audio/Items/Toys/thankyou.ogg - type: UseDelay delay: 1.0 @@ -214,12 +207,11 @@ - type: Item sprite: Objects/Misc/carvings.rsi - type: EmitSoundOnThrow - sound: + sound: path: /Audio/Items/Toys/verygood.ogg - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse - sound: + sound: path: /Audio/Items/Toys/verygood.ogg - type: UseDelay delay: 1.0 @@ -239,7 +231,6 @@ sound: path: /Audio/Items/Toys/imsorry.ogg - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Items/Toys/imsorry.ogg @@ -311,7 +302,6 @@ sprite: Objects/Fun/toys.rsi state: ian - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Items/Toys/ian.ogg diff --git a/Resources/Prototypes/Entities/Objects/Misc/torch.yml b/Resources/Prototypes/Entities/Objects/Misc/torch.yml index e2ca2f54ec..605ac31e3b 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/torch.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/torch.yml @@ -29,7 +29,6 @@ - type: Item sprite: Objects/Misc/torch.rsi HeldPrefix: unlit - - type: LoopingSound - type: Construction graph: lightTorch node: torch diff --git a/Resources/Prototypes/Entities/Objects/Tools/flare.yml b/Resources/Prototypes/Entities/Objects/Tools/flare.yml index 88f24071e3..cf2d18ee78 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/flare.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/flare.yml @@ -32,7 +32,6 @@ sprite: Objects/Misc/flare.rsi color: "#FF0000" HeldPrefix: unlit - - type: LoopingSound - type: Appearance visuals: - type: ExpendableLightVisualizer diff --git a/Resources/Prototypes/Entities/Objects/Tools/flashlight.yml b/Resources/Prototypes/Entities/Objects/Tools/flashlight.yml index cf5bff920b..80ffa05614 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/flashlight.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/flashlight.yml @@ -23,7 +23,6 @@ - type: PointLight enabled: false radius: 3 - - type: LoopingSound - type: Appearance visuals: - type: FlashLightVisualizer diff --git a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml index e50fc08d74..11fc7a70db 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml @@ -23,7 +23,6 @@ radius: 3 energy: 2.5 color: "#FFC458" - - type: LoopingSound - type: Appearance visuals: - type: LanternVisualizer @@ -39,4 +38,3 @@ radius: 5 energy: 10 color: "#FFC458" - - type: LoopingSound diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml index ec7cf4e96a..39f1b8bf96 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml @@ -17,4 +17,3 @@ size: 24 sprite: Objects/Weapons/Melee/pickaxe.rsi prefix: inhand - - type: LoopingSound diff --git a/Resources/Prototypes/Entities/Structures/Dispensers/base.yml b/Resources/Prototypes/Entities/Structures/Dispensers/base.yml index 412c455d39..2f33762b74 100644 --- a/Resources/Prototypes/Entities/Structures/Dispensers/base.yml +++ b/Resources/Prototypes/Entities/Structures/Dispensers/base.yml @@ -26,7 +26,6 @@ interfaces: - key: enum.ReagentDispenserUiKey.Key type: ReagentDispenserBoundUserInterface - - type: LoopingSound - type: Anchorable - type: Pullable - type: Damageable diff --git a/Resources/Prototypes/Entities/Structures/Machines/microwave.yml b/Resources/Prototypes/Entities/Structures/Machines/microwave.yml index aa9093711a..4210bdcac9 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/microwave.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/microwave.yml @@ -11,7 +11,6 @@ - type: Appearance visuals: - type: MicrowaveVisualizer - - type: LoopingSound - type: UserInterface interfaces: - key: enum.MicrowaveUiKey.Key diff --git a/Resources/Prototypes/Entities/Structures/Machines/reagent_grinder.yml b/Resources/Prototypes/Entities/Structures/Machines/reagent_grinder.yml index 4a9a89d5a2..1ab00ae0d5 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/reagent_grinder.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/reagent_grinder.yml @@ -14,7 +14,6 @@ - type: Appearance visuals: - type: ReagentGrinderVisualizer - - type: LoopingSound - type: Physics fixtures: - shape: diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/base.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/base.yml index 7c387a93d4..b64910f093 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Closets/base.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/base.yml @@ -57,4 +57,3 @@ - type: StorageVisualizer state_open: generic_open state_closed: generic_door - - type: LoopingSound diff --git a/Resources/Prototypes/Entities/Structures/Storage/Crates/base.yml b/Resources/Prototypes/Entities/Structures/Storage/Crates/base.yml index eec102958c..2177b328c2 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Crates/base.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Crates/base.yml @@ -49,4 +49,3 @@ - type: StorageVisualizer state_open: crate_open state_closed: crate_door - - type: LoopingSound diff --git a/Resources/Prototypes/Entities/Structures/Storage/morgue.yml b/Resources/Prototypes/Entities/Structures/Storage/morgue.yml index a4dab64e39..92e4e35a4e 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/morgue.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/morgue.yml @@ -105,7 +105,6 @@ openSound: /Audio/Items/deconstruct.ogg trayPrototype: CrematoriumTray doSoulBeep: false - - type: LoopingSound - type: Appearance visuals: - type: CrematoriumVisualizer diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml index 620f13a43d..de20894f6d 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml @@ -16,7 +16,6 @@ !type:PhysShapeAabb bounds: "-0.45, -0.15, 0.45, 0.35" layer: [ Passable ] - - type: LoopingSound - type: Sprite sprite: Structures/Wallmounts/Lighting/light_tube.rsi layers: