refactor mod to account for changes in the IC10Editor mod interface

This commit is contained in:
2025-11-29 12:42:07 -07:00
parent 502c60d45e
commit 18fbf26dae
8 changed files with 409 additions and 297 deletions

View File

@@ -1,66 +1,103 @@
namespace Slang;
using System;
using System.Text;
using StationeersIC10Editor;
namespace Slang
public static unsafe class SlangExtensions
{
public static unsafe class SlangExtensions
/**
* <summary>
* This is a helper method to convert a Rust struct for a string pointer
* into a C# style string.
* </summary>
*/
public static string AsString(this Vec_uint8_t vec)
{
/**
* <summary>
* This is a helper method to convert a Rust struct for a string pointer
* into a C# style string.
* </summary>
*/
public static string AsString(this Vec_uint8_t vec)
if (vec.ptr == null || vec.len == UIntPtr.Zero)
{
if (vec.ptr == null || vec.len == UIntPtr.Zero)
{
return string.Empty;
}
// Rust strings are UTF-8. Read bytes from raw pointer.
var toReturn = Encoding.UTF8.GetString(vec.ptr, (int)vec.len);
return toReturn;
return string.Empty;
}
/**
* <summary>This will free a Rust string struct. Because this is a pointer to a struct, this memory
* is managed by Rust, therefor it must be freed by Rust
* </summary>
*/
public static void Drop(this Vec_uint8_t vec)
// Rust strings are UTF-8. Read bytes from raw pointer.
var toReturn = Encoding.UTF8.GetString(vec.ptr, (int)vec.len);
return toReturn;
}
/**
* <summary>This will free a Rust string struct. Because this is a pointer to a struct, this memory
* is managed by Rust, therefor it must be freed by Rust
* </summary>
*/
public static void Drop(this Vec_uint8_t vec)
{
Ffi.free_string(vec);
}
/**
* <summary>This helper converts a Rust vec to a C# List. This handles freeing the
* Rust allocation after the List is created, there is no need to Drop this memory.
* </summary>
*/
public static Line ToLine(this Vec_FfiToken_t vec, string sourceText)
{
var list = new Line(sourceText);
var currentPtr = vec.ptr;
// Iterate through the raw memory array
for (int i = 0; i < (int)vec.len; i++)
{
Ffi.free_string(vec);
var token = currentPtr[i];
var color = GetColorForKind(token.token_kind);
int colIndex = token.column;
if (colIndex < 0)
colIndex = 0;
var semanticToken = new SemanticToken(
0,
colIndex,
token.length,
color,
token.token_kind
);
string errMsg = token.error.AsString();
if (!string.IsNullOrEmpty(errMsg))
{
semanticToken.IsError = true;
semanticToken.Data = errMsg;
semanticToken.Color = ICodeFormatter.ColorError;
}
list.AddToken(semanticToken);
}
/**
* <summary>This helper converts a Rust vec to a C# List. This handles freeing the
* Rust allocation after the List is created, there is no need to Drop this memory.
* </summary>
*/
public static Line AsList(this Vec_FfiToken_t vec)
Ffi.free_ffi_token_vec(vec);
return list;
}
private static uint GetColorForKind(uint kind)
{
switch (kind)
{
var list = new Line();
list.Capacity = (int)vec.len;
var currentPtr = vec.ptr;
// Iterate through the raw memory array
for (int i = 0; i < (int)vec.len; i++)
{
// Dereference pointer to get the struct at index i
FfiToken_t token = currentPtr[i];
var newToken = new Token(token.text.AsString(), token.column);
list.Add(newToken);
}
Ffi.free_ffi_token_vec(vec);
return list;
case 1:
return SlangFormatter.ColorInstruction; // Keyword
case 2:
return SlangFormatter.ColorDefault; // Identifier
case 3:
return SlangFormatter.ColorNumber; // Number
case 4:
return SlangFormatter.ColorString; // String
case 5:
return SlangFormatter.ColorInstruction; // Boolean
case 6:
return SlangFormatter.ColorDefault; // Symbol
default:
return SlangFormatter.ColorDefault;
}
}
}

View File

@@ -83,17 +83,17 @@ public unsafe partial class Ffi {
slice_ref_uint16_t input);
}
[StructLayout(LayoutKind.Sequential, Size = 104)]
[StructLayout(LayoutKind.Sequential, Size = 64)]
public unsafe struct FfiToken_t {
public Vec_uint8_t text;
public Vec_uint8_t tooltip;
public Vec_uint8_t error;
public Vec_uint8_t status;
public Int32 column;
public Int32 length;
public UInt32 token_kind;
}
/// <summary>

View File

@@ -1,12 +1,24 @@
namespace Slang;
using StationeersIC10Editor;
namespace Slang
public class SlangFormatter : ICodeFormatter
{
public class SlangFormatter : ICodeFormatter
public static readonly uint ColorInstruction = ColorFromHTML("#ffff00");
public static readonly uint ColorString = ColorFromHTML("#ce9178");
public override Line ParseLine(string line)
{
public override Line ParseLine(string line)
return Marshal.TokenizeLine(line);
}
public override string Compile()
{
if (Marshal.CompileFromString(this.Lines.RawText, out string compiled))
{
return Marshal.TokenizeLine(line);
return compiled;
}
return string.Empty;
}
}

View File

