This commit is contained in:
zznty
2022-07-21 21:57:27 +07:00
commit bc4546410e
75 changed files with 2709 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
using Torch;
using Torch.Views;
namespace TorchRemote.Plugin;
public class Config : ViewModel
{
[Display(Name = "Web Server Config", Description = "Basic configuration for serving web api.")]
public ListenerConfig Listener { get; set; } = new();
public string SecurityKey { get; set; } = string.Empty;
}
public class ListenerConfig : ViewModel
{
[Display(Name = "Url Prefix", Description = "Root url for all requests. If you want access server from remote replace + with your public ip or domain.")]
public string UrlPrefix { get; set; } = "http://+:80/";
}

View File

@@ -0,0 +1,118 @@
using System.Net;
using System.Reflection;
using System.Text.RegularExpressions;
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using EmbedIO.WebSockets;
using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using Torch.API;
using Torch.API.Managers;
using Torch.API.Plugins;
using Torch.Commands;
using Torch.Managers;
using Torch.Utils;
using TorchRemote.Models.Requests;
using TorchRemote.Models.Responses;
using TorchRemote.Models.Shared;
using TorchRemote.Plugin.Modules;
using TorchRemote.Plugin.Utils;
using VRage.Network;
namespace TorchRemote.Plugin.Controllers;
public class ChatController : WebApiController
{
private const string RootPath = "/chat";
[ReflectedMethodInfo(typeof(MyMultiplayerBase), "OnChatMessageReceived_BroadcastExcept")]
private static readonly MethodInfo BroadcastExceptMethod = null!;
[ReflectedMethodInfo(typeof(MyMultiplayerBase), "OnChatMessageReceived_SingleTarget")]
private static readonly MethodInfo SingleTargetMethod = null!;
[Route(HttpVerbs.Post, $"{RootPath}/message")]
public void SendMessage([JsonData] ChatMessageRequest request)
{
if (MyMultiplayer.Static is null)
throw new HttpException(HttpStatusCode.ServiceUnavailable);
var msg = new ChatMsg
{
CustomAuthorName = request.Author,
Text = request.Message,
Channel = (byte)request.Channel,
TargetId = request.TargetId.GetValueOrDefault()
};
switch (request.Channel)
{
case ChatChannel.Global:
case ChatChannel.GlobalScripted when request.TargetId is null:
NetworkManager.RaiseStaticEvent(BroadcastExceptMethod, msg);
break;
case ChatChannel.Private when request.TargetId is not null:
case ChatChannel.GlobalScripted:
var steamId = Sync.Players.TryGetSteamId(request.TargetId.Value);
if (steamId == 0)
throw HttpException.NotFound($"Unable to find player with identity id {request.TargetId.Value}", request.TargetId.Value);
NetworkManager.RaiseStaticEvent(SingleTargetMethod, msg, new(steamId));
break;
case ChatChannel.Faction when request.TargetId is not null:
var faction = MySession.Static.Factions.TryGetFactionById(request.TargetId.Value);
if (faction is null)
throw HttpException.NotFound($"Unable to find faction with id {request.TargetId.Value}", request.TargetId.Value);
foreach (var playerId in faction.Members.Keys.Where(Sync.Players.IsPlayerOnline))
{
NetworkManager.RaiseStaticEvent(SingleTargetMethod, msg, new(Sync.Players.TryGetSteamId(playerId)));
}
break;
default:
throw HttpException.BadRequest("Invalid channel and targetId combination");
}
if (Statics.Torch.CurrentSession?.Managers.GetManager<IChatManagerServer>() is { } manager)
manager.DisplayMessageOnSelf(request.Author, request.Message);
}
[Route(HttpVerbs.Post, $"{RootPath}/command")]
public async Task<Guid> InvokeCommand([JsonData] ChatCommandRequest request)
{
if (Statics.CommandManager is null)
throw new HttpException(HttpStatusCode.ServiceUnavailable);
if (Statics.CommandManager.Commands.GetCommand(request.Command, out var argText) is not { } command)
throw HttpException.NotFound($"Unable to find command {request.Command}", request.Command);
var argsList = Regex.Matches(argText, "(\"[^\"]+\"|\\S+)").Cast<Match>().Select(x => x.ToString().Replace("\"", "")).ToList();
var id = new Guid();
var context = new WebSocketCommandContext(Statics.Torch, command.Plugin, argText, argsList, Statics.ChatModule, id);
if (await Statics.Torch.InvokeAsync(() => command.TryInvoke(context)))
return id;
throw HttpException.BadRequest("Invalid syntax", request.Command);
}
}
internal class WebSocketCommandContext : CommandContext
{
private readonly ChatModule _module;
private readonly Guid _id;
public WebSocketCommandContext(ITorchBase torch, ITorchPlugin plugin, string rawArgs, List<string> args, ChatModule module, Guid id) : base(torch, plugin, Sync.MyId, rawArgs, args)
{
_module = module;
_id = id;
}
public override void Respond(string message, string? sender = null, string? font = null)
{
_module.SendChatResponse(new ChatCommandResponse(_id, sender ?? Torch.Config.ChatName, message));
}
}

