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
85 changes: 85 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,88 @@ jobs:
sys.exit(1)
print(f"{len(a)} stamps match")
PY

smoke:
name: Desktop app launches
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- run: npm ci

# Electron needs a display and the usual Chromium shared libraries.
# xvfb-run gives it a virtual one; without this the app cannot start at
# all and "it built" would be the only thing CI ever proved.
- name: Install a virtual display
run: |
sudo apt-get update
sudo apt-get install -y xvfb libgtk-3-0 libnss3 libasound2t64 \
libgbm1 libxss1 libxtst6

# Chromium's SUID sandbox helper has to be owned by root with the setuid
# bit; npm cannot install it that way. The alternative is --no-sandbox,
# which would mean the smoke test exercises a different configuration
# from the one users actually run — so fix the permissions instead.
#
# The binary is fetched lazily rather than during `npm ci`, so run the
# package's own downloader first. Not `npx electron --version` — that
# launches the binary, which trips the very sandbox error being fixed.
- name: Let Chromium use its sandbox
run: |
node node_modules/electron/install.js
sudo chown root:root node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 node_modules/electron/dist/chrome-sandbox
ls -l node_modules/electron/dist/chrome-sandbox

- name: Launch the app and assert the editor came up
run: xvfb-run --auto-servernum npm run smoke

- name: Capture the window
run: xvfb-run --auto-servernum npx electron . --shot /tmp/patternfront.png

- uses: actions/upload-artifact@v4
with:
name: screenshot
path: /tmp/patternfront.png

package:
name: Package (${{ matrix.os }})
# Packaging is slow, so it only runs once the cheap checks are green.
needs: [verify, smoke]
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- run: npm ci

# Unsigned on purpose — see README. CSC_IDENTITY_AUTO_DISCOVERY=false
# stops electron-builder picking up a stray keychain identity on macOS
# runners and producing builds that are signed with something arbitrary.
- name: Build installers
run: npm run dist
env:
CSC_IDENTITY_AUTO_DISCOVERY: false
GH_TOKEN: ""

