56 Commits
Author SHA1 Message Date
sanspie a6822156cd Finish Stage 12 editor fixes and refreshed platformer
ci-platforms / windows amd64 smoke (push) Skipped
ci / release (push) Successful in 8m58s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 4m46s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 13m28s
2026-11-05 17:36:42 +03:00
sanspie 5eb1832907 Complete Stage 12 for v0.12.0
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 15m28s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 7m47s
2026-11-05 11:46:53 +03:00
sanspie ad23798da5 docs: the project description says what Aego is: a 2D game engine built in Go
ci / checks (push) Successful in 7m58s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 11m1s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / release (push) Successful in 8m21s
2026-10-28 02:55:13 +03:00
sanspie 132ddeff70 Stage 12a1: shader ABI, shader import and material runtime (D367, D368, D380, D381, D393)
ci / checks (push) Successful in 8m39s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 12m19s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / release (push) Successful in 8m54s
Shader ABI 1: ub0 frame (16 B), ub1 view (112 B), ub2 material, ub3 mesh
draw; tex0 main, tex1 reserved, tex2-tex7 user; @instance lanes in
vertex slot 1 at locations 4-7, 64 bytes a sprite. The engine sprite,
sprite3d, debug and mesh shaders move to it, and engine includes
sprite, color, noise and lighting ship under engine/res/shaders/include.

.aeshader files import into an AESH artifact with every variant in both
GLSL dialects and a line map; errors are SHD001-SHD009 mapped back to the
source file and line, also for driver compile logs. A failed reimport
keeps the last good shader. .aemat materials (MAT001-MAT004) reference a
shader and textures and are reimported when their shader changes. The
material cache in render/material binds a material with no allocations,
retires replaced shaders, moves materials to a reimported shader in
place (D393) and re-uploads every material after a WebGL context
restore. make ci requires glslangValidator (D367).

web-bench, baseline retaken on nodea in both GPU modes, the same numbers
in swiftshader and hardware, Chromium and Firefox:
- the frame UBO is uploaded once a frame: buffer uploads 2 -> 3 a frame;
- the view UBO is 112 bytes instead of 64: buffer KB 0.372 -> 0.434
  a frame (+16 B frame, +48 B view);
- fewer SetUniformBuffer calls a batch: the sprite pass no longer binds
  the view per batch, frame and view are bound once per view; the GL
  state cache already deduped that bind, so WebGL calls go 50.49 ->
  52.49 a frame with the two new frame-UBO calls, draws 3.31 unchanged;
- buffer KB from lanes is unchanged: the demo has no @instance material.
Frame time and Go bytes in the engine frame (357 -> 342) do not regress.