View File

@@ -0,0 +1,76 @@
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using Torch.API.Session;
using TorchRemote.Models.Requests;
using TorchRemote.Models.Responses;
using TorchRemote.Models.Shared;
using TorchRemote.Plugin.Utils;
namespace TorchRemote.Plugin.Controllers;
public class ServerController : WebApiController
{
private const string RootPath = "/server";
[Route(HttpVerbs.Get, $"{RootPath}/status")]
public ServerStatusResponse GetStatus()
{
return new(Math.Round(Sync.ServerSimulationRatio, 2),
MySession.Static?.Players?.GetOnlinePlayerCount() ?? 0,
Statics.Torch.ElapsedPlayTime,
(ServerStatus)Statics.Torch.State);
}
[Route(HttpVerbs.Post, $"{RootPath}/start")]
public void Start()
{
if (!Statics.Torch.CanRun)
throw HttpException.BadRequest($"Server can't start in state {Statics.Torch.State}", Statics.Torch.State);
Statics.Torch.Start();
}
[Route(HttpVerbs.Post, $"{RootPath}/stop")]
public async Task Stop(StopServerRequest request)
{
if (!Statics.Torch.IsRunning)
throw HttpException.BadRequest($"Server can't stop in state {Statics.Torch.State}", Statics.Torch.State);
var saveResult = await Statics.Torch.Save(exclusive: true);
if (saveResult is not GameSaveResult.Success)
throw HttpException.InternalServerError($"Save resulted in {saveResult}", saveResult);
Statics.Torch.Stop();
}
[Route(HttpVerbs.Get, $"{RootPath}/settings")]
public ServerSettings GetSettings()
{
var settings = Statics.Torch.DedicatedInstance.DedicatedConfig;
return new(settings.ServerName ?? "unamed",
settings.WorldName ?? "unamed",
settings.ServerDescription ?? string.Empty,
settings.SessionSettings.MaxPlayers,
new(settings.IP, settings.Port));
}
[Route(HttpVerbs.Post, $"{RootPath}/settings")]
public async Task SetSettings([JsonData] ServerSettings request)
{
var settings = Statics.Torch.DedicatedInstance.DedicatedConfig;
settings.ServerName = request.ServerName;
settings.WorldName = request.MapName;
settings.ServerDescription = request.ServerDescription;
settings.SessionSettings.MaxPlayers = request.MemberLimit;
settings.IP = request.ListenEndPoint.Ip;
settings.Port = request.ListenEndPoint.Port;
if (Statics.Torch.IsRunning)
await Statics.Torch.InvokeAsync(request.ApplyDynamically);
}
}

View File

