From bed23d5c51c13ef2b3800cf4055878d5733d48f9 Mon Sep 17 00:00:00 2001 From: Ataman Date: Fri, 20 Feb 2026 14:53:23 +0100 Subject: [PATCH 1/5] initial version of AnimusSystem --- .../Actions/AnimusActionAnimationBase.cs | 89 ++++++ .../Actions/AnimusActionSpriteChangeBase.cs | 33 +++ Robust.Client/Animus/AnimusComponent.cs | 33 +++ Robust.Client/Animus/AnimusPrototype.cs | 36 +++ Robust.Client/Animus/AnimusSystem.cs | 269 ++++++++++++++++++ .../Animus/Conditions/AnimusConditionBase.cs | 30 ++ .../Animus/States/AnimusStateAnimation.cs | 107 +++++++ .../Animus/States/AnimusStateBase.cs | 63 ++++ .../Animus/States/AnimusStateSpriteChange.cs | 40 +++ .../Animus/Timers/AnimusTimerBase.cs | 13 + .../Animus/Triggers/AnimusTriggerBase.cs | 50 ++++ .../GameObjects/ServerComponentFactory.cs | 1 + .../Prototypes/ServerPrototypeManager.cs | 1 + 13 files changed, 765 insertions(+) create mode 100644 Robust.Client/Animus/Actions/AnimusActionAnimationBase.cs create mode 100644 Robust.Client/Animus/Actions/AnimusActionSpriteChangeBase.cs create mode 100644 Robust.Client/Animus/AnimusComponent.cs create mode 100644 Robust.Client/Animus/AnimusPrototype.cs create mode 100644 Robust.Client/Animus/AnimusSystem.cs create mode 100644 Robust.Client/Animus/Conditions/AnimusConditionBase.cs create mode 100644 Robust.Client/Animus/States/AnimusStateAnimation.cs create mode 100644 Robust.Client/Animus/States/AnimusStateBase.cs create mode 100644 Robust.Client/Animus/States/AnimusStateSpriteChange.cs create mode 100644 Robust.Client/Animus/Timers/AnimusTimerBase.cs create mode 100644 Robust.Client/Animus/Triggers/AnimusTriggerBase.cs diff --git a/Robust.Client/Animus/Actions/AnimusActionAnimationBase.cs b/Robust.Client/Animus/Actions/AnimusActionAnimationBase.cs new file mode 100644 index 00000000000..20b96d73db9 --- /dev/null +++ b/Robust.Client/Animus/Actions/AnimusActionAnimationBase.cs @@ -0,0 +1,89 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using JetBrains.Annotations; +using Robust.Client.Animations; +using Robust.Client.GameObjects; +using Robust.Shared.Animations; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.Client.Animus.Actions; + +[ImplicitDataDefinitionForInheritors] +[PublicAPI] +public abstract partial class AnimusActionAnimationBase +{ + /// + /// Simple stopping animation for cases where only rotation and offset were affected. + /// + protected static readonly Animation StopAnimation = new() + { + Length = TimeSpan.FromSeconds(0), + AnimationTracks = + { + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Rotation), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(0), 0), + }, + }, + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Offset), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(new Vector2(), 0), + }, + }, + }, + }; + + /// + /// Should this action be restarted on trigger? + /// (Useful if an already running animation needs to change immediately after certain events). + /// + [DataField] + public bool RestartOnTrigger; + + public virtual void Initialize(EntityManager entityManager) + { + + } + + protected abstract Animation? GetNextAnimation(AppearanceSystem appearanceSystem, EntityUid entity, bool restarting); + + protected abstract Animation? GetStopAnimation(AppearanceSystem appearanceSystem, EntityUid entity); + + internal bool TryNextAnimation(AppearanceSystem appearanceSystem, EntityUid entity, [NotNullWhen(true)] out Animation? anim, bool restarting) + { + anim = GetNextAnimation(appearanceSystem, entity, restarting); + return anim != null; + } + + internal bool TryStopAnimation(AppearanceSystem appearanceSystem, EntityUid entity, [NotNullWhen(true)] out Animation? anim) + { + anim = GetStopAnimation(appearanceSystem, entity); + return anim != null; + } +} + +public sealed partial class AnimusActionAnimationNull : AnimusActionAnimationBase +{ + protected override Animation? GetNextAnimation(AppearanceSystem appearanceSystem, EntityUid entity, bool restarting) + { + return null; + } + + protected override Animation? GetStopAnimation(AppearanceSystem appearanceSystem, EntityUid entity) + { + return null; + } +} diff --git a/Robust.Client/Animus/Actions/AnimusActionSpriteChangeBase.cs b/Robust.Client/Animus/Actions/AnimusActionSpriteChangeBase.cs new file mode 100644 index 00000000000..36be38fa123 --- /dev/null +++ b/Robust.Client/Animus/Actions/AnimusActionSpriteChangeBase.cs @@ -0,0 +1,33 @@ +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.Client.Animus.Actions; + +[ImplicitDataDefinitionForInheritors] +[PublicAPI] +public abstract partial class AnimusActionSpriteChangeBase +{ + public abstract void Initialize(Entity entity, EntityManager entityManager); + public abstract void ExecuteSpriteChange(Entity entity); + public abstract void ResetSpriteChange(Entity entity); +} + +public sealed partial class AnimusActionSpriteChangeNull : AnimusActionSpriteChangeBase +{ + public override void Initialize(Entity entity, EntityManager entityManager) + { + + } + + public override void ExecuteSpriteChange(Entity entity) + { + + } + + public override void ResetSpriteChange(Entity entity) + { + + } +} diff --git a/Robust.Client/Animus/AnimusComponent.cs b/Robust.Client/Animus/AnimusComponent.cs new file mode 100644 index 00000000000..e646e71df9f --- /dev/null +++ b/Robust.Client/Animus/AnimusComponent.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Robust.Client.Animus.States; +using Robust.Client.Animus.Timers; +using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.Client.Animus; + +[RegisterComponent] +internal sealed partial class AnimusComponent : Component +{ + [DataField] + internal List> StateMachines; + + internal readonly List ActiveStateMachines = []; +} + +/// +/// Prototypes are global instances but conditions and other objects may require per-instance data. +/// Because of this, conditions +/// +internal sealed class AnimusInstance +{ + internal ProtoId Prototype = default; + internal AnimusStateBase[] States = []; + internal AnimusTimerBase? Timer = null; + internal AnimusStateBase ActiveState = new AnimusStateNull(); + internal AnimusStateBase DefaultState = new AnimusStateNull(); + internal TimeSpan ActiveStateExitTime = TimeSpan.Zero; + internal TimeSpan NextUpdate = TimeSpan.Zero; +} diff --git a/Robust.Client/Animus/AnimusPrototype.cs b/Robust.Client/Animus/AnimusPrototype.cs new file mode 100644 index 00000000000..85eb375371f --- /dev/null +++ b/Robust.Client/Animus/AnimusPrototype.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Robust.Client.Animus.States; +using Robust.Client.Animus.Timers; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.ViewVariables; + +namespace Robust.Client.Animus; + +[Prototype] +internal sealed partial class AnimusPrototype : IPrototype +{ + [ViewVariables] + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// A collection of possible states for this animus. + /// + [DataField] + internal List States = []; + + /// + /// Optional timer for executing an update. + /// Setting this disables periodic condition checks. + /// + [DataField] + internal AnimusTimerBase? Timer; + + /// + /// The default state to enter when no other state matches their conditions. + /// + [DataField] + internal AnimusStateBase DefaultState = new AnimusStateNull(); + +} diff --git a/Robust.Client/Animus/AnimusSystem.cs b/Robust.Client/Animus/AnimusSystem.cs new file mode 100644 index 00000000000..07b171f4e1e --- /dev/null +++ b/Robust.Client/Animus/AnimusSystem.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Robust.Client.Animus.States; +using Robust.Client.GameObjects; +using Robust.Client.Timing; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Serialization.Manager; + +namespace Robust.Client.Animus; + +internal sealed class AnimusSystem : EntitySystem +{ + private const float UpdateInterval = 0.1f; + + [Dependency] private readonly ILogManager _logger = default!; + [Dependency] private readonly IClientGameTiming _timing = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly ISerializationManager _serializationManager = default!; + private ISawmill _sawmill = default!; + private readonly Dictionary> _actingComponentProperties = new(); + // TODO: Subscribe to OnAnimationStarted event to register running legacy animations. + + private void OnAnimationCompleted(Entity entity, ref AnimationCompletedEvent args) + { + // TODO: Deregister legacy animations. + + if (!TryComp(entity, out var comp)) + return; + + AnimusInstance? animusInstance = null; + foreach (var machine in comp.ActiveStateMachines) + { + if (machine.ActiveState is AnimusStateAnimation animState && animState.RunningAnimationKey == args.Key) + { + animusInstance = machine; + } + } + + if (animusInstance == null) + return; + + if (animusInstance.ActiveState.OneShot) + { + SwitchState((entity, comp), + animusInstance.DefaultState, + false); + } + else if (EvaluateConditions((entity, comp), animusInstance.ActiveState)) + { + animusInstance.ActiveState.Update(entity, args.Finished); + } + } + + private void OnAnimationStateMachineComponentInit(Entity entity, ref ComponentInit args) + { + foreach (var animusInstance in entity.Comp.StateMachines) + { + InitializeStateMachine(entity, animusInstance); + } + } + + public override void Initialize() + { + base.Initialize(); + + _sawmill = _logger.GetSawmill("asm"); + + SubscribeLocalEvent(OnAnimationStateMachineComponentInit); + SubscribeLocalEvent(OnAnimationCompleted); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!_timing.IsFirstTimePredicted) + return; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var entity, out var comp)) + { + foreach (var animusInstance in comp.ActiveStateMachines) + { + UpdateStateMachine((entity, comp), animusInstance); + } + } + } + + internal void Trigger(AnimusStateBase state, Entity entity) + { + foreach (var cond in state.Conditions) + { + if (!cond.EvaluateInternal(entity)) + return; + } + + SwitchState(entity, state, true); + } + + internal void RegisterEntityAnimationProperty(EntityUid uid, Type type, string prop) + { + if (!_actingComponentProperties.ContainsKey(uid)) + _actingComponentProperties.Add(uid, []); + if (_actingComponentProperties[uid] + .Any(x => x.Item1 == type && x.Item2 == prop)) + { + _sawmill.Error($"An animation using the {prop} property on {type.Name} has already been registered for entity {uid}"); + return; + } + _actingComponentProperties[uid].Add((type, prop)); + } + + internal void DeregisterEntityAnimationProperty(EntityUid uid, Type type, string prop) + { + if(!_actingComponentProperties.ContainsKey(uid)) + _actingComponentProperties.Add(uid, []); + var tuple = _actingComponentProperties[uid].Single(x => x.Item1 == type && x.Item2 == prop); + _actingComponentProperties[uid].Remove(tuple); + } + + /// + /// Initialize state machine from prototype and add to list of active instances. + /// + private void InitializeStateMachine(Entity entity, ProtoId protoId) + { + List states = []; + + var proto = _prototypeManager.Index(protoId); + + var animusInstance = new AnimusInstance() + { + Prototype = proto, + Timer = _serializationManager.CreateCopy(proto.Timer, null, false), + DefaultState = _serializationManager.CreateCopy(proto.DefaultState, null, false, false), + NextUpdate = _timing.CurTime + TimeSpan.FromSeconds(UpdateInterval), + }; + + animusInstance.ActiveState.Initialize(entity, EntityManager, animusInstance); + animusInstance.DefaultState.Initialize(entity, EntityManager, animusInstance); + + foreach (var state in proto.States) + { + // TODO: Add more info to error message. + if (state.Conditions.Length == 0) + _sawmill.Error("Every AnimusState must have at least one condition. (Except DefaultState)"); + + var stateCopy = _serializationManager.CreateCopy(state, null, false, false); + stateCopy.Initialize(entity.Owner, EntityManager, animusInstance); + + foreach (var cond in stateCopy.Conditions) + { + cond.Initialize(EntityManager); + } + + foreach (var trigger in stateCopy.Triggers) + { + trigger.InitializeInternal(EntityManager, entity, stateCopy); + } + + states.Add(stateCopy); + } + + animusInstance.States = states.ToArray(); + + entity.Comp.ActiveStateMachines.Add(animusInstance); + } + + private void UpdateStateMachine(Entity entity, AnimusInstance animusInstance) + { + UpdateTriggers(entity, animusInstance); + ExitActiveStateIfExitTimerReached(entity, animusInstance); + + if (animusInstance.NextUpdate > _timing.CurTime) + return; + + CheckStateMachineConditionsAndUpdateState(entity, animusInstance); + + animusInstance.NextUpdate = _timing.CurTime + ( + animusInstance.Timer?.GetNextPeriod(_random) ?? + TimeSpan.FromSeconds(UpdateInterval) + ); + } + + private void ExitActiveStateIfExitTimerReached(Entity entity, AnimusInstance animusInstance) + { + if (animusInstance.ActiveStateExitTime == TimeSpan.Zero) + return; + + if (animusInstance.ActiveStateExitTime > _timing.CurTime) + return; + + SwitchState(entity, animusInstance.DefaultState, false); + animusInstance.ActiveStateExitTime = TimeSpan.Zero; + } + + private void CheckStateMachineConditionsAndUpdateState(Entity entity, AnimusInstance animusInstance) + { + var currentState = animusInstance.ActiveState; + var nextState = animusInstance.DefaultState; + + // Return if currentState has conditions that are still fulfilled. + if (currentState is { Conditions.Length: > 0, OneShot: false } && + EvaluateConditions(entity, currentState) && + animusInstance.ActiveStateExitTime == TimeSpan.Zero) + return; + + foreach (var state in animusInstance.States) + { + if (!EvaluateConditions(entity, state)) + continue; + + nextState = state; + break; + } + + SwitchState(entity, nextState, false); + if (nextState.ExitPeriod != TimeSpan.Zero) + animusInstance.ActiveStateExitTime = _timing.CurTime + nextState.ExitPeriod; + } + + private void UpdateTriggers(Entity entity, AnimusInstance animusInstance) + { + foreach (var state in animusInstance.States) + { + EvaluateTriggers(entity, state); + } + } + + private bool EvaluateConditions(Entity entity, AnimusStateBase state) + { + foreach (var cond in state.Conditions) + { + // Skip the check if it was recently false. + if ((!_timing.IsFirstTimePredicted || state.Instance.NextUpdate > _timing.CurTime) && !cond.LastResult) + return false; + + if (!cond.EvaluateInternal(entity)) + return false; + } + return true; + } + + private static void EvaluateTriggers(Entity entity, AnimusStateBase state) + { + foreach (var trigger in state.Triggers) + { + if (trigger.TriggerIfNecessaryInternal(entity)) + { + return; + } + } + } + + private static void SwitchState(Entity entity, AnimusStateBase newState, bool switchedByTrigger) + { + if (newState.Instance.ActiveState == newState) + return; + + newState.Instance.ActiveState.Exit(entity.Owner); + newState.Enter(entity.Owner, switchedByTrigger); + newState.Instance.ActiveState = newState; + } +} diff --git a/Robust.Client/Animus/Conditions/AnimusConditionBase.cs b/Robust.Client/Animus/Conditions/AnimusConditionBase.cs new file mode 100644 index 00000000000..27a377c23df --- /dev/null +++ b/Robust.Client/Animus/Conditions/AnimusConditionBase.cs @@ -0,0 +1,30 @@ +using JetBrains.Annotations; +using Robust.Shared.GameObjects; +using Robust.Shared.Log; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.Client.Animus.Conditions; + +[ImplicitDataDefinitionForInheritors] +[PublicAPI] +public abstract partial class AnimusConditionBase +{ + internal bool LastResult = false; + + /// + /// I couldn't get IoCManager.InjectDependencies to work, use this method to initialize them manually. + /// + /// + public virtual void Initialize(EntityManager entityManager) + { + + } + + protected abstract bool Evaluate(EntityUid ent); + + internal bool EvaluateInternal(Entity ent) + { + LastResult = Evaluate(ent); + return LastResult; + } +} diff --git a/Robust.Client/Animus/States/AnimusStateAnimation.cs b/Robust.Client/Animus/States/AnimusStateAnimation.cs new file mode 100644 index 00000000000..774f893ce9c --- /dev/null +++ b/Robust.Client/Animus/States/AnimusStateAnimation.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using Robust.Client.Animations; +using Robust.Client.Animus.Actions; +using Robust.Client.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.Client.Animus.States; + +internal sealed partial class AnimusStateAnimation : AnimusStateBase +{ + private static readonly AnimusActionAnimationBase NullAction = new AnimusActionAnimationNull(); + + private AnimationPlayerSystem _animationPlayerSystem; + private AnimusSystem _animationStateMachineSystem; + private AppearanceSystem _appearanceSystem; + + private readonly List<(Type, string)> _animationCompProps = []; + + [DataField] + internal AnimusActionAnimationBase Action = NullAction; + + internal string RunningAnimationKey => Action.GetType().Name + "_RUNNING"; + internal string StopAnimationKey => Action.GetType().Name + "_STOP"; + + /// + /// IoCManager.InjectDependencies won't work, use this override to inject your dependencies manually. + /// + internal override void Initialize(EntityUid ent, EntityManager entityManager, AnimusInstance animusInstance) + { + base.Initialize(ent, entityManager, animusInstance); + _animationPlayerSystem = entityManager.System(); + _animationStateMachineSystem = entityManager.System(); + _appearanceSystem = entityManager.System(); + Action.Initialize(entityManager); + } + + internal override void Enter(EntityUid ent, bool enteredByTrigger) + { + if (enteredByTrigger && Action.RestartOnTrigger || + _animationPlayerSystem.HasRunningAnimation(ent, RunningAnimationKey) || + !Action.TryNextAnimation(_appearanceSystem, ent, out var animation, false)) + return; + + UpdateAnimationCompProps(ent, animation); + _animationPlayerSystem.Play(ent, animation, RunningAnimationKey); + } + + internal override void Update(EntityUid ent, bool finished) + { + if (_animationPlayerSystem.HasRunningAnimation(ent, RunningAnimationKey) || + !Action.TryNextAnimation(_appearanceSystem, ent, out var animation, finished)) + return; + + UpdateAnimationCompProps(ent, animation); + _animationPlayerSystem.Play(ent, animation, RunningAnimationKey); + } + + internal override void Exit(EntityUid ent) + { + // Stop the running animation of this state. + if (_animationPlayerSystem.HasRunningAnimation(ent, RunningAnimationKey)) + _animationPlayerSystem.Stop(ent, RunningAnimationKey); + + // TODO: The stopping animation can still clash with registered component properties but to fix this + // a state queue would be required first. + // For now, we'll test and see if any glitches would even occur due to this. + ClearAnimationCompProps(ent); + + // Fetch stopping animation if it isn't running yet. + if (_animationPlayerSystem.HasRunningAnimation(ent, StopAnimationKey) || + !Action.TryStopAnimation(_appearanceSystem, ent, out var animation)) + return; + + // Play the stopping animation. + _animationPlayerSystem.Play(ent, animation, StopAnimationKey); + } + + private void ClearAnimationCompProps(EntityUid ent) + { + foreach (var compProp in _animationCompProps) + { + _animationStateMachineSystem.DeregisterEntityAnimationProperty(ent, + compProp.Item1, + compProp.Item2); + } + _animationCompProps.Clear(); + } + + private void UpdateAnimationCompProps(EntityUid ent, Animation anim) + { + ClearAnimationCompProps(ent); + foreach (var track in anim.AnimationTracks) + { + if (track is not AnimationTrackComponentProperty { ComponentType: not null, Property: not null } propTrack) + continue; + + _animationCompProps.Add((propTrack.ComponentType, propTrack.Property)); + _animationStateMachineSystem.RegisterEntityAnimationProperty(ent, + propTrack.ComponentType, + propTrack.Property); + } + } +} diff --git a/Robust.Client/Animus/States/AnimusStateBase.cs b/Robust.Client/Animus/States/AnimusStateBase.cs new file mode 100644 index 00000000000..b8dd097d6ee --- /dev/null +++ b/Robust.Client/Animus/States/AnimusStateBase.cs @@ -0,0 +1,63 @@ +using System; +using Robust.Client.Animus.Conditions; +using Robust.Client.Animus.Triggers; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.Client.Animus.States; + +[ImplicitDataDefinitionForInheritors] +internal abstract partial class AnimusStateBase +{ + /// + /// A collection of conditions that must be true for this state to activate. + /// + [DataField] + internal AnimusConditionBase[] Conditions = []; + + /// + /// A collection of triggers that cause a conditions check. + /// + [DataField] + internal AnimusTriggerBase[] Triggers = []; + + /// + /// If set to true, the state only executes its action one time instead of restarting it once the animation ends. + /// + [DataField] + internal bool OneShot; + + /// + /// If set to non-zero, state exits automatically after the defined timespan. + /// + [DataField] + internal TimeSpan ExitPeriod = TimeSpan.Zero; + + internal AnimusInstance Instance; + + [MustCallBase] + internal virtual void Initialize(EntityUid ent, EntityManager entityManager, AnimusInstance animusInstance) + { + Instance = animusInstance; + } + + internal virtual void Enter(EntityUid ent, bool enteredByTrigger) + { + + } + + internal virtual void Update(EntityUid ent, bool finished) + { + + } + + internal virtual void Exit(EntityUid ent) + { + + } +} + +internal sealed partial class AnimusStateNull : AnimusStateBase +{ + +} diff --git a/Robust.Client/Animus/States/AnimusStateSpriteChange.cs b/Robust.Client/Animus/States/AnimusStateSpriteChange.cs new file mode 100644 index 00000000000..32b0477ea86 --- /dev/null +++ b/Robust.Client/Animus/States/AnimusStateSpriteChange.cs @@ -0,0 +1,40 @@ +using JetBrains.Annotations; +using Robust.Client.Animus.Actions; +using Robust.Client.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.Client.Animus.States; + +[PublicAPI] +internal sealed partial class AnimusStateSpriteChange : AnimusStateBase +{ + [DataField] + internal AnimusActionSpriteChangeBase Action = NullAction; + + private static readonly AnimusActionSpriteChangeBase NullAction = new AnimusActionSpriteChangeNull(); + private EntityManager _entityManager; + + internal override void Initialize(EntityUid ent, EntityManager entityManager, AnimusInstance animusInstance) + { + base.Initialize(ent, entityManager, animusInstance); + _entityManager = entityManager; + if (!entityManager.TryGetComponent(ent, out var spriteComponent)) + return; + Action.Initialize((ent, spriteComponent), entityManager); + } + + internal override void Enter(EntityUid ent, bool enteredByTrigger) + { + if (!_entityManager.TryGetComponent(ent, out var spriteComponent)) + return; + Action.ExecuteSpriteChange((ent, spriteComponent)); + } + + internal override void Exit(EntityUid ent) + { + if (!_entityManager.TryGetComponent(ent, out var spriteComponent)) + return; + Action.ResetSpriteChange((ent, spriteComponent)); + } +} diff --git a/Robust.Client/Animus/Timers/AnimusTimerBase.cs b/Robust.Client/Animus/Timers/AnimusTimerBase.cs new file mode 100644 index 00000000000..4d91a38582e --- /dev/null +++ b/Robust.Client/Animus/Timers/AnimusTimerBase.cs @@ -0,0 +1,13 @@ +using System; +using JetBrains.Annotations; +using Robust.Shared.Random; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.Client.Animus.Timers; + +[ImplicitDataDefinitionForInheritors] +[PublicAPI] +public abstract partial class AnimusTimerBase +{ + public abstract TimeSpan GetNextPeriod(IRobustRandom random); +} diff --git a/Robust.Client/Animus/Triggers/AnimusTriggerBase.cs b/Robust.Client/Animus/Triggers/AnimusTriggerBase.cs new file mode 100644 index 00000000000..0c6cce4fa3e --- /dev/null +++ b/Robust.Client/Animus/Triggers/AnimusTriggerBase.cs @@ -0,0 +1,50 @@ +using JetBrains.Annotations; +using Robust.Client.Animus.States; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.Client.Animus.Triggers; + +[ImplicitDataDefinitionForInheritors] +[PublicAPI] +public abstract partial class AnimusTriggerBase +{ + private AnimusSystem _animationStateMachineSystem; + private Entity _entity; + private AnimusStateBase _parentState; + private bool _triggered = false; + + public virtual void TriggerIfNecessary(EntityUid entity) + { + + } + + protected virtual void Initialize(EntityManager entityManager) + { + + } + + protected void Trigger() + { + _triggered = true; + _animationStateMachineSystem.Trigger(_parentState, _entity); + } + + internal void InitializeInternal( + EntityManager entityManager, + Entity entity, + AnimusStateBase state) + { + _animationStateMachineSystem = entityManager.System(); + _entity = entity; + _parentState = state; + Initialize(entityManager); + } + + internal bool TriggerIfNecessaryInternal(EntityUid entity) + { + _triggered = false; + TriggerIfNecessary(entity); + return _triggered; + } +} diff --git a/Robust.Server/GameObjects/ServerComponentFactory.cs b/Robust.Server/GameObjects/ServerComponentFactory.cs index 9e718fbb079..0257df45f50 100644 --- a/Robust.Server/GameObjects/ServerComponentFactory.cs +++ b/Robust.Server/GameObjects/ServerComponentFactory.cs @@ -14,5 +14,6 @@ public ServerComponentFactory(IDynamicTypeFactoryInternal typeFactory, IReflecti RegisterIgnore("AnimationPlayer"); RegisterIgnore("GenericVisualizer"); RegisterIgnore("Sprite"); // Fucking finally + RegisterIgnore("Animus"); } } diff --git a/Robust.Server/Prototypes/ServerPrototypeManager.cs b/Robust.Server/Prototypes/ServerPrototypeManager.cs index 25c381ed988..0c270c5c62e 100644 --- a/Robust.Server/Prototypes/ServerPrototypeManager.cs +++ b/Robust.Server/Prototypes/ServerPrototypeManager.cs @@ -25,6 +25,7 @@ public ServerPrototypeManager() RegisterIgnore("shader"); RegisterIgnore("uiTheme"); RegisterIgnore("font"); + RegisterIgnore("animus"); } public override void Initialize() From 45202b7288e6998c4d39378fc22269cb9a2e1985 Mon Sep 17 00:00:00 2001 From: Ataman Date: Fri, 27 Feb 2026 10:20:59 +0100 Subject: [PATCH 2/5] Animus event trigger (WIP) --- Robust.Client/Animus/AnimusSystem.cs | 52 +++++++++++++------ .../AnimusTriggerEventSubscription.cs | 17 ++++++ 2 files changed, 53 insertions(+), 16 deletions(-) create mode 100644 Robust.Client/Animus/Triggers/AnimusTriggerEventSubscription.cs diff --git a/Robust.Client/Animus/AnimusSystem.cs b/Robust.Client/Animus/AnimusSystem.cs index 07b171f4e1e..9d1df04cd3a 100644 --- a/Robust.Client/Animus/AnimusSystem.cs +++ b/Robust.Client/Animus/AnimusSystem.cs @@ -146,29 +146,49 @@ private void InitializeStateMachine(Entity entity, ProtoId(); + foreach (var proto in prototypes) + { - states.Add(stateCopy); } - animusInstance.States = states.ToArray(); + // Read ComponentName and EventName for each - entity.Comp.ActiveStateMachines.Add(animusInstance); + // Subscribe using reflection + } + + private AnimusStateBase CopyInitializeState(Entity entity, AnimusStateBase state, AnimusInstance animusInstance) + { + // TODO: Add more info to error message. + if (state.Conditions.Length == 0) + _sawmill.Error("Every AnimusState must have at least one condition. (Except DefaultState)"); + + var stateCopy = _serializationManager.CreateCopy(state, null, false, false); + stateCopy.Initialize(entity.Owner, EntityManager, animusInstance); + + foreach (var cond in stateCopy.Conditions) + { + cond.Initialize(EntityManager); + } + + foreach (var trigger in stateCopy.Triggers) + { + trigger.InitializeInternal(EntityManager, entity, stateCopy); + } + + return stateCopy; } private void UpdateStateMachine(Entity entity, AnimusInstance animusInstance) diff --git a/Robust.Client/Animus/Triggers/AnimusTriggerEventSubscription.cs b/Robust.Client/Animus/Triggers/AnimusTriggerEventSubscription.cs new file mode 100644 index 00000000000..86c761a750f --- /dev/null +++ b/Robust.Client/Animus/Triggers/AnimusTriggerEventSubscription.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; +using Robust.Client.Animus.States; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.Client.Animus.Triggers; + +[ImplicitDataDefinitionForInheritors] +[PublicAPI] +public sealed partial class AnimusTriggerEventSubscription +{ + [DataField("component")] + public string ComponentName; + + [DataField("event")] + public string EventName; +} From faf384351cc6125471cb9e3b85b33a9e033a6307 Mon Sep 17 00:00:00 2001 From: Ataman Date: Tue, 7 Jul 2026 20:21:07 +0200 Subject: [PATCH 3/5] Small changes to allow Content trigger wrappers. --- Robust.Client/Animus/AnimusComponent.cs | 3 +- Robust.Client/Animus/AnimusSystem.cs | 51 +++++++++++-------- .../Animus/Conditions/AnimusConditionBase.cs | 2 +- .../Animus/Triggers/AnimusTriggerBase.cs | 2 +- .../AnimusTriggerEventSubscription.cs | 17 ------- .../Animus/Triggers/AnimusTriggerEvents.cs | 14 +++++ 6 files changed, 48 insertions(+), 41 deletions(-) delete mode 100644 Robust.Client/Animus/Triggers/AnimusTriggerEventSubscription.cs create mode 100644 Robust.Client/Animus/Triggers/AnimusTriggerEvents.cs diff --git a/Robust.Client/Animus/AnimusComponent.cs b/Robust.Client/Animus/AnimusComponent.cs index e646e71df9f..839dfd560d2 100644 --- a/Robust.Client/Animus/AnimusComponent.cs +++ b/Robust.Client/Animus/AnimusComponent.cs @@ -9,7 +9,7 @@ namespace Robust.Client.Animus; [RegisterComponent] -internal sealed partial class AnimusComponent : Component +public sealed partial class AnimusComponent : Component { [DataField] internal List> StateMachines; @@ -19,7 +19,6 @@ internal sealed partial class AnimusComponent : Component /// /// Prototypes are global instances but conditions and other objects may require per-instance data. -/// Because of this, conditions /// internal sealed class AnimusInstance { diff --git a/Robust.Client/Animus/AnimusSystem.cs b/Robust.Client/Animus/AnimusSystem.cs index 9d1df04cd3a..a102f7d5e68 100644 --- a/Robust.Client/Animus/AnimusSystem.cs +++ b/Robust.Client/Animus/AnimusSystem.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Robust.Client.Animus.States; +using Robust.Client.Animus.Triggers; using Robust.Client.GameObjects; using Robust.Client.Timing; using Robust.Shared.GameObjects; @@ -13,7 +14,7 @@ namespace Robust.Client.Animus; -internal sealed class AnimusSystem : EntitySystem +public sealed class AnimusSystem : EntitySystem { private const float UpdateInterval = 0.1f; @@ -92,7 +93,7 @@ public override void Update(float frameTime) } } - internal void Trigger(AnimusStateBase state, Entity entity) + internal void TriggerInternal(AnimusStateBase state, Entity entity) { foreach (var cond in state.Conditions) { @@ -146,6 +147,13 @@ private void InitializeStateMachine(Entity entity, ProtoId entity, ProtoId(); - foreach (var proto in prototypes) - { - - } - - // Read ComponentName and EventName for each - - // Subscribe using reflection - } - private AnimusStateBase CopyInitializeState(Entity entity, AnimusStateBase state, AnimusInstance animusInstance) { - // TODO: Add more info to error message. - if (state.Conditions.Length == 0) - _sawmill.Error("Every AnimusState must have at least one condition. (Except DefaultState)"); - var stateCopy = _serializationManager.CreateCopy(state, null, false, false); stateCopy.Initialize(entity.Owner, EntityManager, animusInstance); @@ -286,4 +276,25 @@ private static void SwitchState(Entity entity, AnimusStateBase newState.Enter(entity.Owner, switchedByTrigger); newState.Instance.ActiveState = newState; } + + public void TriggerFor(Entity entity) where TEvent : EntityEventArgs + { + if (!TryComp(entity, out var comp)) + return; + + // Good thing this isn't called on a per-frame basis... + foreach (var animusInstance in comp.ActiveStateMachines) + { + foreach (var state in animusInstance.States) + { + foreach (var trigger in state.Triggers) + { + if (trigger is AnimusTriggerEvents eventsTrigger && eventsTrigger.Events.Contains(typeof(TEvent).Name)) + { + TriggerInternal(state, entity); + } + } + } + } + } } diff --git a/Robust.Client/Animus/Conditions/AnimusConditionBase.cs b/Robust.Client/Animus/Conditions/AnimusConditionBase.cs index 27a377c23df..683d80732f8 100644 --- a/Robust.Client/Animus/Conditions/AnimusConditionBase.cs +++ b/Robust.Client/Animus/Conditions/AnimusConditionBase.cs @@ -12,7 +12,7 @@ public abstract partial class AnimusConditionBase internal bool LastResult = false; /// - /// I couldn't get IoCManager.InjectDependencies to work, use this method to initialize them manually. + /// IoCManager.InjectDependencies doesn't work, override this method to initialize dependencies manually. /// /// public virtual void Initialize(EntityManager entityManager) diff --git a/Robust.Client/Animus/Triggers/AnimusTriggerBase.cs b/Robust.Client/Animus/Triggers/AnimusTriggerBase.cs index 0c6cce4fa3e..4db1efdb478 100644 --- a/Robust.Client/Animus/Triggers/AnimusTriggerBase.cs +++ b/Robust.Client/Animus/Triggers/AnimusTriggerBase.cs @@ -27,7 +27,7 @@ protected virtual void Initialize(EntityManager entityManager) protected void Trigger() { _triggered = true; - _animationStateMachineSystem.Trigger(_parentState, _entity); + _animationStateMachineSystem.TriggerInternal(_parentState, _entity); } internal void InitializeInternal( diff --git a/Robust.Client/Animus/Triggers/AnimusTriggerEventSubscription.cs b/Robust.Client/Animus/Triggers/AnimusTriggerEventSubscription.cs deleted file mode 100644 index 86c761a750f..00000000000 --- a/Robust.Client/Animus/Triggers/AnimusTriggerEventSubscription.cs +++ /dev/null @@ -1,17 +0,0 @@ -using JetBrains.Annotations; -using Robust.Client.Animus.States; -using Robust.Shared.GameObjects; -using Robust.Shared.Serialization.Manager.Attributes; - -namespace Robust.Client.Animus.Triggers; - -[ImplicitDataDefinitionForInheritors] -[PublicAPI] -public sealed partial class AnimusTriggerEventSubscription -{ - [DataField("component")] - public string ComponentName; - - [DataField("event")] - public string EventName; -} diff --git a/Robust.Client/Animus/Triggers/AnimusTriggerEvents.cs b/Robust.Client/Animus/Triggers/AnimusTriggerEvents.cs new file mode 100644 index 00000000000..7079433ab9e --- /dev/null +++ b/Robust.Client/Animus/Triggers/AnimusTriggerEvents.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.Client.Animus.Triggers; + +[ImplicitDataDefinitionForInheritors] +[PublicAPI] +public sealed partial class AnimusTriggerEvents : AnimusTriggerBase +{ + [DataField] + public List Events { get; set; } +} From 48773755bd542f2450802fe19ef5174335df75b0 Mon Sep 17 00:00:00 2001 From: Ataman Date: Tue, 7 Jul 2026 20:31:26 +0200 Subject: [PATCH 4/5] RA0051 --- Robust.Client/Animus/AnimusSystem.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Robust.Client/Animus/AnimusSystem.cs b/Robust.Client/Animus/AnimusSystem.cs index a102f7d5e68..2dd8145a33f 100644 --- a/Robust.Client/Animus/AnimusSystem.cs +++ b/Robust.Client/Animus/AnimusSystem.cs @@ -14,19 +14,21 @@ namespace Robust.Client.Animus; -public sealed class AnimusSystem : EntitySystem +public sealed partial class AnimusSystem : EntitySystem { private const float UpdateInterval = 0.1f; - [Dependency] private readonly ILogManager _logger = default!; - [Dependency] private readonly IClientGameTiming _timing = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly ISerializationManager _serializationManager = default!; + [Dependency] private ILogManager _logger = default!; + [Dependency] private IClientGameTiming _timing = default!; + [Dependency] private IRobustRandom _random = default!; + [Dependency] private IPrototypeManager _prototypeManager = default!; + [Dependency] private ISerializationManager _serializationManager = default!; + private ISawmill _sawmill = default!; + private readonly Dictionary> _actingComponentProperties = new(); - // TODO: Subscribe to OnAnimationStarted event to register running legacy animations. + // TODO: Subscribe to OnAnimationStarted event to register running legacy animations. private void OnAnimationCompleted(Entity entity, ref AnimationCompletedEvent args) { // TODO: Deregister legacy animations. From b04e6c3a196c5c570a652432f1590bac57bdf86e Mon Sep 17 00:00:00 2001 From: Ataman Date: Fri, 10 Jul 2026 11:04:44 +0200 Subject: [PATCH 5/5] bye triggers --- .../Actions/AnimusActionAnimationBase.cs | 7 -- Robust.Client/Animus/AnimusComponent.cs | 1 - Robust.Client/Animus/AnimusSystem.cs | 90 ++----------------- .../Animus/Conditions/AnimusConditionBase.cs | 16 ++-- .../Animus/States/AnimusStateAnimation.cs | 5 +- .../Animus/States/AnimusStateBase.cs | 9 +- .../Animus/States/AnimusStateSpriteChange.cs | 2 +- .../Animus/Triggers/AnimusTriggerBase.cs | 50 ----------- .../Animus/Triggers/AnimusTriggerEvents.cs | 14 --- 9 files changed, 20 insertions(+), 174 deletions(-) delete mode 100644 Robust.Client/Animus/Triggers/AnimusTriggerBase.cs delete mode 100644 Robust.Client/Animus/Triggers/AnimusTriggerEvents.cs diff --git a/Robust.Client/Animus/Actions/AnimusActionAnimationBase.cs b/Robust.Client/Animus/Actions/AnimusActionAnimationBase.cs index 20b96d73db9..6d73b413f56 100644 --- a/Robust.Client/Animus/Actions/AnimusActionAnimationBase.cs +++ b/Robust.Client/Animus/Actions/AnimusActionAnimationBase.cs @@ -46,13 +46,6 @@ public abstract partial class AnimusActionAnimationBase }, }; - /// - /// Should this action be restarted on trigger? - /// (Useful if an already running animation needs to change immediately after certain events). - /// - [DataField] - public bool RestartOnTrigger; - public virtual void Initialize(EntityManager entityManager) { diff --git a/Robust.Client/Animus/AnimusComponent.cs b/Robust.Client/Animus/AnimusComponent.cs index 839dfd560d2..2db725d69e3 100644 --- a/Robust.Client/Animus/AnimusComponent.cs +++ b/Robust.Client/Animus/AnimusComponent.cs @@ -28,5 +28,4 @@ internal sealed class AnimusInstance internal AnimusStateBase ActiveState = new AnimusStateNull(); internal AnimusStateBase DefaultState = new AnimusStateNull(); internal TimeSpan ActiveStateExitTime = TimeSpan.Zero; - internal TimeSpan NextUpdate = TimeSpan.Zero; } diff --git a/Robust.Client/Animus/AnimusSystem.cs b/Robust.Client/Animus/AnimusSystem.cs index 2dd8145a33f..9f6494d4632 100644 --- a/Robust.Client/Animus/AnimusSystem.cs +++ b/Robust.Client/Animus/AnimusSystem.cs @@ -2,25 +2,20 @@ using System.Collections.Generic; using System.Linq; using Robust.Client.Animus.States; -using Robust.Client.Animus.Triggers; using Robust.Client.GameObjects; using Robust.Client.Timing; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Prototypes; -using Robust.Shared.Random; using Robust.Shared.Serialization.Manager; namespace Robust.Client.Animus; public sealed partial class AnimusSystem : EntitySystem { - private const float UpdateInterval = 0.1f; - [Dependency] private ILogManager _logger = default!; [Dependency] private IClientGameTiming _timing = default!; - [Dependency] private IRobustRandom _random = default!; [Dependency] private IPrototypeManager _prototypeManager = default!; [Dependency] private ISerializationManager _serializationManager = default!; @@ -50,9 +45,7 @@ private void OnAnimationCompleted(Entity entity, ref A if (animusInstance.ActiveState.OneShot) { - SwitchState((entity, comp), - animusInstance.DefaultState, - false); + SwitchState((entity, comp), animusInstance.DefaultState); } else if (EvaluateConditions((entity, comp), animusInstance.ActiveState)) { @@ -95,17 +88,6 @@ public override void Update(float frameTime) } } - internal void TriggerInternal(AnimusStateBase state, Entity entity) - { - foreach (var cond in state.Conditions) - { - if (!cond.EvaluateInternal(entity)) - return; - } - - SwitchState(entity, state, true); - } - internal void RegisterEntityAnimationProperty(EntityUid uid, Type type, string prop) { if (!_actingComponentProperties.ContainsKey(uid)) @@ -141,7 +123,6 @@ private void InitializeStateMachine(Entity entity, ProtoId entity, Anim cond.Initialize(EntityManager); } - foreach (var trigger in stateCopy.Triggers) - { - trigger.InitializeInternal(EntityManager, entity, stateCopy); - } - return stateCopy; } private void UpdateStateMachine(Entity entity, AnimusInstance animusInstance) { - UpdateTriggers(entity, animusInstance); ExitActiveStateIfExitTimerReached(entity, animusInstance); - - if (animusInstance.NextUpdate > _timing.CurTime) - return; - CheckStateMachineConditionsAndUpdateState(entity, animusInstance); - - animusInstance.NextUpdate = _timing.CurTime + ( - animusInstance.Timer?.GetNextPeriod(_random) ?? - TimeSpan.FromSeconds(UpdateInterval) - ); } private void ExitActiveStateIfExitTimerReached(Entity entity, AnimusInstance animusInstance) @@ -207,7 +173,7 @@ private void ExitActiveStateIfExitTimerReached(Entity entity, A if (animusInstance.ActiveStateExitTime > _timing.CurTime) return; - SwitchState(entity, animusInstance.DefaultState, false); + SwitchState(entity, animusInstance.DefaultState); animusInstance.ActiveStateExitTime = TimeSpan.Zero; } @@ -231,72 +197,28 @@ private void CheckStateMachineConditionsAndUpdateState(Entity e break; } - SwitchState(entity, nextState, false); + SwitchState(entity, nextState); if (nextState.ExitPeriod != TimeSpan.Zero) animusInstance.ActiveStateExitTime = _timing.CurTime + nextState.ExitPeriod; } - private void UpdateTriggers(Entity entity, AnimusInstance animusInstance) - { - foreach (var state in animusInstance.States) - { - EvaluateTriggers(entity, state); - } - } - private bool EvaluateConditions(Entity entity, AnimusStateBase state) { foreach (var cond in state.Conditions) { - // Skip the check if it was recently false. - if ((!_timing.IsFirstTimePredicted || state.Instance.NextUpdate > _timing.CurTime) && !cond.LastResult) - return false; - - if (!cond.EvaluateInternal(entity)) + if (!cond.EvaluateInternal(entity, _timing.CurTime, _timing.IsFirstTimePredicted)) return false; } return true; } - private static void EvaluateTriggers(Entity entity, AnimusStateBase state) - { - foreach (var trigger in state.Triggers) - { - if (trigger.TriggerIfNecessaryInternal(entity)) - { - return; - } - } - } - - private static void SwitchState(Entity entity, AnimusStateBase newState, bool switchedByTrigger) + private static void SwitchState(Entity entity, AnimusStateBase newState) { if (newState.Instance.ActiveState == newState) return; newState.Instance.ActiveState.Exit(entity.Owner); - newState.Enter(entity.Owner, switchedByTrigger); + newState.Enter(entity.Owner); newState.Instance.ActiveState = newState; } - - public void TriggerFor(Entity entity) where TEvent : EntityEventArgs - { - if (!TryComp(entity, out var comp)) - return; - - // Good thing this isn't called on a per-frame basis... - foreach (var animusInstance in comp.ActiveStateMachines) - { - foreach (var state in animusInstance.States) - { - foreach (var trigger in state.Triggers) - { - if (trigger is AnimusTriggerEvents eventsTrigger && eventsTrigger.Events.Contains(typeof(TEvent).Name)) - { - TriggerInternal(state, entity); - } - } - } - } - } } diff --git a/Robust.Client/Animus/Conditions/AnimusConditionBase.cs b/Robust.Client/Animus/Conditions/AnimusConditionBase.cs index 683d80732f8..e373d9a0a28 100644 --- a/Robust.Client/Animus/Conditions/AnimusConditionBase.cs +++ b/Robust.Client/Animus/Conditions/AnimusConditionBase.cs @@ -1,6 +1,6 @@ -using JetBrains.Annotations; +using System; +using JetBrains.Annotations; using Robust.Shared.GameObjects; -using Robust.Shared.Log; using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.Client.Animus.Conditions; @@ -9,7 +9,9 @@ namespace Robust.Client.Animus.Conditions; [PublicAPI] public abstract partial class AnimusConditionBase { - internal bool LastResult = false; + private bool _lastResult; + private TimeSpan _lastUpdate; + protected virtual TimeSpan UpdateInterval { get; } = TimeSpan.FromSeconds(0.1); /// /// IoCManager.InjectDependencies doesn't work, override this method to initialize dependencies manually. @@ -22,9 +24,11 @@ public virtual void Initialize(EntityManager entityManager) protected abstract bool Evaluate(EntityUid ent); - internal bool EvaluateInternal(Entity ent) + internal bool EvaluateInternal(Entity ent, TimeSpan currentTime, bool isFirstTimePredicted) { - LastResult = Evaluate(ent); - return LastResult; + if (!isFirstTimePredicted || currentTime - _lastUpdate < UpdateInterval) + return _lastResult; + _lastResult = Evaluate(ent); + return _lastResult; } } diff --git a/Robust.Client/Animus/States/AnimusStateAnimation.cs b/Robust.Client/Animus/States/AnimusStateAnimation.cs index 774f893ce9c..4bc3425ab37 100644 --- a/Robust.Client/Animus/States/AnimusStateAnimation.cs +++ b/Robust.Client/Animus/States/AnimusStateAnimation.cs @@ -38,10 +38,9 @@ internal override void Initialize(EntityUid ent, EntityManager entityManager, An Action.Initialize(entityManager); } - internal override void Enter(EntityUid ent, bool enteredByTrigger) + internal override void Enter(EntityUid ent) { - if (enteredByTrigger && Action.RestartOnTrigger || - _animationPlayerSystem.HasRunningAnimation(ent, RunningAnimationKey) || + if (_animationPlayerSystem.HasRunningAnimation(ent, RunningAnimationKey) || !Action.TryNextAnimation(_appearanceSystem, ent, out var animation, false)) return; diff --git a/Robust.Client/Animus/States/AnimusStateBase.cs b/Robust.Client/Animus/States/AnimusStateBase.cs index b8dd097d6ee..6427876b1d2 100644 --- a/Robust.Client/Animus/States/AnimusStateBase.cs +++ b/Robust.Client/Animus/States/AnimusStateBase.cs @@ -1,6 +1,5 @@ using System; using Robust.Client.Animus.Conditions; -using Robust.Client.Animus.Triggers; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; @@ -15,12 +14,6 @@ internal abstract partial class AnimusStateBase [DataField] internal AnimusConditionBase[] Conditions = []; - /// - /// A collection of triggers that cause a conditions check. - /// - [DataField] - internal AnimusTriggerBase[] Triggers = []; - /// /// If set to true, the state only executes its action one time instead of restarting it once the animation ends. /// @@ -41,7 +34,7 @@ internal virtual void Initialize(EntityUid ent, EntityManager entityManager, Ani Instance = animusInstance; } - internal virtual void Enter(EntityUid ent, bool enteredByTrigger) + internal virtual void Enter(EntityUid ent) { } diff --git a/Robust.Client/Animus/States/AnimusStateSpriteChange.cs b/Robust.Client/Animus/States/AnimusStateSpriteChange.cs index 32b0477ea86..0b9258608ef 100644 --- a/Robust.Client/Animus/States/AnimusStateSpriteChange.cs +++ b/Robust.Client/Animus/States/AnimusStateSpriteChange.cs @@ -24,7 +24,7 @@ internal override void Initialize(EntityUid ent, EntityManager entityManager, An Action.Initialize((ent, spriteComponent), entityManager); } - internal override void Enter(EntityUid ent, bool enteredByTrigger) + internal override void Enter(EntityUid ent) { if (!_entityManager.TryGetComponent(ent, out var spriteComponent)) return; diff --git a/Robust.Client/Animus/Triggers/AnimusTriggerBase.cs b/Robust.Client/Animus/Triggers/AnimusTriggerBase.cs deleted file mode 100644 index 4db1efdb478..00000000000 --- a/Robust.Client/Animus/Triggers/AnimusTriggerBase.cs +++ /dev/null @@ -1,50 +0,0 @@ -using JetBrains.Annotations; -using Robust.Client.Animus.States; -using Robust.Shared.GameObjects; -using Robust.Shared.Serialization.Manager.Attributes; - -namespace Robust.Client.Animus.Triggers; - -[ImplicitDataDefinitionForInheritors] -[PublicAPI] -public abstract partial class AnimusTriggerBase -{ - private AnimusSystem _animationStateMachineSystem; - private Entity _entity; - private AnimusStateBase _parentState; - private bool _triggered = false; - - public virtual void TriggerIfNecessary(EntityUid entity) - { - - } - - protected virtual void Initialize(EntityManager entityManager) - { - - } - - protected void Trigger() - { - _triggered = true; - _animationStateMachineSystem.TriggerInternal(_parentState, _entity); - } - - internal void InitializeInternal( - EntityManager entityManager, - Entity entity, - AnimusStateBase state) - { - _animationStateMachineSystem = entityManager.System(); - _entity = entity; - _parentState = state; - Initialize(entityManager); - } - - internal bool TriggerIfNecessaryInternal(EntityUid entity) - { - _triggered = false; - TriggerIfNecessary(entity); - return _triggered; - } -} diff --git a/Robust.Client/Animus/Triggers/AnimusTriggerEvents.cs b/Robust.Client/Animus/Triggers/AnimusTriggerEvents.cs deleted file mode 100644 index 7079433ab9e..00000000000 --- a/Robust.Client/Animus/Triggers/AnimusTriggerEvents.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using Robust.Shared.Serialization.Manager.Attributes; - -namespace Robust.Client.Animus.Triggers; - -[ImplicitDataDefinitionForInheritors] -[PublicAPI] -public sealed partial class AnimusTriggerEvents : AnimusTriggerBase -{ - [DataField] - public List Events { get; set; } -}