Add Client and Game OOP features (undocumented)

This commit is contained in:
NGnius (Graham) 2020-06-17 21:04:08 -04:00
parent 0d17a1b509
commit c912f3ba64
11 changed files with 809 additions and 1 deletions

View file

@ -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<GameEventArgs>("GameEnter");
Game.Exit += Assert.CallsBack<GameEventArgs>("GameExit");
Game.Simulate += Assert.CallsBack<GameEventArgs>("GameSimulate");
Game.Edit += Assert.CallsBack<GameEventArgs>("GameEdit");
Client.EnterMenu += Assert.CallsBack<MenuEventArgs>("MenuEnter");
Client.ExitMenu += Assert.CallsBack<MenuEventArgs>("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
}

View file

@ -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<MenuEventArgs> EnterMenu;
public event EventHandler<MenuEventArgs> 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<MyGameDataEntityStruct> mgsevs = entitiesDB.QueryEntities<MyGameDataEntityStruct>(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
{
}
}

View file

@ -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)
{
}
}
}

View file

@ -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<MenuEventArgs> EnterMenu
{
add => appEngine.EnterMenu += value;
remove => appEngine.EnterMenu -= value;
}
public static event EventHandler<MenuEventArgs> 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);
}
}
}

View file

@ -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<string> debugIds = new List<string>();
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<GameEventArgs> Simulate
{
add => buildSimEventEngine.SimulationMode += value;
remove => buildSimEventEngine.SimulationMode -= value;
}
public static event EventHandler<GameEventArgs> Edit
{
add => buildSimEventEngine.BuildMode += value;
remove => buildSimEventEngine.BuildMode -= value;
}
public static event EventHandler<GameEventArgs> Enter
{
add => gameEngine.EnterGame += value;
remove => gameEngine.EnterGame -= value;
}
public static event EventHandler<GameEventArgs> 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<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);
}
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);
}
}
}

View file

@ -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<GameEventArgs> SimulationMode;
public event EventHandler<GameEventArgs> 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;
}
}

View file

@ -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<GameEventArgs> EnterGame;
public event EventHandler<GameEventArgs> 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<TaskContract> 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<GameSceneEntityStruct>(CommonExclusiveGroups.GameSceneEGID).WantsToQuit = true;
entitiesDB.PublishEntityChange<GameSceneEntityStruct>(CommonExclusiveGroups.GameSceneEGID);
}
public bool IsTimeRunningMode()
{
return TimeRunningModeUtil.IsTimeRunningMode(entitiesDB);
}
public bool IsTimeStoppedMode()
{
return TimeRunningModeUtil.IsTimeStoppedMode(entitiesDB);
}
public void ToggleTimeMode()
{
TimeRunningModeUtil.ToggleTimeRunningState(entitiesDB);
}
}
}

View file

@ -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<MyGameDataEntityDescriptor_DamnItFJWhyDidYouMakeThisInternal>(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<MyGameDataEntityStruct>(id); // this will always fail
return true;
}
public uint HighestID()
{
EntityCollection<MyGameDataEntityStruct> games = entitiesDB.QueryEntities<MyGameDataEntityStruct>(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<MyGameDataEntityStruct>(id);
}
public ref MyGameDataEntityStruct GetGameInfo(EGID id)
{
return ref GetComponent<MyGameDataEntityStruct>(id);
}
public ref MyGamesSlotEntityViewStruct GetGameViewInfo(EGID id)
{
return ref GetComponent<MyGamesSlotEntityViewStruct>(new EGID(id.entityID, MyGamesScreenExclusiveGroups.GameSlotGuiEntities));
}
public ref T GetComponent<T>(EGID id) where T: struct, IEntityComponent
{
return ref entitiesDB.QueryEntity<T>(id);
}
}
internal class MyGameDataEntityDescriptor_DamnItFJWhyDidYouMakeThisInternal : GenericEntityDescriptor<MyGameDataEntityStruct> { }
}

View file

@ -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));
}
}
}

View file

@ -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");
}

View file

@ -25,8 +25,15 @@ namespace GamecraftModdingAPI.Utility
/// </summary>
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;
}
}
}