47 Commits
Author SHA1 Message Date
sanspie 01fdeef7a8 Aego 0.9.0-beta.1
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 14m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 16m23s
ci / release (push) Failing after 14m1s
2026-10-20 23:57:45 +03:00
sanspie 71354f6a60 install scripts: fall back to nightly, explain when nothing is published 2026-10-20 23:55:52 +03:00
sanspie 8ae6832503 edge sync: keep the destination when resuming downloads 2026-10-20 20:11:47 +03:00
sanspie 183d75ed50 gendocs: nav golden without the editor-only tour 2026-10-20 20:07:22 +03:00
sanspie 19edcbf9b2 render3d in the web sandbox without the OBJ importer; 3D try scenes select a shape 2026-10-20 19:49:47 +03:00
sanspie 8ed77a7638 Docs: no favicon request 2026-10-20 19:48:24 +03:00
sanspie ca3c9bf2e7 Docs: 3D Try it scenes, render3d in the sandbox, smarter code language detection 2026-10-20 19:43:38 +03:00
sanspie db59f117a4 deploy: short cache for the root page; edge notes in D303 2026-10-20 19:35:50 +03:00
sanspie 0d2c20da26 Docs: revision in the header, content-versioned assets; deploy passes the commit to the image 2026-10-20 19:31:14 +03:00
sanspie dd41ff15a5 deploy: edge serves the site and wasm from its own copy 2026-10-20 19:12:22 +03:00
sanspie 599e09a35d Docs: Try it on Physics 2D guide pages 2026-10-20 18:26:35 +03:00
sanspie 60614938be Release: Windows installer, install.ps1, component page layout 2026-10-20 18:20:18 +03:00
sanspie a60ca47d31 wip release windows 2026-10-20 18:13:57 +03:00
sanspie 00ae2418a0 Docs: more editor screenshots for the editor, prefab, tags, input, time and physics guides 2026-10-20 17:57:58 +03:00
sanspie 8fc9b4c287 Docs: highlighting for every code block, editor-only tour off the site, page icons, prefetch, field docs for physics, wider Try it, demo camera 2026-10-20 17:50:21 +03:00
sanspie b29934b51d deploy: short HTML cache the edge proxy can store 2026-10-20 17:29:55 +03:00
sanspie c475b42e5d gendocs: Script API index links to type pages; nav golden 2026-10-20 17:01:11 +03:00
sanspie b103816400 websmoke: Try smoke checks the scene tree 2026-10-20 17:00:46 +03:00
sanspie 487170dd9c depcheck: tween in the web sandbox 2026-10-20 16:46:52 +03:00
sanspie 57579356d6 Docs: streamed compile with tee'd progress, tween in the sandbox, console runs step module systems 2026-10-20 16:46:03 +03:00
sanspie d5377410e4 Docs: HTTP cache for the docs runtime, autostart after a first start, cheaper warm loads 2026-10-20 16:29:16 +03:00
sanspie f6c83a6e0c Docs: compile-checked examples for modules outside the sandbox, HasScript fix 2026-10-20 16:03:58 +03:00
sanspie 8d7712d671 Docs: runnable Script API module examples and Run it blocks 2026-10-20 16:03:26 +03:00
sanspie 63980faccb Docs: diagnostics with checked examples 2026-10-20 16:00:13 +03:00
sanspie 0041e7e639 Docs: loader streams compile from the original response 2026-10-20 15:59:50 +03:00
sanspie e334be569f Architecture: D301–D303, amendments for the docs overhaul 2026-10-20 15:49:46 +03:00
sanspie b488958444 Docs panel goldens with short-name links 2026-10-20 15:48:31 +03:00
sanspie 5875a65193 Docs: Try it for every 2D physics component, script examples on component pages, Script API index, smooth wheel scrolling on the web 2026-10-20 15:46:33 +03:00
sanspie 6ab66cb8ae Docs: scene tree, loading progress and cache, release builds, install page, editor screenshots 2026-10-20 15:36:03 +03:00
sanspie f8fe54e9c5 Docs: short-name cross references in the site and the editor docs panel, links from code 2026-10-20 15:17:10 +03:00
sanspie e4326e7e7c Docs: editor icons, editor highlighter, redesigned site and navigation 2026-10-20 15:14:55 +03:00
sanspie 6a22396467 Stage 9 complete
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 22m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 23m5s
2026-10-20 14:06:22 +03:00
sanspie 21e03128a8 Stage 9.6d2: MCP server and external edits in the editor (D297, D300) 2026-10-20 13:56:45 +03:00
sanspie e5b1964f3e Stage 9.6d1: agent layer and CLI (D299) 2026-10-20 13:48:01 +03:00
sanspie cffa66514b Stage 9.6c2: prefab apply and data editing without the editor (D298)
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 21m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 22m8s
2026-10-20 13:05:10 +03:00
sanspie 2744cff481 Stage 9.6c1: project refs and refactorings without the editor (D298)
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 23m31s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 21m57s
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
2026-10-20 12:12:18 +03:00
sanspie 91052a54db make lint: staticcheck the editor build
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 16m26s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m48s
2026-10-20 12:06:56 +03:00
sanspie 7f4d87ef2c Stage 9: gameplay toolkit, export and web, agent tooling 2026-10-20 12:06:56 +03:00
sanspie 6cc0673082 CI on master, docs auto-deploy and repo link, macOS build fixes
ci / checks (push) Successful in 13m36s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 14m40s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
2026-10-16 12:10:43 +03:00
sanspie ab9b003457 Stage 8: physics and module boundaries 2026-10-15 19:16:09 +03:00
sanspie 41d949717d deploy: push-to-deploy docs site and playground
make site-headless builds the CLI without cgo (aego_nonative) and the
static site with the wasm playground. deploy/ holds the image, the
container nginx config (gzip_static with Vary), compose and deploy.sh
run by the post-receive hook on mainserv.

.gitignore ignores only the root site/ output; tools/gendocs/site was
never committed because of it.

Docs site: index and section summaries render code, bold and italic
instead of raw markdown; search.json gets the plain text. Page summaries
drop a leading list or quote marker. The nav toggle is three CSS bars
centred in a 32px button.