@@ -1,59 +1,157 @@
namespace Slang;
using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using StationeersIC10Editor;
namespace Slang
public static class Marshal
{
public static class Marshal
{
public static unsafe Line TokenizeLine(string source)
{
if (String.IsNullOrEmpty(source))
{
return new Line();
}
private static IntPtr _libraryHandle = IntPtr.Zero;
fixed (char* ptrString = source)
{
var input = new slice_ref_uint16_t
{
ptr = (ushort*)ptrString,
len = (UIntPtr)source.Length,
};
return Ffi.tokenize_line(input).AsList();
}
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi)]
private static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
[DllImport("kernel32", SetLastError = true)]
private static extern bool FreeLibrary(IntPtr hModule);
private static bool EnsureLibLoaded()
{
if (_libraryHandle != IntPtr.Zero)
{
return true;
}
public static unsafe bool CompileFromString(string inputString, out string compiledString)
try
{
if (String.IsNullOrEmpty(inputString))
_libraryHandle = LoadLibrary(ExtractNativeLibrary(Ffi.RustLib));
CodeFormatters.RegisterFormatter("Slang", () => new SlangFormatter(), true);
return true;
}
catch (Exception ex)
{
L.Error($"Failed to init slang compiler: {ex.Message}");
return false;
}
}
public static bool Init()
{
return EnsureLibLoaded();
}
public static bool Destroy()
{
if (_libraryHandle == IntPtr.Zero)
{
return true;
}
try
{
FreeLibrary(_libraryHandle);
_libraryHandle = IntPtr.Zero;
return true;
}
catch (Exception ex)
{
L.Warning($"Unable to free handle to slang compiler's dll. {ex.Message}");
return false;
}
}
public static unsafe Line TokenizeLine(string source)
{
if (String.IsNullOrEmpty(source))
{
return new Line(source);
}
if (!EnsureLibLoaded())
{
return new Line(source);
}
fixed (char* ptrString = source)
{
var input = new slice_ref_uint16_t
{
compiledString = String.Empty;
return false;
ptr = (ushort*)ptrString,
len = (UIntPtr)source.Length,
};
return Ffi.tokenize_line(input).ToLine(source);
}
}
public static unsafe bool CompileFromString(string inputString, out string compiledString)
{
if (String.IsNullOrEmpty(inputString))
{
compiledString = String.Empty;
return false;
}
if (!EnsureLibLoaded())
{
compiledString = String.Empty;
return false;
}
fixed (char* ptrString = inputString)
{
var input = new slice_ref_uint16_t
{
ptr = (ushort*)ptrString,
len = (UIntPtr)inputString.Length,
};
var result = Ffi.compile_from_string(input);
try
{
if ((ulong)result.len < 1)
{
compiledString = String.Empty;
return false;
}
compiledString = result.AsString();
return true;
}
finally
{
result.Drop();
}
}
}
private static string ExtractNativeLibrary(string libName)
{
string destinationPath = Path.Combine(Path.GetTempPath(), libName);
Assembly assembly = Assembly.GetExecutingAssembly();
using (Stream stream = assembly.GetManifestResourceStream(libName))
{
if (stream == null)
{
L.Error(
$"{libName} not found. This means it was not embedded in the mod. Please contact the mod author!"
);
return "";
}
fixed (char* ptrString = inputString)
try
{
var input = new slice_ref_uint16_t
using (FileStream fileStream = new FileStream(destinationPath, FileMode.Create))
{
ptr = (ushort*)ptrString,
len = (UIntPtr)inputString.Length,
};
var result = Ffi.compile_from_string(input);
try
{
if ((ulong)result.len < 1)
{
compiledString = String.Empty;
return false;
}
compiledString = result.AsString();
return true;
}
finally
{
result.Drop();
stream.CopyTo(fileStream);
}
return destinationPath;
}
catch (IOException e)
{
L.Warning($"Could not overwrite {libName} (it might be in use): {e.Message}");
return "";
}
}
}

View File

@@ -1,9 +1,6 @@
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using BepInEx;
using HarmonyLib;
using StationeersIC10Editor;
namespace Slang
{
@@ -44,6 +41,8 @@ namespace Slang
public const string PluginGuid = "com.biddydev.slang";
public const string PluginName = "Slang";
private Harmony? _harmony;
private static Regex? _slangSourceCheck = null;
private static Regex SlangSourceCheck
@@ -89,44 +88,28 @@ namespace Slang
private void Awake()
{
L.SetLogger(Logger);
this._harmony = new Harmony(PluginGuid);
L.Info("slang loaded");
if (ExtractNativeDll(Ffi.RustLib))
// 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())
{
var harmony = new Harmony(PluginGuid);
harmony.PatchAll();
CodeFormatters.RegisterFormatter("slang", () => new SlangFormatter(), true);
return;
}
this._harmony.PatchAll();
}
private bool ExtractNativeDll(string fileName)
private void OnDestroy()
{
string destinationPath = Path.Combine(Path.GetDirectoryName(Info.Location), fileName);
Assembly assembly = Assembly.GetExecutingAssembly();
using (Stream stream = assembly.GetManifestResourceStream(fileName))
if (Marshal.Destroy())
{
if (stream == null)
{
L.Error(
$"{Ffi.RustLib} not found. This means it was not embedded in the mod. Please contact the mod author!"
);
return false;
}
try
{
using (FileStream fileStream = new FileStream(destinationPath, FileMode.Create))
{
stream.CopyTo(fileStream);
}
return true;
}
catch (IOException e)
{
L.Warning($"Could not overwrite {fileName} (it might be in use): {e.Message}");
return false;
}
L.Info("FFI references cleaned up.");
}
if (this._harmony is not null)
{
this._harmony.UnpatchSelf();
}
}
}