# test_save_snapshot_parity.gd —— 40250 角色全量快照存读档系统 1:1 回归测试套件 extends SceneTree const SaveSnapshotSystem = preload("res://save_snapshot_system.gd") var _passed := 0 var _failed := 0 func _assert(cond: bool, msg: String) -> void: if cond: _passed += 1 print(" [PASS] %s" % msg) else: _failed += 1 printerr(" [FAIL] %s" % msg) func _init() -> void: print("\n=== Running test_save_snapshot_parity (Direction 4: Save Snapshot System 1:1) ===") _test_snapshot_roundtrip() _test_disk_save_load_delete() _test_slot_summaries() _test_validation_and_errors() _finish() func _create_sample_player() -> Dictionary: var inv := [] inv.resize(90) for i in range(90): inv[i] = null inv[0] = { "vnum": 10, "name": "巨剑+9", "count": 1, "sockets": [28430, 28431, 28432], "attrs": [[1, 2000], [5, 10]] } inv[1] = { "vnum": 27003, "name": "红色药水(特大)", "count": 200 } var belt_inv := [] belt_inv.resize(16) for i in range(16): belt_inv[i] = null belt_inv[0] = { "vnum": 27102, "name": "绿色药水(大)", "count": 50 } return { "name": "剑圣东方", "job": 0, # 战士 "level": 75, "exp": 1500000, "hp": 12500, "max_hp": 12500, "sp": 3200, "max_sp": 3200, "stamina": 1000, "alignment": 12000, # 骑士精神 "gold": 98765432, "str": 90, "dex": 70, "con": 90, "int_stat": 30, "stat_points": 5, "skill_points": 2, "pos": Vector3(456.7, 12.3, 789.0), "map_name": "sohan", "playtime": 7200, "equipment": { 0: {"vnum": 189, "name": "毒剑+9", "refine": 9}, 1: {"vnum": 11299, "name": "黑铁战甲+9", "refine": 9}, 11: {"vnum": 18009, "name": "灵魂腰带+9", "refine_level": 9} }, "inventory": inv, "belt_item": {"vnum": 18009, "name": "灵魂腰带+9", "refine_level": 9}, "belt_inventory": belt_inv, "costumes": { 0: {"vnum": 41001, "name": "沙漠武士服(男)", "costume_type": "body", "remaining_time": 500000}, 1: {"vnum": 45001, "name": "武士发型", "costume_type": "hair", "remaining_time": 500000} }, "skills": { 1: {"name": "三连斩", "level": 40, "master_type": 3}, # P 2: {"name": "剑气", "level": 40, "master_type": 3} }, "alchemy": { 0: {"vnum": 111001, "name": "巨龙钻石", "grade": 5} }, "monster_cards": [101, 102, 103, 1093], "achievements": ["FIRST_METIN_KILL", "TOWER_FLOOR_9"], "active_title": "屠龙尊者" } func _test_snapshot_roundtrip() -> void: print("\n--- Test 1: In-Memory Snapshot Roundtrip & Full Schema Parity ---") var sys := SaveSnapshotSystem.new() var original := _create_sample_player() var snapshot := sys.create_snapshot(original, 1) _assert(snapshot.has("meta") and snapshot.has("stats") and snapshot.has("inventory"), "Snapshot contains main root keys") _assert(snapshot["meta"]["schema_version"] == SaveSnapshotSystem.SCHEMA_VERSION, "Schema version is 40250.1") _assert(snapshot["summary"]["name"] == "剑圣东方", "Summary name matches") _assert(snapshot["summary"]["job_name"] == "战士", "Summary job translated to 战士") _assert(snapshot["inventory"].size() == 90, "Inventory has exact 90 slots") _assert(snapshot["belt_system"]["belt_inventory"].size() == 16, "Belt inventory has exact 16 slots") # Restore var restore_res := sys.restore_snapshot(snapshot) _assert(restore_res["ok"] == true, "Restore succeeded") var restored: Dictionary = restore_res["player_data"] _assert(restored["name"] == original["name"], "Player name preserved") _assert(restored["level"] == original["level"], "Level preserved (75)") _assert(restored["gold"] == original["gold"], "Gold preserved (98765432)") _assert(restored["alignment"] == original["alignment"], "Alignment preserved (12000)") _assert(restored["pos"] is Vector3 and restored["pos"].is_equal_approx(Vector3(456.7, 12.3, 789.0)), "Vector3 position accurately restored") # Inventory restoration _assert(restored["inventory"].size() == 90, "Restored inventory size is 90") _assert(restored["inventory"][0]["vnum"] == 10, "Slot 0 item vnum 10 preserved") _assert(restored["inventory"][0]["sockets"] == [28430, 28431, 28432], "Sockets preserved") _assert(restored["inventory"][1]["count"] == 200, "Potion count 200 preserved") _assert(restored["inventory"][2] == null, "Empty inventory slots remain null") # Equipment restoration _assert(restored["equipment"].has(0) and restored["equipment"][0]["name"] == "毒剑+9", "Equipment slot 0 (WEAPON) preserved") _assert(restored["equipment"].has(1) and restored["equipment"][1]["name"] == "黑铁战甲+9", "Equipment slot 1 (ARMOR) preserved") # Belt, Costumes, Progression _assert(restored["belt_inventory"][0]["vnum"] == 27102, "Belt potion in slot 0 preserved") _assert(restored["costumes"].has(0) and restored["costumes"][0]["vnum"] == 41001, "Costume body preserved") _assert(restored["skills"].has(1) and restored["skills"][1]["level"] == 40, "Skill P level preserved") _assert(restored["monster_cards"].size() == 4, "Monster card collection preserved") _assert(restored["active_title"] == "屠龙尊者", "Active title preserved") func _test_disk_save_load_delete() -> void: print("\n--- Test 2: Disk IO - Save, Load, and Delete Slots ---") var sys := SaveSnapshotSystem.new() var test_dir := OS.get_cache_dir().path_join("metin2_snapshot_test_%d" % int(Time.get_unix_time_from_system())) var player := _create_sample_player() var save_signal := {"fired": false, "slot": 0, "path": ""} sys.snapshot_saved.connect(func(s: int, p: String): save_signal["fired"] = true save_signal["slot"] = s save_signal["path"] = p ) var load_signal := {"fired": false, "slot": 0, "path": ""} sys.snapshot_loaded.connect(func(s: int, p: String): load_signal["fired"] = true load_signal["slot"] = s load_signal["path"] = p ) var delete_signal := {"fired": false, "slot": 0} sys.snapshot_deleted.connect(func(s: int): delete_signal["fired"] = true delete_signal["slot"] = s ) # Save to slot 1 var save_res = sys.save_slot(1, player, test_dir) _assert(save_res["ok"] == true, "Save slot 1 returned ok") _assert(save_signal["fired"] == true and save_signal["slot"] == 1, "snapshot_saved signal fired") _assert(FileAccess.file_exists(save_res["path"]), "Save file exists on disk") # Load from slot 1 var load_res = sys.load_slot(1, test_dir) _assert(load_res["ok"] == true, "Load slot 1 returned ok") _assert(load_signal["fired"] == true and load_signal["slot"] == 1, "snapshot_loaded signal fired") _assert(load_res["player_data"]["name"] == "剑圣东方", "Loaded player name matches") _assert(load_res["player_data"]["gold"] == 98765432, "Loaded player gold matches") # Delete slot 1 var del_res = sys.delete_slot(1, test_dir) _assert(del_res["ok"] == true, "Delete slot 1 returned ok") _assert(delete_signal["fired"] == true and delete_signal["slot"] == 1, "snapshot_deleted signal fired") _assert(FileAccess.file_exists(save_res["path"]) == false, "Save file removed from disk") # Loading after deletion should fail gracefully var reload_fail = sys.load_slot(1, test_dir) _assert(reload_fail["ok"] == false and reload_fail["reason"] == "FILE_NOT_FOUND", "Loading deleted slot fails with FILE_NOT_FOUND") # Clean up directory DirAccess.remove_absolute(test_dir) func _test_slot_summaries() -> void: print("\n--- Test 3: Fast Multi-Slot Summaries ---") var sys := SaveSnapshotSystem.new() var test_dir := OS.get_cache_dir().path_join("metin2_summary_test_%d" % int(Time.get_unix_time_from_system())) var player1 := _create_sample_player() var player2 := _create_sample_player() player2["name"] = "幻影刺客" player2["job"] = 1 # 刺客 player2["level"] = 60 # Save slot 1 and slot 3 (leave slot 2, 4, 5 empty) sys.save_slot(1, player1, test_dir) sys.save_slot(3, player2, test_dir) var summaries := sys.get_slot_summaries(test_dir, 5) _assert(summaries.size() == 5, "Summaries has 5 entries") _assert(summaries[0]["is_empty"] == false and summaries[0]["name"] == "剑圣东方" and summaries[0]["level"] == 75, "Slot 1 summary accurate") _assert(summaries[1]["is_empty"] == true, "Slot 2 summary shows is_empty == true") _assert(summaries[2]["is_empty"] == false and summaries[2]["name"] == "幻影刺客" and summaries[2]["job_name"] == "刺客", "Slot 3 summary accurate") _assert(summaries[3]["is_empty"] == true, "Slot 4 summary is_empty") _assert(summaries[4]["is_empty"] == true, "Slot 5 summary is_empty") # Clean up sys.delete_slot(1, test_dir) sys.delete_slot(3, test_dir) DirAccess.remove_absolute(test_dir) func _test_validation_and_errors() -> void: print("\n--- Test 4: Validation & Error Handling ---") var sys := SaveSnapshotSystem.new() var invalid_meta := sys.validate_snapshot({"foo": "bar"}) _assert(invalid_meta["ok"] == false and invalid_meta["reason"] == "MISSING_META", "Missing meta rejected") var bad_version := sys.validate_snapshot({"meta": {"schema_version": "999.0"}}) _assert(bad_version["ok"] == false and bad_version["reason"] == "VERSION_MISMATCH", "Version mismatch rejected") var bad_json_res := sys.import_json("This is not JSON!") _assert(bad_json_res["ok"] == false and bad_json_res["reason"] == "INVALID_JSON_ROOT", "Invalid JSON string rejected") var player := _create_sample_player() var json_str := sys.export_json(player) _assert(json_str.length() > 0, "export_json produces non-empty JSON") var parsed_res := sys.import_json(json_str) _assert(parsed_res["ok"] == true and parsed_res["player_data"]["name"] == "剑圣东方", "import_json parses valid JSON accurately") func _finish() -> void: print("\n==========================================") print("Save Snapshot Parity Tests Finished:") print("Passed: %d, Failed: %d" % [_passed, _failed]) print("==========================================") if _failed > 0: printerr("FAILED: Some tests did not pass.") quit(1) else: print("SUCCESS: All Save Snapshot parity tests passed!") quit(0)