Skip to content

Commit

Permalink
Merge pull request #59 from Archomeda/feature/mumble-link-updates
Browse files Browse the repository at this point in the history
Update Mumble Link with new fields and add support for custom names
  • Loading branch information
Archomeda authored Apr 28, 2020
2 parents c9d5f7d + f48abd1 commit e129bdd
Show file tree
Hide file tree
Showing 12 changed files with 325 additions and 30 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Gw2Sharp History

## 0.9.6
### Services
- Update MumbleLink to support new features ([#58](https://github.com/Archomeda/Gw2Sharp/issues/58), [#59](https://github.com/Archomeda/Gw2Sharp/pull/59)):
- Process id
- Mount
- UI states: IsInCombat
- Add support for custom MumbleLink names through `IGw2MumbleClient[name]` (see documentation for more details)

## 0.9.5
### Endpoints
- Add the following missing properties in `Gw2Sharp.WebApi.V2.Models.Skill`:
Expand Down
28 changes: 26 additions & 2 deletions Gw2Sharp.DocFX/guides/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ var tybaltsSpecializations = await client.WebApi.V2.Characters["Tybalt Leftpaw"]
- [Exception handling](xref:Guides.ExceptionHandling)
- [Supported endpoints](xref:Guides.Endpoints)

---

## Render service
Accessing the render service is done through the main `Gw2Sharp.Gw2Client`:
```cs
Expand All @@ -113,6 +115,8 @@ await renderClient.DownloadToStreamAsync(appleStream, appleUrl);
- [Exception handling](xref:Guides.ExceptionHandling)
- [Using RenderUrl](xref:Guides.RenderUrl)

---

## Mumble Link
Accessing the Mumble Link service is done through the main `Gw2Sharp.Gw2Client`:
```cs
Expand All @@ -139,8 +143,28 @@ In order to keep performance to a maximum, the following JSON properties are onl
- `FieldOfView`
- `UiSize`

If you need the performance, it's recommended to not request these values on every update, but to cache them locally, and only update once in a while.
Any other value is safe to be read every tick without taking any additional performance hit.
*If you need the performance, it's recommended to not request these values on every update, but to cache them locally, and only update once in a while.*
*Any other value is safe to be read every tick without taking any additional performance hit.*

### Custom Mumble Link names
Support for custom Mumble Link names has been added with the April 28th, 2020 update.
This means that you can use any custom name that's different from the default (which is `MumbleLink`).
In order to access any custom named Mumble Link, you can do the following:

```cs
var customMumbleClient = client.Mumble["AnyCustomNameHere"];

// You can treat the custom client the same as the normal client in terms of accessible data
```

All calls to the indexer are redirected to an internal cache of the used clients.
This is to make sure that only one instance of a given name is created to save resources, and that when the root client is disposed, all the child clients are disposed as well.
If just a child client is disposed however, the root client is untouched.

*Keep in mind that you don't want to dispose the root client manually.*
*This is done automatically for you whenever you dispose the `IGw2Client` itself.*

---

## Chat links
Accessing the chat links is a bit different compared to the other services.
Expand Down
55 changes: 54 additions & 1 deletion Gw2Sharp.Tests/Mumble/Gw2MumbleClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void ReadStructCorrectlyTest()
var gw2Client = Substitute.For<IGw2Client>();

using var memorySource = Assembly.GetExecutingAssembly().GetManifestResourceStream($"Gw2Sharp.Tests.TestFiles.Mumble.MemoryMappedFile.bin");
using var memoryMappedFile = MemoryMappedFile.CreateOrOpen(Gw2MumbleClient.MUMBLE_LINK_MAP_NAME, memorySource.Length);
using var memoryMappedFile = MemoryMappedFile.CreateOrOpen(Gw2MumbleClient.DEFAULT_MUMBLE_LINK_MAP_NAME, memorySource.Length);
using var stream = memoryMappedFile.CreateViewStream();
memorySource.CopyTo(stream);

Expand Down Expand Up @@ -66,6 +66,7 @@ public void ReadStructCorrectlyTest()
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);
Expand All @@ -74,6 +75,58 @@ public void ReadStructCorrectlyTest()
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);
}

