From 70b56fb3077076ef9113ee290a7ec3fd1003a272 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Wed, 9 Apr 2025 16:50:43 -0700 Subject: [PATCH 1/9] Aditional Buff & Skill BeginConditions --- .../ActorStateComponent/AnimationState.cs | 352 ++++++++++++++++++ Maple2.File.Ingest/Maple2.File.Ingest.csproj | 2 +- Maple2.File.Ingest/Mapper/AnimationMapper.cs | 14 +- Maple2.File.Ingest/MapperExtensions.cs | 77 +++- Maple2.Model/Enum/CompareType.cs | 5 + Maple2.Model/Game/Npc/Npc.cs | 4 +- Maple2.Model/Metadata/AnimationMetadata.cs | 4 +- Maple2.Model/Metadata/BeginCondition.cs | 22 +- Maple2.Server.Game/Model/Field/Actor/Actor.cs | 22 +- .../ActorStateComponent/AnimationRecord.cs | 61 +++ .../ActorStateComponent/AnimationState.cs | 336 +++++++++-------- .../ActorStateComponent/MovementState.cs | 2 +- .../MovementStateStates/MovementState.Walk.cs | 2 +- .../MovementState.SkillCastTask.cs | 2 +- .../Model/Field/Actor/FieldNpc.cs | 20 +- .../Model/Field/Actor/FieldPlayer.cs | 2 +- .../Field/Actor/Routine/AnimateRoutine.cs | 4 +- .../Model/Field/Actor/Routine/JumpRoutine.cs | 4 +- .../Model/Field/Actor/Routine/MoveRoutine.cs | 4 +- .../Model/Field/Actor/Routine/NpcRoutine.cs | 2 +- Maple2.Server.Game/Model/Skill/SkillQueue.cs | 4 + .../PacketHandlers/SkillHandler.cs | 2 +- Maple2.Server.Game/Util/SkillUtils.cs | 69 ++++ 23 files changed, 818 insertions(+), 198 deletions(-) create mode 100644 .github/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs create mode 100644 Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationRecord.cs diff --git a/.github/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs b/.github/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs new file mode 100644 index 000000000..5a340463b --- /dev/null +++ b/.github/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs @@ -0,0 +1,352 @@ +using Maple2.Model.Metadata; +using Maple2.Server.Core.Packets; +using Maple2.Server.Game.Model.Enum; + +namespace Maple2.Server.Game.Model.ActorStateComponent; + +/// +/// Manages animation sequences for actors in the game. +/// +public class AnimationState { + private readonly IActor actor; + private AnimationRecord? current; + private AnimationRecord? queued; + + public readonly AnimationMetadata? RigMetadata; + public AnimationSequenceMetadata? PlayingSequence => current?.Sequence; + public short IdleSequenceId { get; init; } + public float SequenceSpeed => current?.Speed ?? 1.0f; + + private bool isHandlingKeyframe; + private bool IsPlayerAnimation => actor is FieldPlayer; + public float MoveSpeed { get; set; } = 1; + public float AttackSpeed { get; set; } = 1; + private float lastSequenceTime; + private float sequenceEnd; + private LoopData sequenceLoop; + private long lastTick; + private long sequenceEndTick; + private long sequenceLoopEndTick; + + private bool debugPrintAnimations; + public bool DebugPrintAnimations { + get { return debugPrintAnimations; } + set { + if (actor is FieldPlayer) { + debugPrintAnimations = value; + } + } + } + + /// + /// Initializes a new instance of the AnimationState class. + /// + /// The actor this animation state belongs to + /// The model name to load animations for + public AnimationState(IActor actor, string modelName) { + this.actor = actor; + + RigMetadata = actor.NpcMetadata?.GetAnimation(modelName); + MoveSpeed = 1; + AttackSpeed = 1; + sequenceLoop = new LoopData(0, 0); + + if (RigMetadata is null) { + IdleSequenceId = 0; + return; + } + + string idleName = "Idle_A"; + if (actor is FieldNpc npc) { + idleName = npc.Value.Metadata.Action.Actions.FirstOrDefault()?.Name ?? idleName; + IdleSequenceId = RigMetadata.Sequences.FirstOrDefault(sequence => sequence.Key.Contains(idleName)).Value.Id; + return; + } + IdleSequenceId = RigMetadata.Sequences.FirstOrDefault(sequence => sequence.Key == idleName).Value.Id; + } + + /// + /// Resets the current animation sequence. + /// + private void ResetSequence() { + if (current?.Sequence != null && actor is FieldNpc npc) { + npc.SendControl = true; + } + current = null; + lastSequenceTime = 0; + sequenceLoop = new LoopData(0, 0); + lastTick = 0; + sequenceEnd = 0; + } + + /// + /// Attempts to play an animation sequence with the specified name, speed, and type. + /// + /// The name of the animation sequence to play + /// The speed at which to play the animation + /// The type of animation (Move, Skill, or Misc) + /// True if the sequence was found and started (or queued), false otherwise + public bool TryPlaySequence(string name, float speed, AnimationType type) { + // Can't play animations without metadata + if (RigMetadata is null || !RigMetadata.Sequences.TryGetValue(name, out AnimationSequenceMetadata? sequence)) { + DebugPrint($"Attempt to play nonexistent sequence '{name}' at x{speed} speed, previous: '{PlayingSequence?.Name ?? "none"}' x{SequenceSpeed}"); + ResetSequence(); + return false; + } + + // If we're currently processing a keyframe event, queue this sequence for later + if (isHandlingKeyframe) { + queued = new AnimationRecord(sequence, speed, type); + return true; + } + + // Play the sequence immediately + PlaySequence(sequence, speed, type); + return true; + } + + /// + /// Plays the specified animation sequence. + /// + /// The animation sequence to play + /// The speed at which to play the animation + /// The type of animation (Move, Skill, or Misc) + private void PlaySequence(AnimationSequenceMetadata sequenceMetadata, float speed, AnimationType type) { + // For NPCs, set SendControl flag when changing sequences + if (current?.Sequence != sequenceMetadata && actor is FieldNpc npc) { + npc.SendControl = true; + } + + // Log the sequence change + DebugPrint($"Playing sequence '{sequenceMetadata.Name}' at x{speed} speed, previous: '{PlayingSequence?.Name ?? "none"}' x{SequenceSpeed}"); + + // Reset current sequence state + ResetSequence(); + + // Set the new sequence properties + current = new AnimationRecord(sequenceMetadata, speed, type); + + // Start tracking from current tick + lastTick = actor.Field.FieldTick; + } + + /// + /// Cancels the currently playing animation sequence. + /// + public void CancelSequence() { + // Log the cancellation if a sequence is playing + if (PlayingSequence is not null) { + DebugPrint($"Canceled playing sequence: '{PlayingSequence.Name}' x{SequenceSpeed}"); + } + + // If we're processing a keyframe event, queue the reset for later + if (isHandlingKeyframe) { + queued = null; + return; + } + + // Reset the sequence state + ResetSequence(); + } + + /// + /// Updates the animation state based on the current tick count. + /// + /// The current server tick count + public void Update(long tickCount) { + // Skip update if no animation metadata is available + if (RigMetadata is null) { + return; + } + + // Reset if no valid sequence is playing + if (PlayingSequence?.Keys.Count == 0) { + ResetSequence(); + return; + } + + // Calculate the current sequence time + float sequenceSpeedModifier = current?.Type switch { + AnimationType.Move => MoveSpeed, + AnimationType.Skill => AttackSpeed, + _ => 1, + }; + + long lastServerTick = lastTick == 0 ? tickCount : lastTick; + float speed = SequenceSpeed * sequenceSpeedModifier / 1000; + float delta = (float)(tickCount - lastServerTick) * speed; + float sequenceTime = lastSequenceTime + delta; + + // Process keyframe events + if (PlayingSequence?.Keys != null) { + foreach (AnimationKey key in PlayingSequence.Keys) { + if (HasHitKeyframe(sequenceTime, key)) { + HitKeyframe(sequenceTime, key, speed); + } + } + } + + // Handle sequence looping + if (current?.IsLooping == true && sequenceLoop.end != 0 && sequenceTime > sequenceLoop.end) { + if (!IsPlayerAnimation || tickCount <= sequenceLoopEndTick + Constant.ClientGraceTimeTick) { + if (current.LoopOnlyOnce) { + current.IsLooping = false; + current.LoopOnlyOnce = false; + } + + sequenceTime -= sequenceLoop.end - sequenceLoop.start; + lastSequenceTime = sequenceTime - Math.Max(delta, sequenceTime - sequenceLoop.end + 0.001f); + + // Play all keyframe events from loopstart to current + if (PlayingSequence?.Keys != null) { + foreach (AnimationKey key in PlayingSequence.Keys) { + if (HasHitKeyframe(sequenceTime, key)) { + HitKeyframe(sequenceTime, key, speed); + } + } + } + } + } + + // Check for sequence end + if (sequenceEnd != 0 && sequenceTime > sequenceEnd) { + if (!IsPlayerAnimation || tickCount <= sequenceEndTick + Constant.ClientGraceTimeTick) { + ResetSequence(); + } + } + + // Update timing state + lastTick = tickCount; + lastSequenceTime = sequenceTime; + isHandlingKeyframe = false; + + // Process queued actions + if (queued != null) { + PlaySequence(queued.Sequence!, queued.Speed, queued.Type); + queued = null; + } + } + + /// + /// Sets whether the current sequence should loop. + /// + /// Whether the sequence should loop + /// Whether the sequence should only loop once + public void SetLoopSequence(bool shouldLoop, bool loopOnlyOnce) { + if (current is null) { + return; + } + + current.IsLooping = shouldLoop; + current.LoopOnlyOnce = loopOnlyOnce; + } + + /// + /// Determines if a keyframe has been hit in the current update. + /// + /// The current sequence time + /// The keyframe to check + /// True if the keyframe has been hit, false otherwise + private bool HasHitKeyframe(float sequenceTime, AnimationKey key) { + bool keyBeforeLoop = !current?.IsLooping ?? true || sequenceLoop.end == 0 || key.Time <= sequenceLoop.end + 0.001f; + bool hitKeySinceLastTick = key.Time > lastSequenceTime && key.Time <= sequenceTime; + + return keyBeforeLoop && hitKeySinceLastTick; + } + + /// + /// Gets the normalized time within a segment defined by two keyframes. + /// + /// The name of the first keyframe + /// The name of the second keyframe + /// A value between 0 and 1 representing the position within the segment, or -1 if not in the segment + public float GetSequenceSegmentTime(string keyframe1, string keyframe2) { + if (PlayingSequence is null) { + return -1; + } + + float keyframe1Time = -1; + float keyframe2Time = -1; + + foreach (AnimationKey key in PlayingSequence.Keys) { + if (key.Name == keyframe1) { + keyframe1Time = key.Time; + } + + if (key.Name == keyframe2) { + keyframe2Time = key.Time; + break; + } + } + + // Segment doesn't exist or is malformed + if (keyframe1Time == -1 || keyframe2Time == -1 || keyframe1Time > keyframe2Time) { + return -1; + } + + // Current time out of segment + if (lastSequenceTime < keyframe1Time || lastSequenceTime >= keyframe2Time) { + return -1; + } + + if (keyframe1Time == keyframe2Time) { + // Can only be in the segment, and at the end, or not in the segment + return lastSequenceTime == keyframe1Time ? 1 : -1; + } + + return (lastSequenceTime - keyframe1Time) / (keyframe2Time - keyframe1Time); + } + + /// + /// Processes a keyframe event. + /// + /// The current sequence time + /// The keyframe that was hit + /// The current animation speed + private void HitKeyframe(float sequenceTime, AnimationKey key, float speed) { + isHandlingKeyframe = true; + + DebugPrint($"Sequence '{PlayingSequence!.Name}' keyframe event '{key.Name}'"); + + actor.KeyframeEvent(key.Name); + + switch (key.Name) { + case "loopstart": + sequenceLoop = new LoopData(key.Time, 0); + break; + case "loopend": + sequenceLoop = new LoopData(sequenceLoop.start, key.Time); + sequenceLoopEndTick = actor.Field.FieldTick + (long)((key.Time - sequenceTime) / speed); + break; + case "end": + sequenceEnd = key.Time; + sequenceEndTick = actor.Field.FieldTick + (long)((key.Time - sequenceTime) / speed); + break; + default: + break; + } + } + + /// + /// Prints a debug message if debug printing is enabled. + /// + /// The message to print + private void DebugPrint(string message) { + if (debugPrintAnimations && actor is FieldPlayer player) { + player.Session.Send(NoticePacket.Message(message)); + } + } + + /// + /// Represents a loop section in an animation sequence + /// + private struct LoopData { + public float start; + public float end; + + public LoopData(float start, float end) { + this.start = start; + this.end = end; + } + } +} diff --git a/Maple2.File.Ingest/Maple2.File.Ingest.csproj b/Maple2.File.Ingest/Maple2.File.Ingest.csproj index 40f8c88f0..346a1d178 100644 --- a/Maple2.File.Ingest/Maple2.File.Ingest.csproj +++ b/Maple2.File.Ingest/Maple2.File.Ingest.csproj @@ -19,7 +19,7 @@ - + diff --git a/Maple2.File.Ingest/Mapper/AnimationMapper.cs b/Maple2.File.Ingest/Mapper/AnimationMapper.cs index 0689340fb..2160e4573 100644 --- a/Maple2.File.Ingest/Mapper/AnimationMapper.cs +++ b/Maple2.File.Ingest/Mapper/AnimationMapper.cs @@ -15,24 +15,22 @@ public AnimationMapper(M2dReader xmlReader) { protected override IEnumerable Map() { foreach (AnimationData data in parser.Parse()) { foreach (KeyFrameMotion kfm in data.kfm) { - IEnumerable<(string Name, AnimationSequence Sequence)> sequences = kfm.seq.Select(sequence => { + IEnumerable<(string Name, AnimationSequenceMetadata Sequence)> sequences = kfm.seq.Select(sequence => { List keys = sequence.key.Select(key => new AnimationKey(key.name, (float) key.time)).ToList(); return (sequence.name, - new AnimationSequence( + new AnimationSequenceMetadata( Name: sequence.name, Id: (short) sequence.id, - Time: (float) (sequence.key.FirstOrDefault(key => key.name == "end")?.time ?? default), keys) + Time: (float) (sequence.key.FirstOrDefault(key => key.name == "end")?.time ?? 0), keys) ); }); - var lookup = new Dictionary(); - foreach ((string name, AnimationSequence sequence) in sequences) { - if (lookup.ContainsKey(name)) { + var lookup = new Dictionary(); + foreach ((string name, AnimationSequenceMetadata sequence) in sequences) { + if (!lookup.TryAdd(name, sequence)) { Console.WriteLine($"Ignore Duplicate: {name} for {kfm.name}"); - continue; } - lookup.Add(name, sequence); } yield return new AnimationMetadata(kfm.name, lookup); diff --git a/Maple2.File.Ingest/MapperExtensions.cs b/Maple2.File.Ingest/MapperExtensions.cs index 292d1aabe..5ac806e08 100644 --- a/Maple2.File.Ingest/MapperExtensions.cs +++ b/Maple2.File.Ingest/MapperExtensions.cs @@ -1,4 +1,6 @@ -using System.Diagnostics; +using System.ComponentModel; +using System.Diagnostics; +using System.Reflection; using Maple2.File.Ingest.Utils; using Maple2.File.Parser.Xml; using Maple2.File.Parser.Xml.Common; @@ -317,17 +319,36 @@ public static SkillMetadataAutoTargeting Convert(this AutoTargeting autoTargetin } public static BeginCondition Convert(this Maple2.File.Parser.Xml.Skill.BeginCondition beginCondition) { + List dungeonGroupType = []; + foreach (Parser.Xml.Skill.BeginCondition.DungeonRoomGroupTypes type in beginCondition.requireDungeonRoomGroupTypes) { + if (Enum.TryParse(type.type, true, out DungeonGroupType groupType)) { + dungeonGroupType.Add(groupType); + } + } return new BeginCondition( Level: beginCondition.level, Gender: (Gender) beginCondition.gender, Mesos: beginCondition.money, Stat: beginCondition.stat.ToDictionary(), + Maps: beginCondition.requireMapCodes.Select(mapCodes => mapCodes.code).ToArray(), + MapTypes: beginCondition.requireMapCategoryCodes.Select(mapType => (MapType) mapType.code).ToArray(), + Continents: beginCondition.requireMapContinentCodes.Select(continent => (Continent) continent.code).ToArray(), + ActiveSkill: beginCondition.requireSkillCodes.Select(skill => skill.code).ToArray(), JobCode: beginCondition.job.Select(job => (JobCode) job.code).ToArray(), Probability: beginCondition.probability, CooldownTime: beginCondition.cooldownTime, + DurationWithoutMoving: (int) TimeSpan.FromSeconds(beginCondition.requireDurationWithoutMove).TotalMilliseconds, + DurationWithoutDamage: (int) TimeSpan.FromSeconds(beginCondition.requireDurationWithoutDamage).TotalMilliseconds, OnlyShadowWorld: beginCondition.onlyShadowWorld || beginCondition.isShadowWorld, OnlyFlyableMap: beginCondition.onlyFlyableMap, + OnlySurvival: beginCondition.allowMapleSurvival, AllowDead: beginCondition.allowDeadState, + AllowOnBattleMount: beginCondition.allowBattleRidingState, + OnlyOnBattleMount: beginCondition.onlyBattleRidingState, + DungeonGroupType: beginCondition.requireDungeonRoomGroupTypes + .Where(type => Enum.TryParse(type.type, true, out DungeonGroupType _)) + .Select(type => Enum.Parse(type.type, true)) + .ToArray(), Weapon: beginCondition.weapon.Select(weapon => new BeginConditionWeapon( new ItemType(1, (byte) weapon.lh), new ItemType(1, (byte) weapon.rh))).ToArray(), @@ -337,7 +358,7 @@ public static BeginCondition Convert(this Maple2.File.Parser.Xml.Skill.BeginCond } // We use this default to avoid writing useless checks - private static readonly BeginConditionTarget DefaultBeginConditionTarget = new(Array.Empty(), null); + private static readonly BeginConditionTarget DefaultBeginConditionTarget = new([], null, [], [], [], new Dictionary(), [], []); private static BeginConditionTarget? Convert(SubConditionTarget? target) { if (target == null) { return null; @@ -345,13 +366,39 @@ public static BeginCondition Convert(this Maple2.File.Parser.Xml.Skill.BeginCond var result = new BeginConditionTarget( Buff: ParseBuffs(target), - Event: ParseEvent(target)); + Event: ParseEvent(target), + Stat: ParseStat(target), + States: target.requireStates + .Select(state => Enum.GetValues() + .FirstOrDefault(enumValue => + enumValue.GetType() + .GetField(enumValue.ToString()) + ?.GetCustomAttribute() + ?.Description == state)) + .Where(state => state != ActorState.None) + .ToArray(), + SubStates: target.requireSubStates + .Select(state => Enum.GetValues() + .FirstOrDefault(enumValue => + enumValue.GetType() + .GetField(enumValue.ToString()) + ?.GetCustomAttribute() + ?.Description == state)) + .Where(state => state != ActorSubState.None) + .ToArray(), + Masteries: target.requireMasteryTypes + .Where(type => Enum.TryParse(type, true, out MasteryType _)) + .Select(type => Enum.Parse(type, true)) + .Zip(target.requireMasteryValues, (type, value) => (Type: type, Value: value)) + .ToDictionary(pair => pair.Type, pair => pair.Value), + NpcIds: target.NpcIDs, + HasNotBuffIds: target.hasNotBuffID); return DefaultBeginConditionTarget.Equals(result) ? null : result; BeginConditionTarget.HasBuff[] ParseBuffs(SubConditionTarget data) { if (data.hasBuffID.Length == 0 || data.hasBuffID[0] == 0) { - return Array.Empty(); + return []; } var hasBuff = new BeginConditionTarget.HasBuff[data.hasBuffID.Length]; @@ -383,6 +430,28 @@ BeginConditionTarget.HasBuff[] ParseBuffs(SubConditionTarget data) { SkillIds: data.eventSkillID, BuffIds: data.eventEffectID); } + + BeginConditionTarget.BeginConditionStat[] ParseStat(SubConditionTarget data) { + if (data.compareStat.Count == 0) { + return []; + } + + var stats = new BeginConditionTarget.BeginConditionStat[data.compareStat.Count]; + for (int i = 0; i < stats.Length; i++) { + foreach (BasicAttribute attribute in Enum.GetValues()) { + float value = data.compareStat[i][(byte) attribute]; + if (value != default) { + stats[i] = new BeginConditionTarget.BeginConditionStat( + Attribute: attribute, + Value: value, + Compare: data.compareStat.Count > i ? Enum.Parse(data.compareStat[i].func) : CompareType.Equals, + ValueType: (CompareStatValueType) data.compareStat[i].type); + break; + } + } + } + return stats; + } } public static Dictionary> ToDictionary(this IEnumerable entries) { diff --git a/Maple2.Model/Enum/CompareType.cs b/Maple2.Model/Enum/CompareType.cs index cdfeeedea..4ae5d4ba3 100644 --- a/Maple2.Model/Enum/CompareType.cs +++ b/Maple2.Model/Enum/CompareType.cs @@ -7,3 +7,8 @@ public enum CompareType { Greater = 3, GreaterEquals = 4, } + +public enum CompareStatValueType { + CurrentPercentage = 0, + TotalValue = 1, +} diff --git a/Maple2.Model/Game/Npc/Npc.cs b/Maple2.Model/Game/Npc/Npc.cs index 061b33f09..9a9608cd7 100644 --- a/Maple2.Model/Game/Npc/Npc.cs +++ b/Maple2.Model/Game/Npc/Npc.cs @@ -5,7 +5,7 @@ namespace Maple2.Model.Game; public class Npc { public readonly NpcMetadata Metadata; - public readonly IReadOnlyDictionary Animations; + public readonly IReadOnlyDictionary Animations; public int Id => Metadata.Id; @@ -13,6 +13,6 @@ public class Npc { public Npc(NpcMetadata metadata, AnimationMetadata? animation) { Metadata = metadata; - Animations = animation?.Sequences ?? new Dictionary(); + Animations = animation?.Sequences ?? new Dictionary(); } } diff --git a/Maple2.Model/Metadata/AnimationMetadata.cs b/Maple2.Model/Metadata/AnimationMetadata.cs index 03e216943..b5a36a0d8 100644 --- a/Maple2.Model/Metadata/AnimationMetadata.cs +++ b/Maple2.Model/Metadata/AnimationMetadata.cs @@ -2,8 +2,8 @@ namespace Maple2.Model.Metadata; -public record AnimationMetadata(string Model, IReadOnlyDictionary Sequences); +public record AnimationMetadata(string Model, IReadOnlyDictionary Sequences); -public record AnimationSequence(string Name, short Id, float Time, List? Keys); +public record AnimationSequenceMetadata(string Name, short Id, float Time, List Keys); public record AnimationKey(string Name, float Time); diff --git a/Maple2.Model/Metadata/BeginCondition.cs b/Maple2.Model/Metadata/BeginCondition.cs index e6a212a62..e8ae2967f 100644 --- a/Maple2.Model/Metadata/BeginCondition.cs +++ b/Maple2.Model/Metadata/BeginCondition.cs @@ -11,10 +11,20 @@ public record BeginCondition( JobCode[] JobCode, float Probability, float CooldownTime, + int DurationWithoutDamage, + int DurationWithoutMoving, bool OnlyShadowWorld, bool OnlyFlyableMap, + bool OnlySurvival, bool AllowDead, + bool AllowOnBattleMount, + bool OnlyOnBattleMount, + DungeonGroupType[] DungeonGroupType, IReadOnlyDictionary Stat, + int[] Maps, + MapType[] MapTypes, + Continent[] Continents, + int[] ActiveSkill, BeginConditionWeapon[]? Weapon, BeginConditionTarget? Target, BeginConditionTarget? Owner, @@ -26,7 +36,14 @@ public record BeginConditionWeapon( public record BeginConditionTarget( BeginConditionTarget.HasBuff[] Buff, - BeginConditionTarget.EventCondition? Event + BeginConditionTarget.EventCondition? Event, + BeginConditionTarget.BeginConditionStat[] Stat, + ActorState[] States, + ActorSubState[] SubStates, + IReadOnlyDictionary Masteries, + //string[] NpcTags // not used? + int[] NpcIds, + int[] HasNotBuffIds ) { public record HasBuff(int Id, short Level, bool Owned, int Count, CompareType Compare); @@ -35,4 +52,7 @@ public record HasBuff(int Id, short Level, bool Owned, int Count, CompareType Co // SkillIds => 4,6,7,14,20 // BuffIds => 16,17,102 public record EventCondition(EventConditionType Type, bool IgnoreOwner, int[] SkillIds, int[] BuffIds); + public record BeginConditionStat(BasicAttribute Attribute, float Value, CompareType Compare, CompareStatValueType ValueType); } + + diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index 310761e53..c178b005b 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -48,6 +48,11 @@ public virtual Vector3 Rotation { public virtual BuffManager Buffs { get; } + /// + /// Tick duration of actor in the same position. + /// + public (Vector3 Position, long LastTick, long Duration) PositionTick { get; set; } + protected Actor(FieldManager field, int objectId, T value, string modelName, NpcMetadataStorage npcMetadata) { Field = field; ObjectId = objectId; @@ -58,16 +63,7 @@ protected Actor(FieldManager field, int objectId, T value, string modelName, Npc AnimationState = new AnimationState(this, modelName); SkillState = new SkillState(this); Stats = new StatsManager(this); - } - - protected Actor(FieldManager field, int objectId, T value, string modelName) { - Field = field; - ObjectId = objectId; - Value = value; - Buffs = new BuffManager(this); - Transform = new Transform(); - AnimationState = new AnimationState(this, modelName); - SkillState = new SkillState(this); + PositionTick = new ValueTuple(Vector3.Zero, 0, 0); } public void Dispose() { @@ -206,6 +202,12 @@ public virtual void Update(long tickCount) { return; } + if (PositionTick.Position != Position) { + PositionTick = new ValueTuple(Position, tickCount, 0); + } else { + PositionTick = new ValueTuple(Position, PositionTick.LastTick, tickCount - PositionTick.LastTick); + } + AnimationState.Update(tickCount); Buffs.Update(tickCount); } diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationRecord.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationRecord.cs new file mode 100644 index 000000000..a6c0a1bf8 --- /dev/null +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationRecord.cs @@ -0,0 +1,61 @@ +using Maple2.Model.Metadata; +using Maple2.Server.Game.Model.Enum; + +namespace Maple2.Server.Game.Model.ActorStateComponent; + +public class AnimationRecord { + public AnimationSequenceMetadata? Sequence { get; set; } + public float Speed { get; set; } + public AnimationType Type { get; set; } + public float LastTime { get; set; } + public float EndTime { get; set; } + public LoopData Loop { get; set; } + public long EndTick { get; set; } + public long LoopEndTick { get; set; } + public bool IsLooping { get; set; } + public bool LoopOnlyOnce { get; set; } + public SkillMetadata? Skill { get; set; } + + public AnimationRecord() { + Speed = 1; + LastTime = 0; + Loop = new LoopData(0, 0); + IsLooping = false; + EndTime = 0; + Type = AnimationType.Misc; + } + + public AnimationRecord(AnimationSequenceMetadata sequence, float speed, AnimationType type, SkillMetadata? skill = null) : this() { + Sequence = sequence; + Speed = speed; + Type = type; + Skill = skill; + } + + public struct LoopData { + public float start; + public float end; + + public LoopData(float start, float end) { + this.start = start; + this.end = end; + } + } + + public struct TickPair { + public long server; + public long client; + + // mobs + public TickPair(long server) { + this.server = server; + this.client = server; + } + + // players + public TickPair(long server, long client) { + this.server = server; + this.client = client; + } + } +} diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs index 4a21cec4e..ffca3a5af 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs @@ -1,60 +1,30 @@ -using Maple2.Model.Metadata; +using Maple2.Model.Metadata; using Maple2.Server.Core.Packets; using Maple2.Server.Game.Model.Enum; namespace Maple2.Server.Game.Model.ActorStateComponent; +/// +/// Manages animation sequences for actors in the game. +/// public class AnimationState { private readonly IActor actor; + public AnimationRecord? Current; + private AnimationRecord? queued; - private struct LoopData { - public float start; - public float end; - - public LoopData(float start, float end) { - this.start = start; - this.end = end; - } - } - - public struct TickPair { - public long server; - public long client; - - // mobs - public TickPair(long server) { - this.server = server; - this.client = server; - } - - // players - public TickPair(long server, long client) { - this.server = server; - this.client = client; - } - } - - public AnimationMetadata? RigMetadata { get; init; } - public AnimationSequence? PlayingSequence { get; private set; } + public readonly AnimationMetadata? RigMetadata; + public AnimationSequenceMetadata? PlayingSequence => Current?.Sequence; public short IdleSequenceId { get; init; } - private AnimationSequence? queuedSequence; - private float queuedSequenceSpeed; - private AnimationType queuedSequenceType; - private bool queuedResetSequence = false; - private bool reportingKeyframeEvent; - private bool IsPlayerAnimation { get; set; } - public float MoveSpeed { get; set; } - public float AttackSpeed { get; set; } - public float SequenceSpeed { get; private set; } - private float lastSequenceTime { get; set; } - private float sequenceEnd { get; set; } - private LoopData sequenceLoop { get; set; } - private long sequenceEndTick { get; set; } - private long sequenceLoopEndTick { get; set; } - private long lastTick { get; set; } - private bool isLooping { get; set; } - private bool loopOnlyOnce { get; set; } - private AnimationType sequenceType { get; set; } + public float SequenceSpeed => Current?.Speed ?? 1.0f; + + private bool isHandlingKeyframe; + private bool IsPlayerAnimation => actor is FieldPlayer; + public float MoveSpeed { get; set; } = 1f; + public float AttackSpeed { get; set; } = 1f; + private float lastSequenceTime; + private long lastTick; + private long sequenceEndTick; + private long sequenceLoopEndTick; private bool debugPrintAnimations; public bool DebugPrintAnimations { @@ -66,17 +36,18 @@ public bool DebugPrintAnimations { } } + /// + /// Initializes a new instance of the AnimationState class. + /// + /// The actor this animation state belongs to + /// The model name to load animations for public AnimationState(IActor actor, string modelName) { this.actor = actor; RigMetadata = actor.NpcMetadata?.GetAnimation(modelName); - MoveSpeed = 1; - AttackSpeed = 1; - IsPlayerAnimation = actor is FieldPlayer; if (RigMetadata is null) { IdleSequenceId = 0; - return; } @@ -89,184 +60,231 @@ public AnimationState(IActor actor, string modelName) { IdleSequenceId = RigMetadata.Sequences.FirstOrDefault(sequence => sequence.Key == idleName).Value.Id; } + /// + /// Resets the current animation sequence. + /// private void ResetSequence() { - PlayingSequence = null; - SequenceSpeed = 1; + if (Current?.Sequence != null && actor is FieldNpc npc) { + npc.SendControl = true; + } + Current = null; lastSequenceTime = 0; - sequenceLoop = new LoopData(0, 0); lastTick = 0; - isLooping = false; - sequenceEnd = 0; - sequenceType = AnimationType.Misc; } - public bool TryPlaySequence(string name, float speed, AnimationType type) { - if (RigMetadata is null) { - return false; - } - - bool animationSequenceExists = RigMetadata.Sequences.TryGetValue(name, out AnimationSequence? sequence); - - if (reportingKeyframeEvent) { - queuedSequence = sequence; - queuedSequenceSpeed = speed; - queuedSequenceType = type; - - return animationSequenceExists; - } - - if (!animationSequenceExists) { + /// + /// Attempts to play an animation sequence with the specified name, speed, and type. + /// + /// The name of the animation sequence to play + /// The speed at which to play the animation + /// The type of animation (Move, Skill, or Misc) + /// Optional skill metadata associated with this animation + /// True if the sequence was found and started (or queued), false otherwise + public bool TryPlaySequence(string name, float speed, AnimationType type, SkillMetadata? skill = null) { + // Can't play animations without metadata + if (RigMetadata is null || !RigMetadata.Sequences.TryGetValue(name, out AnimationSequenceMetadata? sequence)) { DebugPrint($"Attempt to play nonexistent sequence '{name}' at x{speed} speed, previous: '{PlayingSequence?.Name ?? "none"}' x{SequenceSpeed}"); - - if (reportingKeyframeEvent) { - queuedResetSequence = true; - } else { - ResetSequence(); - } - + ResetSequence(); return false; } - PlaySequence(sequence!, speed, type); + // If we're currently processing a keyframe event, queue this sequence for later + if (isHandlingKeyframe) { + queued = new AnimationRecord(sequence, speed, type, skill); + return true; + } + // Play the sequence immediately + PlaySequence(sequence, speed, type, skill); return true; } - public bool TryPlaySequence(string name, float speed, AnimationType type, out AnimationSequence? sequence) { - if (!TryPlaySequence(name, speed, type)) { + /// + /// Attempts to play an animation sequence and returns the sequence metadata if successful. + /// + /// The name of the animation sequence to play + /// The speed at which to play the animation + /// The type of animation (Move, Skill, or Misc) + /// When this method returns, contains the animation sequence metadata if found; otherwise, null + /// Optional skill metadata associated with this animation + /// True if the sequence was found and started (or queued), false otherwise + public bool TryPlaySequence(string name, float speed, AnimationType type, out AnimationSequenceMetadata? sequence, SkillMetadata? skill = null) { + // Try to play the sequence + if (!TryPlaySequence(name, speed, type, skill)) { sequence = null; - return false; } - if (queuedSequence is not null) { - sequence = queuedSequence; - } else { - sequence = PlayingSequence; + // Return the appropriate sequence metadata + // If we're in a keyframe event, return the queued sequence metadata + // Otherwise, return the currently playing sequence metadata + sequence = queued?.Sequence ?? PlayingSequence; + + // Double-check that we have a valid sequence + if (sequence == null) { + DebugPrint($"Warning: Failed to get sequence metadata for '{name}' after successful TryPlaySequence"); + return false; } return true; } - private void PlaySequence(AnimationSequence sequence, float speed, AnimationType type) { - if (PlayingSequence != sequence && actor is FieldNpc npc) { + /// + /// Plays the specified animation sequence. + /// + /// The animation sequence to play + /// The speed at which to play the animation + /// The type of animation (Move, Skill, or Misc) + /// Optional skill metadata associated with this animation + private void PlaySequence(AnimationSequenceMetadata sequenceMetadata, float speed, AnimationType type, SkillMetadata? skill = null) { + // For NPCs, set SendControl flag when changing sequences + if (Current?.Sequence != sequenceMetadata && actor is FieldNpc npc) { npc.SendControl = true; } - ResetSequence(); + // Log the sequence change + DebugPrint($"Playing sequence '{sequenceMetadata.Name}' at x{speed} speed, previous: '{PlayingSequence?.Name ?? "none"}' x{SequenceSpeed}"); - DebugPrint($"Playing sequence '{sequence!.Name}' at x{speed} speed, previous: '{PlayingSequence?.Name ?? "none"}' x{SequenceSpeed}"); + // Reset current sequence state + ResetSequence(); - PlayingSequence = sequence; - SequenceSpeed = speed; - sequenceType = type; + // Set the new sequence properties + Current = new AnimationRecord(sequenceMetadata, speed, type, skill); + // Start tracking from current tick lastTick = actor.Field.FieldTick; } + /// + /// Cancels the currently playing animation sequence. + /// public void CancelSequence() { + // Log the cancellation if a sequence is playing if (PlayingSequence is not null) { DebugPrint($"Canceled playing sequence: '{PlayingSequence.Name}' x{SequenceSpeed}"); } - if (reportingKeyframeEvent) { - queuedResetSequence = true; - + // If we're processing a keyframe event, queue the reset for later + if (isHandlingKeyframe) { + queued = null; return; } - if (PlayingSequence != null && actor is FieldNpc npc) { - npc.SendControl = true; - } - + // Reset the sequence state ResetSequence(); } + /// + /// Updates the animation state based on the current tick count. + /// + /// The current server tick count public void Update(long tickCount) { + // Skip update if no animation metadata is available if (RigMetadata is null) { return; } - if (PlayingSequence?.Keys is null) { - + // Reset if no valid sequence is playing + if (PlayingSequence?.Keys.Count == 0) { ResetSequence(); - return; } - float sequenceSpeedModifier = sequenceType switch { + // Calculate the current sequence time + float sequenceSpeedModifier = Current?.Type switch { AnimationType.Move => MoveSpeed, AnimationType.Skill => AttackSpeed, - _ => 1 + _ => 1, }; long lastServerTick = lastTick == 0 ? tickCount : lastTick; float speed = SequenceSpeed * sequenceSpeedModifier / 1000; - float delta = (float) (tickCount - lastServerTick) * speed; + float delta = (float)(tickCount - lastServerTick) * speed; float sequenceTime = lastSequenceTime + delta; - foreach (AnimationKey key in PlayingSequence.Keys) { - if (HasHitKeyframe(sequenceTime, key)) { - HitKeyframe(sequenceTime, key, speed); + // Process keyframe events + if (PlayingSequence != null && PlayingSequence.Keys.Count > 0) { + foreach (AnimationKey key in PlayingSequence.Keys) { + if (HasHitKeyframe(sequenceTime, key)) { + HitKeyframe(sequenceTime, key, speed); + } } } - // TODO: maybe make client grace period ping based instead? - if (isLooping && sequenceLoop.end != 0 && sequenceTime > sequenceLoop.end) { + // Handle sequence looping + if (Current != null && Current.IsLooping && Current.Loop.end != 0 && sequenceTime > Current.Loop.end) { if (!IsPlayerAnimation || tickCount <= sequenceLoopEndTick + Constant.ClientGraceTimeTick) { - if (loopOnlyOnce) { - isLooping = false; - loopOnlyOnce = false; + if (Current.LoopOnlyOnce) { + Current.IsLooping = false; + Current.LoopOnlyOnce = false; } - sequenceTime -= sequenceLoop.end - sequenceLoop.start; - lastSequenceTime = sequenceTime - Math.Max(delta, sequenceTime - sequenceLoop.end + 0.001f); + sequenceTime -= Current.Loop.end - Current.Loop.start; + lastSequenceTime = sequenceTime - Math.Max(delta, sequenceTime - Current.Loop.end + 0.001f); - // play all keyframe events from loopstart to current - foreach (AnimationKey key in PlayingSequence.Keys) { - if (HasHitKeyframe(sequenceTime, key)) { - HitKeyframe(sequenceTime, key, speed); + // Play all keyframe events from loopstart to current + if (PlayingSequence?.Keys != null) { + foreach (AnimationKey key in PlayingSequence.Keys) { + if (HasHitKeyframe(sequenceTime, key)) { + HitKeyframe(sequenceTime, key, speed); + } } } } } - if (sequenceEnd != 0 && sequenceTime > sequenceEnd) { + // Check for sequence end + if (Current != null && Current.EndTime != 0 && sequenceTime > Current.EndTime) { if (!IsPlayerAnimation || tickCount <= sequenceEndTick + Constant.ClientGraceTimeTick) { ResetSequence(); } } + // Update timing state lastTick = tickCount; lastSequenceTime = sequenceTime; - reportingKeyframeEvent = false; + isHandlingKeyframe = false; - if (queuedResetSequence) { - if (PlayingSequence != null && actor is FieldNpc npc) { - npc.SendControl = true; - } - - ResetSequence(); - } else if (queuedSequence is not null) { - PlaySequence(queuedSequence, queuedSequenceSpeed, queuedSequenceType); + // Process queued actions + if (queued != null) { + PlaySequence(queued.Sequence!, queued.Speed, queued.Type); + queued = null; } - - queuedResetSequence = false; - queuedSequence = null; } + /// + /// Sets whether the current sequence should loop. + /// + /// Whether the sequence should loop + /// Whether the sequence should only loop once public void SetLoopSequence(bool shouldLoop, bool loopOnlyOnce) { - isLooping = shouldLoop; - this.loopOnlyOnce = loopOnlyOnce; + if (Current is null) { + return; + } + + Current.IsLooping = shouldLoop; + Current.LoopOnlyOnce = loopOnlyOnce; } + /// + /// Determines if a keyframe has been hit in the current update. + /// + /// The current sequence time + /// The keyframe to check + /// True if the keyframe has been hit, false otherwise private bool HasHitKeyframe(float sequenceTime, AnimationKey key) { - bool keyBeforeLoop = !isLooping || sequenceLoop.end == 0 || key.Time <= sequenceLoop.end + 0.001f; + bool keyBeforeLoop = !Current?.IsLooping ?? true || Current?.Loop.end == 0 || key.Time <= Current?.Loop.end + 0.001f; bool hitKeySinceLastTick = key.Time > lastSequenceTime && key.Time <= sequenceTime; return keyBeforeLoop && hitKeySinceLastTick; } + /// + /// Gets the normalized time within a segment defined by two keyframes. + /// + /// The name of the first keyframe + /// The name of the second keyframe + /// A value between 0 and 1 representing the position within the segment, or -1 if not in the segment public float GetSequenceSegmentTime(string keyframe1, string keyframe2) { if (PlayingSequence is null) { return -1; @@ -282,7 +300,6 @@ public float GetSequenceSegmentTime(string keyframe1, string keyframe2) { if (key.Name == keyframe2) { keyframe2Time = key.Time; - break; } } @@ -305,34 +322,57 @@ public float GetSequenceSegmentTime(string keyframe1, string keyframe2) { return (lastSequenceTime - keyframe1Time) / (keyframe2Time - keyframe1Time); } + /// + /// Processes a keyframe event. + /// + /// The current sequence time + /// The keyframe that was hit + /// The current animation speed private void HitKeyframe(float sequenceTime, AnimationKey key, float speed) { - reportingKeyframeEvent = true; + isHandlingKeyframe = true; - DebugPrint($"Sequence '{PlayingSequence!.Name}' keyframe event '{key.Name}'"); + if (PlayingSequence != null) { + DebugPrint($"Sequence '{PlayingSequence.Name}' keyframe event '{key.Name}'"); + } actor.KeyframeEvent(key.Name); + if (Current == null) return; + switch (key.Name) { case "loopstart": - sequenceLoop = new LoopData(key.Time, 0); + Current.Loop = new AnimationRecord.LoopData(key.Time, 0); break; case "loopend": - sequenceLoop = new LoopData(sequenceLoop.start, key.Time); - sequenceLoopEndTick = (long) ((sequenceTime - key.Time) / speed); - + Current.Loop = new AnimationRecord.LoopData(Current.Loop.start, key.Time); + Current.LoopEndTick = actor.Field.FieldTick + (long)((key.Time - sequenceTime) / speed); + sequenceLoopEndTick = Current.LoopEndTick; break; case "end": - sequenceEnd = key.Time; - sequenceEndTick = (long) ((sequenceTime - key.Time) / speed); + Current.EndTime = key.Time; + Current.EndTick = actor.Field.FieldTick + (long)((key.Time - sequenceTime) / speed); + sequenceEndTick = Current.EndTick; break; default: break; } } + /// + /// Prints a debug message if debug printing is enabled. + /// + /// The message to print private void DebugPrint(string message) { if (debugPrintAnimations && actor is FieldPlayer player) { player.Session.Send(NoticePacket.Message(message)); } } + + /// + /// Checks if an animation is currently playing. + /// + /// True if an animation is playing, false otherwise + public bool IsAnimationPlaying() { + return Current != null && PlayingSequence != null; + } } diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs index 1115aa043..6fafa3dae 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs @@ -14,7 +14,7 @@ public partial class MovementState { public ActorState State { get; private set; } = ActorState.None; public float Speed { get; private set; } public Vector3 Velocity { get; private set; } - private AnimationSequence? stateSequence; + private AnimationSequenceMetadata? stateSequence; #region LastTickData private float lastSpeed; private Vector3 lastVelocity; diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Walk.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Walk.cs index 4f619f6bf..09b1d671b 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Walk.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Walk.cs @@ -22,7 +22,7 @@ private enum WalkType { private (Vector3 start, Vector3 end) walkSegment; private bool walkSegmentSet; private bool walkLookWhenDone = false; - private AnimationSequence? walkSequence = null; + private AnimationSequenceMetadata? walkSequence = null; private float walkSpeed; private NpcTask? walkTask = null; diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs index 3ef9422b0..bbf960a74 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs @@ -97,7 +97,7 @@ private void SkillCast(NpcSkillCastTask task, int id, short level, long uid, byt return; } - if (!actor.AnimationState.TryPlaySequence(cast.Motion.MotionProperty.SequenceName, cast.Motion.MotionProperty.SequenceSpeed, AnimationType.Skill, out AnimationSequence? sequence)) { + if (!actor.AnimationState.TryPlaySequence(cast.Motion.MotionProperty.SequenceName, cast.Motion.MotionProperty.SequenceSpeed, AnimationType.Skill, out AnimationSequenceMetadata? sequence)) { task.Cancel(); return; diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index 4fcdde04f..113498150 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -72,10 +72,10 @@ public short SequenceId { ); public readonly AgentNavigation? Navigation; - public readonly AnimationSequence IdleSequence; - public readonly AnimationSequence? JumpSequence; - public readonly AnimationSequence? WalkSequence; - public readonly AnimationSequence? SpawnSequence; + public readonly AnimationSequenceMetadata IdleSequenceMetadata; + public readonly AnimationSequenceMetadata? JumpSequence; + public readonly AnimationSequenceMetadata? WalkSequence; + public readonly AnimationSequenceMetadata? SpawnSequence; private readonly WeightedSet defaultRoutines; public readonly AiState AiState; public readonly MovementState MovementState; @@ -95,7 +95,7 @@ public short SequenceId { public readonly Dictionary AiExtraData = new(); public FieldNpc(FieldManager field, int objectId, DtCrowdAgent? agent, Npc npc, string aiPath, string spawnAnimation = "", string? patrolDataUUID = null) : base(field, objectId, npc, npc.Metadata.Model.Name, field.NpcMetadata) { - IdleSequence = npc.Animations.GetValueOrDefault("Idle_A") ?? new AnimationSequence(string.Empty, -1, 1f, null); + IdleSequenceMetadata = npc.Animations.GetValueOrDefault("Idle_A") ?? new AnimationSequenceMetadata(string.Empty, -1, 1f, null); JumpSequence = npc.Animations.GetValueOrDefault("Jump_A") ?? npc.Animations.GetValueOrDefault("Jump_B"); WalkSequence = npc.Animations.GetValueOrDefault("Walk_A"); SpawnSequence = npc.Animations.GetValueOrDefault(spawnAnimation); @@ -227,7 +227,7 @@ public override void KeyframeEvent(string keyName) { } string routineName = defaultRoutines.Get(); - if (!Value.Animations.TryGetValue(routineName, out AnimationSequence? sequence)) { + if (!Value.Animations.TryGetValue(routineName, out AnimationSequenceMetadata? sequence)) { Logger.Error("Invalid routine: {Routine} for npc {NpcId}", routineName, Value.Metadata.Id); return MovementState.TryStandby(null, true); @@ -243,7 +243,7 @@ public override void KeyframeEvent(string keyName) { case { } when routineName.StartsWith("Run_"): return MovementState.TryMoveTo(Navigation?.GetRandomPatrolPoint() ?? Position, false, sequence.Name); case { }: - if (!Value.Animations.TryGetValue(routineName, out AnimationSequence? animationSequence)) { + if (!Value.Animations.TryGetValue(routineName, out AnimationSequenceMetadata? animationSequence)) { break; } return MovementState.TryEmote(animationSequence.Name, SpawnSequence is not null); @@ -258,7 +258,7 @@ public override void KeyframeEvent(string keyName) { MS2WayPoint currentWaypoint = Patrol!.WayPoints[currentWaypointIndex]; if (!string.IsNullOrEmpty(currentWaypoint.ArriveAnimation) && idleTask is not MovementState.NpcEmoteTask) { - if (Value.Animations.TryGetValue(currentWaypoint.ArriveAnimation, out AnimationSequence? arriveSequence)) { + if (Value.Animations.TryGetValue(currentWaypoint.ArriveAnimation, out AnimationSequenceMetadata? arriveSequence)) { return MovementState.TryEmote(arriveSequence.Name, false); } } @@ -266,7 +266,7 @@ public override void KeyframeEvent(string keyName) { NpcTask? approachTask = null; if (Navigation!.PathTo(currentWaypoint.Position)) { - if (Value.Animations.TryGetValue(currentWaypoint.ApproachAnimation, out AnimationSequence? patrolSequence)) { + if (Value.Animations.TryGetValue(currentWaypoint.ApproachAnimation, out AnimationSequenceMetadata? patrolSequence)) { approachTask = MovementState.TryMoveTo(currentWaypoint.Position, false, sequence: patrolSequence.Name); } else if (WalkSequence is not null) { approachTask = MovementState.TryMoveTo(currentWaypoint.Position, false, WalkSequence.Name); @@ -306,7 +306,7 @@ protected override void OnDeath() { } public virtual void Animate(string sequenceName, float duration = -1f) { - if (!Value.Animations.TryGetValue(sequenceName, out AnimationSequence? sequence)) { + if (!Value.Animations.TryGetValue(sequenceName, out AnimationSequenceMetadata? sequence)) { Logger.Error("Invalid sequence: {Sequence} for npc {NpcId}", sequenceName, Value.Metadata.Id); return; } diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs index ecb7e6fae..3b25df4c3 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs @@ -87,7 +87,7 @@ public DeathState DeathState { private readonly EventQueue scheduler; - public FieldPlayer(GameSession session, Player player) : base(session.Field!, player.ObjectId, player, GetPlayerModel(player.Character.Gender)) { + public FieldPlayer(GameSession session, Player player) : base(session.Field, player.ObjectId, player, GetPlayerModel(player.Character.Gender), session.NpcMetadata) { Session = session; regenStats = new Dictionary>(); diff --git a/Maple2.Server.Game/Model/Field/Actor/Routine/AnimateRoutine.cs b/Maple2.Server.Game/Model/Field/Actor/Routine/AnimateRoutine.cs index 4dc030d7c..42a78b68c 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Routine/AnimateRoutine.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Routine/AnimateRoutine.cs @@ -6,13 +6,13 @@ public class AnimateRoutine : NpcRoutine { private TimeSpan duration; private readonly bool complete = true; - public AnimateRoutine(FieldNpc npc, AnimationSequence sequence, float duration = -1f) : base(npc, sequence.Id) { + public AnimateRoutine(FieldNpc npc, AnimationSequenceMetadata sequenceMetadata, float duration = -1f) : base(npc, sequenceMetadata.Id) { if (duration != -1f) { this.duration = TimeSpan.FromMilliseconds(duration); complete = false; return; } - this.duration = TimeSpan.FromSeconds(sequence.Time); + this.duration = TimeSpan.FromSeconds(sequenceMetadata.Time); } public override Result Update(TimeSpan elapsed) { diff --git a/Maple2.Server.Game/Model/Field/Actor/Routine/JumpRoutine.cs b/Maple2.Server.Game/Model/Field/Actor/Routine/JumpRoutine.cs index 3f7e9bae6..9810b1caa 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Routine/JumpRoutine.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Routine/JumpRoutine.cs @@ -40,7 +40,7 @@ private JumpRoutine(FieldNpc npc, short sequenceId, Vector3 endPosition, float d this.duration = TimeSpan.FromSeconds(duration); Npc.State = new StateJumpNpc(Npc.Position, endPosition, duration, scale); - NextRoutine = () => new WaitRoutine(npc, npc.IdleSequence.Id, npc.IdleSequence.Time); + NextRoutine = () => new WaitRoutine(npc, npc.IdleSequenceMetadata.Id, npc.IdleSequenceMetadata.Time); } private JumpRoutine(FieldNpc npc, short sequenceId, float duration) : base(npc, sequenceId) { @@ -48,7 +48,7 @@ private JumpRoutine(FieldNpc npc, short sequenceId, float duration) : base(npc, this.duration = TimeSpan.FromSeconds(duration); Npc.State = new StateJumpNpc(Npc.Position); - NextRoutine = () => new WaitRoutine(npc, npc.IdleSequence.Id, npc.IdleSequence.Time); + NextRoutine = () => new WaitRoutine(npc, npc.IdleSequenceMetadata.Id, npc.IdleSequenceMetadata.Time); } public override Result Update(TimeSpan elapsed) { diff --git a/Maple2.Server.Game/Model/Field/Actor/Routine/MoveRoutine.cs b/Maple2.Server.Game/Model/Field/Actor/Routine/MoveRoutine.cs index 321fcb7fd..c174e8b6c 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Routine/MoveRoutine.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Routine/MoveRoutine.cs @@ -10,7 +10,7 @@ public static NpcRoutine Walk(FieldNpc npc, short sequenceId) { try { return new MoveRoutine(npc, sequenceId, npc.Value.Metadata.Action.WalkSpeed); } catch (ArgumentException) { - return new WaitRoutine(npc, npc.IdleSequence.Id, npc.IdleSequence.Time); + return new WaitRoutine(npc, npc.IdleSequenceMetadata.Id, npc.IdleSequenceMetadata.Time); } } @@ -18,7 +18,7 @@ public static NpcRoutine Run(FieldNpc npc, short sequenceId) { try { return new MoveRoutine(npc, sequenceId, npc.Value.Metadata.Action.RunSpeed); } catch (ArgumentException) { - return new WaitRoutine(npc, npc.IdleSequence.Id, npc.IdleSequence.Time); + return new WaitRoutine(npc, npc.IdleSequenceMetadata.Id, npc.IdleSequenceMetadata.Time); } } diff --git a/Maple2.Server.Game/Model/Field/Actor/Routine/NpcRoutine.cs b/Maple2.Server.Game/Model/Field/Actor/Routine/NpcRoutine.cs index 328de36c9..75c83973b 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Routine/NpcRoutine.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Routine/NpcRoutine.cs @@ -31,7 +31,7 @@ public virtual void OnCompleted() { Completed = true; // Force Idle on completion - Npc.SequenceId = Npc.IdleSequence.Id; + Npc.SequenceId = Npc.IdleSequenceMetadata.Id; if (Npc.State.State != ActorState.Idle) { Npc.State = new NpcState(); } diff --git a/Maple2.Server.Game/Model/Skill/SkillQueue.cs b/Maple2.Server.Game/Model/Skill/SkillQueue.cs index 19075ab8a..67ec12feb 100644 --- a/Maple2.Server.Game/Model/Skill/SkillQueue.cs +++ b/Maple2.Server.Game/Model/Skill/SkillQueue.cs @@ -40,6 +40,10 @@ public void Remove(long uid) { } } + public bool Contains(int skillId) { + return casts.Any(cast => cast?.SkillId == skillId); + } + public void Clear() { for (int i = 0; i < MAX_PENDING; i++) { casts[i] = null; diff --git a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs index c0c216387..241f85ca9 100644 --- a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs @@ -158,7 +158,7 @@ private void HandleUse(GameSession session, IByteReader packet) { SkillMetadataMotionProperty motion = metadata.Data.Motions.First().MotionProperty; - session.Player.AnimationState.TryPlaySequence(motion.SequenceName, motion.SequenceSpeed, AnimationType.Skill); + session.Player.AnimationState.TryPlaySequence(motion.SequenceName, motion.SequenceSpeed, AnimationType.Skill, metadata); long startTick = session.Field.FieldTick; foreach (SkillEffectMetadata effect in metadata.Data.Skills) { diff --git a/Maple2.Server.Game/Util/SkillUtils.cs b/Maple2.Server.Game/Util/SkillUtils.cs index 6f9a36697..a38987416 100644 --- a/Maple2.Server.Game/Util/SkillUtils.cs +++ b/Maple2.Server.Game/Util/SkillUtils.cs @@ -3,6 +3,7 @@ using Maple2.Model.Enum; using Maple2.Model.Game; using Maple2.Model.Metadata; +using Maple2.Server.Game.Manager.Field; using Maple2.Server.Game.Model; using Maple2.Tools.Collision; @@ -81,6 +82,9 @@ public static bool Check(this BeginCondition condition, IActor caster, IActor ow if (condition.OnlyFlyableMap && !caster.Field.Metadata.Property.CanFly) { return false; } + if (condition.OnlySurvival && !(caster.Field.Metadata.Property.Type is MapType.SurvivalTeam or MapType.SurvivalSolo)) { + return false; + } if (player.Value.Character.Level < condition.Level) { return false; } @@ -103,6 +107,28 @@ public static bool Check(this BeginCondition condition, IActor caster, IActor ow return false; } } + if (condition.DurationWithoutMoving > 0) { + if (player.PositionTick.Duration < condition.DurationWithoutMoving) { + return false; + } + } + if (condition.Maps.Length > 0 && !condition.Maps.Contains(caster.Field.MapId)) { + return false; + } + if (condition.Maps.Length > 0 && !condition.MapTypes.Contains(caster.Field.Metadata.Property.Type)) { + return false; + } + if (condition.Maps.Length > 0 && !condition.Continents.Contains(caster.Field.Metadata.Property.Continent)) { + return false; + } + if (condition.DungeonGroupType.Length > 0 && + (caster.Field is not DungeonFieldManager dungeonFieldManager || + !condition.DungeonGroupType.Contains(dungeonFieldManager.DungeonMetadata.GroupType))) { + return false; + } + if (condition.ActiveSkill.Length > 0 && condition.ActiveSkill.All(id => owner.AnimationState.Current?.Skill?.Id != id)) { + return false; + } } return condition.Caster.Check(caster) && condition.Owner.Check(owner) && condition.Target.Check(target); @@ -137,6 +163,49 @@ private static bool Check(this BeginConditionTarget? condition, IActor target) { } } + foreach ((BasicAttribute attribute, float value, CompareType compare, CompareStatValueType valueType) in condition.Stat) { + float targetValue = valueType switch { + CompareStatValueType.CurrentPercentage => (float) target.Stats.Values[attribute].Current / target.Stats.Values[attribute].Total, + CompareStatValueType.TotalValue => target.Stats.Values[attribute].Total, + _ => 0, + }; + + bool compareResult = compare switch { + CompareType.Equals => targetValue == value, + CompareType.Less => targetValue < value, + CompareType.LessEquals => targetValue <= value, + CompareType.Greater => targetValue > value, + CompareType.GreaterEquals => targetValue >= value, + _ => true, + }; + if (!compareResult) { + return false; + } + } + + if (condition.HasNotBuffIds.Length > 0 && condition.HasNotBuffIds.Any(id => target.Buffs.HasBuff(id))) { + return false; + } + + // Verify if these conditions are only for player. + if (target is FieldPlayer player) { + if (condition.States.Length > 0 && !condition.States.Contains(player.State)) { + return false; + } + + if (condition.SubStates.Length > 0 && !condition.SubStates.Contains(player.SubState)) { + return false; + } + + if (condition.Masteries.Count > 0 && !condition.Masteries.All(mastery => player.Session.Mastery[mastery.Key] >= mastery.Value)) { + return false; + } + } else if (target is FieldNpc npc) { + if (condition.NpcIds.Length > 0 && !condition.NpcIds.Contains(npc.Value.Id)) { + return false; + } + } + return true; } From 98bca49248f36bbf7fc435baf50ef7389e2ca714 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Wed, 9 Apr 2025 21:48:58 -0700 Subject: [PATCH 2/9] Additional Fixes on Animation --- Maple2.Server.Game/Commands/DebugCommand.cs | 3 +- .../AnimationManager.cs} | 54 +++++++++++++------ .../Field/FieldManager/FieldManager.State.cs | 1 + Maple2.Server.Game/Model/Field/Actor/Actor.cs | 8 +-- .../ActorStateComponent/MovementState.cs | 16 +++--- .../MovementState.Emote.cs | 2 +- .../MovementState.SkillCast.cs | 6 +-- .../MovementStateStates/MovementState.Walk.cs | 8 +-- .../MovementState.EmoteTask.cs | 4 +- .../MovementState.SkillCastTask.cs | 2 +- .../Model/Field/Actor/FieldActor.cs | 4 +- .../Model/Field/Actor/FieldNpc.cs | 2 +- .../Model/Field/Actor/FieldPlayer.cs | 14 ++--- .../Model/Field/Actor/IActor.cs | 2 +- Maple2.Server.Game/Model/Field/Buff.cs | 3 ++ .../PacketHandlers/SkillHandler.cs | 12 ++--- .../Packets/NpcControlPacket.cs | 8 +-- Maple2.Server.Game/Session/GameSession.cs | 2 + Maple2.Server.Game/Util/SkillUtils.cs | 2 +- 19 files changed, 89 insertions(+), 64 deletions(-) rename Maple2.Server.Game/{Model/Field/Actor/ActorStateComponent/AnimationState.cs => Manager/AnimationManager.cs} (89%) diff --git a/Maple2.Server.Game/Commands/DebugCommand.cs b/Maple2.Server.Game/Commands/DebugCommand.cs index 8d68378ec..8673a1831 100644 --- a/Maple2.Server.Game/Commands/DebugCommand.cs +++ b/Maple2.Server.Game/Commands/DebugCommand.cs @@ -10,6 +10,7 @@ using System.Numerics; using Maple2.Model.Common; using System; +using Maple2.Server.Game.Model; namespace Maple2.Server.Game.Commands; @@ -94,7 +95,7 @@ public DebugAnimationCommand(GameSession session) : base("anims", "Prints player } private void Handle(InvocationContext ctx, bool? enabled) { - session.Player.AnimationState.DebugPrintAnimations = enabled ?? true; + session.Player.Animation.DebugPrintAnimations = enabled ?? true; string message = enabled ?? true ? "Enabled" : "Disabled"; ctx.Console.Out.WriteLine($"{message} animation debug info printing"); diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs b/Maple2.Server.Game/Manager/AnimationManager.cs similarity index 89% rename from Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs rename to Maple2.Server.Game/Manager/AnimationManager.cs index ffca3a5af..fcf7cde08 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs +++ b/Maple2.Server.Game/Manager/AnimationManager.cs @@ -1,14 +1,18 @@ -using Maple2.Model.Metadata; +using Maple2.Model.Enum; +using Maple2.Model.Metadata; using Maple2.Server.Core.Packets; +using Maple2.Server.Game.Model; +using Maple2.Server.Game.Model.ActorStateComponent; using Maple2.Server.Game.Model.Enum; +using Maple2.Server.Game.Session; -namespace Maple2.Server.Game.Model.ActorStateComponent; +namespace Maple2.Server.Game.Manager; /// /// Manages animation sequences for actors in the game. /// -public class AnimationState { - private readonly IActor actor; +public class AnimationManager { + private IActor Actor { get; set; } public AnimationRecord? Current; private AnimationRecord? queued; @@ -18,7 +22,7 @@ public class AnimationState { public float SequenceSpeed => Current?.Speed ?? 1.0f; private bool isHandlingKeyframe; - private bool IsPlayerAnimation => actor is FieldPlayer; + private bool IsPlayerAnimation => Actor is FieldPlayer; public float MoveSpeed { get; set; } = 1f; public float AttackSpeed { get; set; } = 1f; private float lastSequenceTime; @@ -30,7 +34,7 @@ public class AnimationState { public bool DebugPrintAnimations { get { return debugPrintAnimations; } set { - if (actor is FieldPlayer) { + if (Actor is FieldPlayer) { debugPrintAnimations = value; } } @@ -41,10 +45,13 @@ public bool DebugPrintAnimations { /// /// The actor this animation state belongs to /// The model name to load animations for - public AnimationState(IActor actor, string modelName) { - this.actor = actor; + public AnimationManager(IActor actor) { + Actor = actor; - RigMetadata = actor.NpcMetadata?.GetAnimation(modelName); + RigMetadata = actor switch { + FieldNpc fieldNpc => actor.Field.NpcMetadata.GetAnimation(fieldNpc.Value.Metadata.Model.Name), + _ => null, + }; if (RigMetadata is null) { IdleSequenceId = 0; @@ -60,11 +67,26 @@ public AnimationState(IActor actor, string modelName) { IdleSequenceId = RigMetadata.Sequences.FirstOrDefault(sequence => sequence.Key == idleName).Value.Id; } + public AnimationManager(GameSession session) { + Actor = session.Player; + string model = session.Player.Value.Character.Gender == Gender.Male ? "male" : "female"; + RigMetadata = session.NpcMetadata.GetAnimation(model); + + if (RigMetadata is null) { + throw new Exception("Failed to initialize AnimationState, could not find metadata for player model " + model); + } + IdleSequenceId = RigMetadata!.Sequences.FirstOrDefault(sequence => sequence.Key == "Idle_A").Value.Id; + } + + public void ResetActor(IActor actor) { + Actor = actor; + } + /// /// Resets the current animation sequence. /// private void ResetSequence() { - if (Current?.Sequence != null && actor is FieldNpc npc) { + if (Current?.Sequence != null && Actor is FieldNpc npc) { npc.SendControl = true; } Current = null; @@ -138,7 +160,7 @@ public bool TryPlaySequence(string name, float speed, AnimationType type, out An /// Optional skill metadata associated with this animation private void PlaySequence(AnimationSequenceMetadata sequenceMetadata, float speed, AnimationType type, SkillMetadata? skill = null) { // For NPCs, set SendControl flag when changing sequences - if (Current?.Sequence != sequenceMetadata && actor is FieldNpc npc) { + if (Current?.Sequence != sequenceMetadata && Actor is FieldNpc npc) { npc.SendControl = true; } @@ -152,7 +174,7 @@ private void PlaySequence(AnimationSequenceMetadata sequenceMetadata, float spee Current = new AnimationRecord(sequenceMetadata, speed, type, skill); // Start tracking from current tick - lastTick = actor.Field.FieldTick; + lastTick = Actor.Field.FieldTick; } /// @@ -335,7 +357,7 @@ private void HitKeyframe(float sequenceTime, AnimationKey key, float speed) { DebugPrint($"Sequence '{PlayingSequence.Name}' keyframe event '{key.Name}'"); } - actor.KeyframeEvent(key.Name); + Actor.KeyframeEvent(key.Name); if (Current == null) return; @@ -345,12 +367,12 @@ private void HitKeyframe(float sequenceTime, AnimationKey key, float speed) { break; case "loopend": Current.Loop = new AnimationRecord.LoopData(Current.Loop.start, key.Time); - Current.LoopEndTick = actor.Field.FieldTick + (long)((key.Time - sequenceTime) / speed); + Current.LoopEndTick = Actor.Field.FieldTick + (long)((key.Time - sequenceTime) / speed); sequenceLoopEndTick = Current.LoopEndTick; break; case "end": Current.EndTime = key.Time; - Current.EndTick = actor.Field.FieldTick + (long)((key.Time - sequenceTime) / speed); + Current.EndTick = Actor.Field.FieldTick + (long)((key.Time - sequenceTime) / speed); sequenceEndTick = Current.EndTick; break; default: @@ -363,7 +385,7 @@ private void HitKeyframe(float sequenceTime, AnimationKey key, float speed) { /// /// The message to print private void DebugPrint(string message) { - if (debugPrintAnimations && actor is FieldPlayer player) { + if (debugPrintAnimations && Actor is FieldPlayer player) { player.Session.Send(NoticePacket.Message(message)); } } diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index dd018e009..fb289a519 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -71,6 +71,7 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId }; session.Stats.ResetActor(fieldPlayer); session.Buffs.ResetActor(fieldPlayer); + session.Animation.ResetActor(fieldPlayer); // Use Portal if needed. if (fieldPlayer.Position == default && Entities.Portals.TryGetValue(portalId, out Portal? portal)) { diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index c178b005b..d8f39f6cf 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -40,7 +40,7 @@ public virtual Vector3 Rotation { set => Transform.RotationAnglesDegrees = value; } public Transform Transform { get; init; } - public AnimationState AnimationState { get; init; } + public virtual AnimationManager Animation { get; } public SkillState SkillState { get; init; } public virtual bool IsDead { get; protected set; } @@ -53,14 +53,14 @@ public virtual Vector3 Rotation { /// public (Vector3 Position, long LastTick, long Duration) PositionTick { get; set; } - protected Actor(FieldManager field, int objectId, T value, string modelName, NpcMetadataStorage npcMetadata) { + protected Actor(FieldManager field, int objectId, T value, NpcMetadataStorage npcMetadata) { Field = field; ObjectId = objectId; Value = value; Buffs = new BuffManager(this); Transform = new Transform(); NpcMetadata = npcMetadata; - AnimationState = new AnimationState(this, modelName); + Animation = new AnimationManager(this); SkillState = new SkillState(this); Stats = new StatsManager(this); PositionTick = new ValueTuple(Vector3.Zero, 0, 0); @@ -208,7 +208,7 @@ public virtual void Update(long tickCount) { PositionTick = new ValueTuple(Position, PositionTick.LastTick, tickCount - PositionTick.LastTick); } - AnimationState.Update(tickCount); + Animation.Update(tickCount); Buffs.Update(tickCount); } diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs index 6fafa3dae..f963fb432 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs @@ -37,7 +37,7 @@ public partial class MovementState { public MovementState(FieldNpc actor) { this.actor = actor; - hasIdleA = actor.AnimationState.RigMetadata?.Sequences?.ContainsKey("Idle_A") ?? false; + hasIdleA = actor.Animation.RigMetadata?.Sequences?.ContainsKey("Idle_A") ?? false; aniSpeed = actor.Value.Metadata.Model.AniSpeed; SetState(ActorState.Spawn); @@ -141,10 +141,10 @@ private void Idle(string sequence = "") { SetState(ActorState.Idle); if (hasIdleA) { - if (actor.AnimationState.TryPlaySequence(sequence, aniSpeed, AnimationType.Misc)) { - stateSequence = actor.AnimationState.PlayingSequence; - } else if (setAttackIdle && actor.AnimationState.TryPlaySequence("Idle_A", aniSpeed, AnimationType.Misc)) { - stateSequence = actor.AnimationState.PlayingSequence; + if (actor.Animation.TryPlaySequence(sequence, aniSpeed, AnimationType.Misc)) { + stateSequence = actor.Animation.PlayingSequence; + } else if (setAttackIdle && actor.Animation.TryPlaySequence("Idle_A", aniSpeed, AnimationType.Misc)) { + stateSequence = actor.Animation.PlayingSequence; } } } @@ -182,7 +182,7 @@ public void KeyframeEvent(string keyName) { } public void Update(long tickCount) { - if (actor.AnimationState.PlayingSequence != stateSequence) { + if (actor.Animation.PlayingSequence != stateSequence) { Idle(); } @@ -203,10 +203,10 @@ public void Update(long tickCount) { StateWalkUpdate(tickCount, tickDelta); break; case ActorState.Spawn: - if (actor.AnimationState.TryPlaySequence("Regen_A", aniSpeed, AnimationType.Misc)) { + if (actor.Animation.TryPlaySequence("Regen_A", aniSpeed, AnimationType.Misc)) { SetState(ActorState.Regen); - stateSequence = actor.AnimationState.PlayingSequence; + stateSequence = actor.Animation.PlayingSequence; } else { Idle(); } diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Emote.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Emote.cs index be93d64a4..7cd3c5614 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Emote.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Emote.cs @@ -16,7 +16,7 @@ public void StateEmoteEvent(string keyName) { case "end": if (emoteLimitTick != 0) { if (emoteActionTask is NpcEmoteTask emoteTask) { - actor.AnimationState.TryPlaySequence(emoteTask.Sequence, 1, AnimationType.Misc); + actor.Animation.TryPlaySequence(emoteTask.Sequence, 1, AnimationType.Misc); } return; } diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.SkillCast.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.SkillCast.cs index 6911216fe..46b7c0474 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.SkillCast.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.SkillCast.cs @@ -23,8 +23,8 @@ public void StateSkillEvent(string keyName) { actor.AppendDebugMessage("Skill Keyframe: " + keyName); - if (actor.AnimationState.PlayingSequence is not null) { - actor.AppendDebugMessage(actor.AnimationState.PlayingSequence.Name); + if (actor.Animation.PlayingSequence is not null) { + actor.AppendDebugMessage(actor.Animation.PlayingSequence.Name); } if (castSkill.Motion.AttackPoints.TryGetValue(keyName, out byte point)) { @@ -104,7 +104,7 @@ private void StateSkillCastMoveUpdate(long tickCount, long tickDelta, float delt castMoveLastTick = -1; } else { - castMoveTick = actor.AnimationState.GetSequenceSegmentTime(castMoveStartKeyframe, castMoveEndKeyframe); + castMoveTick = actor.Animation.GetSequenceSegmentTime(castMoveStartKeyframe, castMoveEndKeyframe); if (castMoveFinished) { castMoveTick = 1; diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Walk.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Walk.cs index 09b1d671b..0bf75bf25 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Walk.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Walk.cs @@ -45,8 +45,8 @@ private void StartWalking(string sequence, NpcTask task) { baseSpeed = isWalking ? actor.Value.Metadata.Action.WalkSpeed : actor.Value.Metadata.Action.RunSpeed; - if (actor.AnimationState.PlayingSequence?.Name == sequence || actor.AnimationState.TryPlaySequence(sequence, aniSpeed * Speed, AnimationType.Misc)) { - stateSequence = actor.AnimationState.PlayingSequence; + if (actor.Animation.PlayingSequence?.Name == sequence || actor.Animation.TryPlaySequence(sequence, aniSpeed * Speed, AnimationType.Misc)) { + stateSequence = actor.Animation.PlayingSequence; walkSequence = stateSequence; walkTask = task; @@ -166,8 +166,8 @@ public void StateWalkEvent(string keyName) { switch (keyName) { case "end": if (State == ActorState.Walk) { - if (actor.AnimationState.TryPlaySequence(stateSequence!.Name, aniSpeed * Speed, AnimationType.Misc)) { - stateSequence = actor.AnimationState.PlayingSequence; + if (actor.Animation.TryPlaySequence(stateSequence!.Name, aniSpeed * Speed, AnimationType.Misc)) { + stateSequence = actor.Animation.PlayingSequence; } return; diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.EmoteTask.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.EmoteTask.cs index 6956d0046..b4c615949 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.EmoteTask.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.EmoteTask.cs @@ -36,7 +36,7 @@ private void Emote(NpcTask task, string sequence, bool isIdle, float duration) { return; } - if (!actor.AnimationState.TryPlaySequence(sequence, 1, AnimationType.Misc)) { + if (!actor.Animation.TryPlaySequence(sequence, 1, AnimationType.Misc)) { task.Cancel(); return; } @@ -46,7 +46,7 @@ private void Emote(NpcTask task, string sequence, bool isIdle, float duration) { } emoteActionTask = task; - stateSequence = actor.AnimationState.PlayingSequence; + stateSequence = actor.Animation.PlayingSequence; SetState(isIdle ? ActorState.Idle : ActorState.Emotion); } diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs index bbf960a74..13ec26981 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs @@ -97,7 +97,7 @@ private void SkillCast(NpcSkillCastTask task, int id, short level, long uid, byt return; } - if (!actor.AnimationState.TryPlaySequence(cast.Motion.MotionProperty.SequenceName, cast.Motion.MotionProperty.SequenceSpeed, AnimationType.Skill, out AnimationSequenceMetadata? sequence)) { + if (!actor.Animation.TryPlaySequence(cast.Motion.MotionProperty.SequenceName, cast.Motion.MotionProperty.SequenceSpeed, AnimationType.Skill, out AnimationSequenceMetadata? sequence)) { task.Cancel(); return; diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldActor.cs b/Maple2.Server.Game/Model/Field/Actor/FieldActor.cs index 70b654680..4873897a0 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldActor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldActor.cs @@ -25,7 +25,7 @@ internal sealed class FieldActor : IActor { public Transform Transform { get; init; } public BuffManager Buffs { get; } public StatsManager Stats { get; } - public AnimationState AnimationState { get; init; } + public AnimationManager Animation { get; init; } public SkillState SkillState { get; init; } public FieldActor(FieldManager field, NpcMetadataStorage npcMetadata) { @@ -34,7 +34,7 @@ public FieldActor(FieldManager field, NpcMetadataStorage npcMetadata) { Buffs = new BuffManager(this); Transform = new Transform(); NpcMetadata = npcMetadata; - AnimationState = new AnimationState(this, string.Empty); // not meant to have animations + Animation = new AnimationManager(this); // not meant to have animations SkillState = new SkillState(this); } diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index 113498150..b9aa32796 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -94,7 +94,7 @@ public short SequenceId { public readonly Dictionary AiExtraData = new(); - public FieldNpc(FieldManager field, int objectId, DtCrowdAgent? agent, Npc npc, string aiPath, string spawnAnimation = "", string? patrolDataUUID = null) : base(field, objectId, npc, npc.Metadata.Model.Name, field.NpcMetadata) { + public FieldNpc(FieldManager field, int objectId, DtCrowdAgent? agent, Npc npc, string aiPath, string spawnAnimation = "", string? patrolDataUUID = null) : base(field, objectId, npc, field.NpcMetadata) { IdleSequenceMetadata = npc.Animations.GetValueOrDefault("Idle_A") ?? new AnimationSequenceMetadata(string.Empty, -1, 1f, null); JumpSequence = npc.Animations.GetValueOrDefault("Jump_A") ?? npc.Animations.GetValueOrDefault("Jump_B"); WalkSequence = npc.Animations.GetValueOrDefault("Walk_A"); diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs index 3b25df4c3..4a3913655 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs @@ -18,6 +18,7 @@ public class FieldPlayer : Actor { public override StatsManager Stats => Session.Stats; public override BuffManager Buffs => Session.Buffs; + public override AnimationManager Animation => Session.Animation; public override IPrism Shape => new Prism(new Circle(new Vector2(Position.X, Position.Y), 10), Position.Z, 100); private ActorState state; public ActorState State { @@ -87,7 +88,7 @@ public DeathState DeathState { private readonly EventQueue scheduler; - public FieldPlayer(GameSession session, Player player) : base(session.Field, player.ObjectId, player, GetPlayerModel(player.Character.Gender), session.NpcMetadata) { + public FieldPlayer(GameSession session, Player player) : base(session.Field, player.ObjectId, player, session.NpcMetadata) { Session = session; regenStats = new Dictionary>(); @@ -101,14 +102,6 @@ public FieldPlayer(GameSession session, Player player) : base(session.Field, pla scheduler.Start(); } - private static string GetPlayerModel(Gender gender) { - return gender switch { - Gender.Male => "male", - Gender.Female => "female", - _ => "male" - }; - } - protected override void Dispose(bool disposing) { scheduler.Stop(); } @@ -192,6 +185,9 @@ public override void Update(long tickCount) { } Session.GameEvent.Update(tickCount); + + //Console.WriteLine($"Playing sequence: {AnimationState.PlayingSequence?.Name ?? "null"}"); + Console.WriteLine($"Current Animation: {Animation.Current?.Sequence?.Name ?? "null"} Skill: {Animation.Current?.Skill?.Id ?? 0}"); } public void OnStateSync(StateSync stateSync) { diff --git a/Maple2.Server.Game/Model/Field/Actor/IActor.cs b/Maple2.Server.Game/Model/Field/Actor/IActor.cs index e7a0f0d1b..ea1a4c085 100644 --- a/Maple2.Server.Game/Model/Field/Actor/IActor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/IActor.cs @@ -15,7 +15,7 @@ public interface IActor : IFieldEntity { public BuffManager Buffs { get; } public StatsManager Stats { get; } - public AnimationState AnimationState { get; init; } + public AnimationManager Animation { get; } public SkillState SkillState { get; init; } public bool IsDead { get; } diff --git a/Maple2.Server.Game/Model/Field/Buff.cs b/Maple2.Server.Game/Model/Field/Buff.cs index 35c064859..df80d7276 100644 --- a/Maple2.Server.Game/Model/Field/Buff.cs +++ b/Maple2.Server.Game/Model/Field/Buff.cs @@ -104,6 +104,9 @@ public virtual void Update(long tickCount) { } public bool UpdateEnabled(bool notifyField = true) { + if (Id == 11000081) { + Console.WriteLine($"Striker buff"); + } bool enabled = Metadata.Condition.Check(Caster, Owner, Owner); if (Enabled != enabled) { Enabled = enabled; diff --git a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs index 241f85ca9..01cc16faf 100644 --- a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs @@ -158,7 +158,7 @@ private void HandleUse(GameSession session, IByteReader packet) { SkillMetadataMotionProperty motion = metadata.Data.Motions.First().MotionProperty; - session.Player.AnimationState.TryPlaySequence(motion.SequenceName, motion.SequenceSpeed, AnimationType.Skill, metadata); + session.Animation.TryPlaySequence(motion.SequenceName, motion.SequenceSpeed, AnimationType.Skill, metadata); long startTick = session.Field.FieldTick; foreach (SkillEffectMetadata effect in metadata.Data.Skills) { @@ -335,7 +335,7 @@ private void HandleSync(GameSession session, IByteReader packet) { return; } - if (session.Player.AnimationState.PlayingSequence is null) { + if (session.Player.Animation.PlayingSequence is null) { Logger.Warning($"Last motion already expired on skill {skillUid}"); } @@ -357,7 +357,7 @@ private void HandleSync(GameSession session, IByteReader packet) { SkillMetadataMotionProperty motion = record.Metadata.Data.Motions[motionPoint].MotionProperty; - session.Player.AnimationState.TryPlaySequence(motion.SequenceName, motion.SequenceSpeed, AnimationType.Skill); + session.Animation.TryPlaySequence(motion.SequenceName, motion.SequenceSpeed, AnimationType.Skill); } private void HandleTickSync(GameSession session, IByteReader packet) { @@ -375,14 +375,14 @@ private void HandleTickSync(GameSession session, IByteReader packet) { } string skillSequence = record.Motion.MotionProperty.SequenceName; - string playingSequence = session.Player.AnimationState.PlayingSequence?.Name ?? ""; + string playingSequence = session.Player.Animation.PlayingSequence?.Name ?? ""; if (skillSequence != playingSequence) { Logger.Warning($"Motion point on skill cast {skillUid} '{skillSequence}' doesn't match playing sequence '{playingSequence}'", skillUid); return; } - session.Player.AnimationState.SetLoopSequence(true, true); + session.Player.Animation.SetLoopSequence(true, true); } private void HandleCancel(GameSession session, IByteReader packet) { @@ -400,6 +400,6 @@ private void HandleCancel(GameSession session, IByteReader packet) { session.Send(NoticePacket.Message($"Skill.Cancel: {skillUid}")); } - session.Player.AnimationState.CancelSequence(); + session.Animation.CancelSequence(); } } diff --git a/Maple2.Server.Game/Packets/NpcControlPacket.cs b/Maple2.Server.Game/Packets/NpcControlPacket.cs index 61d7778c2..faac7ac9f 100644 --- a/Maple2.Server.Game/Packets/NpcControlPacket.cs +++ b/Maple2.Server.Game/Packets/NpcControlPacket.cs @@ -46,25 +46,25 @@ private static void NpcBuffer(this PoolByteWriter buffer, FieldNpc npc, bool isT buffer.Write(npc.Position); buffer.WriteShort((short) (npc.Transform.RotationAnglesDegrees.Z * 10)); buffer.Write(npc.MovementState.Velocity); - buffer.WriteShort((short) (npc.AnimationState.SequenceSpeed * 100)); + buffer.WriteShort((short) (npc.Animation.SequenceSpeed * 100)); if (npc.Value.IsBoss) { buffer.WriteInt(npc.BattleState.TargetId); // ObjectId of Player being targeted? } - short defaultSequenceId = npc.AnimationState.IdleSequenceId; + short defaultSequenceId = npc.Animation.IdleSequenceId; if (isTalk) { buffer.Write(ActorState.Talk); buffer.WriteShort(-1); } else { buffer.Write(npc.MovementState.State); - buffer.WriteShort(npc.AnimationState.PlayingSequence?.Id ?? defaultSequenceId); + buffer.WriteShort(npc.Animation.PlayingSequence?.Id ?? defaultSequenceId); } buffer.WriteShort(npc.SequenceCounter); // Animation (-2 = Jump_A, -3 = Jump_B) - bool isJumpSequence = (npc.AnimationState.PlayingSequence?.Id ?? -1) is ANI_JUMP_A or ANI_JUMP_B; + bool isJumpSequence = (npc.Animation.PlayingSequence?.Id ?? -1) is ANI_JUMP_A or ANI_JUMP_B; if (isJumpSequence) { bool isAbsolute = false; diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index b0d85c9e0..d6603ace8 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -100,6 +100,7 @@ public sealed partial class GameSession : Core.Network.Session { public MarriageManager Marriage { get; set; } = null!; public FishingManager Fishing { get; set; } = null!; public DungeonManager Dungeon { get; set; } = null!; + public AnimationManager Animation { get; set; } = null!; public GameSession(TcpClient tcpClient, GameServer server, IComponentContext context) : base(tcpClient) { @@ -147,6 +148,7 @@ public bool EnterServer(long accountId, Guid machineId, MigrateInResponse migrat db.Commit(); Player = new FieldPlayer(this, player); + Animation = new AnimationManager(this); Currency = new CurrencyManager(this); Mastery = new MasteryManager(this, Lua); Stats = new StatsManager(Player, ServerTableMetadata.UserStatTable); diff --git a/Maple2.Server.Game/Util/SkillUtils.cs b/Maple2.Server.Game/Util/SkillUtils.cs index a38987416..672ec1f00 100644 --- a/Maple2.Server.Game/Util/SkillUtils.cs +++ b/Maple2.Server.Game/Util/SkillUtils.cs @@ -126,7 +126,7 @@ public static bool Check(this BeginCondition condition, IActor caster, IActor ow !condition.DungeonGroupType.Contains(dungeonFieldManager.DungeonMetadata.GroupType))) { return false; } - if (condition.ActiveSkill.Length > 0 && condition.ActiveSkill.All(id => owner.AnimationState.Current?.Skill?.Id != id)) { + if (condition.ActiveSkill.Length > 0 && condition.ActiveSkill.All(id => owner.Animation.Current?.Skill?.Id != id)) { return false; } } From 916e9523c8eec323f36a0b7adba7c680d66c4c25 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:16:43 -0700 Subject: [PATCH 3/9] Fix reset buff casters and owners --- Maple2.Server.Game/Manager/Config/BuffManager.cs | 3 +++ Maple2.Server.Game/Model/Field/Actor/Actor.cs | 2 +- .../Model/Field/Actor/FieldPlayer.cs | 5 +---- Maple2.Server.Game/Model/Field/Buff.cs | 16 +++++++++++----- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Maple2.Server.Game/Manager/Config/BuffManager.cs b/Maple2.Server.Game/Manager/Config/BuffManager.cs index 7065bce43..4eebe79fd 100644 --- a/Maple2.Server.Game/Manager/Config/BuffManager.cs +++ b/Maple2.Server.Game/Manager/Config/BuffManager.cs @@ -49,6 +49,9 @@ public void Initialize() { public void ResetActor(IActor actor) { Actor = actor; + foreach ((int id, Buff buff) in Buffs) { + buff.ResetActor(actor); + } } public void LoadFieldBuffs() { diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index d8f39f6cf..1e7e37a24 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -40,7 +40,7 @@ public virtual Vector3 Rotation { set => Transform.RotationAnglesDegrees = value; } public Transform Transform { get; init; } - public virtual AnimationManager Animation { get; } + public AnimationManager Animation { get; init; } public SkillState SkillState { get; init; } public virtual bool IsDead { get; protected set; } diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs index 4a3913655..d04f29ec2 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs @@ -18,7 +18,6 @@ public class FieldPlayer : Actor { public override StatsManager Stats => Session.Stats; public override BuffManager Buffs => Session.Buffs; - public override AnimationManager Animation => Session.Animation; public override IPrism Shape => new Prism(new Circle(new Vector2(Position.X, Position.Y), 10), Position.Z, 100); private ActorState state; public ActorState State { @@ -90,6 +89,7 @@ public DeathState DeathState { public FieldPlayer(GameSession session, Player player) : base(session.Field, player.ObjectId, player, session.NpcMetadata) { Session = session; + Animation = Session.Animation; regenStats = new Dictionary>(); lastRegenTime = new Dictionary(); @@ -185,9 +185,6 @@ public override void Update(long tickCount) { } Session.GameEvent.Update(tickCount); - - //Console.WriteLine($"Playing sequence: {AnimationState.PlayingSequence?.Name ?? "null"}"); - Console.WriteLine($"Current Animation: {Animation.Current?.Sequence?.Name ?? "null"} Skill: {Animation.Current?.Skill?.Id ?? 0}"); } public void OnStateSync(StateSync stateSync) { diff --git a/Maple2.Server.Game/Model/Field/Buff.cs b/Maple2.Server.Game/Model/Field/Buff.cs index df80d7276..44e8dae92 100644 --- a/Maple2.Server.Game/Model/Field/Buff.cs +++ b/Maple2.Server.Game/Model/Field/Buff.cs @@ -16,8 +16,8 @@ public class Buff : IUpdatable, IByteSerializable { public readonly int ObjectId; public long CastUid { get; set; } - public readonly IActor Caster; - public readonly IActor Owner; + public IActor Caster { get; private set; } + public IActor Owner { get; private set; } public int Id => Metadata.Id; public short Level => Metadata.Level; @@ -55,6 +55,15 @@ public Buff(AdditionalEffectMetadata metadata, int objectId, IActor caster, IAct canExpire = metadata.Property.KeepCondition != BuffKeepCondition.UnlimitedDuration && EndTick >= startTick; } + public void ResetActor(IActor actor) { + if (actor.ObjectId == Caster.ObjectId) { + Caster = actor; + } + if (actor.ObjectId == Owner.ObjectId) { + Owner = actor; + } + } + public bool Stack(long startTick, int amount = 1, int durationMs = 0) { Stacks = Math.Min(Stacks + amount, Metadata.Property.MaxCount); StartTick = startTick; @@ -104,9 +113,6 @@ public virtual void Update(long tickCount) { } public bool UpdateEnabled(bool notifyField = true) { - if (Id == 11000081) { - Console.WriteLine($"Striker buff"); - } bool enabled = Metadata.Condition.Check(Caster, Owner, Owner); if (Enabled != enabled) { Enabled = enabled; From afbc4698200537a61021debac207e4a4189ad339 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:19:14 -0700 Subject: [PATCH 4/9] remove file --- .../ActorStateComponent/AnimationState.cs | 352 ------------------ 1 file changed, 352 deletions(-) delete mode 100644 .github/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs diff --git a/.github/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs b/.github/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs deleted file mode 100644 index 5a340463b..000000000 --- a/.github/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationState.cs +++ /dev/null @@ -1,352 +0,0 @@ -using Maple2.Model.Metadata; -using Maple2.Server.Core.Packets; -using Maple2.Server.Game.Model.Enum; - -namespace Maple2.Server.Game.Model.ActorStateComponent; - -/// -/// Manages animation sequences for actors in the game. -/// -public class AnimationState { - private readonly IActor actor; - private AnimationRecord? current; - private AnimationRecord? queued; - - public readonly AnimationMetadata? RigMetadata; - public AnimationSequenceMetadata? PlayingSequence => current?.Sequence; - public short IdleSequenceId { get; init; } - public float SequenceSpeed => current?.Speed ?? 1.0f; - - private bool isHandlingKeyframe; - private bool IsPlayerAnimation => actor is FieldPlayer; - public float MoveSpeed { get; set; } = 1; - public float AttackSpeed { get; set; } = 1; - private float lastSequenceTime; - private float sequenceEnd; - private LoopData sequenceLoop; - private long lastTick; - private long sequenceEndTick; - private long sequenceLoopEndTick; - - private bool debugPrintAnimations; - public bool DebugPrintAnimations { - get { return debugPrintAnimations; } - set { - if (actor is FieldPlayer) { - debugPrintAnimations = value; - } - } - } - - /// - /// Initializes a new instance of the AnimationState class. - /// - /// The actor this animation state belongs to - /// The model name to load animations for - public AnimationState(IActor actor, string modelName) { - this.actor = actor; - - RigMetadata = actor.NpcMetadata?.GetAnimation(modelName); - MoveSpeed = 1; - AttackSpeed = 1; - sequenceLoop = new LoopData(0, 0); - - if (RigMetadata is null) { - IdleSequenceId = 0; - return; - } - - string idleName = "Idle_A"; - if (actor is FieldNpc npc) { - idleName = npc.Value.Metadata.Action.Actions.FirstOrDefault()?.Name ?? idleName; - IdleSequenceId = RigMetadata.Sequences.FirstOrDefault(sequence => sequence.Key.Contains(idleName)).Value.Id; - return; - } - IdleSequenceId = RigMetadata.Sequences.FirstOrDefault(sequence => sequence.Key == idleName).Value.Id; - } - - /// - /// Resets the current animation sequence. - /// - private void ResetSequence() { - if (current?.Sequence != null && actor is FieldNpc npc) { - npc.SendControl = true; - } - current = null; - lastSequenceTime = 0; - sequenceLoop = new LoopData(0, 0); - lastTick = 0; - sequenceEnd = 0; - } - - /// - /// Attempts to play an animation sequence with the specified name, speed, and type. - /// - /// The name of the animation sequence to play - /// The speed at which to play the animation - /// The type of animation (Move, Skill, or Misc) - /// True if the sequence was found and started (or queued), false otherwise - public bool TryPlaySequence(string name, float speed, AnimationType type) { - // Can't play animations without metadata - if (RigMetadata is null || !RigMetadata.Sequences.TryGetValue(name, out AnimationSequenceMetadata? sequence)) { - DebugPrint($"Attempt to play nonexistent sequence '{name}' at x{speed} speed, previous: '{PlayingSequence?.Name ?? "none"}' x{SequenceSpeed}"); - ResetSequence(); - return false; - } - - // If we're currently processing a keyframe event, queue this sequence for later - if (isHandlingKeyframe) { - queued = new AnimationRecord(sequence, speed, type); - return true; - } - - // Play the sequence immediately - PlaySequence(sequence, speed, type); - return true; - } - - /// - /// Plays the specified animation sequence. - /// - /// The animation sequence to play - /// The speed at which to play the animation - /// The type of animation (Move, Skill, or Misc) - private void PlaySequence(AnimationSequenceMetadata sequenceMetadata, float speed, AnimationType type) { - // For NPCs, set SendControl flag when changing sequences - if (current?.Sequence != sequenceMetadata && actor is FieldNpc npc) { - npc.SendControl = true; - } - - // Log the sequence change - DebugPrint($"Playing sequence '{sequenceMetadata.Name}' at x{speed} speed, previous: '{PlayingSequence?.Name ?? "none"}' x{SequenceSpeed}"); - - // Reset current sequence state - ResetSequence(); - - // Set the new sequence properties - current = new AnimationRecord(sequenceMetadata, speed, type); - - // Start tracking from current tick - lastTick = actor.Field.FieldTick; - } - - /// - /// Cancels the currently playing animation sequence. - /// - public void CancelSequence() { - // Log the cancellation if a sequence is playing - if (PlayingSequence is not null) { - DebugPrint($"Canceled playing sequence: '{PlayingSequence.Name}' x{SequenceSpeed}"); - } - - // If we're processing a keyframe event, queue the reset for later - if (isHandlingKeyframe) { - queued = null; - return; - } - - // Reset the sequence state - ResetSequence(); - } - - /// - /// Updates the animation state based on the current tick count. - /// - /// The current server tick count - public void Update(long tickCount) { - // Skip update if no animation metadata is available - if (RigMetadata is null) { - return; - } - - // Reset if no valid sequence is playing - if (PlayingSequence?.Keys.Count == 0) { - ResetSequence(); - return; - } - - // Calculate the current sequence time - float sequenceSpeedModifier = current?.Type switch { - AnimationType.Move => MoveSpeed, - AnimationType.Skill => AttackSpeed, - _ => 1, - }; - - long lastServerTick = lastTick == 0 ? tickCount : lastTick; - float speed = SequenceSpeed * sequenceSpeedModifier / 1000; - float delta = (float)(tickCount - lastServerTick) * speed; - float sequenceTime = lastSequenceTime + delta; - - // Process keyframe events - if (PlayingSequence?.Keys != null) { - foreach (AnimationKey key in PlayingSequence.Keys) { - if (HasHitKeyframe(sequenceTime, key)) { - HitKeyframe(sequenceTime, key, speed); - } - } - } - - // Handle sequence looping - if (current?.IsLooping == true && sequenceLoop.end != 0 && sequenceTime > sequenceLoop.end) { - if (!IsPlayerAnimation || tickCount <= sequenceLoopEndTick + Constant.ClientGraceTimeTick) { - if (current.LoopOnlyOnce) { - current.IsLooping = false; - current.LoopOnlyOnce = false; - } - - sequenceTime -= sequenceLoop.end - sequenceLoop.start; - lastSequenceTime = sequenceTime - Math.Max(delta, sequenceTime - sequenceLoop.end + 0.001f); - - // Play all keyframe events from loopstart to current - if (PlayingSequence?.Keys != null) { - foreach (AnimationKey key in PlayingSequence.Keys) { - if (HasHitKeyframe(sequenceTime, key)) { - HitKeyframe(sequenceTime, key, speed); - } - } - } - } - } - - // Check for sequence end - if (sequenceEnd != 0 && sequenceTime > sequenceEnd) { - if (!IsPlayerAnimation || tickCount <= sequenceEndTick + Constant.ClientGraceTimeTick) { - ResetSequence(); - } - } - - // Update timing state - lastTick = tickCount; - lastSequenceTime = sequenceTime; - isHandlingKeyframe = false; - - // Process queued actions - if (queued != null) { - PlaySequence(queued.Sequence!, queued.Speed, queued.Type); - queued = null; - } - } - - /// - /// Sets whether the current sequence should loop. - /// - /// Whether the sequence should loop - /// Whether the sequence should only loop once - public void SetLoopSequence(bool shouldLoop, bool loopOnlyOnce) { - if (current is null) { - return; - } - - current.IsLooping = shouldLoop; - current.LoopOnlyOnce = loopOnlyOnce; - } - - /// - /// Determines if a keyframe has been hit in the current update. - /// - /// The current sequence time - /// The keyframe to check - /// True if the keyframe has been hit, false otherwise - private bool HasHitKeyframe(float sequenceTime, AnimationKey key) { - bool keyBeforeLoop = !current?.IsLooping ?? true || sequenceLoop.end == 0 || key.Time <= sequenceLoop.end + 0.001f; - bool hitKeySinceLastTick = key.Time > lastSequenceTime && key.Time <= sequenceTime; - - return keyBeforeLoop && hitKeySinceLastTick; - } - - /// - /// Gets the normalized time within a segment defined by two keyframes. - /// - /// The name of the first keyframe - /// The name of the second keyframe - /// A value between 0 and 1 representing the position within the segment, or -1 if not in the segment - public float GetSequenceSegmentTime(string keyframe1, string keyframe2) { - if (PlayingSequence is null) { - return -1; - } - - float keyframe1Time = -1; - float keyframe2Time = -1; - - foreach (AnimationKey key in PlayingSequence.Keys) { - if (key.Name == keyframe1) { - keyframe1Time = key.Time; - } - - if (key.Name == keyframe2) { - keyframe2Time = key.Time; - break; - } - } - - // Segment doesn't exist or is malformed - if (keyframe1Time == -1 || keyframe2Time == -1 || keyframe1Time > keyframe2Time) { - return -1; - } - - // Current time out of segment - if (lastSequenceTime < keyframe1Time || lastSequenceTime >= keyframe2Time) { - return -1; - } - - if (keyframe1Time == keyframe2Time) { - // Can only be in the segment, and at the end, or not in the segment - return lastSequenceTime == keyframe1Time ? 1 : -1; - } - - return (lastSequenceTime - keyframe1Time) / (keyframe2Time - keyframe1Time); - } - - /// - /// Processes a keyframe event. - /// - /// The current sequence time - /// The keyframe that was hit - /// The current animation speed - private void HitKeyframe(float sequenceTime, AnimationKey key, float speed) { - isHandlingKeyframe = true; - - DebugPrint($"Sequence '{PlayingSequence!.Name}' keyframe event '{key.Name}'"); - - actor.KeyframeEvent(key.Name); - - switch (key.Name) { - case "loopstart": - sequenceLoop = new LoopData(key.Time, 0); - break; - case "loopend": - sequenceLoop = new LoopData(sequenceLoop.start, key.Time); - sequenceLoopEndTick = actor.Field.FieldTick + (long)((key.Time - sequenceTime) / speed); - break; - case "end": - sequenceEnd = key.Time; - sequenceEndTick = actor.Field.FieldTick + (long)((key.Time - sequenceTime) / speed); - break; - default: - break; - } - } - - /// - /// Prints a debug message if debug printing is enabled. - /// - /// The message to print - private void DebugPrint(string message) { - if (debugPrintAnimations && actor is FieldPlayer player) { - player.Session.Send(NoticePacket.Message(message)); - } - } - - /// - /// Represents a loop section in an animation sequence - /// - private struct LoopData { - public float start; - public float end; - - public LoopData(float start, float end) { - this.start = start; - this.end = end; - } - } -} From 822880a878f568306f58e7ec5c7c5adbebb25f40 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:19:54 -0700 Subject: [PATCH 5/9] update nuget for Maple2.File.Parser --- Maple2.File.Ingest/Maple2.File.Ingest.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Maple2.File.Ingest/Maple2.File.Ingest.csproj b/Maple2.File.Ingest/Maple2.File.Ingest.csproj index 346a1d178..57419acf2 100644 --- a/Maple2.File.Ingest/Maple2.File.Ingest.csproj +++ b/Maple2.File.Ingest/Maple2.File.Ingest.csproj @@ -19,7 +19,7 @@ - + From 1301c9a4ab454a4802bdeb2b9954ef3c1141b6e7 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:32:10 -0700 Subject: [PATCH 6/9] Remove added func --- Maple2.Server.Game/Model/Skill/SkillQueue.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Maple2.Server.Game/Model/Skill/SkillQueue.cs b/Maple2.Server.Game/Model/Skill/SkillQueue.cs index 67ec12feb..19075ab8a 100644 --- a/Maple2.Server.Game/Model/Skill/SkillQueue.cs +++ b/Maple2.Server.Game/Model/Skill/SkillQueue.cs @@ -40,10 +40,6 @@ public void Remove(long uid) { } } - public bool Contains(int skillId) { - return casts.Any(cast => cast?.SkillId == skillId); - } - public void Clear() { for (int i = 0; i < MAX_PENDING; i++) { casts[i] = null; From 1740392af00fd45835550062d8480322f6f280eb Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:34:05 -0700 Subject: [PATCH 7/9] Rabbit comments --- Maple2.File.Ingest/MapperExtensions.cs | 6 ------ .../Field/Actor/ActorStateComponent/AnimationRecord.cs | 4 ++-- Maple2.Server.Game/Util/SkillUtils.cs | 5 +++-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Maple2.File.Ingest/MapperExtensions.cs b/Maple2.File.Ingest/MapperExtensions.cs index 5ac806e08..16ba0989a 100644 --- a/Maple2.File.Ingest/MapperExtensions.cs +++ b/Maple2.File.Ingest/MapperExtensions.cs @@ -319,12 +319,6 @@ public static SkillMetadataAutoTargeting Convert(this AutoTargeting autoTargetin } public static BeginCondition Convert(this Maple2.File.Parser.Xml.Skill.BeginCondition beginCondition) { - List dungeonGroupType = []; - foreach (Parser.Xml.Skill.BeginCondition.DungeonRoomGroupTypes type in beginCondition.requireDungeonRoomGroupTypes) { - if (Enum.TryParse(type.type, true, out DungeonGroupType groupType)) { - dungeonGroupType.Add(groupType); - } - } return new BeginCondition( Level: beginCondition.level, Gender: (Gender) beginCondition.gender, diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationRecord.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationRecord.cs index a6c0a1bf8..df6f19aef 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationRecord.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationRecord.cs @@ -4,7 +4,7 @@ namespace Maple2.Server.Game.Model.ActorStateComponent; public class AnimationRecord { - public AnimationSequenceMetadata? Sequence { get; set; } + public AnimationSequenceMetadata? Sequence { get; private set; } public float Speed { get; set; } public AnimationType Type { get; set; } public float LastTime { get; set; } @@ -14,7 +14,7 @@ public class AnimationRecord { public long LoopEndTick { get; set; } public bool IsLooping { get; set; } public bool LoopOnlyOnce { get; set; } - public SkillMetadata? Skill { get; set; } + public SkillMetadata? Skill { get; private set; } public AnimationRecord() { Speed = 1; diff --git a/Maple2.Server.Game/Util/SkillUtils.cs b/Maple2.Server.Game/Util/SkillUtils.cs index 672ec1f00..535069240 100644 --- a/Maple2.Server.Game/Util/SkillUtils.cs +++ b/Maple2.Server.Game/Util/SkillUtils.cs @@ -115,10 +115,10 @@ public static bool Check(this BeginCondition condition, IActor caster, IActor ow if (condition.Maps.Length > 0 && !condition.Maps.Contains(caster.Field.MapId)) { return false; } - if (condition.Maps.Length > 0 && !condition.MapTypes.Contains(caster.Field.Metadata.Property.Type)) { + if (condition.MapTypes.Length > 0 && !condition.MapTypes.Contains(caster.Field.Metadata.Property.Type)) { return false; } - if (condition.Maps.Length > 0 && !condition.Continents.Contains(caster.Field.Metadata.Property.Continent)) { + if (condition.Continents.Length > 0 && !condition.Continents.Contains(caster.Field.Metadata.Property.Continent)) { return false; } if (condition.DungeonGroupType.Length > 0 && @@ -165,6 +165,7 @@ private static bool Check(this BeginConditionTarget? condition, IActor target) { foreach ((BasicAttribute attribute, float value, CompareType compare, CompareStatValueType valueType) in condition.Stat) { float targetValue = valueType switch { + CompareStatValueType.CurrentPercentage when target.Stats.Values[attribute].Total == 0 => 0, CompareStatValueType.CurrentPercentage => (float) target.Stats.Values[attribute].Current / target.Stats.Values[attribute].Total, CompareStatValueType.TotalValue => target.Stats.Values[attribute].Total, _ => 0, From 4a0892390026b6c0db88e4f4ecd1720875fd3bc6 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:47:45 -0700 Subject: [PATCH 8/9] formatting --- Maple2.Server.Game/Manager/AnimationManager.cs | 6 +++--- .../Field/Actor/ActorStateComponent/AnimationRecord.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Maple2.Server.Game/Manager/AnimationManager.cs b/Maple2.Server.Game/Manager/AnimationManager.cs index fcf7cde08..5fa337f08 100644 --- a/Maple2.Server.Game/Manager/AnimationManager.cs +++ b/Maple2.Server.Game/Manager/AnimationManager.cs @@ -221,7 +221,7 @@ public void Update(long tickCount) { long lastServerTick = lastTick == 0 ? tickCount : lastTick; float speed = SequenceSpeed * sequenceSpeedModifier / 1000; - float delta = (float)(tickCount - lastServerTick) * speed; + float delta = (float) (tickCount - lastServerTick) * speed; float sequenceTime = lastSequenceTime + delta; // Process keyframe events @@ -367,12 +367,12 @@ private void HitKeyframe(float sequenceTime, AnimationKey key, float speed) { break; case "loopend": Current.Loop = new AnimationRecord.LoopData(Current.Loop.start, key.Time); - Current.LoopEndTick = Actor.Field.FieldTick + (long)((key.Time - sequenceTime) / speed); + Current.LoopEndTick = Actor.Field.FieldTick + (long) ((key.Time - sequenceTime) / speed); sequenceLoopEndTick = Current.LoopEndTick; break; case "end": Current.EndTime = key.Time; - Current.EndTick = Actor.Field.FieldTick + (long)((key.Time - sequenceTime) / speed); + Current.EndTick = Actor.Field.FieldTick + (long) ((key.Time - sequenceTime) / speed); sequenceEndTick = Current.EndTick; break; default: diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationRecord.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationRecord.cs index df6f19aef..14158341e 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationRecord.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AnimationRecord.cs @@ -1,4 +1,4 @@ -using Maple2.Model.Metadata; +using Maple2.Model.Metadata; using Maple2.Server.Game.Model.Enum; namespace Maple2.Server.Game.Model.ActorStateComponent; From d945d0adba3566f101f0d52beeb4e0a0e7972ab7 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:48:29 -0700 Subject: [PATCH 9/9] formatting