Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/actions/artifact-check/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: Artifact Check
description: Verify the shipping Linux archive's Godot export, replay and terminal player.
runs:
using: composite
steps:
- name: Real Godot terminal artifact check
shell: bash
run: make artifact-check
10 changes: 10 additions & 0 deletions .github/actions/godot-display/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: Godot Rendering Dependencies
description: Install the private X server and Mesa software renderer used by real viewport tests.
runs:
using: composite
steps:
- name: Install virtual display and software OpenGL
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends xvfb libgl1-mesa-dri libglx-mesa0
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: jdx/mise-action@7a4e45a543138629540c9a1616d08632b893e492 # v5.0.1
- uses: ./.github/actions/godot-display

- name: Install pinned build and validation tools
run: make install

Expand All @@ -40,3 +42,6 @@ jobs:

- name: Test
uses: ./.github/actions/test

- name: Artifact Check
uses: ./.github/actions/artifact-check
2 changes: 2 additions & 0 deletions .github/workflows/release-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ jobs:
echo "tag=$tag" >> "$GITHUB_OUTPUT"

- uses: jdx/mise-action@7a4e45a543138629540c9a1616d08632b893e492 # v5.0.1
- uses: ./.github/actions/godot-display

- name: Install pinned build and validation tools
run: make install

Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ jobs:
echo "tag=$tag" >> "$GITHUB_OUTPUT"

- uses: jdx/mise-action@7a4e45a543138629540c9a1616d08632b893e492 # v5.0.1
- uses: ./.github/actions/godot-display

- name: Install pinned build and validation tools
run: make install

Expand All @@ -93,6 +95,9 @@ jobs:
- name: Test
uses: ./.github/actions/test

- name: Artifact Check
uses: ./.github/actions/artifact-check

- name: Verify released binary version
env:
ARCHIVE: dist/termcade_Linux_x86_64.tar.gz
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ game.wasm

