1 // license:BSD-3-Clause
2 // copyright-holders:Nicola Salmoria, Aaron Giles, Nathan Woods
3 /***************************************************************************
4 
5     ui/simpleselgame.cpp
6 
7     Game selector
8 
9 ***************************************************************************/
10 
11 #include "emu.h"
12 
13 #include "ui/simpleselgame.h"
14 
15 #include "ui/info.h"
16 #include "ui/miscmenu.h"
17 #include "ui/optsmenu.h"
18 #include "ui/ui.h"
19 #include "ui/utils.h"
20 
21 #include "audit.h"
22 #include "drivenum.h"
23 #include "emuopts.h"
24 #include "mame.h"
25 #include "uiinput.h"
26 
27 #include <cctype>
28 
29 
30 namespace ui {
31 
32 //-------------------------------------------------
33 //  ctor
34 //-------------------------------------------------
35 
simple_menu_select_game(mame_ui_manager & mui,render_container & container,const char * gamename)36 simple_menu_select_game::simple_menu_select_game(mame_ui_manager &mui, render_container &container, const char *gamename)
37 	: menu(mui, container)
38 	, m_error(false), m_rerandomize(false)
39 	, m_search()
40 	, m_skip_main_items(0)
41 	, m_driverlist(driver_list::total() + 1)
42 	, m_drivlist()
43 	, m_cached_driver(nullptr)
44 	, m_cached_flags(machine_flags::NOT_WORKING)
45 	, m_cached_unemulated(device_t::feature::NONE), m_cached_imperfect(device_t::feature::NONE)
46 	, m_cached_color(ui().colors().background_color())
47 {
48 	build_driver_list();
49 	if (gamename)
50 		m_search.assign(gamename);
51 	m_matchlist[0] = -1;
52 }
53 
54 
55 //-------------------------------------------------
56 //  dtor
57 //-------------------------------------------------
58 
~simple_menu_select_game()59 simple_menu_select_game::~simple_menu_select_game()
60 {
61 }
62 
63 
64 
65 //-------------------------------------------------
66 //  build_driver_list - build a list of available
67 //  drivers
68 //-------------------------------------------------
69 
build_driver_list()70 void simple_menu_select_game::build_driver_list()
71 {
72 	// start with an empty list
73 	m_drivlist = std::make_unique<driver_enumerator>(machine().options());
74 	m_drivlist->exclude_all();
75 
76 	// open a path to the ROMs and find them in the array
77 	file_enumerator path(machine().options().media_path());
78 
79 	// iterate while we get new objects
80 	for (const osd::directory::entry *dir = path.next(); dir; dir = path.next())
81 	{
82 		char drivername[50];
83 		char *dst = drivername;
84 		const char *src;
85 
86 		// build a name for it
87 		for (src = dir->name; *src != 0 && *src != '.' && dst < &drivername[ARRAY_LENGTH(drivername) - 1]; src++)
88 			*dst++ = tolower((uint8_t)*src);
89 		*dst = 0;
90 
91 		int drivnum = m_drivlist->find(drivername);
92 		if (drivnum != -1)
93 			m_drivlist->include(drivnum);
94 	}
95 
96 	// now build the final list
97 	m_drivlist->reset();
98 	int listnum = 0;
99 	while (m_drivlist->next())
100 		m_driverlist[listnum++] = &m_drivlist->driver();
101 
102 	// NULL-terminate
103 	m_driverlist[listnum] = nullptr;
104 }
105 
106 
107 
108 //-------------------------------------------------
109 //  handle - handle the game select menu
110 //-------------------------------------------------
111 
handle()112 void simple_menu_select_game::handle()
113 {
114 	// ignore pause keys by swallowing them before we process the menu
115 	machine().ui_input().pressed(IPT_UI_PAUSE);
116 
117 	// process the menu
118 	const event *menu_event = process(0);
119 	if (menu_event && menu_event->itemref)
120 	{
121 		if (m_error)
122 		{
123 			// reset the error on any future menu_event
124 			m_error = false;
125 			machine().ui_input().reset();
126 		}
127 		else
128 		{
129 			// handle selections
130 			switch(menu_event->iptkey)
131 			{
132 			case IPT_UI_SELECT:
133 				inkey_select(menu_event);
134 				break;
135 			case IPT_UI_CANCEL:
136 				inkey_cancel();
137 				break;
138 			case IPT_SPECIAL:
139 				inkey_special(menu_event);
140 				break;
141 			}
142 		}
143 	}
144 
145 	// if we're in an error state, overlay an error message
146 	if (m_error)
147 	{
148 		ui().draw_text_box(
149 				container(),
150 				_("The selected game is missing one or more required ROM or CHD images. "
151 				"Please select a different game.\n\nPress any key to continue."),
152 				ui::text_layout::CENTER, 0.5f, 0.5f, UI_RED_COLOR);
153 	}
154 }
155 
156 
157 //-------------------------------------------------
158 //  inkey_select
159 //-------------------------------------------------
160 
inkey_select(const event * menu_event)161 void simple_menu_select_game::inkey_select(const event *menu_event)
162 {
163 	const game_driver *driver = (const game_driver *)menu_event->itemref;
164 
165 	// special case for configure inputs
166 	if ((uintptr_t)driver == 1)
167 	{
168 		menu::stack_push<menu_simple_game_options>(
169 				ui(),
170 				container(),
171 				[this] () { reset(reset_options::SELECT_FIRST); });
172 	}
173 
174 	// anything else is a driver
175 	else
176 	{
177 		// audit the game first to see if we're going to work
178 		driver_enumerator enumerator(machine().options(), *driver);
179 		enumerator.next();
180 		media_auditor auditor(enumerator);
181 		media_auditor::summary summary = auditor.audit_media(AUDIT_VALIDATE_FAST);
182 
183 		if (summary == media_auditor::CORRECT || summary == media_auditor::BEST_AVAILABLE || summary == media_auditor::NONE_NEEDED)
184 		{
185 			// if everything looks good, schedule the new driver
186 			mame_machine_manager::instance()->schedule_new_driver(*driver);
187 			machine().schedule_hard_reset();
188 			stack_reset();
189 		}
190 		else
191 		{
192 			// otherwise, display an error
193 			reset(reset_options::REMEMBER_REF);
194 			m_error = true;
195 		}
196 	}
197 }
198 
199 
200 //-------------------------------------------------
201 //  inkey_cancel
202 //-------------------------------------------------
203 
inkey_cancel()204 void simple_menu_select_game::inkey_cancel()
205 {
206 	// escape pressed with non-empty text clears the text
207 	if (!m_search.empty())
208 	{
209 		m_search.clear();
210 		reset(reset_options::SELECT_FIRST);
211 	}
212 }
213 
214 
215 //-------------------------------------------------
216 //  inkey_special - typed characters append to the buffer
217 //-------------------------------------------------
218 
inkey_special(const event * menu_event)219 void simple_menu_select_game::inkey_special(const event *menu_event)
220 {
221 	// typed characters append to the buffer
222 	size_t old_size = m_search.size();
223 	if (input_character(m_search, menu_event->unichar, uchar_is_printable))
224 	{
225 		if (m_search.size() < old_size)
226 			m_rerandomize = true;
227 		reset(reset_options::SELECT_FIRST);
228 	}
229 }
230 
231 
232 //-------------------------------------------------
233 //  populate - populate the game select menu
234 //-------------------------------------------------
235 
populate(float & customtop,float & custombottom)236 void simple_menu_select_game::populate(float &customtop, float &custombottom)
237 {
238 	int matchcount;
239 	int curitem;
240 
241 	for (curitem = matchcount = 0; m_driverlist[curitem] != nullptr && matchcount < VISIBLE_GAMES_IN_LIST; curitem++)
242 		matchcount++;
243 
244 	// if nothing there, add a single multiline item and return
245 	if (matchcount == 0)
246 	{
247 		std::string txt = string_format(
248 				_("No machines found. Please check the rompath specified in the %1$s.ini file.\n\n"
249 				"If this is your first time using %2$s, please see the config.txt file in "
250 				"the docs directory for information on configuring %2$s."),
251 				emulator_info::get_configname(),
252 				emulator_info::get_appname());
253 		item_append(txt, "", FLAG_MULTILINE | FLAG_REDTEXT, nullptr);
254 		return;
255 	}
256 
257 	// otherwise, rebuild the match list
258 	assert(m_drivlist != nullptr);
259 	if (!m_search.empty() || m_matchlist[0] == -1 || m_rerandomize)
260 		m_drivlist->find_approximate_matches(m_search, matchcount, m_matchlist);
261 	m_rerandomize = false;
262 
263 	// iterate over entries
264 	for (curitem = 0; curitem < matchcount; curitem++)
265 	{
266 		int curmatch = m_matchlist[curitem];
267 		if (curmatch != -1)
268 		{
269 			int cloneof = m_drivlist->non_bios_clone(curmatch);
270 			item_append(m_drivlist->driver(curmatch).name, m_drivlist->driver(curmatch).type.fullname(), (cloneof == -1) ? 0 : FLAG_INVERT, (void *)&m_drivlist->driver(curmatch));
271 		}
272 	}
273 
274 	// if we're forced into this, allow general input configuration as well
275 	if (stack_has_special_main_menu())
276 	{
277 		item_append(menu_item_type::SEPARATOR);
278 		item_append(_("Configure Options"), "", 0, (void *)1);
279 		m_skip_main_items = 1;
280 	}
281 
282 	// configure the custom rendering
283 	customtop = ui().get_line_height() + 3.0f * ui().box_tb_border();
284 	custombottom = 4.0f * ui().get_line_height() + 3.0f * ui().box_tb_border();
285 }
286 
287 
288 //-------------------------------------------------
289 //  custom_render - perform our special rendering
290 //-------------------------------------------------
291 
custom_render(void * selectedref,float top,float bottom,float origx1,float origy1,float origx2,float origy2)292 void simple_menu_select_game::custom_render(void *selectedref, float top, float bottom, float origx1, float origy1, float origx2, float origy2)
293 {
294 	const game_driver *driver;
295 	std::string tempbuf[5];
296 
297 	// display the current typeahead
298 	if (!m_search.empty())
299 		tempbuf[0] = string_format(_("Type name or select: %1$s_"), m_search);
300 	else
301 		tempbuf[0] = _("Type name or select: (random)");
302 
303 	// draw the top box
304 	draw_text_box(
305 			tempbuf, tempbuf + 1,
306 			origx1, origx2, origy1 - top, origy1 - ui().box_tb_border(),
307 			ui::text_layout::CENTER, ui::text_layout::TRUNCATE, false,
308 			ui().colors().text_color(), ui().colors().background_color(), 1.0f);
309 
310 	// determine the text to render below
311 	driver = ((uintptr_t)selectedref > m_skip_main_items) ? (const game_driver *)selectedref : nullptr;
312 	if (driver)
313 	{
314 		// first line is game name
315 		tempbuf[0] = string_format(_("%1$-.100s"), driver->type.fullname());
316 
317 		// next line is year, manufacturer
318 		tempbuf[1] = string_format(_("%1$s, %2$-.100s"), driver->year, driver->manufacturer);
319 
320 		// next line source path
321 		tempbuf[2] = string_format(_("Driver: %1$-.100s"), core_filename_extract_base(driver->type.source()));
322 
323 		// update cached values if selection changed
324 		if (driver != m_cached_driver)
325 		{
326 			emu_options clean_options;
327 			machine_static_info const info(ui().options(), machine_config(*driver, clean_options));
328 			m_cached_driver = driver;
329 			m_cached_flags = info.machine_flags();
330 			m_cached_unemulated = info.unemulated_features();
331 			m_cached_imperfect = info.imperfect_features();
332 			m_cached_color = info.status_color();
333 		}
334 
335 		// next line is overall driver status
336 		if (m_cached_flags & machine_flags::NOT_WORKING)
337 			tempbuf[3] = _("Overall: NOT WORKING");
338 		else if ((m_cached_unemulated | m_cached_imperfect) & device_t::feature::PROTECTION)
339 			tempbuf[3] = _("Overall: Unemulated Protection");
340 		else
341 			tempbuf[3] = _("Overall: Working");
342 
343 		// next line is graphics, sound status
344 		if (m_cached_unemulated & device_t::feature::GRAPHICS)
345 			tempbuf[4] = _("Graphics: Unimplemented, ");
346 		else if ((m_cached_unemulated | m_cached_imperfect) & (device_t::feature::GRAPHICS | device_t::feature::PALETTE))
347 			tempbuf[4] = _("Graphics: Imperfect, ");
348 		else
349 			tempbuf[4] = _("Graphics: OK, ");
350 
351 		if (m_cached_flags & machine_flags::NO_SOUND_HW)
352 			tempbuf[4].append(_("Sound: None"));
353 		else if (m_cached_unemulated & device_t::feature::SOUND)
354 			tempbuf[4].append(_("Sound: Unimplemented"));
355 		else if (m_cached_imperfect & device_t::feature::SOUND)
356 			tempbuf[4].append(_("Sound: Imperfect"));
357 		else
358 			tempbuf[4].append(_("Sound: OK"));
359 	}
360 	else
361 	{
362 		const char *s = emulator_info::get_copyright();
363 		unsigned line = 0;
364 
365 		// first line is version string
366 		tempbuf[line++] = string_format("%s %s", emulator_info::get_appname(), build_version);
367 
368 		// output message
369 		while (line < ARRAY_LENGTH(tempbuf))
370 		{
371 			if (!(*s == 0 || *s == '\n'))
372 				tempbuf[line].push_back(*s);
373 
374 			if (*s == '\n')
375 			{
376 				line++;
377 				s++;
378 			} else if (*s != 0)
379 				s++;
380 			else
381 				line++;
382 		}
383 	}
384 
385 	// draw the bottom box
386 	draw_text_box(
387 			tempbuf, tempbuf + 4,
388 			origx1, origx2, origy2 + ui().box_tb_border(), origy2 + bottom,
389 			ui::text_layout::CENTER, ui::text_layout::TRUNCATE, true,
390 			ui().colors().text_color(), driver ? m_cached_color : ui().colors().background_color(), 1.0f);
391 }
392 
393 
394 //-------------------------------------------------
395 //  force_game_select - force the game
396 //  select menu to be visible and inescapable
397 //-------------------------------------------------
398 
force_game_select(mame_ui_manager & mui,render_container & container)399 void simple_menu_select_game::force_game_select(mame_ui_manager &mui, render_container &container)
400 {
401 	char *gamename = (char *)mui.machine().options().system_name();
402 
403 	// reset the menu stack
404 	menu::stack_reset(mui.machine());
405 
406 	// add the quit entry followed by the game select entry
407 	menu::stack_push_special_main<menu_quit_game>(mui, container);
408 	menu::stack_push<simple_menu_select_game>(mui, container, gamename);
409 
410 	// force the menus on
411 	mui.show_menu();
412 
413 	// make sure MAME is paused
414 	mui.machine().pause();
415 }
416 
417 } // namespace ui
418