Skip to content

Shilo/multi-server-test

Repository files navigation

Minimal Godot 4 Three-Role Multiplayer Spike

This is a one-project Godot 4.6 spike proving a small online-world topology with:

  • One client role.
  • One master server role that stays online and owns world lifecycle.
  • One world server role, started on demand once per active world key.
  • Master-owned SQLite persistence (MVP): guests, name-only login, and saved world/position. See VirtuCade Database MVP.
  • Chat hosted by the master server on the same MasterNet socket, showing display names for guests and logged-in players.
  • Local-first networking constants for client-advertised host/ports.
  • Per-launch world registration tokens generated by master.
  • A shared build/version gate so mismatched clients and world servers are rejected before login, route loading, or world travel.
  • Native Godot high-level multiplayer over WebSocketMultiplayerPeer.
  • Separate client multiplayer contexts for master/control/chat and the active world.
  • A persistent master/chat connection while the active world connection is replaced.
  • Server-authority player spawn/despawn with client-authority movement.
  • Four visibly distinct worlds:
    • hub
    • left_world
    • right_world
    • top_world

Portal topology:

  • hub -> left_world
  • hub -> right_world
  • hub -> top_world
  • left_world -> hub
  • left_world -> right_world
  • left_world -> top_world
  • right_world -> hub
  • right_world -> left_world
  • right_world -> top_world
  • top_world -> hub
  • top_world -> left_world
  • top_world -> right_world

For the full walkthrough, read Godot Multi-Server Architecture Guide.