@@ -0,0 +1,24 @@
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using Swan;
using TorchRemote.Models.Responses;
using TorchRemote.Plugin.Utils;
namespace TorchRemote.Plugin.Controllers;
public class SettingsController : WebApiController
{
private const string RootPath = "/settings";
[Route(HttpVerbs.Get, $"{RootPath}/{{id}}")]
public SettingInfoResponse Get(Guid id)
{
if (!Statics.SettingManager.Settings.TryGetValue(id, out var setting))
throw HttpException.NotFound($"Setting with id {id} not found", id);
return new(setting.Name.Humanize(), setting.Properties.Select(b =>
new SettingPropertyInfo(b.DisplayInfo?.Name ?? b.Name.Humanize(),
b.DisplayInfo?.Description, b.DisplayInfo?.Order, b.TypeId))
.ToArray());
}
}

View File

@@ -0,0 +1,62 @@
using System.Net;
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using TorchRemote.Models.Responses;
using TorchRemote.Plugin.Utils;
namespace TorchRemote.Plugin.Controllers;
public class WorldsController : WebApiController
{
private const string RootPath = "/worlds";
[Route(HttpVerbs.Get, RootPath)]
public IEnumerable<Guid> Get()
{
var config = Statics.InstanceManager.DedicatedConfig;
if (config is null)
throw new HttpException(HttpStatusCode.ServiceUnavailable);
return config.Worlds.Select(b => b.FolderName.ToGuid());
}
[Route(HttpVerbs.Get, $"{RootPath}/selected")]
public Guid GetSelected()
{
if (Statics.InstanceManager.DedicatedConfig?.SelectedWorld is not { } world)
throw new HttpException(HttpStatusCode.ServiceUnavailable);
return world.FolderName.ToGuid();
}
[Route(HttpVerbs.Get, $"{RootPath}/{{id}}")]
public WorldResponse GetWorld(Guid id)
{
var config = Statics.InstanceManager.DedicatedConfig;
if (config is null)
throw new HttpException(HttpStatusCode.ServiceUnavailable);
if (config.Worlds.FirstOrDefault(b => b.FolderName.ToGuid() == id) is not { } world)
throw HttpException.NotFound($"World not found by given id {id}", id);
return new(world.FolderName, world.WorldSizeKB);
}
[Route(HttpVerbs.Post, $"{RootPath}/{{id}}/select")]
public void Select(Guid id)
{
var config = Statics.InstanceManager.DedicatedConfig;
if (config is null)
throw new HttpException(HttpStatusCode.ServiceUnavailable);
if (config.Worlds.FirstOrDefault(b => b.FolderName.ToGuid() == id) is not { } world)
throw HttpException.NotFound($"World not found by given id {id}", id);
config.Model.IgnoreLastSession = true;
config.SelectedWorld = world;
config.Save();
}
}

View File

