using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using PhoenixLib.Logging; using WingsEmu.DTOs.Quests; using WingsEmu.Game._enum; using WingsEmu.Game.Algorithm; using WingsEmu.Game.Characters; using WingsEmu.Game.Configurations; using WingsEmu.Game.Entities; using WingsEmu.Game.Helpers.Damages; using WingsEmu.Game.Networking; using WingsEmu.Game.Quests; using WingsEmu.Packets.Enums; using WingsEmu.Packets.Enums.Character; namespace WingsAPI.Game.Extensions.Quests { public static class QuestExtensions { private static readonly string _b64SqstIndex = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{}"; private static readonly QuestType[] DialogQuestTypes = { QuestType.DIALOG, QuestType.DIALOG_2, QuestType.DELIVER_ITEM_TO_NPC, QuestType.GIVE_ITEM_TO_NPC, QuestType.GIVE_ITEM_TO_NPC_2, QuestType.GIVE_NPC_GOLD, QuestType.DIALOG_WHILE_WEARING, QuestType.DIALOG_WHILE_HAVING_ITEM }; public static string GenerateTargetQuest(this IClientSession session, short x, short y, short mapId, int questId) => $"target {x} {y} {mapId} {questId}"; public static string GenerateTargetOffQuest(this IClientSession session, short x, short y, short mapId, int questId) => $"targetoff {x} {y} {mapId} {questId}"; public static string GenerateQnpcPacket(this IClientSession session, short level, short npcVnum, short mapId) => $"qnpc {npcVnum}|{mapId}|{level} -1|-1|-1 -1|-1|-1 -1|-1|-1 -1|-1|-1 -1|-1|-1 -1|-1|-1 -1|-1|-1 -1|-1|-1 -1|-1|-1"; public static string GenerateQrPacket(this IClientSession session, CharacterQuest quest, IReadOnlyCollection rndRewards, QuestsRatesConfiguration questRates) { List prizes = quest.Quest.Prizes; if (prizes == null) { return ""; } var qrPacket = new StringBuilder("qr "); foreach (QuestPrizeDto prize in prizes) { switch (prize.RewardType) { case (byte)QuestRewardType.Reput: case (byte)QuestRewardType.Exp: case (byte)QuestRewardType.SecondExp: case (byte)QuestRewardType.ThirdExp: case (byte)QuestRewardType.JobExp: case (byte)QuestRewardType.SecondJobExp: qrPacket.Append($"{prize.RewardType.ToString()} 0 {prize.Data0.ToString()} "); break; case (byte)QuestRewardType.Gold: qrPacket.Append($"{prize.RewardType.ToString()} 0 {prize.Data0 * questRates.GoldRate} "); break; case (byte)QuestRewardType.AllRewards: qrPacket.Append(prize.Data0 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data0.ToString()} 1 "); qrPacket.Append(prize.Data1 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data1.ToString()} 1 "); qrPacket.Append(prize.Data2 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data2.ToString()} 1 "); qrPacket.Append(prize.Data3 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data3.ToString()} 1 "); break; case (byte)QuestRewardType.SecondGold: qrPacket.Append($"{prize.RewardType.ToString()} 0 {prize.Data0 * questRates.BaseGold * questRates.GoldRate} "); break; case (byte)QuestRewardType.ThirdGold: qrPacket.Append($"{prize.RewardType.ToString()} 0 {prize.Data0 * quest.ObjectiveAmount.Sum(s => s.Value.RequiredAmount) * session.PlayerEntity.Level * questRates.GoldRate} "); break; case (byte)QuestRewardType.Unknow: case (byte)QuestRewardType.ItemsDependingOnClass: switch (session.PlayerEntity.Class) { case ClassType.Swordman: qrPacket.Append(prize.Data0 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data0.ToString()} {prize.Data4.ToString()} "); break; case ClassType.Archer: qrPacket.Append(prize.Data1 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data1.ToString()} {prize.Data4.ToString()} "); break; case ClassType.Magician: qrPacket.Append(prize.Data2 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data2.ToString()} {prize.Data4.ToString()} "); break; default: qrPacket.Append(prize.Data3 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data3.ToString()} {prize.Data4.ToString()} "); break; } break; } } foreach (CharacterQuestGeneratedReward rndReward in rndRewards) { qrPacket.Append($"{(byte)QuestRewardType.RandomReward} {rndReward.ItemVnum} {rndReward.Amount} "); } for (int i = prizes.Count(s => s.RewardType != (byte)QuestRewardType.RandomReward) + rndRewards.Count; i < 4; i++) { qrPacket.Append("0 0 0 "); } qrPacket.Append(quest.QuestId); return qrPacket.ToString(); } public static string GenerateQuestList(this IClientSession session, IQuestManager questManager, int? questToShowInfo = null) { IEnumerable quests = session.PlayerEntity.GetCurrentQuests(); string header = "qstlist"; if (quests == null) { return header; } var packet = new StringBuilder(); foreach (CharacterQuest quest in quests) { packet.Append($" {session.GenerateQuestData(quest, questManager, qstlistCall: true, questToShowInfo: questToShowInfo)}"); } int soundFlowerEmptySlot = session.GetEmptyQuestSlot(QuestSlotType.GENERAL, true); for (int i = 0; i < session.PlayerEntity.GetPendingSoundFlowerQuests(); i++) { packet.Append($" {soundFlowerEmptySlot + i}.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0"); } return $"{header}{packet}"; } public static string GenerateQuestProgress(this IClientSession session, IQuestManager questManager, int questVnum) { CharacterQuest currentQuest = session.PlayerEntity.GetCurrentQuest(questVnum); return currentQuest == null ? string.Empty : $"qsti {session.GenerateQuestData(currentQuest, questManager, session.PlayerEntity.IsQuestCompleted(currentQuest))}"; } private static string GenerateQuestData(this IClientSession session, CharacterQuest characterQuest, IQuestManager questManager, bool isFinished = false, bool qstlistCall = false, int? questToShowInfo = null) { int questSlot = session.GetQuestSlot(characterQuest); if (questSlot == -1) { Log.Debug($"The slot for quest {characterQuest.QuestId} was not found."); return ""; } IReadOnlyList objectives; bool statusAdded; var data = new StringBuilder($"{questSlot}.{characterQuest.QuestId}"); switch (characterQuest.SlotType) { case QuestSlotType.MAIN: data.Append($".{characterQuest.QuestId}"); break; case QuestSlotType.GENERAL: IReadOnlyCollection questlines = questManager.GetQuestlines(characterQuest.QuestId); data.Append($".{(questlines.Any() ? questlines.First() : characterQuest.QuestId)}"); break; case QuestSlotType.SECONDARY: data.Append(".0"); break; } data.Append($".{(byte)characterQuest.Quest.QuestType}"); switch (characterQuest.Quest.QuestType) { case QuestType.WIN_RAID_AND_TALK_TO_NPC: case QuestType.DELIVER_ITEM_TO_NPC: case QuestType.KILL_MONSTER_BY_VNUM: case QuestType.GIVE_ITEM_TO_NPC: case QuestType.GIVE_ITEM_TO_NPC_2: case QuestType.DIALOG_WHILE_WEARING: case QuestType.DIALOG_WHILE_HAVING_ITEM: case QuestType.USE_ITEM_ON_TARGET: case QuestType.CAPTURE_WITHOUT_KEEPING: case QuestType.CAPTURE_AND_KEEP: case QuestType.DROP_IN_TIMESPACE: case QuestType.COMPLETE_TIMESPACE: case QuestType.CRAFT_WITHOUT_KEEPING: case QuestType.DROP_CHANCE: case QuestType.DROP_CHANCE_2: case QuestType.COLLECT: case QuestType.DROP_HARDCODED: case QuestType.KILL_X_MOBS_SOUND_FLOWER: statusAdded = false; objectives = characterQuest.Quest.Objectives; foreach (QuestObjectiveDto objective in objectives) { CharacterQuestObjectiveDto questObjectiveDto = characterQuest.ObjectiveAmount[objective.ObjectiveIndex]; data.Append($".{questObjectiveDto.CurrentAmount}.{questObjectiveDto.RequiredAmount}"); if (!statusAdded) { data.Append($".{(isFinished ? 1 : 0)}"); statusAdded = true; } } for (int i = objectives.Count; i < 5; i++) { data.Append(".0.0"); } break; case QuestType.DIALOG: case QuestType.DIALOG_2: case QuestType.NOTHING: case QuestType.GO_TO_MAP: case QuestType.GIVE_NPC_GOLD: data.Append($".0.0.{(isFinished ? 1 : 0)}.0.0.0.0.0.0.0.0"); break; case QuestType.KILL_PLAYER_IN_REGION: case QuestType.DIE_X_TIMES: case QuestType.EARN_REPUTATION: case QuestType.COMPLETE_TIMESPACE_WITH_ATLEAST_X_POINTS: break; } if (qstlistCall) { data.Append(characterQuest.QuestId != questToShowInfo ? ".0" : $".{(isFinished ? 0 : 1)}"); } else { data.Append(".0"); } return data.ToString(); } public static bool IsQuestCompleted(this IPlayerEntity player, CharacterQuest characterQuest) { if (characterQuest == null) { return false; } IEnumerable objectives = characterQuest.Quest.Objectives; switch (characterQuest.Quest.QuestType) { case QuestType.DELIVER_ITEM_TO_NPC: case QuestType.GIVE_ITEM_TO_NPC: case QuestType.GIVE_ITEM_TO_NPC_2: case QuestType.KILL_MONSTER_BY_VNUM: case QuestType.CAPTURE_WITHOUT_KEEPING: case QuestType.CAPTURE_AND_KEEP: case QuestType.CRAFT_WITHOUT_KEEPING: case QuestType.DROP_CHANCE: case QuestType.DROP_CHANCE_2: case QuestType.DROP_IN_TIMESPACE: case QuestType.COLLECT: case QuestType.DROP_HARDCODED: case QuestType.COMPLETE_TIMESPACE_WITH_ATLEAST_X_POINTS: case QuestType.GIVE_NPC_GOLD: case QuestType.COMPLETE_TIMESPACE: case QuestType.KILL_X_MOBS_SOUND_FLOWER: return !objectives.Any(s => characterQuest.ObjectiveAmount[s.ObjectiveIndex].CurrentAmount < characterQuest.ObjectiveAmount[s.ObjectiveIndex].RequiredAmount); case QuestType.GO_TO_MAP: return player.Position.IsInAoeZone(new Position(characterQuest.Quest.TargetMapX, characterQuest.Quest.TargetMapY), 2); case QuestType.USE_ITEM_ON_TARGET: return !objectives.Any(s => characterQuest.ObjectiveAmount[s.ObjectiveIndex].CurrentAmount <= 0); case QuestType.DIALOG_WHILE_HAVING_ITEM: return !objectives.Any(s => player.CountItemWithVnum(s.Data1) < characterQuest.ObjectiveAmount[s.ObjectiveIndex].RequiredAmount); case QuestType.DIALOG: case QuestType.DIALOG_2: case QuestType.DIALOG_WHILE_WEARING: if ((characterQuest.QuestId == (int)QuestsVnums.TALK_WEARING_KOVOLT_MASK_1 || characterQuest.QuestId == (int)QuestsVnums.TALK_WEARING_KOVOLT_MASK_2) && player.Class != ClassType.Adventurer) // Kovolt Mask { return true; } return !objectives.Any(s => characterQuest.ObjectiveAmount[s.ObjectiveIndex].CurrentAmount <= 0); case QuestType.NOTHING: return true; // Not used for now case QuestType.WIN_RAID_AND_TALK_TO_NPC: case QuestType.DIE_X_TIMES: case QuestType.EARN_REPUTATION: case QuestType.KILL_PLAYER_IN_REGION: break; } return false; } public static string GenerateSqstPacket(this IClientSession session, IQuestManager questManager, int sqstIndex) { IReadOnlyCollection blueAlertQuests = questManager.GetNpcBlueAlertQuests().ToList(); IReadOnlyCollection completedBlueAlertQuests = session.PlayerEntity.GetCompletedQuests().Where(s => blueAlertQuests.Any(t => t.QuestId == s.QuestId)).ToList(); var sb = new StringBuilder(); sb.Append($"sqst {sqstIndex} "); for (byte packetIndex = 0; packetIndex < 250; packetIndex++) { byte[] binary = { 0, 0, 0, 0, 0, 0 }; for (byte bitIndex = 0; bitIndex < 6; bitIndex++) { int questId = packetIndex * 6 + bitIndex + 1500 * sqstIndex; if (completedBlueAlertQuests.Any(s => s.QuestId == questId)) { if (bitIndex == 0) { binary[bitIndex] = 1; } else { binary[^bitIndex] = 1; } } } string binaryString = string.Join("", binary); sb.Append(_b64SqstIndex.ElementAt(Convert.ToInt16(binaryString, 2))); } return sb.ToString(); } public static void SendSqstPackets(this IClientSession session, IQuestManager questManager) { var packets = new List(); int sqstPacketsAmount = (int)Math.Ceiling(questManager.GetNpcBlueAlertQuests().Max(s => s.QuestId ?? 0) / 1500.0); for (int i = 0; i < sqstPacketsAmount; i++) { packets.Add(session.GenerateSqstPacket(questManager, i)); } session.SendPackets(packets); } public static void SendSqstPacket(this IClientSession session, IQuestManager questManager, int sqstIndex) => session.SendPacket(session.GenerateSqstPacket(questManager, sqstIndex)); public static void UpdateQuestSqstPacket(this IClientSession session, IQuestManager questManager, int questVnum) { int sqstIndex = (int)Math.Floor(questVnum / 1500.0); session.SendPacket(session.GenerateSqstPacket(questManager, sqstIndex)); } public static void SendQuestsTargets(this IClientSession session) { IReadOnlyCollection characterQuests = session.PlayerEntity.GetCurrentQuests().Where(s => s.Quest.TargetMapId != 0).ToList(); if (!characterQuests.Any()) { return; } foreach (CharacterQuest characterQuest in characterQuests) { QuestDto quest = characterQuest.Quest; session.SendTargetQuest(quest.TargetMapX, quest.TargetMapY, quest.TargetMapId, quest.Id); } } public static void DeleteQuestTarget(this IClientSession session, CharacterQuest characterQuest) { if (characterQuest.Quest.TargetMapId == 0) { return; } session.SendTargetOffQuest(characterQuest.Quest.TargetMapX, characterQuest.Quest.TargetMapY, characterQuest.Quest.TargetMapId, characterQuest.QuestId); } public static void SendTargetQuest(this IClientSession session, short x, short y, short mapId, int questId) => session.SendPacket(session.GenerateTargetQuest(x, y, mapId, questId)); public static void SendTargetOffQuest(this IClientSession session, short x, short y, short mapId, int questId) => session.SendPacket(session.GenerateTargetOffQuest(x, y, mapId, questId)); public static void RefreshQuestList(this IClientSession session, IQuestManager questManager, int? questToShowInfo) => session.SendPacket(session.GenerateQuestList(questManager, questToShowInfo)); public static void RefreshQuestProgress(this IClientSession session, IQuestManager questManager, int questId) => session.SendPacket(session.GenerateQuestProgress(questManager, questId)); public static void SendQrPacket(this IClientSession session, CharacterQuest quest, IReadOnlyCollection rndRewards, QuestsRatesConfiguration questRates) => session.SendPacket(session.GenerateQrPacket(quest, rndRewards, questRates)); public static void SendQnpcPacket(this IClientSession session, short level, short npcVnum, short mapId) => session.SendPacket(session.GenerateQnpcPacket(level, npcVnum, mapId)); public static CharacterQuest GetQuestById(this IPlayerEntity character, int questId) => character.GetCurrentQuest(questId); public static CharacterQuest GetQuestByActionSlot(this IClientSession session, int action, int slot) => action == 1 ? session.PlayerEntity.GetCurrentQuest(slot) : session.GetQuestBySlot(slot); public static CharacterQuest GetQuestBySlot(this IClientSession session, int slot) { CharacterQuest quest; if (slot < 0 || slot > 10) { Log.Debug($"[ERROR] PACKET QT: Invalid slot index: {slot.ToString()}"); return null; } if (slot < 5) { quest = session.PlayerEntity.GetCurrentQuests().Where(s => s.SlotType == QuestSlotType.GENERAL).ElementAtOrDefault(slot); } else if (slot == 5) { quest = session.PlayerEntity.GetCurrentQuests().Where(s => s.SlotType == QuestSlotType.MAIN).ElementAtOrDefault(0); } else { slot -= 6; quest = session.PlayerEntity.GetCurrentQuests().Where(s => s.SlotType == QuestSlotType.SECONDARY).ElementAtOrDefault(slot); } return quest; } public static long GetLevelXpPercentage(this ICharacterAlgorithm characterAlgorithm, short percentage, short level) => Convert.ToInt64(characterAlgorithm.GetLevelXp(level) * (percentage / 100.0)); public static long GetSpecialistJobXpPercentage(this ICharacterAlgorithm characterAlgorithm, short percentage, short level, bool isFunSpecialist) => Convert.ToInt64(characterAlgorithm.GetSpecialistJobXp(level, isFunSpecialist) * (percentage / 100.0)); public static long GetHeroLevelXpPercentage(this ICharacterAlgorithm characterAlgorithm, short percentage, short level) => Convert.ToInt64(characterAlgorithm.GetHeroLevelXp(level) * (percentage / 100.0)); public static long GetJobXpPercentage(this ICharacterAlgorithm characterAlgorithm, short percentage, short level) => Convert.ToInt64(characterAlgorithm.GetJobXp(level) * (percentage / 100.0)); public static int GetEmptyQuestSlot(this IClientSession session, QuestSlotType slotType, bool isSoundFlower = false) { IReadOnlyCollection quests = session.PlayerEntity.GetCurrentQuests().Where(s => s.SlotType == slotType).ToList(); int currentGeneralQuestsCount = quests.Count + session.PlayerEntity.GetPendingSoundFlowerQuests(); int slot = slotType switch { QuestSlotType.GENERAL => currentGeneralQuestsCount > (isSoundFlower ? 5 : 4) ? -1 : quests.Count, QuestSlotType.MAIN => !quests.Any() ? 5 : -1, QuestSlotType.SECONDARY => quests.Count > 4 ? -1 : quests.Count + 6, _ => -1 }; return slot; } public static int GetQuestSlot(this IClientSession session, CharacterQuest characterQuest) { var quests = session.PlayerEntity.GetCurrentQuests().Where(s => s.SlotType == characterQuest.SlotType).ToList(); return characterQuest.SlotType switch { QuestSlotType.GENERAL => quests.IndexOf(characterQuest), QuestSlotType.MAIN => 5, QuestSlotType.SECONDARY => quests.IndexOf(characterQuest) + 6, _ => -1 }; } public static bool HasAlreadyQuestOrQuestline(this IClientSession session, QuestDto quest, IQuestManager questManager, INpcRunTypeQuestsConfiguration npcRunTypeQuestsConfiguration) { return session.PlayerEntity.HasQuestWithId(quest.Id) || session.PlayerEntity.GetCurrentQuests().SelectMany(s => questManager.GetQuestlines(s.QuestId)).Intersect(questManager.GetQuestlines(quest.Id)).Any() || session.HasRunningPeriodicQuest(quest, npcRunTypeQuestsConfiguration); } public static async Task HasCompletedPeriodicQuest(this IClientSession session, QuestDto quest, IQuestManager questManager, INpcRunTypeQuestsConfiguration npcRunTypeQuestsConfiguration, IPeriodicQuestsConfiguration periodicQuestsConfiguration) { IEnumerable completedQuests = session.PlayerEntity.GetCompletedPeriodicQuests(); PeriodicQuestSet periodicQuestSet = periodicQuestsConfiguration.GetPeriodicQuestSetByQuestId(quest.Id); if (periodicQuestSet == null) { return completedQuests.Any(s => npcRunTypeQuestsConfiguration.HaveTheSameNpcRunType(s.QuestId, quest.Id)); } bool couldAddKey = periodicQuestSet.PerNoswingsAccount is true ? await questManager.TryTakeDailyQuest(session.Account.MasterAccountId, periodicQuestSet.Id) : await questManager.TryTakeDailyQuest(session.PlayerEntity.Id, periodicQuestSet.Id); return completedQuests.Any(s => npcRunTypeQuestsConfiguration.HaveTheSameNpcRunType(s.QuestId, quest.Id)) || !couldAddKey; } public static bool HasRunningPeriodicQuest(this IClientSession session, QuestDto quest, INpcRunTypeQuestsConfiguration npcRunTypeQuestsConfiguration) { IEnumerable activeQuests = session.PlayerEntity.GetCurrentQuests(); return activeQuests.Any(s => npcRunTypeQuestsConfiguration.HaveTheSameNpcRunType(s.QuestId, quest.Id)); } public static PeriodicQuestSet GetPeriodicQuestSet(this QuestDto quest, IQuestManager questManager, IPeriodicQuestsConfiguration periodicQuestsConfiguration) { PeriodicQuestSet periodicQuestSet = periodicQuestsConfiguration.GetDailyQuests().FirstOrDefault(s => s.QuestVnums.Any(t => questManager.GetQuestById(t).NextQuestId == quest.Id)); return periodicQuestSet; } public static bool ShowNpcDialog(this IClientSession session, INpcEntity npcEntity, IQuestManager questManager) { IEnumerable characterQuests = session.PlayerEntity.GetCurrentQuestsByTypes(DialogQuestTypes).ToArray(); if (!characterQuests.Any()) { return true; } foreach (CharacterQuest characterQuest in characterQuests) { if (questManager.IsNpcBlueAlertQuest(characterQuest.QuestId) && (characterQuest.Quest.QuestType == QuestType.DIALOG || characterQuest.Quest.QuestType == QuestType.DIALOG_2)) { continue; } foreach (QuestObjectiveDto objective in characterQuest.Quest.Objectives) { if (npcEntity.NpcVNum != (characterQuest.Quest.QuestType is QuestType.DELIVER_ITEM_TO_NPC ? objective.Data1 : objective.Data0)) { continue; } CharacterQuestObjectiveDto questObjectiveDto = characterQuest.ObjectiveAmount[objective.ObjectiveIndex]; switch (characterQuest.Quest.QuestType) { case QuestType.DELIVER_ITEM_TO_NPC: case QuestType.GIVE_ITEM_TO_NPC_2: case QuestType.GIVE_ITEM_TO_NPC: int amountLeft = questObjectiveDto.RequiredAmount - questObjectiveDto.CurrentAmount; if (amountLeft == 0) { continue; } int amountInPossession = session.PlayerEntity.CountItemWithVnum(characterQuest.Quest.QuestType is QuestType.DELIVER_ITEM_TO_NPC ? objective.Data0 : objective.Data1); if (amountInPossession == 0) { continue; } return false; case QuestType.GIVE_NPC_GOLD: int totalGoldToGive = questObjectiveDto.RequiredAmount; if (session.PlayerEntity.Gold < totalGoldToGive) { continue; } return false; case QuestType.DIALOG: case QuestType.DIALOG_2: return false; } } } return true; } } }