- uses: actions/upload-artifact@v4
with:
name: installers-${{ matrix.os }}
path: |
dist/*.dmg
dist/*.zip
dist/*.exe
if-no-files-found: error
96 changes: 96 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Release

on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "Tag to build and release (e.g. v0.1.0)"
required: true

permissions:
contents: write

jobs:
build:
name: Build (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag || github.ref }}

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- run: npm ci

# Refuse to cut a release from code that does not pass its own checks.
- name: Verify before packaging
shell: bash
run: ./tools/verify-all.sh

# macOS builds both arm64 and x64 slices; Windows builds x64 + arm64.
# Unsigned: see README, "Install". To turn signing on, add CSC_LINK,
# CSC_KEY_PASSWORD, APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD and
# APPLE_TEAM_ID as repository secrets and set hardenedRuntime: true in
# electron-builder.yml. No other change is needed.
- name: Build installers
run: npm run dist
env:
CSC_IDENTITY_AUTO_DISCOVERY: false

- uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.os }}
path: |
dist/*.dmg
dist/*.zip
dist/*.exe
dist/latest*.yml
if-no-files-found: error

release:
name: Publish release
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts

- name: Collect
run: |
mkdir -p out
find artifacts -type f \( -name '*.dmg' -o -name '*.zip' -o -name '*.exe' \) \
-exec cp {} out/ \;
ls -la out

- name: Publish
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
draft: true
generate_release_notes: true
files: out/*
body: |
## Install

**macOS** — download the `.dmg` for your chip (`arm64` for Apple
silicon, `x64` for Intel). These builds are **unsigned**, so the
first launch needs: right-click the app → **Open** → **Open**.

**Windows** — run the `Setup` installer, or use the portable
`.exe`. SmartScreen will show "Windows protected your PC" →
**More info** → **Run anyway**.

Signing certificates cost money this project does not spend yet.
If you would rather not trust an unsigned build, `npm run dist`
builds it yourself from source in one command.
88 changes: 88 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Contributing

## Getting set up

```sh
git clone https://github.com/timbogdanov/patternfront
cd patternfront
npm install
npm start
```

Node 20+ and Python 3.10+. The Python side is stdlib only — no pip install, no virtualenv.

## Before you open a pull request

```sh
npm run verify
```

This has to pass. CI runs the same script, plus a headless launch of the desktop app.

## How the project is laid out

`app/patternfront.html` is the whole editor — markup, styles and logic in one file with no build
step and no dependencies. That is deliberate: it opens in a browser by double-clicking, and the
desktop app loads the identical file. There is no second copy to keep in sync.

Native behaviour is feature-detected behind `window.pfNative`. If you add something desktop-only,
guard it with `native()` and make sure the browser path still works — there is a test that boots
the code with no `pfNative` at all and fails if anything reaches for it.

## Things that will fail CI

**Generated files must match their generator.** Stamps and codec fixtures are produced by scripts;
editing the output by hand is caught. Change the source and re-run:

```sh
python3 tools/gen-stamps.py --emit # then paste the STAMPS block
python3 tools/gen-codec-fixtures.py
```

**The design rules are enforced.** `tools/verify-ui.py` fails the build on border radius above
2px, box shadows, blur, decorative gradients, chrome lighter than `--raised`, and stylesheet
lengths in `px` instead of `rem`. These are not suggestions; the visual direction was reset
several times before they existed. See `docs/09-design-system.md`.

**No OpenFront assets.** The repository ships none, for licensing reasons set out in
[NOTICE](NOTICE), and there is a gate that fails if any reappear.

## Adding a stamp

Stamps are ASCII art in `tools/gen-stamps.py` — edit them as pictures, not as hex:

```python
art("nature", "acorn", """
................
......####......
.....######.....
...
""")
```

Run `python3 tools/gen-stamps.py`. The generator rejects anything under 8% or over 75% ink
(invisible, or a blob) and renders every surviving shape to `docs/assets/stamps.png`.

**Look at that contact sheet.** It is the step that decides what ships. A 16×16 one-bit icon that
you know is a snail often reads as a camera to everyone else. Several shapes have been cut from
this library for exactly that reason, after two or three attempts each. Cutting one is a normal
outcome, not a failure.

## Tests

`tools/verify-behaviour.js` runs the editor's **real** functions by lifting them out of the HTML
and executing them in a sandbox. Nothing in it is a reimplementation — if you change a function,
the test runs your changed version. Add cases there rather than writing a parallel copy of the
logic.

## Style

Match what is around you. The codebase is dense and commented where the reasoning is not obvious
from the code — why a modulo has to be floor-mod, why a track is `minmax(0,1fr)`. Comments that
restate the line above are noise; comments that record a trap are the valuable kind.

## Reporting bugs

Include your OS and app version, and say what you expected. The version is in the About panel —
`PatternFront → About` on macOS, `Help → About` on Windows. If it is a rendering problem, a
screenshot saves a great deal of time.
56 changes: 41 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# PatternFront

A 1-bit pattern editor for [OpenFront.io](https://openfront.io) territory patterns — the small
tiling bitmaps that mark out a player's territory. Runs in any browser, from a single file.
tiling bitmaps that mark out a player's territory. Desktop app for macOS and Windows, and the
same editor runs in a browser.

![Stamps](docs/assets/stamps.png)
![PatternFront](docs/assets/desktop.png)

Patterns are two colours and at most 129×65 pixels, which sounds trivial and is not: they tile
against absolute world coordinates, so a seam that looks fine in the editor can read as a defect
Expand All @@ -21,36 +22,60 @@ exports the exact `patternData` string the game takes.
- **Live tiled preview** — see the seams before the game does
- **Export** — PNG, sprite sheet, animated GIF, and OpenFront `patternData` (or JSON for `cosmetics.json`)

## Run it
On the desktop it is a real application, not a wrapped web page: native menus, `.patternfront`
documents with Open/Save/Save As, double-click to open from Finder or Explorer, recent files,
exports through native Save dialogs, remembered window position, and an unsaved-changes prompt
that will not let you lose work.

```sh
open app/patternfront.html # macOS
xdg-open app/patternfront.html # Linux
```
## Install

Grab a build from [Releases](https://github.com/timbogdanov/patternfront/releases).

**These builds are unsigned.** Paying Apple and Microsoft for certificates is not something this
project does yet, so both systems will warn you the first time:

That is the whole thing. One self-contained file — no dependencies, no bundler, no build step and
no network access. Double-click it and it works.
- **macOS** — right-click the app → **Open** → **Open**. Once, then never again.
- **Windows** — SmartScreen shows "Windows protected your PC" → **More info** → **Run anyway**.

A desktop build for macOS and Windows is [in progress](../../pulls).
If that is not acceptable to you, build it yourself — it takes one command, below.

Running the checks needs Node 20+ and Python 3.10+:
> **Known issue building on recent macOS.** `npm run dist` can fail at the DMG step with
> `hdiutil: convert failed - Resource temporarily unavailable`, from the `dmgbuild` copy that
> electron-builder vendors. The `.zip` targets are unaffected, and the app inside them is
> identical — `npm run dist -- --mac zip` gets you a working build. Unzip and drag to
> Applications.

## Run from source

Needs Node 20+ and Python 3.10+ (Python is only for the generators and the verification suite).

```sh
./tools/verify-all.sh
npm install
npm start # run the desktop app
npm run verify # run every check
npm run dist # build installers for the current platform
```

There is no build step for the editor itself. `app/patternfront.html` is one self-contained file
with no dependencies, no bundler and no network access — open it directly in a browser and it
works. The desktop build loads that same file; nothing is forked.

## How it is put together

```
app/patternfront.html the entire editor — markup, styles, logic, stamps
electron/ main process, preload bridge, menus, document handling
tools/ generators and the verification suite
docs/ design documents
tests/fixtures/ the codec corpus
```

The renderer feature-detects `window.pfNative`. Without it — in a browser — every native path is
skipped and the file behaves exactly as it always did. That is asserted by tests, not assumed.

## Verification

`./tools/verify-all.sh` runs nine suites. They exist because this project kept getting things subtly
`npm run verify` runs nine suites. They exist because this project kept getting things subtly
wrong in ways that only mechanical checking caught — a seam metric that flagged correct patterns,
a modulo that goes negative in JavaScript but not Python, an unguarded `localStorage` read that
killed the app before first paint in any sandboxed frame.
Expand All @@ -63,7 +88,8 @@ killed the app before first paint in any sandboxed frame.
| map-scale sampling | `sampleAt` matches an independent oracle, including at negative world coordinates |
| UI design rules | no shadows, no gradients, no radius above 2px, no light chrome — the design system, enforced |
| stamp library | every stamp is a valid pattern, uniquely named, and legible at 1× |
| editor behaviour | ~70 assertions running the editor's real functions in a sandbox |
| editor behaviour | ~80 assertions running the editor's real functions in a sandbox |
| desktop smoke test | Electron actually launches and the editor comes up, headless |
| docs vs OpenFront | *optional* — cross-checks the format docs against a local game checkout |

The last one needs an OpenFront clone and **skips loudly** without one. It never silently passes.
Expand All @@ -77,7 +103,7 @@ this repo's corpus hits exactly.

## Status, honestly

The editor is real and works. The AI features described in
The editor and the desktop app are real and work. The AI features described in
[`docs/03`](docs/03-ai-integration.md), [`04`](docs/04-image-to-pattern.md) and
[`07`](docs/07-cost-and-abuse.md) — text-to-pattern, AI region editing, hosted credits — are
**designed and not built**. The image importer in the app today is ordinary quantisation and
Expand Down
Loading
Loading