Fix saving/loading games for missions in subdirectories

The historical savegame format cannot support finding a mission in a
subdirectory.  Add a backwards-incompatible modification to store the
full path in the savegame, and store it in a way that old versions will
fail gracefully.[1]  When loading demos, or legacy savegames, search for
the mission in all available directories.  Demos are still written with
an unqualified path because the demo loading code would crash if given
an oversized path.  Mission names sent over the network as part of
multiplayer use the guess logic now, so that guests do not need to have
the mission in the same path as the host.

[1] Versions affected by issue #486 may fail ungracefully.

Reported-by: AlumiuN <https://github.com/dxx-rebirth/dxx-rebirth/issues/491>
This commit is contained in:
Kp 2020-01-18 21:57:39 +00:00
parent 3e2d47f879
commit 1a2cfa35ba
5 changed files with 224 additions and 18 deletions

View file

@ -79,6 +79,8 @@ constexpr std::integral_constant<uint8_t, 127> MAX_SECRET_LEVELS_PER_MISSION{};
//where the missions go
#define MISSION_DIR "missions/"
constexpr std::integral_constant<std::size_t, 128> 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.

View file

@ -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<char, PATH_MAX>;
}
namespace dsx {
namespace {
struct mle;
using mission_candidate_search_path = array<char, PATH_MAX>;
using mission_list_type = std::vector<mle>;
//mission list entry
@ -171,6 +180,67 @@ mle::mle(const char *const name, std::vector<mle> &&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<unsigned>(mission_predicate.descent_version), static_cast<unsigned>(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.";
}

View file

@ -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)

View file

@ -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

View file

@ -136,6 +136,19 @@ struct relocated_player_data
uint8_t hostages_level;
};
struct savegame_mission_path {
array<char, 9> original;
array<char, DXX_MAX_MISSION_PATH_LENGTH> 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<uint8_t>(Current_mission->descent_version);
#endif
mission_pathname.original.back() = static_cast<uint8_t>(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<array<short, MAX_SIDES_PER_SEGMENT>, 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<savegame_mission_name_abi>(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::descent_version_type>(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;
}