Browse Source

Began refactoring

- Created a new Client class
- Made more use of runtime compiled lambdas, not sure if it's a good idea but anyways
- Removed constructor overload for ECS object base, anything other than getting by EGID should be a method
- Started work on automatically getting information about ECS entities
feature/refactor.v3
NorbiPeti 7 months ago
parent
commit
9a195215f9
Signed by: NorbiPeti <szatmari.norbert.peter@gmail.com> GPG Key ID: DBA4C4549A927E56
6 changed files with 212 additions and 53 deletions
  1. +1
    -1
      TechbloxModdingAPI/App/Client.cs
  2. +127
    -0
      TechbloxModdingAPI/Client/App/Client.cs
  3. +11
    -0
      TechbloxModdingAPI/Client/App/GameState.cs
  4. +9
    -0
      TechbloxModdingAPI/Client/Game/ClientEnvironment.cs
  5. +19
    -52
      TechbloxModdingAPI/Common/EcsObjectBase.cs
  6. +45
    -0
      TechbloxModdingAPI/Common/Utils/Reflections.cs

+ 1
- 1
TechbloxModdingAPI/App/Client.cs View File

@@ -134,7 +134,7 @@ namespace TechbloxModdingAPI.App
Type errorHandler = AccessTools.TypeByName("RobocraftX.Services.ErrorHandler");
MethodInfo instance = AccessTools.PropertyGetter(errorHandler, "Instance");
Func<T> getterSimple = (Func<T>) Delegate.CreateDelegate(typeof(Func<T>), null, instance);
Func<object> getterCasted = () => (object) getterSimple();
Func<object> getterCasted = () => getterSimple();
return getterCasted;
}



+ 127
- 0
TechbloxModdingAPI/Client/App/Client.cs View File

@@ -0,0 +1,127 @@
using System;
using System.Reflection;
using HarmonyLib;
using RobocraftX.Services;
using TechbloxModdingAPI.App;
using TechbloxModdingAPI.Client.Game;
using TechbloxModdingAPI.Common.Utils;
using UnityEngine;

namespace TechbloxModdingAPI.Client.App;

/// <summary>
/// Contains information about the game client's current state.
/// </summary>
public static class Client
{ // TODO
public static GameState CurrentState { get; }
private static Func<object> ErrorHandlerInstanceGetter;

private static Action<object, Error> EnqueueError;

/// <summary>
/// An event that fires whenever the game's state changes
/// </summary>
public static event EventHandler<MenuEventArgs> StateChanged
{
add => Game.menuEngine.EnterMenu += value;
remove => Game.menuEngine.EnterMenu -= value;
}
/// <summary>
/// Techblox build version string.
/// Usually this is in the form YYYY.mm.DD.HH.MM.SS
/// </summary>
/// <value>The version.</value>
public static string Version => Application.version;

/// <summary>
/// Unity version string.
/// </summary>
/// <value>The unity version.</value>
public static string UnityVersion => Application.unityVersion;

/// <summary>
/// Environments (maps) currently visible in the menu.
/// These take a second to completely populate after the EnterMenu event fires.
/// </summary>
/// <value>Available environments.</value>
public static ClientEnvironment[] Environments
{
get;
}

/// <summary>
/// Open a popup which prompts the user to click a button.
/// This reuses Techblox's error dialog popup
/// </summary>
/// <param name="popup">The popup to display. Use an instance of SingleChoicePrompt or DualChoicePrompt.</param>
public static void PromptUser(Error popup)
{
// if the stuff wasn't mostly set to internal, this would be written as:
// RobocraftX.Services.ErrorHandler.Instance.EqueueError(error);
object errorHandlerInstance = ErrorHandlerInstanceGetter();
EnqueueError(errorHandlerInstance, popup);
}

public static void CloseCurrentPrompt()
{
object errorHandlerInstance = ErrorHandlerInstanceGetter();
var popup = GetPopupCloseMethods(errorHandlerInstance);
popup.Close();
}

public static void SelectFirstPromptButton()
{
object errorHandlerInstance = ErrorHandlerInstanceGetter();
var popup = GetPopupCloseMethods(errorHandlerInstance);
popup.FirstButton();
}

public static void SelectSecondPromptButton()
{
object errorHandlerInstance = ErrorHandlerInstanceGetter();
var popup = GetPopupCloseMethods(errorHandlerInstance);
popup.SecondButton();
}

internal static void Init()
{
var errorHandler = AccessTools.TypeByName("RobocraftX.Services.ErrorHandler");
ErrorHandlerInstanceGetter = GenInstanceGetter(errorHandler);
EnqueueError = GenEnqueueError(errorHandler);
}

// Creating delegates once is faster than reflection every time
// Admittedly, this way is more difficult to code and less readable
private static Func<object> GenInstanceGetter(Type handler)
{
return Reflections.CreateAccessor<Func<object>>("Instance", handler);
}

private static Action<object, Error> GenEnqueueError(Type handler)
{
var enqueueError = AccessTools.Method(handler, "EnqueueError");
return Reflections.CreateMethodCall<Action<object, Error>>(enqueueError, handler);
}

private static (Action Close, Action FirstButton, Action SecondButton) _errorPopup;

private static (Action Close, Action FirstButton, Action SecondButton) GetPopupCloseMethods(object handler)
{
if (_errorPopup.Close != null)
return _errorPopup;
Type errorHandler = handler.GetType();
FieldInfo field = AccessTools.Field(errorHandler, "errorPopup");
var errorPopup = (ErrorPopup)field.GetValue(handler);
MethodInfo info = AccessTools.Method(errorPopup.GetType(), "ClosePopup");
var close = (Action)Delegate.CreateDelegate(typeof(Action), errorPopup, info);
info = AccessTools.Method(errorPopup.GetType(), "HandleFirstOption");
var first = (Action)Delegate.CreateDelegate(typeof(Action), errorPopup, info);
info = AccessTools.Method(errorPopup.GetType(), "HandleSecondOption");
var second = (Action)Delegate.CreateDelegate(typeof(Action), errorPopup, info);
_errorPopup = (close, first, second);
return _errorPopup;
}
}

