diff --git a/scripts/avatar/avatar_catalog.gd b/scripts/avatar/avatar_catalog.gd index 8264b24..898bc85 100644 --- a/scripts/avatar/avatar_catalog.gd +++ b/scripts/avatar/avatar_catalog.gd @@ -156,8 +156,9 @@ func validate_loadout(loadout: AvatarLoadout) -> Dictionary: ## Compute a hash of the loadout for change detection. -## Returns a uint32 hash. -static func compute_loadout_hash(loadout: AvatarLoadout) -> uint32: +## Returns a 32-bit hash value (GDScript has no fixed-width uint type — int +## is 64-bit signed, the value is just constrained to 32 bits of range). +static func compute_loadout_hash(loadout: AvatarLoadout) -> int: var hasher = HashingContext.new() hasher.start(HashingContext.HASH_SHA256) diff --git a/scripts/avatar/avatar_customization_ui.gd b/scripts/avatar/avatar_customization_ui.gd index 721be73..1602d74 100644 --- a/scripts/avatar/avatar_customization_ui.gd +++ b/scripts/avatar/avatar_customization_ui.gd @@ -326,6 +326,11 @@ func _populate_items() -> void: # Slot selection # --------------------------------------------------------------------------- +## Handle a slot button press. +func _on_slot_button_pressed(slot_id: String) -> void: + _select_slot(slot_id) + + ## Select a slot and refresh the item list. func _select_slot(slot_id: String) -> void: _selected_slot = slot_id diff --git a/scripts/avatar/avatar_loader.gd b/scripts/avatar/avatar_loader.gd index ad18e55..f104e85 100644 --- a/scripts/avatar/avatar_loader.gd +++ b/scripts/avatar/avatar_loader.gd @@ -305,28 +305,26 @@ func _parse_glb_as_avatar(glb_data: PackedByteArray, path: String) -> Node3D: avatar.add_child(attachments_container) # Parse the GLB and extract skeleton + meshes. - # Godot's GLTFState allows custom parsing. var gltf_document := GLTFDocument.new() var gltf_state := GLTFState.new() - var parse_result := gltf_document.parse(glb_data, gltf_state, "", "") + var parse_result := gltf_document.append_from_buffer(glb_data, "", gltf_state) if parse_result != OK: - push_error("GLB parse error: %s" % Error.parse_error(parse_result)) + push_error("GLB parse error: %s" % error_string(parse_result)) avatar.queue_free() return null - # Export the scene from GLTFState. - var root_nodes := gltf_state.get_nodes_as_resources() - if root_nodes.is_empty(): - push_error("No root nodes in GLB: %s" % path) + # Generate the actual scene tree from the parsed GLTF state. + var scene_root := gltf_document.generate_scene(gltf_state) + if scene_root == null: + push_error("Failed to generate scene from GLB: %s" % path) avatar.queue_free() return null - # Merge GLB nodes into our avatar structure. - for node_resource in root_nodes: - if node_resource is Node3D: - _merge_node_into_avatar(node_resource, avatar, body_meshes, - attach_points, attachments_container) + # Merge GLB nodes into our avatar structure (recurses into children). + if scene_root is Node3D: + _merge_node_into_avatar(scene_root, avatar, body_meshes, + attach_points, attachments_container) return avatar @@ -338,22 +336,21 @@ func _parse_glb_as_attachment(glb_data: PackedByteArray, path: String, var gltf_document := GLTFDocument.new() var gltf_state := GLTFState.new() - var parse_result := gltf_document.parse(glb_data, gltf_state, "", "") + var parse_result := gltf_document.append_from_buffer(glb_data, "", gltf_state) if parse_result != OK: - push_error("GLB parse error: %s" % Error.parse_error(parse_result)) + push_error("GLB parse error: %s" % error_string(parse_result)) return null - var root_nodes := gltf_state.get_nodes_as_resources() - if root_nodes.is_empty(): - push_error("No root nodes in attachment GLB: %s" % path) + var scene_root := gltf_document.generate_scene(gltf_state) + if scene_root == null: + push_error("Failed to generate scene from attachment GLB: %s" % path) return null var attachment := Node3D.new() attachment.name = "%s_%s" % [slot_id, path.get_file().get_basename()] - for node_resource in root_nodes: - if node_resource is Node3D: - attachment.add_child(node_resource.duplicate()) + if scene_root is Node3D: + attachment.add_child(scene_root.duplicate()) return attachment @@ -392,6 +389,13 @@ func _merge_node_into_avatar(node: Node3D, avatar: Node3D, var dst_parent_idx := dst_skel.find_bone(parent_name) if dst_parent_idx >= 0: dst_skel.set_bone_parent(new_idx, dst_parent_idx) + # Skinned meshes are commonly children of the skeleton node in a GLB + # export, not siblings — fall through to the recursion below instead + # of returning, so they still get merged in. + for child in src_skel.get_children(): + if child is Node3D: + _merge_node_into_avatar(child, avatar, body_meshes, + attach_points, attachments_container) return # Identify attachment point nodes (empty nodes named attach_*). @@ -468,18 +472,16 @@ func _retarget_skin(attachment_node: Node3D) -> void: if mesh == null: continue - # Handle MultiMeshInstance3D. - if mesh is MultiMesh: - # MultiMesh doesn't have skin weights per-vertex in the same way. - continue - + # Note: MeshInstance3D.mesh is always a Mesh, never a MultiMesh — + # MultiMesh skinning goes through MultiMeshInstance3D.multimesh, + # a completely separate node/property, not reachable here. if mesh is ArrayMesh: var arr_mesh := mesh as ArrayMesh var surface_count := arr_mesh.get_surface_count() for surface_idx in surface_count: var arrays := arr_mesh.surface_get_arrays(surface_idx) - var joints_array := arrays[Mesh.ARRAY_JOINTS] as PackedInt32Array + var joints_array := arrays[Mesh.ARRAY_BONES] as PackedInt32Array var weights_array := arrays[Mesh.ARRAY_WEIGHTS] as PackedFloat32Array if joints_array.is_empty(): @@ -494,10 +496,10 @@ func _retarget_skin(attachment_node: Node3D) -> void: new_joints.append(0) else: var bone_name := skeleton.get_bone_name(old_joint) - var new_idx := _bone_index_map.get(bone_name, 0) + var new_idx: int = _bone_index_map.get(bone_name, 0) new_joints.append(new_idx) - arrays[Mesh.ARRAY_JOINTS] = new_joints + arrays[Mesh.ARRAY_BONES] = new_joints arr_mesh.surface_set_arrays(surface_idx, arrays) diff --git a/scripts/avatar/avatar_slot_registry.gd b/scripts/avatar/avatar_slot_registry.gd index e466ba2..c2b59f4 100644 --- a/scripts/avatar/avatar_slot_registry.gd +++ b/scripts/avatar/avatar_slot_registry.gd @@ -69,17 +69,20 @@ class SlotDefinition: is_body_part = p_is_body_part -# Slot metadata lookup tables (indexed by SLOT_IDS position). +# Slot metadata lookup tables (indexed by SLOT_IDS position). Static: every +# caller uses AvatarSlotRegistry.method(...) directly on the class, never on +# an instance, so this data has to be built once at class-load time via +# _static_init() rather than an instance _init() that's never invoked. # Each entry: { category, mount_bones, exclusive, is_body_part } -var _slot_data: Dictionary = {} -var _slot_definitions: Array[SlotDefinition] = [] +static var _slot_data: Dictionary = {} +static var _slot_definitions: Array[SlotDefinition] = [] -func _init() -> void: +static func _static_init() -> void: _build_registry() -func _build_registry() -> void: +static func _build_registry() -> void: _slot_data = { "head": { "category": "body_part",