Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ jobs:
- run: pip install pytest
- name: verify the tiling and regenerate data
run: python -m pytest tools/tests -q
- name: validate the mod manifest
run: |
python3 -c "
import json
d = json.load(open('info.json'))
assert d['factorio_version'] == '2.0', d['factorio_version']
for k in ('name','version','title','author','description','dependencies'):
assert k in d, k
print('manifest ok:', d['name'], d['version'])
"
# Catch version drift on every push, not only when a release is cut.
- name: check version consistency
run: python3 tools/check_version.py

core:
name: tiling core (Lua 5.2)
Expand All @@ -32,4 +45,4 @@ jobs:
- run: luarocks install busted
- run: luarocks install luacheck
- run: busted
- run: luacheck hat spec
- run: luacheck hat mod prototypes spec
100 changes: 100 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: release

# Releases are cut by pushing a tag. info.json is the single source of truth
# for the version; the tag and the changelog must agree with it or nothing is
# built. That is the whole point -- a mod portal page describing a build
# nobody has is the failure being designed against.
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
publish:
description: "Also upload to the Factorio mod portal"
type: boolean
default: false

permissions:
contents: write

jobs:
build:
name: verify and build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install pytest

# Before anything is built, let alone published.
- name: check version consistency
run: python3 tools/check_version.py ${{ github.ref_type == 'tag' && github.ref_name || '' }}

- name: offline pipeline tests
run: python -m pytest tools/tests -q

# Factorio embeds Lua 5.2.1; testing on anything newer would mask the
# all-doubles behaviour the tiling core depends on.
- uses: leafo/gh-actions-lua@v10
with:
luaVersion: "5.2"
- uses: leafo/gh-actions-luarocks@v4
- run: luarocks install busted
- run: luarocks install luacheck
- run: busted
- run: luacheck hat mod prototypes spec

- name: build the mod zip
run: python3 tools/package.py

- name: name the artifact
id: zip
run: |
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import json
i = json.load(open("info.json"))
print(f"path=build/{i['name']}_{i['version']}.zip")
print(f"version={i['version']}")
PY

- uses: actions/upload-artifact@v4
with:
name: mod-zip
path: ${{ steps.zip.outputs.path }}
if-no-files-found: error

- name: attach to the GitHub release
if: github.ref_type == 'tag'
uses: softprops/action-gh-release@v2
with:
files: ${{ steps.zip.outputs.path }}
generate_release_notes: true

