LuaComb++
Overview
LuaComb++ executes restricted Lua scripts inside a circuit combinator. A script can read the green and red input wires, combine their signals, retain controlled data between executions, and produce independent persistent outputs.
The project began as a fork of Moon Logic 2 by Chilla55 and has since been extensively reworked around a guided editing workflow, Factorio 2.0 signal qualities, deterministic execution, and multiplayer safety.
Main features
Circuit interface
- Read the green wire with
g, the red wire withr, or their combined total withget. - Address signals by name, type, and quality.
- Aggregate all qualities or select one exact numeric quality level.
- Write independently to the green or red output wire.
- Keep outputs active until they are changed, removed with count
0, or cleared explicitly.
State and scheduling
- Store private per-combinator data in
self. - Share data-only global variables between LuaComb++ combinators belonging to the same force.
- Choose an execution interval from the editor: every tick, every 6 ticks, or once per second.
- Preserve the last valid outputs if execution fails.
Guided editor
- Live input and output signal panels.
- Contextual suggestions for Lua, signals, properties, and LuaComb++ functions.
- Factorio signal picker for exact signal descriptors.
- Script formatting and translated Help.
- Recovery of the last successfully compiled script.
- Personal library for up to ten named scripts.
- Settings copy and blueprint support.
Runtime protection
- Restricted, deterministic Lua environment.
- Limits for instructions, source size, strings, tables, nesting, persistent data, and signals.
- Isolated failure handling for each combinator.
- Force-scoped alerts for connected players when a script is disabled.
Design lineage and focus
Moon Logic 2 and LuaComb++ share the same central idea: express circuit-network logic with Lua. Their interfaces emphasize different styles of use.
| Area | Moon Logic 2 approach | LuaComb++ approach |
|---|---|---|
| Editing workflow | Compact code window designed for short scripts and pasted logic | Guided editor with live panels, completion, formatting, Help, and recovery tools |
| Circuit access | red, green, and out tables |
Quality-aware g, r, and get records with explicit output functions |
| Signal identity | Direct name-based wire access | Explicit type + name + quality identity with convenient aggregate access |
| Persistent state | Per-combinator var data |
Per-combinator self data plus force-wide shared globals |
| Scheduling | Script-controlled delay and irq values |
Per-combinator execution profile selected from the editor |
| Output style | Assignment through the persistent out table |
Persistent set, g.set, r.set, and clear operations |
| Runtime goal | Lightweight programmable circuit transformations | Bounded execution, deterministic shared state, and controlled multiplayer behavior |
| Mods data | Great feuture wich provide data from prototypes and other mods stuff | Not implemented |
LuaComb++ keeps the programmable-combinator concept while offering a workflow aimed at players who want to build and debug scripts directly inside the game.
Quick start
- Place a LuaComb++ combinator.
- Connect its input and output circuit wires.
- Open the editor and choose an execution interval.
- Write the script.
- Press Compile, validate and save.
Editor changes remain drafts until validation succeeds.
local plates = get["iron-plate"].count
if plates < 100 then
g.set("signal-A", 1)
else
r.set("signal-A", 1)
end
Script reference
Available functions
LuaComb++
| Function | Description |
|---|---|
clear() |
Removes every persistent output. |
set(signal, count [, quality [, type]]) |
Writes a signal to both output wires. |
g.set(signal, count [, quality [, type]]) |
Writes a signal to the green output. |
r.set(signal, count [, quality [, type]]) |
Writes a signal to the red output. |
random(min, max) |
Returns a random integer between the inclusive limits. |
print(...) |
Adds values to the editor State panel. |
concat(table [, separator [, first [, last]]]) |
Joins values into a size-limited string. |
random uses a private deterministic sequence for each combinator. Its state is
saved with the map, and script validation does not advance the active sequence.
Basic Lua
| Function | Description |
|---|---|
pairs(table) |
Iterates over table keys and values. |
ipairs(table) |
Iterates over consecutive numeric indices. |
next(table [, index]) |
Returns the next table entry. |
select(index, ...) |
Selects values from variable arguments. |
tonumber(value [, base]) |
Converts a value to a number. |
tostring(value) |
Converts a value to text. |
type(value) |
Returns the Lua value type. |
assert(condition [, message]) |
Raises an error when a condition is false. |
error(message [, level]) |
Stops the script with an error. |
Mathematics
The shortcuts below are also available through math where applicable.
| Function | Description |
|---|---|
abs(x) |
Absolute value. |
ceil(x) |
Rounds upward. |
cos(x) |
Cosine in radians. |
exp(x) |
Exponential value. |
floor(x) |
Rounds downward. |
log(x) |
Natural logarithm. |
max(...) |
Largest argument. |
min(...) |
Smallest argument. |
sin(x) |
Sine in radians. |
sqrt(x) |
Square root. |
tan(x) |
Tangent in radians. |
math.acos(x) |
Inverse cosine. |
math.asin(x) |
Inverse sine. |
math.atan(x) |
Inverse tangent. |
math.atan2(y, x) |
Angle from two coordinates. |
math.cosh(x) |
Hyperbolic cosine. |
math.deg(x) |
Converts radians to degrees. |
math.fmod(x, y) |
Floating-point remainder. |
math.frexp(x) |
Splits a number into fraction and exponent. |
math.ldexp(m, e) |
Produces m * 2^e. |
math.log(x [, base]) |
Logarithm with an optional base. |
math.log10(x) |
Base-10 logarithm. |
math.modf(x) |
Splits integer and fractional parts. |
math.pow(x, y) |
Raises x to the power y. |
math.rad(x) |
Converts degrees to radians. |
math.sinh(x) |
Hyperbolic sine. |
math.tanh(x) |
Hyperbolic tangent. |
math.pi and math.huge are available as read-only constants.
Text and tables
| Function | Description |
|---|---|
string.byte(text [, first [, last]]) |
Returns character byte values. |
string.char(...) |
Builds text from byte values. |
string.find(text, pattern [, init [, plain]]) |
Finds a pattern or literal text. |
string.len(text) |
Returns the byte length. |
string.lower(text) |
Converts text to lowercase. |
string.match(text, pattern [, init]) |
Returns the first pattern match. |
string.reverse(text) |
Reverses text. |
string.sub(text, first [, last]) |
Extracts part of a string. |
string.upper(text) |
Converts text to uppercase. |
table.concat(table [, separator [, first [, last]]]) |
Joins table values safely. |
table.insert(table, [position,] value) |
Inserts a value. |
table.remove(table [, position]) |
Removes and returns a value. |
table.sort(table [, compare]) |
Sorts a table in place. |
Reading signals
Every signal record exposes:
| Property | Meaning |
|---|---|
.count |
Signed signal quantity |
.quality |
Numeric quality level |
.quality_level |
Alias of .quality |
.type |
Signal prototype type |
g["iron-plate"].count -- all qualities on green
r["iron-plate"][2].count -- exact quality level 2 on red
get["iron-plate"].count -- all qualities on both wires
get["iron-plate"][0].count -- base quality on both wires
Unqualified access aggregates every quality. Its .quality is the highest quality whose count is not zero, including negative signals. A valid signal that is absent returns count and quality level 0.
Use a complete descriptor when the signal type must be explicit:
get[{name = "iron-plate", quality = 2, type = "item"}].count
Writing signals
set(signal, count [, quality [, type]])
g.set(signal, count [, quality [, type]])
r.set(signal, count [, quality [, type]])
setwrites to both output wires.g.setwrites only to green.r.setwrites only to red.- Count
0removes the exact signal identity. clear()removes every stored output.
Outputs persist across execution cycles, recompilation, saving, and loading until a script changes them explicitly.
Persistent data
Use self for data owned by one combinator:
self.counter = (self.counter or 0) + 1
Use a normal global variable for data shared by every LuaComb++ combinator in the same force:
production_target = (production_target or 0) + 100
Only data can persist. Functions should normally be declared local. A successful recompilation resets self; destroying the combinator removes its local state.
Complete example: seven-segment counter
This example cycles a display through digits 0 to 9.
Setup
- Import the blueprint provided below or build a seven-segment display.
- Connect virtual signals
signal-Athroughsignal-Gto their corresponding segments. - Connect the LuaComb++ green output wire to the display.
- Select 1 execution per second in the editor.
- Compile and save the following script.
Script

