diff options
Diffstat (limited to 'docs/release/src/frontend')
-rw-r--r-- | docs/release/src/frontend/mame/audit.cpp | 780 | ||||
-rw-r--r-- | docs/release/src/frontend/mame/audit.h | 178 | ||||
-rw-r--r-- | docs/release/src/frontend/mame/clifront.cpp | 1786 | ||||
-rw-r--r-- | docs/release/src/frontend/mame/language.cpp | 232 | ||||
-rw-r--r-- | docs/release/src/frontend/mame/mameopts.cpp | 210 | ||||
-rw-r--r-- | docs/release/src/frontend/mame/mameopts.h | 64 | ||||
-rw-r--r-- | docs/release/src/frontend/mame/ui/about.cpp | 266 | ||||
-rw-r--r-- | docs/release/src/frontend/mame/ui/inifile.cpp | 583 |
8 files changed, 4099 insertions, 0 deletions
diff --git a/docs/release/src/frontend/mame/audit.cpp b/docs/release/src/frontend/mame/audit.cpp new file mode 100644 index 00000000000..6f1d52f2f0f --- /dev/null +++ b/docs/release/src/frontend/mame/audit.cpp @@ -0,0 +1,780 @@ +// license:BSD-3-Clause +// copyright-holders:Aaron Giles +/*************************************************************************** + + audit.cpp + + ROM set auditing functions. + +***************************************************************************/ + +#include "emu.h" +#include "audit.h" + +#include "sound/samples.h" + +#include "emuopts.h" +#include "drivenum.h" +#include "romload.h" +#include "softlist_dev.h" + +#include "chd.h" + +#include <algorithm> + +//#define VERBOSE 1 +#define LOG_OUTPUT_FUNC osd_printf_verbose +#include "logmacro.h" + + +namespace { + +struct parent_rom +{ + parent_rom(device_type t, rom_entry const *r) : type(t), name(r->name()), hashes(r->hashdata()), length(rom_file_size(r)) { } + + std::reference_wrapper<std::remove_reference_t<device_type> > type; + std::string name; + util::hash_collection hashes; + uint64_t length; +}; + + +class parent_rom_vector : public std::vector<parent_rom> +{ +public: + using std::vector<parent_rom>::vector; + + void remove_redundant_parents() + { + while (!empty()) + { + // find where the next parent starts + auto const last( + std::find_if( + std::next(cbegin()), + cend(), + [this] (parent_rom const &r) { return &front().type.get() != &r.type.get(); })); + + // examine dumped ROMs in this generation + for (auto i = cbegin(); last != i; ++i) + { + if (!i->hashes.flag(util::hash_collection::FLAG_NO_DUMP)) + { + auto const match( + std::find_if( + last, + cend(), + [&i] (parent_rom const &r) { return (i->length == r.length) && (i->hashes == r.hashes); })); + if (cend() == match) + return; + } + } + erase(cbegin(), last); + } + } + + std::add_pointer_t<device_type> find_shared_device(device_t ¤t, std::string_view name, util::hash_collection const &hashes, uint64_t length) const + { + // if we're examining a child device, it will always have a perfect match + if (current.owner()) + return ¤t.type(); + + // scan backwards through parents for a matching definition + bool const dumped(!hashes.flag(util::hash_collection::FLAG_NO_DUMP)); + std::add_pointer_t<device_type> best(nullptr); + for (const_reverse_iterator it = crbegin(); crend() != it; ++it) + { + if (it->length == length) + { + if (dumped) + { + if (it->hashes == hashes) + return &it->type.get(); + } + else if (it->name == name) + { + if (it->hashes.flag(util::hash_collection::FLAG_NO_DUMP)) + return &it->type.get(); + else if (!best) + best = &it->type.get(); + } + } + } + return best; + } + + std::pair<std::add_pointer_t<device_type>, bool> actual_matches_shared(device_t ¤t, media_auditor::audit_record const &record) + { + // no result if no matching file was found + if ((record.status() != media_auditor::audit_status::GOOD) && (record.status() != media_auditor::audit_status::FOUND_INVALID)) + return std::make_pair(nullptr, false); + + // if we're examining a child device, scan it first + bool matches_device_undumped(false); + if (current.owner()) + { + for (const rom_entry *region = rom_first_region(current); region; region = rom_next_region(region)) + { + for (const rom_entry *rom = rom_first_file(region); rom; rom = rom_next_file(rom)) + { + if (rom_file_size(rom) == record.actual_length()) + { + util::hash_collection const hashes(rom->hashdata()); + if (hashes == record.actual_hashes()) + return std::make_pair(¤t.type(), empty()); + else if (hashes.flag(util::hash_collection::FLAG_NO_DUMP) && (rom->name() == record.name())) + matches_device_undumped = true; + } + } + } + } + + // look for a matching parent ROM + std::add_pointer_t<device_type> closest_bad(nullptr); + for (const_reverse_iterator it = crbegin(); crend() != it; ++it) + { + if (it->length == record.actual_length()) + { + if (it->hashes == record.actual_hashes()) + return std::make_pair(&it->type.get(), it->type.get() == front().type.get()); + else if (it->hashes.flag(util::hash_collection::FLAG_NO_DUMP) && (it->name == record.name())) + closest_bad = &it->type.get(); + } + } + + // fall back to the nearest bad dump + if (closest_bad) + return std::make_pair(closest_bad, front().type.get() == *closest_bad); + else if (matches_device_undumped) + return std::make_pair(¤t.type(), empty()); + else + return std::make_pair(nullptr, false); + } +}; + +} // anonymous namespace + + + +//************************************************************************** +// CORE FUNCTIONS +//************************************************************************** + +//------------------------------------------------- +// media_auditor - constructor +//------------------------------------------------- + +media_auditor::media_auditor(const driver_enumerator &enumerator) + : m_enumerator(enumerator) + , m_validation(AUDIT_VALIDATE_FULL) +{ +} + + +//------------------------------------------------- +// audit_media - audit the media described by the +// currently-enumerated driver +//------------------------------------------------- + +media_auditor::summary media_auditor::audit_media(const char *validation) +{ + // start fresh + m_record_list.clear(); + + // store validation for later + m_validation = validation; + + // first walk the parent chain for required ROMs + parent_rom_vector parentroms; + for (auto drvindex = m_enumerator.find(m_enumerator.driver().parent); 0 <= drvindex; drvindex = m_enumerator.find(m_enumerator.driver(drvindex).parent)) + { + game_driver const &parent(m_enumerator.driver(drvindex)); + LOG("Checking parent %s for ROM files\n", parent.type.shortname()); + std::vector<rom_entry> const roms(rom_build_entries(parent.rom)); + for (rom_entry const *region = rom_first_region(&roms.front()); region; region = rom_next_region(region)) + { + for (rom_entry const *rom = rom_first_file(region); rom; rom = rom_next_file(rom)) + { + LOG("Adding parent ROM %s\n", rom->name()); + parentroms.emplace_back(parent.type, rom); + } + } + } + parentroms.remove_redundant_parents(); + + // count ROMs required/found + std::size_t found(0); + std::size_t required(0); + std::size_t shared_found(0); + std::size_t shared_required(0); + std::size_t parent_found(0); + + // iterate over devices and regions + std::vector<std::string> searchpath; + for (device_t &device : device_enumerator(m_enumerator.config()->root_device())) + { + searchpath.clear(); + + // now iterate over regions and ROMs within + 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)) + { + if (searchpath.empty()) + { + LOG("Audit media for device %s(%s)\n", device.shortname(), device.tag()); + searchpath = device.searchpath(); + } + + // look for a matching parent or device ROM + std::string const &name(rom->name()); + util::hash_collection const hashes(rom->hashdata()); + bool const dumped(!hashes.flag(util::hash_collection::FLAG_NO_DUMP)); + std::add_pointer_t<device_type> const shared_device(parentroms.find_shared_device(device, name, hashes, rom_file_size(rom))); + if (shared_device) + LOG("File '%s' %s%sdumped shared with %s\n", name, ROM_ISOPTIONAL(rom) ? "optional " : "", dumped ? "" : "un", shared_device->shortname()); + else + LOG("File '%s' %s%sdumped\n", name, ROM_ISOPTIONAL(rom) ? "optional " : "", dumped ? "" : "un"); + + // count the number of files with hashes + if (dumped && !ROM_ISOPTIONAL(rom)) + { + required++; + if (shared_device) + shared_required++; + } + + audit_record *record(nullptr); + if (ROMREGION_ISROMDATA(region)) + record = &audit_one_rom(searchpath, rom); + else if (ROMREGION_ISDISKDATA(region)) + record = &audit_one_disk(rom, device); + + if (record) + { + // see if the actual content found belongs to a parent + auto const matchesshared(parentroms.actual_matches_shared(device, *record)); + if (matchesshared.first) + LOG("Actual ROM file shared with %sparent %s\n", matchesshared.second ? "immediate " : "", matchesshared.first->shortname()); + + // count the number of files that are found. + if ((record->status() == audit_status::GOOD) || ((record->status() == audit_status::FOUND_INVALID) && !matchesshared.first)) + { + found++; + if (shared_device) + shared_found++; + if (matchesshared.second) + parent_found++; + } + + record->set_shared_device(shared_device); + } + } + } + + if (!searchpath.empty()) + LOG("Total required=%u (shared=%u) found=%u (shared=%u parent=%u)\n", required, shared_required, found, shared_found, parent_found); + } + + // if we only find files that are in the parent & either the set has no unique files or the parent is not found, then assume we don't have the set at all + if ((found == shared_found) && required && ((required != shared_required) || !parent_found)) + { + m_record_list.clear(); + return NOTFOUND; + } + + // return a summary + return summarize(m_enumerator.driver().name); +} + + +//------------------------------------------------- +// audit_device - audit the device +//------------------------------------------------- + +media_auditor::summary media_auditor::audit_device(device_t &device, const char *validation) +{ + // start fresh + m_record_list.clear(); + + // store validation for later + m_validation = validation; + + std::size_t found = 0; + std::size_t required = 0; + + std::vector<std::string> searchpath; + audit_regions( + [this, &device, &searchpath] (rom_entry const *region, rom_entry const *rom) -> audit_record const * + { + if (ROMREGION_ISROMDATA(region)) + { + if (searchpath.empty()) + searchpath = device.searchpath(); + return &audit_one_rom(searchpath, rom); + } + else if (ROMREGION_ISDISKDATA(region)) + { + return &audit_one_disk(rom, device); + } + else + { + return nullptr; + } + }, + rom_first_region(device), + found, + required); + + if ((found == 0) && (required > 0)) + { + m_record_list.clear(); + return NOTFOUND; + } + + // return a summary + return summarize(device.shortname()); +} + + +//------------------------------------------------- +// audit_software +//------------------------------------------------- +media_auditor::summary media_auditor::audit_software(software_list_device &swlist, const software_info &swinfo, const char *validation) +{ + // start fresh + m_record_list.clear(); + + // store validation for later + m_validation = validation; + + std::size_t found = 0; + std::size_t required = 0; + + // now iterate over software parts + std::vector<std::string> searchpath; + auto const do_audit = + [this, &swlist, &swinfo, &searchpath] (rom_entry const *region, rom_entry const *rom) -> audit_record const * + { + if (ROMREGION_ISROMDATA(region)) + { + if (searchpath.empty()) + searchpath = rom_load_manager::get_software_searchpath(swlist, swinfo); + return &audit_one_rom(searchpath, rom); + } + else if (ROMREGION_ISDISKDATA(region)) + { + return &audit_one_disk(rom, swlist, swinfo); + } + else + { + return nullptr; + } + }; + for (const software_part &part : swinfo.parts()) + audit_regions(do_audit, part.romdata().data(), found, required); + + if ((found == 0) && (required > 0)) + { + m_record_list.clear(); + return NOTFOUND; + } + + // return a summary + return summarize(swlist.list_name().c_str()); +} + + +//------------------------------------------------- +// audit_samples - validate the samples for the +// currently-enumerated driver +//------------------------------------------------- + +media_auditor::summary media_auditor::audit_samples() +{ + // start fresh + m_record_list.clear(); + + std::size_t required = 0; + std::size_t found = 0; + + // iterate over sample entries + for (samples_device &device : samples_device_enumerator(m_enumerator.config()->root_device())) + { + // by default we just search using the driver name + std::string searchpath(m_enumerator.driver().name); + + // add the alternate path if present + samples_iterator iter(device); + if (iter.altbasename() != nullptr) + searchpath.append(";").append(iter.altbasename()); + + // iterate over samples in this entry + for (const char *samplename = iter.first(); samplename; samplename = iter.next()) + { + required++; + + // create a new record + audit_record &record = *m_record_list.emplace(m_record_list.end(), samplename, media_type::SAMPLE); + + // look for the files + emu_file file(m_enumerator.options().sample_path(), OPEN_FLAG_READ | OPEN_FLAG_NO_PRELOAD); + path_iterator path(searchpath); + std::string curpath; + while (path.next(curpath, samplename)) + { + // attempt to access the file (.flac) or (.wav) + std::error_condition filerr = file.open(curpath + ".flac"); + if (filerr) + filerr = file.open(curpath + ".wav"); + + if (!filerr) + { + record.set_status(audit_status::GOOD, audit_substatus::GOOD); + found++; + } + else + { + record.set_status(audit_status::NOT_FOUND, audit_substatus::NOT_FOUND); + } + } + } + } + + if ((found == 0) && (required > 0)) + { + m_record_list.clear(); + return NOTFOUND; + } + + // return a summary + return summarize(m_enumerator.driver().name); +} + + +//------------------------------------------------- +// summary - generate a summary, with an optional +// string format +//------------------------------------------------- + +media_auditor::summary media_auditor::summarize(const char *name, std::ostream *output) const +{ + if (m_record_list.empty()) + return NONE_NEEDED; + + // loop over records + summary overall_status = CORRECT; + for (audit_record const &record : m_record_list) + { + // skip anything that's fine + if (record.substatus() == audit_substatus::GOOD) + continue; + + // output the game name, file name, and length (if applicable) + if (output) + { + if (name) + util::stream_format(*output, "%-12s: %s", name, record.name()); + else + util::stream_format(*output, "%s", record.name()); + if (record.expected_length() > 0) + util::stream_format(*output, " (%d bytes)", record.expected_length()); + *output << " - "; + } + + // use the substatus for finer details + summary best_new_status = INCORRECT; + switch (record.substatus()) + { + case audit_substatus::GOOD_NEEDS_REDUMP: + if (output) *output << "NEEDS REDUMP\n"; + best_new_status = BEST_AVAILABLE; + break; + + case audit_substatus::FOUND_NODUMP: + if (output) *output << "NO GOOD DUMP KNOWN\n"; + best_new_status = BEST_AVAILABLE; + break; + + case audit_substatus::FOUND_BAD_CHECKSUM: + if (output) + { + util::stream_format(*output, "INCORRECT CHECKSUM:\n"); + util::stream_format(*output, "EXPECTED: %s\n", record.expected_hashes().macro_string()); + util::stream_format(*output, " FOUND: %s\n", record.actual_hashes().macro_string()); + } + break; + + case audit_substatus::FOUND_WRONG_LENGTH: + if (output) util::stream_format(*output, "INCORRECT LENGTH: %d bytes\n", record.actual_length()); + break; + + case audit_substatus::NOT_FOUND: + if (output) + { + std::add_pointer_t<device_type> const shared_device = record.shared_device(); + if (shared_device) + util::stream_format(*output, "NOT FOUND (%s)\n", shared_device->shortname()); + else + util::stream_format(*output, "NOT FOUND\n"); + } + break; + + case audit_substatus::NOT_FOUND_NODUMP: + if (output) *output << "NOT FOUND - NO GOOD DUMP KNOWN\n"; + best_new_status = BEST_AVAILABLE; + break; + + case audit_substatus::NOT_FOUND_OPTIONAL: + if (output) *output << "NOT FOUND BUT OPTIONAL\n"; + best_new_status = BEST_AVAILABLE; + break; + + default: + assert(false); + } + + // downgrade the overall status if necessary + overall_status = (std::max)(overall_status, best_new_status); + } + return overall_status; +} + + +//------------------------------------------------- +// audit_regions - validate/count for regions +//------------------------------------------------- + +template <typename T> +void media_auditor::audit_regions(T do_audit, const rom_entry *region, std::size_t &found, std::size_t &required) +{ + // now iterate over regions + std::vector<std::string> searchpath; + for ( ; region; region = rom_next_region(region)) + { + // now iterate over rom definitions + for (rom_entry const *rom = rom_first_file(region); rom; rom = rom_next_file(rom)) + { + // count the number of files with hashes + util::hash_collection const hashes(rom->hashdata()); + if (!hashes.flag(util::hash_collection::FLAG_NO_DUMP) && !ROM_ISOPTIONAL(rom)) + required++; + + audit_record const *const record = do_audit(region, rom); + + // count the number of files that are found. + if (record && ((record->status() == audit_status::GOOD) || (record->status() == audit_status::FOUND_INVALID))) + found++; + } + } +} + + +//------------------------------------------------- +// audit_one_rom - validate a single ROM entry +//------------------------------------------------- + +media_auditor::audit_record &media_auditor::audit_one_rom(const std::vector<std::string> &searchpath, const rom_entry *rom) +{ + // allocate and append a new record + audit_record &record = *m_record_list.emplace(m_record_list.end(), *rom, media_type::ROM); + + // see if we have a CRC and extract it if so + uint32_t crc = 0; + bool const has_crc = record.expected_hashes().crc(crc); + + // find the file and checksum it, getting the file length along the way + emu_file file(m_enumerator.options().media_path(), searchpath, OPEN_FLAG_READ | OPEN_FLAG_NO_PRELOAD); + file.set_restrict_to_mediapath(1); + + // open the file if we can + std::error_condition filerr; + if (has_crc) + filerr = file.open(record.name(), crc); + else + filerr = file.open(record.name()); + + // if it worked, get the actual length and hashes, then stop + if (!filerr) + record.set_actual(file.hashes(m_validation), file.size()); + + // compute the final status + compute_status(record, rom, record.actual_length() != 0); + return record; +} + + +//------------------------------------------------- +// audit_one_disk - validate a single disk entry +//------------------------------------------------- + +template <typename... T> +media_auditor::audit_record &media_auditor::audit_one_disk(const rom_entry *rom, T &&... args) +{ + // allocate and append a new record + audit_record &record = *m_record_list.emplace(m_record_list.end(), *rom, media_type::DISK); + + // open the disk + chd_file source; + const std::error_condition err = rom_load_manager::open_disk_image(m_enumerator.options(), std::forward<T>(args)..., rom, source); + + // if we succeeded, get the hashes + if (!err) + { + util::hash_collection hashes; + + // if there's a SHA1 hash, add them to the output hash + if (source.sha1() != util::sha1_t::null) + hashes.add_sha1(source.sha1()); + + // update the actual values + record.set_actual(hashes); + } + + // compute the final status + compute_status(record, rom, !err); + return record; +} + + +//------------------------------------------------- +// compute_status - compute a detailed status +// based on the information we have +//------------------------------------------------- + +void media_auditor::compute_status(audit_record &record, const rom_entry *rom, bool found) +{ + // if not found, provide more details + if (!found) + { + if (record.expected_hashes().flag(util::hash_collection::FLAG_NO_DUMP)) + record.set_status(audit_status::NOT_FOUND, audit_substatus::NOT_FOUND_NODUMP); + else if (ROM_ISOPTIONAL(rom)) + record.set_status(audit_status::NOT_FOUND, audit_substatus::NOT_FOUND_OPTIONAL); + else + record.set_status(audit_status::NOT_FOUND, audit_substatus::NOT_FOUND); + } + else + { + if (record.expected_length() != record.actual_length()) + record.set_status(audit_status::FOUND_INVALID, audit_substatus::FOUND_WRONG_LENGTH); + else if (record.expected_hashes().flag(util::hash_collection::FLAG_NO_DUMP)) + record.set_status(audit_status::GOOD, audit_substatus::FOUND_NODUMP); + else if (record.expected_hashes() != record.actual_hashes()) + record.set_status(audit_status::FOUND_INVALID, audit_substatus::FOUND_BAD_CHECKSUM); + else if (record.expected_hashes().flag(util::hash_collection::FLAG_BAD_DUMP)) + record.set_status(audit_status::GOOD, audit_substatus::GOOD_NEEDS_REDUMP); + else + record.set_status(audit_status::GOOD, audit_substatus::GOOD); + } +} + + +//------------------------------------------------- +// audit_record - constructor +//------------------------------------------------- + +media_auditor::audit_record::audit_record(const rom_entry &media, media_type type) + : m_type(type) + , m_status(audit_status::UNVERIFIED) + , m_substatus(audit_substatus::UNVERIFIED) + , m_name(media.name()) + , m_explength(rom_file_size(&media)) + , m_length(0) + , m_exphashes(media.hashdata()) + , m_hashes() + , m_shared_device(nullptr) +{ +} + +media_auditor::audit_record::audit_record(const char *name, media_type type) + : m_type(type) + , m_status(audit_status::UNVERIFIED) + , m_substatus(audit_substatus::UNVERIFIED) + , m_name(name) + , m_explength(0) + , m_length(0) + , m_exphashes() + , m_hashes() + , m_shared_device(nullptr) +{ +} + + + +// MESSUI - only report problems that the user can fix +media_auditor::summary media_auditor::winui_summarize(const char *name, std::string *output) +{ + if (m_record_list.empty()) + return NONE_NEEDED; + + // loop over records + summary overall_status = CORRECT; + for (audit_record const &record : m_record_list) + { + summary best_new_status = INCORRECT; + + // skip anything that's fine + if ( (record.substatus() == audit_substatus::GOOD) + || (record.substatus() == audit_substatus::GOOD_NEEDS_REDUMP) + || (record.substatus() == audit_substatus::NOT_FOUND_NODUMP) + || (record.substatus() == audit_substatus::FOUND_NODUMP) + ) + continue; + + // output the game name, file name, and length (if applicable) + //if (output) + { + output->append(string_format("%-12s: %s", name, record.name())); + if (record.expected_length() > 0) + output->append(string_format(" (%d bytes)", record.expected_length())); + output->append(" - "); + } + + // use the substatus for finer details + switch (record.substatus()) + { + case audit_substatus::FOUND_NODUMP: + if (output) output->append("NO GOOD DUMP KNOWN\n"); + best_new_status = BEST_AVAILABLE; + break; + + case audit_substatus::FOUND_BAD_CHECKSUM: + if (output) + { + output->append("INCORRECT CHECKSUM:\n"); + output->append(string_format("EXPECTED: %s\n", record.expected_hashes().macro_string().c_str())); + output->append(string_format(" FOUND: %s\n", record.actual_hashes().macro_string().c_str())); + } + break; + + case audit_substatus::FOUND_WRONG_LENGTH: + if (output) output->append(string_format("INCORRECT LENGTH: %d bytes\n", record.actual_length())); + break; + + case audit_substatus::NOT_FOUND: + if (output) + { + std::add_pointer_t<device_type> const shared_device = record.shared_device(); + if (shared_device == NULL) + output->append("NOT FOUND\n"); + else + output->append(string_format("NOT FOUND (%s)\n", shared_device->shortname())); + } + break; + + case audit_substatus::NOT_FOUND_OPTIONAL: + if (output) output->append("NOT FOUND BUT OPTIONAL\n"); + best_new_status = BEST_AVAILABLE; + break; + + default: + break; + } + + // downgrade the overall status if necessary + overall_status = (std::max)(overall_status, best_new_status); + } + return overall_status; +} diff --git a/docs/release/src/frontend/mame/audit.h b/docs/release/src/frontend/mame/audit.h new file mode 100644 index 00000000000..e1857de5cdf --- /dev/null +++ b/docs/release/src/frontend/mame/audit.h @@ -0,0 +1,178 @@ +// license:BSD-3-Clause +// copyright-holders:Aaron Giles +/*************************************************************************** + + audit.h + + ROM, disk, and sample auditing functions. + +***************************************************************************/ +#ifndef MAME_FRONTEND_AUDIT_H +#define MAME_FRONTEND_AUDIT_H + +#pragma once + +#include <iosfwd> +#include <list> +#include <utility> + + + +//************************************************************************** +// CONSTANTS +//************************************************************************** + +// hashes to use for validation +#define AUDIT_VALIDATE_FAST "R" /* CRC only */ +#define AUDIT_VALIDATE_FULL "RS" /* CRC + SHA1 */ + + + +//************************************************************************** +// TYPE DEFINITIONS +//************************************************************************** + + +// forward declarations +class driver_enumerator; +class software_list_device; + + + +// ======================> media_auditor + +// class which manages auditing of items +class media_auditor +{ +public: + enum class media_type + { + ROM = 0, + DISK, + SAMPLE + }; + + // status values + enum class audit_status + { + GOOD = 0, + FOUND_INVALID, + NOT_FOUND, + UNVERIFIED = 100 + }; + + // substatus values + enum class audit_substatus + { + GOOD = 0, + GOOD_NEEDS_REDUMP, + FOUND_NODUMP, + FOUND_BAD_CHECKSUM, + FOUND_WRONG_LENGTH, + NOT_FOUND, + NOT_FOUND_NODUMP, + NOT_FOUND_OPTIONAL, + UNVERIFIED = 100 + }; + + // summary values + enum summary + { + CORRECT = 0, + NONE_NEEDED, + BEST_AVAILABLE, + INCORRECT, + NOTFOUND + }; + + // holds the result of auditing a single item + class audit_record + { + public: + // media types + // construction/destruction + audit_record(const rom_entry &media, media_type type); + audit_record(const char *name, media_type type); + audit_record(const audit_record &) = default; + audit_record(audit_record &&) = default; + audit_record &operator=(const audit_record &) = default; + audit_record &operator=(audit_record &&) = default; + + // getters + media_type type() const { return m_type; } + audit_status status() const { return m_status; } + audit_substatus substatus() const { return m_substatus; } + const std::string &name() const { return m_name; } + uint64_t expected_length() const { return m_explength; } + uint64_t actual_length() const { return m_length; } + const util::hash_collection &expected_hashes() const { return m_exphashes; } + const util::hash_collection &actual_hashes() const { return m_hashes; } + std::add_pointer_t<device_type> shared_device() const { return m_shared_device; } + + // setters + void set_status(audit_status status, audit_substatus substatus) + { + m_status = status; + m_substatus = substatus; + } + + void set_actual(const util::hash_collection &hashes, uint64_t length = 0) + { + m_hashes = hashes; + m_length = length; + } + + void set_actual(util::hash_collection &&hashes, uint64_t length = 0) + { + m_hashes = std::move(hashes); + m_length = length; + } + + void set_shared_device(std::add_pointer_t<device_type> shared_device) + { + m_shared_device = shared_device; + } + + private: + // internal state + media_type m_type; // type of item that was audited + audit_status m_status; // status of audit on this item + audit_substatus m_substatus; // finer-detail status + std::string m_name; // name of item + uint64_t m_explength; // expected length of item + uint64_t m_length; // actual length of item + util::hash_collection m_exphashes; // expected hash data + util::hash_collection m_hashes; // actual hash information + std::add_pointer_t<device_type> m_shared_device; // device that shares the ROM + }; + using record_list = std::list<audit_record>; + + // construction/destruction + media_auditor(const driver_enumerator &enumerator); + + // getters + const record_list &records() const { return m_record_list; } + + // audit operations + summary audit_media(const char *validation = AUDIT_VALIDATE_FULL); + summary audit_device(device_t &device, const char *validation = AUDIT_VALIDATE_FULL); + summary audit_software(software_list_device &swlist, const software_info &swinfo, const char *validation = AUDIT_VALIDATE_FULL); + summary audit_samples(); + summary summarize(const char *name, std::ostream *output = nullptr) const; + summary winui_summarize(const char *name, std::string *output = nullptr); //WINUI - only report problems that the user can fix + +private: + // internal helpers + template <typename T> void audit_regions(T do_audit, const rom_entry *region, std::size_t &found, std::size_t &required); + audit_record &audit_one_rom(const std::vector<std::string> &searchpath, const rom_entry *rom); + template <typename... T> audit_record &audit_one_disk(const rom_entry *rom, T &&... args); + void compute_status(audit_record &record, const rom_entry *rom, bool found); + + // internal state + record_list m_record_list; + const driver_enumerator & m_enumerator; + const char * m_validation; +}; + + +#endif // MAME_FRONTEND_AUDIT_H diff --git a/docs/release/src/frontend/mame/clifront.cpp b/docs/release/src/frontend/mame/clifront.cpp new file mode 100644 index 00000000000..c2115c943cd --- /dev/null +++ b/docs/release/src/frontend/mame/clifront.cpp @@ -0,0 +1,1786 @@ +// license:BSD-3-Clause +// copyright-holders:Aaron Giles +/*************************************************************************** + + clifront.cpp + + Command-line interface frontend for MAME. + +***************************************************************************/ + +#include "emu.h" +#include "clifront.h" + +#include "ui/moptions.h" + +#include "audit.h" +#include "infoxml.h" +#include "language.h" +#include "luaengine.h" +#include "mame.h" +#include "mameopts.h" +#include "media_ident.h" +#include "pluginopts.h" + +#include "emuopts.h" +#include "romload.h" +#include "softlist_dev.h" +#include "validity.h" +#include "sound/samples.h" + +#include "chd.h" +#include "corestr.h" +#include "unzip.h" +#include "xmlfile.h" + +#include "osdepend.h" + +#include <algorithm> +#include <new> +#include <set> +#include <tuple> +#include <cctype> + + +//************************************************************************** +// CONSTANTS +//************************************************************************** + +// core commands +#define CLICOMMAND_HELP "help" +#define CLICOMMAND_VALIDATE "validate" + +// configuration commands +#define CLICOMMAND_CREATECONFIG "createconfig" +#define CLICOMMAND_SHOWCONFIG "showconfig" +#define CLICOMMAND_SHOWUSAGE "showusage" + +// frontend commands +#define CLICOMMAND_LISTXML "listxml" +#define CLICOMMAND_LISTFULL "listfull" +#define CLICOMMAND_LISTSOURCE "listsource" +#define CLICOMMAND_LISTCLONES "listclones" +#define CLICOMMAND_LISTBROTHERS "listbrothers" +#define CLICOMMAND_LISTCRC "listcrc" +#define CLICOMMAND_LISTROMS "listroms" +#define CLICOMMAND_LISTSAMPLES "listsamples" +#define CLICOMMAND_VERIFYROMS "verifyroms" +#define CLICOMMAND_VERIFYSAMPLES "verifysamples" +#define CLICOMMAND_ROMIDENT "romident" +#define CLICOMMAND_LISTDEVICES "listdevices" +#define CLICOMMAND_LISTSLOTS "listslots" +#define CLICOMMAND_LISTMEDIA "listmedia" +#define CLICOMMAND_LISTSOFTWARE "listsoftware" +#define CLICOMMAND_VERIFYSOFTWARE "verifysoftware" +#define CLICOMMAND_GETSOFTLIST "getsoftlist" +#define CLICOMMAND_VERIFYSOFTLIST "verifysoftlist" +#define CLICOMMAND_VERSION "version" + +// command options +#define CLIOPTION_DTD "dtd" + + +namespace { +//************************************************************************** +// COMMAND-LINE OPTIONS +//************************************************************************** + +const options_entry cli_option_entries[] = +{ + /* core commands */ + { nullptr, nullptr, OPTION_HEADER, "CORE COMMANDS" }, + { CLICOMMAND_HELP ";h;?", "0", OPTION_COMMAND, "show help message" }, + { CLICOMMAND_VALIDATE ";valid", "0", OPTION_COMMAND, "perform validation on system drivers and devices" }, + + /* configuration commands */ + { nullptr, nullptr, OPTION_HEADER, "CONFIGURATION COMMANDS" }, + { CLICOMMAND_CREATECONFIG ";cc", "0", OPTION_COMMAND, "create the default configuration file" }, + { CLICOMMAND_SHOWCONFIG ";sc", "0", OPTION_COMMAND, "display running parameters" }, + { CLICOMMAND_SHOWUSAGE ";su", "0", OPTION_COMMAND, "show this help" }, + + /* frontend commands */ + { nullptr, nullptr, OPTION_HEADER, "FRONTEND COMMANDS" }, + { CLICOMMAND_LISTXML ";lx", "0", OPTION_COMMAND, "all available info on driver in XML format" }, + { CLICOMMAND_LISTFULL ";ll", "0", OPTION_COMMAND, "short name, full name" }, + { CLICOMMAND_LISTSOURCE ";ls", "0", OPTION_COMMAND, "driver sourcefile" }, + { CLICOMMAND_LISTCLONES ";lc", "0", OPTION_COMMAND, "show clones" }, + { CLICOMMAND_LISTBROTHERS ";lb", "0", OPTION_COMMAND, "show \"brothers\", or other drivers from same sourcefile" }, + { CLICOMMAND_LISTCRC, "0", OPTION_COMMAND, "CRC-32s" }, + { CLICOMMAND_LISTROMS ";lr", "0", OPTION_COMMAND, "list required ROMs for a driver" }, + { CLICOMMAND_LISTSAMPLES, "0", OPTION_COMMAND, "list optional samples for a driver" }, + { CLICOMMAND_VERIFYROMS, "0", OPTION_COMMAND, "report romsets that have problems" }, + { CLICOMMAND_VERIFYSAMPLES, "0", OPTION_COMMAND, "report samplesets that have problems" }, + { CLICOMMAND_ROMIDENT, "0", OPTION_COMMAND, "compare files with known MAME ROMs" }, + { CLICOMMAND_LISTDEVICES ";ld", "0", OPTION_COMMAND, "list available devices" }, + { CLICOMMAND_LISTSLOTS ";lslot", "0", OPTION_COMMAND, "list available slots and slot devices" }, + { CLICOMMAND_LISTMEDIA ";lm", "0", OPTION_COMMAND, "list available media for the system" }, + { CLICOMMAND_LISTSOFTWARE ";lsoft", "0", OPTION_COMMAND, "list known software for the system" }, + { CLICOMMAND_VERIFYSOFTWARE ";vsoft", "0", OPTION_COMMAND, "verify known software for the system" }, + { CLICOMMAND_GETSOFTLIST ";glist", "0", OPTION_COMMAND, "retrieve software list by name" }, + { CLICOMMAND_VERIFYSOFTLIST ";vlist", "0", OPTION_COMMAND, "verify software list by name" }, + { CLICOMMAND_VERSION, "0", OPTION_COMMAND, "get MAME version" }, + + { nullptr, nullptr, OPTION_HEADER, "FRONTEND COMMAND OPTIONS" }, + { CLIOPTION_DTD, "1", OPTION_BOOLEAN, "include DTD in XML output" }, + { nullptr } +}; + + +void print_summary( + const media_auditor &auditor, media_auditor::summary summary, bool record_none_needed, + const char *type, const char *name, const char *parent, + unsigned &correct, unsigned &incorrect, unsigned ¬found, + util::ovectorstream &buffer) +{ + if (summary == media_auditor::NOTFOUND) + { + // if not found, count that and leave it at that + ++notfound; + } + else if (record_none_needed || (summary != media_auditor::NONE_NEEDED)) + { + // output the summary of the audit + buffer.clear(); + buffer.seekp(0); + auditor.summarize(name, &buffer); + buffer.put('\0'); + osd_printf_info("%s", &buffer.vec()[0]); + + // output the name of the driver and its parent + osd_printf_info("%sset %s ", type, name); + if (parent) + osd_printf_info("[%s] ", parent); + + // switch off of the result + switch (summary) + { + case media_auditor::INCORRECT: + osd_printf_info("is bad\n"); + ++incorrect; + return; + + case media_auditor::CORRECT: + osd_printf_info("is good\n"); + ++correct; + return; + + case media_auditor::BEST_AVAILABLE: + case media_auditor::NONE_NEEDED: + osd_printf_info("is best available\n"); + ++correct; + return; + + case media_auditor::NOTFOUND: + osd_printf_info("not found\n"); + return; + } + assert(false); + osd_printf_error("has unknown status (%u)\n", unsigned(summary)); + } +} + +} // anonymous namespace + + +//************************************************************************** +// CLI FRONTEND +//************************************************************************** + +//------------------------------------------------- +// cli_frontend - constructor +//------------------------------------------------- + +cli_frontend::cli_frontend(emu_options &options, osd_interface &osd) + : m_options(options) + , m_osd(osd) + , m_result(EMU_ERR_NONE) +{ + m_options.add_entries(cli_option_entries); +} + + +//------------------------------------------------- +// ~cli_frontend - destructor +//------------------------------------------------- + +cli_frontend::~cli_frontend() +{ +} + +void cli_frontend::start_execution(mame_machine_manager *manager, const std::vector<std::string> &args) +{ + std::ostringstream option_errors; + + // because softlist evaluation relies on hashpath being populated, we are going to go through + // a special step to force it to be evaluated + mame_options::populate_hashpath_from_args_and_inis(m_options, args); + + // parse the command line, adding any system-specific options + try + { + m_options.parse_command_line(args, OPTION_PRIORITY_CMDLINE); + } + catch (options_warning_exception &ex) + { + osd_printf_error("%s", ex.message()); + } + catch (options_exception &ex) + { + // if we failed, check for no command and a system name first; in that case error on the name + if (m_options.command().empty() && mame_options::system(m_options) == nullptr && !m_options.attempted_system_name().empty()) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "Unknown system '%s'", m_options.attempted_system_name()); + + // otherwise, error on the options + throw emu_fatalerror(EMU_ERR_INVALID_CONFIG, "%s", ex.message()); + } + m_osd.set_verbose(m_options.verbose()); + + // determine the base name of the EXE + std::string_view exename = core_filename_extract_base(args[0], true); + + // if we have a command, execute that + if (!m_options.command().empty()) + { + execute_commands(exename); + return; + } + + // read INI's, if appropriate + if (m_options.read_config()) + { + mame_options::parse_standard_inis(m_options, option_errors); + m_osd.set_verbose(m_options.verbose()); + } + + // otherwise, check for a valid system + load_translation(m_options); + + manager->start_http_server(); + + manager->start_luaengine(); + + if (option_errors.tellp() > 0) + osd_printf_error("Error in command line:\n%s\n", strtrimspace(option_errors.str())); + + // if we can't find it, give an appropriate error + const game_driver *system = mame_options::system(m_options); + if (system == nullptr && *(m_options.system_name()) != 0) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "Unknown system '%s'", m_options.system_name()); + + // otherwise just run the game + m_result = manager->execute(); +} + +//------------------------------------------------- +// execute - execute a game via the standard +// command line interface +//------------------------------------------------- + +int cli_frontend::execute(std::vector<std::string> &args) +{ + // wrap the core execution in a try/catch to field all fatal errors + m_result = EMU_ERR_NONE; + mame_machine_manager *manager = mame_machine_manager::instance(m_options, m_osd); + + try + { + start_execution(manager, args); + } + // handle exceptions of various types + catch (emu_fatalerror &fatal) + { + osd_printf_error("%s\n", strtrimspace(fatal.what())); + m_result = (fatal.exitcode() != 0) ? fatal.exitcode() : EMU_ERR_FATALERROR; + + // if a game was specified, wasn't a wildcard, and our error indicates this was the + // reason for failure, offer some suggestions + if (m_result == EMU_ERR_NO_SUCH_SYSTEM + && !m_options.attempted_system_name().empty() + && !core_iswildstr(m_options.attempted_system_name().c_str()) + && mame_options::system(m_options) == nullptr) + { + // get the top 16 approximate matches + driver_enumerator drivlist(m_options); + int matches[16]; + drivlist.find_approximate_matches(m_options.attempted_system_name(), std::size(matches), matches); + + // work out how wide the titles need to be + int titlelen(0); + for (int match : matches) + if (0 <= match) + titlelen = (std::max)(titlelen, int(strlen(drivlist.driver(match).type.fullname()))); + + // print them out + osd_printf_error("\n\"%s\" approximately matches the following\n" + "supported machines (best match first):\n\n", m_options.attempted_system_name()); + for (int match : matches) + { + if (0 <= match) + { + game_driver const &drv(drivlist.driver(match)); + osd_printf_error("%-18s%-*s(%s, %s)\n", drv.name, titlelen + 2, drv.type.fullname(), drv.manufacturer, drv.year); + } + } + } + } + catch (emu_exception &) + { + osd_printf_error("Caught unhandled emulator exception\n"); + m_result = EMU_ERR_FATALERROR; + } + catch (tag_add_exception &aex) + { + osd_printf_error("Tag '%s' already exists in tagged map\n", aex.tag()); + m_result = EMU_ERR_FATALERROR; + } + catch (std::exception &ex) + { + osd_printf_error("Caught unhandled %s exception: %s\n", typeid(ex).name(), ex.what()); + m_result = EMU_ERR_FATALERROR; + } + catch (...) + { + osd_printf_error("Caught unhandled exception\n"); + m_result = EMU_ERR_FATALERROR; + } + + util::archive_file::cache_clear(); + delete manager; + + return m_result; +} + + +//------------------------------------------------- +// listxml - output the XML data for one or more +// games +//------------------------------------------------- + +void cli_frontend::listxml(const std::vector<std::string> &args) +{ + // create the XML and print it to stdout + info_xml_creator creator(m_options, m_options.bool_value(CLIOPTION_DTD)); + creator.output(std::cout, args); +} + + +//------------------------------------------------- +// listfull - output the name and description of +// one or more games +//------------------------------------------------- + +void cli_frontend::listfull(const std::vector<std::string> &args) +{ + auto const list_system_name = [] (device_type type, bool first) + { + // print the header + if (first) + osd_printf_info("Name: Description:\n"); + + osd_printf_info("%-17s \"%s\"\n", type.shortname(), type.fullname()); + }; + apply_action( + args, + [&list_system_name] (driver_enumerator &drivlist, bool first) + { list_system_name(drivlist.driver().type, first); }, + [&list_system_name] (device_type type, bool first) + { list_system_name(type, first); }); +} + + +//------------------------------------------------- +// listsource - output the name and source +// filename of one or more games +//------------------------------------------------- + +void cli_frontend::listsource(const std::vector<std::string> &args) +{ + auto const list_system_source = [] (device_type type) + { + osd_printf_info("%-16s %s\n", type.shortname(), core_filename_extract_base(type.source())); + }; + apply_action( + args, + [&list_system_source] (driver_enumerator &drivlist, bool first) + { list_system_source(drivlist.driver().type); }, + [&list_system_source] (device_type type, bool first) + { list_system_source(type); }); +} + + +//------------------------------------------------- +// listclones - output the name and parent of all +// clones matching the given pattern +//------------------------------------------------- + +void cli_frontend::listclones(const std::vector<std::string> &args) +{ + const char *gamename = args.empty() ? nullptr : args[0].c_str(); + + // start with a filtered list of drivers + driver_enumerator drivlist(m_options, gamename); + int const original_count = drivlist.count(); + + // iterate through the remaining ones to see if their parent matches + while (drivlist.next_excluded()) + { + // if we have a non-bios clone and it matches, keep it + int const clone_of = drivlist.clone(); + if ((clone_of >= 0) && !(drivlist.driver(clone_of).flags & machine_flags::IS_BIOS_ROOT)) + if (drivlist.matches(gamename, drivlist.driver(clone_of).name)) + drivlist.include(); + } + + // return an error if none found + if (drivlist.count() == 0) + { + // see if we match but just weren't a clone + if (original_count == 0) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "No matching systems found for '%s'", gamename); + else + osd_printf_info("Found %lu match(es) for '%s' but none were clones\n", (unsigned long)drivlist.count(), gamename); // FIXME: this never gets hit + return; + } + + // print the header + osd_printf_info("Name: Clone of:\n"); + + // iterate through drivers and output the info + drivlist.reset(); + while (drivlist.next()) + { + int clone_of = drivlist.clone(); + if ((clone_of >= 0) && !(drivlist.driver(clone_of).flags & machine_flags::IS_BIOS_ROOT)) + osd_printf_info("%-16s %s\n", drivlist.driver().name, drivlist.driver(clone_of).name); + } +} + + +//------------------------------------------------- +// listbrothers - for each matching game, output +// the list of other games that share the same +// source file +//------------------------------------------------- + +void cli_frontend::listbrothers(const std::vector<std::string> &args) +{ + const char *gamename = args.empty() ? nullptr : args[0].c_str(); + + // start with a filtered list of drivers; return an error if none found + driver_enumerator initial_drivlist(m_options, gamename); + if (initial_drivlist.count() == 0) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "No matching systems found for '%s'", gamename); + + // for the final list, start with an empty driver list + driver_enumerator drivlist(m_options); + drivlist.exclude_all(); + + // scan through the initially-selected drivers + while (initial_drivlist.next()) + { + // if we are already marked in the final list, we don't need to do anything + if (drivlist.included(initial_drivlist.current())) + continue; + + // otherwise, walk excluded items in the final list and mark any that match + drivlist.reset(); + while (drivlist.next_excluded()) + if (strcmp(drivlist.driver().type.source(), initial_drivlist.driver().type.source()) == 0) + drivlist.include(); + } + + // print the header + osd_printf_info("%-20s %-16s %s\n", "Source file:", "Name:", "Parent:"); + + // output the entries found + drivlist.reset(); + while (drivlist.next()) + { + int clone_of = drivlist.clone(); + if (clone_of != -1) + osd_printf_info("%-20s %-16s %s\n", core_filename_extract_base(drivlist.driver().type.source()), drivlist.driver().name, (clone_of == -1 ? "" : drivlist.driver(clone_of).name)); + else + osd_printf_info("%-20s %s\n", core_filename_extract_base(drivlist.driver().type.source()), drivlist.driver().name); + } +} + + +//------------------------------------------------- +// listcrc - output the CRC and name of all ROMs +// referenced by the emulator +//------------------------------------------------- + +void cli_frontend::listcrc(const std::vector<std::string> &args) +{ + apply_device_action( + args, + [] (device_t &root, char const *type, bool first) + { + for (device_t const &device : device_enumerator(root)) + { + for (tiny_rom_entry const *rom = device.rom_region(); rom && !ROMENTRY_ISEND(rom); ++rom) + { + if (ROMENTRY_ISFILE(rom)) + { + // if we have a CRC, display it + uint32_t crc; + if (util::hash_collection(rom->hashdata).crc(crc)) + osd_printf_info("%08x %-32s\t%-16s\t%s\n", crc, rom->name, device.shortname(), device.name()); + } + } + } + }); +} + + +//------------------------------------------------- +// listroms - output the list of ROMs referenced +// by matching systems/devices +//------------------------------------------------- + +void cli_frontend::listroms(const std::vector<std::string> &args) +{ + apply_device_action( + args, + [] (device_t &root, char const *type, bool first) + { + // space between items + if (!first) + osd_printf_info("\n"); + + // iterate through ROMs + std::list<std::tuple<std::string, int64_t, std::string>> entries; + std::set<std::string_view> devnames; + for (device_t const &device : device_enumerator(root)) + { + bool hasroms = false; + 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)) + { + if (!hasroms) + { + hasroms = true; + if (&device != &root) + devnames.insert(device.shortname()); + } + + // accumulate the total length of all chunks + int64_t length = -1; + if (ROMREGION_ISROMDATA(region)) + length = rom_file_size(rom); + + entries.emplace_back(rom->name(), length, rom->hashdata()); + } + } + } + + // print results + if (entries.empty()) + osd_printf_info("No ROMs required for %s \"%s\".\n", type, root.shortname()); + else + { + // print a header + osd_printf_info("ROMs required for %s \"%s\"", type, root.shortname()); + if (!devnames.empty()) + { + osd_printf_info(" (including device%s", devnames.size() > 1 ? "s" : ""); + bool first = true; + for (const std::string_view &devname : devnames) + { + if (first) + first = false; + else + osd_printf_info(","); + osd_printf_info(" \"%s\"", devname); + } + osd_printf_info(")"); + } + osd_printf_info(".\n%-32s %10s %s\n", "Name", "Size", "Checksum"); + + for (auto &entry : entries) + { + // start with the name + osd_printf_info("%-32s ", std::get<0>(entry)); + + // output the length next + int64_t length = std::get<1>(entry); + if (length >= 0) + osd_printf_info("%10u", unsigned(uint64_t(length))); + else + osd_printf_info("%10s", ""); + + // output the hash data + util::hash_collection hashes(std::get<2>(entry)); + if (!hashes.flag(util::hash_collection::FLAG_NO_DUMP)) + { + if (hashes.flag(util::hash_collection::FLAG_BAD_DUMP)) + osd_printf_info(" BAD"); + osd_printf_info(" %s", hashes.macro_string()); + } + else + osd_printf_info(" NO GOOD DUMP KNOWN"); + + // end with a CR + osd_printf_info("\n"); + } + } + }); +} + + +//------------------------------------------------- +// listsamples - output the list of samples +// referenced by a given game or set of games +//------------------------------------------------- + +void cli_frontend::listsamples(const std::vector<std::string> &args) +{ + const char *gamename = args.empty() ? nullptr : args[0].c_str(); + + // determine which drivers to output; return an error if none found + driver_enumerator drivlist(m_options, gamename); + if (drivlist.count() == 0) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "No matching systems found for '%s'", gamename); + + // iterate over drivers, looking for SAMPLES devices + bool first = true; + while (drivlist.next()) + { + // see if we have samples + samples_device_enumerator iter(drivlist.config()->root_device()); + if (iter.count() == 0) + continue; + + // print a header + if (!first) + osd_printf_info("\n"); + first = false; + osd_printf_info("Samples required for driver \"%s\".\n", drivlist.driver().name); + + // iterate over samples devices and print the samples from each one + for (samples_device &device : iter) + { + samples_iterator sampiter(device); + for (const char *samplename = sampiter.first(); samplename != nullptr; samplename = sampiter.next()) + osd_printf_info("%s\n", samplename); + } + } +} + + +//------------------------------------------------- +// listdevices - output the list of devices +// referenced by a given game or set of games +//------------------------------------------------- + +void cli_frontend::listdevices(const std::vector<std::string> &args) +{ + const char *gamename = args.empty() ? nullptr : args[0].c_str(); + + // determine which drivers to output; return an error if none found + driver_enumerator drivlist(m_options, gamename); + if (drivlist.count() == 0) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "No matching systems found for '%s'", gamename); + + // iterate over drivers, looking for SAMPLES devices + bool first = true; + while (drivlist.next()) + { + // print a header + if (!first) + printf("\n"); + first = false; + printf("Driver %s (%s):\n", drivlist.driver().name, drivlist.driver().type.fullname()); + + // build a list of devices + std::vector<device_t *> device_list; + for (device_t &device : device_enumerator(drivlist.config()->root_device())) + device_list.push_back(&device); + + // sort them by tag + std::sort(device_list.begin(), device_list.end(), [](device_t *dev1, device_t *dev2) { + // end of string < ':' < '0' + const char *tag1 = dev1->tag(); + const char *tag2 = dev2->tag(); + while (*tag1 == *tag2 && *tag1 != '\0' && *tag2 != '\0') + { + tag1++; + tag2++; + } + return (*tag1 == ':' ? ' ' : *tag1) < (*tag2 == ':' ? ' ' : *tag2); + }); + + // dump the results + for (auto device : device_list) + { + // extract the tag, stripping the leading colon + const char *tag = device->tag(); + if (*tag == ':') + tag++; + + // determine the depth + int depth = 1; + if (*tag == 0) + { + tag = "<root>"; + depth = 0; + } + else + { + for (const char *c = tag; *c != 0; c++) + if (*c == ':') + { + tag = c + 1; + depth++; + } + } + printf(" %*s%-*s %s", depth * 2, "", 30 - depth * 2, tag, device->name()); + + // add more information + uint32_t clock = device->clock(); + if (clock >= 1000000000) + printf(" @ %d.%02d GHz\n", clock / 1000000000, (clock / 10000000) % 100); + else if (clock >= 1000000) + printf(" @ %d.%02d MHz\n", clock / 1000000, (clock / 10000) % 100); + else if (clock >= 1000) + printf(" @ %d.%02d kHz\n", clock / 1000, (clock / 10) % 100); + else if (clock > 0) + printf(" @ %d Hz\n", clock); + else + printf("\n"); + } + } +} + + +//------------------------------------------------- +// listslots - output the list of slot devices +// referenced by a given game or set of games +//------------------------------------------------- + +void cli_frontend::listslots(const std::vector<std::string> &args) +{ + const char *gamename = args.empty() ? nullptr : args[0].c_str(); + + // determine which drivers to output; return an error if none found + driver_enumerator drivlist(m_options, gamename); + if (drivlist.count() == 0) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "No matching systems found for '%s'", gamename); + + // print header + printf("%-16s %-16s %-16s %s\n", "SYSTEM", "SLOT NAME", "SLOT OPTIONS", "SLOT DEVICE NAME"); + printf("%s %s %s %s\n", std::string(16,'-').c_str(), std::string(16,'-').c_str(), std::string(16,'-').c_str(), std::string(28,'-').c_str()); + + // iterate over drivers + while (drivlist.next()) + { + // iterate + bool first = true; + for (const device_slot_interface &slot : slot_interface_enumerator(drivlist.config()->root_device())) + { + if (slot.fixed()) continue; + + // build a list of user-selectable options + std::vector<device_slot_interface::slot_option const *> option_list; + for (auto &option : slot.option_list()) + if (option.second->selectable()) + option_list.push_back(option.second.get()); + + // sort them by name + std::sort(option_list.begin(), option_list.end(), [](device_slot_interface::slot_option const *opt1, device_slot_interface::slot_option const *opt2) { + return strcmp(opt1->name(), opt2->name()) < 0; + }); + + + // output the line, up to the list of extensions + printf("%-16s %-16s ", first ? drivlist.driver().name : "", slot.device().tag()+1); + + bool first_option = true; + + // get the options and print them + for (device_slot_interface::slot_option const *opt : option_list) + { + if (first_option) + printf("%-16s %s\n", opt->name(), opt->devtype().fullname()); + else + printf("%-34s%-16s %s\n", "", opt->name(), opt->devtype().fullname()); + + first_option = false; + } + if (first_option) + printf("%-16s %s\n", "[none]","No options available"); + // end the line + printf("\n"); + first = false; + } + + // if we didn't get any at all, just print a none line + if (first) + printf("%-16s (none)\n", drivlist.driver().name); + } +} + + +//------------------------------------------------- +// listmedia - output the list of image devices +// referenced by a given game or set of games +//------------------------------------------------- + +void cli_frontend::listmedia(const std::vector<std::string> &args) +{ + const char *gamename = args.empty() ? nullptr : args[0].c_str(); + + // determine which drivers to output; return an error if none found + driver_enumerator drivlist(m_options, gamename); + if (drivlist.count() == 0) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "No matching systems found for '%s'", gamename); + + // print header + printf("%-16s %-16s %-10s %s\n", "SYSTEM", "MEDIA NAME", "(brief)", "IMAGE FILE EXTENSIONS SUPPORTED"); + printf("%s %s-%s %s\n", std::string(16,'-').c_str(), std::string(16,'-').c_str(), std::string(10,'-').c_str(), std::string(31,'-').c_str()); + + // iterate over drivers + while (drivlist.next()) + { + // iterate + bool first = true; + for (const device_image_interface &imagedev : image_interface_enumerator(drivlist.config()->root_device())) + { + if (!imagedev.user_loadable()) + continue; + + // extract the shortname with parentheses + std::string paren_shortname = string_format("(%s)", imagedev.brief_instance_name()); + + // output the line, up to the list of extensions + printf("%-16s %-16s %-10s ", drivlist.driver().name, imagedev.instance_name().c_str(), paren_shortname.c_str()); + + // get the extensions and print them + std::string extensions(imagedev.file_extensions()); + for (int start = 0, end = extensions.find_first_of(',');; start = end + 1, end = extensions.find_first_of(',', start)) + { + std::string curext(extensions, start, (end == -1) ? extensions.length() - start : end - start); + printf(".%-5s", curext.c_str()); + if (end == -1) + break; + } + + // end the line + printf("\n"); + first = false; + } + + // if we didn't get any at all, just print a none line + if (first) + printf("%-16s (none)\n", drivlist.driver().name); + } +} + +//------------------------------------------------- +// verifyroms - verify the ROM sets of one or +// more games +//------------------------------------------------- +void cli_frontend::verifyroms(const std::vector<std::string> &args) +{ + bool const iswild((1U != args.size()) || core_iswildstr(args[0].c_str())); + std::vector<bool> matched(args.size(), false); + unsigned matchcount = 0; + auto const included = [&args, &matched, &matchcount] (char const *name) -> bool + { + if (args.empty()) + { + ++matchcount; + return true; + } + + bool result = false; + auto it = matched.begin(); + for (std::string const &pat : args) + { + if (!core_strwildcmp(pat.c_str(), name)) + { + ++matchcount; + result = true; + *it = true; + } + ++it; + } + return result; + }; + + unsigned correct = 0; + unsigned incorrect = 0; + unsigned notfound = 0; + + // iterate over drivers + driver_enumerator drivlist(m_options); + media_auditor auditor(drivlist); + util::ovectorstream summary_string; + while (drivlist.next()) + { + if (included(drivlist.driver().name)) + { + // audit the ROMs in this set + media_auditor::summary summary = auditor.audit_media(AUDIT_VALIDATE_FAST); + + auto const clone_of = drivlist.clone(); + print_summary( + auditor, summary, true, + "rom", drivlist.driver().name, (clone_of >= 0) ? drivlist.driver(clone_of).name : nullptr, + correct, incorrect, notfound, + summary_string); + + // if it wasn't a wildcard, there can only be one + if (!iswild) + break; + } + } + + if (iswild || !matchcount) + { + machine_config config(GAME_NAME(___empty), m_options); + machine_config::token const tok(config.begin_configuration(config.root_device())); + for (device_type type : registered_device_types) + { + if (included(type.shortname())) + { + // audit the ROMs in this set + device_t *const dev = config.device_add("_tmp", type, 0); + media_auditor::summary summary = auditor.audit_device(*dev, AUDIT_VALIDATE_FAST); + + print_summary( + auditor, summary, false, + "rom", dev->shortname(), nullptr, + correct, incorrect, notfound, + summary_string); + config.device_remove("_tmp"); + + // if it wasn't a wildcard, there can only be one + if (!iswild) + break; + } + } + } + + // clear out any cached files + util::archive_file::cache_clear(); + + // return an error if none found + auto it = matched.begin(); + for (std::string const &pat : args) + { + if (!*it) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "No matching systems found for '%s'", pat); + + ++it; + } + + if ((1U == args.size()) && (matchcount > 0) && (correct == 0) && (incorrect == 0)) + { + // if we didn't get anything at all, display a generic end message + if (notfound > 0) + throw emu_fatalerror(EMU_ERR_MISSING_FILES, "romset \"%s\" not found!\n", args[0]); + else + throw emu_fatalerror(EMU_ERR_MISSING_FILES, "romset \"%s\" has no roms!\n", args[0]); + } + else + { + // otherwise, print a summary + if (incorrect > 0) + throw emu_fatalerror(EMU_ERR_MISSING_FILES, "%u romsets found, %u were OK.\n", correct + incorrect, correct); + else + osd_printf_info("%u romsets found, %u were OK.\n", correct, correct); + } +} + + +//------------------------------------------------- +// info_verifysamples - verify the sample sets of +// one or more games +//------------------------------------------------- + +void cli_frontend::verifysamples(const std::vector<std::string> &args) +{ + const char *gamename = args.empty() ? "*" : args[0].c_str(); + + // determine which drivers to output; return an error if none found + driver_enumerator drivlist(m_options, gamename); + + unsigned correct = 0; + unsigned incorrect = 0; + unsigned notfound = 0; + unsigned matched = 0; + + // iterate over drivers + media_auditor auditor(drivlist); + util::ovectorstream summary_string; + while (drivlist.next()) + { + matched++; + + // audit the samples in this set + media_auditor::summary summary = auditor.audit_samples(); + + auto const clone_of = drivlist.clone(); + print_summary( + auditor, summary, false, + "sample", drivlist.driver().name, (clone_of >= 0) ? drivlist.driver(clone_of).name : nullptr, + correct, incorrect, notfound, + summary_string); + } + + // clear out any cached files + util::archive_file::cache_clear(); + + // return an error if none found + if (matched == 0) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "No matching systems found for '%s'", gamename); + + // if we didn't get anything at all, display a generic end message + if (matched > 0 && correct == 0 && incorrect == 0) + { + if (notfound > 0) + throw emu_fatalerror(EMU_ERR_MISSING_FILES, "sampleset \"%s\" not found!\n", gamename); + else + throw emu_fatalerror(EMU_ERR_MISSING_FILES, "sampleset \"%s\" not required!\n", gamename); + } + + // otherwise, print a summary + else + { + if (incorrect > 0) + throw emu_fatalerror(EMU_ERR_MISSING_FILES, "%u samplesets found, %u were OK.\n", correct + incorrect, correct); + osd_printf_info("%u samplesets found, %u were OK.\n", correct, correct); + } +} + +const char cli_frontend::s_softlist_xml_dtd[] = + "<?xml version=\"1.0\"?>\n" \ + "<!DOCTYPE softwarelists [\n" \ + "<!ELEMENT softwarelists (softwarelist*)>\n" \ + "\t<!ELEMENT softwarelist (notes?, software+)>\n" \ + "\t\t<!ATTLIST softwarelist name CDATA #REQUIRED>\n" \ + "\t\t<!ATTLIST softwarelist description CDATA #IMPLIED>\n" \ + "\t\t<!ELEMENT notes (#PCDATA)>\n" \ + "\t\t<!ELEMENT software (description, year, publisher, notes?, info*, sharedfeat*, part*)>\n" \ + "\t\t\t<!ATTLIST software name CDATA #REQUIRED>\n" \ + "\t\t\t<!ATTLIST software cloneof CDATA #IMPLIED>\n" \ + "\t\t\t<!ATTLIST software supported (yes|partial|no) \"yes\">\n" \ + "\t\t\t<!ELEMENT description (#PCDATA)>\n" \ + "\t\t\t<!ELEMENT year (#PCDATA)>\n" \ + "\t\t\t<!ELEMENT publisher (#PCDATA)>\n" \ + "\t\t\t<!ELEMENT notes (#PCDATA)>\n" \ + "\t\t\t<!ELEMENT info EMPTY>\n" \ + "\t\t\t\t<!ATTLIST info name CDATA #REQUIRED>\n" \ + "\t\t\t\t<!ATTLIST info value CDATA #IMPLIED>\n" \ + "\t\t\t<!ELEMENT sharedfeat EMPTY>\n" \ + "\t\t\t\t<!ATTLIST sharedfeat name CDATA #REQUIRED>\n" \ + "\t\t\t\t<!ATTLIST sharedfeat value CDATA #IMPLIED>\n" \ + "\t\t\t<!ELEMENT part (feature*, dataarea*, diskarea*, dipswitch*)>\n" \ + "\t\t\t\t<!ATTLIST part name CDATA #REQUIRED>\n" \ + "\t\t\t\t<!ATTLIST part interface CDATA #REQUIRED>\n" \ + "\t\t\t\t<!ELEMENT feature EMPTY>\n" \ + "\t\t\t\t\t<!ATTLIST feature name CDATA #REQUIRED>\n" \ + "\t\t\t\t\t<!ATTLIST feature value CDATA #IMPLIED>\n" \ + "\t\t\t\t<!ELEMENT dataarea (rom*)>\n" \ + "\t\t\t\t\t<!ATTLIST dataarea name CDATA #REQUIRED>\n" \ + "\t\t\t\t\t<!ATTLIST dataarea size CDATA #REQUIRED>\n" \ + "\t\t\t\t\t<!ATTLIST dataarea databits (8|16|32|64) \"8\">\n" \ + "\t\t\t\t\t<!ATTLIST dataarea endian (big|little) \"little\">\n" \ + "\t\t\t\t\t<!ELEMENT rom EMPTY>\n" \ + "\t\t\t\t\t\t<!ATTLIST rom name CDATA #IMPLIED>\n" \ + "\t\t\t\t\t\t<!ATTLIST rom size CDATA #IMPLIED>\n" \ + "\t\t\t\t\t\t<!ATTLIST rom length CDATA #IMPLIED>\n" \ + "\t\t\t\t\t\t<!ATTLIST rom crc CDATA #IMPLIED>\n" \ + "\t\t\t\t\t\t<!ATTLIST rom sha1 CDATA #IMPLIED>\n" \ + "\t\t\t\t\t\t<!ATTLIST rom offset CDATA #IMPLIED>\n" \ + "\t\t\t\t\t\t<!ATTLIST rom value CDATA #IMPLIED>\n" \ + "\t\t\t\t\t\t<!ATTLIST rom status (baddump|nodump|good) \"good\">\n" \ + "\t\t\t\t\t\t<!ATTLIST rom loadflag (load16_byte|load16_word|load16_word_swap|load32_byte|load32_word|load32_word_swap|load32_dword|load64_word|load64_word_swap|reload|fill|continue|reload_plain) #IMPLIED>\n" \ + "\t\t\t\t<!ELEMENT diskarea (disk*)>\n" \ + "\t\t\t\t\t<!ATTLIST diskarea name CDATA #REQUIRED>\n" \ + "\t\t\t\t\t<!ELEMENT disk EMPTY>\n" \ + "\t\t\t\t\t\t<!ATTLIST disk name CDATA #REQUIRED>\n" \ + "\t\t\t\t\t\t<!ATTLIST disk sha1 CDATA #IMPLIED>\n" \ + "\t\t\t\t\t\t<!ATTLIST disk status (baddump|nodump|good) \"good\">\n" \ + "\t\t\t\t\t\t<!ATTLIST disk writeable (yes|no) \"no\">\n" \ + "\t\t\t\t<!ELEMENT dipswitch (dipvalue*)>\n" \ + "\t\t\t\t\t<!ATTLIST dipswitch name CDATA #REQUIRED>\n" \ + "\t\t\t\t\t<!ATTLIST dipswitch tag CDATA #REQUIRED>\n" \ + "\t\t\t\t\t<!ATTLIST dipswitch mask CDATA #REQUIRED>\n" \ + "\t\t\t\t\t<!ELEMENT dipvalue EMPTY>\n" \ + "\t\t\t\t\t\t<!ATTLIST dipvalue name CDATA #REQUIRED>\n" \ + "\t\t\t\t\t\t<!ATTLIST dipvalue value CDATA #REQUIRED>\n" \ + "\t\t\t\t\t\t<!ATTLIST dipvalue default (yes|no) \"no\">\n" \ + "]>\n\n"; + +void cli_frontend::output_single_softlist(std::ostream &out, software_list_device &swlistdev) +{ + util::stream_format(out, "\t<softwarelist name=\"%s\" description=\"%s\">\n", swlistdev.list_name(), util::xml::normalize_string(swlistdev.description().c_str())); + for (const software_info &swinfo : swlistdev.get_info()) + { + util::stream_format(out, "\t\t<software name=\"%s\"", util::xml::normalize_string(swinfo.shortname().c_str())); + if (!swinfo.parentname().empty()) + util::stream_format(out, " cloneof=\"%s\"", util::xml::normalize_string(swinfo.parentname().c_str())); + if (swinfo.supported() == software_support::PARTIALLY_SUPPORTED) + out << " supported=\"partial\""; + else if (swinfo.supported() == software_support::UNSUPPORTED) + out << " supported=\"no\""; + out << ">\n"; + util::stream_format(out, "\t\t\t<description>%s</description>\n", util::xml::normalize_string(swinfo.longname().c_str())); + util::stream_format(out, "\t\t\t<year>%s</year>\n", util::xml::normalize_string(swinfo.year().c_str())); + util::stream_format(out, "\t\t\t<publisher>%s</publisher>\n", util::xml::normalize_string(swinfo.publisher().c_str())); + + for (const auto &flist : swinfo.info()) + util::stream_format(out, "\t\t\t<info name=\"%s\" value=\"%s\"/>\n", flist.name(), util::xml::normalize_string(flist.value().c_str())); + + for (const auto &flist : swinfo.shared_features()) + util::stream_format(out, "\t\t\t<sharedfeat name=\"%s\" value=\"%s\"/>\n", flist.name(), util::xml::normalize_string(flist.value().c_str())); + + for (const software_part &part : swinfo.parts()) + { + util::stream_format(out, "\t\t\t<part name=\"%s\"", util::xml::normalize_string(part.name().c_str())); + if (!part.interface().empty()) + util::stream_format(out, " interface=\"%s\"", util::xml::normalize_string(part.interface().c_str())); + + out << ">\n"; + + for (const auto &flist : part.features()) + util::stream_format(out, "\t\t\t\t<feature name=\"%s\" value=\"%s\" />\n", flist.name(), util::xml::normalize_string(flist.value().c_str())); + + // TODO: display ROM region information + for (const rom_entry *region = part.romdata().data(); region; region = rom_next_region(region)) + { + int is_disk = ROMREGION_ISDISKDATA(region); + + if (!is_disk) + util::stream_format(out, "\t\t\t\t<dataarea name=\"%s\" size=\"%d\">\n", util::xml::normalize_string(region->name().c_str()), region->get_length()); + else + util::stream_format(out, "\t\t\t\t<diskarea name=\"%s\">\n", util::xml::normalize_string(region->name().c_str())); + + for (const rom_entry *rom = rom_first_file(region); rom && !ROMENTRY_ISREGIONEND(rom); rom++) + { + if (ROMENTRY_ISFILE(rom)) + { + if (!is_disk) + util::stream_format(out, "\t\t\t\t\t<rom name=\"%s\" size=\"%d\"", util::xml::normalize_string(ROM_GETNAME(rom)), rom_file_size(rom)); + else + util::stream_format(out, "\t\t\t\t\t<disk name=\"%s\"", util::xml::normalize_string(ROM_GETNAME(rom))); + + // dump checksum information only if there is a known dump + util::hash_collection hashes(rom->hashdata()); + if (!hashes.flag(util::hash_collection::FLAG_NO_DUMP)) + util::stream_format(out, " %s", hashes.attribute_string()); + else + out << " status=\"nodump\""; + + if (is_disk) + util::stream_format(out, " writeable=\"%s\"", (ROM_GETFLAGS(rom) & DISK_READONLYMASK) ? "no" : "yes"); + + if ((ROM_GETFLAGS(rom) & ROM_SKIPMASK) == ROM_SKIP(1)) + out << " loadflag=\"load16_byte\""; + + if ((ROM_GETFLAGS(rom) & ROM_SKIPMASK) == ROM_SKIP(3)) + out << " loadflag=\"load32_byte\""; + + if (((ROM_GETFLAGS(rom) & ROM_SKIPMASK) == ROM_SKIP(2)) && ((ROM_GETFLAGS(rom) & ROM_GROUPMASK) == ROM_GROUPWORD)) + { + if (!(ROM_GETFLAGS(rom) & ROM_REVERSEMASK)) + out << " loadflag=\"load32_word\""; + else + out << " loadflag=\"load32_word_swap\""; + } + + if (((ROM_GETFLAGS(rom) & ROM_SKIPMASK) == ROM_SKIP(6)) && ((ROM_GETFLAGS(rom) & ROM_GROUPMASK) == ROM_GROUPWORD)) + { + if (!(ROM_GETFLAGS(rom) & ROM_REVERSEMASK)) + out << " loadflag=\"load64_word\""; + else + out << " loadflag=\"load64_word_swap\""; + } + + if (((ROM_GETFLAGS(rom) & ROM_SKIPMASK) == ROM_NOSKIP) && ((ROM_GETFLAGS(rom) & ROM_GROUPMASK) == ROM_GROUPWORD)) + { + if (!(ROM_GETFLAGS(rom) & ROM_REVERSEMASK)) + out << " loadflag=\"load32_dword\""; + else + out << " loadflag=\"load16_word_swap\""; + } + + out << "/>\n"; + } + else if (ROMENTRY_ISRELOAD(rom)) + { + util::stream_format(out, "\t\t\t\t\t<rom size=\"%d\" offset=\"0x%x\" loadflag=\"reload\" />\n", ROM_GETLENGTH(rom), ROM_GETOFFSET(rom)); + } + else if (ROMENTRY_ISFILL(rom)) + { + util::stream_format(out, "\t\t\t\t\t<rom size=\"%d\" offset=\"0x%x\" loadflag=\"fill\" />\n", ROM_GETLENGTH(rom), ROM_GETOFFSET(rom)); + } + } + + if (!is_disk) + out << "\t\t\t\t</dataarea>\n"; + else + out << "\t\t\t\t</diskarea>\n"; + } + + out << "\t\t\t</part>\n"; + } + + out << "\t\t</software>\n"; + } + out << "\t</softwarelist>\n"; +} + + +/*------------------------------------------------- + info_listsoftware - output the list of + software supported by a given game or set of + games + TODO: Add all information read from the source files +-------------------------------------------------*/ + +void cli_frontend::listsoftware(const std::vector<std::string> &args) +{ + std::unordered_set<std::string> list_map; + bool firstlist(true); + apply_device_action( + args, + [this, &list_map, &firstlist] (device_t &root, char const *type, bool first) + { + for (software_list_device &swlistdev : software_list_device_enumerator(root)) + { + if (list_map.insert(swlistdev.list_name()).second) + { + if (!swlistdev.get_info().empty()) + { + if (firstlist) + { + if (m_options.bool_value(CLIOPTION_DTD)) + std::cout << s_softlist_xml_dtd; + std::cout << "<softwarelists>\n"; + firstlist = false; + } + output_single_softlist(std::cout, swlistdev); + } + } + } + }); + + if (!firstlist) + std::cout << "</softwarelists>\n"; + else + fprintf(stdout, "No software lists found for this system\n"); // TODO: should this go to stderr instead? +} + + +/*------------------------------------------------- + verifysoftware - verify ROMs from the software + list of the specified driver(s) +-------------------------------------------------*/ +void cli_frontend::verifysoftware(const std::vector<std::string> &args) +{ + const char *gamename = args.empty() ? "*" : args[0].c_str(); + + std::unordered_set<std::string> list_map; + + unsigned correct = 0; + unsigned incorrect = 0; + unsigned notfound = 0; + unsigned matched = 0; + unsigned nrlists = 0; + + // determine which drivers to process; return an error if none found + driver_enumerator drivlist(m_options, gamename); + if (drivlist.count() == 0) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "No matching systems found for '%s'", gamename); + + media_auditor auditor(drivlist); + util::ovectorstream summary_string; + while (drivlist.next()) + { + matched++; + + for (software_list_device &swlistdev : software_list_device_enumerator(drivlist.config()->root_device())) + { + if (swlistdev.is_original()) + { + if (list_map.insert(swlistdev.list_name()).second) + { + if (!swlistdev.get_info().empty()) + { + nrlists++; + for (const software_info &swinfo : swlistdev.get_info()) + { + media_auditor::summary summary = auditor.audit_software(swlistdev, swinfo, AUDIT_VALIDATE_FAST); + + print_summary( + auditor, summary, false, + "rom", util::string_format("%s:%s", swlistdev.list_name(), swinfo.shortname()).c_str(), nullptr, + correct, incorrect, notfound, + summary_string); + } + } + } + } + } + } + + // clear out any cached files + util::archive_file::cache_clear(); + + // return an error if none found + if (matched == 0) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "No matching systems found for '%s'", gamename); + + // if we didn't get anything at all, display a generic end message + if (matched > 0 && correct == 0 && incorrect == 0) + { + throw emu_fatalerror(EMU_ERR_MISSING_FILES, "romset \"%s\" has no software entries defined!\n", gamename); + } + // otherwise, print a summary + else + { + if (incorrect > 0) + throw emu_fatalerror(EMU_ERR_MISSING_FILES, "%u romsets found in %u software lists, %u were OK.\n", correct + incorrect, nrlists, correct); + osd_printf_info("%u romsets found in %u software lists, %u romsets were OK.\n", correct, nrlists, correct); + } + +} + + +/*------------------------------------------------- + getsoftlist - retrieve software list by name +-------------------------------------------------*/ + +void cli_frontend::getsoftlist(const std::vector<std::string> &args) +{ + const char *gamename = args.empty() ? "*" : args[0].c_str(); + + std::unordered_set<std::string> list_map; + bool firstlist(true); + apply_device_action( + std::vector<std::string>(), + [this, gamename, &list_map, &firstlist] (device_t &root, char const *type, bool first) + { + for (software_list_device &swlistdev : software_list_device_enumerator(root)) + { + if (core_strwildcmp(gamename, swlistdev.list_name().c_str()) == 0 && list_map.insert(swlistdev.list_name()).second) + { + if (!swlistdev.get_info().empty()) + { + if (firstlist) + { + if (m_options.bool_value(CLIOPTION_DTD)) + std::cout << s_softlist_xml_dtd; + std::cout << "<softwarelists>\n"; + firstlist = false; + } + output_single_softlist(std::cout, swlistdev); + } + } + } + }); + + if (!firstlist) + std::cout << "</softwarelists>\n"; + else + fprintf(stdout, "No such software lists found\n"); // TODO: should this go to stderr instead? +} + + +/*------------------------------------------------- + verifysoftlist - verify software list by name +-------------------------------------------------*/ +void cli_frontend::verifysoftlist(const std::vector<std::string> &args) +{ + const char *gamename = args.empty() ? "*" : args[0].c_str(); + + std::unordered_set<std::string> list_map; + unsigned correct = 0; + unsigned incorrect = 0; + unsigned notfound = 0; + unsigned matched = 0; + + driver_enumerator drivlist(m_options); + media_auditor auditor(drivlist); + util::ovectorstream summary_string; + + while (drivlist.next()) + { + for (software_list_device &swlistdev : software_list_device_enumerator(drivlist.config()->root_device())) + { + if (core_strwildcmp(gamename, swlistdev.list_name().c_str()) == 0 && list_map.insert(swlistdev.list_name()).second) + { + if (!swlistdev.get_info().empty()) + { + matched++; + + // Get the actual software list contents + for (const software_info &swinfo : swlistdev.get_info()) + { + media_auditor::summary summary = auditor.audit_software(swlistdev, swinfo, AUDIT_VALIDATE_FAST); + + print_summary( + auditor, summary, false, + "rom", util::string_format("%s:%s", swlistdev.list_name(), swinfo.shortname()).c_str(), nullptr, + correct, incorrect, notfound, + summary_string); + } + } + } + } + } + + // clear out any cached files + util::archive_file::cache_clear(); + + // return an error if none found + if (matched == 0) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "No matching software lists found for '%s'", gamename); + + // if we didn't get anything at all, display a generic end message + if (matched > 0 && correct == 0 && incorrect == 0) + { + throw emu_fatalerror(EMU_ERR_MISSING_FILES, "no romsets found for software list \"%s\"!\n", gamename); + } + // otherwise, print a summary + else + { + if (incorrect > 0) + throw emu_fatalerror(EMU_ERR_MISSING_FILES, "%u romsets found in %u software lists, %u were OK.\n", correct + incorrect, matched, correct); + osd_printf_info("%u romsets found in %u software lists, %u romsets were OK.\n", correct, matched, correct); + } +} + + +//------------------------------------------------- +// version - emit MAME version to stdout +//------------------------------------------------- + +void cli_frontend::version(const std::vector<std::string> &args) +{ + osd_printf_info("%s", emulator_info::get_build_version()); +} + + +//------------------------------------------------- +// romident - identify ROMs by looking for +// matches in our internal database +//------------------------------------------------- + +void cli_frontend::romident(const std::vector<std::string> &args) +{ + const char *filename = args[0].c_str(); + + // create our own copy of options for the purposes of ROM identification + // so we are not "polluted" with driver-specific slot/image options + emu_options options; + options.set_value(OPTION_HASHPATH, m_options.hash_path(), OPTION_PRIORITY_DEFAULT); + + media_identifier ident(options); + + // identify the file, then output results + osd_printf_info("Identifying %s....\n", filename); + ident.identify(filename); + + // return the appropriate error code + if (ident.total() == 0) + throw emu_fatalerror(EMU_ERR_MISSING_FILES, "No files found.\n"); + else if (ident.matches() == ident.total()) + return; + else if (ident.matches() == ident.total() - ident.nonroms()) + throw emu_fatalerror(EMU_ERR_IDENT_NONROMS, "Out of %d files, %d matched, %d are not roms.\n", ident.total(), ident.matches(), ident.nonroms()); + else if (ident.matches() > 0) + throw emu_fatalerror(EMU_ERR_IDENT_PARTIAL, "Out of %d files, %d matched, %d did not match.\n", ident.total(), ident.matches(), ident.total() - ident.matches()); + else + throw emu_fatalerror(EMU_ERR_IDENT_NONE, "No roms matched.\n"); +} + + +//------------------------------------------------- +// apply_action - apply action to matching +// systems/devices +//------------------------------------------------- + +template <typename T, typename U> void cli_frontend::apply_action(const std::vector<std::string> &args, T &&drvact, U &&devact) + +{ + bool const iswild((1U != args.size()) || core_iswildstr(args[0].c_str())); + std::vector<bool> matched(args.size(), false); + auto const included = [&args, &matched] (char const *name) -> bool + { + if (args.empty()) + return true; + + bool result = false; + auto it = matched.begin(); + for (std::string const &pat : args) + { + if (!core_strwildcmp(pat.c_str(), name)) + { + result = true; + *it = true; + } + ++it; + } + return result; + }; + + // determine which drivers to output + driver_enumerator drivlist(m_options); + + // iterate through matches + bool first(true); + while (drivlist.next()) + { + if (included(drivlist.driver().name)) + { + drvact(drivlist, first); + first = false; + + // if it wasn't a wildcard, there can only be one + if (!iswild) + break; + } + } + + if (iswild || first) + { + for (device_type type : registered_device_types) + { + if (included(type.shortname())) + { + devact(type, first); + first = false; + + // if it wasn't a wildcard, there can only be one + if (!iswild) + break; + } + } + } + + // return an error if none found + auto it = matched.begin(); + for (std::string const &pat : args) + { + if (!*it) + throw emu_fatalerror(EMU_ERR_NO_SUCH_SYSTEM, "No matching systems found for '%s'", pat); + + ++it; + } +} + + +//------------------------------------------------- +// apply_device_action - apply action to matching +// systems/devices +//------------------------------------------------- + +template <typename T> void cli_frontend::apply_device_action(const std::vector<std::string> &args, T &&action) +{ + machine_config config(GAME_NAME(___empty), m_options); + machine_config::token const tok(config.begin_configuration(config.root_device())); + apply_action( + args, + [&action] (driver_enumerator &drivlist, bool first) + { + action(drivlist.config()->root_device(), "driver", first); + }, + [&action, &config] (device_type type, bool first) + { + device_t *const dev = config.device_add("_tmp", type, 0); + action(*dev, "device", first); + config.device_remove("_tmp"); + }); +} + + +//------------------------------------------------- +// find_command +//------------------------------------------------- + +const cli_frontend::info_command_struct *cli_frontend::find_command(const std::string &s) +{ + static const info_command_struct s_info_commands[] = + { + { CLICOMMAND_LISTXML, 0, -1, &cli_frontend::listxml, "[pattern] ..." }, + { CLICOMMAND_LISTFULL, 0, -1, &cli_frontend::listfull, "[pattern] ..." }, + { CLICOMMAND_LISTSOURCE, 0, -1, &cli_frontend::listsource, "[system name]" }, + { CLICOMMAND_LISTCLONES, 0, 1, &cli_frontend::listclones, "[system name]" }, + { CLICOMMAND_LISTBROTHERS, 0, 1, &cli_frontend::listbrothers, "[system name]" }, + { CLICOMMAND_LISTCRC, 0, -1, &cli_frontend::listcrc, "[system name]" }, + { CLICOMMAND_LISTDEVICES, 0, 1, &cli_frontend::listdevices, "[system name]" }, + { CLICOMMAND_LISTSLOTS, 0, 1, &cli_frontend::listslots, "[system name]" }, + { CLICOMMAND_LISTROMS, 0, -1, &cli_frontend::listroms, "[pattern] ..." }, + { CLICOMMAND_LISTSAMPLES, 0, 1, &cli_frontend::listsamples, "[system name]" }, + { CLICOMMAND_VERIFYROMS, 0, -1, &cli_frontend::verifyroms, "[pattern] ..." }, + { CLICOMMAND_VERIFYSAMPLES, 0, 1, &cli_frontend::verifysamples, "[system name|*]" }, + { CLICOMMAND_LISTMEDIA, 0, 1, &cli_frontend::listmedia, "[system name]" }, + { CLICOMMAND_LISTSOFTWARE, 0, 1, &cli_frontend::listsoftware, "[system name]" }, + { CLICOMMAND_VERIFYSOFTWARE, 0, 1, &cli_frontend::verifysoftware, "[system name|*]" }, + { CLICOMMAND_ROMIDENT, 1, 1, &cli_frontend::romident, "(file or directory path)" }, + { CLICOMMAND_GETSOFTLIST, 0, 1, &cli_frontend::getsoftlist, "[system name|*]" }, + { CLICOMMAND_VERIFYSOFTLIST, 0, 1, &cli_frontend::verifysoftlist, "[system name|*]" }, + { CLICOMMAND_VERSION, 0, 0, &cli_frontend::version, "" } + }; + + for (const auto &info_command : s_info_commands) + { + if (s == info_command.option) + return &info_command; + } + return nullptr; +} + + +//------------------------------------------------- +// execute_commands - execute various frontend +// commands +//------------------------------------------------- + +void cli_frontend::execute_commands(std::string_view exename) +{ + // help? + if (m_options.command() == CLICOMMAND_HELP) + { + display_help(exename); + return; + } + + // showusage? + if (m_options.command() == CLICOMMAND_SHOWUSAGE) + { + osd_printf_info("Usage: %s [machine] [media] [software] [options]",exename); + osd_printf_info("\n\nOptions:\n%s", m_options.output_help()); + return; + } + + // validate? + if (m_options.command() == CLICOMMAND_VALIDATE) + { + if (m_options.command_arguments().size() > 1) + { + osd_printf_error("Auxiliary verb -validate takes at most 1 argument\n"); + return; + } + validity_checker valid(m_options, false); + const char *sysname = m_options.command_arguments().empty() ? nullptr : m_options.command_arguments()[0].c_str(); + bool result = valid.check_all_matching(sysname); + if (!result) + throw emu_fatalerror(EMU_ERR_FAILED_VALIDITY, "Validity check failed (%d errors, %d warnings in total)\n", valid.errors(), valid.warnings()); + return; + } + + // other commands need the INIs parsed + std::ostringstream option_errors; + mame_options::parse_standard_inis(m_options,option_errors); + if (option_errors.tellp() > 0) + osd_printf_error("%s\n", option_errors.str()); + + // createconfig? + if (m_options.command() == CLICOMMAND_CREATECONFIG) + { + // attempt to open the output file + emu_file file(OPEN_FLAG_WRITE | OPEN_FLAG_CREATE | OPEN_FLAG_CREATE_PATHS); + if (file.open(std::string(emulator_info::get_configname()) + ".ini")) + throw emu_fatalerror("Unable to create file %s.ini\n",emulator_info::get_configname()); + + // generate the updated INI + file.puts(m_options.output_ini()); + + ui_options ui_opts; + emu_file file_ui(OPEN_FLAG_WRITE | OPEN_FLAG_CREATE | OPEN_FLAG_CREATE_PATHS); + if (file_ui.open("ui.ini")) + throw emu_fatalerror("Unable to create file ui.ini\n"); + + // generate the updated INI + file_ui.puts(ui_opts.output_ini()); + + plugin_options plugin_opts; + path_iterator iter(m_options.plugins_path()); + std::string pluginpath; + while (iter.next(pluginpath)) + { + osd_subst_env(pluginpath, pluginpath); + plugin_opts.scan_directory(pluginpath, true); + } + emu_file file_plugin(OPEN_FLAG_WRITE | OPEN_FLAG_CREATE | OPEN_FLAG_CREATE_PATHS); + if (file_plugin.open("plugin.ini")) + throw emu_fatalerror("Unable to create file plugin.ini\n"); + + // generate the updated INI + file_plugin.puts(plugin_opts.output_ini()); + + return; + } + + // showconfig? + if (m_options.command() == CLICOMMAND_SHOWCONFIG) + { + // print the INI text + printf("%s\n", m_options.output_ini().c_str()); + return; + } + + // all other commands call out to one of the info_commands helpers; first + // find the command + const auto *info_command = find_command(m_options.command()); + if (info_command) + { + // validate argument count + const char *error_message = nullptr; + if (m_options.command_arguments().size() < info_command->min_args) + error_message = "Auxiliary verb -%s requires at least %d argument(s)\n"; + if ((info_command->max_args >= 0) && (m_options.command_arguments().size() > info_command->max_args)) + error_message = "Auxiliary verb -%s takes at most %d argument(s)\n"; + if (error_message) + { + osd_printf_info(error_message, info_command->option, info_command->max_args); + osd_printf_info("\n"); + osd_printf_info("Usage: %s -%s %s\n", exename, info_command->option, info_command->usage); + return; + } + + // invoke the auxiliary command! + (this->*info_command->function)(m_options.command_arguments()); + return; + } + + if (!m_osd.execute_command(m_options.command().c_str())) + // if we get here, we don't know what has been requested + throw emu_fatalerror(EMU_ERR_INVALID_CONFIG, "Unknown command '%s' specified", m_options.command()); +} + + +//------------------------------------------------- +// display_help - display help to standard +// output +//------------------------------------------------- + +// HBMAME - remove reference to software and media. +void cli_frontend::display_help(std::string_view exename) +{ + osd_printf_info( + "%3$s v%2$s\n" + "%5$s\n" + "\n" + "This software reproduces, more or less faithfully, the behaviour of a wide range\n" + "of machines. But hardware is useless without software, so images of the ROMs and\n" + "other media which run on that hardware are also required.\n" + "\n" + "Usage: %1$s [machine] [options]\n" + "\n" + " %1$s -showusage for a list of options\n" + " %1$s -showconfig to show current configuration in %4$s.ini format\n" + " %1$s -createconfig to create a %4$s.ini file\n" + "\n", + exename, + build_version, + emulator_info::get_appname(), + emulator_info::get_configname(), + emulator_info::get_copyright_info()); +} diff --git a/docs/release/src/frontend/mame/language.cpp b/docs/release/src/frontend/mame/language.cpp new file mode 100644 index 00000000000..486283110a6 --- /dev/null +++ b/docs/release/src/frontend/mame/language.cpp @@ -0,0 +1,232 @@ +// license:BSD-3-Clause +// copyright-holders:Vas Crabb +/*************************************************************************** + + language.cpp + + Multi-language support. + +***************************************************************************/ + +#include "emu.h" +#include "language.h" + +#include "emuopts.h" + +#include "corestr.h" + +#include <cstring> +#include <memory> +#include <new> +#include <unordered_map> +#include <utility> + + +namespace { + +constexpr u32 MO_MAGIC = 0x950412de; +constexpr u32 MO_MAGIC_REVERSED = 0xde120495; + +std::unique_ptr<u32 []> f_translation_data; +std::unordered_map<std::string_view, std::pair<char const *, u32> > f_translation_map; + +} // anonymous namespace + + +void load_translation(emu_options &m_options) +{ + f_translation_data.reset(); + f_translation_map.clear(); + + std::string name = m_options.language(); + if (name.empty()) + return; + + strreplace(name, " ", "_"); + strreplace(name, "(", ""); + strreplace(name, ")", ""); + + // MESSUI: See if language file exists. If not, try English, see if that exists. If not, use inbuilt default. + emu_file file(m_options.language_path(), OPEN_FLAG_READ); + if (file.open(name + PATH_SEPARATOR "strings.mo")) + { + osd_printf_verbose("Error opening translation file %s\n", name); + name = "English"; + if (file.open(name + PATH_SEPARATOR "strings.mo")) + { + osd_printf_verbose("Error opening translation file %s\n", name); + return; + } + } + + u64 const size = file.size(); + if (20 > size) + { + file.close(); + osd_printf_error("Error reading translation file %s: %u-byte file is too small to contain translation data\n", name, size); + return; + } + + f_translation_data.reset(new (std::nothrow) u32 [(size + 3) / 4]); + if (!f_translation_data) + { + file.close(); + osd_printf_error("Failed to allocate %u bytes to load translation data file %s\n", size, name); + return; + } + + auto const read = file.read(f_translation_data.get(), size); + file.close(); + if (read != size) + { + osd_printf_error("Error reading translation file %s: requested %u bytes but got %u bytes\n", name, size, read); + f_translation_data.reset(); + return; + } + + if ((f_translation_data[0] != MO_MAGIC) && (f_translation_data[0] != MO_MAGIC_REVERSED)) + { + osd_printf_error("Error reading translation file %s: unrecognized magic number 0x%08X\n", name, f_translation_data[0]); + f_translation_data.reset(); + return; + } + + auto fetch_word = + [reversed = f_translation_data[0] == MO_MAGIC_REVERSED, words = f_translation_data.get()] (size_t offset) + { + return reversed ? swapendian_int32(words[offset]) : words[offset]; + }; + + // FIXME: check major/minor version number + + if ((fetch_word(3) % 4) || (fetch_word(4) % 4)) + { + osd_printf_error("Error reading translation file %s: table offsets %u and %u are not word-aligned\n", name, fetch_word(3), fetch_word(4)); + f_translation_data.reset(); + return; + } + + u32 const number_of_strings = fetch_word(2); + u32 const original_table_offset = fetch_word(3) >> 2; + u32 const translation_table_offset = fetch_word(4) >> 2; + if ((4 * (original_table_offset + (u64(number_of_strings) * 2))) > size) + { + osd_printf_error("Error reading translation file %s: %u-entry original string table at offset %u extends past end of %u-byte file\n", name, number_of_strings, fetch_word(3), size); + f_translation_data.reset(); + return; + } + if ((4 * (translation_table_offset + (u64(number_of_strings) * 2))) > size) + { + osd_printf_error("Error reading translation file %s: %u-entry translated string table at offset %u extends past end of %u-byte file\n", name, number_of_strings, fetch_word(4), size); + f_translation_data.reset(); + return; + } + osd_printf_verbose("Reading translation file %s: %u strings, original table at word offset %u, translated table at word offset %u\n", name, number_of_strings, original_table_offset, translation_table_offset); + + char const *const data = reinterpret_cast<char const *>(f_translation_data.get()); + for (u32 i = 1; number_of_strings > i; ++i) + { + u32 const original_length = fetch_word(original_table_offset + (2 * i)); + u32 const original_offset = fetch_word(original_table_offset + (2 * i) + 1); + if ((original_length + original_offset) >= size) + { + osd_printf_error("Error reading translation file %s: %u-byte original string %u at offset %u extends past end of %u-byte file\n", name, original_length, i, original_offset, size); + continue; + } + if (data[original_length + original_offset]) + { + osd_printf_error("Error reading translation file %s: %u-byte original string %u at offset %u is not correctly NUL-terminated\n", name, original_length, i, original_offset); + continue; + } + + u32 const translation_length = fetch_word(translation_table_offset + (2 * i)); + u32 const translation_offset = fetch_word(translation_table_offset + (2 * i) + 1); + if ((translation_length + translation_offset) >= size) + { + osd_printf_error("Error reading translation file %s: %u-byte translated string %u at offset %u extends past end of %u-byte file\n", name, translation_length, i, translation_offset, size); + continue; + } + if (data[translation_length + translation_offset]) + { + osd_printf_error("Error reading translation file %s: %u-byte translated string %u at offset %u is not correctly NUL-terminated\n", name, translation_length, i, translation_offset); + continue; + } + + std::string_view const original(&data[original_offset], original_length); + char const *const translation(&data[translation_offset]); + auto const ins = f_translation_map.emplace(original, std::make_pair(translation, translation_length)); + if (!ins.second) + { + osd_printf_warning( + "Loading translation file %s: translation %u '%s'='%s' conflicts with previous translation '%s'='%s'\n", + name, + i, + original, + translation, + ins.first->first, + ins.first->second.first); + } + } + + osd_printf_verbose("Loaded %u translations from file %s\n", f_translation_map.size(), name); +} + + +char const *lang_translate(char const *message) +{ + auto const found = f_translation_map.find(message); + if (f_translation_map.end() != found) + return found->second.first; + return message; +} + + +std::string_view lang_translate(std::string_view message) +{ + auto const found = f_translation_map.find(message); + if (f_translation_map.end() != found) + return std::string_view(found->second.first, found->second.second); + return message; +} + + +char const *lang_translate(char const *context, char const *message) +{ + if (!f_translation_map.empty()) + { + auto const ctxlen(std::strlen(context)); + auto const msglen(std::strlen(message)); + std::string key; + key.reserve(ctxlen + 1 + msglen); + key.append(context, ctxlen); + key.append(1, '\004'); + key.append(message, msglen); + auto const found = f_translation_map.find(key); + if (f_translation_map.end() != found) + return found->second.first; + } + return message; +} + + +std::string_view lang_translate(char const *context, std::string_view message) +{ + return lang_translate(std::string_view(context), message); +} + + +std::string_view lang_translate(std::string_view context, std::string_view message) +{ + if (!f_translation_map.empty()) + { + std::string key; + key.reserve(context.length() + 1 + message.length()); + key.append(context); + key.append(1, '\004'); + key.append(message); + auto const found = f_translation_map.find(key); + if (f_translation_map.end() != found) + return std::string_view(found->second.first, found->second.second); + } + return message; +} diff --git a/docs/release/src/frontend/mame/mameopts.cpp b/docs/release/src/frontend/mame/mameopts.cpp new file mode 100644 index 00000000000..a1d370ddb74 --- /dev/null +++ b/docs/release/src/frontend/mame/mameopts.cpp @@ -0,0 +1,210 @@ +// license:BSD-3-Clause +// copyright-holders:Aaron Giles +/*************************************************************************** + + mameopts.cpp + + Options file and command line management. + +***************************************************************************/ + +#include "emu.h" +#include "mameopts.h" + +#include "drivenum.h" +#include "screen.h" +#include "softlist_dev.h" +#include "zippath.h" +#include "hashfile.h" +#include "clifront.h" + +#include <cctype> +#include <stack> + + +//------------------------------------------------- +// parse_standard_inis - parse the standard set +// of INI files +//------------------------------------------------- + +void mame_options::parse_standard_inis(emu_options &options, std::ostream &error_stream, const game_driver *driver) +{ + // parse the INI file defined by the platform (e.g., "mame.ini") + // we do this twice so that the first file can change the INI path + parse_one_ini(options, emulator_info::get_configname(), OPTION_PRIORITY_MAME_INI); + parse_one_ini(options, emulator_info::get_configname(), OPTION_PRIORITY_MAME_INI, &error_stream); + + // debug mode: parse "debug.ini" as well + if (options.debug()) + parse_one_ini(options, "debug", OPTION_PRIORITY_DEBUG_INI, &error_stream); + + // if we have a valid system driver, parse system-specific INI files + game_driver const *const cursystem = !driver ? system(options) : driver; + if (!cursystem) + return; + + // parse "vertical.ini" or "horizont.ini" + if (cursystem->flags & ORIENTATION_SWAP_XY) + parse_one_ini(options, "vertical", OPTION_PRIORITY_ORIENTATION_INI, &error_stream); + else + parse_one_ini(options, "horizont", OPTION_PRIORITY_ORIENTATION_INI, &error_stream); + + switch (cursystem->flags & machine_flags::MASK_TYPE) + { + case machine_flags::TYPE_ARCADE: + parse_one_ini(options, "arcade", OPTION_PRIORITY_SYSTYPE_INI, &error_stream); + break; + case machine_flags::TYPE_CONSOLE: + parse_one_ini(options ,"console", OPTION_PRIORITY_SYSTYPE_INI, &error_stream); + break; + case machine_flags::TYPE_COMPUTER: + parse_one_ini(options, "computer", OPTION_PRIORITY_SYSTYPE_INI, &error_stream); + break; + case machine_flags::TYPE_OTHER: + parse_one_ini(options, "othersys", OPTION_PRIORITY_SYSTYPE_INI, &error_stream); + break; + default: + break; + } + + machine_config config(*cursystem, options); + for (const screen_device &device : screen_device_enumerator(config.root_device())) + { + // parse "raster.ini" for raster games + if (device.screen_type() == SCREEN_TYPE_RASTER) + { + parse_one_ini(options, "raster", OPTION_PRIORITY_SCREEN_INI, &error_stream); + break; + } + // parse "vector.ini" for vector games + if (device.screen_type() == SCREEN_TYPE_VECTOR) + { + parse_one_ini(options, "vector", OPTION_PRIORITY_SCREEN_INI, &error_stream); + break; + } + // parse "lcd.ini" for lcd games + if (device.screen_type() == SCREEN_TYPE_LCD) + { + parse_one_ini(options, "lcd", OPTION_PRIORITY_SCREEN_INI, &error_stream); + break; + } + } + + // next parse "source/<sourcefile>.ini" + std::string sourcename = std::string(core_filename_extract_base(cursystem->type.source(), true)).insert(0, "source" PATH_SEPARATOR); + parse_one_ini(options, sourcename.c_str(), OPTION_PRIORITY_SOURCE_INI, &error_stream); + + // then parse the grandparent, parent, and system-specific INIs + int parent = driver_list::clone(*cursystem); + int gparent = (parent != -1) ? driver_list::clone(parent) : -1; + if (gparent != -1) + parse_one_ini(options, driver_list::driver(gparent).name, OPTION_PRIORITY_GPARENT_INI, &error_stream); + if (parent != -1) + parse_one_ini(options, driver_list::driver(parent).name, OPTION_PRIORITY_PARENT_INI, &error_stream); + parse_one_ini(options, cursystem->name, OPTION_PRIORITY_DRIVER_INI, &error_stream); +} + + +//------------------------------------------------- +// system - return a pointer to the specified +// system driver, or nullptr if no match +//------------------------------------------------- + +const game_driver *mame_options::system(const emu_options &options) +{ + int index = driver_list::find(std::string(core_filename_extract_base(options.system_name(), true)).c_str()); + return (index != -1) ? &driver_list::driver(index) : nullptr; +} + + +//------------------------------------------------- +// parse_one_ini - parse a single INI file +//------------------------------------------------- + +void mame_options::parse_one_ini(emu_options &options, const char *basename, int priority, std::ostream *error_stream) +{ + // don't parse if it has been disabled + if (!options.read_config()) + return; + + // open the file; if we fail, that's ok + emu_file file(options.ini_path(), OPEN_FLAG_READ); + osd_printf_verbose("Attempting load of %s.ini\n", basename); + std::error_condition const filerr = file.open(std::string(basename) + ".ini"); + if (filerr) + return; + + // parse the file + osd_printf_verbose("Parsing %s.ini\n", basename); + try + { + options.parse_ini_file((util::core_file&)file, priority, priority < OPTION_PRIORITY_DRIVER_INI, false); + } + catch (options_exception &ex) + { + if (error_stream) + util::stream_format(*error_stream, "While parsing %s:\n%s\n", file.fullpath(), ex.message()); + return; + } + +} + + +//------------------------------------------------- +// populate_hashpath_from_args_and_inis +//------------------------------------------------- + +void mame_options::populate_hashpath_from_args_and_inis(emu_options &options, const std::vector<std::string> &args) +{ + // The existence of this function comes from the fact that for softlist options to be properly + // evaluated, we need to have the hashpath variable set. The problem is that the hashpath may + // be set anywhere on the command line, but also in any of the myriad INI files that we parse, some + // of which may be system specific (e.g. - nes.ini) or otherwise influenced by the system (e.g. - vector.ini) + // + // I think that it is terrible that we have to do a completely independent pass on the command line and every + // argument simply because any one of these things might be setting - hashpath.Unless we invest the effort in + // building some sort of "late binding" apparatus for options(e.g. - delay evaluation of softlist options until + // we've scoured all INIs for hashpath) that can completely straddle the command line and the INI worlds, doing + // this is the best that we can do IMO. + + // parse the command line + emu_options temp_options(emu_options::option_support::GENERAL_AND_SYSTEM); + + // pick up whatever changes the osd did to the default inipath + temp_options.set_default_value(OPTION_INIPATH, options.ini_path()); + + try + { + temp_options.parse_command_line(args, OPTION_PRIORITY_CMDLINE, true); + } + catch (options_exception &) + { + // Something is very long; we have bigger problems than -hashpath possibly + // being in never-never land. Punt and let the main code fail + return; + } + + // if we have an auxillary verb, hashpath is irrelevant + if (!temp_options.command().empty()) + return; + + // read INI files + if (temp_options.read_config()) + { + std::ostringstream error_stream; + parse_standard_inis(temp_options, error_stream); + } + + // and fish out hashpath + const auto entry = temp_options.get_entry(OPTION_HASHPATH); + if (entry) + { + try + { + options.set_value(OPTION_HASHPATH, entry->value(), entry->priority()); + } + catch (options_exception &) + { + } + } +} diff --git a/docs/release/src/frontend/mame/mameopts.h b/docs/release/src/frontend/mame/mameopts.h new file mode 100644 index 00000000000..2b178fb2ead --- /dev/null +++ b/docs/release/src/frontend/mame/mameopts.h @@ -0,0 +1,64 @@ +// license:BSD-3-Clause +// copyright-holders:Aaron Giles +/*************************************************************************** + + mameopts.h + + Options file and command line management. + +***************************************************************************/ + +#ifndef MAME_FRONTEND_MAMEOPTS_H +#define MAME_FRONTEND_MAMEOPTS_H + +#pragma once + +#include "emuopts.h" + +//************************************************************************** +// CONSTANTS +//************************************************************************** +#undef OPTION_PRIORITY_CMDLINE + +// option priorities +enum +{ + // command-line options are HIGH priority + OPTION_PRIORITY_SUBCMD = OPTION_PRIORITY_HIGH, + OPTION_PRIORITY_CMDLINE, + + // INI-based options are NORMAL priority, in increasing order: + OPTION_PRIORITY_MAME_INI = OPTION_PRIORITY_NORMAL + 1, + OPTION_PRIORITY_DEBUG_INI, + OPTION_PRIORITY_ORIENTATION_INI, + OPTION_PRIORITY_SYSTYPE_INI, + OPTION_PRIORITY_SCREEN_INI, + OPTION_PRIORITY_SOURCE_INI, + OPTION_PRIORITY_GPARENT_INI, + OPTION_PRIORITY_PARENT_INI, + OPTION_PRIORITY_DRIVER_INI, + OPTION_PRIORITY_INI, +}; + +//************************************************************************** +// TYPE DEFINITIONS +//************************************************************************** + +// forward references +class game_driver; +class software_part; + +class mame_options +{ +public: + // parsing wrappers + static void parse_standard_inis(emu_options &options, std::ostream &error_stream, const game_driver *driver = nullptr); + static const game_driver *system(const emu_options &options); + static void populate_hashpath_from_args_and_inis(emu_options &options, const std::vector<std::string> &args); + +private: + // INI parsing helper + static void parse_one_ini(emu_options &options, const char *basename, int priority, std::ostream *error_stream = nullptr); +}; + +#endif // MAME_FRONTEND_MAMEOPTS_H diff --git a/docs/release/src/frontend/mame/ui/about.cpp b/docs/release/src/frontend/mame/ui/about.cpp new file mode 100644 index 00000000000..aa7de9671fc --- /dev/null +++ b/docs/release/src/frontend/mame/ui/about.cpp @@ -0,0 +1,266 @@ +// license:BSD-3-Clause +// copyright-holders:Vas Crabb +/*************************************************************************** + + ui/about.cpp + + About box + +***************************************************************************/ + +#include "emu.h" +#include "ui/about.h" + +#include "ui/ui.h" +#include "ui/utils.h" + +#include "mame.h" + +#include <string_view> + + +namespace ui { + +namespace { + +#include "copying.ipp" + +} // anonymous namespace + + +/************************************************** + ABOUT BOX +**************************************************/ + + +//------------------------------------------------- +// ctor +//------------------------------------------------- + +menu_about::menu_about(mame_ui_manager &mui, render_container &container) + : menu(mui, container) + , m_header{ + util::string_format( +#ifdef MAME_DEBUG + _("%1$s %2$s (%3$s%4$sP%5$s, debug)"), +#else + _("%1$s %2$s (%3$s%4$sP%5$s)"), +#endif + emulator_info::get_appname(), + bare_build_version, + (sizeof(int) == sizeof(void *)) ? "I" : "", + (sizeof(long) == sizeof(void *)) ? "L" : (sizeof(long long) == sizeof(void *)) ? "LL" : "", + sizeof(void *) * 8), + /*util::string_format(_("Revision: %1$s"), bare_vcs_revision)*/" " } // MESSUI +{ +} + + +//------------------------------------------------- +// dtor +//------------------------------------------------- + +menu_about::~menu_about() +{ +} + + +//------------------------------------------------- +// perform our special rendering +//------------------------------------------------- + +void menu_about::custom_render(void *selectedref, float top, float bottom, float origx1, float origy1, float origx2, float origy2) +{ + // draw the title + draw_text_box( + std::begin(m_header), std::end(m_header), + origx1, origx2, origy1 - top, origy1 - ui().box_tb_border(), + text_layout::text_justify::CENTER, text_layout::word_wrapping::TRUNCATE, false, + ui().colors().text_color(), UI_GREEN_COLOR, 1.0f); +} + + +//------------------------------------------------- +// draw - draw about +//------------------------------------------------- + +void menu_about::draw(uint32_t flags) +{ + rgb_t const color = ui().colors().text_color(); + float const aspect = machine().render().ui_aspect(&container()); + float const line_height = ui().get_line_height(); + float const ud_arrow_width = line_height * aspect; + float const gutter_width = 0.52f * line_height * aspect; + float const visible_width = 1.0f - (2.0f * ui().box_lr_border() * aspect); + float const visible_left = (1.0f - visible_width) * 0.5f; + float const extra_height = 2.0f * line_height; + float const visible_extra_menu_height = get_customtop() + get_custombottom() + extra_height; + + // determine effective positions taking into account the hilighting arrows + float const maximum_width = visible_width - 2.0f * gutter_width; + + draw_background(); + map_mouse(); + + // account for extra space at the top and bottom + float visible_main_menu_height = 1.0f - 2.0f * ui().box_tb_border() - visible_extra_menu_height; + m_visible_lines = int(std::trunc(visible_main_menu_height / line_height)); + visible_main_menu_height = float(m_visible_lines) * line_height; + + // compute top/left of inner menu area by centering, if the menu is at the bottom of the extra, adjust + float const visible_top = ((1.0f - (visible_main_menu_height + visible_extra_menu_height)) * 0.5f) + get_customtop(); + + // lay out the text if necessary + if (!m_layout || (m_layout->width() != maximum_width)) + { + m_layout.emplace(ui().create_layout(container(), maximum_width)); + for (char const *const *line = copying_text; *line; ++line) + { + m_layout->add_text(*line, color); + m_layout->add_text("\n", color); + } + } + float const actual_width = m_layout->actual_width(); + + // compute text box size + float const x1 = visible_left + ((maximum_width - actual_width) * 0.5f); + float const y1 = visible_top - ui().box_tb_border(); + float const x2 = visible_left + visible_width - ((maximum_width - actual_width) * 0.5f); + float const y2 = visible_top + visible_main_menu_height + ui().box_tb_border() + extra_height; + float const effective_left = x1 + gutter_width; + float const line_x0 = x1 + 0.5f * UI_LINE_WIDTH; + float const line_x1 = x2 - 0.5f * UI_LINE_WIDTH; + float const separator = visible_top + float(m_visible_lines) * line_height; + + ui().draw_outlined_box(container(), x1, y1, x2, y2, ui().colors().background_color()); + + int const visible_items = m_layout->lines(); + m_visible_lines = (std::min)(visible_items, m_visible_lines); + top_line = (std::max)(0, top_line); + if (top_line + m_visible_lines >= visible_items) + top_line = visible_items - m_visible_lines; + + clear_hover(); + if (top_line) + { + // if we're on the top line, display the up arrow + rgb_t fgcolor = ui().colors().text_color(); + if (mouse_in_rect(line_x0, visible_top, line_x1, visible_top + line_height)) + { + fgcolor = ui().colors().mouseover_color(); + highlight( + line_x0, visible_top, + line_x1, visible_top + line_height, + ui().colors().mouseover_bg_color()); + set_hover(HOVER_ARROW_UP); + } + draw_arrow( + 0.5f * (x1 + x2) - 0.5f * ud_arrow_width, visible_top + 0.25f * line_height, + 0.5f * (x1 + x2) + 0.5f * ud_arrow_width, visible_top + 0.75f * line_height, + fgcolor, ROT0); + } + if ((top_line + m_visible_lines) < visible_items) + { + // if we're on the bottom line, display the down arrow + float const line_y = visible_top + float(m_visible_lines - 1) * line_height; + rgb_t fgcolor = ui().colors().text_color(); + if (mouse_in_rect(line_x0, line_y, line_x1, line_y + line_height)) + { + fgcolor = ui().colors().mouseover_color(); + highlight( + line_x0, line_y, + line_x1, line_y + line_height, + ui().colors().mouseover_bg_color()); + set_hover(HOVER_ARROW_DOWN); + } + draw_arrow( + 0.5f * (x1 + x2) - 0.5f * ud_arrow_width, line_y + 0.25f * line_height, + 0.5f * (x1 + x2) + 0.5f * ud_arrow_width, line_y + 0.75f * line_height, + fgcolor, ROT0 ^ ORIENTATION_FLIP_Y); + } + + // return the number of visible lines, minus 1 for top arrow and 1 for bottom arrow + m_visible_items = m_visible_lines - (top_line ? 1 : 0) - (top_line + m_visible_lines != visible_items); + m_layout->emit( + container(), + top_line ? (top_line + 1) : 0, m_visible_items, + effective_left, visible_top + (top_line ? line_height : 0.0f)); + + // add visual separator before the "return to prevous menu" item + container().add_line( + x1, separator + (0.5f * line_height), + x2, separator + (0.5f * line_height), + UI_LINE_WIDTH, ui().colors().text_color(), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA)); + + menu_item const &pitem = item(0); + std::string_view const itemtext = pitem.text; + float const line_y0 = separator + line_height; + float const line_y1 = line_y0 + line_height; + + if (mouse_in_rect(line_x0, line_y0, line_x1, line_y1) && is_selectable(pitem)) + set_hover(0); + + highlight(line_x0, line_y0, line_x1, line_y1, ui().colors().selected_bg_color()); + ui().draw_text_full( + container(), itemtext, + effective_left, line_y0, actual_width, + text_layout::text_justify::CENTER, text_layout::word_wrapping::TRUNCATE, + mame_ui_manager::NORMAL, + ui().colors().selected_color(), ui().colors().selected_bg_color(), + nullptr, nullptr); + + // if there is something special to add, do it by calling the virtual method + custom_render(get_selection_ref(), get_customtop(), get_custombottom(), x1, y1, x2, y2); +} + + +//------------------------------------------------- +// populate - populates the about modal +//------------------------------------------------- + +void menu_about::populate(float &customtop, float &custombottom) +{ + // make space for the title and revision + customtop = (ui().get_line_height() * m_header.size()) + (ui().box_tb_border() * 3.0f); +} + + +//------------------------------------------------- +// handle - manages inputs in the about modal +//------------------------------------------------- + +void menu_about::handle() +{ + const event *event = process(PROCESS_CUSTOM_NAV); + if (event) + { + switch (event->iptkey) + { + case IPT_UI_UP: + --top_line; + break; + + case IPT_UI_DOWN: + ++top_line; + break; + + case IPT_UI_PAGE_UP: + top_line -= m_visible_lines - 3; + break; + + case IPT_UI_PAGE_DOWN: + top_line += m_visible_lines - 3; + break; + + case IPT_UI_HOME: + top_line = 0; + break; + + case IPT_UI_END: + top_line = m_layout->lines() - m_visible_lines; + break; + } + } +} + +}; diff --git a/docs/release/src/frontend/mame/ui/inifile.cpp b/docs/release/src/frontend/mame/ui/inifile.cpp new file mode 100644 index 00000000000..02713cd5f57 --- /dev/null +++ b/docs/release/src/frontend/mame/ui/inifile.cpp @@ -0,0 +1,583 @@ +// license:BSD-3-Clause +// copyright-holders:Maurizio Petrarota, Vas Crabb +/*************************************************************************** + + ui/inifile.cpp + + UI INIs file manager. + +***************************************************************************/ + +#include "emu.h" +#include "ui/inifile.h" + +#include "ui/moptions.h" + +#include "language.h" + +#include "drivenum.h" +#include "softlist_dev.h" + +#include "corestr.h" + +#include <algorithm> +#include <cstring> +#include <iterator> + + +namespace { + +char const FAVORITE_FILENAME[] = "favorites.ini"; + +} // anonymous namespace + + +//------------------------------------------------- +// ctor +//------------------------------------------------- + +inifile_manager::inifile_manager(ui_options &options) + : m_options(options) + , m_ini_index() +{ + // scan directories and create index + file_enumerator path(m_options.categoryini_path()); + for (osd::directory::entry const *dir = path.next(); dir; dir = path.next()) + { + std::string name(dir->name); + if (core_filename_ends_with(name, ".ini")) + { + emu_file file(m_options.categoryini_path(), OPEN_FLAG_READ); + if (!file.open(name)) + { + init_category(std::move(name), file); + file.close(); + } + } + } + std::stable_sort(m_ini_index.begin(), m_ini_index.end(), [] (auto const &x, auto const &y) { return 0 > core_stricmp(x.first.c_str(), y.first.c_str()); }); +} + +//------------------------------------------------- +// load and indexing ini files +//------------------------------------------------- + +// HBMAME - rewritten using native file usage because mame's file.gets doesn't always work +void inifile_manager::load_ini_category(size_t file, size_t category, std::unordered_set<game_driver const *> &result) const +{ + std::string const &filename(m_ini_index[file].first); + FILE *fp = fopen(filename.c_str(), "r"); + if (!fp) + { + osd_printf_error("Failed to open category file %s for reading\n", filename); + return; + } + + int64_t const offset(m_ini_index[file].second[category].second); + if (fseek(fp, offset, SEEK_SET)) + { + fclose(fp); + osd_printf_error("Failed to seek to category offset in file %s\n", filename); + return; + } + + char rbuf[MAX_CHAR_INFO]; + while (fgets(rbuf, MAX_CHAR_INFO, fp) && rbuf[0] && ('[' != rbuf[0])) + { + auto const tail(std::find_if(std::begin(rbuf), std::prev(std::end(rbuf)), [] (char ch) { return !ch || ('\r' == ch) || ('\n' == ch); })); + *tail = '\0'; + int const dfind(driver_list::find(rbuf)); + if (0 <= dfind) + result.emplace(&driver_list::driver(dfind)); + } + + fclose(fp); +} + +//------------------------------------------------- +// initialize category +//------------------------------------------------- + +// HBMAME - rewritten using native file usage because mame's file.gets doesn't always work +void inifile_manager::init_category(std::string &&filename, emu_file &file) +{ + FILE *fp; + fp = fopen(file.fullpath(), "r"); + if (!fp) + return; + categoryindex index; + char rbuf[MAX_CHAR_INFO]; + std::string name; + while (fgets(rbuf, std::size(rbuf), fp)) + { + if ('[' == rbuf[0]) + { + auto const head(std::next(std::begin(rbuf))); + auto const tail(std::find_if(head, std::end(rbuf), [] (char ch) { return !ch || (']' == ch); })); + name.assign(head, tail); + if ("FOLDER_SETTINGS" != name) + index.emplace_back(std::move(name), ftell(fp)); + } + } + std::stable_sort(index.begin(), index.end(), [] (auto const &x, auto const &y) { return 0 > core_stricmp(x.first.c_str(), y.first.c_str()); }); + if (!index.empty()) + m_ini_index.emplace_back(file.fullpath(), std::move(index)); + + fclose(fp); +} + + +/************************************************************************** + FAVORITE MANAGER +**************************************************************************/ + +bool favorite_manager::favorite_compare::operator()(ui_software_info const &lhs, ui_software_info const &rhs) const +{ + assert(lhs.driver); + assert(rhs.driver); + + if (!lhs.startempty) + { + if (rhs.startempty) + return false; + else if (lhs.listname < rhs.listname) + return true; + else if (lhs.listname > rhs.listname) + return false; + else if (lhs.shortname < rhs.shortname) + return true; + else if (lhs.shortname > rhs.shortname) + return false; + } + else if (!rhs.startempty) + { + return true; + } + + return 0 > std::strncmp(lhs.driver->name, rhs.driver->name, std::size(lhs.driver->name)); +} + +bool favorite_manager::favorite_compare::operator()(ui_software_info const &lhs, game_driver const &rhs) const +{ + assert(lhs.driver); + + if (!lhs.startempty) + return false; + else + return 0 > std::strncmp(lhs.driver->name, rhs.name, std::size(rhs.name)); +} + +bool favorite_manager::favorite_compare::operator()(game_driver const &lhs, ui_software_info const &rhs) const +{ + assert(rhs.driver); + + if (!rhs.startempty) + return true; + else + return 0 > std::strncmp(lhs.name, rhs.driver->name, std::size(lhs.name)); +} + +bool favorite_manager::favorite_compare::operator()(ui_software_info const &lhs, running_software_key const &rhs) const +{ + assert(lhs.driver); + assert(std::get<1>(rhs)); + + if (lhs.startempty) + return true; + else if (lhs.listname < std::get<1>(rhs)) + return true; + else if (lhs.listname > std::get<1>(rhs)) + return false; + else if (lhs.shortname < std::get<2>(rhs)) + return true; + else if (lhs.shortname > std::get<2>(rhs)) + return false; + else + return 0 > std::strncmp(lhs.driver->name, std::get<0>(rhs).name, std::size(lhs.driver->name)); +} + +bool favorite_manager::favorite_compare::operator()(running_software_key const &lhs, ui_software_info const &rhs) const +{ + assert(std::get<1>(lhs)); + assert(rhs.driver); + + if (rhs.startempty) + return false; + else if (std::get<1>(lhs) < rhs.listname) + return true; + else if (std::get<1>(lhs) > rhs.listname) + return false; + else if (std::get<2>(lhs) < rhs.shortname) + return true; + else if (std::get<2>(lhs) > rhs.shortname) + return false; + else + return 0 > std::strncmp(std::get<0>(lhs).name, rhs.driver->name, std::size(rhs.driver->name)); +} + + +//------------------------------------------------- +// construction/destruction +//------------------------------------------------- + +favorite_manager::favorite_manager(ui_options &options) + : m_options(options) + , m_favorites() + , m_sorted() + , m_need_sort(true) +{ + emu_file file(m_options.ui_path(), OPEN_FLAG_READ); + if (!file.open(FAVORITE_FILENAME)) + { + char readbuf[1024]; + file.gets(readbuf, std::size(readbuf)); + + while (readbuf[0] == '[') + file.gets(readbuf, std::size(readbuf)); + + while (file.gets(readbuf, std::size(readbuf))) + { + ui_software_info tmpmatches; + tmpmatches.shortname = chartrimcarriage(readbuf); + file.gets(readbuf, std::size(readbuf)); + tmpmatches.longname = chartrimcarriage(readbuf); + file.gets(readbuf, std::size(readbuf)); + tmpmatches.parentname = chartrimcarriage(readbuf); + file.gets(readbuf, std::size(readbuf)); + tmpmatches.year = chartrimcarriage(readbuf); + file.gets(readbuf, std::size(readbuf)); + tmpmatches.publisher = chartrimcarriage(readbuf); + file.gets(readbuf, std::size(readbuf)); + tmpmatches.supported = software_support(atoi(readbuf)); + file.gets(readbuf, std::size(readbuf)); + tmpmatches.part = chartrimcarriage(readbuf); + file.gets(readbuf, std::size(readbuf)); + chartrimcarriage(readbuf); + auto dx = driver_list::find(readbuf); + if (0 > dx) + continue; + tmpmatches.driver = &driver_list::driver(dx); + file.gets(readbuf, std::size(readbuf)); + tmpmatches.listname = chartrimcarriage(readbuf); + file.gets(readbuf, std::size(readbuf)); + tmpmatches.interface = chartrimcarriage(readbuf); + file.gets(readbuf, std::size(readbuf)); + tmpmatches.instance = chartrimcarriage(readbuf); + file.gets(readbuf, std::size(readbuf)); + tmpmatches.startempty = atoi(readbuf); + file.gets(readbuf, std::size(readbuf)); + tmpmatches.parentlongname = chartrimcarriage(readbuf); + file.gets(readbuf, std::size(readbuf)); + //tmpmatches.usage = chartrimcarriage(readbuf); TODO: recover multi-line info + file.gets(readbuf, std::size(readbuf)); + tmpmatches.devicetype = chartrimcarriage(readbuf); + file.gets(readbuf, std::size(readbuf)); + tmpmatches.available = atoi(readbuf); + + // need to populate this, it isn't displayed anywhere else + tmpmatches.infotext.append(tmpmatches.longname); + tmpmatches.infotext.append(1, '\n'); + tmpmatches.infotext.append(_("swlist-info", "Software list/item")); + tmpmatches.infotext.append(1, '\n'); + tmpmatches.infotext.append(tmpmatches.listname); + tmpmatches.infotext.append(1, ':'); + tmpmatches.infotext.append(tmpmatches.shortname); + + m_favorites.emplace(std::move(tmpmatches)); + } + file.close(); + } +} + + +//------------------------------------------------- +// add +//------------------------------------------------- + +void favorite_manager::add_favorite_system(game_driver const &driver) +{ + add_impl(driver); +} + +void favorite_manager::add_favorite_software(ui_software_info const &swinfo) +{ + add_impl(swinfo); +} + +void favorite_manager::add_favorite(running_machine &machine) +{ + apply_running_machine( + machine, + [this, &machine] (game_driver const &driver, device_image_interface *imagedev, software_info const *software, bool &done) + { + if (imagedev) + { + // creating this is fairly expensive, but we'll assume this usually succeeds + software_part const *const part(imagedev->part_entry()); + assert(software); + assert(part); + ui_software_info info( + *software, + *part, + driver, + imagedev->software_list_name(), + imagedev->instance_name(), + strensure(imagedev->image_type_name())); + + // assume it's available if it's mounted + info.available = true; + + // look up the parent in the list if necessary (eugh, O(n) walk) + if (!info.parentname.empty()) + { + auto const listdev = software_list_device::find_by_name(machine.config(), info.listname); + assert(listdev); + for (software_info const &other : listdev->get_info()) + { + if (other.shortname() == info.parentname) + { + info.parentlongname = other.longname(); + break; + } + } + } + + // hooray for move semantics! + add_impl(std::move(info)); + } + else + { + add_impl(driver); + } + }); +} + +template <typename T> void favorite_manager::add_impl(T &&key) +{ + auto const ins(m_favorites.emplace(std::forward<T>(key))); + if (ins.second) + { + if (!m_sorted.empty()) + m_sorted.emplace_back(std::ref(*ins.first)); + m_need_sort = true; + save_favorites(); + } +} + + +//------------------------------------------------- +// check +//------------------------------------------------- + +bool favorite_manager::is_favorite_system(game_driver const &driver) const +{ + return check_impl(driver); +} + +bool favorite_manager::is_favorite_software(ui_software_info const &swinfo) const +{ + auto found(m_favorites.lower_bound(swinfo)); + if ((m_favorites.end() != found) && (found->listname == swinfo.listname) && (found->shortname == swinfo.shortname)) + return true; + else if (m_favorites.begin() == found) + return false; + + // need to back up and check for matching software with lexically earlier driver + --found; + return (found->listname == swinfo.listname) && (found->shortname == swinfo.shortname); +} + +bool favorite_manager::is_favorite(running_machine &machine) const +{ + bool result(false); + apply_running_machine( + machine, + [this, &result] (game_driver const &driver, device_image_interface *imagedev, software_info const *software, bool &done) + { + assert(!result); + result = imagedev + ? check_impl(running_software_key(driver, imagedev->software_list_name(), software->shortname())) + : check_impl(driver); + done = done || result; + }); + return result; +} + +bool favorite_manager::is_favorite_system_software(ui_software_info const &swinfo) const +{ + return check_impl(swinfo); +} + +template <typename T> bool favorite_manager::check_impl(T const &key) const +{ + return m_favorites.find(key) != m_favorites.end(); +} + + +//------------------------------------------------- +// remove +//------------------------------------------------- + +void favorite_manager::remove_favorite_system(game_driver const &driver) +{ + remove_impl(driver); +} + +void favorite_manager::remove_favorite_software(ui_software_info const &swinfo) +{ + remove_impl(swinfo); +} + +void favorite_manager::remove_favorite(running_machine &machine) +{ + apply_running_machine( + machine, + [this] (game_driver const &driver, device_image_interface *imagedev, software_info const *software, bool &done) + { + done = imagedev + ? remove_impl(running_software_key(driver, imagedev->software_list_name(), software->shortname())) + : remove_impl(driver); + }); +} + +template <typename T> bool favorite_manager::remove_impl(T const &key) +{ + auto const found(m_favorites.find(key)); + if (m_favorites.end() != found) + { + m_favorites.erase(found); + m_sorted.clear(); + m_need_sort = true; + save_favorites(); + return true; + } + else + { + return false; + } +} + + +//------------------------------------------------- +// implementation +//------------------------------------------------- + +template <typename T> +void favorite_manager::apply_running_machine(running_machine &machine, T &&action) +{ + bool done(false); + + // TODO: this should be changed - it interacts poorly with cartslots on arcade systems + if ((machine.system().flags & machine_flags::MASK_TYPE) == machine_flags::TYPE_ARCADE) + { + action(machine.system(), nullptr, nullptr, done); + } + else + { + bool have_software(false); + for (device_image_interface &image_dev : image_interface_enumerator(machine.root_device())) + { + software_info const *const sw(image_dev.software_entry()); + if (image_dev.exists() && image_dev.loaded_through_softlist() && sw) + { + assert(image_dev.software_list_name()); + + have_software = true; + action(machine.system(), &image_dev, sw, done); + if (done) + return; + } + } + + if (!have_software) + action(machine.system(), nullptr, nullptr, done); + } +} + +void favorite_manager::update_sorted() +{ + if (m_need_sort) + { + if (m_sorted.empty()) + std::copy(m_favorites.begin(), m_favorites.end(), std::back_inserter(m_sorted)); + + assert(m_favorites.size() == m_sorted.size()); + std::stable_sort( + m_sorted.begin(), + m_sorted.end(), + [] (ui_software_info const &lhs, ui_software_info const &rhs) -> bool + { + assert(lhs.driver); + assert(rhs.driver); + + int cmp; + + cmp = core_stricmp(lhs.longname.c_str(), rhs.longname.c_str()); + if (0 > cmp) + return true; + else if (0 < cmp) + return false; + + cmp = core_stricmp(lhs.driver->type.fullname(), rhs.driver->type.fullname()); + if (0 > cmp) + return true; + else if (0 < cmp) + return false; + + cmp = std::strcmp(lhs.listname.c_str(), rhs.listname.c_str()); + if (0 > cmp) + return true; + else if (0 < cmp) + return false; + + return false; + }); + + m_need_sort = false; + } +} + +void favorite_manager::save_favorites() +{ + // attempt to open the output file + emu_file file(m_options.ui_path(), OPEN_FLAG_WRITE | OPEN_FLAG_CREATE | OPEN_FLAG_CREATE_PATHS); + if (!file.open(FAVORITE_FILENAME)) + { + if (m_favorites.empty()) + { + // delete it if there are no favorites + file.remove_on_close(); + } + else + { + // generate the favorite INI + file.puts("[ROOT_FOLDER]\n[Favorite]\n\n"); + util::ovectorstream buf; + for (ui_software_info const &info : m_favorites) + { + buf.clear(); + buf.rdbuf()->clear(); + + buf << info.shortname << '\n'; + buf << info.longname << '\n'; + buf << info.parentname << '\n'; + buf << info.year << '\n'; + buf << info.publisher << '\n'; + util::stream_format(buf, "%d\n", int(info.supported)); + buf << info.part << '\n'; + util::stream_format(buf, "%s\n", info.driver->name); + buf << info.listname << '\n'; + buf << info.interface << '\n'; + buf << info.instance << '\n'; + util::stream_format(buf, "%d\n", info.startempty); + buf << info.parentlongname << '\n'; + buf << '\n'; //buf << info.usage << '\n'; TODO: store multi-line info in a recoverable format + buf << info.devicetype << '\n'; + util::stream_format(buf, "%d\n", info.available); + + file.puts(util::buf_to_string_view(buf)); + } + } + file.close(); + } +} |