@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<PropertyChanged />
</Weavers>

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="PropertyChanged" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="InjectOnPropertyNameChanged" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to control if the On_PropertyName_Changed feature is enabled.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="TriggerDependentProperties" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to control if the Dependent properties feature is enabled.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="EnableIsChangedProperty" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to control if the IsChanged property feature is enabled.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="EventInvokerNames" type="xs:string">
<xs:annotation>
<xs:documentation>Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="CheckForEquality" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="CheckForEqualityUsingBaseEquals" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to control if equality checks should use the Equals method resolved from the base class.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UseStaticEqualsFromBase" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to control if equality checks should use the static Equals method resolved from the base class.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="SuppressWarnings" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to turn off build warnings from this weaver.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="SuppressOnPropertyNameChangedWarning" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to turn off build warnings about mismatched On_PropertyName_Changed methods.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@@ -0,0 +1,95 @@
using System.Security.Cryptography;
using System.Text.Json;
using EmbedIO;
using EmbedIO.BearerToken;
using EmbedIO.WebApi;
using Microsoft.IdentityModel.Tokens;
using NLog;
using Torch.API;
using Torch.API.Managers;
using Torch.Managers;
using Torch.Server.Managers;
using TorchRemote.Plugin.Controllers;
using TorchRemote.Plugin.Modules;
using TorchRemote.Plugin.Utils;
using VRage.Game.ModAPI;
namespace TorchRemote.Plugin.Managers;
public class ApiServerManager : Manager
{
private static readonly ILogger Log = LogManager.GetCurrentClassLogger();
private readonly Config _config;
private readonly IWebServer _server;
[Dependency]
private readonly SettingManager _settingManager = null!;
[Dependency]
private readonly InstanceManager _instanceManager = null!;
public ApiServerManager(ITorchBase torchInstance, Config config) : base(torchInstance)
{
_config = config;
if (string.IsNullOrEmpty(_config.SecurityKey))
_config.SecurityKey = CreateSecurityKey();
var apiModule = new WebApiModule("/api/v1", async (context, data) =>
{
try
{
context.Response.ContentType = "application/json";
using var stream = context.OpenResponseStream();
await JsonSerializer.SerializeAsync(stream, data, Statics.SerializerOptions);
}
catch (Exception e)
{
Log.Error(e);
throw HttpException.InternalServerError(e.Message, e.Message);
}
});
var chatModule = new ChatModule("/api/live/chat", true);
Statics.ChatModule = chatModule;
_server = new WebServer(o => o
.WithUrlPrefix(_config.Listener.UrlPrefix)
.WithMicrosoftHttpListener())
.WithLocalSessionManager()
.WithModule(apiModule
.WithController<ServerController>()
.WithController<SettingsController>()
.WithController<WorldsController>()
.WithController<ChatController>())
.WithModule(new LogsModule("/api/live/logs", true))
.WithModule(chatModule)
.WithBearerToken("/api", new SymmetricSecurityKey(Convert.FromBase64String(_config.SecurityKey)), new BasicAuthorizationServerProvider());
}
public override void Attach()
{
base.Attach();
Starter();
Log.Info("Listening on {0}", _config.Listener.UrlPrefix);
//_instanceManager.InstanceLoaded += model => _settingManager.RegisterSetting(model.Model, typeof(IMyConfigDedicated), false);
}
public override void Detach()
{
base.Detach();
_server.Dispose();
}
private async void Starter()
{
await _server.RunAsync();
}
private static string CreateSecurityKey()
{
var aes = Aes.Create();
aes.GenerateIV();
aes.GenerateKey();
return Convert.ToBase64String(aes.Key);
}
}

View File

@@ -0,0 +1,28 @@
using Sandbox.Engine.Multiplayer;
using Torch.API;
using Torch.API.Managers;
using Torch.Managers;
using TorchRemote.Models.Responses;
using TorchRemote.Models.Shared;
using TorchRemote.Plugin.Utils;
namespace TorchRemote.Plugin.Managers;
public class ChatMonitorManager : Manager
{
[Dependency]
private readonly IChatManagerServer _chatManager = null!;
public ChatMonitorManager(ITorchBase torchInstance) : base(torchInstance)
{
}
public override void Attach()
{
base.Attach();
_chatManager.MessageRecieved += ChatManagerOnMessageReceived;
}
private void ChatManagerOnMessageReceived(TorchChatMessage msg, ref bool consumed)
{
Statics.ChatModule.SendChatResponse(new ChatMessageResponse(msg.Author ?? (msg.AuthorSteamId is null ? Torch.Config.ChatName : MyMultiplayer.Static.GetMemberName(msg.AuthorSteamId.Value)),
msg.AuthorSteamId, (ChatChannel)msg.Channel, msg.Message));
}
}

View File

