Analytical Combinator

by bruno77

A programmable circuit combinator controlled via a RISC-V inspired assembly language. Supports reading red and green input signals and writing up to four output signals per tick.

Content
7 days ago
2.0
76
Circuit network

Changelog

Version: 0.8.30
Date: 08. 04. 2026
  Features:
    - Added ANDI rd, rs, imm — bitwise AND with immediate (rd = rs & imm)
    - Added ORI  rd, rs, imm — bitwise OR with immediate  (rd = rs | imm)
    - Added XORI rd, rs, imm — bitwise XOR with immediate (rd = rs ^ imm)
      All three accept decimal, hex (0x prefix), and negative immediates.
      Useful for masking, setting, and toggling specific bits without needing
      a register to hold the mask constant.
  Changes:
    - 7 new tests. 174 tests total.
Version: 0.8.29
Date: 08. 04. 2026
  Features:
    - validate_program() now detects duplicate labels at compile time.
      Error format: [label:N] Duplicate label 'name' (first defined on line M)
      The check runs before the instruction scan so all duplicate labels in a
      program are reported in a single save, alongside any other errors.
    - 2 new tests: duplicate label detection, and confirmation that distinct
      labels do not produce false positives. 167 tests total.
Version: 0.8.28
Date: 08. 04. 2026
  Bugfixes:
    - Fixed 'Cannot serialise lua functions' on save. self.tick_fn stored a function
      reference on the cpu object in storage, which Factorio cannot serialise.
    - Fixed boot/step state being shared across all combinator instances. The previous
      attempt used a module-level tick_fn_ndx variable, so any combinator's boot()
      call would switch every other combinator to step() before it had compiled.
  Changes:
    - self.tick_ndx replaces self.tick_fn. It is an integer (0 or 1) stored on each
      cpu object — fully serialisable. 0 = call boot, 1 = call step.
    - TICK_FN = {boot, step} is a module-level table (not in storage).
      cpu:tick() calls TICK_FN[(self.tick_ndx or 0) + 1](self). The 'or 0' handles
      old saves where tick_ndx was not yet present (nil).
    - boot() sets cpu.tick_ndx = 1 after compiling. All subsequent ticks call step().
    - control.lua reattach_metatables() only calls setmetatable() — no other writes
      to storage, fully compliant with on_load constraints.
    - reset_tick_fn() removed; no longer needed.
    - cpu:tick_step() retained for direct step() access in tests.
Version: 0.8.26
Date: 08. 04. 2026
  Bugfixes:
    - Fixed crash on load when analytical combinators were saved with versions
      prior to 0.8.20. Those saves have no compiled program array. All previous
      fix attempts (0.8.21-0.8.25) tried to compile during on_load, which
      Factorio forbids as a storage write — causing a CRC error instead.
  Changes:
    - Replaced cpu:step() with a two-function tick state machine:
        boot(cpu)  — runs on the first tick per session per combinator
        step(cpu)  — runs on every subsequent tick
      cpu:tick() dispatches through self.tick_fn which starts as boot and
      switches to a direct reference to step after the first tick. After
      initialisation the hot path is: one field read, one function call.
    - boot() compiles self.memory if self.compiled is absent or empty,
      handling all pre-compilation save versions with no on_load storage write.
      After compiling, boot sets tick_fn = step and calls step on the same tick.
    - self.tick_fn is a function reference (not serialisable). control.lua calls
      cpu:reset_tick_fn() in reattach_metatables() after every load to restore it.
      reset_tick_fn() writes only to the Lua state, not to storage.
    - WAIT fast path from 0.8.24 retained in step().
    - events.lua: data.cpu:step() -> data.cpu:tick().
    - test.lua: myCpu:step() -> myCpu:tick() throughout (158 call sites).
