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:
2026-07-02 17:38:50 -04:00
parent e2dc429caa
commit e7299b17e9
3237 changed files with 523530 additions and 18 deletions
+122
View File
@@ -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])