Optimize UI, add easily accessible restart code, fix bug in network manager RaiseEvent

This commit is contained in:
John Gross
2017-07-16 10:14:04 -07:00
parent b814d1210b
commit e9b432288e
35 changed files with 441 additions and 110 deletions

View File

@@ -75,11 +75,16 @@ namespace Torch.API
/// </summary>
void Stop();
/// <summary>
/// Restart the Torch instance.
/// </summary>
void Restart();
/// <summary>
/// Initializes a save of the game.
/// </summary>
/// <param name="callerId">Id of the player who initiated the save.</param>
void Save(long callerId);
Task Save(long callerId);
/// <summary>
/// Initialize the Torch instance.

View File

@@ -14,12 +14,12 @@ namespace Torch.API.Managers
/// <summary>
/// Fired when plugins are loaded.
/// </summary>
event Action<List<ITorchPlugin>> PluginsLoaded;
event Action<IList<ITorchPlugin>> PluginsLoaded;
/// <summary>
/// Collection of loaded plugins.
/// </summary>
ObservableCollection<ITorchPlugin> Plugins { get; }
IList<ITorchPlugin> Plugins { get; }
/// <summary>
/// Updates all loaded plugins.

View File

@@ -1,4 +1,4 @@
using System.Reflection;
[assembly: AssemblyVersion("1.0.186.642")]
[assembly: AssemblyFileVersion("1.0.186.642")]
[assembly: AssemblyVersion("1.0.195.514")]
[assembly: AssemblyFileVersion("1.0.195.514")]

View File

@@ -100,6 +100,8 @@ namespace Torch.Server
var waitProc = Process.GetProcessById(pid);
_log.Warn($"Waiting for process {pid} to exit.");
waitProc.WaitForExit();
_log.Info("Continuing in 5 seconds.");
Thread.Sleep(5000);
}
catch
{
@@ -245,8 +247,8 @@ quit";
}*/
_server = new TorchServer(config);
_server.Init();
_server.Init();
if (config.NoGui || config.Autostart)
{
new Thread(() => _server.Start()).Start();
@@ -255,7 +257,6 @@ quit";
if (!config.NoGui)
{
var ui = new TorchUI((TorchServer)_server);
ui.LoadConfig(config);
ui.ShowDialog();
}
}

View File

@@ -1,4 +1,4 @@
using System.Reflection;
[assembly: AssemblyVersion("1.0.186.642")]
[assembly: AssemblyFileVersion("1.0.186.642")]
[assembly: AssemblyVersion("1.0.195.514")]
[assembly: AssemblyFileVersion("1.0.195.514")]

View File

@@ -24,7 +24,7 @@ namespace Torch.Server
public bool Update { get; set; }
/// <inheritdoc />
[XmlIgnore, Arg("autostart", "Start the server immediately.")]
[Arg("autostart", "Start the server immediately.")]
public bool Autostart { get; set; }
/// <inheritdoc />
@@ -32,7 +32,7 @@ namespace Torch.Server
public bool RestartOnCrash { get; set; }
/// <inheritdoc />
[XmlIgnore, Arg("nogui", "Do not show the Torch UI.")]
[Arg("nogui", "Do not show the Torch UI.")]
public bool NoGui { get; set; }
/// <inheritdoc />

View File

