Merge branch 'master' into customblocks

# Conflicts:
#	GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs
This commit is contained in:
Norbi Peti 2020-09-17 19:18:00 +02:00
commit f295f712b6
74 changed files with 4550 additions and 1275 deletions

67
Automation/bump_version.py Executable file
View file

@ -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"<Version>(.+)</Version>", 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("<Version>"+old_version+"</Version>", "<Version>"+new_version+"</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)

View file

@ -9,12 +9,15 @@ Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Test|Any CPU = Test|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7FD5A7D8-4F3E-426A-B07D-7DC70442A4DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {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}.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.ActiveCfg = Release|Any CPU
{7FD5A7D8-4F3E-426A-B07D-7DC70442A4DF}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View file

@ -0,0 +1,35 @@
using System;
using GamecraftModdingAPI.Tests;
namespace GamecraftModdingAPI.App
{
#if TEST
/// <summary>
/// App callbacks tests.
/// Only available in TEST builds.
/// </summary>
[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,159 @@
using System;
using System.Reflection;
using HarmonyLib;
using RobocraftX.Services;
using UnityEngine;
using GamecraftModdingAPI.Utility;
using RobocraftX.Common;
namespace GamecraftModdingAPI.App
{
/// <summary>
/// The Gamecraft application that is running this code right now.
/// </summary>
public class Client
{
// extensible engine
protected static AppEngine appEngine = new AppEngine();
protected static Func<object> ErrorHandlerInstanceGetter;
protected static Action<object, Error> EnqueueError;
protected static Action<object> HandleErrorClosed;
/// <summary>
/// An event that fires whenever the main menu is loaded.
/// </summary>
public static event EventHandler<MenuEventArgs> EnterMenu
{
add => appEngine.EnterMenu += value;
remove => appEngine.EnterMenu -= value;
}
/// <summary>
/// An event that fire whenever the main menu is exited.
/// </summary>
public static event EventHandler<MenuEventArgs> ExitMenu
{
add => appEngine.ExitMenu += value;
remove => appEngine.ExitMenu -= value;
}
/// <summary>
/// Gamecraft build version string.
/// Usually this is in the form YYYY.mm.DD.HH.MM.SS
/// </summary>
/// <value>The version.</value>
public string Version
{
get => Application.version;
}
/// <summary>
/// Unity version string.
/// </summary>
/// <value>The unity version.</value>
public string UnityVersion
{
get => Application.unityVersion;
}
/// <summary>
/// Game saves currently visible in the menu.
/// These take a second to completely populate after the EnterMenu event fires.
/// </summary>
/// <value>My games.</value>
public Game[] MyGames
{
get
{
if (!appEngine.IsInMenu) return new Game[0];
return appEngine.GetMyGames();
}
}
/// <summary>
/// Whether Gamecraft is in the Main Menu
/// </summary>
/// <value><c>true</c> if in menu; <c>false</c> when loading or in a game.</value>
public bool InMenu
{
get => appEngine.IsInMenu;
}
/// <summary>
/// Open a popup which prompts the user to click a button.
/// This reuses Gamecraft's error dialog popup
/// </summary>
/// <param name="popup">The popup to display. Use an instance of SingleChoicePrompt or DualChoicePrompt.</param>
public void PromptUser(Error popup)
{
// if the stuff wasn't mostly set to internal, this would be written as:
// RobocraftX.Services.ErrorHandler.Instance.EqueueError(error);
object errorHandlerInstance = ErrorHandlerInstanceGetter();
EnqueueError(errorHandlerInstance, popup);
}
// TODO
/*public void CloseCurrentPrompt()
{
// RobocraftX.Services.ErrorHandler.Instance.HandlePopupClosed();
// FIXME: this is a call that is also called when closing, not the actual closing action itself (so it doesn't work)
object errorHandlerInstance = ErrorHandlerInstanceGetter();
HandleErrorClosed(errorHandlerInstance);
}*/
internal static void Init()
{
// this would have been so much simpler if this didn't involve a bunch of internal fields & classes
Type errorHandler = AccessTools.TypeByName("RobocraftX.Services.ErrorHandler");
Type errorHandle = AccessTools.TypeByName("RobocraftX.Services.ErrorHandle");
ErrorHandlerInstanceGetter = (Func<object>) AccessTools.Method("GamecraftModdingAPI.App.Client:GenInstanceGetter")
.MakeGenericMethod(errorHandler)
.Invoke(null, new object[0]);
EnqueueError = (Action<object, Error>) AccessTools.Method("GamecraftModdingAPI.App.Client:GenEnqueueError")
.MakeGenericMethod(errorHandler, errorHandle)
.Invoke(null, new object[0]);
/*HandleErrorClosed = (Action<object>) AccessTools.Method("GamecraftModdingAPI.App.Client:GenHandlePopupClosed")
.MakeGenericMethod(errorHandler)
.Invoke(null, new object[0]);*/
// register engines
MenuEngineManager.AddMenuEngine(appEngine);
}
// Creating delegates once is faster than reflection every time
// Admittedly, this way is more difficult to code and less readable
private static Func<object> GenInstanceGetter<T>()
{
Type errorHandler = AccessTools.TypeByName("RobocraftX.Services.ErrorHandler");
MethodInfo instance = AccessTools.PropertyGetter(errorHandler, "Instance");
Func<T> getterSimple = (Func<T>) Delegate.CreateDelegate(typeof(Func<T>), null, instance);
Func<object> getterCasted = () => (object) getterSimple();
return getterCasted;
}
private static Action<object, Error> GenEnqueueError<T, TRes>()
{
Type errorHandler = AccessTools.TypeByName("RobocraftX.Services.ErrorHandler");
MethodInfo enqueueError = AccessTools.Method(errorHandler, "EnqueueError");
Func<T, Error, TRes> enqueueSimple =
(Func<T, Error, TRes>) Delegate.CreateDelegate(typeof(Func<T, Error, TRes>), enqueueError);
Action<object, Error> enqueueCasted =
(object instance, Error error) => { enqueueSimple((T) instance, error); };
return enqueueCasted;
}
private static Action<object> GenHandlePopupClosed<T>()
{
Type errorHandler = AccessTools.TypeByName("RobocraftX.Services.ErrorHandler");
MethodInfo handlePopupClosed = AccessTools.Method(errorHandler, "HandlePopupClosed");
Action<T> handleSimple =
(Action<T>) Delegate.CreateDelegate(typeof(Action<T>), handlePopupClosed);
Action<object> handleCasted = (object instance) => handleSimple((T) instance);
return handleCasted;
}
}
}

View file

@ -0,0 +1,59 @@
using System;
using HarmonyLib;
using RobocraftX.Services;
using GamecraftModdingAPI.Tests;
namespace GamecraftModdingAPI.App
{
#if TEST
/// <summary>
/// Client popups tests.
/// Only available in TEST builds.
/// </summary>
[APITestClass]
public static class ClientAlertTest
{
private static DualChoicePrompt popup2 = null;
private static SingleChoicePrompt popup1 = null;
[APITestStartUp]
public static void StartUp2()
{
popup2 = new DualChoicePrompt("This is a test double-button popup",
"The cake is a lie",
"lmao",
() => { },
"kek",
() => { });
}
[APITestStartUp]
public static void StartUp1()
{
popup1 = new SingleChoicePrompt("The cake is a lie",
"This is a test single-button popup",
"qwertyuiop",
() => { });
}
[APITestCase(TestType.Menu)]
public static void TestPopUp2()
{
Client c = new Client();
c.PromptUser(popup2);
//c.CloseCurrentPrompt();
}
[APITestCase(TestType.Menu)]
public static void TestPopUp1()
{
Client c = new Client();
c.PromptUser(popup1);
//c.CloseCurrentPrompt();
}
}
#endif
}

View file

@ -0,0 +1,477 @@
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);
}
}
}

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(JobHandle inputDeps)
{
ExceptionUtil.InvokeEvent(SimulationMode, this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder });
return inputDeps;
}
public JobHandle OnInitializeTimeStoppedMode(JobHandle inputDeps)
{
ExceptionUtil.InvokeEvent(BuildMode, this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder });
return inputDeps;
}
}
public struct GameEventArgs
{
public string GameName;
public string GamePath;
}
}

View file

@ -0,0 +1,119 @@
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.Blocks;
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(bool async = false)
{
if (async)
{
ExitCurrentGameAsync().RunOn(Lean.EveryFrameStepRunner_RUNS_IN_TIME_STOPPED_AND_RUNNING);
}
else
{
entitiesDB.QueryEntity<GameSceneEntityStruct>(CommonExclusiveGroups.GameSceneEGID).WantsToQuit = true;
entitiesDB.PublishEntityChange<GameSceneEntityStruct>(CommonExclusiveGroups.GameSceneEGID);
}
}
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 void SaveCurrentGame()
{
ref GameSceneEntityStruct gses = ref entitiesDB.QueryEntity<GameSceneEntityStruct>(CommonExclusiveGroups.GameSceneEGID);
gses.LoadAfterSaving = false;
gses.SaveNow = 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);
}
public EGID[] GetAllBlocksInGame(BlockIDs filter = BlockIDs.Invalid)
{
var allBlocks = entitiesDB.QueryEntities<DBEntityStruct>();
List<EGID> blockEGIDs = new List<EGID>();
if (filter == BlockIDs.Invalid)
{
foreach (var (blocks, _) in allBlocks)
foreach (var block in blocks)
blockEGIDs.Add(block.ID);
return blockEGIDs.ToArray();
}
else
{
foreach (var (blocks, _) in allBlocks)
foreach (var block in blocks)
if (block.DBID == (ulong) filter)
blockEGIDs.Add(block.ID);
return blockEGIDs.ToArray();
}
}
}
}

View file

@ -0,0 +1,139 @@
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;
using Svelto.DataStructures;
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)
{
EntityCollection<MyGamesSlotEntityViewStruct> entities =
entitiesDB.QueryEntities<MyGamesSlotEntityViewStruct>(MyGamesScreenExclusiveGroups.GameSlotGuiEntities);
for (int i = 0; i < entities.count; i++)
{
if (entities[i].ID.entityID == id.entityID)
{
return ref entities[i];
}
}
MyGamesSlotEntityViewStruct[] defRef = new MyGamesSlotEntityViewStruct[1];
return ref defRef[0];
}
public ref T GetComponent<T>(EGID id) where T: unmanaged, 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

@ -0,0 +1,27 @@
using System;
using HarmonyLib;
using RobocraftX.Services;
namespace GamecraftModdingAPI.App
{
public class DualChoicePrompt : MultiChoiceError
{
public DualChoicePrompt(string errorMessage, string title, string firstButtonText, Action firstButtonAction, string secondButtonText, Action secondButtonAction) : base(errorMessage, firstButtonText, firstButtonAction, secondButtonText, secondButtonAction)
{
// internal readonly field smh
new Traverse(this).Field<string>("Title").Value = title;
}
}
public class SingleChoicePrompt : SingleChoiceError
{
public SingleChoicePrompt(string errorMessage, string buttonText, Action buttonClickAction) : base(errorMessage, buttonText, buttonClickAction)
{
}
public SingleChoicePrompt(string titleText, string errorMessage, string buttonText, Action buttonClickAction) : base(titleText, errorMessage, buttonText, buttonClickAction)
{
}
}
}

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Reflection; using System.Collections.Generic;
using System.Threading.Tasks; using System.Linq;
using System.Reflection.Emit;
using Svelto.ECS; using Svelto.ECS;
using Svelto.ECS.EntityStructs; using Svelto.ECS.EntityStructs;
@ -54,23 +55,13 @@ namespace GamecraftModdingAPI
float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0, float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0,
int uscale = 1, float3 scale = default, Player player = null) int uscale = 1, float3 scale = default, Player player = null)
{ {
if (PlacementEngine.IsInGame && GameState.IsBuildMode()) return PlaceNew<Block>(block, position, rotation, color, darkness, uscale, scale, player);
{
return new Block(PlacementEngine.PlaceBlock(block, color, darkness,
position, uscale, scale, player, rotation));
}
return null;
} }
/// <summary> /// <summary>
/// Place a new block at the given position. If scaled, position means the center of the block. The default block size is 0.2 in terms of position. /// Place a new block at the given position. If scaled, position means the center of the block. The default block size is 0.2 in terms of position.
/// Place blocks next to each other to connect them. /// Place blocks next to each other to connect them.
/// The placed block will be a complete block with a placement grid and collision which will be saved along with the game. /// The placed block will be a complete block with a placement grid and collision which will be saved along with the game.
/// <para></para>
/// <para>This method waits for the block to be constructed in the game which may take a significant amount of time.
/// Only use this to place a single block.
/// For placing multiple blocks, use PlaceNew() then AsyncUtils.WaitForSubmission() when done with placing blocks.</para>
/// </summary> /// </summary>
/// <param name="block">The block's type</param> /// <param name="block">The block's type</param>
/// <param name="color">The block's color</param> /// <param name="color">The block's color</param>
@ -81,23 +72,18 @@ namespace GamecraftModdingAPI
/// <param name="scale">The block's non-uniform scale - 0 means <paramref name="uscale"/> is used</param> /// <param name="scale">The block's non-uniform scale - 0 means <paramref name="uscale"/> is used</param>
/// <param name="player">The player who placed the block</param> /// <param name="player">The player who placed the block</param>
/// <returns>The placed block or null if failed</returns> /// <returns>The placed block or null if failed</returns>
public static async Task<Block> PlaceNewAsync(BlockIDs block, float3 position, public static T PlaceNew<T>(BlockIDs block, float3 position,
float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0, float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0,
int uscale = 1, float3 scale = default, Player player = null) int uscale = 1, float3 scale = default, Player player = null) where T : Block
{ {
if (PlacementEngine.IsInGame && GameState.IsBuildMode()) if (PlacementEngine.IsInGame && GameState.IsBuildMode())
{ {
try var egid = PlacementEngine.PlaceBlock(block, color, darkness,
{ position, uscale, scale, player, rotation, out var initializer);
var ret = new Block(PlacementEngine.PlaceBlock(block, color, darkness, var bl = New<T>(egid.entityID, egid.groupID);
position, uscale, scale, player, rotation)); bl.InitData.Group = BlockEngine.InitGroup(initializer);
await AsyncUtils.WaitForSubmission(); Placed += bl.OnPlacedInit;
return ret; return bl;
}
catch (Exception e)
{
Logging.MetaDebugLog(e);
}
} }
return null; return null;
@ -109,7 +95,7 @@ namespace GamecraftModdingAPI
/// <returns>The block object</returns> /// <returns>The block object</returns>
public static Block GetLastPlacedBlock() public static Block GetLastPlacedBlock()
{ {
return new Block(BlockIdentifiers.LatestBlockID); return New<Block>(BlockIdentifiers.LatestBlockID);
} }
/// <summary> /// <summary>
@ -130,27 +116,125 @@ namespace GamecraftModdingAPI
remove => BlockEventsEngine.Removed -= value; remove => BlockEventsEngine.Removed -= value;
} }
private static Dictionary<Type, Func<EGID, Block>> initializers = new Dictionary<Type, Func<EGID, Block>>();
private static Dictionary<Type, ExclusiveGroupStruct[]> typeToGroup =
new Dictionary<Type, ExclusiveGroupStruct[]>
{
{typeof(ConsoleBlock), new[] {CommonExclusiveGroups.BUILD_CONSOLE_BLOCK_GROUP}},
{typeof(LogicGate), new [] {CommonExclusiveGroups.BUILD_LOGIC_BLOCK_GROUP}},
{typeof(Motor), new[] {CommonExclusiveGroups.BUILD_MOTOR_BLOCK_GROUP}},
{typeof(MusicBlock), new[] {CommonExclusiveGroups.BUILD_MUSIC_BLOCK_GROUP}},
{typeof(Piston), new[] {CommonExclusiveGroups.BUILD_PISTON_BLOCK_GROUP}},
{typeof(Servo), new[] {CommonExclusiveGroups.BUILD_SERVO_BLOCK_GROUP}},
{
typeof(SpawnPoint),
new[]
{
CommonExclusiveGroups.BUILD_SPAWNPOINT_BLOCK_GROUP,
CommonExclusiveGroups.BUILD_BUILDINGSPAWN_BLOCK_GROUP
}
},
{typeof(TextBlock), new[] {CommonExclusiveGroups.BUILD_TEXT_BLOCK_GROUP}},
{typeof(Timer), new[] {CommonExclusiveGroups.BUILD_TIMER_BLOCK_GROUP}}
};
/// <summary>
/// Constructs a new instance of T with the given ID and group using dynamically created delegates.
/// It's equivalent to new T(EGID) with a minimal overhead thanks to caching the created delegates.
/// </summary>
/// <param name="id">The block ID</param>
/// <param name="group">The block group</param>
/// <typeparam name="T">The block's type or Block itself</typeparam>
/// <returns>An instance of the provided type</returns>
/// <exception cref="BlockTypeException">The block group doesn't match or cannot be found</exception>
/// <exception cref="MissingMethodException">The block class doesn't have the needed constructor</exception>
private static T New<T>(uint id, ExclusiveGroupStruct? group = null) where T : Block
{
var type = typeof(T);
EGID egid;
if (!group.HasValue)
{
if (typeToGroup.TryGetValue(type, out var gr) && gr.Length == 1)
egid = new EGID(id, gr[0]);
else
egid = BlockEngine.FindBlockEGID(id) ?? throw new BlockTypeException("Could not find block group!");
}
else
{
egid = new EGID(id, group.Value);
if (typeToGroup.TryGetValue(type, out var gr)
&& gr.All(egs => egs != group.Value)) //If this subclass has a specific group, then use that - so Block should still work
throw new BlockTypeException($"Incompatible block type! Type {type.Name} belongs to group {gr.Select(g => g.ToString()).Aggregate((a, b) => a + ", " + b)} instead of {group.Value}");
}
if (initializers.TryGetValue(type, out var func))
{
var bl = (T) func(egid);
return bl;
}
//https://stackoverflow.com/a/10593806/2703239
var ctor = type.GetConstructor(new[] {typeof(EGID)});
if (ctor == null)
throw new MissingMethodException("There is no constructor with an EGID parameter for this object");
DynamicMethod dynamic = new DynamicMethod(string.Empty,
type,
new[] {typeof(EGID)},
type);
ILGenerator il = dynamic.GetILGenerator();
il.DeclareLocal(type);
il.Emit(OpCodes.Ldarg_0); //Load EGID and pass to constructor
il.Emit(OpCodes.Newobj, ctor); //Call constructor
//il.Emit(OpCodes.Stloc_0); - doesn't seem like we need these
//il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ret);
func = (Func<EGID, T>) dynamic.CreateDelegate(typeof(Func<EGID, T>));
initializers.Add(type, func);
var block = (T) func(egid);
return block;
}
public Block(EGID id) public Block(EGID id)
{ {
Id = id; Id = id;
var type = GetType();
if (typeToGroup.TryGetValue(type, out var groups))
{
if (groups.All(gr => gr != id.groupID))
throw new BlockTypeException("The block has the wrong group! The type is " + GetType() +
" while the group is " + id.groupID);
}
else if (type != typeof(Block))
Logging.LogWarning($"Unknown block type! Add {type} to the dictionary.");
} }
public Block(uint id) : this(new EGID(id, CommonExclusiveGroups.OWNED_BLOCKS_GROUP)) /// <summary>
/// This overload searches for the correct group the block is in.
/// It will throw an exception if the block doesn't exist.
/// Use the EGID constructor where possible or subclasses of Block as those specify the group.
/// </summary>
public Block(uint id)
{ {
Id = BlockEngine.FindBlockEGID(id) ?? throw new BlockTypeException("Could not find the appropriate group for the block. The block probably doesn't exist or hasn't been submitted.");
} }
public EGID Id { get; } public EGID Id { get; }
internal BlockEngine.BlockInitData InitData;
/// <summary> /// <summary>
/// The block's current position or zero if the block no longer exists. /// The block's current position or zero if the block no longer exists.
/// A block is 0.2 wide by default in terms of position. /// A block is 0.2 wide by default in terms of position.
/// </summary> /// </summary>
public float3 Position public float3 Position
{ {
get => Exists ? MovementEngine.GetPosition(Id.entityID) : float3.zero; get => MovementEngine.GetPosition(Id, InitData);
set set
{ {
if (Exists) MovementEngine.MoveBlock(Id.entityID, value); MovementEngine.MoveBlock(Id, InitData, value);
} }
} }
@ -159,10 +243,10 @@ namespace GamecraftModdingAPI
/// </summary> /// </summary>
public float3 Rotation public float3 Rotation
{ {
get => Exists ? RotationEngine.GetRotation(Id.entityID) : float3.zero; get => RotationEngine.GetRotation(Id, InitData);
set set
{ {
if (Exists) RotationEngine.RotateBlock(Id.entityID, value); RotationEngine.RotateBlock(Id, InitData, value);
} }
} }
@ -172,12 +256,11 @@ namespace GamecraftModdingAPI
/// </summary> /// </summary>
public float3 Scale public float3 Scale
{ {
get => BlockEngine.GetBlockInfo<ScalingEntityStruct>(Id).scale; get => BlockEngine.GetBlockInfo(this, (ScalingEntityStruct st) => st.scale);
set set
{ {
BlockEngine.SetBlockInfo(this, (ref ScalingEntityStruct st, float3 val) => st.scale = val, value);
if (!Exists) return; //UpdateCollision needs the block to exist if (!Exists) return; //UpdateCollision needs the block to exist
ref var scaling = ref BlockEngine.GetBlockInfo<ScalingEntityStruct>(Id);
scaling.scale = value;
ScalingEngine.UpdateCollision(Id); ScalingEngine.UpdateCollision(Id);
} }
} }
@ -188,11 +271,11 @@ namespace GamecraftModdingAPI
/// </summary> /// </summary>
public int UniformScale public int UniformScale
{ {
get => BlockEngine.GetBlockInfo<UniformBlockScaleEntityStruct>(Id).scaleFactor; get => BlockEngine.GetBlockInfo(this, (UniformBlockScaleEntityStruct st) => st.scaleFactor);
set set
{ {
ref var scaleStruct = ref BlockEngine.GetBlockInfo<UniformBlockScaleEntityStruct>(Id); BlockEngine.SetBlockInfo(this, (ref UniformBlockScaleEntityStruct st, int val) => st.scaleFactor = val,
scaleStruct.scaleFactor = value; value);
Scale = new float3(value, value, value); Scale = new float3(value, value, value);
} }
} }
@ -204,8 +287,7 @@ namespace GamecraftModdingAPI
{ {
get get
{ {
var id = (BlockIDs) BlockEngine.GetBlockInfo<DBEntityStruct>(Id, out var exists).DBID; return BlockEngine.GetBlockInfo(this, (DBEntityStruct st) => (BlockIDs) st.DBID, BlockIDs.Invalid);
return exists ? id : BlockIDs.Invalid;
} }
} }
@ -216,17 +298,19 @@ namespace GamecraftModdingAPI
{ {
get get
{ {
byte index = BlockEngine.GetBlockInfo<ColourParameterEntityStruct>(Id, out var exists).indexInPalette; byte index = BlockEngine.GetBlockInfo(this, (ColourParameterEntityStruct st) => st.indexInPalette,
if (!exists) index = byte.MaxValue; byte.MaxValue);
return new BlockColor(index); return new BlockColor(index);
} }
set set
{ {
ref var color = ref BlockEngine.GetBlockInfo<ColourParameterEntityStruct>(Id); BlockEngine.SetBlockInfo(this, (ref ColourParameterEntityStruct color, BlockColor val) =>
color.indexInPalette = (byte)(value.Color + value.Darkness * 10); {
color.indexInPalette = (byte) (val.Color + val.Darkness * 10);
color.overridePaletteColour = false; color.overridePaletteColour = false;
color.needsUpdate = true; color.needsUpdate = true;
BlockEngine.SetBlockColorFromPalette(ref color); BlockEngine.SetBlockColorFromPalette(ref color);
}, value);
} }
} }
@ -235,32 +319,37 @@ namespace GamecraftModdingAPI
/// </summary> /// </summary>
public float4 CustomColor public float4 CustomColor
{ {
get => BlockEngine.GetBlockInfo<ColourParameterEntityStruct>(Id).overriddenColour; get => BlockEngine.GetBlockInfo(this, (ColourParameterEntityStruct st) => st.overriddenColour);
set set
{ {
ref var color = ref BlockEngine.GetBlockInfo<ColourParameterEntityStruct>(Id); BlockEngine.SetBlockInfo(this, (ref ColourParameterEntityStruct color, float4 val) =>
color.overriddenColour = value; {
color.overriddenColour = val;
color.overridePaletteColour = true; color.overridePaletteColour = true;
color.needsUpdate = true; color.needsUpdate = true;
}, value);
} }
} }
/// <summary> /// <summary>
/// The short text displayed on the block if applicable, or null. /// The text displayed on the block if applicable, or null.
/// Setting it is temporary to the session, it won't be saved. /// Setting it is temporary to the session, it won't be saved.
/// </summary> /// </summary>
public string Label public string Label
{ {
get => BlockEngine.GetBlockInfo<TextLabelEntityViewStruct>(Id).textLabelComponent?.text; get => BlockEngine.GetBlockInfoViewStruct(this, (TextLabelEntityViewStruct st) => st.textLabelComponent?.text);
set set
{ {
ref var text = ref BlockEngine.GetBlockInfo<TextLabelEntityViewStruct>(Id); BlockEngine.SetBlockInfoViewStruct(this, (ref TextLabelEntityViewStruct text, string val) =>
if (text.textLabelComponent != null) text.textLabelComponent.text = value; {
if (text.textLabelComponent != null) text.textLabelComponent.text = val;
}, value);
} }
} }
/// <summary> /// <summary>
/// Whether the block exists. The other properties will return a default value if the block doesn't exist. /// Whether the block exists. The other properties will return a default value if the block doesn't exist.
/// If the block was just placed, then this will also return false but the properties will work correctly.
/// </summary> /// </summary>
public bool Exists => BlockEngine.BlockExists(Id); public bool Exists => BlockEngine.BlockExists(Id);
@ -276,14 +365,21 @@ namespace GamecraftModdingAPI
public bool Remove() => RemovalEngine.RemoveBlock(Id); public bool Remove() => RemovalEngine.RemoveBlock(Id);
/// <summary> /// <summary>
/// Returns the rigid body of the cluster of blocks this one belongs to during simulation. /// Returns the rigid body of the chunk of blocks this one belongs to during simulation.
/// Can be used to apply forces or move the block around while the simulation is running. /// Can be used to apply forces or move the block around while the simulation is running.
/// </summary> /// </summary>
/// <returns>The SimBody of the cluster or null if the block doesn't exist.</returns> /// <returns>The SimBody of the chunk or null if the block doesn't exist.</returns>
public SimBody GetSimBody() public SimBody GetSimBody()
{ {
uint id = BlockEngine.GetBlockInfo<GridConnectionsEntityStruct>(Id, out var exists).machineRigidBodyId; return BlockEngine.GetBlockInfo(this,
return exists ? new SimBody(id) : null; (GridConnectionsEntityStruct st) => new SimBody(st.machineRigidBodyId, st.clusterId));
}
private void OnPlacedInit(object sender, BlockPlacedRemovedEventArgs e)
{ //Member method instead of lambda to avoid constantly creating delegates
if (e.ID != Id) return;
Placed -= OnPlacedInit; //And we can reference it
InitData = default; //Remove initializer as it's no longer valid - if the block gets removed it shouldn't be used again
} }
public override string ToString() public override string ToString()
@ -325,6 +421,8 @@ namespace GamecraftModdingAPI
GameEngineManager.AddGameEngine(BlockEngine); GameEngineManager.AddGameEngine(BlockEngine);
GameEngineManager.AddGameEngine(BlockEventsEngine); GameEngineManager.AddGameEngine(BlockEventsEngine);
GameEngineManager.AddGameEngine(ScalingEngine); GameEngineManager.AddGameEngine(ScalingEngine);
GameEngineManager.AddGameEngine(SignalEngine);
Wire.signalEngine = SignalEngine; // requires same functionality, no need to duplicate the engine
} }
/// <summary> /// <summary>
@ -338,12 +436,11 @@ namespace GamecraftModdingAPI
// C# can't cast to a child of Block unless the object was originally that child type // C# can't cast to a child of Block unless the object was originally that child type
// And C# doesn't let me make implicit cast operators for child types // And C# doesn't let me make implicit cast operators for child types
// So thanks to Microsoft, we've got this horrible implementation using reflection // So thanks to Microsoft, we've got this horrible implementation using reflection
ConstructorInfo ctor = typeof(T).GetConstructor(types: new System.Type[] { typeof(EGID) });
if (ctor == null) //Lets improve that using delegates
{ var block = New<T>(Id.entityID, Id.groupID);
throw new BlockSpecializationException("Specialized block constructor does not accept an EGID"); block.InitData = this.InitData;
} return block;
return (T)ctor.Invoke(new object[] { Id });
} }
#if DEBUG #if DEBUG

