diff --git a/Content.Client/GameObjects/Components/StackComponent.cs b/Content.Client/GameObjects/Components/StackComponent.cs index fb132614ba..10e4f71da8 100644 --- a/Content.Client/GameObjects/Components/StackComponent.cs +++ b/Content.Client/GameObjects/Components/StackComponent.cs @@ -1,9 +1,6 @@ -#nullable enable - using Content.Client.UserInterface.Stylesheets; using Content.Client.Utility; using Content.Shared.GameObjects.Components; -using Robust.Client.GameObjects; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.GameObjects; @@ -17,38 +14,17 @@ namespace Content.Client.GameObjects.Components [ComponentReference(typeof(SharedStackComponent))] public class StackComponent : SharedStackComponent, IItemStatus { - [ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded; - [ComponentDependency] private readonly AppearanceComponent? _appearanceComponent = default!; + [ViewVariables(VVAccess.ReadWrite)] + private bool _uiUpdateNeeded; - public Control MakeControl() => new StatusControl(this); - - public override int Count + public Control MakeControl() { - get => base.Count; - set - { - var valueChanged = value != Count; - base.Count = value; - - if (valueChanged) - { - _appearanceComponent?.SetData(StackVisuals.Actual, Count); - - } - - _uiUpdateNeeded = true; - } + return new StatusControl(this); } - public override void Initialize() + public void DirtyUI() { - base.Initialize(); - - if (!Owner.Deleted) - { - _appearanceComponent?.SetData(StackVisuals.MaxCount, MaxCount); - _appearanceComponent?.SetData(StackVisuals.Hide, false); - } + _uiUpdateNeeded = true; } private sealed class StatusControl : Control diff --git a/Content.Client/GameObjects/EntitySystems/StackSystem.cs b/Content.Client/GameObjects/EntitySystems/StackSystem.cs new file mode 100644 index 0000000000..27e67089d0 --- /dev/null +++ b/Content.Client/GameObjects/EntitySystems/StackSystem.cs @@ -0,0 +1,25 @@ +using Content.Client.GameObjects.Components; +using Content.Shared.GameObjects.Components; +using Content.Shared.GameObjects.EntitySystems; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; + +namespace Content.Client.GameObjects.EntitySystems +{ + [UsedImplicitly] + public class StackSystem : SharedStackSystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStackCountChanged); + } + + private void OnStackCountChanged(EntityUid uid, StackComponent component, StackCountChangedEvent args) + { + // Dirty the UI now that the stack count has changed. + component.DirtyUI(); + } + } +} diff --git a/Content.Server/Construction/StackHelpers.cs b/Content.Server/Construction/StackHelpers.cs deleted file mode 100644 index 990ecdf1d3..0000000000 --- a/Content.Server/Construction/StackHelpers.cs +++ /dev/null @@ -1,33 +0,0 @@ -#nullable enable -using System; -using Content.Server.GameObjects.Components.Stack; -using Content.Shared.GameObjects.Components; -using Content.Shared.Stacks; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Map; - -namespace Content.Server.Construction -{ - public static class StackHelpers - { - /// - /// Spawns a stack of a specified type given an amount. - /// - public static IEntity SpawnStack(StackPrototype stack, int amount, EntityCoordinates coordinates, IEntityManager? entityManager = null) - { - entityManager ??= IoCManager.Resolve(); - - // TODO: Add more. - string prototype = stack.Spawn ?? throw new ArgumentOutOfRangeException(nameof(stack), - "Stack type doesn't have a prototype specified yet!"); - - var ent = entityManager.SpawnEntity(prototype, coordinates); - var stackComponent = ent.GetComponent(); - - stackComponent.Count = Math.Min(amount, stackComponent.MaxCount); - - return ent; - } - } -} diff --git a/Content.Server/GameObjects/Components/Construction/ConstructionComponent.cs b/Content.Server/GameObjects/Components/Construction/ConstructionComponent.cs index b449c9f4cc..9af31581ab 100644 --- a/Content.Server/GameObjects/Components/Construction/ConstructionComponent.cs +++ b/Content.Server/GameObjects/Components/Construction/ConstructionComponent.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Content.Server.GameObjects.Components.Interactable; using Content.Server.GameObjects.Components.Stack; +using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems.DoAfter; using Content.Shared.Construction; using Content.Shared.GameObjects.Components.Interactable; @@ -264,11 +265,17 @@ namespace Content.Server.GameObjects.Components.Construction break; case MaterialConstructionGraphStep materialStep: - if (materialStep.EntityValid(eventArgs.Using, out var sharedStack) + if (materialStep.EntityValid(eventArgs.Using, out var stack) && await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Finished) { - var stack = (StackComponent) sharedStack; - valid = stack.Split(materialStep.Amount, eventArgs.User.Transform.Coordinates, out entityUsing); + var splitStack = new StackSplitEvent() {Amount = materialStep.Amount, SpawnPosition = eventArgs.User.Transform.Coordinates}; + Owner.EntityManager.EventBus.RaiseLocalEvent(stack.Owner.Uid, splitStack); + + if (splitStack.Result != null) + { + entityUsing = splitStack.Result; + valid = true; + } } break; diff --git a/Content.Server/GameObjects/Components/Construction/MachineComponent.cs b/Content.Server/GameObjects/Components/Construction/MachineComponent.cs index 62d2ff618e..f64bf4afcd 100644 --- a/Content.Server/GameObjects/Components/Construction/MachineComponent.cs +++ b/Content.Server/GameObjects/Components/Construction/MachineComponent.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Content.Server.Construction; +using Content.Server.GameObjects.EntitySystems; using Content.Server.Interfaces.GameObjects; using Robust.Shared.Containers; using Robust.Shared.GameObjects; @@ -87,7 +88,14 @@ namespace Content.Server.GameObjects.Components.Construction foreach (var (stackType, amount) in machineBoard.MaterialRequirements) { - var s = StackHelpers.SpawnStack(stackType, amount, Owner.Transform.Coordinates); + var stackSpawn = new StackTypeSpawnEvent() + {Amount = amount, StackType = stackType, SpawnPosition = Owner.Transform.Coordinates}; + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, stackSpawn); + + var s = stackSpawn.Result; + + if (s == null) + throw new Exception($"Couldn't spawn stack of type {stackType}!"); if (!partContainer.Insert(s)) throw new Exception($"Couldn't insert machine material of type {stackType} to machine with prototype {Owner.Prototype?.ID ?? "N/A"}"); diff --git a/Content.Server/GameObjects/Components/Construction/MachineFrameComponent.cs b/Content.Server/GameObjects/Components/Construction/MachineFrameComponent.cs index 22d14bea27..fcd86d3fb3 100644 --- a/Content.Server/GameObjects/Components/Construction/MachineFrameComponent.cs +++ b/Content.Server/GameObjects/Components/Construction/MachineFrameComponent.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Content.Server.Construction; using Content.Server.GameObjects.Components.Stack; +using Content.Server.GameObjects.EntitySystems; using Content.Shared.GameObjects.Components.Construction; using Content.Shared.GameObjects.Components.Tag; using Content.Shared.Interfaces.GameObjects.Components; @@ -314,10 +315,14 @@ namespace Content.Server.GameObjects.Components.Construction return true; } - if (!stack.Split(needed, Owner.Transform.Coordinates, out var newStack)) + var splitStack = new StackSplitEvent() + {Amount = needed, SpawnPosition = Owner.Transform.Coordinates}; + Owner.EntityManager.EventBus.RaiseLocalEvent(stack.Owner.Uid, splitStack); + + if (splitStack.Result == null) return false; - if(!_partContainer.Insert(newStack)) + if(!_partContainer.Insert(splitStack.Result)) return false; _materialProgress[type] += needed; diff --git a/Content.Server/GameObjects/Components/Items/FloorTileItemComponent.cs b/Content.Server/GameObjects/Components/Items/FloorTileItemComponent.cs index a31798fd7d..975d814a21 100644 --- a/Content.Server/GameObjects/Components/Items/FloorTileItemComponent.cs +++ b/Content.Server/GameObjects/Components/Items/FloorTileItemComponent.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Content.Server.GameObjects.Components.Stack; +using Content.Server.GameObjects.EntitySystems; using Content.Shared.Audio; using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Maps; @@ -78,8 +79,14 @@ namespace Content.Server.GameObjects.Components.Items var tile = mapGrid.GetTileRef(location); var baseTurf = (ContentTileDefinition) _tileDefinitionManager[tile.Tile.TypeId]; - if (HasBaseTurf(currentTileDefinition, baseTurf.Name) && stack.Use(1)) + if (HasBaseTurf(currentTileDefinition, baseTurf.Name)) { + var stackUse = new StackUseEvent() {Amount = 1}; + Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, stackUse); + + if (!stackUse.Result) + continue; + PlaceAt(mapGrid, location, currentTileDefinition.TileId); break; } diff --git a/Content.Server/GameObjects/Components/Medical/HealingComponent.cs b/Content.Server/GameObjects/Components/Medical/HealingComponent.cs index 83c212afbb..1df64437ec 100644 --- a/Content.Server/GameObjects/Components/Medical/HealingComponent.cs +++ b/Content.Server/GameObjects/Components/Medical/HealingComponent.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Content.Server.GameObjects.Components.Stack; +using Content.Server.GameObjects.EntitySystems; using Content.Shared.Damage; using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.EntitySystems.ActionBlocker; @@ -41,10 +42,13 @@ namespace Content.Server.GameObjects.Components.Medical return true; } - if (Owner.TryGetComponent(out StackComponent? stack) && - !stack.Use(1)) + if (Owner.HasComponent()) { - return true; + var stackUse = new StackUseEvent() {Amount = 1}; + Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, stackUse); + + if(!stackUse.Result) + return true; } foreach (var (type, amount) in Heal) diff --git a/Content.Server/GameObjects/Components/Power/WirePlacerComponent.cs b/Content.Server/GameObjects/Components/Power/WirePlacerComponent.cs index bfb72c9f66..747c4f9726 100644 --- a/Content.Server/GameObjects/Components/Power/WirePlacerComponent.cs +++ b/Content.Server/GameObjects/Components/Power/WirePlacerComponent.cs @@ -8,6 +8,7 @@ using Robust.Shared.IoC; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; using System.Threading.Tasks; +using Content.Server.GameObjects.EntitySystems; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Map; @@ -49,8 +50,16 @@ namespace Content.Server.GameObjects.Components.Power return true; } } - if (Owner.TryGetComponent(out var stack) && !stack.Use(1)) - return true; + + if (Owner.HasComponent()) + { + var stackUse = new StackUseEvent(){Amount = 1}; + Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, stackUse); + + if(!stackUse.Result) + return true; + } + Owner.EntityManager.SpawnEntity(_wirePrototypeID, grid.GridTileToLocal(snapPos)); return true; } diff --git a/Content.Server/GameObjects/Components/Stack/StackComponent.cs b/Content.Server/GameObjects/Components/Stack/StackComponent.cs index 191994e456..3d68b4c479 100644 --- a/Content.Server/GameObjects/Components/Stack/StackComponent.cs +++ b/Content.Server/GameObjects/Components/Stack/StackComponent.cs @@ -1,14 +1,7 @@ -#nullable enable -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using Content.Shared.GameObjects.Components; using Content.Shared.GameObjects.EntitySystems; -using Content.Shared.Interfaces; -using Content.Shared.Interfaces.GameObjects.Components; using Robust.Shared.GameObjects; using Robust.Shared.Localization; -using Robust.Shared.Map; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; @@ -17,114 +10,10 @@ namespace Content.Server.GameObjects.Components.Stack // TODO: Naming and presentation and such could use some improvement. [RegisterComponent] [ComponentReference(typeof(SharedStackComponent))] - public class StackComponent : SharedStackComponent, IInteractUsing, IExamine + public class StackComponent : SharedStackComponent, IExamine { - private bool _throwIndividually = false; - [ViewVariables(VVAccess.ReadWrite)] - public bool ThrowIndividually - { - get => _throwIndividually; - private set - { - _throwIndividually = value; - Dirty(); - } - } - - public void Add(int amount) - { - Count += amount; - } - - /// - /// Try to use an amount of items on this stack. - /// - /// - /// True if there were enough items to remove, false if not in which case nothing was changed. - public bool Use(int amount) - { - if (Count >= amount) - { - Count -= amount; - return true; - } - return false; - } - - /// - /// Attempts to split this stack in two. - /// - /// amount the new stack will have - /// the position the new stack will spawn at - /// the new stack - /// - public bool Split(int amount, EntityCoordinates spawnPosition, [NotNullWhen(true)] out IEntity? stack) - { - if (Count >= amount) - { - Count -= amount; - - stack = Owner.EntityManager.SpawnEntity(Owner.Prototype?.ID, spawnPosition); - - if (stack.TryGetComponent(out StackComponent? stackComp)) - { - stackComp.Count = amount; - } - - return true; - } - - stack = null; - return false; - } - - async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) - { - if (!eventArgs.Using.TryGetComponent(out var stack)) - return false; - - if (!stack.StackTypeId.Equals(StackTypeId)) - { - return false; - } - - var toTransfer = Math.Min(Count, stack.AvailableSpace); - Count -= toTransfer; - stack.Add(toTransfer); - - var popupPos = eventArgs.ClickLocation; - if (popupPos == EntityCoordinates.Invalid) - { - popupPos = eventArgs.User.Transform.Coordinates; - } - - - if (toTransfer > 0) - { - popupPos.PopupMessage(eventArgs.User, $"+{toTransfer}"); - - if (stack.AvailableSpace == 0) - { - eventArgs.Using.SpawnTimer( - 300, - () => popupPos.PopupMessage( - eventArgs.User, - Loc.GetString("comp-stack-becomes-full") - ) - ); - } - } - else if (toTransfer == 0 && stack.AvailableSpace == 0) - { - popupPos.PopupMessage( - eventArgs.User, - Loc.GetString("comp-stack-already-full") - ); - } - - return true; - } + public bool ThrowIndividually { get; set; } = false; void IExamine.Examine(FormattedMessage message, bool inDetailsRange) { diff --git a/Content.Server/GameObjects/EntitySystems/ConstructionSystem.cs b/Content.Server/GameObjects/EntitySystems/ConstructionSystem.cs index 12711a3305..828396351a 100644 --- a/Content.Server/GameObjects/EntitySystems/ConstructionSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/ConstructionSystem.cs @@ -163,20 +163,22 @@ namespace Content.Server.GameObjects.EntitySystems case MaterialConstructionGraphStep materialStep: foreach (var entity in EnumerateNearby(user)) { - if (!materialStep.EntityValid(entity, out var sharedStack)) + if (!materialStep.EntityValid(entity, out var stack)) continue; - var stack = (StackComponent) sharedStack; + var splitStack = new StackSplitEvent() + {Amount = materialStep.Amount, SpawnPosition = user.ToCoordinates()}; + RaiseLocalEvent(entity.Uid, splitStack); - if (!stack.Split(materialStep.Amount, user.ToCoordinates(), out var newStack)) + if (splitStack.Result == null) continue; if (string.IsNullOrEmpty(materialStep.Store)) { - if (!container.Insert(newStack)) + if (!container.Insert(splitStack.Result)) continue; } - else if (!GetContainer(materialStep.Store).Insert(newStack)) + else if (!GetContainer(materialStep.Store).Insert(splitStack.Result)) continue; handled = true; @@ -230,7 +232,7 @@ namespace Content.Server.GameObjects.EntitySystems BreakOnStun = true, BreakOnTargetMove = false, BreakOnUserMove = true, - NeedHand = true, + NeedHand = false, }; if (await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Cancelled) diff --git a/Content.Server/GameObjects/EntitySystems/HandsSystem.cs b/Content.Server/GameObjects/EntitySystems/HandsSystem.cs index 9d666fb05c..f694b26320 100644 --- a/Content.Server/GameObjects/EntitySystems/HandsSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/HandsSystem.cs @@ -180,12 +180,13 @@ namespace Content.Server.GameObjects.EntitySystems } else { - stackComp.Use(1); - throwEnt = throwEnt.EntityManager.SpawnEntity(throwEnt.Prototype?.ID, playerEnt.Transform.Coordinates); + var splitStack = new StackSplitEvent() {Amount = 1, SpawnPosition = playerEnt.Transform.Coordinates}; + RaiseLocalEvent(throwEnt.Uid, splitStack); - // can only throw one item at a time, regardless of what the prototype stack size is. - if (throwEnt.TryGetComponent(out var newStackComp)) - newStackComp.Count = 1; + if (splitStack.Result == null) + return false; + + throwEnt = splitStack.Result; } var direction = coords.ToMapPos(EntityManager) - playerEnt.Transform.WorldPosition; diff --git a/Content.Server/GameObjects/EntitySystems/SpawnAfterInteractSystem.cs b/Content.Server/GameObjects/EntitySystems/SpawnAfterInteractSystem.cs index 2a8296685d..8611dcac2c 100644 --- a/Content.Server/GameObjects/EntitySystems/SpawnAfterInteractSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/SpawnAfterInteractSystem.cs @@ -65,13 +65,20 @@ namespace Content.Server.GameObjects.EntitySystems if (component.Deleted || component.Owner.Deleted) return; - StackComponent? stack = null; - if (component.RemoveOnInteract && component.Owner.TryGetComponent(out stack) && !stack.Use(1)) - return; + var hasStack = component.Owner.HasComponent(); + + if (hasStack && component.RemoveOnInteract) + { + var stackUse = new StackUseEvent() {Amount = 1}; + RaiseLocalEvent(component.Owner.Uid, stackUse); + + if (!stackUse.Result) + return; + } EntityManager.SpawnEntity(component.Prototype, args.ClickLocation.SnapToGrid(grid)); - if (component.RemoveOnInteract && stack == null && !component.Owner.Deleted) + if (component.RemoveOnInteract && !hasStack && !component.Owner.Deleted) component.Owner.Delete(); } } diff --git a/Content.Server/GameObjects/EntitySystems/StackSystem.cs b/Content.Server/GameObjects/EntitySystems/StackSystem.cs new file mode 100644 index 0000000000..e4960a78a6 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/StackSystem.cs @@ -0,0 +1,256 @@ +using System; +using Content.Server.GameObjects.Components.Stack; +using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Content.Shared.Stacks; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameObjects.EntitySystems +{ + /// + /// Entity system that handles everything relating to stacks. + /// This is a good example for learning how to code in an ECS manner. + /// + [UsedImplicitly] + public class StackSystem : SharedStackSystem + { + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStackInteractUsing); + + // The following subscriptions are basically the "method calls" of this entity system. + SubscribeLocalEvent(OnStackUse); + SubscribeLocalEvent(OnStackSplit); + SubscribeLocalEvent(OnStackTypeSpawn); + } + + /// + /// Try to use an amount of items on this stack. + /// See + /// + private void OnStackUse(EntityUid uid, StackComponent stack, StackUseEvent args) + { + // Check if we have enough things in the stack for this... + if (stack.Count < args.Amount) + { + // Not enough things in the stack, so we set the output result to false. + args.Result = false; + } + else + { + // We do have enough things in the stack, so remove them and set the output result to true. + RaiseLocalEvent(uid, new StackChangeCountEvent(stack.Count - args.Amount)); + args.Result = true; + } + } + + /// + /// Try to split this stack into two. + /// See + /// + private void OnStackSplit(EntityUid uid, StackComponent stack, StackSplitEvent args) + { + // If the stack doesn't have enough things as specified in the parameters, we do nothing. + if (stack.Count < args.Amount) + return; + + // Get a prototype ID to spawn the new entity. Null is also valid, although it should rarely be picked... + var prototype = _prototypeManager.TryIndex(stack.StackTypeId, out var stackType) + ? stackType.Spawn + : stack.Owner.Prototype?.ID ?? null; + + // Remove the amount of things we want to split from the original stack... + RaiseLocalEvent(uid, new StackChangeCountEvent(stack.Count - args.Amount)); + + // Set the output parameter in the event instance to the newly split stack. + args.Result = EntityManager.SpawnEntity(prototype, args.SpawnPosition); + + if (args.Result.TryGetComponent(out StackComponent? stackComp)) + { + // Set the split stack's count. + RaiseLocalEvent(args.Result.Uid, new StackChangeCountEvent(args.Amount)); + } + } + + /// + /// Tries to spawn a stack of a certain type. + /// See + /// + private void OnStackTypeSpawn(StackTypeSpawnEvent args) + { + // Can't spawn a stack for an invalid type. + if (args.StackType == null) + return; + + // Set the output result parameter to the new stack entity... + args.Result = EntityManager.SpawnEntity(args.StackType.Spawn, args.SpawnPosition); + var stack = args.Result.GetComponent(); + + // And finally, set the correct amount! + RaiseLocalEvent(args.Result.Uid, new StackChangeCountEvent(args.Amount)); + } + + private void OnStackInteractUsing(EntityUid uid, StackComponent stack, InteractUsingEvent args) + { + if (!args.Used.TryGetComponent(out var otherStack)) + return; + + if (!otherStack.StackTypeId.Equals(stack.StackTypeId)) + return; + + var toTransfer = Math.Min(stack.Count, otherStack.AvailableSpace); + RaiseLocalEvent(uid, new StackChangeCountEvent(stack.Count - toTransfer)); + RaiseLocalEvent(args.Used.Uid, new StackChangeCountEvent(otherStack.Count + toTransfer)); + + var popupPos = args.ClickLocation; + if (!popupPos.IsValid(EntityManager)) + { + popupPos = args.User.Transform.Coordinates; + } + + switch (toTransfer) + { + case > 0: + popupPos.PopupMessage(args.User, $"+{toTransfer}"); + + if (otherStack.AvailableSpace == 0) + { + args.Used.SpawnTimer( + 300, + () => popupPos.PopupMessage( + args.User, + Loc.GetString("comp-stack-becomes-full") + ) + ); + } + + break; + + case 0 when otherStack.AvailableSpace == 0: + popupPos.PopupMessage( + args.User, + Loc.GetString("comp-stack-already-full") + ); + break; + } + + args.Handled = true; + } + } + + /* + * The following events are actually funny ECS method calls! + * + * Instead of coupling systems together into a ball of spaghetti, + * we raise events that act as method calls. + * + * So for example, instead of having an Use() method in the + * stack component or stack system, we have a StackUseEvent. + * Before raising the event, you would set the Amount property, + * which acts as a parameter or argument, and afterwards the + * entity system in charge of handling this would perform the logic + * and then set the Result on the event instance. + * Then you can access this property to see whether your Use attempt succeeded. + * + * This is very powerful, as it completely removes the coupling + * between entity systems and allows for greater flexibility. + * If you want to intercept this event with another entity system, you can. + * And you don't have to write any bad, hacky code for this! + * You could even use handled events, or cancellable events... + * The possibilities are endless. + * + * Of course, not everything needs to be directed events! + * Broadcast events also work in the same way. + * For example, we use a broadcast event to spawn a stack of a certain type. + * + * Wrapping your head around this may be difficult at first, + * but soon you'll get it, coder. Soon you'll grasp the wisdom. + * Go forth and write some beautiful and robust code! + */ + + /// + /// Uses an amount of things from a stack. + /// Whether this succeeded is stored in . + /// + public class StackUseEvent : EntityEventArgs + { + /// + /// The amount of things to use on the stack. + /// Consider this the equivalent of a parameter for a method call. + /// + public int Amount { get; init; } + + /// + /// Whether the action succeeded or not. + /// Set by the after handling this event. + /// Consider this the equivalent of a return value for a method call. + /// + public bool Result { get; set; } = false; + } + + /// + /// Tries to split a stack into two. + /// If this succeeds, will be the new stack. + /// + public class StackSplitEvent : EntityEventArgs + { + /// + /// The amount of things to take from the original stack. + /// Input parameter. + /// + public int Amount { get; init; } + + /// + /// The position where to spawn the new stack. + /// Input parameter. + /// + public EntityCoordinates SpawnPosition { get; init; } + + /// + /// The newly split stack. May be null if the split failed. + /// Output parameter. + /// + public IEntity? Result { get; set; } = null; + } + + /// + /// Tries to spawn a stack of a certain type. + /// If this succeeds, will be the new stack. + /// + public class StackTypeSpawnEvent : EntityEventArgs + { + /// + /// The amount of things the spawned stack will have. + /// Input parameter. + /// + public int Amount { get; init; } + + /// + /// The stack type to be spawned. + /// Input parameter. + /// + public StackPrototype? StackType { get; init; } + + /// + /// The position where the new stack will be spawned. + /// Input parameter. + /// + public EntityCoordinates SpawnPosition { get; init; } + + /// + /// The newly spawned stack, or null if this failed. + /// Output parameter. + /// + public IEntity? Result { get; set; } = null; + } +} diff --git a/Content.Shared/Content.Shared.csproj b/Content.Shared/Content.Shared.csproj index c1492422ce..e7f2f39d3e 100644 --- a/Content.Shared/Content.Shared.csproj +++ b/Content.Shared/Content.Shared.csproj @@ -30,7 +30,6 @@ - diff --git a/Content.Shared/GameObjects/Components/SharedStackComponent.cs b/Content.Shared/GameObjects/Components/SharedStackComponent.cs index 66dc585a88..4c263bd894 100644 --- a/Content.Shared/GameObjects/Components/SharedStackComponent.cs +++ b/Content.Shared/GameObjects/Components/SharedStackComponent.cs @@ -1,46 +1,34 @@ using System; +using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Stacks; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Log; using Robust.Shared.Players; -using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.ViewVariables; namespace Content.Shared.GameObjects.Components { public abstract class SharedStackComponent : Component, ISerializationHooks { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - - private const string SerializationCache = "stack"; - public sealed override string Name => "Stack"; public sealed override uint? NetID => ContentNetIDs.STACK; - [DataField("count")] - private int _count = 30; - [DataField("max")] private int _maxCount = 30; [ViewVariables(VVAccess.ReadWrite)] - public virtual int Count - { - get => _count; - set - { - _count = value; - if (_count <= 0) - { - Owner.Delete(); - } + [DataField("stackType", required:true, customTypeSerializer:typeof(PrototypeIdSerializer))] + public string StackTypeId { get; private set; } = string.Empty; - Dirty(); - } - } + /// + /// Current stack count. + /// Do NOT set this directly, raise the event instead. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("count")] + public int Count { get; set; } = 30; [ViewVariables] public int MaxCount @@ -48,28 +36,16 @@ namespace Content.Shared.GameObjects.Components get => _maxCount; private set { + if (_maxCount == value) + return; + _maxCount = value; Dirty(); } } - [ViewVariables] public int AvailableSpace => MaxCount - Count; - [ViewVariables] - [DataField("stackType")] - public string StackTypeId { get; } = string.Empty; - - public StackPrototype StackType => _prototypeManager.Index(StackTypeId); - - protected override void Startup() - { - base.Startup(); - - if (StackTypeId != string.Empty && !_prototypeManager.HasIndex(StackTypeId)) - { - Logger.Error($"No {nameof(StackPrototype)} found with id {StackTypeId} for {Owner.Prototype?.ID ?? Owner.Name}"); - } - } + public int AvailableSpace => MaxCount - Count; public override ComponentState GetComponentState(ICommonSession player) { @@ -79,11 +55,10 @@ namespace Content.Shared.GameObjects.Components public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) { if (curState is not StackComponentState cast) - { return; - } - Count = cast.Count; + // This will change the count and call events. + Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new StackChangeCountEvent(cast.Count)); MaxCount = cast.MaxCount; } diff --git a/Content.Shared/GameObjects/EntitySystems/SharedStackSystem.cs b/Content.Shared/GameObjects/EntitySystems/SharedStackSystem.cs new file mode 100644 index 0000000000..df054b0fc6 --- /dev/null +++ b/Content.Shared/GameObjects/EntitySystems/SharedStackSystem.cs @@ -0,0 +1,108 @@ +using Content.Shared.GameObjects.Components; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; + +namespace Content.Shared.GameObjects.EntitySystems +{ + [UsedImplicitly] + public abstract class SharedStackSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStackStarted); + SubscribeLocalEvent(OnStackCountChange); + } + + private void OnStackStarted(EntityUid uid, SharedStackComponent component, ComponentStartup args) + { + if (!ComponentManager.TryGetComponent(uid, out SharedAppearanceComponent? appearance)) + return; + + appearance.SetData(StackVisuals.MaxCount, component.MaxCount); + appearance.SetData(StackVisuals.Hide, false); + } + + protected void OnStackCountChange(EntityUid uid, SharedStackComponent component, StackChangeCountEvent args) + { + if (args.Amount == component.Count) + return; + + var old = component.Count; + + if (args.Amount > component.MaxCount) + { + args.Amount = component.MaxCount; + args.Clamped = true; + } + + if (args.Amount < 0) + { + args.Amount = 0; + args.Clamped = true; + } + + component.Count = args.Amount; + component.Dirty(); + + // Queue delete stack if count reaches zero. + if(component.Count <= 0) + EntityManager.QueueDeleteEntity(uid); + + // Change appearance data. + if (ComponentManager.TryGetComponent(uid, out SharedAppearanceComponent? appearance)) + appearance.SetData(StackVisuals.Actual, component.Count); + + RaiseLocalEvent(uid, new StackCountChangedEvent(old, component.Count)); + } + } + + /// + /// Attempts to change the amount of things in a stack to a specific number. + /// If the amount had to be clamped to zero or the max amount, will be true + /// and the amount will be changed to match the value set. + /// Does nothing if the amount is the same as the stack count already. + /// + public class StackChangeCountEvent : EntityEventArgs + { + /// + /// Amount to set the stack to. + /// Input/Output parameter. + /// + public int Amount { get; set; } + + /// + /// Whether the had to be clamped. + /// Output parameter. + /// + public bool Clamped { get; set; } + + public StackChangeCountEvent(int amount) + { + Amount = amount; + } + } + + /// + /// Event raised when a stack's count has changed. + /// + public class StackCountChangedEvent : EntityEventArgs + { + /// + /// The old stack count. + /// + public int OldCount { get; } + + /// + /// The new stack count. + /// + public int NewCount { get; } + + public StackCountChangedEvent(int oldCount, int newCount) + { + OldCount = oldCount; + NewCount = newCount; + } + } +} diff --git a/Content.Shared/Stacks/StackPrototype.cs b/Content.Shared/Stacks/StackPrototype.cs index d1e7a7454b..67e700e406 100644 --- a/Content.Shared/Stacks/StackPrototype.cs +++ b/Content.Shared/Stacks/StackPrototype.cs @@ -17,12 +17,12 @@ namespace Content.Shared.Stacks public string Name { get; } = string.Empty; [DataField("icon")] - public SpriteSpecifier? Icon { get; } + public SpriteSpecifier? Icon { get; } = null; /// /// The entity id that will be spawned by default from this stack. /// - [DataField("spawn")] - public string? Spawn { get; } + [DataField("spawn", required: true)] + public string Spawn { get; } = string.Empty; } }