Server scripting.

This commit is contained in:
Pieter-Jan Briers
2020-04-30 00:06:59 +02:00
parent 48699837b0
commit 400dcb06fc
34 changed files with 1207 additions and 375 deletions

View File

@@ -23,7 +23,7 @@
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DefineConstants>$(DefineConstants);EXCEPTION_TOLERANCE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(EnableScripting)' == 'True'">
<DefineConstants>$(DefineConstants);SCRIPTING</DefineConstants>
<PropertyGroup Condition="'$(EnableClientScripting)' == 'True'">
<DefineConstants>$(DefineConstants);CLIENT_SCRIPTING</DefineConstants>
</PropertyGroup>
</Project>

View File

@@ -27,8 +27,8 @@
<Python>python3</Python>
<Python Condition="'$(ActualOS)' == 'Windows'">py -3</Python>
<TargetFramework>netcoreapp3.0</TargetFramework>
<EnableScripting>True</EnableScripting>
<!-- Scripting is disabled on full release builds for security and size reasons. -->
<EnableScripting Condition="'$(FullRelease)' == 'True'">False</EnableScripting>
<EnableClientScripting>True</EnableClientScripting>
<!-- Client scripting is disabled on full release builds for security and size reasons. -->
<EnableClientScripting Condition="'$(FullRelease)' == 'True'">False</EnableClientScripting>
</PropertyGroup>
</Project>

View File

@@ -121,6 +121,7 @@ namespace Robust.Client
IoCManager.Register<IViewVariablesManagerInternal, ViewVariablesManager>();
IoCManager.Register<ISignalHandler, ClientSignalHandler>();
IoCManager.Register<IClientConGroupController, ClientConGroupController>();
IoCManager.Register<IScriptClient, ScriptClient>();
}
}
}

View File

@@ -48,6 +48,13 @@ namespace Robust.Client.Console
return _clientConGroup.CanAdminPlace;
}
public bool CanScript()
{
if (_clientConGroup == null)
return false;
return _clientConGroup.CanScript;
}
/// <summary>
/// Update client console group data with message from the server.
/// </summary>

View File

@@ -1,8 +1,11 @@
#if SCRIPTING
using Robust.Client.Interfaces.Console;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
namespace Robust.Client.Console.Commands
{
#if CLIENT_SCRIPTING
internal sealed class ScriptConsoleCommand : IConsoleCommand
{
public string Command => "csi";
@@ -11,10 +14,31 @@ namespace Robust.Client.Console.Commands
public bool Execute(IDebugConsole console, params string[] args)
{
new ScriptConsole().OpenCenteredMinSize();
new ScriptConsoleClient().OpenCenteredMinSize();
return false;
}
}
#endif
internal sealed class ServerScriptConsoleCommand : IConsoleCommand
{
public string Command => "scsi";
public string Description => "Opens a C# interactive console on the server.";
public string Help => "scsi";
public bool Execute(IDebugConsole console, params string[] args)
{
var mgr = IoCManager.Resolve<IScriptClient>();
if (!mgr.CanScript)
{
console.AddLine(Loc.GetString("You do not have server side scripting permission."), Color.Red);
return false;
}
mgr.StartSession();
return false;
}
}
}
#endif

View File

@@ -13,6 +13,7 @@ namespace Robust.Client.Console
bool CanCommand(string cmdName);
bool CanViewVar();
bool CanAdminPlace();
bool CanScript();
event Action ConGroupUpdated;
}
}

View File

@@ -0,0 +1,13 @@
namespace Robust.Client.Console
{
/// <summary>
/// Client manager for server side scripting.
/// </summary>
public interface IScriptClient
{
void Initialize();
bool CanScript { get; }
void StartSession();
}
}

View File

@@ -0,0 +1,100 @@
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Network.Messages;
using Robust.Shared.Utility;
namespace Robust.Client.Console
{
public partial class ScriptClient
{
private sealed class ScriptConsoleServer : ScriptConsole
{
private readonly ScriptClient _client;
private readonly int _session;
private int _linesEntered;
private string _lastEnteredText;
public ScriptConsoleServer(ScriptClient client, int session)
{
_client = client;
_session = session;
Title = Loc.GetString("Robust C# Interactive (SERVER)");
OutputPanel.AddText(Loc.GetString(@"Robust C# interactive console (SERVER)."));
OutputPanel.AddText(">");
}
protected override void Run()
{
if (RunButton.Disabled || string.IsNullOrWhiteSpace(InputBar.Text))
{
return;
}
RunButton.Disabled = true;
var msg = _client._netManager.CreateNetMessage<MsgScriptEval>();
msg.ScriptSession = _session;
msg.Code = _lastEnteredText = InputBar.Text;
_client._netManager.ClientSendMessage(msg);
InputBar.Clear();
}
public override void Close()
{
base.Close();
_client.ConsoleClosed(_session);
}
public void ReceiveResponse(MsgScriptResponse response)
{
RunButton.Disabled = false;
// Remove > or . at the end of the output panel.
OutputPanel.RemoveEntry(^1);
_linesEntered += 1;
if (!response.WasComplete)
{
if (_linesEntered == 1)
{
OutputPanel.AddText($"> {_lastEnteredText}");
}
else
{
OutputPanel.AddText($". {_lastEnteredText}");
}
OutputPanel.AddText(".");
return;
}
// Remove echo of partial submission from the output panel.
for (var i = 1; i < _linesEntered; i++)
{
OutputPanel.RemoveEntry(^1);
}
_linesEntered = 0;
// Echo entered script.
var echoMessage = new FormattedMessage();
echoMessage.PushColor(Color.FromHex("#D4D4D4"));
echoMessage.AddText("> ");
echoMessage.AddMessage(response.Echo);
OutputPanel.AddMessage(echoMessage);
OutputPanel.AddMessage(response.Response);
OutputPanel.AddText(">");
}
}
}
}

