Merge remote-tracking branch 'origin/PreRelease'

This commit is contained in:
John Gross
2019-06-16 02:20:43 -07:00
17 changed files with 391 additions and 67 deletions

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace Torch
{
@@ -12,7 +13,8 @@ namespace Torch
string InstancePath { get; set; }
bool NoGui { get; set; }
bool NoUpdate { get; set; }
List<string> Plugins { get; set; }
List<Guid> Plugins { get; set; }
bool LocalPlugins { get; set; }
bool RestartOnCrash { get; set; }
bool ShouldUpdatePlugins { get; }
bool ShouldUpdateTorch { get; }

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VRage.Collections;
using VRage.Network;
namespace Torch.API.Managers
@@ -41,5 +42,24 @@ namespace Torch.API.Managers
/// <param name="font">Font to use</param>
/// <param name="targetSteamId">Player to send the message to, or everyone by default</param>
void SendMessageAsOther(string author, string message, string font, ulong targetSteamId = 0);
/// <summary>
/// Mute user from global chat.
/// </summary>
/// <param name="steamId"></param>
/// <returns></returns>
bool MuteUser(ulong steamId);
/// <summary>
/// Unmute user from global chat.
/// </summary>
/// <param name="steamId"></param>
/// <returns></returns>
bool UnmuteUser(ulong steamId);
/// <summary>
/// Users which are not allowed to chat.
/// </summary>
HashSetReader<ulong> MutedUsers { get; }
}
}

View File

@@ -283,12 +283,16 @@
<Compile Include="Views\Entities\CharacterView.xaml.cs">
<DependentUpon>CharacterView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\Extensions.cs" />
<Compile Include="Views\ModListControl.xaml.cs">
<DependentUpon>ModListControl.xaml</DependentUpon>
</Compile>
<Compile Include="Views\PluginBrowser.xaml.cs">
<DependentUpon>PluginBrowser.xaml</DependentUpon>
</Compile>
<Compile Include="Views\RoleEditor.xaml.cs">
<DependentUpon>RoleEditor.xaml</DependentUpon>
</Compile>
<Compile Include="Views\ThemeControl.xaml.cs">
<DependentUpon>ThemeControl.xaml</DependentUpon>
</Compile>
@@ -473,6 +477,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\RoleEditor.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\SessionSettingsView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>

View File

@@ -59,7 +59,11 @@ namespace Torch.Server
public int TickTimeout { get; set; } = 60;
/// <inheritdoc />
public List<string> Plugins { get; set; } = new List<string>();
[Arg("plugins", "Starts Torch with the given plugin GUIDs (space delimited).")]
public List<Guid> Plugins { get; set; } = new List<Guid>();
[Arg("localplugins", "Loads all pluhins from disk, ignores the plugins defined in config.")]
public bool LocalPlugins { get; set; }
public string ChatName { get; set; } = "Server";

View File

