This repository has no description
README.md

factorio-map #

A zoomable, deep-zoom web map + timelapse of a running Factorio server (2.0 / Space Age). It renders every surface — Nauvis, the other planets and space platforms — into a tiled map you can pan and zoom in the browser, with train-station overlays, a timelapse slider and shareable deep links.

It runs headless with software OpenGL (Mesa llvmpipe) — no GPU required — and ships each render to any S3-compatible object store.

Screenshots #

Full map with train-station overlays:

Full map with train stations

Zoomed in — factory detail (a train-station loading array):

Factory detail

Every surface, including the other planets (here Fulgora):

Another planet

Features #

  • Every surface — planets and space platforms, one tab each.
  • Sharp, near-native detail — resolution is a single knob (MAP_ZOOM).
  • Train-station overlays — station names, click to zoom in.
  • Resource overlay — each discovered deposit as type + remaining amount (oil shown as well count); patches clustered per chunk.
  • Rail-network highlight — the whole track layout drawn as a canvas layer, readable even zoomed out.
  • Clean ground — pollution is cleared on the throwaway save copy, so the map isn't covered in haze.
  • Timelapse — a slider scrubs through the last KEEP renders and keeps your current view (pan + zoom) across frames.
  • Shareable deep links — the URL carries the surface, game coordinates and zoom, so you can link someone to an exact spot.
  • Toggles — stations / resources / rails layers independently.
  • No GPU — software rendering under Xvfb + Mesa llvmpipe.
  • Storage-agnostic — one tarball per render on any S3-compatible bucket.
  • Container-native — example Kubernetes manifests in deploy/.

How it works #

A scheduled job runs the renderer and produces one tarball per render:

  1. downloads the graphical Factorio client once (build expansion/linux64, authenticated with a Factorio account token) and caches it on a volume;
  2. reads the latest autosave + the server's mods (e.g. from the server's data volume, or copied out of the server container);
  3. renders each surface as a grid of screenshots under Xvfb + llvmpipe. A single take_screenshot maxes out at 16384², so a large base is captured in CELL_PX cells at the target zoom and stitched later. Factorio buffers every queued screenshot in RAM until shutdown, so the render is split into batches (PER_BATCH cells per Factorio invocation) to bound memory. A first plan pass enumerates the grid and writes per-surface station metadata. See image/mod/control.lua;
  4. stitches each surface's cells into one Deep-Zoom pyramid with pyvips (image/stitch.py, arrayjoin→dzsave, streamed so the multi-gigapixel image is never materialised; missing/unexplored cells are filled black);
  5. packs the whole render (all tile pyramids + station JSON) into one tarball and uploads it, plus a small index.json timelapse manifest. One object per render sidesteps per-object throughput limits on the object store. The last KEEP renders are retained.

Serving is decoupled from the object store: a sync sidecar (image/sync.sh) polls index.json, downloads each new tarball and extracts it onto a local volume, and nginx serves the tiles from disk. This means the map stays up even if the object store is slow or unavailable, and the viewer never hits the store per tile.

The frontend (image/web/index.html, OpenSeadragon) adds surface tabs, the timelapse slider, station overlays and the deep-link/ coordinate features. Deep links look like #s=<surface>&x=<gx>&y=<gy>&z=<zoom>&t=<render>, using Factorio game coordinates so a link keeps working across renders.

Why grids + batches + one tarball #

Three hard limits shaped the design:

  • a single take_screenshot caps at 16384² → for a big base that's only ~6 px/tile (blurry), so render a grid and stitch;
  • Factorio buffers all queued screenshots in RAM until shutdown → many 16384² cells at once OOMs → render in batches, one process per batch;
  • object stores throttle per-object transfers and a detailed map is hundreds of thousands of tiles → ship one tarball per render and serve from a volume.

Two more gotchas worth knowing:

  • anti_alias renders at 2× then downscales — at 16384² the 32768² intermediate exceeds llvmpipe's texture limit, so AA is only used when shot_res ≤ 8192;
  • the scratch dir must be a disk-backed volume, not a medium: Memory emptyDir — tmpfs counts against the container memory cgroup and OOM-kills the renderer.

Configuration #

Renderer (environment variables):

Variable Purpose
FA_USERNAME, FA_TOKEN Factorio account, to download the graphical client
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY S3 credentials
S3_ENDPOINT, S3_BUCKET, S3_PREFIX object store target
MAP_ZOOM px/tile = 32·zoom (0.5=16, 0.75=24, 1.0=native 32). Sharper ≈ 4× the cells/tiles/time/storage per doubling
PER_BATCH cells per Factorio invocation (peak RAM ≈ PER_BATCH·~1 GiB + game)
CELL_PX screenshot cell size (default 16384)
JPEG_Q tile JPEG quality (default 90)
KEEP number of renders to retain
RENDER_TIMEOUT per-batch cap

The bucket only needs read/write on its own prefix — a scoped S3 policy:

{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow", "Action": ["s3:ListBucket", "s3:GetBucketLocation"], "Resource": "arn:aws:s3:::YOUR_BUCKET" },
    { "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"], "Resource": "arn:aws:s3:::YOUR_BUCKET/*" }
  ]
}

Deploy #

Example Kubernetes manifests are in deploy/: renderer.yaml (the scheduled render CronJob, RBAC, cache volume) and serving.yaml (the sync sidecar + nginx, serving volume, Service, Ingress). Adapt the image reference, storage classes, namespace, secrets and ingress host to your cluster. They assume the renderer can read the server's save + mods (the sample uses kubectl exec against the server pod, so it grants pods/exec; mounting the save volume works too).

Building the image #

cd image
docker buildx build --platform linux/amd64 \
  -t YOUR_REGISTRY/factorio-map:latest --push .

The image bundles the render pipeline (render.sh, stitch.py, mod/), the sync script (sync.sh) and the web frontend (web/).

Notes #

  • Station labels strip Factorio rich-text tags ([item=…]); icon-only names render as a dot without a label.
  • Occasionally a cell is lost to the shutdown flush (a black patch); a post-render pass could re-shoot only the missing cells.
  • Render time is llvmpipe-bound (no GPU); a GPU node would cut it substantially.

License #

MIT — see LICENSE.