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
[](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.

-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