From 2a1ce85981c4af33168bc94b6a863085da0b6566 Mon Sep 17 00:00:00 2001 From: meleki Date: Mon, 28 Sep 2026 06:04:49 +0200 Subject: [PATCH] Add the Structs page for struct assets (#253) New page Structs with sidebar entry. Behaviors, Architecture and Development point to it. Check total 1500. --- Architecture.md | 4 +-- Behaviors.md | 5 +-- Development.md | 2 +- Structs.md | 91 +++++++++++++++++++++++++++++++++++++++++++++++++ _Sidebar.md | 1 + 5 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 Structs.md diff --git a/Architecture.md b/Architecture.md index e3650fe..8ec5644 100644 --- a/Architecture.md +++ b/Architecture.md @@ -73,9 +73,9 @@ ``` web/index.html Vite source page (dev) index.html built page (generated, served) -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), 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/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 (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) +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) src/core/commands/ undoable commands (pixelArtCommands: one command runs any pixel art edit and keeps the state before it, #130), CommandStack, SetBagValueCommand base, RenameCommand base (undone by renaming back: variable, struct field, port), MoveCommand base (undone by moving back: tile picture, tilemap layer, vector shape), vectorArtCommands (#129: the shapes of a drawing, one gesture one command), one import command shape for texture/font/sound, require.ts (validated lookups, and what an instance may not change), naming.ts src/core/graph/ graph model, NodeRegistry/NodeContext/values (a travelling list may hold live objects, `storedList` narrows one down to what a PropertyBag may hold, #157), interpreter, dropTarget, nested (behaviors as nodes), diff --git a/Behaviors.md b/Behaviors.md index 2222bb8..ca2f36c 100644 --- a/Behaviors.md +++ b/Behaviors.md @@ -230,8 +230,9 @@ playing use the same two words. dropped on load, in a node setting or on the way through a graph. There is no depth limit either. A struct inside a struct field is **declared** (`fields` on the field, like a struct variable), while a list still declares nothing about its items, so a struct item in a list takes its shape - from its value at any depth. Named struct types that several variables share are a topic of their - own. `Pack`, `Expand` and `Item at` take nested data apart **one level at a time**: a struct in a + from its value at any depth. A struct may also follow a **struct asset** instead of declaring its + fields itself (#253): its Type row picks one, and `Pack`/`Expand` have a Type setting that makes + their ports the asset's fields — see [Structs](Structs). `Pack`, `Expand` and `Item at` take nested data apart **one level at a time**: a struct in a struct is two `Expand` nodes in a row, and there is no path syntax like `a.b[2].c`. `+ Item` offers every kind, on a list of its own and on one a struct field holds alike. A live entity is an object on a data edge too, so it carries a `Symbol` brand (`ENTITY_HANDLE`) that no struct diff --git a/Development.md b/Development.md index 6fc66f7..0b53a28 100644 --- a/Development.md +++ b/Development.md @@ -15,7 +15,7 @@ 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 # 1486 editor checks, screenshots in /tmp/gm-e2e-*.png +node tools/seed-e2e.mjs && node tools/e2e.mjs # 1500 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 diff --git a/Structs.md b/Structs.md new file mode 100644 index 0000000..b68251a --- /dev/null +++ b/Structs.md @@ -0,0 +1,91 @@ +A **struct asset** is a named shape for structs. Instead of typing the same fields into every +struct variable that holds, say, a piece of loot, you declare `Loot` once — `Title`, `Amount`, +`Rare` with their defaults — and let the variables, struct fields and `Pack`/`Expand` nodes that +hold loot **follow** it. Change `Loot`, and all of them change with it. + +A struct that declares its own fields, as every struct did before, still works exactly as it did. +A type is something you give a struct, not something it needs. + +## Making one + +**+ → Struct** creates the asset; it stands in the assets panel under *Structs*. Its inspector has +a **FIELDS** section that works like the VARIABLES section of a behavior or a Variables asset: + +- **+ Field** adds a field of any kind — a number, a text, a yes/no, a colour, a texture, an asset, + an object, a list or a struct. +- The name field renames it, the remove button at the end of the row removes it. +- The value next to it is the field's **default**: what a struct of this type starts with. +- **Open** shows the fields in a tab of their own, as it does for variables. + +A field may itself be a struct with a type. What it may not be is its own type, directly or through +another type: `Loot` cannot hold a `Loot`, and if `Chest` holds a `Loot`, `Loot` cannot hold a +`Chest`. Such a value would never end. The type picker does not offer those types. A **list** field +is fine — it starts empty. + +## Giving a struct a type + +Every struct variable, and every struct field inside one, has a **Type** row under its head. It +reads **Own fields** while the struct declares its fields itself. Its menu offers: + +- **Own fields** — the struct keeps the fields it has now and declares them itself again. +- **every struct asset** — the struct takes on that asset's fields. What its value already holds + under the same name stays, if it fits the field's kind. Every other field starts at the default + the asset declares. A field the asset does not have goes. +- **Make struct type** — turns the fields the struct holds by itself into a new struct asset, + named after the variable. The value it holds now becomes the asset's defaults, so nothing reads + differently afterwards. This is the way to turn an existing struct into a type. + +With a type, the fields are no longer edited at the variable: there is no **+ Field**, and the +field names cannot be changed or removed there. The **values** stay per variable, and a placed +object may still override them, as with any variable. + +## Pack and Expand + +`Pack` and `Expand` have a **Type** setting. With a type, their ports are the asset's fields — one +input per field on `Pack`, one output per field on `Expand` — and they follow the asset from then +on. The FIELDS section of such a node lists them without letting you change them. Without a type, +the fields are declared on the node by hand as before (**+ Field**, **From variable**). + +## When the asset changes + +Every change to a struct asset's fields reaches every place of that type at once, and **one undo** +takes all of it back: + +| Change on the asset | What happens at each place | +|---|---| +| Field added | Every value gets it, at the field's default. `Pack`/`Expand` get a port. | +| Field removed | It leaves every value. The port goes, and with it the edge on it and the value typed into it. | +| Field renamed | Every value keeps its value under the new name, overrides included. The port is renamed and keeps its edge. | +| Default changed | Nothing: a default is what a *new* struct of the type starts with. | + +This also works through several levels: if `Chest` holds a `Loot` and a variable holds a `Chest`, a +field renamed on `Loot` is renamed inside that variable's value too. + +## Deleting a struct asset + +The delete dialog counts the structs and `Pack`/`Expand` nodes that use it. After deleting, each of +them **keeps the fields it last had**, now as its own fields — no value is lost, only the link. Its +Type row reads **Own fields** again. + +## In the running game + +Nothing changes there. A struct value is the same plain bundle of fields, with or without a type: +the type does not travel with the value, so `Equals`, `Index of`, saving and loading treat it as +before. Edges do not check the type either — any struct fits any struct port. + +## What it does not do (yet) + +Lists stay **untyped**: a list of structs still takes each item's shape from the item itself, and +**+ Item → Struct** starts an empty struct. Typed lists, where every item is one struct type, are +[issue #254](https://git.nockal.com/meleki/game-maker/issues/254). + +## Under the hood + +A typed struct keeps a **copy** of the type's fields in its declaration (`fields`), next to +`structType`, the asset's id. Everything that reads a struct's fields therefore works unchanged. +`syncStructTypes` in `src/core/model/structTypes.ts` brings every copy, every value kept for it and +every typed `Pack`/`Expand` back in line; it runs after each change to a struct asset and when a +project is loaded, so a hand-edited file heals too. Its writes go through a `Recorder` +(`src/core/util/recorder.ts`), which is what makes an edit of unknown reach one undo step. A struct +asset declares its fields as `variables`, so the variable commands and the inspector's variable +rows edit them unchanged. diff --git a/_Sidebar.md b/_Sidebar.md index adc7fd2..231f64c 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -5,6 +5,7 @@ - [Scenes](Scenes) - [Scene editing](Scene-editing) - [Behaviors](Behaviors) +- [Structs](Structs) - [Colliders](Colliders) - [Physics](Physics) - [Textures](Textures)