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");
|
||||
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
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.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
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