Skip to content

Debugging

Debug factorio-rs mods with Factorio’s native LuaDebugAdapter (--dap). Because the adapter only understands Lua paths, factorio-rs dap sits between your editor and Factorio and remaps locations through generated sourcemaps.

  • Factorio 2.1+ with native --dap / LuaDebugAdapter (2.0.x exits with Option 'dap' does not exist)
  • A Factorio binary (FACTORIO_PATH), not only the Steam protocol launcher
  • VS Code with the thin factorio-rs DAP contribution (or any DAP client that can spawn factorio-rs dap on stdio)
  • Build with the debug profile so emit_sourcemap is on (default)

On Factorio 2.0.x, use FMTK against the generated dist/**/*.lua (no Rust sourcemap proxy), or upgrade to 2.1+.

Dedicated smoke crate: examples/breakpoints (gutter lines marked BREAKPOINT, plus debugger_breakpoint!).

Terminal window
cd examples/breakpoints
factorio-rs build --profile debug # writes dist/**/*.lua.map.json

Install a debugger contribution once (from the factorio-rs repo):

VS Code - Extensions -> … -> Install from Location… -> editors/vscode-factorio-rs-dap

Zed - Extensions -> Install Dev Extension… -> editors/zed-factorio-rs-dap

factorio-rs init writes .vscode/launch.json and .zed/debug.json.

VS Code:

{
"version": "0.2.0",
"configurations": [
{
"type": "factorio-rs",
"request": "launch",
"name": "factorio-rs DAP",
"gui": true
}
]
}

Zed (.zed/debug.json):

[
{
"adapter": "factorio-rs",
"label": "factorio-rs DAP",
"request": "launch",
"gui": true,
"manifestPath": "$ZED_WORKTREE_ROOT"
}
]

Do not run factorio-rs dap in a normal terminal and expect to “attach” afterward - the editor spawns the proxy on stdio. Typing in that terminal (or piping non-DAP data) fails with DAP frame missing Content-Length.

Set breakpoints in .rs sources (gutter click). Start the session (factorio-rs DAP). The proxy translates breakpoints to generated Lua lines; stack frames are rewritten back to Rust when a map exists.

Factorio’s --dap process is only the debug adapter. The game starts from the editor launch request (factorioArgs: mod directory, unsafe debug API, …). Close any other Factorio instance first (.lock), then load/create a game so control stage code (and your breakpoints) actually run.

Artifact Meaning
dist/lua/**/*.lua Generated Lua
dist/lua/**/*.lua.map.json Line table: Lua line -> Rust file/line/column

Profile key emit_sourcemap (default on for debug, off for release):

[profiles.debug]
emit_sourcemap = true
debug_level = 1
[profiles.release]
emit_sourcemap = false

debug_level = 2 also appends -- src/foo.rs:42 trailers on generated lines (human fallback when DAP is not attached).

Prefer debug (no optimize_ir) for the best statement<->line fidelity. factorio-rs dap deploys with a symlink and binds breakpoints to the resolved Lua filesystem path (Factorio’s default followSymlinks: true). Remap lines are also appended to ~/.factorio/factorio-rs-dap.log for diagnosis.

Mark a hot function with #[factorio_rs::profile] - the lowerer wraps the body with guarded debugadapter.start_profile / stop_profile (no-op unless Factorio was launched with DAP):

use factorio_rs::prelude::*;
#[profile]
fn hot_path() {
// work to sample
}
// With an event - keep `profile` closer to the function than `event`:
#[event(OnSingleplayerInit)]
#[profile]
pub fn on_singleplayer_init() {
hot_path();
}

Factorio’s start_profile only accepts { show_hook_events?= boolean? } - there is no profile name. Prefer the bare attribute (empty {}).

Then:

Terminal window
factorio-rs dap --gui # or editor F5
# trigger the profiled function in-game
factorio-rs profile --skip-build

factorio-rs profile waits for a dump under Factorio’s script-output, remaps Lua locations to Rust, and writes:

  • *.rust.txt - remapped Chrome DevTools .cpuprofile (JSON)
  • *.speedscope.json - speedscope sampled profile

Avoid show_hook_events = true for flamegraphs - it attributes nearly all samples to (hook: line) instead of your functions.

See examples/benchmarking for a nested call-tree demo.

factorio_rs::debugger_breakpoint!("hit from Rust");

Expands to a Lua call into FMTK’s legacy __DebugAdapter.breakpoint when that global exists. Native Factorio 2.1 DAP does not provide it - use gutter breakpoints (remapped through sourcemaps) to pause.

With emit_sourcemap, lowered Result tables get a small metatable so the debug adapter can show Ok(...) / Err(...) via __tostring and expand fields through __debugchildren / debugadapter.describe_field. Option values stay transparent (Some(x) -> x, None -> nil).

  • Factorio DAP is single-session / not multiplayer-safe
  • After rustc expand, line numbers track the lowered AST (best for path-based modules and debug profile)
  • Unmapped frames stay on Lua paths (honest fallback)