Browse Source

Create CLre_server mod and port CLre basic functionality

tags/v0.0.2
NGnius (Graham) 1 year ago
parent
commit
14a5fc8a54
24 changed files with 1967 additions and 0 deletions
  1. +6
    -0
      CLre.sln
  2. +10
    -0
      CLre_server/API/Engines/ICLreEngine.cs
  3. +61
    -0
      CLre_server/API/Engines/ServerEngines.cs
  4. +147
    -0
      CLre_server/API/MainServer/Server.cs
  5. +50
    -0
      CLre_server/API/MainServer/ServerEngines.cs
  6. +14
    -0
      CLre_server/API/MainServer/ServerEventArgs.cs
  7. +55
    -0
      CLre_server/API/Tools/AccessToolsWarnings.cs
  8. +101
    -0
      CLre_server/API/Tools/NetServerListener.cs
  9. +114
    -0
      CLre_server/API/Tools/NetServerSender.cs
  10. +88
    -0
      CLre_server/API/Utility/Logging.cs
  11. +119
    -0
      CLre_server/API/Utility/Reflection.cs
  12. +102
    -0
      CLre_server/CLre_server.cs
  13. +292
    -0
      CLre_server/CLre_server.csproj
  14. +118
    -0
      CLre_server/Fixes/BugfixAttribute.cs
  15. +84
    -0
      CLre_server/Fixes/InitLogSooner.cs
  16. +74
    -0
      CLre_server/WebStatus/AssetEndpoints.cs
  17. +13
    -0
      CLre_server/WebStatus/Assets/error404.html
  18. +16
    -0
      CLre_server/WebStatus/Assets/index.html
  19. +35
    -0
      CLre_server/WebStatus/Attributes.cs
  20. +21
    -0
      CLre_server/WebStatus/ConfigurationEndpoints.cs
  21. +44
    -0
      CLre_server/WebStatus/DebugEndpoints.cs
  22. +49
    -0
      CLre_server/WebStatus/LogEndpoints.cs
  23. +192
    -0
      CLre_server/WebStatus/StatusEndpoints.cs
  24. +162
    -0
      CLre_server/WebStatus/WebServer.cs

+ 6
- 0
CLre.sln View File

@@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.29609.76
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLre", "CLre\CLre.csproj", "{E0EEA15D-AB3C-4C73-A000-C49B5AE9EA66}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLre_server", "CLre_server\CLre_server.csproj", "{89B354CF-C654-4E48-8166-5E20BC6E4836}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
{E0EEA15D-AB3C-4C73-A000-C49B5AE9EA66}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E0EEA15D-AB3C-4C73-A000-C49B5AE9EA66}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E0EEA15D-AB3C-4C73-A000-C49B5AE9EA66}.Release|Any CPU.Build.0 = Release|Any CPU
{89B354CF-C654-4E48-8166-5E20BC6E4836}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{89B354CF-C654-4E48-8166-5E20BC6E4836}.Debug|Any CPU.Build.0 = Debug|Any CPU
{89B354CF-C654-4E48-8166-5E20BC6E4836}.Release|Any CPU.ActiveCfg = Release|Any CPU
{89B354CF-C654-4E48-8166-5E20BC6E4836}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE


+ 10
- 0
CLre_server/API/Engines/ICLreEngine.cs View File

@@ -0,0 +1,10 @@
using Game.DataLoader;
using Svelto.ECS;

namespace CLre_server.API.Engines
{
public interface ICLreEngine : IQueryingEntitiesEngine, IEngine, IDataAccess
{
IEntityFactory entityFactory { get; set; }
}
}

+ 61
- 0
CLre_server/API/Engines/ServerEngines.cs View File

@@ -0,0 +1,61 @@
using Game.DataLoader;
using HarmonyLib;
using Svelto.DataStructures;
using Svelto.ECS;

namespace CLre_server.API.Engines
{
public abstract class ServerEnginePreBuild : ICLreEngine
{
public ServerEnginePreBuild()
{
MainGameServer_BuildDeprecatedEngines_Patch.beforeBuildEngines.Add(this);
}
public abstract void Ready();
public abstract IEntitiesDB entitiesDB { get; set; }
public abstract IEntityFactory entityFactory { get; set; }
public IDataDB dataDB { get; set; }
}
public abstract class ServerEnginePostBuild : ICLreEngine
{
public ServerEnginePostBuild()
{
MainGameServer_BuildDeprecatedEngines_Patch.afterBuildEngines.Add(this);
}
public abstract void Ready();
public abstract IEntitiesDB entitiesDB { get; set; }
public abstract IEntityFactory entityFactory { get; set; }
public IDataDB dataDB { get; set; }
}

[HarmonyPatch(typeof(GameServer.GameFramework.MainGameServer), "BuildDeprecatedEngines")]
class MainGameServer_BuildDeprecatedEngines_Patch
{
internal static FasterList<ServerEnginePreBuild> beforeBuildEngines = new FasterList<ServerEnginePreBuild>();
internal static FasterList<ServerEnginePostBuild> afterBuildEngines = new FasterList<ServerEnginePostBuild>();

[HarmonyPrefix]
public static void BeforeMethodCall(GameServer.GameFramework.MainGameServer __instance, IEntityFactory ____entityFactory)
{
foreach (ICLreEngine e in beforeBuildEngines)
{
e.entityFactory = ____entityFactory;
__instance.AddEngine(e);
}
}
[HarmonyPostfix]
public static void AfterMethodCall(GameServer.GameFramework.MainGameServer __instance, IEntityFactory ____entityFactory)
{
foreach (ICLreEngine e in afterBuildEngines)
{
e.entityFactory = ____entityFactory;
__instance.AddEngine(e);
}
}
}
}

+ 147
- 0
CLre_server/API/MainServer/Server.cs View File

@@ -0,0 +1,147 @@
using System;
using System.Reflection;
using GameServer;
using HarmonyLib;
using Svelto.Context;

