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
This commit is contained in:
Norbi Peti 2023-09-27 02:22:39 +02:00
parent 5dff88d703
commit 9a195215f9
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
6 changed files with 212 additions and 53 deletions

View file

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

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;
}
}

View file

@ -0,0 +1,11 @@
namespace TechbloxModdingAPI.Client.App;
public enum GameState
{
InMenu,
InMachineEditor,
InWorldEditor,
InTestMode,
InMatch,
Loading
}

View file

@ -0,0 +1,9 @@
namespace TechbloxModdingAPI.Client.Game;
/// <summary>
/// A build or simulation environment.
/// </summary>
public class ClientEnvironment
{
}

View file

@ -4,6 +4,7 @@ using System.Linq.Expressions;
using Svelto.DataStructures; using Svelto.DataStructures;
using Svelto.ECS; using Svelto.ECS;
using Svelto.ECS.Internal; using Svelto.ECS.Internal;
using TechbloxModdingAPI.Common.Utils;
using TechbloxModdingAPI.Utility; using TechbloxModdingAPI.Utility;
namespace TechbloxModdingAPI namespace TechbloxModdingAPI
@ -12,11 +13,7 @@ namespace TechbloxModdingAPI
{ {
public EGID Id { get; } public EGID Id { get; }
private static readonly Dictionary<Type, WeakDictionary<EGID, EcsObjectBase>> _instances = private static readonly Dictionary<Type, WeakDictionary<EGID, EcsObjectBase>> _instances = new();
new Dictionary<Type, WeakDictionary<EGID, EcsObjectBase>>();
private static readonly WeakDictionary<EGID, EcsObjectBase> _noInstance =
new WeakDictionary<EGID, EcsObjectBase>();
internal static WeakDictionary<EGID, EcsObjectBase> GetInstances(Type type) internal static WeakDictionary<EGID, EcsObjectBase> GetInstances(Type type)
{ {
@ -39,7 +36,7 @@ namespace TechbloxModdingAPI
return (T)instance; return (T)instance;
} }
protected EcsObjectBase(EGID id) protected EcsObjectBase(EGID id, Type entityDescriptorType)
{ {
if (!_instances.TryGetValue(GetType(), out var dict)) if (!_instances.TryGetValue(GetType(), out var dict))
{ {
@ -51,25 +48,18 @@ namespace TechbloxModdingAPI
Id = id; Id = id;
} }
protected EcsObjectBase(Func<EcsObjectBase, EGID> initializer) private void AnalyzeEntityDescriptor(Type entityDescriptorType)
{ {
if (!_instances.TryGetValue(GetType(), out var dict)) // TODO: Cache
{ // TODO: This should be in BlockClassGenerator
dict = new WeakDictionary<EGID, EcsObjectBase>(); // TODO: Add support for creating/deleting entities (getting an up to date server/client engines root)
_instances.Add(GetType(), dict); var templateType = typeof(EntityDescriptorTemplate<>).MakeGenericType(entityDescriptorType);
} var getTemplateClass = Expression.Constant(templateType);
var getDescriptorExpr = Expression.PropertyOrField(getTemplateClass, "descriptor");
var id = initializer(this); var getTemplateDescriptorExpr =
if (!dict.ContainsKey(id)) // Multiple instances may be created Expression.Lambda<Func<IEntityDescriptor>>(getDescriptorExpr);
dict.Add(id, this); var getTemplateDescriptor = getTemplateDescriptorExpr.Compile();
else var builders = getTemplateDescriptor().componentsToBuild;
{
Logging.MetaDebugLog($"An object of this type and ID is already stored: {GetType()} - {id}");
Logging.MetaDebugLog(this);
Logging.MetaDebugLog(dict[id]);
}
Id = id;
} }
#region ECS initializer stuff #region ECS initializer stuff
@ -77,17 +67,18 @@ namespace TechbloxModdingAPI
protected internal EcsInitData InitData; protected internal EcsInitData InitData;
/// <summary> /// <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> /// </summary>
protected internal struct EcsInitData protected internal struct EcsInitData
{ {
private FasterDictionary<RefWrapperType, ITypeSafeDictionary> group; private FasterDictionary<RefWrapperType, ITypeSafeDictionary> group;
private EntityReference reference; 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 }; { 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; public bool Valid => group != null;
} }
@ -97,31 +88,7 @@ namespace TechbloxModdingAPI
/// <summary> /// <summary>
/// Accesses the group field of the initializer /// Accesses the group field of the initializer
/// </summary> /// </summary>
private static GetInitGroupFunc GetInitGroup = CreateAccessor<GetInitGroupFunc>("_group"); private static readonly GetInitGroupFunc GetInitGroup = Reflections.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();
}
#endregion #endregion
} }

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();
}
}