Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7507b3e
Start working on Blackholio for Godot
lisandroct Apr 20, 2026
8a0c39f
Wrap up part 1
lisandroct Apr 22, 2026
1bd67e8
Wrap up demo
lisandroct Apr 23, 2026
7818942
Update tutorial up to part 3
lisandroct Apr 24, 2026
1f2004f
Update Godot docs
lisandroct Apr 27, 2026
3cf24e0
Fix and polish part 1 and part 2 of the Godot tutorial
lisandroct Apr 28, 2026
8d80da0
Fix and polish part 3 and part 4 of the Godot tutorial
lisandroct Apr 28, 2026
bb03df9
Cleanup Unity tutorial part 2
lisandroct Apr 28, 2026
bcbe18d
Fix and polish part 3 and part 4 of the Unity tutorial
lisandroct Apr 28, 2026
af3c983
Add Godot SDK
lisandroct Apr 29, 2026
fe286e1
Add notes about auth tokens and splitting into multiple circles
lisandroct Apr 30, 2026
56a9223
Update demo files
lisandroct Apr 30, 2026
8faf40b
Add Godot SDK csproj
lisandroct May 5, 2026
db415c1
Update demo project
lisandroct May 5, 2026
1e40662
Fix tutorial
lisandroct May 5, 2026
f799652
Use ZIndex in the Arena
lisandroct May 5, 2026
b08e6b0
Use ZIndex in code
lisandroct May 5, 2026
1d0e750
Update csproj to 2.2.0
lisandroct May 6, 2026
d42df6e
Add Scheduled Table attribute to MoveAllPlayersTimer
lisandroct May 6, 2026
3dba518
Fix images
lisandroct May 7, 2026
9b35c37
Fix images urls
lisandroct May 7, 2026
af23c3b
Fix ci problems
lisandroct May 8, 2026
38115af
Add Godot SDK to regen dlls tool
lisandroct May 8, 2026
4258b48
Adding .meta files for new .cs scripts
rekhoff May 8, 2026
d4f304f
Fix .NET test
lisandroct May 8, 2026
bca8a40
Fix smoketest
lisandroct May 8, 2026
db4deea
Clean
lisandroct May 12, 2026
1b01909
[lisandro/godot-blackholio]: use Directory.Build.props for godot outp…
bfops May 12, 2026
2c45ade
[lisandro/godot-blackholio]: Revert "[lisandro/godot-blackholio]: use…
bfops May 14, 2026
ddc00e3
[lisandro/godot-blackholio]: comments
bfops May 14, 2026
d56a0da
Add missing features to demo
lisandroct May 15, 2026
4a7665a
Add last comment to tutorial
lisandroct May 5, 2026
d16e104
Small change to last message in Unity's tutorial
lisandroct May 5, 2026
0da02d1
Update final result image
lisandroct May 5, 2026
e09038b
Add tests
lisandroct May 14, 2026
0c10ba6
Clean
lisandroct May 15, 2026
2d481fc
Clean test
lisandroct May 15, 2026
127ef23
Merge branch 'master' into lisandro/godot-blackholio-completion
bfops May 16, 2026
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
131 changes: 130 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,135 @@ jobs:
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}

godot-testsuite:
needs: [lints]
permissions:
contents: read
runs-on: spacetimedb-new-runner-2
timeout-minutes: 30
env:
CARGO_TARGET_DIR: ${{ github.workspace }}/target
steps:
- name: Checkout repository
id: checkout-stdb
uses: actions/checkout@v4

# Run cheap .NET setup first. If that fails, no need to run expensive Godot tests.

- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
global-json-file: global.json

- name: Override NuGet packages
run: |
dotnet pack crates/bindings-csharp/BSATN.Runtime
dotnet pack crates/bindings-csharp/Runtime

# Write out the nuget config file to `nuget.config`. This causes the spacetimedb-csharp-sdk repository
# to be aware of the local versions of the `bindings-csharp` packages in SpacetimeDB, and use them if
# available. Otherwise, `spacetimedb-csharp-sdk` will use the NuGet versions of the packages.
# This means that (if version numbers match) we will test the local versions of the C# packages, even
# if they're not pushed to NuGet.
# See https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file for more info on the config file.
cd sdks/csharp
./tools~/write-nuget-config.sh ../..