View file

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Gamecraft.Wires; using Gamecraft.Wires;
using RobocraftX.Blocks; using RobocraftX.Blocks;
@ -8,16 +10,16 @@ using RobocraftX.Physics;
using RobocraftX.Scene.Simulation; using RobocraftX.Scene.Simulation;
using Svelto.DataStructures; using Svelto.DataStructures;
using Svelto.ECS; using Svelto.ECS;
using Svelto.ECS.Hybrid;
using GamecraftModdingAPI.Engines; using GamecraftModdingAPI.Engines;
using GamecraftModdingAPI.Utility;
namespace GamecraftModdingAPI.Blocks namespace GamecraftModdingAPI.Blocks
{ {
/// <summary> /// <summary>
/// Engine for executing general block actions /// Engine for executing general block actions
/// </summary> /// </summary>
public class BlockEngine : IApiEngine public partial class BlockEngine : IApiEngine
{ {
public string Name { get; } = "GamecraftModdingAPIBlockGameEngine"; public string Name { get; } = "GamecraftModdingAPIBlockGameEngine";
@ -25,8 +27,6 @@ namespace GamecraftModdingAPI.Blocks
public bool isRemovable => false; public bool isRemovable => false;
internal bool Synced = true;
public void Dispose() public void Dispose()
{ {
} }
@ -38,14 +38,15 @@ namespace GamecraftModdingAPI.Blocks
public Block[] GetConnectedBlocks(EGID blockID) public Block[] GetConnectedBlocks(EGID blockID)
{ {
if (!BlockExists(blockID)) return new Block[0]; if (!BlockExists(blockID)) return new Block[0];
Stack<uint> cubeStack = new Stack<uint>(); Stack<EGID> cubeStack = new Stack<EGID>();
FasterList<uint> cubes = new FasterList<uint>(10); FasterList<EGID> cubes = new FasterList<EGID>(10);
var coll = entitiesDB.QueryEntities<GridConnectionsEntityStruct>(CommonExclusiveGroups var coll = entitiesDB.QueryEntities<GridConnectionsEntityStruct>();
.OWNED_BLOCKS_GROUP); foreach (var (ecoll, _) in coll)
for (int i = 0; i < coll.count; i++) foreach (ref var conn in ecoll)
coll[i].isProcessed = false; conn.isProcessed = false;
ConnectedCubesUtility.TreeTraversal.GetConnectedCubes(entitiesDB, blockID.entityID, cubeStack, cubes, (in GridConnectionsEntityStruct g) => { return false; }); ConnectedCubesUtility.TreeTraversal.GetConnectedCubes(entitiesDB, blockID, cubeStack, cubes,
(in GridConnectionsEntityStruct g) => { return false; });
var ret = new Block[cubes.count]; var ret = new Block[cubes.count];
for (int i = 0; i < cubes.count; i++) for (int i = 0; i < cubes.count; i++)
@ -60,75 +61,110 @@ namespace GamecraftModdingAPI.Blocks
color.paletteColour = paletteEntry.Colour; color.paletteColour = paletteEntry.Colour;
} }
/// <summary> public ref T GetBlockInfo<T>(EGID blockID) where T : unmanaged, IEntityComponent
/// Get a struct of a block. Can be used to set properties.
/// Returns a default value if not found.
/// </summary>
/// <param name="blockID">The block's ID</param>
/// <typeparam name="T">The struct to query</typeparam>
/// <returns>An editable reference to the struct</returns>
public ref T GetBlockInfo<T>(EGID blockID) where T : struct, IEntityComponent
{ {
if (!Synced)
{
Sync();
Synced = true;
}
if (entitiesDB.Exists<T>(blockID)) if (entitiesDB.Exists<T>(blockID))
return ref entitiesDB.QueryEntity<T>(blockID); return ref entitiesDB.QueryEntity<T>(blockID);
T[] structHolder = new T[1]; //Create something that can be referenced T[] structHolder = new T[1]; //Create something that can be referenced
return ref structHolder[0]; //Gets a default value automatically return ref structHolder[0]; //Gets a default value automatically
} }
/// <summary> public ref T GetBlockInfoViewStruct<T>(EGID blockID) where T : struct, INeedEGID, IEntityComponent
/// Get a struct of a block. Can be used to set properties.
/// Returns a default value if not found.
/// </summary>
/// <param name="blockID">The block's ID</param>
/// <param name="exists">Whether the specified struct exists for the block</param>
/// <typeparam name="T">The struct to query</typeparam>
/// <returns>An editable reference to the struct</returns>
public ref T GetBlockInfo<T>(EGID blockID, out bool exists) where T : struct, IEntityComponent
{ {
if (!Synced) if (entitiesDB.Exists<T>(blockID))
{ {
Sync(); // TODO: optimize by using EntitiesDB internal calls instead of iterating over everything
Synced = true; EntityCollection<T> entities = entitiesDB.QueryEntities<T>(blockID.groupID);
for (int i = 0; i < entities.count; i++)
{
if (entities[i].ID == blockID)
{
return ref entities[i];
} }
exists = entitiesDB.Exists<T>(blockID); }
if (exists) }
return ref entitiesDB.QueryEntity<T>(blockID); T[] structHolder = new T[1]; //Create something that can be referenced
T[] structHolder = new T[1]; return ref structHolder[0]; //Gets a default value automatically
return ref structHolder[0];
} }
public bool BlockExists(EGID id) public U GetBlockInfo<T, U>(Block block, Func<T, U> getter,
U def = default) where T : unmanaged, IEntityComponent
{ {
if (!Synced) if (entitiesDB.Exists<T>(block.Id))
{ return getter(entitiesDB.QueryEntity<T>(block.Id));
Sync(); return GetBlockInitInfo(block, getter, def);
Synced = true;
}
return entitiesDB.Exists<DBEntityStruct>(id);
} }
public bool GetBlockInfoExists<T>(EGID blockID) where T : struct, IEntityComponent public U GetBlockInfoViewStruct<T, U>(Block block, Func<T, U> getter,
U def = default) where T : struct, IEntityViewComponent
{ {
if (!Synced) if (entitiesDB.Exists<T>(block.Id))
{ return getter(entitiesDB.QueryEntity<T>(block.Id));
Sync(); return GetBlockInitInfo(block, getter, def);
Synced = true;
} }
return entitiesDB.Exists<T>(blockID);
private U GetBlockInitInfo<T, U>(Block block, Func<T, U> getter, U def) where T : struct, IEntityComponent
{
if (block.InitData.Group == null) return def;
var initializer = new EntityComponentInitializer(block.Id, block.InitData.Group);
if (initializer.Has<T>())
return getter(initializer.Get<T>());
return def;
}
public delegate void Setter<T, U>(ref T component, U value) where T : struct, IEntityComponent;
public void SetBlockInfoViewStruct<T, U>(Block block, Setter<T, U> setter, U value) where T : struct, IEntityViewComponent
{
if (entitiesDB.Exists<T>(block.Id))
setter(ref entitiesDB.QueryEntity<T>(block.Id), value);
else
SetBlockInitInfo(block, setter, value);
}
public void SetBlockInfo<T, U>(Block block, Setter<T, U> setter, U value) where T : unmanaged, IEntityComponent
{
if (entitiesDB.Exists<T>(block.Id))
setter(ref entitiesDB.QueryEntity<T>(block.Id), value);
else
SetBlockInitInfo(block, setter, value);
}
private void SetBlockInitInfo<T, U>(Block block, Setter<T, U> setter, U value)
where T : struct, IEntityComponent
{
if (block.InitData.Group != null)
{
var initializer = new EntityComponentInitializer(block.Id, block.InitData.Group);
T component = initializer.Has<T>() ? initializer.Get<T>() : default;
ref T structRef = ref component;
setter(ref structRef, value);
initializer.Init(structRef);
}
}
public bool BlockExists(EGID blockID)
{
return entitiesDB.Exists<DBEntityStruct>(blockID);
}
public bool GetBlockInfoExists<T>(Block block) where T : struct, IEntityComponent
{
if (entitiesDB.Exists<T>(block.Id))
return true;
if (block.InitData.Group == null)
return false;
var init = new EntityComponentInitializer(block.Id, block.InitData.Group);
return init.Has<T>();
} }
public SimBody[] GetSimBodiesFromID(byte id) public SimBody[] GetSimBodiesFromID(byte id)
{ {
var ret = new FasterList<SimBody>(4); var ret = new FasterList<SimBody>(4);
if (!entitiesDB.HasAny<ObjectIdEntityStruct>(CommonExclusiveGroups.OWNED_BLOCKS_GROUP)) if (!entitiesDB.HasAny<ObjectIdEntityStruct>(CommonExclusiveGroups.BUILD_OBJID_BLOCK_GROUP))
return new SimBody[0]; return new SimBody[0];
var oids = entitiesDB.QueryEntities<ObjectIdEntityStruct>(CommonExclusiveGroups.OWNED_BLOCKS_GROUP); var oids = entitiesDB.QueryEntities<ObjectIdEntityStruct>(CommonExclusiveGroups.BUILD_OBJID_BLOCK_GROUP);
var connections = entitiesDB.QueryMappedEntities<GridConnectionsEntityStruct>(CommonExclusiveGroups.OWNED_BLOCKS_GROUP); var connections = entitiesDB.QueryMappedEntities<GridConnectionsEntityStruct>(CommonExclusiveGroups.BUILD_OBJID_BLOCK_GROUP);
foreach (ref ObjectIdEntityStruct oid in oids) foreach (ref ObjectIdEntityStruct oid in oids)
{ {
if (oid.objectId != id) continue; if (oid.objectId != id) continue;
@ -147,9 +183,9 @@ namespace GamecraftModdingAPI.Blocks
public ObjectIdentifier[] GetObjectIDsFromID(byte id, bool sim) public ObjectIdentifier[] GetObjectIDsFromID(byte id, bool sim)
{ {
var ret = new FasterList<ObjectIdentifier>(4); var ret = new FasterList<ObjectIdentifier>(4);
if (!entitiesDB.HasAny<ObjectIdEntityStruct>(CommonExclusiveGroups.OWNED_BLOCKS_GROUP)) if (!entitiesDB.HasAny<ObjectIdEntityStruct>(CommonExclusiveGroups.BUILD_OBJID_BLOCK_GROUP))
return new ObjectIdentifier[0]; return new ObjectIdentifier[0];
var oids = entitiesDB.QueryEntities<ObjectIdEntityStruct>(CommonExclusiveGroups.OWNED_BLOCKS_GROUP); var oids = entitiesDB.QueryEntities<ObjectIdEntityStruct>(CommonExclusiveGroups.BUILD_OBJID_BLOCK_GROUP);
foreach (ref ObjectIdEntityStruct oid in oids) foreach (ref ObjectIdEntityStruct oid in oids)
if (sim ? oid.simObjectId == id : oid.objectId == id) if (sim ? oid.simObjectId == id : oid.objectId == id)
ret.Add(new ObjectIdentifier(oid.ID)); ret.Add(new ObjectIdentifier(oid.ID));
@ -170,15 +206,47 @@ namespace GamecraftModdingAPI.Blocks
return list.ToArray(); return list.ToArray();
} }
/// <summary> public SimBody[] GetClusterBodies(uint cid)
/// Synchronize newly created entity components with entities DB.
/// This forces a partial game tick, so it may be slow.
/// This also has the potential to make Gamecraft unstable.
/// Use this sparingly.
/// </summary>
private static void Sync()
{ {
DeterministicStepCompositionRootPatch.SubmitEntitiesNow(); var groups = entitiesDB.QueryEntities<GridConnectionsEntityStruct>();
var bodies = new HashSet<uint>();
foreach (var (coll, _) in groups)
{
foreach (var conn in coll)
{
if (conn.clusterId == cid)
bodies.Add(conn.machineRigidBodyId);
}
}
return bodies.Select(id => new SimBody(id)).ToArray();
}
public EGID? FindBlockEGID(uint id)
{
var groups = entitiesDB.FindGroups<DBEntityStruct>();
foreach (ExclusiveGroupStruct group in groups)
{
if (entitiesDB.Exists<DBEntityStruct>(id, group))
return new EGID(id, group);
}
return null;
}
public Cluster GetCluster(uint sbid)
{
var groups = entitiesDB.QueryEntities<GridConnectionsEntityStruct>();
foreach (var (coll, _) in groups)
{
foreach (var conn in coll)
{
if (conn.machineRigidBodyId == sbid)
return new Cluster(conn.clusterId);
}
}
return null;
} }
#if DEBUG #if DEBUG

View file

@ -0,0 +1,52 @@
using System;
using System.Linq.Expressions;
using Svelto.DataStructures;
using Svelto.ECS;
using Svelto.ECS.Internal;
namespace GamecraftModdingAPI.Blocks
{
public partial class BlockEngine
{
/// <summary>
/// Holds information needed to construct a component initializer
/// </summary>
internal struct BlockInitData
{
public FasterDictionary<RefWrapper<Type>, ITypeSafeDictionary> Group;
}
internal delegate FasterDictionary<RefWrapper<Type>, ITypeSafeDictionary> GetInitGroup(
EntityComponentInitializer initializer);
/// <summary>
/// Accesses the group field of the initializer
/// </summary>
internal GetInitGroup InitGroup = CreateAccessor<GetInitGroup>("_group");
//https://stackoverflow.com/questions/55878525/unit-testing-ref-structs-with-private-fields-via-reflection
internal static TDelegate CreateAccessor<TDelegate>(string memberName) where TDelegate : Delegate
{
var invokeMethod = typeof(TDelegate).GetMethod("Invoke");
if (invokeMethod == null)
throw new InvalidOperationException($"{typeof(TDelegate)} signature could not be determined.");
var delegateParameters = invokeMethod.GetParameters();
if (delegateParameters.Length != 1)
throw new InvalidOperationException("Delegate must have a single parameter.");
var paramType = delegateParameters[0].ParameterType;
var objParam = Expression.Parameter(paramType, "obj");
var memberExpr = Expression.PropertyOrField(objParam, memberName);
Expression returnExpr = memberExpr;
if (invokeMethod.ReturnType != memberExpr.Type)
returnExpr = Expression.ConvertChecked(memberExpr, invokeMethod.ReturnType);
var lambda =
Expression.Lambda<TDelegate>(returnExpr, $"Access{paramType.Name}_{memberName}", new[] {objParam});
return lambda.Compile();
}
}
}

View file

@ -1,9 +1,11 @@
using System; using System;
using GamecraftModdingAPI.Engines;
using GamecraftModdingAPI.Utility;
using RobocraftX.Common; using RobocraftX.Common;
using Svelto.ECS; using Svelto.ECS;
using GamecraftModdingAPI.Engines;
using GamecraftModdingAPI.Utility;
namespace GamecraftModdingAPI.Blocks namespace GamecraftModdingAPI.Blocks
{ {
public class BlockEventsEngine : IReactionaryEngine<DBEntityStruct> public class BlockEventsEngine : IReactionaryEngine<DBEntityStruct>
@ -16,6 +18,7 @@ namespace GamecraftModdingAPI.Blocks
} }
public EntitiesDB entitiesDB { get; set; } public EntitiesDB entitiesDB { get; set; }
public void Dispose() public void Dispose()
{ {
} }
@ -23,14 +26,21 @@ namespace GamecraftModdingAPI.Blocks
public string Name { get; } = "GamecraftModdingAPIBlockEventsEngine"; public string Name { get; } = "GamecraftModdingAPIBlockEventsEngine";
public bool isRemovable { get; } = false; public bool isRemovable { get; } = false;
private bool shouldAddRemove;
public void Add(ref DBEntityStruct entityComponent, EGID egid) public void Add(ref DBEntityStruct entityComponent, EGID egid)
{ {
ExceptionUtil.InvokeEvent(Placed, this, new BlockPlacedRemovedEventArgs {ID = egid, Type = (BlockIDs) entityComponent.DBID}); if (!(shouldAddRemove = !shouldAddRemove))
return;
ExceptionUtil.InvokeEvent(Placed, this,
new BlockPlacedRemovedEventArgs {ID = egid, Type = (BlockIDs) entityComponent.DBID});
} }
public void Remove(ref DBEntityStruct entityComponent, EGID egid) public void Remove(ref DBEntityStruct entityComponent, EGID egid)
{ {
ExceptionUtil.InvokeEvent(Removed, this, new BlockPlacedRemovedEventArgs {ID = egid, Type = (BlockIDs) entityComponent.DBID}); if (!(shouldAddRemove = !shouldAddRemove))
return;
ExceptionUtil.InvokeEvent(Removed, this,
new BlockPlacedRemovedEventArgs {ID = egid, Type = (BlockIDs) entityComponent.DBID});
} }
} }
@ -38,5 +48,8 @@ namespace GamecraftModdingAPI.Blocks
{ {
public EGID ID; public EGID ID;
public BlockIDs Type; public BlockIDs Type;
private Block block;
public Block Block => block ?? (block = new Block(ID));
} }
} }

View file

@ -40,4 +40,26 @@ namespace GamecraftModdingAPI.Blocks
{ {
} }
} }
public class WiringException : BlockException
{
public WiringException()
{
}
public WiringException(string message) : base(message)
{
}
}
public class WireInvalidException : WiringException
{
public WireInvalidException()
{
}
public WireInvalidException(string message) : base(message)
{
}
}
} }

View file

