Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ compile_commands.json
/examples/dev_*/ssab_generated
/examples/dev_*/.ssplayer_sources.cfg
/examples/*/addons/spritestudio
# The headless test project's build inputs: the addon comes from
# build-extension.*, the .ssab from deploy-examples.*, and the Godot
# binary run-tests.* runs from fetch-godot.*. None of the three is a
# source file, and a committed .ssab goes stale the moment the SDK's
# tests/ change.
/test_gdextension/addons/spritestudio
/test_gdextension/ssab_generated
/test_gdextension/.ssplayer_sources.cfg
/godot-bin
/scripts/.godot-cache/

### Generated by gibo (https://github.com/simonwhitaker/gibo)
### https://raw.github.com/github/gitignore/4488915eec0b3a45b5c63ead28f286819c0917de/C++.gitignore
Expand Down
49 changes: 49 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,59 @@ Callouts (`> [!NOTE]`) are parsed natively — `mkdocs-callouts` is gone. Two co
| Setup (Prebuilt SDK) | `./scripts/download-sdk.sh` (POSIX) / `.\scripts\download-sdk.ps1` (Win) |
| Deploy Example Assets | `./scripts/deploy-examples.sh` (POSIX) / `.\scripts\deploy-examples.ps1` (Win) |
| Build the release | `./scripts/build-release.sh` (POSIX) / `.\scripts\build-release.ps1` (Win) — the addon zip, from a downloaded matrix build |
| Run the headless tests | `./scripts/run-tests.sh` (POSIX) / `.\scripts\run-tests.ps1` (Win) — the GDExtension build, through GDScript. Needs a Godot binary: `godot=<path>`, else `$GODOT`, `godot-bin/`, then PATH |
| Install the pinned Godot | `./scripts/fetch-godot.sh` (POSIX) / `.\scripts\fetch-godot.ps1` (Win) — the editor build named in `scripts/GODOT_VERSION.txt`, into `godot-bin/`. Nothing runs it for you |
| Format C++ Code | `clang-format -i ss_player/*.{cpp,h}` (if available) |

*Note: Setup (Source SDK) is recommended for developers using the submodule. Setup (Prebuilt SDK) is intended for CI or release-only environments.*

### The headless suite

`test_gdextension/` is a Godot project that loads the built addon and drives
`SpriteStudioPlayer2D` from GDScript — 35 cases over the bound API, the part
override layer and the five signals. It is not a sample and does not live under
`examples/`: the samples are what a reader is shown, and one project cannot be
both that and a scratch pad (MAINTAINING_PLAYERS.md). It wears the
`_gdextension` suffix for the same reason every other project carrying the addon
does: a custom-module build has the classes compiled in already, so which build a
project targets has to be readable from its name. Its two inputs are build
outputs and gitignored — `build-extension.*` installs the addon into it,
`deploy-examples.*` writes its `.ssab`, and `run_tests.gd` refuses to start
without either rather than skipping its way to a green run.

**Two things it deliberately does not cover.** Drawing, because `--headless`
installs a dummy rasteriser and there are no pixels to compare — and
`NOTIFICATION_DRAW` is a no-op in the node anyway, the InternalPlayer issuing
its own RenderingServer calls. And the **custom-module build**, because a module
is compiled into the engine: testing it would mean building Godot rather than
downloading it, and a module binary cannot even open `test_gdextension/` — it registers the
classes a second time and aborts (`run-tests.*` recognises that message and says
so). What the two builds share is one copy of the playback logic; what they do
not share is a layer of `#ifdef SPRITESTUDIO_GODOT_EXTENSION` adapters, which
are includes and type conversions — so the module build's guard is that it still
builds.

Cases step with `advance()` under `ANIMATION_PROCESS_MANUAL`, never the frame
clock, so a result does not depend on how long a frame took. A case that cannot
run on this host declares a **skip**, which is reported apart from the passes
and never counted as one.

**The first headless import crashes, and `run-tests.*` retries it once. It is a
godot-cpp problem, not ours.** The run in which Godot first *discovers* the
extension aborts on the way out (null dereference, caught by Godot's own crash
handler). Not the import — a project with **zero importable files** does it too;
what triggers it is the extension being loaded mid-scan rather than at startup
from `.godot/extension_list.cfg`, and deleting just that file brings it back.
**godot-cpp's own `test/` extension reproduces it exactly**, a project with no
extension does not, and registering nothing at all still does — so it is the
pairing, not this code. godot-cpp has no 4.6/4.7 release branch: it went from
`godot-4.5-stable` straight to the 10.0 line, so an extension for Godot 4.7 is
built from master against `api_version=4.7`. The scan's work completes, so the
second run is clean; the retry requires that second run to pass, because a crash
that repeats is still a failure. Not test-only — anything running
`godot --headless --import` on a fresh checkout meets it.


## Releases

A release is a tag, pushed first and built second: push `v<version>`, then dispatch **release gdextension** from that tag with `upload_release=true`. The tag names the Release; `upload_release=true` from anything else fails the run rather than silently producing none. The default dispatch (`upload_release=false`, off a `release/X.Y` branch) is a build for QA and creates no Release. Drafts are always created as drafts — a human reviews the assets and the generated notes, then publishes from the UI, choosing pre-release / latest there.
Expand Down
1 change: 1 addition & 0 deletions scripts/GODOT_VERSION.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4.7.2-stable
10 changes: 10 additions & 0 deletions scripts/build-extension.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,14 @@ foreach ($project in $OTHER_PROJECTS) {
Copy-Item "examples\$MAIN_PROJECT\addons\spritestudio\icons" "$dest_dir\" -Recurse -Force
}

# The headless test project is not a sample, so it is not under examples\ --
# but it loads the extension exactly as one does, hence the _gdextension suffix
# every project carrying the addon wears. See test_gdextension\project.godot.
$test_dest = "test_gdextension\addons\spritestudio"
mkdir $test_dest -Force | Out-Null
Copy-Item "misc\spritestudio.gdextension" "$test_dest\spritestudio.gdextension" -Force
Write-Host "Syncing binaries and icons to test_gdextension..."
Copy-Item "examples\$MAIN_PROJECT\addons\spritestudio\bin" "$test_dest\" -Recurse -Force
Copy-Item "examples\$MAIN_PROJECT\addons\spritestudio\icons" "$test_dest\" -Recurse -Force

popd
12 changes: 12 additions & 0 deletions scripts/build-extension.sh
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ OTHER_PROJECTS=("overall_gdextension")
/bin/mkdir -p "./examples/${MAIN_PROJECT}/addons/spritestudio/icons"
/bin/cp ss_player/icons/icon_*.svg "./examples/${MAIN_PROJECT}/addons/spritestudio/icons/"

# The headless test project is not a sample, so it is not under examples/ --
# but it loads the extension exactly as one does -- hence the _gdextension
# suffix every project carrying the addon wears. See test_gdextension/project.godot.
TEST_PROJECT="./test_gdextension"

# Copy from MAIN_PROJECT to OTHER_PROJECTS
for project in "${OTHER_PROJECTS[@]}"; do
DEST_DIR="./examples/${project}/addons/spritestudio"
Expand All @@ -138,4 +143,11 @@ for project in "${OTHER_PROJECTS[@]}"; do
/bin/cp -R "./examples/${MAIN_PROJECT}/addons/spritestudio/icons" "${DEST_DIR}/"
done

TEST_DEST="${TEST_PROJECT}/addons/spritestudio"
/bin/mkdir -p "${TEST_DEST}"
/bin/cp misc/spritestudio.gdextension "${TEST_DEST}/"
echo "Syncing binaries and icons to test_gdextension..."
/bin/cp -R "./examples/${MAIN_PROJECT}/addons/spritestudio/bin" "${TEST_DEST}/"
/bin/cp -R "./examples/${MAIN_PROJECT}/addons/spritestudio/icons" "${TEST_DEST}/"

popd > /dev/null # ${ROOTDIR}
25 changes: 25 additions & 0 deletions scripts/deploy-examples.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,29 @@ foreach ($ENTRY in $DEPLOYMENTS) {
}
}

# The headless test project is not a sample and so is not under examples\, but
# it reads the same fixtures -- one entry per pack run_tests.gd's preflight
# names. Keeping the .ssab out of git and regenerating it here is why the suite
# cannot quietly test a stale conversion.
$TEST_PACKS = @(
"overall"
"Ringo"
)

foreach ($TEST in $TEST_PACKS) {
$SSPJ_PATH = Join-Path $SDK_TESTS_DIR "$TEST/$TEST.sspj"
$OUTPUT_DIR = Join-Path $rootDirectory "test_gdextension/ssab_generated/$TEST"

if (!(Test-Path $SSPJ_PATH)) {
Write-Error "${APP}: $SSPJ_PATH not found"
}

Write-Host "Updating SSAB for $TEST in $OUTPUT_DIR..."
mkdir -Force $OUTPUT_DIR > $null
& $CONVERTER "$SSPJ_PATH" -o "$OUTPUT_DIR"
if ($LASTEXITCODE -ne 0) {
Write-Error "${APP}: ssconverter-cli failed for $TEST ($LASTEXITCODE)"
}
}

Write-Host "Done!"
23 changes: 23 additions & 0 deletions scripts/deploy-examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,27 @@ for ENTRY in "${DEPLOYMENTS[@]}"; do
"${CONVERTER}" "${SSPJ_PATH}" -o "${OUTPUT_DIR}"
done

# The headless test project is not a sample and so is not under examples/, but
# it reads the same fixtures -- one entry per pack run_tests.gd's preflight
# names. Keeping the .ssab out of git and regenerating it here is why the suite
# cannot quietly test a stale conversion.
TEST_PACKS=(
"overall"
"Ringo"
)

for TEST in "${TEST_PACKS[@]}"; do
SSPJ_PATH="${SDK_TESTS_DIR}/${TEST}/${TEST}.sspj"
OUTPUT_DIR="${ROOTDIR}/test_gdextension/ssab_generated/${TEST}"

if [ ! -f "${SSPJ_PATH}" ]; then
echo "${APP}: ${SSPJ_PATH} not found" >&2
exit 1
fi

echo "Updating SSAB for ${TEST} in ${OUTPUT_DIR}..."
mkdir -p "${OUTPUT_DIR}"
"${CONVERTER}" "${SSPJ_PATH}" -o "${OUTPUT_DIR}"
done

echo "Done!"
81 changes: 81 additions & 0 deletions scripts/fetch-godot.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env pwsh
#
# Download the stock Godot editor pinned in scripts\GODOT_VERSION.txt into
# godot-bin\ (gitignored), for scripts\run-tests.ps1 to run the headless suite
# against. Windows counterpart of scripts/fetch-godot.sh.
#
# Nothing calls this on your behalf. run-tests.ps1 looks for a binary you
# already have -- godot=<path>, $env:GODOT, godot-bin\, PATH -- and prints this
# command rather than downloading tens of MB without being asked.
#
# The editor build is the whole download, and it is all that is needed: the
# export templates are another ~1.2 GB and the suite exports nothing.
#
# This is deliberately NOT the same binary as godot\bin\*. That one is a custom
# module build of the engine, which has SpriteStudio compiled in -- it would
# register the classes a second time and abort on the extension's own project.
#
# Usage: scripts\fetch-godot.ps1 [force=yes] [out=<dir>]
# force : yes to re-download even when the pinned version is installed
# out : install directory (default: godot-bin\)
#
# Requires PowerShell 5+ (Invoke-WebRequest, Expand-Archive).

$ErrorActionPreference = "Stop"
$RootDir = Split-Path -Parent (Split-Path -Parent $PSCommandPath)
$ScriptDir = Join-Path $RootDir "scripts"

$Force = "no"
$OutDir = Join-Path $RootDir "godot-bin"
foreach ($item in $Args) {
if ($item -match "^-?-?help$" -or $item -eq "-h") {
$emit = $false
foreach ($line in (Get-Content $PSCommandPath)) {
if ($line -match '^# Usage:') { $emit = $true }
if ($emit) { $line -replace '^# ?', '' }
if ($emit -and $line -match '^# Requires') { break }
}
exit 0
}
$kv = $item -split "=", 2
switch ($kv[0]) {
"force" { $Force = $kv[1] }
"out" { $OutDir = $kv[1] }
default { Write-Error "unknown arg '$item'" }
}
}

$Version = (Get-Content (Join-Path $ScriptDir "GODOT_VERSION.txt")).Trim()
$VersionFile = Join-Path $OutDir "VERSION"
$CacheDir = Join-Path $ScriptDir ".godot-cache"

if ($Force -ne "yes" -and (Test-Path $VersionFile) -and
((Get-Content $VersionFile).Trim() -eq $Version)) {
Write-Host "fetch-godot.ps1: Godot $Version is already installed in godot-bin\. Nothing to do."
exit 0
}

$Asset = "Godot_v${Version}_win64.exe.zip"
$Url = "https://github.com/godotengine/godot-builds/releases/download/$Version/$Asset"
$ZipFile = Join-Path $CacheDir $Asset

mkdir $CacheDir -Force | Out-Null
if ((-not (Test-Path $ZipFile)) -or $Force -eq "yes") {
Write-Host "fetch-godot.ps1: downloading $Asset"
Invoke-WebRequest -Uri $Url -OutFile "$ZipFile.part"
Move-Item "$ZipFile.part" $ZipFile -Force
} else {
Write-Host "fetch-godot.ps1: reusing cached $Asset"
}

if (Test-Path $OutDir) { Remove-Item -Recurse -Force $OutDir }
mkdir $OutDir -Force | Out-Null
Expand-Archive -Path $ZipFile -DestinationPath $OutDir -Force

# Normalise what the archive unpacks to, so run-tests.ps1 has one path rather
# than a version-stamped name that changes with every bump.
$bin = Get-ChildItem -Path $OutDir -Filter "Godot_v*.exe" -File | Select-Object -First 1
if ($bin) { Move-Item $bin.FullName (Join-Path $OutDir "godot.exe") -Force }

Set-Content -Path $VersionFile -Value $Version
Write-Host "fetch-godot.ps1: Godot $Version installed in godot-bin\"
97 changes: 97 additions & 0 deletions scripts/fetch-godot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/bin/bash
#
# Download the stock Godot editor pinned in scripts/GODOT_VERSION.txt into
# godot-bin/ (gitignored), for scripts/run-tests.sh to run the headless suite
# against.
#
# Nothing calls this on your behalf. run-tests.sh looks for a binary you
# already have -- godot=<path>, $GODOT, godot-bin/, godot/bin/, PATH -- and
# prints this command rather than downloading tens of MB without being asked.
#
# The editor build is the whole download, and it is all that is needed: the
# export templates are another ~1.2 GB and the suite exports nothing.
#
# This is deliberately NOT the same binary as godot/bin/*. That one is a custom
# module build of the engine, which has SpriteStudio compiled in -- it would
# register the classes a second time and abort on the extension's own project.
# Reach for it only with a project that carries no addons/spritestudio.
#
# Usage: scripts/fetch-godot.sh [force=yes] [out=<dir>]
# force : yes to re-download even when the pinned version is installed
# out : install directory (default: godot-bin/)
#
# Requires curl and unzip.
set -euo pipefail

APP=$(basename "$0")
SCRIPTDIR=$(cd "$(dirname "$0")" && pwd -P)
ROOTDIR=$(cd "$SCRIPTDIR/.." && pwd -P)

usage() { sed -n '/^# Usage:/,/^# Requires/p' "$0" | sed 's/^# \{0,1\}//'; }

FORCE="no"
OUT_DIR="${ROOTDIR}/godot-bin"
for item in "$@"; do
case "$item" in
force=*) FORCE="${item#*=}" ;;
out=*) OUT_DIR="${item#*=}" ;;
-h|--help|help) usage; exit 0 ;;
*) echo "$APP: unknown arg '$item'" >&2; usage; exit 2 ;;
esac
done

VERSION=$(tr -d ' \r\n' < "${SCRIPTDIR}/GODOT_VERSION.txt")
VERSION_FILE="${OUT_DIR}/VERSION"
CACHE_DIR="${SCRIPTDIR}/.godot-cache"

if [ "$FORCE" != "yes" ] && [ -f "$VERSION_FILE" ] \
&& [ "$(tr -d ' \r\n' < "$VERSION_FILE")" = "$VERSION" ]; then
echo "$APP: Godot $VERSION is already installed in $(basename "$OUT_DIR")/. Nothing to do."
exit 0
fi

# The asset name encodes the platform; ARM Linux and 32-bit are not published as
# editor builds, so those hosts have to bring their own binary.
case "$(uname -s)" in
Darwin) ASSET="Godot_v${VERSION}_macos.universal.zip" ;;
Linux)
case "$(uname -m)" in
x86_64) ASSET="Godot_v${VERSION}_linux.x86_64.zip" ;;
*) echo "$APP: no published editor build for linux $(uname -m) — install Godot $VERSION yourself and pass godot=<path> to run-tests.sh" >&2; exit 1 ;;
esac ;;
*) ASSET="Godot_v${VERSION}_win64.exe.zip" ;;
esac

URL="https://github.com/godotengine/godot-builds/releases/download/${VERSION}/${ASSET}"
ZIP_FILE="${CACHE_DIR}/${ASSET}"

command -v curl >/dev/null || { echo "$APP: curl not on PATH" >&2; exit 1; }
command -v unzip >/dev/null || { echo "$APP: unzip not on PATH" >&2; exit 1; }

mkdir -p "$CACHE_DIR"
if [ ! -f "$ZIP_FILE" ] || [ "$FORCE" = "yes" ]; then
echo "$APP: downloading $ASSET"
curl -fL --progress-bar "$URL" -o "${ZIP_FILE}.part"
mv "${ZIP_FILE}.part" "$ZIP_FILE"
else
echo "$APP: reusing cached $(basename "$ZIP_FILE")"
fi

rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR"
unzip -q "$ZIP_FILE" -d "$OUT_DIR"

# Normalise what the archives unpack to, so run-tests.sh has one path per host
# rather than a version-stamped name that changes with every bump.
case "$(uname -s)" in
Darwin) : ;; # Godot.app, already a stable name
Linux)
BIN=$(find "$OUT_DIR" -maxdepth 1 -type f -name 'Godot_v*' | head -n 1)
[ -n "$BIN" ] && mv "$BIN" "${OUT_DIR}/godot" && chmod +x "${OUT_DIR}/godot" ;;
*)
BIN=$(find "$OUT_DIR" -maxdepth 1 -type f -name 'Godot_v*.exe' | head -n 1)
[ -n "$BIN" ] && mv "$BIN" "${OUT_DIR}/godot.exe" ;;
esac

printf '%s\n' "$VERSION" > "$VERSION_FILE"
echo "$APP: Godot $VERSION installed in $(basename "$OUT_DIR")/"
Loading
Loading