Multiple characters allowed. Starting base health lowered. Fylgjr system implemented.
This commit is contained in:
@@ -1,12 +1,35 @@
|
||||
// Save system
|
||||
|
||||
const string SAVE_FILE_PATH = "save.dat";
|
||||
const string SAVE_EXTENSION = ".dat";
|
||||
const string SAVE_ENCRYPTION_KEY = "draugnorak_save_v1";
|
||||
const int SAVE_VERSION = 2;
|
||||
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 file_exists(SAVE_FILE_PATH);
|
||||
return get_save_files().length() > 0;
|
||||
}
|
||||
|
||||
string encrypt_save_data(const string&in rawData) {
|
||||
@@ -93,6 +116,345 @@ string[] get_string_list(dictionary@ data, const string&in key) {
|
||||
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 character's sex.";
|
||||
speak_with_history(prompt + " " + options[selection], true);
|
||||
|
||||
while (true) {
|
||||
wait(5);
|
||||
if (key_pressed(KEY_DOWN)) {
|
||||
selection++;
|
||||
if (selection >= options.length()) selection = 0;
|
||||
speak_with_history(prompt + " " + options[selection], true);
|
||||
}
|
||||
if (key_pressed(KEY_UP)) {
|
||||
selection--;
|
||||
if (selection < 0) selection = options.length() - 1;
|
||||
speak_with_history(prompt + " " + options[selection], true);
|
||||
}
|
||||
if (key_pressed(KEY_RETURN)) {
|
||||
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) {
|
||||
string[] files = get_save_files();
|
||||
if (files.length() == 0) return false;
|
||||
|
||||
string[] options;
|
||||
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)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
int selection = 0;
|
||||
speak_with_history("Load game. Select character.", true);
|
||||
speak_with_history(options[selection], true);
|
||||
|
||||
while (true) {
|
||||
wait(5);
|
||||
if (key_pressed(KEY_DOWN)) {
|
||||
selection++;
|
||||
if (selection >= options.length()) selection = 0;
|
||||
speak_with_history(options[selection], true);
|
||||
}
|
||||
if (key_pressed(KEY_UP)) {
|
||||
selection--;
|
||||
if (selection < 0) selection = int(options.length()) - 1;
|
||||
speak_with_history(options[selection], true);
|
||||
}
|
||||
if (key_pressed(KEY_RETURN)) {
|
||||
filename = files[selection];
|
||||
return true;
|
||||
}
|
||||
if (key_pressed(KEY_ESCAPE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void stop_active_sounds() {
|
||||
if (day_sound_handle != -1) {
|
||||
p.destroy_sound(day_sound_handle);
|
||||
@@ -217,6 +579,7 @@ void reset_game_state() {
|
||||
incense_burning = false;
|
||||
blessing_speed_active = false;
|
||||
blessing_resident_active = false;
|
||||
reset_fylgja_state();
|
||||
|
||||
// Reset inventory using the registry system
|
||||
reset_inventory();
|
||||
@@ -283,6 +646,12 @@ void start_new_game() {
|
||||
init_barricade();
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -398,8 +767,11 @@ bool save_game_state() {
|
||||
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);
|
||||
|
||||
@@ -435,6 +807,11 @@ bool save_game_state() {
|
||||
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++) {
|
||||
@@ -585,20 +962,23 @@ bool save_game_state() {
|
||||
}
|
||||
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(SAVE_FILE_PATH, encryptedData);
|
||||
return save_data(current_save_file, encryptedData);
|
||||
}
|
||||
|
||||
bool load_game_state() {
|
||||
bool load_game_state_from_file(const string&in filename) {
|
||||
last_save_error = "";
|
||||
if (!file_exists(SAVE_FILE_PATH)) {
|
||||
if (!file_exists(filename)) {
|
||||
last_save_error = "No save file found.";
|
||||
return false;
|
||||
}
|
||||
|
||||
string encryptedData;
|
||||
if (!read_file_string(SAVE_FILE_PATH, encryptedData)) {
|
||||
if (!read_file_string(filename, encryptedData)) {
|
||||
last_save_error = "Unable to read save file.";
|
||||
return false;
|
||||
}
|
||||
@@ -623,6 +1003,7 @@ bool load_game_state() {
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -651,8 +1032,20 @@ bool load_game_state() {
|
||||
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;
|
||||
@@ -757,6 +1150,15 @@ bool load_game_state() {
|
||||
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();
|
||||
@@ -1085,3 +1487,11 @@ bool load_game_state() {
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user