@@ -0,0 +1,77 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Text.Json.Serialization;
using System.Xml.Serialization;
using NLog;
using Torch.API;
using Torch.Managers;
using Torch.Views;
using TorchRemote.Models.Responses;
using TorchRemote.Plugin.Utils;
using VRage;
namespace TorchRemote.Plugin.Managers;
public class SettingManager : Manager
{
private static readonly ILogger Log = LogManager.GetCurrentClassLogger();
public SettingManager(ITorchBase torchInstance) : base(torchInstance)
{
}
public Guid RegisterSetting(object value, Type type, bool includeOnlyDisplay = true)
{
var properties = type.IsInterface ? type.GetProperties() : type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var settingProperties = properties
.Where(b => !b.HasAttribute<XmlIgnoreAttribute>() &&
!b.HasAttribute<JsonIgnoreAttribute>() &&
(!includeOnlyDisplay ||
b.HasAttribute<DisplayAttribute>()))
.Select(property => new SettingProperty(property.Name,
GetTypeId(property.PropertyType, property.GetValue(value), includeOnlyDisplay),
property.PropertyType, property.GetMethod, property.SetMethod,
property.GetCustomAttribute<DisplayAttribute>() is { } attr ?
new(attr.Name, attr.Description, attr.GroupName, attr.Order, attr.ReadOnly, attr.Enabled) :
null))
.ToArray();
var setting = new Setting(type.Name, type, settingProperties, value);
var id = (type.FullName! + value.GetHashCode()).ToGuid();
Settings.Add(id, setting);
Log.Debug("Registered type {0} with id {1}", type, id);
return id;
}
private Guid GetTypeId(Type type, object value, bool includeOnlyDisplay)
{
if (type == typeof(int) || type == typeof(uint))
return SettingPropertyTypeEnum.Integer;
if (type == typeof(bool))
return SettingPropertyTypeEnum.Boolean;
if (type == typeof(short) ||
type == typeof(ushort) ||
type == typeof(byte) ||
type == typeof(ulong) ||
type == typeof(long) ||
type == typeof(float) ||
type == typeof(double) ||
type == typeof(MyFixedPoint))
return SettingPropertyTypeEnum.Number;
if (type == typeof(string))
return SettingPropertyTypeEnum.String;
if (type == typeof(DateTime))
return SettingPropertyTypeEnum.DateTime;
if (type == typeof(TimeSpan))
return SettingPropertyTypeEnum.TimeSpan;
if (type == typeof(System.Drawing.Color) || type == typeof(VRageMath.Color))
return SettingPropertyTypeEnum.Color;
return RegisterSetting(value, type, includeOnlyDisplay);
}
public IDictionary<Guid, Setting> Settings { get; } = new ConcurrentDictionary<Guid, Setting>();
}
public record Setting(string Name, Type Type, IEnumerable<SettingProperty> Properties, object? Value = null);
public record SettingProperty(string Name, Guid TypeId, Type Type, MethodInfo Getter, MethodInfo? Setter, SettingPropertyDisplayInfo? DisplayInfo);
public record SettingPropertyDisplayInfo(string? Name, string? Description, string? GroupName, int? Order, bool? IsReadOnly, bool? IsEnabled);

View File

@@ -0,0 +1,28 @@
using System.Net.WebSockets;
using System.Text.Json;
using EmbedIO.WebSockets;
using TorchRemote.Models.Responses;
using TorchRemote.Plugin.Utils;
namespace TorchRemote.Plugin.Modules;
public class ChatModule : WebSocketModule
{
public ChatModule(string urlPath, bool enableConnectionWatchdog) : base(urlPath, enableConnectionWatchdog)
{
}
public async void SendChatResponse(ChatResponseBase response)
{
if (ActiveContexts.Count == 0)
return;
var buffer = JsonSerializer.SerializeToUtf8Bytes(response, Statics.SerializerOptions);
await Task.WhenAll(ActiveContexts
.Where(b => b.WebSocket.State is WebSocketState.Open)
.Select(context => context.WebSocket.SendAsync(buffer, true)));
}
protected override async Task OnMessageReceivedAsync(IWebSocketContext context, byte[] buffer, IWebSocketReceiveResult result)
{
}
}

View File

