diff --git a/common/main/mission.h b/common/main/mission.h index 0d8a0d468..000cf3bef 100644 --- a/common/main/mission.h +++ b/common/main/mission.h @@ -79,6 +79,8 @@ constexpr std::integral_constant MAX_SECRET_LEVELS_PER_MISSION{}; //where the missions go #define MISSION_DIR "missions/" +constexpr std::integral_constant DXX_MAX_MISSION_PATH_LENGTH{}; + /* Path and filename must be kept in sync. */ class Mission_path { @@ -116,6 +118,11 @@ public: enum class descent_version_type : uint8_t { #if defined(DXX_BUILD_DESCENT_II) + /* These values are written to the binary savegame as part of + * the mission name. If the values are reordered or renumbered, + * old savegames will be unable to find the matching mission + * file. + */ descent2a, // !name descent2z, // zname descent2x, // xname @@ -217,6 +224,17 @@ enum class mission_filter_mode #ifdef dsx +namespace dcx { + +enum class mission_name_type +{ + basename, + pathname, + guess, +}; + +} + namespace dsx { #if defined(DXX_BUILD_DESCENT_II) @@ -228,9 +246,29 @@ int load_mission_ham(); void bm_read_extra_robots(const char *fname, Mission::descent_version_type type); #endif +struct mission_entry_predicate +{ + /* May be a basename or may be a path relative to the root of the + * PHYSFS virtual filesystem, depending on what the caller provides. + * + * In both cases, the file extension is omitted. + */ + const char *filesystem_name; +#if defined(DXX_BUILD_DESCENT_II) + bool check_version; + Mission::descent_version_type descent_version; +#endif + mission_entry_predicate with_filesystem_name(const char *fsname) const + { + mission_entry_predicate m = *this; + m.filesystem_name = fsname; + return m; + } +}; + //loads the named mission if it exists. //Returns nullptr if mission loaded ok, else error string. -const char *load_mission_by_name (const char *mission_name); +const char *load_mission_by_name (mission_entry_predicate mission_name, mission_name_type); //Handles creating and selecting from the mission list. //Returns 1 if a mission was loaded. diff --git a/similar/main/mission.cpp b/similar/main/mission.cpp index 158a06ae5..b45718ecc 100644 --- a/similar/main/mission.cpp +++ b/similar/main/mission.cpp @@ -72,10 +72,19 @@ using std::min; #define MISSION_EXTENSION_DESCENT_II ".mn2" #endif +#define CON_PRIORITY_DEBUG_MISSION_LOAD CON_DEBUG + +namespace { + +using mission_candidate_search_path = array; + +} + +namespace dsx { + namespace { struct mle; -using mission_candidate_search_path = array; using mission_list_type = std::vector; //mission list entry @@ -171,6 +180,67 @@ mle::mle(const char *const name, std::vector &&d) : snprintf(mission_name.data(), mission_name.size(), "%s/ [%sMSN:L%zu;T%zu]", name, prepare_mission_list_count_dirbuf(dirbuf, ss.immediate_directories), ss.immediate_missions, ss.total_missions); } +static const mle *compare_mission_predicate_to_leaf(const mission_entry_predicate mission_predicate, const mle &candidate, const char *candidate_filesystem_name) +{ +#if defined(DXX_BUILD_DESCENT_II) + if (mission_predicate.check_version && mission_predicate.descent_version != candidate.descent_version) + { + con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "mission version check requires %u, but found %u; skipping string comparison for mission \"%s\""), static_cast(mission_predicate.descent_version), static_cast(candidate.descent_version), candidate.path.data()); + return nullptr; + } +#endif + if (!d_stricmp(mission_predicate.filesystem_name, candidate_filesystem_name)) + { + con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "found mission \"%s\"[\"%s\"] at %p"), candidate.path.data(), &*candidate.filename, &candidate); + return &candidate; + } + con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "want mission \"%s\", no match for mission \"%s\"[\"%s\"] at %p"), mission_predicate.filesystem_name, candidate.path.data(), &*candidate.filename, &candidate); + return nullptr; +} + +static const mle *compare_mission_by_guess(const mission_entry_predicate mission_predicate, const mle &candidate) +{ + if (candidate.directory.empty()) + return compare_mission_predicate_to_leaf(mission_predicate, candidate, &*candidate.filename); + { + const unsigned long size = candidate.directory.size(); + con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "want mission \"%s\", check %lu missions under \"%s\""), mission_predicate.filesystem_name, size, candidate.path.data()); + } + range_for (auto &i, candidate.directory) + { + if (const auto r = compare_mission_by_guess(mission_predicate, i)) + return r; + } + con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "no matches under \"%s\""), candidate.path.data()); + return nullptr; +} + +static const mle *compare_mission_by_pathname(const mission_entry_predicate mission_predicate, const mle &candidate) +{ + if (candidate.directory.empty()) + return compare_mission_predicate_to_leaf(mission_predicate, candidate, candidate.path.data()); + const auto mission_name = mission_predicate.filesystem_name; + const auto path_length = candidate.path.size(); + if (!strncmp(mission_name, candidate.path.data(), path_length) && mission_name[path_length] == '/') + { + { + const unsigned long size = candidate.directory.size(); + con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "want mission pathname \"%s\", check %lu missions under \"%s\""), mission_predicate.filesystem_name, size, candidate.path.data()); + } + range_for (auto &i, candidate.directory) + { + if (const auto r = compare_mission_by_pathname(mission_predicate, i)) + return r; + } + con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "no matches under \"%s\""), candidate.path.data()); + } + else + con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "want mission pathname \"%s\", ignore non-matching directory \"%s\""), mission_predicate.filesystem_name, candidate.path.data()); + return nullptr; +} + +} + } Mission_ptr Current_mission; // currently loaded mission @@ -496,6 +566,8 @@ static int read_mission_file(mission_list_type &mission_list, mission_candidate_ const auto idx_file_extension = str_pathname.find_first_of('.', idx_filename); if (idx_file_extension == str_pathname.npos) return 0; //missing extension + if (idx_file_extension >= DXX_MAX_MISSION_PATH_LENGTH) + return 0; // path too long, would be truncated in save game files str_pathname.resize(idx_file_extension); mission_list.emplace_back(Mission_path(std::move(str_pathname), idx_filename)); mle *mission = &mission_list.back(); @@ -1133,12 +1205,43 @@ static const char *load_mission(const mle *const mission) //loads the named mission if exists. //Returns nullptr if mission loaded ok, else error string. -const char *load_mission_by_name(const char *const mission_name) +const char *load_mission_by_name (const mission_entry_predicate mission_name, const mission_name_type name_match_mode) { auto &&mission_list = build_mission_list(mission_filter_mode::include_anarchy); - range_for (auto &i, mission_list) - if (!d_stricmp(mission_name, &*i.filename)) - return load_mission(&i); + { + range_for (auto &i, mission_list) + { + switch (name_match_mode) + { + case mission_name_type::basename: + if (!d_stricmp(mission_name.filesystem_name, &*i.filename)) + return load_mission(&i); + continue; + case mission_name_type::pathname: + case mission_name_type::guess: + if (const auto r = compare_mission_by_pathname(mission_name, i)) + return load_mission(r); + continue; + default: + return "Unhandled load mission type"; + } + } + } + if (name_match_mode == mission_name_type::guess) + { + const auto p = strrchr(mission_name.filesystem_name, '/'); + const auto &guess_predicate = p + ? mission_name.with_filesystem_name(p + 1) + : mission_name; + range_for (auto &i, mission_list) + { + if (const auto r = compare_mission_by_guess(guess_predicate, i)) + { + con_printf(CON_NORMAL, "%s:%u: request for guessed mission name \"%s\" found \"%s\"", __FILE__, __LINE__, mission_name.filesystem_name, r->path.c_str()); + return load_mission(r); + } + } + } return "No matching mission found in\ninstalled mission list."; } diff --git a/similar/main/net_udp.cpp b/similar/main/net_udp.cpp index b942f4bf0..a0a6ac7c1 100644 --- a/similar/main/net_udp.cpp +++ b/similar/main/net_udp.cpp @@ -4672,11 +4672,22 @@ int net_udp_do_join_game() } // Check for valid mission name - if (const auto errstr = load_mission_by_name(Netgame.mission_name)) + { + mission_entry_predicate mission_predicate; + mission_predicate.filesystem_name = Netgame.mission_name; +#if defined(DXX_BUILD_DESCENT_II) + /* FIXME: This should be set to true and the version set + * accordingly. However, currently the host does not provide + * the mission version to the guests. + */ + mission_predicate.check_version = false; +#endif + if (const auto errstr = load_mission_by_name(mission_predicate, mission_name_type::guess)) { nm_messagebox(nullptr, 1, TXT_OK, "%s\n\n%s", TXT_MISSION_NOT_FOUND, errstr); return 0; } + } #if defined(DXX_BUILD_DESCENT_II) if (is_D2_OEM) diff --git a/similar/main/newdemo.cpp b/similar/main/newdemo.cpp index c71c09d0e..a12c10aaa 100644 --- a/similar/main/newdemo.cpp +++ b/similar/main/newdemo.cpp @@ -1890,7 +1890,7 @@ static int newdemo_read_demo_start(enum purpose_type purpose) #if defined(DXX_BUILD_DESCENT_I) if (!shareware) { - if ((purpose != PURPOSE_REWRITE) && load_mission_by_name(current_mission)) + if ((purpose != PURPOSE_REWRITE) && load_mission_by_name(mission_entry_predicate{current_mission}, mission_name_type::guess)) { if (purpose == PURPOSE_CHOSE_PLAY) { nm_messagebox( NULL, 1, TXT_OK, TXT_NOMISSION4DEMO, current_mission ); @@ -1899,12 +1899,17 @@ static int newdemo_read_demo_start(enum purpose_type purpose) } } #elif defined(DXX_BUILD_DESCENT_II) - if (load_mission_by_name(current_mission)) { + mission_entry_predicate mission_predicate; + mission_predicate.filesystem_name = current_mission; + mission_predicate.check_version = false; + if (load_mission_by_name(mission_predicate, mission_name_type::guess)) + { if (purpose != PURPOSE_RANDOM_PLAY) { nm_messagebox( NULL, 1, TXT_OK, TXT_NOMISSION4DEMO, current_mission ); } return 1; + } } #endif diff --git a/similar/main/state.cpp b/similar/main/state.cpp index 129baf93a..383da31ac 100644 --- a/similar/main/state.cpp +++ b/similar/main/state.cpp @@ -136,6 +136,19 @@ struct relocated_player_data uint8_t hostages_level; }; +struct savegame_mission_path { + array original; + array full; +}; + +enum class savegame_mission_name_abi : uint8_t +{ + original, + pathname, +}; + +static_assert(sizeof(savegame_mission_path) == sizeof(savegame_mission_path::original) + sizeof(savegame_mission_path::full), "padding error"); + } // Following functions convert object to object_rw and back to be written to/read from Savegames. Mostly object differs to object_rw in terms of timer values (fix/fix64). as we reset GameTime64 for writing so it can fit into fix it's not necessary to increment savegame version. But if we once store something else into object which might be useful after restoring, it might be handy to increment Savegame version and actually store these new infos. @@ -972,7 +985,6 @@ int state_save_all_sub(const char *filename, const char *desc) auto &vmobjptr = Objects.vmptr; auto &RobotCenters = LevelSharedRobotcenterState.RobotCenters; auto &Station = LevelUniqueFuelcenterState.Station; - char mission_filename[9]; fix tmptime32 = 0; #ifndef NDEBUG @@ -1050,9 +1062,15 @@ int state_save_all_sub(const char *filename, const char *desc) } // Save the mission info... - memset(&mission_filename, '\0', 9); - snprintf(mission_filename, 9, "%s", &*Current_mission->filename); // Current_mission_filename is not necessarily 9 bytes long so for saving we use a proper string - preventing corruptions - PHYSFS_write(fp, &mission_filename, 9 * sizeof(char), 1); + savegame_mission_path mission_pathname{}; +#if defined(DXX_BUILD_DESCENT_II) + mission_pathname.original[1] = static_cast(Current_mission->descent_version); +#endif + mission_pathname.original.back() = static_cast(savegame_mission_name_abi::pathname); + auto Current_mission_pathname = Current_mission->path.c_str(); + // Current_mission_filename is not necessarily 9 bytes long so for saving we use a proper string - preventing corruptions + snprintf(mission_pathname.full.data(), mission_pathname.full.size(), "%s", Current_mission_pathname); + PHYSFS_write(fp, &mission_pathname, sizeof(mission_pathname), 1); //Save level info PHYSFS_write(fp, &Current_level_num, sizeof(int), 1); @@ -1491,7 +1509,6 @@ int state_restore_all_sub(const d_level_shared_destructible_light_state &LevelSh int version, coop_player_got[MAX_PLAYERS], coop_org_objnum = get_local_player().objnum; int swap = 0; // if file is not endian native, have to swap all shorts and ints int current_level; - char mission[16]; char id[5]; fix tmptime32 = 0; array, MAX_SEGMENTS> TempTmapNum, TempTmapNum2; @@ -1557,11 +1574,43 @@ int state_restore_all_sub(const d_level_shared_destructible_light_state &LevelSh PHYSFSX_readSXE32(fp, swap); // Read the mission info... - PHYSFS_read(fp, mission, sizeof(char) * 9, 1); - - if (const auto errstr = load_mission_by_name(mission)) + savegame_mission_path mission_pathname{}; + PHYSFS_read(fp, mission_pathname.original.data(), mission_pathname.original.size(), 1); + mission_name_type name_match_mode; + mission_entry_predicate mission_predicate; + switch (static_cast(mission_pathname.original.back())) { - nm_messagebox(nullptr, 1, TXT_OK, "Error!\nUnable to load mission\n'%s'\n\n%s", mission, errstr); + case savegame_mission_name_abi::original: /* Save game without the ability to do extended mission names */ + name_match_mode = mission_name_type::basename; + mission_predicate.filesystem_name = mission_pathname.original.data(); +#if defined(DXX_BUILD_DESCENT_II) + mission_predicate.check_version = false; +#endif + break; + case savegame_mission_name_abi::pathname: /* Save game with extended mission name */ + { + PHYSFS_read(fp, mission_pathname.full.data(), mission_pathname.full.size(), 1); + if (mission_pathname.full.back()) + { + nm_messagebox("ERROR", 1, TXT_OK, "Unable to load game\nUnrecognized mission name format"); + return 0; + } + } + name_match_mode = mission_name_type::pathname; + mission_predicate.filesystem_name = mission_pathname.full.data(); +#if defined(DXX_BUILD_DESCENT_II) + mission_predicate.check_version = true; + mission_predicate.descent_version = static_cast(mission_pathname.original[1]); +#endif + break; + default: /* Save game written by a future version of Rebirth. ABI unknown. */ + nm_messagebox("ERROR", 1, TXT_OK, "Unable to load game\nUnrecognized save game format"); + return 0; + } + + if (const auto errstr = load_mission_by_name(mission_predicate, name_match_mode)) + { + nm_messagebox("ERROR", 1, TXT_OK, "Unable to load mission\n'%s'\n\n%s", mission_pathname.full.data(), errstr); return 0; }