import math import random import struct from pathlib import Path import unreal MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test" PROJECT_ROOT = Path(r"Z:\AgrarianGameBulid") TILE_ID = "gz_us_ca_pacifica_utm10n_e544_n4160" HEIGHTMAP_PATH = PROJECT_ROOT / "Data" / "Terrain" / "Unreal" / TILE_ID / f"{TILE_ID}_unreal_1009.r16" LANDSCAPE_SIZE = 1009 XY_SCALE_CM = 100000.0 / (LANDSCAPE_SIZE - 1) Z_SCALE_CM = 100.0 LANDSCAPE_MIN_XY = -50000.0 FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass" FOLIAGE_RANDOM_SEED = 4160544 FOLIAGE_MESHES = { "tree": "/Game/LevelPrototyping/Meshes/SM_Cylinder", "shrub": "/Game/LevelPrototyping/Meshes/SM_Cube", "grass": "/Game/LevelPrototyping/Meshes/SM_Cylinder", } DEMO_ACTORS = [ { "label": "AGR_DemoPlayerStart", "class": unreal.PlayerStart, "location_xy": unreal.Vector(-18500.0, -6400.0, 0.0), "z_offset": 220.0, "rotation": unreal.Rotator(0.0, 72.0, 0.0), }, { "label": "AGR_DemoWoodResource_01", "class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode", "location_xy": unreal.Vector(-16800.0, -5400.0, 0.0), "z_offset": 80.0, "rotation": unreal.Rotator(0.0, 25.0, 0.0), }, { "label": "AGR_DemoFiberResource_01", "class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode", "location_xy": unreal.Vector(-15600.0, -7850.0, 0.0), "z_offset": 80.0, "rotation": unreal.Rotator(0.0, -15.0, 0.0), }, { "label": "AGR_DemoCampfire_01", "class_path": "/Game/Agrarian/Blueprints/Structures/BP_Campfire", "location_xy": unreal.Vector(-13200.0, -6350.0, 0.0), "z_offset": 90.0, "rotation": unreal.Rotator(0.0, 0.0, 0.0), }, { "label": "AGR_DemoPrimitiveShelter_01", "class_path": "/Game/Agrarian/Blueprints/Structures/BP_PrimitiveShelter", "location_xy": unreal.Vector(-11500.0, -7750.0, 0.0), "z_offset": 120.0, "rotation": unreal.Rotator(0.0, -30.0, 0.0), }, { "label": "AGR_DemoRabbitWildlife_01", "class_path": "/Game/Agrarian/Blueprints/Wildlife/BP_RabbitWildlife", "location_xy": unreal.Vector(-19800.0, -8550.0, 0.0), "z_offset": 120.0, "rotation": unreal.Rotator(0.0, 135.0, 0.0), }, { "label": "AGR_DemoSun", "class": unreal.DirectionalLight, "location_xy": unreal.Vector(-22000.0, -9000.0, 0.0), "fixed_z": 35000.0, "rotation": unreal.Rotator(-42.0, -35.0, 0.0), }, { "label": "AGR_DemoSkyLight", "class": unreal.SkyLight, "location_xy": unreal.Vector(-18000.0, -7000.0, 0.0), "fixed_z": 12000.0, "rotation": unreal.Rotator(0.0, 0.0, 0.0), }, { "label": "AGR_DemoFog", "class": unreal.ExponentialHeightFog, "location_xy": unreal.Vector(-18000.0, -7000.0, 0.0), "fixed_z": 4000.0, "rotation": unreal.Rotator(0.0, 0.0, 0.0), }, { "label": "AGR_DemoNoticeActor", "class": unreal.AgrarianDemoNoticeActor, "location_xy": unreal.Vector(-18500.0, -6400.0, 0.0), "fixed_z": 1600.0, "rotation": unreal.Rotator(0.0, 0.0, 0.0), }, ] BIOME_RESOURCE_ACTORS = [ { "label": "AGR_GZ_Wood_CoastalScrub_01", "class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode", "location_xy": unreal.Vector(-28600.0, 7400.0, 0.0), "z_offset": 70.0, "rotation": unreal.Rotator(0.0, 18.0, 0.0), }, { "label": "AGR_GZ_Wood_CoastalScrub_02", "class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode", "location_xy": unreal.Vector(-5400.0, 21400.0, 0.0), "z_offset": 70.0, "rotation": unreal.Rotator(0.0, -42.0, 0.0), }, { "label": "AGR_GZ_Wood_Hillside_03", "class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode", "location_xy": unreal.Vector(18400.0, 32200.0, 0.0), "z_offset": 70.0, "rotation": unreal.Rotator(0.0, 86.0, 0.0), }, { "label": "AGR_GZ_Fiber_Grassland_01", "class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode", "location_xy": unreal.Vector(-33400.0, -16200.0, 0.0), "z_offset": 65.0, "rotation": unreal.Rotator(0.0, 6.0, 0.0), }, { "label": "AGR_GZ_Fiber_Grassland_02", "class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode", "location_xy": unreal.Vector(8200.0, -18200.0, 0.0), "z_offset": 65.0, "rotation": unreal.Rotator(0.0, -28.0, 0.0), }, { "label": "AGR_GZ_Fiber_Scrub_03", "class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode", "location_xy": unreal.Vector(29200.0, -4200.0, 0.0), "z_offset": 65.0, "rotation": unreal.Rotator(0.0, 54.0, 0.0), }, { "label": "AGR_GZ_Fiber_DrainageCandidate_04", "class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode", "location_xy": unreal.Vector(-6200.0, 9200.0, 0.0), "z_offset": 65.0, "rotation": unreal.Rotator(0.0, 114.0, 0.0), }, { "label": "AGR_GZ_Stone_Slope_01", "class_path": "/Game/Agrarian/Blueprints/Resources/BP_StoneResourceNode", "location_xy": unreal.Vector(22600.0, 17600.0, 0.0), "z_offset": 45.0, "rotation": unreal.Rotator(0.0, 12.0, 0.0), }, { "label": "AGR_GZ_Stone_Slope_02", "class_path": "/Game/Agrarian/Blueprints/Resources/BP_StoneResourceNode", "location_xy": unreal.Vector(37600.0, 28600.0, 0.0), "z_offset": 45.0, "rotation": unreal.Rotator(0.0, -36.0, 0.0), }, { "label": "AGR_GZ_Stone_ExposedTerrain_03", "class_path": "/Game/Agrarian/Blueprints/Resources/BP_StoneResourceNode", "location_xy": unreal.Vector(11800.0, 38200.0, 0.0), "z_offset": 45.0, "rotation": unreal.Rotator(0.0, 71.0, 0.0), }, { "label": "AGR_GZ_Stone_ValleyEdge_04", "class_path": "/Game/Agrarian/Blueprints/Resources/BP_StoneResourceNode", "location_xy": unreal.Vector(-24800.0, 28600.0, 0.0), "z_offset": 45.0, "rotation": unreal.Rotator(0.0, 133.0, 0.0), }, ] FOLIAGE_ZONES = { "trees": { "count": 42, "x_range": (-25000.0, 42000.0), "y_range": (-12000.0, 42000.0), "min_elevation_m": 18.0, "avoid_radius_cm": 3600.0, "scale_range": (0.75, 1.35), }, "shrubs": { "count": 96, "x_range": (-42000.0, 45000.0), "y_range": (-38000.0, 45000.0), "min_elevation_m": 7.0, "avoid_radius_cm": 2600.0, "scale_range": (0.45, 1.05), }, "grass": { "count": 180, "x_range": (-46000.0, 46000.0), "y_range": (-46000.0, 46000.0), "min_elevation_m": 4.0, "avoid_radius_cm": 1800.0, "scale_range": (0.35, 0.9), }, } def get_actor_label(actor): try: return actor.get_actor_label() except Exception: return actor.get_name() def set_actor_label(actor, label): try: actor.set_actor_label(label, mark_dirty=True) except TypeError: actor.set_actor_label(label) def load_blueprint_class(path): generated_class = unreal.EditorAssetLibrary.load_blueprint_class(path) if not generated_class: raise RuntimeError(f"Could not load Blueprint class: {path}") return generated_class def load_required_asset(path): asset = unreal.EditorAssetLibrary.load_asset(path) if not asset: raise RuntimeError(f"Required asset not found: {path}") return asset def load_heightmap(): raw = HEIGHTMAP_PATH.read_bytes() expected_bytes = LANDSCAPE_SIZE * LANDSCAPE_SIZE * 2 if len(raw) != expected_bytes: raise RuntimeError(f"Unexpected heightmap size: {len(raw)} != {expected_bytes}") return struct.unpack(f"<{LANDSCAPE_SIZE * LANDSCAPE_SIZE}H", raw) def terrain_z_cm(height_values, x_cm, y_cm): sample_x = max(0, min(LANDSCAPE_SIZE - 1, round((x_cm - LANDSCAPE_MIN_XY) / XY_SCALE_CM))) sample_y = max(0, min(LANDSCAPE_SIZE - 1, round((y_cm - LANDSCAPE_MIN_XY) / XY_SCALE_CM))) height_value = height_values[int(sample_y) * LANDSCAPE_SIZE + int(sample_x)] elevation_m = (float(height_value) - 32768.0) * Z_SCALE_CM / (100.0 * 128.0) return elevation_m * 100.0 def terrain_elevation_m(height_values, x_cm, y_cm): return terrain_z_cm(height_values, x_cm, y_cm) / 100.0 def remove_existing_demo_actors(labels): for actor in unreal.EditorLevelLibrary.get_all_level_actors(): if get_actor_label(actor) in labels: unreal.EditorLevelLibrary.destroy_actor(actor) def distance_2d(a, b): return math.hypot(a.x - b.x, a.y - b.y) def make_foliage_transform(height_values, x, y, yaw, z_offset, scale_xy, scale_z): return unreal.Transform( location=unreal.Vector(x, y, terrain_z_cm(height_values, x, y) + z_offset), rotation=unreal.Rotator(0.0, yaw, 0.0), scale=unreal.Vector(scale_xy, scale_xy, scale_z), ) def configure_foliage_meshes(foliage_actor): component_meshes = { "tree_instances": load_required_asset(FOLIAGE_MESHES["tree"]), "shrub_instances": load_required_asset(FOLIAGE_MESHES["shrub"]), "grass_instances": load_required_asset(FOLIAGE_MESHES["grass"]), } for property_name, mesh in component_meshes.items(): component = foliage_actor.get_editor_property(property_name) component.set_editor_property("static_mesh", mesh) def choose_foliage_points(height_values, zone, reserved_points, existing_points): rng = random.Random(FOLIAGE_RANDOM_SEED + len(existing_points) + int(zone["count"])) chosen = [] attempts = 0 max_attempts = zone["count"] * 80 while len(chosen) < zone["count"] and attempts < max_attempts: attempts += 1 x = rng.uniform(zone["x_range"][0], zone["x_range"][1]) y = rng.uniform(zone["y_range"][0], zone["y_range"][1]) point = unreal.Vector(x, y, 0.0) if terrain_elevation_m(height_values, x, y) < zone["min_elevation_m"]: continue if any(distance_2d(point, reserved) < zone["avoid_radius_cm"] for reserved in reserved_points): continue if any(distance_2d(point, existing) < zone["avoid_radius_cm"] * 0.55 for existing in existing_points): continue chosen.append(point) existing_points.append(point) if len(chosen) != zone["count"]: unreal.log_warning(f"Placed {len(chosen)} of {zone['count']} requested foliage instances for zone.") return chosen def spawn_foliage_actor(height_values): reserved_points = [ spec["location_xy"] for spec in DEMO_ACTORS if spec["label"] not in {"AGR_DemoSun", "AGR_DemoSkyLight", "AGR_DemoFog", "AGR_DemoNoticeActor"} ] foliage_actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world( unreal.AgrarianFoliagePatch, unreal.Vector(0.0, 0.0, 0.0), unreal.Rotator(0.0, 0.0, 0.0), FOLIAGE_LABEL, ) if not foliage_actor: raise RuntimeError("Could not spawn first-pass foliage actor.") set_actor_label(foliage_actor, FOLIAGE_LABEL) configure_foliage_meshes(foliage_actor) foliage_actor.clear_foliage() existing_points = [] rng = random.Random(FOLIAGE_RANDOM_SEED) for point in choose_foliage_points(height_values, FOLIAGE_ZONES["trees"], reserved_points, existing_points): scale = rng.uniform(*FOLIAGE_ZONES["trees"]["scale_range"]) foliage_actor.add_tree_instance( make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 5.0, scale, scale * 5.5) ) for point in choose_foliage_points(height_values, FOLIAGE_ZONES["shrubs"], reserved_points, existing_points): scale = rng.uniform(*FOLIAGE_ZONES["shrubs"]["scale_range"]) foliage_actor.add_shrub_instance( make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 4.0, scale * 2.2, scale * 0.85) ) for point in choose_foliage_points(height_values, FOLIAGE_ZONES["grass"], reserved_points, existing_points): scale = rng.uniform(*FOLIAGE_ZONES["grass"]["scale_range"]) foliage_actor.add_grass_instance( make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 2.0, scale * 0.28, scale * 1.6) ) unreal.log( "Placed first-pass Ground Zero foliage: " f"{foliage_actor.get_tree_instance_count()} trees, " f"{foliage_actor.get_shrub_instance_count()} shrubs, " f"{foliage_actor.get_grass_instance_count()} grass clumps." ) return foliage_actor def spawn_demo_actor(spec, height_values): location_xy = spec["location_xy"] z = spec.get("fixed_z") if z is None: z = terrain_z_cm(height_values, location_xy.x, location_xy.y) + spec.get("z_offset", 0.0) actor_class = spec.get("class") if actor_class is None: actor_class = load_blueprint_class(spec["class_path"]) actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world( actor_class, unreal.Vector(location_xy.x, location_xy.y, z), spec["rotation"], spec["label"], ) if not actor: raise RuntimeError(f"Could not spawn {spec['label']}") set_actor_label(actor, spec["label"]) unreal.log(f"Placed {spec['label']} at {actor.get_actor_location()}") return actor def main(): if not unreal.EditorLevelLibrary.load_level(MAP_PATH): raise RuntimeError(f"Could not load map: {MAP_PATH}") labels = {spec["label"] for spec in DEMO_ACTORS} labels.update(spec["label"] for spec in BIOME_RESOURCE_ACTORS) labels.add(FOLIAGE_LABEL) remove_existing_demo_actors(labels) height_values = load_heightmap() spawn_foliage_actor(height_values) for spec in BIOME_RESOURCE_ACTORS: spawn_demo_actor(spec, height_values) for spec in DEMO_ACTORS: spawn_demo_actor(spec, height_values) unreal.EditorLevelLibrary.save_current_level() unreal.log("Ground Zero demo map setup complete.") main()