Merge branch 'preview'

# Conflicts:
#	Automation/gen_csproj.py
#	TechbloxModdingAPI/TechbloxModdingAPI.csproj
This commit is contained in:
Norbi Peti 2022-01-29 20:53:07 +01:00
commit 09d3c5e81c
43 changed files with 623 additions and 317 deletions

View file

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using HarmonyLib;
using Svelto.Tasks;
namespace TechbloxModdingAPI.App
{
public static class AntiAntiCheatPatch
{
private delegate bool AntiAnticheatDelegate(ref object __result);
private delegate bool AntiAnticheatDelegateBool(ref bool __result);
private delegate bool AntiAnticheatDelegateTask(ref IEnumerator<TaskContract> __result);
public static void Init(Harmony harmony)
{
var type = AccessTools.TypeByName("Techblox.Services.Eos.Anticheat.Client.Services.AnticheatClientService");
harmony.Patch(type.GetConstructors()[0], new HarmonyMethod(((Func<bool>) AntiAntiCheat).Method));
harmony.Patch(AccessTools.Method(type, "Shutdown"), new HarmonyMethod(((Func<bool>) AntiAntiCheat).Method));
harmony.Patch(AccessTools.Method(type, "StartProtectedSession"), new HarmonyMethod(((AntiAnticheatDelegate) AntiAntiCheat).Method));
harmony.Patch(AccessTools.Method(type, "StopProtectedSession"), new HarmonyMethod(((AntiAnticheatDelegateBool) AntiAntiCheat).Method));
harmony.Patch(AccessTools.Method("Techblox.Services.Eos.Anticheat.Client.EosGetPendingMessagesToSendServiceRequest:Execute"), new HarmonyMethod(((AntiAnticheatDelegateTask)AntiAntiCheatTask).Method));
harmony.Patch(AccessTools.Method("Techblox.Anticheat.Client.Engines.ShowFeedbackDialogEngine:PollAnticheatStatus"), new HarmonyMethod(((AntiAnticheatDelegateTask)AntiAntiCheatTask).Method));
}
private static bool AntiAntiCheat() => false;
private static bool AntiAntiCheat(ref object __result)
{
var targetType =
AccessTools.TypeByName("Techblox.Services.Eos.Anticheat.Client.Services.StartProtectedSessionResult");
var target = Activator.CreateInstance(targetType);
targetType.GetField("Success").SetValue(target, true);
__result = target;
return false;
}
private static bool AntiAntiCheat(ref bool __result)
{
__result = true;
return false;
}
private static bool AntiAntiCheatTask(ref IEnumerator<TaskContract> __result)
{
IEnumerator<TaskContract> Func()
{
yield return Yield.It;
}
__result = Func();
return false;
}
}
}

View file

@ -1,44 +0,0 @@
using System;
using RobocraftX.GUI.MyGamesScreen;
using Svelto.ECS;
using TechbloxModdingAPI.Engines;
using TechbloxModdingAPI.Utility;
namespace TechbloxModdingAPI.App
{
public class AppEngine : IFactoryEngine
{
public WrappedHandler<MenuEventArgs> EnterMenu;
public WrappedHandler<MenuEventArgs> ExitMenu;
public IEntityFactory Factory { set; private get; }
public string Name => "TechbloxModdingAPIAppEngine";
public bool isRemovable => false;
public EntitiesDB entitiesDB { set; private get; }
public void Dispose()
{
IsInMenu = false;
ExitMenu.Invoke(this, new MenuEventArgs { });
}
public void Ready()
{
IsInMenu = true;
EnterMenu.Invoke(this, new MenuEventArgs { });
}
// app functionality
public bool IsInMenu
{
get;
private set;
} = false;
}
}

View file

@ -14,12 +14,12 @@ namespace TechbloxModdingAPI.App
/// </summary>
public class Client
{
public static Client Instance { get; } = new Client();
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>
@ -93,14 +93,31 @@ namespace TechbloxModdingAPI.App
EnqueueError(errorHandlerInstance, popup);
}
// TODO
/*public void CloseCurrentPrompt()
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);
}*/
var popup = GetPopupCloseMethods(errorHandlerInstance);
popup.Close();
}
public void SelectFirstPromptButton()
{
object errorHandlerInstance = ErrorHandlerInstanceGetter();
var popup = GetPopupCloseMethods(errorHandlerInstance);
popup.FirstButton();
}
public void SelectSecondPromptButton()
{
object errorHandlerInstance = ErrorHandlerInstanceGetter();
var popup = GetPopupCloseMethods(errorHandlerInstance);
popup.SecondButton();
}
internal void CloseBetaPopup()
{
Game.menuEngine.CloseBetaPopup();
}
internal static void Init()
{
@ -113,9 +130,6 @@ namespace TechbloxModdingAPI.App
EnqueueError = (Action<object, Error>) AccessTools.Method("TechbloxModdingAPI.App.Client:GenEnqueueError")
.MakeGenericMethod(errorHandler, errorHandle)
.Invoke(null, new object[0]);
/*HandleErrorClosed = (Action<object>) AccessTools.Method("TechbloxModdingAPI.App.Client:GenHandlePopupClosed")
.MakeGenericMethod(errorHandler)
.Invoke(null, new object[0]);*/
}
// Creating delegates once is faster than reflection every time
@ -140,14 +154,23 @@ namespace TechbloxModdingAPI.App
return enqueueCasted;
}
private static Action<object> GenHandlePopupClosed<T>()
private static (Action Close, Action FirstButton, Action SecondButton) _errorPopup;
private static (Action Close, Action FirstButton, Action SecondButton) GetPopupCloseMethods(object handler)
{
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;
if (_errorPopup.Close != null)
return _errorPopup;
Type errorHandler = handler.GetType();
FieldInfo field = AccessTools.Field(errorHandler, "errorPopup");
var errorPopup = (ErrorPopup)field.GetValue(handler);
MethodInfo info = AccessTools.Method(errorPopup.GetType(), "ClosePopup");
var close = (Action)Delegate.CreateDelegate(typeof(Action), errorPopup, info);
info = AccessTools.Method(errorPopup.GetType(), "HandleFirstOption");
var first = (Action)Delegate.CreateDelegate(typeof(Action), errorPopup, info);
info = AccessTools.Method(errorPopup.GetType(), "HandleSecondOption");
var second = (Action)Delegate.CreateDelegate(typeof(Action), errorPopup, info);
_errorPopup = (close, first, second);
return _errorPopup;
}
}
}

View file

@ -42,17 +42,25 @@ namespace TechbloxModdingAPI.App
[APITestCase(TestType.Menu)]
public static void TestPopUp2()
{
Client c = new Client();
c.PromptUser(popup2);
//c.CloseCurrentPrompt();
Client.Instance.PromptUser(popup2);
}
[APITestCase(TestType.Menu)]
public static void TestPopUp1()
{
Client c = new Client();
c.PromptUser(popup1);
//c.CloseCurrentPrompt();
Client.Instance.PromptUser(popup1);
}
[APITestCase(TestType.Menu)]
public static void TestPopUpClose1()
{
Client.Instance.CloseCurrentPrompt();
}
[APITestCase(TestType.Menu)]
public static void TestPopUpClose2()
{
Client.Instance.CloseCurrentPrompt();
}
}
#endif

View file

@ -1,23 +1,27 @@
namespace TechbloxModdingAPI.App
using System;
namespace TechbloxModdingAPI.App
{
public enum CurrentGameMode
{
None,
/// <summary>
/// Building a game
/// Building a world
/// </summary>
Build,
/// <summary>
/// Playing a game
/// Playing on a map
/// </summary>
Play,
/// <summary>
/// Viewing a prefab
/// Viewing a prefab (doesn't exist anymore)
/// </summary>
[Obsolete]
View,
/// <summary>
/// Viewing a tutorial
/// Viewing a tutorial (doesn't exist anymore)
/// </summary>
[Obsolete]
Tutorial
}
}

View file

@ -2,12 +2,10 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using RobocraftX.Common;
using RobocraftX.GUI.MyGamesScreen;
using RobocraftX.StateSync;
using Svelto.ECS;
using Techblox.GameSelection;
using TechbloxModdingAPI;
using TechbloxModdingAPI.Blocks;
using TechbloxModdingAPI.Tasks;
using TechbloxModdingAPI.Utility;
@ -318,7 +316,7 @@ namespace TechbloxModdingAPI.App
get
{
if (menuMode || !VerifyMode()) return CurrentGameMode.None;
return (CurrentGameMode) GameMode.CurrentMode;
return gameEngine.GetGameData().gameMode == GameMode.CreateWorld ? CurrentGameMode.Build : CurrentGameMode.Play;
}
}

View file

