e7299b17e9
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)
115 lines
3.6 KiB
GDScript
115 lines
3.6 KiB
GDScript
@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)
|