View File

@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Network.Messages;
namespace Robust.Client.Console
{
public partial class ScriptClient : IScriptClient
{
[Dependency] private readonly IClientConGroupController _conGroupController = default;
[Dependency] private readonly IClientNetManager _netManager = default;
private readonly Dictionary<int, ScriptConsoleServer> _activeConsoles = new Dictionary<int,ScriptConsoleServer>();
private int _nextSessionId = 1;
public void Initialize()
{
_netManager.RegisterNetMessage<MsgScriptStop>(MsgScriptStop.NAME);
_netManager.RegisterNetMessage<MsgScriptEval>(MsgScriptEval.NAME);
_netManager.RegisterNetMessage<MsgScriptStart>(MsgScriptStart.NAME);
_netManager.RegisterNetMessage<MsgScriptResponse>(MsgScriptResponse.NAME, ReceiveScriptResponse);
_netManager.RegisterNetMessage<MsgScriptStartAck>(MsgScriptStartAck.NAME, ReceiveScriptStartAckResponse);
}
private void ReceiveScriptStartAckResponse(MsgScriptStartAck message)
{
var session = message.ScriptSession;
var console = new ScriptConsoleServer(this, session);
_activeConsoles.Add(session, console);
console.Open();
}
private void ReceiveScriptResponse(MsgScriptResponse message)
{
if (!_activeConsoles.TryGetValue(message.ScriptSession, out var console))
{
return;
}
console.ReceiveResponse(message);
}
public bool CanScript => _conGroupController.CanScript();
public void StartSession()
{
if (!CanScript)
{
throw new InvalidOperationException("We do not have scripting permission.");
}
var msg = _netManager.CreateNetMessage<MsgScriptStart>();
msg.ScriptSession = _nextSessionId++;
_netManager.ClientSendMessage(msg);
}
private void ConsoleClosed(int session)
{
_activeConsoles.Remove(session);
var msg = _netManager.CreateNetMessage<MsgScriptStop>();
msg.ScriptSession = session;
_netManager.ClientSendMessage(msg);
}
}
}

View File

