From 3543b87561086c870a5416b5566f68a748fdaf8b Mon Sep 17 00:00:00 2001 From: Devin Bidwell Date: Sat, 27 Dec 2025 16:03:36 -0700 Subject: [PATCH 1/2] wip --- ModData/About/About.xml | 2 +- csharp_mod/Formatter.cs | 27 +++++++++++---------------- csharp_mod/Marshal.cs | 9 ++++++++- csharp_mod/Plugin.cs | 19 ++++++++++++++----- rust_compiler/Cargo.lock | 2 +- rust_compiler/Cargo.toml | 2 +- 6 files changed, 36 insertions(+), 25 deletions(-) diff --git a/ModData/About/About.xml b/ModData/About/About.xml index 4f3f624..b0025db 100644 --- a/ModData/About/About.xml +++ b/ModData/About/About.xml @@ -2,7 +2,7 @@ Slang JoeDiertay - 0.4.4 + 0.4.5 [h1]Slang: High-Level Programming for Stationeers[/h1] diff --git a/csharp_mod/Formatter.cs b/csharp_mod/Formatter.cs index 890b79c..2335013 100644 --- a/csharp_mod/Formatter.cs +++ b/csharp_mod/Formatter.cs @@ -165,18 +165,15 @@ public class SlangFormatter : ICodeFormatter return; } - var (compilationSuccess, compiled, sourceMap) = await Task.Run( - () => - { - var successful = Marshal.CompileFromString( - inputSrc, - out var compiled, - out var sourceMap - ); - return (successful, compiled, sourceMap); - }, - cancellationToken - ); + var (compilationSuccess, compiled, sourceMap) = await Task.Run(() => + { + var successful = Marshal.CompileFromString( + inputSrc, + out var compiled, + out var sourceMap + ); + return (successful, compiled, sourceMap); + }); if (compilationSuccess) { @@ -192,6 +189,7 @@ public class SlangFormatter : ICodeFormatter } } + // This runs on the main thread private void UpdateIc10Formatter() { var tab = Editor.ParentTab; @@ -215,14 +213,11 @@ public class SlangFormatter : ICodeFormatter entry.SlangSource.StartLine == caretPos || entry.SlangSource.EndLine == caretPos ); - // extract the current "context" of the ic10 compilation. The current Slang source line - // should be directly next to the compiled IC10 source line, and we should highlight the - // IC10 code that directly represents the Slang source - Ic10Editor.ResetCode(ic10CompilationResult); if (lines.Count() < 1) { + L.Debug($"SourceMap count: {ic10SourceMap.Count}"); Ic10Editor.Selection = new TextRange { End = new TextPosition { Col = 0, Line = 0 }, diff --git a/csharp_mod/Marshal.cs b/csharp_mod/Marshal.cs index d5fb834..84e1982 100644 --- a/csharp_mod/Marshal.cs +++ b/csharp_mod/Marshal.cs @@ -67,7 +67,9 @@ public static class Marshal try { _libraryHandle = LoadLibrary(ExtractNativeLibrary(Ffi.RustLib)); + L.Debug("Rust DLL loaded successfully. Enjoy native speed compilations!"); CodeFormatters.RegisterFormatter("Slang", typeof(SlangFormatter), true); + return true; } catch (Exception ex) @@ -91,8 +93,13 @@ public static class Marshal try { - FreeLibrary(_libraryHandle); + CodeFormatters.RegisterFormatter("Slang", typeof(PlainTextFormatter), true); + if (!FreeLibrary(_libraryHandle)) + { + L.Warning("Unable to free Rust library"); + } _libraryHandle = IntPtr.Zero; + L.Debug("Rust DLL library freed"); return true; } catch (Exception ex) diff --git a/csharp_mod/Plugin.cs b/csharp_mod/Plugin.cs index 8b5f08a..662a0fe 100644 --- a/csharp_mod/Plugin.cs +++ b/csharp_mod/Plugin.cs @@ -40,9 +40,9 @@ namespace Slang { public const string PluginGuid = "com.biddydev.slang"; public const string PluginName = "Slang"; - public const string PluginVersion = "0.4.4"; + public const string PluginVersion = "0.4.5"; - private Harmony? _harmony; + private static Harmony? _harmony; private static Regex? _slangSourceCheck = null; @@ -64,19 +64,28 @@ namespace Slang return SlangSourceCheck.IsMatch(input); } - private void Awake() + public void Awake() { L.SetLogger(Logger); - this._harmony = new Harmony(PluginGuid); + _harmony = new Harmony(PluginGuid); // If we failed to load the compiler, bail from the rest of the patches. It won't matter, // as the compiler itself has failed to load. if (!Marshal.Init()) { + L.Error("Marshal failed to init"); return; } - this._harmony.PatchAll(); + _harmony.PatchAll(); + L.Debug("Ran Harmony patches"); + } + + public void OnDestroy() + { + Marshal.Destroy(); + _harmony?.UnpatchSelf(); + L.Debug("Cleaned up Harmony patches"); } } } diff --git a/rust_compiler/Cargo.lock b/rust_compiler/Cargo.lock index b5bca3c..91769ce 100644 --- a/rust_compiler/Cargo.lock +++ b/rust_compiler/Cargo.lock @@ -930,7 +930,7 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "slang" -version = "0.4.3" +version = "0.4.5" dependencies = [ "anyhow", "clap", diff --git a/rust_compiler/Cargo.toml b/rust_compiler/Cargo.toml index 41b30e7..94ee887 100644 --- a/rust_compiler/Cargo.toml +++ b/rust_compiler/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "slang" -version = "0.4.3" +version = "0.4.5" edition = "2021" [workspace] From de3185115342fa8240fc2ed920b9e63838c45924 Mon Sep 17 00:00:00 2001 From: Devin Bidwell Date: Sat, 27 Dec 2025 22:18:21 -0700 Subject: [PATCH 2/2] Fixed IC10Editor implementation bug where IC10 code would not update after clicking 'Cancel' --- Changelog.md | 11 ++++++ csharp_mod/Formatter.cs | 73 ++++++++++++++++++++++++++------------ csharp_mod/Marshal.cs | 77 ++++++++++++++++++++++++++++++++++++++--- csharp_mod/Plugin.cs | 21 ----------- 4 files changed, 134 insertions(+), 48 deletions(-) diff --git a/Changelog.md b/Changelog.md index a8510fb..7f9a5b2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,16 @@ # Changelog +[0.4.5] + +- Fixed issue where after clicking "Cancel" on the IC10 Editor, the side-by-side + IC10 output would no longer update with highlighting or code updates. +- Added ability to live-reload the mod while developing using the `ScriptEngine` + mod from BepInEx + - This required adding in cleanup code to cleanup references to the Rust DLL + before destroying the mod instance. +- Added BepInEx debug logging. This will ONLY show if you have debug logs + enabled in the BepInEx configuration file. + [0.4.4] - Added Stationpedia docs back after removing all harmony patches from the mod diff --git a/csharp_mod/Formatter.cs b/csharp_mod/Formatter.cs index 2335013..c4eb4e1 100644 --- a/csharp_mod/Formatter.cs +++ b/csharp_mod/Formatter.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using StationeersIC10Editor; -using StationeersIC10Editor.IC10; public class SlangFormatter : ICodeFormatter { @@ -14,8 +13,25 @@ public class SlangFormatter : ICodeFormatter private CancellationTokenSource? _lspCancellationToken; private object _tokenLock = new(); - protected Editor? Ic10Editor = null; - private IC10CodeFormatter iC10CodeFormatter = new IC10CodeFormatter(); + protected Editor? __Ic10Editor = null; + + protected Editor Ic10Editor + { + get + { + if (__Ic10Editor == null) + { + var tab = Editor.ParentTab; + tab.ClearExtraEditors(); + __Ic10Editor = new Editor(Editor.KeyHandler); + Ic10Editor.IsReadOnly = true; + tab.AddEditor(__Ic10Editor); + } + + return __Ic10Editor; + } + } + private string ic10CompilationResult = ""; private List ic10SourceMap = new(); @@ -80,7 +96,7 @@ public class SlangFormatter : ICodeFormatter { if (!Marshal.CompileFromString(RawText, out var compilationResult, out var sourceMap)) { - return "Compilation Error"; + return "# Compilation Error"; } return compilationResult + $"\n{EncodeSource(RawText, SLANG_SRC)}"; @@ -118,6 +134,10 @@ public class SlangFormatter : ICodeFormatter return styledLine; } + /// + /// This handles calling the `HandleLsp` function by creating a new `CancellationToken` and + /// cancelling the current call if applicable. + /// private void HandleCodeChanged() { CancellationToken token; @@ -133,6 +153,11 @@ public class SlangFormatter : ICodeFormatter _ = HandleLsp(inputSrc, token); } + /// + /// Takes a copy of the current source code and sends it to the Rust compiler in a background thread + /// to get diagnostic data. This also handles getting a compilation response of optimized IC10 for the + /// side-by-side IC10Editor to show with sourcemap highlighting. + /// private async Task HandleLsp(string inputSrc, CancellationToken cancellationToken) { try @@ -179,7 +204,7 @@ public class SlangFormatter : ICodeFormatter { ic10CompilationResult = compiled; ic10SourceMap = sourceMap; - UpdateIc10Formatter(); + UpdateIc10Content(Ic10Editor); } } catch (OperationCanceledException) { } @@ -189,23 +214,24 @@ public class SlangFormatter : ICodeFormatter } } - // This runs on the main thread + /// + /// Updates the underlying code in the IC10 Editor, after which will call `UpdateIc10Formatter` to + /// update highlighting of relavent fields. + /// + private void UpdateIc10Content(Editor editor) + { + editor.ResetCode(ic10CompilationResult); + UpdateIc10Formatter(); + } + + // This runs on the main thread. This function ONLY updates the highlighting of the IC10 code. + // If you need to update the code in the editor itself, you should use `UpdateIc10Content`. private void UpdateIc10Formatter() { - var tab = Editor.ParentTab; - if (Ic10Editor == null) - { - iC10CodeFormatter = new IC10CodeFormatter(); - Ic10Editor = new Editor(Editor.KeyHandler); - Ic10Editor.IsReadOnly = true; - iC10CodeFormatter.Editor = Ic10Editor; - } - - if (tab.Editors.Count < 2) - { - tab.AddEditor(Ic10Editor); - } - + // Bail if our backing field is null. We don't want to set the field in this function. It + // runs way too much and we might not even have source code to use. + if (__Ic10Editor == null) + return; var caretPos = Editor.CaretPos.Line; // get the slang sourceMap at the current editor line @@ -217,7 +243,6 @@ public class SlangFormatter : ICodeFormatter if (lines.Count() < 1) { - L.Debug($"SourceMap count: {ic10SourceMap.Count}"); Ic10Editor.Selection = new TextRange { End = new TextPosition { Col = 0, Line = 0 }, @@ -240,7 +265,11 @@ public class SlangFormatter : ICodeFormatter }; } - // This runs on the Main Thread + /// + /// Takes diagnostics from the Rust FFI compiler and applies it as semantic tokens to the + /// source in this editor. + /// This runs on the Main Thread + /// private void ApplyDiagnostics(Dictionary> dict) { HashSet linesToRefresh; diff --git a/csharp_mod/Marshal.cs b/csharp_mod/Marshal.cs index 84e1982..672f1ac 100644 --- a/csharp_mod/Marshal.cs +++ b/csharp_mod/Marshal.cs @@ -198,9 +198,9 @@ public static class Marshal Assembly assembly = Assembly.GetExecutingAssembly(); - using (Stream stream = assembly.GetManifestResourceStream(libName)) + using (Stream resourceStream = assembly.GetManifestResourceStream(libName)) { - if (stream == null) + if (resourceStream == null) { L.Error( $"{libName} not found. This means it was not embedded in the mod. Please contact the mod author!" @@ -208,18 +208,85 @@ public static class Marshal return ""; } + // Check if file exists and contents are identical to avoid overwriting locked files + if (File.Exists(destinationPath)) + { + try + { + using ( + FileStream fileStream = new FileStream( + destinationPath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite + ) + ) + { + if (resourceStream.Length == fileStream.Length) + { + if (StreamsContentsAreEqual(resourceStream, fileStream)) + { + L.Debug( + $"DLL {libName} already exists and matches. Skipping extraction." + ); + return destinationPath; + } + } + } + } + catch (IOException ex) + { + L.Warning( + $"Could not verify existing {libName}, attempting overwrite. {ex.Message}" + ); + } + } + + resourceStream.Position = 0; + + // Attempt to overwrite if missing or different try { using (FileStream fileStream = new FileStream(destinationPath, FileMode.Create)) { - stream.CopyTo(fileStream); + resourceStream.CopyTo(fileStream); } return destinationPath; } catch (IOException e) { - L.Warning($"Could not overwrite {libName} (it might be in use): {e.Message}"); - return ""; + // If we fail here, the file is likely locked. + // However, if we are here, it means the file is DIFFERENT or we couldn't read it. + // As a fallback for live-reload, we can try returning the path anyway + // assuming the existing locked file might still work. + L.Warning( + $"Could not overwrite {libName} (it might be in use): {e.Message}. Attempting to use existing file." + ); + return destinationPath; + } + } + } + + private static bool StreamsContentsAreEqual(Stream stream1, Stream stream2) + { + const int bufferSize = 4096; + byte[] buffer1 = new byte[bufferSize]; + byte[] buffer2 = new byte[bufferSize]; + + while (true) + { + int count1 = stream1.Read(buffer1, 0, bufferSize); + int count2 = stream2.Read(buffer2, 0, bufferSize); + + if (count1 != count2) + return false; + if (count1 == 0) + return true; + + for (int i = 0; i < count1; i++) + { + if (buffer1[i] != buffer2[i]) + return false; } } } diff --git a/csharp_mod/Plugin.cs b/csharp_mod/Plugin.cs index 662a0fe..b719291 100644 --- a/csharp_mod/Plugin.cs +++ b/csharp_mod/Plugin.cs @@ -1,4 +1,3 @@ -using System.Text.RegularExpressions; using BepInEx; using HarmonyLib; @@ -44,26 +43,6 @@ namespace Slang private static Harmony? _harmony; - private static Regex? _slangSourceCheck = null; - - private static Regex SlangSourceCheck - { - get - { - if (_slangSourceCheck is null) - { - _slangSourceCheck = new Regex(@"[;{}()]|\b(let|fn|device)\b|\/\/"); - } - - return _slangSourceCheck; - } - } - - public static bool IsSlangSource(ref string input) - { - return SlangSourceCheck.IsMatch(input); - } - public void Awake() { L.SetLogger(Logger);