Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6822156cd | ||
|
|
5eb1832907 | ||
|
|
ad23798da5 | ||
|
|
132ddeff70 | ||
|
|
c8d45b6ff5 | ||
|
|
962c479933 | ||
|
|
72d9a21782 | ||
|
|
cc2a428a7d |
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -9,8 +9,9 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
GLSLANG_APT_VERSION: 15.1.0-2~ubuntu0.24.04.2
|
||||||
LINUX_PKGS: >-
|
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
|
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
|
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
|
libwayland-dev wayland-protocols libdecor-0-dev
|
||||||
@@ -41,7 +42,7 @@ jobs:
|
|||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
- name: linux packages
|
- name: linux packages
|
||||||
if: runner.os == 'Linux'
|
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
|
- name: msys2
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
id: msys2
|
id: msys2
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
GLSLANG_APT_VERSION: 15.1.0-2~ubuntu0.24.04.2
|
||||||
LINUX_PKGS: >-
|
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
|
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
|
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
|
libwayland-dev wayland-protocols libdecor-0-dev
|
||||||
@@ -26,7 +27,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
- name: system packages
|
- 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
|
- uses: actions/cache@v4
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
@@ -61,7 +62,7 @@ jobs:
|
|||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
- name: linux packages
|
- name: linux packages
|
||||||
if: runner.os == 'Linux'
|
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
|
- name: msys2
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
id: msys2
|
id: msys2
|
||||||
@@ -137,7 +138,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
- name: linux packages
|
- 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
|
- uses: actions/cache@v4
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ tools/genicons/genicons
|
|||||||
|
|
||||||
/tests/golden/testdata/actual-*
|
/tests/golden/testdata/actual-*
|
||||||
tools/websmoke/node_modules/
|
tools/websmoke/node_modules/
|
||||||
|
/tools/websmoke/__pycache__/
|
||||||
tools/websmoke/golden/actual-*.png
|
tools/websmoke/golden/actual-*.png
|
||||||
|
|
||||||
# build output belongs in .cache/bin; these catch stray builds in the root
|
# build output belongs in .cache/bin; these catch stray builds in the root
|
||||||
|
|||||||
+108
-8
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
# Aego
|
# Aego — A 2D game engine built in Go.
|
||||||
|
|
||||||
Make 2D games in Go. One editor, one project, desktop and web.
|
Build games visually, write gameplay logic in Aego Script, and extend the engine with native Go.
|
||||||
|
|
||||||
Build scenes in the editor, script them in a small Go-like language, extend the engine with Go when you need to, and ship the same project to Linux, Windows, macOS and the browser. Deterministic simulation, rewind, physics, animation, audio and cameras come built in.
|
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>
|
- Documentation and the playground: <https://aego.akarpov.ru>
|
||||||
- Installing and building from source: [engine/res/docs/install.md](engine/res/docs/install.md)
|
- Installing and building from source: [engine/res/docs/install.md](engine/res/docs/install.md)
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
+32
-90
File diff suppressed because it is too large
Load Diff
+20
-3
@@ -18,6 +18,7 @@ type Arg struct {
|
|||||||
Doc string
|
Doc string
|
||||||
Required bool
|
Required bool
|
||||||
Pos bool
|
Pos bool
|
||||||
|
Items map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
type Command struct {
|
type Command struct {
|
||||||
@@ -45,7 +46,8 @@ type Output struct {
|
|||||||
Value any `json:"value,omitempty"`
|
Value any `json:"value,omitempty"`
|
||||||
Text string `json:"-"`
|
Text string `json:"-"`
|
||||||
// Images go to the model as image content, such as the screenshots of a run.
|
// Images go to the model as image content, such as the screenshots of a run.
|
||||||
Images []Image `json:"-"`
|
Images []Image `json:"-"`
|
||||||
|
Drawing *DrawingEdit `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image is a picture an answer carries.
|
// Image is a picture an answer carries.
|
||||||
@@ -67,12 +69,23 @@ func catalog() []*Command {
|
|||||||
out = append(out, ctrlCommands()...)
|
out = append(out, ctrlCommands()...)
|
||||||
out = append(out, refsCommands()...)
|
out = append(out, refsCommands()...)
|
||||||
out = append(out, assetCommands()...)
|
out = append(out, assetCommands()...)
|
||||||
|
out = append(out, variantCommand())
|
||||||
|
out = append(out, materialCommands()...)
|
||||||
|
out = append(out, postEffectCommands()...)
|
||||||
|
out = append(out, textureCommands()...)
|
||||||
out = append(out, schemaCommands()...)
|
out = append(out, schemaCommands()...)
|
||||||
out = append(out, projectCommands()...)
|
out = append(out, projectCommands()...)
|
||||||
out = append(out, scriptCommands()...)
|
out = append(out, scriptCommands()...)
|
||||||
ls := looks()
|
ls := looks()
|
||||||
for _, c := range out {
|
for _, c := range out {
|
||||||
c.Look = ls[c.Name]
|
c.Look = ls[c.Name]
|
||||||
|
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.")) {
|
||||||
|
name := strings.ReplaceAll(c.Name, ".", " ")
|
||||||
|
c.Look = Look{Icon: "comp.sprite", Title: func(a Args) string { return name + " " + file(a) }, Summary: changes}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||||
return out
|
return out
|
||||||
@@ -137,7 +150,11 @@ func (c *Command) Schema() map[string]any {
|
|||||||
props := map[string]any{}
|
props := map[string]any{}
|
||||||
var req []string
|
var req []string
|
||||||
for _, x := range c.AllArgs() {
|
for _, x := range c.AllArgs() {
|
||||||
props[x.Name] = map[string]any{"type": x.Type, "description": x.Doc}
|
prop := map[string]any{"type": x.Type, "description": x.Doc}
|
||||||
|
if x.Items != nil {
|
||||||
|
prop["items"] = x.Items
|
||||||
|
}
|
||||||
|
props[x.Name] = prop
|
||||||
if x.Required {
|
if x.Required {
|
||||||
req = append(req, x.Name)
|
req = append(req, x.Name)
|
||||||
}
|
}
|
||||||
@@ -175,7 +192,7 @@ func Run(s *Session, name string, a Args) (*Output, error) {
|
|||||||
}
|
}
|
||||||
out, err := c.Run(s, a)
|
out, err := c.Run(s, a)
|
||||||
if out != nil {
|
if out != nil {
|
||||||
out.OK = err == nil
|
out.OK = err == nil && !out.Errors()
|
||||||
}
|
}
|
||||||
return out, err
|
return out, 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
+14
-8
@@ -8,6 +8,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ const maxBody = 8 << 20
|
|||||||
// HTTP serves MCP as JSON-RPC over POST on loopback, for an agent the editor runs (D341).
|
// HTTP serves MCP as JSON-RPC over POST on loopback, for an agent the editor runs (D341).
|
||||||
type HTTP struct {
|
type HTTP struct {
|
||||||
URL string
|
URL string
|
||||||
Token string
|
token atomic.Pointer[string]
|
||||||
srv *http.Server
|
srv *http.Server
|
||||||
s *Server
|
s *Server
|
||||||
}
|
}
|
||||||
@@ -27,12 +28,8 @@ func ListenHTTP(opt Options) (*HTTP, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var key [32]byte
|
h := &HTTP{URL: "http://" + ln.Addr().String() + "/mcp", s: newServer(opt)}
|
||||||
if _, err := rand.Read(key[:]); err != nil {
|
h.RotateToken()
|
||||||
ln.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h := &HTTP{URL: "http://" + ln.Addr().String() + "/mcp", Token: hex.EncodeToString(key[:]), s: newServer(opt)}
|
|
||||||
h.s.ready.Store(true)
|
h.s.ready.Store(true)
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/mcp", h.serve)
|
mux.HandleFunc("/mcp", h.serve)
|
||||||
@@ -41,9 +38,18 @@ func ListenHTTP(opt Options) (*HTTP, error) {
|
|||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) Token() string { return *h.token.Load() }
|
||||||
|
|
||||||
|
func (h *HTTP) RotateToken() {
|
||||||
|
var key [32]byte
|
||||||
|
rand.Read(key[:])
|
||||||
|
token := hex.EncodeToString(key[:])
|
||||||
|
h.token.Store(&token)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HTTP) authorized(r *http.Request) bool {
|
func (h *HTTP) authorized(r *http.Request) bool {
|
||||||
got := r.Header.Get("Authorization")
|
got := r.Header.Get("Authorization")
|
||||||
return subtle.ConstantTimeCompare([]byte(got), []byte("Bearer "+h.Token)) == 1
|
return subtle.ConstantTimeCompare([]byte(got), []byte("Bearer "+h.Token())) == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// local refuses a request whose Host is not loopback, so a web page cannot reach the server by DNS rebinding.
|
// local refuses a request whose Host is not loopback, so a web page cannot reach the server by DNS rebinding.
|
||||||
|
|||||||
@@ -335,6 +335,13 @@ func (s *Server) call(params json.RawMessage) (any, *rpcError) {
|
|||||||
}
|
}
|
||||||
for _, t := range s.opt.Tools {
|
for _, t := range s.opt.Tools {
|
||||||
if t.Name == p.Name {
|
if t.Name == p.Name {
|
||||||
|
if t.Output != nil {
|
||||||
|
out, err := t.Output(p.Arguments)
|
||||||
|
if err != nil {
|
||||||
|
return result(err.Error(), true), nil
|
||||||
|
}
|
||||||
|
return result(out, out != nil && out.Errors()), nil
|
||||||
|
}
|
||||||
text, failed := t.Run(p.Arguments)
|
text, failed := t.Run(p.Arguments)
|
||||||
return result(text, failed), nil
|
return result(text, failed), nil
|
||||||
}
|
}
|
||||||
@@ -364,7 +371,7 @@ func (s *Server) call(params json.RawMessage) (any, *rpcError) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return result(err.Error(), true), nil
|
return result(err.Error(), true), nil
|
||||||
}
|
}
|
||||||
return result(out, false), nil
|
return result(out, out != nil && out.Errors()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) registry() (*docs.Registry, error) {
|
func (s *Server) registry() (*docs.Registry, error) {
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ type Tool struct {
|
|||||||
Schema map[string]any
|
Schema map[string]any
|
||||||
ReadOnly bool
|
ReadOnly bool
|
||||||
// Run answers the call; it may block while the user decides.
|
// Run answers the call; it may block while the user decides.
|
||||||
Run func(args map[string]any) (text string, failed bool)
|
Run func(args map[string]any) (text string, failed bool)
|
||||||
|
Output func(map[string]any) (*agent.Output, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type kitSource struct {
|
type kitSource struct {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -228,6 +228,9 @@ func looks() map[string]Look {
|
|||||||
"schema.show": {"cat.engine", func(a Args) string {
|
"schema.show": {"cat.engine", func(a Args) string {
|
||||||
return i18n.Tr("editor.agent.step.show_type", "Read the fields of {type}", i18n.Str("type", a.Str("type")))
|
return i18n.Tr("editor.agent.step.show_type", "Read the fields of {type}", i18n.Str("type", a.Str("type")))
|
||||||
}, ok},
|
}, ok},
|
||||||
|
"asset.paint": {"file.texture", func(a Args) string {
|
||||||
|
return i18n.Tr("editor.agent.step.draw_pixels", "Draw {file} in pixels", i18n.Str("file", path.Base(a.Str("path"))))
|
||||||
|
}, ok},
|
||||||
"asset.pixels": {"file.texture", func(a Args) string {
|
"asset.pixels": {"file.texture", func(a Args) string {
|
||||||
return i18n.Tr("editor.agent.step.draw_pixels", "Draw {file} in pixels", i18n.Str("file", path.Base(a.Str("path"))))
|
return i18n.Tr("editor.agent.step.draw_pixels", "Draw {file} in pixels", i18n.Str("file", path.Base(a.Str("path"))))
|
||||||
}, ok},
|
}, ok},
|
||||||
@@ -255,6 +258,12 @@ func looks() map[string]Look {
|
|||||||
"refs.fix": {"panel.references", func(a Args) string {
|
"refs.fix": {"panel.references", func(a Args) string {
|
||||||
return i18n.Tr("editor.agent.step.fix_refs", "Point references at {to}", i18n.Str("to", path.Base(a.Str("to"))))
|
return i18n.Tr("editor.agent.step.fix_refs", "Point references at {to}", i18n.Str("to", path.Base(a.Str("to"))))
|
||||||
}, changes},
|
}, changes},
|
||||||
|
"project.main-scene": {"file.scene", func(a Args) string {
|
||||||
|
return i18n.Tr("editor.agent.step.set_project", "Set {target}", i18n.Str("target", "mainScene › "+a.Str("file")))
|
||||||
|
}, changes},
|
||||||
|
"project.input-action": {"doc.project", func(a Args) string {
|
||||||
|
return i18n.Tr("editor.agent.step.set_project", "Set {target}", i18n.Str("target", "input › "+a.Str("name")))
|
||||||
|
}, changes},
|
||||||
"project.get": {"doc.project", func(Args) string {
|
"project.get": {"doc.project", func(Args) string {
|
||||||
return i18n.Tr("editor.agent.step.read_project", "Read project settings")
|
return i18n.Tr("editor.agent.step.read_project", "Read project settings")
|
||||||
}, ok},
|
}, ok},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,8 @@ import (
|
|||||||
|
|
||||||
func projectCommands() []*Command {
|
func projectCommands() []*Command {
|
||||||
return []*Command{
|
return []*Command{
|
||||||
|
{Name: "project.main-scene", Doc: "Set an imported scene as the startup scene by project-relative path.", Args: []Arg{argFile}, Write: true, Run: (*Session).mainScene},
|
||||||
|
{Name: "project.input-action", Doc: "Create/update one action and add it to a context. Binding strings use the project input syntax. Only an empty initial stack is initialized.", Write: true, Args: []Arg{{Name: "name", Type: "string", Required: true, Pos: true}, {Name: "type", Type: "string", Required: true}, {Name: "bind", Type: "array", Items: map[string]any{"type": "string"}, Required: true}, {Name: "context", Type: "string", Doc: "Gameplay by default"}, {Name: "active", Type: "boolean", Doc: "Add the context to the initial stack if absent"}}, Run: (*Session).inputAction},
|
||||||
{Name: "project.validate", Doc: "missing assets and nodes, import errors, prefabs and scenes that do not load", Run: (*Session).projectValidate},
|
{Name: "project.validate", Doc: "missing assets and nodes, import errors, prefabs and scenes that do not load", Run: (*Session).projectValidate},
|
||||||
{Name: "project.get", Doc: "module sections of project.aego (physics, …) with their current values",
|
{Name: "project.get", Doc: "module sections of project.aego (physics, …) with their current values",
|
||||||
Args: []Arg{{Name: "section", Type: "string", Doc: "only this section", Pos: true}}, Run: (*Session).projectGet},
|
Args: []Arg{{Name: "section", Type: "string", Doc: "only this section", Pos: true}}, Run: (*Session).projectGet},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+74
-2
File diff suppressed because it is too large
Load Diff
+25
-6
@@ -42,6 +42,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type RunRequest struct {
|
type RunRequest struct {
|
||||||
|
Camera string `json:"camera,omitempty"`
|
||||||
Scene string `json:"scene,omitempty"`
|
Scene string `json:"scene,omitempty"`
|
||||||
Frames int `json:"frames"`
|
Frames int `json:"frames"`
|
||||||
Seed uint64 `json:"seed"`
|
Seed uint64 `json:"seed"`
|
||||||
@@ -67,12 +68,14 @@ type Check struct {
|
|||||||
|
|
||||||
// Shot is a captured frame: the full image under .aego/cache/runs, and a scaled PNG for the answer.
|
// Shot is a captured frame: the full image under .aego/cache/runs, and a scaled PNG for the answer.
|
||||||
type Shot struct {
|
type Shot struct {
|
||||||
Frame int `json:"frame"`
|
ViewSignature string `json:"viewSignature,omitempty"`
|
||||||
Path string `json:"path,omitempty"`
|
CaptureID string `json:"captureID,omitempty"`
|
||||||
Width int `json:"width,omitempty"`
|
Frame int `json:"frame"`
|
||||||
Height int `json:"height,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
PNG []byte `json:"png,omitempty"`
|
Width int `json:"width,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Height int `json:"height,omitempty"`
|
||||||
|
PNG []byte `json:"png,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track is where a watched node was: at the start, at each quarter of the run and at the end.
|
// Track is where a watched node was: at the start, at each quarter of the run and at the end.
|
||||||
@@ -356,6 +359,14 @@ func (r sources) Texture(ids.AssetID) (texture.Region, bool) {
|
|||||||
return texture.Region{}, false
|
return texture.Region{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disk scene validation reads the same prepared data/colour and atlas metadata as Play.
|
||||||
|
func (r sources) TextureMetadata(id ids.AssetID) (asset.TextureMetadata, bool) {
|
||||||
|
return asset.ReadTextureMetadata(id, func(id ids.AssetID) (*asset.Entry, bool) {
|
||||||
|
e, ok := r.pl.Entry(id)
|
||||||
|
return &e, ok && !e.Missing && e.Error == ""
|
||||||
|
}, func(a asset.Artifact) ([]byte, error) { return r.pl.Artifacts().ReadFile(a.Path) })
|
||||||
|
}
|
||||||
|
|
||||||
func (r sources) Mesh(ids.AssetID) (*mesh.Mesh, bool) {
|
func (r sources) Mesh(ids.AssetID) (*mesh.Mesh, bool) {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
@@ -414,6 +425,11 @@ func (s *Session) asset(text string) (ids.AssetID, error) {
|
|||||||
if id, err := ids.ParseAssetID(text); err == nil {
|
if id, err := ids.ParseAssetID(text); err == nil {
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
for _, builtin := range asset.Builtins() {
|
||||||
|
if strings.EqualFold(text, builtin.Name) {
|
||||||
|
return builtin.ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
rel, err := s.rel(text)
|
rel, err := s.rel(text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ids.AssetID{}, err
|
return ids.AssetID{}, err
|
||||||
@@ -425,6 +441,9 @@ func (s *Session) asset(text string) (ids.AssetID, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) label(id ids.AssetID) string {
|
func (s *Session) label(id ids.AssetID) string {
|
||||||
|
if builtin, ok := asset.LookupBuiltin(id); ok {
|
||||||
|
return builtin.Name
|
||||||
|
}
|
||||||
if rel, ok := s.Pipeline.PathOf(id); ok {
|
if rel, ok := s.Pipeline.PathOf(id); ok {
|
||||||
return rel
|
return rel
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"aego/asset"
|
||||||
|
"aego/format/aetext"
|
||||||
|
)
|
||||||
|
|
||||||
|
func textureCommandFixture(t *testing.T) *Session {
|
||||||
|
t.Helper()
|
||||||
|
s := materialSession(t)
|
||||||
|
img := image.NewNRGBA(image.Rect(0, 0, 4, 4))
|
||||||
|
for y := 0; y < 4; y++ {
|
||||||
|
for x := 0; x < 4; x++ {
|
||||||
|
img.SetNRGBA(x, y, color.NRGBA{R: byte(x * 60), A: 255})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := png.Encode(&b, img); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(s.Project.Root, "assets/height.png"), b.Bytes(), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, _, err := s.Pipeline.Scan(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if batch, err := s.Pipeline.ImportAll(); err != nil || len(batch.Failed) != 0 {
|
||||||
|
t.Fatalf("fixture import %v %v", err, batch)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
func TestTextureCommandsShareAtomicMetadataMutationsAndImageRevision(t *testing.T) {
|
||||||
|
s := textureCommandFixture(t)
|
||||||
|
file := "assets/height.png"
|
||||||
|
get := materialCommand(t, s, "texture.get", Args{"file": file}).Value.(map[string]any)
|
||||||
|
hash := get["sourceHash"].(string)
|
||||||
|
materialCommand(t, s, "texture.set", Args{"file": file, "setting": "role", "value": "height", "expected": hash})
|
||||||
|
before, _ := os.ReadFile(filepath.Join(s.Project.Root, file+".meta"))
|
||||||
|
if _, err := Run(s, "texture.set", Args{"file": file, "setting": "height_strength", "value": "2", "expected": hash}); err == nil {
|
||||||
|
t.Fatal("stale metadata hash accepted")
|
||||||
|
}
|
||||||
|
if _, err := Run(s, "texture.apply", Args{"file": file, "tx": `tx version=1 {op "texture-setting" {path="generate_normal" value=true} op "texture-setting" {path="height_strength" value="bad"}}`}); err == nil {
|
||||||
|
t.Fatal("invalid typed batch accepted")
|
||||||
|
}
|
||||||
|
after, _ := os.ReadFile(filepath.Join(s.Project.Root, file+".meta"))
|
||||||
|
if !bytes.Equal(before, after) {
|
||||||
|
t.Fatal("failed batch partially wrote metadata")
|
||||||
|
}
|
||||||
|
materialCommand(t, s, "texture.set", Args{"file": file, "setting": "generate_normal", "value": "true", "dry-run": true})
|
||||||
|
after, _ = os.ReadFile(filepath.Join(s.Project.Root, file+".meta"))
|
||||||
|
if !bytes.Equal(before, after) {
|
||||||
|
t.Fatal("dry run wrote metadata")
|
||||||
|
}
|
||||||
|
hash = materialCommand(t, s, "texture.get", Args{"file": file}).Value.(map[string]any)["sourceHash"].(string)
|
||||||
|
image, _ := os.ReadFile(filepath.Join(s.Project.Root, file))
|
||||||
|
image = append(image, 0)
|
||||||
|
if err := os.WriteFile(filepath.Join(s.Project.Root, file), image, 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := Run(s, "texture.set", Args{"file": file, "setting": "generate_normal", "value": "true", "expected": hash}); err == nil {
|
||||||
|
t.Fatal("source image change was not in optimistic revision")
|
||||||
|
}
|
||||||
|
materialCommand(t, s, "texture.set", Args{"file": file, "setting": "generate_normal", "value": "true"})
|
||||||
|
out := materialCommand(t, s, "texture.validate", Args{"file": file})
|
||||||
|
if out.Value.(map[string]any)["importChecked"] != true || out.Value.(map[string]any)["gpuLinked"] != false {
|
||||||
|
t.Fatal("validation claimed GPU link or did not run importer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestTextureCommandsUseDirtySharedMetadataDocument(t *testing.T) {
|
||||||
|
s := textureCommandFixture(t)
|
||||||
|
file := "assets/height.png"
|
||||||
|
before, _ := os.ReadFile(filepath.Join(s.Project.Root, file+".meta"))
|
||||||
|
m, err := asset.ParseSourceMeta(before, file+".meta")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
m.Settings["role"], m.Settings["generate_normal"] = aetext.Ident("height"), aetext.Bool(true)
|
||||||
|
host := &auditMaterialHost{text: map[string]string{file + ".meta": string(asset.MarshalSourceMeta(m))}}
|
||||||
|
s.host = host
|
||||||
|
materialCommand(t, s, "texture.set", Args{"file": file, "setting": "height_strength", "value": "4"})
|
||||||
|
edited, err := asset.ParseSourceMeta([]byte(host.text[file+".meta"]), file+".meta")
|
||||||
|
if err != nil || edited.Settings.Float("height_strength", 1) != 4 || !edited.Settings.Bool("generate_normal", false) {
|
||||||
|
t.Fatal("mutation ignored dirty metadata", err)
|
||||||
|
}
|
||||||
|
after, _ := os.ReadFile(filepath.Join(s.Project.Root, file+".meta"))
|
||||||
|
if !bytes.Equal(before, after) {
|
||||||
|
t.Fatal("editor-session texture mutation wrote disk behind history")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"image/png"
|
||||||
|
)
|
||||||
|
|
||||||
|
func variantCommand() *Command {
|
||||||
|
return &Command{Name: "asset.import-variant", Write: true, Doc: "Import an inspected generated PNG variant from this editor conversation as a stable project source asset", Args: []Arg{{Name: "variant", Type: "string", Required: true}, {Name: "path", Type: "string", Required: true}, {Name: "replace", Type: "boolean"}}, Look: Look{}, Run: func(s *Session, a Args) (*Output, error) {
|
||||||
|
host, ok := s.host.(interface{ GeneratedImage(string) ([]byte, bool) })
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("requires an active editor generator session")
|
||||||
|
}
|
||||||
|
data, ok := host.GeneratedImage(a.Str("variant"))
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("variant is missing or belongs to a previous conversation")
|
||||||
|
}
|
||||||
|
path, err := pngPath(a.Str("path"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
img, err := png.Decode(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.drawn(a, path, img, img, nil, false, nil, nil)
|
||||||
|
}}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Package backend runs a model for the Agent panel behind one interface: the Claude Code CLI, or a scripted fake for tests (D341).
|
// Package backend runs Claude Code or Codex for the Agent panel behind one interface.
|
||||||
package backend
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -24,6 +24,7 @@ const (
|
|||||||
const ToolPrefix = "mcp__aego__"
|
const ToolPrefix = "mcp__aego__"
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
|
Images [][]byte
|
||||||
Kind EventKind
|
Kind EventKind
|
||||||
Text string
|
Text string
|
||||||
Tool string
|
Tool string
|
||||||
@@ -32,6 +33,9 @@ type Event struct {
|
|||||||
Result string
|
Result string
|
||||||
Failed bool
|
Failed bool
|
||||||
Err error
|
Err error
|
||||||
|
// Delta appends Text verbatim to MessageID instead of starting a complete text block.
|
||||||
|
Delta bool
|
||||||
|
MessageID string
|
||||||
// Cost in US dollars and Millis of the answer, on EvTurnEnd.
|
// Cost in US dollars and Millis of the answer, on EvTurnEnd.
|
||||||
Cost float64
|
Cost float64
|
||||||
Millis int
|
Millis int
|
||||||
@@ -42,11 +46,15 @@ type Event struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
Binding string
|
||||||
Dir string
|
Dir string
|
||||||
Command string
|
Command string
|
||||||
MCPURL string
|
MCPURL string
|
||||||
Token string
|
Token string
|
||||||
System string
|
System string
|
||||||
|
// Empty Model and Effort inherit the selected CLI's configuration.
|
||||||
|
Model string
|
||||||
|
Effort string
|
||||||
// Record gets every line the CLI writes, for recording sessions the tests replay.
|
// Record gets every line the CLI writes, for recording sessions the tests replay.
|
||||||
Record io.Writer
|
Record io.Writer
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,75 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClaudeProcessFixture(t *testing.T) {
|
||||||
|
if os.Getenv("AEGO_CLAUDE_PROCESS_FIXTURE") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < 200; i++ {
|
||||||
|
fmt.Fprintln(os.Stdout, `{"type":"assistant","message":{"content":[{"type":"text","text":"stream"}]}}`)
|
||||||
|
}
|
||||||
|
_, _ = io.Copy(io.Discard, os.Stdin)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClaudeCloseReapsProcessWithUndrainedEvents(t *testing.T) {
|
||||||
|
cmd := exec.Command(os.Args[0], "-test.run=^TestClaudeProcessFixture$")
|
||||||
|
cmd.Env = append(os.Environ(), "AEGO_CLAUDE_PROCESS_FIXTURE=1")
|
||||||
|
conf := filepath.Join(t.TempDir(), "mcp.json")
|
||||||
|
if err := os.WriteFile(conf, []byte("{}"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
c := &claudeConv{cmd: cmd, conf: conf, events: make(chan Event, 64), done: make(chan struct{})}
|
||||||
|
var err error
|
||||||
|
c.in, err = cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cmd.Stderr = &c.stderr
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
go c.read(out)
|
||||||
|
t.Cleanup(func() { _ = c.Close() })
|
||||||
|
deadline := time.Now().Add(5 * time.Second)
|
||||||
|
for len(c.events) < cap(c.events) && time.Now().Before(deadline) {
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
if len(c.events) != cap(c.events) {
|
||||||
|
t.Fatal("fixture did not reach a blocked event delivery")
|
||||||
|
}
|
||||||
|
if err := c.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
timeout := time.NewTimer(5 * time.Second)
|
||||||
|
defer timeout.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _, ok := <-c.Events():
|
||||||
|
if !ok {
|
||||||
|
if cmd.ProcessState == nil {
|
||||||
|
t.Fatal("child was not reaped")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(conf); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("temporary MCP credentials still exist: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-timeout.C:
|
||||||
|
t.Fatal("closing a replaced Claude conversation leaked its process or reader")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package backend
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -57,6 +58,11 @@ func (e rpcErr) Error() string {
|
|||||||
|
|
||||||
// CallRaw is the result of a tool call as the server sent it, images included.
|
// CallRaw is the result of a tool call as the server sent it, images included.
|
||||||
func (c *Client) CallRaw(tool string, args map[string]any) (json.RawMessage, error) {
|
func (c *Client) CallRaw(tool string, args map[string]any) (json.RawMessage, error) {
|
||||||
|
return c.CallRawContext(context.Background(), tool, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallRawContext cancels an outstanding question/tool when its turn is stopped.
|
||||||
|
func (c *Client) CallRawContext(ctx context.Context, tool string, args map[string]any) (json.RawMessage, error) {
|
||||||
body, err := json.Marshal(map[string]any{
|
body, err := json.Marshal(map[string]any{
|
||||||
"jsonrpc": "2.0", "id": c.next.Add(1), "method": "tools/call",
|
"jsonrpc": "2.0", "id": c.next.Add(1), "method": "tools/call",
|
||||||
"params": map[string]any{"name": tool, "arguments": args},
|
"params": map[string]any{"name": tool, "arguments": args},
|
||||||
@@ -64,7 +70,7 @@ func (c *Client) CallRaw(tool string, args map[string]any) (json.RawMessage, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest(http.MethodPost, c.URL, bytes.NewReader(body))
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.URL, bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEditorCatalogRejectsInvalidOrMismatchedTools(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
raw string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"named", `{"name":"scene_tree"}`, true},
|
||||||
|
{"key-only", `{}`, true},
|
||||||
|
{"null", `null`, false},
|
||||||
|
{"mismatch", `{"name":"different"}`, false},
|
||||||
|
{"invalid-type", `"scene_tree"`, false},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := verifyEditorCatalog(map[string]json.RawMessage{"scene_tree": json.RawMessage(tc.raw)}, []string{"scene_tree"})
|
||||||
|
if (err == nil) != tc.ok || err != nil && !strings.Contains(err.Error(), "scene_tree") {
|
||||||
|
t.Fatalf("catalog verification: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import "os/exec"
|
||||||
|
|
||||||
|
func hideCatalogProcess(*exec.Cmd) {}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func hideCatalogProcess(cmd *exec.Cmd) {
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
|
||||||
|
}
|
||||||
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
@@ -0,0 +1,31 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClaudeDoesNotConfuseTextSessionWithSceneAccess(t *testing.T) {
|
||||||
|
c := &claudeConv{}
|
||||||
|
c.status([]byte(`{"type":"system","subtype":"init","session_id":"one","model":"fixture","mcp_servers":[{"name":"aego","status":"connected"}],"tools":["mcp__aego__scene_tree"]}`))
|
||||||
|
if d := c.Connection(); d.Tools != 1 || d.State != "missing tools" || d.Thread != "one" {
|
||||||
|
t.Fatal(d)
|
||||||
|
}
|
||||||
|
c.status([]byte(`{"type":"control_response","response":{"request_id":"aego-status","subtype":"success","response":{"mcpServers":[{"name":"aego","status":"connected","tools":[{"name":"editor_scene_context"},{"name":"scene_tree"},{"name":"script_read"}]}]}}}`))
|
||||||
|
if d := c.Connection(); d.State != "tools listed" || d.Tools != 3 {
|
||||||
|
t.Fatal(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodexRefusesMissingToolAndStaleEditorBinding(t *testing.T) {
|
||||||
|
for _, tt := range []struct{ mode, state, reason string }{{"missing-scene-tool", "missing tools", "scene_tree"}, {"missing-context", "missing tools", "editor_scene_context"}, {"old-editor", "call rejected", "different editor session"}} {
|
||||||
|
t.Run(tt.mode, func(t *testing.T) {
|
||||||
|
c := startCodexFixture(t, tt.mode, func(cfg *Config) { cfg.Binding = "new-editor" })
|
||||||
|
evs := codexEventUntil(t, c, func(e Event) bool { return e.Kind == EvError })
|
||||||
|
err := evs[len(evs)-1].Err
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.reason) || c.Connection().State != tt.state {
|
||||||
|
t.Fatalf("%+v: %v", c.Connection(), err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"image/png"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toolImages(raw json.RawMessage) [][]byte {
|
||||||
|
// Claude nests the base64 source; Codex reports MCP content directly.
|
||||||
|
var b []struct {
|
||||||
|
Type, MimeType, Data string
|
||||||
|
Source struct {
|
||||||
|
Type string
|
||||||
|
Mime string `json:"media_type"`
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var r struct{ Content json.RawMessage }
|
||||||
|
if len(raw) > 0 && raw[0] == '{' {
|
||||||
|
if json.Unmarshal(raw, &r) != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw = r.Content
|
||||||
|
}
|
||||||
|
if json.Unmarshal(raw, &b) != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var out [][]byte
|
||||||
|
for _, v := range b {
|
||||||
|
if v.Type != "image" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, mime := v.Data, v.MimeType
|
||||||
|
if v.Source.Type == "base64" {
|
||||||
|
data, mime = v.Source.Data, v.Source.Mime
|
||||||
|
}
|
||||||
|
if mime != "image/png" || len(data) > 7<<20 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(data)
|
||||||
|
if err != nil || len(decoded) > 5<<20 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c, err := png.DecodeConfig(bytes.NewReader(decoded))
|
||||||
|
if err != nil || c.Width < 1 || c.Height < 1 || c.Width > 16384 || c.Height > (16<<20)/c.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, decoded)
|
||||||
|
if len(out) == 3 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToolImagesDecodeProviderShapes(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := png.Encode(&b, image.NewNRGBA(image.Rect(0, 0, 7, 3))); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
data := base64.StdEncoding.EncodeToString(b.Bytes())
|
||||||
|
for _, raw := range []any{map[string]any{"content": []any{map[string]any{"type": "image", "mimeType": "image/png", "data": data}}}, []any{map[string]any{"type": "image", "source": map[string]any{"type": "base64", "media_type": "image/png", "data": data}}}} {
|
||||||
|
s, _ := json.Marshal(raw)
|
||||||
|
g := toolImages(s)
|
||||||
|
if len(g) != 1 || !bytes.Equal(g[0], b.Bytes()) {
|
||||||
|
t.Fatal("lost provider image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(toolImages(json.RawMessage(`[{"type":"text","text":"data:image/png;base64,test"}]`))) != 0 {
|
||||||
|
t.Fatal("text is not an image block")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// ForProvider selects a CLI without starting a process or reading its credentials.
|
||||||
|
func ForProvider(name string) (Backend, error) {
|
||||||
|
switch name {
|
||||||
|
case "", "claude":
|
||||||
|
return Claude{}, nil
|
||||||
|
case "codex":
|
||||||
|
return Codex{}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown agent provider %q", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"aego/modules/display"
|
"aego/modules/display"
|
||||||
"aego/modules/gameplay"
|
"aego/modules/gameplay"
|
||||||
inputmod "aego/modules/input"
|
inputmod "aego/modules/input"
|
||||||
|
"aego/modules/particles"
|
||||||
"aego/modules/physics"
|
"aego/modules/physics"
|
||||||
"aego/modules/render2d"
|
"aego/modules/render2d"
|
||||||
"aego/modules/render3d"
|
"aego/modules/render3d"
|
||||||
@@ -38,6 +39,10 @@ func render2dEntry() assembly.Entry {
|
|||||||
return assembly.Entry{ID: render2d.ID, New: func(assembly.Options) module.Module { return render2d.New() }}
|
return assembly.Entry{ID: render2d.ID, New: func(assembly.Options) module.Module { return render2d.New() }}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func particlesEntry() assembly.Entry {
|
||||||
|
return assembly.Entry{ID: particles.ID, Requires: []string{render2d.ID}, New: func(assembly.Options) module.Module { return particles.New() }}
|
||||||
|
}
|
||||||
|
|
||||||
func render3dEntry() assembly.Entry {
|
func render3dEntry() assembly.Entry {
|
||||||
return assembly.Entry{ID: render3d.ID, New: func(assembly.Options) module.Module { return render3d.New() }}
|
return assembly.Entry{ID: render3d.ID, New: func(assembly.Options) module.Module { return render3d.New() }}
|
||||||
}
|
}
|
||||||
@@ -110,5 +115,5 @@ func updateEntry() assembly.Entry {
|
|||||||
|
|
||||||
func Default() assembly.Catalog {
|
func Default() assembly.Catalog {
|
||||||
return assembly.Catalog{core(), dataEntry(), inputEntry(), render2dEntry(), render3dEntry(), scriptEntry(), physicsEntry(), debugEntry(),
|
return assembly.Catalog{core(), dataEntry(), inputEntry(), render2dEntry(), render3dEntry(), scriptEntry(), physicsEntry(), debugEntry(),
|
||||||
displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), animationEntry(), cameraEntry(), gameplayEntry(), spawnEntry(), savegameEntry(), updateEntry(), artEntry()}
|
displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), animationEntry(), cameraEntry(), gameplayEntry(), spawnEntry(), savegameEntry(), updateEntry(), artEntry(), particlesEntry()}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
gameplayeditor "aego/modules/gameplay/editor"
|
gameplayeditor "aego/modules/gameplay/editor"
|
||||||
inputmod "aego/modules/input"
|
inputmod "aego/modules/input"
|
||||||
inputeditor "aego/modules/input/editor"
|
inputeditor "aego/modules/input/editor"
|
||||||
|
"aego/modules/particles"
|
||||||
|
particleseditor "aego/modules/particles/editor"
|
||||||
"aego/modules/physics"
|
"aego/modules/physics"
|
||||||
physicseditor "aego/modules/physics/editor"
|
physicseditor "aego/modules/physics/editor"
|
||||||
"aego/modules/render2d"
|
"aego/modules/render2d"
|
||||||
@@ -23,6 +25,7 @@ import (
|
|||||||
func With(c assembly.Catalog) assembly.Catalog {
|
func With(c assembly.Catalog) assembly.Catalog {
|
||||||
return c.
|
return c.
|
||||||
With(inputmod.ID, inputeditor.Hook).
|
With(inputmod.ID, inputeditor.Hook).
|
||||||
|
With(particles.ID, particleseditor.Hook).
|
||||||
With(render2d.ID, render2deditor.Hook).
|
With(render2d.ID, render2deditor.Hook).
|
||||||
With(render3d.ID, render3deditor.Hook).
|
With(render3d.ID, render3deditor.Hook).
|
||||||
With(physics.ID, physicseditor.Hook).
|
With(physics.ID, physicseditor.Hook).
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user