using HarmonyLib; using Sandbox; using Sandbox.Engine.Multiplayer; using Sandbox.Engine.Networking; using Sandbox.Engine.Utils; using Sandbox.Game.Entities.Character; using Sandbox.Game.Entities; using Sandbox.Game.Gui; using Sandbox.Game.Multiplayer; using Sandbox.Game.SessionComponents; using Sandbox.Game.World; using Sandbox.Graphics.GUI; using SeamlessClient.Utilities; using SpaceEngineers.Game.GUI; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using VRage.Game; using VRage.GameServices; using VRage.Library.Utils; using VRage.Network; using VRageRender; using EmptyKeys.UserInterface.Generated.StoreBlockView_Bindings; using System.Security.Cryptography; using VRage.Groups; using VRage.Game.ModAPI; using VRage; using static Sandbox.ModAPI.MyModAPIHelper; using NLog.Targets; using MyMultiplayer = Sandbox.Engine.Multiplayer.MyMultiplayer; using static Sandbox.Engine.Networking.MyNetworkWriter; using System.Collections.Concurrent; using VRage.Game.Entity; using VRageRender.Effects; using VRage.Scripting; using VRage.Utils; using SpaceEngineers.Game.World; using VRage.Collections; using VRage.Game.Components; using System.CodeDom; using System.Diagnostics; namespace SeamlessClient.Components { public class SeamlessSwitcher : ComponentBase { private static bool isSwitchingServer = false; private PropertyInfo _MyGameServerItemProperty; private PropertyInfo _MultiplayerServerID; private static MethodInfo PauseClient; private static MethodInfo _SendPlayerData; private MethodInfo _SendFlush; private MethodInfo _ClearTransportLayer; private FieldInfo _NetworkWriteQueue; private FieldInfo _TransportLayer; public static SeamlessSwitcher Instance; private static List allGrids = new List(); private long _OriginalCharacterEntity = -1; private long _OriginalGridEntity = -1; private static ulong TargetServerID; private static bool PreventRPC = false; private static bool StartPacketCheck = false; private bool keepGrid = false; private static ModReloader modReload; private static Stopwatch switchTime = new Stopwatch(); public SeamlessSwitcher() { Instance = this; modReload = new ModReloader(); } public override void Patch(Harmony patcher) { _MyGameServerItemProperty = PatchUtils.GetProperty(PatchUtils.ClientType, "Server"); _MultiplayerServerID = PatchUtils.GetProperty(typeof(MyMultiplayerBase), "ServerId"); PauseClient = PatchUtils.GetMethod(PatchUtils.MyMultiplayerClientBase, "PauseClient"); _SendPlayerData = PatchUtils.GetMethod(PatchUtils.ClientType, "SendPlayerData"); _ClearTransportLayer = PatchUtils.GetMethod(PatchUtils.MyTransportLayerType, "Clear"); List st = AccessTools.GetMethodNames(PatchUtils.MyMultiplayerClientBase); var _SendRPC = PatchUtils.GetMethod(PatchUtils.MyMultiplayerClientBase, "VRage.Replication.IReplicationClientCallback.SendEvent"); var preSendRPC = PatchUtils.GetMethod(this.GetType(), "PreventEvents"); _SendFlush = PatchUtils.GetMethod(PatchUtils.MyTransportLayerType, "SendFlush"); _NetworkWriteQueue = PatchUtils.GetField(typeof(MyNetworkWriter), "m_packetsToSend"); _TransportLayer = PatchUtils.GetField(typeof(MySyncLayer), "TransportLayer"); var method = AccessTools.Method(PatchUtils.MyTransportLayerType, "SendMessage", new Type[] { typeof(MyMessageId), typeof(IPacketData), typeof(bool), typeof(EndpointId), typeof(byte) }); var patchSend = PatchUtils.GetMethod(this.GetType(), "SendMessage_Patch"); var OnUserJoin = AccessTools.Method(PatchUtils.ClientType, "OnUserJoined"); var LocalOnUserJoin = PatchUtils.GetMethod(this.GetType(), nameof(OnUserJoined_Patch)); patcher.Patch(method, prefix: patchSend); patcher.Patch(_SendRPC, prefix: preSendRPC); patcher.Patch(OnUserJoin, postfix: LocalOnUserJoin); modReload.Patch(patcher); base.Patch(patcher); } //This method is when we have fully joined the server. It is a patched method private static void OnUserJoined_Patch(JoinResultMsg msg) { if (!isSwitchingServer) return; if (msg.JoinResult != JoinResult.OK) return; Seamless.TryShow($"OnUserJoin! Result: {msg.JoinResult}"); LoadDestinationServer(); isSwitchingServer = false; switchTime.Stop(); Seamless.TryShow($"Total Seamless time: {switchTime.ElapsedMilliseconds}"); } public static void SendMessage_Patch(MyMessageId id, IPacketData data, bool reliable, EndpointId endpoint, byte index = 0) { if (!StartPacketCheck) return; //MyLog.Default?.WriteLineAndConsole($"{System.Environment.StackTrace}"); //Seamless.TryShow($"Id:{id} -> {endpoint}"); } public void StartSwitch(MyGameServerItem TargetServer, MyObjectBuilder_World TargetWorld) { switchTime.Restart(); isSwitchingServer = true; TargetServerID = TargetServer.GameID; MyReplicationClient localMPClient = (MyReplicationClient)MyMultiplayer.Static.ReplicationLayer; _OriginalCharacterEntity = MySession.Static.LocalCharacter?.EntityId ?? 0; if (MySession.Static.LocalCharacter?.Parent != null && MySession.Static.LocalCharacter.Parent is MyCockpit cockpit) { _OriginalGridEntity = cockpit.CubeGrid.EntityId; } UnloadCurrentServer(localMPClient); /* Fix any weird compatibilities */ MySandboxGame.Static.SessionCompatHelper.FixSessionComponentObjectBuilders(TargetWorld.Checkpoint, TargetWorld.Sector); /* Set New Multiplayer Stuff */ _MyGameServerItemProperty.SetValue(MyMultiplayer.Static, TargetServer); _MultiplayerServerID.SetValue(MyMultiplayer.Static, TargetServer.SteamID); /* Connect To Server */ MyGameService.ConnectToServer(TargetServer, delegate (JoinResult joinResult) { MySandboxGame.Static.Invoke(delegate { Seamless.TryShow("Connected to server!"); //_OnConnectToServer.Invoke(MyMultiplayer.Static, new object[] { joinResult }); _SendPlayerData.Invoke(MyMultiplayer.Static, new object[] { MyGameService.OnlineName }); modReload.UnloadModSessionComponents(); }, "OnConnectToServer"); }); } private void UnloadCurrentServer(MyReplicationClient localMPClient) { //Close and Cancel any screens (Medical or QuestLog) MySessionComponentIngameHelp component = MySession.Static.GetComponent(); component?.TryCancelObjective(); MyGuiScreenMedicals.Close(); MethodInfo removeClient = PatchUtils.GetMethod(PatchUtils.ClientType, "MyMultiplayerClient_ClientLeft"); foreach (var connectedClient in Sync.Clients.GetClients()) { if (connectedClient.SteamUserId == Sync.MyId || connectedClient.SteamUserId == Sync.ServerId) continue; removeClient.Invoke(MyMultiplayer.Static, new object[] { connectedClient.SteamUserId, MyChatMemberStateChangeEnum.Left }); } /* Sends disconnect message to outbound server */ MyControlDisconnectedMsg myControlDisconnectedMsg = default(MyControlDisconnectedMsg); myControlDisconnectedMsg.Client = Sync.MyId; 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(); //Remove old signals MyHud.GpsMarkers.Clear(); MyHud.LocationMarkers.Clear(); MyHud.HackingMarkers.Clear(); Seamless.TryShow($"2 Streaming: {localMPClient.HasPendingStreamingReplicables} - LastMessage: {localMPClient.LastMessageFromServer}"); Seamless.TryShow($"2 NexusMajor: {Seamless.NexusVersion.Major} - ConrolledEntity {MySession.Static.ControlledEntity == null} - HumanPlayer {MySession.Static.LocalHumanPlayer == null} - Character {MySession.Static.LocalCharacter == null}"); UnloadOldEntities(); ResetReplicationTime(false); //MyMultiplayer.Static.ReplicationLayer.Disconnect(); //MyMultiplayer.Static.ReplicationLayer.Dispose(); //MyMultiplayer.Static.Dispose(); } private static bool PreventEvents(IPacketData data, bool reliable) { return !PreventRPC; } private void UnloadOldEntities() { foreach (var ent in MyEntities.GetEntities().ToList()) { if (ent is MyPlanet || ent is MyCubeGrid) continue; if((ent.EntityId == _OriginalCharacterEntity && keepGrid) && ent is MyCharacter character) { continue; } ent.Close(); } List grids = new List(); foreach (var gridgroup in MyCubeGridGroups.GetGridGroups(VRage.Game.ModAPI.GridLinkTypeEnum.Physical, grids)) { List localGrids = new List(); localGrids = gridgroup.GetGrids(localGrids); if (localGrids.Count == 0) continue; if (localGrids.Any(x => x.EntityId == _OriginalGridEntity) && keepGrid) { foreach(var grid in localGrids) { MyEntity ent = grid as MyEntity; allGrids.Add((MyCubeGrid)grid); //grid.Synchronized = false; //ent.SyncFlag = false; //ent.Save = false; } continue; } //Delete foreach (var grid in localGrids) { grid.Close(); } } //Following event doesnt clear networked replicables MyMultiplayer.Static.ReplicationLayer.Dispose(); ClearClientReplicables(); PreventRPC = true; StartPacketCheck = true; //_ClearTransportLayer.Invoke(_TransportLayer.GetValue(MyMultiplayer.Static.SyncLayer), null); } private void ClearClientReplicables() { MyReplicationClient replicationClient = (MyReplicationClient)MyMultiplayer.Static.ReplicationLayer; Dictionary networkedobjs = (Dictionary)typeof(MyReplicationLayer).GetField("m_networkIDToObject", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(replicationClient); MethodInfo destroyreplicable = typeof(MyReplicationClient).GetMethod("ReplicableDestroy", BindingFlags.Instance | BindingFlags.NonPublic); int i = 0; foreach(var obj in networkedobjs) { destroyreplicable.Invoke(replicationClient, new object[] { obj.Value, true }); i++; } Seamless.TryShow($"Cleared {i} replicables"); } private static void LoadDestinationServer() { MyReplicationClient clienta = (MyReplicationClient)MyMultiplayer.Static.ReplicationLayer; Seamless.TryShow($"5 Streaming: {clienta.HasPendingStreamingReplicables} - LastMessage: {clienta.LastMessageFromServer}"); 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 * * */ List clients = new List(); foreach (var client in Sync.Clients.GetClients()) { clients.Add(client.SteamUserId); Seamless.TryShow($"ADDING {client.SteamUserId} - {Sync.MyId}"); } foreach (var client in clients) { if (client == TargetServerID || client == Sync.MyId) continue; Seamless.TryShow($"REMOVING {client}"); Sync.Clients.RemoveClient(client); } 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 PreventRPC = false; //_ClearTransportLayer.Invoke(_TransportLayer.GetValue(MyMultiplayer.Static.SyncLayer), null); StartEntitySync(); PauseClient.Invoke(MyMultiplayer.Static, new object[] { false }); MySandboxGame.PausePop(); } private static 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 static void StartEntitySync() { Seamless.TryShow("Requesting Player From Server"); //Request client state batch (MyMultiplayer.Static as MyMultiplayerClientBase).RequestBatchConfirmation(); MyMultiplayer.Static.PendingReplicablesDone += Static_PendingReplicablesDone; //typeof(MyGuiScreenTerminal).GetMethod("CreateTabs") //MySession.Static.LocalHumanPlayer.Controller.TakeControl(originalControlledEntity); MyGuiSandbox.UnloadContent(); MyGuiSandbox.LoadContent(); //MyGuiSandbox.CreateScreen(MyPerGameSettings.GUI.HUDScreen); MyRenderProxy.RebuildCullingStructure(); MyRenderProxy.CollectGarbage(); Seamless.TryShow("OnlinePlayers: " + MySession.Static.Players.GetOnlinePlayers().Count); Seamless.TryShow("Loading Complete!"); } private static void Static_PendingReplicablesDone() { if (MySession.Static.VoxelMaps.Instances.Count > 0) { 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 }); MyGuiScreenHudSpace.Static?.RecreateControls(true); } public static void ResetReplicationTime(bool ClientReady) { if (!ClientReady) { PatchUtils.GetField(typeof(MyReplicationClient), "m_lastServerTimestamp").SetValue(MyMultiplayer.Static.ReplicationLayer, MyTimeSpan.Zero); PatchUtils.GetField(typeof(MyReplicationClient), "m_lastServerTimeStampReceivedTime").SetValue(MyMultiplayer.Static.ReplicationLayer, MyTimeSpan.Zero); PatchUtils.GetField(typeof(MyReplicationClient), "m_clientStartTimeStamp").SetValue(MyMultiplayer.Static.ReplicationLayer, MyTimeSpan.Zero); PatchUtils.GetField(typeof(MyReplicationClient), "m_lastTime").SetValue(MyMultiplayer.Static.ReplicationLayer, MyTimeSpan.Zero); PatchUtils.GetField(typeof(MyReplicationClient), "m_lastClientTime").SetValue(MyMultiplayer.Static.ReplicationLayer, MyTimeSpan.Zero); PatchUtils.GetField(typeof(MyReplicationClient), "m_lastServerTime").SetValue(MyMultiplayer.Static.ReplicationLayer, MyTimeSpan.Zero); PatchUtils.GetField(typeof(MyReplicationClient), "m_lastClientTimestamp").SetValue(MyMultiplayer.Static.ReplicationLayer, MyTimeSpan.Zero); PatchUtils.GetField(typeof(MyReplicationClient), "m_lastStateSyncPacketId").SetValue(MyMultiplayer.Static.ReplicationLayer,(byte)0); PatchUtils.GetField(typeof(MyReplicationClient), "m_lastStreamingPacketId").SetValue(MyMultiplayer.Static.ReplicationLayer, (byte)0); PatchUtils.GetField(typeof(MyReplicationClient), "m_lastStreamingPacketId").SetValue(MyMultiplayer.Static.ReplicationLayer, (byte)0); //PatchUtils.GetField(typeof(MyReplicationClient), "m_acks").SetValue(MyMultiplayer.Static.ReplicationLayer, new List()); } PatchUtils.GetField(typeof(MyReplicationClient), "m_clientReady").SetValue(MyMultiplayer.Static.ReplicationLayer, ClientReady); } } }