Version: 0.8.25
Date: 07. 04. 2026
  Bugfixes:
    - Fixed cross-combinator interference caused by tostring(self) as the compiled
      cache key. tostring() returns a table's memory address, which changes every
      time storage is deserialised. This caused stale cache entries, missing cache
      entries, and multiple combinators accidentally sharing entries — producing the
      symptoms of other combinators halting when a new one is placed, or programs
      stopping when an unrelated combinator's code is saved.
    - Replaced tostring(self) with self.unit_number as the stable cache key.
      unit_number is the entity's permanent Factorio identifier, written onto the
      cpu object at placement time (a normal runtime write, not an on_load write)
      and persisted in storage. on_load reads it to rebuild the cache with zero
      storage writes, satisfying Factorio's on_load CRC constraint.
    - Added cpu:set_unit_number(n) called by register_entity() in events.lua.
      New cpus start with unit_number = 0; set_unit_number() moves the cache entry
      to the real key immediately after placement.
Version: 0.8.24
Date: 07. 04. 2026
  Optimizations:
    - Added a WAIT fast path at the very top of step(). When wait_cycles is non-nil,
      step() decrements the counter and returns immediately — before any cache lookup,
      dispatch table access, or function call. For programs that spend most of their
      time in WAIT (e.g. polling loops with WAIT 60), this eliminates nearly all
      per-tick overhead for the waiting period.
    - Simplified the DISPATCH["WAIT"] handler now that the decrement loop has moved
      to the fast path. The handler's only job is to set wait_cycles on first encounter
      and keep IP on the WAIT instruction.
    - Added test: 'WAIT N occupies exactly N ticks before executing the next instruction'
      to permanently lock in the correct tick-counting semantics. 165 tests total.
Version: 0.8.23
Date: 07. 04. 2026
  Bugfixes:
    - Fixed remaining CRC error on load. The 0.8.22 fix moved the compiled array out
      of storage, but still wrote _cache_key onto the cpu object (which lives in
      storage) during on_load(), triggering the same CRC violation.
    - Root cause: any field assignment to a table that is reachable from storage
      counts as a storage write, even if the field is newly added and not the
      serialised data itself.
    - Fix: replaced _cache_key with tostring(self) as the _compiled_cache lookup key.
      tostring() on a Lua table returns its memory address as a string, which is
      unique per object within a session. No field is written to the cpu object
      (storage) at any point during on_load. The cache is purely a Lua-state
      construct, rebuilt from self.memory on every load with zero storage writes.
    - Removed set_cache_key(), _cache_key, and _next_test_key as they are no
      longer needed.
Version: 0.8.22
Date: 07. 04. 2026
  Bugfixes:
    - Fixed CRC error on load caused by 0.8.21 attempting to write the compiled program
      array into storage during on_load(). Factorio explicitly forbids storage writes
      in on_load() as they cause desyncs in multiplayer.
    - Root cause: the compiled array is ephemeral derived data that must never be
      stored in storage. It belongs in a module-level Lua cache that is rebuilt on
      every load from the persisted self.memory source of truth.
  Changes:
    - Introduced _compiled_cache: a module-level table in cpu.lua (outside storage)
      keyed by unit_number. step() looks up the compiled program here instead of on
      the cpu object. The cache is rebuilt in on_load() via rebuild_compiled() which
      only reads self.memory from storage — no writes, no CRC violation.
    - Added cpu:set_cache_key(unit_number) called by register_entity() in events.lua
      to associate a newly-placed cpu with its entity's stable unit_number as the key.
      New cpu objects created before placement use a temporary negative key.
    - Added cpu:rebuild_compiled() for use in on_load and on_configuration_changed.
    - Removed dead code: the input_signals nil-check backfill in reattach_metatables()
      could never fire since input_signals has been present since the first version.
Version: 0.8.21
Date: 07. 04. 2026
  Bugfixes:
    - Fixed crash on load when a save contains combinators created before 0.8.20.
      The compiled program array introduced in 0.8.20 is not present in older saves,
      so step() crashed with 'attempt to index field compiled (a nil value)'.
    - Added module:ensure_compiled() which re-compiles the program from self.memory
      if the compiled field is absent or empty. Called from reattach_metatables() in
      control.lua immediately after metatable re-attachment, covering both on_load
      (normal resume) and on_configuration_changed (mod update). The compilation
      cost is paid once at load time per affected combinator, not per tick.
