import math from pathlib import Path import unreal MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test" FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass" EXPECTED_FOLIAGE_COUNTS = { "trees": 96, "shrubs": 220, "grass": 420, } CRITICAL_CLEARANCE_CM = { "trees": 5200.0, "shrubs": 3400.0, "grass": 1250.0, } CORRIDOR_CLEARANCE_CM = { "trees": 3600.0, "shrubs": 2100.0, "grass": 850.0, } CORRIDOR_SAMPLE_SPACING_CM = 1600.0 CRITICAL_ACTOR_LABELS = { "AGR_DemoPlayerStart", "AGR_DemoWoodResource_01", "AGR_DemoFiberResource_01", "AGR_DemoCampfire_01", "AGR_DemoPrimitiveShelter_01", "AGR_DemoRabbitWildlife_01", "AGR_GZ_FreshWaterSource_01", "AGR_GZ_Wood_CoastalScrub_01", "AGR_GZ_Wood_CoastalScrub_02", "AGR_GZ_Wood_Hillside_03", "AGR_GZ_Fiber_Grassland_01", "AGR_GZ_Fiber_Grassland_02", "AGR_GZ_Fiber_Scrub_03", "AGR_GZ_Fiber_DrainageCandidate_04", "AGR_GZ_EdiblePlant_CoastalScrub_01", "AGR_GZ_EdiblePlant_Grassland_02", "AGR_GZ_EdiblePlant_DrainageCandidate_03", "AGR_GZ_Stone_Slope_01", "AGR_GZ_Stone_Slope_02", "AGR_GZ_Stone_ExposedTerrain_03", "AGR_GZ_Stone_ValleyEdge_04", } SIGHTLINE_PAIRS = [ ("AGR_DemoPlayerStart", "AGR_DemoWoodResource_01"), ("AGR_DemoPlayerStart", "AGR_DemoFiberResource_01"), ("AGR_DemoPlayerStart", "AGR_DemoCampfire_01"), ("AGR_DemoPlayerStart", "AGR_DemoPrimitiveShelter_01"), ("AGR_DemoPlayerStart", "AGR_DemoRabbitWildlife_01"), ("AGR_DemoPlayerStart", "AGR_GZ_FreshWaterSource_01"), ] def get_actor_label(actor): try: return actor.get_actor_label() except Exception: return actor.get_name() def distance_2d(a, b): return math.hypot(a.x - b.x, a.y - b.y) def transform_location(transform_result): transform = transform_result[0] if isinstance(transform_result, tuple) else transform_result if hasattr(transform, "translation"): return transform.translation try: return transform.get_editor_property("translation") except Exception: return transform.get_location() def instance_locations(component): locations = [] for index in range(component.get_instance_count()): locations.append(transform_location(component.get_instance_transform(index, True))) return locations def sampled_corridor_points(start, end): length_cm = distance_2d(start, end) sample_count = max(1, int(length_cm / CORRIDOR_SAMPLE_SPACING_CM)) points = [] for index in range(1, sample_count): alpha = index / sample_count points.append( unreal.Vector( start.x + ((end.x - start.x) * alpha), start.y + ((end.y - start.y) * alpha), 0.0, ) ) return points def main(): if not unreal.EditorLevelLibrary.load_level(MAP_PATH): raise RuntimeError(f"Could not load map: {MAP_PATH}") actors = unreal.EditorLevelLibrary.get_all_level_actors() actors_by_label = {get_actor_label(actor): actor for actor in actors} failures = [] missing_labels = sorted(label for label in CRITICAL_ACTOR_LABELS if label not in actors_by_label) if missing_labels: failures.append("missing critical actor label(s): " + ", ".join(missing_labels)) foliage = actors_by_label.get(FOLIAGE_LABEL) if not foliage: failures.append(f"missing {FOLIAGE_LABEL}") else: foliage_components = { "trees": foliage.get_editor_property("tree_instances"), "shrubs": foliage.get_editor_property("shrub_instances"), "grass": foliage.get_editor_property("grass_instances"), } foliage_locations = { family: instance_locations(component) for family, component in foliage_components.items() } for family, expected_count in EXPECTED_FOLIAGE_COUNTS.items(): actual_count = len(foliage_locations[family]) if actual_count != expected_count: failures.append(f"{family} expected {expected_count}, got {actual_count}") critical_points = { label: actors_by_label[label].get_actor_location() for label in CRITICAL_ACTOR_LABELS if label in actors_by_label } for family, locations in foliage_locations.items(): clearance = CRITICAL_CLEARANCE_CM[family] for label, point in critical_points.items(): nearest = min((distance_2d(location, point) for location in locations), default=999999.0) if nearest < clearance: failures.append( f"{family} nearest to {label} is {nearest:.0f}cm, below {clearance:.0f}cm clearance" ) for start_label, end_label in SIGHTLINE_PAIRS: start_actor = actors_by_label.get(start_label) end_actor = actors_by_label.get(end_label) if not start_actor or not end_actor: continue corridor_points = sampled_corridor_points( start_actor.get_actor_location(), end_actor.get_actor_location(), ) for family, locations in foliage_locations.items(): clearance = CORRIDOR_CLEARANCE_CM[family] for point in corridor_points: nearest = min((distance_2d(location, point) for location in locations), default=999999.0) if nearest < clearance: failures.append( f"{family} nearest to {start_label}->{end_label} corridor is " f"{nearest:.0f}cm, below {clearance:.0f}cm clearance" ) break project_root = Path(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir())) docs = project_root / "Docs" / "Terrain" / "GroundZeroDensitySightlineTuning.md" roadmap = project_root / "AGRARIAN_DEVELOPMENT_ROADMAP.md" for path, snippets in { docs: ["Ground Zero Density And Sightline Tuning", "First-Look Sightlines"], roadmap: ["[x] Add density and sightline tuning"], }.items(): text = path.read_text(encoding="utf-8") for snippet in snippets: if snippet not in text: failures.append(f"{path} missing snippet: {snippet}") if failures: raise RuntimeError("Ground Zero density/sightline verification failed: " + "; ".join(failures)) unreal.log( "Ground Zero density/sightline verification complete: " f"{len(CRITICAL_ACTOR_LABELS)} critical labels, " f"{len(SIGHTLINE_PAIRS)} sightline corridor(s), " f"{EXPECTED_FOLIAGE_COUNTS['trees']} trees, " f"{EXPECTED_FOLIAGE_COUNTS['shrubs']} shrubs, " f"{EXPECTED_FOLIAGE_COUNTS['grass']} grass clumps." ) main()