Playground: a loading overlay in the editor pane shows a spinner, a
progress bar and the downloaded size while aego.wasm streams in, then
'Starting engine' until the runtime is ready; a failed start keeps the
error on the overlay. docstool writes the wasm size and a version hash
into index.html; play.js, play.css, wasm_exec.js and aego.wasm are
referenced with ?v=<hash> so a deploy never mixes cached old files with
new ones.

Instruction budget: default 1M per callback (was 100k), playground 2M;
the donut needs ~120k per frame. Playground: software 3D donut example,
multi-line log entries render as monospace blocks, a fault is reported
once instead of also as a "script stopped" log line.

The compose project is named aego explicitly; the default name came from
the deploy/ directory and collided with other stacks on the host.
2026-10-14 22:40:20 +03:00
sanspie 69d19e501f Stage 7: documentation, help, playground; templates, aego new/bootstrap/icons, launcher
Documentation: engine/docs assembles one registry from the script
bindings, the schema, module docs and module-owned script paths; problems
are reported as warnings at build time. Module registries record doc
sources and script module owners; diag.Codes lists every diagnostic code.

Project bootstrap: project.defaultSpace ("2d"/"3d"), EnsureDirs creates
the default folders when the editor opens a project (reported in the
console), prefabs/ is a pipeline root. Native builds drop -trimpath so
binding locations resolve inside native/.

Editor files: create scene/prefab/script from menus, move/rename/duplicate
/delete with undo, script package retargeting on move, open scenes and
script panels follow renames, Save As / Rename Scene prompts, scene
picker, startup scene. Scoped shortcuts fall back to the global command
when disabled; Save As is Ctrl+Shift+S, Save All Ctrl+Alt+S.

Script editor: Go to Implementation (stub → native/ source), host
definitions always resolve to stubs, bracket match/mismatch states,
rainbow brackets (persisted), mono code font (ui-mono atlases, per-command
fonts in draw.List, PushFont/PopFont restored by Mark/Restore).

Tests: langsvc definition/implementation, bracket states, dispatch
fallback, script renames, font stack, editor file-op integration, golden
mono glyph and bracket references; depcheck rule for engine/docs.

Docs pipeline: engine, render2d and script/engine ship their own res/docs;
keyword hover text comes from the registry through manifest.Keywords;
project script symbols join the registry; aego docs export|check|build|
serve, --tool docs in native binaries, aego check --explain; app/docstool
and the tools/gendocs module (goldmark) build the static site.

Playground: app/playground + cmd/aego-playground (js/wasm, 3.3 MB gzip)
compile, highlight and run scripts in the browser; run blocks in the
language docs become examples. engine/builtin/headless is the module set
without render3d; workspace.Config.FS; StubFor/StubPaths; backends and the
script importer drop out of js builds.

VM: one frame pointer in the interpreter loop, direct host calls, panic
recovery once per run instead of per host call (Update2k -19%,
Schedule10k -24% against the previous head). bench-build/compare/profile
run pre-built, pinned, interleaved binaries.

Editor: structural bracket levels and current-block highlight, indent
guides, occurrence highlighting with write marks, multi-format clipboard
(copy/cut/paste assets, paste images as sprites or textures). gl33 probes
a renderable depth format and reports framebuffer failures by stage.

7 E:
- project templates empty/2d/2.5d/3d, app/projects as the single creation path (D148)
- launcher and recent projects (D149)
- orthographic Camera3D and Scene View (D150)
- reference states in the Inspector, ids.Short (D151)
- sceneio remaps node references when loading without KeepIDs (D152)
- exported ComponentRef<T> (D153)
- aego icons via module.Audit (D146)
2026-10-13 01:08:56 +03:00
sanspie cf68c71f05 Stage 6: coroutines, signals, ScriptRef, workspace CLI, editor tooling
Language: yield/signal/coroutine become keywords, start is contextual.
Coroutine methods (func Name() coroutine) with start and
yield / wait / frames / fixed / await; signals with Emit/Connect and
Connection handles; ScriptRef<T> and node.GetScript<T>() for fields,
plain methods and signals of other scripts. Diagnostics AES2050-AES2059.

Bytecode v3: ScriptRefs, XFields, XMethods and Connects tables resolved by
name in vm.Link, per-script signals, coroutine flag on functions; new
opcodes START/YIELD/EMIT/CONNECT/DISCONNECT/CONNECTED/COSTOP/CORUN/
SELFREF/GETSCRIPT/GETXF/SETXF/CALLR, checked by verify.

Runtime: script/coroutine scheduler (frames/fixed counters, time heap,
await lists; only due coroutines are visited) and script/signal registry
(snapshot per Emit, deferred removal, no allocations in steady state).
Hot reload cancels coroutines of changed scripts, rebinds connections by
name and signature, then runs Reloaded.

Tooling: script/build removed, aego check/test/dump run on the workspace
(test builds include _test.aes); langsvc completion, hover, signatures and
semantic tokens for the new syntax; editor highlighting, snippets with
placeholders, coroutine/signal profiler graphs and stats.

Docs (script.md sections 16-19, editor.md, ARCHITECTURE), test fixtures and
golden files, platformer enemies wired to the player, scriptreload demo,
and run.Config.FrameTime for deterministic headless fixed steps.

Modules (6.5): engine/module registry and ComponentBuilder are the one
registration path for engine subsystems and native code; render2d,
render3d and script/engine are modules over scene/frame, the script
component moved to scene/scriptcomp, and scene/components and
scene/systems are gone. Editor inspector, templates, picking shapes and
extension hooks are driven by descriptors and editor/builtins; sdk gains
Component/EditorHook; depcheck checks the module graph and the
aego_runtime graph.

Also: stop tracking built tool binaries; gl33 activates the scratch unit
before binding for texture updates.

Verification: typed component method adapters (Method0..2, Proc0..2)
with direct field access and no allocations; app.Build as the single
construction path for run and the editor; modules attach per scene
document; depcheck aego_editor pass; diagnostic codes in one place and
every code covered by a test; indexed coroutine heaps with eager
cancel; dead component references and nil intrinsics fault with
AES6004, host faults keep their code; GC roots of struct fields checked
in verify and vm.Link; 255-field limit; AES4010 for script packages
shadowing host modules; architecture registry D111-D147.