@@ -1,366 +0,0 @@
#if SCRIPTING
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Lidgren.Network;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.CSharp.Scripting.Hosting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.Text;
using Robust.Client.Graphics.Drawing;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.ViewVariables;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
#nullable enable
namespace Robust.Client.Console
{
internal sealed class ScriptConsole : SS14Window
{
#pragma warning disable 649
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
#pragma warning restore 649
private static readonly CSharpParseOptions _parseOptions =
new CSharpParseOptions(kind: SourceCodeKind.Script, languageVersion: LanguageVersion.Latest);
private static readonly Func<Script, bool> _hasReturnValue;
private static readonly string[] _defaultImports =
{
"System",
"System.Linq",
"System.Collections.Generic",
"Robust.Shared.IoC",
"Robust.Shared.Maths",
"Robust.Shared.GameObjects",
"Robust.Shared.Interfaces.GameObjects"
};
private readonly OutputPanel _outputPanel;
private readonly LineEdit _inputBar;
private readonly StringBuilder _inputBuffer = new StringBuilder();
private int _linesEntered;
// Necessary for syntax highlighting.
private readonly Workspace _highlightWorkspace = new AdhocWorkspace();
private readonly ScriptGlobals _globals;
private ScriptState? _state;
static ScriptConsole()
{
// This is the (internal) method that csi seems to use.
// Because it is internal and I can't find an alternative, reflection it is.
// TODO: Find a way that doesn't need me to reflect into Roslyn internals.
var method = typeof(Script).GetMethod("HasReturnValue", BindingFlags.Instance | BindingFlags.NonPublic);
if (method == null)
{
// Fallback path in case they remove that.
// The method literally has a // TODO: remove
_hasReturnValue = _ => true;
return;
}
_hasReturnValue = (Func<Script, bool>) Delegate.CreateDelegate(typeof(Func<Script, bool>), method);
// Run this async so that Roslyn can "warm up" in another thread while you're typing in your first line,
// so the hang when you hit enter is less bad.
Task.Run(async () =>
{
const string code =
"var x = 5 + 5; var y = (object) \"foobar\"; void Foo(object a) { } Foo(y); Foo(x)";
var script = await CSharpScript.RunAsync(code);
var msg = new FormattedMessage();
// Even run the syntax highlighter!
AddWithSyntaxHighlighting(script.Script, msg, code, new AdhocWorkspace());
});
}
public ScriptConsole()
{
_globals = new ScriptGlobals(this);
IoCManager.InjectDependencies(this);
Title = Loc.GetString("Robust C# Interactive");
Contents.AddChild(new VBoxContainer
{
Children =
{
new PanelContainer
{
PanelOverride = new StyleBoxFlat
{
BackgroundColor = Color.FromHex("#1E1E1E"),
ContentMarginLeftOverride = 4
},
Children =
{
(_outputPanel = new OutputPanel
{
SizeFlagsVertical = SizeFlags.FillExpand,
})
},
SizeFlagsVertical = SizeFlags.FillExpand
},
(_inputBar = new HistoryLineEdit {PlaceHolder = Loc.GetString("Your C# code here.")})
}
});
_inputBar.OnTextEntered += InputBarOnOnTextEntered;
CustomMinimumSize = (550, 300);
_outputPanel.AddText(Loc.GetString(@"Robust C# interactive console."));
_outputPanel.AddText(">");
}
private async void InputBarOnOnTextEntered(LineEdit.LineEditEventArgs obj)
{
var code = _inputBar.Text;
_inputBar.Clear();
_inputBuffer.AppendLine(code);
_linesEntered += 1;
// Remove > or . at the end of the output panel.
_outputPanel.RemoveEntry(^1);
var tree = SyntaxFactory.ParseSyntaxTree(SourceText.From(_inputBuffer.ToString()), _parseOptions);
if (!SyntaxFactory.IsCompleteSubmission(tree))
{
if (_linesEntered == 1)
{
_outputPanel.AddText($"> {code}");
}
else
{
_outputPanel.AddText($". {code}");
}
_outputPanel.AddText(".");
return;
}
code = _inputBuffer.ToString().Trim();
// Remove echo of partial submission from the output panel.
for (var i = 1; i < _linesEntered; i++)
{
_outputPanel.RemoveEntry(^1);
}
_inputBuffer.Clear();
_linesEntered = 0;
Script newScript;
if (_state != null)
{
newScript = _state.Script.ContinueWith(code);
}
else
{
var options = GetScriptOptions();
newScript = CSharpScript.Create(code, options, typeof(IScriptGlobals));
}
// Compile ahead of time so that we can do syntax highlighting correctly for the echo.
newScript.Compile();
// Echo entered script.
var echoMessage = new FormattedMessage();
echoMessage.PushColor(Color.FromHex("#D4D4D4"));
echoMessage.AddText("> ");
AddWithSyntaxHighlighting(newScript, echoMessage, code, _highlightWorkspace);
_outputPanel.AddMessage(echoMessage);
try
{
if (_state != null)
{
_state = await newScript.RunFromAsync(_state, _ => true);
}
else
{
_state = await newScript.RunAsync(_globals);
}
}
catch (CompilationErrorException e)
{
var msg = new FormattedMessage();
msg.PushColor(Color.Crimson);
foreach (var diagnostic in e.Diagnostics)
{
msg.AddText(diagnostic.ToString());
msg.AddText("\n");
}
_outputPanel.AddMessage(msg);
_outputPanel.AddText(">");
return;
}
if (_state.Exception != null)
{
var msg = new FormattedMessage();
msg.PushColor(Color.Crimson);
msg.AddText(CSharpObjectFormatter.Instance.FormatException(_state.Exception));
_outputPanel.AddMessage(msg);
}
else if (_hasReturnValue(newScript))
{
var msg = new FormattedMessage();
msg.AddText(CSharpObjectFormatter.Instance.FormatObject(_state.ReturnValue));
_outputPanel.AddMessage(msg);
}
_outputPanel.AddText(">");
}
protected override void Opened()
{
_inputBar.GrabKeyboardFocus();
}
private ScriptOptions GetScriptOptions()
{
return ScriptOptions.Default
.AddImports(_defaultImports)
.AddReferences(GetDefaultReferences());
}
private static void AddWithSyntaxHighlighting(Script script, FormattedMessage msg, string code,
Workspace workspace)
{
var compilation = script.GetCompilation();
var model = compilation.GetSemanticModel(compilation.SyntaxTrees.First());
var classified = Classifier.GetClassifiedSpans(model, TextSpan.FromBounds(0, code.Length), workspace);
var current = 0;
foreach (var span in classified)
{
var start = span.TextSpan.Start;
if (start > current)
{
msg.AddText(code[current..start]);
}
if (current > start)
{
continue;
}
// TODO: there are probably issues with multiple classifications overlapping the same text here.
// Too lazy to fix.
var src = code[span.TextSpan.Start..span.TextSpan.End];
var color = span.ClassificationType switch
{
ClassificationTypeNames.Comment => Color.FromHex("#57A64A"),
ClassificationTypeNames.NumericLiteral => Color.FromHex("#b5cea8"),
ClassificationTypeNames.StringLiteral => Color.FromHex("#D69D85"),
ClassificationTypeNames.Keyword => Color.FromHex("#569CD6"),
ClassificationTypeNames.StaticSymbol => Color.FromHex("#4EC9B0"),
ClassificationTypeNames.ClassName => Color.FromHex("#4EC9B0"),
ClassificationTypeNames.StructName => Color.FromHex("#4EC9B0"),
ClassificationTypeNames.InterfaceName => Color.FromHex("#B8D7A3"),
ClassificationTypeNames.EnumName => Color.FromHex("#B8D7A3"),
_ => Color.FromHex("#D4D4D4")
};
msg.PushColor(color);
msg.AddText(src);
msg.Pop();
current = span.TextSpan.End;
}
msg.AddText(code[current..]);
}
private IEnumerable<Assembly> GetDefaultReferences()
{
var list = new List<Assembly>();
list.AddRange(_reflectionManager.Assemblies);
list.Add(typeof(YamlDocument).Assembly); // YamlDotNet
list.Add(typeof(NetPeer).Assembly); // Lidgren
list.Add(typeof(Vector2).Assembly); // Robust.Shared.Maths
return list;
}
private sealed class ScriptGlobals : IScriptGlobals
{
private readonly ScriptConsole _owner;
[field: Dependency] public IEntityManager ent { get; } = default!;
[field: Dependency] public IComponentManager comp { get; } = default!;
[field: Dependency] public IViewVariablesManager vvm { get; } = default!;
public ScriptGlobals(ScriptConsole owner)
{
IoCManager.InjectDependencies(this);
_owner = owner;
}
public void vv(object a)
{
vvm.OpenVV(a);
}
public T res<T>()
{
return IoCManager.Resolve<T>();
}
public void write(object toString)
{
_owner._outputPanel.AddText(toString?.ToString() ?? "");
}
public void show(object obj)
{
write(CSharpObjectFormatter.Instance.FormatObject(obj));
}
}
}
[SuppressMessage("ReSharper", "InconsistentNaming")]
[PublicAPI]
public interface IScriptGlobals
{
public IEntityManager ent { get; }
public IComponentManager comp { get; }
public IViewVariablesManager vvm { get; }
public void vv(object a);
public T res<T>();
public void write(object toString);
public void show(object obj);
}
}
#endif

