// For licensing and usage information, read docs/release/winui_license.txt // MASTER /*************************************************************************** history.cpp * history functions. * History database engine * Collect all information on the selected driver, and return it as * a string. Called by winui.cpp * Token parsing by Neil Bradley * Modifications and higher-level functions by John Butler * Further work by Mamesick and Robbbert * Completely rewritten by Robbbert in July 2017 * Notes: * - The order listed in m_gameInfo is the order the data is displayed. * - The other tables must have the files in the same places so that * the index numbers line up. Anything with NULL indicates an * unsupported option (the file doesn't contain the info). * - Each table must contain at least MAX_HFILES members (extra lines * are ignored) * - Software comes first, followed by Game then Source. * 2023-06: Robbbert * - Added support for history.xml * - Dropped support for history.dat * - HBMAME: Dropped support for messinfo.dat, sysinfo.dat, story.dat, marp.dat ***************************************************************************/ // license:BSD-3-Clause // copyright-holders:Chris Kirmse, Mike Haaland, René Single, Mamesick, Robbbert #include #include // MAME/MAMEUI headers #include "emu.h" #include "screen.h" #include "speaker.h" #include "drivenum.h" #include "mui_util.h" #include "mui_opts.h" #include "emu_opts.h" #include "sound/samples.h" #include "path.h" /**************************************************************************** * struct definitions ****************************************************************************/ typedef struct { LPCSTR filename; LPCSTR header; LPCSTR descriptor; bool bClone; // if nothing found for a clone, try the parent } HGAMEINFO; typedef struct { LPCSTR filename; LPCSTR header; LPCSTR descriptor; } HSOURCEINFO; /*************************** START CONFIGURABLE AREA *******************************/ // number of dats we support #define MAX_HFILES 4 // The order of these is the order they are displayed const HGAMEINFO m_gameInfo[MAX_HFILES] = { { "history.xml", "\n**** :HISTORY: ****\n\n", "", 1 }, { "mameinfo.dat", "\n**** :MAMEINFO: ****\n\n", "$mame", 1 }, { "gameinit.dat", "\n**** :GAMEINIT: ****\n\n", "$mame", 1 }, { "command.dat", "\n**** :COMMANDS: ****\n\n", "$cmd", 1 }, }; const HSOURCEINFO m_sourceInfo[MAX_HFILES] = { { NULL }, { "mameinfo.dat", "\n***:MAMEINFO DRIVER: ", "$drv" }, { NULL }, { NULL }, }; const HSOURCEINFO m_swInfo[MAX_HFILES] = { { "history.xml", "\n**** :HISTORY item: ", "" }, { NULL }, { NULL }, { NULL }, }; /*************************** END CONFIGURABLE AREA *******************************/ int file_sizes[MAX_HFILES] = { 0, }; std::map mymap[MAX_HFILES]; const size_t npos = std::string::npos; static void create_index_history(const char* datsdir, std::ifstream &fp, std::string file_line, int filenum) { int what_to_do = 0; // 0 = xml not found; 1 = idx not found or version mismatch; 2 = both ok, need to read idx. const std::string text1 = " text_position) // else new key uses the same text as last time { // look for next text text_position = key_position; // set to actual fp for (;found == false;) { std::getline(fp, file_line); find = file_line.find(""); if (find != npos) { found = true; text_position += (find+6); } else text_position += (file_line.size() + 2); } } // Save info mymap[filenum][final_key] = text_position; } fp.seekg(key_position); std::getline(fp, file_line); key_position += (file_line.size() + 2); } // Save idx FILE *f = fopen(filename.c_str(), "w"); if (f == NULL) { printf("Unable to open history.idx for writing.\n"); return; } fprintf(f, "%s\n",xml_ver.c_str()); for (auto const &it : mymap[filenum]) fprintf(f, "%s=%d\n", it.first.c_str(), int(it.second)); fclose(f); } static bool create_index(const char* datsdir, std::ifstream &fp, int filenum) { if (!fp.good()) return false; // get file size fp.seekg(0, std::ios::end); size_t file_size = fp.tellg(); // same file as before? if (file_size == file_sizes[filenum]) return true; // new file, it needs to be indexed mymap[filenum].clear(); file_sizes[filenum] = file_size; fp.seekg(0); std::string file_line; std::getline(fp, file_line); if (filenum == 0) create_index_history(datsdir, fp, file_line, filenum); else { std::streampos position = file_line.size() + 2; // tellg is buggy, this works and is faster while (fp.good()) { if (file_line.find("$info=")==0) { // now start by removing all spaces file_line.erase(remove_if(file_line.begin(), file_line.end(), ::isspace), file_line.end()); file_line.erase(0,6); size_t t1 = npos; while (file_line.length() > 0) { t1 = file_line.find(","); // store into index mymap[filenum][file_line.substr(0, t1)] = position; if (t1 == npos) file_line.erase(); else file_line.erase(0, t1+1); // erase key and comma } } std::getline(fp, file_line); position += (file_line.size() + 2); } } // check contents // if (filenum == 6) // for (auto const &it : mymap[filenum]) // printf("%s = %X\n", it.first.c_str(), int(it.second)); return true; } static std::string convert_xml(std::string buf) { // convert xml to real chars if (!buf.empty()) { bool found = false; size_t find = 0; for (; found == false;) { find = buf.find("&"); if (find != npos) buf.replace(find,5,"&"); else found = true; } found = false; for (; found == false;) { find = buf.find("'"); if (find != npos) buf.replace(find,6,"\'"); else found = true; } found = false; for (; found == false;) { find = buf.find("""); if (find != npos) buf.replace(find,6,"\""); else found = true; } found = false; for (; found == false;) { find = buf.find("<"); if (find != npos) buf.replace(find,4,"<"); else found = true; } found = false; for (; found == false;) { find = buf.find(">"); if (find != npos) buf.replace(find,4,">"); else found = true; } } return buf; } std::string readbuf; static std::string load_datafile_text(std::ifstream &fp, std::string keycode, int filenum, const char *tag) { readbuf.clear(); auto search = mymap[filenum].find(keycode); if (search != mymap[filenum].end()) { std::streampos offset = mymap[filenum].find(keycode)->second; fp.seekg(offset); std::string file_line; // read text until buffer is full or end of entry is encountered while (std::getline(fp, file_line)) { //if (filenum == 6) ("*******2: %s\n",file_line.c_str()); if (file_line == "- CONTRIBUTE -") break; if (file_line.find("$end")==0) break; if (file_line.find("") != npos) break; if (file_line.find(tag)==0) continue; readbuf.append(file_line).append("\n"); } } return readbuf; } std::string buffer; std::string load_swinfo(const game_driver *drv, const char* datsdir, std::string software, int filenum) { buffer.clear(); // if it's a NULL record exit now if (!m_swInfo[filenum].filename) return buffer; // datafile name std::string buf, filename = datsdir + std::string("\\") + m_swInfo[filenum].filename; std::ifstream fp (filename); // try to open datafile if (create_index(datsdir, fp, filenum)) { size_t i = software.find(":"); std::string ssoft = software.substr(i+1); // get info on software buf = load_datafile_text(fp, software, filenum, m_swInfo[filenum].descriptor); if (!buf.empty()) buffer.append(m_swInfo[filenum].header).append(ssoft).append("\n").append(buf).append("\n\n\n"); fp.close(); } return convert_xml(buffer); } std::string load_gameinfo(const game_driver *drv, const char* datsdir, int filenum) { buffer.clear(); // if it's a NULL record exit now if (!m_gameInfo[filenum].filename) return buffer; // datafile name std::string buf, filename = datsdir + std::string("\\") + m_gameInfo[filenum].filename; std::ifstream fp (filename); if (filenum == 0) { // try to open history.xml if (create_index(datsdir, fp, filenum)) { // get info on game buf = load_datafile_text(fp, drv->name, filenum, m_gameInfo[filenum].descriptor); // if nothing, and it's a clone, and it's allowed, try the parent if (buf.empty() && m_gameInfo[filenum].bClone) { int g = driver_list::clone(*drv); if (g != -1) { drv = &driver_list::driver(g); buf = load_datafile_text(fp, drv->name, filenum, m_gameInfo[filenum].descriptor); } } } } else // try to open datafile if (create_index(datsdir, fp, filenum)) { // get info on game buf = load_datafile_text(fp, drv->name, filenum, m_gameInfo[filenum].descriptor); // if nothing, and it's a clone, and it's allowed, try the parent if (buf.empty() && m_gameInfo[filenum].bClone) { int g = driver_list::clone(*drv); if (g != -1) { drv = &driver_list::driver(g); buf = load_datafile_text(fp, drv->name, filenum, m_gameInfo[filenum].descriptor); } } } if (!buf.empty()) buffer.append(m_gameInfo[filenum].header).append(buf).append("\n\n\n"); fp.close(); return convert_xml(buffer); } std::string load_sourceinfo(const game_driver *drv, const char* datsdir, int filenum) { buffer.clear(); // if it's a NULL record exit now if (!m_sourceInfo[filenum].filename) return buffer; // datafile name std::string buf, filename = datsdir + std::string("\\") + m_sourceInfo[filenum].filename; std::ifstream fp (filename); std::string source = drv->type.source(); size_t i = source.find_last_of("/"); source.erase(0,i+1); if (create_index(datsdir, fp, filenum)) { // get info on game driver source buf = load_datafile_text(fp, source, filenum, m_sourceInfo[filenum].descriptor); if (!buf.empty()) buffer.append(m_sourceInfo[filenum].header).append(source).append("\n").append(buf).append("\n\n\n"); fp.close(); } return buffer; } // General hardware information std::string load_driver_geninfo(const game_driver *drv, int drvindex) { machine_config config(*drv, MameUIGlobal()); const game_driver *parent = NULL; char name[512]; bool is_bios = false; buffer = "\n**** :GENERAL MACHINE INFO: ****\n\n"; // List the game info 'flags' uint32_t cache = GetDriverCacheLower(drvindex); if (BIT(cache, 6)) buffer.append("This game doesn't work properly\n"); if (BIT(cache, 22)) buffer.append("This game has protection which isn't fully emulated.\n"); if (BIT(cache, 18)) buffer.append("The video emulation isn't 100% accurate.\n"); if (BIT(cache, 21)) buffer.append("The colors are completely wrong.\n"); if (BIT(cache, 20)) buffer.append("The colors aren't 100% accurate.\n"); if (BIT(cache, 17)) buffer.append("This game lacks sound.\n"); if (BIT(cache, 16)) buffer.append("The sound emulation isn't 100% accurate.\n"); if (BIT(cache, 7)) buffer.append("Save state not supported.\n"); if (BIT(cache, 14)) buffer.append("This game contains mechanical parts.\n"); if (BIT(cache, 15)) buffer.append("This game was never completed.\n"); if (BIT(cache, 13)) buffer.append("This game has no sound hardware.\n"); buffer.append("\n"); if (drv->flags & MACHINE_IS_BIOS_ROOT) is_bios = true; // GAME INFORMATIONS snprintf(name, std::size(name), "\nGAME: %s\n", drv->name); buffer.append(name); snprintf(name, std::size(name), "%s", drv->type.fullname()); buffer.append(name); snprintf(name, std::size(name), " (%s %s)\n\nCPU:\n", drv->manufacturer, drv->year); buffer.append(name); // iterate over CPUs execute_interface_enumerator cpuiter(config.root_device()); std::unordered_set exectags; for (device_execute_interface &exec : cpuiter) { if (!exectags.insert(exec.device().tag()).second) continue; int count = 1; int clock = exec.device().clock(); const char *cpu_name = exec.device().name(); for (device_execute_interface &scan : cpuiter) if (exec.device().type() == scan.device().type() && strcmp(cpu_name, scan.device().name()) == 0 && clock == scan.device().clock()) if (exectags.insert(scan.device().tag()).second) count++; if (count > 1) { snprintf(name, std::size(name), "%d x ", count); buffer.append(name); } if (clock >= 1000000) snprintf(name, std::size(name), "%s %d.%06d MHz\n", cpu_name, clock / 1000000, clock % 1000000); else snprintf(name, std::size(name), "%s %d.%03d kHz\n", cpu_name, clock / 1000, clock % 1000); buffer.append(name); } buffer.append("\nSOUND:\n"); int has_sound = 0; // iterate over sound chips sound_interface_enumerator sounditer(config.root_device()); std::unordered_set soundtags; for (device_sound_interface &sound : sounditer) { if (!soundtags.insert(sound.device().tag()).second) continue; has_sound = 1; int count = 1; int clock = sound.device().clock(); const char *sound_name = sound.device().name(); for (device_sound_interface &scan : sounditer) if (sound.device().type() == scan.device().type() && strcmp(sound_name, scan.device().name()) == 0 && clock == scan.device().clock()) if (soundtags.insert(scan.device().tag()).second) count++; if (count > 1) { snprintf(name, std::size(name), "%d x ", count); buffer.append(name); } buffer.append(sound_name); if (clock) { if (clock >= 1000000) snprintf(name, std::size(name), " %d.%06d MHz", clock / 1000000, clock % 1000000); else snprintf(name, std::size(name), " %d.%03d kHz", clock / 1000, clock % 1000); buffer.append(name); } buffer.append("\n"); } if (has_sound) { speaker_device_enumerator audioiter(config.root_device()); int channels = audioiter.count(); if(channels == 1) snprintf(name, std::size(name), "%d Channel\n", channels); else snprintf(name, std::size(name), "%d Channels\n", channels); buffer.append(name); } buffer.append("\nVIDEO:\n"); screen_device_enumerator screeniter(config.root_device()); int scrcount = screeniter.count(); if (scrcount == 0) buffer.append("Screenless"); else { for (screen_device &screen : screeniter) { if (screen.screen_type() == SCREEN_TYPE_VECTOR) buffer.append("Vector"); else { const rectangle &visarea = screen.visible_area(); if (drv->flags & ORIENTATION_SWAP_XY) snprintf(name, std::size(name), "%d x %d (V) %f Hz", visarea.width(), visarea.height(), ATTOSECONDS_TO_HZ(screen.refresh_attoseconds())); else snprintf(name, std::size(name), "%d x %d (H) %f Hz", visarea.width(), visarea.height(), ATTOSECONDS_TO_HZ(screen.refresh_attoseconds())); buffer.append(name); } buffer.append("\n"); } } buffer.append("\nROM REGION:\n"); int g = driver_list::clone(*drv); if (g != -1) parent = &driver_list::driver(g); for (device_t &device : device_enumerator(config.root_device())) { for (const rom_entry *region = rom_first_region(device); region; region = rom_next_region(region)) { for (const rom_entry *rom = rom_first_file(region); rom; rom = rom_next_file(rom)) { util::hash_collection hashes(rom->hashdata()); if (g != -1) { machine_config pconfig(*parent, MameUIGlobal()); for (device_t &device : device_enumerator(pconfig.root_device())) { for (const rom_entry *pregion = rom_first_region(device); pregion; pregion = rom_next_region(pregion)) { for (const rom_entry *prom = rom_first_file(pregion); prom; prom = rom_next_file(prom)) { util::hash_collection phashes(prom->hashdata()); if (hashes == phashes) break; } } } } snprintf(name, std::size(name), "%-16s \t", ROM_GETNAME(rom)); buffer.append(name); snprintf(name, std::size(name), "%09d \t", rom_file_size(rom)); buffer.append(name); snprintf(name, std::size(name), "%-10s", region->name().c_str()); buffer.append(name).append("\n"); } } } for (samples_device &device : samples_device_enumerator(config.root_device())) { samples_iterator sampiter(device); if (sampiter.altbasename()) { snprintf(name, std::size(name), "\nSAMPLES (%s):\n", sampiter.altbasename()); buffer.append(name); } std::unordered_set already_printed; for (const char *samplename = sampiter.first(); samplename; samplename = sampiter.next()) { // filter out duplicates if (!already_printed.insert(samplename).second) continue; // output the sample name snprintf(name, std::size(name), "%s.wav\n", samplename); buffer.append(name); } } if (!is_bios) { int g = driver_list::clone(*drv); if (g != -1) drv = &driver_list::driver(g); buffer.append("\nORIGINAL:\n"); buffer.append(drv->type.fullname()); buffer.append("\n\nCLONES:\n"); for (int i = 0; i < driver_list::total(); i++) { if (!strcmp (drv->name, driver_list::driver(i).parent)) { buffer.append(driver_list::driver(i).type.fullname()); buffer.append("\n"); } } } string temp = string(core_filename_extract_base(drv->type.source(), false)); char source_file[temp.size()+1], tmp[2048]; strcpy(source_file, temp.c_str()); snprintf(tmp, std::size(tmp), "\nGENERAL SOURCE INFO: %s\n", temp.c_str()); buffer.append(tmp); buffer.append("\nGAMES SUPPORTED:\n"); for (int i = 0; i < driver_list::total(); i++) { std::string t1 = driver_list::driver(i).type.source(); size_t j = t1.find_last_of("/"); t1.erase(0, j+1); if ((strcmp(source_file, t1.c_str())==0) && !(DriverIsBios(i))) buffer.append(driver_list::driver(i).type.fullname()).append("\n"); } return buffer; } // This is check that the tables are at least as big as they should be bool validate_datfiles(void) { bool result = true; if (std::size(m_gameInfo) < MAX_HFILES) { printf("m_gameInfo needs to have at least MAX_HFILES members\n"); result = false; } if (std::size(m_sourceInfo) < MAX_HFILES) { printf("m_sourceInfo needs to have at least MAX_HFILES members\n"); result = false; } if (std::size(m_swInfo) < MAX_HFILES) { printf("m_swInfo needs to have at least MAX_HFILES members\n"); result = false; } return result; } std::string fullbuf; // For all of MAME builds - called by winui.cpp char * GetGameHistory(int driver_index, std::string software) { fullbuf.clear(); if (driver_index < 0) return ConvertToWindowsNewlines(fullbuf.c_str()); if (validate_datfiles()) { // Get the path to dat files std::string t = dir_get_value(23); char buf[t.size()+1]; strcpy(buf, t.c_str()); // only want first path const char* datsdir = strtok(buf, ";"); // validate software BOOL sw_valid = false; if (!software.empty()) { size_t i = software.find(':'); sw_valid = (i != npos) ? true : false; } if (datsdir && osd::directory::open(datsdir)) { for (int filenum = 0; filenum < MAX_HFILES; filenum++) { if (sw_valid) fullbuf.append(load_swinfo(&driver_list::driver(driver_index), datsdir, software, filenum)); fullbuf.append(load_gameinfo(&driver_list::driver(driver_index), datsdir, filenum)); fullbuf.append(load_sourceinfo(&driver_list::driver(driver_index), datsdir, filenum)); } } else fullbuf = "\nThe path to your dat files is invalid.\n\n\n"; } else fullbuf = "\nUnable to display info due to an internal error.\n\n\n"; fullbuf.append(load_driver_geninfo(&driver_list::driver(driver_index), driver_index)); return ConvertToWindowsNewlines(fullbuf.c_str()); } // For Arcade-only builds char * GetGameHistory(int driver_index) { fullbuf.clear(); if (driver_index < 0) return ConvertToWindowsNewlines(fullbuf.c_str()); if (validate_datfiles()) { std::string t = dir_get_value(23); char buf[t.size()+1]; strcpy(buf, t.c_str()); // only want first path const char* datsdir = strtok(buf, ";"); if (datsdir && osd::directory::open(datsdir)) { for (int filenum = 0; filenum < MAX_HFILES; filenum++) { fullbuf.append(load_gameinfo(&driver_list::driver(driver_index), datsdir, filenum)); fullbuf.append(load_sourceinfo(&driver_list::driver(driver_index), datsdir, filenum)); } } else fullbuf = "\nThe path to your dat files is invalid.\n\n\n"; } else fullbuf = "\nUnable to display info due to an internal error.\n\n\n"; fullbuf.append(load_driver_geninfo(&driver_list::driver(driver_index), driver_index)); return ConvertToWindowsNewlines(fullbuf.c_str()); }