From d0ed6bd81d546fd490cd2f8f0d12ac3832b2f6ff Mon Sep 17 00:00:00 2001 From: meleki Date: Tue, 29 Sep 2026 14:15:11 +0200 Subject: [PATCH] Describe the game link and the new zoom steps (#266) A project can now be released as a link that plays without the editor and without a login. - Concepts: a Release section, the new main menu entry and the zoom steps 300%, 500% and 600% in the game tab table. - API: the four release routes, and that the public one is the only route besides auth that needs no session. - Architecture: GameRun, the player page, gamePointer, projectFiles and WorldViewport in its new place under src/viewport. - Development: the PHP release test and the measured sum of 1574 e2e checks. Co-Authored-By: Claude Opus 5.5 --- API.md | 8 ++++++-- Architecture.md | 21 ++++++++++++++++++--- Concepts.md | 25 ++++++++++++++++++++++--- Development.md | 3 ++- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/API.md b/API.md index 80d76c6..65f46ac 100644 --- a/API.md +++ b/API.md @@ -2,7 +2,7 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT | Method | Route | Body → Result | |---|---|---| -| GET | `auth/session` | `{user, role}` — the name and role (`admin` or `user`) of whoever is signed in, both `null` when nobody is. Always 200, and the only route that needs no session | +| GET | `auth/session` | `{user, role}` — the name and role (`admin` or `user`) of whoever is signed in, both `null` when nobody is. Always 200 | | POST | `auth/login` | `{username,password}` → `{user, role}` and the session cookie; **401** `{error}` for a wrong pair | | POST | `auth/logout` | 204, and the cookie is cleared | | POST | `auth/password` | `{currentPassword,newPassword}` → 204. Changes the password of whoever is signed in — the only `auth` route that needs a session. **401** for a wrong current password, **422** when the new one is shorter than 8 characters. Every other session of the account ends; the one asking keeps its own | @@ -10,11 +10,15 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT | POST | `projects` | `{name}` → project document (201) | | GET | `projects/{id}` | `{id,name,revision,updatedAt,project}` | | PUT | `projects/{id}` | `{revision, project}` → `{revision,updatedAt}` or **409** `{error,revision}` when stale | -| DELETE | `projects/{id}` | 204, and the snapshots of the project go with it | +| DELETE | `projects/{id}` | 204, and the snapshots and the release of the project go with it | | GET | `projects/{id}/snapshots` | `[{id,name,revision,createdAt}]`, newest first — see [Concepts](Concepts) | | POST | `projects/{id}/snapshots` | `{name?}` → `{id,name,revision,createdAt}` (201). Copies the project as stored; a blank name becomes the UTC date and time. **422** over 120 characters | | DELETE | `projects/{id}/snapshots/{sid}` | 204 | | POST | `projects/{id}/snapshots/{sid}/restore` | `{revision}` → `{revision,updatedAt,before}`. Keeps the stored project as the snapshot `before` (`Before restoring `), then saves the snapshot as the next revision. **409** `{error,revision}` when `revision` is stale | +| GET | `projects/{id}/release` | `{token,revision,publishedAt}`, or `null` while the project is not released — see [Concepts](Concepts) | +| POST | `projects/{id}/release` | → `{token,revision,publishedAt}`. Copies the project as stored; publishing again replaces the copy and keeps the token | +| DELETE | `projects/{id}/release` | 204, the link is dead; **404** when the project was not released | +| GET | `releases/{token}` | `{name,publishedAt,project}` — the released copy, **without a session**: the one route besides `auth/*` open to everybody. `project.id` is the token, not the project's id. **404** for an unknown or taken-down token | | GET | `textures` | `[{file,name,width,height,bytes,updatedAt}]` — what the image picker offers | | POST | `textures&name=hero.png` | **raw image bytes** → `{file,name,width,height,bytes}` (201) | | DELETE | `textures/{file}` | 204 | diff --git a/Architecture.md b/Architecture.md index bb1f11d..00b5fa2 100644 --- a/Architecture.md +++ b/Architecture.md @@ -73,6 +73,8 @@ ``` web/index.html Vite source page (dev) index.html built page (generated, served) +web/play.html a released game (#266) play.html built page (generated, served) +src/main.ts, play.ts the two entries; page.ts is the canvas both are (the WebGL context, root, resize) src/core/model/ Project (object holders), Asset + AssetTypes, SceneObject, Properties (value types), hierarchy, walk (generic parent/child walks), instance (container content resolved from its template), usages (where an asset is used), container (the cycle guard), placement (what may be dropped where), texture (frames, the slice grid, a frame's collision outlines), frameOutline (#193: a frame's outlines as a collider wears them, centred, a pixel to a unit, cut into pieces once per drawn list), traceCollision (#161: a frame's outlines read off its picture — the pixels from an alpha threshold on are solid, each 4-connected island is walked round along the pixel edges and cut down to a few corners without ever crossing itself; crumbs below 1 % of the frame drop out), generated (pictures that are drawn rather than uploaded, #185: the key `gen:@`, the register of readers that tell the picture path an asset's canvas size and revision, and the `fingerprint` pixel art and vector art make their revision with), vectorart (#129: the canvas as a property, which ring is worth drawing, and the revision as a fingerprint of the drawing rather than a counter, a texture fill's picture included) + vectorPath (what a shape draws, as plain commands: closed rings fill, every ring strokes, a segment is a line while neither of its ends carries a handle; a paint resolved to sorted stops or the picture it names) + vectorPaint (#186: gradients and texture fills — where a paint lies when it is switched on, the colour between two stops, adding, moving and removing a stop, a paint moved with its shape, and the rule that a texture fill takes uploaded pictures only) + vectorBoolean (#187: union, subtract, intersect and exclude on the sampled outlines, each shape read by the even-odd rule; the result rings in the order Pixi's hole detection needs, every outline followed by its holes and preceded by the islands inside them) + vectorCurves (#208: every sampled point remembers its cubic and its `t`, so a run of result points along one cubic becomes that cubic again, cut with de Casteljau), pixelart (#130: the palette a picture is painted in, how the layers of a frame come together, the sheet of all frames, a frame id read as a cut of it, and the revision) + pixelCel (the file of one layer on one frame, and its SHA-256 name worked out in the browser) + pixelDraw (what the tools do to the indices: brush, line, rectangle, ellipse, fill, resize about an anchor, merge, a palette colour taken out) + pixelEdit (every edit of layers, frames and cels as a function from one state to the next), polygon (an outline checked, cut into convex pieces and clipped to a rectangle), tile (the three kinds and their block shapes, reading a tile's pictures, the animation step), autotile (which quarter tile a neighbourhood asks for, the 47/16 tiles a block produces, 1 for a plain tile), tilemap (the cells of a layer, which neighbours a cell is joined to, the cells a stroke or a rectangle covers), tilemapCollision (the solid cells of a map, drawn in or out by their tile's collision scale and merged into rectangles, one group per one-way angle and per surface (`tileMaterials`, #193), the outlined cells as polygons — an autotile's outlines cut into its quarters and pieced together per cell like its picture —, where one sits in the world, and which cells of a placed map a world rectangle can reach, in cells (`cellsWithin`, #184)), tilemapMemo (what a map's cells work out to, kept while they hold the same: a version per cell map compared cell by cell, since strokes paint in place), animation (which frame at time t, a row's value there, what a row may aim at, the players on an object; KeyRef and its lookups) + animationPreview (the root at a frame, written into copies), curve (#247: a drawn easing evaluated at t — each segment a cubic solved for t by bisection, held to its rules by `sanitizedCurve`: ends at 0 and 1, no point overtaking another, no handle past its neighbour — and which keys and `Lerp` nodes use one) + curvePresets (the built-in eases traced as curves, a segment halved only where it strays), graphLookup (what a graph sees of the project: behaviors to run, state machines to read, curves to ease along), structTypes (#253: Struct assets as types — every typed struct variable, struct field and `Pack`/`Expand` found and brought in line with its asset, a renamed field carried through every value first, the cycle rule), Scope (how long a shared value lives: per instance, one for the run, or one per scene and rebuilt with every scene that loads, #164 — for a state machine's current state and for the values of a Variables asset), collisionLayers (which colliders may meet: the project's named layers, a collider's layer and mask, and the one rule `layersMeet` the engine, Solids and the event pair search all answer by) src/core/properties/ PropertySchema, kinds (one row per value kind), listItems (an item's kind, read off its value), resolve (the only override merge), deviation (what is gold), collider/camera/bounds geometry, clip (the region clip areas cut each object to, a convex polygon, #260) (with the side a one-way collider blocks from, and the rectangle around a shape a broad phase looks at first, #184), convex (capsules and polygons: rounded convex pieces against each other), ray (where a ray meets a shape, in double precision), rigidbody (what the engine reads off a rigidbody, a collider's material and the scene), aspect, enabled, editorRun, assetRef/textureRef (how a reference reads), declaredRows (the row a Property node declares in a shader or particle graph, and the params that node has in both) src/core/util/ list (take out, put back and move an item — what a command's undo is made of), vec (dot, the closest point on a segment — shared by the collider maths, the outline hit area and the polygon grips), color (`mixColor`, channel by channel — keys of an animation, stops of a gradient, the particles' Mix colour), recorder (#253: assignments taken back last first — the undo of an edit whose reach is not known in advance) @@ -92,11 +94,13 @@ src/core/store/ serialize (untrusted JSON → Project), projectPlugins (w file before it is deserialized, #119), tilemapCells (the compact form of a layer's cells in the file), http (one API call), ProjectStore/HttpProjectStore (load takes a `prepare` that sees the raw JSON first), SnapshotStore/HttpSnapshotStore (the snapshots of a project, #231), + ReleaseStore/HttpReleaseStore (a project's released copy and the link to it, #266), AuthStore/HttpAuthStore (signing in and out, #177; changing the password, #182), FileStore (what the server holds of one kind) + HttpFileStore (one generic client over the three file collections) — TextureStore, FontStore and SoundStore are its type parameters, nothing else src/core/time.ts the global clock: one scaled step a frame, shared by the world, the particles, the shaders and the animation preview -src/runtime/ World, Entity, System interface, Input tracker, CollisionSystem (the editor run's detection), +src/runtime/ GameRun (one run: start, tick, `Load scene` — shared by Play and a released game, #266), + World, Entity, System interface, Input tracker, CollisionSystem (the editor run's detection), solids (what `Move` collides with and how it slides — one-way solids let it through unless it lands on them —, the first solid a ray meets, which body a collider belongs to, the CollisionSource both collision systems give the behaviors, the contact of a staying pair, @@ -116,7 +120,11 @@ src/ui/ widget toolkit (generic, knows nothing about assets); fon Source Sans 3 in 400, 500, 600 and 700, Source Code Pro 400, Latin only, #202/#214), icons + Icon (the menu's tick boxes and current-entry dot as SVG files, and a widget that shows one file on its own: a symbol the Latin subset lacks is never written as a character, #214); SortableList + sortable (the layer list of every editor that has one: rows dragged into order, eye, duplicate, cross, rename in place; the index arithmetic of a list shown upside down, kept apart so it is measured without a canvas) -src/viewport/ view (transforms), items (what to draw), symbols (how to draw it), +src/viewport/ WorldViewport (the drawing of a world or a holder, the base of every view; it draws from + a `ViewSource` — the project and word of a change — so a released game draws without the + editor, #266), gamePointer (the mouse handed to a running world), projectFiles (the file + libraries pointed at the server, and what a project names fetched ahead), + view (transforms), items (what to draw), symbols (how to draw it), symbolIcons (the SVG files in icons/, parsed once, in the theme's colours, #175), FileLibrary (load once, never retry a failure, tell the listeners) ← TextureLibrary, FontLibrary; generatedPictures (the drawn half of the picture path, #185: one picture per asset kept @@ -140,6 +148,7 @@ src/editor/ App → Editor (layout + ticker), PlaySession (the run), OpenProjectDialog (the project picker behind "Open project…"), SnapshotsDialog + snapshots (the list behind "Snapshots…" and the rule of when a snapshot may be made: only with everything saved, #231), + ReleaseDialog + releases (the link behind "Release…", what the dialog says and warns about, #266), ProjectSettingsDialog + settingsChanges (name, the start-scene switch and the collision layers in one dialog; the pure rule of what Apply has to carry out), EditorRun (run in editor), state, actions, shortcuts, clipboard (#240: copy and paste of @@ -179,8 +188,12 @@ src/editor/users/ the user management (#179): ManagementPane (the tab behin server's own sentence. The server decides everything: `api/lib/AccountRules.php` (not the own account, not the last administrator, not an account with projects), `Auth::requireAdmin()` (403), `Store` with `seesAll` for an administrator -src/editor/viewport/ WorldViewport base → FreeViewport (a camera of its own: fit, pan, wheel zoom) → SceneViewport, PreviewCanvas; +src/player/ GameScreen (the game on the page: Auto zoom, the caption before and after a run), + Player (loads, starts on a click, ticks, stops), playerHook (`window.__gameMakerPlay` + for the check scripts) — a released game (#266), without the editor +src/editor/viewport/ FreeViewport (a camera of its own: fit, pan, wheel zoom) → SceneViewport, PreviewCanvas; GameViewport follows the game's camera instead — editing tools, hence not in src/viewport/. + All of them extend `src/viewport/WorldViewport`. A pan only moves the world and redraws the screen-space overlays (`moveCamera`); a full `redraw()` follows only when an object crosses the edge of the view. The scene view's tools keep their own state and reach the view through `SceneViewHost`: @@ -258,6 +271,8 @@ api/ PHP backend: index.php (routing and the sign-in gate), lib/{Store,TextureStore,FontStore,SoundStore,PluginStore,OriginalName,Response}.php, lib/{Auth,UserStore,SessionStore}.php (#177: accounts in data/users.json, sessions in data/sessions/) lib/SnapshotStore.php (#231: data/snapshots/{projectId}/, one copy of the project per file) + lib/ReleaseStore.php (#266: data/releases/{token}.json and which token a project has, both 0640 + so the web server does not hand them out as files) data/projects/ persisted projects, data/textures/ + data/fonts/ + data/sounds/ content-addressed uploads, data/plugins/ editor plugins, filled from plugins/ by the build and the seeder; which of them run is written in each project (#119) diff --git a/Concepts.md b/Concepts.md index dff1f7e..88be35e 100644 --- a/Concepts.md +++ b/Concepts.md @@ -58,8 +58,8 @@ picture (its `width` x `height`) is drawn: | Choice | Screen pixels per camera pixel | |---|---| -| **100%** (default) | 1. 25%, 50%, 200% and 400% likewise draw exactly that factor. | -| **Auto** | The largest of those five steps at which the whole picture still fits. Below 25% it stays at 25%. | +| **100%** (default) | 1. 25%, 50%, 200%, 300%, 400%, 500% and 600% likewise draw exactly that factor. | +| **Auto** | The largest of those eight steps at which the whole picture still fits. Below 25% it stays at 25%. | | **Fit** | Whatever fills the view on its tighter side, the way the tab always drew before. | The camera's `zoom` magnifies the world on top: a 640x360 camera at zoom 2 shows 320x180 world @@ -102,8 +102,27 @@ sounds and pixel art are stored by their content and never removed, so everythin still there. Deleting the project deletes its snapshots. Whoever can open a project can use its snapshots, an administrator included. +**Release** (#266). *Menu → Release…* turns the project into a link that plays only the game: +no editor, no sign-in, the game view over the whole browser window on the page colour. **Publish** +copies the project as the server has it stored — the autosave is written first, and nothing is +published while anything is unsaved, the same rule as for a snapshot. The copy is frozen: what is +changed in the editor afterwards reaches players only with **Update release**, which replaces the +copy and keeps the link. The dialog says when the copy was taken and whether the saved project has +moved on since, and shows the link with **Copy link** and **Open**. **Take down** asks, then makes +the link dead; publishing again afterwards hands out a new one. Deleting the project takes its +release down. The link is `play.html?game=`, the token 128 random bits and the only key. + +The game on the link always starts in the start scene (the *Active scene* switch is the editor's +alone), zoomed like the game view's **Auto**: the largest zoom step at which the whole camera fits, +never a scale between the steps. It waits for a click or Enter before it starts — browsers play no +sound before one, and the keyboard would have nowhere to go. Every key belongs to the game, Tab +included. A game that stops itself (`Stop game`) offers to play again; `Restart game` and +`Load scene` work as in the editor. Persistent variables are saved in the player's browser under +the link, apart from the editor's own saves. Plugins are not loaded on the link; the dialog warns +when the project uses any, and when it has no scene to play. + **Toolbar and panels.** The toolbar carries five **icon buttons** and no captions (#183): the -**menu** (three bars) on the left with *New project*, *Open project…*, *Snapshots…* and *Project settings…*, +**menu** (three bars) on the left with *New project*, *Open project…*, *Snapshots…*, *Release…* and *Project settings…*, **undo** and **redo** beside it, and **play** and **pause** in the middle — on the middle of the window, not of the space the row has left over — where play turns into a stop square while a run is on, and pause into a second play sign while it is held. On the right stand the diff --git a/Development.md b/Development.md index aa157b3..bdf521e 100644 --- a/Development.md +++ b/Development.md @@ -15,7 +15,8 @@ npm run build # writes dist/ and regenerates index.html php tools/make-user.php [admin|user] # an account; every tool signs in (see Accounts) php tests/php/accounts.test.php # the API's account rules, no server needed (CI's gate job) php tests/php/snapshots.test.php # the snapshot store (#231), the same way -node tools/seed-e2e.mjs && node tools/e2e.mjs # 1562 editor checks, screenshots in /tmp/gm-e2e-*.png +php tests/php/releases.test.php # the release store (#266), the same way +node tools/seed-e2e.mjs && node tools/e2e.mjs # 1574 editor checks, screenshots in /tmp/gm-e2e-*.png GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step ./e2e.sh start [GM_STEPS] # the same run detached, see below ./e2e.sh status # live progress, or the last run's result