diff --git a/Components/EntityPerformanceImprovements.cs b/Components/EntityPerformanceImprovements.cs new file mode 100644 index 0000000..a6b4ba8 --- /dev/null +++ b/Components/EntityPerformanceImprovements.cs @@ -0,0 +1,107 @@ +using HarmonyLib; +using Sandbox.Game.Entities; +using SeamlessClient.Utilities; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using VRage.Game.Entity; + +namespace SeamlessClient.Components +{ + public class EntityPerformanceImprovements : ComponentBase + { + private static bool isEnabled = false; + private static ConcurrentDictionary EntityNameReverseLookup = new ConcurrentDictionary(); + + + public override void Patch(Harmony patcher) + { + if (!isEnabled) + return; + + + + var setEntityName = PatchUtils.GetMethod(PatchUtils.MyEntitiesType, "SetEntityName"); + var removeName = PatchUtils.GetMethod(PatchUtils.MyEntitiesType, "RemoveName"); + var isNameExists = PatchUtils.GetMethod(PatchUtils.MyEntitiesType, "IsNameExists"); + var unloadData = PatchUtils.GetMethod(PatchUtils.MyEntitiesType, "UnloadData"); + + patcher.Patch(setEntityName, prefix: new HarmonyMethod(Get(typeof(EntityPerformanceImprovements), nameof(SetEntityName)))); + patcher.Patch(removeName, prefix: new HarmonyMethod(Get(typeof(EntityPerformanceImprovements), nameof(RemoveName)))); + patcher.Patch(isNameExists, prefix: new HarmonyMethod(Get(typeof(EntityPerformanceImprovements), nameof(IsNameExists)))); + patcher.Patch(unloadData, postfix: new HarmonyMethod(Get(typeof(EntityPerformanceImprovements), nameof(UnloadData)))); + + base.Patch(patcher); + } + + + + // reverse dictionary + + private static bool SetEntityName(MyEntity myEntity, bool possibleRename) + { + if (string.IsNullOrEmpty(myEntity.Name)) + return false; + + + if (possibleRename && EntityNameReverseLookup.ContainsKey(myEntity.EntityId)) + { + var previousName = EntityNameReverseLookup[myEntity.EntityId]; + if (previousName != myEntity.Name) MyEntities.m_entityNameDictionary.Remove(previousName); + } + + + if (MyEntities.m_entityNameDictionary.TryGetValue(myEntity.Name, out var myEntity1)) + { + if (myEntity1 == myEntity) + return false; + } + else + { + MyEntities.m_entityNameDictionary[myEntity.Name] = myEntity; + EntityNameReverseLookup[myEntity.EntityId] = myEntity.Name; + } + + return false; + } + + private static bool RemoveName(MyEntity entity) + { + if (string.IsNullOrEmpty(entity.Name)) + return false; + MyEntities.m_entityNameDictionary.Remove(entity.Name); + EntityNameReverseLookup.Remove(entity.EntityId); + return false; + } + + private static bool IsNameExists(ref bool __result, MyEntity entity, string name) + { + if (string.IsNullOrEmpty(entity.Name)) + { + __result = false; + return false; + } + + if (MyEntities.m_entityNameDictionary.ContainsKey(name)) + { + var ent = MyEntities.m_entityNameDictionary[entity.Name]; + __result = ent != entity; + return false; + } + + __result = false; + return false; + } + + private static void UnloadData() + { + EntityNameReverseLookup.Clear(); + } + + + } +} diff --git a/Components/LoadingScreenComponent.cs b/Components/LoadingScreenComponent.cs new file mode 100644 index 0000000..cfd5463 --- /dev/null +++ b/Components/LoadingScreenComponent.cs @@ -0,0 +1,177 @@ +using HarmonyLib; +using Sandbox.Game.Gui; +using Sandbox.Graphics.GUI; +using Sandbox.Graphics; +using SeamlessClient.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using VRage.Utils; +using VRageMath; +using Sandbox.Game.World; +using Sandbox.Engine.Multiplayer; +using VRage.Game; +using Sandbox.Engine.Networking; +using VRage; +using VRage.GameServices; + +namespace SeamlessClient.Components +{ + public class LoadingScreenComponent : ComponentBase + { + private static MethodInfo LoadMultiplayer; + + private static string _loadingScreenTexture = null; + private static string _serverName; + + + public override void Patch(Harmony patcher) + { + var loadingAction = PatchUtils.GetMethod(typeof(MySessionLoader), "LoadMultiplayerSession"); + var loadingScreenDraw = PatchUtils.GetMethod(typeof(MyGuiScreenLoading), "DrawInternal"); + LoadMultiplayer = PatchUtils.GetMethod(typeof(MySession), "LoadMultiplayer"); + + + patcher.Patch(loadingAction, prefix: new HarmonyMethod(Get(typeof(LoadingScreenComponent), nameof(LoadMultiplayerSession)))); + patcher.Patch(loadingScreenDraw, prefix: new HarmonyMethod(Get(typeof(LoadingScreenComponent), nameof(DrawInternal)))); + + + + base.Patch(patcher); + } + + + + private static bool LoadMultiplayerSession(MyObjectBuilder_World world, MyMultiplayerBase multiplayerSession) + { + MyLog.Default.WriteLine("LoadSession() - Start"); + if (!MyWorkshop.CheckLocalModsAllowed(world.Checkpoint.Mods, false)) + { + MyGuiSandbox.AddScreen(MyGuiSandbox.CreateMessageBox(MyMessageBoxStyleEnum.Error, MyMessageBoxButtonsType.OK, messageCaption: MyTexts.Get(MyCommonTexts.MessageBoxCaptionError), messageText: MyTexts.Get(MyCommonTexts.DialogTextLocalModsDisabledInMultiplayer))); + MyLog.Default.WriteLine("LoadSession() - End"); + return false; + } + + + MyWorkshop.DownloadModsAsync(world.Checkpoint.Mods, delegate (MyGameServiceCallResult result) + { + switch (result) + { + case MyGameServiceCallResult.NotEnoughSpace: + MyGuiSandbox.AddScreen(MyGuiSandbox.CreateMessageBox(MyMessageBoxStyleEnum.Error, MyMessageBoxButtonsType.OK, messageCaption: MyTexts.Get(MyCommonTexts.MessageBoxCaptionError), messageText: MyTexts.Get(MyCommonTexts.DialogTextDownloadModsFailed_NotEnoughSpace), okButtonText: null, cancelButtonText: null, yesButtonText: null, noButtonText: null, callback: delegate + { + MySessionLoader.UnloadAndExitToMenu(); + })); + break; + case MyGameServiceCallResult.OK: + MyScreenManager.CloseAllScreensNowExcept(null); + MyGuiSandbox.Update(16); + + if (MySession.Static != null) + { + MySession.Static.Unload(); + MySession.Static = null; + } + + MySessionLoader.StartLoading(delegate + { + LoadMultiplayer.Invoke(null, new object[] { world, multiplayerSession }); + }); + + break; + + default: + multiplayerSession.Dispose(); + MySessionLoader.UnloadAndExitToMenu(); + if (MyGameService.IsOnline) + { + MyGuiSandbox.AddScreen(MyGuiSandbox.CreateMessageBox(MyMessageBoxStyleEnum.Error, MyMessageBoxButtonsType.OK, messageCaption: MyTexts.Get(MyCommonTexts.MessageBoxCaptionError), messageText: MyTexts.Get(MyCommonTexts.DialogTextDownloadModsFailed))); + } + else + { + MyGuiSandbox.AddScreen(MyGuiSandbox.CreateMessageBox(MyMessageBoxStyleEnum.Error, MyMessageBoxButtonsType.OK, messageCaption: MyTexts.Get(MyCommonTexts.MessageBoxCaptionError), messageText: new StringBuilder(string.Format(MyTexts.GetString(MyCommonTexts.DialogTextDownloadModsFailedSteamOffline), MySession.GameServiceDisplayName)))); + } + break; + } + MyLog.Default.WriteLine("LoadSession() - End"); + }, delegate + { + multiplayerSession.Dispose(); + MySessionLoader.UnloadAndExitToMenu(); + }); + + return false; + } + + private void OnFinished(MyGameServiceCallResult result) + { + + } + + + private static bool DrawInternal(MyGuiScreenLoading __instance) + { + //If we dont have a custom loading screen texture, do not do the special crap below + + const string mFont = "LoadingScreen"; + var mTransitionAlpha = (float)typeof(MyGuiScreenBase).GetField("m_transitionAlpha", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(__instance); + MyGuiManager.DrawString(mFont, "Nexus & SeamlessClient Made by: Casimir", new Vector2(0.95f, 0.95f), + MyGuiSandbox.GetDefaultTextScaleWithLanguage() * 1.1f, + new Color(MyGuiConstants.LOADING_PLEASE_WAIT_COLOR * mTransitionAlpha), + MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_BOTTOM); + + + + if (string.IsNullOrEmpty(_loadingScreenTexture)) + return true; + + + + + var color = new Color(255, 255, 255, 250); + color.A = (byte)(color.A * mTransitionAlpha); + var fullscreenRectangle = MyGuiManager.GetFullscreenRectangle(); + MyGuiManager.DrawSpriteBatch("Textures\\GUI\\Blank.dds", fullscreenRectangle, Color.Black, false, true); + Rectangle outRect; + MyGuiManager.GetSafeHeightFullScreenPictureSize(MyGuiConstants.LOADING_BACKGROUND_TEXTURE_REAL_SIZE, + out outRect); + MyGuiManager.DrawSpriteBatch(_loadingScreenTexture, outRect, + new Color(new Vector4(1f, 1f, 1f, mTransitionAlpha)), true, true); + MyGuiManager.DrawSpriteBatch("Textures\\Gui\\Screens\\screen_background_fade.dds", outRect, + new Color(new Vector4(1f, 1f, 1f, mTransitionAlpha)), true, true); + + //MyGuiSandbox.DrawGameLogoHandler(m_transitionAlpha, MyGuiManager.ComputeFullscreenGuiCoordinate(MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP, 44, 68)); + + var loadScreen = $"Loading into {_serverName}! Please wait!"; + + + MyGuiManager.DrawString(mFont, loadScreen, new Vector2(0.5f, 0.95f), + MyGuiSandbox.GetDefaultTextScaleWithLanguage() * 1.1f, + new Color(MyGuiConstants.LOADING_PLEASE_WAIT_COLOR * mTransitionAlpha), + MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_BOTTOM); + + + + /* + if (string.IsNullOrEmpty(m_customTextFromConstructor)) + { + string font = m_font; + Vector2 positionAbsoluteBottomLeft = m_multiTextControl.GetPositionAbsoluteBottomLeft(); + Vector2 textSize = m_multiTextControl.TextSize; + Vector2 normalizedCoord = positionAbsoluteBottomLeft + new Vector2((m_multiTextControl.Size.X - textSize.X) * 0.5f + 0.025f, 0.025f); + MyGuiManager.DrawString(font, m_authorWithDash.ToString(), normalizedCoord, MyGuiSandbox.GetDefaultTextScaleWithLanguage()); + } + */ + + + //m_multiTextControl.Draw(1f, 1f); + + return false; + } + + + } +} diff --git a/OnlinePlayersWindow/PlayersWindow.cs b/Components/PlayersWindowComponent.cs similarity index 68% rename from OnlinePlayersWindow/PlayersWindow.cs rename to Components/PlayersWindowComponent.cs index 01db270..4d96241 100644 --- a/OnlinePlayersWindow/PlayersWindow.cs +++ b/Components/PlayersWindowComponent.cs @@ -2,6 +2,7 @@ using Sandbox.Game; using Sandbox.Game.Gui; using Sandbox.Game.World; +using SeamlessClient.Messages; using SeamlessClient.Utilities; using System; using System.Collections.Generic; @@ -12,7 +13,7 @@ using System.Threading.Tasks; namespace SeamlessClient.OnlinePlayersWindow { - public class PlayersWindow : ComponentBase + public class PlayersWindowComponent : ComponentBase { public override void Patch(Harmony patcher) @@ -26,6 +27,11 @@ namespace SeamlessClient.OnlinePlayersWindow MyPerGameSettings.GUI.PlayersScreen = typeof(OnlineNexusPlayersWindow); } + public static void ApplyRecievedPlayers(List servers, int CurrentServer) + { + Seamless.TryShow($"Recieved {CurrentServer} - {servers.Count}"); + } + diff --git a/Components/ServerSwitcherComponent.cs b/Components/ServerSwitcherComponent.cs new file mode 100644 index 0000000..27f787b --- /dev/null +++ b/Components/ServerSwitcherComponent.cs @@ -0,0 +1,433 @@ +using HarmonyLib; +using Sandbox; +using Sandbox.Engine.Multiplayer; +using Sandbox.Engine.Networking; +using Sandbox.Game; +using Sandbox.Game.Entities; +using Sandbox.Game.Gui; +using Sandbox.Game.GUI; +using Sandbox.Game.Multiplayer; +using Sandbox.Game.SessionComponents; +using Sandbox.Game.World; +using Sandbox.Game.World.Generator; +using Sandbox.ModAPI; +using SeamlessClient.Components; +using SeamlessClient.OnlinePlayersWindow; +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.Game.ModAPI; +using VRage.GameServices; +using VRage.Network; +using VRage.Utils; +using VRageRender; +using VRageRender.Messages; + +namespace SeamlessClient.ServerSwitching +{ + public class ServerSwitcherComponent : ComponentBase + { + private static FieldInfo RemoteAdminSettings; + private static FieldInfo AdminSettings; + private static MethodInfo UnloadProceduralWorldGenerator; + private static MethodInfo GpsRegisterChat; + private static MethodInfo LoadMembersFromWorld; + private static MethodInfo InitVirtualClients; + + private static FieldInfo VirtualClients; + + + public static ServerSwitcherComponent Instance { get; private set; } + + private MyGameServerItem TargetServer { get; set; } + private MyObjectBuilder_World TargetWorld { get; set; } + private string OldArmorSkin { get; set; } = string.Empty; + + private bool needsEntityUnload = true; + + public static event EventHandler OnJoinEvent; + + + + public ServerSwitcherComponent() + { + Instance = this; + } + + + public override void Patch(Harmony patcher) + { + RemoteAdminSettings = PatchUtils.GetField(typeof(MySession), "m_remoteAdminSettings"); + AdminSettings = PatchUtils.GetField(typeof(MySession), "m_adminSettings"); + VirtualClients = PatchUtils.GetField(typeof(MySession), "VirtualClients"); + + UnloadProceduralWorldGenerator = PatchUtils.GetMethod(typeof(MyProceduralWorldGenerator), "UnloadData"); + GpsRegisterChat = PatchUtils.GetMethod(typeof(MyGpsCollection), "RegisterChat"); + LoadMembersFromWorld = PatchUtils.GetMethod(typeof(MySession), "LoadMembersFromWorld"); + InitVirtualClients = PatchUtils.GetMethod(PatchUtils.VirtualClientsType, "Init"); + + + + var onJoin = PatchUtils.GetMethod(PatchUtils.ClientType, "OnUserJoined"); + patcher.Patch(onJoin, postfix: new HarmonyMethod(Get(typeof(ServerSwitcherComponent), nameof(OnUserJoined)))); + + base.Patch(patcher); + } + + + + private static void OnUserJoined(ref JoinResultMsg msg) + { + if (msg.JoinResult == JoinResult.OK) + { + Seamless.TryShow("User Joined! Result: " + msg.JoinResult.ToString()); + + //Invoke the switch event + OnJoinEvent?.Invoke(null, msg); + } + } + + + + + + public override void Initilized() + { + base.Initilized(); + } + + + + + + public void StartBackendSwitch(MyGameServerItem TargetServer, MyObjectBuilder_World TargetWorld) + { + this.TargetServer = TargetServer; + this.TargetWorld = TargetWorld; + OldArmorSkin = MySession.Static.LocalHumanPlayer.BuildArmorSkin; + + if (Seamless.NexusVersion.Major >= 2) + needsEntityUnload = false; + + + AsyncInvoke.InvokeAsync(() => + { + if(needsEntityUnload) + MySession.Static.SetCameraController(MyCameraControllerEnum.SpectatorFixed); + + + UnloadServer(); + SetNewMultiplayerClient(); + + + }); + } + + private void UnloadServer() + { + if (MyMultiplayer.Static == null) + throw new Exception("MyMultiplayer.Static is null on unloading? dafuq?"); + + UnloadOldEntities(); + + //Close and Cancel any screens (Medical or QuestLog) + MySessionComponentIngameHelp component = MySession.Static.GetComponent(); + component?.TryCancelObjective(); + MyGuiScreenMedicals.Close(); + + + //Clear all old players and clients. + Sync.Clients.Clear(); + Sync.Players.ClearPlayers(); + + //Unregister Chat + MyHud.Chat.UnregisterChat(MyMultiplayer.Static); + + //Clear all local GPS Points + MySession.Static.Gpss.RemovePlayerGpss(MySession.Static.LocalPlayerId); + MyHud.GpsMarkers.Clear(); + + //DisconnectReplication + MyMultiplayer.Static.ReplicationLayer.Disconnect(); + MyMultiplayer.Static.ReplicationLayer.Dispose(); + MyMultiplayer.Static.Dispose(); + MyMultiplayer.Static = null; + } + + private void UnloadOldEntities() + { + foreach (var ent in MyEntities.GetEntities()) + { + if (ent is MyPlanet) + continue; + + //ent.Close(); + } + } + + private void SetNewMultiplayerClient() + { + OnJoinEvent += ServerSwitcherComponent_OnJoinEvent; + } + + private void ServerSwitcherComponent_OnJoinEvent(object sender, JoinResultMsg e) + { + OnJoinEvent -= ServerSwitcherComponent_OnJoinEvent; + if (e.JoinResult != JoinResult.OK) + { + Seamless.TryShow("Failed to join the target server!"); + return; + } + + + /* On Server Successfull Join + * + * + */ + + + SetWorldSettings(); + InitComponents(); + LoadConnectedClients(); + StartEntitySync(); + + + + MyMultiplayer.Static.OnSessionReady(); + + MyHud.Chat.RegisterChat(MyMultiplayer.Static); + GpsRegisterChat.Invoke(MySession.Static.Gpss, new object[] { MyMultiplayer.Static }); + + + // Allow the game to start proccessing incoming messages in the buffer + MyMultiplayer.Static.StartProcessingClientMessages(); + + //Recreate all controls... Will fix weird gui/paint/crap + MyGuiScreenHudSpace.Static.RecreateControls(true); + } + + + + + private void SetWorldSettings() + { + //Clear old list + MySession.Static.PromotedUsers.Clear(); + MySession.Static.CreativeTools.Clear(); + Dictionary AdminSettingsList = (Dictionary)RemoteAdminSettings.GetValue(MySession.Static); + AdminSettingsList.Clear(); + + + + // Set new world settings + MySession.Static.Name = MyStatControlText.SubstituteTexts(TargetWorld.Checkpoint.SessionName); + MySession.Static.Description = TargetWorld.Checkpoint.Description; + + MySession.Static.Mods = TargetWorld.Checkpoint.Mods; + MySession.Static.Settings = TargetWorld.Checkpoint.Settings; + MySession.Static.CurrentPath = MyLocalCache.GetSessionSavesPath(MyUtils.StripInvalidChars(TargetWorld.Checkpoint.SessionName), contentFolder: false, createIfNotExists: false); + MySession.Static.WorldBoundaries = TargetWorld.Checkpoint.WorldBoundaries; + MySession.Static.InGameTime = MyObjectBuilder_Checkpoint.DEFAULT_DATE; + MySession.Static.ElapsedGameTime = new TimeSpan(TargetWorld.Checkpoint.ElapsedGameTime); + MySession.Static.Settings.EnableSpectator = false; + + MySession.Static.Password = TargetWorld.Checkpoint.Password; + MySession.Static.PreviousEnvironmentHostility = TargetWorld.Checkpoint.PreviousEnvironmentHostility; + MySession.Static.RequiresDX = TargetWorld.Checkpoint.RequiresDX; + MySession.Static.CustomLoadingScreenImage = TargetWorld.Checkpoint.CustomLoadingScreenImage; + MySession.Static.CustomLoadingScreenText = TargetWorld.Checkpoint.CustomLoadingScreenText; + MySession.Static.CustomSkybox = TargetWorld.Checkpoint.CustomSkybox; + + try + { + MySession.Static.Gpss = new MyGpsCollection(); + MySession.Static.Gpss.LoadGpss(TargetWorld.Checkpoint); + + } + catch (Exception ex) + { + Seamless.TryShow($"An error occured while loading GPS points! You will have an empty gps list! \n {ex.ToString()}"); + } + + + MyRenderProxy.RebuildCullingStructure(); + //MySession.Static.Toolbars.LoadToolbars(checkpoint); + + Sync.Players.RespawnComponent.InitFromCheckpoint(TargetWorld.Checkpoint); + + + // Set new admin settings + if (TargetWorld.Checkpoint.PromotedUsers != null) + { + MySession.Static.PromotedUsers = TargetWorld.Checkpoint.PromotedUsers.Dictionary; + } + else + { + MySession.Static.PromotedUsers = new Dictionary(); + } + + + + + foreach (KeyValuePair item in TargetWorld.Checkpoint.AllPlayersData.Dictionary) + { + ulong clientId = item.Key.GetClientId(); + AdminSettingsEnum adminSettingsEnum = (AdminSettingsEnum)item.Value.RemoteAdminSettings; + if (TargetWorld.Checkpoint.RemoteAdminSettings != null && TargetWorld.Checkpoint.RemoteAdminSettings.Dictionary.TryGetValue(clientId, out var value)) + { + adminSettingsEnum = (AdminSettingsEnum)value; + } + if (!MyPlatformGameSettings.IsIgnorePcuAllowed) + { + adminSettingsEnum &= ~AdminSettingsEnum.IgnorePcu; + adminSettingsEnum &= ~AdminSettingsEnum.KeepOriginalOwnershipOnPaste; + } + + + AdminSettingsList[clientId] = adminSettingsEnum; + if (!Sync.IsDedicated && clientId == Sync.MyId) + { + AdminSettings.SetValue(MySession.Static, adminSettingsEnum); + } + + + + if (!MySession.Static.PromotedUsers.TryGetValue(clientId, out var value2)) + { + value2 = MyPromoteLevel.None; + } + if (item.Value.PromoteLevel > value2) + { + MySession.Static.PromotedUsers[clientId] = item.Value.PromoteLevel; + } + if (!MySession.Static.CreativeTools.Contains(clientId) && item.Value.CreativeToolsEnabled) + { + MySession.Static.CreativeTools.Add(clientId); + } + } + + + MySector.InitEnvironmentSettings(TargetWorld.Sector.Environment); + + string text = ((!string.IsNullOrEmpty(TargetWorld.Checkpoint.CustomSkybox)) ? TargetWorld.Checkpoint.CustomSkybox : MySector.EnvironmentDefinition.EnvironmentTexture); + MyRenderProxy.PreloadTextures(new string[1] { text }, TextureType.CubeMap); + + + + + } + + private void InitComponents() + { + MyModAPIHelper.Initialize(); + MySession.Static.LoadDataComponents(); + + //MySession.Static.LoadObjectBuildersComponents(TargetWorld.Checkpoint.SessionComponents); + MyModAPIHelper.Initialize(); + // MySession.Static.LoadObjectBuildersComponents(TargetWorld.Checkpoint.SessionComponents); + + UpdateWorldGenerator(); + //MethodInfo A = typeof(MySession).GetMethod("LoadGameDefinition", BindingFlags.Instance | BindingFlags.NonPublic); + // A.Invoke(MySession.Static, new object[] { TargetWorld.Checkpoint }); + } + + + private void UpdateWorldGenerator() + { + //This will re-init the MyProceduralWorldGenerator. (Not doing this will result in asteroids not rendering in properly) + + + //This shoud never be null + var Generator = MySession.Static.GetComponent(); + + //Force component to unload + UnloadProceduralWorldGenerator.Invoke(Generator, null); + + //Re-call the generator init + MyObjectBuilder_WorldGenerator GeneratorSettings = (MyObjectBuilder_WorldGenerator)TargetWorld.Checkpoint.SessionComponents.FirstOrDefault(x => x.GetType() == typeof(MyObjectBuilder_WorldGenerator)); + if (GeneratorSettings != null) + { + //Re-initilized this component (forces to update asteroid areas like not in planets etc) + Generator.Init(GeneratorSettings); + } + + //Force component to reload, re-syncing settings and seeds to the destination server + Generator.LoadData(); + + //We need to go in and force planets to be empty areas in the generator. This is originially done on planet init. + FieldInfo PlanetInitArgs = typeof(MyPlanet).GetField("m_planetInitValues", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + foreach (var Planet in MyEntities.GetEntities().OfType()) + { + MyPlanetInitArguments args = (MyPlanetInitArguments)PlanetInitArgs.GetValue(Planet); + + float MaxRadius = args.MaxRadius; + + Generator.MarkEmptyArea(Planet.PositionComp.GetPosition(), MaxRadius); + } + } + + private void StartEntitySync() + { + Seamless.TryShow("Requesting Player From Server"); + Sync.Players.RequestNewPlayer(Sync.MyId, 0, MyGameService.UserName, null, realPlayer: true, initialPlayer: true); + if (MySession.Static.ControlledEntity == null && Sync.IsServer && !Sandbox.Engine.Platform.Game.IsDedicated) + { + MyLog.Default.WriteLine("ControlledObject was null, respawning character"); + //m_cameraAwaitingEntity = true; + MyPlayerCollection.RequestLocalRespawn(); + } + + //Request client state batch + (MyMultiplayer.Static as MyMultiplayerClientBase).RequestBatchConfirmation(); + MyMultiplayer.Static.PendingReplicablesDone += Static_PendingReplicablesDone; + //typeof(MyGuiScreenTerminal).GetMethod("CreateTabs") + + MySession.Static.LoadDataComponents(); + //MyGuiSandbox.LoadData(false); + //MyGuiSandbox.AddScreen(MyGuiSandbox.CreateScreen(MyPerGameSettings.GUI.HUDScreen)); + MyRenderProxy.RebuildCullingStructure(); + MyRenderProxy.CollectGarbage(); + + Seamless.TryShow("OnlinePlayers: " + MySession.Static.Players.GetOnlinePlayers().Count); + Seamless.TryShow("Loading Complete!"); + } + + private void Static_PendingReplicablesDone() + { + if (MySession.Static.VoxelMaps.Instances.Count > 0) + { + MySandboxGame.AreClipmapsReady = false; + } + MyMultiplayer.Static.PendingReplicablesDone -= Static_PendingReplicablesDone; + } + + private void LoadConnectedClients() + { + LoadMembersFromWorld.Invoke(MySession.Static, new object[] { TargetWorld, MyMultiplayer.Static }); + + + //Re-Initilize Virtual clients + object VirtualClientsValue = VirtualClients.GetValue(MySession.Static); + InitVirtualClients.Invoke(VirtualClientsValue, null); + + + + MyPlayer.PlayerId? savingPlayerId = new MyPlayer.PlayerId(Sync.MyId); + if (!savingPlayerId.HasValue) + { + Seamless.TryShow("SavingPlayerID is null! Creating Default!"); + savingPlayerId = new MyPlayer.PlayerId(Sync.MyId); + } + Seamless.TryShow("Saving PlayerID: " + savingPlayerId.ToString()); + + Sync.Players.LoadConnectedPlayers(TargetWorld.Checkpoint, savingPlayerId); + Sync.Players.LoadControlledEntities(TargetWorld.Checkpoint.ControlledEntities, TargetWorld.Checkpoint.ControlledObject, savingPlayerId); + } + } +} diff --git a/Messages/ClientMessage.cs b/Messages/ClientMessage.cs index 65b05c3..d18c035 100644 --- a/Messages/ClientMessage.cs +++ b/Messages/ClientMessage.cs @@ -21,29 +21,35 @@ namespace SeamlessClient.Messages public class ClientMessage { [ProtoMember(1)] public ClientMessageType MessageType; - [ProtoMember(2)] public TransferData data; + [ProtoMember(2)] public byte[] MessageData; [ProtoMember(3)] public long IdentityID; [ProtoMember(4)] public ulong SteamID; [ProtoMember(5)] public string PluginVersion = "0"; + [ProtoMember(6)] public string NexusVersion = "0"; - public ClientMessage(ClientMessageType Type) + public ClientMessage(string PluginVersion) { - MessageType = Type; - - if (MyAPIGateway.Multiplayer == null || MyAPIGateway.Multiplayer.IsServer) return; - if (MyAPIGateway.Session.LocalHumanPlayer == null) return; + MessageType = ClientMessageType.FirstJoin; IdentityID = MySession.Static?.LocalHumanPlayer?.Identity?.IdentityId ?? 0; SteamID = MySession.Static?.LocalHumanPlayer?.Id.SteamId ?? 0; - //PluginVersion = SeamlessClient.Version; + this.PluginVersion = PluginVersion; + } - - public ClientMessage() + public TransferData GetTransferData() { + return MessageData == null ? default : MessageUtils.Deserialize(MessageData); } + public OnlinePlayerData GetOnlinePlayers() + { + if (MessageData == null) + return default; + var msg = MessageUtils.Deserialize(MessageData); + return msg; + } } } diff --git a/Messages/OnlinePlayerData.cs b/Messages/OnlinePlayerData.cs new file mode 100644 index 0000000..649507a --- /dev/null +++ b/Messages/OnlinePlayerData.cs @@ -0,0 +1,57 @@ +using ProtoBuf; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SeamlessClient.Messages +{ + [ProtoContract] + public class OnlinePlayerData + { + [ProtoMember(10)] public List OnlineServers = new List(); + + [ProtoMember(12)] public int currentServerID; + } + + [ProtoContract] + public class OnlineServer + { + [ProtoMember(2)] public List Players = new List(); + + [ProtoMember(3)] public bool ServerRunning = false; + + [ProtoMember(10)] public int ServerID; + + [ProtoMember(11)] public string ServerName; + + public OnlineServer() + { + } + } + + [ProtoContract] + public class OnlinePlayer + { + [ProtoMember(1)] public string PlayerName; + + [ProtoMember(2)] public ulong SteamID; + + [ProtoMember(3)] public long IdentityID; + + [ProtoMember(4)] public int OnServer; + + public OnlinePlayer(string PlayerName, ulong SteamID, long IdentityID, int OnServer) + { + this.PlayerName = PlayerName; + this.SteamID = SteamID; + this.IdentityID = IdentityID; + this.OnServer = OnServer; + } + + public OnlinePlayer() + { + } + } +} diff --git a/Messages/WorldRequestData.cs b/Messages/WorldRequestData.cs index 8783004..5f0dff2 100644 --- a/Messages/WorldRequestData.cs +++ b/Messages/WorldRequestData.cs @@ -25,6 +25,10 @@ namespace SeamlessClient.Messages [ProtoMember(5)] public MyObjectBuilder_Gps GpsCollection; + public WorldRequest() + { + } + public WorldRequest(ulong playerId, long playerIdentity, string name) { this.PlayerID = playerId; @@ -32,9 +36,7 @@ namespace SeamlessClient.Messages this.IdentityID = playerIdentity; } - public WorldRequest() - { - } + public void SerializeWorldData(MyObjectBuilder_World WorldData) { diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 3e8dc29..1464b76 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -33,5 +33,5 @@ using System.Runtime.InteropServices; // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] //Set these both to the same -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("2.0.0.0")] //Set these both to the same +[assembly: AssemblyFileVersion("2.0.0.0")] diff --git a/Seamless.cs b/Seamless.cs index 06a3a0b..15ec82e 100644 --- a/Seamless.cs +++ b/Seamless.cs @@ -3,6 +3,7 @@ using NLog.Fluent; using Sandbox.Game.World; using Sandbox.ModAPI; using SeamlessClient.Messages; +using SeamlessClient.OnlinePlayersWindow; using SeamlessClient.ServerSwitching; using SeamlessClient.Utilities; using System; @@ -13,6 +14,7 @@ using System.Net.NetworkInformation; using System.Reflection; using System.Text; using System.Threading.Tasks; +using VRage.GameServices; using VRage.Plugins; using VRage.Sync; using VRage.Utils; @@ -22,12 +24,17 @@ namespace SeamlessClient public class Seamless : IPlugin { public static Version SeamlessVersion; + public static Version NexusVersion; private static Harmony SeamlessPatcher; public const ushort SeamlessClientNetId = 2936; private List allComps = new List(); private Assembly thisAssembly; private bool Initilized = false; + public static bool isSeamlessServer = false; + + + #if DEBUG public static bool isDebug = true; @@ -72,6 +79,7 @@ namespace SeamlessClient } } } + private void PatchComponents(Harmony patcher) { foreach (ComponentBase component in allComps) @@ -89,6 +97,7 @@ namespace SeamlessClient } } } + private void InitilizeComponents() { foreach(ComponentBase component in allComps) @@ -112,27 +121,35 @@ namespace SeamlessClient if (!fromServer || sender != 0) return; + if (MyAPIGateway.Multiplayer == null || MyAPIGateway.Multiplayer.IsServer) return; + if (MyAPIGateway.Session.LocalHumanPlayer == null) return; + + ClientMessage msg = MessageUtils.Deserialize(data); if (msg == null) return; + //Get Nexus Version + NexusVersion = Version.Parse(msg.NexusVersion); switch (msg.MessageType) { case ClientMessageType.FirstJoin: - MyAPIGateway.Multiplayer?.SendMessageToServer(SeamlessClientNetId, MessageUtils.Serialize(new ClientMessage(ClientMessageType.FirstJoin))); + + ClientMessage response = new ClientMessage(SeamlessVersion.ToString()); + MyAPIGateway.Multiplayer?.SendMessageToServer(SeamlessClientNetId, MessageUtils.Serialize(response)); break; case ClientMessageType.TransferServer: - ServerSwitcher.StartSwitching(msg.data); + StartSwitch(msg.GetTransferData()); break; - case ClientMessageType.OnlinePlayers: + case ClientMessageType.OnlinePlayers: //Not implemented yet + var playerData = msg.GetOnlinePlayers(); + PlayersWindowComponent.ApplyRecievedPlayers(playerData.OnlineServers, playerData.currentServerID); break; } - - } @@ -160,6 +177,31 @@ namespace SeamlessClient + public static void StartSwitch(TransferData targetServer) + { + if (targetServer.TargetServerId == 0) + { + Seamless.TryShow("This is not a valid server!"); + return; + } + + var server = new MyGameServerItem + { + ConnectionString = targetServer.IpAddress, + SteamID = targetServer.TargetServerId, + Name = targetServer.ServerName + }; + + + Seamless.TryShow($"Beginning Redirect to server: {targetServer.TargetServerId}"); + var world = targetServer.WorldRequest.DeserializeWorldData(); + ServerSwitcherComponent.Instance.StartBackendSwitch(server, world); + } + + + + + diff --git a/SeamlessClient.csproj b/SeamlessClient.csproj index c0a2c88..9cd9f2c 100644 --- a/SeamlessClient.csproj +++ b/SeamlessClient.csproj @@ -61,6 +61,10 @@ GameBinaries\Sandbox.Graphics.dll False + + GameBinaries\SpaceEngineers.Game.dll + False + @@ -86,16 +90,24 @@ GameBinaries\VRage.Math.dll False + + GameBinaries\VRage.Render.dll + + + + + - + - + + @@ -105,7 +117,9 @@ PreserveNewest - + + + PreserveNewest diff --git a/ServerSwitching/ServerSwitcher.cs b/ServerSwitching/ServerSwitcher.cs deleted file mode 100644 index 1a0d717..0000000 --- a/ServerSwitching/ServerSwitcher.cs +++ /dev/null @@ -1,27 +0,0 @@ -using SeamlessClient.Messages; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http.Headers; -using System.Text; -using System.Threading.Tasks; -using VRageMath; - -namespace SeamlessClient.ServerSwitching -{ - public class ServerSwitcher - { - - public static void StartSwitching(TransferData targetServer) - { - - } - - - - - - - - } -} diff --git a/Utilities/AsyncInvoke.cs b/Utilities/AsyncInvoke.cs new file mode 100644 index 0000000..19d797b --- /dev/null +++ b/Utilities/AsyncInvoke.cs @@ -0,0 +1,118 @@ +using Sandbox; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace SeamlessClient.Utilities +{ + public static class AsyncInvoke + { + public static Task InvokeAsync(Action action, [CallerMemberName] string caller = "SeamlessClient") + { + + + //Jimm thank you. This is the best + var ctx = new TaskCompletionSource(); + MySandboxGame.Static.Invoke(() => + { + try + { + action.Invoke(); + ctx.SetResult(null); + ctx.Task.ContinueWith(task => task.Dispose()); + } + catch (Exception e) + { + ctx.SetException(e); + } + + }, caller); + return ctx.Task; + } + + public static Task InvokeAsync(Func action, [CallerMemberName] string caller = "SeamlessClient") + { + + + //Jimm thank you. This is the best + var ctx = new TaskCompletionSource(); + MySandboxGame.Static.Invoke(() => + { + try + { + ctx.SetResult(action.Invoke()); + ctx.Task.ContinueWith(task => task.Dispose()); + } + catch (Exception e) + { + ctx.SetException(e); + } + + }, caller); + return ctx.Task; + } + + public static Task InvokeAsync(Func action, T1 arg, [CallerMemberName] string caller = "SeamlessClient") + { + //Jimm thank you. This is the best + var ctx = new TaskCompletionSource(); + MySandboxGame.Static.Invoke(() => + { + try + { + ctx.SetResult(action.Invoke(arg)); + ctx.Task.ContinueWith(task => task.Dispose()); + } + catch (Exception e) + { + ctx.SetException(e); + } + + }, caller); + return ctx.Task; + } + + public static Task InvokeAsync(Func action, T1 arg, T2 arg2, [CallerMemberName] string caller = "SeamlessClient") + { + //Jimm thank you. This is the best + var ctx = new TaskCompletionSource(); + MySandboxGame.Static.Invoke(() => + { + try + { + ctx.SetResult(action.Invoke(arg, arg2)); + ctx.Task.ContinueWith(task => task.Dispose()); + } + catch (Exception e) + { + ctx.SetException(e); + } + + }, caller); + return ctx.Task; + } + + public static Task InvokeAsync(Func action, T1 arg, T2 arg2, T3 arg3, [CallerMemberName] string caller = "SeamlessClient") + { + //Jimm thank you. This is the best + var ctx = new TaskCompletionSource(); + MySandboxGame.Static.Invoke(() => + { + try + { + ctx.SetResult(action.Invoke(arg, arg2, arg3)); + ctx.Task.ContinueWith(task => task.Dispose()); + } + catch (Exception e) + { + ctx.SetException(e); + } + + }, caller); + return ctx.Task; + } + } +} diff --git a/Utilities/ComponentBase.cs b/Utilities/ComponentBase.cs index 42e1dd5..39be352 100644 --- a/Utilities/ComponentBase.cs +++ b/Utilities/ComponentBase.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -23,5 +24,10 @@ namespace SeamlessClient.Utilities public virtual void Destroy() { } + + public MethodInfo Get(Type type, string v) + { + return type.GetMethod(v, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + } } } diff --git a/Utilities/PatchUtils.cs b/Utilities/PatchUtils.cs new file mode 100644 index 0000000..7f47d7a --- /dev/null +++ b/Utilities/PatchUtils.cs @@ -0,0 +1,152 @@ +using HarmonyLib; +using Sandbox.Game.Gui; +using Sandbox.Game.Multiplayer; +using Sandbox.Game.World.Generator; +using Sandbox.Game.World; +using SeamlessClient.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using VRage.GameServices; +using VRage.Network; + +namespace SeamlessClient.Components +{ + public class PatchUtils : ComponentBase + { + /* All internal classes Types */ + public static readonly Type ClientType = + Type.GetType("Sandbox.Engine.Multiplayer.MyMultiplayerClient, Sandbox.Game"); + + public static readonly Type SyncLayerType = Type.GetType("Sandbox.Game.Multiplayer.MySyncLayer, Sandbox.Game"); + + public static readonly Type MyTransportLayerType = + Type.GetType("Sandbox.Engine.Multiplayer.MyTransportLayer, Sandbox.Game"); + + public static readonly Type MySessionType = Type.GetType("Sandbox.Game.World.MySession, Sandbox.Game"); + + public static readonly Type VirtualClientsType = + Type.GetType("Sandbox.Engine.Multiplayer.MyVirtualClients, Sandbox.Game"); + + public static readonly Type GUIScreenChat = Type.GetType("Sandbox.Game.Gui.MyGuiScreenChat, Sandbox.Game"); + + public static readonly Type MyMultiplayerClientBase = + Type.GetType("Sandbox.Engine.Multiplayer.MyMultiplayerClientBase, Sandbox.Game"); + + public static readonly Type MySteamServerDiscovery = + Type.GetType("VRage.Steam.MySteamServerDiscovery, Vrage.Steam"); + + public static readonly Type MyEntitiesType = + Type.GetType("Sandbox.Game.Entities.MyEntities, Sandbox.Game"); + + public static readonly Type MySlimBlockType = + Type.GetType("Sandbox.Game.Entities.Cube.MySlimBlock, Sandbox.Game"); + + /* Harmony Patcher */ + private static readonly Harmony Patcher = new Harmony("SeamlessClientPatcher"); + + + /* Static Contructors */ + public static ConstructorInfo ClientConstructor { get; private set; } + public static ConstructorInfo SyncLayerConstructor { get; private set; } + public static ConstructorInfo TransportLayerConstructor { get; private set; } + public static ConstructorInfo MySessionConstructor { get; private set; } + public static ConstructorInfo MyMultiplayerClientBaseConstructor { get; private set; } + + + /* Static FieldInfos and PropertyInfos */ + public static PropertyInfo MySessionLayer { get; private set; } + + public static FieldInfo AdminSettings { get; private set; } + public static FieldInfo MPlayerGpsCollection { get; private set; } + + + /* Static MethodInfos */ + public static MethodInfo LoadPlayerInternal { get; private set; } + + + public static MethodInfo SendPlayerData; + + + public static event EventHandler OnJoinEvent; + + + /* WorldGenerator */ + public static MethodInfo UnloadProceduralWorldGenerator; + + + + + public override void Patch(Harmony patcher) + { + ClientConstructor = GetConstructor(ClientType, new[] { typeof(MyGameServerItem), SyncLayerType }); + SyncLayerConstructor = GetConstructor(SyncLayerType, new[] { MyTransportLayerType }); + TransportLayerConstructor = GetConstructor(MyTransportLayerType, new[] { typeof(int) }); + MySessionConstructor = GetConstructor(MySessionType, new[] { typeof(MySyncLayer), typeof(bool) }); + MyMultiplayerClientBaseConstructor = GetConstructor(MyMultiplayerClientBase, new[] { typeof(MySyncLayer) }); + + + /* Get Fields and Properties */ + MySessionLayer = GetProperty(typeof(MySession), "SyncLayer"); + AdminSettings = GetField(typeof(MySession), "m_adminSettings"); + + MPlayerGpsCollection = GetField(typeof(MyPlayerCollection), "m_players"); + + + + /* Get Methods */ + + LoadPlayerInternal = GetMethod(typeof(MyPlayerCollection), "LoadPlayerInternal"); + SendPlayerData = GetMethod(ClientType, "SendPlayerData"); + + + + + //MethodInfo ConnectToServer = GetMethod(typeof(MyGameService), "ConnectToServer", BindingFlags.Static | BindingFlags.Public); + base.Patch(patcher); + } + + + + #region PatchMethods + + public static MethodInfo GetMethod(Type type, string methodName) + { + + var foundMethod = AccessTools.Method(type, methodName); + if (foundMethod == null) + throw new NullReferenceException($"Method for {methodName} is null!"); + return foundMethod; + } + + public static FieldInfo GetField(Type type, string fieldName) + { + var foundField = AccessTools.Field(type, fieldName); + if (foundField == null) + throw new NullReferenceException($"Field for {fieldName} is null!"); + return foundField; + } + + public static PropertyInfo GetProperty(Type type, string propertyName) + { + var foundProperty = AccessTools.Property(type, propertyName); + if (foundProperty == null) + throw new NullReferenceException($"Property for {propertyName} is null!"); + return foundProperty; + } + + public static ConstructorInfo GetConstructor(Type type, Type[] types) + { + var foundConstructor = AccessTools.Constructor(type, types); + if (foundConstructor == null) + throw new NullReferenceException($"Contructor for {type.Name} is null!"); + return foundConstructor; + } + + #endregion + + } +}