diff --git a/README.md b/README.md index 6310fe01..cafc7c38 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Game Maker +# Feuerwerk [![game-maker](https://git.nockal.com/meleki/game-maker/actions/workflows/ci.yml/badge.svg?branch=main)](https://git.nockal.com/meleki/game-maker/actions?workflow=ci.yml) @@ -8,7 +8,7 @@ all in one program, all drawn inside a single WebGL canvas. ![](screenshot.png) -Live: · Widget playground: +Live: · Widget playground: **The documentation is the wiki:** – every concept, node, tab and shortcut, plus the architecture and the development workflow. This file is the short @@ -39,7 +39,7 @@ Node 22 is required. ```bash npm install -npm run dev # http://localhost:5175/game-maker/dist/ – API calls are proxied to production +npm run dev # http://localhost:5175/feuerwerk/dist/ – API calls are proxied to production npm run check # typecheck + lint + format:check + unit tests – the gate CI runs too npm run build # writes dist/ and regenerates index.html; the folder is what nginx serves ``` diff --git a/api/index.php b/api/index.php index eac1729e..21fba1b1 100644 --- a/api/index.php +++ b/api/index.php @@ -2,7 +2,7 @@ declare(strict_types=1); /** - * Game Maker API. + * Feuerwerk API. * * Routing uses the `r` query parameter (e.g. `index.php?r=projects/abc123`) because the * site's nginx configuration does not pass PATH_INFO to PHP. @@ -736,6 +736,6 @@ try { default => Response::error('Not found', 404), }; } catch (Throwable $e) { - error_log('[game-maker api] ' . $e->getMessage()); + error_log('[feuerwerk api] ' . $e->getMessage()); Response::error('Internal server error', 500); } diff --git a/api/lib/Auth.php b/api/lib/Auth.php index 7da17f4d..bf8be378 100644 --- a/api/lib/Auth.php +++ b/api/lib/Auth.php @@ -121,12 +121,12 @@ final class Auth } /** - * The path the editor is served under (`/game-maker/`), read off the script rather than + * The path the editor is served under (`/feuerwerk/`), read off the script rather than * written down: the same checkout answers under a different prefix in CI. */ private static function basePath(): string { - $script = (string) ($_SERVER['SCRIPT_NAME'] ?? '/game-maker/api/index.php'); + $script = (string) ($_SERVER['SCRIPT_NAME'] ?? '/feuerwerk/api/index.php'); $base = dirname(dirname($script)); return $base === '/' || $base === '\\' || $base === '.' ? '/' : $base . '/'; } diff --git a/api/lib/SessionStore.php b/api/lib/SessionStore.php index 8ea937b1..5fdd14a7 100644 --- a/api/lib/SessionStore.php +++ b/api/lib/SessionStore.php @@ -14,7 +14,7 @@ final class SessionStore /** How long a login lasts without being used; every request that finds it pushes it out again. */ public const LIFETIME_SECONDS = 30 * 24 * 60 * 60; - public const COOKIE = 'game_maker_session'; + public const COOKIE = 'feuerwerk_session'; public function __construct(private readonly string $dir) {} diff --git a/package-lock.json b/package-lock.json index 8981a331..bd8d4ee2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "game-maker", + "name": "feuerwerk", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "game-maker", + "name": "feuerwerk", "version": "0.1.0", "dependencies": { "@dimforge/rapier2d-compat": "^0.20.0", diff --git a/package.json b/package.json index 5456cfaf..a19798b7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "game-maker", + "name": "feuerwerk", "private": true, "version": "0.1.0", "type": "module", diff --git a/src/core/music/ogg.ts b/src/core/music/ogg.ts index baf1ba1f..5da3d916 100644 --- a/src/core/music/ogg.ts +++ b/src/core/music/ogg.ts @@ -25,7 +25,7 @@ export interface OpusStream { /** What a page holds at most: 255 lacing values, and so at most a packet of about 64 kB. */ const MAX_SEGMENTS = 255; const SERIAL = 0x6d616b65; -const VENDOR = 'game-maker'; +const VENDOR = 'feuerwerk'; const CRC_TABLE = (() => { const table = new Uint32Array(256); diff --git a/src/core/site.ts b/src/core/site.ts new file mode 100644 index 00000000..e7d9cdc5 --- /dev/null +++ b/src/core/site.ts @@ -0,0 +1,39 @@ +/** The path the site is served under: the editor, the play page, the API and every data file. */ +export const SITE_PATH = '/feuerwerk/'; + +/** The API's one entry point. */ +export const API_ENDPOINT = `${SITE_PATH}api/index.php`; + +/** Where the uploaded files of one kind are served, e.g. `dataPath('textures')`. */ +export function dataPath(kind: string): string { + return `${SITE_PATH}data/${kind}`; +} + +/** The prefix of every `localStorage` key while the editor was called Game Maker. */ +const OLD_KEY_PREFIX = 'game-maker.'; +const KEY_PREFIX = 'feuerwerk.'; + +type KeyStore = Pick; + +/** + * Moves what a browser kept under the old name to the new one: settings, panel sizes, folds and + * saves survive the rename. A key the new name already has keeps its value. Answers how many moved. + */ +export function moveOldStoredKeys(storage: KeyStore): number { + const old: string[] = []; + for (let i = 0; i < storage.length; i++) { + const key = storage.key(i); + if (key?.startsWith(OLD_KEY_PREFIX)) old.push(key); + } + let moved = 0; + for (const key of old) { + const value = storage.getItem(key); + const renamed = KEY_PREFIX + key.slice(OLD_KEY_PREFIX.length); + if (value !== null && storage.getItem(renamed) === null) { + storage.setItem(renamed, value); + moved++; + } + storage.removeItem(key); + } + return moved; +} diff --git a/src/core/store/ReleaseStore.ts b/src/core/store/ReleaseStore.ts index bce3d5b8..91d7592f 100644 --- a/src/core/store/ReleaseStore.ts +++ b/src/core/store/ReleaseStore.ts @@ -1,3 +1,5 @@ +import { SITE_PATH } from '../site'; + /** 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. */ @@ -27,7 +29,7 @@ export interface ReleaseStore { } /** The page a released game is played on, next to the editor's own. */ -const PLAY_PAGE = '/game-maker/play.html'; +const PLAY_PAGE = `${SITE_PATH}play.html`; export function releaseUrl(origin: string, token: string): string { return `${origin}${PLAY_PAGE}?game=${token}`; diff --git a/src/editor/App.ts b/src/editor/App.ts index 984859ac..8c64a20f 100644 --- a/src/editor/App.ts +++ b/src/editor/App.ts @@ -21,10 +21,9 @@ import { Editor } from './Editor'; import { isDragFrame } from './framePreview'; import { startPlugins } from './plugins/startPlugins'; import { CelUploadStore } from './pixelart/celUploadStore'; +import { API_ENDPOINT } from '../core/site'; import { EditorState } from './state'; -const API_ENDPOINT = '/game-maker/api/index.php'; - /** * Shows the sign-in screen until somebody is signed in, and answers with their name. Returns * right away when the session from an earlier visit is still good – which is what lets a @@ -70,7 +69,7 @@ export async function startEditor(app: Application, root: VStack): Promise const { user, role } = await signedInUser(app, root, auth); // Who is signed in goes into the window title: the toolbar is crowded enough, and the title is // also what tells two tabs on two accounts apart. - document.title = `Game Maker – ${user}`; + document.title = `Feuerwerk – ${user}`; const state = new EditorState(); // Drawn pictures (#185) are looked up in the project this session holds; the next session // installs its own, which is also what lets go of everything the old one had drawn. @@ -123,7 +122,7 @@ export async function startEditor(app: Application, root: VStack): Promise */ const endSession = async (editor: Editor): Promise => { await editor.dispose(); - document.title = 'Game Maker'; + document.title = 'Feuerwerk'; // The console panel is page-wide, the lines in it are not: what one account's project printed // is none of the next one's business. consoleLog.clear(); diff --git a/src/editor/auth/LoginScreen.ts b/src/editor/auth/LoginScreen.ts index 3302688b..f90f5a7b 100644 --- a/src/editor/auth/LoginScreen.ts +++ b/src/editor/auth/LoginScreen.ts @@ -46,7 +46,7 @@ export class LoginScreen extends VStack { this.add(new Spacer(), 1); const card = new Card(); - card.add(new Label('Game Maker', { style: 'title' })); + card.add(new Label('Feuerwerk', { style: 'title' })); card.add(new Label('Sign in to open your projects', { style: 'muted' })); card.add(new Spacer(Theme.gap)); diff --git a/src/editor/editorRecordingSettings.ts b/src/editor/editorRecordingSettings.ts index 2e386c2c..a7f348e2 100644 --- a/src/editor/editorRecordingSettings.ts +++ b/src/editor/editorRecordingSettings.ts @@ -5,7 +5,7 @@ */ import { editorRecordingSettingsOf, type EditorRecordingSettings } from '../core/recording/editorRecording'; -const STORAGE_KEY = 'game-maker.editor-recording'; +const STORAGE_KEY = 'feuerwerk.editor-recording'; let kept: EditorRecordingSettings | null = null; diff --git a/src/editor/generalSettings.ts b/src/editor/generalSettings.ts index 38792eaf..fdce7522 100644 --- a/src/editor/generalSettings.ts +++ b/src/editor/generalSettings.ts @@ -5,7 +5,7 @@ */ import { clamp } from '../ui/math'; -const STORAGE_KEY = 'game-maker.general-settings'; +const STORAGE_KEY = 'feuerwerk.general-settings'; export interface GeneralSettings { /** How long a zoom takes to reach its new step, in ms; 0 jumps at once. */ diff --git a/src/editor/panelSizes.ts b/src/editor/panelSizes.ts index 7b73ab89..6129736e 100644 --- a/src/editor/panelSizes.ts +++ b/src/editor/panelSizes.ts @@ -5,7 +5,7 @@ */ import { clamp } from '../ui/math'; -const STORAGE_KEY = 'game-maker.panel-sizes'; +const STORAGE_KEY = 'feuerwerk.panel-sizes'; let sizes: Map | null = null; diff --git a/src/editor/saves.ts b/src/editor/saves.ts index af83ba3a..53a5b115 100644 --- a/src/editor/saves.ts +++ b/src/editor/saves.ts @@ -24,7 +24,7 @@ function browserStore(): Store | null { } export function saveKey(projectId: string, area: SaveAreaName): string { - return `game-maker.saves.${projectId}.${area}`; + return `feuerwerk.saves.${projectId}.${area}`; } function readArea(store: Store | null, key: string): SavedValues { diff --git a/src/editor/sectionFolds.ts b/src/editor/sectionFolds.ts index 9d7c64b2..17129a66 100644 --- a/src/editor/sectionFolds.ts +++ b/src/editor/sectionFolds.ts @@ -5,7 +5,7 @@ */ import { Theme } from '../ui/Theme'; -const STORAGE_KEY = 'game-maker.folded-sections'; +const STORAGE_KEY = 'feuerwerk.folded-sections'; let folded: Set | null = null; diff --git a/src/editor/song/SongEditor.ts b/src/editor/song/SongEditor.ts index 9407ffbd..4c3f7ba1 100644 --- a/src/editor/song/SongEditor.ts +++ b/src/editor/song/SongEditor.ts @@ -53,7 +53,7 @@ const INFO_WIDTH = 200; const GRID_WIDTH = 64; const INPUT_WIDTH = 104; /** Where the browser keeps the input picked last. */ -const INPUT_KEY = 'game-maker.record-input'; +const INPUT_KEY = 'feuerwerk.record-input'; /** What the tab shows below its header: the tracks along time, or where they go (#350). */ export type SongView = 'timeline' | 'routing'; diff --git a/src/editor/video/VideoCanvas.ts b/src/editor/video/VideoCanvas.ts index d12cf5c9..204c2194 100644 --- a/src/editor/video/VideoCanvas.ts +++ b/src/editor/video/VideoCanvas.ts @@ -40,7 +40,7 @@ import { zoomWave, } from '../sound/waveView'; import { soundLibrary } from '../../viewport/SoundLibrary'; -import { openVideo } from '../../viewport/VideoLibrary'; +import { openVideo, videoFileUrl } from '../../viewport/VideoLibrary'; import { chaseSeeks, seekChase, type SeekChase, seekVideo } from '../../viewport/videoSeek'; import type { EditorState } from '../state'; import { containRect } from '../texture/imageGrid'; @@ -376,7 +376,7 @@ export class VideoCanvas extends Widget { chase: seekChase(), asked: held, gain: null, - element: openVideo(`/game-maker/data/videos/${asset.file}`, held, (texture) => { + element: openVideo(videoFileUrl(asset.file), held, (texture) => { if (this.preview !== preview) return texture.destroy(true); preview.texture = texture; texture.source.on('update', () => this.invalidate()); diff --git a/src/page.ts b/src/page.ts index ee163eb1..ba3f8710 100644 --- a/src/page.ts +++ b/src/page.ts @@ -1,6 +1,7 @@ import { Application, Container } from 'pixi.js'; import { initPixelGrid, initRenderLoop, initUI, loadEditorFonts, Theme, tieBreakFor, VStack } from './ui'; import { fixedPageSize, type PageSize, placePage } from './ui/pageSize'; +import { moveOldStoredKeys } from './core/site'; // `desynchronized` bypasses the compositor queue (headless click-to-paint 72 → 16 ms) but may // tear; `?vsync` opts out. Pixi has no option for it, hence our own context. @@ -21,6 +22,11 @@ function lowLatencyContext(): { canvas?: HTMLCanvasElement; context?: WebGL2Rend * window and follows it when it is resized, unless `?w=&h=` give it a fixed size. */ export async function createPage(params: URLSearchParams): Promise<{ app: Application; root: VStack }> { + try { + moveOldStoredKeys(window.localStorage); + } catch { + // A browser that refuses `localStorage` has nothing to move. + } const fonts = loadEditorFonts(); const fixed = fixedPageSize(params); const app = new Application(); diff --git a/src/play.ts b/src/play.ts index 4600d37e..ca88f4c6 100644 --- a/src/play.ts +++ b/src/play.ts @@ -15,8 +15,7 @@ import { projectPictureFactory } from './viewport/generatedPictures'; import { preloadProjectFiles, setUpProjectFiles } from './viewport/projectFiles'; import { textureLibrary } from './viewport/TextureLibrary'; import { createEmptyProject } from './core/model/Project'; - -const API_ENDPOINT = '/game-maker/api/index.php'; +import { API_ENDPOINT } from './core/site'; async function main(): Promise { const params = new URLSearchParams(location.search); diff --git a/src/player/playerHook.ts b/src/player/playerHook.ts index 24ddc5c0..c8c221ea 100644 --- a/src/player/playerHook.ts +++ b/src/player/playerHook.ts @@ -3,12 +3,12 @@ import type { GameScreen } from './GameScreen'; import type { Player } from './Player'; /** - * `window.__gameMakerPlay`: what the check scripts read off a released game (#266). The editor's + * `window.__feuerwerkPlay`: what the check scripts read off a released game (#266). The editor's * `__gm` is not on this page, and neither is anything it would have to reach into. Installed before * the game is loaded, so a link that leads nowhere can be read as well: `player` is null until then. */ export function installPlayerHook(screen: GameScreen, player: () => Player | null): void { - (window as unknown as { __gameMakerPlay: unknown }).__gameMakerPlay = { + (window as unknown as { __feuerwerkPlay: unknown }).__feuerwerkPlay = { running: () => player()?.isRunning ?? false, screen: () => screen.info(), log: () => player()?.log() ?? [], diff --git a/src/playground.ts b/src/playground.ts index 06626cfd..41e86590 100644 --- a/src/playground.ts +++ b/src/playground.ts @@ -119,7 +119,7 @@ export function mountPlayground(app: Application, root: VStack): void { listPanel.content.add(scroll, 1); row.add(listPanel, 1); - root.add(new Label('Game Maker – UI playground', { style: 'bodyBold', padding: Theme.padding })); + root.add(new Label('Feuerwerk – UI playground', { style: 'bodyBold', padding: Theme.padding })); root.add(row, 1); root.add(status); root.resize(app.screen.width, app.screen.height); diff --git a/src/viewport/FontLibrary.ts b/src/viewport/FontLibrary.ts index 4b22a4a5..b70375da 100644 --- a/src/viewport/FontLibrary.ts +++ b/src/viewport/FontLibrary.ts @@ -1,4 +1,5 @@ import { fontFamilyOf } from '../core/model/font'; +import { dataPath } from '../core/site'; import { FileLibrary } from './FileLibrary'; /** @@ -9,7 +10,7 @@ class FontLibrary extends FileLibrary { private readonly ready = new Set(); constructor() { - super('/game-maker/data/fonts', 'fonts'); + super(dataPath('fonts'), 'fonts'); } /** diff --git a/src/viewport/PixelCels.ts b/src/viewport/PixelCels.ts index b8741969..8596dc14 100644 --- a/src/viewport/PixelCels.ts +++ b/src/viewport/PixelCels.ts @@ -1,6 +1,7 @@ import type { Project } from '../core/model/Project'; import { celFilesOf } from '../core/model/pixelart'; import { type CelPixels, celFileName, decodeCel, encodeCel, isEmptyCel } from '../core/model/pixelCel'; +import { dataPath } from '../core/site'; import { FileLibrary } from './FileLibrary'; /** Where cels go on the server: raw bytes in, the stored name back. */ @@ -20,7 +21,7 @@ export class PixelCels extends FileLibrary { private readonly local = new Set(); constructor() { - super('/game-maker/data/pixels', 'pixels'); + super(dataPath('pixels'), 'pixels'); } /** diff --git a/src/viewport/RecordingTake.ts b/src/viewport/RecordingTake.ts index e54423fa..7aa53fab 100644 --- a/src/viewport/RecordingTake.ts +++ b/src/viewport/RecordingTake.ts @@ -408,7 +408,7 @@ export class Take { } : null, durationMs: this.lastMs + frame, - app: 'game-maker', + app: 'feuerwerk', }); this.collect(); const blob = new Blob([head, ...this.folded, ...this.loose, ...rest] as BlobPart[], { diff --git a/src/viewport/SoundLibrary.ts b/src/viewport/SoundLibrary.ts index 80877e95..33307e17 100644 --- a/src/viewport/SoundLibrary.ts +++ b/src/viewport/SoundLibrary.ts @@ -16,6 +16,7 @@ import { stageLatency, } from '../core/soundfx/nodes'; import { releaseTime } from '../core/instrument/envelope'; +import { dataPath } from '../core/site'; import type { EaseShare } from '../core/model/animation'; import { BAKE_RATE } from '../core/music/bake'; import { @@ -66,7 +67,7 @@ import { wake } from './wake'; /** A voice is no louder than a sound item of a song turned all the way up. */ const MAX_VOLUME = dbToGain(SOUND_ITEM_MAX_DB); -const DEFAULT_BASE = '/game-maker/data/sounds'; +const DEFAULT_BASE = dataPath('sounds'); /** The rate a sound loaded ahead of its first play is decoded at; a source resamples it on the way out. */ const DECODE_RATE = 48000; /** The rate the picture of a streamed sound is decoded at (#349): enough for its peaks, a sixth of the size. */ diff --git a/src/viewport/TextureLibrary.ts b/src/viewport/TextureLibrary.ts index d66865fc..b258fce8 100644 --- a/src/viewport/TextureLibrary.ts +++ b/src/viewport/TextureLibrary.ts @@ -3,6 +3,7 @@ import { isGeneratedFile } from '../core/model/generated'; import { isVideoKey } from '../core/model/video'; import type { FrameRect, TextureRef } from '../core/model/texture'; import { type EdgeMode, SHEET_EDGE } from '../core/shader/nodes'; +import { dataPath } from '../core/site'; import { FileLibrary } from './FileLibrary'; import { GeneratedPictures, type PictureFactory } from './generatedPictures'; import { videoLibrary } from './VideoLibrary'; @@ -46,7 +47,7 @@ export class TextureLibrary extends FileLibrary { ); constructor() { - super('/game-maker/data/textures', 'textures'); + super(dataPath('textures'), 'textures'); videoLibrary.onChange(() => this.notifyChange()); } diff --git a/src/viewport/VideoLibrary.ts b/src/viewport/VideoLibrary.ts index ec4bc89a..88c34e10 100644 --- a/src/viewport/VideoLibrary.ts +++ b/src/viewport/VideoLibrary.ts @@ -12,6 +12,7 @@ */ import { Assets, Texture, VideoSource } from 'pixi.js'; import type { PropertyBag } from '../core/model/Properties'; +import { dataPath } from '../core/site'; import type { TextureRef } from '../core/model/texture'; import { drivenTimeOf, @@ -27,7 +28,7 @@ import { soundLibrary } from './SoundLibrary'; import { followVideo, readSpeed, type SpeedReading } from './videoFollow'; import { chaseSeeks, seekVideo, type SeekChase } from './videoSeek'; -const VIDEO_BASE = '/game-maker/data/videos'; +const VIDEO_BASE = dataPath('videos'); /** Where the MP4 of a video asset's `file` is served. */ export function videoFileUrl(file: string): string { diff --git a/src/viewport/projectFiles.ts b/src/viewport/projectFiles.ts index 85d32ff0..ea322f42 100644 --- a/src/viewport/projectFiles.ts +++ b/src/viewport/projectFiles.ts @@ -1,5 +1,6 @@ import type { Renderer } from 'pixi.js'; import type { Project } from '../core/model/Project'; +import { dataPath } from '../core/site'; import { projectFiles } from '../core/model/projectFiles'; import { clipCopyTable, streamTable } from '../core/music/streams'; import { requestRender } from '../ui'; @@ -14,10 +15,10 @@ import { installVectorArtPictures } from './vectorArtPicture'; * Where uploaded images live: the API writes them, the browser reads them straight off disk as * static files named by their content hash. Fonts and sounds work the same way, one directory over. */ -const TEXTURE_BASE = '/game-maker/data/textures'; -const FONT_BASE = '/game-maker/data/fonts'; -const SOUND_BASE = '/game-maker/data/sounds'; -const PIXEL_BASE = '/game-maker/data/pixels'; +const TEXTURE_BASE = dataPath('textures'); +const FONT_BASE = dataPath('fonts'); +const SOUND_BASE = dataPath('sounds'); +const PIXEL_BASE = dataPath('pixels'); /** * Points the file libraries at the server, once per page: the editor's and a released game's (#266). diff --git a/src/viewport/takeRecorder.ts b/src/viewport/takeRecorder.ts index 62dfecc9..5958e149 100644 --- a/src/viewport/takeRecorder.ts +++ b/src/viewport/takeRecorder.ts @@ -109,7 +109,7 @@ export class TakeRecorder { await loadWorklet(context); if (context.state === 'suspended') await context.resume(); const source = context.createMediaStreamSource(stream); - const node = new AudioWorkletNode(context, 'game-maker-take', { + const node = new AudioWorkletNode(context, 'feuerwerk-take', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [1], diff --git a/src/viewport/takeWorklet.ts b/src/viewport/takeWorklet.ts index 815f12a0..0574239a 100644 --- a/src/viewport/takeWorklet.ts +++ b/src/viewport/takeWorklet.ts @@ -49,4 +49,4 @@ class TakeProcessor extends AudioWorkletProcessor { } } -registerProcessor('game-maker-take', TakeProcessor); +registerProcessor('feuerwerk-take', TakeProcessor); diff --git a/tests/core/matroska.test.ts b/tests/core/matroska.test.ts index daa90b9c..3991e209 100644 --- a/tests/core/matroska.test.ts +++ b/tests/core/matroska.test.ts @@ -80,7 +80,7 @@ const HEAD: MatroskaHead = { }, audio: { channels: 2, private: new TextEncoder().encode('OpusHead'), preSkip: 312 }, durationMs: 1234.5, - app: 'game-maker', + app: 'feuerwerk', }; /** The whole file: the head, then the clusters handed out before (`earlier`) and at the end. */ diff --git a/tests/core/releases.test.ts b/tests/core/releases.test.ts index f3016eb5..d8f57ece 100644 --- a/tests/core/releases.test.ts +++ b/tests/core/releases.test.ts @@ -34,7 +34,7 @@ beforeAll(async () => { describe('the link of a release', () => { it('leads to the play page with the token, and the play page reads it back', () => { const url = releaseUrl('https://nockal.com', TOKEN); - expect(url).toBe(`https://nockal.com/game-maker/play.html?game=${TOKEN}`); + expect(url).toBe(`https://nockal.com/feuerwerk/play.html?game=${TOKEN}`); expect(releaseToken(new URL(url).search)).toBe(TOKEN); }); diff --git a/tests/core/site.test.ts b/tests/core/site.test.ts new file mode 100644 index 00000000..1de1556c --- /dev/null +++ b/tests/core/site.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { API_ENDPOINT, dataPath, moveOldStoredKeys } from '../../src/core/site'; + +function memoryStore(initial: Record) { + const items = new Map(Object.entries(initial)); + return { + items, + get length() { + return items.size; + }, + key: (i: number) => [...items.keys()][i] ?? null, + getItem: (key: string) => items.get(key) ?? null, + setItem: (key: string, value: string) => void items.set(key, value), + removeItem: (key: string) => void items.delete(key), + }; +} + +describe('site', () => { + it('serves the API and the data files under /feuerwerk/', () => { + expect(API_ENDPOINT).toBe('/feuerwerk/api/index.php'); + expect(dataPath('textures')).toBe('/feuerwerk/data/textures'); + }); + + it('moves the keys stored under the old name and leaves the others alone', () => { + const store = memoryStore({ + 'game-maker.panel-sizes': '{"left":300}', + 'game-maker.saves.p1.Savegame': '{"Lives":2}', + 'other-site.key': 'x', + }); + expect(moveOldStoredKeys(store)).toBe(2); + expect(Object.fromEntries(store.items)).toEqual({ + 'feuerwerk.panel-sizes': '{"left":300}', + 'feuerwerk.saves.p1.Savegame': '{"Lives":2}', + 'other-site.key': 'x', + }); + }); + + it('keeps a value the new name already has and drops the old one', () => { + const store = memoryStore({ 'game-maker.general-settings': 'old', 'feuerwerk.general-settings': 'new' }); + expect(moveOldStoredKeys(store)).toBe(0); + expect(Object.fromEntries(store.items)).toEqual({ 'feuerwerk.general-settings': 'new' }); + }); + + it('does nothing the second time', () => { + const store = memoryStore({ 'game-maker.folded-sections': '[]' }); + moveOldStoredKeys(store); + expect(moveOldStoredKeys(store)).toBe(0); + expect(Object.fromEntries(store.items)).toEqual({ 'feuerwerk.folded-sections': '[]' }); + }); +}); diff --git a/tests/editor/panel-sizes.test.ts b/tests/editor/panel-sizes.test.ts index 93b0080c..eaaea08e 100644 --- a/tests/editor/panel-sizes.test.ts +++ b/tests/editor/panel-sizes.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { keepPanelSize, panelSize, resetPanelSizes } from '../../src/editor/panelSizes'; -const STORAGE_KEY = 'game-maker.panel-sizes'; +const STORAGE_KEY = 'feuerwerk.panel-sizes'; function memoryStore(initial: Record = {}) { const items = new Map(Object.entries(initial)); diff --git a/tests/editor/sections.test.ts b/tests/editor/sections.test.ts index 4124db86..4a30c07c 100644 --- a/tests/editor/sections.test.ts +++ b/tests/editor/sections.test.ts @@ -264,7 +264,7 @@ describe('the memory of folded groups', () => { setFolded(key, true); expect(isFolded(key)).toBe(true); resetFolds(); - vi.stubGlobal('window', { localStorage: memoryStore({ 'game-maker.folded-sections': '{broken' }) }); + vi.stubGlobal('window', { localStorage: memoryStore({ 'feuerwerk.folded-sections': '{broken' }) }); expect(isFolded(key)).toBe(false); }); diff --git a/tests/editor/view-tween.test.ts b/tests/editor/view-tween.test.ts index 1ed6b89c..01787e8d 100644 --- a/tests/editor/view-tween.test.ts +++ b/tests/editor/view-tween.test.ts @@ -405,7 +405,7 @@ describe('the general settings', () => { keepGeneralSettings({ zoomTransitionMs: 350 }); resetGeneralSettings(); expect(generalSettings().zoomTransitionMs).toBe(350); - expect(JSON.parse(store.items.get('game-maker.general-settings') ?? '')).toEqual({ + expect(JSON.parse(store.items.get('feuerwerk.general-settings') ?? '')).toEqual({ zoomTransitionMs: 350, showEmptyAssetGroups: false, customCursor: true, @@ -437,7 +437,7 @@ describe('the general settings', () => { resetGeneralSettings(); expect(generalSettings().customCursor).toBe(false); // Settings kept before the switch existed show it. - store.setItem('game-maker.general-settings', JSON.stringify({ zoomTransitionMs: 100 })); + store.setItem('feuerwerk.general-settings', JSON.stringify({ zoomTransitionMs: 100 })); resetGeneralSettings(); expect(generalSettings().customCursor).toBe(true); }); diff --git a/tests/php/accounts.test.php b/tests/php/accounts.test.php index 7e2a9e94..d5229ed5 100644 --- a/tests/php/accounts.test.php +++ b/tests/php/accounts.test.php @@ -40,7 +40,7 @@ function throws(callable $work, string $class = InvalidArgumentException::class) return false; } -$dir = sys_get_temp_dir() . '/game-maker-php-test-' . bin2hex(random_bytes(4)); +$dir = sys_get_temp_dir() . '/feuerwerk-php-test-' . bin2hex(random_bytes(4)); mkdir($dir); $file = "{$dir}/users.json"; diff --git a/tests/php/files.test.php b/tests/php/files.test.php index bab02496..276dba99 100644 --- a/tests/php/files.test.php +++ b/tests/php/files.test.php @@ -35,7 +35,7 @@ function wav(string $tag): string return 'RIFF' . pack('V', 36) . 'WAVE' . str_repeat("\0", 32) . $tag; } -$data = sys_get_temp_dir() . '/game-maker-php-files-' . bin2hex(random_bytes(4)); +$data = sys_get_temp_dir() . '/feuerwerk-php-files-' . bin2hex(random_bytes(4)); mkdir("{$data}/projects", 0775, true); $users = new UserStore("{$data}/users.json"); $users->put('ann', 'password1'); diff --git a/tests/php/releases.test.php b/tests/php/releases.test.php index ebb41db9..cdc465b9 100644 --- a/tests/php/releases.test.php +++ b/tests/php/releases.test.php @@ -25,7 +25,7 @@ function check(string $name, bool $ok, mixed $got = null): void echo "FAIL {$name}", $got === null ? '' : ' — got ' . json_encode($got), "\n"; } -$dir = sys_get_temp_dir() . '/game-maker-php-releases-' . bin2hex(random_bytes(4)); +$dir = sys_get_temp_dir() . '/feuerwerk-php-releases-' . bin2hex(random_bytes(4)); mkdir($dir); $releases = new ReleaseStore($dir); diff --git a/tests/php/snapshots.test.php b/tests/php/snapshots.test.php index 63704886..8b2cd1b4 100644 --- a/tests/php/snapshots.test.php +++ b/tests/php/snapshots.test.php @@ -25,7 +25,7 @@ function check(string $name, bool $ok, mixed $got = null): void echo "FAIL {$name}", $got === null ? '' : ' — got ' . json_encode($got), "\n"; } -$dir = sys_get_temp_dir() . '/game-maker-php-snapshots-' . bin2hex(random_bytes(4)); +$dir = sys_get_temp_dir() . '/feuerwerk-php-snapshots-' . bin2hex(random_bytes(4)); mkdir($dir); $snapshots = new SnapshotStore($dir); diff --git a/tests/php/videos.test.php b/tests/php/videos.test.php index 27fbb297..12b896be 100644 --- a/tests/php/videos.test.php +++ b/tests/php/videos.test.php @@ -82,7 +82,7 @@ check('other lines say nothing', VideoFormat::progress('frame=12', 10.0) === nul check('progress never passes the end', VideoFormat::progress('out_time_us=99000000', 10.0) === 1.0); // The store and its queue. -$data = sys_get_temp_dir() . '/game-maker-php-videos-' . bin2hex(random_bytes(4)); +$data = sys_get_temp_dir() . '/feuerwerk-php-videos-' . bin2hex(random_bytes(4)); $store = new VideoStore("{$data}/videos"); $folder = 'a1b2c3d4e5f6'; $rejected = static function (string $bytes) use ($store, $folder): bool { diff --git a/tools/ci/router.php b/tools/ci/router.php index 6238181f..253fe872 100644 --- a/tools/ci/router.php +++ b/tools/ci/router.php @@ -5,7 +5,7 @@ // the server as before. $path = rawurldecode((string) parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH)); -if (!preg_match('#^/game-maker/data/videos/[0-9a-z]+/[0-9a-f]{64}\.mp4$#', $path)) { +if (!preg_match('#^/feuerwerk/data/videos/[0-9a-z]+/[0-9a-f]{64}\.mp4$#', $path)) { return false; } $file = $_SERVER['DOCUMENT_ROOT'] . $path; diff --git a/tools/ci/run-e2e.sh b/tools/ci/run-e2e.sh index b19cc9a2..531a2ef3 100755 --- a/tools/ci/run-e2e.sh +++ b/tools/ci/run-e2e.sh @@ -5,7 +5,7 @@ set -euo pipefail cd "$(dirname "$(readlink -f "$0")")/../.." source tools/ci/serve.sh -game_maker_serve +feuerwerk_serve set +e node tools/seed-e2e.mjs && node tools/e2e.mjs diff --git a/tools/ci/run-smokes.sh b/tools/ci/run-smokes.sh index 4d75bc98..60c74f5e 100755 --- a/tools/ci/run-smokes.sh +++ b/tools/ci/run-smokes.sh @@ -5,7 +5,7 @@ set -euo pipefail cd "$(dirname "$(readlink -f "$0")")/../.." source tools/ci/serve.sh -game_maker_serve +feuerwerk_serve set +e code=0 diff --git a/tools/ci/serve.sh b/tools/ci/serve.sh index 3a967f10..40163adb 100644 --- a/tools/ci/serve.sh +++ b/tools/ci/serve.sh @@ -2,12 +2,12 @@ # Sourced, not run: it leaves the PHP server in the caller's background and exports # GAME_MAKER_BASE_URL, which every tool reads. # -# The build wires its assets absolutely under /game-maker/dist/, so the checkout has to be +# The build wires its assets absolutely under /feuerwerk/dist/, so the checkout has to be # served under that path segment – hence the symlink into /srv rather than `-t .`. -export GAME_MAKER_BASE_URL=http://127.0.0.1:8080/game-maker/ +export GAME_MAKER_BASE_URL=http://127.0.0.1:8080/feuerwerk/ -game_maker_serve() { +feuerwerk_serve() { local root root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) @@ -35,7 +35,7 @@ game_maker_serve() { return 0 fi - mkdir -p /srv && ln -sfn "$root" /srv/game-maker + mkdir -p /srv && ln -sfn "$root" /srv/feuerwerk # Uploaded videos are converted by the worker (#402); on stein2 that is the container # game-maker-video, here a process beside the server. php "$root/tools/video-worker.php" "$root/data" >/tmp/video-worker.log 2>&1 & diff --git a/tools/e2e/02-inspector.mjs b/tools/e2e/02-inspector.mjs index 9145b17b..4da1b7ef 100644 --- a/tools/e2e/02-inspector.mjs +++ b/tools/e2e/02-inspector.mjs @@ -71,7 +71,7 @@ export async function inspector(t) { JSON.stringify({ switches: switches?.y, enabled: enabledRow?.y, position: position?.y }), ); // A click on the band folds the group: its rows go, and the next band moves up against it. - const folds = () => json(`localStorage.getItem('game-maker.folded-sections')`); + const folds = () => json(`localStorage.getItem('feuerwerk.folded-sections')`); await click(transform.x, transform.y); await settle(); transform = await section('TRANSFORM'); diff --git a/tools/e2e/03-play.mjs b/tools/e2e/03-play.mjs index 82c91bfc..c9a66e8c 100644 --- a/tools/e2e/03-play.mjs +++ b/tools/e2e/03-play.mjs @@ -314,7 +314,7 @@ export async function play(t) { Math.abs(frame.x + frame.w / 2 - (view.x + view.w / 2)) <= 1 && Math.abs(frame.y + frame.h / 2 - (view.y + view.h / 2)) <= 1; const undoSteps = () => json('__gm.state.commands.undoStack.length'); - await evaluate(`localStorage.removeItem('game-maker.editor')`); + await evaluate(`localStorage.removeItem('feuerwerk.editor')`); const stepsBefore = await undoSteps(); let g = await scale(); const crateAt100 = (await json(`__gm.gameRect('o1')`)).w; @@ -364,7 +364,7 @@ export async function play(t) { check( 'the scale is saved with the project (#226), not in this browser, and is no undo step', (await json('__gm.state.project.gameScale')) === 'auto' && - (await evaluate(`localStorage.getItem('game-maker.editor')`)) === null && + (await evaluate(`localStorage.getItem('feuerwerk.editor')`)) === null && (await undoSteps()) === stepsBefore, JSON.stringify({ steps: await undoSteps(), stepsBefore }), ); diff --git a/tools/e2e/04-assets.mjs b/tools/e2e/04-assets.mjs index d01cbcfe..5a6a34e9 100644 --- a/tools/e2e/04-assets.mjs +++ b/tools/e2e/04-assets.mjs @@ -629,7 +629,7 @@ async function emptyGroupsOnSwitch(t) { all.filter((g) => g.category === c).every((g) => g.ids.length === 0), ); const seen = { - stored: await json(`JSON.parse(localStorage.getItem('game-maker.general-settings') ?? 'null')`), + stored: await json(`JSON.parse(localStorage.getItem('feuerwerk.general-settings') ?? 'null')`), emptyAlpha: await json(`__gm.assetRowAlpha('group:${empty?.type}')`), fullAlpha: await json(`__gm.assetRowAlpha('group:${full?.type}')`), categoryAlpha: emptyCategory ? await json(`__gm.assetRowAlpha('category:${emptyCategory}')`) : null, @@ -676,7 +676,7 @@ async function emptyGroupsOnSwitch(t) { check( 'switched off again, only the types with assets are listed', shape(off) === shape(before) && - (await json(`JSON.parse(localStorage.getItem('game-maker.general-settings'))`)).showEmptyAssetGroups === + (await json(`JSON.parse(localStorage.getItem('feuerwerk.general-settings'))`)).showEmptyAssetGroups === false, JSON.stringify({ before: shape(before), off: shape(off) }), ); diff --git a/tools/e2e/12-textures.mjs b/tools/e2e/12-textures.mjs index b410b92b..7fb58656 100644 --- a/tools/e2e/12-textures.mjs +++ b/tools/e2e/12-textures.mjs @@ -398,8 +398,8 @@ export async function textures(t) { // a new name renames it (the user's rule), a path is stripped to its last part, and an // upload carrying no name at all leaves the known one alone rather than erasing it. const renamed = await evaluate(`(async () => { - const bytes = await fetch('/game-maker/data/textures/${spare}').then((r) => r.blob()); - const post = (query) => fetch('/game-maker/api/index.php?r=textures' + query, + const bytes = await fetch('/feuerwerk/data/textures/${spare}').then((r) => r.blob()); + const post = (query) => fetch('/feuerwerk/api/index.php?r=textures' + query, { method: 'POST', headers: { 'Content-Type': 'image/png' }, body: bytes }).then((r) => r.json()); const evil = await post('&name=' + encodeURIComponent('../../evil/renamed.png')); const nameless = await post(''); @@ -417,7 +417,7 @@ export async function textures(t) { JSON.stringify(await json('__gm.state.notice')), ); // Bytes that are no picture are the caller's mistake: a 422 that says so, not a server error. - const refusedUpload = await evaluate(`fetch('/game-maker/api/index.php?r=textures&name=x.png', + const refusedUpload = await evaluate(`fetch('/feuerwerk/api/index.php?r=textures&name=x.png', { method: 'POST', headers: { 'Content-Type': 'image/png' }, body: 'hello' }) .then(async (r) => ({ status: r.status, error: (await r.json()).error }))`); check( diff --git a/tools/e2e/30-font.mjs b/tools/e2e/30-font.mjs index 0e248af4..d83f802c 100644 --- a/tools/e2e/30-font.mjs +++ b/tools/e2e/30-font.mjs @@ -62,7 +62,7 @@ export async function font(t) { // 30a. What the server holds, straight from the API: the two files the seed put there. // `evaluate`, not `json`: the latter stringifies the promise itself rather than what it yields. - const stored = await evaluate(`fetch('/game-maker/api/index.php?r=fonts').then((r) => r.json())`); + const stored = await evaluate(`fetch('/feuerwerk/api/index.php?r=fonts').then((r) => r.json())`); const serif = stored.find((item) => item.bytes === SERIF_BYTES); const mono = stored.find((item) => item.bytes === MONO_BYTES); check( @@ -208,7 +208,7 @@ export async function font(t) { // 30g. Dropping a font file onto the editor uploads it and makes an asset named after the file. const dropped = await evaluate(` (async () => { - const bytes = await fetch('/game-maker/data/fonts/${mono.file}').then((r) => r.arrayBuffer()); + const bytes = await fetch('/feuerwerk/data/fonts/${mono.file}').then((r) => r.arrayBuffer()); const file = new File([bytes], 'DejaVuSansMono.ttf', { type: 'font/ttf' }); const data = new DataTransfer(); data.items.add(file); diff --git a/tools/e2e/32-scope.mjs b/tools/e2e/32-scope.mjs index 3f36e07a..57253f87 100644 --- a/tools/e2e/32-scope.mjs +++ b/tools/e2e/32-scope.mjs @@ -243,7 +243,7 @@ export async function scope(t) { g.nodes.find((n) => n.id === 'sc-read').params.variablesAsset = 'sc-vars'; `); const pid = await json('__gm.state.project.id'); - const saveKey = (area) => `game-maker.saves.${pid}.${area}`; + const saveKey = (area) => `feuerwerk.saves.${pid}.${area}`; const saved = (area) => json(`JSON.parse(localStorage.getItem(${JSON.stringify(saveKey(area))}) ?? 'null')`); await t.evaluate( diff --git a/tools/e2e/34-sound.mjs b/tools/e2e/34-sound.mjs index 75f08df7..e57b5d2a 100644 --- a/tools/e2e/34-sound.mjs +++ b/tools/e2e/34-sound.mjs @@ -55,7 +55,7 @@ export async function sound(t) { await showTab('scene'); // 34a. What the server holds, straight from the API: the two files the seed put there. - const stored = await evaluate(`fetch('/game-maker/api/index.php?r=sounds').then((r) => r.json())`); + const stored = await evaluate(`fetch('/feuerwerk/api/index.php?r=sounds').then((r) => r.json())`); const blip = stored.find((item) => item.bytes === BLIP_BYTES); const drone = stored.find((item) => item.bytes === DRONE_BYTES); check( @@ -75,7 +75,7 @@ export async function sound(t) { const bytes = crypto.getRandomValues(new Uint8Array(48)); bytes.set([...'RIFF'].map((c) => c.charCodeAt(0)), 0); bytes.set([...'WAVE'].map((c) => c.charCodeAt(0)), 8); - const r = await fetch('/game-maker/api/index.php?r=sounds&name=spare.wav', { + const r = await fetch('/feuerwerk/api/index.php?r=sounds&name=spare.wav', { method: 'POST', headers: { 'Content-Type': 'audio/wav' }, body: bytes, }); return (await r.json()).file; @@ -115,7 +115,7 @@ export async function sound(t) { () => json('__gm.soundPicker()'), (p) => !p.files.includes(spare), ); - const served = await evaluate(`fetch('/game-maker/data/sounds/${spare}').then((r) => r.status)`); + const served = await evaluate(`fetch('/feuerwerk/data/sounds/${spare}').then((r) => r.status)`); check( 'the first press asks, the second deletes the file on the server', armed.deleteButton.shown === 'Delete for good' && @@ -3327,7 +3327,7 @@ export async function sound(t) { // 34h. Dropping a sound file onto the editor uploads it and makes an asset named after the file. const dropped = await evaluate(` (async () => { - const bytes = await fetch('/game-maker/data/sounds/${drone.file}').then((r) => r.arrayBuffer()); + const bytes = await fetch('/feuerwerk/data/sounds/${drone.file}').then((r) => r.arrayBuffer()); const file = new File([bytes], 'drone.wav', { type: 'audio/wav' }); const data = new DataTransfer(); data.items.add(file); diff --git a/tools/e2e/35-load-scene.mjs b/tools/e2e/35-load-scene.mjs index ac2824e4..f46d672e 100644 --- a/tools/e2e/35-load-scene.mjs +++ b/tools/e2e/35-load-scene.mjs @@ -99,7 +99,7 @@ export async function loadScene(t) { const firstName = await json('__gm.state.activeScene.name'); // A sound to have something playing across the swap; the seed put the files on the server. - const stored = await evaluate(`fetch('/game-maker/api/index.php?r=sounds').then((r) => r.json())`); + const stored = await evaluate(`fetch('/feuerwerk/api/index.php?r=sounds').then((r) => r.json())`); const file = stored[0]?.file ?? ''; // 35a. A second scene with a box of its own, a Variables asset, and the behavior that counts and diff --git a/tools/e2e/38-plugins.mjs b/tools/e2e/38-plugins.mjs index 2a6548b8..84300955 100644 --- a/tools/e2e/38-plugins.mjs +++ b/tools/e2e/38-plugins.mjs @@ -215,7 +215,7 @@ export async function plugins(t) { // The switch is the project's (#119): it stands in the project file and travels with it. The // server's own list knows nothing about it any more – it only says which files there are. const listed = await evaluate( - `fetch('/game-maker/api/index.php?r=plugins').then(async (r) => JSON.stringify(await r.json()))`, + `fetch('/feuerwerk/api/index.php?r=plugins').then(async (r) => JSON.stringify(await r.json()))`, ); const file = JSON.parse(listed).find((e) => e.file === 'demo-plugin.js'); check( diff --git a/tools/e2e/46-topbar.mjs b/tools/e2e/46-topbar.mjs index 75f3f49c..615e7169 100644 --- a/tools/e2e/46-topbar.mjs +++ b/tools/e2e/46-topbar.mjs @@ -426,7 +426,7 @@ export async function topbar(t) { const kept = { dialog: await json('__gm.generalSettingsDialog()'), ms: await json('__gm.zoomTransition()'), - stored: await json(`JSON.parse(localStorage.getItem('game-maker.general-settings') ?? 'null')`), + stored: await json(`JSON.parse(localStorage.getItem('feuerwerk.general-settings') ?? 'null')`), }; check( '"General settings…" opens on the kept transition, and Apply keeps the one typed in the browser', @@ -463,9 +463,7 @@ export async function topbar(t) { const cursorOn = { css: await json('__gm.canvasCursor()'), size: await pictureLoads() }; await switchCursor(); const cursorOff = { css: await json('__gm.canvasCursor()') }; - const storedOff = await json( - `JSON.parse(localStorage.getItem('game-maker.general-settings')).customCursor`, - ); + const storedOff = await json(`JSON.parse(localStorage.getItem('feuerwerk.general-settings')).customCursor`); await switchCursor(); const cursorBack = await json('__gm.canvasCursor()'); check( diff --git a/tools/e2e/48-pixelart.mjs b/tools/e2e/48-pixelart.mjs index 6f4e4c2c..1a35eddd 100644 --- a/tools/e2e/48-pixelart.mjs +++ b/tools/e2e/48-pixelart.mjs @@ -818,7 +818,7 @@ export async function pixelArt(t) { const files = [...new Set(stored.frames.flatMap((f) => Object.values(f.cels)))]; const statuses = JSON.parse( await evaluate( - `Promise.all(${JSON.stringify(files)}.map((f) => fetch('/game-maker/data/pixels/' + f).then((r) => r.status))).then((s) => JSON.stringify(s))`, + `Promise.all(${JSON.stringify(files)}.map((f) => fetch('/feuerwerk/data/pixels/' + f).then((r) => r.status))).then((s) => JSON.stringify(s))`, ), ); check( @@ -828,7 +828,7 @@ export async function pixelArt(t) { ); // 48m-ii. A divider stays where it was left: its place is kept in the browser, not in the project, // so the reload brings the wider panel back. Dragged back after, for the modules to come. - const keptSizes = () => json(`JSON.parse(localStorage.getItem('game-maker.panel-sizes') ?? '{}')`); + const keptSizes = () => json(`JSON.parse(localStorage.getItem('feuerwerk.panel-sizes') ?? '{}')`); const columnBefore = await json('__gm.leftColumn()'); const bars = await json('__gm.panelSplitters()'); await drag(bars.left.x, bars.left.y, bars.left.x + 40, bars.left.y, { steps: 6 }); @@ -878,7 +878,7 @@ export async function pixelArt(t) { folded: foldedHead.folded, h: foldedHead.h, merge: await control('layer:merge'), - stored: await json(`localStorage.getItem('game-maker.folded-sections')`), + stored: await json(`localStorage.getItem('feuerwerk.folded-sections')`), }; await shot('48-list-folded'); await click(foldedHead.x, foldedHead.y); diff --git a/tools/e2e/60-release.mjs b/tools/e2e/60-release.mjs index a7e74898..6154f26a 100644 --- a/tools/e2e/60-release.mjs +++ b/tools/e2e/60-release.mjs @@ -123,12 +123,12 @@ export async function release(t) { await waitUntil('__gm.release() !== null', { label: 'the release dialog opened' }); return json('__gm.release()'); }; - const player = (what) => json(`__gameMakerPlay.${what}`); + const player = (what) => json(`__feuerwerkPlay.${what}`); /** The play page as somebody without an account opens it. */ const openLink = async (link) => { await cdp.send('Network.clearBrowserCookies'); await cdp.send('Page.navigate', { url: link }); - await waitUntil('Boolean(window.__gameMakerPlay)', { timeout: 15000, label: 'the play page is up' }); + await waitUntil('Boolean(window.__feuerwerkPlay)', { timeout: 15000, label: 'the play page is up' }); }; // The step before may leave an edit on its way to the server; leaving the page then would stop at @@ -157,7 +157,7 @@ export async function release(t) { 'Publish in the release dialog makes a link to the play page, for the project as saved', before.link === null && before.state.startsWith('Not released') && - after.link === `${origin}/game-maker/play.html?game=${token}` && + after.link === `${origin}/feuerwerk/play.html?game=${token}` && after.state.endsWith('the link plays the project as saved') && after.warnings.length === 0, JSON.stringify({ before, after }), @@ -202,7 +202,7 @@ export async function release(t) { // 60c. The link, without a session: the page colour, "Click to play", and nothing of the editor. await openLink(after.link); - await waitUntil('__gameMakerPlay.screen().caption === "Click to play"', { + await waitUntil('__feuerwerkPlay.screen().caption === "Click to play"', { timeout: 15000, label: 'the game offers to start', }); @@ -236,7 +236,7 @@ export async function release(t) { HTMLAnchorElement.prototype.click = function () { window.__downloads.push({ name: this.download, href: this.href }); };`); const size = { w: await json('innerWidth'), h: await json('innerHeight') }; await click(size.w / 2, size.h / 2); - await waitUntil('__gameMakerPlay.running()', { label: 'the game runs' }); + await waitUntil('__feuerwerkPlay.running()', { label: 'the game runs' }); await sleep(300); const cornerAfterClick = await player('fullscreen()'); const screen = await player('screen()'); @@ -366,12 +366,12 @@ export async function release(t) { const releasedZoom = (await (await send('GET', `releases/${token}`, undefined, false)).json()).project .gameLinkZoom; await openLink(after.link); - await waitUntil('__gameMakerPlay.screen().caption === "Click to play"', { + await waitUntil('__feuerwerkPlay.screen().caption === "Click to play"', { timeout: 15000, label: 'the fitted game offers to start', }); await click(size.w / 2, size.h / 2); - await waitUntil('__gameMakerPlay.running()', { label: 'the fitted game runs' }); + await waitUntil('__feuerwerkPlay.running()', { label: 'the fitted game runs' }); await sleep(300); const fitted = await player('screen()'); const fit = Math.min(size.w / CAMERA.width, size.h / CAMERA.height); @@ -418,7 +418,7 @@ export async function release(t) { await clickLast('Cancel'); const dead = await send('GET', `releases/${token}`, undefined, false); await openLink(after.link); - await waitUntil('__gameMakerPlay.screen().caption !== "Loading…"', { label: 'the page gave up' }); + await waitUntil('__feuerwerkPlay.screen().caption !== "Loading…"', { label: 'the page gave up' }); check( 'Take down makes the link dead', dead.status === 404 && (await player('screen()')).caption === 'This game is not available.', diff --git a/tools/e2e/64-pattern.mjs b/tools/e2e/64-pattern.mjs index 30c5152c..ebfefd26 100644 --- a/tools/e2e/64-pattern.mjs +++ b/tools/e2e/64-pattern.mjs @@ -35,7 +35,7 @@ export async function pattern(t) { await press('Escape'); await evaluate('__gm.state.select(null)'); await showTab('scene'); - const stored = await evaluate(`fetch('/game-maker/api/index.php?r=sounds').then((r) => r.json())`); + const stored = await evaluate(`fetch('/feuerwerk/api/index.php?r=sounds').then((r) => r.json())`); const blip = stored.find((item) => item.bytes === BLIP_BYTES); await mutate(` __gm.state.project.assets.push( @@ -600,7 +600,7 @@ export async function pattern(t) { await click(unbaked.button.x, unbaked.button.y); const bakedNow = await waitFor(bakeInfo, (info) => info.status === 'baked', 100); const bake = bakedNow.bake; - const listed = (await evaluate(`fetch('/game-maker/api/index.php?r=sounds').then((r) => r.json())`)).find( + const listed = (await evaluate(`fetch('/feuerwerk/api/index.php?r=sounds').then((r) => r.json())`)).find( (item) => item.file === bake?.file, ); check( @@ -678,7 +678,7 @@ export async function pattern(t) { const removed = await bakeInfo(); const fileGone = await waitFor( async () => - (await evaluate(`fetch('/game-maker/api/index.php?r=sounds').then((r) => r.json())`)).some( + (await evaluate(`fetch('/feuerwerk/api/index.php?r=sounds').then((r) => r.json())`)).some( (item) => item.file === bake?.file, ), (there) => !there, diff --git a/tools/e2e/65-song.mjs b/tools/e2e/65-song.mjs index e2ca512e..62e8c331 100644 --- a/tools/e2e/65-song.mjs +++ b/tools/e2e/65-song.mjs @@ -79,7 +79,7 @@ export async function song(t) { await press('Escape'); await evaluate('__gm.state.select(null)'); await showTab('scene'); - const stored = await evaluate(`fetch('/game-maker/api/index.php?r=sounds').then((r) => r.json())`); + const stored = await evaluate(`fetch('/feuerwerk/api/index.php?r=sounds').then((r) => r.json())`); const blip = stored.find((item) => item.bytes === BLIP_BYTES); await mutate(` __gm.state.project.assets.push( diff --git a/tools/e2e/67-recording.mjs b/tools/e2e/67-recording.mjs index 5cdb9d26..69e1b565 100644 --- a/tools/e2e/67-recording.mjs +++ b/tools/e2e/67-recording.mjs @@ -165,7 +165,7 @@ export async function recording(t) { const video = document.createElement('video'); video.muted = true; video.onerror = () => resolve('null'); - video.src = '/game-maker/data/videos/${file}'; + video.src = '/feuerwerk/data/videos/${file}'; video.play().then(() => video.requestVideoFrameCallback(() => { video.pause(); const canvas = document.createElement('canvas'); @@ -297,7 +297,7 @@ export async function recording(t) { const video = document.createElement('video'); video.muted = true; video.onerror = () => resolve('null'); - video.src = '/game-maker/data/videos/${file}'; + video.src = '/feuerwerk/data/videos/${file}'; video.play().then(() => video.requestVideoFrameCallback(() => { video.pause(); video.onseeked = () => setTimeout(() => { @@ -386,7 +386,7 @@ export async function recording(t) { const tabHead = await firstBytes(tabDownload.href); check( "the video tab's Download saves the recording as it was recorded, not the MP4", - tabDownload.href === `/game-maker/data/videos/${file.replace(/\.mp4$/, '.source.mkv')}` && + tabDownload.href === `/feuerwerk/data/videos/${file.replace(/\.mp4$/, '.source.mkv')}` && tabDownload.name === `${take.name}.mkv` && (await json('window.__downloads')).some((d) => d.name === tabDownload.name) && tabHead === '1a45dfa3', @@ -457,7 +457,7 @@ export async function recording(t) { pickerDownload.enabled && pickerConvert.enabled && pickerSaved?.name === `${sharp.name}.mkv` && - pickerSaved.href === `/game-maker/data/videos/${kept.file.replace(/\.mp4$/, '.source.mkv')}` && + pickerSaved.href === `/feuerwerk/data/videos/${kept.file.replace(/\.mp4$/, '.source.mkv')}` && pickerHead === '1a45dfa3' && converted.detail.includes('2304 × 1440') && converted.extras[1].enabled === false && diff --git a/tools/lib/auth.mjs b/tools/lib/auth.mjs index c1db4bc4..f428dab3 100644 --- a/tools/lib/auth.mjs +++ b/tools/lib/auth.mjs @@ -16,7 +16,7 @@ export const E2E_ADMIN = 'e2e-admin'; const DATA = resolve(dirname(fileURLToPath(import.meta.url)), '../../data'); const PASSWORD_FILE = resolve(DATA, 'e2e-password.txt'); const ADMIN_PASSWORD_FILE = resolve(DATA, 'e2e-admin-password.txt'); -const COOKIE = 'game_maker_session'; +const COOKIE = 'feuerwerk_session'; /** One session per server for the whole process; logging in per request would be pointless load. */ const cookies = new Map(); @@ -48,7 +48,7 @@ export async function e2eAdminPassword() { } /** - * Signs in as anyone and answers with a fresh `game_maker_session=…`, uncached – for a step that + * Signs in as anyone and answers with a fresh `feuerwerk_session=…`, uncached – for a step that * needs a second account beside the tools' own. */ export async function logIn(baseUrl, username, password) { @@ -68,7 +68,7 @@ export async function logIn(baseUrl, username, password) { return set.split(';')[0]; } -/** `game_maker_session=…` for `Cookie` headers, logging in on first use. */ +/** `feuerwerk_session=…` for `Cookie` headers, logging in on first use. */ export async function sessionCookie(baseUrl) { const cached = cookies.get(baseUrl); if (cached) return cached; diff --git a/tools/lib/projects.mjs b/tools/lib/projects.mjs index b666c0e0..595b7994 100644 --- a/tools/lib/projects.mjs +++ b/tools/lib/projects.mjs @@ -22,7 +22,7 @@ export const PROJECTS = { actor: '3ccb5c67220b', }; -export const DEFAULT_BASE_URL = 'https://nockal.com/game-maker/'; +export const DEFAULT_BASE_URL = 'https://nockal.com/feuerwerk/'; /** * Reads `[projectId] [baseUrl]` from argv, falling back to `defaultId` and to diff --git a/tools/migrate-file-owners.php b/tools/migrate-file-owners.php index 3a8ab720..5c092258 100644 --- a/tools/migrate-file-owners.php +++ b/tools/migrate-file-owners.php @@ -13,7 +13,7 @@ declare(strict_types=1); * A file is only taken away from where it was when every document naming it could be rewritten: * one named by a document without an owner (or of an account that no longer exists) stays as well. * - * docker exec -u www-data web-php-fpm php /var/www/html/game-maker/tools/migrate-file-owners.php [--dry-run] [data dir] + * docker exec -u www-data web-php-fpm php /var/www/html/feuerwerk/tools/migrate-file-owners.php [--dry-run] [data dir] */ require __DIR__ . '/../api/lib/AtomicFile.php'; diff --git a/vite.config.ts b/vite.config.ts index 32b66e18..91f8c9c5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,11 +3,12 @@ import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { defineConfig, type Plugin } from 'vite'; import { publishPlugins } from './tools/lib/plugins.mjs'; +import { SITE_PATH } from './src/core/site'; const __dirname = dirname(fileURLToPath(import.meta.url)); /** - * The workspace folder itself is what nginx serves under https://nockal.com/game-maker/. + * The workspace folder itself is what nginx serves under https://nockal.com/feuerwerk/. * The Vite source pages live in `web/`: `index.html` is the editor, `play.html` a released game * (#266). The build writes hashed assets into `dist/` and this plugin copies the built pages to the * folder root, so that the site URLs load the production bundles while `api/` and `data/` sit next @@ -17,7 +18,7 @@ const PAGES = ['index.html', 'play.html']; function publishPages(): Plugin { return { - name: 'game-maker:publish-pages', + name: 'feuerwerk:publish-pages', apply: 'build', closeBundle() { for (const page of PAGES) copyFileSync(resolve(__dirname, 'dist', page), resolve(__dirname, page)); @@ -33,7 +34,7 @@ function publishPages(): Plugin { */ function shipPlugins(): Plugin { return { - name: 'game-maker:publish-plugins', + name: 'feuerwerk:publish-plugins', apply: 'build', closeBundle() { publishPlugins(__dirname); @@ -43,7 +44,7 @@ function shipPlugins(): Plugin { export default defineConfig({ root: 'web', - base: '/game-maker/dist/', + base: `${SITE_PATH}dist/`, publicDir: false, plugins: [publishPages(), shipPlugins()], build: { @@ -61,7 +62,7 @@ export default defineConfig({ port: 5175, // During development the PHP API is served by the production nginx; proxy it. proxy: { - '/game-maker/api': { + [`${SITE_PATH}api`]: { target: 'https://nockal.com', changeOrigin: true, }, diff --git a/web/index.html b/web/index.html index a3d7758d..7b785f29 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,7 @@ - Game Maker + Feuerwerk