- name: Restore .NET solution
working-directory: sdks/csharp
run: dotnet restore --configfile NuGet.Config SpacetimeDB.ClientSDK.Godot.sln

# Now, setup the Godot tests.
- name: Patch spacetimedb dependency in Cargo.toml
working-directory: demo/Blackholio/server-rust
run: |
sed -i "s|spacetimedb *=.*|spacetimedb = \{ path = \"../../../crates/bindings\" \}|" Cargo.toml
cat Cargo.toml

- name: Install Rust toolchain
uses: dsherret/rust-toolchain-file@v1
- name: Set default rust toolchain
run: rustup default $(rustup show active-toolchain | cut -d' ' -f1)

- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: ${{ github.workspace }}
shared-key: spacetimedb
# Let the main CI job save the cache since it builds the most things
save-if: false
prefix-key: v1

# This step shouldn't be needed, but somehow we end up with caches that are missing librusty_v8.a.
# ChatGPT suspects that this could be due to different build invocations using the same target dir,
# and this makes sense to me because we only see it in this job where we mix `cargo build -p` with
# `cargo build --manifest-path` (which apparently build different dependency trees).
# However, we've been unable to fix it so... /shrug
- name: Check v8 outputs
run: |
find "${CARGO_TARGET_DIR}"/ -type f | grep '[/_]v8' || true
if ! [ -f "${CARGO_TARGET_DIR}"/release/gn_out/obj/librusty_v8.a ]; then
echo "Could not find v8 output file librusty_v8.a; rebuilding manually."
cargo clean --release -p v8 || true
cargo build --release -p v8
fi

- name: Install SpacetimeDB CLI from the local checkout
run: |
export CARGO_HOME="$HOME/.cargo"
echo "$CARGO_HOME/bin" >> "$GITHUB_PATH"
cargo install --force --path crates/cli --locked --message-format=short
cargo install --force --path crates/standalone --locked --message-format=short
# Add a handy alias using the old binary name, so that we don't have to rewrite all scripts (incl. in submodules).
ln -sf $CARGO_HOME/bin/spacetimedb-cli $CARGO_HOME/bin/spacetime

- name: Generate client bindings
working-directory: demo/Blackholio/server-rust
run: bash ./generate.sh -y

- name: Check for changes
run: |
tools/check-diff.sh demo/Blackholio/client-godot/module_bindings || {
echo 'Error: Godot bindings are dirty. Please run `demo/Blackholio/server-rust/generate.sh`.'
exit 1
}

- name: Patch SpacetimeDB Godot SDK dependency
working-directory: demo/Blackholio/client-godot
run: |
dotnet remove package SpacetimeDB.ClientSDK.Godot
dotnet add reference ../../../sdks/csharp/SpacetimeDB.ClientSDK.Godot.csproj
cat blackholio.csproj

- name: Setup Godot
uses: chickensoft-games/setup-godot@v2
with:
version: 4.6.2
use-dotnet: true

- uses: actions/cache@v3
with:
path: demo/Blackholio/client-godot/.godot
key: Godot-${{ github.head_ref }}
restore-keys: Godot-

- name: Build Godot project
run: godot --headless --path demo/Blackholio/client-godot --build-solutions --quit

- name: Start SpacetimeDB
run: |
spacetime start &
disown

- name: Publish godot-tests module to SpacetimeDB
working-directory: demo/Blackholio/server-rust
run: |
spacetime logout && spacetime login --server-issued-login local
bash ./publish.sh

- name: Run Godot tests
run: godot --headless --path demo/Blackholio/client-godot --scene res://tests/GodotPlayModeTests.tscn

csharp-testsuite:
needs: [lints]
runs-on: spacetimedb-new-runner-2
Expand Down Expand Up @@ -747,7 +876,7 @@ jobs:

- name: Run .NET tests
working-directory: sdks/csharp
run: dotnet test -warnaserror --no-restore
run: dotnet test -warnaserror --no-restore SpacetimeDB.ClientSDK.csproj

