diff --git a/MSBuild/Robust.DefineConstants.targets b/MSBuild/Robust.DefineConstants.targets index c84cfe691..780433cc3 100644 --- a/MSBuild/Robust.DefineConstants.targets +++ b/MSBuild/Robust.DefineConstants.targets @@ -23,7 +23,7 @@ $(DefineConstants);EXCEPTION_TOLERANCE - - $(DefineConstants);SCRIPTING + + $(DefineConstants);CLIENT_SCRIPTING diff --git a/MSBuild/Robust.Properties.targets b/MSBuild/Robust.Properties.targets index 50d1f4672..ac9f732ed 100644 --- a/MSBuild/Robust.Properties.targets +++ b/MSBuild/Robust.Properties.targets @@ -27,8 +27,8 @@ python3 py -3 netcoreapp3.0 - True - - False + True + + False diff --git a/Robust.Client/ClientIoC.cs b/Robust.Client/ClientIoC.cs index d99b00296..b050d1350 100644 --- a/Robust.Client/ClientIoC.cs +++ b/Robust.Client/ClientIoC.cs @@ -121,6 +121,7 @@ namespace Robust.Client IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Robust.Client/Console/ClientConGroupController.cs b/Robust.Client/Console/ClientConGroupController.cs index 7e414620a..833af52b3 100644 --- a/Robust.Client/Console/ClientConGroupController.cs +++ b/Robust.Client/Console/ClientConGroupController.cs @@ -48,6 +48,13 @@ namespace Robust.Client.Console return _clientConGroup.CanAdminPlace; } + public bool CanScript() + { + if (_clientConGroup == null) + return false; + return _clientConGroup.CanScript; + } + /// /// Update client console group data with message from the server. /// diff --git a/Robust.Client/Console/Commands/Scripting.cs b/Robust.Client/Console/Commands/Scripting.cs index f0c5b1ba7..3ab552d59 100644 --- a/Robust.Client/Console/Commands/Scripting.cs +++ b/Robust.Client/Console/Commands/Scripting.cs @@ -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(); + if (!mgr.CanScript) + { + console.AddLine(Loc.GetString("You do not have server side scripting permission."), Color.Red); + return false; + } + + mgr.StartSession(); return false; } } } -#endif diff --git a/Robust.Client/Console/IClientConGroupController.cs b/Robust.Client/Console/IClientConGroupController.cs index 29ce7f7fc..7aefcff84 100644 --- a/Robust.Client/Console/IClientConGroupController.cs +++ b/Robust.Client/Console/IClientConGroupController.cs @@ -13,6 +13,7 @@ namespace Robust.Client.Console bool CanCommand(string cmdName); bool CanViewVar(); bool CanAdminPlace(); + bool CanScript(); event Action ConGroupUpdated; } } diff --git a/Robust.Client/Console/IScriptClient.cs b/Robust.Client/Console/IScriptClient.cs new file mode 100644 index 000000000..4c29f7d7b --- /dev/null +++ b/Robust.Client/Console/IScriptClient.cs @@ -0,0 +1,13 @@ +namespace Robust.Client.Console +{ + /// + /// Client manager for server side scripting. + /// + public interface IScriptClient + { + void Initialize(); + + bool CanScript { get; } + void StartSession(); + } +} diff --git a/Robust.Client/Console/ScriptClient.ScriptConsoleServer.cs b/Robust.Client/Console/ScriptClient.ScriptConsoleServer.cs new file mode 100644 index 000000000..1bf296cee --- /dev/null +++ b/Robust.Client/Console/ScriptClient.ScriptConsoleServer.cs @@ -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(); + 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(">"); + } + } + } +} diff --git a/Robust.Client/Console/ScriptClient.cs b/Robust.Client/Console/ScriptClient.cs new file mode 100644 index 000000000..ab35c9bf9 --- /dev/null +++ b/Robust.Client/Console/ScriptClient.cs @@ -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 _activeConsoles = new Dictionary(); + + private int _nextSessionId = 1; + + public void Initialize() + { + _netManager.RegisterNetMessage(MsgScriptStop.NAME); + _netManager.RegisterNetMessage(MsgScriptEval.NAME); + _netManager.RegisterNetMessage(MsgScriptStart.NAME); + _netManager.RegisterNetMessage(MsgScriptResponse.NAME, ReceiveScriptResponse); + _netManager.RegisterNetMessage(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(); + msg.ScriptSession = _nextSessionId++; + _netManager.ClientSendMessage(msg); + } + + private void ConsoleClosed(int session) + { + _activeConsoles.Remove(session); + + var msg = _netManager.CreateNetMessage(); + msg.ScriptSession = session; + _netManager.ClientSendMessage(msg); + } + } +} diff --git a/Robust.Client/Console/ScriptConsole.cs b/Robust.Client/Console/ScriptConsole.cs deleted file mode 100644 index 8f45ca8cf..000000000 --- a/Robust.Client/Console/ScriptConsole.cs +++ /dev/null @@ -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 _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) Delegate.CreateDelegate(typeof(Func), 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 GetDefaultReferences() - { - var list = new List(); - - 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() - { - return IoCManager.Resolve(); - } - - 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(); - public void write(object toString); - public void show(object obj); - } -} -#endif diff --git a/Robust.Client/Console/ScriptConsoleClient.cs b/Robust.Client/Console/ScriptConsoleClient.cs new file mode 100644 index 000000000..f3844a989 --- /dev/null +++ b/Robust.Client/Console/ScriptConsoleClient.cs @@ -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() + { + return IoCManager.Resolve(); + } + + 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(); + public void write(object toString); + public void show(object obj); + } +} +#endif diff --git a/Robust.Client/GameController.cs b/Robust.Client/GameController.cs index 511bc89f4..45dff0ea3 100644 --- a/Robust.Client/GameController.cs +++ b/Robust.Client/GameController.cs @@ -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(); diff --git a/Robust.Client/Robust.Client.csproj b/Robust.Client/Robust.Client.csproj index 156eb0876..c311bb967 100644 --- a/Robust.Client/Robust.Client.csproj +++ b/Robust.Client/Robust.Client.csproj @@ -29,10 +29,12 @@ - + + + diff --git a/Robust.Client/UserInterface/CustomControls/ScriptConsole.cs b/Robust.Client/UserInterface/CustomControls/ScriptConsole.cs new file mode 100644 index 000000000..e8f843dd5 --- /dev/null +++ b/Robust.Client/UserInterface/CustomControls/ScriptConsole.cs @@ -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(); + } + } +} diff --git a/Robust.Server/BaseServer.cs b/Robust.Server/BaseServer.cs index 3fedad9ba..0b55d3644 100644 --- a/Robust.Server/BaseServer.cs +++ b/Robust.Server/BaseServer.cs @@ -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().Initialize(); IoCManager.Resolve().Initialize(); _entities.Startup(); + _scriptHost.Initialize(); _modLoader.BroadcastRunLevel(ModRunLevel.PostInit); diff --git a/Robust.Server/Console/ConGroupContainer.cs b/Robust.Server/Console/ConGroupContainer.cs index 4732c1d4e..abe01e4dc 100644 --- a/Robust.Server/Console/ConGroupContainer.cs +++ b/Robust.Server/Console/ConGroupContainer.cs @@ -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; + } } } diff --git a/Robust.Server/Console/ConGroupController.cs b/Robust.Server/Console/ConGroupController.cs index 543a68bc0..b63d2f680 100644 --- a/Robust.Server/Console/ConGroupController.cs +++ b/Robust.Server/Console/ConGroupController.cs @@ -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); + } + /// /// Clears all session data. /// diff --git a/Robust.Server/Console/IConGroupController.cs b/Robust.Server/Console/IConGroupController.cs index b9d2c8d30..563433d7b 100644 --- a/Robust.Server/Console/IConGroupController.cs +++ b/Robust.Server/Console/IConGroupController.cs @@ -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); } } diff --git a/Robust.Server/Interfaces/Player/IPlayerManager.cs b/Robust.Server/Interfaces/Player/IPlayerManager.cs index 91e3184e4..8f26a38b0 100644 --- a/Robust.Server/Interfaces/Player/IPlayerManager.cs +++ b/Robust.Server/Interfaces/Player/IPlayerManager.cs @@ -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); /// diff --git a/Robust.Server/Robust.Server.csproj b/Robust.Server/Robust.Server.csproj index 3fdd89098..b6c11cf0d 100644 --- a/Robust.Server/Robust.Server.csproj +++ b/Robust.Server/Robust.Server.csproj @@ -21,6 +21,7 @@ + diff --git a/Robust.Server/Scripting/IScriptHost.cs b/Robust.Server/Scripting/IScriptHost.cs new file mode 100644 index 000000000..b4bdf2f9b --- /dev/null +++ b/Robust.Server/Scripting/IScriptHost.cs @@ -0,0 +1,7 @@ +namespace Robust.Server.Scripting +{ + internal interface IScriptHost + { + public void Initialize(); + } +} diff --git a/Robust.Server/Scripting/ScriptHost.cs b/Robust.Server/Scripting/ScriptHost.cs new file mode 100644 index 000000000..be8af4c79 --- /dev/null +++ b/Robust.Server/Scripting/ScriptHost.cs @@ -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> _instances = + new Dictionary>(); + + public void Initialize() + { + _netManager.RegisterNetMessage(MsgScriptStop.NAME, ReceiveScriptEnd); + _netManager.RegisterNetMessage(MsgScriptEval.NAME, ReceiveScriptEval); + _netManager.RegisterNetMessage(MsgScriptStart.NAME, ReceiveScriptStart); + _netManager.RegisterNetMessage(MsgScriptResponse.NAME); + _netManager.RegisterNetMessage(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(); + 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(); + 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() + { + return IoCManager.Resolve(); + } + + 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(); + public void write(object toString); + public void show(object obj); + } +} diff --git a/Robust.Server/ServerIoC.cs b/Robust.Server/ServerIoC.cs index b89c0842f..b55c6cd94 100644 --- a/Robust.Server/ServerIoC.cs +++ b/Robust.Server/ServerIoC.cs @@ -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(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Robust.Shared.Scripting/Properties/AssemblyInfo.cs b/Robust.Shared.Scripting/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..77d0d33ab --- /dev/null +++ b/Robust.Shared.Scripting/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Robust.Server")] +[assembly: InternalsVisibleTo("Robust.Client")] diff --git a/Robust.Shared.Scripting/Robust.Shared.Scripting.csproj b/Robust.Shared.Scripting/Robust.Shared.Scripting.csproj new file mode 100644 index 000000000..8aa3ec95e --- /dev/null +++ b/Robust.Shared.Scripting/Robust.Shared.Scripting.csproj @@ -0,0 +1,27 @@ + + + + + $(TargetFramework) + 8 + false + false + ../bin/Shared.Maths + Debug;Release + x64 + true + enable + + + + + + + + + + + + + + diff --git a/Robust.Shared.Scripting/ScriptInstanceShared.cs b/Robust.Shared.Scripting/ScriptInstanceShared.cs new file mode 100644 index 000000000..b7d770e9e --- /dev/null +++ b/Robust.Shared.Scripting/ScriptInstanceShared.cs @@ -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 _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) Delegate.CreateDelegate(typeof(Func), 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()); + }); + } + + /// + /// Does nothing, but will invoke the static constructor so Roslyn can warm up. + /// + 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 GetDefaultReferences(IReflectionManager reflectionManager) + { + var list = new List(); + + 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)); + } + } +} diff --git a/Robust.Shared/Console/ConGroup.cs b/Robust.Shared/Console/ConGroup.cs index ac9ccd0f3..e8ba4ac31 100644 --- a/Robust.Shared/Console/ConGroup.cs +++ b/Robust.Shared/Console/ConGroup.cs @@ -10,7 +10,9 @@ namespace Robust.Shared.Console public List 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; } } } diff --git a/Robust.Shared/Console/MsgConGroupUpdate.cs b/Robust.Shared/Console/MsgConGroupUpdate.cs index ee5fb76df..734020515 100644 --- a/Robust.Shared/Console/MsgConGroupUpdate.cs +++ b/Robust.Shared/Console/MsgConGroupUpdate.cs @@ -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(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) diff --git a/Robust.Shared/Network/Messages/MsgScriptEval.cs b/Robust.Shared/Network/Messages/MsgScriptEval.cs new file mode 100644 index 000000000..8173bd323 --- /dev/null +++ b/Robust.Shared/Network/Messages/MsgScriptEval.cs @@ -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); + } + } +} diff --git a/Robust.Shared/Network/Messages/MsgScriptResponse.cs b/Robust.Shared/Network/Messages/MsgScriptResponse.cs new file mode 100644 index 000000000..b9ece30a0 --- /dev/null +++ b/Robust.Shared/Network/Messages/MsgScriptResponse.cs @@ -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(); + + var length = buffer.ReadVariableInt32(); + var stateData = buffer.ReadBytes(length); + + using var memoryStream = new MemoryStream(stateData); + Echo = serializer.Deserialize(memoryStream); + Response = serializer.Deserialize(memoryStream); + } + } + + public override void WriteToBuffer(NetOutgoingMessage buffer) + { + buffer.Write(ScriptSession); + buffer.Write(WasComplete); + + if (WasComplete) + { + var serializer = IoCManager.Resolve(); + + var memoryStream = new MemoryStream(); + serializer.Serialize(memoryStream, Echo); + serializer.Serialize(memoryStream, Response); + + buffer.WriteVariableInt32((int)memoryStream.Length); + buffer.Write(memoryStream.ToArray()); + } + } + } +} diff --git a/Robust.Shared/Network/Messages/MsgScriptStart.cs b/Robust.Shared/Network/Messages/MsgScriptStart.cs new file mode 100644 index 000000000..89b9022ac --- /dev/null +++ b/Robust.Shared/Network/Messages/MsgScriptStart.cs @@ -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); + } + } +} diff --git a/Robust.Shared/Network/Messages/MsgScriptStartAck.cs b/Robust.Shared/Network/Messages/MsgScriptStartAck.cs new file mode 100644 index 000000000..11995556e --- /dev/null +++ b/Robust.Shared/Network/Messages/MsgScriptStartAck.cs @@ -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); + } + } +} diff --git a/Robust.Shared/Network/Messages/MsgScriptStop.cs b/Robust.Shared/Network/Messages/MsgScriptStop.cs new file mode 100644 index 000000000..0715b7cc0 --- /dev/null +++ b/Robust.Shared/Network/Messages/MsgScriptStop.cs @@ -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); + } + } +} diff --git a/Robust.Shared/Utility/CollectionExtensions.cs b/Robust.Shared/Utility/CollectionExtensions.cs index 3383c442d..298a5a398 100644 --- a/Robust.Shared/Utility/CollectionExtensions.cs +++ b/Robust.Shared/Utility/CollectionExtensions.cs @@ -106,5 +106,16 @@ namespace Robust.Shared.Utility } return null; } + + public static TValue GetOrNew(this IDictionary dict, TKey key) where TValue : new() + { + if (!dict.TryGetValue(key, out var value)) + { + value = new TValue(); + dict.Add(key, value); + } + + return value; + } } }