Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
067d8802b6 | ||
![]() |
8f32f64ede | ||
![]() |
90ff3f93f0 | ||
![]() |
ad28b302f9 | ||
![]() |
a0d0976a6a |
@@ -89,8 +89,6 @@ namespace Torch.Server
|
||||
var workingDir = AppContext.BaseDirectory;
|
||||
var gamePath = configuration.GetValue("gamePath", workingDir);
|
||||
var binDir = Path.Combine(gamePath, "DedicatedServer64");
|
||||
|
||||
Directory.SetCurrentDirectory(gamePath);
|
||||
|
||||
var instanceName = configuration.GetValue("instanceName", "Instance");
|
||||
string instancePath;
|
||||
@@ -105,6 +103,8 @@ namespace Torch.Server
|
||||
instancePath = Directory.CreateDirectory(instanceName!).FullName;
|
||||
}
|
||||
|
||||
Directory.SetCurrentDirectory(gamePath);
|
||||
|
||||
return new ApplicationContext(new(workingDir), new(gamePath), new(binDir),
|
||||
new(instancePath), instanceName, isService);
|
||||
}
|
||||
|
231
Torch/Managers/ScriptCompilationManager.cs
Normal file
231
Torch/Managers/ScriptCompilationManager.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Emit;
|
||||
using Sandbox.Game.Entities.Blocks;
|
||||
using Sandbox.Game.EntityComponents;
|
||||
using Sandbox.ModAPI;
|
||||
using Sandbox.ModAPI.Ingame;
|
||||
using Torch.API;
|
||||
using Torch.Utils;
|
||||
using VRage;
|
||||
using VRage.ModAPI;
|
||||
using VRage.Scripting;
|
||||
|
||||
namespace Torch.Managers;
|
||||
|
||||
public class ScriptCompilationManager : Manager
|
||||
{
|
||||
[ReflectedSetter(Name = "m_terminationReason")]
|
||||
private static Action<MyProgrammableBlock, MyProgrammableBlock.ScriptTerminationReason> TerminationReasonSetter = null!;
|
||||
|
||||
[ReflectedGetter(Name = "ScriptComponent")]
|
||||
private static Func<MyProgrammableBlock, MyIngameScriptComponent> ScriptComponentGetter = null!;
|
||||
|
||||
[ReflectedMethod]
|
||||
private static Action<MyProgrammableBlock, string> SetDetailedInfo = null!;
|
||||
|
||||
[ReflectedSetter(Name = "m_instance")]
|
||||
private static Action<MyProgrammableBlock, IMyGridProgram> InstanceSetter = null!;
|
||||
|
||||
[ReflectedSetter(Name = "m_assembly")]
|
||||
private static Action<MyProgrammableBlock, Assembly> AssemblySetter = null!;
|
||||
|
||||
[ReflectedMethod]
|
||||
private static Func<MyProgrammableBlock, Assembly, IEnumerable<string>, string, bool> CreateInstance = null!;
|
||||
|
||||
[ReflectedGetter(Name = "m_compilerErrors")]
|
||||
private static Func<MyProgrammableBlock, List<string>> CompilerErrorsGetter = null!;
|
||||
|
||||
[ReflectedGetter(Name = "m_modApiWhitelistDiagnosticAnalyzer")]
|
||||
private static Func<MyScriptCompiler, DiagnosticAnalyzer> ModWhitelistAnalyzer = null!;
|
||||
|
||||
[ReflectedGetter(Name = "m_ingameWhitelistDiagnosticAnalyzer")]
|
||||
private static Func<MyScriptCompiler, DiagnosticAnalyzer> ScriptWhitelistAnalyzer = null!;
|
||||
|
||||
[ReflectedMethod]
|
||||
private static Func<MyScriptCompiler, CSharpCompilation, SyntaxTree, int, SyntaxTree> InjectMod = null!;
|
||||
|
||||
[ReflectedMethod]
|
||||
private static Func<MyScriptCompiler, CSharpCompilation, SyntaxTree, SyntaxTree> InjectInstructionCounter = null!;
|
||||
|
||||
[ReflectedMethod]
|
||||
private static Func<MyScriptCompiler, CompilationWithAnalyzers, EmitResult, List<Message>, bool, Task<bool>> EmitDiagnostics = null!;
|
||||
|
||||
[ReflectedMethod]
|
||||
private static Func<MyScriptCompiler, MyApiTarget, string, IList<SyntaxTree>, string, Task> WriteDiagnostics = null!;
|
||||
|
||||
[ReflectedMethod(Name = "WriteDiagnostics")]
|
||||
private static Func<MyScriptCompiler, MyApiTarget, string, IEnumerable<Message>, bool, Task> WriteDiagnostics2 = null!;
|
||||
|
||||
[ReflectedGetter(Name = "m_metadataReferences")]
|
||||
private static Func<MyScriptCompiler, List<MetadataReference>> MetadataReferencesGetter = null!;
|
||||
|
||||
private readonly ConditionalWeakTable<MyProgrammableBlock, AssemblyLoadContext> _contexts = new();
|
||||
|
||||
public ScriptCompilationManager(ITorchBase torchInstance) : base(torchInstance)
|
||||
{
|
||||
}
|
||||
|
||||
public async void CompileAsync(MyProgrammableBlock block, string program, string storage, bool instantiate)
|
||||
{
|
||||
TerminationReasonSetter(block, MyProgrammableBlock.ScriptTerminationReason.None);
|
||||
|
||||
var component = ScriptComponentGetter(block);
|
||||
component.NextUpdate = UpdateType.None;
|
||||
component.NeedsUpdate = MyEntityUpdateEnum.NONE;
|
||||
|
||||
try
|
||||
{
|
||||
if (_contexts.TryGetValue(block, out var context))
|
||||
{
|
||||
InstanceSetter(block, null);
|
||||
AssemblySetter(block, null);
|
||||
context!.Unload();
|
||||
}
|
||||
|
||||
_contexts.AddOrUpdate(block, context = new AssemblyLoadContext(null, true));
|
||||
|
||||
var messages = new List<Message>();
|
||||
var assembly = await CompileAsync(context, MyApiTarget.Ingame,
|
||||
$"pb_{block.EntityId}_{Random.Shared.NextInt64()}",
|
||||
new[]
|
||||
{
|
||||
MyVRage.Platform.Scripting.GetIngameScript(
|
||||
program, "Program", nameof(MyGridProgram))
|
||||
}, messages, $"PB: {block.DisplayName} ({block.EntityId})");
|
||||
AssemblySetter(block, assembly);
|
||||
var errors = CompilerErrorsGetter(block);
|
||||
|
||||
errors.Clear();
|
||||
errors.AddRange(messages.Select(b => b.Text));
|
||||
|
||||
if (instantiate)
|
||||
await Torch.InvokeAsync(() => CreateInstance(block, assembly, errors, storage));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await Torch.InvokeAsync(() => SetDetailedInfo(block, e.ToString()));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Assembly> CompileAsync(AssemblyLoadContext context, MyApiTarget target, string assemblyName, IEnumerable<Script> scripts, List<Message> messages, string friendlyName, bool enableDebugInformation = false)
|
||||
{
|
||||
friendlyName ??= "<No Name>";
|
||||
Func<CSharpCompilation, SyntaxTree, SyntaxTree> syntaxTreeInjector;
|
||||
DiagnosticAnalyzer whitelistAnalyzer;
|
||||
switch (target)
|
||||
{
|
||||
case MyApiTarget.None:
|
||||
whitelistAnalyzer = null;
|
||||
syntaxTreeInjector = null;
|
||||
break;
|
||||
case MyApiTarget.Mod:
|
||||
{
|
||||
var modId = MyModWatchdog.AllocateModId(friendlyName);
|
||||
whitelistAnalyzer = ModWhitelistAnalyzer(MyScriptCompiler.Static);
|
||||
syntaxTreeInjector = (c, st) => InjectMod(MyScriptCompiler.Static, c, st, modId);
|
||||
break;
|
||||
}
|
||||
case MyApiTarget.Ingame:
|
||||
syntaxTreeInjector = (c, t) => InjectInstructionCounter(MyScriptCompiler.Static, c, t);
|
||||
whitelistAnalyzer = ScriptWhitelistAnalyzer(MyScriptCompiler.Static);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(target), target, "Invalid compilation target");
|
||||
}
|
||||
var compilation = CreateCompilation(assemblyName, scripts);
|
||||
|
||||
await WriteDiagnostics(MyScriptCompiler.Static, target, assemblyName, compilation.SyntaxTrees, null).ConfigureAwait(false);
|
||||
var injectionFailed = false;
|
||||
var compilationWithoutInjection = compilation;
|
||||
if (syntaxTreeInjector != null)
|
||||
{
|
||||
SyntaxTree[] newSyntaxTrees = null;
|
||||
try
|
||||
{
|
||||
var syntaxTrees = compilation.SyntaxTrees;
|
||||
if (syntaxTrees.Length == 1)
|
||||
{
|
||||
newSyntaxTrees = new[] { syntaxTreeInjector(compilation, syntaxTrees[0]) };
|
||||
}
|
||||
else
|
||||
{
|
||||
newSyntaxTrees = await Task
|
||||
.WhenAll(syntaxTrees.Select(
|
||||
x => Task.Run(() => syntaxTreeInjector(compilation, x))))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
injectionFailed = true;
|
||||
}
|
||||
if (!injectionFailed)
|
||||
{
|
||||
await WriteDiagnostics(MyScriptCompiler.Static, target, assemblyName, newSyntaxTrees, ".injected").ConfigureAwait(false);
|
||||
compilation = compilation.RemoveAllSyntaxTrees().AddSyntaxTrees(newSyntaxTrees);
|
||||
}
|
||||
}
|
||||
CompilationWithAnalyzers analyticCompilation = null;
|
||||
if (whitelistAnalyzer != null)
|
||||
{
|
||||
analyticCompilation = compilation.WithAnalyzers(ImmutableArray.Create(whitelistAnalyzer));
|
||||
compilation = (CSharpCompilation)analyticCompilation.Compilation;
|
||||
}
|
||||
|
||||
using var assemblyStream = new MemoryStream();
|
||||
|
||||
var emitResult = compilation.Emit(assemblyStream);
|
||||
var success = emitResult.Success;
|
||||
var myBlacklistSyntaxVisitor = new MyBlacklistSyntaxVisitor();
|
||||
foreach (var syntaxTree in compilation.SyntaxTrees)
|
||||
{
|
||||
myBlacklistSyntaxVisitor.SetSemanticModel(compilation.GetSemanticModel(syntaxTree, false));
|
||||
myBlacklistSyntaxVisitor.Visit(await syntaxTree.GetRootAsync());
|
||||
}
|
||||
if (myBlacklistSyntaxVisitor.HasAnyResult())
|
||||
{
|
||||
myBlacklistSyntaxVisitor.GetResultMessages(messages);
|
||||
}
|
||||
else
|
||||
{
|
||||
success = await EmitDiagnostics(MyScriptCompiler.Static, analyticCompilation, emitResult, messages, success).ConfigureAwait(false);
|
||||
await WriteDiagnostics2(MyScriptCompiler.Static, target, assemblyName, messages, success).ConfigureAwait(false);
|
||||
assemblyStream.Seek(0, SeekOrigin.Begin);
|
||||
if (injectionFailed) return null;
|
||||
if (success)
|
||||
return context.LoadFromStream(assemblyStream);
|
||||
|
||||
await EmitDiagnostics(MyScriptCompiler.Static, analyticCompilation, compilationWithoutInjection.Emit(assemblyStream), messages, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private readonly CSharpCompilationOptions _compilationOptions = new(OutputKind.DynamicallyLinkedLibrary);
|
||||
private readonly CSharpParseOptions _parseOptions = new(LanguageVersion.CSharp10, DocumentationMode.None);
|
||||
|
||||
private CSharpCompilation CreateCompilation(string assemblyFileName, IEnumerable<Script> scripts)
|
||||
{
|
||||
if (scripts == null)
|
||||
return CSharpCompilation.Create(assemblyFileName, null, MetadataReferencesGetter(MyScriptCompiler.Static),
|
||||
_compilationOptions);
|
||||
|
||||
var parseOptions = _parseOptions.WithPreprocessorSymbols(MyScriptCompiler.Static.ConditionalCompilationSymbols);
|
||||
var enumerable = scripts.Select(s => CSharpSyntaxTree.ParseText(s.Code, parseOptions, s.Name, Encoding.UTF8));
|
||||
|
||||
return CSharpCompilation.Create(assemblyFileName, enumerable, MetadataReferencesGetter(MyScriptCompiler.Static), _compilationOptions);
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
#if false
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
@@ -50,4 +51,5 @@ internal static class GcCollectPatch
|
||||
yield return instruction;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
33
Torch/Patches/ProgramableBlockPatch.cs
Normal file
33
Torch/Patches/ProgramableBlockPatch.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Sandbox.Game.Entities.Blocks;
|
||||
using Sandbox.Game.World;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Managers;
|
||||
using Torch.Managers.PatchManager;
|
||||
using Torch.Utils;
|
||||
|
||||
namespace Torch.Patches;
|
||||
|
||||
[PatchShim]
|
||||
public static class ProgramableBlockPatch
|
||||
{
|
||||
[ReflectedMethodInfo(typeof(MyProgrammableBlock), "Compile")]
|
||||
private static MethodInfo CompileMethod = null!;
|
||||
|
||||
public static void Patch(PatchContext context)
|
||||
{
|
||||
context.GetPattern(CompileMethod).AddPrefix();
|
||||
}
|
||||
|
||||
private static bool Prefix(MyProgrammableBlock __instance, string program, string storage, bool instantiate)
|
||||
{
|
||||
if (!MySession.Static.EnableIngameScripts || __instance.CubeGrid is {IsPreview: true} or {CreatePhysics: false})
|
||||
return false;
|
||||
|
||||
#pragma warning disable CS0618
|
||||
TorchBase.Instance.CurrentSession.Managers.GetManager<ScriptCompilationManager>().CompileAsync(__instance, program, storage, instantiate);
|
||||
#pragma warning restore CS0618
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -92,6 +92,7 @@ namespace Torch
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown if a TorchBase instance already exists.</exception>
|
||||
protected TorchBase(ITorchConfig config)
|
||||
{
|
||||
@@ -133,6 +134,7 @@ namespace Torch
|
||||
#pragma warning disable CS0618
|
||||
Managers.AddManager(Plugins);
|
||||
#pragma warning restore CS0618
|
||||
Managers.AddManager(new ScriptCompilationManager(this));
|
||||
TorchAPI.Instance = this;
|
||||
|
||||
GameStateChanged += (game, state) =>
|
||||
@@ -146,6 +148,20 @@ namespace Torch
|
||||
PatchManager.CommitInternal();
|
||||
}
|
||||
};
|
||||
|
||||
var harmonyLog = LogManager.GetLogger("HarmonyX");
|
||||
HarmonyLib.Tools.Logger.ChannelFilter = HarmonyLib.Tools.Logger.LogChannel.Debug;
|
||||
HarmonyLib.Tools.Logger.MessageReceived += (_, args) => harmonyLog.Log(args.LogChannel switch
|
||||
{
|
||||
HarmonyLib.Tools.Logger.LogChannel.None => LogLevel.Off,
|
||||
HarmonyLib.Tools.Logger.LogChannel.Info => LogLevel.Info,
|
||||
HarmonyLib.Tools.Logger.LogChannel.IL => LogLevel.Trace,
|
||||
HarmonyLib.Tools.Logger.LogChannel.Warn => LogLevel.Warn,
|
||||
HarmonyLib.Tools.Logger.LogChannel.Error => LogLevel.Error,
|
||||
HarmonyLib.Tools.Logger.LogChannel.Debug => LogLevel.Debug,
|
||||
HarmonyLib.Tools.Logger.LogChannel.All => LogLevel.Debug,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
}, args.Message);
|
||||
}
|
||||
|
||||
[Obsolete("Prefer using Managers.GetManager for global managers")]
|
||||
|
Reference in New Issue
Block a user