@tool extends RefCounted # Validator: Scene Structure # # Checks: # - Scene root is a Node3D (or subtype) # - Required nodes present: WorldEnvironment, LightmapGI # - Required groups present: ct_spawn, t_spawn, buy_zone, bomb_site, cubemap_origin # - No duplicate groups on different nodes # - Node naming follows convention (PascalCase, no spaces) const REQUIRED_GROUPS := [ "ct_spawn", "t_spawn", "buy_zone", "bomb_site", "cubemap_origin", ] const DYNAMIC_BAKE_MODE := 0 # light_bake_mode = 0 means dynamic func validate(scene_root: Node, scene_path: String) -> Dictionary: var errors: Array[String] = [] var warnings: Array[String] = [] # 1. Root node type if not scene_root is Node3D: errors.append("Scene root must inherit Node3D, got: " + scene_root.get_class()) else: print(" ✓ Root node: ", scene_root.name, " (", scene_root.get_class(), ")") # 2. Required nodes _check_required_node(scene_root, "WorldEnvironment", errors, "WorldEnvironment") _check_required_node(scene_root, "LightmapGI", errors, "LightmapGI") # 3. Node naming convention _check_naming(scene_root, warnings, []) # 4. Required groups var found_groups: Dictionary = {} # group → [node names] _collect_groups(scene_root, found_groups) for group in REQUIRED_GROUPS: if found_groups.has(group): var nodes := found_groups[group] as Array print(" ✓ Group \"", group, "\" → ", nodes[0], " (", nodes.size(), " node(s))") if nodes.size() > 1: warnings.append("Group \"%s\" assigned to %d nodes — only one expected" % [group, nodes.size()]) else: errors.append("Missing required group: \"" + group + "\" — no node in the map has this group") # 5. Check for orphan / stray groups (as hint) var all_groups := _get_all_group_names(found_groups) for g in all_groups: if g not in REQUIRED_GROUPS and g.begins_with("res://"): warnings.append("Unexpected group with resource path pattern: \"" + g + "\"") return { "pass": errors.is_empty(), "errors": errors, "warnings": warnings, } func _check_required_node(root: Node, node_name: String, errors: Array, type_hint: String) -> void: var found: Array[Node] = [] _find_nodes_by_type(root, type_hint, found) if found.is_empty(): errors.append("Missing required %s — map needs a %s node named \"%s\"" % [type_hint, type_hint, node_name]) return # Check naming if found[0].name != node_name: errors.append("%s node found but not named \"%s\" — got \"%s\", rename to match convention" % [type_hint, node_name, found[0].name]) else: print(" ✓ ", node_name, " (", type_hint, ")") func _find_nodes_by_type(root: Node, expected_type: String, output) -> void: if root.get_class() == expected_type: output.append(root) for child in root.get_children(): _find_nodes_by_type(child, expected_type, output) func _collect_groups(root: Node, groups: Dictionary) -> void: for g in root.get_groups(): # Godot auto-adds "__builtins" and similar — skip those if g.begins_with("__"): continue if not groups.has(g): groups[g] = [] (groups[g] as Array).append(root.name) for child in root.get_children(): _collect_groups(child, groups) func _get_all_group_names(groups: Dictionary) -> Array: var names: Array = [] for key in groups: names.append(key) return names func _check_naming(node: Node, warnings: Array, path: Array) -> void: var path_str: String = "/".join(path + [node.name]) if not path.is_empty() else node.name # Check for spaces in name if " " in node.name: warnings.append("Node name contains spaces: \"" + path_str + "\" — use PascalCase instead") # Check for lowercase start on non-leaf nodes (leaf instances may have instance names) if node.get_child_count() > 0 and node.name.length() > 0: var first: String = String(node.name)[0] if first == first.to_lower() and first != first.to_upper(): warnings.append("Node name should start uppercase (PascalCase): \"" + path_str + "\"") for child in node.get_children(): _check_naming(child, warnings, path + [node.name])