Version: 0.8.20
Date: 02. 04. 2026
  Optimizations:
    - Replaced the if-elseif instruction dispatcher in step() with a dispatch table
      (Lua table keyed by mnemonic string, values are handler functions). The hot path
      is now: one array index, one table lookup, one function call. Previously it was
      up to 40 string comparisons in the worst case.
    - Added a compile() pass that runs once after successful validation. Each source
      line is pre-parsed into a record { op, a1, a2, a3 } with immediate values
      converted to numbers, label references resolved to line numbers, and mnemonics
      upper-cased. step() now performs zero string operations in the hot path.
    - Removed advance_ip() as a separate method; IP advancement is now handled
      inline in step() based on the handler's return value.
    - The advance_ip test was rewritten to use step() directly.
Version: 0.8.19
Date: 02. 04. 2026
  Features:
    - Added WSIGI od, signal, imm — write signal immediate. Outputs the named signal
      with a constant integer count without needing a register to hold the value.
      Accepts decimal, hex (0x prefix), and negative immediates. The signal name and
      output register are validated at load time along with all other arguments.
      Example: WSIGI o0, signal-red, 255
    - 7 new tests covering WSIGI basic use, hex/negative immediates, load-time
      error detection for bad output register, unknown signal, and non-numeric
      immediate, and multi-output independence. 164 tests total.
  Info:
    - README: corrected signal validation note from 'runtime error' to 'when the
      program is saved', reflecting the load-time validation introduced in 0.8.18.
    - README: WSIGI added to circuit network output table.
Version: 0.8.18
Date: 02. 04. 2026
  Optimizations:
    - Complete refactor of load-time validation. validate_program() now checks every
      instruction in the program once when code is loaded or saved, covering: unknown
      mnemonics, wrong argument counts, invalid registers (x0-x31 and o0-o3), invalid
      immediate values, unknown signal names, and undefined branch/JAL labels.
    - step() now contains zero validation logic (except divide-by-zero which is only
      knowable at runtime). Every instruction handler is pure computation.
    - All instructions are described in a declarative INSTR table with argument type
      descriptors (rd, rs, rt, od, imm, sig, lbl, reg_or_imm). Adding a new instruction
      only requires adding a table entry and a handler in step().
    - Refactored tokenize() into a shared local function used by both validate_program()
      and step(), eliminating the duplicated strip-and-split logic.
    - cpu.lua reduced from 1261 lines to 593 lines. Error-setting sites in step()
      reduced from 116 to 7 (divide-by-zero x4, JR range check, nil IP guard,
      unknown instruction fallback).
    - 13 new tests covering load-time detection of unknown instructions, wrong arg
      counts, bad registers, bad immediates, undefined labels, bad WSIG output
      register, bad WAIT argument, multiple errors in one program. 157 tests total.
Version: 0.8.17
Date: 01. 04. 2026
  Bugfixes:
    - Added regression test for the 0.8.16 off-by-one fix. The test loads a program
      containing RSIGR, RSIGG, RSIG, and WSIG with valid signal names and asserts
      that no error is set at load time. This would have caught the 0.8.15 bug
      (tokens[2] = register name being validated as signal name) immediately.
      145 tests total.
Version: 0.8.16
Date: 01. 04. 2026
  Bugfixes:
    - Fixed validate_signals() indexing the signal-name token at position 2 instead of 3.
      The mnemonic is still present at tokens[1] in the validator, so the signal name is
      at tokens[3] (e.g. RSIG x6, signal-name -> tokens = {RSIG, x6, signal-name}).
      In step() the mnemonic is removed via table.remove() before indexing, so args[2]
      correctly refers to the signal. The mismatch caused the register name (e.g. x6)
      to be validated as a signal name, always failing for any RSIG/RSIGR/RSIGG/WSIG
      instruction. Signal instructions in the position table corrected from 2 to 3.
