diff --git a/Archipelago.MultiClient.Net.Tests/Archipelago.MultiClient.Net.Tests.csproj b/Archipelago.MultiClient.Net.Tests/Archipelago.MultiClient.Net.Tests.csproj index 60a00dd..1f1f85a 100644 --- a/Archipelago.MultiClient.Net.Tests/Archipelago.MultiClient.Net.Tests.csproj +++ b/Archipelago.MultiClient.Net.Tests/Archipelago.MultiClient.Net.Tests.csproj @@ -36,7 +36,7 @@ - ..\DLLs\net35\Newtonsoft.Json.dll + ..\DLLs\net40\Newtonsoft.Json.dll @@ -52,7 +52,7 @@ - ..\DLLs\net35\Newtonsoft.Json.dll + ..\DLLs\net45\Newtonsoft.Json.dll diff --git a/Archipelago.MultiClient.Net.Tests/ArchipelagoSessionFixture.cs b/Archipelago.MultiClient.Net.Tests/ArchipelagoSessionFixture.cs index fe86a10..54edfdf 100644 --- a/Archipelago.MultiClient.Net.Tests/ArchipelagoSessionFixture.cs +++ b/Archipelago.MultiClient.Net.Tests/ArchipelagoSessionFixture.cs @@ -1,4 +1,4 @@ -using Archipelago.MultiClient.Net.Cache; +using Archipelago.MultiClient.Net.DataPackage; using Archipelago.MultiClient.Net.Enums; using Archipelago.MultiClient.Net.Helpers; using Archipelago.MultiClient.Net.Models; @@ -65,7 +65,7 @@ public void Should_send_connect_after_retrieving_data_package() var session = CreateTestSession(socket, fileSystemDataPackageProvider); - SetupRoomInfoPacket(socket, new RoomInfoPacket { Games = Array.Empty() }); + SetupRoomInfoPacket(socket, new RoomInfoPacket { Games = new []{ "Game1" } }); SetupLoginResultPacket(socket, new ConnectionRefusedPacket()); var result = session.TryConnectAndLogin("", "", ItemsHandlingFlags.NoItems); @@ -142,17 +142,57 @@ public void Should_add_correct_properties_to_login_successful() Assert.That(successful.Team, Is.EqualTo(3)); } + [Test] + public void Should_say_text() + { + var socket = Substitute.For(); + var fileSystemDataPackageProvider = Substitute.For(); + + var session = CreateTestSession(socket, fileSystemDataPackageProvider); + + session.Say("!message for server"); + + socket.Received().SendPacket(Arg.Is(p => p.Text == "!message for server")); + } + + [Test] + public void Should_update_client_status() + { + var socket = Substitute.For(); + var fileSystemDataPackageProvider = Substitute.For(); + + var session = CreateTestSession(socket, fileSystemDataPackageProvider); + + session.SetClientState(ArchipelagoClientState.ClientReady); + + socket.Received().SendPacket(Arg.Is(p => p.Status == ArchipelagoClientState.ClientReady)); + } + + [Test] + public void Should_update_client_goal() + { + var socket = Substitute.For(); + var fileSystemDataPackageProvider = Substitute.For(); + + var session = CreateTestSession(socket, fileSystemDataPackageProvider); + + session.SetGoalAchieved(); + + socket.Received().SendPacket(Arg.Is(p => p.Status == ArchipelagoClientState.ClientGoal)); + } + static ArchipelagoSession CreateTestSession(IArchipelagoSocketHelper socket, IFileSystemDataPackageProvider fileSystemDataPackageProvider) { var dataPackageCache = new DataPackageCache(socket, fileSystemDataPackageProvider); - var locations = new LocationCheckHelper(socket, dataPackageCache); - var items = new ReceivedItemsHelper(socket, locations, dataPackageCache); var connectionInfo = new ConnectionInfoHelper(socket); + var itemInfoResolver = new ItemInfoResolver(dataPackageCache, connectionInfo); var players = new PlayerHelper(socket, connectionInfo); + var locations = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); + var items = new ReceivedItemsHelper(socket, locations, itemInfoResolver, connectionInfo, players); var roomState = new RoomStateHelper(socket, locations); var dataStorage = new DataStorageHelper(socket, connectionInfo); - var messageLog = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var messageLog = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); return new ArchipelagoSession(socket, items, locations, players, roomState, connectionInfo, dataStorage, messageLog); } diff --git a/Archipelago.MultiClient.Net.Tests/DataPackageFileSystemCacheFixture.cs b/Archipelago.MultiClient.Net.Tests/DataPackageFileSystemCacheFixture.cs index 9c62519..9c9bbb8 100644 --- a/Archipelago.MultiClient.Net.Tests/DataPackageFileSystemCacheFixture.cs +++ b/Archipelago.MultiClient.Net.Tests/DataPackageFileSystemCacheFixture.cs @@ -1,4 +1,4 @@ -using Archipelago.MultiClient.Net.Cache; +using Archipelago.MultiClient.Net.DataPackage; using Archipelago.MultiClient.Net.Helpers; using Archipelago.MultiClient.Net.Models; using Archipelago.MultiClient.Net.Packets; @@ -11,8 +11,7 @@ namespace Archipelago.MultiClient.Net.Tests { //use Directory C:\Users\\AppData\Local\Archipelago\Cache\\.json //retention 1 month since last modified, update File.SetLastWriteTime(file, modifiedTime) in use - - + [TestFixture] public class DataPackageFileSystemCacheFixture { @@ -58,7 +57,7 @@ public void Should_request_data_package_when_no_local_cache_is_available() [Test] public void Should_not_request_data_when_local_contains_a_file_with_same_checksum() { - var localDataPackage = new DataPackage { + var localDataPackage = new Models.DataPackage { Games = new Dictionary { { "One", new TestGameData("3") }, { "Two", new TestGameData("4") }, @@ -94,7 +93,7 @@ public void Should_not_request_data_when_local_contains_a_file_with_same_checksu [Test] public void Should_only_request_data_for_games_that_a_have_different_checksum_then_cached() { - var localDataPackage = new DataPackage + var localDataPackage = new Models.DataPackage { Games = new Dictionary { { "One", new TestGameData("3") }, @@ -137,7 +136,7 @@ public void Should_only_request_data_for_games_that_a_have_different_checksum_th public void Should_save_received_datapackage_contents() { var serverDataPackage = new DataPackagePacket { - DataPackage = new DataPackage { + DataPackage = new Models.DataPackage { Games = new Dictionary { { "One", new GameData { @@ -175,7 +174,7 @@ public void Should_save_received_datapackage_contents() [Test] public void Should_merge_received_datapackage_with_cached_version() { - var localDataPackage = new DataPackage + var localDataPackage = new Models.DataPackage { Games = new Dictionary { { "One", new TestGameData("1") }, @@ -197,7 +196,7 @@ public void Should_merge_received_datapackage_with_cached_version() var serverDataPackage = new DataPackagePacket { - DataPackage = new DataPackage + DataPackage = new Models.DataPackage { Games = new Dictionary { { "Two", new TestGameData("6") } @@ -220,16 +219,16 @@ public void Should_merge_received_datapackage_with_cached_version() sut.TryGetDataPackageFromCache(out var inMemoryDataPackage); - Assert.IsTrue(inMemoryDataPackage.Games.Count == 3 - && inMemoryDataPackage.Games["One"].Checksum == "1" - && inMemoryDataPackage.Games["Two"].Checksum == "6" - && inMemoryDataPackage.Games["Archipelago"].Checksum == "3"); + Assert.IsTrue(inMemoryDataPackage.Count == 3 + && inMemoryDataPackage["One"].Checksum == "1" + && inMemoryDataPackage["Two"].Checksum == "6" + && inMemoryDataPackage["Archipelago"].Checksum == "3"); } [Test] public void Should_request_data_package_for_games_the_server_fails_to_send_the_version_for() { - var localDataPackage = new DataPackage + var localDataPackage = new Models.DataPackage { Games = new Dictionary { { "One", new TestGameData("1") }, @@ -263,7 +262,89 @@ public void Should_request_data_package_for_games_the_server_fails_to_send_the_v )); } - class TestGameData : GameData + + [Test] + public void Should_allow_overlapping_ids() + { + var localDataPackage = new Models.DataPackage + { + Games = new Dictionary { + { "One", new TestGameData("1") }, + { "Two", new TestGameData("3") }, + { "Archipelago", new TestGameData("3") }, + } + }; + + var roomInfo = new RoomInfoPacket + { + Version = new NetworkVersion(0, 4, 0), + Games = new[] { "One", "Two", "Archipelago" }, + DataPackageChecksums = new Dictionary { + { "One", "2" }, + { "Two", "1" }, + { "Archipelago", "3" } + } + }; + + var serverDataPackage = new DataPackagePacket + { + DataPackage = new Models.DataPackage + { + Games = new Dictionary { + { "One", new TestGameData("2") { + ItemLookup = new Dictionary { + { "GameOneItem", 20 }, + { "DuplicatedName", 15 } + }, + LocationLookup = new Dictionary { + { "GameOneLocation", 20 }, + { "DuplicatedName", 15 } + } + }}, + { "Two", new TestGameData("1") { + ItemLookup = new Dictionary { + { "GameTwoItem", 20 }, + { "DuplicatedName", 30 } + }, + LocationLookup = new Dictionary { + { "GameTwoLocation", 20 }, + { "DuplicatedName", 15 } + } + }} + } + } + }; + + var socket = Substitute.For(); + var fileSystemDataPackageProvider = Substitute.For(); + fileSystemDataPackageProvider.TryGetDataPackage("", "", out _) + .ReturnsForAnyArgs(x => { + x[2] = localDataPackage.Games[x.ArgAt(0)]; + return true; + }); + + var sut = new DataPackageCache(socket, fileSystemDataPackageProvider); + + socket.PacketReceived += Raise.Event(roomInfo); + socket.PacketReceived += Raise.Event(serverDataPackage); + + sut.TryGetDataPackageFromCache(out var inMemoryDataPackage); + + Assert.IsTrue(inMemoryDataPackage.Count == 3 + && inMemoryDataPackage["One"].Checksum == "2" + && inMemoryDataPackage["One"].Items["GameOneItem"] == 20 + && inMemoryDataPackage["One"].Items["DuplicatedName"] == 15 + && inMemoryDataPackage["One"].Locations["GameOneLocation"] == 20 + && inMemoryDataPackage["One"].Locations["DuplicatedName"] == 15 + && inMemoryDataPackage["Two"].Checksum == "1" + && inMemoryDataPackage["Two"].Items["GameTwoItem"] == 20 + && inMemoryDataPackage["Two"].Items["DuplicatedName"] == 30 + && inMemoryDataPackage["Two"].Locations["GameTwoLocation"] == 20 + && inMemoryDataPackage["Two"].Locations["DuplicatedName"] == 15 + && inMemoryDataPackage["Archipelago"].Checksum == "3"); + } + + class TestGameData : GameData { public TestGameData(string checksum) { diff --git a/Archipelago.MultiClient.Net.Tests/DataPackageFileSystemCacheFixture_version_based.cs b/Archipelago.MultiClient.Net.Tests/DataPackageFileSystemCacheFixture_version_based.cs deleted file mode 100644 index cb0e45f..0000000 --- a/Archipelago.MultiClient.Net.Tests/DataPackageFileSystemCacheFixture_version_based.cs +++ /dev/null @@ -1,378 +0,0 @@ -using Archipelago.MultiClient.Net.Cache; -using Archipelago.MultiClient.Net.Helpers; -using Archipelago.MultiClient.Net.Models; -using Archipelago.MultiClient.Net.Packets; -using NSubstitute; -using NUnit.Framework; -using System; -using System.Collections.Generic; - -namespace Archipelago.MultiClient.Net.Tests -{ - //use Directory C:\Users\current_user\AppData\Local\Archipelago\Cache - //retention 1 month since last moddified, update File.SetLastWriteTime(file, modifiedTime) in use - - - [TestFixture] - public class DataPackageFileSystemCacheFixture_version_based - { - [Test] - public void Should_use_version_based_file_system_provider() - { - var socket = Substitute.For(); - - var sut = new DataPackageCache(socket); - - socket.PacketReceived += Raise.Event(new RoomInfoPacket - { - Version = new NetworkVersion(0, 3, 7), - Games = Array.Empty() - }); - - Assert.That(sut.FileSystemDataPackageProvider, Is.InstanceOf()); - } - - - [Test] - public void Should_request_data_package_when_no_local_cache_is_available() - { - var socket = Substitute.For(); - var fileSystemDataPackageProvider = Substitute.For(); - fileSystemDataPackageProvider.TryGetDataPackage(Arg.Any(), "", out _) - .Returns(x => - { - x[2] = null; - return false; - }); - - _ = new DataPackageCache(socket, fileSystemDataPackageProvider); - - socket.PacketReceived += Raise.Event(new RoomInfoPacket { - Version = new NetworkVersion(0, 3, 7), - Games = new []{ "One" } - }); - - socket.Received().SendPacket(Arg.Is(p => - p.Games.Length == 2 && p.Games[0] == "One" && p.Games[1] == "Archipelago" - )); - } - - [Test] - public void Should_request_updates_for_archipelago_even_if_its_being_played() - { - var localDataPackage = new DataPackage - { - Games = new Dictionary { - { "One", new TestGameData(1) }, - { "Archipelago", new TestGameData(1) } - } - }; - - var roomInfo = new RoomInfoPacket - { - Version = new NetworkVersion(0, 3, 7), - Games = new[] { "One" }, - DataPackageVersions = new Dictionary { - { "One", 1 }, - { "Archipelago", 2 } - } - }; - - var socket = Substitute.For(); - var fileSystemDataPackageProvider = Substitute.For(); - fileSystemDataPackageProvider.TryGetDataPackage(Arg.Any(), "", out _) - .Returns(x => { - x[2] = localDataPackage.Games[x.ArgAt(0)]; - return true; - }); - - _ = new DataPackageCache(socket, fileSystemDataPackageProvider); - - socket.PacketReceived += Raise.Event(roomInfo); - - socket.Received().SendPacket(Arg.Is(p => - p.Games.Length == 1 && p.Games[0] == "Archipelago" - )); - } - - [Test] - public void Should_not_request_data_when_local_version_is_same_as_server_version() - { - var localDataPackage = new DataPackage { - Games = new Dictionary { - { "One", new TestGameData(3) }, - { "Two", new TestGameData(4) }, - { "Archipelago", new TestGameData(1) } - } - }; - - var roomInfo = new RoomInfoPacket { - Version = new NetworkVersion(0, 3, 7), - Games = new []{ "One", "Two" }, - DataPackageVersions = new Dictionary { - { "One", 3 }, - { "Two", 4 }, - { "Five", 7 }, - { "Archipelago", 1 } - } - }; - - var socket = Substitute.For(); - var fileSystemDataPackageProvider = Substitute.For(); - fileSystemDataPackageProvider.TryGetDataPackage(Arg.Any(), "", out _) - .Returns(x => { - x[2] = localDataPackage.Games[x.ArgAt(0)]; - return true; - }); - - _ = new DataPackageCache(socket, fileSystemDataPackageProvider); - - socket.PacketReceived += Raise.Event(roomInfo); - - socket.DidNotReceive().SendPacket(Arg.Any()); - } - - [Test] - public void Should_only_request_data_for_games_that_a_have_different_version_then_cached() - { - var localDataPackage = new DataPackage - { - Games = new Dictionary { - { "One", new TestGameData(3) }, - { "Two", new TestGameData(7) }, - { "Three", new TestGameData(5) }, - { "Archipelago", new TestGameData(1) } - } - }; - - var roomInfo = new RoomInfoPacket - { - Version = new NetworkVersion(0, 3, 7), - Games = new[] { "One", "Two", "Three" }, - DataPackageVersions = new Dictionary { - { "One", 3 }, - { "Two", 6 }, - { "Three", 6 }, - { "Archipelago", 1 } - } - }; - - var socket = Substitute.For(); - var fileSystemDataPackageProvider = Substitute.For(); - fileSystemDataPackageProvider.TryGetDataPackage(Arg.Any(), "", out _) - .Returns(x => { - x[2] = localDataPackage.Games[x.ArgAt(0)]; - return true; - }); - - _ = new DataPackageCache(socket, fileSystemDataPackageProvider); - - socket.PacketReceived += Raise.Event(roomInfo); - - socket.Received().SendPacket(Arg.Is(p => - p.Games.Length == 2 && p.Games[0] == "Two" && p.Games[1] == "Three" - )); - } - - [Test] - public void Should_save_received_datapackage_contents() - { - var serverDataPackage = new DataPackagePacket { - DataPackage = new DataPackage { - Games = new Dictionary { - { - "One", new GameData { - Version = 2, - ItemLookup = new Dictionary { - { "ItemOne", 101 } - }, - LocationLookup = new Dictionary { - { "LocationOne", 201 }, - { "LocationTwo", 202 } - } - } - } - } - } - }; - - var socket = Substitute.For(); - var fileSystemDataPackageProvider = Substitute.For(); - - _ = new DataPackageCache(socket, fileSystemDataPackageProvider); - - socket.PacketReceived += Raise.Event(serverDataPackage); - - fileSystemDataPackageProvider.Received().SaveDataPackageToFile("One", Arg.Is(d => - d.Version == 2 - && d.ItemLookup.Count == 1 - && d.ItemLookup["ItemOne"] == 101 - && d.LocationLookup.Count == 2 - && d.LocationLookup["LocationOne"] == 201 - && d.LocationLookup["LocationTwo"] == 202 - )); - } - - [Test] - public void Should_merge_received_datapackage_with_cached_version() - { - var localDataPackage = new DataPackage - { - Games = new Dictionary { - { "One", new TestGameData(1) }, - { "Two", new TestGameData(3) }, - { "Archipelago", new TestGameData(3) }, - } - }; - - var roomInfo = new RoomInfoPacket - { - Version = new NetworkVersion(0, 3, 7), - Games = new[] { "One", "Two" }, - DataPackageVersions = new Dictionary { - { "One", 1 }, - { "Two", 6 }, - { "Archipelago", 3 } - } - }; - - var serverDataPackage = new DataPackagePacket - { - DataPackage = new DataPackage - { - Games = new Dictionary { - { "One", new TestGameData(2) } - } - } - }; - - var socket = Substitute.For(); - var fileSystemDataPackageProvider = Substitute.For(); - fileSystemDataPackageProvider.TryGetDataPackage(Arg.Any(), "", out _) - .Returns(x => { - x[2] = localDataPackage.Games[x.ArgAt(0)]; - return true; - }); - - var sut = new DataPackageCache(socket, fileSystemDataPackageProvider); - - socket.PacketReceived += Raise.Event(roomInfo); - socket.PacketReceived += Raise.Event(serverDataPackage); - - sut.TryGetDataPackageFromCache(out var inMemoryDataPackage); - - Assert.IsTrue(inMemoryDataPackage.Games.Count == 3 - && inMemoryDataPackage.Games["One"].Version == 2 - && inMemoryDataPackage.Games["Two"].Version == 3 - && inMemoryDataPackage.Games["Archipelago"].Version == 3); - } - - [Test] - public void Should_not_save_game_when_version_is_0_but_still_keep_it_in_memory() - { - var localDataPackage = new DataPackage - { - Games = new Dictionary { - { "One", new TestGameData(2) } - } - }; - - var serverDataPackage = new DataPackagePacket - { - DataPackage = new DataPackage - { - Games = new Dictionary { - { "One", new TestGameData(3) }, - { "Two", new TestGameData(0) } - } - } - }; - - var socket = Substitute.For(); - var fileSystemDataPackageProvider = Substitute.For(); - fileSystemDataPackageProvider.TryGetDataPackage(Arg.Any(), "", out _) - .Returns(x => { - x[2] = localDataPackage.Games[x.ArgAt(0)]; - return true; - }); - - var sut = new DataPackageCache(socket, fileSystemDataPackageProvider); - - socket.PacketReceived += Raise.Event(serverDataPackage); - - fileSystemDataPackageProvider.Received().SaveDataPackageToFile("One", Arg.Is(d => d.Version == 3)); - fileSystemDataPackageProvider.DidNotReceive().SaveDataPackageToFile("Two", Arg.Any()); - - sut.TryGetGameDataFromCache("One", out var gameDataGameOne); - sut.TryGetGameDataFromCache("Two", out var gameDataGameTwo); - - Assert.That(gameDataGameOne.Version, Is.EqualTo(3)); - Assert.That(gameDataGameTwo.Version, Is.EqualTo(0)); - } - - [Test] - public void Should_request_data_package_for_all_games_when_versions_are_not_provided_on_the_roominfo() - { - var roomInfo = new RoomInfoPacket - { - Version = new NetworkVersion(0, 3, 7), - Games = new[] { "One", "Two" }, - DataPackageVersions = null - }; - - var socket = Substitute.For(); - var fileSystemDataPackageProvider = Substitute.For(); - fileSystemDataPackageProvider.TryGetDataPackage(Arg.Any(), "", out _) - .Returns(x => { - x[2] = new GameData(); - return true; - }); - - _ = new DataPackageCache(socket, fileSystemDataPackageProvider); - - socket.PacketReceived += Raise.Event(roomInfo); - - socket.Received().SendPacket(Arg.Is(p => - p.Games.Length == 3 && p.Games[0] == "One" && p.Games[1] == "Two" && p.Games[2] == "Archipelago" - )); - } - - [Test] - public void Should_request_data_package_for_games_the_server_fails_to_send_the_version_for() - { - var roomInfo = new RoomInfoPacket - { - Version = new NetworkVersion(0, 3, 7), - Games = new[] { "One", "Two" }, - DataPackageVersions = new Dictionary { - { "One", 0 } - } - }; - - var socket = Substitute.For(); - var fileSystemDataPackageProvider = Substitute.For(); - fileSystemDataPackageProvider.TryGetDataPackage(Arg.Any(), "", out _) - .Returns(x => { - x[2] = new GameData(); - return true; - }); - - _ = new DataPackageCache(socket, fileSystemDataPackageProvider); - - socket.PacketReceived += Raise.Event(roomInfo); - - socket.Received().SendPacket(Arg.Is(p => - p.Games.Length == 2 && p.Games[0] == "Two" && p.Games[1] == "Archipelago" - )); - } - class TestGameData : GameData - { - public TestGameData(int version) - { - Version = version; - LocationLookup = new Dictionary(0); - ItemLookup = new Dictionary(0); - } - } - } -} diff --git a/Archipelago.MultiClient.Net.Tests/DataStorageHelperFixture.cs b/Archipelago.MultiClient.Net.Tests/DataStorageHelperFixture.cs index 03e0b92..415fbff 100644 --- a/Archipelago.MultiClient.Net.Tests/DataStorageHelperFixture.cs +++ b/Archipelago.MultiClient.Net.Tests/DataStorageHelperFixture.cs @@ -12,6 +12,10 @@ using System.Threading.Tasks; using Callback = Archipelago.MultiClient.Net.Models.Callback; +#if !NET471 +using System.Numerics; +#endif + // ReSharper disable All // ReSharper disable UnusedVariable namespace Archipelago.MultiClient.Net.Tests @@ -72,43 +76,43 @@ public void Should_get_value_correctly(Func get public static TestCaseData[] CompoundAssignmentTests => new TestCaseData[] { new CompoundAssignmentTest("Assignment", true, (sut, key, value) => sut[key] = value, - (p, key, value) => p.Key == key && (bool)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Boolean && (bool)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", true, (sut, key, value) => sut[key] = value, - (p, key, value) => p.Key == key && (bool)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Boolean && (bool)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", null, (sut, key, value) => sut[key] = value, (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Null && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", 30, (sut, key, value) => sut[key] = value, - (p, key, value) => p.Key == key && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", 30, (sut, key, value) => sut[key] = value, - (p, key, value) => p.Key == key && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", null, (sut, key, value) => sut[key] = value, (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Null && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", 300L, (sut, key, value) => sut[key] = value, - (p, key, value) => p.Key == key && (long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", 300L, (sut, key, value) => sut[key] = value, - (p, key, value) => p.Key == key && (long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", null, (sut, key, value) => sut[key] = value, (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Null && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", 3.003m, (sut, key, value) => sut[key] = value, - (p, key, value) => p.Key == key && (decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", 3.003m, (sut, key, value) => sut[key] = value, - (p, key, value) => p.Key == key && (decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", null, (sut, key, value) => sut[key] = value, (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Null && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", 3.03d, (sut, key, value) => sut[key] = value, - (p, key, value) => p.Key == key && (double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", 3.03d, (sut, key, value) => sut[key] = value, - (p, key, value) => p.Key == key && (double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", null, (sut, key, value) => sut[key] = value, (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Null && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", 3f, (sut, key, value) => sut[key] = value, - (p, key, value) => p.Key == key && (float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", 3f, (sut, key, value) => sut[key] = value, - (p, key, value) => p.Key == key && (float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", null, (sut, key, value) => sut[key] = value, (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Null && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", "test", (sut, key, value) => sut[key] = value, - (p, key, value) => p.Key == key && (string)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.String && (string)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", null, (sut, key, value) => sut[key] = value, (p, key, value) =>p.Key == key && p.Operations[0].Value.Type == JTokenType.Null && p.Operations[0].OperationType == OperationType.Replace), new CompoundAssignmentTest("Assignment", new []{ true, false }, (sut, key, value) => sut[key] = value, @@ -146,17 +150,17 @@ public void Should_get_value_correctly(Func get new CompoundAssignmentTest>("Assignment", new List() { "Hello", 101 }, (sut, key, value) => sut[key] = value, (p, key, value) => p.Key == key && p.Operations[0].OperationType == OperationType.Replace && p.Operations[0].Value is JArray), new CompoundAssignmentTest("Inplace Addition", 10, (sut, key, value) => sut[key] += value, - (p, key, value) => p.Key == key && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), new CompoundAssignmentTest("Inplace Addition", 300L, (sut, key, value) => sut[key] += value, - (p, key, value) => p.Key == key && (long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), new CompoundAssignmentTest("Inplace Addition", 3.003m, (sut, key, value) => sut[key] += value, - (p, key, value) => p.Key == key && (decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), new CompoundAssignmentTest("Inplace Addition", 3.03d, (sut, key, value) => sut[key] += value, - (p, key, value) => p.Key == key && (double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), new CompoundAssignmentTest("Inplace Addition", 3f, (sut, key, value) => sut[key] += value, - (p, key, value) => p.Key == key && (float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), new CompoundAssignmentTest("Inplace Addition", "test", (sut, key, value) => sut[key] += value, - (p, key, value) => p.Key == key && (string)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.String && (string)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), new CompoundAssignmentTest("Inplace Addition", new []{ 1, 2 }, (sut, key, value) => sut[key] += value, (p, key, value) => p.Key == key && p.Operations[0].OperationType == OperationType.Add && p.Operations[0].Value is JArray), new CompoundAssignmentTest("Inplace Addition", new []{ true, false }, (sut, key, value) => sut[key] += value, @@ -193,81 +197,95 @@ public void Should_get_value_correctly(Func get (p, key, value) => p.Key == key && p.Operations[0].OperationType == OperationType.Add && p.Operations[0].Value is JArray), new CompoundAssignmentTest>("Inplace Addition", new List() { "Hello", 101 }, (sut, key, value) => sut[key] += value, (p, key, value) => p.Key == key && p.Operations[0].OperationType == OperationType.Add && p.Operations[0].Value is JArray), - new CompoundAssignmentTest("Inplace Addition", Bitwise.Xor(0xFF), (sut, key, value) => sut[key] += value, + new CompoundAssignmentTest("Inplace Addition of Operator", Bitwise.Xor(0xFF), (sut, key, value) => sut[key] += value, (p, key, value) => p.Key == key && p.Operations[0].OperationType == OperationType.Xor && p.Operations[0].Value == value.Value), - new CompoundAssignmentTest("Inplace Addition", Bitwise.Or(0x00F0), (sut, key, value) => sut[key] += value, + new CompoundAssignmentTest("Inplace Addition of Operator", Bitwise.Or(0x00F0), (sut, key, value) => sut[key] += value, (p, key, value) => p.Key == key && p.Operations[0].OperationType == OperationType.Or && p.Operations[0].Value == value.Value), - new CompoundAssignmentTest("Inplace Addition", Bitwise.And(0b01011), (sut, key, value) => sut[key] += value, + new CompoundAssignmentTest("Inplace Addition of Operator", Bitwise.And(0b01011), (sut, key, value) => sut[key] += value, (p, key, value) => p.Key == key && p.Operations[0].OperationType == OperationType.And && p.Operations[0].Value == value.Value), - new CompoundAssignmentTest("Inplace Addition", Bitwise.LeftShift(4), (sut, key, value) => sut[key] += value, + new CompoundAssignmentTest("Inplace Addition of Operator", Bitwise.LeftShift(4), (sut, key, value) => sut[key] += value, (p, key, value) => p.Key == key && p.Operations[0].OperationType == OperationType.LeftShift && p.Operations[0].Value == value.Value), - new CompoundAssignmentTest("Inplace Addition", Bitwise.RightShift(8), (sut, key, value) => sut[key] += value, + new CompoundAssignmentTest("Inplace Addition of Operator", Bitwise.RightShift(8), (sut, key, value) => sut[key] += value, (p, key, value) => p.Key == key && p.Operations[0].OperationType == OperationType.RightShift && p.Operations[0].Value == value.Value), new CompoundAssignmentTest("Inplace Subtraction", 10, (sut, key, value) => sut[key] -= value, - (p, key, value) => p.Key == key && -(int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && -(int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), new CompoundAssignmentTest("Inplace Subtraction", 300L, (sut, key, value) => sut[key] -= value, - (p, key, value) => p.Key == key && -(long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && -(long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), new CompoundAssignmentTest("Inplace Subtraction", 3.003m, (sut, key, value) => sut[key] -= value, - (p, key, value) => p.Key == key && -(decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && -(decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), new CompoundAssignmentTest("Inplace Subtraction", 3.03d, (sut, key, value) => sut[key] -= value, - (p, key, value) => p.Key == key && -(double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && -(double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), new CompoundAssignmentTest("Inplace Subtraction", 3f, (sut, key, value) => sut[key] -= value, - (p, key, value) => p.Key == key && -(float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && -(float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Add), new CompoundAssignmentTest("Inplace Multiplication", 10, (sut, key, value) => sut[key] *= value, - (p, key, value) => p.Key == key && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mul), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mul), new CompoundAssignmentTest("Inplace Multiplication", 300L, (sut, key, value) => sut[key] *= value, - (p, key, value) => p.Key == key && (long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mul), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mul), new CompoundAssignmentTest("Inplace Multiplication", 3.003m, (sut, key, value) => sut[key] *= value, - (p, key, value) => p.Key == key && (decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mul), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mul), new CompoundAssignmentTest("Inplace Multiplication", 3.03d, (sut, key, value) => sut[key] *= value, - (p, key, value) => p.Key == key && (double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mul), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mul), new CompoundAssignmentTest("Inplace Multiplication", 3f, (sut, key, value) => sut[key] *= value, - (p, key, value) => p.Key == key && (float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mul), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mul), new CompoundAssignmentTest("Inplace Division", 10, (sut, key, value) => sut[key] /= value, - (p, key, value) => p.Key == key && (decimal)p.Operations[0].Value == 1m/value && p.Operations[0].OperationType == OperationType.Mul), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (decimal)p.Operations[0].Value == 1m/value && p.Operations[0].OperationType == OperationType.Mul), new CompoundAssignmentTest("Inplace Division", 300L, (sut, key, value) => sut[key] /= value, - (p, key, value) => p.Key == key && (decimal)p.Operations[0].Value == 1m/value && p.Operations[0].OperationType == OperationType.Mul), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (decimal)p.Operations[0].Value == 1m/value && p.Operations[0].OperationType == OperationType.Mul), new CompoundAssignmentTest("Inplace Division", 3.003m, (sut, key, value) => sut[key] /= value, - (p, key, value) => p.Key == key && (decimal)p.Operations[0].Value == 1m/value && p.Operations[0].OperationType == OperationType.Mul), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (decimal)p.Operations[0].Value == 1m/value && p.Operations[0].OperationType == OperationType.Mul), new CompoundAssignmentTest("Inplace Division", 3.03d, (sut, key, value) => sut[key] /= value, - (p, key, value) => p.Key == key && (double)p.Operations[0].Value == 1d/value && p.Operations[0].OperationType == OperationType.Mul), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (double)p.Operations[0].Value == 1d/value && p.Operations[0].OperationType == OperationType.Mul), new CompoundAssignmentTest("Inplace Division", 3f, (sut, key, value) => sut[key] /= value, - (p, key, value) => p.Key == key && (double)p.Operations[0].Value == 1d/value && p.Operations[0].OperationType == OperationType.Mul), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (double)p.Operations[0].Value == 1d/value && p.Operations[0].OperationType == OperationType.Mul), new CompoundAssignmentTest("Inplace Modulus", 10, (sut, key, value) => sut[key] %= value, - (p, key, value) => p.Key == key && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mod), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mod), new CompoundAssignmentTest("Inplace Modulus", 300L, (sut, key, value) => sut[key] %= value, - (p, key, value) => p.Key == key && (long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mod), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mod), new CompoundAssignmentTest("Inplace Modulus", 3.003m, (sut, key, value) => sut[key] %= value, - (p, key, value) => p.Key == key && (decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mod), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mod), new CompoundAssignmentTest("Inplace Modulus", 3.03d, (sut, key, value) => sut[key] %= value, - (p, key, value) => p.Key == key && (double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mod), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mod), new CompoundAssignmentTest("Inplace Modulus", 3f, (sut, key, value) => sut[key] %= value, - (p, key, value) => p.Key == key && (float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mod), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Mod), new CompoundAssignmentTest("Inplace Exponentiation", 10, (sut, key, value) => sut[key] ^= value, - (p, key, value) => p.Key == key && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Pow), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Pow), new CompoundAssignmentTest("Inplace Exponentiation", 300L, (sut, key, value) => sut[key] ^= value, - (p, key, value) => p.Key == key && (long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Pow), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (long)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Pow), new CompoundAssignmentTest("Inplace Exponentiation", 3.003m, (sut, key, value) => sut[key] ^= value, - (p, key, value) => p.Key == key && (decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Pow), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (decimal)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Pow), new CompoundAssignmentTest("Inplace Exponentiation", 3.03d, (sut, key, value) => sut[key] ^= value, - (p, key, value) => p.Key == key && (double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Pow), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (double)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Pow), new CompoundAssignmentTest("Inplace Exponentiation", 3f, (sut, key, value) => sut[key] ^= value, - (p, key, value) => p.Key == key && (float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Pow), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Float && (float)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Pow), new CompoundAssignmentTest("Postfix Addition", 0, (sut, key, value) => sut[key]++, - (p, key, value) => p.Key == key && (int)p.Operations[0].Value == 1 && p.Operations[0].OperationType == OperationType.Add), + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (int)p.Operations[0].Value == 1 && p.Operations[0].OperationType == OperationType.Add), new CompoundAssignmentTest("Postfix Subtraction", 0, (sut, key, value) => sut[key]--, - (p, key, value) => p.Key == key && (int)p.Operations[0].Value == -1 && p.Operations[0].OperationType == OperationType.Add), - new CompoundAssignmentTest("Inplace Addition", Operation.Max(10), (sut, key, value) => sut[key] += value, + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && (int)p.Operations[0].Value == -1 && p.Operations[0].OperationType == OperationType.Add), + new CompoundAssignmentTest("Inplace Addition of Operator", Operation.Max(10), (sut, key, value) => sut[key] += value, (p, key, value) => p.Key == key && p.Operations[0].OperationType == OperationType.Max && p.Operations[0].Value == value.Value), - new CompoundAssignmentTest("Inplace Addition", Operation.Min(10), (sut, key, value) => sut[key] += value, + new CompoundAssignmentTest("Inplace Addition of Operator", Operation.Min(10), (sut, key, value) => sut[key] += value, (p, key, value) => p.Key == key && p.Operations[0].OperationType == OperationType.Min && p.Operations[0].Value == value.Value), - - //TODO Remove - new CompoundAssignmentTest("Inplace Maximum", 10, (sut, key, value) => sut[key] <<= value, - (p, key, value) => p.Key == key && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Max), - new CompoundAssignmentTest("Inplace Minimum", 10, (sut, key, value) => sut[key] >>= value, - (p, key, value) => p.Key == key && (int)p.Operations[0].Value == value && p.Operations[0].OperationType == OperationType.Min) - }; + new CompoundAssignmentTest("Flooring", Operation.Floor(), (sut, key, value) => sut[key] += value, + (p, key, value) => p.Key == key && p.Operations[0].Value == null && p.Operations[0].OperationType == OperationType.Floor), + new CompoundAssignmentTest("Ceiling", Operation.Ceiling(), (sut, key, value) => sut[key] += value, + (p, key, value) => p.Key == key && p.Operations[0].Value == null && p.Operations[0].OperationType == OperationType.Ceil), + + +#if !NET471 + new CompoundAssignmentTest("Assignment", BigInteger.Parse("9223372036854775808"), (sut, key, value) => sut[key] = value, + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && p.Operations[0].Value.ToString() == "9223372036854775808" && p.Operations[0].OperationType == OperationType.Replace), + new CompoundAssignmentTest("Inplace Addition", BigInteger.Parse("9223372036854775808"), (sut, key, value) => sut[key] += value, + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && p.Operations[0].Value.ToString() == "9223372036854775808" && p.Operations[0].OperationType == OperationType.Add), + new CompoundAssignmentTest("Inplace Subtraction", BigInteger.Parse("9223372036854775808"), (sut, key, value) => sut[key] -= value, + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && p.Operations[0].Value.ToString() == "-9223372036854775808" && p.Operations[0].OperationType == OperationType.Add), + new CompoundAssignmentTest("Inplace Multiplication", BigInteger.Parse("9223372036854775808"), (sut, key, value) => sut[key] *= value, + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && p.Operations[0].Value.ToString() == "9223372036854775808" && p.Operations[0].OperationType == OperationType.Mul), + new CompoundAssignmentTest("Inplace Modulus", BigInteger.Parse("9223372036854775808"), (sut, key, value) => sut[key] %= value, + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && p.Operations[0].Value.ToString() == "9223372036854775808" && p.Operations[0].OperationType == OperationType.Mod), + new CompoundAssignmentTest("Inplace Exponentiation", BigInteger.Parse("9223372036854775808"), (sut, key, value) => sut[key] ^= value, + (p, key, value) => p.Key == key && p.Operations[0].Value.Type == JTokenType.Integer && p.Operations[0].Value.ToString() == "9223372036854775808" && p.Operations[0].OperationType == OperationType.Pow), +#endif + }; [TestCaseSource(nameof(CompoundAssignmentTests))] public void Should_handle_compound_Assignment_correctly( @@ -285,7 +303,32 @@ public void Should_handle_compound_Assignment_correctly( socket.Received().SendPacketAsync(Arg.Is(p => validatePacket(p, "Key", value) && p.Operations.Length == 1)); } - public static TestCaseData[] AssignmentTests => + public static TestCaseData[] CompoundAssignmentThrowsTests => + new TestCaseData[] { +#if !NET471 + // 1 / BigInterger is not posiable in c# unless we get super creative with math + new CompoundAssignmentThrowsTest("Inplace Division", BigInteger.Parse("9223372036854775808"), (sut, key, value) => sut[key] /= value, + new InvalidOperationException("DataStorage[Key] / BigInterger is not supported, due to loss of precision when using integer division")), +#endif + }; + + [TestCaseSource(nameof(CompoundAssignmentThrowsTests))] + public void Should_throw_on_unsupported_Assignment_correctly( + T value, Action action, + Exception expectedException) + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + + var sut = new DataStorageHelper(socket, connectionInfo); + + var thrownException = Assert.Throws(expectedException.GetType(), () => action(sut, "Key", value)); + Assert.That(thrownException.Message, Is.EqualTo(expectedException.Message)); + + socket.DidNotReceive().SendPacket(Arg.Any()); + } + + public static TestCaseData[] AssignmentTests => new TestCaseData[] { new AssignmentTest("Addition", 30, (sut, key) => sut[key] + 5, 35), new AssignmentTest("Addition", 1000L, (sut, key) => sut[key] + 10L, 1010L), @@ -309,11 +352,11 @@ public void Should_handle_compound_Assignment_correctly( new AssignmentTest>("Addition", new List{ 2f }, (sut, key) => sut[key] + new List{ 3f }, new List{ 2f, 3f }), new AssignmentTest>("Addition", new List{ "A" }, (sut, key) => sut[key] + new List{ "B" }, new List{ "A", "B" }), new AssignmentTest>("Addition", new List{ 1 }, (sut, key) => sut[key] + new List{ "C" }, new List{ 1, "C" }), - new AssignmentTest("Addition", 0xF0, (sut, key) => sut[key] + Bitwise.Xor(0xFF), 0x0F), - new AssignmentTest("Addition", 0xF0, (sut, key) => sut[key] + Bitwise.Or(0x0F), 0xFF), - new AssignmentTest("Addition", 0xFF, (sut, key) => sut[key] + Bitwise.And(0x0F), 0x0F), - new AssignmentTest("Addition", 0b101, (sut, key) => sut[key] + Bitwise.LeftShift(1), 0b1010), - new AssignmentTest("Addition", 0b101, (sut, key) => sut[key] + Bitwise.RightShift(2), 0b1), + new AssignmentTest("Xor", 0xF0, (sut, key) => sut[key] + Bitwise.Xor(0xFF), 0x0F), + new AssignmentTest("Or", 0xF0, (sut, key) => sut[key] + Bitwise.Or(0x0F), 0xFF), + new AssignmentTest("And", 0xFF, (sut, key) => sut[key] + Bitwise.And(0x0F), 0x0F), + new AssignmentTest("LeftShift", 0b101, (sut, key) => sut[key] + Bitwise.LeftShift(1), 0b1010), + new AssignmentTest("RigttShift", 0b101, (sut, key) => sut[key] + Bitwise.RightShift(2), 0b1), new AssignmentTest("Subtraction", 30, (sut, key) => sut[key] - 5, 25), new AssignmentTest("Subtraction", 1000L, (sut, key) => sut[key] - 10L, 990L), new AssignmentTest("Subtraction", 1.001m, (sut, key) => sut[key] - 0.2m, 0.801m), @@ -343,12 +386,25 @@ public void Should_handle_compound_Assignment_correctly( new AssignmentTest("Maximum", 20, (sut, key) => sut[key] + Operation.Max(5), 20), new AssignmentTest("Minimum", 2, (sut, key) => sut[key] + Operation.Min(5), 2), new AssignmentTest("Minimum", 20, (sut, key) => sut[key] + Operation.Min(5), 5), - - //TODO: Remove - new AssignmentTest("Maximum", 2, (sut, key) => sut[key] << 5, 5), - new AssignmentTest("Maximum", 20, (sut, key) => sut[key] << 5, 20), - new AssignmentTest("Minimum", 2, (sut, key) => sut[key] >> 5, 2), - new AssignmentTest("Minimum", 20, (sut, key) => sut[key] >> 5, 5), + new AssignmentTest("Floor", 13.0856m, (sut, key) => sut[key] + Operation.Floor(), 13), + new AssignmentTest("Floor", 3.33d, (sut, key) => sut[key] + Operation.Floor(), 3), + new AssignmentTest("Floor", 2.2f, (sut, key) => sut[key] + Operation.Floor(), 2), + new AssignmentTest("Ceiling", 13.0856m, (sut, key) => sut[key] + Operation.Ceiling(), 14), + new AssignmentTest("Ceiling", 3.33d, (sut, key) => sut[key] + Operation.Ceiling(), 4), + new AssignmentTest("Ceiling", 2.2f, (sut, key) => sut[key] + Operation.Ceiling(), 3), + +#if !NET471 + //Beeg int + new AssignmentTest("Addition", BigInteger.Parse("9223372036854775800"), (sut, key) => sut[key] + 8, BigInteger.Parse("9223372036854775808")), + new AssignmentTest("Subtraction", BigInteger.Parse("9223372036854775808"), (sut, key) => sut[key] - BigInteger.Parse("5808"), BigInteger.Parse("9223372036854770000")), + new AssignmentTest("Multiplication", BigInteger.Parse("10000000000"), (sut, key) => sut[key] * 10000000000, BigInteger.Parse("100000000000000000000")), + new AssignmentTest("Modulus", BigInteger.Parse("10"), (sut, key) => sut[key] % 3, BigInteger.Parse("1")), + new AssignmentTest("Exponentiation", BigInteger.Parse("20"), (sut, key) => sut[key] ^ 200, + BigInteger.Parse("160693804425899027554196209234116260252220299378279283530137600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\r\n")), + new AssignmentTest("Maximum", BigInteger.Parse("20"), (sut, key) => sut[key] + Operation.Max(BigInteger.Parse("50")), BigInteger.Parse("50")), + new AssignmentTest("Floor", BigInteger.Parse("200"), (sut, key) => sut[key] + Operation.Floor(), BigInteger.Parse("200")), + new AssignmentTest("Ceiling", BigInteger.Parse("200"), (sut, key) => sut[key] + Operation.Ceiling(), BigInteger.Parse("200")), +#endif }; [TestCaseSource(nameof(AssignmentTests))] @@ -368,7 +424,32 @@ public void Should_handle_Assignment_correctly(T baseValue, Func + new TestCaseData[] { +#if !NET471 + new AssignmentThrowTest("Division", BigInteger.Parse("10"), (sut, key) => sut[key] / 2.5, + new InvalidOperationException($"DataStorage[Key] cannot be converted to BigInterger as its value its not an integer number, value: {1/2.5}")), +#endif + }; + + [TestCaseSource(nameof(AssignmentThrowTests))] + public void Should_throw_Assignment_correctly(T baseValue, Func action, Exception expectedException) + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + + var sut = new DataStorageHelper(socket, connectionInfo); + + Exception thrownException = default; + + ExecuteAsyncWithDelay( + () => thrownException = Assert.Throws(expectedException.GetType(), () => action(sut, "Key")), + () => RaiseRetrieved(socket, "Key", JToken.FromObject(baseValue))); + + Assert.That(thrownException.Message, Is.EqualTo(expectedException.Message)); + } + + [Test] public void Should_throw_on_invalid_operation_on_string() { var socket = Substitute.For(); @@ -417,16 +498,16 @@ public void Should_register_handler_and_recieve_update() object newValueB = null; object newValueC = null; - sut["Key"].OnValueChanged += (o, n) => + sut["Key"].OnValueChanged += (o, n, _) => { oldValueA = (int)o; newValueA = (int)n; }; - sut["KeyB"].OnValueChanged += (o, n) => + sut["KeyB"].OnValueChanged += (o, n, _) => { newValueB = (string)n; }; - sut["KeyB"].OnValueChanged += (o, n) => + sut["KeyB"].OnValueChanged += (o, n, _) => { newValueC = (string)n; }; @@ -457,7 +538,7 @@ public void Should_unregister_handler() int callbackCount = 0; - void OnValueChanged(object oldValue, object newValue) + void OnValueChanged(object oldValue, object newValue, Dictionary additionalArguments) { callbackCount++; } @@ -642,19 +723,19 @@ public void Should_correctly_handle_deplete() socket.SendPacketAsync(Arg.Do(p => { if (p.Key == "DP1") - callback1Reference = p.Reference.Value; + callback1Reference = (Guid)p.AdditionalArguments["Reference"]; if (p.Key == "DP2") - callback2Reference = p.Reference.Value; + callback2Reference = (Guid)p.AdditionalArguments["Reference"]; })); - sut[Scope.Global, "DP1"] = ((sut[Scope.Global, "DP1"] + 50) + Operation.Max(0)) + Callback.Add((o, n) => + sut[Scope.Global, "DP1"] = ((sut[Scope.Global, "DP1"] + 50) + Operation.Max(0)) + Callback.Add((o, n, _) => { actualDepleteValue1 = (decimal)n - (decimal)o; }); - sut["DP2"] = ((sut["DP2"] - 11.53m) + Operation.Max(0)) + Callback.Add((o, n) => + sut["DP2"] = ((sut["DP2"] - 11.53m) + Operation.Max(0)) + Callback.Add((o, n, _) => { actualDepleteValue2 = (decimal)n - (decimal)o; - }); + }) + AdditionalArgument.Add("yarg", 10); socket.Received().SendPacketAsync(Arg.Is( p => p.Key == "DP1" && p.WantReply == true && p.Operations.Length == 2 @@ -663,7 +744,8 @@ public void Should_correctly_handle_deplete() socket.Received().SendPacketAsync(Arg.Is( p => p.Key == "DP2" && p.WantReply == true && p.Operations.Length == 2 && p.Operations[0].OperationType == OperationType.Add && (decimal)p.Operations[0].Value == -11.53m - && p.Operations[1].OperationType == OperationType.Max && (int)p.Operations[1].Value == 0)); + && p.Operations[1].OperationType == OperationType.Max && (int)p.Operations[1].Value == 0 + && (int)p.AdditionalArguments["yarg"] == 10)); var setReplyPacketA = new SetReplyPacket() { @@ -676,14 +758,17 @@ public void Should_correctly_handle_deplete() Key = "DP1", Value = 120, OriginalValue = 70, - Reference = callback1Reference + AdditionalArguments = new Dictionary { { "Reference", callback1Reference } } }; var setReplyPacketC = new SetReplyPacket() { Key = "DP2", Value = 0m, OriginalValue = 10.36m, - Reference = callback2Reference + AdditionalArguments = new Dictionary { + { "Reference", callback2Reference }, + { "yarg", 10 } + } }; socket.PacketReceived += Raise.Event(setReplyPacketA); socket.PacketReceived += Raise.Event(setReplyPacketB); @@ -913,24 +998,6 @@ public void Should_throw_on_write_opperation_on_readonly_key() socket.DidNotReceive().SendPacketAsync(Arg.Any()); } - /* - [Test] - public void Should_allow_list_opperations() - { - var socket = Substitute.For(); - var connectionInfo = Substitute.For(); - - var sut = new DataStorageHelper(socket, connectionInfo); - - //TODO define syntax - sut.Lists["list"] = new string[] { "a", "b" }; - sut["list"].GetAsync() = new string[] { "a", "b" }; - sut.List()["list"] - - socket.DidNotReceive().SendPacket(Arg.Any()); - socket.DidNotReceive().SendPacketAsync(Arg.Any()); - }*/ - public static void ExecuteAsyncWithDelay(Action retrieve, Action raiseEvent) { Task.WaitAll( new [] { @@ -978,7 +1045,19 @@ public CompoundAssignmentTest(string type, T value, Action : TestCaseData + class CompoundAssignmentThrowsTest : TestCaseData + { + public CompoundAssignmentThrowsTest(string type, T value, Action action, Exception expectedException) + : base(value, action, expectedException) + { + if (value == null) + TestName = $"{type} 'null' ({typeof(T)})"; + else + TestName = $"{type} {value} ({typeof(T)})"; + } + } + + class AssignmentTest : TestCaseData { public AssignmentTest(string type, T baseValue, Func action, T expectedValue) : base(baseValue, action, expectedValue) @@ -989,5 +1068,17 @@ public AssignmentTest(string type, T baseValue, Func : TestCaseData + { + public AssignmentThrowTest(string type, T baseValue, Func action, Exception expectedException) + : base(baseValue, action, expectedException) + { + if (baseValue == null) + TestName = $"{type} 'null' ({typeof(T)})"; + else + TestName = $"{type} {baseValue} ({typeof(T)})"; + } + } + } } diff --git a/Archipelago.MultiClient.Net.Tests/DataStorageWrapperFixture.cs b/Archipelago.MultiClient.Net.Tests/DataStorageWrapperFixture.cs index aa631bd..e8b743d 100644 --- a/Archipelago.MultiClient.Net.Tests/DataStorageWrapperFixture.cs +++ b/Archipelago.MultiClient.Net.Tests/DataStorageWrapperFixture.cs @@ -286,6 +286,7 @@ public void GetHintsAsync_should_return_null_for_non_existing_slot() () => { hintBySlotAndTeam = sut.GetHintsAsync(11, 11).Result; }, () => RaiseRetrieved(socket, "_read_hints_11_11", JValue.CreateNull())); #endif + socket.Received().SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_hints_0_11")); socket.Received().SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_hints_11_11")); Assert.Null(hintsBySlot); @@ -547,6 +548,346 @@ public void GetItemNameGroupsAsync_should_return_null_for_non_existing_game() Assert.IsNull(itemNameGroups); } + [Test] + public void GetLocationNameGroups_should_return_item_name_groups_for_current_player_slot() + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + connectionInfo.Game.Returns("MY Game"); + + var sut = new DataStorageHelper(socket, connectionInfo); + + Dictionary slotData = null; + + var serverItemNameGroups = new Dictionary { + { "Group1", new[] { "A", "B" } }, + { "Group3", new[] { "Q", "B" } }, + }; + + ExecuteAsyncWithDelay( + () => { slotData = sut.GetLocationNameGroups(); }, + () => RaiseRetrieved(socket, "_read_location_name_groups_MY Game", JObject.FromObject(serverItemNameGroups))); + + socket.Received().SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_location_name_groups_MY Game")); + + Assert.IsNotNull(slotData); + + Assert.That(slotData["Group1"][0], Is.EqualTo("A")); + Assert.That(slotData["Group1"][1], Is.EqualTo("B")); + Assert.That(slotData["Group3"][0], Is.EqualTo("Q")); + Assert.That(slotData["Group3"][1], Is.EqualTo("B")); + } + + [Test] + public void GetLocationNameGroups_should_return_null_for_non_existing_game() + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + + var sut = new DataStorageHelper(socket, connectionInfo); + + Dictionary locationNameGroups = null; + + ExecuteAsyncWithDelay( + () => { locationNameGroups = sut.GetLocationNameGroups("NOT A REAL GAME"); }, + () => RaiseRetrieved(socket, "_read_location_name_groups_NOT A REAL GAME", JValue.CreateNull())); + + socket.Received().SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_location_name_groups_NOT A REAL GAME")); + + Assert.IsNull(locationNameGroups); + } + + [Test] + public void GetLocationNameGroupsAsync_should_return_item_name_groups_for_current_player_slot() + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + connectionInfo.Game.Returns("Below Zero"); + + var sut = new DataStorageHelper(socket, connectionInfo); + + Dictionary locationNameGroups = null; + + var serverItemNameGroups = new Dictionary { + { "Group1", new[] { "A", "B" } }, + { "Group3", new[] { "Q", "B" } }, + }; + +#if NET471 + bool itemNameGroupsCallbackReceived = false; + + ExecuteAsyncWithDelay( + () => { + sut.GetLocationNameGroupsAsync(t => { + itemNameGroupsCallbackReceived = true; + locationNameGroups = t; + }); + }, + () => RaiseRetrieved(socket, "_read_location_name_groups_Below Zero", JObject.FromObject(serverItemNameGroups))); + + while (!itemNameGroupsCallbackReceived) + Thread.Sleep(10); +#else + ExecuteAsyncWithDelay( + () => { locationNameGroups = sut.GetLocationNameGroupsAsync().Result; }, + () => RaiseRetrieved(socket, "_read_location_name_groups_Below Zero", JObject.FromObject(serverItemNameGroups))); +#endif + + socket.Received().SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_location_name_groups_Below Zero")); + + Assert.IsNotNull(locationNameGroups); + + Assert.That(locationNameGroups["Group1"][0], Is.EqualTo("A")); + Assert.That(locationNameGroups["Group1"][1], Is.EqualTo("B")); + Assert.That(locationNameGroups["Group3"][0], Is.EqualTo("Q")); + Assert.That(locationNameGroups["Group3"][1], Is.EqualTo("B")); + } + + [Test] + public void GetLocationNameGroupsAsync_should_return_null_for_non_existing_game() + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + + var sut = new DataStorageHelper(socket, connectionInfo); + + Dictionary locationNameGroups = null; + +#if NET471 + bool slotDataCallbackReceived = false; + + ExecuteAsyncWithDelay( + () => { + sut.GetLocationNameGroupsAsync(t => { + slotDataCallbackReceived = true; + locationNameGroups = t; + }, "YoloTheGame"); + }, + () => RaiseRetrieved(socket, "_read_location_name_groups_YoloTheGame", JValue.CreateNull())); + + while (!slotDataCallbackReceived) + Thread.Sleep(10); +#else + ExecuteAsyncWithDelay( + () => { locationNameGroups = sut.GetLocationNameGroupsAsync("YoloTheGame").Result; }, + () => RaiseRetrieved(socket, "_read_location_name_groups_YoloTheGame", JValue.CreateNull())); +#endif + + socket.Received().SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_location_name_groups_YoloTheGame")); + + Assert.IsNull(locationNameGroups); + } + + [Test] + public void GetClientStatus_should_return_status_for_current_player_slot() + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + connectionInfo.Slot.Returns(8); + connectionInfo.Team.Returns(2); + + var sut = new DataStorageHelper(socket, connectionInfo); + + ArchipelagoClientState? status = null; + + ExecuteAsyncWithDelay( + () => { status = sut.GetClientStatus(); }, + () => RaiseRetrieved(socket, "_read_client_status_2_8", new JValue((int)ArchipelagoClientState.ClientPlaying))); + + socket.Received().SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_client_status_2_8")); + + Assert.IsNotNull(status); + Assert.That(status, Is.EqualTo(ArchipelagoClientState.ClientPlaying)); + } + + [Test] + public void GetClientStatusAsync_should_return_hints_for_current_player_slot() + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + connectionInfo.Slot.Returns(7); + connectionInfo.Team.Returns(3); + + var sut = new DataStorageHelper(socket, connectionInfo); + + ArchipelagoClientState? status = null; + +#if NET471 + bool statusCallbackReceived = false; + + ExecuteAsyncWithDelay( + () => { + sut.GetClientStatusAsync(t => { + statusCallbackReceived = true; + status = t; + }); + }, + () => RaiseRetrieved(socket, "_read_client_status_3_7", new JValue((int)ArchipelagoClientState.ClientReady))); + + while (!statusCallbackReceived) + Thread.Sleep(10); +#else + ExecuteAsyncWithDelay( + () => { status = sut.GetClientStatusAsync().Result; }, + () => RaiseRetrieved(socket, "_read_client_status_3_7", new JValue((int)ArchipelagoClientState.ClientReady))); +#endif + + socket.Received().SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_client_status_3_7")); + + Assert.IsNotNull(status); + Assert.That(status, Is.EqualTo(ArchipelagoClientState.ClientReady)); + } + + [Test] + public void TrackClientStatus_true_should_call_callback_for_status_changes_and_request_initial_value() + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + connectionInfo.Slot.Returns(7); + connectionInfo.Team.Returns(3); + + var sut = new DataStorageHelper(socket, connectionInfo); + + int statusCallbackCount = 0; + ArchipelagoClientState? status = null; + + // ReSharper disable once RedundantArgumentDefaultValue + ExecuteAsyncWithDelay( + () => { + sut.TrackClientStatus(t => { + statusCallbackCount++; + status = t; + }, true); + }, + () => + { + RaiseRetrieved(socket, "_read_client_status_3_7", new JValue((int)ArchipelagoClientState.ClientGoal)); + RaiseWSetReply(socket, "_read_client_status_3_7", new JValue((int)ArchipelagoClientState.ClientGoal)); + }); + + while (statusCallbackCount < 2) + Thread.Sleep(10); + + Received.InOrder(() => + { + socket.SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_client_status_3_7")); + socket.SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_client_status_3_7")); + }); + + Assert.IsNotNull(status); + Assert.That(status, Is.EqualTo(ArchipelagoClientState.ClientGoal)); + } + + [Test] + public void TracClientStatus_false_should_call_callback_for_hint_changes_but_not_request_initial_value() + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + connectionInfo.Slot.Returns(8); + connectionInfo.Team.Returns(6); + + var sut = new DataStorageHelper(socket, connectionInfo); + + bool statusCallbackReceived = false; + ArchipelagoClientState? status = null; + + ExecuteAsyncWithDelay( + () => { + sut.TrackClientStatus(t => { + statusCallbackReceived = true; + status = t; + }, false); + }, + () => RaiseWSetReply(socket, "_read_client_status_6_8", new JValue((int)ArchipelagoClientState.ClientConnected))); + + while (!statusCallbackReceived) + Thread.Sleep(10); + + socket.Received().SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_client_status_6_8")); + socket.DidNotReceive().SendPacketAsync(Arg.Any()); + + Assert.IsNotNull(status); + Assert.That(status, Is.EqualTo(ArchipelagoClientState.ClientConnected)); + } + + [Test] + public void GetClientStatus_should_return_unknown_for_non_existing_slot() + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + + var sut = new DataStorageHelper(socket, connectionInfo); + + ArchipelagoClientState? statusBySlot = null; + ArchipelagoClientState? statusBySlotAndTeam = null; + + ExecuteAsyncWithDelay( + () => { statusBySlot = sut.GetClientStatus(11); }, + () => RaiseRetrieved(socket, "_read_client_status_0_11", JValue.CreateNull())); + ExecuteAsyncWithDelay( + () => { statusBySlotAndTeam = sut.GetClientStatus(11, 2); }, + () => RaiseRetrieved(socket, "_read_client_status_2_11", JValue.CreateNull())); + + socket.Received().SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_client_status_0_11")); + socket.Received().SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_client_status_2_11")); + + Assert.IsNotNull(statusBySlot); + Assert.That(statusBySlot, Is.EqualTo(ArchipelagoClientState.ClientUnknown)); + Assert.IsNotNull(statusBySlotAndTeam); + Assert.That(statusBySlotAndTeam, Is.EqualTo(ArchipelagoClientState.ClientUnknown)); + } + + [Test] + public void GetClientStatusAsync_should_return_unknown_for_non_existing_slot() + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + + var sut = new DataStorageHelper(socket, connectionInfo); + + ArchipelagoClientState? statusBySlot = null; + ArchipelagoClientState? statusBySlotAndTeam = null; + +#if NET471 + bool statusBySlotCallbackReceived = false; + bool statusBySlotAndTeamCallbackReceived = false; + + ExecuteAsyncWithDelay( + () => { + sut.GetClientStatusAsync(t => { + statusBySlotCallbackReceived = true; + statusBySlot = t; + }, 11); + }, + () => RaiseRetrieved(socket, "_read_client_status_0_11", JValue.CreateNull())); + ExecuteAsyncWithDelay( + () => { + sut.GetClientStatusAsync(t => { + statusBySlotAndTeamCallbackReceived = true; + statusBySlotAndTeam = t; + }, 11, 11); + }, + () => RaiseRetrieved(socket, "_read_client_status_11_11", JValue.CreateNull())); + + while (!statusBySlotCallbackReceived || !statusBySlotAndTeamCallbackReceived) + Thread.Sleep(10); +#else + ExecuteAsyncWithDelay( + () => { statusBySlot = sut.GetClientStatusAsync(11).Result; }, + () => RaiseRetrieved(socket, "_read_client_status_0_11", JValue.CreateNull())); + ExecuteAsyncWithDelay( + () => { statusBySlotAndTeam = sut.GetClientStatusAsync(11, 11).Result; }, + () => RaiseRetrieved(socket, "_read_client_status_11_11", JValue.CreateNull())); +#endif + socket.Received().SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_client_status_0_11")); + socket.Received().SendPacketAsync(Arg.Is(p => p.Keys.FirstOrDefault() == "_read_client_status_11_11")); + + Assert.IsNotNull(statusBySlot); + Assert.That(statusBySlot, Is.EqualTo(ArchipelagoClientState.ClientUnknown)); + Assert.IsNotNull(statusBySlotAndTeam); + Assert.That(statusBySlotAndTeam, Is.EqualTo(ArchipelagoClientState.ClientUnknown)); + } + public static void ExecuteAsyncWithDelay(Action retrieve, Action raiseEvent) { Task.WaitAll(new[] { diff --git a/Archipelago.MultiClient.Net.Tests/GameStateHelperFixture.cs b/Archipelago.MultiClient.Net.Tests/GameStateHelperFixture.cs index fadb29f..4a97d51 100644 --- a/Archipelago.MultiClient.Net.Tests/GameStateHelperFixture.cs +++ b/Archipelago.MultiClient.Net.Tests/GameStateHelperFixture.cs @@ -17,146 +17,148 @@ class RoomStateHelperFixture public static TestCaseData[] RoomStateHelperTests => new TestCaseData[] { // HintCostPercentage - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_get_and_update_hint_cost_percentage", new RoomInfoPacket { HintCostPercentage = 99 }, new RoomUpdatePacket { HintCostPercentage = 777 }, s => s.HintCostPercentage, 99, 777), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_not_update_hint_cost_percentage_when_its_not_provided", new RoomInfoPacket { HintCostPercentage = 99 }, new RoomUpdatePacket { HintCostPercentage = null }, s => s.HintCostPercentage, 99, 99), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_update_hint_cost_percentage_to_0_when_its_provided", new RoomInfoPacket { HintCostPercentage = 99 }, new RoomUpdatePacket { HintCostPercentage = 0 }, s => s.HintCostPercentage, 99, 0), // HintCost - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_get_and_update_hint_cost", new RoomInfoPacket { HintCostPercentage = 40 }, - new RoomUpdatePacket { HintCostPercentage = 25 }, - s => s.HintCost, 60, 37), - new RoomStateHelperTest( + new ConnectedPacket { LocationsChecked = new long[100], MissingChecks = new long[50] }, + s => s.HintCost, 0, 60), + RoomStateHelperTest.Create( "Should_not_update_hint_cost_when_its_not_provided", - new RoomInfoPacket { HintCostPercentage = -10 }, + new RoomInfoPacket { HintCostPercentage = 40 }, + new ConnectedPacket { LocationsChecked = new long[100], MissingChecks = new long[50] }, new RoomUpdatePacket { HintCostPercentage = null }, - s => s.HintCost, 0, 0), - new RoomStateHelperTest( + s => s.HintCost, 0, 60, 60), + RoomStateHelperTest.Create( "Should_update_hint_cost_to_0_when_its_provided", - new RoomInfoPacket { HintCostPercentage = 99 }, - new RoomUpdatePacket { HintCostPercentage = 0 }, - s => s.HintCost, 148, 0), + new RoomInfoPacket { HintCostPercentage = 40 }, + new ConnectedPacket { LocationsChecked = new long[100], MissingChecks = new long[50] }, + new RoomUpdatePacket { HintCostPercentage = 0 }, + s => s.HintCost, 0, 60, 0), // LocationCheckPoints - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_get_and_update_location_check_points", new RoomInfoPacket { LocationCheckPoints = 1 }, new RoomUpdatePacket { LocationCheckPoints = 5 }, s => s.LocationCheckPoints, 1, 5), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_not_update_location_check_points_when_its_not_provided", new RoomInfoPacket { LocationCheckPoints = 2 }, new RoomUpdatePacket { LocationCheckPoints = null }, s => s.LocationCheckPoints, 2, 2), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_update_location_check_points_to_0_when_its_provided", new RoomInfoPacket { LocationCheckPoints = 3 }, new RoomUpdatePacket { LocationCheckPoints = 0 }, s => s.LocationCheckPoints, 3, 0), // HintPoints - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_update_hint_points", new RoomUpdatePacket { HintPoints = 1337 }, new RoomUpdatePacket { HintPoints = 1350 }, s => s.HintPoints, 1337, 1350), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_not_update_hint_points_when_its_not_provided", new RoomUpdatePacket { HintPoints = 1337 }, new RoomUpdatePacket { HintPoints = null }, s => s.HintPoints, 1337, 1337), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_update_hint_points_to_0_when_its_provided", new RoomUpdatePacket { HintPoints = 1337 }, new RoomUpdatePacket { HintPoints = 0 }, s => s.HintPoints, 1337, 0), // Tags - new RoomStateHelperTest>( + RoomStateHelperTest.Create( "Should_update_tags", new RoomInfoPacket { Tags = new []{ "Tag1" } }, new RoomUpdatePacket { Tags = new []{ "Tag2" } }, s => s.ServerTags, new List { "Tag1" }.AsReadOnly(), new List { "Tag2" }.AsReadOnly()), - new RoomStateHelperTest>( + RoomStateHelperTest.Create( "Should_not_update_tags_when_its_not_provided", new RoomInfoPacket { Tags = new []{ "Tag1" } }, new RoomUpdatePacket { Tags = null }, s => s.ServerTags, new List { "Tag1" }.AsReadOnly(), new List { "Tag1" }.AsReadOnly()), - new RoomStateHelperTest>( + RoomStateHelperTest.Create( "Should_update_tags_to_empty_when_its_provided", new RoomInfoPacket { Tags = new []{ "Tag1" } }, new RoomUpdatePacket { Tags = Array.Empty() }, s => s.ServerTags, new List { "Tag1" }.AsReadOnly(), new List().AsReadOnly()), // Password - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_get_and_update_password", new RoomInfoPacket { Password = false }, new RoomUpdatePacket { Password = true }, s => s.HasPassword, false, true), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_not_update_password_when_its_not_provided", new RoomInfoPacket { Password = true }, new RoomUpdatePacket { Password = null }, s => s.HasPassword, true, true), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_update_password_to_false_when_its_provided", new RoomInfoPacket { Password = true }, new RoomUpdatePacket { Password = false }, s => s.HasPassword, true, false), // Release Permissions - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_get_and_update_release_permissions", new RoomInfoPacket { Permissions = new Dictionary { { "release", Permissions.Enabled } } }, new RoomUpdatePacket { Permissions = new Dictionary { { "release", Permissions.Goal } } }, s => s.ReleasePermissions, Permissions.Enabled, Permissions.Goal), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_not_update_release_permissions_when_its_not_provided", new RoomInfoPacket { Permissions = new Dictionary { { "release", Permissions.Disabled } } }, new RoomUpdatePacket { Permissions = null }, s => s.ReleasePermissions, Permissions.Disabled, Permissions.Disabled), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_update_release_permissions_to_false_when_its_provided", new RoomInfoPacket { Permissions = new Dictionary { { "release", Permissions.Auto } } }, new RoomUpdatePacket { Permissions = new Dictionary { { "release", Permissions.Enabled } } }, s => s.ReleasePermissions, Permissions.Auto, Permissions.Enabled), //forward compatibility - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_get_and_update_release_permissions_from_forfeit_permissions", new RoomInfoPacket { Permissions = new Dictionary { { "forfeit", Permissions.Enabled } } }, new RoomUpdatePacket { Permissions = new Dictionary { { "forfeit", Permissions.Goal } } }, s => s.ReleasePermissions, Permissions.Enabled, Permissions.Goal), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_update_release_permissions_to_false_when_its_provided_as_forfeit_permission", new RoomInfoPacket { Permissions = new Dictionary { { "forfeit", Permissions.Auto } } }, new RoomUpdatePacket { Permissions = new Dictionary { { "forfeit", Permissions.Enabled } } }, s => s.ReleasePermissions, Permissions.Auto, Permissions.Enabled), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_get_and_update_forfeit_permissions_from_release_permissions", new RoomInfoPacket { Permissions = new Dictionary { { "release", Permissions.Enabled } } }, new RoomUpdatePacket { Permissions = new Dictionary { { "release", Permissions.Goal } } }, s => s.ReleasePermissions, Permissions.Enabled, Permissions.Goal), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_update_forfeit_permissions_to_false_when_its_provided_as_release_permission", new RoomInfoPacket { Permissions = new Dictionary { { "release", Permissions.Auto } } }, new RoomUpdatePacket { Permissions = new Dictionary { { "release", Permissions.Enabled } } }, s => s.ReleasePermissions, Permissions.Auto, Permissions.Enabled), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_get_and_update_release_permissions_from_release_permissions_over_forfeit_permissions", new RoomInfoPacket { Permissions = new Dictionary { { "forfeit", Permissions.Auto }, @@ -168,17 +170,17 @@ class RoomStateHelperFixture } }, s => s.ReleasePermissions, Permissions.Enabled, Permissions.Auto), // Forfeit Permissions - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_get_and_update_forfeit_permissions", new RoomInfoPacket { Permissions = new Dictionary { { "forfeit", Permissions.Enabled } } }, new RoomUpdatePacket { Permissions = new Dictionary { { "forfeit", Permissions.Goal } } }, s => s.ForfeitPermissions, Permissions.Enabled, Permissions.Goal), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_not_update_forfeit_permissions_when_its_not_provided", new RoomInfoPacket { Permissions = new Dictionary { { "forfeit", Permissions.Disabled } } }, new RoomUpdatePacket { Permissions = null }, s => s.ForfeitPermissions, Permissions.Disabled, Permissions.Disabled), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_update_forfeit_permissions_to_false_when_its_provided", new RoomInfoPacket { Permissions = new Dictionary { { "forfeit", Permissions.Auto } } }, new RoomUpdatePacket { Permissions = new Dictionary { { "forfeit", Permissions.Enabled } } }, @@ -186,117 +188,139 @@ class RoomStateHelperFixture // Collect Permissions - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_get_and_collect_permissions", new RoomInfoPacket { Permissions = new Dictionary { { "collect", Permissions.Enabled } } }, new RoomUpdatePacket { Permissions = new Dictionary { { "collect", Permissions.Goal } } }, s => s.CollectPermissions, Permissions.Enabled, Permissions.Goal), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_not_update_collect_permissions_when_its_not_provided", new RoomInfoPacket { Permissions = new Dictionary { { "collect", Permissions.Disabled } } }, new RoomUpdatePacket { Permissions = null }, s => s.CollectPermissions, Permissions.Disabled, Permissions.Disabled), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_update_collect_permissions_to_enabled_when_its_provided", new RoomInfoPacket { Permissions = new Dictionary { { "collect", Permissions.Auto } } }, new RoomUpdatePacket { Permissions = new Dictionary { { "collect", Permissions.Enabled } } }, s => s.CollectPermissions, Permissions.Auto, Permissions.Enabled), // Remaining Permissions - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_get_and_update_remaining_permissions", new RoomInfoPacket { Permissions = new Dictionary { { "remaining", Permissions.Enabled } } }, new RoomUpdatePacket { Permissions = new Dictionary { { "remaining", Permissions.Goal } } }, s => s.RemainingPermissions, Permissions.Enabled, Permissions.Goal), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_not_update_remaining_permissions_when_its_not_provided", new RoomInfoPacket { Permissions = new Dictionary { { "remaining", Permissions.Disabled } } }, new RoomUpdatePacket { Permissions = null }, s => s.RemainingPermissions, Permissions.Disabled, Permissions.Disabled), - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_update_remaining_permissions_to_enabled_when_its_provided", new RoomInfoPacket { Permissions = new Dictionary { { "remaining", Permissions.Auto } } }, new RoomUpdatePacket { Permissions = new Dictionary { { "remaining", Permissions.Enabled } } }, s => s.RemainingPermissions, Permissions.Auto, Permissions.Enabled), // Version - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_read_version", new RoomInfoPacket { Version = new NetworkVersion(1, 2, 3) }, s => s.Version, new Version(1, 2, 3)), + // Generator Version + RoomStateHelperTest.Create( + "Should_read_generator_version", + new RoomInfoPacket { GeneratorVersion = new NetworkVersion(5, 6, 9) }, + s => s.GeneratorVersion, new Version(5, 6, 9)), + // Seed - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_read_seed", new RoomInfoPacket { SeedName = "436191FC1F3CF6410B92" }, s => s.Seed, "436191FC1F3CF6410B92"), // Time - new RoomStateHelperTest( + RoomStateHelperTest.Create( "Should_read_time", new RoomInfoPacket { Timestamp = new DateTime(2000, 1, 2, 3, 4, 5).ToUnixTimeStamp() }, s => s.RoomInfoSendTime, new DateTime(2000, 1, 2, 3, 4, 5)) }; - [TestCaseSource(nameof(RoomStateHelperTests))] - public void Should_update_values( - ArchipelagoPacketBase firstPacket, ArchipelagoPacketBase secondPacket, - Func getValue, - T expectedValueAfterFirstPacket, T expectedValueAfterSecondPacket) + [TestCaseSource(nameof(RoomStateHelperTests))] + public void Should_update_values( + ArchipelagoPacketBase firstPacket, ArchipelagoPacketBase secondPacket, ArchipelagoPacketBase thirdPacket, + Func getValue, + T expectedValueAfterFirstPacket, T expectedValueAfterSecondPacket, T expectedValueAfterThirdPacket) { - var socket = Substitute.For(); - var locationHelper = Substitute.For(); - locationHelper.AllLocations.Returns(new ReadOnlyCollection(new long[150])); + var socket = Substitute.For(); + var locationHelper = Substitute.For(); + locationHelper.AllLocations.Returns(new ReadOnlyCollection(new long[150])); - var sut = new RoomStateHelper(socket, locationHelper); + var sut = new RoomStateHelper(socket, locationHelper); - Assert.That(getValue(sut), Is.EqualTo(default(T)), - $"initial value before the first RoomInfoPacket is received should be {default(T)} but is {getValue(sut)}"); + Assert.That(getValue(sut), Is.EqualTo(default(T)), + $"initial value before the first RoomInfoPacket is received should be {default(T)} but is {getValue(sut)}"); - socket.PacketReceived += Raise.Event(firstPacket); + socket.PacketReceived += Raise.Event(firstPacket); - Assert.That(getValue(sut), Is.EqualTo(expectedValueAfterFirstPacket), - $"The initial value after the first packet should be {expectedValueAfterFirstPacket} but is {getValue(sut)}"); + Assert.That(getValue(sut), Is.EqualTo(expectedValueAfterFirstPacket), + $"The initial value after the first packet should be {expectedValueAfterFirstPacket} but is {getValue(sut)}"); - if (secondPacket != null) - { - socket.PacketReceived += Raise.Event(secondPacket); + if (secondPacket != null) + { + socket.PacketReceived += Raise.Event(secondPacket); - Assert.That(getValue(sut), Is.EqualTo(expectedValueAfterSecondPacket), - $"The value after the second packet should be {expectedValueAfterSecondPacket} but is {getValue(sut)}"); - } - } - } + Assert.That(getValue(sut), Is.EqualTo(expectedValueAfterSecondPacket), + $"The value after the second packet should be {expectedValueAfterSecondPacket} but is {getValue(sut)}"); + } + + if (thirdPacket != null) + { + socket.PacketReceived += Raise.Event(thirdPacket); - class RoomStateHelperTest : TestCaseData + Assert.That(getValue(sut), Is.EqualTo(expectedValueAfterThirdPacket), + $"The value after the third packet should be {expectedValueAfterThirdPacket} but is {getValue(sut)}"); + } + } + } + + class RoomStateHelperTestBase : TestCaseData { - public RoomStateHelperTest( - ArchipelagoPacketBase firstPacket, ArchipelagoPacketBase secondPacket, - Func getValue, - object expectedValueAfterFirstPacket, object expectedValueAfterSecondPacket) - : base(firstPacket, secondPacket, getValue, expectedValueAfterFirstPacket, expectedValueAfterSecondPacket) + public RoomStateHelperTestBase( + string testName, + ArchipelagoPacketBase firstPacket, ArchipelagoPacketBase secondPacket, ArchipelagoPacketBase thirdPacket, + Func getValue, + object expectedValueAfterFirstPacket, object expectedValueAfterSecondPacket, object expectedValueAfterThirdPacket) + : base(firstPacket, secondPacket, thirdPacket, getValue, + expectedValueAfterFirstPacket, expectedValueAfterSecondPacket, expectedValueAfterThirdPacket) { + TestName = testName; } } - class RoomStateHelperTest : RoomStateHelperTest + static class RoomStateHelperTest { - public RoomStateHelperTest( - string testName, + public static TestCaseData Create( + string testName, + ArchipelagoPacketBase firstPacket, ArchipelagoPacketBase secondPacket, ArchipelagoPacketBase thirdPacket, + Func getValue, + T expectedValueAfterFirstPacket, T expectedValueAfterSecondPacket, T expectedValueAfterThirdPacket) => + new RoomStateHelperTestBase(testName, firstPacket, secondPacket, thirdPacket, s => getValue(s), + expectedValueAfterFirstPacket, expectedValueAfterSecondPacket, expectedValueAfterThirdPacket); + + public static TestCaseData Create( + string testName, ArchipelagoPacketBase firstPacket, ArchipelagoPacketBase secondPacket, Func getValue, - T expectedValueAfterFirstPacket, T expectedValueAfterSecondPacket) - : base(firstPacket, secondPacket, s => getValue(s), expectedValueAfterFirstPacket, expectedValueAfterSecondPacket) - { - SetName(testName); - } - public RoomStateHelperTest( - string testName, + T expectedValueAfterFirstPacket, T expectedValueAfterSecondPacket) => + Create(testName, firstPacket, secondPacket, null, getValue, + expectedValueAfterFirstPacket, expectedValueAfterSecondPacket, default); + + public static TestCaseData Create( + string testName, ArchipelagoPacketBase firstPacket, Func getValue, - T expectedValueAfterFirstUpdate) - : this(testName, firstPacket, null, getValue, expectedValueAfterFirstUpdate, default) - { - } + T expectedValueAfterFirstPacket) => + Create(testName, firstPacket, null, getValue, expectedValueAfterFirstPacket, default); } } diff --git a/Archipelago.MultiClient.Net.Tests/ItemInfoResolverFixture.cs b/Archipelago.MultiClient.Net.Tests/ItemInfoResolverFixture.cs new file mode 100644 index 0000000..9967559 --- /dev/null +++ b/Archipelago.MultiClient.Net.Tests/ItemInfoResolverFixture.cs @@ -0,0 +1,126 @@ +using Archipelago.MultiClient.Net.DataPackage; +using Archipelago.MultiClient.Net.Helpers; +using NSubstitute; +using NUnit.Framework; + +namespace Archipelago.MultiClient.Net.Tests +{ + [TestFixture] + class ItemInfoResolverFixture + { + [Test] + public void Get_Item_Name_From_DataPackage_Does_Not_Throw() + { + var cache = Substitute.For(); + var connectionInfo = Substitute.For(); + connectionInfo.Game.Returns("TestGame"); + + var gameDataLookup = Substitute.For(); + gameDataLookup.Items.Returns(new TwoWayLookup { { 1, "TestItem" } }); + cache.TryGetGameDataFromCache("TestGame", out Arg.Any()).Returns(x => + { + x[1] = gameDataLookup; + return true; + }); + + var sut = new ItemInfoResolver(cache, connectionInfo); + + var itemName = sut.GetItemName(1); + Assert.That(itemName, Is.EqualTo("TestItem")); + } + + [Test] + public void Get_Item_Name_Returns_Null_For_when_ItemId_Doesnt_Exist() + { + var cache = Substitute.For(); + var connectionInfo = Substitute.For(); + + var gameDataLookup = Substitute.For(); + gameDataLookup.Items.Returns(new TwoWayLookup { { 1, "TestItem" } }); + cache.TryGetGameDataFromCache("TestGame", out Arg.Any()).Returns(x => + { + x[1] = gameDataLookup; + return true; + }); + + var sut = new ItemInfoResolver(cache, connectionInfo); + + var itemName = sut.GetItemName(2); + Assert.That(itemName, Is.Null); + } + + [Test] + public void Retrieving_item_name_from_id_use_current_game_as_base() + { + var cache = Substitute.For(); + var connectionInfo = Substitute.For(); + + var sut = new ItemInfoResolver(cache, connectionInfo); + + var archipelagoDataLookup = Substitute.For(); + archipelagoDataLookup.Items.Returns(new TwoWayLookup { { -100, "ArchipelagoItem" } }); + var game1DataLookup = Substitute.For(); + game1DataLookup.Items.Returns(new TwoWayLookup { { 1337, "Game1Item" } }); + var game2DataLookup = Substitute.For(); + game2DataLookup.Items.Returns(new TwoWayLookup { { 1337, "Game2Item" } }); + + cache.TryGetGameDataFromCache("Archipelago", out Arg.Any()).Returns(x => + { + x[1] = archipelagoDataLookup; + return true; + }); + cache.TryGetGameDataFromCache("Game1", out Arg.Any()).Returns(x => + { + x[1] = game1DataLookup; + return true; + }); + cache.TryGetGameDataFromCache("Game2", out Arg.Any()).Returns(x => + { + x[1] = game2DataLookup; + return true; + }); + + connectionInfo.Game.Returns("Game2"); + var itemName = sut.GetItemName(-100); + + Assert.That(itemName, Is.EqualTo("ArchipelagoItem")); + } + + [Test] + public void Retrieving_item_name_from_id_use_specific_game() + { + var cache = Substitute.For(); + var connectionInfo = Substitute.For(); + + var sut = new ItemInfoResolver(cache, connectionInfo); + + var archipelagoDataLookup = Substitute.For(); + archipelagoDataLookup.Items.Returns(new TwoWayLookup { { -100, "ArchipelagoItem" } }); + var game1DataLookup = Substitute.For(); + game1DataLookup.Items.Returns(new TwoWayLookup { { 1337, "Game1Item" } }); + var game2DataLookup = Substitute.For(); + game2DataLookup.Items.Returns(new TwoWayLookup { { 1337, "Game2Item" } }); + + cache.TryGetGameDataFromCache("Archipelago", out Arg.Any()).Returns(x => + { + x[1] = archipelagoDataLookup; + return true; + }); + cache.TryGetGameDataFromCache("Game1", out Arg.Any()).Returns(x => + { + x[1] = game1DataLookup; + return true; + }); + cache.TryGetGameDataFromCache("Game2", out Arg.Any()).Returns(x => + { + x[1] = game2DataLookup; + return true; + }); + + connectionInfo.Game.Returns("Game2"); + var itemName = sut.GetItemName(-100, "Game1"); + + Assert.That(itemName, Is.EqualTo("ArchipelagoItem")); + } + } +} diff --git a/Archipelago.MultiClient.Net.Tests/LocationCheckHelperFixture.cs b/Archipelago.MultiClient.Net.Tests/LocationCheckHelperFixture.cs index 236947f..99bb5b4 100644 --- a/Archipelago.MultiClient.Net.Tests/LocationCheckHelperFixture.cs +++ b/Archipelago.MultiClient.Net.Tests/LocationCheckHelperFixture.cs @@ -1,4 +1,4 @@ -using Archipelago.MultiClient.Net.Cache; +using Archipelago.MultiClient.Net.DataPackage; using Archipelago.MultiClient.Net.Helpers; using Archipelago.MultiClient.Net.Models; using Archipelago.MultiClient.Net.Packets; @@ -6,6 +6,7 @@ using NUnit.Framework; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -19,9 +20,11 @@ class LocationCheckHelperFixture public void Should_not_throw_when_null_or_empty_list_is_checked() { var socket = Substitute.For(); - var cache = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - ILocationCheckHelper sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); Assert.DoesNotThrow(() => { sut.CompleteLocationChecks(null); @@ -39,12 +42,14 @@ public void Should_not_throw_when_null_or_empty_list_is_checked() [Test] public void Should_load_initial_checked_locations_send_by_server() { - var socket = Substitute.For(); - var cache = Substitute.For(); + var socket = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - var sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var connectedPacket = new ConnectedPacket + var connectedPacket = new ConnectedPacket { LocationsChecked = new long[]{ 1, 3 }, MissingChecks = new long[]{ 2 } @@ -65,12 +70,14 @@ public void Should_load_initial_checked_locations_send_by_server() [Test] public void Should_also_load_initial_missing_locations_send_by_server_if_no_location_has_been_checked_yet() { - var socket = Substitute.For(); - var cache = Substitute.For(); + var socket = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - var sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var connectedPacket = new ConnectedPacket + var connectedPacket = new ConnectedPacket { LocationsChecked = Array.Empty(), MissingChecks = new long[] { 1, 2 } @@ -90,12 +97,14 @@ public void Should_also_load_initial_missing_locations_send_by_server_if_no_loca [Test] public void Should_also_load_initial_checked_locations_send_by_server_if_no_location_is_missing() { - var socket = Substitute.For(); - var cache = Substitute.For(); + var socket = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - var sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var connectedPacket = new ConnectedPacket + var connectedPacket = new ConnectedPacket { LocationsChecked = new long[] { 1, 2 }, MissingChecks = Array.Empty() @@ -115,12 +124,14 @@ public void Should_also_load_initial_checked_locations_send_by_server_if_no_loca [Test] public void Should_add_locations_checked_by_client() { - var socket = Substitute.For(); - var cache = Substitute.For(); + var socket = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - var sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var connectedPacket = new ConnectedPacket + var connectedPacket = new ConnectedPacket { LocationsChecked = new long[]{ 1 }, MissingChecks = new long[]{ 2, 3 } @@ -146,47 +157,17 @@ public void Should_add_locations_checked_by_client() Assert.That(sut.AllMissingLocations, Is.Empty); } - [Test] - public void Should_add_locations_that_do_not_exists() - { - var socket = Substitute.For(); - var cache = Substitute.For(); - - var sut = new LocationCheckHelper(socket, cache); - - var connectedPacket = new ConnectedPacket - { - LocationsChecked = Array.Empty(), - MissingChecks = Array.Empty() - }; - - socket.PacketReceived += Raise.Event(connectedPacket); - - sut.CompleteLocationChecks(1); -#if NET471 - sut.CompleteLocationChecksAsync(b => { }, 2); -#else - sut.CompleteLocationChecksAsync(2).Wait(); -#endif - - Assert.Contains(1, sut.AllLocations); - Assert.Contains(2, sut.AllLocations); - - Assert.Contains(1, sut.AllLocationsChecked); - Assert.Contains(2, sut.AllLocationsChecked); - - Assert.That(sut.AllMissingLocations, Is.Empty); - } - - [Test] + [Test] public void Should_check_locations_send_by_the_server() { - var socket = Substitute.For(); - var cache = Substitute.For(); + var socket = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - var sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var connectedPacket = new ConnectedPacket + var connectedPacket = new ConnectedPacket { LocationsChecked = Array.Empty(), MissingChecks = new long[]{ 1, 2, 3 } @@ -212,14 +193,15 @@ public void Should_check_locations_send_by_the_server() [Test] public void Enumeration_over_collection_should_not_throw_when_new_data_is_received() + { + var socket = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - { - var socket = Substitute.For(); - var cache = Substitute.For(); - - var sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var connectedPacket = new ConnectedPacket + var connectedPacket = new ConnectedPacket { LocationsChecked = new long[]{ 1, 2, 3 }, MissingChecks = new long[]{ 4 } @@ -257,44 +239,17 @@ public void Enumeration_over_collection_should_not_throw_when_new_data_is_receiv }); } - [Test] - public void Should_call_event_handler_when_new_locations_are_checked_by_client() - { - var socket = Substitute.For(); - var cache = Substitute.For(); - - var sut = new LocationCheckHelper(socket, cache); - - var newCheckedLocations = new List(); - - sut.CheckedLocationsUpdated += l => - { - newCheckedLocations.Add(l.ToArray()); - }; - - sut.CompleteLocationChecks(1, 2); -#if NET471 - sut.CompleteLocationChecksAsync(b => { }, 3); -#else - sut.CompleteLocationChecksAsync(3).Wait(); -#endif - - Assert.That(newCheckedLocations.Count, Is.EqualTo(2)); - - Assert.Contains(1, newCheckedLocations[0]); - Assert.Contains(2, newCheckedLocations[0]); - Assert.Contains(3, newCheckedLocations[1]); - } - [Test] public void Should_call_event_handler_when_new_locations_are_checked_by_server() { - var socket = Substitute.For(); - var cache = Substitute.For(); + var socket = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - var sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var newCheckedLocations = new List(); + var newCheckedLocations = new List(); sut.CheckedLocationsUpdated += l => { @@ -324,12 +279,14 @@ public void Should_call_event_handler_when_new_locations_are_checked_by_server() [Test] public void Should_not_call_event_handler_when_no_new_locations_are_checked() { - var socket = Substitute.For(); - var cache = Substitute.For(); + var socket = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - var sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var connectedPacket = new ConnectedPacket + var connectedPacket = new ConnectedPacket { LocationsChecked = new long[]{ 1, 2 }, MissingChecks = new long[]{ 3 } @@ -367,12 +324,14 @@ public void Should_not_call_event_handler_when_no_new_locations_are_checked() [Test] public void Should_not_check_duplicated_locations() { - var socket = Substitute.For(); - var cache = Substitute.For(); + var socket = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - var sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var connectedPacket = new ConnectedPacket + var connectedPacket = new ConnectedPacket { LocationsChecked = Array.Empty(), MissingChecks = new long[]{ 1, 2, 3 } @@ -407,12 +366,14 @@ public void Should_not_check_duplicated_locations() [Test] public void Should_not_send_location_checks_already_confirmed_by_the_server() { - var socket = Substitute.For(); - var cache = Substitute.For(); + var socket = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - var sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var connectedPacket = new ConnectedPacket + var connectedPacket = new ConnectedPacket { LocationsChecked = new long[] { 1, 2 }, MissingChecks = new long[] { 3, 4, 5, 6 }, @@ -435,12 +396,14 @@ public void Should_not_send_location_checks_already_confirmed_by_the_server() [Test] public void Should_re_send_location_checks_already_checked_but_not_confirmed_by_server() { - var socket = Substitute.For(); - var cache = Substitute.For(); + var socket = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - var sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var connectedPacket = new ConnectedPacket + var connectedPacket = new ConnectedPacket { LocationsChecked = new long[] { 1 }, MissingChecks = new long[] { 2, 3, 4, 5, 6 }, @@ -465,11 +428,13 @@ public void Should_re_send_location_checks_already_checked_but_not_confirmed_by_ public void Should_not_send_check_if_no_new_locations_are_checked() { var socket = Substitute.For(); - var cache = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - var sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var connectedPacket = new ConnectedPacket + var connectedPacket = new ConnectedPacket { LocationsChecked = new long[] { 1, 2, 3 }, MissingChecks = new long[] { 5, 6 }, @@ -486,11 +451,13 @@ public void Should_not_send_check_if_no_new_locations_are_checked() public void Should_not_fail_when_room_update_is_missing_location_checks() { var socket = Substitute.For(); - var cache = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - _ = new LocationCheckHelper(socket, cache); + _ = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var connectedPacket = new ConnectedPacket + var connectedPacket = new ConnectedPacket { LocationsChecked = Array.Empty(), MissingChecks = new long[] { 1, 2, 3 } @@ -512,12 +479,28 @@ public void Should_not_fail_when_room_update_is_missing_location_checks() [Test] public async Task Should_scout_locations_async() { - var socket = Substitute.For(); - var cache = Substitute.For(); + var socket = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + connectionInfo.Team.Returns(1); + connectionInfo.Slot.Returns(2); + + var players = Substitute.For(); +#if NET472 + players.Players.Returns( + new Dictionary> { + { 1, new ReadOnlyCollection(new List { null, null, new PlayerInfo() }) } + }); +#else + players.Players.Returns(new ReadOnlyDictionary>( + new Dictionary> { + { 1, new ReadOnlyCollection(new List { null, null, new PlayerInfo() }) } + })); +#endif - var sut = new LocationCheckHelper(socket, cache); + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); - var locationScoutResponse = new LocationInfoPacket() + var locationScoutResponse = new LocationInfoPacket() { Locations = new [] { new NetworkItem { Location = 1 } } }; @@ -530,8 +513,31 @@ public async Task Should_scout_locations_async() Assert.That(scoutTask.IsCompleted, Is.True); - await scoutTask; + await scoutTask; } #endif - } + + [Test] + public void Should_ignore_non_existing_locations() + { + var socket = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); + + ILocationCheckHelper sut = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); + + var connectedPacket = new ConnectedPacket + { + LocationsChecked = new long[] { 1, 2, 3 }, + MissingChecks = new long[] { 5, 6 }, + }; + + socket.PacketReceived += Raise.Event(connectedPacket); + + sut.CompleteLocationChecks(5, 7); + + socket.Received().SendPacket(Arg.Is(p => p.Locations.Length == 1 && p.Locations[0] == 5)); + } + } } diff --git a/Archipelago.MultiClient.Net.Tests/MessageLogHelperFixture.cs b/Archipelago.MultiClient.Net.Tests/MessageLogHelperFixture.cs index 414e861..4e2c217 100644 --- a/Archipelago.MultiClient.Net.Tests/MessageLogHelperFixture.cs +++ b/Archipelago.MultiClient.Net.Tests/MessageLogHelperFixture.cs @@ -1,4 +1,5 @@ -using Archipelago.MultiClient.Net.Enums; +using Archipelago.MultiClient.Net.DataPackage; +using Archipelago.MultiClient.Net.Enums; using Archipelago.MultiClient.Net.Helpers; using Archipelago.MultiClient.Net.MessageLog.Messages; using Archipelago.MultiClient.Net.MessageLog.Parts; @@ -19,15 +20,16 @@ class MessageLogHelperFixture public void Should_convert_print_json_packet_into_string() { var socket = Substitute.For(); - var locations = Substitute.For(); - locations.GetLocationNameFromId(6L).Returns("Text6"); - var items = Substitute.For(); - items.GetItemName(4L).Returns("Text4"); + var itemInfoResolver = Substitute.For(); + itemInfoResolver.GetLocationName(6L, "Game666").Returns("Text6"); + itemInfoResolver.GetItemName(4L, "Game123").Returns("Text4"); var players = Substitute.For(); players.GetPlayerAlias(8).Returns("Text8"); - var connectionInfo = Substitute.For(); + players.GetPlayerInfo(123).Returns(new PlayerInfo { Slot = 123, Game = "Game123" }); + players.GetPlayerInfo(666).Returns(new PlayerInfo { Slot = 666, Game = "Game666" }); + var connectionInfo = Substitute.For(); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); string toStringResult = null; @@ -37,12 +39,10 @@ public void Should_convert_print_json_packet_into_string() Data = new[] { new JsonMessagePart { Type = null, Text = "Text1" }, new JsonMessagePart { Type = JsonMessagePartType.Text, Text = "Text2" }, - new JsonMessagePart { - Type = JsonMessagePartType.Color, Text = "Text3", Color = JsonMessagePartColor.Blue - }, - new JsonMessagePart { Type = JsonMessagePartType.ItemId, Text = "4", Flags = ItemFlags.None }, + new JsonMessagePart { Type = JsonMessagePartType.Color, Text = "Text3", Color = JsonMessagePartColor.Blue }, + new JsonMessagePart { Type = JsonMessagePartType.ItemId, Text = "4", Flags = ItemFlags.None, Player = 123 }, new JsonMessagePart { Type = JsonMessagePartType.ItemName, Text = "Text5" }, - new JsonMessagePart { Type = JsonMessagePartType.LocationId, Text = "6" }, + new JsonMessagePart { Type = JsonMessagePartType.LocationId, Text = "6", Player = 666 }, new JsonMessagePart { Type = JsonMessagePartType.LocationName, Text = "Text7" }, new JsonMessagePart { Type = JsonMessagePartType.PlayerId, Text = "8" }, new JsonMessagePart { Type = JsonMessagePartType.PlayerName, Text = "Text9" }, @@ -59,16 +59,17 @@ public void Should_convert_print_json_packet_into_string() public void Should_get_parsed_data_for_print_json_packet() { var socket = Substitute.For(); - var locations = Substitute.For(); - locations.GetLocationNameFromId(6L).Returns("Text6"); - var items = Substitute.For(); - items.GetItemName(4L).Returns("Text4"); + var itemInfoResolver = Substitute.For(); + itemInfoResolver.GetLocationName(6L, "Game666").Returns("Text6"); + itemInfoResolver.GetItemName(4L, "Game123").Returns("Text4"); var players = Substitute.For(); players.GetPlayerAlias(8).Returns("Text8"); - var connectionInfo = Substitute.For(); + players.GetPlayerInfo(123).Returns(new PlayerInfo { Slot = 123, Game = "Game123" }); + players.GetPlayerInfo(666).Returns(new PlayerInfo { Slot = 666, Game = "Game666" }); + var connectionInfo = Substitute.For(); connectionInfo.Slot.Returns(0); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); MessagePart[] parts = null; @@ -78,12 +79,10 @@ public void Should_get_parsed_data_for_print_json_packet() Data = new[] { new JsonMessagePart { Type = null, Text = "Text1" }, new JsonMessagePart { Type = JsonMessagePartType.Text, Text = "Text2" }, - new JsonMessagePart { - Type = JsonMessagePartType.Color, Text = "Text3", Color = JsonMessagePartColor.BlueBg - }, - new JsonMessagePart { Type = JsonMessagePartType.ItemId, Text = "4", Flags = ItemFlags.None }, + new JsonMessagePart { Type = JsonMessagePartType.Color, Text = "Text3", Color = JsonMessagePartColor.BlueBg }, + new JsonMessagePart { Type = JsonMessagePartType.ItemId, Text = "4", Flags = ItemFlags.None, Player = 123}, new JsonMessagePart { Type = JsonMessagePartType.ItemName, Text = "Text5" }, - new JsonMessagePart { Type = JsonMessagePartType.LocationId, Text = "6" }, + new JsonMessagePart { Type = JsonMessagePartType.LocationId, Text = "6", Player = 666}, new JsonMessagePart { Type = JsonMessagePartType.LocationName, Text = "Text7" }, new JsonMessagePart { Type = JsonMessagePartType.PlayerId, Text = "8" }, new JsonMessagePart { Type = JsonMessagePartType.PlayerName, Text = "Text9" }, @@ -149,14 +148,13 @@ public void Should_get_parsed_data_for_print_json_packet() public void Should_mark_local_player_as_magenta() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); - var players = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var players = Substitute.For(); players.GetPlayerAlias(4).Returns("LocalPlayer"); var connectionInfo = Substitute.For(); connectionInfo.Slot.Returns(4); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); MessagePart[] parts = null; @@ -186,13 +184,12 @@ public void Should_mark_local_player_as_magenta() public void Should_mark_progression_items_as_the_correct_color(ItemFlags itemFlags, Color expectedColor) { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); - items.GetItemName(1L).Returns("ItemFour"); + var itemInfoResolver = Substitute.For(); + itemInfoResolver.GetItemName(1L).Returns("ItemFour"); var players = Substitute.For(); var connectionInfo = Substitute.For(); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); MessagePart[] parts = null; @@ -220,12 +217,11 @@ public void Should_mark_progression_items_as_the_correct_color(ItemFlags itemFla public void Should_split_new_lines_in_separate_messages_for_print_json_package() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); - var players = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var players = Substitute.For(); var connectionInfo = Substitute.For(); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); var logMessage = new List(3); @@ -262,15 +258,14 @@ public void Should_split_new_lines_in_separate_messages_for_print_json_package() public void Should_not_go_boom_when_datapackage_doesnt_know_certain_values() { var socket = Substitute.For(); - var locations = Substitute.For(); - locations.GetLocationNameFromId(Arg.Any()).Returns((string)null); - var items = Substitute.For(); - items.GetItemName(Arg.Any()).Returns((string)null); + var itemInfoResolver = Substitute.For(); + itemInfoResolver.GetLocationName(Arg.Any()).Returns((string)null); + itemInfoResolver.GetItemName(Arg.Any()).Returns((string)null); var players = Substitute.For(); players.GetPlayerAlias(Arg.Any()).Returns((string)null); var connectionInfo = Substitute.For(); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); MessagePart[] parts = null; @@ -308,23 +303,24 @@ public void Should_not_go_boom_when_datapackage_doesnt_know_certain_values() public void Should_preserve_extra_properties_on_ItemPrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); + itemInfoResolver.GetItemName(Arg.Any(), Arg.Any()).Returns(_ => null); + itemInfoResolver.GetLocationName(1000L, "Game3").Returns("ItemAtLocation1000InGame3"); var players = Substitute.For(); players.GetPlayerAlias(5).Returns("LocalPlayer"); - players.Players.Returns(GetPlayerCollection(new List { - new PlayerInfo { Team = 0, Slot = 1 }, - new PlayerInfo { Team = 0, Slot = 2 }, - new PlayerInfo { Team = 0, Slot = 3 }, - new PlayerInfo { Team = 0, Slot = 4 }, - new PlayerInfo { Team = 0, Slot = 5 } + new PlayerInfo { Team = 0, Slot = 1, Game = "Game1" }, + new PlayerInfo { Team = 0, Slot = 2, Game = "Game2" }, + new PlayerInfo { Team = 0, Slot = 3, Game = "Game3" }, + new PlayerInfo { Team = 0, Slot = 4, Game = "Game4" }, + new PlayerInfo { Team = 0, Slot = 5, Game = "Game5" } })); + players.GetPlayerInfo(Arg.Any(), Arg.Any()).Returns(x => players.Players[x.ArgAt(0)][x.ArgAt(1)]); var connectionInfo = Substitute.For(); connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(5); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); ItemSendLogMessage logMessage = null; @@ -341,7 +337,17 @@ public void Should_preserve_extra_properties_on_ItemPrintJsonPacket() socket.PacketReceived += Raise.Event(packet); Assert.That(logMessage, Is.Not.Null); - Assert.That(logMessage.Item, Is.EqualTo(packet.Item)); + + Assert.That(logMessage.Item.ItemId, Is.EqualTo(packet.Item.Item)); + Assert.That(logMessage.Item.LocationId, Is.EqualTo(packet.Item.Location)); + Assert.That(logMessage.Item.Player.Slot, Is.EqualTo(packet.Item.Player)); + Assert.That(logMessage.Item.Flags, Is.EqualTo(packet.Item.Flags)); + Assert.That(logMessage.Item.ItemGame, Is.EqualTo("Game5")); + Assert.That(logMessage.Item.LocationGame, Is.EqualTo("Game3")); + Assert.That(logMessage.Item.ItemName, Is.Null); + Assert.That(logMessage.Item.ItemDisplayName, Is.EqualTo($"Item: {packet.Item.Item}")); + Assert.That(logMessage.Item.LocationName, Is.EqualTo("ItemAtLocation1000InGame3")); + Assert.That(logMessage.Item.LocationDisplayName, Is.EqualTo("ItemAtLocation1000InGame3")); Assert.That(logMessage.Receiver.Slot, Is.EqualTo(5)); Assert.That(logMessage.Sender.Slot, Is.EqualTo(3)); @@ -350,28 +356,27 @@ public void Should_preserve_extra_properties_on_ItemPrintJsonPacket() Assert.That(logMessage.IsSenderTheActivePlayer, Is.EqualTo(false)); Assert.That(logMessage.IsRelatedToActivePlayer, Is.EqualTo(true)); - - Assert.That(logMessage.ReceivingPlayerSlot, Is.EqualTo(5)); - Assert.That(logMessage.SendingPlayerSlot, Is.EqualTo(3)); } [Test] public void Should_preserve_extra_properties_on_ItemCheatPrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); + itemInfoResolver.GetItemName(Arg.Any(), Arg.Any()).Returns(_ => null); + itemInfoResolver.GetLocationName(Arg.Any(), Arg.Any()).Returns(_ => null); var players = Substitute.For(); players.GetPlayerAlias(2).Returns("LocalPlayer"); players.Players.Returns(GetPlayerCollection(new List { - new PlayerInfo { Team = 0, Slot = 1 }, - new PlayerInfo { Team = 0, Slot = 2 }, + new PlayerInfo { Team = 0, Slot = 1, Game = "Game1" }, + new PlayerInfo { Team = 0, Slot = 2, Game = "Game2" }, })); + players.GetPlayerInfo(Arg.Any(), Arg.Any()).Returns(x => players.Players[x.ArgAt(0)][x.ArgAt(1)]); var connectionInfo = Substitute.For(); connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(2); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); ItemCheatLogMessage logMessage = null; @@ -380,6 +385,7 @@ public void Should_preserve_extra_properties_on_ItemCheatPrintJsonPacket() var packet = new ItemCheatPrintJsonPacket { Data = new[] { new JsonMessagePart { Text = "" } }, + //Despite the official docs claiming that the player is the sender, in the case of an item cheat message it is actually the slot who executed the command Item = new NetworkItem { Flags = ItemFlags.None, Player = 1, Item = 100, Location = 1000 }, ReceivingPlayer = 1, Team = 0, @@ -389,7 +395,17 @@ public void Should_preserve_extra_properties_on_ItemCheatPrintJsonPacket() socket.PacketReceived += Raise.Event(packet); Assert.That(logMessage, Is.Not.Null); - Assert.That(logMessage.Item, Is.EqualTo(packet.Item)); + + Assert.That(logMessage.Item.ItemId, Is.EqualTo(packet.Item.Item)); + Assert.That(logMessage.Item.LocationId, Is.EqualTo(packet.Item.Location)); + Assert.That(logMessage.Item.Player.Slot, Is.EqualTo(packet.Item.Player)); + Assert.That(logMessage.Item.Flags, Is.EqualTo(packet.Item.Flags)); + Assert.That(logMessage.Item.ItemGame, Is.EqualTo("Game1")); + Assert.That(logMessage.Item.LocationGame, Is.EqualTo("Archipelago")); + Assert.That(logMessage.Item.ItemName, Is.Null); + Assert.That(logMessage.Item.ItemDisplayName, Is.EqualTo($"Item: {packet.Item.Item}")); + Assert.That(logMessage.Item.LocationName, Is.Null); + Assert.That(logMessage.Item.LocationDisplayName, Is.EqualTo($"Location: {packet.Item.Location}")); Assert.That(logMessage.Receiver.Slot, Is.EqualTo(1)); Assert.That(logMessage.Sender.Slot, Is.EqualTo(0)); //Slot 0 = Server @@ -405,22 +421,24 @@ public void Should_preserve_extra_properties_on_ItemCheatPrintJsonPacket() public void Should_preserve_extra_properties_on_HintPrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); + itemInfoResolver.GetItemName(100L, "Game2").Returns("Item100FromGame3"); + itemInfoResolver.GetLocationName(Arg.Any(), Arg.Any()).Returns(_ => null); var players = Substitute.For(); players.GetPlayerAlias(5).Returns("LocalPlayer"); players.Players.Returns(GetPlayerCollection(new List { - new PlayerInfo { Team = 0, Slot = 1 }, - new PlayerInfo { Team = 0, Slot = 2 }, - new PlayerInfo { Team = 0, Slot = 3 }, - new PlayerInfo { Team = 0, Slot = 4 }, - new PlayerInfo { Team = 0, Slot = 5 } + new PlayerInfo { Team = 0, Slot = 1, Game = "Game1" }, + new PlayerInfo { Team = 0, Slot = 2, Game = "Game2" }, + new PlayerInfo { Team = 0, Slot = 3, Game = "Game3" }, + new PlayerInfo { Team = 0, Slot = 4, Game = "Game4" }, + new PlayerInfo { Team = 0, Slot = 5, Game = "Game5" } })); + players.GetPlayerInfo(Arg.Any(), Arg.Any()).Returns(x => players.Players[x.ArgAt(0)][x.ArgAt(1)]); var connectionInfo = Substitute.For(); connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(5); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); HintItemSendLogMessage logMessage = null; @@ -438,7 +456,17 @@ public void Should_preserve_extra_properties_on_HintPrintJsonPacket() socket.PacketReceived += Raise.Event(packet); Assert.That(logMessage, Is.Not.Null); - Assert.That(logMessage.Item, Is.EqualTo(packet.Item)); + + Assert.That(logMessage.Item.ItemId, Is.EqualTo(packet.Item.Item)); + Assert.That(logMessage.Item.LocationId, Is.EqualTo(packet.Item.Location)); + Assert.That(logMessage.Item.Player.Slot, Is.EqualTo(packet.Item.Player)); + Assert.That(logMessage.Item.Flags, Is.EqualTo(packet.Item.Flags)); + Assert.That(logMessage.Item.ItemGame, Is.EqualTo("Game2")); + Assert.That(logMessage.Item.LocationGame, Is.EqualTo("Game3")); + Assert.That(logMessage.Item.ItemName, Is.EqualTo("Item100FromGame3")); + Assert.That(logMessage.Item.ItemDisplayName, Is.EqualTo("Item100FromGame3")); + Assert.That(logMessage.Item.LocationName, Is.Null); + Assert.That(logMessage.Item.LocationDisplayName, Is.EqualTo($"Location: {packet.Item.Location}")); Assert.That(logMessage.Receiver.Slot, Is.EqualTo(2)); Assert.That(logMessage.Sender.Slot, Is.EqualTo(3)); @@ -449,17 +477,13 @@ public void Should_preserve_extra_properties_on_HintPrintJsonPacket() Assert.That(logMessage.IsRelatedToActivePlayer, Is.EqualTo(false)); Assert.That(logMessage.IsFound, Is.EqualTo(true)); - - Assert.That(logMessage.ReceivingPlayerSlot, Is.EqualTo(2)); - Assert.That(logMessage.SendingPlayerSlot, Is.EqualTo(3)); } [Test] public void Should_preserve_extra_properties_on_JoinPrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); var players = Substitute.For(); players.GetPlayerAlias(5).Returns("LocalPlayer"); players.Players.Returns(GetPlayerCollection(new List { @@ -473,7 +497,7 @@ public void Should_preserve_extra_properties_on_JoinPrintJsonPacket() connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(5); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); JoinLogMessage logMessage = null; @@ -506,8 +530,7 @@ public void Should_preserve_extra_properties_on_JoinPrintJsonPacket() public void Should_preserve_extra_properties_on_LeavePrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); var players = Substitute.For(); players.GetPlayerAlias(5).Returns("LocalPlayer"); players.Players.Returns(GetPlayerCollection(new List { @@ -521,7 +544,7 @@ public void Should_preserve_extra_properties_on_LeavePrintJsonPacket() connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(5); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); LeaveLogMessage logMessage = null; @@ -551,8 +574,7 @@ public void Should_preserve_extra_properties_on_LeavePrintJsonPacket() public void Should_preserve_extra_properties_on_ChatPrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); var players = Substitute.For(); players.GetPlayerAlias(5).Returns("LocalPlayer"); players.Players.Returns(GetPlayerCollection(new List { @@ -566,7 +588,7 @@ public void Should_preserve_extra_properties_on_ChatPrintJsonPacket() connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(5); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); ChatLogMessage logMessage = null; @@ -599,8 +621,7 @@ public void Should_preserve_extra_properties_on_ChatPrintJsonPacket() public void Should_preserve_extra_properties_on_ServerChatPrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); var players = Substitute.For(); players.GetPlayerAlias(5).Returns("LocalPlayer"); players.Players.Returns(GetPlayerCollection(new List { @@ -614,7 +635,7 @@ public void Should_preserve_extra_properties_on_ServerChatPrintJsonPacket() connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(5); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); ServerChatLogMessage logMessage = null; @@ -638,8 +659,7 @@ public void Should_preserve_extra_properties_on_ServerChatPrintJsonPacket() public void Should_preserve_extra_properties_on_TutorialPrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); var players = Substitute.For(); players.GetPlayerAlias(5).Returns("LocalPlayer"); players.Players.Returns(GetPlayerCollection(new List { @@ -653,7 +673,7 @@ public void Should_preserve_extra_properties_on_TutorialPrintJsonPacket() connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(5); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); TutorialLogMessage logMessage = null; @@ -674,8 +694,7 @@ public void Should_preserve_extra_properties_on_TutorialPrintJsonPacket() public void Should_preserve_extra_properties_on_TagsChangedPrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); var players = Substitute.For(); players.GetPlayerAlias(5).Returns("LocalPlayer"); players.Players.Returns(GetPlayerCollection(new List { @@ -689,7 +708,7 @@ public void Should_preserve_extra_properties_on_TagsChangedPrintJsonPacket() connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(5); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); TagsChangedLogMessage logMessage = null; @@ -722,8 +741,7 @@ public void Should_preserve_extra_properties_on_TagsChangedPrintJsonPacket() public void Should_preserve_extra_properties_on_CommandResultPrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); var players = Substitute.For(); players.GetPlayerAlias(5).Returns("LocalPlayer"); players.Players.Returns(GetPlayerCollection(new List { @@ -737,7 +755,7 @@ public void Should_preserve_extra_properties_on_CommandResultPrintJsonPacket() connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(5); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); CommandResultLogMessage logMessage = null; @@ -758,8 +776,7 @@ public void Should_preserve_extra_properties_on_CommandResultPrintJsonPacket() public void Should_preserve_extra_properties_on_AdminCommandResultPrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); var players = Substitute.For(); players.GetPlayerAlias(5).Returns("LocalPlayer"); players.Players.Returns(GetPlayerCollection(new List { @@ -773,7 +790,7 @@ public void Should_preserve_extra_properties_on_AdminCommandResultPrintJsonPacke connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(5); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); AdminCommandResultLogMessage logMessage = null; @@ -794,8 +811,7 @@ public void Should_preserve_extra_properties_on_AdminCommandResultPrintJsonPacke public void Should_preserve_extra_properties_on_GoalPrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); var players = Substitute.For(); players.GetPlayerAlias(5).Returns("LocalPlayer"); players.Players.Returns(GetPlayerCollection(new List { @@ -809,7 +825,7 @@ public void Should_preserve_extra_properties_on_GoalPrintJsonPacket() connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(5); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); GoalLogMessage logMessage = null; @@ -839,8 +855,7 @@ public void Should_preserve_extra_properties_on_GoalPrintJsonPacket() public void Should_preserve_extra_properties_on_ReleasePrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); var players = Substitute.For(); players.GetPlayerAlias(5).Returns("LocalPlayer"); players.Players.Returns(GetPlayerCollection(new List { @@ -854,7 +869,7 @@ public void Should_preserve_extra_properties_on_ReleasePrintJsonPacket() connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(5); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); ReleaseLogMessage logMessage = null; @@ -884,8 +899,7 @@ public void Should_preserve_extra_properties_on_ReleasePrintJsonPacket() public void Should_preserve_extra_properties_on_CollectPrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); var players = Substitute.For(); players.GetPlayerAlias(5).Returns("LocalPlayer"); players.Players.Returns(GetPlayerCollection(new List { @@ -899,7 +913,7 @@ public void Should_preserve_extra_properties_on_CollectPrintJsonPacket() connectionInfo.Team.Returns(0); connectionInfo.Slot.Returns(5); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); CollectLogMessage logMessage = null; @@ -929,12 +943,11 @@ public void Should_preserve_extra_properties_on_CollectPrintJsonPacket() public void Should_preserve_extra_properties_on_CountdownPrintJsonPacket() { var socket = Substitute.For(); - var locations = Substitute.For(); - var items = Substitute.For(); + var itemInfoResolver = Substitute.For(); var players = Substitute.For(); var connectionInfo = Substitute.For(); - var sut = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var sut = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); CountdownLogMessage logMessage = null; @@ -955,10 +968,10 @@ public void Should_preserve_extra_properties_on_CountdownPrintJsonPacket() } #if NET471 || NET472 - Dictionary> GetPlayerCollection(IList playerInfos) => + static Dictionary> GetPlayerCollection(IList playerInfos) => new Dictionary>( #else - ReadOnlyDictionary> GetPlayerCollection(IList playerInfos) => + static ReadOnlyDictionary> GetPlayerCollection(IList playerInfos) => new ReadOnlyDictionary>( #endif new Dictionary> { { diff --git a/Archipelago.MultiClient.Net.Tests/MessageParsingFixture.cs b/Archipelago.MultiClient.Net.Tests/MessageParsingFixture.cs index 2c3ac46..5db5048 100644 --- a/Archipelago.MultiClient.Net.Tests/MessageParsingFixture.cs +++ b/Archipelago.MultiClient.Net.Tests/MessageParsingFixture.cs @@ -29,7 +29,6 @@ public void Should_parse_room_info_packet() Assert.That(roomInfoPacket.Version.Major, Is.EqualTo(0)); Assert.That(roomInfoPacket.Version.Minor, Is.EqualTo(3)); Assert.That(roomInfoPacket.Version.Build, Is.EqualTo(5)); - Assert.That(roomInfoPacket.DataPackageVersions.Count, Is.EqualTo(34)); Assert.That(roomInfoPacket.Games.Length, Is.EqualTo(1)); Assert.That(roomInfoPacket.HintCostPercentage, Is.EqualTo(10)); Assert.That(roomInfoPacket.LocationCheckPoints, Is.EqualTo(1)); diff --git a/Archipelago.MultiClient.Net.Tests/ReceivedItemsHelperFixture.cs b/Archipelago.MultiClient.Net.Tests/ReceivedItemsHelperFixture.cs index 02177b0..036d94b 100644 --- a/Archipelago.MultiClient.Net.Tests/ReceivedItemsHelperFixture.cs +++ b/Archipelago.MultiClient.Net.Tests/ReceivedItemsHelperFixture.cs @@ -1,10 +1,9 @@ -using Archipelago.MultiClient.Net.Cache; +using Archipelago.MultiClient.Net.DataPackage; using Archipelago.MultiClient.Net.Helpers; using Archipelago.MultiClient.Net.Models; using Archipelago.MultiClient.Net.Packets; using NSubstitute; using NUnit.Framework; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -18,9 +17,11 @@ public void Enumeration_over_collection_should_not_throw_when_new_data_is_receiv { var socket = Substitute.For(); var locationHelper = Substitute.For(); - var cache = Substitute.For(); + var cache = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); - var sut = new ReceivedItemsHelper(socket, locationHelper, cache); + var sut = new ReceivedItemsHelper(socket, locationHelper, cache, connectionInfo, players); socket.PacketReceived += Raise.Event( @@ -35,12 +36,13 @@ public void Enumeration_over_collection_should_not_throw_when_new_data_is_receiv var enumerateTask = new Task(() => { - var total = 0L; + // ReSharper disable once NotAccessedVariable + var total = 0L; foreach (var networkItem in sut.AllItemsReceived) { Thread.Sleep(1); - total += networkItem.Item; + total += networkItem.ItemId; } }); var receiveNewItemTask = new Task(() => @@ -64,157 +66,47 @@ public void Enumeration_over_collection_should_not_throw_when_new_data_is_receiv }); } - [Test] - public void Get_Item_Name_From_DataPackage_Does_Not_Throw() - { - var socket = Substitute.For(); - var locationHelper = Substitute.For(); - var cache = Substitute.For(); - - var localCache = new DataPackage() - { - Games = new Dictionary() - { - ["TestGame"] = new GameData() - { - ItemLookup = new Dictionary() - { - ["TestItem"] = 1 - } - } - } - }; - cache.TryGetDataPackageFromCache(out Arg.Any()).Returns(x => - { - x[0] = localCache; - return true; - }); - - var sut = new ReceivedItemsHelper(socket, locationHelper, cache); - - sut.ItemReceived += (helper) => - { - var item = helper.DequeueItem(); - var itemName = helper.GetItemName(item.Item); - Assert.That(itemName, Is.EqualTo("TestItem")); - }; - - socket.PacketReceived += - Raise.Event( - new ReceivedItemsPacket - { - Index = 0, - Items = new[] { - new NetworkItem { Item = 1 } - } - }); - } - - [Test] - public void Get_Item_Name_Returns_Null_For_Invalid_ItemId_Where_Game_Doesnt_Exist() - { - var socket = Substitute.For(); - var locationHelper = Substitute.For(); - var cache = Substitute.For(); - - var localCache = new DataPackage() - { - Games = new Dictionary() - { - ["TestGame"] = new GameData() - { - ItemLookup = new Dictionary() - { - ["TestItem"] = 1 - } - } - } - }; - cache.TryGetDataPackageFromCache(out Arg.Any()).Returns(x => - { - x[0] = localCache; - return true; - }); - - var sut = new ReceivedItemsHelper(socket, locationHelper, cache); - - sut.ItemReceived += (helper) => - { - var item = helper.DequeueItem(); - var itemName = helper.GetItemName(item.Item); - Assert.That(itemName, Is.Null); - }; - - socket.PacketReceived += - Raise.Event( - new ReceivedItemsPacket - { - Index = 0, - Items = new[] { - new NetworkItem { Item = 2 } - } - }); - } - //https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net/issues/46 [Test] public void Receiving_Same_Item_From_Same_Location_Should_Add_To_Queue() { - var socket = Substitute.For(); - var locationHelper = Substitute.For(); - var cache = Substitute.For(); - - var localCache = new DataPackage() - { - Games = new Dictionary() - { - ["TestGame"] = new GameData() - { - ItemLookup = new Dictionary() - { - ["TestItem"] = 1 - } - } - } - }; - cache.TryGetDataPackageFromCache(out Arg.Any()).Returns(x => - { - x[0] = localCache; - return true; - }); - - var sut = new ReceivedItemsHelper(socket, locationHelper, cache); - var itemInPacket = new NetworkItem { Item = 1, Location = 1, Player = 1, Flags = Enums.ItemFlags.None }; - - sut.ItemReceived += (helper) => - { - var item = helper.DequeueItem(); - var itemName = helper.GetItemName(item.Item); - Assert.That(item, Is.EqualTo(itemInPacket)); - Assert.That(itemName, Is.EqualTo("TestItem")); - }; - - socket.PacketReceived += - Raise.Event( - new ReceivedItemsPacket - { - Index = 0, - Items = new[] { - itemInPacket - } - }); - - socket.PacketReceived += - Raise.Event( - new ReceivedItemsPacket - { - Index = 1, - Items = new[] { - itemInPacket - } - }); - - Assert.That(sut.Index, Is.EqualTo(2)); + var socket = Substitute.For(); + var locationHelper = Substitute.For(); + var itemInfoResolver = Substitute.For(); + var connectionInfo = Substitute.For(); + var players = Substitute.For(); + + var sut = new ReceivedItemsHelper(socket, locationHelper, itemInfoResolver, connectionInfo, players); + var itemInPacket = new NetworkItem { Item = 1, Location = 1, Player = 1, Flags = Enums.ItemFlags.None }; + + sut.ItemReceived += (helper) => + { + var item = helper.DequeueItem(); + Assert.That(item.ItemId, Is.EqualTo(itemInPacket.Item)); + Assert.That(item.LocationId, Is.EqualTo(itemInPacket.Location)); + }; + + socket.PacketReceived += + Raise.Event( + new ReceivedItemsPacket + { + Index = 0, + Items = new[] { + itemInPacket + } + }); + + socket.PacketReceived += + Raise.Event( + new ReceivedItemsPacket + { + Index = 1, + Items = new[] { + itemInPacket + } + }); + + Assert.That(sut.Index, Is.EqualTo(2)); } - } + } } diff --git a/Archipelago.MultiClient.Net.Tests/SocketFixture.cs b/Archipelago.MultiClient.Net.Tests/SocketFixture.cs index d099442..8951939 100644 --- a/Archipelago.MultiClient.Net.Tests/SocketFixture.cs +++ b/Archipelago.MultiClient.Net.Tests/SocketFixture.cs @@ -14,14 +14,14 @@ namespace Archipelago.MultiClient.Net.Tests [TestFixture] class SocketFixture { - const string WssServer = "archipelago.gg:33459"; - const string WsServer = "archipelago.gg:62176"; + const string WssServer = "archipelago.gg:43929"; + const string WsServer = "localhost:38281"; [TestCase("ws://" + WsServer)] [TestCase("wss://" + WssServer)] [TestCase("unspecified://" + WsServer)] [TestCase("unspecified://" + WssServer)] - public void Should_connect_over_wss(string server) + public void Should_connect_over_ws_or_wss(string server) { var socketIsOpen = false; var errors = ""; @@ -46,9 +46,7 @@ public void Should_connect_over_wss(string server) #endif Assert.IsTrue(socketIsOpen); - - if (!string.IsNullOrEmpty(errors)) - throw new Exception(errors); + Assert.That(errors, Is.Empty); } [Test] @@ -87,8 +85,11 @@ public void Should_timeout_on_non_existing_server() Assert.IsFalse(socketIsOpen); - if (!string.IsNullOrEmpty(errors)) - throw new Exception(errors); +#if NET471 || NET472 + Assert.That(errors, Is.Empty); +#else + Assert.IsTrue(errors.StartsWith("Socket error received:")); +#endif } [Test] @@ -127,8 +128,11 @@ public void Should_timeout_on_invalid_port() Assert.IsFalse(socketIsOpen); - if (!string.IsNullOrEmpty(errors)) - throw new Exception(errors); +#if NET471 || NET472 + Assert.That(errors, Is.Empty); +#else + Assert.IsTrue(errors.StartsWith("Socket error received:")); +#endif } } } \ No newline at end of file diff --git a/Archipelago.MultiClient.Net/Archipelago.MultiClient.Net.csproj b/Archipelago.MultiClient.Net/Archipelago.MultiClient.Net.csproj index 054d743..9ed3e89 100644 --- a/Archipelago.MultiClient.Net/Archipelago.MultiClient.Net.csproj +++ b/Archipelago.MultiClient.Net/Archipelago.MultiClient.Net.csproj @@ -3,16 +3,16 @@ net35;net40;net45;netstandard2.0;net6.0 Archipelago.MultiClient.Net Archipelago.MultiClient.Net - Copyright © 2023 + Copyright © 2024 bin\$(Configuration)\ A client library for use with .NET based prog-langs for interfacing with Archipelago hosts. https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net README.md https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net git - 5.0.6.0 - 5.0.6.0 - 5.0.6 + 6.0.0.0 + 6.0.0.0 + 6.0.0-rc4 MIT False blue-icon.png @@ -42,7 +42,7 @@ - + ..\DLLs\net35\Newtonsoft.Json.dll @@ -51,9 +51,18 @@ + + + ..\DLLs\net40\Newtonsoft.Json.dll + + + ..\DLLs\websocket-sharp.dll + + + - ..\DLLs\net35\Newtonsoft.Json.dll + ..\DLLs\net45\Newtonsoft.Json.dll diff --git a/Archipelago.MultiClient.Net/ArchipelagoSession.cs b/Archipelago.MultiClient.Net/ArchipelagoSession.cs index 8f5b91a..867d717 100644 --- a/Archipelago.MultiClient.Net/ArchipelagoSession.cs +++ b/Archipelago.MultiClient.Net/ArchipelagoSession.cs @@ -19,7 +19,7 @@ namespace Archipelago.MultiClient.Net /// /// `ArchipelagoSession` is the overarching class to access and modify and respond to the multiworld state /// - public class ArchipelagoSession + public partial class ArchipelagoSession { const int ArchipelagoConnectionTimeoutInSeconds = 4; @@ -31,37 +31,38 @@ public class ArchipelagoSession /// /// Provides simplified methods to receive items /// - public ReceivedItemsHelper Items { get; } + public IReceivedItemsHelper Items { get; } /// /// Provides way to mark locations as checked /// - public LocationCheckHelper Locations { get; } + public ILocationCheckHelper Locations { get; } /// /// Provides information about all players in the multiworld /// - public PlayerHelper Players { get; } + public IPlayerHelper Players { get; } /// /// Provides access to a server side data storage to share and store values across sessions and players /// - public DataStorageHelper DataStorage { get; } + public IDataStorageHelper DataStorage { get; } /// /// Provides information about your current connection /// - public ConnectionInfoHelper ConnectionInfo { get; } + public IConnectionInfoProvider ConnectionInfo => connectionInfo; + ConnectionInfoHelper connectionInfo; /// /// Provides information about the current state of the server /// - public RoomStateHelper RoomState { get; } + public IRoomStateHelper RoomState { get; } /// /// Provides simplified handlers to receive messages about events that happen in the multiworld /// - public MessageLogHelper MessageLog { get; } + public IMessageLogHelper MessageLog { get; } #if NET35 volatile bool awaitingRoomInfo; @@ -73,27 +74,26 @@ public class ArchipelagoSession #endif internal ArchipelagoSession(IArchipelagoSocketHelper socket, - ReceivedItemsHelper items, - LocationCheckHelper locations, - PlayerHelper players, - RoomStateHelper roomState, - ConnectionInfoHelper connectionInfo, - DataStorageHelper dataStorage, - MessageLogHelper messageLog) + IReceivedItemsHelper items, + ILocationCheckHelper locations, + IPlayerHelper players, + IRoomStateHelper roomState, + ConnectionInfoHelper connectionInfoHelper, + IDataStorageHelper dataStorage, + IMessageLogHelper messageLog) { Socket = socket; Items = items; Locations = locations; Players = players; RoomState = roomState; - ConnectionInfo = connectionInfo; + connectionInfo = connectionInfoHelper; DataStorage = dataStorage; MessageLog = messageLog; socket.PacketReceived += Socket_PacketReceived; } - - + void Socket_PacketReceived(ArchipelagoPacketBase packet) { switch (packet) @@ -222,7 +222,7 @@ public Task LoginAsync(string game, string name, ItemsHandlingFlags return loginResultTask.Task; } - ConnectionInfo.SetConnectionParameters(game, tags, itemsHandlingFlags, uuid); + connectionInfo.SetConnectionParameters(game, tags, itemsHandlingFlags, uuid); try { @@ -277,7 +277,7 @@ public LoginResult TryConnectAndLogin(string game, string name, ItemsHandlingFla Version version = null, string[] tags = null, string uuid = null, string password = null, bool requestSlotData = true) { #if NET35 - ConnectionInfo.SetConnectionParameters(game, tags, itemsHandlingFlags, uuid); + connectionInfo.SetConnectionParameters(game, tags, itemsHandlingFlags, uuid); try { diff --git a/Archipelago.MultiClient.Net/ArchipelagoSessionActions.cs b/Archipelago.MultiClient.Net/ArchipelagoSessionActions.cs new file mode 100644 index 0000000..8b6661f --- /dev/null +++ b/Archipelago.MultiClient.Net/ArchipelagoSessionActions.cs @@ -0,0 +1,27 @@ +using Archipelago.MultiClient.Net.Enums; +using Archipelago.MultiClient.Net.Packets; + +namespace Archipelago.MultiClient.Net +{ + public partial class ArchipelagoSession + { + /// + /// Send a message on behalf of the current player + /// Can also be used to send commands like !hint item + /// + /// The message that will be broadcasted to the server and all clients + public void Say(string message) => Socket.SendPacket(new SayPacket { Text = message }); + + /// + /// Updates the status of the current player + /// + /// the state to send to the server + public void SetClientState(ArchipelagoClientState state) => Socket.SendPacket(new StatusUpdatePacket { Status = state }); + + /// + /// Informs the server that the current player has reached their goal + /// This cannot be reverted + /// + public void SetGoalAchieved() => SetClientState(ArchipelagoClientState.ClientGoal); + } +} diff --git a/Archipelago.MultiClient.Net/ArchipelagoSessionFactory.cs b/Archipelago.MultiClient.Net/ArchipelagoSessionFactory.cs index 56a5a91..8c45701 100644 --- a/Archipelago.MultiClient.Net/ArchipelagoSessionFactory.cs +++ b/Archipelago.MultiClient.Net/ArchipelagoSessionFactory.cs @@ -1,4 +1,4 @@ -using Archipelago.MultiClient.Net.Cache; +using Archipelago.MultiClient.Net.DataPackage; using Archipelago.MultiClient.Net.Helpers; using System; @@ -19,13 +19,14 @@ public static ArchipelagoSession CreateSession(Uri uri) { var socket = new ArchipelagoSocketHelper(uri); var dataPackageCache = new DataPackageCache(socket); - var locations = new LocationCheckHelper(socket, dataPackageCache); - var items = new ReceivedItemsHelper(socket, locations, dataPackageCache); var connectionInfo = new ConnectionInfoHelper(socket); - var players = new PlayerHelper(socket, connectionInfo); - var roomState = new RoomStateHelper(socket, locations); + var players = new PlayerHelper(socket, connectionInfo); + var itemInfoResolver = new ItemInfoResolver(dataPackageCache, connectionInfo); + var locations = new LocationCheckHelper(socket, itemInfoResolver, connectionInfo, players); + var items = new ReceivedItemsHelper(socket, locations, itemInfoResolver, connectionInfo, players); + var roomState = new RoomStateHelper(socket, locations); var dataStorage = new DataStorageHelper(socket, connectionInfo); - var messageLog = new MessageLogHelper(socket, items, locations, players, connectionInfo); + var messageLog = new MessageLogHelper(socket, itemInfoResolver, players, connectionInfo); return new ArchipelagoSession(socket, items, locations, players, roomState, connectionInfo, dataStorage, messageLog); } diff --git a/Archipelago.MultiClient.Net/BounceFeatures/DeathLink/DeathLink.cs b/Archipelago.MultiClient.Net/BounceFeatures/DeathLink/DeathLink.cs index f012de5..5571e48 100644 --- a/Archipelago.MultiClient.Net/BounceFeatures/DeathLink/DeathLink.cs +++ b/Archipelago.MultiClient.Net/BounceFeatures/DeathLink/DeathLink.cs @@ -5,6 +5,9 @@ namespace Archipelago.MultiClient.Net.BounceFeatures.DeathLink { + /// + /// A DeathLink object that gets sent and received via bounce packets. + /// public class DeathLink : IEquatable { /// @@ -61,6 +64,7 @@ internal static bool TryParse(Dictionary data, out DeathLink dea } } + /// public bool Equals(DeathLink other) { if (ReferenceEquals(null, other)) @@ -76,7 +80,8 @@ public bool Equals(DeathLink other) && Timestamp.Second == other.Timestamp.Second; } - public override bool Equals(object obj) + /// + public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; @@ -90,7 +95,8 @@ public override bool Equals(object obj) return Equals((DeathLink)obj); } - public override int GetHashCode() + /// + public override int GetHashCode() { unchecked { @@ -101,8 +107,10 @@ public override int GetHashCode() } } - public static bool operator ==(DeathLink lhs, DeathLink rhs) => lhs?.Equals(rhs) ?? rhs is null; - - public static bool operator !=(DeathLink lhs, DeathLink rhs) => !(lhs == rhs); - } +#pragma warning disable CS1591 + public static bool operator ==(DeathLink lhs, DeathLink rhs) => lhs?.Equals(rhs) ?? rhs is null; + + public static bool operator !=(DeathLink lhs, DeathLink rhs) => !(lhs == rhs); +#pragma warning restore CS1591 + } } diff --git a/Archipelago.MultiClient.Net/Cache/DataPackageCache.cs b/Archipelago.MultiClient.Net/Cache/DataPackageCache.cs deleted file mode 100644 index 9aaa51b..0000000 --- a/Archipelago.MultiClient.Net/Cache/DataPackageCache.cs +++ /dev/null @@ -1,176 +0,0 @@ -using Archipelago.MultiClient.Net.Helpers; -using Archipelago.MultiClient.Net.Models; -using Archipelago.MultiClient.Net.Packets; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Archipelago.MultiClient.Net.Cache -{ - class DataPackageCache : IDataPackageCache - { - readonly IArchipelagoSocketHelper socket; - - readonly Dictionary dataPackages = new Dictionary(); - - internal IFileSystemDataPackageProvider FileSystemDataPackageProvider; - - public DataPackageCache(IArchipelagoSocketHelper socket) - { - this.socket = socket; - - socket.PacketReceived += Socket_PacketReceived; - } - - public DataPackageCache(IArchipelagoSocketHelper socket, IFileSystemDataPackageProvider fileSystemProvider) - { - this.socket = socket; - - FileSystemDataPackageProvider = fileSystemProvider; - - socket.PacketReceived += Socket_PacketReceived; - } - - void Socket_PacketReceived(ArchipelagoPacketBase packet) - { - switch (packet) - { - case RoomInfoPacket roomInfoPacket: - - List invalidated; - - if (roomInfoPacket.Version == null || roomInfoPacket.Version.ToVersion() < new Version(0, 4, 0)) - { - if (FileSystemDataPackageProvider == null) - FileSystemDataPackageProvider = new FileSystemVersionBasedDataPackageProvider(); - - AddArchipelagoGame(roomInfoPacket); - - LoadDataPackageFromFileCacheByVersion(roomInfoPacket.Games); - - invalidated = GetCacheInvalidatedGamesByVersion(roomInfoPacket); - } - else - { - if (FileSystemDataPackageProvider == null) - FileSystemDataPackageProvider = new FileSystemCheckSumDataPackageProvider(); - - invalidated = GetCacheInvalidatedGamesByChecksum(roomInfoPacket); - } - - if (invalidated.Any()) - { - socket.SendPacket(new GetDataPackagePacket - { - Games = invalidated.ToArray() - }); - } - break; - case DataPackagePacket packagePacket: - UpdateDataPackageFromServer(packagePacket.DataPackage); - break; - } - } - - static void AddArchipelagoGame(RoomInfoPacket roomInfoPacket) => - roomInfoPacket.Games = roomInfoPacket.Games - .Concat(new[] { "Archipelago" }) - .Distinct() - .ToArray(); - - void LoadDataPackageFromFileCacheByVersion(string[] games) - { - foreach (var game in games) - if (TryGetGameDataFromFileCacheByVersion(game, out var cachedPackage)) - dataPackages[game] = cachedPackage; - } - - bool TryGetGameDataFromFileCacheByVersion(string game, out GameData gameData) - { - if (FileSystemDataPackageProvider.TryGetDataPackage(game, "", out var cachedGameData)) - { - gameData = cachedGameData; - return true; - } - - gameData = null; - return false; - } - - public bool TryGetDataPackageFromCache(out DataPackage package) - { - package = new DataPackage { Games = dataPackages }; - - return dataPackages.Count > 1; - } - - public bool TryGetGameDataFromCache(string game, out GameData gameData) - { - if (dataPackages.TryGetValue(game, out var cachedGameData)) - { - gameData = cachedGameData; - return true; - } - - gameData = null; - return false; - } - - internal void UpdateDataPackageFromServer(DataPackage package) - { - foreach (var packageGameData in package.Games) - { - dataPackages[packageGameData.Key] = packageGameData.Value; - - if (FileSystemDataPackageProvider is IFileSystemDataVersionBasedPackageProvider && packageGameData.Value.Version == 0) - continue; - - FileSystemDataPackageProvider.SaveDataPackageToFile(packageGameData.Key, packageGameData.Value); - } - } - - List GetCacheInvalidatedGamesByVersion(RoomInfoPacket packet) - { - var gamesNeedingUpdating = new List(); - - foreach (var game in packet.Games) - { - if (packet.DataPackageVersions != null - && packet.DataPackageVersions.ContainsKey(game) - && dataPackages.TryGetValue(game, out var gameData)) - { - if (gameData.Version != packet.DataPackageVersions[game]) - gamesNeedingUpdating.Add(game); - } - else - { - gamesNeedingUpdating.Add(game); - } - } - - return gamesNeedingUpdating; - } - - List GetCacheInvalidatedGamesByChecksum(RoomInfoPacket packet) - { - var gamesNeedingUpdating = new List(); - - foreach (var game in packet.Games) - { - if (packet.DataPackageChecksums != null - && packet.DataPackageChecksums.TryGetValue(game, out var checksum) - && FileSystemDataPackageProvider.TryGetDataPackage(game, checksum, out var gameData) - && gameData.Checksum == checksum) - { - dataPackages[game] = gameData; - } - else - { - gamesNeedingUpdating.Add(game); - } - } - - return gamesNeedingUpdating; - } - } -} \ No newline at end of file diff --git a/Archipelago.MultiClient.Net/Cache/FileSystemVersionBasedDataPackageProvider.cs b/Archipelago.MultiClient.Net/Cache/FileSystemVersionBasedDataPackageProvider.cs deleted file mode 100644 index 5aa9eb6..0000000 --- a/Archipelago.MultiClient.Net/Cache/FileSystemVersionBasedDataPackageProvider.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Archipelago.MultiClient.Net.Models; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Archipelago.MultiClient.Net.Cache -{ - interface IFileSystemDataVersionBasedPackageProvider : IFileSystemDataPackageProvider - { - } - - class FileSystemVersionBasedDataPackageProvider : IFileSystemDataVersionBasedPackageProvider - { - const string DataPackageFolderName = "ArchipelagoDatapackageCache"; - readonly string cacheFolder = - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DataPackageFolderName); - string GetFilePath(string game) => Path.Combine(cacheFolder, $"{GetFileSystemSafeFileName(game)}.json"); - - readonly object fileAccessLockObjectsLock = new object(); - readonly Dictionary fileAccessLockObjects = new Dictionary(); - - public bool TryGetDataPackage(string game, string _, out GameData gameData) - { - var filePath = GetFilePath(game); - - lock (GetFileLock(game)) - { - - try - { - if (File.Exists(filePath)) - { - var fileText = File.ReadAllText(filePath); - gameData = JsonConvert.DeserializeObject(fileText); - return gameData != null; - } - } - catch - { - // ignored - } - - gameData = null; - return false; - } - } - - public void SaveDataPackageToFile(string game, GameData gameData) - { - Directory.CreateDirectory(cacheFolder); - - lock (GetFileLock(game)) - { - try - { - var contents = JsonConvert.SerializeObject(gameData); - File.WriteAllText(GetFilePath(game), contents); - } - catch - { - // ignored - } - } - } - - string GetFileSystemSafeFileName(string gameName) - { - var safeName = gameName; - - foreach (var c in Path.GetInvalidFileNameChars()) - gameName = gameName.Replace(c, '_'); - - return safeName; - } - - object GetFileLock(string game) - { - lock (fileAccessLockObjectsLock) - { - if (fileAccessLockObjects.TryGetValue(game, out var lockObject)) - return lockObject; - - return fileAccessLockObjects[game] = new object(); - } - } - } -} diff --git a/Archipelago.MultiClient.Net/Cache/IDataPackageCache.cs b/Archipelago.MultiClient.Net/Cache/IDataPackageCache.cs deleted file mode 100644 index addad4a..0000000 --- a/Archipelago.MultiClient.Net/Cache/IDataPackageCache.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Archipelago.MultiClient.Net.Models; - -namespace Archipelago.MultiClient.Net.Cache -{ - interface IDataPackageCache - { - bool TryGetGameDataFromCache(string game, out GameData package); - bool TryGetDataPackageFromCache(out DataPackage package); - } -} \ No newline at end of file diff --git a/Archipelago.MultiClient.Net/Converters/UnixTimeConverter.cs b/Archipelago.MultiClient.Net/Converters/UnixTimeConverter.cs index 3ff83ea..42eab28 100644 --- a/Archipelago.MultiClient.Net/Converters/UnixTimeConverter.cs +++ b/Archipelago.MultiClient.Net/Converters/UnixTimeConverter.cs @@ -2,6 +2,9 @@ namespace Archipelago.MultiClient.Net.Converters { + /// + /// Provides methods to convert between Unix timestamps and DateTime objects. + /// public static class UnixTimeConverter { static DateTime UtcEpoch => @@ -10,7 +13,11 @@ public static class UnixTimeConverter #else DateTime.UnixEpoch; #endif - + /// + /// Converts a Unix timestamp to a DateTime object. + /// + /// a unix timestamp either in 64bit format or 32bit format + /// public static DateTime UnixTimeStampToDateTime(double unixTimeStamp) { if (unixTimeStamp > 1000000000000) //its a 64-bit unix timestamp @@ -19,6 +26,11 @@ public static DateTime UnixTimeStampToDateTime(double unixTimeStamp) return UtcEpoch.AddSeconds(unixTimeStamp); } + /// + /// Converts a DateTime object to a Unix timestamp. + /// + /// a datetime timestamp + /// public static double ToUnixTimeStamp(this DateTime dateTime) => (dateTime - UtcEpoch).TotalMilliseconds / 1000; } } diff --git a/Archipelago.MultiClient.Net/DataPackage/DataPackageCache.cs b/Archipelago.MultiClient.Net/DataPackage/DataPackageCache.cs new file mode 100644 index 0000000..4815da1 --- /dev/null +++ b/Archipelago.MultiClient.Net/DataPackage/DataPackageCache.cs @@ -0,0 +1,103 @@ +using Archipelago.MultiClient.Net.Helpers; +using Archipelago.MultiClient.Net.Packets; +using System.Collections.Generic; +using System.Linq; + +namespace Archipelago.MultiClient.Net.DataPackage +{ + class DataPackageCache : IDataPackageCache + { + readonly IArchipelagoSocketHelper socket; + + // ReSharper disable once ArrangeObjectCreationWhenTypeEvident + readonly Dictionary inMemoryCache = new Dictionary(); + + internal IFileSystemDataPackageProvider FileSystemDataPackageProvider; + + public DataPackageCache(IArchipelagoSocketHelper socket) + { + this.socket = socket; + + socket.PacketReceived += Socket_PacketReceived; + } + + public DataPackageCache(IArchipelagoSocketHelper socket, IFileSystemDataPackageProvider fileSystemProvider) + { + this.socket = socket; + + FileSystemDataPackageProvider = fileSystemProvider; + + socket.PacketReceived += Socket_PacketReceived; + } + + void Socket_PacketReceived(ArchipelagoPacketBase packet) + { + switch (packet) + { + case RoomInfoPacket roomInfoPacket: + if (FileSystemDataPackageProvider == null) + FileSystemDataPackageProvider = new FileSystemCheckSumDataPackageProvider(); + + var invalidated = GetCacheInvalidatedGamesByChecksum(roomInfoPacket); + if (invalidated.Any()) + { + socket.SendPacket(new GetDataPackagePacket + { + Games = invalidated.ToArray() + }); + } + break; + case DataPackagePacket packagePacket: + UpdateDataPackageFromServer(packagePacket.DataPackage); + break; + } + } + + public bool TryGetDataPackageFromCache(out Dictionary gameData) + { + gameData = inMemoryCache; + + return inMemoryCache.Any(); + } + + public bool TryGetGameDataFromCache(string game, out IGameDataLookup gameData) + { + if (inMemoryCache.TryGetValue(game, out var cachedGameData)) + { + gameData = cachedGameData; + return true; + } + + gameData = null; + return false; + } + + internal void UpdateDataPackageFromServer(Models.DataPackage package) + { + foreach (var packageGameData in package.Games) + { + inMemoryCache[packageGameData.Key] = new GameDataLookup(packageGameData.Value); + + FileSystemDataPackageProvider.SaveDataPackageToFile(packageGameData.Key, packageGameData.Value); + } + } + + List GetCacheInvalidatedGamesByChecksum(RoomInfoPacket packet) + { + var gamesNeedingUpdating = new List(); + + foreach (var game in packet.Games) + { + if (packet.DataPackageChecksums != null + && packet.DataPackageChecksums.TryGetValue(game, out var checksum) + && FileSystemDataPackageProvider.TryGetDataPackage(game, checksum, out var gameData) + && gameData.Checksum == checksum) + inMemoryCache[game] = new GameDataLookup(gameData); + else + gamesNeedingUpdating.Add(game); + } + + return gamesNeedingUpdating; + } + } +} \ No newline at end of file diff --git a/Archipelago.MultiClient.Net/Cache/FileSystemCheckSumDataPackageProvider.cs b/Archipelago.MultiClient.Net/DataPackage/FileSystemCheckSumDataPackageProvider.cs similarity index 87% rename from Archipelago.MultiClient.Net/Cache/FileSystemCheckSumDataPackageProvider.cs rename to Archipelago.MultiClient.Net/DataPackage/FileSystemCheckSumDataPackageProvider.cs index 585de64..d5a4111 100644 --- a/Archipelago.MultiClient.Net/Cache/FileSystemCheckSumDataPackageProvider.cs +++ b/Archipelago.MultiClient.Net/DataPackage/FileSystemCheckSumDataPackageProvider.cs @@ -3,7 +3,7 @@ using System; using System.IO; -namespace Archipelago.MultiClient.Net.Cache +namespace Archipelago.MultiClient.Net.DataPackage { class FileSystemCheckSumDataPackageProvider : IFileSystemDataPackageProvider { @@ -43,7 +43,7 @@ public bool TryGetDataPackage(string game, string checksum, out GameData gameDat public void SaveDataPackageToFile(string game, GameData gameData) { var folderPath = Path.Combine(CacheFolder, GetFileSystemSafeFileName(game)); - var filePath = Path.Combine(folderPath, $"{gameData.Checksum}.json"); + var filePath = Path.Combine(folderPath, $"{GetFileSystemSafeFileName(gameData.Checksum)}.json"); try { diff --git a/Archipelago.MultiClient.Net/DataPackage/GameDataLookup.cs b/Archipelago.MultiClient.Net/DataPackage/GameDataLookup.cs new file mode 100644 index 0000000..d12bc81 --- /dev/null +++ b/Archipelago.MultiClient.Net/DataPackage/GameDataLookup.cs @@ -0,0 +1,32 @@ +using Archipelago.MultiClient.Net.Models; + +namespace Archipelago.MultiClient.Net.DataPackage +{ + interface IGameDataLookup + { + TwoWayLookup Locations { get; } + TwoWayLookup Items { get; } + string Checksum { get; } + } + + class GameDataLookup : IGameDataLookup + { + public TwoWayLookup Locations { get; } + public TwoWayLookup Items { get; } + public string Checksum { get; } + + public GameDataLookup(GameData gameData) + { + Locations = new TwoWayLookup(); + Items = new TwoWayLookup(); + + Checksum = gameData.Checksum; + + foreach (var kvp in gameData.LocationLookup) + Locations.Add(kvp.Value, kvp.Key); + + foreach (var kvp in gameData.ItemLookup) + Items.Add(kvp.Value, kvp.Key); + } + } +} diff --git a/Archipelago.MultiClient.Net/DataPackage/IDataPackageCache.cs b/Archipelago.MultiClient.Net/DataPackage/IDataPackageCache.cs new file mode 100644 index 0000000..291e58c --- /dev/null +++ b/Archipelago.MultiClient.Net/DataPackage/IDataPackageCache.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Archipelago.MultiClient.Net.DataPackage +{ + interface IDataPackageCache + { + bool TryGetGameDataFromCache(string game, out IGameDataLookup gameData); + bool TryGetDataPackageFromCache(out Dictionary gameData); + } +} \ No newline at end of file diff --git a/Archipelago.MultiClient.Net/Cache/IFileSystemDataPackageProvider.cs b/Archipelago.MultiClient.Net/DataPackage/IFileSystemDataPackageProvider.cs similarity index 80% rename from Archipelago.MultiClient.Net/Cache/IFileSystemDataPackageProvider.cs rename to Archipelago.MultiClient.Net/DataPackage/IFileSystemDataPackageProvider.cs index 1b207c1..c9d618d 100644 --- a/Archipelago.MultiClient.Net/Cache/IFileSystemDataPackageProvider.cs +++ b/Archipelago.MultiClient.Net/DataPackage/IFileSystemDataPackageProvider.cs @@ -1,6 +1,6 @@ using Archipelago.MultiClient.Net.Models; -namespace Archipelago.MultiClient.Net.Cache +namespace Archipelago.MultiClient.Net.DataPackage { interface IFileSystemDataPackageProvider { diff --git a/Archipelago.MultiClient.Net/DataPackage/ItemInfoResolver.cs b/Archipelago.MultiClient.Net/DataPackage/ItemInfoResolver.cs new file mode 100644 index 0000000..485faa2 --- /dev/null +++ b/Archipelago.MultiClient.Net/DataPackage/ItemInfoResolver.cs @@ -0,0 +1,118 @@ +using Archipelago.MultiClient.Net.Helpers; + +namespace Archipelago.MultiClient.Net.DataPackage +{ + /// + /// Provides methods to resolve item additional information based on ideez + /// + public interface IItemInfoResolver + { + /// + /// Perform a lookup using the DataPackage sent as a source of truth to lookup a particular item id for a particular game. + /// + /// + /// Id of the item to lookup. + /// + /// + /// The game to lookup the item id for, if null will look in the game the local player is connected to. + /// Negative item ids are always looked up under the Archipelago game + /// + /// + /// The name of the item as a string, or null if no such item is found. + /// + string GetItemName(long itemId, string game = null); + + /// + /// Get the name of a location from its id. Useful when receiving a packet and it is necessary to find the name of the location. + /// + /// + /// The Id of the location to look up the name for. Must match the contents of the datapackage. + /// + /// + /// The game to lookup the location id for, if null will look in the game the local player is connected to. + /// Negative location ids are always looked up under the Archipelago game + /// + /// + /// Returns the locationName for the provided locationId, or null if no such location is found. + /// + string GetLocationName(long locationId, string game = null); + + /// + /// Get the Id of a location from its name. Useful when a game knows its locations by name but not by Archipelago Id. + /// + /// + /// The game to look up the locations from + /// + /// + /// The name of the location to check the Id for. Must match the contents of the datapackage. + /// + /// + /// Returns the locationId for the location name that was given or -1 if no location was found. + /// + long GetLocationId(string locationName, string game = null); + } + + /// + class ItemInfoResolver : IItemInfoResolver + { + readonly IDataPackageCache cache; + readonly IConnectionInfoProvider connectionInfoProvider; + + public ItemInfoResolver(IDataPackageCache cache, IConnectionInfoProvider connectionInfoProvider) + { + this.cache = cache; + this.connectionInfoProvider = connectionInfoProvider; + } + + /// + public string GetItemName(long itemId, string game = null) + { + if (game == null) + game = connectionInfoProvider.Game ?? "Archipelago"; + + if (itemId < 0) + game = "Archipelago"; + + if (!cache.TryGetGameDataFromCache(game, out var dataPackage)) + return null; + + return dataPackage.Items.TryGetValue(itemId, out var itemName) + ? itemName + : null; + } + + /// + public string GetLocationName(long locationId, string game = null) + { + if (game == null) + game = connectionInfoProvider.Game ?? "Archipelago"; + + if (locationId < 0) + game = "Archipelago"; + + if (!cache.TryGetGameDataFromCache(game, out var dataPackage)) + return null; + + return dataPackage.Locations.TryGetValue(locationId, out var itemName) + ? itemName + : null; + } + + /// + public long GetLocationId(string locationName, string game = null) + { + if (game == null) + game = connectionInfoProvider.Game ?? "Archipelago"; + + if (cache.TryGetGameDataFromCache(game, out var gameData)) + if (gameData.Locations.TryGetValue(locationName, out var locationIdInGame)) + return locationIdInGame; + + if (cache.TryGetGameDataFromCache("Archipelago", out var archipelagoData)) + if (archipelagoData.Locations.TryGetValue(locationName, out var locationIdInArchipelago)) + return locationIdInArchipelago; + + return -1; //in hindsight -1 isnt a great return here as its a valid locationid in itzelf + } + } +} diff --git a/Archipelago.MultiClient.Net/Enums/ArchipelagoClientState.cs b/Archipelago.MultiClient.Net/Enums/ArchipelagoClientState.cs index 6628ebf..afccdec 100644 --- a/Archipelago.MultiClient.Net/Enums/ArchipelagoClientState.cs +++ b/Archipelago.MultiClient.Net/Enums/ArchipelagoClientState.cs @@ -1,11 +1,29 @@ -namespace Archipelago.MultiClient.Net.Enums -{ - public enum ArchipelagoClientState : int - { - ClientUnknown = 0, - ClientConnected = 5, - ClientReady = 10, - ClientPlaying = 20, - ClientGoal = 30 - } -} +namespace Archipelago.MultiClient.Net.Enums +{ + /// + /// The state of a player + /// + public enum ArchipelagoClientState : int + { + /// + /// Default value, no state is known for this player + /// + ClientUnknown = 0, + /// + /// The player is connected to the multiworld + /// + ClientConnected = 5, + /// + /// The player is ready to start playing + /// + ClientReady = 10, + /// + /// The player started playing + /// + ClientPlaying = 20, + /// + /// The player has finished their goal + /// + ClientGoal = 30 + } +} diff --git a/Archipelago.MultiClient.Net/Enums/ConnectionRefusedError.cs b/Archipelago.MultiClient.Net/Enums/ConnectionRefusedError.cs index aaf1565..8c5204c 100644 --- a/Archipelago.MultiClient.Net/Enums/ConnectionRefusedError.cs +++ b/Archipelago.MultiClient.Net/Enums/ConnectionRefusedError.cs @@ -1,5 +1,8 @@ namespace Archipelago.MultiClient.Net.Enums { + /// + /// The possible reasons for a connection to be refused. + /// public enum ConnectionRefusedError { /// diff --git a/Archipelago.MultiClient.Net/Enums/HintCreationPolicy.cs b/Archipelago.MultiClient.Net/Enums/HintCreationPolicy.cs new file mode 100644 index 0000000..ba2e128 --- /dev/null +++ b/Archipelago.MultiClient.Net/Enums/HintCreationPolicy.cs @@ -0,0 +1,22 @@ +namespace Archipelago.MultiClient.Net.Enums +{ + /// + /// Specifies if a location scout should create an actual hint on the AP server + /// Hints created using location scouts never cost hintpoints + /// + public enum HintCreationPolicy + { + /// + /// Do not create a hint + /// + None = 0, + /// + /// Create a hint and announce it player that the item belongs to + /// + CreateAndAnnounce = 1, + /// + /// Create a hint and announce it player that the item belongs to but only once announce if the hint didnt exist yet + /// + CreateAndAnnounceOnce = 2, + } +} diff --git a/Archipelago.MultiClient.Net/Enums/InvalidPacketErrorType.cs b/Archipelago.MultiClient.Net/Enums/InvalidPacketErrorType.cs index abe28d3..eb47ece 100644 --- a/Archipelago.MultiClient.Net/Enums/InvalidPacketErrorType.cs +++ b/Archipelago.MultiClient.Net/Enums/InvalidPacketErrorType.cs @@ -1,9 +1,18 @@  namespace Archipelago.MultiClient.Net.Enums { + /// + /// Types of errors that can be returned by the server if your packet gets rejected + /// public enum InvalidPacketErrorType { + /// + /// The command in your packet was invalid + /// Cmd, + /// + /// Some of the arguments in your packet were invalid + /// Arguments } } diff --git a/Archipelago.MultiClient.Net/Enums/JsonMessagePartColor.cs b/Archipelago.MultiClient.Net/Enums/JsonMessagePartColor.cs index 0870b8c..48f6bf9 100644 --- a/Archipelago.MultiClient.Net/Enums/JsonMessagePartColor.cs +++ b/Archipelago.MultiClient.Net/Enums/JsonMessagePartColor.cs @@ -1,24 +1,84 @@ namespace Archipelago.MultiClient.Net.Enums { + /// + /// Specified colors for printed messages + /// This color should be used if specified + /// colors suffixed in Bg should be applied to the background + /// There colors are based of standard terminal colors + /// public enum JsonMessagePartColor { + /// + /// Message should be bold + /// Bold, - Underline, - Black, - Red, - Green, - Yellow, - Blue, - Magenta, - Cyan, - White, - BlackBg, - RedBg, - GreenBg, - YellowBg, - BlueBg, - MagentaBg, - CyanBg, - WhiteBg - } + /// + /// Message should be underlined + /// + Underline, + /// + /// Message should be colored black + /// + Black, + /// + /// Message should be colored red + /// + Red, + /// + /// Message should be colored green + /// + Green, + /// + /// Message should be colored yellow + /// + Yellow, + /// + /// Message should be colored blue + /// + Blue, + /// + /// Message should be colored magenta + /// + Magenta, + /// + /// Message should be colored cyan + /// + Cyan, + /// + /// Message should be colored white + /// + White, + /// + /// Message should have a black background + /// + BlackBg, + /// + /// Message should have a red background + /// + RedBg, + /// + /// Message should have a green background + /// + GreenBg, + /// + /// Message should have a yellow background + /// + YellowBg, + /// + /// Message should have a blue background + /// + BlueBg, + /// + /// Message should have a magenta background + /// + MagentaBg, + /// + /// Message should have a cyan background + /// + CyanBg, + /// + /// Message should have a white background + /// + WhiteBg + } } \ No newline at end of file diff --git a/Archipelago.MultiClient.Net/Enums/JsonMessagePartType.cs b/Archipelago.MultiClient.Net/Enums/JsonMessagePartType.cs index 5cf1c1c..f54b384 100644 --- a/Archipelago.MultiClient.Net/Enums/JsonMessagePartType.cs +++ b/Archipelago.MultiClient.Net/Enums/JsonMessagePartType.cs @@ -1,5 +1,8 @@ namespace Archipelago.MultiClient.Net.Enums { + /// + /// Type of message parts of an print message + /// public enum JsonMessagePartType { /// diff --git a/Archipelago.MultiClient.Net/Enums/OperationType.cs b/Archipelago.MultiClient.Net/Enums/OperationType.cs index 1aba26b..ec10f50 100644 --- a/Archipelago.MultiClient.Net/Enums/OperationType.cs +++ b/Archipelago.MultiClient.Net/Enums/OperationType.cs @@ -5,7 +5,8 @@ /// public enum OperationType { - Add, +#pragma warning disable CS1591 + Add, Mul, Max, Min, @@ -20,6 +21,9 @@ public enum OperationType RightShift, Remove, Pop, - Update + Update, + Floor, + Ceil +#pragma warning restore CS1591 } } \ No newline at end of file diff --git a/Archipelago.MultiClient.Net/Enums/Permissions.cs b/Archipelago.MultiClient.Net/Enums/Permissions.cs index 9511df5..7d79c73 100644 --- a/Archipelago.MultiClient.Net/Enums/Permissions.cs +++ b/Archipelago.MultiClient.Net/Enums/Permissions.cs @@ -4,13 +4,32 @@ namespace Archipelago.MultiClient.Net.Enums { + /// + /// Enum flags that describe the permissions of the currently connected player + /// [JsonConverter(typeof(PermissionsEnumConverter))] [Flags] public enum Permissions { + /// + /// Permission is not granted + /// Disabled = 0, - Enabled = 1 << 0, - Goal = 1 << 1, - Auto = 1 << 2 | Goal, - } + /// + /// Permission is granted to manually execute this + /// + Enabled = 1 << 0, + /// + /// Permission is granted to manually execute this after your goal is completed + /// + Goal = 1 << 1, + /// + /// Will automaticly execute this after your goal is completed, No permission is not granted for manually executing this + /// + Auto = 1 << 2 | Goal, + /// + /// Will automaticly execute this after your goal is completed, Permission is granted to manually execute this + /// + AutoEnabled = Auto | Enabled, + } } diff --git a/Archipelago.MultiClient.Net/Enums/SlotType.cs b/Archipelago.MultiClient.Net/Enums/SlotType.cs index 3610681..44639a8 100644 --- a/Archipelago.MultiClient.Net/Enums/SlotType.cs +++ b/Archipelago.MultiClient.Net/Enums/SlotType.cs @@ -1,12 +1,25 @@ using System; namespace Archipelago.MultiClient.Net.Enums -{ - [Flags] - public enum SlotType +{ + + /// + /// The types of slots that can be present in a game. + /// + [Flags] + public enum SlotType { + /// + /// A spectator slot. (wont be allowed to send hints or items) + /// Spectator = 0, - Player = 1 << 0, - Group = 1 << 1 + /// + /// A normal player slot + /// + Player = 1 << 0, + /// + /// A slot that represents a group of players, used for itemlinks + /// + Group = 1 << 1 } } \ No newline at end of file diff --git a/Archipelago.MultiClient.Net/Exceptions/AggregateException.cs b/Archipelago.MultiClient.Net/Exceptions/AggregateException.cs new file mode 100644 index 0000000..aea3e6f --- /dev/null +++ b/Archipelago.MultiClient.Net/Exceptions/AggregateException.cs @@ -0,0 +1,20 @@ +#if NET35 + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Archipelago.MultiClient.Net.Exceptions +{ + public class AggregateException : Exception + { + public List Exceptions { get; } = new List(); + + public AggregateException(IEnumerable exceptions) : base($"AggregateException: {exceptions.Count()} exceptions occurred") + { + Exceptions.AddRange(exceptions); + } + } +} + +#endif diff --git a/Archipelago.MultiClient.Net/Helpers/ArchipelagoSocketHelper_system.net.websockets.cs b/Archipelago.MultiClient.Net/Helpers/ArchipelagoSocketHelper_system.net.websockets.cs index bf45811..cae5260 100644 --- a/Archipelago.MultiClient.Net/Helpers/ArchipelagoSocketHelper_system.net.websockets.cs +++ b/Archipelago.MultiClient.Net/Helpers/ArchipelagoSocketHelper_system.net.websockets.cs @@ -76,24 +76,47 @@ public async Task ConnectAsync() } async Task ConnectToProvidedUri(Uri uri) - { - if (uri.Scheme != "unspecified") - await webSocket.ConnectAsync(uri, CancellationToken.None); + { + if (uri.Scheme != "unspecified") + { + try + { + await webSocket.ConnectAsync(uri, CancellationToken.None); + } + catch (Exception e) + { + OnError(e); + throw; + } + } else { + var errors = new List(0); try { - await ConnectToProvidedUri(uri.AsWss()); + await webSocket.ConnectAsync(uri.AsWss(), CancellationToken.None); if (webSocket.State == WebSocketState.Open) return; } - catch + catch(Exception e) { + errors.Add(e); webSocket = new ClientWebSocket(); } - await ConnectToProvidedUri(uri.AsWs()); + try + { + await webSocket.ConnectAsync(uri.AsWs(), CancellationToken.None); + } + catch (Exception e) + { + errors.Add(e); + + OnError(new AggregateException(errors)); + + throw; + } } } diff --git a/Archipelago.MultiClient.Net/Helpers/ArchipelagoSocketHelper_websocket-sharp.cs b/Archipelago.MultiClient.Net/Helpers/ArchipelagoSocketHelper_websocket-sharp.cs index 298aa71..1b40e03 100644 --- a/Archipelago.MultiClient.Net/Helpers/ArchipelagoSocketHelper_websocket-sharp.cs +++ b/Archipelago.MultiClient.Net/Helpers/ArchipelagoSocketHelper_websocket-sharp.cs @@ -70,32 +70,62 @@ WebSocket CreateWebSocket(Uri uri) /// /// Initiates a connection to the host. /// - public void Connect() - { - ConnectToProvidedUri(Uri); - } + public void Connect() => ConnectToProvidedUri(Uri); void ConnectToProvidedUri(Uri uri) { if (uri.Scheme != "unspecified") { - webSocket = CreateWebSocket(uri); - webSocket.Connect(); + try + { + webSocket = CreateWebSocket(uri); + webSocket.Connect(); + } + catch (Exception e) + { + OnError(e); + } } else { + var errors = new List(); try { - ConnectToProvidedUri(uri.AsWss()); + try + { + ConnectToProvidedUri(uri.AsWss()); + } + catch (Exception e) + { + errors.Add(e); + throw; + } if (webSocket.IsAlive) return; - ConnectToProvidedUri(uri.AsWs()); + try + { + ConnectToProvidedUri(uri.AsWs()); + } + catch (Exception e) + { + errors.Add(e); + throw; + } } catch { - ConnectToProvidedUri(uri.AsWs()); + try + { + ConnectToProvidedUri(uri.AsWs()); + } + catch (Exception e) + { + errors.Add(e); + + OnError(new AggregateException(errors)); + } } } } @@ -383,6 +413,12 @@ void OnError(object sender, ErrorEventArgs e) if (ErrorReceived != null) ErrorReceived(e.Exception, e.Message); } - } + + void OnError(Exception e) + { + if (ErrorReceived != null) + ErrorReceived(e, e.Message); + } + } } #endif \ No newline at end of file diff --git a/Archipelago.MultiClient.Net/Helpers/ConnectionInfoHelper.cs b/Archipelago.MultiClient.Net/Helpers/ConnectionInfoHelper.cs index a35bdbd..39d5235 100644 --- a/Archipelago.MultiClient.Net/Helpers/ConnectionInfoHelper.cs +++ b/Archipelago.MultiClient.Net/Helpers/ConnectionInfoHelper.cs @@ -7,7 +7,7 @@ namespace Archipelago.MultiClient.Net.Helpers /// /// Provides information about your current connection /// - interface IConnectionInfoProvider + public interface IConnectionInfoProvider { /// /// The game you are connected to or an empty string otherwise diff --git a/Archipelago.MultiClient.Net/Helpers/DataStorageHelper.cs b/Archipelago.MultiClient.Net/Helpers/DataStorageHelper.cs index 23cb881..a9167a3 100644 --- a/Archipelago.MultiClient.Net/Helpers/DataStorageHelper.cs +++ b/Archipelago.MultiClient.Net/Helpers/DataStorageHelper.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.Runtime.InteropServices; #if NET35 using System.Threading; @@ -16,11 +17,42 @@ namespace Archipelago.MultiClient.Net.Helpers /// /// Provides access to a server side data storage to share and store values across sessions and players /// - public partial class DataStorageHelper - { - public delegate void DataStorageUpdatedHandler(JToken originalValue, JToken newValue); + public interface IDataStorageHelper : IDataStorageWrapper + { + /// + /// Stored or retrieves a value from the server side data storage. + /// Assignment operations like = or *= will run asynchronously on server only + /// The retrievals are done synchronously however this storage is implemented using deferred execution. + /// + /// The scope of the key + /// The key for which the value should be stored or retrieved + /// A value + DataStorageElement this[Scope scope, string key] { get; set; } + + /// + /// Stored or retrieves a value from the server side data storage. + /// Assignment operations like = or *= will run asynchronously on server only + /// The retrievals are done synchronously however this storage is implemented using deferred execution. + /// + /// The key under which the value should be stored or retrieved + /// A value + DataStorageElement this[string key] { get; set; } + } - readonly Dictionary onValueChangedEventHandlers = new Dictionary(); + /// + /// Provides access to a server side data storage to share and store values across sessions and players + /// + public partial class DataStorageHelper : IDataStorageHelper + { + /// + /// Delegate for the callback that is called when a value in the data storage is updated + /// + /// The original value before the update + /// the current value + /// the additional arguments passed to the set operation + public delegate void DataStorageUpdatedHandler(JToken originalValue, JToken newValue, Dictionary additionalArguments); + + readonly Dictionary onValueChangedEventHandlers = new Dictionary(); readonly Dictionary operationSpecificCallbacks = new Dictionary(); #if NET35 readonly Dictionary> asyncRetrievalCallbacks = new Dictionary>(); @@ -64,41 +96,30 @@ void OnPacketReceived(ArchipelagoPacketBase packet) } break; case SetReplyPacket setReplyPacket: - if (setReplyPacket.Reference.HasValue - && operationSpecificCallbacks.TryGetValue(setReplyPacket.Reference.Value, out var operationCallback)) + if (setReplyPacket.AdditionalArguments != null + && setReplyPacket.AdditionalArguments.ContainsKey("Reference") + && setReplyPacket.AdditionalArguments["Reference"].Type == JTokenType.Guid + && operationSpecificCallbacks.TryGetValue((Guid)setReplyPacket.AdditionalArguments["Reference"], out var operationCallback)) { - operationCallback(setReplyPacket.OriginalValue, setReplyPacket.Value); + operationCallback(setReplyPacket.OriginalValue, setReplyPacket.Value, setReplyPacket.AdditionalArguments); - operationSpecificCallbacks.Remove(setReplyPacket.Reference.Value); + operationSpecificCallbacks.Remove((Guid)setReplyPacket.AdditionalArguments["Reference"]); } if (onValueChangedEventHandlers.TryGetValue(setReplyPacket.Key, out var handler)) - handler(setReplyPacket.OriginalValue, setReplyPacket.Value); + handler(setReplyPacket.OriginalValue, setReplyPacket.Value, setReplyPacket.AdditionalArguments); break; } } - /// - /// Stored or retrieves a value from the server side data storage. - /// Assignment operations like = or *= will run asynchronously on server only - /// The retrievals are done synchronously however this storage is implemented using deferred execution. - /// - /// The scope of the key - /// The key for which the value should be stored or retrieved - /// A value + /// public DataStorageElement this[Scope scope, string key] { get => this[AddScope(scope, key)]; set => this[AddScope(scope, key)] = value; } - /// - /// Stored or retrieves a value from the server side data storage. - /// Assignment operations like = or *= will run asynchronously on server only - /// The retrievals are done synchronously however this storage is implemented using deferred execution. - /// - /// The key under which the value should be stored or retrieved - /// A value - public DataStorageElement this[string key] + /// + public DataStorageElement this[string key] { get => new DataStorageElement(GetContextForKey(key)); set => SetValue(key, value); @@ -199,18 +220,22 @@ void SetValue(string key, DataStorageElement e) }); } + var additionalArguments = e.AdditionalArguments ?? new Dictionary(0); + if (e.Callbacks != null) { var guid = Guid.NewGuid(); operationSpecificCallbacks[guid] = e.Callbacks; - socket.SendPacketAsync(new SetPacket + additionalArguments["Reference"] = JToken.FromObject(guid); + + socket.SendPacketAsync(new SetPacket { Key = key, Operations = e.Operations.ToArray(), WantReply = true, - Reference = guid + AdditionalArguments = additionalArguments }); } else @@ -218,8 +243,9 @@ void SetValue(string key, DataStorageElement e) socket.SendPacketAsync(new SetPacket { Key = key, - Operations = e.Operations.ToArray() - }); + Operations = e.Operations.ToArray(), + AdditionalArguments = additionalArguments + }); } } diff --git a/Archipelago.MultiClient.Net/Helpers/DataStorageWrappers.cs b/Archipelago.MultiClient.Net/Helpers/DataStorageWrappers.cs index f154c36..edff89c 100644 --- a/Archipelago.MultiClient.Net/Helpers/DataStorageWrappers.cs +++ b/Archipelago.MultiClient.Net/Helpers/DataStorageWrappers.cs @@ -10,23 +10,19 @@ namespace Archipelago.MultiClient.Net.Helpers { - public partial class DataStorageHelper + /// + /// Methods for working with read-only data storage sets + /// + public interface IDataStorageWrapper { - DataStorageElement GetHintsElement(int? slot = null, int? team = null) => - this[Scope.ReadOnly, $"hints_{team ?? connectionInfoProvider.Team}_{slot ?? connectionInfoProvider.Slot}"]; - DataStorageElement GetSlotDataElement(int? slot = null) => - this[Scope.ReadOnly, $"slot_data_{slot ?? connectionInfoProvider.Slot}"]; - DataStorageElement GetItemNameGroupsElement(string game = null) => - this[Scope.ReadOnly, $"item_name_groups_{game ?? connectionInfoProvider.Game}"]; - /// /// Retrieves all unlocked hints for the specified player slot and team /// /// the slot id of the player to request hints for, defaults to the current player's slot if left empty /// the team id of the player to request hints for, defaults to the current player's team if left empty /// An array of unlocked hints or null if the slot or team does not exist - public Hint[] GetHints(int? slot = null, int? team = null) => - GetHintsElement(slot, team).To(); + Hint[] GetHints(int? slot = null, int? team = null); + #if NET35 /// /// Retrieves all unlocked hints for the specified player slot and team @@ -35,8 +31,7 @@ public Hint[] GetHints(int? slot = null, int? team = null) => /// the slot id of the player to request hints for, defaults to the current player's slot if left empty /// the team id of the player to request hints for, defaults to the current player's team if left empty /// An array of unlocked hints or null if the slot or team does not exist - public void GetHintsAsync(Action onHintsRetrieved, int? slot = null, int? team = null) => - GetHintsElement(slot, team).GetAsync(t => onHintsRetrieved(t?.ToObject())); + void GetHintsAsync(Action onHintsRetrieved, int? slot = null, int? team = null); #else /// /// Retrieves all unlocked hints for the specified player slot and team @@ -44,8 +39,7 @@ public void GetHintsAsync(Action onHintsRetrieved, int? slot = null, int /// the slot id of the player to request hints for, defaults to the current player's slot if left empty /// the team id of the player to request hints for, defaults to the current player's team if left empty /// An array of unlocked hints or null if the slot or team does not exist - public Task GetHintsAsync(int? slot = null, int? team = null) => - GetHintsElement(slot, team).GetAsync(); + Task GetHintsAsync(int? slot = null, int? team = null); #endif /// @@ -55,27 +49,16 @@ public Task GetHintsAsync(int? slot = null, int? team = null) => /// should the currently unlocked hints be retrieved or just the updates /// the slot id of the player to request hints for, defaults to the current player's slot if left empty /// the team id of the player to request hints for, defaults to the current player's team if left empty - public void TrackHints(Action onHintsUpdated, - bool retrieveCurrentlyUnlockedHints = true, int? slot = null, int? team = null) - { - GetHintsElement(slot, team).OnValueChanged += (_, newValue) => onHintsUpdated(newValue.ToObject()); - - if (retrieveCurrentlyUnlockedHints) -#if NET35 - GetHintsAsync(onHintsUpdated, slot, team); -#else - GetHintsAsync(slot, team).ContinueWith(t => onHintsUpdated(t.Result)); -#endif - } - + void TrackHints(Action onHintsUpdated, + bool retrieveCurrentlyUnlockedHints = true, int? slot = null, int? team = null); /// /// Retrieves the custom slot data for the specified slot /// /// the slot id of the player to request slot data for, defaults to the current player's slot if left empty /// An Dictionary with string keys, and custom defined values, the keys and values differ per game - public Dictionary GetSlotData(int? slot = null) => - GetSlotDataElement(slot).To>(); + Dictionary GetSlotData(int? slot = null); + #if NET35 /// /// Retrieves the custom slot data for the specified slot @@ -83,16 +66,14 @@ public Dictionary GetSlotData(int? slot = null) => /// the method to call with the retrieved slot data /// the slot id of the player to request slot data for, defaults to the current player's slot if left empty /// An Dictionary with string keys, and custom defined values, the keys and values differ per game - public void GetSlotDataAsync(Action> onSlotDataRetrieved, int? slot = null) => - GetSlotDataElement(slot).GetAsync(t => onSlotDataRetrieved(t?.ToObject>())); + void GetSlotDataAsync(Action> onSlotDataRetrieved, int? slot = null); #else /// /// Retrieves the custom slot data for the specified slot /// /// the slot id of the player to request slot data for, defaults to the current player's slot if left empty /// An Dictionary with string keys, and custom defined values, the keys and values differ per game - public Task> GetSlotDataAsync(int? slot = null) => - GetSlotDataElement(slot).GetAsync>(); + Task> GetSlotDataAsync(int? slot = null); #endif /// @@ -100,8 +81,7 @@ public Task> GetSlotDataAsync(int? slot = null) => /// /// the game name to request item name groups for, defaults to the current player's game if left empty /// An Dictionary with item group names for keys and an array of item names as value - public Dictionary GetItemNameGroups(string game = null) => - GetItemNameGroupsElement(game).To>(); + Dictionary GetItemNameGroups(string game = null); #if NET35 /// @@ -110,16 +90,185 @@ public Dictionary GetItemNameGroups(string game = null) => /// the method to call with the retrieved item name groups /// the game name to request item name groups for, defaults to the current player's game if left empty /// An Dictionary with item group names for keys and an array of item names as value - public void GetItemNameGroupsAsync(Action> onItemNameGroupsRetrieved, string game = null) => - GetItemNameGroupsElement(game).GetAsync(t => onItemNameGroupsRetrieved(t?.ToObject>())); + void GetItemNameGroupsAsync(Action> onItemNameGroupsRetrieved, string game = null); #else /// /// Retrieves the defined item name groups for the specified game /// /// the game name to request item name groups for, defaults to the current player's game if left empty /// An Dictionary with item group names for keys and an array of item names as value + Task> GetItemNameGroupsAsync(string game = null); +#endif + + /// + /// Retrieves the defined location name groups for the specified game + /// + /// the game name to request location name groups for, defaults to the current player's game if left empty + /// An Dictionary with location group names for keys and an array of location names as value + Dictionary GetLocationNameGroups(string game = null); + +#if NET35 + /// + /// Retrieves the defined location name groups for the specified game + /// + /// the method to call with the retrieved location name groups + /// the game name to request location name groups for, defaults to the current player's game if left empty + /// An Dictionary with location group names for keys and an array of location names as value + void GetLocationNameGroupsAsync(Action> onLocationNameGroupsRetrieved, string game = null); +#else + /// + /// Retrieves the defined location name groups for the specified game + /// + /// the game name to request location name groups for, defaults to the current player's game if left empty + /// An Dictionary with location group names for keys and an array of location names as value + Task> GetLocationNameGroupsAsync(string game = null); +#endif + + /// + /// Retrieves the client status for the specified player slot and team + /// + /// the slot id of the player to request the status for, defaults to the current player's slot if left empty + /// the team id of the player to request the status for, defaults to the current player's team if left empty + /// The status of the client or null if the slot or team does not exist + ArchipelagoClientState GetClientStatus(int? slot = null, int? team = null); + +#if NET35 + /// + /// Retrieves the client status for the specified player slot and team + /// + /// the method to call with the retrieved client status + /// the slot id of the player to request the status for, defaults to the current player's slot if left empty + /// the team id of the player to request the status for, defaults to the current player's team if left empty + /// The status of the client or null if the slot or team does not exist + void GetClientStatusAsync(Action onStatusRetrieved, int? slot = null, int? team = null); +#else + /// + /// Retrieves the client status for the specified player slot and team + /// + /// the slot id of the player to request the status for, defaults to the current player's slot if left empty + /// the team id of the player to request the status for, defaults to the current player's team if left empty + /// The status of the client or null if the slot or team does not exist + Task GetClientStatusAsync(int? slot = null, int? team = null); +#endif + + /// + /// Sets a callback to be called with all updates to the client status for the specified player slot and team + /// + /// the method to call with the updated hints + /// should the current status be retrieved or just the updates + /// the slot id of the player to request the status for, defaults to the current player's slot if left empty + /// the team id of the player to request the status for, defaults to the current player's team if left empty + void TrackClientStatus(Action onStatusUpdated, + bool retrieveCurrentClientStatus = true, int? slot = null, int? team = null); + } + + public partial class DataStorageHelper : IDataStorageWrapper + { + DataStorageElement GetHintsElement(int? slot = null, int? team = null) => + this[Scope.ReadOnly, $"hints_{team ?? connectionInfoProvider.Team}_{slot ?? connectionInfoProvider.Slot}"]; + DataStorageElement GetSlotDataElement(int? slot = null) => + this[Scope.ReadOnly, $"slot_data_{slot ?? connectionInfoProvider.Slot}"]; + DataStorageElement GetItemNameGroupsElement(string game = null) => + this[Scope.ReadOnly, $"item_name_groups_{game ?? connectionInfoProvider.Game}"]; + DataStorageElement GetLocationNameGroupsElement(string game = null) => + this[Scope.ReadOnly, $"location_name_groups_{game ?? connectionInfoProvider.Game}"]; + DataStorageElement GetClientStatusElement(int? slot = null, int? team = null) => + this[Scope.ReadOnly, $"client_status_{team ?? connectionInfoProvider.Team}_{slot ?? connectionInfoProvider.Slot}"]; + + /// + public Hint[] GetHints(int? slot = null, int? team = null) => + GetHintsElement(slot, team).To(); +#if NET35 + /// + public void GetHintsAsync(Action onHintsRetrieved, int? slot = null, int? team = null) => + GetHintsElement(slot, team).GetAsync(t => onHintsRetrieved(t?.ToObject())); +#else + /// + public Task GetHintsAsync(int? slot = null, int? team = null) => + GetHintsElement(slot, team).GetAsync(); +#endif + + /// + public void TrackHints(Action onHintsUpdated, + bool retrieveCurrentlyUnlockedHints = true, int? slot = null, int? team = null) + { + GetHintsElement(slot, team).OnValueChanged += (_, newValue, x) => onHintsUpdated(newValue.ToObject()); + + if (retrieveCurrentlyUnlockedHints) +#if NET35 + GetHintsAsync(onHintsUpdated, slot, team); +#else + GetHintsAsync(slot, team).ContinueWith(t => onHintsUpdated(t.Result)); +#endif + } + + /// + public Dictionary GetSlotData(int? slot = null) => + GetSlotDataElement(slot).To>(); +#if NET35 + /// + public void GetSlotDataAsync(Action> onSlotDataRetrieved, int? slot = null) => + GetSlotDataElement(slot).GetAsync(t => onSlotDataRetrieved(t?.ToObject>())); +#else + /// + public Task> GetSlotDataAsync(int? slot = null) => + GetSlotDataElement(slot).GetAsync>(); +#endif + + /// + public Dictionary GetItemNameGroups(string game = null) => + GetItemNameGroupsElement(game).To>(); + +#if NET35 + /// + public void GetItemNameGroupsAsync(Action> onItemNameGroupsRetrieved, string game = null) => + GetItemNameGroupsElement(game).GetAsync(t => onItemNameGroupsRetrieved(t?.ToObject>())); +#else + /// public Task> GetItemNameGroupsAsync(string game = null) => GetItemNameGroupsElement(game).GetAsync>(); #endif + + /// + public Dictionary GetLocationNameGroups(string game = null) => + GetLocationNameGroupsElement(game).To>(); + +#if NET35 + /// + public void GetLocationNameGroupsAsync(Action> onLocationNameGroupsRetrieved, string game = null) => + GetLocationNameGroupsElement(game).GetAsync(t => onLocationNameGroupsRetrieved(t?.ToObject>())); +#else + /// + public Task> GetLocationNameGroupsAsync(string game = null) => + GetLocationNameGroupsElement(game).GetAsync>(); +#endif + /// + public ArchipelagoClientState GetClientStatus(int? slot = null, int? team = null) => + GetClientStatusElement(slot, team).To() ?? ArchipelagoClientState.ClientUnknown; +#if NET35 + /// + public void GetClientStatusAsync(Action onStatusRetrieved, int? slot = null, int? team = null) => + GetClientStatusElement(slot, team).GetAsync(t => onStatusRetrieved(t.ToObject() ?? ArchipelagoClientState.ClientUnknown)); +#else + /// + public Task GetClientStatusAsync(int? slot = null, int? team = null) => + GetClientStatusElement(slot, team) + .GetAsync() + .ContinueWith(r => r.Result ?? ArchipelagoClientState.ClientUnknown); +#endif + + /// + public void TrackClientStatus(Action onStatusUpdated, + bool retrieveCurrentClientStatus = true, int? slot = null, int? team = null) + { + GetClientStatusElement(slot, team).OnValueChanged += (_, newValue, x) => onStatusUpdated(newValue.ToObject()); + + if (retrieveCurrentClientStatus) +#if NET35 + GetClientStatusAsync(onStatusUpdated, slot, team); +#else + GetClientStatusAsync(slot, team).ContinueWith(t => onStatusUpdated(t.Result)); +#endif + } } } diff --git a/Archipelago.MultiClient.Net/Helpers/IArchipelagoSocketHelper.cs b/Archipelago.MultiClient.Net/Helpers/IArchipelagoSocketHelper.cs index 40d1961..b9a3ac9 100644 --- a/Archipelago.MultiClient.Net/Helpers/IArchipelagoSocketHelper.cs +++ b/Archipelago.MultiClient.Net/Helpers/IArchipelagoSocketHelper.cs @@ -7,6 +7,9 @@ namespace Archipelago.MultiClient.Net.Helpers { + /// + /// Delegates for the events that the IArchipelagoSocketHelper can raise + /// public class ArchipelagoSocketHelperDelagates { public delegate void PacketReceivedHandler(ArchipelagoPacketBase packet); diff --git a/Archipelago.MultiClient.Net/Helpers/LocationCheckHelper.cs b/Archipelago.MultiClient.Net/Helpers/LocationCheckHelper.cs index 633a31f..499fda9 100644 --- a/Archipelago.MultiClient.Net/Helpers/LocationCheckHelper.cs +++ b/Archipelago.MultiClient.Net/Helpers/LocationCheckHelper.cs @@ -1,6 +1,7 @@ -using Archipelago.MultiClient.Net.Cache; -using Archipelago.MultiClient.Net.ConcurrentCollection; +using Archipelago.MultiClient.Net.ConcurrentCollection; +using Archipelago.MultiClient.Net.DataPackage; using Archipelago.MultiClient.Net.Enums; +using Archipelago.MultiClient.Net.Models; using Archipelago.MultiClient.Net.Packets; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -13,6 +14,7 @@ using Archipelago.MultiClient.Net.Exceptions; #endif +// ReSharper disable ArrangeObjectCreationWhenTypeEvident namespace Archipelago.MultiClient.Net.Helpers { /// @@ -20,27 +22,225 @@ namespace Archipelago.MultiClient.Net.Helpers /// public interface ILocationCheckHelper { + /// + /// All locations of the slot that's is connected to, both already checked and unchecked + /// + ReadOnlyCollection AllLocations { get; } + /// + /// All checked locations of the slot that's is connected to + /// + ReadOnlyCollection AllLocationsChecked { get; } + /// + /// All unchecked locations of the slot that's is connected to + /// + ReadOnlyCollection AllMissingLocations { get; } + + /// + /// event fired when new locations are checked, this can be because a location was checked remotely checked due to an !collect or locations you checked in an earlier session + /// + event LocationCheckHelper.CheckedLocationsUpdatedHandler CheckedLocationsUpdated; + + /// + /// Submit the provided location ids as checked locations. + /// + /// + /// Location ids which have been checked. + /// + /// + /// The websocket connection is not alive. + /// void CompleteLocationChecks(params long[] ids); #if NET35 - void CompleteLocationChecksAsync(Action onComplete, params long[] ids); + /// + /// Submit the provided location ids as checked locations. + /// + /// + /// Action to be called when the async send is completed. + /// + /// + /// Location ids which have been checked. + /// + /// + /// The websocket connection is not alive. + /// + void CompleteLocationChecksAsync(Action onComplete, params long[] ids); #else - Task CompleteLocationChecksAsync(params long[] ids); + /// + /// Submit the provided location ids as checked locations. + /// + /// + /// Location ids which have been checked. + /// + /// + /// The websocket connection is not alive. + /// + Task CompleteLocationChecksAsync(params long[] ids); #endif - long GetLocationIdFromName(string game, string locationName); - string GetLocationNameFromId(long locationId); +#if NET35 + /// + /// Ask the server for the items which are present in the provided location ids. + /// + /// + /// An action to run once the server responds to the scout packet. + /// If the argument to the action is null then the server responded with an InvalidPacket response. + /// + /// + /// Specified if and how an official hint should be created on the server. + /// + /// + /// The locations ids which are to be scouted. + /// + /// + /// Repeated calls of this method before a LocationInfo packet is received will cause the stored + /// callback to be overwritten with the most recent call. It is recommended you chain calls to this method + /// within the callbacks themselves or call this only once. + /// + /// + /// The websocket connection is not alive. + /// + void ScoutLocationsAsync(Action> callback = null, + HintCreationPolicy hintCreationPolicy = HintCreationPolicy.None, params long[] ids); - ReadOnlyCollection AllLocations { get; } - ReadOnlyCollection AllLocationsChecked { get; } - ReadOnlyCollection AllMissingLocations { get; } + /// + /// Ask the server for the items which are present in the provided location ids. + /// + /// + /// An action to run once the server responds to the scout packet. + /// If the argument to the action is null then the server responded with an InvalidPacket response. + /// + /// + /// If true, creates a free hint for these locations. + /// + /// + /// The locations ids which are to be scouted. + /// + /// + /// Repeated calls of this method before a LocationInfo packet is received will cause the stored + /// callback to be overwritten with the most recent call. It is recommended you chain calls to this method + /// within the callbacks themselves or call this only once. + /// + /// + /// The websocket connection is not alive. + /// + void ScoutLocationsAsync(Action> callback = null, bool createAsHint = false, params long[] ids); + + /// + /// Ask the server for the items which are present in the provided location ids. + /// + /// + /// An action to run once the server responds to the scout packet. + /// If the argument to the action is null then the server responded with an InvalidPacket response. + /// + /// + /// The locations ids which are to be scouted. + /// + /// + /// Repeated calls of this method before a LocationInfo packet is received will cause the stored + /// callback to be overwritten with the most recent call. It is recommended you chain calls to this method + /// within the callbacks themselves or call this only once. + /// + /// + /// The websocket connection is not alive. + /// + void ScoutLocationsAsync(Action> callback = null, params long[] ids); +#else + /// + /// Ask the server for the items which are present in the provided location ids. + /// + /// + /// Specified if and how an official hint should be created on the server. + /// + /// + /// The locations ids which are to be scouted. + /// + /// + /// Repeated calls of this method before a LocationInfo packet is received will cause the stored + /// callback to be overwritten with the most recent call. It is recommended you chain calls to this method + /// within the callbacks themselves or call this only once. + /// + /// + /// The websocket connection is not alive. + /// + Task> ScoutLocationsAsync(HintCreationPolicy hintCreationPolicy, params long[] ids); + + /// + /// Ask the server for the items which are present in the provided location ids. + /// + /// + /// If true, creates a free hint for these locations. + /// + /// + /// The locations ids which are to be scouted. + /// + /// + /// Repeated calls of this method before a LocationInfo packet is received will cause the stored + /// callback to be overwritten with the most recent call. It is recommended you chain calls to this method + /// within the callbacks themselves or call this only once. + /// + /// + /// The websocket connection is not alive. + /// + Task> ScoutLocationsAsync(bool createAsHint, params long[] ids); + + /// + /// Ask the server for the items which are present in the provided location ids. + /// + /// + /// The locations ids which are to be scouted. + /// + /// + /// Repeated calls of this method before a LocationInfo packet is received will cause the stored + /// callback to be overwritten with the most recent call. It is recommended you chain calls to this method + /// within the callbacks themselves or call this only once. + /// + /// + /// The websocket connection is not alive. + /// + Task> ScoutLocationsAsync(params long[] ids); +#endif + + /// + /// Get the Id of a location from its name. Useful when a game knows its locations by name but not by Archipelago Id. + /// + /// + /// The game to look up the locations from + /// + /// + /// The name of the location to check the Id for. Must match the contents of the datapackage. + /// + /// + /// Returns the locationId for the location name that was given or -1 if no location was found. + /// + long GetLocationIdFromName(string game, string locationName); + + /// + /// Get the name of a location from its id. Useful when receiving a packet and it is necessary to find the name of the location. + /// + /// + /// The Id of the location to look up the name for. Must match the contents of the datapackage. + /// + /// + /// The game to lookup the location id for, if null will look in the game the local player is connected to. + /// Negative location ids are always looked up under the Archipelago game + /// + /// + /// Returns the locationName for the provided locationId, or null if no such location is found. + /// + string GetLocationNameFromId(long locationId, string game = null); } /// public class LocationCheckHelper : ILocationCheckHelper { + /// + /// delagate for the CheckedLocationsUpdated event + /// + /// public delegate void CheckedLocationsUpdatedHandler(ReadOnlyCollection newCheckedLocations); - public event CheckedLocationsUpdatedHandler CheckedLocationsUpdated; + /// + public event CheckedLocationsUpdatedHandler CheckedLocationsUpdated; readonly IConcurrentHashSet allLocations = new ConcurrentHashSet(); readonly IConcurrentHashSet locationsChecked = new ConcurrentHashSet(); @@ -48,26 +248,30 @@ public class LocationCheckHelper : ILocationCheckHelper ReadOnlyCollection missingLocations = new ReadOnlyCollection(new long[0]); readonly IArchipelagoSocketHelper socket; - readonly IDataPackageCache cache; + readonly IItemInfoResolver itemInfoResolver; + readonly IConnectionInfoProvider connectionInfoProvider; + readonly IPlayerHelper players; - bool awaitingLocationInfoPacket; + bool awaitingLocationInfoPacket; #if NET35 Action locationInfoPacketCallback; #else - TaskCompletionSource locationInfoPacketCallbackTask; + TaskCompletionSource> locationInfoPacketCallbackTask; #endif - - Dictionary> gameLocationNameToIdMapping; - Dictionary locationIdToNameMapping; - - public ReadOnlyCollection AllLocations => allLocations.AsToReadOnlyCollection(); - public ReadOnlyCollection AllLocationsChecked => locationsChecked.AsToReadOnlyCollection(); - public ReadOnlyCollection AllMissingLocations => missingLocations; - - internal LocationCheckHelper(IArchipelagoSocketHelper socket, IDataPackageCache cache) + /// + public ReadOnlyCollection AllLocations => allLocations.AsToReadOnlyCollection(); + /// + public ReadOnlyCollection AllLocationsChecked => locationsChecked.AsToReadOnlyCollection(); + /// + public ReadOnlyCollection AllMissingLocations => missingLocations; + + internal LocationCheckHelper(IArchipelagoSocketHelper socket, IItemInfoResolver itemInfoResolver, + IConnectionInfoProvider connectionInfoProvider, IPlayerHelper players) { this.socket = socket; - this.cache = cache; + this.itemInfoResolver = itemInfoResolver; + this.connectionInfoProvider = connectionInfoProvider; + this.players = players; socket.PacketReceived += Socket_PacketReceived; } @@ -115,9 +319,17 @@ void Socket_PacketReceived(ArchipelagoPacketBase packet) case LocationInfoPacket locationInfoPacket: if (awaitingLocationInfoPacket) { - if (locationInfoPacketCallbackTask != null) - locationInfoPacketCallbackTask.TrySetResult(locationInfoPacket); - + if (locationInfoPacketCallbackTask != null) + { + var items = locationInfoPacket.Locations.ToDictionary( + item => item.Location, + item => new ScoutedItemInfo(item, (players.GetPlayerInfo(item.Player) ?? new PlayerInfo()).Game, + connectionInfoProvider.Game, itemInfoResolver, + players.GetPlayerInfo(item.Player) ?? new PlayerInfo())); + + locationInfoPacketCallbackTask.TrySetResult(items); + } + awaitingLocationInfoPacket = false; locationInfoPacketCallbackTask = null; } @@ -138,16 +350,8 @@ void Socket_PacketReceived(ArchipelagoPacketBase packet) } } - /// - /// Submit the provided location ids as checked locations. - /// - /// - /// Location ids which have been checked. - /// - /// - /// The websocket connection is not alive. - /// - public void CompleteLocationChecks(params long[] ids) + /// + public void CompleteLocationChecks(params long[] ids) { CheckLocations(ids); @@ -158,19 +362,8 @@ public void CompleteLocationChecks(params long[] ids) } #if NET35 - /// - /// Submit the provided location ids as checked locations. - /// - /// - /// Action to be called when the async send is completed. - /// - /// - /// Location ids which have been checked. - /// - /// - /// The websocket connection is not alive. - /// - public void CompleteLocationChecksAsync(Action onComplete, params long[] ids) + /// + public void CompleteLocationChecksAsync(Action onComplete, params long[] ids) { CheckLocations(ids); @@ -180,17 +373,10 @@ public void CompleteLocationChecksAsync(Action onComplete, params long[] i socket.SendPacketAsync(GetLocationChecksPacket(), onComplete); } #else - /// - /// Submit the provided location ids as checked locations. - /// - /// - /// Location ids which have been checked. - /// - /// - /// The websocket connection is not alive. - /// + /// public Task CompleteLocationChecksAsync(params long[] ids) { + // ReSharper disable once ArrangeMethodOrOperatorBody return Task.Factory.StartNew(() => { CheckLocations(ids); @@ -207,168 +393,75 @@ public Task CompleteLocationChecksAsync(params long[] ids) LocationChecksPacket GetLocationChecksPacket() => new LocationChecksPacket { - Locations = locationsChecked.AsToReadOnlyCollectionExcept(serverConfirmedChecks).ToArray() + Locations = locationsChecked + .AsToReadOnlyCollectionExcept(serverConfirmedChecks) + .ToArray() }; #if NET35 - /// - /// Ask the server for the items which are present in the provided location ids. - /// - /// - /// An action to run once the server responds to the scout packet. - /// If the argument to the action is null then the server responded with an InvalidPacket response. - /// - /// - /// If true, creates a free hint for these locations. - /// - /// - /// The locations ids which are to be scouted. - /// - /// - /// Repeated calls of this method before a LocationInfo packet is received will cause the stored - /// callback to be overwritten with the most recent call. It is recommended you chain calls to this method - /// within the callbacks themselves or call this only once. - /// - /// - /// The websocket connection is not alive. - /// - public void ScoutLocationsAsync(Action callback = null, bool createAsHint = false, params long[] ids) + /// + public void ScoutLocationsAsync(Action> callback = null, + HintCreationPolicy hintCreationPolicy = HintCreationPolicy.None, params long[] ids) { - socket.SendPacketAsync(new LocationScoutsPacket() + socket.SendPacketAsync(new LocationScoutsPacket { Locations = ids, - CreateAsHint = createAsHint - }); + CreateAsHint = (int)hintCreationPolicy + }); awaitingLocationInfoPacket = true; - locationInfoPacketCallback = callback; + locationInfoPacketCallback = (scoutResult) => + { + var items = scoutResult.Locations.ToDictionary( + item => item.Location, + item => new ScoutedItemInfo(item, (players.GetPlayerInfo(item.Player) ?? new PlayerInfo()).Game, + connectionInfoProvider.Game, itemInfoResolver, + players.GetPlayerInfo(item.Player) ?? new PlayerInfo())); + + callback(items); + }; } - /// - /// Ask the server for the items which are present in the provided location ids. - /// - /// - /// An action to run once the server responds to the scout packet. - /// If the argument to the action is null then the server responded with an InvalidPacket response. - /// - /// - /// The locations ids which are to be scouted. - /// - /// - /// Repeated calls of this method before a LocationInfo packet is received will cause the stored - /// callback to be overwritten with the most recent call. It is recommended you chain calls to this method - /// within the callbacks themselves or call this only once. - /// - /// - /// The websocket connection is not alive. - /// - public void ScoutLocationsAsync(Action callback = null, params long[] ids) => + /// + public void ScoutLocationsAsync(Action> callback = null, bool createAsHint = false, params long[] ids) => + // Maintain backwards compatibility if createAsHint parameter is not specified. + ScoutLocationsAsync(callback, createAsHint ? HintCreationPolicy.CreateAndAnnounce : HintCreationPolicy.None, ids); + + /// + public void ScoutLocationsAsync(Action> callback = null, params long[] ids) => // Maintain backwards compatibility if createAsHint parameter is not specified. ScoutLocationsAsync(callback, false, ids); #else - /// - /// Ask the server for the items which are present in the provided location ids. - /// - /// - /// If true, creates a free hint for these locations. - /// - /// - /// The locations ids which are to be scouted. - /// - /// - /// Repeated calls of this method before a LocationInfo packet is received will cause the stored - /// callback to be overwritten with the most recent call. It is recommended you chain calls to this method - /// within the callbacks themselves or call this only once. - /// - /// - /// The websocket connection is not alive. - /// - public Task ScoutLocationsAsync(bool createAsHint, params long[] ids) + /// + public Task> ScoutLocationsAsync(HintCreationPolicy hintCreationPolicy, params long[] ids) { - locationInfoPacketCallbackTask = new TaskCompletionSource(); + locationInfoPacketCallbackTask = new TaskCompletionSource>(); awaitingLocationInfoPacket = true; - socket.SendPacket(new LocationScoutsPacket() + socket.SendPacket(new LocationScoutsPacket { Locations = ids, - CreateAsHint = createAsHint - }); + CreateAsHint = (int)hintCreationPolicy + }); return locationInfoPacketCallbackTask.Task; } - /// - /// Ask the server for the items which are present in the provided location ids. - /// - /// - /// The locations ids which are to be scouted. - /// - /// - /// Repeated calls of this method before a LocationInfo packet is received will cause the stored - /// callback to be overwritten with the most recent call. It is recommended you chain calls to this method - /// within the callbacks themselves or call this only once. - /// - /// - /// The websocket connection is not alive. - /// - public Task ScoutLocationsAsync(params long[] ids) => + /// + public Task> ScoutLocationsAsync(bool createAsHint, params long[] ids) => + ScoutLocationsAsync(createAsHint ? HintCreationPolicy.CreateAndAnnounce : HintCreationPolicy.None, ids); + + /// + public Task> ScoutLocationsAsync(params long[] ids) => ScoutLocationsAsync(false, ids); // Maintain backwards compatibility if createAsHint parameter is not specified. #endif - /// - /// Get the Id of a location from its name. Useful when a game knows its locations by name but not by Archipelago Id. - /// - /// - /// The game to look up the locations from - /// - /// - /// The name of the location to check the Id for. Must match the contents of the datapackage. - /// - /// - /// Returns the locationId for the location name that was given or -1 if no location was found. - /// - public long GetLocationIdFromName(string game, string locationName) - { - if (!cache.TryGetDataPackageFromCache(out var dataPackage)) - return -1; - - if (gameLocationNameToIdMapping == null) - gameLocationNameToIdMapping = dataPackage.Games.ToDictionary( - x => x.Key, x => x.Value.LocationLookup.ToDictionary( - y => y.Key, y => y.Value)); - - return gameLocationNameToIdMapping.TryGetValue(game, out var locationNameToIdLookup) - ? locationNameToIdLookup.TryGetValue(locationName, out var locationId) - ? locationId - : -1 - : -1; - } + /// + public long GetLocationIdFromName(string game, string locationName) => itemInfoResolver.GetLocationId(locationName, game); - /// - /// Get the name of a location from its id. Useful when receiving a packet and it is necessary to find the name of the location. - /// - /// - /// The Id of the location to look up the name for. Must match the contents of the datapackage. - /// - /// - /// Returns the locationName for the provided locationId, or null if no such location is found. - /// - public string GetLocationNameFromId(long locationId) - { - if (!cache.TryGetDataPackageFromCache(out var dataPackage)) - return null; - - if (locationIdToNameMapping == null) - locationIdToNameMapping = dataPackage.Games - .Select(x => x.Value) - .SelectMany(x => x.LocationLookup) - .ToDictionary(x => x.Value, x => x.Key); - - return locationIdToNameMapping.TryGetValue(locationId, out var locationName) - ? locationName - : null; - } + /// + public string GetLocationNameFromId(long locationId, string game = null) => itemInfoResolver.GetLocationName(locationId, game); - void CheckLocations(ICollection locationIds) + void CheckLocations(ICollection locationIds) { if (locationIds == null || !locationIds.Any()) return; @@ -377,10 +470,11 @@ void CheckLocations(ICollection locationIds) foreach (var locationId in locationIds) { - allLocations.TryAdd(locationId); - - if (locationsChecked.TryAdd(locationId)) - newLocations.Add(locationId); + if (allLocations.Contains(locationId)) + { + if (locationsChecked.TryAdd(locationId)) + newLocations.Add(locationId); + } } missingLocations = allLocations.AsToReadOnlyCollectionExcept(locationsChecked); diff --git a/Archipelago.MultiClient.Net/Helpers/MessageLogHelper.cs b/Archipelago.MultiClient.Net/Helpers/MessageLogHelper.cs index 8f3853f..f1db547 100644 --- a/Archipelago.MultiClient.Net/Helpers/MessageLogHelper.cs +++ b/Archipelago.MultiClient.Net/Helpers/MessageLogHelper.cs @@ -1,4 +1,5 @@ -using Archipelago.MultiClient.Net.Enums; +using Archipelago.MultiClient.Net.DataPackage; +using Archipelago.MultiClient.Net.Enums; using Archipelago.MultiClient.Net.MessageLog.Messages; using Archipelago.MultiClient.Net.MessageLog.Parts; using Archipelago.MultiClient.Net.Models; @@ -11,27 +12,35 @@ namespace Archipelago.MultiClient.Net.Helpers /// /// Allows clients to easily subscribe to incoming messages and helps formulating those messages correctly /// - public class MessageLogHelper - { - /// + public interface IMessageLogHelper + { + /// + /// Triggered for each message that should be presented to the player + /// + event MessageLogHelper.MessageReceivedHandler OnMessageReceived; + } + + /// + public class MessageLogHelper : IMessageLogHelper + { + /// + /// Delegate for the OnMessageReceived event + /// + /// The log message object, this object can be MessageLog.Messages classes, switch this message based on type to get access to finer details public delegate void MessageReceivedHandler(LogMessage message); - /// - /// Triggered for each message that should be presented to the player - /// - public event MessageReceivedHandler OnMessageReceived; + /// + public event MessageReceivedHandler OnMessageReceived; - readonly IReceivedItemsHelper items; - readonly ILocationCheckHelper locations; + readonly IItemInfoResolver itemInfoResolver; readonly IPlayerHelper players; readonly IConnectionInfoProvider connectionInfo; - internal MessageLogHelper(IArchipelagoSocketHelper socket, - IReceivedItemsHelper items, ILocationCheckHelper locations, + internal MessageLogHelper( + IArchipelagoSocketHelper socket, IItemInfoResolver itemInfoResolver, IPlayerHelper players, IConnectionInfoProvider connectionInfo) { - this.items = items; - this.locations = locations; + this.itemInfoResolver = itemInfoResolver; this.players = players; this.connectionInfo = connectionInfo; @@ -58,17 +67,17 @@ void TriggerOnMessageReceived(PrintJsonPacket printJsonPacket) { case ItemPrintJsonPacket itemPrintJson: message = new ItemSendLogMessage(parts, players, connectionInfo, - itemPrintJson.ReceivingPlayer, itemPrintJson.Item.Player, itemPrintJson.Item); + itemPrintJson.ReceivingPlayer, itemPrintJson.Item.Player, itemPrintJson.Item, itemInfoResolver); break; case ItemCheatPrintJsonPacket itemCheatPrintJson: message = new ItemCheatLogMessage(parts, players, connectionInfo, itemCheatPrintJson.Team, itemCheatPrintJson.ReceivingPlayer, - itemCheatPrintJson.Item); + itemCheatPrintJson.Item, itemInfoResolver); break; case HintPrintJsonPacket hintPrintJson: message = new HintItemSendLogMessage(parts, players, connectionInfo, hintPrintJson.ReceivingPlayer, hintPrintJson.Item.Player, - hintPrintJson.Item, hintPrintJson.Found.HasValue && hintPrintJson.Found.Value); + hintPrintJson.Item, hintPrintJson.Found.HasValue && hintPrintJson.Found.Value, itemInfoResolver); break; case JoinPrintJsonPacket joinPrintJson: message = new JoinLogMessage(parts, players, connectionInfo, @@ -285,13 +294,13 @@ MessagePart GetMessagePart(JsonMessagePart part) { case JsonMessagePartType.ItemId: case JsonMessagePartType.ItemName: - return new ItemMessagePart(items, part); + return new ItemMessagePart(players, itemInfoResolver, part); case JsonMessagePartType.PlayerId: case JsonMessagePartType.PlayerName: return new PlayerMessagePart(players, connectionInfo, part); case JsonMessagePartType.LocationId: case JsonMessagePartType.LocationName: - return new LocationMessagePart(locations, part); + return new LocationMessagePart(players, itemInfoResolver, part); case JsonMessagePartType.EntranceName: return new EntranceMessagePart(part); default: diff --git a/Archipelago.MultiClient.Net/Helpers/PlayerHelper.cs b/Archipelago.MultiClient.Net/Helpers/PlayerHelper.cs index ea96e35..4858a41 100644 --- a/Archipelago.MultiClient.Net/Helpers/PlayerHelper.cs +++ b/Archipelago.MultiClient.Net/Helpers/PlayerHelper.cs @@ -49,7 +49,22 @@ public interface IPlayerHelper /// The slot of which to retrieve the alias /// The player's alias and name in the following format of "Alias (Name)", or null if no such player is found string GetPlayerAliasAndName(int slot); - } + + /// + /// Gets the PlayerInfo for the provided team and slot, or null if no such team / slot exists + /// + /// the team to lookup the slot in + /// the slot to lookup + /// the playerinfo of corresponding slot, or null if no such slot exists + PlayerInfo GetPlayerInfo(int team, int slot); + + /// + /// Gets the PlayerInfo for the provided team and slot, or null if no such slot exists + /// + /// the slot to lookup + /// the playerinfo of corresponding slot, or null if no such slot exists + PlayerInfo GetPlayerInfo(int slot); + } /// public class PlayerHelper : IPlayerHelper @@ -73,6 +88,16 @@ public class PlayerHelper : IPlayerHelper /// public IEnumerable AllPlayers => players.SelectMany(kvp => kvp.Value); + /// + public PlayerInfo GetPlayerInfo(int team, int slot) => + players.Count > team && players[team].Count > slot + ? players[team][slot] + : null; + + /// + public PlayerInfo GetPlayerInfo(int slot) => + GetPlayerInfo(connectionInfo.Team, slot); + internal PlayerHelper(IArchipelagoSocketHelper socket, IConnectionInfoProvider connectionInfo) { this.connectionInfo = connectionInfo; @@ -115,7 +140,7 @@ public string GetPlayerAliasAndName(int slot) return $"{playerInfo.Alias} ({playerInfo.Name})"; } - void PacketReceived(ArchipelagoPacketBase packet) + void PacketReceived(ArchipelagoPacketBase packet) { switch (packet) { @@ -236,6 +261,16 @@ public class PlayerInfo /// The slot to check /// public bool IsSharingGroupWith(int team, int slot) => - Team == team && Groups != null && Groups.Any(g => g.GroupMembers.Any(m => m == slot)); - } + Team == team && Slot == slot || (Groups != null && Groups.Any(g => g.GroupMembers.Any(m => m == slot))); + + /// + /// Converts the PlayerInfo to the slot + /// + public static implicit operator int(PlayerInfo p) => p.Slot; + + /// + /// Returns the Alias of the player + /// + public override string ToString() => Alias; + } } \ No newline at end of file diff --git a/Archipelago.MultiClient.Net/Helpers/ReceivedItemsHelper.cs b/Archipelago.MultiClient.Net/Helpers/ReceivedItemsHelper.cs index 9248fa3..6cdbfaf 100644 --- a/Archipelago.MultiClient.Net/Helpers/ReceivedItemsHelper.cs +++ b/Archipelago.MultiClient.Net/Helpers/ReceivedItemsHelper.cs @@ -1,11 +1,9 @@ -using Archipelago.MultiClient.Net.Cache; -using Archipelago.MultiClient.Net.ConcurrentCollection; +using Archipelago.MultiClient.Net.ConcurrentCollection; +using Archipelago.MultiClient.Net.DataPackage; using Archipelago.MultiClient.Net.Enums; using Archipelago.MultiClient.Net.Models; using Archipelago.MultiClient.Net.Packets; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; #if !NET35 using System.Collections.Concurrent; @@ -18,16 +16,66 @@ namespace Archipelago.MultiClient.Net.Helpers /// public interface IReceivedItemsHelper { - /// - /// Perform a lookup using the DataPackage sent as a source of truth to lookup a particular item id for a particular game. - /// - /// - /// Id of the item to lookup. - /// - /// - /// The name of the item as a string, or null if no such item is found. - /// - string GetItemName(long id); + /// + /// Perform a lookup using the DataPackage sent as a source of truth to lookup a particular item id for a particular game. + /// + /// + /// Id of the item to lookup. + /// + /// + /// The game to lookup the item id for, if null will look in the game the local player is connected to. + /// Negative item ids are always looked up under the Archipelago game + /// + /// + /// The name of the item as a string, or null if no such item is found. + /// + string GetItemName(long id, string game = null); + + /// + /// Total number of items received + /// + int Index { get; } + + /// + /// Full list of all items received + /// + ReadOnlyCollection AllItemsReceived { get; } + + /// + /// Event triggered when an item is received. fires once for each item received + /// + event ReceivedItemsHelper.ItemReceivedHandler ItemReceived; + + /// + /// Check whether there are any items in the queue. + /// + /// + /// True if the queue is not empty, otherwise false. + /// + bool Any(); + + /// + /// Peek the next item on the queue to be handled. + /// The item will remain on the queue until dequeued with . + /// + /// + /// The next item to be handled as a , or null if no such item is found. + /// + /// + /// The is empty. + /// + ItemInfo PeekItem(); + + /// + /// Dequeues and returns the next item on the queue to be handled. + /// + /// + /// The next item to be handled as a . + /// + /// + /// The is empty. + /// + ItemInfo DequeueItem(); } /// @@ -35,110 +83,65 @@ public class ReceivedItemsHelper : IReceivedItemsHelper { readonly IArchipelagoSocketHelper socket; readonly ILocationCheckHelper locationsHelper; - readonly IDataPackageCache dataPackageCache; + readonly IItemInfoResolver itemInfoResolver; + readonly IConnectionInfoProvider connectionInfoProvider; + readonly IPlayerHelper playerHelper; - ConcurrentQueue itemQueue; + ConcurrentQueue itemQueue; - readonly IConcurrentList allItemsReceived; + readonly IConcurrentList allItemsReceived; - Dictionary itemLookupCache; - - ReadOnlyCollection cachedReceivedItems; + ReadOnlyCollection cachedReceivedItems; + /// public int Index => cachedReceivedItems.Count; - public ReadOnlyCollection AllItemsReceived => cachedReceivedItems; + /// + public ReadOnlyCollection AllItemsReceived => cachedReceivedItems; - public delegate void ItemReceivedHandler(ReceivedItemsHelper helper); - public event ItemReceivedHandler ItemReceived; + /// + public delegate void ItemReceivedHandler(ReceivedItemsHelper helper); + /// + public event ItemReceivedHandler ItemReceived; - internal ReceivedItemsHelper(IArchipelagoSocketHelper socket, ILocationCheckHelper locationsHelper, IDataPackageCache dataPackageCache) + internal ReceivedItemsHelper( + IArchipelagoSocketHelper socket, ILocationCheckHelper locationsHelper, + IItemInfoResolver itemInfoResolver, IConnectionInfoProvider connectionInfoProvider, + IPlayerHelper playerHelper) { this.socket = socket; this.locationsHelper = locationsHelper; - this.dataPackageCache = dataPackageCache; + this.itemInfoResolver = itemInfoResolver; + this.connectionInfoProvider = connectionInfoProvider; + this.playerHelper = playerHelper; - itemQueue = new ConcurrentQueue(); - allItemsReceived = new ConcurrentList(); - itemLookupCache = new Dictionary(); + itemQueue = new ConcurrentQueue(); + allItemsReceived = new ConcurrentList(); cachedReceivedItems = allItemsReceived.AsReadOnlyCollection(); socket.PacketReceived += Socket_PacketReceived; } - /// - /// Check whether there are any items in the queue. - /// - /// - /// True if the queue is not empty, otherwise false. - /// - public bool Any() => !itemQueue.IsEmpty; - - /// - /// Peek the next item on the queue to be handled. - /// The item will remain on the queue until dequeued with . - /// - /// - /// The next item to be handled as a , or null if no such item is found. - /// - /// - /// The is empty. - /// - public NetworkItem PeekItem() - { - itemQueue.TryPeek(out var item); - return item; - } + /// + public bool Any() => !itemQueue.IsEmpty; - /// - /// Peek the name of next item on the queue to be handled. - /// - /// - /// The name of the item as a string, or null if no such item is found. - /// - /// - /// The is empty. - /// - public string PeekItemName() + /// + public ItemInfo PeekItem() { itemQueue.TryPeek(out var item); - return GetItemName(item.Item); + return item; } - /// - /// Dequeues and returns the next item on the queue to be handled. - /// - /// - /// The next item to be handled as a . - /// - /// - /// The is empty. - /// - public NetworkItem DequeueItem() + /// + public ItemInfo DequeueItem() { itemQueue.TryDequeue(out var item); return item; } /// - public string GetItemName(long id) - { - if (itemLookupCache.TryGetValue(id, out var name)) - return name; - - if (!dataPackageCache.TryGetDataPackageFromCache(out var dataPackage)) - return null; - - itemLookupCache = dataPackage.Games - .Select(x => x.Value) - .SelectMany(x => x.ItemLookup) - .ToDictionary(x => x.Value, x => x.Key); - - return itemLookupCache.TryGetValue(id, out var itemName) - ? itemName - : null; - } + public string GetItemName(long id, string game = null) => itemInfoResolver.GetItemName(id, game); - void Socket_PacketReceived(ArchipelagoPacketBase packet) + void Socket_PacketReceived(ArchipelagoPacketBase packet) { switch (packet.PacketType) { @@ -159,8 +162,11 @@ void Socket_PacketReceived(ArchipelagoPacketBase packet) break; } - foreach (var item in receivedItemsPacket.Items) + foreach (var networkItem in receivedItemsPacket.Items) { + var sender = playerHelper.GetPlayerInfo(networkItem.Player) ?? new PlayerInfo(); + var item = new ItemInfo(networkItem, connectionInfoProvider.Game, sender.Game, itemInfoResolver, sender); + allItemsReceived.Add(item); itemQueue.Enqueue(item); @@ -180,13 +186,16 @@ void PerformResynchronization(ReceivedItemsPacket receivedItemsPacket) #if NET35 itemQueue.Clear(); #else - itemQueue = new ConcurrentQueue(); + itemQueue = new ConcurrentQueue(); #endif - allItemsReceived.Clear(); + allItemsReceived.Clear(); - foreach (var item in receivedItemsPacket.Items) + foreach (var networkItem in receivedItemsPacket.Items) { - itemQueue.Enqueue(item); + var sender = playerHelper.GetPlayerInfo(networkItem.Player) ?? new PlayerInfo(); + var item = new ItemInfo(networkItem, connectionInfoProvider.Game, sender.Game, itemInfoResolver, sender); + + itemQueue.Enqueue(item); allItemsReceived.Add(item); cachedReceivedItems = allItemsReceived.AsReadOnlyCollection(); diff --git a/Archipelago.MultiClient.Net/Helpers/RoomStateHelper.cs b/Archipelago.MultiClient.Net/Helpers/RoomStateHelper.cs index b64245e..d63644a 100644 --- a/Archipelago.MultiClient.Net/Helpers/RoomStateHelper.cs +++ b/Archipelago.MultiClient.Net/Helpers/RoomStateHelper.cs @@ -9,66 +9,116 @@ namespace Archipelago.MultiClient.Net.Helpers /// /// Provides information about the current state of the server /// - public class RoomStateHelper - { - readonly ILocationCheckHelper locationCheckHelper; - - string[] tags; + public interface IRoomStateHelper + { + /// + /// The amount of points it costs to receive a hint from the server. + /// + int HintCost { get; } - /// - /// The amount of points it costs to receive a hint from the server. - /// - public int HintCost { get; private set; } /// /// The percentage of total locations that need to be checked to receive a hint from the server. /// - public int HintCostPercentage { get; private set; } + int HintCostPercentage { get; } + /// /// The amount of hint points you receive per item/location check completed. /// - public int LocationCheckPoints { get; private set; } - /// - /// The client's current hint points. - /// - public int HintPoints { get; private set; } - /// - /// The version of Archipelago which the server is running. - /// - public Version Version { get; private set; } - /// - /// Denoted whether a password is required to join this room. - /// - public bool HasPassword { get; private set; } - /// - /// An enumeration containing the possible release command permission. - /// - public Permissions ReleasePermissions { get; private set; } + int LocationCheckPoints { get; } + + /// + /// The client's current hint points. + /// + int HintPoints { get; } + + /// + /// The version of Archipelago which the server is running. + /// + Version Version { get; } + + /// + /// The version of Archipelago which generated the multiworld. + /// + Version GeneratorVersion { get; } + + /// + /// Denoted whether a password is required to join this room. + /// + bool HasPassword { get; } + + /// + /// An enumeration containing the possible release command permission. + /// + Permissions ReleasePermissions { get; } + /// /// An enumeration containing the possible forfeit command permission. /// Deprecated, use Release Permissions instead /// + Permissions ForfeitPermissions { get; } + + /// + /// An enumeration containing the possible collect command permission. + /// + Permissions CollectPermissions { get; } + + /// + /// An enumeration containing the possible remaining command permission. + /// + Permissions RemainingPermissions { get; } + + /// + /// Uniquely identifying name of this generation + /// + string Seed { get; } + + /// + /// Time stamp of we first connected to the server, and the server send us the RoomInfoPacker. + /// Send for time synchronization if wanted for things like the DeathLink Bounce. + /// + DateTime RoomInfoSendTime { get; } + + /// + /// Denotes special features or capabilities that the server is capable of. + /// + ReadOnlyCollection ServerTags { get; } + } + + /// + public class RoomStateHelper : IRoomStateHelper + { + readonly ILocationCheckHelper locationCheckHelper; + + string[] tags; + + /// + public int HintCost { get; private set; } + /// + public int HintCostPercentage { get; private set; } + /// + public int LocationCheckPoints { get; private set; } + /// + public int HintPoints { get; private set; } + /// + public Version Version { get; private set; } + /// + public Version GeneratorVersion { get; private set; } + /// + public bool HasPassword { get; private set; } + /// + public Permissions ReleasePermissions { get; private set; } + /// public Permissions ForfeitPermissions => ReleasePermissions; - /// - /// An enumeration containing the possible collect command permission. - /// - public Permissions CollectPermissions { get; private set; } - /// - /// An enumeration containing the possible remaining command permission. - /// - public Permissions RemainingPermissions { get; private set; } - /// - /// Uniquely identifying name of this generation - /// - public string Seed { get; private set; } - /// - /// Time stamp of we first connected to the server, and the server send us the RoomInfoPacker. - /// Send for time synchronization if wanted for things like the DeathLink Bounce. - /// - public DateTime RoomInfoSendTime { get; private set; } - /// - /// Denotes special features or capabilities that the server is capable of. - /// - public ReadOnlyCollection ServerTags => + /// + public Permissions CollectPermissions { get; private set; } + /// + public Permissions RemainingPermissions { get; private set; } + /// + public string Seed { get; private set; } + /// + public DateTime RoomInfoSendTime { get; private set; } + /// + public ReadOnlyCollection ServerTags => tags == null ? default : new ReadOnlyCollection(tags); @@ -100,14 +150,17 @@ void OnConnectedPacketReceived(ConnectedPacket packet) { if (packet.HintPoints.HasValue) HintPoints = packet.HintPoints.Value; - } - void OnRoomInfoPacketReceived(RoomInfoPacket packet) + var totalAmountOfLocations = packet.LocationsChecked.Length + packet.MissingChecks.Length; + HintCost = (int)Math.Max(0m, totalAmountOfLocations * 0.01m * HintCostPercentage); + } + + void OnRoomInfoPacketReceived(RoomInfoPacket packet) { HintCostPercentage = packet.HintCostPercentage; - HintCost = (int)Math.Max(0m, locationCheckHelper.AllLocations.Count * 0.01m * packet.HintCostPercentage); LocationCheckPoints = packet.LocationCheckPoints; Version = packet.Version?.ToVersion(); + GeneratorVersion = packet.GeneratorVersion?.ToVersion(); HasPassword = packet.Password; Seed = packet.SeedName; RoomInfoSendTime = UnixTimeConverter.UnixTimeStampToDateTime(packet.Timestamp); diff --git a/Archipelago.MultiClient.Net/MessageLog/Messages/HintItemSendLogMessage.cs b/Archipelago.MultiClient.Net/MessageLog/Messages/HintItemSendLogMessage.cs index 6676040..d05e192 100644 --- a/Archipelago.MultiClient.Net/MessageLog/Messages/HintItemSendLogMessage.cs +++ b/Archipelago.MultiClient.Net/MessageLog/Messages/HintItemSendLogMessage.cs @@ -1,4 +1,5 @@ -using Archipelago.MultiClient.Net.Helpers; +using Archipelago.MultiClient.Net.DataPackage; +using Archipelago.MultiClient.Net.Helpers; using Archipelago.MultiClient.Net.MessageLog.Parts; using Archipelago.MultiClient.Net.Models; @@ -19,8 +20,8 @@ public class HintItemSendLogMessage : ItemSendLogMessage internal HintItemSendLogMessage(MessagePart[] parts, IPlayerHelper players, IConnectionInfoProvider connectionInfo, - int receiver, int sender, NetworkItem item, bool found) - : base(parts, players, connectionInfo, receiver, sender, item) + int receiver, int sender, NetworkItem item, bool found, IItemInfoResolver itemInfoResolver) + : base(parts, players, connectionInfo, receiver, sender, item, itemInfoResolver) { IsFound = found; } diff --git a/Archipelago.MultiClient.Net/MessageLog/Messages/ItemCheatLogMessage.cs b/Archipelago.MultiClient.Net/MessageLog/Messages/ItemCheatLogMessage.cs index fc68ea0..052716f 100644 --- a/Archipelago.MultiClient.Net/MessageLog/Messages/ItemCheatLogMessage.cs +++ b/Archipelago.MultiClient.Net/MessageLog/Messages/ItemCheatLogMessage.cs @@ -1,4 +1,5 @@ -using Archipelago.MultiClient.Net.Helpers; +using Archipelago.MultiClient.Net.DataPackage; +using Archipelago.MultiClient.Net.Helpers; using Archipelago.MultiClient.Net.MessageLog.Parts; using Archipelago.MultiClient.Net.Models; @@ -13,8 +14,8 @@ public class ItemCheatLogMessage : ItemSendLogMessage { internal ItemCheatLogMessage(MessagePart[] parts, IPlayerHelper players, IConnectionInfoProvider connectionInfo, - int team, int slot, NetworkItem item) - : base(parts, players, connectionInfo, slot, 0, item, team) + int team, int slot, NetworkItem item, IItemInfoResolver itemInfoResolver) + : base(parts, players, connectionInfo, slot, 0, item, team, itemInfoResolver) { } } diff --git a/Archipelago.MultiClient.Net/MessageLog/Messages/ItemSendLogMessage.cs b/Archipelago.MultiClient.Net/MessageLog/Messages/ItemSendLogMessage.cs index a1becb6..c1e7c26 100644 --- a/Archipelago.MultiClient.Net/MessageLog/Messages/ItemSendLogMessage.cs +++ b/Archipelago.MultiClient.Net/MessageLog/Messages/ItemSendLogMessage.cs @@ -1,7 +1,7 @@ -using Archipelago.MultiClient.Net.Helpers; +using Archipelago.MultiClient.Net.DataPackage; +using Archipelago.MultiClient.Net.Helpers; using Archipelago.MultiClient.Net.MessageLog.Parts; using Archipelago.MultiClient.Net.Models; -using System; namespace Archipelago.MultiClient.Net.MessageLog.Messages { @@ -10,22 +10,10 @@ namespace Archipelago.MultiClient.Net.MessageLog.Messages /// The `ItemSendLogMessage` is send in response to a client obtaining an item. /// Item send messages contain additional information about the item that was sent for more specific processing. /// - /// `ItemSendLogMessage` also serves as the base class for & + /// `ItemSendLogMessage` also serves as the base class for and /// public class ItemSendLogMessage : LogMessage { - /// - /// The player slot number of the player who received the item - /// - [Obsolete("Use Receiver.Slot instead")] - public int ReceivingPlayerSlot { get; } - - /// - /// The player slot number of the player who sent the item - /// - [Obsolete("Use Sender.Slot instead")] - public int SendingPlayerSlot { get; } - /// /// The player who received the item /// @@ -54,39 +42,32 @@ public class ItemSendLogMessage : LogMessage /// /// The Item that was send /// - public NetworkItem Item { get; } + public ItemInfo Item { get; } internal ItemSendLogMessage(MessagePart[] parts, IPlayerHelper players, IConnectionInfoProvider connectionInfo, - int receiver, int sender, NetworkItem item) - : this(parts, players, connectionInfo, receiver, sender, item, connectionInfo.Team) + int receiver, int sender, NetworkItem item, IItemInfoResolver itemInfoResolver) + : this(parts, players, connectionInfo, receiver, sender, item, connectionInfo.Team, itemInfoResolver) { } internal ItemSendLogMessage(MessagePart[] parts, IPlayerHelper players, IConnectionInfoProvider connectionInfo, - int receiver, int sender, NetworkItem item, int team) : base(parts) + int receiver, int sender, NetworkItem item, int team, + IItemInfoResolver itemInfoResolver) : base(parts) { - var playerList = players.Players; - - ReceivingPlayerSlot = receiver; - SendingPlayerSlot = sender; - IsReceiverTheActivePlayer = connectionInfo.Team == team && connectionInfo.Slot == receiver; IsSenderTheActivePlayer = connectionInfo.Team == team && connectionInfo.Slot == sender; - Receiver = playerList.Count > team && playerList[team].Count > receiver - ? playerList[team][receiver] - : new PlayerInfo(); - Sender = playerList.Count > team && playerList[team].Count > sender - ? playerList[team][sender] - : new PlayerInfo(); - + Receiver = players.GetPlayerInfo(team, receiver) ?? new PlayerInfo(); + Sender = players.GetPlayerInfo(team, sender) ?? new PlayerInfo(); + var itemPlayer = players.GetPlayerInfo(team, item.Player) ?? new PlayerInfo(); + IsRelatedToActivePlayer = IsReceiverTheActivePlayer || IsSenderTheActivePlayer || Receiver.IsSharingGroupWith(connectionInfo.Team, connectionInfo.Slot) || Sender.IsSharingGroupWith(connectionInfo.Team, connectionInfo.Slot); - Item = item; + Item = new ItemInfo(item, Receiver.Game, Sender.Game, itemInfoResolver, itemPlayer); } } } \ No newline at end of file diff --git a/Archipelago.MultiClient.Net/MessageLog/Messages/PlayerSpecificLogMessage.cs b/Archipelago.MultiClient.Net/MessageLog/Messages/PlayerSpecificLogMessage.cs index d61bf66..217d29d 100644 --- a/Archipelago.MultiClient.Net/MessageLog/Messages/PlayerSpecificLogMessage.cs +++ b/Archipelago.MultiClient.Net/MessageLog/Messages/PlayerSpecificLogMessage.cs @@ -7,7 +7,7 @@ namespace Archipelago.MultiClient.Net.MessageLog.Messages /// The `PlayerSpecificLogMessage` is send a base class for LogMessage's that are made in the context of a specific player. /// Item player specific messages contain additional information about the specific player. /// - /// `ItemSendLogMessage` also serves as the base class for & + /// `ItemSendLogMessage` also serves as the base class for and /// public abstract class PlayerSpecificLogMessage : LogMessage { @@ -22,7 +22,7 @@ public abstract class PlayerSpecificLogMessage : LogMessage public bool IsActivePlayer { get; } /// - /// True if the player this message is concerning shares any slot groups (e.g. itemlinks) with the current connected player + /// True if the player this message is concerning any slot groups (e.g. itemlinks) with the current connected player /// public bool IsRelatedToActivePlayer { get; } diff --git a/Archipelago.MultiClient.Net/MessageLog/Parts/ItemMessagePart.cs b/Archipelago.MultiClient.Net/MessageLog/Parts/ItemMessagePart.cs index dbc6dd7..0c20032 100644 --- a/Archipelago.MultiClient.Net/MessageLog/Parts/ItemMessagePart.cs +++ b/Archipelago.MultiClient.Net/MessageLog/Parts/ItemMessagePart.cs @@ -1,4 +1,5 @@ -using Archipelago.MultiClient.Net.Enums; +using Archipelago.MultiClient.Net.DataPackage; +using Archipelago.MultiClient.Net.Enums; using Archipelago.MultiClient.Net.Helpers; using Archipelago.MultiClient.Net.Models; @@ -20,16 +21,24 @@ public class ItemMessagePart : MessagePart /// public long ItemId { get; } - internal ItemMessagePart(IReceivedItemsHelper items, JsonMessagePart part) : base(MessagePartType.Item, part) + /// + /// The player that owns the item (receiver) + /// + public int Player { get; } + + internal ItemMessagePart(IPlayerHelper players, IItemInfoResolver items, JsonMessagePart part) : base(MessagePartType.Item, part) { Flags = part.Flags ?? ItemFlags.None; Color = GetColor(Flags); + Player = part.Player ?? 0; + + var game = (players.GetPlayerInfo(Player) ?? new PlayerInfo()).Game; switch (part.Type) { case JsonMessagePartType.ItemId: ItemId = long.Parse(part.Text); - Text = items.GetItemName(ItemId) ?? $"Item: {ItemId}"; + Text = items.GetItemName(ItemId, game) ?? $"Item: {ItemId}"; break; case JsonMessagePartType.ItemName: ItemId = 0; // we are not going to try to reverse lookup this value based on the game of the receiving player diff --git a/Archipelago.MultiClient.Net/MessageLog/Parts/LocationMessagePart.cs b/Archipelago.MultiClient.Net/MessageLog/Parts/LocationMessagePart.cs index 2f4b5a0..08a83e6 100644 --- a/Archipelago.MultiClient.Net/MessageLog/Parts/LocationMessagePart.cs +++ b/Archipelago.MultiClient.Net/MessageLog/Parts/LocationMessagePart.cs @@ -1,4 +1,5 @@ -using Archipelago.MultiClient.Net.Enums; +using Archipelago.MultiClient.Net.DataPackage; +using Archipelago.MultiClient.Net.Enums; using Archipelago.MultiClient.Net.Helpers; using Archipelago.MultiClient.Net.Models; @@ -15,17 +16,26 @@ public class LocationMessagePart : MessagePart /// public long LocationId { get; } - internal LocationMessagePart(ILocationCheckHelper locations, JsonMessagePart part) + /// + /// The player that owns the location + /// + public int Player { get; } + + internal LocationMessagePart(IPlayerHelper players, IItemInfoResolver itemInfoResolver, JsonMessagePart part) : base(MessagePartType.Location, part, Color.Green) { + Player = part.Player ?? 0; + + var game = (players.GetPlayerInfo(Player) ?? new PlayerInfo()).Game; + switch (part.Type) { case JsonMessagePartType.LocationId: LocationId = long.Parse(part.Text); - Text = locations.GetLocationNameFromId(LocationId) ?? $"Location: {LocationId}"; + Text = itemInfoResolver.GetLocationName(LocationId, game) ?? $"Location: {LocationId}"; break; - case JsonMessagePartType.PlayerName: - LocationId = 0; // we are not going to try to reverse lookup as we don't know the game this location belongs to + case JsonMessagePartType.LocationName: + LocationId = itemInfoResolver.GetLocationId(part.Text, game); Text = part.Text; break; } diff --git a/Archipelago.MultiClient.Net/Models/DataStorageElement.cs b/Archipelago.MultiClient.Net/Models/DataStorageElement.cs index 1738498..86920ec 100644 --- a/Archipelago.MultiClient.Net/Models/DataStorageElement.cs +++ b/Archipelago.MultiClient.Net/Models/DataStorageElement.cs @@ -1,15 +1,19 @@ using Archipelago.MultiClient.Net.Enums; using Archipelago.MultiClient.Net.Helpers; -using Newtonsoft.Json.Linq; using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json.Linq; #if !NET35 using System.Threading.Tasks; +using System.Numerics; +using System.Globalization; #endif +// ReSharper disable ArrangeObjectCreationWhenTypeEvident + namespace Archipelago.MultiClient.Net.Models { /// @@ -26,11 +30,12 @@ public event DataStorageHelper.DataStorageUpdatedHandler OnValueChanged remove => Context.RemoveHandler(Context.Key, value); } - internal DataStorageElementContext Context; + internal DataStorageElementContext Context; internal List Operations = new List(0); internal DataStorageHelper.DataStorageUpdatedHandler Callbacks; + internal Dictionary AdditionalArguments = new Dictionary(0); - JToken cachedValue; + JToken cachedValue; internal DataStorageElement(DataStorageElementContext context) { @@ -38,49 +43,75 @@ internal DataStorageElement(DataStorageElementContext context) } internal DataStorageElement(OperationType operationType, JToken value) { - Operations = new List(1) { + Operations = new List(1) { new OperationSpecification { OperationType = operationType, Value = value } }; } internal DataStorageElement(DataStorageElement source, OperationType operationType, JToken value) : this(source.Context) { Operations = source.Operations.ToList(); - Operations.Add(new OperationSpecification { OperationType = operationType, Value = value }); Callbacks = source.Callbacks; - } - internal DataStorageElement(DataStorageElement source, Callback callback) : this(source.Context) + AdditionalArguments = source.AdditionalArguments; + + Operations.Add(new OperationSpecification { OperationType = operationType, Value = value }); + } + internal DataStorageElement(DataStorageElement source, Callback callback) : this(source.Context) { Operations = source.Operations.ToList(); Callbacks = source.Callbacks; - Callbacks += callback.Method; + AdditionalArguments = source.AdditionalArguments; + + Callbacks += callback.Method; } + internal DataStorageElement(DataStorageElement source, AdditionalArgument additionalArgument) : this(source.Context) + { + Operations = source.Operations.ToList(); + Callbacks = source.Callbacks; + AdditionalArguments = source.AdditionalArguments; + + AdditionalArguments[additionalArgument.Key] = additionalArgument.Value; + } #pragma warning disable CS1591 public static DataStorageElement operator ++(DataStorageElement a) => new DataStorageElement(a, OperationType.Add, 1); public static DataStorageElement operator --(DataStorageElement a) => new DataStorageElement(a, OperationType.Add, -1); - public static DataStorageElement operator +(DataStorageElement a, JToken b) => new DataStorageElement(a, OperationType.Add, b); - public static DataStorageElement operator +(DataStorageElement a, IEnumerable b) => new DataStorageElement(a, OperationType.Add, JArray.FromObject(b)); + public static DataStorageElement operator +(DataStorageElement a, int b) => new DataStorageElement(a, OperationType.Add, b); + public static DataStorageElement operator +(DataStorageElement a, long b) => new DataStorageElement(a, OperationType.Add, b); + public static DataStorageElement operator +(DataStorageElement a, float b) => new DataStorageElement(a, OperationType.Add, b); + public static DataStorageElement operator +(DataStorageElement a, double b) => new DataStorageElement(a, OperationType.Add, b); + public static DataStorageElement operator +(DataStorageElement a, decimal b) => new DataStorageElement(a, OperationType.Add, b); + public static DataStorageElement operator +(DataStorageElement a, string b) => new DataStorageElement(a, OperationType.Add, b); + public static DataStorageElement operator +(DataStorageElement a, JToken b) => new DataStorageElement(a, OperationType.Add, b); + public static DataStorageElement operator +(DataStorageElement a, IEnumerable b) => new DataStorageElement(a, OperationType.Add, JArray.FromObject(b)); public static DataStorageElement operator +(DataStorageElement a, OperationSpecification s) => new DataStorageElement(a, s.OperationType, s.Value); public static DataStorageElement operator +(DataStorageElement a, Callback c) => new DataStorageElement(a, c); - public static DataStorageElement operator *(DataStorageElement a, JToken b) => new DataStorageElement(a, OperationType.Mul, b); - public static DataStorageElement operator %(DataStorageElement a, JToken b) => new DataStorageElement(a, OperationType.Mod, b); - public static DataStorageElement operator ^(DataStorageElement a, JToken b) => new DataStorageElement(a, OperationType.Pow, b); + public static DataStorageElement operator +(DataStorageElement a, AdditionalArgument arg) => new DataStorageElement(a, arg); + public static DataStorageElement operator *(DataStorageElement a, int b) => new DataStorageElement(a, OperationType.Mul, b); + public static DataStorageElement operator *(DataStorageElement a, long b) => new DataStorageElement(a, OperationType.Mul, b); + public static DataStorageElement operator *(DataStorageElement a, float b) => new DataStorageElement(a, OperationType.Mul, b); + public static DataStorageElement operator *(DataStorageElement a, double b) => new DataStorageElement(a, OperationType.Mul, b); + public static DataStorageElement operator *(DataStorageElement a, decimal b) => new DataStorageElement(a, OperationType.Mul, b); + public static DataStorageElement operator %(DataStorageElement a, int b) => new DataStorageElement(a, OperationType.Mod, b); + public static DataStorageElement operator %(DataStorageElement a, long b) => new DataStorageElement(a, OperationType.Mod, b); + public static DataStorageElement operator %(DataStorageElement a, float b) => new DataStorageElement(a, OperationType.Mod, b); + public static DataStorageElement operator %(DataStorageElement a, double b) => new DataStorageElement(a, OperationType.Mod, b); + public static DataStorageElement operator %(DataStorageElement a, decimal b) => new DataStorageElement(a, OperationType.Mod, b); + public static DataStorageElement operator ^(DataStorageElement a, int b) => new DataStorageElement(a, OperationType.Pow, b); + public static DataStorageElement operator ^(DataStorageElement a, long b) => new DataStorageElement(a, OperationType.Pow, b); + public static DataStorageElement operator ^(DataStorageElement a, float b) => new DataStorageElement(a, OperationType.Pow, b); + public static DataStorageElement operator ^(DataStorageElement a, double b) => new DataStorageElement(a, OperationType.Pow, b); + public static DataStorageElement operator ^(DataStorageElement a, decimal b) => new DataStorageElement(a, OperationType.Pow, b); public static DataStorageElement operator -(DataStorageElement a, int b) => new DataStorageElement(a, OperationType.Add, JToken.FromObject(-b)); public static DataStorageElement operator -(DataStorageElement a, long b) => new DataStorageElement(a, OperationType.Add, JToken.FromObject(-b)); - public static DataStorageElement operator -(DataStorageElement a, decimal b) => new DataStorageElement(a, OperationType.Add, JToken.FromObject(-b)); - public static DataStorageElement operator -(DataStorageElement a, double b) => new DataStorageElement(a, OperationType.Add, JToken.FromObject(-b)); public static DataStorageElement operator -(DataStorageElement a, float b) => new DataStorageElement(a, OperationType.Add, JToken.FromObject(-b)); + public static DataStorageElement operator -(DataStorageElement a, double b) => new DataStorageElement(a, OperationType.Add, JToken.FromObject(-b)); + public static DataStorageElement operator -(DataStorageElement a, decimal b) => new DataStorageElement(a, OperationType.Add, JToken.FromObject(-b)); public static DataStorageElement operator /(DataStorageElement a, int b) => new DataStorageElement(a, OperationType.Mul, JToken.FromObject(1m / b)); public static DataStorageElement operator /(DataStorageElement a, long b) => new DataStorageElement(a, OperationType.Mul, JToken.FromObject(1m / b)); - public static DataStorageElement operator /(DataStorageElement a, decimal b) => new DataStorageElement(a, OperationType.Mul, JToken.FromObject(1m / b)); - public static DataStorageElement operator /(DataStorageElement a, double b) => new DataStorageElement(a, OperationType.Mul, JToken.FromObject(1d / b)); public static DataStorageElement operator /(DataStorageElement a, float b) => new DataStorageElement(a, OperationType.Mul, JToken.FromObject(1d / b)); + public static DataStorageElement operator /(DataStorageElement a, double b) => new DataStorageElement(a, OperationType.Mul, JToken.FromObject(1d / b)); + public static DataStorageElement operator /(DataStorageElement a, decimal b) => new DataStorageElement(a, OperationType.Mul, JToken.FromObject(1m / b)); - [Obsolete("Use + Operation.Min() instead")] - public static DataStorageElement operator >>(DataStorageElement a, int b) => new DataStorageElement(a, OperationType.Min, b); - [Obsolete("Use + Operation.Max() instead")] - public static DataStorageElement operator <<(DataStorageElement a, int b) => new DataStorageElement(a, OperationType.Max, b); - public static implicit operator DataStorageElement(bool b) => new DataStorageElement(OperationType.Replace, b); public static implicit operator DataStorageElement(int i) => new DataStorageElement(OperationType.Replace, i); public static implicit operator DataStorageElement(long l) => new DataStorageElement(OperationType.Replace, l); @@ -105,12 +136,12 @@ internal DataStorageElement(DataStorageElement source, Callback callback) : this public static implicit operator int?(DataStorageElement e) => RetrieveAndReturnDecimalValue(e); public static implicit operator long(DataStorageElement e) => RetrieveAndReturnDecimalValue(e); public static implicit operator long?(DataStorageElement e) => RetrieveAndReturnDecimalValue(e); - public static implicit operator decimal(DataStorageElement e) => RetrieveAndReturnDecimalValue(e); - public static implicit operator decimal?(DataStorageElement e) => RetrieveAndReturnDecimalValue(e); - public static implicit operator double(DataStorageElement e) => RetrieveAndReturnDecimalValue(e); - public static implicit operator double?(DataStorageElement e) => RetrieveAndReturnDecimalValue(e); public static implicit operator float(DataStorageElement e) => RetrieveAndReturnDecimalValue(e); public static implicit operator float?(DataStorageElement e) => RetrieveAndReturnDecimalValue(e); + public static implicit operator double(DataStorageElement e) => RetrieveAndReturnDecimalValue(e); + public static implicit operator double?(DataStorageElement e) => RetrieveAndReturnDecimalValue(e); + public static implicit operator decimal(DataStorageElement e) => RetrieveAndReturnDecimalValue(e); + public static implicit operator decimal?(DataStorageElement e) => RetrieveAndReturnDecimalValue(e); public static implicit operator string(DataStorageElement e) => RetrieveAndReturnStringValue(e); public static implicit operator bool[](DataStorageElement e) => RetrieveAndReturnArrayValue(e); public static implicit operator int[](DataStorageElement e) => RetrieveAndReturnArrayValue(e); @@ -131,6 +162,109 @@ internal DataStorageElement(DataStorageElement source, Callback callback) : this public static implicit operator Array(DataStorageElement e) => RetrieveAndReturnArrayValue(e); public static implicit operator JArray(DataStorageElement e) => RetrieveAndReturnArrayValue(e); public static implicit operator JToken(DataStorageElement e) => e.Context.GetData(e.Context.Key); + +#if !NET35 + public static DataStorageElement operator +(DataStorageElement a, BigInteger b) => new DataStorageElement(a, OperationType.Add, JToken.Parse(b.ToString())); + public static DataStorageElement operator *(DataStorageElement a, BigInteger b) => new DataStorageElement(a, OperationType.Mul, JToken.Parse(b.ToString())); + public static DataStorageElement operator %(DataStorageElement a, BigInteger b) => new DataStorageElement(a, OperationType.Mod, JToken.Parse(b.ToString())); + public static DataStorageElement operator ^(DataStorageElement a, BigInteger b) => new DataStorageElement(a, OperationType.Pow, JToken.Parse(b.ToString())); + public static DataStorageElement operator -(DataStorageElement a, BigInteger b) => new DataStorageElement(a, OperationType.Add, JToken.Parse((-b).ToString())); + public static DataStorageElement operator /(DataStorageElement a, BigInteger b) => + throw new InvalidOperationException( + "DataStorage[Key] / BigInterger is not supported, due to loss of precision when using integer division"); + + public static implicit operator DataStorageElement(BigInteger bi) => new DataStorageElement(OperationType.Replace, JToken.Parse(bi.ToString())); + + public static implicit operator BigInteger(DataStorageElement e) => RetrieveAndReturnBigIntegerValue(e); + public static implicit operator BigInteger?(DataStorageElement e) => RetrieveAndReturnBigIntegerValue(e); + + static T RetrieveAndReturnBigIntegerValue(DataStorageElement e) + { + if (e.cachedValue != null) + { + return BigInteger.TryParse(e.cachedValue.ToString(), out var cachedBigInteger) + ? (T)Convert.ChangeType(cachedBigInteger, IsNullable() ? Nullable.GetUnderlyingType(typeof(T)) : typeof(T)) + : default; + } + + var value = BigInteger.TryParse(e.Context.GetData(e.Context.Key).ToString(), out var parsedValue) + ? parsedValue + : (BigInteger?)null; + + if (!value.HasValue && !IsNullable()) + value = Activator.CreateInstance(); + + foreach (var operation in e.Operations) + { + if (operation.OperationType == OperationType.Floor || operation.OperationType == OperationType.Ceil) + continue; + + if (!BigInteger.TryParse(operation.Value.ToString(), NumberStyles.AllowLeadingSign, null, out var operatorValue)) + throw new InvalidOperationException($"DataStorage[Key] cannot be converted to BigInterger as its value its not an integer number, value: {operation.Value}"); + + switch (operation.OperationType) + { + case OperationType.Replace: + value = operatorValue; + break; + + case OperationType.Add: + value += operatorValue; + break; + + case OperationType.Mul: + value *= operatorValue; + break; + + case OperationType.Mod: + value %= operatorValue; + break; + + case OperationType.Pow: + value = BigInteger.Pow(value.Value, (int)operation.Value); + break; + + case OperationType.Max: + if (operatorValue > value) + value = operatorValue; + break; + + case OperationType.Min: + if (operatorValue < value) + value = operatorValue; + break; + + case OperationType.Xor: + value ^= operatorValue; + break; + + case OperationType.Or: + value |= operatorValue; + break; + + case OperationType.And: + value &= operatorValue; + break; + + case OperationType.LeftShift: + value <<= (int)operation.Value; + break; + + case OperationType.RightShift: + value >>= (int)operation.Value; + break; + } + } + + e.cachedValue = JToken.Parse(value.ToString()); + + return value.HasValue + ? (T)Convert.ChangeType(value.Value, IsNullable() ? Nullable.GetUnderlyingType(typeof(T)) : typeof(T)) + : default; + } +#endif + + #pragma warning restore CS1591 /// /// Initializes a value in the server side data storage @@ -274,8 +408,8 @@ static T RetrieveAndReturnBoolValue(DataStorageElement e) ? (T)Convert.ChangeType(value.Value, IsNullable() ? Nullable.GetUnderlyingType(typeof(T)) : typeof(T)) : default; } - - static T RetrieveAndReturnDecimalValue(DataStorageElement e) + + static T RetrieveAndReturnDecimalValue(DataStorageElement e) { if (e.cachedValue != null) return e.cachedValue.ToObject(); @@ -336,7 +470,15 @@ static T RetrieveAndReturnDecimalValue(DataStorageElement e) case OperationType.RightShift: value = (long)value >> (int)operation.Value; break; - } + + case OperationType.Floor: + value = Math.Floor(value.Value); + break; + + case OperationType.Ceil: + value = Math.Ceiling(value.Value); + break; + } } e.cachedValue = value; diff --git a/Archipelago.MultiClient.Net/Models/ItemInfo.cs b/Archipelago.MultiClient.Net/Models/ItemInfo.cs new file mode 100644 index 0000000..b35f6de --- /dev/null +++ b/Archipelago.MultiClient.Net/Models/ItemInfo.cs @@ -0,0 +1,98 @@ +using Archipelago.MultiClient.Net.DataPackage; +using Archipelago.MultiClient.Net.Enums; +using Archipelago.MultiClient.Net.Helpers; + +namespace Archipelago.MultiClient.Net.Models +{ + /// + /// Information about an item and its location + /// + public class ItemInfo + { + readonly IItemInfoResolver itemInfoResolver; + + /// + /// The item id of the item. Item ids are in the range of ± (2^53)-1. + /// + public long ItemId { get; } + + /// + /// The location id of the item inside the world. Location ids are in the range of ± (2^53)-1. + /// + public long LocationId { get; } + + /// + /// The player of the world the item is located in + /// + public PlayerInfo Player { get; } + + /// + /// Enum flags that describe special properties of the Item + /// + public ItemFlags Flags { get; } + + /// + /// The name of the item, null if the name cannot be resolved + /// + public string ItemName => itemInfoResolver.GetItemName(ItemId, ItemGame); + + /// + /// The name of the item for display purposes, returns a fallback string containing the ItemId if the name cannot be resolved + /// + public string ItemDisplayName => ItemName ?? $"Item: {ItemId}"; + + /// + /// The name of the location that item is at, null if the name cannot be resolved + /// + public string LocationName => itemInfoResolver.GetLocationName(LocationId, LocationGame); + + /// + /// The name of the location for display purposes, returns a fallback string containing the LocationId if the name cannot be resolved + /// + public string LocationDisplayName => LocationName ?? $"Location: {LocationId}"; + + /// + /// The game the item belongs to + /// + public string ItemGame { get; } + + /// + /// The game the location belongs to + /// + public string LocationGame { get; } + + /// + /// The constructor what else did you expect it to be + /// + public ItemInfo(NetworkItem item, string receiverGame, string senderGame, IItemInfoResolver itemInfoResolver, PlayerInfo player) + { + this.itemInfoResolver = itemInfoResolver; + + ItemGame = receiverGame; + LocationGame = senderGame; + ItemId = item.Item; + LocationId = item.Location; + Flags = item.Flags; + Player = player; + } + } + + /// + /// Information about an item and its location + /// + public class ScoutedItemInfo : ItemInfo + { + /// + /// The player to receive the item + /// + public new PlayerInfo Player => base.Player; + + /// + /// The constructor what else did you expect it to be + /// + public ScoutedItemInfo(NetworkItem item, string receiverGame, string senderGame, IItemInfoResolver itemInfoResolver, PlayerInfo player) + : base(item, receiverGame, senderGame, itemInfoResolver, player) + { + } + } +} diff --git a/Archipelago.MultiClient.Net/Models/OperationSpecification.cs b/Archipelago.MultiClient.Net/Models/OperationSpecification.cs index 2c6b9a0..47c0e18 100644 --- a/Archipelago.MultiClient.Net/Models/OperationSpecification.cs +++ b/Archipelago.MultiClient.Net/Models/OperationSpecification.cs @@ -5,6 +5,11 @@ using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using System.Collections; +#if !NET35 +using System.Numerics; +#endif + +// ReSharper disable ArrangeObjectCreationWhenTypeEvident namespace Archipelago.MultiClient.Net.Models { @@ -37,15 +42,55 @@ public static class Operation /// Performs a Math.Min() on the store its current value vs the provided value /// /// The value to compare to - public static OperationSpecification Min(JToken i) => + public static OperationSpecification Min(int i) => + new OperationSpecification { OperationType = OperationType.Min, Value = i }; + /// + public static OperationSpecification Min(long i) => + new OperationSpecification { OperationType = OperationType.Min, Value = i }; + /// + public static OperationSpecification Min(float i) => + new OperationSpecification { OperationType = OperationType.Min, Value = i }; + /// + public static OperationSpecification Min(double i) => new OperationSpecification { OperationType = OperationType.Min, Value = i }; + /// + public static OperationSpecification Min(decimal i) => + new OperationSpecification { OperationType = OperationType.Min, Value = i }; + /// + public static OperationSpecification Min(JToken i) => + new OperationSpecification { OperationType = OperationType.Min, Value = i }; +#if !NET35 + /// + public static OperationSpecification Min(BigInteger i) => + new OperationSpecification { OperationType = OperationType.Min, Value = JToken.Parse(i.ToString()) }; +#endif /// /// Performs a Math.Max() on the store its current value vs the provided value /// /// The value to compare to + public static OperationSpecification Max(int i) => + new OperationSpecification { OperationType = OperationType.Max, Value = i }; + /// + public static OperationSpecification Max(long i) => + new OperationSpecification { OperationType = OperationType.Max, Value = i }; + /// + public static OperationSpecification Max(float i) => + new OperationSpecification { OperationType = OperationType.Max, Value = i }; + /// + public static OperationSpecification Max(double i) => + new OperationSpecification { OperationType = OperationType.Max, Value = i }; + /// + public static OperationSpecification Max(decimal i) => + new OperationSpecification { OperationType = OperationType.Max, Value = i }; + /// public static OperationSpecification Max(JToken i) => new OperationSpecification { OperationType = OperationType.Max, Value = i }; +#if !NET35 + /// + public static OperationSpecification Max(BigInteger i) => + new OperationSpecification { OperationType = OperationType.Max, Value = JToken.Parse(i.ToString()) }; +#endif /// /// Performs a List.Remove() to remove the first occurrence of the provided value @@ -58,15 +103,31 @@ public static OperationSpecification Remove(JToken value) => /// Performs a List.RemoveAt() or Dictionary.Remove() to remove a specified index or key from a list or dictionary /// /// The index or key to remove + public static OperationSpecification Pop(int value) => + new OperationSpecification { OperationType = OperationType.Pop, Value = value }; + /// public static OperationSpecification Pop(JToken value) => new OperationSpecification { OperationType = OperationType.Pop, Value = value }; + /// /// Performs Dictionary merge, adding all keys from value to the original dict overriding existing keys /// /// The dictionary to merge in public static OperationSpecification Update(IDictionary dictionary) => new OperationSpecification { OperationType = OperationType.Update, Value = JObject.FromObject(dictionary) }; + + /// + /// Performs a Math.Floor() on the store its current value + /// + public static OperationSpecification Floor() => + new OperationSpecification { OperationType = OperationType.Floor, Value = null }; + + /// + /// Performs a Math.Ceiling() on the store its current value + /// + public static OperationSpecification Ceiling() => + new OperationSpecification { OperationType = OperationType.Ceil, Value = null }; } /// @@ -74,19 +135,53 @@ public static OperationSpecification Update(IDictionary dictionary) => /// public static class Bitwise { - public static OperationSpecification Xor(long i) => + /// + /// Performs a bitwise Exclusive OR on the store its current value vs the provided value + /// + /// The value to XOR with + public static OperationSpecification Xor(long i) => new OperationSpecification { OperationType = OperationType.Xor, Value = i }; - - public static OperationSpecification Or(long i) => +#if !NET35 + /// + public static OperationSpecification Xor(BigInteger i) => + new OperationSpecification { OperationType = OperationType.Xor, Value = JToken.Parse(i.ToString()) }; +#endif + + /// + /// Performs a bitwise OR on the store its current value vs the provided value + /// + /// The value to OR with + public static OperationSpecification Or(long i) => new OperationSpecification { OperationType = OperationType.Or, Value = i }; - - public static OperationSpecification And(long i) => +#if !NET35 + /// + public static OperationSpecification Or(BigInteger i) => + new OperationSpecification { OperationType = OperationType.Or, Value = JToken.Parse(i.ToString()) }; +#endif + + /// + /// Performs a bitwise AND on the store its current value vs the provided value + /// + /// The value to AND with + public static OperationSpecification And(long i) => new OperationSpecification { OperationType = OperationType.And, Value = i }; - - public static OperationSpecification LeftShift(long i) => +#if !NET35 + /// + public static OperationSpecification And(BigInteger i) => + new OperationSpecification { OperationType = OperationType.And, Value = JToken.Parse(i.ToString()) }; +#endif + /// + /// Performs a bitwise left shift on the store its current value by the provided amount + /// + /// the amount to shift + public static OperationSpecification LeftShift(long i) => new OperationSpecification { OperationType = OperationType.LeftShift, Value = i }; - public static OperationSpecification RightShift(long i) => + /// + /// Performs a bitwise right shift on the store its current value by the provided amount + /// + /// the amount to shift + public static OperationSpecification RightShift(long i) => new OperationSpecification { OperationType = OperationType.RightShift, Value = i }; } @@ -108,4 +203,25 @@ public class Callback public static Callback Add(DataStorageHelper.DataStorageUpdatedHandler callback) => new Callback { Method = callback }; } + + /// + /// Provides a way to add additional arguments to the DataStorage operation + /// additional arguments are send back by the server and can be checked in callbacks or handlers + /// + public class AdditionalArgument + { + internal string Key { get; set; } + internal JToken Value { get; set; } + + AdditionalArgument() { } + + /// + /// Adds an additional argument to the current datastorage operations. + /// additional arguments are send back by the server and can be checked in callbacks or handlers + /// + /// The name of the argument + /// The value of the argument + public static AdditionalArgument Add(string name, JToken value) => + new AdditionalArgument { Key = name, Value = value }; + } } \ No newline at end of file diff --git a/Archipelago.MultiClient.Net/Packets/DataPackagePacket.cs b/Archipelago.MultiClient.Net/Packets/DataPackagePacket.cs index a3b703d..59bfc44 100644 --- a/Archipelago.MultiClient.Net/Packets/DataPackagePacket.cs +++ b/Archipelago.MultiClient.Net/Packets/DataPackagePacket.cs @@ -1,5 +1,4 @@ using Archipelago.MultiClient.Net.Enums; -using Archipelago.MultiClient.Net.Models; using Newtonsoft.Json; namespace Archipelago.MultiClient.Net.Packets @@ -9,6 +8,6 @@ public class DataPackagePacket : ArchipelagoPacketBase public override ArchipelagoPacketType PacketType => ArchipelagoPacketType.DataPackage; [JsonProperty("data")] - public DataPackage DataPackage { get; set; } + public Models.DataPackage DataPackage { get; set; } } } diff --git a/Archipelago.MultiClient.Net/Packets/LocationScoutsPacket.cs b/Archipelago.MultiClient.Net/Packets/LocationScoutsPacket.cs index 8312f8d..62cb7e7 100644 --- a/Archipelago.MultiClient.Net/Packets/LocationScoutsPacket.cs +++ b/Archipelago.MultiClient.Net/Packets/LocationScoutsPacket.cs @@ -11,6 +11,6 @@ public class LocationScoutsPacket : ArchipelagoPacketBase public long[] Locations { get; set; } [JsonProperty("create_as_hint")] - public bool CreateAsHint { get; set; } + public int CreateAsHint { get; set; } } } diff --git a/Archipelago.MultiClient.Net/Packets/RoomInfoPacket.cs b/Archipelago.MultiClient.Net/Packets/RoomInfoPacket.cs index 536ba5a..76defdc 100644 --- a/Archipelago.MultiClient.Net/Packets/RoomInfoPacket.cs +++ b/Archipelago.MultiClient.Net/Packets/RoomInfoPacket.cs @@ -13,6 +13,9 @@ public class RoomInfoPacket : ArchipelagoPacketBase [JsonProperty("version")] public NetworkVersion Version { get; set; } + [JsonProperty("generator_version")] + public NetworkVersion GeneratorVersion { get; set; } + [JsonProperty("tags")] public string[] Tags { get; set; } @@ -34,10 +37,6 @@ public class RoomInfoPacket : ArchipelagoPacketBase [JsonProperty("games")] public string[] Games { get; set; } - [Obsolete("use DataPackageChecksums instead")] - [JsonProperty("datapackage_versions")] - public Dictionary DataPackageVersions { get; set; } - [JsonProperty("datapackage_checksums")] public Dictionary DataPackageChecksums { get; set; } diff --git a/Archipelago.MultiClient.Net/Packets/SetPacket.cs b/Archipelago.MultiClient.Net/Packets/SetPacket.cs index d085f03..158f672 100644 --- a/Archipelago.MultiClient.Net/Packets/SetPacket.cs +++ b/Archipelago.MultiClient.Net/Packets/SetPacket.cs @@ -2,7 +2,8 @@ using Archipelago.MultiClient.Net.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using System; +using System.Collections.Generic; +using System.Runtime.Serialization; namespace Archipelago.MultiClient.Net.Packets { @@ -22,6 +23,10 @@ public class SetPacket : ArchipelagoPacketBase [JsonProperty("want_reply")] public bool WantReply { get; set; } - public Guid? Reference { get; set; } + [JsonExtensionData] + public Dictionary AdditionalArguments { get; set; } + + [OnDeserialized] + internal void OnDeserializedMethod(StreamingContext context) => AdditionalArguments?.Remove("cmd"); } } diff --git a/Archipelago.MultiClient.Net/Packets/SetReplyPacket.cs b/Archipelago.MultiClient.Net/Packets/SetReplyPacket.cs index c9de7f7..d43f2a2 100644 --- a/Archipelago.MultiClient.Net/Packets/SetReplyPacket.cs +++ b/Archipelago.MultiClient.Net/Packets/SetReplyPacket.cs @@ -1,32 +1,17 @@ using Archipelago.MultiClient.Net.Enums; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using System; namespace Archipelago.MultiClient.Net.Packets { - public class SetReplyPacket : ArchipelagoPacketBase + public class SetReplyPacket : SetPacket { public override ArchipelagoPacketType PacketType => ArchipelagoPacketType.SetReply; - [JsonProperty("key")] - public string Key { get; set; } - [JsonProperty("value")] public JToken Value { get; set; } - [JsonProperty("default")] - public JToken DefaultValue { get; set; } - [JsonProperty("original_value")] public JToken OriginalValue { get; set; } - - [JsonProperty("operation")] - public string Operation { get; set; } - - [JsonProperty("want_reply")] - public bool WantReply { get; set; } - - public Guid? Reference { get; set; } - } + } } diff --git a/Archipelago.MultiClient.Net/TwoWayLookup.cs b/Archipelago.MultiClient.Net/TwoWayLookup.cs new file mode 100644 index 0000000..6f375bc --- /dev/null +++ b/Archipelago.MultiClient.Net/TwoWayLookup.cs @@ -0,0 +1,28 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Archipelago.MultiClient.Net +{ + class TwoWayLookup : IEnumerable> + { + readonly Dictionary aToB = new Dictionary(); + readonly Dictionary bToA = new Dictionary(); + + public TA this[TB b] => bToA[b]; + public TB this[TA a] => aToB[a]; + + public void Add(TA a, TB b) + { + aToB[a] = b; + bToA[b] = a; + } + public void Add(TB b, TA a) => Add(a, b); + + public bool TryGetValue(TA a, out TB b) => aToB.TryGetValue(a, out b); + public bool TryGetValue(TB b, out TA a) => bToA.TryGetValue(b, out a); + + public IEnumerator> GetEnumerator() => bToA.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/DLLs/net40/Newtonsoft.Json.dll b/DLLs/net40/Newtonsoft.Json.dll new file mode 100644 index 0000000..a03ed79 Binary files /dev/null and b/DLLs/net40/Newtonsoft.Json.dll differ diff --git a/DLLs/net45/Newtonsoft.Json.dll b/DLLs/net45/Newtonsoft.Json.dll new file mode 100644 index 0000000..996a7fe Binary files /dev/null and b/DLLs/net45/Newtonsoft.Json.dll differ diff --git a/Newtonsoft.Json/Src/Newtonsoft.Json.sln.DotSettings b/Newtonsoft.Json/Src/Newtonsoft.Json.sln.DotSettings index 72cfc36..6c5e822 100644 --- a/Newtonsoft.Json/Src/Newtonsoft.Json.sln.DotSettings +++ b/Newtonsoft.Json/Src/Newtonsoft.Json.sln.DotSettings @@ -15,6 +15,7 @@ True True True + True True True True diff --git a/Newtonsoft.Json/Src/Newtonsoft.Json/Newtonsoft.Json.csproj b/Newtonsoft.Json/Src/Newtonsoft.Json/Newtonsoft.Json.csproj index f32f459..83dcde0 100644 --- a/Newtonsoft.Json/Src/Newtonsoft.Json/Newtonsoft.Json.csproj +++ b/Newtonsoft.Json/Src/Newtonsoft.Json/Newtonsoft.Json.csproj @@ -32,6 +32,7 @@ snupkg Newtonsoft.Json.ruleset true + net45;net40;net35;netstandard2.0 @@ -48,7 +49,7 @@ Json.NET .NET 4.0 - NET40;HAVE_ADO_NET;HAVE_APP_DOMAIN;HAVE_BIG_INTEGER;HAVE_BINARY_FORMATTER;HAVE_BINARY_SERIALIZATION;HAVE_BINARY_EXCEPTION_SERIALIZATION;HAVE_CAS;HAVE_CHAR_TO_STRING_WITH_CULTURE;HAVE_CHAR_TO_LOWER_WITH_CULTURE;HAVE_COM_ATTRIBUTES;HAVE_COMPONENT_MODEL;HAVE_CONCURRENT_COLLECTIONS;HAVE_COVARIANT_GENERICS;HAVE_DATA_CONTRACTS;HAVE_DATE_TIME_OFFSET;HAVE_DB_NULL_TYPE_CODE;HAVE_DYNAMIC;HAVE_EMPTY_TYPES;HAVE_ENTITY_FRAMEWORK;HAVE_EXPRESSIONS;HAVE_FAST_REVERSE;HAVE_FSHARP_TYPES;HAVE_FULL_REFLECTION;HAVE_GUID_TRY_PARSE;HAVE_HASH_SET;HAVE_ICLONEABLE;HAVE_ICONVERTIBLE;HAVE_IGNORE_DATA_MEMBER_ATTRIBUTE;HAVE_INOTIFY_COLLECTION_CHANGED;HAVE_INOTIFY_PROPERTY_CHANGING;HAVE_ISET;HAVE_LINQ;HAVE_MEMORY_BARRIER;HAVE_NON_SERIALIZED_ATTRIBUTE;HAVE_REFLECTION_EMIT;HAVE_SECURITY_SAFE_CRITICAL_ATTRIBUTE;HAVE_SERIALIZATION_BINDER_BIND_TO_NAME;HAVE_STREAM_READER_WRITER_CLOSE;HAVE_STRING_JOIN_WITH_ENUMERABLE;HAVE_TIME_SPAN_PARSE_WITH_CULTURE;HAVE_TIME_SPAN_TO_STRING_WITH_CULTURE;HAVE_TIME_ZONE_INFO;HAVE_TRACE_WRITER;HAVE_TYPE_DESCRIPTOR;HAVE_UNICODE_SURROGATE_DETECTION;HAVE_VARIANT_TYPE_PARAMETERS;HAVE_VERSION_TRY_PARSE;HAVE_XLINQ;HAVE_XML_DOCUMENT;HAVE_XML_DOCUMENT_TYPE;HAVE_CONCURRENT_DICTIONARY;$(AdditionalConstants) + NET40;HAVE_APP_DOMAIN;HAVE_BIG_INTEGER;HAVE_BINARY_FORMATTER;HAVE_BINARY_SERIALIZATION;HAVE_BINARY_EXCEPTION_SERIALIZATION;HAVE_CAS;HAVE_CHAR_TO_STRING_WITH_CULTURE;HAVE_CHAR_TO_LOWER_WITH_CULTURE;HAVE_CONCURRENT_COLLECTIONS;HAVE_COVARIANT_GENERICS;HAVE_DATE_TIME_OFFSET;HAVE_EMPTY_TYPES;HAVE_FAST_REVERSE;HAVE_FSHARP_TYPES;HAVE_FULL_REFLECTION;HAVE_GUID_TRY_PARSE;HAVE_HASH_SET;HAVE_ICLONEABLE;HAVE_ICONVERTIBLE;HAVE_IGNORE_DATA_MEMBER_ATTRIBUTE;HAVE_ISET;HAVE_LINQ;HAVE_MEMORY_BARRIER;HAVE_NON_SERIALIZED_ATTRIBUTE;HAVE_REFLECTION_EMIT;HAVE_SECURITY_SAFE_CRITICAL_ATTRIBUTE;HAVE_SERIALIZATION_BINDER_BIND_TO_NAME;HAVE_STREAM_READER_WRITER_CLOSE;HAVE_STRING_JOIN_WITH_ENUMERABLE;HAVE_TIME_SPAN_PARSE_WITH_CULTURE;HAVE_TIME_SPAN_TO_STRING_WITH_CULTURE;HAVE_TIME_ZONE_INFO;HAVE_TRACE_WRITER;HAVE_UNICODE_SURROGATE_DETECTION;HAVE_VARIANT_TYPE_PARAMETERS;HAVE_VERSION_TRY_PARSE;HAVE_CONCURRENT_DICTIONARY;$(AdditionalConstants) diff --git a/Newtonsoft.Json/Src/global.json b/Newtonsoft.Json/Src/global.json index 6d19c9f..112677b 100644 --- a/Newtonsoft.Json/Src/global.json +++ b/Newtonsoft.Json/Src/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "5.0.200" + "version": "7.0.306" } } \ No newline at end of file