From e3d9d4df0294cb1e8edf0c779c7a3e3e1b294bc5 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Mon, 5 Sep 2022 05:21:21 +1200 Subject: [PATCH] Fix status effect prediction (#8475) --- .../Standing/StandingStateSystem.cs | 20 ++++++---- .../StatusEffect/StatusEffectsComponent.cs | 9 ++++- .../StatusEffect/StatusEffectsSystem.cs | 39 ++++++++++--------- Content.Shared/Stunnable/SharedStunSystem.cs | 11 +++++- 4 files changed, 50 insertions(+), 29 deletions(-) diff --git a/Content.Shared/Standing/StandingStateSystem.cs b/Content.Shared/Standing/StandingStateSystem.cs index ed8a01d04a..84af6fb5d7 100644 --- a/Content.Shared/Standing/StandingStateSystem.cs +++ b/Content.Shared/Standing/StandingStateSystem.cs @@ -8,12 +8,14 @@ using Robust.Shared.Physics; using Content.Shared.Physics; using Robust.Shared.GameStates; using Robust.Shared.Serialization; +using Robust.Shared.Network; namespace Content.Shared.Standing { public sealed class StandingStateSystem : EntitySystem { [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly INetManager _netMan = default!; // If StandingCollisionLayer value is ever changed to more than one layer, the logic needs to be edited. private const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable; @@ -29,11 +31,12 @@ namespace Content.Shared.Standing if (args.Current is not StandingComponentState state) return; component.Standing = state.Standing; + component.ChangedFixtures = new(state.ChangedFixtures); } private void OnGetState(EntityUid uid, StandingStateComponent component, ref ComponentGetState args) { - args.State = new StandingComponentState(component.Standing); + args.State = new StandingComponentState(component.Standing, component.ChangedFixtures); } public bool IsDown(EntityUid uid, StandingStateComponent? standingState = null) @@ -78,9 +81,6 @@ namespace Content.Shared.Standing Dirty(standingState); RaiseLocalEvent(uid, new DownedEvent(), false); - if (!_gameTiming.IsFirstTimePredicted) - return true; - // Seemed like the best place to put it appearance?.SetData(RotationVisuals.RotationState, RotationState.Horizontal); @@ -97,9 +97,11 @@ namespace Content.Shared.Standing } } - // Currently shit is only downed by server but when it's predicted we can probably only play this on server / client - // > no longer true with door crushing. There just needs to be a better way to handle audio prediction. - if (playSound) + if (!_gameTiming.IsFirstTimePredicted) + return true; + + // TODO audio prediction + if (playSound && _netMan.IsServer) { SoundSystem.Play(standingState.DownSound.GetSound(), Filter.Pvs(uid), uid, AudioHelpers.WithVariation(0.25f)); } @@ -151,10 +153,12 @@ namespace Content.Shared.Standing private sealed class StandingComponentState : ComponentState { public bool Standing { get; } + public List ChangedFixtures { get; } - public StandingComponentState(bool standing) + public StandingComponentState(bool standing, List changedFixtures) { Standing = standing; + ChangedFixtures = changedFixtures; } } } diff --git a/Content.Shared/StatusEffect/StatusEffectsComponent.cs b/Content.Shared/StatusEffect/StatusEffectsComponent.cs index c18c89168e..0b852b6db8 100644 --- a/Content.Shared/StatusEffect/StatusEffectsComponent.cs +++ b/Content.Shared/StatusEffect/StatusEffectsComponent.cs @@ -1,4 +1,4 @@ -using Robust.Shared.GameStates; +using Robust.Shared.GameStates; using Robust.Shared.Serialization; namespace Content.Shared.StatusEffect @@ -53,6 +53,13 @@ namespace Content.Shared.StatusEffect CooldownRefresh = refresh; RelevantComponent = relevantComponent; } + + public StatusEffectState(StatusEffectState toCopy) + { + Cooldown = (toCopy.Cooldown.Item1, toCopy.Cooldown.Item2); + CooldownRefresh = toCopy.CooldownRefresh; + RelevantComponent = toCopy.RelevantComponent; + } } [Serializable, NetSerializable] diff --git a/Content.Shared/StatusEffect/StatusEffectsSystem.cs b/Content.Shared/StatusEffect/StatusEffectsSystem.cs index e9d94fe8ed..2238b3a13c 100644 --- a/Content.Shared/StatusEffect/StatusEffectsSystem.cs +++ b/Content.Shared/StatusEffect/StatusEffectsSystem.cs @@ -61,22 +61,24 @@ namespace Content.Shared.StatusEffect { if (!state.ActiveEffects.ContainsKey(effect)) { - TryRemoveStatusEffect(uid, effect, component); + TryRemoveStatusEffect(uid, effect, component, remComp: false); } } - foreach (var effect in state.ActiveEffects) + foreach (var (key, effect) in state.ActiveEffects) { // don't bother with anything if we already have it - if (component.ActiveEffects.ContainsKey(effect.Key)) + if (component.ActiveEffects.ContainsKey(key)) { - component.ActiveEffects[effect.Key] = effect.Value; + component.ActiveEffects[key] = new(effect); continue; } - var time = effect.Value.Cooldown.Item2 - effect.Value.Cooldown.Item1; + var time = effect.Cooldown.Item2 - effect.Cooldown.Item1; - TryAddStatusEffect(uid, effect.Key, time, true, component); + TryAddStatusEffect(uid, key, time, true, component, effect.Cooldown.Item1); + component.ActiveEffects[key].RelevantComponent = effect.RelevantComponent; + // state handling should not add networked components, that is handled separately by the client game state manager. } } @@ -103,7 +105,7 @@ namespace Content.Shared.StatusEffect if (!EntityManager.HasComponent(uid)) { var comp = EntityManager.AddComponent(uid); - status.ActiveEffects[key].RelevantComponent = comp.Name; + status.ActiveEffects[key].RelevantComponent = _componentFactory.GetComponentName(comp.GetType()); } return true; } @@ -143,6 +145,8 @@ namespace Content.Shared.StatusEffect /// How long the effect should last for. /// The status effect cooldown should be refreshed (true) or accumulated (false). /// The status effects component to change, if you already have it. + /// The time at which the status effect started. This exists mostly for prediction + /// resetting. /// False if the effect could not be added, or if the effect already existed. /// /// This obviously does not add any actual 'effects' on its own. Use the generic overload, @@ -152,7 +156,7 @@ namespace Content.Shared.StatusEffect /// If you want special 'effect merging' behavior, do it your own damn self! /// public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool refresh, - StatusEffectsComponent? status = null) + StatusEffectsComponent? status = null, TimeSpan? startTime = null) { if (!Resolve(uid, ref status, false)) return false; @@ -163,7 +167,8 @@ namespace Content.Shared.StatusEffect // is fine var proto = _prototypeManager.Index(key); - (TimeSpan, TimeSpan) cooldown = (_gameTiming.CurTime, _gameTiming.CurTime + time); + var start = startTime ?? _gameTiming.CurTime; + (TimeSpan, TimeSpan) cooldown = (start, start + time); if (HasStatusEffect(uid, key, status)) { @@ -232,13 +237,15 @@ namespace Content.Shared.StatusEffect /// The entity to remove an effect from. /// The effect ID to remove. /// The status effects component to change, if you already have it. + /// If true, status effect removal will also remove the relevant component. This option + /// exists mostly for prediction resetting. /// False if the effect could not be removed, true otherwise. /// /// Obviously this doesn't automatically clear any effects a status effect might have. /// That's up to the removed component to handle itself when it's removed. /// public bool TryRemoveStatusEffect(EntityUid uid, string key, - StatusEffectsComponent? status = null) + StatusEffectsComponent? status = null, bool remComp = true) { if (!Resolve(uid, ref status, false)) return false; @@ -250,16 +257,12 @@ namespace Content.Shared.StatusEffect var state = status.ActiveEffects[key]; // There are cases where a status effect component might be server-only, so TryGetRegistration... - if (state.RelevantComponent != null && _componentFactory.TryGetRegistration(state.RelevantComponent, out var registration)) + if (remComp + && state.RelevantComponent != null + && _componentFactory.TryGetRegistration(state.RelevantComponent, out var registration)) { var type = registration.Type; - - // Make sure the component is actually there first. - // Maybe a badmin badminned the component away, - // or perhaps, on the client, the component deletion sync - // was faster than prediction could predict. Either way, let's not assume the component exists. - if (EntityManager.HasComponent(uid, type)) - EntityManager.RemoveComponent(uid, type); + EntityManager.RemoveComponent(uid, type); } if (proto.Alert != null) diff --git a/Content.Shared/Stunnable/SharedStunSystem.cs b/Content.Shared/Stunnable/SharedStunSystem.cs index 8aca962027..c5d1ae1d22 100644 --- a/Content.Shared/Stunnable/SharedStunSystem.cs +++ b/Content.Shared/Stunnable/SharedStunSystem.cs @@ -29,7 +29,8 @@ namespace Content.Shared.Stunnable public override void Initialize() { SubscribeLocalEvent(OnKnockInit); - SubscribeLocalEvent(OnKnockRemove); + SubscribeLocalEvent(OnKnockShutdown); + SubscribeLocalEvent(OnStandAttempt); SubscribeLocalEvent(OnSlowInit); SubscribeLocalEvent(OnSlowRemove); @@ -96,11 +97,17 @@ namespace Content.Shared.Stunnable _standingStateSystem.Down(uid); } - private void OnKnockRemove(EntityUid uid, KnockedDownComponent component, ComponentRemove args) + private void OnKnockShutdown(EntityUid uid, KnockedDownComponent component, ComponentShutdown args) { _standingStateSystem.Stand(uid); } + private void OnStandAttempt(EntityUid uid, KnockedDownComponent component, StandAttemptEvent args) + { + if (component.LifeStage <= ComponentLifeStage.Running) + args.Cancel(); + } + private void OnSlowInit(EntityUid uid, SlowedDownComponent component, ComponentInit args) { _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(uid);