[SkippableFact]
public void DisposeCorrectlyTest()
{
// Named memory mapped files aren't supported on Unix based systems.
// So we need to skip this test.
Skip.IfNot(Environment.OSVersion.Platform == PlatformID.Win32NT, "Mumble Link is only supported in Windows");

var connection = Substitute.For<IConnection>();
var gw2Client = Substitute.For<IGw2Client>();
var client = new Gw2MumbleClient(connection, gw2Client);
client.Dispose();
Assert.ThrowsAny<ObjectDisposedException>(() => client.Update());
}

[SkippableFact]
public void DisposeChildOnlyCorrectlyTest()
{
// Named memory mapped files aren't supported on Unix based systems.
// So we need to skip this test.
Skip.IfNot(Environment.OSVersion.Platform == PlatformID.Win32NT, "Mumble Link is only supported in Windows");

var connection = Substitute.For<IConnection>();
var gw2Client = Substitute.For<IGw2Client>();
using var rootClient = new Gw2MumbleClient(connection, gw2Client);
var childClientA = rootClient["CinderSteeltemper"];
var childClientB = rootClient["VishenSteelshot"];
childClientA.Dispose();
Assert.ThrowsAny<ObjectDisposedException>(() => childClientA.Update());
childClientB.Update(); // Sibling should not be disposed
rootClient.Update(); // Root should not be disposed
}

[SkippableFact]
public void DisposeAllFromRootCorrectlyTest()
{
// Named memory mapped files aren't supported on Unix based systems.
// So we need to skip this test.
Skip.IfNot(Environment.OSVersion.Platform == PlatformID.Win32NT, "Mumble Link is only supported in Windows");

var connection = Substitute.For<IConnection>();
var gw2Client = Substitute.For<IGw2Client>();
var rootClient = new Gw2MumbleClient(connection, gw2Client);
var childClientA = rootClient["CinderSteeltemper"];
var childClientB = rootClient["VishenSteelshot"];
rootClient.Dispose();
Assert.ThrowsAny<ObjectDisposedException>(() => rootClient.Update());
Assert.ThrowsAny<ObjectDisposedException>(() => childClientA.Update());
Assert.ThrowsAny<ObjectDisposedException>(() => childClientB.Update());
}
}
}
Binary file modified Gw2Sharp.Tests/TestFiles/Mumble/MemoryMappedFile.bin
Binary file not shown.
2 changes: 1 addition & 1 deletion Gw2Sharp/IGw2Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Gw2Sharp
public interface IGw2Client : IDisposable
{
/// <summary>
/// Gets the Mumble Link API.
/// Gets the Mumble Link client API.
/// </summary>
IGw2MumbleClient Mumble { get; }

Expand Down
54 changes: 54 additions & 0 deletions Gw2Sharp/Models/MountType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
namespace Gw2Sharp.Models
{
/// <summary>
/// Represents a mount.
/// Used by Mumble Link.
/// </summary>
public enum MountType : byte
{
/// <summary>
/// No mount.
/// </summary>
None,

/// <summary>
/// The jackal mount.
/// </summary>
Jackal,

/// <summary>
/// The griffon mount.
/// </summary>
Griffon,

/// <summary>
/// The springer mount.
/// </summary>
Springer,

/// <summary>
/// The skimmer mount.
/// </summary>
Skimmer,

/// <summary>
/// The raptor mount.
/// </summary>
Raptor,

/// <summary>
/// The roller beetle mount.
/// </summary>
RollerBeetle,

/// <summary>
/// The warclaw mount.
/// </summary>
Warclaw,

/// <summary>
/// The skyscale mount.
/// </summary>
Skyscale
}
}
5 changes: 5 additions & 0 deletions Gw2Sharp/Mumble/Gw2LinkedMem.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Net.Sockets;
using System.Runtime.InteropServices;
using Gw2Sharp.Models;
using Gw2Sharp.Mumble.Models;

#pragma warning disable IDE0044
Expand Down Expand Up @@ -93,6 +94,10 @@ internal unsafe struct Gw2Context
public float mapCenterY;
[FieldOffset(76)]
public float mapScale;
[FieldOffset(80)]
public uint processId;
[FieldOffset(84)]
public MountType mount;

// Total struct size is 256 bytes
}
Expand Down
68 changes: 65 additions & 3 deletions Gw2Sharp/Mumble/Gw2MumbleClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Net.Sockets;
using System.Text.Json;
using Gw2Sharp.Json;
Expand All @@ -22,7 +24,7 @@ public class Gw2MumbleClient : BaseClient, IGw2MumbleClient
PropertyNamingPolicy = SnakeCaseNamingPolicy.SnakeCase
};