@@ -0,0 +1,85 @@
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using EmbedIO.WebSockets;
using NLog;
using NLog.Targets;
using NLog.Targets.Wrappers;
using Torch.Server;
using TorchRemote.Models.Responses;
using TorchRemote.Plugin.Utils;
namespace TorchRemote.Plugin.Modules;
public class LogsModule : WebSocketModule
{
public LogsModule(string urlPath, bool enableConnectionWatchdog) : base(urlPath, enableConnectionWatchdog)
{
ConfigureLogging();
}
protected override async Task OnMessageReceivedAsync(IWebSocketContext context, byte[] buffer, IWebSocketReceiveResult result)
{
}
public async void OnLogMessageReceived(DateTime time, LogLevel level, string logger, string message)
{
if (ActiveContexts.Count == 0)
return;
var response = new LogLineResponse(time, (LogLineLevel)level.Ordinal, logger, message);
var buffer = JsonSerializer.SerializeToUtf8Bytes(response, Statics.SerializerOptions);
await Task.WhenAll(ActiveContexts
.Where(b => b.WebSocket.State is WebSocketState.Open)
.Select(context => context.WebSocket.SendAsync(buffer, true)));
}
private void ConfigureLogging()
{
var cfg = LogManager.Configuration;
var flowDocumentTarget = cfg.FindTargetByName("wpf");
if (flowDocumentTarget is null or SplitGroupTarget)
return;
flowDocumentTarget.Name = "wpf-old";
var target = new SplitGroupTarget
{
Name = "wpf",
Targets =
{
flowDocumentTarget,
new StupidTarget(this)
}
};
cfg.RemoveTarget("wpf");
cfg.AddTarget(target);
foreach (var rule in cfg.LoggingRules)
{
if (rule.Targets.Remove(flowDocumentTarget))
rule.Targets.Add(target);
}
LogManager.Configuration = cfg;
LogManager.GetCurrentClassLogger().Info("Reconfigured logging");
}
private class StupidTarget : Target
{
private readonly LogsModule _module;
public StupidTarget(LogsModule module)
{
_module = module;
}
protected override void Write(LogEventInfo logEvent)
{
var message = logEvent.FormattedMessage;
if (logEvent.Exception is not null)
message += $"\n{logEvent.Exception}";
_module.OnLogMessageReceived(logEvent.TimeStamp, logEvent.Level, logEvent.LoggerName, message);
}
}
}

View File

@@ -0,0 +1,33 @@
using System.IO;
using System.Windows.Controls;
using Torch;
using Torch.API;
using Torch.API.Managers;
using Torch.API.Plugins;
using Torch.API.Session;
using Torch.Views;
using TorchRemote.Plugin.Managers;
namespace TorchRemote.Plugin;
public class Plugin : TorchPluginBase, IWpfPlugin
{
private Persistent<Config> _config = null!;
public override void Init(ITorchBase torch)
{
base.Init(torch);
_config = Persistent<Config>.Load(Path.Combine(StoragePath, "TorchRemote.cfg"));
Torch.Managers.AddManager(new ApiServerManager(Torch, _config.Data));
Torch.Managers.AddManager(new SettingManager(Torch));
Torch.Managers.GetManager<ITorchSessionManager>()
.AddFactory(s => new ChatMonitorManager(s.Torch));
}
public UserControl GetControl() => new PropertyGrid
{
Margin = new(3),
DataContext = _config.Data
};
}

View File

@@ -0,0 +1,99 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>10</LangVersion>
<UseWpf>true</UseWpf>
<TorchDir>$(SolutionDir)TorchBinaries\</TorchDir>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup>
<Reference Include="NLog">
<HintPath>$(TorchDir)NLog.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Sandbox.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\Sandbox.Common.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Sandbox.Game, Version=0.1.1.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\Sandbox.Game.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Sandbox.Graphics, Version=0.1.1.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\Sandbox.Graphics.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System.Memory">
<HintPath>$(TorchDir)DedicatedServer64\System.Memory.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System.Net.Http" />
<Reference Include="Torch">
<HintPath>$(TorchDir)Torch.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Torch.API">
<HintPath>$(TorchDir)Torch.API.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Torch.Server">
<HintPath>$(TorchDir)Torch.Server.exe</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Game, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Game.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Input, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Input.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Library, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Library.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Math, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Math.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Network, Version=1.0.53.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Network.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="EmbedIO" Version="3.4.3" />
<PackageReference Include="EmbedIO.BearerToken" Version="3.4.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" ExcludeAssets="all" PrivateAssets="all" />
<PackageReference Include="PropertyChanged.Fody" Version="4.0.0" PrivateAssets="all" />
<PackageReference Include="System.Text.Json" Version="7.0.0-preview.6.22324.4" />
</ItemGroup>
<ItemGroup>
<None Update="manifest.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TorchRemote.Models\TorchRemote.Models.csproj" />
</ItemGroup>
<Target Name="CopyToTorch" AfterTargets="Build">
<ZipDirectory DestinationFile="$(TorchDir)Plugins\$(AssemblyName).zip" SourceDirectory="$(TargetDir)" Overwrite="true" />
</Target>
</Project>