self.segments = {
'ABCEFG', 'CF', 'ACDEG', 'ACDFG', 'BCDF',
'ABDFG', 'ABDEFG', 'ACF', 'ABCDEFG', 'ABCDFG'
}
self.tickCount = self.tickCount or 1
local pattern = self.segments[self.tickCount]
clear()
for i = 1, string.len(pattern) do
local letter = tostring(string.sub(pattern, i, i))
g.set(concat({'signal-', letter}), 1)
end
self.tickCount = self.tickCount + 1
if self.tickCount >= 11 then
self.tickCount = 1
end
Execution sequence
self.segmentscontains exactly ten patterns. Table indices1through10represent digits0through9.self.tickCountstarts at1and survives between successful executions.- The current counter value selects the pattern for the digit.
clear()removes the preceding digit because output signals are persistent.- The loop extracts one letter at a time with
string.sub. concatbuilds signal names such assignal-A.g.setactivates every required segment on the green output.- After index
10displays digit9, the counter returns to1.
At one execution per second, each digit remains visible for one second. Recompiling the script resets self, so the sequence restarts from digit 0.
Blueprint
Copy the complete string and import it through Factorio's Import string command:
0eNrVWO1u2jAUfZXIf4DNIBJI+JBWaYW2D1EQMsGANceJbIetQnn3XdsESos2wh8ahMjJ9bkfx7l2QvZoyXOaSSY0
Gu8Ri1Oh0Ph1jxTbCMKNTZCEojFSCeG8zUmSoQIjJlb0Dxr7Bb5A5TmJ02SZZW1zYILoVL5zCoo5RlRophl1yezJ20LkyZJKiIr/
GQmjLFXgnAqT01TRCTF6s9WgFZM0dmNDjECOlilfLOmW7Bj4ggORTG8Tqlm8gOGVDaTMgKLm3BiVJmY
+uhilGZXEhUPfUAEJNNlY9rE047Bmm7zkQaB4SxNidcTpyk4e5euOopsElCrvh7dv/HycPD2
/NPAJTJ7N2WT69OKO1vgIR0uaHsjTg5djw/AximEUM2FTgbhfkzQXGnJ9MHip9PyZ4GlMuJcRrakUJaus8PXcZ
z4TMadENlszsQZ3Bnwfe0pD22w6nIrmIU7LW6Uz4cHHhefUmIGtU0duHnxUvix9sMfg22o5Pxiiugkz
GhPd3Ddca7Ub2EUqWtgHIhWr/+v8blTOBFt/HHiA4n1Pb6nwIOenML6Nj0y3Qsadaeyoa/qVLDldobGWOS0K0/cfujbAF5bKp15tdw/
N2uuExeUGjZmMc6YXZynx0XxsWsNdM6n04rQC9VtmKtgxqXOwnEpyE/mIXM5Tg0MPZ0TadTVGD+iist5VyspVGNxD2M
+bhPWvEtatn7DwKmG9e7bi5CZh0VXCgvpdsUG13aNfn91jWGn3CO8hbHqTsFGl3aNGwvxupVVWJ2V+pY2xX5+N0a/4+
DG4h7Sn26T1qkmLaiStX6kf73LRnm9TVu0RJKqRsqjSDW14D2UvtykbVLqjfVFlGOWKQg6eSlX+XbugdVjpHv
dlr+Ico99M2vcorz7u4x4O5vg1wAHgEg0s6h1tBkUWhWcI7o4GAh2PLBqeIXgsMHBkoEtjLKWXw6HDwTuOxa4
E2O5OHIuHDkfvOFFpB2lM0wSUnt5RYbSjUtnpDKNg1B+NwmE/GsBPUfwFSkhVkw==
Safety and multiplayer
LuaComb++ scripts run inside a bounded sandbox. Factorio objects, metatables, coroutines, dynamic code loading, and unrestricted libraries are unavailable.
If compilation, input handling, or runtime execution fails:
- Only the affected combinator is disabled.
- Its last valid outputs remain active.
- Connected players belonging to the same force receive an alert.
- The last valid script and state remain available for recovery.
Scripts imported from untrusted blueprints should always be reviewed before being enabled.
Credits
LuaComb++ is derived from:
- Moon Logic 2 by Chilla55 and contributors
- Moon Logic by mk-fg
- Sandboxed LuaCombinator by IWTDU
- LuaCombinator 2 by OwnlyMe
Original and upstream contributors include OwnlyMe, IWTDU, mk-fg, smartguy1196, and Chilla55.
Development continues as time permits.