+ 11
- 0
TechbloxModdingAPI/Client/App/GameState.cs View File

@@ -0,0 +1,11 @@
namespace TechbloxModdingAPI.Client.App;

public enum GameState
{
InMenu,
InMachineEditor,
InWorldEditor,
InTestMode,
InMatch,
Loading
}

+ 9
- 0
TechbloxModdingAPI/Client/Game/ClientEnvironment.cs View File

@@ -0,0 +1,9 @@
namespace TechbloxModdingAPI.Client.Game;

/// <summary>
/// A build or simulation environment.
/// </summary>
public class ClientEnvironment
{
}

TechbloxModdingAPI/EcsObjectBase.cs → TechbloxModdingAPI/Common/EcsObjectBase.cs View File

@@ -4,6 +4,7 @@ using System.Linq.Expressions;
using Svelto.DataStructures;
using Svelto.ECS;
using Svelto.ECS.Internal;
using TechbloxModdingAPI.Common.Utils;
using TechbloxModdingAPI.Utility;

namespace TechbloxModdingAPI
@@ -12,11 +13,7 @@ namespace TechbloxModdingAPI
{
public EGID Id { get; }
private static readonly Dictionary<Type, WeakDictionary<EGID, EcsObjectBase>> _instances =
new Dictionary<Type, WeakDictionary<EGID, EcsObjectBase>>();

private static readonly WeakDictionary<EGID, EcsObjectBase> _noInstance =
new WeakDictionary<EGID, EcsObjectBase>();
private static readonly Dictionary<Type, WeakDictionary<EGID, EcsObjectBase>> _instances = new();

internal static WeakDictionary<EGID, EcsObjectBase> GetInstances(Type type)
{
@@ -39,7 +36,7 @@ namespace TechbloxModdingAPI
return (T)instance;
}

protected EcsObjectBase(EGID id)
protected EcsObjectBase(EGID id, Type entityDescriptorType)
{
if (!_instances.TryGetValue(GetType(), out var dict))
{
@@ -51,25 +48,18 @@ namespace TechbloxModdingAPI
Id = id;
}

protected EcsObjectBase(Func<EcsObjectBase, EGID> initializer)
private void AnalyzeEntityDescriptor(Type entityDescriptorType)
{
if (!_instances.TryGetValue(GetType(), out var dict))
{
dict = new WeakDictionary<EGID, EcsObjectBase>();
_instances.Add(GetType(), dict);
}

var id = initializer(this);
if (!dict.ContainsKey(id)) // Multiple instances may be created
dict.Add(id, this);
else
{
Logging.MetaDebugLog($"An object of this type and ID is already stored: {GetType()} - {id}");
Logging.MetaDebugLog(this);
Logging.MetaDebugLog(dict[id]);
}

Id = id;
// TODO: Cache
// TODO: This should be in BlockClassGenerator
// TODO: Add support for creating/deleting entities (getting an up to date server/client engines root)
var templateType = typeof(EntityDescriptorTemplate<>).MakeGenericType(entityDescriptorType);
var getTemplateClass = Expression.Constant(templateType);
var getDescriptorExpr = Expression.PropertyOrField(getTemplateClass, "descriptor");
var getTemplateDescriptorExpr =
Expression.Lambda<Func<IEntityDescriptor>>(getDescriptorExpr);
var getTemplateDescriptor = getTemplateDescriptorExpr.Compile();
var builders = getTemplateDescriptor().componentsToBuild;
}

#region ECS initializer stuff
@@ -77,17 +67,18 @@ namespace TechbloxModdingAPI
protected internal EcsInitData InitData;

/// <summary>
/// Holds information needed to construct a component initializer
/// Holds information needed to construct a component initializer.
/// Necessary because the initializer is a ref struct which cannot be assigned to a field.
/// </summary>
protected internal struct EcsInitData
{
private FasterDictionary<RefWrapperType, ITypeSafeDictionary> group;
private EntityReference reference;

public static implicit operator EcsInitData(EntityInitializer initializer) => new EcsInitData
public static implicit operator EcsInitData(EntityInitializer initializer) => new()
{ group = GetInitGroup(initializer), reference = initializer.reference };

public EntityInitializer Initializer(EGID id) => new EntityInitializer(id, group, reference);
public EntityInitializer Initializer(EGID id) => new(id, group, reference);
public bool Valid => group != null;
}

@@ -97,31 +88,7 @@ namespace TechbloxModdingAPI
/// <summary>
/// Accesses the group field of the initializer
/// </summary>
private static GetInitGroupFunc GetInitGroup = CreateAccessor<GetInitGroupFunc>("_group");

//https://stackoverflow.com/questions/55878525/unit-testing-ref-structs-with-private-fields-via-reflection
private static TDelegate CreateAccessor<TDelegate>(string memberName) where TDelegate : Delegate
{
var invokeMethod = typeof(TDelegate).GetMethod("Invoke");
if (invokeMethod == null)
throw new InvalidOperationException($"{typeof(TDelegate)} signature could not be determined.");

var delegateParameters = invokeMethod.GetParameters();
if (delegateParameters.Length != 1)
throw new InvalidOperationException("Delegate must have a single parameter.");

var paramType = delegateParameters[0].ParameterType;

var objParam = Expression.Parameter(paramType, "obj");
var memberExpr = Expression.PropertyOrField(objParam, memberName);
Expression returnExpr = memberExpr;
if (invokeMethod.ReturnType != memberExpr.Type)
returnExpr = Expression.ConvertChecked(memberExpr, invokeMethod.ReturnType);

var lambda =
Expression.Lambda<TDelegate>(returnExpr, $"Access{paramType.Name}_{memberName}", new[] { objParam });
return lambda.Compile();
}
private static readonly GetInitGroupFunc GetInitGroup = Reflections.CreateAccessor<GetInitGroupFunc>("_group");

#endregion
}

+ 45
- 0
TechbloxModdingAPI/Common/Utils/Reflections.cs View File

@@ -0,0 +1,45 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace TechbloxModdingAPI.Common.Utils;

public static class Reflections
{
//https://stackoverflow.com/questions/55878525/unit-testing-ref-structs-with-private-fields-via-reflection
public static TDelegate CreateAccessor<TDelegate>(string memberName, Type thisType = null) where TDelegate : Delegate
{
return CreateSomeCall<TDelegate>(memberName, thisType, (objParam, _) => Expression.PropertyOrField(objParam, memberName));
}

public static TDelegate CreateMethodCall<TDelegate>(MethodInfo method, Type thisType = null) where TDelegate : Delegate
{
return CreateSomeCall<TDelegate>(method.Name, thisType, (objParam, parameters) => Expression.Call(objParam, method, parameters));
}
private static TDelegate CreateSomeCall<TDelegate>(string memberName, Type thisType, Func<ParameterExpression, ParameterExpression[], Expression> memberExpressionGetter) where TDelegate : Delegate
{
var invokeMethod = typeof(TDelegate).GetMethod("Invoke");
if (invokeMethod == null)
throw new InvalidOperationException($"{typeof(TDelegate)} signature could not be determined.");

var delegateParameters = invokeMethod.GetParameters();
if (delegateParameters.Length != 1)
throw new InvalidOperationException("Delegate must have a single parameter.");

var paramType = thisType ?? delegateParameters[0].ParameterType;

var objParam = Expression.Parameter(paramType, "obj");
var otherParams = delegateParameters.Skip(1)
.Select(pinfo => Expression.Parameter(pinfo.ParameterType, pinfo.Name)).ToArray();
var memberExpr = memberExpressionGetter(objParam, otherParams);
Expression returnExpr = memberExpr;
if (invokeMethod.ReturnType != memberExpr.Type)
returnExpr = Expression.ConvertChecked(memberExpr, invokeMethod.ReturnType);

var lambda =
Expression.Lambda<TDelegate>(returnExpr, $"Access{paramType.Name}_{memberName}", new[] { objParam }.Concat(otherParams));
return lambda.Compile();
}
}

Loading…
Cancel
Save