diff --git a/.gitignore b/.gitignore index 0c2f875..78b5f00 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ release csharp_mod/bin obj ref +.envrc diff --git a/build.sh b/build.sh index 533eb3f..0bbc5a5 100755 --- a/build.sh +++ b/build.sh @@ -38,8 +38,8 @@ echo "--------------------" RUST_WIN_EXE="$RUST_DIR/target/x86_64-pc-windows-gnu/release/slang.exe" RUST_LINUX_BIN="$RUST_DIR/target/x86_64-unknown-linux-gnu/release/slang" -CHARP_DLL="$CSHARP_DIR/bin/Release/net46/StationeersSlang.dll" -CHARP_PDB="$CSHARP_DIR/bin/Release/net46/StationeersSlang.pdb" +CHARP_DLL="$CSHARP_DIR/bin/Release/net48/StationeersSlang.dll" +CHARP_PDB="$CSHARP_DIR/bin/Release/net48/StationeersSlang.pdb" # Check if the release dir exists, if not: create it. if [[ ! -d "$RELEASE_DIR" ]]; then diff --git a/csharp_mod/Formatter.cs b/csharp_mod/Formatter.cs index 29c1822..973351a 100644 --- a/csharp_mod/Formatter.cs +++ b/csharp_mod/Formatter.cs @@ -14,11 +14,7 @@ public class SlangFormatter : ICodeFormatter public override string Compile() { - if (Marshal.CompileFromString(this.Lines.RawText, out string compiled)) - { - return compiled; - } - - return string.Empty; + L.Info("ICodeFormatter attempted to compile source code."); + return this.Lines.RawText; } } diff --git a/csharp_mod/GlobalCode.cs b/csharp_mod/GlobalCode.cs new file mode 100644 index 0000000..185e904 --- /dev/null +++ b/csharp_mod/GlobalCode.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; + +namespace Slang; + +public static class GlobalCode +{ + public const string SLANG_REF = "#SLANG_REF:"; + public const string SLANG_SRC = "#SLANG_SRC:"; + + // This is a Dictionary of ENCODED source code, compressed + // so that save file data is smaller + private static Dictionary codeDict = new(); + + public static void ClearCache() + { + codeDict.Clear(); + } + + public static string GetSource(Guid reference) + { + if (!codeDict.ContainsKey(reference)) + { + return string.Empty; + } + + return DecodeSource(codeDict[reference]); + } + + public static void SetSource(Guid reference, string source) + { + codeDict[reference] = EncodeSource(source); + } + + public static string? GetEncoded(Guid reference) + { + if (codeDict.ContainsKey(reference)) + return null; + + return codeDict[reference]; + } + + public static void SetEncoded(Guid reference, string encodedSource) + { + if (codeDict.ContainsKey(reference)) + { + codeDict[reference] = encodedSource; + } + else + { + codeDict.Add(reference, encodedSource); + } + } + + private static string EncodeSource(string source) + { + if (string.IsNullOrEmpty(source)) + { + return ""; + } + + byte[] bytes = Encoding.UTF8.GetBytes(source); + + using (var memoryStream = new MemoryStream()) + { + using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Compress)) + { + gzipStream.Write(bytes, 0, bytes.Length); + } + return Convert.ToBase64String(memoryStream.ToArray()); + } + } + + private static string DecodeSource(string source) + { + if (string.IsNullOrEmpty(source)) + { + return ""; + } + + byte[] compressedBytes = Convert.FromBase64String(source); + + using (var memoryStream = new MemoryStream(compressedBytes)) + { + using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + { + using (var outputStream = new MemoryStream()) + { + gzipStream.CopyTo(outputStream); + + return Encoding.UTF8.GetString(outputStream.ToArray()); + } + } + } + } +} diff --git a/csharp_mod/Patches.cs b/csharp_mod/Patches.cs index d4c4d82..d5fc8bc 100644 --- a/csharp_mod/Patches.cs +++ b/csharp_mod/Patches.cs @@ -1,41 +1,162 @@ +namespace Slang; + using System; +using Assets.Scripts; +using Assets.Scripts.Objects; +using Assets.Scripts.Objects.Electrical; using Assets.Scripts.Objects.Motherboards; +using Assets.Scripts.UI; using HarmonyLib; -namespace Slang +[HarmonyPatch] +public static class SlangPatches { - [HarmonyPatch] - public static class SlangPatches + [HarmonyPatch( + typeof(ProgrammableChipMotherboard), + nameof(ProgrammableChipMotherboard.InputFinished) + )] + [HarmonyPrefix] + public static void pgmb_InputFinished(ref string result) { - [HarmonyPatch( - typeof(ProgrammableChipMotherboard), - nameof(ProgrammableChipMotherboard.InputFinished) - )] - [HarmonyPrefix] - public static void PGM_InputFinished(ref string result) + // guard to ensure we have valid IC10 before continuing + if ( + !SlangPlugin.IsSlangSource(ref result) + || !Marshal.CompileFromString(result, out string compiled) + || string.IsNullOrEmpty(compiled) + ) { - if (string.IsNullOrEmpty(result) || !SlangPlugin.IsSlangSource(ref result)) - { - return; - } - - L.Debug("Detected Slang source, compiling..."); - - // Compile the Slang source into IC10 - string compiled = SlangPlugin.Compile(result); - - // Ensure that the string is correct - if (string.IsNullOrEmpty(compiled)) - { - return; - } - - var newUuid = Guid.NewGuid().ToString(); - - SlangPlugin.CopySourceToFile(result); - - // Set the result to be the compiled source so the rest of the function can continue as normal - result = compiled; + return; } + + var thisRef = Guid.NewGuid(); + + // Ensure we cache this compiled code for later retreival. + GlobalCode.SetSource(thisRef, result); + + compiled += $"\n{GlobalCode.SLANG_REF}{thisRef}"; + result = compiled; + } + + [HarmonyPatch(typeof(ProgrammableChipMotherboard), nameof(ProgrammableChipMotherboard.OnEdit))] + [HarmonyPrefix] + public static void isc_OnEdit(ProgrammableChipMotherboard __instance) + { + var sourceCode = System.Text.Encoding.UTF8.GetString( + System.Text.Encoding.ASCII.GetBytes(__instance.GetSourceCode()) + ); + + if (string.IsNullOrEmpty(sourceCode)) + { + return; + } + + var tagIndex = sourceCode.LastIndexOf(GlobalCode.SLANG_REF); + + if (tagIndex == -1) + { + // this is not slang managed code + return; + } + + if ( + !Guid.TryParse( + sourceCode.Substring(tagIndex + GlobalCode.SLANG_REF.Length), + out Guid sourceRef + ) + ) + { + // not a valid Guid, not managed by slang + return; + } + + var slangSource = GlobalCode.GetSource(sourceRef); + + if (string.IsNullOrEmpty(slangSource)) + { + // Didn't find that source ref in the global code manager. + return; + } + + __instance.SetSourceCode(slangSource); + } + + [HarmonyPatch(typeof(ProgrammableChip), nameof(ProgrammableChip.SerializeSave))] + [HarmonyPostfix] + public static void pgc_SerializeSave(ProgrammableChip __instance, ref ThingSaveData __result) + { + if (__result is not ProgrammableChipSaveData chipData) + return; + if (string.IsNullOrEmpty(chipData.SourceCode)) + return; + + var firstLine = chipData.SourceCode.Split('\n')[0].Trim(); + + // Check if the file starts with the Reference Tag + if (!firstLine.StartsWith(GlobalCode.SLANG_REF)) + return; + + string guidString = firstLine.Substring(GlobalCode.SLANG_REF.Length).Trim(); + + if (!Guid.TryParse(guidString, out Guid slangRefGuid)) + return; + + var slangEncoded = GlobalCode.GetEncoded(slangRefGuid); + + if (string.IsNullOrEmpty(slangEncoded)) + return; + + // We add 1 to length to remove the '\n' character as well + // Handle edge case where there is only one line + int removeLength = firstLine.Length; + if (chipData.SourceCode.Length > firstLine.Length) + removeLength++; + + var cleanIc10 = chipData.SourceCode.Remove(0, removeLength); + + chipData.SourceCode = $"{cleanIc10}\n{GlobalCode.SLANG_SRC}{slangEncoded}"; + } + + [HarmonyPatch(typeof(ProgrammableChip), nameof(ProgrammableChip.DeserializeSave))] + [HarmonyPrefix] + public static void pgc_DeserializeSave(ref ThingSaveData savedData) + { + // 1. Ensure we are looking at a Programmable Chip + if (savedData is not ProgrammableChipSaveData pcSaveData) + { + return; + } + + // 2. Safety check for null/empty code + if (string.IsNullOrEmpty(pcSaveData.SourceCode)) + return; + + // 3. Check for the #SLANG_SRC: footer we added during serialization + int tagIndex = pcSaveData.SourceCode.LastIndexOf(GlobalCode.SLANG_SRC); + + // If the tag is missing, this is just a normal IC10 script. Do nothing. + if (tagIndex == -1) + return; + + // 4. Extract the Encoded Source (Base64) + // The format in the file is: \n#SLANG_SRC: + string encodedSource = pcSaveData.SourceCode.Substring( + tagIndex + GlobalCode.SLANG_SRC.Length + ); + + // 5. Extract the IC10 Code + // We strip off the tag and the newline we added before it. + // Using TrimEnd() helps clean up that specific newline. + string ic10Code = pcSaveData.SourceCode.Substring(0, tagIndex).TrimEnd(); + + // 6. Generate a new Runtime GUID + // We don't need to persist the GUID from the last session; we just need a key for *this* session. + Guid runtimeGuid = Guid.NewGuid(); + + // 7. Hydrate the Cache + GlobalCode.SetEncoded(runtimeGuid, encodedSource); + + // 8. Rewrite the SourceCode to the "Runtime" format + // This ensures that when the user opens the editor, SlangPlugin.TryRestoreSourceCode matches the header. + pcSaveData.SourceCode = $"{GlobalCode.SLANG_REF} {runtimeGuid}\n{ic10Code}"; } } diff --git a/csharp_mod/Plugin.cs b/csharp_mod/Plugin.cs index 09bf66e..23d24e1 100644 --- a/csharp_mod/Plugin.cs +++ b/csharp_mod/Plugin.cs @@ -1,6 +1,11 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Text; using System.Text.RegularExpressions; using BepInEx; using HarmonyLib; +using LaunchPadBooster; namespace Slang { @@ -41,6 +46,8 @@ namespace Slang public const string PluginGuid = "com.biddydev.slang"; public const string PluginName = "Slang"; + public static Mod MOD = new Mod(PluginName, "0.1.0"); + private Harmony? _harmony; private static Regex? _slangSourceCheck = null; @@ -58,26 +65,26 @@ namespace Slang } } - public static unsafe string Compile(string source) - { - string compiled; - if (Marshal.CompileFromString(source, out compiled)) - { - // TODO: handle saving the original source code - return compiled; - } - else - { - return compiled; - } - } - - /// Take original slang source code and copies it to a file - /// for use in restoring later. + /// + /// Encodes the original slang source code as base64 and uses gzip to compress it, returning the resulting string. /// - public static bool CopySourceToFile(string source) + public static string EncodeSource(string source) { - return true; + if (string.IsNullOrEmpty(source)) + { + return ""; + } + + byte[] bytes = Encoding.UTF8.GetBytes(source); + + using (var memoryStream = new MemoryStream()) + { + using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Compress)) + { + gzipStream.Write(bytes, 0, bytes.Length); + } + return Convert.ToBase64String(memoryStream.ToArray()); + } } public static bool IsSlangSource(ref string input) @@ -89,7 +96,6 @@ namespace Slang { L.SetLogger(Logger); this._harmony = new Harmony(PluginGuid); - L.Info("slang loaded"); // 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. @@ -100,17 +106,5 @@ namespace Slang this._harmony.PatchAll(); } - - private void OnDestroy() - { - if (Marshal.Destroy()) - { - L.Info("FFI references cleaned up."); - } - if (this._harmony is not null) - { - this._harmony.UnpatchSelf(); - } - } } } diff --git a/csharp_mod/stationeersSlang.csproj b/csharp_mod/stationeersSlang.csproj index c281b7f..d613b2e 100644 --- a/csharp_mod/stationeersSlang.csproj +++ b/csharp_mod/stationeersSlang.csproj @@ -1,7 +1,7 @@  - net46 + net48 enable StationeersSlang Slang Compiler Bridge @@ -11,9 +11,8 @@ - /home/dbidwell/.local/share/Steam/steamapps/common/Stationeers/ - $(GameDir)/rocketstation_Data/Managed - $(GameDir)/BepInEx/core + $(STATIONEERS_DIR)/rocketstation_Data/Managed + $(STATIONEERS_DIR)/BepInEx/core @@ -42,14 +41,19 @@ $(ManagedDir)/Assembly-CSharp.dll False - - $(ManagedDir)/Assembly-CSharp-firstpass.dll - False - + ./ref/IC10Editor.dll False + + $(STATIONEERS_DIR)/BepInEx/plugins/StationeersLaunchPad/LaunchPadBooster.dll + False + + + $(STATIONEERS_DIR)/BepInEx/plugins/StationeersLaunchPad/StationeersMods.Interface.dll + False +