Author SHA1 Message Date
sanspie d8ad92166c test: diagnose browser scheduling on audio underruns
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 13m14s
ci / release (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci / checks (push) Failing after 58m31s
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
2026-11-10 14:56:15 +03:00
sanspie 867efcf2f8 test: synchronize idle allocations and camera probe
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 44m58s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 12m46s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
2026-11-10 13:15:15 +03:00
sanspie bd04ec871e ci: serialize gates and settle allocation checks
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 40m46s
ci-platforms / darwin amd64 smoke (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 7m19s
ci / release (push) Skipped
2026-11-10 11:37:47 +03:00
sanspie 442d321f73 test: settle agent availability before budget checks
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 18m0s
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 43m34s
ci / release (push) Skipped
2026-11-10 10:18:19 +03:00
sanspie 9e20b59d00 editor: cull clipped agent transcript lines
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
ci-platforms / darwin amd64 smoke (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 9m5s
github-native / gitea-pending (push) Skipped
GitHub / darwin arm64 native GitHub-hosted native CI: failure
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 47m1s
ci / release (push) Skipped
2026-11-10 10:03:23 +03:00
sanspie 4a7696922a ci: report failing integration race tests
GitHub / darwin arm64 native GitHub-hosted native CI: success
ci-platforms / darwin amd64 smoke (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 9m28s
ci / checks (push) Failing after 56m1s
ci / release (push) Skipped
2026-11-10 09:38:04 +03:00
sanspie 1239bb1542 fix: refresh linked assets after art save
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 9m21s
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin arm64 native GitHub-hosted native CI: success
github-native / gitea-pending (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci / release (push) Skipped
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / checks (push) Failing after 43m10s
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
2026-11-10 09:33:15 +03:00
sanspie 2f613bc927 fix: keep art tools accessible in maximized canvas
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 20m43s
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / checks (push) Failing after 54m14s
ci / release (push) Skipped
2026-11-10 09:16:25 +03:00
sanspie c6682c21b7 fix: register physics icon at application boundary
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 14m11s
ci / checks (push) Failing after 37m52s
ci / release (push) Skipped
2026-11-10 03:05:05 +03:00
sanspie 6ae67ba9f4 test: separate isolated play from autosave
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / gitea-result (push) Skipped
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 8m29s
github-native / gitea-pending (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
ci / checks (push) Failing after 28m57s
ci / release (push) Skipped
2026-11-10 02:53:01 +03:00
sanspie 104051ea96 ci: isolate native integration test execution
ci-platforms / darwin arm64 smoke (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 10m33s
ci / checks (push) Failing after 30m26s
ci / release (push) Skipped
2026-11-10 02:44:24 +03:00
sanspie 768a59fdd6 editor: show documentation navigation icons
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 25m31s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 9m28s
ci / release (push) Skipped
2026-11-10 02:20:58 +03:00
sanspie a636e1b7af test: wait for recovery actions after restore
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m13s
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 41m6s
ci / release (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin arm64 native GitHub-hosted native CI: success
2026-11-10 02:03:29 +03:00
sanspie c3c6efdc71 fix: synchronize localization catalogs
ci / release (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 18m33s
ci / checks (push) Failing after 59m31s
2026-11-10 01:57:44 +03:00
sanspie aa76880358 test: observe stable frame during GPU playback capture
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / checks (push) Failing after 20m20s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m58s
ci / release (push) Skipped
2026-11-10 01:36:42 +03:00
sanspie 703510fb19 fix: reconcile generated art sub-assets correctly
ci / checks (push) Failing after 19m33s
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: success
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 16m43s
ci / release (push) Skipped
2026-11-10 01:31:25 +03:00
sanspie b0e3dc7c9d editor: add interactive alpha picker
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
github-native / gitea-pending (push) Skipped
ci / release (push) Skipped
GitHub / darwin amd64 native GitHub-hosted native CI: failure
ci / checks (push) Failing after 20m3s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 21m13s
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
2026-11-10 01:14:09 +03:00
sanspie 91090d27c4 fix: use precise Windows audio mix timing
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
ci / checks (push) Failing after 13m5s
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / release (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m6s
2026-11-10 01:03:49 +03:00
sanspie 7fb4acc6e4 editor: size art timeline to its controls
ci-platforms / darwin amd64 smoke (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m21s
ci / checks (push) Failing after 21m35s
ci / release (push) Skipped
GitHub / linux amd64 native GitHub-hosted native CI: success
2026-11-10 00:57:53 +03:00
sanspie db61be8ee4 fix: keep studio snapshot checks from writing bytecode
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 15m18s
ci / release (push) Skipped
ci / checks (push) Failing after 17m29s
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
2026-11-10 00:46:43 +03:00
sanspie f5198afa22 fix: resolve NSIS helper path on Windows
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 13m39s
ci / checks (push) Failing after 18m22s
ci / release (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
2026-11-09 20:24:51 +03:00
sanspie ff4e28f100 fix: audit module icons at application boundary
ci-platforms / darwin arm64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m42s
ci / checks (push) Failing after 20m16s
ci / release (push) Skipped
Keep the particles icon in the composed application registry while preserving the full editor icon-use scan, and update its generated inventory.
2026-11-09 16:43:10 +03:00
sanspie b0080132a7 test: refresh documentation navigation screenshots
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Failing after 8m27s
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 9m33s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
Update dark and light references for the intentional Assets and Art Studio navigation. Before/after comparison confirms that the page content is unchanged; screenshot tolerances remain the same.
2026-11-09 16:29:38 +03:00
sanspie fac6fa35a4 fix: stabilize CI and prepare docs release
ci-platforms / darwin arm64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 7m29s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 14m13s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: error
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
Keep recovery copies private on Windows, make cross-platform test fixtures deterministic, and preserve the updated Art and documentation contracts.

Refresh workspace screenshot references for the intentional factory layouts and restore the earlier Russian palette reference after before/after comparison. Keep the existing screenshot tolerances.
2026-11-09 16:24:24 +03:00
sanspie 617c897f59 test: cover narrow art timeline controls
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 8m43s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 6m26s
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
2026-11-09 14:26:48 +03:00
sanspie 60b3ed5bc0 editor: refine art workspace and update documentation 2026-11-09 14:25:23 +03:00
sanspie a46347e2c6 editor: add art studio and stage 12.5 workflows
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m19s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 23m57s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
2026-11-08 18:47:57 +03:00
sanspie ee235bbf14 ci: mirror verified Gitea releases to GitHub
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 6m1s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 11m35s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
2026-11-07 01:12:53 +03:00
sanspie 216bdbb19d test: measure agent allocations after previews settle 2026-11-07 01:11:28 +03:00
sanspie a54ce4902a fix: make native tests portable across Windows
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Successful in 6m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 13m11s
ci / release (push) Skipped
2026-11-07 00:54:36 +03:00
sanspie 2f768d4166 fix: preserve native export and updater checks
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci / checks (push) Successful in 7m12s
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 14m42s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
2026-11-06 21:04:10 +03:00
sanspie a0107b622a ci: report native GitHub results to Gitea
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 12m49s
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Successful in 6m36s
ci / release (push) Skipped
2026-11-06 20:43:11 +03:00
sanspie eb937b4470 ci: route mirror jobs safely and build pinned validator
ci / checks (push) Successful in 5m35s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
ci / release (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 13m0s
2026-11-06 20:33:00 +03:00
sanspie 1d7cee7cc7 test: update light scene preview goldens
ci-platforms / windows amd64 smoke (push) Skipped
github-native / linux amd64 native (push) Skipped
ci / checks (push) Successful in 6m34s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 13m53s
ci / release (push) Skipped
github-native / darwin amd64 native (push) Canceled after 0s
github-native / windows amd64 native (push) Canceled after 0s
github-native / darwin arm64 native (push) Canceled after 0s
2026-11-06 17:11:45 +03:00
sanspie 85d9efc67b ci: make repository paths portable to Windows 2026-11-06 17:11:18 +03:00
sanspie ab6ea7ef58 editor: improve scene tools and prepare native CI
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / linux amd64 native (push) Skipped
ci / checks (push) Successful in 6m33s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m26s
ci / release (push) Skipped
github-native / darwin amd64 native (push) Canceled after 0s
github-native / windows amd64 native (push) Canceled after 0s
github-native / darwin arm64 native (push) Canceled after 0s
2026-11-06 16:49:11 +03:00
sanspie 9c02975882 engine: cache frame pacer capability
ci-platforms / windows amd64 smoke (push) Skipped
ci / checks (push) Successful in 5m59s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 8m7s
ci / release (push) Skipped
2026-11-06 15:47:34 +03:00
sanspie 6a148e63e0 ci: pause releases and fix profiler lint
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 11m27s
ci / release (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci / checks (push) Successful in 5m41s
2026-11-06 15:20:17 +03:00
sanspie 48d1eabef2 profiler: report actual FPS and idle waits
ci / checks (push) Failing after 5m21s
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 7m49s
ci / release (push) Skipped
2026-11-06 12:27:31 +03:00
1141 changed files with 104352 additions and 6331 deletions
+5
View File
@@ -0,0 +1,5 @@
# Golden files and project sources are byte-stable across platforms.
* text=auto eol=lf
# Preserve the upstream font license exactly as committed (it uses CRLF).
tools/gendocs/site/static/fonts/IBM-Plex-OFL.txt -text
+39 -1
View File
@@ -1,3 +1,41 @@
# 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.
Gitea is the source of truth. The Linux workflow runs there; optional Gitea
platform runners remain controlled by `PLATFORM_RUNNERS`.
`github-native.yml` is for the GitHub mirror. It builds and tests on native
Windows, macOS (arm64 and amd64), and Linux runners, and uploads unsigned CI
artifacts and diagnostic logs. The Windows installer smoke is restricted to an
ephemeral GitHub-hosted runner. macOS bundles are ad-hoc signed, not notarized.
These jobs are not evidence of platform support until they actually pass on
their respective runners.
On Gitea, the GitHub-only matrix routes to the Linux runner before being
skipped. This prevents Windows and macOS entries from waiting indefinitely for
runners that Gitea does not have. On GitHub, the actual native labels are used.
macOS and Windows build the pinned GLSL reference validator from verified
source, rather than depending on a changing package-manager version.
To show native GitHub results on the Gitea commit, create the GitHub Actions
secret `GITEA_STATUS_TOKEN` containing a Gitea API token with repository write
permission. Prefer a non-admin bot that has access only to `sanspie/aego`:
Gitea's repository-write token is broader than commit statuses. The reporter
publishes pending and final statuses with links to the real GitHub jobs only
for trusted `master` pushes or manual runs, never for pull requests. If the
secret is missing, CI still runs but no Gitea statuses are published.
Keep GitHub Actions disabled through the first mirror sync: historical tags
can contain older workflows. Before enabling Actions, verify mirrored refs and
restrict the allowed workflows so historical release jobs cannot run. Use a
separate repository-scoped credential for the mirror; do not put it in a URL.
Gitea's build-and-publish job remains paused. `github-release-mirror.yml`
copies already-published Gitea releases and assets to GitHub after the current
master passes Gitea Linux CI and all four real GitHub native jobs. It also
checks periodically and can be dispatched manually from master. Versioned
stable releases are published normally, with the highest stable version marked
Latest; nightly, beta versions, and the stable-channel pointer are prereleases.
The mirror verifies matching git refs and asset sizes/checksums, stages new
releases as drafts, and never moves tags or replaces existing assets. If a
mutable nightly or another mirrored snapshot changes, the mirror stops rather
than overwriting it; a separate reviewed update is needed in that case. Signed
update manifests are copied unchanged and retain their original download URLs.
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
# 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'
if: github.server_url != 'https://github.com' && vars.PLATFORM_RUNNERS == 'true'
strategy:
fail-fast: false
matrix:
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,37 @@
name: github-release-mirror
on:
workflow_run:
workflows: [github-native]
types: [completed]
branches: [master]
schedule:
- cron: '17 */6 * * *'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: github-release-mirror
cancel-in-progress: false
jobs:
mirror:
if: github.server_url == 'https://github.com' && github.repository == 'Alexander-D-Karpov/aego' && github.ref == 'refs/heads/master'
runs-on: ubuntu-24.04
timeout-minutes: 60
permissions:
contents: write
actions: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: refs/heads/master
persist-credentials: false
- name: Test release mirror
run: python3 -m unittest discover -s tools/ci -p mirror_releases_test.py -v
- name: Mirror published Gitea releases
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 tools/ci/mirror_releases.py --apply
+79 -11
View File
File diff suppressed because it is too large Load Diff
+97
View File
@@ -0,0 +1,97 @@
package agent
import (
"fmt"
"strconv"
"strings"
"aego/art"
)
// ArtHost is supplied only by the running editor. Offline commands must not
// reopen files and silently replace the user's dirty graphical documents.
// Like Host.ApplyScene, these calls run on the editor mutation thread.
type ArtHost interface {
ReadArt(file, section string, offset, limit int) (*Output, error)
CaptureArt(file string, frame art.ID, channel art.Channel) (*Output, error)
PrepareArt(file string, revision uint64, label string, ops []art.Operation) (*Output, error)
}
// ArtProposal is an opaque, prepared transaction carried by the existing agent
// proposal queue. Commit is invoked only after its normal approval decision.
// Keeping the prepared transaction also preserves IDs returned by its preview.
type ArtProposal interface {
Commit() (*Output, error)
RetainedBytes() int64
}
func artHost(s *Session) (ArtHost, error) {
if s != nil {
if h, ok := s.host.(ArtHost); ok {
return h, nil
}
}
return nil, fmt.Errorf("art: connect to the running Aego editor; offline writes to graphical documents are disabled")
}
func artCommands() []*Command {
fileArg := Arg{Name: "file", Type: "string", Doc: "Project-relative .aeart source, including unsaved editor content", Required: true, Pos: true}
return []*Command{
{Name: "art.read", Doc: "Read the current art document, not its last saved file. Lists are paginated; revision is a decimal string for art.apply.", Args: []Arg{
fileArg, {Name: "section", Type: "string", Doc: "summary, layers, frames, cels, palette, tags or slices"},
{Name: "offset", Type: "integer", Doc: "First item, default 0"}, {Name: "limit", Type: "integer", Doc: "1..1024 items; default 256"},
}, Run: func(s *Session, a Args) (*Output, error) {
h, err := artHost(s)
if err != nil {
return nil, err
}
offset, err := a.Int("offset", 0)
if err != nil {
return nil, err
}
limit, err := a.Int("limit", 256)
if err != nil {
return nil, err
}
if offset < 0 || limit < 1 || limit > 1024 {
return nil, fmt.Errorf("art: invalid page")
}
return h.ReadArt(a.Str("file"), a.Str("section"), offset, limit)
}},
{Name: "art.capture", Doc: "Capture unsaved CPU pixels with document/frame/channel identity and revision. MCP returns a PNG image; the CLI writes it only with --out to a new file.", Args: []Arg{
fileArg, {Name: "frame", Type: "string", Doc: "Stable frame ID; defaults to current frame"},
{Name: "channel", Type: "string", Doc: "albedo, normal, height or emission; defaults to current channel"},
}, Run: func(s *Session, a Args) (*Output, error) {
h, err := artHost(s)
if err != nil {
return nil, err
}
return h.CaptureArt(a.Str("file"), art.ID(a.Str("frame")), art.Channel(a.Str("channel")))
}},
{Name: "art.apply", Doc: "Propose one atomic graphics edit at the revision returned by art.read. Agent panel edits require approval; CLI and external agents follow the editor's confirmation setting. All use the document's common Undo. Name new entities with as and refer to them as $name later.", Write: true, Destroys: true, Args: []Arg{
fileArg, {Name: "revision", Type: "string", Doc: "Exact decimal revision returned by art.read", Required: true},
{Name: "label", Type: "string", Doc: "Human-readable operation label", Required: true},
{Name: "operations", Type: "array", Doc: "1..4096 typed art.Operation objects; no paths or shell commands", Required: true, Items: map[string]any{"type": "object"}},
}, Run: func(s *Session, a Args) (*Output, error) {
h, err := artHost(s)
if err != nil {
return nil, err
}
if !a.Bool("dry-run") {
return nil, fmt.Errorf("art: edits must pass through the editor proposal queue")
}
revision, err := strconv.ParseUint(a.Str("revision"), 10, 64)
if err != nil {
return nil, fmt.Errorf("art: revision must be an unsigned decimal integer")
}
label := strings.TrimSpace(a.Str("label"))
if label == "" || len(label) > 256 {
return nil, fmt.Errorf("art: label must have 1..256 bytes")
}
ops, err := art.DecodeOperations([]byte(a.Str("operations")))
if err != nil {
return nil, err
}
return h.PrepareArt(a.Str("file"), revision, label, ops)
}},
}
}
File diff suppressed because it is too large Load Diff
+49
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"os"
"strings"
"aego/project"
@@ -14,6 +15,7 @@ type cliFlags struct {
json bool
quiet bool
strict bool
artOut string
}
func Usage(w io.Writer) {
@@ -101,6 +103,22 @@ func parseArgs(c *Command, toks []string) (Args, cliFlags, error) {
i++
}
continue
case "out":
if c.Name != "art.capture" {
break
}
if !has {
if i+1 >= len(toks) {
return nil, f, fmt.Errorf("--out needs a value")
}
i++
val = toks[i]
}
if val == "" || f.artOut != "" {
return nil, f, fmt.Errorf("--out requires one non-empty path")
}
f.artOut = val
continue
}
x, ok := byName[name]
if !ok {
@@ -129,6 +147,9 @@ func parseArgs(c *Command, toks []string) (Args, cliFlags, error) {
}
func report(stdout, stderr io.Writer, f cliFlags, out *Output, err error) int {
if err == nil && f.artOut != "" {
err = writeArtCapture(f.artOut, out)
}
switch {
case f.json:
var v any = out
@@ -151,3 +172,31 @@ func report(stdout, stderr io.Writer, f cliFlags, out *Output, err error) int {
}
return 0
}
// An explicit output path never overwrites a user's existing file.
func writeArtCapture(path string, out *Output) error {
if out == nil || len(out.Images) != 1 || out.Images[0].Mime != "image/png" {
return errors.New("art: editor did not return a PNG capture")
}
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return fmt.Errorf("art: cannot create capture: %w", err)
}
return finishArtCapture(path, f, out.Images[0].Data)
}
// On failure leave the path alone: it may have been replaced while the opened
// file was being written. The caller is told that an incomplete file may remain.
func finishArtCapture(path string, f io.WriteCloser, data []byte) error {
n, err := f.Write(data)
if err == nil && n != len(data) {
err = io.ErrShortWrite
}
if closeErr := f.Close(); err == nil {
err = closeErr
}
if err != nil {
return fmt.Errorf("art: cannot write capture: %w; incomplete output may remain at %q", err, path)
}
return nil
}
+6 -1
View File
@@ -48,6 +48,7 @@ type Output struct {
// Images go to the model as image content, such as the screenshots of a run.
Images []Image `json:"-"`
Drawing *DrawingEdit `json:"-"`
Art ArtProposal `json:"-"`
}
// Image is a picture an answer carries.
@@ -73,6 +74,7 @@ func catalog() []*Command {
out = append(out, materialCommands()...)
out = append(out, postEffectCommands()...)
out = append(out, textureCommands()...)
out = append(out, artCommands()...)
out = append(out, schemaCommands()...)
out = append(out, projectCommands()...)
out = append(out, scriptCommands()...)
@@ -82,7 +84,7 @@ func catalog() []*Command {
if c.Name == "asset.import-variant" {
c.Look = ls["asset.pixels"]
}
if c.Look.Title == nil && (strings.HasPrefix(c.Name, "postfx.") || strings.HasPrefix(c.Name, "material.") || strings.HasPrefix(c.Name, "shader.") || strings.HasPrefix(c.Name, "texture.")) {
if c.Look.Title == nil && (strings.HasPrefix(c.Name, "postfx.") || strings.HasPrefix(c.Name, "material.") || strings.HasPrefix(c.Name, "shader.") || strings.HasPrefix(c.Name, "texture.") || strings.HasPrefix(c.Name, "art.")) {
name := strings.ReplaceAll(c.Name, ".", " ")
c.Look = Look{Icon: "comp.sprite", Title: func(a Args) string { return name + " " + file(a) }, Summary: changes}
}
@@ -143,6 +145,9 @@ func (c *Command) Usage() string {
if c.Write {
parts = append(parts, "[--dry-run]")
}
if c.Name == "art.capture" {
parts = append(parts, "[--out path]")
}
return strings.Join(parts, " ")
}
+59
View File
@@ -0,0 +1,59 @@
package agent
import (
"fmt"
"testing"
"aego/art"
"aego/asset"
)
// Source-only scene validation must not treat a partial sprite sheet as a
// complete generic material texture when native runtime rejects that sampler.
func TestAgentSourceMaterialRejectsPartialArtSheet(t *testing.T) {
for _, multi := range []bool{false, true} {
t.Run(fmt.Sprintf("multi=%v", multi), func(t *testing.T) {
s := materialSession(t)
doc, err := art.New(4, 4, art.RGBA, art.DefaultLimits())
if err != nil {
t.Fatal(err)
}
defer doc.Close()
if multi {
edit, err := doc.Begin(doc.Revision(), "Add frame")
if err != nil {
t.Fatal(err)
}
if _, err := edit.AddFrame(doc.Info().Frames[0].ID, 100); err != nil {
t.Fatal(err)
}
change, err := edit.Prepare()
if err != nil {
t.Fatal(err)
}
if err := change.Apply(); err != nil {
t.Fatal(err)
}
}
data, err := doc.Encode()
if err != nil {
t.Fatal(err)
}
artID, err := s.create("assets/art.aeart", data)
if err != nil {
t.Fatal(err)
}
content := fmt.Sprintf("material version=1 shader=asset(%q) { params { Noise=asset(%q) } }", asset.BuiltinShaderDissolve.String(), artID.String())
materialID, err := s.create("assets/art.aemat", []byte(content))
if err != nil {
t.Fatal(err)
}
if info, ok := (sources{s.Pipeline}).TextureMetadata(artID); !ok || info.Region != multi || info.Width != 4 || info.Height != 4 {
t.Fatalf("source-only metadata lost first-frame bounds: %+v/%v", info, ok)
}
if _, ok := (sources{s.Pipeline}).Material(materialID); ok == multi {
t.Fatalf("source-only material resolver result=%v for multi=%v", ok, multi)
}
})
}
}
+17
View File
@@ -470,6 +470,23 @@ func (r sources) Material(id ids.AssetID) (*material.Material, bool) {
}
e, ok := r.pl.Entry(tid)
info, valid := r.pl.TextureSourceInfo(tid, nil)
if !valid && strings.EqualFold(path.Ext(e.Source), ".aeart") {
// The PNG source inspector cannot decode an art document. Use the
// same prepared frame semantics as the runtime instead of silently
// accepting its whole animation sheet as a generic sampler.
prepared, ready := r.TextureMetadata(tid)
if !ok || e.Missing || e.Error != "" || !ready {
return material.Tex{}, false
}
mode := texture.EmissionNone
switch prepared.Semantic {
case asset.TextureEmissionRGB:
mode = texture.EmissionRGB
case asset.TextureEmissionMask:
mode = texture.EmissionMask
}
return material.Tex{RegionOnly: prepared.Region, NormalMap: prepared.Semantic == asset.TextureNormal, EmissionMode: mode}, true
}
mode := texture.EmissionNone
switch info.Role {
case asset.RoleEmissionRGB:
+1 -1
View File
@@ -41,7 +41,7 @@ var versions = []string{"2025-06-18", "2025-03-26", "2024-11-05"}
var skipDirs = []string{".aego", ".git", "exports", "native"}
const instructions = `Tools for one Aego project. Paths are relative to the project root; a node is a path from the root (Player/Sprite) or #<id> from scene_tree; values use scene syntax: 2.5, [1, 2], #ff8800ff, Kinematic, "text", asset("<id>") or a project path. Read schema_show before setting fields of an unfamiliar component and docs_search / docs_read for how the engine works. Every write tool takes dry-run; scene_apply applies a whole transaction at once. Check results with scene_validate, project_validate and scene_run. A sprite with no texture yet is drawn, not built from colored rectangles: asset_pixels takes a character grid and a palette (frames give an atlas and a flipbook), asset_svg smooth shapes for tiles, UI and icons; both answer with a preview to check. Draw in the style of project.aego's art section (project_get art): its palette, and without a palette argument 0-9 and a-z are its colors.`
const instructions = `Tools for one Aego project. Paths are relative to the project root; a node is a path from the root (Player/Sprite) or #<id> from scene_tree; values use scene syntax: 2.5, [1, 2], #ff8800ff, Kinematic, "text", asset("<id>") or a project path. Read schema_show before setting fields of an unfamiliar component and docs_search / docs_read for how the engine works. Every write tool takes dry-run; scene_apply applies a whole transaction at once. For editable .aeart in a running editor, art_read and art_capture inspect the unsaved document; art_apply proposes an undoable batch at the returned revision. Check results with scene_validate, project_validate and scene_run. A sprite with no texture yet is drawn, not built from colored rectangles: asset_pixels takes a character grid and a palette (frames give an atlas and a flipbook), asset_svg smooth shapes for tiles, UI and icons; both answer with a preview to check. Draw in the style of project.aego's art section (project_get art): its palette, and without a palette argument 0-9 and a-z are its colors.`
type request struct {
JSONRPC string `json:"jsonrpc"`
+17 -1
View File
@@ -3,6 +3,7 @@ package agent
import (
"bufio"
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -91,7 +92,7 @@ func proxyRun(addr, token string, c *Command, a Args) (*Output, error) {
var rep struct {
Result struct {
Content []struct {
Type, Text string
Type, Text, Data, MimeType string
} `json:"content"`
IsError bool `json:"isError"`
} `json:"result"`
@@ -122,6 +123,21 @@ func proxyRun(addr, token string, c *Command, a Args) (*Output, error) {
return &Output{OK: true, Text: text}, nil
}
o := out.Output
if c.Name == "art.capture" {
for _, part := range rep.Result.Content {
if part.Type != "image" || part.MimeType != "image/png" {
continue
}
if len(part.Data) > base64.StdEncoding.EncodedLen(16<<20) {
return nil, errors.New("art: PNG exceeds agent capture budget")
}
data, err := base64.StdEncoding.DecodeString(part.Data)
if err != nil {
return nil, fmt.Errorf("art: invalid PNG response: %w", err)
}
o.Images = append(o.Images, Image{Mime: part.MimeType, Data: data})
}
}
var s string
switch {
case c.Name == "scene.run":
+3
View File
@@ -2,6 +2,8 @@ package editors
import (
"aego/app/assembly"
"aego/modules/animation"
animationeditor "aego/modules/animation/editor"
"aego/modules/audio"
audioeditor "aego/modules/audio/editor"
"aego/modules/camera"
@@ -24,6 +26,7 @@ import (
func With(c assembly.Catalog) assembly.Catalog {
return c.
With(animation.ID, animationeditor.Hook).
With(inputmod.ID, inputeditor.Hook).
With(particles.ID, particleseditor.Hook).
With(render2d.ID, render2deditor.Hook).
+12
View File
@@ -5,6 +5,7 @@ import (
"path"
"sort"
"strings"
"sync"
"time"
"aego/contracts/persistence"
@@ -19,10 +20,13 @@ func UseMemorySaves(e *engine.Engine) {
}
type memUser struct {
mu sync.RWMutex
files map[string][]byte
}
func (m *memUser) ReadFile(p string) ([]byte, error) {
m.mu.RLock()
defer m.mu.RUnlock()
data, ok := m.files[path.Clean(p)]
if !ok {
return nil, fs.ErrNotExist
@@ -31,16 +35,22 @@ func (m *memUser) ReadFile(p string) ([]byte, error) {
}
func (m *memUser) WriteFile(p string, data []byte) error {
m.mu.Lock()
defer m.mu.Unlock()
m.files[path.Clean(p)] = append([]byte(nil), data...)
return nil
}
func (m *memUser) Remove(p string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.files, path.Clean(p))
return nil
}
func (m *memUser) Stat(p string) (platform.UserEntry, error) {
m.mu.RLock()
defer m.mu.RUnlock()
data, ok := m.files[path.Clean(p)]
if !ok {
return platform.UserEntry{}, fs.ErrNotExist
@@ -49,6 +59,8 @@ func (m *memUser) Stat(p string) (platform.UserEntry, error) {
}
func (m *memUser) List(dir string) ([]platform.UserEntry, error) {
m.mu.RLock()
defer m.mu.RUnlock()
prefix := path.Clean(dir) + "/"
var out []platform.UserEntry
for p, data := range m.files {
+60
View File
@@ -0,0 +1,60 @@
package playground
import (
"io/fs"
"sync"
"testing"
)
func TestMemoryUserConcurrentReadWriteAndList(t *testing.T) {
m := &memUser{files: map[string][]byte{}}
start := make(chan struct{})
var wg sync.WaitGroup
for worker := range 4 {
wg.Go(func() {
<-start
for i := range 1000 {
if worker == 0 {
if err := m.WriteFile("saves/slot", []byte{byte(i)}); err != nil {
t.Error(err)
}
if i%2 == 0 {
if err := m.Remove("saves/slot"); err != nil {
t.Error(err)
}
}
} else {
if _, err := m.ReadFile("saves/slot"); err != nil && err != fs.ErrNotExist {
t.Error(err)
}
if _, err := m.Stat("saves/slot"); err != nil && err != fs.ErrNotExist {
t.Error(err)
}
if _, err := m.List("saves"); err != nil {
t.Error(err)
}
}
}
})
}
close(start)
wg.Wait()
}
func TestMemoryUserCopiesBytes(t *testing.T) {
m := &memUser{files: map[string][]byte{}}
data := []byte("save")
if err := m.WriteFile("saves/slot", data); err != nil {
t.Fatal(err)
}
data[0] = 'X'
read, err := m.ReadFile("saves/slot")
if err != nil || string(read) != "save" {
t.Fatalf("write did not retain a copy: %q %v", read, err)
}
read[0] = 'Y'
read, err = m.ReadFile("saves/slot")
if err != nil || string(read) != "save" {
t.Fatalf("read exposed the stored bytes: %q %v", read, err)
}
}
+6 -4
View File
@@ -1,6 +1,7 @@
package bootstrap
import (
"fmt"
"os"
"path/filepath"
"testing"
@@ -98,7 +99,8 @@ func TestKeyTracksNativeSources(t *testing.T) {
func TestLocalReplaces(t *testing.T) {
root := t.TempDir()
writeFile(t, filepath.Join(root, "go.mod"), `module game
absolute := filepath.VolumeName(root) + "/abs/c"
writeFile(t, filepath.Join(root, "go.mod"), fmt.Sprintf(`module game
go 1.27
@@ -107,9 +109,9 @@ replace aego => ../engine // local engine
replace (
example.com/a v1.2.3 => ./vendor/a
example.com/b => example.com/b v1.0.0
example.com/c => /abs/c
example.com/c => %s
)
`)
`, absolute))
got, err := LocalReplaces(root)
if err != nil {
t.Fatal(err)
@@ -117,7 +119,7 @@ replace (
want := map[string]bool{
filepath.Clean(filepath.Join(root, "../engine")): true,
filepath.Clean(filepath.Join(root, "vendor", "a")): true,
filepath.Clean("/abs/c"): true,
filepath.Clean(absolute): true,
}
if len(got) != len(want) {
t.Fatalf("replaces = %v", got)
+8
View File
@@ -8,6 +8,7 @@ import (
"aego/app/assembly"
"aego/app/assembly/builtin"
"aego/app/docicons"
"aego/asset/loader"
"aego/asset/pipeline"
"aego/contracts/persistence"
@@ -45,6 +46,9 @@ type Config struct {
Platform platform.Platform
Profile string
NoProfile bool
// NoExternalImportTools prevents project assets from starting external
// converters and validators, including on later reimport.
NoExternalImportTools bool
// SharedIndex opens the asset index read-only without a warning when another process, the editor, holds it.
SharedIndex bool
}
@@ -134,6 +138,9 @@ func (h *Host) build(cfg Config, mods []module.Module) error {
if h.Importers, err = DevImporters(h.Workspace, h.Modules); err != nil {
return err
}
if cfg.NoExternalImportTools {
disableExternalImportTools(h.Importers)
}
if err = h.Types.Schema().Freeze(); err != nil {
return err
}
@@ -147,6 +154,7 @@ func (h *Host) build(cfg Config, mods []module.Module) error {
ch.Warn("cannot describe the script bindings", log.F("err", derr.Error()))
} else {
h.Docs = docs.FromManifest(m, h.Types.Schema(), h.Modules)
h.Docs.SetNavigationIcons(docicons.Builtin())
m.Keywords = h.Docs.Keywords()
h.Workspace.SetDocs(m)
for _, p := range h.Docs.Problems() {
+13 -3
View File
@@ -78,11 +78,21 @@ type Bundle struct {
func sanitizer() func(string) string {
home, _ := os.UserHomeDir()
return func(s string) string {
if home != "" {
s = strings.ReplaceAll(s, home, "~")
}
return hideHome(s, home)
}
}
func hideHome(s, home string) string {
if home == "" {
return s
}
// Windows paths can appear with either separator in stack traces and logs.
for _, prefix := range [2]string{home, strings.ReplaceAll(home, `\`, "/")} {
s = strings.ReplaceAll(s, prefix+`\`, "~/")
s = strings.ReplaceAll(s, prefix+"/", "~/")
s = strings.ReplaceAll(s, prefix, "~")
}
return s
}
func Write(b Bundle) (string, error) {
+13
View File
@@ -3,6 +3,7 @@ package crash
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
@@ -10,6 +11,9 @@ import (
func TestBundleHidesHome(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
if runtime.GOOS == "windows" {
t.Setenv("USERPROFILE", home)
}
root := filepath.Join(home, "game")
dir, err := Write(Bundle{Root: root, Kind: "panic", Where: "panel inspector", Panic: "boom in " + root, Stack: []byte(root + "/x.go:1")})
if err != nil {
@@ -25,3 +29,12 @@ func TestBundleHidesHome(t *testing.T) {
}
}
}
func TestHideHomeWithWindowsSeparators(t *testing.T) {
home := `C:\Users\Alice`
for _, input := range []string{`C:\Users\Alice\game\main.go`, `C:/Users/Alice/game/main.go`} {
if got := hideHome(input, home); got != `~/game/main.go` && got != `~/game\main.go` {
t.Fatalf("hideHome(%q) = %q", input, got)
}
}
}
+39
View File
@@ -0,0 +1,39 @@
// Package docicons holds the built-in documentation icon policy at the
// application composition boundary. Modules and editor panels do not depend on
// this policy; it is supplied to the documentation registry by the host.
package docicons
import "maps"
// These maps mirror tools/gendocs/site/icons.go; the parity test checks every
// key and value, including module-owned guides, API packages and settings.
var taskIcons = map[string]string{
"install": "file.download", "quickstart": "doc.project", "platformer": "doc.tutorial",
"platformer-effects": "render2d.point_light", "credits": "help.about", "recipes": "doc.recipe",
"editor": "view.dock", "editor/agent": "panel.agent", "agents": "panel.agent",
"editor/icons": "view.grid", "cli": "panel.console", "diagnostics": "panel.problems",
"curves": "ui.curve", "tween": "code.timer", "animation": "animation.player",
"animation/animator": "animation.animator", "animation/controller": "anim.graph",
"animation/clips": "anim.key", "animation/events": "anim.event", "animation/time": "play.speed",
"tween/timers": "code.timer", "render2d/materials": "file.preset",
"render2d/normal-mapping": "render2d.normals", "render2d/art": "file.art",
"render2d/image-viewer": "file.texture", "render2d.PostProcess": "view.overlays",
"camera.CameraBrain2D": "view.split", "formats/overview": "file.data",
"formats/aeanim": "anim.key", "formats/aectrl": "anim.graph",
}
var refIcons = map[string]string{
"aego/builtin": "cat.node", "events": "panel.events", "aego/action": "panel.tasks", "aego/audio": "cat.audio",
"aego/camera": "cat.camera", "aego/curve": "ui.curve", "aego/data": "file.data", "aego/debug": "panel.debug_draw",
"aego/input": "panel.input_devices", "aego/log": "panel.console", "aego/math": "cat.transform", "aego/physics": "cat.physics",
"aego/rand": "shape.dice", "aego/save": "panel.saves", "aego/scene": "file.scene", "aego/settings": "cat.settings",
"aego/testing": "status.ok", "aego/time": "play.speed", "aego/timer": "code.timer", "aego/tween": "vec.velocity",
"section/engine": "panel.settings", "section/input": "panel.input_devices", "section/render2d": "cat.rendering",
"section/physics": "cat.physics", "section/debug": "panel.debug_draw", "section/player": "play.start", "section/audio": "cat.audio",
"section/animation": "animation.player", "section/art": "file.art", "section/update": "file.download",
}
// Builtin returns independent task and reference maps for a host to install.
func Builtin() (tasks, references map[string]string) {
return maps.Clone(taskIcons), maps.Clone(refIcons)
}
+13
View File
@@ -0,0 +1,13 @@
package docicons
import "testing"
func TestBuiltinReturnsIndependentPolicies(t *testing.T) {
tasks, refs := Builtin()
tasks["animation"] = "wrong"
refs["aego/audio"] = "wrong"
otherTasks, otherRefs := Builtin()
if otherTasks["animation"] != "animation.player" || otherRefs["aego/audio"] != "cat.audio" {
t.Fatal("one host changed the built-in policy for another")
}
}
+42
View File
@@ -0,0 +1,42 @@
package app
import (
"path/filepath"
"testing"
"aego/app/docicons"
"aego/core/log"
"aego/engine"
"aego/project"
)
func TestEditorHostInstallsDocumentationIconPolicy(t *testing.T) {
p, err := project.Create(filepath.Join(t.TempDir(), "project"), "Docs icons", false)
if err != nil {
t.Fatal(err)
}
h, err := Build(p, Config{Engine: engine.Config{Headless: true}, Logger: log.Nop(), Docs: true, Editor: true, NoProfile: true})
if err != nil {
t.Fatal(err)
}
defer h.Close()
if h.Docs == nil {
t.Fatal("editor host did not build its documentation registry")
}
tasks, refs := docicons.Builtin()
for id, want := range tasks {
if got := h.Docs.NavigationTaskIcon(id); got != want {
t.Errorf("host task %q = %q, want %q", id, got, want)
}
}
for id, want := range refs {
if got := h.Docs.NavigationReferenceIcon(id); got != want {
t.Errorf("host reference %q = %q, want %q", id, got, want)
}
}
for _, id := range []string{"animation", "animation/animator", "render2d.PostProcess", "camera.CameraBrain2D", "aego/audio", "aego/timer", "section/animation"} {
if _, ok := h.Docs.Get(id); !ok {
t.Errorf("editor host lacks module documentation entry %q", id)
}
}
}
File diff suppressed because it is too large Load Diff
+2
View File
@@ -10,6 +10,7 @@ import (
"aego/app/assembly"
"aego/app/assembly/builtin"
"aego/app/docicons"
"aego/app/playground"
"aego/engine/docs"
"aego/engine/module"
@@ -60,6 +61,7 @@ func build(p *project.Project, mods []module.Module, pg *playground.Playground)
src.Project = p.Name
}
r := docs.Assemble(src)
r.SetNavigationIcons(docicons.Builtin())
localize(r)
bm.Keywords = r.Keywords()
if ws != nil {
+38
View File
@@ -0,0 +1,38 @@
package docstool
import (
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
)
// siteHandler serves the generator's clean URLs, never a directory inventory.
// A guide can have both physics.html and a physics/ directory of child pages.
func siteHandler(root string) http.Handler {
files := http.FileServer(http.Dir(root))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
clean := path.Clean("/" + r.URL.Path)
local := filepath.Join(root, filepath.FromSlash(strings.TrimPrefix(clean, "/")))
if info, err := os.Stat(local + ".html"); err == nil && !info.IsDir() {
if strings.HasSuffix(r.URL.Path, "/") {
target := (&url.URL{Path: clean, RawQuery: r.URL.RawQuery}).String()
http.Redirect(w, r, target, http.StatusPermanentRedirect)
return
}
copy := r.Clone(r.Context())
copy.URL.Path, copy.URL.RawPath = clean+".html", ""
files.ServeHTTP(w, copy)
return
}
if info, err := os.Stat(local); err == nil && info.IsDir() {
if index, err := os.Stat(filepath.Join(local, "index.html")); err != nil || index.IsDir() {
http.NotFound(w, r)
return
}
}
files.ServeHTTP(w, r)
})
}
+41
View File
@@ -0,0 +1,41 @@
package docstool
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestDocsServerCleanURLsDoNotExposeDirectoryListings(t *testing.T) {
root := t.TempDir()
for name, content := range map[string]string{"physics.html": "<h1>Physics</h1>", "physics/units.html": "<h1>Units</h1>", "play/index.html": "<h1>Playground</h1>", "hidden/file.txt": "private inventory"} {
file := filepath.Join(root, name)
if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(file, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
for _, item := range []struct {
path string
status int
text, location string
}{
{"/physics", 200, "<h1>Physics</h1>", ""},
{"/physics/", 308, "", "/physics"},
{"/physics/?test=1", 308, "", "/physics?test=1"},
{"/physics/units", 200, "<h1>Units</h1>", ""},
{"/play/", 200, "<h1>Playground</h1>", ""},
{"/hidden/", 404, "", ""},
} {
r := httptest.NewRecorder()
siteHandler(root).ServeHTTP(r, httptest.NewRequest(http.MethodGet, item.path, nil))
if r.Code != item.status || !strings.Contains(r.Body.String(), item.text) || r.Header().Get("Location") != item.location || strings.Contains(r.Body.String(), "Directory listing") {
t.Errorf("%s: status=%d location=%q body=%s", item.path, r.Code, r.Header().Get("Location"), r.Body.String())
}
}
}
+191 -1
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
+4 -1
View File
@@ -85,6 +85,8 @@ type App struct {
textOn bool
shown *engine.World
closed bool
control ControlState
viewport Viewport
}
func noBlock(string) *aetext.Node {
@@ -346,7 +348,7 @@ func (a *App) showPhysics() {
func (a *App) Update(dt float64) {
e := a.e
now := e.Platform().Now()
a.session.Tick(dt)
a.tickSession(dt)
a.showPhysics()
ui := a.ui
ui.Time = now
@@ -361,6 +363,7 @@ func (a *App) Update(dt float64) {
a.toolbar(bar)
body := mathx.Rect{Y: bar.H, W: size.X, H: max(0, size.Y-bar.H)}
gameR, inspR := layout(body, 1)
a.viewport = Viewport{X: gameR.X, Y: gameR.Y, Width: gameR.W, Height: gameR.H, CanvasWidth: m.LogicalW, CanvasHeight: m.LogicalH}
a.panel(&frame, gameR, a.game.Draw)
a.panel(&frame, inspR, a.insp.Draw)
ctx.DnD.Frame(ui)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
//go:build aego_editor
package docsview
import (
"encoding/json"
"testing"
"aego/app/playground"
"aego/modules/audio"
)
func TestAudioListenerTryDisplaysEffectivePositionAndReset(t *testing.T) {
p, err := playground.New()
if err != nil {
t.Fatal(err)
}
e, ok := p.Docs().Get("audio.AudioListener2D")
if !ok || e.Demo == nil {
t.Fatal("audio listener contract is absent")
}
code, err := p.TryCode(e.TryScript)
if err != nil {
t.Fatal(err)
}
a := newHeadless(t)
req, err := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "code": code, "prefabs": e.TryPrefabs, "artifacts": e.TryArtifacts})
if err != nil {
t.Fatal(err)
}
if reply := a.Call("load", req); reply.Error != "" {
t.Fatal(reply.Error)
}
if err := a.Step(12); err != nil {
t.Fatal(err)
}
check := func(want float32) {
t.Helper()
sc := a.session.Controller().Scene()
marker := sc.Find(sc.Root(), "Effective listener")
if !sc.IsAlive(marker) {
t.Fatal("listener indicator is absent")
}
service := audio.Of(a.session.Controller().World())
if service == nil || service.Listener().X != want || sc.WorldPosition(marker).X != want {
t.Fatalf("marker does not reflect actual audio listener position %v", want)
}
r := a.Call("inspect", []byte(`{"component":"audio.AudioListener2D","fields":["audio.AudioListener2D.ListenerX"]}`))
if r.Error != "" || r.Inspection == nil || r.Inspection.Observables["audio.AudioListener2D.ListenerX"] != float64(want) {
t.Fatalf("effective listener position is not observable: %+v", r)
}
}
check(2)
if r := a.Call("set", []byte(`{"component":"audio.AudioListener2D","field":"Active","value":0}`)); r.Error != "" {
t.Fatal(r.Error)
}
if err := a.Step(12); err != nil {
t.Fatal(err)
}
check(0)
if r := a.Call("reset", nil); r.Error != "" {
t.Fatal(r.Error)
}
if err := a.Step(12); err != nil {
t.Fatal(err)
}
check(2)
if messages := errorsOf(a); len(messages) != 0 {
t.Fatalf("listener Try runtime errors: %v", messages)
}
}
@@ -0,0 +1,91 @@
//go:build aego_editor
package docsview
import (
"encoding/json"
"testing"
"aego/app/playground"
"aego/asset"
"aego/modules/audio"
"aego/modules/render2d"
"aego/scene"
)
// The example requests a real looped voice at volume zero. The color reads the
// audio service's actual SourcePlaying state, not the editable checkbox.
func TestAudioSourceTryStartsRealSilentVoiceAndResets(t *testing.T) {
p, err := playground.New()
if err != nil {
t.Fatal(err)
}
e, ok := p.Docs().Get("audio.AudioSource2D")
if !ok || e.Demo == nil {
t.Fatal("audio source contract is absent")
}
code, err := p.TryCode(e.TryScript)
if err != nil {
t.Fatal(err)
}
a := newHeadless(t)
req, err := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "code": code, "prefabs": e.TryPrefabs, "artifacts": e.TryArtifacts})
if err != nil {
t.Fatal(err)
}
if reply := a.Call("load", req); reply.Error != "" {
t.Fatal(reply.Error)
}
if err := a.Step(12); err != nil {
t.Fatal(err)
}
state := func() (*audio.AudioSource2D, *render2d.SpriteRenderer, bool) {
t.Helper()
sc := a.session.Controller().Scene()
h := sc.Find(sc.Root(), "Source")
src, ok := scene.Get[audio.AudioSource2D](sc, h)
if !ok || src.Clip.ID != asset.BuiltinHum || src.Volume != 0 || !src.Loop {
t.Fatal("expected a silent looping builtin source")
}
marker, ok := scene.Get[render2d.SpriteRenderer](sc, h)
if !ok {
t.Fatal("audio source indicator is absent")
}
service := audio.Of(a.session.Controller().World())
if service == nil {
t.Fatal("audio service is absent")
}
return src, marker, service.SourcePlaying(h)
}
src, marker, playing := state()
idleColor := marker.Tint
if src.PlayOnStart || src.Playing || playing {
t.Fatal("source should start without a voice")
}
if reply := a.Call("inspect", []byte(`{"component":"audio.AudioSource2D","fields":["audio.AudioSource2D.Playing"],"dependencies":["ae900000000000000000000000000a03"]}`)); reply.Error != "" || reply.Inspection == nil || len(reply.Inspection.Dependencies) != 1 {
t.Fatalf("the actual builtin audio dependency was not resolved: %+v", reply)
}
if reply := a.Call("set", []byte(`{"component":"audio.AudioSource2D","field":"PlayOnStart","value":1}`)); reply.Error != "" {
t.Fatal(reply.Error)
}
if err := a.Step(12); err != nil {
t.Fatal(err)
}
src, marker, playing = state()
if !src.PlayOnStart || !src.Playing || !playing || marker.Tint == idleColor {
t.Fatalf("real silent voice did not start: enabled=%v playing=%v service=%v tint=%v", src.PlayOnStart, src.Playing, playing, marker.Tint)
}
if reply := a.Call("reset", nil); reply.Error != "" {
t.Fatal(reply.Error)
}
if err := a.Step(12); err != nil {
t.Fatal(err)
}
src, marker, playing = state()
if src.PlayOnStart || src.Playing || playing || marker.Tint != idleColor {
t.Fatal("Reset did not stop the voice and restore the indicator")
}
if messages := errorsOf(a); len(messages) != 0 {
t.Fatalf("audio Try runtime errors: %v", messages)
}
}
+82
View File
@@ -0,0 +1,82 @@
//go:build aego_editor
package docsview
import (
"encoding/json"
"math"
"testing"
"aego/app/playground"
"aego/engine"
"aego/modules/audio"
"aego/modules/render2d"
"aego/scene"
)
func TestAudioZoneTryChangesRealWorldBusAndResets(t *testing.T) {
p, err := playground.New()
if err != nil {
t.Fatal(err)
}
e, ok := p.Docs().Get("audio.AudioZone2D")
if !ok || e.Demo == nil {
t.Fatal("audio zone contract is absent")
}
code, err := p.TryCode(e.TryScript)
if err != nil {
t.Fatal(err)
}
a := newHeadless(t)
req, err := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "code": code, "prefabs": e.TryPrefabs, "artifacts": e.TryArtifacts})
if err != nil {
t.Fatal(err)
}
if r := a.Call("load", req); r.Error != "" {
t.Fatal(r.Error)
}
if err := a.Step(16); err != nil {
t.Fatal(err)
}
check := func(want float64) *render2d.SpriteRenderer {
t.Helper()
world := a.session.Controller().World()
mod, ok := engine.ServiceOf[*audio.Module](world, audio.MixerID)
if !ok || mod.Mixer() == nil || math.Abs(float64(mod.Mixer().BusGain(int(audio.BusMusic)))-want) > 0.001 {
t.Fatalf("actual mixer music bus did not reach gain %v", want)
}
r := a.Call("inspect", []byte(`{"component":"audio.AudioZone2D","fields":["audio.AudioZone2D.WorldGain"]}`))
if r.Error != "" || r.Inspection == nil || r.Inspection.Observables["audio.AudioZone2D.WorldGain"] != want {
t.Fatalf("runtime world gain is not observable: %+v", r)
}
sc := a.session.Controller().Scene()
h := sc.Find(sc.Root(), "Indicator")
marker, ok := scene.Get[render2d.SpriteRenderer](sc, h)
if !ok {
t.Fatal("gain indicator is absent")
}
return marker
}
before := check(0.25).Tint
if r := a.Call("set", []byte(`{"component":"audio.AudioZone2D","field":"Gain","value":0.75}`)); r.Error != "" {
t.Fatal(r.Error)
}
if err := a.Step(16); err != nil {
t.Fatal(err)
}
if marker := check(0.75); marker.Tint == before {
t.Fatal("indicator did not follow the actual world gain")
}
if r := a.Call("reset", nil); r.Error != "" {
t.Fatal(r.Error)
}
if err := a.Step(16); err != nil {
t.Fatal(err)
}
if marker := check(0.25); marker.Tint != before {
t.Fatal("Reset did not restore the gain indicator")
}
if messages := errorsOf(a); len(messages) != 0 {
t.Fatalf("audio zone Try runtime errors: %v", messages)
}
}
+104 -19
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,83 @@
//go:build aego_editor && aego_gltest && linux && cgo && !aego_nonative
package docsview
import (
"context"
"math"
"runtime"
"testing"
"aego/app/playground"
"aego/modules/camera"
)
// In addition to the rendered-frame contract, check the actual camera service
// so a moving decoration cannot stand in for a changing camera pose.
func TestCameraShakeComponentDemoDrivesRealCamera(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
p, err := playground.New()
if err != nil {
t.Fatal(err)
}
entry, ok := p.Docs().Get("camera.CameraShake2D")
if !ok || entry.Demo == nil || len(entry.Demo.Actions) != 1 {
t.Fatal("camera shake contract is absent")
}
ctx := context.Background()
opened, err := (&nativeTryRunner{p: p}).Open(ctx, *entry.Demo, "native")
if err != nil {
t.Fatal(err)
}
defer func() {
if err := opened.Close(); err != nil {
t.Error(err)
}
}()
instance := opened.(*nativeTryInstance)
service := func() *camera.Service {
t.Helper()
s := camera.Of(instance.app.session.Controller().World())
if s == nil || s.Trauma("") < 0.99 {
t.Fatal("the real camera service did not receive the hit")
}
return s
}
checkStill := func() {
t.Helper()
s := service()
if diff := math.Abs(float64(s.Render("").Rotation - s.Logical("").Rotation)); diff > 1e-6 {
t.Fatalf("zero Max Angle unexpectedly rotated the view: %g", diff)
}
}
checkStill()
if err := instance.Action(ctx, entry.Demo.Actions[0]); err != nil {
t.Fatal(err)
}
world := instance.app.session.Controller().World()
startTick := world.Time.Tick()
var peak float64
for range 60 {
// Presentation frames do not advance a controlled Try world. Advance
// each simulation frame explicitly before sampling its camera pose.
if err := instance.advance(ctx, 1, nil); err != nil {
t.Fatal(err)
}
s := service()
peak = max(peak, math.Abs(float64(s.Render("").Rotation-s.Logical("").Rotation)))
}
if world.Time.Tick() <= startTick {
t.Fatal("camera samples did not advance the Try simulation")
}
if peak < 0.01 {
t.Fatalf("changing Max Angle did not move the real camera pose: peak %g", peak)
}
if err := instance.Reset(ctx, entry.Demo.Reset); err != nil {
t.Fatal(err)
}
checkStill()
if messages := errorsOf(instance.app); len(messages) != 0 {
t.Fatalf("Try runtime errors: %v", messages)
}
}
File diff suppressed because it is too large Load Diff
+96
View File
@@ -0,0 +1,96 @@
//go:build aego_editor
package docsview
import (
"encoding/json"
"fmt"
"testing"
"aego/app/playground"
"aego/modules/gameplay"
"aego/modules/render2d"
"aego/scene"
)
// The indicator follows the target's actual Health after the real gameplay
// system processes the overlap; changing the edited field alone is not enough.
func TestCombatComponentTriesDealDamageAndReset(t *testing.T) {
p, err := playground.New()
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
id, field string
value int
wantHP int32
}{
{"gameplay.Hitbox2D", "Damage", 3, 7},
{"gameplay.Hurtbox2D", "Multiplier", 2, 6},
} {
t.Run(tc.id, func(t *testing.T) {
e, ok := p.Docs().Get(tc.id)
if !ok || e.Demo == nil {
t.Fatal("component contract is absent")
}
code, err := p.TryCode(e.TryScript)
if err != nil {
t.Fatal(err)
}
a := newHeadless(t)
req, err := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "code": code, "prefabs": e.TryPrefabs, "artifacts": e.TryArtifacts})
if err != nil {
t.Fatal(err)
}
if reply := a.Call("load", req); reply.Error != "" {
t.Fatal(reply.Error)
}
if err := a.Step(12); err != nil {
t.Fatal(err)
}
state := func() (*gameplay.Health, *render2d.SpriteRenderer) {
t.Helper()
sc := a.session.Controller().Scene()
target := sc.Find(sc.Root(), "Target")
hp, ok := scene.Get[gameplay.Health](sc, target)
if !ok {
t.Fatal("target Health is absent")
}
marker, ok := scene.Get[render2d.SpriteRenderer](sc, target)
if !ok {
t.Fatal("target indicator is absent")
}
return hp, marker
}
hp, marker := state()
initialColor := marker.Tint
if hp.Max != 10 || hp.Current != 10 || hp.Dead {
t.Fatalf("initial target health: %+v", hp)
}
payload := fmt.Sprintf(`{"component":%q,"field":%q,"value":%d}`, tc.id, tc.field, tc.value)
if reply := a.Call("set", []byte(payload)); reply.Error != "" {
t.Fatal(reply.Error)
}
if err := a.Step(12); err != nil {
t.Fatal(err)
}
hp, marker = state()
if hp.Current != tc.wantHP || hp.Dead || marker.Tint == initialColor {
t.Fatalf("the actual hit and indicator disagree: health=%d want=%d tint=%v", hp.Current, tc.wantHP, marker.Tint)
}
if reply := a.Call("reset", nil); reply.Error != "" {
t.Fatal(reply.Error)
}
if err := a.Step(12); err != nil {
t.Fatal(err)
}
hp, marker = state()
if hp.Current != 10 || hp.Dead || marker.Tint != initialColor {
t.Fatalf("Reset did not restore target health and indicator: health=%d tint=%v", hp.Current, marker.Tint)
}
if messages := errorsOf(a); len(messages) != 0 {
t.Fatalf("combat Try runtime errors: %v", messages)
}
})
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+71
View File
@@ -0,0 +1,71 @@
//go:build aego_editor
package docsview
import (
"crypto/sha256"
"fmt"
"testing"
"aego/app/playground"
)
// Component-specific demonstrations must not silently replace the existing
// authored scenes and scripts used by the broader guides.
func TestComponentTryGuidesPreserveAuthoredScenes(t *testing.T) {
p, err := playground.New()
if err != nil {
t.Fatal(err)
}
for _, spec := range []struct{ guide, component, sum, script string }{
{"camera", "camera.CameraFollow2D", "0a7ed8d6fd9cce7ee144cd2a0ca91a6ff377220cc7b737ebe9feb93e10331f3a", ""},
{"camera/follow", "camera.CameraFollow2D", "0a7ed8d6fd9cce7ee144cd2a0ca91a6ff377220cc7b737ebe9feb93e10331f3a", ""},
{"camera/zones", "camera.CameraZone2D", "9f5b36a8de5584836d466b8437510c1990e3c719d41a4c65dfa591b2b7c634e8", ""},
{"camera/outputs", "camera.CameraBrain2D", "d475b935c8c41973653b44aa1ff75d4871263a0ad2cb9d56097f234818be1aa4", ""},
{"camera/virtual", "camera.VirtualCamera2D", "14e775168e8f7f5250d42970db4578d39219a9e3bb9d0dab839892fd92ae88a8", "af3febb779542a8605bd2899667e87d5cf81826ca4d15efcbab85a0c0efcffab"},
{"physics", "physics.Body2D", "87081fb3f66fb919ef02833f30d052457b456c841b2b6406c9930ad47884644c", ""},
{"physics/components", "physics.CircleCollider2D", "a572890607943e3af0727666c0501e605543dc28a0a9398777ea2fe987e4e213", ""},
{"gameplay/hitbox", "gameplay.Hitbox2D", "06cebaff0c35ea72d0a8a9112bbbe59eacca4016841c92e25bbe7b6fc8354f35", "10b0b317c79134c3d333f17a42381db8e314e343058a8dcfe7d2806f88da5b1c"},
{"gameplay/hurtbox", "gameplay.Hurtbox2D", "4cb707ab73abcf3f50af599dc7a6313837044230c95f48d83809ff33d8d3d2a4", "e7a7ad3733ab9f989f50b6f20fed6ed6b9a3cff635c9d04808f336452e287b9e"},
{"gameplay/killzone", "gameplay.KillZone2D", "ea7ce3acf87237e86e2988a93951ccdfb762b7fe6622ab1a6e84ad3f7ba10fb9", "fe5d7ad926cfc8029e9451992285748c197ef5a018b4eba023a30a74b93d0ecf"},
{"gameplay/pickup", "gameplay.Pickup2D", "685494b9be74b228fc2762e4cfc870cda000f83afe05c814ddfd190e4ecbbaab", "7b038ac87d73a29ee2044b8ffc66f44b681538f5457631fea471df8808ddd33a"},
{"gameplay/checkpoint", "gameplay.Checkpoint2D", "9d35c26516469c4180cfbdb94edf524d8b54f47f2c897a946e93b829152a974f", "951d0c477e0baeeced3c7a64093e324d30588844af32f952279985cf9a62fa61"},
{"gameplay/respawn", "gameplay.Respawn", "c604b98824cc3483b506e1f529e51159cbe8d3c8e058a37eb437c0e35890089a", "d13601abe19642eee5a4f3cd7056e0812dc93f5f59f6e904c972d1e7b09cc260"},
{"audio", "audio.AudioSource2D", "0c43758ee5bf1ad7304668d36b138a3f417dc795e56730d2226ca935e204cf7e", "1f0bde6059abe3241de2e799bd9e3e06f5a4259176bb636ba3e0d60fa7d0750c"},
{"audio/spatial", "audio.AudioListener2D", "3ffe126eea8b20663538fd4e1b2d083c9157edd2f7adfbe983bfec962229ff69", "2e79adf7b30530df3479608124ec67678cf9ef7847b570866731cc402652cac3"},
{"audio/zones", "audio.AudioZone2D", "2181dfaf04ca60b26b93a263c58d686423babf61139b9662b8502cd67ba2228c", "994b455b9af397862a58d284f763fd26b05aa4d51e80965464c4ff9379f80c58"},
{"save", "save.Persist", "19499400ad68dded38129ccec04546a5b018add92114f2e21f0c8bf6f61506cb", "8cde05fe8d4c3f6e6b722ff60b8be7a687160e50fefd5a09001f25a76299675a"},
{"tags/example", "engine.Tags", "e642c5c9983312c9f75b7966b3a9ecfeb2f08196f2cb7ebe9e3bf0adc24bdbbe", "a6459199adcbbd52aab200b5c04e331b478b4e1b4b0ddb6a688e7028416a5215"},
{"language/script-example", "engine.Script", "8927658496fa7c6afc6a77ba8818018faa5b93936c8a32a13bd72f5ec7f59d6d", "18f02ddc587e8719b73b9d68d749f533be2290512075403a1560191c05655ca8"},
{"render2d/materials", "engine.MaterialOverride", "c0a63c2b8cfdf22028ba1df4c7152a75c1f69a973c70175ec44f476aadf19405", "b5c1b4249a9b3232b9845f2a61b2cf2a5af48c3d9d065bf95f7ff877305de3b3"},
{"prefabs/example", "engine.PrefabInstance", "90289773f897db2a386ad6cf9380dbc874f03f6f0cb76a03d2c2d08a1de833da", "5e258045feacf7591993ce7397df8aed9e5247d776fe2d2055b78b55571d1ea1"},
} {
t.Run(spec.guide, func(t *testing.T) {
guide, ok := p.Docs().Get(spec.guide)
if !ok {
t.Fatal("guide not found")
}
wantSelect := spec.component
if spec.guide == "render2d/materials" {
wantSelect = "" // This authored guide never specified a component selection.
}
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(guide.Try))); got != spec.sum || guide.TrySelect != wantSelect {
t.Fatalf("authored guide scene or selection changed: sha=%s select=%s", got, guide.TrySelect)
}
if spec.script != "" {
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(guide.TryScript))); got != spec.script {
t.Fatalf("authored guide script changed: sha=%s", got)
}
}
component, ok := p.Docs().Get(spec.component)
if !ok || component.Demo == nil || component.Try == guide.Try {
t.Fatalf("%s needs its distinct executable component demo", spec.component)
}
})
}
for _, problem := range p.Docs().Problems() {
if problem.Error {
t.Fatalf("documentation registry error: %+v", problem)
}
}
}
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

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