Skip to content

Commit

Permalink
Merge pull request #1885 from Wibble199/feature/autojsonnode
Browse files Browse the repository at this point in the history
Automatic JSON nodes
  • Loading branch information
diogotr7 authored Mar 25, 2020
2 parents 86a13ac + cb92733 commit d143bb1
Show file tree
Hide file tree
Showing 32 changed files with 310 additions and 436 deletions.
100 changes: 100 additions & 0 deletions Project-Aurora/Project-Aurora/Profiles/AutoJsonNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using static System.Linq.Expressions.Expression;

namespace Aurora.Profiles {

/// <summary>
/// A version of <see cref="Node{TClass}"/> which automatically populates the fields defined on it from the parsed JSON data.
/// </summary>
public class AutoJsonNode<TSelf> : Node<TSelf> where TSelf : AutoJsonNode<TSelf> {
// Did consider implementing this auto feature as a Fody weaver however, should profiles become plugin-based, each plugin would need to use Fody if they
// wished to have the automatic capability. Doing it as a class that can be extended means that no additional setup is required for plugin authors.

public AutoJsonNode() : base() { }
public AutoJsonNode(string json) : base(json) {
ctorAction.Value((TSelf)this);
}

#region Constructor builder
// Compiled action to be run during the contructor that will populate relevant fields
private static readonly Lazy<Action<TSelf>> ctorAction = new Lazy<Action<TSelf>>(() => {
var fields = typeof(TSelf).GetFields(bf | BindingFlags.FlattenHierarchy);
var body = new List<Expression>(fields.Length);
var selfParam = Parameter(typeof(TSelf));

// Find all the fields
foreach (var field in fields.Where(f => f.GetCustomAttribute<AutoJsonIgnoreAttribute>() == null)) {
if (TryGetMethodForType(field.FieldType, out var getter))
// If a relevant Getter method exists for this field, add an assignment to the ctor body for this (e.g. adding `this.SomeField = GetString("SomeField");` )
body.Add(
Assign(
Field(selfParam, field),
Call(selfParam, getter, Constant(field.GetCustomAttribute<AutoJsonPropertyNameAttribute>()?.Path ?? field.Name))
)
);
else
Global.logger.Warn($"Could not find an AutoNode getter method for field '{field.Name}' of type '{field.FieldType.Name}'. It will not be automatically populated.");
}

// Compile and return the action
return Lambda<Action<TSelf>>(Block(body), selfParam).Compile();
});
#endregion

#region Getter methods
private static BindingFlags bf = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;

// An auto-generated list of get methods on Node<TSelf>
private static readonly Dictionary<Type, MethodInfo> methods = typeof(TSelf).GetMethods(bf)
// Only count methods that return something, take a single string parameter and whose names start with Get. E.G. bool GetBool(string name) would be a method that is returned
.Where(m => !m.IsSpecialName && m.Name.StartsWith("Get") && m.ReturnType != typeof(void) && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == typeof(string))
.ToDictionary(m => m.ReturnType, m => m);

// Special methods (with signatures that don't match the others)
private static readonly MethodInfo arrayMethod = typeof(TSelf).GetMethod("GetArray", bf);
private static readonly MethodInfo enumMethod = typeof(TSelf).GetMethod("GetEnum", bf);

/// <summary>
/// Tries to get the relevant get method on <see cref="Node{TClass}"/> for the given data type. Returns false if no method found.<para/>
/// Examples:<code>
/// GetMethodForType(typeof(string)); // returns 'GetString'<br/>
/// GetMethodForType(typeof(SomeEnum)); // returns the closed generic variant of 'GetEnum&lt;T&gt;' bound to the given enum.
/// </code></summary>
private static bool TryGetMethodForType(Type type, out MethodInfo method) {
if (type.IsEnum)
method = enumMethod.MakeGenericMethod(type);
else if (type.IsArray)
method = arrayMethod.MakeGenericMethod(type.GetElementType());
else if (methods.TryGetValue(type, out var mi))
method = mi;
else
method = null;
return method != null;
}
#endregion
}


#region Attributes
/// <summary>
/// Attribute to mark a field to indicate that the <see cref="AutoJsonNode{TSelf}"/> should use a different path when accessing the JSON.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class AutoJsonPropertyNameAttribute : Attribute {
public string Path { get; set; }
public AutoJsonPropertyNameAttribute(string path) {
Path = path ?? throw new ArgumentNullException(nameof(path));
}
}

