Files
Andrea 6e2cf973c4 Update MPC bridge handling from standalone Supermodel
Port upstream standalone commit fef2b38 so Step 1.x exposes only MPC105 and Step 2.x only MPC106. Keep bridge selection tied to hardware stepping even when the Libretro CPU-frequency override is active, and remove obsolete per-game PCI overrides.
2026-08-28 08:16:16 +03:00

1031 lines
37 KiB
C++

#include "GameLoader.h"
#include "OSD/Logger.h"
#include "Util/NewConfig.h"
#include "Util/ConfigBuilders.h"
#include "Util/ByteSwap.h"
#include "Util/Format.h"
#include <algorithm>
#include <cctype>
#include <cstring>
#include <iostream>
static const zlib_filefunc64_def *s_zip_filefunc = nullptr;
void GameLoader::SetZipFilefunc(const zlib_filefunc64_def *filefunc)
{
s_zip_filefunc = filefunc;
}
bool GameLoader::LoadZipArchive(ZipArchive *zip, const std::string &zipfilename) const
{
unzFile zf = s_zip_filefunc
? unzOpen2_64(zipfilename.c_str(), const_cast<zlib_filefunc64_def *>(s_zip_filefunc))
: unzOpen(zipfilename.c_str());
if (NULL == zf)
{
ErrorLog("Could not open '%s'.", zipfilename.c_str());
return true;
}
zip->zipfilenames.push_back(zipfilename);
zip->zfs.push_back(zf);
// Identify all files in zip archive
int err;
for (err = unzGoToFirstFile(zf); err == UNZ_OK; err = unzGoToNextFile(zf))
{
unz_file_info file_info;
char filename_buffer[256];
if (UNZ_OK != unzGetCurrentFileInfo(zf, &file_info, filename_buffer, sizeof(filename_buffer), NULL, 0, NULL, 0))
continue;
zip->files_by_crc[file_info.crc].zf = zf;
zip->files_by_crc[file_info.crc].zipfilename = filename_buffer;
zip->files_by_crc[file_info.crc].filename = filename_buffer;
zip->files_by_crc[file_info.crc].uncompressed_size = file_info.uncompressed_size;
zip->files_by_crc[file_info.crc].crc32 = file_info.crc;
}
if (err != UNZ_END_OF_LIST_OF_FILE)
{
ErrorLog("Unable to read the contents of '%s' (code 0x%x).", zipfilename.c_str(), err);
return true;
}
InfoLog("Opened %s.", zipfilename.c_str());
return false;
}
bool GameLoader::FileExistsInZipArchive(const File::ptr_t &file, const ZipArchive &zip) const
{
if (file->has_crc32)
{
auto it = zip.files_by_crc.find(file->crc32);
return it != zip.files_by_crc.end();
}
// Try to lookup by name
for (auto &v: zip.files_by_crc)
{
if (Util::ToLower(v.second.filename) == file->filename)
return true;
}
return false;
}
const GameLoader::ZippedFile *GameLoader::LookupFile(const File::ptr_t &file, const ZipArchive &zip) const
{
if (file->has_crc32)
{
auto it = zip.files_by_crc.find(file->crc32);
if (it == zip.files_by_crc.end())
{
if (zip.zfs.size() == 1)
ErrorLog("'%s' with CRC32 0x%08x not found in '%s'.", file->filename.c_str(), file->crc32, zip.zipfilenames[0].c_str());
else
ErrorLog("'%s' with CRC32 0x%08x not found in '%s'.", file->filename.c_str(), file->crc32, Util::Format("', '").Join(zip.zipfilenames).str().c_str());
return nullptr;
}
return &it->second;
}
// Try to lookup by name
for (auto &v: zip.files_by_crc)
{
if (Util::ToLower(v.second.filename) == file->filename)
return &v.second;
}
if (zip.zfs.size() == 1)
ErrorLog("'%s' not found in '%s'.", file->filename.c_str(), zip.zipfilenames[0].c_str());
else
ErrorLog("'%s' not found in '%s'.", file->filename.c_str(), Util::Format("', '").Join(zip.zipfilenames).str().c_str());
return nullptr;
}
bool GameLoader::LoadZippedFile(std::shared_ptr<uint8_t> *buffer, size_t *file_size, const GameLoader::File::ptr_t &file, const ZipArchive &zip) const
{
// Locate file
const ZippedFile *zipped_file = LookupFile(file, zip);
if (!zipped_file)
return true;
if (UNZ_OK != unzLocateFile(zipped_file->zf, zipped_file->filename.c_str(), 2))
{
ErrorLog("Unable to locate '%s' in '%s'. Is zip file corrupt?", zipped_file->filename.c_str(), zipped_file->zipfilename.c_str());
return true;
}
// Read it in
if (UNZ_OK != unzOpenCurrentFile(zipped_file->zf))
{
ErrorLog("Unable to read '%s' from '%s'. Is zip file corrupt?", zipped_file->filename.c_str(), zipped_file->zipfilename.c_str());
return true;
}
*file_size = zipped_file->uncompressed_size;
buffer->reset(new uint8_t[*file_size], std::default_delete<uint8_t[]>());
size_t bytes_read = (size_t) unzReadCurrentFile(zipped_file->zf, buffer->get(), *file_size);
if (bytes_read != *file_size)
{
ErrorLog("Unable to read '%s' from '%s'. Is zip file corrupt?", zipped_file->filename.c_str(), zipped_file->zipfilename.c_str());
unzCloseCurrentFile(zipped_file->zf);
return true;
}
// And close it
if (UNZ_CRCERROR == unzCloseCurrentFile(zipped_file->zf))
ErrorLog("CRC error reading '%s' from '%s'. File may be corrupt.", zipped_file->filename.c_str(), zipped_file->zipfilename.c_str());
return false;
}
bool GameLoader::MissingAttrib(const GameLoader &loader, const Util::Config::Node &node, const std::string &attribute)
{
if (node[attribute].Empty())
{
ErrorLog("%s: <%s> tag is missing required attribute '%s'.", loader.m_xml_filename.c_str(), node.Key().c_str(), attribute.c_str());
return true;
}
return false;
}
GameLoader::File::ptr_t GameLoader::File::Create(const GameLoader &loader, const Util::Config::Node &file_node)
{
if (GameLoader::MissingAttrib(loader, file_node, "name") | GameLoader::MissingAttrib(loader, file_node, "offset")) // no || to easier detect errors
return ptr_t();
ptr_t file = std::make_shared<File>();
file->offset = file_node["offset"].ValueAs<uint32_t>();
file->filename = Util::ToLower(file_node["name"].ValueAs<std::string>());
file->has_crc32 = file_node["crc32"].Exists();
file->crc32 = file->has_crc32 ? file_node["crc32"].ValueAs<uint32_t>() : 0;
return file;
}
bool GameLoader::File::Matches(const std::string &filename_to_match, uint32_t crc32_to_match) const
{
if (has_crc32)
return crc32_to_match == crc32;
return Util::ToLower(filename_to_match) == filename;
}
bool GameLoader::File::operator==(const File &rhs) const
{
return Matches(rhs.filename, rhs.crc32);
}
GameLoader::Region::ptr_t GameLoader::Region::Create(const GameLoader &loader, const Util::Config::Node &region_node)
{
if (GameLoader::MissingAttrib(loader, region_node, "name") | MissingAttrib(loader, region_node, "stride") | GameLoader::MissingAttrib(loader, region_node, "chunk_size")) // no || to easier detect errors
return ptr_t();
if (region_node["byte_swap"].Exists() && region_node["byte_layout"].Exists())
{
ErrorLog("%s: '%s' region has both 'byte_swap' and 'byte_layout' attributes. Use one or the other.", loader.m_xml_filename.c_str(), region_node["name"].Value<std::string>().c_str());
return ptr_t();
}
ptr_t region = std::make_shared<Region>();
region->region_name = region_node["name"].Value<std::string>();
region->stride = region_node["stride"].ValueAs<size_t>();
if (region->stride == 0)
{
ErrorLog("%s: '%s' stride length must be greater than 0.", loader.m_xml_filename.c_str(), region->region_name.c_str());
return ptr_t();
}
region->chunk_size = region_node["chunk_size"].ValueAs<size_t>();
if (region->chunk_size == 0)
{
ErrorLog("%s: '%s' chunk size must be greater than 0.", loader.m_xml_filename.c_str(), region->region_name.c_str());
return ptr_t();
}
region->required = region_node["required"].ValueAsDefault<bool>(true);
// Byte layout. If byte_swap was specified, construct the byte swapped layout string based on
// stride size. If byte_swap is set to false, empty layout string is fine.
if (region_node["byte_swap"].Exists())
{
if (region_node["byte_swap"].ValueAs<bool>())
{
// Special case: if chunk size and stride are both 1, change them both to 2 so we can allow byte
// swapping (these values are used for singular ROMs that don't need to be merged; technically,
// the stride and chunk size should be 2 since they are 16-bit ROMs).
if (region->stride == 1 && region->chunk_size == 1)
{
region->stride = 2;
region->chunk_size = 2;
}
std::string byte_layout;
for (size_t i = 0; i < region->stride; i++)
{
byte_layout += '0' + (i ^ 1);
}
region->byte_layout = byte_layout;
}
}
else
{
region->byte_layout = region_node["byte_layout"].ValueAsDefault<std::string>(std::string());
}
return region;
}
bool GameLoader::Region::AttribsMatch(const ptr_t &other) const
{
return stride == other->stride && chunk_size == other->chunk_size && byte_layout == other->byte_layout;
}
bool GameLoader::Region::FindFileIndexByOffset(size_t *idx, uint32_t offset) const
{
for (size_t i = 0; i < files.size(); i++)
{
if (files[i]->offset == offset)
{
*idx = i;
return true;
}
}
return false;
}
static void PopulateGameInfo(Game *game, const Util::Config::Node &game_node)
{
game->name = game_node["name"].ValueAs<std::string>();
game->parent = game_node["parent"].ValueAsDefault<std::string>("");
game->title = game_node["identity/title"].ValueAsDefault<std::string>("Unknown");
game->version = game_node["identity/version"].ValueAsDefault<std::string>("");
game->manufacturer = game_node["identity/manufacturer"].ValueAsDefault<std::string>("Unknown");
game->year = game_node["identity/year"].ValueAsDefault<unsigned>(0);
game->stepping = game_node["hardware/stepping"].ValueAsDefault<std::string>("");
game->mpeg_board = game_node["hardware/mpeg_board"].ValueAsDefault<std::string>("");
std::map<std::string, Game::AudioTypes> audio_types
{
{ "", Game::STEREO_LR }, // default to stereo
{ "Mono", Game::MONO },
{ "Stereo", Game::STEREO_LR },
{ "StereoReversed", Game::STEREO_RL },
{ "QuadFrontRear", Game::QUAD_1_FLR_2_RLR },
{ "QuadFrontRearReversed", Game::QUAD_1_FRL_2_RRL },
{ "QuadRearFront", Game::QUAD_1_RLR_2_FLR },
{ "QuadRearFrontReversed", Game::QUAD_1_RRL_2_FRL },
{ "QuadMix", Game::QUAD_1_LR_2_FR_MIX}
};
std::string audio_type = game_node["hardware/audio"].ValueAsDefault<std::string>("");
game->audio = audio_types[audio_type];
game->encryption_key = game_node["hardware/encryption_key"].ValueAsDefault<uint32_t>(0);
game->netboard_present = game_node["hardware/netboard"].ValueAsDefault<bool>(false);
std::map<std::string, uint32_t> input_flags
{
{ "common", Game::INPUT_COMMON },
{ "vehicle", Game::INPUT_VEHICLE },
{ "joystick1", Game::INPUT_JOYSTICK1 },
{ "joystick2", Game::INPUT_JOYSTICK2 },
{ "fighting", Game::INPUT_FIGHTING },
{ "vr4", Game::INPUT_VR4 },
{ "viewchange", Game::INPUT_VIEWCHANGE },
{ "shift4", Game::INPUT_SHIFT4 },
{ "shiftupdown", Game::INPUT_SHIFTUPDOWN },
{ "handbrake", Game::INPUT_HANDBRAKE },
{ "harley", Game::INPUT_HARLEY },
{ "gun1", Game::INPUT_GUN1 },
{ "gun2", Game::INPUT_GUN2 },
{ "analog_joystick", Game::INPUT_ANALOG_JOYSTICK },
{ "twin_joysticks", Game::INPUT_TWIN_JOYSTICKS },
{ "soccer", Game::INPUT_SOCCER },
{ "spikeout", Game::INPUT_SPIKEOUT },
{ "analog_gun1", Game::INPUT_ANALOG_GUN1 },
{ "analog_gun2", Game::INPUT_ANALOG_GUN2 },
{ "ski", Game::INPUT_SKI },
{ "magtruck", Game::INPUT_MAGTRUCK },
{ "fishing", Game::INPUT_FISHING }
};
for (auto &node: game_node["hardware/inputs"])
{
if (node.Key() == "input" && node["type"].Exists())
{
const std::string input_type = node["type"].ValueAs<std::string>();
game->inputs |= input_flags[input_type];
}
}
std::map<std::string, Game::DriveBoardType> drive_board_types
{
{ "Wheel", Game::DRIVE_BOARD_WHEEL },
{ "Joystick", Game::DRIVE_BOARD_JOYSTICK },
{ "Ski", Game::DRIVE_BOARD_SKI },
{ "Billboard", Game::DRIVE_BOARD_BILLBOARD}
};
std::string drive_board_type = game_node["hardware/drive_board"].ValueAsDefault<std::string>(std::string());
game->driveboard_type = drive_board_types[drive_board_type];
}
bool GameLoader::LoadGamesFromXML(const Util::Config::Node &xml)
{
for (auto it = xml.begin(); it != xml.end(); ++it)
{
// Root games node
auto &root_node = *it;
if (root_node.Key() != "games")
continue;
for (auto &game_node: root_node)
{
// Game node
if (game_node.Key() != "game")
continue;
if (game_node["name"].Empty())
{
//TODO: associate line numbers in config
//ErrorLog("%s: Ignoring <game> tag with missing 'name' attribute.", m_xml_filename.c_str());
continue;
}
std::string game_name = game_node["name"].ValueAs<std::string>();
if (m_regions_by_game.find(game_name) != m_regions_by_game.end())
{
ErrorLog("%s: Ignoring redefinition of game '%s'.", m_xml_filename.c_str(), game_name.c_str());
continue;
}
RegionsByName_t &regions_by_name = m_regions_by_game[game_name];
PatchesByRegion_t &patches_by_region = m_patches_by_game[game_name];
PopulateGameInfo(&m_game_info_by_game[game_name], game_node);
for (auto &roms_node: game_node)
{
if (roms_node.Key() != "roms")
continue;
/*
* Regions define contiguous memory areas that individual ROM files are
* loaded into. It is possible to have multiple region tags identifying
* the same region. They will be aggregated. This is useful for parent
* and child ROM sets, which each may need to define the same region,
* with the child set loading in different files to overwrite the parent
* set.
*/
for (auto &region_node: roms_node)
{
if (region_node.Key() != "region")
continue;
// Look up region structure or create new one if needed
std::string region_name = region_node["name"].Value<std::string>();
auto it2 = regions_by_name.find(region_name);
Region::ptr_t region = (it2 != regions_by_name.end()) ? it2->second : Region::Create(*this, region_node);
if (!region)
continue;
/*
* Files are defined by the offset they are loaded at. Normally, there
* should be one file per offset but parent/child ROM sets will violate
* this, and so it is allowed.
*/
std::vector<File::ptr_t> &files = region->files;
for (auto &file_node: region_node)
{
if (file_node.Key() != "file")
continue;
File::ptr_t file = File::Create(*this, file_node);
if (!file)
continue;
files.push_back(file);
}
// Check to ensure that some files were defined in the region
if (files.empty())
ErrorLog("%s: No files defined in region '%s' of '%s'.", m_xml_filename.c_str(), region->region_name.c_str(), game_name.c_str());
else
regions_by_name[region->region_name] = region;
}
// ROM patches, if any
for (auto &patches_node: roms_node)
{
if (patches_node.Key() != "patches")
continue;
for (auto &patch_node: patches_node)
{
if (MissingAttrib(*this, patch_node, "region") ||
MissingAttrib(*this, patch_node, "bits") ||
MissingAttrib(*this, patch_node, "offset") ||
MissingAttrib(*this, patch_node, "value"))
continue;
std::string region = patch_node["region"].ValueAs<std::string>();
unsigned bits = patch_node["bits"].ValueAs<unsigned>();
uint32_t offset = patch_node["offset"].ValueAs<uint32_t>();
uint64_t value = patch_node["value"].ValueAs<uint64_t>();
if (bits != 8 && bits != 16 && bits != 32 && bits != 64)
ErrorLog("%s: Ignoring ROM patch in '%s' with invalid bit length. Must be 8, 16, 32, or 64!", m_xml_filename.c_str(), game_name.c_str());
else
patches_by_region[region].push_back(ROM::BigEndianPatch(offset, value, bits));
}
}
}
// Check to ensure that some ROM regions were defined for the game
if (regions_by_name.empty())
ErrorLog("%s: No ROM regions defined for '%s'.", m_xml_filename.c_str(), game_name.c_str());
}
}
// Check to ensure some games were defined
if (m_regions_by_game.empty())
{
ErrorLog("%s: No games defined.", m_xml_filename.c_str());
return true;
}
return false;
}
static bool IsChildSet(const Game &game)
{
return !game.parent.empty();
}
bool GameLoader::MergeChildrenWithParents()
{
bool error = false;
for (auto &v1: m_regions_by_game)
{
auto &game = m_game_info_by_game[v1.first];
if (!IsChildSet(game)) // we want child sets
continue;
auto &child_regions = v1.second;
auto &parent_regions = m_regions_by_game[game.parent];
// Rebuild child regions by copying over all parent regions first, then
// merge in files from equivalent child regions
RegionsByName_t new_regions;
for (auto &v2: parent_regions)
{
// Copy over parent region (shallow copy is sufficient, vector of files
// will be copied over correctly)
auto &region_name = v2.first;
new_regions[region_name] = std::make_shared<Region>(*v2.second);
auto &new_region = new_regions[region_name];
// Replace equivalent files from child in parent region, appending any
// new ones
if (child_regions.find(region_name) != child_regions.end())
{
auto &child_region = child_regions[region_name];
if (!new_region->AttribsMatch(child_region))
{
ErrorLog("%s: Attributes of region '%s' in parent '%s' and child '%s' do not match.", m_xml_filename.c_str(), region_name.c_str(), game.parent.c_str(), game.name.c_str());
error = true;
}
for (size_t i = 0; i < child_region->files.size(); i++)
{
size_t idx;
if (new_region->FindFileIndexByOffset(&idx, child_region->files[i]->offset))
new_region->files[idx] = child_region->files[i];
else
new_region->files.push_back(child_region->files[i]);
}
}
}
// Simply append any region in child that does *not* exist in parent
for (auto &v2: child_regions)
{
if (new_regions.find(v2.first) == new_regions.end())
{
// Since these are pointers anyway, just insert directly
new_regions[v2.first] = v2.second;
}
}
// Save the final result
m_regions_by_merged_game[v1.first] = new_regions;
}
return error;
}
void GameLoader::LogROMDefinition(const std::string &game_name, const RegionsByName_t &regions_by_name) const
{
InfoLog("%s:", game_name.c_str());
for (auto &v2: regions_by_name)
{
InfoLog(" %s: stride=%zu, chunk size=%zu, byte layout=%s", v2.first.c_str(), v2.second->stride, v2.second->chunk_size, v2.second->byte_layout.c_str());
for (auto &file: v2.second->files)
{
InfoLog(" %s, crc32=0x%08x, offset=0x%08x", file->filename.c_str(), file->crc32, file->offset);
}
}
}
bool GameLoader::ParseXML(const Util::Config::Node &xml)
{
if (LoadGamesFromXML(xml))
return true;
// More than one level of parents not allowed
bool error = false;
std::set<std::string> parents_with_parents;
for (auto &v: m_game_info_by_game)
{
if (IsChildSet(v.second))
{
if (IsChildSet(m_game_info_by_game[v.second.parent]))
{
parents_with_parents.insert(v.second.parent);
error = true;
}
}
}
for (auto &game_name: parents_with_parents)
{
ErrorLog("%s: Parent ROM set '%s' also has parent defined, which is not allowed.", m_xml_filename.c_str(), game_name.c_str());
}
if (MergeChildrenWithParents())
return true;
return error;
}
bool GameLoader::LoadDefinitionXML(const std::string &filename)
{
m_xml_filename = filename;
Util::Config::Node xml("xml");
if (Util::Config::FromXMLFile(&xml, filename))
{
ErrorLog("Game and ROM set definitions could not be loaded! ROMs will not be detected.");
return true;
}
return ParseXML(xml);
}
void GameLoader::FindEquivalentFiles(std::set<File::ptr_t> *equivalent_files, const std::set<File::ptr_t> &a, const std::set<File::ptr_t> &b)
{
// Copy files that are equivalent between a and b from a (doesn't matter
// which we actually use) to output
for (auto &file1: a)
{
for (auto &file2: b)
{
if (*file1 == *file2)
equivalent_files->insert(file1);
}
}
}
void GameLoader::IdentifyGamesInZipArchive(
std::set<std::string> *complete_games,
std::map<std::string, std::set<File::ptr_t>> *files_missing_by_game,
const ZipArchive &zip,
const std::map<std::string, RegionsByName_t> &regions_by_game) const
{
std::map<std::string, std::set<File::ptr_t>> files_required_by_game;
std::map<std::string, std::set<File::ptr_t>> files_found_by_game;
// Determine which files each game requires and which files are present in
// the zip archive. Files belonging to optional regions cannot be used to
// identify games.
for (auto &v1: regions_by_game)
{
const std::string &game_name = v1.first;
auto &regions_by_name = v1.second;
for (auto &v2: regions_by_name)
{
Region::ptr_t region = v2.second;
if (!region->required)
continue;
for (const auto &file: region->files)
{
// Add each file to the set of required files per game
files_required_by_game[game_name].insert(file);
// Check file in ROM definition against all files in zip
if (FileExistsInZipArchive(file, zip))
files_found_by_game[game_name].insert(file);
}
}
}
/*
* Corner case: some child ROM sets legitimately share files, which can fool
* us into thinking two games are partially present. Need to remove the one
* that is not really there by detecting case when only overlapping files
* exist (the ROM set with more present files is the intended one).
*/
std::vector<std::string> to_remove;
for (auto &v1: files_found_by_game)
{
auto &game1_name = v1.first;
auto &game1_files = v1.second;
for (auto &v2: files_found_by_game)
{
auto &game2_name = v2.first;
auto &game2_files = v2.second;
if (game1_name == game2_name)
continue;
std::set<File::ptr_t> equivalent_files;
FindEquivalentFiles(&equivalent_files, game1_files, game2_files);
/*
* If the these two games have a different number of files in the zip
* archive, but one consists only of the overlapping files, we can safely
* conclude that these files represent only the game with the larger
* number of files present. Otherwise, if only the overlapping files are
* present for both, we have a genuine ambiguity and hence do nothing.
*/
if (game1_files.size() != game2_files.size() && equivalent_files.size() == game2_files.size())
to_remove.push_back(game2_name);
}
}
for (auto &game_name: to_remove)
{
files_found_by_game.erase(game_name);
}
// Find the missing files for each game we found in the zip archive, then use
// this to determine whether the complete game exists
auto compare = [](const File::ptr_t &a, const File::ptr_t &b) { return a->filename < b->filename; };
for (auto &v: files_found_by_game)
{
auto &files_found = v.second;
auto &files_required = files_required_by_game[v.first];
auto &files_missing = (*files_missing_by_game)[v.first];
// Need to sort by filename for set_difference to work
std::vector<File::ptr_t> files_found_v(files_found.begin(), files_found.end());
std::vector<File::ptr_t> files_required_v(files_required.begin(), files_required.end());
std::sort(files_found_v.begin(), files_found_v.end(), compare);
std::sort(files_required_v.begin(), files_required_v.end(), compare);
// Use set difference to find missing files
std::set_difference(
files_required_v.begin(), files_required_v.end(),
files_found_v.begin(), files_found_v.end(),
std::inserter(files_missing, files_missing.end()),
compare);
// Is the whole game present?
if (files_found == files_required)
complete_games->insert(v.first);
// Clean up: if no files missing, don't want empty entry in map
if (files_missing.empty())
files_missing_by_game->erase(v.first);
}
}
void GameLoader::ChooseGameInZipArchive(std::string *chosen_game, bool *missing_parent_roms, const ZipArchive &zip, const std::string &zipfilename) const
{
chosen_game->clear();
*missing_parent_roms = false;
// Find complete unmerged games (those that do not need to be merged with a
// parent). This will pick up child-only ROMs, too, which we prune out later.
std::set<std::string> complete_games;
std::map<std::string, std::set<File::ptr_t>> files_missing_by_game;
IdentifyGamesInZipArchive(&complete_games, &files_missing_by_game, zip, m_regions_by_game);
// Find complete, merged games
std::set<std::string> complete_merged_games;
std::map<std::string, std::set<File::ptr_t>> files_missing_by_merged_game;
IdentifyGamesInZipArchive(&complete_merged_games, &files_missing_by_merged_game, zip, m_regions_by_merged_game);
/*
* Find incomplete child games by sorting child games out from the unmerged
* games results and pruning out complete merged games. Don't care about
* missing files because they are not neccessarily an error for these games.
* If one ends up being chosen, we would try to load from a second, parent
* ROM set.
*/
std::set<std::string> incomplete_child_games;
for (auto &v: m_game_info_by_game)
{
auto &game_name = v.first;
if (IsChildSet(v.second))
{
if (complete_games.count(game_name) || files_missing_by_game.find(game_name) != files_missing_by_game.end())
{
incomplete_child_games.insert(game_name);
complete_games.erase(game_name);
files_missing_by_game.erase(game_name);
}
}
}
for (auto &game_name: complete_merged_games)
{
incomplete_child_games.erase(game_name);
}
// Complete merged games take highest precedence
for (auto &game_name: complete_merged_games)
{
const std::string &parent = m_game_info_by_game.find(game_name)->second.parent;
// Complete merged game will be used, so ignore the parent entirely
complete_games.erase(parent);
// Complete merged sets will often have some parent ROMs missing (those
// replaced by the child games). This is not an error, so remove parents of
// complete merged games from missing file list.
files_missing_by_game.erase(parent);
}
// Any remaining incomplete games from the unmerged set are legitimate errors
for (auto &v: files_missing_by_game)
{
for (auto &file: v.second)
{
ErrorLog("'%s' (CRC32 0x%08x) not found in '%s' for game '%s'.", file->filename.c_str(), file->crc32, zipfilename.c_str(), v.first.c_str());
}
ErrorLog("Ignoring game '%s' in '%s' because it is missing files.", v.first.c_str(), zipfilename.c_str());
}
// Choose game: complete merged game > incomplete child game > complete
// unmerged game
if (!complete_merged_games.empty())
chosen_game->assign(*complete_merged_games.begin());
else if (!incomplete_child_games.empty())
{
// TODO: could use scoring to pick game with most files?
chosen_game->assign(*incomplete_child_games.begin());
*missing_parent_roms = true; // try to find missing files in parent ROM zip file
}
else if (!complete_games.empty())
chosen_game->assign(*complete_games.begin());
else
{
ErrorLog("No complete Model 3 games found in '%s'.", zipfilename.c_str());
return;
}
// Print out which game we chose from valid candidates in the zip file
std::set<std::string> candidates(complete_games);
candidates.insert(complete_merged_games.begin(), complete_merged_games.end());
candidates.insert(incomplete_child_games.begin(), incomplete_child_games.end());
if (candidates.size() > 1)
ErrorLog("Multiple games found in '%s' (%s). Loading '%s'.", zipfilename.c_str(), Util::Format(", ").Join(candidates).str().c_str(), chosen_game->c_str());
}
bool GameLoader::ComputeRegionSize(uint32_t *region_size, const GameLoader::Region::ptr_t &region, const ZipArchive &zip) const
{
// Files in region need not be loaded contiguously. To find region size,
// use maximum end_addr = offset + stride * (num_chunks - 1) + chunk_size.
std::vector<uint32_t> end_addr;
bool error = false;
for (const auto &file: region->files)
{
const ZippedFile *zipped_file = LookupFile(file, zip);
if (zipped_file)
{
if (zipped_file->uncompressed_size % region->chunk_size != 0)
{
ErrorLog("File '%s' in '%s' is not sized in %d-byte chunks.", zipped_file->filename.c_str(), zipped_file->zipfilename.c_str(), region->chunk_size);
error = true;
}
uint32_t num_chunks = (uint32_t)(zipped_file->uncompressed_size / region->chunk_size);
end_addr.push_back(file->offset + region->stride * (num_chunks - 1) + region->chunk_size);
}
else
error = true;
}
if (!error)
*region_size = *std::max_element(end_addr.begin(), end_addr.end());
return error;
}
static bool ApplyLayout(ROM *rom, const std::string &byte_layout, size_t stride, const std::string &region_name)
{
// Empty layout means do nothing
if (byte_layout.empty())
return false;
// Validate that the layout string includes the same number of bytes as the region stride. The
// stride is block size that the ROM files all contribute to. We also verify that each byte is
// used once and only once.
if (byte_layout.size() != stride)
{
ErrorLog("Byte layout of '%s' region does not match the stride length (%d bytes but should be %d bytes).", region_name.c_str(), byte_layout.size(), stride);
return true;
}
if (stride > 8)
{
ErrorLog("Region '%s' has stride larger than 8 (%d), which is currently unsupported.", region_name.c_str(), stride);
return true;
}
std::vector<size_t> byte_offsets;
for (char c: byte_layout)
{
if (isdigit(c))
{
byte_offsets.push_back(c - '0');
}
else
{
ErrorLog("Byte layout of '%s' region contains non-numeric characters. Use single-digit byte indices only.", region_name.c_str());
return true;
}
}
// Check all byte indices 0..N-1 are present
std::vector<size_t> sorted(byte_offsets);
std::sort(sorted.begin(), sorted.end()); // ascending order
size_t expected_offset = 0;
for (size_t offset: sorted)
{
if (offset != expected_offset)
{
ErrorLog("Byte layout of '%s' region must specify all byte offsets exactly once.", region_name.c_str());
return true;
}
expected_offset += 1;
}
// Okay, all good. Now we can reshuffle the region memory according to layout.
uint8_t *buffer = new uint8_t[stride];
uint8_t *dest = rom->data.get();
for (size_t dest_offset = 0; (dest_offset + stride) <= rom->size; dest_offset += stride)
{
// Copy current region bytes to temporary buffer. The layout offsets refer to this original layout.
memcpy(buffer, dest + dest_offset, stride);
// Place the bytes back into the ROM region in the layout order specified.
for (size_t i = 0; i < stride; i++)
{
dest[dest_offset + i] = buffer[byte_offsets[i]];
}
}
delete [] buffer;
return false; // no error
}
bool GameLoader::LoadRegion(ROM *rom, const GameLoader::Region::ptr_t &region, const ZipArchive &zip) const
{
bool error = false;
for (auto &file: region->files)
{
std::shared_ptr<uint8_t> tmp;
size_t file_size;
error |= LoadZippedFile(&tmp, &file_size, file, zip);
if (!error)
{
uint8_t *dest = rom->data.get();
uint8_t *src = tmp.get();
if (region->chunk_size == region->stride)
{
memcpy(dest + file->offset, src, file_size);
}
else
{
uint32_t num_chunks = (uint32_t)(file_size / region->chunk_size);
uint32_t dest_offset = file->offset;
uint32_t src_offset = 0;
uint32_t chunk_size = (uint32_t)region->chunk_size; // cache these as pointer dereferencing cripples performance in a tight loop
uint32_t stride = (uint32_t)region->stride;
for (uint32_t i = 0; i < num_chunks; i++)
{
memcpy(dest + dest_offset, src + src_offset, chunk_size);
dest_offset += stride;
src_offset += chunk_size;
}
}
}
}
if (!error)
{
error = ApplyLayout(rom, region->byte_layout, region->stride, region->region_name);
}
return error;
}
bool GameLoader::LoadROMs(ROMSet *rom_set, const std::string &game_name, const ZipArchive &zip) const
{
auto it = m_game_info_by_game.find(game_name);
if (it == m_game_info_by_game.end())
{
ErrorLog("Cannot load unknown game '%s'. Is it defined in '%s'?", game_name.c_str(), m_xml_filename.c_str());
return true;
}
// Load up the ROMs
auto &regions_by_name = IsChildSet(it->second) ? m_regions_by_merged_game.find(game_name)->second : m_regions_by_game.find(game_name)->second;
LogROMDefinition(game_name, regions_by_name);
bool error = false;
for (auto &v: regions_by_name)
{
auto &region = v.second;
uint32_t region_size = 0;
bool error_loading_region = false;
// Attempt to load the region
if (ComputeRegionSize(&region_size, region, zip))
error_loading_region = true;
else
{
// Load up the ROM region
auto &rom = rom_set->rom_by_region[region->region_name];
rom.data.reset(new uint8_t[region_size], std::default_delete<uint8_t[]>());
rom.size = region_size;
error_loading_region = LoadRegion(&rom, region, zip);
}
if (error_loading_region && !region->required)
{
// Failed to load the region but it wasn't required anyway, so remove it
// and proceed
rom_set->rom_by_region.erase(region->region_name);
ErrorLog("Optional ROM region '%s' in '%s' could not be loaded.", region->region_name.c_str(), game_name.c_str());
}
else
{
// Proceed normally: accumulate errors
error |= error_loading_region;
}
}
// Attach the patches and do some more error checking here
auto &patches_by_region = m_patches_by_game.find(game_name)->second;
for (auto &v: patches_by_region)
{
auto &region_name = v.first;
auto &patches = v.second;
if (regions_by_name.find(region_name) == regions_by_name.end())
ErrorLog("%s: Ignoring ROM patch for undefined region '%s' in '%s'.", m_xml_filename.c_str(), region_name.c_str(), game_name.c_str());
else if (rom_set->rom_by_region.find(region_name) != rom_set->rom_by_region.end())
rom_set->rom_by_region[region_name].patches = patches;
}
return error;
}
std::string StripFilename(const std::string &filepath)
{
// Search for last '/' or '\', if any
size_t last_slash = std::string::npos;
for (size_t i = filepath.length() - 1; i < filepath.length(); i--)
{
if (filepath[i] == '/' || filepath[i] =='\\')
{
last_slash = i;
break;
}
}
// If none found, there is directory component here
if (last_slash == std::string::npos)
return std::string();
// Otherwise, strip everything after the slash
return std::string(filepath, 0, last_slash + 1);
}
bool GameLoader::Load(Game *game, ROMSet *rom_set, const std::string &zipfilename) const
{
*game = Game();
// Read the zip contents
ZipArchive zip;
if (LoadZipArchive(&zip, zipfilename))
return true;
// Pick the game to load (there could be multiple ROM sets in a zip file)
std::string chosen_game;
bool missing_parent_roms = false;
ChooseGameInZipArchive(&chosen_game, &missing_parent_roms, zip, zipfilename);
if (chosen_game.empty())
return true;
// Return game information to caller
*game = m_game_info_by_game.find(chosen_game)->second;
// Bring in additional parent ROM set if needed
if (missing_parent_roms)
{
std::string parent_zipfilename = StripFilename(zipfilename) + game->parent + ".zip";
if (LoadZipArchive(&zip, parent_zipfilename))
{
ErrorLog("Expected to find parent ROM set of '%s' at '%s'.", game->name.c_str(), parent_zipfilename.c_str());
return true;
}
}
// Load
bool error = LoadROMs(rom_set, game->name, zip);
if (error)
*game = Game();
return error;
}
GameLoader::GameLoader(const std::string &xml_file)
{
LoadDefinitionXML(xml_file);
}
GameLoader::GameLoader(const char *xml_data, size_t xml_size, const std::string &source_name)
{
m_xml_filename = source_name;
Util::Config::Node xml("xml");
std::string text(xml_data, xml_size);
if (Util::Config::FromXML(&xml, text))
{
ErrorLog("Game and ROM set definitions could not be parsed from '%s'!", source_name.c_str());
return;
}
ParseXML(xml);
}