From c59cf2b5e792cee56d1910caf552af9f8d2eab79 Mon Sep 17 00:00:00 2001
From: Jan Hangebrauck
Date: Fri, 29 Aug 2025 10:24:08 +0200
Subject: [PATCH 01/25] potential fix for iframe keyboard events
---
game/src/engine/main.cpp | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/game/src/engine/main.cpp b/game/src/engine/main.cpp
index d44f9d91e..a1274b58a 100644
--- a/game/src/engine/main.cpp
+++ b/game/src/engine/main.cpp
@@ -1282,6 +1282,13 @@ int main(int argc, char **argv)
{
logoutf("init: sdl");
+#if __EMSCRIPTEN__
+ // Ensure keyboard events are captured when running inside iframes by
+ // attaching SDL's keyboard listeners to the iframe document.
+ // This must be set before SDL_Init.
+ SDL_SetHint(SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT, "#document");
+#endif
+
if(SDL_Init(SDL_INIT_TIMER|SDL_INIT_VIDEO|SDL_INIT_AUDIO)<0) fatal("Unable to initialize SDL: %s", SDL_GetError());
#ifdef SDL_VIDEO_DRIVER_X11
From 5bc8b968a5c4ead004a99fc2137dfbb0cd40fc5b Mon Sep 17 00:00:00 2001
From: Jan Hangebrauck
Date: Fri, 29 Aug 2025 10:24:39 +0200
Subject: [PATCH 02/25] wip: dockerize build pipeline and serve
---
.gitignore | 2 ++
README.md | 33 ++++++++++++++++++-
assets/.gitignore | 2 ++
assets/README.md | 10 +++---
assets/base.py | 10 +++---
assets/setup | 2 +-
dev.auto.yaml | 7 ++++
docker/emscripten.Dockerfile | 59 +++++++++++++++++++++++++++++++++
scripts/build-assets-docker | 49 ++++++++++++++++++++++++++++
scripts/build-game-docker | 58 +++++++++++++++++++++++++++++++++
scripts/clean-generated | 15 +++++++++
scripts/serve-docker | 63 ++++++++++++++++++++++++++++++++++++
12 files changed, 298 insertions(+), 12 deletions(-)
create mode 100644 dev.auto.yaml
create mode 100644 docker/emscripten.Dockerfile
create mode 100755 scripts/build-assets-docker
create mode 100755 scripts/build-game-docker
create mode 100755 scripts/clean-generated
create mode 100755 scripts/serve-docker
diff --git a/.gitignore b/.gitignore
index c94076d36..819c28627 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@ earthly/**
/sour
/sourdump
/sour-build
+.home/
+.pip-cache/
\ No newline at end of file
diff --git a/README.md b/README.md
index 3f14e8c3e..250cbae9f 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,37 @@ Running `sour` will start a Sour server accessible to web clients at `http://0.0
By serving on `0.0.0.0` by default, the Sour server will be available to other devices on the local network at IP of the device running the Sour server.
+### Building the web game with Docker (Emscripten)
+
+If you prefer building in a containerized environment, a Dockerfile and helper script are provided:
+
+```bash
+# Build the image and compile the game into game/dist/game
+./scripts/build-game-docker
+
+# Optionally control output directory
+GAME_OUTPUT_DIR=client/dist/game ./scripts/build-game-docker
+```
+
+This uses an Ubuntu base with Emscripten 3.1.8 (same as CI), mounts your checkout at `/workspace`, and runs `game/build` inside the container. Artifacts will appear under `game/dist/game` by default.
+
+### Running the server in Docker
+
+After building the game and client (and optionally assets), you can run the integrated server with:
+
+```bash
+# Default: serves on 0.0.0.0:1337
+./scripts/serve-docker
+
+# With a config file
+./scripts/serve-docker dev.yaml
+
+# Override bind address/port
+WEB_ADDR=127.0.0.1 WEB_PORT=1337 ./scripts/serve-docker
+```
+
+The script mounts your workspace and runs `go run ./cmd/sour serve` inside the container using your UID/GID so no files are owned by root.
+
## Configuration
Sour is highly configurable. When run without arguments, `sour` defaults to running `sour serve` with the [default Sour configuration](https://github.com/cfoust/sour/blob/main/pkg/config/default.yaml). You change Sour's configuration by providing the path to a configuration file to `sour serve`:
@@ -80,7 +111,7 @@ Here is a high level description of the repository's contents:
- Gives clients both on the web and desktop client access to game servers managed by Sour.
- `game`: All of the Cube 2 code and Emscripten compilation scripts. Originally this was a fork of [BananaBread](https://github.com/kripken/BananaBread), kripken's original attempt at compiling Sauerbraten for the web. Since then I have upgraded the game to the newest mainline version several times and moved to WebGL2.
- `client`: A React web application that uses the compiled Sauerbraten game found in `game`, pulls assets, and proxies all server communication over a WebSocket.
-- `assets`: Scripts for building web-compatible game assets. This is an extremely complicated topic and easily the most difficult aspect of shipping Sauerbraten to the web. Check out this [section's README](services/assets) for more information.
+- `assets`: Scripts for building web-compatible game assets. This is an extremely complicated topic and easily the most difficult aspect of shipping Sauerbraten to the web. Check out this [section's README](assets/README.md) for more information.
## Contributing
diff --git a/assets/.gitignore b/assets/.gitignore
index 330106c1a..755b8260e 100644
--- a/assets/.gitignore
+++ b/assets/.gitignore
@@ -7,3 +7,5 @@ quadropolis
*.tar.gz
cache/**
/dist
+.index.source
+.index.json
\ No newline at end of file
diff --git a/assets/README.md b/assets/README.md
index 387f511d4..b9878e546 100644
--- a/assets/README.md
+++ b/assets/README.md
@@ -57,15 +57,15 @@ Asset sources are specified at runtime using the `ASSET_SOURCE` environment vari
# Valid ASSET_SOURCES:
######################
-# /assets/.index.json is the asset source that comes baked into the image. Generally you want this even if you're using your own map sources; this is because Sour automatically loads the `base` bundle, which contains all of the basic assets necessary to run the game, like main menu graphics.
-ASSET_SOURCE="/assets/.index.json"
+# /assets/.index.source is the asset source that comes baked into the image. Generally you want this even if you're using your own map sources; this is because Sour automatically loads the `base` bundle, which contains all of the basic assets necessary to run the game, like main menu graphics.
+ASSET_SOURCE="/assets/.index.source"
# Asset sources are separated by single semicolons.
-ASSET_SOURCE="/assets/.index.json;https://example.com/2bfc017.index.json"
-# As an example, if a user runs `/map complex`, Sour first searches /assets/.index.json; if there is a `complex` map, it loads that verssion even if one also exists in the second source.
+ASSET_SOURCE="/assets/.index.source;https://example.com/2bfc017.index.source"
+# As an example, if a user runs `/map complex`, Sour first searches /assets/.index.source; if there is a `complex` map, it loads that verssion even if one also exists in the second source.
# In production (sourga.me) the ASSET_SOURCE looks like this:
-ASSET_SOURCE="/assets/.index.json;https://static.sourga.me/blobs/XXXXX.index.json;https://static.sourga.me/quadropolis/XXXXX.index.json"
+ASSET_SOURCE="/assets/.index.source;https://static.sourga.me/blobs/XXXXX.index.source;https://static.sourga.me/quadropolis/XXXXX.index.source"
# In other words, Sour will load maps that appear in the latest SVN version of the game _first_, then from Quadropolis if the map did not appear in the base game.
```
diff --git a/assets/base.py b/assets/base.py
index 92ded19d7..504fd85cb 100644
--- a/assets/base.py
+++ b/assets/base.py
@@ -172,11 +172,11 @@ def expand_hudguns(prefix: str) -> List[str]:
maps = list(filter(lambda a: a.endswith('.ogz'), files))
if args.maps:
maps = list(map(lambda a: f"packages/base/{a}.ogz", args.maps))
-
- maps.append("packages/base/xmwhub.ogz")
-
- if 'none' in args.maps:
- maps = []
+ if 'none' in args.maps:
+ maps = []
+ else:
+ # Only add the default map when no explicit map list is provided.
+ maps.append("packages/base/xmwhub.ogz")
outdir = args.outdir
os.makedirs(outdir, exist_ok=True)
diff --git a/assets/setup b/assets/setup
index 03c841f6d..ecee1e1d6 100755
--- a/assets/setup
+++ b/assets/setup
@@ -9,7 +9,7 @@ set -e
mkdir -p cache
if ! pip3 list | grep "cbor2" > /dev/null 2>&1; then
- pip3 install -r requirements.txt
+ pip3 install --user -r requirements.txt
fi
#sauer_archive="sauerbraten-6481.tar.gz"
diff --git a/dev.auto.yaml b/dev.auto.yaml
new file mode 100644
index 000000000..ec67cfd4c
--- /dev/null
+++ b/dev.auto.yaml
@@ -0,0 +1,7 @@
+server:json
+ cacheDirectory: "./.cache/assets"
+ assets:
+ - "fs:assets/dist/.index.source"
+client:
+ assets:
+ - "#origin/assets/0/.index.source"
diff --git a/docker/emscripten.Dockerfile b/docker/emscripten.Dockerfile
new file mode 100644
index 000000000..59abce986
--- /dev/null
+++ b/docker/emscripten.Dockerfile
@@ -0,0 +1,59 @@
+FROM ubuntu:22.04
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN apt-get update && apt-get install -y \
+ build-essential \
+ cmake \
+ git \
+ python3 \
+ python3-pip \
+ curl \
+ ca-certificates \
+ unzip \
+ xz-utils \
+ patch \
+ pkg-config \
+ imagemagick \
+ inotify-tools \
+ ucommon-utils \
+ unrar \
+ zlib1g-dev \
+ libenet-dev \
+ swig \
+ npm \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install Emscripten SDK (same version used in CI)
+RUN git clone https://github.com/emscripten-core/emsdk.git /emsdk \
+ && cd /emsdk \
+ && ./emsdk install 3.1.8 \
+ && ./emsdk activate 3.1.8
+
+# Install modern Go toolchain (for go.mod go 1.22.x)
+ENV GOVER=1.22.5
+RUN curl -fsSL https://go.dev/dl/go${GOVER}.linux-amd64.tar.gz -o /tmp/go.tgz \
+ && rm -rf /usr/local/go \
+ && tar -C /usr/local -xzf /tmp/go.tgz \
+ && rm /tmp/go.tgz
+
+# Make Emscripten available in all shells
+ENV EMSDK=/emsdk \
+ EM_CONFIG=/emsdk/.emscripten \
+ PATH=/emsdk:/emsdk/upstream/emscripten:/emsdk/node/14.18.2_64bit/bin:$PATH
+
+# Prepend Go to PATH
+ENV PATH=/usr/local/go/bin:$PATH
+
+# Ensure Yarn is available in the build image (installed as root)
+RUN npm i -g yarn
+
+# Default workdir where the repo will be mounted
+WORKDIR /workspace
+
+# Show versions for easier troubleshooting
+RUN emcc -v && python3 --version && cmake --version && go version
+
+CMD ["bash"]
+
+
diff --git a/scripts/build-assets-docker b/scripts/build-assets-docker
new file mode 100755
index 000000000..362130346
--- /dev/null
+++ b/scripts/build-assets-docker
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+IMAGE_TAG="sour-emscripten:3.1.8"
+PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
+
+build_image() {
+ docker build -f "$PROJECT_ROOT/docker/emscripten.Dockerfile" -t "$IMAGE_TAG" "$PROJECT_ROOT"
+}
+
+build_image
+
+HOST_UID="$(id -u)"
+HOST_GID="$(id -g)"
+
+ASSET_OUTPUT_DIR=${ASSET_OUTPUT_DIR:-output}
+# Space-separated maps to build; use 'none' to skip maps
+ASSET_MAPS=${ASSET_MAPS:-"complex dust2 turbine"}
+
+exec docker run --rm \
+ --user "$HOST_UID:$HOST_GID" \
+ -e ASSET_OUTPUT_DIR="$ASSET_OUTPUT_DIR" \
+ -e HOME="/workspace/.home" \
+ -e PIP_CACHE_DIR="/workspace/.pip-cache" \
+ -e ASSET_MAPS="$ASSET_MAPS" \
+ -v "$PROJECT_ROOT":/workspace \
+ -w /workspace/assets \
+ "$IMAGE_TAG" \
+ bash -lc '
+ set -e
+ mkdir -p "$HOME" "$PIP_CACHE_DIR"
+ python3 -m pip install --user -r requirements.txt || true
+ ./setup
+ # Rebuild sourdump to ensure latest code is used
+ rm -f sourdump
+ go build -o sourdump ../cmd/sourdump/main.go
+ # Build base assets; configure maps via ASSET_MAPS
+ # Expand ASSET_MAPS into positional args
+ set -- $ASSET_MAPS
+ python3 base.py \
+ --root https://static.sourga.me/blobs/6481/.index.source \
+ --models \
+ --download \
+ --outdir dist \
+ "$@"
+ '
+
+
diff --git a/scripts/build-game-docker b/scripts/build-game-docker
new file mode 100755
index 000000000..ceb71dc60
--- /dev/null
+++ b/scripts/build-game-docker
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+IMAGE_TAG="sour-emscripten:3.1.8"
+PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
+SCRIPTS_DIR="$PROJECT_ROOT/scripts"
+EM_CACHE_DIR="$PROJECT_ROOT/.emscripten-cache"
+
+build_image() {
+ docker build -f "$PROJECT_ROOT/docker/emscripten.Dockerfile" -t "$IMAGE_TAG" "$PROJECT_ROOT"
+}
+
+build_image
+
+
+# Allow overriding output dir
+GAME_OUTPUT_DIR=${GAME_OUTPUT_DIR:-dist/game}
+
+# Run as the host user to avoid root-owned files; mount a writable EM_CACHE
+HOST_UID="$(id -u)"
+HOST_GID="$(id -g)"
+mkdir -p "$EM_CACHE_DIR"
+
+exec docker run --rm \
+ --user "$HOST_UID:$HOST_GID" \
+ -e GAME_OUTPUT_DIR="$GAME_OUTPUT_DIR" \
+ -e EM_CACHE="/workspace/.emscripten-cache" \
+ -v "$PROJECT_ROOT":/workspace \
+ -w /workspace/game \
+ "$IMAGE_TAG" \
+ bash -lc '
+ set -e
+ source /emsdk/emsdk_env.sh
+
+ echo "[1/3] Building WASM game..."
+ cd /workspace/game
+ ./build
+
+ echo "[2/3] Building web client..."
+ cd /workspace/client
+ yarn install
+ yarn build
+ cp src/index.html src/favicon.ico src/background.png dist/
+ mkdir -p dist/game && cp -r ../game/dist/game/* dist/game/
+
+ echo "[3/3] Staging site for Go server..."
+ rm -rf /workspace/pkg/server/static/site
+ mkdir -p /workspace/pkg/server/static/site
+ cp -r dist/* /workspace/pkg/server/static/site/
+
+ echo "[4/4] Building assets..."
+ '
+
+cd "$SCRIPTS_DIR"
+./build-assets-docker
+
+
diff --git a/scripts/clean-generated b/scripts/clean-generated
new file mode 100755
index 000000000..56da64ba1
--- /dev/null
+++ b/scripts/clean-generated
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
+
+rm -rf "$PROJECT_ROOT/game/dist/game" || true
+rm -rf "$PROJECT_ROOT/client/dist" || true
+rm -rf "$PROJECT_ROOT/pkg/server/static/site" || true
+rm -rf "$PROJECT_ROOT/.emscripten-cache" || true
+
+# Also remove common temporary outputs under assets
+rm -rf "$PROJECT_ROOT/assets/dist" || true
+
+echo "Cleaned generated outputs."
diff --git a/scripts/serve-docker b/scripts/serve-docker
new file mode 100755
index 000000000..c3fb1849f
--- /dev/null
+++ b/scripts/serve-docker
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+IMAGE_TAG="sour-emscripten:3.1.8"
+PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
+
+# Build the image if missing
+if ! docker image inspect "$IMAGE_TAG" >/dev/null 2>&1; then
+ docker build -f "$PROJECT_ROOT/docker/emscripten.Dockerfile" -t "$IMAGE_TAG" "$PROJECT_ROOT"
+fi
+
+HOST_UID="$(id -u)"
+HOST_GID="$(id -g)"
+
+# Ports and address
+WEB_ADDR=${WEB_ADDR:-0.0.0.0}
+WEB_PORT=${WEB_PORT:-1337}
+
+# Optional config file path (in host workspace). Pass as first arg.
+CONFIG_FILE=${1:-}
+
+# Ensure local assets index symlink exists (use relative path so it works in container)
+if [ -f "$PROJECT_ROOT/assets/dist/.index.source" ]; then
+ # Always refresh symlink to ensure it works inside the container
+ (cd "$PROJECT_ROOT/assets" && ln -sf "dist/.index.source" ".index.source")
+fi
+
+CMD="go run ./cmd/sour serve"
+if [ -n "$CONFIG_FILE" ]; then
+ # Map host path to container path
+ if [ -f "$CONFIG_FILE" ]; then
+ REL="${CONFIG_FILE#$PROJECT_ROOT/}"
+ CMD="go run ./cmd/sour serve \"$REL\""
+ fi
+else
+ # Auto-generate a simple config pointing to local assets index if present
+ if [ -f "$PROJECT_ROOT/assets/.index.source" ]; then
+ AUTO_CFG="$PROJECT_ROOT/dev.auto.yaml"
+ cat > "$AUTO_CFG" <
Date: Fri, 29 Aug 2025 12:58:37 +0200
Subject: [PATCH 03/25] fix game serving fix keyboard events not detected issue
(in iframe as well as standalone) wip: update readme
---
README.md | 42 +++++++++++++++++++++++++-----
client/src/index.tsx | 12 +++++++++
dev.auto.yaml | 21 ++++++++++++++-
game/src/engine/main.cpp | 8 +++---
scripts/build-all | 10 ++++++++
scripts/build-assets-docker | 6 -----
scripts/build-docker-image | 13 ++++++++++
scripts/build-game-docker | 15 -----------
scripts/clean-generated | 9 +++++++
scripts/serve-docker | 51 +++++++++++++++++++++++++++++++------
10 files changed, 146 insertions(+), 41 deletions(-)
create mode 100755 scripts/build-all
create mode 100755 scripts/build-docker-image
diff --git a/README.md b/README.md
index 250cbae9f..c5d421caa 100644
--- a/README.md
+++ b/README.md
@@ -4,14 +4,12 @@
-
-
-
-
-
-
## What is this?
+This is a fork of the excellent [original sour repository](https://github.com/cfoust/sour) made by cfoust. It has been made with the goal to embed sauerbraten as an in-community game experience into the Common Ground platform. I want to enable a Quake-like experience, and add additional features like Tournaments between communities.
+
+
+
Sour is a Cube 2: Sauerbraten server that serves a fully-featured web-version of Sauerbraten (with support for mobile devices) in addition to accepting connections from the traditional, desktop version of the game. Sour is the easiest way to play Sauerbraten with your friends.
Give it a try.
@@ -32,6 +30,24 @@ brew install cfoust/taps/sour@0.2.2
In addition to all of the base game assets, these archives only contain three maps: `complex`, `dust2`, and `turbine`.
+### Prerequisite: Git LFS
+
+This repository stores large binary assets (textures, images, etc.) in Git LFS. After cloning, ensure LFS is installed and fetch objects, or some files will be tiny pointer stubs that fail at runtime.
+
+Brief setup:
+
+```bash
+# Ubuntu/Debian
+sudo apt install git-lfs
+
+# macOS (Homebrew)
+brew install git-lfs
+
+# Oneātime init, then pull LFS content
+git lfs install
+git lfs pull
+```
+
## Running Sour
To run Sour, extract a release archive anywhere you wish, navigate to that directory, and run `./sour`. If you installed Sour with `brew`, just run `sour` in any terminal session.
@@ -113,9 +129,21 @@ Here is a high level description of the repository's contents:
- `client`: A React web application that uses the compiled Sauerbraten game found in `game`, pulls assets, and proxies all server communication over a WebSocket.
- `assets`: Scripts for building web-compatible game assets. This is an extremely complicated topic and easily the most difficult aspect of shipping Sauerbraten to the web. Check out this [section's README](assets/README.md) for more information.
+**Updates in this fork**
+
+- dockerized the build pipeline for the game client as well as assets
+- uses one docker helper container that compiles everything and can also serve the game server
+- fixed a bug that prevented keyboard events to work in iframes
+
+
+
## Contributing
-Join us on [Discord](https://discord.gg/WP3EbYym4M) to chat with us and see how you can help out! Check out the [issues tab](https://github.com/cfoust/sour/issues) to get an idea of what needs doing.
+This repository is maintained by the Common Ground Team (I'm one of the founders) as an in-community gaming experience. Common Ground itself is a progressive web app and supports embedding custom games and plugins into Communities. If you're interested in the project, join our [Common Ground community on app.cg](https://app.cg/c/commonground/).
+
+Besides Sour / Sauerbraten, I also made a Luanti (think "open source minecraft") game plugin available on app.cg. Like Sour, it is also a web assembly game with an original c++ codebase. You can find my [minetest-wasm repository here](https://github.com/Kaesual/minetest-wasm). You can play both games right in your browser, in the [Video Games community on app.cg](https://app.cg/c/videogames/).
+
+The original repository was made by cfoust. You can join the community on [Discord](https://discord.gg/WP3EbYym4M) to chat with them and see how you can help out! Check out the [cfoust sour issues tab](https://github.com/cfoust/sour/issues) to get an idea of what needs doing.
## Inspiration
diff --git a/client/src/index.tsx b/client/src/index.tsx
index 16bf363ff..03ce610b2 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -940,6 +940,7 @@ function App() {
className="game"
style={{ opacity: state.type !== GameStateType.Ready ? 0 : 1 }}
id="canvas"
+ tabIndex={0}
ref={(canvas) => {
if (canvas != null) {
// This is a bug in mobile Safari where Reader holds on to canvas refs
@@ -948,9 +949,20 @@ function App() {
canvas._evaluatedForTextContent = true
// @ts-ignore
canvas._cachedElementBoundingRect = {}
+ // Ensure the canvas is focusable for keyboard events
+ // (SDL with Emscripten binds keyboard to the target element)
+ // eslint-disable-next-line no-param-reassign
+ canvas.tabIndex = 0
}
Module.canvas = canvas
}}
+ onMouseDown={(_e: React.MouseEvent) => {
+ // Focus the canvas so key events are delivered here
+ // (important when SDL binds to #canvas)
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ const el = document.getElementById('canvas') as HTMLCanvasElement | null
+ if (el) el.focus()
+ }}
onContextMenu={(event) => event.preventDefault()}
>
{BROWSER.isMobile && (
diff --git a/dev.auto.yaml b/dev.auto.yaml
index ec67cfd4c..c93434a2a 100644
--- a/dev.auto.yaml
+++ b/dev.auto.yaml
@@ -1,7 +1,26 @@
-server:json
+server:
cacheDirectory: "./.cache/assets"
assets:
- "fs:assets/dist/.index.source"
+ presets:
+ - name: "default"
+ default: true
+ config:
+ defaultMode: "ffa"
+ defaultMap: "complex"
+ maps: ["complex", "dust2", "turbine"]
+ - name: "insta"
+ config:
+ defaultMode: "insta"
+ defaultMap: "dust2"
+ maps: ["complex", "dust2", "turbine"]
+ spaces:
+ - preset: default
+ config:
+ alias: lobby
+ - preset: insta
+ config:
+ alias: insta
client:
assets:
- "#origin/assets/0/.index.source"
diff --git a/game/src/engine/main.cpp b/game/src/engine/main.cpp
index a1274b58a..efb3af60f 100644
--- a/game/src/engine/main.cpp
+++ b/game/src/engine/main.cpp
@@ -1283,10 +1283,10 @@ int main(int argc, char **argv)
logoutf("init: sdl");
#if __EMSCRIPTEN__
- // Ensure keyboard events are captured when running inside iframes by
- // attaching SDL's keyboard listeners to the iframe document.
- // This must be set before SDL_Init.
- SDL_SetHint(SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT, "#document");
+ // Bind SDL keyboard events to the canvas element present in the DOM.
+ // This avoids null targets in some environments and works in iframes.
+ // Must be set before SDL_Init.
+ SDL_SetHint(SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT, "#canvas");
#endif
if(SDL_Init(SDL_INIT_TIMER|SDL_INIT_VIDEO|SDL_INIT_AUDIO)<0) fatal("Unable to initialize SDL: %s", SDL_GetError());
diff --git a/scripts/build-all b/scripts/build-all
new file mode 100755
index 000000000..c9ff1d16c
--- /dev/null
+++ b/scripts/build-all
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+./scripts/build-docker-helper
+./scripts/build-assets-docker
+./scripts/build-game-docker
+
+echo "Build complete. You can now run ./scripts/serve-docker to run the game server."
\ No newline at end of file
diff --git a/scripts/build-assets-docker b/scripts/build-assets-docker
index 362130346..62941573a 100755
--- a/scripts/build-assets-docker
+++ b/scripts/build-assets-docker
@@ -5,12 +5,6 @@ set -euo pipefail
IMAGE_TAG="sour-emscripten:3.1.8"
PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
-build_image() {
- docker build -f "$PROJECT_ROOT/docker/emscripten.Dockerfile" -t "$IMAGE_TAG" "$PROJECT_ROOT"
-}
-
-build_image
-
HOST_UID="$(id -u)"
HOST_GID="$(id -g)"
diff --git a/scripts/build-docker-image b/scripts/build-docker-image
new file mode 100755
index 000000000..1d69d58e4
--- /dev/null
+++ b/scripts/build-docker-image
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+IMAGE_TAG="sour-emscripten:3.1.8"
+PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
+EM_CACHE_DIR="$PROJECT_ROOT/.emscripten-cache"
+
+build_image() {
+ docker build -f "$PROJECT_ROOT/docker/emscripten.Dockerfile" -t "$IMAGE_TAG" "$PROJECT_ROOT"
+}
+
+build_image
\ No newline at end of file
diff --git a/scripts/build-game-docker b/scripts/build-game-docker
index ceb71dc60..80ca480ac 100755
--- a/scripts/build-game-docker
+++ b/scripts/build-game-docker
@@ -4,16 +4,8 @@ set -euo pipefail
IMAGE_TAG="sour-emscripten:3.1.8"
PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
-SCRIPTS_DIR="$PROJECT_ROOT/scripts"
EM_CACHE_DIR="$PROJECT_ROOT/.emscripten-cache"
-build_image() {
- docker build -f "$PROJECT_ROOT/docker/emscripten.Dockerfile" -t "$IMAGE_TAG" "$PROJECT_ROOT"
-}
-
-build_image
-
-
# Allow overriding output dir
GAME_OUTPUT_DIR=${GAME_OUTPUT_DIR:-dist/game}
@@ -48,11 +40,4 @@ exec docker run --rm \
rm -rf /workspace/pkg/server/static/site
mkdir -p /workspace/pkg/server/static/site
cp -r dist/* /workspace/pkg/server/static/site/
-
- echo "[4/4] Building assets..."
'
-
-cd "$SCRIPTS_DIR"
-./build-assets-docker
-
-
diff --git a/scripts/clean-generated b/scripts/clean-generated
index 56da64ba1..0307813fc 100755
--- a/scripts/clean-generated
+++ b/scripts/clean-generated
@@ -8,8 +8,17 @@ rm -rf "$PROJECT_ROOT/game/dist/game" || true
rm -rf "$PROJECT_ROOT/client/dist" || true
rm -rf "$PROJECT_ROOT/pkg/server/static/site" || true
rm -rf "$PROJECT_ROOT/.emscripten-cache" || true
+rm -rf "$PROJECT_ROOT/.pip-cache" || true
+rm -rf "$PROJECT_ROOT/.home" || true
# Also remove common temporary outputs under assets
rm -rf "$PROJECT_ROOT/assets/dist" || true
+rm -rf "$PROJECT_ROOT/assets/output" || true
+rm -rf "$PROJECT_ROOT/assets/cache" || true
+rm -f "$PROJECT_ROOT/assets/.index.source" || true
+rm -f "$PROJECT_ROOT/assets/.index.json" || true
+
+# Remove auto-generated dev config
+rm -f "$PROJECT_ROOT/dev.auto.yaml" || true
echo "Cleaned generated outputs."
diff --git a/scripts/serve-docker b/scripts/serve-docker
index c3fb1849f..8bf8ca3e4 100755
--- a/scripts/serve-docker
+++ b/scripts/serve-docker
@@ -5,11 +5,6 @@ set -euo pipefail
IMAGE_TAG="sour-emscripten:3.1.8"
PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
-# Build the image if missing
-if ! docker image inspect "$IMAGE_TAG" >/dev/null 2>&1; then
- docker build -f "$PROJECT_ROOT/docker/emscripten.Dockerfile" -t "$IMAGE_TAG" "$PROJECT_ROOT"
-fi
-
HOST_UID="$(id -u)"
HOST_GID="$(id -g)"
@@ -26,6 +21,13 @@ if [ -f "$PROJECT_ROOT/assets/dist/.index.source" ]; then
(cd "$PROJECT_ROOT/assets" && ln -sf "dist/.index.source" ".index.source")
fi
+# Ensure the static site can serve required overlay assets (like downloading.png)
+SITE_DIR="$PROJECT_ROOT/pkg/server/static/site"
+if [ -d "$PROJECT_ROOT/assets/sour/packages" ]; then
+ mkdir -p "$SITE_DIR/packages"
+ cp -rf "$PROJECT_ROOT/assets/sour/packages/"* "$SITE_DIR/packages/" 2>/dev/null || true
+fi
+
CMD="go run ./cmd/sour serve"
if [ -n "$CONFIG_FILE" ]; then
# Map host path to container path
@@ -38,10 +40,29 @@ else
if [ -f "$PROJECT_ROOT/assets/.index.source" ]; then
AUTO_CFG="$PROJECT_ROOT/dev.auto.yaml"
cat > "$AUTO_CFG" </dev/null 2>&1 || true' INT TERM EXIT
+
+docker run \
--rm \
+ --init \
--user "$HOST_UID:$HOST_GID" \
--name sour-serve \
-e HOME="/workspace/.home" \
@@ -60,4 +92,7 @@ exec docker run \
-v "$PROJECT_ROOT":/workspace \
-w /workspace \
"$IMAGE_TAG" \
- bash -lc "$CMD"
+ bash -lc "$CMD" &
+
+PID=$!
+wait $PID
From 0bd0d58e923b1c6bafd58d16eaf9d23453c5a121 Mon Sep 17 00:00:00 2001
From: Jan Hangebrauck
Date: Fri, 29 Aug 2025 13:04:32 +0200
Subject: [PATCH 04/25] rename scripts
---
scripts/build-all | 10 ++++++----
scripts/{build-assets-docker => build-assets} | 0
scripts/{build-game-docker => build-game} | 0
scripts/{serve-docker => serve} | 0
4 files changed, 6 insertions(+), 4 deletions(-)
rename scripts/{build-assets-docker => build-assets} (100%)
rename scripts/{build-game-docker => build-game} (100%)
rename scripts/{serve-docker => serve} (100%)
diff --git a/scripts/build-all b/scripts/build-all
index c9ff1d16c..2c3e79d27 100755
--- a/scripts/build-all
+++ b/scripts/build-all
@@ -3,8 +3,10 @@
set -euo pipefail
cd "$(dirname "$0")/.."
-./scripts/build-docker-helper
-./scripts/build-assets-docker
-./scripts/build-game-docker
+pwd
-echo "Build complete. You can now run ./scripts/serve-docker to run the game server."
\ No newline at end of file
+./scripts/build-docker-image
+./scripts/build-assets
+./scripts/build-game
+
+echo "Build complete. You can now run ./scripts/serve to run the game server."
\ No newline at end of file
diff --git a/scripts/build-assets-docker b/scripts/build-assets
similarity index 100%
rename from scripts/build-assets-docker
rename to scripts/build-assets
diff --git a/scripts/build-game-docker b/scripts/build-game
similarity index 100%
rename from scripts/build-game-docker
rename to scripts/build-game
diff --git a/scripts/serve-docker b/scripts/serve
similarity index 100%
rename from scripts/serve-docker
rename to scripts/serve
From 5f4504a7bb635e767d5b78ec537388c3cae7ec40 Mon Sep 17 00:00:00 2001
From: Jan Hangebrauck
Date: Fri, 29 Aug 2025 23:12:13 +0200
Subject: [PATCH 05/25] build fully dockerized and in userspace unified cache
dir handling during build unified env var handling for build steps added menu
items for servers improved build structure
---
.gitignore | 6 +++-
assets/package.py | 4 +++
assets/sour/data/menus.cfg | 3 ++
client/src/index.tsx | 30 ++++++++++++-----
cmd/sour/serve.go | 3 ++
dev.auto.yaml | 69 +++++++++++++++++++++++++++++++++++---
docker/common.env | 6 ++++
docker/serve-entrypoint.sh | 35 +++++++++++++++++++
docker/serve.Dockerfile | 27 +++++++++++++++
scripts/build-all | 4 +++
scripts/build-assets | 7 ++--
scripts/build-game | 16 ++-------
scripts/build-proxy | 24 +++++++++++++
scripts/build-serve-image | 25 ++++++++++++++
scripts/build-web | 41 ++++++++++++++++++++++
scripts/clean-generated | 18 ++++++----
scripts/run-serve-image | 34 +++++++++++++++++++
scripts/serve | 39 +++------------------
18 files changed, 322 insertions(+), 69 deletions(-)
create mode 100644 docker/common.env
create mode 100644 docker/serve-entrypoint.sh
create mode 100644 docker/serve.Dockerfile
create mode 100755 scripts/build-proxy
create mode 100755 scripts/build-serve-image
create mode 100755 scripts/build-web
create mode 100755 scripts/run-serve-image
diff --git a/.gitignore b/.gitignore
index 819c28627..95c2cf93a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,8 @@ earthly/**
/sourdump
/sour-build
.home/
-.pip-cache/
\ No newline at end of file
+.pip-cache/
+.go-cache/
+.emscripten-cache/
+.gopath/
+bin/sour
\ No newline at end of file
diff --git a/assets/package.py b/assets/package.py
index 3193e8f9b..4b994a867 100644
--- a/assets/package.py
+++ b/assets/package.py
@@ -266,8 +266,12 @@ def run_sourdump(roots: List[str], args: List[str]) -> str:
root_args.append("-root")
root_args.append(root)
+ cache_dir = os.getenv('ASSET_CACHE_DIR', 'cache/')
+
args = [
"./sourdump",
+ "-cache",
+ cache_dir,
*root_args,
*args,
]
diff --git a/assets/sour/data/menus.cfg b/assets/sour/data/menus.cfg
index 3cbeda8d0..21fcaff2d 100644
--- a/assets/sour/data/menus.cfg
+++ b/assets/sour/data/menus.cfg
@@ -39,6 +39,9 @@ newgui about [
guitext [BananaBread written by Alon Zakai]
guitext [Continued by Caleb Foust]
guitext "https://github.com/cfoust/sour" 0
+ guitext [Continued and embedded into app.cg by Jan]
+ guitext "https://github.com/Kaesual/sour" 0
+ guitext "https://app.cg/c/videogames" 0
]
genmapitems = [
diff --git a/client/src/index.tsx b/client/src/index.tsx
index 03ce610b2..d2aa1ba19 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -375,13 +375,22 @@ function App() {
React.useEffect(() => {
if (state.type !== GameStateType.Ready) return
- const menu = `
- newgui discord [
- guibutton "copy authkey command.." [js "Module.discord.copyKey()"]
- //guibutton "regenerate auth key.." [js "Module.discord.regenKey()"]
- guibutton "log out.." [js "Module.discord.logout()"]
- ]
+ // To show the server browser, add:
+ // guibutton "server browser.." "showgui servers"
+ //
+ // Proxy setup is incomplete though at the moment, so
+ // connecting won't work
+
+ // Removed discord button for now
+ //
+ // newgui discord [
+ // guibutton "copy authkey command.." [js "Module.discord.copyKey()"]
+ // //guibutton "regenerate auth key.." [js "Module.discord.regenKey()"]
+ // guibutton "log out.." [js "Module.discord.logout()"]
+ // ]
+
+ const menu = `
newgui content [
guibutton "mods.." "showgui mods"
guibutton "put mods in url.." [js "Module.assets.modsToURL()"]
@@ -417,6 +426,10 @@ function App() {
guibar
] [
${CONFIG.menuOptions}
+ guibutton "join insta-dust2" "join insta-dust2"
+ guibutton "join ffa-dust2" "join ffa-dust2"
+ guibutton "join insta rotating maps" "join insta"
+ guibutton "join ffa rotating maps" "join lobby"
guibutton "create private game..." "creategame ffa"
]
guibutton "random map.." "map random"
@@ -816,17 +829,18 @@ function App() {
if (serverMessage.Op === MessageType.Info) {
const { Cluster, Master } = serverMessage
+ const combined = [...(Master || []), ...(Cluster || [])]
if (
BananaBread == null ||
BananaBread.execute == null ||
BananaBread.injectServer == null
) {
- cachedServers = Master
+ cachedServers = combined
return
}
- injectServers(Master)
+ injectServers(combined)
return
}
diff --git a/cmd/sour/serve.go b/cmd/sour/serve.go
index 2fe267113..91accaed7 100644
--- a/cmd/sour/serve.go
+++ b/cmd/sour/serve.go
@@ -220,6 +220,9 @@ func serveCommand(configs []string) error {
go cluster.PollUsers(ctx, newConnections)
go cluster.PollDuels(ctx)
+ // Start periodic server list watcher/broadcasts for the web server browser
+ wsIngress.StartWatcher(ctx)
+
// Encode the client config as json
clientConfig, err := json.Marshal(config.Client)
if err != nil {
diff --git a/dev.auto.yaml b/dev.auto.yaml
index c93434a2a..a94ab6855 100644
--- a/dev.auto.yaml
+++ b/dev.auto.yaml
@@ -1,26 +1,87 @@
+# This is Sour's default configuration.
server:
cacheDirectory: "./.cache/assets"
assets:
- "fs:assets/dist/.index.source"
presets:
+ - name: "ffa-duel"
+ virtual: true
+ config:
+ defaultMode: "ffa"
+ defaultMap: "turbine"
+ - name: "insta-duel"
+ virtual: true
+ config:
+ defaultMode: "insta"
+ defaultMap: "turbine"
- name: "default"
default: true
+ config:
+ defaultMode: "coop"
+ defaultMap: "xmwhub"
+ - name: "ffa"
config:
defaultMode: "ffa"
defaultMap: "complex"
- maps: ["complex", "dust2", "turbine"]
+ maps:
+ - "turbine"
+ - "complex"
+ - "dust2"
- name: "insta"
config:
defaultMode: "insta"
+ defaultMap: "complex"
+ maps:
+ - "turbine"
+ - "complex"
+ - "dust2"
+ - name: "insta-dust2"
+ config:
+ defaultMode: "insta"
+ defaultMap: "dust2"
+ maps:
+ - "dust2"
+ - name: "ffa-dust2"
+ config:
+ defaultMode: "ffa"
defaultMap: "dust2"
- maps: ["complex", "dust2", "turbine"]
+ maps:
+ - "dust2"
+ - name: "explore"
+ config:
+ matchLength: 180
+
+ ingress:
+ desktop:
+ - port: 28785
+ target: lobby
+ serverInfo:
+ enabled: false
+
+ matchmaking:
+ duel:
+ - name: "ffa"
+ preset: "ffa-duel"
+ forceRespawn: "dead"
+ default: true
+ - name: "insta"
+ preset: "insta-duel"
+ forceRespawn: "dead"
+
spaces:
- - preset: default
+ - preset: ffa
config:
alias: lobby
+ - preset: insta-dust2
+ config:
+ alias: insta-dust2
+ - preset: ffa-dust2
+ config:
+ alias: ffa-dust2
- preset: insta
config:
alias: insta
+
client:
assets:
- - "#origin/assets/0/.index.source"
+ - "#origin/assets/0/.index.source"
\ No newline at end of file
diff --git a/docker/common.env b/docker/common.env
new file mode 100644
index 000000000..2aaeb7c23
--- /dev/null
+++ b/docker/common.env
@@ -0,0 +1,6 @@
+HOME=/workspace/.home
+GOPATH=/workspace/.gopath
+GOCACHE=/workspace/.go-cache
+PIP_CACHE_DIR=/workspace/.pip-cache
+EM_CACHE=/workspace/.emscripten-cache
+
diff --git a/docker/serve-entrypoint.sh b/docker/serve-entrypoint.sh
new file mode 100644
index 000000000..244f6988f
--- /dev/null
+++ b/docker/serve-entrypoint.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+# Optional: dev config mounted at /workspace/dev.auto.yaml
+CONFIG_FILE=""
+if [ -f "/workspace/dev.auto.yaml" ]; then
+ CONFIG_FILE="dev.auto.yaml"
+fi
+
+# Start wsproxy (websocket -> UDP ENet proxy) if present
+if [ -x "/workspace/proxy/wsproxy" ]; then
+ # Run wsproxy to handle /service/proxy/u/
+ # We will front this via the Go server later; for now, bind on 1338
+ /workspace/proxy/wsproxy 1338 &
+ PROXY_PID=$!
+fi
+
+if [ -n "$CONFIG_FILE" ]; then
+ /workspace/bin/sour serve "$CONFIG_FILE" &
+else
+ /workspace/bin/sour serve &
+fi
+SERVER_PID=$?
+# Ensure the script stops and cleans up on SIGINT and SIGTERM
+
+cleanup() {
+ [[ -n "${PROXY_PID:-}" ]] && kill "${PROXY_PID}" >/dev/null 2>&1 || true
+ [[ -n "${SERVER_PID:-}" ]] && kill "${SERVER_PID}" >/dev/null 2>&1 || true
+ exit 0
+}
+
+trap cleanup INT TERM
+
+wait
diff --git a/docker/serve.Dockerfile b/docker/serve.Dockerfile
new file mode 100644
index 000000000..5623b7865
--- /dev/null
+++ b/docker/serve.Dockerfile
@@ -0,0 +1,27 @@
+FROM ubuntu:22.04
+
+WORKDIR /workspace
+
+RUN apt-get update && apt-get install -y \
+ zlib1g \
+ libenet7 \
+ && rm -rf /var/lib/apt/lists/*
+
+# Copy built artifacts from mounted context at build time
+# Expect caller to have run build-web, build-game, build-assets, build-proxy
+COPY pkg/server/static/site /workspace/pkg/server/static/site
+COPY assets/dist /workspace/assets/dist
+RUN ln -s /workspace/assets/dist/.index.source /workspace/assets/.index.source
+COPY bin/sour /workspace/bin/sour
+COPY proxy/wsproxy /workspace/proxy/wsproxy
+
+# Entrypoint script
+COPY docker/serve-entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+RUN chmod -R 777 /workspace
+
+EXPOSE 1337
+
+ENTRYPOINT ["/entrypoint.sh"]
+
+
diff --git a/scripts/build-all b/scripts/build-all
index 2c3e79d27..30e64f923 100755
--- a/scripts/build-all
+++ b/scripts/build-all
@@ -5,8 +5,12 @@ cd "$(dirname "$0")/.."
pwd
+./scripts/clean-generated
./scripts/build-docker-image
./scripts/build-assets
./scripts/build-game
+./scripts/build-proxy
+./scripts/build-web
+./scripts/build-serve-image
echo "Build complete. You can now run ./scripts/serve to run the game server."
\ No newline at end of file
diff --git a/scripts/build-assets b/scripts/build-assets
index 62941573a..b7caf35ca 100755
--- a/scripts/build-assets
+++ b/scripts/build-assets
@@ -8,15 +8,17 @@ PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
HOST_UID="$(id -u)"
HOST_GID="$(id -g)"
+# Cache for downloaded assets persists across runs
+ASSET_CACHE_DIR=${ASSET_CACHE_DIR:-cache}
ASSET_OUTPUT_DIR=${ASSET_OUTPUT_DIR:-output}
# Space-separated maps to build; use 'none' to skip maps
ASSET_MAPS=${ASSET_MAPS:-"complex dust2 turbine"}
exec docker run --rm \
--user "$HOST_UID:$HOST_GID" \
+ --env-file "$PROJECT_ROOT/docker/common.env" \
-e ASSET_OUTPUT_DIR="$ASSET_OUTPUT_DIR" \
- -e HOME="/workspace/.home" \
- -e PIP_CACHE_DIR="/workspace/.pip-cache" \
+ -e ASSET_CACHE_DIR="$ASSET_CACHE_DIR" \
-e ASSET_MAPS="$ASSET_MAPS" \
-v "$PROJECT_ROOT":/workspace \
-w /workspace/assets \
@@ -25,6 +27,7 @@ exec docker run --rm \
set -e
mkdir -p "$HOME" "$PIP_CACHE_DIR"
python3 -m pip install --user -r requirements.txt || true
+ mkdir -p "$ASSET_CACHE_DIR"
./setup
# Rebuild sourdump to ensure latest code is used
rm -f sourdump
diff --git a/scripts/build-game b/scripts/build-game
index 80ca480ac..65df2c259 100755
--- a/scripts/build-game
+++ b/scripts/build-game
@@ -16,8 +16,8 @@ mkdir -p "$EM_CACHE_DIR"
exec docker run --rm \
--user "$HOST_UID:$HOST_GID" \
+ --env-file "$PROJECT_ROOT/docker/common.env" \
-e GAME_OUTPUT_DIR="$GAME_OUTPUT_DIR" \
- -e EM_CACHE="/workspace/.emscripten-cache" \
-v "$PROJECT_ROOT":/workspace \
-w /workspace/game \
"$IMAGE_TAG" \
@@ -25,19 +25,7 @@ exec docker run --rm \
set -e
source /emsdk/emsdk_env.sh
- echo "[1/3] Building WASM game..."
+ echo "[1/1] Building WASM game..."
cd /workspace/game
./build
-
- echo "[2/3] Building web client..."
- cd /workspace/client
- yarn install
- yarn build
- cp src/index.html src/favicon.ico src/background.png dist/
- mkdir -p dist/game && cp -r ../game/dist/game/* dist/game/
-
- echo "[3/3] Staging site for Go server..."
- rm -rf /workspace/pkg/server/static/site
- mkdir -p /workspace/pkg/server/static/site
- cp -r dist/* /workspace/pkg/server/static/site/
'
diff --git a/scripts/build-proxy b/scripts/build-proxy
new file mode 100755
index 000000000..59a5902b7
--- /dev/null
+++ b/scripts/build-proxy
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+IMAGE_TAG="sour-emscripten:3.1.8"
+PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
+
+HOST_UID="$(id -u)"
+HOST_GID="$(id -g)"
+
+exec docker run --rm \
+ --user "$HOST_UID:$HOST_GID" \
+ --env-file "$PROJECT_ROOT/docker/common.env" \
+ -v "$PROJECT_ROOT":/workspace \
+ -w /workspace/proxy \
+ "$IMAGE_TAG" \
+ bash -lc '
+ set -e
+ make clean || true
+ make
+ echo "Built proxy: /workspace/proxy/wsproxy"
+ '
+
+
diff --git a/scripts/build-serve-image b/scripts/build-serve-image
new file mode 100755
index 000000000..79867a864
--- /dev/null
+++ b/scripts/build-serve-image
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+IMAGE_TAG="sour-emscripten:3.1.8"
+PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
+HOST_UID="$(id -u)"
+HOST_GID="$(id -g)"
+
+# Build sour binary
+docker run \
+ --rm \
+ --user "$HOST_UID:$HOST_GID" \
+ --env-file "$PROJECT_ROOT/docker/common.env" \
+ -v "$PROJECT_ROOT":/workspace \
+ -w /workspace \
+ "$IMAGE_TAG" \
+ bash -lc 'go build -o /workspace/bin/sour ./cmd/sour'
+
+# Build the serve image
+docker build -f "$PROJECT_ROOT/docker/serve.Dockerfile" -t sour-serve:local "$PROJECT_ROOT"
+
+echo "Built image sour-serve:local"
+
+
diff --git a/scripts/build-web b/scripts/build-web
new file mode 100755
index 000000000..89b02274f
--- /dev/null
+++ b/scripts/build-web
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+IMAGE_TAG="sour-emscripten:3.1.8"
+PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
+EM_CACHE_DIR="$PROJECT_ROOT/.emscripten-cache"
+
+# Allow overriding output dir
+GAME_OUTPUT_DIR=${GAME_OUTPUT_DIR:-dist/game}
+
+# Run as the host user to avoid root-owned files; mount a writable EM_CACHE
+HOST_UID="$(id -u)"
+HOST_GID="$(id -g)"
+mkdir -p "$EM_CACHE_DIR"
+
+exec docker run \
+ --rm \
+ --user "$HOST_UID:$HOST_GID" \
+ --env-file "$PROJECT_ROOT/docker/common.env" \
+ -e GAME_OUTPUT_DIR="$GAME_OUTPUT_DIR" \
+ -v "$PROJECT_ROOT":/workspace \
+ -w /workspace/game \
+ "$IMAGE_TAG" \
+ bash -lc '
+ set -e
+ source /emsdk/emsdk_env.sh
+
+ echo "[1/2] Building web client..."
+ cd /workspace/client
+ rm -rf dist
+ yarn install
+ yarn build
+ cp src/index.html src/favicon.ico src/background.png dist/
+ mkdir -p dist/game && cp -r ../game/dist/game/* dist/game/
+
+ echo "[2/2] Staging site for Go server..."
+ rm -rf /workspace/pkg/server/static/site
+ mkdir -p /workspace/pkg/server/static/site
+ cp -r dist/* /workspace/pkg/server/static/site/
+ '
diff --git a/scripts/clean-generated b/scripts/clean-generated
index 0307813fc..07645d2d2 100755
--- a/scripts/clean-generated
+++ b/scripts/clean-generated
@@ -7,18 +7,24 @@ PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
rm -rf "$PROJECT_ROOT/game/dist/game" || true
rm -rf "$PROJECT_ROOT/client/dist" || true
rm -rf "$PROJECT_ROOT/pkg/server/static/site" || true
-rm -rf "$PROJECT_ROOT/.emscripten-cache" || true
-rm -rf "$PROJECT_ROOT/.pip-cache" || true
-rm -rf "$PROJECT_ROOT/.home" || true
# Also remove common temporary outputs under assets
rm -rf "$PROJECT_ROOT/assets/dist" || true
rm -rf "$PROJECT_ROOT/assets/output" || true
-rm -rf "$PROJECT_ROOT/assets/cache" || true
rm -f "$PROJECT_ROOT/assets/.index.source" || true
rm -f "$PROJECT_ROOT/assets/.index.json" || true
-# Remove auto-generated dev config
-rm -f "$PROJECT_ROOT/dev.auto.yaml" || true
+# Cache for emscripten, pip, go, and gopath persists across runs, only remove if you want to
+# re-build the assets
+# rm -rf "$PROJECT_ROOT/.emscripten-cache" || true
+# rm -rf "$PROJECT_ROOT/.pip-cache" || true
+# rm -rf "$PROJECT_ROOT/.go-cache" || true
+# rm -rf "$PROJECT_ROOT/.gopath" || true
+# chmod -R +w "$PROJECT_ROOT/.home" || true # Allow removal of .home
+# rm -rf "$PROJECT_ROOT/.home" || true
+
+# Cache for downloaded assets persists across runs, only remove if you want to
+# re-download all assets
+# rm -rf "$PROJECT_ROOT/assets/cache" || true
echo "Cleaned generated outputs."
diff --git a/scripts/run-serve-image b/scripts/run-serve-image
new file mode 100755
index 000000000..d6356e6a9
--- /dev/null
+++ b/scripts/run-serve-image
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
+
+HOST_UID="$(id -u)"
+HOST_GID="$(id -g)"
+WEB_ADDR=${WEB_ADDR:-0.0.0.0}
+WEB_PORT=${WEB_PORT:-1337}
+
+# Optionally pass a config file (e.g., dev.auto.yaml)
+CONFIG_FILE=${1:-}
+
+handle_signal() {
+ echo "SIGINT received, stopping container..."
+ docker stop sour-serve
+ exit 0
+}
+trap handle_signal SIGINT
+trap handle_signal SIGTERM
+
+docker run \
+ --rm \
+ --init \
+ --name sour-serve \
+ --user "$HOST_UID:$HOST_GID" \
+ --env-file "$PROJECT_ROOT/docker/common.env" \
+ -p "$WEB_ADDR:$WEB_PORT:$WEB_PORT" \
+ -v "$PROJECT_ROOT/dev.auto.yaml":/workspace/dev.auto.yaml:ro \
+ sour-serve:local &
+
+PID=$!
+wait $PID
diff --git a/scripts/serve b/scripts/serve
index 8bf8ca3e4..4a544f84e 100755
--- a/scripts/serve
+++ b/scripts/serve
@@ -36,39 +36,10 @@ if [ -n "$CONFIG_FILE" ]; then
CMD="go run ./cmd/sour serve \"$REL\""
fi
else
- # Auto-generate a simple config pointing to local assets index if present
- if [ -f "$PROJECT_ROOT/assets/.index.source" ]; then
- AUTO_CFG="$PROJECT_ROOT/dev.auto.yaml"
- cat > "$AUTO_CFG" <
Date: Sat, 30 Aug 2025 00:00:06 +0200
Subject: [PATCH 06/25] podman compatibility
---
scripts/build-assets | 14 +++++++++++++-
scripts/build-docker-image | 14 +++++++++++++-
scripts/build-game | 14 +++++++++++++-
scripts/build-proxy | 14 +++++++++++++-
scripts/build-serve-image | 16 ++++++++++++++--
scripts/build-web | 14 +++++++++++++-
scripts/run-serve-image | 16 ++++++++++++++--
scripts/serve | 21 ++++++++++++++++++---
8 files changed, 111 insertions(+), 12 deletions(-)
diff --git a/scripts/build-assets b/scripts/build-assets
index b7caf35ca..72d7cb6ca 100755
--- a/scripts/build-assets
+++ b/scripts/build-assets
@@ -2,6 +2,18 @@
set -euo pipefail
+CONTAINER_ENGINE=${CONTAINER_ENGINE:-}
+if [ -z "${CONTAINER_ENGINE}" ]; then
+ if command -v docker >/dev/null 2>&1; then
+ CONTAINER_ENGINE=docker
+ elif command -v podman >/dev/null 2>&1; then
+ CONTAINER_ENGINE=podman
+ else
+ echo "Neither docker nor podman found. Please install one or set CONTAINER_ENGINE."
+ exit 1
+ fi
+fi
+
IMAGE_TAG="sour-emscripten:3.1.8"
PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
@@ -14,7 +26,7 @@ ASSET_OUTPUT_DIR=${ASSET_OUTPUT_DIR:-output}
# Space-separated maps to build; use 'none' to skip maps
ASSET_MAPS=${ASSET_MAPS:-"complex dust2 turbine"}
-exec docker run --rm \
+exec "$CONTAINER_ENGINE" run --rm \
--user "$HOST_UID:$HOST_GID" \
--env-file "$PROJECT_ROOT/docker/common.env" \
-e ASSET_OUTPUT_DIR="$ASSET_OUTPUT_DIR" \
diff --git a/scripts/build-docker-image b/scripts/build-docker-image
index 1d69d58e4..68536d80c 100755
--- a/scripts/build-docker-image
+++ b/scripts/build-docker-image
@@ -2,12 +2,24 @@
set -euo pipefail
+CONTAINER_ENGINE=${CONTAINER_ENGINE:-}
+if [ -z "${CONTAINER_ENGINE}" ]; then
+ if command -v docker >/dev/null 2>&1; then
+ CONTAINER_ENGINE=docker
+ elif command -v podman >/dev/null 2>&1; then
+ CONTAINER_ENGINE=podman
+ else
+ echo "Neither docker nor podman found. Please install one or set CONTAINER_ENGINE."
+ exit 1
+ fi
+fi
+
IMAGE_TAG="sour-emscripten:3.1.8"
PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
EM_CACHE_DIR="$PROJECT_ROOT/.emscripten-cache"
build_image() {
- docker build -f "$PROJECT_ROOT/docker/emscripten.Dockerfile" -t "$IMAGE_TAG" "$PROJECT_ROOT"
+ "$CONTAINER_ENGINE" build -f "$PROJECT_ROOT/docker/emscripten.Dockerfile" -t "$IMAGE_TAG" "$PROJECT_ROOT"
}
build_image
\ No newline at end of file
diff --git a/scripts/build-game b/scripts/build-game
index 65df2c259..9ea3ce10a 100755
--- a/scripts/build-game
+++ b/scripts/build-game
@@ -2,6 +2,18 @@
set -euo pipefail
+CONTAINER_ENGINE=${CONTAINER_ENGINE:-}
+if [ -z "${CONTAINER_ENGINE}" ]; then
+ if command -v docker >/dev/null 2>&1; then
+ CONTAINER_ENGINE=docker
+ elif command -v podman >/dev/null 2>&1; then
+ CONTAINER_ENGINE=podman
+ else
+ echo "Neither docker nor podman found. Please install one or set CONTAINER_ENGINE."
+ exit 1
+ fi
+fi
+
IMAGE_TAG="sour-emscripten:3.1.8"
PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
EM_CACHE_DIR="$PROJECT_ROOT/.emscripten-cache"
@@ -14,7 +26,7 @@ HOST_UID="$(id -u)"
HOST_GID="$(id -g)"
mkdir -p "$EM_CACHE_DIR"
-exec docker run --rm \
+exec "$CONTAINER_ENGINE" run --rm \
--user "$HOST_UID:$HOST_GID" \
--env-file "$PROJECT_ROOT/docker/common.env" \
-e GAME_OUTPUT_DIR="$GAME_OUTPUT_DIR" \
diff --git a/scripts/build-proxy b/scripts/build-proxy
index 59a5902b7..6faebf6cc 100755
--- a/scripts/build-proxy
+++ b/scripts/build-proxy
@@ -2,13 +2,25 @@
set -euo pipefail
+CONTAINER_ENGINE=${CONTAINER_ENGINE:-}
+if [ -z "${CONTAINER_ENGINE}" ]; then
+ if command -v docker >/dev/null 2>&1; then
+ CONTAINER_ENGINE=docker
+ elif command -v podman >/dev/null 2>&1; then
+ CONTAINER_ENGINE=podman
+ else
+ echo "Neither docker nor podman found. Please install one or set CONTAINER_ENGINE."
+ exit 1
+ fi
+fi
+
IMAGE_TAG="sour-emscripten:3.1.8"
PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
HOST_UID="$(id -u)"
HOST_GID="$(id -g)"
-exec docker run --rm \
+exec "$CONTAINER_ENGINE" run --rm \
--user "$HOST_UID:$HOST_GID" \
--env-file "$PROJECT_ROOT/docker/common.env" \
-v "$PROJECT_ROOT":/workspace \
diff --git a/scripts/build-serve-image b/scripts/build-serve-image
index 79867a864..9a7a90a40 100755
--- a/scripts/build-serve-image
+++ b/scripts/build-serve-image
@@ -2,13 +2,25 @@
set -euo pipefail
+CONTAINER_ENGINE=${CONTAINER_ENGINE:-}
+if [ -z "${CONTAINER_ENGINE}" ]; then
+ if command -v docker >/dev/null 2>&1; then
+ CONTAINER_ENGINE=docker
+ elif command -v podman >/dev/null 2>&1; then
+ CONTAINER_ENGINE=podman
+ else
+ echo "Neither docker nor podman found. Please install one or set CONTAINER_ENGINE."
+ exit 1
+ fi
+fi
+
IMAGE_TAG="sour-emscripten:3.1.8"
PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
HOST_UID="$(id -u)"
HOST_GID="$(id -g)"
# Build sour binary
-docker run \
+"$CONTAINER_ENGINE" run \
--rm \
--user "$HOST_UID:$HOST_GID" \
--env-file "$PROJECT_ROOT/docker/common.env" \
@@ -18,7 +30,7 @@ docker run \
bash -lc 'go build -o /workspace/bin/sour ./cmd/sour'
# Build the serve image
-docker build -f "$PROJECT_ROOT/docker/serve.Dockerfile" -t sour-serve:local "$PROJECT_ROOT"
+"$CONTAINER_ENGINE" build -f "$PROJECT_ROOT/docker/serve.Dockerfile" -t sour-serve:local "$PROJECT_ROOT"
echo "Built image sour-serve:local"
diff --git a/scripts/build-web b/scripts/build-web
index 89b02274f..307ef8c64 100755
--- a/scripts/build-web
+++ b/scripts/build-web
@@ -2,6 +2,18 @@
set -euo pipefail
+CONTAINER_ENGINE=${CONTAINER_ENGINE:-}
+if [ -z "${CONTAINER_ENGINE}" ]; then
+ if command -v docker >/dev/null 2>&1; then
+ CONTAINER_ENGINE=docker
+ elif command -v podman >/dev/null 2>&1; then
+ CONTAINER_ENGINE=podman
+ else
+ echo "Neither docker nor podman found. Please install one or set CONTAINER_ENGINE."
+ exit 1
+ fi
+fi
+
IMAGE_TAG="sour-emscripten:3.1.8"
PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
EM_CACHE_DIR="$PROJECT_ROOT/.emscripten-cache"
@@ -14,7 +26,7 @@ HOST_UID="$(id -u)"
HOST_GID="$(id -g)"
mkdir -p "$EM_CACHE_DIR"
-exec docker run \
+exec "$CONTAINER_ENGINE" run \
--rm \
--user "$HOST_UID:$HOST_GID" \
--env-file "$PROJECT_ROOT/docker/common.env" \
diff --git a/scripts/run-serve-image b/scripts/run-serve-image
index d6356e6a9..d4cc8f727 100755
--- a/scripts/run-serve-image
+++ b/scripts/run-serve-image
@@ -2,6 +2,18 @@
set -euo pipefail
+CONTAINER_ENGINE=${CONTAINER_ENGINE:-}
+if [ -z "${CONTAINER_ENGINE}" ]; then
+ if command -v docker >/dev/null 2>&1; then
+ CONTAINER_ENGINE=docker
+ elif command -v podman >/dev/null 2>&1; then
+ CONTAINER_ENGINE=podman
+ else
+ echo "Neither docker nor podman found. Please install one or set CONTAINER_ENGINE."
+ exit 1
+ fi
+fi
+
PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
HOST_UID="$(id -u)"
@@ -14,13 +26,13 @@ CONFIG_FILE=${1:-}
handle_signal() {
echo "SIGINT received, stopping container..."
- docker stop sour-serve
+ "$CONTAINER_ENGINE" stop sour-serve
exit 0
}
trap handle_signal SIGINT
trap handle_signal SIGTERM
-docker run \
+"$CONTAINER_ENGINE" run \
--rm \
--init \
--name sour-serve \
diff --git a/scripts/serve b/scripts/serve
index 4a544f84e..8cb190caa 100755
--- a/scripts/serve
+++ b/scripts/serve
@@ -2,6 +2,18 @@
set -euo pipefail
+CONTAINER_ENGINE=${CONTAINER_ENGINE:-}
+if [ -z "${CONTAINER_ENGINE}" ]; then
+ if command -v docker >/dev/null 2>&1; then
+ CONTAINER_ENGINE=docker
+ elif command -v podman >/dev/null 2>&1; then
+ CONTAINER_ENGINE=podman
+ else
+ echo "Neither docker nor podman found. Please install one or set CONTAINER_ENGINE."
+ exit 1
+ fi
+fi
+
IMAGE_TAG="sour-emscripten:3.1.8"
PROJECT_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
@@ -45,15 +57,18 @@ fi
handle_signal() {
echo "SIGINT received, stopping container..."
- docker stop sour-serve
+ "$CONTAINER_ENGINE" stop sour-serve
exit 0
}
trap handle_signal SIGINT
trap handle_signal SIGTERM
-trap 'docker rm -f sour-serve >/dev/null 2>&1 || true' INT TERM EXIT
+cleanup() {
+ "$CONTAINER_ENGINE" rm -f sour-serve >/dev/null 2>&1 || true
+}
+trap cleanup INT TERM EXIT
-docker run \
+"$CONTAINER_ENGINE" run \
--rm \
--init \
--user "$HOST_UID:$HOST_GID" \
From 8ffb3379594f2979be3fc7a6dd834805a9c9c50b Mon Sep 17 00:00:00 2001
From: Jan Hangebrauck
Date: Sat, 30 Aug 2025 09:58:25 +0200
Subject: [PATCH 07/25] fixed all urls for relative hosting
---
client/src/config.ts | 25 +++++++++++++++++++++++--
client/src/index.html | 12 ++++++++----
client/src/index.tsx | 18 +++++++++++-------
client/src/unsafe-startup.js | 11 +++++++----
docker/common.env | 3 +--
docker/serve.Dockerfile | 3 ++-
scripts/build-serve-image | 4 ++--
scripts/build-web | 6 +++---
scripts/run-serve-image | 1 -
9 files changed, 57 insertions(+), 26 deletions(-)
diff --git a/client/src/config.ts b/client/src/config.ts
index e2622dd2d..7904ba6d8 100644
--- a/client/src/config.ts
+++ b/client/src/config.ts
@@ -32,12 +32,33 @@ function fillHost(url: string): string {
function fillAssetHost(url: string): string {
const newHost = fillHost(url)
+ // Rebase assets to current path if pointing at same-origin /assets
+ // This makes assets work when the app is hosted under a subpath (e.g., /sour/)
+ const rebase = (absolute: string): string => {
+ try {
+ const u = new URL(absolute, window.location.href)
+ if (
+ u.origin === window.location.origin &&
+ u.pathname.startsWith('/assets/')
+ ) {
+ const baseAssetsPath = new URL('assets/', window.location.href).pathname
+ u.pathname = baseAssetsPath + u.pathname.slice('/assets/'.length)
+ return u.toString()
+ }
+ return u.toString()
+ } catch (_e) {
+ return absolute
+ }
+ }
+
+ const rebased = rebase(newHost)
+
// Don't cache asset sources pointing to this host
if (url.includes(REPLACED.HOST) || url.includes(REPLACED.ORIGIN)) {
- return `!${newHost}`
+ return `!${rebased}`
}
- return newHost
+ return rebased
}
function getInjected(): Maybe {
diff --git a/client/src/index.html b/client/src/index.html
index 90905a67d..589f695f9 100644
--- a/client/src/index.html
+++ b/client/src/index.html
@@ -33,7 +33,7 @@
{
WASM_PROMISE_RESOLVE = resolve;
})
+ // Override the locateFile function to use the game directory
+ Module.locateFile = function(path, prefix) {
+ return new URL('game/' + path, window.location.href).toString();
+ }
-
+
-
-
+
+