using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using RobocraftX.Common; using RobocraftX.GUI.MyGamesScreen; using RobocraftX.StateSync; using Svelto.ECS; using GamecraftModdingAPI; using GamecraftModdingAPI.Blocks; using GamecraftModdingAPI.Tasks; using GamecraftModdingAPI.Utility; namespace GamecraftModdingAPI.App { /// <summary> /// An in-game save. /// This can be a menu item for a local save or the currently loaded save. /// Support for Steam Workshop coming soon (hopefully). /// </summary> public class Game { // extensible engines protected static GameGameEngine gameEngine = new GameGameEngine(); protected static GameMenuEngine menuEngine = new GameMenuEngine(); protected static DebugInterfaceEngine debugOverlayEngine = new DebugInterfaceEngine(); protected static GameBuildSimEventEngine buildSimEventEngine = new GameBuildSimEventEngine(); private List<string> debugIds = new List<string>(); private bool menuMode = true; private bool hasId = false; /// <summary> /// Initializes a new instance of the <see cref="T:GamecraftModdingAPI.App.Game"/> class. /// </summary> /// <param name="id">Menu identifier.</param> public Game(uint id) : this(new EGID(id, MyGamesScreenExclusiveGroups.MyGames)) { } /// <summary> /// Initializes a new instance of the <see cref="T:GamecraftModdingAPI.App.Game"/> class. /// </summary> /// <param name="id">Menu identifier.</param> public Game(EGID id) { this.Id = id.entityID; this.EGID = id; this.hasId = true; menuMode = true; 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?)"); } /// <summary> /// Initializes a new instance of the <see cref="T:GamecraftModdingAPI.App.Game"/> class without id. /// This is assumed to be the current game. /// </summary> public Game() { menuMode = false; 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?)"); if (menuEngine.IsInMenu) throw new GameNotFoundException("Game not found."); } /// <summary> /// Returns the currently loaded game. /// If in a menu, manipulating the returned object may not work as intended. /// </summary> /// <returns>The current game.</returns> public static Game CurrentGame() { return new Game(); } /// <summary> /// Creates a new game and adds it to the menu. /// If not in a menu, this will throw AppStateException. /// </summary> /// <returns>The new game.</returns> public static Game NewGame() { if (!menuEngine.IsInMenu) throw new AppStateException("New Game cannot be created while not in a menu."); uint nextId = menuEngine.HighestID() + 1; EGID egid = new EGID(nextId, MyGamesScreenExclusiveGroups.MyGames); menuEngine.CreateMyGame(egid); return new Game(egid); } /// <summary> /// An event that fires whenever a game is switched to simulation mode (time running mode). /// </summary> public static event EventHandler<GameEventArgs> Simulate { add => buildSimEventEngine.SimulationMode += value; remove => buildSimEventEngine.SimulationMode -= value; } /// <summary> /// An event that fires whenever a game is switched to edit mode (time stopped mode). /// This does not fire when a game is loaded. /// </summary> public static event EventHandler<GameEventArgs> Edit { add => buildSimEventEngine.BuildMode += value; remove => buildSimEventEngine.BuildMode -= value; } /// <summary> /// An event that fires right after a game is completely loaded. /// </summary> public static event EventHandler<GameEventArgs> Enter { add => gameEngine.EnterGame += value; remove => gameEngine.EnterGame -= value; } /// <summary> /// An event that fires right before a game returns to the main menu. /// At this point, Gamecraft is transitioning state so many things are invalid/unstable here. /// </summary> public static event EventHandler<GameEventArgs> Exit { add => gameEngine.ExitGame += value; remove => gameEngine.ExitGame -= value; } /// <summary> /// The game's unique menu identifier. /// </summary> /// <value>The identifier.</value> public uint Id { get; private set; } /// <summary> /// The game's unique menu EGID. /// </summary> /// <value>The egid.</value> public EGID EGID { get; private set; } /// <summary> /// Whether the game is a (valid) menu item. /// </summary> /// <value><c>true</c> if menu item; otherwise, <c>false</c>.</value> public bool MenuItem { get => menuMode && hasId; } /// <summary> /// The game's name. /// </summary> /// <value>The name.</value> public string Name { get { if (!VerifyMode()) return null; if (menuMode) return menuEngine.GetGameInfo(EGID).GameName; return GameMode.SaveGameDetails.Name; } set { if (!VerifyMode()) return; if (menuMode) { menuEngine.SetGameName(EGID, value); } else { GameMode.SaveGameDetails.Name = value; } } } /// <summary> /// The game's description. /// </summary> /// <value>The description.</value> public string Description { get { if (!VerifyMode()) return null; if (menuMode) return menuEngine.GetGameInfo(EGID).GameDescription; return ""; } set { if (!VerifyMode()) return; if (menuMode) { menuEngine.SetGameDescription(EGID, value); } else { // No description exists in-game } } } /// <summary> /// The path to the game's save folder. /// </summary> /// <value>The path.</value> public string Path { get { if (!VerifyMode()) return null; if (menuMode) return menuEngine.GetGameInfo(EGID).SavedGamePath; return GameMode.SaveGameDetails.Folder; } set { if (!VerifyMode()) return; if (menuMode) { menuEngine.GetGameInfo(EGID).SavedGamePath.Set(value); } else { // this likely breaks things GameMode.SaveGameDetails = new SaveGameDetails(GameMode.SaveGameDetails.Name, value, GameMode.SaveGameDetails.WorkshopId); } } } /// <summary> /// The Steam Workshop Id of the game save. /// In most cases this is invalid and returns 0, so this can be ignored. /// </summary> /// <value>The workshop identifier.</value> public ulong WorkshopId { get { if (!VerifyMode()) return 0uL; if (menuMode) return 0uL; // MyGames don't have workshop IDs return GameMode.SaveGameDetails.WorkshopId; } set { VerifyMode(); if (menuMode) { // MyGames don't have workshop IDs // menuEngine.GetGameInfo(EGID).GameName.Set(value); } else { // this likely breaks things GameMode.SaveGameDetails = new SaveGameDetails(GameMode.SaveGameDetails.Name, GameMode.SaveGameDetails.Folder, value); } } } /// <summary> /// Whether the game is in simulation mode. /// </summary> /// <value><c>true</c> if is simulating; otherwise, <c>false</c>.</value> public bool IsSimulating { get { if (!VerifyMode()) return false; return !menuMode && gameEngine.IsTimeRunningMode(); } set { if (!VerifyMode()) return; if (!menuMode && gameEngine.IsTimeRunningMode() != value) gameEngine.ToggleTimeMode(); } } /// <summary> /// Whether the game is in time-running mode. /// Alias of IsSimulating. /// </summary> /// <value><c>true</c> if is time running; otherwise, <c>false</c>.</value> public bool IsTimeRunning { get => IsSimulating; set { IsSimulating = value; } } /// <summary> /// Whether the game is in time-stopped mode. /// </summary> /// <value><c>true</c> if is time stopped; otherwise, <c>false</c>.</value> public bool IsTimeStopped { get { if (!VerifyMode()) return false; return !menuMode && gameEngine.IsTimeStoppedMode(); } set { if (!VerifyMode()) return; if (!menuMode && gameEngine.IsTimeStoppedMode() != value) gameEngine.ToggleTimeMode(); } } /// <summary> /// Toggles the time mode. /// </summary> public void ToggleTimeMode() { if (!VerifyMode()) return; if (menuMode || !gameEngine.IsInGame) { throw new AppStateException("Game menu item cannot toggle it's time mode"); } gameEngine.ToggleTimeMode(); } /// <summary> /// Load the game save. /// This happens asynchronously, so when this method returns the game not loaded yet. /// Use the Game.Enter event to perform operations after the game has completely loaded. /// </summary> public void EnterGame() { if (!VerifyMode()) return; if (!hasId) { throw new GameNotFoundException("Game has an invalid ID"); } ISchedulable task = new Once(() => { menuEngine.EnterGame(EGID); this.menuMode = false; }); Scheduler.Schedule(task); } /// <summary> /// Return to the menu. /// Part of this always happens asynchronously, so when this method returns the game has not exited yet. /// Use the Client.EnterMenu event to perform operations after the game has completely exited. /// </summary> /// <param name="async">If set to <c>true</c>, do this async.</param> public void ExitGame(bool async = false) { if (!VerifyMode()) return; if (menuMode) { throw new GameNotFoundException("Cannot exit game using menu ID"); } gameEngine.ExitCurrentGame(async); this.menuMode = true; } /// <summary> /// Saves the game. /// Part of this happens asynchronously, so when this method returns the game has not been saved yet. /// </summary> public void SaveGame() { if (!VerifyMode()) return; if (menuMode) { throw new GameNotFoundException("Cannot save game using menu ID"); } gameEngine.SaveCurrentGame(); } /// <summary> /// Add information to the in-game debug display. /// When this object is garbage collected, this debug info is automatically removed. /// </summary> /// <param name="id">Debug info identifier.</param> /// <param name="contentGetter">Content getter.</param> public void AddDebugInfo(string id, Func<string> contentGetter) { if (!VerifyMode()) return; if (menuMode) { throw new GameNotFoundException("Game object references a menu item but AddDebugInfo only works on the currently-loaded game"); } debugOverlayEngine.SetInfo(id, contentGetter); debugIds.Add(id); } /// <summary> /// Remove information from the in-game debug display. /// </summary> /// <returns><c>true</c>, if debug info was removed, <c>false</c> otherwise.</returns> /// <param name="id">Debug info identifier.</param> public bool RemoveDebugInfo(string id) { if (!VerifyMode()) return false; if (menuMode) { throw new GameNotFoundException("Game object references a menu item but RemoveDebugInfo only works on the currently-loaded game"); } if (!debugIds.Contains(id)) return false; debugOverlayEngine.RemoveInfo(id); return debugIds.Remove(id); } /// <summary> /// Gets the blocks in the game. /// This returns null when in a loading state, and throws AppStateException when in menu. /// </summary> /// <returns>The blocks in game.</returns> /// <param name="filter">The block to search for. BlockIDs.Invalid will return all blocks.</param> public Block[] GetBlocksInGame(BlockIDs filter = BlockIDs.Invalid) { if (!VerifyMode()) return null; if (menuMode) { throw new AppStateException("Game object references a menu item but GetBlocksInGame only works on the currently-loaded game"); } EGID[] blockEGIDs = gameEngine.GetAllBlocksInGame(filter); Block[] blocks = new Block[blockEGIDs.Length]; for (int b = 0; b < blockEGIDs.Length; b++) { blocks[b] = new Block(blockEGIDs[b]); } return blocks; } ~Game() { foreach (string id in debugIds) { debugOverlayEngine.RemoveInfo(id); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool VerifyMode() { if (menuMode && (!menuEngine.IsInMenu || gameEngine.IsInGame)) { // either game loading or API is broken return false; } if (!menuMode && (menuEngine.IsInMenu || !gameEngine.IsInGame)) { // either game loading or API is broken return false; } return true; } internal static void Init() { GameEngineManager.AddGameEngine(gameEngine); GameEngineManager.AddGameEngine(debugOverlayEngine); MenuEngineManager.AddMenuEngine(menuEngine); } internal static void InitDeterministic(StateSyncRegistrationHelper stateSyncReg) { stateSyncReg.AddDeterministicEngine(buildSimEventEngine); } } }