Documentation
¶
Overview ¶
Package ebitenmcp turns a running Ebitengine game into something an agent can see and drive: frames, state, traces and synthetic input, over MCP.
The whole integration is one line, replacing ebiten.RunGame with this package's. Nothing happens until EBITEN_MCP_ADDR is set, so the call can stay in a shipped build without opening a port or starting a goroutine. Setting it is what opens one, and there is no authentication behind it — see the README.
Index ¶
- Constants
- Variables
- func GamepadsAvailable() error
- func RunGame(game ebiten.Game, opts ...Option) error
- func RunGameWithOptions(game ebiten.Game, ebitenOptions *ebiten.RunGameOptions, opts ...Option) error
- func RunTests(m *testing.M, factory func() ebiten.Game, opts ...Option)
- func UpdateGolden() bool
- type Artifact
- type Artifacts
- type CommandPanic
- type CompareOutput
- type Crash
- type Driver
- func (d *Driver) Click(x, y float64, button ...ebiten.MouseButton)
- func (d *Driver) Drag(x0, y0, x1, y1 float64, steps int)
- func (d *Driver) Golden(name string)
- func (d *Driver) GoldenImage(name string, got *image.RGBA)
- func (d *Driver) Hold(ticks int, keys ...ebiten.Key)
- func (d *Driver) Inspect(path string) any
- func (d *Driver) KeyDown(keys ...ebiten.Key)
- func (d *Driver) KeyUp(keys ...ebiten.Key)
- func (d *Driver) Move(x, y float64)
- func (d *Driver) Runtime() *Runtime
- func (d *Driver) Screenshot() *image.RGBA
- func (d *Driver) ScreenshotStage(stage Stage) *image.RGBA
- func (d *Driver) Scroll(x, y float64)
- func (d *Driver) Tap(keys ...ebiten.Key)
- func (d *Driver) Tick(n int)
- func (d *Driver) Type(text string)
- func (d *Driver) WithGame(fn func(ebiten.Game))
- type Frame
- type FrameOutput
- type FrameTiming
- type FramesOutput
- type FrametimesOutput
- type GamepadIdentity
- type GamepadState
- type Gamepads
- func (g *Gamepads) Apply(id ebiten.GamepadID, buttons map[uint16]bool, axes map[uint16]int32) error
- func (g *Gamepads) Axis(id ebiten.GamepadID, code uint16, value int32) error
- func (g *Gamepads) Button(id ebiten.GamepadID, code uint16, pressed bool) error
- func (g *Gamepads) Close()
- func (g *Gamepads) Connect(ctx context.Context, identity GamepadIdentity) (ebiten.GamepadID, error)
- func (g *Gamepads) Connected() []ebiten.GamepadID
- func (g *Gamepads) Disconnect(id ebiten.GamepadID) error
- func (g *Gamepads) Identity(id ebiten.GamepadID) (GamepadIdentity, error)
- type GoStats
- type GoroutinesOutput
- type Injector
- func (i Injector) KeyDown(key ebiten.Key)
- func (i Injector) KeyUp(key ebiten.Key)
- func (i Injector) MouseDown(button ebiten.MouseButton)
- func (i Injector) MouseUp(button ebiten.MouseButton)
- func (i Injector) MoveCursor(x, y float64)
- func (i Injector) ReleaseAll()
- func (i Injector) ReleaseCursor()
- func (i Injector) Scroll(x, y float64)
- func (i Injector) SetTouches(touches []Touch)
- func (i Injector) Type(text string)
- type InputOutput
- type InputStateOutput
- type InputStatus
- type Inspect
- type InspectOutput
- type LoopOutput
- type Offset
- type Option
- type Options
- type Percentiles
- type Point
- type ProfileOutput
- type RecordOutput
- type ResetOutput
- type RingStatus
- type Runtime
- func (r *Runtime) Capture(ctx context.Context) (*Frame, error)
- func (r *Runtime) CaptureStage(ctx context.Context, stage Stage) (*Frame, error)
- func (r *Runtime) Close() error
- func (r *Runtime) Crash() *Crash
- func (r *Runtime) DefaultStage() Stage
- func (r *Runtime) Do(ctx context.Context, fn func()) error
- func (r *Runtime) Gamepads() *Gamepads
- func (r *Runtime) HasFinalPass() bool
- func (r *Runtime) Injector() Injector
- func (r *Runtime) InputError() error
- func (r *Runtime) LastFrame() *Frame
- func (r *Runtime) LastTick() time.Time
- func (r *Runtime) Pause()
- func (r *Runtime) Paused() (paused bool, steps int)
- func (r *Runtime) RegisterState(name string, fn StateProvider)
- func (r *Runtime) Reset(ctx context.Context) error
- func (r *Runtime) Resume()
- func (r *Runtime) Ring() *frameRing
- func (r *Runtime) Server() *Server
- func (r *Runtime) SetGame(ctx context.Context, game ebiten.Game) error
- func (r *Runtime) Step(n int)
- func (r *Runtime) Terminate()
- func (r *Runtime) Tick() int64
- func (r *Runtime) Timings() []FrameTiming
- func (r *Runtime) UnregisterState(name string)
- func (r *Runtime) Uptime() time.Duration
- func (r *Runtime) WaitTicks(ctx context.Context, n int) error
- type Screen
- type ScriptOutput
- type ScriptStep
- type Server
- type Size
- type Stage
- type StateOutput
- type StateProvider
- type TPSOutput
- type Touch
- type TraceLine
- type TracesOutput
- type Truncated
- type WaitOutput
Constants ¶
const AddrEnv = wire.AddrEnv
AddrEnv is EBITEN_MCP_ADDR, the variable that turns the server on. Empty means no socket is opened and no goroutine is started, which is why leaving RunGame in a release build costs nothing. Setting it opens an unauthenticated port; bind loopback, and see the README before doing anything else with it.
const CaptureEnv = wire.CaptureEnv
CaptureEnv is EBITEN_MCP_CAPTURE, which overrides the stage captures come from. Set it to "offscreen" in a game whose final pass is expensive and whose final pass you do not care about; see WithCaptureStage for why that is not the default.
const MediaDir = ".ebitenmcp/media"
MediaDir is where artifacts are written, relative to the game's working directory. Add it to .gitignore.
const PanicRecoveryEnv = wire.PanicRecoveryEnv
PanicRecoveryEnv is EBITEN_MCP_RECOVER_PANICS. Set it to "1" to catch game panics and keep the MCP server answering. Unset or any other value leaves Ebitengine's normal panic behaviour unchanged.
const Path = wire.Path
Path is /mcp, the route the MCP endpoint is mounted on.
const RendererEnv = wire.RendererEnv
RendererEnv is EBITENMCP_RENDERER, which carries the rasteriser a run is drawing with. `ebitenmcp run` sets it after asking the display; nothing sets it when a game is started by hand, and then the checks below simply have nothing to say.
Variables ¶
var ErrLoopStalled = errors.New("ebitenmcp: the game loop did not run the request in time")
ErrLoopStalled is returned by anything that needs the game loop when the loop is not running it. A crashed game, a paused one that nobody resumed, or an Update stuck in a deadlock all look like this.
It is a distinct error because "the game is wedged" is a diagnosis, not a failure of the tool that reported it.
Functions ¶
func GamepadsAvailable ¶
func GamepadsAvailable() error
GamepadsAvailable reports why virtual controllers cannot be created, or nil when they can.
func RunGame ¶
RunGame is a drop-in replacement for ebiten.RunGame.
The game runs exactly as it would have. With EBITEN_MCP_ADDR set it also serves MCP, so an agent can watch it, drive it and ask what it is doing.
func RunGameWithOptions ¶
func RunGameWithOptions(game ebiten.Game, ebitenOptions *ebiten.RunGameOptions, opts ...Option) error
RunGameWithOptions is a drop-in replacement for ebiten.RunGameWithOptions.
func RunTests ¶
RunTests runs a package's tests against one game loop.
Put it in TestMain and take the driver in each test:
func TestMain(m *testing.M) {
ebitenmcp.RunTests(m, func() ebiten.Game { return NewGame() })
}
func TestMenu(t *testing.T) {
d := ebitenmcp.T(t)
d.Tap(ebiten.KeyArrowDown)
d.Golden("menu_selected.png")
}
It does not return: like os.Exit in a plain TestMain, it ends the process with the suite's status.
func UpdateGolden ¶
func UpdateGolden() bool
UpdateGolden reports whether -update was given, in which case golden files are rewritten instead of compared.
Types ¶
type Artifact ¶
type Artifact struct {
Path string `json:"path"`
URL string `json:"url,omitempty"`
Kind string `json:"kind"`
Width int `json:"width"`
Height int `json:"height"`
Bytes int `json:"bytes"`
// Frames and FPS describe a recording, and are left out of everything else.
//
// They are here because the answer would otherwise not say, and the two
// things that write a gif do not agree: with ffmpeg the frames are
// resampled and roughly half of them are dropped, without it every one is
// kept and played twice as fast. Same tool, same arguments, different
// machine. A caller comparing two recordings, or recording frames in order
// to step through them, needs to know which of those it got.
Frames int `json:"frames,omitempty"`
FPS float64 `json:"fps,omitempty"`
}
Artifact is one produced file, in the three forms it is needed in: on disk for a person to open, over HTTP for a client that can render it, and inline in the tool result for the agent and for the chat.
type Artifacts ¶
type Artifacts struct {
Artifact *Artifact `json:"artifact,omitempty"`
Video *Artifact `json:"video,omitempty"`
Sheet *Artifact `json:"contact_sheet,omitempty"`
}
Artifacts is what a tool produced on disk.
type CommandPanic ¶
CommandPanic is what a caller gets when the work it asked to run inside the game loop panicked.
It exists so that the panic is the caller's problem rather than everybody's. Without it the panic unwinds through the loop and kills the process — which game_inspect can do just by reading, since reflection over a value taken from an unexported field panics on Interface(). A tool marked read-only bringing down the game it is inspecting is the worst outcome available here; being told what went wrong, by a game that is still running, is much the best.
func (*CommandPanic) Error ¶
func (e *CommandPanic) Error() string
type CompareOutput ¶
type CompareOutput struct {
BeforeTick int64 `json:"before_tick"`
AfterTick int64 `json:"after_tick"`
ChangedPixels int `json:"changed_pixels"`
TotalPixels int `json:"total_pixels"`
Artifact *Artifact `json:"artifact"`
}
CompareOutput is game_compare's answer.
type Crash ¶
type Crash struct {
Value string `json:"value"`
Stack string `json:"stack"`
Tick int64 `json:"tick"`
When time.Time `json:"when"`
Phase string `json:"phase"` // "update", "draw" or "final"
}
Crash records a panic the wrapped game raised when panic recovery is enabled.
type Driver ¶
type Driver struct {
// Timeout bounds each operation. It is generous by default because a
// software renderer in CI is slow, not because anything should take long.
Timeout time.Duration
// GoldenTolerance is the per-channel difference two pixels may have and
// still count as equal.
GoldenTolerance int
// GoldenMaxDiff is the fraction of pixels allowed to differ beyond the
// tolerance before a golden comparison fails.
GoldenMaxDiff float64
// contains filtered or unexported fields
}
Driver drives the game from a test.
Every method fails the test rather than returning an error. A test that has lost the game loop has nothing useful left to do, and threading errors through every call would bury the test's own intent.
func T ¶
T returns a driver for one test, with a game freshly built by the factory.
Resetting rather than restarting is what makes tests independent inside the one loop the process is allowed.
func (*Driver) Click ¶
func (d *Driver) Click(x, y float64, button ...ebiten.MouseButton)
Click presses and releases at a position.
func (*Driver) Drag ¶
Drag walks from one point to another with the button held, over several ticks. A game that samples the cursor per tick sees a path, not a jump.
func (*Driver) Golden ¶
Golden compares the next frame against testdata/<name>.
With -update it writes the file instead, which is how a golden is created and how an intended change is accepted.
The game is held still for the capture. Without that, the frame caught is whichever one the loop happened to draw next, and any tick counter, clock or animation on screen lands on a different value each run — which makes the comparison fail sometimes and pass others, and an intermittent golden is worse than none, because it teaches people to ignore it.
func (*Driver) GoldenImage ¶
GoldenImage compares an image the caller already has.
func (*Driver) Runtime ¶
Runtime exposes the underlying runtime, for anything the driver does not wrap.
func (*Driver) Screenshot ¶
Screenshot captures the next frame at the game's own resolution.
func (*Driver) ScreenshotStage ¶
ScreenshotStage captures a particular stage.
Screenshot reads the offscreen rather than what the player sees, and for a test that is the right way round: the final screen is the size of the window, so an assertion about it — a golden above all — would depend on the monitor the test happened to run on. Ask for StageFinal when the game's own DrawFinalScreen is the thing under test, and expect to size the window yourself.
func (*Driver) Tap ¶
Tap presses keys for one tick and releases them, which is what a quick key press looks like to a game.
func (*Driver) WithGame ¶
WithGame runs fn against the game under test, inside the game loop.
Assertions are often easier to write against the real type than against an inspected tree, and this is how to do that safely. It used to be a Game() that handed the object back, which read as convenient and was a trap: the caller then touched a live game from the test's goroutine while Update was running, which is the one thing this package's whole design exists to prevent — every tool goes through Do for exactly this reason, and the escape hatch quietly did not.
fn must not block: the loop is waiting for it.
type Frame ¶
type Frame struct {
Tick int64 `json:"tick"`
Time time.Time `json:"time"`
Stage Stage `json:"stage"`
Image *image.RGBA `json:"-"`
}
Frame is one captured screen, untouched.
type FrameOutput ¶
type FrameOutput struct {
Tick int64 `json:"tick"`
Stage Stage `json:"stage"`
Artifact *Artifact `json:"artifact"`
Inline Size `json:"inline"`
}
FrameOutput is one captured frame: game_screenshot's answer, and the shape the others borrow.
type FrameTiming ¶
type FrameTiming struct {
Tick int64 `json:"tick"`
Update int64 `json:"update_ns"`
Draw int64 `json:"draw_ns"`
Wall int64 `json:"wall_ns"`
}
FrameTiming is what one tick cost.
type FramesOutput ¶
type FramesOutput struct {
Ring RingStatus `json:"ring"`
Note string `json:"note,omitempty"`
Frames int `json:"frames,omitempty"`
FirstTick int64 `json:"first_tick,omitempty"`
LastTick int64 `json:"last_tick,omitempty"`
Sheet *Artifact `json:"contact_sheet,omitempty"`
}
FramesOutput is game_frames' answer: either the buffer's status, or what it was holding.
type FrametimesOutput ¶
type FrametimesOutput struct {
Ticks int `json:"ticks"`
Update Percentiles `json:"update,omitempty"`
Draw Percentiles `json:"draw,omitempty"`
Wall Percentiles `json:"wall,omitempty"`
ActualTPS float64 `json:"actual_tps,omitempty"`
ActualFPS float64 `json:"actual_fps,omitempty"`
// Reading is the sentence that says what the numbers mean, because the
// interesting part of them is a comparison and not any one figure.
Reading string `json:"reading,omitempty"`
Timings []FrameTiming `json:"timings,omitempty"`
}
FrametimesOutput is game_frametimes' answer.
type GamepadIdentity ¶
GamepadIdentity is what a virtual controller claims to be.
The layout is deliberately not part of it. Every virtual pad has an Xbox 360 controller's buttons and axes, because Ebitengine's controller database has a complete standard mapping for that layout — so a game asking for the standard buttons gets them. A pad with a layout nothing has a mapping for would answer no to IsStandardGamepadLayoutAvailable, which is the first thing most games check, and would prove nothing about the game's real handling.
What a game does switch on is the vendor and the product, and that is worth choosing: a controller that could only ever claim to be an Xbox pad would send those games down a different path than their own hardware.
A zero field means the Xbox 360 pad's own, so setting only Vendor is a sensible thing to do.
func DefaultGamepadIdentity ¶
func DefaultGamepadIdentity() GamepadIdentity
DefaultGamepadIdentity is the Xbox 360 pad, spelled out for a caller who wants to see what the zero value means.
func (GamepadIdentity) SDLID ¶
func (i GamepadIdentity) SDLID() string
SDLID is the identifier Ebitengine builds from the four numbers, and the one a game matches on.
type GamepadState ¶
type GamepadState struct {
ID int `json:"id"`
Name string `json:"name"`
SDLID string `json:"sdl_id"`
StandardLayout bool `json:"standard_layout"`
ButtonsPressed []int `json:"buttons_pressed"`
Axes []float64 `json:"axes"`
}
GamepadState is one controller as the game sees it.
The SDL id is here because it is what a game switches on to decide what kind of controller this is, so seeing it is often the whole answer to "why is the game ignoring my gamepad".
type Gamepads ¶
type Gamepads struct {
// contains filtered or unexported fields
}
Gamepads are virtual controllers the operating system creates and Ebitengine discovers on its own.
This is the one part of the library that does not reach into Ebitengine at all, so it is also the one part that cannot break when Ebitengine changes — and it keeps working in a build with -tags ebitenmcp_nohook, where the keyboard and mouse injection is compiled out.
It covers what Ebitengine sees: buttons, axes, hats, the name and the SDL id, and the standard layout that follows from them. It does not cover a game that also opens its controller as a USB device, because a uinput device is not on the USB bus. See internal/uinput for what that means in practice.
func (*Gamepads) Apply ¶
Apply sends several changes as one movement.
Input drivers batch events and publish them together, so a stick pushed diagonally is two axis events and one sync. Sending them separately would show up as two straight movements, which is not what the player did.
func (*Gamepads) Close ¶
func (g *Gamepads) Close()
Close unplugs everything created here.
A virtual device outlives the process that made it unless it is destroyed, so skipping this would leave phantom controllers on the machine.
func (*Gamepads) Connect ¶
Connect creates a controller and returns the id the game will know it by.
It waits for the game to notice. The kernel makes the device immediately, but Ebitengine only picks it up while polling inside its own loop, so returning any earlier would hand back an id for a controller nothing can see yet — and the first button press would land nowhere with no error to explain it.
func (*Gamepads) Disconnect ¶
Disconnect unplugs a controller this package created.
type GoStats ¶
type GoStats struct {
Version string `json:"version"`
Goroutines int `json:"goroutines"`
HeapMB float64 `json:"heap_mb"`
GCCycles uint32 `json:"gc_cycles"`
}
GoStats is what the runtime says about itself.
type GoroutinesOutput ¶
type GoroutinesOutput struct {
Count int `json:"count"`
}
GoroutinesOutput is game_goroutines' answer. The dump itself comes back as text, since it is for reading rather than for parsing.
type Injector ¶
type Injector struct {
// contains filtered or unexported fields
}
Injector writes synthetic input for the game to read.
It is the layer the tools and the test driver are built on, exposed for a game that wants to drive itself from Go — a demo mode, a replay, a soak test. Reach for Driver in a test and for the MCP tools from an agent; both handle the timing described below, and this does not.
Nothing lands immediately. The wrapper applies whatever is pending inside Update, once per tick, before the game runs — so what is written here is what the game sees on its next tick.
States and events ¶
A key or a button held down, and the cursor's position, are states: they are rewritten every tick until something releases them, so writing one from any goroutine is safe and a tick's delay costs a tick.
Type and Scroll are events. They reach the game for exactly one tick and are then gone, which is what makes them behave like the real thing — and it means writing one from outside the loop races with the tick it was meant for. Use Runtime.Do to write those where the game will see them:
rt.Do(ctx, func() { rt.Injector().Type("hello") })
A build with injection compiled out, or one where the self-check could not confirm the mirror it writes through, leaves every method below doing nothing; Runtime.InputError says which. A zero Injector is a different thing and is not usable — take one from Runtime.Injector.
func (Injector) MouseDown ¶
func (i Injector) MouseDown(button ebiten.MouseButton)
MouseDown presses a mouse button and leaves it pressed.
func (Injector) MouseUp ¶
func (i Injector) MouseUp(button ebiten.MouseButton)
MouseUp releases a mouse button.
func (Injector) MoveCursor ¶
MoveCursor puts the cursor at a position in the game's logical pixels, and pins it there until ReleaseCursor.
func (Injector) ReleaseAll ¶
func (i Injector) ReleaseAll()
ReleaseAll drops every held key, button and finger, and unpins the cursor. The releases are stamped properly, so the game sees a real release rather than input that vanished.
func (Injector) ReleaseCursor ¶
func (i Injector) ReleaseCursor()
ReleaseCursor hands the cursor back to whatever the window reports.
func (Injector) SetTouches ¶
SetTouches replaces the set of fingers on the screen. Passing none lifts them all; leaving one out lifts that one.
type InputOutput ¶
type InputOutput struct {
Tick int64 `json:"tick"`
Keys []string `json:"keys,omitempty"`
Held []string `json:"held,omitempty"`
Released []string `json:"released,omitempty"`
Text string `json:"text,omitempty"`
Touches int `json:"touches,omitempty"`
MovedTo []float64 `json:"moved_to,omitempty"`
DraggedTo []float64 `json:"dragged_to,omitempty"`
Scrolled []float64 `json:"scrolled,omitempty"`
Clicked string `json:"clicked,omitempty"`
Holding string `json:"holding,omitempty"`
Button string `json:"released_button,omitempty"`
Cursor string `json:"cursor,omitempty"`
// A gamepad call goes through the same finish, so its answers live here too
// rather than in a type that would be this one with four fields added.
Connected *int `json:"connected,omitempty"`
SDLID string `json:"sdl_id,omitempty"`
GamepadID *int `json:"gamepad_id,omitempty"`
Disconnected *int `json:"disconnected,omitempty"`
}
InputOutput is what the tools that drive the game answer with: the tick the input landed in, and a description of what was done.
One type for all of them rather than one each, because every field here is "what did this call do" and a client that wants to know reads the same place whichever tool it called. The empty ones are left out.
type InputStateOutput ¶
type InputStateOutput struct {
Tick int64 `json:"tick"`
Injection InputStatus `json:"injection"`
KeysPressed []string `json:"keys_pressed"`
MouseButtons []string `json:"mouse_buttons"`
Touches []touchPoint `json:"touches"`
Gamepads []GamepadState `json:"gamepads"`
Cursor Point `json:"cursor"`
Wheel Offset `json:"wheel"`
}
InputStateOutput is game_input_state's answer: what the game currently believes about its input, read from inside the loop.
type InputStatus ¶
type InputStatus struct {
Available bool `json:"available"`
Reason string `json:"reason,omitempty"`
}
InputStatus says whether synthetic input works, and why not when it does not.
type Inspect ¶
type Inspect struct {
// MaxDepth is how far down the tree to walk. Zero means the default.
MaxDepth int
// MaxItems caps the elements taken from any one slice, array or map.
MaxItems int
// MaxNodes is the total budget, which stops a wide-but-shallow structure
// from being just as expensive as a deep one.
MaxNodes int
}
Inspect turns a live value into plain JSON-able data.
It reads unexported fields. That is the whole point: a Go game keeps virtually all of its state unexported, and an inspector that respected visibility would show an empty struct and call it a state dump. Reading is done through reflect.NewAt on an addressable value, which is why the root has to be a pointer to be useful.
The limits are not decoration. A game object reaches half the heap through pointers, so without a depth bound, an item cap and cycle detection the first call would try to serialise the world.
type InspectOutput ¶
type InspectOutput struct {
Tick int64 `json:"tick"`
Path string `json:"path"`
Value any `json:"value"`
}
InspectOutput is game_inspect's answer.
type LoopOutput ¶
type LoopOutput struct {
Tick int64 `json:"tick"`
Paused bool `json:"paused"`
QueuedSteps int `json:"queued_steps,omitempty"`
TPS int `json:"tps,omitempty"`
Waited string `json:"waited,omitempty"`
Ran int64 `json:"ran,omitempty"`
}
LoopOutput is what the tools that hold and release the game answer with.
type Option ¶
type Option func(*Options)
Option customises Options.
func WithCaptureStage ¶
WithCaptureStage sets which stage captures come from by default.
StageOffscreen skips the screen-sized copy the final pass needs, which is worth having when that pass is expensive and you are not debugging it. It is not the default, because returning the image from before a game's own final pass — without saying so — is returning something the player never saw.
func WithFactory ¶
WithFactory supplies a constructor for fresh games.
func WithMediaDir ¶ added in v0.2.0
WithMediaDir puts the artifacts somewhere writable.
The default is relative to the working directory, which an app packaged for a phone does not choose and generally cannot write to. Pass a directory the platform hands the app — Context.getFilesDir() on Android, Documents on iOS.
func WithPanicRecovery ¶ added in v0.3.0
WithPanicRecovery controls whether panics raised by the game's Update, Draw, Layout and final-screen callbacks are recorded while the MCP server keeps answering. It overrides EBITEN_MCP_RECOVER_PANICS.
func WithState ¶
func WithState(name string, fn StateProvider) Option
WithState publishes a named snapshot of the game, reachable as @name from game_inspect. The provider is handed whatever game is running when it is called, so it keeps working across game_reset.
type Options ¶
type Options struct {
// Addr is the address the MCP server listens on. Defaults to
// EBITEN_MCP_ADDR; empty means do not serve.
Addr string
// RecoverPanics keeps the server answering after the game panics. It defaults
// to whether EBITEN_MCP_RECOVER_PANICS is "1"; otherwise a wrapped game
// preserves Ebitengine's normal panic behaviour.
RecoverPanics bool
// Name identifies this game to a client that finds several running.
Name string
// Factory builds a fresh game, which is what game_reset and the test driver
// use to start over. Without it there is nothing to reset to, since
// Ebitengine's loop cannot be restarted.
Factory func() ebiten.Game
// CaptureStage is which stage a capture comes from when it does not ask for
// one. Defaults to StageFinal, and only means anything for a game that draws
// its own final screen.
CaptureStage Stage
// MediaDir is where screenshots, videos and profiles are written. Empty
// means the package's MediaDir, relative to the working directory.
//
// Which is fine wherever a game is started from a shell, and is the wrong
// bet inside an app bundle: a working directory that cannot be written to
// stops newMedia creating the default, Serve returns that error, and Wrap
// carries on with no server at all. Give it somewhere writable on any
// platform where you do not choose the working directory. See WithMediaDir.
MediaDir string
// States are named snapshots the game publishes, reachable as @name from
// game_inspect. See WithState.
States map[string]StateProvider
}
Options configures a wrapped game.
type Percentiles ¶
type Percentiles struct {
Mean string `json:"mean"`
P50 string `json:"p50"`
P95 string `json:"p95"`
P99 string `json:"p99"`
Max string `json:"max"`
}
Percentiles summarises one column of the frame timings. Durations as text, because "1.2ms" is what somebody reading this wants and a nanosecond count is not.
type ProfileOutput ¶
type ProfileOutput struct {
Kind string `json:"kind"`
Tick int64 `json:"tick"`
Artifact *Artifact `json:"artifact"`
Note string `json:"note,omitempty"`
}
ProfileOutput is game_profile's answer.
type RecordOutput ¶
type RecordOutput struct {
Frames int `json:"frames"`
Stage Stage `json:"stage"`
FirstTick int64 `json:"first_tick"`
LastTick int64 `json:"last_tick"`
TicksCovered int64 `json:"ticks_covered"`
Sheet *Artifact `json:"contact_sheet"`
Video *Artifact `json:"video,omitempty"`
// Capped says the recording stopped short of what was asked for, and why.
Capped string `json:"capped,omitempty"`
}
RecordOutput is game_record's answer.
type ResetOutput ¶
ResetOutput is game_reset's answer.
type RingStatus ¶
type RingStatus struct {
Enabled bool `json:"enabled"`
Frames int `json:"frames,omitempty"`
MemoryMB float64 `json:"memory_mb,omitempty"`
BudgetMB float64 `json:"budget_mb,omitempty"`
Every int `json:"every,omitempty"`
Stage Stage `json:"stage,omitempty"`
Encoded int64 `json:"encoded,omitempty"`
Dropped int64 `json:"dropped,omitempty"`
OldestTick int64 `json:"oldest_tick,omitempty"`
NewestTick int64 `json:"newest_tick,omitempty"`
Note string `json:"note,omitempty"`
}
RingStatus is what the retrospective buffer is doing. Reported by game_state as well, so that a buffer throwing away half of what it is given is visible rather than something to deduce from tick numbers.
type Runtime ¶
type Runtime struct {
// contains filtered or unexported fields
}
Runtime is the state behind a wrapped game: the tick counter, the command queue, the input injector and the captures.
Everything that reads or writes game state has to go through Do, which runs it inside Update. Everything that describes the runtime itself is readable from any goroutine at any time, on purpose: those are exactly the questions worth asking when the loop has stopped answering.
func Wrap ¶
Wrap returns the game to hand to Ebitengine and the runtime that drives it.
Use it when the game is started by something other than RunGame — a custom runner, a mobile entry point, a test. RunGame is the same thing plus the call to ebiten.RunGame.
The server starts here rather than in RunGame so that a custom runner gets it too. Calling Wrap explicitly always installs the wrapper, because its Runtime is useful to custom runners and tests even without a server. RunGame bypasses the wrapper when no server is available.
func (*Runtime) Capture ¶
Capture asks for the next frame the game draws, at whichever stage this runtime is configured for.
Reading pixels back is a synchronisation point with the GPU, so it only happens when somebody is waiting for one. Capturing every frame unconditionally would cost the game a large part of its frame budget for images nobody looks at.
func (*Runtime) CaptureStage ¶
CaptureStage asks for the next frame at a particular stage.
Asking for the offscreen of a game that has a final pass is how you tell whether a visual bug is in the game or in the pass — and it is what tests should use, since the final screen is the size of the window and a golden that changes with the monitor is no golden at all.
func (*Runtime) Close ¶
Close stops the MCP server, puts stdout and stderr back, drops the frame buffer and unplugs any virtual controllers. The game is untouched.
Two of those outlive the process if they are skipped, which is why this is not optional and why RunGame and RunTests both call it:
- A uinput device belongs to the kernel, not to the process that asked for it, so leaving without destroying one leaves a phantom gamepad on the machine.
- Trace capture redirects descriptors 1 and 2 through a pipe. Until they are put back, everything written to them reaches the terminal only by way of a goroutine — and os.Exit does not wait for goroutines, so whatever was written last is lost. For a test binary that is the failure message.
Safe to call twice; the second one has nothing to do.
func (*Runtime) DefaultStage ¶
DefaultStage is the stage Capture uses when nobody asks for one.
func (*Runtime) Do ¶
Do runs fn inside the game loop, at the point the game's own Update would run, and waits for it to finish.
Every caller must pass a deadline. A game that has crashed or deadlocked never drains the queue, and a debugging tool that hangs along with the thing it is debugging is worse than useless.
func (*Runtime) HasFinalPass ¶
HasFinalPass reports whether the game draws its own final screen, which is what makes the two stages different images.
func (*Runtime) Injector ¶
Injector is the handle used to synthesise input. It does nothing when injection is unavailable; InputError says why.
func (*Runtime) InputError ¶
InputError reports why input injection is unavailable, or nil when it works. It is only meaningful after the first tick, which is when the layout self-check runs.
func (*Runtime) LastFrame ¶
LastFrame is the most recently captured frame, or nil if nothing has been captured yet. Note that this is the last frame somebody asked for, not the last frame drawn: nothing is captured behind the caller's back.
func (*Runtime) LastTick ¶
LastTick is when the game last completed a tick. Compared against now, it is the cheapest way to tell a paused game from a wedged one.
func (*Runtime) Pause ¶
func (r *Runtime) Pause()
Pause stops calling the game's Update. Draw keeps running, so the last frame stays on screen and can still be captured.
func (*Runtime) RegisterState ¶
func (r *Runtime) RegisterState(name string, fn StateProvider)
RegisterState publishes a named snapshot. Registering the same name twice replaces it.
func (*Runtime) Reset ¶
Reset replaces the running game with a new one from the factory given at construction. It fails if there is no factory.
func (*Runtime) Ring ¶
func (r *Runtime) Ring() *frameRing
Ring is the retrospective frame buffer, building it on the first ask.
It is a constructor as much as a getter, which matters because game_state reports the buffer's status: merely asking what the game is doing brings the buffer into being. Empty and disabled, so it costs an allocation and nothing else, but it is why nothing downstream needs to handle a nil one.
func (*Runtime) Server ¶
Server returns the MCP server serving this game, or nil when none was started.
func (*Runtime) SetGame ¶
SetGame replaces the running game with a new one at the next tick boundary.
This exists because ebiten.RunGame cannot be called twice in a process, so "start over" cannot mean restarting the loop. Swapping the game the wrapper holds is the only way to get a fresh one, and it is what makes independent tests possible inside a single test binary.
func (*Runtime) Terminate ¶
func (r *Runtime) Terminate()
Terminate ends the game loop cleanly, as if the game had returned ebiten.Termination.
It deliberately ignores pause, steps and a recorded crash: a game held or wedged in any of those states still has to be able to shut down, and it is the only way out for a test binary that owns the process's single loop.
func (*Runtime) Tick ¶
Tick is the number of ticks the wrapped game has actually run. It is not Ebitengine's tick: a paused game keeps being ticked by the engine while this counter stands still, which is what makes stepping meaningful.
func (*Runtime) Timings ¶
func (r *Runtime) Timings() []FrameTiming
Timings returns the most recent frame timings, oldest first.
func (*Runtime) UnregisterState ¶
UnregisterState removes one.
func (*Runtime) WaitTicks ¶
WaitTicks blocks until the game has advanced n ticks. A paused game advances none, so this is also how a caller notices it is paused.
It counts against a target tick rather than counting wake-ups, and that is not a detail. Waiting for the channel n times loses any tick that lands between one wake-up and the next read of the channel, which with vsync off is most of them — so waiting for four hundred ticks would sit there long after four hundred had gone by, and report a stalled loop that had done exactly what it was asked. Reading the tick and the channel under the same lock closes that: a tick that arrives in between has either already been counted or will close the channel now held.
type Screen ¶
type Screen struct {
Width int `json:"width"`
Height int `json:"height"`
CapturedAtTick int64 `json:"captured_at_tick"`
}
Screen is the last frame's size and when it was taken.
type ScriptOutput ¶
type ScriptOutput struct {
Steps int `json:"steps"`
Ticks int64 `json:"ticks"`
Tick int64 `json:"tick"`
Captured int `json:"captured"`
Log []ScriptStep `json:"log"`
Sheet *Artifact `json:"contact_sheet,omitempty"`
}
ScriptOutput is game_script's answer.
type ScriptStep ¶
type ScriptStep struct {
Step int `json:"step"`
At int `json:"at"`
Tick int64 `json:"tick"`
Label string `json:"label,omitempty"`
}
ScriptStep is one line of a script's log: which step ran, when it was meant to, and when it did.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server exposes one running game over MCP.
func Serve ¶
Serve starts the MCP server for a wrapped game.
The listener is bound before returning, so the address in the result is the real one even when the requested port was taken.
type Stage ¶
type Stage string
Stage is which of Ebitengine's two drawing steps a frame was read from.
Ebitengine draws twice. First the game's Draw fills an offscreen at the logical resolution. Then, if the game implements ebiten.FinalScreenDrawer, its DrawFinalScreen composites that offscreen onto the real screen — and a CRT shader, scanlines, a letterbox or a custom scaling filter all live in that second step. Reading the offscreen would show the image before any of it, which is not what the player is looking at.
type StateOutput ¶
type StateOutput struct {
Name string `json:"name"`
Loop string `json:"loop"`
Tick int64 `json:"tick"`
SinceLastTick string `json:"since_last_tick"`
Uptime string `json:"uptime"`
Paused bool `json:"paused"`
QueuedSteps int `json:"queued_steps"`
TPS int `json:"tps"`
ActualTPS float64 `json:"actual_tps"`
ActualFPS float64 `json:"actual_fps"`
VSync bool `json:"vsync"`
Window Size `json:"window"`
DeviceScaleFactor float64 `json:"device_scale_factor"`
Screen *Screen `json:"screen,omitempty"`
Input InputStatus `json:"input"`
Go GoStats `json:"go"`
StateProviders []string `json:"state_providers"`
MediaDir string `json:"media_dir"`
FrameRing RingStatus `json:"frame_ring"`
Crash *Crash `json:"crash,omitempty"`
CrashTraces []TraceLine `json:"crash_traces,omitempty"`
}
StateOutput is game_state's answer: how the game and the process are doing.
A struct with three optional fields, not a map. It looked like a map's job because so much goes in it, but nothing about its shape depends on what the tool found — only its values do, and a field that is sometimes absent is what omitempty is for.
type StateProvider ¶
StateProvider is a named view a game publishes of itself, so an inspection can return "the podium, as the game understands it" rather than a struct dump the caller has to interpret.
It is handed the game that is running now. That is the whole reason this is not a plain func() any: the registry used to be global and the function captured whatever game existed when it was written, so after a game_reset built a fresh one, @summary went on describing the object nobody was playing any more — and looked entirely plausible doing it.
It runs inside the game loop, so it can read state without locking, and it must not block.
type Touch ¶
type Touch struct {
// ID identifies this finger across calls, the way a real touch keeps its
// id from the moment it lands until it lifts.
ID int
X int
Y int
}
Touch is one synthetic finger on the screen, in the game's logical pixels.
type TraceLine ¶
type TraceLine struct {
Tick int64 `json:"tick"`
Time time.Time `json:"time"`
Stream string `json:"stream"`
Text string `json:"text"`
}
TraceLine is one line the process wrote.
type TracesOutput ¶
type TracesOutput struct {
Lines []TraceLine `json:"lines"`
Tick int64 `json:"tick"`
Total int `json:"total"`
// no capture at all on a platform without descriptor duplication, or one
// of the two streams missing. Absent when the capture is whole.
Unavailable string `json:"unavailable,omitempty"`
}
TracesOutput is game_traces' answer.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
ebitenmcp
command
Command ebitenmcp is everything around a game that the game cannot do for itself: give it a display, start it, wire it into an agent, and talk to it from a shell.
|
Command ebitenmcp is everything around a game that the game cannot do for itself: give it a display, start it, wire it into an agent, and talk to it from a shell. |
|
examples
|
|
|
playground
command
Command playground is the test bed for go-ebiten-mcp.
|
Command playground is the test bed for go-ebiten-mcp. |
|
internal
|
|
|
hook
Package hook reaches into Ebitengine's internals to write synthetic input into the state the game reads every tick.
|
Package hook reaches into Ebitengine's internals to write synthetic input into the state the game reads every tick. |
|
uinput
Package uinput creates a virtual gamepad the operating system treats as real.
|
Package uinput creates a virtual gamepad the operating system treats as real. |
|
upstream
Package upstream records what this module assumes about Ebitengine's internals, and nothing else.
|
Package upstream records what this module assumes about Ebitengine's internals, and nothing else. |
|
wire
Package wire holds the handful of things the library and the command line both have to agree on.
|
Package wire holds the handful of things the library and the command line both have to agree on. |