View File

@@ -0,0 +1,210 @@
#if CLIENT_SCRIPTING
using System.Diagnostics.CodeAnalysis;
using System.Text;
using JetBrains.Annotations;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.CSharp.Scripting.Hosting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.Text;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.ViewVariables;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Scripting;
using Robust.Shared.Utility;
#nullable enable
namespace Robust.Client.Console
{
internal sealed class ScriptConsoleClient : ScriptConsole
{
#pragma warning disable 649
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
#pragma warning restore 649
private readonly StringBuilder _inputBuffer = new StringBuilder();
private int _linesEntered;
// Necessary for syntax highlighting.
private readonly Workspace _highlightWorkspace = new AdhocWorkspace();
private readonly ScriptGlobals _globals;
private ScriptState? _state;
public ScriptConsoleClient()
{
Title = Loc.GetString("Robust C# Interactive (CLIENT)");
ScriptInstanceShared.InitDummy();
_globals = new ScriptGlobals(this);
IoCManager.InjectDependencies(this);
OutputPanel.AddText(Loc.GetString(@"Robust C# interactive console (CLIENT)."));
OutputPanel.AddText(">");
}
protected override async void Run()
{
var code = InputBar.Text;
InputBar.Clear();
// Remove > or . at the end of the output panel.
OutputPanel.RemoveEntry(^1);
_inputBuffer.AppendLine(code);
_linesEntered += 1;
var tree = SyntaxFactory.ParseSyntaxTree(SourceText.From(_inputBuffer.ToString()), ScriptInstanceShared.ParseOptions);
if (!SyntaxFactory.IsCompleteSubmission(tree))
{
if (_linesEntered == 1)
{
OutputPanel.AddText($"> {code}");
}
else
{
OutputPanel.AddText($". {code}");
}
OutputPanel.AddText(".");
return;
}
code = _inputBuffer.ToString().Trim();
// Remove echo of partial submission from the output panel.
for (var i = 1; i < _linesEntered; i++)
{
OutputPanel.RemoveEntry(^1);
}
_inputBuffer.Clear();
_linesEntered = 0;
Script newScript;
if (_state != null)
{
newScript = _state.Script.ContinueWith(code);
}
else
{
var options = ScriptInstanceShared.GetScriptOptions(_reflectionManager);
newScript = CSharpScript.Create(code, options, typeof(IScriptGlobals));
}
// Compile ahead of time so that we can do syntax highlighting correctly for the echo.
newScript.Compile();
// Echo entered script.
var echoMessage = new FormattedMessage();
echoMessage.PushColor(Color.FromHex("#D4D4D4"));
echoMessage.AddText("> ");
ScriptInstanceShared.AddWithSyntaxHighlighting(newScript, echoMessage, code, _highlightWorkspace);
OutputPanel.AddMessage(echoMessage);
try
{
if (_state != null)
{
_state = await newScript.RunFromAsync(_state, _ => true);
}
else
{
_state = await newScript.RunAsync(_globals);
}
}
catch (CompilationErrorException e)
{
var msg = new FormattedMessage();
msg.PushColor(Color.Crimson);
foreach (var diagnostic in e.Diagnostics)
{
msg.AddText(diagnostic.ToString());
msg.AddText("\n");
}
OutputPanel.AddMessage(msg);
OutputPanel.AddText(">");
return;
}
if (_state.Exception != null)
{
var msg = new FormattedMessage();
msg.PushColor(Color.Crimson);
msg.AddText(CSharpObjectFormatter.Instance.FormatException(_state.Exception));
OutputPanel.AddMessage(msg);
}
else if (ScriptInstanceShared.HasReturnValue(newScript))
{
var msg = new FormattedMessage();
msg.AddText(CSharpObjectFormatter.Instance.FormatObject(_state.ReturnValue));
OutputPanel.AddMessage(msg);
}
OutputPanel.AddText(">");
}
private sealed class ScriptGlobals : IScriptGlobals
{
private readonly ScriptConsoleClient _owner;
[field: Dependency] public IEntityManager ent { get; } = default!;
[field: Dependency] public IComponentManager comp { get; } = default!;
[field: Dependency] public IViewVariablesManager vvm { get; } = default!;
public ScriptGlobals(ScriptConsoleClient owner)
{
IoCManager.InjectDependencies(this);
_owner = owner;
}
public void vv(object a)
{
vvm.OpenVV(a);
}
public T res<T>()
{
return IoCManager.Resolve<T>();
}
public void write(object toString)
{
_owner.OutputPanel.AddText(toString?.ToString() ?? "");
}
public void show(object obj)
{
write(CSharpObjectFormatter.Instance.FormatObject(obj));
}
}
}
[SuppressMessage("ReSharper", "InconsistentNaming")]
[PublicAPI]
public interface IScriptGlobals
{
public IEntityManager ent { get; }
public IComponentManager comp { get; }
public IViewVariablesManager vvm { get; }
public void vv(object a);
public T res<T>();
public void write(object toString);
public void show(object obj);
}
}
#endif

View File

@@ -68,6 +68,7 @@ namespace Robust.Client
[Dependency] private readonly IModLoader _modLoader;
[Dependency] private readonly ISignalHandler _signalHandler;
[Dependency] private readonly IClientConGroupController _conGroupController;
[Dependency] private readonly IScriptClient _scriptClient;
#pragma warning restore 649
private CommandLineArgs _commandLineArgs;
@@ -174,6 +175,7 @@ namespace Robust.Client
_placementManager.Initialize();
_viewVariablesManager.Initialize();
_conGroupController.Initialize();
_scriptClient.Initialize();
_client.Initialize();
_discord.Initialize();