namespace CLre_server.API.MainServer
{
public class Server
{
// static
private static Server _instance = null;

public static Server Instance
{
get
{
if (_instance == null) _instance = new Server();
return _instance;
}
}
// instance events
public event EventHandler<StartingEventArgs> InitStart
{
add => MainGameServer_Constructor_Patch.preConstructor += value;
remove => MainGameServer_Constructor_Patch.preConstructor -= value;
}
public event EventHandler<StartedEventArgs> InitComplete
{
add => ServerReadyEngine.serverEngineReady += value;
remove => ServerReadyEngine.serverEngineReady -= value;
}
public event EventHandler<StartedEventArgs> FrameworkReady
{
add => ServerReadyEngine.serverFrameworkReady += value;
remove => ServerReadyEngine.serverFrameworkReady -= value;
}
public event EventHandler<StopEventArgs> FrameworkExit
{
add => ServerReadyEngine.serverFrameworkDestroyed += value;
remove => ServerReadyEngine.serverFrameworkDestroyed -= value;
}

// properties
public GameServerSettings GameServerSettings
{
get => MainGameServer_SetupMods_Patch._gameServerSettings;

set
{
MainGameServer_SetupMods_Patch._gameServerSettings = value;
Traverse.Create(MainGameServer_Constructor_Patch.mgs).Field<GameServerSettings>("_gameServerSettings").Value = value;
}
}

private Server()
{
new ServerReadyEngine();
}
}
[HarmonyPatch]
class MainGameServer_SetupMods_Patch
{
internal static GameServerSettings _gameServerSettings;

[HarmonyPostfix]
public static void AfterMethodCall(GameServerSettings ____gameServerSettings)
{
_gameServerSettings = ____gameServerSettings;
}
[HarmonyTargetMethod]
public static MethodBase Target()
{
return AccessTools.Method("GameServer.GameFramework.MainGameServer:SetupMods");
}
}
[HarmonyPatch]
class MainGameServer_Constructor_Patch
{

internal static ICompositionRoot mgs = null;

internal static event EventHandler<StartingEventArgs> preConstructor;
internal static event EventHandler<StartingEventArgs> postConstructor;

[HarmonyPrefix]
public static void BeforeMethodCall()
{
if (preConstructor != null) preConstructor(null, default(StartingEventArgs));
}
[HarmonyPostfix]
public static void AfterMethodCall(ICompositionRoot __instance)
{
mgs = __instance;
if (postConstructor != null) postConstructor(__instance, default(StartingEventArgs));
}
[HarmonyTargetMethod]
public static MethodBase Target()
{
return AccessTools.Constructor(AccessTools.TypeByName("GameServer.GameFramework.MainGameServer"));
}
}

[HarmonyPatch(typeof(PhotonNetwork), "ConnectUsingSettings")]
class PhotonNetwork_ConnectUsingSettings_Patch
{
internal static event EventHandler<StartedEventArgs> preConnect;
internal static event EventHandler<StartedEventArgs> postConnect;
[HarmonyPostfix]
public static void AfterMethodCall(string gameVersion)
{
if (postConnect != null) postConnect(null, new StartedEventArgs
{
photonVersion = gameVersion,
photonRegion = PhotonNetwork.CloudRegion,
worldName = MainGameServer_SetupMods_Patch._gameServerSettings.GetWorldName(),
gameGuid = MainGameServer_SetupMods_Patch._gameServerSettings.GetGameGuid(),
});
}

[HarmonyPrefix]
public static void BeforeMethodCall(string gameVersion)
{
if (preConnect != null) preConnect(null, new StartedEventArgs
{
photonVersion = gameVersion,
photonRegion = PhotonNetwork.CloudRegion,
worldName = MainGameServer_SetupMods_Patch._gameServerSettings.GetWorldName(),
gameGuid = MainGameServer_SetupMods_Patch._gameServerSettings.GetGameGuid(),
});
}
}
}

+ 50
- 0
CLre_server/API/MainServer/ServerEngines.cs View File

@@ -0,0 +1,50 @@
using System;
using CLre_server.API.Engines;
using Game.DataLoader;
using GameServer;
using Svelto.Context;
using Svelto.ECS;

namespace CLre_server.API.MainServer
{
class ServerReadyEngine : ServerEnginePostBuild, IWaitForFrameworkInitialization, IWaitForFrameworkDestruction
{
internal static event EventHandler<StartedEventArgs> serverEngineReady;
internal static event EventHandler<StartedEventArgs> serverFrameworkReady;
internal static event EventHandler<StopEventArgs> serverFrameworkDestroyed;
public override void Ready()
{
GameServerSettings gss = Server.Instance.GameServerSettings;
if (serverEngineReady != null) serverEngineReady(this, new StartedEventArgs
{
photonVersion = PhotonNetwork.gameVersion,
photonRegion = PhotonNetwork.CloudRegion,
gameGuid = gss.GetGameGuid(),
worldName = gss.GetWorldName(),
});
}

public override IEntitiesDB entitiesDB { get; set; }
public override IEntityFactory entityFactory { get; set; }

public void OnFrameworkInitialized()
{
GameServerSettings gss = Server.Instance.GameServerSettings;
if (serverFrameworkReady != null) serverFrameworkReady(this, new StartedEventArgs
{
photonVersion = PhotonNetwork.gameVersion,
photonRegion = PhotonNetwork.CloudRegion,
gameGuid = gss.GetGameGuid(),
worldName = gss.GetWorldName(),
});
}

public void OnFrameworkDestroyed()
{
if (serverFrameworkDestroyed != null) serverFrameworkDestroyed(this, new StopEventArgs{});
}
}
}

+ 14
- 0
CLre_server/API/MainServer/ServerEventArgs.cs View File

@@ -0,0 +1,14 @@
namespace CLre_server.API.MainServer
{
public struct StartingEventArgs{}

public struct StartedEventArgs
{
public string photonVersion;
public CloudRegionCode photonRegion;
public string worldName;
public string gameGuid;
}

public struct StopEventArgs{}
}

+ 55
- 0
CLre_server/API/Tools/AccessToolsWarnings.cs View File

@@ -0,0 +1,55 @@
using System;
using System.Diagnostics;
using System.Reflection;
using HarmonyLib;

#if DEBUG
namespace CLre_server.API.Tools
{
public static class AccessToolsWarnings
{
internal static bool isEnabled = false;

public static void Enable()
{
isEnabled = true;
}
public static void Disable()
{
isEnabled = false;
}
}

[HarmonyPatch(typeof(AccessTools), "TypeByName")]
class AccessTools_TypeByName_Patch
{
[HarmonyPostfix]
public static void AfterMethodCall(Type __result, string name)
{
if (!AccessToolsWarnings.isEnabled) return;
if (__result == null)
{
var method = (new StackTrace()).GetFrame(2).GetMethod();
Utility.Logging.LogWarning($"[{method.DeclaringType.FullName}.{method.Name}] AccessTools.TypeByName(\"{name}\") returned null result");
}
}
}
[HarmonyPatch(typeof(AccessTools), "Method",
new Type[] {typeof(string), typeof(Type[]), typeof(Type[])})]
class AccessTools_Method_Patch
{
[HarmonyPostfix]
public static void AfterMethodCall(MethodInfo __result, string typeColonMethodname)
{
if (!AccessToolsWarnings.isEnabled) return;
if (__result == null)
{
var method = (new StackTrace()).GetFrame(2).GetMethod();
Utility.Logging.LogWarning($"[{method.DeclaringType.FullName}.{method.Name}] AccessTools.Method(\"{typeColonMethodname}\") returned null result");
}
}
}
}
#endif

+ 101
- 0
CLre_server/API/Tools/NetServerListener.cs View File

@@ -0,0 +1,101 @@
using System.Reflection;
using System.Text;
using GameNetworkLayer.Shared;
using HarmonyLib;
using Svelto.DataStructures;
using Svelto.DataStructures.Experimental;

