From e24473ee17b7e6a1097b4beccc7c976722ec6a24 Mon Sep 17 00:00:00 2001 From: Zerocker Date: Mon, 18 May 2020 17:44:21 +0900 Subject: [PATCH 1/3] Support for text-based savefiles --- .gitignore | 1 + src/game/save_file.c | 297 ++++++++++++++++++++++++++++++++++++++++++- src/game/save_file.h | 8 ++ src/pc/ini.c | 265 ++++++++++++++++++++++++++++++++++++++ src/pc/ini.h | 25 ++++ 5 files changed, 589 insertions(+), 7 deletions(-) create mode 100644 src/pc/ini.c create mode 100644 src/pc/ini.h diff --git a/.gitignore b/.gitignore index 2f56cebf..0caa3e08 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ build/* *.mio0 *.z64 *.map +*.sav .assets-local.txt sm64_save_file.bin sm64config.txt diff --git a/src/game/save_file.c b/src/game/save_file.c index b5112eb5..698c6837 100644 --- a/src/game/save_file.c +++ b/src/game/save_file.c @@ -1,5 +1,5 @@ #include - +#include #include "sm64.h" #include "game_init.h" #include "main.h" @@ -11,6 +11,7 @@ #include "level_table.h" #include "course_table.h" #include "thread6.h" +#include "pc/ini.h" #define MENU_DATA_MAGIC 0x4849 #define SAVE_FILE_MAGIC 0x4441 @@ -50,6 +51,69 @@ s8 gLevelToCourseNumTable[] = { STATIC_ASSERT(ARRAY_COUNT(gLevelToCourseNumTable) == LEVEL_COUNT - 1, "change this array if you are adding levels"); +/* Flag key */ +const char *sav_flags[NUM_FLAGS] = { + "file_exists", "wing_cap", "metal_cap", "vanish_cap", "key_1", "key_2", + "basement_door", "upstairs_door", "ddd_moved_back", "moat_drained", + "pps_door", "wf_door", "ccm_door", "jrb_door", "bitdw_door", + "bitfs_door", "", "", "", "", "50star_door" +}; + +/* Main course keys */ +const char *sav_courses[NUM_COURSES] = { + "bob", "wf", "jrb", "ccm", "bbh", "hmc", "lll", + "ssl", "ddd", "sl", "wdw", "ttm", "thi", "ttc", "rr" +}; + +/* Bonus courses keys (including Castle Course) */ +const char *sav_bonus_courses[NUM_BONUS_COURSES] = { + "hub", "bitdw", "bitfs", "bits", "pss", "cotmc", + "totwc", "vcutm", "wmotr", "sa", +}; + +/* Mario's cap type keys */ +const char *cap_on_types[NUM_CAP_ON] = { + "ground", "klepto", "ukiki", "mrblizzard" +}; + +/* Get current timestamp */ +static void get_timestamp(char* buffer) +{ + time_t timer; + struct tm* tm_info; + + timer = time(NULL); + tm_info = localtime(&timer); + + strftime(buffer, 26, "%Y-%m-%d %H:%M:%S", tm_info); +} + +/* Convert 'binary' integer to decimal integer */ +static u32 bin_to_int(u32 n) +{ + s32 dec = 0, i = 0, rem; + while (n != 0) { + rem = n % 10; + n /= 10; + dec += rem * (1 << i); + ++i; + } + return dec; +} + +/* Convert decimal integer to 'binary' integer */ +static u32 int_to_bin(u32 n) +{ + s32 bin = 0, rem, i = 1; + while (n != 0) { + rem = n % 2; + n /= 2; + bin += rem * i; + i *= 10; + } + return bin; +} + // This was probably used to set progress to 100% for debugging, but // it was removed from the release ROM. static void stub_save_file_1(void) { @@ -178,6 +242,208 @@ static inline s32 write_eeprom_menudata(const u32 slot, const u32 num) { #endif } +/** + * Write SaveFile and MainMenuSaveData structs to a text-based savefile. + */ +static s32 write_text_save(s32 fileIndex) +{ + FILE* file; + char *filename, *value; + struct SaveFile *savedata; + struct MainMenuSaveData *menudata; + + u32 i, flags, coins, stars, starFlags; + + /* Define savefile's name */ + filename = (char*)malloc(14 * sizeof(char)); + if (sprintf(filename, FILENAME_FORMAT, fileIndex) < 0) + { + return -1; + } + + file = fopen(filename, "wt"); + if (file == NULL) { + return -1; + } + + /* Write header */ + fprintf(file, "# Super Mario 64 save file\n"); + fprintf(file, "# Comment starts with #\n"); + fprintf(file, "# True = 1, False = 0\n"); + + /* Write current timestamp */ + value = (char*)malloc(26 * sizeof(char)); + get_timestamp(value); + fprintf(file, "# %s\n", value); + + /* Write MainMenuSaveData info */ + menudata = &gSaveBuffer.menuData[0]; + fprintf(file, "\n[menu]\n"); + fprintf(file, "coin_score_age = %d\n", menudata->coinScoreAges[fileIndex]); + fprintf(file, "sound_mode = %u\n", menudata->soundMode); + + /* Write all flags */ + fprintf(file, "\n[flags]\n"); + for (i = 1; i < NUM_FLAGS; i++) { + if (strcmp(sav_flags[i], "")) { + flags = save_file_get_flags(); + flags = (flags & (1 << i)); /* Get a specific bit */ + flags = (flags) ? 1 : 0; /* Determine if bit is set or not */ + + fprintf(file, "%s = %d\n", sav_flags[i], flags); + } + } + + /* Write coin count and star flags from each course (except Castle Grounds) */ + fprintf(file, "\n[courses]\n"); + for (i = 0; i < NUM_COURSES; i++) { + stars = save_file_get_star_flags(fileIndex, i); + coins = save_file_get_course_coin_score(fileIndex, i); + starFlags = int_to_bin(stars); /* 63 -> 111111 */ + + fprintf(file, "%s = \"%d, %07d\"\n", sav_courses[i], coins, starFlags); + } + + /* Write star flags from each bonus cource (including Castle Grounds) */ + fprintf(file, "\n[bonus]\n"); + for (i = 0; i < NUM_BONUS_COURSES; i++) { + if (i == 0) { + stars = save_file_get_star_flags(fileIndex, -1); + } else { + stars = save_file_get_star_flags(fileIndex, i+15); + } + starFlags = int_to_bin(stars); + + fprintf(file, "%s = %d\n", sav_bonus_courses[i], starFlags); + } + + /* Write who steal Mario's cap */ + fprintf(file, "\n[cap]\n"); + for (i = 0; i < NUM_CAP_ON; i++) { + flags = save_file_get_flags(); // Read all flags + flags = (flags & (1 << (i+16))); // Get `cap` flags + flags = (flags) ? 1 : 0; // Determine if bit is set or not + if (flags) { + fprintf(file, "type = %s\n", cap_on_types[i]); + break; + } + } + + /* Write in what course and area Mario losted its cap, and cap's position */ + savedata = &gSaveBuffer.files[fileIndex][0]; + fprintf(file, "level = %d\n", savedata->capLevel); + fprintf(file, "area = %d\n", savedata->capArea); + + fclose(file); + return 1; +} + +/** + * Read gSaveBuffer data from a text-based savefile. + */ +static s32 read_text_save(s32 fileIndex) +{ + char *filename, *temp; + const char *value; + ini_t *savedata; + + u32 i, flag, coins, stars, starFlags; + u32 capLevel, capArea; + Vec3s capPos; + + /* Define savefile's name */ + filename = (char*)malloc(14 * sizeof(char)); + if (sprintf(filename, FILENAME_FORMAT, fileIndex) < 0) + { + return -1; + } + + /* Try to open the file */ + savedata = ini_load(filename); + if (savedata == NULL) { + return -1; + } + else { + /* Good, file exists for gSaveBuffer */ + gSaveBuffer.files[fileIndex][0].flags |= SAVE_FLAG_FILE_EXISTS; + } + + /* Read coin score age for selected file and sound mode */ + ini_sget(savedata, "menu", "coin_score_age", "%d", + &gSaveBuffer.menuData[0].coinScoreAges[fileIndex]); + ini_sget(savedata, "menu", "sound_mode", "%u", + &gSaveBuffer.menuData[0].soundMode); // Can override 4 times! + + /* Parse main flags */ + for (i = 1; i < NUM_FLAGS; i++) { + value = ini_get(savedata, "flags", sav_flags[i]); + + if (value) { + flag = strtol(value, &temp, 10); + if (flag) { + flag = 1 << i; /* Look #define in header.. */ + gSaveBuffer.files[fileIndex][0].flags |= flag; + } + } + } + + /* Parse coin and star values for each main course */ + for (i = 0; i < NUM_COURSES; i++) { + value = ini_get(savedata, "courses", sav_courses[i]); + if (value) { + sscanf(value, "%d, %d", &coins, &stars); + starFlags = bin_to_int(stars); /* 111111 -> 63 */ + + save_file_set_star_flags(fileIndex, i, starFlags); + gSaveBuffer.files[fileIndex][0].courseCoinScores[i] = coins; + } + } + + /* Parse star values for each bonus course */ + for (i = 0; i < NUM_BONUS_COURSES; i++) { + value = ini_get(savedata, "bonus", sav_bonus_courses[i]); + if (value) { + sscanf(value, "%d", &stars); + starFlags = bin_to_int(stars); + + if (strlen(value) == 5) { + /* Process Castle Grounds */ + save_file_set_star_flags(fileIndex, -1, starFlags); + } + else if (strlen(value) == 2) { + /* Process Princess's Secret Slide */ + save_file_set_star_flags(fileIndex, COURSE_PSS, starFlags); + } + else { + /* Process another shitty bonus course */ + save_file_set_star_flags(fileIndex, i+15, starFlags); + } + } + } + + /* Find, who steal Mario's cap ... */ + for (i = 0; i < NUM_CAP_ON; i++) { + value = ini_get(savedata, "cap", "type"); + if (value) { + if (!strcmp(value, cap_on_types[i])) { + flag = (1 << (16 + i)); + gSaveBuffer.files[fileIndex][0].flags |= flag; + break; + } + } + } + + /* ... also it's position, area and level */ + sscanf(ini_get(savedata, "cap", "level"), "%d", &capLevel); + sscanf(ini_get(savedata, "cap", "area"), "%d", &capArea); + + gSaveBuffer.files[fileIndex][0].capLevel = capLevel; + gSaveBuffer.files[fileIndex][0].capArea = capArea; + + /* Cleaning up after ourselves */ + ini_free(savedata); + return 1; +} /** * Sum the bytes in data to data + size - 2. The last two bytes are ignored @@ -242,7 +508,7 @@ static void save_main_menu_data(void) { bcopy(&gSaveBuffer.menuData[0], &gSaveBuffer.menuData[1], sizeof(gSaveBuffer.menuData[1])); // Write to EEPROM - write_eeprom_menudata(0, 2); + write_eeprom_menudata(0, 2); gMainMenuDataModified = FALSE; } @@ -350,7 +616,16 @@ static void save_file_bswap(struct SaveBuffer *buf) { } void save_file_do_save(s32 fileIndex) { - if (gSaveFileModified) { + if (gSaveFileModified) +#ifdef TEXTSAVES + { + // Write to text file + write_text_save(fileIndex); + gSaveFileModified = FALSE; + gMainMenuDataModified = FALSE; + } +#else + { // Compute checksum add_save_block_signature(&gSaveBuffer.files[fileIndex][0], sizeof(gSaveBuffer.files[fileIndex][0]), SAVE_FILE_MAGIC); @@ -361,11 +636,11 @@ void save_file_do_save(s32 fileIndex) { // Write to EEPROM write_eeprom_savefile(fileIndex, 0, 2); - + gSaveFileModified = FALSE; } - save_main_menu_data(); +#endif } void save_file_erase(s32 fileIndex) { @@ -396,6 +671,14 @@ void save_file_load_all(void) { gSaveFileModified = FALSE; bzero(&gSaveBuffer, sizeof(gSaveBuffer)); + +#ifdef TEXTSAVES + for (file = 0; file < NUM_SAVE_FILES; file++) { + read_text_save(file); + } + gSaveFileModified = TRUE; + gMainMenuDataModified = TRUE; +#else read_eeprom_data(&gSaveBuffer, sizeof(gSaveBuffer)); if (save_file_need_bswap(&gSaveBuffer)) @@ -432,7 +715,7 @@ void save_file_load_all(void) { break; } } - +#endif stub_save_file_1(); } @@ -739,4 +1022,4 @@ s32 check_warp_checkpoint(struct WarpNode *warpNode) { } return isWarpCheckpointActive; -} +} \ No newline at end of file diff --git a/src/game/save_file.h b/src/game/save_file.h index 198748de..373b732d 100644 --- a/src/game/save_file.h +++ b/src/game/save_file.h @@ -160,4 +160,12 @@ void eu_set_language(u16 language); u16 eu_get_language(void); #endif +#ifdef TEXTSAVES +#define FILENAME_FORMAT "save_file_%d.sav" +#define NUM_COURSES 15 +#define NUM_BONUS_COURSES 10 +#define NUM_FLAGS 21 +#define NUM_CAP_ON 4 +#endif + #endif diff --git a/src/pc/ini.c b/src/pc/ini.c new file mode 100644 index 00000000..71d6d408 --- /dev/null +++ b/src/pc/ini.c @@ -0,0 +1,265 @@ +/** + * Copyright (c) 2016 rxi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include +#include +#include "ini.h" + +/* Case insensitive string compare */ +static int strcmpci(const char *a, const char *b) { + for (;;) { + int d = tolower(*a) - tolower(*b); + if (d != 0 || !*a) { + return d; + } + a++, b++; + } +} + +/* Returns the next string in the split data */ +static char* next(ini_t *ini, char *p) { + p += strlen(p); + while (p < ini->end && *p == '\0') { + p++; + } + return p; +} + +static void trim_back(ini_t *ini, char *p) { + while (p >= ini->data && (*p == ' ' || *p == '\t' || *p == '\r')) { + *p-- = '\0'; + } +} + +static char* discard_line(ini_t *ini, char *p) { + while (p < ini->end && *p != '\n') { + *p++ = '\0'; + } + return p; +} + + +static char *unescape_quoted_value(ini_t *ini, char *p) { + /* Use `q` as write-head and `p` as read-head, `p` is always ahead of `q` + * as escape sequences are always larger than their resultant data */ + char *q = p; + p++; + while (p < ini->end && *p != '"' && *p != '\r' && *p != '\n') { + if (*p == '\\') { + /* Handle escaped char */ + p++; + switch (*p) { + default : *q = *p; break; + case 'r' : *q = '\r'; break; + case 'n' : *q = '\n'; break; + case 't' : *q = '\t'; break; + case '\r' : + case '\n' : + case '\0' : goto end; + } + + } else { + /* Handle normal char */ + *q = *p; + } + q++, p++; + } +end: + return q; +} + + +/* Splits data in place into strings containing section-headers, keys and + * values using one or more '\0' as a delimiter. Unescapes quoted values */ +static void split_data(ini_t *ini) { + char *value_start, *line_start; + char *p = ini->data; + + while (p < ini->end) { + switch (*p) { + case '\r': + case '\n': + case '\t': + case ' ': + *p = '\0'; + /* Fall through */ + + case '\0': + p++; + break; + + case '[': + p += strcspn(p, "]\n"); + *p = '\0'; + break; + + case ';': + p = discard_line(ini, p); + break; + + default: + line_start = p; + p += strcspn(p, "=\n"); + + /* Is line missing a '='? */ + if (*p != '=') { + p = discard_line(ini, line_start); + break; + } + trim_back(ini, p - 1); + + /* Replace '=' and whitespace after it with '\0' */ + do { + *p++ = '\0'; + } while (*p == ' ' || *p == '\r' || *p == '\t'); + + /* Is a value after '=' missing? */ + if (*p == '\n' || *p == '\0') { + p = discard_line(ini, line_start); + break; + } + + if (*p == '"') { + /* Handle quoted string value */ + value_start = p; + p = unescape_quoted_value(ini, p); + + /* Was the string empty? */ + if (p == value_start) { + p = discard_line(ini, line_start); + break; + } + + /* Discard the rest of the line after the string value */ + p = discard_line(ini, p); + + } else { + /* Handle normal value */ + p += strcspn(p, "\n"); + trim_back(ini, p - 1); + } + break; + } + } +} + + + +ini_t* ini_load(const char *filename) { + ini_t *ini = NULL; + FILE *fp = NULL; + int n, sz; + + /* Init ini struct */ + ini = malloc(sizeof(*ini)); + if (!ini) { + goto fail; + } + memset(ini, 0, sizeof(*ini)); + + /* Open file */ + fp = fopen(filename, "rb"); + if (!fp) { + goto fail; + } + + /* Get file size */ + fseek(fp, 0, SEEK_END); + sz = ftell(fp); + rewind(fp); + + /* Load file content into memory, null terminate, init end var */ + ini->data = malloc(sz + 1); + ini->data[sz] = '\0'; + ini->end = ini->data + sz; + n = fread(ini->data, 1, sz, fp); + if (n != sz) { + goto fail; + } + + /* Prepare data */ + split_data(ini); + + /* Clean up and return */ + fclose(fp); + return ini; + +fail: + if (fp) fclose(fp); + if (ini) ini_free(ini); + return NULL; +} + +void ini_free(ini_t *ini) { + free(ini->data); + free(ini); +} + +const char* ini_get(ini_t *ini, const char *section, const char *key) { + char *current_section = ""; + char *val; + char *p = ini->data; + + if (*p == '\0') { + p = next(ini, p); + } + + while (p < ini->end) { + if (*p == '[') { + /* Handle section */ + current_section = p + 1; + + } else { + /* Handle key */ + val = next(ini, p); + if (!section || !strcmpci(section, current_section)) { + if (!strcmpci(p, key)) { + return val; + } + } + p = val; + } + + p = next(ini, p); + } + + return NULL; +} + + +int ini_sget( + ini_t *ini, const char *section, const char *key, + const char *scanfmt, void *dst +) { + const char *val = ini_get(ini, section, key); + if (!val) { + return 0; + } + if (scanfmt) { + sscanf(val, scanfmt, dst); + } else { + *((const char**) dst) = val; + } + return 1; +} diff --git a/src/pc/ini.h b/src/pc/ini.h new file mode 100644 index 00000000..ca5b89bb --- /dev/null +++ b/src/pc/ini.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2016 rxi + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the MIT license. See `ini.c` for details. + */ + +#ifndef INI_H +#define INI_H + +#define INI_VERSION "0.1.1" + +typedef struct ini_t ini_t; + +struct ini_t { + char *data; + char *end; +}; + +ini_t* ini_load(const char *filename); +void ini_free(ini_t *ini); +const char* ini_get(ini_t *ini, const char *section, const char *key); +int ini_sget(ini_t *ini, const char *section, const char *key, const char *scanfmt, void *dst); + +#endif \ No newline at end of file From 2417004d204c1e2b0cc6f5bee163f630c9cc91a1 Mon Sep 17 00:00:00 2001 From: Zerocker Date: Mon, 18 May 2020 17:45:28 +0900 Subject: [PATCH 2/3] Added flag to support only text-based savefiles --- Makefile | 7 ++++ build.sh | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 build.sh diff --git a/Makefile b/Makefile index 6767bf89..105cfd29 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,8 @@ NODRAWINGDISTANCE ?= 0 TEXTURE_FIX ?= 0 # Enable extended options menu by default EXT_OPTIONS_MENU ?= 1 +# Disable text-based save-files by default +TEXTSAVES ?= 0 # Build for Emscripten/WebGL TARGET_WEB ?= 0 @@ -490,6 +492,11 @@ ifeq ($(BETTERCAMERA),1) EXT_OPTIONS_MENU := 1 endif +ifeq ($(TEXTSAVES),1) + CC_CHECK += -DTEXTSAVES + CFLAGS += -DTEXTSAVES +endif + # Check for no drawing distance option ifeq ($(NODRAWINGDISTANCE),1) CC_CHECK += -DNODRAWINGDISTANCE diff --git a/build.sh b/build.sh new file mode 100644 index 00000000..d5429ce5 --- /dev/null +++ b/build.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +# Directories and Files +LIBDIR=./tools/lib/ +LIBAFA=libaudiofile.a +LIBAFLA=libaudiofile.la +AUDDIR=./tools/audiofile-0.3.6 + +# Command line options +OPTIONS=("Analog Camera" "No Draw Distance" "Text-saves" "Smoke Texture Fix" "Clean build") +EXTRA=("BETTERCAMERA=1" "NODRAWINGDISTANCE=1" "TEXTSAVES=1" "TEXTURE_FIX=1" "clean") + +# Colors +RED=$(tput setaf 1) +GREEN=$(tput setaf 2) +YELLOW=$(tput setaf 3) +CYAN=$(tput setaf 6) +RESET=$(tput sgr0) + +# Checks to see if the libaudio directory and files exist +if [ -d "$LIBDIR" -a -e "${LIBDIR}$LIBAFA" -a -e "${LIBDIR}$LIBAFLA" ]; then + printf "\n${GREEN}libaudio files exist, going straight to compiling.${RESET}\n" +else + printf "\n${GREEN}libaudio files not found, starting initialization process.${RESET}\n\n" + + printf "${YELLOW} Changing directory to: ${CYAN}${AUDDIR}${RESET}\n\n" + cd $AUDDIR + + printf "${YELLOW} Executing: ${CYAN}autoreconf -i${RESET}\n\n" + autoreconf -i + + printf "\n${YELLOW} Executing: ${CYAN}./configure --disable-docs${RESET}\n\n" + PATH=/mingw64/bin:/mingw32/bin:$PATH LIBS=-lstdc++ ./configure --disable-docs + + printf "\n${YELLOW} Executing: ${CYAN}make -j${RESET}\n\n" + PATH=/mingw64/bin:/mingw32/bin:$PATH make -j + + printf "\n${YELLOW} Making new directory ${CYAN}../lib${RESET}\n\n" + mkdir ../lib + + + printf "${YELLOW} Copying libaudio files to ${CYAN}../lib${RESET}\n\n" + cp libaudiofile/.libs/libaudiofile.a ../lib/ + cp libaudiofile/.libs/libaudiofile.la ../lib/ + + printf "${YELLOW} Going up one directory.${RESET}\n\n" + cd ../ + + printf "${GREEN}Notepad will now open, please follow the instructions carefully.\n\n" + printf "${YELLOW}Locate the line: " + printf "${CYAN}tabledesign_CFLAGS := -Wno-uninitialized -laudiofile\n" + printf "${YELLOW}Then add at the end: ${CYAN}-lstdc++\n" + printf "${YELLOW}So it reads: " + printf "${CYAN}tabledesign_CFLAGS := -Wno-uninitialized -laudiofile -lstdc++\n\n" + notepad "Makefile" + read -n 1 -r -s -p $'\e[32mPRESS ENTER TO CONTINUE...\e[0m\n' + + printf "${YELLOW} Executing: ${CYAN}make -j${RESET}\n\n" + PATH=/mingw64/bin:/mingw32/bin:$PATH make -j + + printf "\n${YELLOW} Going up one directory.${RESET}\n" + cd ../ +fi + +menu() { + printf "\nAvaliable options:\n" + for i in ${!OPTIONS[@]}; do + printf "%3d%s) %s\n" $((i+1)) "${choices[i]:- }" "${OPTIONS[i]}" + done + if [[ "$msg" ]]; then echo "$msg"; fi + printf "${YELLOW}Please do not select \"Clean build\" with any other option.\n" + printf "Leave all options unchecked for a Vanilla build.\n${RESET}" +} + +prompt="Check an option (again to uncheck, press ENTER):" +while menu && read -rp "$prompt" num && [[ "$num" ]]; do + [[ "$num" != *[![:digit:]]* ]] && + (( num > 0 && num <= ${#OPTIONS[@]} )) || + { msg="Invalid option: $num"; continue; } + ((num--)); # msg="${OPTIONS[num]} was ${choices[num]:+un}checked" + [[ "${choices[num]}" ]] && choices[num]="" || choices[num]="+" +done + +for i in ${!OPTIONS[@]}; do + [[ "${choices[i]}" ]] && { CMDL+=" ${EXTRA[i]}"; } +done + +printf "\n${YELLOW} Executing: ${CYAN}make ${CMDL} -j${RESET}\n\n" +PATH=/mingw32/bin:/mingw64/bin:$PATH make $CMDL -j -d + +if [ "${CMDL}" != " clean" ]; then + + printf "\n${GREEN}If all went well you should have a compiled .EXE in the 'builds/us_pc/' folder.\n" + printf "${CYAN}Would you like to run the game? [y or n]: ${RESET}" + read TEST + + if [ "${TEST}" = "y" ]; then + exec ./build/us_pc/sm64.us.f3dex2e.exe + fi +else + printf "\nYour build is now clean\n" +fi \ No newline at end of file From bea8e2233e73a508b7666822feea453fe9a3b90f Mon Sep 17 00:00:00 2001 From: Zerocker Date: Mon, 18 May 2020 19:17:04 +0900 Subject: [PATCH 3/3] Added savefile format documentation --- SAVE_FORMAT.MD | 158 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 SAVE_FORMAT.MD diff --git a/SAVE_FORMAT.MD b/SAVE_FORMAT.MD new file mode 100644 index 00000000..c11d9194 --- /dev/null +++ b/SAVE_FORMAT.MD @@ -0,0 +1,158 @@ +# Text-based savefile format +This small document describes a new, TOML-like text format for game saves. This format allows the user to use a simple text editor to change the data necessary for the game (for example, special saves for speedrun training). + +All data is stored in pairs (*key = value*). Pairs can be placed arbitrarily within a single section. The format of values may differ for each section. + +Each savefile (4 total) must be named `save_file_X.sav`, where X - save slot (0, 1, 2 or 3). + +> **Note**: The game creates the savefile only when you are saved in the game after completing a course! +___ +## Header +The header is automatically generated by the game when saving progress. It mainly contains information about the value of the 0 and 1 flags, the character that the comment starts with, and the time when the file content changed. + +Example: +``` +# Super Mario 64 save file +# Comment starts with # +# True = 1, False = 0 +# 2020-05-18 17:37:07 +``` + +___ +## Commenting +All comment lines starts with `#` character. Comments can be located on a separate line or as part of another line. When the game reads the save, comments are ignored. + +Example: +``` +# This is a comment +coin_score_age = ?? # This is a comment too! +``` + +___ +## Menu Section - [menu] +This section contains two flags for menu. + +| Flag | Value | Description | +|---|---|---| +| coin_score_age | ?? | Each save file has a 2 bit "age" for each course. The higher this value, the older the high score is. This is used for tie-breaking when displaying on the high score screen +| sound_mode | ?? | Sound mode for the game: stereo, mono, or headset + +Example: +``` +[menu] +coin_score_age = ?? +sound_mode = ?? +``` +___ +## Flags Section - [flags] +This section contains all main game flags (walkthought milestones). +> **Note**: The value can be only 0 (False) or 1 (True). + +| Flag | Description | +|---|---| +| wing_cap | **Red Switch** is pressed +| metal_cap | **Green Switch** is pressed +| vanish_cap | **Blue Switch** is pressed. +| key_1 | Key is found in **Bowser in the Dark World** +| key_2 | Key is found in **Bowser in the Fire Sea** +| basement_door | Mario unlocked castle's basement door +| upstairs_door | Mario unlocked castle's upper floors +| ddd_moved_back | **Dire Dire Docks** painting is moved back +| moat_drained | Water is drained in the moat of the castle +| pps_door | **Princess's Secret Slide** window is unlocked +| wf_door | **Whomp's Fortress door** is unlocked +| ccm_door | **Cool, Cool Mountain door** is unlocked +| jrb_door | **Jolly Roger Bay door** is unlocked +| bitdw_door | **Bowser in the Dark World door** door is unlocked +| bitfs_door | **Bowser in the Fire Sea** door is unlocked +| 50star_door | **Endless Staircase** is not endless anymore + +Example: +``` +[flags] +wing_cap = 1 +metal_cap = 1 +vanish_cap = 0 +key_1 = 1 +key_2 = 1 +``` +___ +## Main Courses Section - [courses] +This section contains all stars and coins that Mario collected in each main course. + +The first value stores the number of coins collected. +> **Warning!**: Make sure that coins count will not exceed 255! + +The second value stores the stars (or completed missions). Each mission (6 main + 1 bonus) is must be marked `0` (not completed) and `1` (completed). +> **Warning!**: The sequence of stars' missions goes from **RIGHT** to **LEFT**! +> **Note**: Last star flag is **100 coins star** + +| Flag | Short for | +|---|---| +| bob | Bob-omb Battlefield | +| wf | Whomp's Fortress +| jrb | Jolly Roger Bay +| ccm | Cool, Cool Mountain +| bbh | Big Boo's Haunt +| hmc | Hazy Maze Cave +| lll | Lethal Lava Land +| ssl | Shifting Sand Land +| ddd | Dire, Dire Docks +| sl | Snowman's Land +| wdw | Wet-Dry World +| ttm | Tall, Tall Mountain +| thi | Tiny-Huge Island +| ttc | Tick Tock Clock +| rr | Rainbow Ride + +Example: +``` +[courses] +bob = "3, 0000011" +wf = "3, 0000101" +jrb = "0, 1000000" +ccm = "1, 1111111" +``` +___ +## Bonus Section - [bonus] +This section contains all bonus stars that Mario collected in the castle and all bonus courses. +> **Note**: The game takes into account only the number of bonus stars collected, the order of stars flags can be arbitrary + +| Flag | Stars | Description | +|---|---|---| +| hub | 5 | MIPS' stars and Toads' stars +| bitdw | 1 | Bowser in the Dark World +| bitfs | 1 | Bowser in the Fire Sea +| bits | 1 | Bowser in the Sky +| pss | 2 | The Princess's Secret Slide +| cotmc | 1 | Cavern of the Metal Cap +| totwc | 1 | Tower of the Wing Cap +| vcutm | 1 | Vanish Cap Under the Moat +| wmotr | 1 | Wing Mario Over the Rainbow +| sa | 1 | The Secret Aquarium + +Example: +``` +[bonus] +hub = 11101 +bitdw = 1 +bitfs = 0 +bits = 1 +pss = 10 +``` +___ +## Cap Section - [cap] +This section contains information about where Mario lost his cap and who take it. +| Flag | Value | Description | +|---|---|---| +| type | ground, klepto, ukiki, mrblizzard | The one who or what took the cap from Mario. +| level | ?? | Specifies the course where the cap is located. +| area | ?? | Specifies the area in the course. + +Example: +``` +[cap] +type = onground +level = 5 +area = 0 +``` \ No newline at end of file