310 lines
13 KiB
Python
310 lines
13 KiB
Python
import unreal
|
|
|
|
|
|
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
|
|
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
|
|
MATERIALS = {
|
|
"terrain": "/Game/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub",
|
|
"tree": "/Game/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak",
|
|
"shrub": "/Game/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush",
|
|
"grass": "/Game/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal",
|
|
"wood_resource": "/Game/Agrarian/Materials/M_AGR_GZ_Wood_Resource",
|
|
"fiber_resource": "/Game/Agrarian/Materials/M_AGR_GZ_Fiber_Resource",
|
|
"edible_plant_resource": "/Game/Agrarian/Materials/M_AGR_GZ_EdiblePlant_Resource",
|
|
"stone_resource": "/Game/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone",
|
|
"fresh_water": "/Game/Agrarian/Materials/M_AGR_GZ_FreshWater",
|
|
}
|
|
EXPECTED_FOLIAGE_COUNTS = {
|
|
"trees": 96,
|
|
"shrubs": 220,
|
|
"grass": 420,
|
|
}
|
|
EXPECTED_FOLIAGE_MESHES = {
|
|
"tree_instances": "/Game/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoastalOak_Proxy",
|
|
"shrub_instances": "/Game/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoyoteBrush_Proxy",
|
|
"grass_instances": "/Game/Agrarian/Environment/Vegetation/SM_AGR_GZ_DryGrassClump_Proxy",
|
|
}
|
|
EXPECTED_FOLIAGE_CULL_DISTANCES = {
|
|
"tree_instances": (65000, 95000),
|
|
"shrub_instances": (28000, 52000),
|
|
"grass_instances": (9000, 22000),
|
|
}
|
|
FORBIDDEN_FOLIAGE_MESH_PREFIXES = (
|
|
"/Engine/BasicShapes/",
|
|
"/Game/LevelPrototyping/",
|
|
)
|
|
RESOURCE_MATERIALS = {
|
|
"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",
|
|
}
|
|
VARIATION_PREFIX = "AGR_GZ_EnvVar_"
|
|
EXPECTED_VARIATION_COUNT = 31
|
|
EXPECTED_VARIATION_MATERIALS = {
|
|
"AGR_GZ_EnvVar_FirstLook_OakCanopy": "tree",
|
|
"AGR_GZ_EnvVar_FirstLook_OakTrunk": "wood_resource",
|
|
"AGR_GZ_EnvVar_FirstLook_Brush": "shrub",
|
|
"AGR_GZ_EnvVar_FirstLook_GrassMat": "grass",
|
|
"AGR_GZ_EnvVar_FirstLook_Rock": "stone_resource",
|
|
"AGR_GZ_EnvVar_Tree_Canopy": "tree",
|
|
"AGR_GZ_EnvVar_Tree_Trunk": "wood_resource",
|
|
"AGR_GZ_EnvVar_Bush": "shrub",
|
|
"AGR_GZ_EnvVar_Brush": "shrub",
|
|
"AGR_GZ_EnvVar_Grass": "grass",
|
|
"AGR_GZ_EnvVar_Rock": "stone_resource",
|
|
"AGR_GZ_EnvVar_Water_Bank": "stone_resource",
|
|
"AGR_GZ_EnvVar_Water_Reed": "grass",
|
|
"AGR_GZ_EnvVar_Water": "fresh_water",
|
|
}
|
|
|
|
|
|
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 asset_path(asset):
|
|
if not asset:
|
|
return ""
|
|
return asset.get_path_name().split(".", 1)[0]
|
|
|
|
|
|
def get_component_property(component, property_name, default=None):
|
|
try:
|
|
return component.get_editor_property(property_name)
|
|
except Exception:
|
|
return getattr(component, property_name, default)
|
|
|
|
|
|
def static_mesh_vertex_count(mesh):
|
|
library = getattr(unreal, "EditorStaticMeshLibrary", None)
|
|
if not mesh or not library:
|
|
return -1
|
|
try:
|
|
return library.get_number_verts(mesh, 0)
|
|
except Exception:
|
|
return -1
|
|
|
|
|
|
def material_key_for_label(label):
|
|
for prefix, material_key in RESOURCE_MATERIALS.items():
|
|
if label.startswith(prefix):
|
|
return material_key
|
|
return None
|
|
|
|
|
|
def variation_material_key_for_label(label):
|
|
for prefix, material_key in EXPECTED_VARIATION_MATERIALS.items():
|
|
if label.startswith(prefix):
|
|
return material_key
|
|
return None
|
|
|
|
|
|
def assert_asset(path):
|
|
asset = unreal.EditorAssetLibrary.load_asset(path)
|
|
if not asset:
|
|
raise RuntimeError(f"Missing required environment material: {path}")
|
|
return asset
|
|
|
|
|
|
def verify_terrain_material_is_not_flat(material, failures):
|
|
project_root = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir())
|
|
material_package = project_root + "Content/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub.uasset"
|
|
try:
|
|
with open(material_package, "rb") as handle:
|
|
package_bytes = handle.read()
|
|
except Exception as exc:
|
|
failures.append(f"could not inspect terrain material package: {exc}")
|
|
return
|
|
|
|
noise_count = package_bytes.count(b"MaterialExpressionNoise")
|
|
lerp_count = package_bytes.count(b"MaterialExpressionLinearInterpolate")
|
|
color_count = package_bytes.count(b"MaterialExpressionConstant3Vector")
|
|
|
|
if noise_count < 1:
|
|
failures.append("terrain material should include a noise expression for color breakup")
|
|
if lerp_count < 1:
|
|
failures.append("terrain material should include a blend expression instead of a flat base color")
|
|
if color_count < 1:
|
|
failures.append("terrain material should include coastal scrub color vector expressions")
|
|
|
|
|
|
def verify_foliage_material_has_variation(material_path_name, failures):
|
|
project_root = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir())
|
|
package_path = project_root + "Content" + material_path_name.replace("/Game", "") + ".uasset"
|
|
try:
|
|
with open(package_path, "rb") as handle:
|
|
package_bytes = handle.read()
|
|
except Exception as exc:
|
|
failures.append(f"could not inspect foliage material package {material_path_name}: {exc}")
|
|
return
|
|
|
|
if package_bytes.count(b"MaterialExpressionLinearInterpolate") < 1:
|
|
failures.append(f"{material_path_name} should blend foliage color variation")
|
|
if package_bytes.count(b"MaterialExpressionConstant3Vector") < 1:
|
|
failures.append(f"{material_path_name} should include foliage color vectors")
|
|
|
|
|
|
def main():
|
|
if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
|
|
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
|
|
|
materials = {key: assert_asset(path) for key, path in MATERIALS.items()}
|
|
actors = unreal.EditorLevelLibrary.get_all_level_actors()
|
|
failures = []
|
|
|
|
landscapes = [actor for actor in actors if isinstance(actor, unreal.Landscape)]
|
|
if not landscapes:
|
|
failures.append("no landscape actor found")
|
|
for landscape in landscapes:
|
|
assigned = material_path(landscape.get_editor_property("landscape_material"))
|
|
expected = MATERIALS["terrain"]
|
|
if assigned != expected:
|
|
failures.append(f"landscape material expected {expected}, got {assigned}")
|
|
verify_terrain_material_is_not_flat(materials["terrain"], failures)
|
|
|
|
foliage_actors = [actor for actor in actors if get_actor_label(actor) == FOLIAGE_LABEL]
|
|
if len(foliage_actors) != 1:
|
|
failures.append(f"expected one {FOLIAGE_LABEL}, found {len(foliage_actors)}")
|
|
else:
|
|
foliage = foliage_actors[0]
|
|
counts = {
|
|
"trees": foliage.get_tree_instance_count(),
|
|
"shrubs": foliage.get_shrub_instance_count(),
|
|
"grass": foliage.get_grass_instance_count(),
|
|
}
|
|
for key, expected_count in EXPECTED_FOLIAGE_COUNTS.items():
|
|
if counts[key] != expected_count:
|
|
failures.append(f"{key} expected {expected_count}, got {counts[key]}")
|
|
|
|
component_expectations = {
|
|
"tree_instances": "tree",
|
|
"shrub_instances": "shrub",
|
|
"grass_instances": "grass",
|
|
}
|
|
for property_name, material_key in component_expectations.items():
|
|
component = foliage.get_editor_property(property_name)
|
|
static_mesh = component.get_editor_property("static_mesh")
|
|
mesh_path = asset_path(static_mesh)
|
|
expected_mesh = EXPECTED_FOLIAGE_MESHES[property_name]
|
|
if mesh_path != expected_mesh:
|
|
failures.append(f"{property_name} mesh expected {expected_mesh}, got {mesh_path}")
|
|
if mesh_path.startswith(FORBIDDEN_FOLIAGE_MESH_PREFIXES):
|
|
failures.append(f"{property_name} still uses placeholder/basic mesh {mesh_path}")
|
|
vertex_count = static_mesh_vertex_count(static_mesh)
|
|
if vertex_count <= 0:
|
|
failures.append(f"{property_name} mesh {mesh_path} has no renderable vertices")
|
|
|
|
assigned = material_path(component.get_material(0))
|
|
expected = MATERIALS[material_key]
|
|
if assigned != expected:
|
|
failures.append(f"{property_name} material expected {expected}, got {assigned}")
|
|
verify_foliage_material_has_variation(expected, failures)
|
|
|
|
start_cull, end_cull = EXPECTED_FOLIAGE_CULL_DISTANCES[property_name]
|
|
actual_start = int(get_component_property(component, "instance_start_cull_distance", -1))
|
|
actual_end = int(get_component_property(component, "instance_end_cull_distance", -1))
|
|
if actual_start != start_cull or actual_end != end_cull:
|
|
failures.append(
|
|
f"{property_name} cull distances expected {start_cull}/{end_cull}, "
|
|
f"got {actual_start}/{actual_end}"
|
|
)
|
|
|
|
checked_resource_actors = 0
|
|
for actor in actors:
|
|
label = get_actor_label(actor)
|
|
material_key = material_key_for_label(label)
|
|
if not material_key:
|
|
continue
|
|
|
|
checked_resource_actors += 1
|
|
expected = MATERIALS[material_key]
|
|
mesh_components = actor.get_components_by_class(unreal.StaticMeshComponent)
|
|
if not mesh_components:
|
|
failures.append(f"{label} has no static mesh component for material assignment")
|
|
continue
|
|
assigned_any = any(material_path(component.get_material(0)) == expected for component in mesh_components)
|
|
if not assigned_any:
|
|
failures.append(f"{label} did not use expected material {expected}")
|
|
|
|
if checked_resource_actors < 10:
|
|
failures.append(f"expected at least 10 dressed resource/water actors, checked {checked_resource_actors}")
|
|
|
|
variation_actors = [actor for actor in actors if get_actor_label(actor).startswith(VARIATION_PREFIX)]
|
|
if len(variation_actors) != EXPECTED_VARIATION_COUNT:
|
|
failures.append(f"expected {EXPECTED_VARIATION_COUNT} environment variation actors, found {len(variation_actors)}")
|
|
|
|
variation_meshes = set()
|
|
variation_scales = set()
|
|
variation_material_keys = set()
|
|
for actor in variation_actors:
|
|
label = get_actor_label(actor)
|
|
expected_material_key = variation_material_key_for_label(label)
|
|
if not expected_material_key:
|
|
failures.append(f"{label} does not match a known variation material family")
|
|
continue
|
|
|
|
mesh_components = actor.get_components_by_class(unreal.StaticMeshComponent)
|
|
if not mesh_components:
|
|
failures.append(f"{label} has no static mesh component")
|
|
continue
|
|
|
|
component = mesh_components[0]
|
|
mesh = component.get_editor_property("static_mesh")
|
|
variation_meshes.add(mesh.get_path_name().split(".", 1)[0] if mesh else "")
|
|
scale = actor.get_actor_scale3d()
|
|
variation_scales.add((round(scale.x, 2), round(scale.y, 2), round(scale.z, 2)))
|
|
variation_material_keys.add(expected_material_key)
|
|
|
|
assigned = material_path(component.get_material(0))
|
|
expected = MATERIALS[expected_material_key]
|
|
if assigned != expected:
|
|
failures.append(f"{label} material expected {expected}, got {assigned}")
|
|
|
|
if len(variation_meshes) < 4:
|
|
failures.append(f"expected at least 4 variation mesh silhouettes, got {len(variation_meshes)}")
|
|
if len(variation_scales) < 18:
|
|
failures.append(f"environment variation actors should use at least 18 scale profiles, got {len(variation_scales)}")
|
|
if not {"tree", "wood_resource", "shrub", "grass", "stone_resource", "fresh_water"}.issubset(variation_material_keys):
|
|
failures.append("environment variation actors do not cover tree, shrub, grass, resource, and water families")
|
|
|
|
docs = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) + "Docs/Terrain/GroundZeroNaturalEnvironmentPass.md"
|
|
roadmap = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) + "AGRARIAN_DEVELOPMENT_ROADMAP.md"
|
|
for path, snippet in [
|
|
(docs, "asset variation"),
|
|
(docs, "procedural coastal scrub terrain material"),
|
|
(docs, "native low-poly coastal scrub vegetation meshes"),
|
|
(roadmap, "[x] Replace grey-box environment presentation with an MVP natural environment pass"),
|
|
(roadmap, "[x] Add first-pass environment asset variation"),
|
|
(roadmap, "[x] Replace or upgrade the terrain material first so Ground Zero no longer reads as flat tan placeholder ground."),
|
|
(roadmap, "[x] Replace or upgrade grasses, shrubs, and trees with believable coastal-scrub vegetation assets"),
|
|
]:
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
text = handle.read()
|
|
if snippet not in text:
|
|
failures.append(f"{path} missing {snippet}")
|
|
|
|
if failures:
|
|
raise RuntimeError("Ground Zero natural environment verification failed: " + "; ".join(failures))
|
|
|
|
unreal.log(
|
|
"Ground Zero natural environment verification complete: "
|
|
f"{len(materials)} materials, {len(landscapes)} landscape(s), "
|
|
f"{checked_resource_actors} dressed resource/water actor(s), {len(variation_actors)} variation actor(s)."
|
|
)
|
|
|
|
|
|
main()
|