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)
158 lines
6.1 KiB
GDScript
158 lines
6.1 KiB
GDScript
@tool
|
||
extends RefCounted
|
||
|
||
# Validator: Texture Size
|
||
#
|
||
# Checks:
|
||
# - All textures referenced by materials in the scene are ≤ 1024×1024
|
||
# - Checks StandardMaterial3D, ORMMaterial3D, and ShaderMaterial textures
|
||
# - Warns on textures > 512×512 (suggested for secondary surfaces)
|
||
# - Skips LightmapGI textures (baked lightmaps are handled separately)
|
||
|
||
const MAX_TEXTURE_SIZE := 1024
|
||
const WARN_TEXTURE_SIZE := 512
|
||
|
||
|
||
func validate(scene_root: Node, scene_path: String) -> Dictionary:
|
||
var errors: Array[String] = []
|
||
var warnings: Array[String] = []
|
||
|
||
var inspected := 0
|
||
var oversized: Array[String] = []
|
||
|
||
# Collect all MeshInstance3D nodes and their materials
|
||
var mesh_instances: Array[MeshInstance3D] = []
|
||
_collect_mesh_instances(scene_root, mesh_instances)
|
||
|
||
# Track visited textures to avoid duplicate reports
|
||
var visited_textures: Dictionary = {}
|
||
|
||
for mi in mesh_instances:
|
||
var mesh: Mesh = mi.mesh
|
||
if mesh == null:
|
||
continue
|
||
|
||
for surf_idx in mesh.get_surface_count():
|
||
var mat := mesh.surface_get_material(surf_idx)
|
||
if mat == null:
|
||
# Check override material on MeshInstance3D
|
||
mat = mi.get_surface_override_material(surf_idx)
|
||
if mat == null:
|
||
# Check material_override on MeshInstance3D
|
||
mat = mi.material_override
|
||
if mat == null:
|
||
continue
|
||
|
||
_check_material_textures(mat, visited_textures, errors, warnings, inspected)
|
||
|
||
# Check material_override if no per-surface materials
|
||
if mesh_instances.is_empty():
|
||
_find_materials_on_node(scene_root, visited_textures, errors, warnings, inspected)
|
||
|
||
# Check LightmapGI light_data texture sizes separately
|
||
var lightmaps: Array[LightmapGI] = []
|
||
_find_nodes_by_type(scene_root, "LightmapGI", lightmaps)
|
||
for lm in lightmaps:
|
||
if lm.light_data != null:
|
||
var lightmap_tex = lm.light_data.get_texture()
|
||
if lightmap_tex != null and not visited_textures.has(lightmap_tex.resource_path):
|
||
visited_textures[lightmap_tex.resource_path] = true
|
||
var size := _get_texture_size(lightmap_tex)
|
||
if size > 0:
|
||
print(" Lightmap texture: ", lightmap_tex.resource_path, " — ", size, "×")
|
||
if size > MAX_TEXTURE_SIZE:
|
||
warnings.append("Lightmap texture exceeds %d: %s (%d×)" % [MAX_TEXTURE_SIZE, lightmap_tex.resource_path, size])
|
||
|
||
print("")
|
||
print(" Textures inspected: ", inspected)
|
||
if oversized.is_empty():
|
||
print(" ✓ All textures within ", MAX_TEXTURE_SIZE, "×", MAX_TEXTURE_SIZE, " limit")
|
||
else:
|
||
for o in oversized:
|
||
print(" ✖ ", o)
|
||
|
||
return {
|
||
"pass": errors.is_empty(),
|
||
"errors": errors,
|
||
"warnings": warnings,
|
||
}
|
||
|
||
|
||
func _check_material_textures(mat: Material, visited: Dictionary, errors: Array, warnings: Array, inspected: int) -> void:
|
||
if mat is StandardMaterial3D:
|
||
var smat := mat as StandardMaterial3D
|
||
_check_single_texture(smat.albedo_texture, "albedo", smat.resource_path, visited, errors, warnings, inspected)
|
||
_check_single_texture(smat.normal_texture, "normal", smat.resource_path, visited, errors, warnings, inspected)
|
||
_check_single_texture(smat.roughness_texture, "roughness", smat.resource_path, visited, errors, warnings, inspected)
|
||
_check_single_texture(smat.metallic_texture, "metallic", smat.resource_path, visited, errors, warnings, inspected)
|
||
_check_single_texture(smat.emission_texture, "emission", smat.resource_path, visited, errors, warnings, inspected)
|
||
_check_single_texture(smat.ao_texture, "ambient_occlusion", smat.resource_path, visited, errors, warnings, inspected)
|
||
|
||
elif mat is ORMMaterial3D:
|
||
var orm := mat as ORMMaterial3D
|
||
_check_single_texture(orm.albedo_texture, "albedo", orm.resource_path, visited, errors, warnings, inspected)
|
||
_check_single_texture(orm.normal_texture, "normal", orm.resource_path, visited, errors, warnings, inspected)
|
||
_check_single_texture(orm.orm_texture, "ORM", orm.resource_path, visited, errors, warnings, inspected)
|
||
_check_single_texture(orm.emission_texture, "emission", orm.resource_path, visited, errors, warnings, inspected)
|
||
|
||
elif mat is ShaderMaterial:
|
||
var sm := mat as ShaderMaterial
|
||
# ShaderMaterial textures are set via shader params — check all Texture2D params
|
||
var param_list = sm.get_shader_parameter_list() if sm.shader else []
|
||
for param in param_list:
|
||
var val = sm.get_shader_parameter(param)
|
||
if val is Texture2D:
|
||
_check_single_texture(val as Texture2D, param, sm.resource_path, visited, errors, warnings, inspected)
|
||
|
||
|
||
func _check_single_texture(tex: Texture2D, slot_name: String, material_path: String, visited: Dictionary, errors: Array, warnings: Array, inspected: int) -> void:
|
||
if tex == null:
|
||
return
|
||
var path := tex.resource_path
|
||
if path.is_empty():
|
||
path = tex.name # built-in/placeholder texture
|
||
if visited.has(path):
|
||
return
|
||
visited[path] = true
|
||
inspected += 1
|
||
|
||
var size := _get_texture_size(tex)
|
||
if size <= 0:
|
||
return
|
||
|
||
var label := "%s/%s" % [material_path.get_file(), slot_name]
|
||
|
||
if size > MAX_TEXTURE_SIZE:
|
||
errors.append("Texture exceeds %d×%d: %s (%s) — %d× (path: %s)" % [MAX_TEXTURE_SIZE, MAX_TEXTURE_SIZE, label, path, size, path])
|
||
elif size > WARN_TEXTURE_SIZE:
|
||
warnings.append("Texture > %d×%d: %s (%s) — %d× (consider downscaling)" % [WARN_TEXTURE_SIZE, WARN_TEXTURE_SIZE, label, path, size])
|
||
|
||
|
||
func _get_texture_size(tex: Texture2D) -> int:
|
||
# Return the larger dimension
|
||
return max(tex.get_width(), tex.get_height())
|
||
|
||
|
||
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 _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 _find_materials_on_node(node: Node, visited: Dictionary, errors: Array, warnings: Array, inspected: int) -> void:
|
||
# Fallback: walk all nodes and check their material_override
|
||
for child in node.get_children():
|
||
if child.has_method("get_material_override"):
|
||
var mat = child.get("material_override")
|
||
if mat != null and mat is Material:
|
||
_check_material_textures(mat, visited, errors, warnings, inspected)
|
||
_find_materials_on_node(child, visited, errors, warnings, inspected)
|