Golden: the harness renders on the thread that owns the GL context,
ReadPixels returns rows in memory order (render targets are top-down per
D17), and the first rendering references are committed.
2026-10-12 14:22:08 +03:00
sanspie 08f0b15e1e docs: update architecture spec and script reference 2026-10-08 13:31:21 +03:00
sanspie 17cb5e4291 Stage 2-5: assets, scene/ECS, editor, 3D, and Aego Script
Stage 2: render pipeline and UI toolkit over gpu/platform.
Stage 3: schema registry, asset pipeline + runtime loader, scene graph and ECS.
Stage 4: editor — docking, inspector, scene/game views, play mode, asset browser, console, profiler; 4.5 adds the 3D foundation (mixed 2D/3D spaces, meshes, depth).
Stage 5: Aego Script — compiler (lexer/parser/types/resolver/checker/asm/compiler), bytecode with verifier and register VM, instance runtime, stdlib, native bindings, and editor/CLI integration (aego run/edit/check/test/dump).
2026-10-08 13:17:28 +03:00
sanspie eb9ac1e589 Initial commit 2026-09-26 16:32:40 +03:00
sanspie dae4bc19e1 Stage 1: engine core, platform, gpu, vfs
Foundation for the aego engine.

core/    errs, log, handle arena, mathx, clock, stats
platform SDL3 backend plus a headless one, input state, events
gpu      device abstraction with gl33 and null backends
vfs      scheme-based virtual filesystem with overlay mounts
engine   builder, module registry, phase-ordered system loop, jobs
demos    basic, resize, input, headless; sandbox driver