@@ -10,6 +10,7 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Xml.Serialization.GeneratedAssembly;
using Sandbox.Engine.Analytics;
using Sandbox.Game.Multiplayer;
@@ -20,6 +21,7 @@ using Torch.Managers;
using VRage.Dedicated;
using VRage.FileSystem;
using VRage.Game;
using VRage.Game.ModAPI;
using VRage.Game.ObjectBuilder;
using VRage.Game.SessionComponents;
using VRage.Library;
@@ -49,6 +51,7 @@ namespace Torch.Server
private float _simRatio;
private readonly AutoResetEvent _stopHandle = new AutoResetEvent(false);
private Timer _watchdog;
private Stopwatch _uptime;
public TorchServer(TorchConfig config = null)
{
@@ -133,6 +136,7 @@ namespace Torch.Server
if (State != ServerState.Stopped)
return;
_uptime = Stopwatch.StartNew();
IsRunning = true;
GameThread = Thread.CurrentThread;
Config.Save();
@@ -166,7 +170,8 @@ namespace Torch.Server
{
base.Update();
SimulationRatio = Sync.ServerSimulationRatio;
ElapsedPlayTime = MySession.Static?.ElapsedPlayTime ?? default(TimeSpan);
var elapsed = TimeSpan.FromSeconds(Math.Floor(_uptime.Elapsed.TotalSeconds));
ElapsedPlayTime = elapsed;
if (_watchdog == null && Instance.Config.TickTimeout > 0)
{
@@ -215,15 +220,21 @@ namespace Torch.Server
IsRunning = false;
}
public void Restart()
/// <summary>
/// Restart the program. DOES NOT SAVE!
/// </summary>
public override void Restart()
{
var exe = Assembly.GetExecutingAssembly().Location;
((TorchConfig)Config).WaitForPID = Process.GetCurrentProcess().Id.ToString();
Process.Start(exe, Config.ToString());
Environment.Exit(0);
}
/// <inheritdoc/>
public override void Save(long callerId)
public override Task Save(long callerId)
{
base.SaveGameAsync((statusCode) => SaveCompleted(statusCode, callerId));
return SaveGameAsync(statusCode => SaveCompleted(statusCode, callerId));
}
/// <summary>
@@ -231,7 +242,7 @@ namespace Torch.Server
/// </summary>
/// <param name="statusCode">Return code of the save operation</param>
/// <param name="callerId">Caller of the save operation</param>
private void SaveCompleted(SaveGameStatus statusCode, long callerId)
private void SaveCompleted(SaveGameStatus statusCode, long callerId = 0)
{
switch (statusCode)
{

View File

@@ -56,7 +56,7 @@ namespace Torch.Server.ViewModels
public SessionSettingsViewModel SessionSettings { get; }
public ObservableCollection<string> WorldPaths { get; } = new ObservableCollection<string>();
public ObservableList<string> WorldPaths { get; } = new ObservableList<string>();
private string _administrators;
public string Administrators { get => _administrators; set { _administrators = value; OnPropertyChanged(); } }
private string _banned;

View File

@@ -15,7 +15,7 @@ namespace Torch.Server.ViewModels.Blocks
public class BlockViewModel : EntityViewModel
{
public IMyTerminalBlock Block { get; }
public MTObservableCollection<PropertyViewModel> Properties { get; } = new MTObservableCollection<PropertyViewModel>();
public ObservableList<PropertyViewModel> Properties { get; } = new ObservableList<PropertyViewModel>();
public string FullName => $"{Block.CubeGrid.CustomName} - {Block.CustomName}";
@@ -24,8 +24,11 @@ namespace Torch.Server.ViewModels.Blocks
get => Block?.CustomName ?? "null";
set
{
TorchBase.Instance.InvokeBlocking(() => Block.CustomName = value);
TorchBase.Instance.Invoke(() =>
{
Block.CustomName = value;
OnPropertyChanged();
});
}
}
@@ -37,8 +40,11 @@ namespace Torch.Server.ViewModels.Blocks
get => ((MySlimBlock)Block.SlimBlock).BuiltBy;
set
{
TorchBase.Instance.InvokeBlocking(() => ((MySlimBlock)Block.SlimBlock).TransferAuthorship(value));
TorchBase.Instance.Invoke(() =>
{
((MySlimBlock)Block.SlimBlock).TransferAuthorship(value);
OnPropertyChanged();
});
}
}

View File

@@ -16,17 +16,15 @@ namespace Torch.Server.ViewModels.Blocks
public T Value
{
get
{
var val = default(T);
TorchBase.Instance.InvokeBlocking(() => val = _prop.GetValue(Block.Block));
return val;
}
get => _prop.GetValue(Block.Block);
set
{
TorchBase.Instance.InvokeBlocking(() => _prop.SetValue(Block.Block, value));
TorchBase.Instance.Invoke(() =>
{
_prop.SetValue(Block.Block, value);
OnPropertyChanged();
Block.RefreshModel();
});
}
}

View File

@@ -40,6 +40,7 @@ namespace Torch.Server.ViewModels.Entities
public EntityViewModel(IMyEntity entity, EntityTreeViewModel tree)
{
Entity = entity;
Tree = tree;
}
public EntityViewModel()

View File

@@ -1,6 +1,9 @@
using System.Linq;
using System;
using System.Linq;
using NLog;
using Sandbox.Game.Entities;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using Sandbox.ModAPI;
using Torch.Server.ViewModels.Blocks;
@@ -9,17 +12,16 @@ namespace Torch.Server.ViewModels.Entities
public class GridViewModel : EntityViewModel, ILazyLoad
{
private MyCubeGrid Grid => (MyCubeGrid)Entity;
public MTObservableCollection<BlockViewModel> Blocks { get; } = new MTObservableCollection<BlockViewModel>();
private static readonly Logger Log = LogManager.GetLogger(nameof(GridViewModel));
public ObservableList<BlockViewModel> Blocks { get; } = new ObservableList<BlockViewModel>();
/// <inheritdoc />
public string DescriptiveName => $"{Name} ({Grid.BlocksCount} blocks)";
public string DescriptiveName { get; }
public GridViewModel() { }
public GridViewModel(MyCubeGrid grid, EntityTreeViewModel tree) : base(grid, tree)
{
Log.Debug($"Creating model {Grid.DisplayName}");
DescriptiveName = $"{grid.DisplayName} ({grid.BlocksCount} blocks)";
Blocks.Add(new BlockViewModel(null, Tree));
}
@@ -28,7 +30,6 @@ namespace Torch.Server.ViewModels.Entities
if (obj.FatBlock != null)
Blocks.RemoveWhere(b => b.Block.EntityId == obj.FatBlock?.EntityId);
Blocks.Sort(b => b.Block.GetType().AssemblyQualifiedName);
OnPropertyChanged(nameof(Name));
}
@@ -36,9 +37,8 @@ namespace Torch.Server.ViewModels.Entities
{
var block = obj.FatBlock as IMyTerminalBlock;
if (block != null)
Blocks.Add(new BlockViewModel(block, Tree));
Blocks.Insert(new BlockViewModel(block, Tree), b => b.Name);
Blocks.Sort(b => b.Block.GetType().AssemblyQualifiedName);
OnPropertyChanged(nameof(Name));
}
@@ -48,20 +48,23 @@ namespace Torch.Server.ViewModels.Entities
if (_load)
return;
Log.Debug($"Loading model {Grid.DisplayName}");
_load = true;
Blocks.Clear();
TorchBase.Instance.InvokeBlocking(() =>
TorchBase.Instance.Invoke(() =>
{
foreach (var block in Grid.GetFatBlocks().Where(b => b is IMyTerminalBlock))
{
Blocks.Add(new BlockViewModel((IMyTerminalBlock)block, Tree));
}
});
Blocks.Sort(b => b.Block.GetType().AssemblyQualifiedName);
Grid.OnBlockAdded += Grid_OnBlockAdded;
Grid.OnBlockRemoved += Grid_OnBlockRemoved;
Tree.ControlDispatcher.BeginInvoke(() =>
{
Blocks.Sort(b => b.Block.CustomName);
});
});
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Torch.Server.ViewModels.Entities
public override bool CanStop => false;
public MTObservableCollection<GridViewModel> AttachedGrids { get; } = new MTObservableCollection<GridViewModel>();
public ObservableList<GridViewModel> AttachedGrids { get; } = new ObservableList<GridViewModel>();
public async Task UpdateAttachedGrids()
{

View File

@@ -3,22 +3,28 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using Sandbox.Game.Entities;
using Sandbox.Game.Entities.Character;
using Torch.Server.ViewModels.Entities;
using VRage.Game.ModAPI;
using VRage.ModAPI;
using System.Windows.Threading;
using NLog;
namespace Torch.Server.ViewModels
{
public class EntityTreeViewModel : ViewModel
{
public MTObservableCollection<GridViewModel> Grids { get; set; } = new MTObservableCollection<GridViewModel>();
public MTObservableCollection<CharacterViewModel> Characters { get; set; } = new MTObservableCollection<CharacterViewModel>();
public MTObservableCollection<EntityViewModel> FloatingObjects { get; set; } = new MTObservableCollection<EntityViewModel>();
public MTObservableCollection<VoxelMapViewModel> VoxelMaps { get; set; } = new MTObservableCollection<VoxelMapViewModel>();
//TODO: these should be sorted sets for speed
public ObservableList<GridViewModel> Grids { get; set; } = new ObservableList<GridViewModel>();
public ObservableList<CharacterViewModel> Characters { get; set; } = new ObservableList<CharacterViewModel>();
public ObservableList<EntityViewModel> FloatingObjects { get; set; } = new ObservableList<EntityViewModel>();
public ObservableList<VoxelMapViewModel> VoxelMaps { get; set; } = new ObservableList<VoxelMapViewModel>();
public Dispatcher ControlDispatcher => _control.Dispatcher;
private EntityViewModel _currentEntity;
private UserControl _control;
public EntityViewModel CurrentEntity
{
@@ -26,7 +32,12 @@ namespace Torch.Server.ViewModels
set { _currentEntity = value; OnPropertyChanged(); }
}
public EntityTreeViewModel()
public EntityTreeViewModel(UserControl control)
{
_control = control;
}
public void Init()
{
MyEntities.OnEntityAdd += MyEntities_OnEntityAdd;
MyEntities.OnEntityRemove += MyEntities_OnEntityRemove;
@@ -56,20 +67,16 @@ namespace Torch.Server.ViewModels
switch (obj)
{
case MyCubeGrid grid:
if (Grids.All(g => g.Entity.EntityId != obj.EntityId))
Grids.Add(new GridViewModel(grid, this));
Grids.Insert(new GridViewModel(grid, this), g => g.Name);
break;
case MyCharacter character:
if (Characters.All(g => g.Entity.EntityId != obj.EntityId))
Characters.Add(new CharacterViewModel(character, this));
Characters.Insert(new CharacterViewModel(character, this), c => c.Name);
break;
case MyFloatingObject floating:
if (FloatingObjects.All(g => g.Entity.EntityId != obj.EntityId))
FloatingObjects.Add(new FloatingObjectViewModel(floating, this));
FloatingObjects.Insert(new FloatingObjectViewModel(floating, this), f => f.Name);
break;
case MyVoxelBase voxel:
if (VoxelMaps.All(g => g.Entity.EntityId != obj.EntityId))
VoxelMaps.Add(new VoxelMapViewModel(voxel, this));
VoxelMaps.Insert(new VoxelMapViewModel(voxel, this), v => v.Name);
break;
}
}

View File

@@ -11,7 +11,7 @@ namespace Torch.Server.ViewModels
{
public class PluginManagerViewModel : ViewModel
{
public MTObservableCollection<PluginViewModel> Plugins { get; } = new MTObservableCollection<PluginViewModel>();
public ObservableList<PluginViewModel> Plugins { get; } = new ObservableList<PluginViewModel>();
private PluginViewModel _selectedPlugin;
public PluginViewModel SelectedPlugin
@@ -24,10 +24,12 @@ namespace Torch.Server.ViewModels
public PluginManagerViewModel(IPluginManager pluginManager)
{
foreach (var plugin in pluginManager)
Plugins.Add(new PluginViewModel(plugin));
pluginManager.PluginsLoaded += PluginManager_PluginsLoaded;
}
private void PluginManager_PluginsLoaded(List<ITorchPlugin> obj)
private void PluginManager_PluginsLoaded(IList<ITorchPlugin> obj)
{
Plugins.Clear();
foreach (var plugin in obj)

View File

@@ -35,7 +35,7 @@ namespace Torch.Server.ViewModels
BlockLimits.Add(new BlockLimitViewModel(this, limit.Key, limit.Value));
}
public MTObservableCollection<BlockLimitViewModel> BlockLimits { get; } = new MTObservableCollection<BlockLimitViewModel>();
public ObservableList<BlockLimitViewModel> BlockLimits { get; } = new ObservableList<BlockLimitViewModel>();
#region Multipliers

View File

@@ -11,6 +11,7 @@
<TextBox x:Name="Message" DockPanel.Dock="Left" Margin="5,5,5,5" KeyDown="Message_OnKeyDown"></TextBox>
</DockPanel>
<ListView x:Name="ChatItems" ItemsSource="{Binding ChatHistory}" Margin="5,5,5,5">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"/>
<ListView.ItemTemplate>
<DataTemplate>
<WrapPanel>

View File

@@ -41,8 +41,10 @@ namespace Torch.Server
{
_server = (TorchBase)server;
_multiplayer = (MultiplayerManager)server.Multiplayer;
ChatItems.Items.Clear();
DataContext = _multiplayer;
_multiplayer.ChatHistory.CollectionChanged += ChatHistory_CollectionChanged;
if (_multiplayer.ChatHistory is INotifyCollectionChanged ncc)
ncc.CollectionChanged += ChatHistory_CollectionChanged;
}
private void ChatHistory_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
@@ -74,22 +76,26 @@ namespace Torch.Server
return;
var commands = _server.Commands;
string response = null;
if (commands.IsCommand(text))
{
_multiplayer.ChatHistory.Add(new ChatMessage(DateTime.Now, 0, "Server", text));
_server.InvokeBlocking(() =>
_server.Invoke(() =>
{
response = commands.HandleCommandFromServer(text);
var response = commands.HandleCommandFromServer(text);
Dispatcher.BeginInvoke(() => OnMessageEntered_Callback(response));
});
}
else
{
_server.Multiplayer.SendMessage(text);
}
if (!string.IsNullOrEmpty(response))
_multiplayer.ChatHistory.Add(new ChatMessage(DateTime.Now, 0, "Server", response));
Message.Text = "";
}
private void OnMessageEntered_Callback(string response)
{
if (!string.IsNullOrEmpty(response))
_multiplayer.ChatHistory.Add(new ChatMessage(DateTime.Now, 0, "Server", response));
}
}
}

View File

@@ -80,7 +80,7 @@
</DockPanel>
<Button Content="Add" Margin="3" Click="AddLimit_OnClick" />
<ListView ItemsSource="{Binding BlockLimits}" Margin="3">
<ListBox.ItemTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding BlockType}" Width="150" Margin="3" />
@@ -88,7 +88,7 @@
<Button Content=" X " Margin="3" Click="RemoveLimit_OnClick" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Expander>

View File

@@ -53,7 +53,6 @@ namespace Torch.Server.Views
Log.Info("Saved DS config.");
try
{
//var checkpoint = MyLocalCache.LoadCheckpoint(Config.LoadWorld, out _);
MyObjectBuilderSerializer.DeserializeXML(Path.Combine(Config.LoadWorld, "Sandbox.sbc"), out MyObjectBuilder_Checkpoint checkpoint, out ulong sizeInBytes);
if (checkpoint == null)
{

View File

@@ -8,9 +8,6 @@
xmlns:entities="clr-namespace:Torch.Server.ViewModels.Entities"
xmlns:blocks="clr-namespace:Torch.Server.ViewModels.Blocks"
mc:Ignorable="d">
<UserControl.DataContext>
<viewModels:EntityTreeViewModel />
</UserControl.DataContext>
<DockPanel>
<DockPanel DockPanel.Dock="Left">
<StackPanel DockPanel.Dock="Bottom">

View File

@@ -27,12 +27,14 @@ namespace Torch.Server.Views
/// </summary>
public partial class EntitiesControl : UserControl
{
public EntityTreeViewModel Entities { get; set; } = new EntityTreeViewModel();
public EntityTreeViewModel Entities { get; set; }
public EntitiesControl()
{
InitializeComponent();
Entities = new EntityTreeViewModel(this);
DataContext = Entities;
Entities.Init();
}
private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)

View File

@@ -47,6 +47,7 @@ namespace Torch.Server
Chat.BindServer(server);
PlayerList.BindServer(server);
Plugins.BindServer(server);
LoadConfig((TorchConfig)server.Config);
}
public void LoadConfig(TorchConfig config)

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
@@ -10,15 +9,18 @@ using System.Windows.Threading;
namespace Torch
{
[Obsolete("Use ObservableList<T>.")]
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())
foreach (var del in collectionChanged.GetInvocationList())
{
var nh = (NotifyCollectionChangedEventHandler)del;
var dispObj = nh.Target as DispatcherObject;
var dispatcher = dispObj?.Dispatcher;

View File

@@ -0,0 +1,186 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Threading;
namespace Torch
{
/// <summary>
/// An observable version of <see cref="List{T}"/>.
/// </summary>
public class ObservableList<T> : IList<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
private List<T> _internalList = new List<T>();
/// <inheritdoc />
public event NotifyCollectionChangedEventHandler CollectionChanged;
/// <inheritdoc />
public event PropertyChangedEventHandler PropertyChanged;
/// <inheritdoc />
public void Clear()
{
_internalList.Clear();
OnPropertyChanged(nameof(Count));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
/// <inheritdoc />
public bool Contains(T item)
{
return _internalList.Contains(item);
}
/// <inheritdoc />
public void CopyTo(T[] array, int arrayIndex)
{
_internalList.CopyTo(array, arrayIndex);
}
/// <inheritdoc />
public bool Remove(T item)
{
var oldIndex = _internalList.IndexOf(item);
if (!_internalList.Remove(item))
return false;
OnPropertyChanged(nameof(Count));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, oldIndex));
return true;
}
/// <inheritdoc />
public int Count => _internalList.Count;
/// <inheritdoc />
public bool IsReadOnly => false;
/// <inheritdoc />
public void Add(T item)
{
_internalList.Add(item);
OnPropertyChanged(nameof(Count));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, Count - 1));
}
/// <inheritdoc />
public int IndexOf(T item) => _internalList.IndexOf(item);
/// <inheritdoc />
public void Insert(int index, T item)
{
_internalList.Insert(index, item);
OnPropertyChanged(nameof(Count));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
}
/// <summary>
/// Inserts an item in order based on the provided selector and comparer. This will only work properly on a pre-sorted list.
/// </summary>
public void Insert<TKey>(T item, Func<T, TKey> selector, IComparer<TKey> comparer = null)
{
comparer = comparer ?? Comparer<TKey>.Default;
var key1 = selector(item);
for (var i = 0; i < _internalList.Count; i++)
{
var key2 = selector(_internalList[i]);
if (comparer.Compare(key1, key2) < 1)
{
Insert(i, item);
return;
}
}
Add(item);
}
/// <inheritdoc />
public void RemoveAt(int index)
{
var old = this[index];
_internalList.RemoveAt(index);
OnPropertyChanged(nameof(Count));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, old, index));
}
public T this[int index]
{
get => _internalList[index];
set
{
var old = _internalList[index];
if (old.Equals(value))
return;
_internalList[index] = value;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, old, index));
}
}
/// <summary>
/// Sorts the list using the given selector and comparer./>
/// </summary>
public void Sort<TKey>(Func<T, TKey> selector, IComparer<TKey> comparer = null)
{
comparer = comparer ?? Comparer<TKey>.Default;
var sortedItems = _internalList.OrderBy(selector, comparer).ToList();
_internalList = sortedItems;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
/// <summary>
/// Removes all items that satisfy the given condition.
/// </summary>
public void RemoveWhere(Func<T, bool> condition)
{
for (var i = Count - 1; i > 0; i--)
{
if (condition?.Invoke(this[i]) ?? false)
RemoveAt(i);
}
}
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var collectionChanged = CollectionChanged;
if (collectionChanged != null)
foreach (var del in collectionChanged.GetInvocationList())
{
var nh = (NotifyCollectionChangedEventHandler)del;
var dispObj = nh.Target as DispatcherObject;
var dispatcher = dispObj?.Dispatcher;
if (dispatcher != null && !dispatcher.CheckAccess())
{
dispatcher.BeginInvoke(() => nh.Invoke(this, e), DispatcherPriority.DataBind);
continue;
}
nh.Invoke(this, e);
}
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <inheritdoc />
public IEnumerator<T> GetEnumerator()
{
return _internalList.GetEnumerator();
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)_internalList).GetEnumerator();
}
}
}

