From 54986e03474353e38658d310e9495610000a6f0a Mon Sep 17 00:00:00 2001 From: Winkarst-cpu <74284083+Winkarst-cpu@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:27:24 +0000 Subject: [PATCH] Prediction + small cleanup: GatherableSystem (#44840) * Predict * Reuse pos * Update Content.Shared/Gatherable/GatherableSystem.cs Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com> * Update Content.Shared/Gatherable/GatherableSystem.cs Co-authored-by: Connor Huffine * Review * refactor: GatherableSystem xml-doc --------- Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com> Co-authored-by: Connor Huffine Co-authored-by: Fildrance --- .../Components/GatherableComponent.cs | 40 ------ .../Components/SoundOnGatherComponent.cs | 19 --- .../Gatherable/GatherableSystem.Projectile.cs | 30 ----- Content.Server/Gatherable/GatherableSystem.cs | 87 ------------- .../Components/GatherableComponent.cs | 44 +++++++ .../GatheringProjectileComponent.cs | 9 +- .../Components/SoundOnGatherComponent.cs | 21 ++++ Content.Shared/Gatherable/GatherableSystem.cs | 115 ++++++++++++++++++ 8 files changed, 186 insertions(+), 179 deletions(-) delete mode 100644 Content.Server/Gatherable/Components/GatherableComponent.cs delete mode 100644 Content.Server/Gatherable/Components/SoundOnGatherComponent.cs delete mode 100644 Content.Server/Gatherable/GatherableSystem.Projectile.cs delete mode 100644 Content.Server/Gatherable/GatherableSystem.cs create mode 100644 Content.Shared/Gatherable/Components/GatherableComponent.cs rename {Content.Server => Content.Shared}/Gatherable/Components/GatheringProjectileComponent.cs (54%) create mode 100644 Content.Shared/Gatherable/Components/SoundOnGatherComponent.cs create mode 100644 Content.Shared/Gatherable/GatherableSystem.cs diff --git a/Content.Server/Gatherable/Components/GatherableComponent.cs b/Content.Server/Gatherable/Components/GatherableComponent.cs deleted file mode 100644 index 8eb593fd13..0000000000 --- a/Content.Server/Gatherable/Components/GatherableComponent.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Content.Shared.EntityTable.EntitySelectors; -using Content.Shared.Whitelist; - -namespace Content.Server.Gatherable.Components; - -[RegisterComponent] -[Access(typeof(GatherableSystem))] -public sealed partial class GatherableComponent : Component -{ - /// - /// Whitelist for specifying the kind of tools can be used on a resource - /// Supports multiple tags. - /// - [DataField(required: true)] - public EntityWhitelist? ToolWhitelist; - - /// - /// YAML example below - /// (Tag1, Tag2, LootTableID1, LootTableID2 are placeholders for example) - /// -------------------- - /// useMappedLoot: true - /// toolWhitelist: - /// tags: - /// - Tag1 - /// - Tag2 - /// loot: - /// Tag1: !type:NestedSelector - /// tableId: LootTableID1 - /// Tag2: !type:NestedSelector - /// tableId: LootTableID2 - /// - [DataField] - public Dictionary? Loot = new(); - - /// - /// Random shift of the appearing entity during gathering - /// - [DataField] - public float GatherOffset = 0.3f; -} diff --git a/Content.Server/Gatherable/Components/SoundOnGatherComponent.cs b/Content.Server/Gatherable/Components/SoundOnGatherComponent.cs deleted file mode 100644 index f46eb281f8..0000000000 --- a/Content.Server/Gatherable/Components/SoundOnGatherComponent.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Shared.Audio; -using Robust.Shared.Audio; - -namespace Content.Server.Gatherable.Components; - -/// -/// Plays the specified sound when this entity is gathered. -/// -[RegisterComponent, Access(typeof(GatherableSystem))] -public sealed partial class SoundOnGatherComponent : Component -{ - [ViewVariables(VVAccess.ReadWrite), DataField("sound")] - public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/break_stone.ogg") - { - Params = AudioParams.Default - .WithVariation(SharedContentAudioSystem.DefaultVariation) - .WithVolume(-3f), - }; -} diff --git a/Content.Server/Gatherable/GatherableSystem.Projectile.cs b/Content.Server/Gatherable/GatherableSystem.Projectile.cs deleted file mode 100644 index 3ab8872fd7..0000000000 --- a/Content.Server/Gatherable/GatherableSystem.Projectile.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Content.Server.Gatherable.Components; -using Content.Shared.Projectiles; -using Robust.Shared.Physics.Events; - -namespace Content.Server.Gatherable; - -public sealed partial class GatherableSystem -{ - private void InitializeProjectile() - { - SubscribeLocalEvent(OnProjectileCollide); - } - - private void OnProjectileCollide(Entity gathering, ref StartCollideEvent args) - { - if (!args.OtherFixture.Hard || - args.OurFixtureId != SharedProjectileSystem.ProjectileFixture || - gathering.Comp.Amount <= 0 || - !TryComp(args.OtherEntity, out var gatherable)) - { - return; - } - - Gather(args.OtherEntity, gathering, gatherable); - gathering.Comp.Amount--; - - if (gathering.Comp.Amount <= 0) - QueueDel(gathering); - } -} diff --git a/Content.Server/Gatherable/GatherableSystem.cs b/Content.Server/Gatherable/GatherableSystem.cs deleted file mode 100644 index 3ecfd1d45a..0000000000 --- a/Content.Server/Gatherable/GatherableSystem.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Content.Server.Destructible; -using Content.Server.Gatherable.Components; -using Content.Shared.EntityTable; -using Content.Shared.Interaction; -using Content.Shared.Tag; -using Content.Shared.Weapons.Melee.Events; -using Content.Shared.Whitelist; -using Robust.Server.GameObjects; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Random; - -namespace Content.Server.Gatherable; - -public sealed partial class GatherableSystem : EntitySystem -{ - [Dependency] private IRobustRandom _random = default!; - [Dependency] private DestructibleSystem _destructible = default!; - [Dependency] private SharedAudioSystem _audio = default!; - [Dependency] private TagSystem _tagSystem = default!; - [Dependency] private TransformSystem _transform = default!; - [Dependency] private EntityWhitelistSystem _whitelistSystem = default!; - [Dependency] private EntityTableSystem _entityTable = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnActivate); - SubscribeLocalEvent(OnAttacked); - InitializeProjectile(); - } - - private void OnAttacked(Entity gatherable, ref AttackedEvent args) - { - if (_whitelistSystem.IsWhitelistFailOrNull(gatherable.Comp.ToolWhitelist, args.Used)) - return; - - Gather(gatherable, args.User); - } - - private void OnActivate(Entity gatherable, ref ActivateInWorldEvent args) - { - if (args.Handled || !args.Complex) - return; - - if (_whitelistSystem.IsWhitelistFailOrNull(gatherable.Comp.ToolWhitelist, args.User)) - return; - - Gather(gatherable, args.User); - args.Handled = true; - } - - public void Gather(EntityUid gatheredUid, EntityUid? gatherer = null, GatherableComponent? component = null) - { - if (!Resolve(gatheredUid, ref component)) - return; - - if (TryComp(gatheredUid, out var soundComp)) - { - _audio.PlayPvs(soundComp.Sound, Transform(gatheredUid).Coordinates); - } - - // Complete the gathering process - _destructible.DestroyEntity(gatheredUid); - - // Spawn the loot! - if (component.Loot == null) - return; - - var pos = _transform.GetMapCoordinates(gatheredUid); - - foreach (var (tag, table) in component.Loot) - { - if (tag != "All") - { - if (gatherer != null && !_tagSystem.HasTag(gatherer.Value, tag)) - continue; - } - var spawnLoot = _entityTable.GetSpawns(table); - foreach (var loot in spawnLoot) - { - var spawnPos = pos.Offset(_random.NextVector2(component.GatherOffset)); - Spawn(loot, spawnPos); - } - } - } -} diff --git a/Content.Shared/Gatherable/Components/GatherableComponent.cs b/Content.Shared/Gatherable/Components/GatherableComponent.cs new file mode 100644 index 0000000000..de45dc27ee --- /dev/null +++ b/Content.Shared/Gatherable/Components/GatherableComponent.cs @@ -0,0 +1,44 @@ +using Content.Shared.EntityTable.EntitySelectors; +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; + +namespace Content.Shared.Gatherable.Components; + +/// +/// Makes the entity possible to gather. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(GatherableSystem))] +public sealed partial class GatherableComponent : Component +{ + /// + /// Whitelist for specifying the kind of tools can be used on a resource. + /// Supports multiple tags. + /// + [DataField(required: true)] + public EntityWhitelist? ToolWhitelist; + + /// + /// YAML example below + /// (Tag1, Tag2, LootTableID1, LootTableID2 are placeholders for example) + /// -------------------- + /// useMappedLoot: true + /// toolWhitelist: + /// tags: + /// - Tag1 + /// - Tag2 + /// loot: + /// Tag1: !type:NestedSelector + /// tableId: LootTableID1 + /// Tag2: !type:NestedSelector + /// tableId: LootTableID2 + /// + [DataField] + public Dictionary? Loot = []; + + /// + /// Random shift of the appearing entity during gathering. + /// + [DataField] + public float GatherOffset = 0.3f; +} diff --git a/Content.Server/Gatherable/Components/GatheringProjectileComponent.cs b/Content.Shared/Gatherable/Components/GatheringProjectileComponent.cs similarity index 54% rename from Content.Server/Gatherable/Components/GatheringProjectileComponent.cs rename to Content.Shared/Gatherable/Components/GatheringProjectileComponent.cs index 2ac8eedcd1..a908524ae0 100644 --- a/Content.Server/Gatherable/Components/GatheringProjectileComponent.cs +++ b/Content.Shared/Gatherable/Components/GatheringProjectileComponent.cs @@ -1,14 +1,17 @@ -namespace Content.Server.Gatherable.Components; +using Robust.Shared.GameStates; + +namespace Content.Shared.Gatherable.Components; /// /// Destroys a gatherable entity when colliding with it. /// -[RegisterComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(GatherableSystem))] public sealed partial class GatheringProjectileComponent : Component { /// /// How many more times we can gather. /// - [ViewVariables(VVAccess.ReadWrite), DataField("amount")] + [DataField, AutoNetworkedField] public int Amount = 1; } diff --git a/Content.Shared/Gatherable/Components/SoundOnGatherComponent.cs b/Content.Shared/Gatherable/Components/SoundOnGatherComponent.cs new file mode 100644 index 0000000000..9dc706ab18 --- /dev/null +++ b/Content.Shared/Gatherable/Components/SoundOnGatherComponent.cs @@ -0,0 +1,21 @@ +using Content.Shared.Audio; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.Gatherable.Components; + +/// +/// Plays the specified sound when this entity is gathered. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(GatherableSystem))] +public sealed partial class SoundOnGatherComponent : Component +{ + /// + /// Sound to play when this entity is gathered. + /// + [DataField] + public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/break_stone.ogg", AudioParams.Default + .WithVariation(SharedContentAudioSystem.DefaultVariation) + .WithVolume(-3f)); +} diff --git a/Content.Shared/Gatherable/GatherableSystem.cs b/Content.Shared/Gatherable/GatherableSystem.cs new file mode 100644 index 0000000000..8c944cc8dc --- /dev/null +++ b/Content.Shared/Gatherable/GatherableSystem.cs @@ -0,0 +1,115 @@ +using Content.Shared.Destructible; +using Content.Shared.EntityTable; +using Content.Shared.Gatherable.Components; +using Content.Shared.Interaction; +using Content.Shared.Projectiles; +using Content.Shared.Random.Helpers; +using Content.Shared.Tag; +using Content.Shared.Weapons.Melee.Events; +using Content.Shared.Whitelist; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Physics.Events; +using Robust.Shared.Timing; + +namespace Content.Shared.Gatherable; + +/// +/// System for handling collecting items from in-the-world structures, +/// like gathering mushrooms or some rare herbs. +/// +public sealed partial class GatherableSystem : EntitySystem +{ + [Dependency] private SharedAudioSystem _audio = default!; + [Dependency] private SharedDestructibleSystem _destructible = default!; + [Dependency] private EntityTableSystem _entityTable = default!; + [Dependency] private TagSystem _tagSystem = default!; + [Dependency] private IGameTiming _timing = default!; + [Dependency] private EntityWhitelistSystem _whitelistSystem = default!; + + /// + /// If a gatherable entity is attacked with a tool that passes the tool entity whitelist (or if that whitelist doesn't exist), then it will be gathered. + /// + [SubscribeLocalEvent] + private void OnAttacked(Entity gatherable, ref AttackedEvent args) + { + if (_whitelistSystem.IsWhitelistFailOrNull(gatherable.Comp.ToolWhitelist, args.Used)) + return; + + Gather(gatherable.AsNullable(), args.User); + } + + /// + /// If a gatherable entity is activated by a user that passes the tool entity whitelist (or if that whitelist doesn't exist), then it will be gathered. + /// + [SubscribeLocalEvent] + private void OnActivate(Entity gatherable, ref ActivateInWorldEvent args) + { + if (args.Handled || !args.Complex) + return; + + if (_whitelistSystem.IsWhitelistFailOrNull(gatherable.Comp.ToolWhitelist, args.User)) + return; + + Gather(gatherable.AsNullable(), args.User); + args.Handled = true; + } + + /// + /// If a projectile hits a gatherable entity, it will gather that entity and decrease its own gathering amount. + /// If the projectile’s gathering amount is zero or less, it will be deleted. + /// + [SubscribeLocalEvent] + private void OnProjectileCollide(Entity gathering, ref StartCollideEvent args) + { + if (!args.OtherFixture.Hard) + return; + + if (args.OurFixtureId != SharedProjectileSystem.ProjectileFixture) + return; + + if (gathering.Comp.Amount <= 0) + return; + + if (!TryComp(args.OtherEntity, out var gatherable)) + return; + + Gather((args.OtherEntity, gatherable), gathering); + gathering.Comp.Amount--; + Dirty(gathering); + + if (gathering.Comp.Amount <= 0) + PredictedQueueDel(gathering); + } + + /// + /// Destroys the gathered entity, plays a sound if it has , and spawns loot if possible. + /// + /// The entity that was gathered. + /// The entity that gathered it. + public void Gather(Entity gathered, EntityUid? gatherer = null) + { + if (!Resolve(gathered, ref gathered.Comp)) + return; + + var pos = Transform(gathered).Coordinates; + if (TryComp(gathered, out var soundComp)) + _audio.PlayPredicted(soundComp.Sound, pos, gatherer); + + _destructible.DestroyEntity(gathered); + + if (gathered.Comp.Loot is null) + return; + + foreach (var (tag, table) in gathered.Comp.Loot) + { + if (tag != "All" && gatherer != null && !_tagSystem.HasTag(gatherer.Value, tag)) + continue; + + var random = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(gathered)); + foreach (var loot in _entityTable.GetSpawns(table, random)) + { + PredictedSpawnAtPosition(loot, pos.Offset(random.NextVector2(gathered.Comp.GatherOffset))); + } + } + } +}