Fix ESC-crashes-the-game, avatar facing backward, invisible build stamp
checkpoint / checkpoint (push) Successful in 1m47s

ESC: player.gd changed scene directly while game.gd._exit_tree closed the
socket, which re-fired server_disconnected -> _on_server_lost ran mid-
teardown (add_child on an exiting node, await on a null get_tree(), a
second change_scene_to_file). Moved ESC to game.gd as one ordered path:
_leave_world() sets a _leaving guard, disconnects the _on_server_lost
handlers before closing the connection, then changes scene. _on_server_lost
and _exit_tree now respect the guard. Verified headless: ESC -> world_list,
no errors; 2p multiplayer still connects.

Avatar faced +Z but a Godot node's forward is -Z, so walking forward
showed the character its own face. Rebuilt the placeholder facing -Z (face
plates, eyes, jaw, toes, and front-mounted attachments all moved to
negative Z; backpack/ponytail to +Z). The procedural walk was already
written for -Z forward, so it's unchanged.

Build stamp was there but tiny/low-contrast bottom-left -- bumped to 20px
opaque yellow with a heavy outline, moved to the top-left strip.

limb_animator: guard _process with is_instance_valid(_skel) so a frame
landing during avatar teardown can't touch a freed skeleton.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
2026-09-06 14:24:45 +00:00
parent 3fb45f0ef2
commit eb4da62f99
11 changed files with 71 additions and 39 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -42,7 +42,7 @@ func setup(skeleton: Skeleton3D) -> void:
func _process(delta: float) -> void: func _process(delta: float) -> void:
if _skel == null or _idx.is_empty(): if not is_instance_valid(_skel) or _idx.is_empty():
return return
var moving := speed > MOVE_EPS var moving := speed > MOVE_EPS
+51 -18
View File
@@ -22,6 +22,7 @@ const CORRECTION_THRESHOLD := 0.6
var _net: NetworkClient = null var _net: NetworkClient = null
var _networked := false var _networked := false
var _leaving := false # a scene change is already in flight
var _aligned := false # snapped to the server spawn yet? var _aligned := false # snapped to the server spawn yet?
var _remotes: Dictionary = {} # peer_id -> Node3D var _remotes: Dictionary = {} # peer_id -> Node3D
var _remote_prev: Dictionary = {} # peer_id -> Vector3 (last snapshot position) var _remote_prev: Dictionary = {} # peer_id -> Vector3 (last snapshot position)
@@ -34,6 +35,37 @@ func _ready() -> void:
_setup_networked() _setup_networked()
func _unhandled_input(event: InputEvent) -> void:
# ESC leaves the world back to the browser. Handled here (the scene root
# that owns the connection) rather than in player.gd so the teardown is
# a single ordered path -- doing it from two places raced _on_server_lost
# against the scene change and looked like a crash.
if event.is_action_pressed("ui_cancel"):
get_viewport().set_input_as_handled()
_leave_world()
func _leave_world() -> void:
if _leaving:
return
_leaving = true
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_teardown_net()
Session.clear()
get_tree().change_scene_to_file("res://scenes/world_list.tscn")
## Close the ENet connection and stop it from calling us back mid-teardown.
func _teardown_net() -> void:
if _net == null or not is_instance_valid(_net):
return
if _net.server_disconnected.is_connected(_on_server_lost):
_net.server_disconnected.disconnect(_on_server_lost)
if _net.connection_failed.is_connected(_on_server_lost):
_net.connection_failed.disconnect(_on_server_lost)
_net.disconnect_from_server()
static func _build_label() -> String: static func _build_label() -> String:
var f := FileAccess.open("res://assets/build_info.json", FileAccess.READ) var f := FileAccess.open("res://assets/build_info.json", FileAccess.READ)
@@ -51,30 +83,28 @@ func _show_build_stamp() -> void:
add_child(layer) add_child(layer)
var lbl := Label.new() var lbl := Label.new()
lbl.text = "build %s" % _build_label() lbl.text = "build %s" % _build_label()
lbl.add_theme_color_override("font_color", Color(1, 1, 0.45)) lbl.add_theme_color_override("font_color", Color(1, 1, 0.35))
lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85)) lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 1))
lbl.add_theme_constant_override("outline_size", 4) lbl.add_theme_constant_override("outline_size", 6)
lbl.add_theme_font_size_override("font_size", 14) lbl.add_theme_font_size_override("font_size", 20)
# Anchor a full-width strip to the bottom of the screen and left-align the # Full-width strip pinned to the TOP-left. (A bare preset + .position on a
# text in it. (A bare preset + .position on a zero-size Label collapses the # zero-size Label collapses the rect to negative width and nothing draws --
# rect to negative width and nothing draws -- which is why this never showed.) # which is why this never showed; explicit offsets fix it.)
lbl.set_anchors_preset(Control.PRESET_BOTTOM_WIDE) lbl.set_anchors_preset(Control.PRESET_TOP_WIDE)
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
lbl.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM lbl.offset_left = 12
lbl.offset_left = 8 lbl.offset_right = -12
lbl.offset_right = -8 lbl.offset_top = 8
lbl.offset_top = -30 lbl.offset_bottom = 40
lbl.offset_bottom = -6
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
layer.add_child(lbl) layer.add_child(lbl)
func _exit_tree() -> void: func _exit_tree() -> void:
# Leaving the world -- close the ENet connection cleanly so the server # Backstop for any exit path that didn't go through _leave_world (e.g. the
# fires peer_disconnected and removes our player (otherwise everyone else # app quitting): close the ENet connection so the server fires
# keeps seeing a frozen "ghost" of us). # peer_disconnected and drops our player instead of leaving a ghost.
if _net != null and is_instance_valid(_net): _teardown_net()
_net.disconnect_from_server()
func _setup_networked() -> void: func _setup_networked() -> void:
@@ -205,6 +235,9 @@ func _all_mesh_instances(node: Node, out: Array = []) -> Array:
func _on_server_lost() -> void: func _on_server_lost() -> void:
if _leaving:
return
_leaving = true
_networked = false _networked = false
_show_status("Disconnected from the server. Returning to worlds…") _show_status("Disconnected from the server. Returning to worlds…")
await get_tree().create_timer(1.5).timeout await get_tree().create_timer(1.5).timeout
+2 -5
View File
@@ -128,11 +128,8 @@ func _unhandled_input(event: InputEvent) -> void:
_update_input_direction(key) _update_input_direction(key)
elif not key.pressed: elif not key.pressed:
_clear_input_direction(key) _clear_input_direction(key)
# Escape exits the level back to the world browse list, mirroring # ESC (leave the world) is handled by game.gd, the scene root that
# the Back button pattern already used in world_list.gd/main_menu.gd. # owns the network connection, so the teardown is a single path.
if key.pressed and key.keycode == KEY_ESCAPE:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
get_tree().change_scene_to_file("res://scenes/world_list.tscn")
elif event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: elif event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
_handle_mouse_look(event as InputEventMouseMotion) _handle_mouse_look(event as InputEventMouseMotion)
+17 -15
View File
@@ -17,6 +17,8 @@ const OUT_DIR := "res://assets/avatars/"
# Canonical rig -- must match avatar_loader.gd CANONICAL_BONES. Each entry: # Canonical rig -- must match avatar_loader.gd CANONICAL_BONES. Each entry:
# name -> [parent_name, global_rest_position] (A-pose, arms down, Y-up, metres) # name -> [parent_name, global_rest_position] (A-pose, arms down, Y-up, metres)
# The character faces -Z (Godot's node "forward"), so the face, toes and
# front-mounted attachments live at negative Z.
const BONES := { const BONES := {
"Root": ["", Vector3(0.00, 0.00, 0.00)], "Root": ["", Vector3(0.00, 0.00, 0.00)],
"Hips": ["Root", Vector3(0.00, 0.76, 0.00)], "Hips": ["Root", Vector3(0.00, 0.76, 0.00)],
@@ -25,9 +27,9 @@ const BONES := {
"Neck": ["Chest", Vector3(0.00, 1.50, 0.00)], "Neck": ["Chest", Vector3(0.00, 1.50, 0.00)],
"Head": ["Neck", Vector3(0.00, 1.58, 0.00)], "Head": ["Neck", Vector3(0.00, 1.58, 0.00)],
"HeadTop": ["Head", Vector3(0.00, 2.00, 0.00)], "HeadTop": ["Head", Vector3(0.00, 2.00, 0.00)],
"Jaw": ["Head", Vector3(0.00, 1.64, 0.16)], "Jaw": ["Head", Vector3(0.00, 1.64, -0.16)],
"LeftEye": ["Head", Vector3(0.12, 1.76, 0.27)], "LeftEye": ["Head", Vector3(0.12, 1.76, -0.27)],
"RightEye": ["Head", Vector3(-0.12, 1.76, 0.27)], "RightEye": ["Head", Vector3(-0.12, 1.76, -0.27)],
"LeftShoulder": ["Chest", Vector3(0.42, 1.50, 0.00)], "LeftShoulder": ["Chest", Vector3(0.42, 1.50, 0.00)],
"LeftUpperArm": ["LeftShoulder", Vector3(0.60, 1.44, 0.00)], "LeftUpperArm": ["LeftShoulder", Vector3(0.60, 1.44, 0.00)],
"LeftLowerArm": ["LeftUpperArm", Vector3(0.60, 1.14, 0.00)], "LeftLowerArm": ["LeftUpperArm", Vector3(0.60, 1.14, 0.00)],
@@ -38,10 +40,10 @@ const BONES := {
"RightHand": ["RightLowerArm", Vector3(-0.60, 0.82, 0.00)], "RightHand": ["RightLowerArm", Vector3(-0.60, 0.82, 0.00)],
"LeftUpperLeg": ["Hips", Vector3(0.21, 0.74, 0.00)], "LeftUpperLeg": ["Hips", Vector3(0.21, 0.74, 0.00)],
"LeftLowerLeg": ["LeftUpperLeg", Vector3(0.21, 0.38, 0.00)], "LeftLowerLeg": ["LeftUpperLeg", Vector3(0.21, 0.38, 0.00)],
"LeftFoot": ["LeftLowerLeg", Vector3(0.21, 0.05, 0.06)], "LeftFoot": ["LeftLowerLeg", Vector3(0.21, 0.05, -0.06)],
"RightUpperLeg": ["Hips", Vector3(-0.21, 0.74, 0.00)], "RightUpperLeg": ["Hips", Vector3(-0.21, 0.74, 0.00)],
"RightLowerLeg": ["RightUpperLeg", Vector3(-0.21, 0.38, 0.00)], "RightLowerLeg": ["RightUpperLeg", Vector3(-0.21, 0.38, 0.00)],
"RightFoot": ["RightLowerLeg", Vector3(-0.21, 0.05, 0.06)], "RightFoot": ["RightLowerLeg", Vector3(-0.21, 0.05, -0.06)],
} }
# Order matters: parents before children. # Order matters: parents before children.
@@ -70,10 +72,10 @@ const PARTS := [
["RightUpperArm", Vector3(-0.58, 1.14, 0.00), Vector3(0.34, 0.78, 0.40), C_SKIN], ["RightUpperArm", Vector3(-0.58, 1.14, 0.00), Vector3(0.34, 0.78, 0.40), C_SKIN],
["LeftUpperLeg", Vector3(0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.40), C_PANTS], ["LeftUpperLeg", Vector3(0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.40), C_PANTS],
["RightUpperLeg", Vector3(-0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.40), C_PANTS], ["RightUpperLeg", Vector3(-0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.40), C_PANTS],
# painted-on face -- thin plates on the front of the head (+Z), rigged to Head # painted-on face -- thin plates on the front of the head (-Z), rigged to Head
["Head", Vector3(0.13, 1.78, 0.28), Vector3(0.09, 0.12, 0.02), C_FACE], ["Head", Vector3(0.13, 1.78, -0.28), Vector3(0.09, 0.12, 0.02), C_FACE],
["Head", Vector3(-0.13, 1.78, 0.28), Vector3(0.09, 0.12, 0.02), C_FACE], ["Head", Vector3(-0.13, 1.78, -0.28), Vector3(0.09, 0.12, 0.02), C_FACE],
["Head", Vector3(0.00, 1.64, 0.28), Vector3(0.32, 0.05, 0.02), C_FACE], ["Head", Vector3(0.00, 1.64, -0.28), Vector3(0.32, 0.05, 0.02), C_FACE],
] ]
# Attachments: id -> [Mesh, local_offset_from_mount_bone, colour] # Attachments: id -> [Mesh, local_offset_from_mount_bone, colour]
@@ -81,17 +83,17 @@ const PARTS := [
const ATTACHMENTS := { const ATTACHMENTS := {
"head_cyber_01": ["head", Vector3(0, 0.04, 0.0), Color(0.20, 0.85, 0.95)], "head_cyber_01": ["head", Vector3(0, 0.04, 0.0), Color(0.20, 0.85, 0.95)],
"hair_mohawk_01": ["mohawk", Vector3(0, 0.0, 0.0), Color(0.90, 0.20, 0.35)], "hair_mohawk_01": ["mohawk", Vector3(0, 0.0, 0.0), Color(0.90, 0.20, 0.35)],
"hair_ponytail_01": ["ponytail", Vector3(0, -0.06, -0.16), Color(0.45, 0.30, 0.15)], "hair_ponytail_01": ["ponytail", Vector3(0, -0.06, 0.16), Color(0.45, 0.30, 0.15)],
"face_glasses_01": ["glasses", Vector3(0, 0.12, 0.16), Color(0.10, 0.10, 0.12)], "face_glasses_01": ["glasses", Vector3(0, 0.12, -0.16), Color(0.10, 0.10, 0.12)],
"chest_bandit_01": ["torso", Vector3(0, -0.12, 0.0), Color(0.55, 0.20, 0.20)], "chest_bandit_01": ["torso", Vector3(0, -0.12, 0.0), Color(0.55, 0.20, 0.20)],
"back_backpack_01": ["box", Vector3(0, -0.06, -0.28), Color(0.30, 0.35, 0.25)], "back_backpack_01": ["box", Vector3(0, -0.06, 0.28), Color(0.30, 0.35, 0.25)],
"pants_cargo_01": ["hips", Vector3(0, -0.04, 0.0), Color(0.30, 0.35, 0.45)], "pants_cargo_01": ["hips", Vector3(0, -0.04, 0.0), Color(0.30, 0.35, 0.45)],
"left_hand_glove_01": ["blob", Vector3(0, 0.0, 0.0), Color(0.15, 0.15, 0.18)], "left_hand_glove_01": ["blob", Vector3(0, 0.0, 0.0), Color(0.15, 0.15, 0.18)],
"right_hand_glove_01": ["blob", Vector3(0, 0.0, 0.0), Color(0.15, 0.15, 0.18)], "right_hand_glove_01": ["blob", Vector3(0, 0.0, 0.0), Color(0.15, 0.15, 0.18)],
"left_foot_boot_01": ["boot", Vector3(0, -0.02, 0.05), Color(0.12, 0.10, 0.09)], "left_foot_boot_01": ["boot", Vector3(0, -0.02, -0.05), Color(0.12, 0.10, 0.09)],
"right_foot_boot_01": ["boot", Vector3(0, -0.02, 0.05), Color(0.12, 0.10, 0.09)], "right_foot_boot_01": ["boot", Vector3(0, -0.02, -0.05), Color(0.12, 0.10, 0.09)],
"accessory_belt_01": ["belt", Vector3(0, 0.74, 0.0), Color(0.20, 0.18, 0.12)], "accessory_belt_01": ["belt", Vector3(0, 0.74, 0.0), Color(0.20, 0.18, 0.12)],
"accessory_necklace_01": ["necklace", Vector3(0, 1.34, 0.10), Color(0.85, 0.75, 0.20)], "accessory_necklace_01": ["necklace", Vector3(0, 1.34, -0.10), Color(0.85, 0.75, 0.20)],
} }
var _fail := 0 var _fail := 0