diff --git a/CLre.sln b/CLre.sln index f8f3807..02426c3 100644 --- a/CLre.sln +++ b/CLre.sln @@ -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 diff --git a/CLre_server/API/Engines/ICLreEngine.cs b/CLre_server/API/Engines/ICLreEngine.cs new file mode 100644 index 0000000..9224a1c --- /dev/null +++ b/CLre_server/API/Engines/ICLreEngine.cs @@ -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; } + } +} \ No newline at end of file diff --git a/CLre_server/API/Engines/ServerEngines.cs b/CLre_server/API/Engines/ServerEngines.cs new file mode 100644 index 0000000..8c31b0f --- /dev/null +++ b/CLre_server/API/Engines/ServerEngines.cs @@ -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 beforeBuildEngines = new FasterList(); + + internal static FasterList afterBuildEngines = new FasterList(); + + [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); + } + } + } +} \ No newline at end of file diff --git a/CLre_server/API/MainServer/Server.cs b/CLre_server/API/MainServer/Server.cs new file mode 100644 index 0000000..e32dbef --- /dev/null +++ b/CLre_server/API/MainServer/Server.cs @@ -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 InitStart + { + add => MainGameServer_Constructor_Patch.preConstructor += value; + remove => MainGameServer_Constructor_Patch.preConstructor -= value; + } + + public event EventHandler InitComplete + { + add => ServerReadyEngine.serverEngineReady += value; + remove => ServerReadyEngine.serverEngineReady -= value; + } + + public event EventHandler FrameworkReady + { + add => ServerReadyEngine.serverFrameworkReady += value; + remove => ServerReadyEngine.serverFrameworkReady -= value; + } + + public event EventHandler 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").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 preConstructor; + + internal static event EventHandler 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 preConnect; + + internal static event EventHandler 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(), + }); + } + } +} \ No newline at end of file diff --git a/CLre_server/API/MainServer/ServerEngines.cs b/CLre_server/API/MainServer/ServerEngines.cs new file mode 100644 index 0000000..b3b990d --- /dev/null +++ b/CLre_server/API/MainServer/ServerEngines.cs @@ -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 serverEngineReady; + + internal static event EventHandler serverFrameworkReady; + + internal static event EventHandler 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{}); + } + } +} \ No newline at end of file diff --git a/CLre_server/API/MainServer/ServerEventArgs.cs b/CLre_server/API/MainServer/ServerEventArgs.cs new file mode 100644 index 0000000..c68ce50 --- /dev/null +++ b/CLre_server/API/MainServer/ServerEventArgs.cs @@ -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{} +} \ No newline at end of file diff --git a/CLre_server/API/Tools/AccessToolsWarnings.cs b/CLre_server/API/Tools/AccessToolsWarnings.cs new file mode 100644 index 0000000..3cce05f --- /dev/null +++ b/CLre_server/API/Tools/AccessToolsWarnings.cs @@ -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 \ No newline at end of file diff --git a/CLre_server/API/Tools/NetServerListener.cs b/CLre_server/API/Tools/NetServerListener.cs new file mode 100644 index 0000000..0fe381c --- /dev/null +++ b/CLre_server/API/Tools/NetServerListener.cs @@ -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> callbacks = new FasterDictionary>(); + + 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 handlers)) + { + handlers.Add(callback); + } + else + { + FasterList newHandlers = new FasterList(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 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("dispatcherCode").Value; + byte[] data = result.Field("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"); + } + } +} \ No newline at end of file diff --git a/CLre_server/API/Tools/NetServerSender.cs b/CLre_server/API/Tools/NetServerSender.cs new file mode 100644 index 0000000..e0464d2 --- /dev/null +++ b/CLre_server/API/Tools/NetServerSender.cs @@ -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) GetSendMessageMethod).Method + .GetBaseDefinition() + .GetGenericMethodDefinition();*/ + + private static readonly MethodInfo _genericLog = + AccessTools.Method(typeof(NetServerSender), "Log");/* + ((Action) Log).Method + .GetBaseDefinition() + .GetGenericMethodDefinition();*/ + + private static readonly MethodInfo _genericGetLogMethod = + AccessTools.Method(typeof(NetServerSender), "GetLogMethod", new Type[0]);/* + ((Func) GetLogMethod).Method + .GetBaseDefinition() + .GetGenericMethodDefinition();*/ + + public static MethodInfo GetSendMessageMethod(Type t) + { + return (MethodInfo) _genericGetSendMessageMethod.MakeGenericMethod(t) + .Invoke(null, new object[0]); + } + + public static MethodInfo GetSendMessageMethod() where T : struct, ISerializedNetData + { + return _genericSendMessage.MakeGenericMethod(typeof(T)); + } + + public static MethodInfo DebugSendMessage(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(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() where T : struct, ISerializedNetData + { + return _genericLog.MakeGenericMethod(typeof(T)); + } + + private static void Log(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()); + } + } +} \ No newline at end of file diff --git a/CLre_server/API/Utility/Logging.cs b/CLre_server/API/Utility/Logging.cs new file mode 100644 index 0000000..b5e3e43 --- /dev/null +++ b/CLre_server/API/Utility/Logging.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Diagnostics; + +namespace CLre_server.API.Utility +{ + /// + /// Utility class to access Cardlife's built-in logging capabilities. + /// The log is saved to outputLog#.Log + /// + public static class Logging + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Log(string msg) + { + Svelto.Console.Log(msg); + } + + /// + /// Write a regular message to Cardlife's log + /// + /// The object to log + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Log(object obj) + { + Svelto.Console.Log(obj.ToString()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LogError(string msg, Dictionary extraData = null) + { + Svelto.Console.LogError(msg, extraData); + } + + /// + /// Write an error message to Cardlife's log + /// + /// The object to log + /// The extra data to pass to the ILogger + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LogError(object obj, Dictionary extraData = null) + { + Svelto.Console.LogError(obj.ToString(), extraData); + } + + /// + /// Write an exception to Cardlife's log and to the screen and exit game + /// + /// The exception to log + /// The extra data to pass to the ILogger. + /// This is automatically populated with "OuterException#" and "OuterStacktrace#" entries + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LogException(Exception e, string msg = null, Dictionary extraData = null) + { + Svelto.Console.LogException(msg, e, extraData); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LogWarning(string msg) + { + Svelto.Console.LogWarning(msg); + } + + /// + /// Write a warning message to Cardlife's log + /// + /// The object to log + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LogWarning(object obj) + { + Svelto.Console.LogWarning(obj.ToString()); + } + + // descriptive logging + + /// + /// Write a descriptive message to Cardlife's log including the calling method's name + /// + /// The object to log + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MetaLog(object obj) + { + var method = (new StackTrace()).GetFrame(1).GetMethod(); + Log($"[{method.DeclaringType.FullName}.{method.Name}] {obj.ToString()}"); + } + } +} diff --git a/CLre_server/API/Utility/Reflection.cs b/CLre_server/API/Utility/Reflection.cs new file mode 100644 index 0000000..9361b50 --- /dev/null +++ b/CLre_server/API/Utility/Reflection.cs @@ -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(); + + public delegate bool ExistsV1(EGID egid); + + public delegate bool ExistsV2(int id, int groupid); + + public delegate T QueryEntityViewV1(EGID egid); + + public delegate T QueryEntityViewV2(int id, ExclusiveGroup.ExclusiveGroupStruct groupid); + + public delegate ReadOnlyCollectionStruct QueryEntityViews(int group) where T : class, IEntityViewStruct; + + public delegate T[] QueryEntitiesV2(int group, out int count) where T : IEntityStruct; + + public delegate T[] QueryEntitiesV1(ExclusiveGroup.ExclusiveGroupStruct group, out int count) where T : IEntityStruct; + + public delegate object[] SendMessage(NetworkDispatcherCode dispatcherCode, ref T value) where T : struct, ISerializedNetData; + + // useful reflection functions + public static TFuncProto BuildDelegate(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(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(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(method, instance); + } + return BuildDelegate(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(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(method, instance); + } + return BuildDelegate(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; + } + } +} \ No newline at end of file diff --git a/CLre_server/CLre_server.cs b/CLre_server/CLre_server.cs new file mode 100644 index 0000000..fde19b4 --- /dev/null +++ b/CLre_server/CLre_server.cs @@ -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"); + + netData = AccessTools.TypeByName("Shared.Inventory.HandheldEquipmentRequest"); + NetServerSender.DebugSendMessage(netData, harmonyInstance, + NetServerSender.GetLogMethod(netData)); + API.Utility.Logging.MetaLog("Patched SendMessage"); + + 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 + } + } + } +} diff --git a/CLre_server/CLre_server.csproj b/CLre_server/CLre_server.csproj new file mode 100644 index 0000000..2d2ed79 --- /dev/null +++ b/CLre_server/CLre_server.csproj @@ -0,0 +1,292 @@ + + + + net472 + true + 0.0.2 + NGnius + MIT + https://git.exmods.org/NGnius/CLre + en-CA + + + + + + + + + + + + + + + + ..\..\cl\CardlifeGameServer_Data\Managed\Accessibility.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\Assembly-CSharp-firstpass.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\Assembly-CSharp.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\BehaviorDesignerRuntime.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\DOTween.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\EasyAntiCheat.Client.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\EasyAntiCheat.Server.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\Fabric.AudioSpline.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\Fabric.Core.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\log4net.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\mscorlib.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\Novell.Directory.Ldap.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\Photon3Unity3D.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\Rewired_Core.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\Rewired_Windows_Lib.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\Svelto.Common.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\Svelto.ECS.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\Svelto.Tasks.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\Unity.Postprocessing.Runtime.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\Unity.TextMeshPro.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.AccessibilityModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.AIModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.AnimationModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ARModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.AssetBundleModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.AudioModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.BaselibModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ClothModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ClusterInputModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ClusterRendererModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.CoreModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.CrashReportingModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.DirectorModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.FileSystemHttpModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.GameCenterModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.GridModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.HotReloadModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ImageConversionModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.IMGUIModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.InputModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.JSONSerializeModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.LocalizationModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.Networking.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ParticleSystemModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.PerformanceReportingModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.Physics2DModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.PhysicsModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ProfilerModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.ScreenCaptureModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.SharedInternalsModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.SpatialTracking.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.SpriteMaskModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.SpriteShapeModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.StreamingModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.StyleSheetsModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.SubstanceModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TerrainModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TerrainPhysicsModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TextCoreModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TextRenderingModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TilemapModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.Timeline.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TimelineModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.TLSModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UI.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UIElementsModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UIModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UmbraModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UNETModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityAnalyticsModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityConnectModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityTestProtocolModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityWebRequestAssetBundleModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityWebRequestAudioModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityWebRequestModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityWebRequestTextureModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.UnityWebRequestWWWModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.VehiclesModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.VFXModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.VideoModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.VRModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.WindModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\UnityEngine.XRModule.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\IllusionInjector.dll + + + ..\..\cl\CardlifeGameServer_Data\Managed\IllusionPlugin.dll + + + + + + + + + + + diff --git a/CLre_server/Fixes/BugfixAttribute.cs b/CLre_server/Fixes/BugfixAttribute.cs new file mode 100644 index 0000000..490f8c5 --- /dev/null +++ b/CLre_server/Fixes/BugfixAttribute.cs @@ -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 keys = new List(); + Dictionary fixes = new Dictionary(); + foreach (Type t in Assembly.GetExecutingAssembly().GetTypes()) + { + BugfixAttribute b = t.GetCustomAttribute(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(); + } + } + } +} \ No newline at end of file diff --git a/CLre_server/Fixes/InitLogSooner.cs b/CLre_server/Fixes/InitLogSooner.cs new file mode 100644 index 0000000..0e94a5b --- /dev/null +++ b/CLre_server/Fixes/InitLogSooner.cs @@ -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; + } + } + } +} \ No newline at end of file diff --git a/CLre_server/WebStatus/AssetEndpoints.cs b/CLre_server/WebStatus/AssetEndpoints.cs new file mode 100644 index 0000000..be1af94 --- /dev/null +++ b/CLre_server/WebStatus/AssetEndpoints.cs @@ -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("Asset not found

"); + sb.Append(target.GetName().Name); + sb.Append(" available assets

"); + foreach (string asset in target.GetManifestResourceNames()) + { + sb.Append("
  • "); + sb.Append(asset); + sb.Append("
  • "); + } + + sb.Append(""); + 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(); + } + } +} \ No newline at end of file diff --git a/CLre_server/WebStatus/Assets/error404.html b/CLre_server/WebStatus/Assets/error404.html new file mode 100644 index 0000000..5af6067 --- /dev/null +++ b/CLre_server/WebStatus/Assets/error404.html @@ -0,0 +1,13 @@ + + + + + 404 Not Found + + +

    Page not found

    +
    + Home +
    + + \ No newline at end of file diff --git a/CLre_server/WebStatus/Assets/index.html b/CLre_server/WebStatus/Assets/index.html new file mode 100644 index 0000000..5b84398 --- /dev/null +++ b/CLre_server/WebStatus/Assets/index.html @@ -0,0 +1,16 @@ + + + + + CLre Server Status + + +

    Welcome to the CLre HTTP server!

    +
    + View log +
    + +
    + + + \ No newline at end of file diff --git a/CLre_server/WebStatus/Attributes.cs b/CLre_server/WebStatus/Attributes.cs new file mode 100644 index 0000000..706eb93 --- /dev/null +++ b/CLre_server/WebStatus/Attributes.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/CLre_server/WebStatus/ConfigurationEndpoints.cs b/CLre_server/WebStatus/ConfigurationEndpoints.cs new file mode 100644 index 0000000..d3d4874 --- /dev/null +++ b/CLre_server/WebStatus/ConfigurationEndpoints.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/CLre_server/WebStatus/DebugEndpoints.cs b/CLre_server/WebStatus/DebugEndpoints.cs new file mode 100644 index 0000000..9437a1e --- /dev/null +++ b/CLre_server/WebStatus/DebugEndpoints.cs @@ -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 + } +} \ No newline at end of file diff --git a/CLre_server/WebStatus/LogEndpoints.cs b/CLre_server/WebStatus/LogEndpoints.cs new file mode 100644 index 0000000..3f2d1ff --- /dev/null +++ b/CLre_server/WebStatus/LogEndpoints.cs @@ -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"); + } + } +} \ No newline at end of file diff --git a/CLre_server/WebStatus/StatusEndpoints.cs b/CLre_server/WebStatus/StatusEndpoints.cs new file mode 100644 index 0000000..0ffb959 --- /dev/null +++ b/CLre_server/WebStatus/StatusEndpoints.cs @@ -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 OnlinePlayers; + + public int MaxPlayers; + + public RunState State; + + public static ServerStateCache Empty() + { + return new ServerStateCache + { + OnlinePlayers = new List(), + 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 accounts = + entitiesDB.QueryEntityViews(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(method, this); + } + + _playerPositionFunc(); + } + + #pragma warning disable 0618 + private void getPlayerPositionsGeneric() where T : EntityView // EntityView is deprecated lol + { + ReadOnlyCollectionStruct scpnCollection = entitiesDB.QueryEntityViews(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("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); + } + } +} \ No newline at end of file diff --git a/CLre_server/WebStatus/WebServer.cs b/CLre_server/WebStatus/WebServer.cs new file mode 100644 index 0000000..6c1ac7b --- /dev/null +++ b/CLre_server/WebStatus/WebServer.cs @@ -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 _handlers = new Dictionary(); + + public bool IsRunning + { + get => _httpListener.IsListening; + } + + private string _ip_addr; + private uint _port; + + public static WebServer MainInstance { get; internal set; } + + internal static List _assembliesToCheck = new List(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(); + foreach (Assembly asm in _assembliesToCheck.ToArray()) + { + foreach (Type t in asm.GetTypes()) + { + foreach (MethodInfo m in t.GetMethods(AccessTools.all)) + { + WebEndpointAttribute attr = m.GetCustomAttribute(); + 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)); + } + } + } + } + } + } +} \ No newline at end of file