Hook into various methods to ensure slang code is populated when editor is open
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ release
|
|||||||
csharp_mod/bin
|
csharp_mod/bin
|
||||||
obj
|
obj
|
||||||
ref
|
ref
|
||||||
|
.envrc
|
||||||
|
|||||||
4
build.sh
4
build.sh
@@ -38,8 +38,8 @@ echo "--------------------"
|
|||||||
|
|
||||||
RUST_WIN_EXE="$RUST_DIR/target/x86_64-pc-windows-gnu/release/slang.exe"
|
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"
|
RUST_LINUX_BIN="$RUST_DIR/target/x86_64-unknown-linux-gnu/release/slang"
|
||||||
CHARP_DLL="$CSHARP_DIR/bin/Release/net46/StationeersSlang.dll"
|
CHARP_DLL="$CSHARP_DIR/bin/Release/net48/StationeersSlang.dll"
|
||||||
CHARP_PDB="$CSHARP_DIR/bin/Release/net46/StationeersSlang.pdb"
|
CHARP_PDB="$CSHARP_DIR/bin/Release/net48/StationeersSlang.pdb"
|
||||||
|
|
||||||
# Check if the release dir exists, if not: create it.
|
# Check if the release dir exists, if not: create it.
|
||||||
if [[ ! -d "$RELEASE_DIR" ]]; then
|
if [[ ! -d "$RELEASE_DIR" ]]; then
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ public class SlangFormatter : ICodeFormatter
|
|||||||
|
|
||||||
public override string Compile()
|
public override string Compile()
|
||||||
{
|
{
|
||||||
if (Marshal.CompileFromString(this.Lines.RawText, out string compiled))
|
L.Info("ICodeFormatter attempted to compile source code.");
|
||||||
{
|
return this.Lines.RawText;
|
||||||
return compiled;
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Empty;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
99
csharp_mod/GlobalCode.cs
Normal file
99
csharp_mod/GlobalCode.cs
Normal file
@@ -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<Guid, string> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +1,162 @@
|
|||||||
|
namespace Slang;
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using Assets.Scripts;
|
||||||
|
using Assets.Scripts.Objects;
|
||||||
|
using Assets.Scripts.Objects.Electrical;
|
||||||
using Assets.Scripts.Objects.Motherboards;
|
using Assets.Scripts.Objects.Motherboards;
|
||||||
|
using Assets.Scripts.UI;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
|
||||||
namespace Slang
|
[HarmonyPatch]
|
||||||
|
public static class SlangPatches
|
||||||
{
|
{
|
||||||
[HarmonyPatch]
|
[HarmonyPatch(
|
||||||
public static class SlangPatches
|
typeof(ProgrammableChipMotherboard),
|
||||||
|
nameof(ProgrammableChipMotherboard.InputFinished)
|
||||||
|
)]
|
||||||
|
[HarmonyPrefix]
|
||||||
|
public static void pgmb_InputFinished(ref string result)
|
||||||
{
|
{
|
||||||
[HarmonyPatch(
|
// guard to ensure we have valid IC10 before continuing
|
||||||
typeof(ProgrammableChipMotherboard),
|
if (
|
||||||
nameof(ProgrammableChipMotherboard.InputFinished)
|
!SlangPlugin.IsSlangSource(ref result)
|
||||||
)]
|
|| !Marshal.CompileFromString(result, out string compiled)
|
||||||
[HarmonyPrefix]
|
|| string.IsNullOrEmpty(compiled)
|
||||||
public static void PGM_InputFinished(ref string result)
|
)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(result) || !SlangPlugin.IsSlangSource(ref result))
|
return;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: <IC10_CODE>\n#SLANG_SRC:<BASE64>
|
||||||
|
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}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using BepInEx;
|
using BepInEx;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
using LaunchPadBooster;
|
||||||
|
|
||||||
namespace Slang
|
namespace Slang
|
||||||
{
|
{
|
||||||
@@ -41,6 +46,8 @@ namespace Slang
|
|||||||
public const string PluginGuid = "com.biddydev.slang";
|
public const string PluginGuid = "com.biddydev.slang";
|
||||||
public const string PluginName = "Slang";
|
public const string PluginName = "Slang";
|
||||||
|
|
||||||
|
public static Mod MOD = new Mod(PluginName, "0.1.0");
|
||||||
|
|
||||||
private Harmony? _harmony;
|
private Harmony? _harmony;
|
||||||
|
|
||||||
private static Regex? _slangSourceCheck = null;
|
private static Regex? _slangSourceCheck = null;
|
||||||
@@ -58,26 +65,26 @@ namespace Slang
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unsafe string Compile(string source)
|
/// <summary>
|
||||||
{
|
/// Encodes the original slang source code as base64 and uses gzip to compress it, returning the resulting string.
|
||||||
string compiled;
|
|
||||||
if (Marshal.CompileFromString(source, out compiled))
|
|
||||||
{
|
|
||||||
// TODO: handle saving the original source code
|
|
||||||
return compiled;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return compiled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Take original slang source code and copies it to a file
|
|
||||||
/// for use in restoring later.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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)
|
public static bool IsSlangSource(ref string input)
|
||||||
@@ -89,7 +96,6 @@ namespace Slang
|
|||||||
{
|
{
|
||||||
L.SetLogger(Logger);
|
L.SetLogger(Logger);
|
||||||
this._harmony = new Harmony(PluginGuid);
|
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,
|
// 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.
|
// as the compiler itself has failed to load.
|
||||||
@@ -100,17 +106,5 @@ namespace Slang
|
|||||||
|
|
||||||
this._harmony.PatchAll();
|
this._harmony.PatchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
|
||||||
{
|
|
||||||
if (Marshal.Destroy())
|
|
||||||
{
|
|
||||||
L.Info("FFI references cleaned up.");
|
|
||||||
}
|
|
||||||
if (this._harmony is not null)
|
|
||||||
{
|
|
||||||
this._harmony.UnpatchSelf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net46</TargetFramework>
|
<TargetFramework>net48</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AssemblyName>StationeersSlang</AssemblyName>
|
<AssemblyName>StationeersSlang</AssemblyName>
|
||||||
<Description>Slang Compiler Bridge</Description>
|
<Description>Slang Compiler Bridge</Description>
|
||||||
@@ -11,9 +11,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<GameDir>/home/dbidwell/.local/share/Steam/steamapps/common/Stationeers/</GameDir>
|
<ManagedDir>$(STATIONEERS_DIR)/rocketstation_Data/Managed</ManagedDir>
|
||||||
<ManagedDir>$(GameDir)/rocketstation_Data/Managed</ManagedDir>
|
<BepInExDir>$(STATIONEERS_DIR)/BepInEx/core</BepInExDir>
|
||||||
<BepInExDir>$(GameDir)/BepInEx/core</BepInExDir>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -42,14 +41,19 @@
|
|||||||
<HintPath>$(ManagedDir)/Assembly-CSharp.dll</HintPath>
|
<HintPath>$(ManagedDir)/Assembly-CSharp.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Assembly-CSharp-firstpass">
|
|
||||||
<HintPath>$(ManagedDir)/Assembly-CSharp-firstpass.dll</HintPath>
|
|
||||||
<Private>False</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="IC10Editor.dll">
|
<Reference Include="IC10Editor.dll">
|
||||||
<HintPath>./ref/IC10Editor.dll</HintPath>
|
<HintPath>./ref/IC10Editor.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="LaunchPadBooster.dll">
|
||||||
|
<HintPath>$(STATIONEERS_DIR)/BepInEx/plugins/StationeersLaunchPad/LaunchPadBooster.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="StationeersMods.Interface.dll">
|
||||||
|
<HintPath>$(STATIONEERS_DIR)/BepInEx/plugins/StationeersLaunchPad/StationeersMods.Interface.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user