From 3800ced14e5ea409417fdfc8d91049e2e20c7db3 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:21:45 -0700 Subject: [PATCH 1/6] KillCommand, Improve Warp and Buff command, Level Up Potion --- .../Storage/Metadata/MapEntityStorage.cs | 6 +- .../Storage/Metadata/QuestMetadataStorage.cs | 15 ++ Maple2.File.Ingest/Mapper/MapEntityMapper.cs | 4 +- Maple2.Model/Enum/Admin.cs | 20 +-- Maple2.Model/Metadata/MapEntity/SpawnPoint.cs | 20 +-- Maple2.Server.Game/Commands/BuffCommand.cs | 64 +++++++- Maple2.Server.Game/Commands/KillCommand.cs | 137 ++++++++++++++++++ Maple2.Server.Game/Commands/WarpCommand.cs | 2 +- .../Manager/Config/BuffManager.cs | 9 +- .../Field/FieldManager/FieldManager.State.cs | 11 +- .../Field/FieldManager/FieldManager.cs | 1 + Maple2.Server.Game/Manager/QuestManager.cs | 85 ++++++++++- Maple2.Server.Game/Model/Field/Actor/Actor.cs | 2 +- Maple2.Server.Game/Model/Field/Buff.cs | 12 +- .../Model/Field/Entity/FieldNpcSpawnPoint.cs | 25 +++- .../PacketHandlers/ItemUseHandler.cs | 41 +++++- 16 files changed, 393 insertions(+), 61 deletions(-) create mode 100644 Maple2.Server.Game/Commands/KillCommand.cs diff --git a/Maple2.Database/Storage/Metadata/MapEntityStorage.cs b/Maple2.Database/Storage/Metadata/MapEntityStorage.cs index 1730875ac..7383389a8 100644 --- a/Maple2.Database/Storage/Metadata/MapEntityStorage.cs +++ b/Maple2.Database/Storage/Metadata/MapEntityStorage.cs @@ -75,17 +75,17 @@ public class MapEntityStorage(MetadataContext context) : MetadataStorage GetQuestsByNpc(int npcId) { } } + public IEnumerable GetQuestsByType(QuestType type) { + lock (Context) { + if (type == QuestType.EpicQuest) { + // Type is not saved because EpicQuest value is 0. + return Context.QuestMetadata + .FromSqlRaw($@"SELECT * FROM `quest` WHERE JSON_UNQUOTE(JSON_EXTRACT(Basic, '$.Type')) IS NULL") + .ToList(); + } + return Context.QuestMetadata + .FromSqlRaw($"SELECT * FROM `quest` WHERE JSON_EXTRACT(Basic, '$.Type')={(int) type}") + .ToList(); + } + } + public List Search(string name) { lock (Context) { return Context.QuestMetadata diff --git a/Maple2.File.Ingest/Mapper/MapEntityMapper.cs b/Maple2.File.Ingest/Mapper/MapEntityMapper.cs index e75cd60c3..1f9f481a4 100644 --- a/Maple2.File.Ingest/Mapper/MapEntityMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapEntityMapper.cs @@ -102,13 +102,13 @@ private IEnumerable ParseMap(string xblock, IEnumerable e switch (npcSpawn) { case IEventSpawnPointNPC eventNpcSpawn: yield return new MapEntity(xblock, new Guid(entity.EntityId), entity.EntityName) { - Block = new EventSpawnPointNPC(npcSpawn.SpawnPointID, npcSpawn.Position, npcSpawn.Rotation, npcSpawn.IsVisible, npcSpawn.IsSpawnOnFieldCreate, npcSpawn.SpawnRadius, npcList, (int) npcSpawn.RegenCheckTime, (int) eventNpcSpawn.LifeTime, eventNpcSpawn.SpawnAnimation) + Block = new EventSpawnPointNPC(npcSpawn.EntityId, npcSpawn.SpawnPointID, npcSpawn.Position, npcSpawn.Rotation, npcSpawn.IsVisible, npcSpawn.IsSpawnOnFieldCreate, npcSpawn.SpawnRadius, npcList, (int) npcSpawn.RegenCheckTime, (int) eventNpcSpawn.LifeTime, eventNpcSpawn.SpawnAnimation) }; continue; default: string? patrolData = npcSpawn.PatrolData != "00000000-0000-0000-0000-000000000000" ? npcSpawn.PatrolData.Replace("-", string.Empty) : null; yield return new MapEntity(xblock, new Guid(entity.EntityId), entity.EntityName) { - Block = new SpawnPointNPC(npcSpawn.SpawnPointID, npcSpawn.Position, npcSpawn.Rotation, npcSpawn.IsVisible, npcSpawn.IsSpawnOnFieldCreate, npcSpawn.SpawnRadius, npcList, (int) npcSpawn.RegenCheckTime, patrolData) + Block = new SpawnPointNPC(npcSpawn.EntityId, npcSpawn.SpawnPointID, npcSpawn.Position, npcSpawn.Rotation, npcSpawn.IsVisible, npcSpawn.IsSpawnOnFieldCreate, npcSpawn.SpawnRadius, npcList, (int) npcSpawn.RegenCheckTime, patrolData) }; continue; } diff --git a/Maple2.Model/Enum/Admin.cs b/Maple2.Model/Enum/Admin.cs index a9e8e0f45..fa3fc8fa8 100644 --- a/Maple2.Model/Enum/Admin.cs +++ b/Maple2.Model/Enum/Admin.cs @@ -65,15 +65,17 @@ public enum PetReportFlag { [Flags] public enum AdminPermissions { - Alert, - Ban, - SpawnItem, - SpawnNpc, - Debug, - Quest, - Warp, - Find, - PlayerCommands, + Alert = 1, + StringBoard = 2, + EventManagement = 4, + Ban = 8, + SpawnItem = 16, + SpawnNpc = 32, + Debug = 64, + Quest = 128, + Warp = 256, + Find = 512, + PlayerCommands = 1024, Spawn = SpawnNpc | SpawnItem, Admin = int.MaxValue, diff --git a/Maple2.Model/Metadata/MapEntity/SpawnPoint.cs b/Maple2.Model/Metadata/MapEntity/SpawnPoint.cs index 2e46f71bb..98a39d213 100644 --- a/Maple2.Model/Metadata/MapEntity/SpawnPoint.cs +++ b/Maple2.Model/Metadata/MapEntity/SpawnPoint.cs @@ -4,20 +4,21 @@ namespace Maple2.Model.Metadata; -public abstract record SpawnPoint(int Id, Vector3 Position, Vector3 Rotation, bool Visible) : MapBlock; +public abstract record SpawnPoint(int SpawnPointId, Vector3 Position, Vector3 Rotation, bool Visible) : MapBlock; public record SpawnPointPC( - int Id, + int SpawnPointId, Vector3 Position, Vector3 Rotation, bool Visible, bool Enable -) : SpawnPoint(Id, Position, Rotation, Visible); +) : SpawnPoint(SpawnPointId, Position, Rotation, Visible); public record SpawnPointNPCListEntry(int NpcId, int Count); public record SpawnPointNPC( - int Id, + string EntityId, + int SpawnPointId, Vector3 Position, Vector3 Rotation, bool Visible, @@ -26,10 +27,11 @@ public record SpawnPointNPC( IList NpcList, int RegenCheckTime, string? PatrolData -) : SpawnPoint(Id, Position, Rotation, Visible); +) : SpawnPoint(SpawnPointId, Position, Rotation, Visible); public record EventSpawnPointNPC( - int Id, + string EntityId, + int SpawnPointId, Vector3 Position, Vector3 Rotation, bool Visible, @@ -39,10 +41,10 @@ public record EventSpawnPointNPC( int RegenCheckTime, int LifeTime, string SpawnAnimation -) : SpawnPointNPC(Id, Position, Rotation, Visible, SpawnOnFieldCreate, SpawnRadius, NpcList, RegenCheckTime, String.Empty); +) : SpawnPointNPC(EntityId, SpawnPointId, Position, Rotation, Visible, SpawnOnFieldCreate, SpawnRadius, NpcList, RegenCheckTime, String.Empty); public record EventSpawnPointItem( - int Id, + int SpawnPointId, Vector3 Position, Vector3 Rotation, float Lifetime, @@ -50,4 +52,4 @@ public record EventSpawnPointItem( int GlobalDropBoxId, int GlobalDropLevel, bool Visible -) : SpawnPoint(Id, Position, Rotation, Visible); +) : SpawnPoint(SpawnPointId, Position, Rotation, Visible); diff --git a/Maple2.Server.Game/Commands/BuffCommand.cs b/Maple2.Server.Game/Commands/BuffCommand.cs index 8d7a6902e..fbcc52ba5 100644 --- a/Maple2.Server.Game/Commands/BuffCommand.cs +++ b/Maple2.Server.Game/Commands/BuffCommand.cs @@ -3,6 +3,7 @@ using System.CommandLine.IO; using Maple2.Database.Storage; using Maple2.Model.Metadata; +using Maple2.Server.Game.Model; using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; @@ -22,25 +23,76 @@ public BuffCommand(GameSession session, SkillMetadataStorage skillStorage) : bas var id = new Argument("id", "Id of buff to activate."); var level = new Option(["--level", "-l"], () => 1, "Buff level."); var stack = new Option(["--stack", "-s"], () => 1, "Amount of stacks on the buff."); + var duration = new Option(["--duration", "-d"], () => -1, "Duration of the buff in seconds."); + var all = new Option(["--all", "-a"], () => false, "Apply to all players in the field."); + var target = new Option(["--target", "-t"], () => string.Empty, "Target player by name."); + var remove = new Option(["--remove", "-r"], () => false, "Remove buff from target."); AddArgument(id); AddOption(level); AddOption(stack); - this.SetHandler(Handle, id, level, stack); + AddOption(duration); + AddOption(all); + AddOption(target); + AddOption(remove); + this.SetHandler(Handle, id, level, stack, duration, all, target, remove); } - private void Handle(InvocationContext ctx, int buffId, int level, int stack) { + private void Handle(InvocationContext ctx, int buffId, int level, int stack, int duration, bool all, string target, bool remove) { try { if (!skillStorage.TryGetEffect(buffId, (short) level, out AdditionalEffectMetadata? _)) { ctx.Console.Error.WriteLine($"Invalid buff: {buffId}, level: {level}"); return; } - session.Player.Buffs.AddBuff(session.Player, session.Player, buffId, (short) level); - if (stack > 1) { - session.Player.Buffs.Buffs[buffId].Stack(stack); - session.Field?.Broadcast(BuffPacket.Update(session.Player.Buffs.Buffs[buffId])); + if (duration > 0) { + duration *= 1000; // convert to milliseconds } + + if (all) { + foreach (FieldPlayer player in session.Field.Players.Values) { + if (remove) { + player.Buffs.Remove(buffId); + } else { + player.Buffs.AddBuff(session.Player, player, buffId, (short) level, duration); + if (stack > 1) { + player.Buffs.Buffs[buffId].Stack(stack); + session.Field?.Broadcast(BuffPacket.Update(player.Buffs.Buffs[buffId])); + } + } + + } + } else if (!string.IsNullOrEmpty(target)) { + FieldPlayer? player = session.Field.GetPlayers().Values + .FirstOrDefault(player => string.Equals(player.Value.Character.Name, target, StringComparison.OrdinalIgnoreCase)); + if (player is null) { + ctx.Console.Error.WriteLine($"Player {target} not found."); + return; + } + + if (remove) { + player.Buffs.Remove(buffId); + session.Field?.Broadcast(BuffPacket.Remove(player.Buffs.Buffs[buffId])); + return; + } + player.Buffs.AddBuff(session.Player, player, buffId, (short) level, duration); + if (stack > 1) { + player.Buffs.Buffs[buffId].Stack(stack); + session.Field.Broadcast(BuffPacket.Update(player.Buffs.Buffs[buffId])); + } + } else { + if (remove) { + session.Player.Buffs.Remove(buffId); + session.Field?.Broadcast(BuffPacket.Remove(session.Player.Buffs.Buffs[buffId])); + return; + } + session.Player.Buffs.AddBuff(session.Player, session.Player, buffId, (short) level, duration); + if (stack > 1) { + session.Player.Buffs.Buffs[buffId].Stack(stack); + session.Field?.Broadcast(BuffPacket.Update(session.Player.Buffs.Buffs[buffId])); + } + } + ctx.ExitCode = 0; } catch (SystemException ex) { ctx.Console.Error.WriteLine(ex.Message); diff --git a/Maple2.Server.Game/Commands/KillCommand.cs b/Maple2.Server.Game/Commands/KillCommand.cs new file mode 100644 index 000000000..1ebc378b0 --- /dev/null +++ b/Maple2.Server.Game/Commands/KillCommand.cs @@ -0,0 +1,137 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using System.CommandLine.IO; +using System.Numerics; +using Maple2.Model.Enum; +using Maple2.Model.Metadata; +using Maple2.Server.Game.Model; +using Maple2.Server.Game.Model.Skill; +using Maple2.Server.Game.Packets; +using Maple2.Server.Game.Session; +using Maple2.Tools.Collision; + +namespace Maple2.Server.Game.Commands; + +public class KillCommand : Command { + private const string NAME = "kill"; + private const string DESCRIPTION = "Kill NPCs and/or Mobs."; + + private readonly GameSession session; + + public KillCommand(GameSession session) : base(NAME, DESCRIPTION) { + this.session = session; + AddCommand(new KillAllNpcCommand(session)); + AddCommand(new KillNearNpcCommand(session)); + } + + private class KillAllNpcCommand : Command { + private readonly GameSession session; + + public KillAllNpcCommand(GameSession session) : base("all", "Kill all NPCs in map.") { + this.session = session; + + var npcOnly = new Option("npc", () => false, "Kill only npcs."); + var mobOnly = new Option("mob", () => false, "Kill only mobs."); + + AddOption(npcOnly); + AddOption(mobOnly); + this.SetHandler(Handle, npcOnly, mobOnly); + } + + private void Handle(InvocationContext ctx, bool npcOnly, bool mobOnly) { + if (!session.SkillMetadata.TryGet(10000001, 1, out SkillMetadata? skill)) { + ctx.Console.Out.WriteLine("Skill not found."); + return; + } + + if (npcOnly == mobOnly) { + // Consider both to be true + foreach (FieldNpc npc in session.Field.Npcs.Values) { + Kill(session, npc, skill); + + } + foreach (FieldNpc npc in session.Field.Mobs.Values) { + Kill(session, npc, skill); + } + ctx.Console.Out.WriteLine("All NPCs and Mobs removed."); + return; + } + + if (npcOnly) { + foreach (FieldNpc npc in session.Field.Npcs.Values) { + Kill(session, npc, skill); + } + ctx.Console.Out.WriteLine("All NPCs removed."); + } else { + foreach (FieldNpc npc in session.Field.Mobs.Values) { + Kill(session, npc, skill); + } + ctx.Console.Out.WriteLine("All Mobs removed."); + } + } + } + + private class KillNearNpcCommand : Command { + private readonly GameSession session; + + public KillNearNpcCommand(GameSession session) : base("near", "Kill mobs/npcs within a given range.") { + this.session = session; + + var distance = new Option(["--distance", "-d"], () => 150, "Radius to kill mobs/npcs."); + + AddOption(distance); + this.SetHandler(Handle, distance); + + } + + private void Handle(InvocationContext ctx, int distance) { + var rec = new Rectangle(new Vector2(session.Player.Position.X, session.Player.Position.Y), distance, distance, 0); + var prism = new Prism(rec, session.Player.Position.Z, distance * 2); + if (!session.SkillMetadata.TryGet(10000001, 1, out SkillMetadata? skill)) { + ctx.Console.Out.WriteLine("Skill not found."); + return; + } + foreach (FieldNpc npc in session.Field.Npcs.Values) { + if (prism.Intersects(npc.Shape)) { + ctx.Console.Out.WriteLine($"Killing {npc.Value.Metadata.Name} - ObjectId: ({npc.ObjectId})"); + Kill(session, npc, skill); + } + } + + foreach (FieldNpc npc in session.Field.Mobs.Values) { + if (prism.Intersects(npc.Shape)) { + ctx.Console.Out.WriteLine($"Killing {npc.Value.Metadata.Name} - ObjectId: ({npc.ObjectId})"); + Kill(session, npc, skill); + }; + + } + } + } + + public static void Kill(GameSession session, FieldNpc npc, SkillMetadata skill) { + var damageRecord = new DamageRecord(skill, skill.Data.Motions[0].Attacks[0]) { + CasterId = session.Player.ObjectId, + TargetUid = ((long) Random.Shared.Next(int.MinValue, int.MaxValue) << 32) | (uint) Random.Shared.Next(int.MinValue, int.MaxValue), + OwnerId = session.Player.ObjectId, + SkillId = skill.Id, + Level = skill.Level, + AttackPoint = 0, + MotionPoint = 0, + Position = npc.Position, + Direction = session.Player.Rotation, + }; + var target = new DamageRecordTarget() { + ObjectId = npc.ObjectId, + Position = session.Player.Position, + Direction = session.Player.Rotation, + }; + + long damage = npc.Stats.Values[BasicAttribute.Health].Current; + target.AddDamage(DamageType.Normal, damage); + damageRecord.Targets.Add(target); + npc.Stats.Values[BasicAttribute.Health].Add(-damage); + session.Field.Broadcast(StatsPacket.Update(npc, BasicAttribute.Health)); + + session.Field.Broadcast(SkillDamagePacket.Damage(damageRecord)); + } +} diff --git a/Maple2.Server.Game/Commands/WarpCommand.cs b/Maple2.Server.Game/Commands/WarpCommand.cs index a1175c61f..0ff171e42 100644 --- a/Maple2.Server.Game/Commands/WarpCommand.cs +++ b/Maple2.Server.Game/Commands/WarpCommand.cs @@ -79,7 +79,7 @@ public MapCommand(GameSession session, MapMetadataStorage mapStorage) : base("ma AddArgument(mapName); AddOption(mapIndex); - this.SetHandler(Handle, mapName); + this.SetHandler(Handle, mapName, mapIndex); } private void Handle(InvocationContext ctx, string[] mapName, int mapIndex) { diff --git a/Maple2.Server.Game/Manager/Config/BuffManager.cs b/Maple2.Server.Game/Manager/Config/BuffManager.cs index a4c725772..66760abd6 100644 --- a/Maple2.Server.Game/Manager/Config/BuffManager.cs +++ b/Maple2.Server.Game/Manager/Config/BuffManager.cs @@ -58,12 +58,16 @@ public void LoadFieldBuffs() { } } - public void AddBuff(IActor caster, IActor owner, int id, short level, bool notifyField = true) { + public void AddBuff(IActor caster, IActor owner, int id, short level, int durationSec = -1, bool notifyField = true) { if (!owner.Field.SkillMetadata.TryGetEffect(id, level, out AdditionalEffectMetadata? additionalEffect)) { logger.Error("Invalid buff: {SkillId},{Level}", id, level); return; } + if (durationSec < 0) { + durationSec = additionalEffect.Property.DurationTick; + } + // Check if immune to any present buffs if (!CheckImmunity(id, additionalEffect.Property.Category)) { return; @@ -93,8 +97,7 @@ public void AddBuff(IActor caster, IActor owner, int id, short level, bool notif } } - - var buff = new Buff(owner.Field, additionalEffect, NextLocalId(), caster, owner); + var buff = new Buff(owner.Field, additionalEffect, NextLocalId(), caster, owner, durationSec); if (!Buffs.TryAdd(id, buff)) { Buffs[id].Stack(); owner.Field.Broadcast(BuffPacket.Update(buff)); diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index 055414581..e509cf8fd 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -38,7 +38,7 @@ public partial class FieldManager { private readonly ConcurrentDictionary fieldInstruments = new(); private readonly ConcurrentDictionary fieldItems = new(); private readonly ConcurrentDictionary fieldMobSpawns = new(); - private readonly ConcurrentDictionary fieldSpawnPointNpcs = new(); + private readonly ConcurrentDictionary fieldSpawnPointNpcs = new(); private readonly ConcurrentDictionary fieldPlayerSpawnPoints = new(); private readonly ConcurrentDictionary fieldSpawnGroups = new(); private readonly ConcurrentDictionary fieldSkills = new(); @@ -364,7 +364,7 @@ public FieldSpawnPointNpc AddSpawnPointNpc(SpawnPointNPC metadata) { Rotation = metadata.Rotation, }; - fieldSpawnPointNpcs[metadata.Id] = fieldSpawnPointNpc; + fieldSpawnPointNpcs[metadata.EntityId] = fieldSpawnPointNpc; fieldSpawnPointNpc.SpawnOnCreate(); return fieldSpawnPointNpc; } @@ -379,11 +379,10 @@ public void ToggleCombineSpawn(SpawnGroupMetadata metadata, bool enable) { } public void ToggleNpcSpawnPoint(int spawnId) { - if (!fieldSpawnPointNpcs.TryGetValue(spawnId, out FieldSpawnPointNpc? fieldSpawnGroup)) { - return; + List spawns = fieldSpawnPointNpcs.Values.Where(spawn => spawn.Value.SpawnPointId == spawnId).ToList(); + foreach (FieldSpawnPointNpc spawn in spawns) { + spawn.TriggerSpawn(); } - - fieldSpawnGroup.TriggerSpawn(); } public void SpawnInteractObject(SpawnInteractObjectMetadata metadata) { diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs index 6b46763e7..7113b953b 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs @@ -281,6 +281,7 @@ public void Update() { foreach (FieldInteract interact in fieldAdBalloons.Values) interact.Update(FieldTick); foreach (FieldItem item in fieldItems.Values) item.Update(FieldTick); foreach (FieldMobSpawn mobSpawn in fieldMobSpawns.Values) mobSpawn.Update(FieldTick); + foreach (FieldSpawnPointNpc spawnPointNpc in fieldSpawnPointNpcs.Values) spawnPointNpc.Update(FieldTick); foreach (FieldSkill skill in fieldSkills.Values) skill.Update(FieldTick); foreach (FieldPortal portal in fieldPortals.Values) portal.Update(FieldTick); UpdateBanners(); diff --git a/Maple2.Server.Game/Manager/QuestManager.cs b/Maple2.Server.Game/Manager/QuestManager.cs index c398e59e5..93f3de6f0 100644 --- a/Maple2.Server.Game/Manager/QuestManager.cs +++ b/Maple2.Server.Game/Manager/QuestManager.cs @@ -448,7 +448,7 @@ private void TryJobAdvance(int questId) { session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo)); } - private void CompleteChapter(int questId) { + private void CompleteChapter(int questId, bool ignoreItemRewards = false) { IEnumerable entries = session.TableMetadata.ChapterBookTable.Entries.Values.Where(entry => entry.EndQuestId == questId).ToList(); if (!entries.Any()) { return; @@ -459,14 +459,16 @@ private void CompleteChapter(int questId) { continue; } - foreach (ItemComponent itemComponent in entry.Items) { - Item? item = session.Field.ItemDrop.CreateItem(itemComponent.ItemId, itemComponent.Rarity, itemComponent.Amount); - if (item == null) { - continue; - } + if (!ignoreItemRewards) { + foreach (ItemComponent itemComponent in entry.Items) { + Item? item = session.Field.ItemDrop.CreateItem(itemComponent.ItemId, itemComponent.Rarity, itemComponent.Amount); + if (item == null) { + continue; + } - if (!session.Item.Inventory.Add(item, true)) { - session.Item.MailItem(item); + if (!session.Item.Inventory.Add(item, true)) { + session.Item.MailItem(item); + } } } @@ -509,6 +511,73 @@ public void CompleteFieldMission(int mission) { } } + public void LevelPotion(int level, int lastQuest = 0) { + List> chapterQuestsGroups = session.QuestMetadata.GetQuestsByType(QuestType.EpicQuest) + .Where(q => q.Require.Level <= level) + .Where(q => q.Basic.Disabled == false) + .GroupBy(q => q.Basic.ChapterId) + .ToList(); + + using GameStorage.Request db = session.GameStorage.Context(); + bool lastQuestFound = false; + foreach (IGrouping chapterQuests in chapterQuestsGroups.OrderBy(q => q.Key)) { + Console.WriteLine($"Chapter {chapterQuests.Key}"); + if (lastQuestFound) { + break; + } + foreach (QuestMetadata metadata in chapterQuests.OrderBy(q => q.Id)) { + if (TryGetQuest(metadata.Id, out Quest? quest)) { + if (quest.State == QuestState.Completed) { + continue; + } + + foreach (Quest.Condition condition in quest.Conditions.Values) { + condition.Counter = (int) condition.Metadata.Value; + } + quest.State = QuestState.Completed; + quest.CompletionCount++; + quest.EndTime = DateTime.Now.ToEpochSeconds(); + continue; + } + + if (metadata.Require.Job.Length > 0 && !metadata.Require.Job.Contains(session.Player.Value.Character.Job.Code())) { + continue; + } + + var epicQuest = new Quest(metadata) { + State = QuestState.Completed, + StartTime = DateTime.Now.ToEpochSeconds(), + EndTime = DateTime.Now.ToEpochSeconds() + 1, + CompletionCount = 1, + Track = true, + }; + + for (int i = 0; i < metadata.Conditions.Length; i++) { + epicQuest.Conditions.Add(i, new Quest.Condition(metadata.Conditions[i])); + epicQuest.Conditions[i].Counter = (int) epicQuest.Conditions[i].Metadata.Value; + } + + long ownerId = metadata.Basic.Account > 0 ? session.AccountId : session.CharacterId; + epicQuest = db.CreateQuest(ownerId, epicQuest); + if (epicQuest == null) { + logger.Error("Failed to create quest entry {questId}", metadata.Id); + continue; + } + Add(epicQuest); + CompleteChapter(epicQuest.Id, true); + + if (epicQuest.Id == lastQuest) { + lastQuestFound = true; + break; + } + } + } + + //TODO: Start and load fame quests + Load(); + session.Send(QuestPacket.LoadQuests([])); + } + public void Save(GameStorage.Request db) { db.SaveQuests(session.AccountId, accountValues.Values); db.SaveQuests(session.CharacterId, characterValues.Values); diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index 995106ee5..e00d0b488 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -81,7 +81,7 @@ public virtual void ApplyEffect(IActor caster, IActor owner, SkillEffectMetadata Debug.Assert(effect.Condition != null); foreach (SkillEffectMetadata.Skill skill in effect.Skills) { - Buffs.AddBuff(caster, owner, skill.Id, skill.Level, notifyField); + Buffs.AddBuff(caster, owner, skill.Id, skill.Level, notifyField: notifyField); } } diff --git a/Maple2.Server.Game/Model/Field/Buff.cs b/Maple2.Server.Game/Model/Field/Buff.cs index 4fdea42cb..636e06901 100644 --- a/Maple2.Server.Game/Model/Field/Buff.cs +++ b/Maple2.Server.Game/Model/Field/Buff.cs @@ -38,7 +38,7 @@ public class Buff : IUpdatable, IByteSerializable { private readonly ILogger logger = Log.ForContext(); - public Buff(FieldManager field, AdditionalEffectMetadata metadata, int objectId, IActor caster, IActor owner) { + public Buff(FieldManager field, AdditionalEffectMetadata metadata, int objectId, IActor caster, IActor owner, int duration) { this.field = field; Metadata = metadata; ObjectId = objectId; @@ -49,20 +49,24 @@ public Buff(FieldManager field, AdditionalEffectMetadata metadata, int objectId, // Buffs with IntervalTick=0 will just proc a single time IntervalTick = metadata.Property.IntervalTick > 0 ? metadata.Property.IntervalTick : metadata.Property.DurationTick + 1000; - Stack(); + Stack(duration: duration); NextProcTick = StartTick + this.Metadata.Property.DelayTick + this.Metadata.Property.IntervalTick; UpdateEnabled(false); canProc = metadata.Property.KeepCondition != BuffKeepCondition.UnlimitedDuration; canExpire = metadata.Property.KeepCondition != BuffKeepCondition.UnlimitedDuration && EndTick >= StartTick; } - public bool Stack(int amount = 1) { - Stacks = Math.Min(Stacks + 1, Metadata.Property.MaxCount); + public bool Stack(int amount = 1, int duration = 0) { + Stacks = Math.Min(Stacks + amount, Metadata.Property.MaxCount); StartTick = Environment.TickCount64; if (Stacks == 1 || Metadata.Property.ResetCondition != BuffResetCondition.PersistEndTick) { EndTick = StartTick + Metadata.Property.DurationTick; } + + if (duration > 0) { + EndTick = StartTick + duration; + } return true; } diff --git a/Maple2.Server.Game/Model/Field/Entity/FieldNpcSpawnPoint.cs b/Maple2.Server.Game/Model/Field/Entity/FieldNpcSpawnPoint.cs index 87bc43986..0fafe4a0b 100644 --- a/Maple2.Server.Game/Model/Field/Entity/FieldNpcSpawnPoint.cs +++ b/Maple2.Server.Game/Model/Field/Entity/FieldNpcSpawnPoint.cs @@ -6,13 +6,13 @@ namespace Maple2.Server.Game.Model; public class FieldSpawnPointNpc : FieldEntity { - private readonly List spawnedMobs; - private int SpawnId => Value.Id; + private int SpawnId => Value.SpawnPointId; private bool active; + private long checkTick; + public FieldSpawnPointNpc(FieldManager field, int objectId, SpawnPointNPC metadata) : base(field, objectId, metadata) { - spawnedMobs = []; if (metadata.RegenCheckTime > 0) { active = true; } @@ -24,23 +24,36 @@ public void SpawnOnCreate() { } } + public override void Update(long tickCount) { + if (!active || Value.RegenCheckTime <= 0) { + return; + } + if (checkTick > tickCount) { + return; + } + + checkTick = tickCount + (long) TimeSpan.FromSeconds(Value.RegenCheckTime).TotalMilliseconds; + TriggerSpawn(); + } + public void TriggerSpawn() { + IEnumerable npcs = Field.GetActorsBySpawnId(SpawnId).OfType().ToList(); // Only get npcs foreach (SpawnPointNPCListEntry spawn in Value.NpcList) { if (!Field.NpcMetadata.TryGet(spawn.NpcId, out NpcMetadata? npcMetadata)) { Log.Logger.Warning("Npc {NpcId} failed to load for map {MapId}", spawn.NpcId, Field.MapId); continue; } - for (int i = 0; i < spawn.Count; i++) { + int spawnCountNeeded = spawn.Count - npcs.Count(x => x.Value.Id == spawn.NpcId); + for (int i = 0; i < spawnCountNeeded; i++) { FieldNpc? npc = Field.SpawnNpc(npcMetadata, Value); if (npc == null) { continue; } - npc.SpawnPointId = 0; + npc.SpawnPointId = SpawnId; Field.Broadcast(FieldPacket.AddNpc(npc)); Field.Broadcast(ProxyObjectPacket.AddNpc(npc)); - spawnedMobs.Add(npc.ObjectId); } } } diff --git a/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs b/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs index c1afd068a..da68ba653 100644 --- a/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs @@ -102,7 +102,10 @@ public override void Handle(GameSession session, IByteReader packet) { HandleOpenMassive(session, packet, item); break; case ItemFunction.DefenseGuard: - HandleDefenseGuard(session, packet, item); + HandleDefenseGuard(session, item); + break; + case ItemFunction.LevelPotion: + HandleLevelPotion(session, item); break; default: Logger.Warning("Unhandled item function: {Name}", item.Metadata.Function?.Type); @@ -258,7 +261,7 @@ private void HandleBuddyBadgeBox(GameSession session, IByteReader packet, Item i return; } - int[] buddyBadgeBoxParams = item.Metadata.Function?.Parameters.Split(',').Select(int.Parse).ToArray() ?? Array.Empty(); + int[] buddyBadgeBoxParams = item.Metadata.Function?.Parameters.Split(',').Select(int.Parse).ToArray() ?? []; if (buddyBadgeBoxParams.Length != 2) { session.Send(NoticePacket.MessageBox(StringCode.s_couple_effect_error_openbox_unknown)); Logger.Error("Invalid buddy badge box parameters: {Parameters}", item.Metadata.Function?.Parameters); @@ -485,7 +488,7 @@ private static void HandleOpenMassive(GameSession session, IByteReader packet, I session.Field.UsePortal(session, portal.Value.Id, password); } - private static void HandleDefenseGuard(GameSession session, IByteReader packet, Item item) { + private static void HandleDefenseGuard(GameSession session, Item item) { int[] npcParameters = item.Metadata.Function?.Parameters.Split(',').Select(int.Parse).ToArray() ?? []; if (npcParameters.Length < 3) { return; @@ -510,4 +513,36 @@ private static void HandleDefenseGuard(GameSession session, IByteReader packet, session.Field.RemoveNpc(fieldNpc.ObjectId, (int) TimeSpan.FromSeconds(lifeSpanSeconds).TotalMilliseconds); session.Item.Inventory.Consume(item.Uid, 1); } + + private static void HandleLevelPotion(GameSession session, Item item) { + Dictionary xmlParameters = XmlParseUtil.GetParameters(item.Metadata.Function?.Parameters); + if (!xmlParameters.ContainsKey("targetLevel") || !short.TryParse(xmlParameters["targetLevel"], out short level)) { + return; + } + + int lastClearEpicQuestId = 0; + if (xmlParameters.TryGetValue("lastClearEpic", out string? lastEpicQuestId) && !int.TryParse(lastEpicQuestId, out lastClearEpicQuestId)) { + lastClearEpicQuestId = 0; + } + + int currentLevel = session.Player.Value.Character.Level; + session.Player.Value.Character.Level = level; + session.Field?.Broadcast(LevelUpPacket.LevelUp(session.Player)); + session.Stats.Refresh(); + + session.Quest.LevelPotion(level - 1, lastClearEpicQuestId); + + for (int i = currentLevel; i < level; i++) { + session.ConditionUpdate(ConditionType.level, targetLong: i); + } + + session.PlayerInfo.SendUpdate(new PlayerUpdateRequest { + AccountId = session.AccountId, + CharacterId = session.CharacterId, + Level = level, + Async = true, + }); + + session.Item.Inventory.Consume(item.Uid, 1); + } } From 31e62f2905cae53306eae2cdf4180d0b77ceb520 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:24:39 -0700 Subject: [PATCH 2/6] Check for Job Advance --- Maple2.Server.Game/Manager/QuestManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Maple2.Server.Game/Manager/QuestManager.cs b/Maple2.Server.Game/Manager/QuestManager.cs index 93f3de6f0..a580590f1 100644 --- a/Maple2.Server.Game/Manager/QuestManager.cs +++ b/Maple2.Server.Game/Manager/QuestManager.cs @@ -564,6 +564,7 @@ public void LevelPotion(int level, int lastQuest = 0) { continue; } Add(epicQuest); + TryJobAdvance(epicQuest.Id); CompleteChapter(epicQuest.Id, true); if (epicQuest.Id == lastQuest) { @@ -575,7 +576,6 @@ public void LevelPotion(int level, int lastQuest = 0) { //TODO: Start and load fame quests Load(); - session.Send(QuestPacket.LoadQuests([])); } public void Save(GameStorage.Request db) { From ff58cca10c2a63eee573f2dc7d5ce89ef522b3b2 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:27:54 -0700 Subject: [PATCH 3/6] remove broadcast of buff removal. already happens upon remove func --- Maple2.Server.Game/Commands/BuffCommand.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Maple2.Server.Game/Commands/BuffCommand.cs b/Maple2.Server.Game/Commands/BuffCommand.cs index fbcc52ba5..e61501a9b 100644 --- a/Maple2.Server.Game/Commands/BuffCommand.cs +++ b/Maple2.Server.Game/Commands/BuffCommand.cs @@ -72,7 +72,6 @@ private void Handle(InvocationContext ctx, int buffId, int level, int stack, int if (remove) { player.Buffs.Remove(buffId); - session.Field?.Broadcast(BuffPacket.Remove(player.Buffs.Buffs[buffId])); return; } player.Buffs.AddBuff(session.Player, player, buffId, (short) level, duration); @@ -83,7 +82,6 @@ private void Handle(InvocationContext ctx, int buffId, int level, int stack, int } else { if (remove) { session.Player.Buffs.Remove(buffId); - session.Field?.Broadcast(BuffPacket.Remove(session.Player.Buffs.Buffs[buffId])); return; } session.Player.Buffs.AddBuff(session.Player, session.Player, buffId, (short) level, duration); From a50c4655dbcfe6a15a71f3ad9e2c52171fe26db8 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:41:55 -0700 Subject: [PATCH 4/6] Debug line --- Maple2.Server.Game/Manager/QuestManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Maple2.Server.Game/Manager/QuestManager.cs b/Maple2.Server.Game/Manager/QuestManager.cs index a580590f1..fc178fc15 100644 --- a/Maple2.Server.Game/Manager/QuestManager.cs +++ b/Maple2.Server.Game/Manager/QuestManager.cs @@ -521,7 +521,6 @@ public void LevelPotion(int level, int lastQuest = 0) { using GameStorage.Request db = session.GameStorage.Context(); bool lastQuestFound = false; foreach (IGrouping chapterQuests in chapterQuestsGroups.OrderBy(q => q.Key)) { - Console.WriteLine($"Chapter {chapterQuests.Key}"); if (lastQuestFound) { break; } From 8bc6daa966533497ce797f0800ff0f40c300c07c Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:43:33 -0700 Subject: [PATCH 5/6] formatting --- Maple2.Server.Game/Commands/KillCommand.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Maple2.Server.Game/Commands/KillCommand.cs b/Maple2.Server.Game/Commands/KillCommand.cs index 1ebc378b0..c43df10cf 100644 --- a/Maple2.Server.Game/Commands/KillCommand.cs +++ b/Maple2.Server.Game/Commands/KillCommand.cs @@ -16,10 +16,7 @@ public class KillCommand : Command { private const string NAME = "kill"; private const string DESCRIPTION = "Kill NPCs and/or Mobs."; - private readonly GameSession session; - public KillCommand(GameSession session) : base(NAME, DESCRIPTION) { - this.session = session; AddCommand(new KillAllNpcCommand(session)); AddCommand(new KillNearNpcCommand(session)); } @@ -103,12 +100,11 @@ private void Handle(InvocationContext ctx, int distance) { ctx.Console.Out.WriteLine($"Killing {npc.Value.Metadata.Name} - ObjectId: ({npc.ObjectId})"); Kill(session, npc, skill); }; - } } } - public static void Kill(GameSession session, FieldNpc npc, SkillMetadata skill) { + private static void Kill(GameSession session, FieldNpc npc, SkillMetadata skill) { var damageRecord = new DamageRecord(skill, skill.Data.Motions[0].Attacks[0]) { CasterId = session.Player.ObjectId, TargetUid = ((long) Random.Shared.Next(int.MinValue, int.MaxValue) << 32) | (uint) Random.Shared.Next(int.MinValue, int.MaxValue), From 7d47a0da7657c0b2ac6fb4ca8ba363e3b84d1291 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:48:05 -0700 Subject: [PATCH 6/6] formatting never ends --- Maple2.Server.Game/Commands/KillCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Maple2.Server.Game/Commands/KillCommand.cs b/Maple2.Server.Game/Commands/KillCommand.cs index c43df10cf..aa4d4b097 100644 --- a/Maple2.Server.Game/Commands/KillCommand.cs +++ b/Maple2.Server.Game/Commands/KillCommand.cs @@ -99,7 +99,7 @@ private void Handle(InvocationContext ctx, int distance) { if (prism.Intersects(npc.Shape)) { ctx.Console.Out.WriteLine($"Killing {npc.Value.Metadata.Name} - ObjectId: ({npc.ObjectId})"); Kill(session, npc, skill); - }; + } } } }