diff --git a/Torch/ConnectionState.cs b/Torch.API/ConnectionState.cs similarity index 93% rename from Torch/ConnectionState.cs rename to Torch.API/ConnectionState.cs index 991fbc4..5565c6c 100644 --- a/Torch/ConnectionState.cs +++ b/Torch.API/ConnectionState.cs @@ -1,6 +1,6 @@ using System; -namespace Torch +namespace Torch.API { /// /// Identifies a player's current connection state. diff --git a/Torch.API/ITorchServer.cs b/Torch.API/IChatItem.cs similarity index 55% rename from Torch.API/ITorchServer.cs rename to Torch.API/IChatItem.cs index 4f2aecf..bfab467 100644 --- a/Torch.API/ITorchServer.cs +++ b/Torch.API/IChatItem.cs @@ -6,9 +6,10 @@ using System.Threading.Tasks; namespace Torch.API { - public interface ITorchServer + public interface IChatItem { - void Start(); - void Stop(); + IPlayer Player { get; } + string Message { get; } + DateTime Time { get; } } } diff --git a/Torch.API/IEnvironmentInfo.cs b/Torch.API/IEnvironmentInfo.cs deleted file mode 100644 index d941b2b..0000000 --- a/Torch.API/IEnvironmentInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Torch.API -{ - public interface IEnvironmentInfo - { - EnvironmentType Type { get; } - } - - public enum EnvironmentType - { - DedicatedServer, - GameClient - } -} \ No newline at end of file diff --git a/Torch.API/IMultiplayer.cs b/Torch.API/IMultiplayer.cs new file mode 100644 index 0000000..f7b18f4 --- /dev/null +++ b/Torch.API/IMultiplayer.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Torch.API +{ + public interface IMultiplayer + { + event Action PlayerJoined; + event Action PlayerLeft; + event Action MessageReceived; + Dictionary Players { get; } + List Chat { get; } + void SendMessage(string message); + void KickPlayer(ulong id); + void BanPlayer(ulong id, bool banned = true); + } +} \ No newline at end of file diff --git a/Torch.API/IPlayer.cs b/Torch.API/IPlayer.cs new file mode 100644 index 0000000..9d673e0 --- /dev/null +++ b/Torch.API/IPlayer.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Torch.API +{ + public interface IPlayer + { + ulong SteamId { get; } + List IdentityIds { get; } + string Name { get; } + ConnectionState State { get; } + DateTime LastConnected { get; } + void SetConnectionState(ConnectionState state); + } +} diff --git a/Torch.API/IPluginManager.cs b/Torch.API/IPluginManager.cs new file mode 100644 index 0000000..142976f --- /dev/null +++ b/Torch.API/IPluginManager.cs @@ -0,0 +1,21 @@ +using System; +using VRage.Collections; +using VRage.Plugins; + +namespace Torch.API +{ + public interface IPluginManager + { + ListReader Plugins { get; } + + string[] GetPluginFolders(); + string GetPluginName(Type pluginType); + void LoadAllPlugins(); + void LoadPlugin(IPlugin plugin); + void LoadPluginFolder(string folderName); + void ReloadAll(); + void ReloadPlugin(IPlugin plugin, bool forceNonPiston = false); + bool UnblockDll(string fileName); + void UnloadPlugin(IPlugin plugin); + } +} \ No newline at end of file diff --git a/Torch.API/ITorchBase.cs b/Torch.API/ITorchBase.cs new file mode 100644 index 0000000..193c592 --- /dev/null +++ b/Torch.API/ITorchBase.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Torch.API +{ + public interface ITorchBase + { + event Action SessionLoaded; + IMultiplayer Multiplayer { get; } + IPluginManager Plugins { get; } + void GameAction(Action action); + void BeginGameAction(Action action, Action callback = null, object state = null); + void Start(); + void Stop(); + void Init(); + } + + public interface ITorchServer : ITorchBase + { + bool IsRunning { get; } + string[] RunArgs { get; set; } + } + + public interface ITorchClient : ITorchBase + { + + } +} diff --git a/Torch.API/ITorchPlugin.cs b/Torch.API/ITorchPlugin.cs index 5acf2bc..55d7af2 100644 --- a/Torch.API/ITorchPlugin.cs +++ b/Torch.API/ITorchPlugin.cs @@ -10,7 +10,7 @@ namespace Torch.API { public interface ITorchPlugin : IPlugin { - void Init(ITorchServer server); + void Init(ITorchBase torch); void Reload(); } } diff --git a/Torch.API/Torch.API.csproj b/Torch.API/Torch.API.csproj index 7b66c74..840aed1 100644 --- a/Torch.API/Torch.API.csproj +++ b/Torch.API/Torch.API.csproj @@ -45,13 +45,20 @@ C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\VRage.dll + + False + ..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\VRage.Library.dll + - + + + + + - - + diff --git a/Torch.API/TorchAPI.cs b/Torch.API/TorchAPI.cs deleted file mode 100644 index 8f09746..0000000 --- a/Torch.API/TorchAPI.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Controls; - -namespace Torch.API -{ - -} diff --git a/Torch.Server/Program.cs b/Torch.Server/Program.cs index 6331b38..f07646b 100644 --- a/Torch.Server/Program.cs +++ b/Torch.Server/Program.cs @@ -9,26 +9,29 @@ using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; using Torch; +using Torch.API; namespace Torch.Server { public static class Program { - private static TorchServer _server = new TorchServer(); + private static readonly ITorchServer _server = new TorchServer(); + private static TorchUI _ui; [STAThread] public static void Main(string[] args) { _server.Init(); - _server.Server.RunArgs = new[] { "-console" }; + _server.RunArgs = new[] { "-console" }; + _ui = new TorchUI(_server); if (args.Contains("-nogui")) - _server.Server.StartServer(); + _server.Start(); else StartUI(); - if (args.Contains("-autostart") && !_server.Server.IsRunning) - _server.Server.StartServerThread(); + if (args.Contains("-autostart") && !_server.IsRunning) + new Thread(() => _server.Start()).Start(); Dispatcher.Run(); } @@ -36,12 +39,12 @@ namespace Torch.Server public static void StartUI() { Thread.CurrentThread.Name = "UI Thread"; - _server.UI.Show(); + _ui.Show(); } public static void FullRestart() { - _server.Server.StopServer(); + _server.Stop(); Process.Start("TorchServer.exe", "-autostart"); Environment.Exit(1); } diff --git a/Torch.Server/ServerManager.cs b/Torch.Server/ServerManager.cs index a49f0fe..78e19ad 100644 --- a/Torch.Server/ServerManager.cs +++ b/Torch.Server/ServerManager.cs @@ -13,6 +13,7 @@ using Sandbox.Engine.Multiplayer; using Sandbox.Game; using Sandbox.Game.World; using SpaceEngineers.Game; +using Torch.API; using VRage.Dedicated; using VRage.Game; using VRage.Game.SessionComponents; @@ -20,7 +21,7 @@ using VRage.Profiler; namespace Torch.Server { - public class ServerManager : IDisposable + public class TorchServer : ITorchServer { public Thread ServerThread { get; private set; } public string[] RunArgs { get; set; } = new string[0]; @@ -31,12 +32,12 @@ namespace Torch.Server private readonly ManualResetEvent _stopHandle = new ManualResetEvent(false); - internal ServerManager() + internal TorchServer() { MySession.OnLoading += OnSessionLoading; } - public void InitSandbox() + public void Init() { SpaceEngineersGame.SetupBasicGameInfo(); SpaceEngineersGame.SetupPerGameSettings(); @@ -53,7 +54,7 @@ namespace Torch.Server SpaceEngineersGame.SetupBasicGameInfo(); SpaceEngineersGame.SetupPerGameSettings(); }; - int? gameVersion = MyPerGameSettings.BasicGameInfo.GameVersion; + var gameVersion = MyPerGameSettings.BasicGameInfo.GameVersion; MyFinalBuildConstants.APP_VERSION = gameVersion ?? 0; } @@ -150,14 +151,14 @@ namespace Torch.Server return; } - ServerThread = new Thread(StartServer); + ServerThread = new Thread(Start); ServerThread.Start(); } /// /// Start server on the current thread. /// - public void StartServer() + public void Start() { IsRunning = true; Logger.Write("Starting server."); @@ -171,12 +172,12 @@ namespace Torch.Server /// /// Stop the server. /// - public void StopServer() + public void Stop() { if (Thread.CurrentThread.ManagedThreadId != ServerThread?.ManagedThreadId) { Logger.Write("Requesting server stop."); - MySandboxGame.Static.Invoke(StopServer); + MySandboxGame.Static.Invoke(Stop); _stopHandle.WaitOne(); return; } @@ -204,11 +205,5 @@ namespace Torch.Server typeof(MyRenderProfiler).GetField("m_gpuProfiler", BindingFlags.Static | BindingFlags.NonPublic).SetValue(null, null); (typeof(MyRenderProfiler).GetField("m_threadProfilers", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null) as List).Clear(); } - - public void Dispose() - { - if (IsRunning) - StopServer(); - } } } diff --git a/Torch.Server/Torch.Server.csproj b/Torch.Server/Torch.Server.csproj index 0ca4f42..694f13f 100644 --- a/Torch.Server/Torch.Server.csproj +++ b/Torch.Server/Torch.Server.csproj @@ -111,20 +111,17 @@ ChatControl.xaml - ModsControl.xaml - - PistonUI.xaml + + TorchUI.xaml - - PlayerListControl.xaml - + PropertyGrid.xaml @@ -181,7 +178,7 @@ Designer MSBuild:Compile - + MSBuild:Compile Designer diff --git a/Torch.Server/TorchServer.cs b/Torch.Server/TorchServer.cs index 3e1d45d..0ced27b 100644 --- a/Torch.Server/TorchServer.cs +++ b/Torch.Server/TorchServer.cs @@ -1,75 +1,127 @@ using System; using System.Collections.Generic; -using System.Drawing; +using System.IO; using System.Linq; +using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; +using System.Windows; using Torch; using Sandbox; +using Sandbox.Engine.Multiplayer; using Sandbox.Game; +using Sandbox.Game.World; using SpaceEngineers.Game; using Torch.API; -using Torch.Launcher; using VRage.Dedicated; using VRage.Game; using VRage.Game.SessionComponents; +using VRage.Profiler; namespace Torch.Server { - /// - /// Entry point for all Piston server functionality. - /// - public class TorchServer : ITorchServer + public class TorchServer : TorchBase, ITorchServer { - public ServerManager Server { get; private set; } - public MultiplayerManager Multiplayer { get; private set; } - public PluginManager Plugins { get; private set; } - public PistonUI UI { get; private set; } + public Thread ServerThread { get; private set; } + public string[] RunArgs { get; set; } = new string[0]; + public bool IsRunning { get; private set; } - private bool _init; + public event Action SessionLoading; - public void Start() + private readonly ManualResetEvent _stopHandle = new ManualResetEvent(false); + + internal TorchServer() { - if (!_init) - Init(); - - Server.StartServer(); - } - - public void Stop() - { - Server.StopServer(); - } - - public void Init() - { - if (_init) - return; - - Logger.Write("Initializing Torch"); - _init = true; - Server = new ServerManager(); - Multiplayer = new MultiplayerManager(this); + MySession.OnLoading += OnSessionLoading; Plugins = new PluginManager(); - UI = new PistonUI(); - - Server.SessionLoaded += Plugins.LoadAllPlugins; - Server.InitSandbox(); - SteamHelper.Init(); - UI.PropGrid.SetObject(MySandboxGame.ConfigDedicated); } - public void Reset() + public override void Init() { - Logger.Write("Resetting Torch"); - Server.Dispose(); - UI.Close(); + SpaceEngineersGame.SetupBasicGameInfo(); + SpaceEngineersGame.SetupPerGameSettings(); + 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 = 244850u; + ConfigForm.GameAttributes = Game.SpaceEngineers; + ConfigForm.OnReset = delegate + { + SpaceEngineersGame.SetupBasicGameInfo(); + SpaceEngineersGame.SetupPerGameSettings(); + }; + var gameVersion = MyPerGameSettings.BasicGameInfo.GameVersion; + MyFinalBuildConstants.APP_VERSION = gameVersion ?? 0; + } - Server = null; - Multiplayer = null; - Plugins = null; - UI = null; - _init = false; + private void OnSessionLoading() + { + SessionLoading?.Invoke(); + MySession.Static.OnReady += OnSessionReady; + } + + private void OnSessionReady() + { + Plugins.LoadAllPlugins(); + InvokeSessionLoaded(); + } + + /// + /// Start server on the current thread. + /// + public override void Start() + { + if (IsRunning) + throw new InvalidOperationException("Server is already running."); + + IsRunning = true; + Logger.Write("Starting server."); + + if (MySandboxGame.Log.LogEnabled) + MySandboxGame.Log.Close(); + + DedicatedServer.Run(RunArgs); + } + + /// + /// Stop the server. + /// + public override void Stop() + { + if (Thread.CurrentThread.ManagedThreadId != ServerThread?.ManagedThreadId) + { + Logger.Write("Requesting server stop."); + MySandboxGame.Static.Invoke(Stop); + _stopHandle.WaitOne(); + return; + } + + Logger.Write("Stopping server."); + MySession.Static.Save(); + MySession.Static.Unload(); + MySandboxGame.Static.Exit(); + + //Unload all the static junk. + //TODO: Finish unloading all server data so it's in a completely clean state. + VRage.FileSystem.MyFileSystem.Reset(); + VRage.Input.MyGuiGameControlsHelpers.Reset(); + VRage.Input.MyInput.UnloadData(); + CleanupProfilers(); + + Logger.Write("Server stopped."); + _stopHandle.Set(); + IsRunning = false; + } + + private void CleanupProfilers() + { + typeof(MyRenderProfiler).GetField("m_threadProfiler", BindingFlags.Static | BindingFlags.NonPublic).SetValue(null, null); + typeof(MyRenderProfiler).GetField("m_gpuProfiler", BindingFlags.Static | BindingFlags.NonPublic).SetValue(null, null); + (typeof(MyRenderProfiler).GetField("m_threadProfilers", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null) as List).Clear(); } } } diff --git a/Torch.Server/Views/ChatControl.xaml.cs b/Torch.Server/Views/ChatControl.xaml.cs index 2f53ec2..b1a23a6 100644 --- a/Torch.Server/Views/ChatControl.xaml.cs +++ b/Torch.Server/Views/ChatControl.xaml.cs @@ -18,6 +18,7 @@ using Sandbox; using Sandbox.Engine.Multiplayer; using Sandbox.Game.World; using SteamSDK; +using Torch.API; namespace Torch.Server { @@ -26,10 +27,27 @@ namespace Torch.Server /// public partial class ChatControl : UserControl { + private ITorchServer _server; + public ChatControl() { InitializeComponent(); - ChatItems.ItemsSource = TorchServer.Multiplayer.ChatView; + } + + public void BindServer(ITorchServer server) + { + _server = server; + server.Multiplayer.MessageReceived += Refresh; + Refresh(); + } + + private void Refresh(IChatItem chatItem = null) + { + Dispatcher.Invoke(() => + { + ChatItems.ItemsSource = null; + ChatItems.ItemsSource = _server.Multiplayer.Chat; + }); } private void SendButton_Click(object sender, RoutedEventArgs e) @@ -47,7 +65,7 @@ namespace Torch.Server { //Can't use Message.Text directly because of object ownership in WPF. var text = Message.Text; - TorchServer.Multiplayer.SendMessage(text); + _server.Multiplayer.SendMessage(text); Message.Text = ""; } } diff --git a/Torch.Server/Views/PlayerListControl.xaml.cs b/Torch.Server/Views/PlayerListControl.xaml.cs index 68c759f..87d30df 100644 --- a/Torch.Server/Views/PlayerListControl.xaml.cs +++ b/Torch.Server/Views/PlayerListControl.xaml.cs @@ -18,6 +18,7 @@ using Sandbox.Engine.Multiplayer; using Sandbox.Game.Multiplayer; using Sandbox.Game.World; using SteamSDK; +using Torch.API; namespace Torch.Server { @@ -26,27 +27,45 @@ namespace Torch.Server /// public partial class PlayerListControl : UserControl { + private ITorchServer _server; + public PlayerListControl() { InitializeComponent(); - PlayerList.ItemsSource = TorchServer.Multiplayer.PlayersView; + } + + public void BindServer(ITorchServer server) + { + _server = server; + server.Multiplayer.PlayerJoined += Refresh; + server.Multiplayer.PlayerLeft += Refresh; + Refresh(); + } + + private void Refresh(IPlayer player = null) + { + Dispatcher.Invoke(() => + { + PlayerList.ItemsSource = null; + PlayerList.ItemsSource = _server.Multiplayer.Players; + }); } private void KickButton_Click(object sender, RoutedEventArgs e) { - var player = PlayerList.SelectedItem as PlayerInfo; + var player = PlayerList.SelectedItem as Player; if (player != null) { - TorchServer.Multiplayer.KickPlayer(player.SteamId); + _server.Multiplayer.KickPlayer(player.SteamId); } } private void BanButton_Click(object sender, RoutedEventArgs e) { - var player = PlayerList.SelectedItem as PlayerInfo; + var player = PlayerList.SelectedItem as Player; if (player != null) { - TorchServer.Multiplayer.BanPlayer(player.SteamId); + _server.Multiplayer.BanPlayer(player.SteamId); } } } diff --git a/Torch.Server/Views/PistonUI.xaml b/Torch.Server/Views/TorchUI.xaml similarity index 97% rename from Torch.Server/Views/PistonUI.xaml rename to Torch.Server/Views/TorchUI.xaml index 1afa759..f93e62b 100644 --- a/Torch.Server/Views/PistonUI.xaml +++ b/Torch.Server/Views/TorchUI.xaml @@ -1,4 +1,4 @@ - - /// Interaction logic for PistonUI.xaml + /// Interaction logic for TorchUI.xaml /// - public partial class PistonUI : Window + public partial class TorchUI : Window { + private ITorchServer _server; private DateTime _startTime; private readonly Timer _uiUpdate = new Timer { @@ -31,13 +35,15 @@ namespace Torch.Server AutoReset = true, }; - public PistonUI() + public TorchUI(ITorchServer server) { + _server = server; InitializeComponent(); _startTime = DateTime.Now; _uiUpdate.Elapsed += UiUpdate_Elapsed; - TabControl.Items.Add(new TabItem()); + Chat.BindServer(server); + PlayerList.BindServer(server); } private void UiUpdate_Elapsed(object sender, ElapsedEventArgs e) @@ -61,7 +67,7 @@ namespace Torch.Server ((Button) sender).IsEnabled = false; BtnStop.IsEnabled = true; _uiUpdate.Start(); - TorchServer.Server.StartServerThread(); + new Thread(() => _server.Start()).Start(); } private void BtnStop_Click(object sender, RoutedEventArgs e) @@ -72,12 +78,12 @@ namespace Torch.Server //HACK: Uncomment when restarting is possible. //BtnStart.IsEnabled = true; _uiUpdate.Stop(); - TorchServer.Server.StopServer(); + _server.Stop(); } protected override void OnClosing(CancelEventArgs e) { - TorchServer.Reset(); + _server.Stop(); } private void BtnRestart_Click(object sender, RoutedEventArgs e) diff --git a/Torch/Collections/MTObservableCollection.cs b/Torch/Collections/MTObservableCollection.cs index 3c1d1e1..6b89071 100644 --- a/Torch/Collections/MTObservableCollection.cs +++ b/Torch/Collections/MTObservableCollection.cs @@ -20,12 +20,11 @@ namespace Torch { var dispObj = nh.Target as DispatcherObject; - Dispatcher dispatcher = dispObj?.Dispatcher; + var dispatcher = dispObj?.Dispatcher; if (dispatcher != null && !dispatcher.CheckAccess()) { dispatcher.BeginInvoke( - (Action)(() => nh.Invoke(this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))), + (Action)(() => nh.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))), DispatcherPriority.DataBind); continue; } diff --git a/Torch/Collections/PlayerInfoCache.cs b/Torch/Collections/PlayerInfoCache.cs index c5e64ef..0a9c0fb 100644 --- a/Torch/Collections/PlayerInfoCache.cs +++ b/Torch/Collections/PlayerInfoCache.cs @@ -3,25 +3,25 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Torch.API; namespace Torch { public static class PlayerInfoCache { - private static readonly Dictionary _cache = new Dictionary(); + private static readonly Dictionary _cache = new Dictionary(); - public static PlayerInfo GetOrCreate(ulong steamId) + public static Player GetOrCreate(ulong steamId) { - PlayerInfo info; - if (_cache.TryGetValue(steamId, out info)) + if (_cache.TryGetValue(steamId, out Player info)) return info; - info = new PlayerInfo(steamId) {State = ConnectionState.Unknown}; + info = new Player(steamId) {State = ConnectionState.Unknown}; _cache.Add(steamId, info); return info; } - public static void Add(PlayerInfo info) + public static void Add(Player info) { if (_cache.ContainsKey(info.SteamId)) return; diff --git a/Torch/Commands/ChatCommandModule.cs b/Torch/Commands/ChatCommandModule.cs index 84dd0f6..53f9b94 100644 --- a/Torch/Commands/ChatCommandModule.cs +++ b/Torch/Commands/ChatCommandModule.cs @@ -5,6 +5,6 @@ namespace Torch.Commands public class ChatCommandModule { public ITorchPlugin Plugin { get; set; } - public ITorchServer Server { get; set; } + public ITorchBase Server { get; set; } } } \ No newline at end of file diff --git a/Torch/Commands/CommandContext.cs b/Torch/Commands/CommandContext.cs index e3332d1..f711afd 100644 --- a/Torch/Commands/CommandContext.cs +++ b/Torch/Commands/CommandContext.cs @@ -1,8 +1,26 @@ -namespace Torch.Commands +using System.Linq; +using System.Text.RegularExpressions; + +namespace Torch.Commands { public struct CommandContext { public string Argument; public ulong SteamId; + + /// + /// Splits the argument by single words and quoted blocks. + /// + /// + public string[] SplitArgument() + { + var split = Regex.Split(Argument, "(\"[^\"]+\"|\\S+)"); + for (var i = 0; i < split.Length; i++) + { + split[i] = Regex.Replace(split[i], "\"", ""); + } + + return split; + } } } \ No newline at end of file diff --git a/Torch/Commands/CommandSystem.cs b/Torch/Commands/CommandSystem.cs index c5e6aa8..5891642 100644 --- a/Torch/Commands/CommandSystem.cs +++ b/Torch/Commands/CommandSystem.cs @@ -7,12 +7,12 @@ namespace Torch.Commands { public class CommandSystem { - public ITorchServer Server { get; } + public ITorchBase Server { get; } public char Prefix { get; set; } public Dictionary Commands { get; } = new Dictionary(); - public CommandSystem(ITorchServer server, char prefix = '/') + public CommandSystem(ITorchBase server, char prefix = '/') { Server = server; Prefix = prefix; @@ -40,6 +40,12 @@ namespace Torch.Commands if (commandAttrib == null) continue; + if (Commands.ContainsKey(commandAttrib.Name)) + { + Console.WriteLine($"[ERROR]: Command \"{method.Name}\" is already registered!"); + continue; + } + var parameters = method.GetParameters(); if (parameters.Length != 1 || parameters[0].ParameterType != typeof(CommandContext)) { @@ -82,8 +88,6 @@ namespace Torch.Commands Commands[cmdName].Invoke(context); - - //Regex.Matches(command, "(\"[^\"]+\"|\\S+)"); } } } diff --git a/Torch.Server/MultiplayerManager.cs b/Torch/MultiplayerManager.cs similarity index 81% rename from Torch.Server/MultiplayerManager.cs rename to Torch/MultiplayerManager.cs index 66d8ffb..22cc2c0 100644 --- a/Torch.Server/MultiplayerManager.cs +++ b/Torch/MultiplayerManager.cs @@ -18,40 +18,43 @@ using Sandbox.Engine.Multiplayer; using Sandbox.Game.Multiplayer; using Sandbox.Game.World; using SteamSDK; -using Torch.Server.ViewModels; +using Torch.API; +using Torch.ViewModels; using VRage.Game; using VRage.Library.Collections; using VRage.Network; using VRage.Utils; -namespace Torch.Server +namespace Torch { /// /// Provides a proxy to the game's multiplayer-related functions. /// - public class MultiplayerManager + public class MultiplayerManager : IMultiplayer { - public event Action PlayerJoined; - public event Action PlayerLeft; - public event Action ChatMessageReceived; + public event Action PlayerJoined; + public event Action PlayerLeft; + public event Action MessageReceived; - public MTObservableCollection PlayersView { get; } = new MTObservableCollection(); - public MTObservableCollection ChatView { get; } = new MTObservableCollection(); - public PlayerInfo LocalPlayer { get; private set; } + //public MTObservableCollection PlayersView { get; } = new MTObservableCollection(); + //public MTObservableCollection ChatView { get; } = new MTObservableCollection(); + public List Chat { get; } = new List(); + public Dictionary Players { get; } = new Dictionary(); + public Player LocalPlayer { get; private set; } - private TorchServer _server; + private readonly ITorchBase _torch; - internal MultiplayerManager(TorchServer server) + internal MultiplayerManager(ITorchBase torch) { - _server = server; - _server.Server.SessionLoaded += OnSessionLoaded; + _torch = torch; + _torch.SessionLoaded += OnSessionLoaded; } - public void KickPlayer(ulong steamId) => _server.Server.BeginGameAction(() => MyMultiplayer.Static.KickClient(steamId)); + public void KickPlayer(ulong steamId) => _torch.BeginGameAction(() => MyMultiplayer.Static.KickClient(steamId)); public void BanPlayer(ulong steamId, bool banned = true) { - _server.Server.BeginGameAction(() => + _torch.BeginGameAction(() => { MyMultiplayer.Static.BanClient(steamId, banned); if (_gameOwnerIds.ContainsKey(steamId)) @@ -65,12 +68,12 @@ namespace Torch.Server public void SendMessage(string message) { MyMultiplayer.Static.SendChatMessage(message); - ChatView.Add(new ChatItemInfo(LocalPlayer, message)); + //ChatView.Add(new ChatItem(LocalPlayer, message)); } private void OnSessionLoaded() { - LocalPlayer = new PlayerInfo(MyMultiplayer.Static.ServerId) { Name = "Server", State = ConnectionState.Connected }; + LocalPlayer = new Player(MyMultiplayer.Static.ServerId) { Name = "Server", State = ConnectionState.Connected }; MyMultiplayer.Static.ChatMessageReceived += OnChatMessage; MyMultiplayer.Static.ClientKicked += OnClientKicked; @@ -79,21 +82,21 @@ namespace Torch.Server //TODO: Move these with the methods? RemoveHandlers(); - SteamSDK.SteamServerAPI.Instance.GameServer.ValidateAuthTicketResponse += ValidateAuthTicketResponse; - SteamSDK.SteamServerAPI.Instance.GameServer.UserGroupStatus += UserGroupStatus; + SteamServerAPI.Instance.GameServer.ValidateAuthTicketResponse += ValidateAuthTicketResponse; + SteamServerAPI.Instance.GameServer.UserGroupStatus += UserGroupStatus; _members = (List)typeof(MyDedicatedServerBase).GetField("m_members", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(MyMultiplayer.Static); _waitingForGroup = (HashSet)typeof(MyDedicatedServerBase).GetField("m_waitingForGroup", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(MyMultiplayer.Static); } private void OnChatMessage(ulong steamId, string message, ChatEntryTypeEnum chatType) { - var player = PlayersView.FirstOrDefault(p => p.SteamId == steamId); - if (player == null || player == LocalPlayer) + var player = MyMultiplayer.Static.GetMemberName(steamId); + if (string.IsNullOrEmpty(player)) return; - var info = new ChatItemInfo(player, message); - ChatView.Add(info); - ChatMessageReceived?.Invoke(info); + var info = new ChatItem(new Player(steamId), message); + Chat.Add(info); + MessageReceived?.Invoke(info); } /// @@ -102,8 +105,20 @@ namespace Torch.Server private void OnPlayerRequesting(PlayerRequestArgs args) { var steamId = args.PlayerId.SteamId; - var player = new PlayerInfo(steamId) {State = ConnectionState.Connected}; - PlayersView.Add(player); + + IPlayer player; + if (!Players.ContainsKey(steamId)) + { + player = new Player(steamId) { State = ConnectionState.Connected }; + Players.Add(steamId, player); + } + else + { + player = Players[steamId]; + player.SetConnectionState(ConnectionState.Connected); + } + + Logger.Write($"{player.Name} connected."); PlayerJoined?.Invoke(player); } @@ -114,13 +129,12 @@ namespace Torch.Server private void OnClientLeft(ulong steamId, ChatMemberStateChangeEnum stateChange) { - var player = PlayersView.FirstOrDefault(p => p.SteamId == steamId); - - if (player == null) + if (!Players.ContainsKey(steamId)) return; - player.State = (ConnectionState)stateChange; - PlayersView.Remove(player); + var player = Players[steamId]; + Logger.Write($"{player.Name} disconnected ({(ConnectionState)stateChange})."); + player.SetConnectionState((ConnectionState)stateChange); PlayerLeft?.Invoke(player); } diff --git a/Torch/PlayerInfo.cs b/Torch/Player.cs similarity index 59% rename from Torch/PlayerInfo.cs rename to Torch/Player.cs index f3e91f5..337a0c8 100644 --- a/Torch/PlayerInfo.cs +++ b/Torch/Player.cs @@ -1,11 +1,14 @@ -using Sandbox.Engine.Multiplayer; +using System; +using System.Collections.Generic; +using Sandbox.Engine.Multiplayer; +using Torch.API; namespace Torch { /// /// Stores player information in an observable format. /// - public class PlayerInfo : ViewModel + public class Player : ViewModel, IPlayer { private ulong _steamId; private string _name; @@ -23,17 +26,30 @@ namespace Torch set { _name = value; OnPropertyChanged(); } } + //TODO: track identity history + public List IdentityIds { get; } = new List(); + + public DateTime LastConnected { get; private set; } + public ConnectionState State { get { return _state; } set { _state = value; OnPropertyChanged(); } } - public PlayerInfo(ulong steamId) + public Player(ulong steamId) { _steamId = steamId; _name = MyMultiplayer.Static.GetMemberName(steamId); _state = ConnectionState.Unknown; } + + public void SetConnectionState(ConnectionState state) + { + if (state == ConnectionState.Connected) + LastConnected = DateTime.Now; + + State = state; + } } } \ No newline at end of file diff --git a/Torch/PluginManager.cs b/Torch/PluginManager.cs index 0035a38..5af9a52 100644 --- a/Torch/PluginManager.cs +++ b/Torch/PluginManager.cs @@ -15,7 +15,7 @@ using VRage.Library.Collections; namespace Torch { - public class PluginManager + public class PluginManager : IPluginManager { //TODO: Disable reloading if the plugin has static elements because they prevent a full reload. diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj index 1f70850..9b6085b 100644 --- a/Torch/Torch.csproj +++ b/Torch/Torch.csproj @@ -126,12 +126,14 @@ - + + - + + diff --git a/Torch/TorchBase.cs b/Torch/TorchBase.cs new file mode 100644 index 0000000..0037a29 --- /dev/null +++ b/Torch/TorchBase.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Sandbox; +using Torch.API; + +namespace Torch +{ + public abstract class TorchBase : ITorchBase + { + public IPluginManager Plugins { get; protected set; } + public IMultiplayer Multiplayer { get; protected set; } + + public event Action SessionLoaded; + + protected void InvokeSessionLoaded() + { + SessionLoaded?.Invoke(); + } + + protected TorchBase() + { + Plugins = new PluginManager(); + Multiplayer = new MultiplayerManager(this); + } + + /// + /// Invokes an action on the game thread and blocks until completion + /// + /// + public void GameAction(Action action) + { + if (action == null) + return; + + try + { + if (Thread.CurrentThread == MySandboxGame.Static.UpdateThread) + { + action(); + } + else + { + AutoResetEvent e = new AutoResetEvent(false); + + MySandboxGame.Static.Invoke(() => + { + try + { + action(); + } + catch (Exception ex) + { + //log + } + finally + { + e.Set(); + } + }); + + //timeout so we don't accidentally hang the server + e.WaitOne(60000); + } + } + catch (Exception ex) + { + //we need a logger :( + } + } + + /// + /// Queues an action for invocation on the game thread and optionally runs a callback on completion + /// + /// + /// + /// + public void BeginGameAction(Action action, Action callback = null, object state = null) + { + if (action == null) + return; + + try + { + if (Thread.CurrentThread == MySandboxGame.Static.UpdateThread) + { + action(); + } + else + { + Task.Run(() => + { + GameAction(action); + callback?.Invoke(state); + }); + } + } + catch (Exception ex) + { + // log + } + } + + public abstract void Start(); + public abstract void Stop(); + public abstract void Init(); + } +} diff --git a/Torch.Server/ViewModels/ChatItemInfo.cs b/Torch/ViewModels/ChatItem.cs similarity index 53% rename from Torch.Server/ViewModels/ChatItemInfo.cs rename to Torch/ViewModels/ChatItem.cs index a4a91a3..c10d4b3 100644 --- a/Torch.Server/ViewModels/ChatItemInfo.cs +++ b/Torch/ViewModels/ChatItem.cs @@ -1,14 +1,15 @@ using System; +using Torch.API; -namespace Torch.Server.ViewModels +namespace Torch.ViewModels { - public class ChatItemInfo : ViewModel + public class ChatItem : ViewModel, IChatItem { - private PlayerInfo _sender; + private IPlayer _sender; private string _message; private DateTime _timestamp; - public PlayerInfo Sender + public IPlayer Player { get { return _sender; } set { _sender = value; OnPropertyChanged(); } @@ -20,19 +21,23 @@ namespace Torch.Server.ViewModels set { _message = value; OnPropertyChanged(); } } - public DateTime Timestamp + public DateTime Time { get { return _timestamp; } set { _timestamp = value; OnPropertyChanged(); } } - public string Time => Timestamp.ToShortTimeString(); + public string TimeString => Time.ToShortTimeString(); - public ChatItemInfo(PlayerInfo sender, string message) + public ChatItem(IPlayer sender, string message, DateTime timestamp = default(DateTime)) { _sender = sender; _message = message; - _timestamp = DateTime.Now; + + if (timestamp == default(DateTime)) + _timestamp = DateTime.Now; + else + _timestamp = timestamp; } } } \ No newline at end of file