React Virtual DOM Reconciler
Full JSX syntax, functional components, stateful hooks (useState, useInterval, useEntityLifecycle), dirty property diffing, and zero-cost sub-tree Bailouts.
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:
react_tab_button, react_slot_button_*, frames, margins) are registered automatically in data.lua.settings.lua.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" ]}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" }}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/**/*"]}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 / shortcutevent.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()} /> );});In pure Lua, you can require modules directly from __fcore__:
local react = require("__fcore__/react/index")local components = require("__fcore__/react-components/index")local event = require("__fcore__/utils/event")local strace = require("__fcore__/utils/strace").stracelocal table_util = require("__fcore__/utils/table")
local createElement = react.createElementlocal useState = react.useStatelocal createRoot = react.createRoot
local WindowFrame = components.WindowFramelocal Titlebar = components.Titlebarlocal Button = components.Buttonlocal Label = components.Labellocal VFlow = components.VFlow
-- 1. Functional component in Lualocal function MyWindow(props) local count, setCount = useState(0)
return createElement(WindowFrame, { styles = { width = 320 } }, { createElement(Titlebar, { caption = "My Mod Window", onClose = props.onClose }), createElement(VFlow, { styles = { padding = 12 } }, { createElement(Label, { caption = "Clicked: " .. tostring(count) .. " times" }), createElement(Button, { caption = "Increment", onClick = function() setCount(count + 1) end }) }) })end
-- 2. Open window via event dispatcherevent.on_custom_input("open-my-gui", function(e) local player = game.get_player(e.player_index) if not player then return end
local root = createRoot(player.gui.screen, "my_mod_window") root.render(createElement(MyWindow, { playerIndex = e.player_index, onClose = function() root.unmount() end }))end)| Factorio Lua Pattern | TypeScript + fcore Framework Solution |
|---|---|
| Silent nil Indexing & Typos | Strict compile-time checks catch missing prototype names, invalid properties, and misspelled keys before game launch. |
| Branded Engine IDs | Native PlayerIndex, SurfaceIndex, UnitNumber branded types prevent mixing numeric identifiers. |
| Verbose Nested Lua Tables | Clean, declarative JSX syntax replaces hundreds of lines of nested element.add({ type = "frame", ... }). |
| Complex State Management | Standard React hooks (useState, useReducer, useMemo, useCallback) preserve component state across game saves. |
| Direct Compilation | TSTL transpiles directly to clean, optimal Lua 5.2 bytecode without heavy class polyfills or reflection overhead. |
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.
When compiling TypeScript to Factorio Lua using TSTL (TypeScript-To-Lua), follow these core rules to ensure optimal VM performance and prevent runtime desyncs:
(this: void) on CallbacksIn 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 Luatype ClickHandler = (e: OnGuiClickEvent) => void;
// ✅ Best Practice: Guarantees direct Lua function signaturetype ClickHandler = (this: void, e: OnGuiClickEvent) => void;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 VMconst keyStr = String(item.key);const countNum = Number(textValue);const isValid = Boolean(flags && flags.enabled);
// ✅ Best Practice: Use native Lua functions and boolean expressionsconst keyStr = tostring(item.key);const countNum = tonumber(textValue);const isValid = flags !== undefined && flags.enabled === true;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;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);}