View File

@@ -29,10 +29,12 @@
<PackageReference Include="OpenTK" Version="3.1.0" />
<PackageReference Include="SpaceWizards.SharpFont" Version="1.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(EnableScripting)' == 'True'">
<ItemGroup Condition="'$(EnableClientScripting)' == 'True'">
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="3.5.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.5.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.5.0" />
<ProjectReference Include="..\Robust.Shared.Scripting\Robust.Shared.Scripting.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lidgren.Network\Lidgren.Network.csproj" />

View File

@@ -0,0 +1,63 @@
using Robust.Client.Graphics.Drawing;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface.CustomControls
{
internal abstract class ScriptConsole : SS14Window
{
protected OutputPanel OutputPanel { get; }
protected HistoryLineEdit InputBar { get; }
protected Button RunButton { get; }
protected ScriptConsole()
{
Contents.AddChild(new VBoxContainer
{
Children =
{
new PanelContainer
{
PanelOverride = new StyleBoxFlat
{
BackgroundColor = Color.FromHex("#1E1E1E"),
ContentMarginLeftOverride = 4
},
Children =
{
(OutputPanel = new OutputPanel
{
SizeFlagsVertical = SizeFlags.FillExpand,
})
},
SizeFlagsVertical = SizeFlags.FillExpand
},
new HBoxContainer
{
Children =
{
(InputBar = new HistoryLineEdit
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
PlaceHolder = Loc.GetString("Your C# code here.")
}),
(RunButton = new Button {Text = Loc.GetString("Run")})
}
},
}
});
InputBar.OnTextEntered += _ => Run();
RunButton.OnPressed += _ => Run();
CustomMinimumSize = (550, 300);
}
protected abstract void Run();
protected override void Opened()
{
InputBar.GrabKeyboardFocus();
}
}
}

View File

@@ -32,6 +32,7 @@ using Robust.Shared.Interfaces.Resources;
using Robust.Shared.Exceptions;
using Robust.Shared.Localization;
using Robust.Server.Interfaces.Debugging;
using Robust.Server.Scripting;
using Robust.Server.ServerStatus;
using Robust.Shared;
using Robust.Shared.Network.Messages;
@@ -61,6 +62,7 @@ namespace Robust.Server
[Dependency] private IRuntimeLog runtimeLog;
[Dependency] private readonly IModLoader _modLoader;
[Dependency] private readonly IWatchdogApi _watchdogApi;
[Dependency] private readonly IScriptHost _scriptHost;
#pragma warning restore 649
private CommandLineArgs _commandLineArgs;
@@ -245,6 +247,7 @@ namespace Robust.Server
IoCManager.Resolve<IConsoleShell>().Initialize();
IoCManager.Resolve<IConGroupController>().Initialize();
_entities.Startup();
_scriptHost.Initialize();
_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);

View File

@@ -149,5 +149,16 @@ namespace Robust.Server.Console
_logger.Error($"Unknown groupIndex: {groupIndex}");
return false;
}
public bool CanScript(ConGroupIndex groupIndex)
{
if (_groups.TryGetValue(groupIndex, out var group))
{
return group.CanScript;
}
_logger.Error($"Unknown groupIndex: {groupIndex}");
return false;
}
}
}

View File

@@ -88,6 +88,13 @@ namespace Robust.Server.Console
return _groups.CanAdminPlace(group);
}
public bool CanScript(IPlayerSession session)
{
var group = _sessions.GetSessionGroup(session);
return _groups.CanScript(group);
}
/// <summary>
/// Clears all session data.
/// </summary>

View File

@@ -10,6 +10,7 @@ namespace Robust.Server.Console
bool CanCommand(IPlayerSession session, string cmdName);
bool CanViewVar(IPlayerSession session);
bool CanAdminPlace(IPlayerSession session);
bool CanScript(IPlayerSession session);
void SetGroup(IPlayerSession session, ConGroupIndex newGroup);
}
}

View File

@@ -49,6 +49,8 @@ namespace Robust.Server.Interfaces.Player
IPlayerSession GetSessionByChannel(INetChannel channel);
bool TryGetSessionByChannel(INetChannel channel, out IPlayerSession session);
bool TryGetSessionById(NetSessionId sessionId, out IPlayerSession session);
/// <summary>

View File

@@ -21,6 +21,7 @@
<ItemGroup>
<ProjectReference Include="..\Lidgren.Network\Lidgren.Network.csproj" />
<ProjectReference Include="..\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\Robust.Shared.Scripting\Robust.Shared.Scripting.csproj" />
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,7 @@
namespace Robust.Server.Scripting
{
internal interface IScriptHost
{
public void Initialize();
}
}

View File

