refactor: separate content from presentation assets
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
class_name PresentationCatalog
|
||||
extends RefCounted
|
||||
|
||||
const CORE_CATALOG_PATH := "res://world/presentation/catalogs/core_presentation.tres"
|
||||
|
||||
var _valid := false
|
||||
var _errors: Array[String] = []
|
||||
var _definitions: Array[PresentationCueDefinition] = []
|
||||
var _definitions_by_id: Dictionary = {}
|
||||
|
||||
|
||||
static func create_core(validate_assets := true) -> PresentationCatalog:
|
||||
var catalog := PresentationCatalog.new()
|
||||
var resource := load(CORE_CATALOG_PATH) as PresentationCatalogResource
|
||||
if resource == null:
|
||||
catalog._errors = ["Core presentation catalog failed to load"]
|
||||
return catalog
|
||||
catalog.rebuild(resource.cues, validate_assets)
|
||||
return catalog
|
||||
|
||||
|
||||
func rebuild(
|
||||
definitions: Array[PresentationCueDefinition], validate_assets := true
|
||||
) -> Array[String]:
|
||||
_valid = false
|
||||
_errors.clear()
|
||||
_definitions.clear()
|
||||
_definitions_by_id.clear()
|
||||
var ordered := definitions.duplicate()
|
||||
ordered.sort_custom(_definition_before)
|
||||
for definition in ordered:
|
||||
if definition == null:
|
||||
_errors.append("Presentation catalog contains a null cue")
|
||||
continue
|
||||
for error in (
|
||||
definition.validate_assets() if validate_assets else definition.validate_metadata()
|
||||
):
|
||||
_errors.append("Cue '%s': %s" % [definition.cue_id, error])
|
||||
if _definitions_by_id.has(definition.cue_id):
|
||||
_errors.append("Duplicate presentation cue '%s'" % definition.cue_id)
|
||||
else:
|
||||
_definitions_by_id[definition.cue_id] = definition
|
||||
_definitions.append(definition)
|
||||
_errors.sort()
|
||||
if not _errors.is_empty():
|
||||
_definitions.clear()
|
||||
_definitions_by_id.clear()
|
||||
return get_errors()
|
||||
_valid = true
|
||||
return []
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return _valid
|
||||
|
||||
|
||||
func get_errors() -> Array[String]:
|
||||
return _errors.duplicate()
|
||||
|
||||
|
||||
func get_definition(cue_id: StringName) -> PresentationCueDefinition:
|
||||
return _definitions_by_id.get(cue_id) as PresentationCueDefinition
|
||||
|
||||
|
||||
func get_cue_ids() -> Array[StringName]:
|
||||
var result: Array[StringName] = []
|
||||
for definition in _definitions:
|
||||
result.append(definition.cue_id)
|
||||
return result
|
||||
|
||||
|
||||
func instantiate_scene(cue_id: StringName) -> Node:
|
||||
var definition := get_definition(cue_id)
|
||||
return definition.instantiate_scene() if definition != null else null
|
||||
|
||||
|
||||
func _definition_before(
|
||||
first: PresentationCueDefinition, second: PresentationCueDefinition
|
||||
) -> bool:
|
||||
if first == null:
|
||||
return second != null
|
||||
if second == null:
|
||||
return false
|
||||
return String(first.cue_id) < String(second.cue_id)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dd1bsnixqs5hr
|
||||
@@ -0,0 +1,4 @@
|
||||
class_name PresentationCatalogResource
|
||||
extends Resource
|
||||
|
||||
@export var cues: Array[PresentationCueDefinition] = []
|
||||
@@ -0,0 +1 @@
|
||||
uid://g61fe7gl8hqo
|
||||
@@ -0,0 +1,89 @@
|
||||
class_name PresentationCueDefinition
|
||||
extends Resource
|
||||
|
||||
const ALLOWED_PARAMETER_KEYS := {
|
||||
"visual_scale": true,
|
||||
"body_scale": true,
|
||||
"fluff_scale": true,
|
||||
"show_horns": true,
|
||||
"show_beard": true,
|
||||
}
|
||||
|
||||
@export var cue_id: StringName
|
||||
@export_file("*.tscn") var scene_path: String
|
||||
@export_file var icon_path: String
|
||||
@export_file var material_path: String
|
||||
@export var animation_hook_id: StringName
|
||||
@export var parameters: Dictionary = {}
|
||||
|
||||
|
||||
func validate_metadata() -> Array[String]:
|
||||
var errors: Array[String] = []
|
||||
if not AnimalDefinition.is_valid_stable_id(cue_id):
|
||||
errors.append("cue_id is invalid")
|
||||
_validate_optional_path(scene_path, "scene_path", ["tscn", "scn"], errors)
|
||||
_validate_optional_path(icon_path, "icon_path", ["png", "svg", "webp"], errors)
|
||||
_validate_optional_path(material_path, "material_path", ["tres", "res", "material"], errors)
|
||||
if (
|
||||
not animation_hook_id.is_empty()
|
||||
and not AnimalDefinition.is_valid_stable_id(animation_hook_id)
|
||||
):
|
||||
errors.append("animation_hook_id is invalid for '%s'" % cue_id)
|
||||
_validate_parameters(errors)
|
||||
return errors
|
||||
|
||||
|
||||
func validate_assets() -> Array[String]:
|
||||
var errors := validate_metadata()
|
||||
if not errors.is_empty():
|
||||
return errors
|
||||
for path_field in [
|
||||
["scene_path", scene_path, "PackedScene"],
|
||||
["icon_path", icon_path, "Texture2D"],
|
||||
["material_path", material_path, "Material"],
|
||||
]:
|
||||
var path: String = path_field[1]
|
||||
if not path.is_empty() and not ResourceLoader.exists(path, path_field[2]):
|
||||
errors.append("%s does not resolve for '%s'" % [path_field[0], cue_id])
|
||||
return errors
|
||||
|
||||
|
||||
func get_parameters() -> Dictionary:
|
||||
return parameters.duplicate(true)
|
||||
|
||||
|
||||
func instantiate_scene() -> Node:
|
||||
if scene_path.is_empty():
|
||||
return null
|
||||
var scene := load(scene_path) as PackedScene
|
||||
return scene.instantiate() if scene != null else null
|
||||
|
||||
|
||||
func _validate_parameters(errors: Array[String]) -> void:
|
||||
for raw_key in parameters:
|
||||
if raw_key is not String and raw_key is not StringName:
|
||||
errors.append("parameter key must be a string for '%s'" % cue_id)
|
||||
continue
|
||||
var key := String(raw_key)
|
||||
if not ALLOWED_PARAMETER_KEYS.has(key):
|
||||
errors.append("unsupported parameter '%s' for '%s'" % [key, cue_id])
|
||||
continue
|
||||
var value: Variant = parameters[raw_key]
|
||||
if key.ends_with("_scale"):
|
||||
if value is not Vector3 or not _is_positive_vector(value):
|
||||
errors.append("parameter '%s' must be a positive Vector3 for '%s'" % [key, cue_id])
|
||||
elif value is not bool:
|
||||
errors.append("parameter '%s' must be bool for '%s'" % [key, cue_id])
|
||||
|
||||
|
||||
func _validate_optional_path(
|
||||
path: String, field: String, extensions: Array[String], errors: Array[String]
|
||||
) -> void:
|
||||
if path.is_empty():
|
||||
return
|
||||
if not path.begins_with("res://") or path.get_extension().to_lower() not in extensions:
|
||||
errors.append("%s is invalid for '%s'" % [field, cue_id])
|
||||
|
||||
|
||||
func _is_positive_vector(value: Vector3) -> bool:
|
||||
return value.is_finite() and value.x > 0.0 and value.y > 0.0 and value.z > 0.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://bqm8k67sgvnb0
|
||||
@@ -53,7 +53,7 @@ func get_pending_ids() -> Array[StringName]:
|
||||
var result: Array[StringName] = []
|
||||
for stable_id in _pending_by_id:
|
||||
result.append(StringName(stable_id))
|
||||
result.sort()
|
||||
result.sort_custom(_stable_id_before)
|
||||
return result
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ func get_in_flight_ids() -> Array[StringName]:
|
||||
var result: Array[StringName] = []
|
||||
for stable_id in _in_flight_ids:
|
||||
result.append(StringName(stable_id))
|
||||
result.sort()
|
||||
result.sort_custom(_stable_id_before)
|
||||
return result
|
||||
|
||||
|
||||
@@ -75,3 +75,7 @@ func _request_before(first: Dictionary, second: Dictionary) -> bool:
|
||||
if int(first["priority"]) != int(second["priority"]):
|
||||
return int(first["priority"]) > int(second["priority"])
|
||||
return String(first["stable_id"]) < String(second["stable_id"])
|
||||
|
||||
|
||||
func _stable_id_before(first: StringName, second: StringName) -> bool:
|
||||
return String(first) < String(second)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[gd_resource type="Resource" script_class="PresentationCueDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/presentation/PresentationCueDefinition.gd" id="1_cue"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_cue")
|
||||
cue_id = &"animal.domestic_goat"
|
||||
scene_path = "res://world/animals/grazer/cozy_grazer.tscn"
|
||||
animation_hook_id = &"grazer_routine"
|
||||
parameters = {
|
||||
"body_scale": Vector3(1, 1, 1),
|
||||
"fluff_scale": Vector3(1, 1, 1),
|
||||
"show_beard": true,
|
||||
"show_horns": true,
|
||||
"visual_scale": Vector3(1, 1, 1)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[gd_resource type="Resource" script_class="PresentationCueDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/presentation/PresentationCueDefinition.gd" id="1_cue"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_cue")
|
||||
cue_id = &"animal.domestic_sheep"
|
||||
scene_path = "res://world/animals/grazer/cozy_grazer.tscn"
|
||||
animation_hook_id = &"grazer_routine"
|
||||
parameters = {
|
||||
"body_scale": Vector3(1.12, 1.08, 1.12),
|
||||
"fluff_scale": Vector3(1.16, 1.14, 1.16),
|
||||
"show_beard": false,
|
||||
"show_horns": false,
|
||||
"visual_scale": Vector3(1.08, 1.04, 1.08)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_resource type="Resource" script_class="PresentationCatalogResource" load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/presentation/PresentationCatalogResource.gd" id="1_catalog"]
|
||||
[ext_resource type="Resource" path="res://world/presentation/catalogs/animal_domestic_goat.tres" id="2_goat"]
|
||||
[ext_resource type="Resource" path="res://world/presentation/catalogs/animal_domestic_sheep.tres" id="3_sheep"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_catalog")
|
||||
cues = Array[ExtResource("2_goat")]([ExtResource("2_goat"), ExtResource("3_sheep")])
|
||||
Reference in New Issue
Block a user