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