@@ -0,0 +1,282 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using JetBrains.Annotations;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.CSharp.Scripting.Hosting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.Text;
using Robust.Server.Console;
using Robust.Server.Interfaces.Player;
using Robust.Server.Player;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Network.Messages;
using Robust.Shared.Scripting;
using Robust.Shared.Utility;
#nullable enable
namespace Robust.Server.Scripting
{
internal sealed class ScriptHost : IScriptHost
{
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IConGroupController _conGroupController = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
readonly Dictionary<IPlayerSession, Dictionary<int, ScriptInstance>> _instances =
new Dictionary<IPlayerSession, Dictionary<int, ScriptInstance>>();
public void Initialize()
{
_netManager.RegisterNetMessage<MsgScriptStop>(MsgScriptStop.NAME, ReceiveScriptEnd);
_netManager.RegisterNetMessage<MsgScriptEval>(MsgScriptEval.NAME, ReceiveScriptEval);
_netManager.RegisterNetMessage<MsgScriptStart>(MsgScriptStart.NAME, ReceiveScriptStart);
_netManager.RegisterNetMessage<MsgScriptResponse>(MsgScriptResponse.NAME);
_netManager.RegisterNetMessage<MsgScriptStartAck>(MsgScriptStartAck.NAME);
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
}
private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
// GC it up.
_instances.Remove(e.Session);
}
private void ReceiveScriptEnd(MsgScriptStop message)
{
if (!_playerManager.TryGetSessionByChannel(message.MsgChannel, out var session))
{
return;
}
if (!_instances.TryGetValue(session, out var instances))
{
return;
}
instances.Remove(message.ScriptSession);
}
private void ReceiveScriptStart(MsgScriptStart message)
{
var reply = _netManager.CreateNetMessage<MsgScriptStartAck>();
reply.ScriptSession = message.ScriptSession;
reply.WasAccepted = false;
if (!_playerManager.TryGetSessionByChannel(message.MsgChannel, out var session))
{
_netManager.ServerSendMessage(reply, message.MsgChannel);
return;
}
if (!_conGroupController.CanViewVar(session))
{
Logger.WarningS("script", "Client {0} tried to access Scripting without permissions.", session);
_netManager.ServerSendMessage(reply, message.MsgChannel);
return;
}
var instances = _instances.GetOrNew(session);
if (instances.ContainsKey(message.ScriptSession))
{
// Already got one with this ID, client's problem.
_netManager.ServerSendMessage(reply, message.MsgChannel);
return;
}
ScriptInstanceShared.InitDummy();
var instance = new ScriptInstance();
instances.Add(message.ScriptSession, instance);
reply.WasAccepted = true;
_netManager.ServerSendMessage(reply, message.MsgChannel);
}
private async void ReceiveScriptEval(MsgScriptEval message)
{
if (!_playerManager.TryGetSessionByChannel(message.MsgChannel, out var session))
{
return;
}
if (!_conGroupController.CanViewVar(session))
{
Logger.WarningS("script", "Client {0} tried to access Scripting without permissions.", session);
return;
}
if (!_instances.TryGetValue(session, out var instances) ||
!instances.TryGetValue(message.ScriptSession, out var instance))
{
return;
}
var replyMessage = _netManager.CreateNetMessage<MsgScriptResponse>();
replyMessage.ScriptSession = message.ScriptSession;
var code = message.Code;
instance.InputBuffer.AppendLine(code);
var tree = SyntaxFactory.ParseSyntaxTree(SourceText.From(instance.InputBuffer.ToString()),
ScriptInstanceShared.ParseOptions);
if (!SyntaxFactory.IsCompleteSubmission(tree))
{
replyMessage.WasComplete = false;
_netManager.ServerSendMessage(replyMessage, message.MsgChannel);
return;
}
replyMessage.WasComplete = true;
code = instance.InputBuffer.ToString().Trim();
instance.InputBuffer.Clear();
Script newScript;
if (instance.State != null)
{
newScript = instance.State.Script.ContinueWith(code);
}
else
{
var options = ScriptInstanceShared.GetScriptOptions(_reflectionManager);
newScript = CSharpScript.Create(code, options, typeof(IScriptGlobals));
}
// Compile ahead of time so that we can do syntax highlighting correctly for the echo.
newScript.Compile();
// Echo entered script.
var echoMessage = new FormattedMessage();
ScriptInstanceShared.AddWithSyntaxHighlighting(newScript, echoMessage, code, instance.HighlightWorkspace);
replyMessage.Echo = echoMessage;
var msg = new FormattedMessage();
try
{
instance.RunningScript = true;
if (instance.State != null)
{
instance.State = await newScript.RunFromAsync(instance.State, _ => true);
}
else
{
instance.State = await newScript.RunAsync(instance.Globals);
}
}
catch (CompilationErrorException e)
{
msg.PushColor(Color.Crimson);
foreach (var diagnostic in e.Diagnostics)
{
msg.AddText(diagnostic.ToString());
msg.AddText("\n");
}
replyMessage.Response = msg;
_netManager.ServerSendMessage(replyMessage, message.MsgChannel);
return;
}
finally
{
instance.RunningScript = false;
}
if (instance.OutputBuffer.Length != 0)
{
msg.AddText(instance.OutputBuffer.ToString());
instance.OutputBuffer.Clear();
}
if (instance.State.Exception != null)
{
msg.PushColor(Color.Crimson);
msg.AddText(CSharpObjectFormatter.Instance.FormatException(instance.State.Exception));
}
else if (ScriptInstanceShared.HasReturnValue(newScript))
{
msg.AddText(CSharpObjectFormatter.Instance.FormatObject(instance.State.ReturnValue));
}
replyMessage.Response = msg;
_netManager.ServerSendMessage(replyMessage, message.MsgChannel);
}
private sealed class ScriptInstance
{
public Workspace HighlightWorkspace { get; } = new AdhocWorkspace();
public StringBuilder InputBuffer { get; } = new StringBuilder();
public StringBuilder OutputBuffer { get; } = new StringBuilder();
public bool RunningScript { get; set; }
public ScriptGlobals Globals { get; }
public ScriptState? State { get; set; }
public ScriptInstance()
{
Globals = new ScriptGlobals(this);
}
}
private sealed class ScriptGlobals : IScriptGlobals
{
private readonly ScriptInstance _scriptInstance;
public ScriptGlobals(ScriptInstance scriptInstance)
{
_scriptInstance = scriptInstance;
IoCManager.InjectDependencies(this);
}
[field: Dependency] public IEntityManager ent { get; } = default!;
[field: Dependency] public IComponentManager comp { get; } = default!;
public T res<T>()
{
return IoCManager.Resolve<T>();
}
public void write(object toString)
{
if (_scriptInstance.RunningScript)
{
_scriptInstance.OutputBuffer.AppendLine(toString?.ToString());
}
}
public void show(object obj)
{
write(CSharpObjectFormatter.Instance.FormatObject(obj));
}
}
}
[SuppressMessage("ReSharper", "InconsistentNaming")]
[PublicAPI]
public interface IScriptGlobals
{
public IEntityManager ent { get; }
public IComponentManager comp { get; }
public T res<T>();
public void write(object toString);
public void show(object obj);
}
}

