Fix backwards controls + the exe's bare-capsule avatar
checkpoint / checkpoint (push) Successful in 1m41s

Two real bugs from the last change, both only visible in a real run:

1. Controls reversed: the third-person SpringArm3D had a 180° Y rotation
   that put the camera IN FRONT of the player looking back -- a mirror, so
   W read as backward and mouse pitch as inverted. SpringArm3D extends its
   child along +Z, not -Z; with no rotation the camera sits behind the
   player (+Z world) and looks -Z straight at them. Verified the vectors
   headless: camera behind, W decreases Z, mouse-up looks up.

2. Bare capsule instead of an avatar in the .exe: avatar_catalog.gd reads
   assets/avatars/catalog.json with FileAccess.open(), but the export used
   `all_resources` + empty include_filter, which does NOT pack plain .json
   files. ensure_catalog() failed -> no loadout -> build_current_avatar()
   returned null -> capsule stayed visible. From source it worked because
   res:// is the real filesystem. Fixed: include_filter="*.json" in both
   export presets (verified catalog.json + the .glb pack are now in the
   exported .pck).

- scripts/verify_export_contents.gd + a checkpoint-runner step: export a
  pack and assert the non-resource runtime files are inside it. This is
  the guard for the "works from source, broken in the .exe" class.

Client-side -- rebuild the exe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
2026-09-05 23:30:00 +00:00
parent 87f78b0226
commit 54bc763dff
4 changed files with 80 additions and 4 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ advanced_options=false
dedicated_server=false dedicated_server=false
custom_features="" custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter="*.json"
exclude_filter="" exclude_filter=""
export_path="export/linux/agp-game.x86_64" export_path="export/linux/agp-game.x86_64"
encryption_include_filters="" encryption_include_filters=""
@@ -38,7 +38,7 @@ advanced_options=false
dedicated_server=false dedicated_server=false
custom_features="" custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter="*.json"
exclude_filter="" exclude_filter=""
export_path="export/windows/agp-game.exe" export_path="export/windows/agp-game.exe"
encryption_include_filters="" encryption_include_filters=""
+1 -2
View File
@@ -20,10 +20,9 @@ shape = SubResource("CapsuleShape1")
mesh = SubResource("CapsuleMesh1") mesh = SubResource("CapsuleMesh1")
[node name="CameraPivot" type="Node3D" parent="."] [node name="CameraPivot" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.7, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4, 0)
[node name="SpringArm3D" type="SpringArm3D" parent="CameraPivot"] [node name="SpringArm3D" type="SpringArm3D" parent="CameraPivot"]
transform = Transform3D(-1, 0, -8.7422777e-08, 0, 1, 0, 8.7422777e-08, 0, -1, 0, 0, 0)
spring_length = 4.0 spring_length = 4.0
margin = 0.2 margin = 0.2
+11
View File
@@ -180,6 +180,17 @@ def main():
fc_env["GODOT_BIN"] = godot_bin fc_env["GODOT_BIN"] = godot_bin
r = run(["bash", str(fc_script)], env=fc_env) r = run(["bash", str(fc_script)], env=fc_env)
results["fresh-clone boot"] = r.returncode == 0 results["fresh-clone boot"] = r.returncode == 0
# Export packaging: the game reads some files (catalog.json)
# via raw FileAccess, which `all_resources` export doesn't
# pack unless include_filter covers them. Passes from source,
# bare capsule in the .exe. This guards that.
ec_script = ROOT / "scripts" / "verify_export_contents.gd"
if ec_script.exists():
print("\n--- scripts/verify_export_contents.gd ---")
r = run([godot_bin, "--headless", "--path", str(ROOT),
"--script", "res://scripts/verify_export_contents.gd"], env=godot_env)
results["export packaging"] = r.returncode == 0
finally: finally:
server_proc.terminate() server_proc.terminate()
try: try:
+66
View File
@@ -0,0 +1,66 @@
extends SceneTree
## Exports a resource pack and checks that the files the game reads at runtime
## via raw FileAccess (not as Godot resources) are actually inside it.
##
## `export_filter="all_resources"` packs scenes/scripts/imported assets but
## NOT arbitrary files like assets/avatars/catalog.json -- which the avatar
## system opens with FileAccess.open(). Missing it means the game runs fine
## from source but the exported .exe shows a bare capsule instead of an
## avatar. This is the guard for that class of bug.
##
## Run: godot --headless --path . --script res://scripts/verify_export_contents.gd
const PACK_PATH := "res://.export_check.pck"
## Files that MUST be in the pack for the game to work, that aren't Godot
## resources (so they only get packed via export_presets include_filter).
const REQUIRED := [
"res://assets/avatars/catalog.json",
]
## A resource we know should be there, as a sanity check that the pack loaded.
const SANITY := "res://scenes/game.tscn"
func _initialize() -> void:
var abs_pack := ProjectSettings.globalize_path(PACK_PATH)
var export := OS.execute(_godot_bin(), [
"--headless", "--path", ProjectSettings.globalize_path("res://"),
"--export-pack", "Linux", abs_pack,
], [], true)
if not FileAccess.file_exists(PACK_PATH):
_fail("export-pack produced no file:\n%s" % export)
if not ProjectSettings.load_resource_pack(PACK_PATH, false):
_fail("could not load the exported pack back")
if not FileAccess.file_exists(SANITY):
_fail("pack sanity check failed -- %s not found (pack may not have loaded)" % SANITY)
var missing := []
for f in REQUIRED:
if not FileAccess.file_exists(f):
missing.append(f)
DirAccess.remove_absolute(abs_pack)
if missing.is_empty():
print("=== SMOKE RESULT: PASS === (required non-resource files are in the export)")
quit(0)
else:
print("=== SMOKE RESULT: FAIL ===")
for f in missing:
printerr(" MISSING from export: ", f,
" -- add its glob to export_presets.cfg include_filter")
quit(1)
func _godot_bin() -> String:
return OS.get_executable_path()
func _fail(msg: String) -> void:
printerr(msg)
print("=== SMOKE RESULT: FAIL ===")
quit(1)