18 Commits
0.4.0 ... 0.4.4

Author SHA1 Message Date
6bee591484 Merge pull request 'Added stationpedia docs back into the game patches' (#6) from docs-fix into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 33s
CI/CD Pipeline / build (push) Successful in 1m44s
CI/CD Pipeline / release (push) Successful in 5s
Reviewed-on: #6
2025-12-26 16:20:16 -07:00
c3c14cec23 Added stationpedia docs back into the game patches
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 33s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-26 16:18:24 -07:00
4e885847a8 Merge pull request 'Remove MOD from Plugin.cs which fixed networking' (#5) from slp-removal into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 34s
CI/CD Pipeline / build (push) Successful in 1m43s
CI/CD Pipeline / release (push) Successful in 5s
Reviewed-on: #5
2025-12-24 22:28:51 -07:00
0ca6b27a11 Remove MOD from Plugin.cs which fixed networking
All checks were successful
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
CI/CD Pipeline / test (pull_request) Successful in 34s
2025-12-24 22:27:43 -07:00
9b8900d7a7 Merge pull request 'ic10editor-update' (#4) from ic10editor-update into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 33s
CI/CD Pipeline / build (push) Successful in 1m43s
CI/CD Pipeline / release (push) Successful in 5s
Reviewed-on: #4
2025-12-24 12:41:00 -07:00
792bba4875 Removed unused macro imports as they are implicit
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 33s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-24 12:39:21 -07:00
1c39e146fb Clear editor selection for IC10 if no slang source maps to an IC10 source
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 34s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-24 12:36:33 -07:00
47bcd0be34 Cleaned up BepInEx logging
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 34s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-24 12:10:11 -07:00
445f731170 Fixed IC10 ouput window, refactored IC10 highlighting
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 33s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-24 12:08:17 -07:00
c7aa30581d Added logging around the creation of the IC10Editor tab
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 34s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-24 11:10:10 -07:00
42b0b0acf9 Working saving / loading from the IC10Editor mod. Removed all patches until they can be properly re-implemented
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 35s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-24 01:24:16 -07:00
5230c620e8 WIP -- before another refactor 2025-12-23 20:32:27 -07:00
06a0ec28eb Modify IC10 view logic to conform to the new IC10Editor update 2025-12-22 17:45:42 -07:00
73e08b9896 Merge pull request 'Fix for Gitea actions' (#3) from actions-fix into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 33s
CI/CD Pipeline / build (push) Successful in 1m45s
CI/CD Pipeline / release (push) Successful in 15s
Reviewed-on: #3
2025-12-21 16:42:59 -07:00
e83ff67af8 Fix for Gitea actions
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 34s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-21 16:42:29 -07:00
cacff4ff55 Merge pull request '35-accept-variables' (#2) from 35-accept-variables into master
Some checks failed
CI/CD Pipeline / test (push) Successful in 34s
CI/CD Pipeline / build (push) Failing after 1m55s
CI/CD Pipeline / release (push) Has been skipped
Reviewed-on: #2
2025-12-21 16:34:26 -07:00
7295b14f6a Update changelog, update workflow files
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 34s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-21 16:32:11 -07:00
93873dfa93 Accept expressions for the slotIndex in the slot logic syscalls 2025-12-21 15:59:40 -07:00
20 changed files with 148 additions and 548 deletions

View File

@@ -4,6 +4,7 @@ name: CI/CD Pipeline
on: on:
push: push:
branches: ["master"] branches: ["master"]
tags: ["*.*.*"]
pull_request: pull_request:
branches: ["master"] branches: ["master"]
@@ -57,6 +58,10 @@ jobs:
slang-builder \ slang-builder \
./build.sh ./build.sh
- name: Zip Workshop Folder
run: |
zip -r release/workshop.zip release/workshop/
# 3. Fix Permissions # 3. Fix Permissions
# Docker writes files as root. We need to own them to upload them. # Docker writes files as root. We need to own them to upload them.
- name: Fix Permissions - name: Fix Permissions
@@ -65,7 +70,36 @@ jobs:
# 4. Upload to GitHub # 4. Upload to GitHub
- name: Upload Release Artifacts - name: Upload Release Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: StationeersSlang-Release name: StationeersSlang-Release
path: release/ path: release/
release:
needs: build
runs-on: self-hosted
# ONLY run this job if we pushed a tag (e.g., v1.0.1)
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v4
# We download the artifact from the previous 'build' job
- name: Download Build Artifacts
uses: actions/download-artifact@v3
with:
name: StationeersSlang-Release
path: ./release-files
- name: Create Gitea Release
uses: https://gitea.com/actions/gitea-release-action@v1
with:
files: |
./release-files/workshop.zip
./release-files/slang
./release-files/slang.exe
name: ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,5 +1,34 @@
# Changelog # Changelog
[0.4.4]
- Added Stationpedia docs back after removing all harmony patches from the mod
[0.4.3]
- Removed references to the `Mod` class from SLP. This was the root of the multiplayer
connectivity issues. Multiplayer should now work with Slang installed.
[0.4.2]
- Removed all harmony patches as most functionality as been added into the
`IC10 Editor` mod
- IC10 runtime errors will have been reverted back to showing as IC10 line
numbers instead of Slang line numbers.
- The IC10 line should be easily mapped to a Slang line via the side-by-side
IC10 compilation view.
[0.4.1]
- Update syscalls for `loadSlot` and `setSlot` to support expressions instead of
just variables for the slot index
- Moved the main repository from GitHub to a self-hosted Gitea
- Restructured workflow files to support this change
- GitHub will still remain as a mirrored repository of the new
Gitea instance.
- This is in response to the new upcoming changes to the pricing model
for self-hosted GitHub action runners.
[0.4.0] [0.4.0]
- First pass getting compiled IC10 to output along side the Slang source code - First pass getting compiled IC10 to output along side the Slang source code

View File

@@ -2,7 +2,7 @@
<ModMetadata xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ModMetadata xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>Slang</Name> <Name>Slang</Name>
<Author>JoeDiertay</Author> <Author>JoeDiertay</Author>
<Version>0.4.0</Version> <Version>0.4.4</Version>
<Description> <Description>
[h1]Slang: High-Level Programming for Stationeers[/h1] [h1]Slang: High-Level Programming for Stationeers[/h1]
@@ -86,7 +86,7 @@ A: Yes! Slang does not modify any existing IC10 code, it is only a compiler. As
<Tag>Quality of Life</Tag> <Tag>Quality of Life</Tag>
</Tags> </Tags>
<DependsOn WorkshopHandle="3592775931" /> <DependsOn WorkshopHandle="3592775931" />
<OrderBefore WorkshopHandle="3592775931" /> <OrderAfter WorkshopHandle="3592775931" />
<InGameDescription><![CDATA[ <InGameDescription><![CDATA[
<size=30><color=#ffff00>Slang - High Level Language Compiler</color></size> <size=30><color=#ffff00>Slang - High Level Language Compiler</color></size>
A modern programming experience for Stationeers. Write C-style code that compiles to IC10 instantly. A modern programming experience for Stationeers. Write C-style code that compiles to IC10 instantly.

View File

@@ -5,21 +5,21 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ImGuiNET;
using StationeersIC10Editor; using StationeersIC10Editor;
using StationeersIC10Editor.IC10; using StationeersIC10Editor.IC10;
using UnityEngine;
public class SlangFormatter : ICodeFormatter public class SlangFormatter : ICodeFormatter
{ {
public const string SLANG_SRC = "SLANG_SRC";
private CancellationTokenSource? _lspCancellationToken; private CancellationTokenSource? _lspCancellationToken;
private object _tokenLock = new(); private object _tokenLock = new();
protected Editor? Ic10Editor = null;
private IC10CodeFormatter iC10CodeFormatter = new IC10CodeFormatter(); private IC10CodeFormatter iC10CodeFormatter = new IC10CodeFormatter();
private string ic10CompilationResult = ""; private string ic10CompilationResult = "";
private List<SourceMapEntry> ic10SourceMap = new(); private List<SourceMapEntry> ic10SourceMap = new();
// VS Code Dark Theme Palette #region Colors
public static readonly uint ColorControl = ColorFromHTML("#C586C0"); // Pink (if, return, loop) public static readonly uint ColorControl = ColorFromHTML("#C586C0"); // Pink (if, return, loop)
public static readonly uint ColorDeclaration = ColorFromHTML("#569CD6"); // Blue (let, device, fn) public static readonly uint ColorDeclaration = ColorFromHTML("#569CD6"); // Blue (let, device, fn)
public static readonly uint ColorFunction = ColorFromHTML("#DCDCAA"); // Yellow (syscalls) public static readonly uint ColorFunction = ColorFromHTML("#DCDCAA"); // Yellow (syscalls)
@@ -28,10 +28,8 @@ public class SlangFormatter : ICodeFormatter
public static readonly uint ColorBoolean = ColorFromHTML("#569CD6"); // Blue (true/false) public static readonly uint ColorBoolean = ColorFromHTML("#569CD6"); // Blue (true/false)
public static readonly uint ColorIdentifier = ColorFromHTML("#9CDCFE"); // Light Blue (variables) public static readonly uint ColorIdentifier = ColorFromHTML("#9CDCFE"); // Light Blue (variables)
public static new readonly uint ColorDefault = ColorFromHTML("#D4D4D4"); // White (punctuation ; { } ) public static new readonly uint ColorDefault = ColorFromHTML("#D4D4D4"); // White (punctuation ; { } )
// Operators are often the same color as default text in VS Code Dark,
// but having a separate definition lets you tweak it (e.g. make them slightly darker or distinct)
public static readonly uint ColorOperator = ColorFromHTML("#D4D4D4"); public static readonly uint ColorOperator = ColorFromHTML("#D4D4D4");
#endregion
private HashSet<uint> _linesWithErrors = new(); private HashSet<uint> _linesWithErrors = new();
private int _lastLineCount = -1; private int _lastLineCount = -1;
@@ -49,6 +47,11 @@ public class SlangFormatter : ICodeFormatter
if (string.IsNullOrWhiteSpace(input)) if (string.IsNullOrWhiteSpace(input))
return 0d; return 0d;
if (input.Contains(SLANG_SRC))
{
return 1.0;
}
// Run the compiler to get diagnostics // Run the compiler to get diagnostics
var diagnostics = Marshal.DiagnoseSource(input); var diagnostics = Marshal.DiagnoseSource(input);
@@ -75,35 +78,28 @@ public class SlangFormatter : ICodeFormatter
public override string Compile() public override string Compile()
{ {
return this.Lines.RawText; if (!Marshal.CompileFromString(RawText, out var compilationResult, out var sourceMap))
{
return "Compilation Error";
} }
public override void DrawLine(int lineIndex, TextRange selection, bool drawLineNumber = true) return compilationResult + $"\n{EncodeSource(RawText, SLANG_SRC)}";
}
public override void ResetCode(string code)
{ {
Vector2 cursorPos = ImGui.GetCursorScreenPos(); // for compatibility, we need to check for GlobalCode.SLANG_SRC
Vector2 space = ImGui.GetContentRegionAvail(); // `#SLANG_SRC:<code>`
base.DrawLine(lineIndex, selection, drawLineNumber); // and replace with `# SLANG_SRC: <code>`
if (code.Contains(GlobalCode.SLANG_SRC))
var charWidth = Settings.CharWidth; {
code = code.Replace(GlobalCode.SLANG_SRC, $"# {SLANG_SRC}: ");
var width = Mathf.Max(Lines.Width + 10.0f + LineNumberOffset * charWidth, space.x / 2); }
if (code.Contains(SLANG_SRC))
ImGui {
.GetWindowDrawList() code = ExtractEncodedSource(code, SLANG_SRC);
.AddLine( }
new Vector2(cursorPos.x + width + 4.5f * charWidth, cursorPos.y), base.ResetCode(code);
new Vector2(
cursorPos.x + width + 4.5f * charWidth,
cursorPos.y + space.y + Settings.LineHeight
),
ColorLineNumber,
1.0f
);
cursorPos.x += width;
ImGui.SetCursorScreenPos(cursorPos);
if (lineIndex < iC10CodeFormatter.Lines.Count)
iC10CodeFormatter.DrawLine(lineIndex, new TextRange(), true);
} }
public override StyledLine ParseLine(string line) public override StyledLine ParseLine(string line)
@@ -198,7 +194,20 @@ public class SlangFormatter : ICodeFormatter
private void UpdateIc10Formatter() private void UpdateIc10Formatter()
{ {
iC10CodeFormatter.Editor = Editor; 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);
}
var caretPos = Editor.CaretPos.Line; var caretPos = Editor.CaretPos.Line;
// get the slang sourceMap at the current editor line // get the slang sourceMap at the current editor line
@@ -210,37 +219,30 @@ public class SlangFormatter : ICodeFormatter
// should be directly next to the compiled IC10 source line, and we should highlight the // should be directly next to the compiled IC10 source line, and we should highlight the
// IC10 code that directly represents the Slang source // IC10 code that directly represents the Slang source
iC10CodeFormatter.ResetCode(ic10CompilationResult); Ic10Editor.ResetCode(ic10CompilationResult);
if (lines.Count() < 1) if (lines.Count() < 1)
{ {
Ic10Editor.Selection = new TextRange
{
End = new TextPosition { Col = 0, Line = 0 },
Start = new TextPosition { Col = 0, Line = 0 },
};
return; return;
} }
// get the total range of the IC10 source for the selected Slang line // get the total range of the IC10 source for the selected Slang line
var max = lines.Max(line => line.Ic10Line); var max = lines.Max(line => line.Ic10Line);
var min = lines.Min(line => line.Ic10Line); var min = lines.Min(line => line.Ic10Line);
Ic10Editor.CaretPos = new TextPosition { Col = 0, Line = (int)max };
// highlight all the IC10 lines that are within the specified range // highlight all the IC10 lines that are within the specified range
foreach (var index in Enumerable.Range((int)min, (int)(max - min) + 1)) Ic10Editor.Selection.Start = new TextPosition { Col = 0, Line = (int)min };
Ic10Editor.Selection.End = new TextPosition
{ {
var lineText = iC10CodeFormatter.Lines[index].Text; Col = Ic10Editor.Lines[(int)max].Text.Length,
Line = (int)max,
var newLine = new StyledLine( };
lineText,
[
new SemanticToken
{
Column = 0,
Length = lineText.Length,
Line = index,
Background = ColorIdentifier,
Color = ColorFromHTML("black"),
},
]
);
iC10CodeFormatter.Lines[index] = newLine;
}
} }
// This runs on the Main Thread // This runs on the Main Thread

View File

@@ -1,30 +1,24 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
namespace Slang; namespace Slang;
public static class GlobalCode public static class GlobalCode
{ {
public const string SLANG_REF = "#SLANG_REF:"; /// <summary>
/// This is the OLD way of handling saving / loading. This has been replaced with a native
/// save / load from IC10 Editor. However; this needs to remain for compatibility with
/// previous versions of code not compiled with 0.4.2 or later.
/// </summary>
public const string SLANG_SRC = "#SLANG_SRC:"; public const string SLANG_SRC = "#SLANG_SRC:";
// This is a Dictionary of ENCODED source code, compressed /// <summary>
// so that save file data is smaller /// This Dictionary stores the source maps for the given SLANG_REF, where
private static Dictionary<Guid, string> codeDict = new(); /// the key is the IC10 line, and the value is a List of Slang ranges where that
/// line would have come from
// This Dictionary stores the source maps for the given SLANG_REF, where /// </summary>
// the key is the IC10 line, and the value is a List of Slang ranges where that
// line would have come from
private static Dictionary<Guid, Dictionary<uint, List<Range>>> sourceMaps = new(); private static Dictionary<Guid, Dictionary<uint, List<Range>>> sourceMaps = new();
public static void ClearCache()
{
codeDict.Clear();
}
public static void SetSourceMap(Guid reference, List<SourceMapEntry> sourceMapEntries) public static void SetSourceMap(Guid reference, List<SourceMapEntry> sourceMapEntries)
{ {
var builtDictionary = new Dictionary<uint, List<Range>>(); var builtDictionary = new Dictionary<uint, List<Range>>();
@@ -72,81 +66,4 @@ public static class GlobalCode
slangSpan = foundRange[0]; slangSpan = foundRange[0];
return true; return true;
} }
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());
}
}
}
}
} }

View File

@@ -1,355 +1,11 @@
namespace Slang; namespace Slang;
using System;
using System.Runtime.CompilerServices;
using Assets.Scripts.Objects;
using Assets.Scripts.Objects.Electrical;
using Assets.Scripts.Objects.Motherboards;
using Assets.Scripts.UI; using Assets.Scripts.UI;
using HarmonyLib; using HarmonyLib;
class LineErrorData
{
public AsciiString SourceRef;
public uint IC10ErrorSource;
public string SlangErrorReference;
public Range SlangErrorSpan;
public LineErrorData(
AsciiString sourceRef,
uint ic10ErrorSource,
string slangErrorRef,
Range slangErrorSpan
)
{
this.SourceRef = sourceRef;
this.IC10ErrorSource = ic10ErrorSource;
this.SlangErrorReference = slangErrorRef;
this.SlangErrorSpan = slangErrorSpan;
}
}
[HarmonyPatch] [HarmonyPatch]
public static class SlangPatches public static class SlangPatches
{ {
private static ProgrammableChipMotherboard? _currentlyEditingMotherboard;
private static AsciiString? _motherboardCachedCode;
private static Guid? _currentlyEditingGuid;
private static ConditionalWeakTable<ProgrammableChip, LineErrorData> _errorReferenceTable =
new();
[HarmonyPatch(
typeof(ProgrammableChipMotherboard),
nameof(ProgrammableChipMotherboard.InputFinished)
)]
[HarmonyPrefix]
public static void pgmb_InputFinished(ref string result)
{
_currentlyEditingMotherboard = null;
_motherboardCachedCode = null;
// guard to ensure we have valid IC10 before continuing
if (
!SlangPlugin.IsSlangSource(ref result)
|| !Marshal.CompileFromString(result, out var compiled, out var sourceMap)
|| string.IsNullOrEmpty(compiled)
)
{
return;
}
var thisRef = _currentlyEditingGuid ?? Guid.NewGuid();
// Ensure we cache this compiled code for later retreival.
GlobalCode.SetSource(thisRef, result);
GlobalCode.SetSourceMap(thisRef, sourceMap);
_currentlyEditingGuid = null;
// Append REF to the bottom
compiled += $"\n{GlobalCode.SLANG_REF}{thisRef}";
result = compiled;
}
[HarmonyPatch(typeof(ProgrammableChipMotherboard), nameof(ProgrammableChipMotherboard.OnEdit))]
[HarmonyPrefix]
public static void isc_OnEdit(ProgrammableChipMotherboard __instance)
{
_currentlyEditingMotherboard = __instance;
_motherboardCachedCode = __instance.GetSourceCode();
var sourceCode = System.Text.Encoding.UTF8.GetString(
System.Text.Encoding.ASCII.GetBytes(__instance.GetSourceCode())
);
if (string.IsNullOrEmpty(sourceCode))
{
return;
}
// Look for REF at the bottom
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).Trim(),
out Guid sourceRef
)
)
{
// not a valid Guid, not managed by slang
return;
}
_currentlyEditingGuid = sourceRef;
var slangSource = GlobalCode.GetSource(sourceRef);
if (string.IsNullOrEmpty(slangSource))
{
// Didn't find that source ref in the global code manager.
return;
}
__instance.SetSourceCode(slangSource);
}
private static void HandleSerialization(ref string sourceCode)
{
if (string.IsNullOrEmpty(sourceCode))
return;
// Check if the file ends with the Reference Tag
var tagIndex = sourceCode.LastIndexOf(GlobalCode.SLANG_REF);
if (tagIndex == -1)
return;
string guidString = sourceCode.Substring(tagIndex + GlobalCode.SLANG_REF.Length).Trim();
if (!Guid.TryParse(guidString, out Guid slangRefGuid))
{
L.Warning($"Found SLANG_REF but failed to parse GUID: {guidString}");
return;
}
var slangEncoded = GlobalCode.GetEncoded(slangRefGuid);
if (string.IsNullOrEmpty(slangEncoded))
{
L.Warning(
$"Could not find encoded source for ref {slangRefGuid}. Save will contain compiled IC10 only."
);
return;
}
// Extract the clean IC10 code (everything before the tag)
var cleanIc10 = sourceCode.Substring(0, tagIndex).TrimEnd();
// Append the encoded source tag to the bottom
sourceCode = $"{cleanIc10}\n{GlobalCode.SLANG_SRC}{slangEncoded}";
}
[HarmonyPatch(typeof(ProgrammableChip), nameof(ProgrammableChip.SerializeSave))]
[HarmonyPostfix]
public static void pgc_SerializeSave(ProgrammableChip __instance, ref ThingSaveData __result)
{
if (__result is not ProgrammableChipSaveData chipData)
return;
string code = chipData.SourceCode;
HandleSerialization(ref code);
chipData.SourceCode = code;
}
[HarmonyPatch(
typeof(ProgrammableChip),
nameof(ProgrammableChip.ErrorLineNumberString),
MethodType.Getter
)]
[HarmonyPostfix]
public static void pgc_ErrorLineNumberString(ProgrammableChip __instance, ref string __result)
{
if (
String.IsNullOrEmpty(__result)
|| !uint.TryParse(__result.Trim(), out var ic10ErrorLineNumber)
)
{
return;
}
var sourceAscii = __instance.GetSourceCode();
if (_errorReferenceTable.TryGetValue(__instance, out var cache))
{
if (cache.SourceRef.Equals(sourceAscii) && cache.IC10ErrorSource == ic10ErrorLineNumber)
{
__result = cache.SlangErrorReference;
return;
}
}
var source = System.Text.Encoding.UTF8.GetString(
System.Text.Encoding.ASCII.GetBytes(__instance.GetSourceCode())
);
var slangIndex = source.LastIndexOf(GlobalCode.SLANG_REF);
if (
slangIndex < 0
|| !Guid.TryParse(
source
.Substring(
source.LastIndexOf(GlobalCode.SLANG_REF) + GlobalCode.SLANG_REF.Length
)
.Trim(),
out var slangGuid
)
|| !GlobalCode.GetSlangErrorLineFromICError(
slangGuid,
ic10ErrorLineNumber,
out var slangErrorLineNumber,
out var slangSpan
)
)
{
return;
}
L.Warning($"IC error at: {__result} -- Slang source error line: {slangErrorLineNumber}");
__result = slangErrorLineNumber.ToString();
_errorReferenceTable.Remove(__instance);
_errorReferenceTable.Add(
__instance,
new LineErrorData(
sourceAscii,
ic10ErrorLineNumber,
slangErrorLineNumber.ToString(),
slangSpan
)
);
}
[HarmonyPatch(
typeof(ProgrammableChip),
nameof(ProgrammableChip.SetSourceCode),
new Type[] { typeof(string) }
)]
[HarmonyPostfix]
public static void pgc_SetSourceCode_string(ProgrammableChip __instance, string sourceCode)
{
_errorReferenceTable.Remove(__instance);
}
[HarmonyPatch(
typeof(ProgrammableChip),
nameof(ProgrammableChip.SetSourceCode),
new Type[] { typeof(string), typeof(ICircuitHolder) }
)]
[HarmonyPostfix]
public static void pgc_SetSourceCode_string_parent(
ProgrammableChip __instance,
string sourceCode,
ICircuitHolder parent
)
{
_errorReferenceTable.Remove(__instance);
}
[HarmonyPatch(
typeof(ProgrammableChipMotherboard),
nameof(ProgrammableChipMotherboard.SerializeSave)
)]
[HarmonyPostfix]
public static void pgmb_SerializeSave(
ProgrammableChipMotherboard __instance,
ref ThingSaveData __result
)
{
if (__result is not ProgrammableChipMotherboardSaveData chipData)
return;
string code = chipData.SourceCode;
HandleSerialization(ref code);
chipData.SourceCode = code;
}
private static void HandleDeserialization(ref string sourceCode)
{
// Safety check for null/empty code
if (string.IsNullOrEmpty(sourceCode))
return;
// Check for the #SLANG_SRC: footer
int tagIndex = sourceCode.LastIndexOf(GlobalCode.SLANG_SRC);
// If the tag is missing, this is just a normal IC10 script. Do nothing.
if (tagIndex == -1)
return;
// Extract the Encoded Source (Base64)
string encodedSource = sourceCode.Substring(tagIndex + GlobalCode.SLANG_SRC.Length).Trim();
// Extract the IC10 Code (strip off the tag and the newline before it)
string ic10Code = sourceCode.Substring(0, tagIndex).TrimEnd();
// Generate a new Runtime GUID for this session
Guid runtimeGuid = Guid.NewGuid();
// Hydrate the Cache
GlobalCode.SetEncoded(runtimeGuid, encodedSource);
// Rewrite the SourceCode to the "Runtime" format (REF at bottom)
sourceCode = $"{ic10Code}\n{GlobalCode.SLANG_REF}{runtimeGuid}";
}
[HarmonyPatch(typeof(ProgrammableChip), nameof(ProgrammableChip.DeserializeSave))]
[HarmonyPrefix]
public static void pgc_DeserializeSave(ref ThingSaveData savedData)
{
if (savedData is not ProgrammableChipSaveData pcSaveData)
return;
string code = pcSaveData.SourceCode;
HandleDeserialization(ref code);
pcSaveData.SourceCode = code;
}
[HarmonyPatch(
typeof(ProgrammableChipMotherboard),
nameof(ProgrammableChipMotherboard.DeserializeSave)
)]
[HarmonyPrefix]
public static void pgmb_DeserializeSave(ref ThingSaveData savedData)
{
if (savedData is not ProgrammableChipMotherboardSaveData pcSaveData)
return;
string code = pcSaveData.SourceCode;
HandleDeserialization(ref code);
pcSaveData.SourceCode = code;
}
[HarmonyPatch(typeof(InputSourceCode), nameof(InputSourceCode.ButtonInputCancel))]
[HarmonyPrefix]
public static void isc_ButtonInputCancel()
{
if (_currentlyEditingMotherboard is null || _motherboardCachedCode is null)
{
return;
}
_currentlyEditingMotherboard.SetSourceCode(_motherboardCachedCode);
_currentlyEditingMotherboard = null;
_motherboardCachedCode = null;
_currentlyEditingGuid = null;
}
[HarmonyPatch(typeof(Stationpedia), nameof(Stationpedia.Regenerate))] [HarmonyPatch(typeof(Stationpedia), nameof(Stationpedia.Regenerate))]
[HarmonyPostfix] [HarmonyPostfix]
public static void Stationpedia_Regenerate() public static void Stationpedia_Regenerate()

View File

@@ -1,7 +1,6 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using BepInEx; using BepInEx;
using HarmonyLib; using HarmonyLib;
using LaunchPadBooster;
namespace Slang namespace Slang
{ {
@@ -41,9 +40,7 @@ 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 const string PluginVersion = "0.4.0"; public const string PluginVersion = "0.4.4";
public static Mod MOD = new Mod(PluginName, PluginVersion);
private Harmony? _harmony; private Harmony? _harmony;

View File

@@ -5,7 +5,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>StationeersSlang</AssemblyName> <AssemblyName>StationeersSlang</AssemblyName>
<Description>Slang Compiler Bridge</Description> <Description>Slang Compiler Bridge</Description>
<Version>0.4.0</Version> <Version>0.4.2</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>

View File

@@ -930,7 +930,7 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]] [[package]]
name = "slang" name = "slang"
version = "0.4.0" version = "0.4.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "slang" name = "slang"
version = "0.4.0" version = "0.4.3"
edition = "2021" edition = "2021"
[workspace] [workspace]

View File

@@ -1,4 +1,3 @@
use crate::compile;
use anyhow::Result; use anyhow::Result;
use indoc::indoc; use indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

View File

@@ -1,4 +1,3 @@
use crate::compile;
use indoc::indoc; use indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

View File

@@ -1,4 +1,3 @@
use crate::compile;
use indoc::indoc; use indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

View File

@@ -1,4 +1,3 @@
use crate::compile;
use indoc::indoc; use indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

View File

@@ -1,4 +1,3 @@
use crate::compile;
use indoc::indoc; use indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

View File

@@ -1,4 +1,3 @@
use crate::compile;
use anyhow::Result; use anyhow::Result;
use indoc::indoc; use indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

View File

@@ -1,4 +1,3 @@
use crate::compile;
use indoc::indoc; use indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

View File

@@ -2233,10 +2233,7 @@ impl<'a> Compiler<'a> {
System::LoadSlot(dev_name, slot_index, logic_type) => { System::LoadSlot(dev_name, slot_index, logic_type) => {
let (dev_hash, hash_cleanup) = let (dev_hash, hash_cleanup) =
self.compile_literal_or_variable(dev_name.node, scope)?; self.compile_literal_or_variable(dev_name.node, scope)?;
let (slot_index, slot_cleanup) = self.compile_literal_or_variable( let (slot_index, slot_cleanup) = self.compile_operand(*slot_index, scope)?;
LiteralOrVariable::Literal(slot_index.node),
scope,
)?;
let (logic_type, logic_cleanup) = self.compile_literal_or_variable( let (logic_type, logic_cleanup) = self.compile_literal_or_variable(
LiteralOrVariable::Literal(logic_type.node), LiteralOrVariable::Literal(logic_type.node),
scope, scope,
@@ -2261,10 +2258,7 @@ impl<'a> Compiler<'a> {
System::SetSlot(dev_name, slot_index, logic_type, var) => { System::SetSlot(dev_name, slot_index, logic_type, var) => {
let (dev_name, name_cleanup) = let (dev_name, name_cleanup) =
self.compile_literal_or_variable(dev_name.node, scope)?; self.compile_literal_or_variable(dev_name.node, scope)?;
let (slot_index, index_cleanup) = self.compile_literal_or_variable( let (slot_index, index_cleanup) = self.compile_operand(*slot_index, scope)?;
LiteralOrVariable::Literal(slot_index.node),
scope,
)?;
let (logic_type, type_cleanup) = self.compile_literal_or_variable( let (logic_type, type_cleanup) = self.compile_literal_or_variable(
LiteralOrVariable::Literal(logic_type.node), LiteralOrVariable::Literal(logic_type.node),
scope, scope,

View File

@@ -1834,20 +1834,8 @@ impl<'a> Parser<'a> {
let mut args = args!(3); let mut args = args!(3);
let next = args.next(); let next = args.next();
let dev_name = literal_or_variable!(next); let dev_name = literal_or_variable!(next);
let next = args.next(); let slot_index = args.next().ok_or(Error::UnexpectedEOF)?;
let slot_index = get_arg!(Literal, literal_or_variable!(next));
if !matches!(
slot_index,
Spanned {
node: Literal::Number(_),
..
},
) {
return Err(Error::InvalidSyntax(
slot_index.span,
"Expected a number".to_string(),
));
}
let next = args.next(); let next = args.next();
let slot_logic = get_arg!(Literal, literal_or_variable!(next)); let slot_logic = get_arg!(Literal, literal_or_variable!(next));
if !matches!( if !matches!(
@@ -1864,27 +1852,17 @@ impl<'a> Parser<'a> {
} }
Ok(SysCall::System(System::LoadSlot( Ok(SysCall::System(System::LoadSlot(
dev_name, slot_index, slot_logic, dev_name,
boxed!(slot_index),
slot_logic,
))) )))
} }
"setSlot" | "ss" => { "setSlot" | "ss" => {
let mut args = args!(4); let mut args = args!(4);
let next = args.next(); let next = args.next();
let dev_name = literal_or_variable!(next); let dev_name = literal_or_variable!(next);
let next = args.next(); let slot_index = args.next().ok_or(Error::UnexpectedEOF)?;
let slot_index = get_arg!(Literal, literal_or_variable!(next));
if !matches!(
slot_index,
Spanned {
node: Literal::Number(_),
..
}
) {
return Err(Error::InvalidSyntax(
slot_index.span,
"Expected a number".into(),
));
}
let next = args.next(); let next = args.next();
let slot_logic = get_arg!(Literal, literal_or_variable!(next)); let slot_logic = get_arg!(Literal, literal_or_variable!(next));
if !matches!( if !matches!(
@@ -1904,9 +1882,9 @@ impl<'a> Parser<'a> {
Ok(SysCall::System(System::SetSlot( Ok(SysCall::System(System::SetSlot(
dev_name, dev_name,
slot_index, boxed!(slot_index),
slot_logic, slot_logic,
Box::new(expr), boxed!(expr),
))) )))
} }
"loadReagent" | "lr" => { "loadReagent" | "lr" => {

View File

@@ -223,7 +223,7 @@ documented! {
/// `let isOccupied = ls(deviceHash, 2, "Occupied");` /// `let isOccupied = ls(deviceHash, 2, "Occupied");`
LoadSlot( LoadSlot(
Spanned<LiteralOrVariable<'a>>, Spanned<LiteralOrVariable<'a>>,
Spanned<Literal<'a>>, Box<Spanned<Expression<'a>>>,
Spanned<Literal<'a>> Spanned<Literal<'a>>
), ),
/// Stores a value of LogicType on a device by the index value /// Stores a value of LogicType on a device by the index value
@@ -234,7 +234,7 @@ documented! {
/// `ss(deviceHash, 0, "Open", true);` /// `ss(deviceHash, 0, "Open", true);`
SetSlot( SetSlot(
Spanned<LiteralOrVariable<'a>>, Spanned<LiteralOrVariable<'a>>,
Spanned<Literal<'a>>, Box<Spanned<Expression<'a>>>,
Spanned<Literal<'a>>, Spanned<Literal<'a>>,
Box<Spanned<Expression<'a>>> Box<Spanned<Expression<'a>>>
), ),