View File

@@ -17,6 +17,7 @@ using Robust.Server.Placement;
using Robust.Server.Player;
using Robust.Server.Prototypes;
using Robust.Server.Reflection;
using Robust.Server.Scripting;
using Robust.Server.ServerStatus;
using Robust.Server.Timing;
using Robust.Server.ViewVariables;
@@ -71,6 +72,7 @@ namespace Robust.Server
IoCManager.Register<IViewVariablesHost, ViewVariablesHost>();
IoCManager.Register<IDebugDrawingManager, DebugDrawingManager>();
IoCManager.Register<IWatchdogApi, WatchdogApi>();
IoCManager.Register<IScriptHost, ScriptHost>();
}
}
}

View File

@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Robust.Server")]
[assembly: InternalsVisibleTo("Robust.Client")]

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\MSBuild\Robust.Properties.targets" />
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>8</LangVersion>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>../bin/Shared.Maths</OutputPath>
<Configurations>Debug;Release</Configurations>
<Platforms>x64</Platforms>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
</PropertyGroup>
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="3.5.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.5.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Lidgren.Network;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.Text;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Robust.Shared.Scripting
{
internal static class ScriptInstanceShared
{
public static CSharpParseOptions ParseOptions { get; } =
new CSharpParseOptions(kind: SourceCodeKind.Script, languageVersion: LanguageVersion.Latest);
private static readonly Func<Script, bool> _hasReturnValue;
private static readonly string[] _defaultImports =
{
"System",
"System.Linq",
"System.Collections.Generic",
"Robust.Shared.IoC",
"Robust.Shared.Maths",
"Robust.Shared.GameObjects",
"Robust.Shared.Interfaces.GameObjects"
};
static ScriptInstanceShared()
{
// This is the (internal) method that csi seems to use.
// Because it is internal and I can't find an alternative, reflection it is.
// TODO: Find a way that doesn't need me to reflect into Roslyn internals.
var method = typeof(Script).GetMethod("HasReturnValue", BindingFlags.Instance | BindingFlags.NonPublic);
if (method == null)
{
// Fallback path in case they remove that.
// The method literally has a // TODO: remove
_hasReturnValue = _ => true;
return;
}
_hasReturnValue = (Func<Script, bool>) Delegate.CreateDelegate(typeof(Func<Script, bool>), method);
// Run this async so that Roslyn can "warm up" in another thread while you're typing in your first line,
// so the hang when you hit enter is less bad.
Task.Run(async () =>
{
const string code =
"var x = 5 + 5; var y = (object) \"foobar\"; void Foo(object a) { } Foo(y); Foo(x)";
var script = await CSharpScript.RunAsync(code);
var msg = new FormattedMessage();
// Even run the syntax highlighter!
AddWithSyntaxHighlighting(script.Script, msg, code, new AdhocWorkspace());
});
}
/// <summary>
/// Does nothing, but will invoke the static constructor so Roslyn can warm up.
/// </summary>
public static void InitDummy()
{
// Nada.
}
public static bool HasReturnValue(Script script)
{
return _hasReturnValue(script);
}
public static void AddWithSyntaxHighlighting(Script script, FormattedMessage msg, string code,
Workspace workspace)
{
var compilation = script.GetCompilation();
var model = compilation.GetSemanticModel(compilation.SyntaxTrees.First());
var classified = Classifier.GetClassifiedSpans(model, TextSpan.FromBounds(0, code.Length), workspace);
var current = 0;
foreach (var span in classified)
{
var start = span.TextSpan.Start;
if (start > current)
{
msg.AddText(code[current..start]);
}
if (current > start)
{
continue;
}
// TODO: there are probably issues with multiple classifications overlapping the same text here.
// Too lazy to fix.
var src = code[span.TextSpan.Start..span.TextSpan.End];
var color = span.ClassificationType switch
{
ClassificationTypeNames.Comment => Color.FromHex("#57A64A"),
ClassificationTypeNames.NumericLiteral => Color.FromHex("#b5cea8"),
ClassificationTypeNames.StringLiteral => Color.FromHex("#D69D85"),
ClassificationTypeNames.Keyword => Color.FromHex("#569CD6"),
ClassificationTypeNames.StaticSymbol => Color.FromHex("#4EC9B0"),
ClassificationTypeNames.ClassName => Color.FromHex("#4EC9B0"),
ClassificationTypeNames.StructName => Color.FromHex("#4EC9B0"),
ClassificationTypeNames.InterfaceName => Color.FromHex("#B8D7A3"),
ClassificationTypeNames.EnumName => Color.FromHex("#B8D7A3"),
_ => Color.FromHex("#D4D4D4")
};
msg.PushColor(color);
msg.AddText(src);
msg.Pop();
current = span.TextSpan.End;
}
msg.AddText(code[current..]);
}
private static IEnumerable<Assembly> GetDefaultReferences(IReflectionManager reflectionManager)
{
var list = new List<Assembly>();
list.AddRange(reflectionManager.Assemblies);
list.Add(typeof(YamlDocument).Assembly); // YamlDotNet
list.Add(typeof(NetPeer).Assembly); // Lidgren
list.Add(typeof(Vector2).Assembly); // Robust.Shared.Maths
return list;
}
public static ScriptOptions GetScriptOptions(IReflectionManager reflectionManager)
{
return ScriptOptions.Default
.AddImports(_defaultImports)
.AddReferences(GetDefaultReferences(reflectionManager));
}
}
}

