first
This commit is contained in:
17
TorchRemote.Plugin/Config.cs
Normal file
17
TorchRemote.Plugin/Config.cs
Normal 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/";
|
||||
}
|
118
TorchRemote.Plugin/Controllers/ChatController.cs
Normal file
118
TorchRemote.Plugin/Controllers/ChatController.cs
Normal 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));
|
||||
}
|
||||
}
|
76
TorchRemote.Plugin/Controllers/ServerController.cs
Normal file
76
TorchRemote.Plugin/Controllers/ServerController.cs
Normal 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);
|
||||
}
|
||||
}
|
24
TorchRemote.Plugin/Controllers/SettingsController.cs
Normal file
24
TorchRemote.Plugin/Controllers/SettingsController.cs
Normal 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());
|
||||
}
|
||||
}
|
62
TorchRemote.Plugin/Controllers/WorldsController.cs
Normal file
62
TorchRemote.Plugin/Controllers/WorldsController.cs
Normal 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();
|
||||
}
|
||||
}
|
3
TorchRemote.Plugin/FodyWeavers.xml
Normal file
3
TorchRemote.Plugin/FodyWeavers.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<PropertyChanged />
|
||||
</Weavers>
|
74
TorchRemote.Plugin/FodyWeavers.xsd
Normal file
74
TorchRemote.Plugin/FodyWeavers.xsd
Normal 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>
|
95
TorchRemote.Plugin/Managers/ApiServerManager.cs
Normal file
95
TorchRemote.Plugin/Managers/ApiServerManager.cs
Normal 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);
|
||||
}
|
||||
}
|
28
TorchRemote.Plugin/Managers/ChatMonitorManager.cs
Normal file
28
TorchRemote.Plugin/Managers/ChatMonitorManager.cs
Normal 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));
|
||||
}
|
||||
}
|
77
TorchRemote.Plugin/Managers/SettingManager.cs
Normal file
77
TorchRemote.Plugin/Managers/SettingManager.cs
Normal 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);
|
28
TorchRemote.Plugin/Modules/ChatModule.cs
Normal file
28
TorchRemote.Plugin/Modules/ChatModule.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
85
TorchRemote.Plugin/Modules/LogsModule.cs
Normal file
85
TorchRemote.Plugin/Modules/LogsModule.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
33
TorchRemote.Plugin/Plugin.cs
Normal file
33
TorchRemote.Plugin/Plugin.cs
Normal 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
|
||||
};
|
||||
}
|
99
TorchRemote.Plugin/TorchRemote.Plugin.csproj
Normal file
99
TorchRemote.Plugin/TorchRemote.Plugin.csproj
Normal 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>
|
29
TorchRemote.Plugin/Utils/Extensions.cs
Normal file
29
TorchRemote.Plugin/Utils/Extensions.cs
Normal 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)));
|
||||
}
|
||||
}
|
34
TorchRemote.Plugin/Utils/JsonDataAttribute.cs
Normal file
34
TorchRemote.Plugin/Utils/JsonDataAttribute.cs
Normal 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}.");
|
||||
}
|
||||
}
|
||||
}
|
24
TorchRemote.Plugin/Utils/Statics.cs
Normal file
24
TorchRemote.Plugin/Utils/Statics.cs
Normal 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!;
|
||||
}
|
6
TorchRemote.Plugin/manifest.xml
Normal file
6
TorchRemote.Plugin/manifest.xml
Normal 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>
|
20
TorchRemote.Plugin/setup.bat
Normal file
20
TorchRemote.Plugin/setup.bat
Normal 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
|
Reference in New Issue
Block a user