Skip to content

Building and running Goanna

Goanna is a GDExtension. You build a shared library from this repository, drop it into the Godot project that lives in project/, and run that project against a Luanti server.

Everything below has been done on Linux with GCC. Windows and macOS are not yet supported, not because of anything fundamental, but because nobody has tried. Reports are welcome.

Goanna is pre-alpha. Expect it to connect, show you terrain and let you walk around, and expect it to do very little else. See the README for the honest list.

What you need

Version Notes
Godot 4.5.x, standard build The release binary. You do not need Godot's source and you do not rebuild the engine.
GPU Vulkan capable, mid range discrete or better Forward+ only. There is no Compatibility or Mobile fallback, on purpose.
CMake 3.22 or newer
Ninja or Make any Ninja is what the author uses.
C++ compiler C++17 GCC 12 or newer, or Clang 15 or newer. Verified on GCC 16.
Python 3.4 or newer Not optional. godot-cpp runs it to generate the bindings.
zlib development headers find_package(ZLIB REQUIRED).
Zstandard library and headers, static preferred The build fails with zstd not found if it is missing.
git any The submodules are not optional.
A Luanti server 5.17.0 tested See "Connecting to a server" below.

Debian and Ubuntu:

sudo apt install build-essential cmake ninja-build git python3 zlib1g-dev libzstd-dev

Fedora:

sudo dnf install gcc-c++ cmake ninja-build git python3 zlib-devel libzstd-devel

Arch:

sudo pacman -S base-devel cmake ninja git python zlib zstd

Building

git clone --recurse-submodules --shallow-submodules \
    https://github.com/p0ss/Goanna.git goanna
cd goanna

The submodules are large. Full history is around 170 MB; --shallow-submodules cuts that to about 55 MB, which is plenty to build from. If you already cloned without them, git submodule update --init --depth 1 does the same job.

luanti is pinned to release 5.17.0 and godot-cpp tracks the 4.5 branch. Both are used unmodified. Goanna does not patch either one, and if it ever needs to, that is a bug in Goanna. See docs/develop/transplanting.md.

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build

Pass the build type explicitly. CMakeLists.txt sets no default, so omitting it gives you a build with no optimisation at all, which will make the meshing and collision code look far slower than it is.

The first build compiles godot-cpp as well as Goanna, and takes a while. On an eight core machine, budget five to fifteen minutes, and expect the build/ tree to reach about 1.4 GB.

The output goes straight into the Godot project, which is where the .gdextension file expects it:

project/bin/libgoanna.linux.template_debug.x86_64.so

If you would rather build with Make, drop -G Ninja.

If zstd is not found

cmake/luanti_core.cmake looks for a static libzstd.a first and falls back to whatever zstd the linker can find. If your zstd is somewhere unusual, point CMake at it:

cmake -S . -B build -G Ninja \
  -DZSTD_STATIC_LIB=/path/to/libzstd.a \
  -DZSTD_INCLUDE_DIR=/path/to/include

or, if both live under one prefix, -DZSTD_ROOT=/that/prefix.

A note on the build target

CMakeLists.txt forces godot-cpp's template_debug target, and hardcodes the library name to match. That is a spike-stage shortcut, and it has two consequences worth knowing:

  • -DGODOTCPP_TARGET=template_release is silently ignored, because the variable is set with FORCE.
  • An exported release build of the Godot project will not find its library, because project/goanna.gdextension names a template_release file that nothing produces. Running from the editor, or with the Godot binary directly as below, is the supported path for now.

CMAKE_BUILD_TYPE and GODOTCPP_TARGET are separate things. The first controls optimisation of Goanna's own code, the second controls which godot-cpp variant is linked. Setting RelWithDebInfo above does not turn off Godot's debug checks.

One time: let Godot register the extension

A freshly built library is not enough. Godot only writes project/.godot/extension_list.cfg, which is what actually loads a GDExtension at run time, during an editor filesystem scan. That directory is gitignored, so a fresh clone never has one, and a plain run will fail with Could not find type "GoannaClient" and then hang.

