From 5363746a18bc68acf638ff714d53c23bd43416f0 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Thu, 4 May 2023 21:49:10 -0700 Subject: [PATCH 01/15] Make animation timings wrok more consistently. Fixes: - Main character moving/attacking more slowly than vanilla client - Other characters moving more slowly than vanilla client, causing weird walk glitches when their walk ended - Main character timestamps not having high enough resolution - Main character timestamps not being sent at consistent enough intervals --- EOLib/Domain/Character/CharacterActions.cs | 17 +++++++------ EOLib/GameStartTimeRepository.cs | 25 +++++++++++++++++++ EOLib/misc.cs | 3 ++- EndlessClient/GameExecution/EndlessGame.cs | 16 ++++++------ .../Rendering/Character/CharacterAnimator.cs | 13 +++++----- .../Rendering/FixedTimeStepRepository.cs | 4 +-- 6 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 EOLib/GameStartTimeRepository.cs diff --git a/EOLib/Domain/Character/CharacterActions.cs b/EOLib/Domain/Character/CharacterActions.cs index 9ee0de5df..5e2106f04 100644 --- a/EOLib/Domain/Character/CharacterActions.cs +++ b/EOLib/Domain/Character/CharacterActions.cs @@ -16,14 +16,17 @@ public class CharacterActions : ICharacterActions private readonly IPacketSendService _packetSendService; private readonly ICharacterRepository _characterRepository; private readonly IESFFileProvider _spellFileProvider; + private readonly IGameStartTimeProvider _gameStartTimeProvider; public CharacterActions(IPacketSendService packetSendService, ICharacterRepository characterRepository, - IESFFileProvider spellFileProvider) + IESFFileProvider spellFileProvider, + IGameStartTimeProvider gameStartTimeProvider) { _packetSendService = packetSendService; _characterRepository = characterRepository; _spellFileProvider = spellFileProvider; + _gameStartTimeProvider = gameStartTimeProvider; } public void Face(EODirection direction) @@ -43,7 +46,7 @@ public void Walk() var packet = new PacketBuilder(PacketFamily.Walk, admin ? PacketAction.Admin : PacketAction.Player) .AddChar((int)renderProperties.Direction) - .AddThree(DateTime.Now.ToEOTimeStamp()) + .AddThree(_gameStartTimeProvider.TimeStamp) .AddChar(renderProperties.GetDestinationX()) .AddChar(renderProperties.GetDestinationY()) .Build(); @@ -59,7 +62,7 @@ public void Attack() var packet = new PacketBuilder(PacketFamily.Attack, PacketAction.Use) .AddChar((int) _characterRepository.MainCharacter.RenderProperties.Direction) - .AddThree(DateTime.Now.ToEOTimeStamp()) + .AddThree(_gameStartTimeProvider.TimeStamp) .Build(); _packetSendService.SendPacket(packet); @@ -100,7 +103,7 @@ public void PrepareCastSpell(int spellId) { var packet = new PacketBuilder(PacketFamily.Spell, PacketAction.Request) .AddShort(spellId) - .AddThree(DateTime.Now.ToEOTimeStamp()) + .AddThree(_gameStartTimeProvider.TimeStamp) .Build(); _packetSendService.SendPacket(packet); @@ -128,7 +131,7 @@ public void CastSpell(int spellId, ISpellTargetable target) { builder = builder .AddShort(spellId) - .AddThree(DateTime.Now.ToEOTimeStamp()); + .AddThree(_gameStartTimeProvider.TimeStamp); } else { @@ -146,13 +149,13 @@ public void CastSpell(int spellId, ISpellTargetable target) .AddShort(1) // unknown .AddShort(spellId) .AddShort(target.Index) - .AddThree(DateTime.Now.ToEOTimeStamp()); + .AddThree(_gameStartTimeProvider.TimeStamp); } else { builder = builder .AddShort(spellId) - .AddInt(DateTime.Now.ToEOTimeStamp()); + .AddInt(_gameStartTimeProvider.TimeStamp); } } diff --git a/EOLib/GameStartTimeRepository.cs b/EOLib/GameStartTimeRepository.cs new file mode 100644 index 000000000..38c656c23 --- /dev/null +++ b/EOLib/GameStartTimeRepository.cs @@ -0,0 +1,25 @@ +using AutomaticTypeMapper; +using System; +using System.Diagnostics; + +namespace EOLib +{ + public interface IGameStartTimeProvider + { + DateTime StartTime { get; } + + Stopwatch Elapsed { get; } + + int TimeStamp { get; } + } + + [AutoMappedType(IsSingleton = true)] + public class GameStartTimeRepository : IGameStartTimeProvider + { + public DateTime StartTime { get; } = DateTime.UtcNow; + + public Stopwatch Elapsed { get; } = Stopwatch.StartNew(); + + public int TimeStamp => StartTime.ToEOTimeStamp(Elapsed.ElapsedTicks); + } +} diff --git a/EOLib/misc.cs b/EOLib/misc.cs index 0f4d81fff..919ce6f62 100644 --- a/EOLib/misc.cs +++ b/EOLib/misc.cs @@ -20,8 +20,9 @@ public static T[] SubArray(this T[] arr, int offset, int count) public static class DateTimeExtension { - public static int ToEOTimeStamp(this DateTime dt) + public static int ToEOTimeStamp(this DateTime dt, long elapsedTicks) { + dt = dt.Add(TimeSpan.FromTicks(elapsedTicks)); return dt.Hour * 360000 + dt.Minute * 6000 + dt.Second * 100 + dt.Millisecond / 10; } } diff --git a/EndlessClient/GameExecution/EndlessGame.cs b/EndlessClient/GameExecution/EndlessGame.cs index 23c5cbdf1..801bc3d4c 100644 --- a/EndlessClient/GameExecution/EndlessGame.cs +++ b/EndlessClient/GameExecution/EndlessGame.cs @@ -21,6 +21,7 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using MonoGame.Extended.BitmapFonts; +using MonoGame.Extended.Input; namespace EndlessClient.GameExecution { @@ -111,6 +112,9 @@ protected override void Initialize() IsMouseVisible = true; IsFixedTimeStep = false; + + TargetElapsedTime = TimeSpan.FromMilliseconds(12); + _previousKeyState = Keyboard.GetState(); // setting Width/Height in window size repository applies the change to disable vsync @@ -175,17 +179,15 @@ protected override void LoadContent() protected override void Update(GameTime gameTime) { - // Force update at 60FPS - // Some game components rely on ~60FPS update times. See: https://github.com/ethanmoffat/EndlessClient/issues/199 + // Force updates to wait every 12ms + // Some game components rely on slower update times. 60FPS was the original, but 12ms factors nicely in 120ms "ticks" + // See: https://github.com/ethanmoffat/EndlessClient/issues/199 // Using IsFixedTimeStep = true with TargetUpdateTime set to 60FPS also limits the draw rate, which is not desired - if ((gameTime.TotalGameTime - _lastFrameUpdate).TotalMilliseconds > 1000.0 / 60) + if ((gameTime.TotalGameTime - _lastFrameUpdate).TotalMilliseconds >= 12.0) { - #if DEBUG - //todo: this is a debug-only mode launched with the F5 key. - //todo: move this to be handled by some sort of key listener once function keys are handled in-game var currentKeyState = Keyboard.GetState(); - if (_previousKeyState.IsKeyDown(Keys.F5) && currentKeyState.IsKeyUp(Keys.F5)) + if (KeyboardExtended.GetState().WasKeyJustDown(Keys.F5)) { _testModeLauncher.LaunchTestMode(); } diff --git a/EndlessClient/Rendering/Character/CharacterAnimator.cs b/EndlessClient/Rendering/Character/CharacterAnimator.cs index ec23be281..c97104075 100644 --- a/EndlessClient/Rendering/Character/CharacterAnimator.cs +++ b/EndlessClient/Rendering/Character/CharacterAnimator.cs @@ -1,7 +1,6 @@ using EndlessClient.GameExecution; using EndlessClient.HUD; using EndlessClient.HUD.Spells; -using EndlessClient.Input; using EOLib; using EOLib.Domain.Character; using EOLib.Domain.Extensions; @@ -20,9 +19,9 @@ namespace EndlessClient.Rendering.Character { public class CharacterAnimator : GameComponent, ICharacterAnimator { - public const int WALK_FRAME_TIME_MS = 125; - public const int ATTACK_FRAME_TIME_MS = 125; - public const int EMOTE_FRAME_TIME_MS = 250; + public const int WALK_FRAME_TIME_MS = 96; + public const int ATTACK_FRAME_TIME_MS = 108; + public const int FRAME_TIME_MS = 120; private readonly ICharacterRepository _characterRepository; private readonly ICurrentMapStateRepository _currentMapStateRepository; @@ -481,7 +480,7 @@ private void AnimateCharacterSpells() { _mainPlayerStartShoutTime.MatchSome(t => { - if (t.ElapsedMilliseconds >= (_shoutSpellData.CastTime - 1) * 480 + 350) + if (t.ElapsedMilliseconds >= _shoutSpellData.CastTime * 480) { _otherPlayerStartSpellCastTimes.Add(_characterRepository.MainCharacter.ID, new RenderFrameActionTime(_characterRepository.MainCharacter.ID)); _characterActions.CastSpell(_shoutSpellData.ID, _spellTarget); @@ -495,7 +494,7 @@ private void AnimateCharacterSpells() var playersDoneCasting = new HashSet(); foreach (var pair in _otherPlayerStartSpellCastTimes.Values) { - if (pair.ActionTimer.ElapsedMilliseconds >= ATTACK_FRAME_TIME_MS) + if (pair.ActionTimer.ElapsedMilliseconds >= FRAME_TIME_MS) { GetCurrentCharacterFromRepository(pair).Match( none: () => playersDoneCasting.Add(pair.UniqueID), @@ -527,7 +526,7 @@ private void AnimateCharacterEmotes() var playersDoneEmoting = new HashSet(); foreach (var pair in _startEmoteTimes.Values) { - if (pair.ActionTimer.ElapsedMilliseconds >= EMOTE_FRAME_TIME_MS) + if (pair.ActionTimer.ElapsedMilliseconds >= FRAME_TIME_MS * 2) { GetCurrentCharacterFromRepository(pair).Match( none: () => playersDoneEmoting.Add(pair.UniqueID), diff --git a/EndlessClient/Rendering/FixedTimeStepRepository.cs b/EndlessClient/Rendering/FixedTimeStepRepository.cs index a19896933..2714c7d3b 100644 --- a/EndlessClient/Rendering/FixedTimeStepRepository.cs +++ b/EndlessClient/Rendering/FixedTimeStepRepository.cs @@ -6,13 +6,13 @@ namespace EndlessClient.Rendering [AutoMappedType(IsSingleton = true)] public class FixedTimeStepRepository : IFixedTimeStepRepository { - private const int FIXED_UPDATE_TIME_MS = 24; // 40 FPS (walk updates at 10 FPS) + private const int FIXED_UPDATE_TIME_MS = 10; // 100 FPS (walk updates at 50 FPS) private int _isWalkUpdate; public Stopwatch FixedUpdateTimer { get; set; } - public bool IsUpdateFrame => FixedUpdateTimer.ElapsedMilliseconds > FIXED_UPDATE_TIME_MS; + public bool IsUpdateFrame => FixedUpdateTimer.ElapsedMilliseconds >= FIXED_UPDATE_TIME_MS; public bool IsWalkUpdateFrame => IsUpdateFrame && _isWalkUpdate == 3; From 7c5cda4cac990b90257f45a516e8396bd7554087 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Thu, 4 May 2023 21:52:36 -0700 Subject: [PATCH 02/15] Convert lambdas in MapRenderer to local functions. Reduces allocations that need to be collected by GC --- EndlessClient/Rendering/Map/MapRenderer.cs | 28 +++++++++++++--------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/EndlessClient/Rendering/Map/MapRenderer.cs b/EndlessClient/Rendering/Map/MapRenderer.cs index 917cccf3c..12e466a09 100644 --- a/EndlessClient/Rendering/Map/MapRenderer.cs +++ b/EndlessClient/Rendering/Map/MapRenderer.cs @@ -359,7 +359,7 @@ private void DrawToSpriteBatch(SpriteBatch spriteBatch, GameTime gameTime) spriteBatch.Begin(); var drawLoc = _gridDrawCoordinateCalculator.CalculateGroundLayerRenderTargetDrawCoordinates(); - var offset = _quakeState.Map(qs => qs.Offset).Match(some: o => o, none: () => 0); + var offset = _quakeState.Map(GetOffset).ValueOr(0); lock (_rt_locker_) { @@ -378,11 +378,13 @@ private void DrawToSpriteBatch(SpriteBatch spriteBatch, GameTime gameTime) spriteBatch.End(); } + + static float GetOffset(MapQuakeState quakeState) => quakeState.Offset; } private void DrawBaseLayers(SpriteBatch spriteBatch) { - var offset = _quakeState.Map(qs => qs.Offset).Match(some: o => o, none: () => 0); + var offset = _quakeState.Map(GetOffset).ValueOr(0); var renderBounds = _mapRenderDistanceCalculator.CalculateRenderBounds(_characterProvider.MainCharacter, _currentMapProvider.CurrentMap); @@ -399,6 +401,8 @@ private void DrawBaseLayers(SpriteBatch spriteBatch) } } } + + static float GetOffset(MapQuakeState quakeState) => quakeState.Offset; } private int GetAlphaForCoordinates(int objX, int objY, EOLib.Domain.Character.Character character) @@ -422,20 +426,22 @@ private int GetAlphaForCoordinates(int objX, int objY, EOLib.Domain.Character.Ch } else if (metric == _mapTransitionState.TransitionMetric) { - _mapTransitionState.StartTime - .MatchSome(startTime => - { - var ms = (DateTime.Now - startTime).TotalMilliseconds; - alpha = (int)Math.Round(ms / TRANSITION_TIME_MS * 255); - - if (ms / TRANSITION_TIME_MS >= 1) - _mapTransitionState = new MapTransitionState(Option.Some(DateTime.Now), _mapTransitionState.TransitionMetric + 1); - }); + alpha = _mapTransitionState.StartTime.Map(HandleStartTime).ValueOr(alpha); } else alpha = 0; return alpha; + + int HandleStartTime(DateTime startTime) + { + var ms = (DateTime.Now - startTime).TotalMilliseconds; + + if (ms / TRANSITION_TIME_MS >= 1) + _mapTransitionState = new MapTransitionState(Option.Some(DateTime.Now), _mapTransitionState.TransitionMetric + 1); + + return (int)Math.Round(ms / TRANSITION_TIME_MS * 255); + } } private void ResizeGameWindow(object sender, EventArgs e) From 1300e6f7418da3fadd2654c1456edc71d4a48b65 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Thu, 4 May 2023 21:53:00 -0700 Subject: [PATCH 03/15] Reduce allocations of temporary LibraryGraphicPair objects in NativeGraphicsManager --- EOLib.Graphics.Test/LibraryGraphicPairTest.cs | 50 ------------------- EOLib.Graphics/LibraryGraphicPair.cs | 41 --------------- EOLib.Graphics/NativeGraphicsManager.cs | 26 ++++++---- 3 files changed, 16 insertions(+), 101 deletions(-) delete mode 100644 EOLib.Graphics.Test/LibraryGraphicPairTest.cs delete mode 100644 EOLib.Graphics/LibraryGraphicPair.cs diff --git a/EOLib.Graphics.Test/LibraryGraphicPairTest.cs b/EOLib.Graphics.Test/LibraryGraphicPairTest.cs deleted file mode 100644 index 10214ea66..000000000 --- a/EOLib.Graphics.Test/LibraryGraphicPairTest.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using NUnit.Framework; - -namespace EOLib.Graphics.Test -{ - [TestFixture, ExcludeFromCodeCoverage] - public class LibraryGraphicPairTest - { - [Test] - public void Equals_ReturnsFalse_WhenOtherObjectIsNotLibraryGraphicPair() - { - var pair = new LibraryGraphicPair(1, 1); - Assert.IsFalse(pair.Equals(new object())); - } - - [Test] - public void CompareTo_ReturnsNeg1_WhenOtherObjectIsNotLibraryGraphicPair() - { - var pair = new LibraryGraphicPair(1, 1); - Assert.AreEqual(-1, pair.CompareTo(new object())); - } - - [Test] - public void CompareTo_ReturnsNeg1_WhenOtherObjectHasDifferentLibraryNumber() - { - var pair = new LibraryGraphicPair(1, 1); - var other = new LibraryGraphicPair(2, 1); - - Assert.AreEqual(-1, pair.CompareTo(other)); - } - - [Test] - public void CompareTo_ReturnsNeg1_WhenOtherObjectHasDifferentGFXNumber() - { - var pair = new LibraryGraphicPair(1, 1); - var other = new LibraryGraphicPair(1, 2); - - Assert.AreEqual(-1, pair.CompareTo(other)); - } - - [Test] - public void CompareTo_Returns0_WhenOtherObjectHasSameValues() - { - var pair = new LibraryGraphicPair(1, 1); - var other = new LibraryGraphicPair(1, 1); - - Assert.AreEqual(0, pair.CompareTo(other)); - } - } -} diff --git a/EOLib.Graphics/LibraryGraphicPair.cs b/EOLib.Graphics/LibraryGraphicPair.cs deleted file mode 100644 index e4a3d3555..000000000 --- a/EOLib.Graphics/LibraryGraphicPair.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; - -namespace EOLib.Graphics -{ - public struct LibraryGraphicPair : IComparable - { - private readonly int LibraryNumber; - private readonly int GraphicNumber; - - public LibraryGraphicPair(int lib, int gfx) - { - LibraryNumber = lib; - GraphicNumber = gfx; - } - - public int CompareTo(object other) - { - if (!(other is LibraryGraphicPair)) - return -1; - - LibraryGraphicPair rhs = (LibraryGraphicPair)other; - - if (rhs.LibraryNumber == LibraryNumber && rhs.GraphicNumber == GraphicNumber) - return 0; - - return -1; - } - - public override bool Equals(object obj) - { - if (!(obj is LibraryGraphicPair)) return false; - LibraryGraphicPair other = (LibraryGraphicPair)obj; - return other.GraphicNumber == GraphicNumber && other.LibraryNumber == LibraryNumber; - } - - public override int GetHashCode() - { - return (LibraryNumber << 16) | GraphicNumber; - } - } -} diff --git a/EOLib.Graphics/NativeGraphicsManager.cs b/EOLib.Graphics/NativeGraphicsManager.cs index d2383ef99..de92be5de 100644 --- a/EOLib.Graphics/NativeGraphicsManager.cs +++ b/EOLib.Graphics/NativeGraphicsManager.cs @@ -6,21 +6,23 @@ using SixLabors.ImageSharp.Processing; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; +using System.Linq; namespace EOLib.Graphics { [MappedType(BaseType = typeof(INativeGraphicsManager), IsSingleton = true)] public sealed class NativeGraphicsManager : INativeGraphicsManager { - private readonly ConcurrentDictionary _cache; + private readonly ConcurrentDictionary> _cache; private readonly INativeGraphicsLoader _gfxLoader; private readonly IGraphicsDeviceProvider _graphicsDeviceProvider; public NativeGraphicsManager(INativeGraphicsLoader gfxLoader, IGraphicsDeviceProvider graphicsDeviceProvider) { - _cache = new ConcurrentDictionary(); + _cache = new ConcurrentDictionary>(); _gfxLoader = gfxLoader; _graphicsDeviceProvider = graphicsDeviceProvider; } @@ -29,18 +31,16 @@ public Texture2D TextureFromResource(GFXTypes file, int resourceVal, bool transp { Texture2D ret; - var key = new LibraryGraphicPair((int)file, 100 + resourceVal); - - if (_cache.ContainsKey(key)) + if (_cache.ContainsKey(file) && _cache[file].ContainsKey(resourceVal)) { if (reloadFromFile) { - _cache[key]?.Dispose(); - _cache.TryRemove(key, out var _); + _cache[file][resourceVal]?.Dispose(); + _cache[file].Remove(resourceVal, out _); } else { - return _cache[key]; + return _cache[file][resourceVal]; } } @@ -61,7 +61,12 @@ public Texture2D TextureFromResource(GFXTypes file, int resourceVal, bool transp } } - _cache.TryAdd(key, ret); + if (_cache.ContainsKey(file) || + _cache.TryAdd(file, new ConcurrentDictionary())) + { + _cache[file].TryAdd(resourceVal, ret); + } + return ret; } @@ -96,8 +101,9 @@ private static void CrossPlatformMakeTransparent(Image bmp, Color transparentCol public void Dispose() { - foreach (var text in _cache.Values) + foreach (var text in _cache.SelectMany(x => x.Value.Values)) text.Dispose(); + _cache.Clear(); } } From 07547bbf729eeb9f8c02dcc96b143ea239f10786 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Sun, 7 May 2023 22:07:26 -0700 Subject: [PATCH 04/15] Reduce lambda usage in entity renderers. Reduces garbage collections --- .../MapItemLayerRenderer.cs | 7 +++++-- .../MapEntityRenderers/NPCEntityRenderer.cs | 8 +++++--- .../OtherCharacterEntityRenderer.cs | 19 +++++++++++-------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/EndlessClient/Rendering/MapEntityRenderers/MapItemLayerRenderer.cs b/EndlessClient/Rendering/MapEntityRenderers/MapItemLayerRenderer.cs index 0933ca119..2d7072e11 100644 --- a/EndlessClient/Rendering/MapEntityRenderers/MapItemLayerRenderer.cs +++ b/EndlessClient/Rendering/MapEntityRenderers/MapItemLayerRenderer.cs @@ -32,14 +32,15 @@ public class MapItemLayerRenderer : BaseMapEntityRenderer protected override bool ElementExistsAt(int row, int col) { - return _currentMapStateProvider.MapItems.Any(item => item.X == col && item.Y == row); + return _currentMapStateProvider.MapItems.Any(IsItemAt); + bool IsItemAt(MapItem item) => item.X == col && item.Y == row; } public override void RenderElementAt(SpriteBatch spriteBatch, int row, int col, int alpha, Vector2 additionalOffset = default) { var items = _currentMapStateProvider .MapItems - .Where(item => item.X == col && item.Y == row) + .Where(IsItemAt) .OrderBy(item => item.UniqueID); foreach (var item in items) @@ -53,6 +54,8 @@ public override void RenderElementAt(SpriteBatch spriteBatch, int row, int col, itemPos.Y - (int) Math.Round(itemTexture.Height/2.0)) + additionalOffset, Color.FromNonPremultiplied(255, 255, 255, alpha)); } + + bool IsItemAt(MapItem item) => item.X == col && item.Y == row; } protected override Vector2 GetDrawCoordinatesFromGridUnits(int gridX, int gridY) diff --git a/EndlessClient/Rendering/MapEntityRenderers/NPCEntityRenderer.cs b/EndlessClient/Rendering/MapEntityRenderers/NPCEntityRenderer.cs index dba818d45..f8f5b37e0 100644 --- a/EndlessClient/Rendering/MapEntityRenderers/NPCEntityRenderer.cs +++ b/EndlessClient/Rendering/MapEntityRenderers/NPCEntityRenderer.cs @@ -29,14 +29,14 @@ public class NPCEntityRenderer : BaseMapEntityRenderer protected override bool ElementExistsAt(int row, int col) { - return _npcRendererProvider.NPCRenderers.Values - .Count(n => IsNpcAt(n.NPC, row, col)) > 0; + return _npcRendererProvider.NPCRenderers.Values.Any(IsNpcAt); + bool IsNpcAt(INPCRenderer rend) => NPCEntityRenderer.IsNpcAt(rend.NPC, row, col); } public override void RenderElementAt(SpriteBatch spriteBatch, int row, int col, int alpha, Vector2 additionalOffset = default) { var indicesToRender = _npcRendererProvider.NPCRenderers.Values - .Where(n => IsNpcAt(n.NPC, row, col)) + .Where(IsNpcAt) .Select(n => n.NPC.Index); foreach (var index in indicesToRender) @@ -49,6 +49,8 @@ public override void RenderElementAt(SpriteBatch spriteBatch, int row, int col, var renderer = _npcRendererProvider.NPCRenderers[index]; renderer.DrawToSpriteBatch(spriteBatch); } + + bool IsNpcAt(INPCRenderer rend) => NPCEntityRenderer.IsNpcAt(rend.NPC, row, col); } private static bool IsNpcAt(EOLib.Domain.NPC.NPC npc, int row, int col) diff --git a/EndlessClient/Rendering/MapEntityRenderers/OtherCharacterEntityRenderer.cs b/EndlessClient/Rendering/MapEntityRenderers/OtherCharacterEntityRenderer.cs index dfe825bdc..e27c6aaf9 100644 --- a/EndlessClient/Rendering/MapEntityRenderers/OtherCharacterEntityRenderer.cs +++ b/EndlessClient/Rendering/MapEntityRenderers/OtherCharacterEntityRenderer.cs @@ -1,5 +1,4 @@ using EndlessClient.Rendering.Character; -using EndlessClient.Rendering.Chat; using EndlessClient.Rendering.Map; using EOLib.Domain.Character; using Microsoft.Xna.Framework; @@ -31,16 +30,19 @@ public class OtherCharacterEntityRenderer : BaseMapEntityRenderer protected override bool ElementExistsAt(int row, int col) { return _characterStateCache.OtherCharacters.Values - .Select(x => x.RenderProperties) - .Any(c => c.MapY == row && c.MapX == col); + .Any(IsCharAt); + + bool IsCharAt(EOLib.Domain.Character.Character c) => OtherCharacterEntityRenderer.IsCharAt(c, row, col); } public override void RenderElementAt(SpriteBatch spriteBatch, int row, int col, int alpha, Vector2 additionalOffset = default) { - var idsToRender = _characterStateCache.OtherCharacters.Keys.Where(k => IsAtPosition(k, row, col)); + var toRender = _characterStateCache.OtherCharacters.Values.Where(IsCharAt); - foreach (var id in idsToRender) + foreach (var rend in toRender) { + var id = rend.ID; + if (!_characterRendererProvider.CharacterRenderers.ContainsKey(id) || _characterRendererProvider.CharacterRenderers[id] == null) return; @@ -48,11 +50,12 @@ public override void RenderElementAt(SpriteBatch spriteBatch, int row, int col, var renderer = _characterRendererProvider.CharacterRenderers[id]; renderer.DrawToSpriteBatch(spriteBatch); } + + bool IsCharAt(EOLib.Domain.Character.Character c) => OtherCharacterEntityRenderer.IsCharAt(c, row, col); } - private bool IsAtPosition(int characterId, int row, int col) + private static bool IsCharAt(EOLib.Domain.Character.Character c, int row, int col) { - var rp = _characterStateCache.OtherCharacters[characterId].RenderProperties; - return row == rp.MapY && col == rp.MapX; + return row == c.Y && col == c.X; } } } \ No newline at end of file From 1aeff07300ff6f1b3f85161b8d9b07a636481096 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Sun, 7 May 2023 22:07:57 -0700 Subject: [PATCH 05/15] Load HUD background once instead of in draw loop. Eliminates massive number of garbage collection events. --- EndlessClient/UIControls/StatusBarLabel.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/EndlessClient/UIControls/StatusBarLabel.cs b/EndlessClient/UIControls/StatusBarLabel.cs index 340d9a78c..95500fe3b 100644 --- a/EndlessClient/UIControls/StatusBarLabel.cs +++ b/EndlessClient/UIControls/StatusBarLabel.cs @@ -21,6 +21,8 @@ public class StatusBarLabel : XNALabel private readonly Rectangle _background = new Rectangle(70, 451, 548, 20); private readonly Rectangle _rightSide = new Rectangle(618, 451, 12, 20); + private readonly Texture2D _hudBackground; + public StatusBarLabel(INativeGraphicsManager nativeGraphicsManager, IClientWindowSizeProvider clientWindowSizeProvider, IStatusLabelTextProvider statusLabelTextProvider) @@ -34,6 +36,8 @@ public class StatusBarLabel : XNALabel { DrawArea = new Rectangle(40, _clientWindowSizeProvider.Height - 15, 1, 1); _clientWindowSizeProvider.GameWindowSizeChanged += (_, _) => DrawArea = new Rectangle(40, _clientWindowSizeProvider.Height - 15, 1, 1); + + _hudBackground = _nativeGraphicsManager.TextureFromResource(GFXTypes.PostLoginUI, 1, false, true); } else { @@ -60,16 +64,15 @@ protected override void OnDrawControl(GameTime gameTime) { if (_clientWindowSizeProvider.Resizable) { - var hudBackground = _nativeGraphicsManager.TextureFromResource(GFXTypes.PostLoginUI, 1, false, true); var bgDrawArea = new Rectangle(0, _clientWindowSizeProvider.Height - 20, _clientWindowSizeProvider.Width, 20); _spriteBatch.Begin(samplerState: SamplerState.LinearWrap); - _spriteBatch.Draw(hudBackground, bgDrawArea, _background, Color.White); + _spriteBatch.Draw(_hudBackground, bgDrawArea, _background, Color.White); _spriteBatch.End(); _spriteBatch.Begin(); - _spriteBatch.Draw(hudBackground, bgDrawArea.Location.ToVector2(), _leftSide, Color.White); - _spriteBatch.Draw(hudBackground, new Vector2(_clientWindowSizeProvider.Width - _rightSide.Width, bgDrawArea.Y), _rightSide, Color.White); + _spriteBatch.Draw(_hudBackground, bgDrawArea.Location.ToVector2(), _leftSide, Color.White); + _spriteBatch.Draw(_hudBackground, new Vector2(_clientWindowSizeProvider.Width - _rightSide.Width, bgDrawArea.Y), _rightSide, Color.White); _spriteBatch.End(); } From f3e7235f4a48a99c065c8440c8624c8eed00d0d1 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Mon, 8 May 2023 14:18:45 -0700 Subject: [PATCH 06/15] Update to PELoaderLib v1.6 and use byte memory directly when constructing textures instead of parsing through SixLabors.ImageSharp Reduces number of transient heap-allocated objects that need to be garbage collected. Reduces data copies and external dependencies. --- .../EOLib.Graphics.Test.csproj | 2 - .../NativeGraphicsLoaderTest.cs | 30 ++--- .../NativeGraphicsManagerTest.cs | 111 ++++++++++-------- EOLib.Graphics/EOLib.Graphics.csproj | 6 +- EOLib.Graphics/INativeGraphicsLoader.cs | 5 +- EOLib.Graphics/NativeGraphicsLoader.cs | 9 +- EOLib.Graphics/NativeGraphicsManager.cs | 67 +++++------ 7 files changed, 109 insertions(+), 121 deletions(-) diff --git a/EOLib.Graphics.Test/EOLib.Graphics.Test.csproj b/EOLib.Graphics.Test/EOLib.Graphics.Test.csproj index 7502cc71a..ad3cd8f54 100644 --- a/EOLib.Graphics.Test/EOLib.Graphics.Test.csproj +++ b/EOLib.Graphics.Test/EOLib.Graphics.Test.csproj @@ -16,8 +16,6 @@ - - diff --git a/EOLib.Graphics.Test/NativeGraphicsLoaderTest.cs b/EOLib.Graphics.Test/NativeGraphicsLoaderTest.cs index 20828cd6c..2d6d20331 100644 --- a/EOLib.Graphics.Test/NativeGraphicsLoaderTest.cs +++ b/EOLib.Graphics.Test/NativeGraphicsLoaderTest.cs @@ -1,9 +1,8 @@ using Moq; using NUnit.Framework; using PELoaderLib; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -29,9 +28,7 @@ public void WhenLoadGFX_CallsPEFile_GetEmbeddedBitmapResourceByID() { var peFileMock = SetupPEFileForGFXType(GFXTypes.PreLoginUI, CreateDataArrayForBitmap()); - using (var bmp = _nativeGraphicsLoader.LoadGFX(GFXTypes.PreLoginUI, 1)) - bmp.Dispose(); //hide warning for empty using statement - + var data = _nativeGraphicsLoader.LoadGFX(GFXTypes.PreLoginUI, 1); peFileMock.Verify(x => x.GetEmbeddedBitmapResourceByID(It.IsAny(), ExpectedCulture), Times.Once()); } @@ -43,34 +40,27 @@ public void WhenLoadGFX_CallsPEFile_WithResourceValueIncreasedBy100() var peFileMock = SetupPEFileForGFXType(GFXTypes.PreLoginUI, CreateDataArrayForBitmap()); - using (var bmp = _nativeGraphicsLoader.LoadGFX(GFXTypes.PreLoginUI, requestedResourceID)) - bmp.Dispose(); //hide warning for empty using statement - + var data = _nativeGraphicsLoader.LoadGFX(GFXTypes.PreLoginUI, requestedResourceID); peFileMock.Verify(x => x.GetEmbeddedBitmapResourceByID(expectedResourceID, ExpectedCulture)); } [Test] - public void WhenLoadGFX_EmptyDataArray_ThrowsException() + public void WhenLoadGFX_EmptyDataArray_ThrowsInDebug_EmptyInRelease() { const int requestedResourceID = 1; SetupPEFileForGFXType(GFXTypes.PreLoginUI, new byte[] { }); +#if DEBUG Assert.Throws(() => _nativeGraphicsLoader.LoadGFX(GFXTypes.PreLoginUI, requestedResourceID)); +#else + Assert.That(_nativeGraphicsLoader.LoadGFX(GFXTypes.PreLoginUI, requestedResourceID), Has.Length.EqualTo(0)); +#endif } private byte[] CreateDataArrayForBitmap() { - byte[] array; - using (var retBmp = new Image(10, 10)) - { - using (var ms = new MemoryStream()) - { - retBmp.Save(ms, new BmpEncoder()); - array = ms.ToArray(); - } - } - return array; + return new byte[1]; } private Mock SetupPEFileForGFXType(GFXTypes type, byte[] array) diff --git a/EOLib.Graphics.Test/NativeGraphicsManagerTest.cs b/EOLib.Graphics.Test/NativeGraphicsManagerTest.cs index 95565f8c6..14c2ffeb6 100644 --- a/EOLib.Graphics.Test/NativeGraphicsManagerTest.cs +++ b/EOLib.Graphics.Test/NativeGraphicsManagerTest.cs @@ -4,10 +4,7 @@ using NUnit.Framework; using Microsoft.Xna.Framework.Graphics; using Moq; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Drawing.Processing; +using Microsoft.Xna.Framework; namespace EOLib.Graphics.Test { @@ -52,8 +49,8 @@ public void WhenLoadTexture_CallGraphicsLoader() const int requestedResource = 1; var graphicsLoaderMock = Mock.Get(_graphicsLoader); - using (LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource)) - _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); + LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource); + _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); graphicsLoaderMock.Verify(x => x.LoadGFX(GFXTypes.PreLoginUI, requestedResource), Times.Once()); } @@ -64,11 +61,9 @@ public void WhenLoadCachedTexture_DoNotCallGraphicsLoader() const int requestedResource = 1; var graphicsLoaderMock = Mock.Get(_graphicsLoader); - using (LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource)) - { - _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); - _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); - } + LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource); + _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); + _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); graphicsLoaderMock.Verify(x => x.LoadGFX(GFXTypes.PreLoginUI, requestedResource), Times.Once()); } @@ -79,10 +74,10 @@ public void WhenLoadCachedTexture_WhenReloadFromFile_CallGraphicsLoader() const int requestedResource = 1; var graphicsLoaderMock = Mock.Get(_graphicsLoader); - using (LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource)) - _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); - using (LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource)) - _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource, reloadFromFile: true); + LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource); + _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); + LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource); + _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource, reloadFromFile: true); graphicsLoaderMock.Verify(x => x.LoadGFX(GFXTypes.PreLoginUI, requestedResource), Times.Exactly(2)); } @@ -94,14 +89,12 @@ public void WhenLoadCachedTexture_WhenReloadFromFile_DisposesOriginalTextue() var textureHasBeenDisposed = false; - using (LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource)) - { - var texture = _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); - texture.Disposing += (o, e) => textureHasBeenDisposed = true; - } + LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource); + var texture = _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); + texture.Disposing += (o, e) => textureHasBeenDisposed = true; - using (LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource)) - _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource, reloadFromFile: true); + LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource); + _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource, reloadFromFile: true); Assert.IsTrue(textureHasBeenDisposed); } @@ -114,8 +107,8 @@ public void WhenLoadManyTextures_CallsGraphicsLoaderSameNumberOfTimes() const int totalRequestedResources = 100; for (int requestedResource = 1; requestedResource <= totalRequestedResources; ++requestedResource) { - using (LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource)) - _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); + LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource); + _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); var localRequestedResource = requestedResource; graphicsLoaderMock.Verify(x => x.LoadGFX(GFXTypes.PreLoginUI, localRequestedResource), Times.Once()); @@ -131,8 +124,10 @@ public void WhenLoadCachedTexture_ManyTimes_CallsGraphicsLoaderOnce() const int requestedResource = 1; for (int i = 1; i <= 100; ++i) - using (LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource)) - _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); + { + LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource); + _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); + } graphicsLoaderMock.Verify(x => x.LoadGFX(GFXTypes.PreLoginUI, It.IsAny()), Times.Once()); } @@ -143,11 +138,9 @@ public void WhenLoadTexture_Transparent_SetsBlackToTransparent() const int requestedResource = 1; Texture2D resultTexture; - using (var bmp = LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource)) - { - FillBitmapWithColor(bmp, Color.Black); - resultTexture = _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource, true); - } + var bmp = LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource); + FillBitmapWithColor(bmp, Color.Black); + resultTexture = _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource, true); var data = new Microsoft.Xna.Framework.Color[resultTexture.Width*resultTexture.Height]; resultTexture.GetData(data); @@ -161,11 +154,9 @@ public void WhenLoadTexture_MaleHat_Transparent_SetsSpecialColorToTransparent() const int requestedResource = 1; Texture2D resultTexture; - using (var bmp = LoadGFXReturnsBitmap(GFXTypes.MaleHat, requestedResource)) - { - FillBitmapWithColor(bmp, Color.FromRgba(0x08, 0x00, 0x00, 0xff)); - resultTexture = _nativeGraphicsManager.TextureFromResource(GFXTypes.MaleHat, requestedResource, true); - } + var bmp = LoadGFXReturnsBitmap(GFXTypes.MaleHat, requestedResource); + FillBitmapWithColor(bmp, new Color(0x08, 0x00, 0x00, 0xff)); + resultTexture = _nativeGraphicsManager.TextureFromResource(GFXTypes.MaleHat, requestedResource, true); var data = new Microsoft.Xna.Framework.Color[resultTexture.Width * resultTexture.Height]; resultTexture.GetData(data); @@ -179,33 +170,44 @@ public void WhenLoadTexture_FemaleHat_Transparent_SetsSpecialColorToTransparent( const int requestedResource = 1; Texture2D resultTexture; - using (var bmp = LoadGFXReturnsBitmap(GFXTypes.FemaleHat, requestedResource)) - { - FillBitmapWithColor(bmp, Color.FromRgba(0x08, 0x00, 0x00, 0xff)); - resultTexture = _nativeGraphicsManager.TextureFromResource(GFXTypes.FemaleHat, requestedResource, true); - } + var bmp = LoadGFXReturnsBitmap(GFXTypes.FemaleHat, requestedResource); + FillBitmapWithColor(bmp, new Color(0x08, 0x00, 0x00, 0xff)); + resultTexture = _nativeGraphicsManager.TextureFromResource(GFXTypes.FemaleHat, requestedResource, true); - var data = new Microsoft.Xna.Framework.Color[resultTexture.Width * resultTexture.Height]; + var data = new Color[resultTexture.Width * resultTexture.Height]; resultTexture.GetData(data); Assert.IsTrue(data.All(x => x.A == 0)); } - [Test, Ignore("TODO: This is broken, probably because my computer is fast now")] + [Test] public void WhenLoadTexture_RaceCondition_DisposesExistingCachedTextureAndReturnsSecondOne() { const int requestedResource = 1; Texture2D resultTexture; - using (LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource, () => GetTextureAgain(GFXTypes.PreLoginUI, requestedResource))) - resultTexture = _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); + LoadGFXReturnsBitmap(GFXTypes.PreLoginUI, requestedResource, () => GetTextureAgain(GFXTypes.PreLoginUI, requestedResource)); + resultTexture = _nativeGraphicsManager.TextureFromResource(GFXTypes.PreLoginUI, requestedResource); Assert.IsFalse(resultTexture.IsDisposed); } - private IImage LoadGFXReturnsBitmap(GFXTypes whichFile, int requestedResource, Action loadCallback = null) + // Manually builds a byte array that is a valid bitmap image (10x10 pixels, 32bpp) + // Mocks the native graphics loader to return this array, and passes it back to the caller + private Memory LoadGFXReturnsBitmap(GFXTypes whichFile, int requestedResource, Action loadCallback = null) { - var bitmapToReturn = new Image(10, 10); + // 32bpp 10x10 image + 14 byte BMP header + 40 byte BITMAPINFO header + // (4 * 100) + 14 + 40 = 454 + var bitmapToReturn = new byte[454]; + Array.Copy(new[] { (byte)'B', (byte)'M', }, bitmapToReturn, 2); + Array.Copy(BitConverter.GetBytes(454), 0, bitmapToReturn, 2, 4); // total image size [2..5] + Array.Copy(BitConverter.GetBytes(54), 0, bitmapToReturn, 10, 4); // image data offset [10..13] + + Array.Copy(BitConverter.GetBytes(40), 0, bitmapToReturn, 14, 4); // bitmap header size [14..17] + Array.Copy(BitConverter.GetBytes(10), 0, bitmapToReturn, 18, 4); // width [18..21] + Array.Copy(BitConverter.GetBytes(10), 0, bitmapToReturn, 22, 4); // height [22..25] + Array.Copy(BitConverter.GetBytes((short)1), 0, bitmapToReturn, 26, 2); // planes (1) [26..27] + Array.Copy(BitConverter.GetBytes((short)32), 0, bitmapToReturn, 28, 2); // bpp [28..29] var graphicsLoaderMock = Mock.Get(_graphicsLoader); graphicsLoaderMock.Setup(x => x.LoadGFX(whichFile, requestedResource)) @@ -215,10 +217,15 @@ private IImage LoadGFXReturnsBitmap(GFXTypes whichFile, int requestedResource, A return bitmapToReturn; } - private static void FillBitmapWithColor(IImage image, Color color) + private static void FillBitmapWithColor(Memory image, Color color) { - var colorBrush = new SolidBrush(color); - ((Image)image).Mutate(x => x.Clear(colorBrush)); + for (int i = 54; i < image.Length; i+=4) + { + image.Span[i] = color.R; + image.Span[i + 1] = color.G; + image.Span[i + 2] = color.B; + image.Span[i + 3] = color.A; + } } private void GetTextureAgain(GFXTypes whichFile, int requestedResource) @@ -226,8 +233,8 @@ private void GetTextureAgain(GFXTypes whichFile, int requestedResource) if (_keepFromInfiniteLoop) return; _keepFromInfiniteLoop = true; - using (LoadGFXReturnsBitmap(whichFile, requestedResource)) - _nativeGraphicsManager.TextureFromResource(whichFile, requestedResource); + LoadGFXReturnsBitmap(whichFile, requestedResource); + _nativeGraphicsManager.TextureFromResource(whichFile, requestedResource); } } } diff --git a/EOLib.Graphics/EOLib.Graphics.csproj b/EOLib.Graphics/EOLib.Graphics.csproj index 46702f21d..4b865906d 100644 --- a/EOLib.Graphics/EOLib.Graphics.csproj +++ b/EOLib.Graphics/EOLib.Graphics.csproj @@ -5,6 +5,7 @@ Library ..\bin\$(Configuration)\lib\ Library for interacting with Endless Online gfx files + true $(DefineConstants);LINUX @@ -14,10 +15,9 @@ - + + - - diff --git a/EOLib.Graphics/INativeGraphicsLoader.cs b/EOLib.Graphics/INativeGraphicsLoader.cs index 75ad144df..d24b705c7 100644 --- a/EOLib.Graphics/INativeGraphicsLoader.cs +++ b/EOLib.Graphics/INativeGraphicsLoader.cs @@ -1,10 +1,9 @@ -using PELoaderLib; -using SixLabors.ImageSharp; +using System; namespace EOLib.Graphics { public interface INativeGraphicsLoader { - IImage LoadGFX(GFXTypes file, int resourceValue); + ReadOnlyMemory LoadGFX(GFXTypes file, int resourceValue); } } diff --git a/EOLib.Graphics/NativeGraphicsLoader.cs b/EOLib.Graphics/NativeGraphicsLoader.cs index 2f900674d..b201051f0 100644 --- a/EOLib.Graphics/NativeGraphicsLoader.cs +++ b/EOLib.Graphics/NativeGraphicsLoader.cs @@ -1,5 +1,4 @@ using AutomaticTypeMapper; -using SixLabors.ImageSharp; using System; namespace EOLib.Graphics @@ -14,9 +13,9 @@ public NativeGraphicsLoader(IPEFileCollection modules) _modules = modules; } - public IImage LoadGFX(GFXTypes file, int resourceValue) + public ReadOnlyMemory LoadGFX(GFXTypes file, int resourceValue) { - var fileBytes = Array.Empty(); + var fileBytes = ReadOnlyMemory.Empty; try { fileBytes = _modules[file].GetEmbeddedBitmapResourceByID(resourceValue + 100); @@ -33,11 +32,11 @@ public IImage LoadGFX(GFXTypes file, int resourceValue) #if DEBUG throw new GFXLoadException(resourceValue, file); #else - return new Image(1, 1); + return Array.Empty(); #endif } - return Image.Load(fileBytes); + return fileBytes; } } } diff --git a/EOLib.Graphics/NativeGraphicsManager.cs b/EOLib.Graphics/NativeGraphicsManager.cs index de92be5de..46e8cf8d1 100644 --- a/EOLib.Graphics/NativeGraphicsManager.cs +++ b/EOLib.Graphics/NativeGraphicsManager.cs @@ -1,13 +1,10 @@ using AutomaticTypeMapper; +using CommunityToolkit.HighPerformance; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.IO; using System.Linq; namespace EOLib.Graphics @@ -29,8 +26,6 @@ public NativeGraphicsManager(INativeGraphicsLoader gfxLoader, IGraphicsDevicePro public Texture2D TextureFromResource(GFXTypes file, int resourceVal, bool transparent = false, bool reloadFromFile = false) { - Texture2D ret; - if (_cache.ContainsKey(file) && _cache[file].ContainsKey(resourceVal)) { if (reloadFromFile) @@ -44,23 +39,7 @@ public Texture2D TextureFromResource(GFXTypes file, int resourceVal, bool transp } } - using (var i = BitmapFromResource(file, resourceVal, transparent)) - { - if (!i.DangerousTryGetSinglePixelMemory(out var mem)) - { - using (var ms = new MemoryStream()) - { - i.SaveAsPng(ms); - ret = Texture2D.FromStream(_graphicsDeviceProvider.GraphicsDevice, ms); - } - } - else - { - ret = new Texture2D(_graphicsDeviceProvider.GraphicsDevice, i.Width, i.Height); - ret.SetData(mem.ToArray()); - } - } - + var ret = LoadTexture(file, resourceVal, transparent); if (_cache.ContainsKey(file) || _cache.TryAdd(file, new ConcurrentDictionary())) { @@ -70,33 +49,49 @@ public Texture2D TextureFromResource(GFXTypes file, int resourceVal, bool transp return ret; } - private Image BitmapFromResource(GFXTypes file, int resourceVal, bool transparent) + private Texture2D LoadTexture(GFXTypes file, int resourceVal, bool transparent) { - var ret = (Image)_gfxLoader.LoadGFX(file, resourceVal); + var rawData = _gfxLoader.LoadGFX(file, resourceVal); + + if (rawData.IsEmpty) + return new Texture2D(_graphicsDeviceProvider.GraphicsDevice, 1, 1); + + Action processAction = null; if (transparent) { - // TODO: 0x000000 is supposed to clip hair below it - // need to figure out how to clip this - // if (file != GFXTypes.FemaleHat && file != GFXTypes.MaleHat) - CrossPlatformMakeTransparent(ret, Color.Black); + // for all gfx: 0x000000 is transparent + processAction = data => CrossPlatformMakeTransparent(data, Color.Black); // for hats: 0x080000 is transparent if (file == GFXTypes.FemaleHat || file == GFXTypes.MaleHat) { - CrossPlatformMakeTransparent(ret, Color.FromRgba(0x08, 0x00, 0x00, 0xFF)); - CrossPlatformMakeTransparent(ret, Color.FromRgba(0x00, 0x08, 0x00, 0xFF)); - CrossPlatformMakeTransparent(ret, Color.FromRgba(0x00, 0x00, 0x08, 0xFF)); + processAction = data => CrossPlatformMakeTransparent(data, + // TODO: 0x000000 is supposed to clip hair below it + new Color(0xff000000), + new Color(0xff080000), + new Color(0xff000800), + new Color(0xff000008)); } } + using var ms = rawData.AsStream(); + var ret = Texture2D.FromStream(_graphicsDeviceProvider.GraphicsDevice, ms, processAction); + return ret; } - private static void CrossPlatformMakeTransparent(Image bmp, Color transparentColor) + private static unsafe void CrossPlatformMakeTransparent(byte[] data, params Color[] transparentColors) { - var brush = new RecolorBrush(transparentColor, Color.Transparent, 0.0001f); - bmp.Mutate(x => x.Clear(brush)); + fixed (byte* ptr = data) + { + for (int i = 0; i < data.Length; i += 4) + { + uint* addr = (uint*)(ptr + i); + if (transparentColors.Contains(new Color(*addr))) + *addr = 0; + } + } } public void Dispose() From 8df81f2acb8aa861b63797d6590169e226be6314 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Mon, 8 May 2023 15:22:27 -0700 Subject: [PATCH 07/15] Fix off-by-one error in map item layer renderer --- .../Rendering/MapEntityRenderers/MapItemLayerRenderer.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/EndlessClient/Rendering/MapEntityRenderers/MapItemLayerRenderer.cs b/EndlessClient/Rendering/MapEntityRenderers/MapItemLayerRenderer.cs index 2d7072e11..2c23f3d30 100644 --- a/EndlessClient/Rendering/MapEntityRenderers/MapItemLayerRenderer.cs +++ b/EndlessClient/Rendering/MapEntityRenderers/MapItemLayerRenderer.cs @@ -45,8 +45,7 @@ public override void RenderElementAt(SpriteBatch spriteBatch, int row, int col, foreach (var item in items) { - //note: col is offset by 1. I'm not sure why this is needed. Maybe I did something wrong when translating the packets... - var itemPos = GetDrawCoordinatesFromGridUnits(col + 1, row); + var itemPos = GetDrawCoordinatesFromGridUnits(col, row); var itemTexture = _mapItemGraphicProvider.GetItemGraphic(item.ItemID, item.Amount); spriteBatch.Draw(itemTexture, @@ -57,10 +56,5 @@ public override void RenderElementAt(SpriteBatch spriteBatch, int row, int col, bool IsItemAt(MapItem item) => item.X == col && item.Y == row; } - - protected override Vector2 GetDrawCoordinatesFromGridUnits(int gridX, int gridY) - { - return _gridDrawCoordinateCalculator.CalculateBaseLayerDrawCoordinatesFromGridUnits(gridX, gridY); - } } } From 232c38b824d431f9ac44c4f64d781edf2f8dcd74 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Mon, 8 May 2023 16:38:06 -0700 Subject: [PATCH 08/15] Cache sprite data for NPCs. Reduces garbage generated from detecting the pixel below the mouse cursor. --- EndlessClient/Rendering/NPC/NPCRenderer.cs | 32 ++++---- .../Rendering/NPC/NPCRendererFactory.cs | 4 + .../Rendering/Sprites/NPCSpriteDataCache.cs | 77 +++++++++++++++++++ 3 files changed, 98 insertions(+), 15 deletions(-) create mode 100644 EndlessClient/Rendering/Sprites/NPCSpriteDataCache.cs diff --git a/EndlessClient/Rendering/NPC/NPCRenderer.cs b/EndlessClient/Rendering/NPC/NPCRenderer.cs index cb7f5ba5c..c003538f6 100644 --- a/EndlessClient/Rendering/NPC/NPCRenderer.cs +++ b/EndlessClient/Rendering/NPC/NPCRenderer.cs @@ -1,6 +1,4 @@ -using EndlessClient.Controllers; -using EndlessClient.GameExecution; -using EndlessClient.HUD.Spells; +using EndlessClient.GameExecution; using EndlessClient.Input; using EndlessClient.Rendering.Chat; using EndlessClient.Rendering.Effects; @@ -16,8 +14,8 @@ using Microsoft.Xna.Framework.Graphics; using Optional; using System; +using System.Collections.Generic; using System.Linq; -using System.Windows.Markup; using XNAControls; namespace EndlessClient.Rendering.NPC @@ -29,6 +27,7 @@ public class NPCRenderer : DrawableGameComponent, INPCRenderer private readonly IClientWindowSizeProvider _clientWindowSizeProvider; private readonly IENFFileProvider _enfFileProvider; private readonly INPCSpriteSheet _npcSpriteSheet; + private readonly INPCSpriteDataCache _npcSpriteDataCache; private readonly IGridDrawCoordinateCalculator _gridDrawCoordinateCalculator; private readonly IHealthBarRendererFactory _healthBarRendererFactory; private readonly IChatBubbleFactory _chatBubbleFactory; @@ -67,6 +66,7 @@ public class NPCRenderer : DrawableGameComponent, INPCRenderer IClientWindowSizeProvider clientWindowSizeProvider, IENFFileProvider enfFileProvider, INPCSpriteSheet npcSpriteSheet, + INPCSpriteDataCache npcSpriteDataCache, IGridDrawCoordinateCalculator gridDrawCoordinateCalculator, IHealthBarRendererFactory healthBarRendererFactory, IChatBubbleFactory chatBubbleFactory, @@ -80,6 +80,7 @@ public class NPCRenderer : DrawableGameComponent, INPCRenderer _clientWindowSizeProvider = clientWindowSizeProvider; _enfFileProvider = enfFileProvider; _npcSpriteSheet = npcSpriteSheet; + _npcSpriteDataCache = npcSpriteDataCache; _gridDrawCoordinateCalculator = gridDrawCoordinateCalculator; _healthBarRendererFactory = healthBarRendererFactory; _chatBubbleFactory = chatBubbleFactory; @@ -131,10 +132,9 @@ public override void Initialize() _spriteBatch = new SpriteBatch(Game.GraphicsDevice); - var frameTexture = _npcSpriteSheet.GetNPCTexture(_enfFileProvider.ENFFile[NPC.ID].Graphic, NPC.Frame, NPC.Direction); - var data = new Color[frameTexture.Width * frameTexture.Height]; - frameTexture.GetData(data); - _isBlankSprite = data.All(x => x.A == 0); + var graphic = _enfFileProvider.ENFFile[NPC.ID].Graphic; + _npcSpriteDataCache.Populate(graphic); + _isBlankSprite = _npcSpriteDataCache.IsBlankSprite(graphic); base.Initialize(); } @@ -169,16 +169,18 @@ public override void Update(GameTime gameTime) public bool IsClickablePixel(Point currentMousePosition) { - var currentFrame = _npcSpriteSheet.GetNPCTexture(_enfFileProvider.ENFFile[NPC.ID].Graphic, NPC.Frame, NPC.Direction); - - var colorData = new Color[] { Color.FromNonPremultiplied(0, 0, 0, 255) }; - if (currentFrame != null && !_isBlankSprite) + var cachedTexture = _npcSpriteDataCache.GetData(_enfFileProvider.ENFFile[NPC.ID].Graphic, NPC.Frame); + if (!_isBlankSprite && cachedTexture.Length > 0 && _npcRenderTarget.Bounds.Contains(currentMousePosition)) { - if (_npcRenderTarget.Bounds.Contains(currentMousePosition)) - _npcRenderTarget.GetData(0, new Rectangle(currentMousePosition.X, currentMousePosition.Y, 1, 1), colorData, 0, 1); + 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; } - return _isBlankSprite || colorData[0].A > 0; + return true; } public void DrawToSpriteBatch(SpriteBatch spriteBatch) diff --git a/EndlessClient/Rendering/NPC/NPCRendererFactory.cs b/EndlessClient/Rendering/NPC/NPCRendererFactory.cs index 0f636ebe1..9f69cbc9a 100644 --- a/EndlessClient/Rendering/NPC/NPCRendererFactory.cs +++ b/EndlessClient/Rendering/NPC/NPCRendererFactory.cs @@ -16,6 +16,7 @@ public class NPCRendererFactory : INPCRendererFactory private readonly IClientWindowSizeProvider _clientWindowSizeProvider; private readonly IENFFileProvider _enfFileProvider; private readonly INPCSpriteSheet _npcSpriteSheet; + private readonly INPCSpriteDataCache _npcSpriteDataCache; private readonly IGridDrawCoordinateCalculator _gridDrawCoordinateCalculator; private readonly IHealthBarRendererFactory _healthBarRendererFactory; private readonly IChatBubbleFactory _chatBubbleFactory; @@ -27,6 +28,7 @@ public class NPCRendererFactory : INPCRendererFactory IClientWindowSizeProvider clientWindowSizeProvider, IENFFileProvider enfFileProvider, INPCSpriteSheet npcSpriteSheet, + INPCSpriteDataCache npcSpriteDataCache, IGridDrawCoordinateCalculator gridDrawCoordinateCalculator, IHealthBarRendererFactory healthBarRendererFactory, IChatBubbleFactory chatBubbleFactory, @@ -38,6 +40,7 @@ public class NPCRendererFactory : INPCRendererFactory _clientWindowSizeProvider = clientWindowSizeProvider; _enfFileProvider = enfFileProvider; _npcSpriteSheet = npcSpriteSheet; + _npcSpriteDataCache = npcSpriteDataCache; _gridDrawCoordinateCalculator = gridDrawCoordinateCalculator; _healthBarRendererFactory = healthBarRendererFactory; _chatBubbleFactory = chatBubbleFactory; @@ -52,6 +55,7 @@ public INPCRenderer CreateRendererFor(EOLib.Domain.NPC.NPC npc) _clientWindowSizeProvider, _enfFileProvider, _npcSpriteSheet, + _npcSpriteDataCache, _gridDrawCoordinateCalculator, _healthBarRendererFactory, _chatBubbleFactory, diff --git a/EndlessClient/Rendering/Sprites/NPCSpriteDataCache.cs b/EndlessClient/Rendering/Sprites/NPCSpriteDataCache.cs new file mode 100644 index 000000000..f2cd50224 --- /dev/null +++ b/EndlessClient/Rendering/Sprites/NPCSpriteDataCache.cs @@ -0,0 +1,77 @@ +using AutomaticTypeMapper; +using EOLib; +using EOLib.Domain.NPC; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EndlessClient.Rendering.Sprites +{ + [AutoMappedType(IsSingleton = true)] + public class NPCSpriteDataCache : INPCSpriteDataCache + { + private readonly INPCSpriteSheet _npcSpriteSheet; + private readonly Dictionary>> _spriteData; + + public NPCSpriteDataCache(INPCSpriteSheet npcSpriteSheet) + { + _npcSpriteSheet = npcSpriteSheet; + _spriteData = new Dictionary>>(); + } + + public void Populate(int graphic) + { + if (_spriteData.ContainsKey(graphic)) + return; + + _spriteData[graphic] = new Dictionary>(); + + foreach (NPCFrame frame in Enum.GetValues(typeof(NPCFrame))) + { + var text = _npcSpriteSheet.GetNPCTexture(graphic, frame, EODirection.Down); + var data = Array.Empty(); + + if (text != null) + { + data = new Color[text.Width * text.Height]; + text.GetData(data); + } + + _spriteData[graphic][frame] = data; + } + + } + + public ReadOnlySpan GetData(int graphic, NPCFrame frame) + { + if (!_spriteData.ContainsKey(graphic)) + { + Populate(graphic); + } + + return _spriteData[graphic][frame].Span; + } + + public bool IsBlankSprite(int graphic) + { + if (!_spriteData.ContainsKey(graphic)) + { + Populate(graphic); + } + + return _spriteData[graphic][NPCFrame.Standing].Span.ToArray().Any(AlphaIsZero); + } + + private static bool AlphaIsZero(Color input) => input.A == 0; + } + + public interface INPCSpriteDataCache + { + void Populate(int graphic); + + ReadOnlySpan GetData(int graphic, NPCFrame frame); + + bool IsBlankSprite(int graphic); + } +} From 8e05a2723b6143b80db1fb089244229ffab22265 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Mon, 8 May 2023 21:40:21 -0700 Subject: [PATCH 09/15] Implement LRU cache for NPC sprite data. Prevents large number of NPCs from consuming too much memory. --- .../Rendering/Map/MapChangedActions.cs | 18 ++++++++ .../Rendering/Sprites/NPCSpriteDataCache.cs | 43 ++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/EndlessClient/Rendering/Map/MapChangedActions.cs b/EndlessClient/Rendering/Map/MapChangedActions.cs index a15163e2f..c67ce197d 100644 --- a/EndlessClient/Rendering/Map/MapChangedActions.cs +++ b/EndlessClient/Rendering/Map/MapChangedActions.cs @@ -6,12 +6,15 @@ using EndlessClient.Rendering.Character; using EndlessClient.Rendering.Effects; using EndlessClient.Rendering.NPC; +using EndlessClient.Rendering.Sprites; using EOLib.Config; using EOLib.Domain.Chat; using EOLib.Domain.Map; using EOLib.Domain.Notifiers; using EOLib.IO.Map; +using EOLib.IO.Repositories; using EOLib.Localization; +using System.Linq; namespace EndlessClient.Rendering.Map { @@ -21,8 +24,10 @@ public class MapChangedActions : IMapChangedNotifier, IMapChangedActions { private readonly ICharacterStateCache _characterStateCache; private readonly INPCStateCache _npcStateCache; + private readonly INPCSpriteDataCache _npcSpriteDataCache; private readonly ICharacterRendererRepository _characterRendererRepository; private readonly INPCRendererRepository _npcRendererRepository; + private readonly IENFFileProvider _enfFileProvider; private readonly IHudControlProvider _hudControlProvider; private readonly IChatRepository _chatRepository; private readonly ILocalizedStringFinder _localizedStringFinder; @@ -35,8 +40,10 @@ public class MapChangedActions : IMapChangedNotifier, IMapChangedActions public MapChangedActions(ICharacterStateCache characterStateCache, INPCStateCache npcStateCache, + INPCSpriteDataCache npcSpriteDataCache, ICharacterRendererRepository characterRendererRepository, INPCRendererRepository npcRendererRepository, + IENFFileProvider enfFileProvider, IHudControlProvider hudControlProvider, IChatRepository chatRepository, ILocalizedStringFinder localizedStringFinder, @@ -49,8 +56,10 @@ public class MapChangedActions : IMapChangedNotifier, IMapChangedActions { _characterStateCache = characterStateCache; _npcStateCache = npcStateCache; + _npcSpriteDataCache = npcSpriteDataCache; _characterRendererRepository = characterRendererRepository; _npcRendererRepository = npcRendererRepository; + _enfFileProvider = enfFileProvider; _hudControlProvider = hudControlProvider; _chatRepository = chatRepository; _localizedStringFinder = localizedStringFinder; @@ -130,6 +139,15 @@ private void ClearCharacterRenderersAndCache() private void ClearNPCRenderersAndCache() { + var currentMapNpcGraphics = _currentMapStateRepository.NPCs.Select(x => _enfFileProvider.ENFFile[x.ID].Graphic).ToList(); + var priorMapNpcGraphics = _npcRendererRepository.NPCRenderers.Select(x => _enfFileProvider.ENFFile[x.Value.NPC.ID].Graphic); + + foreach (var evict in priorMapNpcGraphics.Except(currentMapNpcGraphics)) + _npcSpriteDataCache.MarkForEviction(evict); + + foreach (var unevict in currentMapNpcGraphics) + _npcSpriteDataCache.UnmarkForEviction(unevict); + foreach (var npcRenderer in _npcRendererRepository.NPCRenderers) npcRenderer.Value.Dispose(); _npcRendererRepository.NPCRenderers.Clear(); diff --git a/EndlessClient/Rendering/Sprites/NPCSpriteDataCache.cs b/EndlessClient/Rendering/Sprites/NPCSpriteDataCache.cs index f2cd50224..bc454e27f 100644 --- a/EndlessClient/Rendering/Sprites/NPCSpriteDataCache.cs +++ b/EndlessClient/Rendering/Sprites/NPCSpriteDataCache.cs @@ -11,13 +11,20 @@ namespace EndlessClient.Rendering.Sprites [AutoMappedType(IsSingleton = true)] public class NPCSpriteDataCache : INPCSpriteDataCache { + private const int CACHE_SIZE = 32; + private readonly INPCSpriteSheet _npcSpriteSheet; + private readonly Dictionary>> _spriteData; + private readonly List _lru; + private readonly HashSet _reclaimable; public NPCSpriteDataCache(INPCSpriteSheet npcSpriteSheet) { _npcSpriteSheet = npcSpriteSheet; - _spriteData = new Dictionary>>(); + _spriteData = new Dictionary>>(CACHE_SIZE); + _lru = new List(CACHE_SIZE); + _reclaimable = new HashSet(CACHE_SIZE); } public void Populate(int graphic) @@ -25,7 +32,28 @@ public void Populate(int graphic) if (_spriteData.ContainsKey(graphic)) return; + if (_lru.Count >= CACHE_SIZE && _reclaimable.Count > 0) + { + // find and "reclaim" the first available candidate based on the order they were added to the LRU + // 'reclaimable' candidates are updated when the map changes + // candidates will never be NPCs that are on the current map + // a map with >= CACHE_SIZE different NPCs will cause problems here + for (int i = 0; i < _lru.Count; i++) + { + var candidate = _lru[i]; + if (_reclaimable.Contains(candidate)) + { + _spriteData.Remove(candidate); + _reclaimable.Remove(candidate); + _lru.RemoveAt(i); + break; + } + } + } + _spriteData[graphic] = new Dictionary>(); + _reclaimable.Remove(graphic); + _lru.Add(graphic); foreach (NPCFrame frame in Enum.GetValues(typeof(NPCFrame))) { @@ -40,7 +68,16 @@ public void Populate(int graphic) _spriteData[graphic][frame] = data; } + } + public void MarkForEviction(int graphic) + { + _reclaimable.Add(graphic); + } + + public void UnmarkForEviction(int graphic) + { + _reclaimable.Remove(graphic); } public ReadOnlySpan GetData(int graphic, NPCFrame frame) @@ -70,6 +107,10 @@ public interface INPCSpriteDataCache { void Populate(int graphic); + void MarkForEviction(int graphic); + + void UnmarkForEviction(int graphic); + ReadOnlySpan GetData(int graphic, NPCFrame frame); bool IsBlankSprite(int graphic); From 2ff8af3037778313a146565f1393f5cab973c9c3 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 9 May 2023 01:08:47 -0700 Subject: [PATCH 10/15] Fix timestamp generation on non-windows --- EOLib/GameStartTimeRepository.cs | 7 +------ EOLib/misc.cs | 7 ++++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/EOLib/GameStartTimeRepository.cs b/EOLib/GameStartTimeRepository.cs index 38c656c23..89120a571 100644 --- a/EOLib/GameStartTimeRepository.cs +++ b/EOLib/GameStartTimeRepository.cs @@ -1,13 +1,10 @@ using AutomaticTypeMapper; -using System; using System.Diagnostics; namespace EOLib { public interface IGameStartTimeProvider { - DateTime StartTime { get; } - Stopwatch Elapsed { get; } int TimeStamp { get; } @@ -16,10 +13,8 @@ public interface IGameStartTimeProvider [AutoMappedType(IsSingleton = true)] public class GameStartTimeRepository : IGameStartTimeProvider { - public DateTime StartTime { get; } = DateTime.UtcNow; - public Stopwatch Elapsed { get; } = Stopwatch.StartNew(); - public int TimeStamp => StartTime.ToEOTimeStamp(Elapsed.ElapsedTicks); + public int TimeStamp => Elapsed.ToEOTimeStamp(); } } diff --git a/EOLib/misc.cs b/EOLib/misc.cs index 919ce6f62..01aee3b41 100644 --- a/EOLib/misc.cs +++ b/EOLib/misc.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; namespace EOLib { @@ -20,10 +21,10 @@ public static T[] SubArray(this T[] arr, int offset, int count) public static class DateTimeExtension { - public static int ToEOTimeStamp(this DateTime dt, long elapsedTicks) + public static int ToEOTimeStamp(this Stopwatch sw) { - dt = dt.Add(TimeSpan.FromTicks(elapsedTicks)); - return dt.Hour * 360000 + dt.Minute * 6000 + dt.Second * 100 + dt.Millisecond / 10; + var elapsedHundreths = Math.Round(sw.ElapsedTicks / (Stopwatch.Frequency / 100.0)); + return (int)elapsedHundreths; } } From 859365f45d2217a88496221fd7bb6fa000a1d0f5 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 9 May 2023 11:23:17 -0700 Subject: [PATCH 11/15] Update all animation timing to use tick-based approach. Fixes animation timing issues and makes behavior more consistent with vanilla client --- EOLib/Domain/Character/CharacterActions.cs | 18 +++--- EOLib/FixedTimeStepRepository.cs | 28 +++++++++ EOLib/GameStartTimeRepository.cs | 20 ------ .../ControlSets/ControlSetFactory.cs | 6 +- .../ControlSets/LoggedInControlSet.cs | 5 +- EndlessClient/GameExecution/EndlessGame.cs | 20 +++--- .../HUD/Controls/HudControlsFactory.cs | 12 ++-- .../Input/PreviousUserInputTracker.cs | 9 +-- .../Rendering/Character/CharacterAnimator.cs | 63 +++++++++---------- .../Rendering/Character/CharacterRenderer.cs | 25 +++----- .../Character/CharacterRendererUpdater.cs | 8 ++- .../Character/CharacterStateCache.cs | 7 ++- .../Factories/CharacterRendererFactory.cs | 12 +--- .../Rendering/Factories/MapRendererFactory.cs | 2 +- .../Rendering/FixedTimeStepRepository.cs | 38 ----------- EndlessClient/Rendering/Map/MapRenderer.cs | 1 + EndlessClient/Rendering/NPC/NPCAnimator.cs | 26 ++++---- .../Rendering/RenderFrameActionTime.cs | 11 ++-- 18 files changed, 131 insertions(+), 180 deletions(-) create mode 100644 EOLib/FixedTimeStepRepository.cs delete mode 100644 EOLib/GameStartTimeRepository.cs delete mode 100644 EndlessClient/Rendering/FixedTimeStepRepository.cs diff --git a/EOLib/Domain/Character/CharacterActions.cs b/EOLib/Domain/Character/CharacterActions.cs index 5e2106f04..e01db6ac9 100644 --- a/EOLib/Domain/Character/CharacterActions.cs +++ b/EOLib/Domain/Character/CharacterActions.cs @@ -16,17 +16,17 @@ public class CharacterActions : ICharacterActions private readonly IPacketSendService _packetSendService; private readonly ICharacterRepository _characterRepository; private readonly IESFFileProvider _spellFileProvider; - private readonly IGameStartTimeProvider _gameStartTimeProvider; + private readonly IFixedTimeStepRepository _fixedTimeStepRepository; public CharacterActions(IPacketSendService packetSendService, ICharacterRepository characterRepository, IESFFileProvider spellFileProvider, - IGameStartTimeProvider gameStartTimeProvider) + IFixedTimeStepRepository fixedTimeStepRepository) { _packetSendService = packetSendService; _characterRepository = characterRepository; _spellFileProvider = spellFileProvider; - _gameStartTimeProvider = gameStartTimeProvider; + _fixedTimeStepRepository = fixedTimeStepRepository; } public void Face(EODirection direction) @@ -46,7 +46,7 @@ public void Walk() var packet = new PacketBuilder(PacketFamily.Walk, admin ? PacketAction.Admin : PacketAction.Player) .AddChar((int)renderProperties.Direction) - .AddThree(_gameStartTimeProvider.TimeStamp) + .AddThree((int)_fixedTimeStepRepository.TickCount) .AddChar(renderProperties.GetDestinationX()) .AddChar(renderProperties.GetDestinationY()) .Build(); @@ -62,7 +62,7 @@ public void Attack() var packet = new PacketBuilder(PacketFamily.Attack, PacketAction.Use) .AddChar((int) _characterRepository.MainCharacter.RenderProperties.Direction) - .AddThree(_gameStartTimeProvider.TimeStamp) + .AddThree((int)_fixedTimeStepRepository.TickCount) .Build(); _packetSendService.SendPacket(packet); @@ -103,7 +103,7 @@ public void PrepareCastSpell(int spellId) { var packet = new PacketBuilder(PacketFamily.Spell, PacketAction.Request) .AddShort(spellId) - .AddThree(_gameStartTimeProvider.TimeStamp) + .AddThree((int)_fixedTimeStepRepository.TickCount) .Build(); _packetSendService.SendPacket(packet); @@ -131,7 +131,7 @@ public void CastSpell(int spellId, ISpellTargetable target) { builder = builder .AddShort(spellId) - .AddThree(_gameStartTimeProvider.TimeStamp); + .AddThree((int)_fixedTimeStepRepository.TickCount); } else { @@ -149,13 +149,13 @@ public void CastSpell(int spellId, ISpellTargetable target) .AddShort(1) // unknown .AddShort(spellId) .AddShort(target.Index) - .AddThree(_gameStartTimeProvider.TimeStamp); + .AddThree((int)_fixedTimeStepRepository.TickCount); } else { builder = builder .AddShort(spellId) - .AddInt(_gameStartTimeProvider.TimeStamp); + .AddInt((int)_fixedTimeStepRepository.TickCount); } } diff --git a/EOLib/FixedTimeStepRepository.cs b/EOLib/FixedTimeStepRepository.cs new file mode 100644 index 000000000..f89bbb7b6 --- /dev/null +++ b/EOLib/FixedTimeStepRepository.cs @@ -0,0 +1,28 @@ +using AutomaticTypeMapper; + +namespace EOLib +{ + [AutoMappedType(IsSingleton = true)] + public class FixedTimeStepRepository : IFixedTimeStepRepository + { + public const double TICK_TIME_MS = 10.0; + + public ulong TickCount { get; private set; } + + public bool IsWalkUpdateFrame => TickCount % 4 == 0; + + public void Tick() + { + TickCount++; + } + } + + public interface IFixedTimeStepRepository + { + public ulong TickCount { get; } + + bool IsWalkUpdateFrame { get; } + + void Tick(); + } +} diff --git a/EOLib/GameStartTimeRepository.cs b/EOLib/GameStartTimeRepository.cs deleted file mode 100644 index 89120a571..000000000 --- a/EOLib/GameStartTimeRepository.cs +++ /dev/null @@ -1,20 +0,0 @@ -using AutomaticTypeMapper; -using System.Diagnostics; - -namespace EOLib -{ - public interface IGameStartTimeProvider - { - Stopwatch Elapsed { get; } - - int TimeStamp { get; } - } - - [AutoMappedType(IsSingleton = true)] - public class GameStartTimeRepository : IGameStartTimeProvider - { - public Stopwatch Elapsed { get; } = Stopwatch.StartNew(); - - public int TimeStamp => Elapsed.ToEOTimeStamp(); - } -} diff --git a/EndlessClient/ControlSets/ControlSetFactory.cs b/EndlessClient/ControlSets/ControlSetFactory.cs index 1d9cd9755..4e5bf2dfb 100644 --- a/EndlessClient/ControlSets/ControlSetFactory.cs +++ b/EndlessClient/ControlSets/ControlSetFactory.cs @@ -29,7 +29,6 @@ public class ControlSetFactory : IControlSetFactory private readonly IUserInputRepository _userInputRepository; private readonly IActiveDialogRepository _activeDialogRepository; private readonly IClientWindowSizeRepository _clientWindowSizeRepository; - private readonly IFixedTimeStepRepository _fixedTimeStepRepository; private IMainButtonController _mainButtonController; private IAccountController _accountController; @@ -46,8 +45,7 @@ public class ControlSetFactory : IControlSetFactory IEndlessGameProvider endlessGameProvider, IUserInputRepository userInputRepository, IActiveDialogRepository activeDialogRepository, - IClientWindowSizeRepository clientWindowSizeRepository, - IFixedTimeStepRepository fixedTimeStepRepository) + IClientWindowSizeRepository clientWindowSizeRepository) { _nativeGraphicsManager = nativeGraphicsManager; _messageBoxFactory = messageBoxFactory; @@ -60,7 +58,6 @@ public class ControlSetFactory : IControlSetFactory _userInputRepository = userInputRepository; _activeDialogRepository = activeDialogRepository; _clientWindowSizeRepository = clientWindowSizeRepository; - _fixedTimeStepRepository = fixedTimeStepRepository; } public IControlSet CreateControlsForState(GameStates newState, IControlSet currentControlSet) @@ -105,7 +102,6 @@ private IControlSet GetSetBasedOnState(GameStates newState) _accountController, _endlessGameProvider, _userInputRepository, - _fixedTimeStepRepository, _clientWindowSizeRepository); case GameStates.PlayingTheGame: return new InGameControlSet(_mainButtonController, _messageBoxFactory, _hudControlsFactory, _activeDialogRepository, _clientWindowSizeRepository); diff --git a/EndlessClient/ControlSets/LoggedInControlSet.cs b/EndlessClient/ControlSets/LoggedInControlSet.cs index b45b74c32..1a6cd2dd4 100644 --- a/EndlessClient/ControlSets/LoggedInControlSet.cs +++ b/EndlessClient/ControlSets/LoggedInControlSet.cs @@ -20,7 +20,6 @@ public class LoggedInControlSet : IntermediateControlSet private readonly IAccountController _accountController; private readonly IEndlessGameProvider _endlessGameProvider; private readonly IUserInputRepository _userInputRepository; - private readonly IFixedTimeStepRepository _fixedTimeStepRepository; private readonly List _characterInfoPanels; private IXNAButton _changePasswordButton; @@ -36,7 +35,6 @@ public class LoggedInControlSet : IntermediateControlSet IAccountController accountController, IEndlessGameProvider endlessGameProvider, IUserInputRepository userInputRepository, - IFixedTimeStepRepository fixedTimeStepRepository, IClientWindowSizeRepository clientWindowSizeRepository) : base(mainButtonController, clientWindowSizeRepository) { @@ -46,7 +44,6 @@ public class LoggedInControlSet : IntermediateControlSet _accountController = accountController; _endlessGameProvider = endlessGameProvider; _userInputRepository = userInputRepository; - _fixedTimeStepRepository = fixedTimeStepRepository; _characterInfoPanels = new List(); } @@ -58,7 +55,7 @@ protected override void InitializeControlsHelper(IControlSet currentControlSet) _characterInfoPanels.AddRange(_characterInfoPanelFactory.CreatePanels(_characterSelectorProvider.Characters)); _allComponents.Add(new CurrentUserInputTracker(_endlessGameProvider, _userInputRepository)); - _allComponents.Add(new PreviousUserInputTracker(_endlessGameProvider, _userInputRepository, _fixedTimeStepRepository)); + _allComponents.Add(new PreviousUserInputTracker(_endlessGameProvider, _userInputRepository)); _allComponents.Add(_changePasswordButton); _allComponents.AddRange(_characterInfoPanels); } diff --git a/EndlessClient/GameExecution/EndlessGame.cs b/EndlessClient/GameExecution/EndlessGame.cs index 801bc3d4c..1928a5170 100644 --- a/EndlessClient/GameExecution/EndlessGame.cs +++ b/EndlessClient/GameExecution/EndlessGame.cs @@ -1,9 +1,4 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using AutomaticTypeMapper; +using AutomaticTypeMapper; using EndlessClient.Audio; using EndlessClient.Content; using EndlessClient.ControlSets; @@ -22,6 +17,11 @@ using Microsoft.Xna.Framework.Input; using MonoGame.Extended.BitmapFonts; using MonoGame.Extended.Input; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; namespace EndlessClient.GameExecution { @@ -42,6 +42,7 @@ public class EndlessGame : Game, IEndlessGame private readonly IConfigurationProvider _configurationProvider; private readonly IMfxPlayer _mfxPlayer; private readonly IXnaControlSoundMapper _soundMapper; + private readonly IFixedTimeStepRepository _fixedTimeStepRepository; private GraphicsDeviceManager _graphicsDeviceManager; private KeyboardState _previousKeyState; @@ -67,7 +68,8 @@ public class EndlessGame : Game, IEndlessGame IShaderRepository shaderRepository, IConfigurationProvider configurationProvider, IMfxPlayer mfxPlayer, - IXnaControlSoundMapper soundMapper) + IXnaControlSoundMapper soundMapper, + IFixedTimeStepRepository fixedTimeStepRepository) { _windowSizeRepository = windowSizeRepository; _contentProvider = contentProvider; @@ -83,6 +85,7 @@ public class EndlessGame : Game, IEndlessGame _configurationProvider = configurationProvider; _mfxPlayer = mfxPlayer; _soundMapper = soundMapper; + _fixedTimeStepRepository = fixedTimeStepRepository; _graphicsDeviceManager = new GraphicsDeviceManager(this) { PreferredBackBufferWidth = ClientWindowSizeRepository.DEFAULT_BACKBUFFER_WIDTH, @@ -183,7 +186,7 @@ protected override void Update(GameTime gameTime) // Some game components rely on slower update times. 60FPS was the original, but 12ms factors nicely in 120ms "ticks" // See: https://github.com/ethanmoffat/EndlessClient/issues/199 // Using IsFixedTimeStep = true with TargetUpdateTime set to 60FPS also limits the draw rate, which is not desired - if ((gameTime.TotalGameTime - _lastFrameUpdate).TotalMilliseconds >= 12.0) + if ((gameTime.TotalGameTime - _lastFrameUpdate).TotalMilliseconds >= FixedTimeStepRepository.TICK_TIME_MS) { #if DEBUG var currentKeyState = Keyboard.GetState(); @@ -194,6 +197,7 @@ protected override void Update(GameTime gameTime) _previousKeyState = currentKeyState; #endif + _fixedTimeStepRepository.Tick(); try { diff --git a/EndlessClient/HUD/Controls/HudControlsFactory.cs b/EndlessClient/HUD/Controls/HudControlsFactory.cs index 5fedb29a2..30f6219f0 100644 --- a/EndlessClient/HUD/Controls/HudControlsFactory.cs +++ b/EndlessClient/HUD/Controls/HudControlsFactory.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using AutomaticTypeMapper; +using AutomaticTypeMapper; using EndlessClient.Audio; using EndlessClient.Content; using EndlessClient.Controllers; @@ -28,6 +25,9 @@ using EOLib.Localization; using EOLib.Net.Communication; using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; using XNAControls; namespace EndlessClient.HUD.Controls @@ -566,7 +566,7 @@ private ICharacterAnimator CreateCharacterAnimator() private INPCAnimator CreateNPCAnimator() { - return new NPCAnimator(_endlessGameProvider, _currentMapStateRepository); + return new NPCAnimator(_endlessGameProvider, _currentMapStateRepository, _fixedTimeStepRepository); } private IPeriodicEmoteHandler CreatePeriodicEmoteHandler(ICharacterAnimator characterAnimator) @@ -576,7 +576,7 @@ private IPeriodicEmoteHandler CreatePeriodicEmoteHandler(ICharacterAnimator char private PreviousUserInputTracker CreatePreviousUserInputTracker() { - return new PreviousUserInputTracker(_endlessGameProvider, _userInputRepository, _fixedTimeStepRepository); + return new PreviousUserInputTracker(_endlessGameProvider, _userInputRepository); } } } \ No newline at end of file diff --git a/EndlessClient/Input/PreviousUserInputTracker.cs b/EndlessClient/Input/PreviousUserInputTracker.cs index cc082c5f0..fe128f5a6 100644 --- a/EndlessClient/Input/PreviousUserInputTracker.cs +++ b/EndlessClient/Input/PreviousUserInputTracker.cs @@ -1,5 +1,4 @@ using EndlessClient.GameExecution; -using EndlessClient.Rendering; using Microsoft.Xna.Framework; namespace EndlessClient.Input @@ -7,16 +6,13 @@ namespace EndlessClient.Input public class PreviousUserInputTracker : GameComponent { private readonly IUserInputRepository _userInputRepository; - private readonly IFixedTimeStepRepository _fixedTimeStepRepository; public PreviousUserInputTracker( IEndlessGameProvider endlessGameProvider, - IUserInputRepository userInputRepository, - IFixedTimeStepRepository fixedTimeStepRepository) + IUserInputRepository userInputRepository) : base((Game)endlessGameProvider.Game) { _userInputRepository = userInputRepository; - _fixedTimeStepRepository = fixedTimeStepRepository; UpdateOrder = int.MaxValue; } @@ -25,9 +21,6 @@ public override void Update(GameTime gameTime) _userInputRepository.PreviousKeyState = _userInputRepository.CurrentKeyState; _userInputRepository.PreviousMouseState = _userInputRepository.CurrentMouseState; - if (_fixedTimeStepRepository.IsUpdateFrame) - _fixedTimeStepRepository.RestartTimer(); - base.Update(gameTime); } } diff --git a/EndlessClient/Rendering/Character/CharacterAnimator.cs b/EndlessClient/Rendering/Character/CharacterAnimator.cs index c97104075..e0ce97f7a 100644 --- a/EndlessClient/Rendering/Character/CharacterAnimator.cs +++ b/EndlessClient/Rendering/Character/CharacterAnimator.cs @@ -19,9 +19,9 @@ namespace EndlessClient.Rendering.Character { public class CharacterAnimator : GameComponent, ICharacterAnimator { - public const int WALK_FRAME_TIME_MS = 96; - public const int ATTACK_FRAME_TIME_MS = 108; - public const int FRAME_TIME_MS = 120; + public const int TICKS_PER_WALK_FRAME = 9; // 9 x10ms ticks per walk frame + public const int TICKS_PER_FRAME = 12; // 12 x10ms ticks per attack frame + public const int TICKS_PER_CAST_TIME = 48; private readonly ICharacterRepository _characterRepository; private readonly ICurrentMapStateRepository _currentMapStateRepository; @@ -40,7 +40,7 @@ public class CharacterAnimator : GameComponent, ICharacterAnimator private readonly Dictionary _otherPlayerStartSpellCastTimes; private readonly Dictionary _startEmoteTimes; - private Option _mainPlayerStartShoutTime; + private Option _mainPlayerStartShoutTick; private ESFRecord _shoutSpellData; private ISpellTargetable _spellTarget; @@ -79,18 +79,15 @@ public class CharacterAnimator : GameComponent, ICharacterAnimator public override void Update(GameTime gameTime) { - if (_fixedTimeStepRepository.IsUpdateFrame) + if (_fixedTimeStepRepository.IsWalkUpdateFrame) { - if (_fixedTimeStepRepository.IsWalkUpdateFrame) - { - AnimateCharacterWalking(); - } - - AnimateCharacterAttacking(); - AnimateCharacterSpells(); - AnimateCharacterEmotes(); + AnimateCharacterWalking(); } + AnimateCharacterAttacking(); + AnimateCharacterSpells(); + AnimateCharacterEmotes(); + base.Update(gameTime); } @@ -143,7 +140,7 @@ void doTheWalk() return; } - var startWalkingTime = new RenderFrameActionTime(_characterRepository.MainCharacter.ID, sfxCallback); + var startWalkingTime = new RenderFrameActionTime(_characterRepository.MainCharacter.ID, _fixedTimeStepRepository.TickCount, sfxCallback); _otherPlayerStartWalkingTimes.Add(_characterRepository.MainCharacter.ID, startWalkingTime); _characterActions.Walk(); @@ -164,16 +161,16 @@ public void StartMainCharacterAttackAnimation(Action sfxCallback) return; } - var startAttackingTime = new RenderFrameActionTime(_characterRepository.MainCharacter.ID, sfxCallback); + var startAttackingTime = new RenderFrameActionTime(_characterRepository.MainCharacter.ID, _fixedTimeStepRepository.TickCount, sfxCallback); _otherPlayerStartAttackingTimes.Add(_characterRepository.MainCharacter.ID, startAttackingTime); } public bool MainCharacterShoutSpellPrep(ESFRecord spellData, ISpellTargetable target) { - if (_mainPlayerStartShoutTime.HasValue) + if (_mainPlayerStartShoutTick.HasValue) return false; - _mainPlayerStartShoutTime = Option.Some(Stopwatch.StartNew()); + _mainPlayerStartShoutTick = Option.Some(_fixedTimeStepRepository.TickCount); _shoutSpellData = spellData; _spellTarget = target; return true; @@ -181,7 +178,7 @@ public bool MainCharacterShoutSpellPrep(ESFRecord spellData, ISpellTargetable ta public void MainCharacterCancelSpellPrep() { - _mainPlayerStartShoutTime = Option.None(); + _mainPlayerStartShoutTick = Option.None(); _shoutSpellData = null; _spellTarget = null; @@ -199,7 +196,7 @@ public void StartOtherCharacterWalkAnimation(int characterID, MapCoordinate dest return; } - var startWalkingTimeAndID = new RenderFrameActionTime(characterID); + var startWalkingTimeAndID = new RenderFrameActionTime(characterID, _fixedTimeStepRepository.TickCount); _otherPlayerStartWalkingTimes.Add(characterID, startWalkingTimeAndID); } @@ -211,7 +208,7 @@ public void StartOtherCharacterAttackAnimation(int characterID, Action sfxCallba return; } - var startAttackingTime = new RenderFrameActionTime(characterID, sfxCallback); + var startAttackingTime = new RenderFrameActionTime(characterID, _fixedTimeStepRepository.TickCount, sfxCallback); _otherPlayerStartAttackingTimes.Add(characterID, startAttackingTime); } @@ -227,7 +224,7 @@ public void StartOtherCharacterSpellCast(int characterID) _otherPlayerStartSpellCastTimes.Remove(characterID); } - var startAttackingTimeAndID = new RenderFrameActionTime(characterID); + var startAttackingTimeAndID = new RenderFrameActionTime(characterID, _fixedTimeStepRepository.TickCount); _otherPlayerStartSpellCastTimes.Add(characterID, startAttackingTimeAndID); } @@ -239,7 +236,7 @@ public bool Emote(int characterID, Emote whichEmote) _startEmoteTimes.ContainsKey(characterID)) return false; - var startEmoteTime = new RenderFrameActionTime(characterID); + var startEmoteTime = new RenderFrameActionTime(characterID, _fixedTimeStepRepository.TickCount); if (characterID == _characterRepository.MainCharacter.ID) { var rp = _characterRepository.MainCharacter.RenderProperties.WithEmote(whichEmote); @@ -285,7 +282,7 @@ private void AnimateCharacterWalking() { var sendWalk = false; - if (pair.ActionTimer.ElapsedMilliseconds >= WALK_FRAME_TIME_MS) + if ((_fixedTimeStepRepository.TickCount - pair.ActionTick) >= TICKS_PER_WALK_FRAME) { GetCurrentCharacterFromRepository(pair).Match( none: () => playersDoneWalking.Add(pair.UniqueID), @@ -294,7 +291,7 @@ private void AnimateCharacterWalking() var renderProperties = currentCharacter.RenderProperties; var nextFrameRenderProperties = AnimateOneWalkFrame(renderProperties); - pair.UpdateActionStartTime(); + pair.UpdateActionStartTime(_fixedTimeStepRepository.TickCount); if (nextFrameRenderProperties.IsActing(CharacterActionState.Standing)) { var isMainCharacter = currentCharacter == _characterRepository.MainCharacter; @@ -435,7 +432,7 @@ private void AnimateCharacterAttacking() var playersDoneAttacking = new HashSet(); foreach (var pair in _otherPlayerStartAttackingTimes.Values) { - if (pair.ActionTimer.ElapsedMilliseconds >= ATTACK_FRAME_TIME_MS) + if ((_fixedTimeStepRepository.TickCount - pair.ActionTick) >= TICKS_PER_FRAME) { GetCurrentCharacterFromRepository(pair).Match( none: () => playersDoneAttacking.Add(pair.UniqueID), @@ -447,7 +444,7 @@ private void AnimateCharacterAttacking() if (nextFrameRenderProperties.ActualAttackFrame == 2) pair.SoundEffect(); - pair.UpdateActionStartTime(); + pair.UpdateActionStartTime(_fixedTimeStepRepository.TickCount); if (nextFrameRenderProperties.IsActing(CharacterActionState.Standing)) { if (pair.Replay) @@ -478,11 +475,11 @@ private void AnimateCharacterAttacking() private void AnimateCharacterSpells() { - _mainPlayerStartShoutTime.MatchSome(t => + _mainPlayerStartShoutTick.MatchSome(startTick => { - if (t.ElapsedMilliseconds >= _shoutSpellData.CastTime * 480) + if (_fixedTimeStepRepository.TickCount - startTick >= (ulong)(_shoutSpellData.CastTime * TICKS_PER_CAST_TIME)) { - _otherPlayerStartSpellCastTimes.Add(_characterRepository.MainCharacter.ID, new RenderFrameActionTime(_characterRepository.MainCharacter.ID)); + _otherPlayerStartSpellCastTimes.Add(_characterRepository.MainCharacter.ID, new RenderFrameActionTime(_characterRepository.MainCharacter.ID, _fixedTimeStepRepository.TickCount)); _characterActions.CastSpell(_shoutSpellData.ID, _spellTarget); MainCharacterCancelSpellPrep(); @@ -494,7 +491,7 @@ private void AnimateCharacterSpells() var playersDoneCasting = new HashSet(); foreach (var pair in _otherPlayerStartSpellCastTimes.Values) { - if (pair.ActionTimer.ElapsedMilliseconds >= FRAME_TIME_MS) + if ((_fixedTimeStepRepository.TickCount - pair.ActionTick) >= TICKS_PER_FRAME) { GetCurrentCharacterFromRepository(pair).Match( none: () => playersDoneCasting.Add(pair.UniqueID), @@ -503,7 +500,7 @@ private void AnimateCharacterSpells() var renderProperties = currentCharacter.RenderProperties; var nextFrameRenderProperties = renderProperties.WithNextSpellCastFrame(); - pair.UpdateActionStartTime(); + pair.UpdateActionStartTime(_fixedTimeStepRepository.TickCount); if (nextFrameRenderProperties.IsActing(CharacterActionState.Standing)) playersDoneCasting.Add(pair.UniqueID); @@ -526,7 +523,7 @@ private void AnimateCharacterEmotes() var playersDoneEmoting = new HashSet(); foreach (var pair in _startEmoteTimes.Values) { - if (pair.ActionTimer.ElapsedMilliseconds >= FRAME_TIME_MS * 2) + if ((pair.ActionTick - _fixedTimeStepRepository.TickCount) >= TICKS_PER_FRAME * 2) { GetCurrentCharacterFromRepository(pair).Match( none: () => playersDoneEmoting.Add(pair.UniqueID), @@ -535,7 +532,7 @@ private void AnimateCharacterEmotes() var renderProperties = currentCharacter.RenderProperties; var nextFrameRenderProperties = renderProperties.WithNextEmoteFrame(); - pair.UpdateActionStartTime(); + pair.UpdateActionStartTime(_fixedTimeStepRepository.TickCount); if (nextFrameRenderProperties.IsActing(CharacterActionState.Standing, CharacterActionState.Sitting)) playersDoneEmoting.Add(pair.UniqueID); diff --git a/EndlessClient/Rendering/Character/CharacterRenderer.cs b/EndlessClient/Rendering/Character/CharacterRenderer.cs index 591adbe20..89bd3a8ed 100644 --- a/EndlessClient/Rendering/Character/CharacterRenderer.cs +++ b/EndlessClient/Rendering/Character/CharacterRenderer.cs @@ -34,13 +34,11 @@ public class CharacterRenderer : DrawableGameComponent, ICharacterRenderer private readonly IRenderOffsetCalculator _renderOffsetCalculator; private readonly ICharacterPropertyRendererBuilder _characterPropertyRendererBuilder; private readonly ICharacterTextures _characterTextures; - private readonly ICharacterSpriteCalculator _characterSpriteCalculator; private readonly IGameStateProvider _gameStateProvider; private readonly ICurrentMapProvider _currentMapProvider; private readonly IUserInputProvider _userInputProvider; private readonly ISfxPlayer _sfxPlayer; private readonly IClientWindowSizeRepository _clientWindowSizeRepository; - private readonly IFixedTimeStepRepository _fixedTimeStepRepository; private readonly IEffectRenderer _effectRenderer; private EOLib.Domain.Character.Character _character; @@ -91,15 +89,13 @@ public EOLib.Domain.Character.Character Character IRenderOffsetCalculator renderOffsetCalculator, ICharacterPropertyRendererBuilder characterPropertyRendererBuilder, ICharacterTextures characterTextures, - ICharacterSpriteCalculator characterSpriteCalculator, EOLib.Domain.Character.Character character, IGameStateProvider gameStateProvider, ICurrentMapProvider currentMapProvider, IUserInputProvider userInputProvider, IEffectRendererFactory effectRendererFactory, ISfxPlayer sfxPlayer, - IClientWindowSizeRepository clientWindowSizeRepository, - IFixedTimeStepRepository fixedTimeStepRepository) + IClientWindowSizeRepository clientWindowSizeRepository) : base(game) { _renderTargetFactory = renderTargetFactory; @@ -109,7 +105,6 @@ public EOLib.Domain.Character.Character Character _renderOffsetCalculator = renderOffsetCalculator; _characterPropertyRendererBuilder = characterPropertyRendererBuilder; _characterTextures = characterTextures; - _characterSpriteCalculator = characterSpriteCalculator; _character = character; _gameStateProvider = gameStateProvider; _currentMapProvider = currentMapProvider; @@ -117,7 +112,6 @@ public EOLib.Domain.Character.Character Character _effectRenderer = effectRendererFactory.Create(); _sfxPlayer = sfxPlayer; _clientWindowSizeRepository = clientWindowSizeRepository; - _fixedTimeStepRepository = fixedTimeStepRepository; _chatBubble = new Lazy(() => _chatBubbleFactory.CreateChatBubble(this)); @@ -188,18 +182,15 @@ public override void Update(GameTime gameTime) if (!Visible) return; - if (_fixedTimeStepRepository.IsUpdateFrame) - { - if (_positionIsRelative) - SetGridCoordinatePosition(); + if (_positionIsRelative) + SetGridCoordinatePosition(); - if (_textureUpdateRequired) - { - _characterTextures.Refresh(_character.RenderProperties); - DrawToRenderTarget(); + if (_textureUpdateRequired) + { + _characterTextures.Refresh(_character.RenderProperties); + DrawToRenderTarget(); - _textureUpdateRequired = false; - } + _textureUpdateRequired = false; } if (_gameStateProvider.CurrentState == GameStates.PlayingTheGame) diff --git a/EndlessClient/Rendering/Character/CharacterRendererUpdater.cs b/EndlessClient/Rendering/Character/CharacterRendererUpdater.cs index e08e8fa5c..e70539e42 100644 --- a/EndlessClient/Rendering/Character/CharacterRendererUpdater.cs +++ b/EndlessClient/Rendering/Character/CharacterRendererUpdater.cs @@ -1,6 +1,7 @@ using AutomaticTypeMapper; using EndlessClient.Rendering.Effects; using EndlessClient.Rendering.Factories; +using EOLib; using EOLib.Domain.Character; using EOLib.Domain.Map; using Microsoft.Xna.Framework; @@ -20,18 +21,21 @@ public class CharacterRendererUpdater : ICharacterRendererUpdater private readonly ICharacterRendererFactory _characterRendererFactory; private readonly ICharacterRendererRepository _characterRendererRepository; private readonly ICharacterStateCache _characterStateCache; + private readonly IFixedTimeStepRepository _fixedTimeStepRepository; public CharacterRendererUpdater(ICharacterProvider characterProvider, ICurrentMapStateRepository currentMapStateRepository, ICharacterRendererFactory characterRendererFactory, ICharacterRendererRepository characterRendererRepository, - ICharacterStateCache characterStateCache) + ICharacterStateCache characterStateCache, + IFixedTimeStepRepository fixedTimeStepRepository) { _characterProvider = characterProvider; _currentMapStateRepository = currentMapStateRepository; _characterRendererFactory = characterRendererFactory; _characterRendererRepository = characterRendererRepository; _characterStateCache = characterStateCache; + _fixedTimeStepRepository = fixedTimeStepRepository; } public void UpdateCharacters(GameTime gameTime) @@ -151,7 +155,7 @@ private void UpdateDeadCharacters() none: () => _characterStateCache.AddDeathStartTime(character.ID), some: actionTime => { - if (actionTime.ActionTimer.ElapsedMilliseconds >= 2) + if ((_fixedTimeStepRepository.TickCount - actionTime.ActionTick) >= 1) // prior value was 2ms; ticks are 12ms { _characterStateCache.RemoveDeathStartTime(character.ID); _characterStateCache.RemoveCharacterState(character.ID); diff --git a/EndlessClient/Rendering/Character/CharacterStateCache.cs b/EndlessClient/Rendering/Character/CharacterStateCache.cs index 01cf8f1cd..bbd518cf3 100644 --- a/EndlessClient/Rendering/Character/CharacterStateCache.cs +++ b/EndlessClient/Rendering/Character/CharacterStateCache.cs @@ -1,4 +1,5 @@ using AutomaticTypeMapper; +using EOLib; using Optional; using System; using System.Collections.Generic; @@ -15,16 +16,18 @@ public class CharacterStateCache : ICharacterStateCache private readonly Dictionary _otherCharacters; private readonly List _deathStartTimes; + private readonly IFixedTimeStepRepository _fixedTimeStepRepository; public IReadOnlyDictionary OtherCharacters => _otherCharacters; public IReadOnlyList DeathStartTimes => _deathStartTimes; - public CharacterStateCache() + public CharacterStateCache(IFixedTimeStepRepository fixedTimeStepRepository) { MainCharacter = Option.None(); _otherCharacters = new Dictionary(); _deathStartTimes = new List(); + _fixedTimeStepRepository = fixedTimeStepRepository; } public bool HasCharacterWithID(int id) @@ -52,7 +55,7 @@ public void AddDeathStartTime(int id) if (_deathStartTimes.Any(x => x.UniqueID == id)) throw new ArgumentException("That character already started dying...", nameof(id)); - _deathStartTimes.Add(new RenderFrameActionTime(id)); + _deathStartTimes.Add(new RenderFrameActionTime(id, _fixedTimeStepRepository.TickCount)); } public void RemoveDeathStartTime(int id) diff --git a/EndlessClient/Rendering/Factories/CharacterRendererFactory.cs b/EndlessClient/Rendering/Factories/CharacterRendererFactory.cs index 1539d71f2..815fc0d92 100644 --- a/EndlessClient/Rendering/Factories/CharacterRendererFactory.cs +++ b/EndlessClient/Rendering/Factories/CharacterRendererFactory.cs @@ -25,14 +25,12 @@ public class CharacterRendererFactory : ICharacterRendererFactory private readonly IRenderOffsetCalculator _renderOffsetCalculator; private readonly ICharacterPropertyRendererBuilder _characterPropertyRendererBuilder; private readonly ICharacterTextures _characterTextures; - private readonly ICharacterSpriteCalculator _characterSpriteCalculator; private readonly IGameStateProvider _gameStateProvider; private readonly ICurrentMapProvider _currentMapProvider; private readonly IUserInputProvider _userInputProvider; private readonly IEffectRendererFactory _effectRendererFactory; private readonly ISfxPlayer _sfxPlayer; private readonly IClientWindowSizeRepository _clientWindowSizeRepository; - private readonly IFixedTimeStepRepository _fixedTimeStepRepository; public CharacterRendererFactory(IEndlessGameProvider gameProvider, IRenderTargetFactory renderTargetFactory, @@ -42,14 +40,12 @@ public class CharacterRendererFactory : ICharacterRendererFactory IRenderOffsetCalculator renderOffsetCalculator, ICharacterPropertyRendererBuilder characterPropertyRendererBuilder, ICharacterTextures characterTextures, - ICharacterSpriteCalculator characterSpriteCalculator, IGameStateProvider gameStateProvider, ICurrentMapProvider currentMapProvider, IUserInputProvider userInputProvider, IEffectRendererFactory effectRendererFactory, ISfxPlayer sfxPlayer, - IClientWindowSizeRepository clientWindowSizeRepository, - IFixedTimeStepRepository fixedTimeStepRepository) + IClientWindowSizeRepository clientWindowSizeRepository) { _gameProvider = gameProvider; _renderTargetFactory = renderTargetFactory; @@ -59,14 +55,12 @@ public class CharacterRendererFactory : ICharacterRendererFactory _renderOffsetCalculator = renderOffsetCalculator; _characterPropertyRendererBuilder = characterPropertyRendererBuilder; _characterTextures = characterTextures; - _characterSpriteCalculator = characterSpriteCalculator; _gameStateProvider = gameStateProvider; _currentMapProvider = currentMapProvider; _userInputProvider = userInputProvider; _effectRendererFactory = effectRendererFactory; _sfxPlayer = sfxPlayer; _clientWindowSizeRepository = clientWindowSizeRepository; - _fixedTimeStepRepository = fixedTimeStepRepository; } public ICharacterRenderer CreateCharacterRenderer(EOLib.Domain.Character.Character character) @@ -80,15 +74,13 @@ public ICharacterRenderer CreateCharacterRenderer(EOLib.Domain.Character.Charact _renderOffsetCalculator, _characterPropertyRendererBuilder, _characterTextures, - _characterSpriteCalculator, character, _gameStateProvider, _currentMapProvider, _userInputProvider, _effectRendererFactory, _sfxPlayer, - _clientWindowSizeRepository, - _fixedTimeStepRepository); + _clientWindowSizeRepository); } } } \ No newline at end of file diff --git a/EndlessClient/Rendering/Factories/MapRendererFactory.cs b/EndlessClient/Rendering/Factories/MapRendererFactory.cs index cee8d189d..a8ac95479 100644 --- a/EndlessClient/Rendering/Factories/MapRendererFactory.cs +++ b/EndlessClient/Rendering/Factories/MapRendererFactory.cs @@ -5,6 +5,7 @@ using EndlessClient.Rendering.Map; using EndlessClient.Rendering.MapEntityRenderers; using EndlessClient.Rendering.NPC; +using EOLib; using EOLib.Config; using EOLib.Domain.Character; using EOLib.Domain.Map; @@ -42,7 +43,6 @@ public class MapRendererFactory : IMapRendererFactory IDynamicMapObjectUpdater dynamicMapObjectUpdater, IConfigurationProvider configurationProvider, IMouseCursorRendererFactory mouseCursorRendererFactory, - IRenderOffsetCalculator renderOffsetCalculator, IGridDrawCoordinateCalculator gridDrawCoordinateCalculator, IClientWindowSizeRepository clientWindowSizeRepository, IFixedTimeStepRepository fixedTimeStepRepository) diff --git a/EndlessClient/Rendering/FixedTimeStepRepository.cs b/EndlessClient/Rendering/FixedTimeStepRepository.cs deleted file mode 100644 index 2714c7d3b..000000000 --- a/EndlessClient/Rendering/FixedTimeStepRepository.cs +++ /dev/null @@ -1,38 +0,0 @@ -using AutomaticTypeMapper; -using System.Diagnostics; - -namespace EndlessClient.Rendering -{ - [AutoMappedType(IsSingleton = true)] - public class FixedTimeStepRepository : IFixedTimeStepRepository - { - private const int FIXED_UPDATE_TIME_MS = 10; // 100 FPS (walk updates at 50 FPS) - - private int _isWalkUpdate; - - public Stopwatch FixedUpdateTimer { get; set; } - - public bool IsUpdateFrame => FixedUpdateTimer.ElapsedMilliseconds >= FIXED_UPDATE_TIME_MS; - - public bool IsWalkUpdateFrame => IsUpdateFrame && _isWalkUpdate == 3; - - public FixedTimeStepRepository() => FixedUpdateTimer = Stopwatch.StartNew(); - - public void RestartTimer() - { - FixedUpdateTimer.Restart(); - _isWalkUpdate = ++_isWalkUpdate % 4; - } - } - - public interface IFixedTimeStepRepository - { - Stopwatch FixedUpdateTimer { get; set; } - - bool IsUpdateFrame { get; } - - bool IsWalkUpdateFrame { get; } - - void RestartTimer(); - } -} diff --git a/EndlessClient/Rendering/Map/MapRenderer.cs b/EndlessClient/Rendering/Map/MapRenderer.cs index 12e466a09..64ccffb44 100644 --- a/EndlessClient/Rendering/Map/MapRenderer.cs +++ b/EndlessClient/Rendering/Map/MapRenderer.cs @@ -4,6 +4,7 @@ using EndlessClient.Rendering.Factories; using EndlessClient.Rendering.MapEntityRenderers; using EndlessClient.Rendering.NPC; +using EOLib; using EOLib.Config; using EOLib.Domain.Character; using EOLib.Domain.Map; diff --git a/EndlessClient/Rendering/NPC/NPCAnimator.cs b/EndlessClient/Rendering/NPC/NPCAnimator.cs index 331dbfcae..240cab225 100644 --- a/EndlessClient/Rendering/NPC/NPCAnimator.cs +++ b/EndlessClient/Rendering/NPC/NPCAnimator.cs @@ -1,27 +1,31 @@ -using System.Collections.Generic; -using System.Linq; -using EndlessClient.GameExecution; +using EndlessClient.GameExecution; +using EOLib; using EOLib.Domain.Extensions; using EOLib.Domain.Map; using EOLib.Domain.NPC; using Microsoft.Xna.Framework; using Optional.Collections; +using System.Collections.Generic; +using System.Linq; namespace EndlessClient.Rendering.NPC { public class NPCAnimator : GameComponent, INPCAnimator { - private const int ACTION_FRAME_TIME_MS = 70; + private const int TICKS_PER_ACTION_FRAME = 7; // 7 x10ms ticks per action frame private readonly List _npcStartWalkingTimes; private readonly List _npcStartAttackingTimes; private readonly ICurrentMapStateRepository _currentMapStateRepository; + private readonly IFixedTimeStepRepository _fixedTimeStepRepository; public NPCAnimator(IEndlessGameProvider gameProvider, - ICurrentMapStateRepository currentMapStateRepository) + ICurrentMapStateRepository currentMapStateRepository, + IFixedTimeStepRepository fixedTimeStepRepository) : base((Game)gameProvider.Game) { _currentMapStateRepository = currentMapStateRepository; + _fixedTimeStepRepository = fixedTimeStepRepository; _npcStartWalkingTimes = new List(); _npcStartAttackingTimes = new List(); } @@ -39,7 +43,7 @@ public void StartWalkAnimation(int npcIndex) if (_npcStartWalkingTimes.Any(x => x.UniqueID == npcIndex)) return; - var startWalkingTimeAndID = new RenderFrameActionTime(npcIndex); + var startWalkingTimeAndID = new RenderFrameActionTime(npcIndex, _fixedTimeStepRepository.TickCount); _npcStartWalkingTimes.Add(startWalkingTimeAndID); } @@ -49,7 +53,7 @@ public void StartAttackAnimation(int npcIndex) if (_npcStartAttackingTimes.Any(x => x.UniqueID == npcIndex)) return; - var startAttackingTimeAndID = new RenderFrameActionTime(npcIndex); + var startAttackingTimeAndID = new RenderFrameActionTime(npcIndex, _fixedTimeStepRepository.TickCount); _npcStartAttackingTimes.Add(startAttackingTimeAndID); } @@ -64,7 +68,7 @@ private void AnimateNPCWalking() var npcsDoneWalking = new List(); foreach (var pair in _npcStartWalkingTimes) { - if (pair.ActionTimer.ElapsedMilliseconds >= ACTION_FRAME_TIME_MS) + if ((_fixedTimeStepRepository.TickCount - pair.ActionTick) >= TICKS_PER_ACTION_FRAME) { var npc = _currentMapStateRepository.NPCs.SingleOrNone(x => x.Index == pair.UniqueID); @@ -72,7 +76,7 @@ private void AnimateNPCWalking() some: n => { var nextFrameNPC = AnimateOneWalkFrame(n); - pair.UpdateActionStartTime(); + pair.UpdateActionStartTime(_fixedTimeStepRepository.TickCount); if (nextFrameNPC.Frame == NPCFrame.Standing) npcsDoneWalking.Add(pair); @@ -92,7 +96,7 @@ private void AnimateNPCAttacking() var npcsDoneAttacking = new List(); foreach (var pair in _npcStartAttackingTimes) { - if (pair.ActionTimer.ElapsedMilliseconds >= ACTION_FRAME_TIME_MS) + if ((_fixedTimeStepRepository.TickCount - pair.ActionTick) >= TICKS_PER_ACTION_FRAME) { var npc = _currentMapStateRepository.NPCs.SingleOrNone(x => x.Index == pair.UniqueID); @@ -100,7 +104,7 @@ private void AnimateNPCAttacking() some: n => { var nextFrameNPC = n.WithNextAttackFrame(); - pair.UpdateActionStartTime(); + pair.UpdateActionStartTime(_fixedTimeStepRepository.TickCount); if (nextFrameNPC.Frame == NPCFrame.Standing) npcsDoneAttacking.Add(pair); diff --git a/EndlessClient/Rendering/RenderFrameActionTime.cs b/EndlessClient/Rendering/RenderFrameActionTime.cs index fc0925e47..a4ee2626b 100644 --- a/EndlessClient/Rendering/RenderFrameActionTime.cs +++ b/EndlessClient/Rendering/RenderFrameActionTime.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; namespace EndlessClient.Rendering { @@ -9,20 +8,20 @@ public class RenderFrameActionTime public int UniqueID { get; private set; } - public Stopwatch ActionTimer { get; private set; } + public ulong ActionTick { get; private set; } public bool Replay { get; private set; } - public RenderFrameActionTime(int uniqueID, Action sfxCallback = null) + public RenderFrameActionTime(int uniqueID, ulong ticks, Action sfxCallback = null) { UniqueID = uniqueID; _sfxCallback = sfxCallback; - UpdateActionStartTime(); + UpdateActionStartTime(ticks); } - public void UpdateActionStartTime() + public void UpdateActionStartTime(ulong ticks) { - ActionTimer = Stopwatch.StartNew(); + ActionTick = ticks; } public void SetReplay(Action sfxCallback = null) From 57e1ca735b27c625c57507039beb564e01d8d140 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 9 May 2023 11:40:04 -0700 Subject: [PATCH 12/15] Fix death animation happening too quickly Bug was introduced in commit 2ceeff52 --- EndlessClient/Rendering/Character/CharacterRendererUpdater.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EndlessClient/Rendering/Character/CharacterRendererUpdater.cs b/EndlessClient/Rendering/Character/CharacterRendererUpdater.cs index e70539e42..46703a6c6 100644 --- a/EndlessClient/Rendering/Character/CharacterRendererUpdater.cs +++ b/EndlessClient/Rendering/Character/CharacterRendererUpdater.cs @@ -155,7 +155,7 @@ private void UpdateDeadCharacters() none: () => _characterStateCache.AddDeathStartTime(character.ID), some: actionTime => { - if ((_fixedTimeStepRepository.TickCount - actionTime.ActionTick) >= 1) // prior value was 2ms; ticks are 12ms + if ((_fixedTimeStepRepository.TickCount - actionTime.ActionTick) >= 200) // 200 ticks * 10ms = 2 seconds { _characterStateCache.RemoveDeathStartTime(character.ID); _characterStateCache.RemoveCharacterState(character.ID); From 095f51b2394c793e3868dd4e35b18d413c6d9fdc Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 9 May 2023 13:09:01 -0700 Subject: [PATCH 13/15] Make NPC animation timings slightly slower. More accurate to original client. --- EndlessClient/Rendering/NPC/NPCAnimator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EndlessClient/Rendering/NPC/NPCAnimator.cs b/EndlessClient/Rendering/NPC/NPCAnimator.cs index 240cab225..e1115a58f 100644 --- a/EndlessClient/Rendering/NPC/NPCAnimator.cs +++ b/EndlessClient/Rendering/NPC/NPCAnimator.cs @@ -12,7 +12,7 @@ namespace EndlessClient.Rendering.NPC { public class NPCAnimator : GameComponent, INPCAnimator { - private const int TICKS_PER_ACTION_FRAME = 7; // 7 x10ms ticks per action frame + private const int TICKS_PER_ACTION_FRAME = 8; // 8 x10ms ticks per action frame private readonly List _npcStartWalkingTimes; private readonly List _npcStartAttackingTimes; From cc228dcb8dbcee0b3b2a3f34a44cefb15f76f5ff Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 9 May 2023 15:47:59 -0700 Subject: [PATCH 14/15] Update trainerbot with new timing mechanisms --- EOBot/TrainerBot.cs | 29 +++++++++++++++++++++-------- EOLib/FixedTimeStepRepository.cs | 6 +++--- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/EOBot/TrainerBot.cs b/EOBot/TrainerBot.cs index 8f9bc7838..ef55494e8 100644 --- a/EOBot/TrainerBot.cs +++ b/EOBot/TrainerBot.cs @@ -6,7 +6,6 @@ using EOLib.Domain.Item; using EOLib.Domain.Login; using EOLib.Domain.Map; -using EOLib.Domain.NPC; using EOLib.IO; using EOLib.IO.Pub; using EOLib.IO.Repositories; @@ -23,8 +22,8 @@ namespace EOBot internal class TrainerBot : BotBase { private const int CONSECUTIVE_ATTACK_COUNT = 150; - private const int ATTACK_BACKOFF_MS = 500; - private const int WALK_BACKOFF_MS = 400; + private const int ATTACK_BACKOFF_MS = 600; + private const int WALK_BACKOFF_MS = 480; private const int FACE_BACKOFF_MS = 120; private static readonly int[] JunkItemIds = new[] @@ -44,6 +43,7 @@ internal class TrainerBot : BotBase private IItemActions _itemActions; private ICharacterRepository _characterRepository; + private IFixedTimeStepRepository _fixedTimeStepRepository; private IPubFile _itemData; private IPubFile _npcData; @@ -97,6 +97,8 @@ protected override async Task DoWorkAsync(CancellationToken ct) _chatProvider = c.Resolve(); _cachedChat = new HashSet(); + _fixedTimeStepRepository = c.Resolve(); + var healItems = new List(); var healSpells = new List(); @@ -249,7 +251,8 @@ protected override async Task DoWorkAsync(CancellationToken ct) } } - await Task.Delay(TimeSpan.FromSeconds(1.0 / 8.0)); + await Task.Delay(TimeSpan.FromMilliseconds(120)); + _fixedTimeStepRepository.Tick(12); } } @@ -258,6 +261,7 @@ private async Task Attack(IMapCellState cellState) cellState.NPC.MatchSome(npc => ConsoleHelper.WriteMessage(ConsoleHelper.Type.Attack, $"{npc.Index,7} - {_npcData.Single(x => x.ID == npc.ID).Name}")); await TrySend(_characterActions.Attack); await Task.Delay(TimeSpan.FromMilliseconds(ATTACK_BACKOFF_MS)); + _fixedTimeStepRepository.Tick(ATTACK_BACKOFF_MS / 10); } private async Task Walk() @@ -266,6 +270,7 @@ private async Task Walk() ConsoleHelper.WriteMessage(ConsoleHelper.Type.Walk, $"{renderProps.GetDestinationX(),3},{renderProps.GetDestinationY(),3}"); await TrySend(_characterActions.Walk); await Task.Delay(TimeSpan.FromMilliseconds(WALK_BACKOFF_MS)); + _fixedTimeStepRepository.Tick(WALK_BACKOFF_MS / 10); } private async Task Face(EODirection direction) @@ -279,6 +284,7 @@ private async Task Face(EODirection direction) .WithRenderProperties(_characterRepository.MainCharacter.RenderProperties.WithDirection(direction)); await Task.Delay(TimeSpan.FromMilliseconds(FACE_BACKOFF_MS)); + _fixedTimeStepRepository.Tick(FACE_BACKOFF_MS / 10); } private async Task FaceCoordinateIfNeeded(MapCoordinate originalCoord, MapCoordinate targetCoord) @@ -307,6 +313,7 @@ private async Task PickUpItems(IMapCellState cellState) if (JunkItemIds.Contains(item.ItemID)) { await Task.Delay(TimeSpan.FromSeconds(1)); + _fixedTimeStepRepository.Tick(100); await JunkItem(item); } } @@ -324,6 +331,7 @@ private async Task PickUpItem(MapItem item) ConsoleHelper.WriteMessage(ConsoleHelper.Type.Warning, $"Ignoring item {itemName} x{item.Amount} due to pickup error {pickupResult}", ConsoleColor.DarkYellow); }); await Task.Delay(TimeSpan.FromMilliseconds(ATTACK_BACKOFF_MS)); + _fixedTimeStepRepository.Tick(ATTACK_BACKOFF_MS / 10); } private async Task JunkItem(MapItem item) @@ -331,6 +339,7 @@ private async Task JunkItem(MapItem item) ConsoleHelper.WriteMessage(ConsoleHelper.Type.JunkItem, $"{item.Amount,7} - {_itemData.Single(x => x.ID == item.ItemID).Name}"); await TrySend(() => _itemActions.JunkItem(item.ItemID, item.Amount)); await Task.Delay(TimeSpan.FromMilliseconds(ATTACK_BACKOFF_MS)); + _fixedTimeStepRepository.Tick(ATTACK_BACKOFF_MS / 10); } private async Task CastHealSpell(IEnumerable healSpells) @@ -344,10 +353,12 @@ private async Task CastHealSpell(IEnumerable healSpells) ConsoleHelper.WriteMessage(ConsoleHelper.Type.Cast, $"{spellToUse.HP,4} HP - {spellToUse.Name} - TP {stats[CharacterStat.TP]}/{stats[CharacterStat.MaxTP]}"); await TrySend(() => _characterActions.PrepareCastSpell(spellToUse.ID)); - await Task.Delay((spellToUse.CastTime - 1) * 480 + 350); + await Task.Delay(spellToUse.CastTime * 480); + _fixedTimeStepRepository.Tick((uint)spellToUse.CastTime * 48); await TrySend(() => _characterActions.CastSpell(spellToUse.ID, _characterRepository.MainCharacter)); - await Task.Delay(600); // 600ms cooldown between spell casts + await Task.Delay(ATTACK_BACKOFF_MS); // 600ms cooldown between spell casts + _fixedTimeStepRepository.Tick(ATTACK_BACKOFF_MS / 10); } private async Task UseHealItem(IEnumerable healItems) @@ -363,6 +374,7 @@ private async Task UseHealItem(IEnumerable healItems) await TrySend(() => _itemActions.UseItem(itemToUse.ID)); await Task.Delay(ATTACK_BACKOFF_MS); + _fixedTimeStepRepository.Tick(ATTACK_BACKOFF_MS / 10); } private async Task ToggleSit() @@ -373,9 +385,9 @@ private async Task ToggleSit() await TrySend(_characterActions.ToggleSit); } - private async Task TrySend(Action action, int attempts = 3) + private async Task TrySend(Action action, uint attempts = 3) { - for (int i = 1; i <= attempts; i++) + for (uint i = 1; i <= attempts; i++) { try { @@ -389,6 +401,7 @@ private async Task TrySend(Action action, int attempts = 3) throw; await Task.Delay(TimeSpan.FromSeconds(i * i)); + _fixedTimeStepRepository.Tick(i * i * 100); } } } diff --git a/EOLib/FixedTimeStepRepository.cs b/EOLib/FixedTimeStepRepository.cs index f89bbb7b6..81cbe0ec5 100644 --- a/EOLib/FixedTimeStepRepository.cs +++ b/EOLib/FixedTimeStepRepository.cs @@ -11,9 +11,9 @@ public class FixedTimeStepRepository : IFixedTimeStepRepository public bool IsWalkUpdateFrame => TickCount % 4 == 0; - public void Tick() + public void Tick(uint ticks = 1) { - TickCount++; + TickCount += ticks; } } @@ -23,6 +23,6 @@ public interface IFixedTimeStepRepository bool IsWalkUpdateFrame { get; } - void Tick(); + void Tick(uint ticks = 1); } } From 5ef4c762bbbfa7ff790a1f54ea7889c3325f49ba Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Tue, 9 May 2023 16:35:36 -0700 Subject: [PATCH 15/15] Remove DateTimeExtensions Use single Delay method in TrainerBot Use 10ms target update time in EndlessGame --- EOBot/TrainerBot.cs | 40 +++++++++------------- EOLib/misc.cs | 14 +------- EndlessClient/GameExecution/EndlessGame.cs | 2 +- 3 files changed, 19 insertions(+), 37 deletions(-) diff --git a/EOBot/TrainerBot.cs b/EOBot/TrainerBot.cs index ef55494e8..00f16c780 100644 --- a/EOBot/TrainerBot.cs +++ b/EOBot/TrainerBot.cs @@ -251,8 +251,7 @@ protected override async Task DoWorkAsync(CancellationToken ct) } } - await Task.Delay(TimeSpan.FromMilliseconds(120)); - _fixedTimeStepRepository.Tick(12); + await Delay(120); } } @@ -260,8 +259,7 @@ private async Task Attack(IMapCellState cellState) { cellState.NPC.MatchSome(npc => ConsoleHelper.WriteMessage(ConsoleHelper.Type.Attack, $"{npc.Index,7} - {_npcData.Single(x => x.ID == npc.ID).Name}")); await TrySend(_characterActions.Attack); - await Task.Delay(TimeSpan.FromMilliseconds(ATTACK_BACKOFF_MS)); - _fixedTimeStepRepository.Tick(ATTACK_BACKOFF_MS / 10); + await Delay(ATTACK_BACKOFF_MS); } private async Task Walk() @@ -269,8 +267,7 @@ private async Task Walk() var renderProps = _characterRepository.MainCharacter.RenderProperties; ConsoleHelper.WriteMessage(ConsoleHelper.Type.Walk, $"{renderProps.GetDestinationX(),3},{renderProps.GetDestinationY(),3}"); await TrySend(_characterActions.Walk); - await Task.Delay(TimeSpan.FromMilliseconds(WALK_BACKOFF_MS)); - _fixedTimeStepRepository.Tick(WALK_BACKOFF_MS / 10); + await Delay(WALK_BACKOFF_MS); } private async Task Face(EODirection direction) @@ -283,8 +280,7 @@ private async Task Face(EODirection direction) _characterRepository.MainCharacter = _characterRepository.MainCharacter .WithRenderProperties(_characterRepository.MainCharacter.RenderProperties.WithDirection(direction)); - await Task.Delay(TimeSpan.FromMilliseconds(FACE_BACKOFF_MS)); - _fixedTimeStepRepository.Tick(FACE_BACKOFF_MS / 10); + await Delay(FACE_BACKOFF_MS); } private async Task FaceCoordinateIfNeeded(MapCoordinate originalCoord, MapCoordinate targetCoord) @@ -312,8 +308,7 @@ private async Task PickUpItems(IMapCellState cellState) if (JunkItemIds.Contains(item.ItemID)) { - await Task.Delay(TimeSpan.FromSeconds(1)); - _fixedTimeStepRepository.Tick(100); + await Delay(1000); await JunkItem(item); } } @@ -330,16 +325,14 @@ private async Task PickUpItem(MapItem item) else ConsoleHelper.WriteMessage(ConsoleHelper.Type.Warning, $"Ignoring item {itemName} x{item.Amount} due to pickup error {pickupResult}", ConsoleColor.DarkYellow); }); - await Task.Delay(TimeSpan.FromMilliseconds(ATTACK_BACKOFF_MS)); - _fixedTimeStepRepository.Tick(ATTACK_BACKOFF_MS / 10); + await Delay(ATTACK_BACKOFF_MS); } private async Task JunkItem(MapItem item) { ConsoleHelper.WriteMessage(ConsoleHelper.Type.JunkItem, $"{item.Amount,7} - {_itemData.Single(x => x.ID == item.ItemID).Name}"); await TrySend(() => _itemActions.JunkItem(item.ItemID, item.Amount)); - await Task.Delay(TimeSpan.FromMilliseconds(ATTACK_BACKOFF_MS)); - _fixedTimeStepRepository.Tick(ATTACK_BACKOFF_MS / 10); + await Delay(ATTACK_BACKOFF_MS); } private async Task CastHealSpell(IEnumerable healSpells) @@ -353,12 +346,10 @@ private async Task CastHealSpell(IEnumerable healSpells) ConsoleHelper.WriteMessage(ConsoleHelper.Type.Cast, $"{spellToUse.HP,4} HP - {spellToUse.Name} - TP {stats[CharacterStat.TP]}/{stats[CharacterStat.MaxTP]}"); await TrySend(() => _characterActions.PrepareCastSpell(spellToUse.ID)); - await Task.Delay(spellToUse.CastTime * 480); - _fixedTimeStepRepository.Tick((uint)spellToUse.CastTime * 48); + await Delay((uint)spellToUse.CastTime * 480); await TrySend(() => _characterActions.CastSpell(spellToUse.ID, _characterRepository.MainCharacter)); - await Task.Delay(ATTACK_BACKOFF_MS); // 600ms cooldown between spell casts - _fixedTimeStepRepository.Tick(ATTACK_BACKOFF_MS / 10); + await Delay(ATTACK_BACKOFF_MS); // 600ms cooldown between spell casts } private async Task UseHealItem(IEnumerable healItems) @@ -372,9 +363,7 @@ private async Task UseHealItem(IEnumerable healItems) ConsoleHelper.WriteMessage(ConsoleHelper.Type.UseItem, $"{itemToUse.Name} - {itemToUse.HP} HP - inventory: {amount - 1} - (other heal item types: {healItems.Count() - 1})"); await TrySend(() => _itemActions.UseItem(itemToUse.ID)); - - await Task.Delay(ATTACK_BACKOFF_MS); - _fixedTimeStepRepository.Tick(ATTACK_BACKOFF_MS / 10); + await Delay(ATTACK_BACKOFF_MS); } private async Task ToggleSit() @@ -400,10 +389,15 @@ private async Task TrySend(Action action, uint attempts = 3) if (i == attempts) throw; - await Task.Delay(TimeSpan.FromSeconds(i * i)); - _fixedTimeStepRepository.Tick(i * i * 100); + await Delay(i * i * 1000); } } } + + private Task Delay(uint milliseconds) + { + _fixedTimeStepRepository.Tick(milliseconds / 10); + return Task.Delay((int)milliseconds); + } } } diff --git a/EOLib/misc.cs b/EOLib/misc.cs index 01aee3b41..bbbe235e1 100644 --- a/EOLib/misc.cs +++ b/EOLib/misc.cs @@ -1,7 +1,4 @@ -using System; -using System.Diagnostics; - -namespace EOLib +namespace EOLib { public static class ArrayExtension { @@ -19,15 +16,6 @@ public static T[] SubArray(this T[] arr, int offset, int count) } } - public static class DateTimeExtension - { - public static int ToEOTimeStamp(this Stopwatch sw) - { - var elapsedHundreths = Math.Round(sw.ElapsedTicks / (Stopwatch.Frequency / 100.0)); - return (int)elapsedHundreths; - } - } - public static class Constants { public const int ResponseTimeout = 5000; diff --git a/EndlessClient/GameExecution/EndlessGame.cs b/EndlessClient/GameExecution/EndlessGame.cs index 1928a5170..b5657a6d3 100644 --- a/EndlessClient/GameExecution/EndlessGame.cs +++ b/EndlessClient/GameExecution/EndlessGame.cs @@ -116,7 +116,7 @@ protected override void Initialize() IsMouseVisible = true; IsFixedTimeStep = false; - TargetElapsedTime = TimeSpan.FromMilliseconds(12); + TargetElapsedTime = TimeSpan.FromMilliseconds(FixedTimeStepRepository.TICK_TIME_MS); _previousKeyState = Keyboard.GetState();