publish:
name: upload to the mod portal
needs: build
runs-on: ubuntu-latest
# Only on a tag, or an explicit dispatch asking for it. A portal release
# cannot be withdrawn, so this must never fire by accident.
if: github.ref_type == 'tag' || inputs.publish
# Configure this environment with a required reviewer if you want a human
# gate before anything reaches the portal.
environment: mod-portal
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: mod-zip
path: build
- name: upload
env:
FACTORIO_API_KEY: ${{ secrets.FACTORIO_API_KEY }}
run: |
if [ -z "$FACTORIO_API_KEY" ]; then
echo "::error::FACTORIO_API_KEY is not set; add it as a repository secret"
exit 1
fi
sh tools/publish_mod.sh build/*.zip
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ __pycache__/
.venv/
luacov.*
.wiki-tmp/
build/
29 changes: 28 additions & 1 deletion .luacheckrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
std = "lua52"
files["spec"] = { std = "+busted" }
max_line_length = 100

-- Factorio's Lua environment. These are provided by the game, not by Lua.
local factorio_globals = {
"data", "mods", "settings", "defines", "game", "script",
"prototypes", "remote", "commands", "rendering", "log", "serpent",
"table_size", "helpers",
}

-- storage is the mod's persistent table: Factorio provides it, mods write to
-- it, so it is not read-only.
local factorio_writable = { "storage", "table" }

files["prototypes"] = {
read_globals = factorio_globals,
-- Factorio adds table.deepcopy
globals = factorio_writable,
}

files["mod"] = {
read_globals = factorio_globals,
globals = factorio_writable,
}

files["spec"] = { std = "+busted" }

-- hat/ is deliberately free of Factorio dependencies: it must load under
-- plain lua5.2 so it can be tested in CI. No Factorio globals here.
files["hat"] = {}
14 changes: 11 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ else
RUN_BUSTED := $(BUSTED)
endif

.PHONY: all test pytest lint data check preview wiki
.PHONY: all test pytest lint data check preview wiki package version

all: check

Expand All @@ -38,9 +38,17 @@ preview:
done

lint:
luacheck hat spec
luacheck hat mod prototypes spec

check: pytest test lint
check: version pytest test lint

## check info.json, changelog and tag agree
version:
python3 tools/check_version.py

## build the distributable mod zip
package:
python3 tools/package.py

## publish wiki/ to the GitHub wiki (needs one page created in the UI first)
wiki:
Expand Down
29 changes: 16 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ discovered in 2023. Cells never repeat, so neither can your blueprints.

![The hat tiling as Hattorio generates it](docs/preview/tiling.png)

> **Status: in development.** The tiling engine is finished and tested. The
> mod itself — terrain, build restrictions, settings — is not written yet.
> There is nothing to install from the mod portal today.
> **Status: playable, not yet released.** Terrain, build restrictions and
> settings all work and have been tested in game. Not on the mod portal yet,
> and **multiplayer is unverified** — the determinism argument is sound but
> has never been confirmed with two clients. Single player should be solid.

## What it does to your game

Expand All @@ -24,33 +25,35 @@ That single rule changes how you build:
default settings there are **624 distinct buildable tile shapes**, and no
single one covers more than 1.1% of the map. A layout that fills one cell
will not fit the next.
- **Small blueprints still travel.** Anything up to 11×11 at the default
- **Small blueprints still travel.** Anything up to 18×18 at the default
settings fits every cell. Assembler clusters are fine; a 30×30 mall is not.
- **Every connection between cells is a puzzle.** Belts cross by underground
pair, against an edge that is never axis-aligned.

The result is a factory that has to be fitted to the ground it sits on.

![Cells rasterised onto Factorio's tile grid](docs/preview/bands-26-2.png)
![Cells rasterised onto Factorio's tile grid](docs/preview/bands-41-3.png)

## Settings

Per planet, set at world creation.

| Setting | Default | Range |
| Setting | Default | Choices |
|---|---|---|
| Hat size (centre to vertex, tiles) | **26** | 15–90 |
| Band width (tiles) | **2** | 1–6 |
| Cell size (centre to vertex, tiles) | **41** (normal) | 15, 26, 41, 52 |
| Band width (tiles) | **3** (wide) | 1, 2, 3, 4 |
| Band appearance | Dark liquid | liquid, void |
| Band colour | Deep violet | violet, cold, oily, ember, void black |

Hat size is the dial that matters. It decides how much room you get and how
large a blueprint survives:

| size | cell area | unbuildable | reusable blueprint | preview |
| size | difficulty | cell area | reusable blueprint | preview |
|---|---|---|---|---|
| 15 | 116 t | 37.4% | ~5×5 | [look](docs/preview/bands-15-2.png) |
| **26** | **347 t** | **22.5%** | **11×11** | [look](docs/preview/bands-26-2.png) |
| 41 | 944 t | 14.6% | 18×18 | [look](docs/preview/bands-41-2.png) |
| 52 | 1,584 t | 11.5% | 23×23 | [look](docs/preview/bands-52-2.png) |
| 15 | very hard | 116 t | ~5×5 | [look](docs/preview/bands-15-2.png) |
| 26 | hard | 347 t | 11×11 | [look](docs/preview/bands-26-2.png) |
| **41** | **normal** | **944 t** | **18×18** | [look](docs/preview/bands-41-3.png) |
| 52 | easy | 1,584 t | 23×23 | [look](docs/preview/bands-52-2.png) |

Changing the setting later affects only newly created surfaces; existing ones
keep the geometry their terrain was built with, so nothing ever seams.
Expand Down
7 changes: 7 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---------------------------------------------------------------------------------------------------
Version: 0.1.0
Date: 2026-09-20
Major Features:
- Planets are divided into cells of the aperiodic hat monotile tiling.
- Cells are separated by narrow unbuildable bands. Undergrounds tunnel
beneath them; rails cross them; bots and power ignore them.
2 changes: 2 additions & 0 deletions control.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require("mod.terrain")
require("mod.commands")
1 change: 1 addition & 0 deletions data-final-fixes.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require("prototypes.patch")
2 changes: 2 additions & 0 deletions data.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require("prototypes.collision")
require("prototypes.tiles")
37 changes: 30 additions & 7 deletions docs/design/hattorio-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,13 +409,36 @@ Per planet, startup:

| Setting | Default | Range |
|---|---|---|
| `hattorio-hat-size-<planet>` | **26** (centre-to-vertex, tiles) | 15-90 |
| `hattorio-band-width-<planet>` | **2** | 1-6 |

Both are genuine player settings, per planet, exactly as Hextorio does
it. 26/2 gives a 347-tile cell where blueprints up to 11x11 -- about
one assembler cluster -- stay reusable while everything larger must be
hand-fitted. Hextorio parity would be size 52; 41 is a middle option.
| `hattorio-hat-size-<planet>` | **41** (normal) | 15, 26, 41, 52 |
| `hattorio-band-width-<planet>` | **3** (wide) | 1, 2, 3, 4 |
| `hattorio-band-style` | liquid | liquid, void |
| `hattorio-band-colour` | violet | violet, cold, oily, ember, ink |
| `hattorio-richness-override` (map) | 0 = automatic | 0 - 10 |
| `hattorio-show-outline` (per player) | off | on, off |

Size and band are **dropdowns**, not sliders: the difficulty of a given
size is not readable from the number, and the interesting range is four
values rather than seventy-six. Each is labelled with the difficulty it
implies -- 15 very hard, 26 hard, 41 normal, 52 easy.

**41/3 is the default, chosen by playtest.** An earlier 26/2 proved
cramped in practice. 41 gives a 944-tile cell where blueprints up to
18x18 stay reusable -- enough to build in, while every standard layout
still breaks. Band 3 keeps the bands visually substantial, which band 2
does not at that cell size.

Band style and band colour are **independent**: the style picks the
shader laid over the surface, the colour picks what it is tinted.

A third style using Vulcanus's lava shader was built and then removed
after playtesting -- it read as a hazard rather than a void and fought
the calm look of everything else. Removing it also dropped a Space Age
conditional from the data stage.

Only size and band are per planet. Band style and colour are global, and the other
two are scoped by what they affect: richness is map-wide because ore is
shared, and the outline is per player because it is pure presentation
and cannot change what anyone else sees.

**Why startup rather than runtime.** The geometry determines terrain,
so changing it under an existing save would leave every generated
Expand Down
31 changes: 31 additions & 0 deletions docs/design/toolchain-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,34 @@ Design section 7 states the three rules that make it explicit.

The one deliberate exception is the descent's output accumulator, mutated in
place for speed. It is local to `hat/tiling.lua` and never escapes.

## Releasing

`info.json` is the single source of truth for the version. Everything else
must agree with it, and `tools/check_version.py` enforces that on every push,
not only when a release is cut:

- the changelog must have a `Version: X.Y.Z` entry for it, listed first
- the changelog must open with exactly 99 dashes, or Factorio refuses to load
the mod with an unhelpful message
- a release tag, if given, must be `v<version>`

A release is cut by pushing a tag:

```bash
# bump info.json and add a changelog entry first
git tag v0.2.0 && git push origin v0.2.0
```

`.github/workflows/release.yml` then verifies the version, runs the full suite
under Lua 5.2, builds the zip, attaches it to a GitHub release, and uploads it
to the Factorio mod portal.

The portal upload needs a `FACTORIO_API_KEY` repository secret, from
https://factorio.com/profile with the *ModPortal: Upload Mods* permission. It
runs in a `mod-portal` GitHub environment, which can be given a required
reviewer so a human approves before anything is published.

**A portal release cannot be withdrawn.** That is why every check runs before
the upload step rather than after, and why the upload never fires on a branch
push — only on a tag, or a manual dispatch that explicitly asks for it.
7 changes: 6 additions & 1 deletion docs/plans/the-mod.md
Original file line number Diff line number Diff line change
Expand Up @@ -1069,7 +1069,12 @@ git commit -m "feat(mod): thumbnail and distributable zip"

---

### Task 10: Multiplayer determinism check
### Task 10: Multiplayer determinism check [DEFERRED]

> **Deferred by decision, not forgotten.** This needs a second machine and
> ideally a second operating system, which is the whole point of the check --
> the risk being tested is divergence between platforms. It gates *release*,
> not the code, and the mod is marked single-player-only until it is done.

The spec's determinism argument is from first principles — correctly-rounded
IEEE operations, no `sin`/`cos`/`pow`, one VM instruction per operation. It
Expand Down
Binary file added docs/preview/band-colours.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/preview/bands-41-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading