From c6b92d1dd99977bcc3aa5e8da31f5505466d680a Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 2 May 2023 16:55:21 -0700 Subject: [PATCH 01/15] Don't show health bar when characters use mana potions --- EndlessClient/Subscribers/MainCharacterEventSubscriber.cs | 2 ++ .../Subscribers/OtherCharacterEventSubscriber.cs | 8 +++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/EndlessClient/Subscribers/MainCharacterEventSubscriber.cs b/EndlessClient/Subscribers/MainCharacterEventSubscriber.cs index eed11278d..5ee5145fb 100644 --- a/EndlessClient/Subscribers/MainCharacterEventSubscriber.cs +++ b/EndlessClient/Subscribers/MainCharacterEventSubscriber.cs @@ -45,6 +45,8 @@ public void NotifyGainedExp(int expDifference) public void NotifyTakeDamage(int damageTaken, int playerPercentHealth, bool isHeal) { + if (isHeal && damageTaken == 0) return; + _characterRendererProvider.MainCharacterRenderer.MatchSome(r => r.ShowDamageCounter(damageTaken, playerPercentHealth, isHeal)); } diff --git a/EndlessClient/Subscribers/OtherCharacterEventSubscriber.cs b/EndlessClient/Subscribers/OtherCharacterEventSubscriber.cs index 5a328f3d8..304ce80d0 100644 --- a/EndlessClient/Subscribers/OtherCharacterEventSubscriber.cs +++ b/EndlessClient/Subscribers/OtherCharacterEventSubscriber.cs @@ -25,12 +25,10 @@ public OtherCharacterEventSubscriber(IChatBubbleActions chatBubbleActions, _friendIgnoreListService = friendIgnoreListService; } - public void OtherCharacterTakeDamage(int characterID, - int playerPercentHealth, - int damageTaken, - bool isHeal) + public void OtherCharacterTakeDamage(int characterID, int playerPercentHealth, int damageTaken, bool isHeal) { - if (!_characterRendererProvider.CharacterRenderers.ContainsKey(characterID)) + if (!_characterRendererProvider.CharacterRenderers.ContainsKey(characterID) || + (isHeal && damageTaken == 0)) return; _characterRendererProvider.CharacterRenderers[characterID].ShowDamageCounter(damageTaken, playerPercentHealth, isHeal); From 4414a6bae1b3035f2959a288fba0a4e1e2922984 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 2 May 2023 21:01:11 -0700 Subject: [PATCH 02/15] Fix files always redownloading when switching from a server using split pubs to single pub --- EOLib/Net/FileTransfer/FileRequestActions.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/EOLib/Net/FileTransfer/FileRequestActions.cs b/EOLib/Net/FileTransfer/FileRequestActions.cs index 26f84b198..96098830a 100644 --- a/EOLib/Net/FileTransfer/FileRequestActions.cs +++ b/EOLib/Net/FileTransfer/FileRequestActions.cs @@ -9,6 +9,7 @@ using EOLib.IO.Services; using Optional; using System; +using System.IO; using System.Threading.Tasks; namespace EOLib.Net.FileTransfer @@ -83,15 +84,20 @@ public async Task GetMapFromServer(int mapID, int sessionID) public async Task GetItemFileFromServer(int sessionID) { + DeleteExisting(PubFileNameConstants.EIFFilter); + var itemFiles = await _fileRequestService.RequestFile(InitFileType.Item, sessionID); foreach (var file in itemFiles) _pubFileSaveService.SaveFile(string.Format(PubFileNameConstants.EIFFormat, file.ID), file, rewriteChecksum: false); + _pubFileRepository.EIFFiles = itemFiles; _pubFileRepository.EIFFile = PubFileExtensions.Merge(itemFiles); } public async Task GetNPCFileFromServer(int sessionID) { + DeleteExisting(PubFileNameConstants.ENFFilter); + var npcFiles = await _fileRequestService.RequestFile(InitFileType.Npc, sessionID); foreach (var file in npcFiles) _pubFileSaveService.SaveFile(string.Format(PubFileNameConstants.ENFFormat, file.ID), file, rewriteChecksum: false); @@ -101,6 +107,8 @@ public async Task GetNPCFileFromServer(int sessionID) public async Task GetSpellFileFromServer(int sessionID) { + DeleteExisting(PubFileNameConstants.ESFFilter); + var spellFiles = await _fileRequestService.RequestFile(InitFileType.Spell, sessionID); foreach (var file in spellFiles) _pubFileSaveService.SaveFile(string.Format(PubFileNameConstants.ESFFormat, file.ID), file, rewriteChecksum: false); @@ -110,6 +118,8 @@ public async Task GetSpellFileFromServer(int sessionID) public async Task GetClassFileFromServer(int sessionID) { + DeleteExisting(PubFileNameConstants.ECFFilter); + var classFiles = await _fileRequestService.RequestFile(InitFileType.Class, sessionID); foreach (var file in classFiles) _pubFileSaveService.SaveFile(string.Format(PubFileNameConstants.ECFFormat, file.ID), file, rewriteChecksum: false); @@ -152,6 +162,16 @@ private bool NeedPub(InitFileType fileType) throw new ArgumentOutOfRangeException(nameof(fileType), fileType, null); } } + + private static void DeleteExisting(string filter) + { + try + { + foreach (var file in Directory.GetFiles("pub", filter)) + File.Delete(file); + } + catch (IOException) { } + } } public interface IFileRequestActions From f447949b88b176245058894f8eced6539c89f960 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 2 May 2023 21:01:32 -0700 Subject: [PATCH 03/15] Fix emote display when sitting --- .../CharacterRenderPropertiesExtensions.cs | 4 +++- EndlessClient/Controllers/NumPadController.cs | 2 +- .../Rendering/Character/CharacterAnimator.cs | 3 ++- .../CharacterProperties/FaceRenderer.cs | 8 ++++++-- .../CharacterProperties/WeaponRenderer.cs | 3 ++- .../Sprites/CharacterSpriteCalculator.cs | 16 ++++++++++++---- 6 files changed, 26 insertions(+), 10 deletions(-) diff --git a/EOLib/Domain/Extensions/CharacterRenderPropertiesExtensions.cs b/EOLib/Domain/Extensions/CharacterRenderPropertiesExtensions.cs index e475bef6c..8ea46ba4e 100644 --- a/EOLib/Domain/Extensions/CharacterRenderPropertiesExtensions.cs +++ b/EOLib/Domain/Extensions/CharacterRenderPropertiesExtensions.cs @@ -87,8 +87,10 @@ public static CharacterRenderProperties WithNextEmoteFrame(this CharacterRenderP { var props = rp.ToBuilder(); props.EmoteFrame = (props.EmoteFrame + 1) % CharacterRenderProperties.MAX_NUMBER_OF_EMOTE_FRAMES; + + var resetAction = props.SitState == SitState.Standing ? CharacterActionState.Standing : CharacterActionState.Sitting; props.CurrentAction = props.EmoteFrame == 0 - ? CharacterActionState.Standing + ? resetAction : props.CurrentAction == CharacterActionState.Attacking // when using an instrument keep the current state as "Attacking" ? CharacterActionState.Attacking : CharacterActionState.Emote; diff --git a/EndlessClient/Controllers/NumPadController.cs b/EndlessClient/Controllers/NumPadController.cs index be501e11a..a2c390b11 100644 --- a/EndlessClient/Controllers/NumPadController.cs +++ b/EndlessClient/Controllers/NumPadController.cs @@ -23,7 +23,7 @@ public NumPadController(ICharacterActions characterActions, public void Emote(Emote whichEmote) { - if (!_characterProvider.MainCharacter.RenderProperties.IsActing(CharacterActionState.Standing)) + if (!_characterProvider.MainCharacter.RenderProperties.IsActing(CharacterActionState.Standing, CharacterActionState.Sitting)) return; _characterActions.Emote(whichEmote); diff --git a/EndlessClient/Rendering/Character/CharacterAnimator.cs b/EndlessClient/Rendering/Character/CharacterAnimator.cs index b21889979..ec23be281 100644 --- a/EndlessClient/Rendering/Character/CharacterAnimator.cs +++ b/EndlessClient/Rendering/Character/CharacterAnimator.cs @@ -254,6 +254,7 @@ public bool Emote(int characterID, Emote whichEmote) else { _currentMapStateRepository.UnknownPlayerIDs.Add(characterID); + return false; } _startEmoteTimes[characterID] = startEmoteTime; @@ -536,7 +537,7 @@ private void AnimateCharacterEmotes() var nextFrameRenderProperties = renderProperties.WithNextEmoteFrame(); pair.UpdateActionStartTime(); - if (nextFrameRenderProperties.IsActing(CharacterActionState.Standing)) + if (nextFrameRenderProperties.IsActing(CharacterActionState.Standing, CharacterActionState.Sitting)) playersDoneEmoting.Add(pair.UniqueID); var nextFrameCharacter = currentCharacter.WithRenderProperties(nextFrameRenderProperties); diff --git a/EndlessClient/Rendering/CharacterProperties/FaceRenderer.cs b/EndlessClient/Rendering/CharacterProperties/FaceRenderer.cs index b0c295efd..5392a2cec 100644 --- a/EndlessClient/Rendering/CharacterProperties/FaceRenderer.cs +++ b/EndlessClient/Rendering/CharacterProperties/FaceRenderer.cs @@ -35,8 +35,12 @@ public override void Render(SpriteBatch spriteBatch, Rectangle parentCharacterDr return; var skinLoc = _skinRenderLocationCalculator.CalculateDrawLocationOfCharacterSkin(_skinSheet.SourceRectangle, parentCharacterDrawArea); - var facePos = new Vector2(skinLoc.X + (_renderProperties.IsFacing(EODirection.Down) ? 2 : 3), - skinLoc.Y + (_renderProperties.Gender == 0 ? 2 : 0)); + + var adjustX = _renderProperties.IsFacing(EODirection.Down) + ? _renderProperties.SitState == SitState.Standing ? 2 : 8 + : 3; + + var facePos = new Vector2(skinLoc.X + adjustX, skinLoc.Y + (_renderProperties.Gender == 0 ? 2 : 0)); Render(spriteBatch, _faceSheet, facePos); } diff --git a/EndlessClient/Rendering/CharacterProperties/WeaponRenderer.cs b/EndlessClient/Rendering/CharacterProperties/WeaponRenderer.cs index 6dc37a046..f201c0871 100644 --- a/EndlessClient/Rendering/CharacterProperties/WeaponRenderer.cs +++ b/EndlessClient/Rendering/CharacterProperties/WeaponRenderer.cs @@ -23,7 +23,8 @@ public WeaponRenderer(CharacterRenderProperties renderProperties, public override void Render(SpriteBatch spriteBatch, Rectangle parentCharacterDrawArea) { - if (_renderProperties.IsActing(CharacterActionState.Sitting, CharacterActionState.SpellCast)) + if (_renderProperties.IsActing(CharacterActionState.Sitting, CharacterActionState.SpellCast) || + (_renderProperties.CurrentAction == CharacterActionState.Emote && _renderProperties.SitState != SitState.Standing)) return; var offsets = GetOffsets(parentCharacterDrawArea); diff --git a/EndlessClient/Rendering/Sprites/CharacterSpriteCalculator.cs b/EndlessClient/Rendering/Sprites/CharacterSpriteCalculator.cs index aa16e76ee..07a2059bd 100644 --- a/EndlessClient/Rendering/Sprites/CharacterSpriteCalculator.cs +++ b/EndlessClient/Rendering/Sprites/CharacterSpriteCalculator.cs @@ -31,7 +31,11 @@ public ISpriteSheet GetBootsTexture(CharacterRenderProperties characterRenderPro return new EmptySpriteSheet(); var type = BootsSpriteType.Standing; - switch (characterRenderProperties.CurrentAction) + var currentAction = characterRenderProperties.CurrentAction; + if (currentAction == CharacterActionState.Emote && characterRenderProperties.SitState != SitState.Standing) + currentAction = CharacterActionState.Sitting; + + switch (currentAction) { case CharacterActionState.Walking: switch (characterRenderProperties.RenderWalkFrame) @@ -71,7 +75,11 @@ public ISpriteSheet GetArmorTexture(CharacterRenderProperties characterRenderPro return new EmptySpriteSheet(); var type = ArmorShieldSpriteType.Standing; - switch (characterRenderProperties.CurrentAction) + var currentAction = characterRenderProperties.CurrentAction; + if (currentAction == CharacterActionState.Emote && characterRenderProperties.SitState != SitState.Standing) + currentAction = CharacterActionState.Sitting; + + switch (currentAction) { case CharacterActionState.Walking: switch (characterRenderProperties.RenderWalkFrame) @@ -172,7 +180,7 @@ public ISpriteSheet GetShieldTexture(CharacterRenderProperties characterRenderPr { type = ArmorShieldSpriteType.SpellCast; } - else if(characterRenderProperties.CurrentAction == CharacterActionState.Sitting) + else if(characterRenderProperties.SitState != SitState.Standing) { return new EmptySpriteSheet(); } @@ -298,7 +306,7 @@ public ISpriteSheet GetSkinTexture(CharacterRenderProperties characterRenderProp { gfxNum = 4; } - else if (characterRenderProperties.CurrentAction == CharacterActionState.Sitting) + else if (characterRenderProperties.SitState != SitState.Standing) { if (characterRenderProperties.SitState == SitState.Floor) gfxNum = 6; else if (characterRenderProperties.SitState == SitState.Chair) gfxNum = 5; From 0a96d42746265fc4dd831c222d12616a13390ecb Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Wed, 3 May 2023 21:24:03 -0700 Subject: [PATCH 04/15] Fix startup crash when no pub files are present --- EndlessClient/GameExecution/EndlessGame.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/EndlessClient/GameExecution/EndlessGame.cs b/EndlessClient/GameExecution/EndlessGame.cs index 1f1d647cd..23c5cbdf1 100644 --- a/EndlessClient/GameExecution/EndlessGame.cs +++ b/EndlessClient/GameExecution/EndlessGame.cs @@ -241,36 +241,36 @@ private void AttemptToLoadPubFiles() { _pubFileLoadActions.LoadItemFile(rangedWeaponIds: Constants.RangedWeaponIDs.Concat(Constants.InstrumentIDs)); } - catch (IOException ioe) + catch (Exception ex) when (ex is IOException || ex is ArgumentException) { - _loggerProvider.Logger.Log(PUB_LOG_MSG, string.Format(PubFileNameConstants.EIFFormat, 1), ioe.Message); + _loggerProvider.Logger.Log(PUB_LOG_MSG, string.Format(PubFileNameConstants.EIFFormat, 1), ex.Message); } try { _pubFileLoadActions.LoadNPCFile(); } - catch (IOException ioe) + catch (Exception ex) when (ex is IOException || ex is ArgumentException) { - _loggerProvider.Logger.Log(PUB_LOG_MSG, string.Format(PubFileNameConstants.ENFFormat, 1), ioe.Message); + _loggerProvider.Logger.Log(PUB_LOG_MSG, string.Format(PubFileNameConstants.ENFFormat, 1), ex.Message); } try { _pubFileLoadActions.LoadSpellFile(); } - catch (IOException ioe) + catch (Exception ex) when (ex is IOException || ex is ArgumentException) { - _loggerProvider.Logger.Log(PUB_LOG_MSG, string.Format(PubFileNameConstants.ESFFormat, 1), ioe.Message); + _loggerProvider.Logger.Log(PUB_LOG_MSG, string.Format(PubFileNameConstants.ESFFormat, 1), ex.Message); } try { _pubFileLoadActions.LoadClassFile(); } - catch (IOException ioe) + catch (Exception ex) when (ex is IOException || ex is ArgumentException) { - _loggerProvider.Logger.Log(PUB_LOG_MSG, string.Format(PubFileNameConstants.ECFFormat, 1), ioe.Message); + _loggerProvider.Logger.Log(PUB_LOG_MSG, string.Format(PubFileNameConstants.ECFFormat, 1), ex.Message); } } From 4dd21eaa1a50105944da20988b80b3e10524bc08 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Wed, 3 May 2023 21:51:55 -0700 Subject: [PATCH 05/15] Center trade dialog when beginning trade. Cancel offer agreement when other party changes offer. --- EOLib/PacketHandlers/Trade/TradeOfferUpdateHandler.cs | 3 +++ EndlessClient/Dialogs/TradeDialog.cs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/EOLib/PacketHandlers/Trade/TradeOfferUpdateHandler.cs b/EOLib/PacketHandlers/Trade/TradeOfferUpdateHandler.cs index 43ae0e195..1fbedd2be 100644 --- a/EOLib/PacketHandlers/Trade/TradeOfferUpdateHandler.cs +++ b/EOLib/PacketHandlers/Trade/TradeOfferUpdateHandler.cs @@ -57,6 +57,9 @@ public override bool HandlePacket(IPacket packet) x.PlayerTwoOffer = x.PlayerTwoOffer.WithItems(player1Items); }); + _tradeRepository.PlayerOneOffer = _tradeRepository.PlayerOneOffer.WithAgrees(false); + _tradeRepository.PlayerTwoOffer = _tradeRepository.PlayerTwoOffer.WithAgrees(false); + return true; } } diff --git a/EndlessClient/Dialogs/TradeDialog.cs b/EndlessClient/Dialogs/TradeDialog.cs index 8917884ba..876c65bb6 100644 --- a/EndlessClient/Dialogs/TradeDialog.cs +++ b/EndlessClient/Dialogs/TradeDialog.cs @@ -153,6 +153,11 @@ public TradeDialog(INativeGraphicsManager nativeGraphicsManager, _rightItems = new List(); _leftOffer = new TradeOffer.Builder().ToImmutable(); _rightOffer = new TradeOffer.Builder().ToImmutable(); + + CenterInGameView(); + + if (!Game.Window.AllowUserResizing) + DrawPosition = new Vector2(DrawPosition.X, 15); } public override void Initialize() From fc858f551b1e9cf832bc392a9c8cd29b901b9a0b Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Wed, 3 May 2023 22:40:38 -0700 Subject: [PATCH 06/15] Feature parity with vanilla client: making space for inventory items that aren't displayed causes them to appear --- EndlessClient/HUD/Panels/InventoryPanel.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/EndlessClient/HUD/Panels/InventoryPanel.cs b/EndlessClient/HUD/Panels/InventoryPanel.cs index d95cfea36..a83e58e9a 100644 --- a/EndlessClient/HUD/Panels/InventoryPanel.cs +++ b/EndlessClient/HUD/Panels/InventoryPanel.cs @@ -256,6 +256,11 @@ protected override void OnUpdateControl(GameTime gameTime) } _cachedInventory = _characterInventoryProvider.ItemInventory.ToHashSet(); + + if (removed.Any()) + { + RemoveHiddenItemsFromCachedInventory(); + } } base.OnUpdateControl(gameTime); @@ -498,6 +503,10 @@ private void HandleItemDoneDragging(object sender, DragCompletedEventArgs childItems) _inventoryService.SetSlots(_inventorySlotRepository.FilledSlots, childItem.Slot, childItem.Data.Size); } + private void RemoveHiddenItemsFromCachedInventory() + { + // the item fits in the new slot, and there is no chained drag, snapback, or continued drag + // under these conditions, check if there are any items that don't have a matching childItem and remove them from the cached list + // the next update loop will detect these items as 'added' and attempt to show them in the empty space + + var notDisplayedItems = _cachedInventory.Where(x => _childItems.All(ci => x != ci.InventoryItem)); + _cachedInventory.RemoveWhere(notDisplayedItems.Contains); + } + private static IEnumerable GetOverlappingTakenSlots(int newSlot, ItemSize size, IEnumerable<(int Slot, ItemSize Size)> items) { var slotX = newSlot % InventoryRowSlots; From e47a42509bac86beba6c3aad5588b29beec346e2 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Wed, 3 May 2023 22:46:44 -0700 Subject: [PATCH 07/15] Fix inventory panel item label draw order when inventory panel has layout stored that is > 200 --- EndlessClient/HUD/Inventory/InventoryPanelItem.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/EndlessClient/HUD/Inventory/InventoryPanelItem.cs b/EndlessClient/HUD/Inventory/InventoryPanelItem.cs index c1fc3e814..eb9b75a88 100644 --- a/EndlessClient/HUD/Inventory/InventoryPanelItem.cs +++ b/EndlessClient/HUD/Inventory/InventoryPanelItem.cs @@ -123,6 +123,10 @@ public override void Initialize() _nameLabel.Initialize(); _nameLabel.SetParentControl(_parentContainer); _nameLabel.ResizeBasedOnText(16, 9); + //_nameLabel.DrawOrderChanged += (_, e) => + //{ + // _nameLabel.DrawOrder = _parentContainer.DrawOrder + 200 + //}; base.Initialize(); } @@ -202,7 +206,7 @@ private void UpdateNameLabelPosition() } DrawOrder = _parentContainer.DrawOrder + 2; - _nameLabel.DrawOrder = 200; + _nameLabel.DrawOrder = _parentContainer.DrawOrder + 200; } protected override void OnDraggingFinished(DragCompletedEventArgs args) From ff35d2702be7948c9b558f9ca96445aad73cc383 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Wed, 3 May 2023 22:55:04 -0700 Subject: [PATCH 08/15] Exclusive curse filter now properly filters curses from other players --- EOLib/Domain/Chat/ChatActions.cs | 4 ++-- .../Subscribers/OtherCharacterEventSubscriber.cs | 13 +++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/EOLib/Domain/Chat/ChatActions.cs b/EOLib/Domain/Chat/ChatActions.cs index 6ad3da35f..e2de5c0de 100644 --- a/EOLib/Domain/Chat/ChatActions.cs +++ b/EOLib/Domain/Chat/ChatActions.cs @@ -71,11 +71,11 @@ public ChatActions(IChatRepository chatRepository, } else if (chatType == ChatType.Party && !_partyDataProvider.Members.Any()) { - return (ChatResult.HideAll, String.Empty); + return (ChatResult.HideAll, string.Empty); } else if (chatType == ChatType.Global && _currentMapStateProvider.IsJail) { - return (ChatResult.JailProtection, String.Empty); + return (ChatResult.JailProtection, string.Empty); } chat = _chatProcessor.RemoveFirstCharacterIfNeeded(chat, chatType, targetCharacter); diff --git a/EndlessClient/Subscribers/OtherCharacterEventSubscriber.cs b/EndlessClient/Subscribers/OtherCharacterEventSubscriber.cs index 304ce80d0..86c2df45b 100644 --- a/EndlessClient/Subscribers/OtherCharacterEventSubscriber.cs +++ b/EndlessClient/Subscribers/OtherCharacterEventSubscriber.cs @@ -3,6 +3,8 @@ using EndlessClient.Rendering.Character; using EndlessClient.Services; using EOLib; +using EOLib.Config; +using EOLib.Domain.Chat; using EOLib.Domain.Notifiers; using System; using System.Linq; @@ -15,14 +17,20 @@ public class OtherCharacterEventSubscriber : IOtherCharacterEventNotifier private readonly IChatBubbleActions _chatBubbleActions; private readonly ICharacterRendererProvider _characterRendererProvider; private readonly IFriendIgnoreListService _friendIgnoreListService; + private readonly IConfigurationProvider _configurationProvider; + private readonly IChatProcessor _chatProcessor; public OtherCharacterEventSubscriber(IChatBubbleActions chatBubbleActions, ICharacterRendererProvider characterRendererProvider, - IFriendIgnoreListService friendIgnoreListService) + IFriendIgnoreListService friendIgnoreListService, + IConfigurationProvider configurationProvider, + IChatProcessor chatProcessor) { _chatBubbleActions = chatBubbleActions; _characterRendererProvider = characterRendererProvider; _friendIgnoreListService = friendIgnoreListService; + _configurationProvider = configurationProvider; + _chatProcessor = chatProcessor; } public void OtherCharacterTakeDamage(int characterID, int playerPercentHealth, int damageTaken, bool isHeal) @@ -57,7 +65,8 @@ private void SaySomethingShared(int characterID, string message, bool isGroupCha var name = _characterRendererProvider.CharacterRenderers[characterID].Character.Name; var ignoreList = _friendIgnoreListService.LoadList(Constants.IgnoreListFile); - if (ignoreList.Any(x => x.Equals(name, StringComparison.InvariantCultureIgnoreCase))) + if (ignoreList.Any(x => x.Equals(name, StringComparison.InvariantCultureIgnoreCase)) || + (_configurationProvider.StrictFilterEnabled && !_chatProcessor.FilterCurses(message).ShowChat)) return; _characterRendererProvider.CharacterRenderers[characterID].ShowChatBubble(message, isGroupChat); From 3dcc3ae23a5fa0258250a36d91cfeb65705f144b Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Wed, 3 May 2023 23:12:58 -0700 Subject: [PATCH 09/15] Update character name label position when character sits based on sit state --- EndlessClient/Rendering/Character/CharacterRenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EndlessClient/Rendering/Character/CharacterRenderer.cs b/EndlessClient/Rendering/Character/CharacterRenderer.cs index ff22c8e34..591adbe20 100644 --- a/EndlessClient/Rendering/Character/CharacterRenderer.cs +++ b/EndlessClient/Rendering/Character/CharacterRenderer.cs @@ -342,7 +342,6 @@ private void SetScreenCoordinates(int xPosition, int yPosition) // size of standing still skin texture DrawArea = new Rectangle(xPosition, yPosition, 18, 58); HorizontalCenter = xPosition + 9; - NameLabelY = DrawArea.Y - 12 - (int)(_nameLabel?.ActualHeight ?? 0); _textureUpdateRequired = true; } } @@ -395,6 +394,7 @@ private void UpdateNameLabel() private Vector2 GetNameLabelPosition() { + NameLabelY = DrawArea.Y - 12 - (int)(_nameLabel?.ActualHeight ?? 0) + ((int)Character.RenderProperties.SitState)*10; return new Vector2(HorizontalCenter - (_nameLabel.ActualWidth / 2f), NameLabelY); } From 2d7511f85dab774734007d857c0217a85befeb64 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Wed, 3 May 2023 23:13:11 -0700 Subject: [PATCH 10/15] Update chat bubble render position to be closer to character's head --- EndlessClient/Rendering/Chat/ChatBubble.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EndlessClient/Rendering/Chat/ChatBubble.cs b/EndlessClient/Rendering/Chat/ChatBubble.cs index 23b42c927..3af07edc7 100644 --- a/EndlessClient/Rendering/Chat/ChatBubble.cs +++ b/EndlessClient/Rendering/Chat/ChatBubble.cs @@ -168,7 +168,7 @@ private void SetLabelDrawPosition() { _textLabel.DrawPosition = new Vector2( _parent.HorizontalCenter - _textLabel.ActualWidth / 2.0f, - _parent.NameLabelY - _textLabel.ActualHeight); + _parent.NameLabelY - _textLabel.ActualHeight + 10); } protected override void Dispose(bool disposing) From 885c425fee33b2bb0f19e2b04a63743bbdf46657 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Wed, 3 May 2023 23:26:25 -0700 Subject: [PATCH 11/15] Fix render location of NPC name labels, using NameLabel metadata property for location instead of arbitrary location based on draw area --- EndlessClient/Rendering/NPC/NPCRenderer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EndlessClient/Rendering/NPC/NPCRenderer.cs b/EndlessClient/Rendering/NPC/NPCRenderer.cs index 66e4291ad..cb7f5ba5c 100644 --- a/EndlessClient/Rendering/NPC/NPCRenderer.cs +++ b/EndlessClient/Rendering/NPC/NPCRenderer.cs @@ -261,7 +261,8 @@ private void UpdateDrawAreas() var horizontalOffset = _npcSpriteSheet.GetNPCMetadata(data.Graphic).OffsetX * (NPC.IsFacing(EODirection.Down, EODirection.Left) ? -1 : 1); HorizontalCenter = DrawArea.X + (DrawArea.Width / 2) + horizontalOffset; - NameLabelY = DrawArea.Y - (int)(_nameLabel?.ActualHeight + 4 ?? 0); + var nameLabelGridCoordinates = _gridDrawCoordinateCalculator.CalculateDrawCoordinates(NPC.WithX(NPC.X - 1).WithY(NPC.Y - 1)); + NameLabelY = (int)nameLabelGridCoordinates.Y - metaData.NameLabelOffset; EffectTargetArea = DrawArea.WithSize(DrawArea.Width + horizontalOffset * 2, DrawArea.Height); } From 0a01c60b3ab805fd9f6bee6d8c46e30d718a5ef5 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Thu, 4 May 2023 10:11:36 -0700 Subject: [PATCH 12/15] Prevent sitting while otherwise acting. Use map warp backoff when a refresh packet is received. --- EOLib/PacketHandlers/Refresh/RefreshReplyHandler.cs | 5 +++-- EndlessClient/Controllers/FunctionKeyController.cs | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/EOLib/PacketHandlers/Refresh/RefreshReplyHandler.cs b/EOLib/PacketHandlers/Refresh/RefreshReplyHandler.cs index 2db88b4e3..23f99dbda 100644 --- a/EOLib/PacketHandlers/Refresh/RefreshReplyHandler.cs +++ b/EOLib/PacketHandlers/Refresh/RefreshReplyHandler.cs @@ -52,12 +52,11 @@ public override bool HandlePacket(IPacket packet) .WithMapY(updatedMainCharacter.RenderProperties.MapY); var withoutMainCharacter = data.Characters.Where(x => !IDMatches(x)); - data = data.WithCharacters(withoutMainCharacter.ToList()); _characterRepository.MainCharacter = _characterRepository.MainCharacter .WithRenderProperties(updatedRenderProperties); - _currentMapStateRepository.Characters = data.Characters.ToDictionary(k => k.ID, v => v); + _currentMapStateRepository.Characters = withoutMainCharacter.ToDictionary(k => k.ID, v => v); _currentMapStateRepository.NPCs = new HashSet(data.NPCs); _currentMapStateRepository.MapItems = new HashSet(data.Items); @@ -65,6 +64,8 @@ public override bool HandlePacket(IPacket packet) _currentMapStateRepository.PendingDoors.Clear(); _currentMapStateRepository.VisibleSpikeTraps.Clear(); + _currentMapStateRepository.MapWarpTime = Optional.Option.Some(System.DateTime.Now.AddMilliseconds(-100)); + foreach (var notifier in _mapChangedNotifiers) notifier.NotifyMapChanged(differentMapID: false, warpAnimation: WarpAnimation.None); diff --git a/EndlessClient/Controllers/FunctionKeyController.cs b/EndlessClient/Controllers/FunctionKeyController.cs index 4d2335e18..ca5d339f3 100644 --- a/EndlessClient/Controllers/FunctionKeyController.cs +++ b/EndlessClient/Controllers/FunctionKeyController.cs @@ -93,6 +93,9 @@ public bool SelectSpell(int index, bool isAlternate) public bool Sit() { + if (_characterProvider.MainCharacter.RenderProperties.IsActing(CharacterActionState.Walking, CharacterActionState.Attacking, CharacterActionState.SpellCast)) + return false; + _characterActions.ToggleSit(); return true; } From 323fb253ebbfc8f9d6f039d6100f89f78c010685 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Thu, 4 May 2023 11:26:16 -0700 Subject: [PATCH 13/15] Fix refresh request packet to work with GameServer --- EOLib/Domain/Map/MapActions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EOLib/Domain/Map/MapActions.cs b/EOLib/Domain/Map/MapActions.cs index 157131d5b..ec6e64b0a 100644 --- a/EOLib/Domain/Map/MapActions.cs +++ b/EOLib/Domain/Map/MapActions.cs @@ -30,7 +30,9 @@ public MapActions(IPacketSendService packetSendService, public void RequestRefresh() { - var packet = new PacketBuilder(PacketFamily.Refresh, PacketAction.Request).Build(); + var packet = new PacketBuilder(PacketFamily.Refresh, PacketAction.Request) + .AddByte(255) + .Build(); _packetSendService.SendPacket(packet); } From 239c61ab4796b5ecafd7dd316f55c62a7c8086c1 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Thu, 4 May 2023 11:29:44 -0700 Subject: [PATCH 14/15] In fixed window size mode, remove NPCs/characters that are out of range. Fixes display bugs with GameServer/resoserv. --- .../HUD/Controls/HudControlsFactory.cs | 2 +- .../Network/UnknownEntitiesRequester.cs | 74 ++++++++++++++++++- .../Rendering/NPC/NPCRendererUpdater.cs | 20 +++++ 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/EndlessClient/HUD/Controls/HudControlsFactory.cs b/EndlessClient/HUD/Controls/HudControlsFactory.cs index ee480b465..52548059e 100644 --- a/EndlessClient/HUD/Controls/HudControlsFactory.cs +++ b/EndlessClient/HUD/Controls/HudControlsFactory.cs @@ -533,7 +533,7 @@ private PeriodicStatUpdaterComponent CreatePeriodicStatUpdater() private UnknownEntitiesRequester CreateUnknownEntitiesRequester() { - return new UnknownEntitiesRequester(_endlessGameProvider, _currentMapStateRepository, _packetSendService); + return new UnknownEntitiesRequester(_endlessGameProvider, _clientWindowSizeRepository, (ICharacterProvider)_characterRepository, _currentMapStateRepository, _packetSendService); } private StatusBarLabel CreateStatusLabel() diff --git a/EndlessClient/Network/UnknownEntitiesRequester.cs b/EndlessClient/Network/UnknownEntitiesRequester.cs index 76d9edb8a..c2a2f02c3 100644 --- a/EndlessClient/Network/UnknownEntitiesRequester.cs +++ b/EndlessClient/Network/UnknownEntitiesRequester.cs @@ -1,25 +1,38 @@ using EndlessClient.GameExecution; +using EndlessClient.Rendering; +using EOLib.Domain.Character; using EOLib.Domain.Map; +using EOLib.Domain.NPC; using EOLib.Net; using EOLib.Net.Communication; using Microsoft.Xna.Framework; using System; +using System.Collections.Generic; namespace EndlessClient.Network { public class UnknownEntitiesRequester : GameComponent { + private const double REQUEST_INTERVAL_SECONDS = 1.0; + + private readonly IClientWindowSizeProvider _clientWindowSizeProvider; + private readonly ICharacterProvider _characterProvider; private readonly ICurrentMapStateRepository _currentMapStateRepository; private readonly IPacketSendService _packetSendService; + private DateTime _lastRequestTime; - private const double REQUEST_INTERVAL_SECONDS = 1; + // todo: create actions in EOLib.Domain for requesting unknown entities, instead of using packetsendservice directly public UnknownEntitiesRequester(IEndlessGameProvider gameProvider, - ICurrentMapStateRepository currentMapStateRepository, - IPacketSendService packetSendService) + IClientWindowSizeProvider clientWindowSizeProvider, + ICharacterProvider characterProvider, + ICurrentMapStateRepository currentMapStateRepository, + IPacketSendService packetSendService) : base((Game) gameProvider.Game) { + _clientWindowSizeProvider = clientWindowSizeProvider; + _characterProvider = characterProvider; _currentMapStateRepository = currentMapStateRepository; _packetSendService = packetSendService; _lastRequestTime = DateTime.Now; @@ -58,6 +71,8 @@ public override void Update(GameTime gameTime) { _lastRequestTime = DateTime.Now; } + + ClearOutOfRangeActors(); } base.Update(gameTime); @@ -101,5 +116,58 @@ private IPacket CreateRequestForPlayers() } return builder.Build(); } + + private void ClearOutOfRangeActors() + { + // todo: the server should communicate the "seedistance" to clients + // for now, disable auto remove of entities in Resizable mode + if (_clientWindowSizeProvider.Resizable) + { + return; + } + + var mc = _characterProvider.MainCharacter; + + var idsToRemove = new List(); + foreach (var id in _currentMapStateRepository.Characters.Keys) + { + var c = _currentMapStateRepository.Characters[id]; + + var xDiff = Math.Abs(mc.X - c.X); + var yDiff = Math.Abs(mc.Y - c.Y); + + if (c.X < mc.X || c.Y < mc.Y) + { + if (xDiff + yDiff > 11) + idsToRemove.Add(id); + } + else if (xDiff + yDiff > 14) + { + idsToRemove.Add(id); + } + } + + foreach (var id in idsToRemove) + _currentMapStateRepository.Characters.Remove(id); + + var npcsToRemove = new List(); + foreach (var npc in _currentMapStateRepository.NPCs) + { + var xDiff = Math.Abs(mc.X - npc.X); + var yDiff = Math.Abs(mc.Y - npc.Y); + + if (npc.X < mc.X || npc.Y < mc.Y) + { + if (xDiff + yDiff > 11) + npcsToRemove.Add(npc); + } + else if (xDiff + yDiff > 14) + { + npcsToRemove.Add(npc); + } + } + + _currentMapStateRepository.NPCs.RemoveWhere(npcsToRemove.Contains); + } } } diff --git a/EndlessClient/Rendering/NPC/NPCRendererUpdater.cs b/EndlessClient/Rendering/NPC/NPCRendererUpdater.cs index fadd24275..ead7a9d82 100644 --- a/EndlessClient/Rendering/NPC/NPCRendererUpdater.cs +++ b/EndlessClient/Rendering/NPC/NPCRendererUpdater.cs @@ -29,6 +29,7 @@ public NPCRendererUpdater(ICurrentMapStateProvider currentMapStateProvider, public void UpdateNPCs(GameTime gameTime) { CleanUpDeadNPCs(); + CleanUpRemovedNPCs(); CreateAndCacheNPCRenderers(); UpdateNPCRenderers(gameTime); } @@ -47,6 +48,25 @@ private void CleanUpDeadNPCs() } } + private void CleanUpRemovedNPCs() + { + var removedNPCs = _npcRendererRepository.NPCRenderers.Values + .Where(x => x.IsAlive) + .Select(x => x.NPC.Index) + .Where(x => !_currentMapStateProvider.NPCs.Select(y => y.Index).Any(y => y == x)) + .ToList(); + + foreach (var index in removedNPCs) + { + if (!_npcRendererRepository.NPCRenderers.TryGetValue(index, out var renderer)) + continue; + + renderer.Dispose(); + _npcRendererRepository.NPCRenderers.Remove(index); + _npcStateCache.RemoveStateByIndex(index); + } + } + private void CreateAndCacheNPCRenderers() { foreach (var npc in _currentMapStateProvider.NPCs) From c339317e325c7351e9831ebdab1dd3095d797623 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Thu, 4 May 2023 12:43:58 -0700 Subject: [PATCH 15/15] Update NPC_PLAYER packet handler to work with game server using 'chunked' approach to packet parsing --- EOLib/PacketHandlers/NPC/NPCPlayerHandler.cs | 214 +++++++++++-------- 1 file changed, 123 insertions(+), 91 deletions(-) diff --git a/EOLib/PacketHandlers/NPC/NPCPlayerHandler.cs b/EOLib/PacketHandlers/NPC/NPCPlayerHandler.cs index f6872e1af..2e5cd5159 100644 --- a/EOLib/PacketHandlers/NPC/NPCPlayerHandler.cs +++ b/EOLib/PacketHandlers/NPC/NPCPlayerHandler.cs @@ -5,12 +5,15 @@ using EOLib.Domain.Login; using EOLib.Domain.Map; using EOLib.Domain.Notifiers; +using EOLib.Domain.NPC; using EOLib.IO.Repositories; using EOLib.Net; using EOLib.Net.Handlers; using Optional; +using Optional.Collections; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using DomainNPC = EOLib.Domain.NPC.NPC; @@ -23,10 +26,6 @@ namespace EOLib.PacketHandlers.NPC [AutoMappedType] public class NPCPlayerHandler : InGameOnlyPacketHandler { - private const int NPC_WALK_ACTION = 0; - private const int NPC_ATTK_ACTION = 1; - private const int NPC_TALK_ACTION = 2; - private readonly ICharacterRepository _characterRepository; private readonly IChatRepository _chatRepository; private readonly IENFFileProvider _enfFileProvider; @@ -60,116 +59,149 @@ public NPCPlayerHandler(IPlayerInfoProvider playerInfoProvider, public override bool HandlePacket(IPacket packet) { - var num255s = 0; - while (packet.PeekByte() == byte.MaxValue) + var chunks = new List(); + while (packet.ReadPosition < packet.Length) { - num255s++; - packet.ReadByte(); - } + var data = packet.RawData.Skip(packet.ReadPosition).TakeWhile(x => x != 255).ToArray(); + packet.Seek(data.Length, SeekOrigin.Current); + if (packet.ReadPosition < packet.Length) + packet.ReadByte(); - var index = packet.ReadChar(); - DomainNPC npc; - try - { - npc = _currentMapStateRepository.NPCs.Single(n => n.Index == index); - } - catch (InvalidOperationException) - { - _currentMapStateRepository.UnknownNPCIndexes.Add(index); - return true; + chunks.Add(new PacketBuilder(packet.Family, packet.Action).AddBytes(data).Build()); } - var updatedNpc = Option.None(); - switch (num255s) - { - case NPC_WALK_ACTION: HandleNPCWalk(packet, npc); break; - case NPC_ATTK_ACTION: updatedNpc = Option.Some(HandleNPCAttack(packet, npc)); break; - case NPC_TALK_ACTION: HandleNPCTalk(packet, npc); break; - default: throw new MalformedPacketException("Unknown NPC action " + num255s + " specified in packet from server!", packet); - } + if (chunks.Count < 3 || chunks.Count > 4) + throw new MalformedPacketException($"Expected 3 or 4 chunks in NPC_PLAYER packet, got {chunks.Count}", packet); - updatedNpc.MatchSome(n => + HandleNPCWalk(chunks[0]); + HandleNPCAttack(chunks[1]); + HandleNPCTalk(chunks[2]); + + if (chunks.Count > 3) { - _currentMapStateRepository.NPCs.Remove(npc); - _currentMapStateRepository.NPCs.Add(n); - }); + var hp = chunks[3].ReadShort(); + var tp = chunks[3].ReadShort(); + + var stats = _characterRepository.MainCharacter.Stats + .WithNewStat(CharacterStat.HP, hp) + .WithNewStat(CharacterStat.TP, tp); + _characterRepository.MainCharacter = _characterRepository.MainCharacter.WithStats(stats); + } return true; } - private void HandleNPCWalk(IPacket packet, DomainNPC npc) + private void HandleNPCWalk(IPacket packet) { - //npc remove from view sets x/y to either 0,0 or 252,252 based on target coords - var x = packet.ReadChar(); - var y = packet.ReadChar(); - var npcDirection = (EODirection)packet.ReadChar(); - if (packet.ReadBytes(3).Any(b => b != 255)) - throw new MalformedPacketException("Expected 3 bytes of value 0xFF in NPC_PLAYER packet for Walk action", packet); - - var updatedNPC = npc.WithDirection(npcDirection); - updatedNPC = EnsureCorrectXAndY(updatedNPC, x, y); - - _currentMapStateRepository.NPCs.Remove(npc); - _currentMapStateRepository.NPCs.Add(updatedNPC); - - foreach (var notifier in _npcAnimationNotifiers) - notifier.StartNPCWalkAnimation(npc.Index); + while (packet.ReadPosition < packet.Length) + { + var index = packet.ReadChar(); + var x = packet.ReadChar(); + var y = packet.ReadChar(); + var npcDirection = (EODirection)packet.ReadChar(); + + var npc = GetNPC(index); + npc.Match( + some: n => + { + var updated = n.WithDirection(npcDirection); + updated = EnsureCorrectXAndY(updated, x, y); + ReplaceNPC(n, updated); + + foreach (var notifier in _npcAnimationNotifiers) + notifier.StartNPCWalkAnimation(n.Index); + }, + none: () => _currentMapStateRepository.UnknownNPCIndexes.Add(index)); + } } - private DomainNPC HandleNPCAttack(IPacket packet, DomainNPC npc) + private void HandleNPCAttack(IPacket packet) { - var isDead = packet.ReadChar() == 2; //2 if target player is dead, 1 if alive - var npcDirection = (EODirection)packet.ReadChar(); - var characterID = packet.ReadShort(); - var damageTaken = packet.ReadThree(); - var playerPercentHealth = packet.ReadThree(); - if (packet.ReadBytes(2).Any(b => b != 255)) - throw new MalformedPacketException("Expected 2 bytes of value 0xFF in NPC_PLAYER packet for Attack action", packet); - - if (characterID == _characterRepository.MainCharacter.ID) - { - var characterToUpdate = _characterRepository.MainCharacter; - - var stats = characterToUpdate.Stats; - stats = stats.WithNewStat(CharacterStat.HP, Math.Max(stats[CharacterStat.HP] - damageTaken, 0)); + // note: eoserv incorrectly sends playerPercentHealth as a three byte number. GameServer sends a single char. + const int DATA_LENGTH = 9; - var props = characterToUpdate.RenderProperties.WithIsDead(isDead); - _characterRepository.MainCharacter = characterToUpdate.WithStats(stats).WithRenderProperties(props); - - foreach (var notifier in _mainCharacterNotifiers) - notifier.NotifyTakeDamage(damageTaken, playerPercentHealth, isHeal: false); - } - else if (_currentMapStateRepository.Characters.ContainsKey(characterID)) + while (packet.ReadPosition + DATA_LENGTH < packet.Length) { - var updatedCharacter = _currentMapStateRepository.Characters[characterID].WithDamage(damageTaken, isDead); - _currentMapStateRepository.Characters[characterID] = updatedCharacter; - - foreach (var notifier in _otherCharacterNotifiers) - notifier.OtherCharacterTakeDamage(characterID, playerPercentHealth, damageTaken, isHeal: false); + var index = packet.ReadChar(); + var isDead = packet.ReadChar() == 2; // 2 if target player is dead, 1 if alive + var npcDirection = (EODirection)packet.ReadChar(); + var characterID = packet.ReadShort(); + var damageTaken = packet.ReadThree(); + var playerPercentHealth = packet.ReadChar(); + + if (characterID == _characterRepository.MainCharacter.ID) + { + var characterToUpdate = _characterRepository.MainCharacter; + + var stats = characterToUpdate.Stats; + stats = stats.WithNewStat(CharacterStat.HP, Math.Max(stats[CharacterStat.HP] - damageTaken, 0)); + + var props = characterToUpdate.RenderProperties.WithIsDead(isDead); + _characterRepository.MainCharacter = characterToUpdate.WithStats(stats).WithRenderProperties(props); + + foreach (var notifier in _mainCharacterNotifiers) + notifier.NotifyTakeDamage(damageTaken, playerPercentHealth, isHeal: false); + } + else if (_currentMapStateRepository.Characters.ContainsKey(characterID)) + { + var updatedCharacter = _currentMapStateRepository.Characters[characterID].WithDamage(damageTaken, isDead); + _currentMapStateRepository.Characters[characterID] = updatedCharacter; + + foreach (var notifier in _otherCharacterNotifiers) + notifier.OtherCharacterTakeDamage(characterID, playerPercentHealth, damageTaken, isHeal: false); + } + else + { + _currentMapStateRepository.UnknownPlayerIDs.Add(characterID); + } + + var npc = GetNPC(index); + npc.Match( + some: n => + { + var updated = n.WithDirection(npcDirection); + ReplaceNPC(n, updated); + + foreach (var notifier in _npcAnimationNotifiers) + notifier.StartNPCAttackAnimation(index); + }, + none: () => _currentMapStateRepository.UnknownNPCIndexes.Add(index)); } - else + } + + private void HandleNPCTalk(IPacket packet) + { + while (packet.ReadPosition < packet.Length) { - _currentMapStateRepository.UnknownPlayerIDs.Add(characterID); + var index = packet.ReadChar(); + var messageLength = packet.ReadChar(); + var message = packet.ReadString(messageLength); + + var npc = GetNPC(index); + npc.Match( + some: n => + { + var npcData = _enfFileProvider.ENFFile[n.ID]; + + var chatData = new ChatData(ChatTab.Local, npcData.Name, message, ChatIcon.Note); + _chatRepository.AllChat[ChatTab.Local].Add(chatData); + + foreach (var notifier in _npcAnimationNotifiers) + notifier.ShowNPCSpeechBubble(index, message); + }, + none: () => _currentMapStateRepository.UnknownNPCIndexes.Add(index)); } - - foreach (var notifier in _npcAnimationNotifiers) - notifier.StartNPCAttackAnimation(npc.Index); - - return npc.WithDirection(npcDirection); } - private void HandleNPCTalk(IPacket packet, DomainNPC npc) + private Option GetNPC(int index) { - var messageLength = packet.ReadChar(); - var message = packet.ReadString(messageLength); - - var npcData = _enfFileProvider.ENFFile[npc.ID]; - - var chatData = new ChatData(ChatTab.Local, npcData.Name, message, ChatIcon.Note); - _chatRepository.AllChat[ChatTab.Local].Add(chatData); + return _currentMapStateRepository.NPCs.SingleOrNone(n => n.Index == index); + } - foreach (var notifier in _npcAnimationNotifiers) - notifier.ShowNPCSpeechBubble(npc.Index, message); + private void ReplaceNPC(DomainNPC npc, DomainNPC updatedNPC) + { + _currentMapStateRepository.NPCs.Remove(npc); + _currentMapStateRepository.NPCs.Add(updatedNPC); } private static DomainNPC EnsureCorrectXAndY(DomainNPC npc, int destinationX, int destinationY)