View File

@@ -10,7 +10,9 @@ namespace Robust.Shared.Console
public List<string> Commands { get; set; }
// NOTE: When adding special permissions, do NOT forget to add it to MsgConGroupUpdate!!
public bool CanViewVar { get; set; }
public bool CanAdminPlace { get; set; }
public bool CanScript { get; set; }
}
}

View File

@@ -28,6 +28,7 @@ namespace Robust.Shared.Console
ClientConGroup.Name = buffer.ReadString();
ClientConGroup.CanViewVar = buffer.ReadBoolean();
ClientConGroup.CanAdminPlace = buffer.ReadBoolean();
ClientConGroup.CanScript = buffer.ReadBoolean();
int numCommands = buffer.ReadInt32();
ClientConGroup.Commands = new List<string>(numCommands);
@@ -43,6 +44,7 @@ namespace Robust.Shared.Console
buffer.Write(ClientConGroup.Name);
buffer.Write(ClientConGroup.CanViewVar);
buffer.Write(ClientConGroup.CanAdminPlace);
buffer.Write(ClientConGroup.CanScript);
buffer.Write(ClientConGroup.Commands.Count);
foreach (var command in ClientConGroup.Commands)

View File

@@ -0,0 +1,34 @@
using Lidgren.Network;
using Robust.Shared.Interfaces.Network;
namespace Robust.Shared.Network.Messages
{
public class MsgScriptEval : NetMessage
{
#region REQUIRED
public const MsgGroups GROUP = MsgGroups.Command;
public const string NAME = nameof(MsgScriptEval);
public MsgScriptEval(INetChannel channel) : base(NAME, GROUP)
{
}
#endregion
public int ScriptSession { get; set; }
public string Code { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
ScriptSession = buffer.ReadInt32();
Code = buffer.ReadString();
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(ScriptSession);
buffer.Write(Code);
}
}
}

View File

@@ -0,0 +1,66 @@
using System.IO;
using Lidgren.Network;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
namespace Robust.Shared.Network.Messages
{
public class MsgScriptResponse : NetMessage
{
#region REQUIRED
public const MsgGroups GROUP = MsgGroups.Command;
public const string NAME = nameof(MsgScriptResponse);
public MsgScriptResponse(INetChannel channel) : base(NAME, GROUP)
{
}
public int ScriptSession { get; set; }
public bool WasComplete { get; set; }
// Echo of the entered code with syntax highlighting applied.
public FormattedMessage Echo { get; set; }
public FormattedMessage Response { get; set; }
#endregion
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
ScriptSession = buffer.ReadInt32();
WasComplete = buffer.ReadBoolean();
if (WasComplete)
{
var serializer = IoCManager.Resolve<IRobustSerializer>();
var length = buffer.ReadVariableInt32();
var stateData = buffer.ReadBytes(length);
using var memoryStream = new MemoryStream(stateData);
Echo = serializer.Deserialize<FormattedMessage>(memoryStream);
Response = serializer.Deserialize<FormattedMessage>(memoryStream);
}
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(ScriptSession);
buffer.Write(WasComplete);
if (WasComplete)
{
var serializer = IoCManager.Resolve<IRobustSerializer>();
var memoryStream = new MemoryStream();
serializer.Serialize(memoryStream, Echo);
serializer.Serialize(memoryStream, Response);
buffer.WriteVariableInt32((int)memoryStream.Length);
buffer.Write(memoryStream.ToArray());
}
}
}
}

View File

@@ -0,0 +1,31 @@
using Lidgren.Network;
using Robust.Shared.Interfaces.Network;
namespace Robust.Shared.Network.Messages
{
public class MsgScriptStart : NetMessage
{
#region REQUIRED
public const MsgGroups GROUP = MsgGroups.Command;
public const string NAME = nameof(MsgScriptStart);
public MsgScriptStart(INetChannel channel) : base(NAME, GROUP)
{
}
#endregion
public int ScriptSession { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
ScriptSession = buffer.ReadInt32();
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(ScriptSession);
}
}
}

View File

@@ -0,0 +1,34 @@
using Lidgren.Network;
using Robust.Shared.Interfaces.Network;
namespace Robust.Shared.Network.Messages
{
public class MsgScriptStartAck : NetMessage
{
#region REQUIRED
public const MsgGroups GROUP = MsgGroups.Command;
public const string NAME = nameof(MsgScriptStartAck);
public MsgScriptStartAck(INetChannel channel) : base(NAME, GROUP)
{
}
#endregion
public bool WasAccepted { get; set; }
public int ScriptSession { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
WasAccepted = buffer.ReadBoolean();
ScriptSession = buffer.ReadInt32();
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(WasAccepted);
buffer.Write(ScriptSession);
}
}
}

View File

@@ -0,0 +1,31 @@
using Lidgren.Network;
using Robust.Shared.Interfaces.Network;
namespace Robust.Shared.Network.Messages
{
public class MsgScriptStop : NetMessage
{
#region REQUIRED
public const MsgGroups GROUP = MsgGroups.Command;
public const string NAME = nameof(MsgScriptStop);
public MsgScriptStop(INetChannel channel) : base(NAME, GROUP)
{
}
#endregion
public int ScriptSession { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
ScriptSession = buffer.ReadInt32();
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(ScriptSession);
}
}
}

View File

@@ -106,5 +106,16 @@ namespace Robust.Shared.Utility
}
return null;
}
public static TValue GetOrNew<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key) where TValue : new()
{
if (!dict.TryGetValue(key, out var value))
{
value = new TValue();
dict.Add(key, value);
}
return value;
}
}
}