// license:BSD-3-Clause
// copyright-holders:Maurizio Petrarota, Vas Crabb
/*********************************************************************
ui/selgame.cpp
Main UI menu.
*********************************************************************/
#include "emu.h"
#include "ui/selgame.h"
#include "ui/auditmenu.h"
#include "ui/icorender.h"
#include "ui/inifile.h"
#include "ui/miscmenu.h"
#include "ui/optsmenu.h"
#include "ui/selector.h"
#include "ui/selsoft.h"
#include "ui/ui.h"
#include "infoxml.h"
#include "luaengine.h"
#include "mame.h"
#include "audit.h"
#include "drivenum.h"
#include "emuopts.h"
#include "rendutil.h"
#include "romload.h"
#include "softlist_dev.h"
#include "uiinput.h"
#include <atomic>
#include <condition_variable>
#include <cstring>
#include <iterator>
#include <memory>
#include <mutex>
#include <thread>
extern const char UI_VERSION_TAG[];
namespace ui {
namespace {
constexpr uint32_t FLAGS_UI = ui::menu::FLAG_LEFT_ARROW | ui::menu::FLAG_RIGHT_ARROW;
} // anonymous namespace
class menu_select_game::persistent_data
{
public:
enum available : unsigned
{
AVAIL_NONE = 0U,
AVAIL_SORTED_LIST = 1U << 0,
AVAIL_BIOS_COUNT = 1U << 1,
AVAIL_UCS_SHORTNAME = 1U << 2,
AVAIL_UCS_DESCRIPTION = 1U << 3,
AVAIL_UCS_MANUF_DESC = 1U << 4,
AVAIL_FILTER_DATA = 1U << 5
};
~persistent_data()
{
if (m_thread)
m_thread->join();
}
void cache_data()
{
std::unique_lock<std::mutex> lock(m_mutex);
do_start_caching();
}
bool is_available(available desired)
{
return (m_available.load(std::memory_order_acquire) & desired) == desired;
}
void wait_available(available desired)
{
if (!is_available(desired))
{
std::unique_lock<std::mutex> lock(m_mutex);
do_start_caching();
m_condition.wait(lock, [this, desired] () { return is_available(desired); });
}
}
std::vector<ui_system_info> &sorted_list()
{
wait_available(AVAIL_SORTED_LIST);
return m_sorted_list;
}
int bios_count()
{
wait_available(AVAIL_BIOS_COUNT);
return m_bios_count;
}
bool unavailable_systems()
{
wait_available(AVAIL_SORTED_LIST);
return std::find_if(m_sorted_list.begin(), m_sorted_list.end(), [] (ui_system_info const &info) { return !info.available; }) != m_sorted_list.end();
}
machine_filter_data &filter_data()
{
wait_available(AVAIL_FILTER_DATA);
return m_filter_data;
}
static persistent_data &instance()
{
static persistent_data data;
return data;
}
private:
persistent_data()
: m_started(false)
, m_available(AVAIL_NONE)
, m_bios_count(0)
{
}
void notify_available(available value)
{
std::unique_lock<std::mutex> lock(m_mutex);
m_available.fetch_or(value, std::memory_order_release);
m_condition.notify_all();
}
void do_start_caching()
{
if (!m_started)
{
m_started = true;
m_thread = std::make_unique<std::thread>([this] { do_cache_data(); });
}
}
void do_cache_data()
{
// generate full list
m_sorted_list.reserve(driver_list::total());
std::unordered_set<std::string> manufacturers, years;
for (int x = 0; x < driver_list::total(); ++x)
{
game_driver const &driver(driver_list::driver(x));
if (&driver != &GAME_NAME(___empty))
{
if (driver.flags & machine_flags::IS_BIOS_ROOT)
++m_bios_count;
m_sorted_list.emplace_back(driver, x, false);
m_filter_data.add_manufacturer(driver.manufacturer);
m_filter_data.add_year(driver.year);
}
}
// notify that BIOS count is valie
notify_available(AVAIL_BIOS_COUNT);
// sort drivers and notify
std::stable_sort(
m_sorted_list.begin(),
m_sorted_list.end(),
[] (ui_system_info const &lhs, ui_system_info const &rhs) { return sorted_game_list(lhs.driver, rhs.driver); });
notify_available(AVAIL_SORTED_LIST);
// sort manufacturers and years
m_filter_data.finalise();
notify_available(AVAIL_FILTER_DATA);
// convert shortnames to UCS-4
for (ui_system_info &info : m_sorted_list)
info.ucs_shortname = ustr_from_utf8(normalize_unicode(info.driver->name, unicode_normalization_form::D, true));
notify_available(AVAIL_UCS_SHORTNAME);
// convert descriptions to UCS-4
for (ui_system_info &info : m_sorted_list)
info.ucs_description = ustr_from_utf8(normalize_unicode(info.driver->type.fullname(), unicode_normalization_form::D, true));
notify_available(AVAIL_UCS_DESCRIPTION);
// convert "<manufacturer> <description>" to UCS-4
std::string buf;
for (ui_system_info &info : m_sorted_list)
{
buf.assign(info.driver->manufacturer);
buf.append(1, ' ');
buf.append(info.driver->type.fullname());
info.ucs_manufacturer_description = ustr_from_utf8(normalize_unicode(buf, unicode_normalization_form::D, true));
}
notify_available(AVAIL_UCS_MANUF_DESC);
}
// synchronisation
std::mutex m_mutex;
std::condition_variable m_condition;
std::unique_ptr<std::thread> m_thread;
std::atomic<bool> m_started;
std::atomic<unsigned> m_available;
// data
std::vector<ui_system_info> m_sorted_list;
machine_filter_data m_filter_data;
int m_bios_count;
};
bool menu_select_game::s_first_start = true;
//-------------------------------------------------
// ctor
//-------------------------------------------------
menu_select_game::menu_select_game(mame_ui_manager &mui, render_container &container, const char *gamename)
: menu_select_launch(mui, container, false)
, m_persistent_data(persistent_data::instance())
, m_icons(MAX_ICONS_RENDER)
, m_icon_paths()
, m_displaylist()
, m_searchlist()
, m_searched_fields(persistent_data::AVAIL_NONE)
, m_populated_favorites(false)
{
std::string error_string, last_filter, sub_filter;
ui_options &moptions = mui.options();
// load drivers cache
m_persistent_data.cache_data();
// check if there are available system icons
check_for_icons(nullptr);
// build drivers list
if (!load_available_machines())
build_available_list();
if (s_first_start)
{
//s_first_start = false; TODO: why wansn't it ever clearing the first start flag?
reselect_last::set_driver(moptions.last_used_machine());
ui_globals::rpanel = std::min<int>(std::max<int>(moptions.last_right_panel(), RP_FIRST), RP_LAST);
std::string tmp(moptions.last_used_filter());
std::size_t const found = tmp.find_first_of(',');
std::string fake_ini;
if (found == std::string::npos)
{
fake_ini = util::string_format("%s = 1\n", tmp);
}
else
{
std::string const sub_filter(tmp.substr(found + 1));
tmp.resize(found);
fake_ini = util::string_format("%s = %s\n", tmp, sub_filter);
}
emu_file file(ui().options().ui_path(), OPEN_FLAG_READ);
if (file.open_ram(fake_ini.c_str(), fake_ini.size()) == osd_file::error::NONE)
{
m_persistent_data.filter_data().load_ini(file);
file.close();
}
}
// do this after processing the last used filter setting so it overwrites the placeholder
load_custom_filters();
m_filter_highlight = m_persistent_data.filter_data().get_current_filter_type();
if (!moptions.remember_last())
reselect_last::reset();
mui.machine().options().set_value(OPTION_SNAPNAME, "%g/%i", OPTION_PRIORITY_CMDLINE);
ui_globals::curdats_view = 0;
ui_globals::panels_status = moptions.hide_panels();
ui_globals::curdats_total = 1;
}
//-------------------------------------------------
// dtor
//-------------------------------------------------
menu_select_game::~menu_select_game()
{
std::string error_string, last_driver;
game_driver const *driver;
ui_software_info const *swinfo;
get_selection(swinfo, driver);
if (swinfo)
last_driver = swinfo->shortname;
else
if (driver)
last_driver = driver->name;
std::string const filter(m_persistent_data.filter_data().get_config_string());
ui_options &mopt = ui().options();
mopt.set_value(OPTION_LAST_RIGHT_PANEL, ui_globals::rpanel, OPTION_PRIORITY_CMDLINE);
mopt.set_value(OPTION_LAST_USED_FILTER, filter, OPTION_PRIORITY_CMDLINE);
mopt.set_value(OPTION_LAST_USED_MACHINE, last_driver, OPTION_PRIORITY_CMDLINE);
mopt.set_value(OPTION_HIDE_PANELS, ui_globals::panels_status, OPTION_PRIORITY_CMDLINE);
ui().save_ui_options();
}
//-------------------------------------------------
// handle
//-------------------------------------------------
void menu_select_game::handle()
{
if (!m_prev_selected)
m_prev_selected = item(0).ref;
// if I have to load datfile, perform a hard reset
if (ui_globals::reset)
{
ui_globals::reset = false;
machine().schedule_hard_reset();
stack_reset();
return;
}
// if I have to select software, force software list submenu
if (reselect_last::get())
{
const game_driver *driver;
const ui_software_info *software;
get_selection(software, driver);
menu::stack_push<menu_select_software>(ui(), container(), *driver);
return;
}
// ignore pause keys by swallowing them before we process the menu
machine().ui_input().pressed(IPT_UI_PAUSE);
// process the menu
const event *menu_event = process(PROCESS_LR_REPEAT);
if (menu_event)
{
if (dismiss_error())
{
// reset the error on any future menu_event
}
else switch (menu_event->iptkey)
{
case IPT_UI_UP:
if ((get_focus() == focused_menu::LEFT) && (machine_filter::FIRST < m_filter_highlight))
--m_filter_highlight;
break;
case IPT_UI_DOWN:
if ((get_focus() == focused_menu::LEFT) && (machine_filter::LAST > m_filter_highlight))
m_filter_highlight++;
break;
case IPT_UI_HOME:
if (get_focus() == focused_menu::LEFT)
m_filter_highlight = machine_filter::FIRST;
break;
case IPT_UI_END:
if (get_focus() == focused_menu::LEFT)
m_filter_highlight = machine_filter::LAST;
break;
case IPT_UI_EXPORT:
inkey_export();
break;
case IPT_UI_DATS:
inkey_dats();
break;
default:
if (menu_event->itemref)
{
switch (menu_event->iptkey)
{
case IPT_UI_SELECT:
if (get_focus() == focused_menu::MAIN)
{
if (m_populated_favorites)
inkey_select_favorite(menu_event);
else
inkey_select(menu_event);
}
break;
case IPT_CUSTOM:
// handle IPT_CUSTOM (mouse right click)
if (!m_populated_favorites)
{
menu::stack_push<menu_machine_configure>(
ui(), container(),
*reinterpret_cast<const game_driver *>(m_prev_selected),
nullptr,
menu_event->mouse.x0, menu_event->mouse.y0);
}
else
{
ui_software_info *sw = reinterpret_cast<ui_software_info *>(m_prev_selected);
menu::stack_push<menu_machine_configure>(
ui(), container(),
*sw->driver,
[this, empty = sw->startempty] (bool fav, bool changed)
{
if (changed)
reset(empty ? reset_options::SELECT_FIRST : reset_options::REMEMBER_REF);
},
menu_event->mouse.x0, menu_event->mouse.y0);
}
break;
case IPT_UI_LEFT:
if (ui_globals::rpanel == RP_IMAGES)
{
// Images
previous_image_view();
}
else if (ui_globals::rpanel == RP_INFOS)
{
// Infos
change_info_pane(-1);
}
break;
case IPT_UI_RIGHT:
if (ui_globals::rpanel == RP_IMAGES)
{
// Images
next_image_view();
}
else if (ui_globals::rpanel == RP_INFOS)
{
// Infos
change_info_pane(1);
}
break;
case IPT_UI_FAVORITES:
if (uintptr_t(menu_event->itemref) > skip_main_items)
{
favorite_manager &mfav(mame_machine_manager::instance()->favorite());
if (!m_populated_favorites)
{
game_driver const *const driver(reinterpret_cast<game_driver const *>(menu_event->itemref));
if (!mfav.is_favorite_system(*driver))
{
mfav.add_favorite_system(*driver);
machine().popmessage(_("%s\n added to favorites list."), driver->type.fullname());
}
else
{
mfav.remove_favorite_system(*driver);
machine().popmessage(_("%s\n removed from favorites list."), driver->type.fullname());
}
}
else
{
ui_software_info const *const swinfo(reinterpret_cast<ui_software_info const *>(menu_event->itemref));
machine().popmessage(_("%s\n removed from favorites list."), swinfo->longname);
mfav.remove_favorite_software(*swinfo);
reset(reset_options::SELECT_FIRST);
}
}
break;
case IPT_UI_AUDIT_FAST:
if (m_persistent_data.unavailable_systems())
menu::stack_push<menu_audit>(ui(), container(), m_persistent_data.sorted_list(), menu_audit::mode::FAST);
break;
case IPT_UI_AUDIT_ALL:
menu::stack_push<menu_audit>(ui(), container(), m_persistent_data.sorted_list(), menu_audit::mode::ALL);
break;
}
}
}
}
// if we're in an error state, overlay an error message
draw_error_text();
}
//-------------------------------------------------
// populate
//-------------------------------------------------
void menu_select_game::populate(float &customtop, float &custombottom)
{
for (auto &icon : m_icons) // TODO: why is this here? maybe better on resize or setting change?
icon.second.texture.reset();
set_switch_image();
int old_item_selected = -1;
if (!isfavorite())
{
m_populated_favorites = false;
m_displaylist.clear();
machine_filter const *const flt(m_persistent_data.filter_data().get_current_filter());
// if search is not empty, find approximate matches
if (!m_search.empty())
{
populate_search();
if (flt)
{
for (auto it = m_searchlist.begin(); (m_searchlist.end() != it) && (MAX_VISIBLE_SEARCH > m_displaylist.size()); ++it)
{
if (flt->apply(it->second))
m_displaylist.emplace_back(it->second);
}
}
else
{
std::transform(
m_searchlist.begin(),
std::next(m_searchlist.begin(), (std::min)(m_searchlist.size(), MAX_VISIBLE_SEARCH)),
std::back_inserter(m_displaylist),
[] (auto const &entry) { return entry.second; });
}
}
else
{
// if filter is set on category, build category list
std::vector<ui_system_info> const &sorted(m_persistent_data.sorted_list());
if (!flt)
std::copy(sorted.begin(), sorted.end(), std::back_inserter(m_displaylist));
else
flt->apply(sorted.begin(), sorted.end(), std::back_inserter(m_displaylist));
}
// iterate over entries
int curitem = 0;
for (ui_system_info const &elem : m_displaylist)
{
if (old_item_selected == -1 && elem.driver->name == reselect_last::driver())
old_item_selected = curitem;
bool cloneof = strcmp(elem.driver->parent, "0");
if (cloneof)
{
int cx = driver_list::find(elem.driver->parent);
if (cx != -1 && ((driver_list::driver(cx).flags & machine_flags::IS_BIOS_ROOT) != 0))
cloneof = false;
}
item_append(elem.driver->type.fullname(), "", (cloneof) ? (FLAGS_UI | FLAG_INVERT) : FLAGS_UI, (void *)elem.driver);
curitem++;
}
}
else
{
// populate favorites list
m_populated_favorites = true;
m_search.clear();
mame_machine_manager::instance()->favorite().apply_sorted(
[this, &old_item_selected, curitem = 0] (ui_software_info const &info) mutable
{
if (info.startempty == 1)
{
if (old_item_selected == -1 && info.shortname == reselect_last::driver())
old_item_selected = curitem;
bool cloneof = strcmp(info.driver->parent, "0");
if (cloneof)
{
int cx = driver_list::find(info.driver->parent);
if (cx != -1 && ((driver_list::driver(cx).flags & machine_flags::IS_BIOS_ROOT) != 0))
cloneof = false;
}
item_append(info.longname, "", cloneof ? (FLAGS_UI | FLAG_INVERT) : FLAGS_UI, (void *)&info);
}
else
{
if (old_item_selected == -1 && info.shortname == reselect_last::driver())
old_item_selected = curitem;
item_append(info.longname, info.devicetype,
info.parentname.empty() ? FLAGS_UI : (FLAG_INVERT | FLAGS_UI), (void *)&info);
}
curitem++;
});
}
item_append(menu_item_type::SEPARATOR, FLAGS_UI);
// add special items
if (stack_has_special_main_menu())
{
item_append(_("Configure Options"), "", FLAGS_UI, (void *)(uintptr_t)CONF_OPTS);
item_append(_("Configure Machine"), "", FLAGS_UI, (void *)(uintptr_t)CONF_MACHINE);
skip_main_items = 2;
if (machine().options().plugins())
{
item_append(_("Plugins"), "", FLAGS_UI, (void *)(uintptr_t)CONF_PLUGINS);
skip_main_items++;
}
}
else
skip_main_items = 0;
// configure the custom rendering
customtop = 3.0f * ui().get_line_height() + 5.0f * ui().box_tb_border();
custombottom = 5.0f * ui().get_line_height() + 3.0f * ui().box_tb_border();
// reselect prior game launched, if any
if (old_item_selected != -1)
{
set_selected_index(old_item_selected);
if (ui_globals::visible_main_lines == 0)
top_line = (selected_index() != 0) ? selected_index() - 1 : 0;
else
top_line = selected_index() - (ui_globals::visible_main_lines / 2);
if (reselect_last::software().empty())
reselect_last::reset();
}
else
{
reselect_last::reset();
}
}
//-------------------------------------------------
// build a list of available drivers
//-------------------------------------------------
void menu_select_game::build_available_list()
{
std::size_t const total = driver_list::total();
std::vector<bool> included(total, false);
// iterate over ROM directories and look for potential ROMs
file_enumerator path(machine().options().media_path());
for (osd::directory::entry const *dir = path.next(); dir; dir = path.next())
{
char drivername[50];
char *dst = drivername;
char const *src;
// build a name for it
for (src = dir->name; *src != 0 && *src != '.' && dst < &drivername[ARRAY_LENGTH(drivername) - 1]; ++src)
*dst++ = tolower(uint8_t(*src));
*dst = 0;
int const drivnum = driver_list::find(drivername);
if (0 <= drivnum)
included[drivnum] = true;
}
// now check and include NONE_NEEDED
if (!ui().options().hide_romless())
{
// FIXME: can't use the convenience macros tiny ROM entries
auto const is_required_rom =
[] (tiny_rom_entry const &rom) { return ROMENTRY_ISFILE(rom) && !ROM_ISOPTIONAL(rom) && !std::strchr(rom.hashdata, '!'); };
for (std::size_t x = 0; total > x; ++x)
{
game_driver const &driver(driver_list::driver(x));
if (!included[x] && (&GAME_NAME(___empty) != &driver))
{
bool noroms(true);
tiny_rom_entry const *rom;
for (rom = driver.rom; !ROMENTRY_ISEND(rom); ++rom)
{
// check optional and NO_DUMP
if (is_required_rom(*rom))
{
noroms = false;
break; // break before incrementing, or it will subtly break the check for all ROMs belonging to parent
}
}
if (!noroms)
{
// check if clone == parent
auto const cx(driver_list::clone(driver));
if ((0 <= cx) && included[cx])
{
game_driver const &parent(driver_list::driver(cx));
if (driver.rom == parent.rom)
{
noroms = true;
}
else
{
// check if clone < parent
noroms = true;
for ( ; noroms && !ROMENTRY_ISEND(rom); ++rom)
{
if (is_required_rom(*rom))
{
util::hash_collection const hashes(rom->hashdata);
bool found(false);
for (tiny_rom_entry const *parentrom = parent.rom; !found && !ROMENTRY_ISEND(parentrom); ++parentrom)
{
if (is_required_rom(*parentrom) && (rom->length == parentrom->length))
{
util::hash_collection const parenthashes(parentrom->hashdata);
if (hashes == parenthashes)
found = true;
}
}
noroms = found;
}
}
}
}
}
if (noroms)
included[x] = true;
}
}
}
// copy into the persistent sorted list
for (ui_system_info &info : m_persistent_data.sorted_list())
info.available = included[info.index];
}
//-------------------------------------------------
// force the game select menu to be visible
// and inescapable
//-------------------------------------------------
void menu_select_game::force_game_select(mame_ui_manager &mui, render_container &container)
{
// reset the menu stack
menu::stack_reset(mui.machine());
// add the quit entry followed by the game select entry
menu::stack_push_special_main<menu_quit_game>(mui, container);
menu::stack_push<menu_select_game>(mui, container, nullptr);
// force the menus on
mui.show_menu();
// make sure MAME is paused
mui.machine().pause();
}
//-------------------------------------------------
// handle select key event
//-------------------------------------------------
void menu_select_game::inkey_select(const event *menu_event)
{
const game_driver *driver = (const game_driver *)menu_event->itemref;
if ((uintptr_t)driver == CONF_OPTS)
{
// special case for configure options
menu::stack_push<menu_game_options>(
ui(),
container(),
m_persistent_data.filter_data(),
[this] () { reset(reset_options::SELECT_FIRST); });
}
else if (uintptr_t(driver) == CONF_MACHINE)
{
// special case for configure machine
if (m_prev_selected)
menu::stack_push<menu_machine_configure>(ui(), container(), *reinterpret_cast<const game_driver *>(m_prev_selected));
return;
}
else if ((uintptr_t)driver == CONF_PLUGINS)
{
// special case for configure plugins
menu::stack_push<menu_plugins_configure>(ui(), container());
}
else
{
// anything else is a driver
// audit the game first to see if we're going to work
driver_enumerator enumerator(machine().options(), *driver);
enumerator.next();
media_auditor auditor(enumerator);
media_auditor::summary const summary = auditor.audit_media(AUDIT_VALIDATE_FAST);
// if everything looks good, schedule the new driver
if (summary == media_auditor::CORRECT || summary == media_auditor::BEST_AVAILABLE || summary == media_auditor::NONE_NEEDED)
{
for (software_list_device &swlistdev : software_list_device_iterator(enumerator.config()->root_device()))
{
if (!swlistdev.get_info().empty())
{
menu::stack_push<menu_select_software>(ui(), container(), *driver);
return;
}
}
if (!select_bios(*driver, false))
launch_system(*driver);
}
else
{
// otherwise, display an error
set_error(reset_options::REMEMBER_REF, make_error_text(media_auditor::NOTFOUND != summary, auditor));
}
}
}
//-------------------------------------------------
// handle select key event for favorites menu
//-------------------------------------------------
void menu_select_game::inkey_select_favorite(const event *menu_event)
{
ui_software_info *ui_swinfo = (ui_software_info *)menu_event->itemref;
if ((uintptr_t)ui_swinfo == CONF_OPTS)
{
// special case for configure options
menu::stack_push<menu_game_options>(
ui(),
container(),
m_persistent_data.filter_data(),
[this] () { reset(reset_options::SELECT_FIRST); });
}
else if ((uintptr_t)ui_swinfo == CONF_MACHINE)
{
// special case for configure machine
if (m_prev_selected)
{
ui_software_info *swinfo = reinterpret_cast<ui_software_info *>(m_prev_selected);
menu::stack_push<menu_machine_configure>(
ui(),
container(),
*swinfo->driver,
[this, empty = swinfo->startempty] (bool fav, bool changed)
{
if (changed)
reset(empty ? reset_options::SELECT_FIRST : reset_options::REMEMBER_REF);
});
}
return;
}
else if ((uintptr_t)ui_swinfo == CONF_PLUGINS)
{
// special case for configure plugins
menu::stack_push<menu_plugins_configure>(ui(), container());
}
else if (ui_swinfo->startempty == 1)
{
// audit the game first to see if we're going to work
driver_enumerator enumerator(machine().options(), *ui_swinfo->driver);
enumerator.next();
media_auditor auditor(enumerator);
media_auditor::summary const summary = auditor.audit_media(AUDIT_VALIDATE_FAST);
if (summary == media_auditor::CORRECT || summary == media_auditor::BEST_AVAILABLE || summary == media_auditor::NONE_NEEDED)
{
for (software_list_device &swlistdev : software_list_device_iterator(enumerator.config()->root_device()))
{
if (!swlistdev.get_info().empty())
{
menu::stack_push<menu_select_software>(ui(), container(), *ui_swinfo->driver);
return;
}
}
// if everything looks good, schedule the new driver
if (!select_bios(*ui_swinfo->driver, false))
{
reselect_last::reselect(true);
launch_system(*ui_swinfo->driver);
}
}
else
{
// otherwise, display an error
set_error(reset_options::REMEMBER_REF, make_error_text(media_auditor::NOTFOUND != summary, auditor));
}
}
else
{
// first validate
driver_enumerator drv(machine().options(), *ui_swinfo->driver);
media_auditor auditor(drv);
drv.next();
software_list_device *swlist = software_list_device::find_by_name(*drv.config(), ui_swinfo->listname);
const software_info *swinfo = swlist->find(ui_swinfo->shortname);
media_auditor::summary const summary = auditor.audit_software(*swlist, *swinfo, AUDIT_VALIDATE_FAST);
if (summary == media_auditor::CORRECT || summary == media_auditor::BEST_AVAILABLE || summary == media_auditor::NONE_NEEDED)
{
if (!select_bios(*ui_swinfo, false) && !select_part(*swinfo, *ui_swinfo))
launch_system(drv.driver(), *ui_swinfo, ui_swinfo->part);
}
else
{
// otherwise, display an error
set_error(reset_options::REMEMBER_POSITION, make_error_text(media_auditor::NOTFOUND != summary, auditor));
}
}
}
//-------------------------------------------------
// returns if the search can be activated
//-------------------------------------------------
bool menu_select_game::isfavorite() const
{
return machine_filter::FAVORITE == m_persistent_data.filter_data().get_current_filter_type();
}
//-------------------------------------------------
// change what's displayed in the info box
//-------------------------------------------------
void menu_select_game::change_info_pane(int delta)
{
auto const cap_delta = [this, &delta] (uint8_t ¤t, uint8_t &total)
{
if ((0 > delta) && (-delta > current))
delta = -int(unsigned(current));
else if ((0 < delta) && ((current + unsigned(delta)) >= total))
delta = int(unsigned(total - current - 1));
if (delta)
{
current += delta;
m_topline_datsview = 0;
}
};
game_driver const *drv;
ui_software_info const *soft;
get_selection(soft, drv);
if (!m_populated_favorites)
{
if (uintptr_t(drv) > skip_main_items)
cap_delta(ui_globals::curdats_view, ui_globals::curdats_total);
}
else if (uintptr_t(soft) > skip_main_items)
{
if (soft->startempty)
cap_delta(ui_globals::curdats_view, ui_globals::curdats_total);
else
cap_delta(ui_globals::cur_sw_dats_view, ui_globals::cur_sw_dats_total);
}
}
//-------------------------------------------------
// populate search list
//-------------------------------------------------
void menu_select_game::populate_search()
{
// ensure search list is populated
if (m_searchlist.empty())
{
std::vector<ui_system_info> const &sorted(m_persistent_data.sorted_list());
m_searchlist.reserve(sorted.size());
for (ui_system_info const &info : sorted)
m_searchlist.emplace_back(1.0, std::ref(info));
}
// keep track of what we matched against
const std::u32string ucs_search(ustr_from_utf8(normalize_unicode(m_search, unicode_normalization_form::D, true)));
// match shortnames
if (m_persistent_data.is_available(persistent_data::AVAIL_UCS_SHORTNAME))
{
m_searched_fields |= persistent_data::AVAIL_UCS_SHORTNAME;
for (std::pair<double, std::reference_wrapper<ui_system_info const> > &info : m_searchlist)
info.first = util::edit_distance(ucs_search, info.second.get().ucs_shortname);
}
// match descriptions
if (m_persistent_data.is_available(persistent_data::AVAIL_UCS_DESCRIPTION))
{
m_searched_fields |= persistent_data::AVAIL_UCS_DESCRIPTION;
for (std::pair<double, std::reference_wrapper<ui_system_info const> > &info : m_searchlist)
{
if (info.first)
{
double const penalty(util::edit_distance(ucs_search, info.second.get().ucs_description));
info.first = (std::min)(penalty, info.first);
}
}
}
// match "<manufacturer> <description>"
if (m_persistent_data.is_available(persistent_data::AVAIL_UCS_MANUF_DESC))
{
m_searched_fields |= persistent_data::AVAIL_UCS_MANUF_DESC;
for (std::pair<double, std::reference_wrapper<ui_system_info const> > &info : m_searchlist)
{
if (info.first)
{
double const penalty(util::edit_distance(ucs_search, info.second.get().ucs_manufacturer_description));
info.first = (std::min)(penalty, info.first);
}
}
}
// sort according to edit distance
std::stable_sort(
m_searchlist.begin(),
m_searchlist.end(),
[] (auto const &lhs, auto const &rhs) { return lhs.first < rhs.first; });
}
//-------------------------------------------------
// generate general info
//-------------------------------------------------
void menu_select_game::general_info(const game_driver *driver, std::string &buffer)
{
system_flags const &flags(get_system_flags(*driver));
std::ostringstream str;
str << "#j2\n";
util::stream_format(str, _("Romset\t%1$-.100s\n"), driver->name);
util::stream_format(str, _("Year\t%1$s\n"), driver->year);
util::stream_format(str, _("Manufacturer\t%1$-.100s\n"), driver->manufacturer);
int cloneof = driver_list::non_bios_clone(*driver);
if (cloneof != -1)
util::stream_format(str, _("Driver is Clone of\t%1$-.100s\n"), driver_list::driver(cloneof).type.fullname());
else
str << _("Driver is Parent\t\n");
if (flags.has_analog())
str << _("Analog Controls\tYes\n");
if (flags.has_keyboard())
str << _("Keyboard Inputs\tYes\n");
if (flags.machine_flags() & machine_flags::NOT_WORKING)
str << _("Overall\tNOT WORKING\n");
else if ((flags.unemulated_features() | flags.imperfect_features()) & device_t::feature::PROTECTION)
str << _("Overall\tUnemulated Protection\n");
else
str << _("Overall\tWorking\n");
if (flags.unemulated_features() & device_t::feature::GRAPHICS)
str << _("Graphics\tUnimplemented\n");
else if (flags.unemulated_features() & device_t::feature::PALETTE)
str << _("Graphics\tWrong Colors\n");
else if (flags.imperfect_features() & device_t::feature::PALETTE)
str << _("Graphics\tImperfect Colors\n");
else if (flags.imperfect_features() & device_t::feature::GRAPHICS)
str << _("Graphics\tImperfect\n");
else
str << _("Graphics\tOK\n");
if (flags.machine_flags() & machine_flags::NO_SOUND_HW)
str << _("Sound\tNone\n");
else if (flags.unemulated_features() & device_t::feature::SOUND)
str << _("Sound\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::SOUND)
str << _("Sound\tImperfect\n");
else
str << _("Sound\tOK\n");
if (flags.unemulated_features() & device_t::feature::CAPTURE)
str << _("Capture\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::CAPTURE)
str << _("Capture\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::CAMERA)
str << _("Camera\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::CAMERA)
str << _("Camera\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::MICROPHONE)
str << _("Microphone\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::MICROPHONE)
str << _("Microphone\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::CONTROLS)
str << _("Controls\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::CONTROLS)
str << _("Controls\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::KEYBOARD)
str << _("Keyboard\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::KEYBOARD)
str << _("Keyboard\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::MOUSE)
str << _("Mouse\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::MOUSE)
str << _("Mouse\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::MEDIA)
str << _("Media\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::MEDIA)
str << _("Media\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::DISK)
str << _("Disk\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::DISK)
str << _("Disk\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::PRINTER)
str << _("Printer\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::PRINTER)
str << _("Printer\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::TAPE)
str << _("Mag. Tape\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::TAPE)
str << _("Mag. Tape\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::PUNCH)
str << _("Punch Tape\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::PUNCH)
str << _("Punch Tape\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::DRUM)
str << _("Mag. Drum\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::DRUM)
str << _("Mag. Drum\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::ROM)
str << _("(EP)ROM\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::ROM)
str << _("(EP)ROM\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::COMMS)
str << _("Communications\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::COMMS)
str << _("Communications\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::LAN)
str << _("LAN\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::LAN)
str << _("LAN\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::WAN)
str << _("WAN\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::WAN)
str << _("WAN\tImperfect\n");
if (flags.unemulated_features() & device_t::feature::TIMING)
str << _("Timing\tUnimplemented\n");
else if (flags.imperfect_features() & device_t::feature::TIMING)
str << _("Timing\tImperfect\n");
str << ((flags.machine_flags() & machine_flags::MECHANICAL) ? _("Mechanical Machine\tYes\n") : _("Mechanical Machine\tNo\n"));
str << ((flags.machine_flags() & machine_flags::REQUIRES_ARTWORK) ? _("Requires Artwork\tYes\n") : _("Requires Artwork\tNo\n"));
str << ((flags.machine_flags() & machine_flags::CLICKABLE_ARTWORK) ? _("Requires Clickable Artwork\tYes\n") : _("Requires Clickable Artwork\tNo\n"));
str << ((flags.machine_flags() & machine_flags::NO_COCKTAIL) ? _("Support Cocktail\tYes\n") : _("Support Cocktail\tNo\n"));
str << ((flags.machine_flags() & machine_flags::IS_BIOS_ROOT) ? _("Driver is BIOS\tYes\n") : _("Driver is BIOS\tNo\n"));
str << ((flags.machine_flags() & machine_flags::SUPPORTS_SAVE) ? _("Support Save\tYes\n") : _("Support Save\tNo\n"));
str << ((flags.machine_flags() & ORIENTATION_SWAP_XY) ? _("Screen Orientation\tVertical\n") : _("Screen Orientation\tHorizontal\n"));
bool found = false;
for (romload::region const ®ion : romload::entries(driver->rom).get_regions())
{
if (region.is_diskdata())
{
found = true;
break;
}
}
str << (found ? _("Requires CHD\tYes\n") : _("Requires CHD\tNo\n"));
// audit the game first to see if we're going to work
if (ui().options().info_audit())
{
driver_enumerator enumerator(machine().options(), *driver);
enumerator.next();
media_auditor auditor(enumerator);
media_auditor::summary summary = auditor.audit_media(AUDIT_VALIDATE_FAST);
media_auditor::summary summary_samples = auditor.audit_samples();
// if everything looks good, schedule the new driver
if (summary == media_auditor::CORRECT || summary == media_auditor::BEST_AVAILABLE || summary == media_auditor::NONE_NEEDED)
str << _("ROM Audit Result\tOK\n");
else
str << _("ROM Audit Result\tBAD\n");
if (summary_samples == media_auditor::NONE_NEEDED)
str << _("Samples Audit Result\tNone Needed\n");
else if (summary_samples == media_auditor::CORRECT || summary_samples == media_auditor::BEST_AVAILABLE)
str << _("Samples Audit Result\tOK\n");
else
str << _("Samples Audit Result\tBAD\n");
}
else
{
str << _("ROM Audit \tDisabled\nSamples Audit \tDisabled\n");
}
buffer = str.str();
}
//-------------------------------------------------
// get (possibly cached) icon texture
//-------------------------------------------------
render_texture *menu_select_game::get_icon_texture(int linenum, void *selectedref)
{
game_driver const *const driver(m_populated_favorites
? reinterpret_cast<ui_software_info const *>(selectedref)->driver
: reinterpret_cast<game_driver const *>(selectedref));
assert(driver);
icon_cache::iterator icon(m_icons.find(driver));
if ((m_icons.end() == icon) || !icon->second.texture)
{
if (m_icon_paths.empty())
m_icon_paths = make_icon_paths(nullptr);
// allocate an entry or allocate a texture on forced redraw
if (m_icons.end() == icon)
{
icon = m_icons.emplace(driver, texture_ptr(machine().render().texture_alloc(), machine().render())).first;
}
else
{
assert(!icon->second.texture);
icon->second.texture.reset(machine().render().texture_alloc());
}
// set clone status
bool cloneof = strcmp(driver->parent, "0");
if (cloneof)
{
auto cx = driver_list::find(driver->parent);
if ((cx >= 0) && (driver_list::driver(cx).flags & machine_flags::IS_BIOS_ROOT))
cloneof = false;
}
bitmap_argb32 tmp;
emu_file snapfile(std::string(m_icon_paths), OPEN_FLAG_READ);
if (snapfile.open(std::string(driver->name) + ".ico") == osd_file::error::NONE)
{
render_load_ico_highest_detail(snapfile, tmp);
snapfile.close();
}
if (!tmp.valid() && cloneof && (snapfile.open(std::string(driver->parent) + ".ico") == osd_file::error::NONE))
{
render_load_ico_highest_detail(snapfile, tmp);
snapfile.close();
}
scale_icon(std::move(tmp), icon->second);
}
return icon->second.bitmap.valid() ? icon->second.texture.get() : nullptr;
}
void menu_select_game::inkey_export()
{
std::vector<game_driver const *> list;
if (m_populated_favorites)
{
// iterate over favorites
mame_machine_manager::instance()->favorite().apply(
[&list] (ui_software_info const &info)
{
assert(info.driver);
if (info.startempty)
list.push_back(info.driver);
});
}
else
{
list.reserve(m_displaylist.size());
for (ui_system_info const &info : m_displaylist)
list.emplace_back(info.driver);
}
menu::stack_push<menu_export>(ui(), container(), std::move(list));
}
//-------------------------------------------------
// load drivers infos from file
//-------------------------------------------------
bool menu_select_game::load_available_machines()
{
// try to load available drivers from file
emu_file file(ui().options().ui_path(), OPEN_FLAG_READ);
if (file.open(std::string(emulator_info::get_configname()) + "_avail.ini") != osd_file::error::NONE)
return false;
char rbuf[MAX_CHAR_INFO];
std::string readbuf;
file.gets(rbuf, MAX_CHAR_INFO);
file.gets(rbuf, MAX_CHAR_INFO);
readbuf = chartrimcarriage(rbuf);
std::string a_rev = string_format("%s%s", UI_VERSION_TAG, bare_build_version);
// version not matching ? exit
if (a_rev != readbuf)
{
file.close();
return false;
}
// load available list
std::unordered_set<std::string> available;
while (file.gets(rbuf, MAX_CHAR_INFO))
{
readbuf = rbuf;
strtrimspace(readbuf);
if (readbuf.empty() || ('#' == readbuf[0])) // ignore empty lines and line comments
;
else if ('[' == readbuf[0]) // throw out the rest of the file if we find a section heading
break;
else
available.emplace(std::move(readbuf));
}
file.close();
// turn it into the sorted system list we all love
for (ui_system_info &info : m_persistent_data.sorted_list())
{
std::unordered_set<std::string>::iterator const it(available.find(&info.driver->name[0]));
bool const found(available.end() != it);
info.available = found;
if (found)
available.erase(it);
}
return true;
}
//-------------------------------------------------
// load custom filters info from file
//-------------------------------------------------
void menu_select_game::load_custom_filters()
{
emu_file file(ui().options().ui_path(), OPEN_FLAG_READ);
if (file.open(util::string_format("custom_%s_filter.ini", emulator_info::get_configname())) == osd_file::error::NONE)
{
machine_filter::ptr flt(machine_filter::create(file, m_persistent_data.filter_data()));
if (flt)
m_persistent_data.filter_data().set_filter(std::move(flt)); // not emplace/insert - could replace bogus filter from ui.ini line
file.close();
}
}
//-------------------------------------------------
// draw left box
//-------------------------------------------------
float menu_select_game::draw_left_panel(float x1, float y1, float x2, float y2)
{
machine_filter_data &filter_data(m_persistent_data.filter_data());
return menu_select_launch::draw_left_panel<machine_filter>(filter_data.get_current_filter_type(), filter_data.get_filters(), x1, y1, x2, y2);
}
//-------------------------------------------------
// get selected software and/or driver
//-------------------------------------------------
void menu_select_game::get_selection(ui_software_info const *&software, game_driver const *&driver) const
{
if (m_populated_favorites)
{
software = reinterpret_cast<ui_software_info const *>(get_selection_ptr());
driver = software ? software->driver : nullptr;
}
else
{
software = nullptr;
driver = reinterpret_cast<game_driver const *>(get_selection_ptr());
}
}
void menu_select_game::make_topbox_text(std::string &line0, std::string &line1, std::string &line2) const
{
line0 = string_format(_("%1$s %2$s ( %3$d / %4$d machines (%5$d BIOS) )"),
emulator_info::get_appname(),
bare_build_version,
m_available_items,
(driver_list::total() - 1),
m_persistent_data.bios_count());
if (m_populated_favorites)
{
line1.clear();
}
else
{
machine_filter const *const it(m_persistent_data.filter_data().get_current_filter());
char const *const filter(it ? it->filter_text() : nullptr);
if (filter)
line1 = string_format(_("%1$s: %2$s - Search: %3$s_"), it->display_name(), filter, m_search);
else
line1 = string_format(_("Search: %1$s_"), m_search);
}
line2.clear();
}
std::string menu_select_game::make_driver_description(game_driver const &driver) const
{
// first line is game name
return string_format(_("Romset: %1$-.100s"), driver.name);
}
std::string menu_select_game::make_software_description(ui_software_info const &software) const
{
// first line is system
return string_format(_("System: %1$-.100s"), software.driver->type.fullname());
}
void menu_select_game::filter_selected()
{
if ((machine_filter::FIRST <= m_filter_highlight) && (machine_filter::LAST >= m_filter_highlight))
{
m_persistent_data.filter_data().get_filter(machine_filter::type(m_filter_highlight)).show_ui(
ui(),
container(),
[this] (machine_filter &filter)
{
set_switch_image();
machine_filter::type const new_type(filter.get_type());
if (machine_filter::CUSTOM == new_type)
{
emu_file file(ui().options().ui_path(), OPEN_FLAG_WRITE | OPEN_FLAG_CREATE | OPEN_FLAG_CREATE_PATHS);
if (file.open(util::string_format("custom_%s_filter.ini", emulator_info::get_configname())) == osd_file::error::NONE)
{
filter.save_ini(file, 0);
file.close();
}
}
m_persistent_data.filter_data().set_current_filter_type(new_type);
reset(reset_options::SELECT_FIRST);
});
}
}
std::string menu_select_game::make_error_text(bool summary, media_auditor const &auditor)
{
std::ostringstream str;
str << _("The selected machine is missing one or more required ROM or CHD images. Please select a different machine.\n\n");
if (summary)
{
auditor.summarize(nullptr, &str);
str << "\n";
}
str << _("Press any key to continue.");
return str.str();
}
} // namespace ui