@ -27,12 +27,14 @@ namespace TechbloxModdingAPI.App
public JobHandle OnInitializeTimeRunningMode(JobHandle inputDeps)
{
Console.WriteLine("Init time running mode");
SimulationMode.Invoke(this, new GameEventArgs { GameName = "", GamePath = "" }); // TODO
return inputDeps;
}
public JobHandle OnInitializeTimeStoppedMode(JobHandle inputDeps)
{
Console.WriteLine("Init time stopped mode");
BuildMode.Invoke(this, new GameEventArgs { GameName = "", GamePath = "" });
return inputDeps;
}

View file

@ -1,5 +1,7 @@
using System.Collections.Generic;
using HarmonyLib;
using RobocraftX;
using RobocraftX.Common;
using RobocraftX.Schedulers;
using RobocraftX.SimulationModeState;
@ -8,11 +10,15 @@ using Svelto.Tasks;
using Svelto.Tasks.Lean;
using RobocraftX.Blocks;
using RobocraftX.Common.Loading;
using RobocraftX.Multiplayer;
using RobocraftX.ScreenshotTaker;
using Techblox.Environment.Transition;
using Techblox.GameSelection;
using TechbloxModdingAPI.Blocks;
using TechbloxModdingAPI.Engines;
using TechbloxModdingAPI.Input;
using TechbloxModdingAPI.Players;
using TechbloxModdingAPI.Utility;
namespace TechbloxModdingAPI.App
@ -30,16 +36,34 @@ namespace TechbloxModdingAPI.App
public EntitiesDB entitiesDB { set; private get; }
private bool enteredGame;
private bool loadingFinished;
private bool playerJoined;
public void Dispose()
{
if (GameReloadedPatch.IsReload)
return; // Toggling time mode
ExitGame.Invoke(this, new GameEventArgs { GameName = GetGameData().saveName, GamePath = GetGameData().gameID });
IsInGame = false;
loadingFinished = false;
playerJoined = false;
enteredGame = false;
}
public void Ready()
{
if (GameReloadedPatch.IsReload)
return; // Toggling time mode
enteredGame = true;
Player.Joined += OnPlayerJoined;
}
private void OnPlayerJoined(object sender, PlayerEventArgs args)
{
if (args.Player.Type != PlayerType.Local) return;
playerJoined = true;
Player.Joined -= OnPlayerJoined;
CheckJoinEvent();
}
// game functionality
@ -54,7 +78,7 @@ namespace TechbloxModdingAPI.App
{
if (async)
{
ExitCurrentGameAsync().RunOn(Lean.EveryFrameStepRunner_TimeRunningAndStopped);
ExitCurrentGameAsync().RunOn(ClientLean.EveryFrameStepRunner_TimeRunningAndStopped);
}
else
{
@ -94,10 +118,23 @@ namespace TechbloxModdingAPI.App
public void ToggleTimeMode()
{
if (!entitiesDB.FoundInGroups<BlockTagEntityStruct>())
throw new AppStateException("At least one block must exist in the world to enter simulation");
SwitchAnimationUtil.Start(entitiesDB);
}
if (TimeRunningModeUtil.IsTimeStoppedMode(entitiesDB))
FakeInput.ActionInput(toggleMode: true);
else
{
IEnumerator<TaskContract> ReloadBuildModeTask()
{
SwitchAnimationUtil.Start(entitiesDB);
while (SwitchAnimationUtil.IsFadeOutActive(entitiesDB))
yield return (TaskContract)Yield.It;
FullGameFields._multiplayerParams.MultiplayerMode = MultiplayerMode.SinglePlayer;
AccessTools.Method(typeof(FullGameCompositionRoot), "ReloadGame")
.Invoke(FullGameFields.Instance, new object[] { });
}
ReloadBuildModeTask().RunOn(ClientLean.UIScheduler);
}
}
public EGID[] GetAllBlocksInGame(BlockIDs filter = BlockIDs.Invalid)
{
@ -142,9 +179,16 @@ namespace TechbloxModdingAPI.App
public void Remove(ref LoadingActionEntityStruct entityComponent, EGID egid)
{ // Finished loading
if (!enteredGame) return;
enteredGame = false;
loadingFinished = true;
CheckJoinEvent();
}
private void CheckJoinEvent()
{
if (!loadingFinished || !playerJoined) return;
EnterGame.Invoke(this, new GameEventArgs { GameName = GetGameData().saveName, GamePath = GetGameData().gameID });
IsInGame = true;
enteredGame = false;
}
}
}

View file

@ -3,16 +3,15 @@ using System.Reflection;
using HarmonyLib;
using RobocraftX;
using RobocraftX.Common;
using RobocraftX.FrontEnd;
using RobocraftX.GUI;
using RobocraftX.GUI.MyGamesScreen;
using RobocraftX.Multiplayer;
using Svelto.ECS;
using Svelto.ECS.Experimental;
using Techblox.GameSelection;
using TechbloxModdingAPI.Engines;
using TechbloxModdingAPI.Utility;
using GameMode = RobocraftX.Common.GameMode;
namespace TechbloxModdingAPI.App
{
@ -105,20 +104,16 @@ namespace TechbloxModdingAPI.App
return EnterGame(mgdes.GameName, mgdes.FileId);
}
public bool EnterGame(string gameName, string fileId, bool autoEnterSim = false)
public bool EnterGame(ECSString gameName, string fileId, bool autoEnterSim = false)
{
GameMode.CurrentMode = autoEnterSim ? RCXMode.Play : RCXMode.Build;
var data = new GameSelectionData
{
gameMode = Techblox.GameSelection.GameMode.PlayGame,
gameType = GameType.MachineEditor,
saveName = gameName,
saveType = SaveType.ExistingSave,
gameID = "GAMEID_Road_Track", //TODO: Expose to the API
userContentID = fileId
};
// the private FullGameCompositionRoot.SwitchToGame() method gets passed to menu items for this reason
AccessTools.Method(typeof(FullGameCompositionRoot), "SwitchToGame").Invoke(FullGameFields.Instance, new object[]{data});
FullGameFields._multiplayerParams.MultiplayerMode = MultiplayerMode.SinglePlayer;
ref var selection = ref entitiesDB.QueryEntity<GameSelectionComponent>(GameSelectionConstants.GameSelectionEGID);
selection.userContentID.Set(fileId);
selection.triggerStart = true;
selection.saveType = SaveType.ExistingSave;
selection.saveName = gameName;
selection.gameMode = GameMode.PlayGame;
selection.gameID.Set("GAMEID_Road_Track"); //TODO: Expose to the API
return true;
}
@ -161,6 +156,16 @@ namespace TechbloxModdingAPI.App
{
return ref entitiesDB.QueryEntity<T>(id);
}
internal void CloseBetaPopup()
{
var (buffer, count) = entitiesDB.QueryEntities<TogglePanelButtonEntityViewStruct>(ExclusiveGroup.Search("BetaPopup"));
for (int index = 0; index < count; ++index)
{
entitiesDB.QueryEntity<GUIEntityViewStruct>(buffer[index].TogglePanelButtonComponent.targetPanel)
.guiRoot.enabled = false;
}
}
}
internal class MyGameDataEntityDescriptor_DamnItFJWhyDidYouMakeThisInternal : GenericEntityDescriptor<MyGameDataEntityStruct> { }
@ -178,7 +183,7 @@ namespace TechbloxModdingAPI.App
public static MethodBase TargetMethod()
{
return AccessTools.Method(typeof(FullGameCompositionRoot), "GoToMenu");
return AccessTools.Method(typeof(FullGameCompositionRoot), "SwitchToMenu");
}
}

View file

@ -8,9 +8,10 @@ using Svelto.ECS.EntityStructs;
using RobocraftX.Common;
using RobocraftX.Blocks;
using Unity.Mathematics;
using Gamecraft.Blocks.GUI;
using HarmonyLib;
using RobocraftX.PilotSeat;
using RobocraftX.Rendering;
using Techblox.BlockLabels;
using TechbloxModdingAPI.Blocks;
using TechbloxModdingAPI.Blocks.Engines;
@ -339,11 +340,9 @@ namespace TechbloxModdingAPI
[TestValue(null)]
public string Label
{
get => BlockEngine.GetBlockInfoViewComponent<TextLabelEntityViewStruct>(this).textLabelComponent?.text;
get => BlockEngine.GetBlockInfo<LabelResourceIDComponent>(this).ToString(); //TODO: Block labels
set
{
var comp = BlockEngine.GetBlockInfoViewComponent<TextLabelEntityViewStruct>(this).textLabelComponent;
if (comp != null) comp.text = value;
{ //TODO
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;

View file

@ -273,6 +273,12 @@ namespace TechbloxModdingAPI.Blocks
/// <summary>
/// The grid block used by the world editor, named Small Grid like the other one
/// </summary>
SmallGridInWorldEditor
SmallGridInWorldEditor,
SegoeUITextblock = 376,
GravtracTextblock,
HauserTextblock,
TechnopollasTextblock,
BitBlock = 385,
Timer
}
}

View file

@ -22,7 +22,7 @@ namespace TechbloxModdingAPI.Blocks
base(new EGID(id, CommonExclusiveGroups.ENGINE_BLOCK_BUILD_GROUP))
{
}
/*
/// <summary>
/// Gets or sets the Engine's On property. May not be saved.
/// </summary>
@ -34,6 +34,7 @@ namespace TechbloxModdingAPI.Blocks
}
set
{
Techblox.BlockColours.BlockColoursCompositionRoot
BlockEngine.GetBlockInfo<Techblox.EngineBlock.EngineBlockComponent>(this).engineOn = value;
}
}
@ -377,6 +378,6 @@ namespace TechbloxModdingAPI.Blocks
{
BlockEngine.GetBlockInfo<Techblox.EngineBlock.EngineBlockReadonlyComponent>(this).manualToAutoGearCoolOffTime = value;
}
}
}*/
}
}

View file

@ -60,7 +60,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
copyToBlock.Invoke(Patch.copyEngine, new object[] {pickedBlock.ID, pickedBlock});
ExclusiveGroupStruct group = WiresExclusiveGroups.WIRES_COPY_GROUP + playerID;
ExclusiveGroupStruct group = BuildModeWiresGroups.WIRES_COPY_GROUP + playerID;
copyWireToBlock.Invoke(Patch.createWireEngine, new object[] {group, pickedBlock.ID});
pickedBlock.placedBlockTweaksMustCopy = false;

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Reflection;
using Gamecraft.Blocks.BlockGroups;
@ -173,7 +173,8 @@ namespace TechbloxModdingAPI.Blocks.Engines
foreach (var block in blocks)
{
GhostChildUtility.BuildGhostChild(in playerID, block.Id, in pos, in rot, entitiesDB,
BuildGhostBlueprintFactory, false, bssesopt.Get().buildingDroneReference);
BuildGhostBlueprintFactory, false, bssesopt.Get().buildingDroneReference,
FullGameFields._managers.blockLabelResourceManager);
}
}
@ -272,7 +273,8 @@ namespace TechbloxModdingAPI.Blocks.Engines
uint ghostChildBlockId = CommonExclusiveGroups.GetNewGhostChildBlockID();
var ghostEntityReference = GhostBlockUtils.GetGhostEntityReference(sourceId.entityID, entitiesDB);
var entityInitializer = BuildGhostBlueprintFactory.Build(
new EGID(ghostChildBlockId, BoxSelectExclusiveGroups.GhostChildEntitiesExclusiveGroup), /*dbStruct.DBID*/ (uint)BlockIDs.Cube);
new EGID(ghostChildBlockId, BoxSelectExclusiveGroups.GhostChildEntitiesExclusiveGroup), /*dbStruct.DBID*/ (uint)BlockIDs.Cube,
FullGameFields._managers.blockLabelResourceManager);
entityInitializer.Init(dbStruct);
entityInitializer.Init(new GFXPrefabEntityStructGPUI(
PrefabsID.GetOrCreatePrefabID((ushort)entityInitializer.Get<PrefabAssetIDComponent>().prefabAssetID,

View file

@ -1,5 +1,5 @@
using RobocraftX.Common;
using RobocraftX.UECS;
using RobocraftX.DOTS;
using Svelto.ECS;
using Svelto.ECS.EntityStructs;
using Unity.Mathematics;
@ -40,7 +40,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
ref PositionEntityStruct posStruct = ref this.entitiesDB.QueryEntityOrDefault<PositionEntityStruct>(block);
ref GridRotationStruct gridStruct = ref this.entitiesDB.QueryEntityOrDefault<GridRotationStruct>(block);
ref LocalTransformEntityStruct transStruct = ref this.entitiesDB.QueryEntityOrDefault<LocalTransformEntityStruct>(block);
ref UECSPhysicsEntityStruct phyStruct = ref this.entitiesDB.QueryEntityOrDefault<UECSPhysicsEntityStruct>(block);
ref DOTSPhysicsEntityStruct phyStruct = ref this.entitiesDB.QueryEntityOrDefault<DOTSPhysicsEntityStruct>(block);
// main (persistent) position
posStruct.position = vector;
// placement grid position
@ -50,7 +50,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
// collision position
if (phyStruct.ID != default)
{ //It exists
FullGameFields._physicsWorld.EntityManager.SetComponentData(phyStruct.uecsEntity, new Translation
FullGameFields._physicsWorld.EntityManager.SetComponentData(phyStruct.dotsEntity, new Translation
{
Value = posStruct.position
});

View file

@ -54,7 +54,8 @@ namespace TechbloxModdingAPI.Blocks.Engines
//RobocraftX.CR.MachineEditing.PlaceSingleBlockEngine
DBEntityStruct dbEntity = new DBEntityStruct {DBID = block};
EntityInitializer structInitializer = _blockEntityFactory.Build(CommonExclusiveGroups.nextBlockEntityID, block); //The ghost block index is only used for triggers
//TODO: Test
EntityInitializer structInitializer = _blockEntityFactory.Build(CommonExclusiveGroups.blockIDGeneratorClient.Next(), block); //The ghost block index is only used for triggers
uint prefabAssetID = structInitializer.Has<PrefabAssetIDComponent>()
? structInitializer.Get<PrefabAssetIDComponent>().prefabAssetID
: throw new BlockException("Prefab asset ID not found!"); //Set by the game

View file

@ -1,5 +1,5 @@
using RobocraftX.Common;
using RobocraftX.UECS;
using RobocraftX.DOTS;
using Svelto.ECS;
using Svelto.ECS.EntityStructs;
using Unity.Mathematics;
@ -40,7 +40,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
ref RotationEntityStruct rotStruct = ref this.entitiesDB.QueryEntityOrDefault<RotationEntityStruct>(block);
ref GridRotationStruct gridStruct = ref this.entitiesDB.QueryEntityOrDefault<GridRotationStruct>(block);
ref LocalTransformEntityStruct transStruct = ref this.entitiesDB.QueryEntityOrDefault<LocalTransformEntityStruct>(block);
ref UECSPhysicsEntityStruct phyStruct = ref this.entitiesDB.QueryEntityOrDefault<UECSPhysicsEntityStruct>(block);
ref DOTSPhysicsEntityStruct phyStruct = ref this.entitiesDB.QueryEntityOrDefault<DOTSPhysicsEntityStruct>(block);
// main (persistent) rotation
Quaternion newRotation = rotStruct.rotation;
newRotation.eulerAngles = vector;
@ -52,7 +52,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
// collision rotation
if (phyStruct.ID != default)
{ //It exists
FullGameFields._physicsWorld.EntityManager.SetComponentData(phyStruct.uecsEntity,
FullGameFields._physicsWorld.EntityManager.SetComponentData(phyStruct.dotsEntity,
new Unity.Transforms.Rotation
{
Value = rotStruct.rotation

View file

@ -2,7 +2,7 @@
using HarmonyLib;
using RobocraftX.Common;
using RobocraftX.UECS;
using RobocraftX.DOTS;
using Svelto.ECS;
using Unity.Entities;
@ -13,7 +13,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
{
public class ScalingEngine : IApiEngine
{
private static IReactOnAddAndRemove<UECSPhysicsEntityCreationStruct> physicsEngine;
private static IReactOnAddAndRemove<DOTSPhysicsEntityCreationStruct> physicsEngine;
public void Ready()
{
@ -34,16 +34,16 @@ namespace TechbloxModdingAPI.Blocks.Engines
if (_entityManager == default)
_entityManager = FullGameFields._physicsWorld.EntityManager;
//Assuming the block exists
var entity = entitiesDB.QueryEntity<UECSPhysicsEntityStruct>(egid).uecsEntity;
var pes = new UECSPhysicsEntityCreationStruct();
physicsEngine.Add(ref pes, egid); //Create new UECS entity
var entity = entitiesDB.QueryEntity<DOTSPhysicsEntityStruct>(egid).dotsEntity;
var pes = new DOTSPhysicsEntityCreationStruct();
physicsEngine.Add(ref pes, egid); //Create new DOTS entity
_entityManager.DestroyEntity(entity);
}
[HarmonyPatch]
class PhysicsEnginePatch
{
static void Postfix(IReactOnAddAndRemove<UECSPhysicsEntityCreationStruct> __instance)
static void Postfix(IReactOnAddAndRemove<DOTSPhysicsEntityCreationStruct> __instance)
{
physicsEngine = __instance;
Logging.MetaDebugLog("Physics engine injected.");
@ -51,7 +51,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
static MethodBase TargetMethod(Harmony instance)
{
return AccessTools.Method("RobocraftX.StateSync.HandleUECSPhysicEntitiesWithPrefabCreationEngine" +
return AccessTools.Method("RobocraftX.StateSync.HandleDOTSPhysicEntitiesWithPrefabCreationEngine" +
":Ready");
}
}

View file

@ -43,7 +43,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
public WireEntityStruct CreateNewWire(EGID startBlock, byte startPort, EGID endBlock, byte endPort)
{
EGID wireEGID = new EGID(WiresExclusiveGroups.NewWireEntityId, NamedExclusiveGroup<WiresGroup>.Group);
EGID wireEGID = new EGID(BuildModeWiresGroups.NewWireEntityId, BuildModeWiresGroups.WiresGroup.Group);
EntityInitializer wireInitializer = Factory.BuildEntity<WireEntityDescriptor>(wireEGID);
wireInitializer.Init(new WireEntityStruct
{
@ -77,8 +77,8 @@ namespace TechbloxModdingAPI.Blocks.Engines
public ref PortEntityStruct GetPortByOffset(BlockPortsStruct bps, byte portNumber, bool input)
{
ExclusiveGroup group = input
? NamedExclusiveGroup<InputPortsGroup>.Group
: NamedExclusiveGroup<OutputPortsGroup>.Group;
? NamedExclusiveGroup<BuildModeWiresGroups.InputPortsGroup>.Group
: NamedExclusiveGroup<BuildModeWiresGroups.OutputPortsGroup>.Group;
uint id = (input ? bps.firstInputID : bps.firstOutputID) + portNumber;
EGID egid = new EGID(id, group);
if (!entitiesDB.Exists<PortEntityStruct>(egid))
@ -193,7 +193,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
EGID[] inputs = new EGID[ports.inputCount];
for (uint i = 0; i < ports.inputCount; i++)
{
inputs[i] = new EGID(i + ports.firstInputID, NamedExclusiveGroup<InputPortsGroup>.Group);
inputs[i] = new EGID(i + ports.firstInputID, NamedExclusiveGroup<BuildModeWiresGroups.InputPortsGroup>.Group);
}
return inputs;
}
@ -204,7 +204,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
EGID[] outputs = new EGID[ports.outputCount];
for (uint i = 0; i < ports.outputCount; i++)
{
outputs[i] = new EGID(i + ports.firstOutputID, NamedExclusiveGroup<OutputPortsGroup>.Group);
outputs[i] = new EGID(i + ports.firstOutputID, NamedExclusiveGroup<BuildModeWiresGroups.OutputPortsGroup>.Group);
}
return outputs;
}
@ -219,8 +219,8 @@ namespace TechbloxModdingAPI.Blocks.Engines
if (!entitiesDB.Exists<BlockPortsStruct>(block))
return default;
var group = output
? NamedExclusiveGroup<OutputPortsGroup>.Group
: NamedExclusiveGroup<InputPortsGroup>.Group;
? NamedExclusiveGroup<BuildModeWiresGroups.OutputPortsGroup>.Group
: NamedExclusiveGroup<BuildModeWiresGroups.InputPortsGroup>.Group;
BlockPortsStruct ports = entitiesDB.QueryEntity<BlockPortsStruct>(block);
if (!entitiesDB.TryQueryMappedEntities<PortEntityStruct>(group, out var mapper))
return default;
@ -237,7 +237,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
public ref WireEntityStruct MatchPortToWire(PortEntityStruct port, EGID blockID, out bool exists)
{
var wires = entitiesDB.QueryEntities<WireEntityStruct>(NamedExclusiveGroup<WiresGroup>.Group);
var wires = entitiesDB.QueryEntities<WireEntityStruct>(NamedExclusiveGroup<BuildModeWiresGroups.WiresGroup>.Group);
var wiresB = wires.ToBuffer().buffer;
for (uint i = 0; i < wires.count; i++)
{
@ -264,7 +264,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
else
{
BlockPortsStruct ports = entitiesDB.QueryEntity<BlockPortsStruct>(startBlock);
startPorts = new EGID[] {new EGID(ports.firstOutputID + startPort, NamedExclusiveGroup<OutputPortsGroup>.Group) };
startPorts = new EGID[] {new EGID(ports.firstOutputID + startPort, NamedExclusiveGroup<BuildModeWiresGroups.OutputPortsGroup>.Group) };
}
EGID[] endPorts;
@ -276,10 +276,10 @@ namespace TechbloxModdingAPI.Blocks.Engines
else
{
BlockPortsStruct ports = entitiesDB.QueryEntity<BlockPortsStruct>(endBlock);
endPorts = new EGID[] {new EGID(ports.firstInputID + endPort, NamedExclusiveGroup<InputPortsGroup>.Group) };
endPorts = new EGID[] {new EGID(ports.firstInputID + endPort, NamedExclusiveGroup<BuildModeWiresGroups.InputPortsGroup>.Group) };
}
EntityCollection<WireEntityStruct> wires = entitiesDB.QueryEntities<WireEntityStruct>(NamedExclusiveGroup<WiresGroup>.Group);
EntityCollection<WireEntityStruct> wires = entitiesDB.QueryEntities<WireEntityStruct>(NamedExclusiveGroup<BuildModeWiresGroups.WiresGroup>.Group);
var wiresB = wires.ToBuffer().buffer;
for (int endIndex = 0; endIndex < endPorts.Length; endIndex++)
{
@ -304,7 +304,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
public OptionalRef<ChannelDataStruct> GetChannelDataStruct(EGID portID)
{
var port = GetPort(portID);
var channels = entitiesDB.QueryEntities<ChannelDataStruct>(NamedExclusiveGroup<ChannelDataGroup>.Group);
var channels = entitiesDB.QueryEntities<ChannelDataStruct>(NamedExclusiveGroup<BuildModeWiresGroups.ChannelDataGroup>.Group);
var channelsB = channels.ToBuffer();
return port.firstChannelIndexCachedInSim < channels.count
? new OptionalRef<ChannelDataStruct>(channelsB.buffer, port.firstChannelIndexCachedInSim)
@ -329,7 +329,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
public EGID[] WiredToInput(EGID block, byte port)
{
WireEntityStruct[] wireEntityStructs = Search(NamedExclusiveGroup<WiresGroup>.Group,
WireEntityStruct[] wireEntityStructs = Search(NamedExclusiveGroup<BuildModeWiresGroups.WiresGroup>.Group,
(WireEntityStruct wes) => wes.destinationPortUsage == port && wes.destinationBlockEGID == block);
EGID[] result = new EGID[wireEntityStructs.Length];
for (uint i = 0; i < wireEntityStructs.Length; i++)
@ -342,7 +342,7 @@ namespace TechbloxModdingAPI.Blocks.Engines
public EGID[] WiredToOutput(EGID block, byte port)
{
WireEntityStruct[] wireEntityStructs = Search(NamedExclusiveGroup<WiresGroup>.Group,
WireEntityStruct[] wireEntityStructs = Search(NamedExclusiveGroup<BuildModeWiresGroups.WiresGroup>.Group,
(WireEntityStruct wes) => wes.sourcePortUsage == port && wes.sourceBlockEGID == block);
EGID[] result = new EGID[wireEntityStructs.Length];
for (uint i = 0; i < wireEntityStructs.Length; i++)
@ -371,13 +371,13 @@ namespace TechbloxModdingAPI.Blocks.Engines
private EntityCollection<ChannelDataStruct> GetSignalStruct(uint signalID, out uint index, bool input = true)
{
ExclusiveGroup group = input
? NamedExclusiveGroup<InputPortsGroup>.Group
: NamedExclusiveGroup<OutputPortsGroup>.Group;
? NamedExclusiveGroup<BuildModeWiresGroups.InputPortsGroup>.Group
: NamedExclusiveGroup<BuildModeWiresGroups.OutputPortsGroup>.Group;
if (entitiesDB.Exists<PortEntityStruct>(signalID, group))
{
index = entitiesDB.QueryEntity<PortEntityStruct>(signalID, group).anyChannelIndex;
var channelData =
entitiesDB.QueryEntities<ChannelDataStruct>(NamedExclusiveGroup<ChannelDataGroup>.Group);
entitiesDB.QueryEntities<ChannelDataStruct>(NamedExclusiveGroup<BuildModeWiresGroups.ChannelDataGroup>.Group);
return channelData;
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using Unity.Mathematics;
using UnityEngine;

View file

@ -1,34 +0,0 @@
using System;
using System.Reflection;
using HarmonyLib;
using Svelto.ECS;
using RobocraftX.CR.MainGame;
using RobocraftX.Multiplayer;
using RobocraftX.StateSync;
using TechbloxModdingAPI.Utility;
namespace TechbloxModdingAPI.Commands
{
/// <summary>
/// Patch of RobocraftX.CR.MainGame.MainGameCompositionRoot.DeterministicCompose<T>()
/// Initializes custom commands
/// </summary>
[HarmonyPatch]
static class CommandPatch
{
public static void Postfix(StateSyncRegistrationHelper stateSyncReg)
{
/*CommandLineCompositionRoot.Compose(contextHolder, stateSyncReg.enginesRoot, reloadGame, multiplayerParameters,
stateSyncReg); - uREPL C# compilation not supported anymore */
var enginesRoot = stateSyncReg.enginesRoot;
CommandManager.RegisterEngines(enginesRoot);
}
public static MethodInfo TargetMethod()
{
return AccessTools.Method(typeof(MainGameCompositionRoot), "DeterministicCompose")
.MakeGenericMethod(typeof(object));
}
}
}

View file

@ -62,6 +62,7 @@ namespace TechbloxModdingAPI
var id = initializer(this);
if (!dict.ContainsKey(id)) // Multiple instances may be created
dict.Add(id, this);
Id = id;
}
#region ECS initializer stuff

View file

@ -6,27 +6,67 @@ using RobocraftX.FrontEnd;
using RobocraftX.StateSync;
using Svelto.ECS;
using Svelto.ECS.Schedulers;
using TechbloxModdingAPI.Commands;
using TechbloxModdingAPI.Utility;
namespace TechbloxModdingAPI.Engines
{
[HarmonyPatch]
class GameLoadedEnginePatch
static class GameLoadedTimeStoppedEnginePatch
{
public static EntitiesSubmissionScheduler Scheduler { get; private set; }
public static void Postfix(StateSyncRegistrationHelper stateSyncReg)
{
// register all game engines, including deterministic
GameEngineManager.RegisterEngines(stateSyncReg);
Scheduler = stateSyncReg.enginesRoot.scheduler;
// register command engines
/*CommandLineCompositionRoot.Compose(contextHolder, stateSyncReg.enginesRoot, reloadGame, multiplayerParameters,
stateSyncReg); - uREPL C# compilation not supported anymore */
CommandManager.RegisterEngines(stateSyncReg.enginesRoot);
}
public static MethodBase TargetMethod()
{
return AccessTools.Method(typeof(MainGameCompositionRoot), "DeterministicCompose").MakeGenericMethod(typeof(object));
return AccessTools.Method(typeof(MainGameCompositionRoot), "DeterministicTimeStoppedCompose").MakeGenericMethod(typeof(object));
}
}
[HarmonyPatch]
static class GameLoadedTimeRunningEnginePatch
{
public static void Postfix(StateSyncRegistrationHelper stateSyncReg)
{
GameEngineManager.RegisterEngines(stateSyncReg);
CommandManager.RegisterEngines(stateSyncReg.enginesRoot);
}
public static MethodBase TargetMethod()
{
return AccessTools.Method(typeof(MainGameCompositionRoot), "DeterministicTimeRunningCompose").MakeGenericMethod(typeof(object));
}
}
[HarmonyPatch]
static class GameReloadedPatch
{
internal static bool IsReload;
public static void Prefix() => IsReload = true;
public static MethodBase TargetMethod() => AccessTools.Method(typeof(FullGameCompositionRoot), "ReloadGame");
}
[HarmonyPatch]
static class GameSwitchedToPatch
{
public static void Prefix() => GameReloadedPatch.IsReload = false;
public static MethodBase TargetMethod() => AccessTools.Method(typeof(FullGameCompositionRoot), "SwitchToGame");
}
[HarmonyPatch]
static class MenuSwitchedToPatch
{
public static void Prefix() => GameReloadedPatch.IsReload = false;
public static MethodBase TargetMethod() => AccessTools.Method(typeof(FullGameCompositionRoot), "SwitchToMenu");
}
[HarmonyPatch]
class MenuLoadedEnginePatch
{

View file

@ -1,15 +1,13 @@
using System;
using RobocraftX.Common.Input;
using RobocraftX.Common;
using RobocraftX.Common.Input;
using Svelto.ECS;
using TechbloxModdingAPI.App;
using TechbloxModdingAPI.Utility;
namespace TechbloxModdingAPI.Input
{
public static class FakeInput
{
private static readonly FakeInputEngine inputEngine = new FakeInputEngine();
internal static readonly FakeInputEngine inputEngine = new FakeInputEngine();
/// <summary>
/// Customize the local input.
@ -103,39 +101,39 @@ namespace TechbloxModdingAPI.Input
}
public static void ActionInput(uint playerID = uint.MaxValue, bool toggleMode = false, bool forward = false, bool backward = false, bool up = false, bool down = false, bool left = false, bool right = false, bool sprint = false, bool toggleFly = false, bool alt = false, bool primary = false, bool secondary = false, bool tertiary = false, bool primaryHeld = false, bool secondaryHeld = false, bool toggleUnitGrid = false, bool ctrl = false, bool toggleColourMode = false, bool scaleBlockUp = false, bool scaleBlockDown = false, bool rotateBlockClockwise = false, bool rotateBlockCounterclockwise = false, bool cutSelection = false, bool copySelection = false, bool deleteSelection = false)
{
if (playerID == uint.MaxValue)
{
playerID = inputEngine.GetLocalPlayerID();
}
ref LocalPlayerInputEntityStruct currentInput = ref inputEngine.GetPlayerInputRef(playerID);
{ // TODO: We can only alter our own inputs clientside
ref var currentInput = ref inputEngine._localInputCache;
//Utility.Logging.CommandLog($"Current sim frame {currentInput.frame}");
// set inputs
if (toggleMode) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.ToggleTimeRunningMode;
if (forward) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.Forward;
if (backward) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.Backward;
if (up) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.Up;
if (down) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.Down;
if (left) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.Left;
if (right) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.Right;
if (sprint) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.Sprint;
//if (toggleFly) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.SwitchFlyMode;
//if (alt) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.AltAction;
if (primary) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.PrimaryAction;
if (secondary) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.SecondaryAction;
if (tertiary) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.TertiaryAction;
if (primaryHeld) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.PrimaryActionHeld;
if (secondaryHeld) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.SecondaryActionHeld;
//if (toggleUnitGrid) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.ToggleUnitGrid;
if (ctrl) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.CtrlAction;
if (toggleColourMode) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.ToggleColourMode;
if (scaleBlockUp) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.ScaleBlockUp;
if (scaleBlockDown) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.ScaleBlockDown;
if (rotateBlockClockwise) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.RotateBlockClockwise;
if (rotateBlockCounterclockwise) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.RotateBlockAnticlockwise;
if (cutSelection) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.CutSelection;
if (copySelection) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.CopySelection;
if (deleteSelection) currentInput.actionMask |= RobocraftX.Common.Input.ActionInput.DeleteSelection;
if (toggleMode) currentInput |= RobocraftX.Common.Input.ActionInput.ToggleTimeRunningModePlay; //TODO: Test, play
if (forward) currentInput |= RobocraftX.Common.Input.ActionInput.Forward;
if (backward) currentInput |= RobocraftX.Common.Input.ActionInput.Backward;
if (up) currentInput |= RobocraftX.Common.Input.ActionInput.Up;
if (down) currentInput |= RobocraftX.Common.Input.ActionInput.Down;
if (left) currentInput |= RobocraftX.Common.Input.ActionInput.Left;
if (right) currentInput |= RobocraftX.Common.Input.ActionInput.Right;
if (sprint) currentInput |= RobocraftX.Common.Input.ActionInput.Sprint;
//if (toggleFly) currentInput |= RobocraftX.Common.Input.ActionInput.SwitchFlyMode;
//if (alt) currentInput |= RobocraftX.Common.Input.ActionInput.AltAction;
if (primary) currentInput |= RobocraftX.Common.Input.ActionInput.PrimaryActionClick;
if (secondary) currentInput |= RobocraftX.Common.Input.ActionInput.SecondaryActionClick;
if (tertiary) currentInput |= RobocraftX.Common.Input.ActionInput.TertiaryAction;
if (primaryHeld) currentInput |= RobocraftX.Common.Input.ActionInput.PrimaryActionHeld;
if (secondaryHeld) currentInput |= RobocraftX.Common.Input.ActionInput.SecondaryActionHeld;
//if (toggleUnitGrid) currentInput |= RobocraftX.Common.Input.ActionInput.ToggleUnitGrid;
if (ctrl) currentInput |= RobocraftX.Common.Input.ActionInput.CtrlAction;
if (toggleColourMode) currentInput |= RobocraftX.Common.Input.ActionInput.ToggleColourMode;
if (scaleBlockUp) currentInput |= RobocraftX.Common.Input.ActionInput.ScaleBlockUp;
if (scaleBlockDown) currentInput |= RobocraftX.Common.Input.ActionInput.ScaleBlockDown;
if (rotateBlockClockwise) currentInput |= RobocraftX.Common.Input.ActionInput.RotateBlockClockwise;
if (rotateBlockCounterclockwise) currentInput |= RobocraftX.Common.Input.ActionInput.RotateBlockAnticlockwise;
if (cutSelection) currentInput |= RobocraftX.Common.Input.ActionInput.CutSelection;
if (copySelection) currentInput |= RobocraftX.Common.Input.ActionInput.CopySelection;
if (deleteSelection) currentInput |= RobocraftX.Common.Input.ActionInput.DeleteSelection;
if(Game.CurrentGame().IsTimeStopped)
inputEngine.HandleCustomInput(); // Only gets called when online, so calling it here as well
}
public static void Init()

View file

@ -20,6 +20,8 @@ namespace TechbloxModdingAPI.Input
public bool IsReady = false;
internal ActionInput _localInputCache;
public void Dispose()
{
IsReady = false;
@ -86,6 +88,14 @@ namespace TechbloxModdingAPI.Input
return ref entitiesDB.QueryEntity<LocalPlayerInputEntityStruct>(egid);
}
internal void HandleCustomInput()
{
if (!LocalPlayerIDUtility.DoesLocalPlayerExist(entitiesDB))
return;
GetPlayerInputRef(GetLocalPlayerID()).actionMask |= _localInputCache;
_localInputCache = default;
}
public uint GetLocalPlayerID()
{
return LocalPlayerIDUtility.GetLocalPlayerID(entitiesDB);

View file

@ -0,0 +1,19 @@
using System.Reflection;
using HarmonyLib;
namespace TechbloxModdingAPI.Input
{
[HarmonyPatch]
public static class FakeInputPatch
{
public static void Prefix()
{
FakeInput.inputEngine.HandleCustomInput(); // This gets called right before the input is sent to the server
}
public static MethodBase TargetMethod()
{
return AccessTools.Method("RobocraftX.Multiplayer.Input.NetworkInputRecorderEngine:RecordDeterministicInput");
}
}
}

View file

@ -1,13 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using TechbloxModdingAPI.App;
using TechbloxModdingAPI.Utility;
using Rewired.Internal;
using Svelto.DataStructures;
using Svelto.Tasks;
using Svelto.Tasks.ExtraLean;
using Svelto.Tasks.ExtraLean.Unity;
using TechbloxModdingAPI.Tasks;
using UnityEngine;
namespace TechbloxModdingAPI.Interface.IMGUI

View file

@ -3,13 +3,11 @@ using System.Reflection;
using HarmonyLib;
using RobocraftX;
using RobocraftX.Schedulers;
using RobocraftX.Services;
using Svelto.Context;
using Svelto.Tasks.ExtraLean;
using TechbloxModdingAPI.App;
using TechbloxModdingAPI.Blocks;
using TechbloxModdingAPI.Events;
using TechbloxModdingAPI.Tasks;
using TechbloxModdingAPI.Utility;
@ -51,34 +49,42 @@ namespace TechbloxModdingAPI
harmony.PatchAll(currentAssembly);
}
catch (Exception e)
{ //Can't use ErrorBuilder or Logging.LogException (which eventually uses ErrorBuilder) yet
Logging.Log(e.ToString());
Logging.LogWarning("Failed to patch Techblox. Attempting to patch to display error...");
harmony.Patch(AccessTools.Method(typeof(FullGameCompositionRoot), "OnContextInitialized")
.MakeGenericMethod(typeof(UnityContext<FullGameCompositionRoot>)),
new HarmonyMethod(((Action) OnPatchError).Method)); //Can't use lambdas here :(
{
HandleError(e, "Failed to patch Techblox. Attempting to patch to display error...", OnPatchError);
return;
}
// init utility
Logging.MetaDebugLog($"Initializing Utility");
Utility.GameState.Init();
// init block implementors
Logging.MetaDebugLog($"Initializing Blocks");
// init input
Input.FakeInput.Init();
// init object-oriented classes
Player.Init();
Block.Init();
BlockGroup.Init();
Wire.Init();
Logging.MetaDebugLog($"Initializing Client");
Client.Init();
Game.Init();
// init UI
Interface.IMGUI.Constants.Init();
Interface.IMGUI.IMGUIManager.Init();
Logging.MetaLog($"{currentAssembly.GetName().Name} v{currentAssembly.GetName().Version} initialized");
try
{
// init utility
Logging.MetaDebugLog($"Initializing Utility");
Utility.GameState.Init();
// init block implementors
Logging.MetaDebugLog($"Initializing Blocks");
// init input
Input.FakeInput.Init();
// init object-oriented classes
Player.Init();
Block.Init();
BlockGroup.Init();
Wire.Init();
// init client
Logging.MetaDebugLog($"Initializing Client");
Client.Init();
Game.Init();
// init UI
Logging.MetaDebugLog($"Initializing UI");
Interface.IMGUI.Constants.Init();
Interface.IMGUI.IMGUIManager.Init();
// init anti-anticheat
Logging.MetaDebugLog("Initializing anti-anticheat");
AntiAntiCheatPatch.Init(harmony);
Logging.MetaLog($"{currentAssembly.GetName().Name} v{currentAssembly.GetName().Version} initialized");
}
catch (Exception e)
{
HandleError(e, "Failed to initialize the API! Attempting to patch to display error...", OnInitError);
}
}
/// <summary>
@ -109,5 +115,26 @@ namespace TechbloxModdingAPI
ErrorBuilder.DisplayMustQuitError("Failed to patch Techblox!\n" +
"Make sure you're using the latest version of TechbloxModdingAPI or disable mods if the API isn't released yet.");
}
private static void OnInitError()
{
ErrorBuilder.DisplayMustQuitError("Failed to initialize the modding API!\n" +
"Make sure you're using the latest version. If you are, please report the error.");
}
/// <summary>
/// Handles an init error. Logs the exception, a log message, and allows displaying an error in-game.
/// </summary>
/// <param name="e">The exception</param>
/// <param name="logMsg">The log message</param>
/// <param name="onInit">The action to run when the game is ready to display error messages</param>
private static void HandleError(Exception e, string logMsg, Action onInit)
{ //Can't use ErrorBuilder or Logging.LogException (which eventually uses ErrorBuilder) yet
Logging.Log(e.ToString());
Logging.LogWarning(logMsg);
harmony.Patch(AccessTools.Method(typeof(FullGameCompositionRoot), "OnContextInitialized")
.MakeGenericMethod(typeof(UnityContext<FullGameCompositionRoot>)),
new HarmonyMethod(onInit.Method)); //Can't use lambdas here :(
}
}
}

View file

@ -25,7 +25,7 @@ namespace TechbloxModdingAPI.Persistence
Logging.MetaDebugLog("Skipping component serialization: no serializers registered!");
return;
}
serializationData.data.ExpandBy((uint)frameStart.Length);
serializationData.data.IncreaseCapacityBy((uint)frameStart.Length);
BinaryBufferWriter bbw = new BinaryBufferWriter(serializationData.data.ToArrayFast(out int buffLen), serializationData.dataPos);
uint originalPos = serializationData.dataPos;
Logging.MetaDebugLog($"dataPos: {originalPos}");
@ -35,14 +35,14 @@ namespace TechbloxModdingAPI.Persistence
bbw.Write(frameStart[i]);
}
Logging.MetaDebugLog($"dataPos (after frame start): {bbw.Position}");
serializationData.data.ExpandBy(4u);
serializationData.data.IncreaseCapacityBy(4u);
bbw.Write((uint)SerializerManager.GetSerializersCount());
string[] serializerKeys = SerializerManager.GetSerializerNames();
for (uint c = 0; c < serializerKeys.Length; c++)
{
Logging.MetaDebugLog($"dataPos (loop start): {bbw.Position}");
// write component info
serializationData.data.ExpandBy(4u + (uint)serializerKeys[c].Length);
serializationData.data.IncreaseCapacityBy(4u + (uint)serializerKeys[c].Length);
bbw.Write((uint)serializerKeys[c].Length);
Logging.MetaDebugLog($"dataPos (now): {bbw.Position}");
byte[] nameBytes = Encoding.UTF8.GetBytes(serializerKeys[c]);
@ -51,7 +51,7 @@ namespace TechbloxModdingAPI.Persistence
bbw.Write(nameBytes[i]);
}
Logging.MetaDebugLog($"dataPos (now): {bbw.Position}");
serializationData.data.ExpandBy(4u);
serializationData.data.IncreaseCapacityBy(4u);
serializationData.dataPos = bbw.Position + 4u;
Logging.MetaDebugLog($"dataPos (now): {bbw.Position}");
Logging.MetaDebugLog($"dataPos (appears to be): {serializationData.dataPos}");
@ -73,8 +73,8 @@ namespace TechbloxModdingAPI.Persistence
}
public static MethodBase TargetMethod()
{
return typeof(SaveGameEngine).GetMethod("SerializeGameToBuffer");
{
return AccessTools.TypeByName("RobocraftX.SaveAndLoad.SaveGameEngine").GetMethod("SerializeGameToBuffer");
}
}
}

View file

@ -46,7 +46,7 @@ namespace TechbloxModdingAPI.Persistence
public bool Serialize(ref ISerializationData serializationData, EntitiesDB entitiesDB, IEntitySerialization entitySerializer)
{
serializationData.data.ExpandBy(4u);
serializationData.data.IncreaseCapacityBy(4u);
BinaryBufferWriter bbw = new BinaryBufferWriter(serializationData.data.ToArrayFast(out int count), serializationData.dataPos);
EGID[] toSerialize = getEntitiesToSerialize(entitiesDB);
bbw.Write((uint)toSerialize.Length);

View file

@ -20,6 +20,22 @@ namespace TechbloxModdingAPI
add => seatExited += value;
remove => seatExited -= value;
}
internal static WrappedHandler<PlayerEventArgs> joined;
public static event EventHandler<PlayerEventArgs> Joined
{
add => joined += value;
remove => joined -= value;
}
internal static WrappedHandler<PlayerEventArgs> left;
public static event EventHandler<PlayerEventArgs> Left
{
add => left += value;
remove => left -= value;
}
}
public struct PlayerSeatEventArgs
@ -27,4 +43,10 @@ namespace TechbloxModdingAPI
public EGID SeatId;
public Seat Seat => (Seat)Block.New(SeatId);
}
public struct PlayerEventArgs
{
public EGID PlayerId;
public Player Player => Player.GetInstance(PlayerId.entityID);
}
}

View file

@ -23,8 +23,8 @@ namespace TechbloxModdingAPI
public partial class Player : EcsObjectBase, IEquatable<Player>, IEquatable<EGID>
{
// static functionality
private static PlayerEngine playerEngine = new PlayerEngine();
private static PlayerEventsEngine playerEventsEngine = new PlayerEventsEngine();
private static readonly PlayerEngine playerEngine = new PlayerEngine();
private static readonly PlayerEventsEngine playerEventsEngine = new PlayerEventsEngine();
private static Player localPlayer;
/// <summary>
@ -76,6 +76,12 @@ namespace TechbloxModdingAPI
}
}
internal static Player GetInstance(uint id)
{
return EcsObjectBase.GetInstance(new EGID(id, CharacterExclusiveGroups.OnFootGroup),
e => new Player(e.entityID));
}
/// <summary>
/// Initializes a new instance of the <see cref="T:TechbloxModdingAPI.Player"/> class.
/// </summary>
@ -119,6 +125,7 @@ namespace TechbloxModdingAPI
})
{
this.Type = player;
Id = base.Id.entityID;
}
// object fields & properties
@ -187,8 +194,6 @@ namespace TechbloxModdingAPI
public float Mass =>
1f / playerEngine.GetCharacterStruct<RigidBodyEntityStruct>(Id).Get().physicsMass.InverseMass;
private float _ping = -1f;
/// <summary>
/// The player's latest network ping time.
/// </summary>
@ -197,12 +202,7 @@ namespace TechbloxModdingAPI
{
get
{
var opt = playerEngine.GetPlayerStruct<PlayerNetworkStatsEntityStruct>(Id, Type);
if (opt)
{
_ping = opt.Get().lastPingTimeSinceLevelLoad ?? _ping;
}
return _ping;
return playerEngine.GetPing() / 1000f;
}
}
@ -435,15 +435,38 @@ namespace TechbloxModdingAPI
playerEngine.SetLocation(Id, location, exitSeat: exitSeat);
}
/// <summary>
/// Enter the given seat.
/// </summary>
/// <param name="seat">The seat to enter.</param>
public void EnterSeat(Seat seat)
{
playerEngine.EnterSeat(Id, seat.Id);
}
/// <summary>
/// Exit the seat the player is currently in.
/// </summary>
public void ExitSeat()
{
playerEngine.ExitSeat(Id);
}
/// <summary>
/// Spawn the machine the player is holding in time running mode.
/// </summary>
public bool SpawnMachine()
{
return playerEngine.SpawnMachine(Id);
}
/// <summary>
/// Despawn the player's machine in time running mode and place it in their hand.
/// </summary>
public bool DespawnMachine()
{
return playerEngine.DespawnMachine(Id);
}
/// <summary>
/// Returns the block the player is currently looking at in build mode.
@ -481,7 +504,7 @@ namespace TechbloxModdingAPI
{
var egid = playerEngine.GetThingLookedAt(Id, maxDistance);
return egid != default && egid.groupID == WiresGUIExclusiveGroups.WireGroup
? EcsObjectBase.GetInstance(new EGID(egid.entityID, NamedExclusiveGroup<WiresGroup>.Group),
? EcsObjectBase.GetInstance(new EGID(egid.entityID, BuildModeWiresGroups.WiresGroup.Group),
e => new Wire(e))
: null;
}

View file

@ -10,7 +10,9 @@ using RobocraftX.Physics;
using RobocraftX.Blocks.Ghost;
using Gamecraft.GUI.HUDFeedbackBlocks;
using RobocraftX.Blocks;
using RobocraftX.Multiplayer;
using RobocraftX.PilotSeat;
using RobocraftX.SimulationModeState;
using Svelto.ECS;
using Techblox.Camera;
using Unity.Mathematics;
@ -19,6 +21,7 @@ using Svelto.ECS.EntityStructs;
using Techblox.BuildingDrone;
using TechbloxModdingAPI.Engines;
using TechbloxModdingAPI.Input;
using TechbloxModdingAPI.Utility;
namespace TechbloxModdingAPI.Players
@ -204,6 +207,8 @@ namespace TechbloxModdingAPI.Players
public void EnterSeat(uint playerId, EGID seatId)
{
if (!TimeRunningModeUtil.IsTimeRunningMode(entitiesDB))
return;
PilotSeatGroupUtils.SwapTagTo<OCCUPIED_TAG>(Functions, seatId);
var opt = GetCharacterStruct<CharacterPilotSeatEntityStruct>(playerId, out var group);
if (!opt) return;
@ -221,11 +226,46 @@ namespace TechbloxModdingAPI.Players
public void ExitSeat(uint playerId)
{
if (!TimeRunningModeUtil.IsTimeRunningMode(entitiesDB))
return;
EGID egid = new EGID(playerId, CharacterExclusiveGroups.InPilotSeatGroup);
var opt = entitiesDB.QueryEntityOptional<CharacterPilotSeatEntityStruct>(egid);
if (!opt) return;
opt.Get().instantExit = true;
entitiesDB.PublishEntityChange<CharacterPilotSeatEntityStruct>(egid);
}
public bool SpawnMachine(uint playerId)
{
if (!TimeRunningModeUtil.IsTimeRunningMode(entitiesDB))
return false;
EGID egid = new EGID(playerId, CharacterExclusiveGroups.MachineSpawningGroup);
if (!entitiesDB.Exists<CharacterTagEntityStruct>(egid))
return false;
//Functions.SwapEntityGroup<CharacterEntityDescriptor>(egid, CharacterExclusiveGroups.OnFootGroup);
FakeInput.ActionInput(playerId, primary: true);
return true;
}
public bool DespawnMachine(uint playerId)
{
if (!TimeRunningModeUtil.IsTimeRunningMode(entitiesDB))
return false;
GetCharacterStruct<CharacterTagEntityStruct>(playerId, out var group);
if (group.isInvalid)
return false;
EGID egid = new EGID(playerId, group);
if (!entitiesDB.Exists<CharacterTagEntityStruct>(egid))
return false;
Functions.SwapEntityGroup<CharacterEntityDescriptor>(egid, CharacterExclusiveGroups.MachineSpawningGroup);
return true;
}
public uint GetPing()
{
return entitiesDB
.QueryUniqueEntity<NetworkStatsEntityStruct>(MultiplayerExclusiveGroups.MultiplayerStateGroup)
.networkStats.PingMs;
}
}
}

View file

@ -1,11 +1,13 @@
using RobocraftX.Character;
using RobocraftX.Character.Movement;
using RobocraftX.Common.Input;
using Svelto.ECS;
using TechbloxModdingAPI.Engines;
namespace TechbloxModdingAPI.Players
{
public class PlayerEventsEngine : IApiEngine, IReactOnSwap<CharacterPilotSeatEntityStruct>
public class PlayerEventsEngine : IApiEngine, IReactOnSwap<CharacterPilotSeatEntityStruct>, IReactOnAddAndRemove<PlayerIDStruct>
{
public void Ready()
{
@ -21,13 +23,22 @@ namespace TechbloxModdingAPI.Players
public void MovedTo(ref CharacterPilotSeatEntityStruct entityComponent, ExclusiveGroupStruct previousGroup, EGID egid)
{
var seatId = entityComponent.pilotSeatEntity.ToEGID(entitiesDB);
var player = EcsObjectBase.GetInstance(new EGID(egid.entityID, CharacterExclusiveGroups.OnFootGroup),
e => new Player(e.entityID));
entitiesDB.TryGetEGID(entityComponent.pilotSeatEntity, out var seatId); //TODO: Can't get EGID
var player = Player.GetInstance(egid.entityID);
if (previousGroup == CharacterExclusiveGroups.InPilotSeatGroup)
player.seatExited.Invoke(this, new PlayerSeatEventArgs { SeatId = seatId});
else if (egid.groupID == CharacterExclusiveGroups.InPilotSeatGroup)
player.seatEntered.Invoke(this, new PlayerSeatEventArgs { SeatId = seatId });
}
public void Add(ref PlayerIDStruct entityComponent, EGID egid)
{
Player.joined.Invoke(this, new PlayerEventArgs { PlayerId = egid });
}
public void Remove(ref PlayerIDStruct entityComponent, EGID egid)
{
Player.left.Invoke(this, new PlayerEventArgs { PlayerId = egid });
}
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Svelto.Tasks;
using Svelto.Tasks.Enumerators;
@ -7,6 +8,7 @@ using Unity.Mathematics;
using TechbloxModdingAPI.App;
using TechbloxModdingAPI.Blocks;
using TechbloxModdingAPI.Tests;
using TechbloxModdingAPI.Utility;
namespace TechbloxModdingAPI.Players
{
@ -45,7 +47,20 @@ namespace TechbloxModdingAPI.Players
[APITestCase(TestType.SimulationMode)]
public static IEnumerator<TaskContract> SeatEventTestSim()
{
yield return new WaitForSecondsEnumerator(1).Continue();
Assert.Equal(Player.LocalPlayer.SpawnMachine(), true, "Failed to spawn the player's machine.", "Successfully spawned the player's machine.");
yield return new WaitForSecondsEnumerator(1).Continue();
var seats = Game.CurrentGame().GetBlocksInGame(BlockIDs.DriverSeat);
int c = 0;
while (seats.Length == 0 && c < 10)
{
Logging.MetaLog("Waiting for a seat to be spawned...");
yield return new WaitForSecondsEnumerator(1).Continue();
Console.WriteLine("Spawn machine: " + Player.LocalPlayer.SpawnMachine());
seats = Game.CurrentGame().GetBlocksInGame(BlockIDs.DriverSeat);
c++;
}
if (seats.Length == 0)
{
Assert.Fail("No driver seat found!");

View file

@ -0,0 +1,16 @@
using System.Collections;
using Svelto.Tasks;
using Svelto.Tasks.ExtraLean;
using Svelto.Tasks.Unity.Internal;
namespace TechbloxModdingAPI.Tasks
{
public class OnGuiRunner : BaseRunner<ExtraLeanSveltoTask<IEnumerator>>
{
public OnGuiRunner(string name, uint runningOrder = 0)
: base(name)
{
UnityCoroutineRunner.StartOnGuiCoroutine(this._processEnumerator, runningOrder);
}
}
}

View file

@ -20,7 +20,7 @@ namespace TechbloxModdingAPI.Tasks
{
get
{
return RobocraftX.Schedulers.Lean.UIScheduler;
return RobocraftX.Schedulers.ClientLean.UIScheduler;
}
}
@ -28,7 +28,7 @@ namespace TechbloxModdingAPI.Tasks
{
get
{
return RobocraftX.Schedulers.ExtraLean.UIScheduler;
return RobocraftX.Schedulers.ClientExtraLean.UIScheduler;
}
}

View file

@ -10,12 +10,10 @@ using IllusionInjector;
using RobocraftX.FrontEnd;
using Unity.Mathematics;
using UnityEngine;
using RobocraftX.Common.Input;
using Svelto.Tasks;
using Svelto.Tasks.Lean;
using TechbloxModdingAPI.Blocks;
using TechbloxModdingAPI.Commands;
using TechbloxModdingAPI.Input;
using TechbloxModdingAPI.Players;
using TechbloxModdingAPI.Tasks;
using TechbloxModdingAPI.Utility;
@ -345,6 +343,11 @@ namespace TechbloxModdingAPI.Tests
Logging.CommandLog(asset);
}
}).Build();
Client.EnterMenu += (sender, args) => Scheduler.Schedule(new Once(() => Client.Instance.CloseBetaPopup()));
Game.Enter += (sender, args) =>
Console.WriteLine(
$"Current game selection data: {FullGameFields._gameSelectionData.gameMode} - {FullGameFields._gameSelectionData.saveType}");
#if TEST
TestRoot.RunTests();
#endif
@ -360,15 +363,6 @@ namespace TechbloxModdingAPI.Tests
return modsString = sb.ToString();
}
public override void OnUpdate()
{
if (UnityEngine.Input.GetKeyDown(KeyCode.End))
{
Console.WriteLine("Pressed button to toggle console");
FakeInput.CustomInput(new LocalCosmeticInputEntityComponent {commandLineToggleInput = true});
}
}
[HarmonyPatch]
public class MinimumSpecsPatch
{

View file

@ -7,7 +7,9 @@ using System.Linq; // welcome to the dark side
using Svelto.Tasks;
using Svelto.Tasks.Lean;
using Svelto.Tasks.Enumerators;
using Svelto.Tasks.Lean.Unity;
using UnityEngine;
using TechbloxModdingAPI.App;
using TechbloxModdingAPI.Tasks;
using TechbloxModdingAPI.Utility;
@ -64,7 +66,7 @@ namespace TechbloxModdingAPI.Tests
_testsCountPassed = 0;
_testsCountFailed = 0;
// flow control
Game.Enter += (sender, args) => { GameTests().RunOn(RobocraftX.Schedulers.Lean.EveryFrameStepRunner_TimeRunningAndStopped); };
Game.Enter += (sender, args) => { GameTests().RunOn(new UpdateMonoRunner("TechbloxModdingAPITestRunner")); };
Game.Exit += (s, a) => state = "ReturningFromGame";
Client.EnterMenu += (sender, args) =>
{
@ -129,7 +131,7 @@ namespace TechbloxModdingAPI.Tests
private static IEnumerator<TaskContract> GoToGameTests()
{
Client app = new Client();
Client app = Client.Instance;
int oldLength = 0;
while (app.MyGames.Length == 0 || oldLength != app.MyGames.Length)
{
@ -137,7 +139,15 @@ namespace TechbloxModdingAPI.Tests
yield return new WaitForSecondsEnumerator(1).Continue();
}
yield return Yield.It;
app.MyGames[0].EnterGame();
try
{
app.MyGames[0].EnterGame();
}
catch (Exception e)
{
Console.WriteLine("Failed to go to game tests");
Console.WriteLine(e);
}
/*Game newGame = Game.NewGame();
yield return new WaitForSecondsEnumerator(5).Continue(); // wait for sync
newGame.EnterGame();*/
@ -157,6 +167,7 @@ namespace TechbloxModdingAPI.Tests
};
for (var index = 0; index < testTypesToRun.Length; index++)
{
Logging.MetaLog($"Running test type {testTypesToRun[index]}");
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
@ -198,7 +209,16 @@ namespace TechbloxModdingAPI.Tests
}
if (index + 1 < testTypesToRun.Length) //Don't toggle on the last test
{
bool running = currentGame.IsTimeRunning;
currentGame.ToggleTimeMode();
while (running ? !currentGame.IsTimeStopped : !currentGame.IsTimeRunning)
{
Logging.MetaLog($"Waiting for time to {(running?"stop":"start")}...");
yield return new WaitForSecondsEnumerator(1).Continue();
}
}
yield return new WaitForSecondsEnumerator(5).Continue();
}
// exit game

View file

@ -1,20 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DataLoader;
using DataLoader;
using HarmonyLib;
using RobocraftX;
using RobocraftX.Common.Utilities;
using RobocraftX.CR.MainGame;
using RobocraftX.GUI;
using RobocraftX.Multiplayer;
using RobocraftX.Rendering;
using Svelto.Context;
using Svelto.DataStructures;
using Svelto.ECS;
using Svelto.ECS.Schedulers;
using Svelto.ECS.GUI;
using Techblox.GameSelection;
using UnityEngine;
using Unity.Entities;
using Unity.Physics.Systems;
@ -104,14 +98,6 @@ namespace TechbloxModdingAPI.Utility
}
}
public static PhysicsUtility _physicsUtility
{
get
{
return (PhysicsUtility)fgcr?.Field("_physicsUtility").GetValue();
}
}
/*public static UnityEntitySubmissionScheduler _frontEndSubmissionScheduler
{
get
@ -136,11 +122,11 @@ namespace TechbloxModdingAPI.Utility
}
}
public static ECSGameObjectResourceManager _eCsGameObjectResourceManager
public static ECSResourceManagers _managers
{
get
{
return (ECSGameObjectResourceManager)fgcr?.Field("_eCsGameObjectResourceManager").GetValue();
return (ECSResourceManagers)fgcr?.Field("_managers").GetValue();
}
}
@ -160,6 +146,22 @@ namespace TechbloxModdingAPI.Utility
}
}
public static SveltoGUI _frontEndGUI
{
get
{
return (SveltoGUI)fgcr?.Field("_frontEndGUI").GetValue();
}
}
public static GameSelectionData _gameSelectionData
{
get
{
return (GameSelectionData)fgcr?.Field("_gameSelectionData").GetValue();
}
}
private static Traverse fgcr;
public static void Init(FullGameCompositionRoot instance)

View file

@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RobocraftX.StateSync;
using Svelto.ECS;

View file

@ -34,6 +34,12 @@ namespace TechbloxModdingAPI.Utility
Logging.LogWarning(wrappedException.ToString());
}
};
if (wrappers.ContainsKey(added))
{
original.eventHandler -= wrapped;
wrappers.Remove(added);
}
wrappers.Add(added, wrapped);
return new WrappedHandler<T> { eventHandler = original.eventHandler + wrapped };
}