Structure

  • addons/godot-sqlite/: vendored 2shady4u/godot-sqlite GDExtension (master-only SQLite).
  • shared/main/: feature-tag bootstrap scene.
  • client/: playable client root and UI, including client/login/ (bottom-right login widget).
  • server/master/: master server scene, script, and world process manager. Hosts MasterNet.
  • server/master/db/: master-owned DatabaseService and repositories (the only SQLite access boundary).
  • server/world/: world server scene and script.
  • server/worlds/: playable world source folders. These are included in server exports and can be packed separately for client download.
  • addons/pack_rat/: PackRat runtime DLC/PCK loader subtree used by clients before world travel.
  • shared/net/: endpoint scripts and minimal derived network config.
  • shared/world/: reusable world base scene and portal logic.
  • shared/player/: replicated player scene and script.
  • tools/: export and smoke-test scripts.
  • docs/: architecture, research, and audit notes.
  • editor/run_instance_grid.gd: editor-only autoload that tiles visible Run Instances windows for manual debugging. Export scripts remove this autoload during export so /editor/* never ships in runtime builds.

Main documentation:

Historical research, including the Nakama branch notes, remains under docs/ but is not the canonical current architecture.

PackRat Subtree

PackRat is tracked as a Git subtree at addons/pack_rat/. The subtree source is the PackRat repository's addon branch, not main. That branch contains only the contents that should live directly under res://addons/pack_rat/; pulling from PackRat main would nest the full project inside this addon folder.

Update PackRat with:

git subtree pull --prefix=addons/pack_rat https://github.com/Shilo/pack-rat.git addon --squash -m "chore: update PackRat subtree"

Do not edit addons/pack_rat/ directly in this project unless the change is an urgent local integration fix. Prefer changing Shilo/pack-rat, let its workflow sync the addon branch, then pull the subtree here. The current subtree was imported from PackRat addon commit 505a656, which was synced from PackRat main commit 37e0f54 and tag v1.0.2.

Role Selection

The exported client uses the default client main scene. The exported server uses one server feature tag and command-line user args:

  • server or dedicated_server with no user args: starts res://server/master/master.tscn
  • server or dedicated_server with a world key after --: starts res://server/world/world.tscn
  • no server feature tag: starts res://client/client.tscn

The main scene is:

res://shared/main/main.tscn

For smoke tests and CI with the editor binary, launch the master and client scenes directly with Godot's built-in --scene option. The master starts world scenes itself. Exported smoke runs one standalone server executable; that server creates additional instances of itself for worlds.

Build Version Gate

The runtime compatibility version is the Godot project setting:

application/config/version="0.1"

Client, master, and world server processes all read that same setting with ProjectSettings.get_setting("application/config/version"). The login panel shows the value as v<version> so testers can see which client build is running.

On startup:

  1. Client connects to the master.
  2. Client calls request_routes(<application/config/version>).
  3. Master rejects the route request if the version differs.
  4. Web clients show a "Game Updated" prompt with a reload button that appends ?v=<server_version> for cache busting.
  5. World servers also include application/config/version when registering with the master, so a stale world process cannot join a newer cluster.

Web exports are patched after Godot finishes exporting so index.html, index.js, index.wasm, and index.pck are requested with the same build query string. That keeps the GitHub Pages/browser cache behavior explicit without adding service workers or custom CDN headers.

This is a compatibility gate, not real authentication. Public VirtuCade builds still need authenticated sessions before account/resume security is production ready.

World Keys

World selection still uses a bare positional key after Godot's --. Master-owned launches append a private launch token after that key so the world can register with master.

Examples:

& $godot --headless --path . --scene res://server/world/world.tscn -- hub
& $godot --headless --path . --scene res://server/world/world.tscn -- left_world
& $godot --headless --path . --scene res://server/world/world.tscn -- right_world
& $godot --headless --path . --scene res://server/world/world.tscn -- top_world

These direct commands are useful for isolated world-scene debugging, but they do not register with master because they do not have a master-issued launch token. A world key is required. If the key is missing or unknown, startup fails clearly.

Manual CLI Run

Use the local Godot binary:

$godot = "C:\Programming_Files\Godot\Godot_v4.6.3-stable_win64.exe\Godot_v4.6.3-stable_win64.exe"

Start the master in one terminal:

& $godot --headless --path . --scene res://server/master/master.tscn

Launch a manual client:

& $godot --path . --scene res://client/client.tscn

Manual client mode requires only the master at startup. The master starts the initial world when the client asks for routes, then starts transfer targets on demand.

In editor/manual runs, PackRat uses local editor-exported packs automatically. This keeps local testing fresh and fast while still exercising the same cache/mount path used by exported clients. The client prints WORLD_PACK_EDITOR_EXPORT, WORLD_PACK_PROGRESS, and WORLD_PACK_READY while it builds or reuses those editor packs.

To force the HTTP/static download path locally instead, build and serve world packs, start the master from a shell with the matching pack URL/path, then pass the client arg:

powershell -ExecutionPolicy Bypass -File tools\export_world_packs.ps1
powershell -ExecutionPolicy Bypass -File tools\start_world_pack_server.ps1
$env:MULTI_SERVER_WORLD_PACK_BASE_URL="http://127.0.0.1:19100/world_packs"
$env:MULTI_SERVER_WORLD_PACK_DIR=(Resolve-Path builds\world_packs)
& $godot --path . --scene res://client/client.tscn -- force_packrat_world_packs

That forced path should print WORLD_PACK_START, WORLD_PACK_PROGRESS, and WORLD_PACK_READY. If the local static server is not running, the client is waiting on a dead http://127.0.0.1:19100/world_packs/... URL and will print WORLD_PACK_FAILED when the PackRat request times out/fails.

Editor mode uses the World Pack - <world_key> export preset for each world. Those presets are the universal pack presets: they enable both desktop (s3tc/bptc) and mobile (etc2/astc) texture variants so the same exported PCK can be used by Windows, Web, macOS, Linux, iOS, and Android clients. PackRat then copies the generated pack through its normal cache/mount path and simulates one second of local load progress for uncached packs.

Editor Run Instances

Use Godot's editor launcher when you want visible local clients and visible local servers.

Open:

Debug > Customize Run Instances...

Recommended setup for two visible clients:

  • Main editor run: visible client with no launch arguments.
  • Extra instance 1: visible client with no launch arguments.
  • Extra instance 2: master using the server feature tag and no user args.

Stop the previous run before starting another one so old processes do not keep ports 19080 through 19084 bound. World instances are spawned by the master and inherit the master's display mode: visible masters spawn visible worlds, while headless masters spawn headless worlds. Empty worlds should shut down automatically after they are empty.

Automated Smoke Test

Editor/headless smoke:

powershell -ExecutionPolicy Bypass -File tools\run_smoke.ps1

Two simultaneous editor/headless clients:

powershell -ExecutionPolicy Bypass -File tools\run_smoke.ps1 -ClientCount 2

PackRat world-pack smoke:

powershell -ExecutionPolicy Bypass -File tools\run_smoke.ps1 -UsePackRatWorldPacks -TimeoutSeconds 90

This exports one temporary local PCK per server/worlds/<world_key>/ folder using the matching World Pack - <world_key> export preset into .logs/smoke/pack_server/world_packs/, serves those packs over http://127.0.0.1:19100/, and forces the editor client to download and mount them through PackRat before loading each world scene. It is the current automated proof that world travel works without relying on scenes already bundled into the client.

After running tools\export_all.ps1, exported PackRat smoke can run the same download path through the exported client and server executables:

powershell -ExecutionPolicy Bypass -File tools\run_smoke.ps1 -UseExported -UsePackRatWorldPacks -TimeoutSeconds 120

SQLite persistence smoke can also run through the exported server/client. This is the proof that the server export still has the SQLite GDExtension packaged while the client export remains free of server-only SQLite sidecars:

powershell -ExecutionPolicy Bypass -File tools\run_db_test.ps1 -UseExported -TimeoutSeconds 180

PackRat editor-export smoke:

powershell -ExecutionPolicy Bypass -File tools\run_smoke.ps1 -UsePackRatEditorExports -TimeoutSeconds 120

This does not start the local HTTP pack server. Instead, the client sets PackRatOptions.editor_pack_export_preset to World Pack - <world_key> and PackRatOptions.editor_simulated_local_load_seconds to 1.0, so PackRat builds fresh editor packs only when the watched world resources or export presets change. This is the normal editor behavior; the smoke requires WORLD_PACK_EDITOR_EXPORT and WORLD_PACK_READY.

Version gate smoke:

powershell -ExecutionPolicy Bypass -File tools\run_version_gate_smoke.ps1

This temporarily launches the master with one project version and the client with another. It passes only if the client is rejected before any world process starts, then restores project.godot.

Project version unit smoke:

powershell -ExecutionPolicy Bypass -File tools\run_project_version_test.ps1

This validates strict MAJOR.MINOR parsing, minor rollover, and the local PowerShell compatibility wrapper. It backs up and restores project.godot.

PackRat version/cache smoke:

powershell -ExecutionPolicy Bypass -File tools\run_packrat_version_cache_smoke.ps1

This exports universal world packs once, downloads them at one app version, bumps only application/config/version, then confirms the next run keeps using PackRat cache hits for the unchanged pack metadata.

Successful full editor smoke logs include:

  • MASTER_READY
  • MASTER_WORLD_STARTED key=hub
  • MASTER_WORLD_REGISTERED key=hub
  • MASTER_WORLD_STOP_REQUESTED key=hub reason=idle
  • MASTER_WORLD_STOPPED key=hub
  • MASTER_WORLD_STARTED key=left_world
  • MASTER_WORLD_REGISTERED key=left_world
  • MASTER_WORLD_STOP_REQUESTED key=left_world reason=idle
  • MASTER_WORLD_STOPPED key=left_world
  • MASTER_WORLD_STARTED key=right_world
  • MASTER_WORLD_REGISTERED key=right_world
  • MASTER_WORLD_STOP_REQUESTED key=right_world reason=idle
  • MASTER_WORLD_STOPPED key=right_world
  • MASTER_WORLD_STARTED key=top_world
  • MASTER_WORLD_REGISTERED key=top_world
  • MASTER_WORLD_STOP_REQUESTED key=top_world reason=idle
  • MASTER_WORLD_STOPPED key=top_world
  • SMOKE_PROCESS_GONE hub_after_master_kill
  • SMOKE_PASS

PackRat smoke also includes client-side WORLD_PACK_READY lines. Editor-export PackRat smoke also includes WORLD_PACK_EDITOR_EXPORT.

Logs are written under .logs/ and ignored by git.

Web export smoke:

powershell -ExecutionPolicy Bypass -File tools\run_web_smoke.ps1

This exports the Windows client/server, exports the Web client, builds world PCKs, verifies the exported PCK file tables, starts a local static host for builds/web/, starts the exported master server, and drives the exported Web client through Edge/Playwright. It proves the browser client downloads world_packs/*.pck before joining worlds, then reuses PackRat cache on repeated world visits.

If artifacts are already exported and verified, run only the Web smoke:

powershell -ExecutionPolicy Bypass -File tools\run_web_smoke.ps1 -SkipExport

Export

Install Godot export templates for 4.6.3.stable first if needed.

Export the client and standalone server artifacts:

powershell -ExecutionPolicy Bypass -File tools\export_all.ps1

Debug exports are the default because the smoke tests are diagnostic. To build release-shaped artifacts:

powershell -ExecutionPolicy Bypass -File tools\export_all.ps1 -Release

Inspect or update the project version:

python tools/project_version.py --print
python tools/project_version.py --set 1.4
python tools/project_version.py --bump-minor

The version format is MAJOR.MINOR; minor rolls from 9 to the next major (0.9 -> 1.0). Local exports do not bump the version. They use the committed application/config/version.

Local PowerShell export outputs:

  • builds/client/client.exe
  • builds/client/client.pck
  • builds/server/server.exe
  • builds/web/index.html
  • builds/web/index.js
  • builds/web/index.pck
  • builds/web/index.wasm
  • builds/world_packs/*.pck as the universal source-of-truth world packs
  • builds/web/world_packs/*.pck as byte-for-byte mirrored copies for Web/static hosting

After exporting, verify that client artifacts do not bundle server/world scenes and that each platform world pack contains the expected isolated world scene:

python tools\verify_export_artifacts.py

The GitHub release workflow verifies Linux/Web release artifacts with:

python tools/verify_export_artifacts.py --server-binary server/server.x86_64

The deployed Web site also includes a generated manifest for release auditing:

https://shilo.github.io/multi-server-test/deployment_manifest.json

That manifest lists deployed Web files and world PCK files with file sizes, SHA-256 fingerprints, the project version, the release commit, the workflow trigger commit, and the workflow run id. GitHub Pages does not provide an FTP-style file browser for Actions deployments, so this manifest is the lightweight way to check exactly what was published.

World packs exported through Godot's --export-pack are not literal raw copies of only server/worlds/<world_key>/. They include the world scene remap, the converted .godot/exported/... scene, and small Godot-generated metadata such as project.binary, UID cache, global script class cache, and icon.svg. The verification tools treat only those generated entries as allowed and fail if a world pack contains editor files, client files, another world folder, or any other unexpected source directory.

The server executable contains the master server, world server, and all discovered world scenes. Starting it with no user args runs the master. The master starts one additional process per active world key by creating another instance of the same executable and passing the world key plus a private launch token. The builds/world_packs/*.pck files are the universal client-downloadable DLC artifacts. builds/web/world_packs/*.pck is only a hosted mirror of those same files so the Web export can serve them beside index.html.

For CDN/static hosting, deploy the contents of builds/web/ as the Web site. Keep builds/web/world_packs/*.pck beside index.html under the world_packs/ path, or upload that folder to a dedicated static file host. The master/VPS should advertise that same public pack base:

MULTI_SERVER_WORLD_PACK_BASE_URL=https://<owner>.github.io/<repo>/world_packs
MULTI_SERVER_WORLD_PACK_DIR=<server filesystem path to the exact hosted Web world_packs mirror>

The master advertises stable pack URLs without a version query. PackRat appends ?v=<application/config/version> to outbound HTTP requests when the URL does not already include v. PackRat still decides whether to reuse or redownload a world pack from the server-provided pack_modified_time and pack_size, so app version bumps do not redownload unchanged packs.

Current GitHub Pages deployment is handled by the manual GitHub Actions workflow. The repository Pages setting should be Build and deployment -> Source: GitHub Actions. The workflow exports the Web client, exports one universal world-pack set, mirrors those PCKs into builds/web/world_packs/, verifies that runtime builds do not include the wrong folders, uploads builds/web/ as a Pages artifact, and deploys that artifact with actions/deploy-pages. The deployed page is:

https://shilo.github.io/multi-server-test/

Do not use Deploy from a branch or a generated gh-pages branch for this project. Branch deployment is useful for simple static files, but this project must atomically build and publish the Web client, Web PCK files, Linux server artifact, version bump, and release tag from one manual release workflow. Generated Web/PCK binaries should stay out of git history.

GitHub Pages does not provide an FTP-style file browser for Actions deployments. Use these release records instead:

  • Git history and tags for source/version history, for example v0.6.
  • The workflow run artifacts for generated release outputs.
  • The live deployment manifest for published Web/PCK files:
https://shilo.github.io/multi-server-test/deployment_manifest.json

For this GitHub Pages test, gameplay still runs locally. Start the master after exporting/deploying so it reads metadata from the same Web PCK files that GitHub Pages serves:

$env:MULTI_SERVER_WORLD_PACK_BASE_URL="https://shilo.github.io/multi-server-test/world_packs"
$env:MULTI_SERVER_WORLD_PACK_DIR=(Resolve-Path builds\web\world_packs)
& $godot --headless --path . --scene res://server/master/master.tscn

For this test, the Web client still points at 127.0.0.1 / ws by default, so the GitHub Pages browser client connects to the local gameplay server while downloading Web client/PCK files from GitHub Pages. Public server testing can override that without rebuilding by appending ?master_url=wss://<host>/&world_url_template=wss://<host>/{world_key} to the page URL.

For setting up a fresh DigitalOcean/Ubuntu VPS for GitHub Actions deploys, see DigitalOcean VPS Setup.

GitHub Actions uses manual workflow dispatch only. One run sets an exact MAJOR.MINOR version or bumps the minor version once, creates a local release commit for the visible project.godot change, exports Linux server and Web artifacts from that release commit, verifies them, runs the exported Web smoke, pushes the release commit, uploads the Linux server artifact plus world packs, deploys the Web client and all Web world packs to GitHub Pages, verifies the live hosted bytes against the deployment manifest, stages the new Linux server and world-pack mirror on the configured VPS, stops the VPS server, swaps in the new files, starts the VPS server, then tags the release commit. If a release tag already exists, it must point at the current commit or the workflow fails before publishing. Releases are intentionally restricted to the main branch.

VPS deploy uses these GitHub Actions repository secrets:

  • VIRTUCADE_HOST: VPS public host or IP.
  • VIRTUCADE_USER: restricted deploy user, currently github-deploy.
  • VIRTUCADE_SSH_KEY: private SSH key for that deploy user.
  • VIRTUCADE_KNOWN_HOSTS: pinned SSH host key line for the VPS.

Optional reverse-proxy deploy uses these GitHub Actions variables or secrets:

  • VIRTUCADE_GAME_HOST: public gameplay DNS name, for example game.example.com.
  • VIRTUCADE_ACME_EMAIL: optional ACME contact email for Caddy.

The VPS service is virtucade.service. The workflow uploads the full builds/server/ Linux export folder and builds/world_packs/*.pck to staging folders first, preserving PCK modified times. Only after staging succeeds does it stop the service, swap the staged files into /opt/virtucade/server/ and /opt/virtucade/world_packs/, start the service, and check that it is active. The github-deploy user should only have write access to /opt/virtucade and limited passwordless sudo for virtucade.service, caddy.service, Caddy validation, and installing /etc/caddy/Caddyfile. When VIRTUCADE_GAME_HOST is set, the workflow also renders a static Caddyfile, validates it on the VPS, installs it to /etc/caddy/Caddyfile when changed, and reloads caddy.service after the Godot backend is healthy. The workflow writes /opt/virtucade/virtucade.env on every deploy so stale public URL settings cannot survive between releases.

The workflow title shows Release v<version> when an exact version input is provided. Auto-bump runs are titled Release auto-bump because GitHub computes workflow run names before steps can read and bump project.godot; the computed version is still written to logs, the release commit, the tag, and the artifact name.

Release deploys intentionally publish the Web client and every world pack together. That keeps the client, PackRat PCK metadata, and server artifact on one version without partial-deploy ambiguity. The release tag is created after the hosted Pages verification step, so the tag means the release commit was built, smoked, uploaded as a server artifact, deployed to Pages, and checked against the hosted manifest.

If a release run fails after the workflow has committed the version but before the final release tag is created, rerun the workflow with the exact failed version in the version input. Do not use the empty auto-bump path for that retry, or it will intentionally create the next release version.

The release workflow patches the generated Web shell with tools/patch_web_cache_bust.py so the project version is applied to Godot's generated index.js, index.wasm, and index.pck requests. The local PowerShell export script still performs the same patch for Windows/editor diagnostics.

For local Web smoke, the script sets the base URL to http://127.0.0.1:19200/world_packs, matching the temporary static server. The master reads pack size and modified time from MULTI_SERVER_WORLD_PACK_DIR; the static host must serve matching bytes. The release workflow also downloads every live GitHub Pages file listed in deployment_manifest.json after deploy and verifies its size/SHA-256 value.

GitHub Pages may rewrite Last-Modified headers to the deployment time. The release workflow treats those mismatches as warnings, then syncs the VPS /opt/virtucade/world_packs/ mirror mtimes to the actual hosted Last-Modified headers before restarting the server. That way the master sends PackRat metadata that matches what clients will see from GitHub Pages.

The server/ and client/ folders are export-bundle ownership labels. The concern ends at what gets bundled into each executable/artifact; runtime clients can still mount a downloaded pack at a res://server/worlds/... path because that path is not user-facing.

The export script uses three executable/Web presets:

  • Windows Client: no role feature tag.
  • Windows Server: dedicated server export with the server feature tag.
  • Web Client: browser client export. It must not include server/worlds/*; worlds are downloaded as separate PackRat PCKs.

PackRat editor-export testing uses four additional Windows Desktop PCK presets:

  • World Pack - hub
  • World Pack - left_world
  • World Pack - right_world
  • World Pack - top_world

These same World Pack - <world_key> presets are also the production universal pack presets. Each one has both texture_format/s3tc_bptc=true and texture_format/etc2_astc=true, while the project keeps rendering/textures/vram_compression/import_s3tc_bptc=true and rendering/textures/vram_compression/import_etc2_astc=true enabled so imported textures actually generate both variants. Smoke/CI launches master and client scenes directly when testing from the editor binary. Editor runs use PackRat editor exports by default. -UsePackRatWorldPacks exercises HTTP downloads from a temporary local static server under .logs/smoke/pack_server/, while -UsePackRatEditorExports explicitly asserts the default PackRat editor export flow. Production and smoke world packs are exported through Godot's --export-pack CLI once, then mirrored into builds/web/world_packs/ for Web hosting.

tools\export_world_packs.ps1 fails early when a discovered world folder is missing the matching export preset for the selected prefix. This keeps new VirtuCade-style mini-games from silently missing their downloadable pack.

Network Constants

This MVP keeps local bind ports and default advertised client URLs in shared/net/net_config.gd. Playable worlds live at server/worlds/<world_key>/<world_key>.tscn, discovered by folder convention. Ports are derived from the sorted world keys. The master and world server processes communicate on 127.0.0.1; clients dial the advertised public host/scheme resolved from deployment overrides first, then the local defaults.

World registration uses per-launch tokens generated by server/master/world_process_manager.gd, not a shared secret stored in client-exported scripts. Client travel uses two credentials: a refreshable TravelLease while the client prepares assets, then a short-lived one-use join ticket after the lease is redeemed. The master can advertise world routes and pack metadata without starting the world process; the world process starts only after PackRat is finished and the client redeems the TravelLease. Source-world transfer locks are keyed to the exact lease and clear only when that lease fails or the target world confirms the join ticket was consumed.

When a world scene is not bundled, the master attaches flat PackRat metadata to the route endpoint:

pack_url
pack_modified_time
pack_size

The client derives the expected scene path from the world key, calls PackRat.load_resource_pack(pack_url, PackRatOptions.from_expected_metadata(...)), then loads res://server/worlds/<world_key>/<world_key>.tscn after the pack is mounted.

Travel order:

1. Master validates route, portal, or login-resume travel.
2. Master sends a TravelLease and PackRat metadata.
3. Client refreshes the TravelLease while downloading or mounting the PCK.
4. Client redeems the TravelLease after `WORLD_PACK_READY`.
5. Master starts the target world if needed and issues the one-use join ticket.
6. Client connects to the world with that join ticket.
7. Target world confirms admission back to master; master commits active world state and clears the source transfer lock.

TravelLeases are refreshed every few seconds during asset preparation. They have a short soft expiry that refreshes while the client is active and a one-hour hard expiry so abandoned or malicious transfers cannot live forever. If the hard expiry is too close to redeem safely, the client cancels the PackRat request and fails the travel attempt clearly.

Local defaults:

default client host=127.0.0.1 in shared/net/net_config.gd
default client scheme=ws in shared/net/net_config.gd
default world pack base URL=https://shilo.github.io/multi-server-test/world_packs
PackRat request URLs append ?v=<application/config/version> when missing
editor default MULTI_SERVER_WORLD_PACK_DIR=builds/world_packs
exported server default=<server executable directory>/../world_packs

Production/server testing with Caddy should set full public URLs and bind Godot to localhost, then set the world-pack base URL/path to the CDN/static-host location and matching local metadata mirror where the current PCK files live. Web builds can override the same public URLs without rebuilding by using URL query parameters:

?master_url=wss://<public gameplay host>/&world_url_template=wss://<public gameplay host>/{world_key}&world_pack_base_url=<public pack base url>

The query override only changes the client's initial connection targets. The master still advertises world URLs from its own environment, so the VPS must also set MULTI_SERVER_PUBLIC_MASTER_URL / MULTI_SERVER_PUBLIC_WORLD_URL_TEMPLATE correctly or world joins will still point at the wrong address.

For Caddy reverse-proxy mode, use full public URLs instead of host/port composition:

MULTI_SERVER_BIND_HOST=127.0.0.1
MULTI_SERVER_PUBLIC_MASTER_URL=wss://game.example.com/
MULTI_SERVER_PUBLIC_WORLD_URL_TEMPLATE=wss://game.example.com/{world_key}

This keeps Caddy as the only public 443 listener while master and temporary world servers remain on deterministic localhost ports. The same override can be tested from the hosted Web client with:

?master_url=wss://game.example.com/&world_url_template=wss://game.example.com/{world_key}

If the gameplay server should speak wss:// directly, also set:

MULTI_SERVER_TLS_CERT=<absolute path to fullchain .crt>
MULTI_SERVER_TLS_KEY=<absolute path to private .key>

Godot's WebSocketMultiplayerPeer.create_server() supports TLS directly, so a Hetzner VPS can run the exported master/world servers on their normal ports with real certificates instead of requiring a protocol-converting proxy.

This prototype assumes master/world server processes run together on one host and use local ports internally.

Current Limits

  • Persistence is the MVP described in VirtuCade Database MVP: master-owned SQLite, guests, name-only login, and saved world/position.
  • No real auth: login is name-only with no password (anyone can log in as any existing name).
  • Name-only sessions are still unauthenticated, but world travel already uses master-owned TravelLease and join-ticket checks.
  • No authenticated client identity.
  • No inventory/currency/per-experience persistence yet (schema is ready to grow).
  • No standalone gateway.
  • No standalone chat process.
  • PackRat is wired for per-world PCK downloads, with automated HTTP, editor-export, exported Windows, and exported Web smoke coverage.
  • No external production supervisor configuration yet.
  • No server-side movement validation.

Minimum guardrails already present: master-owned process launch, per-launch world registration tokens, ACK-gated registration, refreshable TravelLeases for asset preparation, short-lived world join tickets, server-side portal/travel validation, start-timeout reaping, heartbeat expiry, player-count plus pending-join idle shutdown, target-world startup after lease redemption, chat length/rate caps, and master-only database access with server-validated position saves. Missing before public testing: authenticated sessions (login is name-only today), replacing command-line launch tokens on shared hosts, systemd/service hardening for the master, final VPS service wiring, and stronger movement validation.

About

One-project Godot 4.6 spike proving a small online-world topology.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors