diff --git a/Src/Game.cpp b/Src/Game.cpp new file mode 100644 index 0000000..66fd55a --- /dev/null +++ b/Src/Game.cpp @@ -0,0 +1,379 @@ +#include "Game.h" +#include "OSD/Logger.h" +#include "Util/NewConfig.h" +#include "Util/ByteSwap.h" +#include + +#include + + +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"].ValueAsUnsigned(); + file->filename = Util::ToLower(file_node["name"].Value()); + file->has_crc32 = file_node["crc32"].Exists(); + file->crc32 = file->has_crc32 ? file_node["crc32"].ValueAsUnsigned() : 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"].ValueAsUnsigned(); + region->chunk_size = region_node["chunk_size"].ValueAsUnsigned(); + region->byte_swap = region_node["byte_swap"].ValueAsBoolWithDefault(false); + return region; +} + +static void PopulateGameInfo(Game *game, const Util::Config::Node &game_node) +{ + game->title = game_node["identity/title"].Value(); + game->version = game_node["identity/version"].Value(); + game->manufacturer = game_node["identity/manufacturer"].Value(); + game->year = game_node["identity/year"].ValueAsUnsigned(); + game->stepping = game_node["identity/stepping"].Value(); +} + +bool GameLoader::ParseXML(const Util::Config::Node::ConstPtr_t &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"].Value(); + 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::ByteSwap(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 + { + std::cout << region->region_name << " -> " << Util::Hex(region_size) << std::endl; + 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::ConstPtr_t xml = std::const_pointer_cast(Util::Config::FromXMLFile(filename)); + 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; +} diff --git a/Src/Game.h b/Src/Game.h new file mode 100644 index 0000000..c7338ed --- /dev/null +++ b/Src/Game.h @@ -0,0 +1,82 @@ +#ifndef INCLUDED_GAME_H +#define INCLUDED_GAME_H + +#include "Util/NewConfig.h" +#include "Pkgs/unzip.h" +#include +#include + +struct Game +{ + std::string title; + std::string version; + std::string manufacturer; + int year; + std::string stepping; + // Holds a ROM region + struct ROM + { + std::shared_ptr data; + size_t size = 0; + }; + std::map roms; +}; + +//TODO: move to separate GameLoader.cpp +class GameLoader +{ +private: + // Describes a file node in the game XML + struct File + { + typedef std::shared_ptr Ptr_t; + uint32_t offset; + std::string filename; + uint32_t crc32; + bool has_crc32; + static Ptr_t Create(const GameLoader &loader, const Util::Config::Node &file_node); + bool Matches(const std::string &filename, uint32_t crc32) const; + }; + + // Describes a region node in the game XML + struct Region + { + typedef std::shared_ptr Ptr_t; + std::string region_name; + size_t stride; + size_t chunk_size; + bool byte_swap; + typedef std::map FilesByOffset_t; + FilesByOffset_t files_by_offset; + static Ptr_t Create(const GameLoader &loader, const Util::Config::Node ®ion_node); + }; + + std::map m_game_info_by_game; // ROMs not loaded here + typedef std::map RegionsByName_t; + std::map m_regions_by_game; + std::string m_xml_filename; + std::string m_zipfilename; + unzFile m_zf; + std::map m_filename_by_crc32; + std::map m_zip_info_by_filename; + std::map m_zip_info_by_crc32; + + static bool MissingAttrib(const GameLoader &loader, const Util::Config::Node &node, const std::string &attribute); + bool ParseXML(const Util::Config::Node::ConstPtr_t &xml); + std::set IdentifyGamesFileBelongsTo(const std::string &filename, uint32_t crc32) const; + const unz_file_info *LookupZippedFile(const File::Ptr_t &file) const; + bool ComputeRegionSize(uint32_t *region_size, const Region::Ptr_t ®ion) const; + bool LoadZippedFile(std::shared_ptr *buffer, size_t *file_size, const GameLoader::File::Ptr_t &file); + bool LoadRegion(Game::ROM *buffer, const GameLoader::Region::Ptr_t ®ion); + bool LoadROMs(std::map *roms, const std::string &game_name); + +public: + GameLoader() + { + } + + bool LoadDefinitionXML(const std::string &filename); + bool Load(Game *game, const std::string &zipfilename); +}; + +#endif // INCLUDED_GAME_H