diff --git a/EOLib/Domain/Interact/INPCInteractionNotifier.cs b/EOLib/Domain/Interact/INPCInteractionNotifier.cs new file mode 100644 index 000000000..90132af19 --- /dev/null +++ b/EOLib/Domain/Interact/INPCInteractionNotifier.cs @@ -0,0 +1,16 @@ +using AutomaticTypeMapper; +using EOLib.IO; + +namespace EOLib.Domain.Interact +{ + public interface INPCInteractionNotifier + { + void NotifyInteractionFromNPC(NPCType npcType); + } + + [AutoMappedType] + public class NoOpNPCInteractionNotifier : INPCInteractionNotifier + { + public void NotifyInteractionFromNPC(NPCType npcType) { } + } +} diff --git a/EOLib/Domain/Interact/MapNPCActions.cs b/EOLib/Domain/Interact/MapNPCActions.cs index eb04fcc91..4eafbf762 100644 --- a/EOLib/Domain/Interact/MapNPCActions.cs +++ b/EOLib/Domain/Interact/MapNPCActions.cs @@ -1,4 +1,6 @@ using AutomaticTypeMapper; +using EOLib.Domain.NPC; +using EOLib.IO.Repositories; using EOLib.Net; using EOLib.Net.Communication; @@ -8,16 +10,31 @@ namespace EOLib.Domain.Interact public class MapNPCActions : IMapNPCActions { private readonly IPacketSendService _packetSendService; + private readonly IENFFileProvider _enfFileProvider; - public MapNPCActions(IPacketSendService packetSendService) + public MapNPCActions(IPacketSendService packetSendService, + IENFFileProvider enfFileProvider) { _packetSendService = packetSendService; + _enfFileProvider = enfFileProvider; } - public void RequestShop(byte index) + public void RequestShop(INPC npc) { var packet = new PacketBuilder(PacketFamily.Shop, PacketAction.Open) - .AddShort(index) + .AddShort(npc.Index) + .Build(); + + _packetSendService.SendPacket(packet); + } + + public void RequestQuest(INPC npc) + { + var data = _enfFileProvider.ENFFile[npc.ID]; + + var packet = new PacketBuilder(PacketFamily.Quest, PacketAction.Use) + .AddShort(npc.Index) + .AddShort(data.VendorID) .Build(); _packetSendService.SendPacket(packet); @@ -26,6 +43,8 @@ public void RequestShop(byte index) public interface IMapNPCActions { - void RequestShop(byte index); + void RequestShop(INPC npc); + + void RequestQuest(INPC npc); } } 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/IStatusLabelNotifier.cs b/EOLib/Domain/Interact/Quest/IStatusLabelNotifier.cs new file mode 100644 index 000000000..de069a688 --- /dev/null +++ b/EOLib/Domain/Interact/Quest/IStatusLabelNotifier.cs @@ -0,0 +1,15 @@ +using AutomaticTypeMapper; + +namespace EOLib.Domain.Interact.Quest +{ + public interface IStatusLabelNotifier + { + void ShowWarning(string message); + } + + [AutoMappedType] + public class NoOpStatusLabelNotifier : IStatusLabelNotifier + { + public void ShowWarning(string message) { } + } +} diff --git a/EOLib/Domain/Interact/Quest/QuestActions.cs b/EOLib/Domain/Interact/Quest/QuestActions.cs new file mode 100644 index 000000000..0be5b3df5 --- /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 = 0); + } +} diff --git a/EOLib/Domain/Interact/Quest/QuestDataRepository.cs b/EOLib/Domain/Interact/Quest/QuestDataRepository.cs new file mode 100644 index 000000000..e6ae8083f --- /dev/null +++ b/EOLib/Domain/Interact/Quest/QuestDataRepository.cs @@ -0,0 +1,38 @@ +using AutomaticTypeMapper; +using EOLib.Domain.NPC; +using Optional; + +namespace EOLib.Domain.Interact.Quest +{ + public interface IQuestDataRepository : IResettable + { + INPC RequestedNPC { get; set; } + + Option QuestDialogData { get; set; } + } + + public interface IQuestDataProvider : IResettable + { + INPC RequestedNPC { get; } + + Option QuestDialogData { get; } + } + + [AutoMappedType(IsSingleton = true)] + public class QuestDataRepository : IQuestDataProvider, IQuestDataRepository + { + public INPC RequestedNPC { get; set; } + + 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/Message.cs b/EOLib/Net/API/Message.cs deleted file mode 100644 index 2f128cda8..000000000 --- a/EOLib/Net/API/Message.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using EOLib.Net.Handlers; - -namespace EOLib.Net.API -{ - partial class PacketAPI - { - public event Action OnStatusMessage; - - private void _createMessageMembers() - { - m_client.AddPacketHandler(new FamilyActionPair(PacketFamily.Message, PacketAction.Open), _handleMessageOpen, true); - } - - private void _handleMessageOpen(OldPacket pkt) - { - if (OnStatusMessage != null) - OnStatusMessage(pkt.GetEndString()); - } - } -} diff --git a/EOLib/Net/API/PacketAPI.cs b/EOLib/Net/API/PacketAPI.cs index f57f3b8cf..f0c22ad66 100644 --- a/EOLib/Net/API/PacketAPI.cs +++ b/EOLib/Net/API/PacketAPI.cs @@ -24,7 +24,6 @@ public PacketAPI(EOClient client) _createChestMembers(); _createInitMembers(); _createLockerMembers(); - _createMessageMembers(); _createMusicMembers(); _createPartyMembers(); _createNPCMembers(); 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/Interact/Shop/ShopOpenHandler.cs b/EOLib/PacketHandlers/Interact/Shop/ShopOpenHandler.cs index 79e250ad7..66d9eb0d4 100644 --- a/EOLib/PacketHandlers/Interact/Shop/ShopOpenHandler.cs +++ b/EOLib/PacketHandlers/Interact/Shop/ShopOpenHandler.cs @@ -1,4 +1,5 @@ using AutomaticTypeMapper; +using EOLib.Domain.Interact; using EOLib.Domain.Interact.Shop; using EOLib.Domain.Login; using EOLib.Net; @@ -11,16 +12,19 @@ namespace EOLib.PacketHandlers.Interact.Shop public class ShopOpenHandler : InGameOnlyPacketHandler { private readonly IShopDataRepository _shopDataRepository; + private readonly IEnumerable _npcInteractionNotifiers; public override PacketFamily Family => PacketFamily.Shop; public override PacketAction Action => PacketAction.Open; public ShopOpenHandler(IPlayerInfoProvider playerInfoProvider, - IShopDataRepository shopDataRepository) + IShopDataRepository shopDataRepository, + IEnumerable npcInteractionNotifiers) : base(playerInfoProvider) { _shopDataRepository = shopDataRepository; + _npcInteractionNotifiers = npcInteractionNotifiers; } public override bool HandlePacket(IPacket packet) @@ -62,6 +66,9 @@ public override bool HandlePacket(IPacket packet) _shopDataRepository.CraftItems = craftItems; + foreach (var notifier in _npcInteractionNotifiers) + notifier.NotifyInteractionFromNPC(IO.NPCType.Shop); + return true; } } diff --git a/EOLib/PacketHandlers/Items/QuestItemChangeHandler.cs b/EOLib/PacketHandlers/Items/QuestItemChangeHandler.cs index 9d91e47e2..1228f9c3f 100644 --- a/EOLib/PacketHandlers/Items/QuestItemChangeHandler.cs +++ b/EOLib/PacketHandlers/Items/QuestItemChangeHandler.cs @@ -31,19 +31,20 @@ public override bool HandlePacket(IPacket packet) var amount = packet.ReadThree(); var weight = packet.ReadChar(); - var inventoryItem = _inventoryRepository.ItemInventory.SingleOrNone(x => x.ItemID == id); - inventoryItem.MatchSome(x => _inventoryRepository.ItemInventory.Remove(x)); + var inventoryItem = _inventoryRepository.ItemInventory + .SingleOrNone(x => x.ItemID == id) + .Match(x => x, () => new InventoryItem(id, 0)); + _inventoryRepository.ItemInventory.Remove(inventoryItem); if (amount > 0) { - var amountRemaining = inventoryItem.Match( - some: x => Action == PacketAction.Kick ? x.Amount - amount : x.Amount + amount, - none: () => Action == PacketAction.Kick ? 0 : amount); + var amountRemaining = Action == PacketAction.Kick + ? amount + : inventoryItem.Amount + amount; if (amountRemaining > 0) { - inventoryItem.Map(x => x.WithAmount(amount)) - .MatchSome(x => _inventoryRepository.ItemInventory.Add(x)); + _inventoryRepository.ItemInventory.Add(inventoryItem.WithAmount(amountRemaining)); } } diff --git a/EOLib/PacketHandlers/Quest/QuestDialogHandler.cs b/EOLib/PacketHandlers/Quest/QuestDialogHandler.cs new file mode 100644 index 000000000..97ddaa08d --- /dev/null +++ b/EOLib/PacketHandlers/Quest/QuestDialogHandler.cs @@ -0,0 +1,83 @@ +using AutomaticTypeMapper; +using EOLib.Domain.Interact; +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 readonly IEnumerable _npcInteractionNotifiers; + + 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, + IEnumerable npcInteractionNotifiers) + : base(playerInfoProvider) + { + _questDataRepository = questDataRepository; + _npcInteractionNotifiers = npcInteractionNotifiers; + } + + 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); + + foreach (var notifier in _npcInteractionNotifiers) + notifier.NotifyInteractionFromNPC(IO.NPCType.Quest); + + return true; + } + } +} diff --git a/EOLib/PacketHandlers/Quest/QuestStatusMessageHandler.cs b/EOLib/PacketHandlers/Quest/QuestStatusMessageHandler.cs new file mode 100644 index 000000000..d16b97a19 --- /dev/null +++ b/EOLib/PacketHandlers/Quest/QuestStatusMessageHandler.cs @@ -0,0 +1,43 @@ +using AutomaticTypeMapper; +using EOLib.Domain.Chat; +using EOLib.Domain.Interact.Quest; +using EOLib.Domain.Login; +using EOLib.Net; +using EOLib.Net.Handlers; +using System.Collections.Generic; + +namespace EOLib.PacketHandlers.Quest +{ + [AutoMappedType] + public class QuestStatusMessageHandler : InGameOnlyPacketHandler + { + private readonly IChatRepository _chatRepository; + private readonly IEnumerable _statusLabelNotifiers; + + public override PacketFamily Family => PacketFamily.Message; + + public override PacketAction Action => PacketAction.Open; + + public QuestStatusMessageHandler(IPlayerInfoProvider playerInfoProvider, + IChatRepository chatRepository, + IEnumerable statusLabelNotifiers) + : base(playerInfoProvider) + { + _chatRepository = chatRepository; + _statusLabelNotifiers = statusLabelNotifiers; + } + + public override bool HandlePacket(IPacket packet) + { + var message = packet.ReadEndString(); + + foreach (var notifier in _statusLabelNotifiers) + notifier.ShowWarning(message); + + var chatData = new ChatData(string.Empty, message, ChatIcon.QuestMessage, ChatColor.Server); + _chatRepository.AllChat[ChatTab.System].Add(chatData); + + return true; + } + } +} diff --git a/EndlessClient/Content/ContentProvider.cs b/EndlessClient/Content/ContentProvider.cs index 7994ee752..99080f5fd 100644 --- a/EndlessClient/Content/ContentProvider.cs +++ b/EndlessClient/Content/ContentProvider.cs @@ -89,6 +89,7 @@ private void RefreshTextures() private void RefreshFonts() { _fonts[Constants.FontSize08] = _content.Load(Constants.FontSize08); + _fonts[Constants.FontSize09] = _content.Load(Constants.FontSize09); } } } diff --git a/EndlessClient/Controllers/MapInteractionController.cs b/EndlessClient/Controllers/MapInteractionController.cs index f4f32598c..54ffa556a 100644 --- a/EndlessClient/Controllers/MapInteractionController.cs +++ b/EndlessClient/Controllers/MapInteractionController.cs @@ -97,7 +97,7 @@ public void LeftClick(IMapCellState cellState, IMouseCursorRenderer mouseRendere { _characterActions.ToggleSit(); } - else if (cellState.InBounds) + else if (cellState.InBounds && !cellState.Character.HasValue && !cellState.NPC.HasValue) { mouseRenderer.AnimateClick(); _hudControlProvider.GetComponent(HudControlIdentifier.CharacterAnimator) diff --git a/EndlessClient/Controllers/NPCInteractionController.cs b/EndlessClient/Controllers/NPCInteractionController.cs index 86c96e295..43d27f8fa 100644 --- a/EndlessClient/Controllers/NPCInteractionController.cs +++ b/EndlessClient/Controllers/NPCInteractionController.cs @@ -1,5 +1,4 @@ using AutomaticTypeMapper; -using EndlessClient.Dialogs.Actions; using EOLib.Domain.Interact; using EOLib.Domain.NPC; using EOLib.IO.Repositories; @@ -10,15 +9,12 @@ namespace EndlessClient.Controllers public class NPCInteractionController : INPCInteractionController { private readonly IMapNPCActions _mapNpcActions; - private readonly IInGameDialogActions _inGameDialogActions; private readonly IENFFileProvider _enfFileProvider; public NPCInteractionController(IMapNPCActions mapNpcActions, - IInGameDialogActions inGameDialogActions, IENFFileProvider enfFileProvider) { _mapNpcActions = mapNpcActions; - _inGameDialogActions = inGameDialogActions; _enfFileProvider = enfFileProvider; } @@ -29,8 +25,10 @@ public void ShowNPCDialog(INPC npc) switch(data.Type) { case EOLib.IO.NPCType.Shop: - _mapNpcActions.RequestShop(npc.Index); - _inGameDialogActions.ShowShopDialog(); + _mapNpcActions.RequestShop(npc); + break; + case EOLib.IO.NPCType.Quest: + _mapNpcActions.RequestQuest(npc); break; } } diff --git a/EndlessClient/Dialogs/Actions/InGameDialogActions.cs b/EndlessClient/Dialogs/Actions/InGameDialogActions.cs index c0d4e9d4e..20382a4e9 100644 --- a/EndlessClient/Dialogs/Actions/InGameDialogActions.cs +++ b/EndlessClient/Dialogs/Actions/InGameDialogActions.cs @@ -1,31 +1,41 @@ using AutomaticTypeMapper; using EndlessClient.Dialogs.Factories; using EOLib.Domain.Character; +using EOLib.Domain.Interact; +using EOLib.Domain.Interact.Quest; using EOLib.Domain.Interact.Shop; +using EOLib.Domain.NPC; +using EOLib.IO; using Optional; namespace EndlessClient.Dialogs.Actions { [AutoMappedType] - public class InGameDialogActions : IInGameDialogActions + public class InGameDialogActions : IInGameDialogActions, INPCInteractionNotifier { private readonly IFriendIgnoreListDialogFactory _friendIgnoreListDialogFactory; private readonly IPaperdollDialogFactory _paperdollDialogFactory; private readonly IActiveDialogRepository _activeDialogRepository; private readonly IShopDataRepository _shopDataRepository; + private readonly IQuestDataRepository _questDataRepository; private readonly IShopDialogFactory _shopDialogFactory; + private readonly IQuestDialogFactory _questDialogFactory; public InGameDialogActions(IFriendIgnoreListDialogFactory friendIgnoreListDialogFactory, IPaperdollDialogFactory paperdollDialogFactory, IActiveDialogRepository activeDialogRepository, IShopDataRepository shopDataRepository, - IShopDialogFactory shopDialogFactory) + IQuestDataRepository questDataRepository, + IShopDialogFactory shopDialogFactory, + IQuestDialogFactory questDialogFactory) { _friendIgnoreListDialogFactory = friendIgnoreListDialogFactory; _paperdollDialogFactory = paperdollDialogFactory; _activeDialogRepository = activeDialogRepository; _shopDataRepository = shopDataRepository; + _questDataRepository = questDataRepository; _shopDialogFactory = shopDialogFactory; + _questDialogFactory = questDialogFactory; } public void ShowFriendListDialog() @@ -64,6 +74,19 @@ public void ShowPaperdollDialog(ICharacter character, bool isMainCharacter) }); } + public void NotifyInteractionFromNPC(NPCType npcType) + { + // originally, these methods were called directly from NPCInteractionController + // however, this resulted in empty responses (e.g. no shop or quest) showing an empty dialog + // instead, wait for the response packet to notify this class and then show the dialog + // once data has been received from the server + switch (npcType) + { + case NPCType.Shop: ShowShopDialog(); break; + case NPCType.Quest: ShowQuestDialog(); break; + } + } + public void ShowShopDialog() { _activeDialogRepository.ShopDialog.MatchNone(() => @@ -79,6 +102,22 @@ public void ShowShopDialog() dlg.Show(); }); } + + public void ShowQuestDialog() + { + _activeDialogRepository.QuestDialog.MatchNone(() => + { + var dlg = _questDialogFactory.Create(); + dlg.DialogClosed += (_, _) => + { + _activeDialogRepository.QuestDialog = Option.None(); + _questDataRepository.ResetState(); + }; + _activeDialogRepository.QuestDialog = Option.Some(dlg); + + dlg.Show(); + }); + } } public interface IInGameDialogActions @@ -90,5 +129,7 @@ public interface IInGameDialogActions void ShowPaperdollDialog(ICharacter character, bool isMainCharacter); void ShowShopDialog(); + + void ShowQuestDialog(); } } diff --git a/EndlessClient/Dialogs/ActiveDialogRepository.cs b/EndlessClient/Dialogs/ActiveDialogRepository.cs index 97da40ac4..bab7fc4ec 100644 --- a/EndlessClient/Dialogs/ActiveDialogRepository.cs +++ b/EndlessClient/Dialogs/ActiveDialogRepository.cs @@ -15,6 +15,8 @@ public interface IActiveDialogProvider : IDisposable Option ShopDialog { get; } + Option QuestDialog { get; } + IReadOnlyList> ActiveDialogs { get; } } @@ -26,6 +28,8 @@ public interface IActiveDialogRepository : IDisposable Option ShopDialog { get; set; } + Option QuestDialog { get; set; } + IReadOnlyList> ActiveDialogs { get; } } @@ -38,6 +42,8 @@ public class ActiveDialogRepository : IActiveDialogRepository, IActiveDialogProv public Option ShopDialog { get; set; } + public Option QuestDialog { get; set; } + IReadOnlyList> ActiveDialogs { get @@ -47,6 +53,7 @@ IReadOnlyList> ActiveDialogs FriendIgnoreDialog.Map(d => (IXNADialog)d), PaperdollDialog.Map(d => (IXNADialog)d), ShopDialog.Map(d => (IXNADialog)d), + QuestDialog.Map(d => (IXNADialog)d), }.ToList(); } } @@ -63,6 +70,7 @@ public void Dispose() FriendIgnoreDialog = Option.None(); PaperdollDialog = Option.None(); ShopDialog = Option.None(); + QuestDialog = Option.None(); } } } diff --git a/EndlessClient/Dialogs/Factories/QuestDialogFactory.cs b/EndlessClient/Dialogs/Factories/QuestDialogFactory.cs new file mode 100644 index 000000000..0fdc86289 --- /dev/null +++ b/EndlessClient/Dialogs/Factories/QuestDialogFactory.cs @@ -0,0 +1,50 @@ +using AutomaticTypeMapper; +using EndlessClient.Content; +using EndlessClient.Dialogs.Services; +using EOLib.Domain.Interact.Quest; +using EOLib.Graphics; +using EOLib.IO.Repositories; + +namespace EndlessClient.Dialogs.Factories +{ + [AutoMappedType] + public class QuestDialogFactory : IQuestDialogFactory + { + private readonly INativeGraphicsManager _nativeGraphicsManager; + private readonly IQuestActions _questActions; + private readonly IEODialogButtonService _dialogButtonService; + private readonly IQuestDataProvider _questDataProvider; + private readonly IENFFileProvider _enfFileProvider; + private readonly IContentProvider _contentProvider; + + public QuestDialogFactory(INativeGraphicsManager nativeGraphicsManager, + IQuestActions questActions, + IEODialogButtonService dialogButtonService, + IQuestDataProvider questDataProvider, + IENFFileProvider enfFileProvider, + IContentProvider contentProvider) + { + _nativeGraphicsManager = nativeGraphicsManager; + _questActions = questActions; + _dialogButtonService = dialogButtonService; + _questDataProvider = questDataProvider; + _enfFileProvider = enfFileProvider; + _contentProvider = contentProvider; + } + + public QuestDialog Create() + { + return new QuestDialog(_nativeGraphicsManager, + _questActions, + _dialogButtonService, + _questDataProvider, + _enfFileProvider, + _contentProvider); + } + } + + public interface IQuestDialogFactory + { + QuestDialog Create(); + } +} diff --git a/EndlessClient/Dialogs/ListDialogItem.cs b/EndlessClient/Dialogs/ListDialogItem.cs index 3ddb51833..b98267dff 100644 --- a/EndlessClient/Dialogs/ListDialogItem.cs +++ b/EndlessClient/Dialogs/ListDialogItem.cs @@ -223,10 +223,9 @@ protected override void OnUpdateControl(GameTime gameTime) else if(CurrentMouseState.LeftButton == ButtonState.Released && PreviousMouseState.LeftButton == ButtonState.Pressed) { - // todo: this might cause the click event to be fired twice, need to double check it - if (_subText is XNAHyperLink && _subText.MouseOver) - ((XNAHyperLink)_subText).Click(); - else + if ((_subText is IXNAHyperLink && !_subText.MouseOver) || + (_primaryText is IXNAHyperLink && !_primaryText.MouseOver) || + !(_primaryText is IXNAHyperLink || _subText is IXNAHyperLink)) LeftClick?.Invoke(this, EventArgs.Empty); _parentList.ChildControlClickHandled = true; diff --git a/EndlessClient/Dialogs/Old/QuestDialog.cs b/EndlessClient/Dialogs/Old/QuestDialog.cs deleted file mode 100644 index 83212e3cf..000000000 --- a/EndlessClient/Dialogs/Old/QuestDialog.cs +++ /dev/null @@ -1,266 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using EndlessClient.Dialogs.Services; -using EndlessClient.Old; -using EndlessClient.UIControls; -using EOLib; -using EOLib.Graphics; -using EOLib.Net.API; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using XNAControls; -using LabelAlignment = XNAControls.Old.LabelAlignment; -using XNAButton = XNAControls.Old.XNAButton; -using XNADialogResult = XNAControls.Old.XNADialogResult; -using XNALabel = XNAControls.Old.XNALabel; - -namespace EndlessClient.Dialogs.Old -{ - public class QuestDialog : OldScrollingListDialog - { - public static QuestDialog Instance { get; private set; } - - 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)! - - if (!api.TalkToQuestNPC(npcIndex, questID)) - EOGame.Instance.DoShowLostConnectionDialogAndReturnToMainMenu(); - } - - public static void SetupInstance(PacketAPI api) - { - if(Instance != null) - Instance.Close(null, XNADialogResult.NO_BUTTON_PRESSED); - - Instance = new QuestDialog(api); - } - - private QuestState _stateInfo; - private Dictionary _dialogNames, _links; - private List _pages; - private short _pageIndex; - - private static string NPCName { get; set; } - - private QuestDialog(PacketAPI api) - : base(api) - { - DialogClosing += (o, e) => - { - if (e.Result == XNADialogResult.OK) - { - if (!m_api.RespondToQuestDialog(_stateInfo, DialogReply.Ok)) - ((EOGame) Game).DoShowLostConnectionDialogAndReturnToMainMenu(); - } - Instance = null; - }; - - _dialogNames = new Dictionary(); - _links = new Dictionary(); - _pages = new List(); - - _setBackgroundTexture(((EOGame)Game).GFXManager.TextureFromResource(GFXTypes.PostLoginUI, 67)); - - caption = new XNALabel(new Rectangle(16, 16, 255, 18), Constants.FontSize08pt5) - { - AutoSize = false, - TextAlign = LabelAlignment.MiddleLeft, - ForeColor = ColorConstants.LightGrayText - }; - caption.SetParent(this); - - m_scrollBar.SetParent(null); - m_scrollBar.Close(); - - m_scrollBar = new OldScrollBar(this, new Vector2(252, 44), new Vector2(16, 99), ScrollBarColors.LightOnMed); - m_scrollBar.SetParent(this); - SmallItemStyleMaxItemDisplay = 6; - } - - public override void Update(GameTime gt) - { - try - { - base.Update(gt); - } - catch - { - //Running up against weird thread synchronization error. The call to base XNAControl.Update throws an exception because - // the draw area is 0, but the debugger has the correct values. This is a temporary workaround. Basically re-throws - // the exception when the draw area is ACTUALLY invalid. - if (DrawArea.Width*DrawArea.Height == 0 || children.Any(x => x.DrawArea.Width*x.DrawArea.Height == 0)) - throw; - - base.Update(gt); - } - } - - public void SetDisplayData(QuestState stateinfo, Dictionary dialognames, List pages, Dictionary links) - { - if(dialognames.Count == 0) - throw new ArgumentException("Invalid quest dialog data received from server", nameof(dialognames)); - - _stateInfo = stateinfo; - _dialogNames = dialognames; - _pages = pages; - _links = links; - - _pageIndex = 0; - - _setDialogTitle(); - _setDialogText(); - _setDialogButtons(); - } - - private void _setDialogTitle() - { - string title = NPCName; - if (!_dialogNames.ContainsKey(_stateInfo.VendorID) && _dialogNames.Count == 1) - title += " - " + _dialogNames.First(); - else if(_dialogNames.ContainsKey(_stateInfo.VendorID)) - title += " - " + _dialogNames[_stateInfo.VendorID]; - - caption.Text = title; - caption.ResizeBasedOnText(); - } - - private void _setDialogText() - { - ClearItemList(); - - List rows = new List(); - - TextSplitter ts = new TextSplitter(_pages[_pageIndex], Game.Content.Load(Constants.FontSize08pt5)); - if (!ts.NeedsProcessing) - rows.Add(_pages[_pageIndex]); - else - rows.AddRange(ts.SplitIntoLines()); - - int index = 0; - foreach (var row in rows) - { - var rowItem = new OldListDialogItem(this, OldListDialogItem.ListItemStyle.Small, index++) - { - Text = row - }; - AddItemToList(rowItem, false); - } - - if (_pageIndex < _pages.Count - 1) - return; - - OldListDialogItem item = new OldListDialogItem(this, OldListDialogItem.ListItemStyle.Small, index++) { Text = " " }; - AddItemToList(item, false); - - foreach (var link in _links) - { - OldListDialogItem linkItem = new OldListDialogItem(this, OldListDialogItem.ListItemStyle.Small, index++) - { - Text = link.Value - }; - - var linkIndex = (byte)link.Key; - linkItem.SetPrimaryTextLink(() => _clickLink(linkIndex)); - AddItemToList(linkItem, false); - } - } - - private void _setDialogButtons() - { - dlgButtons.ForEach(btn => - { - btn.SetParent(null); - btn.Close(); - }); - dlgButtons.Clear(); - - bool morePages = _pageIndex < _pages.Count - 1; - bool firstPage = _pageIndex == 0; - - Vector2 firstLoc = new Vector2(89, 153), secondLoc = new Vector2(183, 153); - - if (firstPage && morePages) - { - //show cancel/next - XNAButton cancel = new XNAButton(smallButtonSheet, firstLoc, _getSmallButtonOut(SmallButton.Cancel), _getSmallButtonOver(SmallButton.Cancel)); - cancel.OnClick += (o, e) => Close(cancel, XNADialogResult.Cancel); - cancel.SetParent(this); - dlgButtons.Add(cancel); - - XNAButton next = new XNAButton(smallButtonSheet, secondLoc, _getSmallButtonOut(SmallButton.Next), _getSmallButtonOver(SmallButton.Next)); - next.OnClick += (o, e) => _nextPage(); - next.SetParent(this); - dlgButtons.Add(next); - } - else if (!firstPage && morePages) - { - //show back/next - XNAButton back = new XNAButton(smallButtonSheet, firstLoc, _getSmallButtonOut(SmallButton.Back), _getSmallButtonOver(SmallButton.Back)); - back.OnClick += (o, e) => _prevPage(); - back.SetParent(this); - dlgButtons.Add(back); - - XNAButton next = new XNAButton(smallButtonSheet, secondLoc, _getSmallButtonOut(SmallButton.Next), _getSmallButtonOver(SmallButton.Next)); - next.OnClick += (o, e) => _nextPage(); - next.SetParent(this); - dlgButtons.Add(next); - } - else if (firstPage) - { - //show cancel/ok - XNAButton cancel = new XNAButton(smallButtonSheet, firstLoc, _getSmallButtonOut(SmallButton.Cancel), _getSmallButtonOver(SmallButton.Cancel)); - cancel.OnClick += (o, e) => Close(cancel, XNADialogResult.Cancel); - cancel.SetParent(this); - dlgButtons.Add(cancel); - - XNAButton ok = new XNAButton(smallButtonSheet, secondLoc, _getSmallButtonOut(SmallButton.Ok), _getSmallButtonOver(SmallButton.Ok)); - ok.OnClick += (o, e) => Close(ok, XNADialogResult.OK); - ok.SetParent(this); - dlgButtons.Add(ok); - } - else - { - //show back/ok - XNAButton back = new XNAButton(smallButtonSheet, firstLoc, _getSmallButtonOut(SmallButton.Back), _getSmallButtonOver(SmallButton.Back)); - back.OnClick += (o, e) => _prevPage(); - back.SetParent(this); - dlgButtons.Add(back); - - XNAButton ok = new XNAButton(smallButtonSheet, secondLoc, _getSmallButtonOut(SmallButton.Ok), _getSmallButtonOver(SmallButton.Ok)); - ok.OnClick += (o, e) => Close(ok, XNADialogResult.OK); - ok.SetParent(this); - dlgButtons.Add(ok); - } - } - - 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(); - } - - Close(null, XNADialogResult.Cancel); - } - - private void _nextPage() - { - _pageIndex++; - _setDialogText(); - _setDialogButtons(); - } - - private void _prevPage() - { - _pageIndex--; - _setDialogText(); - _setDialogButtons(); - } - } -} diff --git a/EndlessClient/Dialogs/QuestDialog.cs b/EndlessClient/Dialogs/QuestDialog.cs new file mode 100644 index 000000000..fa5ddabd2 --- /dev/null +++ b/EndlessClient/Dialogs/QuestDialog.cs @@ -0,0 +1,180 @@ +using EndlessClient.Content; +using EndlessClient.Dialogs.Services; +using EOLib; +using EOLib.Domain.Interact.Quest; +using EOLib.Domain.NPC; +using EOLib.Graphics; +using EOLib.IO.Repositories; +using Microsoft.Xna.Framework; +using Optional; +using System; +using System.Collections.Generic; +using XNAControls; + +namespace EndlessClient.Dialogs +{ + public class QuestDialog : ScrollingListDialog + { + private readonly IQuestActions _questActions; + private readonly IQuestDataProvider _questDataProvider; + private readonly IENFFileProvider _enfFileProvider; + private readonly IContentProvider _contentProvider; + + private Option _cachedData; + + private int _pageIndex = 0; + + public QuestDialog(INativeGraphicsManager nativeGraphicsManager, + IQuestActions questActions, + IEODialogButtonService dialogButtonService, + IQuestDataProvider questDataProvider, + IENFFileProvider enfFileProvider, + IContentProvider contentProvider) + : base(nativeGraphicsManager, dialogButtonService, dialogSize: ScrollingListDialogSize.SmallDialog) + { + _questActions = questActions; + _questDataProvider = questDataProvider; + _enfFileProvider = enfFileProvider; + _contentProvider = contentProvider; + + _cachedData = Option.None(); + + ListItemType = ListDialogItem.ListItemStyle.Small; + + NextAction += NextPage; + BackAction += PreviousPage; + DialogClosing += (_, e) => + { + if (e.Result == XNADialogResult.OK) + _questActions.RespondToQuestDialog(DialogReply.Ok); + }; + } + + protected override void OnUpdateControl(GameTime gameTime) + { + _questDataProvider.QuestDialogData.MatchSome(data => UpdateCachedDataIfNeeded(_cachedData, data)); + base.OnUpdateControl(gameTime); + } + + private void UpdateCachedDataIfNeeded(Option cachedData, IQuestDialogData repoData) + { + cachedData.Match( + some: cached => + { + _cachedData = Option.Some(repoData); + if (!cached.Equals(repoData)) + UpdateDialogControls(repoData); + }, + none: () => + { + _cachedData = Option.Some(repoData); + UpdateDialogControls(repoData); + }); + } + + private void UpdateDialogControls(IQuestDialogData repoData) + { + _pageIndex = 0; + + UpdateTitle(repoData); + UpdateDialogDisplayText(repoData); + UpdateButtons(repoData); + } + + private void UpdateTitle(IQuestDialogData repoData) + { + var npcName = _enfFileProvider.ENFFile[_questDataProvider.RequestedNPC.ID].Name; + var titleText = npcName; + if (!repoData.DialogTitles.ContainsKey(repoData.VendorID) && repoData.DialogTitles.Count == 1) + titleText += $" - {repoData.DialogTitles[0]}"; + else if (repoData.DialogTitles.ContainsKey(repoData.VendorID)) + titleText += $" - {repoData.DialogTitles[repoData.VendorID]}"; + + _titleText.Text = titleText; + _titleText.ResizeBasedOnText(); + } + + private void UpdateDialogDisplayText(IQuestDialogData repoData) + { + ClearItemList(); + + var rows = new List(); + + var ts = new TextSplitter(repoData.PageText[_pageIndex], _contentProvider.Fonts[Constants.FontSize09]); + if (!ts.NeedsProcessing) + rows.Add(repoData.PageText[_pageIndex]); + else + rows.AddRange(ts.SplitIntoLines()); + + int index = 0; + foreach (var row in rows) + { + var rowItem = new ListDialogItem(this, ListDialogItem.ListItemStyle.Small, index++) + { + PrimaryText = row, + }; + + AddItemToList(rowItem, sortList: false); + } + + // The links are only shown on the last page of the quest dialog + if (_pageIndex < repoData.PageText.Count - 1) + return; + + var item = new ListDialogItem(this, ListDialogItem.ListItemStyle.Small, index++) { PrimaryText = " " }; + AddItemToList(item, sortList: false); + + foreach (var action in repoData.Actions) + { + var actionItem = new ListDialogItem(this, ListDialogItem.ListItemStyle.Small, index++) + { + PrimaryText = action.DisplayText + }; + + var linkIndex = (byte)action.ActionID; + actionItem.SetPrimaryClickAction((_, _) => + { + _questActions.RespondToQuestDialog(DialogReply.Link, linkIndex); + Close(XNADialogResult.Cancel); + }); + + AddItemToList(actionItem, sortList: false); + } + } + + private void UpdateButtons(IQuestDialogData repoData) + { + bool morePages = _pageIndex < repoData.PageText.Count - 1; + bool firstPage = _pageIndex == 0; + + if (firstPage && morePages) + Buttons = ScrollingListDialogButtons.CancelNext; + else if (!firstPage && morePages) + Buttons = ScrollingListDialogButtons.BackNext; + else if (firstPage) + Buttons = ScrollingListDialogButtons.CancelOk; + else + Buttons = ScrollingListDialogButtons.BackOk; + } + + private void NextPage(object sender, EventArgs e) + { + _cachedData.MatchSome(data => + { + _pageIndex++; + UpdateDialogDisplayText(data); + UpdateButtons(data); + }); + } + + private void PreviousPage(object sender, EventArgs e) + { + _cachedData.MatchSome(data => + { + _pageIndex--; + UpdateDialogDisplayText(data); + UpdateButtons(data); + }); + } + } +} diff --git a/EndlessClient/Dialogs/ScrollingListDialog.cs b/EndlessClient/Dialogs/ScrollingListDialog.cs index 3fbf7e08a..2d8c34f5b 100644 --- a/EndlessClient/Dialogs/ScrollingListDialog.cs +++ b/EndlessClient/Dialogs/ScrollingListDialog.cs @@ -10,17 +10,32 @@ namespace EndlessClient.Dialogs { + [Flags] public enum ScrollingListDialogButtons { - AddCancel, - Cancel, - BackCancel, + Add = 1, + Cancel = 2, + Back = 4, + Next = 8, + Ok = 16, + DualButtons = 32, + // todo: if enum values are ever added to this, the logic in ScrollingListDialog.Buttons needs to be updated + AddCancel = DualButtons | Add | Cancel, + BackCancel = DualButtons | Back | Cancel, + BackOk = DualButtons | Back | Ok, + CancelOk = DualButtons | Cancel | Ok, + BackNext = DualButtons | Back | Next, + CancelNext = DualButtons | Cancel | Next, } - public class ScrollingListDialog : BaseEODialog + public enum ScrollingListDialogSize { - private static readonly Vector2 _cancelButtonRightPosition, _cancelButtonCenteredPosition; + LargeDialog, + SmallDialog, + } + public class ScrollingListDialog : BaseEODialog + { private readonly List _listItems; protected readonly ScrollBar _scrollBar; @@ -28,6 +43,9 @@ public class ScrollingListDialog : BaseEODialog private ListDialogItem.ListItemStyle _listItemType; protected readonly XNAButton _add, _back, _cancel; + protected readonly XNAButton _next, _ok; + + protected readonly Vector2 _button1Position, _button2Position, _buttonCenterPosition; private ScrollingListDialogButtons _buttons; @@ -49,8 +67,11 @@ public ListDialogItem.ListItemStyle ListItemType get => _listItemType; set { + if (value == ListDialogItem.ListItemStyle.Large && DialogSize == ScrollingListDialogSize.SmallDialog) + throw new InvalidOperationException("Can't use large ListDialogItem with small scrolling dialog"); + _listItemType = value; - ItemsToShow = _listItemType == ListDialogItem.ListItemStyle.Large ? 5 : 12; + ItemsToShow = _listItemType == ListDialogItem.ListItemStyle.Large ? 5 : DialogSize == ScrollingListDialogSize.SmallDialog ? 6 : 12; _scrollBar.LinesToRender = ItemsToShow; } } @@ -61,48 +82,82 @@ public ScrollingListDialogButtons Buttons set { _buttons = value; - _add.Visible = Buttons == ScrollingListDialogButtons.AddCancel; - _back.Visible = Buttons == ScrollingListDialogButtons.BackCancel; - _cancel.DrawPosition = Buttons == ScrollingListDialogButtons.Cancel - ? _cancelButtonCenteredPosition - : _cancelButtonRightPosition; + _add.Visible = Buttons.HasFlag(ScrollingListDialogButtons.Add); + _back.Visible = Buttons.HasFlag(ScrollingListDialogButtons.Back); + _next.Visible = Buttons.HasFlag(ScrollingListDialogButtons.Next); + _ok.Visible = Buttons.HasFlag(ScrollingListDialogButtons.Ok); + _cancel.Visible = Buttons.HasFlag(ScrollingListDialogButtons.Cancel); + + if (Buttons.HasFlag(ScrollingListDialogButtons.DualButtons)) + { + if (Buttons == ScrollingListDialogButtons.BackCancel || + Buttons == ScrollingListDialogButtons.AddCancel) + { + _add.DrawPosition = _button1Position; + _back.DrawPosition = _button1Position; + _cancel.DrawPosition = _button2Position; + } + else + { + _back.DrawPosition = _button1Position; + _cancel.DrawPosition = _button1Position; + + _next.DrawPosition = _button2Position; + _ok.DrawPosition = _button2Position; + } + } + else + { + _add.DrawPosition = _buttonCenterPosition; + _back.DrawPosition = _buttonCenterPosition; + _next.DrawPosition = _buttonCenterPosition; + _ok.DrawPosition = _buttonCenterPosition; + _cancel.DrawPosition = _buttonCenterPosition; + } } } public INativeGraphicsManager GraphicsManager { get; } + public ScrollingListDialogSize DialogSize { get; } + public event EventHandler AddAction; public event EventHandler BackAction; - public bool ChildControlClickHandled { get; set; } + public event EventHandler NextAction; - static ScrollingListDialog() - { - _cancelButtonRightPosition = new Vector2(144, 252); - _cancelButtonCenteredPosition = new Vector2(96, 252); - } + public bool ChildControlClickHandled { get; set; } public ScrollingListDialog(INativeGraphicsManager nativeGraphicsManager, - IEODialogButtonService dialogButtonService) + IEODialogButtonService dialogButtonService, + ScrollingListDialogSize dialogSize = ScrollingListDialogSize.LargeDialog) : base(isInGame: true) { + GraphicsManager = nativeGraphicsManager; + DialogSize = dialogSize; + + var isLargeDialog = DialogSize == ScrollingListDialogSize.LargeDialog; + _listItems = new List(); _titleText = new XNALabel(Constants.FontSize09) { - DrawArea = new Rectangle(16, 13, 253, 19), + DrawArea = isLargeDialog ? new Rectangle(16, 13, 253, 19) : new Rectangle(16, 16, 255, 18), AutoSize = false, TextAlign = LabelAlignment.MiddleLeft, ForeColor = ColorConstants.LightGrayText }; _titleText.SetParentControl(this); - _scrollBar = new ScrollBar(new Vector2(252, 44), new Vector2(16, 199), ScrollBarColors.LightOnMed, nativeGraphicsManager); + _scrollBar = new ScrollBar(new Vector2(252, 44), new Vector2(16, isLargeDialog ? 199 : 99), ScrollBarColors.LightOnMed, GraphicsManager); _scrollBar.SetParentControl(this); - _add = new XNAButton(dialogButtonService.SmallButtonSheet, - new Vector2(48, 252), + BackgroundTexture = GraphicsManager.TextureFromResource(GFXTypes.PostLoginUI, isLargeDialog ? 52 : 67); + + var yCoord = isLargeDialog ? 252 : 152; + + _add = new XNAButton(dialogButtonService.SmallButtonSheet, Vector2.Zero, dialogButtonService.GetSmallDialogButtonOutSource(SmallButton.Add), dialogButtonService.GetSmallDialogButtonOverSource(SmallButton.Add)) { @@ -113,8 +168,7 @@ public ScrollingListDialog(INativeGraphicsManager nativeGraphicsManager, _add.OnClick += (o, e) => AddAction?.Invoke(o, e); AddAction += (_, _) => _otherClicked = true; - _back = new XNAButton(dialogButtonService.SmallButtonSheet, - new Vector2(48, 252), + _back = new XNAButton(dialogButtonService.SmallButtonSheet, Vector2.Zero, dialogButtonService.GetSmallDialogButtonOutSource(SmallButton.Back), dialogButtonService.GetSmallDialogButtonOverSource(SmallButton.Back)) { @@ -125,24 +179,47 @@ public ScrollingListDialog(INativeGraphicsManager nativeGraphicsManager, _back.OnClick += (o, e) => BackAction?.Invoke(o, e); BackAction += (_, _) => _otherClicked = true; - _cancel = new XNAButton(dialogButtonService.SmallButtonSheet, - _cancelButtonRightPosition, + _next = new XNAButton(dialogButtonService.SmallButtonSheet, Vector2.Zero, + dialogButtonService.GetSmallDialogButtonOutSource(SmallButton.Next), + dialogButtonService.GetSmallDialogButtonOverSource(SmallButton.Next)) + { + Visible = false, + UpdateOrder = 1, + }; + _next.SetParentControl(this); + _next.OnClick += (o, e) => NextAction?.Invoke(o, e); + NextAction += (_, _) => _otherClicked = true; + + _ok = new XNAButton(dialogButtonService.SmallButtonSheet, Vector2.Zero, + dialogButtonService.GetSmallDialogButtonOutSource(SmallButton.Ok), + dialogButtonService.GetSmallDialogButtonOverSource(SmallButton.Ok)) + { + Visible = false, + UpdateOrder = 2, + }; + _ok.SetParentControl(this); + _ok.OnClick += (_, _) => { if (!_otherClicked) { Close(XNADialogResult.OK); } }; + + _cancel = new XNAButton(dialogButtonService.SmallButtonSheet, Vector2.Zero, dialogButtonService.GetSmallDialogButtonOutSource(SmallButton.Cancel), dialogButtonService.GetSmallDialogButtonOverSource(SmallButton.Cancel)) { - Visible = true, + Visible = false, UpdateOrder = 2, }; _cancel.SetParentControl(this); _cancel.OnClick += (_, _) => { if (!_otherClicked) { Close(XNADialogResult.Cancel); } }; - ItemsToShow = ListItemType == ListDialogItem.ListItemStyle.Large ? 5 : 12; + ItemsToShow = ListItemType == ListDialogItem.ListItemStyle.Large ? 5 : DialogSize == ScrollingListDialogSize.SmallDialog ? 6 : 12; - BackgroundTexture = nativeGraphicsManager.TextureFromResource(GFXTypes.PostLoginUI, 52); + _button1Position = new Vector2(isLargeDialog ? 48 : 89, yCoord); + _button2Position = new Vector2(isLargeDialog ? 144 : 183, yCoord); + _buttonCenterPosition = new Vector2(96, yCoord); + + Buttons = ScrollingListDialogButtons.AddCancel; CenterInGameView(); DrawPosition = new Vector2(DrawPosition.X, 15); - GraphicsManager = nativeGraphicsManager; } public void SetItemList(List itemList) @@ -213,6 +290,8 @@ public override void Initialize() { _add.Initialize(); _back.Initialize(); + _next.Initialize(); + _ok.Initialize(); _cancel.Initialize(); _scrollBar.Initialize(); _titleText.Initialize(); diff --git a/EndlessClient/HUD/StatusLabelSetter.cs b/EndlessClient/HUD/StatusLabelSetter.cs index 6506a5925..8627bc17a 100644 --- a/EndlessClient/HUD/StatusLabelSetter.cs +++ b/EndlessClient/HUD/StatusLabelSetter.cs @@ -1,11 +1,12 @@ using System; using AutomaticTypeMapper; +using EOLib.Domain.Interact.Quest; using EOLib.Localization; namespace EndlessClient.HUD { - [MappedType(BaseType = typeof(IStatusLabelSetter))] - public class StatusLabelSetter : IStatusLabelSetter + [AutoMappedType] + public class StatusLabelSetter : IStatusLabelSetter, IStatusLabelNotifier { private readonly IStatusLabelTextRepository _statusLabelTextRepository; private readonly ILocalizedStringFinder _localizedStringFinder; @@ -40,6 +41,11 @@ public void SetStatusLabel(EOResourceID type, string text) SetStatusLabelText(_localizedStringFinder.GetString(type), text); } + public void ShowWarning(string message) + { + SetStatusLabel(EOResourceID.STATUS_LABEL_TYPE_WARNING, message); + } + private void SetStatusLabelText(string type, string text, string extra = "") { _statusLabelTextRepository.StatusText = $"[ {type} ] {text}{extra}"; diff --git a/EndlessClient/Old/OldWorld.cs b/EndlessClient/Old/OldWorld.cs index 2a4ff2a59..e03dd4850 100644 --- a/EndlessClient/Old/OldWorld.cs +++ b/EndlessClient/Old/OldWorld.cs @@ -288,7 +288,6 @@ public static void IgnoreDialogs(XNAControl control) control.IgnoreDialog(typeof(LockerDialog)); control.IgnoreDialog(typeof(TradeDialog)); control.IgnoreDialog(typeof(SkillmasterDialog)); - control.IgnoreDialog(typeof(QuestDialog)); control.IgnoreDialog(typeof(QuestProgressDialog)); } diff --git a/EndlessClient/Old/PacketAPICallbackManager.cs b/EndlessClient/Old/PacketAPICallbackManager.cs index f32047686..2fc251a5c 100644 --- a/EndlessClient/Old/PacketAPICallbackManager.cs +++ b/EndlessClient/Old/PacketAPICallbackManager.cs @@ -5,8 +5,6 @@ using EndlessClient.Dialogs; using EndlessClient.Dialogs.Old; using EOLib.Domain.Character; -using EOLib.Domain.Chat; -using EOLib.Domain.Map; using EOLib.Localization; using EOLib.Net.API; using XNAControls.Old; @@ -71,10 +69,8 @@ public void AssignCallbacks() m_packetAPI.OnCharacterStatsReset += _statskillReset; //quests - m_packetAPI.OnQuestDialog += _questDialog; m_packetAPI.OnViewQuestProgress += _questProgress; m_packetAPI.OnViewQuestHistory += _questHistory; - m_packetAPI.OnStatusMessage += _setStatusLabel; m_packetAPI.OnPlaySoundEffect += _playSoundEffect; @@ -344,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; @@ -369,12 +354,6 @@ private void _questHistory(short numquests, List completedquestnames) QuestProgressDialog.Instance.SetHistoryDisplayData(numquests, completedquestnames); } - private void _setStatusLabel(string message) - { - m_game.Hud.SetStatusLabel(EOResourceID.STATUS_LABEL_TYPE_WARNING, message); - m_game.Hud.AddChat(ChatTab.System, "", message, ChatIcon.QuestMessage, ChatColor.Server); - } - private void _playSoundEffect(int effectID) { try 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; } } } diff --git a/EndlessClient/Subscribers/MainCharacterEventSubscriber.cs b/EndlessClient/Subscribers/MainCharacterEventSubscriber.cs index 0094fdf5f..1b7c0a523 100644 --- a/EndlessClient/Subscribers/MainCharacterEventSubscriber.cs +++ b/EndlessClient/Subscribers/MainCharacterEventSubscriber.cs @@ -34,7 +34,7 @@ public void NotifyGainedExp(int expDifference) { _statusLabelSetter.SetStatusLabel(EOResourceID.STATUS_LABEL_TYPE_INFORMATION, EOResourceID.STATUS_LABEL_YOU_GAINED_EXP, - $"{expDifference} EXP"); + $" {expDifference} EXP"); var youGained = _localizedStringFinder.GetString(EOResourceID.STATUS_LABEL_YOU_GAINED_EXP); var message = $"{youGained} {expDifference} EXP";