- name: Verify C# formatting
working-directory: sdks/csharp
Expand Down
43 changes: 36 additions & 7 deletions crates/smoketests/tests/smoketests/quickstart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,32 @@ fn create_nuget_config(sources: &[(String, PathBuf)], mappings: &[(String, Strin

/// Override nuget config to use a local NuGet package on a .NET project.
fn override_nuget_package(project_dir: &Path, package: &str, source_dir: &Path, build_subdir: &str) -> Result<()> {
override_nuget_package_from_project(project_dir, package, source_dir, None, build_subdir)
}

/// Override nuget config to use a local NuGet package built from a specific .NET project.
fn override_nuget_package_from_project(
project_dir: &Path,
package: &str,
source_dir: &Path,
source_project: Option<&str>,
build_subdir: &str,
) -> Result<()> {
println!("Override {package}: {project_dir:?} with {source_dir:?}");

// Make sure the local package is built
let workspace = workspace_root();
let repo_nuget_config = workspace.join("NuGet.Config");
let source_project_path = source_project.map(|project| source_dir.join(project));
if repo_nuget_config.exists() {
let output = Command::new("dotnet")
.args(["restore", "--configfile", repo_nuget_config.to_str().unwrap()])
let mut command = Command::new("dotnet");
command.arg("restore");
if let Some(source_project_path) = &source_project_path {
command.arg(source_project_path);
}
let output = command
.arg("--configfile")
.arg(&repo_nuget_config)
.current_dir(source_dir)
.output()
.context("Failed to run dotnet restore")?;
Expand All @@ -152,8 +170,13 @@ fn override_nuget_package(project_dir: &Path, package: &str, source_dir: &Path,
);
}

let output = Command::new("dotnet")
.args(["pack", "-c", "Release", "--no-restore"])
let mut command = Command::new("dotnet");
command.arg("pack");
if let Some(source_project_path) = &source_project_path {
command.arg(source_project_path);
}
let output = command
.args(["-c", "Release", "--no-restore"])
.current_dir(source_dir)
.output()
.context("Failed to run dotnet pack")?;
Expand All @@ -165,8 +188,13 @@ fn override_nuget_package(project_dir: &Path, package: &str, source_dir: &Path,
);
}
} else {
let output = Command::new("dotnet")
.args(["pack", "-c", "Release"])
let mut command = Command::new("dotnet");
command.arg("pack");
if let Some(source_project_path) = &source_project_path {
command.arg(source_project_path);
}
let output = command
.args(["-c", "Release"])
.current_dir(source_dir)
.output()
.context("Failed to run dotnet pack")?;
Expand Down Expand Up @@ -637,10 +665,11 @@ log = "0.4"
&workspace.join("crates/bindings-csharp/BSATN.Runtime"),
"bin/Release",
)?;
override_nuget_package(
override_nuget_package_from_project(
client_path,
"SpacetimeDB.ClientSDK",
&workspace.join("sdks/csharp"),
Some("SpacetimeDB.ClientSDK.csproj"),
"bin~/Release",
)?;

Expand Down
4 changes: 4 additions & 0 deletions demo/Blackholio/client-godot/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
root = true

[*]
charset = utf-8
2 changes: 2 additions & 0 deletions demo/Blackholio/client-godot/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
3 changes: 3 additions & 0 deletions demo/Blackholio/client-godot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Godot 4+ specific ignores
.godot/
/android/
49 changes: 49 additions & 0 deletions demo/Blackholio/client-godot/CameraController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Godot;

public partial class CameraController : Camera2D
{
[Export]
public float BaseVisibleRadius { get; set; } = 50.0f;

[Export]
public float FollowLerpSpeed { get; set; } = 8.0f;

[Export]
public float ZoomLerpSpeed { get; set; } = 2.0f;

private float WorldSize { get; }

public CameraController(float worldSize)
{
WorldSize = worldSize;
}

public override void _Process(double delta)
{
Vector2 targetPosition;
if (GameManager.IsConnected() && PlayerController.Local != null && PlayerController.Local.TryGetCenterOfMass(out var centerOfMass))
{
targetPosition = centerOfMass;
}
else
{
var hWorldSize = WorldSize * 0.5f;
targetPosition = new Vector2(hWorldSize, hWorldSize);
}

GlobalPosition = GlobalPosition.Lerp(targetPosition, (float)delta * FollowLerpSpeed);

if (PlayerController.Local == null)
{
return;
}

var targetCameraSize = CalculateCameraSize(PlayerController.Local);
var desiredZoom = Vector2.One * (BaseVisibleRadius / Mathf.Max(targetCameraSize, 1.0f));
Zoom = Zoom.Lerp(desiredZoom, (float)delta * ZoomLerpSpeed);
}

private static float CalculateCameraSize(PlayerController player) => 10.0f
+ Mathf.Min(10.0f, player.TotalMass() / 5.0f)
+ Mathf.Min(player.NumberOfOwnedCircles - 1, 1) * 30.0f;
}
1 change: 1 addition & 0 deletions demo/Blackholio/client-godot/CameraController.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c5uuweuf2vu0e
103 changes: 103 additions & 0 deletions demo/Blackholio/client-godot/Circle2D.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using Godot;

public enum CircleVisualStyle
{
Player,
Food
}

public abstract partial class Circle2D : Node2D
{
private float _radius = 10.0f;
[Export]
public float Radius
{
get => _radius;
set
{
if (Mathf.IsEqualApprox(_radius, value)) return;

_radius = value;
QueueRedraw();
}
}

private Color _color = Colors.Brown;
[Export]
public Color Color
{
get => _color;
set
{
if (_color == value) return;

_color = value;
QueueRedraw();
}
}

[Export]
public CircleVisualStyle VisualStyle { get; set; } = CircleVisualStyle.Player;

[Export]
public float AnimationSeed { get; set; }

public override void _Draw()
{
if (Radius <= 0.01f) return;

switch (VisualStyle)
{
case CircleVisualStyle.Player:
DrawPlayerCircle();
break;
case CircleVisualStyle.Food:
DrawFood();
break;
default:
throw new ArgumentOutOfRangeException();
}
}

protected void RedrawAnimatedVisuals() => QueueRedraw();

private void DrawPlayerCircle()
{
var time = Time.GetTicksMsec() / 1000.0f;
var pulse = 0.5f + 0.5f * Mathf.Sin(time * 2.2f + AnimationSeed);
DrawCircle(Vector2.Zero, Radius * (1.16f + pulse * 0.04f), WithAlpha(Color, 0.14f));
DrawCircle(Vector2.Zero, Radius, Shade(Color, 0.58f));
DrawCircle(Vector2.Zero, Radius * 0.82f, Color);
DrawCircle(new Vector2(-Radius * 0.22f, -Radius * 0.24f), Radius * 0.34f, WithAlpha(Shade(Color, 1.42f), 0.72f));

var outline = new Vector2[73];
for (var i = 0; i < outline.Length; i++)
{
var angle = Mathf.Tau * i / (outline.Length - 1);
var wave = Mathf.Sin(angle * 7.0f + time * 3.0f + AnimationSeed) * 0.035f;
outline[i] = Vector2.FromAngle(angle) * Radius * (1.015f + wave);
}

DrawPolyline(outline, WithAlpha(Shade(Color, 1.55f), 0.88f), Mathf.Clamp(Radius * 0.085f, 1.5f, 5.0f), true);
}

private void DrawFood()
{
var time = Time.GetTicksMsec() / 1000.0f;
var pulse = 0.5f + 0.5f * Mathf.Sin(time * 5.0f + AnimationSeed);
DrawCircle(Vector2.Zero, Radius * (1.32f + pulse * 0.09f), WithAlpha(Color, 0.1f));
DrawCircle(Vector2.Zero, Radius, Shade(Color, 0.72f));
DrawCircle(Vector2.Zero, Radius * 0.64f, Color);
DrawCircle(Vector2.Zero, Radius * 0.24f, WithAlpha(Shade(Color, 1.55f), 0.86f));
}

private static Color Shade(Color color, float multiplier) => new Color(
Mathf.Clamp(color.R * multiplier, 0.0f, 1.0f),
Mathf.Clamp(color.G * multiplier, 0.0f, 1.0f),
Mathf.Clamp(color.B * multiplier, 0.0f, 1.0f),
color.A
);

private static Color WithAlpha(Color color, float alpha) => new(color.R, color.G, color.B, alpha);
}
1 change: 1 addition & 0 deletions demo/Blackholio/client-godot/Circle2D.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://7lmrsq2i7mi1
Loading
Loading