@@ -45,6 +45,8 @@ namespace Torch.Server
private ServerState _state;
private Stopwatch _uptime;
private Timer _watchdog;
private int _players;
private MultiplayerManagerDedicated _multiplayerManagerDedicated;
/// <inheritdoc />
public TorchServer(TorchConfig config = null)
@@ -92,6 +94,8 @@ namespace Torch.Server
/// <inheritdoc />
public string InstancePath => Config?.InstancePath;
public int OnlinePlayers { get => _players; private set => SetValue(ref _players, value); }
/// <inheritdoc />
public override void Init()
{
@@ -188,6 +192,7 @@ namespace Torch.Server
if (newState == TorchSessionState.Loaded)
{
_multiplayerManagerDedicated = CurrentSession.Managers.GetManager<MultiplayerManagerDedicated>();
CurrentSession.Managers.GetManager<CommandManager>().RegisterCommandModule(typeof(WhitelistCommands));
ModCommunication.Register();
}
@@ -211,6 +216,7 @@ namespace Torch.Server
SimulationRatio = Math.Min(Sync.ServerSimulationRatio, 1);
var elapsed = TimeSpan.FromSeconds(Math.Floor(_uptime.Elapsed.TotalSeconds));
ElapsedPlayTime = elapsed;
OnlinePlayers = _multiplayerManagerDedicated?.Players.Count ?? 0;
if (_watchdog == null && Config.TickTimeout > 0)
{

View File

@@ -58,7 +58,7 @@
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Disabled">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@@ -99,6 +99,7 @@
<TextBox Text="{Binding Administrators, Converter={StaticResource ListConverterString}}"
Margin="3"
Height="100" AcceptsReturn="true" VerticalScrollBarVisibility="Auto" />
<Button Content="Edit Roles" Click="RoleEdit_Onlick" Margin="3"/>
<Label Content="Reserved Players" />
<TextBox Margin="3" Height="100" AcceptsReturn="true" VerticalScrollBarVisibility="Auto"
Style="{StaticResource ValidatedTextBox}">

View File

@@ -12,6 +12,7 @@ using Torch.API.Managers;
using Torch.Server.Annotations;
using Torch.Server.Managers;
using Torch.Server.ViewModels;
using Torch.Views;
namespace Torch.Server.Views
{
@@ -122,5 +123,13 @@ namespace Torch.Server.Views
var c = new WorldGeneratorDialog(_instanceManager);
c.Show();
}
private void RoleEdit_Onlick(object sender, RoutedEventArgs e)
{
//var w = new RoleEditor(_instanceManager.DedicatedConfig.SelectedWorld);
//w.Show();
var d = new RoleEditor();
d.Edit(_instanceManager.DedicatedConfig.SelectedWorld.Checkpoint.PromotedUsers.Dictionary);
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace Torch.Server.Views
{
public static class Extensions
{
public static readonly DependencyProperty ScrollContainerProperty = DependencyProperty.RegisterAttached("ScrollContainer", typeof(bool), typeof(Extensions), new PropertyMetadata(true));
public static bool GetScrollContainer(this UIElement ui)
{
return (bool)ui.GetValue(ScrollContainerProperty);
}
public static void SetScrollContainer(this UIElement ui, bool value)
{
ui.SetValue(ScrollContainerProperty, value);
}
}
}

View File

@@ -90,6 +90,7 @@ namespace Torch.Server.Views
private void DownloadButton_OnClick(object sender, RoutedEventArgs e)
{
var item = CurrentItem;
TorchBase.Instance.Config.Plugins.Add(new Guid(item.ID));
Task.Run(async () =>
{
var result = await PluginQuery.Instance.DownloadPlugin(item.ID);

View File

@@ -41,7 +41,9 @@ namespace Torch.Server.Views
{
if (propertyChangedEventArgs.PropertyName == nameof(PluginManagerViewModel.SelectedPlugin))
{
if (((PluginManagerViewModel)DataContext).SelectedPlugin.Control is PropertyGrid)
var plugin = ((PluginManagerViewModel)DataContext).SelectedPlugin;
if (plugin.Control is PropertyGrid || !plugin.Control.GetScrollContainer())
PScroll.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
else
PScroll.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;

View File

@@ -0,0 +1,52 @@
<Window x:Class="Torch.Server.Views.RoleEditor"
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.Views"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:modApi="clr-namespace:VRage.Game.ModAPI;assembly=VRage.Game"
mc:Ignorable="d"
Title="RoleEditor" Height="300" Width="300">
<Window.Resources>
<ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="GetEnumValues">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="modApi:MyPromoteLevel"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DataGrid x:Name="ItemGrid" AutoGenerateColumns="false" CanUserAddRows="true" Grid.Row="0">
<DataGrid.Columns>
<DataGridTextColumn Width="5*" Header="Key" Binding="{Binding Key}"/>
<DataGridComboBoxColumn Width ="5*" Header="Value" ItemsSource="{Binding Source={StaticResource GetEnumValues}}" SelectedValueBinding="{Binding Value, Mode=TwoWay}"/>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="1" Content="Add New" Margin="5" Click="AddNew_OnClick"></Button>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="Cancel" Margin="5" Click="Cancel_OnClick" />
<Button Grid.Column="1" Content="OK" Margin="5" Click="Ok_OnClick" />
</Grid>
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ComboBox Name="BulkSelect" ItemsSource="{Binding Source={StaticResource GetEnumValues}}" SelectedValue ="{Binding
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:RoleEditor}},
Path = BulkPromote, Mode=TwoWay}" Margin="5"/>
<Button Grid.Column="1" Content="Bulk edit" Margin ="5" Click="BulkEdit"/>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,123 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
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.Shapes;
using Torch.Server.Managers;
using Torch.Views;
using VRage.Game.ModAPI;
namespace Torch.Server.Views
{
/// <summary>
/// Interaction logic for RoleEditor.xaml
/// </summary>
public partial class RoleEditor : Window
{
public RoleEditor()
{
InitializeComponent();
DataContext = Items;
}
public ObservableCollection<IDictionaryItem> Items { get; } = new ObservableCollection<IDictionaryItem>();
private Type _itemType;
private Action _commitChanges;
public MyPromoteLevel BulkPromote { get; set; } = MyPromoteLevel.Scripter;
public void Edit(IDictionary dict)
{
Items.Clear();
var dictType = dict.GetType();
_itemType = typeof(DictionaryItem<,>).MakeGenericType(dictType.GenericTypeArguments[0], dictType.GenericTypeArguments[1]);
foreach (var key in dict.Keys)
{
Items.Add((IDictionaryItem)Activator.CreateInstance(_itemType, key, dict[key]));
}
ItemGrid.ItemsSource = Items;
_commitChanges = () =>
{
dict.Clear();
foreach (var item in Items)
{
dict[item.Key] = item.Value;
}
};
Show();
}
private void Cancel_OnClick(object sender, RoutedEventArgs e)
{
Close();
}
private void Ok_OnClick(object sender, RoutedEventArgs e)
{
_commitChanges?.Invoke();
Close();
}
public interface IDictionaryItem
{
object Key { get; set; }
object Value { get; set; }
}
public class DictionaryItem<TKey, TValue> : ViewModel, IDictionaryItem
{
private TKey _key;
private TValue _value;
object IDictionaryItem.Key { get => _key; set => SetValue(ref _key, (TKey)value); }
object IDictionaryItem.Value { get => _value; set => SetValue(ref _value, (TValue)value); }
public TKey Key { get => _key; set => SetValue(ref _key, value); }
public TValue Value { get => _value; set => SetValue(ref _value, value); }
public DictionaryItem()
{
_key = default(TKey);
_value = default(TValue);
}
public DictionaryItem(TKey key, TValue value)
{
_key = key;
_value = value;
}
}
private void AddNew_OnClick(object sender, RoutedEventArgs e)
{
Items.Add((IDictionaryItem)Activator.CreateInstance(_itemType));
}
private void BulkEdit(object sender, RoutedEventArgs e)
{
List<ulong> l = Items.Where(i => i.Value.Equals(BulkPromote)).Select(i => (ulong)i.Key).ToList();
var w = new CollectionEditor();
w.Edit((ICollection<ulong>)l, "Bulk edit");
var r = Items.Where(j => j.Value.Equals(BulkPromote) || l.Contains((ulong)j.Key)).ToList();
foreach (var k in r)
Items.Remove(k);
foreach (var m in l)
Items.Add(new DictionaryItem<ulong, MyPromoteLevel>(m, BulkPromote));
}
}
}

View File

@@ -57,7 +57,7 @@
</Label>
<Label x:Name="LabelPlayers">
<Label.Content>
<TextBlock ></TextBlock>
<TextBlock Text="{Binding OnlinePlayers, StringFormat=Players: {0}}"/>
</Label.Content>
</Label>
</StackPanel>

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using NLog;
namespace Torch
{
@@ -12,6 +13,7 @@ namespace Torch
{
private readonly string _argPrefix;
private readonly Dictionary<ArgAttribute, PropertyInfo> _args = new Dictionary<ArgAttribute, PropertyInfo>();
private readonly Logger _log = LogManager.GetCurrentClassLogger();
protected CommandLine(string argPrefix = "-")
{
@@ -89,6 +91,24 @@ namespace Torch
if (property.Value.PropertyType == typeof(string))
property.Value.SetValue(this, args[++i]);
if (property.Value.PropertyType == typeof(List<Guid>))
{
i++;
var l = new List<Guid>(16);
while (i < args.Length && !args[i].StartsWith(_argPrefix))
{
if (Guid.TryParse(args[i], out Guid g))
{
l.Add(g);
_log.Info($"added plugin {g}");
}
else
_log.Warn($"Failed to parse GUID {args[i]}");
i++;
}
property.Value.SetValue(this, l);
}
}
}
catch

View File

@@ -57,10 +57,16 @@ namespace Torch.Commands
if (node != null)
{
var command = node.Command;
var children = node.Subcommands.Select(x => x.Key);
var children = node.Subcommands.Where(e => Context.Player == null || e.Value.Command?.MinimumPromoteLevel <= Context.Player.PromoteLevel).Select(x => x.Key);
var sb = new StringBuilder();
if (command?.MinimumPromoteLevel > Context.Player.PromoteLevel)
{
Context.Respond("You are not authorized to use this command.");
return;
}
if (command != null)
{
sb.AppendLine($"Syntax: {command.SyntaxHelp}");
@@ -94,11 +100,11 @@ namespace Torch.Commands
if (node != null)
{
var command = node.Command;
var children = node.Subcommands.Select(x => x.Key);
var children = node.Subcommands.Where(e => e.Value.Command?.MinimumPromoteLevel <= Context.Player.PromoteLevel).Select(x => x.Key);
var sb = new StringBuilder();
if (command != null)
if (command != null && (Context.Player == null || command.MinimumPromoteLevel <= Context.Player.PromoteLevel))
{
sb.AppendLine($"Syntax: {command.SyntaxHelp}");
sb.Append(command.HelpText);
@@ -114,7 +120,7 @@ namespace Torch.Commands
var sb = new StringBuilder();
foreach (var command in commandManager.Commands.WalkTree())
{
if (command.IsCommand)
if (command.IsCommand && (Context.Player == null || command.Command.MinimumPromoteLevel <= Context.Player.PromoteLevel))
sb.AppendLine($"{command.Command.SyntaxHelp}\n {command.Command.HelpText}");
}

View File

@@ -16,6 +16,7 @@ using Torch.API.Managers;
using Torch.Managers.PatchManager;
using Torch.Utils;
using VRage;
using VRage.Collections;
using VRage.Library.Collections;
using VRage.Network;
@@ -47,6 +48,10 @@ namespace Torch.Managers.ChatManager
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private static readonly Logger _chatLog = LogManager.GetLogger("Chat");
private readonly HashSet<ulong> _muted = new HashSet<ulong>();
/// <inheritdoc />
public HashSetReader<ulong> MutedUsers => _muted;
/// <inheritdoc />
public ChatManagerServer(ITorchBase torchInstance) : base(torchInstance)
{
@@ -56,6 +61,18 @@ namespace Torch.Managers.ChatManager
/// <inheritdoc />
public event MessageProcessingDel MessageProcessing;
/// <inheritdoc />
public bool MuteUser(ulong steamId)
{
return _muted.Add(steamId);
}
/// <inheritdoc />
public bool UnmuteUser(ulong steamId)
{
return _muted.Remove(steamId);
}
/// <inheritdoc />
public void SendMessageAsOther(ulong authorId, string message, ulong targetSteamId = 0)
{
@@ -128,6 +145,13 @@ namespace Torch.Managers.ChatManager
internal void RaiseMessageRecieved(ChatMsg message, ref bool consumed)
{
var torchMsg = new TorchChatMessage(GetMemberName(message.Author), message.Author, message.Text, (ChatChannel)message.Channel, message.TargetId);
if (_muted.Contains(message.Author))
{
consumed = true;
_chatLog.Warn($"MUTED USER: [{torchMsg.Channel}:{torchMsg.Target}] {torchMsg.Author}: {torchMsg.Message}");
return;
}
MessageProcessing?.Invoke(torchMsg, ref consumed);
if (!consumed)

View File

@@ -96,6 +96,8 @@ namespace Torch.Managers
public void LoadPlugins()
{
bool firstLoad = Torch.Config.Plugins.Count == 0;
List<Guid> foundPlugins = new List<Guid>();
if (Torch.Config.ShouldUpdatePlugins)
DownloadPluginUpdates();
@@ -106,22 +108,56 @@ namespace Torch.Managers
var path = Path.Combine(PluginDir, item);
var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
var manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
if (manifest == null)
if (!Torch.Config.LocalPlugins)
{
_log.Warn($"Item '{item}' is missing a manifest, skipping.");
continue;
}
if (_plugins.ContainsKey(manifest.Guid))
if (isZip && !Torch.Config.Plugins.Contains(manifest.Guid))
{
_log.Error($"The GUID provided by {manifest.Name} ({item}) is already in use by {_plugins[manifest.Guid].Name}");
continue;
if (!firstLoad)
{
_log.Warn($"Plugin {manifest.Name} ({item}) exists in the plugin directory, but is not listed in torch.cfg. Skipping load!");
return;
}
_log.Info($"First-time load: Plugin {manifest.Name} added to torch.cfg.");
Torch.Config.Plugins.Add(manifest.Guid);
}
if(isZip)
LoadPluginFromZip(path);
else
LoadPluginFromFolder(path);
foundPlugins.Add(manifest.Guid);
}
LoadPlugin(item);
}
if (!Torch.Config.LocalPlugins && firstLoad)
Torch.Config.Save();
if (!Torch.Config.LocalPlugins)
{
List<string> toLoad = new List<string>();
//This is actually the easiest way to batch process async tasks and block until completion (????)
Task.WhenAll(Torch.Config.Plugins.Select(async g =>
{
try
{
if (foundPlugins.Contains(g))
{
return;
}
var item = await PluginQuery.Instance.QueryOne(g);
string s = Path.Combine(PluginDir, item.Name + ".zip");
await PluginQuery.Instance.DownloadPlugin(item, s);
lock (toLoad)
toLoad.Add(s);
}
catch (Exception ex)
{
_log.Error(ex);
}
}));
foreach (var l in toLoad)
{
LoadPlugin(l);
}
}
foreach (var plugin in _plugins.Values)
@@ -132,11 +168,34 @@ namespace Torch.Managers
PluginsLoaded?.Invoke(_plugins.Values.AsReadOnly());
}
private void LoadPlugin(string item)
{
var path = Path.Combine(PluginDir, item);
var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
var manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
if (manifest == null)
{
_log.Warn($"Item '{item}' is missing a manifest, skipping.");
return;
}
if (_plugins.ContainsKey(manifest.Guid))
{
_log.Error($"The GUID provided by {manifest.Name} ({item}) is already in use by {_plugins[manifest.Guid].Name}");
return;
}
if (isZip)
LoadPluginFromZip(path);
else
LoadPluginFromFolder(path);
}
private void DownloadPluginUpdates()
{
_log.Info("Checking for plugin updates...");
var count = 0;
var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip").Union(Directory.EnumerateDirectories(PluginDir));
var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip");
Parallel.ForEach(pluginItems, async item =>
{
PluginManifest manifest = null;
@@ -144,7 +203,12 @@ namespace Torch.Managers
{
var path = Path.Combine(PluginDir, item);
var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
if (!isZip)
{
_log.Warn($"Unzipped plugins cannot be auto-updated. Skipping plugin {item}");
return;
}
manifest = GetManifestFromZip(path);
if (manifest == null)
{
_log.Warn($"Item '{item}' is missing a manifest, skipping update check.");
@@ -188,48 +252,6 @@ namespace Torch.Managers
_log.Info($"Updated {count} plugins.");
}
private async Task<Tuple<Version, string>> GetLatestArchiveAsync(string repository)
{
try
{
//var split = repository.Split('/');
//var latest = await _gitClient.Repository.Release.GetLatest(split[0], split[1]).ConfigureAwait(false);
//if (!latest.TagName.TryExtractVersion(out Version latestVersion))
//{
// _log.Error($"Unable to parse version tag for the latest release of '{repository}.'");
//}
//var zipAsset = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip", StringComparison.CurrentCultureIgnoreCase));
//if (zipAsset == null)
//{
// _log.Error($"Unable to find archive for the latest release of '{repository}.'");
//}
//return new Tuple<Version, string>(latestVersion, zipAsset?.BrowserDownloadUrl);
return null;
}
catch (Exception e)
{
_log.Error($"Unable to get the latest release of '{repository}.'");
_log.Error(e);
return default(Tuple<Version, string>);
}
}
private Task UpdatePluginAsync(string localPath, string downloadUrl)
{
if (File.Exists(localPath))
File.Delete(localPath);
if (Directory.Exists(localPath))
Directory.Delete(localPath, true);
var fileName = downloadUrl.Split('/').Last();
var filePath = Path.Combine(PluginDir, fileName);
return new WebClient().DownloadFileTaskAsync(downloadUrl, filePath);
}
private void LoadPluginFromFolder(string directory)
{
var assemblies = new List<Assembly>();