Compare commits

...
Author SHA1 Message Date
branderson 97168ea0e5 ci: publish only a latest image tag, built from main
Drops version tag builds and the main tag. Images report the short
commit SHA as their version.
2026-10-15 17:34:14 -07:00
branderson a2d899be6f Merge pull request 'Mirror main to GitHub and publish images to GHCR' (#14) from github-mirror into main
Images / app (push) Successful in 5m35s
Images / session-manager (push) Successful in 1m37s
Images / streamer (push) Successful in 3m32s
Reviewed-on: #14
Reviewed-by: branderson <brad@branderson.io>
2026-10-15 17:00:58 -07:00
branderson 2d1d8cce46 feat: mirror main to GitHub and publish images to GHCR
mirror/github-mirror.sh publishes main and its tags to a read-only
GitHub mirror, adding a notice under the README title, with a systemd
timer to run it hourly. A GitHub Actions workflow builds the images on
the mirror and pushes them to ghcr.io. The docs point prebuilt images
there, and CONTRIBUTING no longer invites issues and pull requests.

upstream-status.sh now fetches without tags, so upstream's version tags
stay out of local clones.
2026-10-15 16:48:04 -07:00
branderson 3c3bb5dfbf Merge pull request 'Add upstream sync process and agent notes' (#13) from upstream-sync-process into main
Images / app (push) Successful in 4m55s
Images / session-manager (push) Successful in 1m46s
Images / streamer (push) Successful in 3m44s
Reviewed-on: #13
Reviewed-by: branderson <brad@branderson.io>
2026-10-15 16:36:56 -07:00
branderson 13a7fd9d19 docs: don't pin our migration numbers in sync notes 2026-10-15 16:36:43 -07:00
branderson 4fd43cd17d docs: add upstream sync process and agent notes
Adds scripts/upstream-status.sh to report upstream flashpoint-web changes
since the last merge, docs/08-development/upstream-sync.md describing the
merge process, and .dejima.md for agents working in dejima.
2026-10-15 16:31:33 -07:00
branderson 110e7fcd78 Merge pull request 'Credit Texgriddy and Fredoka; rename the color scheme to Slate' (#12) from credit-texgriddy into main
Images / app (push) Successful in 5m7s
Images / session-manager (push) Successful in 1m44s
Images / streamer (push) Successful in 3m33s
Reviewed-on: #12
Reviewed-by: branderson <brad@branderson.io>
2026-10-15 16:24:17 -07:00
branderson f3d85f5239 docs: credit Texgriddy and Fredoka; rename the color scheme to Slate
The portal's look is inspired by Texgriddy, an ES-DE theme by
Weestuarty, recreated in CSS without any of its files. The README and
the portal doc now say so, and credit the Fredoka font. The color
scheme was named after Texgriddy's "Chilled" scheme; it's now Slate.
2026-10-15 16:16:00 -07:00
branderson 9a9d7e3787 Merge pull request 'Streaming: measure Flash Player's menu bar from the movie's offset' (#11) from flash-menu-offset into main
Images / app (push) Successful in 4m53s
Images / session-manager (push) Successful in 1m41s
Images / streamer (push) Successful in 3m27s
Reviewed-on: #11
Reviewed-by: branderson <brad@branderson.io>
2026-10-14 18:18:01 -07:00
branderson e531228fa9 fix(streaming): keep Flash Player's window in place while it's open
Flash Player moves its window back to the top of the screen once the
movie has loaded, bringing the menu bar back. The streamer now checks
the window twice a second and puts it back, logging the first few
times.
2026-10-14 18:10:11 -07:00
branderson d1cebec142 fix(streaming): measure Flash Player's menu bar from the movie's offset
GTK draws the projector's menu bar in its window, not in a child window,
so the previous lookup found nothing and, under set -e and pipefail,
silently ended the block that moves the window. The movie is the
window's largest child, and its offset is the menu bar's height (56px
on flashbox). The block no longer stops on errors, logs what it measured
and where the window ended up, and GAME_WIDTH/GAME_HEIGHT, unused now,
are dropped.
2026-10-14 18:03:52 -07:00
branderson 3c4d3e7216 Merge pull request 'Streaming: hide Flash Player's menu bar without full screen' (#9) from flash-windowed into main
Images / app (push) Successful in 4m53s
Images / session-manager (push) Successful in 1m46s
Images / streamer (push) Successful in 3m31s
Reviewed-on: #9
Reviewed-by: branderson <brad@branderson.io>
2026-10-14 17:26:37 -07:00
branderson e697638827 Merge pull request 'Carry Flash saves between Ruffle and the original Flash Player' (#8) from flash-save-sync into main
Images / app (push) Successful in 4m55s
Images / session-manager (push) Successful in 1m41s
Images / streamer (push) Successful in 3m31s
Reviewed-on: #8
Reviewed-by: branderson <brad@branderson.io>
2026-10-14 17:26:19 -07:00
branderson 1306a88b0e fix(streaming): hide Flash Player's menu bar without full screen
Some games break in Flash Player's full-screen mode: in Bloons darts
make a sound but never fly. Instead of pressing Ctrl+F, the streamer
stretches the projector's window to the display and moves it up by the
menu bar's height, measured from the window's opening height and the
movie's. The session manager now passes the game's exact size as
GAME_WIDTH/GAME_HEIGHT and rounds only the display size to even.
2026-10-14 17:25:27 -07:00
24 changed files with 574 additions and 145 deletions
+50
View File
@@ -0,0 +1,50 @@
# Dotcom Arcade: notes for agents
Dotcom Arcade is a heavily modified fork of
[flashpoint-web](https://github.com/darkraise/flashpoint-web) by darkraise
(GPL-3.0), with game streaming added under `streaming/`. Coding standards and
architecture notes are in `CLAUDE.md`; feature and deployment docs are in
`docs/`.
## Upstream
- **Upstream** is https://github.com/darkraise/flashpoint-web, branch `master`.
It's read-only from here: don't push to it or open pull requests there unless
asked.
- **Most of upstream's code is still used:** the backend, the frontend's data
layer, the player, and the account and admin pages. Fixes to these should come
in from upstream rather than being rewritten here.
- **To see what upstream has changed** since it was last merged, and how each
touched file stands here, run `scripts/upstream-status.sh`.
- **To pull changes in**, follow `docs/08-development/upstream-sync.md`. In
short:
- merge upstream's `master` on a branch (don't cherry-pick);
- keep files removed here removed;
- keep the Dotcom Arcade branding;
- keep upstream's migration file names;
- have the pull request merged with a merge commit, never squashed, so the
next sync starts from the right place.
- **Parts that are only ours:**
- `streaming/`;
- `scripts/`;
- `frontend/src/components/portal/`;
- `views/GamePage.tsx`;
- `lib/saves/flashSaves.ts`;
- the discovery backend;
- `.gitea/`;
- migrations upstream doesn't have.
## GitHub mirror
`mirror/` publishes `main` to a read-only GitHub mirror from the operator's
host, never from a session; see `docs/08-development/github-mirror.md`. GitHub
runs `.github/workflows`, the forge `.gitea/workflows`; keep the two image
workflows in step.
## CI
Workflows are in `.gitea/workflows`. Jobs that run docker builds use the
`dejima-privileged` runner label; anything else uses `dejima-confined`. The
privileged job image has no Docker client, so install `docker-ce-cli` first.
Never hardcode the forge's host name: read it at run time, or take it from the
`REGISTRY_HOST` repository variable.
+1 -1
View File
@@ -13,7 +13,7 @@ JWT_SECRET=replace-with-a-secure-random-string
# Built from source by default. To pull published images instead, set the
# registry path they live under (ending in /), then:
# docker compose pull && docker compose up -d
# IMAGE_PREFIX=registry.example.com/owner/
# IMAGE_PREFIX=ghcr.io/branderson/
# IMAGE_TAG=latest
# "Latest release" API URL to check for updates (GitHub or Gitea); off when unset
# APP_UPDATE_URL=https://git.example.com/api/v1/repos/owner/dotcom-arcade/releases/latest
+8 -7
View File
@@ -10,7 +10,6 @@ name: Images
on:
push:
branches: [main]
tags: ['v*.*.*']
workflow_dispatch:
# Jobs that run docker builds use dejima-privileged; anything else should use
@@ -59,6 +58,12 @@ jobs:
echo "host=$host" >> "$GITHUB_OUTPUT"
echo "owner=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_OUTPUT"
# Shown in Settings, since images carry no version number
- name: Name the version
id: version
shell: bash
run: echo "sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
@@ -71,11 +76,7 @@ jobs:
uses: docker/metadata-action@v5
with:
images: ${{ steps.registry.outputs.host }}/${{ steps.registry.outputs.owner }}/${{ matrix.image }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=ref,event=branch
tags: type=raw,value=latest
- uses: docker/build-push-action@v6
with:
@@ -85,5 +86,5 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
build-args: |
VERSION=${{ steps.meta.outputs.version }}
VERSION=${{ steps.version.outputs.sha }}
provenance: false
+70
View File
@@ -0,0 +1,70 @@
# Builds the app, session manager and streamer images on the GitHub mirror and
# pushes them to GitHub's container registry, under the repository's owner.
# The forge runs .gitea/workflows/images.yml instead; Gitea ignores this
# directory while .gitea/workflows exists.
name: Images
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: app
image: dotcom-arcade
context: .
- name: session-manager
image: dotcom-arcade-session-manager
context: streaming/session-manager
- name: streamer
image: dotcom-arcade-streamer
context: streaming/streamer
steps:
- uses: actions/checkout@v4
- name: Lowercase the owner
id: owner
shell: bash
run: echo "name=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_OUTPUT"
# Shown in Settings, since images carry no version number
- name: Name the version
id: version
shell: bash
run: echo "sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ steps.owner.outputs.name }}/${{ matrix.image }}
tags: type=raw,value=latest
- uses: docker/build-push-action@v6
with:
context: ${{ matrix.context }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
build-args: |
VERSION=${{ steps.version.outputs.sha }}
provenance: false
+15 -105
View File
@@ -1,44 +1,11 @@
# Contributing to Dotcom Arcade
Thanks for your interest in contributing! We welcome bug fixes, features, documentation improvements, and questions.
Dotcom Arcade is developed on a private forge and published to GitHub as a
read-only mirror, so issues and pull requests aren't accepted there. You're
welcome to fork it under the [GPL-3.0](LICENSE).
## What is Dotcom Arcade?
A self-hosted web application for browsing and playing games from [Flashpoint Archive](https://flashpointarchive.org/).
**Tech Stack:** Node.js 20+, Express, React 18, TypeScript, SQLite, TanStack Query, Tailwind CSS
## Table of Contents
- [Where to Start](#where-to-start)
- [Getting Started](#getting-started)
- [Development Setup](#development-setup)
- [Running Tests](#running-tests)
- [Making Changes](#making-changes)
- [Submitting a Pull Request](#submitting-a-pull-request)
- [Code Style](#code-style)
- [Reporting Bugs](#reporting-bugs)
- [License](#license)
## Where to Start
- Browse issues labeled [`good first issue`](../../labels/good%20first%20issue) or [`help wanted`](../../labels/help%20wanted)
- Check the [project documentation](docs/) for architecture and patterns
## Getting Started
```bash
# Fork the repository on GitHub, then clone your fork
git clone https://github.com/YOUR_USERNAME/flashpoint-web.git
cd flashpoint-web
# Add upstream remote
git remote add upstream https://github.com/ORIGINAL_OWNER/flashpoint-web.git
# Keep your fork synced
git fetch upstream
git rebase upstream/master
```
It's built on [flashpoint-web](https://github.com/darkraise/flashpoint-web).
Fixes to code the two still share may belong upstream.
## Development Setup
@@ -47,6 +14,7 @@ git rebase upstream/master
```
backend/ # Express REST API + game content serving (:3100)
frontend/ # React + Vite application (:5173)
streaming/ # Session manager and streamer for games browsers can't run
docs/ # Documentation
```
@@ -69,6 +37,7 @@ The app (UI and API) runs on `http://localhost:80`.
### Option 2: Manual Setup (Recommended for Development)
**Prerequisites:**
- Node.js 20+
- Flashpoint Archive installed locally
@@ -99,80 +68,21 @@ cd backend && npm test -- --coverage
cd frontend && npm test
```
## Making Changes
### Branch Naming
```
feat/add-playlist-export
fix/game-search-pagination
refactor/auth-service
docs/update-api-reference
```
### Commit Messages
Follow [Conventional Commits](https://www.conventionalcommits.org/):
```
<type>(<scope>): <description>
feat(games): add advanced search filters
fix(auth): resolve token refresh race condition
docs: update deployment guide
refactor(api): simplify error handling
```
**Types:** `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`
### Workflow
1. Create a branch from `master`
2. Make your changes
3. Run `npm run typecheck` and `npm test`
4. Update documentation if needed
5. Submit a pull request
## Submitting a Pull Request
### Before Submitting
```bash
git fetch upstream
git rebase upstream/master
npm run typecheck
npm run format
npm test
```
### PR Guidelines
- Keep PRs focused and reasonably sized
- Reference related issues (`Fixes #123`)
- Include a clear description of changes
- Update documentation for new features
## Code Style
See [CLAUDE.md](CLAUDE.md) for detailed coding standards. Key points:
- **TypeScript:** No `any` types, use `unknown` or proper interfaces
- **Backend:** Wrap async handlers with `asyncHandler()`, static routes before parameterized
- **Frontend:** Use `@/` imports, all API calls through `lib/api.ts`, use theme tokens
- **Backend:** Wrap async handlers with `asyncHandler()`, static routes before
parameterized
- **Frontend:** Use `@/` imports, all API calls through `lib/api.ts`, use theme
tokens
- **General:** Validate inputs, handle errors properly, clean up resources
Run `npm run format` before committing.
## Reporting Bugs
Before reporting, search [existing issues](../../issues) to avoid duplicates.
Include:
- Steps to reproduce
- Expected vs actual behavior
- Environment details (OS, Node version, browser)
- Screenshots or logs if applicable
Run `npm run format` before committing. Commit messages follow
[Conventional Commits](https://www.conventionalcommits.org/).
## License
This project is licensed under [GPL-3.0](LICENSE). By contributing, you agree that your contributions will be licensed under the same license.
This project is licensed under [GPL-3.0](LICENSE). By contributing, you agree
that your contributions will be licensed under the same license.
+8
View File
@@ -135,6 +135,9 @@ PGID=1000
docker compose up -d --build
```
To pull prebuilt images instead of building them, see
[prebuilt images](docs/09-deployment/docker-deployment.md#prebuilt-images).
Open `http://<server>/` and create the admin account. On first start the app
indexes the collection, which takes a few minutes on a full Ultimate install.
Under **Settings → App** you can set the site name and tagline.
@@ -192,6 +195,11 @@ and development. Useful starting points:
- [Ruffle](https://ruffle.rs/), for running Flash in modern browsers.
- [Selkies](https://github.com/selkies-project/selkies), which streams the
games.
- [Texgriddy](https://github.com/Weestuarty/texgriddy-es-de) by Weestuarty, an
ES-DE theme that inspired the site's look. The look was recreated from scratch
in CSS; none of Texgriddy's files, images or fonts are used.
- [Fredoka](https://fontsource.org/fonts/fredoka) (SIL Open Font License), the
display font.
## Legal
+2
View File
@@ -15,6 +15,8 @@ the Dotcom Arcade application.
5. **[Testing Guide](./testing-guide.md)** - Testing approach and best practices
6. **[Debugging Guide](./debugging.md)** - Debugging tips and troubleshooting
7. **[Common Pitfalls](./common-pitfalls.md)** - Known issues and solutions
8. **[Upstream Sync](./upstream-sync.md)** - Merging changes from flashpoint-web
9. **[GitHub Mirror](./github-mirror.md)** - Publishing to GitHub and GHCR
## Quick Start
+28
View File
@@ -0,0 +1,28 @@
# GitHub mirror
Dotcom Arcade is developed on a private forge. `mirror/github-mirror.sh`
publishes its `main` and the tags on `main` to
[github.com/branderson/dotcom-arcade](https://github.com/branderson/dotcom-arcade),
hourly, from a host that can push to GitHub. Issues and pull requests are off
there. Actions stays on, since it builds the images that `ghcr.io/branderson/`
serves (see [publishing images](../09-deployment/docker-deployment.md#publishing-images)).
The mirror's `main` is the forge's plus one commit, which adds a read-only notice
under the README's title. Every forge commit is in the mirror unchanged, and
each sync fast-forwards. If the README loses its `# ` title, the sync stops
instead of publishing without the notice.
## Setup
1. Set up the timer as described at the top of
`mirror/dotcom-arcade-github-mirror.service`. Give it a deploy key with write
access to the GitHub repository.
2. Run `github-mirror.sh --dry-run` to build what a sync would publish without
pushing it.
3. Once the first sync has pushed, run `github-mirror.sh --settings` (needs
`gh`). It turns off issues, pull requests, projects, wiki and discussions.
4. The first image build creates its packages as private. In each package's
settings on GitHub, set its visibility to public.
The timer runs a copy of the script, not the checkout's. Reinstall it after
reviewing a change to `mirror/`.
+99
View File
@@ -0,0 +1,99 @@
# Syncing with upstream flashpoint-web
Dotcom Arcade is a heavily modified fork of
[flashpoint-web](https://github.com/darkraise/flashpoint-web) by darkraise
(branch `master`). Most of upstream's code is still used here: the backend, the
frontend's data layer, the player, and the account and admin pages. Upstream's
fixes and features to those parts should keep coming in, so the two projects
don't drift apart.
## What's whose
Comparing this repository with the last upstream commit merged into it:
- **Unchanged here:** most of the backend, the API client, hooks, stores and
types, the player, the account and admin views, and most docs. Upstream
changes to these merge as they are.
- **Changed here:** files upstream also owns that this project has modified.
The main ones:
- `GameService` (mature filter, release ranges, shuffle, related games);
- `server.ts` and `config.ts` (stream proxy, update check);
- `App.tsx` (portal routes);
- `GamePlayer` (streaming, saves);
- the browse layout and filters;
- the rebrand across names and docs.
Upstream changes to these need merging by hand.
- **Removed here:**
- the Flash and HTML5 sections;
- the GitHub stars endpoint;
- the old homepage pieces;
- the dev compose file;
- upstream's CI workflow and design notes.
These stay removed.
- **Only here:**
- `streaming/`;
- `scripts/`;
- `frontend/src/components/portal/`;
- `views/GamePage.tsx`;
- `lib/saves/flashSaves.ts`;
- `.gitea/`;
- the discovery backend;
- migrations upstream doesn't have.
Upstream never touches these, but a change elsewhere can still need matching
work here.
`scripts/upstream-status.sh` lists upstream's new commits and puts every file
they touch in one of these groups, so this list doesn't need keeping up to date
by hand.
## When
Run `scripts/upstream-status.sh` when upstream publishes a release, and at
least monthly. Sync when it shows new commits.
## How
1. On a branch from `main`, merge upstream:
```sh
scripts/upstream-status.sh
git checkout -b upstream-sync-$(date +%Y-%m-%d) origin/main
git merge refs/remotes/upstream/master
```
2. Resolve conflicts by group:
- **Changed here:** keep this project's behavior and take upstream's fix or
feature with it. Where they truly conflict, prefer this project's
behavior, and note what was left out in the pull request.
- **Removed here:** keep the file deleted
(`git rm <file>`). If upstream's change matters elsewhere, such as a bug
fix in shared logic, port that part to where the code lives now.
- **New upstream:** take it, unless it belongs to something removed here.
New UI usually lands in the old application layout. Decide whether it
belongs in the portal, and if so, follow up with a portal version.
- **Branding:** keep "Dotcom Arcade", our image and container names, and our
update check (`APP_UPDATE_URL`); don't take back upstream's.
3. Migrations: keep upstream's migration files under their own names, even
when a number clashes with ours. The migrations table records full names,
so `009_app_tagline` and an upstream `009_something` both run, in name
order. Renaming an upstream migration would run it a second time on
databases that already applied it. Check that ours and theirs don't depend
on each other's order.
4. Check the result:
- backend and frontend typecheck, tests and build;
- the session manager's tests if `streaming/` was affected;
- `docker compose up -d --build` with and without the `streaming` profile;
- the docs, for anything the merge made inaccurate.
5. Open a pull request listing upstream's commits and how each conflict was
resolved. **Merge it with a merge commit, not squash or rebase.** The merge
records how far upstream has been synced, so the next run of
`scripts/upstream-status.sh` starts from the right place.
## Sending changes upstream
Fixes made here to code upstream still shares, such as backend bugs, may be
worth offering upstream. Do that separately, from a branch based on upstream's
`master`, and only when the project's maintainer decides to.
+15 -7
View File
@@ -28,22 +28,30 @@ All three are built from this repository by `docker compose up -d --build`.
### Prebuilt images
To pull published images instead of building, set `IMAGE_PREFIX` in `.env` to
the registry path they live under, ending in `/`, and optionally `IMAGE_TAG`:
Images are published to GitHub's container registry. To pull them instead of
building, set `IMAGE_PREFIX` in `.env`, ending in `/`. There's one tag,
`latest`, built from the tip of `main`.
```sh
IMAGE_PREFIX=registry.example.com/owner/
IMAGE_TAG=latest
IMAGE_PREFIX=ghcr.io/branderson/
```
Then run `docker compose pull && docker compose up -d`.
Then run `docker compose pull && docker compose up -d`. Images from another
registry work the same way, with its path as the prefix.
### Publishing images
Two workflows build the same images with the same tags:
- `.github/workflows/images.yml` runs on the
[GitHub mirror](../08-development/github-mirror.md) and pushes to
`ghcr.io/<owner>/` with the workflow's own token. It needs no setup.
- `.gitea/workflows/images.yml` runs on the forge. Gitea ignores
`.github/workflows` while `.gitea/workflows` exists.
`.gitea/workflows/images.yml` builds all three images on Gitea Actions and
pushes them to the Gitea instance's container registry, under the repository's
owner. Pushes to `main` publish a `main` tag; `v1.2.3` tags publish `1.2.3`,
`1.2` and `latest`. Its jobs run docker builds, so they need a runner labelled
owner. Each push to `main` publishes `latest`. Its jobs run docker builds, so they need a runner labelled
`dejima-privileged`; jobs that don't build images should use `dejima-confined`.
It also needs a repository secret, `REGISTRY_TOKEN`: an access token with
package write access, belonging to the repository's owner, whom the workflow
+7 -1
View File
@@ -30,9 +30,15 @@ tokens are applied to `<body>` while the portal is mounted, which takes
precedence over the app theme on `<html>` and also reaches dialogs and menus
rendered into the body.
One scheme ships, **Chilled** (slate background, sage boxes). Adding a scheme
One scheme ships, **Slate** (slate background, sage boxes). Adding a scheme
means adding an entry to `PORTAL_SCHEMES`; no component changes are needed.
The portal's look is inspired by
[Texgriddy](https://github.com/Weestuarty/texgriddy-es-de), an ES-DE theme by
Weestuarty: the framed page, glossy tiles with a metallic ring, outlined info
boxes, capsule buttons and the slate and sage palette. It was recreated from
scratch in CSS; none of Texgriddy's files, images or fonts are used.
Display text uses Fredoka (OFL, bundled via `@fontsource-variable/fredoka`);
body text uses Verdana.
+2
View File
@@ -125,6 +125,8 @@ for Flashpoint Ultimate.
- [Testing Guide](08-development/testing-guide.md) - Testing approach
- [Debugging](08-development/debugging.md) - Debugging tips
- [Common Pitfalls](08-development/common-pitfalls.md) - Known issues
- [Upstream Sync](08-development/upstream-sync.md) - Merging changes from flashpoint-web
- [GitHub Mirror](08-development/github-mirror.md) - Publishing to GitHub and GHCR
### 09. Deployment
@@ -25,7 +25,7 @@ export function PortalLayout() {
const searchRef = useRef<HTMLInputElement>(null);
const [rolling, setRolling] = useState(false);
useEffect(() => applyPortalScheme('chilled'), []);
useEffect(() => applyPortalScheme('slate'), []);
useMatureSync();
const playRandom = async () => {
+2 -2
View File
@@ -8,8 +8,8 @@ export interface PortalScheme {
}
export const PORTAL_SCHEMES = {
chilled: {
label: 'Chilled',
slate: {
label: 'Slate',
tokens: {
'--p-bg': '#4c6878',
'--p-bg-mix': '#2f4553',
@@ -0,0 +1,26 @@
# User unit for the GitHub mirror, on a host that can push to GitHub. Never in
# a dejima session: a sandbox may push only to its own forge.
#
# install -m 755 mirror/github-mirror.sh ~/.local/bin/dotcom-arcade-github-mirror
# cp mirror/dotcom-arcade-github-mirror.service mirror/dotcom-arcade-github-mirror.timer ~/.config/systemd/user/
# systemctl --user daemon-reload
# systemctl --user enable --now dotcom-arcade-github-mirror.timer
#
# A copy, not a link into the checkout: the timer holds a key that can push to
# GitHub, and a session working in this checkout can edit the script.
# Reinstall it after reviewing an update.
#
# ~/.config/dotcom-arcade/github-mirror.env sets MIRROR_SOURCE, the forge
# clone URL, and whatever else mirror/github-mirror.sh documents. The GitHub
# key, a deploy key with write access to the repository, belongs behind an ssh
# Host alias named in MIRROR_TARGET, so it is not also offered to the forge:
#
# MIRROR_SOURCE=git@forge.example.com:owner/dotcom-arcade.git
# MIRROR_TARGET=git@github-dotcom-arcade:branderson/dotcom-arcade.git
[Unit]
Description=Mirror Dotcom Arcade's main and release tags to GitHub
[Service]
Type=oneshot
EnvironmentFile=%h/.config/dotcom-arcade/github-mirror.env
ExecStart=%h/.local/bin/dotcom-arcade-github-mirror
+9
View File
@@ -0,0 +1,9 @@
[Unit]
Description=Mirror Dotcom Arcade's main and release tags to GitHub, hourly
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.target
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# Publishes main and its release tags to a read-only mirror on GitHub.
#
# MIRROR_SOURCE=<forge clone URL> mirror/github-mirror.sh sync
# ... mirror/github-mirror.sh --dry-run build and print, push nothing
# mirror/github-mirror.sh --settings configure the GitHub repo (needs gh)
# mirror/github-mirror.sh --transform DIR rewrite a checkout in place
#
# Runs on a host that can push to GitHub, never in a dejima session: a sandbox
# may push only to its own forge.
#
# The GitHub copy is not byte-for-byte. Its main is one extra commit on top of
# the forge's main, whose tree is the forge's with the rewrites in transform()
# applied, and whose parents are the forge commit and the previous mirror
# commit. So every forge SHA is in the mirror unchanged, and each sync is a
# fast-forward for anyone tracking it. Tags are pushed as they are, pointing at
# the untransformed forge commits.
#
# Environment:
# MIRROR_SOURCE forge clone URL (required to sync)
# MIRROR_REPO GitHub repository, default branderson/dotcom-arcade
# MIRROR_TARGET push URL, default git@github.com:$MIRROR_REPO.git
# MIRROR_STATE working bare repository, default ~/.cache/dotcom-arcade-github-mirror.git
set -euo pipefail
MIRROR_REPO="${MIRROR_REPO:-branderson/dotcom-arcade}"
MIRROR_TARGET="${MIRROR_TARGET:-git@github.com:$MIRROR_REPO.git}"
MIRROR_STATE="${MIRROR_STATE:-${XDG_CACHE_HOME:-$HOME/.cache}/dotcom-arcade-github-mirror.git}"
say() { echo "github-mirror: $*"; }
die() { say "$*" >&2; exit 1; }
WORK=""
cleanup() {
[ -n "$WORK" ] || return 0
git -C "$MIRROR_STATE" worktree remove --force "$WORK" 2>/dev/null || true
rm -rf "$WORK"
}
# Rewrites a checkout for GitHub. Each rewrite is checked, so a README that
# drifted away from what this expects stops the mirror instead of publishing
# the forge's version. Running it twice changes nothing.
transform() {
local readme="$1/README.md"
# perl -i would write a symlink's target back as the README, publishing it.
[ -f "$readme" ] && [ ! -L "$readme" ] || die "README.md is missing or a symlink in $1"
# The first thing a visitor to a mirror does is open an issue or a pull
# request. Under the title.
grep -q '^> \*\*This is a read-only mirror' "$readme" || perl -0pi -e '
s{^(# [^\n]*)\n}{$1\n\n> **This is a read-only mirror.** Dotcom Arcade is developed on a private\n> forge, and issues and pull requests are not accepted here.\n}m
' "$readme"
grep -q '^> \*\*This is a read-only mirror' "$readme" \
|| die "README.md: no title to put the mirror notice under"
}
# Issues, pull requests, wiki and the like off; Actions stays on, since it
# publishes the images. Settings rather than content, so a sync does not
# reapply them; run once after creating the repository.
settings() {
command -v gh >/dev/null || die "--settings needs the gh CLI"
gh api -X PATCH "repos/$MIRROR_REPO" --silent \
-F has_issues=false -F has_pull_requests=false -F has_projects=false \
-F has_wiki=false -F has_discussions=false
}
sync() {
[ -n "${MIRROR_SOURCE:-}" ] || die "MIRROR_SOURCE is not set"
local git=(git -C "$MIRROR_STATE")
trap cleanup EXIT
[ -d "$MIRROR_STATE" ] || git init -q --bare "$MIRROR_STATE"
"${git[@]}" fetch -q --no-tags --prune "$MIRROR_SOURCE" \
'+refs/heads/main:refs/source/main' '+refs/tags/*:refs/source/tags/*'
local src prev=""
src="$("${git[@]}" rev-parse refs/source/main)"
# An absent main (exit 2) is a first run; anything else is a real failure.
local rc=0
git ls-remote -q --exit-code "$MIRROR_TARGET" refs/heads/main >/dev/null || rc=$?
if [ "$rc" -eq 0 ]; then
"${git[@]}" fetch -q --no-tags "$MIRROR_TARGET" '+refs/heads/main:refs/mirror/main'
prev="$("${git[@]}" rev-parse refs/mirror/main)"
elif [ "$rc" -ne 2 ]; then
die "cannot read $MIRROR_TARGET"
fi
WORK="$(mktemp -d)"
"${git[@]}" worktree add -q --detach "$WORK" "$src"
transform "$WORK"
git -C "$WORK" add -A
local tree
tree="$(git -C "$WORK" write-tree)"
if [ -n "$prev" ] && [ "$("${git[@]}" rev-parse "$prev^1")" = "$src" ] \
&& [ "$("${git[@]}" rev-parse "$prev^{tree}")" = "$tree" ]; then
say "already at ${src:0:12}"
else
# The forge commit's committer and date, so the same input always
# makes the same commit.
local name email date commit
IFS=$'\t' read -r name email date < <("${git[@]}" log -1 --format='%cn%x09%ce%x09%cI' "$src")
commit="$(GIT_AUTHOR_NAME="$name" GIT_AUTHOR_EMAIL="$email" GIT_AUTHOR_DATE="$date" \
GIT_COMMITTER_NAME="$name" GIT_COMMITTER_EMAIL="$email" GIT_COMMITTER_DATE="$date" \
"${git[@]}" commit-tree "$tree" -p "$src" ${prev:+-p "$prev"} \
-m "Mirror ${src:0:12} for GitHub" \
-m "README header added by mirror/github-mirror.sh.")"
"${git[@]}" update-ref refs/mirror/next "$commit"
[ -n "$DRY_RUN" ] || "${git[@]}" push -q "$MIRROR_TARGET" "$commit:refs/heads/main"
say "main is ${src:0:12} as ${commit:0:12}"
fi
# Only tags on main: one on another branch would publish unreleased work.
local ref
local refspecs=()
while IFS= read -r ref; do
if "${git[@]}" merge-base --is-ancestor "$ref" "$src"; then
refspecs+=("$ref:refs/tags/${ref#refs/source/tags/}")
say "tag ${ref#refs/source/tags/}"
fi
done < <("${git[@]}" for-each-ref --format='%(refname)' refs/source/tags/)
[ "${#refspecs[@]}" -eq 0 ] || [ -n "$DRY_RUN" ] \
|| "${git[@]}" push -q "$MIRROR_TARGET" "${refspecs[@]}"
}
DRY_RUN=""
case "${1:-}" in
"") sync ;;
--dry-run) DRY_RUN=1; sync ;;
--settings) settings ;;
--transform) [ -n "${2:-}" ] || die "--transform needs a directory"; transform "$2" ;;
*) sed -n '4,7p' "$0" >&2; exit 2 ;;
esac
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Shows what upstream flashpoint-web has changed since it was last merged, and
# how this project stands on each file it touches. Changes nothing but the
# refs/remotes/upstream/master ref. See docs/08-development/upstream-sync.md.
set -euo pipefail
URL=${UPSTREAM_URL:-https://github.com/darkraise/flashpoint-web.git}
BRANCH=master
UPSTREAM=refs/remotes/upstream/$BRANCH
cd "$(git rev-parse --show-toplevel)"
git fetch -q --no-tags "$URL" "+$BRANCH:$UPSTREAM"
base=$(git merge-base HEAD "$UPSTREAM")
count=$(git rev-list --count "$base..$UPSTREAM")
echo "Last merged from upstream: $(git log -1 --format='%h %ad %s' --date=short "$base")"
if ((count == 0)); then
echo "Up to date with upstream $BRANCH."
exit 0
fi
echo "$count new upstream commit(s):"
git log --format=' %h %ad %s' --date=short "$base..$UPSTREAM"
echo
echo "Files they change, by where this project stands on each:"
echo " unchanged here: merges as is changed here: merge both sides"
echo " removed here: keep it removed new upstream: take it, unless it belongs to a removed area"
echo
git diff --name-only "$base" "$UPSTREAM" | while IFS= read -r file; do
if ! git cat-file -e "HEAD:$file" 2>/dev/null; then
if git cat-file -e "$base:$file" 2>/dev/null; then
echo "removed here $file"
else
echo "new upstream $file"
fi
elif git diff --quiet "$base" HEAD -- "$file"; then
echo "unchanged here $file"
else
echo "changed here $file"
fi
done | sort
+3 -2
View File
@@ -46,8 +46,9 @@ export class DockerDriver implements Driver {
`PUID=${uid}`,
`PGID=${gid}`,
`SUBFOLDER=${streamPath(id)}`,
`SELKIES_MANUAL_WIDTH=${size.width}`,
`SELKIES_MANUAL_HEIGHT=${size.height}`,
// The game's size, rounded up to even for H.264
`SELKIES_MANUAL_WIDTH=${size.width + (size.width % 2)}`,
`SELKIES_MANUAL_HEIGHT=${size.height + (size.height % 2)}`,
'SELKIES_UI_SHOW_SIDEBAR=false',
'HARDEN_DESKTOP=true',
'RESTART_APP=true',
+6 -11
View File
@@ -59,7 +59,7 @@ export class GameCatalog {
return null;
}
const page = await this.readContent(gameId, row, row.cmd).catch(() => null);
return even((page && appletSize(page.toString('latin1'))) ?? DEFAULT_SIZE);
return ((page && appletSize(page.toString('latin1'))) ?? DEFAULT_SIZE);
}
const program = await this.launchProgram(row);
if (!program) {
@@ -68,19 +68,19 @@ export class GameCatalog {
const url = row.cmd.match(/https?:\/\/[^\s"']+/)?.[0];
if (!url) {
const exe = await readFile(program).catch(() => null);
return even((exe && projectorStageSize(exe)) ?? DEFAULT_SIZE);
return ((exe && projectorStageSize(exe)) ?? DEFAULT_SIZE);
}
const content = await this.readContent(gameId, row, url).catch(() => null);
const extension = path.extname(new URL(url).pathname).toLowerCase();
if (['.dcr', '.dir', '.dxr', '.cct', '.cst'].includes(extension)) {
return even((content && directorStageSize(content)) ?? DEFAULT_SIZE);
return ((content && directorStageSize(content)) ?? DEFAULT_SIZE);
}
if (extension === '.swf') {
// The exact stage: in full screen, games that set noScale keep their native size
return even((content && swfStageSize(content)) ?? DEFAULT_SIZE);
// The exact stage, so the movie fills the display without scaling
return ((content && swfStageSize(content)) ?? DEFAULT_SIZE);
}
const embed = content && embedSize(content.toString('latin1'));
return even(embed ? { width: embed.width + 2 * PAGE_MARGIN, height: embed.height + 2 * PAGE_MARGIN } : BROWSER_SIZE);
return (embed ? { width: embed.width + 2 * PAGE_MARGIN, height: embed.height + 2 * PAGE_MARGIN } : BROWSER_SIZE);
}
/**
@@ -151,11 +151,6 @@ async function resolveCaseInsensitive(root: string, relative: string): Promise<s
return (await stat(current).catch(() => null))?.isFile() ? current : null;
}
/** H.264 needs even dimensions. */
function even({ width, height }: Size): Size {
return { width: width + (width % 2), height: height + (height % 2) };
}
/** Reads the stage size from a SWF header: a rect of twips after the 8-byte header. */
export function swfStageSize(swf: Buffer): Size | null {
const signature = swf.toString('latin1', 0, 3);
+3 -3
View File
@@ -177,11 +177,11 @@ test('GameCatalog streams allowed platforms whose launcher resolves, sized from
legacy: { width: 300, height: 200 },
missing: { width: 800, height: 600 },
webstart: null,
spr: { width: 500, height: 426 },
oldcpu: { width: 500, height: 426 },
spr: { width: 500, height: 425 },
oldcpu: { width: 500, height: 425 },
secure: { width: 640, height: 480 },
navigator: { width: 528, height: 400 },
flash: { width: 500, height: 376 },
flash: { width: 500, height: 375 },
unity: null,
'no-program': null,
exe: { width: 800, height: 600 },
+1
View File
@@ -30,6 +30,7 @@ RUN apt-get update && \
libxtst6 \
libxxf86vm1 \
sqlite3 \
x11-utils \
xdotool && \
rm -rf /var/lib/apt/lists/*
+8 -3
View File
@@ -52,6 +52,11 @@ writable mirror of the install, since they write beside themselves; a
Director projector's `Prefs`, where it saves, is linked into `/config/saves`, as
is Flash Player's save directory, so the session manager syncs both to the
player's browser. OldCPUSimulator hangs under Wine, so its target runs under
`cpulimit` at the same share of the CPU. Flash projectors switch to full screen,
which hides their menu bar and scales the movie to the display, and Flashpoint
Navigator's toolbars are hidden.
`cpulimit` at the same share of the CPU. A Flash projector's window is
stretched to the display and moved up by its menu bar's height, which hides the
menu bar. GTK draws the menu bar in the window itself and the movie in a child
window below it, so the movie's offset, read with `xwininfo`, is the menu bar's
height. The window is made override-redirect first, since the window manager
keeps windows on screen, and is kept in place while it's open, since Flash
Player moves it back once the movie has loaded. Its full-screen mode isn't used, since some games, such
as Bloons, don't play right in it. Flashpoint Navigator's toolbars are hidden.
@@ -47,8 +47,42 @@ case ${app,,} in
mkdir -p "$HOME/saves/Flash_Player" "$HOME/.macromedia"
[[ -e $HOME/.macromedia/Flash_Player ]] || ln -s "$HOME/saves/Flash_Player" "$HOME/.macromedia/Flash_Player"
if [[ ${app,,} == 'fpsoftware\flash\'* ]]; then
# Full screen hides the projector's menu bar and scales the movie to the display
(xdotool search --sync --onlyvisible --name 'Flash Player' windowactivate --sync key ctrl+f) &
# Hide the projector's menu bar by moving it above the display, and fill
# the display with the movie. Not full screen, which some games, such as
# Bloons, don't play right in. The window manager keeps windows on
# screen, so the window is taken out of its hands first.
(
set +eo pipefail
wid=$(xdotool search --sync --onlyvisible --name 'Flash Player' | head -1)
read -r width height < <(xdotool getdisplaygeometry)
# GTK draws the menu bar in the window itself, and the movie in the
# largest child window, below it: the movie's offset is the menu bar's height
menu=$(xwininfo -children -id "$wid" | awk '
match($0, /[0-9]+x[0-9]+\+-?[0-9]+\+-?[0-9]+/) {
split(substr($0, RSTART, RLENGTH), g, /[x+]/)
if (g[1] * g[2] > area) { area = g[1] * g[2]; top = g[4] }
}
END { print top + 0 }')
((menu >= 0 && menu < height)) || menu=0
echo "Flash Player window $wid: menu bar ${menu}px, display ${width}x${height}"
xdotool windowunmap --sync "$wid"
xdotool set_window --overrideredirect 1 "$wid"
xdotool windowmap --sync "$wid"
place() { xdotool windowsize "$wid" "$width" $((height + menu)) windowmove "$wid" 0 $((-menu)); }
place
xdotool windowfocus "$wid"
# Flash Player moves its window back once the movie has loaded, so it's
# kept in place for as long as it's open
moves=0
while geometry=$(xdotool getwindowgeometry --shell "$wid" 2>/dev/null); do
eval "$geometry"
if ((X != 0 || Y != -menu || WIDTH != width || HEIGHT != height + menu)); then
((++moves <= 3)) && echo "Flash Player window moved to $X,$Y (${WIDTH}x${HEIGHT}); putting it back"
place
fi
sleep 0.5
done
) &
fi
cd "${argv[0]}"
exec "${argv[@]:1}" ;;