Files
feuerwerk/src/core/store/ReleaseStore.ts
T
adminandClaude Opus 5.5 4194f3e150 Share a link that plays the game without the editor (#266)
A project could only be played inside the editor, behind the login.
To show a game to someone else there was no way short of an account.
A release freezes the saved project under a random token and serves
it on a page that holds nothing but the game view.

- Menu -> Release... opens a dialog with the state of the release,
  the link with Copy link and Open, Publish or Update release, and
  Take down behind a confirmation.
- Publish saves first and refuses while anything is unsaved, like
  snapshots. Updating keeps the link. Take down kills it, and a new
  Publish hands out a new token.
- The link is play.html?game=<32 hex>. The page needs no session,
  paints Theme.colors.background, zooms with Auto and waits for
  "Click to play" so that sound and keyboard focus are allowed.
- The game always starts in the start scene. Persistent variables
  are stored per link in the browser. Plugins are not loaded, and the
  dialog warns about them and about a project without a scene.
- GET ?r=releases/{token} is the only route besides auth/* that works
  without a login. The public answer carries the token as project.id,
  so the project id stays hidden. Release files are written with 0640
  and nginx does not serve them directly (measured 403).
- Starting, the frame and Load scene moved into runtime/GameRun.ts.
  Play in the editor and the play page run through the same code.
- WorldViewport moved to src/viewport/ and no longer needs the
  EditorState. Pointer forwarding (gamePointer.ts) and setting up
  textures, fonts and sounds (projectFiles.ts) exist only once.
- Vite builds two pages. The play page loads 4 kB of its own code
  plus the shared chunk (Pixi, runtime, drawing, 653 kB) and Rapier,
  without the editor.

Found on the way, not fixed here: data/projects/<id>.json answers 200
without a login because the files are 0644.

Tests:
- ./check.sh: 194 files, 2808 tests, exit 0.
- tests/php/releases.test.php: 20 checks, also added to CI.
- New e2e module 60 with 10 checks, played in a browser without a
  session. Module 46 counts the menu entries and was updated.
- Full e2e run: 1572 checks passed.
- Counter-checks: 3 unit, 2 PHP and 3 browser mutations all fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:30:38 +02:00

41 lines
1.5 KiB
TypeScript

/** A project's released copy as the release dialog shows it (#266). */
export interface ReleaseSummary {
/** What the link carries: 32 hex digits, the same for as long as the project stays released. */
token: string;
/** The project revision the copy was taken from. */
revision: number;
/** ISO time, UTC. */
publishedAt: string;
}
/** What the play page loads: the copy alone, under the token rather than the project's own id. */
export interface ReleasedGame {
name: string;
publishedAt: string;
project: unknown;
}
/**
* The released copy of a project. Publishing copies the project as the server stored it, so what a
* tab has not saved yet is not part of it; publishing again replaces the copy and keeps the link.
*/
export interface ReleaseStore {
/** null while the project is not released. */
of(projectId: string): Promise<ReleaseSummary | null>;
publish(projectId: string): Promise<ReleaseSummary>;
takeDown(projectId: string): Promise<void>;
}
/** The page a released game is played on, next to the editor's own. */
export const PLAY_PAGE = '/game-maker/play.html';
export function releaseUrl(origin: string, token: string): string {
return `${origin}${PLAY_PAGE}?game=${token}`;
}
/** The token a play page was opened with, or null when the address carries none that could be one. */
export function releaseToken(search: string): string | null {
const token = new URLSearchParams(search).get('game') ?? '';
return /^[a-f0-9]{32}$/.test(token) ? token : null;
}