.artifacts/
__pycache__/
.godot/
*.tgd
examples/godot/*/build/
11 changes: 8 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ SHELL := /bin/bash
.DEFAULT_GOAL := help
.NOTPARALLEL:
VERSION ?= dev
.PHONY: help install lint test build check dev stop clean
export PATH := $(CURDIR)/.artifacts/godot/bin:$(PATH)
.PHONY: help install lint test build artifact-check check dev stop clean
help:
@echo 'make check: complete five-platform archive build, lint and race/consumer checks'
@echo 'make check: five-platform archives, lint, race/consumer and real Godot terminal checks'
install:
@command -v "$${TERMCADE_XVFB_BIN:-Xvfb}" >/dev/null || { echo "Install Xvfb and Mesa for Godot framebuffer previews (see README)."; exit 1; }
mise trust .mise.toml
mise install go python actionlint shellcheck
mise exec -- go mod download
cd sdk && mise exec -- go mod download
mise exec -- python3 scripts/install-godot.py
lint:
mise exec -- bash scripts/lint.sh
mise exec -- actionlint
Expand All @@ -19,7 +22,9 @@ test:
mise exec -- bash scripts/test.sh
build:
mise exec -- env VERSION="$(VERSION)" bash scripts/build.sh
check: build lint test
artifact-check:
mise exec -- python3 scripts/test-godot-cli.py
check: build lint test artifact-check
dev stop:
@echo '$@: unsupported: interactive arcade sessions require caller-owned terminal and account configuration'
clean:
Expand Down
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<!-- Generated from private documentation source. Do not edit directly. Source SHA256: f43ce6ca2cfa1100e4921d4b4dff9e3a809550b934541b2ebf2063f4f390c538 -->

# termcade

A terminal arcade. Classic games rendered at sub-cell resolution with block
Expand Down Expand Up @@ -293,3 +295,77 @@ go test ./... # unit + shell tests
go test ./internal/plugin/ # includes wasm end-to-end tests (builds guests)
go vet ./...
```

## Godot terminal target (experimental)

Godot 4.7.2 projects can export to a custom **Termcade** target and play inside
terminal character cells. Godot renders the game's actual viewport, including
cameras, animated sprites, UI, shaders, custom drawing, and 3D. Termcade resizes
that image into its quadrant, sextant, half-block or ASCII cell renderer. A
private virtual display keeps game windows off your desktop; play happens in
the terminal.

This experimental framebuffer runtime currently requires **Linux**, **Xvfb**
and **Mesa OpenGL**. On Arch install `xorg-server-xvfb mesa`; on Ubuntu install
`xvfb libgl1-mesa-dri libglx-mesa0`. Xvfb must be on PATH, or set
`TERMCADE_XVFB_BIN` to its executable. The preview uses Godot's Compatibility
renderer with software OpenGL. Games requiring Forward+, compute shaders, C#,
native extensions, XR or additional windows are not supported by this runtime.

Install Godot **4.7.2.stable.official.ed1daf0bf** and make `godot` available on
PATH, or set `GODOT_BIN` to that executable. From a source checkout, `make install`
installs the checksum-pinned Linux engine needed by the full gate. Release
archives do not bundle Godot, Xvfb or Mesa.

```sh
termcade godot export examples/godot/paddle build/paddle.tgd
termcade godot play --trusted build/paddle.tgd
termcade godot capture --trusted --json --columns 160 --rows 45 --frames 120 build/paddle.tgd frame.png
termcade godot benchmark --trusted --json --columns 160 --rows 45 --frames 120 --warmup 60 build/paddle.tgd
```

For your own project, `termcade godot init /path/to/project` installs the addon
and adds a Termcade preset to Godot's Export dialog. The CLI exporter uses that
same platform in a disposable project copy, leaving your source settings and
import cache untouched. Output files must not already exist. Export and capture
support `--json` for success and failure results. Flags precede positional paths.
The new framebuffer protocol requires newly exported packages; re-export packages
created by the earlier software-node prototype.

Godot renders near the terminal's pixel resolution while retaining the authored
logical coordinates and UI layout. Final conversion uses bilinear filtering.
For games with resolution-dependent shaders or viewport logic, set
`TERMCADE_GODOT_FULL_RES=1` to keep the original render target. Re-export older
packages to get this optimization. The player counts rendering and transfer time
inside its 60 Hz frame budget, avoiding an extra full-frame wait.

The player uses the available terminal space, preserves the game's aspect ratio,
and resizes captures without restarting gameplay. **Ctrl+P** pauses and **Ctrl+C**
exits. Escape goes to the game so its menus work. Arrow keys, letters, space, tab,
backspace and enter are translated to Godot key events. Terminals with release
reporting supply exact held keys; other terminals use an auto-repeat timeout.
The preview needs at least 72 columns by 22 rows. A larger terminal and smaller
font give games with detailed UI more readable pixels. Game labels are rendered
pixels rather than selectable terminal text.

Capture accepts `--input replay.json`, an array of `{ "frame": 1, "code": 4194321,
"down": true }` events using Godot key codes; `4194321` is the right arrow.
Each capture starts a fresh engine. Output is a PNG of the framebuffer before
terminal character conversion. Defaults are 72 columns and 20 rows. Capture
resolution is bounded to 600 × 360 pixels across all cell shapes. Determinism
depends on the game's code and inputs.

`godot benchmark` runs an unpaced replay, excludes `--warmup` frames, and reports
frame-exchange mean/p95, terminal-cell conversion time, ANSI bytes and available
engine render/readback/resize timings. Input replay frame numbers include warmup.
It measures the pipeline, not terminal-emulator paint time or startup latency.
Benchmark and capture share the same bounded input format and resolution limits.

This is a **local developer target**. `.tgd` packages execute native Godot code
with host access, which is why play and capture require `--trusted`; they are
not Wasm-sandboxed `.tcade` packages and cannot be published to the existing
marketplace. User data is temporary for each preview. Audio, mouse/controller
input, modifier combinations and terminal image protocols are not implemented.
A hung frame terminates its engine and private display rather than blocking the
terminal indefinitely. Ordinary rendering compatibility does not guarantee
that every game is readable or controllable at terminal resolution.
16 changes: 16 additions & 0 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"slices"
"strings"

"github.com/aviorstudio/termcade/internal/plugin"
Expand Down Expand Up @@ -46,6 +48,14 @@ usage:
termcade dev build [dir] build a game directory into a .tcade package
termcade dev install <file> install a local .tcade while developing

termcade godot init <project> install the Godot Termcade export target
termcade godot export <project> <game.tgd>
validate and export a Godot 2D game
termcade godot play --trusted <game.tgd>
play a local Godot game inside the terminal
termcade godot capture --trusted <game.tgd> <frame.png>
capture a terminal-resolution frame for agents

This arcade is also the whole dev kit: dev new, hack, dev build, dev install,
play. See docs/sdk.md to get started.
`
Expand Down Expand Up @@ -93,6 +103,8 @@ func runCommand(args []string) bool {
default:
err = fmt.Errorf("unknown dev subcommand; try: termcade dev new <author/slug> · termcade dev build [dir] · termcade dev install <file>")
}
case "godot":
err = cmdGodot(args[1:])
case "version", "-v", "--version":
fmt.Println("termcade", resolveVersion())
case "help", "-h", "--help":
Expand All @@ -102,6 +114,10 @@ func runCommand(args []string) bool {
os.Exit(2)
}
if err != nil {
if args[0] == "godot" && slices.Contains(args, "--json") {
_ = json.NewEncoder(os.Stdout).Encode(map[string]any{"ok": false, "error": err.Error(), "runtime": "godot-terminal-framebuffer-v1"})
os.Exit(1)
}
fmt.Fprintln(os.Stderr, "termcade:", err)
os.Exit(1)
}
Expand Down
4 changes: 4 additions & 0 deletions engineering-profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"make",
"check"
],
"artifact-check": [
"make",
"artifact-check"
],
"clean": [
"make",
"clean"
Expand Down
Binary file added examples/godot/paddle/badge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions examples/godot/paddle/game.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
extends Node2D

var velocity := Vector2(24, 28)

func _physics_process(delta: float) -> void:
$Paddle.position.x = clampf($Paddle.position.x + Input.get_axis("ui_left", "ui_right") * 80.0 * delta, 14, 130)
$Ball.position += velocity * delta
if $Ball.position.x < 3 or $Ball.position.x > 141:
velocity.x = -velocity.x
if $Ball.position.y < 3:
velocity.y = absf(velocity.y)
if velocity.y > 0 and $Ball.position.y >= 66 and $Ball.position.y <= 72 and absf($Ball.position.x - $Paddle.position.x) < 14:
velocity.y = -absf(velocity.y)
if $Ball.position.y > 82:
$Ball.position = Vector2(72, 40)
velocity = Vector2(24, 28)
26 changes: 26 additions & 0 deletions examples/godot/paddle/main.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[gd_scene load_steps=3 format=3]

[ext_resource type="Script" path="res://game.gd" id="1"]
[ext_resource type="Texture2D" path="res://badge.png" id="2"]

[node name="PaddleGame" type="Node2D"]
script = ExtResource("1")

[node name="Badge" type="Sprite2D" parent="."]
position = Vector2(132, 10)
texture = ExtResource("2")

[node name="Paddle" type="Polygon2D" parent="."]
position = Vector2(72, 70)
color = Color(0.25, 0.77, 0.79, 1)
polygon = PackedVector2Array(-12, -2, 12, -2, 12, 2, -12, 2)

[node name="Ball" type="Polygon2D" parent="."]
position = Vector2(72, 40)
color = Color(0.9, 0.79, 0.27, 1)
polygon = PackedVector2Array(-2, -2, 2, -2, 2, 2, -2, 2)

[node name="Boundary" type="Line2D" parent="."]
points = PackedVector2Array(1, 79, 1, 1, 143, 1, 143, 79)
width = 1.0
default_color = Color(0.25, 0.25, 0.25, 1)
13 changes: 13 additions & 0 deletions examples/godot/paddle/project.godot
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
config_version=5

[application]
config/name="Godot Terminal Paddle"
run/main_scene="res://main.tscn"

[display]
window/size/viewport_width=144
window/size/viewport_height=80

[rendering]
renderer/rendering_method="gl_compatibility"
environment/defaults/default_clear_color=Color(0, 0, 0, 1)
Loading
Loading