namespace CLre_server.API.Tools
{
public class NetServerListener
{
internal static bool isEnabled = false;

private static FasterDictionary<short, FasterList<NetReceiveMessageCallback>> callbacks = new FasterDictionary<short,FasterList<NetReceiveMessageCallback>>();

public delegate void NetReceiveMessageCallback(NetworkDispatcherCode code, byte[] data, int playerId);

public static void Enable()
{
isEnabled = true;
}
public static void Disable()
{
isEnabled = false;
}

public static void DebugReceiveMessage(NetworkDispatcherCode code, NetReceiveMessageCallback callback)
{
short key = (short)code;
if (callbacks.TryGetValue(key, out FasterList<NetReceiveMessageCallback> handlers))
{
handlers.Add(callback);
}
else
{
FasterList<NetReceiveMessageCallback> newHandlers = new FasterList<NetReceiveMessageCallback>(new [] {callback});
callbacks.Add(key, newHandlers);
}
}

internal static bool RunDebugCallbacks(NetworkDispatcherCode code, byte[] data, int playerId)
{
short key = (short)code;
if (callbacks.TryGetValue(key, out FasterList<NetReceiveMessageCallback> handlers))
{
foreach (NetReceiveMessageCallback callback in handlers)
{
callback(code, data, playerId);
}

return true;
}
else
{
return false;
}
}

public static void Log(NetworkDispatcherCode code, byte[] data, int playerId)
{
StringBuilder sb = new StringBuilder("Received ");
sb.Append(code.ToString());
sb.Append(" for player #");
sb.Append(playerId);
sb.Append(": 0x");
foreach (byte b in data)
{
sb.Append(b.ToString("X"));
}
Utility.Logging.Log(sb.ToString());
}
}

[HarmonyPatch]
class NetMessageClientListener_HandleAllMessages_Patch
{
[HarmonyPrefix]
public static void BeforeMethodCall(object ____deserializer, int playerId, object value)
{
if (!NetServerListener.isEnabled) return;
// TODO optimize this to not use Traverse
Traverse result = Traverse.Create(____deserializer).Method("Deserialize", value);
NetworkDispatcherCode code = result.Field<NetworkDispatcherCode>("dispatcherCode").Value;
byte[] data = result.Field<byte[]>("bytes").Value;
if (data == null)
{
Utility.Logging.LogWarning("Network message data was deserialized as null");
return;
}
bool isHandled = NetServerListener.RunDebugCallbacks(code, data, playerId);
if (!isHandled) Utility.Logging.Log($"Received network message for player {playerId} (code: {code.ToString()}, len: {data.Length})");
}
[HarmonyTargetMethod]
public static MethodBase Target()
{
return AccessTools.Method("GameNetworkLayer.Server.NetMessageServerListener:HandleAllMessages");
}
}
}

+ 114
- 0
CLre_server/API/Tools/NetServerSender.cs View File

@@ -0,0 +1,114 @@
using System;
using System.Reflection;
using System.Text;
using GameNetworkLayer.Shared;
using HarmonyLib;
using NetworkFramework.Shared;

namespace CLre_server.API.Tools
{
public class NetServerSender
{
private struct DummyNetDataStruct : ISerializedNetData
{
public byte[] Serialize()
{
return new byte[0];
}

public void Deserialize(byte[] data)
{
}
}

private static readonly MethodInfo _genericSendMessage = AccessTools.Method("GameNetworkLayer.Server.NetMessageServerSender:SendMessage");

private static readonly MethodInfo _genericGetSendMessageMethod =
AccessTools.Method(typeof(NetServerSender), "GetSendMessageMethod",parameters: new Type[0]);/*
/*((Func<MethodInfo>) GetSendMessageMethod<DummyNetDataStruct>).Method
.GetBaseDefinition()
.GetGenericMethodDefinition();*/
private static readonly MethodInfo _genericLog =
AccessTools.Method(typeof(NetServerSender), "Log");/*
((Action<NetworkDispatcherCode, DummyNetDataStruct>) Log<DummyNetDataStruct>).Method
.GetBaseDefinition()
.GetGenericMethodDefinition();*/
private static readonly MethodInfo _genericGetLogMethod =
AccessTools.Method(typeof(NetServerSender), "GetLogMethod", new Type[0]);/*
((Func<MethodInfo>) GetLogMethod<DummyNetDataStruct>).Method
.GetBaseDefinition()
.GetGenericMethodDefinition();*/

public static MethodInfo GetSendMessageMethod(Type t)
{
return (MethodInfo) _genericGetSendMessageMethod.MakeGenericMethod(t)
.Invoke(null, new object[0]);
}

public static MethodInfo GetSendMessageMethod<T>() where T : struct, ISerializedNetData
{
return _genericSendMessage.MakeGenericMethod(typeof(T));
}

public static MethodInfo DebugSendMessage<T>(Harmony instance = null, MethodInfo before = null, MethodInfo after = null, MethodInfo transpiler = null, MethodInfo finalizer = null) where T : struct, ISerializedNetData
{
return DebugSendMessage(typeof(T), instance, before, after, transpiler, finalizer);
}
public static MethodInfo DebugSendMessage(Type generic, Harmony instance = null, MethodInfo before = null, MethodInfo after = null, MethodInfo transpiler = null, MethodInfo finalizer = null)
{
return DebugSendMessage(
generic, instance,
before == null ? null : new HarmonyMethod(before),
after == null ? null : new HarmonyMethod(after),
transpiler == null ? null : new HarmonyMethod(transpiler),
finalizer == null ? null : new HarmonyMethod(finalizer));
}
public static MethodInfo DebugSendMessage<T>(Harmony instance = null, HarmonyMethod before = null, HarmonyMethod after = null, HarmonyMethod transpiler = null, HarmonyMethod finalizer = null) where T : struct, ISerializedNetData
{
return DebugSendMessage(typeof(T), instance, before, after, transpiler, finalizer);
}
public static MethodInfo DebugSendMessage(Type generic, Harmony instance = null, HarmonyMethod before = null, HarmonyMethod after = null, HarmonyMethod transpiler = null, HarmonyMethod finalizer = null)
{
if (instance == null) instance = CLre.harmonyInstance;
MethodInfo target = GetSendMessageMethod(generic);
return instance.Patch(target,
before,
after,
transpiler,
finalizer);
}

public static MethodInfo GetLogMethod(Type t)
{
return (MethodInfo) _genericGetLogMethod.MakeGenericMethod(t)
.Invoke(null, new object[0]);
}
public static MethodInfo GetLogMethod<T>() where T : struct, ISerializedNetData
{
return _genericLog.MakeGenericMethod(typeof(T));
}
private static void Log<T>(NetworkDispatcherCode code, ref T data) where T : struct, ISerializedNetData
{
//Utility.Logging.Log($"Sending ISerializedNetData {data.GetType().FullName} (code: {code.ToString()})");
Traverse d = Traverse.Create(data);
StringBuilder sb = new StringBuilder($"Sending ISerializedNetData {data.GetType().FullName} (code: {code.ToString()})");
foreach (string fieldName in d.Fields())
{
Traverse field = d.Field(fieldName);
sb.Append("\n");
sb.Append("\"");
sb.Append(fieldName.Substring(fieldName.IndexOf('<')+1, fieldName.LastIndexOf('>')-1));
sb.Append("\": ");
sb.Append(field.GetValue());
}
Utility.Logging.Log(sb.ToString());
}
}
}

+ 88
- 0
CLre_server/API/Utility/Logging.cs View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Diagnostics;