View File

@@ -6,7 +6,10 @@ using System.Text;
namespace Torch
{
public class CommandLine
/// <summary>
/// Base class that adds tools for setting type properties through the command line.
/// </summary>
public abstract class CommandLine
{
private readonly string _argPrefix;
private readonly Dictionary<ArgAttribute, PropertyInfo> _args = new Dictionary<ArgAttribute, PropertyInfo>();

View File

@@ -21,7 +21,7 @@ namespace Torch.Commands
public CommandTree Commands { get; set; } = new CommandTree();
private Logger _log = LogManager.GetLogger(nameof(CommandManager));
public CommandManager(ITorchBase torch, char prefix = '/') : base(torch)
public CommandManager(ITorchBase torch, char prefix = '!') : base(torch)
{
Prefix = prefix;
}

View File

@@ -45,6 +45,42 @@ namespace Torch.Commands
}
}
[Command("longhelp", "Get verbose help. Will send a long message, check the Comms tab.")]
public void LongHelp()
{
var commandManager = Context.Torch.GetManager<CommandManager>();
commandManager.Commands.GetNode(Context.Args, out CommandTree.CommandNode node);
if (node != null)
{
var command = node.Command;
var children = node.Subcommands.Select(x => x.Key);
var sb = new StringBuilder();
if (command != null)
{
sb.AppendLine($"Syntax: {command.SyntaxHelp}");
sb.Append(command.HelpText);
}
if (node.Subcommands.Count() != 0)
sb.Append($"\nSubcommands: {string.Join(", ", children)}");
Context.Respond(sb.ToString());
}
else
{
var sb = new StringBuilder("Available commands:\n");
foreach (var command in commandManager.Commands.WalkTree())
{
if (command.IsCommand)
sb.AppendLine($"{command.Command.SyntaxHelp}\n {command.Command.HelpText}");
}
Context.Respond(sb.ToString());
}
}
[Command("ver", "Shows the running Torch version.")]
[Permission(MyPromoteLevel.None)]
public void Version()
@@ -62,12 +98,23 @@ namespace Torch.Commands
}
[Command("stop", "Stops the server.")]
public void Stop()
public void Stop(bool save = true)
{
Context.Respond("Stopping server.");
if (save)
Context.Torch.Save(Context.Player?.IdentityId ?? 0).Wait();
Context.Torch.Stop();
}
[Command("restart", "Restarts the server.")]
public void Restart(bool save = true)
{
Context.Respond("Restarting server.");
if (save)
Context.Torch.Save(Context.Player?.IdentityId ?? 0).Wait();
Context.Torch.Restart();
}
/// <summary>
/// Initializes a save of the game.
/// Caller id defaults to 0 in the case of triggering the chat command from server.

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace Torch
{
public static class DispatcherExtensions
{
public static DispatcherOperation BeginInvoke(this Dispatcher dispatcher, Action action, DispatcherPriority priority = DispatcherPriority.Normal)
{
return dispatcher.BeginInvoke(priority, action);
}
}
}

