Skip to content

The mod: hat-shaped cells, unbuildable bands, settings - #2

Merged
vetkat merged 17 commits into
mainfrom
feat/the-mod
Sep 20, 2026
Merged

vetkat merged 17 commits into
mainfrom
feat/the-mod

Conversation

@vetkat

@vetkat vetkat commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Turns the finished tiling core into a playable mod. Terrain, build restrictions and settings all work and have been tested in game.

How building is blocked

The design spec called for script-side rejection — hooking build events and removing the entity afterwards. Factorio 2.0 turned out to offer something better: mods can declare custom collision layers, and two prototypes collide only if they share one.

So band tiles carry a hattorio_band layer that data-final-fixes adds to buildable structures but not to the character, vehicles or rails. The game refuses placement itself: red preview, blueprints skip those tiles, bots never attempt them, no ghosts. Script rejection would have placed each entity and then removed it, needing four event handlers plus ghost cleanup to approximate one prototype field.

It also removed the need for a per-chunk band bitmap in storage — the collision system does that lookup.

Performance

Band rasterisation started at 39.7 ms/chunk — fifty times the tiling descent, enough to stutter visibly while exploring. Rasterising each edge into its own bounding box instead of testing every tile against every edge brought it to 1.26 ms, a 31× speedup with byte-identical output. tools/bench.lua gates both stages.

Settings

Per planet: cell size and band width, both dropdowns labelled with the difficulty they imply. Default 41/3, chosen by playtest after 26/2 proved cramped.

Global: band style (animated liquid / flat void) and colour (five options, deep violet default). Map-scoped: ore richness override. Per player: cell outline overlay.

Bugs caught along the way

  • collision_mask defaults per entity type. Assigning a fresh one would have stripped entities of every normal collision. Fixed by using collision_mask_util.get_mask.
  • LuaSurface methods take dot notation. A cached get_tile would have passed the surface where x belongs.
  • Two dropdown combinations are broken. Measuring all sixteen found 15/4 leaves a 4×4 largest square where a drill is 3×3. The richness formula was worst exactly there — flooring at 20% drillable where the truth is 5%. Replaced the fit with the sixteen measurements; the fit survives only as a fallback.
  • Semicolons in locale values. ; is a comment marker in .cfg; vanilla never uses one inside a value across 7,301 strings.

Testing

82 Lua + 46 Python tests, luacheck clean. mod/config.lua and mod/bands.lua have no Factorio dependencies, so they run in CI under Lua 5.2 like hat/ does.

Verified in game: loads clean (135 prototypes patched, 0 skipped), terrain generates without chunk-boundary seams, lakes survive, blocking works, rails cross, undergrounds tunnel, outline overlay works, richness override applies.

Not verified

Multiplayer. The determinism argument is sound — correctly-rounded IEEE operations, no sin/cos/pow, one VM instruction per operation — but has never been confirmed with two clients. Plan 2's Task 10 is deferred by decision, and the README and wiki both say to play single player for now.

vetkat added 17 commits September 20, 2026 13:47
Refits the richness compensation against measured drillable fractions.
The coefficients in the plan overestimated reachable ore by up to 26
percentage points; the fitted form is accurate to 3.8. A calibration
test pins it to the measurements so it cannot drift silently.
Uses collision_mask_util.get_mask rather than assigning a fresh mask:
a prototype that does not declare collision_mask inherits a per-type
default, and overwriting it would strip the entity of every normal
collision.
Rasterises each hat edge into its own bounding box rather than testing
every tile against every edge: 39.7 ms/chunk -> 1.26 ms, a 31x speedup
with identical output. The naive form would have stuttered visibly while
exploring. tools/bench.lua now gates both stages.
Packaging is Python rather than a shell script calling zip(1), which is
not installed everywhere. A test asserts the archive shape: one
<name>_<version>/ directory with info.json at its root, the tiling core
and its data present, and no spec, docs, tools or wiki files.
Bands now clone deepwater rather than stone-path: its transitions are
authored for liquid meeting land, and it carries the only animated
shader in the base game. Three styles select the effect over it --
liquid (animated), void (none), rift (lava, Space Age only, falls back
to liquid and logs when absent).

