From 3af87eacde42ba24eb5ca7f98ebedd5c1334c454 Mon Sep 17 00:00:00 2001 From: Archomeda Date: Sun, 1 Aug 2021 01:48:20 +0200 Subject: [PATCH 1/5] Add beginnings of custom Mumble Link reader support --- Gw2Sharp.Tests/Gw2Sharp.Tests.csproj | 1 + .../Mumble/Gw2MumbleClientReaderTests.cs | 103 ++++++++++++++++ Gw2Sharp/Mumble/Gw2LinkedMem.cs | 112 +++++++++++++++++- Gw2Sharp/Mumble/Gw2MumbleClient.cs | 40 ++++--- Gw2Sharp/Mumble/Gw2MumbleClientReader.cs | 97 +++++++++++++++ Gw2Sharp/Mumble/IGw2MumbleClient.cs | 12 ++ Gw2Sharp/Mumble/IGw2MumbleClientReader.cs | 32 +++++ Gw2Sharp/Mumble/Models/UiState.cs | 2 +- 8 files changed, 379 insertions(+), 20 deletions(-) create mode 100644 Gw2Sharp.Tests/Mumble/Gw2MumbleClientReaderTests.cs create mode 100644 Gw2Sharp/Mumble/Gw2MumbleClientReader.cs create mode 100644 Gw2Sharp/Mumble/IGw2MumbleClientReader.cs diff --git a/Gw2Sharp.Tests/Gw2Sharp.Tests.csproj b/Gw2Sharp.Tests/Gw2Sharp.Tests.csproj index 0c4a11e11..a0d338227 100644 --- a/Gw2Sharp.Tests/Gw2Sharp.Tests.csproj +++ b/Gw2Sharp.Tests/Gw2Sharp.Tests.csproj @@ -3,6 +3,7 @@ net5.0;netcoreapp3.1;netcoreapp2.1;net461 preview + true annotations false true diff --git a/Gw2Sharp.Tests/Mumble/Gw2MumbleClientReaderTests.cs b/Gw2Sharp.Tests/Mumble/Gw2MumbleClientReaderTests.cs new file mode 100644 index 000000000..09fed2345 --- /dev/null +++ b/Gw2Sharp.Tests/Mumble/Gw2MumbleClientReaderTests.cs @@ -0,0 +1,103 @@ +using System; +using System.IO.MemoryMappedFiles; +using System.Net.Sockets; +using System.Reflection; +using FluentAssertions; +using FluentAssertions.Execution; +using Gw2Sharp.Models; +using Gw2Sharp.Mumble; +using Gw2Sharp.Mumble.Models; +using Xunit; + +namespace Gw2Sharp.Tests.Mumble +{ + public class Gw2MumbleClientReaderTests + { + private bool isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT; + + [Fact] + public void ThrowsPlatformNotSupportedExceptionIfNotWindowsTest() + { + Action createClient = () => + { + using var reader = new Gw2MumbleClientReader(); + }; + + if (this.isWindows) + createClient.Should().NotThrow(); + else + createClient.Should().Throw(); + } + + [SkippableFact] + public unsafe void ReadStructCorrectlyTest() + { + // Named memory mapped files aren't supported on Unix based systems. + // So we need to skip this test. + Skip.IfNot(this.isWindows, "Mumble Link is only supported on Windows"); + + using var memorySource = Assembly.GetExecutingAssembly().GetManifestResourceStream($"Gw2Sharp.Tests.TestFiles.Mumble.MemoryMappedFile.bin"); + using var memoryMappedFile = MemoryMappedFile.CreateOrOpen(Gw2MumbleClient.DEFAULT_MUMBLE_LINK_MAP_NAME, memorySource.Length); + using var stream = memoryMappedFile.CreateViewStream(); + memorySource.CopyTo(stream); + + using var client = new Gw2MumbleClientReader(); + client.Open(Gw2MumbleClient.DEFAULT_MUMBLE_LINK_MAP_NAME); + var mem = client.Read(); + + using (new AssertionScope()) + { + mem.uiVersion.Should().Be(2); + mem.uiTick.Should().Be(7244); + mem.fAvatarPosition[0].Should().BeApproximately(-68.27287f, 5); + mem.fAvatarPosition[1].Should().BeApproximately(119.209f, 3); + mem.fAvatarPosition[2].Should().BeApproximately(-6.154798f, 6); + mem.fAvatarFront[0].Should().BeApproximately(0.8834972f, 7); + mem.fAvatarFront[1].Should().BeApproximately(0f, 5); + mem.fAvatarFront[2].Should().BeApproximately(-0.4684365f, 7); + new string(mem.name).Should().BeEquivalentTo(Gw2MumbleClient.MUMBLE_LINK_GAME_NAME_GUILD_WARS_2); + mem.fCameraPosition[0].Should().BeApproximately(-78.08988f, 5); + mem.fCameraPosition[1].Should().BeApproximately(126.4892f, 4); + mem.fCameraPosition[2].Should().BeApproximately(-0.2525153f, 7); + mem.fCameraFront[0].Should().BeApproximately(0.8136908f, 7); + mem.fCameraFront[1].Should().BeApproximately(-0.3139677f, 7); + mem.fCameraFront[2].Should().BeApproximately(-0.4892152f, 7); + new string(mem.identity).Should().BeEquivalentTo(@"{""name"":""Reiga Fiercecrusher"",""profession"":2,""spec"":18,""race"":1,""map_id"":1206,""world_id"":268435460,""team_color_id"":0,""commander"":false,""map"":1206,""fov"":0.960,""uisz"":1}"); + mem.context.socketAddressFamily.Should().Be(AddressFamily.InterNetwork); + mem.context.socketAddress4[0].Should().Be(18); + mem.context.socketAddress4[1].Should().Be(197); + mem.context.socketAddress4[2].Should().Be(217); + mem.context.socketAddress4[3].Should().Be(165); + mem.context.socketPort.Should().Be(0); + mem.context.mapId.Should().Be(1206); + mem.context.mapType.Should().Be((uint)MapType.PublicMini); + mem.context.shardId.Should().Be(268435460u); + mem.context.instance.Should().Be(0); + mem.context.buildId.Should().Be(99552); + mem.context.uiState.Should().Be(UiState.IsCompassRotationEnabled | UiState.DoesGameHaveFocus | UiState.IsCompetitiveMode | UiState.DoesAnyInputHaveFocus); + mem.context.compassWidth.Should().Be(362); + mem.context.compassHeight.Should().Be(229); + mem.context.compassRotation.Should().BeApproximately(-2.11212f, 5); + mem.context.playerMapX.Should().BeApproximately(14400.01f, 2); + mem.context.playerMapY.Should().BeApproximately(18180.19f, 2); + mem.context.mapCenterX.Should().BeApproximately(14400.01f, 2); + mem.context.mapCenterY.Should().BeApproximately(18180.19f, 2); + mem.context.mapScale.Should().Be(1); + mem.context.processId.Should().Be(15101); + mem.context.mount.Should().Be(MountType.Griffon); + } + } + + [SkippableFact] + public void DisposeCorrectlyTest() + { + // Named memory mapped files aren't supported on Unix based systems. + // So we need to skip this test. + Skip.IfNot(this.isWindows, "Mumble Link is only supported in Windows"); + + var reader = new Gw2MumbleClientReader(); + reader.Dispose(); + Assert.ThrowsAny(() => reader.Read()); + } + } +} diff --git a/Gw2Sharp/Mumble/Gw2LinkedMem.cs b/Gw2Sharp/Mumble/Gw2LinkedMem.cs index ede13d5b5..2d3eb42bd 100644 --- a/Gw2Sharp/Mumble/Gw2LinkedMem.cs +++ b/Gw2Sharp/Mumble/Gw2LinkedMem.cs @@ -13,20 +13,56 @@ namespace Gw2Sharp.Mumble { + /// + /// The Guild Wars 2 Mumble Link struct. + /// [StructLayout(LayoutKind.Explicit)] - internal unsafe struct Gw2LinkedMem + public unsafe struct Gw2LinkedMem { + /// + /// The Mumble Link struct size. + /// public const int SIZE = 5460; + /// + /// The Mumble Link version. + /// [FieldOffset(0)] public uint uiVersion; + /// + /// The Mumble Link tick count. + /// [FieldOffset(4)] public uint uiTick; + /// + /// The avatar position. + /// [FieldOffset(8)] public fixed float fAvatarPosition[3]; + /// + /// The avatar front. + /// [FieldOffset(20)] public fixed float fAvatarFront[3]; + /// + /// The game name. + /// [FieldOffset(44)] public fixed char name[256]; + /// + /// The camera position. + /// [FieldOffset(556)] public fixed float fCameraPosition[3]; + /// + /// The camera front. + /// [FieldOffset(568)] public fixed float fCameraFront[3]; + /// + /// The Mumble Link identity. + /// [FieldOffset(592)] public fixed char identity[256]; + /// + /// The context struct size. + /// [FieldOffset(1104)] public uint contextLen; + /// + /// The Mumble Link context. + /// [FieldOffset(1108)] public Gw2Context context; // Unused fields @@ -42,36 +78,108 @@ internal unsafe struct Gw2LinkedMem // Total struct size is 5460 bytes } + /// + /// Guild Wars 2 specific Mumble Link context struct. + /// [StructLayout(LayoutKind.Explicit)] - internal unsafe struct Gw2Context + public unsafe struct Gw2Context { + /// + /// The socket address struct size. + /// public const int SOCKET_ADDRESS_SIZE = 28; + /// + /// The raw socket address data. + /// [FieldOffset(0)] public fixed byte socketAddress[SOCKET_ADDRESS_SIZE]; + /// + /// The socket address family. + /// [FieldOffset(0)] private ushort _socketAddressFamily; + /// + /// The socket address family. + /// public AddressFamily socketAddressFamily => (AddressFamily)this._socketAddressFamily; + /// + /// The socket port. + /// [FieldOffset(2)] public ushort socketPort; + /// + /// The socket address in IPv4 format. + /// [FieldOffset(4)] public fixed byte socketAddress4[4]; + /// + /// The socket address in IPv6 format. + /// [FieldOffset(8)] public fixed ushort socketAddress6[8]; + /// + /// The map id. + /// [FieldOffset(28)] public uint mapId; + /// + /// The map type. + /// [FieldOffset(32)] public uint mapType; + /// + /// The shard id. + /// [FieldOffset(36)] public uint shardId; + /// + /// The instance. + /// [FieldOffset(40)] public uint instance; + /// + /// The build id. + /// [FieldOffset(44)] public uint buildId; + /// + /// The UI state. + /// [FieldOffset(48)] public UiState uiState; + /// + /// The compass width. + /// [FieldOffset(52)] public ushort compassWidth; + /// + /// The compass height. + /// [FieldOffset(54)] public ushort compassHeight; + /// + /// The compass rotation. + /// [FieldOffset(56)] public float compassRotation; + /// + /// The player map x-coordinate. + /// [FieldOffset(60)] public float playerMapX; + /// + /// The player map y-coordinate. + /// [FieldOffset(64)] public float playerMapY; + /// + /// The map center x-coordinate. + /// [FieldOffset(68)] public float mapCenterX; + /// + /// The map center y-coordinate. + /// [FieldOffset(72)] public float mapCenterY; + /// + /// The map scale. + /// [FieldOffset(76)] public float mapScale; + /// + /// The process id. + /// [FieldOffset(80)] public uint processId; + /// + /// The mount. + /// [FieldOffset(84)] public MountType mount; // Total struct size is 256 bytes diff --git a/Gw2Sharp/Mumble/Gw2MumbleClient.cs b/Gw2Sharp/Mumble/Gw2MumbleClient.cs index c671d9be6..e3e94a260 100644 --- a/Gw2Sharp/Mumble/Gw2MumbleClient.cs +++ b/Gw2Sharp/Mumble/Gw2MumbleClient.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Concurrent; -using System.IO.MemoryMappedFiles; using System.Linq; using System.Net.Sockets; -using System.Runtime.Versioning; using System.Text.Json; using Gw2Sharp.Json; using Gw2Sharp.Models; @@ -14,9 +12,6 @@ namespace Gw2Sharp.Mumble /// /// A client for the Guild Wars 2 Mumble Link API service. /// -#if NET5_0_OR_GREATER - [SupportedOSPlatform("windows")] -#endif public class Gw2MumbleClient : IGw2MumbleClient { /// @@ -37,9 +32,6 @@ public class Gw2MumbleClient : IGw2MumbleClient private readonly object identityLock = new object(); private readonly object serverAddressLock = new object(); - private readonly Lazy memoryMappedFile; - private readonly Lazy memoryMappedViewAccessor; - private Gw2LinkedMem linkedMem; private readonly ConcurrentDictionary> mumbleClientCache; @@ -57,10 +49,12 @@ protected internal Gw2MumbleClient(string mumbleLinkName = DEFAULT_MUMBLE_LINK_M if (this.mumbleLinkName == DEFAULT_MUMBLE_LINK_MAP_NAME) this.mumbleClientCache.TryAdd(DEFAULT_MUMBLE_LINK_MAP_NAME, new WeakReference(this, false)); - this.memoryMappedFile = new Lazy( - () => MemoryMappedFile.CreateOrOpen(this.mumbleLinkName, Gw2LinkedMem.SIZE, MemoryMappedFileAccess.ReadWrite), true); - this.memoryMappedViewAccessor = new Lazy( - () => this.memoryMappedFile.Value.CreateViewAccessor(), true); +#if NET5_0_OR_GREATER + if (OperatingSystem.IsWindows()) +#else + if (Environment.OSVersion.Platform == PlatformID.Win32NT) +#endif + this.Reader = new Gw2MumbleClientReader(); } private unsafe void UpdateIdentityIfNeeded() @@ -111,6 +105,10 @@ private CharacterIdentity? Identity } + /// + public IGw2MumbleClientReader? Reader { get; private set; } + + /// public IGw2MumbleClient this[string name] { @@ -344,8 +342,12 @@ public unsafe void Update() { if (this.isDisposed) throw new ObjectDisposedException(nameof(Gw2MumbleClient)); + if (this.Reader is null) + throw new InvalidOperationException("No reader was defined. Make sure to set the reader through .WithReader() before calling .Update()"); - this.memoryMappedViewAccessor.Value.Read(0, out var mem); + if (!this.Reader.IsOpen) + this.Reader.Open(this.mumbleLinkName); + var mem = this.Reader.Read(); int oldTick = this.Tick; if (mem.uiTick != oldTick) @@ -368,6 +370,13 @@ public unsafe void Update() this.linkedMem = mem; } + /// + public IGw2MumbleClient WithReader() where T : IGw2MumbleClientReader, new() + { + this.Reader = new T(); + return this; + } + #region IDisposable Support @@ -384,10 +393,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { - if (this.memoryMappedViewAccessor.IsValueCreated) - this.memoryMappedViewAccessor.Value.Dispose(); - if (this.memoryMappedFile.IsValueCreated) - this.memoryMappedFile.Value.Dispose(); + this.Reader?.Dispose(); // Only dispose the full client cache tree if we are the default one if (this.mumbleLinkName == DEFAULT_MUMBLE_LINK_MAP_NAME) diff --git a/Gw2Sharp/Mumble/Gw2MumbleClientReader.cs b/Gw2Sharp/Mumble/Gw2MumbleClientReader.cs new file mode 100644 index 000000000..e96a3d095 --- /dev/null +++ b/Gw2Sharp/Mumble/Gw2MumbleClientReader.cs @@ -0,0 +1,97 @@ +using System; +using System.IO.MemoryMappedFiles; + +namespace Gw2Sharp.Mumble +{ + /// + /// The default Guild Wars 2 Mumble client reader. + /// Uses the official Mumble Link API, which is only available on Windows operating systems, or in Windows-emulated environments. + /// +#if NET5_0_OR_GREATER + [System.Runtime.Versioning.SupportedOSPlatform("windows")] +#endif + public class Gw2MumbleClientReader : IGw2MumbleClientReader + { + private MemoryMappedFile? file; + private MemoryMappedViewAccessor? accessor; + + /// + /// Creates a new . + /// + /// Only Windows operating systems are supported. + public Gw2MumbleClientReader() + { +#if NET5_0_OR_GREATER + if (!OperatingSystem.IsWindows()) +#else + if (Environment.OSVersion.Platform != PlatformID.Win32NT) +#endif + throw new PlatformNotSupportedException("Only Windows operating systems support the Mumble Link"); + } + + /// + public bool IsOpen { get; private set; } + + /// + public void Open(string mumbleLinkName) + { + this.file = MemoryMappedFile.CreateOrOpen(mumbleLinkName, Gw2LinkedMem.SIZE, MemoryMappedFileAccess.ReadWrite); + this.accessor = this.file.CreateViewAccessor(); + this.IsOpen = true; + } + + /// + public void Close() + { + this.accessor?.Dispose(); + this.file?.Dispose(); + + this.accessor = null; + this.file = null; + this.IsOpen = false; + } + + /// + public Gw2LinkedMem Read() + { + if (this.isDisposed) + throw new ObjectDisposedException(nameof(Gw2MumbleClientReader)); + if (this.file is null || this.accessor is null) + throw new InvalidOperationException("The reader has to be opened first"); + + this.accessor.Read(0, out var mem); + return mem; + } + + +#region IDisposable Support + + private bool isDisposed = false; // To detect redundant calls + + /// + /// Disposes the object. + /// + /// Dispose managed resources. + protected virtual void Dispose(bool disposing) + { + if (this.isDisposed) + return; + + if (disposing) + { + this.Close(); + } + + this.isDisposed = true; + } + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + +#endregion + } +} diff --git a/Gw2Sharp/Mumble/IGw2MumbleClient.cs b/Gw2Sharp/Mumble/IGw2MumbleClient.cs index d894e5fed..1009fe2a8 100644 --- a/Gw2Sharp/Mumble/IGw2MumbleClient.cs +++ b/Gw2Sharp/Mumble/IGw2MumbleClient.cs @@ -10,6 +10,11 @@ namespace Gw2Sharp.Mumble /// public interface IGw2MumbleClient : IDisposable { + /// + /// The Mumble client reader instance. + /// + IGw2MumbleClientReader Reader { get; } + /// /// Gets a custom named Mumble Link client API. /// @@ -243,5 +248,12 @@ public interface IGw2MumbleClient : IDisposable /// Call this every frame and/or every time you want to update the Mumble Link data before reading. /// void Update(); + + /// + /// Sets a custom Mumble client reader. + /// + /// The reader type. + /// This instance. + IGw2MumbleClient WithReader() where T : IGw2MumbleClientReader, new(); } } diff --git a/Gw2Sharp/Mumble/IGw2MumbleClientReader.cs b/Gw2Sharp/Mumble/IGw2MumbleClientReader.cs new file mode 100644 index 000000000..43b35ad7e --- /dev/null +++ b/Gw2Sharp/Mumble/IGw2MumbleClientReader.cs @@ -0,0 +1,32 @@ +using System; + +namespace Gw2Sharp.Mumble +{ + /// + /// An interface for implementing a reader for the Guild Wars 2 Mumble client. + /// + public interface IGw2MumbleClientReader : IDisposable + { + /// + /// Whether this reader is open. + /// + bool IsOpen { get; } + + /// + /// Opens the reader. + /// + /// The Mumble Link name. + void Open(string mumbleLinkName); + + /// + /// Closes the reader. + /// + void Close(); + + /// + /// Reads the Guild Wars 2 Mumble Link data into the struct. + /// + /// The read Guild Wars 2 Mumble Link data as . + Gw2LinkedMem Read(); + } +} diff --git a/Gw2Sharp/Mumble/Models/UiState.cs b/Gw2Sharp/Mumble/Models/UiState.cs index a0d88ea9f..cc85389f9 100644 --- a/Gw2Sharp/Mumble/Models/UiState.cs +++ b/Gw2Sharp/Mumble/Models/UiState.cs @@ -6,7 +6,7 @@ namespace Gw2Sharp.Mumble.Models /// The UI state. /// [Flags] - internal enum UiState : uint + public enum UiState : uint { /// /// Whether the map is currently open. From 252a0c33b57c04e0bc77870a32097b8c6c365182 Mon Sep 17 00:00:00 2001 From: Archomeda Date: Thu, 5 Aug 2021 23:35:44 +0200 Subject: [PATCH 2/5] Introduce reader factory to support custom Mumble Link readers --- Gw2Sharp.Tests/ConnectionTests.cs | 19 ++ .../Mumble/Gw2MumbleClientReaderTests.cs | 23 ++- Gw2Sharp.Tests/Mumble/Gw2MumbleClientTests.cs | 183 ++++++++---------- Gw2Sharp/Connection.cs | 21 ++ Gw2Sharp/Gw2Client.cs | 30 +-- Gw2Sharp/IConnection.cs | 12 +- Gw2Sharp/IGw2Client.cs | 5 - Gw2Sharp/Mumble/Gw2MumbleClient.cs | 45 ++--- Gw2Sharp/Mumble/Gw2MumbleClientReader.cs | 22 ++- Gw2Sharp/Mumble/IGw2MumbleClient.cs | 12 -- Gw2Sharp/Mumble/IGw2MumbleClientReader.cs | 10 +- .../UnsupportedMumblePlatformClientReader.cs | 38 ++++ 12 files changed, 237 insertions(+), 183 deletions(-) create mode 100644 Gw2Sharp/Mumble/UnsupportedMumblePlatformClientReader.cs diff --git a/Gw2Sharp.Tests/ConnectionTests.cs b/Gw2Sharp.Tests/ConnectionTests.cs index 870d81643..27d843be1 100644 --- a/Gw2Sharp.Tests/ConnectionTests.cs +++ b/Gw2Sharp.Tests/ConnectionTests.cs @@ -1,7 +1,9 @@ using System; using System.Diagnostics; +using AutoFixture.Xunit2; using FluentAssertions; using FluentAssertions.Execution; +using Gw2Sharp.Mumble; using Gw2Sharp.WebApi; using Gw2Sharp.WebApi.Caching; using Gw2Sharp.WebApi.Http; @@ -108,5 +110,22 @@ public void LocaleStringTest(string expected, Locale locale) var connection = new Connection(locale); Assert.Equal(expected, connection.LocaleString); } + + [Theory] + [AutoData] + public void UsesCorrectMumbleClientReaderFactoryTest(string mumbleLinkName) + { + var connection = new Connection(); + using var actual = connection.MumbleClientReaderFactory(mumbleLinkName); + +#if NET5_0_OR_GREATER + if (OperatingSystem.IsWindows()) +#else + if (Environment.OSVersion.Platform == PlatformID.Win32NT) +#endif + actual.Should().BeOfType(); + else + actual.Should().BeOfType(); + } } } diff --git a/Gw2Sharp.Tests/Mumble/Gw2MumbleClientReaderTests.cs b/Gw2Sharp.Tests/Mumble/Gw2MumbleClientReaderTests.cs index 09fed2345..a731168d4 100644 --- a/Gw2Sharp.Tests/Mumble/Gw2MumbleClientReaderTests.cs +++ b/Gw2Sharp.Tests/Mumble/Gw2MumbleClientReaderTests.cs @@ -2,6 +2,7 @@ using System.IO.MemoryMappedFiles; using System.Net.Sockets; using System.Reflection; +using AutoFixture.Xunit2; using FluentAssertions; using FluentAssertions.Execution; using Gw2Sharp.Models; @@ -13,14 +14,19 @@ namespace Gw2Sharp.Tests.Mumble { public class Gw2MumbleClientReaderTests { +#if NET5_0_OR_GREATER + private bool isWindows = OperatingSystem.IsWindows(); +#else private bool isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT; +#endif - [Fact] - public void ThrowsPlatformNotSupportedExceptionIfNotWindowsTest() + [Theory] + [AutoData] + public void ThrowsPlatformNotSupportedExceptionIfNotWindowsTest(string mumbleLinkName) { Action createClient = () => { - using var reader = new Gw2MumbleClientReader(); + using var reader = new Gw2MumbleClientReader(mumbleLinkName); }; if (this.isWindows) @@ -41,8 +47,8 @@ public unsafe void ReadStructCorrectlyTest() using var stream = memoryMappedFile.CreateViewStream(); memorySource.CopyTo(stream); - using var client = new Gw2MumbleClientReader(); - client.Open(Gw2MumbleClient.DEFAULT_MUMBLE_LINK_MAP_NAME); + using var client = new Gw2MumbleClientReader(Gw2MumbleClient.DEFAULT_MUMBLE_LINK_MAP_NAME); + client.Open(); var mem = client.Read(); using (new AssertionScope()) @@ -88,14 +94,15 @@ public unsafe void ReadStructCorrectlyTest() } } - [SkippableFact] - public void DisposeCorrectlyTest() + [SkippableTheory] + [AutoData] + public void DisposeCorrectlyTest(string mumbleLinkName) { // Named memory mapped files aren't supported on Unix based systems. // So we need to skip this test. Skip.IfNot(this.isWindows, "Mumble Link is only supported in Windows"); - var reader = new Gw2MumbleClientReader(); + var reader = new Gw2MumbleClientReader(mumbleLinkName); reader.Dispose(); Assert.ThrowsAny(() => reader.Read()); } diff --git a/Gw2Sharp.Tests/Mumble/Gw2MumbleClientTests.cs b/Gw2Sharp.Tests/Mumble/Gw2MumbleClientTests.cs index 2deb9443f..49f674f66 100644 --- a/Gw2Sharp.Tests/Mumble/Gw2MumbleClientTests.cs +++ b/Gw2Sharp.Tests/Mumble/Gw2MumbleClientTests.cs @@ -1,10 +1,11 @@ using System; -using System.IO.MemoryMappedFiles; using System.Reflection; +using System.Runtime.InteropServices; +using AutoFixture.Xunit2; using FluentAssertions; -using Gw2Sharp.Models; +using FluentAssertions.Execution; using Gw2Sharp.Mumble; -using Gw2Sharp.Mumble.Models; +using NSubstitute; using Xunit; namespace Gw2Sharp.Tests.Mumble @@ -13,128 +14,110 @@ public class Gw2MumbleClientTests { private bool isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT; + private Gw2LinkedMem GetLinkedMem() + { + using var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"Gw2Sharp.Tests.TestFiles.Mumble.MemoryMappedFile.bin"); + var buffer = new byte[resourceStream.Length]; + resourceStream.Read(buffer, 0, buffer.Length); + + var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + return Marshal.PtrToStructure(handle.AddrOfPinnedObject()); + } + finally + { + handle.Free(); + } + } + [Fact] - public void ThrowsPlatformNotSupportedExceptionIfNotWindowsTest() + public void ReadsCorrectlyTest() { - Action createClient = () => + using var reader = Substitute.For(); + reader.IsOpen.Returns(false); + reader.Read().Returns(GetLinkedMem()); + + using (var client = new Gw2MumbleClient(_ => reader)) { - using var client = new Gw2MumbleClient(); client.Update(); - }; - if (this.isWindows) - createClient.Should().NotThrow(); - else - createClient.Should().Throw(); - } + reader.Received(1).Open(); + reader.Received(1).Read(); + } - [SkippableFact] - public void ReadStructCorrectlyTest() - { - // Named memory mapped files aren't supported on Unix based systems. - // So we need to skip this test. - Skip.IfNot(this.isWindows, "Mumble Link is only supported in Windows"); - - using var memorySource = Assembly.GetExecutingAssembly().GetManifestResourceStream($"Gw2Sharp.Tests.TestFiles.Mumble.MemoryMappedFile.bin"); - using var memoryMappedFile = MemoryMappedFile.CreateOrOpen(Gw2MumbleClient.DEFAULT_MUMBLE_LINK_MAP_NAME, memorySource.Length); - using var stream = memoryMappedFile.CreateViewStream(); - memorySource.CopyTo(stream); - - using var client = new Gw2MumbleClient(); - client.Update(); - Assert.True(client.IsAvailable); - Assert.Equal(2, client.Version); - Assert.Equal(7244, client.Tick); - Assert.Equal(-68.27287, client.AvatarPosition.X, 5); - Assert.Equal(119.209, client.AvatarPosition.Y, 3); - Assert.Equal(-6.154798, client.AvatarPosition.Z, 6); - Assert.Equal(0.8834972, client.AvatarFront.X, 7); - Assert.Equal(0, client.AvatarFront.Y, 5); - Assert.Equal(-0.4684365, client.AvatarFront.Z, 7); - Assert.Equal(Gw2MumbleClient.MUMBLE_LINK_GAME_NAME_GUILD_WARS_2, client.Name); - Assert.Equal(-78.08988, client.CameraPosition.X, 5); - Assert.Equal(126.4892, client.CameraPosition.Y, 4); - Assert.Equal(-0.2525153, client.CameraPosition.Z, 7); - Assert.Equal(0.8136908, client.CameraFront.X, 7); - Assert.Equal(-0.3139677, client.CameraFront.Y, 7); - Assert.Equal(-0.4892152, client.CameraFront.Z, 7); - Assert.Equal(@"{""name"":""Reiga Fiercecrusher"",""profession"":2,""spec"":18,""race"":1,""map_id"":1206,""world_id"":268435460,""team_color_id"":0,""commander"":false,""map"":1206,""fov"":0.960,""uisz"":1}", client.RawIdentity); - Assert.Equal("Reiga Fiercecrusher", client.CharacterName); - Assert.Equal(ProfessionType.Warrior, client.Profession); - Assert.Equal(RaceType.Charr, client.Race); - Assert.Equal(18, client.Specialization); - Assert.Equal(0, client.TeamColorId); - Assert.False(client.IsCommander); - Assert.Equal(0.960, client.FieldOfView); - Assert.Equal(UiSize.Normal, client.UiSize); - Assert.Equal("18.197.217.165", client.ServerAddress); - Assert.Equal(0, client.ServerPort); - Assert.Equal(1206, client.MapId); - Assert.Equal(MapType.PublicMini, client.MapType); - Assert.Equal(268435460u, client.ShardId); - Assert.Equal(0u, client.Instance); - Assert.Equal(99552, client.BuildId); - Assert.False(client.IsMapOpen); - Assert.False(client.IsCompassTopRight); - Assert.True(client.IsCompassRotationEnabled); - Assert.True(client.DoesGameHaveFocus); - Assert.True(client.IsCompetitiveMode); - Assert.True(client.DoesAnyInputHaveFocus); - Assert.False(client.IsInCombat); - Assert.Equal(362, client.Compass.Width); - Assert.Equal(229, client.Compass.Height); - Assert.Equal(-2.11212, client.CompassRotation, 5); - Assert.Equal(14400.01, client.PlayerLocationMap.X, 2); - Assert.Equal(18180.19, client.PlayerLocationMap.Y, 2); - Assert.Equal(14400.01, client.MapCenter.X, 2); - Assert.Equal(18180.19, client.MapCenter.Y, 2); - Assert.Equal(1, client.MapScale); - Assert.Equal(15101u, client.ProcessId); - Assert.Equal(MountType.Griffon, client.Mount); + reader.Received(1).Dispose(); } - [SkippableFact] + [Fact] public void DisposeCorrectlyTest() { - // Named memory mapped files aren't supported on Unix based systems. - // So we need to skip this test. - Skip.IfNot(this.isWindows, "Mumble Link is only supported in Windows"); - - var client = new Gw2MumbleClient(); + using var reader = Substitute.For(); + var client = new Gw2MumbleClient(_ => reader); client.Dispose(); - Assert.ThrowsAny(() => client.Update()); + + Action act = () => client.Update(); + act.Should().Throw(); } - [SkippableFact] + [Fact] public void DisposeChildOnlyCorrectlyTest() { - // Named memory mapped files aren't supported on Unix based systems. - // So we need to skip this test. - Skip.IfNot(this.isWindows, "Mumble Link is only supported in Windows"); - - using var rootClient = new Gw2MumbleClient(); + using var reader = Substitute.For(); + using var rootClient = new Gw2MumbleClient(_ => reader); var childClientA = rootClient["CinderSteeltemper"]; var childClientB = rootClient["VishenSteelshot"]; childClientA.Dispose(); - Assert.ThrowsAny(() => childClientA.Update()); - childClientB.Update(); // Sibling should not be disposed - rootClient.Update(); // Root should not be disposed + + using (new AssertionScope()) + { + Action actRoot = () => rootClient.Update(); + Action actChildA = () => childClientA.Update(); + Action actChildB = () => childClientB.Update(); + + // Only child A should be disposed + actRoot.Should().NotThrow(); + actChildA.Should().Throw(); + actChildB.Should().NotThrow(); + } } - [SkippableFact] + [Fact] public void DisposeAllFromRootCorrectlyTest() { - // Named memory mapped files aren't supported on Unix based systems. - // So we need to skip this test. - Skip.IfNot(this.isWindows, "Mumble Link is only supported in Windows"); - - var rootClient = new Gw2MumbleClient(); + using var reader = Substitute.For(); + var rootClient = new Gw2MumbleClient(_ => reader); var childClientA = rootClient["CinderSteeltemper"]; var childClientB = rootClient["VishenSteelshot"]; rootClient.Dispose(); - Assert.ThrowsAny(() => rootClient.Update()); - Assert.ThrowsAny(() => childClientA.Update()); - Assert.ThrowsAny(() => childClientB.Update()); + + using (new AssertionScope()) + { + Action actRoot = () => rootClient.Update(); + Action actChildA = () => childClientA.Update(); + Action actChildB = () => childClientB.Update(); + + // Everything should be disposed + actRoot.Should().Throw(); + actChildA.Should().Throw(); + actChildB.Should().Throw(); + } + } + + [Theory] + [AutoData] + public void UsesSameInstanceForSameNamesTest(string mumbleLinkName) + { + using var client = new Gw2MumbleClient(x => Substitute.For()); + var childClientA = client[mumbleLinkName]; + var childClientB = client[mumbleLinkName]; + var childClientAA = childClientA[mumbleLinkName]; + + using (new AssertionScope()) + { + childClientA.Should().BeSameAs(childClientB); + childClientA.Should().BeSameAs(childClientAA); + } } } } diff --git a/Gw2Sharp/Connection.cs b/Gw2Sharp/Connection.cs index d110d998c..f6e68e19a 100644 --- a/Gw2Sharp/Connection.cs +++ b/Gw2Sharp/Connection.cs @@ -4,10 +4,12 @@ using System.Collections.Specialized; using System.Diagnostics; using System.Net.Http.Headers; +using Gw2Sharp.Mumble; using Gw2Sharp.WebApi; using Gw2Sharp.WebApi.Caching; using Gw2Sharp.WebApi.Http; using Gw2Sharp.WebApi.Middleware; +using static Gw2Sharp.Mumble.IGw2MumbleClientReader; namespace Gw2Sharp { @@ -100,6 +102,21 @@ public Connection( this.middleware.CollectionChanged += this.Middleware_CollectionChanged; this.UseDefaultMiddleware(); + +#if NET5_0_OR_GREATER + if (OperatingSystem.IsWindows()) +#else + if (Environment.OSVersion.Platform == PlatformID.Win32NT) +#endif + { +#pragma warning disable CA1416 // We can't get a different platform than Windows here, but the analyzer doesn't understand this + this.MumbleClientReaderFactory = x => new Gw2MumbleClientReader(x); +#pragma warning restore CA1416 + } + else + { + this.MumbleClientReaderFactory = _ => new UnsupportedMumblePlatformClientReader(); + } } @@ -179,6 +196,10 @@ public ICacheMethod RenderCacheMethod /// public int MiddlewareHashCode { get; protected set; } + /// + public Gw2MumbleLinkReaderFactory MumbleClientReaderFactory { get; set; } + + /// /// Resets this connection's middleware to the default list. /// diff --git a/Gw2Sharp/Gw2Client.cs b/Gw2Sharp/Gw2Client.cs index 29474dc65..34faa34f1 100644 --- a/Gw2Sharp/Gw2Client.cs +++ b/Gw2Sharp/Gw2Client.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.Versioning; using Gw2Sharp.Mumble; using Gw2Sharp.WebApi; @@ -10,7 +9,7 @@ namespace Gw2Sharp /// public class Gw2Client : IGw2Client { - private readonly IGw2MumbleClient? mumble; + private readonly IGw2MumbleClient mumble; private readonly IGw2WebApiClient webApi; /// @@ -28,21 +27,12 @@ public Gw2Client(IConnection connection) if (connection == null) throw new ArgumentNullException(nameof(connection)); -#if NET5_0_OR_GREATER - if (OperatingSystem.IsWindows()) - this.mumble = new Gw2MumbleClient(); -#else - this.mumble = new Gw2MumbleClient(); -#endif + this.mumble = new Gw2MumbleClient(connection.MumbleClientReaderFactory); this.webApi = new Gw2WebApiClient(connection, this); } /// -#if NET5_0_OR_GREATER - [SupportedOSPlatform("windows")] -#endif - public virtual IGw2MumbleClient Mumble => - this.mumble ?? throw new PlatformNotSupportedException("Mumble Link is only available on Windows platforms"); + public virtual IGw2MumbleClient Mumble => this.mumble; /// public virtual IGw2WebApiClient WebApi => this.webApi; @@ -57,15 +47,15 @@ public Gw2Client(IConnection connection) /// Dispose managed resources. protected virtual void Dispose(bool disposing) { - if (!this.isDisposed) - { - if (disposing) - { - this.mumble?.Dispose(); - } + if (this.isDisposed) + return; - this.isDisposed = true; + if (disposing) + { + this.mumble?.Dispose(); } + + this.isDisposed = true; } /// diff --git a/Gw2Sharp/IConnection.cs b/Gw2Sharp/IConnection.cs index cafaf523c..624828b59 100644 --- a/Gw2Sharp/IConnection.cs +++ b/Gw2Sharp/IConnection.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; using System.Threading; +using Gw2Sharp.Mumble; using Gw2Sharp.WebApi; using Gw2Sharp.WebApi.Caching; using Gw2Sharp.WebApi.Http; using Gw2Sharp.WebApi.Middleware; +using static Gw2Sharp.Mumble.IGw2MumbleClientReader; namespace Gw2Sharp { @@ -55,13 +57,11 @@ public interface IConnection /// /// Gets the HTTP client that's used for the API requests. /// - /// value is . IHttpClient HttpClient { get; } /// /// Gets the cache controller that's used for API requests. /// - /// value is . ICacheMethod CacheMethod { get; } /// @@ -73,7 +73,6 @@ public interface IConnection /// /// Gets the cache controller that's used for render file API requests. /// - /// value is . ICacheMethod RenderCacheMethod { get; } /// @@ -87,5 +86,12 @@ public interface IConnection /// This value can be used to determine if the list has changed. /// int MiddlewareHashCode { get; } + + /// + /// Gets the Mumble client reader factory. + /// This factory has the Mumble Link name as parameter, and should return a new instance of . + /// Defaults to on Windows. + /// + Gw2MumbleLinkReaderFactory MumbleClientReaderFactory { get; } } } diff --git a/Gw2Sharp/IGw2Client.cs b/Gw2Sharp/IGw2Client.cs index 06f983ec0..892e7fbb8 100644 --- a/Gw2Sharp/IGw2Client.cs +++ b/Gw2Sharp/IGw2Client.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.Versioning; using Gw2Sharp.Mumble; using Gw2Sharp.WebApi; @@ -13,10 +12,6 @@ public interface IGw2Client : IDisposable /// /// Gets the Mumble Link client API. /// - /// Mumble Link is not available on non-Windows platforms. -#if NET5_0_OR_GREATER - [SupportedOSPlatform("windows")] -#endif IGw2MumbleClient Mumble { get; } /// diff --git a/Gw2Sharp/Mumble/Gw2MumbleClient.cs b/Gw2Sharp/Mumble/Gw2MumbleClient.cs index e3e94a260..29116160e 100644 --- a/Gw2Sharp/Mumble/Gw2MumbleClient.cs +++ b/Gw2Sharp/Mumble/Gw2MumbleClient.cs @@ -6,6 +6,7 @@ using Gw2Sharp.Json; using Gw2Sharp.Models; using Gw2Sharp.Mumble.Models; +using static Gw2Sharp.Mumble.IGw2MumbleClientReader; namespace Gw2Sharp.Mumble { @@ -33,6 +34,9 @@ public class Gw2MumbleClient : IGw2MumbleClient private readonly object serverAddressLock = new object(); private Gw2LinkedMem linkedMem; + private readonly Gw2MumbleLinkReaderFactory readerFactory; + private readonly IGw2MumbleClientReader reader; + private readonly bool isRoot = false; private readonly ConcurrentDictionary> mumbleClientCache; private readonly string mumbleLinkName; @@ -40,21 +44,19 @@ public class Gw2MumbleClient : IGw2MumbleClient /// /// Creates a new . /// + /// The reader factory. /// The Mumble Link name. /// The parent Mumble Link client to track child objects. - protected internal Gw2MumbleClient(string mumbleLinkName = DEFAULT_MUMBLE_LINK_MAP_NAME, Gw2MumbleClient? parent = null) + protected internal Gw2MumbleClient(Gw2MumbleLinkReaderFactory readerFactory, string mumbleLinkName = DEFAULT_MUMBLE_LINK_MAP_NAME, Gw2MumbleClient? parent = null) { + this.readerFactory = readerFactory ?? throw new ArgumentNullException(nameof(readerFactory)); + this.reader = this.readerFactory(mumbleLinkName); + this.isRoot = parent is null; + this.mumbleClientCache = parent?.mumbleClientCache ?? new ConcurrentDictionary>(); this.mumbleLinkName = !string.IsNullOrEmpty(mumbleLinkName) ? mumbleLinkName : DEFAULT_MUMBLE_LINK_MAP_NAME; if (this.mumbleLinkName == DEFAULT_MUMBLE_LINK_MAP_NAME) this.mumbleClientCache.TryAdd(DEFAULT_MUMBLE_LINK_MAP_NAME, new WeakReference(this, false)); - -#if NET5_0_OR_GREATER - if (OperatingSystem.IsWindows()) -#else - if (Environment.OSVersion.Platform == PlatformID.Win32NT) -#endif - this.Reader = new Gw2MumbleClientReader(); } private unsafe void UpdateIdentityIfNeeded() @@ -105,21 +107,17 @@ private CharacterIdentity? Identity } - /// - public IGw2MumbleClientReader? Reader { get; private set; } - - /// public IGw2MumbleClient this[string name] { get { var reference = this.mumbleClientCache.GetOrAdd(name, - x => new WeakReference(new Gw2MumbleClient(name, this), false)); + x => new WeakReference(new Gw2MumbleClient(this.readerFactory, name, this), false)); if (!reference.TryGetTarget(out var client)) { - client = new Gw2MumbleClient(name, this); + client = new Gw2MumbleClient(this.readerFactory, name, this); reference.SetTarget(client); } return client; @@ -342,12 +340,10 @@ public unsafe void Update() { if (this.isDisposed) throw new ObjectDisposedException(nameof(Gw2MumbleClient)); - if (this.Reader is null) - throw new InvalidOperationException("No reader was defined. Make sure to set the reader through .WithReader() before calling .Update()"); - if (!this.Reader.IsOpen) - this.Reader.Open(this.mumbleLinkName); - var mem = this.Reader.Read(); + if (!this.reader.IsOpen) + this.reader.Open(); + var mem = this.reader.Read(); int oldTick = this.Tick; if (mem.uiTick != oldTick) @@ -370,13 +366,6 @@ public unsafe void Update() this.linkedMem = mem; } - /// - public IGw2MumbleClient WithReader() where T : IGw2MumbleClientReader, new() - { - this.Reader = new T(); - return this; - } - #region IDisposable Support @@ -393,9 +382,9 @@ protected virtual void Dispose(bool disposing) if (disposing) { - this.Reader?.Dispose(); + this.reader.Dispose(); - // Only dispose the full client cache tree if we are the default one + // Only dispose the full client cache tree if we are the default one and are the root if (this.mumbleLinkName == DEFAULT_MUMBLE_LINK_MAP_NAME) { foreach (var reference in this.mumbleClientCache.Select(x => x.Value)) diff --git a/Gw2Sharp/Mumble/Gw2MumbleClientReader.cs b/Gw2Sharp/Mumble/Gw2MumbleClientReader.cs index e96a3d095..ab57452d0 100644 --- a/Gw2Sharp/Mumble/Gw2MumbleClientReader.cs +++ b/Gw2Sharp/Mumble/Gw2MumbleClientReader.cs @@ -15,11 +15,16 @@ public class Gw2MumbleClientReader : IGw2MumbleClientReader private MemoryMappedFile? file; private MemoryMappedViewAccessor? accessor; + private readonly string mumbleLinkName; + /// /// Creates a new . /// + /// The Mumble Link name. /// Only Windows operating systems are supported. - public Gw2MumbleClientReader() + /// is . + /// is an empty string or only contains whitespaces. + public Gw2MumbleClientReader(string mumbleLinkName) { #if NET5_0_OR_GREATER if (!OperatingSystem.IsWindows()) @@ -27,15 +32,22 @@ public Gw2MumbleClientReader() if (Environment.OSVersion.Platform != PlatformID.Win32NT) #endif throw new PlatformNotSupportedException("Only Windows operating systems support the Mumble Link"); + + if (mumbleLinkName is null) + throw new ArgumentNullException(nameof(mumbleLinkName)); + if (string.IsNullOrWhiteSpace(mumbleLinkName)) + throw new ArgumentException($"'{nameof(mumbleLinkName)}' may not be empty or only contain whitespaces", nameof(mumbleLinkName)); + + this.mumbleLinkName = mumbleLinkName; } /// public bool IsOpen { get; private set; } /// - public void Open(string mumbleLinkName) + public void Open() { - this.file = MemoryMappedFile.CreateOrOpen(mumbleLinkName, Gw2LinkedMem.SIZE, MemoryMappedFileAccess.ReadWrite); + this.file = MemoryMappedFile.CreateOrOpen(this.mumbleLinkName, Gw2LinkedMem.SIZE, MemoryMappedFileAccess.ReadWrite); this.accessor = this.file.CreateViewAccessor(); this.IsOpen = true; } @@ -64,7 +76,7 @@ public Gw2LinkedMem Read() } -#region IDisposable Support + #region IDisposable Support private bool isDisposed = false; // To detect redundant calls @@ -92,6 +104,6 @@ public void Dispose() GC.SuppressFinalize(this); } -#endregion + #endregion } } diff --git a/Gw2Sharp/Mumble/IGw2MumbleClient.cs b/Gw2Sharp/Mumble/IGw2MumbleClient.cs index 1009fe2a8..d894e5fed 100644 --- a/Gw2Sharp/Mumble/IGw2MumbleClient.cs +++ b/Gw2Sharp/Mumble/IGw2MumbleClient.cs @@ -10,11 +10,6 @@ namespace Gw2Sharp.Mumble /// public interface IGw2MumbleClient : IDisposable { - /// - /// The Mumble client reader instance. - /// - IGw2MumbleClientReader Reader { get; } - /// /// Gets a custom named Mumble Link client API. /// @@ -248,12 +243,5 @@ public interface IGw2MumbleClient : IDisposable /// Call this every frame and/or every time you want to update the Mumble Link data before reading. /// void Update(); - - /// - /// Sets a custom Mumble client reader. - /// - /// The reader type. - /// This instance. - IGw2MumbleClient WithReader() where T : IGw2MumbleClientReader, new(); } } diff --git a/Gw2Sharp/Mumble/IGw2MumbleClientReader.cs b/Gw2Sharp/Mumble/IGw2MumbleClientReader.cs index 43b35ad7e..06cfab1a8 100644 --- a/Gw2Sharp/Mumble/IGw2MumbleClientReader.cs +++ b/Gw2Sharp/Mumble/IGw2MumbleClientReader.cs @@ -7,6 +7,13 @@ namespace Gw2Sharp.Mumble /// public interface IGw2MumbleClientReader : IDisposable { + /// + /// A delegate for creating a new with a Mumble Link name as parameter. + /// + /// The Mumble Link name. + /// A new instance specifically for this Mumble Link name. + public delegate IGw2MumbleClientReader Gw2MumbleLinkReaderFactory(string mumbleLinkName); + /// /// Whether this reader is open. /// @@ -15,8 +22,7 @@ public interface IGw2MumbleClientReader : IDisposable /// /// Opens the reader. /// - /// The Mumble Link name. - void Open(string mumbleLinkName); + void Open(); /// /// Closes the reader. diff --git a/Gw2Sharp/Mumble/UnsupportedMumblePlatformClientReader.cs b/Gw2Sharp/Mumble/UnsupportedMumblePlatformClientReader.cs new file mode 100644 index 000000000..007b634c3 --- /dev/null +++ b/Gw2Sharp/Mumble/UnsupportedMumblePlatformClientReader.cs @@ -0,0 +1,38 @@ +using System; + +namespace Gw2Sharp.Mumble +{ + /// + /// An unsupported Mumble platform client reader. + /// Does nothing besides throwing for each method. + /// + internal sealed class UnsupportedMumblePlatformClientReader : IGw2MumbleClientReader + { + /// + public bool IsOpen => + throw new PlatformNotSupportedException($"Mumble Link is not supported on {Environment.OSVersion.Platform}"); + + /// + public void Open() => + throw new PlatformNotSupportedException($"Mumble Link is not supported on {Environment.OSVersion.Platform}"); + + /// + public void Close() => + throw new PlatformNotSupportedException($"Mumble Link is not supported on {Environment.OSVersion.Platform}"); + + /// + public Gw2LinkedMem Read() => + throw new PlatformNotSupportedException($"Mumble Link is not supported on {Environment.OSVersion.Platform}"); + + + #region IDisposable Support + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + } + + #endregion + } +} From 1f39c8161787cb29f0fe39171d547d2a0f26cd27 Mon Sep 17 00:00:00 2001 From: Archomeda Date: Mon, 15 Nov 2021 12:20:51 +0100 Subject: [PATCH 3/5] Add changelog notes --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca5e5efe2..b9304823b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,18 @@ # Gw2Sharp History ## 1.6.0 (15 November 2021) -This release includes a feature to change the API hostname(s) in case you want to run a proxy API server. See [the documentation](https://archomeda.github.io/Gw2Sharp/master/guides/proxy.html) for more information. +This release includes support for a couple of additional customizations: +- Ability to change the API hostname(s) in case you want to run a proxy API server (see [the documentation](https://archomeda.github.io/Gw2Sharp/master/guides/proxy.html) for more information) +- Ability to change the Mumble Link reader for mocking, or to use a different method and/or source for Mumble Link ### HTTP -- Add `ApiBaseUrl` and `RenderBaseUrl` properties to `Gw2Sharp.IConnection` ([#108](https://github.com/Archomeda/Gw2Sharp/issues/108), [#107](https://github.com/Archomeda/Gw2Sharp/pull/109)) +- Add `ApiBaseUrl` and `RenderBaseUrl` properties to `Gw2Sharp.IConnection` ([#108](https://github.com/Archomeda/Gw2Sharp/issues/108), [#109](https://github.com/Archomeda/Gw2Sharp/pull/109)) + +### Mumble Link +- Add support for custom Mumble Link readers ([#110](https://github.com/Archomeda/Gw2Sharp/pull/110)) + - Add `MumbleClientReaderFactory` property to `Gw2Sharp.IConnection` that constructs a new Mumble Link reader that implements `Gw2Sharp.Mumble.IGw2MumbleClientReader` + - .NET 5 and up: `SupportedOSPlatformAttribute`s have been moved around to support these changes + - The following structs that are used in the reader have been made public: `Gw2Sharp.Mumble.Gw2LinkedMem`, `Gw2Sharp.Mumble.Gw2Context` and `Gw2Sharp.Mumble.Models.UiState` --- From a7be0c55f15b24f3c7e5a4418113a6297ef5a8a0 Mon Sep 17 00:00:00 2001 From: Archomeda Date: Mon, 15 Nov 2021 12:29:46 +0100 Subject: [PATCH 4/5] Add isRoot check --- Gw2Sharp/Mumble/Gw2MumbleClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gw2Sharp/Mumble/Gw2MumbleClient.cs b/Gw2Sharp/Mumble/Gw2MumbleClient.cs index 29116160e..4b00a4be8 100644 --- a/Gw2Sharp/Mumble/Gw2MumbleClient.cs +++ b/Gw2Sharp/Mumble/Gw2MumbleClient.cs @@ -385,7 +385,7 @@ protected virtual void Dispose(bool disposing) this.reader.Dispose(); // Only dispose the full client cache tree if we are the default one and are the root - if (this.mumbleLinkName == DEFAULT_MUMBLE_LINK_MAP_NAME) + if (this.mumbleLinkName == DEFAULT_MUMBLE_LINK_MAP_NAME && this.isRoot) { foreach (var reference in this.mumbleClientCache.Select(x => x.Value)) { From 6dac371849f2ae21d4fabf569deed3d5905cf0d4 Mon Sep 17 00:00:00 2001 From: Archomeda Date: Thu, 30 Dec 2021 21:01:56 +0100 Subject: [PATCH 5/5] Update release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9304823b..eae603aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Gw2Sharp History -## 1.6.0 (15 November 2021) +## 1.6.0 (30 December 2021) This release includes support for a couple of additional customizations: - Ability to change the API hostname(s) in case you want to run a proxy API server (see [the documentation](https://archomeda.github.io/Gw2Sharp/master/guides/proxy.html) for more information) - Ability to change the Mumble Link reader for mocking, or to use a different method and/or source for Mumble Link