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

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

Read tools keep large reads small: scene_tree up to a number of nodes and levels with the rest counted and a node argument for a part, docs_search up to a number of results, script_read in parts for a long script. The limits are in the popup and in prefs.aetext. MCP results are compact JSON. scene_run through the editor answers ok when the game ran.
2026-10-26 15:11:53 +03:00

342 lines
10 KiB
Go

package edit
import (
"fmt"
"image"
"image/png"
"os"
"path/filepath"
"strconv"
"strings"
"aego/app/edit/agentbridge"
"aego/core/ids"
"aego/editor/core/agentui"
"aego/editor/core/animedit"
"aego/editor/core/command"
"aego/editor/core/context"
"aego/editor/core/presence"
"aego/editor/core/vectors"
"aego/editor/core/workspace"
agentpanel "aego/editor/panels/agent"
animpanel "aego/editor/panels/animation"
"aego/editor/panels/animator"
"aego/editor/panels/gameview"
"aego/editor/shell"
"aego/ui/theme"
)
// EnvShots makes the editor write the documentation screenshots to this directory and exit.
const EnvShots = "AEGO_SHOTS"
type shot struct {
name string
frames int
inset int
bottom int
setup func(s *Session) error
}
func (s *Session) shots(dir string) error {
defer s.Close()
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
if !s.Step() {
return fmt.Errorf("the editor quit")
}
ctx := s.App.Context()
scene := func() { ctx.Open(context.At("scenes/main.aescene", 1, 1)) }
frameAll := func() error {
if err := ctx.Commands.Run(command.EditSelectAll); err != nil {
return err
}
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
return err
}
for i := 0; i < 30; i++ {
s.Step()
}
return nil
}
list := []shot{
{name: "editor", frames: 240, setup: func(s *Session) error {
scene()
s.App.Workspace().Show(workspace.Inspector)
if err := frameAll(); err != nil {
return err
}
return selectNamed(ctx, "Player")
}},
{name: "agent-follow", frames: 60, setup: func(s *Session) error {
f := s.App.Follow()
f.SetOn(true)
now := s.Engine.Platform().Now()
f.Step("scene_set", `{"file":"scenes/main.aescene","node":"Player","path":"Motion2D.maxSpeed","value":"9"}`, now)
sc := ctx.Scene()
player := sc.ID(sc.Find(sc.Root(), "Player"))
s.App.Presence().Light(presence.Flash{Spot: presence.Spot{Doc: "scenes/main.aescene", Node: player, Type: "gameplay.Motion2D", Field: "maxSpeed"}, At: now})
ctx.Select()
return nil
}},
{name: "hitbox-gizmo", frames: 60, setup: func(s *Session) error {
s.App.Follow().SetOn(false)
s.App.Presence().Clear()
s.App.AgentSelection().Clear()
sc := ctx.Scene()
var span []ids.NodeID
for _, n := range []string{"EnemyLeft", "PatrolA1", "PatrolB1"} {
span = append(span, sc.ID(sc.Find(sc.Root(), n)))
}
ctx.Select(span...)
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
return err
}
for i := 0; i < 30; i++ {
s.Step()
}
return selectNamed(ctx, "EnemyLeft")
}},
{name: "camera-follow", frames: 60, setup: func(s *Session) error {
sc := ctx.Scene()
ctx.Select(sc.ID(sc.Find(sc.Root(), "Camera")), sc.ID(sc.Find(sc.Root(), "Player")))
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
return err
}
for i := 0; i < 30; i++ {
s.Step()
}
return selectNamed(ctx, "Camera")
}},
{name: "camera-zone", frames: 60, setup: func(s *Session) error {
sc := ctx.Scene()
ctx.Select(sc.ID(sc.Find(sc.Root(), "ArenaZone")), sc.ID(sc.Find(sc.Root(), "PatrolA2")), sc.ID(sc.Find(sc.Root(), "PatrolB2")), sc.ID(sc.Find(sc.Root(), "GroundRight")))
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
return err
}
for i := 0; i < 30; i++ {
s.Step()
}
return selectNamed(ctx, "ArenaZone")
}},
{name: "clip-inspector", frames: 60, setup: func(s *Session) error {
id, ok := s.Pipeline.Lookup("assets/audio/music.ogg")
if !ok {
return fmt.Errorf("no assets/audio/music.ogg")
}
s.App.Data().Inspect(id)
return nil
}},
{name: "add-required", frames: 30, setup: func(s *Session) error {
if err := selectNamed(ctx, "PatrolA1"); err != nil {
return err
}
s.App.Inspector().OpenAdd("follow")
return nil
}},
{name: "add-palette", frames: 30, setup: func(s *Session) error {
s.App.AddPalette().Close()
if err := selectNamed(ctx, "PatrolA1"); err != nil {
return err
}
s.App.AddPalette().AddComponent(ctx.Selected(), nil)
s.App.AddPalette().Palette().SetQuery("col")
return nil
}},
{name: "tags", frames: 60, setup: func(s *Session) error {
s.App.AddPalette().Close()
ctx.UI.CloseAllPopups()
return selectNamed(ctx, "Player")
}},
{name: "prefab", frames: 60, setup: func(s *Session) error {
return selectNamed(ctx, "EnemyLeft")
}},
{name: "settings-input", frames: 60, setup: func(s *Session) error {
ctx.Open(context.SettingsAt("input"))
return nil
}},
{name: "settings-physics", frames: 60, setup: func(s *Session) error {
ctx.Open(context.SettingsAt("physics"))
return nil
}},
{name: "animator", frames: 60, setup: func(s *Session) error {
scene()
s.App.Workspace().Switch(workspace.Design)
s.App.Workspace().Show(animator.ID)
return selectNamed(ctx, "Player")
}},
{name: "attack-preview", frames: 30, setup: func(s *Session) error {
scene()
if err := selectNamed(ctx, "Player"); err != nil {
return err
}
s.App.Workspace().Show(animpanel.ID)
for range 5 {
s.Step()
}
if !s.App.AnimationPanel().PickClip("assets/anim/player_hit.aeanim") {
return fmt.Errorf("the Player has no player_hit clip")
}
for range 3 {
s.Step()
}
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
return err
}
svc := ctx.Anim.For(ctx.Doc())
svc.SetMode(animedit.Preview)
svc.SetTime(0.17)
return nil
}},
{name: "slicer", frames: 60, setup: func(s *Session) error {
svc := ctx.Anim.For(ctx.Doc())
svc.SetMode(animedit.Normal)
for range 3 {
s.Step()
}
if m := svc.Mode(); m != animedit.Normal {
return fmt.Errorf("the clip is still in mode %d", m)
}
s.App.Workspace().Show(workspace.Inspector)
id, ok := s.Pipeline.Lookup("assets/sprites/slash.png")
if !ok {
return fmt.Errorf("no assets/sprites/slash.png")
}
s.App.Data().Inspect(id)
return nil
}},
{name: "script", frames: 90, setup: func(s *Session) error {
ctx.Open(context.At("scripts/player/player.aes", 1, 1))
return nil
}},
{name: "workspace-programming", frames: 60, setup: func(s *Session) error {
s.App.Workspace().Switch(workspace.Programming)
return nil
}},
{name: "play", frames: 150, setup: func(s *Session) error {
s.App.Workspace().Switch(workspace.Design)
scene()
return ctx.Commands.Run(command.PlayStart)
}},
{name: "motion-play", frames: 30, setup: func(s *Session) error {
s.App.Workspace().Show(workspace.Inspector)
s.App.Inspector().ScrollTo("gameplay.Motion2D")
return selectNamed(ctx, "Player")
}},
{name: "mixer", frames: 120, setup: func(s *Session) error {
s.App.Workspace().Switch(workspace.Audio)
s.App.Workspace().Show(workspace.Mixer)
return nil
}},
{name: "physics-debug", frames: 30, setup: func(s *Session) error {
s.App.Workspace().Switch(workspace.Design)
ctx.Overlay.Set("physics.debug.contacts", true)
return ctx.Commands.Run(gameview.CommandOverlays)
}},
{name: "demo", frames: 60, inset: 2, bottom: 28, setup: func(s *Session) error {
for _, f := range []string{gameview.CommandOverlays, vectors.CommandToggle} {
ctx.Overlay.Set(f, false)
}
return ctx.Commands.Run(shell.CommandFullscreenPlay)
}},
{name: "prefab-mode", frames: 120, setup: func(s *Session) error {
if err := ctx.Commands.Run(command.PlayStop); err != nil {
return err
}
id, ok := s.Pipeline.Lookup("prefabs/enemy.aeprefab")
if !ok {
return fmt.Errorf("no prefabs/enemy.aeprefab")
}
return s.App.Prefabs().Open(id)
}},
{name: "agent-context", frames: 30, setup: func(s *Session) error {
p := s.App.Agent()
if p == nil {
return fmt.Errorf("no Agent panel")
}
s.App.Workspace().Show(agentpanel.ID)
p.ShowMeter(shotMeter())
p.OpenContext(agentui.CatScene)
return nil
}},
}
if s.App.ThemeMode() == theme.ModeLight {
for i := range list {
list[i].name += "-light"
}
}
for _, sh := range list {
if err := sh.setup(s); err != nil {
return fmt.Errorf("%s: %w", sh.name, err)
}
if err := s.capture(sh, filepath.Join(dir, sh.name+".png")); err != nil {
return fmt.Errorf("%s: %w", sh.name, err)
}
}
return nil
}
func selectNamed(ctx *context.Editor, name string) error {
sc := ctx.Scene()
if sc == nil {
return fmt.Errorf("no scene is open")
}
h := sc.Find(sc.Root(), name)
if !sc.IsAlive(h) {
return fmt.Errorf("the scene has no node %q", name)
}
ctx.Select(sc.ID(h))
return nil
}
func (s *Session) capture(sh shot, path string) error {
for i := 0; i < sh.frames; i++ {
if !s.Step() {
return fmt.Errorf("the editor quit")
}
}
var img *image.RGBA
var err error
s.Engine.CaptureFrame(func(got *image.RGBA, e error) { img, err = got, e })
s.Step()
if err != nil {
return err
}
if img == nil {
return fmt.Errorf("no frame was presented")
}
b := img.Bounds()
crop := img.SubImage(image.Rect(b.Min.X+sh.inset, b.Min.Y+sh.inset, b.Max.X-sh.inset, b.Max.Y-sh.inset-sh.bottom))
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return png.Encode(f, crop)
}
// shotMeter is a session three quarters into a 200k window, for the Context popup's screenshot.
func shotMeter() *agentui.Meter {
tree := strings.Repeat(`{"id":"#c0000000000000000000000000000003","name":"Platform","path":"Root/Platform","components":["engine.Body2D"]},`, 900)
m := agentui.NewMeter(agentbridge.Guide, strings.Repeat(`{"name":"scene_tree","description":"node tree"},`, 400))
m.SetModel("claude-opus-5-5")
m.User("Why does the player fall through the moving platform?")
m.Usage(21_400)
reads := []struct{ tool, label, detail, text string }{
{"scene_tree", "Read scene main.aescene", "48 nodes", tree},
{"scene_get", "Read Player", "8 components", tree[:6000]},
{"script_read", "Read player.aes", "212 lines", strings.Repeat("\tif m.OnGround() && input.Pressed(\"Jump\") {\n\t\tm.Jump()\n\t}\n", 700)},
{"docs_search", "Search docs: moving platform", "10 results", strings.Repeat("Moving platforms carry the body that stands on them. ", 200)},
{"project_get", "Read project settings", "", tree[:9000]},
}
for i, r := range reads {
id := strconv.Itoa(i)
m.Call(r.tool, id, "{}", r.label)
m.Result(r.tool, id, r.text, r.detail, false)
}
m.Text("The platform moves in the render step, so the body lags one frame behind it.")
m.Usage(152_300)
m.SetWindow(200_000)
return m
}