View File

@@ -16,6 +16,7 @@ using NLog;
using Torch;
using Sandbox;
using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Entities.Character;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using Sandbox.ModAPI;
@@ -43,7 +44,7 @@ namespace Torch.Managers
/// <inheritdoc />
public event MessageReceivedDel MessageReceived;
public MTObservableCollection<IChatMessage> ChatHistory { get; } = new MTObservableCollection<IChatMessage>();
public IList<IChatMessage> ChatHistory { get; } = new ObservableList<IChatMessage>();
public ObservableDictionary<ulong, PlayerViewModel> Players { get; } = new ObservableDictionary<ulong, PlayerViewModel>();
public IMyPlayer LocalPlayer => MySession.Static.LocalHumanPlayer;
private static readonly Logger Log = LogManager.GetLogger(nameof(MultiplayerManager));
@@ -99,6 +100,17 @@ namespace Torch.Managers
return p;
}
public ulong GetSteamId(long identityId)
{
foreach (var kv in _onlinePlayers)
{
if (kv.Value.Identity.IdentityId == identityId)
return kv.Key.SteamId;
}
return 0;
}
/// <inheritdoc />
public string GetSteamUsername(ulong steamId)
{
@@ -108,17 +120,24 @@ namespace Torch.Managers
/// <inheritdoc />
public void SendMessage(string message, string author = "Server", long playerId = 0, string font = MyFontEnum.Red)
{
ChatHistory.Add(new ChatMessage(DateTime.Now, 0, "Server", message));
ChatHistory.Add(new ChatMessage(DateTime.Now, 0, author, message));
var commands = Torch.GetManager<CommandManager>();
if (commands.IsCommand(message))
{
var response = commands.HandleCommandFromServer(message);
ChatHistory.Add(new ChatMessage(DateTime.Now, 0, "Server", response));
ChatHistory.Add(new ChatMessage(DateTime.Now, 0, author, response));
}
else
{
var msg = new ScriptedChatMsg { Author = author, Font = font, Target = playerId, Text = message };
MyMultiplayerBase.SendScriptedChatMessage(ref msg);
var character = MySession.Static.Players.TryGetIdentity(playerId)?.Character;
var steamId = GetSteamId(playerId);
if (character == null)
return;
var addToGlobalHistoryMethod = typeof(MyCharacter).GetMethod("OnGlobalMessageSuccess", BindingFlags.Instance | BindingFlags.NonPublic);
Torch.GetManager<NetworkManager>().RaiseEvent(addToGlobalHistoryMethod, character, steamId, steamId, message);
}
}

View File

@@ -311,7 +311,7 @@ namespace Torch.Managers
var parameters = method.GetParameters();
for (var i = 0; i < parameters.Length; i++)
{
if (argTypes[i] != parameters[i].ParameterType)
if (argTypes[i + 1] != parameters[i].ParameterType)
throw new TypeLoadException($"Type mismatch on method parameters. Expected {string.Join(", ", parameters.Select(p => p.ParameterType.ToString()))} got {string.Join(", ", argTypes.Select(t => t.ToString()))}");
}

View File

@@ -23,9 +23,9 @@ namespace Torch.Managers
private UpdateManager _updateManager;
/// <inheritdoc />
public ObservableCollection<ITorchPlugin> Plugins { get; } = new ObservableCollection<ITorchPlugin>();
public IList<ITorchPlugin> Plugins { get; } = new ObservableList<ITorchPlugin>();
public event Action<List<ITorchPlugin>> PluginsLoaded;
public event Action<IList<ITorchPlugin>> PluginsLoaded;
public PluginManager(ITorchBase torchInstance) : base(torchInstance)
{

View File

@@ -5,9 +5,24 @@
/// </summary>
public enum SaveGameStatus : byte
{
/// <summary>
/// The game was saved.
/// </summary>
Success = 0,
/// <summary>
/// A save operation is already in progress.
/// </summary>
SaveInProgress = 1,
/// <summary>
/// The game is not in a save-able state.
/// </summary>
GameNotReady = 2,
/// <summary>
/// The save operation timed out.
/// </summary>
TimedOut = 3
};
}

View File

@@ -146,6 +146,8 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ChatMessage.cs" />
<Compile Include="Collections\ObservableList.cs" />
<Compile Include="DispatcherExtensions.cs" />
<Compile Include="SaveGameStatus.cs" />
<Compile Include="Collections\KeyTree.cs" />
<Compile Include="Collections\ObservableDictionary.cs" />

View File

@@ -128,7 +128,7 @@ namespace Torch
return Thread.CurrentThread.ManagedThreadId == MySandboxGame.Static.UpdateThread.ManagedThreadId;
}
public async Task SaveGameAsync(Action<SaveGameStatus> callback)
public Task SaveGameAsync(Action<SaveGameStatus> callback)
{
Log.Info("Saving game");
@@ -142,26 +142,17 @@ namespace Torch
}
else
{
using (var e = new AutoResetEvent(false))
var e = new AutoResetEvent(false);
MyAsyncSaving.Start(() => e.Set());
return Task.Run(() =>
{
MyAsyncSaving.Start(() =>
{
MySector.ResetEyeAdaptation = true;
e.Set();
callback?.Invoke(e.WaitOne(5000) ? SaveGameStatus.Success : SaveGameStatus.TimedOut);
e.Dispose();
});
await Task.Run(() =>
{
if (e.WaitOne(60000))
{
callback?.Invoke(SaveGameStatus.Success);
return;
}
callback?.Invoke(SaveGameStatus.TimedOut);
}).ConfigureAwait(false);
}
}
return Task.CompletedTask;
}
#region Game Actions
@@ -306,19 +297,28 @@ namespace Torch
}
/// <inheritdoc/>
public virtual void Save(long callerId)
public virtual Task Save(long callerId)
{
return Task.CompletedTask;
}
/// <inheritdoc
/// <inheritdoc/>
public virtual void Start()
{
}
/// <inheritdoc />
public virtual void Stop() { }
public virtual void Stop()
{
}
/// <inheritdoc />
public virtual void Restart()
{
}
/// <inheritdoc />
public virtual void Dispose()