Adds the two settings that genuinely belong outside startup: a
per-player cell outline overlay, and a map-scoped ore richness
override where 0 means compensate automatically.
Playtesting found 26/2 cramped. Both settings are now dropdowns rather
than sliders, labelled with the difficulty each implies -- the number
alone does not convey it, and the interesting range is four values, not
seventy-six. Still per planet.

Also replaces the colour comparison sheet: the first one showed four
near-blacks as two-tile lines, which were indistinguishable. The new one
zooms in, widens the band and shows the shader highlight, which is what
actually separates the options.
Colour is independent of style: the style picks the shader, the colour
picks what it is tinted, so a glowing rift can be violet and a flat void
can be ember.

Colour is no longer per planet. It is one player-chosen look shared
across every surface, so the mod reads as one thing rather than five.
Reflected cells keep a stronger tint of the same colourway.

Tests assert the colourways and the settings dropdown agree in both
directions, that every one has a locale label, and that every body stays
near-black with a lighter highlight -- without which an option is
invisible.
The lava-shader style read as a hazard rather than a void and fought the
calm look of the rest of the mod. Removing it also drops a Space Age
conditional from the data stage.

Highlights lifted about a third across every colourway, which is what
the shader catches at a band's edges and the only thing that tells the
options apart. Two tests now encode that: a highlight must be clearly
lighter than its body, and no two colourways may have near-identical
highlights.
Factorio's .cfg format documents ';' as a comment marker but does not
say whether it is honoured mid-line. Vanilla never risks it: across
7,301 English values, zero contain a semicolon, while '#' appears four
times. Cheaper to avoid than to depend on.

Adds locale guards for the failure modes that are silent in-game --
a semicolon in a value, whitespace before '=' (which defines a key
that never matches), duplicate keys, and keys outside any section.
The ore richness override could not be verified by observation: 0 means
'derive it from cell size and band width', and nothing reported what
that derivation produced or whether an override took effect. Surface
init now logs the effective multiplier and its source, and
/hattorio-info reports the geometry and richness in force. Both read
the same Surface.richness(), so the number shown is the number applied.

Also warns when the mod is added to a world that already has terrain:
existing chunks keep vanilla ground forever, leaving a permanent
boundary that nothing can fix retroactively.
Checking all sixteen dropdown combinations found two that are broken:
size 15 with band 4 leaves 46 buildable tiles and a 4x4 largest square,
where a drill and an assembler are both 3x3, and 15/3 leaves 11% of ore
reachable.

The richness formula was worst exactly there -- it floored at 20%
drillable where the truth is 5%, under-compensating where compensation
matters most. Since the dropdowns offer exactly sixteen combinations and
all sixteen are now measured, the table replaces the fit; the fit
remains only as a fallback, refitted against all sixteen points (worst
error 7.5pp, down from 14.6).

Severe combinations now warn on load rather than being discovered an
hour in.
The README claimed the mod was not written yet, which stopped being
true several commits ago. Multiplayer is called out as unverified in
both the README and the wiki, since the determinism argument has never
been confirmed with two clients.
info.json is the single source of truth for the version.
tools/check_version.py asserts the changelog documents it, leads with
it, and opens with the 99 dashes Factorio requires, and that any release
tag names it. That runs on every push, so drift is caught long before a
release.

Pushing a v* tag 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 mod portal via the documented two-step API.

A portal release cannot be withdrawn, so the upload is gated three ways:
only on a tag or an explicit dispatch, behind a mod-portal environment
that can require a reviewer, and after every other check has passed.
@vetkat
vetkat merged commit ef67e85 into main Sep 20, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant