From 0fc7fee73f2f7ca44c5a5333850ef760ff07cd17 Mon Sep 17 00:00:00 2001 From: Garrett <52760019+Casimir255@users.noreply.github.com> Date: Sat, 9 Nov 2024 23:38:17 -0600 Subject: [PATCH] Some changes to support mod reloading. (ModAPI messages are still not being recieved properly in nexus mod) this could be a serverside issue but unknown currently --- Components/ModReloader.cs | 140 +++++++++++++++++++++++++++++++++ Components/SeamlessSwitcher.cs | 89 ++++++++++++++------- Models/ModByte.cs | 28 +++++++ Models/ModCache.cs | 83 +++++++++++++++++++ Seamless.cs | 5 ++ SeamlessClient.csproj | 20 +++++ Utilities/NetUtils.cs | 36 +++++++++ 7 files changed, 371 insertions(+), 30 deletions(-) create mode 100644 Components/ModReloader.cs create mode 100644 Models/ModByte.cs create mode 100644 Models/ModCache.cs create mode 100644 Utilities/NetUtils.cs diff --git a/Components/ModReloader.cs b/Components/ModReloader.cs new file mode 100644 index 0000000..1420563 --- /dev/null +++ b/Components/ModReloader.cs @@ -0,0 +1,140 @@ +using Sandbox.Game.World; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using VRage.Collections; +using VRage.Game.Components; +using VRage.Game; +using static Sandbox.Game.World.MySession; +using System.IO; +using VRage.Scripting; +using Microsoft.CodeAnalysis.CSharp; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using HarmonyLib; +using SeamlessClient.Models; +using VRage; + +namespace SeamlessClient.Components +{ + /* Keen compiles mods into assemblies. Unfortunetly, we cannot trust these assemblies after calling unload due to changed variables as the ASSEMBLY level. aka default types + * To fix this we need to go further and actually attempt to call the assembly to be unloaded, then reload the assembly and get the update type to be initiated. + * + * This might be a time consuming process? We will just have to see. Ideally would like to keep the whole mod script reloading down to like less than a second or two for N amount of mods. Might + * be able to speed up loading compared to keens way too + * + * + */ + public class ModReloader + { + Dictionary sessionsBeingRemoved = new Dictionary(); + private static ModCache modCache = new ModCache(); + + + public void Patch(Harmony patcher) + { + MethodInfo assemblyLoad = AccessTools.Method(typeof(Assembly), "Load", new Type[] { typeof(byte[]) }); + + var patchAsmLoad = PatchUtils.GetMethod(this.GetType(), "AssemblyLoad"); + patcher.Patch(assemblyLoad, postfix: patchAsmLoad); + + } + + private static void AssemblyLoad(Assembly __result, byte[] rawAssembly) + { + //This will get all of the mods being loading into the game. Worry about saving/loading later + modCache.AddModToCache(__result, rawAssembly); + } + + + + public void UnloadModSessionComponents() + { + CachingDictionary sessionComponents = (CachingDictionary)typeof(MySession).GetField("m_sessionComponents", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(MySession.Static); + List m_loadOrder = (List)typeof(MySession).GetField("m_loadOrder", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(MySession.Static); + List m_sessionComponentForDraw = (List)typeof(MySession).GetField("m_sessionComponentForDraw", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(MySession.Static); + Dictionary> m_sessionComponentsForUpdate = (Dictionary>)typeof(MySession).GetField("m_sessionComponentsForUpdate", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(MySession.Static); + + foreach (var component in sessionComponents) + { + if (component.Value.ModContext != null && !component.Value.ModContext.IsBaseGame) + { + Seamless.TryShow($"{component.Key.FullName}"); + sessionsBeingRemoved.Add(component.Key, component.Value.ModContext as MyModContext); + + //Calls the component to be unloaded + component.Value.UnloadDataConditional(); + m_loadOrder.Remove(component.Value); + } + } + + + + /* Remove all */ + + //Remove from session components + foreach (var item in sessionsBeingRemoved) + sessionComponents.Remove(item.Key, true); + + //Remove from draw + m_sessionComponentForDraw.RemoveAll(x => x.ModContext != null && !x.ModContext.IsBaseGame); + + + //Remove from update + foreach (var kvp in m_sessionComponentsForUpdate) + { + kvp.Value.RemoveWhere(x => sessionsBeingRemoved.ContainsKey(x.ComponentType)); + } + + + + + } + + public void AddModComponents() + { + List m_sessionComponentForDraw = (List)typeof(MySession).GetField("m_sessionComponentForDraw", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(MySession.Static); + + + List newSessions = new List(); + int newLoadedmods = 0; + foreach (var item in sessionsBeingRemoved) + { + + //If it fails, skip the mod? Or try to use old type? (May Fail to load) + if (!modCache.TryGetModAssembly(item.Value.ModItem.PublishedFileId, out Assembly newModAssembly)) + continue; + + + Type newType = newModAssembly.GetType(item.Key.FullName); + MySessionComponentBase mySessionComponentBase = (MySessionComponentBase)Activator.CreateInstance(newType); + mySessionComponentBase.ModContext = item.Value; + + + MySession.Static.RegisterComponent(mySessionComponentBase, mySessionComponentBase.UpdateOrder, mySessionComponentBase.Priority); + newSessions.Add(mySessionComponentBase); + + m_sessionComponentForDraw.Add(mySessionComponentBase); + newLoadedmods++; + + } + + //Will check toi see if session is actually loaded before calling load + MySession.Static.LoadDataComponents(); + sessionsBeingRemoved.Clear(); + typeof(MySession).GetMethod("InitDataComponents", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(MySession.Static, null); + + //Call before start + foreach (var session in newSessions) + session.BeforeStart(); + + + Seamless.TryShow($"Loaded {newLoadedmods} mods"); + } + } + + +} diff --git a/Components/SeamlessSwitcher.cs b/Components/SeamlessSwitcher.cs index 6dfd242..5ac5a14 100644 --- a/Components/SeamlessSwitcher.cs +++ b/Components/SeamlessSwitcher.cs @@ -38,6 +38,9 @@ using VRageRender.Effects; using VRage.Scripting; using VRage.Utils; using SpaceEngineers.Game.World; +using VRage.Collections; +using VRage.Game.Components; +using System.CodeDom; namespace SeamlessClient.Components { @@ -66,10 +69,15 @@ namespace SeamlessClient.Components private static bool StartPacketCheck = false; private bool keepGrid = false; + private ModReloader modReload; + + public SeamlessSwitcher() { - Instance = this; + Instance = this; + modReload = new ModReloader(); + } @@ -106,7 +114,7 @@ namespace SeamlessClient.Components patcher.Patch(_SendRPC, prefix: preSendRPC); - + modReload.Patch(patcher); base.Patch(patcher); } @@ -143,6 +151,8 @@ namespace SeamlessClient.Components /* Set New Multiplayer Stuff */ _MyGameServerItemProperty.SetValue(MyMultiplayer.Static, TargetServer); _MultiplayerServerID.SetValue(MyMultiplayer.Static, TargetServer.SteamID); + + /* Connect To Server */ MyGameService.ConnectToServer(TargetServer, delegate (JoinResult joinResult) @@ -184,7 +194,7 @@ namespace SeamlessClient.Components typeof(MyMultiplayerBase).GetMethod("SendControlMessage", BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(typeof(MyControlDisconnectedMsg)).Invoke(MyMultiplayer.Static, new object[] { Sync.ServerId, myControlDisconnectedMsg, true }); MyGameService.Peer2Peer.CloseSession(Sync.ServerId); MyGameService.DisconnectFromServer(); - + MyGameService.ClearCache(); @@ -199,6 +209,8 @@ namespace SeamlessClient.Components UnloadOldEntities(); ResetReplicationTime(false); + + //MyMultiplayer.Static.ReplicationLayer.Disconnect(); //MyMultiplayer.Static.ReplicationLayer.Dispose(); @@ -284,6 +296,10 @@ namespace SeamlessClient.Components } + + + + private void ClearClientReplicables() { MyReplicationClient replicationClient = (MyReplicationClient)MyMultiplayer.Static.ReplicationLayer; @@ -310,6 +326,8 @@ namespace SeamlessClient.Components return; Seamless.TryShow($"OnUserJoin! Result: {joinResult}"); + modReload.UnloadModSessionComponents(); + LoadDestinationServer(); @@ -324,6 +342,8 @@ namespace SeamlessClient.Components Seamless.TryShow($"5 NexusMajor: {Seamless.NexusVersion.Major} - ConrolledEntity {MySession.Static.ControlledEntity == null} - HumanPlayer {MySession.Static.LocalHumanPlayer == null} - Character {MySession.Static.LocalCharacter == null}"); Seamless.TryShow("Starting new MP Client!"); + + /* On Server Successfull Join * * @@ -350,33 +370,28 @@ namespace SeamlessClient.Components typeof(MySandboxGame).GetField("m_pauseStackCount", BindingFlags.Static | BindingFlags.NonPublic).SetValue(null, 0); + Seamless.TryShow($"6 NexusMajor: {Seamless.NexusVersion.Major} - ConrolledEntity {MySession.Static.ControlledEntity == null} - HumanPlayer {MySession.Static.LocalHumanPlayer == null} - Character {MySession.Static.LocalCharacter == null}"); Seamless.TryShow($"6 Streaming: {clienta.HasPendingStreamingReplicables} - LastMessage: {clienta.LastMessageFromServer}"); - - - + modReload.AddModComponents(); + SendClientReady(); + + + - ResetReplicationTime(true); //MyPlayerCollection.ChangePlayerCharacter(MySession.Static.LocalHumanPlayer, MySession.Static.LocalCharacter, MySession.Static.LocalCharacter); // Allow the game to start proccessing incoming messages in the buffer //MyMultiplayer.Static.StartProcessingClientMessages(); //Send Client Ready - ClientReadyDataMsg clientReadyDataMsg = default(ClientReadyDataMsg); - clientReadyDataMsg.ForcePlayoutDelayBuffer = MyFakes.ForcePlayoutDelayBuffer; - clientReadyDataMsg.UsePlayoutDelayBufferForCharacter = true; - clientReadyDataMsg.UsePlayoutDelayBufferForJetpack = true; - clientReadyDataMsg.UsePlayoutDelayBufferForGrids = true; - ClientReadyDataMsg msg = clientReadyDataMsg; - clienta.SendClientReady(ref msg); - PreventRPC = false; //_ClearTransportLayer.Invoke(_TransportLayer.GetValue(MyMultiplayer.Static.SyncLayer), null); + StartEntitySync(); - Seamless.SendSeamlessVersion(); + @@ -388,23 +403,25 @@ namespace SeamlessClient.Components } + private void SendClientReady() + { + MyReplicationClient clienta = (MyReplicationClient)MyMultiplayer.Static.ReplicationLayer; + ClientReadyDataMsg clientReadyDataMsg = default(ClientReadyDataMsg); + clientReadyDataMsg.ForcePlayoutDelayBuffer = MyFakes.ForcePlayoutDelayBuffer; + clientReadyDataMsg.UsePlayoutDelayBufferForCharacter = true; + clientReadyDataMsg.UsePlayoutDelayBufferForJetpack = true; + clientReadyDataMsg.UsePlayoutDelayBufferForGrids = true; + ClientReadyDataMsg msg = clientReadyDataMsg; + clienta.SendClientReady(ref msg); + + Seamless.SendSeamlessVersion(); + } + private void StartEntitySync() { Seamless.TryShow("Requesting Player From Server"); - Sync.Players.RequestNewPlayer(Sync.MyId, 0, MyGameService.UserName, null, true, true); - if (!Sandbox.Engine.Platform.Game.IsDedicated && MySession.Static.LocalHumanPlayer == null) - { - Seamless.TryShow("RequestNewPlayer"); - - - } - else if (MySession.Static.ControlledEntity == null && Sync.IsServer && !Sandbox.Engine.Platform.Game.IsDedicated) - { - Seamless.TryShow("ControlledObject was null, respawning character"); - //m_cameraAwaitingEntity = true; - MyPlayerCollection.RequestLocalRespawn(); - } + @@ -438,7 +455,19 @@ namespace SeamlessClient.Components MySandboxGame.AreClipmapsReady = false; } MyMultiplayer.Static.PendingReplicablesDone -= Static_PendingReplicablesDone; - + + + Sync.Players.RequestNewPlayer(Sync.MyId, 0, MyGameService.UserName, null, true, true); + if (!Sandbox.Engine.Platform.Game.IsDedicated && MySession.Static.LocalHumanPlayer == null) + { + Seamless.TryShow("RequestNewPlayer"); + } + else if (MySession.Static.ControlledEntity == null && Sync.IsServer && !Sandbox.Engine.Platform.Game.IsDedicated) + { + Seamless.TryShow("ControlledObject was null, respawning character"); + //m_cameraAwaitingEntity = true; + MyPlayerCollection.RequestLocalRespawn(); + } //Try find existing seat: //bool found = (bool)typeof(MySpaceRespawnComponent).GetMethod("TryFindExistingCharacter", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { MySession.Static.LocalHumanPlayer }); diff --git a/Models/ModByte.cs b/Models/ModByte.cs new file mode 100644 index 0000000..738e614 --- /dev/null +++ b/Models/ModByte.cs @@ -0,0 +1,28 @@ +using ProtoBuf; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace SeamlessClient.Models +{ + [ProtoContract] + public class ModByte + { + [ProtoMember(1)] + public ulong ModID { get; set; } + + [ProtoMember(2)] + public byte[] AssemblyBytes { get; set; } + + + + public Assembly GetNewAssembly() + { + return Assembly.Load(AssemblyBytes); + } + + } +} diff --git a/Models/ModCache.cs b/Models/ModCache.cs new file mode 100644 index 0000000..dbe5405 --- /dev/null +++ b/Models/ModCache.cs @@ -0,0 +1,83 @@ +using ProtoBuf; +using SeamlessClient.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using VRage.Serialization; + +namespace SeamlessClient.Models +{ + [ProtoContract] + public class ModCache + { + + [ProtoMember(1)] + public List CachedMods { get; set; } = new List(); + + + + public void AddModToCache(Assembly asm, byte[] raw) + { + //Get the modID from the loaded assembly name + ulong? modid = GetLeadingNumber(asm.FullName); + if (!modid.HasValue || modid.Value == 0) + return; + + //Check to see if the loading mod is already in our cache + if(CachedMods.Any(x => x.ModID == modid.Value)) + return; + + + ModByte mod = new ModByte(); + mod.ModID = modid.Value; + mod.AssemblyBytes = raw; + + CachedMods.Add(mod); + } + + public static void SaveToFile(ModCache cache) + { + byte[] data = NetUtils.Serialize(cache); + File.WriteAllBytes("", data); + } + public ModCache LoadFromFile() + { + byte[] data = File.ReadAllBytes(""); + return NetUtils.Deserialize(data); + } + + public bool TryGetModAssembly(ulong modid, out Assembly asm) + { + asm = null; + if (modid == 0) + return false; + + ModByte mod = CachedMods.FirstOrDefault(x => x.ModID == modid); + if(mod == null) + return false; + + //Compiles new assembly + try + { + asm = mod.GetNewAssembly(); + return true; + } + catch (Exception ex) + { + + return false; + } + } + static ulong? GetLeadingNumber(string assemblyName) + { + Match match = Regex.Match(assemblyName, @"^\d+"); + return match.Success ? ulong.Parse(match.Value) : (ulong?)null; + } + + } +} diff --git a/Seamless.cs b/Seamless.cs index a72f849..fd5d9f4 100644 --- a/Seamless.cs +++ b/Seamless.cs @@ -113,6 +113,10 @@ namespace SeamlessClient } } + private static void MessageHandler2(ushort packetID, byte[] data, ulong sender, bool fromServer) + { + Seamless.TryShow("Recieved visuals"); + } private static void MessageHandler(ushort packetID, byte[] data, ulong sender, bool fromServer) { @@ -174,6 +178,7 @@ namespace SeamlessClient if (!Initilized) { MyAPIGateway.Multiplayer.RegisterSecureMessageHandler(SeamlessClientNetId, MessageHandler); + MyAPIGateway.Multiplayer.RegisterSecureMessageHandler(2938, MessageHandler2); InitilizeComponents(); Initilized = true; diff --git a/SeamlessClient.csproj b/SeamlessClient.csproj index d3d72dd..70204d9 100644 --- a/SeamlessClient.csproj +++ b/SeamlessClient.csproj @@ -34,6 +34,14 @@ packages\Lib.Harmony.2.3.3\lib\net48\0Harmony.dll + + GameBinaries\Microsoft.CodeAnalysis.dll + False + + + GameBinaries\Microsoft.CodeAnalysis.CSharp.dll + False + GameBinaries\NLog.dll False @@ -68,6 +76,10 @@ False + + False + GameBinaries\System.Collections.Immutable.dll + @@ -99,17 +111,25 @@ GameBinaries\VRage.Render.dll False + + GameBinaries\VRage.Scripting.dll + False + + + + + diff --git a/Utilities/NetUtils.cs b/Utilities/NetUtils.cs new file mode 100644 index 0000000..b017f86 --- /dev/null +++ b/Utilities/NetUtils.cs @@ -0,0 +1,36 @@ +using ProtoBuf; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SeamlessClient.Utilities +{ + public class NetUtils + { + public static byte[] Serialize(T instance) + { + if (instance == null) + return null; + + using (var m = new MemoryStream()) + { + Serializer.Serialize(m, instance); + return m.ToArray(); + } + } + + public static T Deserialize(byte[] data) + { + if (data == null) + return default(T); + + using (var m = new MemoryStream(data)) + { + return Serializer.Deserialize(m); + } + } + } +}