Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c59cf2b
potential fix for iframe keyboard events
Kaesual Aug 29, 2025
5bc8b96
wip: dockerize build pipeline and serve
Kaesual Aug 29, 2025
43f2b04
fix game serving
Kaesual Aug 29, 2025
0bd0d58
rename scripts
Kaesual Aug 29, 2025
5f4504a
build fully dockerized and in userspace
Kaesual Aug 29, 2025
4fbb85c
podman compatibility
Kaesual Aug 29, 2025
8ffb337
fixed all urls for relative hosting
Kaesual Aug 30, 2025
7035a24
update image and readme
Kaesual Aug 30, 2025
6d677df
update readme
Kaesual Aug 30, 2025
70a6467
update readme
Kaesual Aug 30, 2025
0aa34fd
update readme
Kaesual Aug 31, 2025
5f8013f
update readme
Kaesual Sep 3, 2025
2cd54ca
fix build consistency
Kaesual Sep 3, 2025
6fea922
ensure deterministic packet distribution in relay
Kaesual Sep 3, 2025
869658a
update build scripts
Kaesual Sep 4, 2025
4d01950
Merge branch 'main' into fix-desync
Kaesual Sep 4, 2025
6d2467a
fix container restart / init behavior
Kaesual Sep 5, 2025
955877e
allow position updates from editing players
Kaesual Sep 5, 2025
c5f8c3a
fix packet slicing mechanism for clients with nil packets (e.g. netwo…
Kaesual Sep 5, 2025
9493257
update readme
Kaesual Sep 6, 2025
affe0fd
update readme
Kaesual Sep 6, 2025
bab4d1c
Merge branch 'fix-desync'
Kaesual Sep 8, 2025
eb55a34
added server browser for integrated game servers
Kaesual Sep 10, 2025
d276b0d
unadjustedMovement patch to prevent mouse acceleration
Kaesual Sep 10, 2025
07dd34b
fix GetClientByCN
Kaesual Sep 10, 2025
904a96b
use unadjustedMovement if available
Kaesual Sep 10, 2025
53ac829
proxy ip filter added
Kaesual Sep 11, 2025
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ earthly/**
/sour
/sourdump
/sour-build
.home/
.pip-cache/
.go-cache/
.emscripten-cache/
.gopath/
bin/sour
127 changes: 95 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,114 @@
<p align="center">
<a href="https://sourga.me" target="_blank">
<img src="gh-assets/header.png" alt="Sour Cover Image">
</a>
<img src="gh-assets/sour-cg.png" alt="Sour on app.cg Cover Image">
</p>

<p align="center">
<a href="https://discord.gg/WP3EbYym4M"><img src="https://img.shields.io/discord/1071091858576523274?color=5865F2&label=discord&style=flat-square" alt="Discord" /></a>
<a href="https://github.com/cfoust/sour/releases"><img src="https://img.shields.io/github/downloads/cfoust/sour/latest/total?style=flat-square" alt="sour releases" /></a>
<a href="https://github.com/cfoust/sour/blob/main/LICENSE"><img src="https://img.shields.io/github/license/cfoust/sour?color=48AC75&style=flat-square" alt="sour License Badge MIT" /></a>
</p>
## What is Sour?

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 I'm planning to add additional features like Tournaments between communities - stay tuned! 🚀

Sour is a <a target="_blank" href="http://sauerbraten.org/">Cube 2: Sauerbraten</a> 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.

There's multiple deployments of this game available:

- The original version made and hosted by cfoust, available on [sourga.me](https://sourga.me)
- The in-community version on app.cg, in the [Common Games community](https://app.cg/c/commongames)
- A standalone version of the Common Ground version [here](https://embed.commonground.cg/sour/)

## The Common Games Collection

This repository is part of a broader effort to build a collection of Open Source games, which I call the [Common Games Collection](https://github.com/Kaesual/common-games-collection). Many such games exist, but are often cumbersome to build and thereby restricted to experts. I'm trying to build a unified collection, and make sure that all the games

## What is this?
- have a proper dockerized build pipeline (simple to run on any OS)
- can generate docker images ready for self hosting
- can easily be hosted on any path behind a reverse nginx proxy (for https support and structure)
- can be run in iframes, since this is required for my use case

Sour is a <a target="_blank" href="http://sauerbraten.org/">Cube 2: Sauerbraten</a> 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.
My idea is that as a collective, we can build a collection of great games where the community can *focus on modifying the games*, and knowledge about the (sometimes delicate) process of *converting a game to web assembly* can be shared, too. I mainly focus on polished multiplayer games at the moment.

<a target="_blank" href="https://sourga.me/">Give it a try.</a>
If you want to get in touch, all details are in the [Common Games Collection Repository](https://github.com/Kaesual/common-games-collection).

## Installation
## Updates in this fork

You can download an archive containing the Sour server and all necessary assets from [the releases page](https://github.com/cfoust/sour/releases). For now, only Linux and macOS are supported.
- The build is now fully dockerized, completely in userspace
- The build generates a ready-to-host docker image with all assets. The image is also available [here](https://hub.docker.com/r/janhan/sour), but it currently lacks the config file (see `scripts/serve-image` for how to do it).
- Fixed an issue that prevented keyboard events from being picked up when running in iframes
- Made hosting possible under a relative path when combined with a reverse nginx proxy + rewrite rule
- I added some more pre-set server types running on the web server for instant connection (insta + ffa dust2 & insta + ffa map rotation), which can be joined directly from the main menu

You can also install Sour via `brew`:
The docker build is only tested on linux, with docker, but I tried to make it podman compatible.

## Issues / What's missing

There's currently still desync issues in the built-in gameserver. Players become invisible to each other sometimes and stop being able to damage each other. This does not affect all player combinations, some players can still be damaged, seen by others, the combination seems random. I'm trying fix attempts on the `fix-desync` branch, but no success so far. I've added a lot of logging there too, which I will merge into the main branch at some point.

Also, the built in proxy server of the docker image (which is already compiled in the according step) is not yet set up for proper use, so it's not possible yet to connect to official servers with the code in this repository. I'll try to fix that soon, too. (tm)

## Local Development and Deployment

### 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
# Install the latest version:
brew install cfoust/taps/sour
# Ubuntu/Debian
sudo apt install git-lfs

# macOS (Homebrew)
brew install git-lfs

# Or a specific one:
brew install cfoust/taps/sour@0.2.2
# One‑time init, then pull LFS content
git lfs install
git lfs pull
```

In addition to all of the base game assets, these archives only contain three maps: `complex`, `dust2`, and `turbine`.
### Building the web game with Docker (Emscripten)

## Running Sour
A Dockerfile and helper script for building in userspace are provided. To avoid issues, make sure to check out the repository with the same user who will run the build. When running the build with docker, this user also needs to be in the docker group. For podman this is not necessary.

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.
```bash
# Build everything and put it into a nice new docker image, ready to host
./scripts/build
```

Running `sour` will start a Sour server accessible to web clients at `http://0.0.0.0:1337` and to desktop clients on port 28785. In other words, you should be able to connect to the Sour server in the Sauerbraten desktop client by running `/connect localhost`.
This uses an Ubuntu base with Emscripten 3.1.8 (same as CI), mounts your checkout at `/workspace`, and runs the build scripts in the container. It creates a new docker image called `sour-game:latest` by default. `build` is just a wrapper for the following build scripts:

```bash
scripts/build-builder-image # builds the builder docker image
scripts/build-assets
scripts/build-game
scripts/build-proxy
scripts/build-web
scripts/build-server
scripts/build-game-image
```

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.
All steps can be run independently, e.g. if you only updated the web interface, you can run `scripts/build web image` to update the image. Some assets are downloaded and cached during the first asset build, so it takes longer the first time. After that, building assets runs quite fast.

## Configuration
### Running the server in Docker

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`:
After building, you can run the integrated server locally with:

```bash
sour serve config.yaml
# Default: serves on 0.0.0.0:1337
./scripts/serve-image

# Override bind address/port
WEB_ADDR=127.0.0.1 WEB_PORT=1337 ./scripts/serve-image
```

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. There's also the older `scripts/serve` that I used before the docker images, should still work if you don't want to re-build the container every time, but must be restarted after making updates.

## Configuration

Sour is highly configurable. When run without arguments, `sour` defaults to running `sour serve` with the [default Sour configuration](https://github.com/Kaesual/sour/blob/main/pkg/config/default.yaml). By default, the `serve-image` script mounts the `dev.auto.yaml` from this repository folder. You can make changes there or mount your own config file.

Sour can be configured using `.yaml` or `.json` files; the structure is the same in both cases.

Warning: The section below is from the original readme and hasn't been updated yet, it will probably need a docker command instead as the docker image is where the system requirements are installed.

To print the default configuration to standard output, run `sour config`:

```bash
Expand All @@ -70,6 +129,8 @@ These configurations are merged from left to right using [CUE](https://cuelang.o
- **Preserve the experience of playing the original game.** While it is possible that Sour may someday support arbitrary game modes, assets, clients, and server code, the vanilla game experience should still be available.
- **Be the best example of a cross-platform, open-source FPS.** Deployment of Sour on your own infrastructure with whatever configuration you like should be easy. Every aspect of Sour should be configurable.

Note: The goals above are originally from cfoust. My own long-term goal to add would be embedding Sour into app.cg in a way that there's cross-community gaming activity and ranked / tournaments. And potentially, community governed report / review systems.

## Architecture

Here is a high level description of the repository's contents:
Expand All @@ -80,16 +141,18 @@ 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.

## Contributing
- `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.

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.
Above is the original architecture section by cfoust. This repository additionally has:

## Inspiration
- `scripts/`: a simple, dockerized build pipeline for the game client, assets, server etc.
- generates a docker container ready for hosting
- uses one docker helper container that compiles everything and can also serve the game server
- a bugfix for keyboard events to work in iframes

Some years ago I came across [BananaBread](https://github.com/kripken/BananaBread), which was a basic tech demo that used [Emscripten](https://emscripten.org/) to compile Sauerbraten for the web. The project was limited in scope and done at a time when bandwidth was a lot more precious.

## License

Original text from cfoust

Each project that was forked into this repository has its own original license intact, though the glue code and subsequent modifications I have made are licensed according to the MIT license specified in `LICENSE`.
2 changes: 2 additions & 0 deletions assets/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ quadropolis
*.tar.gz
cache/**
/dist
.index.source
.index.json
10 changes: 5 additions & 5 deletions assets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
```

Expand Down
10 changes: 5 additions & 5 deletions assets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions assets/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]
Expand Down
2 changes: 1 addition & 1 deletion assets/setup
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions assets/sour/data/menus.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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/commongames" 0
]

genmapitems = [
Expand Down
25 changes: 23 additions & 2 deletions client/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Configuration> {
Expand Down
12 changes: 8 additions & 4 deletions client/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

<body
style="
background: #393e3f repeat url('/background.png');
background: #393e3f repeat url('background.png');
background-size: 400px;
overflow: hidden;
"
Expand All @@ -52,10 +52,14 @@
var WASM_PROMISE = new Promise((resolve, reject) => {
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();
}
</script>
<script src="/index.js"></script>
<script src="index.js"></script>

<script async src="/game/api.js"></script>
<script async type="text/javascript" src="/game/sauerbraten.js"></script>
<script async src="game/api.js"></script>
<script async type="text/javascript" src="game/sauerbraten.js"></script>
</body>
</html>
Loading