// Save system const string SAVE_EXTENSION = ".dat"; const string SAVE_ENCRYPTION_KEY = "draugnorak_save_v1"; const int SAVE_VERSION = 3; string last_save_error = ""; string current_save_file = ""; string[] get_save_files() { string[] result; string[]@ items = glob("*" + SAVE_EXTENSION); if (@items == null) return result; for (uint i = 0; i < items.length(); i++) { string item = items[i]; if (item.length() >= SAVE_EXTENSION.length() && item.substr(item.length() - SAVE_EXTENSION.length()) == SAVE_EXTENSION) { result.insert_last(item); } } if (result.length() > 1) { result.sort(sort_string_case_insensitive); } return result; } bool sort_string_case_insensitive(const string &in a, const string &in b) { return a.lower() < b.lower(); } bool has_save_game() { return get_save_files().length() > 0; } string encrypt_save_data(const string&in rawData) { return string_aes_encrypt(rawData, SAVE_ENCRYPTION_KEY); } string decrypt_save_data(const string&in encryptedData) { return string_aes_decrypt(encryptedData, SAVE_ENCRYPTION_KEY); } bool save_data(const string&in filename, const string&in data) { if (data.length() == 0) { return false; } file tmp; if (!tmp.open(filename, "wb")) { return false; } if (tmp.write(data) < data.length()) { tmp.close(); return false; } tmp.close(); return true; } bool read_file_string(const string&in filename, string&out data) { file tmp; if (!tmp.open(filename, "rb")) { return false; } data = tmp.read(); tmp.close(); return data.length() > 0; } double get_number(dictionary@ data, const string&in key, double defaultValue) { double value; if (@data == null) return defaultValue; if (data.get(key, value)) return value; int value_int; if (data.get(key, value_int)) return value_int; string value_str; if (data.get(key, value_str)) { return parse_int(value_str); } return defaultValue; } bool get_bool(dictionary@ data, const string&in key, bool defaultValue) { bool value; if (@data == null) return defaultValue; if (data.get(key, value)) return value; int value_int; if (data.get(key, value_int)) return value_int != 0; string value_str; if (data.get(key, value_str)) return value_str == "1" || value_str == "true"; return defaultValue; } bool dictionary_has_keys(dictionary@ data) { if (@data == null) return false; string[]@ keys = data.get_keys(); return keys.length() > 0; } bool has_number_key(dictionary@ data, const string&in key) { double value; if (@data == null) return false; if (data.get(key, value)) return true; int value_int; if (data.get(key, value_int)) return true; string value_str; if (data.get(key, value_str)) return value_str.length() > 0; return false; } string[] get_string_list(dictionary@ data, const string&in key) { string[] result; if (@data == null) return result; if (!data.get(key, result)) return result; return result; } string flatten_exception_text(const string&in text) { string result = ""; bool lastWasSpace = false; for (uint i = 0; i < text.length(); i++) { string ch = text.substr(i, 1); if (ch == "\r" || ch == "\n") { if (!lastWasSpace) { result += " | "; lastWasSpace = true; } continue; } result += ch; lastWasSpace = false; } return result; } string format_log_timestamp() { datetime dt; string stamp = dt.format(DATE_TIME_FORMAT_RFC1123); return "[" + stamp + "]"; } void log_unhandled_exception(const string&in context) { string info = get_exception_info(); string filePath = get_exception_file(); int line = get_exception_line(); string func = get_exception_function(); string stack = flatten_exception_text(last_exception_call_stack); string message = "Unhandled exception"; if (context != "") message += " (" + context + ")"; if (info != "") message += ": " + info; if (filePath != "") message += " at " + filePath; if (line > 0) message += ":" + line; if (func != "") message += " in " + func; if (stack != "") message += " | stack: " + stack; message += " " + format_log_timestamp(); file logFile; if (logFile.open("crash.log", "ab")) { logFile.write(message + "\r\n"); logFile.close(); } } string normalize_player_name(string name) { string result = ""; bool lastWasSpace = true; for (uint i = 0; i < name.length(); i++) { string ch = name.substr(i, 1); bool isSpace = (ch == " " || ch == "\t" || ch == "\r" || ch == "\n"); if (isSpace) { if (!lastWasSpace) { result += " "; lastWasSpace = true; } continue; } result += ch; lastWasSpace = false; } if (result.length() > 0 && result.substr(result.length() - 1) == " ") { result = result.substr(0, result.length() - 1); } return result; } bool is_windows_reserved_name(const string&in upperName) { if (upperName == "CON" || upperName == "PRN" || upperName == "AUX" || upperName == "NUL") return true; if (upperName.length() == 4 && upperName.substr(0, 3) == "COM") { int num = parse_int(upperName.substr(3)); if (num >= 1 && num <= 9) return true; } if (upperName.length() == 4 && upperName.substr(0, 3) == "LPT") { int num = parse_int(upperName.substr(3)); if (num >= 1 && num <= 9) return true; } return false; } string sanitize_save_filename_base(string name) { string normalized = normalize_player_name(name); string result = ""; bool lastSeparator = false; for (uint i = 0; i < normalized.length(); i++) { string ch = normalized.substr(i, 1); bool isUpper = (ch >= "A" && ch <= "Z"); bool isLower = (ch >= "a" && ch <= "z"); bool isDigit = (ch >= "0" && ch <= "9"); if (isUpper || isLower || isDigit) { result += ch; lastSeparator = false; continue; } if (ch == " " || ch == "_" || ch == "-") { if (!lastSeparator && result.length() > 0) { result += "_"; lastSeparator = true; } } } while (result.length() > 0 && result.substr(result.length() - 1) == "_") { result = result.substr(0, result.length() - 1); } if (result.length() == 0) { result = "character"; } if (result.length() > 40) { result = result.substr(0, 40); } while (result.length() > 0 && result.substr(result.length() - 1) == "_") { result = result.substr(0, result.length() - 1); } string upperName = result.upper(); if (upperName == "." || upperName == "..") { result = "character"; upperName = result.upper(); } if (is_windows_reserved_name(upperName)) { result = "save_" + result; } return result; } string get_save_filename_for_name(const string&in name) { return sanitize_save_filename_base(name) + SAVE_EXTENSION; } string strip_save_extension(const string&in filename) { if (filename.length() >= SAVE_EXTENSION.length() && filename.substr(filename.length() - SAVE_EXTENSION.length()) == SAVE_EXTENSION) { return filename.substr(0, filename.length() - SAVE_EXTENSION.length()); } return filename; } bool read_save_metadata(const string&in filename, string &out displayName, int &out sex, int &out day) { string encryptedData; if (!read_file_string(filename, encryptedData)) { return false; } string rawData = decrypt_save_data(encryptedData); dictionary@ saveData = deserialize(rawData); if (@saveData == null || !dictionary_has_keys(saveData)) { saveData = deserialize(encryptedData); } if (@saveData == null || !dictionary_has_keys(saveData)) { return false; } displayName = ""; if (!saveData.get("player_name", displayName)) { displayName = ""; } sex = int(get_number(saveData, "player_sex", SEX_MALE)); if (sex != SEX_FEMALE) sex = SEX_MALE; day = int(get_number(saveData, "time_current_day", 1)); if (day < 1) day = 1; if (displayName == "") { displayName = strip_save_extension(filename); } return true; } bool is_name_used(const string&in name, const string[]@ usedNames) { string target = name.lower(); if (@usedNames != null) { for (uint i = 0; i < usedNames.length(); i++) { if (usedNames[i].lower() == target) return true; } } if (file_exists(get_save_filename_for_name(name))) return true; return false; } string pick_random_name(const string[]@ pool, const string[]@ usedNames) { string[] available; if (@pool != null) { for (uint i = 0; i < pool.length(); i++) { if (!is_name_used(pool[i], usedNames)) { available.insert_last(pool[i]); } } } const string[]@ pickFrom = @available; if (available.length() == 0) { @pickFrom = pool; } if (@pickFrom == null || pickFrom.length() == 0) return "character"; int index = random(0, int(pickFrom.length()) - 1); return pickFrom[index]; } string[] get_existing_character_names() { string[] names; string[] files = get_save_files(); for (uint i = 0; i < files.length(); i++) { string displayName; int sex = SEX_MALE; int day = 1; if (read_save_metadata(files[i], displayName, sex, day)) { if (displayName.length() > 0) { names.insert_last(displayName); } } } return names; } string[] male_name_pool = { "Arne", "Asbjorn", "Aegir", "Bjorn", "Brand", "Egil", "Einar", "Eirik", "Erik", "Gunnar", "Gudmund", "Hakon", "Halfdan", "Hallvard", "Harald", "Hjalmar", "Hrafn", "Hrolf", "Ivar", "Ketil", "Knut", "Leif", "Magnus", "Njord", "Odd", "Olaf", "Orm", "Ragnar", "Roald", "Rolf", "Sigurd", "Sten", "Stig", "Sven", "Svend", "Thor", "Toke", "Torbjorn", "Torstein", "Trygve", "Ulf", "Ulrik", "Valdemar", "Vidar", "Yngvar", "Haldor", "Skjold", "Eystein", "Gorm", "Havard" }; string[] female_name_pool = { "Astrid", "Asta", "Birgit", "Brynhild", "Dagny", "Eira", "Freya", "Frida", "Gerda", "Gudrun", "Gunhild", "Halla", "Helga", "Hild", "Hilda", "Inga", "Ingrid", "Kari", "Lagertha", "Liv", "Ragna", "Ragnhild", "Randi", "Runa", "Sif", "Signy", "Sigrid", "Solveig", "Sunniva", "Thora", "Thyra", "Tora", "Tove", "Tyrna", "Ulla", "Yrsa", "Ylva", "Aud", "Eydis", "Herdis", "Ingunn", "Jorunn", "Ragnheid", "Sigrun", "Torhild", "Ase", "Alfhild", "Gudlaug", "Katra", "Rikissa" }; string pick_random_name_for_sex(int sex, const string[]@ usedNames) { if (sex == SEX_FEMALE) { return pick_random_name(female_name_pool, usedNames); } return pick_random_name(male_name_pool, usedNames); } bool select_player_sex(int &out sex) { string[] options = {"Male", "Female"}; int selection = 0; string prompt = "Choose your sex."; speak_with_history(prompt + " " + options[selection], true); while (true) { wait(5); if (key_pressed(KEY_DOWN)) { play_menu_move_sound(); selection++; if (selection >= options.length()) selection = 0; speak_with_history(options[selection], true); } if (key_pressed(KEY_UP)) { play_menu_move_sound(); selection--; if (selection < 0) selection = options.length() - 1; speak_with_history(options[selection], true); } if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); sex = (selection == 1) ? SEX_FEMALE : SEX_MALE; return true; } if (key_pressed(KEY_ESCAPE)) { return false; } } return false; } bool setup_new_character() { int selectedSex = SEX_MALE; if (!select_player_sex(selectedSex)) { return false; } string[] existingNames = get_existing_character_names(); while (true) { string entered = ui_input_box("Draugnorak", "Enter your name or press Enter for random.", ""); string normalized = normalize_player_name(entered); if (normalized.length() == 0) { normalized = pick_random_name_for_sex(selectedSex, existingNames); } string saveFile = get_save_filename_for_name(normalized); if (file_exists(saveFile)) { int confirm = ui_question("", "Save found for " + normalized + ". Overwrite?"); if (confirm != 1) { continue; } } player_name = normalized; player_sex = selectedSex; current_save_file = saveFile; return true; } return false; } bool select_save_file(string &out filename) { while (true) { string[] files = get_save_files(); if (files.length() == 0) return false; string[] options; string[] displayNames; int[] sexValues; int[] dayValues; bool[] hasMetadata; for (uint i = 0; i < files.length(); i++) { string displayName; int sex = SEX_MALE; int day = 1; bool gotMeta = read_save_metadata(files[i], displayName, sex, day); if (!gotMeta) { displayName = strip_save_extension(files[i]); options.insert_last(displayName); } else { string sex_label = (sex == SEX_FEMALE) ? "female" : "male"; options.insert_last(displayName + ", " + sex_label + ", day " + day); } displayNames.insert_last(displayName); sexValues.insert_last(sex); dayValues.insert_last(day); hasMetadata.insert_last(gotMeta); } int selection = 0; speak_with_history("Load game. Select character. Press delete to remove a save.", true); speak_with_history(options[selection], true); bool refreshList = false; while (true) { wait(5); if (key_pressed(KEY_DOWN)) { play_menu_move_sound(); selection++; if (selection >= options.length()) selection = 0; speak_with_history(options[selection], true); } if (key_pressed(KEY_UP)) { play_menu_move_sound(); selection--; if (selection < 0) selection = int(options.length()) - 1; speak_with_history(options[selection], true); } if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); filename = files[selection]; return true; } if (key_pressed(KEY_DELETE)) { string prompt = "Are you sure you want to delete the character " + displayNames[selection]; if (hasMetadata[selection]) { string sex_label = (sexValues[selection] == SEX_FEMALE) ? "female" : "male"; prompt += " gender " + sex_label + " days " + dayValues[selection] + "?"; } else { prompt += "?"; } int confirm = ui_question("", prompt); if (confirm == 1) { if (file_delete(files[selection])) { speak_with_history("Save deleted.", true); refreshList = true; break; } else { ui_info_box("Draugnorak", "Delete Save", "Unable to delete save."); speak_with_history(options[selection], true); } } else { speak_with_history(options[selection], true); } } if (key_pressed(KEY_ESCAPE)) { return false; } } if (!refreshList) { return false; } } return false; } void stop_active_sounds() { if (day_sound_handle != -1) { p.destroy_sound(day_sound_handle); day_sound_handle = -1; } if (night_sound_handle != -1) { p.destroy_sound(night_sound_handle); night_sound_handle = -1; } if (fall_sound_handle != -1) { p.destroy_sound(fall_sound_handle); fall_sound_handle = -1; } if (sling_sound_handle != -1) { p.destroy_sound(sling_sound_handle); sling_sound_handle = -1; } if (bow_shot_sound_handle != -1) { p.destroy_sound(bow_shot_sound_handle); bow_shot_sound_handle = -1; } stop_all_weather_sounds(); } void clear_world_objects() { for (uint i = 0; i < world_snares.length(); i++) { world_snares[i].destroy(); } world_snares.resize(0); for (uint i = 0; i < world_fires.length(); i++) { world_fires[i].destroy(); } world_fires.resize(0); for (uint i = 0; i < world_streams.length(); i++) { world_streams[i].destroy(); } world_streams.resize(0); world_firepits.resize(0); world_herb_gardens.resize(0); world_storages.resize(0); world_pastures.resize(0); world_stables.resize(0); world_altars.resize(0); for (uint i = 0; i < trees.length(); i++) { if (trees[i].sound_handle != -1) { p.destroy_sound(trees[i].sound_handle); trees[i].sound_handle = -1; } } trees.resize(0); clear_zombies(); clear_bandits(); clear_flying_creatures(); clear_mountains(); clear_world_drops(); } void reset_game_state() { stop_active_sounds(); clear_world_objects(); x = 0; y = 0; facing = 1; jumping = false; climbing = false; falling = false; climb_target_y = 0; fall_start_y = 0; sling_charging = false; bow_drawing = false; bow_shot_active = false; bow_shot_start_x = 0; bow_shot_end_x = 0; bow_shot_hit_x = -1; bow_shot_hit_type = 0; bow_shot_duration_ms = 0; bow_shot_sound_handle = -1; bow_shot_drop_pending = false; bow_shot_drop_pos = -1; searching = false; rope_climbing = false; rope_climb_up = true; rope_climb_target_x = 0; rope_climb_target_y = 0; rope_climb_start_y = 0; rope_climb_sound_handle = -1; pending_rope_climb_x = -1; pending_rope_climb_elevation = 0; is_casting = false; line_in_water = false; fish_on_line = false; is_reeling = false; cast_position = -1; reel_position = -1; line_position = -1; cast_origin_x = -1; cast_direction = 0; reel_direction = 0; reel_start_direction = 0; target_stream_start = -1; target_stream_end = -1; catch_chance = 0; fishing_checks_done = 0; hooked_fish_type = ""; if (cast_sound_handle != -1) { p.destroy_sound(cast_sound_handle); cast_sound_handle = -1; } player_health = 10; base_max_health = 10; max_health = 10; favor = 0.0; last_adventure_day = -1; incense_hours_remaining = 0; incense_burning = false; blessing_speed_active = false; blessing_resident_active = false; reset_fylgja_state(); reset_pet_state(); // Reset inventory using the registry system reset_inventory(); spear_equipped = false; axe_equipped = false; sling_equipped = false; bow_equipped = false; fishing_pole_equipped = false; equipped_head = EQUIP_NONE; equipped_torso = EQUIP_NONE; equipped_arms = EQUIP_NONE; equipped_hands = EQUIP_NONE; equipped_legs = EQUIP_NONE; equipped_feet = EQUIP_NONE; reset_quick_slots(); update_max_health_from_equipment(); MAP_SIZE = 35; expanded_area_start = -1; expanded_area_end = -1; expanded_terrain_types.resize(0); barricade_health = 0; barricade_initialized = false; residents_count = 0; undead_residents_count = 0; undead_residents_pending = 0; horses_count = 0; livestock_count = 0; storage_level = STORAGE_LEVEL_BASE; current_hour = 8; current_day = 1; is_daytime = true; sun_setting_warned = false; sunrise_warned = false; crossfade_active = false; crossfade_to_night = false; area_expanded_today = false; invasion_active = false; invasion_start_hour = -1; invasion_chance = 25; invasion_triggered_today = false; invasion_roll_done_today = false; invasion_scheduled_hour = -1; invasion_started_once = false; zombie_swarm_active = false; zombie_swarm_start_hour = -1; zombie_swarm_scheduled_hour = -1; zombie_swarm_triggered_today = false; zombie_swarm_roll_done_today = false; zombie_swarm_duration_hours = 0; quest_roll_done_today = false; quest_queue.resize(0); playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MIN; playerItemBreaksToday = 0; playerItemBreakPending = false; playerItemBreakPendingType = -1; playerItemBreakMessage = ""; playerItemBreakSoundHandle = -1; playerItemBreakSoundPending = false; walktimer.restart(); jumptimer.restart(); search_timer.restart(); search_delay_timer.restart(); attack_timer.restart(); hour_timer.restart(); fire_damage_timer.restart(); healing_timer.restart(); sling_charge_timer.restart(); bow_draw_timer.restart(); bow_shot_timer.restart(); fall_timer.restart(); climb_timer.restart(); } void start_new_game() { reset_game_state(); spawn_trees(5, 19); normalize_tree_positions(); init_barricade(); ensure_base_storage(); init_time(); init_weather(); if (player_name.length() == 0) { player_name = "Character"; } if (current_save_file == "") { current_save_file = get_save_filename_for_name(player_name); } save_game_state(); } string serialize_bool(bool value) { return value ? "1" : "0"; } string serialize_tree(Tree@ tree) { return tree.position + "|" + tree.sticks + "|" + tree.vines + "|" + tree.health + "|" + tree.height + "|" + serialize_bool(tree.depleted) + "|" + serialize_bool(tree.is_chopped) + "|" + tree.minutes_since_depletion; } string serialize_snare(WorldSnare@ snare) { return snare.position + "|" + serialize_bool(snare.has_catch) + "|" + snare.catch_type + "|" + snare.catch_chance + "|" + snare.escape_chance + "|" + serialize_bool(snare.active) + "|" + snare.hours_with_catch; } string serialize_fire(WorldFire@ fire) { return fire.position + "|" + fire.fuel_remaining + "|" + serialize_bool(fire.low_fuel_warned); } string serialize_stream(WorldStream@ stream) { return stream.start_position + "|" + stream.get_width(); } string serialize_bandit(Bandit@ bandit) { return bandit.position + "|" + bandit.health + "|" + bandit.weapon_type + "|" + bandit.behavior_state + "|" + bandit.wander_direction + "|" + bandit.move_interval + "|" + bandit.invader_type + "|" + bandit.home_start + "|" + bandit.home_end; } string serialize_mountain(MountainRange@ mountain) { string result = mountain.start_position + "|" + mountain.end_position + "|"; // Serialize elevations for (int i = 0; i < int(mountain.elevations.length()); i++) { if (i > 0) result += ","; result += mountain.elevations[i]; } result += "|"; // Serialize terrain types for (int i = 0; i < int(mountain.terrain_types.length()); i++) { if (i > 0) result += ","; result += mountain.terrain_types[i]; } result += "|"; // Serialize stream positions for (uint i = 0; i < mountain.stream_positions.length(); i++) { if (i > 0) result += ","; result += mountain.stream_positions[i]; } return result; } string join_string_array(const string[]@ arr) { if (@arr == null || arr.length() == 0) return ""; string result = arr[0]; for (uint i = 1; i < arr.length(); i++) { result += "\n" + arr[i]; } return result; } string[] split_string_array(const string&in data) { string[] result; if (data.length() == 0) return result; result = data.split("\n"); return result; } string[] get_string_list_or_split(dictionary@ data, const string&in key) { string[] result = get_string_list(data, key); if (result.length() > 0) return result; string value; if (@data != null && data.get(key, value)) { return split_string_array(value); } return result; } // Serialize inventory array to comma-separated string string serialize_inventory_array(const int[]@ arr) { if (@arr == null || arr.length() == 0) return ""; string result = "" + arr[0]; for (uint i = 1; i < arr.length(); i++) { result += "," + arr[i]; } return result; } // Deserialize comma-separated string to inventory array void deserialize_inventory_array(const string&in data, int[]@ arr, int expected_size) { arr.resize(expected_size); for (int i = 0; i < expected_size; i++) { arr[i] = 0; } if (data.length() == 0) return; string[]@ parts = data.split(","); uint count = parts.length(); if (count > uint(expected_size)) count = uint(expected_size); for (uint i = 0; i < count; i++) { arr[i] = parse_int(parts[i]); } } bool save_game_state() { dictionary saveData; saveData.set("version", SAVE_VERSION); saveData.set("player_x", x); saveData.set("player_y", y); saveData.set("player_facing", facing); saveData.set("player_health", player_health); saveData.set("player_base_health", base_max_health); saveData.set("player_max_health", max_health); saveData.set("player_name", player_name); saveData.set("player_sex", player_sex); saveData.set("player_favor", favor); saveData.set("player_last_adventure_day", last_adventure_day); saveData.set("adventure_completion_counts", serialize_inventory_array(adventureCompletionCounts)); saveData.set("incense_hours_remaining", incense_hours_remaining); saveData.set("incense_burning", incense_burning); saveData.set("pet_active", petActive); saveData.set("pet_sound_path", petSoundPath); saveData.set("pet_type", petType); saveData.set("pet_gender", petGender); saveData.set("pet_loyalty", petLoyalty); saveData.set("pet_health", petHealth); saveData.set("pet_ko_hours_remaining", petKnockoutHoursRemaining); // Save inventory arrays using new compact format saveData.set("personal_inventory", serialize_inventory_array(personal_inventory)); saveData.set("storage_inventory", serialize_inventory_array(storage_inventory)); saveData.set("personal_small_game_types", join_string_array(personal_small_game_types)); saveData.set("storage_small_game_types", join_string_array(storage_small_game_types)); string[] personalFishWeightsData; for (uint i = 0; i < personal_fish_weights.length(); i++) { personalFishWeightsData.insert_last("" + personal_fish_weights[i]); } saveData.set("personal_fish_weights", join_string_array(personalFishWeightsData)); string[] storageFishWeightsData; for (uint i = 0; i < storage_fish_weights.length(); i++) { storageFishWeightsData.insert_last("" + storage_fish_weights[i]); } saveData.set("storage_fish_weights", join_string_array(storageFishWeightsData)); saveData.set("equipment_spear_equipped", spear_equipped); saveData.set("equipment_axe_equipped", axe_equipped); saveData.set("equipment_sling_equipped", sling_equipped); saveData.set("equipment_bow_equipped", bow_equipped); saveData.set("equipment_fishing_pole_equipped", fishing_pole_equipped); saveData.set("equipment_head", equipped_head); saveData.set("equipment_torso", equipped_torso); saveData.set("equipment_arms", equipped_arms); saveData.set("equipment_hands", equipped_hands); saveData.set("equipment_legs", equipped_legs); saveData.set("equipment_feet", equipped_feet); string[] quickSlotData; for (uint i = 0; i < quick_slots.length(); i++) { quickSlotData.insert_last("" + quick_slots[i]); } saveData.set("equipment_quick_slots", join_string_array(quickSlotData)); string[] quickSlotRuneData; for (uint i = 0; i < quick_slot_runes.length(); i++) { quickSlotRuneData.insert_last("" + quick_slot_runes[i]); } saveData.set("equipment_quick_slot_runes", join_string_array(quickSlotRuneData)); string[] itemCountSlotData; for (uint i = 0; i < item_count_slots.length(); i++) { itemCountSlotData.insert_last("" + item_count_slots[i]); } saveData.set("item_count_slots", join_string_array(itemCountSlotData)); // Rune system data saveData.set("rune_swiftness_unlocked", rune_swiftness_unlocked); saveData.set("rune_destruction_unlocked", rune_destruction_unlocked); saveData.set("unicorn_boss_defeated", unicorn_boss_defeated); saveData.set("equipped_head_rune", equipped_head_rune); saveData.set("equipped_torso_rune", equipped_torso_rune); saveData.set("equipped_arms_rune", equipped_arms_rune); saveData.set("equipped_hands_rune", equipped_hands_rune); saveData.set("equipped_legs_rune", equipped_legs_rune); saveData.set("equipped_feet_rune", equipped_feet_rune); saveData.set("equipped_weapon_rune", equipped_weapon_rune); // Save runed items dictionary as key:value pairs string[] runed_items_data; string[]@ keys = runed_items.get_keys(); for (uint i = 0; i < keys.length(); i++) { string key = keys[i]; int count = int(runed_items[key]); if (count > 0) { runed_items_data.insert_last(key + "=" + count); } } saveData.set("runed_items", join_string_array(runed_items_data)); // Save stored runed items dictionary string[] stored_runed_items_data; string[]@ stored_keys = stored_runed_items.get_keys(); for (uint i = 0; i < stored_keys.length(); i++) { string key = stored_keys[i]; int count = int(stored_runed_items[key]); if (count > 0) { stored_runed_items_data.insert_last(key + "=" + count); } } saveData.set("stored_runed_items", join_string_array(stored_runed_items_data)); saveData.set("time_current_hour", current_hour); saveData.set("time_current_day", current_day); saveData.set("time_is_daytime", is_daytime); saveData.set("time_sun_setting_warned", sun_setting_warned); saveData.set("time_sunrise_warned", sunrise_warned); saveData.set("time_area_expanded_today", area_expanded_today); saveData.set("time_invasion_active", invasion_active); saveData.set("time_invasion_start_hour", invasion_start_hour); saveData.set("time_invasion_chance", invasion_chance); saveData.set("time_invasion_triggered_today", invasion_triggered_today); saveData.set("time_invasion_roll_done_today", invasion_roll_done_today); saveData.set("time_invasion_scheduled_hour", invasion_scheduled_hour); saveData.set("time_invasion_enemy_type", invasion_enemy_type); saveData.set("time_invasion_started_once", invasion_started_once); saveData.set("time_zombie_swarm_active", zombie_swarm_active); saveData.set("time_zombie_swarm_start_hour", zombie_swarm_start_hour); saveData.set("time_zombie_swarm_scheduled_hour", zombie_swarm_scheduled_hour); saveData.set("time_zombie_swarm_triggered_today", zombie_swarm_triggered_today); saveData.set("time_zombie_swarm_roll_done_today", zombie_swarm_roll_done_today); saveData.set("time_zombie_swarm_duration_hours", zombie_swarm_duration_hours); saveData.set("player_item_break_chance", playerItemBreakChance); saveData.set("player_item_breaks_today", playerItemBreaksToday); saveData.set("player_item_break_pending", playerItemBreakPending); saveData.set("player_item_break_pending_type", playerItemBreakPendingType); saveData.set("quest_roll_done_today", quest_roll_done_today); saveData.set("wight_spawn_chance", wight_spawn_chance); saveData.set("wight_spawned_this_night", wight_spawned_this_night_count); saveData.set("vampyr_spawn_chance", vampyr_spawn_chance); saveData.set("vampyr_spawned_this_night", vampyr_spawned_this_night_count); string[] questData; for (uint i = 0; i < quest_queue.length(); i++) { questData.insert_last("" + quest_queue[i]); } saveData.set("quest_queue", join_string_array(questData)); saveData.set("weather_data", serialize_weather()); saveData.set("world_map_size", MAP_SIZE); saveData.set("world_expanded_area_start", expanded_area_start); saveData.set("world_expanded_area_end", expanded_area_end); saveData.set("world_barricade_initialized", barricade_initialized); saveData.set("world_barricade_health", barricade_health); saveData.set("world_residents_count", residents_count); saveData.set("world_undead_residents_count", undead_residents_count); saveData.set("world_undead_residents_pending", undead_residents_pending); saveData.set("world_horses_count", horses_count); saveData.set("world_livestock_count", livestock_count); saveData.set("world_storage_level", storage_level); saveData.set("world_expanded_terrain_types", join_string_array(expanded_terrain_types)); string[] treeData; for (uint i = 0; i < trees.length(); i++) { treeData.insert_last(serialize_tree(trees[i])); } saveData.set("trees_data", join_string_array(treeData)); string[] snareData; for (uint i = 0; i < world_snares.length(); i++) { snareData.insert_last(serialize_snare(world_snares[i])); } saveData.set("snares_data", join_string_array(snareData)); string[] fireData; for (uint i = 0; i < world_fires.length(); i++) { fireData.insert_last(serialize_fire(world_fires[i])); } saveData.set("fires_data", join_string_array(fireData)); string[] firepitPositions; for (uint i = 0; i < world_firepits.length(); i++) { firepitPositions.insert_last("" + world_firepits[i].position); } saveData.set("firepits_positions", join_string_array(firepitPositions)); string[] herbPositions; for (uint i = 0; i < world_herb_gardens.length(); i++) { herbPositions.insert_last("" + world_herb_gardens[i].position); } saveData.set("herb_gardens_positions", join_string_array(herbPositions)); string[] storagePositions; for (uint i = 0; i < world_storages.length(); i++) { storagePositions.insert_last("" + world_storages[i].position); } saveData.set("storages_positions", join_string_array(storagePositions)); string[] pasturePositions; for (uint i = 0; i < world_pastures.length(); i++) { pasturePositions.insert_last("" + world_pastures[i].position); } saveData.set("pastures_positions", join_string_array(pasturePositions)); string[] stablePositions; for (uint i = 0; i < world_stables.length(); i++) { stablePositions.insert_last("" + world_stables[i].position); } saveData.set("stables_positions", join_string_array(stablePositions)); string[] altarPositions; for (uint i = 0; i < world_altars.length(); i++) { altarPositions.insert_last("" + world_altars[i].position); } saveData.set("altars_positions", join_string_array(altarPositions)); string[] streamData; for (uint i = 0; i < world_streams.length(); i++) { streamData.insert_last(serialize_stream(world_streams[i])); } saveData.set("streams_data", join_string_array(streamData)); string[] banditData; for (uint i = 0; i < bandits.length(); i++) { banditData.insert_last(serialize_bandit(bandits[i])); } saveData.set("bandits_data", join_string_array(banditData)); string[] mountainData; for (uint i = 0; i < world_mountains.length(); i++) { mountainData.insert_last(serialize_mountain(world_mountains[i])); } saveData.set("mountains_data", join_string_array(mountainData)); string[] dropData; for (uint i = 0; i < world_drops.length(); i++) { dropData.insert_last(world_drops[i].position + "|" + world_drops[i].type); } saveData.set("drops_data", join_string_array(dropData)); if (current_save_file == "") { current_save_file = get_save_filename_for_name(player_name); } string rawData = saveData.serialize(); string encryptedData = encrypt_save_data(rawData); return save_data(current_save_file, encryptedData); } bool load_game_state_from_file(const string&in filename) { last_save_error = ""; if (!file_exists(filename)) { last_save_error = "No save file found."; return false; } string encryptedData; if (!read_file_string(filename, encryptedData)) { last_save_error = "Unable to read save file."; return false; } string rawData = decrypt_save_data(encryptedData); dictionary@ saveData = deserialize(rawData); if (@saveData == null || !dictionary_has_keys(saveData)) { saveData = deserialize(encryptedData); } if (@saveData == null || !dictionary_has_keys(saveData)) { last_save_error = "Save file is corrupted or unreadable."; return false; } double version; bool has_version = saveData.get("version", version); if (!has_version) { if (!has_number_key(saveData, "player_x") || !has_number_key(saveData, "player_health") || !has_number_key(saveData, "time_current_day")) { last_save_error = "Save file is missing required data."; return false; } version = 0; } reset_game_state(); current_save_file = filename; MAP_SIZE = int(get_number(saveData, "world_map_size", 35)); expanded_area_start = int(get_number(saveData, "world_expanded_area_start", -1)); expanded_area_end = int(get_number(saveData, "world_expanded_area_end", -1)); string[] loadedTerrain = get_string_list_or_split(saveData, "world_expanded_terrain_types"); expanded_terrain_types.resize(0); for (uint i = 0; i < loadedTerrain.length(); i++) { expanded_terrain_types.insert_last(loadedTerrain[i]); } barricade_initialized = get_bool(saveData, "world_barricade_initialized", true); barricade_health = int(get_number(saveData, "world_barricade_health", BARRICADE_BASE_HEALTH)); residents_count = int(get_number(saveData, "world_residents_count", 0)); if (residents_count < 0) residents_count = 0; undead_residents_count = int(get_number(saveData, "world_undead_residents_count", 0)); if (undead_residents_count < 0) undead_residents_count = 0; undead_residents_pending = int(get_number(saveData, "world_undead_residents_pending", 0)); if (undead_residents_pending < 0) undead_residents_pending = 0; horses_count = int(get_number(saveData, "world_horses_count", 0)); livestock_count = int(get_number(saveData, "world_livestock_count", 0)); if (horses_count < 0) horses_count = 0; if (livestock_count < 0) livestock_count = 0; if (horses_count > MAX_HORSES) horses_count = MAX_HORSES; if (livestock_count > MAX_LIVESTOCK) livestock_count = MAX_LIVESTOCK; if (!barricade_initialized) { init_barricade(); } else { if (barricade_health < 0) barricade_health = 0; if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH; } x = int(get_number(saveData, "player_x", 0)); y = int(get_number(saveData, "player_y", 0)); facing = int(get_number(saveData, "player_facing", 1)); player_health = int(get_number(saveData, "player_health", 10)); max_health = int(get_number(saveData, "player_max_health", 10)); base_max_health = int(get_number(saveData, "player_base_health", max_health)); string loadedName; if (saveData.get("player_name", loadedName)) { player_name = loadedName; } else { player_name = strip_save_extension(filename); } player_sex = int(get_number(saveData, "player_sex", SEX_MALE)); if (player_sex != SEX_FEMALE) player_sex = SEX_MALE; favor = get_number(saveData, "player_favor", 0.0); last_adventure_day = int(get_number(saveData, "player_last_adventure_day", -1)); string adventureCountsStr; if (saveData.get("adventure_completion_counts", adventureCountsStr)) { deserialize_inventory_array(adventureCountsStr, adventureCompletionCounts, int(adventureIds.length())); } incense_hours_remaining = int(get_number(saveData, "incense_hours_remaining", 0)); incense_burning = get_bool(saveData, "incense_burning", false); if (incense_hours_remaining > 0) incense_burning = true; petActive = get_bool(saveData, "pet_active", false); string loadedPetSound; petSoundPath = ""; if (saveData.get("pet_sound_path", loadedPetSound)) { petSoundPath = loadedPetSound; } string loadedPetType; petType = ""; if (saveData.get("pet_type", loadedPetType)) { petType = loadedPetType; } string loadedPetGender; petGender = ""; if (saveData.get("pet_gender", loadedPetGender)) { petGender = loadedPetGender; } petLoyalty = int(get_number(saveData, "pet_loyalty", 0)); petHealth = int(get_number(saveData, "pet_health", PET_HEALTH_MAX)); petKnockoutHoursRemaining = int(get_number(saveData, "pet_ko_hours_remaining", 0)); if (!petActive) { petSoundPath = ""; petType = ""; petGender = ""; petLoyalty = 0; petHealth = 0; petKnockoutHoursRemaining = 0; } if (petActive && petSoundPath != "" && !file_exists(petSoundPath)) { petActive = false; petSoundPath = ""; petType = ""; petGender = ""; petLoyalty = 0; petHealth = 0; petKnockoutHoursRemaining = 0; } if (petActive) { if (petKnockoutHoursRemaining < 0) petKnockoutHoursRemaining = 0; if (petKnockoutHoursRemaining > PET_KNOCKOUT_COOLDOWN_HOURS) { petKnockoutHoursRemaining = PET_KNOCKOUT_COOLDOWN_HOURS; } if (petKnockoutHoursRemaining > 0) { petHealth = 0; } else { if (petHealth <= 0) petHealth = PET_HEALTH_MAX; if (petHealth > PET_HEALTH_MAX) petHealth = PET_HEALTH_MAX; } petAttackTimer.restart(); petRetrieveTimer.restart(); } if (x < 0) x = 0; if (x >= MAP_SIZE) x = MAP_SIZE - 1; if (y < 0) y = 0; if (facing != 0 && facing != 1) facing = 1; // Load inventory using new array format string personal_inv_str; if (saveData.get("personal_inventory", personal_inv_str)) { deserialize_inventory_array(personal_inv_str, personal_inventory, ITEM_COUNT); } string storage_inv_str; if (saveData.get("storage_inventory", storage_inv_str)) { deserialize_inventory_array(storage_inv_str, storage_inventory, ITEM_COUNT); } // Load small game types string[] loadedSmallGameTypes = get_string_list_or_split(saveData, "personal_small_game_types"); personal_small_game_types.resize(0); for (uint i = 0; i < loadedSmallGameTypes.length(); i++) { personal_small_game_types.insert_last(loadedSmallGameTypes[i]); } // Sync small game count with types array int small_game_count = get_personal_count(ITEM_SMALL_GAME); if (personal_small_game_types.length() == 0 && small_game_count > 0) { for (int i = 0; i < small_game_count; i++) { personal_small_game_types.insert_last("small game"); } } else { set_personal_count(ITEM_SMALL_GAME, int(personal_small_game_types.length())); } string[] loadedStorageSmallGameTypes = get_string_list_or_split(saveData, "storage_small_game_types"); storage_small_game_types.resize(0); for (uint i = 0; i < loadedStorageSmallGameTypes.length(); i++) { storage_small_game_types.insert_last(loadedStorageSmallGameTypes[i]); } int storage_small_game_count = get_storage_count(ITEM_SMALL_GAME); if (storage_small_game_types.length() == 0 && storage_small_game_count > 0) { for (int i = 0; i < storage_small_game_count; i++) { storage_small_game_types.insert_last("small game"); } } else { set_storage_count(ITEM_SMALL_GAME, int(storage_small_game_types.length())); } string[] loadedPersonalFishWeights = get_string_list_or_split(saveData, "personal_fish_weights"); personal_fish_weights.resize(0); for (uint i = 0; i < loadedPersonalFishWeights.length(); i++) { personal_fish_weights.insert_last(parse_int(loadedPersonalFishWeights[i])); } int personal_fish_count = get_personal_count(ITEM_FISH); if (personal_fish_weights.length() < personal_fish_count) { int missing = personal_fish_count - int(personal_fish_weights.length()); for (int i = 0; i < missing; i++) { personal_fish_weights.insert_last(get_default_fish_weight()); } } else if (personal_fish_weights.length() > personal_fish_count) { personal_fish_weights.resize(personal_fish_count); } string[] loadedStorageFishWeights = get_string_list_or_split(saveData, "storage_fish_weights"); storage_fish_weights.resize(0); for (uint i = 0; i < loadedStorageFishWeights.length(); i++) { storage_fish_weights.insert_last(parse_int(loadedStorageFishWeights[i])); } int storage_fish_count = get_storage_count(ITEM_FISH); if (storage_fish_weights.length() < storage_fish_count) { int missing = storage_fish_count - int(storage_fish_weights.length()); for (int i = 0; i < missing; i++) { storage_fish_weights.insert_last(get_default_fish_weight()); } } else if (storage_fish_weights.length() > storage_fish_count) { storage_fish_weights.resize(storage_fish_count); } spear_equipped = get_bool(saveData, "equipment_spear_equipped", false); axe_equipped = get_bool(saveData, "equipment_axe_equipped", false); sling_equipped = get_bool(saveData, "equipment_sling_equipped", false); bow_equipped = get_bool(saveData, "equipment_bow_equipped", false); fishing_pole_equipped = get_bool(saveData, "equipment_fishing_pole_equipped", false); equipped_head = int(get_number(saveData, "equipment_head", EQUIP_NONE)); equipped_torso = int(get_number(saveData, "equipment_torso", EQUIP_NONE)); equipped_arms = int(get_number(saveData, "equipment_arms", EQUIP_NONE)); equipped_hands = int(get_number(saveData, "equipment_hands", EQUIP_NONE)); equipped_legs = int(get_number(saveData, "equipment_legs", EQUIP_NONE)); equipped_feet = int(get_number(saveData, "equipment_feet", EQUIP_NONE)); reset_quick_slots(); string[] loadedQuickSlots = get_string_list_or_split(saveData, "equipment_quick_slots"); uint slot_count = loadedQuickSlots.length(); if (slot_count > quick_slots.length()) slot_count = quick_slots.length(); for (uint i = 0; i < slot_count; i++) { int slot_value = parse_int(loadedQuickSlots[i]); if (slot_value >= EQUIP_NONE && slot_value <= EQUIP_FISHING_POLE) { quick_slots[i] = slot_value; } } string[] loadedQuickSlotRunes = get_string_list_or_split(saveData, "equipment_quick_slot_runes"); uint rune_slot_count = loadedQuickSlotRunes.length(); if (rune_slot_count > quick_slot_runes.length()) rune_slot_count = quick_slot_runes.length(); for (uint i = 0; i < rune_slot_count; i++) { int slot_value = parse_int(loadedQuickSlotRunes[i]); if (slot_value >= RUNE_NONE) { quick_slot_runes[i] = slot_value; } } string[] loadedItemCountSlots = get_string_list_or_split(saveData, "item_count_slots"); uint count_slot_count = loadedItemCountSlots.length(); if (count_slot_count > item_count_slots.length()) count_slot_count = item_count_slots.length(); for (uint i = 0; i < count_slot_count; i++) { int slot_value = parse_int(loadedItemCountSlots[i]); if (slot_value >= 0 && slot_value < ITEM_COUNT) { item_count_slots[i] = slot_value; } else if (is_runed_item_type(slot_value)) { item_count_slots[i] = slot_value; } } // Load rune system data rune_swiftness_unlocked = get_bool(saveData, "rune_swiftness_unlocked", false); rune_destruction_unlocked = get_bool(saveData, "rune_destruction_unlocked", false); unicorn_boss_defeated = get_bool(saveData, "unicorn_boss_defeated", false); equipped_head_rune = int(get_number(saveData, "equipped_head_rune", RUNE_NONE)); equipped_torso_rune = int(get_number(saveData, "equipped_torso_rune", RUNE_NONE)); equipped_arms_rune = int(get_number(saveData, "equipped_arms_rune", RUNE_NONE)); equipped_hands_rune = int(get_number(saveData, "equipped_hands_rune", RUNE_NONE)); equipped_legs_rune = int(get_number(saveData, "equipped_legs_rune", RUNE_NONE)); equipped_feet_rune = int(get_number(saveData, "equipped_feet_rune", RUNE_NONE)); equipped_weapon_rune = int(get_number(saveData, "equipped_weapon_rune", RUNE_NONE)); // Load runed items dictionary runed_items.delete_all(); string[] loaded_runed_items = get_string_list_or_split(saveData, "runed_items"); for (uint i = 0; i < loaded_runed_items.length(); i++) { string entry = loaded_runed_items[i]; int eq_pos = entry.find("="); if (eq_pos > 0) { string key = entry.substr(0, eq_pos); int count = parse_int(entry.substr(eq_pos + 1)); if (count > 0) { runed_items.set(key, count); } } } // Load stored runed items dictionary stored_runed_items.delete_all(); string[] loaded_stored_runed_items = get_string_list_or_split(saveData, "stored_runed_items"); for (uint i = 0; i < loaded_stored_runed_items.length(); i++) { string entry = loaded_stored_runed_items[i]; int eq_pos = entry.find("="); if (eq_pos > 0) { string key = entry.substr(0, eq_pos); int count = parse_int(entry.substr(eq_pos + 1)); if (count > 0) { stored_runed_items.set(key, count); } } } // Validate equipped items now that runed items are loaded if (!equipment_available(EQUIP_SPEAR)) spear_equipped = false; if (!equipment_available(EQUIP_AXE)) axe_equipped = false; if (!equipment_available(EQUIP_SLING)) sling_equipped = false; if (!equipment_available(EQUIP_BOW)) bow_equipped = false; if (!equipment_available(EQUIP_FISHING_POLE)) fishing_pole_equipped = false; bool any_weapon_equipped = spear_equipped || axe_equipped || sling_equipped || bow_equipped || fishing_pole_equipped; if (!any_weapon_equipped) equipped_weapon_rune = RUNE_NONE; if (!equipment_available(equipped_head)) { equipped_head = EQUIP_NONE; equipped_head_rune = RUNE_NONE; } if (!equipment_available(equipped_torso)) { equipped_torso = EQUIP_NONE; equipped_torso_rune = RUNE_NONE; } if (!equipment_available(equipped_hands)) { equipped_hands = EQUIP_NONE; equipped_hands_rune = RUNE_NONE; } if (!equipment_available(equipped_legs)) { equipped_legs = EQUIP_NONE; equipped_legs_rune = RUNE_NONE; } if (!equipment_available(equipped_feet)) { equipped_feet = EQUIP_NONE; equipped_feet_rune = RUNE_NONE; } if (!equipment_available(equipped_arms)) { equipped_arms = EQUIP_NONE; equipped_arms_rune = RUNE_NONE; } // Now that both equipment and runes are loaded, update stats update_max_health_from_equipment(); current_hour = int(get_number(saveData, "time_current_hour", 8)); current_day = int(get_number(saveData, "time_current_day", 1)); sun_setting_warned = get_bool(saveData, "time_sun_setting_warned", false); sunrise_warned = get_bool(saveData, "time_sunrise_warned", false); area_expanded_today = get_bool(saveData, "time_area_expanded_today", false); invasion_active = get_bool(saveData, "time_invasion_active", false); invasion_start_hour = int(get_number(saveData, "time_invasion_start_hour", -1)); invasion_chance = int(get_number(saveData, "time_invasion_chance", 25)); invasion_triggered_today = get_bool(saveData, "time_invasion_triggered_today", false); invasion_roll_done_today = get_bool(saveData, "time_invasion_roll_done_today", false); invasion_scheduled_hour = int(get_number(saveData, "time_invasion_scheduled_hour", -1)); string loaded_invasion_type; if (saveData.get("time_invasion_enemy_type", loaded_invasion_type)) { invasion_enemy_type = loaded_invasion_type; } else { invasion_enemy_type = "bandit"; } if (invasion_enemy_type == "") { invasion_enemy_type = "bandit"; } bool loaded_invasion_started = false; if (saveData.get("time_invasion_started_once", loaded_invasion_started)) { invasion_started_once = loaded_invasion_started; } else { invasion_started_once = (expanded_area_start != -1); } zombie_swarm_active = get_bool(saveData, "time_zombie_swarm_active", false); zombie_swarm_start_hour = int(get_number(saveData, "time_zombie_swarm_start_hour", -1)); zombie_swarm_scheduled_hour = int(get_number(saveData, "time_zombie_swarm_scheduled_hour", -1)); zombie_swarm_triggered_today = get_bool(saveData, "time_zombie_swarm_triggered_today", false); zombie_swarm_roll_done_today = get_bool(saveData, "time_zombie_swarm_roll_done_today", false); zombie_swarm_duration_hours = int(get_number(saveData, "time_zombie_swarm_duration_hours", 0)); if (invasion_chance < 0) invasion_chance = 0; if (invasion_chance > 100) invasion_chance = 100; if (invasion_scheduled_hour < -1) invasion_scheduled_hour = -1; if (invasion_scheduled_hour > 23) invasion_scheduled_hour = -1; if (zombie_swarm_start_hour < -1 || zombie_swarm_start_hour > 23) zombie_swarm_start_hour = -1; if (zombie_swarm_scheduled_hour < -1 || zombie_swarm_scheduled_hour > 23) zombie_swarm_scheduled_hour = -1; if (zombie_swarm_duration_hours < 0) zombie_swarm_duration_hours = 0; if (!zombie_swarm_active) { zombie_swarm_start_hour = -1; zombie_swarm_duration_hours = 0; } playerItemBreakChance = float(get_number(saveData, "player_item_break_chance", PLAYER_ITEM_BREAK_CHANCE_MIN)); playerItemBreaksToday = int(get_number(saveData, "player_item_breaks_today", 0)); playerItemBreakPending = get_bool(saveData, "player_item_break_pending", false); playerItemBreakPendingType = int(get_number(saveData, "player_item_break_pending_type", -1)); if (playerItemBreakChance < PLAYER_ITEM_BREAK_CHANCE_MIN) { playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MIN; } if (playerItemBreakChance > PLAYER_ITEM_BREAK_CHANCE_MAX) { playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MAX; } if (playerItemBreaksToday < 0) playerItemBreaksToday = 0; if (playerItemBreaksToday > PLAYER_ITEM_BREAKS_PER_DAY) { playerItemBreaksToday = PLAYER_ITEM_BREAKS_PER_DAY; } if (playerItemBreakPending) { bool validPending = (playerItemBreakPendingType >= 0 && playerItemBreakPendingType < ITEM_COUNT) || is_runed_item_type(playerItemBreakPendingType); if (!validPending) { playerItemBreakPending = false; playerItemBreakPendingType = -1; } } else { playerItemBreakPendingType = -1; } reset_player_item_break_audio_state(); quest_roll_done_today = get_bool(saveData, "quest_roll_done_today", false); wight_spawn_chance = int(get_number(saveData, "wight_spawn_chance", WIGHT_SPAWN_CHANCE_START)); if (has_number_key(saveData, "wight_spawned_this_night")) { wight_spawned_this_night_count = int(get_number(saveData, "wight_spawned_this_night", 0)); } else { wight_spawned_this_night_count = get_bool(saveData, "wight_spawned_this_night", false) ? 1 : 0; } if (wight_spawned_this_night_count < 0) wight_spawned_this_night_count = 0; if (wight_spawn_chance < WIGHT_SPAWN_CHANCE_START) wight_spawn_chance = WIGHT_SPAWN_CHANCE_START; if (wight_spawn_chance > 100) wight_spawn_chance = 100; vampyr_spawn_chance = int(get_number(saveData, "vampyr_spawn_chance", VAMPYR_SPAWN_CHANCE_START)); if (has_number_key(saveData, "vampyr_spawned_this_night")) { vampyr_spawned_this_night_count = int(get_number(saveData, "vampyr_spawned_this_night", 0)); } else { vampyr_spawned_this_night_count = get_bool(saveData, "vampyr_spawned_this_night", false) ? 1 : 0; } if (vampyr_spawned_this_night_count < 0) vampyr_spawned_this_night_count = 0; if (vampyr_spawn_chance < VAMPYR_SPAWN_CHANCE_START) vampyr_spawn_chance = VAMPYR_SPAWN_CHANCE_START; if (vampyr_spawn_chance > 100) vampyr_spawn_chance = 100; quest_queue.resize(0); string[] loadedQuests = get_string_list_or_split(saveData, "quest_queue"); for (uint i = 0; i < loadedQuests.length(); i++) { int quest_type = parse_int(loadedQuests[i]); if (quest_type >= 0 && quest_type < QUEST_TYPE_COUNT) { quest_queue.insert_last(quest_type); if (quest_queue.length() >= QUEST_MAX_ACTIVE) break; } } if (current_hour < 0) current_hour = 0; if (current_hour > 23) current_hour = 23; if (current_day < 1) current_day = 1; is_daytime = (current_hour >= 6 && current_hour < 19); hour_timer.restart(); string weather_data; if (saveData.get("weather_data", weather_data) && weather_data.length() > 0) { deserialize_weather(weather_data); } else { init_weather(); } string[] treeData = get_string_list_or_split(saveData, "trees_data"); for (uint i = 0; i < treeData.length(); i++) { string[]@ parts = treeData[i].split("|"); if (parts.length() < 8) continue; int pos = parse_int(parts[0]); Tree@ tree = Tree(pos); tree.sticks = parse_int(parts[1]); tree.vines = parse_int(parts[2]); tree.health = parse_int(parts[3]); tree.height = parse_int(parts[4]); tree.depleted = (parse_int(parts[5]) == 1); tree.is_chopped = (parse_int(parts[6]) == 1); tree.minutes_since_depletion = parse_int(parts[7]); tree.regen_timer.restart(); trees.insert_last(tree); } normalize_tree_positions(); string[] snareData = get_string_list_or_split(saveData, "snares_data"); for (uint i = 0; i < snareData.length(); i++) { string[]@ parts = snareData[i].split("|"); if (parts.length() < 6) continue; int pos = parse_int(parts[0]); WorldSnare@ snare = WorldSnare(pos); snare.has_catch = (parse_int(parts[1]) == 1); snare.catch_type = parts[2]; snare.catch_chance = parse_int(parts[3]); snare.escape_chance = parse_int(parts[4]); snare.active = (parse_int(parts[5]) == 1); // Load hours_with_catch if available (for backwards compatibility with old saves) if (parts.length() >= 7) { snare.hours_with_catch = parse_int(parts[6]); } snare.hour_timer.restart(); world_snares.insert_last(snare); } string[] fireData = get_string_list_or_split(saveData, "fires_data"); for (uint i = 0; i < fireData.length(); i++) { string[]@ parts = fireData[i].split("|"); if (parts.length() < 3) continue; int pos = parse_int(parts[0]); WorldFire@ fire = WorldFire(pos); fire.fuel_remaining = parse_int(parts[1]); fire.low_fuel_warned = (parse_int(parts[2]) == 1); fire.fuel_timer.restart(); world_fires.insert_last(fire); } string[] firepitPositions = get_string_list_or_split(saveData, "firepits_positions"); for (uint i = 0; i < firepitPositions.length(); i++) { add_world_firepit(parse_int(firepitPositions[i])); } string[] herbPositions = get_string_list_or_split(saveData, "herb_gardens_positions"); for (uint i = 0; i < herbPositions.length(); i++) { add_world_herb_garden(parse_int(herbPositions[i])); } string[] storagePositions = get_string_list_or_split(saveData, "storages_positions"); for (uint i = 0; i < storagePositions.length(); i++) { add_world_storage(parse_int(storagePositions[i])); } int loadedStorageLevel = int(get_number(saveData, "world_storage_level", -1)); if (loadedStorageLevel >= STORAGE_LEVEL_BASE && loadedStorageLevel <= STORAGE_LEVEL_UPGRADE_2) { storage_level = loadedStorageLevel; } else { storage_level = (world_storages.length() > 0) ? STORAGE_LEVEL_UPGRADE_1 : STORAGE_LEVEL_BASE; } ensure_base_storage(); string[] pasturePositions = get_string_list_or_split(saveData, "pastures_positions"); for (uint i = 0; i < pasturePositions.length(); i++) { add_world_pasture(parse_int(pasturePositions[i])); } string[] stablePositions = get_string_list_or_split(saveData, "stables_positions"); for (uint i = 0; i < stablePositions.length(); i++) { add_world_stable(parse_int(stablePositions[i])); } string[] altarPositions = get_string_list_or_split(saveData, "altars_positions"); for (uint i = 0; i < altarPositions.length(); i++) { add_world_altar(parse_int(altarPositions[i])); } string[] streamData = get_string_list_or_split(saveData, "streams_data"); for (uint i = 0; i < streamData.length(); i++) { string[]@ parts = streamData[i].split("|"); if (parts.length() < 2) continue; int startPos = parse_int(parts[0]); int width = parse_int(parts[1]); if (width < 1) width = 1; add_world_stream(startPos, width); } string[] banditData = get_string_list_or_split(saveData, "bandits_data"); for (uint i = 0; i < banditData.length(); i++) { string[]@ parts = banditData[i].split("|"); if (parts.length() < 6) continue; int pos = parse_int(parts[0]); int health = parse_int(parts[1]); string weapon = parts[2]; string state = parts[3]; int wander_dir = parse_int(parts[4]); int move_int = parse_int(parts[5]); string invader_type = "bandit"; int home_start = pos; int home_end = pos; if (parts.length() >= 7) { invader_type = parts[6]; if (invader_type == "") invader_type = "bandit"; } if (parts.length() >= 9) { home_start = parse_int(parts[7]); home_end = parse_int(parts[8]); } else { if (expanded_area_start != -1 && pos >= expanded_area_start) { int area_start = -1; int area_end = -1; if (get_audio_area_bounds_for_position(pos, area_start, area_end)) { home_start = area_start; home_end = area_end; } } } // Create bandit with dummy expansion area (position will be overridden) Bandit@ b = Bandit(pos, home_start, home_end, invader_type); b.position = pos; b.health = health; b.weapon_type = weapon; b.behavior_state = state; b.wander_direction = wander_dir; b.move_interval = move_int; b.invader_type = invader_type; b.wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); b.wander_direction_timer.restart(); b.move_timer.restart(); b.attack_timer.restart(); // Restore alert sound based on invader type b.alert_sound = pick_invader_alert_sound(invader_type); bandits.insert_last(b); // Start looping sound for loaded bandit int[] areaStarts; int[] areaEnds; get_active_audio_areas(areaStarts, areaEnds); if (areaStarts.length() == 0 || range_overlaps_active_areas(b.position, b.position, areaStarts, areaEnds)) { b.sound_handle = play_1d_with_volume_step(b.alert_sound, x, b.position, true, BANDIT_SOUND_VOLUME_STEP); } } string[] mountainData = get_string_list_or_split(saveData, "mountains_data"); for (uint i = 0; i < mountainData.length(); i++) { string[]@ parts = mountainData[i].split("|"); if (parts.length() < 5) continue; int start_pos = parse_int(parts[0]); int end_pos = parse_int(parts[1]); int size = end_pos - start_pos + 1; // Create mountain with minimal init (we'll override everything) MountainRange@ mountain = MountainRange(start_pos, 1); mountain.start_position = start_pos; mountain.end_position = end_pos; // Parse elevations string[]@ elev_parts = parts[2].split(","); mountain.elevations.resize(elev_parts.length()); for (uint j = 0; j < elev_parts.length(); j++) { mountain.elevations[j] = parse_int(elev_parts[j]); } // Parse terrain types string[]@ terrain_parts = parts[3].split(","); mountain.terrain_types.resize(terrain_parts.length()); for (uint j = 0; j < terrain_parts.length(); j++) { mountain.terrain_types[j] = terrain_parts[j]; } // Parse stream positions if (parts[4].length() > 0) { string[]@ stream_parts = parts[4].split(","); for (uint j = 0; j < stream_parts.length(); j++) { mountain.stream_positions.insert_last(parse_int(stream_parts[j])); } } world_mountains.insert_last(mountain); } string[] dropData = get_string_list_or_split(saveData, "drops_data"); for (uint i = 0; i < dropData.length(); i++) { string[]@ parts = dropData[i].split("|"); if (parts.length() < 2) continue; add_world_drop(parse_int(parts[0]), parts[1]); } update_ambience(true); return true; } bool load_game_state() { if (current_save_file == "") { last_save_error = "No save selected."; return false; } return load_game_state_from_file(current_save_file); }