Do this once, after the first build:

/path/to/Godot_v4.5.1-stable_linux.x86_64 --headless --editor --quit --path project

Opening project/ in the graphical editor once has the same effect.

Known issue: with the extension loaded, the editor segfaults on shutdown (signal 11). The extension_list.cfg file is written before the crash, so the step above still does its job and you can carry on. A plain run, without --editor, exits cleanly. The cause is most likely static destructor ordering in luanti_core when Godot unloads the library, and it is not yet fixed.

Check your diff afterwards. Opening the project in the editor, including with --headless --import, rewrites project/project.godot. It adds a boilerplate header, and it silently drops any setting whose value happens to match Godot's own default. That has already cost this repository its explicit [rendering] renderer/rendering_method="forward_plus" line once. Forward+ is the default, so nothing broke, but that line is the one place the project asserts in machine readable form the thing the README spends a paragraph on. Run git diff project/project.godot after any editor pass and revert it unless you meant it.

Windows

Untested. The build system knows about Windows and there is a CI job for it, but no Windows binary has been produced on real hardware yet, and nobody has played it. If you try this, a report either way is genuinely useful.

The route is MSVC with vcpkg, not MinGW. Upstream Luanti abandoned GCC MinGW because thread-local storage is broken there, and Luanti's core leans on TLS, so that is not a road worth walking again.

You need Visual Studio, 2022 or newer, with the C++ workload, CMake, Git and Python, and vcpkg for zlib and zstd, which a stock Windows machine does not have. The manifest at vcpkg.json lists them.

git clone --recurse-submodules --shallow-submodules https://github.com/p0ss/Goanna.git goanna
cd goanna
cmake -S . -B build -A x64 ^
  -DCMAKE_TOOLCHAIN_FILE=C:/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake ^
  -DVCPKG_TARGET_TRIPLET=x64-windows
cmake --build build --config RelWithDebInfo

No -G on purpose. CMake picks the newest Visual Studio it finds when the generator is left unspecified, which is one less thing to go stale as new Visual Studio versions ship. Pass -G "Visual Studio 18 2026", or whatever generator name matches your install, if you need a specific version.

Keep the x64-windows triplet. It uses the dynamic CRT, which is what godot-cpp defaults to; a static triplet gives a CRT mismatch at link time, and the error message will not tell you that is what happened.

The library lands at project/bin/libgoanna.windows.template_debug.x86_64.dll, which is the name project/goanna.gdextension expects.