View File

@@ -0,0 +1,29 @@
using System.Security.Cryptography;
using System.Text;
using Sandbox.Engine.Multiplayer;
using Sandbox.Engine.Networking;
using Sandbox.Game;
using Sandbox.Game.World;
using TorchRemote.Models.Shared;
namespace TorchRemote.Plugin.Utils;
public static class Extensions
{
public static void ApplyDynamically(this ServerSettings settings)
{
MyGameService.GameServer.SetServerName(settings.ServerName);
MyMultiplayer.Static.HostName = settings.ServerName;
MyMultiplayer.Static.WorldName = settings.MapName;
MySession.Static.Name = settings.MapName;
MyMultiplayer.Static.MemberLimit = settings.MemberLimit;
MyCachedServerItem.SendSettingsToSteam();
}
public static Guid ToGuid(this string s)
{
using var md5 = MD5.Create();
return new(md5.ComputeHash(Encoding.UTF8.GetBytes(s)));
}
}

View File

@@ -0,0 +1,34 @@
using System.Text.Json;
using EmbedIO;
using EmbedIO.WebApi;
namespace TorchRemote.Plugin.Utils;
/// <summary>
/// <para>Specifies that a parameter of a controller method will receive
/// an object obtained by deserializing the request body as JSON.</para>
/// <para>The received object will be <see langword="null"/>
/// only if the deserialized object is <c>null</c>.</para>
/// <para>If the request body is not valid JSON,
/// or if it cannot be deserialized to the type of the parameter,
/// a <c>400 Bad Request</c> response will be sent to the client.</para>
/// <para>This class cannot be inherited.</para>
/// </summary>
/// <seealso cref="Attribute" />
/// <seealso cref="IRequestDataAttribute{TController}" />
[AttributeUsage(AttributeTargets.Parameter)]
public class JsonDataAttribute : Attribute, IRequestDataAttribute<WebApiController>
{
/// <inheritdoc />
public async Task<object?> GetRequestDataAsync(WebApiController controller, Type type, string parameterName)
{
try
{
using var stream = controller.HttpContext.OpenRequestStream();
return await JsonSerializer.DeserializeAsync(stream, type, Statics.SerializerOptions);
}
catch (JsonException)
{
throw HttpException.BadRequest($"Expected request body to be deserializable to {type.FullName}.");
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json;
using Torch;
using Torch.API;
using Torch.API.Managers;
using Torch.Commands;
using Torch.Server;
using Torch.Server.Managers;
using TorchRemote.Plugin.Managers;
using TorchRemote.Plugin.Modules;
namespace TorchRemote.Plugin.Utils;
internal static class Statics
{
#pragma warning disable CS0618
public static TorchServer Torch => (TorchServer)TorchBase.Instance;
#pragma warning restore CS0618
public static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
public static SettingManager SettingManager => Torch.Managers.GetManager<SettingManager>();
public static InstanceManager InstanceManager => Torch.Managers.GetManager<InstanceManager>();
public static CommandManager? CommandManager => Torch.CurrentSession?.Managers.GetManager<CommandManager>();
public static ChatModule ChatModule = null!;
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0"?>
<PluginManifest xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>TorchRemote.Plugin</Name>
<Guid>284017F3-9682-4841-A544-EB04DB8CB9BA</Guid>
<Version>v1.0.0</Version>
</PluginManifest>

View File

@@ -0,0 +1,20 @@
:: This script creates a symlink to the game binaries to account for different installation directories on different systems.
@echo off
set /p path="Please enter the folder location of your Torch.Server.exe: "
cd %~dp0
rmdir TorchBinaries > nul 2>&1
mklink /J ..\TorchBinaries "%path%"
if errorlevel 1 goto Error
echo Done!
echo You can now open the plugin without issue.
goto EndFinal
:Error
echo An error occured creating the symlink.
goto EndFinal
:EndFinal
pause