Version: 0.8.15
Date: 01. 04. 2026
  Optimizations:
    - Moved signal name validation from per-tick (inside step()) to load time
      (inside new() and update_code()). Previously valid_signal_name() was called
      into Factorio's prototype tables on every tick for every RSIG, RSIGR, RSIGG,
      and WSIG instruction. It now runs once via validate_signals() when the program
      is loaded or saved, with zero overhead during normal execution.
    - validate_signals() scans the program's token stream, identifies signal-name
      arguments in the four affected instructions, and pre-populates self.errors
      with descriptive messages for any unknown names. The CPU starts in error state
      immediately rather than discovering the problem mid-execution.
    - Updated tests: validation tests no longer call step() before checking errors.
      Added test confirming update_code() re-validates signal names.
      144 tests total.
Version: 0.8.14
Date: 01. 04. 2026
  Features:
    - Added RSIG rd, signal — reads the named signal from BOTH the red and green input
      wires and stores the sum in rd. Equivalent to RSIGR/RSIGG followed by ADD, but in
      a single instruction. A signal absent on one or both wires contributes 0 to the sum.
      RSIGR and RSIGG are retained for cases where per-wire distinction matters.
    - Signal name validation: RSIG, RSIGR, RSIGG, and WSIG now validate the signal name
      against Factorio's prototype tables (virtual_signal, item, fluid) at runtime.
      An unrecognised name produces a descriptive error rather than silently reading 0
      or emitting a signal that Factorio would reject. A valid signal name that is simply
      absent on the wire at that moment still correctly returns 0 without error.
    - Added prototypes mock to test.lua so signal-name validation tests run under Busted
      without a live Factorio instance. Known signal names used in tests are registered
      in the mock; unknown names correctly trigger error paths.
    - 11 new tests (143 total).
Version: 0.8.12
Date: 01. 04. 2026
  Bugfixes:
    - Fixed changelog.txt format so the in-game mod changelog GUI displays correctly.
      Problems were: double separator lines between every version section; two early
      version entries missing Version: lines (assigned 0.7.1 and 0.7.2); non-standard
      category name 'Documentation:' replaced with 'Info:'; trailing whitespace stripped.
Version: 0.8.11
Date: 01. 04. 2026
  Features:
    - Added LI rd, imm — load immediate. Sets rd = imm directly. Syntactic sugar for
      ADDI rd, x0, imm; cleaner when the intent is loading a constant rather than adding.
      Accepts decimal, hex (0x prefix), and negative values. All standard error checks apply.
    - Instructions are now case-insensitive. Mnemonics may be written in upper, lower, or
      mixed case (ADDI, addi, Addi). The mnemonic is uppercased once during parsing via
      instruction:upper() before any comparison. Register names (x0-x31, o0-o3) and signal
      names remain case-sensitive as Factorio requires.
  Info:
    - Updated README.md and LICENSE.md to user-supplied versions. LICENSE now retains
      Joakim Lönnegren's original MIT copyright alongside the Analytical Combinator
      contributors' copyright. README includes Credits section acknowledging the original
      mod and Claude's role in development.
    - README: LI added to arithmetic table; new "Instruction case" section documents
      case-insensitive parsing.
    - 12 new tests (132 total): LI basic use, hex/negative immediates, x0 suppression,
      overwrite, error cases; case-insensitive mnemonic tests including a lower-case
      loop with BNEI; signal/register case-sensitivity confirmation.
Version: 0.8.10
Date: 29. 03. 2026
  Bugfixes:
    - Fixed BEQ and BNE: missing register nil-checks meant that a literal (e.g. BEQ x3, 0,
      label) or invalid register (e.g. BEQ x3, x99, label) was silently miscompared rather
      than raising an error. nil==nil is true in Lua, so BEQ x3, x99 would always branch
      regardless of x3; BEQ x3, 0 would never branch. Both now validate both operands first.
    - Fixed WSIG: the count source register (third argument) had no nil check. An invalid
      register name would silently emit a signal with count nil (treated as 0).
    - Fixed JAL: the destination register for the saved return address had no nil check.
      Writing to an invalid key (e.g. x99) would silently create a junk table entry.
    - Fixed SLT: added nil check on the destination register (first argument).
  Features:
    - Added BEQI rs, imm, label — branch if rs == immediate value
    - Added BNEI rs, imm, label — branch if rs != immediate value
    - Added BLTI rs, imm, label — branch if rs < immediate value
    - Added BLEI rs, imm, label — branch if rs <= immediate value
    - Added BGTI rs, imm, label — branch if rs > immediate value
    - Added BGEI rs, imm, label — branch if rs >= immediate value
    - All immediate branches accept hex literals (0x prefix) and validate both the
      register and immediate operands, erroring cleanly on invalid input.
    - 20 new tests including regression tests for the nil-check bugs. 120 tests total.