Expect the first attempts to fail inside src/transplant/client/*.cpp rather than in the CMake plumbing. MSVC is stricter than GCC about two-phase name lookup in templates, and that code is Luanti's, carried across unchanged. If it gets unpleasant, -T ClangCL is a reasonable escape hatch, and upstream Luanti supports that toolset too.

Running

Goanna talks to a Luanti server, always. It has no built in server and is not going to grow one, because the whole point is that it is a client for the ordinary ecosystem.

"Start Game" in the menu is that principle applied rather than an exception to it. Goanna finds Luanti on your machine, launches an ordinary unmodified server on localhost, and joins it over the ordinary protocol, exactly as if you had started the server yourself in another terminal. It looks for every Luanti install on the machine (distribution packages, Flatpak, Snap, AppImages, unpacked builds; "finding Luanti" in project/net/local_server.gd lists where) and applies Luanti's own path rules to find each one's games and data. It uses the install the player chose on the menu's Luanti screen, or else the first that has a game, and remembers the choice in goanna.cfg under [luanti]. Set GOANNA_SERVER_CMD to override the command, and GOANNA_SERVER_DATA_DIR for its data directory. It enumerates the games that install has, creates the world, waits for the port to open, and connects. Quitting or disconnecting shuts that server down.

So there is no singleplayer code path, no embedded engine and no private protocol. There is a server, it is Luanti's, and Goanna just started it for you.

Running a server yourself

How to start an ordinary Luanti server for Goanna to join, from a package or a Flatpak, is in hosting a world.

Point Goanna at it

Run the project and the main menu (project/menu/menu.gd) opens. Its buttons are Start Game, Join Game, Local players, Content, Settings, About and Quit. To join a server that is already running:

  1. Press Join Game. The top of the screen lists the public servers from https://servers.luanti.org/list, busiest first, with Refresh list below it. Picking a row fills in the address and port; double clicking it joins at once. Without a network the list says so and the fields still work.
  2. Fill in Server address (127.0.0.1 when empty), Port (30000 when empty), Player name (1 to 20 letters, digits, _ or -) and Password. On a server that allows registration, a new name is registered with that password.
  3. Choose Graphics: PBR with the server's own materials (the default, which also uses Goanna's installed materials for the server's game when that game is known), Standard with no PBR, an installed Minetest Game or Mineclonia material pack, or one of the texture packs in the Luanti data directory.
  4. Press Connect, or Enter in any of the fields.

The menu remembers the address, port, player name and graphics choice in Godot's user data directory (user://goanna.cfg, which on Linux is ~/.local/share/godot/app_userdata/Goanna/goanna.cfg), and never the password. Start Game, the local server described above, has its own screen with World, Mods and Hosting tabs. Local players sets up experimental split screen, whose first player is the one named on Start Game or Join Game.

The menu steps aside when the details are already in the environment, which is what scripts and the automated runs below use. Set GOANNA_HOST or GOANNA_NAME (or one of the test modes, listed in SKIP_VARS at the top of menu.gd) and Goanna connects straight away; set GOANNA_MENU=1 to get the menu anyway. Join Game then takes its port from GOANNA_PORT when that is set, and the rest from goanna.cfg. The variables are read in project/game/main.gd:

Variable Default Meaning
GOANNA_HOST 127.0.0.1 Server address
GOANNA_PORT 30000 Server port
GOANNA_NAME goanna Player name
GOANNA_PASS empty Password. On a server that allows registration, the first connection registers this name with this password.

Two warnings about GOANNA_PASS. Leaving it empty registers the name with an empty password, which is fine for a local devtest world and not fine for anything else. And putting it on the command line writes it to your shell history and exposes it in /proc/<pid>/environ. Export it, or use a password you do not mind leaking, for anything public.

Run the project with the Godot binary, skipping the menu:

GOANNA_HOST=127.0.0.1 GOANNA_PORT=30000 \
GOANNA_NAME=goanna GOANNA_PASS=hunter2 \
/path/to/Godot_v4.5.1-stable_linux.x86_64 --path project

Or open project/ in the Godot editor and press F5, having set the variables in your shell before launching the editor.

Note that project/ is not self-contained. Run from a checkout, GoannaClient resolves Luanti's share path to res://../luanti, so it reads the base texture pack out of the submodule checkout, and the Godot project cannot be moved or copied away from the repository. An exported build looks instead for a luanti folder beside the folder that holds the executable, which is how a release is laid out (GoannaClient::connect_to in src/goanna_client.cpp).

Progress is printed to the console once a second: connection state, media files received, mapblocks received and meshed, materials built, and the player's position and ground state. If something is wrong, that line is the first place to look.

Controls

The controls are listed for players in controls. Walking uses Luanti's own LocalPlayer and collision code, so speeds, gravity, step height and sneak behaviour are the server's, not an approximation.

F toggles a free camera that leaves the body behind and passes through terrain. step_player, the only thing that moves the player, does not run while it is on, so the server never sees the camera move. It still sees what the player could not walk to, which is reach no vanilla client has, so F is accepted only when this client started the server itself. It is not the game's fly privilege and asks the server for nothing.

Automated runs

project/game/main.gd has a few environment variables used for smoke tests and for producing the screenshots in docs/. They are development aids, not features.

Variable Effect
GOANNA_CONTROL=<port> Open a loopback command channel on that port (1 means 30800), so the running client can be teleported, posed, questioned and photographed instead of relaunched for each question. docs/agents/control-channel.md has the commands and the rule about not reporting a live tweak as a result. It also turns on test mode (below).
GOANNA_NO_POINTER_CAPTURE=1 Test mode without the channel: the client never captures the OS pointer and asks for no focus. Capture is kept inside the client instead, and only mouse input pushed in through the control channel counts as captured. Test clients belong in tools/goanna-headless anyway; this is for the rare one that has to be on the desktop.
GOANNA_SMOKE=<seconds> Connect, run for that many seconds, disconnect and quit. Useful in CI or after a change to the session code.
GOANNA_SHOT=<directory> Fly to three fixed viewpoints, save a PNG at each, then quit.
GOANNA_WALKTEST=1 Drive the movement controls from a script rather than the keyboard. With GOANNA_SHOT, saves two frames mid walk.
GOANNA_VIEW="name:x,y,z:pitch,yaw;..." With GOANNA_SHOT, replaces the three fixed viewpoints. Positions are relative to the spawn eye position, or absolute with a leading @.
GOANNA_SUN, GOANNA_WHITE, GOANNA_EXPOSURE, GOANNA_SKY_FILL, GOANNA_SDFGI, GOANNA_SSAO Seed the Lighting tab's values: sun energy, ACES white point, base exposure, sky fill strength, bounced light, corner shading. project/studies/lighting_chart.tscn reads the same names, so a value judged there transfers.
GOANNA_TOD=<0..1> Override the time of day, and drop any day night ratio the server is overriding (Mineclonia does in weather), so the frame is that hour.
GOANNA_GAME=<gameid> The game the server runs, set by the menu when it launches a local world. Picks per game defaults the player has not chosen: today the bundled project/texture_maps/<gameid>.csv, when one exists.
GOANNA_TEXTURE_MAP=<csv> A game_texture,pack_path CSV (project/texture_maps/), the same as the settings entry: lets a Minecraft resource pack dress the game, and gives the classifier its block column.
GOANNA_DEBUG_PBR=1 Print the classifier's counts and, per array texture, how many layers are authored, classed or inferred, and how many are left neutral.
GOANNA_PERF=1 Print one telemetry line a second: frame time, mesh and upload cost, draw calls, objects, video memory, and the far tier counts.
GOANNA_BENCH=1 Build the per frame recorder (project/bench/bench.gd), which the control channel's bench and route commands drive and tools/bench/goanna-bench.py runs plans against. Turns vsync and the frame cap off, and stamps the time to connected, to playable and to settled from process start. See docs/develop/benchmark.md.
GOANNA_LOD=<blocks> Draw blocks beyond this many mapblocks as coarse tiers (docs/systems/far-rendering.md); the same as the detail distance slider. GOANNA_LOD_CELL=<nodes> sets the first tier's cell size, a power of two from 2 to 16.
GOANNA_STORE=<dir>, GOANNA_NO_STORE=1 Relocate or turn off the local block store (docs/design/far-rendering-plan.md rung 5), which otherwise lives under user://goanna_store, one directory per server. GOANNA_STORE_CAP_MB sets its cap (512). GOANNA_FAR_DISTANCE=<nodes> caps how far stored blocks are drawn when the server grants far rendering.
GOANNA_DEBUG_LOD=1 Print every far tier region build: tier, cell, members, faces, quads and the surfaces it produced.
GOANNA_SHADERPACK=<directory> Load an Iris or OptiFine shader pack (unpacked) and run its deferred, composite and final programs as a compositor effect. Only the screen space chain; see docs/design/iris-compat.md. Wins over the Shader pack setting (docs/develop/shaderpack-testing.md).
GOANNA_SHADERPACK_RAW=1 With a pack: hand it Godot's linear HDR colour and keep Godot's tonemap, instead of the Minecraft style gamma space bridge.
GOANNA_SHADERPACK_DUMP=<directory> With a pack: write each program's translated Vulkan GLSL there, for reading compile errors.
GOANNA_VISUAL_TEST=lighting_walk With GOANNA_SHOT, request a named deterministic fixture, capture its fixed camera path and write the capture metadata. Requires goanna_visual_test_mod in a dedicated singlenode world.
GOANNA_MENU_SHOT=<file.png> Render the main menu once, save it, quit. GOANNA_MENU_SCREEN picks the screen (new, join, settings, settings-tab-<Tab>, content, about, luanti, setup and others in menu.gd), GOANNA_MENU_SCALE=2 lays it out at 200 percent, GOANNA_MENU_SCROLL=<pixels> scrolls its scrolling bodies, and GOANNA_MENU_INFO=1 opens every (i) on it.
GOANNA_MENU_BACKDROP=<file.png> Put this picture behind the main menu instead of the village still, so a screenshot can show the menu's panel over another scene.
GOANNA_NO_SHOWCASE=1 Disable the normal menu's real-world Mineclonia treetree backdrop. The default view uses (25.3, 2.0, -115.8), dawn lighting and a bounded 18-mapblock stream.
GOANNA_UI_SHOT=<directory> Save the HUD, inventory, chat and pause menu at fixed times.
GOANNA_UI_TEST=move Open the inventory and move the stack in main slot 0 to slot 10, then split it to slot 11, printing the list each time. Exercises inventory_action through the UI.
GOANNA_MENU_TEST="host:port:name:pass" Fill the menu and press Connect, to exercise the menu to game hand-over. Combine with GOANNA_MENU=1 if a skip variable such as GOANNA_SMOKE is also set.
GOANNA_TEST_LABEL=<text> What this session is testing, drawn in the corner while the control channel is open, so someone watching the window can tell a stalled client from an agent that is thinking. docs/agents/control-channel.md has how to read it; the label command sets it while running.
GOANNA_LOCAL_TEST="game:world" Drive the "start a local game" screen without a human: it does not save the world and game into the player's remembered choices, and the server it launches gets enable_damage = false and time_speed = 0. A frozen clock and no damage is what makes two captures comparable, since otherwise a night falling or a mob landing a hit between them shows up as a difference the run did not cause. Set the hour a shot is taken at with GOANNA_TOD or the control channel's time.

A minimal check that everything still works, end to end:

GOANNA_SMOKE=20 /path/to/godot --path project

Deterministic visual fixtures

Do not use a survival spawn or the material gallery for renderer comparisons. Terrain, time, weather, nearby lights and previously built tests all change the result, so a repeat run is not the same experiment.

goanna_visual_test_mod/ is a worldmod for a fresh test world using the singlenode mapgen. Copy or link it to the world's worldmods/ directory and enable goanna_visual_test in world.mt. The mod refuses to build in any other mapgen, so an accidental install cannot clear part of a survival world.

Run the lighting path against that server with:

GOANNA_HOST=127.0.0.1 GOANNA_NAME=goanna \
GOANNA_SHOT=/tmp/goanna-lighting \
GOANNA_VISUAL_TEST=lighting_walk \
/path/to/godot --path project

The fixture fixes midday, removes clouds, disables the camera headlight and captures 17 positions half a node apart with one pitch and yaw. The output directory also receives lighting_walk.json, recording the positions and lighting settings used. Analyse adjacent changes with:

tools/dev/shotcheck.py --walk-series \
  /tmp/goanna-lighting/lighting_walk_*.png

Pass --max-step N once a measured acceptance threshold has been chosen. The checker reports the result without inventing a pass threshold by default.

The ao, materials and ice fixture sites are 1024 nodes from one another and from lighting_walk. Their automated captures should be added at those sites rather than extending one shared gallery.

The formspec renderer has a separate headless conformance suite. It checks the local Luanti parser registry as well as Godot-side layout and interaction fixtures:

tools/test/test-formspec.sh

See formspec conformance for classifications, reference screenshots and instructions for extending the suite.

Release checks

Cutting v0.9.0-alpha shipped four defects that nothing checked for: native tests that ran stale binaries, a client that threw away every bundle it downloaded, a bundle whose height maps had been flattened, and an asset epoch left as a draft so every catalogue URL answered 404. Each now has a check. Run all four for a release candidate. Each exits non-zero on failure.

Native tests, built first.

cmake --build build --target check

The native test targets are EXCLUDE_FROM_ALL, so cmake --build build never rebuilds them, and running ./build/<test> runs whatever binary is there, however old. check depends on every test, so the build system rebuilds whichever are stale and then runs them all from the repository root, stopping at the first failure. Configuring fails if a src/goanna_*_test.cpp has no entry in GOANNA_NATIVE_TESTS in CMakeLists.txt, so a new test cannot be left out of it. To run one test, build it by name in the same command: cmake --build build --target goanna_lod_test && ./build/goanna_lod_test.

A real bundle install.

godot --headless --path project --script res://tests/asset_bundle_install.gd

Writes a few small textures, including mcl_bamboo_bamboo beside mcl_bamboo_bamboo_plank (a stem that is a prefix of another, which is what broke AssetStore.install_archive), packs them with tools/pbr/pbr_bundle.py build, and installs the archive into an empty store with the call asset_updater.gd makes on a finished download. It checks the composed profiles/<game>/textures file by file, and that nothing is left staged in the store. It needs python3 with Pillow on PATH.

Height maps that fill the byte.

python3 tools/pbr/check-pbr-height.py pbr_packs/mineclonia/textures dist/assets/*.zip

Fails a pack whose baked _n maps have a median alpha span under 224 of 255. The shader applies each material's depth itself, so a bake that scaled the byte by it applied it twice; the per map quality gate could not see that. tools/pbr/pbr_bundle.py build and verify run it, and so does tools/pbr/check-pbr-quality.py, so it also runs inside tools/release/publish-assets.sh. Authored maps (PNG text chunk goanna_pipeline=authored) are reported separately and never fail it.

Published URLs answer. After publishing an asset epoch:

python3 tools/release/check-asset-catalogue.py --live

Sends a HEAD request to every URL in asset_bundles/catalogue.json, following redirects, and fails on anything but a 200 whose size matches the catalogue. tools/release/publish-assets.sh prints this command, with the one that publishes the draft, as its last output. Commit the catalogue only after it passes.

Sign the release, or no client updates to it. After the GitHub release for a tag exists with both zips on it:

tools/release/sign-release.sh v0.11.0-alpha

Writes dist/manifest.json (each zip's size and SHA-256) and dist/manifest.json.sig, signed with the private key in ~/.config/goanna-release/update-signing.pem, checks the signature against project/update_key.pub.pem, and uploads both to the release. A client offers the update only when that signature checks and the version is newer than its own (project/update/updater.gd). Keep the private key backed up and out of the repository: losing it means players download one release by hand to move to a new key.

Troubleshooting

zstd not found during CMake configure. Install the Zstandard development package, or pass -DZSTD_STATIC_LIB and -DZSTD_INCLUDE_DIR as above.

CMake cannot find Python. godot-cpp requires Python 3.4 or newer to generate its bindings. It is in the dependency list above and is easy to miss on a minimal container image.

Godot starts, reports Could not find type "GoannaClient", then hangs. Almost always this is the missing project/.godot/extension_list.cfg, not a missing library. Run the one time editor step above. If the file exists, check that project/bin/libgoanna.linux.template_debug.x86_64.so is there and that Godot is 4.5 or newer.

The editor crashes when you close it. Known, see the note above. It happens on extension unload and does not affect a plain run.

Undefined symbol errors when loading the extension. The link uses --no-undefined, so this should be caught at build time rather than load time. If it happens anyway, say so in an issue with the full message and your compiler version.

It connects, then sits at "media". The server is still sending media files. Devtest sends around 440 and takes about a second on a local server. A large game over the internet takes considerably longer, and the count in the console line tells you whether it is progressing.

The server rejects the password. Goanna registers a new name on first connect if the server allows it, and uses SRP to log in afterwards. If the name already exists with a different password, pick another GOANNA_NAME.

A surface is blank, white or missing. Goanna supports Luanti's drawtypes and array-texture grouping, but an unusual texture modifier, material or model can still expose a conversion bug. Run with GOANNA_DEBUG_CONTENT=1 and report the node name, game and relevant log lines.