@ -6,7 +6,7 @@ namespace GamecraftModdingAPI.Blocks
public enum BlockIDs : ushort public enum BlockIDs : ushort
{ {
/// <summary> /// <summary>
/// A custom value for the API. Doesn't exist for Gamecraft. /// Called "nothing" in Gamecraft. (DBID.NOTHING)
/// </summary> /// </summary>
Invalid = ushort.MaxValue, Invalid = ushort.MaxValue,
AluminiumCube = 0, AluminiumCube = 0,
@ -192,6 +192,9 @@ namespace GamecraftModdingAPI.Blocks
PlayerFilter, PlayerFilter,
TeamFilter, TeamFilter,
Number2Text, //193 Number2Text, //193
DestructionManager = 260,
ChunkHealthModifier,
ClusterHealthModifier, //262
BeachTree1 = 200, BeachTree1 = 200,
BeachTree2, BeachTree2,
BeachTree3, BeachTree3,
@ -243,6 +246,8 @@ namespace GamecraftModdingAPI.Blocks
AdvancedRotator, AdvancedRotator,
MusicBlock, //256 MusicBlock, //256
PlasmaCannonBlock, PlasmaCannonBlock,
QuantumRiflePickup = 300,
QuantumRifleAmmoPickup,
MagmaRockCube=777, MagmaRockCube=777,
MagmaRockCubeSliced, MagmaRockCubeSliced,
MagmaRockSlope, MagmaRockSlope,

View file

@ -12,8 +12,8 @@ namespace GamecraftModdingAPI.Blocks
{ {
/// <summary> /// <summary>
/// Blocks placed by the player /// Blocks placed by the player
/// </summary> /// </summary> - TODO
public static ExclusiveGroup OWNED_BLOCKS { get { return CommonExclusiveGroups.OWNED_BLOCKS_GROUP; } } //public static ExclusiveGroup OWNED_BLOCKS { get { return CommonExclusiveGroups.REAL_BLOCKS_GROUPS_DON_T_USE_IN_NEW_CODE; } }
/// <summary> /// <summary>
/// Extra parts used in functional blocks /// Extra parts used in functional blocks
@ -23,7 +23,7 @@ namespace GamecraftModdingAPI.Blocks
/// <summary> /// <summary>
/// Blocks which are disabled in Simulation mode /// Blocks which are disabled in Simulation mode
/// </summary> /// </summary>
public static ExclusiveGroup SIM_BLOCKS_DISABLED { get { return CommonExclusiveGroups.BLOCKS_DISABLED_IN_SIM_GROUP; } } public static ExclusiveGroup SIM_BLOCKS_DISABLED { get { return CommonExclusiveGroups.DISABLED_JOINTS_IN_SIM_GROUP; } }
//public static ExclusiveGroup SPAWN_POINTS { get { return CommonExclusiveGroups.SPAWN_POINTS_GROUP; } } //public static ExclusiveGroup SPAWN_POINTS { get { return CommonExclusiveGroups.SPAWN_POINTS_GROUP; } }
@ -34,7 +34,7 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public static uint LatestBlockID { public static uint LatestBlockID {
get get
{ { //Need the private field as the property increments itself
return ((uint) AccessTools.Field(typeof(CommonExclusiveGroups), "_nextBlockEntityID").GetValue(null)) - 1; return ((uint) AccessTools.Field(typeof(CommonExclusiveGroups), "_nextBlockEntityID").GetValue(null)) - 1;
} }
} }

View file

@ -0,0 +1,124 @@
using System;
using Gamecraft.Wires;
using GamecraftModdingAPI;
using GamecraftModdingAPI.Tests;
using GamecraftModdingAPI.Utility;
namespace GamecraftModdingAPI.Blocks
{
#if TEST
/// <summary>
/// Block test cases. Not accessible in release versions.
/// </summary>
[APITestClass]
public static class BlockTests
{
[APITestCase(TestType.EditMode)]
public static void TestPlaceNew()
{
Block newBlock = Block.PlaceNew(BlockIDs.AluminiumCube, Unity.Mathematics.float3.zero);
Assert.NotNull(newBlock.Id, "Newly placed block is missing Id. This should be populated when the block is placed.", "Newly placed block Id is not null, block successfully placed.");
}
[APITestCase(TestType.EditMode)]
public static void TestInitProperty()
{
Block newBlock = Block.PlaceNew(BlockIDs.AluminiumCube, Unity.Mathematics.float3.zero + 2);
if (!Assert.CloseTo(newBlock.Position, (Unity.Mathematics.float3.zero + 2), $"Newly placed block at {newBlock.Position} is expected at {Unity.Mathematics.float3.zero + 2}.", "Newly placed block position matches.")) return;
//Assert.Equal(newBlock.Exists, true, "Newly placed block does not exist, possibly because Sync() skipped/missed/failed.", "Newly placed block exists, Sync() successful.");
}
[APITestCase(TestType.EditMode)]
public static void TestTextBlock()
{
TextBlock textBlock = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler
Assert.Errorless(() => { textBlock = Block.PlaceNew<TextBlock>(BlockIDs.TextBlock, Unity.Mathematics.float3.zero + 1); }, "Block.PlaceNew<TextBlock>() raised an exception: ", "Block.PlaceNew<TextBlock>() completed without issue.");
if (!Assert.NotNull(textBlock, "Block.PlaceNew<TextBlock>() returned null, possibly because it failed silently.", "Specialized TextBlock is not null.")) return;
if (!Assert.NotNull(textBlock.Text, "TextBlock.Text is null, possibly because it failed silently.", "TextBlock.Text is not null.")) return;
if (!Assert.NotNull(textBlock.TextBlockId, "TextBlock.TextBlockId is null, possibly because it failed silently.", "TextBlock.TextBlockId is not null.")) return;
}
[APITestCase(TestType.EditMode)]
public static void TestMotor()
{
Block newBlock = Block.PlaceNew(BlockIDs.MotorS, Unity.Mathematics.float3.zero + 1);
Motor b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler
Assert.Errorless(() => { b = newBlock.Specialise<Motor>(); }, "Block.Specialize<Motor>() raised an exception: ", "Block.Specialize<Motor>() completed without issue.");
if (!Assert.NotNull(b, "Block.Specialize<Motor>() returned null, possibly because it failed silently.", "Specialized Motor is not null.")) return;
if (!Assert.CloseTo(b.Torque, 75f, $"Motor.Torque {b.Torque} does not equal default value, possibly because it failed silently.", "Motor.Torque close enough to default.")) return;
if (!Assert.CloseTo(b.TopSpeed, 30f, $"Motor.TopSpeed {b.TopSpeed} does not equal default value, possibly because it failed silently.", "Motor.Torque is close enough to default.")) return;
if (!Assert.Equal(b.Reverse, false, $"Motor.Reverse {b.Reverse} does not equal default value, possibly because it failed silently.", "Motor.Reverse is default.")) return;
}
[APITestCase(TestType.EditMode)]
public static void TestPiston()
{
Block newBlock = Block.PlaceNew(BlockIDs.PneumaticPiston, Unity.Mathematics.float3.zero + 1);
Piston b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler
Assert.Errorless(() => { b = newBlock.Specialise<Piston>(); }, "Block.Specialize<Piston>() raised an exception: ", "Block.Specialize<Piston>() completed without issue.");
if (!Assert.NotNull(b, "Block.Specialize<Piston>() returned null, possibly because it failed silently.", "Specialized Piston is not null.")) return;
if (!Assert.CloseTo(b.MaximumExtension, 1.01f, $"Piston.MaximumExtension {b.MaximumExtension} does not equal default value, possibly because it failed silently.", "Piston.MaximumExtension is close enough to default.")) return;
if (!Assert.CloseTo(b.MaximumForce, 750f, $"Piston.MaximumForce {b.MaximumForce} does not equal default value, possibly because it failed silently.", "Piston.MaximumForce is close enough to default.")) return;
}
[APITestCase(TestType.EditMode)]
public static void TestServo()
{
Block newBlock = Block.PlaceNew(BlockIDs.ServoAxle, Unity.Mathematics.float3.zero + 1);
Servo b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler
Assert.Errorless(() => { b = newBlock.Specialise<Servo>(); }, "Block.Specialize<Servo>() raised an exception: ", "Block.Specialize<Servo>() completed without issue.");
if (!Assert.NotNull(b, "Block.Specialize<Servo>() returned null, possibly because it failed silently.", "Specialized Servo is not null.")) return;
if (!Assert.CloseTo(b.MaximumAngle, 180f, $"Servo.MaximumAngle {b.MaximumAngle} does not equal default value, possibly because it failed silently.", "Servo.MaximumAngle is close enough to default.")) return;
if (!Assert.CloseTo(b.MinimumAngle, -180f, $"Servo.MinimumAngle {b.MinimumAngle} does not equal default value, possibly because it failed silently.", "Servo.MinimumAngle is close enough to default.")) return;
if (!Assert.CloseTo(b.MaximumForce, 750f, $"Servo.MaximumForce {b.MaximumForce} does not equal default value, possibly because it failed silently.", "Servo.MaximumForce is close enough to default.")) return;
}
[APITestCase(TestType.Game)]
public static void TestMusicBlock1()
{
Block newBlock = Block.PlaceNew(BlockIDs.MusicBlock, Unity.Mathematics.float3.zero + 2);
MusicBlock b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler
Assert.Errorless(() => { b = newBlock.Specialise<MusicBlock>(); }, "Block.Specialize<MusicBlock>() raised an exception: ", "Block.Specialize<MusicBlock>() completed without issue.");
if (!Assert.NotNull(b, "Block.Specialize<MusicBlock>() returned null, possibly because it failed silently.", "Specialized MusicBlock is not null.")) return;
if (!Assert.CloseTo(b.Volume, 100f, $"MusicBlock.Volume {b.Volume} does not equal default value, possibly because it failed silently.", "MusicBlock.Volume is close enough to default.")) return;
if (!Assert.Equal(b.TrackIndex, 0, $"MusicBlock.TrackIndex {b.TrackIndex} does not equal default value, possibly because it failed silently.", "MusicBlock.TrackIndex is equal to default.")) return;
_musicBlock = b;
}
private static MusicBlock _musicBlock;
[APITestCase(TestType.EditMode)]
public static void TestMusicBlock2()
{
//Block newBlock = Block.GetLastPlacedBlock();
var b = _musicBlock;
if (!Assert.NotNull(b, "Block.Specialize<MusicBlock>() returned null, possibly because it failed silently.", "Specialized MusicBlock is not null.")) return;
b.IsPlaying = true; // play sfx
if (!Assert.Equal(b.IsPlaying, true, $"MusicBlock.IsPlaying {b.IsPlaying} does not equal true, possibly because it failed silently.", "MusicBlock.IsPlaying is set properly.")) return;
if (!Assert.Equal(b.ChannelType, ChannelType.None, $"MusicBlock.ChannelType {b.ChannelType} does not equal default value, possibly because it failed silently.", "MusicBlock.ChannelType is equal to default.")) return;
//Assert.Log(b.Track.ToString());
if (!Assert.Equal(b.Track.ToString(), new Guid("3237ff8f-f5f2-4f84-8144-496ca280f8c0").ToString(), $"MusicBlock.Track {b.Track} does not equal default value, possibly because it failed silently.", "MusicBlock.Track is equal to default.")) return;
}
[APITestCase(TestType.EditMode)]
public static void TestLogicGate()
{
Block newBlock = Block.PlaceNew(BlockIDs.NOTLogicBlock, Unity.Mathematics.float3.zero + 1);
LogicGate b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler
Assert.Errorless(() => { b = newBlock.Specialise<LogicGate>(); }, "Block.Specialize<LogicGate>() raised an exception: ", "Block.Specialize<LogicGate>() completed without issue.");
if (!Assert.NotNull(b, "Block.Specialize<LogicGate>() returned null, possibly because it failed silently.", "Specialized LogicGate is not null.")) return;
if (!Assert.Equal(b.InputCount, 1u, $"LogicGate.InputCount {b.InputCount} does not equal default value, possibly because it failed silently.", "LogicGate.InputCount is default.")) return;
if (!Assert.Equal(b.OutputCount, 1u, $"LogicGate.OutputCount {b.OutputCount} does not equal default value, possibly because it failed silently.", "LogicGate.OutputCount is default.")) return;
if (!Assert.NotNull(b, "Block.Specialize<LogicGate>() returned null, possibly because it failed silently.", "Specialized LogicGate is not null.")) return;
//if (!Assert.Equal(b.PortName(0, true), "Input", $"LogicGate.PortName(0, input:true) {b.PortName(0, true)} does not equal default value, possibly because it failed silently.", "LogicGate.PortName(0, input:true) is close enough to default.")) return;
LogicGate target = null;
if (!Assert.Errorless(() => { target = Block.PlaceNew<LogicGate>(BlockIDs.ANDLogicBlock, Unity.Mathematics.float3.zero + 2); })) return;
Wire newWire = null;
if (!Assert.Errorless(() => { newWire = b.Connect(0, target, 0);})) return;
if (!Assert.NotNull(newWire, "SignalingBlock.Connect(...) returned null, possible because it failed silently.", "SignalingBlock.Connect(...) returned a non-null value.")) return;
}
}
#endif
}

View file

@ -1,6 +1,7 @@
using System; using System;
using RobocraftX.Blocks; using RobocraftX.Blocks;
using RobocraftX.Common;
using Svelto.ECS; using Svelto.ECS;
using Unity.Mathematics; using Unity.Mathematics;
@ -9,80 +10,65 @@ using GamecraftModdingAPI.Utility;
namespace GamecraftModdingAPI.Blocks namespace GamecraftModdingAPI.Blocks
{ {
public class ConsoleBlock : Block public class ConsoleBlock : SignalingBlock
{ {
public static ConsoleBlock PlaceNew(float3 position,
float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0,
int uscale = 1, float3 scale = default, Player player = null)
{
if (PlacementEngine.IsInGame && GameState.IsBuildMode())
{
EGID id = PlacementEngine.PlaceBlock(BlockIDs.ConsoleBlock, color, darkness,
position, uscale, scale, player, rotation);
return new ConsoleBlock(id);
}
return null;
}
public ConsoleBlock(EGID id): base(id) public ConsoleBlock(EGID id): base(id)
{ {
if (!BlockEngine.GetBlockInfoExists<ConsoleBlockEntityStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
public ConsoleBlock(uint id): base(id) public ConsoleBlock(uint id): base(new EGID(id, CommonExclusiveGroups.BUILD_CONSOLE_BLOCK_GROUP))
{ {
if (!BlockEngine.GetBlockInfoExists<ConsoleBlockEntityStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
// custom console block properties // custom console block properties
/// <summary>
/// Setting a nonexistent command will crash the game when switching to simulation
/// </summary>
public string Command public string Command
{ {
get get
{ {
return BlockEngine.GetBlockInfo<ConsoleBlockEntityStruct>(Id).commandName; return BlockEngine.GetBlockInfo(this, (ConsoleBlockEntityStruct st) => st.commandName);
} }
set set
{ {
BlockEngine.GetBlockInfo<ConsoleBlockEntityStruct>(Id).commandName.Set(value); BlockEngine.SetBlockInfo(this, (ref ConsoleBlockEntityStruct st, string val) => st.commandName.Set(val),
value);
} }
} }
public string Arg1 public string Arg1
{ {
get => BlockEngine.GetBlockInfo<ConsoleBlockEntityStruct>(Id).arg1; get => BlockEngine.GetBlockInfo(this, (ConsoleBlockEntityStruct st) => st.arg1);
set set
{ {
BlockEngine.GetBlockInfo<ConsoleBlockEntityStruct>(Id).arg1.Set(value); BlockEngine.SetBlockInfo(this, (ref ConsoleBlockEntityStruct st, string val) => st.arg1.Set(val),
value);
} }
} }
public string Arg2 public string Arg2
{ {
get => BlockEngine.GetBlockInfo<ConsoleBlockEntityStruct>(Id).arg2; get => BlockEngine.GetBlockInfo(this, (ConsoleBlockEntityStruct st) => st.arg2);
set set
{ {
BlockEngine.GetBlockInfo<ConsoleBlockEntityStruct>(Id).arg2.Set(value); BlockEngine.SetBlockInfo(this, (ref ConsoleBlockEntityStruct st, string val) => st.arg2.Set(val),
value);
} }
} }
public string Arg3 public string Arg3
{ {
get => BlockEngine.GetBlockInfo<ConsoleBlockEntityStruct>(Id).arg3; get => BlockEngine.GetBlockInfo(this, (ConsoleBlockEntityStruct st) => st.arg3);
set set
{ {
BlockEngine.GetBlockInfo<ConsoleBlockEntityStruct>(Id).arg3.Set(value); BlockEngine.SetBlockInfo(this, (ref ConsoleBlockEntityStruct st, string val) => st.arg3.Set(val),
value);
} }
} }
} }

View file

@ -0,0 +1,16 @@
using RobocraftX.Common;
using Svelto.ECS;
namespace GamecraftModdingAPI.Blocks
{
public class LogicGate : SignalingBlock
{
public LogicGate(EGID id) : base(id)
{
}
public LogicGate(uint id) : base(new EGID(id, CommonExclusiveGroups.BUILD_LOGIC_BLOCK_GROUP))
{
}
}
}

View file

@ -1,6 +1,7 @@
using System; using System;
using RobocraftX.Blocks; using RobocraftX.Blocks;
using RobocraftX.Common;
using Svelto.ECS; using Svelto.ECS;
using Unity.Mathematics; using Unity.Mathematics;
@ -8,45 +9,14 @@ using GamecraftModdingAPI.Utility;
namespace GamecraftModdingAPI.Blocks namespace GamecraftModdingAPI.Blocks
{ {
public class Motor : Block public class Motor : SignalingBlock
{ {
/// <summary>
/// Places a new motor.
/// Any valid motor type is accepted.
/// This re-implements Block.PlaceNew(...)
/// </summary>
public static new Motor PlaceNew(BlockIDs block, float3 position,
float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0,
int uscale = 1, float3 scale = default, Player player = null)
{
if (!(block == BlockIDs.MotorS || block == BlockIDs.MotorM))
{
throw new BlockTypeException($"Block is not a {typeof(Motor).Name} block");
}
if (PlacementEngine.IsInGame && GameState.IsBuildMode())
{
EGID id = PlacementEngine.PlaceBlock(block, color, darkness,
position, uscale, scale, player, rotation);
return new Motor(id);
}
return null;
}
public Motor(EGID id) : base(id) public Motor(EGID id) : base(id)
{ {
if (!BlockEngine.GetBlockInfoExists<MotorReadOnlyStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
public Motor(uint id) : base(id) public Motor(uint id): base(new EGID(id, CommonExclusiveGroups.BUILD_MOTOR_BLOCK_GROUP))
{ {
if (!BlockEngine.GetBlockInfoExists<MotorReadOnlyStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
// custom motor properties // custom motor properties
@ -58,13 +28,12 @@ namespace GamecraftModdingAPI.Blocks
{ {
get get
{ {
return BlockEngine.GetBlockInfo<MotorReadOnlyStruct>(Id).maxVelocity; return BlockEngine.GetBlockInfo(this, (MotorReadOnlyStruct st) => st.maxVelocity);
} }
set set
{ {
ref MotorReadOnlyStruct motor = ref BlockEngine.GetBlockInfo<MotorReadOnlyStruct>(Id); BlockEngine.SetBlockInfo(this, (ref MotorReadOnlyStruct st, float val) => st.maxVelocity = val, value);
motor.maxVelocity = value;
} }
} }
@ -75,13 +44,12 @@ namespace GamecraftModdingAPI.Blocks
{ {
get get
{ {
return BlockEngine.GetBlockInfo<MotorReadOnlyStruct>(Id).maxForce; return BlockEngine.GetBlockInfo(this, (MotorReadOnlyStruct st) => st.maxForce);
} }
set set
{ {
ref MotorReadOnlyStruct motor = ref BlockEngine.GetBlockInfo<MotorReadOnlyStruct>(Id); BlockEngine.SetBlockInfo(this, (ref MotorReadOnlyStruct st, float val) => st.maxForce = val, value);
motor.maxForce = value;
} }
} }
@ -92,13 +60,12 @@ namespace GamecraftModdingAPI.Blocks
{ {
get get
{ {
return BlockEngine.GetBlockInfo<MotorReadOnlyStruct>(Id).reverse; return BlockEngine.GetBlockInfo(this, (MotorReadOnlyStruct st) => st.reverse);
} }
set set
{ {
ref MotorReadOnlyStruct motor = ref BlockEngine.GetBlockInfo<MotorReadOnlyStruct>(Id); BlockEngine.SetBlockInfo(this, (ref MotorReadOnlyStruct st, bool val) => st.reverse = val, value);
motor.reverse = value;
} }
} }
} }

View file

@ -35,12 +35,21 @@ namespace GamecraftModdingAPI.Blocks
// implementations for Movement static class // implementations for Movement static class
public float3 MoveBlock(uint blockID, float3 vector) internal float3 MoveBlock(EGID blockID, BlockEngine.BlockInitData data, float3 vector)
{ {
ref PositionEntityStruct posStruct = ref this.entitiesDB.QueryEntity<PositionEntityStruct>(blockID, CommonExclusiveGroups.OWNED_BLOCKS_GROUP); if (!entitiesDB.Exists<PositionEntityStruct>(blockID))
ref GridRotationStruct gridStruct = ref this.entitiesDB.QueryEntity<GridRotationStruct>(blockID, CommonExclusiveGroups.OWNED_BLOCKS_GROUP); {
ref LocalTransformEntityStruct transStruct = ref this.entitiesDB.QueryEntity<LocalTransformEntityStruct>(blockID, CommonExclusiveGroups.OWNED_BLOCKS_GROUP); if (data.Group == null) return float3.zero;
ref UECSPhysicsEntityStruct phyStruct = ref this.entitiesDB.QueryEntity<UECSPhysicsEntityStruct>(blockID, CommonExclusiveGroups.OWNED_BLOCKS_GROUP); var init = new EntityComponentInitializer(blockID, data.Group);
init.Init(new PositionEntityStruct {position = vector});
init.Init(new GridRotationStruct {position = vector});
init.Init(new LocalTransformEntityStruct {position = vector});
return vector;
}
ref PositionEntityStruct posStruct = ref this.entitiesDB.QueryEntity<PositionEntityStruct>(blockID);
ref GridRotationStruct gridStruct = ref this.entitiesDB.QueryEntity<GridRotationStruct>(blockID);
ref LocalTransformEntityStruct transStruct = ref this.entitiesDB.QueryEntity<LocalTransformEntityStruct>(blockID);
ref UECSPhysicsEntityStruct phyStruct = ref this.entitiesDB.QueryEntity<UECSPhysicsEntityStruct>(blockID);
// main (persistent) position // main (persistent) position
posStruct.position = vector; posStruct.position = vector;
// placement grid position // placement grid position
@ -52,13 +61,19 @@ namespace GamecraftModdingAPI.Blocks
{ {
Value = posStruct.position Value = posStruct.position
}); });
entitiesDB.QueryEntity<GridConnectionsEntityStruct>(blockID, CommonExclusiveGroups.OWNED_BLOCKS_GROUP).isProcessed = false; entitiesDB.QueryEntity<GridConnectionsEntityStruct>(blockID).isProcessed = false;
return posStruct.position; return posStruct.position;
} }
public float3 GetPosition(uint blockID) internal float3 GetPosition(EGID blockID, BlockEngine.BlockInitData data)
{ {
ref PositionEntityStruct posStruct = ref this.entitiesDB.QueryEntity<PositionEntityStruct>(blockID, CommonExclusiveGroups.OWNED_BLOCKS_GROUP); if (!entitiesDB.Exists<PositionEntityStruct>(blockID))
{
if (data.Group == null) return float3.zero;
var init = new EntityComponentInitializer(blockID, data.Group);
return init.Has<PositionEntityStruct>() ? init.Get<PositionEntityStruct>().position : float3.zero;
}
ref PositionEntityStruct posStruct = ref this.entitiesDB.QueryEntity<PositionEntityStruct>(blockID);
return posStruct.position; return posStruct.position;
} }
} }

View file

@ -0,0 +1,146 @@
using System;
using FMOD.Studio;
using FMODUnity;
using Gamecraft.Wires;
using RobocraftX.Common;
using RobocraftX.Blocks;
using Svelto.ECS;
using Unity.Mathematics;
using GamecraftModdingAPI;
using GamecraftModdingAPI.Tests;
using GamecraftModdingAPI.Utility;
namespace GamecraftModdingAPI.Blocks
{
public class MusicBlock : SignalingBlock
{
public MusicBlock(EGID id) : base(id)
{
}
public MusicBlock(uint id) : base(new EGID(id, CommonExclusiveGroups.BUILD_MUSIC_BLOCK_GROUP))
{
}
public byte TrackIndex
{
get
{
return BlockEngine.GetBlockInfo(this, (MusicBlockDataEntityStruct st) => st.trackIndx);
}
set
{
BlockEngine.SetBlockInfo(this,
(ref MusicBlockDataEntityStruct msdes, byte val) => msdes.trackIndx = val, value);
}
}
public Guid Track
{
get
{
return BlockEngine.GetBlockInfo(this,
(MusicBlockDataEntityStruct msdes) => msdes.fmod2DEventPaths.Get<Guid>(msdes.trackIndx));
}
set
{
BlockEngine.SetBlockInfo(this, (ref MusicBlockDataEntityStruct msdes, Guid val) =>
{
for (byte i = 0; i < msdes.fmod2DEventPaths.Count<Guid>(); i++)
{
Guid track = msdes.fmod2DEventPaths.Get<Guid>(i);
if (track == val)
{
msdes.trackIndx = i;
break;
}
}
}, value);
}
}
public Guid[] Tracks
{
get
{
return BlockEngine.GetBlockInfo(this, (MusicBlockDataEntityStruct msdes) =>
{
Guid[] tracks = new Guid[msdes.fmod2DEventPaths.Count<Guid>()];
for (byte i = 0; i < tracks.Length; i++)
{
tracks[i] = msdes.fmod2DEventPaths.Get<Guid>(i);
}
return tracks;
});
}
}
public float Volume
{
get
{
return BlockEngine.GetBlockInfo(this, (MusicBlockDataEntityStruct msdes) => msdes.tweakableVolume);
}
set
{
BlockEngine.SetBlockInfo(this,
(ref MusicBlockDataEntityStruct msdes, float val) => msdes.tweakableVolume = val, value);
}
}
public ChannelType ChannelType
{
get
{
Assert.Log("Block exists: " + Exists);
return BlockEngine.GetBlockInfo(this,
(MusicBlockDataEntityStruct msdes) => (ChannelType) msdes.channelType);
}
set
{
BlockEngine.SetBlockInfo(this,
(ref MusicBlockDataEntityStruct msdes, ChannelType val) => msdes.channelType = (byte) val, value);
}
}
public bool IsPlaying
{
get
{
return BlockEngine.GetBlockInfo(this,
(MusicBlockDataEntityStruct msdes) => msdes.isPlaying);
}
set
{
BlockEngine.SetBlockInfo(this, (ref MusicBlockDataEntityStruct msdes, bool val) =>
{
if (msdes.isPlaying == val) return;
if (val)
{
// start playing
EventInstance inst = RuntimeManager.CreateInstance(msdes.fmod2DEventPaths.Get<Guid>(msdes.trackIndx));
inst.setVolume(msdes.tweakableVolume / 100f);
inst.start();
msdes.eventHandle = inst.handle;
}
else
{
// stop playing
EventInstance inst = default(EventInstance);
inst.handle = msdes.eventHandle;
inst.stop(FMOD.Studio.STOP_MODE.ALLOWFADEOUT);
inst.release();
}
msdes.isPlaying = val;
}, value);
}
}
}
}

View file

@ -1,4 +1,5 @@
using Gamecraft.Wires; using Gamecraft.Wires;
using RobocraftX.Common;
using Svelto.ECS; using Svelto.ECS;
namespace GamecraftModdingAPI.Blocks namespace GamecraftModdingAPI.Blocks
@ -7,27 +8,22 @@ namespace GamecraftModdingAPI.Blocks
{ {
public ObjectIdentifier(EGID id) : base(id) public ObjectIdentifier(EGID id) : base(id)
{ {
if (!BlockEngine.GetBlockInfoExists<ObjectIdEntityStruct>(Id))
{
throw new BlockTypeException($"Block is not a {GetType().Name} block");
}
} }
public ObjectIdentifier(uint id) : base(id) public ObjectIdentifier(uint id) : base(new EGID(id, CommonExclusiveGroups.BUILD_OBJID_BLOCK_GROUP))
{ {
if (!BlockEngine.GetBlockInfoExists<ObjectIdEntityStruct>(Id))
{
throw new BlockTypeException($"Block is not a {GetType().Name} block");
}
} }
public char Identifier public char Identifier
{ {
get => (char) (BlockEngine.GetBlockInfo<ObjectIdEntityStruct>(Id).objectId + 'A'); get => (char) BlockEngine.GetBlockInfo(this, (ObjectIdEntityStruct st) => st.objectId + 'A');
set set
{ {
BlockEngine.GetBlockInfo<ObjectIdEntityStruct>(Id).objectId = (byte) (value - 'A'); BlockEngine.SetBlockInfo(this, (ref ObjectIdEntityStruct st, char val) =>
Label = value + ""; //The label isn't updated automatically {
st.objectId = (byte) (val - 'A');
Label = val + ""; //The label isn't updated automatically
}, value);
} }
} }
@ -36,7 +32,7 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public byte SimID public byte SimID
{ {
get => BlockEngine.GetBlockInfo<ObjectIdEntityStruct>(Id).simObjectId; get => BlockEngine.GetBlockInfo(this, (ObjectIdEntityStruct st) => st.simObjectId);
} }
/// <summary> /// <summary>

View file

@ -5,48 +5,18 @@ using Svelto.ECS;
using Unity.Mathematics; using Unity.Mathematics;
using GamecraftModdingAPI.Utility; using GamecraftModdingAPI.Utility;
using RobocraftX.Common;
namespace GamecraftModdingAPI.Blocks namespace GamecraftModdingAPI.Blocks
{ {
public class Piston : Block public class Piston : SignalingBlock
{ {
/// <summary>
/// Places a new piston.
/// Any valid piston type is accepted.
/// This re-implements Block.PlaceNew(...)
/// </summary>
public static new Piston PlaceNew(BlockIDs block, float3 position,
float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0,
int uscale = 1, float3 scale = default, Player player = null)
{
if (!(block == BlockIDs.ServoPiston || block == BlockIDs.StepperPiston || block == BlockIDs.PneumaticPiston))
{
throw new BlockTypeException($"Block is not a {typeof(Piston).Name} block");
}
if (PlacementEngine.IsInGame && GameState.IsBuildMode())
{
EGID id = PlacementEngine.PlaceBlock(block, color, darkness,
position, uscale, scale, player, rotation);
return new Piston(id);
}
return null;
}
public Piston(EGID id) : base(id) public Piston(EGID id) : base(id)
{ {
if (!BlockEngine.GetBlockInfoExists<PistonReadOnlyStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
public Piston(uint id) : base(id) public Piston(uint id) : base(new EGID(id, CommonExclusiveGroups.BUILD_PISTON_BLOCK_GROUP))
{ {
if (!BlockEngine.GetBlockInfoExists<PistonReadOnlyStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
// custom piston properties // custom piston properties
@ -56,12 +26,12 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public float MaximumExtension public float MaximumExtension
{ {
get => BlockEngine.GetBlockInfo<PistonReadOnlyStruct>(Id).maxDeviation; get => BlockEngine.GetBlockInfo(this, (PistonReadOnlyStruct st) => st.maxDeviation);
set set
{ {
ref PistonReadOnlyStruct piston = ref BlockEngine.GetBlockInfo<PistonReadOnlyStruct>(Id); BlockEngine.SetBlockInfo(this, (ref PistonReadOnlyStruct st, float val) => st.maxDeviation = val,
piston.maxDeviation = value; value);
} }
} }
@ -70,12 +40,11 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public float MaximumForce public float MaximumForce
{ {
get => BlockEngine.GetBlockInfo<PistonReadOnlyStruct>(Id).maxForce; get => BlockEngine.GetBlockInfo(this, (PistonReadOnlyStruct st) => st.maxForce);
set set
{ {
ref PistonReadOnlyStruct piston = ref BlockEngine.GetBlockInfo<PistonReadOnlyStruct>(Id); BlockEngine.SetBlockInfo(this, (ref PistonReadOnlyStruct st, float val) => st.maxForce = val, value);
piston.maxForce = value;
} }
} }
} }

View file

@ -40,15 +40,16 @@ namespace GamecraftModdingAPI.Blocks
private static BlockEntityFactory _blockEntityFactory; //Injected from PlaceBlockEngine private static BlockEntityFactory _blockEntityFactory; //Injected from PlaceBlockEngine
public EGID PlaceBlock(BlockIDs block, BlockColors color, byte darkness, float3 position, int uscale, public EGID PlaceBlock(BlockIDs block, BlockColors color, byte darkness, float3 position, int uscale,
float3 scale, Player player, float3 rotation) float3 scale, Player player, float3 rotation, out EntityComponentInitializer initializer)
{ //It appears that only the non-uniform scale has any visible effect, but if that's not given here it will be set to the uniform one { //It appears that only the non-uniform scale has any visible effect, but if that's not given here it will be set to the uniform one
if (darkness > 9) if (darkness > 9)
throw new Exception("That is too dark. Make sure to use 0-9 as darkness. (0 is default.)"); throw new Exception("That is too dark. Make sure to use 0-9 as darkness. (0 is default.)");
return BuildBlock((ushort) block, (byte) (color + darkness * 10), position, uscale, scale, rotation, initializer = BuildBlock((ushort) block, (byte) (color + darkness * 10), position, uscale, scale, rotation,
(player ?? new Player(PlayerType.Local)).Id); (player ?? new Player(PlayerType.Local)).Id);
return initializer.EGID;
} }
private EGID BuildBlock(ushort block, byte color, float3 position, int uscale, float3 scale, float3 rot, uint playerId) private EntityComponentInitializer BuildBlock(ushort block, byte color, float3 position, int uscale, float3 scale, float3 rot, uint playerId)
{ {
if (_blockEntityFactory == null) if (_blockEntityFactory == null)
throw new Exception("The factory is null."); throw new Exception("The factory is null.");
@ -66,8 +67,6 @@ namespace GamecraftModdingAPI.Blocks
RotationEntityStruct rotation = new RotationEntityStruct {rotation = rotQ}; RotationEntityStruct rotation = new RotationEntityStruct {rotation = rotQ};
GridRotationStruct gridRotation = new GridRotationStruct GridRotationStruct gridRotation = new GridRotationStruct
{position = position, rotation = rotQ}; {position = position, rotation = rotQ};
CubeCategoryStruct category = new CubeCategoryStruct
{category = CubeCategory.General, type = CubeType.Block};
DBEntityStruct dbEntity = new DBEntityStruct {DBID = dbid}; DBEntityStruct dbEntity = new DBEntityStruct {DBID = dbid};
BlockPlacementScaleEntityStruct placementScale = new BlockPlacementScaleEntityStruct BlockPlacementScaleEntityStruct placementScale = new BlockPlacementScaleEntityStruct
{ {
@ -76,21 +75,10 @@ namespace GamecraftModdingAPI.Blocks
unitSnapOffset = 0, isUsingUnitSize = true unitSnapOffset = 0, isUsingUnitSize = true
}; };
EquippedColourStruct colour = new EquippedColourStruct {indexInPalette = color}; EquippedColourStruct colour = new EquippedColourStruct {indexInPalette = color};
EGID newBlockID;
switch (category.category)
{
case CubeCategory.SpawnPoint:
case CubeCategory.BuildingSpawnPoint:
newBlockID = MachineEditingGroups.NewUncheckedBlockEGID;
break;
default:
newBlockID = MachineEditingGroups.NewBlockID;
break;
}
EntityComponentInitializer EntityComponentInitializer
structInitializer = structInitializer =
_blockEntityFactory.Build(newBlockID, dbid); //The ghost block index is only used for triggers _blockEntityFactory.Build(CommonExclusiveGroups.nextBlockEntityID, dbid); //The ghost block index is only used for triggers
if (colour.indexInPalette != byte.MaxValue) if (colour.indexInPalette != byte.MaxValue)
structInitializer.Init(new ColourParameterEntityStruct structInitializer.Init(new ColourParameterEntityStruct
{ {
@ -117,10 +105,9 @@ namespace GamecraftModdingAPI.Blocks
PrimaryRotationUtility.InitialisePrimaryDirection(rotation.rotation, ref structInitializer); PrimaryRotationUtility.InitialisePrimaryDirection(rotation.rotation, ref structInitializer);
EGID playerEGID = new EGID(playerId, CharacterExclusiveGroups.OnFootGroup); EGID playerEGID = new EGID(playerId, CharacterExclusiveGroups.OnFootGroup);
ref PickedBlockExtraDataStruct pickedBlock = ref entitiesDB.QueryEntity<PickedBlockExtraDataStruct>(playerEGID); ref PickedBlockExtraDataStruct pickedBlock = ref entitiesDB.QueryEntity<PickedBlockExtraDataStruct>(playerEGID);
pickedBlock.placedBlockEntityID = playerEGID; pickedBlock.placedBlockEntityID = structInitializer.EGID;
pickedBlock.placedBlockWasAPickedBlock = false; pickedBlock.placedBlockWasAPickedBlock = false;
Block.BlockEngine.Synced = false; // Block entities will need to be submitted before properties can be used return structInitializer;
return newBlockID;
} }
public string Name { get; } = "GamecraftModdingAPIPlacementGameEngine"; public string Name { get; } = "GamecraftModdingAPIPlacementGameEngine";

View file

@ -35,37 +35,55 @@ namespace GamecraftModdingAPI.Blocks
// implementations for Rotation static class // implementations for Rotation static class
public float3 RotateBlock(uint blockID, Vector3 vector) internal float3 RotateBlock(EGID blockID, BlockEngine.BlockInitData data, Vector3 vector)
{ {
ref RotationEntityStruct rotStruct = ref this.entitiesDB.QueryEntity<RotationEntityStruct>(blockID, CommonExclusiveGroups.OWNED_BLOCKS_GROUP); if (!entitiesDB.Exists<RotationEntityStruct>(blockID))
ref GridRotationStruct gridStruct = ref this.entitiesDB.QueryEntity<GridRotationStruct>(blockID, CommonExclusiveGroups.OWNED_BLOCKS_GROUP); {
ref LocalTransformEntityStruct transStruct = ref this.entitiesDB.QueryEntity<LocalTransformEntityStruct>(blockID, CommonExclusiveGroups.OWNED_BLOCKS_GROUP); if (data.Group == null) return float3.zero;
ref UECSPhysicsEntityStruct phyStruct = ref this.entitiesDB.QueryEntity<UECSPhysicsEntityStruct>(blockID, CommonExclusiveGroups.OWNED_BLOCKS_GROUP); var init = new EntityComponentInitializer(blockID, data.Group);
init.Init(new RotationEntityStruct {rotation = new Quaternion {eulerAngles = vector}});
init.Init(new GridRotationStruct {rotation = new Quaternion {eulerAngles = vector}});
init.Init(new LocalTransformEntityStruct {rotation = new Quaternion {eulerAngles = vector}});
return vector;
}
ref RotationEntityStruct rotStruct = ref this.entitiesDB.QueryEntity<RotationEntityStruct>(blockID);
ref GridRotationStruct gridStruct = ref this.entitiesDB.QueryEntity<GridRotationStruct>(blockID);
ref LocalTransformEntityStruct transStruct = ref this.entitiesDB.QueryEntity<LocalTransformEntityStruct>(blockID);
ref UECSPhysicsEntityStruct phyStruct = ref this.entitiesDB.QueryEntity<UECSPhysicsEntityStruct>(blockID);
// main (persistent) position // main (persistent) position
Quaternion newRotation = (Quaternion)rotStruct.rotation; Quaternion newRotation = rotStruct.rotation;
newRotation.eulerAngles += vector; newRotation.eulerAngles = vector;
rotStruct.rotation = (quaternion)newRotation; rotStruct.rotation = newRotation;
// placement grid rotation // placement grid rotation
Quaternion newGridRotation = (Quaternion)gridStruct.rotation; Quaternion newGridRotation = gridStruct.rotation;
newGridRotation.eulerAngles += vector; newGridRotation.eulerAngles = vector;
gridStruct.rotation = (quaternion)newGridRotation; gridStruct.rotation = newGridRotation;
// rendered position // rendered position
Quaternion newTransRotation = (Quaternion)rotStruct.rotation; Quaternion newTransRotation = rotStruct.rotation;
newTransRotation.eulerAngles += vector; newTransRotation.eulerAngles = vector;
transStruct.rotation = newTransRotation; transStruct.rotation = newTransRotation;
// collision position // collision position
FullGameFields._physicsWorld.EntityManager.SetComponentData(phyStruct.uecsEntity, new Unity.Transforms.Rotation FullGameFields._physicsWorld.EntityManager.SetComponentData(phyStruct.uecsEntity, new Unity.Transforms.Rotation
{ {
Value = rotStruct.rotation Value = rotStruct.rotation
}); });
entitiesDB.QueryEntity<GridConnectionsEntityStruct>(blockID, CommonExclusiveGroups.OWNED_BLOCKS_GROUP).isProcessed = false; entitiesDB.QueryEntity<GridConnectionsEntityStruct>(blockID).isProcessed = false;
return ((Quaternion)rotStruct.rotation).eulerAngles; return ((Quaternion)rotStruct.rotation).eulerAngles;
} }
public float3 GetRotation(uint blockID) internal float3 GetRotation(EGID blockID, BlockEngine.BlockInitData data)
{ {
ref RotationEntityStruct rotStruct = ref entitiesDB.QueryEntity<RotationEntityStruct>(blockID, CommonExclusiveGroups.OWNED_BLOCKS_GROUP); if (!entitiesDB.Exists<RotationEntityStruct>(blockID))
{
if (data.Group == null) return float3.zero;
var init = new EntityComponentInitializer(blockID, data.Group);
return init.Has<RotationEntityStruct>()
? (float3) ((Quaternion) init.Get<RotationEntityStruct>().rotation).eulerAngles
: float3.zero;
}
ref RotationEntityStruct rotStruct = ref entitiesDB.QueryEntity<RotationEntityStruct>(blockID);
return ((Quaternion) rotStruct.rotation).eulerAngles; return ((Quaternion) rotStruct.rotation).eulerAngles;
} }
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using RobocraftX.Blocks; using RobocraftX.Blocks;
using RobocraftX.Common;
using Svelto.ECS; using Svelto.ECS;
using Unity.Mathematics; using Unity.Mathematics;
@ -8,45 +9,14 @@ using GamecraftModdingAPI.Utility;
namespace GamecraftModdingAPI.Blocks namespace GamecraftModdingAPI.Blocks
{ {
public class Servo : Block public class Servo : SignalingBlock
{ {
/// <summary>
/// Places a new servo.
/// Any valid servo type is accepted.
/// This re-implements Block.PlaceNew(...)
/// </summary>
public static new Servo PlaceNew(BlockIDs block, float3 position,
float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0,
int uscale = 1, float3 scale = default, Player player = null)
{
if (!(block == BlockIDs.ServoAxle || block == BlockIDs.ServoHinge || block == BlockIDs.ServoPiston))
{
throw new BlockTypeException($"Block is not a {nameof(Servo)} block");
}
if (PlacementEngine.IsInGame && GameState.IsBuildMode())
{
EGID id = PlacementEngine.PlaceBlock(block, color, darkness,
position, uscale, scale, player, rotation);
return new Servo(id);
}
return null;
}
public Servo(EGID id) : base(id) public Servo(EGID id) : base(id)
{ {
if (!BlockEngine.GetBlockInfoExists<ServoReadOnlyStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
public Servo(uint id) : base(id) public Servo(uint id) : base(new EGID(id, CommonExclusiveGroups.BUILD_SERVO_BLOCK_GROUP))
{ {
if (!BlockEngine.GetBlockInfoExists<ServoReadOnlyStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
// custom servo properties // custom servo properties
@ -56,12 +26,11 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public float MinimumAngle public float MinimumAngle
{ {
get => BlockEngine.GetBlockInfo<ServoReadOnlyStruct>(Id).minDeviation; get => BlockEngine.GetBlockInfo(this, (ServoReadOnlyStruct st) => st.minDeviation);
set set
{ {
ref ServoReadOnlyStruct servo = ref BlockEngine.GetBlockInfo<ServoReadOnlyStruct>(Id); BlockEngine.SetBlockInfo(this, (ref ServoReadOnlyStruct st, float val) => st.minDeviation = val, value);
servo.minDeviation = value;
} }
} }
@ -70,12 +39,11 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public float MaximumAngle public float MaximumAngle
{ {
get => BlockEngine.GetBlockInfo<ServoReadOnlyStruct>(Id).maxDeviation; get => BlockEngine.GetBlockInfo(this, (ServoReadOnlyStruct st) => st.maxDeviation);
set set
{ {
ref ServoReadOnlyStruct servo = ref BlockEngine.GetBlockInfo<ServoReadOnlyStruct>(Id); BlockEngine.SetBlockInfo(this, (ref ServoReadOnlyStruct st, float val) => st.maxDeviation = val, value);
servo.maxDeviation = value;
} }
} }
@ -84,12 +52,11 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public float MaximumForce public float MaximumForce
{ {
get => BlockEngine.GetBlockInfo<ServoReadOnlyStruct>(Id).maxForce; get => BlockEngine.GetBlockInfo(this, (ServoReadOnlyStruct st) => st.maxForce);
set set
{ {
ref ServoReadOnlyStruct servo = ref BlockEngine.GetBlockInfo<ServoReadOnlyStruct>(Id); BlockEngine.SetBlockInfo(this, (ref ServoReadOnlyStruct st, float val) => st.maxForce = val, value);
servo.maxForce = value;
} }
} }
@ -98,12 +65,11 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public bool Reverse public bool Reverse
{ {
get => BlockEngine.GetBlockInfo<ServoReadOnlyStruct>(Id).reverse; get => BlockEngine.GetBlockInfo(this, (ServoReadOnlyStruct st) => st.reverse);
set set
{ {
ref ServoReadOnlyStruct servo = ref BlockEngine.GetBlockInfo<ServoReadOnlyStruct>(Id); BlockEngine.SetBlockInfo(this, (ref ServoReadOnlyStruct st, bool val) => st.reverse = val, value);
servo.reverse = value;
} }
} }
} }

View file

@ -1,4 +1,6 @@
using Svelto.ECS; using System;
using Svelto.ECS;
using Svelto.DataStructures;
using Gamecraft.Wires; using Gamecraft.Wires;
using GamecraftModdingAPI.Engines; using GamecraftModdingAPI.Engines;
@ -8,7 +10,7 @@ namespace GamecraftModdingAPI.Blocks
/// <summary> /// <summary>
/// Engine which executes signal actions /// Engine which executes signal actions
/// </summary> /// </summary>
public class SignalEngine : IApiEngine public class SignalEngine : IApiEngine, IFactoryEngine
{ {
public const float POSITIVE_HIGH = 1.0f; public const float POSITIVE_HIGH = 1.0f;
public const float NEGATIVE_HIGH = -1.0f; public const float NEGATIVE_HIGH = -1.0f;
@ -19,6 +21,8 @@ namespace GamecraftModdingAPI.Blocks
public EntitiesDB entitiesDB { set; private get; } public EntitiesDB entitiesDB { set; private get; }
public IEntityFactory Factory { get; set; }
public bool isRemovable => false; public bool isRemovable => false;
public bool IsInGame = false; public bool IsInGame = false;
@ -33,7 +37,74 @@ namespace GamecraftModdingAPI.Blocks
IsInGame = true; IsInGame = true;
} }
// implementations for Signal static class // implementations for block wiring
public WireEntityStruct CreateNewWire(EGID startBlock, byte startPort, EGID endBlock, byte endPort)
{
EGID wireEGID = new EGID(WiresExclusiveGroups.NewWireEntityId, NamedExclusiveGroup<WiresGroup>.Group);
EntityComponentInitializer wireInitializer = Factory.BuildEntity<WireEntityDescriptor>(wireEGID);
wireInitializer.Init(new WireEntityStruct
{
sourceBlockEGID = startBlock,
sourcePortUsage = startPort,
destinationBlockEGID = endBlock,
destinationPortUsage = endPort,
ID = wireEGID
});
return wireInitializer.Get<WireEntityStruct>();
}
public ref WireEntityStruct GetWire(EGID wire)
{
if (!entitiesDB.Exists<WireEntityStruct>(wire))
{
throw new WiringException($"Wire {wire} does not exist");
}
return ref entitiesDB.QueryEntity<WireEntityStruct>(wire);
}
public ref PortEntityStruct GetPort(EGID port)
{
if (!entitiesDB.Exists<PortEntityStruct>(port))
{
throw new WiringException($"Port {port} does not exist (yet?)");
}
return ref entitiesDB.QueryEntity<PortEntityStruct>(port);
}
public ref PortEntityStruct GetPortByOffset(BlockPortsStruct bps, byte portNumber, bool input)
{
ExclusiveGroup group = input
? NamedExclusiveGroup<InputPortsGroup>.Group
: NamedExclusiveGroup<OutputPortsGroup>.Group;
uint id = (input ? bps.firstInputID : bps.firstOutputID) + portNumber;
EGID egid = new EGID(id, group);
if (!entitiesDB.Exists<PortEntityStruct>(egid))
{
throw new WiringException("Port does not exist");
}
return ref entitiesDB.QueryEntity<PortEntityStruct>(egid);
}
public ref PortEntityStruct GetPortByOffset(Block block, byte portNumber, bool input)
{
BlockPortsStruct bps = GetFromDbOrInitData<BlockPortsStruct>(block, block.Id, out bool exists);
if (!exists)
{
throw new BlockException("Block does not exist");
}
return ref GetPortByOffset(bps, portNumber, input);
}
public ref T GetComponent<T>(EGID egid) where T : unmanaged, IEntityComponent
{
return ref entitiesDB.QueryEntity<T>(egid);
}
public bool Exists<T>(EGID egid) where T : struct, IEntityComponent
{
return entitiesDB.Exists<T>(egid);
}
public bool SetSignal(EGID blockID, float signal, out uint signalID, bool input = true) public bool SetSignal(EGID blockID, float signal, out uint signalID, bool input = true)
{ {
@ -133,6 +204,42 @@ namespace GamecraftModdingAPI.Blocks
return outputs; return outputs;
} }
public EGID MatchBlockInputToPort(Block block, byte portUsage, out bool exists)
{
BlockPortsStruct ports = GetFromDbOrInitData<BlockPortsStruct>(block, block.Id, out exists);
return new EGID(ports.firstInputID + portUsage, NamedExclusiveGroup<InputPortsGroup>.Group);
}
public EGID MatchBlockInputToPort(EGID block, byte portUsage, out bool exists)
{
if (!entitiesDB.Exists<BlockPortsStruct>(block))
{
exists = false;
return default;
}
exists = true;
BlockPortsStruct ports = entitiesDB.QueryEntity<BlockPortsStruct>(block);
return new EGID(ports.firstInputID + portUsage, NamedExclusiveGroup<InputPortsGroup>.Group);
}
public EGID MatchBlockOutputToPort(Block block, byte portUsage, out bool exists)
{
BlockPortsStruct ports = GetFromDbOrInitData<BlockPortsStruct>(block, block.Id, out exists);
return new EGID(ports.firstOutputID + portUsage, NamedExclusiveGroup<OutputPortsGroup>.Group);
}
public EGID MatchBlockOutputToPort(EGID block, byte portUsage, out bool exists)
{
if (!entitiesDB.Exists<BlockPortsStruct>(block))
{
exists = false;
return default;
}
exists = true;
BlockPortsStruct ports = entitiesDB.QueryEntity<BlockPortsStruct>(block);
return new EGID(ports.firstOutputID + portUsage, NamedExclusiveGroup<OutputPortsGroup>.Group);
}
public ref WireEntityStruct MatchPortToWire(EGID portID, EGID blockID, out bool exists) public ref WireEntityStruct MatchPortToWire(EGID portID, EGID blockID, out bool exists)
{ {
ref PortEntityStruct port = ref entitiesDB.QueryEntity<PortEntityStruct>(portID); ref PortEntityStruct port = ref entitiesDB.QueryEntity<PortEntityStruct>(portID);
@ -151,6 +258,57 @@ namespace GamecraftModdingAPI.Blocks
return ref defRef[0]; return ref defRef[0];
} }
public ref WireEntityStruct MatchBlocksToWire(EGID startBlock, EGID endBlock, out bool exists, byte startPort = byte.MaxValue,
byte endPort = byte.MaxValue)
{
EGID[] startPorts;
if (startPort == byte.MaxValue)
{
// search all output ports on source block
startPorts = GetSignalOutputs(startBlock);
}
else
{
BlockPortsStruct ports = entitiesDB.QueryEntity<BlockPortsStruct>(startBlock);
startPorts = new EGID[] {new EGID(ports.firstOutputID + startPort, NamedExclusiveGroup<OutputPortsGroup>.Group) };
}
EGID[] endPorts;
if (startPort == byte.MaxValue)
{
// search all input ports on destination block
endPorts = GetSignalInputs(endBlock);
}
else
{
BlockPortsStruct ports = entitiesDB.QueryEntity<BlockPortsStruct>(endBlock);
endPorts = new EGID[] {new EGID(ports.firstInputID + endPort, NamedExclusiveGroup<InputPortsGroup>.Group) };
}
EntityCollection<WireEntityStruct> wires = entitiesDB.QueryEntities<WireEntityStruct>(NamedExclusiveGroup<WiresGroup>.Group);
for (int endIndex = 0; endIndex < endPorts.Length; endIndex++)
{
PortEntityStruct endPES = entitiesDB.QueryEntity<PortEntityStruct>(endPorts[endIndex]);
for (int startIndex = 0; startIndex < startPorts.Length; startIndex++)
{
PortEntityStruct startPES = entitiesDB.QueryEntity<PortEntityStruct>(startPorts[startIndex]);
for (int w = 0; w < wires.count; w++)
{
if ((wires[w].destinationPortUsage == endPES.usage && wires[w].destinationBlockEGID == endBlock)
&& (wires[w].sourcePortUsage == startPES.usage && wires[w].sourceBlockEGID == startBlock))
{
exists = true;
return ref wires[w];
}
}
}
}
exists = false;
WireEntityStruct[] defRef = new WireEntityStruct[1];
return ref defRef[0];
}
public ref ChannelDataStruct GetChannelDataStruct(EGID portID, out bool exists) public ref ChannelDataStruct GetChannelDataStruct(EGID portID, out bool exists)
{ {
ref PortEntityStruct port = ref entitiesDB.QueryEntity<PortEntityStruct>(portID); ref PortEntityStruct port = ref entitiesDB.QueryEntity<PortEntityStruct>(portID);
@ -167,20 +325,74 @@ namespace GamecraftModdingAPI.Blocks
public EGID[] GetElectricBlocks() public EGID[] GetElectricBlocks()
{ {
uint count = entitiesDB.Count<BlockPortsStruct>(BlockIdentifiers.OWNED_BLOCKS) + entitiesDB.Count<BlockPortsStruct>(BlockIdentifiers.FUNCTIONAL_BLOCK_PARTS); var res = new FasterList<EGID>();
uint i = 0; foreach (var (coll, _) in entitiesDB.QueryEntities<BlockPortsStruct>())
EGID[] res = new EGID[count]; foreach (ref BlockPortsStruct s in coll)
foreach (ref BlockPortsStruct s in entitiesDB.QueryEntities<BlockPortsStruct>(BlockIdentifiers.OWNED_BLOCKS)) res.Add(s.ID);
{ return res.ToArray();
res[i] = s.ID;
i++;
} }
foreach (ref BlockPortsStruct s in entitiesDB.QueryEntities<BlockPortsStruct>(BlockIdentifiers.FUNCTIONAL_BLOCK_PARTS))
public EGID[] WiredToInput(EGID block, byte port)
{ {
res[i] = s.ID; WireEntityStruct[] wireEntityStructs = Search(NamedExclusiveGroup<WiresGroup>.Group,
i++; (WireEntityStruct wes) => wes.destinationPortUsage == port && wes.destinationBlockEGID == block);
EGID[] result = new EGID[wireEntityStructs.Length];
for (uint i = 0; i < wireEntityStructs.Length; i++)
{
result[i] = wireEntityStructs[i].ID;
} }
return res;
return result;
}
public EGID[] WiredToOutput(EGID block, byte port)
{
WireEntityStruct[] wireEntityStructs = Search(NamedExclusiveGroup<WiresGroup>.Group,
(WireEntityStruct wes) => wes.sourcePortUsage == port && wes.sourceBlockEGID == block);
EGID[] result = new EGID[wireEntityStructs.Length];
for (uint i = 0; i < wireEntityStructs.Length; i++)
{
result[i] = wireEntityStructs[i].ID;
}
return result;
}
private T[] Search<T>(ExclusiveGroup group, Func<T, bool> isMatch) where T : struct, IEntityComponent
{
FasterList<T> results = new FasterList<T>();
EntityCollection<T> components = entitiesDB.QueryEntities<T>(group);
for (uint i = 0; i < components.count; i++)
{
if (isMatch(components[i]))
{
results.Add(components[i]);
}
}
return results.ToArray();
}
private ref T GetFromDbOrInitData<T>(Block block, EGID id, out bool exists) where T : unmanaged, IEntityComponent
{
T[] defRef = new T[1];
if (entitiesDB.Exists<T>(id))
{
exists = true;
return ref entitiesDB.QueryEntity<T>(id);
}
if (block == null || block.InitData.Group == null)
{
exists = false;
return ref defRef[0];
}
EntityComponentInitializer initializer = new EntityComponentInitializer(block.Id, block.InitData.Group);
if (initializer.Has<T>())
{
exists = true;
return ref initializer.Get<T>();
}
exists = false;
return ref defRef[0];
} }
private EntityCollection<ChannelDataStruct> GetSignalStruct(uint signalID, out uint index, bool input = true) private EntityCollection<ChannelDataStruct> GetSignalStruct(uint signalID, out uint index, bool input = true)

View file

@ -14,44 +14,12 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public class SignalingBlock : Block public class SignalingBlock : Block
{ {
/// <summary>
/// Places a new signaling block.
/// Any valid functional block type with IO ports will work.
/// This re-implements Block.PlaceNew(...)
/// </summary>
public static new SignalingBlock PlaceNew(BlockIDs block, float3 position,
float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0,
int uscale = 1, float3 scale = default, Player player = null)
{
if (PlacementEngine.IsInGame && GameState.IsBuildMode())
{
EGID id = PlacementEngine.PlaceBlock(block, color, darkness,
position, uscale, scale, player, rotation);
return new SignalingBlock(id);
}
return null;
}
public SignalingBlock(EGID id) : base(id) public SignalingBlock(EGID id) : base(id)
{ {
if (!BlockEngine.GetBlockInfoExists<BlockPortsStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
public SignalingBlock(uint id) : base(id) public SignalingBlock(uint id) : base(id)
{ {
if (!BlockEngine.GetBlockInfoExists<BlockPortsStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
}
protected ref BlockPortsStruct GetBlockPortsStruct()
{
return ref BlockEngine.GetBlockInfo<BlockPortsStruct>(Id);
} }
/// <summary> /// <summary>
@ -72,16 +40,6 @@ namespace GamecraftModdingAPI.Blocks
return SignalEngine.GetSignalOutputs(Id); return SignalEngine.GetSignalOutputs(Id);
} }
/// <summary>
/// Gets the port struct.
/// </summary>
/// <returns>The port struct.</returns>
/// <param name="portId">Port identifier.</param>
protected ref PortEntityStruct GetPortStruct(EGID portId)
{
return ref BlockEngine.GetBlockInfo<PortEntityStruct>(portId);
}
/// <summary> /// <summary>
/// Gets the connected wire. /// Gets the connected wire.
/// </summary> /// </summary>
@ -109,7 +67,7 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public uint InputCount public uint InputCount
{ {
get => GetBlockPortsStruct().inputCount; get => BlockEngine.GetBlockInfo(this, (BlockPortsStruct st) => st.inputCount);
} }
/// <summary> /// <summary>
@ -117,7 +75,93 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public uint OutputCount public uint OutputCount
{ {
get => GetBlockPortsStruct().outputCount; get => BlockEngine.GetBlockInfo(this, (BlockPortsStruct st) => st.outputCount);
}
/// <summary>
/// Connect an output on this block to an input on another block.
/// </summary>
/// <param name="sourcePort">Output port number.</param>
/// <param name="destination">Input block.</param>
/// <param name="destinationPort">Input port number.</param>
/// <returns>The wire connection</returns>
/// <exception cref="WiringException">The wire could not be created.</exception>
public Wire Connect(byte sourcePort, SignalingBlock destination, byte destinationPort)
{
if (sourcePort >= OutputCount)
{
throw new WiringException("Source port does not exist");
}
if (destinationPort >= destination.InputCount)
{
throw new WiringException("Destination port does not exist");
}
return Wire.Connect(this, sourcePort, destination, destinationPort);
}
/// <summary>
/// The port's name.
/// This is localized to the user's language, so this is not reliable for port identification.
/// </summary>
/// <param name="port">Port number.</param>
/// <param name="input">Whether the port is an input (true) or an output (false).</param>
/// <returns>The localized port name.</returns>
public string PortName(byte port, bool input)
{
BlockPortsStruct bps = BlockEngine.GetBlockInfo(this, (BlockPortsStruct a) => a);
PortEntityStruct pes = SignalEngine.GetPortByOffset(this, port, input);
return pes.portNameLocalised;
}
/// <summary>
/// The input port's name.
/// </summary>
/// <param name="port">Input port number.</param>
/// <returns>The port name, localized to the user's language.</returns>
public string InputPortName(byte port) => PortName(port, true);
/// <summary>
/// The output port's name.
/// </summary>
/// <param name="port">Output port number.</param>
/// <returns>The port name, localized to the user's language.</returns>
public string OutputPortName(byte port) => PortName(port, false);
/// <summary>
/// All wires connected to the input port.
/// These wires will always be wired output -> input.
/// </summary>
/// <param name="port">Port number.</param>
/// <returns>Wires connected to the input port.</returns>
public Wire[] ConnectedToInput(byte port)
{
if (port >= InputCount) throw new WiringException($"Port input {port} does not exist");
EGID[] wireEgids = SignalEngine.WiredToInput(Id, port);
Wire[] wires = new Wire[wireEgids.Length];
for (uint i = 0; i < wireEgids.Length; i++)
{
wires[i] = new Wire(wireEgids[i]);
}
return wires;
}
/// <summary>
/// All wires connected to the output port.
/// These wires will always be wired output -> input.
/// </summary>
/// <param name="port">Port number.</param>
/// <returns>Wires connected to the output port.</returns>
public Wire[] ConnectedToOutput(byte port)
{
if (port >= OutputCount) throw new WiringException($"Port output {port} does not exist");
EGID[] wireEgids = SignalEngine.WiredToOutput(Id, port);
Wire[] wires = new Wire[wireEgids.Length];
for (uint i = 0; i < wireEgids.Length; i++)
{
wires[i] = new Wire(wireEgids[i]);
}
return wires;
} }
} }
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using RobocraftX.Blocks; using RobocraftX.Blocks;
using RobocraftX.Common;
using Gamecraft.CharacterVulnerability; using Gamecraft.CharacterVulnerability;
using Svelto.ECS; using Svelto.ECS;
using Unity.Mathematics; using Unity.Mathematics;
@ -12,43 +13,12 @@ namespace GamecraftModdingAPI.Blocks
{ {
public class SpawnPoint : Block public class SpawnPoint : Block
{ {
/// <summary>
/// Places a new spawn point.
/// Any valid spawn block type is accepted.
/// This re-implements Block.PlaceNew(...)
/// </summary>
public static new SpawnPoint PlaceNew(BlockIDs block, float3 position,
float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0,
int uscale = 1, float3 scale = default, Player player = null)
{
if (!(block == BlockIDs.LargeSpawn || block == BlockIDs.SmallSpawn || block == BlockIDs.MediumSpawn || block == BlockIDs.PlayerSpawn))
{
throw new BlockTypeException($"Block is not a {nameof(SpawnPoint)} block");
}
if (PlacementEngine.IsInGame && GameState.IsBuildMode())
{
EGID id = PlacementEngine.PlaceBlock(block, color, darkness,
position, uscale, scale, player, rotation);
return new SpawnPoint(id);
}
return null;
}
public SpawnPoint(EGID id) : base(id) public SpawnPoint(EGID id) : base(id)
{ {
if (!BlockEngine.GetBlockInfoExists<SpawnPointStatsEntityStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
public SpawnPoint(uint id) : base(id) public SpawnPoint(uint id) : base(new EGID(id, CommonExclusiveGroups.BUILD_SPAWNPOINT_BLOCK_GROUP))
{ {
if (!BlockEngine.GetBlockInfoExists<SpawnPointStatsEntityStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
// custom spawn point properties // custom spawn point properties
@ -58,15 +28,11 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public uint Lives public uint Lives
{ {
get get => BlockEngine.GetBlockInfo(this, (SpawnPointStatsEntityStruct st) => st.lives);
{
return BlockEngine.GetBlockInfo<SpawnPointStatsEntityStruct>(Id).lives;
}
set set
{ {
ref SpawnPointStatsEntityStruct spses = ref BlockEngine.GetBlockInfo<SpawnPointStatsEntityStruct>(Id); BlockEngine.SetBlockInfo(this, (ref SpawnPointStatsEntityStruct st, uint val) => st.lives = val, value);
spses.lives = value;
} }
} }
@ -75,15 +41,11 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public bool Damageable public bool Damageable
{ {
get get => BlockEngine.GetBlockInfo(this, (SpawnPointStatsEntityStruct st) => st.canTakeDamage);
{
return BlockEngine.GetBlockInfo<SpawnPointStatsEntityStruct>(Id).canTakeDamage;
}
set set
{ {
ref SpawnPointStatsEntityStruct spses = ref BlockEngine.GetBlockInfo<SpawnPointStatsEntityStruct>(Id); BlockEngine.SetBlockInfo(this, (ref SpawnPointStatsEntityStruct st, bool val) => st.canTakeDamage = val, value);
spses.canTakeDamage = value;
} }
} }
@ -92,15 +54,11 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public bool GameOverEnabled public bool GameOverEnabled
{ {
get get => BlockEngine.GetBlockInfo(this, (SpawnPointStatsEntityStruct st) => st.gameOverScreen);
{
return BlockEngine.GetBlockInfo<SpawnPointStatsEntityStruct>(Id).gameOverScreen;
}
set set
{ {
ref SpawnPointStatsEntityStruct spses = ref BlockEngine.GetBlockInfo<SpawnPointStatsEntityStruct>(Id); BlockEngine.SetBlockInfo(this, (ref SpawnPointStatsEntityStruct st, bool val) => st.gameOverScreen = val, value);
spses.gameOverScreen = value;
} }
} }
@ -109,15 +67,11 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public byte Team public byte Team
{ {
get get => BlockEngine.GetBlockInfo(this, (SpawnPointIdsEntityStruct st) => st.teamId);
{
return BlockEngine.GetBlockInfo<SpawnPointIdsEntityStruct>(Id).teamId;
}
set set
{ {
ref SpawnPointIdsEntityStruct spses = ref BlockEngine.GetBlockInfo<SpawnPointIdsEntityStruct>(Id); BlockEngine.SetBlockInfo(this, (ref SpawnPointIdsEntityStruct st, byte val) => st.teamId = val, value);
spses.teamId = value;
} }
} }
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using Gamecraft.Blocks.GUI; using Gamecraft.Blocks.GUI;
using RobocraftX.Common;
using Svelto.ECS; using Svelto.ECS;
using Unity.Mathematics; using Unity.Mathematics;
@ -9,37 +10,14 @@ using GamecraftModdingAPI.Utility;
namespace GamecraftModdingAPI.Blocks namespace GamecraftModdingAPI.Blocks
{ {
public class TextBlock : Block public class TextBlock : SignalingBlock
{ {
public static TextBlock PlaceNew(float3 position,
float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0,
int uscale = 1, float3 scale = default, Player player = null)
{
if (PlacementEngine.IsInGame && GameState.IsBuildMode())
{
EGID id = PlacementEngine.PlaceBlock(BlockIDs.TextBlock, color, darkness,
position, uscale, scale, player, rotation);
return new TextBlock(id);
}
return null;
}
public TextBlock(EGID id) : base(id) public TextBlock(EGID id) : base(id)
{ {
if (!BlockEngine.GetBlockInfoExists<TextBlockDataStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
public TextBlock(uint id) : base(id) public TextBlock(uint id) : base(new EGID(id, CommonExclusiveGroups.BUILD_TEXT_BLOCK_GROUP))
{ {
if (!BlockEngine.GetBlockInfoExists<TextBlockDataStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
// custom text block properties // custom text block properties
@ -49,17 +27,17 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public string Text public string Text
{ {
get get => BlockEngine.GetBlockInfo(this, (TextBlockDataStruct st) => st.textCurrent);
{
return BlockEngine.GetBlockInfo<TextBlockDataStruct>(Id).textCurrent;
}
set set
{ {
ref TextBlockDataStruct tbds = ref BlockEngine.GetBlockInfo<TextBlockDataStruct>(Id); BlockEngine.SetBlockInfo(this, (ref TextBlockDataStruct tbds, string val) =>
tbds.textCurrent.Set(value); {
tbds.textStored.Set(value); tbds.textCurrent.Set(val);
BlockEngine.GetBlockInfo<TextBlockNetworkDataStruct>(Id).newTextBlockStringContent.Set(value); tbds.textStored.Set(val);
}, value);
BlockEngine.SetBlockInfo(this,
(ref TextBlockNetworkDataStruct st, string val) => st.newTextBlockStringContent.Set(val), value);
} }
} }
@ -68,15 +46,14 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public string TextBlockId public string TextBlockId
{ {
get get => BlockEngine.GetBlockInfo(this, (TextBlockDataStruct st) => st.textBlockID);
{
return BlockEngine.GetBlockInfo<TextBlockDataStruct>(Id).textBlockID;
}
set set
{ {
BlockEngine.GetBlockInfo<TextBlockDataStruct>(Id).textBlockID.Set(value); BlockEngine.SetBlockInfo(this, (ref TextBlockDataStruct tbds, string val) =>
BlockEngine.GetBlockInfo<TextBlockNetworkDataStruct>(Id).newTextBlockID.Set(value); tbds.textBlockID.Set(val), value);
BlockEngine.SetBlockInfo(this,
(ref TextBlockNetworkDataStruct st, string val) => st.newTextBlockID.Set(val), value);
} }
} }
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using RobocraftX.Blocks; using RobocraftX.Blocks;
using RobocraftX.Common;
using Gamecraft.Blocks.TimerBlock; using Gamecraft.Blocks.TimerBlock;
using Svelto.ECS; using Svelto.ECS;
using Unity.Mathematics; using Unity.Mathematics;
@ -10,39 +11,14 @@ using GamecraftModdingAPI.Utility;
namespace GamecraftModdingAPI.Blocks namespace GamecraftModdingAPI.Blocks
{ {
public class Timer : Block public class Timer : SignalingBlock
{ {
/// <summary>
/// Places a new timer block.
/// </summary>
public static Timer PlaceNew(float3 position,
float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0,
int uscale = 1, float3 scale = default, Player player = null)
{
if (PlacementEngine.IsInGame && GameState.IsBuildMode())
{
EGID id = PlacementEngine.PlaceBlock(BlockIDs.Timer, color, darkness,
position, uscale, scale, player, rotation);
return new Timer(id);
}
return null;
}
public Timer(EGID id) : base(id) public Timer(EGID id) : base(id)
{ {
if (!BlockEngine.GetBlockInfoExists<TimerBlockDataStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
public Timer(uint id) : base(id) public Timer(uint id) : base(new EGID(id, CommonExclusiveGroups.BUILD_TIMER_BLOCK_GROUP))
{ {
if (!BlockEngine.GetBlockInfoExists<TimerBlockDataStruct>(this.Id))
{
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
}
} }
// custom timer properties // custom timer properties
@ -52,15 +28,12 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public float Start public float Start
{ {
get get => BlockEngine.GetBlockInfo(this, (TimerBlockDataStruct st) => st.startTime);
{
return BlockEngine.GetBlockInfo<TimerBlockDataStruct>(Id).startTime;
}
set set
{ {
ref TimerBlockDataStruct tbds = ref BlockEngine.GetBlockInfo<TimerBlockDataStruct>(Id); BlockEngine.SetBlockInfo(this, (ref TimerBlockDataStruct tbds, float val) => tbds.startTime = val,
tbds.startTime = value; value);
} }
} }
@ -69,15 +42,12 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public float End public float End
{ {
get get => BlockEngine.GetBlockInfo(this, (TimerBlockDataStruct st) => st.endTime);
{
return BlockEngine.GetBlockInfo<TimerBlockDataStruct>(Id).endTime;
}
set set
{ {
ref TimerBlockDataStruct tbds = ref BlockEngine.GetBlockInfo<TimerBlockDataStruct>(Id); BlockEngine.SetBlockInfo(this, (ref TimerBlockDataStruct tbds, float val) => tbds.endTime = val,
tbds.endTime = value; value);
} }
} }
@ -86,15 +56,12 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public bool DisplayMilliseconds public bool DisplayMilliseconds
{ {
get get => BlockEngine.GetBlockInfo(this, (TimerBlockDataStruct st) => st.outputFormatHasMS);
{
return BlockEngine.GetBlockInfo<TimerBlockDataStruct>(Id).outputFormatHasMS;
}
set set
{ {
ref TimerBlockDataStruct tbds = ref BlockEngine.GetBlockInfo<TimerBlockDataStruct>(Id); BlockEngine.SetBlockInfo(this, (ref TimerBlockDataStruct tbds, bool val) => tbds.outputFormatHasMS = val,
tbds.outputFormatHasMS = value; value);
} }
} }
@ -103,15 +70,12 @@ namespace GamecraftModdingAPI.Blocks
/// </summary> /// </summary>
public int CurrentTime public int CurrentTime
{ {
get get => BlockEngine.GetBlockInfo(this, (TimerBlockLabelCacheEntityStruct st) => st.timeLastRenderFrameMS);
{
return BlockEngine.GetBlockInfo<TimerBlockLabelCacheEntityStruct>(Id).timeLastRenderFrameMS;
}
set set
{ {
ref TimerBlockLabelCacheEntityStruct tblces = ref BlockEngine.GetBlockInfo<TimerBlockLabelCacheEntityStruct>(Id); BlockEngine.SetBlockInfo(this, (ref TimerBlockLabelCacheEntityStruct tbds, int val) => tbds.timeLastRenderFrameMS = val,
tblces.timeLastRenderFrameMS = value; value);
} }
} }
} }

View file

@ -0,0 +1,355 @@
using System;
using Gamecraft.Wires;
using Svelto.ECS;
using Svelto.ECS.Experimental;
using GamecraftModdingAPI.Utility;
namespace GamecraftModdingAPI.Blocks
{
public class Wire
{
internal static SignalEngine signalEngine;
protected EGID startPortEGID;
protected EGID endPortEGID;
protected EGID startBlockEGID;
protected EGID endBlockEGID;
protected EGID wireEGID;
protected bool inputToOutput;
protected byte startPort;
protected byte endPort;
public static Wire Connect(SignalingBlock start, byte startPort, SignalingBlock end, byte endPort)
{
WireEntityStruct wire = signalEngine.CreateNewWire(start.Id, startPort, end.Id, endPort);
return new Wire(wire, start, end);
}
/// <summary>
/// An existing wire connection ending at the specified input.
/// If multiple exist, this will return the first one found.
/// </summary>
/// <param name="end">Destination block.</param>
/// <param name="endPort">Port number.</param>
/// <returns>The wire, where the end of the wire is the block port specified, or null if does not exist.</returns>
public static Wire ConnectedToInputPort(SignalingBlock end, byte endPort)
{
EGID port = signalEngine.MatchBlockInputToPort(end, endPort, out bool exists);
if (!exists) return null;
WireEntityStruct wire = signalEngine.MatchPortToWire(port, end.Id, out exists);
if (exists)
{
return new Wire(new Block(wire.sourceBlockEGID), end, wire.sourcePortUsage, endPort);
}
return null;
}
/// <summary>
/// An existing wire connection starting at the specified output.
/// If multiple exist, this will return the first one found.
/// </summary>
/// <param name="start">Source block entity ID.</param>
/// <param name="startPort">Port number.</param>
/// <returns>The wire, where the start of the wire is the block port specified, or null if does not exist.</returns>
public static Wire ConnectedToOutputPort(SignalingBlock start, byte startPort)
{
EGID port = signalEngine.MatchBlockOutputToPort(start, startPort, out bool exists);
if (!exists) return null;
WireEntityStruct wire = signalEngine.MatchPortToWire(port, start.Id, out exists);
if (exists)
{
return new Wire(start, new Block(wire.destinationBlockEGID), startPort, wire.destinationPortUsage);
}
return null;
}
/// <summary>
/// Construct a wire object from an existing connection.
/// </summary>
/// <param name="start">Starting block ID.</param>
/// <param name="end">Ending block ID.</param>
/// <param name="startPort">Starting port number, or guess if omitted.</param>
/// <param name="endPort">Ending port number, or guess if omitted.</param>
/// <exception cref="WireInvalidException">Guessing failed or wire does not exist.</exception>
public Wire(Block start, Block end, byte startPort = Byte.MaxValue, byte endPort = Byte.MaxValue)
{
startBlockEGID = start.Id;
endBlockEGID = end.Id;
// find block ports
WireEntityStruct wire = signalEngine.MatchBlocksToWire(start.Id, end.Id, out bool exists, startPort, endPort);
if (exists)
{
wireEGID = wire.ID;
endPortEGID = signalEngine.MatchBlockInputToPort(end, wire.destinationPortUsage, out exists);
if (!exists) throw new WireInvalidException("Wire end port not found");
startPortEGID = signalEngine.MatchBlockOutputToPort(start, wire.sourcePortUsage, out exists);
if (!exists) throw new WireInvalidException("Wire start port not found");
inputToOutput = false;
endPort = wire.destinationPortUsage;
startPort = wire.sourcePortUsage;
}
else
{
// flip I/O around and try again
wire = signalEngine.MatchBlocksToWire(end.Id, start.Id, out exists, endPort, startPort);
if (exists)
{
wireEGID = wire.ID;
endPortEGID = signalEngine.MatchBlockOutputToPort(end, wire.sourcePortUsage, out exists);
if (!exists) throw new WireInvalidException("Wire end port not found");
startPortEGID = signalEngine.MatchBlockInputToPort(start, wire.destinationPortUsage, out exists);
if (!exists) throw new WireInvalidException("Wire start port not found");
inputToOutput = true; // end is actually the source
// NB: start and end are handled exactly as they're received as params.
// This makes wire traversal easier, but makes logic in this class a bit more complex
endPort = wire.sourcePortUsage;
startPort = wire.destinationPortUsage;
}
else
{
throw new WireInvalidException("Wire not found");
}
}
}
/// <summary>
/// Construct a wire object from an existing wire connection.
/// </summary>
/// <param name="start">Starting block ID.</param>
/// <param name="end">Ending block ID.</param>
/// <param name="startPort">Starting port number.</param>
/// <param name="endPort">Ending port number.</param>
/// <param name="wire">The wire ID.</param>
/// <param name="inputToOutput">Whether the wire direction goes input -> output (true) or output -> input (false, preferred).</param>
public Wire(Block start, Block end, byte startPort, byte endPort, EGID wire, bool inputToOutput)
{
this.startBlockEGID = start.Id;
this.endBlockEGID = end.Id;
this.inputToOutput = inputToOutput;
this.wireEGID = wire;
if (inputToOutput)
{
endPortEGID = signalEngine.MatchBlockOutputToPort(start, startPort, out bool exists);
if (!exists) throw new WireInvalidException("Wire end port not found");
startPortEGID = signalEngine.MatchBlockInputToPort(end, endPort, out exists);
if (!exists) throw new WireInvalidException("Wire start port not found");
}
else
{
endPortEGID = signalEngine.MatchBlockInputToPort(end, endPort, out bool exists);
if (!exists) throw new WireInvalidException("Wire end port not found");
startPortEGID = signalEngine.MatchBlockOutputToPort(start, startPort, out exists);
if (!exists) throw new WireInvalidException("Wire start port not found");
}
this.startPort = startPort;
this.endPort = endPort;
}
/// <summary>
/// Construct a wire object from an existing wire connection.
/// </summary>
/// <param name="wireEgid">The wire ID.</param>
public Wire(EGID wireEgid)
{
this.wireEGID = wireEgid;
WireEntityStruct wire = signalEngine.GetWire(wireEGID);
this.startBlockEGID = wire.sourceBlockEGID;
this.endBlockEGID = wire.destinationBlockEGID;
this.inputToOutput = false;
endPortEGID = signalEngine.MatchBlockInputToPort(wire.destinationBlockEGID, wire.destinationPortUsage, out bool exists);
if (!exists) throw new WireInvalidException("Wire end port not found");
startPortEGID = signalEngine.MatchBlockOutputToPort(wire.sourceBlockEGID, wire.sourcePortUsage, out exists);
if (!exists) throw new WireInvalidException("Wire start port not found");
this.endPort = wire.destinationPortUsage;
this.startPort = wire.sourcePortUsage;
}
internal Wire(WireEntityStruct wire, SignalingBlock src, SignalingBlock dest)
{
this.wireEGID = wire.ID;
this.startBlockEGID = wire.sourceBlockEGID;
this.endBlockEGID = wire.destinationBlockEGID;
inputToOutput = false;
endPortEGID = signalEngine.MatchBlockInputToPort(dest, wire.destinationPortUsage, out bool exists);
if (!exists) throw new WireInvalidException("Wire end port not found");
startPortEGID = signalEngine.MatchBlockOutputToPort(src, wire.sourcePortUsage, out exists);
if (!exists) throw new WireInvalidException("Wire start port not found");
this.endPort = wire.destinationPortUsage;
this.startPort = wire.sourcePortUsage;
}
/// <summary>
/// The wire's in-game id.
/// </summary>
public EGID Id
{
get => wireEGID;
}
/// <summary>
/// The wire's signal value, as a float.
/// </summary>
public float Float
{
get
{
ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
if (!exists) return 0f;
return cds.valueAsFloat;
}
set
{
ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
if (!exists) return;
cds.valueAsFloat = value;
}
}
/// <summary>
/// The wire's string signal.
/// </summary>
public string String
{
get
{
ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
if (!exists) return "";
return cds.valueAsEcsString;
}
set
{
ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
if (!exists) return;
cds.valueAsEcsString.Set(value);
}
}
/// <summary>
/// The wire's raw string signal.
/// </summary>
public ECSString ECSString
{
get
{
ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
if (!exists) return default;
return cds.valueAsEcsString;
}
set
{
ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
if (!exists) return;
cds.valueAsEcsString = value;
}
}
/// <summary>
/// The wire's signal id.
/// I'm 50% sure this is useless.
/// </summary>
public uint SignalId
{
get
{
ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
if (!exists) return uint.MaxValue;
return cds.valueAsID;
}
set
{
ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
if (!exists) return;
cds.valueAsID = value;
}
}
/// <summary>
/// The block at the beginning of the wire.
/// </summary>
public SignalingBlock Start
{
get => new SignalingBlock(startBlockEGID);
}
/// <summary>
/// The port number that the beginning of the wire connects to.
/// </summary>
public byte StartPort
{
get => startPort;
}
/// <summary>
/// The block at the end of the wire.
/// </summary>
public SignalingBlock End
{
get => new SignalingBlock(endBlockEGID);
}
/// <summary>
/// The port number that the end of the wire connects to.
/// </summary>
public byte EndPort
{
get => endPort;
}
/// <summary>
/// Create a copy of the wire object where the direction of the wire is guaranteed to be from a block output to a block input.
/// This is simply a different memory configuration and does not affect the in-game wire (which is always output -> input).
/// </summary>
/// <returns>A copy of the wire object.</returns>
public Wire OutputToInputCopy()
{
return new Wire(wireEGID);
}
/// <summary>
/// Convert the wire object to the direction the signal flows.
/// Signals on wires always flow from a block output port to a block input port.
/// This is simply a different memory configuration and does not affect the in-game wire (which is always output -> input).
/// </summary>
public void OutputToInputInPlace()
{
if (inputToOutput)
{
inputToOutput = false;
// swap inputs and outputs
EGID temp = endBlockEGID;
endBlockEGID = startBlockEGID;
startBlockEGID = temp;
temp = endPortEGID;
endPortEGID = startPortEGID;
startPortEGID = temp;
byte tempPortNumber = endPort;
endPort = startPort;
startPort = tempPortNumber;
}
}
public override string ToString()
{
if (signalEngine.Exists<WireEntityStruct>(wireEGID))
{
return $"{nameof(Id)}: {Id}, Start{nameof(Start.Id)}: {Start.Id}, End{nameof(End.Id)}: {End.Id}, ({Start.Type}::{StartPort} aka {Start.PortName(StartPort, inputToOutput)}) -> ({End.Type}::{EndPort} aka {End.PortName(EndPort, !inputToOutput)})";
}
return $"{nameof(Id)}: {Id}, Start{nameof(Start.Id)}: {Start.Id}, End{nameof(End.Id)}: {End.Id}, ({Start.Type}::{StartPort} -> {End.Type}::{EndPort})";
}
internal static void Init() { }
}
}

View file

@ -0,0 +1,41 @@
using Gamecraft.Damage;
using RobocraftX.Common;
using Svelto.ECS;
namespace GamecraftModdingAPI
{
/// <summary>
/// Represnts a cluster of blocks in time running mode, meaning blocks that are connected either directly or via joints.
/// </summary>
public class Cluster
{
public EGID Id { get; }
public Cluster(EGID id)
{
Id = id;
}
public Cluster(uint id) : this(new EGID(id, CommonExclusiveGroups.SIMULATION_CLUSTERS_GROUP))
{
}
public float InitialHealth
{
get => Block.BlockEngine.GetBlockInfo<HealthEntityComponent>(Id).initialHealth;
set => Block.BlockEngine.GetBlockInfo<HealthEntityComponent>(Id).initialHealth = value;
}
public float CurrentHealth
{
get => Block.BlockEngine.GetBlockInfo<HealthEntityComponent>(Id).currentHealth;
set => Block.BlockEngine.GetBlockInfo<HealthEntityComponent>(Id).currentHealth = value;
}
public float HealthMultiplier
{
get => Block.BlockEngine.GetBlockInfo<HealthEntityComponent>(Id).healthMultiplier;
set => Block.BlockEngine.GetBlockInfo<HealthEntityComponent>(Id).healthMultiplier = value;
}
}
}

View file

@ -18,6 +18,7 @@ namespace GamecraftModdingAPI.Events
/// Patch of RobocraftX.StateSync.DeterministicStepCompositionRoot.ComposeEnginesGroups(...) /// Patch of RobocraftX.StateSync.DeterministicStepCompositionRoot.ComposeEnginesGroups(...)
/// </summary> /// </summary>
//[HarmonyPatch(typeof(DeterministicStepCompositionRoot), "DeterministicCompose")] //[HarmonyPatch(typeof(DeterministicStepCompositionRoot), "DeterministicCompose")]
[Obsolete]
[HarmonyPatch] [HarmonyPatch]
class GameHostTransitionDeterministicGroupEnginePatch class GameHostTransitionDeterministicGroupEnginePatch
{ {

View file

@ -4,6 +4,7 @@ using Svelto.ECS;
namespace GamecraftModdingAPI.Events namespace GamecraftModdingAPI.Events
{ {
[Obsolete]
public class EmitterBuilder public class EmitterBuilder
{ {
private string name; private string name;

View file

@ -11,6 +11,7 @@ namespace GamecraftModdingAPI.Events
/// <summary> /// <summary>
/// Convenient factories for mod event engines /// Convenient factories for mod event engines
/// </summary> /// </summary>
[Obsolete]
public static class EventEngineFactory public static class EventEngineFactory
{ {
/// <summary> /// <summary>

View file

@ -14,6 +14,7 @@ namespace GamecraftModdingAPI.Events
/// Keeps track of event handlers and emitters. /// Keeps track of event handlers and emitters.
/// This is used to add, remove and get API event handlers and emitters. /// This is used to add, remove and get API event handlers and emitters.
/// </summary> /// </summary>
[Obsolete("This will be removed in an upcoming update. Use the new C# event architecture from GamecraftModdingAPI.App")]
public static class EventManager public static class EventManager
{ {
private static Dictionary<string, IEventEmitterEngine> _eventEmitters = new Dictionary<string, IEventEmitterEngine>(); private static Dictionary<string, IEventEmitterEngine> _eventEmitters = new Dictionary<string, IEventEmitterEngine>();

View file

@ -17,6 +17,7 @@ namespace GamecraftModdingAPI.Events
/// <summary> /// <summary>
/// Patch of RobocraftX.FullGameCompositionRoot.ActivateGame() /// Patch of RobocraftX.FullGameCompositionRoot.ActivateGame()
/// </summary> /// </summary>
[Obsolete]
[HarmonyPatch] [HarmonyPatch]
class GameActivatedComposePatch class GameActivatedComposePatch
{ {

View file

@ -14,6 +14,7 @@ namespace GamecraftModdingAPI.Events
/// <summary> /// <summary>
/// Patch of RobocraftX.FullGameCompositionRoot.ReloadGame() /// Patch of RobocraftX.FullGameCompositionRoot.ReloadGame()
/// </summary> /// </summary>
[Obsolete]
[HarmonyPatch(typeof(FullGameCompositionRoot), "ReloadGame")] [HarmonyPatch(typeof(FullGameCompositionRoot), "ReloadGame")]
class GameReloadedPatch class GameReloadedPatch
{ {

View file

@ -12,6 +12,7 @@ namespace GamecraftModdingAPI.Events
/// <summary> /// <summary>
/// Event emitter engine for switching to to build mode. /// Event emitter engine for switching to to build mode.
/// </summary> /// </summary>
[Obsolete]
public class GameStateBuildEmitterEngine : IEventEmitterEngine, IUnorderedInitializeOnTimeStoppedModeEntered public class GameStateBuildEmitterEngine : IEventEmitterEngine, IUnorderedInitializeOnTimeStoppedModeEntered
{ {
public string Name { get; } = "GamecraftModdingAPIGameStateBuildEventEmitter" ; public string Name { get; } = "GamecraftModdingAPIGameStateBuildEventEmitter" ;
@ -43,10 +44,10 @@ namespace GamecraftModdingAPI.Events
} }
} }
public JobHandle OnInitializeTimeStoppedMode() public JobHandle OnInitializeTimeStoppedMode(JobHandle inputDeps)
{ {
Emit(); Emit();
return default(JobHandle); return inputDeps;
} }
public void Ready() { } public void Ready() { }

View file

@ -12,6 +12,7 @@ namespace GamecraftModdingAPI.Events
/// <summary> /// <summary>
/// Event emitter engine for switching to simulation mode. /// Event emitter engine for switching to simulation mode.
/// </summary> /// </summary>
[Obsolete]
public class GameStateSimulationEmitterEngine : IEventEmitterEngine, IUnorderedInitializeOnTimeRunningModeEntered public class GameStateSimulationEmitterEngine : IEventEmitterEngine, IUnorderedInitializeOnTimeRunningModeEntered
{ {
public string Name { get; } = "GamecraftModdingAPIGameStateSimulationEventEmitter" ; public string Name { get; } = "GamecraftModdingAPIGameStateSimulationEventEmitter" ;
@ -42,10 +43,10 @@ namespace GamecraftModdingAPI.Events
} }
} }
public JobHandle OnInitializeTimeRunningMode() public JobHandle OnInitializeTimeRunningMode(JobHandle inputDeps)
{ {
Emit(); Emit();
return default(JobHandle); return inputDeps;
} }
public void Ready() { } public void Ready() { }

View file

@ -18,6 +18,7 @@ namespace GamecraftModdingAPI.Events
/// Patch of RobocraftX.FullGameCompositionRoot.ActivateGame() /// Patch of RobocraftX.FullGameCompositionRoot.ActivateGame()
/// (scheduled for execution during RobocraftX.FullGameCompositionRoot.SwitchToGame()) /// (scheduled for execution during RobocraftX.FullGameCompositionRoot.SwitchToGame())
/// </summary> /// </summary>
[Obsolete]
[HarmonyPatch(typeof(FullGameCompositionRoot), "SwitchToGame")] [HarmonyPatch(typeof(FullGameCompositionRoot), "SwitchToGame")]
class GameSwitchedToPatch class GameSwitchedToPatch
{ {

View file

@ -4,6 +4,7 @@ using Svelto.ECS;
namespace GamecraftModdingAPI.Events namespace GamecraftModdingAPI.Events
{ {
[Obsolete]
public class HandlerBuilder public class HandlerBuilder
{ {
private string name; private string name;

View file

@ -13,6 +13,7 @@ namespace GamecraftModdingAPI.Events
/// <summary> /// <summary>
/// Engine interface to create a ModEventEntityStruct in entitiesDB when a specific event occurs. /// Engine interface to create a ModEventEntityStruct in entitiesDB when a specific event occurs.
/// </summary> /// </summary>
[Obsolete]
public interface IEventEmitterEngine : IFactoryEngine public interface IEventEmitterEngine : IFactoryEngine
{ {
/// <summary> /// <summary>

View file

@ -14,6 +14,7 @@ namespace GamecraftModdingAPI.Events
/// <summary> /// <summary>
/// Engine interface to handle ModEventEntityStruct events emitted by IEventEmitterEngines. /// Engine interface to handle ModEventEntityStruct events emitted by IEventEmitterEngines.
/// </summary> /// </summary>
[Obsolete]
public interface IEventHandlerEngine : IReactionaryEngine<ModEventEntityStruct> public interface IEventHandlerEngine : IReactionaryEngine<ModEventEntityStruct>
{ {
} }

View file

@ -15,6 +15,7 @@ namespace GamecraftModdingAPI.Events
/// <summary> /// <summary>
/// Patch of RobocraftX.FullGameCompositionRoot.ActivateMenu() /// Patch of RobocraftX.FullGameCompositionRoot.ActivateMenu()
/// </summary> /// </summary>
[Obsolete]
[HarmonyPatch(typeof(FullGameCompositionRoot), "ActivateMenu")] [HarmonyPatch(typeof(FullGameCompositionRoot), "ActivateMenu")]
class MenuActivatedPatch class MenuActivatedPatch
{ {
@ -31,6 +32,7 @@ namespace GamecraftModdingAPI.Events
{ {
firstLoad = false; firstLoad = false;
FullGameFields.Init(__instance); FullGameFields.Init(__instance);
//Application.Application.SetFullGameCompositionRoot(__instance);
Logging.Log("Dispatching App Init event"); Logging.Log("Dispatching App Init event");
EventManager.GetEventEmitter("GamecraftModdingAPIApplicationInitializedEventEmitter").Emit(); EventManager.GetEventEmitter("GamecraftModdingAPIApplicationInitializedEventEmitter").Emit();
} }

View file

@ -15,6 +15,7 @@ namespace GamecraftModdingAPI.Events
/// <summary> /// <summary>
/// Patch of RobocraftX.FullGameCompositionRoot.SwitchToMenu() /// Patch of RobocraftX.FullGameCompositionRoot.SwitchToMenu()
/// </summary> /// </summary>
[Obsolete]
[HarmonyPatch(typeof(FullGameCompositionRoot), "SwitchToMenu")] [HarmonyPatch(typeof(FullGameCompositionRoot), "SwitchToMenu")]
class MenuSwitchedToPatch class MenuSwitchedToPatch
{ {

View file

@ -12,6 +12,7 @@ namespace GamecraftModdingAPI.Events
/// <summary> /// <summary>
/// A simple implementation of IEventEmitterEngine sufficient for most uses /// A simple implementation of IEventEmitterEngine sufficient for most uses
/// </summary> /// </summary>
[Obsolete]
public class SimpleEventEmitterEngine : IEventEmitterEngine public class SimpleEventEmitterEngine : IEventEmitterEngine
{ {
public string Name { get; set; } public string Name { get; set; }

View file

@ -13,6 +13,7 @@ namespace GamecraftModdingAPI.Events
/// <summary> /// <summary>
/// A simple implementation of IEventHandlerEngine sufficient for most uses /// A simple implementation of IEventHandlerEngine sufficient for most uses
/// </summary> /// </summary>
[Obsolete]
public class SimpleEventHandlerEngine : IEventHandlerEngine public class SimpleEventHandlerEngine : IEventHandlerEngine
{ {
public int type { get; set; } public int type { get; set; }
@ -20,6 +21,10 @@ namespace GamecraftModdingAPI.Events
private bool isActivated = false; private bool isActivated = false;
private bool jankActivateFix = false;
private bool jankDestroyFix = false;
private readonly Action<EntitiesDB> onActivated; private readonly Action<EntitiesDB> onActivated;
private readonly Action<EntitiesDB> onDestroyed; private readonly Action<EntitiesDB> onDestroyed;
@ -32,6 +37,8 @@ namespace GamecraftModdingAPI.Events
{ {
if (entityView.type.Equals(this.type)) if (entityView.type.Equals(this.type))
{ {
jankActivateFix = !jankActivateFix;
if (jankActivateFix) return;
isActivated = true; isActivated = true;
onActivatedInvokeCatchError(entitiesDB); onActivatedInvokeCatchError(entitiesDB);
} }
@ -51,12 +58,19 @@ namespace GamecraftModdingAPI.Events
} }
} }
public void Deactivate()
{
isActivated = false;
}
public void Ready() { } public void Ready() { }
public void Remove(ref ModEventEntityStruct entityView, EGID egid) public void Remove(ref ModEventEntityStruct entityView, EGID egid)
{ {
if (entityView.type.Equals(this.type) && isActivated) if (entityView.type.Equals(this.type) && isActivated)
{ {
jankDestroyFix = !jankDestroyFix;
if (jankDestroyFix) return;
isActivated = false; isActivated = false;
onDestroyedInvokeCatchError(entitiesDB); onDestroyedInvokeCatchError(entitiesDB);
} }

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Reflection; using System.Reflection;
using GamecraftModdingAPI.Blocks;
using HarmonyLib; using HarmonyLib;
using GamecraftModdingAPI.Utility; using GamecraftModdingAPI.Utility;
@ -49,6 +49,7 @@ namespace GamecraftModdingAPI
harmony.PatchAll(currentAssembly); harmony.PatchAll(currentAssembly);
// init utility // init utility
Logging.MetaDebugLog($"Initializing Utility"); Logging.MetaDebugLog($"Initializing Utility");
#pragma warning disable 0612,0618
Utility.GameState.Init(); Utility.GameState.Init();
Utility.VersionTracking.Init(); Utility.VersionTracking.Init();
// create default event emitters // create default event emitters
@ -61,6 +62,7 @@ namespace GamecraftModdingAPI
EventManager.AddEventEmitter(new SimpleEventEmitterEngine(EventType.GameSwitchedTo, "GamecraftModdingAPIGameSwitchedToEventEmitter", false)); EventManager.AddEventEmitter(new SimpleEventEmitterEngine(EventType.GameSwitchedTo, "GamecraftModdingAPIGameSwitchedToEventEmitter", false));
EventManager.AddEventEmitter(GameHostTransitionDeterministicGroupEnginePatch.buildEngine); EventManager.AddEventEmitter(GameHostTransitionDeterministicGroupEnginePatch.buildEngine);
EventManager.AddEventEmitter(GameHostTransitionDeterministicGroupEnginePatch.simEngine); EventManager.AddEventEmitter(GameHostTransitionDeterministicGroupEnginePatch.simEngine);
#pragma warning restore 0612,0618
// init block implementors // init block implementors
Logging.MetaDebugLog($"Initializing Blocks"); Logging.MetaDebugLog($"Initializing Blocks");
// init inventory // init inventory
@ -70,8 +72,11 @@ namespace GamecraftModdingAPI
// init object-oriented classes // init object-oriented classes
Player.Init(); Player.Init();
Block.Init(); Block.Init();
Wire.Init();
GameClient.Init(); GameClient.Init();
AsyncUtils.Init(); AsyncUtils.Init();
GamecraftModdingAPI.App.Client.Init();
GamecraftModdingAPI.App.Game.Init();
Logging.MetaLog($"{currentAssembly.GetName().Name} v{currentAssembly.GetName().Version} initialized"); Logging.MetaLog($"{currentAssembly.GetName().Name} v{currentAssembly.GetName().Version} initialized");
} }

View file

@ -12,12 +12,6 @@ namespace GamecraftModdingAPI.Persistence
/// </summary> /// </summary>
public interface IEntitySerializer : IDeserializationFactory, IQueryingEntitiesEngine public interface IEntitySerializer : IDeserializationFactory, IQueryingEntitiesEngine
{ {
/// <summary>
/// The entity factory used for creating entities and entity components.
/// </summary>
/// <value>The entity factory.</value>
IEntityFactory EntityFactory { set; }
/// <summary> /// <summary>
/// Serialize the entities. /// Serialize the entities.
/// </summary> /// </summary>

View file

@ -29,7 +29,6 @@ namespace GamecraftModdingAPI.Persistence
_registrations[name] = (IEntitySerialization ies) => { ies.RegisterSerializationFactory<T>(serializer); }; _registrations[name] = (IEntitySerialization ies) => { ies.RegisterSerializationFactory<T>(serializer); };
if (_lastEnginesRoot != null) if (_lastEnginesRoot != null)
{ {
serializer.EntityFactory = _lastEnginesRoot.GenerateEntityFactory();
_registrations[name].Invoke(_lastEnginesRoot.GenerateEntitySerializer()); _registrations[name].Invoke(_lastEnginesRoot.GenerateEntitySerializer());
_lastEnginesRoot.AddEngine(serializer); _lastEnginesRoot.AddEngine(serializer);
} }
@ -63,12 +62,10 @@ namespace GamecraftModdingAPI.Persistence
public static void RegisterSerializers(EnginesRoot enginesRoot) public static void RegisterSerializers(EnginesRoot enginesRoot)
{ {
_lastEnginesRoot = enginesRoot; _lastEnginesRoot = enginesRoot;
IEntityFactory factory = enginesRoot.GenerateEntityFactory();
IEntitySerialization ies = enginesRoot.GenerateEntitySerializer(); IEntitySerialization ies = enginesRoot.GenerateEntitySerializer();
foreach (string key in _serializers.Keys) foreach (string key in _serializers.Keys)
{ {
Logging.MetaDebugLog($"Registering IEntitySerializer for {key}"); Logging.MetaDebugLog($"Registering IEntitySerializer for {key}");
_serializers[key].EntityFactory = factory;
_registrations[key].Invoke(ies); _registrations[key].Invoke(ies);
enginesRoot.AddEngine(_serializers[key]); enginesRoot.AddEngine(_serializers[key]);
} }

View file

@ -21,13 +21,11 @@ namespace GamecraftModdingAPI.Persistence
protected int serializationType; protected int serializationType;
public IEntityFactory EntityFactory { set; protected get; }
public EntitiesDB entitiesDB { set; protected get; } public EntitiesDB entitiesDB { set; protected get; }
public EntityComponentInitializer BuildDeserializedEntity(EGID egid, ISerializationData serializationData, ISerializableEntityDescriptor entityDescriptor, int serializationType, IEntitySerialization entitySerialization) public EntityComponentInitializer BuildDeserializedEntity(EGID egid, ISerializationData serializationData, ISerializableEntityDescriptor entityDescriptor, int serializationType, IEntitySerialization entitySerialization, IEntityFactory factory, bool enginesRootIsDeserializationOnly)
{ {
EntityComponentInitializer esi = EntityFactory.BuildEntity<Descriptor>(egid); EntityComponentInitializer esi = factory.BuildEntity<Descriptor>(egid);
entitySerialization.DeserializeEntityComponents(serializationData, entityDescriptor, ref esi, serializationType); entitySerialization.DeserializeEntityComponents(serializationData, entityDescriptor, ref esi, serializationType);
return esi; return esi;
} }

View file

@ -45,6 +45,15 @@ namespace GamecraftModdingAPI
return playerEngine.ExistsById(player); return playerEngine.ExistsById(player);
} }
/// <summary>
/// The amount of Players in the current game.
/// </summary>
/// <returns>The count.</returns>
public static uint Count()
{
return (uint) playerEngine.GetAllPlayerCount();
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="T:GamecraftModdingAPI.Player"/> class. /// Initializes a new instance of the <see cref="T:GamecraftModdingAPI.Player"/> class.
/// </summary> /// </summary>
@ -348,7 +357,7 @@ namespace GamecraftModdingAPI
public Block GetBlockLookedAt(float maxDistance = -1f) public Block GetBlockLookedAt(float maxDistance = -1f)
{ {
var egid = playerEngine.GetThingLookedAt(Id, maxDistance); var egid = playerEngine.GetThingLookedAt(Id, maxDistance);
return egid.HasValue && egid.Value.groupID == CommonExclusiveGroups.OWNED_BLOCKS_GROUP return egid.HasValue && egid.Value.groupID != CommonExclusiveGroups.SIMULATION_BODIES_GROUP
? new Block(egid.Value) ? new Block(egid.Value)
: null; : null;
} }
@ -404,7 +413,7 @@ namespace GamecraftModdingAPI
// internal methods // internal methods
public static void Init() internal static void Init()
{ {
Utility.GameEngineManager.AddGameEngine(playerEngine); Utility.GameEngineManager.AddGameEngine(playerEngine);
} }

View file

@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using RobocraftX.Character; using RobocraftX.Character;
@ -10,14 +12,16 @@ using RobocraftX.Physics;
using RobocraftX.Blocks.Ghost; using RobocraftX.Blocks.Ghost;
using RobocraftX.Character.Camera; using RobocraftX.Character.Camera;
using RobocraftX.Character.Factories; using RobocraftX.Character.Factories;
using Gamecraft.CharacterVulnerability; using Gamecraft.GUI.HUDFeedbackBlocks;
using Gamecraft.CharacterVulnerability.Entities;
using Svelto.ECS; using Svelto.ECS;
using Unity.Mathematics; using Unity.Mathematics;
using Unity.Physics; using Unity.Physics;
using UnityEngine; using UnityEngine;
using GamecraftModdingAPI.Engines; using GamecraftModdingAPI.Engines;
using HarmonyLib;
using RobocraftX.Common;
using Svelto.ECS.DataStructures;
namespace GamecraftModdingAPI.Players namespace GamecraftModdingAPI.Players
{ {
@ -65,14 +69,39 @@ namespace GamecraftModdingAPI.Players
return uint.MaxValue; return uint.MaxValue;
} }
public long GetAllPlayerCount()
{
if (entitiesDB == null) return 0;
long count = 0;
foreach (ExclusiveGroupStruct eg in PlayersExclusiveGroups.AllPlayers)
{
count += entitiesDB.Count<PlayerIDStruct>(eg);
}
return count;
}
public long GetLocalPlayerCount()
{
if (entitiesDB == null) return 0;
return entitiesDB.Count<PlayerIDStruct>(PlayersExclusiveGroups.LocalPlayers);
}
public long GetRemotePlayerCount()
{
if (entitiesDB == null) return 0;
return entitiesDB.Count<PlayerIDStruct>(PlayersExclusiveGroups.RemotePlayers);
}
public bool ExistsById(uint playerId) public bool ExistsById(uint playerId)
{ {
if (entitiesDB == null) return false;
return entitiesDB.Exists<PlayerIDStruct>(playerId, PlayersExclusiveGroups.LocalPlayers) return entitiesDB.Exists<PlayerIDStruct>(playerId, PlayersExclusiveGroups.LocalPlayers)
|| entitiesDB.Exists<PlayerIDStruct>(playerId, PlayersExclusiveGroups.RemotePlayers); || entitiesDB.Exists<PlayerIDStruct>(playerId, PlayersExclusiveGroups.RemotePlayers);
} }
public float3 GetLocation(uint playerId) public float3 GetLocation(uint playerId)
{ {
if (entitiesDB == null) return float3.zero;
ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists); ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -83,6 +112,7 @@ namespace GamecraftModdingAPI.Players
public bool SetLocation(uint playerId, float3 location, bool exitSeat = true) public bool SetLocation(uint playerId, float3 location, bool exitSeat = true)
{ {
if (entitiesDB == null) return false;
var characterGroups = CharacterExclusiveGroups.AllCharacters; var characterGroups = CharacterExclusiveGroups.AllCharacters;
for (int i = 0; i < characterGroups.count; i++) for (int i = 0; i < characterGroups.count; i++)
{ {
@ -104,16 +134,18 @@ namespace GamecraftModdingAPI.Players
public float3 GetRotation(uint playerId) public float3 GetRotation(uint playerId)
{ {
if (entitiesDB == null) return float3.zero;
ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists); ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
return ((Quaternion) rbes.rotation).eulerAngles; return ((Quaternion) rbes.rotation).eulerAngles;
} }
return default; return default(float3);
} }
public bool SetRotation(uint playerId, float3 value) public bool SetRotation(uint playerId, float3 value)
{ {
if (entitiesDB == null) return false;
ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists); ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -127,6 +159,7 @@ namespace GamecraftModdingAPI.Players
public float3 GetLinearVelocity(uint playerId) public float3 GetLinearVelocity(uint playerId)
{ {
if (entitiesDB == null) return float3.zero;
ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists); ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -137,6 +170,7 @@ namespace GamecraftModdingAPI.Players
public bool SetLinearVelocity(uint playerId, float3 value) public bool SetLinearVelocity(uint playerId, float3 value)
{ {
if (entitiesDB == null) return false;
ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists); ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -148,6 +182,7 @@ namespace GamecraftModdingAPI.Players
public float3 GetAngularVelocity(uint playerId) public float3 GetAngularVelocity(uint playerId)
{ {
if (entitiesDB == null) return float3.zero;
ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists); ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -158,6 +193,7 @@ namespace GamecraftModdingAPI.Players
public bool SetAngularVelocity(uint playerId, float3 value) public bool SetAngularVelocity(uint playerId, float3 value)
{ {
if (entitiesDB == null) return false;
ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists); ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -169,16 +205,18 @@ namespace GamecraftModdingAPI.Players
public PhysicsMass GetMass(uint playerId) public PhysicsMass GetMass(uint playerId)
{ {
if (entitiesDB == null) return default(PhysicsMass);
ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists); ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
return rbes.physicsMass; return rbes.physicsMass;
} }
return default; return default(PhysicsMass);
} }
public bool SetInverseMass(uint playerId, float inverseMass) public bool SetInverseMass(uint playerId, float inverseMass)
{ {
if (entitiesDB == null) return false;
ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists); ref var rbes = ref GetCharacterStruct<RigidBodyEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -190,6 +228,7 @@ namespace GamecraftModdingAPI.Players
public float? GetLastPingTime(uint playerId, PlayerType type) public float? GetLastPingTime(uint playerId, PlayerType type)
{ {
if (entitiesDB == null) return null;
EGID egid = new EGID(playerId, PlayerGroupFromEnum(type)); EGID egid = new EGID(playerId, PlayerGroupFromEnum(type));
if (entitiesDB.Exists<PlayerNetworkStatsEntityStruct>(egid)) if (entitiesDB.Exists<PlayerNetworkStatsEntityStruct>(egid))
{ {
@ -200,6 +239,7 @@ namespace GamecraftModdingAPI.Players
public float GetInitialHealth(uint playerId) public float GetInitialHealth(uint playerId)
{ {
if (entitiesDB == null) return 0;
ref var c = ref GetCharacterStruct<CharacterHealthEntityStruct>(playerId, out bool exists); ref var c = ref GetCharacterStruct<CharacterHealthEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -210,6 +250,7 @@ namespace GamecraftModdingAPI.Players
public bool SetInitialHealth(uint playerId, float val) public bool SetInitialHealth(uint playerId, float val)
{ {
if (entitiesDB == null) return false;
ref var c = ref GetCharacterStruct<CharacterHealthEntityStruct>(playerId, out bool exists); ref var c = ref GetCharacterStruct<CharacterHealthEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -221,6 +262,7 @@ namespace GamecraftModdingAPI.Players
public float GetCurrentHealth(uint playerId) public float GetCurrentHealth(uint playerId)
{ {
if (entitiesDB == null) return 0;
ref var c = ref GetCharacterStruct<CharacterHealthEntityStruct>(playerId, out bool exists); ref var c = ref GetCharacterStruct<CharacterHealthEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -231,6 +273,7 @@ namespace GamecraftModdingAPI.Players
public bool SetCurrentHealth(uint playerId, float val) public bool SetCurrentHealth(uint playerId, float val)
{ {
if (entitiesDB == null) return false;
ref var c = ref GetCharacterStruct<CharacterHealthEntityStruct>(playerId, out bool exists); ref var c = ref GetCharacterStruct<CharacterHealthEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -242,18 +285,13 @@ namespace GamecraftModdingAPI.Players
public bool DamagePlayer(uint playerId, float amount) public bool DamagePlayer(uint playerId, float amount)
{ {
Factory.BuildEntity<DamageEntityDescriptor>( if (entitiesDB == null) return false;
new EGID(CharacterVulnerabilityExclusiveGroups.NextDamageEntityId, CharacterVulnerabilityExclusiveGroups.CharacterDamageExclusiveGroup) return SetCurrentHealth(playerId, GetCurrentHealth(playerId) - amount);
).Init(new DamageEntityStruct
{
damage = amount,
targetPlayerEntityId = playerId,
});
return true;
} }
public bool GetDamageable(uint playerId) public bool GetDamageable(uint playerId)
{ {
if (entitiesDB == null) return false;
ref var c = ref GetCharacterStruct<CharacterHealthEntityStruct>(playerId, out bool exists); ref var c = ref GetCharacterStruct<CharacterHealthEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -264,6 +302,7 @@ namespace GamecraftModdingAPI.Players
public bool SetDamageable(uint playerId, bool val) public bool SetDamageable(uint playerId, bool val)
{ {
if (entitiesDB == null) return false;
ref var ches = ref GetCharacterStruct<CharacterHealthEntityStruct>(playerId, out bool exists); ref var ches = ref GetCharacterStruct<CharacterHealthEntityStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -276,6 +315,7 @@ namespace GamecraftModdingAPI.Players
public uint GetInitialLives(uint playerId) public uint GetInitialLives(uint playerId)
{ {
if (entitiesDB == null) return 0;
ref var c = ref GetCharacterStruct<CharacterLivesEntityComponent>(playerId, out bool exists); ref var c = ref GetCharacterStruct<CharacterLivesEntityComponent>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -286,6 +326,7 @@ namespace GamecraftModdingAPI.Players
public bool SetInitialLives(uint playerId, uint val) public bool SetInitialLives(uint playerId, uint val)
{ {
if (entitiesDB == null) return false;
ref var c = ref GetCharacterStruct<CharacterLivesEntityComponent>(playerId, out bool exists); ref var c = ref GetCharacterStruct<CharacterLivesEntityComponent>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -297,6 +338,7 @@ namespace GamecraftModdingAPI.Players
public uint GetCurrentLives(uint playerId) public uint GetCurrentLives(uint playerId)
{ {
if (entitiesDB == null) return 0;
ref var c = ref GetCharacterStruct<CharacterLivesEntityComponent>(playerId, out bool exists); ref var c = ref GetCharacterStruct<CharacterLivesEntityComponent>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -307,6 +349,7 @@ namespace GamecraftModdingAPI.Players
public bool SetCurrentLives(uint playerId, uint val) public bool SetCurrentLives(uint playerId, uint val)
{ {
if (entitiesDB == null) return false;
ref var c = ref GetCharacterStruct<CharacterLivesEntityComponent>(playerId, out bool exists); ref var c = ref GetCharacterStruct<CharacterLivesEntityComponent>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -318,21 +361,21 @@ namespace GamecraftModdingAPI.Players
public bool GetGameOverScreen(uint playerId) public bool GetGameOverScreen(uint playerId)
{ {
ref var c = ref GetCharacterStruct<CharacterLivesEntityComponent>(playerId, out bool exists); if (entitiesDB == null) return false;
if (exists) ref HudActivatedBlocksEntityStruct habes = ref entitiesDB.QueryEntity<HudActivatedBlocksEntityStruct>(HUDFeedbackBlocksGUIExclusiveGroups.GameOverHudEgid);
{ NativeDynamicArrayCast<EGID> nativeDynamicArrayCast = new NativeDynamicArrayCast<EGID>(habes.activatedBlocksOrdered);
return c.gameOverScreen; return nativeDynamicArrayCast.count > 0;
}
return false;
} }
public bool IsDead(uint playerId) public bool IsDead(uint playerId)
{ {
if (entitiesDB == null) return true;
return entitiesDB.Exists<RigidBodyEntityStruct>(playerId, CharacterExclusiveGroups.DeadCharacters); return entitiesDB.Exists<RigidBodyEntityStruct>(playerId, CharacterExclusiveGroups.DeadCharacters);
} }
public int GetSelectedBlock(uint playerId) public int GetSelectedBlock(uint playerId)
{ {
if (entitiesDB == null) return 0;
ref var c = ref GetCharacterStruct<EquippedPartStruct>(playerId, out bool exists); ref var c = ref GetCharacterStruct<EquippedPartStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {
@ -343,6 +386,7 @@ namespace GamecraftModdingAPI.Players
public byte GetSelectedColor(uint playerId) public byte GetSelectedColor(uint playerId)
{ {
if (entitiesDB == null) return 0;
ref var c = ref GetCharacterStruct<EquippedColourStruct>(playerId, out bool exists); ref var c = ref GetCharacterStruct<EquippedColourStruct>(playerId, out bool exists);
if (exists) if (exists)
{ {

View file

@ -0,0 +1,42 @@
using System;
using Unity.Mathematics;
using GamecraftModdingAPI;
using GamecraftModdingAPI.Tests;
namespace GamecraftModdingAPI.Players
{
#if TEST
/// <summary>
/// Player test cases. Not accessible in release versions.
/// </summary>
[APITestClass]
public static class PlayerTests
{
[APITestCase(TestType.EditMode)]
public static void ExistsTest()
{
if (!Assert.Equal(Player.Exists(PlayerType.Local), true, "Local player does not exist.", "Local player detected.")) return;
Assert.Equal(Player.Count(), 1u, "Player.Count() is not one, possibly because it failed silently.", "Player count is one for single player game.");
}
[APITestCase(TestType.EditMode)]
public static void PositionTest()
{
Player p = new Player(PlayerType.Local);
if (!Assert.Errorless(() => { p.Teleport(0, 0, 0, relative: false); }, "Player.Teleport(origin) errored: ", "Player teleported to origin successfully.")) return;
if (!Assert.CloseTo(p.Position, float3.zero, "Player is not close to origin despite being teleported there.", "Player.Position is at origin.")) return;
if (!Assert.Errorless(() => { p.Position = float3.zero + 1; }, "Player.Position = origin+1 errored: ", "Player moved to origin+1.")) return;
Assert.CloseTo(p.Position, float3.zero + 1, "Player is not close to origin+1 despite being teleported there.", "Player.Position is at origin+1.");
}
[APITestCase(TestType.Menu)]
public static void InvalidStateTest()
{
if (!Assert.Errorless(() => { Player.Count(); }, "Player.Count() errored in menu.", "Player.Count() succeeded in menu.")) return;
Assert.Equal(Player.Count(), 0u, "Player.Count() is not zero in menu.", "Player count is zero in menu as expected.");
}
}
#endif
}

View file

@ -3,18 +3,27 @@ using Svelto.ECS;
using Unity.Mathematics; using Unity.Mathematics;
using UnityEngine; using UnityEngine;
using Gamecraft.Damage;
using RobocraftX.Common; using RobocraftX.Common;
using RobocraftX.Physics; using RobocraftX.Physics;
namespace GamecraftModdingAPI namespace GamecraftModdingAPI
{ {
/// <summary> /// <summary>
/// A rigid body (like a cluster of connected blocks) during simulation. /// A rigid body (like a chunk of connected blocks) during simulation.
/// </summary> /// </summary>
public class SimBody : IEquatable<SimBody>, IEquatable<EGID> public class SimBody : IEquatable<SimBody>, IEquatable<EGID>
{ {
public EGID Id { get; } public EGID Id { get; }
/// <summary>
/// The cluster this chunk belongs to, or null if the chunk doesn't exist. Get the SimBody from a Block if possible for good performance here.
/// </summary>
public Cluster Cluster => cluster ?? (cluster = clusterId == uint.MaxValue ? Block.BlockEngine.GetCluster(Id.entityID) : new Cluster(clusterId));
private Cluster cluster;
private uint clusterId;
public SimBody(EGID id) public SimBody(EGID id)
{ {
Id = id; Id = id;
@ -24,6 +33,11 @@ namespace GamecraftModdingAPI
{ {
} }
internal SimBody(uint id, uint clusterID) : this(id)
{
clusterId = clusterID;
}
/// <summary> /// <summary>
/// The position of this body. When setting the position, update the position of the connected bodies as well, /// The position of this body. When setting the position, update the position of the connected bodies as well,
/// otherwise unexpected forces may arise. /// otherwise unexpected forces may arise.
@ -70,6 +84,29 @@ namespace GamecraftModdingAPI
//set => GetStruct().physicsMass.CenterOfMass = value; //set => GetStruct().physicsMass.CenterOfMass = value;
} }
public float Volume
{
get => GetStruct().volume;
}
public float InitialHealth
{
get => Block.BlockEngine.GetBlockInfo<HealthEntityComponent>(Id).initialHealth;
set => Block.BlockEngine.GetBlockInfo<HealthEntityComponent>(Id).initialHealth = value;
}
public float CurrentHealth
{
get => Block.BlockEngine.GetBlockInfo<HealthEntityComponent>(Id).currentHealth;
set => Block.BlockEngine.GetBlockInfo<HealthEntityComponent>(Id).currentHealth = value;
}
public float HealthMultiplier
{
get => Block.BlockEngine.GetBlockInfo<HealthEntityComponent>(Id).healthMultiplier;
set => Block.BlockEngine.GetBlockInfo<HealthEntityComponent>(Id).healthMultiplier = value;
}
/// <summary> /// <summary>
/// Whether the body can be moved or static. /// Whether the body can be moved or static.
/// </summary> /// </summary>

View file

@ -0,0 +1,65 @@
using System;
namespace GamecraftModdingAPI.Tests
{
/// <summary>
/// Test type.
/// When provided to APITestCaseAttribute, this dictates when the test case is called.
/// </summary>
public enum TestType
{
Menu,
Game,
SimulationMode,
EditMode,
}
/// <summary>
/// API Test Class attribute.
/// Classes without this attribute will be ignored when searching for test cases.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class APITestClassAttribute : Attribute
{
internal string Name;
public APITestClassAttribute(string name = "")
{
this.Name = name;
}
}
/// <summary>
/// API Test Case attribute.
/// Static methods with this attribute will be called when the API test system is running.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class APITestCaseAttribute : Attribute
{
internal TestType TestType;
public APITestCaseAttribute(TestType testType)
{
this.TestType = testType;
}
}
/// <summary>
/// API Test StartUp attribute.
/// Static methods with this attribute will be called before any test case is run by the API test system.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class APITestStartUpAttribute : Attribute
{
}
/// <summary>
/// API Test TearDown attribute.
/// Static methods with this attribute will be called after all API test system test cases have completed (failed or succeeded).
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class APITestTearDownAttribute : Attribute
{
}
}

View file

@ -0,0 +1,220 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Runtime.CompilerServices;
using Unity.Mathematics;
namespace GamecraftModdingAPI.Tests
{
/// <summary>
/// API test system assertion utilities.
/// </summary>
public static class Assert
{
private static StreamWriter logFile = null;
private static ConcurrentDictionary<string, string> callbacks = new ConcurrentDictionary<string, string>();
private const string PASS = "SUCCESS: ";
private const string FAIL = "FAILURE: ";
private const string WARN = "WARNING: ";
private const string INFO = "DEBUG: ";
/// <summary>
/// Log a message to the test log.
/// </summary>
/// <param name="msg">Message.</param>
/// <param name="end">Message ending.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Log(string msg, string end = "\n")
{
if (logFile == null) openTestLog();
logFile.Write(msg + end);
logFile.Flush();
}
/// <summary>
/// Asserts that the event receives a callback... eventually.
/// Add the eventhandler returned by this method to the relevant event.
/// This does not assert that the callback happens under that event's intended circumstances.
/// Add another event handler to assert specific circumstance requirements.
/// </summary>
/// <returns>The callback event handler.</returns>
/// <param name="eventName">Event name.</param>
/// <param name="eventMsg">Event error message.</param>
/// <typeparam name="T">The event handler callback argument object.</typeparam>
public static EventHandler<T> CallsBack<T>(string eventName, string eventMsg = null)
{
if (eventMsg == null) eventMsg = $"expected callback to {eventName} but it never occurred...";
callbacks[eventName] = eventMsg;
return (sender, args) =>
{
string value = null;
if (!callbacks.TryRemove(eventName, out value)) { Log(WARN + $"callback to {eventName} occurred again or a related error occurred... (Received '{args.ToString()}' from '{(sender == null ? (string)sender : sender.ToString())}')"); }
Log(PASS + $"callback to {eventName} occurred... (Received '{args.ToString()}' from '{(sender == null ? (string)sender : sender.ToString())}')");
TestRoot.TestsPassed = true;
};
}
public static bool NotNull<T>(T obj, string err = null, string success = null)
{
if (err == null) err = $"{nameof(T)} object was null.";
if (success == null) success = $"{nameof(T)} '{obj}' not null";
if (obj == null)
{
Log(FAIL + err);
TestRoot.TestsPassed = false;
return false;
}
else
{
Log(PASS + success);
TestRoot.TestsPassed = true;
return true;
}
}
public static bool Equal<T>(T obj1, T obj2, string err = null, string success = null)
{
if (err == null) err = $"{nameof(T)} '{obj1}' is not equal to '{obj2}'.";
if (success == null) success = $"{nameof(T)} '{obj1}' is equal to '{obj2}'.";
if ((obj1 == null && obj2 == null)
|| (obj1 != null && obj2 != null && obj1.Equals(obj2) && obj2.Equals(obj1)))
{
// pass
Log(PASS + success);
TestRoot.TestsPassed = true;
return true;
}
else
{
// fail
Log(FAIL + err);
TestRoot.TestsPassed = false;
return false;
}
}
public static bool Errorless(Action tryThis, string err = null, string success = null)
{
if (err == null) err = $"{tryThis} raised an exception: ";
if (success == null) success = $"{tryThis} completed without raising an exception.";
try
{
tryThis();
}
catch (Exception e)
{
Log(FAIL + err + e);
TestRoot.TestsPassed = false;
return false;
}
TestRoot.TestsPassed = true;
Log(PASS + success);
return true;
}
public static bool CloseTo(float a, float b, string err = null, string success = null, float delta = float.Epsilon)
{
if (err == null) err = $"{a} is not within {delta} of {b}.";
if (success == null) success = $"{a} is close enough to {b}.";
if (Math.Abs(a - b) > delta)
{
Log(FAIL + err);
TestRoot.TestsPassed = false;
return false;
}
else
{
TestRoot.TestsPassed = true;
Log(PASS + success);
return true;
}
}
public static bool CloseTo(double a, double b, string err = null, string success = null, double delta = double.Epsilon)
{
if (err == null) err = $"{a} is not within {delta} of {b}.";
if (success == null) success = $"{a} is close enough to {b}.";
if (Math.Abs(a - b) > delta)
{
Log(FAIL + err);
TestRoot.TestsPassed = false;
return false;
}
else
{
TestRoot.TestsPassed = true;
Log(PASS + success);
return true;
}
}
public static bool CloseTo(float3 a, float3 b, string err = null, string success = null, float delta = float.Epsilon)
{
if (err == null) err = $"{a} is not within {delta} of {b} in every direction.";
if (success == null) success = $"{a} is close enough to {b}.";
bool xClose = CloseTo(a.x, b.x, err, success, delta);
bool yClose = CloseTo(a.y, b.y, err, success, delta);
bool zClose = CloseTo(a.z, b.z, err, success, delta);
if (xClose && yClose && zClose)
{
//TestRoot.TestsPassed = true;
//Log(PASS + success);
return true;
}
else
{
//Log(FAIL + err);
//TestRoot.TestsPassed = false;
return false;
}
}
public static void Fail(string msg = null)
{
if (msg == null) msg = $"Manual test failure with no message provided.";
Log(FAIL + msg);
TestRoot.TestsPassed = false;
}
public static void Pass(string msg = null)
{
if (msg == null) msg = $"Manual test pass with no message provided.";
Log(PASS + msg);
TestRoot.TestsPassed = true;
}
public static void Warn(string msg = null)
{
if (msg == null) msg = $"Manual test warning with no message provided.";
Log(WARN + msg);
TestRoot.TestsPassed = true;
}
internal static void CallsComplete()
{
foreach(string key in callbacks.Keys)
{
Log(FAIL + callbacks[key]);
TestRoot.TestsPassed = false;
}
}
internal static void CloseLog()
{
if (logFile != null) logFile.Close();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void openTestLog()
{
logFile = File.CreateText(TestRoot.ReportFile);
}
}
}

View file

@ -25,33 +25,29 @@ using GamecraftModdingAPI.Players;
namespace GamecraftModdingAPI.Tests namespace GamecraftModdingAPI.Tests
{ {
#if DEBUG
// unused by design // unused by design
/// <summary> /// <summary>
/// Modding API implemented as a standalone IPA Plugin. /// Modding API implemented as a standalone IPA Plugin.
/// Ideally, GamecraftModdingAPI should be loaded by another mod; not itself /// Ideally, GamecraftModdingAPI should be loaded by another mod; not itself
/// </summary> /// </summary>
public class GamecraftModdingAPIPluginTest public class GamecraftModdingAPIPluginTest : IllusionPlugin.IEnhancedPlugin
#if DEBUG
: IllusionPlugin.IEnhancedPlugin
#endif
{ {
private static Harmony harmony { get; set; } private static Harmony harmony { get; set; }
public string[] Filter { get; } = new string[] { "Gamecraft", "GamecraftPreview" }; public override string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name;
public string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name; public override string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString();
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString();
public string HarmonyID { get; } = "org.git.exmods.modtainers.gamecraftmoddingapi"; public string HarmonyID { get; } = "org.git.exmods.modtainers.gamecraftmoddingapi";
public void OnApplicationQuit() public override void OnApplicationQuit()
{ {
GamecraftModdingAPI.Main.Shutdown(); GamecraftModdingAPI.Main.Shutdown();
} }
public void OnApplicationStart() public override void OnApplicationStart()
{ {
FileLog.Reset(); FileLog.Reset();
Harmony.DEBUG = true; Harmony.DEBUG = true;
@ -65,12 +61,13 @@ namespace GamecraftModdingAPI.Tests
// disable some Gamecraft analytics // disable some Gamecraft analytics
//AnalyticsDisablerPatch.DisableAnalytics = true; //AnalyticsDisablerPatch.DisableAnalytics = true;
// disable background music // disable background music
Logging.MetaDebugLog("Audio Mixers: "+string.Join(",", AudioTools.GetMixers())); Logging.MetaDebugLog("Audio Mixers: " + string.Join(",", AudioTools.GetMixers()));
//AudioTools.SetVolume(0.0f, "Music"); // The game now sets this from settings again after this is called :( //AudioTools.SetVolume(0.0f, "Music"); // The game now sets this from settings again after this is called :(
//Utility.VersionTracking.Enable();//(very) unstable //Utility.VersionTracking.Enable();//(very) unstable
// debug/test handlers // debug/test handlers
#pragma warning disable 0612
HandlerBuilder.Builder() HandlerBuilder.Builder()
.Name("appinit API debug") .Name("appinit API debug")
.Handle(EventType.ApplicationInitialized) .Handle(EventType.ApplicationInitialized)
@ -118,6 +115,14 @@ namespace GamecraftModdingAPI.Tests
.Handle(EventType.Menu) .Handle(EventType.Menu)
.OnActivation(() => { throw new Exception("Event Handler always throws an exception!"); }) .OnActivation(() => { throw new Exception("Event Handler always throws an exception!"); })
.Build(); .Build();
#pragma warning restore 0612
/*HandlerBuilder.Builder("enter game from menu test")
.Handle(EventType.Menu)
.OnActivation(() =>
{
Tasks.Scheduler.Schedule(new Tasks.Repeatable(enterGame, shouldRetry, 0.2f));
})
.Build();*/
// debug/test commands // debug/test commands
if (Dependency.Hell("ExtraCommands")) if (Dependency.Hell("ExtraCommands"))
@ -207,7 +212,7 @@ namespace GamecraftModdingAPI.Tests
return; return;
} }
new Player(PlayerType.Local).GetBlockLookedAt().Color = new Player(PlayerType.Local).GetBlockLookedAt().Color =
new BlockColor {Color = color}; new BlockColor { Color = color };
Logging.CommandLog("Colored block to " + color); Logging.CommandLog("Colored block to " + color);
}).Build(); }).Build();
@ -227,6 +232,51 @@ namespace GamecraftModdingAPI.Tests
} }
}).Build(); }).Build();
CommandBuilder.Builder()
.Name("PlaceConsole")
.Description("Place a bunch of console block with a given text - entering simulation with them crashes the game as the cmd doesn't exist")
.Action((float x, float y, float z) =>
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 100; j++)
{
var block = Block.PlaceNew<ConsoleBlock>(BlockIDs.ConsoleBlock,
new float3(x + i, y, z + j));
block.Command = "test_command";
}
}
sw.Stop();
Logging.CommandLog($"Blocks placed in {sw.ElapsedMilliseconds} ms");
})
.Build();
CommandBuilder.Builder()
.Name("WireTest")
.Description("Place two blocks and then wire them together")
.Action(() =>
{
LogicGate notBlock = Block.PlaceNew<LogicGate>(BlockIDs.NOTLogicBlock, new float3(1, 2, 0));
LogicGate andBlock = Block.PlaceNew<LogicGate>(BlockIDs.ANDLogicBlock, new float3(2, 2, 0));
// connect NOT Gate output to AND Gate input #2 (ports are zero-indexed, so 1 is 2nd position and 0 is 1st position)
Wire conn = notBlock.Connect(0, andBlock, 1);
Logging.CommandLog(conn.ToString());
})
.Build();
CommandBuilder.Builder("TestChunkHealth", "Sets the chunk looked at to the given health.")
.Action((float val, float max) =>
{
var body = new Player(PlayerType.Local).GetSimBodyLookedAt();
if (body == null) return;
body.CurrentHealth = val;
body.InitialHealth = max;
Logging.CommandLog("Health set to: " + val);
}).Build();
GameClient.SetDebugInfo("InstalledMods", InstalledMods); GameClient.SetDebugInfo("InstalledMods", InstalledMods);
Block.Placed += (sender, args) => Block.Placed += (sender, args) =>
Logging.MetaDebugLog("Placed block " + args.Type + " with ID " + args.ID); Logging.MetaDebugLog("Placed block " + args.Type + " with ID " + args.ID);
@ -302,6 +352,10 @@ namespace GamecraftModdingAPI.Tests
o1.Merge(o2, new JsonMergeSettings {MergeArrayHandling = MergeArrayHandling.Union}); o1.Merge(o2, new JsonMergeSettings {MergeArrayHandling = MergeArrayHandling.Union});
File.WriteAllText(@"Gamecraft_Data\StreamingAssets\aa\Windows\catalog.json", o1.ToString());*/ File.WriteAllText(@"Gamecraft_Data\StreamingAssets\aa\Windows\catalog.json", o1.ToString());*/
CustomBlock.Prep().RunOn(ExtraLean.UIScheduler); CustomBlock.Prep().RunOn(ExtraLean.UIScheduler);
#if TEST
TestRoot.RunTests();
#endif
} }
private string modsString; private string modsString;
@ -314,15 +368,39 @@ namespace GamecraftModdingAPI.Tests
return modsString = sb.ToString(); return modsString = sb.ToString();
} }
public void OnFixedUpdate() { } private bool retry = true;
public void OnLateUpdate() { } private bool shouldRetry()
{
return retry;
}
public void OnLevelWasInitialized(int level) { } private void enterGame()
{
public void OnLevelWasLoaded(int level) { } App.Client app = new App.Client();
App.Game[] myGames = app.MyGames;
public void OnUpdate() { } Logging.MetaDebugLog($"MyGames count {myGames.Length}");
if (myGames.Length != 0)
{
Logging.MetaDebugLog($"MyGames[0] EGID {myGames[0].EGID}");
retry = false;
try
{
//myGames[0].Description = "test msg pls ignore"; // make sure game exists first
Logging.MetaDebugLog($"Entering game {myGames[0].Name}");
myGames[0].EnterGame();
}
catch (Exception e)
{
Logging.MetaDebugLog($"Failed to enter game; exception: {e}");
retry = true;
}
}
else
{
Logging.MetaDebugLog("MyGames not populated yet :(");
}
}
[HarmonyPatch] [HarmonyPatch]
public class MinimumSpecsPatch public class MinimumSpecsPatch
@ -338,4 +416,5 @@ namespace GamecraftModdingAPI.Tests
} }
} }
} }
#endif
} }

View file

@ -0,0 +1,295 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Linq; // welcome to the dark side
using Svelto.Tasks;
using Svelto.Tasks.Lean;
using Svelto.Tasks.Enumerators;
using UnityEngine;
using GamecraftModdingAPI.App;
using GamecraftModdingAPI.Tasks;
using GamecraftModdingAPI.Utility;
namespace GamecraftModdingAPI.Tests
{
/// <summary>
/// API test system root class.
/// </summary>
public static class TestRoot
{
public static bool AutoShutdown = true;
public const string ReportFile = "GamecraftModdingAPI_tests.log";
private static bool _testsPassed = false;
private static uint _testsCount = 0;
private static uint _testsCountPassed = 0;
private static uint _testsCountFailed = 0;
private static string state = "StartingUp";
private static Stopwatch timer;
private static List<Type> testTypes = null;
public static bool TestsPassed
{
get => _testsPassed;
set
{
_testsPassed = _testsPassed && value;
_testsCount++;
if (value)
{
_testsCountPassed++;
}
else
{
_testsCountFailed++;
}
}
}
private static void StartUp()
{
// init
timer = Stopwatch.StartNew();
_testsPassed = true;
_testsCount = 0;
_testsCountPassed = 0;
_testsCountFailed = 0;
// flow control
Game.Enter += (sender, args) => { GameTests().RunOn(RobocraftX.Schedulers.Lean.EveryFrameStepRunner_RUNS_IN_TIME_STOPPED_AND_RUNNING); };
Game.Exit += (s, a) => state = "ReturningFromGame";
Client.EnterMenu += (sender, args) =>
{
if (state == "EnteringMenu")
{
MenuTests().RunOn(Scheduler.leanRunner);
state = "EnteringGame";
}
if (state == "ReturningFromGame")
{
TearDown().RunOn(Scheduler.leanRunner);
state = "ShuttingDown";
}
};
// init tests here
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
if (m.GetCustomAttribute<APITestStartUpAttribute>() != null)
{
try
{
m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Fail($"Start up method '{m}' raised an exception: {e.ToString()}");
}
}
}
}
state = "EnteringMenu";
}
private static IEnumerator<TaskContract> MenuTests()
{
yield return Yield.It;
// menu tests
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>();
if (a != null && a.TestType == TestType.Menu)
{
try
{
m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Fail($"Menu test '{m}' raised an exception: {e.ToString()}");
}
yield return Yield.It;
}
}
}
// load game
yield return GoToGameTests().Continue();
}
private static IEnumerator<TaskContract> GoToGameTests()
{
Client app = new Client();
int oldLength = 0;
while (app.MyGames.Length == 0 || oldLength != app.MyGames.Length)
{
oldLength = app.MyGames.Length;
yield return new WaitForSecondsEnumerator(1).Continue();
}
yield return Yield.It;
app.MyGames[0].EnterGame();
/*Game newGame = Game.NewGame();
yield return new WaitForSecondsEnumerator(5).Continue(); // wait for sync
newGame.EnterGame();*/
}
private static IEnumerator<TaskContract> GameTests()
{
yield return Yield.It;
Game currentGame = Game.CurrentGame();
// in-game tests
yield return new WaitForSecondsEnumerator(5).Continue(); // wait for game to finish loading
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>();
if (a != null && a.TestType == TestType.Game)
{
try
{
m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Fail($"Game test '{m}' raised an exception: {e.ToString()}");
}
yield return Yield.It;
}
}
}
currentGame.ToggleTimeMode();
yield return new WaitForSecondsEnumerator(5).Continue();
// simulation tests
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>();
if (a != null && a.TestType == TestType.SimulationMode)
{
try
{
m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Fail($"Simulation test '{m}' raised an exception: {e.ToString()}");
}
yield return Yield.It;
}
}
}
currentGame.ToggleTimeMode();
yield return new WaitForSecondsEnumerator(5).Continue();
// build tests
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>();
if (a != null && a.TestType == TestType.EditMode)
{
try
{
m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Fail($"Build test '{m}' raised an exception: {e.ToString()}");
}
yield return Yield.It;
}
}
}
// exit game
yield return new WaitForSecondsEnumerator(5).Continue();
yield return ReturnToMenu().Continue();
}
private static IEnumerator<TaskContract> ReturnToMenu()
{
Logging.MetaLog("Returning to main menu");
yield return Yield.It;
Game.CurrentGame().ExitGame();
}
private static IEnumerator<TaskContract> TearDown()
{
yield return new WaitForSecondsEnumerator(5).Continue();
Logging.MetaLog("Tearing down test run");
// dispose tests here
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
if (m.GetCustomAttribute<APITestTearDownAttribute>() != null)
{
try
{
m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Warn($"Tear down method '{m}' raised an exception: {e.ToString()}");
}
yield return Yield.It;
}
}
}
// finish up
Assert.CallsComplete();
timer.Stop();
string verdict = _testsPassed ? "--- PASSED :) ---" : "--- FAILED :( ---";
Assert.Log($"VERDICT: {verdict} ({_testsCountPassed}/{_testsCountFailed}/{_testsCount} P/F/T in {timer.ElapsedMilliseconds}ms)");
yield return Yield.It;
// end game
Logging.MetaLog("Completed test run: " + verdict);
yield return Yield.It;
Assert.CloseLog();
if (AutoShutdown) Application.Quit();
}
private static void FindTests(Assembly asm)
{
testTypes = new List<Type>();
foreach (Type t in asm.GetTypes())
{
if (t.GetCustomAttribute<APITestClassAttribute>() != null)
{
testTypes.Add(t);
}
}
}
/// <summary>
/// Runs the tests.
/// </summary>
/// <param name="asm">Assembly to search for tests. When set to null, this uses the GamecraftModdingAPI assembly. </param>
public static void RunTests(Assembly asm = null)
{
if (asm == null) asm = Assembly.GetExecutingAssembly();
FindTests(asm);
Logging.MetaLog("Starting test run");
// log metadata
Assert.Log($"Unity {Application.unityVersion}");
Assert.Log($"Gamecraft {Application.version}");
Assert.Log($"GamecraftModdingAPI {Assembly.GetExecutingAssembly().GetName().Version}");
Assert.Log($"Testing {asm.GetName().Name} {asm.GetName().Version}");
Assert.Log($"START: --- {DateTime.Now.ToString()} --- ({testTypes.Count} tests classes detected)");
StartUp();
Logging.MetaLog("Test StartUp complete");
}
}
}

View file

@ -0,0 +1,52 @@
using System;
using System.Reflection;
using HarmonyLib;
namespace GamecraftModdingAPI.Tests
{
#if TEST
/// <summary>
/// Test test test.
/// </summary>
[APITestClass]
public static class TestTest
{
public static event EventHandler<TestEventArgs> StartUp;
public static event EventHandler<TestEventArgs> Test;
public static event EventHandler<TestEventArgs> TearDown;
[APITestStartUp]
public static void Init()
{
StartUp += Assert.CallsBack<TestEventArgs>("TestStartUp");
Test += Assert.CallsBack<TestEventArgs>("TestCase");
TearDown += Assert.CallsBack<TestEventArgs>("TestTearDown");
StartUp(null, default(TestEventArgs));
}
[APITestCase(TestType.Menu)]
public static void RunTest()
{
Test(null, default(TestEventArgs));
}
[APITestTearDown]
public static void End()
{
TearDown(null, default(TestEventArgs));
}
}
public struct TestEventArgs
{
public override string ToString()
{
return "TestEventArgs{}";
}
}
#endif
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Reflection.Emit; using System.Reflection.Emit;
using System.Text;
using System.Text.Formatting; using System.Text.Formatting;
using GamecraftModdingAPI.Blocks; using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Engines; using GamecraftModdingAPI.Engines;
@ -46,9 +47,9 @@ namespace GamecraftModdingAPI.Utility
var array = new CodeInstruction[] var array = new CodeInstruction[]
{ {
new CodeInstruction(OpCodes.Ldloc_0), //StringBuffer new CodeInstruction(OpCodes.Ldloc_0), //StringBuffer
new CodeInstruction(OpCodes.Call, ((Action<StringBuffer>)AddInfo).Method) new CodeInstruction(OpCodes.Call, ((Action<StringBuilder>)AddInfo).Method)
}; };
list.InsertRange(index, array); list.InsertRange(index - 1, array); //-1: ldloc.1 ("local") before ldfld
} }
catch (Exception e) catch (Exception e)
{ {
@ -58,13 +59,15 @@ namespace GamecraftModdingAPI.Utility
return list; return list;
} }
public static void AddInfo(StringBuffer sb) public static void AddInfo(StringBuilder sb)
{ {
foreach (var info in _extraInfo) foreach (var info in _extraInfo)
{ {
try try
{ {
sb.Append(info.Value() + "\n"); string text = info.Value().Trim();
if (text.Length != 0)
sb.Append(text + "\n");
} }
catch (Exception e) catch (Exception e)
{ {

View file

@ -1,25 +0,0 @@
using System;
using RobocraftX.StateSync;
using Svelto.ECS;
using HarmonyLib;
namespace GamecraftModdingAPI.Utility
{
[HarmonyPatch(typeof(DeterministicStepCompositionRoot), "ResetWorld")]
public static class DeterministicStepCompositionRootPatch
{
private static SimpleEntitiesSubmissionScheduler engineRootScheduler;
public static void Postfix(SimpleEntitiesSubmissionScheduler scheduler)
{
engineRootScheduler = scheduler;
}
internal static void SubmitEntitiesNow()
{
if (engineRootScheduler != null)
engineRootScheduler.SubmitEntities();
}
}
}

View file

@ -25,8 +25,15 @@ namespace GamecraftModdingAPI.Utility
/// </summary> /// </summary>
public static class FullGameFields public static class FullGameFields
{ {
public static FullGameCompositionRoot Instance
{
private set;
get;
} = null;
public static MultiplayerInitParameters _multiplayerParams public static MultiplayerInitParameters _multiplayerParams
{ get {
get
{ {
return (MultiplayerInitParameters)fgcr?.Field("_multiplayerParams").GetValue(); return (MultiplayerInitParameters)fgcr?.Field("_multiplayerParams").GetValue();
} }
@ -157,6 +164,7 @@ namespace GamecraftModdingAPI.Utility
public static void Init(FullGameCompositionRoot instance) public static void Init(FullGameCompositionRoot instance)
{ {
fgcr = new Traverse(instance); fgcr = new Traverse(instance);
FullGameFields.Instance = instance;
} }
} }
} }

View file

@ -14,6 +14,7 @@ namespace GamecraftModdingAPI.Utility
/// Tracks the API version the current game was built for. /// Tracks the API version the current game was built for.
/// For compatibility reasons, this must be enabled before it will work. /// For compatibility reasons, this must be enabled before it will work.
/// </summary> /// </summary>
[Obsolete]
public static class VersionTracking public static class VersionTracking
{ {
private static readonly VersionTrackingEngine versionEngine = new VersionTrackingEngine(); private static readonly VersionTrackingEngine versionEngine = new VersionTrackingEngine();
@ -58,6 +59,7 @@ namespace GamecraftModdingAPI.Utility
} }
[Obsolete]
internal class VersionTrackingEngine : IEventEmitterEngine internal class VersionTrackingEngine : IEventEmitterEngine
{ {
public string Name { get; } = "GamecraftModdingAPIVersionTrackingGameEngine"; public string Name { get; } = "GamecraftModdingAPIVersionTrackingGameEngine";
@ -94,11 +96,13 @@ namespace GamecraftModdingAPI.Utility
public void Emit() { } public void Emit() { }
} }
[Obsolete]
public struct ModVersionStruct : IEntityComponent public struct ModVersionStruct : IEntityComponent
{ {
public uint version; public uint version;
} }
[Obsolete]
public class ModVersionDescriptor: SerializableEntityDescriptor<ModVersionDescriptor._ModVersionDescriptor> public class ModVersionDescriptor: SerializableEntityDescriptor<ModVersionDescriptor._ModVersionDescriptor>
{ {
[HashName("GamecraftModdingAPIVersionV0")] [HashName("GamecraftModdingAPIVersionV0")]

View file

@ -10,10 +10,16 @@ This means your code won't break when the GamecraftModdingAPI or Gamecraft updat
For more info, please check out the [official documentation](https://mod.exmods.org). For more info, please check out the [official documentation](https://mod.exmods.org).
For more support, join the ExMods [Discord](https://discord.gg/xjnFxQV). For more support, join the ExMods [Discord](https://discord.exmods.org).
## Installation ## Installation
[Please follow the official mod installation guide](https://www.exmods.org/guides/install.html) [Please follow the official mod installation guide](https://www.exmods.org/guides/install.html) or use GCMM.
## Development
To get started, create a symbolic link called `ref` in the root of the project, or one folder higher, linking to the Gamecraft install folder.
This will allow your IDE to resolve references to Gamecraft files for building and IDE tools.
GamecraftModdingAPI version numbers follow the [Semantic Versioning guidelines](https://semver.org/).
## External Libraries ## External Libraries
GamecraftModdingAPI includes [Harmony](https://github.com/pardeike/Harmony) to modify the behaviour of existing Gamecraft code. GamecraftModdingAPI includes [Harmony](https://github.com/pardeike/Harmony) to modify the behaviour of existing Gamecraft code.
@ -21,4 +27,5 @@ GamecraftModdingAPI includes [Harmony](https://github.com/pardeike/Harmony) to m
# Disclaimer # Disclaimer
This API is an unofficial modification of Gamecraft software, and is not endorsed or supported by FreeJam or Gamecraft. This API is an unofficial modification of Gamecraft software, and is not endorsed or supported by FreeJam or Gamecraft.
The GamecraftModdingAPI developer(s) claim no rights on the Gamecraft code referenced within this project. The GamecraftModdingAPI developer(s) claim no rights on the Gamecraft code referenced within this project.
All code contained in this project is licensed under the [GNU Public License v3](https://git.exmods.org/modtainers/GamecraftModdingAPI/src/branch/master/LICENSE).

View file

@ -38,7 +38,7 @@ PROJECT_NAME = "GamecraftModdingAPI"
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = "v1.2.0" PROJECT_NUMBER = "v1.5.0"
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a