autoebiten

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 5, 2026 License: MIT Imports: 11 Imported by: 0

README

autoebiten

CLI tool for automating Ebitengine games via input injection, screenshots, scripted sequences, and custom commands.

Offers two integration methods: Patch (zero-code changes for existing games using Ebiten's native input) and Library (direct API integration for new projects).

Installation

go install github.com/s3cy/autoebiten/cmd/autoebiten@latest

Which Integration Method?

├─ Already using ebiten's native input functions (ebiten.IsKeyPressed, etc.)?
│  └─→ YES: Use the Patch method. No code changes required!
│
└── Writing a new game or willing to modify input handling code?
   └─→ Use the Library method.

Trade-offs:

Aspect Patch Library
Code changes None required Must use autoebiten.IsKeyPressed() etc.
Ebiten version Locked to v2.9.9 (may need custom patch for other versions) Works with any version
Third-party libs Compatible with libs that use Ebiten input Incompatible with libs using raw Ebiten input
Implementation Hack - relies on Ebiten internals Clean - direct API integration

⚠️ Don't mix Patch and Library methods. If Ebiten is patched, calling autoebiten.Update(), autoebiten.Capture(), and any input related methods will panic. With the Patch method, these calls happen automatically inside Ebiten.

Quick Start (Patch Method)

No code changes required! Patch Ebiten to add automation capabilities.

1. Clone and patch Ebiten
git clone https://github.com/hajimehoshi/ebiten.git /path/to/ebiten
cd /path/to/ebiten
git checkout v2.9.9  # Ensure correct version
git apply /path/to/autoebiten/ebiten.patch
go mod tidy
2. Update your game's go.mod
replace github.com/hajimehoshi/ebiten/v2 => /path/to/local/ebiten
3. Build and run your game normally
go build ./cmd/my-game
./my-game
4. Control it via CLI

Note on ticks: 1 tick = 1 Update() call. Ebiten runs at 60 ticks per second (TPS) by default. This is not the same as FPS—your game can render at 120 FPS on a high refresh monitor while still running at 60 TPS. The default --duration_ticks 6 means a key is held for 6 Update() calls (~100ms at 60 TPS). See TPS vs FPS for details.

# Press a key
autoebiten input --key KeyW --action press

# Hold a key for 6 ticks (default)
autoebiten input --key KeySpace --action hold

# Move mouse and click
autoebiten mouse --action position -x 100 -y 200
autoebiten mouse --action press --button MouseButtonLeft
autoebiten mouse --button MouseButtonLeft  # defaults to hold action
autoebiten mouse -x 100 -y 200 --button MouseButtonLeft  # move to position, then trigger the button

# Scroll wheel
autoebiten wheel -y -3

# Get injected positions (returns last injected values, not real OS positions)
autoebiten get_mouse_position
autoebiten get_wheel_position

# Take a screenshot
autoebiten screenshot --output shot.png

# Run a script file
autoebiten run --script script.json

# Run an inline script
autoebiten run --inline '{"version":"1.0","commands":[{"input":{"action":"press","key":"KeySpace"}}]}'

# Get JSON Schema for IDE support
autoebiten schema > autoebiten-schema.json

# Check connection
autoebiten ping

# Print version information
autoebiten version

# List available keys
autoebiten keys

# List mouse buttons
autoebiten mouse_buttons
Patch Limitations
  • What it modifies: The patch wraps Ebiten's public input API functions (IsKeyPressed, CursorPosition, Wheel, etc.) to check for injected input before falling back to real OS input. It does not modify Ebiten's internal inputstate package.
  • Version dependent: The patch relies on Ebiten's internal implementation details and has only been tested with Ebiten v2.9.9. It may not apply cleanly to other versions.
  • Custom patches: To use a different Ebiten version, you may need to write your own patch by adapting the changes in ebiten.patch to your target version's source code.
  • Maintenance: You will need to re-apply the patch when updating Ebiten versions.
  • Release builds: To ship your game to players without automation capabilities, simply remove the replace directive from your go.mod and use the official Ebiten module.

Library Method

For new games or when you prefer explicit integration, use the library directly.

1. Add the library to your game
import "github.com/s3cy/autoebiten"

func (g *Game) Update() error {
    if !autoebiten.Update() {
        return errors.New("exit requested")
    }
    // Your update logic
    return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
    // Your draw logic
    autoebiten.Capture(screen) // Call at the end
}

Note: When using the library integration method, you need to use autoebiten.IsKeyPressed() and other wrapper functions instead of Ebiten's native functions.

2. Build and run
# Development (default)
go build ./cmd/my-game
./my-game

# Release build (no CLI automation)
go build -tags release ./cmd/my-game
3. Control via CLI

Same CLI commands as the Patch method above.

Build Tags
  • Default (go run or go build): Full RPC server enabled. Your game listens for CLI commands via Unix socket.
  • Release (go build -tags release): RPC server disabled. All functions are no-ops that delegate directly to ebiten. Use this when shipping your game to players.
Input Modes

The library operates in different input modes to control how real and injected inputs are combined:

Mode Description
InjectionOnly Only CLI-injected input is recognized
InjectionFallback Per-key fallback: if a key/button has injected state, return it; otherwise check real input (default)
Passthrough All input passes through to ebiten directly; no injection

Note on InjectionFallback: Each input is checked independently. For example, if you inject 'KeyS' via CLI while physically holding 'KeyW' on your keyboard, IsKeyPressed(KeyS) returns the injected state and IsKeyPressed(KeyW) returns the real keyboard state. They do not interfere with each other.

Custom Commands

Games can register custom commands that can be invoked from the CLI:

Game Side
// In your game initialization
autoebiten.Register("getPlayerInfo", func(ctx autoebiten.CommandContext) {
    info := fmt.Sprintf("Health: %d, Mana: %d", playerHealth, playerMana)
    ctx.Respond(info)
})

autoebiten.Register("heal", func(ctx autoebiten.CommandContext) {
    playerHealth = min(playerHealth+20, 100)
    ctx.Respond(fmt.Sprintf("Healed to %d", playerHealth))
})

The CommandContext provides:

  • Request() string - The request data sent from CLI
  • Respond(response string) - Send response back to CLI (can be called immediately or deferred)
CLI Side
# List available custom commands
autoebiten list_custom

# Execute a custom command
autoebiten custom getPlayerInfo

# Execute with request data
autoebiten custom echo --request "hello world"

See examples/custom_commands for a complete example.

Scripted Automation

Create a JSON script for complex sequences. Script files support comments (// and /* */):

{
  "version": "1.0",
  // Move forward and wait
  "commands": [
    {"input": {"action": "press", "key": "KeyW"}},
    {"delay": {"ms": 100}},
    /* Repeat this block 3 times */
    {"repeat": {"times": 3, "commands": [
      {"input": {"action": "press", "key": "KeyA"}},
      {"delay": {"ms": 200}}
    ]}}
  ]
}

Run from a file:

autoebiten run --script script.json

Or pass an inline JSON string:

autoebiten run --inline '{"version":"1.0","commands":[{"input":{"action":"press","key":"KeySpace"}}]}'
JSON Schema

Generate the JSON Schema for IDE autocompletion and validation:

autoebiten schema > autoebiten-schema.json

The schema defines all available commands, their fields, and valid values. Use it with editors that support JSON Schema for intelligent autocomplete and inline validation.

Script Error Handling

Scripts execute commands sequentially. If any command fails, script execution stops immediately and the error is returned. There is no continue-on-error or try/catch mechanism—fix the issue and re-run the script.

Platform Support

  • macOS: Fully supported
  • Linux: Fully supported
  • Windows: Not supported (uses Unix domain sockets)

Multiple Game Instances

Each game instance uses a PID-based socket at /tmp/autoebiten/autoebiten-{PID}.sock.

Auto-detection: If --pid is not provided, autoebiten automatically detects a running game. If only one game is running, it uses that. If multiple games are running, it lists them and exits with an error—you must specify which one with --pid.

Target a specific game:

autoebiten --pid 12345 input --key KeySpace --action press

Or set the socket path manually:

AUTOEBITEN_SOCKET=/tmp/autoebiten/autoebiten-12345.sock autoebiten ping

Public API

// In your game loop
autoebiten.Update()        // Process RPC commands, returns false on exit
autoebiten.Capture(screen) // Capture screenshot (call in Draw)

// Query input state
autoebiten.IsKeyPressed(key ebiten.Key)           // bool
autoebiten.IsMouseButtonPressed(button ebiten.MouseButton) // bool
autoebiten.CursorPosition()                       // (x, y int)
autoebiten.Wheel()                               // (x, y float64)

// inpututil wrappers (respect input mode)
autoebiten.IsKeyJustPressed(key ebiten.Key)       // bool
autoebiten.IsKeyJustReleased(key ebiten.Key)      // bool
autoebiten.KeyPressDuration(key ebiten.Key)       // int (ticks)
autoebiten.IsMouseButtonJustPressed(button ebiten.MouseButton)  // bool
autoebiten.IsMouseButtonJustReleased(button ebiten.MouseButton) // bool
autoebiten.MouseButtonPressDuration(button ebiten.MouseButton)  // int (ticks)

// Configure input mode
autoebiten.SetMode(autoebiten.InjectionFallback) // default: injected + real input

// Custom commands
autoebiten.Register(name string, handler func(CommandContext)) // Register a custom command
autoebiten.Unregister(name string) bool                        // Remove a custom command
autoebiten.ListCustomCommands() []string                       // List registered commands

Testing with testkit

The testkit package provides a Go testing framework for autoebiten games. It supports two testing modes:

  • Black-Box Mode (Game): Launches game in separate process, controls via RPC
  • White-Box Mode (Mock): Tests game logic in same process with mocked inputs

See the testkit package documentation for details.

Quick Example
import "github.com/s3cy/autoebiten/testkit"

// Black-box test
func TestPlayerMovement(t *testing.T) {
    game := testkit.Launch(t, "./mygame")
    defer game.Shutdown()

    game.HoldKey(ebiten.KeyD, 10)
    x, _ := game.StateQuery("mygamestate", "Player.X")
    assert.Equal(t, 10, x)
}

// White-box test
func TestPlayerTakesDamage(t *testing.T) {
    g := NewGame()
    mock := testkit.NewMock(t, g)
    g.EnemyAttacks()
    mock.Tick()
    assert.Equal(t, 90, g.Player.Health)
}

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrPathNotFound = errors.New("path not found")

ErrPathNotFound is returned when a state query path cannot be resolved. This occurs when:

  • The path references a non-existent field
  • An array index is out of bounds
  • A map key does not exist
  • The path traverses a nil pointer
View Source
var StateExporterPathPrefix = ".state."

Functions

func Capture

func Capture(screen image.Image)

Capture processes screenshots for injection. Panic when using the patch integration method.

func CursorPosition

func CursorPosition() (x, y int)

CursorPosition wraps ebiten.CursorPosition, respecting the current mode. Panic when using the patch integration method.

func GetCustomCommand added in v0.3.0

func GetCustomCommand(name string) func(CommandContext)

GetCustomCommand returns the handler for a custom command. Returns nil if the command is not registered. This is primarily for internal use.

func IsKeyJustPressed added in v0.2.0

func IsKeyJustPressed(key ebiten.Key) bool

IsKeyJustPressed wraps inpututil.IsKeyJustPressed, respecting the current mode. Panic when using the patch integration method.

func IsKeyJustReleased added in v0.2.0

func IsKeyJustReleased(key ebiten.Key) bool

IsKeyJustReleased wraps inpututil.IsKeyJustReleased, respecting the current mode. Panic when using the patch integration method.

func IsKeyPressed

func IsKeyPressed(key ebiten.Key) bool

IsKeyPressed wraps ebiten.IsKeyPressed, respecting the current mode. Panic when using the patch integration method.

func IsMouseButtonJustPressed added in v0.2.0

func IsMouseButtonJustPressed(button ebiten.MouseButton) bool

IsMouseButtonJustPressed wraps inpututil.IsMouseButtonJustPressed, respecting the current mode. Panic when using the patch integration method.

func IsMouseButtonJustReleased added in v0.2.0

func IsMouseButtonJustReleased(button ebiten.MouseButton) bool

IsMouseButtonJustReleased wraps inpututil.IsMouseButtonJustReleased, respecting the current mode. Panic when using the patch integration method.

func IsMouseButtonPressed

func IsMouseButtonPressed(button ebiten.MouseButton) bool

IsMouseButtonPressed wraps ebiten.IsMouseButtonPressed, respecting the current mode. Panic when using the patch integration method.

func KeyPressDuration added in v0.2.0

func KeyPressDuration(key ebiten.Key) int

KeyPressDuration wraps inpututil.KeyPressDuration, respecting the current mode. Panic when using the patch integration method.

func ListCustomCommands added in v0.3.0

func ListCustomCommands() []string

ListCustomCommands returns a list of all registered custom command names.

func MouseButtonPressDuration added in v0.2.0

func MouseButtonPressDuration(button ebiten.MouseButton) int

MouseButtonPressDuration wraps inpututil.MouseButtonPressDuration, respecting the current mode. Panic when using the patch integration method.

func Register added in v0.3.0

func Register(name string, handler func(CommandContext))

Register registers a custom command handler. The name must be unique; registering a duplicate name will panic. The handler receives a CommandContext containing the request and a Respond method.

Example:

autoebiten.Register("getPlayerInfo", func(ctx autoebiten.CommandContext) {
	info := getPlayerInfo() // user-defined function
	ctx.Respond(fmt.Sprintf("Health: %d, Mana: %d", info.Health, info.Mana))
})

func RegisterStateExporter added in v0.5.0

func RegisterStateExporter(name string, root any)

RegisterStateExporter registers a custom command that exposes game state via reflection-based path queries. The path uses dot notation:

  • "Player.X" - access struct field
  • "Inventory.0.Name" - access array/slice index
  • "Skills.Sword" - access map key

Game state can be retrieved by calling the testkit.StateQuery function

In game:

type Player struct {
	X      float64
	Y      float64
	Health int
}

type Game struct {
	Player Player
}

func main() {
	var g := Game{Player: Player{X: 0, Y: 0, Health: 100}}
	RegisterStateExporter("mystate", &g)
}

In test:

func TestPlayerMovement(t *testing.T) {
    game := testkit.Launch(t, "./mygame")
    defer game.Shutdown()

	game.HoldKey(ebiten.KeyD, 10)
    x, _ := game.StateQuery("mystate", "Player.X")
    assert.Equal(t, 10, x)
}

func SetMode

func SetMode(mode Mode)

SetMode sets the input handling mode.

func Unregister added in v0.3.0

func Unregister(name string) bool

Unregister removes a custom command handler. Returns true if the command was found and removed, false otherwise.

func Update

func Update() bool

Update runs the internal update loop. Panic when using the patch integration method.

func Wheel

func Wheel() (x, y float64)

Wheel wraps ebiten.Wheel, respecting the current mode. Panic when using the patch integration method.

Types

type CommandContext added in v0.3.0

type CommandContext = custom.Context

CommandContext provides context for custom command execution. It contains the request data and a method to send the response.

type Mode

type Mode int

Mode represents the input handling mode.

const (
	// InjectionOnly mode returns only injected input results.
	InjectionOnly Mode = iota
	// InjectionFallback mode returns injected results if available,
	// otherwise falls back to ebiten's native input handling.
	InjectionFallback
	// Passthrough mode only uses ebiten's native input handling.
	Passthrough
)

func GetMode

func GetMode() Mode

GetMode returns the current input handling mode.

type StateQueryPath added in v0.5.0

type StateQueryPath struct {
	// contains filtered or unexported fields
}

StateQueryPath represents a parsed state query path. It can be used for repeated queries against different roots.

func ParseStateQueryPath added in v0.5.0

func ParseStateQueryPath(path string) (*StateQueryPath, error)

ParseStateQueryPath parses a state query path for reuse.

func (*StateQueryPath) Query added in v0.5.0

func (p *StateQueryPath) Query(root any) (any, error)

Query executes the parsed path against a root value.

Directories

Path Synopsis
cmd
autoebiten command
examples
custom_commands command
simple command
internal
cli
custom
Package custom provides custom command registration and execution.
Package custom provides custom command registration and execution.
rpc
Package testkit provides a testing framework for autoebiten games.
Package testkit provides a testing framework for autoebiten games.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL