From 6397611827ad34b0e0f82af5242ce824b5314859 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 5 Apr 2022 14:40:36 -0700 Subject: [PATCH] Implement network actions for opening quest dialog and responding --- EOLib/Domain/Interact/MapNPCActions.cs | 12 ++ EOLib/Domain/Interact/Quest/DialogReply.cs | 8 + EOLib/Domain/Interact/Quest/QuestActions.cs | 44 +++++ .../Interact/Quest/QuestDataRepository.cs | 31 +++ .../Domain/Interact/Quest/QuestDialogData.cs | 186 ++++++++++++++++++ EOLib/Net/API/Quest.cs | 75 ------- .../Quest/QuestDialogHandler.cs | 76 +++++++ EndlessClient/Dialogs/Old/QuestDialog.cs | 44 ++--- EndlessClient/Old/PacketAPICallbackManager.cs | 12 -- EndlessClient/Rendering/OldNPCRenderer.cs | 1 - 10 files changed, 379 insertions(+), 110 deletions(-) create mode 100644 EOLib/Domain/Interact/Quest/DialogReply.cs create mode 100644 EOLib/Domain/Interact/Quest/QuestActions.cs create mode 100644 EOLib/Domain/Interact/Quest/QuestDataRepository.cs create mode 100644 EOLib/Domain/Interact/Quest/QuestDialogData.cs create mode 100644 EOLib/PacketHandlers/Quest/QuestDialogHandler.cs diff --git a/EOLib/Domain/Interact/MapNPCActions.cs b/EOLib/Domain/Interact/MapNPCActions.cs index eb04fcc91..b1a5ecf90 100644 --- a/EOLib/Domain/Interact/MapNPCActions.cs +++ b/EOLib/Domain/Interact/MapNPCActions.cs @@ -22,10 +22,22 @@ public void RequestShop(byte index) _packetSendService.SendPacket(packet); } + + public void RequestQuest(short index, short vendorId) + { + var packet = new PacketBuilder(PacketFamily.Quest, PacketAction.Use) + .AddShort(index) + .AddShort(vendorId) + .Build(); + + _packetSendService.SendPacket(packet); + } } public interface IMapNPCActions { void RequestShop(byte index); + + void RequestQuest(short index, short vendorId); } } diff --git a/EOLib/Domain/Interact/Quest/DialogReply.cs b/EOLib/Domain/Interact/Quest/DialogReply.cs new file mode 100644 index 000000000..ed8e668a0 --- /dev/null +++ b/EOLib/Domain/Interact/Quest/DialogReply.cs @@ -0,0 +1,8 @@ +namespace EOLib.Domain.Interact.Quest +{ + public enum DialogReply + { + Ok = 1, + Link + } +} diff --git a/EOLib/Domain/Interact/Quest/QuestActions.cs b/EOLib/Domain/Interact/Quest/QuestActions.cs new file mode 100644 index 000000000..5c9e6ef81 --- /dev/null +++ b/EOLib/Domain/Interact/Quest/QuestActions.cs @@ -0,0 +1,44 @@ +using AutomaticTypeMapper; +using EOLib.Net; +using EOLib.Net.Communication; + +namespace EOLib.Domain.Interact.Quest +{ + [AutoMappedType] + public class QuestActions : IQuestActions + { + private readonly IPacketSendService _packetSendService; + private readonly IQuestDataProvider _questDataProvider; + + public QuestActions(IPacketSendService packetSendService, + IQuestDataProvider questDataProvider) + { + _packetSendService = packetSendService; + _questDataProvider = questDataProvider; + } + + public void RespondToQuestDialog(DialogReply reply, byte linkId = 0) + { + _questDataProvider.QuestDialogData.MatchSome(data => + { + var builder = new PacketBuilder(PacketFamily.Quest, PacketAction.Accept) + .AddShort(data.SessionID) // ignored by eoserv + .AddShort(data.DialogID) // ignored by eoserv + .AddShort(data.QuestID) + .AddShort(data.VendorID) // ignored by eoserv + .AddChar((byte)reply); + + if (reply == DialogReply.Link) + builder = builder.AddChar(linkId); + + var packet = builder.Build(); + _packetSendService.SendPacket(packet); + }); + } + } + + public interface IQuestActions + { + void RespondToQuestDialog(DialogReply reply, byte linkId); + } +} diff --git a/EOLib/Domain/Interact/Quest/QuestDataRepository.cs b/EOLib/Domain/Interact/Quest/QuestDataRepository.cs new file mode 100644 index 000000000..96548e827 --- /dev/null +++ b/EOLib/Domain/Interact/Quest/QuestDataRepository.cs @@ -0,0 +1,31 @@ +using AutomaticTypeMapper; +using Optional; + +namespace EOLib.Domain.Interact.Quest +{ + public interface IQuestDataRepository : IResettable + { + Option QuestDialogData { get; set; } + } + + public interface IQuestDataProvider : IResettable + { + Option QuestDialogData { get; } + } + + [AutoMappedType(IsSingleton = true)] + public class QuestDataRepository : IQuestDataProvider, IQuestDataRepository + { + public Option QuestDialogData { get; set; } + + public QuestDataRepository() + { + ResetState(); + } + + public void ResetState() + { + QuestDialogData = Option.None(); + } + } +} diff --git a/EOLib/Domain/Interact/Quest/QuestDialogData.cs b/EOLib/Domain/Interact/Quest/QuestDialogData.cs new file mode 100644 index 000000000..f2782072a --- /dev/null +++ b/EOLib/Domain/Interact/Quest/QuestDialogData.cs @@ -0,0 +1,186 @@ +using System.Collections.Generic; +using System.Linq; + +namespace EOLib.Domain.Interact.Quest +{ + public class QuestDialogData : IQuestDialogData + { + public short VendorID { get; private set; } + + public short QuestID { get; private set; } + + public short SessionID { get; private set; } + + public short DialogID { get; private set; } + + public IReadOnlyDictionary DialogTitles { get; private set; } + + public IReadOnlyList PageText { get; private set; } + + public IReadOnlyList<(short ActionID, string DisplayText)> Actions { get; private set; } + + public QuestDialogData() + { + DialogTitles = new Dictionary(); + PageText = new List(); + Actions = new List<(short, string)>(); + } + + private QuestDialogData(short vendorID, + short questID, + short sessionID, + short dialogID, + IReadOnlyDictionary dialogTitles, + IReadOnlyList pageText, + IReadOnlyList<(short, string)> actions) + { + VendorID = vendorID; + QuestID = questID; + SessionID = sessionID; + DialogID = dialogID; + DialogTitles = dialogTitles; + PageText = pageText; + Actions = actions; + } + + public IQuestDialogData WithVendorID(short vendorId) + { + var copy = MakeCopy(this); + copy.VendorID = vendorId; + return copy; + } + + public IQuestDialogData WithQuestID(short questId) + { + var copy = MakeCopy(this); + copy.QuestID = questId; + return copy; + } + + public IQuestDialogData WithSessionID(short sessionId) + { + var copy = MakeCopy(this); + copy.SessionID = sessionId; + return copy; + } + + public IQuestDialogData WithDialogID(short dialogId) + { + var copy = MakeCopy(this); + copy.DialogID = dialogId; + return copy; + } + + public IQuestDialogData WithDialogTitles(IReadOnlyDictionary dialogTitles) + { + var copy = MakeCopy(this); + copy.DialogTitles = dialogTitles; + return copy; + } + + public IQuestDialogData WithPageText(IReadOnlyList pageText) + { + var copy = MakeCopy(this); + copy.PageText = pageText; + return copy; + } + + public IQuestDialogData WithActions(IReadOnlyList<(short, string)> actions) + { + var copy = MakeCopy(this); + copy.Actions = actions; + return copy; + } + + private static QuestDialogData MakeCopy(IQuestDialogData other) + { + return new QuestDialogData( + other.VendorID, + other.QuestID, + other.SessionID, + other.DialogID, + other.DialogTitles.ToDictionary(k => k.Key, v => v.Value), + new List(other.PageText), + new List<(short, string)>(other.Actions)); + } + + public override bool Equals(object obj) + { + var other = obj as QuestDialogData; + if (other == null) return false; + + return other.VendorID == VendorID + && other.QuestID == QuestID + && other.SessionID == SessionID + && other.DialogID == DialogID + && other.DialogTitles.SequenceEqual(DialogTitles) + && other.PageText.SequenceEqual(PageText) + && other.Actions.SequenceEqual(Actions); + } + + public override int GetHashCode() + { + int hashCode = 170256730; + hashCode = hashCode * -1521134295 + VendorID.GetHashCode(); + hashCode = hashCode * -1521134295 + QuestID.GetHashCode(); + hashCode = hashCode * -1521134295 + SessionID.GetHashCode(); + hashCode = hashCode * -1521134295 + DialogID.GetHashCode(); + hashCode = hashCode * -1521134295 + DialogTitles.Aggregate(170256730, (a, b) => a * -1521134295 + b.GetHashCode()); + hashCode = hashCode * -1521134295 + PageText.Aggregate(170256730, (a, b) => a * -1521134295 + b.GetHashCode()); + hashCode = hashCode * -1521134295 + Actions.Aggregate(170256730, (a, b) => a * -1521134295 + b.GetHashCode()); + return hashCode; + } + } + + public interface IQuestDialogData + { + /// + /// NPC Vendor ID () + /// + short VendorID { get; } + + /// + /// Quest ID, for the current dialog being shown (part of quest state in EO+ parser) + /// + short QuestID { get; } + + /// + /// Session ID, not used by eoserv + /// + short SessionID { get; } + + /// + /// Dialog ID, not used by eoserv + /// + short DialogID { get; } + + /// + /// Quest dialog titles for the current character, keyed on . + /// + IReadOnlyDictionary DialogTitles { get; } + + /// + /// Text for the quest dialog, one entry per page. + /// + IReadOnlyList PageText { get; } + + /// + /// Links for the quest dialog, only shown on the last page. + /// + IReadOnlyList<(short ActionID, string DisplayText)> Actions { get; } + + IQuestDialogData WithVendorID(short vendorId); + + IQuestDialogData WithQuestID(short questId); + + IQuestDialogData WithSessionID(short sessionId); + + IQuestDialogData WithDialogID(short dialogId); + + IQuestDialogData WithDialogTitles(IReadOnlyDictionary dialogTitles); + + IQuestDialogData WithPageText(IReadOnlyList pageText); + + IQuestDialogData WithActions(IReadOnlyList<(short, string)> actions); + } +} diff --git a/EOLib/Net/API/Quest.cs b/EOLib/Net/API/Quest.cs index fa6ef6fff..84d5a1d1b 100644 --- a/EOLib/Net/API/Quest.cs +++ b/EOLib/Net/API/Quest.cs @@ -4,12 +4,6 @@ namespace EOLib.Net.API { - public enum DialogEntry : byte - { - DialogText = 1, - DialogLink - } - public enum DialogReply : byte { Ok = 1, @@ -116,45 +110,14 @@ internal InProgressQuestData(OldPacket pkt) partial class PacketAPI { - public event QuestDialogEvent OnQuestDialog; public event ViewQuestProgressEvent OnViewQuestProgress; public event ViewQuestHistoryEvent OnViewQuestHistory; private void _createQuestMembers() { - m_client.AddPacketHandler(new FamilyActionPair(PacketFamily.Quest, PacketAction.Dialog), _handleQuestDialog, true); m_client.AddPacketHandler(new FamilyActionPair(PacketFamily.Quest, PacketAction.List), _handleQuestList, true); } - public bool TalkToQuestNPC(short npcIndex, short questID) - { - if (!Initialized || !m_client.ConnectedAndInitialized) - return false; - - OldPacket pkt = new OldPacket(PacketFamily.Quest, PacketAction.Use); - pkt.AddShort(npcIndex); - pkt.AddShort(questID); - - return m_client.SendPacket(pkt); - } - - public bool RespondToQuestDialog(QuestState state, DialogReply reply, byte action = 0) - { - if (!Initialized || !m_client.ConnectedAndInitialized) - return false; - - OldPacket pkt = new OldPacket(PacketFamily.Quest, PacketAction.Accept); - pkt.AddShort(state.SessionID); //session ID - ignored by default EOSERV - pkt.AddShort(state.DialogID); //dialog ID - ignored by default EOSERV - pkt.AddShort(state.QuestID); - pkt.AddShort(state.NPCIndex); //npc index - ignored by default EOSERV - pkt.AddChar((byte) reply); - if (reply == DialogReply.Link) - pkt.AddChar(action); - - return m_client.SendPacket(pkt); - } - public bool RequestQuestHistory(QuestPage page) { if (!Initialized || !m_client.ConnectedAndInitialized) @@ -166,44 +129,6 @@ public bool RequestQuestHistory(QuestPage page) return m_client.SendPacket(pkt); } - private void _handleQuestDialog(OldPacket pkt) - { - if (OnQuestDialog == null) return; - - int numDialogs = pkt.GetChar(); - short vendorID = pkt.GetShort(); - short questID = pkt.GetShort(); - short sessionID = pkt.GetShort(); //not used by eoserv - short dialogID = pkt.GetShort(); //not used by eoserv - if (pkt.GetByte() != 255) return; - - QuestState stateInfo = new QuestState(sessionID, dialogID, questID, vendorID); - - var dialogNames = new Dictionary(numDialogs); - for (int i = 0; i < numDialogs; ++i) - { - dialogNames.Add(pkt.GetShort(), pkt.GetBreakString()); - } - - var pages = new List(); - var links = new Dictionary(); - while (pkt.ReadPos != pkt.Length) - { - var entry = (DialogEntry) pkt.GetShort(); - switch (entry) - { - case DialogEntry.DialogText: - pages.Add(pkt.GetBreakString()); - break; - case DialogEntry.DialogLink: - links.Add(pkt.GetShort(), pkt.GetBreakString()); - break; - } - } - - OnQuestDialog(stateInfo, dialogNames, pages, links); - } - private void _handleQuestList(OldPacket pkt) { QuestPage page = (QuestPage) pkt.GetChar(); diff --git a/EOLib/PacketHandlers/Quest/QuestDialogHandler.cs b/EOLib/PacketHandlers/Quest/QuestDialogHandler.cs new file mode 100644 index 000000000..245bb6d79 --- /dev/null +++ b/EOLib/PacketHandlers/Quest/QuestDialogHandler.cs @@ -0,0 +1,76 @@ +using AutomaticTypeMapper; +using EOLib.Domain.Interact.Quest; +using EOLib.Domain.Login; +using EOLib.Net; +using EOLib.Net.Handlers; +using Optional; +using System.Collections.Generic; + +namespace EOLib.PacketHandlers.Quest +{ + [AutoMappedType] + public class QuestDialogHandler : InGameOnlyPacketHandler + { + private readonly IQuestDataRepository _questDataRepository; + + private enum DialogEntryType : byte + { + Text = 1, + Link + } + + public override PacketFamily Family => PacketFamily.Quest; + + public override PacketAction Action => PacketAction.Dialog; + + public QuestDialogHandler(IPlayerInfoProvider playerInfoProvider, + IQuestDataRepository questDataRepository) + : base(playerInfoProvider) + { + _questDataRepository = questDataRepository; + } + + public override bool HandlePacket(IPacket packet) + { + var numDialogs = packet.ReadChar(); + var vendorID = packet.ReadShort(); + var questID = packet.ReadShort(); + var sessionID = packet.ReadShort(); + var dialogID = packet.ReadShort(); + + if (packet.ReadByte() != 255) + return false; + + var questData = new QuestDialogData() + .WithVendorID(vendorID) + .WithQuestID(questID) + .WithSessionID(sessionID) // not used by eoserv + .WithDialogID(dialogID); // not used by eoserv + + var dialogTitles = new Dictionary(numDialogs); + for (int i = 0; i < numDialogs; i++) + dialogTitles.Add(packet.ReadShort(), packet.ReadBreakString()); + + var pages = new List(); + var links = new List<(short, string)>(); + while (packet.ReadPosition < packet.Length) + { + var entryType = (DialogEntryType)packet.ReadShort(); + switch (entryType) + { + case DialogEntryType.Text: pages.Add(packet.ReadBreakString()); break; + case DialogEntryType.Link: links.Add((packet.ReadShort(), packet.ReadBreakString())); break; + default: return false; + } + } + + questData = questData.WithDialogTitles(dialogTitles) + .WithPageText(pages) + .WithActions(links); + + _questDataRepository.QuestDialogData = Option.Some(questData); + + return true; + } + } +} diff --git a/EndlessClient/Dialogs/Old/QuestDialog.cs b/EndlessClient/Dialogs/Old/QuestDialog.cs index 83212e3cf..51b7c235c 100644 --- a/EndlessClient/Dialogs/Old/QuestDialog.cs +++ b/EndlessClient/Dialogs/Old/QuestDialog.cs @@ -19,25 +19,25 @@ namespace EndlessClient.Dialogs.Old { public class QuestDialog : OldScrollingListDialog { - public static QuestDialog Instance { get; private set; } + //public static QuestDialog Instance { get; private set; } - public static void Show(PacketAPI api, short npcIndex, short questID, string name) - { - NPCName = name; + //public static void Show(PacketAPI api, short npcIndex, short questID, string name) + //{ + // NPCName = name; - //note: dialog is created in packet callback! sometimes talking to the quest NPC does nothing (if you already completed)! + // //note: dialog is created in packet callback! sometimes talking to the quest NPC does nothing (if you already completed)! - if (!api.TalkToQuestNPC(npcIndex, questID)) - EOGame.Instance.DoShowLostConnectionDialogAndReturnToMainMenu(); - } + // if (!api.TalkToQuestNPC(npcIndex, questID)) + // EOGame.Instance.DoShowLostConnectionDialogAndReturnToMainMenu(); + //} - public static void SetupInstance(PacketAPI api) - { - if(Instance != null) - Instance.Close(null, XNADialogResult.NO_BUTTON_PRESSED); + //public static void SetupInstance(PacketAPI api) + //{ + // if(Instance != null) + // Instance.Close(null, XNADialogResult.NO_BUTTON_PRESSED); - Instance = new QuestDialog(api); - } + // Instance = new QuestDialog(api); + //} private QuestState _stateInfo; private Dictionary _dialogNames, _links; @@ -53,10 +53,10 @@ private QuestDialog(PacketAPI api) { if (e.Result == XNADialogResult.OK) { - if (!m_api.RespondToQuestDialog(_stateInfo, DialogReply.Ok)) - ((EOGame) Game).DoShowLostConnectionDialogAndReturnToMainMenu(); + //if (!m_api.RespondToQuestDialog(_stateInfo, DialogReply.Ok)) + // ((EOGame) Game).DoShowLostConnectionDialogAndReturnToMainMenu(); } - Instance = null; + //Instance = null; }; _dialogNames = new Dictionary(); @@ -240,11 +240,11 @@ private void _setDialogButtons() private void _clickLink(byte linkID) { //send to server with linkID - if (!m_api.RespondToQuestDialog(_stateInfo, DialogReply.Link, linkID)) - { - Close(null, XNADialogResult.NO_BUTTON_PRESSED); - ((EOGame)Game).DoShowLostConnectionDialogAndReturnToMainMenu(); - } + //if (!m_api.RespondToQuestDialog(_stateInfo, DialogReply.Link, linkID)) + //{ + // Close(null, XNADialogResult.NO_BUTTON_PRESSED); + // ((EOGame)Game).DoShowLostConnectionDialogAndReturnToMainMenu(); + //} Close(null, XNADialogResult.Cancel); } diff --git a/EndlessClient/Old/PacketAPICallbackManager.cs b/EndlessClient/Old/PacketAPICallbackManager.cs index bb60c66f3..2fc251a5c 100644 --- a/EndlessClient/Old/PacketAPICallbackManager.cs +++ b/EndlessClient/Old/PacketAPICallbackManager.cs @@ -69,7 +69,6 @@ public void AssignCallbacks() m_packetAPI.OnCharacterStatsReset += _statskillReset; //quests - m_packetAPI.OnQuestDialog += _questDialog; m_packetAPI.OnViewQuestProgress += _questProgress; m_packetAPI.OnViewQuestHistory += _questHistory; @@ -341,17 +340,6 @@ private void _statskillReset(StatResetData data) m_game.Hud.RemoveAllSpells(); } - private void _questDialog(QuestState stateinfo, Dictionary dialognames, List pages, Dictionary links) - { - if (QuestDialog.Instance == null) - QuestDialog.SetupInstance(m_packetAPI); - - if (QuestDialog.Instance == null) - throw new InvalidOperationException("Something went wrong creating the instance"); - - QuestDialog.Instance.SetDisplayData(stateinfo, dialognames, pages, links); - } - private void _questProgress(short numquests, List questinfo) { if (QuestProgressDialog.Instance == null) return; diff --git a/EndlessClient/Rendering/OldNPCRenderer.cs b/EndlessClient/Rendering/OldNPCRenderer.cs index 64b323840..48d7c4edc 100644 --- a/EndlessClient/Rendering/OldNPCRenderer.cs +++ b/EndlessClient/Rendering/OldNPCRenderer.cs @@ -339,7 +339,6 @@ private void HandleLeftClick() case NPCType.Priest: break; case NPCType.Law: break; case NPCType.Skills: SkillmasterDialog.Show(api, NPC.Index); break; - case NPCType.Quest: QuestDialog.Show(api, NPC.Index, NPC.Data.VendorID, NPC.Data.Name); break; } } }