Phase 7: netfox + godot-jolt stack upgrade
Stack installed: - netfox v1.35.3 (core + extras + noray + internals) - godot-jolt v0.16.0-stable Architecture: - Server: ENet transport (works headless, no netfox deps) - Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator) New/modified: - docs/migration-netfox-plan.md — migration architecture - scripts/network/network_manager.gd — netfox-aware ENet fallback - scripts/network/player.gd — clean base player - client/characters/player_netfox.gd — rollback player w/ WeaponManager - client/characters/input/player_net_input.gd — BaseNetInput subclass - client/characters/character/fps_character_controller.gd — netfox input feed - client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager - client/scripts/round_replicator.gd — client-side round state bridge - server/scripts/round_manager.gd — improved state machine - server/scripts/plugin_api/plugin_manager.gd — refined plugin system - config: enemy_tag, ally_tag for meatball targeting Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
@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])
|
||||
Reference in New Issue
Block a user