diff --git a/Jenkinsfile b/Jenkinsfile index 3b62660..d1f7870 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -71,16 +71,4 @@ node { ]] ]) } - - if (env.BRANCH_NAME == "master") { - gitVersion = bat(returnStdout: true, script: "@git describe --tags").trim() - gitSimpleVersion = bat(returnStdout: true, script: "@git describe --tags --abbrev=0").trim() - if (gitVersion == gitSimpleVersion) { - stage('${buildMode}') { - withCredentials([usernamePassword(credentialsId: 'torch-github', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) { - powershell "& ./Jenkins/${buildMode}.ps1 \"https://api.github.com/repos/TorchAPI/Torch/\" \"$gitSimpleVersion\" \"$USERNAME:$PASSWORD\" @(\"bin/torch-server.zip\", \"bin/torch-client.zip\")" - } - } - } - } } \ No newline at end of file diff --git a/Torch.API/Event/IEventManager.cs b/Torch.API/Event/IEventManager.cs index 038db33..29ad642 100644 --- a/Torch.API/Event/IEventManager.cs +++ b/Torch.API/Event/IEventManager.cs @@ -1,11 +1,12 @@ using System.Runtime.CompilerServices; +using Torch.API.Managers; namespace Torch.API.Event { /// /// Manager class responsible for registration of event handlers. /// - public interface IEventManager + public interface IEventManager : IManager { /// /// Registers all event handler methods contained in the given instance diff --git a/Torch.API/ITorchBase.cs b/Torch.API/ITorchBase.cs index ba766c5..a89534c 100644 --- a/Torch.API/ITorchBase.cs +++ b/Torch.API/ITorchBase.cs @@ -80,12 +80,12 @@ namespace Torch.API Task InvokeAsync(Action action, [CallerMemberName] string caller = ""); /// - /// Start the Torch instance. + /// Signals the torch instance to start, then blocks until it's started. /// void Start(); /// - /// Stop the Torch instance. + /// Signals the torch instance to stop, then blocks until it's stopped. /// void Stop(); @@ -101,10 +101,15 @@ namespace Torch.API Task Save(long callerId); /// - /// Initialize the Torch instance. + /// Initialize the Torch instance. Before this is invalid. /// void Init(); + /// + /// Disposes the Torch instance. After this is invalid. + /// + void Dispose(); + /// /// The current state of the game this instance of torch is controlling. /// diff --git a/Torch.Client/Torch.Client.csproj b/Torch.Client/Torch.Client.csproj index 9ed8a34..40f2506 100644 --- a/Torch.Client/Torch.Client.csproj +++ b/Torch.Client/Torch.Client.csproj @@ -127,7 +127,6 @@ - @@ -140,6 +139,9 @@ Settings.settings True + + + ResXFileCodeGenerator Resources.Designer.cs diff --git a/Torch.Client/TorchClient.cs b/Torch.Client/TorchClient.cs index 839c40a..22d2d15 100644 --- a/Torch.Client/TorchClient.cs +++ b/Torch.Client/TorchClient.cs @@ -14,6 +14,7 @@ using Torch.API; using Torch.API.Managers; using Torch.API.Session; using Torch.Client.Manager; +using Torch.Client.UI; using Torch.Session; using VRage; using VRage.FileSystem; @@ -27,7 +28,9 @@ namespace Torch.Client { private MyCommonProgramStartup _startup; private IMyRender _renderer; - private const uint APP_ID = 244850; + + protected override uint SteamAppId => 244850; + protected override string SteamAppName => "Space Engineers"; public TorchClient() { @@ -46,10 +49,9 @@ namespace Torch.Client Directory.SetCurrentDirectory(Program.SpaceEngineersInstallAlias); MyFileSystem.ExePath = Path.Combine(Program.SpaceEngineersInstallAlias, Program.SpaceEngineersBinaries); Log.Info("Initializing Torch Client"); - base.Init(); - - SpaceEngineersGame.SetupBasicGameInfo(); _startup = new MyCommonProgramStartup(RunArgs); + SpaceEngineersGame.SetupBasicGameInfo(); + SpaceEngineersGame.SetupPerGameSettings(); if (_startup.PerformReporting()) throw new InvalidOperationException("Torch client won't launch when started in error reporting mode"); @@ -58,27 +60,16 @@ namespace Torch.Client throw new InvalidOperationException("Only one instance of Space Engineers can be running at a time."); var appDataPath = _startup.GetAppDataPath(); - MyInitializer.InvokeBeforeRun(APP_ID, MyPerGameSettings.BasicGameInfo.ApplicationName, appDataPath); - MyInitializer.InitCheckSum(); - _startup.InitSplashScreen(); - if (!_startup.Check64Bit()) - throw new InvalidOperationException("Torch requires a 64bit operating system"); + Config.InstancePath = appDataPath; + base.Init(); + OverrideMenus(); + SetRenderWindowTitle($"Space Engineers v{GameVersion} with Torch v{TorchVersion}"); + } - _startup.DetectSharpDxLeaksBeforeRun(); - var steamService = new SteamService(Game.IsDedicated, APP_ID); - MyServiceManager.Instance.AddService(steamService); - _renderer = null; - SpaceEngineersGame.SetupPerGameSettings(); - // I'm sorry, but it's what Keen does in SpaceEngineers.MyProgram -#pragma warning disable 612 - SpaceEngineersGame.SetupRender(); -#pragma warning restore 612 - InitializeRender(); - if (!_startup.CheckSteamRunning()) - throw new InvalidOperationException("Space Engineers requires steam to be running"); - - if (!Game.IsDedicated) - MyFileSystem.InitUserSpecific(MyGameService.UserId.ToString()); + public override void Dispose() + { + base.Dispose(); + _startup.DetectSharpDxLeaksAfterRun(); } private void OverrideMenus() @@ -95,18 +86,6 @@ namespace Torch.Client MyPerGameSettings.GUI.MainMenu = typeof(TorchMainMenuScreen); } - - public override void Start() - { - using (var spaceEngineersGame = new SpaceEngineersGame(RunArgs)) - { - Log.Info("Starting client"); - OverrideMenus(); - spaceEngineersGame.OnGameLoaded += SpaceEngineersGame_OnGameLoaded; - spaceEngineersGame.OnGameExit += Dispose; - spaceEngineersGame.Run(false, _startup.DisposeSplashScreen); - } - } private void SetRenderWindowTitle(string title) { @@ -125,58 +104,9 @@ namespace Torch.Client }); } - private void SpaceEngineersGame_OnGameLoaded(object sender, EventArgs e) + public override void Restart() { - SetRenderWindowTitle($"Space Engineers v{GameVersion} with Torch v{TorchVersion}"); - } - - public override void Dispose() - { - MyGameService.ShutDown(); - _startup.DetectSharpDxLeaksAfterRun(); - MyInitializer.InvokeAfterRun(); - } - - public override void Stop() - { - MySandboxGame.ExitThreadSafe(); - } - - private void InitializeRender() - { - try - { - if (Game.IsDedicated) - { - _renderer = new MyNullRender(); - } - else - { - var graphicsRenderer = MySandboxGame.Config.GraphicsRenderer; - if (graphicsRenderer == MySandboxGame.DirectX11RendererKey) - { - _renderer = new MyDX11Render(); - if (!_renderer.IsSupported) - { - MySandboxGame.Log.WriteLine("DirectX 11 renderer not supported. No renderer to revert back to."); - _renderer = null; - } - } - if (_renderer == null) - throw new MyRenderException("The current version of the game requires a Dx11 card. \\n For more information please see : http://blog.marekrosa.org/2016/02/space-engineers-news-full-source-code_26.html", MyRenderExceptionEnum.GpuNotSupported); - - MySandboxGame.Config.GraphicsRenderer = graphicsRenderer; - } - - MyRenderProxy.Initialize(_renderer); - MyRenderProxy.GetRenderProfiler().SetAutocommit(false); - MyRenderProxy.GetRenderProfiler().InitMemoryHack("MainEntryPoint"); - } - catch (Exception ex) - { - MessageBox.Show(ex.Message, "Render Initialization Failed"); - Environment.Exit(-1); - } + throw new NotImplementedException(); } } } diff --git a/Torch.Client/TorchMainMenuScreen.cs b/Torch.Client/TorchMainMenuScreen.cs deleted file mode 100644 index 0bd9e5c..0000000 --- a/Torch.Client/TorchMainMenuScreen.cs +++ /dev/null @@ -1,45 +0,0 @@ -#pragma warning disable 618 -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Sandbox.Game.Gui; -using Sandbox.Graphics; -using Sandbox.Graphics.GUI; -using Sandbox.Gui; -using SpaceEngineers.Game.GUI; -using VRage.Game; -using VRage.Utils; -using VRageMath; - -namespace Torch.Client -{ - public class TorchMainMenuScreen : MyGuiScreenMainMenu - { - public TorchMainMenuScreen() - : this(false) - { - } - - public TorchMainMenuScreen(bool pauseGame) - : base(pauseGame) - { - } - /// - public override void RecreateControls(bool constructor) - { - base.RecreateControls(constructor); - - var buttonSize = MyGuiControlButton.GetVisualStyle(MyGuiControlButtonStyleEnum.Default).NormalTexture.MinSizeGui; - Vector2 leftButtonPositionOrigin = MyGuiManager.ComputeFullscreenGuiCoordinate(MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_BOTTOM) + new Vector2(buttonSize.X / 2f, 0f); - var btn = MakeButton(leftButtonPositionOrigin - 9 * MyGuiConstants.MENU_BUTTONS_POSITION_DELTA, MyStringId.GetOrCompute("Torch"), TorchButtonClicked); - Controls.Add(btn); - } - - private void TorchButtonClicked(MyGuiControlButton obj) - { - MyGuiSandbox.AddScreen(new TorchSettingsScreen()); - } - } -} diff --git a/Torch.Client/UI/TorchMainMenuScreen.cs b/Torch.Client/UI/TorchMainMenuScreen.cs new file mode 100644 index 0000000..2ccbfe6 --- /dev/null +++ b/Torch.Client/UI/TorchMainMenuScreen.cs @@ -0,0 +1,56 @@ +#pragma warning disable 618 +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Sandbox.Engine.Networking; +using Sandbox.Engine.Utils; +using Sandbox.Game; +using Sandbox.Game.Gui; +using Sandbox.Graphics; +using Sandbox.Graphics.GUI; +using Sandbox.Gui; +using SpaceEngineers.Game.GUI; +using Torch.Utils; +using VRage.Game; +using VRage.Utils; +using VRageMath; + +namespace Torch.Client.UI +{ + public class TorchMainMenuScreen : MyGuiScreenMainMenu + { +#pragma warning disable 169 + [ReflectedGetter(Name = "m_elementGroup")] + private static Func _elementsGroup; +#pragma warning restore 169 + + public TorchMainMenuScreen() : this(false) + { + } + + public TorchMainMenuScreen(bool pauseGame) + : base(pauseGame) + { + } + /// + public override void RecreateControls(bool constructor) + { + base.RecreateControls(constructor); + + Vector2 minSizeGui = MyGuiControlButton.GetVisualStyle(MyGuiControlButtonStyleEnum.Default).NormalTexture.MinSizeGui; + Vector2 value = MyGuiManager.ComputeFullscreenGuiCoordinate(MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_BOTTOM, 54, 54) + new Vector2(minSizeGui.X / 2f, 0f) + new Vector2(15f, 0f) / MyGuiConstants.GUI_OPTIMAL_SIZE; + + MyGuiControlButton myGuiControlButton = MakeButton(value - 9 * MyGuiConstants.MENU_BUTTONS_POSITION_DELTA, + MyStringId.GetOrCompute("Torch"), TorchButtonClicked, MyCommonTexts.ToolTipExitToWindows); + Controls.Add(myGuiControlButton); + _elementsGroup.Invoke(this).Add(myGuiControlButton); + } + + private void TorchButtonClicked(MyGuiControlButton obj) + { + MyGuiSandbox.AddScreen(MyGuiSandbox.CreateScreen()); + } + } +} diff --git a/Torch.Client/UI/TorchNavScreen.cs b/Torch.Client/UI/TorchNavScreen.cs new file mode 100644 index 0000000..9b657a2 --- /dev/null +++ b/Torch.Client/UI/TorchNavScreen.cs @@ -0,0 +1,49 @@ +using Sandbox; +using Sandbox.Game.Gui; +using Sandbox.Graphics.GUI; +using VRage; +using VRage.Game; +using VRage.Utils; +using VRageMath; + +namespace Torch.Client.UI +{ + public class TorchNavScreen : MyGuiScreenBase + { + private MyGuiControlElementGroup _elementGroup; + + public TorchNavScreen() : base(new Vector2(0.5f, 0.5f), MyGuiConstants.SCREEN_BACKGROUND_COLOR, new Vector2(0.35875f, 0.558333337f)) + { + EnabledBackgroundFade = true; + RecreateControls(true); + } + + public override void RecreateControls(bool constructor) + { + base.RecreateControls(constructor); + _elementGroup = new MyGuiControlElementGroup(); + _elementGroup.HighlightChanged += ElementGroupHighlightChanged; + AddCaption(MyCommonTexts.ScreenCaptionOptions, null, null); + var value = new Vector2(0f, -m_size.Value.Y / 2f + 0.146f); + var num = 0; + var myGuiControlButton = new MyGuiControlButton(value + num++ * MyGuiConstants.MENU_BUTTONS_POSITION_DELTA, MyGuiControlButtonStyleEnum.Default, null, null, MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_CENTER, null, MyTexts.Get(MyCommonTexts.ScreenOptionsButtonGame), 0.8f, MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_CENTER, MyGuiControlHighlightType.WHEN_ACTIVE, delegate(MyGuiControlButton sender) + { + MyGuiSandbox.AddScreen(MyGuiSandbox.CreateScreen()); + }, GuiSounds.MouseClick, 1f, null); + Controls.Add(myGuiControlButton); + _elementGroup.Add(myGuiControlButton); + CloseButtonEnabled = true; + } + + private void ElementGroupHighlightChanged(MyGuiControlElementGroup obj) + { + foreach (MyGuiControlBase current in _elementGroup) + if (current.HasFocus && obj.SelectedElement != current) + FocusedControl = obj.SelectedElement; + } + + public override string GetFriendlyName() => "Torch"; + + public void OnBackClick(MyGuiControlButton sender) => CloseScreen(); + } +} diff --git a/Torch.Client/UI/TorchSettingsScreen.cs b/Torch.Client/UI/TorchSettingsScreen.cs new file mode 100644 index 0000000..07f4726 --- /dev/null +++ b/Torch.Client/UI/TorchSettingsScreen.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Sandbox.Graphics.GUI; +using VRageMath; + +namespace Torch.Client.UI +{ + public class TorchSettingsScreen : MyGuiScreenBase + { + public TorchSettingsScreen() : base(new Vector2(0.5f, 0.5f), MyGuiConstants.SCREEN_BACKGROUND_COLOR, + new Vector2(0.35875f, 0.558333337f)) + { + EnabledBackgroundFade = true; + RecreateControls(true); + } + + /// + public override string GetFriendlyName() => "Torch Settings"; + + public void OnBackClick(MyGuiControlButton sender) => CloseScreen(); + } +} diff --git a/Torch.Server/Initializer.cs b/Torch.Server/Initializer.cs index b025727..020171f 100644 --- a/Torch.Server/Initializer.cs +++ b/Torch.Server/Initializer.cs @@ -90,7 +90,7 @@ quit"; { var ui = new TorchUI(_server); if (_config.Autostart) - new Thread(_server.Start).Start(); + _server.Start(); ui.ShowDialog(); } else diff --git a/Torch.Server/Managers/InstanceManager.cs b/Torch.Server/Managers/InstanceManager.cs index f11a446..ab26246 100644 --- a/Torch.Server/Managers/InstanceManager.cs +++ b/Torch.Server/Managers/InstanceManager.cs @@ -32,23 +32,13 @@ namespace Torch.Server.Managers { } - - /// - public override void Attach() - { - MyFileSystem.ExePath = Path.Combine(_filesystemManager.TorchDirectory, "DedicatedServer64"); - MyFileSystem.Init("Content", Torch.Config.InstancePath); - //Initializes saves path. Why this isn't in Init() we may never know. - MyFileSystem.InitUserSpecific(null); - } - + public void LoadInstance(string path, bool validate = true) { if (validate) ValidateInstance(path); MyFileSystem.Reset(); - MyFileSystem.ExePath = Path.Combine(_filesystemManager.TorchDirectory, "DedicatedServer64"); MyFileSystem.Init("Content", path); //Initializes saves path. Why this isn't in Init() we may never know. MyFileSystem.InitUserSpecific(null); diff --git a/Torch.Server/Managers/MultiplayerManagerDedicated.cs b/Torch.Server/Managers/MultiplayerManagerDedicated.cs index d8427fc..e08521e 100644 --- a/Torch.Server/Managers/MultiplayerManagerDedicated.cs +++ b/Torch.Server/Managers/MultiplayerManagerDedicated.cs @@ -25,8 +25,7 @@ namespace Torch.Server.Managers private static readonly Logger _log = LogManager.GetCurrentClassLogger(); #pragma warning disable 649 - [ReflectedGetter(Name = "m_members")] - private static Func> _members; + [ReflectedGetter(Name = "m_members")] private static Func> _members; [ReflectedGetter(Name = "m_waitingForGroup")] private static Func> _waitingForGroup; #pragma warning restore 649 @@ -37,7 +36,9 @@ namespace Torch.Server.Managers private Dictionary _gameOwnerIds = new Dictionary(); /// - public MultiplayerManagerDedicated(ITorchBase torch) : base(torch) { } + public MultiplayerManagerDedicated(ITorchBase torch) : base(torch) + { + } /// public void KickPlayer(ulong steamId) => Torch.Invoke(() => MyMultiplayer.Static.KickClient(steamId)); @@ -54,7 +55,8 @@ namespace Torch.Server.Managers } /// - public bool IsBanned(ulong steamId) => _isClientBanned.Invoke(MyMultiplayer.Static, steamId) || MySandboxGame.ConfigDedicated.Banned.Contains(steamId); + public bool IsBanned(ulong steamId) => _isClientBanned.Invoke(MyMultiplayer.Static, steamId) || + MySandboxGame.ConfigDedicated.Banned.Contains(steamId); /// public override void Attach() @@ -62,8 +64,10 @@ namespace Torch.Server.Managers base.Attach(); _gameServerValidateAuthTicketReplacer = _gameServerValidateAuthTicketFactory.Invoke(); _gameServerUserGroupStatusReplacer = _gameServerUserGroupStatusFactory.Invoke(); - _gameServerValidateAuthTicketReplacer.Replace(new Action(ValidateAuthTicketResponse), MyGameService.GameServer); - _gameServerUserGroupStatusReplacer.Replace(new Action(UserGroupStatusResponse), MyGameService.GameServer); + _gameServerValidateAuthTicketReplacer.Replace( + new Action(ValidateAuthTicketResponse), MyGameService.GameServer); + _gameServerUserGroupStatusReplacer.Replace(new Action(UserGroupStatusResponse), + MyGameService.GameServer); _log.Info("Inserted steam authentication intercept"); } @@ -80,15 +84,20 @@ namespace Torch.Server.Managers #pragma warning disable 649 - [ReflectedEventReplace(typeof(MySteamGameServer), nameof(MySteamGameServer.ValidateAuthTicketResponse), typeof(MyDedicatedServerBase), "GameServer_ValidateAuthTicketResponse")] + [ReflectedEventReplace(typeof(MySteamGameServer), nameof(MySteamGameServer.ValidateAuthTicketResponse), + typeof(MyDedicatedServerBase), "GameServer_ValidateAuthTicketResponse")] private static Func _gameServerValidateAuthTicketFactory; - [ReflectedEventReplace(typeof(MySteamGameServer), nameof(MySteamGameServer.UserGroupStatusResponse), typeof(MyDedicatedServerBase), "GameServer_UserGroupStatus")] + + [ReflectedEventReplace(typeof(MySteamGameServer), nameof(MySteamGameServer.UserGroupStatusResponse), + typeof(MyDedicatedServerBase), "GameServer_UserGroupStatus")] private static Func _gameServerUserGroupStatusFactory; + private ReflectedEventReplacer _gameServerValidateAuthTicketReplacer; private ReflectedEventReplacer _gameServerUserGroupStatusReplacer; #pragma warning restore 649 #region CustomAuth + #pragma warning disable 649 [ReflectedStaticMethod(Type = typeof(MyDedicatedServerBase), Name = "ConvertSteamIDFrom64")] private static Func _convertSteamIDFrom64; @@ -96,78 +105,130 @@ namespace Torch.Server.Managers [ReflectedStaticMethod(Type = typeof(MyGameService), Name = "GetServerAccountType")] private static Func _getServerAccountType; - [ReflectedMethod(Name = "UserAccepted")] - private static Action _userAcceptedImpl; + [ReflectedMethod(Name = "UserAccepted")] private static Action _userAcceptedImpl; [ReflectedMethod(Name = "UserRejected")] private static Action _userRejected; - [ReflectedMethod(Name = "IsClientBanned")] - private static Func _isClientBanned; - [ReflectedMethod(Name = "IsClientKicked")] - private static Func _isClientKicked; + + [ReflectedMethod(Name = "IsClientBanned")] private static Func _isClientBanned; + [ReflectedMethod(Name = "IsClientKicked")] private static Func _isClientKicked; + [ReflectedMethod(Name = "RaiseClientKicked")] private static Action _raiseClientKicked; #pragma warning restore 649 + private const int _waitListSize = 32; + private readonly List _waitingForGroupLocal = new List(_waitListSize); + + private struct WaitingForGroup + { + public readonly ulong SteamId; + public readonly JoinResult Response; + public readonly ulong SteamOwner; + + public WaitingForGroup(ulong id, JoinResult response, ulong owner) + { + SteamId = id; + Response = response; + SteamOwner = owner; + } + } + //Largely copied from SE private void ValidateAuthTicketResponse(ulong steamID, JoinResult response, ulong steamOwner) { _log.Debug($"ValidateAuthTicketResponse(user={steamID}, response={response}, owner={steamOwner}"); - if (IsBanned(steamOwner)) + if (MySandboxGame.ConfigDedicated.GroupID == 0uL) + RunEvent(new ValidateAuthTicketEvent(steamID, steamOwner, response, 0, true, false)); + else if (_getServerAccountType(MySandboxGame.ConfigDedicated.GroupID) != MyGameServiceAccountType.Clan) + UserRejected(steamID, JoinResult.GroupIdInvalid); + else if (MyGameService.GameServer.RequestGroupStatus(steamID, MySandboxGame.ConfigDedicated.GroupID)) + lock (_waitingForGroupLocal) + { + if (_waitingForGroupLocal.Count >= _waitListSize) + _waitingForGroupLocal.RemoveAt(0); + _waitingForGroupLocal.Add(new WaitingForGroup(steamID, response, steamOwner)); + } + else + UserRejected(steamID, JoinResult.SteamServersOffline); + } + + private void RunEvent(ValidateAuthTicketEvent info) + { + MultiplayerManagerDedicatedEventShim.RaiseValidateAuthTicket(ref info); + + if (info.FutureVerdict == null) { - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.BannedByAdmins); - _raiseClientKicked.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID); - } - else if (_isClientKicked.Invoke(MyMultiplayer.Static, steamOwner)) - { - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.KickedRecently); - _raiseClientKicked.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID); - } - if (response != JoinResult.OK) - { - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, response); + if (IsBanned(info.SteamOwner) || IsBanned(info.SteamID)) + CommitVerdict(info.SteamID, JoinResult.BannedByAdmins); + else if (_isClientKicked(MyMultiplayer.Static, info.SteamID) || + _isClientKicked(MyMultiplayer.Static, info.SteamOwner)) + CommitVerdict(info.SteamID, JoinResult.KickedRecently); + else if (info.SteamResponse != JoinResult.OK) + CommitVerdict(info.SteamID, info.SteamResponse); + else if (MyMultiplayer.Static.MemberLimit > 0 && + MyMultiplayer.Static.MemberCount + 1 > MyMultiplayer.Static.MemberLimit) + CommitVerdict(info.SteamID, JoinResult.ServerFull); + else if (MySandboxGame.ConfigDedicated.GroupID == 0uL || + MySandboxGame.ConfigDedicated.Administrators.Contains(info.SteamID.ToString()) || + MySandboxGame.ConfigDedicated.Administrators.Contains(_convertSteamIDFrom64(info.SteamID))) + CommitVerdict(info.SteamID, JoinResult.OK); + else if (MySandboxGame.ConfigDedicated.GroupID == info.Group && (info.Member || info.Officer)) + CommitVerdict(info.SteamID, JoinResult.OK); + else + CommitVerdict(info.SteamID, JoinResult.NotInGroup); return; } - if (MyMultiplayer.Static.MemberLimit > 0 && _members.Invoke((MyDedicatedServerBase)MyMultiplayer.Static).Count - 1 >= MyMultiplayer.Static.MemberLimit) + + info.FutureVerdict.ContinueWith((task) => { - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.ServerFull); - return; - } - if (MySandboxGame.ConfigDedicated.GroupID == 0uL || - MySandboxGame.ConfigDedicated.Administrators.Contains(steamID.ToString()) || - MySandboxGame.ConfigDedicated.Administrators.Contains(_convertSteamIDFrom64(steamID))) - { - this.UserAccepted(steamID); - return; - } - if (_getServerAccountType(MySandboxGame.ConfigDedicated.GroupID) != MyGameServiceAccountType.Clan) - { - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.GroupIdInvalid); - return; - } - if (MyGameService.GameServer.RequestGroupStatus(steamID, MySandboxGame.ConfigDedicated.GroupID)) - { - _waitingForGroup.Invoke((MyDedicatedServerBase)MyMultiplayer.Static).Add(steamID); - return; - } - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.SteamServersOffline); + JoinResult verdict; + if (task.IsFaulted) + { + _log.Error(task.Exception, $"Future validation verdict faulted"); + verdict = JoinResult.TicketCanceled; + } + else + verdict = task.Result; + Torch.Invoke(() => { CommitVerdict(info.SteamID, verdict); }); + }); + } + + private void CommitVerdict(ulong steamId, JoinResult verdict) + { + if (verdict == JoinResult.OK) + UserAccepted(steamId); + else + UserRejected(steamId, verdict); } private void UserGroupStatusResponse(ulong userId, ulong groupId, bool member, bool officer) { - if (groupId == MySandboxGame.ConfigDedicated.GroupID && _waitingForGroup.Invoke((MyDedicatedServerBase)MyMultiplayer.Static).Remove(userId)) - { - if (member || officer) - UserAccepted(userId); - else - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, userId, JoinResult.NotInGroup); - } + lock (_waitingForGroupLocal) + for (var j = 0; j < _waitingForGroupLocal.Count; j++) + { + var wait = _waitingForGroupLocal[j]; + if (wait.SteamId == userId) + { + RunEvent(new ValidateAuthTicketEvent(wait.SteamId, wait.SteamOwner, wait.Response, groupId, + member, officer)); + _waitingForGroupLocal.RemoveAt(j); + break; + } + } } + + private void UserRejected(ulong steamId, JoinResult reason) + { + _userRejected.Invoke((MyDedicatedServerBase) MyMultiplayer.Static, steamId, reason); + } + private void UserAccepted(ulong steamId) { - _userAcceptedImpl.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamId); + _userAcceptedImpl.Invoke((MyDedicatedServerBase) MyMultiplayer.Static, steamId); base.RaiseClientJoined(steamId); } + #endregion } -} +} \ No newline at end of file diff --git a/Torch.Server/Managers/MultiplayerManagerDedicatedEventShim.cs b/Torch.Server/Managers/MultiplayerManagerDedicatedEventShim.cs new file mode 100644 index 0000000..c9a302d --- /dev/null +++ b/Torch.Server/Managers/MultiplayerManagerDedicatedEventShim.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NLog; +using Sandbox; +using Torch.API.Event; +using Torch.Event; +using VRage.Network; + +namespace Torch.Server.Managers +{ + [EventShim] + internal static class MultiplayerManagerDedicatedEventShim + { + private static readonly EventList _eventValidateAuthTicket = + new EventList(); + + + internal static void RaiseValidateAuthTicket(ref ValidateAuthTicketEvent info) + { + _eventValidateAuthTicket?.RaiseEvent(ref info); + } + } + + /// + /// Event that occurs when a player tries to connect to a dedicated server. + /// Use these values to choose a , + /// or leave it unset to allow the default logic to handle the request. + /// + public struct ValidateAuthTicketEvent : IEvent + { + /// + /// SteamID of the player + /// + public readonly ulong SteamID; + + /// + /// SteamID of the game owner + /// + public readonly ulong SteamOwner; + + /// + /// The response from steam + /// + public readonly JoinResult SteamResponse; + + /// + /// ID of the queried group, or 0 if no group. + /// + public readonly ulong Group; + + /// + /// Is this person a member of . If no group this is true. + /// + public readonly bool Member; + + /// + /// Is this person an officer of . If no group this is false. + /// + public readonly bool Officer; + + /// + /// A future verdict on this authorization request. If null, let the default logic choose. If not async use + /// + public Task FutureVerdict; + + internal ValidateAuthTicketEvent(ulong steamId, ulong steamOwner, JoinResult steamResponse, + ulong serverGroup, bool member, bool officer) + { + SteamID = steamId; + SteamOwner = steamOwner; + SteamResponse = steamResponse; + Group = serverGroup; + Member = member; + Officer = officer; + FutureVerdict = null; + } + + /// + public bool Cancelled => FutureVerdict != null; + } +} \ No newline at end of file diff --git a/Torch.Server/Torch.Server.csproj b/Torch.Server/Torch.Server.csproj index ea5c705..2a75e82 100644 --- a/Torch.Server/Torch.Server.csproj +++ b/Torch.Server/Torch.Server.csproj @@ -198,6 +198,7 @@ + diff --git a/Torch.Server/TorchServer.cs b/Torch.Server/TorchServer.cs index 81bf533..aad2fc5 100644 --- a/Torch.Server/TorchServer.cs +++ b/Torch.Server/TorchServer.cs @@ -44,19 +44,58 @@ namespace Torch.Server { //public MyConfigDedicated DedicatedConfig { get; set; } /// - public float SimulationRatio { get => _simRatio; set { _simRatio = value; OnPropertyChanged(); } } + public float SimulationRatio + { + get => _simRatio; + set + { + _simRatio = value; + OnPropertyChanged(); + } + } + /// - public TimeSpan ElapsedPlayTime { get => _elapsedPlayTime; set { _elapsedPlayTime = value; OnPropertyChanged(); } } + public TimeSpan ElapsedPlayTime + { + get => _elapsedPlayTime; + set + { + _elapsedPlayTime = value; + OnPropertyChanged(); + } + } + /// public Thread GameThread { get; private set; } + /// - public ServerState State { get => _state; private set { _state = value; OnPropertyChanged(); } } + public ServerState State + { + get => _state; + private set + { + _state = value; + OnPropertyChanged(); + } + } + /// - public bool IsRunning { get => _isRunning; set { _isRunning = value; OnPropertyChanged(); } } + public bool IsRunning + { + get => _isRunning; + set + { + _isRunning = value; + OnPropertyChanged(); + } + } + /// public InstanceManager DedicatedInstance { get; } + /// public string InstanceName => Config?.InstanceName; + /// public string InstancePath => Config?.InstancePath; @@ -64,7 +103,6 @@ namespace Torch.Server private ServerState _state; private TimeSpan _elapsedPlayTime; private float _simRatio; - private readonly AutoResetEvent _stopHandle = new AutoResetEvent(false); private Timer _watchdog; private Stopwatch _uptime; @@ -80,30 +118,20 @@ namespace Torch.Server sessionManager.AddFactory((x) => new MultiplayerManagerDedicated(this)); } + /// + protected override uint SteamAppId => 244850; + + /// + protected override string SteamAppName => "SpaceEngineersDedicated"; + /// public override void Init() { Log.Info($"Init server '{Config.InstanceName}' at '{Config.InstancePath}'"); + Sandbox.Engine.Platform.Game.IsDedicated = true; + base.Init(); - MyPerGameSettings.SendLogToKeen = false; - MyPerServerSettings.GameName = MyPerGameSettings.GameName; - MyPerServerSettings.GameNameSafe = MyPerGameSettings.GameNameSafe; - MyPerServerSettings.GameDSName = MyPerServerSettings.GameNameSafe + "Dedicated"; - MyPerServerSettings.GameDSDescription = "Your place for space engineering, destruction and exploring."; - MySessionComponentExtDebug.ForceDisable = true; - MyPerServerSettings.AppId = 244850; - MyFinalBuildConstants.APP_VERSION = MyPerGameSettings.BasicGameInfo.GameVersion; - InvokeBeforeRun(); - - //MyObjectBuilderSerializer.RegisterFromAssembly(typeof(MyObjectBuilder_CheckpointSerializer).Assembly); - MyPlugins.RegisterGameAssemblyFile(MyPerGameSettings.GameModAssembly); - MyPlugins.RegisterGameObjectBuildersAssemblyFile(MyPerGameSettings.GameModObjBuildersAssembly); - MyPlugins.RegisterSandboxAssemblyFile(MyPerGameSettings.SandboxAssembly); - MyPlugins.RegisterSandboxGameAssemblyFile(MyPerGameSettings.SandboxGameAssembly); - MyPlugins.Load(); - MyGlobalTypeMetadata.Static.Init(); - Managers.GetManager().SessionStateChanged += OnSessionStateChanged; GetManager().LoadInstance(Config.InstancePath); } @@ -117,81 +145,65 @@ namespace Torch.Server } } - private void InvokeBeforeRun() - { - MySandboxGame.Log.Init("SpaceEngineers-Dedicated.log", MyFinalBuildConstants.APP_VERSION_STRING); - MySandboxGame.Log.WriteLine("Steam build: Always true"); - MySandboxGame.Log.WriteLine("Environment.ProcessorCount: " + MyEnvironment.ProcessorCount); - //MySandboxGame.Log.WriteLine("Environment.OSVersion: " + GetOsName()); - MySandboxGame.Log.WriteLine("Environment.CommandLine: " + Environment.CommandLine); - MySandboxGame.Log.WriteLine("Environment.Is64BitProcess: " + MyEnvironment.Is64BitProcess); - MySandboxGame.Log.WriteLine("Environment.Is64BitOperatingSystem: " + Environment.Is64BitOperatingSystem); - //MySandboxGame.Log.WriteLine("Environment.Version: " + GetNETFromRegistry()); - MySandboxGame.Log.WriteLine("Environment.CurrentDirectory: " + Environment.CurrentDirectory); - MySandboxGame.Log.WriteLine("MainAssembly.ProcessorArchitecture: " + Assembly.GetExecutingAssembly().GetArchitecture()); - MySandboxGame.Log.WriteLine("ExecutingAssembly.ProcessorArchitecture: " + MyFileSystem.MainAssembly.GetArchitecture()); - MySandboxGame.Log.WriteLine("IntPtr.Size: " + IntPtr.Size); - MySandboxGame.Log.WriteLine("Default Culture: " + CultureInfo.CurrentCulture.Name); - MySandboxGame.Log.WriteLine("Default UI Culture: " + CultureInfo.CurrentUICulture.Name); - MySandboxGame.Log.WriteLine("IsAdmin: " + new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)); - - MyLog.Default = MySandboxGame.Log; - - Thread.CurrentThread.Name = "Main thread"; - - //Because we want exceptions from users to be in english - Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; - - MySandboxGame.Config = new MyConfig("SpaceEngineers-Dedicated.cfg"); - MySandboxGame.Config.Load(); - } - - [ReflectedStaticMethod(Type = typeof(DedicatedServer), Name = "RunInternal")] - private static Action _dsRunInternal; - /// public override void Start() { if (State != ServerState.Stopped) return; + State = ServerState.Starting; + IsRunning = true; + Log.Info("Starting server."); + MySandboxGame.ConfigDedicated = DedicatedInstance.DedicatedConfig.Model; DedicatedInstance.SaveConfig(); _uptime = Stopwatch.StartNew(); - IsRunning = true; - GameThread = Thread.CurrentThread; - State = ServerState.Starting; - Log.Info("Starting server."); - - MySandboxGame.IsDedicated = true; - MySandboxGame.ConfigDedicated = DedicatedInstance.DedicatedConfig.Model; - Environment.SetEnvironmentVariable("SteamAppId", MyPerServerSettings.AppId.ToString()); - - VRage.Service.ExitListenerSTA.OnExit += delegate { MySandboxGame.Static?.Exit(); }; - base.Start(); - // Stops RunInternal from calling MyFileSystem.InitUserSpecific(null), we call it in InstanceManager. - MySandboxGame.IsReloading = true; - try - { - _dsRunInternal.Invoke(); - } - catch (TargetInvocationException e) - { - // Makes log formatting a little nicer. - throw e.InnerException ?? e; - } + } + + /// + public override void Stop() + { + if (State == ServerState.Stopped) + Log.Error("Server is already stopped"); + Log.Info("Stopping server."); + base.Stop(); + Log.Info("Server stopped."); - MySandboxGame.Log.Close(); State = ServerState.Stopped; + IsRunning = false; + } + + /// + /// Restart the program. + /// + public override void Restart() + { + Save(0).Wait(); + Stop(); + LogManager.Flush(); + + var exe = Assembly.GetExecutingAssembly().Location; + ((TorchConfig)Config).WaitForPID = Process.GetCurrentProcess().Id.ToString(); + Config.Autostart = true; + Process.Start(exe, Config.ToString()); + + Process.GetCurrentProcess().Kill(); } /// public override void Init(object gameInstance) { base.Init(gameInstance); - State = ServerState.Running; - SteamServerAPI.Instance.GameServer.SetKeyValue("SM", "Torch"); + var game = gameInstance as MySandboxGame; + if (game != null && MySession.Static != null) + { + State = ServerState.Running; +// SteamServerAPI.Instance.GameServer.SetKeyValue("SM", "Torch"); + } + else + { + State = ServerState.Stopped; + } } /// @@ -205,18 +217,22 @@ namespace Torch.Server if (_watchdog == null && Config.TickTimeout > 0) { Log.Info("Starting server watchdog."); - _watchdog = new Timer(CheckServerResponding, this, TimeSpan.Zero, TimeSpan.FromSeconds(Config.TickTimeout)); + _watchdog = new Timer(CheckServerResponding, this, TimeSpan.Zero, + TimeSpan.FromSeconds(Config.TickTimeout)); } } + #region Freeze Detection + private static void CheckServerResponding(object state) { var mre = new ManualResetEvent(false); - ((TorchServer)state).Invoke(() => mre.Set()); + ((TorchServer) state).Invoke(() => mre.Set()); if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout))) { #if DEBUG - Log.Error($"Server watchdog detected that the server was frozen for at least {((TorchServer)state).Config.TickTimeout} seconds."); + Log.Error( + $"Server watchdog detected that the server was frozen for at least {((TorchServer) state).Config.TickTimeout} seconds."); Log.Error(DumpFrozenThread(MySandboxGame.Static.UpdateThread)); #else Log.Error(DumpFrozenThread(MySandboxGame.Static.UpdateThread)); @@ -278,47 +294,7 @@ namespace Torch.Server return stack; } - /// - public override void Stop() - { - if (State == ServerState.Stopped) - Log.Error("Server is already stopped"); - - if (Thread.CurrentThread != MySandboxGame.Static.UpdateThread) - { - Log.Debug("Invoking server stop on game thread."); - Invoke(Stop); - return; - } - - Log.Info("Stopping server."); - - //Unload all the static junk. - //TODO: Finish unloading all server data so it's in a completely clean state. - MySandboxGame.Static.Exit(); - - Log.Info("Server stopped."); - LogManager.Flush(); - _stopHandle.Set(); - State = ServerState.Stopped; - IsRunning = false; - Process.GetCurrentProcess().Kill(); - } - - /// - /// Restart the program. DOES NOT SAVE! - /// - public override void Restart() - { - var exe = Assembly.GetExecutingAssembly().Location; - ((TorchConfig)Config).WaitForPID = Process.GetCurrentProcess().Id.ToString(); - Config.Autostart = true; - Process.Start(exe, Config.ToString()); - Save(0).Wait(); - Stop(); - LogManager.Flush(); - Process.GetCurrentProcess().Kill(); - } + #endregion /// public override Task Save(long callerId) @@ -357,8 +333,9 @@ namespace Torch.Server } if (MySession.Static.Players.TryGetPlayerId(callerId, out MyPlayer.PlayerId result)) { - Managers.GetManager()?.SendMessageAsOther("Server", response, statusCode == SaveGameStatus.Success ? MyFontEnum.Green : MyFontEnum.Red, result.SteamId); + Managers.GetManager()?.SendMessageAsOther("Server", response, + statusCode == SaveGameStatus.Success ? MyFontEnum.Green : MyFontEnum.Red, result.SteamId); } } } -} +} \ No newline at end of file diff --git a/Torch.Server/Views/TorchUI.xaml.cs b/Torch.Server/Views/TorchUI.xaml.cs index 92b1083..1bee4c3 100644 --- a/Torch.Server/Views/TorchUI.xaml.cs +++ b/Torch.Server/Views/TorchUI.xaml.cs @@ -66,7 +66,7 @@ namespace Torch.Server private void BtnStart_Click(object sender, RoutedEventArgs e) { _server.GetManager().SaveConfig(); - new Thread(_server.Start).Start(); + _server.Start(); } private void BtnStop_Click(object sender, RoutedEventArgs e) diff --git a/Torch.Tests/ReflectionTestManager.cs b/Torch.Tests/ReflectionTestManager.cs index 87c3757..61494a9 100644 --- a/Torch.Tests/ReflectionTestManager.cs +++ b/Torch.Tests/ReflectionTestManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; using Torch.Utils; @@ -42,8 +43,15 @@ namespace Torch.Tests public ReflectionTestManager Init(Assembly asm) { - foreach (Type type in asm.GetTypes()) - Init(type); + try + { + foreach (Type type in asm.GetTypes()) + Init(type); + } + catch (ReflectionTypeLoadException e) + { + throw e.LoaderExceptions[0]; + } return this; } diff --git a/Torch.Tests/TestUtils.cs b/Torch.Tests/TestUtils.cs index 323186b..286fc66 100644 --- a/Torch.Tests/TestUtils.cs +++ b/Torch.Tests/TestUtils.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; using Torch.Utils; @@ -32,4 +34,4 @@ namespace Torch.Tests private static TorchAssemblyResolver _torchResolver; } -} +} \ No newline at end of file diff --git a/Torch/Collections/MTObservableCollection.cs b/Torch/Collections/MTObservableCollection.cs index 3096bea..f6fa204 100644 --- a/Torch/Collections/MTObservableCollection.cs +++ b/Torch/Collections/MTObservableCollection.cs @@ -38,7 +38,9 @@ namespace Torch.Collections ~MtObservableCollection() { - _flushEventQueue.Dispose(); + Timer queue = _flushEventQueue; + _flushEventQueue = null; + queue?.Dispose(); } /// @@ -208,10 +210,10 @@ namespace Torch.Collections return; _collectionEventQueue.Enqueue(e); // In half a second, flush the events - _flushEventQueue.Change(500, -1); + _flushEventQueue?.Change(500, -1); } - private readonly Timer _flushEventQueue; + private Timer _flushEventQueue; private readonly Queue _collectionEventQueue = new Queue(); diff --git a/Torch/Event/EventManager.cs b/Torch/Event/EventManager.cs index e752322..f3875aa 100644 --- a/Torch/Event/EventManager.cs +++ b/Torch/Event/EventManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -36,7 +37,7 @@ namespace Torch.Event _log.Warn($"Registering type {type.FullName} as an event dispatch type, even though it isn't declared singleton"); var listsFound = 0; RuntimeHelpers.RunClassConstructor(type.TypeHandle); - foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)) if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(EventList<>)) { Type eventType = field.FieldType.GenericTypeArguments[0]; @@ -70,7 +71,7 @@ namespace Torch.Event ParameterInfo[] ps = x.GetParameters(); if (ps.Length != 1) return false; - return ps[0].ParameterType.IsByRef && typeof(IEvent).IsAssignableFrom(ps[0].ParameterType); + return ps[0].ParameterType.IsByRef && typeof(IEvent).IsAssignableFrom(ps[0].ParameterType.GetElementType()); }); return exploreType.BaseType != null ? enumerable.Concat(EventHandlers(exploreType.BaseType)) : enumerable; } @@ -78,9 +79,12 @@ namespace Torch.Event /// private static void RegisterHandlerInternal(IEventHandler instance) { + var foundHandler = false; foreach (MethodInfo handler in EventHandlers(instance.GetType())) { - Type eventType = handler.GetParameters()[0].ParameterType; + Type eventType = handler.GetParameters()[0].ParameterType.GetElementType(); + Debug.Assert(eventType != null); + foundHandler = true; if (eventType.IsInterface) { var foundList = false; @@ -100,6 +104,9 @@ namespace Torch.Event } _log.Error($"Unable to find event handler list for event type {eventType.FullName}"); } + if (!foundHandler) + _log.Warn($"Found no handlers in {instance.GetType().FullName} or base types"); + } /// diff --git a/Torch/Event/EventShimAttribute.cs b/Torch/Event/EventShimAttribute.cs index 87e5c66..8b2d25e 100644 --- a/Torch/Event/EventShimAttribute.cs +++ b/Torch/Event/EventShimAttribute.cs @@ -14,7 +14,7 @@ namespace Torch.Event /// Event shims should be singleton, and have one (or more) fields that are of type . /// [AttributeUsage(AttributeTargets.Class)] - internal class EventShimAttribute : Attribute + public class EventShimAttribute : Attribute { } } diff --git a/Torch/Managers/PatchManager/MSIL/MsilInstruction.cs b/Torch/Managers/PatchManager/MSIL/MsilInstruction.cs index 879343d..a85e265 100644 --- a/Torch/Managers/PatchManager/MSIL/MsilInstruction.cs +++ b/Torch/Managers/PatchManager/MSIL/MsilInstruction.cs @@ -186,10 +186,10 @@ namespace Torch.Managers.PatchManager.MSIL -#pragma warning disable 169 +#pragma warning disable 649 [ReflectedMethod(Name = "StackChange")] private static Func _stackChange; -#pragma warning restore 169 +#pragma warning restore 649 /// /// Estimates the stack delta for this instruction. diff --git a/Torch/Session/TorchSessionManager.cs b/Torch/Session/TorchSessionManager.cs index 426b320..7d8ea14 100644 --- a/Torch/Session/TorchSessionManager.cs +++ b/Torch/Session/TorchSessionManager.cs @@ -97,6 +97,7 @@ namespace Torch.Session CurrentSession.Managers.AddManager(manager); } (CurrentSession as TorchSession)?.Attach(); + _log.Info($"Loaded torch session for {MySession.Static.Name}"); SetState(TorchSessionState.Loaded); } catch (Exception e) @@ -115,7 +116,9 @@ namespace Torch.Session _log.Warn("Session unloading event occurred when we don't have a session."); return; } + _log.Info($"Unloading torch session for {_currentSession.KeenSession.Name}"); SetState(TorchSessionState.Unloading); + _currentSession.Detach(); } catch (Exception e) { @@ -133,9 +136,8 @@ namespace Torch.Session _log.Warn("Session unloading event occurred when we don't have a session."); return; } - _log.Info($"Unloading torch session for {_currentSession.KeenSession.Name}"); + _log.Info($"Unloaded torch session for {_currentSession.KeenSession.Name}"); SetState(TorchSessionState.Unloaded); - _currentSession.Detach(); _currentSession = null; } catch (Exception e) diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj index a77ba4a..d999355 100644 --- a/Torch/Torch.csproj +++ b/Torch/Torch.csproj @@ -255,6 +255,7 @@ CollectionEditor.xaml + diff --git a/Torch/TorchBase.cs b/Torch/TorchBase.cs index 0d93ac7..83b6dc1 100644 --- a/Torch/TorchBase.cs +++ b/Torch/TorchBase.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime; using System.Runtime.CompilerServices; +using System.Security.Principal; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -12,6 +15,9 @@ using NLog; using ProtoBuf.Meta; using Sandbox; using Sandbox.Engine.Multiplayer; +using Sandbox.Engine.Networking; +using Sandbox.Engine.Platform.VideoMode; +using Sandbox.Engine.Utils; using Sandbox.Game; using Sandbox.Game.Multiplayer; using Sandbox.Game.Screens.Helpers; @@ -19,6 +25,7 @@ using Sandbox.Game.World; using Sandbox.Graphics.GUI; using Sandbox.ModAPI; using SpaceEngineers.Game; +using SpaceEngineers.Game.GUI; using Torch.API; using Torch.API.Managers; using Torch.API.ModAPI; @@ -31,16 +38,22 @@ using Torch.Managers.PatchManager; using Torch.Patches; using Torch.Utils; using Torch.Session; +using VRage; using VRage.Collections; using VRage.FileSystem; using VRage.Game; using VRage.Game.Common; using VRage.Game.Components; using VRage.Game.ObjectBuilder; +using VRage.Game.SessionComponents; +using VRage.GameServices; +using VRage.Library; using VRage.ObjectBuilders; using VRage.Plugins; using VRage.Scripting; +using VRage.Steam; using VRage.Utils; +using VRageRender; namespace Torch { @@ -67,8 +80,10 @@ namespace Torch /// Use only if necessary, prefer dependency injection. /// public static ITorchBase Instance { get; private set; } + /// public ITorchConfig Config { get; protected set; } + /// public Version TorchVersion { get; } @@ -79,8 +94,10 @@ namespace Torch /// public Version GameVersion { get; private set; } + /// public string[] RunArgs { get; set; } + /// [Obsolete("Use GetManager() or the [Dependency] attribute.")] public IPluginManager Plugins { get; protected set; } @@ -90,10 +107,13 @@ namespace Torch /// public event Action SessionLoading; + /// public event Action SessionLoaded; + /// public event Action SessionUnloading; + /// public event Action SessionUnloaded; @@ -120,7 +140,9 @@ namespace Torch Instance = this; TorchVersion = Assembly.GetExecutingAssembly().GetName().Version; - TorchVersionVerbose = Assembly.GetEntryAssembly().GetCustomAttribute()?.InformationalVersion ?? TorchVersion.ToString(); + TorchVersionVerbose = Assembly.GetEntryAssembly() + .GetCustomAttribute() + ?.InformationalVersion ?? TorchVersion.ToString(); RunArgs = new string[0]; Managers = new DependencyManager(); @@ -140,6 +162,39 @@ namespace Torch Managers.AddManager(new EventManager(this)); Managers.AddManager(Plugins); TorchAPI.Instance = this; + + GameStateChanged += (game, state) => + { + if (state == TorchGameState.Created) + { + // If the attached assemblies change (MySandboxGame.ctor => MySandboxGame.ParseArgs => MyPlugins.RegisterFromArgs) + // attach assemblies to object factories again. + ObjectFactoryInitPatch.ForceRegisterAssemblies(); + // safe to commit here; all important static ctors have run + PatchManager.CommitInternal(); + } + }; + + sessionManager.SessionStateChanged += (session, state) => + { + switch (state) + { + case TorchSessionState.Loading: + SessionLoading?.Invoke(); + break; + case TorchSessionState.Loaded: + SessionLoaded?.Invoke(); + break; + case TorchSessionState.Unloading: + SessionUnloading?.Invoke(); + break; + case TorchSessionState.Unloaded: + SessionUnloaded?.Invoke(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(state), state, null); + } + }; } [Obsolete("Prefer using Managers.GetManager for global managers")] @@ -252,34 +307,30 @@ namespace Torch #endregion + #region Torch Init/Destroy + + protected abstract uint SteamAppId { get; } + protected abstract string SteamAppName { get; } + /// public virtual void Init() { Debug.Assert(!_init, "Torch instance is already initialized."); SpaceEngineersGame.SetupBasicGameInfo(); SpaceEngineersGame.SetupPerGameSettings(); - // If the attached assemblies change (MySandboxGame.ctor => MySandboxGame.ParseArgs => MyPlugins.RegisterFromArgs) - // attach assemblies to object factories again. ObjectFactoryInitPatch.ForceRegisterAssemblies(); - GameStateChanged += (game, state) => - { - if (state == TorchGameState.Created) - { - ObjectFactoryInitPatch.ForceRegisterAssemblies(); - // safe to commit here; all important static ctors have run - PatchManager.CommitInternal(); - } - }; - Debug.Assert(MyPerGameSettings.BasicGameInfo.GameVersion != null, "MyPerGameSettings.BasicGameInfo.GameVersion != null"); - GameVersion = new Version(new MyVersion(MyPerGameSettings.BasicGameInfo.GameVersion.Value).FormattedText.ToString().Replace("_", ".")); + Debug.Assert(MyPerGameSettings.BasicGameInfo.GameVersion != null, + "MyPerGameSettings.BasicGameInfo.GameVersion != null"); + GameVersion = new Version(new MyVersion(MyPerGameSettings.BasicGameInfo.GameVersion.Value).FormattedText + .ToString().Replace("_", ".")); try { Console.Title = $"{Config.InstanceName} - Torch {TorchVersion}, SE {GameVersion}"; } catch { - // Running as service + // Running without a console } #if DEBUG @@ -292,11 +343,9 @@ namespace Torch Log.Info($"Executing assembly: {Assembly.GetEntryAssembly().FullName}"); Log.Info($"Executing directory: {AppDomain.CurrentDomain.BaseDirectory}"); - MySession.OnLoading += OnSessionLoading; - MySession.AfterLoading += OnSessionLoaded; - MySession.OnUnloading += OnSessionUnloading; - MySession.OnUnloaded += OnSessionUnloaded; - RegisterVRagePlugin(); + _game = new VRageGame(this, TweakGameSettings, SteamAppName, SteamAppId, Config.InstancePath, RunArgs); + if (!_game.WaitFor(VRageGame.GameState.Stopped, TimeSpan.FromMinutes(5))) + Log.Warn("Failed to wait for game to be initialized"); Managers.GetManager().LoadPlugins(); Managers.Attach(); _init = true; @@ -306,109 +355,57 @@ namespace Torch PatchManager.CommitInternal(); } - private void OnSessionLoading() + /// + public virtual void Dispose() { - Log.Debug("Session loading"); - try - { - SessionLoading?.Invoke(); - } - catch (Exception e) - { - Log.Error(e); - throw; - } + Managers.Detach(); + _game.SignalDestroy(); + if (!_game.WaitFor(VRageGame.GameState.Destroyed, TimeSpan.FromSeconds(15))) + Log.Warn("Failed to wait for the game to be destroyed"); + _game = null; } - private void OnSessionLoaded() - { - Log.Debug("Session loaded"); - try - { - SessionLoaded?.Invoke(); - } - catch (Exception e) - { - Log.Error(e); - throw; - } - } + #endregion - private void OnSessionUnloading() - { - Log.Debug("Session unloading"); - try - { - SessionUnloading?.Invoke(); - } - catch (Exception e) - { - Log.Error(e); - throw; - } - } - - private void OnSessionUnloaded() - { - Log.Debug("Session unloaded"); - try - { - SessionUnloaded?.Invoke(); - } - catch (Exception e) - { - Log.Error(e); - throw; - } - } + private VRageGame _game; /// - /// Hook into the VRage plugin system for updates. + /// Called after the basic game information is filled, but before the game is created. /// - private void RegisterVRagePlugin() + protected virtual void TweakGameSettings() { - var fieldName = "m_plugins"; - var pluginList = typeof(MyPlugins).GetField(fieldName, BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null) as List; - if (pluginList == null) - throw new TypeLoadException($"{fieldName} field not found in {nameof(MyPlugins)}"); - - pluginList.Add(this); } + /// public virtual Task Save(long callerId) { - return Task.CompletedTask; + return SaveGameAsync(null); } /// public virtual void Start() { - + _game.SignalStart(); + if (!_game.WaitFor(VRageGame.GameState.Running, TimeSpan.FromSeconds(15))) + Log.Warn("Failed to wait for the game to be started"); } /// public virtual void Stop() { - + LogManager.Flush(); + _game.SignalStop(); + if (!_game.WaitFor(VRageGame.GameState.Stopped, TimeSpan.FromSeconds(15))) + Log.Warn("Failed to wait for the game to be stopped"); } /// - public virtual void Restart() - { - - } - - /// - public virtual void Dispose() - { - Managers.Detach(); - } + public abstract void Restart(); /// public virtual void Init(object gameInstance) { - } /// @@ -435,6 +432,7 @@ namespace Torch public event TorchGameStateChangedDel GameStateChanged; private static readonly HashSet _registeredCoreAssemblies = new HashSet(); + /// /// Registers a core (Torch) assembly with the system, including its /// shims, shims, and components. @@ -467,4 +465,4 @@ namespace Torch } } } -} +} \ No newline at end of file diff --git a/Torch/Utils/Reflected/ReflectedManager.cs b/Torch/Utils/Reflected/ReflectedManager.cs index 08eb4a1..f6d1180 100644 --- a/Torch/Utils/Reflected/ReflectedManager.cs +++ b/Torch/Utils/Reflected/ReflectedManager.cs @@ -237,8 +237,12 @@ namespace Torch.Utils argExp[i] = Expression.Convert(paramExp[i + 1], invokeTypes[i]); else argExp[i] = paramExp[i + 1]; + Debug.Assert(methodInstance.DeclaringType != null); + Expression instanceExp = paramExp[0].Type != methodInstance.DeclaringType + ? Expression.Convert(paramExp[0], methodInstance.DeclaringType) + : (Expression) paramExp[0]; field.SetValue(null, - Expression.Lambda(Expression.Call(paramExp[0], methodInstance, argExp), paramExp) + Expression.Lambda(Expression.Call(instanceExp, methodInstance, argExp), paramExp) .Compile()); _log.Trace($"Reflecting field {field.DeclaringType?.FullName}#{field.Name} with {methodInstance.DeclaringType?.FullName}#{methodInstance.Name}"); } @@ -318,7 +322,12 @@ namespace Torch.Utils Expression impl; if (attr is ReflectedSetterAttribute) { - impl = Expression.Block(Expression.Assign(fieldExp, paramExp[isStatic ? 0 : 1]), Expression.Default(typeof(void))); + var valParam = paramExp[isStatic ? 0 : 1]; + var valExpr = (Expression)valParam; + var setType = sourceField?.FieldType ?? sourceProperty.PropertyType; + if (valParam.Type != setType) + valExpr = Expression.Convert(valExpr, setType); + impl = Expression.Block(Expression.Assign(fieldExp, valExpr), Expression.Default(typeof(void))); } else { diff --git a/Torch/VRageGame.cs b/Torch/VRageGame.cs new file mode 100644 index 0000000..65fd80b --- /dev/null +++ b/Torch/VRageGame.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Havok; +using NLog; +using NLog.Fluent; +using Sandbox; +using Sandbox.Engine.Multiplayer; +using Sandbox.Engine.Networking; +using Sandbox.Engine.Platform.VideoMode; +using Sandbox.Game; +using Sandbox.Game.World; +using SpaceEngineers.Game; +using SpaceEngineers.Game.GUI; +using Torch.Utils; +using VRage; +using VRage.Audio; +using VRage.FileSystem; +using VRage.Game; +using VRage.Game.SessionComponents; +using VRage.GameServices; +using VRage.Plugins; +using VRage.Steam; +using VRage.Utils; +using VRageRender; + +namespace Torch +{ + public class VRageGame + { + private static readonly ILogger _log = LogManager.GetCurrentClassLogger(); + +#pragma warning disable 649 + [ReflectedGetter(Name = "m_plugins", Type = typeof(MyPlugins))] + private static readonly Func> _getVRagePluginList; + + [ReflectedGetter(Name = "Static", TypeName = "Sandbox.Game.Audio.MyMusicController, Sandbox.Game")] + private static readonly Func _getMusicControllerStatic; + + + [ReflectedSetter(Name = "Static", TypeName = "Sandbox.Game.Audio.MyMusicController, Sandbox.Game")] + private static readonly Action _setMusicControllerStatic; + + + [ReflectedMethod(Name = "Unload", TypeName = "Sandbox.Game.Audio.MyMusicController, Sandbox.Game")] + private static readonly Action _musicControllerUnload; +#pragma warning restore 649 + + private readonly TorchBase _torch; + private readonly Action _tweakGameSettings; + private readonly string _userDataPath; + private readonly string _appName; + private readonly uint _appSteamId; + private readonly string[] _runArgs; + private SpaceEngineersGame _game; + private readonly Thread _updateThread; + + private bool _startGame = false; + private readonly AutoResetEvent _commandChanged = new AutoResetEvent(false); + private bool _destroyGame = false; + + private readonly AutoResetEvent _stateChangedEvent = new AutoResetEvent(false); + private GameState _state; + + public enum GameState + { + Creating, + Stopped, + Running, + Destroyed + } + + internal VRageGame(TorchBase torch, Action tweakGameSettings, string appName, uint appSteamId, + string userDataPath, string[] runArgs) + { + _torch = torch; + _tweakGameSettings = tweakGameSettings; + _appName = appName; + _appSteamId = appSteamId; + _userDataPath = userDataPath; + _runArgs = runArgs; + _updateThread = new Thread(Run); + _updateThread.Start(); + } + + private void StateChange(GameState s) + { + if (_state == s) + return; + _state = s; + _stateChangedEvent.Set(); + } + + private void Run() + { + StateChange(GameState.Creating); + try + { + Create(); + _destroyGame = false; + while (!_destroyGame) + { + StateChange(GameState.Stopped); + _commandChanged.WaitOne(); + if (_startGame) + { + _startGame = false; + DoStart(); + } + } + } + finally + { + Destroy(); + StateChange(GameState.Destroyed); + } + } + + private void Create() + { + bool dedicated = Sandbox.Engine.Platform.Game.IsDedicated; + Environment.SetEnvironmentVariable("SteamAppId", _appSteamId.ToString()); + MyServiceManager.Instance.AddService(new MySteamService(dedicated, _appSteamId)); + if (dedicated && !MyGameService.HasGameServer) + { + _log.Warn("Steam service is not running! Please reinstall dedicated server."); + return; + } + + SpaceEngineersGame.SetupBasicGameInfo(); + SpaceEngineersGame.SetupPerGameSettings(); + MyFinalBuildConstants.APP_VERSION = MyPerGameSettings.BasicGameInfo.GameVersion; + MySessionComponentExtDebug.ForceDisable = true; + MyPerGameSettings.SendLogToKeen = false; + // SpaceEngineersGame.SetupAnalytics(); + + MyFileSystem.ExePath = Path.GetDirectoryName(typeof(SpaceEngineersGame).Assembly.Location); + + _tweakGameSettings(); + + MyFileSystem.Reset(); + MyInitializer.InvokeBeforeRun(_appSteamId, _appName, _userDataPath); + // MyInitializer.InitCheckSum(); + + + // Hook into the VRage plugin system for updates. + _getVRagePluginList().Add(_torch); + + if (!MySandboxGame.IsReloading) + MyFileSystem.InitUserSpecific(dedicated ? null : MyGameService.UserId.ToString()); + MySandboxGame.IsReloading = dedicated; + + // render init + { + IMyRender renderer = null; + if (dedicated) + { + renderer = new MyNullRender(); + } + else + { + MyPerformanceSettings preset = MyGuiScreenOptionsGraphics.GetPreset(MyRenderQualityEnum.NORMAL); + MyRenderProxy.Settings.User = MyVideoSettingsManager.GetGraphicsSettingsFromConfig(ref preset) + .PerformanceSettings.RenderSettings; + MyStringId graphicsRenderer = MySandboxGame.Config.GraphicsRenderer; + if (graphicsRenderer == MySandboxGame.DirectX11RendererKey) + { + renderer = new MyDX11Render(new MyRenderSettings?(MyRenderProxy.Settings)); + if (!renderer.IsSupported) + { + MySandboxGame.Log.WriteLine( + "DirectX 11 renderer not supported. No renderer to revert back to."); + renderer = null; + } + } + if (renderer == null) + { + throw new MyRenderException( + "The current version of the game requires a Dx11 card. \\n For more information please see : http://blog.marekrosa.org/2016/02/space-engineers-news-full-source-code_26.html", + MyRenderExceptionEnum.GpuNotSupported); + } + MySandboxGame.Config.GraphicsRenderer = graphicsRenderer; + } + MyRenderProxy.Initialize(renderer); + MyRenderProxy.GetRenderProfiler().SetAutocommit(false); + MyRenderProxy.GetRenderProfiler().InitMemoryHack("MainEntryPoint"); + } + + _game = new SpaceEngineersGame(_runArgs); + } + + private void Destroy() + { + _game.Dispose(); + _game = null; + + MyGameService.ShutDown(); + + _getVRagePluginList().Remove(_torch); + + MyInitializer.InvokeAfterRun(); + } + + private void DoStart() + { + if (MySandboxGame.FatalErrorDuringInit) + { + _log.Warn("Failed to start sandbox game: fatal error during init"); + return; + } + try + { + StateChange(GameState.Running); + _game.Run(); + } + finally + { + StateChange(GameState.Stopped); + } + } + + private void LoadSession(string sessionPath) + { + // ? + MySessionLoader.LoadSingleplayerSession(sessionPath); + } + + private void UnloadSession() + { + if (MySession.Static != null) + { + MySession.Static.Unload(); + MySession.Static = null; + } + { + var musicCtl = _getMusicControllerStatic(); + if (musicCtl != null) + { + _musicControllerUnload(musicCtl); + _setMusicControllerStatic(null); + MyAudio.Static.MusicAllowed = true; + } + } + if (MyMultiplayer.Static != null) + { + MyMultiplayer.Static.Dispose(); + } + } + + private void DoStop() + { + ParallelTasks.Parallel.Scheduler.WaitForTasksToFinish(TimeSpan.FromSeconds(10.0)); + MySandboxGame.Static.Exit(); + } + + /// + /// Signals the game to stop itself. + /// + public void SignalStop() + { + _startGame = false; + _game.Invoke(DoStop, $"{nameof(VRageGame)}::{nameof(SignalStop)}"); + } + + /// + /// Signals the game to start itself + /// + public void SignalStart() + { + _startGame = true; + _commandChanged.Set(); + } + + /// + /// Signals the game to destroy itself + /// + public void SignalDestroy() + { + _destroyGame = true; + SignalStop(); + _commandChanged.Set(); + } + + /// + /// Waits for the game to transition to the given state + /// + /// State to transition to + /// Timeout + /// + public bool WaitFor(GameState state, TimeSpan? timeout) + { + // Kinda icky, but we can't block the update and expect the state to change. + if (Thread.CurrentThread == _updateThread) + return _state == state; + + DateTime? end = timeout.HasValue ? (DateTime?) (DateTime.Now + timeout.Value) : null; + while (_state != state && (!end.HasValue || end > DateTime.Now + TimeSpan.FromSeconds(1))) + if (end.HasValue) + _stateChangedEvent.WaitOne(end.Value - DateTime.Now); + else + _stateChangedEvent.WaitOne(); + return _state == state; + } + } +} \ No newline at end of file diff --git a/Versioning/version.ps1 b/Versioning/version.ps1 index 2e624b4..25f7560 100644 --- a/Versioning/version.ps1 +++ b/Versioning/version.ps1 @@ -1,20 +1,17 @@ +$buildSalt = $Env:BUILD_NUMBER $gitVersion = git describe --tags $gitSimpleVersion = git describe --tags --abbrev=0 -if ($gitSimpleVersion.Equals($gitVersion)) { - $buildSalt = 0 -} else { - $gitLatestCommit = git rev-parse --short HEAD - $buildSalt = [System.Numerics.BigInteger]::Abs([System.Numerics.BigInteger]::Parse($gitLatestCommit, [System.Globalization.NumberStyles]::HexNumber) % 16383) + 1 -} -$dotNetVersion = echo $gitSimpleVersion | Select-String -Pattern "([0-9]+)\.([0-9]+)\.([0-9]+)" | % {$_.Matches} | %{$_.Groups[1].Value+"."+$_.Groups[2].Value+"."+$_.Groups[3].Value+".$buildSalt"} +$simpleVersionStandard = echo $gitSimpleVersion | Select-String -Pattern "([0-9]+)\.([0-9]+)\.([0-9]+)" | % {$_.Matches} | %{$_.Groups[1].Value+"."+$_.Groups[2].Value+"."+$_.Groups[3].Value} +$dotNetVersion = "$simpleVersionStandard.$buildSalt" +$infoVersion = "$gitVersion" -replace "([0-9]+)\.([0-9]+)\.([0-9]+)","$dotNetVersion" $fileContent = @" using System.Reflection; [assembly: AssemblyVersion("$dotNetVersion")] -[assembly: AssemblyInformationalVersion("$gitVersion")] +[assembly: AssemblyInformationalVersion("$infoVersion")] "@ echo $fileContent | Set-Content "$PSScriptRoot/AssemblyVersion.cs" -echo "$gitVersion / $dotNetVersion" \ No newline at end of file +echo "$infoVersion" \ No newline at end of file