- 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 entitiesfeature/refactor.v3
@@ -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; | |||
} | |||
@@ -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; | |||
} | |||
} |
@@ -0,0 +1,11 @@ | |||
namespace TechbloxModdingAPI.Client.App; | |||
public enum GameState | |||
{ | |||
InMenu, | |||
InMachineEditor, | |||
InWorldEditor, | |||
InTestMode, | |||
InMatch, | |||
Loading | |||
} |
@@ -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 | |||
} |
@@ -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(); | |||
} | |||
} |