Expand character customization assets
This commit is contained in:
parent
23cf53fa14
commit
df2a4d7d77
31 changed files with 2423 additions and 215 deletions
|
|
@ -59,7 +59,7 @@ func _validate_player_save_migration() -> void:
|
|||
{"save_version": 11},
|
||||
11,
|
||||
)
|
||||
assert(int(migrated.get("save_version", -1)) == 12)
|
||||
assert(int(migrated.get("save_version", -1)) == 13)
|
||||
assert(
|
||||
migrated.get("message_bottles", {})
|
||||
== PlayerBottleMessageStateType.default_save_data()
|
||||
|
|
|
|||
278
tests/character_customization_expansion_validation.gd
Normal file
278
tests/character_customization_expansion_validation.gd
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
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))
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://drj5amsurmywk
|
||||
211
tests/character_customizer_ui_validation.gd
Normal file
211
tests/character_customizer_ui_validation.gd
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
extends SceneTree
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var page := ProfilePage.new()
|
||||
page.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
root.add_child(page)
|
||||
await process_frame
|
||||
await process_frame
|
||||
page.set(
|
||||
"_draft_appearance",
|
||||
CharacterCustomizationCatalog.default_snapshot(),
|
||||
)
|
||||
var preview := page.get("_preview") as ProfilePreview
|
||||
var default_appearance := (
|
||||
CharacterCustomizationCatalog.default_snapshot()
|
||||
)
|
||||
preview.apply_appearance_profile(default_appearance)
|
||||
var visible_bounds_result: Dictionary = preview.call(
|
||||
"_visible_mesh_bounds"
|
||||
)
|
||||
assert(bool(visible_bounds_result.get("has_bounds", false)))
|
||||
var visible_bounds: AABB = visible_bounds_result.get("bounds", AABB())
|
||||
assert(
|
||||
(preview.get("_camera_target") as Vector3).is_equal_approx(
|
||||
visible_bounds.get_center()
|
||||
)
|
||||
)
|
||||
var stable_fit := float(preview.get("_camera_fit_distance"))
|
||||
preview.apply_appearance_profile(default_appearance)
|
||||
assert(is_equal_approx(
|
||||
float(preview.get("_camera_fit_distance")), stable_fit
|
||||
))
|
||||
var shell := page.find_child("UtilityMainBox", true, false) as Control
|
||||
assert(shell != null)
|
||||
var shell_position := shell.position
|
||||
var shell_size := shell.size
|
||||
for category_id: String in ["species", "arms", "ears", "tail"]:
|
||||
page.call("_select_category", category_id)
|
||||
await process_frame
|
||||
await process_frame
|
||||
var scroll := page.find_child(
|
||||
"ModelOptionScroll", true, false
|
||||
) as ScrollContainer
|
||||
var grid := page.find_child(
|
||||
"ModelOptionGrid", true, false
|
||||
) as GridContainer
|
||||
assert(scroll != null)
|
||||
assert(grid != null)
|
||||
assert(grid.columns == ProfilePage.MODEL_OPTION_GRID_COLUMNS)
|
||||
assert(
|
||||
grid.get_child_count()
|
||||
== CharacterCustomizationCatalog.options_for(category_id).size()
|
||||
)
|
||||
assert(
|
||||
scroll.horizontal_scroll_mode
|
||||
== ScrollContainer.SCROLL_MODE_DISABLED
|
||||
)
|
||||
assert(
|
||||
scroll.vertical_scroll_mode
|
||||
== ScrollContainer.SCROLL_MODE_AUTO
|
||||
)
|
||||
assert(shell.position.is_equal_approx(shell_position))
|
||||
assert(shell.size.is_equal_approx(shell_size))
|
||||
|
||||
page.call("_select_category", "species")
|
||||
await process_frame
|
||||
await process_frame
|
||||
var species_scroll := page.find_child(
|
||||
"ModelOptionScroll", true, false
|
||||
) as ScrollContainer
|
||||
assert(species_scroll != null)
|
||||
var species_scroll_max := int(
|
||||
maxf(
|
||||
species_scroll.get_v_scroll_bar().max_value
|
||||
- species_scroll.get_v_scroll_bar().page,
|
||||
0.0,
|
||||
)
|
||||
)
|
||||
assert(species_scroll_max > 0)
|
||||
var saved_position := mini(species_scroll_max, 36)
|
||||
species_scroll.scroll_vertical = saved_position
|
||||
page.call("_select_category", "arms")
|
||||
await process_frame
|
||||
page.call("_select_category", "species")
|
||||
await process_frame
|
||||
await process_frame
|
||||
species_scroll = page.find_child(
|
||||
"ModelOptionScroll", true, false
|
||||
) as ScrollContainer
|
||||
assert(species_scroll != null)
|
||||
assert(species_scroll.scroll_vertical == saved_position)
|
||||
page.call("activate")
|
||||
page.call("set_interactive", true)
|
||||
var category_scroll := page.find_child(
|
||||
"CategoryScroll", true, false
|
||||
) as ScrollContainer
|
||||
assert(category_scroll != null and category_scroll.follow_focus)
|
||||
page.set("_controller_zone", ProfilePage.ControllerZone.CATEGORIES)
|
||||
page.call("_apply_controller_zone_focus")
|
||||
var category_list := page.get("_category_list") as VBoxContainer
|
||||
var last_category_button := category_list.get_child(
|
||||
category_list.get_child_count() - 1
|
||||
) as Button
|
||||
assert(last_category_button != null)
|
||||
last_category_button.grab_focus()
|
||||
await process_frame
|
||||
assert(category_scroll.scroll_vertical > 0)
|
||||
|
||||
page.set("_controller_zone", ProfilePage.ControllerZone.OPTIONS)
|
||||
page.call("_apply_controller_zone_focus")
|
||||
var species_grid := page.find_child(
|
||||
"ModelOptionGrid", true, false
|
||||
) as GridContainer
|
||||
var last_species_button := species_grid.get_child(
|
||||
species_grid.get_child_count() - 1
|
||||
) as Button
|
||||
assert(last_species_button != null)
|
||||
last_species_button.grab_focus()
|
||||
await process_frame
|
||||
assert(species_scroll.scroll_vertical > saved_position)
|
||||
|
||||
page.call("_select_category", "special")
|
||||
await process_frame
|
||||
await process_frame
|
||||
var special_scroll := page.find_child(
|
||||
"SpecialOptionScroll", true, false
|
||||
) as ScrollContainer
|
||||
assert(special_scroll != null)
|
||||
for field_id: String in CharacterCustomizationCatalog.SPECIAL_SLOT_IDS:
|
||||
var slot_name := "SpecialSlot_%s" % field_id.trim_prefix("special_")
|
||||
var slot := page.find_child(slot_name, true, false) as HBoxContainer
|
||||
assert(slot != null)
|
||||
assert(slot.get_child_count() == 2)
|
||||
var slot_label := slot.get_child(0) as Label
|
||||
var choices := slot.get_child(1) as HBoxContainer
|
||||
assert(slot_label != null and choices != null)
|
||||
assert(
|
||||
slot_label.text
|
||||
== CharacterCustomizationCatalog.special_slot_label(field_id)
|
||||
)
|
||||
assert(
|
||||
choices.get_child_count()
|
||||
== CharacterCustomizationCatalog.options_for(field_id).size()
|
||||
)
|
||||
assert(shell.position.is_equal_approx(shell_position))
|
||||
assert(shell.size.is_equal_approx(shell_size))
|
||||
|
||||
var back_slot := page.find_child(
|
||||
"SpecialSlot_back", true, false
|
||||
) as HBoxContainer
|
||||
var back_choices := back_slot.get_child(1) as HBoxContainer
|
||||
var shell_button := back_choices.get_child(1) as Button
|
||||
assert(shell_button != null)
|
||||
shell_button.pressed.emit()
|
||||
await process_frame
|
||||
await process_frame
|
||||
var draft: Dictionary = page.get("_draft_appearance")
|
||||
assert(str(draft.get("special_back", "")) == "shell")
|
||||
for other_id: String in CharacterCustomizationCatalog.SPECIAL_SLOT_IDS:
|
||||
if other_id != "special_back":
|
||||
assert(str(draft.get(other_id, "")) == "none")
|
||||
var wings_slot := page.find_child(
|
||||
"SpecialSlot_wings", true, false
|
||||
) as HBoxContainer
|
||||
var wings_choices := wings_slot.get_child(1) as HBoxContainer
|
||||
var moth_button := wings_choices.get_child(
|
||||
wings_choices.get_child_count() - 1
|
||||
) as Button
|
||||
assert(moth_button != null)
|
||||
moth_button.grab_focus()
|
||||
await process_frame
|
||||
moth_button.pressed.emit()
|
||||
await process_frame
|
||||
await process_frame
|
||||
draft = page.get("_draft_appearance")
|
||||
assert(str(draft.get("special_back", "")) == "shell")
|
||||
assert(str(draft.get("special_wings", "")) == "moth")
|
||||
var restored_focus := page.get_viewport().gui_get_focus_owner() as Button
|
||||
assert(restored_focus != null)
|
||||
assert(
|
||||
str(restored_focus.get_meta(&"appearance_focus_key", ""))
|
||||
== "special_wings:moth"
|
||||
)
|
||||
|
||||
var ordinary_bounds := AABB(Vector3.ZERO, Vector3(1.0, 2.0, 1.0))
|
||||
var wide_bounds := AABB(Vector3.ZERO, Vector3(4.0, 2.0, 1.0))
|
||||
var tall_bounds := AABB(Vector3.ZERO, Vector3(1.0, 4.0, 1.0))
|
||||
var viewport_size := Vector2(320.0, 330.0)
|
||||
var ordinary_fit := ProfilePreview.calculate_camera_fit_distance(
|
||||
ordinary_bounds, viewport_size, 75.0
|
||||
)
|
||||
assert(
|
||||
ProfilePreview.calculate_camera_fit_distance(
|
||||
wide_bounds, viewport_size, 75.0
|
||||
) > ordinary_fit
|
||||
)
|
||||
assert(
|
||||
ProfilePreview.calculate_camera_fit_distance(
|
||||
tall_bounds, viewport_size, 75.0
|
||||
) > ordinary_fit
|
||||
)
|
||||
|
||||
print("Character customizer UI validation: PASS")
|
||||
page.queue_free()
|
||||
await process_frame
|
||||
quit()
|
||||
1
tests/character_customizer_ui_validation.gd.uid
Normal file
1
tests/character_customizer_ui_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://by314ps17phf3
|
||||
|
|
@ -41,6 +41,116 @@ const EXPECTED_ANIMATIONS: Array[StringName] = [
|
|||
&"walking",
|
||||
&"walking_show",
|
||||
]
|
||||
const EXPECTED_HEAD_MESHES: PackedStringArray = [
|
||||
"head_round",
|
||||
"head_pointy",
|
||||
"head_anteater",
|
||||
"head_axolotl",
|
||||
"head_bat",
|
||||
"head_bird",
|
||||
"head_boar",
|
||||
"head_butterfly",
|
||||
"head_donkey",
|
||||
"head_elephant",
|
||||
"head_goat",
|
||||
"head_hamster",
|
||||
"head_horse",
|
||||
"head_moth",
|
||||
"head_opossum",
|
||||
"head_owl",
|
||||
"head_panda",
|
||||
"head_pig",
|
||||
"head_shark",
|
||||
"head_sheep",
|
||||
"head_snail",
|
||||
]
|
||||
const EXPECTED_ARM_MESHES: PackedStringArray = [
|
||||
"arms_mammal", "arms_fish", "arms_avian",
|
||||
]
|
||||
const EXPECTED_EAR_MESHES: PackedStringArray = [
|
||||
"ears_cat",
|
||||
"ears_long",
|
||||
"ears_fox",
|
||||
"ears_bunny",
|
||||
"ears_antlers_round",
|
||||
"ears_bear",
|
||||
"ears_anteater",
|
||||
"ears_bat",
|
||||
"ears_boar",
|
||||
"ears_butterfly",
|
||||
"ears_donkey",
|
||||
"ears_axolotl",
|
||||
"ears_elephant",
|
||||
"ears_goat",
|
||||
"ears_hamster",
|
||||
"ears_horse",
|
||||
"ears_opossum",
|
||||
"ears_moth",
|
||||
"ears_pig",
|
||||
"ears_owl",
|
||||
"ears_panda",
|
||||
]
|
||||
const EXPECTED_TAIL_MESHES: PackedStringArray = [
|
||||
"tails_gator",
|
||||
"tails_fox",
|
||||
"tails_bunny",
|
||||
"tails_cat",
|
||||
"tails_bear",
|
||||
"tails_pointy",
|
||||
"tails_anteater",
|
||||
"tails_boar",
|
||||
"tails_butterfly",
|
||||
"tail_donkey",
|
||||
"tails_axolotl",
|
||||
"tails_horse",
|
||||
"tails_opossum",
|
||||
"tails_moth",
|
||||
"tails_pig",
|
||||
"tails_shark",
|
||||
"tails_bird",
|
||||
]
|
||||
const EXPECTED_SPECIAL_MESHES: PackedStringArray = [
|
||||
"tusks_boar",
|
||||
"tusks_elephant",
|
||||
"wings_butterfly",
|
||||
"wings_bat",
|
||||
"wings_moth",
|
||||
"horns_goat",
|
||||
"horns_sheep",
|
||||
"back_shell",
|
||||
"head_fin",
|
||||
"beak_owl",
|
||||
"beak_bird",
|
||||
]
|
||||
const FACE_FEATURES: PackedStringArray = ["eyes", "nose", "mouth"]
|
||||
const EXCLUDED_AUTHORING_MESHES: PackedStringArray = [
|
||||
"frog_eyes_1",
|
||||
"frog_eyes_2",
|
||||
"frog_head_1",
|
||||
"frog_head_2",
|
||||
"rod_socket_preview-noimp",
|
||||
]
|
||||
const EXPECTED_BONE_PARENTS: Dictionary = {
|
||||
"root": "",
|
||||
"hips": "root",
|
||||
"spine": "hips",
|
||||
"clavicle.L": "spine",
|
||||
"upper_arm.L": "clavicle.L",
|
||||
"forearm.L": "upper_arm.L",
|
||||
"hand.L": "forearm.L",
|
||||
"clavicle.R": "spine",
|
||||
"upper_arm.R": "clavicle.R",
|
||||
"forearm.R": "upper_arm.R",
|
||||
"hand.R": "forearm.R",
|
||||
"rod_socket": "hand.R",
|
||||
"rod_socket.001": "rod_socket",
|
||||
"neck": "spine",
|
||||
"head": "neck",
|
||||
"thigh.L": "hips",
|
||||
"shin.L": "thigh.L",
|
||||
"thigh.R": "hips",
|
||||
"shin.R": "thigh.R",
|
||||
}
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
|
|
@ -57,6 +167,21 @@ func _run() -> void:
|
|||
) as AnimationPlayer
|
||||
assert(skeleton != null)
|
||||
assert(animation_player != null)
|
||||
assert(character.get_path_to(skeleton) == ^"CharacterRig/Skeleton3D")
|
||||
assert(skeleton.get_bone_count() == EXPECTED_BONE_PARENTS.size())
|
||||
for bone_name: String in EXPECTED_BONE_PARENTS:
|
||||
var bone_index := skeleton.find_bone(bone_name)
|
||||
assert(bone_index >= 0, "Missing bone: %s" % bone_name)
|
||||
var parent_index := skeleton.get_bone_parent(bone_index)
|
||||
var actual_parent := (
|
||||
str(skeleton.get_bone_name(parent_index))
|
||||
if parent_index >= 0
|
||||
else ""
|
||||
)
|
||||
assert(
|
||||
actual_parent == str(EXPECTED_BONE_PARENTS[bone_name]),
|
||||
"Unexpected parent for bone %s: %s" % [bone_name, actual_parent],
|
||||
)
|
||||
|
||||
var socket_index := skeleton.find_bone("rod_socket")
|
||||
assert(socket_index >= 0)
|
||||
|
|
@ -79,26 +204,65 @@ func _run() -> void:
|
|||
"Animation %s lost its authored rod socket pose." % expected,
|
||||
)
|
||||
|
||||
var arms := character.find_child(
|
||||
"body_arms",
|
||||
true,
|
||||
false,
|
||||
) as MeshInstance3D
|
||||
assert(arms != null)
|
||||
var blended_arm_vertices := _validate_mesh_weights(arms)
|
||||
assert(blended_arm_vertices > 0, "body_arms has no blended joint weights.")
|
||||
var body := character.find_child(
|
||||
"body_main",
|
||||
true,
|
||||
false,
|
||||
) as MeshInstance3D
|
||||
assert(body != null)
|
||||
_validate_mesh_weights(body)
|
||||
var expected_mesh_names := _expected_runtime_mesh_names()
|
||||
assert(expected_mesh_names.size() == 137)
|
||||
var face_decal_count := 0
|
||||
for head_name: String in EXPECTED_HEAD_MESHES:
|
||||
for feature_name: String in FACE_FEATURES:
|
||||
assert(expected_mesh_names.has(
|
||||
"%s_%s" % [head_name, feature_name]
|
||||
))
|
||||
face_decal_count += 1
|
||||
assert(face_decal_count == 63)
|
||||
|
||||
var runtime_meshes: Array[MeshInstance3D] = []
|
||||
_collect_mesh_instances(character, runtime_meshes)
|
||||
assert(runtime_meshes.size() == expected_mesh_names.size())
|
||||
var actual_mesh_names: Dictionary = {}
|
||||
var total_blended_vertices := 0
|
||||
for mesh_instance: MeshInstance3D in runtime_meshes:
|
||||
assert(not actual_mesh_names.has(mesh_instance.name))
|
||||
actual_mesh_names[mesh_instance.name] = true
|
||||
assert(
|
||||
expected_mesh_names.has(mesh_instance.name),
|
||||
"Unexpected runtime character mesh: %s" % mesh_instance.name,
|
||||
)
|
||||
total_blended_vertices += _validate_mesh_weights(mesh_instance)
|
||||
assert(actual_mesh_names.size() == expected_mesh_names.size())
|
||||
for mesh_name: String in expected_mesh_names:
|
||||
assert(actual_mesh_names.has(mesh_name), "Missing mesh: %s" % mesh_name)
|
||||
for mesh_name: String in EXCLUDED_AUTHORING_MESHES:
|
||||
assert(not actual_mesh_names.has(mesh_name))
|
||||
|
||||
var blended_arm_vertices := 0
|
||||
for mesh_name: String in EXPECTED_ARM_MESHES:
|
||||
var arms := character.find_child(
|
||||
mesh_name,
|
||||
true,
|
||||
false,
|
||||
) as MeshInstance3D
|
||||
assert(arms != null)
|
||||
var blended_vertices := _validate_mesh_weights(arms)
|
||||
assert(
|
||||
blended_vertices > 0,
|
||||
"%s has no blended joint weights." % mesh_name,
|
||||
)
|
||||
blended_arm_vertices += blended_vertices
|
||||
assert(total_blended_vertices >= blended_arm_vertices)
|
||||
|
||||
print(
|
||||
"Character rig validation: PASS "
|
||||
+ "(animations=%d, blended arm vertices=%d)"
|
||||
% [EXPECTED_ANIMATIONS.size(), blended_arm_vertices]
|
||||
(
|
||||
"Character rig validation: PASS "
|
||||
+ "(meshes=%d, face decals=%d, bones=%d, animations=%d, "
|
||||
+ "blended arm vertices=%d)"
|
||||
)
|
||||
% [
|
||||
runtime_meshes.size(),
|
||||
face_decal_count,
|
||||
skeleton.get_bone_count(),
|
||||
EXPECTED_ANIMATIONS.size(),
|
||||
blended_arm_vertices,
|
||||
]
|
||||
)
|
||||
character.queue_free()
|
||||
quit()
|
||||
|
|
@ -125,12 +289,39 @@ func _validate_mesh_weights(mesh_instance: MeshInstance3D) -> int:
|
|||
if weight > 0.000001:
|
||||
influence_count += 1
|
||||
assert(absf(total - 1.0) <= 0.0001)
|
||||
assert(influence_count <= 2)
|
||||
if influence_count == 2:
|
||||
assert(influence_count <= 4)
|
||||
if influence_count > 1:
|
||||
blended_vertices += 1
|
||||
return blended_vertices
|
||||
|
||||
|
||||
func _expected_runtime_mesh_names() -> Dictionary:
|
||||
var result: Dictionary = {"body_main": true}
|
||||
for mesh_name: String in EXPECTED_ARM_MESHES:
|
||||
result[mesh_name] = true
|
||||
for head_name: String in EXPECTED_HEAD_MESHES:
|
||||
result[head_name] = true
|
||||
for feature_name: String in FACE_FEATURES:
|
||||
result["%s_%s" % [head_name, feature_name]] = true
|
||||
for mesh_name: String in EXPECTED_EAR_MESHES:
|
||||
result[mesh_name] = true
|
||||
for mesh_name: String in EXPECTED_TAIL_MESHES:
|
||||
result[mesh_name] = true
|
||||
for mesh_name: String in EXPECTED_SPECIAL_MESHES:
|
||||
result[mesh_name] = true
|
||||
return result
|
||||
|
||||
|
||||
func _collect_mesh_instances(
|
||||
parent: Node,
|
||||
result: Array[MeshInstance3D],
|
||||
) -> void:
|
||||
if parent is MeshInstance3D:
|
||||
result.append(parent as MeshInstance3D)
|
||||
for child: Node in parent.get_children():
|
||||
_collect_mesh_instances(child, result)
|
||||
|
||||
|
||||
func _find_first(parent: Node, class_name_to_find: String) -> Node:
|
||||
if parent.is_class(class_name_to_find):
|
||||
return parent
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ func _run() -> void:
|
|||
var hotbar_data: Dictionary = parsed["hotbar"]
|
||||
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
|
||||
assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
|
||||
assert(int(parsed["save_version"]) == 12)
|
||||
assert(int(parsed["save_version"]) == 13)
|
||||
assert(not PlayerHomeState.sanitize_save_data(
|
||||
parsed.get("home", {})
|
||||
).is_empty())
|
||||
|
|
@ -204,7 +204,7 @@ func _run() -> void:
|
|||
var enet_channel_count: int = NetworkProtocol.ENET_CHANNEL_COUNT
|
||||
var fish_quality_capability: String = NetworkProtocol.FISH_QUALITY_CAPABILITY
|
||||
var showcase_capability: StringName = NetworkFishShowcaseProtocol.CAPABILITY
|
||||
assert(protocol_version == 13)
|
||||
assert(protocol_version == 14)
|
||||
assert(enet_channel_count == 18)
|
||||
assert(
|
||||
fish_quality_capability == "fish_quality_v1"
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func _run() -> void:
|
|||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY
|
||||
)
|
||||
var fish_quality_capability: String = NetworkProtocol.FISH_QUALITY_CAPABILITY
|
||||
assert(protocol_version == 13)
|
||||
assert(protocol_version == 14)
|
||||
assert(enet_channel_count == 18)
|
||||
assert(movement_animation_channel == 11)
|
||||
assert(
|
||||
|
|
@ -505,7 +505,7 @@ func _validate_version_four_migration() -> void:
|
|||
version_four,
|
||||
4,
|
||||
)
|
||||
assert(int(migrated.get("save_version", -1)) == 12)
|
||||
assert(int(migrated.get("save_version", -1)) == 13)
|
||||
assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
|
||||
assert(
|
||||
is_equal_approx(
|
||||
|
|
|
|||
|
|
@ -156,9 +156,9 @@ func _run() -> void:
|
|||
)
|
||||
|
||||
var body_texture := _assert_pattern_texture("calico", "body_main")
|
||||
var arms_texture := _assert_pattern_texture("paws", "body_arms")
|
||||
var arms_texture := _assert_pattern_texture("paws", "arms_mammal")
|
||||
var head_texture := _assert_pattern_texture("tiger", "head_round")
|
||||
var ears_texture := _assert_pattern_texture("fox", "ears_pointy_short")
|
||||
var ears_texture := _assert_pattern_texture("fox", "ears_cat")
|
||||
var tail_texture := _assert_pattern_texture("fox", "tails_fox")
|
||||
_assert_pattern_texture("tummy", "body_main")
|
||||
_assert_pattern_texture("stripes", "body_main")
|
||||
|
|
@ -186,13 +186,13 @@ func _run() -> void:
|
|||
skeleton, "body_main", body_texture
|
||||
)
|
||||
var arms_material := _assert_mesh_pattern(
|
||||
skeleton, "body_arms", arms_texture
|
||||
skeleton, "arms_mammal", arms_texture
|
||||
)
|
||||
var head_material := _assert_mesh_pattern(
|
||||
skeleton, "head_round", head_texture
|
||||
)
|
||||
var ears_material := _assert_mesh_pattern(
|
||||
skeleton, "ears_pointy_short", ears_texture
|
||||
skeleton, "ears_cat", ears_texture
|
||||
)
|
||||
var tail_material := _assert_mesh_pattern(
|
||||
skeleton, "tails_fox", tail_texture
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ func _run() -> void:
|
|||
)
|
||||
assert(bool(decoded.get("ok", false)))
|
||||
var save_data: Dictionary = decoded["data"]
|
||||
assert(int(save_data.get("save_version", -1)) == 12)
|
||||
assert(int(save_data.get("save_version", -1)) == 13)
|
||||
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
|
||||
assert(not PlayerHomeState.sanitize_save_data(
|
||||
save_data.get("home", {})
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ func _validate_save_migration() -> void:
|
|||
version_five,
|
||||
5,
|
||||
)
|
||||
assert(int(migrated.get("save_version", -1)) == 12)
|
||||
assert(int(migrated.get("save_version", -1)) == 13)
|
||||
var experience_data: Dictionary = migrated.get("experience", {})
|
||||
assert(int(experience_data.get("total_experience", -1)) == 0)
|
||||
var world_data: Dictionary = migrated.get("world", {})
|
||||
|
|
|
|||
|
|
@ -177,6 +177,11 @@ func _run_client() -> void:
|
|||
var service := main.get_node(
|
||||
"%NetworkProfileService"
|
||||
) as NetworkProfileService
|
||||
var spawn := main.get_node("%PlayerSpawnService") as PlayerSpawnService
|
||||
var player := spawn.get_local_player() as Player
|
||||
var host_avatar := spawn.get_avatar(1) as Player
|
||||
assert(player != null)
|
||||
assert(host_avatar != null)
|
||||
var original: Dictionary = service.get_persisted_appearance()
|
||||
var changed: Dictionary = _changed_appearance(original)
|
||||
assert(service.preview_appearance(changed))
|
||||
|
|
@ -189,6 +194,11 @@ func _run_client() -> void:
|
|||
service.apply_finished.connect(func(accepted: bool, message: String) -> void:
|
||||
apply_results.append([accepted, message])
|
||||
)
|
||||
# Generated spawn points can be farther apart than audible speech range.
|
||||
# Establish proximity after initial replication settles so this voice test
|
||||
# does not depend on the terrain seed's peer spawn spacing.
|
||||
player.global_position = host_avatar.global_position + Vector3.RIGHT
|
||||
assert(player.global_position.distance_to(host_avatar.global_position) < 4.0)
|
||||
assert(service.apply_profile(
|
||||
service.get_persisted_name(),
|
||||
changed,
|
||||
|
|
@ -213,7 +223,6 @@ func _run_client() -> void:
|
|||
== "trail guide"
|
||||
)
|
||||
assert(Dictionary(session.get("_local_appearance_snapshot")) == changed)
|
||||
var player := main.get_node("%PlayerSpawnService").get_local_player() as Player
|
||||
assert(player.get_animalese_voice_id() == "deep")
|
||||
assert(player.get_animalese_sample_set_id() == "kim")
|
||||
var host_voice_messages: Array[Dictionary] = []
|
||||
|
|
@ -265,18 +274,17 @@ func _run_client() -> void:
|
|||
|
||||
func _changed_appearance(original: Dictionary) -> Dictionary:
|
||||
var changed := original.duplicate(true)
|
||||
for category_id: String in CharacterCustomizationCatalog.CATEGORY_IDS:
|
||||
if category_id in CharacterCustomizationCatalog.FUR_STYLE_IDS:
|
||||
continue
|
||||
var options: Array = CharacterCustomizationCatalog.options_for(category_id)
|
||||
for option: Dictionary in options:
|
||||
var option_id: Variant = option.get("id")
|
||||
if option_id != changed.get(category_id):
|
||||
changed[category_id] = option_id
|
||||
if CharacterCustomizationCatalog.validate_snapshot(changed):
|
||||
return changed
|
||||
changed[category_id] = original.get(category_id)
|
||||
assert(false, "No alternate appearance option is available.")
|
||||
changed["species"] = "snail"
|
||||
changed["arms"] = "avian"
|
||||
changed["ears"] = "owl"
|
||||
changed["tail"] = "donkey"
|
||||
changed["special_back"] = "shell"
|
||||
changed["special_beak"] = "bird"
|
||||
changed["special_head"] = "fin"
|
||||
changed["special_horns"] = "goat"
|
||||
changed["special_tusks"] = "elephant"
|
||||
changed["special_wings"] = "moth"
|
||||
assert(CharacterCustomizationCatalog.validate_snapshot(changed))
|
||||
return changed
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,33 @@ func _validate_network_payloads() -> void:
|
|||
var invalid_hello := hello.duplicate(true)
|
||||
invalid_hello["player_title"] = ""
|
||||
assert(not NetworkProtocol.validate_client_hello(invalid_hello).is_empty())
|
||||
var missing_appearance_field := hello.duplicate(true)
|
||||
missing_appearance_field["cosmetic_snapshot"] = appearance.duplicate(true)
|
||||
missing_appearance_field["cosmetic_snapshot"].erase("special_wings")
|
||||
assert(not NetworkProtocol.validate_client_hello(
|
||||
missing_appearance_field
|
||||
).is_empty())
|
||||
var extra_appearance_field := hello.duplicate(true)
|
||||
extra_appearance_field["cosmetic_snapshot"] = appearance.duplicate(true)
|
||||
extra_appearance_field["cosmetic_snapshot"]["unexpected"] = "value"
|
||||
assert(not NetworkProtocol.validate_client_hello(
|
||||
extra_appearance_field
|
||||
).is_empty())
|
||||
var invalid_appearance_option := hello.duplicate(true)
|
||||
invalid_appearance_option["cosmetic_snapshot"] = appearance.duplicate(true)
|
||||
invalid_appearance_option["cosmetic_snapshot"]["species"] = "unknown"
|
||||
assert(not NetworkProtocol.validate_client_hello(
|
||||
invalid_appearance_option
|
||||
).is_empty())
|
||||
var noncanonical_appearance := hello.duplicate(true)
|
||||
noncanonical_appearance["cosmetic_snapshot"] = appearance.duplicate(true)
|
||||
noncanonical_appearance["cosmetic_snapshot"]["ears"] = "pointy_wide"
|
||||
assert(CharacterCustomizationCatalog.validate_snapshot(
|
||||
noncanonical_appearance["cosmetic_snapshot"]
|
||||
))
|
||||
assert(not NetworkProtocol.validate_client_hello(
|
||||
noncanonical_appearance
|
||||
).is_empty())
|
||||
|
||||
var check_request := {
|
||||
"request_id": "check",
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func _run() -> void:
|
|||
_assert_opaque(save_path)
|
||||
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path)
|
||||
assert(bool(decoded.get("ok", false)))
|
||||
assert(int((decoded["data"] as Dictionary)["save_version"]) == 12)
|
||||
assert(int((decoded["data"] as Dictionary)["save_version"]) == 13)
|
||||
var saved_character: Dictionary = (
|
||||
(decoded["data"] as Dictionary).get("character", {})
|
||||
)
|
||||
|
|
@ -292,6 +292,55 @@ func _run() -> void:
|
|||
assert(bool(unchanged_migration.get("ok", false)))
|
||||
assert(not bool(unchanged_migration.get("changed", true)))
|
||||
assert(data_root.root_path == unchanged_root)
|
||||
var appearance_path: String = data_root.root_path.path_join(
|
||||
"player/player_appearance.json"
|
||||
)
|
||||
assert(appearance_store.save_snapshot(appearance_store.get_snapshot()))
|
||||
var appearance_file := FileAccess.open(appearance_path, FileAccess.READ)
|
||||
assert(appearance_file != null)
|
||||
var parsed_appearance_data: Variant = JSON.parse_string(
|
||||
appearance_file.get_as_text()
|
||||
)
|
||||
appearance_file.close()
|
||||
assert(typeof(parsed_appearance_data) == TYPE_DICTIONARY)
|
||||
var current_appearance_data := parsed_appearance_data as Dictionary
|
||||
assert(
|
||||
int(current_appearance_data.get("format_version", -1))
|
||||
== PlayerAppearanceStore.FORMAT_VERSION
|
||||
)
|
||||
assert(PortableDataMigration._valid_owned_data(
|
||||
"player_appearance.json", current_appearance_data
|
||||
))
|
||||
var legacy_appearance_data: Dictionary = current_appearance_data.duplicate(
|
||||
true
|
||||
)
|
||||
legacy_appearance_data["format_version"] = 1
|
||||
var legacy_appearance: Dictionary = legacy_appearance_data["appearance"]
|
||||
for added_id: String in [
|
||||
"arms",
|
||||
"special_back",
|
||||
"special_beak",
|
||||
"special_head",
|
||||
"special_horns",
|
||||
"special_tusks",
|
||||
"special_wings",
|
||||
]:
|
||||
legacy_appearance.erase(added_id)
|
||||
assert(PortableDataMigration._valid_owned_data(
|
||||
"player_appearance.json", legacy_appearance_data
|
||||
))
|
||||
var future_appearance_data := current_appearance_data.duplicate(true)
|
||||
future_appearance_data["format_version"] = (
|
||||
PlayerAppearanceStore.FORMAT_VERSION + 1
|
||||
)
|
||||
assert(not PortableDataMigration._valid_owned_data(
|
||||
"player_appearance.json", future_appearance_data
|
||||
))
|
||||
var malformed_appearance_data := current_appearance_data.duplicate(true)
|
||||
malformed_appearance_data["format_version"] = "2"
|
||||
assert(not PortableDataMigration._valid_owned_data(
|
||||
"player_appearance.json", malformed_appearance_data
|
||||
))
|
||||
|
||||
var migrated_root: String = data_root.root_path.get_base_dir().path_join(
|
||||
"progression-migrated-data"
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ func _run() -> void:
|
|||
var snapshot_entities_per_envelope: int = (
|
||||
NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE
|
||||
)
|
||||
assert(protocol_version == 13)
|
||||
assert(protocol_version == 14)
|
||||
assert(
|
||||
world_spawn_capability == protocol_world_spawn_capability
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue