From de3185115342fa8240fc2ed920b9e63838c45924 Mon Sep 17 00:00:00 2001 From: Devin Bidwell Date: Sat, 27 Dec 2025 22:18:21 -0700 Subject: [PATCH] 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);