From dd365f8e938136fd07e274072e1ed6385aadda83 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Tue, 13 May 2025 20:37:53 -0700 Subject: [PATCH 1/4] Skills and stuff --- .../Storage/Metadata/QuestMetadataStorage.cs | 4 + .../Mapper/AdditionalEffectMapper.cs | 6 +- Maple2.File.Ingest/Mapper/SkillMapper.cs | 21 ++- Maple2.Model/Enum/Buff.cs | 9 +- Maple2.Model/Enum/CompulsionEventType.cs | 8 - Maple2.Model/Enum/Skill.cs | 22 ++- .../Metadata/AdditionalEffectMetadata.cs | 2 +- Maple2.Model/Metadata/Constants.cs | 1 + Maple2.Model/Metadata/SkillMetadata.cs | 4 +- Maple2.Server.Game/Commands/PlayerCommand.cs | 165 ++++++++++++------ .../Manager/AchievementManager.cs | 36 ++++ .../Manager/Config/BuffManager.cs | 10 +- .../Manager/NpcScriptManager.cs | 2 +- Maple2.Server.Game/Manager/QuestManager.cs | 45 +++++ Maple2.Server.Game/Manager/StatsManager.cs | 5 +- Maple2.Server.Game/Model/Field/Actor/Actor.cs | 28 ++- .../MovementState.SkillCastTask.cs | 2 +- .../Model/Field/Actor/FieldNpc.cs | 7 +- .../Model/Field/Actor/FieldPlayer.cs | 102 +++++++---- .../Model/Field/Actor/IActor.cs | 4 +- Maple2.Server.Game/Model/Skill/SkillQueue.cs | 9 +- Maple2.Server.Game/Model/Skill/SkillRecord.cs | 2 + Maple2.Server.Game/Model/Stats.cs | 10 +- .../PacketHandlers/SkillHandler.cs | 66 +++---- .../PacketHandlers/StateSkillHandler.cs | 20 ++- Maple2.Server.Game/Packets/NpcTalkPacket.cs | 3 +- .../Service/ChannelService.Heartbeat.cs | 11 +- Maple2.Server.Game/Util/DamageCalculator.cs | 6 +- .../20250304061437_InteractCubeFix.cs | 8 +- 29 files changed, 432 insertions(+), 186 deletions(-) delete mode 100644 Maple2.Model/Enum/CompulsionEventType.cs diff --git a/Maple2.Database/Storage/Metadata/QuestMetadataStorage.cs b/Maple2.Database/Storage/Metadata/QuestMetadataStorage.cs index 7ed802ae6..37a3c58cc 100644 --- a/Maple2.Database/Storage/Metadata/QuestMetadataStorage.cs +++ b/Maple2.Database/Storage/Metadata/QuestMetadataStorage.cs @@ -61,6 +61,10 @@ public IEnumerable GetQuestsByType(QuestType type) { return GetQuests().Where(x => x.Basic.Type == type); } + public IEnumerable GetQuestsByChapter(int chapterId) { + return GetQuests().Where(x => x.Basic.ChapterId == chapterId); + } + public List Search(string name) { return GetQuests().Where(x => x.Name != null && x.Name.Contains(name, StringComparison.OrdinalIgnoreCase)).ToList(); } diff --git a/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs b/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs index 85e687abf..ebab28ffb 100644 --- a/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs +++ b/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs @@ -158,13 +158,13 @@ private static AdditionalEffectMetadataStatus Convert(StatusProperty status, Off resistances.AddIfNotDefault(BasicAttribute.AttackSpeed, status.resAspR); Debug.Assert(status.compulsionEventTypes.Length <= 1 && status.compulsionEventRate.Length <= 1); - var compulsionEventType = CompulsionEventType.None; + var compulsionEventType = BuffCompulsionEventType.None; if (status.compulsionEventTypes.Length > 0) { - compulsionEventType = (CompulsionEventType) status.compulsionEventTypes[0]; + compulsionEventType = (BuffCompulsionEventType) status.compulsionEventTypes[0]; } AdditionalEffectMetadataStatus.CompulsionEvent? compulsionEvent = null; - if (compulsionEventType != CompulsionEventType.None) { + if (compulsionEventType != BuffCompulsionEventType.None) { float compulsionEventRate = 0; if (status.compulsionEventRate.Length > 0) { compulsionEventRate = status.compulsionEventRate[0]; diff --git a/Maple2.File.Ingest/Mapper/SkillMapper.cs b/Maple2.File.Ingest/Mapper/SkillMapper.cs index bb36817e9..c163a458e 100644 --- a/Maple2.File.Ingest/Mapper/SkillMapper.cs +++ b/Maple2.File.Ingest/Mapper/SkillMapper.cs @@ -1,4 +1,6 @@ -using System.Diagnostics; +using System.ComponentModel; +using System.Diagnostics; +using System.Reflection; using Maple2.File.IO; using Maple2.File.Parser; using Maple2.File.Parser.Xml.Skill; @@ -15,10 +17,13 @@ public SkillMapper(M2dReader xmlReader) { } protected override IEnumerable Map() { + List magicPaths = []; + List cubeMagicPaths = []; foreach ((int id, string name, SkillData data) in parser.Parse()) { if (data.basic == null) continue; // Old_JobChange_01 Debug.Assert(data.basic.kinds.groupIDs.Length <= 1); + // Note: 90000775 has cubeMagicPathID="2147483647" which should be cubeMagicPathID="9000073111" Dictionary levels = data.level.ToDictionary( level => level.value, @@ -65,7 +70,7 @@ protected override IEnumerable Map() { Explosion: attack.arrowProperty.explosion, RayPhysXTest: attack.arrowProperty.rayPhysxTest, NonTarget: (SkillTargetType) attack.arrowProperty.nonTarget, - BounceType: attack.arrowProperty.bounceType, + BounceType: (BounceType) attack.arrowProperty.bounceType, BounceCount: attack.arrowProperty.bounceCount, BounceRadius: attack.arrowProperty.bounceRadius, BounceOverlap: attack.arrowProperty.bounceType > 0 && attack.arrowProperty.bounceOverlap, @@ -111,7 +116,14 @@ protected override IEnumerable Map() { RangeType: (RangeType) data.basic.kinds.rangeType, AttackType: (AttackType) data.basic.ui.attackType, Element: (Element) data.basic.kinds.element, - State: Enum.TryParse(data.basic.kinds.state, true, out ActorState state) ? state : ActorState.None, + State: string.IsNullOrEmpty(data.basic.kinds.state) + ? ActorState.None + : Enum.GetValues() + .FirstOrDefault(enumValue => + enumValue.GetType() + .GetField(enumValue.ToString()) + ?.GetCustomAttribute() + ?.Description == data.basic.kinds.state), ContinueSkill: data.basic.kinds.continueSkill, SpRecoverySkill: data.basic.kinds.spRecoverySkill, ImmediateActive: data.basic.kinds.immediateActive, @@ -123,7 +135,7 @@ protected override IEnumerable Map() { MaxLevel: levels.Keys.Max()), State: new SkillMetadataState( InBattle: data.basic.stateAttr.battle == 1, - SuperArmor: data.basic.stateAttr.superArmor, + SuperArmor: (SuperArmor) data.basic.stateAttr.superArmor, UseInGameTime: data.basic.stateAttr.useInGameTime == 1, IgnoreReduceCooldown: data.basic.stateAttr.ignoreReduceCooldown == 1, CooldownGroupId: data.basic.stateAttr.cooldownGroupID, @@ -138,6 +150,7 @@ private static SkillMetadataRange Convert(RegionSkill region) { Type: region.rangeType switch { "box" => SkillRegion.Box, "cylinder" => SkillRegion.Cylinder, + "circle" => SkillRegion.Cylinder, "frustum" => SkillRegion.Frustum, "hole_cylinder" => SkillRegion.HoleCylinder, "1200" => SkillRegion.None, // skill/60/60012051.xml diff --git a/Maple2.Model/Enum/Buff.cs b/Maple2.Model/Enum/Buff.cs index 28012999b..c369367a8 100644 --- a/Maple2.Model/Enum/Buff.cs +++ b/Maple2.Model/Enum/Buff.cs @@ -32,7 +32,7 @@ public enum BuffCategory { Unknown4 = 4, EnemyDot = 6, Stunned = 7, // ? - Slow = 8, // ? + Slow = 8, BossResistance = 9, Unknown99 = 99, MonsterStunned = 1007, // ? @@ -91,3 +91,10 @@ public enum InvokeEffectType : byte { // 57 (90050351) // Triggered from 10500061 (Sharp Eyes) - IncreaseHealing = 58, } + +public enum BuffCompulsionEventType : byte { + None = 0, + CritChanceOverride = 1, + EvasionChanceOverride = 2, + BlockChance = 3, +} diff --git a/Maple2.Model/Enum/CompulsionEventType.cs b/Maple2.Model/Enum/CompulsionEventType.cs deleted file mode 100644 index e4126d8ad..000000000 --- a/Maple2.Model/Enum/CompulsionEventType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Maple2.Model.Enum; - -public enum CompulsionEventType : byte { - None = 0, - CritChanceOverride = 1, - EvasionChanceOverride = 2, - BlockChance = 3, -} diff --git a/Maple2.Model/Enum/Skill.cs b/Maple2.Model/Enum/Skill.cs index d4dd18970..3dffd0fa8 100644 --- a/Maple2.Model/Enum/Skill.cs +++ b/Maple2.Model/Enum/Skill.cs @@ -184,9 +184,9 @@ public enum EventConditionType { public enum CompulsionType { None = 0, - Critical = 1, - Evasion = 2, - Block = 3, + Hit = 1, + Critical = 2, + Interrupt = 3, // unconfirmed } public enum TargetType { @@ -200,3 +200,19 @@ public enum TargetType { HungryMobs = 8 } + +public enum BounceType { + None = 0, + Range = 1, // within range + Chain = 2, // Bounce continues as long as another entity is in range of the last bounce + Pierce = 3, // Linear + Boomerang = 4, // Shield Toss, Shadow Cutter + Unknown5 = 5, +} + +[Flags] +public enum SuperArmor { + None = 0, + StunImmunity = 1, + KnockbackImmunity = 2, +} diff --git a/Maple2.Model/Metadata/AdditionalEffectMetadata.cs b/Maple2.Model/Metadata/AdditionalEffectMetadata.cs index 7d62fb69d..72b3eb691 100644 --- a/Maple2.Model/Metadata/AdditionalEffectMetadata.cs +++ b/Maple2.Model/Metadata/AdditionalEffectMetadata.cs @@ -85,7 +85,7 @@ public record AdditionalEffectMetadataStatus( int ImmuneBreak, bool Invincible) { - public record CompulsionEvent(CompulsionEventType Type, float Rate, int[] SkillIds); + public record CompulsionEvent(BuffCompulsionEventType Type, float Rate, int[] SkillIds); public record StatConversion(BasicAttribute BaseAttribute, BasicAttribute ResultAttribute, float Rate); } diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index a87539ade..2c68cd5bc 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -102,6 +102,7 @@ public static class Constant { public const int Grade1WeddingCouponItemId = 20303166; public const int Grade2WeddingCouponItemId = 20303167; public const int Grade3WeddingCouponItemId = 20303168; + public const int MinStatIntervalTick = 100; public const int MaxMentees = 3; diff --git a/Maple2.Model/Metadata/SkillMetadata.cs b/Maple2.Model/Metadata/SkillMetadata.cs index b354a1d92..12fa1103a 100644 --- a/Maple2.Model/Metadata/SkillMetadata.cs +++ b/Maple2.Model/Metadata/SkillMetadata.cs @@ -42,7 +42,7 @@ public record SkillMetadataProperty( public record SkillMetadataState( bool InBattle, - int SuperArmor, // 0, 1, 3 + SuperArmor SuperArmor, bool UseInGameTime, int CooldownGroupId, bool IgnoreReduceCooldown, @@ -139,7 +139,7 @@ public record SkillMetadataArrow( bool Explosion, bool RayPhysXTest, SkillTargetType NonTarget, - int BounceType, // 2: chain, 3: pierce + BounceType BounceType, int BounceCount, float BounceRadius, bool BounceOverlap, diff --git a/Maple2.Server.Game/Commands/PlayerCommand.cs b/Maple2.Server.Game/Commands/PlayerCommand.cs index d83f8f5f3..294d6627c 100644 --- a/Maple2.Server.Game/Commands/PlayerCommand.cs +++ b/Maple2.Server.Game/Commands/PlayerCommand.cs @@ -83,7 +83,7 @@ public ExpCommand(GameSession session) : base("exp", "Add player experience.") { private void Handle(InvocationContext ctx, long exp) { try { - session.Exp.AddExp(ExpType.none, exp); + session.Exp.AddExp(ExpType.expDrop, exp); ctx.ExitCode = 0; } catch (SystemException ex) { @@ -138,52 +138,128 @@ public JobCommand(GameSession session) : base("job", "Set player job.") { private void Handle(InvocationContext ctx, JobCode jobCode, bool awakening) { try { - Job job = jobCode switch { + Job selectedJob = jobCode switch { JobCode.Newbie => Job.Newbie, - JobCode.Knight => awakening ? Job.KnightII : Job.Knight, - JobCode.Berserker => awakening ? Job.BerserkerII : Job.Berserker, - JobCode.Wizard => awakening ? Job.WizardII : Job.Wizard, - JobCode.Priest => awakening ? Job.PriestII : Job.Priest, - JobCode.Archer => awakening ? Job.ArcherII : Job.Archer, - JobCode.HeavyGunner => awakening ? Job.HeavyGunnerII : Job.HeavyGunner, - JobCode.Thief => awakening ? Job.ThiefII : Job.Thief, - JobCode.Assassin => awakening ? Job.AssassinII : Job.Assassin, - JobCode.RuneBlader => awakening ? Job.RuneBladerII : Job.RuneBlader, - JobCode.Striker => awakening ? Job.StrikerII : Job.Striker, - JobCode.SoulBinder => awakening ? Job.SoulBinderII : Job.SoulBinder, - _ => throw new ArgumentException($"Invalid JobCode: {jobCode}") + JobCode.Knight => Job.Knight, + JobCode.Berserker => Job.Berserker, + JobCode.Wizard => Job.Wizard, + JobCode.Priest => Job.Priest, + JobCode.Archer => Job.Archer, + JobCode.HeavyGunner => Job.HeavyGunner, + JobCode.Thief => Job.Thief, + JobCode.Assassin => Job.Assassin, + JobCode.RuneBlader => Job.RuneBlader, + JobCode.Striker => Job.Striker, + JobCode.SoulBinder => Job.SoulBinder, + _ => throw new ArgumentException($"Invalid JobCode: {jobCode}"), }; - Job currentJob = session.Player.Value.Character.Job; - if (currentJob.Code() != job.Code()) { - foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) { - skillTab.Skills.Clear(); + if (selectedJob == session.Player.Value.Character.Job) { + if (!awakening) { + // nothing to do + return; } - } else if (job < currentJob) { - foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) { - foreach (int skillId in skillTab.Skills.Keys.ToList()) { - if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) { - skillTab.Skills.Remove(skillId); - } - } + Awaken(ctx, jobCode); + return; + } else { + if (awakening) { + Awaken(ctx, jobCode); + } else { + JobAdvance(selectedJob); } - session.Config.Skill.ResetSkills(SkillRank.Awakening); } - - session.Player.Value.Character.Job = job; - session.Config.Skill.SkillInfo.SetJob(job); - - session.Player.Buffs.Clear(); - session.Player.Buffs.Initialize(); - session.Player.Buffs.LoadFieldBuffs(); - session.Stats.Refresh(); - session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo)); ctx.ExitCode = 0; } catch (SystemException ex) { ctx.Console.Error.WriteLine(ex.Message); ctx.ExitCode = 1; } } + + private void Awaken(InvocationContext ctx, JobCode jobCode) { + Job awakenedJob = jobCode switch { + JobCode.Newbie => Job.Newbie, + JobCode.Knight => Job.KnightII, + JobCode.Berserker => Job.BerserkerII, + JobCode.Wizard => Job.WizardII, + JobCode.Priest => Job.PriestII, + JobCode.Archer => Job.ArcherII, + JobCode.HeavyGunner => Job.HeavyGunnerII, + JobCode.Thief => Job.ThiefII, + JobCode.Assassin => Job.AssassinII, + JobCode.RuneBlader => Job.RuneBladerII, + JobCode.Striker => Job.StrikerII, + JobCode.SoulBinder => Job.SoulBinderII, + _ => throw new ArgumentException($"Invalid JobCode: {jobCode}"), + }; + Job baseJob = jobCode switch { + JobCode.Newbie => Job.Newbie, + JobCode.Knight => Job.Knight, + JobCode.Berserker => Job.Berserker, + JobCode.Wizard => Job.Wizard, + JobCode.Priest => Job.Priest, + JobCode.Archer => Job.Archer, + JobCode.HeavyGunner => Job.HeavyGunner, + JobCode.Thief => Job.Thief, + JobCode.Assassin => Job.Assassin, + JobCode.RuneBlader => Job.RuneBlader, + JobCode.Striker => Job.Striker, + JobCode.SoulBinder => Job.SoulBinder, + _ => throw new ArgumentException($"Invalid JobCode: {jobCode}"), + }; + + if (!session.TableMetadata.ChangeJobTable.Entries.TryGetValue(baseJob, out ChangeJobMetadata? changeJobMetadata)) { + ctx.Console.Error.WriteLine($"Invalid JobCode: {jobCode}"); + return; + } + + if (!session.QuestMetadata.TryGet(changeJobMetadata.StartQuestId, out QuestMetadata? startQuestMetadata)) { + ctx.Console.Error.WriteLine($"Invalid StartQuestId for awakening: {changeJobMetadata.StartQuestId}"); + return; + } + session.Quest.DebugCompleteChapter(startQuestMetadata.Basic.ChapterId); + JobAdvance(awakenedJob); + UnlockMasterSkills(); + } + + private void JobAdvance(Job job) { + Job currentJob = session.Player.Value.Character.Job; + if (currentJob.Code() != job.Code()) { + foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) { + skillTab.Skills.Clear(); + } + } else if (job < currentJob) { + foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) { + foreach (int skillId in skillTab.Skills.Keys.ToList()) { + if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) { + skillTab.Skills.Remove(skillId); + } + } + } + session.Config.Skill.ResetSkills(SkillRank.Awakening); + } + + session.Player.Value.Character.Job = job; + session.Config.Skill.SkillInfo.SetJob(job); + + session.Player.Buffs.Clear(); + session.Player.Buffs.Initialize(); + session.Player.Buffs.LoadFieldBuffs(); + session.Stats.Refresh(); + session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo)); + } + + private void UnlockMasterSkills() { + const int masterSkillQuestId = 40002795; + if (session.Quest.TryGetQuest(masterSkillQuestId, out Quest? quest) && quest.State == QuestState.Completed) { + return; + } + + session.Quest.Start(masterSkillQuestId, true); + if (!session.Quest.TryGetQuest(masterSkillQuestId, out quest)) { + return; + } + session.Quest.Complete(quest, true); + } } private class InfoCommand : Command { @@ -424,21 +500,12 @@ private void Handle(InvocationContext ctx, string trophyId, short grade) { } private void UnlockAllTrophies(InvocationContext ctx) { - try { - ICollection achievementMetadataCollection = achievementMetadataStorage.GetAll(); - foreach (AchievementMetadata metadata in achievementMetadataCollection) { - int trophyId = metadata.Id; - int maxGrade = metadata.Grades.Keys.Max(); - - UnlockTrophy(trophyId, metadata, maxGrade); - } + ctx.Console.Out.WriteLine($"Unlocking all trophies... This may take a few seconds"); + session.Achievement.DebugCompleteAllTrophies(); + session.Achievement.Load(); - ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades."); - ctx.ExitCode = 0; - } catch (Exception ex) { - ctx.Console.Error.WriteLine($"Failed to unlock all trophies: {ex.Message}"); - ctx.ExitCode = 1; - } + ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades."); + ctx.ExitCode = 0; } private void UnlockSingleTrophy(InvocationContext ctx, int trophyId, short grade) { diff --git a/Maple2.Server.Game/Manager/AchievementManager.cs b/Maple2.Server.Game/Manager/AchievementManager.cs index 38c4e3463..0a3c1ddfb 100644 --- a/Maple2.Server.Game/Manager/AchievementManager.cs +++ b/Maple2.Server.Game/Manager/AchievementManager.cs @@ -244,6 +244,42 @@ public bool HasAchievement(int achievementId, int grade = -1) { return achievement.Grades.ContainsKey(grade); } + public void DebugCompleteAllTrophies() { + ICollection achievementMetadataCollection = session.AchievementMetadata.GetAll(); + foreach (AchievementMetadata metadata in achievementMetadataCollection) { + int trophyId = metadata.Id; + int maxGrade = metadata.Grades.Keys.Max(); + + if (!TryGetAchievement(trophyId, out Achievement? achievement)) { + achievement = new Achievement(metadata) { + CurrentGrade = metadata.Grades.Keys.Min(), + RewardGrade = metadata.Grades.Keys.Min(), + }; + GameStorage.Request db = session.GameStorage.Context(); + achievement = db.CreateAchievement(metadata.AccountWide ? session.AccountId : session.CharacterId, achievement); + if (achievement == null) { + throw new InvalidOperationException($"Failed to create achievement: {metadata.Id}"); + } + if (metadata.AccountWide) { + accountValues.Add(metadata.Id, achievement); + } else { + characterValues.Add(metadata.Id, achievement); + } + } + + for (int grade = achievement.CurrentGrade; grade <= maxGrade; grade++) { + if (achievement.Grades.ContainsKey(grade)) { + achievement.Grades[grade] = DateTime.Now.ToEpochSeconds(); + GiveReward(achievement); + continue; + } + achievement.Grades.Add(grade, DateTime.Now.ToEpochSeconds()); + GiveReward(achievement); + } + achievement.CurrentGrade = maxGrade; + } + } + public void Save(GameStorage.Request db) { db.SaveAchievements(session.AccountId, accountValues.Values.ToList()); db.SaveAchievements(session.CharacterId, characterValues.Values.ToList()); diff --git a/Maple2.Server.Game/Manager/Config/BuffManager.cs b/Maple2.Server.Game/Manager/Config/BuffManager.cs index fb04ffdd3..49c21d58a 100644 --- a/Maple2.Server.Game/Manager/Config/BuffManager.cs +++ b/Maple2.Server.Game/Manager/Config/BuffManager.cs @@ -26,7 +26,7 @@ public class BuffManager : IUpdatable { public IActor Actor { get; private set; } private readonly ConcurrentDictionary> buffs = []; public IDictionary> Invokes { get; init; } - public IDictionary> Compulsions { get; init; } + public IDictionary> Compulsions { get; init; } private Dictionary Resistances { get; } = new(); public ConcurrentDictionary CooldownTimes { get; } = new(); // TODO: Cache this public ReflectRecord? Reflect; @@ -35,7 +35,7 @@ public class BuffManager : IUpdatable { public BuffManager(IActor actor) { Actor = actor; Invokes = new ConcurrentDictionary>(); - Compulsions = new ConcurrentDictionary>(); + Compulsions = new ConcurrentDictionary>(); } public void Initialize() { @@ -157,8 +157,6 @@ public void AddBuff(IActor caster, IActor owner, int id, short level, long start SetUpdates(buff); ModifyBuffStackCount(buff); - owner.ApplyEffects(buff.Metadata.Skills, caster, owner, type, skillId: id, buffId: id, targets: [owner]); - // refresh stats if needed if (buff.Metadata.Status.Values.Any() || buff.Metadata.Status.Rates.Any() || buff.Metadata.Status.SpecialValues.Any() || buff.Metadata.Status.SpecialRates.Any()) { Actor.Stats.Refresh(); @@ -286,7 +284,7 @@ private void SetCompulsionEvent(Buff buff) { return; } - CompulsionEventType eventType = buff.Metadata.Status.Compulsion.Type; + BuffCompulsionEventType eventType = buff.Metadata.Status.Compulsion.Type; if (Compulsions.TryGetValue(eventType, out IDictionary? nestedCompulsionDic)) { Compulsions.RemoveAll(buff.Id); @@ -301,7 +299,7 @@ private void SetCompulsionEvent(Buff buff) { }); } - public float TotalCompulsionRate(CompulsionEventType type, int skillId = 0) { + public float TotalCompulsionRate(BuffCompulsionEventType type, int skillId = 0) { if (!Compulsions.TryGetValue(type, out IDictionary? nestedCompulsionDic)) { return 0; } diff --git a/Maple2.Server.Game/Manager/NpcScriptManager.cs b/Maple2.Server.Game/Manager/NpcScriptManager.cs index 2bbd58a13..0b57ba245 100644 --- a/Maple2.Server.Game/Manager/NpcScriptManager.cs +++ b/Maple2.Server.Game/Manager/NpcScriptManager.cs @@ -376,7 +376,7 @@ public void ProcessScriptFunction(bool enter = true) { } if (!string.IsNullOrEmpty(scriptFunction.MoveMapMovie)) { - session.Send(NpcTalkPacket.Cutscene(scriptFunction.MoveMapMovie)); + session.Send(NpcTalkPacket.Cutscene(scriptFunction.MoveMapMovie, scriptFunction.MoveMapId)); } if (scriptFunction.PortalId > 0 && session.Field.TryGetPortal(scriptFunction.PortalId, out FieldPortal? dstPortal)) { diff --git a/Maple2.Server.Game/Manager/QuestManager.cs b/Maple2.Server.Game/Manager/QuestManager.cs index d29119e2e..34efd9f93 100644 --- a/Maple2.Server.Game/Manager/QuestManager.cs +++ b/Maple2.Server.Game/Manager/QuestManager.cs @@ -583,6 +583,51 @@ public void LevelPotion(int level, int lastQuest = 0) { Load(); } + /// + /// Only used for debugging purposes. Completes all quests in the chapter silently. + /// + /// + public void DebugCompleteChapter(int chapterId) { + using GameStorage.Request db = session.GameStorage.Context(); + IEnumerable questIds = session.QuestMetadata.GetQuestsByChapter(chapterId).Select(q => q.Id); + foreach (int questId in questIds) { + if (TryGetQuest(questId, out Quest? quest)) { + if (quest.State == QuestState.Completed) { + continue; + } + + quest.State = QuestState.Completed; + quest.CompletionCount++; + quest.EndTime = DateTime.Now.ToEpochSeconds(); + } else { + if (!session.QuestMetadata.TryGet(questId, out QuestMetadata? metadata)) { + continue; + } + var newQuest = new Quest(metadata) { + State = QuestState.Completed, + StartTime = DateTime.Now.ToEpochSeconds(), + EndTime = DateTime.Now.ToEpochSeconds() + 1, + CompletionCount = 1, + Track = true, + }; + + for (int i = 0; i < newQuest.Metadata.Conditions.Length; i++) { + newQuest.Conditions.Add(i, new Quest.Condition(newQuest.Metadata.Conditions[i])); + newQuest.Conditions[i].Counter = (int) newQuest.Conditions[i].Metadata.Value; + } + + long ownerId = newQuest.Metadata.Basic.Account > 0 ? session.AccountId : session.CharacterId; + newQuest = db.CreateQuest(ownerId, newQuest); + if (newQuest == null) { + logger.Error("Failed to create quest entry {questId}", metadata.Id); + continue; + } + Add(newQuest); + } + } + + Load(); + } public void Save(GameStorage.Request db) { db.SaveQuests(session.AccountId, accountValues.Values); db.SaveQuests(session.CharacterId, characterValues.Values); diff --git a/Maple2.Server.Game/Manager/StatsManager.cs b/Maple2.Server.Game/Manager/StatsManager.cs index c6841e535..25f2090aa 100644 --- a/Maple2.Server.Game/Manager/StatsManager.cs +++ b/Maple2.Server.Game/Manager/StatsManager.cs @@ -180,8 +180,9 @@ private void AddBuffs(FieldPlayer player) { foreach ((BasicAttribute valueBasicAttribute, long value) in buff.Metadata.Status.Values) { Values[valueBasicAttribute].AddTotal(value); } - foreach ((BasicAttribute ratespecialAttribute, float rate) in buff.Metadata.Status.Rates) { - Values[ratespecialAttribute].AddRate(rate); + foreach ((BasicAttribute rateBasicAttribute, float rate) in buff.Metadata.Status.Rates) { + // ensure regen intervals do not drop below 0.1 + Values[rateBasicAttribute].AddRate(rate); } foreach ((SpecialAttribute valueSpecialAttribute, float value) in buff.Metadata.Status.SpecialValues) { Values[valueSpecialAttribute].AddTotal((long) value); diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index d2250bb2e..d3d8938a5 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -51,6 +51,12 @@ public virtual Vector3 Rotation { public virtual BuffManager Buffs { get; } public Lua.Lua Lua { get; init; } + /// + /// Counter for skill casting ID creation + /// + private int localIdCounter = 1; + protected int NextLocalId() => Interlocked.Increment(ref localIdCounter); + /// /// Tick duration of actor in the same position. /// @@ -192,10 +198,16 @@ public virtual void TargetAttack(SkillRecord record) { ApplyEffects(record.Attack.Skills, record.Caster, this, skillId: record.SkillId, targets: record.Targets.Values.ToArray()); ApplyEffects(record.Attack.SkillsOnDamage, record.Caster, damage, record.Targets.Values.ToArray()); - /*foreach (SkillEffectMetadata effect in record.Attack.Skills.Where(e => e.Splash != null)) { - // This should not be sent on init skill use from PLAYER because a Splash skill packet is sent from client to server. - // Field.AddSkill(record.Caster, effect, [record.Caster.Position], record.Caster.Rotation); - }*/ + foreach (IActor target in record.Targets.Values) { + foreach (SkillEffectMetadata effect in record.Attack.Skills.Where(e => e.Splash != null)) { + Field.AddSkill(record.Caster, effect, [target.Position], record.Caster.Rotation); + } + } + + } + + public virtual void SkillAttackPoint(SkillRecord record, byte attackPoint) { + } public virtual IActor GetTarget(SkillTargetType targetType, IActor caster, IActor target, IActor owner) { @@ -304,19 +316,21 @@ public virtual void Update(long tickCount) { public virtual void KeyframeEvent(string keyName) { } - public virtual SkillRecord? CastSkill(int id, short level, long uid = 0, byte motionPoint = 0) { + public virtual SkillRecord? CastSkill(int id, short level, long uid, int castTick, in Vector3 position = default, in Vector3 direction = default, in Vector3 rotation = default, float rotateZ = 0f, byte motionPoint = 0) { if (!Field.SkillMetadata.TryGet(id, level, out SkillMetadata? metadata)) { Logger.Error("Invalid skill use: {SkillId},{Level}", id, level); return null; } var record = new SkillRecord(metadata, uid, this) { - Position = Position, - Rotation = Rotation, + Position = position == default ? Position : position, + Rotation = Rotation == default ? Rotation : rotation, Rotate2Z = 2 * Rotation.Z, + ServerTick = castTick, }; if (!record.TrySetMotionPoint(motionPoint)) { + Logger.Error("Invalid MotionPoint({MotionPoint}) for {Record}", motionPoint, record); return 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 13ec26981..67c410a58 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 @@ -89,7 +89,7 @@ private void SkillCast(NpcSkillCastTask task, int id, short level, long uid, byt Velocity = new Vector3(0, 0, 0); - SkillRecord? cast = actor.CastSkill(id, level, uid, motion); + SkillRecord? cast = actor.CastSkill(id, level, uid, (int) actor.Field.FieldTick, motionPoint: motion); if (cast is null) { task.Cancel(); diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index 7403d67f7..02223f832 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -353,7 +353,7 @@ public void DropLoot(FieldPlayer firstPlayer) { } } - public override SkillRecord? CastSkill(int id, short level, long uid = 0, byte motionPoint = 0) { + public override SkillRecord? CastSkill(int id, short level, long uid, int castTick, in Vector3 position = default, in Vector3 direction = default, in Vector3 rotation = default, float rotateZ = 0f, 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); return null; @@ -361,13 +361,12 @@ public void DropLoot(FieldPlayer firstPlayer) { if (uid == 0) { // The client derives the player's skill cast skillSn/uid using this formula so I'm using it here for mob casts for parity. - uid = (long) ((DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds * 100000) % (long) 1e14; + uid = (long) NextLocalId() << 32 | (uint) Environment.TickCount; } Field.Broadcast(NpcControlPacket.Control(this)); - var cast = base.CastSkill(id, level, uid, motionPoint); - + SkillRecord? cast = base.CastSkill(id, level, uid, castTick, motionPoint: motionPoint); return cast; } diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs index bbca1d7fa..4a00288f6 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs @@ -64,6 +64,7 @@ protected set { private long StateSyncTrackingTick { get; set; } public Tombstone? Tombstone { get; set; } + public DeathState DeathState { get => Value.Character.DeathState; set { @@ -140,6 +141,7 @@ public bool DebugAi { public override void Update(long tickCount) { base.Update(tickCount); + Session.GameEvent.Update(tickCount); if (Flag != PlayerObjectFlag.None && tickCount > flagTick) { Field.Broadcast(ProxyObjectPacket.UpdatePlayer(this, Flag)); @@ -161,44 +163,72 @@ public override void Update(long tickCount) { InBattle = false; } - if (!IsDead) { - // Loops through each registered regen stat and applies regen - var statsToRemove = new List(); - foreach (BasicAttribute attribute in regenStats.Keys) { - Stat stat = Stats.Values[attribute]; - Stat regen = Stats.Values[regenStats[attribute].Item1]; - Stat interval = Stats.Values[regenStats[attribute].Item2]; - - if (stat.Current >= stat.Total) { - // Removes stat from regen stats so it won't be listened for - statsToRemove.Add(attribute); - continue; - } + //Console.WriteLine($"State: {State}"); - lastRegenTime.TryGetValue(attribute, out long regenTime); - - if (tickCount - regenTime > interval.Base) { - lastRegenTime[attribute] = tickCount; - switch (attribute) { - case BasicAttribute.Health: - RecoverHp((int) regen.Total); - continue; - case BasicAttribute.Spirit: - RecoverSp((int) regen.Total); - continue; - case BasicAttribute.Stamina: - RecoverStamina((int) regen.Total); - continue; - } - Session.Send(StatsPacket.Update(this, attribute)); - } + UpdateStateSkill(); + + if (IsDead) { + return; + } + // Loops through each registered regen stat and applies regen + var statsToRemove = new List(); + foreach (BasicAttribute attribute in regenStats.Keys) { + Stat stat = Stats.Values[attribute]; + Stat regen = Stats.Values[regenStats[attribute].Item1]; + Stat interval = Stats.Values[regenStats[attribute].Item2]; + + if (stat.Current >= stat.Total) { + // Removes stat from regen stats so it won't be listened for + statsToRemove.Add(attribute); + continue; } - foreach (BasicAttribute attribute in statsToRemove) { - regenStats.Remove(attribute); + + lastRegenTime.TryGetValue(attribute, out long regenTime); + + if (tickCount - regenTime > Math.Max(interval.Current, Constant.MinStatIntervalTick)) { + lastRegenTime[attribute] = tickCount; + switch (attribute) { + case BasicAttribute.Health: + RecoverHp((int) regen.Total); + continue; + case BasicAttribute.Spirit: + RecoverSp((int) regen.Total); + continue; + case BasicAttribute.Stamina: + RecoverStamina((int) regen.Total); + continue; + } + Session.Send(StatsPacket.Update(this, attribute)); } } + foreach (BasicAttribute attribute in statsToRemove) { + regenStats.Remove(attribute); + } + CheckRegen(); + return; - Session.GameEvent.Update(tickCount); + void UpdateStateSkill() { + SkillRecord? stateSkill = ActiveSkills.StateSkill; + if (stateSkill == null) { + return; + } + + if (stateSkill.StateNextTick > tickCount) { + return; + } + + if (stateSkill.Metadata.Property.State != State) { + Field.Broadcast(SkillPacket.Cancel(stateSkill)); + ActiveSkills.StateSkill = null; + return; + } + + stateSkill.StateNextTick = tickCount + (int) TimeSpan.FromSeconds(stateSkill.Motion.MotionProperty.SequenceSpeed).TotalMilliseconds; + if (!SkillCastConsume(stateSkill)) { + ActiveSkills.StateSkill = null; + Field.Broadcast(SkillPacket.Cancel(stateSkill)); + } + } } public void OnStateSync(StateSync stateSync) { @@ -512,19 +542,19 @@ public void ConsumeStamina(int amount, bool noRegen = false) { public void CheckRegen() { // Health - var health = Stats.Values[BasicAttribute.Health]; + Stat health = Stats.Values[BasicAttribute.Health]; if (health.Current < health.Total && !regenStats.ContainsKey(BasicAttribute.Health)) { regenStats.Add(BasicAttribute.Health, new Tuple(BasicAttribute.HpRegen, BasicAttribute.HpRegenInterval)); } // Spirit - var spirit = Stats.Values[BasicAttribute.Spirit]; + Stat spirit = Stats.Values[BasicAttribute.Spirit]; if (spirit.Current < spirit.Total && !regenStats.ContainsKey(BasicAttribute.Spirit)) { regenStats.Add(BasicAttribute.Spirit, new Tuple(BasicAttribute.SpRegen, BasicAttribute.SpRegenInterval)); } // Stamina - var stamina = Stats.Values[BasicAttribute.Stamina]; + Stat stamina = Stats.Values[BasicAttribute.Stamina]; if (stamina.Current < stamina.Total && !regenStats.ContainsKey(BasicAttribute.Stamina)) { regenStats.Add(BasicAttribute.Stamina, new Tuple(BasicAttribute.StaminaRegen, BasicAttribute.StaminaRegenInterval)); } diff --git a/Maple2.Server.Game/Model/Field/Actor/IActor.cs b/Maple2.Server.Game/Model/Field/Actor/IActor.cs index 9efa39a53..34b7ccc7c 100644 --- a/Maple2.Server.Game/Model/Field/Actor/IActor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/IActor.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Numerics; using Maple2.Database.Storage; using Maple2.Model.Enum; using Maple2.Model.Metadata; @@ -11,7 +12,6 @@ namespace Maple2.Server.Game.Model; public interface IActor : IFieldEntity { - protected static readonly ConcurrentDictionary NoBuffs = new(); public NpcMetadataStorage? NpcMetadata { get; init; } public BuffManager Buffs { get; } @@ -31,7 +31,7 @@ public virtual void ApplyDamage(IActor caster, DamageRecord damage, SkillMetadat public virtual void TargetAttack(SkillRecord record) { } - public virtual SkillRecord? CastSkill(int id, short level, long uid = 0, byte motionPoint = 0) { return null; } + public virtual SkillRecord? CastSkill(int id, short level, long uid, int castTick, in Vector3 position = default, in Vector3 direction = default, in Vector3 rotation = default, float rotateZ = 0f, byte motionPoint = 0) { return null; } public virtual void KeyframeEvent(string keyName) { } } diff --git a/Maple2.Server.Game/Model/Skill/SkillQueue.cs b/Maple2.Server.Game/Model/Skill/SkillQueue.cs index 19075ab8a..1f4182977 100644 --- a/Maple2.Server.Game/Model/Skill/SkillQueue.cs +++ b/Maple2.Server.Game/Model/Skill/SkillQueue.cs @@ -1,4 +1,6 @@ -namespace Maple2.Server.Game.Model.Skill; +using Maple2.Model.Enum; + +namespace Maple2.Server.Game.Model.Skill; // Used to keep track and lookup pending skills // This is a circular array and will overwrite old pending skills @@ -6,6 +8,7 @@ public class SkillQueue { private const int MAX_PENDING = 3; private readonly SkillRecord?[] casts; + public SkillRecord? StateSkill; private int index; public SkillQueue() { @@ -16,6 +19,10 @@ public SkillQueue() { public void Add(SkillRecord cast) { casts[index] = cast; + if (cast.Metadata.Property.State != ActorState.None) { + StateSkill = cast; + } + index = (index + 1) % MAX_PENDING; } diff --git a/Maple2.Server.Game/Model/Skill/SkillRecord.cs b/Maple2.Server.Game/Model/Skill/SkillRecord.cs index 936528459..c975f7e00 100644 --- a/Maple2.Server.Game/Model/Skill/SkillRecord.cs +++ b/Maple2.Server.Game/Model/Skill/SkillRecord.cs @@ -32,6 +32,8 @@ public class SkillRecord { public string HoldString = string.Empty; public long ItemUid; + public long StateNextTick; // Used for state skills only + public ConcurrentDictionary Targets; public SkillRecord(SkillMetadata metadata, long castUid, IActor caster) { diff --git a/Maple2.Server.Game/Model/Stats.cs b/Maple2.Server.Game/Model/Stats.cs index dd4df362d..cac79642a 100644 --- a/Maple2.Server.Game/Model/Stats.cs +++ b/Maple2.Server.Game/Model/Stats.cs @@ -167,14 +167,14 @@ public Stat(long total, long @base, long current) { } public void AddBase(long amount) { - Total += amount; - Base += amount; - Current += amount; + Total = Math.Max(0, Total + amount); + Base = Math.Max(0, Base + amount); + Current = Math.Max(0, Current + amount); } public void AddTotal(long amount) { - Total += amount; - Current += amount; + Total = Math.Max(0, Total + amount); + Current = Math.Max(0, Current + amount); } public void AddTotal(BasicOption option) { diff --git a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs index 5fa5de337..9dfa7d5ff 100644 --- a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs @@ -29,7 +29,7 @@ private enum Command : byte { private enum SubCommand : byte { Point = 0, Target = 1, - Splash = 2, + CubeMagicPath = 2, } #region Autofac Autowired @@ -54,8 +54,8 @@ public override void Handle(GameSession session, IByteReader packet) { case SubCommand.Target: HandleTarget(session, packet); return; - case SubCommand.Splash: - HandleSplash(session, packet); + case SubCommand.CubeMagicPath: + HandleCubeMagicPath(session, packet); return; } return; @@ -85,28 +85,19 @@ private void HandleUse(GameSession session, IByteReader packet) { return; } } - - if (!SkillMetadata.TryGet(skillId, level, out SkillMetadata? metadata)) { - Logger.Error("Invalid skill use: {SkillId},{Level}", skillId, level); - return; - } - - var record = new SkillRecord(metadata, skillUid, session.Player) { - ServerTick = serverTick, - }; byte motionPoint = packet.ReadByte(); - if (!record.TrySetMotionPoint(motionPoint)) { - Logger.Error("Invalid MotionPoint({MotionPoint}) for {Record}", motionPoint, record); + var position = packet.Read(); + var direction = packet.Read(); + var rotation = packet.Read(); + float rotate2Z = packet.ReadFloat(); // Rotation2Z + + SkillRecord? record = session.Player.CastSkill(skillId, level, skillUid, serverTick, position, direction, rotation, rotate2Z, motionPoint); + if (record == null) { return; } - SkillMetadataMotionProperty motion = metadata.Data.Motions.First().MotionProperty; - session.Animation.TryPlaySequence(motion.SequenceName, motion.SequenceSpeed, AnimationType.Skill, metadata); - - record.Position = packet.Read(); - record.Direction = packet.Read(); - record.Rotation = packet.Read(); - record.Rotate2Z = packet.ReadFloat(); // Rotation2Z + SkillMetadataMotionProperty motion = record.Metadata.Data.Motions.First().MotionProperty; + session.Animation.TryPlaySequence(motion.SequenceName, motion.SequenceSpeed, AnimationType.Skill, record.Metadata); packet.ReadInt(); // ClientTick record.Unknown = packet.ReadBool(); // UnkBool @@ -128,16 +119,16 @@ private void HandleUse(GameSession session, IByteReader packet) { } long startTick = session.Field.FieldTick; - session.Player.InBattle = metadata.State.InBattle; + session.Player.InBattle = record.Metadata.State.InBattle; session.Player.ActiveSkills.Add(record); session.Field.Broadcast(SkillPacket.Use(record)); session.Field.Broadcast(StatsPacket.Init(session.Player)); - session.Player.ApplyEffects(metadata.Data.Skills, session.Player, session.Player, EventConditionType.Activate, skillId: metadata.Id, targets: [session.Player]); - session.Buffs.TriggerEvent(session.Player, session.Player, session.Player, EventConditionType.OnSkillCasted, skillId: metadata.Id); + session.Player.ApplyEffects(record.Metadata.Data.Skills, session.Player, session.Player, EventConditionType.Activate, skillId: record.SkillId, targets: [session.Player]); + session.Buffs.TriggerEvent(session.Player, session.Player, session.Player, EventConditionType.OnSkillCasted, skillId: record.SkillId); session.ConditionUpdate(ConditionType.skill, codeLong: skillId, targetLong: session.Field.MapId); - session.Config.SaveSkillCooldown(metadata, startTick); + session.Config.SaveSkillCooldown(record.Metadata, startTick); } private void HandlePoint(GameSession session, IByteReader packet) { @@ -265,7 +256,7 @@ private void HandleTarget(GameSession session, IByteReader packet) { session.Player.TargetAttack(record); } - private void HandleSplash(GameSession session, IByteReader packet) { + private void HandleCubeMagicPath(GameSession session, IByteReader packet) { long skillUid = packet.ReadLong(); SkillRecord? record = session.Player.ActiveSkills.Get(skillUid); if (record == null) { @@ -284,13 +275,13 @@ private void HandleSplash(GameSession session, IByteReader packet) { Logger.Error("Unhandled skill-MagicPath value1({Value}): {Record}", unknown1, record); } - int unknown2 = packet.ReadInt(); // Unknown(0) - if (unknown2 != 0) { - Logger.Error("Unhandled skill-MagicPath value2({Value}): {Record}", unknown2, record); + int attackIndex = packet.ReadInt(); // Unknown(0) + if (attackIndex != 0) { + Logger.Error("Unhandled skill-MagicPath attackIndex ({Value}): {Record}", attackIndex, record); } if (session.Player.DebugSkills) { - session.Send(NoticePacket.Message($"Skill.Attack.Region: {skillUid}; AttackPoint: {attackPoint}; UnkInt: {unknown1}; UnkInt: {unknown2}")); + session.Send(NoticePacket.Message($"Skill.Attack.Region: {skillUid}; AttackPoint: {attackPoint}; UnkInt: {unknown1}; UnkInt: {attackIndex}")); } record.Position = packet.Read(); @@ -330,13 +321,7 @@ private void HandleSync(GameSession session, IByteReader packet) { bool isRelease = packet.ReadBool(); int unk3 = packet.ReadInt(); - /* TODO: Consume when new iteration - if (!session.Player.SkillCastConsume(record)) { - session.Send(SkillUseFailedPacket.Fail(record)); - return; - }*/ - - //session.Field?.Broadcast(SkillPacket.Sync(record), session); + session.Field?.Broadcast(SkillPacket.Sync(record), session); if (session.Player.DebugSkills) { session.Send(NoticePacket.Message($"Skill.Sync: {skillId},{skillUid}; AttackPoint: {motionPoint}; IsCharge: {isCharge}; IsReleased: {isRelease}; UnkInt: {unk3}; Direction: {direction}")); @@ -361,6 +346,11 @@ private void HandleTickSync(GameSession session, IByteReader packet) { session.Send(NoticePacket.Message($"Skill.SyncTick: {skillUid}")); } + if (!session.Player.SkillCastConsume(record)) { + session.Send(SkillUseFailedPacket.Fail(record)); + return; + } + string skillSequence = record.Motion.MotionProperty.SequenceName; string playingSequence = session.Player.Animation.PlayingSequence?.Name ?? ""; @@ -381,7 +371,7 @@ private void HandleCancel(GameSession session, IByteReader packet) { } session.Player.InBattle = true; - session.Field?.Broadcast(SkillPacket.Cancel(record), session); + session.Field.Broadcast(SkillPacket.Cancel(record)); if (session.Player.DebugSkills) { session.Send(NoticePacket.Message($"Skill.Cancel: {skillUid}")); diff --git a/Maple2.Server.Game/PacketHandlers/StateSkillHandler.cs b/Maple2.Server.Game/PacketHandlers/StateSkillHandler.cs index 42c56c421..0dc4da46a 100644 --- a/Maple2.Server.Game/PacketHandlers/StateSkillHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/StateSkillHandler.cs @@ -1,7 +1,9 @@ using Maple2.Model.Enum; +using Maple2.Model.Metadata; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Server.Core.PacketHandlers; +using Maple2.Server.Game.Model.Skill; using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; @@ -12,15 +14,16 @@ public class StateSkillHandler : PacketHandler { public override void Handle(GameSession session, IByteReader packet) { byte function = packet.ReadByte(); - if (function != 0 || session.Field == null) { + if (function != 0) { + Logger.Warning("Unhandled StateSkill function: {Function}", function); return; } long skillCastUid = packet.ReadLong(); int serverTick = packet.ReadInt(); int skillId = packet.ReadInt(); - packet.ReadShort(); // 1 - session.Player.State = (ActorState) packet.ReadInt(); + short skillLevel = packet.ReadShort(); + var state = (ActorState) packet.ReadInt(); int clientTick = packet.ReadInt(); long itemUid = packet.ReadLong(); @@ -28,6 +31,17 @@ public override void Handle(GameSession session, IByteReader packet) { return; // Invalid item } + if (!session.Field.SkillMetadata.TryGet(skillId, skillLevel, out SkillMetadata? metadata)) { + return; + } + + var cast = new SkillRecord(metadata, skillCastUid, session.Player); + if (!session.Player.SkillCastConsume(cast)) { + return; + } + + cast.StateNextTick = session.Field.FieldTick + (int) TimeSpan.FromSeconds(cast.Motion.MotionProperty.SequenceSpeed).TotalMilliseconds; + session.Player.ActiveSkills.Add(cast); session.Field.Broadcast(SkillPacket.StateSkill(session.Player, skillId, skillCastUid)); } } diff --git a/Maple2.Server.Game/Packets/NpcTalkPacket.cs b/Maple2.Server.Game/Packets/NpcTalkPacket.cs index afa05c10a..b9818aee5 100644 --- a/Maple2.Server.Game/Packets/NpcTalkPacket.cs +++ b/Maple2.Server.Game/Packets/NpcTalkPacket.cs @@ -98,10 +98,11 @@ public static ByteWriter RewardMeso(long mesos) { return pWriter; } - public static ByteWriter Cutscene(string movieString) { + public static ByteWriter Cutscene(string movieString, int mapId) { var pWriter = Packet.Of(SendOp.NpcTalk); pWriter.Write(Command.Action); pWriter.Write(NpcTalkAction.Cutscene); + pWriter.WriteInt(mapId); pWriter.WriteUnicodeString(movieString); return pWriter; diff --git a/Maple2.Server.Game/Service/ChannelService.Heartbeat.cs b/Maple2.Server.Game/Service/ChannelService.Heartbeat.cs index 7a2d8494b..a8463e807 100644 --- a/Maple2.Server.Game/Service/ChannelService.Heartbeat.cs +++ b/Maple2.Server.Game/Service/ChannelService.Heartbeat.cs @@ -1,16 +1,23 @@ using Grpc.Core; using Maple2.Server.Core.Packets; using Maple2.Server.Game.Session; +using Serilog; namespace Maple2.Server.Game.Service; public partial class ChannelService { public override Task Heartbeat(HeartbeatRequest request, ServerCallContext context) { if (request.CharacterId == 0) { - throw new RpcException(new Status(StatusCode.NotFound, "Character ID is 0.")); + Log.Warning("Heartbeat from unknown session. No character id."); + return Task.FromResult(new HeartbeatResponse { + Success = false, + }); } if (!server.GetSession(request.CharacterId, out GameSession? session)) { - throw new RpcException(new Status(StatusCode.NotFound, "Session not found.")); + Log.Warning("Heartbeat from unknown session: {CharacterId}", request.CharacterId); + return Task.FromResult(new HeartbeatResponse { + Success = false, + }); } session.Send(RequestPacket.Heartbeat()); diff --git a/Maple2.Server.Game/Util/DamageCalculator.cs b/Maple2.Server.Game/Util/DamageCalculator.cs index b45667ed0..4d0f8e158 100644 --- a/Maple2.Server.Game/Util/DamageCalculator.cs +++ b/Maple2.Server.Game/Util/DamageCalculator.cs @@ -10,12 +10,12 @@ namespace Maple2.Server.Game.Util; public static class DamageCalculator { public static (DamageType, long) CalculateDamage(IActor caster, IActor target, DamagePropertyRecord property) { // Check block - if (Damage.TriggerCompulsionEvent(target.Buffs.TotalCompulsionRate(CompulsionEventType.BlockChance, property.SkillId))) { + if (Damage.TriggerCompulsionEvent(target.Buffs.TotalCompulsionRate(BuffCompulsionEventType.BlockChance, property.SkillId))) { return (DamageType.Block, 0); } // Check evase - if (Damage.TriggerCompulsionEvent(target.Buffs.TotalCompulsionRate(CompulsionEventType.EvasionChanceOverride, property.SkillId))) { + if (Damage.TriggerCompulsionEvent(target.Buffs.TotalCompulsionRate(BuffCompulsionEventType.EvasionChanceOverride, property.SkillId))) { return (DamageType.Miss, 0); } @@ -80,7 +80,7 @@ public static (DamageType, long) CalculateDamage(IActor caster, IActor target, D } if (damageType != DamageType.Critical) { - damageType = caster.Stats.GetCriticalRate(target.Stats.Values[BasicAttribute.CriticalEvasion].Total, caster.Buffs.TotalCompulsionRate(CompulsionEventType.CritChanceOverride, property.SkillId)); + damageType = caster.Stats.GetCriticalRate(target.Stats.Values[BasicAttribute.CriticalEvasion].Total, caster.Buffs.TotalCompulsionRate(BuffCompulsionEventType.CritChanceOverride, property.SkillId)); } } diff --git a/Maple2.Server.World/Migrations/20250304061437_InteractCubeFix.cs b/Maple2.Server.World/Migrations/20250304061437_InteractCubeFix.cs index 7cfa98f82..18031f555 100644 --- a/Maple2.Server.World/Migrations/20250304061437_InteractCubeFix.cs +++ b/Maple2.Server.World/Migrations/20250304061437_InteractCubeFix.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Maple2.Server.Core.Constants; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -7,8 +8,9 @@ namespace Maple2.Server.World.Migrations { public partial class InteractCubeFix : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.Sql("DELETE FROM `game-server`.`ugcmap-cube` WHERE `Interact` IS NOT NULL AND `Interact` <> '';"); - migrationBuilder.Sql("DELETE FROM `game-server`.`home-layout-cube` WHERE `Interact` IS NOT NULL AND `Interact` <> '';"); + var dbName = Environment.GetEnvironmentVariable("GaME_DB_NAME"); + migrationBuilder.Sql($"DELETE FROM `{dbName}`.`ugcmap-cube` WHERE `Interact` IS NOT NULL AND `Interact` <> '';"); + migrationBuilder.Sql($"DELETE FROM `{dbName}`.`home-layout-cube` WHERE `Interact` IS NOT NULL AND `Interact` <> '';"); } /// From 8c067895385f0ec859c1548366ea995ffeddb0ff Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Tue, 13 May 2025 20:41:56 -0700 Subject: [PATCH 2/4] Remove debug line --- Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs index 4a00288f6..bf5a69f20 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs @@ -163,8 +163,6 @@ public override void Update(long tickCount) { InBattle = false; } - //Console.WriteLine($"State: {State}"); - UpdateStateSkill(); if (IsDead) { From cbb8f6b788192e01e4538f487f6602ead9f56f3f Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Tue, 13 May 2025 21:16:54 -0700 Subject: [PATCH 3/4] rabbit comments --- Maple2.Server.Game/Commands/PlayerCommand.cs | 2 +- Maple2.Server.Game/Manager/AchievementManager.cs | 4 ++-- Maple2.Server.Game/Model/Field/Actor/Actor.cs | 2 +- Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs | 3 +-- Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Maple2.Server.Game/Commands/PlayerCommand.cs b/Maple2.Server.Game/Commands/PlayerCommand.cs index 294d6627c..bbb7cb041 100644 --- a/Maple2.Server.Game/Commands/PlayerCommand.cs +++ b/Maple2.Server.Game/Commands/PlayerCommand.cs @@ -157,10 +157,10 @@ private void Handle(InvocationContext ctx, JobCode jobCode, bool awakening) { if (selectedJob == session.Player.Value.Character.Job) { if (!awakening) { // nothing to do + ctx.ExitCode = 0; return; } Awaken(ctx, jobCode); - return; } else { if (awakening) { Awaken(ctx, jobCode); diff --git a/Maple2.Server.Game/Manager/AchievementManager.cs b/Maple2.Server.Game/Manager/AchievementManager.cs index 0a3c1ddfb..f5dd40ef8 100644 --- a/Maple2.Server.Game/Manager/AchievementManager.cs +++ b/Maple2.Server.Game/Manager/AchievementManager.cs @@ -261,9 +261,9 @@ public void DebugCompleteAllTrophies() { throw new InvalidOperationException($"Failed to create achievement: {metadata.Id}"); } if (metadata.AccountWide) { - accountValues.Add(metadata.Id, achievement); + accountValues.TryAdd(metadata.Id, achievement); } else { - characterValues.Add(metadata.Id, achievement); + characterValues.TryAdd(metadata.Id, achievement); } } diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index d3d8938a5..4c9d5a86e 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -325,7 +325,7 @@ public virtual void KeyframeEvent(string keyName) { } var record = new SkillRecord(metadata, uid, this) { Position = position == default ? Position : position, Rotation = Rotation == default ? Rotation : rotation, - Rotate2Z = 2 * Rotation.Z, + Rotate2Z = rotateZ, ServerTick = castTick, }; diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index 02223f832..5dd609853 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -366,8 +366,7 @@ public void DropLoot(FieldPlayer firstPlayer) { Field.Broadcast(NpcControlPacket.Control(this)); - SkillRecord? cast = base.CastSkill(id, level, uid, castTick, motionPoint: motionPoint); - return cast; + return base.CastSkill(id, level, uid, castTick, position, direction, rotation, rotateZ, motionPoint); } public NpcTask CastAiSkill(int id, short level, int faceTarget, Vector3 facePos, long uid = 0) { diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs index bf5a69f20..3737cc6a9 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs @@ -221,7 +221,7 @@ void UpdateStateSkill() { return; } - stateSkill.StateNextTick = tickCount + (int) TimeSpan.FromSeconds(stateSkill.Motion.MotionProperty.SequenceSpeed).TotalMilliseconds; + stateSkill.StateNextTick = tickCount + (int) TimeSpan.FromSeconds(Math.Max(0.01f, stateSkill.Motion.MotionProperty.SequenceSpeed)).TotalMilliseconds; if (!SkillCastConsume(stateSkill)) { ActiveSkills.StateSkill = null; Field.Broadcast(SkillPacket.Cancel(stateSkill)); From 4db21ceb596e85c55bea5a9d1cd5d42530e53883 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Tue, 13 May 2025 21:38:41 -0700 Subject: [PATCH 4/4] formatting --- Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index 5dd609853..f64b11ae8 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -366,7 +366,7 @@ public void DropLoot(FieldPlayer firstPlayer) { Field.Broadcast(NpcControlPacket.Control(this)); - return base.CastSkill(id, level, uid, castTick, position, direction, rotation, rotateZ, motionPoint); + return base.CastSkill(id, level, uid, castTick, position, direction, rotation, rotateZ, motionPoint); } public NpcTask CastAiSkill(int id, short level, int faceTarget, Vector3 facePos, long uid = 0) {