/// <summary>
/// Attribute to mark a field to indicate that the <see cref="AutoJsonNode{TSelf}"/> should ignore this field when populating the class members.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class AutoJsonIgnoreAttribute : Attribute { }
#endregion
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Aurora.Profiles.Discord.GSI.Nodes;
using Aurora.Profiles.Generic.GSI.Nodes;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
Expand Down

This file was deleted.

14 changes: 12 additions & 2 deletions Project-Aurora/Project-Aurora/Profiles/GameState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using NAudio.CoreAudioApi;
using Aurora.Utils;

namespace Aurora.Profiles
{
Expand Down Expand Up @@ -44,10 +45,13 @@ public interface IGameState
String GetNode(string name);
}

public class GameState<T> : StringProperty<T>, IGameState where T : GameState<T>
public class GameState<TSelf> : StringProperty<TSelf>, IGameState where TSelf : GameState<TSelf>
{
private static LocalPCInformation _localpcinfo;

// Holds a cache of the child nodes on this gamestate
private readonly Dictionary<string, object> childNodes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Information about the local system
/// </summary>
Expand Down Expand Up @@ -92,12 +96,18 @@ public String GetNode(string name)
{
Newtonsoft.Json.Linq.JToken value;

if (_ParsedData.TryGetValue(name, out value))
if (_ParsedData.TryGetValue(name, StringComparison.OrdinalIgnoreCase, out value))
return value.ToString();
else
return "";
}

/// <summary>
/// Use this method to more-easily lazily return the child node of the given name that exists on this AutoNode.
/// </summary>
protected TNode NodeFor<TNode>(string name) where TNode : Node<TNode>
=> (TNode)(childNodes.TryGetValue(name, out var n) ? n : (childNodes[name] = Instantiator<TNode, string>.Create(_ParsedData[name]?.ToString() ?? "")));

/// <summary>
/// Displays the JSON, representative of the GameState data
/// </summary>
Expand Down
16 changes: 16 additions & 0 deletions Project-Aurora/Project-Aurora/Profiles/Generic/ProviderNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Aurora.Profiles.Generic.GSI.Nodes {

public class ProviderNode : AutoJsonNode<ProviderNode> {

public string Name;
public int AppID;

internal ProviderNode(string json) : base(json) { }
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Aurora.Profiles.Minecraft.GSI.Nodes;
using Aurora.Profiles.Generic.GSI.Nodes;
using Aurora.Profiles.Minecraft.GSI.Nodes;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
Expand All @@ -10,54 +11,25 @@ namespace Aurora.Profiles.Minecraft.GSI {

public class GameState_Minecraft : GameState<GameState_Minecraft> {

private ProviderNode _Provider;
private GameNode _Game;
private WorldNode _World;
private PlayerNode _Player;

/// <summary>
/// Provider node provides information about the data source so that Aurora can update the correct gamestate.
/// </summary>
public ProviderNode Provider {
get {
if (_Provider == null)
_Provider = new ProviderNode(_ParsedData["provider"]?.ToString() ?? "");
return _Provider;
}
}
public ProviderNode Provider => NodeFor<ProviderNode>("provider");

/// <summary>
/// Player node provides information about the player (e.g. health and hunger).
/// </summary>
public GameNode Game {
get {
if (_Game == null)
_Game = new GameNode(_ParsedData["game"]?.ToString() ?? "");
return _Game;
}
}
public GameNode Game => NodeFor<GameNode>("game");

/// <summary>
/// World node provides information about the world (e.g. rain intensity and time).
/// </summary>
public WorldNode World {
get {
if (_World == null)
_World = new WorldNode(_ParsedData["world"]?.ToString() ?? "");
return _World;
}
}
public WorldNode World => NodeFor<WorldNode>("world");

/// <summary>
/// Player node provides information about the player (e.g. health and hunger).
/// </summary>
public PlayerNode Player {
get {
if (_Player == null)
_Player = new PlayerNode(_ParsedData["player"]?.ToString() ?? "");
return _Player;
}
}
public PlayerNode Player => NodeFor<PlayerNode>("player");

/// <summary>
/// Creates a default GameState_Minecraft instance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,13 @@

namespace Aurora.Profiles.Minecraft.GSI.Nodes {

public class GameNode : Node<GameNode> {
public class GameNode : AutoJsonNode<GameNode> {

public MinecraftKeyBinding[] KeyBindings;
[AutoJsonPropertyName("keys")] public MinecraftKeyBinding[] KeyBindings;
public bool ControlsGuiOpen;
public bool ChatGuiOpen;

internal GameNode() : base() { }
internal GameNode(string json) : base(json) {
KeyBindings = GetArray<MinecraftKeyBinding>("keys");
ControlsGuiOpen = GetBool("controlsGuiOpen");
ChatGuiOpen = GetBool("chatGuiOpen");
}
internal GameNode(string json) : base(json) { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,20 @@
using System.Threading.Tasks;

namespace Aurora.Profiles.Minecraft.GSI.Nodes {
public class PlayerEffectsNode : Node<PlayerEffectsNode> {
public class PlayerEffectsNode : AutoJsonNode<PlayerEffectsNode> {

public bool HasAbsorption;
public bool HasBlindness;
public bool HasFireResistance;
public bool HasInvisibility;
public bool HasNausea;
public bool HasPoison;
public bool HasRegeneration;
public bool HasSlowness;
public bool HasSpeed;
public bool HasWither;
[AutoJsonPropertyName("absorption")] public bool HasAbsorption;
[AutoJsonPropertyName("blindness")] public bool HasBlindness;
[AutoJsonPropertyName("fireResistance")] public bool HasFireResistance;
[AutoJsonPropertyName("invisibility")] public bool HasInvisibility;
[AutoJsonPropertyName("confusion")] public bool HasNausea;
[AutoJsonPropertyName("poison")] public bool HasPoison;
[AutoJsonPropertyName("regeneration")] public bool HasRegeneration;
[AutoJsonPropertyName("moveSlowdown")] public bool HasSlowness;
[AutoJsonPropertyName("moveSpeed")] public bool HasSpeed;
[AutoJsonPropertyName("wither")] public bool HasWither;

internal PlayerEffectsNode() : base() { }
internal PlayerEffectsNode(string json) : base(json) {
HasAbsorption = GetBool("absorption");
HasBlindness = GetBool("blindness");
HasFireResistance = GetBool("fireResistance");
HasInvisibility = GetBool("invisibility");
HasNausea = GetBool("confusion");
HasPoison = GetBool("poison");
HasRegeneration = GetBool("regeneration");
HasSlowness = GetBool("moveSlowdown");
HasSpeed = GetBool("moveSpeed");
HasWither = GetBool("wither");
}
internal PlayerEffectsNode(string json) : base(json) { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

namespace Aurora.Profiles.Minecraft.GSI.Nodes {

public class PlayerNode : Node<PlayerNode> {
public class PlayerNode : AutoJsonNode<PlayerNode> {

public bool InGame;

public float Health;
public float HealthMax;
[AutoJsonPropertyName("maxHealth")] public float HealthMax;
public float Absorption;
public float AbsorptionMax = 20;
public bool IsDead;
Expand All @@ -32,34 +32,8 @@ public class PlayerNode : Node<PlayerNode> {
public bool IsBurning;
public bool IsInWater;

private PlayerEffectsNode _playerEffects;
public PlayerEffectsNode PlayerEffects {
get {
_playerEffects = _playerEffects ?? new PlayerEffectsNode(_ParsedData["playerEffects"]?.ToString() ?? "");
return _playerEffects;
}
}
public PlayerEffectsNode PlayerEffects => NodeFor<PlayerEffectsNode>("playerEffects");

internal PlayerNode(string json) : base(json) {
InGame = GetBool("inGame");

Health = GetFloat("health");
HealthMax = GetFloat("maxHealth");
Absorption = GetFloat("absorption");
IsDead = GetBool("isDead");
Armor = GetInt("armor");

ExperienceLevel = GetInt("experienceLevel");
Experience = GetFloat("experience");

FoodLevel = GetInt("foodLevel");
SaturationLevel = GetFloat("saturationLevel");

IsSneaking = GetBool("isSneaking");
IsRidingHorse = GetBool("isRidingHorse");
IsBurning = GetBool("isBurning");
IsInWater = GetBool("isInWater");
}

internal PlayerNode(string json) : base(json) { }
}
}

This file was deleted.

Loading

0 comments on commit d143bb1

Please sign in to comment.