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:
parent
5dff88d703
commit
9a195215f9
6 changed files with 212 additions and 53 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
127
TechbloxModdingAPI/Client/App/Client.cs
Normal file
127
TechbloxModdingAPI/Client/App/Client.cs
Normal 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
TechbloxModdingAPI/Client/App/GameState.cs
Normal file
11
TechbloxModdingAPI/Client/App/GameState.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
namespace TechbloxModdingAPI.Client.App;
|
||||||
|
|
||||||
|
public enum GameState
|
||||||
|
{
|
||||||
|
InMenu,
|
||||||
|
InMachineEditor,
|
||||||
|
InWorldEditor,
|
||||||
|
InTestMode,
|
||||||
|
InMatch,
|
||||||
|
Loading
|
||||||
|
}
|
9
TechbloxModdingAPI/Client/Game/ClientEnvironment.cs
Normal file
9
TechbloxModdingAPI/Client/Game/ClientEnvironment.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace TechbloxModdingAPI.Client.Game;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A build or simulation environment.
|
||||||
|
/// </summary>
|
||||||
|
public class ClientEnvironment
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
45
TechbloxModdingAPI/Common/Utils/Reflections.cs
Normal file
45
TechbloxModdingAPI/Common/Utils/Reflections.cs
Normal 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…
Reference in a new issue