class_name ContentCatalog extends RefCounted const CORE_PACK_PATH := "res://simulation/definitions/packs/core.tres" var _valid := false var _errors: Array[String] = [] var _packs: Array[SimulationContentPack] = [] var _actions: Array[ActionDefinition] = [] var _professions: Array[ProfessionDefinition] = [] var _items: Array[ItemDefinition] = [] var _enemies: Array[EnemyDefinition] = [] var _packs_by_id: Dictionary = {} var _actions_by_id: Dictionary = {} var _professions_by_id: Dictionary = {} var _items_by_id: Dictionary = {} var _enemies_by_id: Dictionary = {} static func create_core(supported_handler_ids: Array[StringName] = []) -> ContentCatalog: var catalog := ContentCatalog.new() var pack := load(CORE_PACK_PATH) as SimulationContentPack if pack == null: catalog._errors = ["Core content pack failed to load from '%s'" % CORE_PACK_PATH] return catalog var packs: Array[SimulationContentPack] = [pack] catalog.rebuild(packs, supported_handler_ids) return catalog func rebuild( content_packs: Array[SimulationContentPack], supported_handler_ids: Array[StringName] = [] ) -> Array[String]: _clear_published_content() var errors: Array[String] = [] var pack_list := content_packs.duplicate() pack_list.sort_custom(_sort_packs) var supported_handlers := {} for handler_id in supported_handler_ids: if handler_id.is_empty(): errors.append("Supported handler ID is empty") elif supported_handlers.has(handler_id): errors.append("Duplicate supported handler '%s'" % handler_id) else: supported_handlers[handler_id] = true var packs_by_id := {} var actions_by_id := {} var professions_by_id := {} var items_by_id := {} var enemies_by_id := {} var actions: Array[ActionDefinition] = [] var professions: Array[ProfessionDefinition] = [] var items: Array[ItemDefinition] = [] var enemies: Array[EnemyDefinition] = [] for pack in pack_list: if pack == null: errors.append("Content pack list contains a null pack") continue for error in pack.validate(): errors.append("Pack '%s': %s" % [pack.pack_id, error]) if packs_by_id.has(pack.pack_id): errors.append("Duplicate pack_id '%s'" % pack.pack_id) else: packs_by_id[pack.pack_id] = pack _collect_actions(pack, actions, actions_by_id, errors) _collect_professions(pack, professions, professions_by_id, errors) _collect_items(pack, items, items_by_id, errors) _collect_enemies(pack, enemies, enemies_by_id, errors) for pack in pack_list: if pack == null: continue for required_pack_id in pack.required_pack_ids: if not required_pack_id.is_empty() and not packs_by_id.has(required_pack_id): errors.append( ( "Pack '%s' references unknown required pack '%s'" % [pack.pack_id, required_pack_id] ) ) for required_handler_id in pack.required_handler_ids: if ( not required_handler_id.is_empty() and not supported_handlers.has(required_handler_id) ): errors.append( ( "Pack '%s' references unknown required handler '%s'" % [pack.pack_id, required_handler_id] ) ) _validate_action_references( actions, actions_by_id, professions_by_id, items_by_id, supported_handlers, errors ) _validate_enemy_references(enemies, items_by_id, errors) errors.sort() _errors = errors if not errors.is_empty(): return get_errors() actions.sort_custom(_sort_actions) professions.sort_custom(_sort_professions) items.sort_custom(_sort_items) enemies.sort_custom(_sort_enemies) _packs = pack_list _actions = actions _professions = professions _items = items _enemies = enemies _packs_by_id = packs_by_id _actions_by_id = actions_by_id _professions_by_id = professions_by_id _items_by_id = items_by_id _enemies_by_id = enemies_by_id _valid = true return [] func is_valid() -> bool: return _valid func get_errors() -> Array[String]: return _errors.duplicate() func get_packs() -> Array[SimulationContentPack]: return _packs.duplicate() func get_actions() -> Array[ActionDefinition]: return _actions.duplicate() func get_professions() -> Array[ProfessionDefinition]: return _professions.duplicate() func get_items() -> Array[ItemDefinition]: return _items.duplicate() func get_enemies() -> Array[EnemyDefinition]: return _enemies.duplicate() func get_pack(pack_id: StringName) -> SimulationContentPack: return _packs_by_id.get(pack_id) as SimulationContentPack func get_action(action_id: StringName) -> ActionDefinition: return _actions_by_id.get(action_id) as ActionDefinition func get_profession(profession_id: StringName) -> ProfessionDefinition: return _professions_by_id.get(profession_id) as ProfessionDefinition func get_item(item_id: StringName) -> ItemDefinition: return _items_by_id.get(item_id) as ItemDefinition func get_enemy(enemy_id: StringName) -> EnemyDefinition: return _enemies_by_id.get(enemy_id) as EnemyDefinition func _clear_published_content() -> void: _valid = false _errors.clear() _packs.clear() _actions.clear() _professions.clear() _items.clear() _enemies.clear() _packs_by_id.clear() _actions_by_id.clear() _professions_by_id.clear() _items_by_id.clear() _enemies_by_id.clear() static func _collect_actions( pack: SimulationContentPack, definitions: Array[ActionDefinition], definitions_by_id: Dictionary, errors: Array[String] ) -> void: for definition in pack.actions: if definition == null: continue if definitions_by_id.has(definition.action_id): errors.append("Duplicate action_id '%s'" % definition.action_id) continue definitions_by_id[definition.action_id] = definition definitions.append(definition) static func _collect_professions( pack: SimulationContentPack, definitions: Array[ProfessionDefinition], definitions_by_id: Dictionary, errors: Array[String] ) -> void: for definition in pack.professions: if definition == null: continue if definitions_by_id.has(definition.profession_id): errors.append("Duplicate profession_id '%s'" % definition.profession_id) continue definitions_by_id[definition.profession_id] = definition definitions.append(definition) static func _collect_items( pack: SimulationContentPack, definitions: Array[ItemDefinition], definitions_by_id: Dictionary, errors: Array[String] ) -> void: for definition in pack.items: if definition == null: continue if definitions_by_id.has(definition.item_id): errors.append("Duplicate item_id '%s'" % definition.item_id) continue definitions_by_id[definition.item_id] = definition definitions.append(definition) static func _collect_enemies( pack: SimulationContentPack, definitions: Array[EnemyDefinition], definitions_by_id: Dictionary, errors: Array[String] ) -> void: for definition in pack.enemies: if definition == null: continue if definitions_by_id.has(definition.enemy_id): errors.append("Duplicate enemy_id '%s'" % definition.enemy_id) continue definitions_by_id[definition.enemy_id] = definition definitions.append(definition) static func _validate_action_references( actions: Array[ActionDefinition], actions_by_id: Dictionary, professions_by_id: Dictionary, items_by_id: Dictionary, supported_handlers: Dictionary, errors: Array[String] ) -> void: for definition in actions: for handler_field in [ ["selection policy", definition.selection_policy_id], ["target policy", definition.target_policy_id], ["completion handler", definition.completion_handler_id], ]: var handler_id: StringName = handler_field[1] if not handler_id.is_empty() and not supported_handlers.has(handler_id): errors.append( ( "Action '%s' references unknown %s '%s'" % [definition.action_id, handler_field[0], handler_id] ) ) if ( not definition.preferred_profession_id.is_empty() and not professions_by_id.has(definition.preferred_profession_id) ): errors.append( ( "Action '%s' references unknown profession '%s'" % [definition.action_id, definition.preferred_profession_id] ) ) if ( definition.target_type == SimulationIds.TARGET_RESOURCE and not actions_by_id.has(definition.resource_action_id) ): errors.append( ( "Action '%s' references unknown resource action '%s'" % [definition.action_id, definition.resource_action_id] ) ) if ( not definition.completion_cost_resource_id.is_empty() and not items_by_id.has(definition.completion_cost_resource_id) ): errors.append( ( "Action '%s' references unknown completion cost item '%s'" % [definition.action_id, definition.completion_cost_resource_id] ) ) elif not definition.completion_cost_resource_id.is_empty(): var cost_item := ( items_by_id.get(definition.completion_cost_resource_id) as ItemDefinition ) if cost_item != null and not cost_item.is_resource(): errors.append( ( "Action '%s' references non-resource completion cost item '%s'" % [definition.action_id, definition.completion_cost_resource_id] ) ) static func _validate_enemy_references( enemies: Array[EnemyDefinition], items_by_id: Dictionary, errors: Array[String] ) -> void: for definition in enemies: var weapon := items_by_id.get(definition.weapon_id) as ItemDefinition if weapon == null: errors.append( ( "Enemy '%s' references unknown weapon '%s'" % [definition.enemy_id, definition.weapon_id] ) ) elif not weapon.is_weapon(): errors.append( ( "Enemy '%s' references non-weapon item '%s'" % [definition.enemy_id, definition.weapon_id] ) ) static func _sort_packs(first: SimulationContentPack, second: SimulationContentPack) -> bool: if first == null: return second != null if second == null: return false return String(first.pack_id) < String(second.pack_id) static func _sort_actions(first: ActionDefinition, second: ActionDefinition) -> bool: return String(first.action_id) < String(second.action_id) static func _sort_professions(first: ProfessionDefinition, second: ProfessionDefinition) -> bool: return String(first.profession_id) < String(second.profession_id) static func _sort_items(first: ItemDefinition, second: ItemDefinition) -> bool: return String(first.item_id) < String(second.item_id) static func _sort_enemies(first: EnemyDefinition, second: EnemyDefinition) -> bool: return String(first.enemy_id) < String(second.enemy_id)