diff --git a/Automation/bump_version.py b/Automation/bump_version.py
new file mode 100755
index 0000000..d3de051
--- /dev/null
+++ b/Automation/bump_version.py
@@ -0,0 +1,67 @@
+#!/usr/bin/python3
+
+import argparse
+import re
+# this assumes a mostly semver-complient version number
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Increment GamecraftModdingAPI version")
+ parser.add_argument('version', metavar="VN", type=str, help="The version number to increment, or the index of the number (zero-indexed).")
+ args = parser.parse_args()
+
+ version_index = -1
+ try:
+ version_index = int(args.version)
+ except Exception:
+ if args.version.lower() == "major":
+ version_index = 0
+ elif args.version.lower() == "minor":
+ version_index = 1
+ elif args.version.lower() == "patch":
+ version_index = 2
+
+ if version_index < 0:
+ print("Could not parse version argument.")
+ exit(version_index)
+
+ print(version_index)
+ old_version = ""
+ new_version = ""
+
+ with open("../GamecraftModdingAPI/GamecraftModdingAPI.csproj", "r") as xmlFile:
+ print("Parsing GamecraftModdingAPI.csproj")
+ fileStr = xmlFile.read()
+ versionMatch = re.search(r"(.+)", fileStr)
+ if versionMatch is None:
+ print("Unable to find version number in GamecraftModdingAPI.csproj")
+ exit(1)
+ old_version = versionMatch.group(1)
+ versionList = old_version.split(".")
+ if len(versionList) <= version_index:
+ print("Invalid version string")
+ exit(1)
+ versionList[version_index] = str(int(versionList[version_index]) + 1)
+ for i in range(version_index + 1, len(versionList)):
+ try:
+ int(versionList[i])
+ versionList[i] = "0"
+ except Exception:
+ tmp = versionList[i].split("-")
+ tmp[0] = "0"
+ versionList[i] = "-".join(tmp)
+ new_version = ".".join(versionList)
+ print(new_version)
+ newFileContents = fileStr.replace(""+old_version+"", ""+new_version+"")
+
+ with open("../GamecraftModdingAPI/GamecraftModdingAPI.csproj", "w") as xmlFile:
+ print("Writing new version to project file")
+ xmlFile.write(newFileContents)
+
+ with open("../doxygen.conf", "r") as doxFile:
+ print("Parsing doxygen.conf")
+ doxStr = doxFile.read()
+ newFileContents = doxStr.replace("= \"v" + old_version + "\"", "= \"v" + new_version + "\"")
+
+ with open("../doxygen.conf", "w") as doxFile:
+ print("Writing new version to doxygen config")
+ doxFile.write(newFileContents)
diff --git a/GamecraftModdingAPI.sln b/GamecraftModdingAPI.sln
index ff9fc7f..6482776 100644
--- a/GamecraftModdingAPI.sln
+++ b/GamecraftModdingAPI.sln
@@ -9,12 +9,15 @@ Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
+ Test|Any CPU = Test|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7FD5A7D8-4F3E-426A-B07D-7DC70442A4DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7FD5A7D8-4F3E-426A-B07D-7DC70442A4DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7FD5A7D8-4F3E-426A-B07D-7DC70442A4DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7FD5A7D8-4F3E-426A-B07D-7DC70442A4DF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7FD5A7D8-4F3E-426A-B07D-7DC70442A4DF}.Test|Any CPU.ActiveCfg = Test|Any CPU
+ {7FD5A7D8-4F3E-426A-B07D-7DC70442A4DF}.Test|Any CPU.Build.0 = Test|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/GamecraftModdingAPI/App/AppCallbacksTest.cs b/GamecraftModdingAPI/App/AppCallbacksTest.cs
new file mode 100644
index 0000000..041ac50
--- /dev/null
+++ b/GamecraftModdingAPI/App/AppCallbacksTest.cs
@@ -0,0 +1,35 @@
+using System;
+
+using GamecraftModdingAPI.Tests;
+
+namespace GamecraftModdingAPI.App
+{
+#if TEST
+ ///
+ /// App callbacks tests.
+ /// Only available in TEST builds.
+ ///
+ [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..ca5dcfa
--- /dev/null
+++ b/GamecraftModdingAPI/App/Client.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Reflection;
+using HarmonyLib;
+
+using RobocraftX.Services;
+using UnityEngine;
+
+using GamecraftModdingAPI.Utility;
+using RobocraftX.Common;
+
+namespace GamecraftModdingAPI.App
+{
+ ///
+ /// The Gamecraft application that is running this code right now.
+ ///
+ public class Client
+ {
+ // extensible engine
+ protected static AppEngine appEngine = new AppEngine();
+
+ protected static Func