Skip to content

fcore — Reactive UI Engine & Mod Framework for Factorio 2.0

Declarative Virtual DOM, React Hooks, O(1) Event Routing, and Bucketed Scheduler for Factorio 2.0 mods.

Quickstart: Integrating fcore into your Mod

fcore is a standalone Factorio mod and library designed for both TypeScript-To-Lua (TSTL) and Pure Lua modding in Factorio 2.0.

Because fcore runs as an independent Factorio mod:

  • Zero Code Duplication: Library code is not bundled into every mod archive.
  • Automatic Prototype Styles: All GUI styles (react_tab_button, react_slot_button_*, frames, margins) are registered automatically in data.lua.
  • Automatic Settings: Global settings (such as logging levels) are registered in settings.lua.
  • Self-Bootstrapping React: React event listeners and savegame hydration are initialized automatically upon module load.

1. Declare Mod Dependency (info.json)

In your mod’s info.json (or static/info.json), add fcore to dependencies:

{
"name": "my-factorio-mod",
"version": "1.0.0",
"factorio_version": "2.1",
"dependencies": [
"fcore >= 1.0.0",
"base >= 2.1.0"
]
}

2. Configure TypeScript Project

package.json

Link fcore as a development dependency:

{
"name": "my-factorio-mod",
"version": "1.0.0",
"dependencies": {
"fcore": "file:../fcore"
},
"devDependencies": {
"typescript": "^6.0.0",
"typescript-to-lua": "^1.37.0",
"typed-factorio": "^4.2.0"
}
}

tsconfig.json

Configure TSTL path mapping, plugin, and noResolvePaths:

{
"compilerOptions": {
"target": "ESNext",
"moduleResolution": "Node",
"jsx": "react",
"jsxFactory": "createElement",
"jsxFragmentFactory": "Fragment",
"types": [
"typed-factorio/runtime",
"typed-factorio/prototype",
"@typescript-to-lua/language-extensions"
],
"paths": {
"fcore/*": ["./node_modules/fcore/dist/*"]
}
},
"tstl": {
"luaTarget": "5.2",
"luaLibImport": "require",
"noImplicitSelf": true,
"sourceMapTraceback": true,
"luaPlugins": [
{ "name": "fcore/plugin.cjs" }
],
"noResolvePaths": ["util", "__fcore__/**", "__fcore__*"]
},
"include": ["src/**/*"]
}

3. Write Declarative UI (control.ts)

import { createElement, useState, createRoot } from "fcore/react";
import { WindowFrame, Titlebar, Button, Label, VFlow } from "fcore/react-components";
import * as event from "fcore/utils/event";
function MyWindow(props: { playerIndex: PlayerIndex; onClose: () => void }) {
const [count, setCount] = useState(0);
return (
<WindowFrame styles={{ width: 320 }}>
<Titlebar caption="My Mod Window" onClose={props.onClose} />
<VFlow styles={{ padding: 12 }}>
<Label caption={`Clicked: ${count} times`} />
<Button
caption="Increment"
onClick={() => setCount((prev) => prev + 1)}
/>
</VFlow>
</WindowFrame>
);
}
// Open window on custom input / shortcut
event.onCustomInput("open-my-gui", (e) => {
const player = game.get_player(e.player_index);
if (!player) return;
const root = createRoot(player.gui.screen, "my_mod_window");
root.render(
<MyWindow
playerIndex={e.player_index}
onClose={() => root.unmount()}
/>
);
});

Technical Comparison

Factorio Lua PatternTypeScript + fcore Framework Solution
Silent nil Indexing & TyposStrict compile-time checks catch missing prototype names, invalid properties, and misspelled keys before game launch.
Branded Engine IDsNative PlayerIndex, SurfaceIndex, UnitNumber branded types prevent mixing numeric identifiers.
Verbose Nested Lua TablesClean, declarative JSX syntax replaces hundreds of lines of nested element.add({ type = "frame", ... }).
Complex State ManagementStandard React hooks (useState, useReducer, useMemo, useCallback) preserve component state across game saves.
Direct CompilationTSTL transpiles directly to clean, optimal Lua 5.2 bytecode without heavy class polyfills or reflection overhead.

Core Framework Modules

React Virtual DOM Reconciler

Full JSX syntax, functional components, stateful hooks (useState, useInterval, useEntityLifecycle), dirty property diffing, and zero-cost sub-tree Bailouts.

Tag-Based Event Bus

High-speed integer event dispatcher resolves Factorio GUI events directly to target fiber handlers via tags.__reactId. Zero linear tree traversal.

Bucketed Tick Scheduler

Distributes delayed and recurring tasks across discrete tick buckets in storage._sched, maintaining 60 UPS without per-frame on_tick polling.

Prototype Style Engine

Compile-time type-safe Factorio gui-style generation with StyleFor<E> inference and smart C++ property diffing.

Structured Logger (strace)

Structured diagnostic logger with lazy callback evaluation (traceLazy, debugLazy) for high-frequency game ticks.

Signal and Quality Engine

Encode and decode Factorio 2.0 composite signal keys (item:iron-plate:legendary) with $O(1)$ lookup and serialization helpers.


🛠 TSTL Best Practices for Factorio Lua

When compiling TypeScript to Factorio Lua using TSTL (TypeScript-To-Lua), follow these core rules to ensure optimal VM performance and prevent runtime desyncs:

1. Explicit (this: void) on Callbacks

In TypeScript, callback types without context annotations generate an implicit self parameter (function(____, a, b)). When invoked by the Factorio engine, arguments shift by 1 position. Always declare callbacks with (this: void, ...):

// ❌ Anti-pattern: Implicit self shifts event arguments in Lua
type ClickHandler = (e: OnGuiClickEvent) => void;
// ✅ Best Practice: Guarantees direct Lua function signature
type ClickHandler = (this: void, e: OnGuiClickEvent) => void;

2. Never Use JavaScript Global Constructors

In Lua 5.2 VM, String, Number, and Boolean globals are nil. Calling them causes an immediate runtime crash (attempt to call global 'String' (a nil value)):

// ❌ Anti-pattern: Crashes in Lua VM
const keyStr = String(item.key);
const countNum = Number(textValue);
const isValid = Boolean(flags && flags.enabled);
// ✅ Best Practice: Use native Lua functions and boolean expressions
const keyStr = tostring(item.key);
const countNum = tonumber(textValue);
const isValid = flags !== undefined && flags.enabled === true;

3. Deleting Table Keys (obj[key] = undefined)

In TSTL, delete obj[key] generates the __TS__Delete polyfill helper. Assigning undefined compiles to a single native Lua opcode instruction (obj[key] = nil):

// ❌ Generates __TS__Delete polyfill:
delete storage.activeWindows[playerIndex];
// ✅ Compiles directly to storage.activeWindows[playerIndex] = nil:
storage.activeWindows[playerIndex] = undefined;

4. Table and Array Iteration

Standard Object.keys() or Object.entries() allocate intermediate JavaScript array polyfills. In Factorio Lua, use native pairs() and for..of for maximum speed:

// Array iteration (compiles to native ipairs):
for (const item of items) {
processItem(item);
}
// Record/Table iteration (compiles to native pairs):
for (const [key, value] of pairs(dictionary)) {
processEntry(key, value);
}