Rename to Torch, add command system

This commit is contained in:
John Gross
2016-12-17 01:59:03 -08:00
parent 7d51ac9295
commit fb521abbda
85 changed files with 366 additions and 354 deletions

View File

@@ -0,0 +1,281 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using System.Windows.Threading;
using Torch;
using Sandbox;
using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using SteamSDK;
using Torch.Server.ViewModels;
using VRage.Game;
using VRage.Library.Collections;
using VRage.Network;
using VRage.Utils;
namespace Torch.Server
{
/// <summary>
/// Provides a proxy to the game's multiplayer-related functions.
/// </summary>
public class MultiplayerManager
{
public event Action<PlayerInfo> PlayerJoined;
public event Action<PlayerInfo> PlayerLeft;
public event Action<ChatItemInfo> ChatMessageReceived;
public MTObservableCollection<PlayerInfo> PlayersView { get; } = new MTObservableCollection<PlayerInfo>();
public MTObservableCollection<ChatItemInfo> ChatView { get; } = new MTObservableCollection<ChatItemInfo>();
public PlayerInfo LocalPlayer { get; private set; }
private TorchServer _server;
internal MultiplayerManager(TorchServer server)
{
_server = server;
_server.Server.SessionLoaded += OnSessionLoaded;
}
public void KickPlayer(ulong steamId) => _server.Server.BeginGameAction(() => MyMultiplayer.Static.KickClient(steamId));
public void BanPlayer(ulong steamId, bool banned = true)
{
_server.Server.BeginGameAction(() =>
{
MyMultiplayer.Static.BanClient(steamId, banned);
if (_gameOwnerIds.ContainsKey(steamId))
MyMultiplayer.Static.BanClient(_gameOwnerIds[steamId], banned);
});
}
/// <summary>
/// Send a message in chat.
/// </summary>
public void SendMessage(string message)
{
MyMultiplayer.Static.SendChatMessage(message);
ChatView.Add(new ChatItemInfo(LocalPlayer, message));
}
private void OnSessionLoaded()
{
LocalPlayer = new PlayerInfo(MyMultiplayer.Static.ServerId) { Name = "Server", State = ConnectionState.Connected };
MyMultiplayer.Static.ChatMessageReceived += OnChatMessage;
MyMultiplayer.Static.ClientKicked += OnClientKicked;
MyMultiplayer.Static.ClientLeft += OnClientLeft;
MySession.Static.Players.PlayerRequesting += OnPlayerRequesting;
//TODO: Move these with the methods?
RemoveHandlers();
SteamSDK.SteamServerAPI.Instance.GameServer.ValidateAuthTicketResponse += ValidateAuthTicketResponse;
SteamSDK.SteamServerAPI.Instance.GameServer.UserGroupStatus += UserGroupStatus;
_members = (List<ulong>)typeof(MyDedicatedServerBase).GetField("m_members", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(MyMultiplayer.Static);
_waitingForGroup = (HashSet<ulong>)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)
return;
var info = new ChatItemInfo(player, message);
ChatView.Add(info);
ChatMessageReceived?.Invoke(info);
}
/// <summary>
/// Invoked when a client logs in and hits the respawn screen.
/// </summary>
private void OnPlayerRequesting(PlayerRequestArgs args)
{
var steamId = args.PlayerId.SteamId;
var player = new PlayerInfo(steamId) {State = ConnectionState.Connected};
PlayersView.Add(player);
PlayerJoined?.Invoke(player);
}
private void OnClientKicked(ulong steamId)
{
OnClientLeft(steamId, ChatMemberStateChangeEnum.Kicked);
}
private void OnClientLeft(ulong steamId, ChatMemberStateChangeEnum stateChange)
{
var player = PlayersView.FirstOrDefault(p => p.SteamId == steamId);
if (player == null)
return;
player.State = (ConnectionState)stateChange;
PlayersView.Remove(player);
PlayerLeft?.Invoke(player);
}
//TODO: Split the following into a new file?
//These methods override some Keen code to allow us full control over client authentication.
//This lets us have a server set to private (admins only) or friends (friends of all listed admins)
private List<ulong> _members;
private HashSet<ulong> _waitingForGroup;
private HashSet<ulong> _waitingForFriends;
private Dictionary<ulong, ulong> _gameOwnerIds = new Dictionary<ulong, ulong>();
/// <summary>
/// Removes Keen's hooks into some Steam events so we have full control over client authentication
/// </summary>
private static void RemoveHandlers()
{
var eventField = typeof(GameServer).GetField("<backing_store>ValidateAuthTicketResponse", BindingFlags.NonPublic | BindingFlags.Instance);
var eventDel = eventField?.GetValue(SteamServerAPI.Instance.GameServer) as MulticastDelegate;
if (eventDel != null)
{
foreach (var handle in eventDel.GetInvocationList())
{
if (handle.Method.Name == "GameServer_ValidateAuthTicketResponse")
{
SteamServerAPI.Instance.GameServer.ValidateAuthTicketResponse -= handle as ValidateAuthTicketResponse;
}
}
}
eventField = typeof(GameServer).GetField("<backing_store>UserGroupStatus", BindingFlags.NonPublic | BindingFlags.Instance);
eventDel = eventField?.GetValue(SteamServerAPI.Instance.GameServer) as MulticastDelegate;
if (eventDel != null)
{
foreach (var handle in eventDel.GetInvocationList())
{
if (handle.Method.Name == "GameServer_UserGroupStatus")
{
SteamServerAPI.Instance.GameServer.UserGroupStatus -= handle as UserGroupStatus;
}
}
}
}
//Largely copied from SE
private void ValidateAuthTicketResponse(ulong steamID, AuthSessionResponseEnum response, ulong ownerSteamID)
{
MyLog.Default.WriteLineAndConsole($"{Logger.Prefix} Server ValidateAuthTicketResponse ({response}), owner: {ownerSteamID}");
if (steamID != ownerSteamID)
{
MyLog.Default.WriteLineAndConsole($"User {steamID} is using a game owned by {ownerSteamID}. Tracking...");
_gameOwnerIds[steamID] = ownerSteamID;
if (MySandboxGame.ConfigDedicated.Banned.Contains(ownerSteamID))
{
MyLog.Default.WriteLineAndConsole($"Game owner {ownerSteamID} is banned. Banning and rejecting client {steamID}...");
UserRejected(steamID, JoinResult.BannedByAdmins);
BanPlayer(steamID, true);
}
}
if (response == AuthSessionResponseEnum.OK)
{
if (MySession.Static.MaxPlayers > 0 && _members.Count - 1 >= MySession.Static.MaxPlayers)
{
UserRejected(steamID, JoinResult.ServerFull);
}
else if (MySandboxGame.ConfigDedicated.Administrators.Contains(steamID.ToString()) || MySandboxGame.ConfigDedicated.Administrators.Contains(MyDedicatedServerBase.ConvertSteamIDFrom64(steamID)))
{
UserAccepted(steamID);
}
else if (MySandboxGame.ConfigDedicated.GroupID == 0)
{
switch (MySession.Static.OnlineMode)
{
case MyOnlineModeEnum.PUBLIC:
UserAccepted(steamID);
break;
case MyOnlineModeEnum.PRIVATE:
UserRejected(steamID, JoinResult.NotInGroup);
break;
case MyOnlineModeEnum.FRIENDS:
//TODO: actually verify friendship
UserRejected(steamID, JoinResult.NotInGroup);
break;
}
}
else if (SteamServerAPI.Instance.GetAccountType(MySandboxGame.ConfigDedicated.GroupID) != AccountType.Clan)
{
UserRejected(steamID, JoinResult.GroupIdInvalid);
}
else if (SteamServerAPI.Instance.GameServer.RequestGroupStatus(steamID, MySandboxGame.ConfigDedicated.GroupID))
{
// Returns false when there's no connection to Steam
_waitingForGroup.Add(steamID);
}
else
{
UserRejected(steamID, JoinResult.SteamServersOffline);
}
}
else
{
JoinResult joinResult = JoinResult.TicketInvalid;
switch (response)
{
case AuthSessionResponseEnum.AuthTicketCanceled:
joinResult = JoinResult.TicketCanceled;
break;
case AuthSessionResponseEnum.AuthTicketInvalidAlreadyUsed:
joinResult = JoinResult.TicketAlreadyUsed;
break;
case AuthSessionResponseEnum.LoggedInElseWhere:
joinResult = JoinResult.LoggedInElseWhere;
break;
case AuthSessionResponseEnum.NoLicenseOrExpired:
joinResult = JoinResult.NoLicenseOrExpired;
break;
case AuthSessionResponseEnum.UserNotConnectedToSteam:
joinResult = JoinResult.UserNotConnected;
break;
case AuthSessionResponseEnum.VACBanned:
joinResult = JoinResult.VACBanned;
break;
case AuthSessionResponseEnum.VACCheckTimedOut:
joinResult = JoinResult.VACCheckTimedOut;
break;
}
UserRejected(steamID, joinResult);
}
}
private void UserGroupStatus(ulong userId, ulong groupId, bool member, bool officier)
{
if (groupId == MySandboxGame.ConfigDedicated.GroupID && _waitingForGroup.Remove(userId))
{
if ((member || officier))
{
UserAccepted(userId);
}
else
{
UserRejected(userId, JoinResult.NotInGroup);
}
}
}
private void UserAccepted(ulong steamId)
{
//TODO: Raise user joined event here
typeof(MyDedicatedServerBase).GetMethod("UserAccepted", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(MyMultiplayer.Static, new object[] {steamId});
}
private void UserRejected(ulong steamId, JoinResult reason)
{
typeof(MyDedicatedServerBase).GetMethod("UserRejected", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(MyMultiplayer.Static, new object[] {steamId, reason});
}
}
}

49
Torch.Server/Program.cs Normal file
View File

@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using Torch;
namespace Torch.Server
{
public static class Program
{
private static TorchServer _server = new TorchServer();
[STAThread]
public static void Main(string[] args)
{
_server.Init();
_server.Server.RunArgs = new[] { "-console" };
if (args.Contains("-nogui"))
_server.Server.StartServer();
else
StartUI();
if (args.Contains("-autostart") && !_server.Server.IsRunning)
_server.Server.StartServerThread();
Dispatcher.Run();
}
public static void StartUI()
{
Thread.CurrentThread.Name = "UI Thread";
_server.UI.Show();
}
public static void FullRestart()
{
_server.Server.StopServer();
Process.Start("TorchServer.exe", "-autostart");
Environment.Exit(1);
}
}
}

View File

@@ -0,0 +1,55 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("TorchServer")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("TorchServer")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
//In order to begin building localizable applications, set
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
//inside a <PropertyGroup>. For example, if you are using US english
//in your source files, set the <UICulture> to en-US. Then uncomment
//the NeutralResourceLanguage attribute below. Update the "en-US" in
//the line below to match the UICulture setting in the project file.
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Torch.Server.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Torch.Server.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Torch.Server.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View File

@@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
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 VRage.Dedicated;
using VRage.Game;
using VRage.Game.SessionComponents;
using VRage.Profiler;
namespace Torch.Server
{
public class ServerManager : IDisposable
{
public Thread ServerThread { get; private set; }
public string[] RunArgs { get; set; } = new string[0];
public bool IsRunning { get; private set; }
public event Action SessionLoading;
public event Action SessionLoaded;
private readonly ManualResetEvent _stopHandle = new ManualResetEvent(false);
internal ServerManager()
{
MySession.OnLoading += OnSessionLoading;
}
public void InitSandbox()
{
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<MyObjectBuilder_SessionSettings>.GameAttributes = Game.SpaceEngineers;
ConfigForm<MyObjectBuilder_SessionSettings>.OnReset = delegate
{
SpaceEngineersGame.SetupBasicGameInfo();
SpaceEngineersGame.SetupPerGameSettings();
};
int? gameVersion = MyPerGameSettings.BasicGameInfo.GameVersion;
MyFinalBuildConstants.APP_VERSION = gameVersion ?? 0;
}
/// <summary>
/// Invokes an action on the game thread and blocks until completion
/// </summary>
/// <param name="action"></param>
public void GameAction(Action action)
{
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 :(
}
}
/// <summary>
/// Queues an action for invocation on the game thread and optionally runs a callback on completion
/// </summary>
/// <param name="action"></param>
/// <param name="callback"></param>
/// <param name="state"></param>
public void BeginGameAction(Action action, Action<object> callback = null, object state = null)
{
try
{
if (Thread.CurrentThread == MySandboxGame.Static.UpdateThread)
{
action();
}
else
{
Task.Run(() =>
{
GameAction(action);
callback?.Invoke(state);
});
}
}
catch (Exception ex)
{
// log
}
}
private void OnSessionLoading()
{
SessionLoading?.Invoke();
MySession.Static.OnReady += OnSessionReady;
}
private void OnSessionReady()
{
SessionLoaded?.Invoke();
}
/// <summary>
/// Start server on a new thread.
/// </summary>
public void StartServerThread()
{
if (ServerThread?.IsAlive ?? false)
{
Logger.Write("Cannot start the server because it's already running.");
return;
}
ServerThread = new Thread(StartServer);
ServerThread.Start();
}
/// <summary>
/// Start server on the current thread.
/// </summary>
public void StartServer()
{
IsRunning = true;
Logger.Write("Starting server.");
if (MySandboxGame.Log.LogEnabled)
MySandboxGame.Log.Close();
DedicatedServer.Run<MyObjectBuilder_SessionSettings>(RunArgs);
}
/// <summary>
/// Stop the server.
/// </summary>
public void StopServer()
{
if (Thread.CurrentThread.ManagedThreadId != ServerThread?.ManagedThreadId)
{
Logger.Write("Requesting server stop.");
MySandboxGame.Static.Invoke(StopServer);
_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<MyProfiler>).Clear();
}
public void Dispose()
{
if (IsRunning)
StopServer();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -0,0 +1,208 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Torch.Server</RootNamespace>
<AssemblyName>Torch.Server</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup>
<StartupObject>Torch.Server.Program</StartupObject>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>SpaceEngineers.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="HavokWrapper, Version=1.0.6051.28726, Culture=neutral, processorArchitecture=AMD64">
<SpecificVersion>False</SpecificVersion>
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\DedicatedServer64\HavokWrapper.dll</HintPath>
</Reference>
<Reference Include="Sandbox.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\DedicatedServer64\Sandbox.Common.dll</HintPath>
</Reference>
<Reference Include="Sandbox.Game, Version=0.1.6101.33378, Culture=neutral, processorArchitecture=AMD64">
<SpecificVersion>False</SpecificVersion>
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\DedicatedServer64\Sandbox.Game.dll</HintPath>
</Reference>
<Reference Include="SpaceEngineers.Game, Version=1.0.0.0, Culture=neutral, processorArchitecture=AMD64">
<SpecificVersion>False</SpecificVersion>
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\SpaceEngineers.Game.dll</HintPath>
</Reference>
<Reference Include="SpaceEngineersDedicated">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\DedicatedServer64\SpaceEngineersDedicated.exe</HintPath>
</Reference>
<Reference Include="SteamSDK, Version=0.0.0.0, Culture=neutral, processorArchitecture=AMD64">
<SpecificVersion>False</SpecificVersion>
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\SteamSDK.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="VRage, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\VRage.dll</HintPath>
</Reference>
<Reference Include="VRage.Dedicated, Version=1.0.0.0, Culture=neutral, processorArchitecture=AMD64">
<SpecificVersion>False</SpecificVersion>
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\DedicatedServer64\VRage.Dedicated.dll</HintPath>
</Reference>
<Reference Include="VRage.Game, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\DedicatedServer64\VRage.Game.dll</HintPath>
</Reference>
<Reference Include="VRage.Input, Version=1.0.0.0, Culture=neutral, processorArchitecture=AMD64">
<SpecificVersion>False</SpecificVersion>
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\VRage.Input.dll</HintPath>
</Reference>
<Reference Include="VRage.Library, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\VRage.Library.dll</HintPath>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<Compile Include="ViewModels\ConfigDedicatedViewModel.cs" />
<Compile Include="Views\AddWorkshopItemsDialog.xaml.cs">
<DependentUpon>AddWorkshopItemsDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Views\ChatControl.xaml.cs">
<DependentUpon>ChatControl.xaml</DependentUpon>
</Compile>
<Compile Include="ViewModels\ChatItemInfo.cs" />
<Compile Include="Views\ModsControl.xaml.cs">
<DependentUpon>ModsControl.xaml</DependentUpon>
</Compile>
<Compile Include="Views\PistonUI.xaml.cs">
<DependentUpon>PistonUI.xaml</DependentUpon>
</Compile>
<Compile Include="MultiplayerManager.cs" />
<Compile Include="TorchServer.cs" />
<Compile Include="Views\PlayerListControl.xaml.cs">
<DependentUpon>PlayerListControl.xaml</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="ServerManager.cs" />
<Compile Include="Views\PropertyGrid.xaml.cs">
<DependentUpon>PropertyGrid.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<AppDesigner Include="Properties\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Torch.API\Torch.API.csproj">
<Project>{fba5d932-6254-4a1e-baf4-e229fa94e3c2}</Project>
<Name>Torch.API</Name>
</ProjectReference>
<ProjectReference Include="..\Torch.Launcher\Torch.Launcher.csproj">
<Project>{19292801-5B9C-4EE0-961F-0FA37B3A6C3D}</Project>
<Name>Torch.Launcher</Name>
</ProjectReference>
<ProjectReference Include="..\Torch\Torch.csproj">
<Project>{7e01635c-3b67-472e-bcd6-c5539564f214}</Project>
<Name>Torch</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Page Include="Views\AddWorkshopItemsDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\ChatControl.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\ModsControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\PistonUI.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\PlayerListControl.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\PropertyGrid.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Resource Include="SpaceEngineers.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Torch;
using Sandbox;
using Sandbox.Game;
using SpaceEngineers.Game;
using Torch.API;
using Torch.Launcher;
using VRage.Dedicated;
using VRage.Game;
using VRage.Game.SessionComponents;
namespace Torch.Server
{
/// <summary>
/// Entry point for all Piston server functionality.
/// </summary>
public class TorchServer : 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; }
private bool _init;
public void Start()
{
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);
Plugins = new PluginManager();
UI = new PistonUI();
Server.SessionLoaded += Plugins.LoadAllPlugins;
Server.InitSandbox();
SteamHelper.Init();
UI.PropGrid.SetObject(MySandboxGame.ConfigDedicated);
}
public void Reset()
{
Logger.Write("Resetting Torch");
Server.Dispose();
UI.Close();
Server = null;
Multiplayer = null;
Plugins = null;
UI = null;
_init = false;
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
namespace Torch.Server.ViewModels
{
public class ChatItemInfo : ViewModel
{
private PlayerInfo _sender;
private string _message;
private DateTime _timestamp;
public PlayerInfo Sender
{
get { return _sender; }
set { _sender = value; OnPropertyChanged(); }
}
public string Message
{
get { return _message; }
set { _message = value; OnPropertyChanged(); }
}
public DateTime Timestamp
{
get { return _timestamp; }
set { _timestamp = value; OnPropertyChanged(); }
}
public string Time => Timestamp.ToShortTimeString();
public ChatItemInfo(PlayerInfo sender, string message)
{
_sender = sender;
_message = message;
_timestamp = DateTime.Now;
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sandbox.Engine.Utils;
using VRage.Game;
using VRage.Game.ModAPI;
namespace Torch.Server.ViewModels
{
public class ConfigDedicatedViewModel : ViewModel
{
public IMyConfigDedicated Config { get; }
public MTObservableCollection<string> Administrators { get; } = new MTObservableCollection<string>();
public MTObservableCollection<ulong> BannedPlayers { get; } = new MTObservableCollection<ulong>();
public int AsteroidAmount
{
get { return Config.AsteroidAmount; }
set { Config.AsteroidAmount = value; OnPropertyChanged(); }
}
public ConfigDedicatedViewModel(IMyConfigDedicated config)
{
Config = config;
Config.Administrators.ForEach(x => Administrators.Add(x));
Config.Banned.ForEach(x => BannedPlayers.Add(x));
}
public void FlushConfig()
{
Config.Administrators = Administrators.ToList();
}
}
}

View File

@@ -0,0 +1,14 @@
<Window x:Class="Torch.Server.AddModsDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Torch.Server"
mc:Ignorable="d"
Title="Add Workshop Item" Height="200" Width="400">
<DockPanel Background="LightGray">
<Label DockPanel.Dock="Top" Content="Add each workshop URL or ID on its own line." HorizontalAlignment="Center"/>
<Button DockPanel.Dock="Bottom" Content="Done" Margin="5,0,5,5" Click="Done_Clicked"/>
<TextBox x:Name="urlBlock" Margin="5,0,5,5" Background="White" AcceptsReturn="True"/>
</DockPanel>
</Window>

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Torch.Server
{
/// <summary>
/// Interaction logic for AddModsDialog.xaml
/// </summary>
public partial class AddModsDialog : Window
{
public ulong[] Result = new ulong[0];
public AddModsDialog()
{
InitializeComponent();
}
private void Done_Clicked(object sender, RoutedEventArgs e)
{
var text = urlBlock.Text;
var lines = text.Split('\n');
Result = new ulong[lines.Length];
for (var i = 0; i < lines.Length; i++)
{
Result[i] = ExtractId(lines[i]);
}
Close();
}
private ulong ExtractId(string input)
{
ulong result;
var match = Regex.Match(input, @"(?<=id=)\d+").Value;
if (string.IsNullOrEmpty(match))
ulong.TryParse(input, out result);
else
ulong.TryParse(match, out result);
return result;
}
}
}

View File

@@ -0,0 +1,27 @@
<UserControl x:Class="Torch.Server.ChatControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Torch.Server"
mc:Ignorable="d">
<DockPanel>
<DockPanel DockPanel.Dock="Bottom">
<Button x:Name="Send" Content="Send" DockPanel.Dock="Right" Width="50" Margin="5,5,5,5" Click="SendButton_Click"></Button>
<TextBox x:Name="Message" DockPanel.Dock="Left" Margin="5,5,5,5" KeyDown="Message_OnKeyDown"></TextBox>
</DockPanel>
<ListView x:Name="ChatItems" Margin="5,5,5,5">
<ListView.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="{Binding Time}"/>
<TextBlock Text=" "/>
<TextBlock Text="{Binding Sender.Name}" FontWeight="Bold"/>
<TextBlock Text=": "/>
<TextBlock Text="{Binding Message}"/>
</WrapPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DockPanel>
</UserControl>

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using Torch;
using Sandbox;
using Sandbox.Engine.Multiplayer;
using Sandbox.Game.World;
using SteamSDK;
namespace Torch.Server
{
/// <summary>
/// Interaction logic for ChatControl.xaml
/// </summary>
public partial class ChatControl : UserControl
{
public ChatControl()
{
InitializeComponent();
ChatItems.ItemsSource = TorchServer.Multiplayer.ChatView;
}
private void SendButton_Click(object sender, RoutedEventArgs e)
{
OnMessageEntered();
}
private void Message_OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
OnMessageEntered();
}
private void OnMessageEntered()
{
//Can't use Message.Text directly because of object ownership in WPF.
var text = Message.Text;
TorchServer.Multiplayer.SendMessage(text);
Message.Text = "";
}
}
}

View File

@@ -0,0 +1,20 @@
<UserControl x:Class="Torch.Server.ModsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Torch.Server"
mc:Ignorable="d">
<StackPanel Margin="0,0,0,0" Orientation="Vertical">
<Label Content="Mods" Margin="5,5,5,5"/>
<ListView x:Name="ModList" HorizontalAlignment="Left" Height="265" VerticalAlignment="Top" Width="300" Margin="5,5,5,5" MouseDoubleClick="modList_OnMouesDoubleClick">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FriendlyName}" ToolTip="{Binding Description}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button x:Name="AddBtn" Content="Add Mods" Click="addBtn_Click" Margin="5,5,5,5"/>
<Button x:Name="RemBtn" Content="Remove Mods" Margin="5,5,5,5" Click="remBtn_Click"/>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Sandbox.Engine.Networking;
using SteamSDK;
using VRage.Game;
namespace Torch.Server
{
/// <summary>
/// Interaction logic for ModsControl.xaml
/// </summary>
public partial class ModsControl : UserControl
{
public ModsControl()
{
InitializeComponent();
}
private void addBtn_Click(object sender, RoutedEventArgs e)
{
var dialog = new AddModsDialog();
dialog.ShowDialog();
foreach (var id in dialog.Result)
{
var details = SteamHelper.GetItemDetails(id);
if (details.FileType != WorkshopFileType.Community)
continue;
var item = SteamHelper.GetModItem(details);
var desc = string.Join("\n", details.Description.ReadLines(5, true), "Double click to open the workshop page.");
ModList.Items.Add(new ModViewModel(item, desc));
}
}
private void modList_OnMouesDoubleClick(object sender, MouseButtonEventArgs e)
{
var box = (ListView)sender;
if (box.SelectedItem == null)
return;
var id = ((ModViewModel)box.SelectedItem).PublishedFileId;
Process.Start($"http://steamcommunity.com/sharedfiles/filedetails/?id={id}");
}
private void remBtn_Click(object sender, RoutedEventArgs e)
{
var box = (ListView)sender;
if (box.SelectedItems == null || box.SelectedItems.Count == 0)
return;
foreach (var item in box.SelectedItems)
{
box.Items.Remove(item);
}
}
}
}

View File

@@ -0,0 +1,38 @@
<Window x:Class="Torch.Server.PistonUI"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Torch.Server"
xmlns:views="clr-namespace:Torch.Server.Views"
mc:Ignorable="d"
Title="Piston" Height="600" Width="900">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Margin="5,5,5,5" Orientation="Horizontal">
<Button x:Name="BtnStart" Content="Start" Height="24" Width="75" Margin="5,0,5,0" HorizontalAlignment="Left" Click="BtnStart_Click" IsDefault="True"/>
<Button x:Name="BtnStop" Content="Stop" Height="24" Width="75" Margin="5,0,5,0" HorizontalAlignment="Left" Click="BtnStop_Click" IsEnabled="False"/>
<Button x:Name="BtnRestart" Content="Restart" Height="24" Width="75" Margin="5,0,5,0" HorizontalAlignment="Left" Click="BtnRestart_Click"/>
<Label x:Name="LabelSimulation" Content="Sim Ratio: 0.00"/>
<Label x:Name="LabelUptime" Content="Uptime: 0d 0h 0m"/>
</StackPanel>
<TabControl x:Name="TabControl" DockPanel.Dock="Bottom" Margin="5,0,5,5">
<TabItem Header="Configuration">
<DockPanel>
<local:ModsControl/>
<views:PropertyGrid x:Name="PropGrid"/>
</DockPanel>
</TabItem>
<TabItem Header="Chat/Players">
<DockPanel>
<local:PlayerListControl x:Name="PlayerList" DockPanel.Dock="Right" Width="250" IsEnabled="False"/>
<local:ChatControl x:Name="Chat" IsEnabled="False"/>
</DockPanel>
</TabItem>
<TabItem Header="Entity Manager">
</TabItem>
<TabItem Header="Plugins">
</TabItem>
</TabControl>
</DockPanel>
</Window>

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Torch.Server
{
/// <summary>
/// Interaction logic for PistonUI.xaml
/// </summary>
public partial class PistonUI : Window
{
private DateTime _startTime;
private readonly Timer _uiUpdate = new Timer
{
Interval = 1000,
AutoReset = true,
};
public PistonUI()
{
InitializeComponent();
_startTime = DateTime.Now;
_uiUpdate.Elapsed += UiUpdate_Elapsed;
TabControl.Items.Add(new TabItem());
}
private void UiUpdate_Elapsed(object sender, ElapsedEventArgs e)
{
UpdateUptime();
}
private void UpdateUptime()
{
var currentTime = DateTime.Now;
var uptime = currentTime - _startTime;
Dispatcher.Invoke(() => LabelUptime.Content = $"Uptime: {uptime.Days}d {uptime.Hours}h {uptime.Minutes}m");
}
private void BtnStart_Click(object sender, RoutedEventArgs e)
{
_startTime = DateTime.Now;
Chat.IsEnabled = true;
PlayerList.IsEnabled = true;
((Button) sender).IsEnabled = false;
BtnStop.IsEnabled = true;
_uiUpdate.Start();
TorchServer.Server.StartServerThread();
}
private void BtnStop_Click(object sender, RoutedEventArgs e)
{
Chat.IsEnabled = false;
PlayerList.IsEnabled = false;
((Button) sender).IsEnabled = false;
//HACK: Uncomment when restarting is possible.
//BtnStart.IsEnabled = true;
_uiUpdate.Stop();
TorchServer.Server.StopServer();
}
protected override void OnClosing(CancelEventArgs e)
{
TorchServer.Reset();
}
private void BtnRestart_Click(object sender, RoutedEventArgs e)
{
Program.FullRestart();
}
}
}

View File

@@ -0,0 +1,26 @@
<UserControl x:Class="Torch.Server.PlayerListControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Torch.Server"
mc:Ignorable="d">
<DockPanel>
<StackPanel DockPanel.Dock="Bottom">
<Button x:Name="KickButton" Content="Kick" Margin="5,5,5,5" Click="KickButton_Click"/>
<Button x:Name="BanButton" Content="Ban" Margin="5,5,5,5" Click="BanButton_Click"/>
</StackPanel>
<ListView x:Name="PlayerList" DockPanel.Dock="Top" Margin="5,5,5,5">
<ListView.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="{Binding Name}" FontWeight="Bold"/>
<TextBlock Text=" ("/>
<TextBlock Text="{Binding State}"/>
<TextBlock Text=")"/>
</WrapPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DockPanel>
</UserControl>

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Torch;
using Sandbox;
using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using SteamSDK;
namespace Torch.Server
{
/// <summary>
/// Interaction logic for PlayerListControl.xaml
/// </summary>
public partial class PlayerListControl : UserControl
{
public PlayerListControl()
{
InitializeComponent();
PlayerList.ItemsSource = TorchServer.Multiplayer.PlayersView;
}
private void KickButton_Click(object sender, RoutedEventArgs e)
{
var player = PlayerList.SelectedItem as PlayerInfo;
if (player != null)
{
TorchServer.Multiplayer.KickPlayer(player.SteamId);
}
}
private void BanButton_Click(object sender, RoutedEventArgs e)
{
var player = PlayerList.SelectedItem as PlayerInfo;
if (player != null)
{
TorchServer.Multiplayer.BanPlayer(player.SteamId);
}
}
}
}

View File

@@ -0,0 +1,15 @@
<UserControl x:Class="Torch.Server.Views.PropertyGrid"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Torch.Server.Views"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<DataGrid x:Name="Grid" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn x:Name="NameCol" Width="1*" Header="Name" Binding="{Binding Name}" IsReadOnly="True"/>
<DataGridTemplateColumn x:Name="ValCol" Width="1*" Header="Value"/>
</DataGrid.Columns>
</DataGrid>
</UserControl>

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Torch.Server.Views
{
/// <summary>
/// Interaction logic for PropertyGrid.xaml
/// </summary>
public partial class PropertyGrid : UserControl
{
public PropertyGrid()
{
InitializeComponent();
}
public void SetObject(object obj)
{
var props = obj.GetType().GetProperties();
foreach (var prop in props)
{
var p = prop.GetValue(obj);
Grid.Items.Add(new PropertyView(p, prop.Name));
}
}
}
public class PropertyView : ViewModel
{
private object _obj;
public string Name { get; }
public string Value { get { return _obj.ToString(); } }
public DataTemplate ValueEditTemplate;
public PropertyView(object obj, string name)
{
Name = name;
_obj = obj;
ValueEditTemplate = new DataTemplate();
}
}
/*
public class PropertyGridDataTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item is IEnumerable)
{
}
}
}*/
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Extended.Wpf.Toolkit" version="2.9" targetFramework="net461" />
</packages>