Version: 0.8.9
Date: 29. 03. 2026
  Features:
    - Added MULI rd, rs, imm — multiply register by immediate (rd = rs * imm)
    - Added DIV  rd, rs, rt  — integer division, floor toward -inf (rd = floor(rs/rt))
    - Added DIVI rd, rs, imm — divide by immediate
    - Added REM  rd, rs, rt  — remainder, sign follows dividend C-style (math.fmod)
    - Added REMI rd, rs, imm — remainder by immediate
    - DIV, DIVI, REM, REMI all produce a descriptive runtime error on divide-by-zero
      rather than crashing. Lua's math.floor and math.fmod are used to give consistent
      signed behaviour across all values.
    - Note: MUL was already present since 0.8.4; MULI is the new immediate variant.
    - 18 new tests covering basic operation, negative values, divide-by-zero errors,
      hex immediates, and a practical modulo wrap-around example. 100 tests total.
Version: 0.8.8
Date: 29. 03. 2026
  Changes:
    - Fixed JAL to save IP+1 (address of the next instruction) rather than the line number
      of the JAL itself. This matches standard RISC-V convention and makes JR work cleanly
      without a compensating +1. Existing programs using JAL x0 (discard return address)
      are unaffected. Programs relying on the specific numeric value saved by JAL will need
      to add 1 to their expected values.
    - Fixed JR to jump directly to the value in rs (no +1 adjustment needed now that JAL
      saves the correct return address).
    - JR x0 is now a defined special case: jumps to line 1 (program restart). Since x0 is
      always 0 and 0 is not a valid line number, this provides a convenient restart without
      requiring a label on line 1.
    - Updated all tests: JAL return-address assertions updated (x1=3→4 etc.),
      JR test comments corrected, new JR x0 restart test added. 82 tests total.
    - README: JAL/JR table entries updated; subroutine example rewritten — the old version
      needed an awkward BGT workaround because JAL saved the wrong value; the new example
      is clean and straightforward.
Version: 0.8.7
Date: 29. 03. 2026
  Features:
    - Added BLT rd, rt, label — branch if rs < rt
    - Added BLE rd, rt, label — branch if rs <= rt
    - Added BGT rd, rt, label — branch if rs > rt
    - Added BGE rd, rt, label — branch if rs >= rt
    - Added JR rs — jump to instruction at rs+1, enabling subroutine returns.
      JAL saves the line number of the JAL instruction itself, so JR adds 1 to
      land on the instruction after the call site.
    - 16 new tests (81 total): branch taken/not-taken cases for all four new
      instructions, JR call/return, nested calls with distinct return registers,
      and error cases.
    - README: control flow table updated; new subroutine call/return example.
Version: 0.8.6
Date: 29. 03. 2026
  Features:
    - Added SRL rd, rs, rt   — shift right logical by register (zero-fill)
    - Added SRLI rd, rs, imm — shift right logical by immediate (zero-fill)
    - Hex immediates: all immediate arguments now accept 0x prefix (e.g. ADDI x10, x0, 0xFF).
      tonumber() auto-detects base, so decimal and hex both work everywhere.
  Changes:
    - Renamed shift instructions to match RISC-V convention:
        SHL  → SLL  (shift left logical, register)
        SHLI → SLLI (shift left logical, immediate)
        SHR  → SRA  (shift right arithmetic, register)
        SHRI → SRAI (shift right arithmetic, immediate)
    - Moved bitlib setup (bit32/bit) to module level rather than inside step() to
      avoid re-evaluating it on every tick.
    - Fixed indentation bug: the else clause of the instruction dispatcher was
      accidentally indented inside the CNTSG block.
  Info:
    - README Shifts table expanded to all six variants with logical/arithmetic distinction noted.
    - README new section "Immediate value formats" documents decimal, hex, and negative literals.
    - Test suite: shift tests renamed; 4 new tests contrasting SRL/SRLI (zero-fill) against
      SRA/SRAI (sign-extend) on negative values. 65 tests total.
