diff --git a/GamecraftModdingAPI/App/AppCallbacksTest.cs b/GamecraftModdingAPI/App/AppCallbacksTest.cs new file mode 100644 index 0000000..0c2987b --- /dev/null +++ b/GamecraftModdingAPI/App/AppCallbacksTest.cs @@ -0,0 +1,31 @@ +using System; + +using GamecraftModdingAPI.Tests; + +namespace GamecraftModdingAPI.App +{ +#if TEST + [APITestClass] + public static class AppCallbacksTest + { + [APITestStartUp] + public static void StartUp() + { + // this could be split into 6 separate test cases + Game.Enter += Assert.CallsBack("GameEnter"); + Game.Exit += Assert.CallsBack("GameExit"); + Game.Simulate += Assert.CallsBack("GameSimulate"); + Game.Edit += Assert.CallsBack("GameEdit"); + Client.EnterMenu += Assert.CallsBack("MenuEnter"); + Client.ExitMenu += Assert.CallsBack("MenuExit"); + } + + [APITestCase(TestType.Game)] + public static void Test() + { + // the test is actually completely implemented in StartUp() + // this is here just so it looks less weird (not required) + } + } +#endif +} diff --git a/GamecraftModdingAPI/App/AppEngine.cs b/GamecraftModdingAPI/App/AppEngine.cs new file mode 100644 index 0000000..4f20b7e --- /dev/null +++ b/GamecraftModdingAPI/App/AppEngine.cs @@ -0,0 +1,63 @@ +using System; + +using RobocraftX.GUI.MyGamesScreen; +using RobocraftX.GUI; +using Svelto.ECS; + +using GamecraftModdingAPI.Engines; +using GamecraftModdingAPI.Utility; + +namespace GamecraftModdingAPI.App +{ + public class AppEngine : IFactoryEngine + { + public event EventHandler EnterMenu; + + public event EventHandler ExitMenu; + + public IEntityFactory Factory { set; private get; } + + public string Name => "GamecraftModdingAPIAppEngine"; + + public bool isRemovable => false; + + public EntitiesDB entitiesDB { set; private get; } + + public void Dispose() + { + IsInMenu = false; + ExceptionUtil.InvokeEvent(ExitMenu, this, new MenuEventArgs { }); + } + + public void Ready() + { + IsInMenu = true; + ExceptionUtil.InvokeEvent(EnterMenu, this, new MenuEventArgs { }); + } + + // app functionality + + public bool IsInMenu + { + get; + private set; + } = false; + + public Game[] GetMyGames() + { + EntityCollection mgsevs = entitiesDB.QueryEntities(MyGamesScreenExclusiveGroups.MyGames); + Game[] games = new Game[mgsevs.count]; + for (int i = 0; i < mgsevs.count; i++) + { + Utility.Logging.MetaDebugLog($"Found game named {mgsevs[i].GameName}"); + games[i] = new Game(mgsevs[i].ID); + } + return games; + } + } + + public struct MenuEventArgs + { + + } +} diff --git a/GamecraftModdingAPI/App/AppExceptions.cs b/GamecraftModdingAPI/App/AppExceptions.cs new file mode 100644 index 0000000..2e67695 --- /dev/null +++ b/GamecraftModdingAPI/App/AppExceptions.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.Serialization; + +namespace GamecraftModdingAPI.App +{ + public class AppException : GamecraftModdingAPIException + { + public AppException() + { + } + + public AppException(string message) : base(message) + { + } + + public AppException(string message, Exception innerException) : base(message, innerException) + { + } + } + + public class AppStateException : AppException + { + public AppStateException() + { + } + + public AppStateException(string message) : base(message) + { + } + } + + public class GameNotFoundException : AppException + { + public GameNotFoundException() + { + } + + public GameNotFoundException(string message) : base(message) + { + } + } +} diff --git a/GamecraftModdingAPI/App/Client.cs b/GamecraftModdingAPI/App/Client.cs new file mode 100644 index 0000000..2f1f005 --- /dev/null +++ b/GamecraftModdingAPI/App/Client.cs @@ -0,0 +1,49 @@ +using System; + +using UnityEngine; + +using GamecraftModdingAPI.Utility; + +namespace GamecraftModdingAPI.App +{ + public class Client + { + protected static AppEngine appEngine = new AppEngine(); + + public static event EventHandler EnterMenu + { + add => appEngine.EnterMenu += value; + remove => appEngine.EnterMenu -= value; + } + + public static event EventHandler ExitMenu + { + add => appEngine.ExitMenu += value; + remove => appEngine.ExitMenu -= value; + } + + public string Version + { + get => Application.version; + } + + public string UnityVersion + { + get => Application.unityVersion; + } + + public Game[] MyGames + { + get + { + if (!appEngine.IsInMenu) return new Game[0]; + return appEngine.GetMyGames(); + } + } + + internal static void Init() + { + MenuEngineManager.AddMenuEngine(appEngine); + } + } +} diff --git a/GamecraftModdingAPI/App/Game.cs b/GamecraftModdingAPI/App/Game.cs new file mode 100644 index 0000000..0d31abf --- /dev/null +++ b/GamecraftModdingAPI/App/Game.cs @@ -0,0 +1,331 @@ +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.Tasks; +using GamecraftModdingAPI.Utility; +// TODO: exceptions + +namespace GamecraftModdingAPI.App +{ + public class Game + { + 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 debugIds = new List(); + + private bool menuMode = true; + private bool hasId = false; + + public Game(uint id) : this(new EGID(id, MyGamesScreenExclusiveGroups.MyGames)) + { + } + + 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?)"); + } + + 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."); + } + + public static Game CurrentGame() + { + return new Game(); + } + + 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); + } + + public static event EventHandler Simulate + { + add => buildSimEventEngine.SimulationMode += value; + remove => buildSimEventEngine.SimulationMode -= value; + } + + public static event EventHandler Edit + { + add => buildSimEventEngine.BuildMode += value; + remove => buildSimEventEngine.BuildMode -= value; + } + + public static event EventHandler Enter + { + add => gameEngine.EnterGame += value; + remove => gameEngine.EnterGame -= value; + } + + public static event EventHandler Exit + { + add => gameEngine.ExitGame += value; + remove => gameEngine.ExitGame -= value; + } + + public uint Id + { + get; + private set; + } + + public EGID EGID + { + get; + private set; + } + + public bool MenuItem + { + get => menuMode && hasId; + } + + 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; + } + } + } + + 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 + } + } + } + + 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); + } + } + } + + 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); + } + } + } + + public bool IsSimulating + { + get + { + if (!VerifyMode()) return false; + return !menuMode && gameEngine.IsTimeRunningMode(); + } + + set + { + if (!VerifyMode()) return; + if (!menuMode && gameEngine.IsTimeRunningMode() != value) + gameEngine.ToggleTimeMode(); + } + } + + public bool IsTimeRunning + { + get => IsSimulating; + + set + { + IsSimulating = value; + } + } + + public bool IsTimeStopped + { + get + { + if (!VerifyMode()) return false; + return !menuMode && gameEngine.IsTimeStoppedMode(); + } + + set + { + if (!VerifyMode()) return; + if (!menuMode && gameEngine.IsTimeStoppedMode() != value) + gameEngine.ToggleTimeMode(); + } + } + + public void ToggleTimeMode() + { + if (!VerifyMode()) return; + if (menuMode || !gameEngine.IsInGame) + { + throw new AppStateException("Game menu item cannot toggle it's time mode"); + } + gameEngine.ToggleTimeMode(); + } + + 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); + } + + public void ExitGame() + { + if (!VerifyMode()) return; + if (menuMode) + { + throw new GameNotFoundException("Cannot exit game using menu ID"); + } + ISchedulable task = new Once(() => { gameEngine.ExitCurrentGame(); this.menuMode = true; }); + Scheduler.Schedule(task); + } + + public void AddDebugInfo(string id, Func 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); + } + + 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); + } + + ~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); + } + } +} diff --git a/GamecraftModdingAPI/App/GameBuildSimEventEngine.cs b/GamecraftModdingAPI/App/GameBuildSimEventEngine.cs new file mode 100644 index 0000000..b4bc049 --- /dev/null +++ b/GamecraftModdingAPI/App/GameBuildSimEventEngine.cs @@ -0,0 +1,48 @@ +using System; + +using RobocraftX.Common; +using RobocraftX.StateSync; +using Svelto.ECS; +using Unity.Jobs; + +using GamecraftModdingAPI.Engines; +using GamecraftModdingAPI.Utility; + +namespace GamecraftModdingAPI.App +{ + public class GameBuildSimEventEngine : IApiEngine, IUnorderedInitializeOnTimeRunningModeEntered, IUnorderedInitializeOnTimeStoppedModeEntered + { + public event EventHandler SimulationMode; + + public event EventHandler BuildMode; + + public string Name => "GamecraftModdingAPIBuildSimEventGameEngine"; + + public bool isRemovable => false; + + public EntitiesDB entitiesDB { set; private get; } + + public void Dispose() { } + + public void Ready() { } + + public JobHandle OnInitializeTimeRunningMode() + { + ExceptionUtil.InvokeEvent(SimulationMode, this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder }); + return default(JobHandle); + } + + public JobHandle OnInitializeTimeStoppedMode() + { + ExceptionUtil.InvokeEvent(BuildMode, this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder }); + return default(JobHandle); + } + } + + public struct GameEventArgs + { + public string GameName; + + public string GamePath; + } +} diff --git a/GamecraftModdingAPI/App/GameGameEngine.cs b/GamecraftModdingAPI/App/GameGameEngine.cs new file mode 100644 index 0000000..9253af2 --- /dev/null +++ b/GamecraftModdingAPI/App/GameGameEngine.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using HarmonyLib; + +using RobocraftX; +using RobocraftX.Common; +using RobocraftX.Schedulers; +using RobocraftX.SimulationModeState; +using Svelto.ECS; +using Svelto.Tasks; +using Svelto.Tasks.Lean; + +using GamecraftModdingAPI.Engines; +using GamecraftModdingAPI.Utility; + +namespace GamecraftModdingAPI.App +{ + public class GameGameEngine : IApiEngine + { + public event EventHandler EnterGame; + + public event EventHandler ExitGame; + + public string Name => "GamecraftModdingAPIGameInfoMenuEngine"; + + public bool isRemovable => false; + + public EntitiesDB entitiesDB { set; private get; } + + public void Dispose() + { + ExceptionUtil.InvokeEvent(ExitGame, this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder }); + IsInGame = false; + } + + public void Ready() + { + ExceptionUtil.InvokeEvent(EnterGame, this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder }); + IsInGame = true; + } + + // game functionality + + public bool IsInGame + { + get; + private set; + } = false; + + public void ExitCurrentGame() + { + ExitCurrentGameAsync().RunOn(Lean.EveryFrameStepRunner_RUNS_IN_TIME_STOPPED_AND_RUNNING); + } + + public IEnumerator ExitCurrentGameAsync() + { + /* + while (Lean.EveryFrameStepRunner_RUNS_IN_TIME_STOPPED_AND_RUNNING.isStopping) { yield return Yield.It; } + AccessTools.Method(typeof(FullGameCompositionRoot), "SwitchToMenu").Invoke(FullGameFields.Instance, new object[0]);*/ + yield return Yield.It; + entitiesDB.QueryEntity(CommonExclusiveGroups.GameSceneEGID).WantsToQuit = true; + entitiesDB.PublishEntityChange(CommonExclusiveGroups.GameSceneEGID); + } + + public bool IsTimeRunningMode() + { + return TimeRunningModeUtil.IsTimeRunningMode(entitiesDB); + } + + public bool IsTimeStoppedMode() + { + return TimeRunningModeUtil.IsTimeStoppedMode(entitiesDB); + } + + public void ToggleTimeMode() + { + TimeRunningModeUtil.ToggleTimeRunningState(entitiesDB); + } + } +} diff --git a/GamecraftModdingAPI/App/GameMenuEngine.cs b/GamecraftModdingAPI/App/GameMenuEngine.cs new file mode 100644 index 0000000..fc2c35f --- /dev/null +++ b/GamecraftModdingAPI/App/GameMenuEngine.cs @@ -0,0 +1,128 @@ +using System; +using HarmonyLib; + +using RobocraftX; +using RobocraftX.Common; +using RobocraftX.GUI; +using RobocraftX.GUI.MyGamesScreen; +using Svelto.ECS; +using Svelto.ECS.Experimental; + +using GamecraftModdingAPI.Engines; +using GamecraftModdingAPI.Utility; + +namespace GamecraftModdingAPI.App +{ + public class GameMenuEngine : IFactoryEngine + { + public IEntityFactory Factory { set; private get; } + + public string Name => "GamecraftModdingAPIGameInfoGameEngine"; + + public bool isRemovable => false; + + public EntitiesDB entitiesDB { set; private get; } + + public void Dispose() + { + IsInMenu = false; + } + + public void Ready() + { + IsInMenu = true; + } + + // game functionality + + public bool IsInMenu + { + get; + private set; + } = false; + + public bool CreateMyGame(EGID id, string path = "", uint thumbnailId = 0, string gameName = "", string creatorName = "", string description = "", long createdDate = 0L) + { + EntityComponentInitializer eci = Factory.BuildEntity(id); + eci.Init(new MyGameDataEntityStruct + { + SavedGamePath = new ECSString(path), + ThumbnailId = thumbnailId, + GameName = new ECSString(gameName), + CreatorName = new ECSString(creatorName), + GameDescription = new ECSString(description), + CreatedDate = createdDate, + }); + // entitiesDB.PublishEntityChange(id); // this will always fail + return true; + } + + public uint HighestID() + { + EntityCollection games = entitiesDB.QueryEntities(MyGamesScreenExclusiveGroups.MyGames); + uint max = 0; + for (int i = 0; i < games.count; i++) + { + if (games[i].ID.entityID > max) + { + max = games[i].ID.entityID; + } + } + return max; + } + + public bool EnterGame(EGID id) + { + if (!ExistsGameInfo(id)) return false; + ref MyGameDataEntityStruct mgdes = ref GetGameInfo(id); + return EnterGame(mgdes.GameName, mgdes.SavedGamePath); + } + + public bool EnterGame(string gameName, string path, ulong workshopId = 0uL, bool autoEnterSim = false) + { + GameMode.CurrentMode = autoEnterSim ? RCXMode.Play : RCXMode.Build; + GameMode.SaveGameDetails = new SaveGameDetails(gameName, path, workshopId); + // the private FullGameCompositionRoot.SwitchToGame() method gets passed to menu items for this reason + AccessTools.Method(typeof(FullGameCompositionRoot), "SwitchToGame").Invoke(FullGameFields.Instance, new object[0]); + return true; + } + + public bool SetGameName(EGID id, string name) + { + if (!ExistsGameInfo(id)) return false; + GetGameInfo(id).GameName.Set(name); + GetGameViewInfo(id).MyGamesSlotComponent.GameName = StringUtil.SanitiseString(name); + return true; + } + + public bool SetGameDescription(EGID id, string name) + { + if (!ExistsGameInfo(id)) return false; + GetGameInfo(id).GameDescription.Set(name); + GetGameViewInfo(id).MyGamesSlotComponent.GameDescription = StringUtil.SanitiseString(name); + return true; + } + + public bool ExistsGameInfo(EGID id) + { + return entitiesDB.Exists(id); + } + + public ref MyGameDataEntityStruct GetGameInfo(EGID id) + { + return ref GetComponent(id); + } + + public ref MyGamesSlotEntityViewStruct GetGameViewInfo(EGID id) + { + return ref GetComponent(new EGID(id.entityID, MyGamesScreenExclusiveGroups.GameSlotGuiEntities)); + } + + public ref T GetComponent(EGID id) where T: struct, IEntityComponent + { + return ref entitiesDB.QueryEntity(id); + } + } + + internal class MyGameDataEntityDescriptor_DamnItFJWhyDidYouMakeThisInternal : GenericEntityDescriptor { } +} diff --git a/GamecraftModdingAPI/App/StateSyncRegPatch.cs b/GamecraftModdingAPI/App/StateSyncRegPatch.cs new file mode 100644 index 0000000..9c2ce68 --- /dev/null +++ b/GamecraftModdingAPI/App/StateSyncRegPatch.cs @@ -0,0 +1,26 @@ +using System; +using System.Reflection; + +using RobocraftX.CR.MainGame; +using RobocraftX.StateSync; + +using HarmonyLib; + +namespace GamecraftModdingAPI.App +{ + [HarmonyPatch] + class StateSyncRegPatch + { + public static void Postfix(StateSyncRegistrationHelper stateSyncReg) + { + // register sim/build events engines + Game.InitDeterministic(stateSyncReg); + } + + [HarmonyTargetMethod] + public static MethodBase Target() + { + return AccessTools.Method(typeof(MainGameCompositionRoot), "DeterministicCompose").MakeGenericMethod(typeof(object)); + } + } +} diff --git a/GamecraftModdingAPI/Main.cs b/GamecraftModdingAPI/Main.cs index bea58e1..aa80e2a 100644 --- a/GamecraftModdingAPI/Main.cs +++ b/GamecraftModdingAPI/Main.cs @@ -72,6 +72,8 @@ namespace GamecraftModdingAPI Block.Init(); GameClient.Init(); AsyncUtils.Init(); + GamecraftModdingAPI.App.Client.Init(); + GamecraftModdingAPI.App.Game.Init(); Logging.MetaLog($"{currentAssembly.GetName().Name} v{currentAssembly.GetName().Version} initialized"); } diff --git a/GamecraftModdingAPI/Utility/FullGameFields.cs b/GamecraftModdingAPI/Utility/FullGameFields.cs index 5291d4a..8fd8895 100644 --- a/GamecraftModdingAPI/Utility/FullGameFields.cs +++ b/GamecraftModdingAPI/Utility/FullGameFields.cs @@ -25,8 +25,15 @@ namespace GamecraftModdingAPI.Utility /// public static class FullGameFields { + public static FullGameCompositionRoot Instance + { + private set; + get; + } = null; + public static MultiplayerInitParameters _multiplayerParams - { get + { + get { return (MultiplayerInitParameters)fgcr?.Field("_multiplayerParams").GetValue(); } @@ -157,6 +164,7 @@ namespace GamecraftModdingAPI.Utility public static void Init(FullGameCompositionRoot instance) { fgcr = new Traverse(instance); + FullGameFields.Instance = instance; } } }