diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index a85542bd7..15d72f504 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -921,6 +921,7 @@ public static class Constant { public const string PaybackGuideUrl = "http://maplestory2.nexon.com/News/Events"; public const int DummyNpcMale = 2040998; public const int DummyNpcFemale = 2040999; + public static int DummyNpc(Gender gender) => gender is Gender.Female ? DummyNpcFemale : DummyNpcMale; #endregion diff --git a/Maple2.Server.Game/Commands/DebugCommand.cs b/Maple2.Server.Game/Commands/DebugCommand.cs index 62ad18283..70a802bfb 100644 --- a/Maple2.Server.Game/Commands/DebugCommand.cs +++ b/Maple2.Server.Game/Commands/DebugCommand.cs @@ -372,6 +372,7 @@ public LogoutCommand(GameSession session) : base("logout", "Logs out.") { } private void Handle(InvocationContext ctx) { + ctx.Console.Out.WriteLine("Logging out..."); session.Disconnect(); } } diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs index d7097bc00..45710747a 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs @@ -552,9 +552,7 @@ public void MovePlayerAlongPath(string pathName) { } foreach (FieldPlayer player in Players.Values) { - int dummyNpcId = player.Value.Character.Gender is Gender.Male ? Constant.DummyNpcMale : Constant.DummyNpcFemale; - - if (!NpcMetadata.TryGet(dummyNpcId, out NpcMetadata? npcMetadata)) { + if (!NpcMetadata.TryGet(Constant.DummyNpc(player.Value.Character.Gender), out NpcMetadata? npcMetadata)) { continue; } @@ -566,7 +564,7 @@ public void MovePlayerAlongPath(string pathName) { Broadcast(ProxyObjectPacket.AddNpc(dummyNpc)); dummyNpc.SetPatrolData(patrolData); - dummyNpc.MovementState.CleanupPatrolData(); + dummyNpc.MovementState.CleanupPatrolData(player); player.Session.Send(FollowNpcPacket.FollowNpc(dummyNpc.ObjectId)); } } diff --git a/Maple2.Server.Game/Model/Enum/NpcTaskPriority.cs b/Maple2.Server.Game/Model/Enum/NpcTaskPriority.cs index fa8c00369..509fd5dc1 100644 --- a/Maple2.Server.Game/Model/Enum/NpcTaskPriority.cs +++ b/Maple2.Server.Game/Model/Enum/NpcTaskPriority.cs @@ -3,7 +3,8 @@ public enum NpcTaskPriority { None, Cleanup, - IdleAction, // wander, patrol, bore emote + IdleAction, // wander, patrol + Emote, BattleStandby, BattleWalk, // trace/runaway/move BattleAction, // skill cast, jump diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs index f963fb432..04678a7d7 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs @@ -86,11 +86,12 @@ public NpcTask TryMoveTargetDistance(IActor target, float distance, bool isBattl public NpcTask TryStandby(IActor? target, bool isIdle, string sequence = "") { NpcTaskPriority priority = isIdle ? NpcTaskPriority.IdleAction : NpcTaskPriority.BattleStandby; + return new NpcStandbyTask(actor.TaskState, this, sequence, priority, isIdle); } public NpcTask TryEmote(string sequenceName, bool isIdle, float duration = -1f) { - NpcTaskPriority priority = isIdle ? NpcTaskPriority.IdleAction : NpcTaskPriority.BattleStandby; + NpcTaskPriority priority = isIdle ? NpcTaskPriority.Emote : NpcTaskPriority.BattleStandby; return new NpcEmoteTask(actor.TaskState, this, sequenceName, priority, isIdle, duration); } @@ -118,8 +119,8 @@ public NpcTask TryCastSkill(int id, short level, int faceTarget, Vector3 facePos return new NpcSkillCastTask(actor.TaskState, this, id, level, faceTarget, facePos, uid); } - public NpcTask CleanupPatrolData() { - return new NpcCleanupPatrolDataTask(actor.TaskState, this); + public NpcTask CleanupPatrolData(FieldPlayer player) { + return new NpcCleanupPatrolDataTask(player, actor.TaskState, this); } private void SetState(ActorState state) { diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs index 528d3e940..d2ba0c346 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs @@ -1,4 +1,6 @@ -using Maple2.Server.Game.Model.Enum; +using System.Numerics; +using Maple2.Model.Metadata; +using Maple2.Server.Game.Model.Enum; using static Maple2.Server.Game.Model.ActorStateComponent.TaskState; namespace Maple2.Server.Game.Model.ActorStateComponent; @@ -6,16 +8,45 @@ namespace Maple2.Server.Game.Model.ActorStateComponent; public partial class MovementState { public class NpcCleanupPatrolDataTask : NpcTask { private readonly MovementState movement; + private readonly FieldPlayer player; + private readonly Vector3? lastPosition; public override bool CancelOnInterrupt => false; - public NpcCleanupPatrolDataTask(TaskState taskState, MovementState movement) : base(taskState, NpcTaskPriority.Cleanup) { + public NpcCleanupPatrolDataTask(FieldPlayer player, TaskState taskState, MovementState movement) : base(taskState, NpcTaskPriority.Cleanup) { this.movement = movement; + this.player = player; + MS2WayPoint? last = movement.actor.Patrol?.WayPoints.LastOrDefault(); + if (last is null) { + return; + } + lastPosition = last.Position; } protected override void TaskResumed() { - if (movement.actor.Patrol is null) { - movement.actor.Field.RemoveNpc(movement.actor.ObjectId); + if (movement.actor.Patrol is not null) { + return; } + + movement.actor.Field.RemoveNpc(movement.actor.ObjectId); + + if (lastPosition is null) { + return; + } + + const float maxDistance = Constant.TalkableDistance * Constant.TalkableDistance; + + // find nearest npc + FieldNpc? closestNpc = player.Field.Npcs.Values + .Where(npc => npc != movement.actor && Vector3.DistanceSquared(player.Position, npc.Position) < maxDistance) + .OrderBy(npc => Vector3.DistanceSquared(player.Position, npc.Position)) + .FirstOrDefault(); + + if (closestNpc == null) { + return; + } + + player.Transform.LookTo(Vector3.Normalize(closestNpc.Position - lastPosition.Value)); + player.MoveToPosition(lastPosition.Value, player.Rotation); } } } 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 b4c615949..2bcee7b30 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 @@ -7,10 +7,10 @@ namespace Maple2.Server.Game.Model.ActorStateComponent; public partial class MovementState { public class NpcEmoteTask : NpcTask { private readonly MovementState movement; - public string Sequence { get; init; } = string.Empty; - public bool IsIdle { get; init; } + public string Sequence { get; init; } + private bool IsIdle { get; init; } public override bool CancelOnInterrupt => true; - public float Duration { get; init; } + private float Duration { get; init; } public NpcEmoteTask(TaskState taskState, MovementState movement, string sequence, NpcTaskPriority priority, bool isIdle, float duration) : base(taskState, priority) { this.movement = movement; @@ -24,10 +24,15 @@ public NpcEmoteTask(TaskState taskState, MovementState movement, string sequence protected override void TaskResumed() { movement.Emote(this, Sequence, IsIdle, Duration); } + protected override void TaskFinished(bool isCompleted) { movement.emoteLimitTick = 0; movement.Idle(); } + + public override string ToString() { + return $"{GetType().Name} (Priority: {Priority}, Status: {Status}, Sequence: {Sequence})"; + } } private void Emote(NpcTask task, string sequence, bool isIdle, float duration) { diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/TaskState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/TaskState.cs index d3038083a..c71359781 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/TaskState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/TaskState.cs @@ -1,14 +1,15 @@ -using Maple2.Server.Game.Model.Enum; +using System.Diagnostics.CodeAnalysis; +using Maple2.Server.Game.Model.Enum; namespace Maple2.Server.Game.Model.ActorStateComponent; public class TaskState { - public FieldNpc Actor { get; init; } + private FieldNpc Actor { get; } - private PriorityQueue taskQueue; - private NpcTask?[] runningTasks; - private bool isPendingStart = false; - private NpcTask? pendingTask = null; + private readonly PriorityQueue taskQueue; + private readonly NpcTask?[] runningTasks; + private bool isPendingStart; + private NpcTask? pendingTask; public TaskState(FieldNpc actor) { Actor = actor; @@ -22,17 +23,16 @@ public TaskState(FieldNpc actor) { private NpcTaskStatus QueueTask(NpcTask task) { NpcTask? queued = runningTasks[task.PriorityValue]; - if (queued != null && !task.ShouldOverride(task)) { + if (queued != null && !task.ShouldOverride(queued)) { return NpcTaskStatus.Cancelled; } - if (taskQueue.TryPeek(out NpcTask? currentTask, out NpcTaskPriority priority)) { - bool cancelLowerPriority = currentTask.PriorityValue < task.PriorityValue && currentTask.CancelOnInterrupt; - bool cancelEqualPriority = currentTask.PriorityValue == task.PriorityValue; + queued?.Cancel(); - if (cancelLowerPriority || cancelEqualPriority) { + if (taskQueue.TryPeek(out NpcTask? currentTask, out NpcTaskPriority priority) && currentTask.PriorityValue < task.PriorityValue) { + if (currentTask.CancelOnInterrupt) { currentTask.Cancel(); - } else if (currentTask.PriorityValue < task.PriorityValue) { + } else { currentTask.Pause(); } } @@ -40,7 +40,8 @@ private NpcTaskStatus QueueTask(NpcTask task) { runningTasks[task.PriorityValue] = task; taskQueue.Enqueue(task, task.Priority); - if (taskQueue.Peek() == task) { + NpcTask npcTask = taskQueue.Peek(); + if (npcTask == task) { isPendingStart = true; pendingTask = task; @@ -57,19 +58,28 @@ private void FinishTask(NpcTask task) { runningTasks[task.PriorityValue] = null; - if (taskQueue.TryPeek(out NpcTask? currentTask, out NpcTaskPriority priority) && currentTask == task) { - taskQueue.Dequeue(); + if (!taskQueue.TryPeek(out NpcTask? currentTask, out _) || currentTask != task) { + return; + } - if (taskQueue.TryPeek(out currentTask, out priority)) { - isPendingStart = true; - pendingTask = currentTask; - } + taskQueue.Dequeue(); + + if (!taskQueue.TryPeek(out currentTask, out _)) { + return; } + isPendingStart = true; + pendingTask = currentTask; } public void Update(long tickCount) { if (isPendingStart) { - if (taskQueue.TryPeek(out NpcTask? task, out NpcTaskPriority priority) && task == pendingTask) { + NpcTask? task; + + while (taskQueue.TryPeek(out task, out _) && task.Status == NpcTaskStatus.Cancelled) { + taskQueue.Dequeue(); + } + + if (taskQueue.TryPeek(out task, out _) && task == pendingTask) { task.Resume(); } } @@ -79,22 +89,15 @@ public void Update(long tickCount) { } public abstract class NpcTask { - protected TaskState queue { get; private set; } - - public NpcTaskPriority Priority { get; private init; } - public int PriorityValue { get => (int) Priority; } - public NpcTaskStatus Status { - get => status; - private set { - status = value; - } - } - public bool IsDone { get => Status == NpcTaskStatus.Cancelled || Status == NpcTaskStatus.Complete; } + private TaskState Queue { get; } + public NpcTaskPriority Priority { get; } + public int PriorityValue => (int) Priority; + public NpcTaskStatus Status { get; private set; } + public bool IsDone => Status is NpcTaskStatus.Cancelled or NpcTaskStatus.Complete; public virtual bool CancelOnInterrupt { get; } - private NpcTaskStatus status; - public NpcTask(TaskState queue, NpcTaskPriority priority) { - this.queue = queue; + protected NpcTask(TaskState queue, NpcTaskPriority priority) { + this.Queue = queue; Priority = priority; Status = queue.QueueTask(this); @@ -131,7 +134,7 @@ public void Finish(bool isCompleted) { Status = isCompleted ? NpcTaskStatus.Complete : NpcTaskStatus.Cancelled; - queue.FinishTask(this); + Queue.FinishTask(this); TaskFinished(isCompleted); } @@ -144,5 +147,9 @@ public void Cancel() { public void Completed() { Finish(true); } + + public override string ToString() { + return $"{GetType().Name} (Priority: {Priority}, Status: {Status})"; + } } } diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index b9aa32796..7403d67f7 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -88,9 +88,9 @@ public short SequenceId { public MS2PatrolData? Patrol { get; private set; } private int currentWaypointIndex; - private bool hasBeenBattling = false; + private bool hasBeenBattling; private NpcTask? idleTask; - private long idleTaskLimitTick = 0; + private long idleTaskLimitTick; public readonly Dictionary AiExtraData = new(); @@ -170,7 +170,7 @@ public override void Update(long tickCount) { if (tickCount >= nextDebugPacket && playersListeningToDebugNow && debugMessages.Count > 0) { sentDebugPacket = true; - Field.BroadcastAiMessage(CinematicPacket.BalloonTalk(false, ObjectId, String.Join("", debugMessages.ToArray()), 2500, 0)); + Field.BroadcastAiMessage(CinematicPacket.BalloonTalk(false, ObjectId, string.Join("", debugMessages.ToArray()), 2500, 0)); } if (sentDebugPacket || tickCount >= nextDebugPacket) { @@ -206,6 +206,8 @@ private void DoIdleBehavior(long tickCount) { if (idleTask is MovementState.NpcStandbyTask && idleTaskLimitTick == 0) { idleTaskLimitTick = tickCount + 1000; + } else if (idleTask is not MovementState.NpcStandbyTask && idleTaskLimitTick != 0) { + idleTaskLimitTick = 0; } bool hitLimit = idleTaskLimitTick != 0 && tickCount >= idleTaskLimitTick; @@ -351,7 +353,6 @@ public void DropLoot(FieldPlayer firstPlayer) { } } - public override SkillRecord? CastSkill(int id, short level, long uid = 0, byte motionPoint = 0) { if (!Field.SkillMetadata.TryGet(id, level, out SkillMetadata? metadata) || metadata.Data.Motions.Length <= motionPoint) { Logger.Error("Invalid skill use: {SkillId},{Level},{motionPoint}", id, level, motionPoint); diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index cced816e3..a9bd3d9d6 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -723,11 +723,6 @@ protected override void Dispose(bool disposing) { State = SessionState.Disconnected; Complete(); } finally { -#if !DEBUG - if (Player.Value.Character.ReturnMapId != 0) { - Player.Value.Character.MapId = Player.Value.Character.ReturnMapId; - } -#endif Guild.Dispose(); Buddy.Dispose(); Party.Dispose(); diff --git a/Maple2.Tools/VectorMath/Transform.cs b/Maple2.Tools/VectorMath/Transform.cs index 21584d999..65430220c 100644 --- a/Maple2.Tools/VectorMath/Transform.cs +++ b/Maple2.Tools/VectorMath/Transform.cs @@ -138,6 +138,9 @@ public void LookTo(Vector3 direction, bool snapToGroundPlane = true) { } public void LookTo(Vector3 direction, Vector3 up, bool snapToGroundPlane = true) { + direction = Vector3.Normalize(direction); + up = Vector3.Normalize(up); + if (snapToGroundPlane) { direction = Vector3.Normalize(direction - Vector3.Dot(direction, up) * up); // plane projection formula @@ -147,6 +150,8 @@ public void LookTo(Vector3 direction, Vector3 up, bool snapToGroundPlane = true) } Vector3 right = Vector3.Cross(direction, up); + up = Vector3.Cross(right, direction); + float scale = Scale; RightAxis = scale * right;