wasm size, gzip: playground 6.46 -> 6.50 MB, try 6.98 -> 7.01 MB, web
runtime 6.05 -> 6.07 MB (total 27.27 -> 27.46, 29.54 -> 29.67,
25.56 -> 25.65 MB).
2026-10-28 01:23:11 +03:00
sanspie c8d45b6ff5 docs: the Animator demo jumps and hits; the web demo caption names J to attack
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 12m59s
ci / release (push) Successful in 9m16s
ci-platforms / windows amd64 smoke (push) Skipped
ci / checks (push) Successful in 8m18s
ci-platforms / darwin amd64 smoke (push) Skipped
The built-in platformer controller plays engine/anim/jump in Jump and engine/anim/hit in Hit; before, both states had no clip and nothing showed.
The Animator try scene jumps with Space (Grounded false in the air) instead of toggling the parameter.
2026-10-27 23:09:31 +03:00
sanspie 962c479933 web: the browser build drops the self-update core (D362 amended)
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci / checks (push) Successful in 10m24s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 12m13s
ci / release (push) Successful in 8m57s
modules/update links aego/update (net/http, TLS, x509, archives) only off js; the web build gets a browser backend with the same section, settings and aego/update API, where Check and Apply fail and State stays idle. A test compares both builds' registrations.
depcheck: wasm-runtime and wasm-try variants, update in wasmDenied, wasmDeniedStd (net/http, crypto/tls, crypto/x509, archive/*) with the wasm-reaches-std rule.
wasmsize -prev: playground-size and try-size print the change since their last run.
Loaders show the compressed download size: wasmGzip in an export's files.json, aego-*-gzip and data-wasm-gzip on the site; docstool writes the .wasm.gz copies.
Demo game.wasm 33.9 -> 26.0 MB, gzip 8.07 -> 6.25 MB.
2026-10-27 20:06:16 +03:00
sanspie 72d9a21782 Stage 12w: web runtime performance (D365, D366, D390, D391)
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 8m33s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 12m38s
ci / release (push) Successful in 9m25s
make web-bench measures the exported platformer in Chromium and Firefox
in lockstep, with exact WebGL counters against a per-mode baseline, time
compared with Mann-Whitney and a profile pass that gates allocations in
the engine frame to zero, except listed first-decode and growth sites.

On the web the frame loop fills the audio output, the stream feeder
waits for ring space, per-event allocations are gone and Go allocations
per frame drop from 7.2 to 3.8 in Chromium. The pixel ratio is capped at
1.5 by default (display.maxPixelRatio) and Frame Limit works on the web.
The game loop stays on the main thread; a Worker is not built (D391).
2026-10-27 16:29:41 +03:00
sanspie cc2a428a7d engine: version 0.12.0-dev until the 0.12.0 release
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 8m13s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 11m21s
ci / release (push) Successful in 9m18s
2026-10-26 22:33:39 +03:00
sanspie 824aa2e9a0 tests: the agent panel and key spacing checks allow a busy runner's noise
ci-platforms / darwin arm64 smoke (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci / release (push) Successful in 11m19s
ci / checks (push) Successful in 9m7s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 9m50s
2026-10-26 22:20:54 +03:00
sanspie c2736c158f ci: the release job takes AEGO_UPDATE_KEY from a variable or a secret
ci-platforms / darwin amd64 smoke (push) Skipped
ci / checks (push) Successful in 6m58s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 6m7s
2026-10-26 21:46:40 +03:00
sanspie d4b5b88be1 Release 0.11.0 2026-10-26 21:09:36 +03:00
sanspie 78e86970d7 scene_run: a run with screenshots steps a fixed frame time, so it moves the game like a run without them 2026-10-26 21:09:27 +03:00
sanspie a6ebf841d0 Stage 11b8: agents draw their art (D363)
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 9m10s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 12m25s
ci / release (push) Failing after 4m59s
asset.pixels: pixel art as a character grid and a palette; frames of one size become an atlas with a sprite per frame and a flipbook clip; a preview image in the answer.
asset.svg: an SVG subset drawn by an in-tree rasterizer (format/svg) for tiles, UI and icons.
art module: project.aego art { palette pixelsPerUnit }, the default palette and scale for drawings; Project Health HLT009 for pixel art outside the palette.
The agent prompts say to draw instead of building sprites from rectangles.
Agent runs open the asset index shared without a warning; prefab.create on a scene open in the editor goes through the prefab service, so unsaved scenes work.
scene_run with shot names xvfb when there is no display; local make ci lists untracked files outside .gitignore.
2026-10-26 19:36:15 +03:00
sanspie f5944907cc launcher: new projects go to AegoProjects, without a space 2026-10-26 19:36:15 +03:00
sanspie d436311f69 Stage 11i4: signed self-updates for Aego and exported games (D362)
update core: signed release.json (Ed25519) per channel, ETag check, resumable download with sha256, Auto/Ask/Off policy, appliers for the install.sh layout, AppImage, NSIS installer (keeps the replaced setup.exe for Restore) and .app, trial run with rollback.
Editor and launcher: banner, status bar, Help commands, Task Center download, install on restart.
CLI: aego update, aego release keygen|manifest, aego export --release-json.
Games: modules/update with update { url publicKey }, settings and the aego/update script API.
release.sh refuses to publish without AEGO_RELEASE_KEY and AEGO_UPDATE_KEY; install.sh lays out versions; make test-update runs a real v1 -> v2 update.
2026-10-26 19:36:15 +03:00
sanspie 6d7adba259 agent: scene_run screenshots keep the run on one OS thread
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci / checks (push) Successful in 7m51s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 11m18s
ci / release (push) Successful in 12m21s
A run with screenshots opens a hidden window with a GL context; the goroutine running it could move to another OS thread, and the shaders then failed to compile now and then. The run is locked to its thread while it lasts.
2026-10-26 16:31:31 +03:00
sanspie 1e28291320 repo: no build artifacts in the root
Builds and test binaries belong in .cache/bin. The root .gitignore catches stray builds (aego-*, i18n, profiles, data dumps), make affected builds into .cache/bin/affected, make clean empties .cache/bin, and make ci ends with tools/ci-clean-root.sh: it fails when the root holds an untracked executable or a *.test file, and in CI when the run left changes git sees. .claude/CLAUDE.md tells agents to build only into .cache/bin and to run make ci before every push.
2026-10-26 15:53:47 +03:00
sanspie 49c9e97203 Polish 11: translator notes
The notes of the Russian translation are worked off. GMP001 names the component as the editor does, Rigid Body 2D. The editor guide points at Play > Never Show Play Changes, which is where the list is turned off. The AI preset of Debug Draw has a translated name. The Coroutines section of the language reference has its Waiting heading back before the yield table and says call into a script instead of callback. The catalog key of a method named like a field is api.<id>.method instead of carrying parentheses. The 21 easing presets say how each one moves and what it suits. Data types such as Audio Event get catalog entries for their names and fields, so the Inspector and the menus show them translated, and Audio Ranges has a translated name.

The Russian table of agent commands in cli.ru.md is generated from the catalog like the English one, so a new or changed command shows up as a missing translation instead of a stale row.
2026-10-26 15:52:53 +03:00
sanspie a1eb467600 Stage 11b6: QA tools for the agent and the editor's proxy (D341 amended)
scene_run plays the game the way a player would and checks the result in the same run. input presses actions by frame: press, hold over a range, release, and axis values. assert reads values with the paths scene_set takes, at a frame (@120) or at any frame of the run, with ~= comparing numbers within 0.01, and exists or gone for nodes; the report gives ok or failed with the value and the frame, a failed check fails the run, and the CLI exits with 2. shot captures up to three frames from a run with a hidden window: the answer carries them as images scaled to 640 wide (scale changes it) and the full frames stay under .aego/cache/runs.

While the editor is open, the CLI and aego mcp go through it. The editor serves the agent commands on a second loopback MCP server over the same bridge and writes its address in .aego/editor.lock. A change becomes an Undo step named after its origin, via CLI or via agent with the client's name, and asset creation works because the editor does it. Window > Confirm Changes from Outside Agents (editor.proxy.confirm) makes these changes wait in the Agent panel instead; follow mode shows outside calls like the panel's agent's. When the editor does not answer, the CLI works on the files as before.

The native Windows and macOS jobs of ci-platforms are named smoke: the Windows build itself is cross-compiled on the Linux runner.
2026-10-26 15:39:35 +03:00
sanspie a25db02077 Stage 11b5c: context meter, read budgets, new sessions from a summary (D343 amended)
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 7m55s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 11m8s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / release (push) Successful in 8m55s
The Agent panel counts the session's context. A gauge in the header shows how full the model's window is, from the CLI's usage on each answer and the window it reports; it turns yellow past 80 % and near compaction. Its popup lays the window out as one bar and a row per part: messages, scene, code, docs, project and schema, tool results, the editor's instructions and tool definitions, the CLI's own prompt, the compaction buffer and free room. The parts are the editor's estimate of what it sent, scaled to the usage; on a recorded session the estimate stays within 15 % of the CLI's count.

A part opens to its contributions with Drop and Pin. The CLI cannot edit its history, so Drop starts the next message in a new session with a shortened history: messages in full, results as summaries, pinned ones in full, dropped ones left out. New Session with Summary asks the agent for a summary and sends it, with the names of the changed nodes, with the next message. The open scene and the selection can go with every message. A compaction shows in the transcript.

Read tools keep large reads small: scene_tree up to a number of nodes and levels with the rest counted and a node argument for a part, docs_search up to a number of results, script_read in parts for a long script. The limits are in the popup and in prefs.aetext. MCP results are compact JSON. scene_run through the editor answers ok when the game ran.
2026-10-26 15:11:53 +03:00
sanspie 239a449fe2 agent: scripts on nodes, scene_run in the editor, following returns to the scene
ci / checks (push) Successful in 5m14s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m39s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / release (push) Successful in 5m44s
The agent attaches and removes scripts with add-comp and remove-comp by the script's type name, reads and sets their exported fields like component fields with short names (EnemyAI.PointA), and node reference fields take a node path. scene_get lists a node's script fields.

scene_run works inside the editor: the game runs headless in a child process of the editor's own binary (--tool agent-run), with the open scene as the editor has it, unsaved changes included; scripts with unsaved changes are named in the answer. watch samples the world positions of nodes through the run, so the agent can check that something moves.

Following brings the Scene View back in front of the code editor on a scene step and shows the Inspector, and a scene_apply leads to the node its transaction names.
2026-10-26 02:16:45 +03:00
sanspie b6cc16531d tests: the setting-changed test picks a volume that differs from the saved one, so it passes when run again in the same process
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 6m34s
ci / release (push) Successful in 5m9s
ci / checks (push) Successful in 4m47s
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
2026-10-26 01:42:55 +03:00
sanspie 70be14646a Stage 11b5b: follow mode and agent presence (D343 amended)
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 9m1s
ci / release (push) Successful in 11m0s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 6m39s
Follow mode leads the editor after the agent: each tool call opens the scene, selects the node in a second selection that Delete and Copy never read, frames it smoothly and shows it in the Inspector at the component; scripts open at the line without taking the keyboard, with the lines of a waiting card as ghost lines and changed lines marked in the gutter; runs and checks bring the Console or Problems forward with new entries lit. Changed fields light for two seconds, new nodes and components light whole, removed nodes fade where they were.

The agent's avatar marks where it works in the Hierarchy, the Scene View and the Inspector; the Agent panel says where it is, with a timeline of its steps to go back to them or undo to one. Follow is the eye in the header, Window → Follow the Agent or Ctrl+Alt+F, kept in prefs.aetext; the user's own input pauses it until Resume or 30 idle seconds. A user edit of a field a waiting card changes is made again after the card. During Play the agent edits the running game's scene, so its changes show in Play changes.

editor/core/presence holds who works where; ui/widgets/presence draws it. backend.Replay plays a recorded stream-json session against the editor's MCP server for the tests.
2026-10-26 00:52:59 +03:00
sanspie 70534c271c Stage 11b5a: Agent panel UI (D343 amended)
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 5m42s
ci / checks (push) Successful in 5m48s
ci / release (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
Steps instead of a raw log: every tool declares an icon, a title from its arguments and a summary of its result; read-only steps fold into one row, a step opens to its result laid out as search results, a node tree, fields, components, assets or a log, with links to what it names. Cards show changes by node with Inspector labels and names instead of ids, with Apply, Dry-run and Skip, then Undo and Select; Apply All sits over the first of several waiting cards; a failed step has Retry and Ask Agent to Fix. The agent asks with ask_user, a card of options that waits for the answer. A header with the session and what the agent does, a menu with New Session, History, Plan Only and Stop, @ chips, Stop in place of Send, the cost and time of the last answer, an empty state with examples.

The transcript is virtual: blocks are measured once per width and only those in view are laid out and drawn; text is laid out once per message. 500 messages stay within the frame budget and an idle panel allocates nothing.

script.read, script.edit, script.write and script.create change scripts through the open buffer or on disk, with Undo on the card and the compiler's AES diagnostics in the answer. docs_search matches several words against an entry and its parent, so a component and its signal find the signal.
2026-10-26 00:19:21 +03:00
sanspie a15d2a1fe3 render: the editor's sprite batch grows instead of dropping; the profiler skips graphs out of view and merges sub-pixel flame events
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 5m5s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m25s
ci / release (push) Successful in 5m14s
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
A busy frame (the profiler's 30 s graphs and a long agent transcript) passed the 65536 sprites of the editor's batch, and everything drawn after it, the right column and the menus, was dropped. The editor and the launcher now let the batch double up to the submission limit; games keep MaxSprites as their budget.
2026-10-25 23:47:58 +03:00
sanspie 6e8ebcd90f docs: the scenes docs check plays after each step stay out of the pages
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 5m39s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m49s
ci / release (push) Successful in 7m23s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
2026-10-25 18:13:09 +03:00
sanspie adcdd5fbad engine: version 0.11.0-dev until the 0.11.0 release 2026-10-25 18:11:12 +03:00
sanspie 0ec28ad86b web: fullscreen keeps the touch gamepad; the gamepad hides while the settings screen is open; settings tabs wrap and buttons shrink on a narrow screen
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Successful in 4m43s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / checks (push) Successful in 4m59s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m26s
2026-10-25 17:49:02 +03:00
sanspie ba9d35443f smoke: a one-off runtime allocation in the measured frames gets a second window; the alloc profile rate is set before goroutines start
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 5m59s
ci / release (push) Successful in 4m44s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 3m53s
2026-10-25 17:16:09 +03:00
sanspie 0fa39a3e17 11g3: one Russian label for a diagnostic's fix, Исправление
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / checks (push) Successful in 4m33s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 6m51s
ci / release (push) Skipped
2026-10-25 17:06:09 +03:00
sanspie fa6f37b7f5 cli: aego version prints the engine version and the commit it was built from
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 4m14s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 6m11s
ci / release (push) Successful in 5m10s
2026-10-25 16:31:41 +03:00
sanspie 6151233e05 11g3: the Try it toolbar and widget texts in Russian
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 3m53s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 6m34s
ci / release (push) Successful in 4m47s
2026-10-25 16:17:32 +03:00
sanspie 53ed8c6476 site: new tagline and lead; agents only in their card and the llms.txt footer link
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 3m45s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 3m53s
ci / release (push) Skipped
2026-10-25 16:10:37 +03:00
sanspie ff896506f5 11g3: Try it, the download block and kind labels in Russian; the Try it view follows the page language
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 6m21s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 4m45s
ci / release (push) Successful in 5m28s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
2026-10-25 16:00:13 +03:00
sanspie 8a842b19a6 11g3: the rest of the Russian site: llms.txt lead, module pages, project sections, handle types, field notes, diagnostic phases and notes
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Successful in 8m36s
ci / checks (push) Successful in 3m54s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 5m59s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
2026-10-25 15:51:54 +03:00
sanspie f22242e5aa Stage 11g2–11g3: registry texts, CLI and the site in Russian (D348)
ci / release (push) Successful in 5m5s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 4m41s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m14s
The remaining editor strings and undo step names; component, field, enum, setting, diagnostic and Script API texts under schema.*, enum.*, setting.*, diag.* and api.*; the launcher, the in-game Settings View with a Language setting, the CLI and aego schema show --lang. Every documentation page has a .ru.md translation; the site has a full Russian tree with hreflang, a language switch and ru/llms.txt; aego docs check --lang ru. New doc.tutorial and doc.recipe icons. Fix: an animator restored by a rewind prepares its controller before the replayed step.
2026-10-25 15:31:22 +03:00
sanspie 2e10968d48 Stage 11f3: landing, Getting started, guides; demo fixes (D347, D289)
Landing page and Install Aego / Your first project in English and Russian; the 2D template matches them. Platformer from scratch and Recipes with every step scene run by docs check. Builtin data and handle methods in the reference, Inspector names in field tables, Markdown copies and llms-full.txt, strict docs check in CI, engine/docs/README.md.
Fixes: a body cast from a touching contact uses the face normal, so characters no longer stick after a respawn or against walls; Respawn.resetVelocity works; a touch gamepad on exported web pages.
2026-10-25 15:31:22 +03:00
sanspie 2fcbaa6c12 Stage 11g1: the editor in Russian (D348)
engine/i18n with catalogs, plural forms and module catalogs; tools/i18n extract and check; the editor UI, menus, panels and notices in Russian with the author's terms; overflow test and tooltip wrapping; the English log.
2026-10-25 15:31:22 +03:00
sanspie 67e2d77823 Stage 11f1–11f2: documentation style and rewrite (D347)
docs/style.md, docs/glossary.md, style checks DOC010–DOC023 and docs report; the site in a light theme; the Editor, Aego Script, Gameplay, Physics, Scenes, Rendering, Tools and Reference sections rewritten.
2026-10-25 15:31:22 +03:00
sanspie 55871f666b Stage 11e: rewind keyframes (D346)
Adaptive keyframes with age thinning, a scrub cache, allocation-free replay, idle pacing in paused Play; job workers start before the engine returns.
2026-10-25 15:31:22 +03:00
sanspie ef59ae297f Stage 11d: light theme (D345)
Theme tokens for every editor color with a literal check, System/Dark/Light preference that follows the OS, light goldens, Scene View overlays that keep viewport colors.
2026-10-25 15:31:22 +03:00
sanspie 7b83e82c6e Stage 11b: AI agent in the editor (D341–D343)
MCP over loopback HTTP and an editor session over open documents; proposals with apply, reject, auto-apply and card undo; the Agent panel with Markdown answers, @ mentions, Ask Agent from Hierarchy and Problems, session logs.
2026-10-25 15:31:22 +03:00
sanspie 515f4ca1b3 Stage 11a: animation (D333–D340, D344, D354–D357)
Clips (.aeanim) and controllers (.aectrl) with importers, AnimationPlayer and Animator with crossfades, root motion and events on fixed steps, rewind support. Editor: Animation panel (dope sheet, curves, record, preview, trajectory), Animator panel, sprite sheet slicer and flipbooks, Programming workspace, export panel redesign. Motion 2D ledge climb. 11c: the component and Create palette.
2026-10-25 15:31:22 +03:00
sanspie 3d8e3b2fd3 Web export: the page build key covers the runtime and scripts, so a new engine gets new immutable URLs
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / release (push) Successful in 5m0s
ci / checks (push) Successful in 3m50s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 4m21s
2026-10-22 19:26:12 +03:00
sanspie c6a24ba393 Try it: scene keys in the hint, J label on Health and Hurtbox, faster Respawn, sound starts with the Try click; lighter web audio pump; release notes from tools/release/notes
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 / release (push) Successful in 7m34s
ci / checks (push) Successful in 2m53s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 5m2s
2026-10-22 18:55:33 +03:00
sanspie 7dca06e4b1 Icons in the engine style for gameplay, shake, clips, curves, dice, debug draw and install; web loader retries a stalled download
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 / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m5s
ci / release (push) Successful in 9m30s
ci / checks (push) Successful in 2m25s
2026-10-22 17:38:02 +03:00
sanspie 6dfdb824e2 Stage 10d3b: physics debug layers follow Game View overlays, PHY diagnostics pages
ci / checks (push) Successful in 3m4s
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 / build (amd64, linux, ubuntu-24.04) (push) Successful in 3m53s
ci / release (push) Successful in 5m37s
2026-10-22 16:25:06 +03:00
sanspie de49a53172 Stage 10d3: docs, site demo, check levels, boundary audit (D316)
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 21m50s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 6m46s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / release (push) Successful in 17m42s
2026-10-22 16:00:33 +03:00
sanspie 1030dc9a43 Stage 10d2: audio fast path, shutdown order, profiler and monitor sections
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 4m58s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 27m47s
ci / release (push) Successful in 11m57s
2026-10-22 14:41:24 +03:00
sanspie 3d25734dfa Stage 10d1: add-component requirements, play diff from history, node labels; one sound button on the web
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 / build (amd64, linux, ubuntu-24.04) (push) Failing after 1s
ci / checks (push) Failing after 1s
ci / release (push) Skipped
2026-10-22 13:29:11 +03:00
sanspie 12f628c469 Stage 10c3: platformer and top-down examples on the gameplay kit, new music 2026-10-22 12:16:37 +03:00
sanspie 1529213434 Stage 10c2: health, hitboxes, zones, respawn, pickups, lifetime, templates, StateMachine, gameplay editor tools (D313, D330, D332) 2026-10-22 10:53:54 +03:00
sanspie f8bb865920 Stage 10c1: shape casts and Motion2D (D312, D328, D329) 2026-10-22 03:01:46 +03:00
sanspie f5fb32565e Stage 10b3: desktop onboarding, app icon, clip inspector, asset grid audio cards, web audio and docs fixes (D327) 2026-10-22 03:01:46 +03:00
sanspie f1a8f538d3 Stage 10b: audio mixer, codecs, clips, events, sources, zones and mixer UI (D307–D311, D321–D326) 2026-10-21 20:33:25 +03:00
sanspie 963b01a011 Docs: Try it for every component, quieter design, self-hosted fonts, OS downloads, page meta 2026-10-21 19:20:52 +03:00
2480 changed files with 228444 additions and 7178 deletions
+4
View File
@@ -0,0 +1,4 @@
# Agents working on Aego
- Binaries go to `.cache/bin` only. Never run `go build` without `-o` in the repository root, and build test binaries only with `go test -c -o .cache/bin/<name>.test`. `make clean` empties `.cache/bin`; `make ci` fails when the root holds a binary or a `*.test` file (`tools/ci-clean-root.sh`).
- Run the full `make ci` before every push. Between pushes, run the checks that cover the change: the tests of the changed packages, `make lint`, `make depcheck`, the docs check and the golden tests when the UI changed.
+3
View File
@@ -0,0 +1,3 @@
# Claude configuration
This directory contains project-specific guidance and local configuration for Claude Code. The guidance records repository hygiene and verification expectations for agents working on Aego.
+3
View File
@@ -0,0 +1,3 @@
# CI workflows
The workflows in this directory automate checks, builds, tests, and release publishing for Aego. Optional platform-runner jobs cover Windows and macOS alongside the main Linux checks.
+6 -3
View File
@@ -9,15 +9,18 @@ permissions:
contents: read
env:
GLSLANG_APT_VERSION: 15.1.0-2~ubuntu0.24.04.2
LINUX_PKGS: >-
build-essential cmake ninja-build pkg-config
build-essential cmake ninja-build pkg-config openssl
libgl1-mesa-dev libegl1-mesa-dev libgles2-mesa-dev
libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev libxkbcommon-dev
libwayland-dev wayland-protocols libdecor-0-dev
libasound2-dev libpulse-dev libdrm-dev libgbm-dev libudev-dev libdbus-1-dev libibus-1.0-dev
jobs:
build:
# Native runs on Windows and macOS machines; the Windows build itself is cross-compiled on Linux in ci.yml (D301).
smoke:
name: ${{ matrix.goos }} ${{ matrix.goarch }} smoke
if: vars.PLATFORM_RUNNERS == 'true'
strategy:
fail-fast: false
@@ -39,7 +42,7 @@ jobs:
go-version-file: go.mod
- name: linux packages
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y $LINUX_PKGS
run: sudo apt-get update && sudo apt-get install -y $LINUX_PKGS "glslang-tools=$GLSLANG_APT_VERSION" && bash tools/check-glslang.sh
- name: msys2
if: runner.os == 'Windows'
id: msys2
+12 -4
View File
@@ -10,8 +10,9 @@ permissions:
contents: read
env:
GLSLANG_APT_VERSION: 15.1.0-2~ubuntu0.24.04.2
LINUX_PKGS: >-
build-essential cmake ninja-build pkg-config
build-essential cmake ninja-build pkg-config openssl
libgl1-mesa-dev libegl1-mesa-dev libgles2-mesa-dev
libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev libxkbcommon-dev
libwayland-dev wayland-protocols libdecor-0-dev
@@ -26,7 +27,7 @@ jobs:
with:
go-version-file: go.mod
- name: system packages
run: sudo apt-get update && sudo apt-get install -y $LINUX_PKGS
run: sudo apt-get update && sudo apt-get install -y $LINUX_PKGS "glslang-tools=$GLSLANG_APT_VERSION" && bash tools/check-glslang.sh
- uses: actions/cache@v4
continue-on-error: true
with:
@@ -37,6 +38,10 @@ jobs:
- run: go vet ./platform/sdl
- run: make lint
- run: make depcheck
- name: packaging tools
run: sudo apt-get install -y nsis squashfs-tools
- name: installer dry run
run: make nsis-check
build:
strategy:
@@ -57,7 +62,7 @@ jobs:
go-version-file: go.mod
- name: linux packages
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y $LINUX_PKGS
run: sudo apt-get update && sudo apt-get install -y $LINUX_PKGS "glslang-tools=$GLSLANG_APT_VERSION" && bash tools/check-glslang.sh
- name: msys2
if: runner.os == 'Windows'
id: msys2
@@ -123,6 +128,9 @@ jobs:
env:
CGO_ENABLED: "1"
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the key that signs release.json and the public half the binaries trust; without them no release.json is published (D362)
AEGO_RELEASE_KEY: ${{ secrets.AEGO_RELEASE_KEY }}
AEGO_UPDATE_KEY: ${{ vars.AEGO_UPDATE_KEY || secrets.AEGO_UPDATE_KEY }}
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
steps:
- uses: actions/checkout@v4
@@ -130,7 +138,7 @@ jobs:
with:
go-version-file: go.mod
- name: linux packages
run: sudo apt-get update && sudo apt-get install -y $LINUX_PKGS
run: sudo apt-get update && sudo apt-get install -y $LINUX_PKGS "glslang-tools=$GLSLANG_APT_VERSION" && bash tools/check-glslang.sh
- uses: actions/cache@v4
continue-on-error: true
with:
+9
View File
@@ -17,4 +17,13 @@ tools/genicons/genicons
/tests/golden/testdata/actual-*
tools/websmoke/node_modules/
/tools/websmoke/__pycache__/
tools/websmoke/golden/actual-*.png
# build output belongs in .cache/bin; these catch stray builds in the root
/aego-*
/i18n
*.prof
*.pprof
/data*.txt
*.key
+278 -33
View File
File diff suppressed because one or more lines are too long
+259 -33
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
# Aego — A 2D game engine built in Go.
Build games visually, write gameplay logic in Aego Script, and extend the engine with native Go.
A complete development environment with a visual editor, hot reload, deterministic simulation, frame-by-frame rewind, and cross-platform export. From your first scene to a playable game on Windows, Linux, macOS, and the web.
- Documentation and the playground: <https://aego.akarpov.ru>
- Installing and building from source: [engine/res/docs/install.md](engine/res/docs/install.md)
- Contributing, check levels and rules for engine code: [docs/contributing.md](docs/contributing.md)
- Architecture and decisions: [ARCHITECTURE.md](ARCHITECTURE.md)
+3
View File
@@ -0,0 +1,3 @@
# Animation
`anim` provides the shared animation kernel for clips, keyframe tracks, events, and controller graphs. It samples tracks, evaluates state transitions and blending, and encodes clips and controllers in text and binary forms for use by the engine and editor.
+238
View File
File diff suppressed because it is too large Load Diff
+234
View File
File diff suppressed because it is too large Load Diff
+138
View File
File diff suppressed because it is too large Load Diff
+149
View File
File diff suppressed because it is too large Load Diff
+146
View File
File diff suppressed because it is too large Load Diff
+340
View File
File diff suppressed because it is too large Load Diff
+109
View File
File diff suppressed because it is too large Load Diff
+98
View File
@@ -0,0 +1,98 @@
package anim
import (
"slices"
"testing"
)
func clip(loop LoopMode, ts ...float32) *Clip {
c := &Clip{Length: 1, Loop: loop}
for _, t := range ts {
c.Events = append(c.Events, Event{T: t, Name: "e"})
}
c.Fix()
return c
}
func evs(cs []Crossing) []int32 {
out := make([]int32, len(cs))
for i, c := range cs {
out[i] = c.Event
}
return out
}
func cycles(cs []Crossing) []int64 {
out := make([]int64, len(cs))
for i, c := range cs {
out[i] = c.Cycle
}
return out
}
func expect[T comparable](t *testing.T, name string, got, want []T) {
t.Helper()
if !slices.Equal(got, want) {
t.Fatalf("%s: got %v, want %v", name, got, want)
}
}
func TestOnce(t *testing.T) {
c := clip(LoopOnce, 0, 0.5, 1)
expect(t, "start", evs(Crossings(c, LoopOnce, 0, 0.6, true, nil)), []int32{0, 1})
expect(t, "end", evs(Crossings(c, LoopOnce, 0.6, 1.5, false, nil)), []int32{2})
expect(t, "after", evs(Crossings(c, LoopOnce, 1, 2, false, nil)), []int32{})
}
func TestZeroKeyEveryCycle(t *testing.T) {
c := clip(LoopRepeat, 0)
expect(t, "start", cycles(Crossings(c, LoopRepeat, 0, 0.5, true, nil)), []int64{0})
expect(t, "next", cycles(Crossings(c, LoopRepeat, 0.5, 1.5, false, nil)), []int64{1})
expect(t, "two", cycles(Crossings(c, LoopRepeat, 1.5, 3.2, false, nil)), []int64{2, 3})
}
func TestLengthKeyOncePerCycle(t *testing.T) {
c := clip(LoopRepeat, 1)
expect(t, "start", evs(Crossings(c, LoopRepeat, 0, 0.5, true, nil)), []int32{})
expect(t, "first", cycles(Crossings(c, LoopRepeat, 0.5, 1, false, nil)), []int64{0})
expect(t, "second", cycles(Crossings(c, LoopRepeat, 1, 2, false, nil)), []int64{1})
}
func TestSameMoment(t *testing.T) {
c := clip(LoopOnce, 0.5, 0.5)
expect(t, "forward", evs(Crossings(c, LoopOnce, 0, 1, false, nil)), []int32{0, 1})
expect(t, "backward", evs(Crossings(c, LoopOnce, 1, 0, false, nil)), []int32{0, 1})
}
func TestStillAndBackward(t *testing.T) {
c := clip(LoopRepeat, 0.2, 0.8)
expect(t, "speed 0", evs(Crossings(c, LoopRepeat, 0.3, 0.3, false, nil)), []int32{})
got := Crossings(c, LoopRepeat, 1.5, 0.1, false, nil)
expect(t, "backward events", evs(got), []int32{0, 1, 0})
expect(t, "backward cycles", cycles(got), []int64{1, 0, 0})
}
func TestManyCycles(t *testing.T) {
c := clip(LoopRepeat, 0.5)
expect(t, "three", cycles(Crossings(c, LoopRepeat, 0, 3, true, nil)), []int64{0, 1, 2})
}
func TestPingPongTurns(t *testing.T) {
c := clip(LoopPingPong, 0, 1)
got := Crossings(c, LoopPingPong, 0, 4, true, nil)
expect(t, "events", evs(got), []int32{0, 1, 0, 1, 0})
expect(t, "cycles", cycles(got), []int64{0, 0, 2, 2, 4})
mid := clip(LoopPingPong, 0.25)
got = Crossings(mid, LoopPingPong, 0, 2, true, nil)
if len(got) != 2 || got[0].T != 0.25 || got[1].T != 1.75 {
t.Fatalf("mid %+v", got)
}
}
func TestCrossingsNoAllocs(t *testing.T) {
c := clip(LoopRepeat, 0, 0.25, 0.5, 0.75)
buf := make([]Crossing, 0, 64)
if n := testing.AllocsPerRun(100, func() { buf = Crossings(c, LoopRepeat, 0.1, 3.9, false, buf) }); n != 0 {
t.Fatalf("Crossings allocates %v", n)
}
}
+332
View File
File diff suppressed because it is too large Load Diff
+221
View File
File diff suppressed because it is too large Load Diff
+87
View File
@@ -0,0 +1,87 @@
package anim
import (
"testing"
"aego/format/aetext"
)
const layoutSrc = `controller version=1 {
entry="Idle"
param "Speed" kind=float {}
state "Idle" {}
state "Run" {}
state "Jump" pos=[500, -40] {}
state "Lost" {}
transition "Idle" to="Run" duration=0.1 {
cond "Speed" op=gt value=0.1 {}
}
transition "Run" to="Idle" duration=0.1 {
cond "Speed" op=le value=0.1 {}
}
transition "any" to="Jump" {
cond "Speed" op=lt value=0 {}
}
}
`
func layoutGraph(t *testing.T) *Graph {
t.Helper()
doc, err := aetext.Parse([]byte(layoutSrc), "l.aectrl")
if err != nil {
t.Fatal(err)
}
g, probs, err := DecodeGraph(doc)
if err != nil || len(probs) > 0 {
t.Fatalf("%v %+v", err, probs)
}
return g
}
func TestLayoutPlacesEveryStateApart(t *testing.T) {
g := layoutGraph(t)
g.Layout()
seen := map[[2]float32]string{}
for _, s := range g.States {
if !s.HasPos {
t.Fatalf("%s has no position", s.Name)
}
if o, dup := seen[s.Pos]; dup {
t.Fatalf("%s and %s share %v", s.Name, o, s.Pos)
}
seen[s.Pos] = s.Name
}
idle, run, jump := g.States[0].Pos, g.States[1].Pos, g.States[2].Pos
if idle[0] != 0 || run[0] != LayoutColumn {
t.Fatalf("entry %v, next %v: the entry is the first column", idle, run)
}
if jump != [2]float32{500, -40} {
t.Fatalf("a written position moved to %v", jump)
}
}
func TestPosRoundTrips(t *testing.T) {
g := layoutGraph(t)
g.Layout()
text := aetext.Write(EncodeGraph(g))
doc, err := aetext.Parse(text, "l.aectrl")
if err != nil {
t.Fatal(err)
}
back, _, err := DecodeGraph(doc)
if err != nil {
t.Fatal(err)
}
bin, err := UnmarshalGraph(MarshalGraph(back))
if err != nil {
t.Fatal(err)
}
for i := range g.States {
if back.States[i].Pos != g.States[i].Pos || bin.States[i].Pos != g.States[i].Pos {
t.Fatalf("%s: text %v binary %v, want %v", g.States[i].Name, back.States[i].Pos, bin.States[i].Pos, g.States[i].Pos)
}
}
if _, err := UnmarshalGraph(MarshalGraph(back)[:len(MarshalGraph(back))-4]); err == nil {
t.Fatal("a truncated state was accepted")
}
}
+185
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
package anim
// RootDelta is the change of a root track between two clip times: a looping clip adds its end minus its start for every cycle crossed, a ping-pong clip only moves back and forth.
func RootDelta(c *Clip, s *Sampler, track int, loop LoopMode, t0, t1 float64) [2]float32 {
if !(c.Length > 0) || t0 == t1 || track < 0 || track >= len(c.Tracks) {
return [2]float32{}
}
u0, k0 := c.Local(loop, t0)
u1, k1 := c.Local(loop, t1)
a := s.Track(c, track, u0).L
b := s.Track(c, track, u1).L
d := [2]float32{b[0] - a[0], b[1] - a[1]}
if loop == LoopRepeat && k1 != k0 {
start := s.Track(c, track, 0).L
end := s.Track(c, track, c.Length).L
n := float32(k1 - k0)
d[0] += n * (end[0] - start[0])
d[1] += n * (end[1] - start[1])
}
return d
}
+59
View File
@@ -0,0 +1,59 @@
package anim
import (
"testing"
"aego/core/curve"
"aego/schema"
)
func walkClip(loop LoopMode) *Clip {
c := &Clip{Length: 1, Loop: loop, Tracks: []Track{{
Property: RootProp, Kind: schema.KindVec2, Root: true,
Keys: []Key{{T: 0, V: Lanes([4]float32{0, 0}), Interp: curve.InterpLinear}, {T: 1, V: Lanes([4]float32{1, 0.5})}},
}}}
c.Fix()
return c
}
func TestRootDelta(t *testing.T) {
var s Sampler
loop := walkClip(LoopRepeat)
for _, tc := range []struct {
t0, t1 float64
want [2]float32
}{
{0.25, 0.75, [2]float32{0.5, 0.25}},
{0.5, 1.5, [2]float32{1, 0.5}},
{0.25, 2.25, [2]float32{2, 1}},
{1.25, 0.25, [2]float32{-1, -0.5}},
{0.75, 1, [2]float32{0.25, 0.125}},
} {
d := RootDelta(loop, &s, 0, LoopRepeat, tc.t0, tc.t1)
if !near(d[0], tc.want[0]) || !near(d[1], tc.want[1]) {
t.Fatalf("loop %v..%v: %v, want %v", tc.t0, tc.t1, d, tc.want)
}
}
ping := walkClip(LoopPingPong)
if d := RootDelta(ping, &s, 0, LoopPingPong, 0.5, 1.5); !near(d[0], 0) || !near(d[1], 0) {
t.Fatalf("ping-pong turns back: %v", d)
}
once := walkClip(LoopOnce)
if d := RootDelta(once, &s, 0, LoopOnce, 0.75, 3); !near(d[0], 0.25) {
t.Fatalf("once stops at the end: %v", d)
}
if d := RootDelta(loop, &s, 0, LoopRepeat, 0.5, 0.5); d != ([2]float32{}) {
t.Fatalf("no time, no motion: %v", d)
}
}
func TestRootCheck(t *testing.T) {
c := walkClip(LoopRepeat)
if ps := c.Check(nil); len(ps) != 0 {
t.Fatalf("a good root track reports %+v", ps)
}
c.Tracks[0].Target = "Body"
if ps := c.Check(nil); len(ps) != 1 || ps[0].Kind != ProblemRoot {
t.Fatalf("a root track on a child: %+v", ps)
}
}
+146
View File
File diff suppressed because it is too large Load Diff
+142
View File
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
package anim
import (
"slices"
"strconv"
"strings"
"aego/format/aetext"
)
// shape lists the attributes, fields and child blocks a block of a clip or controller may hold.
type shape struct {
attrs []string
fields []string
blocks []string
}
var (
clipShape = shape{attrs: []string{"version", "length", "fps", "loop"}, blocks: []string{"track", "frames", "event"}}
trackShape = shape{attrs: []string{"prop", "kind", "numeric", "root", "authority", "onEnd"}, blocks: []string{"key"}}
keyShape = shape{attrs: []string{"t", "v", "interp", "tangent", "in", "out"}}
framesShape = shape{attrs: []string{"start", "fps", "prop", "flipProp", "authority"}, fields: []string{"sprites", "durations", "flipX"}}
eventShape = shape{attrs: []string{"t", "name", "payload", "mode"}}
ctrlShape = shape{attrs: []string{"version", "ease", "entryPos", "anyPos"}, fields: []string{"entry"}, blocks: []string{"param", "state", "transition"}}
paramShape = shape{attrs: []string{"kind", "default"}}
stateShape = shape{attrs: []string{"clip", "speed", "loop", "pos", "events"}}
transShape = shape{attrs: []string{"to", "duration", "exitTime", "interrupt"}, blocks: []string{"cond"}}
condShape = shape{attrs: []string{"op", "value"}}
)
// opening is how the block's first line reads with the attribute moved into it.
func opening(n *aetext.Node, f aetext.Field) string {
var b strings.Builder
b.WriteString(n.Head)
if n.Label != "" || n.Head == "frames" || n.Head == "track" {
b.WriteString(" " + strconv.Quote(n.Label))
}
b.WriteString(" " + f.Name + "=" + aetext.FormatValue(f.Value) + " {")
return b.String()
}
// strict refuses names a block does not know, so a misspelt or misplaced setting is never dropped silently.
func strict(n *aetext.Node, s shape) error {
for _, f := range n.Attrs {
switch {
case slices.Contains(s.attrs, f.Name):
case slices.Contains(s.fields, f.Name):
return textErr(n, f.Name+" is a field of "+n.Head+": write "+f.Name+"="+aetext.FormatValue(f.Value)+" inside the block")
default:
return textErr(n, "unknown attribute "+f.Name+" of "+n.Head+"; it takes "+strings.Join(s.attrs, ", "))
}
}
for _, f := range n.Fields {
switch {
case slices.Contains(s.fields, f.Name):
case slices.Contains(s.attrs, f.Name):
return textErr(n, f.Name+" is an attribute of "+n.Head+": write `"+opening(n, f)+"`")
case len(s.fields) == 0:
return textErr(n, "unknown field "+f.Name+": "+n.Head+" takes no fields")
default:
return textErr(n, "unknown field "+f.Name+" of "+n.Head+"; it takes "+strings.Join(s.fields, ", "))
}
}
for _, b := range n.Blocks {
if !slices.Contains(s.blocks, b.Head) {
if len(s.blocks) == 0 {
return textErr(b, n.Head+" holds no blocks, found "+b.Head)
}
return textErr(b, "unknown block "+b.Head+" in "+n.Head+"; it holds "+strings.Join(s.blocks, ", "))
}
}
return nil
}
+60
View File
@@ -0,0 +1,60 @@
package anim
import (
"strings"
"testing"
"aego/format/aetext"
)
func decodeText(t *testing.T, src string, ctrl bool) error {
t.Helper()
n, err := aetext.Parse([]byte(src), "t")
if err != nil {
t.Fatal(err)
}
if ctrl {
_, _, err = DecodeGraph(n)
} else {
_, err = Decode(n)
}
return err
}
// Every block of a clip refuses names it does not know instead of dropping them.
func TestClipRefusesUnknownNames(t *testing.T) {
cases := []struct{ name, src, want string }{
{"root", `anim version=1 length=1 speed=2 {}`, "unknown attribute speed of anim"},
{"root block", "anim version=1 length=1 {\n curve \"\" {}\n}\n", "unknown block curve"},
{"track", "anim version=1 length=1 {\n track \"\" prop=\"SpriteRenderer.size\" kind=vec2 loop=true {}\n}\n", "unknown attribute loop of track"},
{"key", "anim version=1 length=1 {\n track \"\" prop=\"SpriteRenderer.size\" kind=vec2 {\n key t=0 v=[1, 1] ease=linear {}\n }\n}\n", "unknown attribute ease of key"},
{"frames", "anim version=1 length=1 {\n frames \"Slash\" {\n sprites=[asset(\"ae900000000000000000000000000b01\")]\n start=0.08\n }\n}\n", "start is an attribute of frames: write `frames \"Slash\" start=0.08 {`"},
{"frames field", "anim version=1 length=1 {\n frames \"\" {\n sprites=[asset(\"ae900000000000000000000000000b01\")]\n duration=[0.1]\n }\n}\n", "unknown field duration of frames"},
{"event", "anim version=1 length=1 {\n event t=0.1 name=\"Hit\" when=1 {}\n}\n", "unknown attribute when of event"},
{"event field", "anim version=1 length=1 {\n event t=0.1 name=\"Hit\" {\n payload=1\n }\n}\n", "payload is an attribute of event"},
}
for _, c := range cases {
err := decodeText(t, c.src, false)
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: got %v, want %q", c.name, err, c.want)
}
}
}
// Every block of a controller refuses names it does not know instead of dropping them.
func TestControllerRefusesUnknownNames(t *testing.T) {
cases := []struct{ name, src, want string }{
{"root", "controller version=1 start=\"Idle\" {\n state \"Idle\" {}\n}\n", "unknown attribute start of controller"},
{"root field", "controller version=1 {\n ease=linear\n state \"Idle\" {}\n}\n", "ease is an attribute of controller"},
{"param", "controller version=1 {\n param \"Speed\" kind=float min=0 {}\n}\n", "unknown attribute min of param"},
{"state", "controller version=1 {\n state \"Idle\" clipp=asset(\"a0a10000000000000000000000000005\") {}\n}\n", "unknown attribute clipp of state"},
{"transition", "controller version=1 {\n state \"A\" {}\n state \"B\" {}\n transition \"A\" to=\"B\" exit=1 {}\n}\n", "unknown attribute exit of transition"},
{"cond", "controller version=1 {\n param \"Speed\" kind=float {}\n state \"A\" {}\n state \"B\" {}\n transition \"A\" to=\"B\" {\n cond \"Speed\" op=gt val=1 {}\n }\n}\n", "unknown attribute val of cond"},
}
for _, c := range cases {
err := decodeText(t, c.src, true)
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: got %v, want %q", c.name, err, c.want)
}
}
}
+521
View File
File diff suppressed because it is too large Load Diff
+87
View File
@@ -0,0 +1,87 @@
package anim
import (
"aego/core/ids"
"aego/format/aetext"
"aego/schema"
)
type Value struct {
L [4]float32
I int64
A ids.AssetID
}
func Float(f float32) Value { return Value{L: [4]float32{f}} }
func Lanes(l [4]float32) Value { return Value{L: l} }
func Int(i int64) Value { return Value{I: i} }
func Asset(id ids.AssetID) Value { return Value{A: id} }
func (v Value) Truth() bool { return v.I != 0 }
func Bool(b bool) Value {
if b {
return Value{I: 1}
}
return Value{}
}
type class uint8
const (
classNone class = iota
classLanes
classInt
classDiscrete
classQuat
classAsset
)
func classOf(k schema.Kind) class {
switch k {
case schema.KindFloat32, schema.KindFloat64, schema.KindVec2, schema.KindVec3, schema.KindVec4, schema.KindColor, schema.KindRect:
return classLanes
case schema.KindInt32, schema.KindInt64:
return classInt
case schema.KindBool, schema.KindEnum:
return classDiscrete
case schema.KindQuat:
return classQuat
case schema.KindAssetRef:
return classAsset
}
return classNone
}
func lanesOf(k schema.Kind) int {
switch k {
case schema.KindFloat32, schema.KindFloat64:
return 1
case schema.KindVec2:
return 2
case schema.KindVec3:
return 3
}
return 4
}
func Supported(k schema.Kind) bool {
return classOf(k) != classNone
}
func Interpolates(k schema.Kind) bool {
c := classOf(k)
return c == classLanes || c == classQuat
}
// LaneCount is the number of float lanes of a lane kind, 0 for discrete kinds.
func LaneCount(k schema.Kind) int {
if classOf(k) != classLanes && classOf(k) != classQuat {
return 0
}
return lanesOf(k)
}
// ValueText writes v the way a key stores it in .aeanim.
func ValueText(kind schema.Kind, v Value) aetext.Value {
return writeValue(kind, v)
}
+3
View File
@@ -0,0 +1,3 @@
# app
The `app` packages assemble the engine, modules, asset pipeline, and scripting workspace into development hosts. They coordinate workflows such as project creation, editing, running, and exporting outside the engine core.
+549
View File
File diff suppressed because it is too large Load Diff
+207
View File
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
package agent
import (
"strconv"
"strings"
"testing"
)
func tree(depth, fan int) NodeView {
v := NodeView{Name: "n"}
if depth > 0 {
for i := 0; i < fan; i++ {
v.Children = append(v.Children, tree(depth-1, fan))
}
}
return v
}
func TestCapTreeKeepsTheFirstNodes(t *testing.T) {
v := tree(4, 4)
all := nodeCount(&v)
left := capTree(&v, 50, 0)
if kept := nodeCount(&v); kept != 50 || kept+left != all {
t.Fatalf("kept %d and left %d of %d", kept, left, all)
}
more := 0
var walk func(n *NodeView)
walk = func(n *NodeView) {
more += n.More
for i := range n.Children {
walk(&n.Children[i])
}
}
walk(&v)
if more != left {
t.Fatalf("the more counts add up to %d, %d were left out", more, left)
}
v = tree(4, 2)
if left := capTree(&v, 0, 2); nodeCount(&v) != 7 || left != 31-7 {
t.Fatalf("depth 2 kept %d nodes", nodeCount(&v))
}
}
func TestScriptWindow(t *testing.T) {
var b strings.Builder
for i := 1; i <= 1000; i++ {
b.WriteString("line " + strconv.Itoa(i) + "\n")
}
text := b.String()
if got, note := window(text, 0, 0, 2000); got != text || note != "" {
t.Fatal("a short script is not read whole")
}
got, note := window(text, 0, 0, 400)
if !strings.HasPrefix(got, "line 1\n") || !strings.HasSuffix(got, "line 400\n") || !strings.Contains(note, "1-400 of 1001") {
t.Fatalf("the first window is wrong: %q", note)
}
got, _ = window(text, 600, 620, 400)
if !strings.HasPrefix(got, "line 600\n") || !strings.HasSuffix(got, "line 620\n") {
t.Fatalf("the asked range is wrong: %q", got[:20])
}
}
+7
View File
@@ -2,6 +2,7 @@ package agent
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
@@ -47,6 +48,12 @@ func Main(p *project.Project, args []string, stdout, stderr io.Writer, opt Optio
fmt.Fprintln(stderr, "usage: aego "+c.Usage())
return 2
}
if addr, token, ok := EditorProxy(p.Root); ok {
out, err := proxyRun(addr, token, c, a)
if !errors.As(err, new(errProxyDown)) {
return report(stdout, stderr, f, out, err)
}
}
s, err := Open(p, opt)
if err != nil {
return report(stdout, stderr, f, nil, err)
+51 -4
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+60
View File
@@ -0,0 +1,60 @@
package mcp
import (
"bufio"
"encoding/json"
"io"
"aego/app/agent"
)
// Relay passes an outside agent's MCP session to the open editor, so its changes become the editor's Undo steps, named after the client.
func Relay(in io.Reader, out io.Writer, addr, token string) error {
client := "agent"
sc := bufio.NewScanner(in)
sc.Buffer(make([]byte, 0, 1<<20), maxBody)
for sc.Scan() {
line := sc.Bytes()
if len(line) == 0 {
continue
}
var msg map[string]any
if err := json.Unmarshal(line, &msg); err != nil {
continue
}
params, _ := msg["params"].(map[string]any)
switch msg["method"] {
case "initialize":
if info, ok := params["clientInfo"].(map[string]any); ok {
if name, ok := info["name"].(string); ok && name != "" {
client = name
}
}
case "tools/call":
if params != nil {
args, _ := params["arguments"].(map[string]any)
if args == nil {
args = map[string]any{}
}
args[agent.ViaArg] = "via agent: " + client
params["arguments"] = args
}
}
body, _ := json.Marshal(msg)
rep, err := agent.ProxyPost(addr, token, body)
if err != nil {
id, ok := msg["id"]
if !ok {
continue
}
rep, _ = json.Marshal(map[string]any{"jsonrpc": "2.0", "id": id, "error": map[string]any{"code": -32000, "message": "the editor closed; start the MCP server again: " + err.Error()}})
}
if len(rep) == 0 {
continue
}
if _, err := out.Write(append(rep, '\n')); err != nil {
return err
}
}
return sc.Err()
}
+102 -36
View File
File diff suppressed because it is too large Load Diff
+68
View File
@@ -0,0 +1,68 @@
package mcp
import (
"aego/app/agent"
"aego/project"
)
// Source runs the tool catalog: the CLI opens a session per call, the editor runs on its own documents (D341).
type Source interface {
Run(name string, a agent.Args) (*agent.Output, error)
Paths() ([]string, error)
Project() *project.Project
Rel(p string) (string, error)
}
// Extra is a resource a Source adds to the list, such as the editor's selection.
type Extra struct {
URI string
Name string
Description string
MimeType string
Read func() (string, error)
}
// Tool is a tool the host adds next to the agent commands, such as the editor's ask_user.
type Tool struct {
Name string
Description string
Schema map[string]any
ReadOnly bool
// Run answers the call; it may block while the user decides.
Run func(args map[string]any) (text string, failed bool)
Output func(map[string]any) (*agent.Output, error)
}
type kitSource struct {
kit *agent.Kit
}
func (k kitSource) Run(name string, a agent.Args) (*agent.Output, error) {
sess, err := k.kit.Open()
if err != nil {
return nil, err
}
defer sess.Close()
return agent.Run(sess, name, a)
}
func (k kitSource) Paths() ([]string, error) {
sess, err := k.kit.Open()
if err != nil {
return nil, err
}
defer sess.Close()
var out []string
for _, a := range sess.Pipeline.Assets() {
out = append(out, a.Path)
}
return out, nil
}
func (k kitSource) Project() *project.Project {
return k.kit.Project
}
func (k kitSource) Rel(p string) (string, error) {
return k.kit.Rel(p)
}
+221
View File
File diff suppressed because it is too large Load Diff
+101
View File
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
package agent
import (
"os"
"path/filepath"
"testing"
"aego/modules/render2d"
"aego/scene/mutate"
)
type postCameraHost struct {
Host
doc *mutate.Document
edits int
}
func (h *postCameraHost) OpenScene(string) (*mutate.Document, bool) { return h.doc, true }
func (h *postCameraHost) ApplyScene(_ string, _ string, tx mutate.Tx) (*mutate.Result, error) {
r, err := h.doc.Apply(tx)
if err == nil {
h.edits++
}
return r, err
}
func TestPostEffectCommandTargetsDirtyCameraByStableID(t *testing.T) {
s := materialSession(t)
const file = "scenes/test.aescene"
disk := []byte(`scene version=1 {
node "43980000000000000000000000000001" name="Camera" {
component "engine.Camera2D" version=1 {}
component "render2d.PostProcess" version=1 { enabled=true effects=[{typeID="postfx.bloom" instanceID="first" enabled=true version=1 parameters=[]}] }
}
}`)
full := filepath.Join(s.Project.Root, file)
if err := os.WriteFile(full, disk, 0644); err != nil {
t.Fatal(err)
}
_, _, doc, err := s.sceneDisk(file)
if err != nil {
t.Fatal(err)
}
if _, err = doc.Apply(mutate.Tx{Label: "Unsaved rename", Ops: []mutate.Op{mutate.Rename("Camera", "UnsavedCamera")}}); err != nil {
t.Fatal(err)
}
h := &postCameraHost{doc: doc}
s.host = h
a := Args{"file": file, "node": "UnsavedCamera", "instance": "first", "field": "Intensity", "value": "0.37", "dry-run": true}
if _, err := Run(s, "postfx.set", a); err != nil || h.edits != 0 {
t.Fatalf("dry run: %v / %d", err, h.edits)
}
delete(a, "dry-run")
if _, err := Run(s, "postfx.set", a); err != nil || h.edits != 1 {
t.Fatalf("apply: %v / %d", err, h.edits)
}
o, err := Run(s, "postfx.get", Args{"file": file, "node": "UnsavedCamera"})
if err != nil {
t.Fatal(err)
}
stack := o.Value.(render2d.PostProcess)
e, ok := stack.Effect("first")
if !ok || len(e.Parameters) != 1 || e.Parameters[0].Value.X != 0.37 {
t.Fatal(stack)
}
for _, bad := range []string{"-1", "true", "[1,2]"} {
a["value"] = bad
if _, err := Run(s, "postfx.set", a); err == nil || h.edits != 1 {
t.Fatalf("invalid %q wrote scene", bad)
}
}
a["instance"] = "deleted"
if _, err := Run(s, "postfx.set", a); err == nil {
t.Fatal("missing instance accepted")
}
if b, err := os.ReadFile(full); err != nil || string(b) != string(disk) {
t.Fatal("saved document changed")
}
}
+49 -4
View File
@@ -22,8 +22,8 @@ func prefabCommands() []*Command {
{Name: "parent", Type: "string", Doc: "parent node, the root by default"}, {Name: "name", Type: "string", Doc: "name of the instance"}, argIndex}, Run: (*Session).prefabInstantiate},
{Name: "prefab.apply", Doc: "write instance overrides into the prefab file", Write: true,
Args: []Arg{argFile, argNode, level, owner, field, {Name: "nearest", Type: "boolean", Doc: "apply to the nearest nested prefab"}}, Run: (*Session).prefabApply},
{Name: "prefab.revert", Doc: "drop instance overrides", Write: true, Args: []Arg{argFile, argNode, level, owner, field}, Run: (*Session).prefabRevert},
{Name: "prefab.unpack", Doc: "turn an instance into plain nodes", Write: true,
{Name: "prefab.revert", Doc: "drop instance overrides", Write: true, Destroys: true, Args: []Arg{argFile, argNode, level, owner, field}, Run: (*Session).prefabRevert},
{Name: "prefab.unpack", Doc: "turn an instance into plain nodes", Write: true, Destroys: true,
Args: []Arg{argFile, argNode, {Name: "deep", Type: "boolean", Doc: "unpack nested prefabs too"}}, Run: func(s *Session, a Args) (*Output, error) {
return s.sceneTx(a, "Unpack Prefab", mutate.Unpack(a.Str("node"), a.Bool("deep")))
}},
@@ -44,7 +44,10 @@ func levelOf(a Args, def string) (prefab.Level, error) {
}
func (s *Session) prefabCreate(a Args) (*Output, error) {
rel, before, doc, err := s.scene(a)
if out, ok, err := s.prefabCreateOpen(a); ok {
return out, err
}
rel, before, doc, err := s.sceneFile(a)
if err != nil {
return nil, err
}
@@ -81,6 +84,48 @@ func (s *Session) prefabCreate(a Args) (*Output, error) {
return &Output{Changes: res.Changes, Problems: res.Problems, Created: []string{id.String()}, Files: append([]string{out}, files...)}, err
}
// prefabCreateOpen makes the prefab through the editor when the scene is open there, so unsaved edits count; false when there is no such editor or scene.
func (s *Session) prefabCreateOpen(a Args) (*Output, bool, error) {
ph, ok := s.host.(PrefabHost)
if !ok {
return nil, false, nil
}
rel, err := s.rel(a.Str("file"))
if err != nil {
return nil, true, err
}
doc, open := s.host.OpenScene(rel)
if !open {
return nil, false, nil
}
out, err := s.rel(a.Str("out"))
if err != nil {
return nil, true, err
}
if path.Ext(out) != PrefabExt {
out += PrefabExt
}
if s.exists(out) {
return nil, true, errors.New(out + " already exists")
}
h, err := doc.Node(a.Str("node"))
if err != nil {
return nil, true, err
}
if err := refactor.CheckCreate(doc.Scene, h); err != nil {
return nil, true, err
}
if a.Bool("dry-run") {
return &Output{DryRun: true, Notes: []string{"would create " + out, "would link the node in " + rel + " as an Undo step"}}, true, nil
}
id, err := ph.CreatePrefab(rel, a.Str("node"), out)
if err != nil {
return nil, true, err
}
s.reload()
return &Output{OK: true, Created: []string{id.String()}, Files: []string{out}}, true, nil
}
func (s *Session) prefabInstantiate(a Args) (*Output, error) {
op := mutate.Instantiate(a.Str("parent"), a.Str("prefab"))
op.Name = a.Str("name")
@@ -95,7 +140,7 @@ func (s *Session) prefabInstantiate(a Args) (*Output, error) {
}
func (s *Session) prefabApply(a Args) (*Output, error) {
rel, before, doc, err := s.scene(a)
rel, before, doc, err := s.sceneFile(a)
if err != nil {
return nil, err
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More