diff --git a/TechbloxModdingAPI/App/Client.cs b/TechbloxModdingAPI/App/Client.cs index 317adec..249d7a3 100644 --- a/TechbloxModdingAPI/App/Client.cs +++ b/TechbloxModdingAPI/App/Client.cs @@ -134,7 +134,7 @@ namespace TechbloxModdingAPI.App Type errorHandler = AccessTools.TypeByName("RobocraftX.Services.ErrorHandler"); MethodInfo instance = AccessTools.PropertyGetter(errorHandler, "Instance"); Func getterSimple = (Func) Delegate.CreateDelegate(typeof(Func), null, instance); - Func getterCasted = () => (object) getterSimple(); + Func getterCasted = () => getterSimple(); return getterCasted; } diff --git a/TechbloxModdingAPI/Client/App/Client.cs b/TechbloxModdingAPI/Client/App/Client.cs new file mode 100644 index 0000000..4b24e82 --- /dev/null +++ b/TechbloxModdingAPI/Client/App/Client.cs @@ -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; + +/// +/// Contains information about the game client's current state. +/// +public static class Client +{ // TODO + public static GameState CurrentState { get; } + + private static Func ErrorHandlerInstanceGetter; + + private static Action EnqueueError; + + /// + /// An event that fires whenever the game's state changes + /// + public static event EventHandler StateChanged + { + add => Game.menuEngine.EnterMenu += value; + remove => Game.menuEngine.EnterMenu -= value; + } + + /// + /// Techblox build version string. + /// Usually this is in the form YYYY.mm.DD.HH.MM.SS + /// + /// The version. + public static string Version => Application.version; + + /// + /// Unity version string. + /// + /// The unity version. + public static string UnityVersion => Application.unityVersion; + + /// + /// Environments (maps) currently visible in the menu. + /// These take a second to completely populate after the EnterMenu event fires. + /// + /// Available environments. + public static ClientEnvironment[] Environments + { + get; + } + + /// + /// Open a popup which prompts the user to click a button. + /// This reuses Techblox's error dialog popup + /// + /// The popup to display. Use an instance of SingleChoicePrompt or DualChoicePrompt. + 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 GenInstanceGetter(Type handler) + { + return Reflections.CreateAccessor>("Instance", handler); + } + + private static Action GenEnqueueError(Type handler) + { + var enqueueError = AccessTools.Method(handler, "EnqueueError"); + return Reflections.CreateMethodCall>(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; + } +} \ No newline at end of file diff --git a/TechbloxModdingAPI/Client/App/GameState.cs b/TechbloxModdingAPI/Client/App/GameState.cs new file mode 100644 index 0000000..d11c3ef --- /dev/null +++ b/TechbloxModdingAPI/Client/App/GameState.cs @@ -0,0 +1,11 @@ +namespace TechbloxModdingAPI.Client.App; + +public enum GameState +{ + InMenu, + InMachineEditor, + InWorldEditor, + InTestMode, + InMatch, + Loading +} \ No newline at end of file diff --git a/TechbloxModdingAPI/Client/Game/ClientEnvironment.cs b/TechbloxModdingAPI/Client/Game/ClientEnvironment.cs new file mode 100644 index 0000000..29054bb --- /dev/null +++ b/TechbloxModdingAPI/Client/Game/ClientEnvironment.cs @@ -0,0 +1,9 @@ +namespace TechbloxModdingAPI.Client.Game; + +/// +/// A build or simulation environment. +/// +public class ClientEnvironment +{ + +} \ No newline at end of file diff --git a/TechbloxModdingAPI/EcsObjectBase.cs b/TechbloxModdingAPI/Common/EcsObjectBase.cs similarity index 52% rename from TechbloxModdingAPI/EcsObjectBase.cs rename to TechbloxModdingAPI/Common/EcsObjectBase.cs index 13a74f9..4bbfe58 100644 --- a/TechbloxModdingAPI/EcsObjectBase.cs +++ b/TechbloxModdingAPI/Common/EcsObjectBase.cs @@ -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> _instances = - new Dictionary>(); - - private static readonly WeakDictionary _noInstance = - new WeakDictionary(); + private static readonly Dictionary> _instances = new(); internal static WeakDictionary 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 initializer) + private void AnalyzeEntityDescriptor(Type entityDescriptorType) { - if (!_instances.TryGetValue(GetType(), out var dict)) - { - dict = new WeakDictionary(); - _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>(getDescriptorExpr); + var getTemplateDescriptor = getTemplateDescriptorExpr.Compile(); + var builders = getTemplateDescriptor().componentsToBuild; } #region ECS initializer stuff @@ -77,17 +67,18 @@ namespace TechbloxModdingAPI protected internal EcsInitData InitData; /// - /// 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. /// protected internal struct EcsInitData { private FasterDictionary 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 /// /// Accesses the group field of the initializer /// - private static GetInitGroupFunc GetInitGroup = CreateAccessor("_group"); - - //https://stackoverflow.com/questions/55878525/unit-testing-ref-structs-with-private-fields-via-reflection - private static TDelegate CreateAccessor(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(returnExpr, $"Access{paramType.Name}_{memberName}", new[] { objParam }); - return lambda.Compile(); - } + private static readonly GetInitGroupFunc GetInitGroup = Reflections.CreateAccessor("_group"); #endregion } diff --git a/TechbloxModdingAPI/Common/Utils/Reflections.cs b/TechbloxModdingAPI/Common/Utils/Reflections.cs new file mode 100644 index 0000000..2d9bb4f --- /dev/null +++ b/TechbloxModdingAPI/Common/Utils/Reflections.cs @@ -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(string memberName, Type thisType = null) where TDelegate : Delegate + { + return CreateSomeCall(memberName, thisType, (objParam, _) => Expression.PropertyOrField(objParam, memberName)); + } + + public static TDelegate CreateMethodCall(MethodInfo method, Type thisType = null) where TDelegate : Delegate + { + return CreateSomeCall(method.Name, thisType, (objParam, parameters) => Expression.Call(objParam, method, parameters)); + } + + private static TDelegate CreateSomeCall(string memberName, Type thisType, Func 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(returnExpr, $"Access{paramType.Name}_{memberName}", new[] { objParam }.Concat(otherParams)); + return lambda.Compile(); + } +} \ No newline at end of file