mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
Server scripting.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -121,6 +121,7 @@ namespace Robust.Client
|
||||
IoCManager.Register<IViewVariablesManagerInternal, ViewVariablesManager>();
|
||||
IoCManager.Register<ISignalHandler, ClientSignalHandler>();
|
||||
IoCManager.Register<IClientConGroupController, ClientConGroupController>();
|
||||
IoCManager.Register<IScriptClient, ScriptClient>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace Robust.Client.Console
|
||||
bool CanCommand(string cmdName);
|
||||
bool CanViewVar();
|
||||
bool CanAdminPlace();
|
||||
bool CanScript();
|
||||
event Action ConGroupUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
13
Robust.Client/Console/IScriptClient.cs
Normal file
13
Robust.Client/Console/IScriptClient.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
100
Robust.Client/Console/ScriptClient.ScriptConsoleServer.cs
Normal file
100
Robust.Client/Console/ScriptClient.ScriptConsoleServer.cs
Normal 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(">");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
Robust.Client/Console/ScriptClient.cs
Normal file
69
Robust.Client/Console/ScriptClient.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
210
Robust.Client/Console/ScriptConsoleClient.cs
Normal file
210
Robust.Client/Console/ScriptConsoleClient.cs
Normal 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
|
||||
@@ -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();
|
||||
|
||||
@@ -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" />
|
||||
|
||||
63
Robust.Client/UserInterface/CustomControls/ScriptConsole.cs
Normal file
63
Robust.Client/UserInterface/CustomControls/ScriptConsole.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
7
Robust.Server/Scripting/IScriptHost.cs
Normal file
7
Robust.Server/Scripting/IScriptHost.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Robust.Server.Scripting
|
||||
{
|
||||
internal interface IScriptHost
|
||||
{
|
||||
public void Initialize();
|
||||
}
|
||||
}
|
||||
282
Robust.Server/Scripting/ScriptHost.cs
Normal file
282
Robust.Server/Scripting/ScriptHost.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
Robust.Shared.Scripting/Properties/AssemblyInfo.cs
Normal file
4
Robust.Shared.Scripting/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Robust.Server")]
|
||||
[assembly: InternalsVisibleTo("Robust.Client")]
|
||||
27
Robust.Shared.Scripting/Robust.Shared.Scripting.csproj
Normal file
27
Robust.Shared.Scripting/Robust.Shared.Scripting.csproj
Normal 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>
|
||||
148
Robust.Shared.Scripting/ScriptInstanceShared.cs
Normal file
148
Robust.Shared.Scripting/ScriptInstanceShared.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
34
Robust.Shared/Network/Messages/MsgScriptEval.cs
Normal file
34
Robust.Shared/Network/Messages/MsgScriptEval.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
Robust.Shared/Network/Messages/MsgScriptResponse.cs
Normal file
66
Robust.Shared/Network/Messages/MsgScriptResponse.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Robust.Shared/Network/Messages/MsgScriptStart.cs
Normal file
31
Robust.Shared/Network/Messages/MsgScriptStart.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Robust.Shared/Network/Messages/MsgScriptStartAck.cs
Normal file
34
Robust.Shared/Network/Messages/MsgScriptStartAck.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Robust.Shared/Network/Messages/MsgScriptStop.cs
Normal file
31
Robust.Shared/Network/Messages/MsgScriptStop.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user