import math from pathlib import Path import unreal MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test" WATER_SOURCE_LABEL = "AGR_GZ_FreshWaterSource_01" EXPECTED_WATER_LOCATION = unreal.Vector(-7200.0, 10400.0, 0.0) MAX_WATER_LOCATION_DISTANCE_CM = 500.0 MAX_DRESSING_DISTANCE_CM = 3500.0 FRESH_WATER_MATERIAL = "/Game/Agrarian/Materials/M_AGR_GZ_FreshWater" STONE_MATERIAL = "/Game/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone" REQUIRED_COMPONENTS = { "Mesh", "WaterSurfaceProxy", "StoneBankProxy", "CollectMarkerProxy", } REQUIRED_NEARBY_DRESSING = { "AGR_GZ_EnvVar_Water_Surface_01", "AGR_GZ_EnvVar_Water_Bank_01", "AGR_GZ_EnvVar_Water_Reed_01", "AGR_GZ_EnvVar_Water_Reed_02", } def get_actor_label(actor): try: return actor.get_actor_label() except Exception: return actor.get_name() def material_path(material): if not material: return "" return material.get_path_name().split(".", 1)[0] def distance_2d(a, b): return math.hypot(a.x - b.x, a.y - b.y) def component_by_name(actor, component_name): for component in actor.get_components_by_class(unreal.StaticMeshComponent): if component.get_name() == component_name: return component return None 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() failures = [] water_sources = [actor for actor in actors if get_actor_label(actor) == WATER_SOURCE_LABEL] if len(water_sources) != 1: raise RuntimeError(f"Expected exactly one {WATER_SOURCE_LABEL}, found {len(water_sources)}") water_source = water_sources[0] if not isinstance(water_source, unreal.AgrarianWaterSource): failures.append(f"{WATER_SOURCE_LABEL} is not an AgrarianWaterSource") components = { component_name: component_by_name(water_source, component_name) for component_name in REQUIRED_COMPONENTS } for component_name, component in components.items(): if component is None: failures.append(f"{WATER_SOURCE_LABEL} missing {component_name}") water_surface = components.get("WaterSurfaceProxy") if water_surface: if material_path(water_surface.get_material(0)) != FRESH_WATER_MATERIAL: failures.append("WaterSurfaceProxy does not use the freshwater material") scale = water_surface.get_editor_property("relative_scale3d") if scale.x < 2.0 or scale.y < 1.3: failures.append(f"WaterSurfaceProxy scale is too small: {scale}") collect_marker = components.get("CollectMarkerProxy") if collect_marker and material_path(collect_marker.get_material(0)) != FRESH_WATER_MATERIAL: failures.append("CollectMarkerProxy does not use the freshwater material") for component_name in ("Mesh", "StoneBankProxy"): component = components.get(component_name) if component and material_path(component.get_material(0)) != STONE_MATERIAL: failures.append(f"{component_name} does not use the sandstone material") water_location = water_source.get_actor_location() if distance_2d(water_location, EXPECTED_WATER_LOCATION) > MAX_WATER_LOCATION_DISTANCE_CM: failures.append(f"{WATER_SOURCE_LABEL} moved too far from drainage-candidate placement: {water_location}") nearby_labels = {} for actor in actors: label = get_actor_label(actor) if label in REQUIRED_NEARBY_DRESSING: nearby_labels[label] = actor missing_dressing = REQUIRED_NEARBY_DRESSING.difference(nearby_labels) if missing_dressing: failures.append("missing water dressing actors: " + ", ".join(sorted(missing_dressing))) for label, actor in nearby_labels.items(): if distance_2d(water_location, actor.get_actor_location()) > MAX_DRESSING_DISTANCE_CM: failures.append(f"{label} is too far from the freshwater source") project_root = Path(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir())) docs = project_root / "Docs" / "Terrain" / "GroundZeroWaterSourceVisualPass.md" roadmap = project_root / "AGRARIAN_DEVELOPMENT_ROADMAP.md" for path, snippets in { docs: ["Ground Zero Water Source Visual Pass", "WaterSurfaceProxy", "stone bank/edge treatment"], roadmap: ["[x] Add a real water-source visual pass", "surface material, edge treatment, scale, and placement"], }.items(): text = path.read_text(encoding="utf-8") for snippet in snippets: if snippet not in text: failures.append(f"{path} missing {snippet!r}") if failures: raise RuntimeError("Ground Zero water-source visual verification failed: " + "; ".join(failures)) unreal.log( "Ground Zero water-source visual verification complete: " f"{WATER_SOURCE_LABEL}, {len(REQUIRED_COMPONENTS)} native visual components, " f"{len(REQUIRED_NEARBY_DRESSING)} nearby dressing actors." ) main()