A stable modding interface between Techblox and mods https://mod.exmods.org/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

172 lines
6.3KB

  1. using System;
  2. using System.Reflection;
  3. using HarmonyLib;
  4. using RobocraftX.Services;
  5. using UnityEngine;
  6. using RobocraftX.Common;
  7. using TechbloxModdingAPI.Utility;
  8. namespace TechbloxModdingAPI.App
  9. {
  10. /// <summary>
  11. /// The Techblox application that is running this code right now.
  12. /// </summary>
  13. public class Client
  14. {
  15. public static Client Instance { get; } = new Client();
  16. protected static Func<object> ErrorHandlerInstanceGetter;
  17. protected static Action<object, Error> EnqueueError;
  18. /// <summary>
  19. /// An event that fires whenever the main menu is loaded.
  20. /// </summary>
  21. public static event EventHandler<MenuEventArgs> EnterMenu
  22. {
  23. add => Game.menuEngine.EnterMenu += value;
  24. remove => Game.menuEngine.EnterMenu -= value;
  25. }
  26. /// <summary>
  27. /// An event that fire whenever the main menu is exited.
  28. /// </summary>
  29. public static event EventHandler<MenuEventArgs> ExitMenu
  30. {
  31. add => Game.menuEngine.ExitMenu += value;
  32. remove => Game.menuEngine.ExitMenu -= value;
  33. }
  34. /// <summary>
  35. /// Techblox build version string.
  36. /// Usually this is in the form YYYY.mm.DD.HH.MM.SS
  37. /// </summary>
  38. /// <value>The version.</value>
  39. public string Version
  40. {
  41. get => Application.version;
  42. }
  43. /// <summary>
  44. /// Unity version string.
  45. /// </summary>
  46. /// <value>The unity version.</value>
  47. public string UnityVersion
  48. {
  49. get => Application.unityVersion;
  50. }
  51. /// <summary>
  52. /// Game saves currently visible in the menu.
  53. /// These take a second to completely populate after the EnterMenu event fires.
  54. /// </summary>
  55. /// <value>My games.</value>
  56. public Game[] MyGames
  57. {
  58. get
  59. {
  60. if (!Game.menuEngine.IsInMenu) return Array.Empty<Game>();
  61. return Game.menuEngine.GetMyGames();
  62. }
  63. }
  64. /// <summary>
  65. /// Whether Techblox is in the Main Menu
  66. /// </summary>
  67. /// <value><c>true</c> if in menu; <c>false</c> when loading or in a game.</value>
  68. public bool InMenu
  69. {
  70. get => Game.menuEngine.IsInMenu;
  71. }
  72. /// <summary>
  73. /// Open a popup which prompts the user to click a button.
  74. /// This reuses Techblox's error dialog popup
  75. /// </summary>
  76. /// <param name="popup">The popup to display. Use an instance of SingleChoicePrompt or DualChoicePrompt.</param>
  77. public void PromptUser(Error popup)
  78. {
  79. // if the stuff wasn't mostly set to internal, this would be written as:
  80. // RobocraftX.Services.ErrorHandler.Instance.EqueueError(error);
  81. object errorHandlerInstance = ErrorHandlerInstanceGetter();
  82. EnqueueError(errorHandlerInstance, popup);
  83. }
  84. public void CloseCurrentPrompt()
  85. {
  86. object errorHandlerInstance = ErrorHandlerInstanceGetter();
  87. var popup = GetPopupCloseMethods(errorHandlerInstance);
  88. popup.Close();
  89. }
  90. public void SelectFirstPromptButton()
  91. {
  92. object errorHandlerInstance = ErrorHandlerInstanceGetter();
  93. var popup = GetPopupCloseMethods(errorHandlerInstance);
  94. popup.FirstButton();
  95. }
  96. public void SelectSecondPromptButton()
  97. {
  98. object errorHandlerInstance = ErrorHandlerInstanceGetter();
  99. var popup = GetPopupCloseMethods(errorHandlerInstance);
  100. popup.SecondButton();
  101. }
  102. internal static void Init()
  103. {
  104. // this would have been so much simpler if this didn't involve a bunch of internal fields & classes
  105. Type errorHandler = AccessTools.TypeByName("RobocraftX.Services.ErrorHandler");
  106. Type errorHandle = AccessTools.TypeByName("RobocraftX.Services.ErrorHandle");
  107. ErrorHandlerInstanceGetter = (Func<object>) AccessTools.Method("TechbloxModdingAPI.App.Client:GenInstanceGetter")
  108. .MakeGenericMethod(errorHandler)
  109. .Invoke(null, new object[0]);
  110. EnqueueError = (Action<object, Error>) AccessTools.Method("TechbloxModdingAPI.App.Client:GenEnqueueError")
  111. .MakeGenericMethod(errorHandler, errorHandle)
  112. .Invoke(null, new object[0]);
  113. }
  114. // Creating delegates once is faster than reflection every time
  115. // Admittedly, this way is more difficult to code and less readable
  116. private static Func<object> GenInstanceGetter<T>()
  117. {
  118. Type errorHandler = AccessTools.TypeByName("RobocraftX.Services.ErrorHandler");
  119. MethodInfo instance = AccessTools.PropertyGetter(errorHandler, "Instance");
  120. Func<T> getterSimple = (Func<T>) Delegate.CreateDelegate(typeof(Func<T>), null, instance);
  121. Func<object> getterCasted = () => (object) getterSimple();
  122. return getterCasted;
  123. }
  124. private static Action<object, Error> GenEnqueueError<T, TRes>()
  125. {
  126. Type errorHandler = AccessTools.TypeByName("RobocraftX.Services.ErrorHandler");
  127. MethodInfo enqueueError = AccessTools.Method(errorHandler, "EnqueueError");
  128. Func<T, Error, TRes> enqueueSimple =
  129. (Func<T, Error, TRes>) Delegate.CreateDelegate(typeof(Func<T, Error, TRes>), enqueueError);
  130. Action<object, Error> enqueueCasted =
  131. (object instance, Error error) => { enqueueSimple((T) instance, error); };
  132. return enqueueCasted;
  133. }
  134. private static (Action Close, Action FirstButton, Action SecondButton) _errorPopup;
  135. private static (Action Close, Action FirstButton, Action SecondButton) GetPopupCloseMethods(object handler)
  136. {
  137. if (_errorPopup.Close != null)
  138. return _errorPopup;
  139. Type errorHandler = handler.GetType();
  140. FieldInfo field = AccessTools.Field(errorHandler, "errorPopup");
  141. var errorPopup = (ErrorPopup)field.GetValue(handler);
  142. MethodInfo info = AccessTools.Method(errorPopup.GetType(), "ClosePopup");
  143. var close = (Action)Delegate.CreateDelegate(typeof(Action), errorPopup, info);
  144. info = AccessTools.Method(errorPopup.GetType(), "HandleFirstOption");
  145. var first = (Action)Delegate.CreateDelegate(typeof(Action), errorPopup, info);
  146. info = AccessTools.Method(errorPopup.GetType(), "HandleSecondOption");
  147. var second = (Action)Delegate.CreateDelegate(typeof(Action), errorPopup, info);
  148. _errorPopup = (close, first, second);
  149. return _errorPopup;
  150. }
  151. }
  152. }