OpenGL bindings are generated into gpu/gl33/gl by tools/gen-gl.sh,
so the module has no external dependencies.
2026-09-18 20:51:54 +03:00
13 changed files with 34 additions and 500 deletions
-1
View File
@@ -142,7 +142,6 @@ jobs:
- name: publish
env:
REQUIRE_WINDOWS: "1"
GITEA_RESOLVE: git.akarpov.ru:443:79.142.92.250
run: |
case "$GITHUB_REF" in
refs/tags/v*) sh tools/release/release.sh "$GITHUB_REF_NAME" ;;
+2 -2
View File
@@ -519,10 +519,10 @@ The Stage 8 specification numbered these decisions D153, D155, D158, D159, D168
| D298 | Project refactorings without the editor. `project/refs` finds what references an asset from the pipeline index (`Entry.Deps`) and `mainScene`, locates the places in a file from its text (node path, owner, field, list index), lists missing assets (with the graveyard entry and a same-name suggestion of the same kind) and node references to nodes a scene does not define; it reads through `refs.Source`, which `*pipeline.Pipeline` satisfies. An ID counts as known when it is a builtin, has a file, or is in the index without a grave, so the page artifact of a multi-file script package is not reported as missing. `project/refactor` plans and applies: `PlanReplace` collects the referring files, `Edits` computes their new bytes by replacing the `asset("<id>")` token (overrides, data assets, tables and presets included, which is why it is not a `mutate.Tx`), `Write` backs the originals up under `.aego/recovery/refs-<time>` and writes atomically, `ApplyProject` moves `mainScene`, `Reimport` reimports what changed; `Users` lists referrers outside a deleted set (safe delete), `Delete` deletes through the pipeline, whose scan buries the IDs in the graveyard; `Move` moves an asset with its `.meta` and rewrites the `package` line of a script that changes package; `PlanRename`, `RenameOps` (with the temporary-name pass when names swap) and `ApplyOps` (rolled back on failure) carry batch rename. The rename rules live in `project/rename`. `project.WriteAtomic` is the one atomic writer (`project.Save` and `document.WriteAtomic` use it; files are written 0644). The editor's References, Missing References, Batch Rename and asset move are adapters: open documents are still edited through their history, files on disk go through `project/refactor`. Prefab Apply and Create From Node follow in 9.6c2. Amended in 9.6c2: prefab Apply and Create From Node work without the editor. `refactor.PlanApply` locates the instance (`Locate`), splits its overrides with `prefab.Pick`, bakes the selection into the prefab models (the baker moved from the editor; `Nearest` routes an apply to the nearest nested prefab) and returns the new models, the instance block before and after; `ApplyPlan.Edits` encodes the changed prefab files. `ApplyPrefab` sets the baked models into the document's library and replaces the instance root and every other prefab instance of the document through `mutate`, so the other instances pick up the new values and keep their overrides; `Result.Inverse` carries the blocks taken before the apply and is valid against the old prefab files. `WriteAll` writes the files and rolls back on failure. `mutate.Document.Bytes` saves through `Env.Library`, so overrides on instances are written. `CheckCreate`, `LinkBlock` and `CreatePrefab` turn a node into a prefab file (written, scanned, imported) and link the node to it keeping every node ID; a failure removes the file and its `.meta`. `PrefabLibrary` builds a library from the prefab sources, so tools need no loader. The editor's Apply and Create Prefab call the same functions and keep their history across documents. The data asset and table editors (`editor/core/dataedit`) edit a `mutate.DataDocument`: every change is a `data-set`, `data-reset`, `data-base` (new: the base asset, by ID or path), `row-add`, `row-remove`, `row-restore` or `row-rename` with exact inverses, consecutive edits of one field merge, a value equal to the inherited one (the base for a data asset, the type default for a table row) resets the field, and saving writes the document's own bytes, so untouched fields keep their text. Gizmo property drags go through `commands.SetField` after `props.Normalize` clamps them to the field range; `commands.SetProperty` is gone. |
| D299 | Agent layer and CLI. `app/agent` is one catalog of commands (`Command`: name `group.verb`, typed arguments with positional order, write flag, `Run(*Session, Args) (*Output, error)`) that the CLI and the MCP server share; a command's MCP tool name and JSON schema come from the same definition. A `Session` loads types, modules, importers and the pipeline like `app.Build` without the engine, renderer or loader, publishes the compiled script types, builds the prefab library from sources (`refactor.PrefabLibrary`) and resolves scenes and prefabs from files; it edits through `scene/mutate` and `project/refactor` and writes atomically, then reimports and reloads the library. Commands: `scene tree|get|set|reset|add-node|remove-node|move|rename|add-comp|remove-comp|apply|validate|new|run`, `prefab create|instantiate|apply|revert|unpack|overrides`, `data get|set|reset|base|rows|add-row|remove-row|rename-row|import|export`, `refs find|missing|fix`, `asset list|move|rename|delete`, `schema list|show`, `project validate`. The CLI form is `aego <group> <verb> <positional> [--flags]`; flags may follow positionals, `--` ends them, a value starting with one dash is positional. `--json` prints `Output` (ok, dryRun, changes, problems, created, files, notes, value) or `{"ok":false,"error":…}`; exit 0, 1 on error, 2 on error-level problems or, with `--strict`, any problem. `--dry-run` applies and rolls back in memory. A project with native code runs the commands in its own binary (`--tool agent`), so its types are known. When another process holds the asset index (`Pipeline.Shared`), commands that create assets refuse, so an asset never gets a derived ID the editor would later replace. `scene run` releases the session's pipeline and runs a scene headless in process through `run.AgentRunner` (no window, no player profile, warnings and errors of the log, the world hash of `run.WorldHash`). Module `OnAdd` hooks run without the editor's visual bounds. `prefab-instantiate` creates an owner instance block (as the editor does), not the runtime spawn copy. Closes D242. |
| D300 | MCP server. `aego mcp` (`app/agent/mcp`) speaks MCP over stdio, one JSON-RPC message per line, without a dependency: `initialize` (protocol 2025-06-18, older 2025-03-26 and 2024-11-05 echoed), `ping`, `tools/list|call`, `resources/list|read|subscribe|unsubscribe`, `resources/templates/list`, `notifications/resources/updated|list_changed`. Tools are the D299 commands one to one (name with underscores, the command's JSON schema, `readOnlyHint` for commands that do not write) plus `docs_search` and `docs_read`; a failing command returns `isError` with the message, a successful one its `Output` as text and `structuredContent`. Resources: `aego://project/manifest`, `aego://project/problems`, `aego://docs/index|<id>`, `aego://schema/index|<type>`, `aego://file/<path>` for scenes, prefabs, data assets and tables. The server keeps an `agent.Kit` (types, modules, importers) and opens a pipeline session per request (a few milliseconds on the platformer), so it never holds the asset index lock between calls and an editor started later owns it. `--watch` polls the project with the pipeline's watcher and notifies subscribed resources; a document that appears or disappears also sends `list_changed`. A native project is served by its own binary (`--tool mcp`, stdin passed through). |
| D301 | Release builds. The `release` job of `ci.yml` runs after `checks` and `build` on every push: master publishes the rolling pre-release `nightly` (the release and its tag are recreated at the new commit), a `v*` tag publishes that release. `tools/release/release.sh [--dry-run] <vX.Y.Z|nightly>` builds `cmd/aego` with cgo, `-trimpath -s -w` for the runner's GOOS/GOARCH (Linux amd64 only for now), packs `aego-<version|nightly>-<os>-<arch>.tar.gz` (`bin/aego`, `VERSION` with tag and commit, `README.txt`), lists every file in one `SHA256SUMS.txt` (the Gitea instance accepts only some attachment types: `.gz`, `.zip`, `.txt`, not `.exe` or `.sha256`), and uploads them through the Gitea API with the job token; a tag that differs from `engine.Version` fails. `tools/release/install.sh`, served at the site root, picks the latest release (`--nightly`, `--version vX.Y.Z`), verifies the checksum, unpacks to `<prefix>/share/aego` and links `<prefix>/bin/aego` (`~/.local` by default), installs missing runtime libraries (Mesa GL and EGL, X11, Wayland, xkbcommon) with the system package manager unless `--no-deps`, and warns about glibc older than 2.39 or OpenGL below 3.3; other systems are told to build from source. On the same Linux runner the job cross-builds Windows with mingw-w64 (`tools/bootstrap-native.sh` sets `CMAKE_SYSTEM_NAME=Windows` when it is not on Windows): `aego.exe`, `aego-editor.exe` (GUI subsystem) and the NSIS installer, shipped zipped as `aego-<version>-windows-amd64-setup.zip` (`tools/release/windows/aego.nsi`: per user under `%LOCALAPPDATA%\Programs\Aego`, no administrator rights, Start menu and desktop shortcuts, the directory on the user `PATH`, an entry in Settings → Apps; running a newer installer updates in place), plus a `.zip`; `install.ps1` at the site root downloads the latest installer archive, checks it against `SHA256SUMS.txt`, unpacks it and runs the installer silently. A prebuilt `aego` edits, runs and exports script-only projects without Go; native modules and exports to other systems need Go and a checkout (`AEGO_ROOT` or `--engine`). |
| D301 | Release builds. The `release` job of `ci.yml` runs after `checks` and `build` on every push: master publishes the rolling pre-release `nightly` (the release and its tag are recreated at the new commit), a `v*` tag publishes that release. `tools/release/release.sh [--dry-run] <vX.Y.Z|nightly>` builds `cmd/aego` with cgo, `-trimpath -s -w` for the runner's GOOS/GOARCH (Linux amd64 only for now), packs `aego-<version|nightly>-<os>-<arch>.tar.gz` (`bin/aego`, `VERSION` with tag and commit, `README.txt`) with a `.sha256`, and uploads both through the Gitea API with the job token; a tag that differs from `engine.Version` fails. `tools/release/install.sh`, served at the site root, picks the latest release (`--nightly`, `--version vX.Y.Z`), verifies the checksum, unpacks to `<prefix>/share/aego` and links `<prefix>/bin/aego` (`~/.local` by default), installs missing runtime libraries (Mesa GL and EGL, X11, Wayland, xkbcommon) with the system package manager unless `--no-deps`, and warns about glibc older than 2.39 or OpenGL below 3.3; other systems are told to build from source. On the same Linux runner the job cross-builds Windows with mingw-w64 (`tools/bootstrap-native.sh` sets `CMAKE_SYSTEM_NAME=Windows` when it is not on Windows): `aego.exe`, `aego-editor.exe` (GUI subsystem) and the NSIS installer `aego-<version>-windows-amd64-setup.exe` (`tools/release/windows/aego.nsi`: per user under `%LOCALAPPDATA%\Programs\Aego`, no administrator rights, Start menu and desktop shortcuts, the directory on the user `PATH`, an entry in Settings → Apps; running a newer installer updates in place), plus a `.zip`; `install.ps1` at the site root downloads the latest installer, checks its checksum and runs it silently. A prebuilt `aego` edits, runs and exports script-only projects without Go; native modules and exports to other systems need Go and a checkout (`AEGO_ROOT` or `--engine`). |
| D302 | Documentation screenshots. `make shots` opens a copy of `examples/platformer` in the real editor under a clean user profile with `AEGO_SHOTS=<dir>` (`app/edit/shots.go`: a fixed sequence of editor states, each after a fixed number of frames) and writes `docs/shots/*.png`, which are committed; `make shots-check` (part of `make ci`) regenerates them and `tools/shotdiff` fails when more than 3% of the pixels of any image moved by more than 24/255. Frames are read with `Engine.CaptureFrame`, a one-shot hook between `EndFrame` and the swap that reads the default framebuffer through the optional `gpu.BackbufferReader` (implemented by GL 3.3 only). Pages use them as `![alt](shots/name.png)`; gendocs copies the directory to `assets/shots/`, the editor's docs panel skips images, and `docs/shots/demo.png` is the poster of the landing page demo. |
| D303 | Docs delivery. The landing page embeds the web demo as a click-to-play iframe over its poster and starts it at once for a visitor who has played it before (a localStorage mark). The Try it loader compiles `aego-docs.wasm` straight from the network response, so the browser keeps both the file and its compiled code in the HTTP cache, and counts a copy of the first download for a progress bar against the uncompressed size written into the page; a localStorage mark of the versioned URL tells later pages the runtime was started before, and they start the first Try it block that scrolls into view without waiting for a click (the Cache API was tried and dropped: responses rebuilt from it lose the compiled-code cache, and probing the HTTP cache with `only-if-cached` holds its lock on the same URL); the block shows the scene as a tree (`sceneio.Outline`, node → components → typed fields linked to their docs, values updated from the runtime and changed ones marked) with Open in editor (downloads the `.aescene`) and Copy as .aescene. The docs runtime shows the `physics.shapes` and `physics.joints` debug draw channels, so colliders and joints are visible without art. nginx serves every `.wasm` with a brotli or gzip copy made at image build, `?v=` URLs as `immutable` for a year, HTML and JSON for 60 seconds (`no-cache` kept the edge proxy in front of the site from storing new pages, so it served stale ones), and `install.sh` as text; the web export preloads its `game.wasm`. Amended 2026-10-20: the site sits behind the edge proxy at 185.170.196.188, whose route to mainserv is slow (~0.2 MB/s) and drops, so the edge serves aego from its own copy (`deploy/edge`): the image carries `__edge/site.tar.gz` and `__edge/version`, the edge timer `aego-wasm-sync` fetches a new site archive with resumable downloads and switches `/var/www/aego-site/current` atomically, and copies each wasm into `/var/www/aego-wasm/<version>/`; anything not there yet goes to mainserv through the proxy, so a new `?v=` never gets old bytes. Pages show the commit in the header, and styles, scripts and the icon sprite are loaded as `?v=<hash of their contents>`, so a new revision reaches every browser within the 60 s HTML lifetime. |
| D304 | Camera adjustment is a pull contract. `contracts/camera2d` declares `Adjuster` under `camera2d.adjust`. `render2d.cameras` builds the view of every active Camera 2D from its interpolated pose, then passes it through `view = Adjust(node, view)` when the play world has the service; the value form keeps the per-frame path free of allocations. Rendering never names the camera module and the camera module never names render systems (D204); the contract may import `render/view` (a depcheck exception). An output is evaluated once per `camera.update` tick; a second Camera 2D on the same output reuses the pose. The edit world has no adjuster, fixed views are never adjusted. The module ID `camera` collides with the scene view state field `"camera"`, which is excepted in depcheck. |
| D304 | Camera adjustment is a pull contract. `contracts/camera2d` declares `Adjuster` under `camera2d.adjust`. `render2d.cameras` builds the view of every active Camera 2D from its interpolated pose, then replaces it with `Adjust(node, view)` when the play world has the service; the view goes by value, so the frame does not allocate. Rendering never names the camera module and the camera module never names render systems (D204); the contract may import `render/view` (a depcheck exception). An output is evaluated once per `camera.update` tick; a second Camera 2D on the same output reuses the pose. The edit world has no adjuster, fixed views are never adjusted. The module ID `camera` collides with the scene view state field `"camera"`, which is excepted in depcheck. |
| D305 | Virtual cameras, priorities, blends and zones. Each output picks the enabled virtual camera with the highest priority; ties go to the lower node handle index, then zones over plain cameras, the later zone over the earlier, and BlendTo/Cut over everything until Release. Zones are derived each tick from the exact world position of the target (not the interpolated one), so entering is deterministic; they never write component fields. A switch blends from the current logical pose, zoom in log space; a replaced blend completes with finished=false. Blends are awaitable handles `aego/camera.Blend`. Outputs are chosen per Camera 2D by `camera.CameraBrain2D`; without it the camera shows the main output. |
| D306 | Follow, shake, accessibility and time. Follow reads the interpolated target pose and runs in game time with exponential, rate-independent damping, a deadzone in view fractions, lookahead in seconds of velocity and snapping on teleports. Shake is trauma^exponent times deterministic gradient noise seeded by the output name; it never touches node transforms or the world RNG. Accessibility provides `camera2d.accessibility` with its setting IDs: Screen Shake scales shake, Reduce Motion disables shake and lookahead. Blends and shake follow the VC time domain (unscaled by default). The rewind hash covers active cameras, overrides, zones and game-domain blends and trauma; follow is captured but not hashed, unscaled blends finish on restore. |
+2 -2
View File
@@ -33,13 +33,13 @@ The script downloads the [latest release](https://git.akarpov.ru/sanspie/aego/re
curl -fsSL https://aego.akarpov.ru/install.sh | sh -s -- --nightly
```
To install by hand, download `aego-<version>-linux-amd64.tar.gz` and `SHA256SUMS.txt` from the [releases page](https://git.akarpov.ru/sanspie/aego/releases), check the archive with `sha256sum -c --ignore-missing SHA256SUMS.txt`, unpack it anywhere and put its `bin` directory on `PATH`.
To install by hand, download `aego-<version>-linux-amd64.tar.gz` and its `.sha256` from the [releases page](https://git.akarpov.ru/sanspie/aego/releases), check the sum with `sha256sum -c`, unpack the archive anywhere and put its `bin` directory on `PATH`.
The prebuilt editor needs glibc 2.39 or newer (Ubuntu 24.04, Debian 13, Fedora 40 and later) and OpenGL 3.3 on X11 or Wayland. The script installs the system libraries it needs when they are missing (Mesa OpenGL, X11, Wayland and xkbcommon) with apt, dnf, pacman or zypper, using `sudo` when it is not run as root; `--no-deps` skips that. When `glxinfo` is present it also warns if the driver offers less than OpenGL 3.3. It edits, runs and exports projects that use scripts only. Projects with native Go modules, and exports to other operating systems, need Go and a source checkout: see below.
## Windows
Download `aego-<version>-windows-amd64-setup.zip` from the [latest release](https://git.akarpov.ru/sanspie/aego/releases/latest), open it and run the setup program inside. It installs for your user without administrator rights, into `%LOCALAPPDATA%\Programs\Aego`: the editor with Start menu and desktop shortcuts, and `aego` on `PATH` for terminals. Run a newer installer to update; uninstall from Settings → Apps. The editor needs a GPU driver with OpenGL 3.3, which every driver from the last ten years has.
Download `aego-<version>-windows-amd64-setup.exe` from the [latest release](https://git.akarpov.ru/sanspie/aego/releases/latest) and run it. It installs for your user without administrator rights, into `%LOCALAPPDATA%\Programs\Aego`: the editor with Start menu and desktop shortcuts, and `aego` on `PATH` for terminals. Run a newer installer to update; uninstall from Settings → Apps. The editor needs a GPU driver with OpenGL 3.3, which every driver from the last ten years has.
The same from PowerShell, also for updating:
-1
View File
@@ -112,7 +112,6 @@ func (p *plugin) vcInspector(ctx *context.Editor, nodes []ids.NodeID, def func()
}
d.View.Camera.Position = st.Pose.Position
d.View.Camera.Zoom = zoom
d.View.Camera.Rotation = st.Pose.Rotation
}
}
+1 -2
View File
@@ -84,8 +84,7 @@ func (m *Module) AttachWorld(ctx *module.AttachContext) error {
if !ctx.Play || ctx.World == nil || ctx.Scene == nil {
return nil
}
s := NewService(ctx.Scene, ctx.World)
s.acc = &m.acc
s := newService(ctx.Scene, ctx.World, &m.acc)
ctx.World.SetService(ServiceID, s)
ctx.World.SetView(camera2d.ServiceID, camera2d.Adjuster(s))
if s.hub != nil {
-10
View File
@@ -1,10 +0,0 @@
MODULE_CI += test-camera
.PHONY: test-camera bench-camera
test-camera:
go test ./modules/camera/... ./tests/camera/...
go test -run 'Rewind' ./tests/integration
bench-camera:
$(BENCH_PIN) go test -run '^$$' -bench BrainStep -benchmem -count=$(BENCH_COUNT) ./tests/camera
+3 -10
View File
@@ -110,19 +110,14 @@ var (
errDisabled = errs.New(errs.EngineModule, "camera: the virtual camera is disabled")
)
func NewService(s *scene.Scene, w *engine.World) *Service {
svc := &Service{scene: s, world: w, shakeScale: 1}
func newService(s *scene.Scene, w *engine.World, acc *access) *Service {
svc := &Service{scene: s, world: w, acc: acc, shakeScale: 1}
if w != nil {
svc.hub, _ = engine.ServiceOf[signals.Hub](w, signals.ServiceID)
}
return svc
}
func (s *Service) SetAccessibility(shake float32, reduced bool) {
s.acc = nil
s.shakeScale, s.reduced = shake, reduced
}
func (s *Service) find(name string) *output {
for _, o := range s.outputs {
if o.name == name {
@@ -158,9 +153,7 @@ func (s *Service) Update(game, real float32) {
}
s.ticks++
s.dtGame, s.dtReal = game, real
if s.acc != nil {
s.shakeScale, s.reduced = s.acc.read()
}
s.shakeScale, s.reduced = s.acc.read()
for _, o := range s.outputs {
o.vcs = o.vcs[:0]
}
+5 -5
View File
@@ -63,7 +63,7 @@ func TestPriorityBlendAndRelease(t *testing.T) {
close := vcam(s, "close", mathx.Vec2{X: 10}, -1, 0.5)
s.Flush()
s.UpdateTransforms()
svc := NewService(s, nil)
svc := newService(s, nil, nil)
if v := frame(svc, cam); v.Camera.Position != (mathx.Vec2{}) || svc.Active("") != wide {
t.Fatalf("wide camera drives the view: %v", v.Camera.Position)
}
@@ -103,7 +103,7 @@ func TestZoneLiftsCamera(t *testing.T) {
z.Zoom = 50
s.Flush()
s.UpdateTransforms()
svc := NewService(s, nil)
svc := newService(s, nil, nil)
frame(svc, cam)
if svc.Active("") != base {
t.Fatal("outside the zone the base camera drives")
@@ -136,7 +136,7 @@ func TestOutputsAreIndependent(t *testing.T) {
vm.Output = "map"
s.Flush()
s.UpdateTransforms()
svc := NewService(s, nil)
svc := newService(s, nil, nil)
svc.Update(step, step)
a, b := baseView(), baseView()
a, b = svc.Adjust(main, a), svc.Adjust(mini, b)
@@ -155,7 +155,7 @@ func TestShakeFadesAndRespectsReducedMotion(t *testing.T) {
vcam(s, "a", mathx.Vec2{}, 0, 0)
s.Flush()
s.UpdateTransforms()
svc := NewService(s, nil)
svc := newService(s, nil, nil)
svc.Shake("", 0.6)
v := frame(svc, cam)
if v.Camera.Position == (mathx.Vec2{}) {
@@ -190,7 +190,7 @@ func TestFollowSnapsOnTeleport(t *testing.T) {
f.Target = scene.RefTo(s, target)
s.Flush()
s.UpdateTransforms()
svc := NewService(s, nil)
svc := newService(s, nil, nil)
frame(svc, cam)
s.SetPosition(target, mathx.Vec2{X: 100})
s.Flush()
File diff suppressed because it is too large Load Diff
-48
View File
@@ -6,9 +6,7 @@ import (
"aego/app/edit"
"aego/core/log"
"aego/core/mathx"
"aego/core/rewind"
"aego/editor/core/play"
"aego/modules/camera"
"aego/modules/physics"
"aego/platform"
"aego/platform/input"
@@ -167,49 +165,3 @@ func TestEditorPauseRecordsNothing(t *testing.T) {
t.Fatalf("timeline %+v after resuming, want recording to go on", tl)
}
}
func cameraHash(t *testing.T, pc *play.Controller) [32]byte {
t.Helper()
s := camera.Of(pc.World())
if s == nil {
t.Fatal("the platformer play world has no camera service")
}
h := rewind.NewHasher()
s.Hash(h)
return h.Sum()
}
func TestEditorRewindKeepsTheCamera(t *testing.T) {
s, pc := rewindSession(t)
in := s.Host.Engine.Input()
hold := func(k input.Scancode, down bool) {
kind := platform.EvKeyUp
if down {
kind = platform.EvKeyDown
}
platform.ApplyInput(in, platform.Event{Kind: kind, Key: input.KeyEvent{Scan: k}})
}
marks := map[int][32]byte{}
for i := 0; i < 240; i++ {
hold(input.KeyD, i >= 10 && i < 200)
hold(input.KeySpace, i >= 40 && i < 44)
pc.Tick(1.0 / 60)
if i+1 == 20 || i+1 == 120 {
marks[i+1] = cameraHash(t, pc)
}
}
if camera.Of(pc.World()).Active("").IsZero() {
t.Fatal("the platformer camera has no active virtual camera")
}
if marks[20] == marks[120] {
t.Fatal("the camera state is the same before and inside the arena zone")
}
for _, f := range []int{120, 20, 120} {
if err := pc.Seek(f); err != nil {
t.Fatal(err)
}
if cameraHash(t, pc) != marks[f] {
t.Fatalf("camera state differs after seeking to frame %d", f)
}
}
}
+11 -16
View File
@@ -15,23 +15,18 @@ if (-not $release -and $which -eq "latest") {
try { $release = Invoke-RestMethod "$api/releases/$which" } catch { $release = $null }
}
if (-not $release) { throw "No Aego release is published yet; build it from source: https://aego.akarpov.ru/install.html#build-from-source" }
$setup = $release.assets | Where-Object { $_.name -like "*-windows-amd64-setup.zip" } | Select-Object -First 1
$setup = $release.assets | Where-Object { $_.name -like "*-windows-amd64-setup.exe" } | Select-Object -First 1
if (-not $setup) { throw "release $which has no Windows installer" }
$sums = $release.assets | Where-Object { $_.name -eq "SHA256SUMS.txt" } | Select-Object -First 1
if (-not $sums) { throw "release $which has no SHA256SUMS.txt" }
$dir = Join-Path $env:TEMP ("aego-setup-" + [guid]::NewGuid())
New-Item -ItemType Directory -Path $dir | Out-Null
$zip = Join-Path $dir $setup.name
$sum = $release.assets | Where-Object { $_.name -eq "$($setup.name).sha256" } | Select-Object -First 1
$file = Join-Path $env:TEMP $setup.name
Write-Host "Downloading $($setup.name)"
Invoke-WebRequest $setup.browser_download_url -OutFile $zip -UseBasicParsing
$line = ((Invoke-WebRequest $sums.browser_download_url -UseBasicParsing).Content -split "`n") | Where-Object { $_ -match [regex]::Escape($setup.name) + '\s*$' } | Select-Object -First 1
if (-not $line) { throw "SHA256SUMS.txt does not list $($setup.name)" }
$want = ($line -split '\s+')[0]
$got = (Get-FileHash $zip -Algorithm SHA256).Hash
if ($want -ne $got) { Remove-Item -Recurse $dir; throw "checksum mismatch, refusing to install" }
Expand-Archive $zip -DestinationPath $dir
$exe = Get-ChildItem $dir -Filter "*-setup.exe" | Select-Object -First 1
Invoke-WebRequest $setup.browser_download_url -OutFile $file -UseBasicParsing
if ($sum) {
$want = ((Invoke-WebRequest $sum.browser_download_url -UseBasicParsing).Content -split '\s+')[0]
$got = (Get-FileHash $file -Algorithm SHA256).Hash
if ($want -ne $got) { Remove-Item $file; throw "checksum mismatch, refusing to install" }
}
Write-Host "Installing"
Start-Process $exe.FullName -ArgumentList "/S" -Wait
Remove-Item -Recurse $dir
Start-Process $file -ArgumentList "/S" -Wait
Remove-Item $file
Write-Host "Aego is installed: open it from the Start menu, or run 'aego' in a new terminal."
+2 -5
View File
@@ -57,11 +57,8 @@ trap 'rm -rf "$tmp"' EXIT
file="$tmp/$(basename "$url")"
say "Downloading $url"
curl -fSL --progress-bar -o "$file" "$url"
sums=$(printf '%s' "$json" | tr ',' '\n' | sed -n 's/.*"browser_download_url":"\([^"]*\/SHA256SUMS\.txt\)".*/\1/p' | head -n 1)
[ -n "$sums" ] || { say "install.sh: the release has no SHA256SUMS.txt"; exit 1; }
curl -fsSL -o "$tmp/SHA256SUMS.txt" "$sums"
want=$(grep " $(basename "$url")\$" "$tmp/SHA256SUMS.txt" | cut -d' ' -f1)
[ -n "$want" ] || { say "install.sh: SHA256SUMS.txt does not list $(basename "$url")"; exit 1; }
curl -fsSL -o "$file.sha256" "$url.sha256"
want=$(cut -d' ' -f1 "$file.sha256")
if command -v sha256sum >/dev/null 2>&1; then
got=$(sha256sum "$file" | cut -d' ' -f1)
else
+8 -39
View File
@@ -31,9 +31,8 @@ label="$version"
dist="$root/.cache/dist"
rm -rf "$dist" && mkdir -p "$dist"
# sum adds a file to SHA256SUMS.txt; Gitea accepts only a few attachment types, .txt among them.
sum() {
(cd "$dist" && sha256sum "$1" >> SHA256SUMS.txt)
(cd "$dist" && sha256sum "$1" > "$1.sha256")
}
stage() {
@@ -63,11 +62,9 @@ if command -v "$wcc" >/dev/null 2>&1 && command -v makensis >/dev/null 2>&1; the
rmdir "$dist/$win/bin"
(cd "$dist" && zip -qr "$win.zip" "$win")
sum "$win.zip"
mkdir -p "$dist/setup"
makensis -V2 -DVERSION="$label" -DSRC="$dist/$win" -DOUT="$dist/setup/$win-setup.exe" tools/release/windows/aego.nsi
(cd "$dist/setup" && zip -q "../$win-setup.zip" "$win-setup.exe")
sum "$win-setup.zip"
echo "release: built $win.zip and $win-setup.zip"
makensis -V2 -DVERSION="$label" -DSRC="$dist/$win" -DOUT="$dist/$win-setup.exe" tools/release/windows/aego.nsi
sum "$win-setup.exe"
echo "release: built $win.zip and $win-setup.exe"
elif [ "${REQUIRE_WINDOWS:-0}" = 1 ]; then
echo "release: $wcc and makensis are needed for the Windows build" >&2
exit 1
@@ -78,20 +75,16 @@ body="Built from $short."
if [ "$dry" = 1 ]; then
echo "release: dry run, would publish $name ($tag, prerelease=$pre) at $commit with:"
ls -l "$dist"/aego-*.* "$dist"/SHA256SUMS.txt
cat "$dist"/SHA256SUMS.txt
ls -l "$dist"/aego-*.*
exit 0
fi
: "${GITEA_TOKEN:?GITEA_TOKEN is not set}"
: "${GITEA_API:?GITEA_API is not set}"
# GITEA_RESOLVE=host:443:ip sends API calls straight to the Gitea server, past a slow proxy.
api() {
method=$1
shift
set -- -fsS -m 1800 -X "$method" -H "Authorization: token $GITEA_TOKEN" "$@"
[ -n "${GITEA_RESOLVE:-}" ] && set -- --resolve "$GITEA_RESOLVE" "$@"
curl "$@"
curl -fsS -X "$method" -H "Authorization: token $GITEA_TOKEN" "$@"
}
release_id() {
api GET "$GITEA_API/releases/tags/$1" 2>/dev/null | sed -n 's/^{"id":\([0-9]*\),.*/\1/p'
@@ -108,36 +101,12 @@ if [ -z "$id" ]; then
id=$(api POST "$GITEA_API/releases" -H "Content-Type: application/json" -d "$json" | sed -n 's/^{"id":\([0-9]*\),.*/\1/p')
fi
[ -n "$id" ] || { echo "release: could not create release $tag" >&2; exit 1; }
failed=""
for f in "$dist"/aego-*.tar.gz "$dist"/SHA256SUMS.txt "$dist"/aego-*.zip; do
for f in "$dist"/aego-*.tar.gz "$dist"/aego-*.zip "$dist"/aego-*-setup.exe "$dist"/aego-*.sha256; do
[ -f "$f" ] || continue
n=$(basename "$f")
old=$(api GET "$GITEA_API/releases/$id/assets" | tr '{' '\n' | sed -n "s/^\"id\":\([0-9]*\),\"name\":\"$n\".*/\1/p")
[ -n "$old" ] && api DELETE "$GITEA_API/releases/$id/assets/$old" >/dev/null
ok=0
for attempt in 1 2 3 4 5; do
code=$(curl -sS -m 1800 ${GITEA_RESOLVE:+--resolve "$GITEA_RESOLVE"} -o "$dist/upload.out" -w '%{http_code}' \
-H "Authorization: token $GITEA_TOKEN" -F "attachment=@$f" "$GITEA_API/releases/$id/assets?name=$n" || echo 000)
if [ "$code" = 201 ]; then
ok=1
break
fi
echo "release: upload of $n failed with HTTP $code, attempt $attempt: $(head -c 400 "$dist/upload.out" 2>/dev/null)" >&2
sleep 10
stale=$(api GET "$GITEA_API/releases/$id/assets" | tr '{' '\n' | sed -n "s/^\"id\":\([0-9]*\),\"name\":\"$n\".*/\1/p")
[ -n "$stale" ] && api DELETE "$GITEA_API/releases/$id/assets/$stale" >/dev/null
done
if [ "$ok" != 1 ]; then
why=$(printf 'Upload of %s failed with HTTP %s: %s' "$n" "$code" "$(head -c 300 "$dist/upload.out" 2>/dev/null | tr -d '"\\\n\r')")
api PATCH "$GITEA_API/releases/$id" -H "Content-Type: application/json" -d "{\"body\":\"$body $why\"}" >/dev/null || true
echo "release: $why" >&2
failed="$failed $n"
continue
fi
api POST "$GITEA_API/releases/$id/assets?name=$n" -F "attachment=@$f" >/dev/null
echo "release: uploaded $n"
done
if [ -n "$failed" ]; then
echo "release: published $tag without$failed" >&2
exit 1
fi
echo "release: published $tag"