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,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)
|
||||
Reference in New Issue
Block a user