diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 596a49b22..664bce9b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -3236,6 +3236,28 @@ namespace Barotrauma GameMain.Lua.Initialize(); })); + + commands.Add(new Command("cl_net", "lua_net: runs a script on the client", (string[] args) => + { + if (GameMain.Client != null && !GameMain.Client.HasPermission(ClientPermissions.ConsoleCommands)) + { + ThrowError("Command not permitted."); + return; + } + + GameMain.Lua.DoString(string.Join(" ", args)); + })); + + commands.Add(new Command("cl_reloadnet", "reloads lua on the client", (string[] args) => + { + if (GameMain.Client != null && !GameMain.Client.HasPermission(ClientPermissions.ConsoleCommands)) + { + ThrowError("Command not permitted."); + return; + } + + GameMain.Lua.Initialize(); + })); } private static void ReloadWearables(Character character, int variant = 0) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index b24141c0f..24b11096c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -21,6 +21,7 @@ namespace Barotrauma class GameMain : Game { public static LuaSetup Lua; + public static NetSetup Net; public static bool ShowFPS = false; public static bool ShowPerf = false; @@ -227,6 +228,8 @@ namespace Barotrauma } Lua = new LuaSetup(); + Net = new NetSetup(); + Net.Execute(); GameSettings.Init(); @@ -924,6 +927,7 @@ namespace Barotrauma GameMain.Lua.Update(); GameMain.Lua.hook.Call("think", new object[] { }); + GameMain.Net.Update(); Timing.Accumulator -= Timing.Step; @@ -1090,6 +1094,7 @@ namespace Barotrauma GameSession = null; GameMain.Lua.Stop(); + GameMain.Net.Stop(); } public void ShowCampaignDisclaimer(Action onContinue = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 645ff460d..3a5eb48ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -2705,6 +2705,7 @@ namespace Barotrauma.Networking public override void Disconnect() { GameMain.Lua.Stop(); + GameMain.Net.Stop(); allowReconnect = false; diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index f78c9a0f3..017c19e8b 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -132,6 +132,7 @@ + diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 4a9d31f13..8e2934ebe 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -137,6 +137,7 @@ + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index c82cea7e0..e4c148f66 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -86,6 +86,8 @@ + + diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index bfcc41ed5..a2053d881 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -35,6 +35,7 @@ namespace Barotrauma } public static LuaSetup Lua; + public static NetSetup Net; public static GameServer Server; public static NetworkMember NetworkMember @@ -121,6 +122,8 @@ namespace Barotrauma CheckContentPackage(); Lua = new LuaSetup(); + Net = new NetSetup(); + Net.Execute(); } @@ -349,6 +352,7 @@ namespace Barotrauma GameMain.Lua.Update(); GameMain.Lua.hook.Call("think", new object[] { }); + GameMain.Net.Update(); performanceCounterTimer.Stop(); LuaTimer.LastUpdateTime = performanceCounterTimer.ElapsedMilliseconds; performanceCounterTimer.Reset(); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 1b264f8ad..783f5c989 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -82,7 +82,9 @@ - + + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Net/ANetMod.cs b/Barotrauma/BarotraumaShared/SharedSource/Net/ANetMod.cs new file mode 100644 index 000000000..349b0afb5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Net/ANetMod.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + public abstract class ANetMod : IDisposable + { + private static List mods = new List(); + public static List LoadedMods + { + get => mods; + } + public ANetMod() + { + LoadedMods.Add(this); + } + + public abstract void Dispose(); + + // TODO: some hooks + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Net/NetScriptFilter.cs b/Barotrauma/BarotraumaShared/SharedSource/Net/NetScriptFilter.cs new file mode 100644 index 000000000..e70adb182 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Net/NetScriptFilter.cs @@ -0,0 +1,96 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Scripting; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +partial class NetScript +{ + public class NetScriptFilter + { + private const bool useWhitelist = false; + + private static string[] classesPermited = new string[] {}; + private static string[] classesProhibited = new string[] { }; + public static bool IsClassAllowed(string usingName) + { + if (useWhitelist && !classesPermited.Any(u => u.Equals(usingName))) return false; + if (classesProhibited.Any(u => u.Equals(usingName))) return false; + return true; + } + + public static string FilterSyntaxTree(CSharpSyntaxTree tree) + { + if (tree == null) throw new ArgumentNullException("Syntax tree must not be null."); + { // Disallow top-level statements + var nodeCheck = tree.GetRoot().DescendantNodes(); + + var tlStatements = nodeCheck.Where(n => n is GlobalStatementSyntax).ToList(); + if (tlStatements.Count > 0) + { + string errStr = "Cmopilation Error:"; + foreach (var tls in tlStatements) tls.GetDiagnostics().ToList().ForEach(d => errStr += "\n" + d.ToString()); + return errStr; + } + } + + var compRoot = tree.GetCompilationUnitRoot(); + var refDirs = compRoot.GetReferenceDirectives().ToList(); + Console.WriteLine($"Reference Directives [{refDirs.Count}]:"); + refDirs.ForEach(d => Console.WriteLine(d.ToFullString())); + + List allUsedTypes = new List(); + { // Find all used types + } + + List allResolvedTypes = new List(); + { // Resolve all types + } + + { // Check all used types + } + + if (!Directory.Exists("./SyntaxTrees")) Directory.CreateDirectory("./SyntaxTrees"); + string fileName = "./SyntaxTrees/" + tree.FilePath.Replace("/", "--") + ".txt"; + if (File.Exists(fileName)) File.Delete(fileName); + var fileWriter = File.CreateText(fileName); + + var nodes = new Queue<(SyntaxNode, int)>(); + nodes.Enqueue((tree.GetRoot(), 0)); + while (nodes.Count > 0) + { + var nodeElem = nodes.Dequeue(); + var node = nodeElem.Item1; + var indent = nodeElem.Item2; + + node.ChildNodes().ToList().ForEach(n => { + if (n.ChildNodes().Count() > 0) nodes.Enqueue((n, indent + 1)); + if (!( + n is MemberAccessExpressionSyntax || + n is UsingDirectiveSyntax || + n is BaseTypeSyntax || + n is TypeSyntax + )) return; + //Console.WriteLine(new String(' ', indent * 2) + n.GetType().Name + " | " + n.GetText()?.ToString() ?? "null"); + fileWriter.WriteLine(new String(' ', indent * 2) + n.GetType().Name + " | " + n.GetText()?.ToString() ?? "null"); + }); + node.DescendantNodes().ToList().ForEach(n => { + if (n.DescendantNodes().Count() > 0 && !nodes.Contains((n, indent + 1))) nodes.Enqueue((n, indent + 1)); + if (!( + n is MemberAccessExpressionSyntax || + n is UsingDirectiveSyntax || + n is BaseTypeSyntax || + n is TypeSyntax + )) return; + //Console.WriteLine(new String(' ', indent * 2) + n.GetType().Name + " | " + n.GetText()?.ToString() ?? "null"); + fileWriter.WriteLine(new String(' ', indent * 2) + n.GetType().Name + " | " + n.GetText()?.ToString() ?? "null"); + }); + } + fileWriter.Close(); + return null; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Net/NetScriptLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/Net/NetScriptLoader.cs new file mode 100644 index 000000000..8dd616986 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Net/NetScriptLoader.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.CodeAnalysis.Scripting; +using System.Reflection; +using Microsoft.CodeAnalysis.CSharp; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; +using System.Runtime.Loader; +using static NetScript; +using Microsoft.CodeAnalysis.Emit; +using System.Reflection.PortableExecutable; +using System.Reflection.Metadata; + +namespace Barotrauma +{ + partial class NetSetup + { + + public class NetScriptLoader : AssemblyLoadContext + { + public NetSetup net; + private List defaultReferences; + private List syntaxTrees; + public Assembly Assembly { get; private set; } + + public NetScriptLoader(NetSetup net) + { + this.net = net; + + defaultReferences = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit"))) + .Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference) + .ToList(); + + syntaxTrees = new List(); + Assembly = null; + } + + public void SearchFolders() + { + foreach(ContentPackage cp in GameMain.Config.AllEnabledPackages) + { + var path = Path.GetDirectoryName(cp.Path); + RunFolder(path); + } + } + + public void RunFolder(string folder) + { + var scriptFiles = new List(); + foreach (var str in DirSearch(folder)) + { + var s = str.Replace("\\", "/"); + + if (s.EndsWith(".cs")) + { + NetSetup.PrintMessage(s); + scriptFiles.Add(s); + } + } + + try + { + if (scriptFiles.Count <= 0) return; + + var mainFile = scriptFiles.Find(s => s.EndsWith("Main.cs")); + if (mainFile == null) throw new Exception("Mod folder has no Main.cs file"); + scriptFiles.Remove(mainFile); + scriptFiles.Add(mainFile); + + // Check file content for prohibited stuff + foreach (var file in scriptFiles) + { + var tree = SyntaxFactory.ParseSyntaxTree(File.ReadAllText(file), CSharpParseOptions.Default, file); + var error = NetScriptFilter.FilterSyntaxTree(tree as CSharpSyntaxTree); + if (error != null) throw new Exception(error); + + syntaxTrees.Add(tree); + } + } + catch (CompilationErrorException ex) + { + string errStr = "Cmopilation Error in '" + folder + "':"; + foreach (var diag in ex.Diagnostics) + { + errStr += "\n" + diag.ToString(); + } + NetSetup.PrintMessage(errStr); + } + catch (Exception ex) + { + NetSetup.PrintMessage("Error loading '" + folder + "':\n" + ex.Message + "\n" + ex.StackTrace); + } + } + + public List Compile() + { + var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + .WithMetadataImportOptions(MetadataImportOptions.All) + .WithOptimizationLevel(OptimizationLevel.Release) + .WithAllowUnsafe(false); + var compilation = CSharpCompilation.Create("NetScriptAssembly",syntaxTrees, defaultReferences, options); + + using (var mem = new MemoryStream()) + { + var result = compilation.Emit(mem); + if (!result.Success) + { + IEnumerable failures = result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error); + + string errStr = "NET MODS NOT LOADED | Mod cmopilation errors:"; + foreach (Diagnostic diagnostic in failures) + { + errStr = $"\n{diagnostic}"; + } + NetSetup.PrintMessage(errStr); + } + else + { + mem.Seek(0, SeekOrigin.Begin); + var reader = new PEReader(mem); + var mdReader = reader.GetMetadataReader(); + mdReader.AssemblyReferences.ToList().ForEach(a => + { + var aRef = mdReader.GetAssemblyReference(a); + Console.WriteLine(aRef.GetAssemblyName() + " " + aRef.Version); + }); + Console.WriteLine(); + mdReader.TypeReferences.ToList().ForEach(t => + { + var tRef = mdReader.GetTypeReference(t); + Console.WriteLine(mdReader.GetString(tRef.Namespace) + " - " + mdReader.GetString(tRef.Name)); + }); + mem.Seek(0, SeekOrigin.Begin); + Assembly = LoadFromStream(mem); + } + } + syntaxTrees.Clear(); + + if (Assembly != null) + return Assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(ANetMod))).ToList(); + else + throw new Exception("Unable to create net mods assembly."); + } + + static string[] DirSearch(string sDir) + { + List files = new List(); + + try + { + foreach (string f in Directory.GetFiles(sDir)) + { + files.Add(f); + } + + foreach (string d in Directory.GetDirectories(sDir)) + { + files.AddRange(DirSearch(d)); + } + } + catch (System.Exception excpt) + { + Console.WriteLine(excpt.Message); + } + + return files.ToArray(); + } + + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Net/NetSetup.cs b/Barotrauma/BarotraumaShared/SharedSource/Net/NetSetup.cs new file mode 100644 index 000000000..4c67cb86a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Net/NetSetup.cs @@ -0,0 +1,134 @@ +using System; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using HarmonyLib; +using System.Runtime.CompilerServices; +//using System.Linq; +//using System.Collections.Generic; +//using Microsoft.CodeAnalysis; +//using Microsoft.CodeAnalysis.CSharp; + +[assembly: InternalsVisibleTo("NetScriptAssembly", AllInternalsVisible = true)] + +namespace Barotrauma +{ + partial class NetSetup + { + public NetScriptLoader Loader { get; private set; } + public Harmony harmony; + public LuaHook hook; + public NetSetup() => Initialize(); + + public void Reload() + { + Stop(); + Initialize(); + Execute(); + } + public void Initialize() + { + hook = new LuaHook(); + Loader = new NetScriptLoader(this); + Loader.SearchFolders(); + + //var baseType = typeof(CSharpSyntaxNode); + //var typeDict = new Dictionary() { { baseType, new Dictionary() } }; + //var sytaxTypes = AppDomain.CurrentDomain + // .GetAssemblies() + // .Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit"))) + // .Select(a => a.GetTypes().Where(t => t.IsSubclassOf(baseType))) + // .Aggregate((t1, t2) => t1.Concat(t2)) + // .ToList(); + + //var q = new Queue<(Type, Dictionary)>(); + //q.Enqueue((baseType, typeDict[baseType] as Dictionary)); + //while (q.Count > 0) + //{ + // var entry = q.Dequeue(); + // var type = entry.Item1; + // var dict = entry.Item2; + + // sytaxTypes.Where(t => t.BaseType == type).ToList().ForEach(t => + // { + // var newDict = new Dictionary(); + // dict.Add(t, newDict); + // q.Enqueue((t, newDict)); + // }); + //} + + //Action> rprint = null; + //rprint = (indent, kv) => + //{ + // string idnt = ""; + // for (int i = 0; i < indent - 1; i++) idnt += "│ "; + // if (indent > 0) idnt += "├─"; + // Console.WriteLine(idnt + kv.Key.Name); + // foreach (var _kv in kv.Value as Dictionary) + // rprint(indent + 1, _kv); + //}; + //foreach (var kv in typeDict) rprint(0, kv); + } + public void Execute() + { + if (Loader == null) throw new Exception("NetSetup was not properly initialized."); + try + { + var modTypes = Loader.Compile(); + modTypes.ForEach(t => t.GetConstructor(new Type[] { }).Invoke(null)); + } + catch (Exception ex) + { + PrintMessage(ex); + } + } + public void Stop() + { + harmony?.UnpatchAll(); + hook?.Call("stop", new object[] { }); + + hook = null; + Loader = null; + + ANetMod.LoadedMods.ForEach(m => m.Dispose()); + ANetMod.LoadedMods.Clear(); + Loader.Unload(); + } + + public void Update() + { + hook?.Update(); + } + + private static bool wasPublicized = false; + + public static void PrintMessage(object message) + { + if (message == null) { message = "null"; } + string str = message.ToString(); + +#if SERVER + if (GameMain.Server != null) + { + for (int i = 0; i < str.Length; i += 1024) + { + string subStr = str.Substring(i, Math.Min(1024, str.Length - i)); + + + foreach (var c in GameMain.Server.ConnectedClients) + { + GameMain.Server.SendDirectChatMessage(ChatMessage.Create("", subStr, ChatMessageType.Console, null, textColor: Color.MediumPurple), c); + } + + GameServer.Log("[NET] " + subStr, ServerLog.MessageType.ServerMessage); + } + } + else + { + DebugConsole.NewMessage("[NET]" + message.ToString(), Color.MediumPurple); + } +#else + DebugConsole.NewMessage(message.ToString(), Color.Purple); +#endif + } + } +} \ No newline at end of file