diff --git a/pyproject.toml b/pyproject.toml index db9e26f..2e1fc90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ keywords = ["palworld", "editor", "pal"] license = {file = "LICENSE"} requires-python = ">=3.11" readme = "README.md" -version = "0.6.0" +version = "0.7.0" dependencies = [ "setuptools==69.1.0", "palworld_save_tools==0.22.0", diff --git a/src/palworld_pal_editor/api/save.py b/src/palworld_pal_editor/api/save.py index 1918aaa..f412e3a 100644 --- a/src/palworld_pal_editor/api/save.py +++ b/src/palworld_pal_editor/api/save.py @@ -1,3 +1,4 @@ +import platform import traceback from flask import Blueprint, jsonify, request from flask_jwt_extended import jwt_required @@ -13,7 +14,7 @@ @save_blueprint.route("/fetch_config", methods=["GET"]) def fetch_config(): tk_status = False - if Config.mode == "gui": + if Config.mode == "gui" and platform.system() != "Darwin": try: import tkinter as tk tk_status = True diff --git a/src/palworld_pal_editor/assets/data/pal_exp_table.json b/src/palworld_pal_editor/assets/data/pal_exp_table.json new file mode 100644 index 0000000..e742d5a --- /dev/null +++ b/src/palworld_pal_editor/assets/data/pal_exp_table.json @@ -0,0 +1,802 @@ +[ + { + "Level": 1, + "DropEXP": 7, + "NextEXP": 0, + "PalNextEXP": 0, + "TotalEXP": 0, + "PalTotalEXP": 0 + }, + { + "Level": 2, + "DropEXP": 9, + "NextEXP": 8, + "PalNextEXP": 25, + "TotalEXP": 8, + "PalTotalEXP": 25 + }, + { + "Level": 3, + "DropEXP": 11, + "NextEXP": 30, + "PalNextEXP": 31, + "TotalEXP": 38, + "PalTotalEXP": 56 + }, + { + "Level": 4, + "DropEXP": 13, + "NextEXP": 98, + "PalNextEXP": 37, + "TotalEXP": 136, + "PalTotalEXP": 93 + }, + { + "Level": 5, + "DropEXP": 15, + "NextEXP": 180, + "PalNextEXP": 45, + "TotalEXP": 316, + "PalTotalEXP": 138 + }, + { + "Level": 6, + "DropEXP": 17, + "NextEXP": 278, + "PalNextEXP": 69, + "TotalEXP": 593, + "PalTotalEXP": 207 + }, + { + "Level": 7, + "DropEXP": 20, + "NextEXP": 395, + "PalNextEXP": 99, + "TotalEXP": 988, + "PalTotalEXP": 306 + }, + { + "Level": 8, + "DropEXP": 23, + "NextEXP": 536, + "PalNextEXP": 134, + "TotalEXP": 1524, + "PalTotalEXP": 440 + }, + { + "Level": 9, + "DropEXP": 26, + "NextEXP": 705, + "PalNextEXP": 176, + "TotalEXP": 2229, + "PalTotalEXP": 616 + }, + { + "Level": 10, + "DropEXP": 38, + "NextEXP": 908, + "PalNextEXP": 227, + "TotalEXP": 3138, + "PalTotalEXP": 843 + }, + { + "Level": 11, + "DropEXP": 45, + "NextEXP": 1152, + "PalNextEXP": 288, + "TotalEXP": 4290, + "PalTotalEXP": 1131 + }, + { + "Level": 12, + "DropEXP": 52, + "NextEXP": 1444, + "PalNextEXP": 361, + "TotalEXP": 5734, + "PalTotalEXP": 1492 + }, + { + "Level": 13, + "DropEXP": 58, + "NextEXP": 1795, + "PalNextEXP": 449, + "TotalEXP": 7529, + "PalTotalEXP": 1941 + }, + { + "Level": 14, + "DropEXP": 66, + "NextEXP": 2216, + "PalNextEXP": 554, + "TotalEXP": 9745, + "PalTotalEXP": 2495 + }, + { + "Level": 15, + "DropEXP": 74, + "NextEXP": 2721, + "PalNextEXP": 680, + "TotalEXP": 12467, + "PalTotalEXP": 3175 + }, + { + "Level": 16, + "DropEXP": 84, + "NextEXP": 3328, + "PalNextEXP": 832, + "TotalEXP": 15795, + "PalTotalEXP": 4007 + }, + { + "Level": 17, + "DropEXP": 97, + "NextEXP": 4055, + "PalNextEXP": 1014, + "TotalEXP": 19850, + "PalTotalEXP": 5021 + }, + { + "Level": 18, + "DropEXP": 110, + "NextEXP": 4928, + "PalNextEXP": 1232, + "TotalEXP": 24778, + "PalTotalEXP": 6253 + }, + { + "Level": 19, + "DropEXP": 124, + "NextEXP": 5976, + "PalNextEXP": 1494, + "TotalEXP": 30754, + "PalTotalEXP": 7747 + }, + { + "Level": 20, + "DropEXP": 138, + "NextEXP": 7233, + "PalNextEXP": 1808, + "TotalEXP": 37988, + "PalTotalEXP": 9555 + }, + { + "Level": 21, + "DropEXP": 153, + "NextEXP": 8742, + "PalNextEXP": 2185, + "TotalEXP": 46730, + "PalTotalEXP": 11740 + }, + { + "Level": 22, + "DropEXP": 171, + "NextEXP": 10552, + "PalNextEXP": 2638, + "TotalEXP": 57282, + "PalTotalEXP": 14378 + }, + { + "Level": 23, + "DropEXP": 194, + "NextEXP": 12725, + "PalNextEXP": 3181, + "TotalEXP": 70007, + "PalTotalEXP": 17559 + }, + { + "Level": 24, + "DropEXP": 217, + "NextEXP": 15332, + "PalNextEXP": 3833, + "TotalEXP": 85338, + "PalTotalEXP": 21392 + }, + { + "Level": 25, + "DropEXP": 241, + "NextEXP": 18460, + "PalNextEXP": 4615, + "TotalEXP": 103799, + "PalTotalEXP": 26007 + }, + { + "Level": 26, + "DropEXP": 267, + "NextEXP": 22214, + "PalNextEXP": 5554, + "TotalEXP": 126013, + "PalTotalEXP": 31561 + }, + { + "Level": 27, + "DropEXP": 300, + "NextEXP": 26719, + "PalNextEXP": 6680, + "TotalEXP": 152732, + "PalTotalEXP": 38241 + }, + { + "Level": 28, + "DropEXP": 337, + "NextEXP": 32125, + "PalNextEXP": 8031, + "TotalEXP": 184856, + "PalTotalEXP": 46272 + }, + { + "Level": 29, + "DropEXP": 374, + "NextEXP": 38612, + "PalNextEXP": 9653, + "TotalEXP": 223468, + "PalTotalEXP": 55925 + }, + { + "Level": 30, + "DropEXP": 417, + "NextEXP": 46396, + "PalNextEXP": 11599, + "TotalEXP": 269864, + "PalTotalEXP": 67524 + }, + { + "Level": 31, + "DropEXP": 467, + "NextEXP": 55737, + "PalNextEXP": 13934, + "TotalEXP": 325601, + "PalTotalEXP": 81458 + }, + { + "Level": 32, + "DropEXP": 518, + "NextEXP": 66947, + "PalNextEXP": 16737, + "TotalEXP": 392548, + "PalTotalEXP": 98195 + }, + { + "Level": 33, + "DropEXP": 580, + "NextEXP": 80398, + "PalNextEXP": 20099, + "TotalEXP": 472946, + "PalTotalEXP": 118294 + }, + { + "Level": 34, + "DropEXP": 647, + "NextEXP": 96540, + "PalNextEXP": 24135, + "TotalEXP": 569485, + "PalTotalEXP": 142429 + }, + { + "Level": 35, + "DropEXP": 719, + "NextEXP": 115909, + "PalNextEXP": 28977, + "TotalEXP": 685395, + "PalTotalEXP": 171406 + }, + { + "Level": 36, + "DropEXP": 806, + "NextEXP": 139153, + "PalNextEXP": 34788, + "TotalEXP": 824548, + "PalTotalEXP": 206194 + }, + { + "Level": 37, + "DropEXP": 896, + "NextEXP": 167046, + "PalNextEXP": 41761, + "TotalEXP": 991594, + "PalTotalEXP": 247955 + }, + { + "Level": 38, + "DropEXP": 1003, + "NextEXP": 200517, + "PalNextEXP": 50129, + "TotalEXP": 1192111, + "PalTotalEXP": 298084 + }, + { + "Level": 39, + "DropEXP": 1118, + "NextEXP": 240683, + "PalNextEXP": 60171, + "TotalEXP": 1432794, + "PalTotalEXP": 358255 + }, + { + "Level": 40, + "DropEXP": 1249, + "NextEXP": 288881, + "PalNextEXP": 72220, + "TotalEXP": 1721675, + "PalTotalEXP": 430475 + }, + { + "Level": 41, + "DropEXP": 1394, + "NextEXP": 346719, + "PalNextEXP": 86680, + "TotalEXP": 2068394, + "PalTotalEXP": 517155 + }, + { + "Level": 42, + "DropEXP": 1554, + "NextEXP": 416125, + "PalNextEXP": 104031, + "TotalEXP": 2484520, + "PalTotalEXP": 621186 + }, + { + "Level": 43, + "DropEXP": 1736, + "NextEXP": 499412, + "PalNextEXP": 124853, + "TotalEXP": 2983932, + "PalTotalEXP": 746039 + }, + { + "Level": 44, + "DropEXP": 1940, + "NextEXP": 599357, + "PalNextEXP": 149839, + "TotalEXP": 3583289, + "PalTotalEXP": 895878 + }, + { + "Level": 45, + "DropEXP": 2168, + "NextEXP": 719290, + "PalNextEXP": 179823, + "TotalEXP": 4302579, + "PalTotalEXP": 1075701 + }, + { + "Level": 46, + "DropEXP": 2422, + "NextEXP": 863210, + "PalNextEXP": 215803, + "TotalEXP": 5165789, + "PalTotalEXP": 1291504 + }, + { + "Level": 47, + "DropEXP": 2704, + "NextEXP": 1035914, + "PalNextEXP": 258979, + "TotalEXP": 6201703, + "PalTotalEXP": 1550483 + }, + { + "Level": 48, + "DropEXP": 3018, + "NextEXP": 1243159, + "PalNextEXP": 310790, + "TotalEXP": 7444862, + "PalTotalEXP": 1861273 + }, + { + "Level": 49, + "DropEXP": 3374, + "NextEXP": 1491853, + "PalNextEXP": 372963, + "TotalEXP": 8936715, + "PalTotalEXP": 2234236 + }, + { + "Level": 50, + "DropEXP": 3774, + "NextEXP": 1790285, + "PalNextEXP": 447571, + "TotalEXP": 10727001, + "PalTotalEXP": 2681807 + }, + { + "Level": 51, + "DropEXP": 4218, + "NextEXP": 2148405, + "PalNextEXP": 537101, + "TotalEXP": 12875405, + "PalTotalEXP": 3218908 + }, + { + "Level": 52, + "DropEXP": 4718, + "NextEXP": 2578147, + "PalNextEXP": 644537, + "TotalEXP": 15453553, + "PalTotalEXP": 3863445 + }, + { + "Level": 53, + "DropEXP": 5271, + "NextEXP": 3093839, + "PalNextEXP": 773460, + "TotalEXP": 18547392, + "PalTotalEXP": 4636905 + }, + { + "Level": 54, + "DropEXP": 5897, + "NextEXP": 3712669, + "PalNextEXP": 928167, + "TotalEXP": 22260061, + "PalTotalEXP": 5565072 + }, + { + "Level": 55, + "DropEXP": 6592, + "NextEXP": 4455265, + "PalNextEXP": 1113816, + "TotalEXP": 26715325, + "PalTotalEXP": 6678888 + }, + { + "Level": 56, + "DropEXP": 7374, + "NextEXP": 5346379, + "PalNextEXP": 1336595, + "TotalEXP": 32061705, + "PalTotalEXP": 8015483 + }, + { + "Level": 57, + "DropEXP": 8248, + "NextEXP": 6415717, + "PalNextEXP": 1603929, + "TotalEXP": 38477422, + "PalTotalEXP": 9619412 + }, + { + "Level": 58, + "DropEXP": 9227, + "NextEXP": 7698923, + "PalNextEXP": 1924731, + "TotalEXP": 46176345, + "PalTotalEXP": 11544143 + }, + { + "Level": 59, + "DropEXP": 12227, + "NextEXP": 9238769, + "PalNextEXP": 2309692, + "TotalEXP": 55415114, + "PalTotalEXP": 13853835 + }, + { + "Level": 60, + "DropEXP": 15227, + "NextEXP": 11086585, + "PalNextEXP": 2771646, + "TotalEXP": 66501699, + "PalTotalEXP": 16625481 + }, + { + "Level": 61, + "DropEXP": 18227, + "NextEXP": 13303964, + "PalNextEXP": 3325991, + "TotalEXP": 79805663, + "PalTotalEXP": 19951472 + }, + { + "Level": 62, + "DropEXP": 21227, + "NextEXP": 15964819, + "PalNextEXP": 3991205, + "TotalEXP": 95770482, + "PalTotalEXP": 23942677 + }, + { + "Level": 63, + "DropEXP": 24227, + "NextEXP": 19157845, + "PalNextEXP": 4789461, + "TotalEXP": 114928327, + "PalTotalEXP": 28732138 + }, + { + "Level": 64, + "DropEXP": 27227, + "NextEXP": 22989476, + "PalNextEXP": 5747369, + "TotalEXP": 137917803, + "PalTotalEXP": 34479507 + }, + { + "Level": 65, + "DropEXP": 30227, + "NextEXP": 27587433, + "PalNextEXP": 6896858, + "TotalEXP": 165505236, + "PalTotalEXP": 41376365 + }, + { + "Level": 66, + "DropEXP": 33227, + "NextEXP": 33104982, + "PalNextEXP": 8276245, + "TotalEXP": 198610218, + "PalTotalEXP": 49652610 + }, + { + "Level": 67, + "DropEXP": 36227, + "NextEXP": 39726040, + "PalNextEXP": 9931510, + "TotalEXP": 238336258, + "PalTotalEXP": 59584120 + }, + { + "Level": 68, + "DropEXP": 39227, + "NextEXP": 47671310, + "PalNextEXP": 11917827, + "TotalEXP": 286007568, + "PalTotalEXP": 71501947 + }, + { + "Level": 69, + "DropEXP": 42227, + "NextEXP": 57205634, + "PalNextEXP": 14301408, + "TotalEXP": 343213202, + "PalTotalEXP": 85803355 + }, + { + "Level": 70, + "DropEXP": 45227, + "NextEXP": 68646823, + "PalNextEXP": 17161706, + "TotalEXP": 411860024, + "PalTotalEXP": 102965061 + }, + { + "Level": 71, + "DropEXP": 48227, + "NextEXP": 82376249, + "PalNextEXP": 20594062, + "TotalEXP": 494236274, + "PalTotalEXP": 123559123 + }, + { + "Level": 72, + "DropEXP": 51227, + "NextEXP": 98851561, + "PalNextEXP": 24712890, + "TotalEXP": 593087835, + "PalTotalEXP": 148272013 + }, + { + "Level": 73, + "DropEXP": 54227, + "NextEXP": 118621935, + "PalNextEXP": 29655484, + "TotalEXP": 711709770, + "PalTotalEXP": 177927497 + }, + { + "Level": 74, + "DropEXP": 57227, + "NextEXP": 142346384, + "PalNextEXP": 35586596, + "TotalEXP": 854056155, + "PalTotalEXP": 213514093 + }, + { + "Level": 75, + "DropEXP": 60227, + "NextEXP": 170815723, + "PalNextEXP": 42703931, + "TotalEXP": 1024871878, + "PalTotalEXP": 256218024 + }, + { + "Level": 76, + "DropEXP": 63227, + "NextEXP": 204978930, + "PalNextEXP": 51244732, + "TotalEXP": 1229850808, + "PalTotalEXP": 307462756 + }, + { + "Level": 77, + "DropEXP": 66227, + "NextEXP": 245974778, + "PalNextEXP": 61493694, + "TotalEXP": 1475825586, + "PalTotalEXP": 368956450 + }, + { + "Level": 78, + "DropEXP": 69227, + "NextEXP": 295169796, + "PalNextEXP": 73792449, + "TotalEXP": 1770995381, + "PalTotalEXP": 442748899 + }, + { + "Level": 79, + "DropEXP": 72227, + "NextEXP": 354203817, + "PalNextEXP": 88550954, + "TotalEXP": 2125199198, + "PalTotalEXP": 531299853 + }, + { + "Level": 80, + "DropEXP": 75227, + "NextEXP": 425044642, + "PalNextEXP": 106261161, + "TotalEXP": 2147483647, + "PalTotalEXP": 637561014 + }, + { + "Level": 81, + "DropEXP": 78227, + "NextEXP": 510053632, + "PalNextEXP": 127513408, + "TotalEXP": 2147483647, + "PalTotalEXP": 765074422 + }, + { + "Level": 82, + "DropEXP": 81227, + "NextEXP": 612064421, + "PalNextEXP": 153016105, + "TotalEXP": 2147483647, + "PalTotalEXP": 918090527 + }, + { + "Level": 83, + "DropEXP": 84227, + "NextEXP": 734477367, + "PalNextEXP": 183619342, + "TotalEXP": 2147483647, + "PalTotalEXP": 1101709869 + }, + { + "Level": 84, + "DropEXP": 87227, + "NextEXP": 881372903, + "PalNextEXP": 220343226, + "TotalEXP": 2147483647, + "PalTotalEXP": 1322053095 + }, + { + "Level": 85, + "DropEXP": 90227, + "NextEXP": 1057647545, + "PalNextEXP": 264411886, + "TotalEXP": 2147483647, + "PalTotalEXP": 1586464981 + }, + { + "Level": 86, + "DropEXP": 93227, + "NextEXP": 1269177116, + "PalNextEXP": 317294279, + "TotalEXP": 2147483647, + "PalTotalEXP": 1903759260 + }, + { + "Level": 87, + "DropEXP": 96227, + "NextEXP": 1523012601, + "PalNextEXP": 380753150, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + }, + { + "Level": 88, + "DropEXP": 99227, + "NextEXP": 1827615183, + "PalNextEXP": 456903796, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + }, + { + "Level": 89, + "DropEXP": 102227, + "NextEXP": 2147483647, + "PalNextEXP": 548284571, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + }, + { + "Level": 90, + "DropEXP": 105227, + "NextEXP": 2147483647, + "PalNextEXP": 657941500, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + }, + { + "Level": 91, + "DropEXP": 108227, + "NextEXP": 2147483647, + "PalNextEXP": 789529816, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + }, + { + "Level": 92, + "DropEXP": 111227, + "NextEXP": 2147483647, + "PalNextEXP": 947435794, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + }, + { + "Level": 93, + "DropEXP": 114227, + "NextEXP": 2147483647, + "PalNextEXP": 1136922969, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + }, + { + "Level": 94, + "DropEXP": 117227, + "NextEXP": 2147483647, + "PalNextEXP": 1364307578, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + }, + { + "Level": 95, + "DropEXP": 120227, + "NextEXP": 2147483647, + "PalNextEXP": 1637169109, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + }, + { + "Level": 96, + "DropEXP": 123227, + "NextEXP": 2147483647, + "PalNextEXP": 1964602946, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + }, + { + "Level": 97, + "DropEXP": 126227, + "NextEXP": 2147483647, + "PalNextEXP": 2147483647, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + }, + { + "Level": 98, + "DropEXP": 129227, + "NextEXP": 2147483647, + "PalNextEXP": 2147483647, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + }, + { + "Level": 99, + "DropEXP": 132227, + "NextEXP": 2147483647, + "PalNextEXP": 2147483647, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + }, + { + "Level": 100, + "DropEXP": 135227, + "NextEXP": 2147483647, + "PalNextEXP": 2147483647, + "TotalEXP": 2147483647, + "PalTotalEXP": 2147483647 + } +] \ No newline at end of file diff --git a/src/palworld_pal_editor/assets/data/pal_xp_thresholds.json b/src/palworld_pal_editor/assets/data/pal_xp_thresholds.json deleted file mode 100644 index b893ccb..0000000 --- a/src/palworld_pal_editor/assets/data/pal_xp_thresholds.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - 0, 25, 56, 93, 138, 207, 306, 440, 616, 843, 1131, 1492, 1941, 2495, 3175, - 4007, 5021, 6253, 7747, 9555, 11740, 14378, 17559, 21392, 26007, 31561, 38241, - 46272, 55925, 67524, 81458, 98195, 118294, 142429, 171406, 206194, 247955, - 298134, 358305, 430525, 517205, 621236, 746089, 895928, 1075751, 1291554, - 1550533, 1861323, 2234286, 2681857 -] diff --git a/src/palworld_pal_editor/assets/data/pal_xp_thresholds_max.json b/src/palworld_pal_editor/assets/data/pal_xp_thresholds_max.json deleted file mode 100644 index 029631d..0000000 --- a/src/palworld_pal_editor/assets/data/pal_xp_thresholds_max.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - 24, 55, 92, 137, 206, 305, 439, 615, 842, 1130, 1491, 1940, 2494, 3174, 4006, - 5020, 6252, 7746, 9554, 11739, 14377, 17558, 21391, 26006, 31560, 38240, - 46271, 55924, 67523, 81457, 98194, 118293, 142428, 171405, 206193, 247954, - 298133, 358304, 430524, 517204, 621235, 746088, 895927, 1075750, 1291553, - 1550532, 1861322, 2234285, 2681856, 2681857 -] diff --git a/src/palworld_pal_editor/config.py b/src/palworld_pal_editor/config.py index da4a380..638cba7 100644 --- a/src/palworld_pal_editor/config.py +++ b/src/palworld_pal_editor/config.py @@ -13,7 +13,7 @@ CONFIG_PATH = PROGRAM_PATH / 'config.json' -VERSION = "0.6.0" +VERSION = "0.7.0" class Config: i18n: str = "en" diff --git a/src/palworld_pal_editor/core/pal_entity.py b/src/palworld_pal_editor/core/pal_entity.py index af533e5..ca24580 100644 --- a/src/palworld_pal_editor/core/pal_entity.py +++ b/src/palworld_pal_editor/core/pal_entity.py @@ -7,7 +7,7 @@ from palworld_pal_editor.config import Config from palworld_pal_editor.utils import LOGGER, clamp, DataProvider -from palworld_pal_editor.core.pal_objects import PalObjects, PalGender, PalRank, get_attr_value, get_nested_attr, toUUID +from palworld_pal_editor.core.pal_objects import PalObjects, PalGender, PalRank, get_nested_attr, dumps from palworld_pal_editor.utils.util import type_guard @@ -24,17 +24,13 @@ def __init__(self, pal_obj: dict) -> None: if self.InstanceId is None: raise Exception(f"No GUID, skipping {self}") - if get_attr_value(self._pal_param, "IsPlayer"): + if PalObjects.get_BaseType(self._pal_param.get("IsPlayer")): raise TypeError("Expecting pal_obj, received player_obj: {} - {} - {}".format(self.NickName, self.PlayerUId, self.InstanceId)) - # self._derived_hp_scaling = self._derive_hp_scaling() self._display_name_cache = {} self.owner_player_entity = None - ## TODO - # self._isBoss_cache = {} - # self._raw_specie_key_cache = {} - # self._data_access_key_cache = {} self.is_unreferenced_pal = False + self.is_new_pal = False def __str__(self) -> str: return "{} - {} - {}".format(self.DisplayName, self.OwnerName, self.InstanceId) @@ -68,7 +64,7 @@ def group_id(self, id: UUID | str): @property def PlayerUId(self) -> Optional[UUID]: # should be EMPTY UUID, but sometimes it's set to player uid in singleplayer game, weird - return get_attr_value(self._pal_key, "PlayerUId") + return PalObjects.get_BaseType(self._pal_key.get("PlayerUId")) @PlayerUId.setter def PlayerUId(self, id: UUID | str) -> Optional[UUID]: @@ -76,7 +72,7 @@ def PlayerUId(self, id: UUID | str) -> Optional[UUID]: @property def InstanceId(self) -> Optional[UUID]: - return get_attr_value(self._pal_key, "InstanceId") + return PalObjects.get_BaseType(self._pal_key.get("InstanceId")) @InstanceId.setter def InstanceId(self, id: UUID | str): @@ -84,8 +80,8 @@ def InstanceId(self, id: UUID | str): @property def OwnerPlayerUId(self) -> Optional[UUID]: - return get_attr_value(self._pal_param, "OwnerPlayerUId") - + return PalObjects.get_BaseType(self._pal_param.get("OwnerPlayerUId")) + @property def LastOwnerPlayerUId(self) -> Optional[UUID]: if self.OldOwnerPlayerUIds: @@ -123,13 +119,13 @@ def SlotIndex(self) -> Optional[int]: @property def CharacterID(self) -> Optional[str]: - return get_attr_value(self._pal_param, "CharacterID") + return PalObjects.get_BaseType(self._pal_param.get("CharacterID")) @CharacterID.setter @LOGGER.change_logger('CharacterID') @type_guard def CharacterID(self, value: str) -> None: - og_specie = self._RawSpecieKey + og_specie = self.RawSpecieKey if self.CharacterID is None: self._pal_param["CharacterID"] = PalObjects.NameProperty(value) @@ -149,19 +145,21 @@ def CharacterID(self, value: str) -> None: # well, just randomly picked lol self.Gender = PalGender.FEMALE - new_specie = self._RawSpecieKey + new_specie = self.RawSpecieKey if new_specie != og_specie: self.remove_unique_attacks() self.learn_attacks() + if self.IsTower or self.IsRAID: + self.equip_all_pal_attacks() + self.heal_pal() self.clear_worker_sick() - # self.MaxHP = self.ComputedMaxHP if maxHP := self.ComputedMaxHP: self.HP = maxHP @property - def _RawSpecieKey(self) -> Optional[str]: + def RawSpecieKey(self) -> Optional[str]: key = self.CharacterID if self._IsBOSS: if "Boss_" in key: @@ -207,7 +205,7 @@ def IconAccessKey(self) -> Optional[str]: if self.IsHuman: return "Human" if self.IsRAID: - return self._RawSpecieKey + return self.RawSpecieKey return self.DataAccessKey @property @@ -217,7 +215,7 @@ def DataAccessKey(self) -> Optional[str]: if self.IsRAID: return self.CharacterID - key = self._RawSpecieKey + key = self.RawSpecieKey match key: case "Sheepball": key = "SheepBall" @@ -281,10 +279,9 @@ def IsTower(self) -> bool: @type_guard def IsTower(self, value: bool) -> None: if not value: - self.CharacterID = self._RawSpecieKey + self.CharacterID = self.RawSpecieKey else: - self.CharacterID = f"GYM_{self._RawSpecieKey}" - # self.MaxHP = self.ComputedMaxHP + self.CharacterID = f"GYM_{self.RawSpecieKey}" if maxHP := self.ComputedMaxHP: self.HP = maxHP @@ -302,9 +299,9 @@ def _IsBOSS(self) -> bool: @type_guard def _IsBOSS(self, value: bool) -> None: if not value: - self.CharacterID = self._RawSpecieKey + self.CharacterID = self.RawSpecieKey elif not self._IsBOSS and value: - self.CharacterID = f"BOSS_{self._RawSpecieKey}" + self.CharacterID = f"BOSS_{self.RawSpecieKey}" @property def IsBOSS(self) -> bool: @@ -325,14 +322,13 @@ def IsBOSS(self, value: bool) -> None: if self.IsRarePal and value: self.IsRarePal = False self._IsBOSS = value - # Update MaxHP - # self.MaxHP = self.ComputedMaxHP + if maxHP := self.ComputedMaxHP: self.HP = maxHP @property def IsRarePal(self) -> Optional[bool]: - return get_attr_value(self._pal_param, "IsRarePal") + return PalObjects.get_BaseType(self._pal_param.get("IsRarePal")) @IsRarePal.setter @LOGGER.change_logger('IsRarePal') @@ -341,8 +337,6 @@ def IsRarePal(self, value: bool) -> None: # Boss and Rare can only exist one if self.IsBOSS and not value: return - # if self.IsBOSS and value: - # self.IsBOSS = False if self.IsRarePal is None: self._pal_param["IsRarePal"] = PalObjects.BoolProperty(value) @@ -356,7 +350,7 @@ def IsRarePal(self, value: bool) -> None: @property def NickName(self) -> Optional[str]: - return get_attr_value(self._pal_param, "NickName") + return PalObjects.get_BaseType(self._pal_param.get("NickName")) @NickName.setter @LOGGER.change_logger('NickName') @@ -366,13 +360,13 @@ def NickName(self, value: str) -> None: self._pal_param["NickName"] = PalObjects.StrProperty(value) else: self._pal_param["NickName"]["value"] = value - # clear up + if not self.NickName: self._pal_param.pop("NickName", None) @property def Level(self) -> Optional[int]: - return get_attr_value(self._pal_param, "Level") + return PalObjects.get_BaseType(self._pal_param.get("Level")) @Level.setter @LOGGER.change_logger('Level') @@ -384,16 +378,15 @@ def Level(self, value: int) -> None: else: self._pal_param["Level"]["value"] = value self.Exp = DataProvider.get_level_xp(self.Level) - # Update MaxHP - # self.MaxHP = self.ComputedMaxHP + if maxHP := self.ComputedMaxHP: self.HP = maxHP - # Learn Attacks + self.learn_attacks() @property def Exp(self) -> Optional[int]: - return get_attr_value(self._pal_param, "Exp") + return PalObjects.get_BaseType(self._pal_param.get("Exp")) @Exp.setter @LOGGER.change_logger('Exp') @@ -406,7 +399,7 @@ def Exp(self, value: int) -> None: @property def Rank(self) -> Optional[PalRank]: - return PalRank.from_value(get_attr_value(self._pal_param, "Rank")) + return PalRank.from_value(PalObjects.get_BaseType(self._pal_param.get("Rank"))) @Rank.setter @LOGGER.change_logger('Rank') @@ -425,7 +418,6 @@ def Rank(self, rank: PalRank | int) -> None: else: PalObjects.set_BaseType(self._pal_param["Rank"], pal_rank.value) - # self.MaxHP = self.ComputedMaxHP if maxHP := self.ComputedMaxHP: self.HP = maxHP @@ -434,26 +426,25 @@ def Rank(self, rank: PalRank | int) -> None: @property def Rank_HP(self) -> Optional[int]: - return get_attr_value(self._pal_param, "Rank_HP") + return PalObjects.get_BaseType(self._pal_param.get("Rank_HP")) @property def Rank_Attack(self) -> Optional[int]: - return get_attr_value(self._pal_param, "Rank_Attack") + return PalObjects.get_BaseType(self._pal_param.get("Rank_Attack")) @property def Rank_Defence(self) -> Optional[int]: - return get_attr_value(self._pal_param, "Rank_Defence") + return PalObjects.get_BaseType(self._pal_param.get("Rank_Defence")) @property def Rank_CraftSpeed(self) -> Optional[int]: - return get_attr_value(self._pal_param, "Rank_CraftSpeed") + return PalObjects.get_BaseType(self._pal_param.get("Rank_CraftSpeed")) @Rank_HP.setter @LOGGER.change_logger('Rank_HP') @type_guard def Rank_HP(self, rank: int) -> None: self._set_soul_rank('Rank_HP', rank) - # self.MaxHP = self.ComputedMaxHP if maxHP := self.ComputedMaxHP: self.HP = maxHP @@ -549,22 +540,6 @@ def HP(self, value: int) -> None: else: PalObjects.set_FixedPoint64(self._pal_param["HP"], value) - # Deprecated since Palworld 0.2.x - # @property - # def MaxHP(self) -> Optional[int]: - # return PalObjects.get_FixedPoint64(self._pal_param.get("MaxHP")) - - # Deprecated since Palworld 0.2.x - # @MaxHP.setter - # @LOGGER.change_logger("MaxHP") - # def MaxHP(self, val: int) -> None: - # if self.MaxHP is None: - # self._pal_param["MaxHP"] = PalObjects.FixedPoint64(val) - # else: - # PalObjects.set_FixedPoint64(self._pal_param["MaxHP"], val) - - # self.HP = self.MaxHP - @property def PassiveSkillList(self) -> Optional[list[str]]: return PalObjects.get_ArrayProperty(self._pal_param.get("PassiveSkillList")) @@ -589,8 +564,7 @@ def add_PassiveSkillList(self, skill: str) -> bool: self.PassiveSkillList.append(skill) LOGGER.info(f"Added {DataProvider.get_passive_i18n(skill)[0]} to PassiveSkillList") - # Update MaxHP, but no such skill atm. - # self.MaxHP = self.ComputedMaxHP + # Update HP, but no such skill atm. # if maxHP := self.ComputedMaxHP: # self.HP = maxHP return True @@ -602,8 +576,9 @@ def pop_PassiveSkillList(self, idx: int = None, item: str = None) -> Optional[st idx = self.PassiveSkillList.index(item) skill = self.PassiveSkillList.pop(int(idx)) LOGGER.info(f"Removed {DataProvider.get_passive_i18n(skill)[0]} from PassiveSkillList") - # Update MaxHP, but no such skill atm. - # self.MaxHP = self.ComputedMaxHP + # Update HP, but no such skill atm. + # if maxHP := self.ComputedMaxHP: + # self.HP = maxHP return skill except Exception as e: LOGGER.warning(f"{e}") @@ -614,7 +589,7 @@ def EquipWaza(self) -> Optional[list[str]]: @LOGGER.change_logger('EquipWaza') @type_guard - def add_EquipWaza(self, waza: str) -> bool: + def add_EquipWaza(self, waza: str, force=False) -> bool: """ Normally you can't add the same "waza" twice on a pal. """ @@ -628,7 +603,7 @@ def add_EquipWaza(self, waza: str) -> bool: LOGGER.warning(f"{self} has already equipped waza {waza}, skipping") return False - if len(self.EquipWaza) >= 3: + if len(self.EquipWaza) >= 3 and not force: LOGGER.warning(f"{self} EquipWaza has maxed out: {self.EquipWaza}, consider add to MasteredWaza instead.") return False @@ -663,12 +638,6 @@ def num_EmptyEquipWaza(self) -> int: def MasteredWaza(self) -> Optional[list[str]]: return PalObjects.get_ArrayProperty(self._pal_param.get("MasteredWaza")) - # @property - # def MasteredWazaSet(self) -> Optional[set[str]]: - # # Unused - # # We need a way to cache it, otherwise it's no better than idx into a list - # return set(self.MasteredWaza) if self.MasteredWaza is not None else None - @LOGGER.change_logger('MasteredWaza') @type_guard def add_MasteredWaza(self, waza: str) -> bool: @@ -687,7 +656,7 @@ def add_MasteredWaza(self, waza: str) -> bool: return False self.MasteredWaza.append(waza) - # PalObjects.add_ArrayProperty(self._pal_param["MasteredWaza"], waza) + LOGGER.info(f"Added {DataProvider.get_attack_i18n(waza)[0]} to MasteredWaza") if self.num_EmptyEquipWaza > 0: @@ -704,7 +673,7 @@ def pop_MasteredWaza(self, idx: int = None, item: str = None) -> Optional[str]: waza = self.MasteredWaza.pop(int(idx)) if waza in (self.EquipWaza or []): self.pop_EquipWaza(item=waza) - # return PalObjects.pop_ArrayProperty(self._pal_param["MasteredWaza"], idx) + LOGGER.info(f"Removed {DataProvider.get_attack_i18n(waza)[0]} from MasteredWaza") return waza except Exception as e: @@ -731,7 +700,7 @@ def Talent_Defense(self) -> Optional[int]: @type_guard def Talent_HP(self, value: int): self._set_iv("Talent_HP", value) - # self.MaxHP = self.ComputedMaxHP + if maxHP := self.ComputedMaxHP: self.HP = maxHP @@ -865,8 +834,7 @@ def heal_pal(self): self._pal_param.pop("PalReviveTimer", None) if self.PhysicalHealth == "EPalStatusPhysicalHealthType::Dying": self._pal_param.pop("PhysicalHealth", None) - # if not self.MaxHP: - # self.MaxHP = self.ComputedMaxHP + if maxHP := self.ComputedMaxHP: self.HP = maxHP @@ -904,9 +872,14 @@ def learn_attacks(self): for atk in DataProvider.get_attacks_to_learn(self.DataAccessKey, self.Level or 1): if atk not in (self.MasteredWaza or []): self.add_MasteredWaza(atk) - # for atk in DataProvider.get_attacks_to_forget(self.DataAccessKey, self.Level): - # if atk in self.MasteredWaza: - # self.pop_MasteredWaza(atk) + + def equip_all_pal_attacks(self): + atks = DataProvider.get_attacks_to_learn(self.DataAccessKey, self.Level or 1) + if not atks: return + (self.EquipWaza or []).clear() + for atk in atks: + if atk not in (self.EquipWaza or []): + self.add_EquipWaza(atk, True) def remove_unique_attacks(self): if self.MasteredWaza is None: @@ -957,7 +930,7 @@ def print_obj(self): print(self.dump_obj()) def dump_obj(self) -> str: - return str(self._pal_obj) + return dumps(self._pal_obj) def _set_soul_rank(self, property_name: str, rank: int): rank = clamp(0, 10, rank) diff --git a/src/palworld_pal_editor/core/pal_objects.py b/src/palworld_pal_editor/core/pal_objects.py index 3d44905..00e112c 100644 --- a/src/palworld_pal_editor/core/pal_objects.py +++ b/src/palworld_pal_editor/core/pal_objects.py @@ -1,9 +1,15 @@ from enum import Enum +import json from typing import Any, Optional +import uuid from palworld_save_tools.archive import UUID +from palworld_save_tools.json_tools import CustomEncoder from palworld_pal_editor.utils import LOGGER, clamp +def dumps(data: dict) -> str: + return json.dumps(data, indent=4, cls=CustomEncoder, ensure_ascii=False) + def isUUIDStr(uuid_str: str) -> Optional[UUID]: try: @@ -26,47 +32,6 @@ def UUID2HexStr(uuid: str | UUID) -> str: return str(uuid).upper().replace("-", "") -def get_attr_value( - data_container: dict, attr_name: str, nested_keys: list = None -) -> Optional[Any]: - """ - Generic method to retrieve the value of an attribute from the pal data. - - Parameters: - attr_name (str): The name of the attribute to retrieve. - nested_keys (list): A list of keys to navigate through nested dictionaries if necessary. - - Returns: - Optional[Any]: The value of the attribute, or None if the attribute does not exist. - """ - try: - if data_container is None: - raise TypeError("Expected dict, get None.") - attr = data_container.get(attr_name) - - if attr is None: - raise IndexError(f"Providing dict does not have `{attr_name}` attribute.") - - if nested_keys: - for key in nested_keys: - attr = attr.get(key, None) - if attr is None: - raise KeyError( - f"trying to get attr `{attr_name}`, but nested key `{key}` not found in dict {data_container}." - ) - - if attr and "value" in attr: - return attr["value"] - else: - raise KeyError( - f"trying to get attr `{attr_name}`, but final key `value` not found in dict {data_container}." - ) - - except Exception as e: - # LOGGER.warning(e) - return None - - def get_nested_attr(container: dict, keys: list) -> Optional[Any]: """ Retrieve a value from a nested dictionary using a sequence of keys. @@ -121,6 +86,7 @@ def from_value(value: int): class PalObjects: EMPTY_UUID = toUUID("00000000-0000-0000-0000-000000000000") + TIME = 638486453957560000 @staticmethod def StrProperty(value: str): @@ -344,6 +310,16 @@ def set_PalCharacterSlotId( PalObjects.set_PalContainerId(container["value"]["ContainerId"], container_id) PalObjects.set_BaseType(container["value"]["SlotIndex"], slot_idx) + @staticmethod + def FloatContainer(value: dict): + return { + "struct_type": "FloatContainer", + "struct_id": PalObjects.EMPTY_UUID, + "id": None, + "value": value, + "type": "StructProperty", + } + @staticmethod def get_container_value(container: dict) -> Optional[Any]: case_1 = { @@ -400,6 +376,32 @@ def Vector(x, y, z): }, "type": "StructProperty", } + + @staticmethod + def PalLoggedinPlayerSaveDataRecordData(value: dict = None): + return { + "struct_type": "PalLoggedinPlayerSaveDataRecordData", + "struct_id": PalObjects.EMPTY_UUID, + "id": None, + "value": value or {}, + "type": "StructProperty" + } + + @staticmethod + def MapProperty(key_type: str, value_type: str, key_struct_type=None, value_struct_type=None): + return { + "key_type": key_type, + "value_type": value_type, + "key_struct_type": key_struct_type, + "value_struct_type": value_struct_type, + "id": None, + "value": [], + "type": "MapProperty" + } + + @staticmethod + def get_MapProperty(container: dict) -> Optional[list[dict]]: + return get_nested_attr(container, ["value"]) EPalWorkSuitabilities = [ "EPalWorkSuitability::EmitFlame", @@ -491,9 +493,7 @@ def PalSaveParameter(InstanceId, OwnerPlayerUId, ContainerId, SlotIndex, group_i "NameProperty", {"values": []} ), "MP": PalObjects.FixedPoint64(10000), - "OwnedTime": PalObjects.DateTime( - 638478651098960000 - ), + "OwnedTime": PalObjects.DateTime(PalObjects.TIME), "OwnerPlayerUId": PalObjects.Guid(OwnerPlayerUId), "OldOwnerPlayerUIds": PalObjects.ArrayProperty( "StructProperty", @@ -505,8 +505,11 @@ def PalSaveParameter(InstanceId, OwnerPlayerUId, ContainerId, SlotIndex, group_i "id": PalObjects.EMPTY_UUID, }, ), - # "MaxHP": PalObjects.FixedPoint64(545000), # MaxHP is no longer stored in the game save. + # MaxHP is no longer stored in the game save. + # "MaxHP": PalObjects.FixedPoint64(545000), "CraftSpeed": PalObjects.IntProperty(70), + # Do not omit CraftSpeeds, otherwise the pal works super slow + # TODO use accurate data (even tho this is useless) "CraftSpeeds": PalObjects.ArrayProperty( "StructProperty", { @@ -522,24 +525,14 @@ def PalSaveParameter(InstanceId, OwnerPlayerUId, ContainerId, SlotIndex, group_i "id": PalObjects.EMPTY_UUID, }, ), - # "EquipItemContainerId": { - # "struct_type": "PalContainerId", - # "struct_id": PalObjects.EMPTY_UUID, - # "id": None, - # "value": { - # "ID": { - # "struct_type": "Guid", - # "struct_id": PalObjects.EMPTY_UUID, - # "id": None, - # "value": "2ee46d97-4a5a-4e11-837c-276e4c6b9c7b", - # "type": "StructProperty", - # } - # }, - # "type": "StructProperty", - # }, + "SanityValue": PalObjects.FloatProperty(100.0), + "EquipItemContainerId": PalObjects.PalContainerId( + str(uuid.uuid4()) + ), "SlotID": PalObjects.PalCharacterSlotId( SlotIndex, ContainerId ), + # TODO Need accurate values "MaxFullStomach": PalObjects.FloatProperty(150.0), "GotStatusPointList": PalObjects.ArrayProperty( "StructProperty", @@ -567,12 +560,14 @@ def PalSaveParameter(InstanceId, OwnerPlayerUId, ContainerId, SlotIndex, group_i "id": PalObjects.EMPTY_UUID, }, ), - "LastJumpedLocation": PalObjects.Vector(0, 0, 0), + "DecreaseFullStomachRates": PalObjects.FloatContainer({}), + "CraftSpeedRates": PalObjects.FloatContainer({}), + "LastJumpedLocation": PalObjects.Vector(0, 0, 7088.5), }, "type": "StructProperty", } }, - "unknown_bytes": (0, 0, 0, 0), + "unknown_bytes": [0, 0, 0, 0], "group_id": group_id, }, ".worldSaveData.CharacterSaveParameterMap.Value.RawData", diff --git a/src/palworld_pal_editor/core/player_entity.py b/src/palworld_pal_editor/core/player_entity.py index 4ac4767..f682683 100644 --- a/src/palworld_pal_editor/core/player_entity.py +++ b/src/palworld_pal_editor/core/player_entity.py @@ -4,7 +4,8 @@ from palworld_pal_editor.utils import LOGGER, alphanumeric_key from palworld_pal_editor.core.pal_entity import PalEntity -from palworld_pal_editor.core.pal_objects import PalObjects, get_attr_value +from palworld_pal_editor.core.pal_objects import PalObjects +from palworld_pal_editor.utils.data_provider import DataProvider class PlayerEntity: @@ -18,6 +19,7 @@ def __init__( ) -> None: self._player_obj: dict = player_obj self._palbox: dict[str, PalEntity] = palbox + self._new_palbox: dict[str, PalEntity] = {} self._gvas_file: GvasFile = gvas_file self._gvas_compression_times: int = compression_times self.group_id = group_id @@ -36,11 +38,10 @@ def __init__( self._player_param: dict = self._player_obj["value"]["RawData"]["value"][ "object" ]["SaveParameter"]["value"] - - if not get_attr_value(self._player_param, "IsPlayer"): + if not PalObjects.get_BaseType(self._player_param.get("IsPlayer")): raise TypeError( "Expecting player_obj, received pal_obj: {} - {} - {} - {}".format( - get_attr_value(self._player_param, "CharacterID"), + PalObjects.get_BaseType(self._player_param.get("CharacterID")), self.NickName, self.PlayerUId, self.InstanceId, @@ -76,15 +77,15 @@ def __eq__(self, __value: object) -> bool: @property def PlayerUId(self) -> Optional[UUID]: - return get_attr_value(self._player_key, "PlayerUId") + return PalObjects.get_BaseType(self._player_key.get("PlayerUId")) @property def InstanceId(self) -> Optional[UUID]: - return get_attr_value(self._player_key, "InstanceId") + return PalObjects.get_BaseType(self._player_key.get("InstanceId")) @property def NickName(self) -> Optional[str]: - return get_attr_value(self._player_param, "NickName") + return PalObjects.get_BaseType(self._player_param.get("NickName")) @property def OtomoCharacterContainerId(self) -> Optional[UUID]: @@ -142,21 +143,109 @@ def add_pal(self, pal_entity: PalEntity) -> bool: pal_guid = str(pal_entity.InstanceId) if pal_guid in self._palbox: return False + + if pal_entity.is_new_pal: + self._new_palbox[pal_guid] = pal_entity + self._palbox[pal_guid] = pal_entity pal_entity.set_owner_player_entity(self) return True + + def try_create_pal_record_data(self): + if "RecordData" not in self._player_save_data: + self._player_save_data["RecordData"] = PalObjects.PalLoggedinPlayerSaveDataRecordData() + record_data = self._player_save_data["RecordData"]["value"] + + if "PalCaptureCount" not in record_data: + record_data["PalCaptureCount"] = PalObjects.MapProperty("NameProperty", "IntProperty") + + if "PaldeckUnlockFlag" not in record_data: + record_data["PaldeckUnlockFlag"] = PalObjects.MapProperty("NameProperty", "BoolProperty") + + @property + def PalCaptureCount(self) -> Optional[list]: + if not (record_data := self._player_save_data.get("RecordData", None)): + return None + record_data: dict = record_data["value"] + return PalObjects.get_MapProperty(record_data.get("PalCaptureCount", None)) + + @property + def PaldeckUnlockFlag(self) -> Optional[list]: + if not (record_data := self._player_save_data.get("RecordData", None)): + return None + record_data: dict = record_data["value"] + return PalObjects.get_MapProperty(record_data.get("PaldeckUnlockFlag", None)) + + def get_pal_capture_count(self, name: str) -> int: + try: + return self._player_save_data["RecordData"]["value"]["PalCaptureCount"]["value"][name] + except: + return 0 + + def inc_pal_capture_count(self, name: str): + self.try_create_pal_record_data() + for record in self.PalCaptureCount: + if record['key'].lower() == name.lower(): + record['value'] += 1 + return + self.PalCaptureCount.append({ + 'key': name, + 'value': 1 + }) + + def unlock_paldeck(self, name: str): + self.try_create_pal_record_data() + for record in self.PaldeckUnlockFlag: + if record['key'].lower() == name.lower(): + record['value'] = True + return + self.PaldeckUnlockFlag.append({ + 'key': name, + 'value': True + }) + + def save_new_pal_records(self): + """ + This should only be called on save + """ + def handle_special_keys(key) -> str: + match key: + case 'PlantSlime_Flower': return 'PlantSlime' + case 'SheepBall': return 'Sheepball' + case 'LazyCatFish': return 'LazyCatfish' + return key + + for guid in self._new_palbox: + pal_entity = self._new_palbox[guid] + if DataProvider.is_pal_invalid(pal_entity.DataAccessKey): + LOGGER.info(f"Skip player records update for invalid pal: {pal_entity}") + continue + if pal_entity.IsHuman or not DataProvider.get_pal_sorting_key(pal_entity.DataAccessKey): + LOGGER.info(f"Skip player records update for pal: {pal_entity}") + continue + + key = handle_special_keys(pal_entity.RawSpecieKey) + self.inc_pal_capture_count(key) + self.unlock_paldeck(key) + pal_entity.is_new_pal = False + + self._new_palbox.clear() def get_pals(self) -> list[PalEntity]: return self._palbox.values() def pop_pal(self, guid: str | UUID) -> Optional[PalEntity]: + if guid in self._new_palbox: + self._new_palbox.pop(guid) return self._palbox.pop(guid, None) - def get_pal(self, guid: UUID | str) -> Optional[PalEntity]: + def get_pal(self, guid: UUID | str, disable_warning=False) -> Optional[PalEntity]: guid = str(guid) if guid in self._palbox: return self._palbox[guid] - LOGGER.warning(f"Player {self} has no pal {guid}.") + + if not disable_warning: + LOGGER.warning(f"Player {self} has no pal {guid}.") def get_sorted_pals(self, sorting_key="paldeck") -> list[PalEntity]: match sorting_key: diff --git a/src/palworld_pal_editor/core/save_manager.py b/src/palworld_pal_editor/core/save_manager.py index 4334048..9c6ac1f 100644 --- a/src/palworld_pal_editor/core/save_manager.py +++ b/src/palworld_pal_editor/core/save_manager.py @@ -8,7 +8,6 @@ from palworld_save_tools.gvas import GvasFile from palworld_save_tools.archive import FArchiveReader, FArchiveWriter, UUID -from palworld_save_tools.json_tools import CustomEncoder from palworld_save_tools.palsav import compress_gvas_to_sav, decompress_sav_to_gvas from palworld_save_tools.paltypes import PALWORLD_CUSTOM_PROPERTIES, PALWORLD_TYPE_HINTS @@ -16,7 +15,7 @@ from palworld_pal_editor.core.container_data import ContainerData -from palworld_pal_editor.core.pal_objects import PalObjects, UUID2HexStr, get_attr_value, toUUID +from palworld_pal_editor.core.pal_objects import PalObjects, UUID2HexStr, toUUID from palworld_pal_editor.core.player_entity import PlayerEntity from palworld_pal_editor.core.pal_entity import PalEntity from palworld_pal_editor.utils import LOGGER, alphanumeric_key @@ -111,15 +110,13 @@ def skip_encode(writer: FArchiveWriter, property_type: str, properties: dict) -> MAIN_SKIP_PROPERTIES[".worldSaveData.InvaderSaveData"] = (skip_decode, skip_encode) MAIN_SKIP_PROPERTIES[".worldSaveData.DungeonPointMarkerSaveData"] = (skip_decode, skip_encode) MAIN_SKIP_PROPERTIES[".worldSaveData.GameTimeSaveData"] = (skip_decode, skip_encode) -# PALEDITOR_CUSTOM_PROPERTIES[".worldSaveData.CharacterContainerSaveData"] = (skip_decode, skip_encode) -# PALEDITOR_CUSTOM_PROPERTIES[".worldSaveData.GroupSaveDataMap"] = (skip_decode, skip_encode) PLAYER_SKIP_PROPERTIES = copy.deepcopy(PALWORLD_CUSTOM_PROPERTIES) PLAYER_SKIP_PROPERTIES[".SaveData.PlayerCharacterMakeData"] = (skip_decode, skip_encode) PLAYER_SKIP_PROPERTIES[".SaveData.LastTransform"] = (skip_decode, skip_encode) PLAYER_SKIP_PROPERTIES[".SaveData.inventoryInfo"] = (skip_decode, skip_encode) -PLAYER_SKIP_PROPERTIES[".SaveData.RecordData"] = (skip_decode, skip_encode) +# PLAYER_SKIP_PROPERTIES[".SaveData.RecordData"] = (skip_decode, skip_encode) class SaveManager: # Although these are class attrs, SaveManager itself is singleton so it should be fine? @@ -173,7 +170,7 @@ def get_pal(self, guid: UUID | str) -> Optional[PalEntity]: if guid in self._dangling_pals: return self._dangling_pals[guid] for player in self.get_players(): - if pal := player.get_pal(guid): + if pal := player.get_pal(guid, disable_warning=True): return pal LOGGER.warning(f"Can't find pal {guid}") @@ -192,10 +189,10 @@ def _load_entities(self): LOGGER.warning(f"Non-player/pal data found in CharacterSaveParameterMap, skipping {entity}") continue - entity_param = entity_struct['value'] + entity_param: dict = entity_struct['value'] try: - if get_attr_value(entity_param, "IsPlayer"): - uid_str = str(get_attr_value(entity['key'], "PlayerUId")) + if PalObjects.get_BaseType(entity_param.get("IsPlayer")): + uid_str = str(PalObjects.get_BaseType(entity["key"].get("PlayerUId"))) if uid_str in self.player_mapping: LOGGER.error(f"Duplicated player found: \n\t{self.player_mapping[uid_str]}, skipping...") @@ -299,6 +296,8 @@ def open(self, file_path: str) -> Optional[GvasFile]: self._raw_gvas, PALWORLD_TYPE_HINTS, MAIN_SKIP_PROPERTIES ) + PalObjects.TIME = PalObjects.get_BaseType(self.gvas_file.properties.get("Timestamp")) or PalObjects.TIME + try: self.group_data = GroupData(self.gvas_file) except Exception as e: @@ -386,7 +385,7 @@ def add_pal(self, player_uid: str | UUID, pal_obj: dict = None) -> Optional[PalE LOGGER.warning(f"Player {player_uid} not found") return None - player_container_ids = [player.PalStorageContainerId, player.OtomoCharacterContainerId] + player_container_ids = [player.OtomoCharacterContainerId, player.PalStorageContainerId] pal_container = None for id in player_container_ids: @@ -420,13 +419,19 @@ def add_pal(self, player_uid: str | UUID, pal_obj: dict = None) -> Optional[PalE pal_entity.InstanceId = pal_instanceId pal_entity.SlotID = (container_id, slot_idx) # I don't know why some captured pals have PlayerUId, - # and having this non-empty ID will cause the game to discard the duped pal + # But having non-empty ID will cause the game to hide the duped pal pal_entity.PlayerUId = PalObjects.EMPTY_UUID - pal_entity._pal_param.pop("EquipItemContainerId", None) - # (pal_entity.OldOwnerPlayerUIds or []).clear() + # It seems the item container id is not necessarily referenced in the ItemContainerSaveData + # so just assign a randomly for now. + pal_entity._pal_param["EquipItemContainerId"] = PalObjects.PalContainerId(str(uuid.uuid4())) + # pal_entity._pal_param.pop("EquipItemContainerId", None) + pal_entity.NickName = "!!!DUPED PAL!!!" - player.add_pal(pal_entity) + pal_entity.is_new_pal = True + + if not player.add_pal(pal_entity): + raise Exception("Duplicated Pal ID, Try Again!") self._entities_list.append(pal_obj) except: LOGGER.error(f"Failed adding pal: {traceback.format_exc()}") @@ -503,6 +508,7 @@ def save_player_sav(self, player_entity: PlayerEntity) -> bool: if player_entity.PlayerGVAS is None: return False + player_entity.save_new_pal_records() gvas_file, compression_times = player_entity.PlayerGVAS player_path: Path = self._file_path / "Players" / f"{UUID2HexStr(player_entity.PlayerUId)}.sav" diff --git a/src/palworld_pal_editor/utils/data_provider.py b/src/palworld_pal_editor/utils/data_provider.py index c5e41e8..00bbf80 100644 --- a/src/palworld_pal_editor/utils/data_provider.py +++ b/src/palworld_pal_editor/utils/data_provider.py @@ -32,7 +32,7 @@ def load_json(filename: str) -> Any: PAL_ATTACKS: dict[str, dict] = load_json("pal_attacks.json") PAL_DATA: dict[str, dict] = load_json("pal_data.json") PAL_PASSIVES: dict[str, dict] = load_json("pal_passives.json") -PAL_XP_THRESHOLDS: list[int] = load_json("pal_xp_thresholds.json") +PAL_EXP_TABLE: list[int] = load_json("pal_exp_table.json") # PAL_ICONS: dict[str] = load_icons("pals") @@ -146,7 +146,7 @@ def get_pal_attacks(pal: str) -> Optional[list[str]]: @staticmethod def get_level_xp(lv: int) -> Optional[int]: try: - return PAL_XP_THRESHOLDS[lv - 1] + return PAL_EXP_TABLE[lv - 1]["PalTotalEXP"] except IndexError: LOGGER.warning(f"Level {lv} is out of bounds.") return None