extends GutTest func test_headless_animal_content_uses_only_stable_cues_and_never_asset_paths() -> void: var content := ContentCatalog.create_core() assert_true(content.is_valid(), "%s" % [content.get_errors()]) var animals := content.get_animals() assert_eq(animals.size(), 2) for animal in animals: assert_true(AnimalDefinition.is_valid_stable_id(animal.presentation_cue_id)) assert_false("res://" in String(animal.presentation_cue_id)) assert_false(_resource_has_asset_path_property(animal)) assert_true(animal.validate().is_empty()) var missing_asset_cue := PresentationCueDefinition.new() missing_asset_cue.cue_id = &"animal.missing_asset" missing_asset_cue.scene_path = "res://does/not/exist/animal.tscn" assert_true(missing_asset_cue.validate_metadata().is_empty()) assert_false(missing_asset_cue.validate_assets().is_empty()) var headless_catalog := ContentCatalog.new() var pack := SimulationContentPack.new() pack.pack_id = &"headless_animals" pack.display_name = "Headless animals" pack.animals = [_animal(&"headless_goat", missing_asset_cue.cue_id)] var errors := headless_catalog.rebuild( [pack] as Array[SimulationContentPack], [], [missing_asset_cue.cue_id] ) assert_true(errors.is_empty(), "%s" % [errors]) assert_true(headless_catalog.is_valid()) errors = headless_catalog.rebuild( [pack] as Array[SimulationContentPack], [], [&"animal.some_other_cue"] ) assert_true(_has_error(errors, "unknown presentation cue 'animal.missing_asset'")) assert_false(headless_catalog.is_valid()) func test_presentation_catalog_rejects_missing_assets_and_duplicate_cues_transactionally() -> void: var missing_id := PresentationCueDefinition.new() var catalog := PresentationCatalog.new() var errors := catalog.rebuild([missing_id] as Array[PresentationCueDefinition], false) assert_true(_has_error(errors, "cue_id is invalid")) assert_false(catalog.is_valid()) var valid := _cue(&"animal.valid", "res://world/animals/grazer/cozy_grazer.tscn") var duplicate := _cue(&"animal.valid", "res://world/animals/goat/cozy_goat.tscn") errors = catalog.rebuild([valid, duplicate] as Array[PresentationCueDefinition], false) assert_true(_has_error(errors, "Duplicate presentation cue 'animal.valid'")) assert_false(catalog.is_valid()) assert_true(catalog.get_cue_ids().is_empty()) assert_null(catalog.get_definition(&"animal.valid")) var missing := _cue(&"animal.missing", "res://world/animals/absent.tscn") errors = catalog.rebuild([missing] as Array[PresentationCueDefinition], true) assert_true(_has_error(errors, "scene_path does not resolve"), "%s" % [errors]) assert_false(catalog.is_valid()) assert_true(catalog.get_cue_ids().is_empty()) func test_goat_and_sheep_are_data_only_content_additions_using_one_scene_contract() -> void: var animal_catalog := AnimalCatalog.create_core() var presentation_catalog := PresentationCatalog.create_core() assert_not_null(animal_catalog) assert_true(presentation_catalog.is_valid(), "%s" % [presentation_catalog.get_errors()]) assert_eq( presentation_catalog.get_cue_ids(), [PresentationCueIds.ANIMAL_DOMESTIC_GOAT, PresentationCueIds.ANIMAL_DOMESTIC_SHEEP] ) var goat := animal_catalog.get_definition(&"domestic_goat") var sheep := animal_catalog.get_definition(&"domestic_sheep") var goat_cue := presentation_catalog.get_definition(goat.presentation_cue_id) var sheep_cue := presentation_catalog.get_definition(sheep.presentation_cue_id) assert_eq(goat.behavior_id, AnimalDefinition.BEHAVIOR_GRAZER_ROUTINE) assert_eq(sheep.behavior_id, AnimalDefinition.BEHAVIOR_GRAZER_ROUTINE) assert_ne(goat.presentation_cue_id, sheep.presentation_cue_id) assert_eq(goat_cue.scene_path, sheep_cue.scene_path) assert_eq(goat_cue.animation_hook_id, sheep_cue.animation_hook_id) assert_eq(goat_cue.animation_hook_id, AnimalDefinition.BEHAVIOR_GRAZER_ROUTINE) assert_true(bool(goat_cue.parameters["show_horns"])) assert_false(bool(sheep_cue.parameters["show_horns"])) var factory := AnimalFactory.new(animal_catalog, presentation_catalog) var goat_node := factory.create_presentation(&"domestic_goat", &"factory_goat", "Goat") var sheep_node := factory.create_presentation(&"domestic_sheep", &"factory_sheep", "Sheep") assert_not_null(goat_node) assert_not_null(sheep_node) add_child_autofree(goat_node) add_child_autofree(sheep_node) await wait_process_frames(1) assert_eq(goat_node.get_script(), sheep_node.get_script()) assert_eq(goat_node.get_node("Visual").get_script(), sheep_node.get_node("Visual").get_script()) assert_true(goat_node.get_node("Visual/HeadRoot/HornLeft").visible) assert_false(sheep_node.get_node("Visual/HeadRoot/HornLeft").visible) func test_factory_requires_an_injected_mapping_for_every_animal_cue() -> void: var animal_catalog := AnimalCatalog.create_core() var headless_factory := AnimalFactory.new(animal_catalog) assert_null(headless_factory.get_presentation_catalog()) assert_eq(headless_factory.last_error, "") assert_null(headless_factory.create_presentation(&"domestic_goat", &"headless_goat", "Goat")) assert_true("must be injected" in headless_factory.last_error) var presentation_catalog := PresentationCatalog.new() var only_goat := PresentationCatalog.create_core().get_definition( PresentationCueIds.ANIMAL_DOMESTIC_GOAT ) assert_true( presentation_catalog.rebuild([only_goat] as Array[PresentationCueDefinition]).is_empty() ) var factory := AnimalFactory.new(animal_catalog, presentation_catalog) assert_null(factory.create_presentation(&"domestic_sheep", &"missing_sheep", "Sheep")) assert_true("Unknown presentation cue 'animal.domestic_sheep'" in factory.last_error) func _animal(animal_id: StringName, cue_id: StringName) -> AnimalDefinition: var animal := AnimalDefinition.new() animal.animal_definition_id = animal_id animal.species_id = &"test_species" animal.display_name = "Headless animal" animal.supported_action_ids = [] animal.presentation_cue_id = cue_id return animal func _cue(cue_id: StringName, scene_path: String) -> PresentationCueDefinition: var cue := PresentationCueDefinition.new() cue.cue_id = cue_id cue.scene_path = scene_path return cue func _resource_has_asset_path_property(resource: Resource) -> bool: for property in resource.get_property_list(): if (int(property["usage"]) & PROPERTY_USAGE_SCRIPT_VARIABLE) == 0: continue var property_name := String(property["name"]) if property_name.ends_with("_path") or property_name in ["scene", "icon", "material"]: return true return false func _has_error(errors: Array[String], fragment: String) -> bool: for error in errors: if fragment in error: return true return false