#include "Game.h" #include "OSD/Logger.h" #include "Util/NewConfig.h" #include "Util/ConfigBuilders.h" #include "Util/ByteSwap.h" #include #include void Game::ROM::CopyTo(uint8_t *dest, size_t dest_size) const { if (!data || !size || !dest || !dest_size) return; size_t bytes_to_copy = std::min(size, dest_size); const uint8_t *src = data.get(); memcpy(dest, src, bytes_to_copy); } 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")) return ptr_t(); ptr_t file = std::make_shared(); file->offset = file_node["offset"].ValueAs(); file->filename = Util::ToLower(file_node["name"].ValueAs()); file->has_crc32 = file_node["crc32"].Exists(); file->crc32 = file->has_crc32 ? file_node["crc32"].ValueAs() : 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; } GameLoader::Region::ptr_t GameLoader::Region::Create(const GameLoader &loader, const Util::Config::Node ®ion_node) { if (GameLoader::MissingAttrib(loader, region_node, "name") | MissingAttrib(loader, region_node, "stride") | GameLoader::MissingAttrib(loader, region_node, "chunk_size")) return ptr_t(); ptr_t region = std::make_shared(); region->region_name = region_node["name"].Value(); region->stride = region_node["stride"].ValueAs(); region->chunk_size = region_node["chunk_size"].ValueAs(); region->byte_swap = region_node["byte_swap"].ValueAsDefault(false); return region; } static void PopulateGameInfo(Game *game, const Util::Config::Node &game_node) { game->name = game_node["name"].ValueAs(); game->title = game_node["identity/title"].ValueAsDefault("Unknown"); game->version = game_node["identity/version"].ValueAsDefault(""); game->manufacturer = game_node["identity/manufacturer"].ValueAsDefault("Unknown"); game->year = game_node["identity/year"].ValueAsDefault(0); game->stepping = game_node["hardware/stepping"].ValueAsDefault(""); game->mpeg_board = game_node["hardware/mpeg_board"].ValueAsDefault(""); game->encryption_key = game_node["hardware/encryption_key"].ValueAsDefault(0); std::map 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(); game->inputs |= input_flags[input_type]; } } } bool GameLoader::ParseXML(const Util::Config::Node &xml) { for (auto it = xml.begin(); it != xml.end(); ++it) { // Game node auto &game_node = *it; if (game_node.Key() != "game") continue; if (game_node["name"].Empty()) { //TODO: associate line numbers in config //ErrorLog("%s: Ignoring tag with missing 'name' attribute.", m_xml_filename.c_str()); continue; } std::string game_name = game_node["name"].ValueAs(); 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 ®ions_by_name = m_regions_by_game[game_name]; PopulateGameInfo(&m_game_info_by_game[game_name], game_node); for (auto it = game_node.begin(); it != game_node.end(); ++it) { auto &roms_node = *it; if (roms_node.Key() != "roms") continue; // Regions defined uniquely per game for (auto it = roms_node.begin(); it != roms_node.end(); ++it) { auto ®ion_node = *it; if (region_node.Key() != "region") continue; Region::ptr_t region = Region::Create(*this, region_node); if (!region) continue; if (regions_by_name.find(region->region_name) != regions_by_name.end()) { ErrorLog("%s: Ignoring redefinition of region '%s' of '%s'.", m_xml_filename.c_str(), region->region_name.c_str(), game_name.c_str()); continue; } Region::FilesByOffset_t &files_by_offset = region->files_by_offset; // Files defined uniquely per region for (auto it = region_node.begin(); it != region_node.end(); ++it) { auto &file_node = *it; if (file_node.Key() != "file") continue; File::ptr_t file = File::Create(*this, file_node); if (!file) continue; // Ensure file offset not defined multiple times. We allow the same // file to be reused, however (e.g., a blank file loaded at multiple // offsets). if (files_by_offset.find(file->offset) != files_by_offset.end()) { ErrorLog("%s: Ignoring redefinition of offset 0x%x in region '%s' of '%s'.", m_xml_filename.c_str(), file->offset, region->region_name.c_str(), game_name.c_str()); continue; } files_by_offset[file->offset] = file; } // Check to ensure that some files were defined in the region if (files_by_offset.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; } } // 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; } std::set GameLoader::IdentifyGamesFileBelongsTo(const std::string &filename, uint32_t crc32) const { std::set games; for (auto &v_game: m_regions_by_game) { const std::string &game_name = v_game.first; auto ®ions_by_name = v_game.second; for (auto &v_region: regions_by_name) { Region::ptr_t region = v_region.second; for (auto &v_file: region->files_by_offset) { File::ptr_t file = v_file.second; if (file->Matches(filename, crc32)) games.insert(game_name); } } } return games; } const unz_file_info *GameLoader::LookupZippedFile(const File::ptr_t &file) const { if (file->has_crc32) { auto it = m_zip_info_by_crc32.find(file->crc32); if (it != m_zip_info_by_crc32.end()) return &it->second; ErrorLog("'%s' with CRC32 0x%08x not found in '%s'.", file->filename.c_str(), file->crc32, m_zipfilename.c_str()); return 0; } auto it = m_zip_info_by_filename.find(file->filename); if (it != m_zip_info_by_filename.end()) return &it->second; ErrorLog("'%s' not found in '%s'.", file->filename.c_str(), m_zipfilename.c_str()); return 0; } bool GameLoader::ComputeRegionSize(uint32_t *region_size, const GameLoader::Region::ptr_t ®ion) 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 end_addr; bool error = false; for (auto &v_file: region->files_by_offset) { auto &file = v_file.second; const unz_file_info *info = LookupZippedFile(file); if (info) { if (info->uncompressed_size % region->chunk_size != 0) { std::string filename = file->filename; auto it = m_filename_by_crc32.find(info->crc); if (it != m_filename_by_crc32.end()) filename = it->second; ErrorLog("File '%s' in '%s' is not sized in %d-byte chunks.", filename.c_str(), m_zipfilename.c_str(), region->chunk_size); error = true; } uint32_t num_chunks = info->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; } bool GameLoader::LoadZippedFile(std::shared_ptr *buffer, size_t *file_size, const GameLoader::File::ptr_t &file) { unz_file_info info; for (int err = unzGoToFirstFile(m_zf); err == UNZ_OK; err = unzGoToNextFile(m_zf)) { char current_filename[256]; if (UNZ_OK != unzGetCurrentFileInfo(m_zf, &info, current_filename, sizeof(current_filename), NULL, 0, NULL, 0)) continue; if (file->Matches(current_filename, info.crc)) { // Found file, load it! err = unzOpenCurrentFile(m_zf); if (UNZ_OK != err) { ErrorLog("Unable to read '%s' from '%s'. Is zip file corrupt?", current_filename, m_zipfilename.c_str()); return true; } *file_size = info.uncompressed_size; buffer->reset(new uint8_t[*file_size], std::default_delete()); ZPOS64_T bytes_read = unzReadCurrentFile(m_zf, buffer->get(), *file_size); if (bytes_read != *file_size) { ErrorLog("Unable to read '%s' from '%s'. Is zip file corrupt?", current_filename, m_zipfilename.c_str()); unzCloseCurrentFile(m_zf); return true; } err = unzCloseCurrentFile(m_zf); if (UNZ_CRCERROR == err) ErrorLog("CRC error reading '%s' from '%s'. File may be corrupt.", current_filename, m_zipfilename.c_str()); return false; } } if (file->has_crc32) ErrorLog("'%s' with CRC32 0x%08x not found in '%s'.", file->filename.c_str(), file->crc32, m_zipfilename.c_str()); else ErrorLog("'%s' not found in '%s'.", file->filename.c_str(), m_zipfilename.c_str()); return true; } bool GameLoader::LoadRegion(Game::ROM *buffer, const GameLoader::Region::ptr_t ®ion) { bool error = false; for (auto &v_file: region->files_by_offset) { auto &file = v_file.second; std::shared_ptr tmp; size_t file_size; error |= LoadZippedFile(&tmp, &file_size, file); if (!error) { size_t num_chunks = file_size / region->chunk_size; for (size_t i = 0; i < num_chunks; i++) { uint8_t *dest = buffer->data.get() + file->offset + i * region->stride; uint8_t *src = tmp.get() + i * region->chunk_size; memcpy(dest, src, region->chunk_size); } } } if (region->byte_swap) Util::FlipEndian16(buffer->data.get(), buffer->size); return error; } bool GameLoader::LoadROMs(std::map *roms, const std::string &game_name) { auto it = m_regions_by_game.find(game_name); if (it == m_regions_by_game.end()) { ErrorLog("Game '%s' not found in '%s'.", game_name.c_str(), m_zipfilename.c_str()); return true; } bool error = false; auto ®ions_by_name = it->second; for (auto &v_region: regions_by_name) { auto ®ion = v_region.second; uint32_t region_size = 0; if (ComputeRegionSize(®ion_size, region)) error |= true; else { auto &buffer = (*roms)[region->region_name]; buffer.size = region_size; buffer.data.reset(new uint8_t[region_size], std::default_delete()); error |= LoadRegion(&buffer, region); } } return error; } bool GameLoader::LoadDefinitionXML(const std::string &filename) { m_xml_filename = filename; Util::Config::Node xml("xml"); if (Util::Config::FromXMLFile(&xml, filename)) return true; return ParseXML(xml); } bool GameLoader::Load(Game *game, const std::string &zipfilename) { *game = Game(); m_zf = NULL; m_filename_by_crc32.clear(); m_zip_info_by_filename.clear(); m_zip_info_by_crc32.clear(); m_zipfilename = zipfilename; m_zf = unzOpen(zipfilename.c_str()); if (NULL == m_zf) { ErrorLog("Could not open '%s'.", zipfilename.c_str()); return true; } // Identify all files in zip archive int err = UNZ_OK; for (err = unzGoToFirstFile(m_zf); err == UNZ_OK; err = unzGoToNextFile(m_zf)) { unz_file_info file_info; char filename_buffer[256]; if (UNZ_OK != unzGetCurrentFileInfo(m_zf, &file_info, filename_buffer, sizeof(filename_buffer), NULL, 0, NULL, 0)) continue; std::string filename = Util::ToLower(filename_buffer); m_zip_info_by_filename[filename] = file_info; m_zip_info_by_crc32[file_info.crc] = file_info; m_filename_by_crc32[file_info.crc] = filename; } 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; } // Which game is this? std::map files_per_game; std::set all_games_found; using value_type = std::pair; for (auto &v: m_filename_by_crc32) { std::set games = IdentifyGamesFileBelongsTo(v.second, v.first); all_games_found.insert(games.begin(), games.end()); for (auto &game: games) files_per_game[game]++; } auto v = std::max_element(files_per_game.begin(), files_per_game.end(), [](const value_type &v1, const value_type &v2) { return v1.second < v2.second; }); if (v == files_per_game.end()) { ErrorLog("No valid Model 3 ROMs found in '%s'.", zipfilename.c_str()); return true; } std::string game_name = v->first; if (files_per_game.size() != 1) ErrorLog("Multiple games found in '%s' (%s). Loading '%s'.", zipfilename.c_str(), std::string(Util::Format(", ").Join(all_games_found)).c_str(), game_name.c_str()); // Load it *game = m_game_info_by_game[game_name]; bool error = LoadROMs(&game->roms, game_name); if (error) *game = Game(); unzClose(m_zf); return error; } GameLoader::GameLoader(const Util::Config::Node &config) { LoadDefinitionXML(config["GameXMLFile"].ValueAs()); }