diff --git a/EOLib/Domain/Character/CharacterInventoryRepository.cs b/EOLib/Domain/Character/CharacterInventoryRepository.cs index c5783edcd..8fd40cade 100644 --- a/EOLib/Domain/Character/CharacterInventoryRepository.cs +++ b/EOLib/Domain/Character/CharacterInventoryRepository.cs @@ -5,31 +5,31 @@ namespace EOLib.Domain.Character { public interface ICharacterInventoryRepository { - List ItemInventory { get; set; } + HashSet ItemInventory { get; set; } - List SpellInventory { get; set; } + HashSet SpellInventory { get; set; } } public interface ICharacterInventoryProvider { - IReadOnlyList ItemInventory { get; } + IReadOnlyCollection ItemInventory { get; } - IReadOnlyList SpellInventory { get; } + IReadOnlyCollection SpellInventory { get; } } [AutoMappedType(IsSingleton = true)] public class CharacterInventoryRepository : ICharacterInventoryRepository, ICharacterInventoryProvider { - public List ItemInventory { get; set; } - public List SpellInventory { get; set; } + public HashSet ItemInventory { get; set; } + public HashSet SpellInventory { get; set; } - IReadOnlyList ICharacterInventoryProvider.ItemInventory => ItemInventory; - IReadOnlyList ICharacterInventoryProvider.SpellInventory => SpellInventory; + IReadOnlyCollection ICharacterInventoryProvider.ItemInventory => ItemInventory; + IReadOnlyCollection ICharacterInventoryProvider.SpellInventory => SpellInventory; public CharacterInventoryRepository() { - ItemInventory = new List(32); - SpellInventory = new List(32); + ItemInventory = new HashSet(); + SpellInventory = new HashSet(); } } } diff --git a/EOLib/Domain/Character/InventoryItem.cs b/EOLib/Domain/Character/InventoryItem.cs index 46494bf10..4cbcdcbd1 100644 --- a/EOLib/Domain/Character/InventoryItem.cs +++ b/EOLib/Domain/Character/InventoryItem.cs @@ -16,6 +16,21 @@ public IInventoryItem WithAmount(int newAmount) { return new InventoryItem(ItemID, newAmount); } + + public override bool Equals(object obj) + { + var other = obj as InventoryItem; + if (other == null) return false; + return other.ItemID == ItemID && other.Amount == Amount; + } + + public override int GetHashCode() + { + int hashCode = 1754760722; + hashCode = hashCode * -1521134295 + ItemID.GetHashCode(); + hashCode = hashCode * -1521134295 + Amount.GetHashCode(); + return hashCode; + } } public interface IInventoryItem diff --git a/EOLib/Domain/Character/InventorySpell.cs b/EOLib/Domain/Character/InventorySpell.cs index 4682e133a..cbc3af8d3 100644 --- a/EOLib/Domain/Character/InventorySpell.cs +++ b/EOLib/Domain/Character/InventorySpell.cs @@ -16,6 +16,21 @@ public IInventorySpell WithLevel(short newLevel) { return new InventorySpell(ID, newLevel); } + + public override bool Equals(object obj) + { + var other = obj as InventorySpell; + if (other == null) return false; + return other.ID == ID && other.Level == Level; + } + + public override int GetHashCode() + { + int hashCode = 1754760722; + hashCode = hashCode * -1521134295 + ID.GetHashCode(); + hashCode = hashCode * -1521134295 + Level.GetHashCode(); + return hashCode; + } } public interface IInventorySpell diff --git a/EOLib/Domain/Login/LoginActions.cs b/EOLib/Domain/Login/LoginActions.cs index 4a9572939..af11dc03d 100644 --- a/EOLib/Domain/Login/LoginActions.cs +++ b/EOLib/Domain/Login/LoginActions.cs @@ -171,8 +171,8 @@ public async Task CompleteCharacterLogin() .WithStats(stats) .WithRenderProperties(mainCharacter.RenderProperties); - _characterInventoryRepository.ItemInventory = data.CharacterItemInventory.ToList(); - _characterInventoryRepository.SpellInventory = data.CharacterSpellInventory.ToList(); + _characterInventoryRepository.ItemInventory = new HashSet(data.CharacterItemInventory); + _characterInventoryRepository.SpellInventory = new HashSet(data.CharacterSpellInventory); _currentMapStateRepository.Characters = data.MapCharacters.Except(new[] { mainCharacter }).ToDictionary(k => k.ID, v => v); _currentMapStateRepository.NPCs = new HashSet(data.MapNPCs); diff --git a/EOLib/Net/Translators/LoginRequestCompletedPacketTranslator.cs b/EOLib/Net/Translators/LoginRequestCompletedPacketTranslator.cs index 332d9171e..f5907de6f 100644 --- a/EOLib/Net/Translators/LoginRequestCompletedPacketTranslator.cs +++ b/EOLib/Net/Translators/LoginRequestCompletedPacketTranslator.cs @@ -39,6 +39,9 @@ public override ILoginRequestCompletedData TranslatePacket(IPacket packet) var maxWeight = packet.ReadChar(); var inventoryItems = GetInventoryItems(packet).ToList(); + if (!inventoryItems.Any(x => x.ItemID == 1)) + inventoryItems.Insert(0, new InventoryItem(1, 0)); + var inventorySpells = GetInventorySpells(packet).ToList(); if (inventoryItems.All(x => x.ItemID != 1)) diff --git a/EOLib/PacketHandlers/ItemEquipHandler.cs b/EOLib/PacketHandlers/ItemEquipHandler.cs index abdd49227..748dfecff 100644 --- a/EOLib/PacketHandlers/ItemEquipHandler.cs +++ b/EOLib/PacketHandlers/ItemEquipHandler.cs @@ -98,7 +98,7 @@ protected bool HandlePaperdollPacket(IPacket packet, bool itemUnequipped) .Match(some: invItem => invItem.WithAmount(itemUnequipped ? invItem.Amount + amount : amount), none: () => new InventoryItem(itemId, amount)); - _characterInventoryRepository.ItemInventory.RemoveAll(x => x.ItemID == itemId); + _characterInventoryRepository.ItemInventory.RemoveWhere(x => x.ItemID == itemId); _characterInventoryRepository.ItemInventory.Add(updatedItem); } else diff --git a/EOLib/misc.cs b/EOLib/misc.cs index 9ed5fb536..442169185 100644 --- a/EOLib/misc.cs +++ b/EOLib/misc.cs @@ -45,6 +45,8 @@ public static class Constants public const string FriendListFile = "config/friends.ini"; public const string IgnoreListFile = "config/ignore.ini"; + public const string InventoryFile = "config/inventory.ini"; + //Should be easily customizable between different clients (based on graphics) //not a config option because this shouldn't be exposed at the user level public static readonly int[] TrapSpikeGFXObjectIDs = {449, 450, 451, 452}; diff --git a/EndlessClient/HUD/Panels/HudPanelFactory.cs b/EndlessClient/HUD/Panels/HudPanelFactory.cs index 07755e770..c05cc3d81 100644 --- a/EndlessClient/HUD/Panels/HudPanelFactory.cs +++ b/EndlessClient/HUD/Panels/HudPanelFactory.cs @@ -2,6 +2,7 @@ using EndlessClient.Content; using EndlessClient.Controllers; using EndlessClient.ControlSets; +using EndlessClient.Dialogs.Actions; using EndlessClient.Dialogs.Factories; using EndlessClient.Rendering.Chat; using EndlessClient.Services; @@ -20,40 +21,49 @@ public class HudPanelFactory : IHudPanelFactory private const int HUD_CONTROL_LAYER = 130; private readonly INativeGraphicsManager _nativeGraphicsManager; + private readonly IInGameDialogActions _inGameDialogActions; private readonly IContentProvider _contentProvider; private readonly IHudControlProvider _hudControlProvider; private readonly INewsProvider _newsProvider; private readonly IChatProvider _chatProvider; + private readonly IPlayerInfoProvider _playerInfoProvider; private readonly ICharacterProvider _characterProvider; private readonly ICharacterInventoryProvider _characterInventoryProvider; private readonly IExperienceTableProvider _experienceTableProvider; private readonly IEOMessageBoxFactory _messageBoxFactory; private readonly ITrainingController _trainingController; private readonly IFriendIgnoreListService _friendIgnoreListService; + private readonly IStatusLabelSetter _statusLabelSetter; public HudPanelFactory(INativeGraphicsManager nativeGraphicsManager, + IInGameDialogActions inGameDialogActions, IContentProvider contentProvider, IHudControlProvider hudControlProvider, INewsProvider newsProvider, IChatProvider chatProvider, + IPlayerInfoProvider playerInfoProvider, ICharacterProvider characterProvider, ICharacterInventoryProvider characterInventoryProvider, IExperienceTableProvider experienceTableProvider, IEOMessageBoxFactory messageBoxFactory, ITrainingController trainingController, - IFriendIgnoreListService friendIgnoreListService) + IFriendIgnoreListService friendIgnoreListService, + IStatusLabelSetter statusLabelSetter) { _nativeGraphicsManager = nativeGraphicsManager; + _inGameDialogActions = inGameDialogActions; _contentProvider = contentProvider; _hudControlProvider = hudControlProvider; _newsProvider = newsProvider; _chatProvider = chatProvider; + _playerInfoProvider = playerInfoProvider; _characterProvider = characterProvider; _characterInventoryProvider = characterInventoryProvider; _experienceTableProvider = experienceTableProvider; _messageBoxFactory = messageBoxFactory; _trainingController = trainingController; _friendIgnoreListService = friendIgnoreListService; + _statusLabelSetter = statusLabelSetter; } public NewsPanel CreateNewsPanel() @@ -68,7 +78,12 @@ public NewsPanel CreateNewsPanel() public InventoryPanel CreateInventoryPanel() { - return new InventoryPanel(_nativeGraphicsManager) { DrawOrder = HUD_CONTROL_LAYER }; + return new InventoryPanel(_nativeGraphicsManager, + _inGameDialogActions, + _statusLabelSetter, + _playerInfoProvider, + _characterProvider, + _characterInventoryProvider) { DrawOrder = HUD_CONTROL_LAYER }; } public ActiveSpellsPanel CreateActiveSpellsPanel() diff --git a/EndlessClient/HUD/Panels/InventoryPanel.cs b/EndlessClient/HUD/Panels/InventoryPanel.cs index c31cc1da5..d5b045d0c 100644 --- a/EndlessClient/HUD/Panels/InventoryPanel.cs +++ b/EndlessClient/HUD/Panels/InventoryPanel.cs @@ -1,19 +1,224 @@ -using EOLib.Graphics; +using EndlessClient.Dialogs.Actions; +using EndlessClient.UIControls; +using EOLib; +using EOLib.Config; +using EOLib.Domain.Character; +using EOLib.Domain.Login; +using EOLib.Graphics; +using EOLib.Localization; +using Microsoft.Win32; using Microsoft.Xna.Framework; +using Optional; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; using XNAControls; namespace EndlessClient.HUD.Panels { public class InventoryPanel : XNAPanel, IHudPanel { + public const int InventoryRowSlots = 14; + + // uses absolute coordinates + private static readonly Rectangle InventoryGridArea = new Rectangle(110, 334, 377, 116); + private readonly INativeGraphicsManager _nativeGraphicsManager; + private readonly IStatusLabelSetter _statusLabelSetter; + private readonly IPlayerInfoProvider _playerInfoProvider; + private readonly ICharacterProvider _characterProvider; + private readonly ICharacterInventoryProvider _characterInventoryProvider; + + private readonly bool[,] _usedSlots = new bool[4, InventoryRowSlots]; + private readonly Dictionary _itemSlotMap; + //private readonly List _childItems = new List(); - public InventoryPanel(INativeGraphicsManager nativeGraphicsManager) + private readonly IXNALabel _weightLabel; + private readonly IXNAButton _drop, _junk, _paperdoll; + //private readonly ScrollBar _scrollBar; + + private Option _cachedStats; + private HashSet _cachedInventory; + + public InventoryPanel(INativeGraphicsManager nativeGraphicsManager, + IInGameDialogActions inGameDialogActions, + IStatusLabelSetter statusLabelSetter, + IPlayerInfoProvider playerInfoProvider, + ICharacterProvider characterProvider, + ICharacterInventoryProvider characterInventoryProvider) { _nativeGraphicsManager = nativeGraphicsManager; + _statusLabelSetter = statusLabelSetter; + _playerInfoProvider = playerInfoProvider; + _characterProvider = characterProvider; + _characterInventoryProvider = characterInventoryProvider; + + _weightLabel = new XNALabel(Constants.FontSize08pt5) + { + DrawArea = new Rectangle(385, 37, 88, 18), + ForeColor = ColorConstants.LightGrayText, + TextAlign = LabelAlignment.MiddleCenter, + Visible = true, + AutoSize = false + }; + + _itemSlotMap = GetItemSlotMap(_playerInfoProvider.LoggedInAccountName, _characterProvider.MainCharacter.Name); + + var weirdOffsetSheet = _nativeGraphicsManager.TextureFromResource(GFXTypes.PostLoginUI, 27); + + _paperdoll = new XNAButton(weirdOffsetSheet, new Vector2(385, 9), new Rectangle(39, 385, 88, 19), new Rectangle(126, 385, 88, 19)); + _paperdoll.OnMouseEnter += MouseOverButton; + _paperdoll.OnClick += (_, _) => inGameDialogActions.ShowPaperdollDialog(characterProvider.MainCharacter, isMainCharacter: true); + _drop = new XNAButton(weirdOffsetSheet, new Vector2(389, 68), new Rectangle(0, 15, 38, 37), new Rectangle(0, 52, 38, 37)); + _drop.OnMouseEnter += MouseOverButton; + _junk = new XNAButton(weirdOffsetSheet, new Vector2(431, 68), new Rectangle(0, 89, 38, 37), new Rectangle(0, 126, 38, 37)); + _junk.OnMouseEnter += MouseOverButton; + + _cachedStats = Option.None(); + _cachedInventory = new HashSet(); BackgroundImage = _nativeGraphicsManager.TextureFromResource(GFXTypes.PostLoginUI, 44); DrawArea = new Rectangle(102, 330, BackgroundImage.Width, BackgroundImage.Height); } + + public override void Initialize() + { + _weightLabel.Initialize(); + _weightLabel.SetParentControl(this); + + _paperdoll.Initialize(); + _paperdoll.SetParentControl(this); + + _drop.Initialize(); + _drop.SetParentControl(this); + + _junk.Initialize(); + _junk.SetParentControl(this); + + base.Initialize(); + } + + protected override void OnUpdateControl(GameTime gameTime) + { + _cachedStats.Match( + some: stats => + { + stats.SomeWhen(s => s != _characterProvider.MainCharacter.Stats) + .MatchSome(s => + { + _cachedStats = Option.Some(_characterProvider.MainCharacter.Stats); + _weightLabel.Text = $"{stats[CharacterStat.Weight]} / {stats[CharacterStat.MaxWeight]}"; + }); + }, + none: () => + { + var stats = _characterProvider.MainCharacter.Stats; + _cachedStats = Option.Some(stats); + _weightLabel.Text = $"{stats[CharacterStat.Weight]} / {stats[CharacterStat.MaxWeight]}"; + }); + + if (!_cachedInventory.SetEquals(_characterInventoryProvider.ItemInventory)) + { + var added = _characterInventoryProvider.ItemInventory.Where(i => !_cachedInventory.Any(j => i.ItemID == j.ItemID)); + var removed = _cachedInventory.Where(i => !_characterInventoryProvider.ItemInventory.Any(j => i.ItemID == j.ItemID)); + + _cachedInventory = _characterInventoryProvider.ItemInventory.ToHashSet(); + var updated = _cachedInventory.Except(added); + + // todo: update child inventory items + } + + base.OnUpdateControl(gameTime); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + var inventory = new IniReader(Constants.InventoryFile); + if (inventory.Load() && inventory.Sections.ContainsKey(_playerInfoProvider.LoggedInAccountName)) + { + var section = inventory.Sections[_playerInfoProvider.LoggedInAccountName]; + // todo: write out item inventory slots to file + } + + _paperdoll.OnMouseEnter -= MouseOverButton; + _drop.OnMouseEnter -= MouseOverButton; + _junk.OnMouseEnter -= MouseOverButton; + } + + base.Dispose(disposing); + } + + private void MouseOverButton(object sender, EventArgs e) + { + var id = sender == _paperdoll + ? EOResourceID.STATUS_LABEL_INVENTORY_SHOW_YOUR_PAPERDOLL + : sender == _drop + ? EOResourceID.STATUS_LABEL_INVENTORY_DROP_BUTTON + : EOResourceID.STATUS_LABEL_INVENTORY_JUNK_BUTTON; + _statusLabelSetter.SetStatusLabel(EOResourceID.STATUS_LABEL_TYPE_BUTTON, id); + } + + private static Dictionary GetItemSlotMap(string accountName, string characterName) + { + var map = new Dictionary(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !File.Exists(Constants.InventoryFile)) + { + using var inventoryKey = TryGetCharacterRegistryKey(accountName, characterName); + if (inventoryKey != null) + { + for (int i = 0; i < InventoryRowSlots * 4; ++i) + { + if (int.TryParse(inventoryKey.GetValue($"item{i}")?.ToString() ?? string.Empty, out var id)) + map[i] = id; + } + } + } + + var inventory = new IniReader(Constants.InventoryFile); + if (inventory.Load() && inventory.Sections.ContainsKey(accountName)) + { + var section = inventory.Sections[accountName]; + foreach (var key in section.Keys.Where(x => x.Contains(characterName, StringComparison.OrdinalIgnoreCase))) + { + if (!key.Contains(".")) + continue; + + var slot = key.Split(".")[1]; + if (!int.TryParse(slot, out var slotIndex)) + continue; + + if (int.TryParse(section[key], out var id)) + map[slotIndex] = id; + } + } + + return map; + } + + [SupportedOSPlatform("Windows")] + private static RegistryKey TryGetCharacterRegistryKey(string accountName, string characterName) + { + using RegistryKey currentUser = Registry.CurrentUser; + + var pathSegments = $"Software\\EndlessClient\\{accountName}\\{characterName}\\inventory".Split('\\'); + var currentPath = string.Empty; + + RegistryKey retKey = null; + foreach (var segment in pathSegments) + { + retKey?.Dispose(); + + currentPath = Path.Combine(currentPath, segment); + retKey = currentUser.CreateSubKey(currentPath, RegistryKeyPermissionCheck.ReadSubTree); + } + + return retKey; + } } } \ No newline at end of file