Files
tactical-shooter/client/tools/validate_map/validate_lights.gd
T
shawn e7299b17e9 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)
2026-07-02 17:39:22 -04:00

152 lines
5.1 KiB
GDScript

@tool
extends RefCounted
# Validator: Light Count & Lightmap Status
#
# Checks:
# - Dynamic (non-baked) OmniLight3D + SpotLight3D count ≤ 4
# - DirectionalLight3D is allowed (counts as 1 toward dynamic budget if not baked)
# - WorldEnvironment has tonemap/ambient configured
# - LightmapGI present and baked (light_data != null)
# - ReflectionProbe count and settings
# - Light bake modes should be 2 (Static) for all static geometry lights
const MAX_DYNAMIC_LIGHTS := 4 # Omni + Spot + unbaked Directional
func validate(scene_root: Node, scene_path: String) -> Dictionary:
var errors: Array[String] = []
var warnings: Array[String] = []
var all_lights: Array[Node] = []
_collect_light_nodes(scene_root, all_lights)
var dynamic_count := 0
var baked_count := 0
var directional_count := 0
var omni_count := 0
var spot_count := 0
for light in all_lights:
var bake_mode := -1
if light.has_method("get_light_bake_mode"):
bake_mode = light.get("light_bake_mode") as int
var is_dynamic := bake_mode == 0 # BAKE_DISABLED
if light is DirectionalLight3D:
directional_count += 1
if is_dynamic:
dynamic_count += 1
else:
baked_count += 1
elif light is OmniLight3D:
omni_count += 1
if is_dynamic:
dynamic_count += 1
else:
baked_count += 1
elif light is SpotLight3D:
spot_count += 1
if is_dynamic:
dynamic_count += 1
else:
baked_count += 1
print(" Light breakdown:")
print(" DirectionalLight3D: ", directional_count)
print(" OmniLight3D: ", omni_count)
print(" SpotLight3D: ", spot_count)
print(" Dynamic (non-baked): ", dynamic_count)
print(" Baked: ", baked_count)
print("")
# Check dynamic light budget
if dynamic_count > MAX_DYNAMIC_LIGHTS:
errors.append("Too many dynamic lights: %d (max %d) — set light_bake_mode = 2 (Static) on static lights to reduce" % [dynamic_count, MAX_DYNAMIC_LIGHTS])
else:
print(" ✓ Dynamic light count: ", dynamic_count, " (limit: ", MAX_DYNAMIC_LIGHTS, ")")
# Check LightmapGI
var lightmaps: Array[LightmapGI] = []
_find_nodes_by_type(scene_root, "LightmapGI", lightmaps)
if lightmaps.is_empty():
errors.append("No LightmapGI node found — maps should include a LightmapGI for baked global illumination")
else:
var lm := lightmaps[0]
if lightmaps.size() > 1:
warnings.append("Multiple LightmapGI nodes found (%d) — only one recommended" % lightmaps.size())
if lm.light_data != null:
print(" ✓ LightmapGI baked (light_data present)")
else:
warnings.append("LightmapGI node found but NOT BAKED — light_data is null. Select LightmapGI → 'Bake Lightmap' button in editor")
# Quality settings check
print(" Quality: ", lm.quality, " (0=Low, 1=Med, 2=High, 3=Ultra)")
print(" Bounces: ", lm.bounces)
print(" Texel Scale: ", lm.texel_scale)
print(" Max Texture Size: ", lm.max_texture_size)
if lm.bounces < 2:
warnings.append("LightmapGI bounces set to %d — recommend a minimum of 2 for accurate indirect lighting" % lm.bounces)
if lm.max_texture_size < 512:
warnings.append("LightmapGI max_texture_size is %d — may cause visible seams, recommend ≥ 512" % lm.max_texture_size)
# Check WorldEnvironment
var envs: Array[WorldEnvironment] = []
_find_nodes_by_type(scene_root, "WorldEnvironment", envs)
if envs.is_empty():
errors.append("No WorldEnvironment node found — maps need a WorldEnvironment for ambient lighting and sky")
else:
var env := envs[0].environment
if env == null:
warnings.append("WorldEnvironment node has no Environment resource assigned")
else:
print(" ✓ WorldEnvironment configured")
# ReflectionProbe check
var probes: Array[ReflectionProbe] = []
_find_nodes_by_type(scene_root, "ReflectionProbe", probes)
if probes.is_empty():
warnings.append("No ReflectionProbe found — interior maps benefit from at least one ReflectionProbe for specular reflections")
else:
print(" ReflectionProbes: ", probes.size())
for p in probes:
if p.max_distance < 5.0:
warnings.append("ReflectionProbe \"%s\" max_distance is %.1f — may be too short for interior spaces" % [p.name, p.max_distance])
# Warn on lights with shadows enabled that aren't baked
for light in all_lights:
if light.has_method("is_shadow_enabled") and light.get("shadow_enabled") == true:
var bake_mode := light.get("light_bake_mode") as int
if bake_mode == 0:
var light_name := light.name
var light_type := light.get_class()
warnings.append("Dynamic light has shadows enabled: \"%s\" (%s) — shadows on dynamic lights cost performance" % [light_name, light_type])
return {
"pass": errors.is_empty(),
"errors": errors,
"warnings": warnings,
}
func _collect_light_nodes(node: Node, result: Array) -> void:
if node is DirectionalLight3D or node is OmniLight3D or node is SpotLight3D:
result.append(node)
for child in node.get_children():
_collect_light_nodes(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)