278 lines
9.8 KiB
GDScript
278 lines
9.8 KiB
GDScript
extends SceneTree
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
_validate_catalog_and_legacy_names()
|
|
_validate_version_12_migration()
|
|
_validate_appearance_profile_migration_and_strictness()
|
|
_validate_signatures_cover_independent_accessories()
|
|
await _validate_presenter_selection()
|
|
print("Character customization expansion validation: PASS")
|
|
quit()
|
|
|
|
|
|
func _validate_catalog_and_legacy_names() -> void:
|
|
var defaults := CharacterCustomizationCatalog.default_snapshot()
|
|
assert(CharacterCustomizationCatalog.validate_snapshot(defaults))
|
|
assert(defaults.size() == CharacterCustomizationCatalog.SNAPSHOT_IDS.size())
|
|
assert(CharacterCustomizationCatalog.SNAPSHOT_IDS.size() == 23)
|
|
assert(CharacterCustomizationCatalog.CATEGORY_IDS.count("special") == 1)
|
|
assert("special" not in CharacterCustomizationCatalog.SNAPSHOT_IDS)
|
|
assert(CharacterCustomizationCatalog.SPECIAL_SLOT_IDS == PackedStringArray([
|
|
"special_back",
|
|
"special_beak",
|
|
"special_head",
|
|
"special_horns",
|
|
"special_tusks",
|
|
"special_wings",
|
|
]))
|
|
|
|
var expected_counts := {
|
|
"species": 21,
|
|
"arms": 3,
|
|
"ears": 21,
|
|
"tail": 17,
|
|
"special_back": 1,
|
|
"special_beak": 2,
|
|
"special_head": 1,
|
|
"special_horns": 2,
|
|
"special_tusks": 2,
|
|
"special_wings": 3,
|
|
}
|
|
var all_mesh_names: Array[String] = []
|
|
for category_id: String in expected_counts:
|
|
var options := CharacterCustomizationCatalog.options_for(category_id)
|
|
var option_ids: Array[String] = []
|
|
var mesh_names: Array[String] = []
|
|
for option: Dictionary in options:
|
|
var option_id := str(option.get("id", ""))
|
|
assert(not option_id.is_empty())
|
|
assert(option_id not in option_ids)
|
|
option_ids.append(option_id)
|
|
var mesh_name := CharacterCustomizationCatalog.model_mesh_name(
|
|
category_id,
|
|
option_id,
|
|
)
|
|
if option_id == "none":
|
|
assert(mesh_name.is_empty())
|
|
continue
|
|
assert(not mesh_name.is_empty())
|
|
assert(mesh_name not in mesh_names)
|
|
assert(mesh_name not in all_mesh_names)
|
|
mesh_names.append(mesh_name)
|
|
all_mesh_names.append(mesh_name)
|
|
assert(mesh_names.size() == int(expected_counts[category_id]))
|
|
assert(
|
|
CharacterCustomizationCatalog.model_mesh_names(category_id).size()
|
|
== int(expected_counts[category_id])
|
|
)
|
|
assert(
|
|
CharacterCustomizationCatalog.model_mesh_names("special").size()
|
|
== 11
|
|
)
|
|
|
|
assert(
|
|
CharacterCustomizationCatalog.model_mesh_name("arms", "body_arms")
|
|
== "arms_mammal"
|
|
)
|
|
assert(
|
|
CharacterCustomizationCatalog.model_mesh_name("ears", "pointy_long")
|
|
== "ears_long"
|
|
)
|
|
assert(
|
|
CharacterCustomizationCatalog.model_mesh_name("ears", "pointy_short")
|
|
== "ears_cat"
|
|
)
|
|
assert(
|
|
CharacterCustomizationCatalog.model_mesh_name("ears", "pointy_wide")
|
|
== "ears_fox"
|
|
)
|
|
|
|
var legacy := defaults.duplicate(true)
|
|
legacy.erase("arms")
|
|
for special_slot_id: String in CharacterCustomizationCatalog.SPECIAL_SLOT_IDS:
|
|
legacy.erase(special_slot_id)
|
|
legacy["ears"] = "pointy_wide"
|
|
var sanitized := CharacterCustomizationCatalog.sanitized_snapshot(legacy)
|
|
assert(CharacterCustomizationCatalog.validate_snapshot(sanitized))
|
|
assert(sanitized["arms"] == "mammal")
|
|
assert(sanitized["ears"] == "fox")
|
|
for special_slot_id: String in CharacterCustomizationCatalog.SPECIAL_SLOT_IDS:
|
|
assert(sanitized[special_slot_id] == "none")
|
|
|
|
|
|
func _validate_version_12_migration() -> void:
|
|
var legacy_appearance := CharacterCustomizationCatalog.default_snapshot()
|
|
legacy_appearance.erase("arms")
|
|
for special_slot_id: String in CharacterCustomizationCatalog.SPECIAL_SLOT_IDS:
|
|
legacy_appearance.erase(special_slot_id)
|
|
legacy_appearance["ears"] = "pointy_short"
|
|
var manager := PlayerSaveManager.new()
|
|
root.add_child(manager)
|
|
var migrated: Dictionary = manager.call(
|
|
"_migrate_save",
|
|
{
|
|
"save_version": 12,
|
|
"character": {
|
|
"appearance": legacy_appearance,
|
|
},
|
|
},
|
|
12,
|
|
)
|
|
assert(int(migrated.get("save_version", -1)) == 13)
|
|
var appearance: Dictionary = migrated["character"]["appearance"]
|
|
assert(CharacterCustomizationCatalog.validate_snapshot(appearance))
|
|
assert(appearance["arms"] == "mammal")
|
|
assert(appearance["ears"] == "cat")
|
|
for special_slot_id: String in CharacterCustomizationCatalog.SPECIAL_SLOT_IDS:
|
|
assert(appearance[special_slot_id] == "none")
|
|
manager.queue_free()
|
|
|
|
|
|
func _validate_appearance_profile_migration_and_strictness() -> void:
|
|
var legacy := CharacterCustomizationCatalog.default_snapshot()
|
|
legacy.erase("arms")
|
|
for special_slot_id: String in CharacterCustomizationCatalog.SPECIAL_SLOT_IDS:
|
|
legacy.erase(special_slot_id)
|
|
legacy["ears"] = "pointy_wide"
|
|
var legacy_path := "user://appearance-v1-%d.json" % Time.get_ticks_usec()
|
|
var legacy_file := FileAccess.open(legacy_path, FileAccess.WRITE)
|
|
assert(legacy_file != null)
|
|
legacy_file.store_string(JSON.stringify({
|
|
"format_version": 1,
|
|
"appearance": legacy,
|
|
}))
|
|
legacy_file.close()
|
|
var legacy_store := PlayerAppearanceStore.new()
|
|
root.add_child(legacy_store)
|
|
legacy_store.configure_storage(legacy_path, null)
|
|
assert(legacy_store.load_preferences())
|
|
var migrated := legacy_store.get_snapshot()
|
|
assert(CharacterCustomizationCatalog.validate_snapshot(migrated))
|
|
assert(migrated["arms"] == "mammal")
|
|
assert(migrated["ears"] == "fox")
|
|
legacy_store.queue_free()
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(legacy_path))
|
|
|
|
var malformed := CharacterCustomizationCatalog.default_snapshot()
|
|
malformed["species"] = "definitely_not_a_head"
|
|
var current_path := "user://appearance-v2-%d.json" % Time.get_ticks_usec()
|
|
var current_file := FileAccess.open(current_path, FileAccess.WRITE)
|
|
assert(current_file != null)
|
|
current_file.store_string(JSON.stringify({
|
|
"format_version": PlayerAppearanceStore.FORMAT_VERSION,
|
|
"appearance": malformed,
|
|
}))
|
|
current_file.close()
|
|
var current_store := PlayerAppearanceStore.new()
|
|
root.add_child(current_store)
|
|
current_store.configure_storage(current_path, null)
|
|
assert(not current_store.load_preferences())
|
|
assert(not current_store.restore_from_save_data(malformed, false))
|
|
current_store.queue_free()
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(current_path))
|
|
|
|
|
|
func _validate_signatures_cover_independent_accessories() -> void:
|
|
var baseline := CharacterCustomizationCatalog.default_snapshot()
|
|
var previous_signature := NetworkProfileProtocol.signature_fields({
|
|
"appearance": baseline,
|
|
})
|
|
var selections := {
|
|
"special_back": "shell",
|
|
"special_beak": "bird",
|
|
"special_head": "fin",
|
|
"special_horns": "goat",
|
|
"special_tusks": "boar",
|
|
"special_wings": "bat",
|
|
}
|
|
for category_id: String in CharacterCustomizationCatalog.SPECIAL_SLOT_IDS:
|
|
var changed := baseline.duplicate(true)
|
|
changed[category_id] = selections[category_id]
|
|
assert(CharacterCustomizationCatalog.validate_snapshot(changed))
|
|
var changed_signature := NetworkProfileProtocol.signature_fields({
|
|
"appearance": changed,
|
|
})
|
|
assert(changed_signature != previous_signature)
|
|
previous_signature = changed_signature
|
|
|
|
|
|
func _validate_presenter_selection() -> void:
|
|
var visuals := PlayerVisualPresenter.instantiate_visuals()
|
|
root.add_child(visuals)
|
|
var skeleton := visuals.find_child("Skeleton3D", true, false) as Skeleton3D
|
|
assert(skeleton != null)
|
|
var snapshot := CharacterCustomizationCatalog.default_snapshot()
|
|
PlayerVisualPresenter.apply_appearance(visuals, snapshot)
|
|
_assert_exact_selection(skeleton, "species", "head_round")
|
|
_assert_exact_selection(skeleton, "arms", "arms_mammal")
|
|
_assert_exact_selection(skeleton, "ears", "")
|
|
_assert_exact_selection(skeleton, "tail", "")
|
|
for special_slot_id: String in CharacterCustomizationCatalog.SPECIAL_SLOT_IDS:
|
|
_assert_exact_selection(skeleton, special_slot_id, "")
|
|
|
|
for species_option: Dictionary in CharacterCustomizationCatalog.options_for(
|
|
"species"
|
|
):
|
|
var species_id := str(species_option.get("id", ""))
|
|
snapshot["species"] = species_id
|
|
PlayerVisualPresenter.apply_appearance(visuals, snapshot)
|
|
var selected_head := CharacterCustomizationCatalog.model_mesh_name(
|
|
"species",
|
|
species_id,
|
|
)
|
|
_assert_exact_selection(skeleton, "species", selected_head)
|
|
for feature_name: String in ["eyes", "nose", "mouth"]:
|
|
var feature := skeleton.get_node_or_null(
|
|
"%s_%s" % [selected_head, feature_name]
|
|
) as MeshInstance3D
|
|
assert(feature != null and feature.visible)
|
|
var feature_material := feature.material_override as StandardMaterial3D
|
|
assert(feature_material != null)
|
|
assert(feature_material.albedo_texture != null)
|
|
|
|
snapshot["arms"] = "avian"
|
|
snapshot["ears"] = "owl"
|
|
snapshot["tail"] = "donkey"
|
|
snapshot["special_back"] = "shell"
|
|
snapshot["special_beak"] = "bird"
|
|
snapshot["special_head"] = "fin"
|
|
snapshot["special_horns"] = "goat"
|
|
snapshot["special_tusks"] = "elephant"
|
|
snapshot["special_wings"] = "moth"
|
|
assert(CharacterCustomizationCatalog.validate_snapshot(snapshot))
|
|
PlayerVisualPresenter.apply_appearance(visuals, snapshot)
|
|
_assert_exact_selection(skeleton, "arms", "arms_avian")
|
|
_assert_exact_selection(skeleton, "ears", "ears_owl")
|
|
_assert_exact_selection(skeleton, "tail", "tail_donkey")
|
|
_assert_exact_selection(skeleton, "special_back", "back_shell")
|
|
_assert_exact_selection(skeleton, "special_beak", "beak_bird")
|
|
_assert_exact_selection(skeleton, "special_head", "head_fin")
|
|
_assert_exact_selection(skeleton, "special_horns", "horns_goat")
|
|
_assert_exact_selection(skeleton, "special_tusks", "tusks_elephant")
|
|
_assert_exact_selection(skeleton, "special_wings", "wings_moth")
|
|
|
|
var wings := skeleton.get_node("wings_moth") as MeshInstance3D
|
|
var cached_material := wings.material_override
|
|
assert(cached_material != null)
|
|
PlayerVisualPresenter.apply_appearance(visuals, snapshot)
|
|
assert(wings.material_override == cached_material)
|
|
visuals.queue_free()
|
|
await process_frame
|
|
|
|
|
|
func _assert_exact_selection(
|
|
skeleton: Skeleton3D,
|
|
category_id: String,
|
|
expected_mesh_name: String,
|
|
) -> void:
|
|
for mesh_name: String in CharacterCustomizationCatalog.model_mesh_names(
|
|
category_id
|
|
):
|
|
var mesh := skeleton.get_node_or_null(mesh_name) as Node3D
|
|
assert(mesh != null)
|
|
assert(mesh.visible == (mesh_name == expected_mesh_name))
|