From b9ffd060d6a4f2b09f99afec1b16170f785952b4 Mon Sep 17 00:00:00 2001 From: Kyle Tyo <36606155+VerinSenpai@users.noreply.github.com> Date: Tue, 15 Jul 2025 20:21:18 -0400 Subject: [PATCH] Predict DevourSystem. (#38970) Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- Content.Client/Devour/DevourSystem.cs | 6 - Content.Server/Devour/DevourSystem.cs | 62 -------- .../Devour/Components/DevourerComponent.cs | 82 ++++++---- Content.Shared/Devour/DevourSystem.cs | 145 ++++++++++++++++++ Content.Shared/Devour/SharedDevourSystem.cs | 89 ----------- 5 files changed, 199 insertions(+), 185 deletions(-) delete mode 100644 Content.Client/Devour/DevourSystem.cs delete mode 100644 Content.Server/Devour/DevourSystem.cs create mode 100644 Content.Shared/Devour/DevourSystem.cs delete mode 100644 Content.Shared/Devour/SharedDevourSystem.cs diff --git a/Content.Client/Devour/DevourSystem.cs b/Content.Client/Devour/DevourSystem.cs deleted file mode 100644 index ad905ffab3..0000000000 --- a/Content.Client/Devour/DevourSystem.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Content.Shared.Devour; - -namespace Content.Client.Devour; -public sealed class DevourSystem : SharedDevourSystem -{ -} diff --git a/Content.Server/Devour/DevourSystem.cs b/Content.Server/Devour/DevourSystem.cs deleted file mode 100644 index ede13fa817..0000000000 --- a/Content.Server/Devour/DevourSystem.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Content.Server.Body.Systems; -using Content.Shared.Body.Events; -using Content.Shared.Chemistry.Components; -using Content.Shared.Devour; -using Content.Shared.Devour.Components; -using Content.Shared.Whitelist; - -namespace Content.Server.Devour; - -public sealed class DevourSystem : SharedDevourSystem -{ - [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!; - [Dependency] private readonly EntityWhitelistSystem _entityWhitelistSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(OnGibContents); - } - - private void OnDoAfter(EntityUid uid, DevourerComponent component, DevourDoAfterEvent args) - { - if (args.Handled || args.Cancelled) - return; - - var ichorInjection = new Solution(component.Chemical, component.HealRate); - - // Grant ichor if the devoured thing meets the dragon's food preference - if (args.Args.Target != null && _entityWhitelistSystem.IsWhitelistPassOrNull(component.FoodPreferenceWhitelist, (EntityUid)args.Args.Target)) - { - _bloodstreamSystem.TryAddToChemicals(uid, ichorInjection); - } - - // If the devoured thing meets the stomach whitelist criteria, add it to the stomach - if (args.Args.Target != null && _entityWhitelistSystem.IsWhitelistPass(component.StomachStorageWhitelist, (EntityUid)args.Args.Target)) - { - ContainerSystem.Insert(args.Args.Target.Value, component.Stomach); - } - //TODO: Figure out a better way of removing structures via devour that still entails standing still and waiting for a DoAfter. Somehow. - //If it's not alive, it must be a structure. - // Delete if the thing isn't in the stomach storage whitelist (or the stomach whitelist is null/empty) - else if (args.Args.Target != null) - { - QueueDel(args.Args.Target.Value); - } - - _audioSystem.PlayPvs(component.SoundDevour, uid); - } - - private void OnGibContents(EntityUid uid, DevourerComponent component, ref BeingGibbedEvent args) - { - if (component.StomachStorageWhitelist == null) - return; - - // For some reason we have two different systems that should handle gibbing, - // and for some another reason GibbingSystem, which should empty all containers, doesn't get involved in this process - ContainerSystem.EmptyContainer(component.Stomach); - } -} - diff --git a/Content.Shared/Devour/Components/DevourerComponent.cs b/Content.Shared/Devour/Components/DevourerComponent.cs index d110175951..857e99062c 100644 --- a/Content.Shared/Devour/Components/DevourerComponent.cs +++ b/Content.Shared/Devour/Components/DevourerComponent.cs @@ -4,53 +4,79 @@ using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Devour.Components; -[RegisterComponent, NetworkedComponent] -[Access(typeof(SharedDevourSystem))] +/// +/// Allows an entity to eat whitelisted entities via an action. +/// Eaten mobs will be stored inside a container and released when the devourer is gibbed. +/// Eating something that fits their food preference will reward the devourer by being injected with a specific reagent. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(DevourSystem))] public sealed partial class DevourerComponent : Component { - [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? DevourAction = "ActionDevour"; - + /// + /// Action prototype for devouring. + /// [DataField] + public EntProtoId DevourAction = "ActionDevour"; + + /// + /// The spawned action entity for devouring. + /// + [DataField, AutoNetworkedField] public EntityUid? DevourActionEntity; - [DataField] + /// + /// The amount of time it takes to devour a mob. + /// + [DataField, AutoNetworkedField] + public float DevourTime = 3f; + + /// + /// The amount of time it takes to devour a structure. + /// + /// NOTE: original intended design was to increase this proportionally with damage thresholds, but those proved quite difficult to get consistently. right now it devours the structure at a fixed timer. + /// + /// + [DataField, AutoNetworkedField] + public float StructureDevourTime = 10f; + + /// + /// The sound to play when finishing devouring something. + /// + [DataField, AutoNetworkedField] public SoundSpecifier? SoundDevour = new SoundPathSpecifier("/Audio/Effects/demon_consume.ogg") { Params = AudioParams.Default.WithVolume(-3f), }; - [DataField] - public float DevourTime = 3f; - /// - /// The amount of time it takes to devour something - /// - /// NOTE: original intended design was to increase this proportionally with damage thresholds, but those proved quite difficult to get consistently. right now it devours the structure at a fixed timer. - /// + /// The sound to play when starting to devour a structure. /// - [DataField] - public float StructureDevourTime = 10f; - - [DataField] + [DataField, AutoNetworkedField] public SoundSpecifier? SoundStructureDevour = new SoundPathSpecifier("/Audio/Machines/airlock_creaking.ogg") { Params = AudioParams.Default.WithVolume(-3f), }; + /// + /// The container to store the eaten entities in. + /// + [ViewVariables] + public static string StomachContainerId = "stomach"; + /// /// Where the entities go when it devours them, empties when it is butchered. /// + [ViewVariables] public Container Stomach = default!; /// /// Determines what things the devourer can consume. /// - [DataField] + [DataField, AutoNetworkedField] public EntityWhitelist? Whitelist = new() { Components = new[] @@ -63,26 +89,26 @@ public sealed partial class DevourerComponent : Component /// Determines what things end up in the dragon's stomach if they eat it. /// If it isn't in the whitelist, it's deleted. /// - [DataField] + [DataField, AutoNetworkedField] public EntityWhitelist? StomachStorageWhitelist; /// - /// Determine's the dragon's food preference. If the eaten thing matches, - /// it is rewarded with the reward chemical. If null, all food is fine. + /// Determine's the dragon's food preference. If the eaten thing matches, + /// it is rewarded with the reward chemical. If null, all food is fine. /// - [DataField] + [DataField, AutoNetworkedField] public EntityWhitelist? FoodPreferenceWhitelist; /// - /// The chemical ID injected upon devouring + /// The chemical ID injected upon devouring. /// - [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Chemical = "Ichor"; + [DataField, AutoNetworkedField] + public ProtoId Chemical = "Ichor"; /// - /// The amount of ichor injected per devour + /// The amount of solution injected per devour. /// - [DataField] + [DataField, AutoNetworkedField] public float HealRate = 15f; } diff --git a/Content.Shared/Devour/DevourSystem.cs b/Content.Shared/Devour/DevourSystem.cs new file mode 100644 index 0000000000..71b1bd6396 --- /dev/null +++ b/Content.Shared/Devour/DevourSystem.cs @@ -0,0 +1,145 @@ +using Content.Shared.Actions; +using Content.Shared.Body.Events; +using Content.Shared.Body.Systems; +using Content.Shared.Chemistry.Components; +using Content.Shared.Devour.Components; +using Content.Shared.DoAfter; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Popups; +using Content.Shared.Whitelist; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Containers; +using Robust.Shared.Serialization; + +namespace Content.Shared.Devour; + +public sealed class DevourSystem : EntitySystem +{ + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + [Dependency] private readonly SharedBloodstreamSystem _bloodstreamSystem = default!; + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnDevourAction); + SubscribeLocalEvent(OnDoAfter); + SubscribeLocalEvent(OnGibContents); + } + + private void OnStartup(Entity ent, ref ComponentStartup args) + { + //Devourer doesn't actually chew, since he sends targets right into his stomach. + //I did it mom, I added ERP content into upstream. Legally! + ent.Comp.Stomach = _containerSystem.EnsureContainer(ent.Owner, DevourerComponent.StomachContainerId); + } + + private void OnInit(Entity ent, ref MapInitEvent args) + { + _actionsSystem.AddAction(ent.Owner, ref ent.Comp.DevourActionEntity, ent.Comp.DevourAction); + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + _actionsSystem.RemoveAction(ent.Owner, ent.Comp.DevourActionEntity); + } + + /// + /// The devour action + /// + private void OnDevourAction(Entity ent, ref DevourActionEvent args) + { + if (args.Handled || _whitelistSystem.IsWhitelistFailOrNull(ent.Comp.Whitelist, args.Target)) + return; + + args.Handled = true; + var target = args.Target; + + // Structure and mob devours handled differently. + if (TryComp(target, out MobStateComponent? targetState)) + { + switch (targetState.CurrentState) + { + case MobState.Critical: + case MobState.Dead: + + _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, ent.Owner, ent.Comp.DevourTime, new DevourDoAfterEvent(), ent.Owner, target: target, used: ent.Owner) + { + BreakOnMove = true, + }); + break; + case MobState.Invalid: + case MobState.Alive: + default: + _popupSystem.PopupClient(Loc.GetString("devour-action-popup-message-fail-target-alive"), ent.Owner, ent.Owner); + break; + } + + return; + } + + _popupSystem.PopupClient(Loc.GetString("devour-action-popup-message-structure"), ent.Owner, ent.Owner); + + if (ent.Comp.SoundStructureDevour != null) + _audioSystem.PlayPredicted(ent.Comp.SoundStructureDevour, ent.Owner, ent.Owner, ent.Comp.SoundStructureDevour.Params); + + _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, ent.Owner, ent.Comp.StructureDevourTime, new DevourDoAfterEvent(), ent.Owner, target: target, used: ent.Owner) + { + BreakOnMove = true, + }); + } + + private void OnDoAfter(Entity ent, ref DevourDoAfterEvent args) + { + if (args.Handled || args.Cancelled) + return; + + var ichorInjection = new Solution(ent.Comp.Chemical, ent.Comp.HealRate); + + // Grant ichor if the devoured thing meets the dragon's food preference + if (args.Args.Target != null && _whitelistSystem.IsWhitelistPassOrNull(ent.Comp.FoodPreferenceWhitelist, (EntityUid)args.Args.Target)) + { + _bloodstreamSystem.TryAddToChemicals(ent.Owner, ichorInjection); + } + + // If the devoured thing meets the stomach whitelist criteria, add it to the stomach + if (args.Args.Target != null && _whitelistSystem.IsWhitelistPass(ent.Comp.StomachStorageWhitelist, (EntityUid)args.Args.Target)) + { + _containerSystem.Insert(args.Args.Target.Value, ent.Comp.Stomach); + } + //TODO: Figure out a better way of removing structures via devour that still entails standing still and waiting for a DoAfter. Somehow. + //If it's not alive, it must be a structure. + // Delete if the thing isn't in the stomach storage whitelist (or the stomach whitelist is null/empty) + else if (args.Args.Target != null) + { + PredictedQueueDel(args.Args.Target.Value); + } + + _audioSystem.PlayPredicted(ent.Comp.SoundDevour, ent.Owner, ent.Owner); + } + + private void OnGibContents(Entity ent, ref BeingGibbedEvent args) + { + if (ent.Comp.StomachStorageWhitelist == null) + return; + + // For some reason we have two different systems that should handle gibbing, + // and for some another reason GibbingSystem, which should empty all containers, doesn't get involved in this process + _containerSystem.EmptyContainer(ent.Comp.Stomach); + } +} + +public sealed partial class DevourActionEvent : EntityTargetActionEvent; + +[Serializable, NetSerializable] +public sealed partial class DevourDoAfterEvent : SimpleDoAfterEvent; + diff --git a/Content.Shared/Devour/SharedDevourSystem.cs b/Content.Shared/Devour/SharedDevourSystem.cs deleted file mode 100644 index 702884c984..0000000000 --- a/Content.Shared/Devour/SharedDevourSystem.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Content.Shared.Actions; -using Content.Shared.Devour.Components; -using Content.Shared.DoAfter; -using Content.Shared.Mobs; -using Content.Shared.Mobs.Components; -using Content.Shared.Popups; -using Content.Shared.Whitelist; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Containers; -using Robust.Shared.Serialization; - -namespace Content.Shared.Devour; - -public abstract class SharedDevourSystem : EntitySystem -{ - [Dependency] protected readonly SharedAudioSystem _audioSystem = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; - [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; - [Dependency] protected readonly SharedContainerSystem ContainerSystem = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnDevourAction); - } - - protected void OnInit(EntityUid uid, DevourerComponent component, MapInitEvent args) - { - //Devourer doesn't actually chew, since he sends targets right into his stomach. - //I did it mom, I added ERP content into upstream. Legally! - component.Stomach = ContainerSystem.EnsureContainer(uid, "stomach"); - - _actionsSystem.AddAction(uid, ref component.DevourActionEntity, component.DevourAction); - } - - /// - /// The devour action - /// - protected void OnDevourAction(EntityUid uid, DevourerComponent component, DevourActionEvent args) - { - if (args.Handled || _whitelistSystem.IsWhitelistFailOrNull(component.Whitelist, args.Target)) - return; - - args.Handled = true; - var target = args.Target; - - // Structure and mob devours handled differently. - if (TryComp(target, out MobStateComponent? targetState)) - { - switch (targetState.CurrentState) - { - case MobState.Critical: - case MobState.Dead: - - _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, uid, component.DevourTime, new DevourDoAfterEvent(), uid, target: target, used: uid) - { - BreakOnMove = true, - }); - break; - default: - _popupSystem.PopupClient(Loc.GetString("devour-action-popup-message-fail-target-alive"), uid,uid); - break; - } - - return; - } - - _popupSystem.PopupClient(Loc.GetString("devour-action-popup-message-structure"), uid, uid); - - if (component.SoundStructureDevour != null) - _audioSystem.PlayPredicted(component.SoundStructureDevour, uid, uid, component.SoundStructureDevour.Params); - - _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, uid, component.StructureDevourTime, new DevourDoAfterEvent(), uid, target: target, used: uid) - { - BreakOnMove = true, - }); - } -} - -public sealed partial class DevourActionEvent : EntityTargetActionEvent { } - -[Serializable, NetSerializable] -public sealed partial class DevourDoAfterEvent : SimpleDoAfterEvent { } -