Finish chat/players functionality, refactor logic to non-static classes with a single static entry point, add data binding

This commit is contained in:
John Michael Gross
2016-09-19 12:45:44 -07:00
parent 15d6a89f9c
commit 30b0e37fff
25 changed files with 449 additions and 212 deletions

View File

@@ -5,11 +5,11 @@ VisualStudioVersion = 15.0.25618.0
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Piston", "Piston\Piston.csproj", "{7E01635C-3B67-472E-BCD6-C5539564F214}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Piston", "Piston\Piston.csproj", "{7E01635C-3B67-472E-BCD6-C5539564F214}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PistonAPI", "PistonAPI\PistonAPI.csproj", "{FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Piston.API", "PistonAPI\Piston.API.csproj", "{FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PistonClient", "PistonClient\PistonClient.csproj", "{E36DF745-260B-4956-A2E8-09F08B2E7161}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Piston.Client", "PistonClient\Piston.Client.csproj", "{E36DF745-260B-4956-A2E8-09F08B2E7161}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PistonServer", "PistonServer\PistonServer.csproj", "{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Piston.Server", "PistonServer\Piston.Server.csproj", "{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace Piston
{
public class MTObservableCollection<T> : ObservableCollection<T>
{
public override event NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedEventHandler collectionChanged = CollectionChanged;
if (collectionChanged != null)
foreach (NotifyCollectionChangedEventHandler nh in collectionChanged.GetInvocationList())
{
var dispObj = nh.Target as DispatcherObject;
Dispatcher dispatcher = dispObj?.Dispatcher;
if (dispatcher != null && !dispatcher.CheckAccess())
{
dispatcher.BeginInvoke(
(Action)(() => nh.Invoke(this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))),
DispatcherPriority.DataBind);
continue;
}
nh.Invoke(this, e);
}
}
}
}

20
Piston/ObservableType.cs Normal file
View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace Piston
{
public class ObservableType : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
}

View File

@@ -111,17 +111,20 @@
<Reference Include="VRage.Scripting"> <Reference Include="VRage.Scripting">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\VRage.Scripting.dll</HintPath> <HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\VRage.Scripting.dll</HintPath>
</Reference> </Reference>
<Reference Include="WindowsBase" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Logger.cs" /> <Compile Include="Logger.cs" />
<Compile Include="MTObservableCollection.cs" />
<Compile Include="MyPlayerCollectionExtensions.cs" /> <Compile Include="MyPlayerCollectionExtensions.cs" />
<Compile Include="ObservableType.cs" />
<Compile Include="PluginManager.cs" /> <Compile Include="PluginManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\PistonAPI\PistonAPI.csproj"> <ProjectReference Include="..\PistonAPI\Piston.API.csproj">
<Project>{fba5d932-6254-4a1e-baf4-e229fa94e3c2}</Project> <Project>{fba5d932-6254-4a1e-baf4-e229fa94e3c2}</Project>
<Name>PistonAPI</Name> <Name>Piston.API</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View File

@@ -5,7 +5,7 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using PistonAPI; using Piston.API;
using Sandbox; using Sandbox;
using VRage.Plugins; using VRage.Plugins;
using VRage.Collections; using VRage.Collections;
@@ -17,13 +17,12 @@ namespace Piston
{ {
//TODO: Disable reloading if the plugin has static elements because they prevent a full reload. //TODO: Disable reloading if the plugin has static elements because they prevent a full reload.
public static PluginManager Static { get; } = new PluginManager();
public ListReader<IPlugin> Plugins => MyPlugins.Plugins; public ListReader<IPlugin> Plugins => MyPlugins.Plugins;
private List<IPlugin> _plugins; private List<IPlugin> _plugins;
public const string PluginDir = "Plugins"; public const string PluginDir = "Plugins";
private PluginManager() public PluginManager()
{ {
if (!Directory.Exists(PluginDir)) if (!Directory.Exists(PluginDir))
Directory.CreateDirectory(PluginDir); Directory.CreateDirectory(PluginDir);

View File

@@ -1,4 +1,4 @@
namespace PistonAPI namespace Piston.API
{ {
public interface IEnvironmentInfo public interface IEnvironmentInfo
{ {

View File

@@ -6,7 +6,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using VRage.Plugins; using VRage.Plugins;
namespace PistonAPI namespace Piston.API
{ {
public interface IPistonPlugin : IPlugin public interface IPistonPlugin : IPlugin
{ {

View File

@@ -1,6 +1,6 @@
using System.Windows.Controls; using System.Windows.Controls;
namespace PistonAPI namespace Piston.API
{ {
public interface IServerControls public interface IServerControls
{ {

View File

@@ -5,7 +5,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Controls; using System.Windows.Controls;
namespace PistonAPI namespace Piston.API
{ {
public static class PistonAPI public static class PistonAPI
{ {

View File

@@ -1,15 +1,27 @@
<UserControl x:Class="PistonServer.ChatControl" <UserControl x:Class="Piston.Server.ChatControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PistonServer" xmlns:local="clr-namespace:Piston.Server"
mc:Ignorable="d"> mc:Ignorable="d">
<DockPanel> <DockPanel>
<DockPanel DockPanel.Dock="Bottom"> <DockPanel DockPanel.Dock="Bottom">
<Button x:Name="Send" Content="Send" DockPanel.Dock="Right" Width="50" Margin="5,5,5,5" Click="SendButton_Click"></Button> <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> <TextBox x:Name="Message" DockPanel.Dock="Left" Margin="5,5,5,5" KeyDown="Message_OnKeyDown"></TextBox>
</DockPanel> </DockPanel>
<TextBox x:Name="Chat" IsReadOnly="True" Margin="5,5,5,5"/> <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> </DockPanel>
</UserControl> </UserControl>

View File

@@ -19,45 +19,17 @@ using Sandbox.Engine.Multiplayer;
using Sandbox.Game.World; using Sandbox.Game.World;
using SteamSDK; using SteamSDK;
namespace PistonServer namespace Piston.Server
{ {
/// <summary> /// <summary>
/// Interaction logic for ChatControl.xaml /// Interaction logic for ChatControl.xaml
/// </summary> /// </summary>
public partial class ChatControl : UserControl public partial class ChatControl : UserControl
{ {
public event Action<string> MessageEntered;
public ChatControl() public ChatControl()
{ {
InitializeComponent(); InitializeComponent();
ServerManager.Static.SessionReady += InitChatHandler; ChatItems.ItemsSource = PistonServer.Multiplayer.ChatView;
}
public void InitChatHandler()
{
MyMultiplayer.Static.ChatMessageReceived += MessageReceived;
}
public void MessageReceived(ulong steamId, string message, ChatEntryTypeEnum chatType)
{
//Messages sent from server loop back around.
if (steamId == MyMultiplayer.Static.ServerId)
return;
var name = MyMultiplayer.Static.GetMemberName(steamId);
Dispatcher.Invoke(() => AddMessage(name, message), DispatcherPriority.Normal);
}
public void AddMessage(string sender, string message)
{
Chat.Text += $"{DateTime.Now.ToLongTimeString()} | {sender}: {message}\n";
Program.UserInterface.Players.RefreshNames();
}
public void SendMessage(string message)
{
MyMultiplayer.Static.SendChatMessage(message);
Dispatcher.Invoke(() => AddMessage("Server", message));
} }
private void SendButton_Click(object sender, RoutedEventArgs e) private void SendButton_Click(object sender, RoutedEventArgs e)
@@ -65,18 +37,18 @@ namespace PistonServer
OnMessageEntered(); OnMessageEntered();
} }
private void OnMessageEntered()
{
var text = Message.Text;
SendMessage(text);
MessageEntered?.Invoke(Message.Text);
Message.Text = "";
}
private void Message_OnKeyDown(object sender, KeyEventArgs e) private void Message_OnKeyDown(object sender, KeyEventArgs e)
{ {
if (e.Key == Key.Enter) if (e.Key == Key.Enter)
OnMessageEntered(); OnMessageEntered();
} }
private void OnMessageEntered()
{
//Can't use Message.Text directly because of object ownership in WPF.
var text = Message.Text;
PistonServer.Multiplayer.SendMessage(text);
Message.Text = "";
}
} }
} }

View File

@@ -1,25 +1,25 @@
<Window x:Class="PistonServer.MainWindow" <Window x:Class="Piston.Server.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:PistonServer" xmlns:local="clr-namespace:Piston.Server"
mc:Ignorable="d" mc:Ignorable="d"
Title="Piston" Height="600" Width="900"> Title="Piston" Height="600" Width="900">
<DockPanel> <DockPanel>
<StackPanel DockPanel.Dock="Top" Margin="5,5,5,5" Orientation="Horizontal"> <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="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="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"/> <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="LabelSimulation" Content="Sim Ratio: 0.00"/>
<Label x:Name="LabelUptime" Content="Uptime: 0d 0h 0m"/> <Label x:Name="LabelUptime" Content="Uptime: 0d 0h 0m"/>
</StackPanel> </StackPanel>
<TabControl x:Name="TabControl" DockPanel.Dock="Bottom" Margin="5,0,5,5"> <TabControl x:Name="TabControl" DockPanel.Dock="Bottom" Margin="5,0,5,5">
<TabItem Header="Configuration"> <TabItem Header="Configuration">
</TabItem> </TabItem>
<TabItem Header="Chat/Players"> <TabItem Header="Chat/PlayerList">
<DockPanel> <DockPanel>
<local:PlayersControl x:Name="Players" DockPanel.Dock="Right" Width="250" IsEnabled="False"></local:PlayersControl> <local:PlayerListControl x:Name="PlayerList" DockPanel.Dock="Right" Width="250" IsEnabled="False"/>
<local:ChatControl x:Name="Chat" IsEnabled="False"/> <local:ChatControl x:Name="Chat" IsEnabled="False"/>
</DockPanel> </DockPanel>
</TabItem> </TabItem>
@@ -30,3 +30,4 @@
</TabControl> </TabControl>
</DockPanel> </DockPanel>
</Window> </Window>

View File

@@ -17,7 +17,7 @@ using System.Windows.Media.Imaging;
using System.Windows.Navigation; using System.Windows.Navigation;
using System.Windows.Shapes; using System.Windows.Shapes;
namespace PistonServer namespace Piston.Server
{ {
/// <summary> /// <summary>
/// Interaction logic for MainWindow.xaml /// Interaction logic for MainWindow.xaml
@@ -57,27 +57,32 @@ namespace PistonServer
{ {
startTime = DateTime.Now; startTime = DateTime.Now;
Chat.IsEnabled = true; Chat.IsEnabled = true;
Players.IsEnabled = true; PlayerList.IsEnabled = true;
((Button) sender).IsEnabled = false; ((Button) sender).IsEnabled = false;
BtnStop.IsEnabled = true; BtnStop.IsEnabled = true;
uiUpdate.Start(); uiUpdate.Start();
ServerManager.Static.StartServerThread(); PistonServer.Server.StartServerThread();
} }
private void BtnStop_Click(object sender, RoutedEventArgs e) private void BtnStop_Click(object sender, RoutedEventArgs e)
{ {
Chat.IsEnabled = false; Chat.IsEnabled = false;
Players.IsEnabled = false; PlayerList.IsEnabled = false;
((Button) sender).IsEnabled = false; ((Button) sender).IsEnabled = false;
//HACK: Uncomment when restarting is possible. //HACK: Uncomment when restarting is possible.
//BtnStart.IsEnabled = true; //BtnStart.IsEnabled = true;
uiUpdate.Stop(); uiUpdate.Stop();
ServerManager.Static.StopServer(); PistonServer.Server.StopServer();
} }
protected override void OnClosing(CancelEventArgs e) protected override void OnClosing(CancelEventArgs e)
{ {
ServerManager.Static.StopServer(); PistonServer.Server.StopServer();
}
private void BtnRestart_Click(object sender, RoutedEventArgs e)
{
Program.FullRestart();
} }
} }
} }

View File

@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
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 Piston;
using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using SteamSDK;
using VRage.Library.Collections;
namespace Piston.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; }
internal MultiplayerManager(ServerManager serverManager)
{
serverManager.SessionLoaded += OnSessionLoaded;
}
public void KickPlayer(ulong steamId) => MyMultiplayer.Static.KickClient(steamId);
public void BanPlayer(ulong steamId, bool banned = true) => MyMultiplayer.Static.BanClient(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;
}
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);
}
}
/// <summary>
/// Stores player information in an observable format.
/// </summary>
public class PlayerInfo : ObservableType
{
private ulong _steamId;
private string _name;
private ConnectionState _state;
public ulong SteamId
{
get { return _steamId; }
set { _steamId = value; OnPropertyChanged(); }
}
public string Name
{
get { return _name; }
set { _name = value; OnPropertyChanged(); }
}
public ConnectionState State
{
get { return _state; }
set { _state = value; OnPropertyChanged(); }
}
public PlayerInfo(ulong steamId)
{
_steamId = steamId;
_name = MyMultiplayer.Static.GetMemberName(steamId);
_state = ConnectionState.Unknown;
}
}
public class ChatItemInfo : ObservableType
{
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;
}
}
/// <summary>
/// Identifies a player's current connection state.
/// </summary>
[Flags]
public enum ConnectionState
{
Unknown,
Connected = 1,
Left = 2,
Disconnected = 4,
Kicked = 8,
Banned = 16,
}
}

View File

@@ -112,8 +112,10 @@
<Compile Include="MainWindow.xaml.cs"> <Compile Include="MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon> <DependentUpon>MainWindow.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="PlayersControl.xaml.cs"> <Compile Include="MultiplayerManager.cs" />
<DependentUpon>PlayersControl.xaml</DependentUpon> <Compile Include="PistonServer.cs" />
<Compile Include="PlayerListControl.xaml.cs">
<DependentUpon>PlayerListControl.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Program.cs" /> <Compile Include="Program.cs" />
<Compile Include="ServerManager.cs" /> <Compile Include="ServerManager.cs" />
@@ -157,7 +159,7 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
</Page> </Page>
<Page Include="PlayersControl.xaml"> <Page Include="PlayerListControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
</Page> </Page>

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Piston;
namespace Piston.Server
{
/// <summary>
/// Entry point for all Piston server functionality.
/// </summary>
public static class PistonServer
{
public static ServerManager Server;
public static MultiplayerManager Multiplayer;
public static PluginManager Plugins;
private static bool _init;
public static void Init()
{
if (!_init)
{
Logger.Write("Initializing Piston");
_init = true;
Server = new ServerManager();
Multiplayer = new MultiplayerManager(Server);
Plugins = new PluginManager();
}
}
public static void Reset()
{
Logger.Write("Resetting Piston");
Server = null;
Multiplayer = null;
Plugins = null;
_init = false;
}
}
}

View File

@@ -1,15 +1,26 @@
<UserControl x:Class="PistonServer.PlayersControl" <UserControl x:Class="Piston.Server.PlayerListControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PistonServer" xmlns:local="clr-namespace:Piston.Server"
mc:Ignorable="d"> mc:Ignorable="d">
<DockPanel> <DockPanel>
<StackPanel DockPanel.Dock="Bottom"> <StackPanel DockPanel.Dock="Bottom">
<Button x:Name="KickButton" Content="Kick" Margin="5,5,5,5" Click="KickButton_Click"/> <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"/> <Button x:Name="BanButton" Content="Ban" Margin="5,5,5,5" Click="BanButton_Click"/>
</StackPanel> </StackPanel>
<ListBox x:Name="PlayerList" DockPanel.Dock="Top" Margin="5,5,5,5"/> <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> </DockPanel>
</UserControl> </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 Piston;
using Sandbox;
using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using SteamSDK;
namespace Piston.Server
{
/// <summary>
/// Interaction logic for PlayerListControl.xaml
/// </summary>
public partial class PlayerListControl : UserControl
{
public PlayerListControl()
{
InitializeComponent();
PlayerList.ItemsSource = PistonServer.Multiplayer.PlayersView;
}
private void KickButton_Click(object sender, RoutedEventArgs e)
{
var player = PlayerList.SelectedItem as PlayerInfo;
if (player != null)
{
PistonServer.Multiplayer.KickPlayer(player.SteamId);
}
}
private void BanButton_Click(object sender, RoutedEventArgs e)
{
var player = PlayerList.SelectedItem as PlayerInfo;
if (player != null)
{
PistonServer.Multiplayer.BanPlayer(player.SteamId);
}
}
}
}

View File

@@ -1,114 +0,0 @@
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 Piston;
using Sandbox;
using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using SteamSDK;
namespace PistonServer
{
/// <summary>
/// Interaction logic for PlayersControl.xaml
/// </summary>
public partial class PlayersControl : UserControl
{
public PlayersControl()
{
InitializeComponent();
ServerManager.Static.SessionReady += Static_SessionReady;
}
public void RefreshNames()
{
Dispatcher.Invoke(() =>
{
foreach (var player in PlayerList.Items)
{
var p = (PlayerItem)player;
p.Name = MyMultiplayer.Static.GetMemberName(p.SteamId);
}
PlayerList.Items.Refresh();
});
}
private void Static_SessionReady()
{
MyMultiplayer.Static.ClientKicked += OnClientKicked;
MyMultiplayer.Static.ClientLeft += OnClientLeft;
MySession.Static.Players.PlayerRequesting += OnPlayerRequesting;
}
/// <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 PlayerItem { Name = MyMultiplayer.Static.GetMemberName(steamId), SteamId = steamId };
Program.UserInterface.Chat.SendMessage($"{player.Name} connected.");
Dispatcher.Invoke(() => PlayerList.Items.Add(player));
}
private void OnClientKicked(ulong steamId)
{
OnClientLeft(steamId, ChatMemberStateChangeEnum.Kicked);
}
private void OnClientLeft(ulong steamId, ChatMemberStateChangeEnum stateChange)
{
Dispatcher.Invoke(() =>
{
var player = PlayerList.Items.Cast<PlayerItem>().FirstOrDefault(x => x.SteamId == steamId);
if (player == null)
return;
Program.UserInterface.Chat.SendMessage($"{player.Name} {stateChange.ToString().ToLower()}.");
PlayerList.Items.Remove(player);
});
}
public class PlayerItem
{
public ulong SteamId;
public string Name;
public override string ToString()
{
return $"{Name} ({SteamId})";
}
}
private void KickButton_Click(object sender, RoutedEventArgs e)
{
if (PlayerList.SelectedItem == null)
return;
var player = (PlayerItem)PlayerList.SelectedItem;
MyMultiplayer.Static.KickClient(player.SteamId);
}
private void BanButton_Click(object sender, RoutedEventArgs e)
{
if (PlayerList.SelectedItem == null)
return;
var player = (PlayerItem)PlayerList.SelectedItem;
MyMultiplayer.Static.BanClient(player.SteamId, true);
}
}
}

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -9,7 +10,7 @@ using System.Windows;
using System.Windows.Threading; using System.Windows.Threading;
using Piston; using Piston;
namespace PistonServer namespace Piston.Server
{ {
public static class Program public static class Program
{ {
@@ -19,18 +20,19 @@ namespace PistonServer
[STAThread] [STAThread]
public static void Main(string[] args) public static void Main(string[] args)
{ {
ServerManager.Static.RunArgs = new[] { "-console" }; Logger.Write("Initializing");
PistonServer.Server.RunArgs = new[] { "-console" };
MainDispatcher = Dispatcher.CurrentDispatcher; MainDispatcher = Dispatcher.CurrentDispatcher;
Console.WriteLine(MainDispatcher.Thread.ManagedThreadId);
#if DEBUG
Directory.SetCurrentDirectory(@"C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\DedicatedServer64");
#endif
if (args.Contains("-nogui")) if (args.Contains("-nogui"))
ServerManager.Static.StartServer();
PistonServer.Server.StartServer();
else else
StartUI(); StartUI();
if (args.Contains("-autostart") && !PistonServer.Server.Running)
PistonServer.Server.StartServerThread();
Dispatcher.Run(); Dispatcher.Run();
} }
@@ -39,5 +41,12 @@ namespace PistonServer
Thread.CurrentThread.Name = "UI Thread"; Thread.CurrentThread.Name = "UI Thread";
UserInterface.Show(); UserInterface.Show();
} }
public static void FullRestart()
{
PistonServer.Server.StopServer();
Process.Start("PistonServer.exe", "-autostart");
Environment.Exit(1);
}
} }
} }

View File

@@ -8,7 +8,7 @@
// </auto-generated> // </auto-generated>
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
namespace PistonServer.Properties { namespace Piston.Server.Properties {
using System; using System;

View File

@@ -8,7 +8,7 @@
// </auto-generated> // </auto-generated>
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
namespace PistonServer.Properties { namespace Piston.Server.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]

View File

@@ -13,20 +13,21 @@ using Sandbox.Engine.Multiplayer;
using Sandbox.Game.World; using Sandbox.Game.World;
using VRage.Profiler; using VRage.Profiler;
namespace PistonServer namespace Piston.Server
{ {
public class ServerManager public class ServerManager
{ {
public static ServerManager Static { get; } = new ServerManager();
public Thread ServerThread { get; private set; } public Thread ServerThread { get; private set; }
public string[] RunArgs { get; set; } = new string[0]; public string[] RunArgs { get; set; } = new string[0];
public bool Running { get; private set; }
public event Action SessionLoading; public event Action SessionLoading;
public event Action SessionReady; public event Action SessionLoaded;
private readonly Assembly _dsAssembly; private readonly Assembly _dsAssembly;
private readonly ManualResetEvent _stopHandle = new ManualResetEvent(false);
private ServerManager() internal ServerManager()
{ {
using (var f = File.OpenRead("SpaceEngineersDedicated.exe")) using (var f = File.OpenRead("SpaceEngineersDedicated.exe"))
{ {
@@ -47,7 +48,7 @@ namespace PistonServer
private void OnSessionReady() private void OnSessionReady()
{ {
SessionReady?.Invoke(); SessionLoaded?.Invoke();
} }
/// <summary> /// <summary>
@@ -69,6 +70,7 @@ namespace PistonServer
/// </summary> /// </summary>
public void StartServer() public void StartServer()
{ {
Running = true;
Logger.Write("Starting server."); Logger.Write("Starting server.");
if (MySandboxGame.Log.LogEnabled) if (MySandboxGame.Log.LogEnabled)
@@ -89,15 +91,19 @@ namespace PistonServer
/// <summary> /// <summary>
/// Stop the server. /// Stop the server.
/// </summary> /// </summary>
/// <param name="abortThread"></param> public void StopServer()
public void StopServer(bool abortThread = false)
{ {
if (Thread.CurrentThread != ServerThread) if (Thread.CurrentThread.ManagedThreadId != ServerThread.ManagedThreadId)
{ {
MySandboxGame.Static?.Invoke(() => StopServer(true)); Logger.Write("Requesting server stop.");
MySandboxGame.Static.Invoke(StopServer);
_stopHandle.WaitOne();
return; return;
} }
Logger.Write("Stopping server.");
MySession.Static.Save();
MySession.Static.Unload();
MySandboxGame.Static.Exit(); MySandboxGame.Static.Exit();
//Unload all the static junk. //Unload all the static junk.
@@ -106,18 +112,10 @@ namespace PistonServer
VRage.Input.MyGuiGameControlsHelpers.Reset(); VRage.Input.MyGuiGameControlsHelpers.Reset();
VRage.Input.MyInput.UnloadData(); VRage.Input.MyInput.UnloadData();
CleanupProfilers(); CleanupProfilers();
GC.Collect(2);
Logger.Write("Server stopped."); Logger.Write("Server stopped.");
if (abortThread) _stopHandle.Set();
{ Running = false;
try { ServerThread.Abort(); }
catch (ThreadAbortException)
{
Logger.Write("Server thread aborted.");
}
ServerThread = null;
}
} }
private void CleanupProfilers() private void CleanupProfilers()