// Save system const string SAVE_FILE_PATH = "save.dat"; const string SAVE_ENCRYPTION_KEY = "draugnorak_save_v1"; const int SAVE_VERSION = 1; string last_save_error = ""; bool has_save_game() { return file_exists(SAVE_FILE_PATH); } 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; } 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; } } 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; 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; 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; inv_stones = 0; inv_sticks = 0; inv_vines = 0; inv_reeds = 0; inv_logs = 0; inv_clay = 0; inv_small_game = 0; inv_small_game_types.resize(0); inv_meat = 0; inv_skins = 0; inv_feathers = 0; inv_down = 0; inv_incense = 0; inv_spears = 0; inv_snares = 0; inv_axes = 0; inv_knives = 0; inv_fishing_poles = 0; inv_slings = 0; inv_ropes = 0; inv_reed_baskets = 0; inv_clay_pots = 0; inv_skin_hats = 0; inv_skin_gloves = 0; inv_skin_pants = 0; inv_skin_tunics = 0; inv_moccasins = 0; inv_skin_pouches = 0; storage_stones = 0; storage_sticks = 0; storage_vines = 0; storage_reeds = 0; storage_logs = 0; storage_clay = 0; storage_small_game = 0; storage_small_game_types.resize(0); storage_meat = 0; storage_skins = 0; storage_feathers = 0; storage_down = 0; storage_incense = 0; storage_spears = 0; storage_snares = 0; storage_axes = 0; storage_knives = 0; storage_fishing_poles = 0; storage_slings = 0; storage_ropes = 0; storage_reed_baskets = 0; storage_clay_pots = 0; storage_skin_hats = 0; storage_skin_gloves = 0; storage_skin_pants = 0; storage_skin_tunics = 0; storage_moccasins = 0; storage_skin_pouches = 0; spear_equipped = false; axe_equipped = false; sling_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; 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; quest_roll_done_today = false; quest_queue.resize(0); 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(); fall_timer.restart(); climb_timer.restart(); } void start_new_game() { reset_game_state(); spawn_trees(5, 19); normalize_tree_positions(); init_barricade(); init_time(); init_weather(); 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); } 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; } 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; } int get_byte_at(const string&in data, int index) { string single = data.substr(index, 1); return character_to_ascii(single); } bool find_raw_key(const string&in rawData, const string&in key, int &out pos_after_key) { int key_len = key.length(); if (key_len <= 0 || key_len > 65535) return false; int low = key_len & 0xFF; int high = (key_len >> 8) & 0xFF; int limit = rawData.length() - key_len - 2; for (int i = 0; i <= limit; i++) { if (get_byte_at(rawData, i) != low) continue; if (get_byte_at(rawData, i + 1) != high) continue; if (rawData.substr(i + 2, key_len) == key) { pos_after_key = i + 2 + key_len; return true; } } return false; } bool get_raw_number(const string&in rawData, const string&in key, int &out value) { int pos; if (!find_raw_key(rawData, key, pos)) return false; if (pos >= rawData.length()) return false; int type = get_byte_at(rawData, pos); if (type != 2) return false; if (pos + 1 + 8 > rawData.length()) return false; double result = 0; double multiplier = 1; for (int i = 0; i < 8; i++) { result += get_byte_at(rawData, pos + 1 + i) * multiplier; multiplier *= 256; } value = int(result); return true; } bool get_raw_bool(const string&in rawData, const string&in key, bool &out value) { int pos; if (!find_raw_key(rawData, key, pos)) return false; if (pos >= rawData.length()) return false; int type = get_byte_at(rawData, pos); if (type != 1) return false; if (pos + 1 >= rawData.length()) return false; value = (get_byte_at(rawData, pos + 1) != 0); return true; } bool load_game_state_from_raw(const string&in rawData) { reset_game_state(); int value; bool bool_value; if (!get_raw_number(rawData, "player_x", value)) return false; x = value; if (!get_raw_number(rawData, "player_health", value)) return false; player_health = value; if (!get_raw_number(rawData, "time_current_day", value)) return false; current_day = value; if (get_raw_number(rawData, "player_y", value)) y = value; if (get_raw_number(rawData, "player_facing", value)) facing = value; if (get_raw_number(rawData, "player_base_health", value)) base_max_health = value; if (get_raw_number(rawData, "player_max_health", value)) max_health = value; if (get_raw_number(rawData, "player_favor", value)) favor = value; if (get_raw_number(rawData, "player_last_adventure_day", value)) last_adventure_day = value; if (get_raw_number(rawData, "incense_hours_remaining", value)) incense_hours_remaining = value; if (get_raw_bool(rawData, "incense_burning", bool_value)) incense_burning = bool_value; if (get_raw_number(rawData, "time_current_hour", value)) current_hour = value; if (get_raw_number(rawData, "world_map_size", value)) MAP_SIZE = value; if (get_raw_number(rawData, "world_expanded_area_start", value)) expanded_area_start = value; if (get_raw_number(rawData, "world_expanded_area_end", value)) expanded_area_end = value; if (get_raw_number(rawData, "world_barricade_health", value)) barricade_health = value; if (get_raw_number(rawData, "world_residents_count", value)) residents_count = value; if (get_raw_bool(rawData, "world_barricade_initialized", bool_value)) barricade_initialized = bool_value; if (get_raw_bool(rawData, "time_sun_setting_warned", bool_value)) sun_setting_warned = bool_value; if (get_raw_bool(rawData, "time_sunrise_warned", bool_value)) sunrise_warned = bool_value; if (get_raw_bool(rawData, "time_area_expanded_today", bool_value)) area_expanded_today = bool_value; if (get_raw_bool(rawData, "time_invasion_active", bool_value)) invasion_active = bool_value; if (get_raw_bool(rawData, "time_invasion_triggered_today", bool_value)) invasion_triggered_today = bool_value; if (get_raw_bool(rawData, "time_invasion_roll_done_today", bool_value)) invasion_roll_done_today = bool_value; if (get_raw_bool(rawData, "quest_roll_done_today", bool_value)) quest_roll_done_today = bool_value; if (get_raw_number(rawData, "time_invasion_start_hour", value)) invasion_start_hour = value; if (get_raw_number(rawData, "time_invasion_chance", value)) invasion_chance = value; if (get_raw_number(rawData, "time_invasion_scheduled_hour", value)) invasion_scheduled_hour = value; if (get_raw_number(rawData, "inventory_stones", value)) inv_stones = value; if (get_raw_number(rawData, "inventory_sticks", value)) inv_sticks = value; if (get_raw_number(rawData, "inventory_vines", value)) inv_vines = value; if (get_raw_number(rawData, "inventory_reeds", value)) inv_reeds = value; if (get_raw_number(rawData, "inventory_logs", value)) inv_logs = value; if (get_raw_number(rawData, "inventory_clay", value)) inv_clay = value; if (get_raw_number(rawData, "inventory_small_game", value)) inv_small_game = value; if (get_raw_number(rawData, "inventory_meat", value)) inv_meat = value; if (get_raw_number(rawData, "inventory_skins", value)) inv_skins = value; if (get_raw_number(rawData, "inventory_feathers", value)) inv_feathers = value; if (get_raw_number(rawData, "inventory_down", value)) inv_down = value; if (get_raw_number(rawData, "inventory_incense", value)) inv_incense = value; if (get_raw_number(rawData, "inventory_spears", value)) inv_spears = value; if (get_raw_number(rawData, "inventory_snares", value)) inv_snares = value; if (get_raw_number(rawData, "inventory_axes", value)) inv_axes = value; if (get_raw_number(rawData, "inventory_knives", value)) inv_knives = value; if (get_raw_number(rawData, "inventory_fishing_poles", value)) inv_fishing_poles = value; if (get_raw_number(rawData, "inventory_slings", value)) inv_slings = value; if (get_raw_number(rawData, "inventory_ropes", value)) inv_ropes = value; if (get_raw_number(rawData, "inventory_reed_baskets", value)) inv_reed_baskets = value; if (get_raw_number(rawData, "inventory_clay_pots", value)) inv_clay_pots = value; if (get_raw_number(rawData, "inventory_skin_hats", value)) inv_skin_hats = value; if (get_raw_number(rawData, "inventory_skin_gloves", value)) inv_skin_gloves = value; if (get_raw_number(rawData, "inventory_skin_pants", value)) inv_skin_pants = value; if (get_raw_number(rawData, "inventory_skin_tunics", value)) inv_skin_tunics = value; if (get_raw_number(rawData, "inventory_moccasins", value)) inv_moccasins = value; if (get_raw_number(rawData, "inventory_skin_pouches", value)) inv_skin_pouches = value; if (get_raw_bool(rawData, "equipment_spear_equipped", bool_value)) spear_equipped = bool_value; if (get_raw_bool(rawData, "equipment_axe_equipped", bool_value)) axe_equipped = bool_value; if (get_raw_bool(rawData, "equipment_sling_equipped", bool_value)) sling_equipped = bool_value; if (get_raw_number(rawData, "equipment_head", value)) equipped_head = value; if (get_raw_number(rawData, "equipment_torso", value)) equipped_torso = value; if (get_raw_number(rawData, "equipment_hands", value)) equipped_hands = value; if (get_raw_number(rawData, "equipment_legs", value)) equipped_legs = value; if (get_raw_number(rawData, "equipment_feet", value)) equipped_feet = value; if (get_raw_number(rawData, "equipment_arms", value)) equipped_arms = value; if (equipped_arms != EQUIP_POUCH && equipped_arms != EQUIP_BACKPACK) equipped_arms = EQUIP_NONE; if (equipped_arms == EQUIP_POUCH && inv_skin_pouches <= 0) equipped_arms = EQUIP_NONE; if (equipped_arms == EQUIP_BACKPACK && inv_backpacks <= 0) equipped_arms = EQUIP_NONE; if (incense_hours_remaining > 0) incense_burning = true; if (inv_small_game_types.length() == 0 && inv_small_game > 0) { for (int i = 0; i < inv_small_game; i++) { inv_small_game_types.insert_last("small game"); } } if (!barricade_initialized) { init_barricade(); } else { if (barricade_health < 0) barricade_health = 0; if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH; } spawn_trees(5, 19); is_daytime = (current_hour >= 6 && current_hour < 19); hour_timer.restart(); update_max_health_from_equipment(); return true; } 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_favor", favor); saveData.set("player_last_adventure_day", last_adventure_day); saveData.set("incense_hours_remaining", incense_hours_remaining); saveData.set("incense_burning", incense_burning); saveData.set("inventory_stones", inv_stones); saveData.set("inventory_sticks", inv_sticks); saveData.set("inventory_vines", inv_vines); saveData.set("inventory_reeds", inv_reeds); saveData.set("inventory_logs", inv_logs); saveData.set("inventory_clay", inv_clay); saveData.set("inventory_small_game", inv_small_game); saveData.set("inventory_meat", inv_meat); saveData.set("inventory_skins", inv_skins); saveData.set("inventory_feathers", inv_feathers); saveData.set("inventory_down", inv_down); saveData.set("inventory_incense", inv_incense); saveData.set("inventory_spears", inv_spears); saveData.set("inventory_snares", inv_snares); saveData.set("inventory_axes", inv_axes); saveData.set("inventory_knives", inv_knives); saveData.set("inventory_fishing_poles", inv_fishing_poles); saveData.set("inventory_slings", inv_slings); saveData.set("inventory_ropes", inv_ropes); saveData.set("inventory_reed_baskets", inv_reed_baskets); saveData.set("inventory_clay_pots", inv_clay_pots); saveData.set("inventory_skin_hats", inv_skin_hats); saveData.set("inventory_skin_gloves", inv_skin_gloves); saveData.set("inventory_skin_pants", inv_skin_pants); saveData.set("inventory_skin_tunics", inv_skin_tunics); saveData.set("inventory_moccasins", inv_moccasins); saveData.set("inventory_skin_pouches", inv_skin_pouches); saveData.set("inventory_backpacks", inv_backpacks); saveData.set("inventory_small_game_types", join_string_array(inv_small_game_types)); saveData.set("storage_stones", storage_stones); saveData.set("storage_sticks", storage_sticks); saveData.set("storage_vines", storage_vines); saveData.set("storage_reeds", storage_reeds); saveData.set("storage_logs", storage_logs); saveData.set("storage_clay", storage_clay); saveData.set("storage_small_game", storage_small_game); saveData.set("storage_meat", storage_meat); saveData.set("storage_skins", storage_skins); saveData.set("storage_feathers", storage_feathers); saveData.set("storage_down", storage_down); saveData.set("storage_incense", storage_incense); saveData.set("storage_spears", storage_spears); saveData.set("storage_snares", storage_snares); saveData.set("storage_axes", storage_axes); saveData.set("storage_knives", storage_knives); saveData.set("storage_fishing_poles", storage_fishing_poles); saveData.set("storage_slings", storage_slings); saveData.set("storage_ropes", storage_ropes); saveData.set("storage_reed_baskets", storage_reed_baskets); saveData.set("storage_clay_pots", storage_clay_pots); saveData.set("storage_skin_hats", storage_skin_hats); saveData.set("storage_skin_gloves", storage_skin_gloves); saveData.set("storage_skin_pants", storage_skin_pants); saveData.set("storage_skin_tunics", storage_skin_tunics); saveData.set("storage_moccasins", storage_moccasins); saveData.set("storage_skin_pouches", storage_skin_pouches); saveData.set("storage_backpacks", storage_backpacks); saveData.set("storage_small_game_types", join_string_array(storage_small_game_types)); saveData.set("equipment_spear_equipped", spear_equipped); saveData.set("equipment_axe_equipped", axe_equipped); saveData.set("equipment_sling_equipped", sling_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)); // Rune system data saveData.set("rune_swiftness_unlocked", rune_swiftness_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)); 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("quest_roll_done_today", quest_roll_done_today); 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_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)); string rawData = saveData.serialize(); string encryptedData = encrypt_save_data(rawData); return save_data(SAVE_FILE_PATH, encryptedData); } bool load_game_state() { last_save_error = ""; if (!file_exists(SAVE_FILE_PATH)) { last_save_error = "No save file found."; return false; } string encryptedData; if (!read_file_string(SAVE_FILE_PATH, 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)) { if (load_game_state_from_raw(rawData)) { return true; } 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(); 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; 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)); favor = get_number(saveData, "player_favor", 0.0); last_adventure_day = int(get_number(saveData, "player_last_adventure_day", -1)); 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; 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; inv_stones = int(get_number(saveData, "inventory_stones", 0)); inv_sticks = int(get_number(saveData, "inventory_sticks", 0)); inv_vines = int(get_number(saveData, "inventory_vines", 0)); inv_reeds = int(get_number(saveData, "inventory_reeds", 0)); inv_logs = int(get_number(saveData, "inventory_logs", 0)); inv_clay = int(get_number(saveData, "inventory_clay", 0)); inv_small_game = int(get_number(saveData, "inventory_small_game", 0)); inv_meat = int(get_number(saveData, "inventory_meat", 0)); inv_skins = int(get_number(saveData, "inventory_skins", 0)); inv_feathers = int(get_number(saveData, "inventory_feathers", 0)); inv_down = int(get_number(saveData, "inventory_down", 0)); inv_incense = int(get_number(saveData, "inventory_incense", 0)); inv_spears = int(get_number(saveData, "inventory_spears", 0)); inv_snares = int(get_number(saveData, "inventory_snares", 0)); inv_axes = int(get_number(saveData, "inventory_axes", 0)); inv_knives = int(get_number(saveData, "inventory_knives", 0)); inv_fishing_poles = int(get_number(saveData, "inventory_fishing_poles", 0)); inv_slings = int(get_number(saveData, "inventory_slings", 0)); inv_ropes = int(get_number(saveData, "inventory_ropes", 0)); inv_reed_baskets = int(get_number(saveData, "inventory_reed_baskets", 0)); inv_clay_pots = int(get_number(saveData, "inventory_clay_pots", 0)); inv_skin_hats = int(get_number(saveData, "inventory_skin_hats", 0)); inv_skin_gloves = int(get_number(saveData, "inventory_skin_gloves", 0)); inv_skin_pants = int(get_number(saveData, "inventory_skin_pants", 0)); inv_skin_tunics = int(get_number(saveData, "inventory_skin_tunics", 0)); inv_moccasins = int(get_number(saveData, "inventory_moccasins", 0)); inv_skin_pouches = int(get_number(saveData, "inventory_skin_pouches", 0)); inv_backpacks = int(get_number(saveData, "inventory_backpacks", 0)); string[] loadedSmallGameTypes = get_string_list_or_split(saveData, "inventory_small_game_types"); inv_small_game_types.resize(0); for (uint i = 0; i < loadedSmallGameTypes.length(); i++) { inv_small_game_types.insert_last(loadedSmallGameTypes[i]); } if (inv_small_game_types.length() == 0 && inv_small_game > 0) { for (int i = 0; i < inv_small_game; i++) { inv_small_game_types.insert_last("small game"); } } else { inv_small_game = inv_small_game_types.length(); } storage_stones = int(get_number(saveData, "storage_stones", 0)); storage_sticks = int(get_number(saveData, "storage_sticks", 0)); storage_vines = int(get_number(saveData, "storage_vines", 0)); storage_reeds = int(get_number(saveData, "storage_reeds", 0)); storage_logs = int(get_number(saveData, "storage_logs", 0)); storage_clay = int(get_number(saveData, "storage_clay", 0)); storage_small_game = int(get_number(saveData, "storage_small_game", 0)); storage_meat = int(get_number(saveData, "storage_meat", 0)); storage_skins = int(get_number(saveData, "storage_skins", 0)); storage_feathers = int(get_number(saveData, "storage_feathers", 0)); storage_down = int(get_number(saveData, "storage_down", 0)); storage_incense = int(get_number(saveData, "storage_incense", 0)); storage_spears = int(get_number(saveData, "storage_spears", 0)); storage_snares = int(get_number(saveData, "storage_snares", 0)); storage_axes = int(get_number(saveData, "storage_axes", 0)); storage_knives = int(get_number(saveData, "storage_knives", 0)); storage_fishing_poles = int(get_number(saveData, "storage_fishing_poles", 0)); storage_slings = int(get_number(saveData, "storage_slings", 0)); storage_ropes = int(get_number(saveData, "storage_ropes", 0)); storage_reed_baskets = int(get_number(saveData, "storage_reed_baskets", 0)); storage_clay_pots = int(get_number(saveData, "storage_clay_pots", 0)); storage_skin_hats = int(get_number(saveData, "storage_skin_hats", 0)); storage_skin_gloves = int(get_number(saveData, "storage_skin_gloves", 0)); storage_skin_pants = int(get_number(saveData, "storage_skin_pants", 0)); storage_skin_tunics = int(get_number(saveData, "storage_skin_tunics", 0)); storage_moccasins = int(get_number(saveData, "storage_moccasins", 0)); storage_skin_pouches = int(get_number(saveData, "storage_skin_pouches", 0)); storage_backpacks = int(get_number(saveData, "storage_backpacks", 0)); 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]); } if (storage_small_game_types.length() == 0 && storage_small_game > 0) { for (int i = 0; i < storage_small_game; i++) { storage_small_game_types.insert_last("small game"); } } else { storage_small_game = storage_small_game_types.length(); } 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); 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)); if (equipped_head != EQUIP_HAT) equipped_head = EQUIP_NONE; if (equipped_torso != EQUIP_TUNIC) equipped_torso = EQUIP_NONE; if (equipped_hands != EQUIP_GLOVES) equipped_hands = EQUIP_NONE; if (equipped_legs != EQUIP_PANTS) equipped_legs = EQUIP_NONE; if (equipped_feet != EQUIP_MOCCASINS) equipped_feet = EQUIP_NONE; if (equipped_arms != EQUIP_POUCH && equipped_arms != EQUIP_BACKPACK) equipped_arms = EQUIP_NONE; if (equipped_arms == EQUIP_POUCH && inv_skin_pouches <= 0) equipped_arms = EQUIP_NONE; if (equipped_arms == EQUIP_BACKPACK && inv_backpacks <= 0) equipped_arms = 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_BACKPACK) { quick_slots[i] = slot_value; } } update_max_health_from_equipment(); // Load rune system data rune_swiftness_unlocked = get_bool(saveData, "rune_swiftness_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); } } } 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)); 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; quest_roll_done_today = get_bool(saveData, "quest_roll_done_today", false); 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); snare.minute_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])); } 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]); // Create bandit with dummy expansion area (position will be overridden) Bandit@ b = Bandit(pos, pos, pos); 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.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.alert_timer.restart(); b.attack_timer.restart(); // Restore alert sound based on weapon type int sound_index = random(0, bandit_sounds.length() - 1); b.alert_sound = bandit_sounds[sound_index]; b.next_alert_delay = random(BANDIT_ALERT_MIN_DELAY, BANDIT_ALERT_MAX_DELAY); bandits.insert_last(b); } 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; }