internal const string MUMBLE_LINK_MAP_NAME = "MumbleLink";
internal const string DEFAULT_MUMBLE_LINK_MAP_NAME = "MumbleLink";
internal const string MUMBLE_LINK_GAME_NAME_GUILD_WARS_2 = "Guild Wars 2";
internal static readonly char[] mumbleLinkGameName = new[] { 'G', 'u', 'i', 'l', 'd', ' ', 'W', 'a', 'r', 's', ' ', '2', '\0' };
private const string EMPTY_IDENTITY = "{}";
Expand All @@ -36,21 +38,31 @@ public class Gw2MumbleClient : BaseClient, IGw2MumbleClient

private Gw2LinkedMem linkedMem;

private readonly ConcurrentDictionary<string, WeakReference<Gw2MumbleClient>> mumbleClientCache;
private readonly string mumbleLinkName;

/// <summary>
/// Creates a new <see cref="Gw2MumbleClient"/>.
/// </summary>
/// <param name="connection">The connection used to make requests, see <see cref="IConnection"/>.</param>
/// <param name="gw2Client">The Guild Wars 2 client.</param>
/// <param name="mumbleLinkName">The Mumble Link name.</param>
/// <param name="parent">The parent Mumble Link client to track child objects.</param>
/// <exception cref="ArgumentNullException"><paramref name="connection"/> or <paramref name="gw2Client"/> is <c>null</c>.</exception>
protected internal Gw2MumbleClient(IConnection connection, IGw2Client gw2Client) : base(connection, gw2Client)
protected internal Gw2MumbleClient(IConnection connection, IGw2Client gw2Client, string mumbleLinkName = DEFAULT_MUMBLE_LINK_MAP_NAME, Gw2MumbleClient? parent = null) : base(connection, gw2Client)
{
if (connection == null)
throw new ArgumentNullException(nameof(connection));
if (gw2Client == null)
throw new ArgumentNullException(nameof(gw2Client));

this.mumbleClientCache = parent?.mumbleClientCache ?? new ConcurrentDictionary<string, WeakReference<Gw2MumbleClient>>();
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<Gw2MumbleClient>(this, false));

this.memoryMappedFile = new Lazy<MemoryMappedFile>(
() => MemoryMappedFile.CreateOrOpen(MUMBLE_LINK_MAP_NAME, Gw2LinkedMem.SIZE, MemoryMappedFileAccess.ReadWrite), true);
() => MemoryMappedFile.CreateOrOpen(this.mumbleLinkName, Gw2LinkedMem.SIZE, MemoryMappedFileAccess.ReadWrite), true);
this.memoryMappedViewAccessor = new Lazy<MemoryMappedViewAccessor>(
() => this.memoryMappedFile.Value.CreateViewAccessor(), true);
}
Expand Down Expand Up @@ -102,6 +114,23 @@ private unsafe CharacterIdentity? Identity
}


/// <inheritdoc />
public IGw2MumbleClient this[string mumbleLinkName]
{
get
{
var reference = this.mumbleClientCache.GetOrAdd(mumbleLinkName,
x => new WeakReference<Gw2MumbleClient>(new Gw2MumbleClient(this.Connection, this.Gw2Client!, mumbleLinkName, this), false));

if (!reference.TryGetTarget(out var client))
{
client = new Gw2MumbleClient(this.Connection, this.Gw2Client!, mumbleLinkName, this);
reference.SetTarget(client);
}
return client;
}
}

