This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
AgrarianGameArchive/Scripts/setup_ground_zero_demo_map.py
T

1653 lines
63 KiB
Python

import math
import random
import struct
from pathlib import Path
import unreal
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
PROJECT_ROOT = Path(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()))
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
PLACEHOLDER_MESH_FOLDER = "/Game/Agrarian/Environment/PlaceholderMeshes"
VEGETATION_MESH_FOLDER = "/Game/Agrarian/Environment/Vegetation"
VEGETATION_SOURCE_FOLDER = PROJECT_ROOT / "Saved" / "CodexGenerated" / "Vegetation"
PLACEHOLDER_MESH_SOURCES = {
"SM_AGR_Placeholder_Cube": "/Game/LevelPrototyping/Meshes/SM_Cube",
"SM_AGR_Placeholder_ChamferCube": "/Game/LevelPrototyping/Meshes/SM_ChamferCube",
"SM_AGR_Placeholder_Cylinder": "/Game/LevelPrototyping/Meshes/SM_Cylinder",
"SM_AGR_Placeholder_QuarterCylinder": "/Game/LevelPrototyping/Meshes/SM_QuarterCylinder",
"SM_AGR_Placeholder_Plane": "/Game/LevelPrototyping/Meshes/SM_Plane",
}
PLACEHOLDER_MESHES = {
name: f"{PLACEHOLDER_MESH_FOLDER}/{name}"
for name in PLACEHOLDER_MESH_SOURCES
}
FOLIAGE_MESHES = {
"tree": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_CoastalOak_Proxy",
"shrub": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_CoyoteBrush_Proxy",
"grass": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_DryGrassClump_Proxy",
}
VARIATION_MESHES = {
"cube": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cube"],
"chamfer_cube": PLACEHOLDER_MESHES["SM_AGR_Placeholder_ChamferCube"],
"cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cylinder"],
"quarter_cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_QuarterCylinder"],
"plane": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Plane"],
"coastal_oak": FOLIAGE_MESHES["tree"],
"coyote_brush": FOLIAGE_MESHES["shrub"],
"dry_grass_clump": FOLIAGE_MESHES["grass"],
}
MATERIAL_FOLDER = "/Game/Agrarian/Materials"
ENVIRONMENT_MATERIALS = {
"terrain": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Terrain_CoastalScrub",
"color": unreal.LinearColor(0.16, 0.23, 0.12, 1.0),
"dry_soil_color": unreal.LinearColor(0.28, 0.24, 0.16, 1.0),
"scrub_green_color": unreal.LinearColor(0.12, 0.22, 0.10, 1.0),
"sandy_path_color": unreal.LinearColor(0.42, 0.36, 0.23, 1.0),
"noise_scale": 42.0,
"roughness": 0.92,
},
"tree": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Tree_CoastalOak",
"color": unreal.LinearColor(0.07, 0.18, 0.06, 1.0),
"variation_color": unreal.LinearColor(0.14, 0.24, 0.09, 1.0),
"roughness": 0.88,
"used_with_instanced_static_meshes": True,
},
"shrub": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Shrub_CoyoteBrush",
"color": unreal.LinearColor(0.15, 0.28, 0.10, 1.0),
"variation_color": unreal.LinearColor(0.24, 0.34, 0.15, 1.0),
"roughness": 0.9,
"used_with_instanced_static_meshes": True,
"two_sided": True,
},
"grass": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Grass_DryCoastal",
"color": unreal.LinearColor(0.32, 0.34, 0.13, 1.0),
"variation_color": unreal.LinearColor(0.52, 0.45, 0.22, 1.0),
"roughness": 0.95,
"used_with_instanced_static_meshes": True,
"two_sided": True,
},
"wood_resource": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Wood_Resource",
"color": unreal.LinearColor(0.26, 0.16, 0.08, 1.0),
"roughness": 0.86,
},
"fiber_resource": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Fiber_Resource",
"color": unreal.LinearColor(0.55, 0.49, 0.28, 1.0),
"roughness": 0.93,
},
"edible_plant_resource": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_EdiblePlant_Resource",
"color": unreal.LinearColor(0.22, 0.46, 0.18, 1.0),
"roughness": 0.9,
},
"stone_resource": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Stone_Sandstone",
"color": unreal.LinearColor(0.43, 0.40, 0.35, 1.0),
"roughness": 0.97,
},
"fresh_water": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_FreshWater",
"color": unreal.LinearColor(0.02, 0.16, 0.30, 1.0),
"roughness": 0.35,
},
}
RESOURCE_MATERIAL_BY_LABEL_PREFIX = {
"AGR_GZ_Wood": "wood_resource",
"AGR_GZ_Fiber": "fiber_resource",
"AGR_GZ_EdiblePlant": "edible_plant_resource",
"AGR_GZ_Stone": "stone_resource",
"AGR_DemoWoodResource": "wood_resource",
"AGR_DemoFiberResource": "fiber_resource",
"AGR_GZ_FreshWaterSource": "fresh_water",
}
SAFE_SPAWN_CONFIG = {
"player_start_label": "AGR_DemoPlayerStart",
"safe_z_offset": 220.0,
"min_elevation_m": 2.0,
"max_slope_degrees": 8.0,
"slope_sample_distance_cm": 500.0,
"min_water_distance_cm": 10000.0,
"min_resource_distance_cm": 5000.0,
"fallback_location_xy": unreal.Vector(-22000.0, -3500.0, 0.0),
"candidate_locations_xy": [
unreal.Vector(-22000.0, -3500.0, 0.0),
unreal.Vector(-25000.0, -4000.0, 0.0),
unreal.Vector(-20500.0, -10500.0, 0.0),
unreal.Vector(-28000.0, -9000.0, 0.0),
unreal.Vector(-30000.0, -12000.0, 0.0),
unreal.Vector(-35000.0, 0.0, 0.0),
],
}
MAP_BOUNDARY_CONFIG = {
"label": "AGR_GroundZeroMapBoundary",
"boundary_id": "ground_zero_mvp_tile",
"location": unreal.Vector(0.0, 0.0, 10000.0),
"extent": unreal.Vector(50000.0, 50000.0, 25000.0),
"padding_cm": 250.0,
"warning_distance_cm": 3000.0,
}
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_DemoSkyLightingController",
"class": unreal.AgrarianSkyLightingController,
"location_xy": unreal.Vector(-18000.0, -7000.0, 0.0),
"fixed_z": 12000.0,
"rotation": unreal.Rotator(-42.0, -35.0, 0.0),
},
{
"label": "AGR_DemoWeatherAudioController",
"class": unreal.AgrarianWeatherAudioController,
"location_xy": unreal.Vector(-18000.0, -7000.0, 0.0),
"fixed_z": 11800.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),
},
]
LEGACY_DEMO_LIGHTING_LABELS = {
"AGR_DemoSun",
"AGR_DemoSkyLight",
"AGR_DemoFog",
}
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_EdiblePlant_CoastalScrub_01",
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_EdiblePlantResourceNode",
"location_xy": unreal.Vector(-31400.0, -7600.0, 0.0),
"z_offset": 58.0,
"rotation": unreal.Rotator(0.0, 21.0, 0.0),
},
{
"label": "AGR_GZ_EdiblePlant_Grassland_02",
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_EdiblePlantResourceNode",
"location_xy": unreal.Vector(6200.0, -26800.0, 0.0),
"z_offset": 58.0,
"rotation": unreal.Rotator(0.0, -48.0, 0.0),
},
{
"label": "AGR_GZ_EdiblePlant_DrainageCandidate_03",
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_EdiblePlantResourceNode",
"location_xy": unreal.Vector(-9400.0, 12800.0, 0.0),
"z_offset": 58.0,
"rotation": unreal.Rotator(0.0, 96.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),
},
]
WATER_SOURCE_ACTORS = [
{
"label": "AGR_GZ_FreshWaterSource_01",
"class_path": "/Game/Agrarian/Blueprints/World/BP_FreshWaterSource",
"location_xy": unreal.Vector(-7200.0, 10400.0, 0.0),
"z_offset": 36.0,
"rotation": unreal.Rotator(0.0, 0.0, 0.0),
},
]
WEATHER_EXPOSURE_ZONES = [
{
"label": "AGR_GZ_WeatherExposure_Ridge_01",
"zone_id": "ground_zero_ridge_exposed",
"location_xy": unreal.Vector(36000.0, 29200.0, 0.0),
"z_offset": 450.0,
"extent": unreal.Vector(11500.0, 9200.0, 2200.0),
"exposure_multiplier": 1.45,
"temperature_offset_c": -1.8,
},
{
"label": "AGR_GZ_WeatherExposure_CoastalWind_01",
"zone_id": "ground_zero_coastal_wind",
"location_xy": unreal.Vector(-31500.0, -14600.0, 0.0),
"z_offset": 350.0,
"extent": unreal.Vector(10500.0, 7600.0, 1600.0),
"exposure_multiplier": 1.25,
"temperature_offset_c": -0.9,
},
{
"label": "AGR_GZ_WeatherExposure_DrainageCool_01",
"zone_id": "ground_zero_drainage_cool",
"location_xy": unreal.Vector(-7200.0, 10400.0, 0.0),
"z_offset": 260.0,
"extent": unreal.Vector(7200.0, 5400.0, 1100.0),
"exposure_multiplier": 1.1,
"temperature_offset_c": -1.2,
},
]
ENVIRONMENT_VARIATION_ACTORS = [
{
"label": "AGR_GZ_EnvVar_Tree_Canopy_01",
"mesh_key": "coastal_oak",
"material_key": "tree",
"location_xy": unreal.Vector(-27500.0, 6900.0, 0.0),
"z_offset": 390.0,
"scale": unreal.Vector(2.5, 2.0, 1.6),
"rotation": unreal.Rotator(0.0, 32.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Tree_Trunk_01",
"mesh_key": "cylinder",
"material_key": "wood_resource",
"location_xy": unreal.Vector(-27500.0, 6900.0, 0.0),
"z_offset": 170.0,
"scale": unreal.Vector(0.35, 0.35, 3.6),
"rotation": unreal.Rotator(0.0, 32.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Tree_Canopy_02",
"mesh_key": "coastal_oak",
"material_key": "tree",
"location_xy": unreal.Vector(17600.0, 31800.0, 0.0),
"z_offset": 430.0,
"scale": unreal.Vector(2.0, 2.8, 1.45),
"rotation": unreal.Rotator(0.0, -18.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Tree_Trunk_02",
"mesh_key": "cylinder",
"material_key": "wood_resource",
"location_xy": unreal.Vector(17600.0, 31800.0, 0.0),
"z_offset": 190.0,
"scale": unreal.Vector(0.3, 0.3, 4.1),
"rotation": unreal.Rotator(0.0, -18.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Bush_Rounded_01",
"mesh_key": "coyote_brush",
"material_key": "shrub",
"location_xy": unreal.Vector(-33400.0, -15200.0, 0.0),
"z_offset": 70.0,
"scale": unreal.Vector(1.9, 1.4, 0.7),
"rotation": unreal.Rotator(0.0, 11.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Bush_Rounded_02",
"mesh_key": "coyote_brush",
"material_key": "shrub",
"location_xy": unreal.Vector(30400.0, -3900.0, 0.0),
"z_offset": 75.0,
"scale": unreal.Vector(1.45, 2.1, 0.85),
"rotation": unreal.Rotator(0.0, 73.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Grass_Mat_01",
"mesh_key": "plane",
"material_key": "grass",
"location_xy": unreal.Vector(8200.0, -17100.0, 0.0),
"z_offset": 14.0,
"scale": unreal.Vector(3.6, 2.2, 1.0),
"rotation": unreal.Rotator(0.0, -34.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Grass_Mat_02",
"mesh_key": "plane",
"material_key": "grass",
"location_xy": unreal.Vector(-6200.0, 8300.0, 0.0),
"z_offset": 14.0,
"scale": unreal.Vector(2.7, 4.0, 1.0),
"rotation": unreal.Rotator(0.0, 41.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Rock_Slab_01",
"mesh_key": "chamfer_cube",
"material_key": "stone_resource",
"location_xy": unreal.Vector(22900.0, 18300.0, 0.0),
"z_offset": 45.0,
"scale": unreal.Vector(1.8, 1.05, 0.45),
"rotation": unreal.Rotator(0.0, 24.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Rock_Slab_02",
"mesh_key": "quarter_cylinder",
"material_key": "stone_resource",
"location_xy": unreal.Vector(38100.0, 29300.0, 0.0),
"z_offset": 55.0,
"scale": unreal.Vector(1.2, 1.8, 0.75),
"rotation": unreal.Rotator(0.0, -47.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Water_Surface_01",
"mesh_key": "plane",
"material_key": "fresh_water",
"location_xy": unreal.Vector(-7200.0, 10400.0, 0.0),
"z_offset": 18.0,
"scale": unreal.Vector(2.6, 1.6, 1.0),
"rotation": unreal.Rotator(0.0, -15.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Tree_Canopy_03",
"mesh_key": "chamfer_cube",
"material_key": "tree",
"location_xy": unreal.Vector(-8200.0, 12600.0, 0.0),
"z_offset": 360.0,
"scale": unreal.Vector(1.85, 2.25, 1.35),
"rotation": unreal.Rotator(0.0, 61.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Tree_Trunk_03",
"mesh_key": "cylinder",
"material_key": "wood_resource",
"location_xy": unreal.Vector(-8200.0, 12600.0, 0.0),
"z_offset": 155.0,
"scale": unreal.Vector(0.24, 0.24, 3.25),
"rotation": unreal.Rotator(0.0, 61.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Brush_Coyote_01",
"mesh_key": "chamfer_cube",
"material_key": "shrub",
"location_xy": unreal.Vector(-12600.0, -5100.0, 0.0),
"z_offset": 72.0,
"scale": unreal.Vector(1.7, 1.15, 0.62),
"rotation": unreal.Rotator(0.0, 7.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Brush_Coyote_02",
"mesh_key": "chamfer_cube",
"material_key": "shrub",
"location_xy": unreal.Vector(-10400.0, -8500.0, 0.0),
"z_offset": 74.0,
"scale": unreal.Vector(1.2, 1.85, 0.7),
"rotation": unreal.Rotator(0.0, -38.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Brush_Coyote_03",
"mesh_key": "chamfer_cube",
"material_key": "shrub",
"location_xy": unreal.Vector(-15800.0, -9800.0, 0.0),
"z_offset": 78.0,
"scale": unreal.Vector(1.55, 1.35, 0.58),
"rotation": unreal.Rotator(0.0, 47.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Grass_Mat_03",
"mesh_key": "plane",
"material_key": "grass",
"location_xy": unreal.Vector(-14600.0, -4200.0, 0.0),
"z_offset": 14.0,
"scale": unreal.Vector(3.2, 1.8, 1.0),
"rotation": unreal.Rotator(0.0, 16.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Grass_Mat_04",
"mesh_key": "plane",
"material_key": "grass",
"location_xy": unreal.Vector(-11800.0, -10600.0, 0.0),
"z_offset": 14.0,
"scale": unreal.Vector(2.1, 3.1, 1.0),
"rotation": unreal.Rotator(0.0, -27.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Rock_Slab_03",
"mesh_key": "quarter_cylinder",
"material_key": "stone_resource",
"location_xy": unreal.Vector(-9200.0, -7600.0, 0.0),
"z_offset": 48.0,
"scale": unreal.Vector(0.95, 1.45, 0.55),
"rotation": unreal.Rotator(0.0, 34.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Rock_Slab_04",
"mesh_key": "chamfer_cube",
"material_key": "stone_resource",
"location_xy": unreal.Vector(-18200.0, -5000.0, 0.0),
"z_offset": 42.0,
"scale": unreal.Vector(1.05, 0.72, 0.32),
"rotation": unreal.Rotator(0.0, -64.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Water_Bank_01",
"mesh_key": "quarter_cylinder",
"material_key": "stone_resource",
"location_xy": unreal.Vector(-6150.0, 9700.0, 0.0),
"z_offset": 42.0,
"scale": unreal.Vector(1.4, 0.75, 0.45),
"rotation": unreal.Rotator(0.0, 22.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Water_Reed_01",
"mesh_key": "cylinder",
"material_key": "grass",
"location_xy": unreal.Vector(-8050.0, 9600.0, 0.0),
"z_offset": 74.0,
"scale": unreal.Vector(0.08, 0.08, 1.35),
"rotation": unreal.Rotator(0.0, 11.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_Water_Reed_02",
"mesh_key": "cylinder",
"material_key": "grass",
"location_xy": unreal.Vector(-6600.0, 11700.0, 0.0),
"z_offset": 68.0,
"scale": unreal.Vector(0.07, 0.07, 1.15),
"rotation": unreal.Rotator(0.0, -19.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_OakCanopy_01",
"mesh_key": "chamfer_cube",
"material_key": "tree",
"location_xy": unreal.Vector(-26200.0, -7200.0, 0.0),
"z_offset": 410.0,
"scale": unreal.Vector(2.2, 2.6, 1.55),
"rotation": unreal.Rotator(0.0, 24.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_OakTrunk_01",
"mesh_key": "cylinder",
"material_key": "wood_resource",
"location_xy": unreal.Vector(-26200.0, -7200.0, 0.0),
"z_offset": 180.0,
"scale": unreal.Vector(0.28, 0.28, 3.7),
"rotation": unreal.Rotator(0.0, 24.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_Brush_01",
"mesh_key": "chamfer_cube",
"material_key": "shrub",
"location_xy": unreal.Vector(-19100.0, -8350.0, 0.0),
"z_offset": 72.0,
"scale": unreal.Vector(1.65, 1.25, 0.68),
"rotation": unreal.Rotator(0.0, -14.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_Brush_02",
"mesh_key": "chamfer_cube",
"material_key": "shrub",
"location_xy": unreal.Vector(-23900.0, 1300.0, 0.0),
"z_offset": 76.0,
"scale": unreal.Vector(1.25, 1.95, 0.72),
"rotation": unreal.Rotator(0.0, 39.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_GrassMat_01",
"mesh_key": "plane",
"material_key": "grass",
"location_xy": unreal.Vector(-20500.0, -6400.0, 0.0),
"z_offset": 15.0,
"scale": unreal.Vector(3.8, 2.4, 1.0),
"rotation": unreal.Rotator(0.0, 18.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_GrassMat_02",
"mesh_key": "plane",
"material_key": "grass",
"location_xy": unreal.Vector(-24200.0, -1800.0, 0.0),
"z_offset": 15.0,
"scale": unreal.Vector(2.9, 3.7, 1.0),
"rotation": unreal.Rotator(0.0, -31.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_Rock_01",
"mesh_key": "quarter_cylinder",
"material_key": "stone_resource",
"location_xy": unreal.Vector(-18400.0, -2400.0, 0.0),
"z_offset": 45.0,
"scale": unreal.Vector(1.15, 1.55, 0.52),
"rotation": unreal.Rotator(0.0, 66.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_Rock_02",
"mesh_key": "chamfer_cube",
"material_key": "stone_resource",
"location_xy": unreal.Vector(-27900.0, -3600.0, 0.0),
"z_offset": 42.0,
"scale": unreal.Vector(0.98, 0.82, 0.36),
"rotation": unreal.Rotator(0.0, -52.0, 0.0),
},
]
RUIN_PLACEHOLDER_ACTORS = [
{
"label": "AGR_GZ_Landmark_Ruin_Foundation_01",
"mesh_key": "chamfer_cube",
"material_key": "stone_resource",
"location_xy": unreal.Vector(6400.0, 24700.0, 0.0),
"z_offset": 38.0,
"scale": unreal.Vector(4.8, 3.2, 0.22),
"rotation": unreal.Rotator(0.0, -22.0, 0.0),
},
{
"label": "AGR_GZ_Landmark_Ruin_Wall_01",
"mesh_key": "chamfer_cube",
"material_key": "stone_resource",
"location_xy": unreal.Vector(5750.0, 25350.0, 0.0),
"z_offset": 165.0,
"scale": unreal.Vector(0.42, 2.35, 1.65),
"rotation": unreal.Rotator(0.0, -22.0, 0.0),
},
{
"label": "AGR_GZ_Landmark_Ruin_Wall_02",
"mesh_key": "chamfer_cube",
"material_key": "stone_resource",
"location_xy": unreal.Vector(7250.0, 24250.0, 0.0),
"z_offset": 118.0,
"scale": unreal.Vector(0.36, 1.7, 1.05),
"rotation": unreal.Rotator(0.0, 68.0, 0.0),
},
{
"label": "AGR_GZ_Landmark_Ruin_Cairn_01",
"mesh_key": "cylinder",
"material_key": "stone_resource",
"location_xy": unreal.Vector(7900.0, 25300.0, 0.0),
"z_offset": 92.0,
"scale": unreal.Vector(0.9, 0.9, 1.15),
"rotation": unreal.Rotator(0.0, 11.0, 0.0),
},
{
"label": "AGR_GZ_Landmark_Ruin_Threshold_01",
"mesh_key": "quarter_cylinder",
"material_key": "stone_resource",
"location_xy": unreal.Vector(6550.0, 23550.0, 0.0),
"z_offset": 70.0,
"scale": unreal.Vector(1.8, 0.55, 0.5),
"rotation": unreal.Rotator(0.0, -22.0, 0.0),
},
]
FOLIAGE_ZONES = {
"trees": {
"count": 96,
"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": 220,
"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": 420,
"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),
},
}
SIGHTLINE_TUNING_CONFIG = {
"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",
},
"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 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 ensure_native_placeholder_meshes():
unreal.EditorAssetLibrary.make_directory(PLACEHOLDER_MESH_FOLDER)
for asset_name, source_path in PLACEHOLDER_MESH_SOURCES.items():
destination_path = f"{PLACEHOLDER_MESH_FOLDER}/{asset_name}"
if unreal.EditorAssetLibrary.does_asset_exist(destination_path):
continue
if not unreal.EditorAssetLibrary.duplicate_asset(source_path, destination_path):
raise RuntimeError(f"Could not create native placeholder mesh {destination_path} from {source_path}")
unreal.EditorAssetLibrary.save_asset(destination_path)
unreal.log(f"Created native placeholder mesh: {destination_path}")
def write_obj_mesh(path, vertices, faces):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as handle:
handle.write("# Agrarian generated coastal scrub vegetation proxy\n")
for vertex in vertices:
handle.write(f"v {vertex[0]:.3f} {vertex[1]:.3f} {vertex[2]:.3f}\n")
for face in faces:
handle.write("f " + " ".join(str(index + 1) for index in face) + "\n")
def add_box(vertices, faces, center, size):
cx, cy, cz = center
sx, sy, sz = size[0] * 0.5, size[1] * 0.5, size[2] * 0.5
base = len(vertices)
vertices.extend(
[
(cx - sx, cy - sy, cz - sz),
(cx + sx, cy - sy, cz - sz),
(cx + sx, cy + sy, cz - sz),
(cx - sx, cy + sy, cz - sz),
(cx - sx, cy - sy, cz + sz),
(cx + sx, cy - sy, cz + sz),
(cx + sx, cy + sy, cz + sz),
(cx - sx, cy + sy, cz + sz),
]
)
faces.extend(
[
(base + 0, base + 1, base + 2, base + 3),
(base + 4, base + 7, base + 6, base + 5),
(base + 0, base + 4, base + 5, base + 1),
(base + 1, base + 5, base + 6, base + 2),
(base + 2, base + 6, base + 7, base + 3),
(base + 3, base + 7, base + 4, base + 0),
]
)
def add_leaf_card(vertices, faces, center, width, height, yaw_degrees, lean_degrees=0.0):
yaw = math.radians(yaw_degrees)
lean = math.radians(lean_degrees)
right = (math.cos(yaw) * width * 0.5, math.sin(yaw) * width * 0.5, 0.0)
up_offset = (math.sin(lean) * height * 0.35, 0.0, math.cos(lean) * height)
cx, cy, cz = center
base = len(vertices)
vertices.extend(
[
(cx - right[0], cy - right[1], cz),
(cx + right[0], cy + right[1], cz),
(cx + right[0] + up_offset[0], cy + right[1] + up_offset[1], cz + up_offset[2]),
(cx - right[0] + up_offset[0], cy - right[1] + up_offset[1], cz + up_offset[2]),
]
)
faces.append((base + 0, base + 1, base + 2, base + 3))
def add_irregular_leaf_card(vertices, faces, center, width, height, yaw_degrees, lean_degrees=0.0, pinch=0.18):
yaw = math.radians(yaw_degrees)
lean = math.radians(lean_degrees)
right = (math.cos(yaw) * width * 0.5, math.sin(yaw) * width * 0.5, 0.0)
up_offset = (math.sin(lean) * height * 0.35, 0.0, math.cos(lean) * height)
cx, cy, cz = center
base = len(vertices)
vertices.extend(
[
(cx - right[0] * 0.86, cy - right[1] * 0.86, cz),
(cx + right[0] * 0.92, cy + right[1] * 0.92, cz + height * 0.08),
(cx + right[0] * pinch + up_offset[0], cy + right[1] * pinch + up_offset[1], cz + up_offset[2]),
(cx - right[0] * 0.68 + up_offset[0] * 0.55, cy - right[1] * 0.68 + up_offset[1] * 0.55, cz + up_offset[2] * 0.58),
]
)
faces.append((base + 0, base + 1, base + 2, base + 3))
def add_tapered_cylinder(vertices, faces, base_center, height, base_radius, top_radius, segments=9, yaw_offset_degrees=0.0, top_offset=(0.0, 0.0)):
bx, by, bz = base_center
yaw_offset = math.radians(yaw_offset_degrees)
base_indices = []
top_indices = []
for index in range(segments):
angle = yaw_offset + (math.tau * index) / segments
cos_a = math.cos(angle)
sin_a = math.sin(angle)
base_indices.append(len(vertices))
vertices.append((bx + cos_a * base_radius, by + sin_a * base_radius, bz))
top_indices.append(len(vertices))
vertices.append((bx + top_offset[0] + cos_a * top_radius, by + top_offset[1] + sin_a * top_radius, bz + height))
base_center_index = len(vertices)
vertices.append((bx, by, bz))
top_center_index = len(vertices)
vertices.append((bx + top_offset[0], by + top_offset[1], bz + height))
for index in range(segments):
next_index = (index + 1) % segments
faces.append((base_indices[index], base_indices[next_index], top_indices[next_index], top_indices[index]))
faces.append((base_center_index, base_indices[index], base_indices[next_index]))
faces.append((top_center_index, top_indices[next_index], top_indices[index]))
def add_low_poly_ellipsoid(vertices, faces, center, radius_x, radius_y, radius_z, segments=10):
cx, cy, cz = center
top_index = len(vertices)
vertices.append((cx, cy, cz + radius_z))
upper = []
lower = []
for ring_z, ring_radius_scale, target in ((0.18, 0.9, upper), (-0.25, 1.0, lower)):
for index in range(segments):
angle = (math.tau * index) / segments
target.append(len(vertices))
vertices.append(
(
cx + math.cos(angle) * radius_x * ring_radius_scale,
cy + math.sin(angle) * radius_y * ring_radius_scale,
cz + (radius_z * ring_z),
)
)
bottom_index = len(vertices)
vertices.append((cx, cy, cz - radius_z * 0.45))
for index in range(segments):
next_index = (index + 1) % segments
faces.append((top_index, upper[index], upper[next_index]))
faces.append((upper[index], lower[index], lower[next_index], upper[next_index]))
faces.append((bottom_index, lower[next_index], lower[index]))
def coastal_oak_mesh():
vertices = []
faces = []
add_tapered_cylinder(vertices, faces, (0.0, 0.0, 0.0), 275.0, 23.0, 12.0, 11, 8.0, (18.0, -10.0))
add_tapered_cylinder(vertices, faces, (-8.0, 2.0, 180.0), 145.0, 12.0, 6.0, 8, 21.0, (-86.0, 36.0))
add_tapered_cylinder(vertices, faces, (12.0, -4.0, 205.0), 150.0, 11.0, 5.5, 8, 12.0, (92.0, -42.0))
add_tapered_cylinder(vertices, faces, (5.0, 0.0, 235.0), 120.0, 9.0, 5.0, 8, 44.0, (22.0, 90.0))
for center, rx, ry, rz, segments in (
((0.0, 4.0, 362.0), 165.0, 126.0, 86.0, 13),
((-108.0, 38.0, 330.0), 116.0, 82.0, 66.0, 11),
((105.0, -34.0, 342.0), 122.0, 88.0, 70.0, 11),
((24.0, 106.0, 330.0), 88.0, 70.0, 58.0, 9),
):
add_low_poly_ellipsoid(vertices, faces, center, rx, ry, rz, segments)
for yaw in (8.0, 52.0, 96.0, 141.0):
add_irregular_leaf_card(vertices, faces, (0.0, 0.0, 285.0), 235.0, 150.0, yaw, 6.0, 0.3)
return vertices, faces
def coyote_brush_mesh():
vertices = []
faces = []
for yaw in (0.0, 27.0, 55.0, 88.0, 122.0, 156.0):
add_irregular_leaf_card(vertices, faces, (0.0, 0.0, 0.0), 170.0, 132.0, yaw, 10.0, 0.24)
for center, rx, ry, rz in (
((-38.0, 18.0, 72.0), 88.0, 62.0, 45.0),
((45.0, -22.0, 68.0), 82.0, 66.0, 42.0),
((0.0, 45.0, 62.0), 72.0, 48.0, 37.0),
((18.0, -56.0, 58.0), 62.0, 44.0, 32.0),
):
add_low_poly_ellipsoid(vertices, faces, center, rx, ry, rz, 9)
return vertices, faces
def dry_grass_clump_mesh():
vertices = []
faces = []
for index, yaw in enumerate((0.0, 16.0, 31.0, 49.0, 73.0, 97.0, 121.0, 148.0, 172.0)):
width = 18.0 + (index % 3) * 4.0
height = 95.0 + (index % 4) * 14.0
add_irregular_leaf_card(vertices, faces, (0.0, 0.0, 0.0), width, height, yaw, -8.0 + (index % 3) * 6.0, 0.08)
return vertices, faces
def import_static_mesh_obj(obj_path, destination_folder, asset_name):
task = unreal.AssetImportTask()
task.set_editor_property("filename", str(obj_path))
task.set_editor_property("destination_path", destination_folder)
task.set_editor_property("destination_name", asset_name)
task.set_editor_property("automated", True)
task.set_editor_property("replace_existing", True)
task.set_editor_property("save", True)
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
asset_path = f"{destination_folder}/{asset_name}"
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
if not asset:
raise RuntimeError(f"Could not import vegetation mesh asset: {asset_path}")
return asset
def ensure_ground_zero_vegetation_meshes():
if not unreal.EditorAssetLibrary.does_directory_exist(VEGETATION_MESH_FOLDER):
unreal.EditorAssetLibrary.make_directory(VEGETATION_MESH_FOLDER)
mesh_builders = {
"SM_AGR_GZ_CoastalOak_Proxy": coastal_oak_mesh,
"SM_AGR_GZ_CoyoteBrush_Proxy": coyote_brush_mesh,
"SM_AGR_GZ_DryGrassClump_Proxy": dry_grass_clump_mesh,
}
for asset_name, builder in mesh_builders.items():
vertices, faces = builder()
obj_path = VEGETATION_SOURCE_FOLDER / f"{asset_name}.obj"
write_obj_mesh(obj_path, vertices, faces)
import_static_mesh_obj(obj_path, VEGETATION_MESH_FOLDER, asset_name)
unreal.log(f"Created or refreshed Ground Zero vegetation mesh: {VEGETATION_MESH_FOLDER}/{asset_name}")
def ensure_environment_materials():
if not unreal.EditorAssetLibrary.does_directory_exist(MATERIAL_FOLDER):
unreal.EditorAssetLibrary.make_directory(MATERIAL_FOLDER)
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
created_or_loaded = {}
for key, spec in ENVIRONMENT_MATERIALS.items():
material = unreal.EditorAssetLibrary.load_asset(spec["path"])
if not material:
asset_name = spec["path"].rsplit("/", 1)[-1]
material = asset_tools.create_asset(asset_name, MATERIAL_FOLDER, unreal.Material, unreal.MaterialFactoryNew())
if not material:
raise RuntimeError(f"Could not create material: {spec['path']}")
base_color = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionConstant3Vector, -420, -120
)
base_color.set_editor_property("constant", spec["color"])
unreal.MaterialEditingLibrary.connect_material_property(
base_color, "", unreal.MaterialProperty.MP_BASE_COLOR
)
roughness = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionConstant, -420, 80
)
roughness.set_editor_property("r", spec["roughness"])
unreal.MaterialEditingLibrary.connect_material_property(
roughness, "", unreal.MaterialProperty.MP_ROUGHNESS
)
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"])
unreal.log(f"Created Ground Zero environment material: {spec['path']}")
else:
base_color = unreal.MaterialEditingLibrary.get_material_property_input_node(
material, unreal.MaterialProperty.MP_BASE_COLOR
)
if base_color and hasattr(base_color, "set_editor_property"):
try:
base_color.set_editor_property("constant", spec["color"])
except Exception:
pass
roughness = unreal.MaterialEditingLibrary.get_material_property_input_node(
material, unreal.MaterialProperty.MP_ROUGHNESS
)
if roughness and hasattr(roughness, "set_editor_property"):
try:
roughness.set_editor_property("r", spec["roughness"])
except Exception:
pass
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False)
if spec.get("used_with_instanced_static_meshes"):
material.set_editor_property("used_with_instanced_static_meshes", True)
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False)
if key == "terrain":
rebuild_ground_zero_terrain_material(material, spec)
if key in {"tree", "shrub", "grass"}:
rebuild_ground_zero_foliage_material(material, spec)
created_or_loaded[key] = material
return created_or_loaded
def set_expression_property(expression, property_name, value):
try:
expression.set_editor_property(property_name, value)
return True
except Exception:
return False
def connect_expression(source, source_output, target, target_input):
try:
unreal.MaterialEditingLibrary.connect_material_expressions(source, source_output, target, target_input)
return True
except Exception as exc:
unreal.log_warning(f"Could not connect material expression input {target_input}: {exc}")
return False
def create_constant_color(material, color, x, y):
expression = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionConstant3Vector, x, y
)
expression.set_editor_property("constant", color)
return expression
def create_constant_scalar(material, value, x, y):
expression = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionConstant, x, y
)
expression.set_editor_property("r", value)
return expression
def rebuild_ground_zero_terrain_material(material, spec):
"""Build a simple procedural material so the landscape no longer reads as a flat color."""
if hasattr(unreal.MaterialEditingLibrary, "delete_all_material_expressions"):
unreal.MaterialEditingLibrary.delete_all_material_expressions(material)
dry_soil = create_constant_color(material, spec["dry_soil_color"], -900, -260)
scrub_green = create_constant_color(material, spec["scrub_green_color"], -900, -80)
sandy_path = create_constant_color(material, spec["sandy_path_color"], -900, 100)
broad_noise = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionNoise, -560, -160
)
set_expression_property(broad_noise, "scale", spec.get("noise_scale", 42.0))
set_expression_property(broad_noise, "quality", 3)
set_expression_property(broad_noise, "levels", 5)
fine_noise = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionNoise, -560, 100
)
set_expression_property(fine_noise, "scale", 145.0)
set_expression_property(fine_noise, "quality", 2)
set_expression_property(fine_noise, "levels", 3)
scrub_blend = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionLinearInterpolate, -260, -180
)
connect_expression(dry_soil, "", scrub_blend, "A")
connect_expression(scrub_green, "", scrub_blend, "B")
connect_expression(broad_noise, "", scrub_blend, "Alpha")
final_blend = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionLinearInterpolate, 40, -70
)
connect_expression(scrub_blend, "", final_blend, "A")
connect_expression(sandy_path, "", final_blend, "B")
connect_expression(fine_noise, "", final_blend, "Alpha")
unreal.MaterialEditingLibrary.connect_material_property(
final_blend, "", unreal.MaterialProperty.MP_BASE_COLOR
)
roughness = create_constant_scalar(material, spec["roughness"], -260, 160)
unreal.MaterialEditingLibrary.connect_material_property(
roughness, "", unreal.MaterialProperty.MP_ROUGHNESS
)
specular = create_constant_scalar(material, 0.18, -260, 260)
unreal.MaterialEditingLibrary.connect_material_property(
specular, "", unreal.MaterialProperty.MP_SPECULAR
)
material.set_editor_property("use_material_attributes", False)
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False)
unreal.log("Rebuilt Ground Zero terrain material with coastal scrub color variation and procedural noise.")
def rebuild_ground_zero_foliage_material(material, spec):
"""Use per-instance color variation so repeated foliage clumps do not tile visually."""
if hasattr(unreal.MaterialEditingLibrary, "delete_all_material_expressions"):
unreal.MaterialEditingLibrary.delete_all_material_expressions(material)
color_a = create_constant_color(material, spec["color"], -720, -160)
color_b = create_constant_color(material, spec["variation_color"], -720, 20)
blend = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionLinearInterpolate, -360, -80
)
connect_expression(color_a, "", blend, "A")
connect_expression(color_b, "", blend, "B")
random_expression_class = getattr(unreal, "MaterialExpressionPerInstanceRandom", None)
if random_expression_class:
random_expression = unreal.MaterialEditingLibrary.create_material_expression(
material, random_expression_class, -600, 170
)
connect_expression(random_expression, "", blend, "Alpha")
unreal.MaterialEditingLibrary.connect_material_property(
blend, "", unreal.MaterialProperty.MP_BASE_COLOR
)
roughness = create_constant_scalar(material, spec["roughness"], -360, 140)
unreal.MaterialEditingLibrary.connect_material_property(
roughness, "", unreal.MaterialProperty.MP_ROUGHNESS
)
material.set_editor_property("use_material_attributes", False)
material.set_editor_property("used_with_instanced_static_meshes", True)
if spec.get("two_sided"):
material.set_editor_property("two_sided", True)
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False)
def apply_material_to_actor_meshes(actor, material):
applied_count = 0
for component in actor.get_components_by_class(unreal.StaticMeshComponent):
slot_count = component.get_num_materials()
if slot_count == 0:
component.set_material(0, material)
applied_count += 1
continue
for slot_index in range(slot_count):
component.set_material(slot_index, material)
applied_count += 1
return applied_count
def apply_water_source_materials(actor, materials):
component_materials = {
"Mesh": materials["stone_resource"],
"StoneBankProxy": materials["stone_resource"],
"WaterSurfaceProxy": materials["fresh_water"],
"CollectMarkerProxy": materials["fresh_water"],
}
for component in actor.get_components_by_class(unreal.StaticMeshComponent):
material = component_materials.get(component.get_name())
if material:
component.set_material(0, material)
def material_key_for_actor_label(label):
for prefix, material_key in RESOURCE_MATERIAL_BY_LABEL_PREFIX.items():
if label.startswith(prefix):
return material_key
return None
def apply_landscape_material(material):
applied = 0
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
if isinstance(actor, unreal.Landscape):
actor.set_editor_property("landscape_material", material)
applied += 1
unreal.log(f"Applied Ground Zero terrain material to {applied} landscape actor(s).")
return applied
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 terrain_slope_degrees(height_values, x_cm, y_cm, sample_distance_cm):
dz_dx_m = (
terrain_elevation_m(height_values, x_cm + sample_distance_cm, y_cm)
- terrain_elevation_m(height_values, x_cm - sample_distance_cm, y_cm)
) / ((sample_distance_cm * 2.0) / 100.0)
dz_dy_m = (
terrain_elevation_m(height_values, x_cm, y_cm + sample_distance_cm)
- terrain_elevation_m(height_values, x_cm, y_cm - sample_distance_cm)
) / ((sample_distance_cm * 2.0) / 100.0)
return math.degrees(math.atan(math.sqrt((dz_dx_m * dz_dx_m) + (dz_dy_m * dz_dy_m))))
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 reserved_point(point, source_label, clearances):
return {
"point": point,
"source_label": source_label,
"clearances": clearances,
}
def reserved_point_clearance(reservation, foliage_family):
return reservation["clearances"].get(foliage_family, 0.0)
def add_sightline_corridor_reservations(reservations, label_points):
spacing_cm = SIGHTLINE_TUNING_CONFIG["corridor_sample_spacing_cm"]
corridor_clearances = SIGHTLINE_TUNING_CONFIG["corridor_clearance_cm"]
for start_label, end_label in SIGHTLINE_TUNING_CONFIG["sightline_pairs"]:
start = label_points.get(start_label)
end = label_points.get(end_label)
if not start or not end:
continue
length_cm = distance_2d(start, end)
sample_count = max(1, int(length_cm / spacing_cm))
for index in range(1, sample_count):
alpha = index / sample_count
point = unreal.Vector(
start.x + ((end.x - start.x) * alpha),
start.y + ((end.y - start.y) * alpha),
0.0,
)
reservations.append(
reserved_point(point, f"{start_label}->{end_label}", corridor_clearances)
)
def validate_safe_spawn_location(height_values, location_xy, resource_points, water_points):
failures = []
elevation_m = terrain_elevation_m(height_values, location_xy.x, location_xy.y)
slope_degrees = terrain_slope_degrees(
height_values,
location_xy.x,
location_xy.y,
SAFE_SPAWN_CONFIG["slope_sample_distance_cm"],
)
min_resource_distance_cm = min(distance_2d(location_xy, point) for point in resource_points)
min_water_distance_cm = min(distance_2d(location_xy, point) for point in water_points)
if elevation_m < SAFE_SPAWN_CONFIG["min_elevation_m"]:
failures.append(f"elevation {elevation_m:.2f}m below minimum")
if slope_degrees > SAFE_SPAWN_CONFIG["max_slope_degrees"]:
failures.append(f"slope {slope_degrees:.2f}deg above maximum")
if min_resource_distance_cm < SAFE_SPAWN_CONFIG["min_resource_distance_cm"]:
failures.append(f"resource distance {min_resource_distance_cm:.0f}cm below minimum")
if min_water_distance_cm < SAFE_SPAWN_CONFIG["min_water_distance_cm"]:
failures.append(f"water distance {min_water_distance_cm:.0f}cm below minimum")
return failures
def select_safe_spawn_location(height_values):
resource_points = [spec["location_xy"] for spec in BIOME_RESOURCE_ACTORS]
resource_points.extend(
spec["location_xy"]
for spec in DEMO_ACTORS
if spec["label"] in {"AGR_DemoWoodResource_01", "AGR_DemoFiberResource_01"}
)
water_points = [spec["location_xy"] for spec in WATER_SOURCE_ACTORS]
candidates = list(SAFE_SPAWN_CONFIG["candidate_locations_xy"])
fallback = SAFE_SPAWN_CONFIG["fallback_location_xy"]
if all(distance_2d(fallback, candidate) > 1.0 for candidate in candidates):
candidates.append(fallback)
rejected = []
for candidate in candidates:
failures = validate_safe_spawn_location(height_values, candidate, resource_points, water_points)
if not failures:
unreal.log(
"Selected safe Ground Zero spawn at "
f"({candidate.x:.0f}, {candidate.y:.0f}) "
f"elevation {terrain_elevation_m(height_values, candidate.x, candidate.y):.2f}m "
f"slope {terrain_slope_degrees(height_values, candidate.x, candidate.y, SAFE_SPAWN_CONFIG['slope_sample_distance_cm']):.2f}deg"
)
return candidate
rejected.append(f"({candidate.x:.0f}, {candidate.y:.0f}): {', '.join(failures)}")
raise RuntimeError("No safe Ground Zero spawn candidate found. Rejected: " + "; ".join(rejected))
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 apply_foliage_materials(foliage_actor, materials):
component_materials = {
"tree_instances": materials["tree"],
"shrub_instances": materials["shrub"],
"grass_instances": materials["grass"],
}
for property_name, material in component_materials.items():
component = foliage_actor.get_editor_property(property_name)
component.set_material(0, material)
def choose_foliage_points(height_values, family_name, zone, reservations, 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, reservation["point"]) < reserved_point_clearance(reservation, family_name)
for reservation in reservations
):
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 build_foliage_reservations(safe_spawn_location_xy):
label_points = {}
specs = DEMO_ACTORS + BIOME_RESOURCE_ACTORS + WATER_SOURCE_ACTORS
for spec in specs:
label = spec["label"]
location_xy = spec["location_xy"]
if label == SAFE_SPAWN_CONFIG["player_start_label"] and safe_spawn_location_xy is not None:
location_xy = safe_spawn_location_xy
label_points[label] = location_xy
reservations = []
critical_clearances = SIGHTLINE_TUNING_CONFIG["critical_clearance_cm"]
for label in SIGHTLINE_TUNING_CONFIG["critical_actor_labels"]:
point = label_points.get(label)
if point:
reservations.append(reserved_point(point, label, critical_clearances))
for spec in BIOME_RESOURCE_ACTORS:
reservations.append(reserved_point(spec["location_xy"], spec["label"], critical_clearances))
add_sightline_corridor_reservations(reservations, label_points)
unreal.log(f"Prepared {len(reservations)} Ground Zero foliage sightline reservation point(s).")
return reservations
def spawn_foliage_actor(height_values, materials, safe_spawn_location_xy):
reservations = build_foliage_reservations(safe_spawn_location_xy)
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)
apply_foliage_materials(foliage_actor, materials)
foliage_actor.clear_foliage()
existing_points = []
rng = random.Random(FOLIAGE_RANDOM_SEED)
for point in choose_foliage_points(height_values, "trees", FOLIAGE_ZONES["trees"], reservations, 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, "shrubs", FOLIAGE_ZONES["shrubs"], reservations, 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, "grass", FOLIAGE_ZONES["grass"], reservations, 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, materials, safe_spawn_location_xy=None):
location_xy = spec["location_xy"]
if spec["label"] == SAFE_SPAWN_CONFIG["player_start_label"] and safe_spawn_location_xy is not None:
location_xy = safe_spawn_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"])
try:
actor.set_editor_property("persistence_node_id", spec["label"])
except Exception:
pass
if spec["label"].startswith("AGR_GZ_FreshWaterSource"):
apply_water_source_materials(actor, materials)
else:
material_key = material_key_for_actor_label(spec["label"])
if material_key:
apply_material_to_actor_meshes(actor, materials[material_key])
unreal.log(f"Placed {spec['label']} at {actor.get_actor_location()}")
return actor
def spawn_environment_variation_actor(spec, height_values, materials):
location_xy = spec["location_xy"]
z = terrain_z_cm(height_values, location_xy.x, location_xy.y) + spec.get("z_offset", 0.0)
actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
unreal.StaticMeshActor,
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"])
actor.set_actor_scale3d(spec["scale"])
mesh_component = actor.static_mesh_component
mesh_component.set_editor_property("static_mesh", load_required_asset(VARIATION_MESHES[spec["mesh_key"]]))
mesh_component.set_material(0, materials[spec["material_key"]])
unreal.log(f"Placed {spec['label']} at {actor.get_actor_location()}")
return actor
def spawn_weather_exposure_zone(spec, height_values):
location_xy = spec["location_xy"]
z = terrain_z_cm(height_values, location_xy.x, location_xy.y) + spec.get("z_offset", 0.0)
actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
unreal.AgrarianWeatherExposureZone,
unreal.Vector(location_xy.x, location_xy.y, z),
unreal.Rotator(0.0, 0.0, 0.0),
spec["label"],
)
if not actor:
raise RuntimeError(f"Could not spawn {spec['label']}")
set_actor_label(actor, spec["label"])
actor.set_editor_property("exposure_zone_id", spec["zone_id"])
actor.set_editor_property("exposure_multiplier", spec["exposure_multiplier"])
actor.set_editor_property("temperature_offset_c", spec["temperature_offset_c"])
actor.exposure_volume.set_box_extent(spec["extent"], True)
unreal.log(
f"Placed {spec['label']} at {actor.get_actor_location()} "
f"exposure x{spec['exposure_multiplier']} temp {spec['temperature_offset_c']} C"
)
return actor
def spawn_map_boundary_volume():
actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
unreal.AgrarianMapBoundaryVolume,
MAP_BOUNDARY_CONFIG["location"],
unreal.Rotator(0.0, 0.0, 0.0),
MAP_BOUNDARY_CONFIG["label"],
)
if not actor:
raise RuntimeError(f"Could not spawn {MAP_BOUNDARY_CONFIG['label']}")
set_actor_label(actor, MAP_BOUNDARY_CONFIG["label"])
actor.set_editor_property("boundary_id", MAP_BOUNDARY_CONFIG["boundary_id"])
actor.set_editor_property("boundary_padding_cm", MAP_BOUNDARY_CONFIG["padding_cm"])
actor.set_editor_property("warning_distance_cm", MAP_BOUNDARY_CONFIG["warning_distance_cm"])
actor.set_editor_property("clamp_players_at_boundary", True)
actor.boundary_volume.set_box_extent(MAP_BOUNDARY_CONFIG["extent"], True)
unreal.log(
f"Placed {MAP_BOUNDARY_CONFIG['label']} at {actor.get_actor_location()} "
f"extent {MAP_BOUNDARY_CONFIG['extent']}"
)
return actor
def main():
if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
raise RuntimeError(f"Could not load map: {MAP_PATH}")
ensure_native_placeholder_meshes()
ensure_ground_zero_vegetation_meshes()
labels = {spec["label"] for spec in DEMO_ACTORS}
labels.update(LEGACY_DEMO_LIGHTING_LABELS)
labels.update(spec["label"] for spec in BIOME_RESOURCE_ACTORS)
labels.update(spec["label"] for spec in WATER_SOURCE_ACTORS)
labels.update(spec["label"] for spec in WEATHER_EXPOSURE_ZONES)
labels.update(spec["label"] for spec in ENVIRONMENT_VARIATION_ACTORS)
labels.update(spec["label"] for spec in RUIN_PLACEHOLDER_ACTORS)
labels.add(FOLIAGE_LABEL)
labels.add(MAP_BOUNDARY_CONFIG["label"])
remove_existing_demo_actors(labels)
materials = ensure_environment_materials()
apply_landscape_material(materials["terrain"])
height_values = load_heightmap()
safe_spawn_location_xy = select_safe_spawn_location(height_values)
spawn_foliage_actor(height_values, materials, safe_spawn_location_xy)
for spec in BIOME_RESOURCE_ACTORS:
spawn_demo_actor(spec, height_values, materials)
for spec in WATER_SOURCE_ACTORS:
spawn_demo_actor(spec, height_values, materials)
for spec in WEATHER_EXPOSURE_ZONES:
spawn_weather_exposure_zone(spec, height_values)
for spec in ENVIRONMENT_VARIATION_ACTORS:
spawn_environment_variation_actor(spec, height_values, materials)
for spec in RUIN_PLACEHOLDER_ACTORS:
spawn_environment_variation_actor(spec, height_values, materials)
spawn_map_boundary_volume()
for spec in DEMO_ACTORS:
spawn_demo_actor(spec, height_values, materials, safe_spawn_location_xy)
unreal.EditorLevelLibrary.save_current_level()
unreal.log("Ground Zero demo map setup complete.")
main()