Version: 0.8.5
Date: 29. 03. 2026
  Bugfixes:
    - Fixed bitwise instruction implementation to use bit32 or bit library functions rather
      than typical operators.
Version: 0.8.4
Date: 29. 03. 2026
  Features:
    - Added MUL rd, rs, rt  — multiply two registers (rd = rs * rt)
    - Added AND rd, rs, rt  — bitwise AND
    - Added OR  rd, rs, rt  — bitwise OR
    - Added XOR rd, rs, rt  — bitwise XOR
    - Added NOT rd, rs      — bitwise NOT, unary (rd = ~rs)
    - Added SHL  rd, rs, rt  — shift left by register
    - Added SHLI rd, rs, imm — shift left by immediate
    - Added SHR  rd, rs, rt  — logical shift right by register
    - Added SHRI rd, rs, imm — logical shift right by immediate
    - Added CNTSR rd — count of distinct signals on the red input wire
    - Added CNTSG rd — count of distinct signals on the green input wire
    - All new instructions respect the x0 write-suppression rule and produce
      descriptive errors for wrong argument counts or invalid register names
    - 25 new tests (61 total)
    - README: Bitwise and Shifts instruction tables added; CNTSR/CNTSG added to
      circuit input table; two new examples (bit masking, signal presence detection)
Version: 0.8.3
Date: 29. 03. 2026
  Bugfixes:
    - Fixed README: the "Red/green mixer" example was using SUB (red minus green) despite
      claiming to output a sum. Replaced with two correct ADD-based examples:
      "Red/green signal sum" (continuous summing loop) and "Red/green sum with threshold gate"
      (fires signal-A once combined total exceeds 500).
Version: 0.8.2
Date: 29. 03. 2026
  Features:
    - Added ADD instruction: ADD rd, rs, rt sets rd = rs + rt (register + register addition).
      This is the missing complement to SUB and ADDI. Useful for combining values from two
      sources, e.g. summing red and green wire readings of the same signal.
      Example: RSIGR x10, iron-plate / RSIGG x11, iron-plate / ADD x12, x10, x11
    - Added 6 tests for ADD covering basic addition, accumulation in place, x0 write
      suppression, negative values, invalid register, and wrong argument count.
    - README: ADD added to instruction table; new example showing red+green signal sum
      with threshold gate.
Version: 0.8.1
Date: 29. 03. 2026
  Features:
    - Assembly code editor now uses a monospace font (NotoMono via the built-in "default-mono"
      descriptor). A FontPrototype "ac-mono-14" is declared in prototypes/fonts.lua and applied
      to the text-box at GUI creation time via textbox.style.font. No external TTF file is
      required as NotoMono-Regular.ttf ships with Factorio core.
Version: 0.8.0
Date: 29. 03. 2026
  Changes:
    - New technology tree icon (256x256): dark PCB circle with circuit traces, central IC chip,
      amber LED, and assembly code line art — no longer uses the vanilla circuit-network graphic
    - New mod portal thumbnail (144x144): shows the combinator body with screen chip, green
      input wire, red output wire, and a floating assembly code snippet
    - Rewrote all locale descriptions in locale/en/base.cfg to specifically describe the
      assembly-language programming model rather than generic combinator marketing copy
Version: 0.7.9
Date: 29. 03. 2026
  Changes:
    - Mod renamed from "Assembly Combinator" to "Analytical Combinator" to distinguish it
      from the original constant-combinator-based mod by joalon. All internal entity names,
      storage keys, GUI names, prototype IDs, and locale strings updated accordingly.
    - License changed to Zero-Clause BSD (0BSD) — no attribution required.
    - Updated mod description to better reflect RISC-V inspired assembly language and
      dual-wire input capability.
