31 Commits
0.4.0 ... 0.4.6

Author SHA1 Message Date
e272737ea2 Merge pull request 'Fixed const -> let bug' (#10) from declaration_const_as_let into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 32s
CI/CD Pipeline / build (push) Successful in 1m43s
CI/CD Pipeline / release (push) Successful in 5s
Reviewed-on: #10
2025-12-29 02:44:50 -07:00
f679601818 Fixed const -> let bug
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-29 02:31:23 -07:00
3ca6f97db1 Merge pull request 'IC10Editor fix' (#9) from live-reload into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 33s
CI/CD Pipeline / build (push) Successful in 1m41s
CI/CD Pipeline / release (push) Successful in 4s
Reviewed-on: #9
2025-12-27 22:26:50 -07:00
34817ee111 Merge branch 'master' of ssh://git.biddydev.com:2222/dbidwell/stationeers_lang into live-reload
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 32s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-27 22:19:09 -07:00
9eef8a77b6 Merge pull request 'Update About.xml docs for the workshop' (#8) from documentation into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 34s
CI/CD Pipeline / build (push) Successful in 1m41s
CI/CD Pipeline / release (push) Has been skipped
Reviewed-on: #8
2025-12-27 22:19:00 -07:00
de31851153 Fixed IC10Editor implementation bug where IC10 code would not update after clicking 'Cancel' 2025-12-27 22:18:21 -07:00
3543b87561 wip 2025-12-27 16:03:36 -07:00
effef64add Update About.xml docs for the workshop
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 22:33:48 -07:00
794b27b8c6 Merge pull request 'documentation' (#7) from documentation into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 33s
CI/CD Pipeline / build (push) Successful in 1m42s
CI/CD Pipeline / release (push) Has been skipped
Reviewed-on: #7
2025-12-26 22:02:46 -07:00
27e8987831 removed the temporary example slang scripts from the root directory
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-26 22:00:28 -07:00
0fdceac22c changed markdown tags from slang to rust 2025-12-26 21:57:11 -07:00
85f8b136e1 Update markdown tags to use rust instead of mips 2025-12-26 21:56:21 -07:00
c91086157a Added documentation for various language features and in-game functions. Added example scripts 2025-12-26 21:55:36 -07:00
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
29 changed files with 1384 additions and 749 deletions

View File

@@ -4,6 +4,7 @@ name: CI/CD Pipeline
on:
push:
branches: ["master"]
tags: ["*.*.*"]
pull_request:
branches: ["master"]
@@ -57,6 +58,10 @@ jobs:
slang-builder \
./build.sh
- name: Zip Workshop Folder
run: |
zip -r release/workshop.zip release/workshop/
# 3. Fix Permissions
# Docker writes files as root. We need to own them to upload them.
- name: Fix Permissions
@@ -65,7 +70,36 @@ jobs:
# 4. Upload to GitHub
- name: Upload Release Artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: StationeersSlang-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,50 @@
# Changelog
[0.4.6]
- Fixed bug in compiler where you were unable to assign a `const` value to
a `let` variable
[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
[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]
- First pass getting compiled IC10 to output along side the Slang source code

View File

@@ -2,11 +2,11 @@
<ModMetadata xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>Slang</Name>
<Author>JoeDiertay</Author>
<Version>0.4.0</Version>
<Version>0.4.6</Version>
<Description>
[h1]Slang: High-Level Programming for Stationeers[/h1]
Stop writing assembly. Start writing code.
Iterate faster. Stop writing assembly. Start writing code.
Slang (Stationeers Language) brings modern programming to Stationeers. It allows you to write scripts using a familiar C-style syntax (variables, functions, if/else, loops) directly in the in-game editor. When you hit confirm, Slang compiles your code into IC10 instantly.
@@ -15,7 +15,7 @@ Slang (Stationeers Language) brings modern programming to Stationeers. It allows
[h2]Features[/h2]
[list]
[*] [b]In-Game Compilation:[/b] No external tools needed. Write Slang directly in the IC editor.
[*] [b]No More Register Juggling:[/b] Define variables with let (e.g., let temp = 300). The compiler manages r0-r15 for you.
[*] [b]No More Register Juggling:[/b] Define variables with let (e.g., let temp = 200c). The compiler manages r0-r15 for you.
[*] [b]Control Flow:[/b] Write readable logic with if, else, while, loop, break, and continue.
[*] [b]Functions:[/b] Create reusable code blocks with arguments.
[*] [b]Smart Editor:[/b] Get real-time syntax highlighting and error checking (red text) as you type.
@@ -53,14 +53,13 @@ loop {
[h2]Known Issues (Beta)[/h2]
[list]
[*] [b]Stack Access:[/b] Direct stack memory access is disabled to prevent conflicts with the compiler's internal memory management. A workaround is being planned.
[*] [b]Documentation:[/b] In-game tooltips for syscalls (like load, set) are WIP. Check the "Slang" entry in the Stationpedia (F1) for help.
[*] [b]Documentation:[/b] In-game tooltips for syscalls (like load, set) are WIP. Check the "Slang" entry in the Stationpedia (F1) for help, or checkout [url=https://github.com/dbidwell94/stationeers_lang/blob/master/docs/getting-started.md]The Docs[/url] for guides on how to get started.
[/list]
[h2]Planned Features[/h2]
[list]
[*] Enhanced LSP features (Autocomplete, Go to Definition).
[*] Full feature parity with all IC10 instructions.
[*] Tutorials and beginner script examples.
[/list]
[h2]FAQ[/h2]
@@ -70,10 +69,18 @@ A: The Slang compiler is built in Rust for performance and reliability. It is co
[b]Q: Is this compatible with my current save?[/b]
A: Yes! Slang does not modify any existing IC10 code, it is only a compiler. As a matter of fact: if you wish to stop using Slang at any time, your compiled IC10 will still exist on the chip. However: Slang adds a comment at the bottom of your compiled IC10 which is a GZIP and base64 encoded version of your Slang source code. This might break line limits with Slang not installed. If you wish to no longer use Slang, I recommend you remove this comment from the source code after uninstalling Slang.
[b]Q: Does this modify the in-game scripting language[/b]
A: No! Slang compiles directly to IC10. Any valid Slang file will produce valid IC10. The goal of this mod is twofold:
[list]
[*] Allow experienced users to quickly iterate on their scripts to get back into base upgrades faster
[*] Allow newcomers who already know C-style languages (JS/C#/Java/Rust/C/etc) to see how it maps to IC10 so they can get to understand IC10 better
[/list]
[h2]Useful Links[/h2]
[url=https://github.com/dbidwell94/stationeers_lang]Source Code on GitHub[/url]
[url=https://discord.gg/stationeers]Stationeers Official Discord[/url]
[url=https://discord.gg/M4sCfYMacs]Stationeers Modding Discord[/url]
[url=https://github.com/dbidwell94/stationeers_lang/blob/master/docs/getting-started.md]Getting Started Guide[/url]
</Description>
<ChangeLog xsi:nil="true" />
<WorkshopHandle>3619985558</WorkshopHandle>
@@ -86,7 +93,7 @@ A: Yes! Slang does not modify any existing IC10 code, it is only a compiler. As
<Tag>Quality of Life</Tag>
</Tags>
<DependsOn WorkshopHandle="3592775931" />
<OrderBefore WorkshopHandle="3592775931" />
<OrderAfter WorkshopHandle="3592775931" />
<InGameDescription><![CDATA[
<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.
@@ -116,5 +123,7 @@ A: Yes! Slang does not modify any existing IC10 code, it is only a compiler. As
See: https://github.com/StationeersLaunchPad/StationeersLaunchPad
Source Code: https://github.com/dbidwell94/stationeers_lang
Documentation: https://github.com/dbidwell94/stationeers_lang/blob/master/docs/getting-started.md
]]></InGameDescription>
</ModMetadata>

View File

@@ -1,30 +1,45 @@
# Stationeers Language (slang)
# Slang Language Documentation
This is an ambitious attempt at creating:
Slang is a high-level programming language that compiles to IC10 assembly for [Stationeers](https://store.steampowered.com/app/544550/Stationeers/).
It provides a familiar C-like syntax while targeting the limited instruction set
of in-game IC10.
- A new programming language (slang)
- A compiler to translate slang -> IC10
- A mod to allow direct input of slang in the in-game script editor to
automatically compile to IC10 before running
## Quick Links
This project currently outputs 3 files:
- [Getting Started](docs/getting-started.md) - Installation and first program
- [Language Reference](docs/language-reference.md) - Complete syntax guide
- [Built-in Functions](docs/builtins.md) - System calls and math functions
- [Examples](docs/examples.md) - Real-world code samples
- A Linux CLI
- A Windows CLI
- A Windows FFI dll
- Contains a single function: `compile_from_string`
## Overview
The aim of this project is to lower the amount of time it takes to code simple
scripts in Stationeers so you can get back to engineering atmospherics or
whatever you are working on. This project is NOT meant to fully replace IC10.
Obviously hand-coded assembly written by an experienced programmer is more
optimized and smaller than something that a C compiler will spit out. This is
the same way. It WILL produce valid IC10, but for large complicated projects it
might produce over the allowed limit of lines the in-game editor supports.
Slang aims to reduce the time spent writing IC10 assembly by providing:
Current Unknowns
- **Familiar syntax** - C-like declarations, control flow, and expressions
- **Device abstraction** - Named device bindings with property access
- **Automatic register allocation** - No manual register management
- **Built-in functions** - Math operations and device I/O as function calls
- **Temperature literals** - Native support for Celsius, Fahrenheit, and Kelvin
- Should I support a configurable script line length in-game to allow larger
scripts to be saved?
- Should compilation be "behind the scenes" (in game editor will ALWAYS be what
you put in. IC10 will be IC10, slang will be slang)
## Example
```rust
device gasSensor = "d0";
device airCon = "d1";
const TARGET_TEMP = 20c;
loop {
yield();
airCon.On = gasSensor.Temperature > TARGET_TEMP;
}
```
This compiles to IC10 that monitors temperature and controls an air
conditioner.
## Project Status
Slang is under active development. It may produce suboptimal code for complex programs.
It is not a replacement for IC10, for performance-critical or large scripts,
hand-written IC10 may still be preferred.

View File

@@ -5,21 +5,37 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ImGuiNET;
using StationeersIC10Editor;
using StationeersIC10Editor.IC10;
using UnityEngine;
public class SlangFormatter : ICodeFormatter
{
public const string SLANG_SRC = "SLANG_SRC";
private CancellationTokenSource? _lspCancellationToken;
private object _tokenLock = new();
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<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 ColorDeclaration = ColorFromHTML("#569CD6"); // Blue (let, device, fn)
public static readonly uint ColorFunction = ColorFromHTML("#DCDCAA"); // Yellow (syscalls)
@@ -28,10 +44,8 @@ public class SlangFormatter : ICodeFormatter
public static readonly uint ColorBoolean = ColorFromHTML("#569CD6"); // Blue (true/false)
public static readonly uint ColorIdentifier = ColorFromHTML("#9CDCFE"); // Light Blue (variables)
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");
#endregion
private HashSet<uint> _linesWithErrors = new();
private int _lastLineCount = -1;
@@ -49,6 +63,11 @@ public class SlangFormatter : ICodeFormatter
if (string.IsNullOrWhiteSpace(input))
return 0d;
if (input.Contains(SLANG_SRC))
{
return 1.0;
}
// Run the compiler to get diagnostics
var diagnostics = Marshal.DiagnoseSource(input);
@@ -75,35 +94,28 @@ public class SlangFormatter : ICodeFormatter
public override string Compile()
{
return this.Lines.RawText;
if (!Marshal.CompileFromString(RawText, out var compilationResult, out var sourceMap))
{
return "# Compilation Error";
}
return compilationResult + $"\n{EncodeSource(RawText, SLANG_SRC)}";
}
public override void DrawLine(int lineIndex, TextRange selection, bool drawLineNumber = true)
public override void ResetCode(string code)
{
Vector2 cursorPos = ImGui.GetCursorScreenPos();
Vector2 space = ImGui.GetContentRegionAvail();
base.DrawLine(lineIndex, selection, drawLineNumber);
var charWidth = Settings.CharWidth;
var width = Mathf.Max(Lines.Width + 10.0f + LineNumberOffset * charWidth, space.x / 2);
ImGui
.GetWindowDrawList()
.AddLine(
new Vector2(cursorPos.x + width + 4.5f * charWidth, cursorPos.y),
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);
// for compatibility, we need to check for GlobalCode.SLANG_SRC
// `#SLANG_SRC:<code>`
// and replace with `# SLANG_SRC: <code>`
if (code.Contains(GlobalCode.SLANG_SRC))
{
code = code.Replace(GlobalCode.SLANG_SRC, $"# {SLANG_SRC}: ");
}
if (code.Contains(SLANG_SRC))
{
code = ExtractEncodedSource(code, SLANG_SRC);
}
base.ResetCode(code);
}
public override StyledLine ParseLine(string line)
@@ -122,6 +134,10 @@ public class SlangFormatter : ICodeFormatter
return styledLine;
}
/// <summary>
/// This handles calling the `HandleLsp` function by creating a new `CancellationToken` and
/// cancelling the current call if applicable.
/// </summary>
private void HandleCodeChanged()
{
CancellationToken token;
@@ -137,6 +153,11 @@ public class SlangFormatter : ICodeFormatter
_ = HandleLsp(inputSrc, token);
}
/// <summary>
/// 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.
/// </summary>
private async Task HandleLsp(string inputSrc, CancellationToken cancellationToken)
{
try
@@ -169,24 +190,21 @@ public class SlangFormatter : ICodeFormatter
return;
}
var (compilationSuccess, compiled, sourceMap) = await Task.Run(
() =>
{
var successful = Marshal.CompileFromString(
inputSrc,
out var compiled,
out var sourceMap
);
return (successful, compiled, sourceMap);
},
cancellationToken
);
var (compilationSuccess, compiled, sourceMap) = await Task.Run(() =>
{
var successful = Marshal.CompileFromString(
inputSrc,
out var compiled,
out var sourceMap
);
return (successful, compiled, sourceMap);
});
if (compilationSuccess)
{
ic10CompilationResult = compiled;
ic10SourceMap = sourceMap;
UpdateIc10Formatter();
UpdateIc10Content(Ic10Editor);
}
}
catch (OperationCanceledException) { }
@@ -196,9 +214,24 @@ public class SlangFormatter : ICodeFormatter
}
}
/// <summary>
/// Updates the underlying code in the IC10 Editor, after which will call `UpdateIc10Formatter` to
/// update highlighting of relavent fields.
/// </summary>
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()
{
iC10CodeFormatter.Editor = Editor;
// 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
@@ -206,44 +239,37 @@ public class SlangFormatter : ICodeFormatter
entry.SlangSource.StartLine == caretPos || entry.SlangSource.EndLine == caretPos
);
// extract the current "context" of the ic10 compilation. The current Slang source line
// should be directly next to the compiled IC10 source line, and we should highlight the
// IC10 code that directly represents the Slang source
iC10CodeFormatter.ResetCode(ic10CompilationResult);
Ic10Editor.ResetCode(ic10CompilationResult);
if (lines.Count() < 1)
{
Ic10Editor.Selection = new TextRange
{
End = new TextPosition { Col = 0, Line = 0 },
Start = new TextPosition { Col = 0, Line = 0 },
};
return;
}
// get the total range of the IC10 source for the selected Slang line
var max = lines.Max(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
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;
var newLine = new StyledLine(
lineText,
[
new SemanticToken
{
Column = 0,
Length = lineText.Length,
Line = index,
Background = ColorIdentifier,
Color = ColorFromHTML("black"),
},
]
);
iC10CodeFormatter.Lines[index] = newLine;
}
Col = Ic10Editor.Lines[(int)max].Text.Length,
Line = (int)max,
};
}
// This runs on the Main Thread
/// <summary>
/// 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
/// </summary>
private void ApplyDiagnostics(Dictionary<uint, IGrouping<uint, Diagnostic>> dict)
{
HashSet<uint> linesToRefresh;

View File

@@ -1,30 +1,24 @@
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:";
/// <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:";
// This is a Dictionary of ENCODED source code, compressed
// so that save file data is smaller
private static Dictionary<Guid, string> codeDict = new();
// This Dictionary stores the source maps for the given SLANG_REF, where
// the key is the IC10 line, and the value is a List of Slang ranges where that
// line would have come from
/// <summary>
/// This Dictionary stores the source maps for the given SLANG_REF, where
/// the key is the IC10 line, and the value is a List of Slang ranges where that
/// line would have come from
/// </summary>
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)
{
var builtDictionary = new Dictionary<uint, List<Range>>();
@@ -72,81 +66,4 @@ public static class GlobalCode
slangSpan = foundRange[0];
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

@@ -67,7 +67,9 @@ public static class Marshal
try
{
_libraryHandle = LoadLibrary(ExtractNativeLibrary(Ffi.RustLib));
L.Debug("Rust DLL loaded successfully. Enjoy native speed compilations!");
CodeFormatters.RegisterFormatter("Slang", typeof(SlangFormatter), true);
return true;
}
catch (Exception ex)
@@ -91,8 +93,13 @@ public static class Marshal
try
{
FreeLibrary(_libraryHandle);
CodeFormatters.RegisterFormatter("Slang", typeof(PlainTextFormatter), true);
if (!FreeLibrary(_libraryHandle))
{
L.Warning("Unable to free Rust library");
}
_libraryHandle = IntPtr.Zero;
L.Debug("Rust DLL library freed");
return true;
}
catch (Exception ex)
@@ -191,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!"
@@ -201,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;
}
}
}

View File

@@ -1,355 +1,11 @@
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 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]
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))]
[HarmonyPostfix]
public static void Stationpedia_Regenerate()

View File

@@ -1,7 +1,5 @@
using System.Text.RegularExpressions;
using BepInEx;
using HarmonyLib;
using LaunchPadBooster;
namespace Slang
{
@@ -41,45 +39,32 @@ namespace Slang
{
public const string PluginGuid = "com.biddydev.slang";
public const string PluginName = "Slang";
public const string PluginVersion = "0.4.0";
public const string PluginVersion = "0.4.6";
public static Mod MOD = new Mod(PluginName, PluginVersion);
private static Harmony? _harmony;
private 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);
}
private void Awake()
public void Awake()
{
L.SetLogger(Logger);
this._harmony = new Harmony(PluginGuid);
_harmony = new Harmony(PluginGuid);
// 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())
{
L.Error("Marshal failed to init");
return;
}
this._harmony.PatchAll();
_harmony.PatchAll();
L.Debug("Ran Harmony patches");
}
public void OnDestroy()
{
Marshal.Destroy();
_harmony?.UnpatchSelf();
L.Debug("Cleaned up Harmony patches");
}
}
}

