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.

491 lines
15KB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Runtime.CompilerServices;
  4. using RobocraftX.GUI.MyGamesScreen;
  5. using Svelto.ECS;
  6. using Techblox.GameSelection;
  7. using TechbloxModdingAPI.Blocks;
  8. using TechbloxModdingAPI.Tasks;
  9. using TechbloxModdingAPI.Utility;
  10. namespace TechbloxModdingAPI.App
  11. {
  12. /// <summary>
  13. /// An in-game save.
  14. /// This can be a menu item for a local save or the currently loaded save.
  15. /// Support for Steam Workshop coming soon (hopefully).
  16. /// </summary>
  17. public class Game
  18. {
  19. // extensible engines
  20. protected static GameGameEngine gameEngine = new GameGameEngine();
  21. protected internal static GameMenuEngine menuEngine = new GameMenuEngine();
  22. protected static DebugInterfaceEngine debugOverlayEngine = new DebugInterfaceEngine();
  23. protected static GameBuildSimEventEngine buildSimEventEngine = new GameBuildSimEventEngine();
  24. private List<string> debugIds = new List<string>();
  25. private bool menuMode = true;
  26. private bool hasId = false;
  27. /// <summary>
  28. /// Initializes a new instance of the <see cref="T:TechbloxModdingAPI.App.Game"/> class.
  29. /// </summary>
  30. /// <param name="id">Menu identifier.</param>
  31. public Game(uint id) : this(new EGID(id, MyGamesScreenExclusiveGroups.MyGames))
  32. {
  33. }
  34. /// <summary>
  35. /// Initializes a new instance of the <see cref="T:TechbloxModdingAPI.App.Game"/> class.
  36. /// </summary>
  37. /// <param name="id">Menu identifier.</param>
  38. public Game(EGID id)
  39. {
  40. this.Id = id.entityID;
  41. this.EGID = id;
  42. this.hasId = true;
  43. menuMode = true;
  44. if (!VerifyMode()) throw new AppStateException("Game cannot be created while not in a game nor in a menu (is the game in a loading screen?)");
  45. }
  46. /// <summary>
  47. /// Initializes a new instance of the <see cref="T:TechbloxModdingAPI.App.Game"/> class without id.
  48. /// This is assumed to be the current game.
  49. /// </summary>
  50. public Game()
  51. {
  52. menuMode = false;
  53. if (!VerifyMode()) throw new AppStateException("Game cannot be created while not in a game nor in a menu (is the game in a loading screen?)");
  54. if (menuEngine.IsInMenu) throw new GameNotFoundException("Game not found.");
  55. }
  56. /// <summary>
  57. /// Returns the currently loaded game.
  58. /// If in a menu, manipulating the returned object may not work as intended.
  59. /// </summary>
  60. /// <returns>The current game.</returns>
  61. public static Game CurrentGame()
  62. {
  63. return new Game();
  64. }
  65. /// <summary>
  66. /// Creates a new game and adds it to the menu.
  67. /// If not in a menu, this will throw AppStateException.
  68. /// </summary>
  69. /// <returns>The new game.</returns>
  70. public static Game NewGame()
  71. {
  72. if (!menuEngine.IsInMenu) throw new AppStateException("New Game cannot be created while not in a menu.");
  73. uint nextId = menuEngine.HighestID() + 1;
  74. EGID egid = new EGID(nextId, MyGamesScreenExclusiveGroups.MyGames);
  75. menuEngine.CreateMyGame(egid);
  76. return new Game(egid);
  77. }
  78. /// <summary>
  79. /// An event that fires whenever a game is switched to simulation mode (time running mode).
  80. /// </summary>
  81. public static event EventHandler<GameEventArgs> Simulate
  82. {
  83. add => buildSimEventEngine.SimulationMode += value;
  84. remove => buildSimEventEngine.SimulationMode -= value;
  85. }
  86. /// <summary>
  87. /// An event that fires whenever a game is switched to edit mode (time stopped mode).
  88. /// This does not fire when a game is loaded.
  89. /// </summary>
  90. public static event EventHandler<GameEventArgs> Edit
  91. {
  92. add => buildSimEventEngine.BuildMode += value;
  93. remove => buildSimEventEngine.BuildMode -= value;
  94. }
  95. /// <summary>
  96. /// An event that fires right after a game is completely loaded.
  97. /// </summary>
  98. public static event EventHandler<GameEventArgs> Enter
  99. {
  100. add => gameEngine.EnterGame += value;
  101. remove => gameEngine.EnterGame -= value;
  102. }
  103. /// <summary>
  104. /// An event that fires right before a game returns to the main menu.
  105. /// At this point, Techblox is transitioning state so many things are invalid/unstable here.
  106. /// </summary>
  107. public static event EventHandler<GameEventArgs> Exit
  108. {
  109. add => gameEngine.ExitGame += value;
  110. remove => gameEngine.ExitGame -= value;
  111. }
  112. /// <summary>
  113. /// The game's unique menu identifier.
  114. /// </summary>
  115. /// <value>The identifier.</value>
  116. public uint Id
  117. {
  118. get;
  119. private set;
  120. }
  121. /// <summary>
  122. /// The game's unique menu EGID.
  123. /// </summary>
  124. /// <value>The egid.</value>
  125. public EGID EGID
  126. {
  127. get;
  128. private set;
  129. }
  130. /// <summary>
  131. /// Whether the game is a (valid) menu item.
  132. /// </summary>
  133. /// <value><c>true</c> if menu item; otherwise, <c>false</c>.</value>
  134. public bool MenuItem
  135. {
  136. get => menuMode && hasId;
  137. }
  138. /// <summary>
  139. /// The game's name.
  140. /// </summary>
  141. /// <value>The name.</value>
  142. public string Name
  143. {
  144. get
  145. {
  146. if (!VerifyMode()) return null;
  147. if (menuMode) return menuEngine.GetGameInfo(EGID).GameName;
  148. return gameEngine.GetGameData().saveName;
  149. }
  150. set
  151. {
  152. if (!VerifyMode()) return;
  153. if (menuMode)
  154. {
  155. menuEngine.SetGameName(EGID, value);
  156. } // Save details are directly saved from user input or not changed at all when in game
  157. }
  158. }
  159. /// <summary>
  160. /// The game's description.
  161. /// </summary>
  162. /// <value>The description.</value>
  163. public string Description
  164. {
  165. get
  166. {
  167. if (!VerifyMode()) return null;
  168. if (menuMode) return menuEngine.GetGameInfo(EGID).GameDescription;
  169. return "";
  170. }
  171. set
  172. {
  173. if (!VerifyMode()) return;
  174. if (menuMode)
  175. {
  176. menuEngine.SetGameDescription(EGID, value);
  177. } // No description exists in-game
  178. }
  179. }
  180. /// <summary>
  181. /// The path to the game's save folder.
  182. /// </summary>
  183. /// <value>The path.</value>
  184. public string Path
  185. {
  186. get
  187. {
  188. if (!VerifyMode()) return null;
  189. if (menuMode) return menuEngine.GetGameInfo(EGID).SavedGamePath;
  190. return gameEngine.GetGameData().gameID;
  191. }
  192. set
  193. {
  194. if (!VerifyMode()) return;
  195. if (menuMode)
  196. {
  197. menuEngine.GetGameInfo(EGID).SavedGamePath.Set(value);
  198. }
  199. }
  200. }
  201. /// <summary>
  202. /// The Steam Workshop Id of the game save.
  203. /// In most cases this is invalid and returns 0, so this can be ignored.
  204. /// </summary>
  205. /// <value>The workshop identifier.</value>
  206. [Obsolete]
  207. public ulong WorkshopId
  208. {
  209. get
  210. {
  211. return 0uL; // Not supported anymore
  212. }
  213. set
  214. {
  215. }
  216. }
  217. /// <summary>
  218. /// Whether the game is in simulation mode.
  219. /// </summary>
  220. /// <value><c>true</c> if is simulating; otherwise, <c>false</c>.</value>
  221. public bool IsSimulating
  222. {
  223. get
  224. {
  225. if (!VerifyMode()) return false;
  226. return !menuMode && gameEngine.IsTimeRunningMode();
  227. }
  228. set
  229. {
  230. if (!VerifyMode()) return;
  231. if (!menuMode && gameEngine.IsTimeRunningMode() != value)
  232. gameEngine.ToggleTimeMode();
  233. }
  234. }
  235. /// <summary>
  236. /// Whether the game is in time-running mode.
  237. /// Alias of IsSimulating.
  238. /// </summary>
  239. /// <value><c>true</c> if is time running; otherwise, <c>false</c>.</value>
  240. public bool IsTimeRunning
  241. {
  242. get => IsSimulating;
  243. set
  244. {
  245. IsSimulating = value;
  246. }
  247. }
  248. /// <summary>
  249. /// Whether the game is in time-stopped mode.
  250. /// </summary>
  251. /// <value><c>true</c> if is time stopped; otherwise, <c>false</c>.</value>
  252. public bool IsTimeStopped
  253. {
  254. get
  255. {
  256. if (!VerifyMode()) return false;
  257. return !menuMode && gameEngine.IsTimeStoppedMode();
  258. }
  259. set
  260. {
  261. if (!VerifyMode()) return;
  262. if (!menuMode && gameEngine.IsTimeStoppedMode() != value)
  263. gameEngine.ToggleTimeMode();
  264. }
  265. }
  266. /// <summary>
  267. /// Toggles the time mode.
  268. /// </summary>
  269. public void ToggleTimeMode()
  270. {
  271. if (!VerifyMode()) return;
  272. if (menuMode || !gameEngine.IsInGame)
  273. {
  274. throw new AppStateException("Game menu item cannot toggle it's time mode");
  275. }
  276. gameEngine.ToggleTimeMode();
  277. }
  278. /// <summary>
  279. /// The mode of the game.
  280. /// </summary>
  281. public CurrentGameMode Mode
  282. {
  283. get
  284. {
  285. if (menuMode || !VerifyMode()) return CurrentGameMode.None;
  286. return gameEngine.GetGameData().gameMode == GameMode.CreateWorld ? CurrentGameMode.Build : CurrentGameMode.Play;
  287. }
  288. }
  289. /// <summary>
  290. /// Load the game save.
  291. /// This happens asynchronously, so when this method returns the game not loaded yet.
  292. /// Use the Game.Enter event to perform operations after the game has completely loaded.
  293. /// </summary>
  294. public void EnterGame()
  295. {
  296. if (!VerifyMode()) return;
  297. if (!hasId)
  298. {
  299. throw new GameNotFoundException("Game has an invalid ID");
  300. }
  301. ISchedulable task = new Once(() => { menuEngine.EnterGame(EGID); this.menuMode = false; });
  302. Scheduler.Schedule(task);
  303. }
  304. /// <summary>
  305. /// Return to the menu.
  306. /// Part of this always happens asynchronously, so when this method returns the game has not exited yet.
  307. /// Use the Client.EnterMenu event to perform operations after the game has completely exited.
  308. /// </summary>
  309. /// <param name="async">If set to <c>true</c>, do this async.</param>
  310. public void ExitGame(bool async = false)
  311. {
  312. if (!VerifyMode()) return;
  313. if (menuMode)
  314. {
  315. throw new GameNotFoundException("Cannot exit game using menu ID");
  316. }
  317. gameEngine.ExitCurrentGame(async);
  318. this.menuMode = true;
  319. }
  320. /// <summary>
  321. /// Saves the game.
  322. /// Part of this happens asynchronously, so when this method returns the game has not been saved yet.
  323. /// </summary>
  324. public void SaveGame()
  325. {
  326. if (!VerifyMode()) return;
  327. if (menuMode)
  328. {
  329. throw new GameNotFoundException("Cannot save game using menu ID");
  330. }
  331. gameEngine.SaveCurrentGame();
  332. }
  333. /// <summary>
  334. /// Add information to the in-game debug display.
  335. /// When this object is garbage collected, this debug info is automatically removed.
  336. /// The provided getter function is called each frame so make sure it returns quickly.
  337. /// </summary>
  338. /// <param name="id">Debug info identifier.</param>
  339. /// <param name="contentGetter">A function that returns the current information.</param>
  340. public void AddDebugInfo(string id, Func<string> contentGetter)
  341. {
  342. if (!VerifyMode()) return;
  343. if (menuMode)
  344. {
  345. throw new GameNotFoundException("Game object references a menu item but AddDebugInfo only works on the currently-loaded game");
  346. }
  347. debugOverlayEngine.SetInfo("game_" + id, contentGetter);
  348. debugIds.Add(id);
  349. }
  350. /// <summary>
  351. /// Remove information from the in-game debug display.
  352. /// </summary>
  353. /// <returns><c>true</c>, if debug info was removed, <c>false</c> otherwise.</returns>
  354. /// <param name="id">Debug info identifier.</param>
  355. public bool RemoveDebugInfo(string id)
  356. {
  357. if (!VerifyMode()) return false;
  358. if (menuMode)
  359. {
  360. throw new GameNotFoundException("Game object references a menu item but RemoveDebugInfo only works on the currently-loaded game");
  361. }
  362. if (!debugIds.Contains(id)) return false;
  363. debugOverlayEngine.RemoveInfo("game_" + id);
  364. return debugIds.Remove(id);
  365. }
  366. /// <summary>
  367. /// Add information to the in-game debug display.
  368. /// This debug info will be present for all games until it is manually removed.
  369. /// The provided getter function is called each frame so make sure it returns quickly.
  370. /// </summary>
  371. /// <param name="id">Debug info identifier.</param>
  372. /// <param name="contentGetter">A function that returns the current information.</param>
  373. public static void AddPersistentDebugInfo(string id, Func<string> contentGetter)
  374. {
  375. debugOverlayEngine.SetInfo("persistent_" + id, contentGetter);
  376. }
  377. /// <summary>
  378. /// Remove persistent information from the in-game debug display.
  379. /// </summary>
  380. /// <returns><c>true</c>, if debug info was removed, <c>false</c> otherwise.</returns>
  381. /// <param name="id">Debug info identifier.</param>
  382. public static bool RemovePersistentDebugInfo(string id)
  383. {
  384. return debugOverlayEngine.RemoveInfo("persistent_" + id);
  385. }
  386. /// <summary>
  387. /// Gets the blocks in the game.
  388. /// This returns null when in a loading state, and throws AppStateException when in menu.
  389. /// </summary>
  390. /// <returns>The blocks in game.</returns>
  391. /// <param name="filter">The block to search for. BlockIDs.Invalid will return all blocks.</param>
  392. public Block[] GetBlocksInGame(BlockIDs filter = BlockIDs.Invalid)
  393. {
  394. if (!VerifyMode()) return null;
  395. if (menuMode)
  396. {
  397. throw new AppStateException("Game object references a menu item but GetBlocksInGame only works on the currently-loaded game");
  398. }
  399. EGID[] blockEGIDs = gameEngine.GetAllBlocksInGame(filter);
  400. Block[] blocks = new Block[blockEGIDs.Length];
  401. for (int b = 0; b < blockEGIDs.Length; b++)
  402. {
  403. blocks[b] = Block.New(blockEGIDs[b]);
  404. }
  405. return blocks;
  406. }
  407. /// <summary>
  408. /// Enable the screenshot taker for updating the game's screenshot. Breaks the pause menu in a new save.
  409. /// </summary>
  410. public void EnableScreenshotTaker()
  411. {
  412. if (!VerifyMode()) return;
  413. gameEngine.EnableScreenshotTaker();
  414. }
  415. ~Game()
  416. {
  417. foreach (string id in debugIds)
  418. {
  419. debugOverlayEngine.RemoveInfo(id);
  420. }
  421. }
  422. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  423. private bool VerifyMode()
  424. {
  425. if (menuMode && (!menuEngine.IsInMenu || gameEngine.IsInGame))
  426. {
  427. // either game loading or API is broken
  428. return false;
  429. }
  430. if (!menuMode && (menuEngine.IsInMenu || !gameEngine.IsInGame))
  431. {
  432. // either game loading or API is broken
  433. return false;
  434. }
  435. return true;
  436. }
  437. internal static void Init()
  438. {
  439. GameEngineManager.AddGameEngine(gameEngine);
  440. GameEngineManager.AddGameEngine(debugOverlayEngine);
  441. GameEngineManager.AddGameEngine(buildSimEventEngine);
  442. MenuEngineManager.AddMenuEngine(menuEngine);
  443. }
  444. }
  445. }