Version: 0.7.8
Date: 28. 03. 2026
  Bugfixes:
    - Fixed crash when WSIG is used with virtual or fluid signals (e.g. signal-red, crude-oil).
      Output signals were always emitted with type="item", causing Factorio to reject any name
      that isn't a valid item. write_outputs now resolves each signal name against
      prototypes.virtual_signal, prototypes.item, and prototypes.fluid in that order to
      determine the correct type. Invalid signal names are silently skipped rather than crashing.
Version: 0.7.7
Date: 28. 03. 2026
  Info:
    - Fixed incorrect README description for the Threshold Gate example which claimed the
      output signal appeared "on the red wire". WSIG writes to an output register with no
      wire-colour specificity; the signal appears on whichever wire(s) are physically connected
      to the output side of the combinator.
Version: 0.7.6
Date: 28. 03. 2026
  Bugfixes:
    - Fixed "everything" signal appearing on output wire in alt-mode and leaking onto the circuit
      network. The always-true condition used signal-everything as its first_signal, which is a
      special aggregate virtual signal that Factorio propagates to the output. Replaced with a
      pure constant condition (first_constant=0, comparator="=", second_constant=0) which
      references no signals at all and has no side effects on the network.
Version: 0.7.5
Date: 28. 03. 2026
  Bugfixes:
    - Fixed crash on load: defines.circuit_connector_id was removed in Factorio 2.0 and replaced
      by defines.wire_connector_id with per-colour IDs. get_circuit_network() now takes a single
      wire_connector_id (e.g. combinator_input_red / combinator_input_green) rather than a
      wire_type + circuit_connector_id pair. Fixed in both events.lua and gui.lua.
Version: 0.7.4
Date: 28. 03. 2026
  Bugfixes:
    - Fixed RSIGR and RSIGG always reading zero: get_circuit_network() requires an explicit
      circuit_connector_id for entities with separate input and output connectors. Without it
      the call returns nil on a decider combinator. Both events.lua and gui.lua now pass
      defines.circuit_connector_id.combinator_input when reading the input-side network.
Version: 0.7.3
Date: 28. 03. 2026
  Bugfixes:
    - Fixed crash when loading a saved game containing analytical combinators: Factorio does not
      persist Lua metatables across saves, so stored cpu objects lost all their methods on load.
      control.lua now re-attaches the cpu module metatable in both on_load (normal resume) and
      on_configuration_changed (mod update). Also backfills the input_signals field for saves
      predating 0.7.0 that were created without it.
Version: 0.7.2
Date: 28. 03. 2026
  Bugfixes:
    - Fixed crash on placement: LuaDeciderCombinatorControlBehavior does not expose
      sections_count / add_section / get_section (those belong to the constant combinator).
      Output writing now uses behavior.parameters with a conditions+outputs table, which is
      the correct 2.0 API for decider combinators. Clearing outputs uses behavior.parameters=nil.
Version: 0.7.1
Date: 28. 03. 2026
  Graphics:
    - Added custom microchip display sprite (30x22) replacing the decider combinator's
      comparison operator symbols (=, >, <, ≠, ≥, ≤) so the entity is visually distinct
      in-world regardless of which operator mode Factorio internally selects
    - Added custom 64x64 inventory/crafting icon showing the combinator body with the chip
      symbol on screen, a green status LED, and an 'AC' label
Version: 0.7.0
Date: 28. 03. 2026
  Major Features:
    - Fork of Assembly combinator version 0.6.2 from joalon
    - Analytical combinator is now based on a Decider Combinator, providing both input and output
      circuit network connections (red and green wires)
    - Added RSIGR instruction: read a named signal from the red input wire into a register
    - Added RSIGG instruction: read a named signal from the green input wire into a register
    - Red and green channels are read independently each tick, enabling programs to respond
      to live circuit network state
  Changes:
    - Recipe now requires a Decider Combinator instead of a Constant Combinator