From b77e5ce02a9a75f1ec6f78accc2a04804eedf524 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 29 Oct 2024 11:28:25 -0700 Subject: [PATCH 1/9] Render roof layer tiles one row later (except for last row) Fixes rendering overlap error where walls are unintentionally drawn over roofs without rearchitecting to use a depth buffer --- .../MapEntityRenderers/RoofLayerRenderer.cs | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/EndlessClient/Rendering/MapEntityRenderers/RoofLayerRenderer.cs b/EndlessClient/Rendering/MapEntityRenderers/RoofLayerRenderer.cs index 12c7f3398..03c6718bc 100644 --- a/EndlessClient/Rendering/MapEntityRenderers/RoofLayerRenderer.cs +++ b/EndlessClient/Rendering/MapEntityRenderers/RoofLayerRenderer.cs @@ -1,4 +1,5 @@ -using EndlessClient.Rendering.Map; +using System.Collections.Generic; +using EndlessClient.Rendering.Map; using EOLib.Domain.Character; using EOLib.Domain.Map; using EOLib.Graphics; @@ -30,16 +31,34 @@ public RoofLayerRenderer(INativeGraphicsManager nativeGraphicsManager, protected override bool ElementExistsAt(int row, int col) { - return CurrentMap.GFX[MapLayer.Roof][row, col] > 0; + if (row == CurrentMap.Properties.Height) + { + return CurrentMap.GFX[MapLayer.Roof][row, col] > 0 || + CurrentMap.GFX[MapLayer.Roof][row - 1, col] > 0; + } + + return row - 1 >= 0 && CurrentMap.GFX[MapLayer.Roof][row - 1, col] > 0; } public override void RenderElementAt(SpriteBatch spriteBatch, int row, int col, int alpha, Vector2 additionalOffset = default) { - int gfxNum = CurrentMap.GFX[MapLayer.Roof][row, col]; - var gfx = _nativeGraphicsManager.TextureFromResource(GFXTypes.MapWallTop, gfxNum, true); + var gfxCandidates = new List(); + if (CurrentMap.GFX[MapLayer.Roof][row - 1, col] > 0) + gfxCandidates.Add(CurrentMap.GFX[MapLayer.Roof][row - 1, col]); + if (row == CurrentMap.Properties.Height) + { + if (CurrentMap.GFX[MapLayer.Roof][row, col] > 0) + gfxCandidates.Add(CurrentMap.GFX[MapLayer.Roof][row, col]); + } + + //int gfxNum = CurrentMap.GFX[MapLayer.Roof][row-1, col]; + foreach (var gfxNum in gfxCandidates) + { + var gfx = _nativeGraphicsManager.TextureFromResource(GFXTypes.MapWallTop, gfxNum, true); - var pos = GetDrawCoordinatesFromGridUnits(col, row); - spriteBatch.Draw(gfx, pos + additionalOffset, Color.FromNonPremultiplied(255, 255, 255, alpha)); + var pos = GetDrawCoordinatesFromGridUnits(col, row - 1); + spriteBatch.Draw(gfx, pos + additionalOffset, Color.FromNonPremultiplied(255, 255, 255, alpha)); + } } private IMapFile CurrentMap => _currentMapProvider.CurrentMap; From 06aeb9b7664ab18ccf54925be3c42e804f8cc711 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 29 Oct 2024 11:30:55 -0700 Subject: [PATCH 2/9] Render map items as part of normal map entity renderers Fixes display bug where items would be rendered beneath all other gfx, including those on previous rows (e.g. gold pile rendered below locker on prior row) --- .../Rendering/Map/MapEntityRendererProvider.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/EndlessClient/Rendering/Map/MapEntityRendererProvider.cs b/EndlessClient/Rendering/Map/MapEntityRendererProvider.cs index 356dbed37..2958176c6 100644 --- a/EndlessClient/Rendering/Map/MapEntityRendererProvider.cs +++ b/EndlessClient/Rendering/Map/MapEntityRendererProvider.cs @@ -43,16 +43,16 @@ public MapEntityRendererProvider(INativeGraphicsManager nativeGraphicsManager, currentMapProvider, characterProvider, gridDrawCoordinateCalculator, - clientWindowSizeProvider), - new MapItemLayerRenderer(characterProvider, - gridDrawCoordinateCalculator, - clientWindowSizeProvider, - currentMapStateProvider, - mapItemGraphicProvider) + clientWindowSizeProvider) }; MapEntityRenderers = new List { + new MapItemLayerRenderer(characterProvider, + gridDrawCoordinateCalculator, + clientWindowSizeProvider, + currentMapStateProvider, + mapItemGraphicProvider), new ShadowLayerRenderer(nativeGraphicsManager, currentMapProvider, characterProvider, From 3dc4dc75fa9c8ee888c9762792a67f0820047a2e Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 29 Oct 2024 13:24:34 -0700 Subject: [PATCH 3/9] Render Overlay layer last Fixes bug with display of character sitting on chairs where character would be drawn over the back of the chair --- .../Rendering/MapEntityRenderers/BaseMapEntityRenderer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EndlessClient/Rendering/MapEntityRenderers/BaseMapEntityRenderer.cs b/EndlessClient/Rendering/MapEntityRenderers/BaseMapEntityRenderer.cs index 9ff23015a..ae2492e83 100644 --- a/EndlessClient/Rendering/MapEntityRenderers/BaseMapEntityRenderer.cs +++ b/EndlessClient/Rendering/MapEntityRenderers/BaseMapEntityRenderer.cs @@ -38,7 +38,9 @@ static BaseMapEntityRenderer() public abstract MapRenderLayer RenderLayer { get; } - public bool ShouldRenderLast => RenderLayer == MapRenderLayer.Overlay2 || RenderLayer == MapRenderLayer.MainCharacterTransparent; + public bool ShouldRenderLast => RenderLayer == MapRenderLayer.Overlay || + RenderLayer == MapRenderLayer.Overlay2 || + RenderLayer == MapRenderLayer.MainCharacterTransparent; protected abstract int RenderDistance { get; } From 04958a1d49158c2e85b2d84bb032ae5375e55a67 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 29 Oct 2024 21:55:18 -0700 Subject: [PATCH 4/9] BU Support: handle rebirth Make sure character's experience is updated even when the expDifference is <= 0 --- EOLib/PacketHandlers/NPC/NPCDeathHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EOLib/PacketHandlers/NPC/NPCDeathHandler.cs b/EOLib/PacketHandlers/NPC/NPCDeathHandler.cs index 4dc648b9c..3b1a6f01a 100644 --- a/EOLib/PacketHandlers/NPC/NPCDeathHandler.cs +++ b/EOLib/PacketHandlers/NPC/NPCDeathHandler.cs @@ -121,13 +121,13 @@ protected void UpdatePlayerExperience(int experienceValue) foreach (var notifier in _mainCharacterEventNotifiers) notifier.NotifyGainedExp(expDifference); - UpdateCharacterStat(CharacterStat.Experience, experienceValue); - _characterSessionRepository.LastKillExp = expDifference; if (expDifference > _characterSessionRepository.BestKillExp) _characterSessionRepository.BestKillExp = expDifference; _characterSessionRepository.TodayTotalExp += Convert.ToUInt64(Math.Max(expDifference, 0)); } + + UpdateCharacterStat(CharacterStat.Experience, experienceValue); } protected void ApplyStats(LevelUpStats levelUpStats) From 116c9ac01121f9e7eddf8077edd7b69b4e96a0ad Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 29 Oct 2024 22:27:38 -0700 Subject: [PATCH 5/9] BU Support: Wrap links in QuestDialog text. Fixes display bug where long responses would be rendered outside the bounds of the dialog frame --- EndlessClient/Dialogs/QuestDialog.cs | 47 ++++++---------------------- 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/EndlessClient/Dialogs/QuestDialog.cs b/EndlessClient/Dialogs/QuestDialog.cs index 99220dd07..3794a9715 100644 --- a/EndlessClient/Dialogs/QuestDialog.cs +++ b/EndlessClient/Dialogs/QuestDialog.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using EndlessClient.Content; using EndlessClient.Dialogs.Services; @@ -170,49 +169,23 @@ private void UpdateDialogDisplayText(QuestDialogData repoData) { case State.TalkToNpc: { - 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); - } + AddTextAsListItems(_contentProvider.Fonts[Constants.FontSize08pt5], insertLineBreaks: true, [], repoData.PageText[_pageIndex].Replace("\n", string.Empty)); // 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 linkClickActions = repoData.Actions.Select(action => { - var actionItem = new ListDialogItem(this, ListDialogItem.ListItemStyle.Small, index++) - { - PrimaryText = action.DisplayText - }; - var linkIndex = action.ActionID; - actionItem.SetPrimaryClickAction((_, _) => - { - _questActions.RespondToQuestDialog(DialogReply.Link, linkIndex); - ClickSoundEffect?.Invoke(this, EventArgs.Empty); - Close(XNADialogResult.Cancel); - }); - - AddItemToList(actionItem, sortList: false); - } + return new Action(() => + { + _questActions.RespondToQuestDialog(DialogReply.Link, linkIndex); + ClickSoundEffect?.Invoke(this, EventArgs.Empty); + Close(XNADialogResult.Cancel); + }); + }).ToList(); + AddTextAsListItems(_contentProvider.Fonts[Constants.FontSize08pt5], insertLineBreaks: false, linkClickActions, repoData.Actions.Select(x => $"*{x.DisplayText}").ToArray()); } break; case State.SwitchQuest: From 86625ef3a706d1793001c7a73470a5a67f3e1c22 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 29 Oct 2024 22:28:19 -0700 Subject: [PATCH 6/9] BU Support: avoid crash when NPC mouseover pixel is out of texture bounds --- EndlessClient/Rendering/NPC/NPCRenderer.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/EndlessClient/Rendering/NPC/NPCRenderer.cs b/EndlessClient/Rendering/NPC/NPCRenderer.cs index 5fe3b492a..fc8d4fd4d 100644 --- a/EndlessClient/Rendering/NPC/NPCRenderer.cs +++ b/EndlessClient/Rendering/NPC/NPCRenderer.cs @@ -168,9 +168,8 @@ public bool IsClickablePixel(Point currentMousePosition) var currentFrame = _npcSpriteSheet.GetNPCTexture(_enfFileProvider.ENFFile[NPC.ID].Graphic, NPC.Frame, NPC.Direction); var adjustedPos = currentMousePosition - DrawArea.Location; - var pixel = cachedTexture[adjustedPos.Y * currentFrame.Width + adjustedPos.X]; - - return pixel.A > 0; + var index = adjustedPos.Y * currentFrame.Width + adjustedPos.X; + return index < cachedTexture.Length && cachedTexture[index].A > 0; } return true; From 20b7dfb9d1812cc54353ebf552ba05c0778c0fe5 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Wed, 30 Oct 2024 10:29:53 -0700 Subject: [PATCH 7/9] Fix fast NPCs sometimes jumping around NPC updated direction/position is only applied in NPCAnimator. This fixes a race where overlapping (replayable) animations would still update the coordinates of the domain model prior to executing the animation. Now, animations that are queued while another animation is in progress will only update the domain model once the first animation has completed. --- .../Extensions/MapCoordinateExtensions.cs | 10 + EOLib/Domain/Notifiers/INPCActionNotifier.cs | 8 +- EOLib/PacketHandlers/NPC/NPCPlayerHandler.cs | 86 +++----- EndlessClient/Rendering/NPC/NPCActions.cs | 9 +- EndlessClient/Rendering/NPC/NPCAnimator.cs | 193 +++++++++++++----- 5 files changed, 188 insertions(+), 118 deletions(-) create mode 100644 EOLib/Domain/Extensions/MapCoordinateExtensions.cs diff --git a/EOLib/Domain/Extensions/MapCoordinateExtensions.cs b/EOLib/Domain/Extensions/MapCoordinateExtensions.cs new file mode 100644 index 000000000..bb6e30948 --- /dev/null +++ b/EOLib/Domain/Extensions/MapCoordinateExtensions.cs @@ -0,0 +1,10 @@ +using EOLib.Domain.Map; +using Moffat.EndlessOnline.SDK.Protocol; + +namespace EOLib.Domain.Extensions +{ + public static class MapCoordinateExtensions + { + public static MapCoordinate ToCoordinate(this Coords coords) => new MapCoordinate(coords.X, coords.Y); + } +} diff --git a/EOLib/Domain/Notifiers/INPCActionNotifier.cs b/EOLib/Domain/Notifiers/INPCActionNotifier.cs index 63df65f7c..1f5375e0c 100644 --- a/EOLib/Domain/Notifiers/INPCActionNotifier.cs +++ b/EOLib/Domain/Notifiers/INPCActionNotifier.cs @@ -6,9 +6,9 @@ namespace EOLib.Domain.Notifiers { public interface INPCActionNotifier { - void StartNPCWalkAnimation(int npcIndex); + void StartNPCWalkAnimation(int npcIndex, MapCoordinate coords, EODirection direction); - void StartNPCAttackAnimation(int npcIndex); + void StartNPCAttackAnimation(int npcIndex, EODirection direction); void RemoveNPCFromView(int npcIndex, int playerId, Option spellId, Option damage, bool showDeathAnimation); @@ -22,9 +22,9 @@ public interface INPCActionNotifier [AutoMappedType] public class NoOpNPCActionNotifier : INPCActionNotifier { - public void StartNPCWalkAnimation(int npcIndex) { } + public void StartNPCWalkAnimation(int npcIndex, MapCoordinate coords, EODirection direction) { } - public void StartNPCAttackAnimation(int npcIndex) { } + public void StartNPCAttackAnimation(int npcIndex, EODirection direction) { } public void RemoveNPCFromView(int npcIndex, int playerId, Option spellId, Option damage, bool showDeathAnimation) { } diff --git a/EOLib/PacketHandlers/NPC/NPCPlayerHandler.cs b/EOLib/PacketHandlers/NPC/NPCPlayerHandler.cs index 89fffbf05..aa1e9b1df 100644 --- a/EOLib/PacketHandlers/NPC/NPCPlayerHandler.cs +++ b/EOLib/PacketHandlers/NPC/NPCPlayerHandler.cs @@ -11,9 +11,6 @@ using EOLib.Net.Handlers; using Moffat.EndlessOnline.SDK.Protocol.Net; using Moffat.EndlessOnline.SDK.Protocol.Net.Server; -using Optional; -using Optional.Collections; -using DomainNPC = EOLib.Domain.NPC.NPC; namespace EOLib.PacketHandlers.NPC { @@ -78,18 +75,15 @@ private void HandleNPCWalk(IReadOnlyList positions) { foreach (var position in positions) { - var npc = GetNPC(position.NpcIndex); - npc.Match( - some: n => - { - var updated = n.WithDirection((EODirection)position.Direction); - updated = EnsureCorrectXAndY(updated, position.Coords.X, position.Coords.Y); - _currentMapStateRepository.NPCs.Update(n, updated); - - foreach (var notifier in _npcAnimationNotifiers) - notifier.StartNPCWalkAnimation(n.Index); - }, - none: () => _currentMapStateRepository.UnknownNPCIndexes.Add(position.NpcIndex)); + if (_currentMapStateRepository.NPCs.TryGetValue(position.NpcIndex, out var npc)) + { + foreach (var notifier in _npcAnimationNotifiers) + notifier.StartNPCWalkAnimation(npc.Index, position.Coords.ToCoordinate(), (EODirection)position.Direction); + } + else + { + _currentMapStateRepository.UnknownNPCIndexes.Add(position.NpcIndex); + } } } @@ -130,17 +124,15 @@ private void HandleNPCAttack(IReadOnlyList attacks) _currentMapStateRepository.UnknownPlayerIDs.Add(characterID); } - var npc = GetNPC(index); - npc.Match( - some: n => - { - var updated = n.WithDirection(npcDirection); - _currentMapStateRepository.NPCs.Update(n, updated); - - foreach (var notifier in _npcAnimationNotifiers) - notifier.StartNPCAttackAnimation(index); - }, - none: () => _currentMapStateRepository.UnknownNPCIndexes.Add(index)); + if (_currentMapStateRepository.NPCs.TryGetValue(index, out var npc)) + { + foreach (var notifier in _npcAnimationNotifiers) + notifier.StartNPCAttackAnimation(index, (EODirection)attack.Direction); + } + else + { + _currentMapStateRepository.UnknownNPCIndexes.Add(index); + } } } @@ -148,37 +140,21 @@ private void HandleNPCTalk(IReadOnlyList chats) { foreach (var chat in chats) { - var npc = GetNPC(chat.NpcIndex); - npc.Match( - some: n => - { - var npcData = _enfFileProvider.ENFFile[n.ID]; - - var chatData = new ChatData(ChatTab.Local, npcData.Name, chat.Message, ChatIcon.Note, filter: false); - _chatRepository.AllChat[ChatTab.Local].Add(chatData); - - foreach (var notifier in _npcAnimationNotifiers) - notifier.ShowNPCSpeechBubble(chat.NpcIndex, chat.Message); - }, - none: () => _currentMapStateRepository.UnknownNPCIndexes.Add(chat.NpcIndex)); - } - } + if (_currentMapStateRepository.NPCs.TryGetValue(chat.NpcIndex, out var npc)) + { + var npcData = _enfFileProvider.ENFFile[npc.ID]; - private Option GetNPC(int index) - { - return _currentMapStateRepository.NPCs.SingleOrNone(n => n.Index == index); - } + var chatData = new ChatData(ChatTab.Local, npcData.Name, chat.Message, ChatIcon.Note, filter: false); + _chatRepository.AllChat[ChatTab.Local].Add(chatData); - private static DomainNPC EnsureCorrectXAndY(DomainNPC npc, int destinationX, int destinationY) - { - var opposite = npc.Direction.Opposite(); - var tempNPC = npc - .WithDirection(opposite) - .WithX(destinationX) - .WithY(destinationY); - return npc - .WithX(tempNPC.GetDestinationX()) - .WithY(tempNPC.GetDestinationY()); + foreach (var notifier in _npcAnimationNotifiers) + notifier.ShowNPCSpeechBubble(chat.NpcIndex, chat.Message); + } + else + { + _currentMapStateRepository.UnknownNPCIndexes.Add(chat.NpcIndex); + } + } } } } diff --git a/EndlessClient/Rendering/NPC/NPCActions.cs b/EndlessClient/Rendering/NPC/NPCActions.cs index 02fe143b4..e4c363374 100644 --- a/EndlessClient/Rendering/NPC/NPCActions.cs +++ b/EndlessClient/Rendering/NPC/NPCActions.cs @@ -4,6 +4,7 @@ using EndlessClient.HUD.Chat; using EndlessClient.HUD.Controls; using EndlessClient.Rendering.Character; +using EOLib; using EOLib.Domain.Chat; using EOLib.Domain.Map; using EOLib.Domain.Notifiers; @@ -50,20 +51,20 @@ public NPCActions(IHudControlProvider hudControlProvider, _sfxPlayer = sfxPlayer; } - public void StartNPCWalkAnimation(int npcIndex) + public void StartNPCWalkAnimation(int npcIndex, MapCoordinate coords, EODirection direction) { if (!_hudControlProvider.IsInGame) return; - Animator.StartWalkAnimation(npcIndex); + Animator.StartWalkAnimation(npcIndex, coords, direction); } - public void StartNPCAttackAnimation(int npcIndex) + public void StartNPCAttackAnimation(int npcIndex, EODirection direction) { if (!_hudControlProvider.IsInGame) return; - Animator.StartAttackAnimation(npcIndex); + Animator.StartAttackAnimation(npcIndex, direction); _sfxPlayer.PlaySfx(SoundEffectID.PunchAttack); } diff --git a/EndlessClient/Rendering/NPC/NPCAnimator.cs b/EndlessClient/Rendering/NPC/NPCAnimator.cs index 794439da7..d950e6ce3 100644 --- a/EndlessClient/Rendering/NPC/NPCAnimator.cs +++ b/EndlessClient/Rendering/NPC/NPCAnimator.cs @@ -1,12 +1,10 @@ using System.Collections.Generic; -using System.Linq; using EndlessClient.GameExecution; using EOLib; using EOLib.Domain.Extensions; using EOLib.Domain.Map; using EOLib.Domain.NPC; using Microsoft.Xna.Framework; -using Optional.Collections; namespace EndlessClient.Rendering.NPC { @@ -14,8 +12,11 @@ public class NPCAnimator : GameComponent, INPCAnimator { private const int TICKS_PER_ACTION_FRAME = 8; // 8 x10ms ticks per action frame - private readonly List _npcStartWalkingTimes; - private readonly List _npcStartAttackingTimes; + private readonly Dictionary _npcStartWalkingTimes = []; + private readonly Dictionary _npcStartAttackingTimes = []; + private readonly Dictionary _queuedWalk = []; + private readonly Dictionary _queuedAttack = []; + private readonly ICurrentMapStateRepository _currentMapStateRepository; private readonly IFixedTimeStepRepository _fixedTimeStepRepository; @@ -26,8 +27,6 @@ public NPCAnimator(IEndlessGameProvider gameProvider, { _currentMapStateRepository = currentMapStateRepository; _fixedTimeStepRepository = fixedTimeStepRepository; - _npcStartWalkingTimes = new List(); - _npcStartAttackingTimes = new List(); } public override void Update(GameTime gameTime) @@ -38,86 +37,164 @@ public override void Update(GameTime gameTime) base.Update(gameTime); } - public void StartWalkAnimation(int npcIndex) + public void StartWalkAnimation(int npcIndex, MapCoordinate coords, EODirection direction) { - if (_npcStartWalkingTimes.Any(x => x.UniqueID == npcIndex)) - return; - - var startWalkingTimeAndID = new RenderFrameActionTime(npcIndex, _fixedTimeStepRepository.TickCount); + if (_npcStartWalkingTimes.TryGetValue(npcIndex, out RenderFrameActionTime value)) + { + value.SetReplay(); + _queuedWalk[npcIndex] = (coords, direction); + } + else if (_npcStartAttackingTimes.ContainsKey(npcIndex)) + { + _queuedWalk[npcIndex] = (coords, direction); + } + else + { + var startWalkingTimeAndID = new RenderFrameActionTime(npcIndex, _fixedTimeStepRepository.TickCount); + _npcStartWalkingTimes.Add(npcIndex, startWalkingTimeAndID); - _npcStartWalkingTimes.Add(startWalkingTimeAndID); + if (_currentMapStateRepository.NPCs.TryGetValue(npcIndex, out var npc)) + { + _currentMapStateRepository.NPCs.Update(npc, EnsureCorrectXAndY(npc, coords, direction)); + } + } } - public void StartAttackAnimation(int npcIndex) + public void StartAttackAnimation(int npcIndex, EODirection direction) { - if (_npcStartAttackingTimes.Any(x => x.UniqueID == npcIndex)) - return; - - var startAttackingTimeAndID = new RenderFrameActionTime(npcIndex, _fixedTimeStepRepository.TickCount); + if (_npcStartAttackingTimes.TryGetValue(npcIndex, out RenderFrameActionTime value)) + { + value.SetReplay(); + _queuedAttack[npcIndex] = direction; + } + else if (_npcStartWalkingTimes.ContainsKey(npcIndex)) + { + _queuedAttack[npcIndex] = direction; + } + else + { + var startAttackingTimeAndID = new RenderFrameActionTime(npcIndex, _fixedTimeStepRepository.TickCount); + _npcStartAttackingTimes.Add(npcIndex, startAttackingTimeAndID); - _npcStartAttackingTimes.Add(startAttackingTimeAndID); + if (_currentMapStateRepository.NPCs.TryGetValue(npcIndex, out var npc)) + { + _currentMapStateRepository.NPCs.Update(npc, npc.WithDirection(direction)); + } + } } public void StopAllAnimations() { _npcStartWalkingTimes.Clear(); + _npcStartAttackingTimes.Clear(); + _queuedWalk.Clear(); + _queuedAttack.Clear(); } private void AnimateNPCWalking() { - var npcsDoneWalking = new List(); - foreach (var pair in _npcStartWalkingTimes) + var npcsDoneWalking = new List(); + foreach (var pair in _npcStartWalkingTimes.Values) { - if ((_fixedTimeStepRepository.TickCount - pair.ActionTick) >= TICKS_PER_ACTION_FRAME) + if (_fixedTimeStepRepository.TickCount - pair.ActionTick >= TICKS_PER_ACTION_FRAME) { - var npc = _currentMapStateRepository.NPCs.SingleOrNone(x => x.Index == pair.UniqueID); + if (_currentMapStateRepository.NPCs.TryGetValue(pair.UniqueID, out var npc)) + { + var nextFrameNPC = AnimateOneWalkFrame(npc); + pair.UpdateActionStartTime(_fixedTimeStepRepository.TickCount); - npc.Match( - some: n => + if (nextFrameNPC.IsActing(NPCActionState.Standing)) { - var nextFrameNPC = AnimateOneWalkFrame(n); - pair.UpdateActionStartTime(_fixedTimeStepRepository.TickCount); - - if (nextFrameNPC.Frame == NPCFrame.Standing) - npcsDoneWalking.Add(pair); - - _currentMapStateRepository.NPCs.Remove(n); - _currentMapStateRepository.NPCs.Add(nextFrameNPC); - }, - none: () => npcsDoneWalking.Add(pair)); + if (pair.Replay) + { + nextFrameNPC = AnimateOneWalkFrame(nextFrameNPC); + + if (_queuedWalk.TryGetValue(pair.UniqueID, out var update)) + { + nextFrameNPC = EnsureCorrectXAndY(nextFrameNPC, update.Coord, update.Direction); + _queuedWalk.Remove(pair.UniqueID); + } + + pair.ClearReplay(); + } + else + { + npcsDoneWalking.Add(pair.UniqueID); + + if (_queuedAttack.TryGetValue(pair.UniqueID, out var update)) + { + nextFrameNPC = nextFrameNPC.WithDirection(update); + _npcStartAttackingTimes.Add(pair.UniqueID, pair); + _queuedAttack.Remove(pair.UniqueID); + } + } + } + + _currentMapStateRepository.NPCs.Update(npc, nextFrameNPC); + } + else + { + npcsDoneWalking.Add(pair.UniqueID); + } } } - _npcStartWalkingTimes.RemoveAll(npcsDoneWalking.Contains); + foreach (var index in npcsDoneWalking) + _npcStartWalkingTimes.Remove(index); } private void AnimateNPCAttacking() { - var npcsDoneAttacking = new List(); - foreach (var pair in _npcStartAttackingTimes) + var npcsDoneAttacking = new List(); + foreach (var pair in _npcStartAttackingTimes.Values) { - if ((_fixedTimeStepRepository.TickCount - pair.ActionTick) >= TICKS_PER_ACTION_FRAME) + if (_fixedTimeStepRepository.TickCount - pair.ActionTick >= TICKS_PER_ACTION_FRAME) { - var npc = _currentMapStateRepository.NPCs.SingleOrNone(x => x.Index == pair.UniqueID); + if (_currentMapStateRepository.NPCs.TryGetValue(pair.UniqueID, out var npc)) + { + var nextFrameNPC = npc.WithNextAttackFrame(); + pair.UpdateActionStartTime(_fixedTimeStepRepository.TickCount); - npc.Match( - some: n => + if (nextFrameNPC.Frame == NPCFrame.Standing) { - var nextFrameNPC = n.WithNextAttackFrame(); - pair.UpdateActionStartTime(_fixedTimeStepRepository.TickCount); - - if (nextFrameNPC.Frame == NPCFrame.Standing) - npcsDoneAttacking.Add(pair); - - _currentMapStateRepository.NPCs.Remove(n); - _currentMapStateRepository.NPCs.Add(nextFrameNPC); - - }, - none: () => npcsDoneAttacking.Add(pair)); + if (pair.Replay) + { + nextFrameNPC = npc.WithNextAttackFrame(); + + if (_queuedAttack.TryGetValue(pair.UniqueID, out var update)) + { + nextFrameNPC = nextFrameNPC.WithDirection(update); + _queuedAttack.Remove(pair.UniqueID); + } + + pair.ClearReplay(); + } + else + { + npcsDoneAttacking.Add(pair.UniqueID); + + if (_queuedWalk.TryGetValue(pair.UniqueID, out var update)) + { + nextFrameNPC = EnsureCorrectXAndY(nextFrameNPC, update.Coord, update.Direction); + _queuedWalk.Remove(pair.UniqueID); + + _npcStartWalkingTimes.Add(pair.UniqueID, pair); + } + } + } + + _currentMapStateRepository.NPCs.Remove(npc); + _currentMapStateRepository.NPCs.Add(nextFrameNPC); + } + else + { + npcsDoneAttacking.Add(pair.UniqueID); + } } } - _npcStartAttackingTimes.RemoveAll(npcsDoneAttacking.Contains); + foreach (var index in npcsDoneAttacking) + _npcStartAttackingTimes.Remove(index); } private static EOLib.Domain.NPC.NPC AnimateOneWalkFrame(EOLib.Domain.NPC.NPC npc) @@ -133,13 +210,19 @@ private static EOLib.Domain.NPC.NPC AnimateOneWalkFrame(EOLib.Domain.NPC.NPC npc return nextFrameNPC; } + + private static EOLib.Domain.NPC.NPC EnsureCorrectXAndY(EOLib.Domain.NPC.NPC npc, MapCoordinate coordinate, EODirection direction) + { + var tmpNpc = npc.WithX(coordinate.X).WithY(coordinate.Y).WithDirection(direction.Opposite()); + return npc.WithDirection(direction).WithX(tmpNpc.GetDestinationX()).WithY(tmpNpc.GetDestinationY()); + } } public interface INPCAnimator : IGameComponent { - void StartWalkAnimation(int npcIndex); + void StartWalkAnimation(int npcIndex, MapCoordinate coords, EODirection direction); - void StartAttackAnimation(int npcIndex); + void StartAttackAnimation(int npcIndex, EODirection direction); void StopAllAnimations(); } From 4495d936a3a086510e485153250a1a283a015fdb Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Wed, 30 Oct 2024 10:48:21 -0700 Subject: [PATCH 8/9] Fix compilation failure due to updated notifier interface --- EOBot/Program.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/EOBot/Program.cs b/EOBot/Program.cs index d9417cc92..9d23e7e47 100644 --- a/EOBot/Program.cs +++ b/EOBot/Program.cs @@ -61,19 +61,20 @@ public void ShowNPCSpeechBubble(int npcIndex, string message) { } - public void StartNPCAttackAnimation(int npcIndex) + public void StartNPCAttackAnimation(int npcIndex, EODirection direction) { } - public void StartNPCWalkAnimation(int npcIndex) + public void StartNPCWalkAnimation(int npcIndex, MapCoordinate coordinate, EODirection direction) { - // immediately walk the NPC to the destination index - var npc = _currentMapStateRepository.NPCs.SingleOrDefault(x => x.Index == npcIndex); - if (npc == null) return; - - var newNpc = npc.WithX(npc.GetDestinationX()).WithY(npc.GetDestinationY()).WithFrame(NPCFrame.Standing); - _currentMapStateRepository.NPCs.Remove(npc); - _currentMapStateRepository.NPCs.Add(newNpc); + if (_currentMapStateRepository.NPCs.TryGetValue(npcIndex, out var npc)) + { + // immediately walk the NPC to the destination index + _currentMapStateRepository.NPCs.Update( + npc, + npc.WithDirection(direction).WithX(coordinate.X).WithY(coordinate.Y) + ); + } } public void NPCDropItem(MapItem item) From ad19a8f289b04692ec5fc0bc4c5b3f21e22c9437 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Wed, 30 Oct 2024 10:56:06 -0700 Subject: [PATCH 9/9] Cleanup in RoofLayerRenderer --- .../Rendering/MapEntityRenderers/RoofLayerRenderer.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/EndlessClient/Rendering/MapEntityRenderers/RoofLayerRenderer.cs b/EndlessClient/Rendering/MapEntityRenderers/RoofLayerRenderer.cs index 03c6718bc..603cf8e02 100644 --- a/EndlessClient/Rendering/MapEntityRenderers/RoofLayerRenderer.cs +++ b/EndlessClient/Rendering/MapEntityRenderers/RoofLayerRenderer.cs @@ -44,14 +44,15 @@ public override void RenderElementAt(SpriteBatch spriteBatch, int row, int col, { var gfxCandidates = new List(); if (CurrentMap.GFX[MapLayer.Roof][row - 1, col] > 0) + { gfxCandidates.Add(CurrentMap.GFX[MapLayer.Roof][row - 1, col]); - if (row == CurrentMap.Properties.Height) + } + + if (row == CurrentMap.Properties.Height && CurrentMap.GFX[MapLayer.Roof][row, col] > 0) { - if (CurrentMap.GFX[MapLayer.Roof][row, col] > 0) - gfxCandidates.Add(CurrentMap.GFX[MapLayer.Roof][row, col]); + gfxCandidates.Add(CurrentMap.GFX[MapLayer.Roof][row, col]); } - //int gfxNum = CurrentMap.GFX[MapLayer.Roof][row-1, col]; foreach (var gfxNum in gfxCandidates) { var gfx = _nativeGraphicsManager.TextureFromResource(GFXTypes.MapWallTop, gfxNum, true);