namespace CLre_server.API.Utility
{
/// <summary>
/// Utility class to access Cardlife's built-in logging capabilities.
/// The log is saved to outputLog#.Log
/// </summary>
public static class Logging
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Log(string msg)
{
Svelto.Console.Log(msg);
}

/// <summary>
/// Write a regular message to Cardlife's log
/// </summary>
/// <param name="obj">The object to log</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Log(object obj)
{
Svelto.Console.Log(obj.ToString());
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogError(string msg, Dictionary<string, string> extraData = null)
{
Svelto.Console.LogError(msg, extraData);
}

/// <summary>
/// Write an error message to Cardlife's log
/// </summary>
/// <param name="obj">The object to log</param>
/// <param name="extraData">The extra data to pass to the ILogger</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogError(object obj, Dictionary<string, string> extraData = null)
{
Svelto.Console.LogError(obj.ToString(), extraData);
}

/// <summary>
/// Write an exception to Cardlife's log and to the screen and exit game
/// </summary>
/// <param name="e">The exception to log</param>
/// <param name="extraData">The extra data to pass to the ILogger.
/// This is automatically populated with "OuterException#" and "OuterStacktrace#" entries</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogException(Exception e, string msg = null, Dictionary<string, string> extraData = null)
{
Svelto.Console.LogException(msg, e, extraData);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogWarning(string msg)
{
Svelto.Console.LogWarning(msg);
}

/// <summary>
/// Write a warning message to Cardlife's log
/// </summary>
/// <param name="obj">The object to log</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogWarning(object obj)
{
Svelto.Console.LogWarning(obj.ToString());
}

// descriptive logging

/// <summary>
/// Write a descriptive message to Cardlife's log including the calling method's name
/// </summary>
/// <param name="obj">The object to log</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void MetaLog(object obj)
{
var method = (new StackTrace()).GetFrame(1).GetMethod();
Log($"[{method.DeclaringType.FullName}.{method.Name}] {obj.ToString()}");
}
}
}

+ 119
- 0
CLre_server/API/Utility/Reflection.cs View File

@@ -0,0 +1,119 @@
using System;
using System.Reflection;
using GameNetworkLayer.Shared;
using HarmonyLib;
using NetworkFramework.Shared;
using Svelto.DataStructures;
using Svelto.ECS;

namespace CLre_server.API.Utility
{
public static class Reflection
{
// useful function & method prototypes
public delegate T Getter<T>();

public delegate bool ExistsV1(EGID egid);

public delegate bool ExistsV2(int id, int groupid);

public delegate T QueryEntityViewV1<T>(EGID egid);
public delegate T QueryEntityViewV2<T>(int id, ExclusiveGroup.ExclusiveGroupStruct groupid);

public delegate ReadOnlyCollectionStruct<T> QueryEntityViews<T>(int group) where T : class, IEntityViewStruct;

public delegate T[] QueryEntitiesV2<T>(int group, out int count) where T : IEntityStruct;
public delegate T[] QueryEntitiesV1<T>(ExclusiveGroup.ExclusiveGroupStruct group, out int count) where T : IEntityStruct;

public delegate object[] SendMessage<T>(NetworkDispatcherCode dispatcherCode, ref T value) where T : struct, ISerializedNetData;

// useful reflection functions
public static TFuncProto BuildDelegate<TFuncProto>(MethodInfo method) where TFuncProto : Delegate
{
return (TFuncProto) Delegate.CreateDelegate(typeof(TFuncProto), method);
}
public static Delegate BuildDelegateRaw(MethodInfo method, Type TFuncProto)
{
return Delegate.CreateDelegate(TFuncProto, method);
}
public static TFuncProto BuildDelegate<TFuncProto>(MethodInfo method, object instance) where TFuncProto : Delegate
{
return (TFuncProto) Delegate.CreateDelegate(typeof(TFuncProto), instance, method, true);
}
public static Delegate BuildDelegateRaw(MethodInfo method, object instance, Type TFuncProto)
{
return Delegate.CreateDelegate(TFuncProto, instance, method, true);
}

public static TFuncProto MethodAsDelegate<TFuncProto>(Type class_, string methodName, Type[] parameters = null, Type[] generics = null, object instance = null) where TFuncProto : Delegate
{
MethodInfo method = AccessTools.Method(class_, methodName, parameters, generics);
if (instance != null)
{
return BuildDelegate<TFuncProto>(method, instance);
}
return BuildDelegate<TFuncProto>(method);
}
public static Delegate MethodAsDelegateRaw(Type class_, Type TFuncProto, string methodName, Type[] parameters = null, Type[] generics = null, object instance = null)
{
MethodInfo method = AccessTools.Method(class_, methodName, parameters, generics);
if (instance != null)
{
return BuildDelegateRaw(method, instance, TFuncProto);
}
return BuildDelegateRaw(method, TFuncProto);
}
public static TFuncProto MethodAsDelegate<TFuncProto>(string typeColonName, Type[] parameters = null, Type[] generics = null, object instance = null) where TFuncProto : Delegate
{
MethodInfo method = AccessTools.Method(typeColonName, parameters, generics);
if (instance != null)
{
return BuildDelegate<TFuncProto>(method, instance);
}
return BuildDelegate<TFuncProto>(method);
}
public static Delegate MethodAsDelegateRaw(string typeColonName, Type TFuncProto, Type[] parameters = null, Type[] generics = null, object instance = null)
{
MethodInfo method = AccessTools.Method(typeColonName, parameters, generics);
if (instance != null)
{
return BuildDelegateRaw(method, instance, TFuncProto);
}
return BuildDelegateRaw(method, TFuncProto);
}

public static PropertyInfo GetIndexer(this Type type, Type[] arguments = null, BindingFlags bindingFlags = BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.SetField | BindingFlags.SetProperty | BindingFlags.Static)
{
foreach (PropertyInfo p in type.GetProperties(bindingFlags))
{
if (arguments == null && p.GetIndexParameters().Length != 0) return p;
if (arguments != null)
{
uint count = 0;
foreach (ParameterInfo param in p.GetIndexParameters())
{
if (param.ParameterType != arguments[count])
{
break;
}

count++;
}
if (count == arguments.Length)
{
return p;
}
}
}
return null;
}
}
}

+ 102
- 0
CLre_server/CLre_server.cs View File

@@ -0,0 +1,102 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using CLre_server.API.Tools;
using CLre_server.WebStatus;
using GameNetworkLayer.Shared;
using HarmonyLib;
using Svelto.ECS;
using UnityEngine;

namespace CLre_server
{
public class CLre : IllusionPlugin.IEnhancedPlugin // the Illusion Plugin Architecture (IPA) will ignore classes that don't implement IPlugin
{
public override string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name;

public override string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString();

internal static Harmony harmonyInstance = null;

// called when Cardlife shuts down
public override void OnApplicationQuit()
{
WebServer.Deinit();
harmonyInstance.UnpatchAll();
}

// called when Cardlife starts up
public override void OnApplicationStart()
{
#if DEBUG
FileLog.Reset();
Harmony.DEBUG = true;
// enable CLre debug functionality
AccessToolsWarnings.Enable();
NetServerListener.Enable();
#endif
// init all Harmony patches in project
harmonyInstance = new Harmony(Name);
harmonyInstance.PatchAll();
// patches for bugs
Fixes.InitLogSooner.Init();
// misc
LogIPAPlugins(); // log plugins again so they show up in the log, and not just stdout
Fixes.BugfixAttributeUtility.LogBugfixes(); // log bugfixes that are applied

#if DEBUG
// test CLre debug functionality
Type netData = AccessTools.TypeByName("Game.Handhelds.DrawingStateMessage");
NetServerSender.DebugSendMessage(netData, harmonyInstance,
NetServerSender.GetLogMethod(netData));
API.Utility.Logging.MetaLog("Patched SendMessage<Game.Handhelds.DrawingStateMessage>");

netData = AccessTools.TypeByName("Shared.Inventory.HandheldEquipmentRequest");
NetServerSender.DebugSendMessage(netData, harmonyInstance,
NetServerSender.GetLogMethod(netData));
API.Utility.Logging.MetaLog("Patched SendMessage<Shared.Inventory.HandheldEquipmentRequest>");
NetServerListener.DebugReceiveMessage(NetworkDispatcherCode.EACMessageServerToClient,
NetServerListener.Log);
// API debug and testing
API.MainServer.Server.Instance.FrameworkReady += (_, __) => API.Utility.Logging.MetaLog("(!) Server framework ready for business");
API.MainServer.Server.Instance.FrameworkExit += (_, __) => API.Utility.Logging.MetaLog("(!) Server framework shutting down"); // this seems to never happen
API.MainServer.Server.Instance.InitStart += (_, __) => API.Utility.Logging.MetaLog("(!) Server initialising");
API.MainServer.Server.Instance.InitComplete += (_, __) => API.Utility.Logging.MetaLog("(!) Server successfully initialised");
#endif
WebServer.Init();
// Log info
API.Utility.Logging.MetaLog($"{Name} init complete.");
}

private static void LogIPAPlugins()
{
StringBuilder sb = new StringBuilder();
sb.AppendFormat("Running on Unity {0}\n", Application.unityVersion);
sb.AppendFormat("Running on CardLife Server {0} (aka {1})\n", Game.Utilities.VersionReader.GetVersion(), Application.version);
sb.AppendFormat("-----------------------------\n");
sb.AppendFormat("Loading plugins from {0} and found {1}\n", System.IO.Path.Combine(Environment.CurrentDirectory, "Plugins"), IllusionInjector.PluginManager.Plugins.Count());
sb.AppendFormat("-----------------------------\n");
foreach (IllusionPlugin.IPlugin plugin in IllusionInjector.PluginManager.Plugins)
{

sb.AppendFormat(" {0}: {1}\n", plugin.Name, plugin.Version);
}
sb.AppendFormat("-----------------------------\n");
API.Utility.Logging.Log(sb.ToString());
}
public override void OnGUI()
{
if (GUI.Button(new Rect(10, 10, 50, 50), "QUIT"))
{
Application.Quit(); // yeet
}
}
}
}

+ 292
- 0
CLre_server/CLre_server.csproj View File

@@ -0,0 +1,292 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Version>0.0.2</Version>
<Authors>NGnius</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://git.exmods.org/NGnius/CLre</PackageProjectUrl>
<NeutralLanguage>en-CA</NeutralLanguage>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Lib.Harmony" Version="2.0.4" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
</ItemGroup>



<!--Start Dependencies-->
<ItemGroup>
<Reference Include="Accessibility">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Accessibility.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp-firstpass">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Assembly-CSharp-firstpass.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="BehaviorDesignerRuntime">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\BehaviorDesignerRuntime.dll</HintPath>
</Reference>
<Reference Include="DOTween">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\DOTween.dll</HintPath>
</Reference>
<Reference Include="EasyAntiCheat.Client">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\EasyAntiCheat.Client.dll</HintPath>
</Reference>
<Reference Include="EasyAntiCheat.Server">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\EasyAntiCheat.Server.dll</HintPath>
</Reference>
<Reference Include="Fabric.AudioSpline">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Fabric.AudioSpline.dll</HintPath>
</Reference>
<Reference Include="Fabric.Core">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Fabric.Core.dll</HintPath>
</Reference>
<Reference Include="log4net">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\log4net.dll</HintPath>
</Reference>
<Reference Include="mscorlib">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\mscorlib.dll</HintPath>
</Reference>
<Reference Include="Novell.Directory.Ldap">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Novell.Directory.Ldap.dll</HintPath>
</Reference>
<Reference Include="Photon3Unity3D">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Photon3Unity3D.dll</HintPath>
</Reference>
<Reference Include="Rewired_Core">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Rewired_Core.dll</HintPath>
</Reference>
<Reference Include="Rewired_Windows_Lib">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Rewired_Windows_Lib.dll</HintPath>
</Reference>
<Reference Include="Svelto.Common">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Svelto.Common.dll</HintPath>
</Reference>
<Reference Include="Svelto.ECS">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Svelto.ECS.dll</HintPath>
</Reference>
<Reference Include="Svelto.Tasks">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Svelto.Tasks.dll</HintPath>
</Reference>
<Reference Include="Unity.Postprocessing.Runtime">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Unity.Postprocessing.Runtime.dll</HintPath>
</Reference>
<Reference Include="Unity.TextMeshPro">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\Unity.TextMeshPro.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.AccessibilityModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.AccessibilityModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.AIModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.AIModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.AnimationModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.AnimationModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ARModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ARModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.AssetBundleModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.AssetBundleModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.AudioModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.AudioModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.BaselibModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.BaselibModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ClothModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ClothModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ClusterInputModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ClusterInputModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ClusterRendererModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ClusterRendererModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CrashReportingModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.CrashReportingModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.DirectorModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.DirectorModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.FileSystemHttpModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.FileSystemHttpModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.GameCenterModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.GameCenterModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.GridModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.GridModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.HotReloadModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.HotReloadModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ImageConversionModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ImageConversionModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.IMGUIModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.IMGUIModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.InputModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.InputModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.JSONSerializeModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.JSONSerializeModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.LocalizationModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.LocalizationModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.Networking">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.Networking.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ParticleSystemModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ParticleSystemModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.PerformanceReportingModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.PerformanceReportingModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.Physics2DModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.Physics2DModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.PhysicsModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.PhysicsModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ProfilerModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ProfilerModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ScreenCaptureModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ScreenCaptureModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.SharedInternalsModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.SharedInternalsModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.SpatialTracking">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.SpatialTracking.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.SpriteMaskModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.SpriteMaskModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.SpriteShapeModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.SpriteShapeModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.StreamingModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.StreamingModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.StyleSheetsModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.StyleSheetsModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.SubstanceModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.SubstanceModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TerrainModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TerrainModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TerrainPhysicsModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TerrainPhysicsModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TextCoreModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TextCoreModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TextRenderingModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TextRenderingModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TilemapModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TilemapModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.Timeline">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.Timeline.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TimelineModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TimelineModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TLSModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TLSModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UI">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UI.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UIElementsModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UIElementsModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UIModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UIModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UmbraModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UmbraModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UNETModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UNETModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityAnalyticsModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityAnalyticsModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityConnectModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityConnectModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityTestProtocolModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityTestProtocolModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityWebRequestAssetBundleModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityWebRequestAssetBundleModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityWebRequestAudioModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityWebRequestAudioModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityWebRequestModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityWebRequestModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityWebRequestTextureModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityWebRequestTextureModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityWebRequestWWWModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityWebRequestWWWModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.VehiclesModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.VehiclesModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.VFXModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.VFXModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.VideoModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.VideoModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.VRModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.VRModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.WindModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.WindModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.XRModule">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.XRModule.dll</HintPath>
</Reference>
<Reference Include="IllusionInjector">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\IllusionInjector.dll</HintPath>
</Reference>
<Reference Include="IllusionPlugin">
<HintPath>..\..\cl\CardlifeGameServer_Data\Managed\IllusionPlugin.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Remove="WebStatus\Assets\index.html" />
<EmbeddedResource Include="WebStatus\Assets\index.html" />
<None Remove="WebStatus\Assets\error404.html" />
<EmbeddedResource Include="WebStatus\Assets\error404.html" />
</ItemGroup>
<!--End Dependencies-->

</Project>

+ 118
- 0
CLre_server/Fixes/BugfixAttribute.cs View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace CLre_server.Fixes
{
[AttributeUsage(AttributeTargets.Class)]
public class BugfixAttribute : Attribute
{
public string name { get; set; }
public string description { get; set; }
public Type target { get; set; }
public string more { get; set; }
public uint id { get; set; }
public BugfixType component { get; set; }
}

public enum BugfixType : byte
{
Miscellaneous = 0x00,
HarmonyPatch,
Initialiser,
Workaround,
API,
Debug
}

internal static class BugfixAttributeUtility
{
public static void LogBugfixes()
{
List<uint> keys = new List<uint>();
Dictionary<uint, BugfixStruct> fixes = new Dictionary<uint, BugfixStruct>();
foreach (Type t in Assembly.GetExecutingAssembly().GetTypes())
{
BugfixAttribute b = t.GetCustomAttribute<BugfixAttribute>(true);
if (b != null)
{
if (!fixes.ContainsKey(b.id))
{
BugfixStruct bugfixStruct = new BugfixStruct{id = b.id};
bugfixStruct.Merge(b);
fixes[b.id] = bugfixStruct;
keys.Add(b.id);
}
else
{
BugfixStruct bugfixStruct = fixes[b.id];
bugfixStruct.Merge(b);
fixes[b.id] = bugfixStruct;
}
}
}
keys.Sort();
//keys.Sort((u, u1) => u.CompareTo(u1));
StringBuilder sb = new StringBuilder();
sb.AppendFormat("Applying {0} CLre fixes\n", keys.Count);
sb.AppendFormat("-----------------------------\n");
foreach (uint i in keys)
{
sb.Append(fixes[i].ToString());
sb.Append("\n");
}
sb.AppendFormat("-----------------------------\n");
API.Utility.Logging.Log(sb.ToString());
}

private struct BugfixStruct
{
public string name;
public string description;
public Type target;
public string more;
public uint id;
private uint total;
private uint[] bugfixTypeCount;

public void Merge(BugfixAttribute b)
{
if (name == null && b.name != null) name = b.name;
if (description == null && b.description != null) description = b.description;
if (target == null && b.target != null) target = b.target;
if (more == null && b.more != null) more = b.more;
total++;
if (bugfixTypeCount == null) bugfixTypeCount = new uint[Enum.GetNames(typeof(BugfixType)).Length];
bugfixTypeCount[(byte) b.component]++;
}

public override string ToString()
{
StringBuilder sb = new StringBuilder();
sb.AppendFormat(" {0}: ", name);
if (more != null)
{
sb.AppendFormat("[MORE: {0}] ", more);
}
else if (description != null)
{
sb.Append(description);
sb.Append(" ");
}

if (target != null)
{
sb.AppendFormat("[TARGET: {0}] ", target.FullName);
}
sb.AppendFormat("[ID: {0}] ", id);
sb.AppendFormat("({0}M/{1}P/{2}I/{3}W/{4}A/{5}D/{6}T)",
bugfixTypeCount[(byte) BugfixType.Miscellaneous], bugfixTypeCount[(byte) BugfixType.HarmonyPatch],
bugfixTypeCount[(byte) BugfixType.Initialiser], bugfixTypeCount[(byte) BugfixType.Workaround],
bugfixTypeCount[(byte) BugfixType.API], bugfixTypeCount[(byte) BugfixType.Debug], total);
return sb.ToString();
}
}
}
}

+ 84
- 0
CLre_server/Fixes/InitLogSooner.cs View File

@@ -0,0 +1,84 @@
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using HarmonyLib;

namespace CLre_server.Fixes
{
[Bugfix(name = "InitLogSooner",
description = "Start the logger slightly sooner than Cardlife does",
component = BugfixType.Initialiser, id = 0)]
public static class InitLogSooner
{
public static int millisecondsTimeout = 5000;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Init()
{
try
{
CustomLoggerThread_CreateGameObject_Patch.allowed = true;
CustomLoggerThread.CreateGameObject();
CustomLoggerThread_CreateGameObject_Patch.allowed = false;
API.Utility.Logging.Log($"Completed early log init, hello!");
//System.IO.File.WriteAllText("InitLogSooner.log", $"Done at " + System.DateTime.Now.ToString());
}
catch (Exception e)
{
API.Utility.Logging.Log($"Failed to initialise log sooner, reason:\n" + e);
System.IO.File.WriteAllText("InitLogSooner.log", e.ToString());
}
}

[Bugfix(name = "InitLogSooner",
target = typeof(CustomLoggerThread),
component = BugfixType.HarmonyPatch, id = 0)]
[HarmonyPatch(typeof(CustomLoggerThread), "CreateGameObject")]
class CustomLoggerThread_CreateGameObject_Patch
{
internal static bool allowed = false;

public static bool Prefix()
{
return allowed;
}
}
[Bugfix(name = "InitLogSooner",
component = BugfixType.HarmonyPatch, id = 0)]
[HarmonyPatch(typeof(CustomLoggerThread), "StartQueue")]
class CustomLoggerThread_StartQueue_Patch
{
internal static volatile bool IsLogStarted = false;
private delegate void Flusher();

public static bool Prefix()
{
// setup thru reflection
FieldInfo quitThreadField = AccessTools.Field(typeof(CustomLoggerThread), "_quitThread");
MethodInfo flushLoggerMethod = AccessTools.Method(typeof(CustomLoggerThread), "FlushLogger");
Flusher flushLogger = (Flusher) Delegate.CreateDelegate(typeof(Action), null, flushLoggerMethod);

MethodInfo forceFlushMethod = AccessTools.Method("CustomLogger:ForceFlush");
Flusher forceFlush = (Flusher) Delegate.CreateDelegate(typeof(Action), null, forceFlushMethod);
Thread.MemoryBarrier();
IsLogStarted = true;
while (!(bool) quitThreadField.GetValue(null))
{
flushLogger();
forceFlush();
Thread.Sleep(millisecondsTimeout);
}

IsLogStarted = false;
return false;
}
}
}
}

+ 74
- 0
CLre_server/WebStatus/AssetEndpoints.cs View File

@@ -0,0 +1,74 @@
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;

namespace CLre_server.WebStatus
{
public static class AssetEndpoints
{
[WebEndpoint("/")]
public static void LandingPage(HttpListenerContext ctx)
{
ctx.Response.Headers.Add("Content-Type", "text/html");
Asset(ctx, "CLre_server.WebStatus.Assets.index.html");
}
[WebEndpoint("/asset")]
public static void AllAssets(HttpListenerContext ctx)
{
ctx.Response.Headers.Add("Content-Type", "text/html");
Asset(ctx, "");
}
[WebEndpoint("/asset/404")]
public static void Asset404(HttpListenerContext ctx)
{
ctx.Response.Headers.Add("Content-Type", "text/html");
Asset(ctx, "CLre_server.WebStatus.Assets.error404.html");
}
private static bool Asset(HttpListenerContext ctx, string name)
{
Assembly asm = Assembly.GetCallingAssembly();
Stream resource = asm.GetManifestResourceStream(name);
if (resource == null)
{
string assetStr = ListAssetsHtml(asm);
byte[] output = Encoding.UTF8.GetBytes(assetStr);
ctx.Response.OutputStream.Write(output, 0, output.Length);
return false;
}
resource.CopyTo(ctx.Response.OutputStream);
return true;
}
private static string ListAssetsHtml(Assembly target)
{
StringBuilder sb = new StringBuilder("<!DOCTYPE html><html lang='en'><head><meta charset='UTF-8'><title>Asset not found</title></head><body><h1>");
sb.Append(target.GetName().Name);
sb.Append(" available assets</h1>");
foreach (string asset in target.GetManifestResourceNames())
{
sb.Append("<li>");
sb.Append(asset);
sb.Append("</li>");
}

sb.Append("</ul></body></html>");
return sb.ToString();
}
private static string ListAssetsText(Assembly target)
{
StringBuilder sb = new StringBuilder(target.FullName);
foreach (string asset in target.GetManifestResourceNames())
{
sb.Append("\n\t");
sb.Append(asset);
}

return sb.ToString();
}
}
}

+ 13
- 0
CLre_server/WebStatus/Assets/error404.html View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404 Not Found</title>
</head>
<body>
<h1>Page not found</h1>
<div>
<a href="/">Home</a>
</div>
</body>
</html>

+ 16
- 0
CLre_server/WebStatus/Assets/index.html View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CLre Server Status</title>
</head>
<body>
<h1 style="margin:auto; text-align: center; justify-content: center;">Welcome to the CLre HTTP server!</h1>
<div style="width: 100%;">
<a href="/l/current">View log</a>
<br/>
<iframe src="/l/current" style="width: 100%; height: available;">Loading...</iframe>
</div>
</body>
</html>

+ 35
- 0
CLre_server/WebStatus/Attributes.cs View File

@@ -0,0 +1,35 @@
using System.Reflection;

namespace CLre_server.WebStatus
{
public class Attributes
{
}

[System.AttributeUsage(System.AttributeTargets.Method)]
public class WebEndpointAttribute : System.Attribute
{
private readonly string endpoint;
public WebEndpointAttribute(string path)
{
endpoint = path;
Assembly asm = Assembly.GetCallingAssembly();
if (!WebServer._assembliesToCheck.Contains(asm))
{
WebServer._assembliesToCheck.Add(asm);
}

if (WebServer.MainInstance != null && WebServer.MainInstance.IsRunning)
{
}
}

internal string GetPath()
{
return endpoint;
}
}
}

+ 21
- 0
CLre_server/WebStatus/ConfigurationEndpoints.cs View File

@@ -0,0 +1,21 @@
using System.Net;
using System.Text;

namespace CLre_server.WebStatus
{
public class ConfigurationEndpoints
{
[WebEndpoint("/c/game.json")]
public static void GameServerSettings (HttpListenerContext ctx)
{
ctx.Response.Headers.Add("Content-Type", "application/json");
GameServer.GameServerSettings gss = API.MainServer.Server.Instance.GameServerSettings;
string json = UnityEngine.JsonUtility.ToJson(gss);
#if DEBUG
API.Utility.Logging.MetaLog("JSONified settings: " + json);
#endif
byte[] output = Encoding.UTF8.GetBytes(json);
ctx.Response.OutputStream.Write(output, 0, output.Length);
}
}
}

+ 44
- 0
CLre_server/WebStatus/DebugEndpoints.cs View File

@@ -0,0 +1,44 @@
using System.Net;
using System.Text;

namespace CLre_server.WebStatus
{
public static class DebugEndpoints
{
[WebEndpoint("/d/ping")]
private static void PingPong(HttpListenerContext ctx)
{
byte[] output = Encoding.UTF8.GetBytes("pong");
ctx.Response.OutputStream.Write(output, 0, output.Length);
}

[WebEndpoint("/d/version")]
internal static void VersionInfo(HttpListenerContext ctx)
{
StringBuilder sb = new StringBuilder();
sb.Append("CardLife Version (Unity): \t");
sb.Append(UnityEngine.Application.version);
sb.Append("\n");
sb.Append("CardLife Version (Game): \t");
sb.Append(Game.Utilities.VersionReader.GetVersion());
sb.Append("\n");
sb.Append("Unity Version: \t\t\t");
sb.Append(UnityEngine.Application.unityVersion);
sb.Append("\n");
sb.Append("CLre Version: \t\t\t");
sb.Append(System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString());
sb.Append("\n");
byte[] output = Encoding.UTF8.GetBytes(sb.ToString());
ctx.Response.OutputStream.Write(output, 0, output.Length);
}
#if DEBUG
[WebEndpoint("/d/test")]
internal static void Experiment(HttpListenerContext ctx)
{
string test = "";
byte[] output = Encoding.UTF8.GetBytes(test);
ctx.Response.OutputStream.Write(output, 0, output.Length);
}
#endif
}
}

+ 49
- 0
CLre_server/WebStatus/LogEndpoints.cs View File

@@ -0,0 +1,49 @@
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using HarmonyLib;

namespace CLre_server.WebStatus
{
public class LogEndpoints
{
[WebEndpoint("/l/current")]
private static void FullLog(HttpListenerContext ctx)
{
if (CustomLogger_GetFileNameToUse_Patch.currentLogFile == null)
{
byte[] output = Encoding.UTF8.GetBytes("No log file available");
ctx.Response.OutputStream.Write(output, 0, output.Length);
return;
}
// copy file because log is already open for writing
string copyFilename = CustomLogger_GetFileNameToUse_Patch.currentLogFile + ".copy";
File.Copy(CustomLogger_GetFileNameToUse_Patch.currentLogFile, copyFilename, true);
FileStream logFile = new FileStream(copyFilename, FileMode.Open, FileAccess.Read, FileShare.Read);
logFile.CopyTo(ctx.Response.OutputStream);
logFile.Close();
}
}

[HarmonyPatch]
class CustomLogger_GetFileNameToUse_Patch
{
internal static string currentLogFile = null;

[HarmonyPostfix]
public static void AfterMethodCall(string __result)
{
#if DEBUG
API.Utility.Logging.MetaLog($"Current logfile is {__result}");
#endif
currentLogFile = __result;
}
[HarmonyTargetMethod]
public static MethodBase Target()
{
return AccessTools.Method("CustomLogger:GetFileNameToUse");
}
}
}

+ 192
- 0
CLre_server/WebStatus/StatusEndpoints.cs View File

@@ -0,0 +1,192 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Reflection;
using System.Text;
using CLre_server.API.Engines;
using CLre_server.API.MainServer;
using Game.CommonComponents;
using HarmonyLib;
using Svelto.DataStructures;
using Svelto.ECS;
using User.Server;

namespace CLre_server.WebStatus
{
public static class StatusEndpoints
{
private struct ServerStateCache
{
public List<PlayerData> OnlinePlayers;

public int MaxPlayers;

public RunState State;

public static ServerStateCache Empty()
{
return new ServerStateCache
{
OnlinePlayers = new List<PlayerData>(),
MaxPlayers = -1,
State = RunState.Initialising,
};
}

public string Json()
{
// Unity's built-in JSON serializer does not work with arrays or lists :(
// I miss Newtonsoft...
StringBuilder sb = new StringBuilder($"{{\"PlayersMax\":{MaxPlayers},\"PlayerCount\":{OnlinePlayers.Count},\"Status\":\"{State.ToString()}\",\"OnlinePlayers\":[");
foreach (PlayerData p in OnlinePlayers.ToArray())
{
sb.Append(UnityEngine.JsonUtility.ToJson(p));
sb.Append(",");
}
if (OnlinePlayers.Count > 0) sb.Remove(sb.Length - 1, 1);
sb.Append("]}");
return sb.ToString();
}
}

[Serializable]
private struct PlayerData
{
public string id;
public string name;
public bool isDev;
public float x;
public float y;
public float z;
}

private enum RunState : short
{
Initialising,
Initialised,
Online,
Quitting,
}
private class StatusPollingEngine : ServerEnginePostBuild
{
public override void Ready()
{
API.Utility.Logging.MetaLog("StatusPolling Engine ready");
pollLoop().Run();
}

public override IEntitiesDB entitiesDB { get; set; }
public override IEntityFactory entityFactory { get; set; }

private delegate void PlayerPositionFunc();

private PlayerPositionFunc _playerPositionFunc = null;

private IEnumerator pollLoop()
{
FieldInfo f = AccessTools.Field(AccessTools.TypeByName("User.Server.AccountExclusiveGroups"), "accountGroup");
ExclusiveGroup accountGroup = (ExclusiveGroup) f.GetValue(null);
while (_clState.State != RunState.Quitting)
{
ReadOnlyCollectionStruct<AccountIdServerNode> accounts =
entitiesDB.QueryEntityViews<AccountIdServerNode>(accountGroup);
int index = 0;
foreach (AccountIdServerNode user in accounts)
{
if (index < _clState.OnlinePlayers.Count)
{
PlayerData p = _clState.OnlinePlayers[index];
p.id = user.accountId.publicId.ToString();
p.name = user.accountId.displayName;
p.isDev = (user.accountId.userFlags & UserFlags.Dev) == UserFlags.Dev;
_clState.OnlinePlayers[index] = p;
}
else
{
PlayerData p = default(PlayerData);
p.id = user.accountId.publicId.ToString();
p.name = user.accountId.displayName;
p.isDev = (user.accountId.userFlags & UserFlags.Dev) == UserFlags.Dev;
_clState.OnlinePlayers.Add(p);
}
index++;
}
if (index != 0) syncPlayerPositions();
if (index < _clState.OnlinePlayers.Count) _clState.OnlinePlayers.RemoveRange(index, _clState.OnlinePlayers.Count - index);
//API.Utility.Logging.MetaLog($"Polled {index} Online Users");
yield return null;
}
}

private void syncPlayerPositions()
{
if (_playerPositionFunc == null)
{
// build non-generic method using reflection
MethodInfo method = AccessTools.Method(typeof(StatusPollingEngine), "getPlayerPositionsGeneric",
generics: new[] {AccessTools.TypeByName("Game.Character.ServerCharacterPositionNode")});
_playerPositionFunc = API.Utility.Reflection.BuildDelegate<PlayerPositionFunc>(method, this);
}

_playerPositionFunc();
}

#pragma warning disable 0618
private void getPlayerPositionsGeneric<T>() where T : EntityView // EntityView is deprecated lol
{
ReadOnlyCollectionStruct<T> scpnCollection = entitiesDB.QueryEntityViews<T>(DEPRECATED_SveltoExtensions.DEPRECATED_GROUP);
//API.Utility.Logging.MetaLog($"Found {scpnCollection.Count} player positions");
int i = 0;
foreach (T scpn in scpnCollection)
{
PlayerData p = _clState.OnlinePlayers[i];
UnityEngine.Vector3 pos = Traverse.Create(scpn).Field<IPositionComponent>("positionComponent")
.Value.position;
p.x = pos.x;
p.y = pos.y;
p.z = pos.z;
_clState.OnlinePlayers[i] = p;
i++;
}
}
#pragma warning restore 0618
}
private static ServerStateCache _clState = ServerStateCache.Empty();

internal static void Init()
{
#if DEBUG
API.Utility.Logging.MetaLog("Status Endpoint initialising");
#endif
new StatusPollingEngine();
// register API event callbacks
Server.Instance.InitStart += (_, __) => _clState.State = RunState.Initialising;
Server.Instance.InitComplete += (_, __) =>
{
_clState.State = RunState.Initialised;
_clState.MaxPlayers = Server.Instance.GameServerSettings.GetMaxPlayers();
};
Server.Instance.FrameworkReady += (_, __) =>
{
_clState.State = RunState.Online;
_clState.MaxPlayers = Server.Instance.GameServerSettings.GetMaxPlayers();
};
Server.Instance.FrameworkExit += (_, __) => _clState.State = RunState.Quitting;
}

[WebEndpoint("/status.json")]
internal static void StatusJson(HttpListenerContext ctx)
{
ctx.Response.Headers.Add("Content-Type", "application/json");
string json = _clState.Json();
#if DEBUG
API.Utility.Logging.MetaLog("JSONified status: " + json);
#endif
byte[] output = Encoding.UTF8.GetBytes(json);
ctx.Response.OutputStream.Write(output, 0, output.Length);
}
}
}

+ 162
- 0
CLre_server/WebStatus/WebServer.cs View File

@@ -0,0 +1,162 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using HarmonyLib;
using Svelto.DataStructures;

namespace CLre_server.WebStatus
{
public class WebServer
{
private const uint DEFAULT_PORT = 5030;
private const string DEFAULT_IP = "localhost";
private readonly HttpListener _httpListener;

public delegate void RequestHandler(HttpListenerContext ctx);
private Dictionary<string, RequestHandler> _handlers = new Dictionary<string, RequestHandler>();

public bool IsRunning
{
get => _httpListener.IsListening;
}

private string _ip_addr;
private uint _port;

public static WebServer MainInstance { get; internal set; }
internal static List<Assembly> _assembliesToCheck = new List<Assembly>(new []{typeof(CLre).Assembly});

public WebServer() : this(DEFAULT_IP, DEFAULT_PORT)
{
}
public WebServer(string ip, uint port)
{
if (!HttpListener.IsSupported)
{
API.Utility.Logging.LogWarning("HTTP Server is unsupported on earlier Windows versions. It will fail to start.");
}
_httpListener = new HttpListener();
_httpListener.Prefixes.Add($"http://{ip}:{port}/");
_ip_addr = ip;
_port = port;
}

internal static void Init()
{
if (Environment.GetCommandLineArgs().Contains("-web"))
{
API.Utility.Logging.Log("Starting status web server");
StatusEndpoints.Init();
MainInstance = new WebServer();
MainInstance.Start();
}
else
{
API.Utility.Logging.Log("Not starting web server (use CLI argument -web to enable)");
}
}

internal static void Deinit()
{
if (MainInstance != null) MainInstance.Stop();
MainInstance = null;
}
public void Start()
{
LoadHandlers();
try
{
_httpListener.Start();
}
catch (Exception e)
{
API.Utility.Logging.LogWarning(e);
return;
}

HandleAllRequests().Run();
}

public void Stop()
{
try
{
_httpListener.Stop();
_httpListener.Close();
}
catch (Exception e)
{
API.Utility.Logging.LogWarning(e);
return;
}
}

private IEnumerator HandleAllRequests()
{
API.Utility.Logging.MetaLog($"Started HTTP web server at http://{_ip_addr}:{_port}/");
while (_httpListener.IsListening)
{
var awaiter = _httpListener.GetContextAsync();
awaiter.GetAwaiter().OnCompleted(() => DoRequest(awaiter.Result));
yield return null;
}
API.Utility.Logging.MetaLog("Terminated HTTP web server");
}

private void DoRequest(HttpListenerContext ctx)
{
string endpoint = ctx.Request.Url.LocalPath.ToLower();
#if DEBUG
API.Utility.Logging.LogWarning($"Handling HTTP request {endpoint}");
#endif
bool handled = false;
foreach (string path in _handlers.Keys)
{
if (endpoint == path)
{
handled = true;
_handlers[path](ctx);
break;
}
}

if (!handled)
{
AssetEndpoints.Asset404(ctx);
}
//byte[] output = Encoding.UTF8.GetBytes(endpoint);
//ctx.Response.OutputStream.Write(output, 0, output.Length);
ctx.Response.Close();
}

private void LoadHandlers()
{
_handlers = new Dictionary<string, RequestHandler>();
foreach (Assembly asm in _assembliesToCheck.ToArray())
{
foreach (Type t in asm.GetTypes())
{
foreach (MethodInfo m in t.GetMethods(AccessTools.all))
{
WebEndpointAttribute attr = m.GetCustomAttribute<WebEndpointAttribute>();
if (attr != null)
{
// TODO validate that method signature matches that of RequestHandler
string key = attr.GetPath().ToLower();
API.Utility.Logging.MetaLog($"{t.FullName}:{m.Name} is handling {key}");
_handlers.Add(key, (RequestHandler) Delegate.CreateDelegate(typeof(RequestHandler), m));
}
}
}
}
}
}
}

Loading…
Cancel
Save