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
@@ -0,0 +1,114 @@
@tool
extends RefCounted
# Validator: Polygon Count
#
# Checks:
# - Total mesh triangle count across all MeshInstance3D nodes ≤ 50K
# - Per-mesh breakdown printed for debugging
# - Warns on individual meshes > 5K triangles
const MAX_TOTAL_TRIANGLES := 50000
const PER_MESH_WARN_THRESHOLD := 5000
func validate(scene_root: Node, scene_path: String) -> Dictionary:
var errors: Array[String] = []
var warnings: Array[String] = []
var mesh_instances: Array[MeshInstance3D] = []
_collect_mesh_instances(scene_root, mesh_instances)
var total_triangles := 0
var mesh_breakdown: Array[Dictionary] = []
for mi in mesh_instances:
var mesh: Mesh = mi.mesh
if mesh == null:
continue
var tri_count := _count_triangles(mesh)
total_triangles += tri_count
var entry := {
"node": mi.name,
"path": _node_path_to_string(scene_root, mi),
"triangles": tri_count,
"mesh_type": mesh.get_class(),
}
mesh_breakdown.append(entry)
if tri_count > PER_MESH_WARN_THRESHOLD:
warnings.append("High-poly mesh: \"%s\" (%s) — %d triangles (limit: %d per mesh recommend)" % [
mi.name, entry["path"], tri_count, PER_MESH_WARN_THRESHOLD])
# Print breakdown
print(" Meshes scanned: ", mesh_instances.size())
print(" Total triangles: ", total_triangles)
print(" Budget: ", MAX_TOTAL_TRIANGLES)
print("")
# Sort and show top contributors
mesh_breakdown.sort_custom(func(a, b): return a["triangles"] > b["triangles"])
var top := mesh_breakdown.slice(0, min(10, mesh_breakdown.size()))
if not top.is_empty():
print(" Top meshes by triangle count:")
for m in top:
print(" %6d %s (%s)" % [m["triangles"], m["node"], m["mesh_type"]])
if mesh_breakdown.size() > 10:
print(" ... and ", mesh_breakdown.size() - 10, " more")
if total_triangles > MAX_TOTAL_TRIANGLES:
errors.append("Map exceeds %d triangle budget: %d total (%.1f%% over budget)" % [
MAX_TOTAL_TRIANGLES, total_triangles,
float(total_triangles - MAX_TOTAL_TRIANGLES) / MAX_TOTAL_TRIANGLES * 100.0])
else:
var pct := float(total_triangles) / MAX_TOTAL_TRIANGLES * 100.0
print(" ✓ Within budget (%.1f%% of %d)" % [pct, MAX_TOTAL_TRIANGLES])
# Warn if mesh_instances is empty (suspicious map with no geometry)
if mesh_instances.is_empty():
warnings.append("No MeshInstance3D nodes found in scene — map may be empty or using unsupported geometry type")
return {
"pass": errors.is_empty(),
"errors": errors,
"warnings": warnings,
}
func _collect_mesh_instances(node: Node, result: Array) -> void:
if node is MeshInstance3D:
result.append(node)
for child in node.get_children():
_collect_mesh_instances(child, result)
func _count_triangles(mesh: Mesh) -> int:
var total := 0
for i in mesh.get_surface_count():
var arrays := mesh.surface_get_arrays(i)
if arrays.is_empty():
continue
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
if indices and indices.size() > 0:
total += indices.size() / 3
else:
# Non-indexed mesh — count vertices / 3 as approximation
var verts := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
if verts:
total += verts.size() / 3
return total
func _node_path_to_string(root: Node, node: Node) -> String:
var full_path: String = node.get_path_to(root)
# full_path is a NodePath string like "../NodeA/NodeB" — split and drop the root
var parts := full_path.split("/")
# Remove the first segment (the "../" back-reference) and last if empty
var effective: Array[String] = []
for p in parts:
if p.is_empty() or p == "." or p.begins_with(".."):
continue
effective.append(p)
return "/".join(effective)