Browse Source

Add Client and Game OOP features (undocumented)

tags/v1.3.0
NGnius (Graham) 3 years ago
parent
commit
c912f3ba64
11 changed files with 809 additions and 1 deletions
  1. +31
    -0
      GamecraftModdingAPI/App/AppCallbacksTest.cs
  2. +63
    -0
      GamecraftModdingAPI/App/AppEngine.cs
  3. +42
    -0
      GamecraftModdingAPI/App/AppExceptions.cs
  4. +49
    -0
      GamecraftModdingAPI/App/Client.cs
  5. +331
    -0
      GamecraftModdingAPI/App/Game.cs
  6. +48
    -0
      GamecraftModdingAPI/App/GameBuildSimEventEngine.cs
  7. +80
    -0
      GamecraftModdingAPI/App/GameGameEngine.cs
  8. +128
    -0
      GamecraftModdingAPI/App/GameMenuEngine.cs
  9. +26
    -0
      GamecraftModdingAPI/App/StateSyncRegPatch.cs
  10. +2
    -0
      GamecraftModdingAPI/Main.cs
  11. +9
    -1
      GamecraftModdingAPI/Utility/FullGameFields.cs

+ 31
- 0
GamecraftModdingAPI/App/AppCallbacksTest.cs 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
}

+ 63
- 0
GamecraftModdingAPI/App/AppEngine.cs 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
{

}
}

+ 42
- 0
GamecraftModdingAPI/App/AppExceptions.cs 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)
{
}
}
}

+ 49
- 0
GamecraftModdingAPI/App/Client.cs 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);
}
}
}

+ 331
- 0
GamecraftModdingAPI/App/Game.cs 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);
}
}
}

+ 48
- 0
GamecraftModdingAPI/App/GameBuildSimEventEngine.cs 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;
}
}

+ 80
- 0
GamecraftModdingAPI/App/GameGameEngine.cs 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);
}
}
}

+ 128
- 0
GamecraftModdingAPI/App/GameMenuEngine.cs 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> { }
}

+ 26
- 0
GamecraftModdingAPI/App/StateSyncRegPatch.cs 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));
}
}
}

+ 2
- 0
GamecraftModdingAPI/Main.cs 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");
}



+ 9
- 1
GamecraftModdingAPI/Utility/FullGameFields.cs 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;
}
}
}

Loading…
Cancel
Save