diff --git a/EOBot/TrainerBot.cs b/EOBot/TrainerBot.cs index 00f16c78..37b7eccf 100644 --- a/EOBot/TrainerBot.cs +++ b/EOBot/TrainerBot.cs @@ -116,6 +116,7 @@ protected override async Task DoWorkAsync(CancellationToken ct) ConsoleHelper.WriteMessage(ConsoleHelper.Type.Chat, $"{message.Who}: {message.Message}", ConsoleColor.Cyan); _cachedChat = _chatProvider.AllChat[ChatTab.Local].ToHashSet(); + Console.Beep(261, 1500); } var character = _characterRepository.MainCharacter; @@ -129,6 +130,9 @@ protected override async Task DoWorkAsync(CancellationToken ct) if (cachedPlayerCount > 0) { ConsoleHelper.WriteMessage(ConsoleHelper.Type.Warning, $"{cachedPlayerCount,7} - Players on map - You may not be able to train here", ConsoleColor.DarkYellow); + Console.Beep(220, 500); + Console.Beep(247, 500); + Console.Beep(220, 500); } } diff --git a/EOLib/Domain/Login/LoginActions.cs b/EOLib/Domain/Login/LoginActions.cs index 37d2cdea..8b179a35 100644 --- a/EOLib/Domain/Login/LoginActions.cs +++ b/EOLib/Domain/Login/LoginActions.cs @@ -185,7 +185,7 @@ public async Task CompleteCharacterLogin(int sessionID) _currentMapStateRepository.Characters = data.MapCharacters.Except(new[] { mainCharacter }).ToDictionary(k => k.ID, v => v); _currentMapStateRepository.NPCs = new HashSet(data.MapNPCs); - _currentMapStateRepository.MapItems = new HashSet(data.MapItems); + _currentMapStateRepository.MapItems = new MapEntityCollectionHashSet(item => item.UniqueID, item => new MapCoordinate(item.X, item.Y), data.MapItems); _playerInfoRepository.PlayerIsInGame = true; _characterSessionRepository.ResetState(); diff --git a/EOLib/Domain/Map/CurrentMapStateRepository.cs b/EOLib/Domain/Map/CurrentMapStateRepository.cs index ad0ed4aa..80873c57 100644 --- a/EOLib/Domain/Map/CurrentMapStateRepository.cs +++ b/EOLib/Domain/Map/CurrentMapStateRepository.cs @@ -19,7 +19,7 @@ public interface ICurrentMapStateRepository HashSet NPCs { get; set; } - HashSet MapItems { get; set; } + MapEntityCollectionHashSet MapItems { get; set; } HashSet OpenDoors { get; set; } @@ -54,7 +54,7 @@ public interface ICurrentMapStateProvider IReadOnlyCollection NPCs { get; } - IReadOnlyCollection MapItems { get; } + IReadOnlyMapEntityCollection MapItems { get; } IReadOnlyCollection OpenDoors { get; } @@ -90,7 +90,7 @@ public class CurrentMapStateRepository : ICurrentMapStateRepository, ICurrentMap public HashSet NPCs { get; set; } - public HashSet MapItems { get; set; } + public MapEntityCollectionHashSet MapItems { get; set; } public HashSet OpenDoors { get; set; } @@ -114,7 +114,7 @@ public class CurrentMapStateRepository : ICurrentMapStateRepository, ICurrentMap IReadOnlyCollection ICurrentMapStateProvider.NPCs => NPCs; - IReadOnlyCollection ICurrentMapStateProvider.MapItems => MapItems; + IReadOnlyMapEntityCollection ICurrentMapStateProvider.MapItems => MapItems; IReadOnlyCollection ICurrentMapStateProvider.OpenDoors => OpenDoors; @@ -135,7 +135,7 @@ public void ResetState() Characters = new Dictionary(); NPCs = new HashSet(); - MapItems = new HashSet(); + MapItems = new MapEntityCollectionHashSet(x => x.UniqueID, x => new MapCoordinate(x.X, x.Y)); OpenDoors = new HashSet(); PendingDoors = new HashSet(); VisibleSpikeTraps = new HashSet(); diff --git a/EOLib/Domain/Map/MapCellStateProvider.cs b/EOLib/Domain/Map/MapCellStateProvider.cs index b198abc9..32041457 100644 --- a/EOLib/Domain/Map/MapCellStateProvider.cs +++ b/EOLib/Domain/Map/MapCellStateProvider.cs @@ -6,6 +6,7 @@ using EOLib.IO.Repositories; using Optional; using Optional.Collections; +using System.Collections.Generic; using System.Linq; namespace EOLib.Domain.Map @@ -43,7 +44,9 @@ public IMapCellState GetCellStateAt(int x, int y) .Where(c => CharacterAtCoordinates(c, x, y)) .ToList(); var npc = _mapStateProvider.NPCs.FirstOrNone(n => NPCAtCoordinates(n, x, y)); - var items = _mapStateProvider.MapItems.Where(i => i.X == x && i.Y == y).OrderByDescending(i => i.UniqueID); + var items = _mapStateProvider.MapItems.TryGetValues(new MapCoordinate(x, y), out var mapItems) + ? mapItems.OrderByDescending(i => i.UniqueID) + : Enumerable.Empty(); return new MapCellState { diff --git a/EOLib/Domain/Map/MapEntityCollectionHashSet.cs b/EOLib/Domain/Map/MapEntityCollectionHashSet.cs new file mode 100644 index 00000000..d5730640 --- /dev/null +++ b/EOLib/Domain/Map/MapEntityCollectionHashSet.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace EOLib.Domain.Map +{ + public class MapEntityCollectionHashSet : IReadOnlyMapEntityCollection + { + private readonly Dictionary _uniqueIdToHash; + private readonly Dictionary> _mapCoordinateToHashList; + + private readonly Dictionary _valueSet; + + private readonly Func _uniqueIdSelector; + private readonly Func _mapCoordinateSelector; + + public MapEntityCollectionHashSet(Func uniqueIdSelector, + Func mapCoordinateSelector) + { + _uniqueIdToHash = new Dictionary(); + _mapCoordinateToHashList = new Dictionary>(); + _valueSet = new Dictionary(); + + _uniqueIdSelector = uniqueIdSelector; + _mapCoordinateSelector = mapCoordinateSelector; + } + + public MapEntityCollectionHashSet(Func uniqueIdSelector, + Func mapCoordinateSelector, + IEnumerable values) + : this(uniqueIdSelector, mapCoordinateSelector) + { + foreach (var value in values) + { + Add(value); + } + } + + public TValue this[int key1] => _valueSet[_uniqueIdToHash[key1]]; + + public HashSet this[MapCoordinate key2] => new HashSet(_mapCoordinateToHashList[key2].Select(x => _valueSet[x])); + + public void Add(TValue value) + { + var key1 = _uniqueIdSelector.Invoke(value); + + var hash = value.GetHashCode(); + _uniqueIdToHash[key1] = hash; + + var key2 = _mapCoordinateSelector.Invoke(value); + if (!_mapCoordinateToHashList.ContainsKey(key2)) + _mapCoordinateToHashList.Add(key2, new HashSet()); + + _mapCoordinateToHashList[key2].Add(hash); + _valueSet[hash] = value; + } + + public IEnumerator GetEnumerator() => _valueSet.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Remove(TValue value) + { + var key1 = _uniqueIdSelector.Invoke(value); + var key2 = _mapCoordinateSelector.Invoke(value); + _uniqueIdToHash.Remove(key1); + + var hash = value.GetHashCode(); + _mapCoordinateToHashList[key2].Remove(hash); + if (_mapCoordinateToHashList[key2].Count == 0) + _mapCoordinateToHashList.Remove(key2); + + _valueSet.Remove(hash); + } + + public bool TryGetValue(int uniqueId, out TValue value) + { + value = default; + + if (!_uniqueIdToHash.ContainsKey(uniqueId)) + return false; + + var hash = _uniqueIdToHash[uniqueId]; + if (!_valueSet.ContainsKey(hash)) + return false; + + value = _valueSet[hash]; + + return true; + } + + public bool TryGetValues(MapCoordinate mapCoordinate, out HashSet values) + { + values = default; + + if (!_mapCoordinateToHashList.ContainsKey(mapCoordinate)) + return false; + + var hashes = _mapCoordinateToHashList[mapCoordinate]; + if (!_valueSet.Any(x => hashes.Contains(x.Key))) + return false; + + values = new HashSet(_mapCoordinateToHashList[mapCoordinate].Select(x => _valueSet[x])); + + return true; + } + } + + public interface IReadOnlyMapEntityCollection : IEnumerable + { + TValue this[int key1] { get; } + HashSet this[MapCoordinate key2] { get; } + + bool TryGetValue(int key1, out TValue value); + bool TryGetValues(MapCoordinate key2, out HashSet values); + } +} diff --git a/EOLib/Domain/Map/MapItem.cs b/EOLib/Domain/Map/MapItem.cs index 48c2deb8..bf5f263e 100644 --- a/EOLib/Domain/Map/MapItem.cs +++ b/EOLib/Domain/Map/MapItem.cs @@ -1,11 +1,12 @@ using Amadevus.RecordGenerator; +using EOLib.IO.Map; using Optional; using System; namespace EOLib.Domain.Map { [Record] - public sealed partial class MapItem + public sealed partial class MapItem : IMapEntity { public int UniqueID { get; } diff --git a/EOLib/PacketHandlers/Items/ItemGetHandler.cs b/EOLib/PacketHandlers/Items/ItemGetHandler.cs index ecd4818f..40f03bcc 100644 --- a/EOLib/PacketHandlers/Items/ItemGetHandler.cs +++ b/EOLib/PacketHandlers/Items/ItemGetHandler.cs @@ -46,10 +46,10 @@ public override bool HandlePacket(IPacket packet) var weight = packet.ReadChar(); var maxWeight = packet.ReadChar(); - var existing = _characterInventoryRepository.ItemInventory.SingleOrNone(x => x.ItemID == id); - existing.MatchSome(x => _characterInventoryRepository.ItemInventory.Remove(x)); + var existingInventoryItem = _characterInventoryRepository.ItemInventory.SingleOrNone(x => x.ItemID == id); + existingInventoryItem.MatchSome(x => _characterInventoryRepository.ItemInventory.Remove(x)); - existing.Map(x => x.WithAmount(x.Amount + amountTaken)) + existingInventoryItem.Map(x => x.WithAmount(x.Amount + amountTaken)) .Match(some: _characterInventoryRepository.ItemInventory.Add, none: () => _characterInventoryRepository.ItemInventory.Add(new InventoryItem(id, amountTaken))); @@ -58,7 +58,7 @@ public override bool HandlePacket(IPacket packet) .WithNewStat(CharacterStat.MaxWeight, maxWeight); _characterRepository.MainCharacter = _characterRepository.MainCharacter.WithStats(newStats); - _mapStateRepository.MapItems.RemoveWhere(x => x.UniqueID == uid); + _mapStateRepository.MapItems.Remove(_mapStateRepository.MapItems[uid]); foreach (var notifier in _mainCharacterEventNotifiers) { diff --git a/EOLib/PacketHandlers/Items/ItemRemoveHandler.cs b/EOLib/PacketHandlers/Items/ItemRemoveHandler.cs index cec8ee72..b8669ff5 100644 --- a/EOLib/PacketHandlers/Items/ItemRemoveHandler.cs +++ b/EOLib/PacketHandlers/Items/ItemRemoveHandler.cs @@ -28,7 +28,7 @@ public class ItemRemoveHandler : InGameOnlyPacketHandler public override bool HandlePacket(IPacket packet) { var uid = packet.ReadShort(); - _currentMapStateRepository.MapItems.RemoveWhere(x => x.UniqueID == uid); + _currentMapStateRepository.MapItems.Remove(_currentMapStateRepository.MapItems[uid]); return true; } } diff --git a/EOLib/PacketHandlers/NPC/NPCSpecHandler.cs b/EOLib/PacketHandlers/NPC/NPCSpecHandler.cs index ad4fa638..8a9e1209 100644 --- a/EOLib/PacketHandlers/NPC/NPCSpecHandler.cs +++ b/EOLib/PacketHandlers/NPC/NPCSpecHandler.cs @@ -8,6 +8,7 @@ using Optional; using System; using System.Collections.Generic; +using System.Security.Cryptography; namespace EOLib.PacketHandlers.NPC { @@ -152,7 +153,9 @@ private void ShowDroppedItem(int playerID, int droppedItemUID, int droppedItemID .WithDropTime(Option.Some(DateTime.Now)) .WithOwningPlayerID(Option.Some(playerID)); - _currentMapStateRepository.MapItems.RemoveWhere(item => item.UniqueID == droppedItemUID); + if (_currentMapStateRepository.MapItems.TryGetValue(droppedItemID, out var oldItem)) + _currentMapStateRepository.MapItems.Remove(oldItem); + _currentMapStateRepository.MapItems.Add(mapItem); foreach (var notifier in _npcActionNotifiers) diff --git a/EOLib/PacketHandlers/Refresh/RefreshReplyHandler.cs b/EOLib/PacketHandlers/Refresh/RefreshReplyHandler.cs index 23f99dbd..bfd30076 100644 --- a/EOLib/PacketHandlers/Refresh/RefreshReplyHandler.cs +++ b/EOLib/PacketHandlers/Refresh/RefreshReplyHandler.cs @@ -58,7 +58,7 @@ public override bool HandlePacket(IPacket packet) _currentMapStateRepository.Characters = withoutMainCharacter.ToDictionary(k => k.ID, v => v); _currentMapStateRepository.NPCs = new HashSet(data.NPCs); - _currentMapStateRepository.MapItems = new HashSet(data.Items); + _currentMapStateRepository.MapItems = new MapEntityCollectionHashSet(item => item.UniqueID, item => new MapCoordinate(item.X, item.Y), data.Items); _currentMapStateRepository.OpenDoors.Clear(); _currentMapStateRepository.PendingDoors.Clear(); diff --git a/EOLib/PacketHandlers/Warp/WarpAgreeHandler.cs b/EOLib/PacketHandlers/Warp/WarpAgreeHandler.cs index b8d3d63e..7512a17c 100644 --- a/EOLib/PacketHandlers/Warp/WarpAgreeHandler.cs +++ b/EOLib/PacketHandlers/Warp/WarpAgreeHandler.cs @@ -75,7 +75,7 @@ public override bool HandlePacket(IPacket packet) _currentMapStateRepository.Characters = warpAgreePacketData.Characters.ToDictionary(k => k.ID, v => v); _currentMapStateRepository.NPCs = new HashSet(warpAgreePacketData.NPCs); - _currentMapStateRepository.MapItems = new HashSet(warpAgreePacketData.Items); + _currentMapStateRepository.MapItems = new MapEntityCollectionHashSet(item => item.UniqueID, item => new MapCoordinate(item.X, item.Y), warpAgreePacketData.Items); _currentMapStateRepository.OpenDoors.Clear(); _currentMapStateRepository.VisibleSpikeTraps.Clear(); _currentMapStateRepository.ShowMiniMap = _currentMapStateRepository.ShowMiniMap && diff --git a/EndlessClient/Network/UnknownEntitiesRequester.cs b/EndlessClient/Network/UnknownEntitiesRequester.cs index c2a2f02c..1d9cbed3 100644 --- a/EndlessClient/Network/UnknownEntitiesRequester.cs +++ b/EndlessClient/Network/UnknownEntitiesRequester.cs @@ -3,16 +3,22 @@ using EOLib.Domain.Character; using EOLib.Domain.Map; using EOLib.Domain.NPC; +using EOLib.IO.Map; using EOLib.Net; using EOLib.Net.Communication; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata.Ecma335; namespace EndlessClient.Network { public class UnknownEntitiesRequester : GameComponent { + private const int UPPER_SEE_DISTANCE = 11; + private const int LOWER_SEE_DISTANCE = 14; + private const double REQUEST_INTERVAL_SECONDS = 1.0; private readonly IClientWindowSizeProvider _clientWindowSizeProvider; @@ -119,55 +125,41 @@ private IPacket CreateRequestForPlayers() private void ClearOutOfRangeActors() { - // todo: the server should communicate the "seedistance" to clients - // for now, disable auto remove of entities in Resizable mode - if (_clientWindowSizeProvider.Resizable) - { - return; - } - var mc = _characterProvider.MainCharacter; - var idsToRemove = new List(); - foreach (var id in _currentMapStateRepository.Characters.Keys) - { - var c = _currentMapStateRepository.Characters[id]; - - var xDiff = Math.Abs(mc.X - c.X); - var yDiff = Math.Abs(mc.Y - c.Y); - - if (c.X < mc.X || c.Y < mc.Y) - { - if (xDiff + yDiff > 11) - idsToRemove.Add(id); - } - else if (xDiff + yDiff > 14) - { - idsToRemove.Add(id); - } - } + var entities = _currentMapStateRepository.MapItems.Cast() + .Concat(_currentMapStateRepository.NPCs) + .Concat(_currentMapStateRepository.Characters.Values); - foreach (var id in idsToRemove) - _currentMapStateRepository.Characters.Remove(id); + var seeDistanceUpper = (int)((_clientWindowSizeProvider.Height / 480.0) * UPPER_SEE_DISTANCE); + var seeDistanceLower = (int)((_clientWindowSizeProvider.Height / 480.0) * LOWER_SEE_DISTANCE); - var npcsToRemove = new List(); - foreach (var npc in _currentMapStateRepository.NPCs) + var entitiesToRemove = new List(); + foreach (var entity in entities) { - var xDiff = Math.Abs(mc.X - npc.X); - var yDiff = Math.Abs(mc.Y - npc.Y); + var xDiff = Math.Abs(mc.X - entity.X); + var yDiff = Math.Abs(mc.Y - entity.Y); - if (npc.X < mc.X || npc.Y < mc.Y) + if (entity.X < mc.X || entity.Y < mc.Y) { - if (xDiff + yDiff > 11) - npcsToRemove.Add(npc); + if (xDiff + yDiff > seeDistanceUpper) + entitiesToRemove.Add(entity); } - else if (xDiff + yDiff > 14) + else if (xDiff + yDiff > seeDistanceLower) { - npcsToRemove.Add(npc); + entitiesToRemove.Add(entity); } } - _currentMapStateRepository.NPCs.RemoveWhere(npcsToRemove.Contains); + foreach (var entity in entitiesToRemove) + { + if (entity is Character c) + _currentMapStateRepository.Characters.Remove(c.ID); + else if (entity is NPC n) + _currentMapStateRepository.NPCs.Remove(n); + else if (entity is MapItem i) + _currentMapStateRepository.MapItems.Remove(i); + } } } } diff --git a/EndlessClient/Rendering/MapEntityRenderers/MapItemLayerRenderer.cs b/EndlessClient/Rendering/MapEntityRenderers/MapItemLayerRenderer.cs index 2c23f3d3..24787a12 100644 --- a/EndlessClient/Rendering/MapEntityRenderers/MapItemLayerRenderer.cs +++ b/EndlessClient/Rendering/MapEntityRenderers/MapItemLayerRenderer.cs @@ -1,12 +1,10 @@ -using System; -using System.Linq; -using EndlessClient.Rendering.Map; -using EOLib; +using EndlessClient.Rendering.Map; using EOLib.Domain.Character; -using EOLib.Domain.Extensions; using EOLib.Domain.Map; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System; +using System.Linq; namespace EndlessClient.Rendering.MapEntityRenderers { @@ -32,18 +30,14 @@ public class MapItemLayerRenderer : BaseMapEntityRenderer protected override bool ElementExistsAt(int row, int col) { - return _currentMapStateProvider.MapItems.Any(IsItemAt); - bool IsItemAt(MapItem item) => item.X == col && item.Y == row; + return _currentMapStateProvider.MapItems.TryGetValues(new MapCoordinate(col, row), out var mapItems) && mapItems.Count > 0; } public override void RenderElementAt(SpriteBatch spriteBatch, int row, int col, int alpha, Vector2 additionalOffset = default) { - var items = _currentMapStateProvider - .MapItems - .Where(IsItemAt) - .OrderBy(item => item.UniqueID); + var items = _currentMapStateProvider.MapItems[new MapCoordinate(col, row)]; - foreach (var item in items) + foreach (var item in items.OrderBy(item => item.UniqueID)) { var itemPos = GetDrawCoordinatesFromGridUnits(col, row); var itemTexture = _mapItemGraphicProvider.GetItemGraphic(item.ItemID, item.Amount); @@ -53,8 +47,6 @@ 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; } } }