/// <inheritdoc />
public bool IsAvailable { get; private set; }

Expand Down Expand Up @@ -217,6 +246,10 @@ public unsafe string ServerAddress
public bool DoesAnyInputHaveFocus =>
this.IsAvailable ? this.linkedMem.context.uiState.HasFlag(UiState.DoesAnyInputHaveFocus) : default;

/// <inheritdoc />
public bool IsInCombat =>
this.IsAvailable ? this.linkedMem.context.uiState.HasFlag(UiState.IsInCombat) : default;

/// <inheritdoc />
public Size Compass =>
this.IsAvailable ? new Size(this.linkedMem.context.compassWidth, this.linkedMem.context.compassHeight) : default;
Expand All @@ -237,6 +270,21 @@ public unsafe string ServerAddress
public double MapScale =>
this.IsAvailable ? this.linkedMem.context.mapScale : default;

/// <inheritdoc />
public uint ProcessId =>
this.IsAvailable ? this.linkedMem.context.processId : default;

/// <inheritdoc />
public MountType Mount
{
get
{
// Currently mount 10 is actually None
// Since this might be fixed later, we just redirect 10 to None for now
return this.IsAvailable && this.linkedMem.context.mount != (MountType)10 ? this.linkedMem.context.mount : default;
}
}


/// <inheritdoc />
public int MapId =>
Expand Down Expand Up @@ -331,6 +379,7 @@ public unsafe void Update()
this.linkedMem = linkedMem;
}


#region IDisposable Support

private bool isDisposed = false; // To detect redundant calls
Expand All @@ -349,6 +398,19 @@ protected virtual void Dispose(bool disposing)
this.memoryMappedViewAccessor.Value.Dispose();
if (this.memoryMappedFile.IsValueCreated)
this.memoryMappedFile.Value.Dispose();

// Only dispose the full client cache tree if we are the default one
if (this.mumbleLinkName == DEFAULT_MUMBLE_LINK_MAP_NAME)
{
foreach (var reference in this.mumbleClientCache.Select(x => x.Value))
{
if (reference.TryGetTarget(out var client) && client != this)
client?.Dispose();
}
this.mumbleClientCache.Clear();
}
// Otherwise only remove ourselves from the tree
this.mumbleClientCache.TryRemove(this.mumbleLinkName, out _);
}

this.isDisposed = true;
Expand Down
21 changes: 21 additions & 0 deletions Gw2Sharp/Mumble/IGw2MumbleClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ namespace Gw2Sharp.Mumble
/// </summary>
public interface IGw2MumbleClient : IClient, IDisposable
{
/// <summary>
/// Gets a custom named Mumble Link client API.
/// </summary>
/// <param name="mumbleLinkName">The custom name.</param>
/// <returns>The Mumble Link client API.</returns>
IGw2MumbleClient this[string mumbleLinkName] { get; }

/// <summary>
/// Whether the Guild Wars 2 Mumble Link API is available or not.
/// Use this to check if the Mumble Link API contains valid Guild Wars 2 data.
Expand Down Expand Up @@ -106,6 +113,11 @@ public interface IGw2MumbleClient : IClient, IDisposable
/// </summary>
bool DoesAnyInputHaveFocus { get; }

/// <summary>
/// Whether the player is currently in combat.
/// </summary>
bool IsInCombat { get; }

/// <summary>
/// The compass size.
/// </summary>
Expand Down Expand Up @@ -133,6 +145,15 @@ public interface IGw2MumbleClient : IClient, IDisposable
/// </summary>
double MapScale { get; }

/// <summary>
/// The client process id.
/// </summary>
uint ProcessId { get; }

/// <summary>
/// The mount that's currently used by the player.
/// </summary>
MountType Mount { get; }

/// <summary>
/// The current map id.
Expand Down
Loading

0 comments on commit e129bdd

Please sign in to comment.