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

This commit is contained in:
Garrett
2024-11-09 23:38:17 -06:00
parent f631d2b9ff
commit 0fc7fee73f
7 changed files with 371 additions and 30 deletions

140
Components/ModReloader.cs Normal file
View File

@@ -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<Type, MyModContext> sessionsBeingRemoved = new Dictionary<Type, MyModContext>();
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<Type, MySessionComponentBase> sessionComponents = (CachingDictionary<Type, MySessionComponentBase>)typeof(MySession).GetField("m_sessionComponents", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(MySession.Static);
List<MySessionComponentBase> m_loadOrder = (List<MySessionComponentBase>)typeof(MySession).GetField("m_loadOrder", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(MySession.Static);
List<MySessionComponentBase> m_sessionComponentForDraw = (List<MySessionComponentBase>)typeof(MySession).GetField("m_sessionComponentForDraw", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(MySession.Static);
Dictionary<int, SortedSet<MySessionComponentBase>> m_sessionComponentsForUpdate = (Dictionary<int, SortedSet<MySessionComponentBase>>)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<MySessionComponentBase> m_sessionComponentForDraw = (List<MySessionComponentBase>)typeof(MySession).GetField("m_sessionComponentForDraw", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(MySession.Static);
List<MySessionComponentBase> newSessions = new List<MySessionComponentBase>();
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");
}
}
}

View File

@@ -38,6 +38,9 @@ using VRageRender.Effects;
using VRage.Scripting; using VRage.Scripting;
using VRage.Utils; using VRage.Utils;
using SpaceEngineers.Game.World; using SpaceEngineers.Game.World;
using VRage.Collections;
using VRage.Game.Components;
using System.CodeDom;
namespace SeamlessClient.Components namespace SeamlessClient.Components
{ {
@@ -66,10 +69,15 @@ namespace SeamlessClient.Components
private static bool StartPacketCheck = false; private static bool StartPacketCheck = false;
private bool keepGrid = false; private bool keepGrid = false;
private ModReloader modReload;
public SeamlessSwitcher() public SeamlessSwitcher()
{ {
Instance = this; Instance = this;
modReload = new ModReloader();
} }
@@ -106,7 +114,7 @@ namespace SeamlessClient.Components
patcher.Patch(_SendRPC, prefix: preSendRPC); patcher.Patch(_SendRPC, prefix: preSendRPC);
modReload.Patch(patcher);
base.Patch(patcher); base.Patch(patcher);
} }
@@ -144,6 +152,8 @@ namespace SeamlessClient.Components
_MyGameServerItemProperty.SetValue(MyMultiplayer.Static, TargetServer); _MyGameServerItemProperty.SetValue(MyMultiplayer.Static, TargetServer);
_MultiplayerServerID.SetValue(MyMultiplayer.Static, TargetServer.SteamID); _MultiplayerServerID.SetValue(MyMultiplayer.Static, TargetServer.SteamID);
/* Connect To Server */ /* Connect To Server */
MyGameService.ConnectToServer(TargetServer, delegate (JoinResult joinResult) 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 }); 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.Peer2Peer.CloseSession(Sync.ServerId);
MyGameService.DisconnectFromServer(); MyGameService.DisconnectFromServer();
MyGameService.ClearCache();
@@ -200,6 +210,8 @@ namespace SeamlessClient.Components
UnloadOldEntities(); UnloadOldEntities();
ResetReplicationTime(false); ResetReplicationTime(false);
//MyMultiplayer.Static.ReplicationLayer.Disconnect(); //MyMultiplayer.Static.ReplicationLayer.Disconnect();
//MyMultiplayer.Static.ReplicationLayer.Dispose(); //MyMultiplayer.Static.ReplicationLayer.Dispose();
//MyMultiplayer.Static.Dispose(); //MyMultiplayer.Static.Dispose();
@@ -284,6 +296,10 @@ namespace SeamlessClient.Components
} }
private void ClearClientReplicables() private void ClearClientReplicables()
{ {
MyReplicationClient replicationClient = (MyReplicationClient)MyMultiplayer.Static.ReplicationLayer; MyReplicationClient replicationClient = (MyReplicationClient)MyMultiplayer.Static.ReplicationLayer;
@@ -310,6 +326,8 @@ namespace SeamlessClient.Components
return; return;
Seamless.TryShow($"OnUserJoin! Result: {joinResult}"); Seamless.TryShow($"OnUserJoin! Result: {joinResult}");
modReload.UnloadModSessionComponents();
LoadDestinationServer(); 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($"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!"); Seamless.TryShow("Starting new MP Client!");
/* On Server Successfull Join /* On Server Successfull Join
* *
* *
@@ -350,10 +370,12 @@ namespace SeamlessClient.Components
typeof(MySandboxGame).GetField("m_pauseStackCount", BindingFlags.Static | BindingFlags.NonPublic).SetValue(null, 0); 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 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}"); Seamless.TryShow($"6 Streaming: {clienta.HasPendingStreamingReplicables} - LastMessage: {clienta.LastMessageFromServer}");
modReload.AddModComponents();
SendClientReady();
@@ -364,19 +386,12 @@ namespace SeamlessClient.Components
// Allow the game to start proccessing incoming messages in the buffer // Allow the game to start proccessing incoming messages in the buffer
//MyMultiplayer.Static.StartProcessingClientMessages(); //MyMultiplayer.Static.StartProcessingClientMessages();
//Send Client Ready //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; PreventRPC = false;
//_ClearTransportLayer.Invoke(_TransportLayer.GetValue(MyMultiplayer.Static.SyncLayer), null); //_ClearTransportLayer.Invoke(_TransportLayer.GetValue(MyMultiplayer.Static.SyncLayer), null);
StartEntitySync(); StartEntitySync();
Seamless.SendSeamlessVersion();
@@ -388,24 +403,26 @@ 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() private void StartEntitySync()
{ {
Seamless.TryShow("Requesting Player From Server"); 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();
}
//Request client state batch //Request client state batch
@@ -440,6 +457,18 @@ namespace SeamlessClient.Components
MyMultiplayer.Static.PendingReplicablesDone -= Static_PendingReplicablesDone; 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: //Try find existing seat:
//bool found = (bool)typeof(MySpaceRespawnComponent).GetMethod("TryFindExistingCharacter", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { MySession.Static.LocalHumanPlayer }); //bool found = (bool)typeof(MySpaceRespawnComponent).GetMethod("TryFindExistingCharacter", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { MySession.Static.LocalHumanPlayer });
MyGuiScreenHudSpace.Static?.RecreateControls(true); MyGuiScreenHudSpace.Static?.RecreateControls(true);

28
Models/ModByte.cs Normal file
View File

@@ -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);
}
}
}

83
Models/ModCache.cs Normal file
View File

@@ -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<ModByte> CachedMods { get; set; } = new List<ModByte>();
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<ModCache>(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;
}
}
}

View File

@@ -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) private static void MessageHandler(ushort packetID, byte[] data, ulong sender, bool fromServer)
{ {
@@ -174,6 +178,7 @@ namespace SeamlessClient
if (!Initilized) if (!Initilized)
{ {
MyAPIGateway.Multiplayer.RegisterSecureMessageHandler(SeamlessClientNetId, MessageHandler); MyAPIGateway.Multiplayer.RegisterSecureMessageHandler(SeamlessClientNetId, MessageHandler);
MyAPIGateway.Multiplayer.RegisterSecureMessageHandler(2938, MessageHandler2);
InitilizeComponents(); InitilizeComponents();
Initilized = true; Initilized = true;

View File

@@ -34,6 +34,14 @@
<Reference Include="0Harmony, Version=2.3.3.0, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="0Harmony, Version=2.3.3.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>packages\Lib.Harmony.2.3.3\lib\net48\0Harmony.dll</HintPath> <HintPath>packages\Lib.Harmony.2.3.3\lib\net48\0Harmony.dll</HintPath>
</Reference> </Reference>
<Reference Include="Microsoft.CodeAnalysis">
<HintPath>GameBinaries\Microsoft.CodeAnalysis.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.CodeAnalysis.CSharp">
<HintPath>GameBinaries\Microsoft.CodeAnalysis.CSharp.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="NLog"> <Reference Include="NLog">
<HintPath>GameBinaries\NLog.dll</HintPath> <HintPath>GameBinaries\NLog.dll</HintPath>
<Private>False</Private> <Private>False</Private>
@@ -68,6 +76,10 @@
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Collections.Immutable, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>GameBinaries\System.Collections.Immutable.dll</HintPath>
</Reference>
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
@@ -99,17 +111,25 @@
<HintPath>GameBinaries\VRage.Render.dll</HintPath> <HintPath>GameBinaries\VRage.Render.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="VRage.Scripting">
<HintPath>GameBinaries\VRage.Scripting.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Components\EntityPerformanceImprovements.cs" /> <Compile Include="Components\EntityPerformanceImprovements.cs" />
<Compile Include="Components\LoadingScreenComponent.cs" /> <Compile Include="Components\LoadingScreenComponent.cs" />
<Compile Include="Components\ModReloader.cs" />
<Compile Include="Components\MyGUIScreenMedicalsPatch.cs" /> <Compile Include="Components\MyGUIScreenMedicalsPatch.cs" />
<Compile Include="Components\MyHudTimeTracker.cs" /> <Compile Include="Components\MyHudTimeTracker.cs" />
<Compile Include="Components\SeamlessScriptManager.cs" /> <Compile Include="Components\SeamlessScriptManager.cs" />
<Compile Include="Components\SeamlessSwitcher.cs" /> <Compile Include="Components\SeamlessSwitcher.cs" />
<Compile Include="GUI\Screens\GUILoadingScreen.cs" /> <Compile Include="GUI\Screens\GUILoadingScreen.cs" />
<Compile Include="Messages\OnlinePlayerData.cs" /> <Compile Include="Messages\OnlinePlayerData.cs" />
<Compile Include="Models\ModCache.cs" />
<Compile Include="Models\ModByte.cs" />
<Compile Include="Utilities\GameEvents.cs" /> <Compile Include="Utilities\GameEvents.cs" />
<Compile Include="Utilities\NetUtils.cs" />
<Compile Include="Utilities\UtilExtensions.cs" /> <Compile Include="Utilities\UtilExtensions.cs" />
<Compile Include="Utilities\PatchUtils.cs" /> <Compile Include="Utilities\PatchUtils.cs" />
<Compile Include="Messages\ClientMessage.cs" /> <Compile Include="Messages\ClientMessage.cs" />

36
Utilities/NetUtils.cs Normal file
View File

@@ -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>(T instance)
{
if (instance == null)
return null;
using (var m = new MemoryStream())
{
Serializer.Serialize(m, instance);
return m.ToArray();
}
}
public static T Deserialize<T>(byte[] data)
{
if (data == null)
return default(T);
using (var m = new MemoryStream(data))
{
return Serializer.Deserialize<T>(m);
}
}
}
}