View File

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

302
docs/builtins.md Normal file
View File

@@ -0,0 +1,302 @@
# Built-in Functions
<!--toc:start-->
- [Built-in Functions](#built-in-functions)
- [System Functions](#system-functions)
- [`yield()`](#yield)
- [`sleep(ticks)`](#sleepticks)
- [`hash(prefabName)`](#hashprefabname)
- [Device I/O Functions](#device-io-functions)
- [Reading from Devices](#reading-from-devices)
- [Load from device](#load-from-device)
- [Load From Device Batched](#load-from-device-batched)
- [Load From Device Batched Named](#load-from-device-batched-named)
- [Load Slot](#load-slot)
- [Load Reagent](#load-reagent)
- [Writing to Devices](#writing-to-devices)
- [Set On Device](#set-on-device)
- [Set On Device Batched](#set-on-device-batched)
- [Set On Device Batched Named](#set-on-device-batched-named)
- [Set Slot](#set-slot)
- [Math Functions](#math-functions)
- [Trigonometric Functions](#trigonometric-functions)
- [Trig Example](#trig-example)
- [Rounding Functions](#rounding-functions)
- [Rounding Example](#rounding-example)
- [Other Math Functions](#other-math-functions)
- [Math Example](#math-example)
- [See Also](#see-also)
<!--toc:end-->
Slang provides built-in functions for device I/O and mathematical operations.
These map directly to IC10 instructions.
## System Functions
### `yield()`
Pauses execution for exactly one game tick.
```rust
yield();
```
**IC10:** `yield`
---
### `sleep(ticks)`
Pauses execution for the specified number of ticks.
```rust
sleep(10); // Sleep for 10 ticks
```
**IC10:** `sleep ticks`
---
### `hash(prefabName)`
Computes the in-game hash for a prefab name. The hash is computed at compile
time and no runtime code is generated.
```rust
const AC_HASH = hash("StructureAirConditioner");
```
**Note:** This is different from IC10's `hash` instruction, which computes the
hash at runtime.
```rust
setBatched(AC_HASH, "On", 0);
```
**IC10:** `sb -2087593337 On 0` (no hash computation at runtime)
---
## Device I/O Functions
### Reading from Devices
#### Load from device
`load(device, property)` / `l(device, property)`
Loads a property value from a device:
```rust
let temp = load(sensor, "Temperature");
let temp = l(sensor, "Temperature");
// Preferred: use dot notation
let temp = sensor.Temperature;
```
**IC10:** `l r? d? var`
---
#### Load From Device Batched
`loadBatched(deviceHash, property, batchMode)` / `lb(...)`
Loads a property from all devices matching a hash, aggregated by batch mode:
```rust
const SENSOR = hash("StructureGasSensor");
let avgTemp = loadBatched(SENSOR, "Temperature", "Average");
let maxTemp = lb(SENSOR, "Temperature", "Maximum");
```
**Batch Modes:** `"Average"`, `"Sum"`, `"Minimum"`, `"Maximum"`
**IC10:** `lb r? deviceHash logicType batchMode`
---
#### Load From Device Batched Named
`loadBatchedNamed(deviceHash, nameHash, property, batchMode)` / `lbn(...)`
Loads a property from devices matching both device hash and name hash:
```rust
const SENSOR_HASH = hash("StructureGasSensor");
const SENSOR_NAME_HASH = hash("Outdoor Gas Sensor");
let avgTemp = loadBatchedNamed(SENSOR_HASH, SENSOR_NAME_HASH, "Temperature", "Average");
let maxTemp = lbn(SENSOR_HASH, SENSOR_NAME_HASH, "Temperature", "Maximum");
```
**IC10:** `lbn r? deviceHash nameHash logicType batchMode`
**Note:** This function is useful when a script interfaces with a lot of
devices, as it allows for arbitrary device access without limited to the 6 `dx` pins.
---
#### Load Slot
`loadSlot(device, slotIndex, property)` / `ls(...)`
Loads a slot property from a device:
```rust
let occupied = loadSlot(sorter, 0, "Occupied");
let occupied = ls(sorter, 0, "Occupied");
```
**IC10:** `ls r? d? slotIndex logicSlotType`
---
#### Load Reagent
`loadReagent(device, reagentMode, reagentHash)` / `lr(...)`
Loads reagent information from a device:
```rust
let amount = loadReagent(furnace, "Contents", reagentHash);
let amount = lr(furnace, "Contents", reagentHash);
```
**IC10:** `lr r? d? reagentMode reagentHash`
---
### Writing to Devices
#### Set On Device
`set(device, property, value)` / `s(...)`
Sets a property on a device:
```rust
set(valve, "On", true);
s(valve, "On", true);
// Preferred: use dot notation
valve.On = true;
```
**IC10:** `s d? logicType r?`
---
#### Set On Device Batched
`setBatched(deviceHash, property, value)` / `sb(...)`
Sets a property on all devices matching a hash:
```rust
const LIGHT_HASH = hash("StructureWallLight");
setBatched(LIGHT_HASH, "On", true);
sb(LIGHT_HASH, "On", true);
```
**IC10:** `sb deviceHash logicType r?`
**Note:** This function is useful when a script interfaces with a lot of devices,
as it allows for arbitrary device access without limited to the 6 `dx` pins.
---
#### Set On Device Batched Named
`setBatchedNamed(deviceHash, nameHash, property, value)` / `sbn(...)`
Sets a property on devices matching both device hash and name hash:
```rust
const SENSOR_HASH = hash("StructureGasSensor");
const SENSOR_NAME_HASH = hash("Outdoor Gas Sensor");
setBatchedNamed(SENSOR_HASH, SENSOR_NAME_HASH, "On", true);
sbn(SENSOR_HASH, SENSOR_NAME_HASH, "On", true);
```
**IC10:** `sbn deviceHash nameHash logicType r?`
---
#### Set Slot
`setSlot(device, slotIndex, property, value)` / `ss(...)`
Sets a slot property on a device:
```rust
setSlot(sorter, 0, "Open", true);
ss(sorter, 0, "Open", true);
```
**IC10:** `ss d? slotIndex logicSlotType r?`
---
## Math Functions
All math functions accept numbers, variables, or expressions as arguments.
### Trigonometric Functions
| Function | Description | IC10 |
| ------------- | ---------------------------- | ------- |
| `sin(x)` | Sine of angle in radians | `sin` |
| `cos(x)` | Cosine of angle in radians | `cos` |
| `tan(x)` | Tangent of angle in radians | `tan` |
| `asin(x)` | Arc sine, returns radians | `asin` |
| `acos(x)` | Arc cosine, returns radians | `acos` |
| `atan(x)` | Arc tangent, returns radians | `atan` |
| `atan2(y, x)` | Two-argument arc tangent | `atan2` |
#### Trig Example
```rust
let angle = atan2(y, x);
let sineValue = sin(angle);
```
### Rounding Functions
| Function | Description | IC10 |
| ---------- | ----------------------------- | ------- |
| `ceil(x)` | Round up to nearest integer | `ceil` |
| `floor(x)` | Round down to nearest integer | `floor` |
| `trunc(x)` | Remove decimal portion | `trunc` |
| `abs(x)` | Absolute value | `abs` |
#### Rounding Example
```rust
let rounded = floor(3.7); // 3
let positive = abs(-5); // 5
```
### Other Math Functions
| Function | Description | IC10 |
| ----------- | ----------------------------- | ------ |
| `sqrt(x)` | Square root | `sqrt` |
| `log(x)` | Natural logarithm | `log` |
| `max(a, b)` | Maximum of two values | `max` |
| `min(a, b)` | Minimum of two values | `min` |
| `rand()` | Random number between 0 and 1 | `rand` |
#### Math Example
```rust
let root = sqrt(16); // 4
let bigger = max(a, b);
let randomVal = rand();
```
## See Also
- [Language Reference](language-reference.md) — Complete syntax guide
- [Examples](examples.md) — Real-world code samples

254
docs/examples.md Normal file
View File

@@ -0,0 +1,254 @@
# Examples
Real-world Slang programs demonstrating common patterns.
## Temperature Control
Basic thermostat that controls an air conditioner based on room temperature:
```rust
device ac = "db";
device roomGasSensor = "d0";
const TARGET_TEMP = 22c;
const HYSTERESIS = 1;
loop {
yield();
let temp = roomGasSensor.Temperature;
if (temp > TARGET_TEMP + HYSTERESIS) {
ac.On = true;
} else if (temp < TARGET_TEMP - HYSTERESIS) {
ac.On = false;
}
}
```
**Note:** The IC10 chip is assumed to be inserted in the air conditioner's IC slot.
---
## Two-Axis Solar Panel Tracking
Handles two-axis solar panel tracking based on the sun's position:
```rust
device sensor = "d0";
const H_PANELS = hash("StructureSolarPanelDual");
loop {
setBatched(H_PANELS, "Horizontal", sensor.Horizontal);
setBatched(H_PANELS, "Vertical", sensor.Vertical + 90);
yield();
}
```
**Note:** Assumes the daylight sensor is mounted with its port looking 90
degrees east of the solar panel's data port, an offset can be added on the
horizontal angle if needed.
---
## Day/Night Lighting
Controls grow lights during the day and ambient lights at night:
```rust
device greenhouseSensor = "d0";
const daylightSensor = hash("StructureDaylightSensor");
const growLight = hash("StructureGrowLight");
const wallLight = hash("StructureLightLong");
loop {
yield();
let solarAngle = lb(daylightSensor, "SolarAngle", "Average");
let isDaylight = solarAngle < 90;
sb(growLight, "On", isDaylight);
sb(wallLight, "On", !isDaylight);
}
```
---
## Pressure Relief Valve
Controls a volume pump based on pressure readings for emergency pressure relief:
```rust
device volumePump = "d0";
device pipeSensor = "d1";
const MAX_PRESSURE = 10_000;
const R = 8.314;
loop {
yield();
let pressure = pipeSensor.Pressure;
if (pressure > MAX_PRESSURE) {
// Use PV=nRT to calculate the amount of mols we need to move
// n = PV / RT
let molsToMove = (pressure - MAX_PRESSURE) *
pipeSensor.Volume / (R * pipeSensor.Temperature);
// V = nRT / P
let setting = molsToMove * R * pipeSensor.Temperature / pressure;
volumePump.Setting = setting;
volumePump.On = true;
} else {
volumePump.On = false;
}
}
```
---
## Greenhouse Environment Controller
Complete greenhouse control with pressure, temperature, and lighting:
```rust
device self = "db";
device emergencyRelief = "d0";
device greenhouseSensor = "d1";
device recycleValve = "d2";
const MAX_INTERIOR_PRESSURE = 80;
const MAX_INTERIOR_TEMP = 28c;
const MIN_INTERIOR_PRESSURE = 75;
const MIN_INTERIOR_TEMP = 25c;
const daylightSensor = 1076425094;
const growLight = hash("StructureGrowLight");
const wallLight = hash("StructureLightLong");
const lightRound = hash("StructureLightRound");
let shouldPurge = false;
loop {
yield();
let interiorPress = greenhouseSensor.Pressure;
let interiorTemp = greenhouseSensor.Temperature;
shouldPurge = (
interiorPress > MAX_INTERIOR_PRESSURE ||
interiorTemp > MAX_INTERIOR_TEMP
) || shouldPurge;
emergencyRelief.On = shouldPurge;
recycleValve.On = !shouldPurge;
if (
shouldPurge && (
interiorPress < MIN_INTERIOR_PRESSURE &&
interiorTemp < MIN_INTERIOR_TEMP
)
) {
shouldPurge = false;
}
let solarAngle = lb(daylightSensor, "SolarAngle", "Average");
let isDaylight = solarAngle < 90;
sb(growLight, "On", isDaylight);
sb(wallLight, "On", !isDaylight);
sb(lightRound, "On", !isDaylight);
}
```
---
## Advanced Furnace Pressure Control
Automates multi-furnace pump control based on dial setting for pressure target:
```rust
const FURNACE1 = 1234;
const DIAL1 = 1123;
const ANALYZER1 = 1223;
const FURNACE2 = 1235;
const DIAL2 = 1124;
const ANALYZER2 = 1224;
const FURNACE3 = 1236;
const DIAL3 = 1124;
const ANALYZER3 = 1225;
const R = 8.314;
fn handleFurnace(furnace, dial, analyzer) {
let pressure = furnace.Pressure;
let targetPressure = max(dial.Setting, 0.1) * 1000;
if (abs(targetPressure - pressure) <= 0.1) {
furnace.On = false;
return;
}
let molsToMove = max(furnace.TotalMoles, 1) * (
(targetPressure / pressure) - 1
);
// V = nRT / P
if (molsToMove > 0) {
// Calculate volume required
if (analyzer.Pressure == 0) {
// No more gas to add
furnace.On = false;
return;
}
let volume = molsToMove * R * analyzer.Temperature / analyzer.Pressure;
furnace.On = true;
furnace.SettingOutput = 0;
furnace.SettingInput = volume;
return;
}
// Calculate volume required
let volume = (-molsToMove) * R * furnace.Temperature / pressure;
furnace.On = true;
furnace.SettingInput = 0;
furnace.SettingOutput = volume;
return;
}
loop {
yield();
handleFurnace(FURNACE1, DIAL1, ANALYZER1);
handleFurnace(FURNACE2, DIAL2, ANALYZER2);
handleFurnace(FURNACE3, DIAL3, ANALYZER3);
}
```
**Note:** This example does not handle edge cases such as insufficient gas in
the input network or overfilling the furnace/pipe network.
---
## Common Patterns
### Waiting for a Condition
```rust
fn waitForDeviceToTurnOff(device) {
while (device.On) {
yield();
}
}
```
## See Also
- [Getting Started](getting-started.md) — First steps with Slang
- [Language Reference](language-reference.md) — Complete syntax guide
- [Built-in Functions](builtins.md) — System calls and math functions

99
docs/getting-started.md Normal file
View File

@@ -0,0 +1,99 @@
# Getting Started
<!--toc:start-->
- [Getting Started](#getting-started)
- [Program Structure](#program-structure)
- [The `yield()` Function](#the-yield-function)
- [Your First Program](#your-first-program)
- [Explanation](#explanation)
- [Comments](#comments)
- [See Also](#see-also)
<!--toc:end-->
This guide covers the basics of writing your first Slang program.
## Program Structure
A Slang program consists of top-level declarations and a main loop:
```rust
// Device declarations
device self = "db";
device sensor = "d0";
// Constants
const THRESHOLD = 100;
// Variables
let counter = 0;
// Main program loop
loop {
yield();
// Your logic here
}
```
## The `yield()` Function
IC10 programs run continuously. The `yield()` function pauses execution for one
game tick, preventing the script from consuming excessive resources.
**Important:** You should always include `yield()` in your main loop unless you
know what you're doing.
```rust
loop {
yield(); // Recommended!
// ...
}
```
## Your First Program
Here's a simple program that turns on a light when a gas sensor detects low
pressure:
```rust
device gasSensor = "d0";
device light = "d1";
const LOW_PRESSURE = 50;
loop {
yield();
light.On = gasSensor.Pressure < LOW_PRESSURE;
}
```
### Explanation
1. `device gasSensor = "d0"` — Binds the device at port `d0` to the name
`gasSensor`
2. `device light = "d1"` — Binds the device at port `d1` to the name `light`
3. `const LOW_PRESSURE = 50` — Defines a compile-time constant
4. `loop { ... }` — Creates an infinite loop
5. `yield()` — Pauses for one tick
6. `light.On = gasSensor.Pressure < LOW_PRESSURE` — Reads the pressure and sets
the light state
## Comments
Slang supports single-line comments and documentation comments:
```rust
// This is a regular comment
/// This is a documentation comment
/// It can span multiple lines
fn myFunction() {
// ...
}
```
## See Also
- [Language Reference](language-reference.md) — Complete syntax guide
- [Built-in Functions](builtins.md) — Available system calls
- [Examples](examples.md) — Real-world programs and patterns

339
docs/language-reference.md Normal file
View File

@@ -0,0 +1,339 @@
# Language Reference
<!--toc:start-->
- [Language Reference](#language-reference)
- [Literals](#literals)
- [Numbers](#numbers)
- [Temperature Literals](#temperature-literals)
- [Booleans](#booleans)
- [Strings](#strings)
- [Variables](#variables)
- [`let` - Mutable Variables](#let-mutable-variables)
- [`const` - Constants](#const-constants)
- [Device Declarations](#device-declarations)
- [Device Property Access](#device-property-access)
- [Device Property Assignment](#device-property-assignment)
- [Operators](#operators)
- [Arithmetic Operators](#arithmetic-operators)
- [Comparison Operators](#comparison-operators)
- [Logical Operators](#logical-operators)
- [Ternary Operator](#ternary-operator)
- [Operator Precedence](#operator-precedence)
- [Control Flow](#control-flow)
- [`if` / `else`](#if-and-else)
- [`loop`](#loop)
- [`while`](#while)
- [`break`](#break)
- [`continue`](#continue)
- [Functions](#functions)
- [Declaration](#declaration)
- [Invocation](#invocation)
- [Return Values](#return-values)
- [Parentheses for Grouping](#parentheses-for-grouping)
- [See Also](#see-also)
<!--toc:end-->
Complete syntax reference for the Slang programming language.
## Literals
### Numbers
Numbers can be integers or decimals. Underscores are allowed as visual
separators:
```rust
const integer = 42; // Integer
const decimal = 3.14; // Decimal
const million = 1_000_000; // Integer with separators
const decimalSeparators = 5_000.50; // Decimal with separators
```
### Temperature Literals
Append a unit suffix to specify temperature. Values are automatically converted
to Kelvin at compile time:
| Suffix | Unit | Example |
| ------ | ---------- | ------- |
| `c` | Celsius | `20c` |
| `f` | Fahrenheit | `68f` |
| `k` | Kelvin | `293k` |
```rust
const ROOM_TEMP = 20c; // Converts to 293.15 Kelvin
const FREEZING = 32f; // Converts to 273.15 Kelvin
const ABSOLUTE1 = 0k; // Already in Kelvin
const ABSOLUTE2 = 0; // Assumed to be in Kelvin
```
### Booleans
Booleans compile to integer values `1` and `0` in IC10.
```rust
device ac = "d0";
ac.Mode = false;
ac.On = true;
```
### Strings
Strings use double or single quotes. They are primarily used for prefab and
name hashes.
```rust
const AC_HASH = hash("StructureAirConditioner");
const AC_NAME_HASH = hash("Greenhouse Air Conditioner");
```
## Variables
### `let` Mutable Variables
Declares a variable that can be reassigned:
```rust
let counter = 0;
// ...
counter = counter + 1;
```
### `const` Constants
Declares a compile-time constant. Constants are inlined and do not consume
registers:
```rust
const MAX_PRESSURE = 10_000;
const DOOR_HASH = hash("StructureCompositeDoor");
```
Constants support the `hash()` function for compile-time hash computation.
## Device Declarations
The `device` keyword binds a device port or reference ID to a named variable:
```rust
device self = "db"; // IC housing, or device the IC is plugged into (eg. an AC)
device sensor = "d0"; // Device at port d0
device valve = "d1"; // Device at port d1
device ac1 = "$3FC"; // Device with reference ID $3FC (hexadecimal 1020)
device ac2 = "1020"; // Device with reference ID 1020 (decimal)
```
**Note:** Reference IDs can be found in-game using the Configuration cartridge.
### Device Property Access
Read device properties using dot notation:
```rust
let temp = sensor.Temperature;
let pressure = sensor.Pressure;
let isOn = valve.On;
```
### Device Property Assignment
Write to device properties using dot notation:
```rust
valve.On = true;
valve.Setting = 100;
```
## Operators
### Arithmetic Operators
| Operator | Description | Example |
| -------- | -------------- | -------- |
| `+` | Addition | `a + b` |
| `-` | Subtraction | `a - b` |
| `*` | Multiplication | `a * b` |
| `/` | Division | `a / b` |
| `%` | Modulo | `a % b` |
| `**` | Exponentiation | `a ** b` |
| `-` | Negation | `-a` |
### Comparison Operators
| Operator | Description | Example |
| -------- | --------------------- | -------- |
| `==` | Equal | `a == b` |
| `!=` | Not equal | `a != b` |
| `<` | Less than | `a < b` |
| `>` | Greater than | `a > b` |
| `<=` | Less than or equal | `a <= b` |
| `>=` | Greater than or equal | `a >= b` |
### Logical Operators
| Operator | Description | Example |
| -------- | ----------- | ---------- |
| `&&` | Logical AND | `a && b` |
| `\|\|` | Logical OR | `a \|\| b` |
| `!` | Logical NOT | `!a` |
### Ternary Operator
Conditional expressions using `?` and `:`:
```rust
let result = condition ? valueIfTrue : valueIfFalse;
```
### Operator Precedence
Operators are evaluated in the following order, from highest to lowest
precedence:
| Precedence | Operator(s) | Description |
| ---------- | ----------------- | -------------------------------- |
| 1 | `()` `.` | Grouping, Property access |
| 2 | `!` `-` | Logical NOT, Negation |
| 3 | `**` | Exponentiation |
| 4 | `*` `/` `%` | Multiplication, Division, Modulo |
| 5 | `+` `-` | Addition, Subtraction |
| 6 | `<` `<=` `>` `>=` | Comparison |
| 7 | `==` `!=` | Equality |
| 8 | `&&` | Logical AND |
| 9 | `\|\|` | Logical OR |
| 10 | `?:` | Ternary conditional |
| 11 | `=` | Assignment |
Use parentheses to override precedence:
```rust
let result = (20 + 10) * 5;
```
## Control Flow
### if and else
Conditional branching:
```rust
if (tank.Temperature > 30c) {
ac.On = true;
} else {
ac.On = false;
}
```
### `loop`
Infinite loop that runs until `break`:
```rust
loop {
yield();
// Loop body
if (condition) {
break; // Exit the loop
}
}
```
### `while`
Conditional loop that runs while the condition is true:
```rust
while (counter < 100) {
counter = counter + 1;
yield();
}
```
### `break`
Exits the current loop:
```rust
loop {
yield();
// ...
if (done) {
break;
}
}
```
### `continue`
Skips to the next iteration of the current loop:
```rust
loop {
yield();
if (shouldSkip) {
continue;
}
// This code is skipped when shouldSkip is true
// ...
}
```
## Functions
**Warning:** Functions are currently experimental and may produce suboptimal code.
### Declaration
```rust
fn functionName(arg1, arg2) {
// Function body
return arg1 + arg2;
}
```
### Invocation
```rust
let result = functionName(10, 20);
```
### Return Values
Use `return` to exit a function and optionally return a value:
```rust
fn calculate(x) {
if (x < 0) {
return 0; // Early return
}
return x * 2;
}
fn doWork() {
// No return value
return;
}
```
## Parentheses for Grouping
Use parentheses to control operator precedence:
```rust
let result = (a + b) * c;
let complex = (
temp > 0c &&
stress < 50 &&
(pressure < 10_000 || temp > 20c)
);
```
## See Also
- [Getting Started](getting-started.md) — First steps with Slang
- [Built-in Functions](builtins.md) — System calls and math functions
- [Examples](examples.md) — Real-world code samples

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -168,3 +168,28 @@ fn test_const_hash_expr() -> anyhow::Result<()> {
);
Ok(())
}
#[test]
fn test_declaration_is_const() -> anyhow::Result<()> {
let compiled = compile! {
debug
r#"
const MAX = 100;
let max = MAX;
"#
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 100
"
}
);
Ok(())
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -714,7 +714,12 @@ impl<'a> Compiler<'a> {
Operand::Register(VariableScope::TEMP_STACK_REGISTER)
}
VariableLocation::Constant(_) | VariableLocation::Device(_) => unreachable!(),
VariableLocation::Constant(Literal::Number(num)) => Operand::Number(num.into()),
VariableLocation::Constant(Literal::Boolean(b)) => {
Operand::Number(Number::from(b).into())
}
VariableLocation::Device(_)
| VariableLocation::Constant(Literal::String(_)) => unreachable!(),
};
self.emit_variable_assignment(&var_loc, src)?;
(var_loc, None)
@@ -2233,10 +2238,7 @@ impl<'a> Compiler<'a> {
System::LoadSlot(dev_name, slot_index, logic_type) => {
let (dev_hash, hash_cleanup) =
self.compile_literal_or_variable(dev_name.node, scope)?;
let (slot_index, slot_cleanup) = self.compile_literal_or_variable(
LiteralOrVariable::Literal(slot_index.node),
scope,
)?;
let (slot_index, slot_cleanup) = self.compile_operand(*slot_index, scope)?;
let (logic_type, logic_cleanup) = self.compile_literal_or_variable(
LiteralOrVariable::Literal(logic_type.node),
scope,
@@ -2261,10 +2263,7 @@ impl<'a> Compiler<'a> {
System::SetSlot(dev_name, slot_index, logic_type, var) => {
let (dev_name, name_cleanup) =
self.compile_literal_or_variable(dev_name.node, scope)?;
let (slot_index, index_cleanup) = self.compile_literal_or_variable(
LiteralOrVariable::Literal(slot_index.node),
scope,
)?;
let (slot_index, index_cleanup) = self.compile_operand(*slot_index, scope)?;
let (logic_type, type_cleanup) = self.compile_literal_or_variable(
LiteralOrVariable::Literal(logic_type.node),
scope,

View File

@@ -1834,20 +1834,8 @@ impl<'a> Parser<'a> {
let mut args = args!(3);
let next = args.next();
let dev_name = literal_or_variable!(next);
let next = args.next();
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 slot_index = args.next().ok_or(Error::UnexpectedEOF)?;
let next = args.next();
let slot_logic = get_arg!(Literal, literal_or_variable!(next));
if !matches!(
@@ -1864,27 +1852,17 @@ impl<'a> Parser<'a> {
}
Ok(SysCall::System(System::LoadSlot(
dev_name, slot_index, slot_logic,
dev_name,
boxed!(slot_index),
slot_logic,
)))
}
"setSlot" | "ss" => {
let mut args = args!(4);
let next = args.next();
let dev_name = literal_or_variable!(next);
let next = args.next();
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 slot_index = args.next().ok_or(Error::UnexpectedEOF)?;
let next = args.next();
let slot_logic = get_arg!(Literal, literal_or_variable!(next));
if !matches!(
@@ -1904,9 +1882,9 @@ impl<'a> Parser<'a> {
Ok(SysCall::System(System::SetSlot(
dev_name,
slot_index,
boxed!(slot_index),
slot_logic,
Box::new(expr),
boxed!(expr),
)))
}
"loadReagent" | "lr" => {

View File

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

View File

@@ -1,43 +0,0 @@
device self = "db";
device gasSensor = "d0";
device atmosAnal = "d1";
device atmosValve = "d2";
device atmosTank = "d3";
device atmosInlet = "d4";
atmosInlet.Lock = true;
atmosInlet.Mode = 1;
atmosValve.On = false;
atmosValve.Lock = true;
let isPumping = false;
let tempPressure = 0;
loop {
yield();
let temp = gasSensor.Temperature;
let pres = atmosAnal.Pressure;
let liqV = atmosAnal.VolumeOfLiquid;
let tempVol = atmosAnal.Volume;
let stress = 5_000 * liqV / tempVol;
tempPressure = isPumping ? 1_000 : 10_000;
let shouldTurnOnInlet = (
temp > 0c &&
pres < tempPressure &&
stress < 50
);
isPumping = (
!shouldTurnOnInlet &&
atmosTank.Pressure < 35_000 &&
atmosAnal.RatioPollutant == 0 &&
atmosAnal.RatioLiquidPollutant == 0 &&
atmosAnal.Pressure > 1_000
);
atmosValve.On = isPumping;
atmosInlet.On = shouldTurnOnInlet;
}

View File

@@ -1,72 +0,0 @@
/// Laree script V1
device self = "db";
device larre = "d0";
device exportChute = "d1";
const TOTAL_SLOTS = 19;
const EXPORT_CHUTE = 1;
const START_STATION = 2;
let currentIndex = 0;
/// Waits for the larre to be idle before continuing
fn waitForIdle() {
yield();
while (!larre.Idle) {
yield();
}
}
/// Instructs the Larre to go to the chute and deposit
/// what is currently in its arm
fn deposit() {
larre.Setting = EXPORT_CHUTE;
waitForIdle();
larre.Activate = true;
waitForIdle();
exportChute.Open = false;
}
/// This function is responsible for checking the plant under
/// the larre at this index, and harvesting if applicable
fn checkAndHarvest(currentIndex) {
if (currentIndex <= EXPORT_CHUTE || ls(larre, 255, "Seeding") < 1) {
return;
}
// harvest from this device
while (ls(larre, 255, "Mature")) {
yield();
larre.Activate = true;
}
let hasRemainingPlant = ls(larre, 255, "Occupied");
// move to the export chute
larre.Setting = EXPORT_CHUTE;
waitForIdle();
deposit();
if (hasRemainingPlant) {
deposit();
}
larre.Setting = currentIndex;
waitForIdle();
if (ls(larre, 0, "Occupied")) {
larre.Activate = true;
}
waitForIdle();
}
loop {
yield();
if (!larre.Idle) {
continue;
}
let newIndex = currentIndex + 1 > TOTAL_SLOTS ? START_STATION : currentIndex + 1;
checkAndHarvest(currentIndex);
larre.Setting = newIndex;
currentIndex = newIndex;
}