The admin toolkit's 'Spawn at look-point' created a spirit entity but left
it in presence mode (is_manifested == false), so the only feedback the admin
got was the Population counter ticking up. The mesh existed and was at the
right position but the SpiritComponent contract says presence = "no body",
so admins saw nothing.
Fix: in scripts/admin/world_edit_commands.gd::_apply_spawn, when def_id is
"spirit", set is_manifested = true and current_power = max(1, current_power)
on the SpiritComponent AFTER EntityRegistry.spawn_entity returns. The
scripted summon_spirit() path already sets these explicitly after on_attach,
so it still wins there. SpiritPopulation is also incremented to match what
summon_spirit does.
Tests:
- tests/unit/test_spirit_spawn_visible.gd + .tscn: regression guard that
spawns via the same WorldEditCommands.apply path the admin uses, asserts
is_manifested == true and current_power >= 1, and (with a GPU renderer)
counts purple pixels in the rendered viewport.
|
||
|---|---|---|
| .forgejo/workflows | ||
| .gsd | ||
| data | ||
| docs | ||
| scenes | ||
| scripts | ||
| tests | ||
| .gdlintrc | ||
| .gitignore | ||
| export_presets.cfg | ||
| icon.svg | ||
| icon.svg.import | ||
| LICENSE | ||
| project.godot | ||
| README.md | ||
| SPEC.md | ||
VEILWALKERS
A modular, multiplayer, 3D living-world game framework for Android, Linux, and Windows, built with Godot 4.3 (GDScript, GL Compatibility renderer).
Inspired by Luanti/Minetest's moddability — but instead of a box/voxel world, VEILWALKERS is a living world: entities wander, metabolize planar energy, and reproduce on their own. Players are planeswalkers: they harvest mana from five planes (Aether, Ember, Tide, Grove, Void) and channel it into world-changing effects. Administrators (and trusted builders) get a full in-game creation toolkit: spawn, edit, and script entities live, terraform the ground, and manage the server's security — all without restarting the server.
Feature Map
| System | What it does | Where |
|---|---|---|
| Living world core | Component-based entities (health, wander, reproduction, energy) ticked by a LivingWorld scheduler; population cap; JSON save/load |
scripts/core/ |
| Self-reproducing entities | ReproductionComponent — entities store energy and spawn offspring when they can afford it |
scripts/core/components/ |
| Multiplayer | ENet server-authoritative networking, host/join menu, entity snapshot sync, chat | scripts/net/, scripts/autoload/network_manager.gd |
| Security | Role system (guest/player/builder/admin), permission checks, rate limiting, audit log | scripts/sec/, scripts/autoload/security.gd |
| Script sandbox | Runtime-compiled GDScript entity scripts restricted to a whitelisted ScriptEntityAPI; blacklist scan; per-tick CPU budget with auto-detach |
scripts/autoload/script_engine.gd |
| Admin creation toolkit | In-game window (F1): entity browser/spawner, live property editor, script editor with templates + compile check, terraform brush, save/load, security/points panel | scenes/admin/, scripts/admin/ |
| Creator points | Server-side currency for world-altering actions; passive earning; grant/spend audited | scripts/autoload/creator_points.gd |
| Player systems | Inventory (24 slots), skills (harvesting/channeling/weaving/terraforming with XP levels), 5-plane mana pool | scripts/player/ |
| Controls | WASD + mouse (desktop), virtual joystick + touch look (Android) | scenes/player/, scenes/hud/ |
Running
- Install Godot 4.3 (standard build, exact version — see Forward-compat policy below). Open
project.godot. - Press F5 → Host World to start a server, or run a second instance and Join World
(
127.0.0.1:43210). Headless server (no editor, no display):godot --headless -- --server # default port 43210 godot --headless -- --server --port 43210 - In game: WASD move, mouse look, F harvest glowing entities, 1/2/3 channel mana effects, F1 admin toolkit, Enter chat.
Exporting
export_presets.cfg ships with skeleton presets for Windows Desktop, Linux/X11, and
Android (arm64). Install Godot export templates (Editor → Manage Export Templates), and for
Android configure the Android SDK in Editor Settings, then Project → Export.
CI exercises the Linux/X11 release export on every push to main: it downloads a pinned
Godot 4.3 binary + matching export templates, runs godot --headless --export-release "Linux/X11"
headlessly, smoke-tests the produced binary's --version, and archives it as the
veilwalkers-linux-x86_64 workflow artifact (retained 14 days). A failing export fails the
export-linux job and the overall workflow, so a broken preset can't merge undetected.
See .forgejo/workflows/ci.yml.
Forward-compat policy
VEILWALKERS is built against Godot 4.3 exactly — not "4.3 or newer". project.godot pins
config/features=PackedStringArray("4.3", "GL Compatibility"), and the Linux/X11 export CI
job downloads the official 4.3 stable binary regardless of which Godot the runner happens
to have installed.
Newer Godot releases have already broken us once: under Godot 4.7 an inner VirtualJoystick
class collided with the engine's built-in VirtualJoystick (a native class shadowing the
script), which was fixed in commit c16562f by renaming the script's class to TouchJoystick.
The lesson generalizes — Godot periodically introduces built-in classes whose names collide
with existing user code, and the only safe response is to lock the editor version until the
collision surface has been audited.
Concretely:
- Dev hosts: install Godot 4.3 stable. A 4.4+ editor will open the project but may silently re-export the project with newer metadata that 4.3 can't read, or shadow a user class.
- CI: the
export-linuxjob downloads Godot 4.3 explicitly; don't replace that with${{ runner.godot }}style indirection. - Upgrading: bumping the target version is a deliberate migration task (re-test every
system, audit for native-class collisions, regenerate
.godot/import artifacts on the new version, then re-pin CI). It's not something the CI does for you.
Headless verification
All tests run without a display. From the repo root:
godot --headless --path . res://tests/e2e.tscn # integrated smoke (34 checks)
godot --headless --path . res://tests/unit/test_mana_pool.tscn # 5-plane mana: add/spend/cap
godot --headless --path . res://tests/unit/test_skill_book.tscn # XP/level math + leveled_up signal
godot --headless --path . res://tests/unit/test_inventory.tscn # add/remove/stack + slot cap
godot --headless --path . res://tests/unit/test_net_protocol_security.tscn # payload validation + role/perm matrix
godot --headless --path . res://tests/unit/test_living_world.tscn # tick scheduler + population cap
Each test prints PASS:/FAIL: lines and a final UT_RESULT <name> pass=N fail=N line, and
exits with code 1 if any check failed. CI runs the full set; see .forgejo/workflows/ci.yml.
Modding / extending
Everything is modular by contract (see docs/ARCHITECTURE.md):
- New entity: drop a JSON def in
data/entity_defs/(shape, color, components, script). - New behavior: write a component in
scripts/core/components/and reference it in the def. - New live script: use the toolkit's Script tab — scripts are sandboxed to
ScriptEntityAPI. - New permission: add a constant in
scripts/sec/permissions.gdand checkSecurity.require.
Safety notes
The script sandbox blocks obvious escape hatches (OS, FileAccess, ClassDB, load(),
get_tree(), …) and enforces a per-tick CPU budget — but GDScript is not a formally hardened
sandbox. Only grant the builder/admin roles (which carry entity.script) to people you trust,
the same trust model as Luanti's server/basic_privs privileges.