1 /*
2  * Portions of this file are copyright Rebirth contributors and licensed as
3  * described in COPYING.txt.
4  * Portions of this file are copyright Parallax Software and licensed
5  * according to the Parallax license below.
6  * See COPYING.txt for license details.
7 
8 THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX
9 SOFTWARE CORPORATION ("PARALLAX").  PARALLAX, IN DISTRIBUTING THE CODE TO
10 END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A
11 ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS
12 IN USING, DISPLAYING,  AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS
13 SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE
14 FREE PURPOSES.  IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE
15 CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES.  THE END-USER UNDERSTANDS
16 AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE.
17 COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION.  ALL RIGHTS RESERVED.
18 */
19 
20 /*
21  *
22  * Inferno High Scores and Statistics System
23  *
24  */
25 
26 #include <stdio.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <sstream>
30 #include <ctype.h>
31 
32 #include "scores.h"
33 #include "dxxerror.h"
34 #include "pstypes.h"
35 #include "window.h"
36 #include "gr.h"
37 #include "key.h"
38 #include "mouse.h"
39 #include "palette.h"
40 #include "game.h"
41 #include "gamefont.h"
42 #include "u_mem.h"
43 #include "newmenu.h"
44 #include "menu.h"
45 #include "player.h"
46 #include "object.h"
47 #include "screens.h"
48 #include "gamefont.h"
49 #include "mouse.h"
50 #include "joy.h"
51 #include "timer.h"
52 #include "text.h"
53 #include "strutil.h"
54 #include "physfsx.h"
55 #include "compiler-range_for.h"
56 #include "d_enumerate.h"
57 #include "d_levelstate.h"
58 #include "d_range.h"
59 #include "d_zip.h"
60 
61 #define VERSION_NUMBER 		1
62 #define SCORES_FILENAME 	"descent.hi"
63 #define COOL_MESSAGE_LEN 	50
64 namespace dcx {
65 constexpr std::integral_constant<unsigned, 10> MAX_HIGH_SCORES{};
66 
67 struct score_items_context
68 {
69 	const font_x_scaled_float name, score, difficulty, levels, time_played;
score_items_contextdcx::score_items_context70 	score_items_context(const font_x_scale_float fspacx, const unsigned border_x) :
71 		name(fspacx(51) + border_x), score(fspacx(134) + border_x), difficulty(fspacx(151) + border_x), levels(fspacx(217) + border_x), time_played(fspacx(261) + border_x)
72 	{
73 	}
74 };
75 
76 }
77 
78 namespace dsx {
79 
80 namespace {
81 
82 #if defined(DXX_BUILD_DESCENT_I)
83 #define DXX_SCORE_STRUCT_PACK	__pack__
84 #elif defined(DXX_BUILD_DESCENT_II)
85 #define DXX_SCORE_STRUCT_PACK
86 #endif
87 
88 struct stats_info
89 {
90 	callsign_t name;
91 	int		score;
92 	sbyte   starting_level;
93 	sbyte   ending_level;
94 	sbyte   diff_level;
95 	short 	kill_ratio;		// 0-100
96 	short	hostage_ratio;  //
97 	int		seconds;		// How long it took in seconds...
98 } DXX_SCORE_STRUCT_PACK;
99 
100 struct all_scores
101 {
102 	char			signature[3];			// DHS
103 	sbyte           version;				// version
104 	char			cool_saying[COOL_MESSAGE_LEN];
105 	stats_info	stats[MAX_HIGH_SCORES];
106 } DXX_SCORE_STRUCT_PACK;
107 #if defined(DXX_BUILD_DESCENT_I)
108 static_assert(sizeof(all_scores) == 294, "high score size wrong");
109 #elif defined(DXX_BUILD_DESCENT_II)
110 static_assert(sizeof(all_scores) == 336, "high score size wrong");
111 #endif
112 
113 void scores_view(grs_canvas &canvas, const stats_info *last_game, int citem);
114 
assign_builtin_placeholder_scores(all_scores & scores)115 static void assign_builtin_placeholder_scores(all_scores &scores)
116 {
117 	strcpy(scores.cool_saying, TXT_REGISTER_DESCENT);
118 	scores.stats[0].name = "Parallax";
119 	scores.stats[1].name = "Matt";
120 	scores.stats[2].name = "Mike";
121 	scores.stats[3].name = "Adam";
122 	scores.stats[4].name = "Mark";
123 	scores.stats[5].name = "Jasen";
124 	scores.stats[6].name = "Samir";
125 	scores.stats[7].name = "Doug";
126 	scores.stats[8].name = "Dan";
127 	scores.stats[9].name = "Jason";
128 
129 	for (auto &&[idx, stat] : enumerate(scores.stats))
130 		stat.score = (10 - idx) * 1000;
131 }
132 
scores_read(all_scores * scores)133 static void scores_read(all_scores *scores)
134 {
135 	int fsize;
136 
137 	// clear score array...
138 	*scores = {};
139 
140 	RAIIPHYSFS_File fp{PHYSFS_openRead(SCORES_FILENAME)};
141 	if (!fp)
142 	{
143 	 	// No error message needed, code will work without a scores file
144 		assign_builtin_placeholder_scores(*scores);
145 		return;
146 	}
147 
148 	fsize = PHYSFS_fileLength(fp);
149 
150 	if ( fsize != sizeof(all_scores) )	{
151 		return;
152 	}
153 	// Read 'em in...
154 	PHYSFS_read(fp, scores, sizeof(all_scores), 1);
155 	if ( (scores->version!=VERSION_NUMBER)||(scores->signature[0]!='D')||(scores->signature[1]!='H')||(scores->signature[2]!='S') )	{
156 		*scores = {};
157 		return;
158 	}
159 }
160 
scores_write(all_scores * scores)161 static void scores_write(all_scores *scores)
162 {
163 	RAIIPHYSFS_File fp{PHYSFS_openWrite(SCORES_FILENAME)};
164 	if (!fp)
165 	{
166 		nm_messagebox(menu_title{TXT_WARNING}, 1, TXT_OK, "%s\n'%s'", TXT_UNABLE_TO_OPEN, SCORES_FILENAME);
167 		return;
168 	}
169 
170 	scores->signature[0]='D';
171 	scores->signature[1]='H';
172 	scores->signature[2]='S';
173 	scores->version = VERSION_NUMBER;
174 	PHYSFS_write(fp, scores,sizeof(all_scores), 1);
175 }
176 
scores_fill_struct(stats_info * stats)177 static void scores_fill_struct(stats_info * stats)
178 {
179 	auto &Objects = LevelUniqueObjectState.Objects;
180 	auto &vmobjptr = Objects.vmptr;
181 	auto &plr = get_local_player();
182 	stats->name = plr.callsign;
183 	auto &player_info = get_local_plrobj().ctype.player_info;
184 	stats->score = player_info.mission.score;
185 	stats->ending_level = plr.level;
186 	if (const auto robots_total = GameUniqueState.accumulated_robots)
187 		stats->kill_ratio = (plr.num_kills_total * 100) / robots_total;
188 	else
189 		stats->kill_ratio = 0;
190 
191 	if (const auto hostages_total = GameUniqueState.total_hostages)
192 		stats->hostage_ratio = (player_info.mission.hostages_rescued_total * 100) / hostages_total;
193 	else
194 		stats->hostage_ratio = 0;
195 
196 	stats->seconds = f2i(plr.time_total) + (plr.hours_total * 3600);
197 
198 	stats->diff_level = GameUniqueState.Difficulty_level;
199 	stats->starting_level = plr.starting_level;
200 }
201 
202 }
203 
204 }
205 
206 namespace dcx {
207 
208 namespace {
209 
get_placement_slot_string(const unsigned position)210 static inline const char *get_placement_slot_string(const unsigned position)
211 {
212 	switch(position)
213 	{
214 		default:
215 			Int3();
216 			DXX_BOOST_FALLTHROUGH;
217 		case 0: return TXT_1ST;
218 		case 1: return TXT_2ND;
219 		case 2: return TXT_3RD;
220 		case 3: return TXT_4TH;
221 		case 4: return TXT_5TH;
222 		case 5: return TXT_6TH;
223 		case 6: return TXT_7TH;
224 		case 7: return TXT_8TH;
225 		case 8: return TXT_9TH;
226 		case 9: return TXT_10TH;
227 	}
228 }
229 
230 struct request_user_high_score_comment :
231 	std::array<char, sizeof(all_scores::cool_saying)>,
232 	std::array<newmenu_item, 2>,
233 	newmenu
234 {
235 	all_scores &scores;
request_user_high_score_commentdcx::__anon5f2de7980211::request_user_high_score_comment236 	request_user_high_score_comment(all_scores &scores, grs_canvas &canvas) :
237 		std::array<newmenu_item, 2>{{
238 			newmenu_item::nm_item_text{TXT_COOL_SAYING},
239 			newmenu_item::nm_item_input(prepare_input_saying(*this)),
240 		}},
241 		newmenu(menu_title{TXT_HIGH_SCORE}, menu_subtitle{TXT_YOU_PLACED_1ST}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(*static_cast<std::array<newmenu_item, 2> *>(this), 0), canvas),
242 		scores(scores)
243 	{
244 	}
245 	virtual window_event_result event_handler(const d_event &) override;
prepare_input_sayingdcx::__anon5f2de7980211::request_user_high_score_comment246 	static std::array<char, sizeof(all_scores::cool_saying)> &prepare_input_saying(std::array<char, sizeof(all_scores::cool_saying)> &buf)
247 	{
248 		buf.front() = 0;
249 		return buf;
250 	}
251 };
252 
event_handler(const d_event & event)253 window_event_result request_user_high_score_comment::event_handler(const d_event &event)
254 {
255 	switch (event.type)
256 	{
257 		case EVENT_WINDOW_CLOSE:
258 			{
259 				std::array<char, sizeof(all_scores::cool_saying)> &text1 = *this;
260 				strcpy(scores.cool_saying, text1[0] ? text1.data() : "No comment");
261 			}
262 			break;
263 		default:
264 			break;
265 	}
266 	return newmenu::event_handler(event);
267 }
268 
269 }
270 
271 }
272 
273 namespace dsx {
274 
scores_maybe_add_player()275 void scores_maybe_add_player()
276 {
277 	auto &Objects = LevelUniqueObjectState.Objects;
278 	auto &vmobjptr = Objects.vmptr;
279 	all_scores scores;
280 	stats_info last_game;
281 
282 	if ((Game_mode & GM_MULTI) && !(Game_mode & GM_MULTI_COOP))
283 		return;
284 	scores_read(&scores);
285 	auto &player_info = get_local_plrobj().ctype.player_info;
286 	const auto predicate = [player_mission_score = player_info.mission.score](const stats_info &stats) {
287 		return player_mission_score > stats.score;
288 	};
289 	const auto begin_score_stats = std::begin(scores.stats);
290 	const auto end_score_stats = std::end(scores.stats);
291 	/* Find the position at which the player's score should be placed.
292 	 */
293 	const auto iter_position = std::find_if(begin_score_stats, end_score_stats, predicate);
294 	const auto position = std::distance(begin_score_stats, iter_position);
295 	/* If iter_position == end_score_stats, then the player's score does
296 	 * not beat any of the existing high scores.  Include a special case
297 	 * so that the player's statistics can be shown for the duration of
298 	 * this menu, despite not being a new record.
299 	 */
300 	stats_info *const ptr_last_game = (iter_position == end_score_stats)
301 		? &last_game
302 		: nullptr;
303 	if (ptr_last_game)
304 	{
305 		/* Not a new record */
306 		scores_fill_struct(ptr_last_game);
307 	} else {
308 		/* New record - check whether it is the best score.  If so,
309 		 * allow the player to leave a comment.
310 		 */
311 		if (iter_position == begin_score_stats)
312 		{
313 			run_blocking_newmenu<request_user_high_score_comment>(scores, grd_curscreen->sc_canvas);
314 		} else {
315 			/* New record, but not a new best score.  Tell the player
316 			 * what slot the new record earned.
317 			 */
318 			nm_messagebox(menu_title{TXT_HIGH_SCORE}, 1, TXT_OK, "%s %s!", TXT_YOU_PLACED, get_placement_slot_string(position));
319 		}
320 
321 		// move everyone down...
322 		std::move_backward(iter_position, std::prev(end_score_stats), end_score_stats);
323 		scores_fill_struct(iter_position);
324 		scores_write(&scores);
325 	}
326 	scores_view(grd_curscreen->sc_canvas, ptr_last_game, position);
327 }
328 
329 }
330 
331 namespace dcx {
332 
333 namespace {
334 
__attribute_nonnull()335 __attribute_nonnull()
336 static void scores_rputs(grs_canvas &canvas, const grs_font &cv_font, const font_x_scaled_float x, const font_y_scaled_float y, const char *const buffer)
337 {
338 	const auto &&[w, h] = gr_get_string_size(cv_font, buffer);
339 	gr_string(canvas, cv_font, x - w, y, buffer, w, h);
340 }
341 
compute_score_y_coordinate(const unsigned i)342 static unsigned compute_score_y_coordinate(const unsigned i)
343 {
344 	const unsigned y = 59 + i * 9;
345 	return i ? y : y - 8;
346 }
347 
348 }
349 
350 }
351 
352 namespace dsx {
353 
354 namespace {
355 
356 struct scores_menu_items
357 {
358 	struct row
359 	{
360 		callsign_t name;
361 		uint8_t diff_level;
362 		std::array<char, 16> score;
363 		std::array<char, 10> levels;
364 		std::array<char, 14> time_played;
365 	};
366 	struct numbered_row : row
367 	{
368 		std::array<char, sizeof("10.")> line_number;
369 	};
370 	const int citem;
371 	fix64 time_last_color_change = timer_query();
372 	uint8_t looper = 0;
373 	std::array<numbered_row, MAX_HIGH_SCORES> scores;
374 	row last_game;
375 	std::array<char, COOL_MESSAGE_LEN> cool_saying;
376 	static void prepare_row(row &r, const stats_info &stats, std::ostringstream &oss);
377 	static void prepare_row(numbered_row &r, const stats_info &stats, std::ostringstream &oss, unsigned idx);
378 	static std::ostringstream build_locale_stringstream();
379 	void prepare_scores(const all_scores &all_scores, std::ostringstream &oss);
380 	scores_menu_items(const int citem, const all_scores &all_scores, const stats_info *last_game_stats);
381 };
382 
prepare_row(row & r,const stats_info & stats,std::ostringstream & oss)383 void scores_menu_items::prepare_row(row &r, const stats_info &stats, std::ostringstream &oss)
384 {
385 	r.name = stats.name;
386 	r.diff_level = stats.diff_level;
387 	{
388 		/* This std::ostringstream is shared among multiple rows, to
389 		 * avoid reinitializing the std::locale each time.  Clear the
390 		 * text before inserting the score for this row.
391 		 */
392 		oss.str("");
393 		oss << stats.score;
394 		auto &&buffer = oss.str();
395 		//replace the digit '1' with special wider 1
396 		const auto bb = buffer.begin();
397 		const auto be = buffer.end();
398 		auto ri = r.score.begin();
399 		if (std::distance(bb, be) < r.score.size() - 1)
400 			ri = std::replace_copy(bb, be, ri, '1', '\x84');
401 		*ri = 0;
402 	}
403 	{
404 		r.levels.front() = 0;
405 		auto starting_level = stats.starting_level;
406 		auto ending_level = stats.ending_level;
407 		if (starting_level || ending_level)
408 		{
409 			const auto secret_start = (starting_level < 0) ? (starting_level = -starting_level, "S") : "";
410 			const auto secret_end = (ending_level < 0) ? (ending_level = -ending_level, "S") : "";
411 			auto levels_length = std::snprintf(r.levels.data(), r.levels.size(), "%s%d-%s%d", secret_start, starting_level, secret_end, ending_level);
412 			auto lb = r.levels.begin();
413 			std::replace(lb, std::next(lb, levels_length), '1', '\x84');
414 		}
415 	}
416 	{
417 		const auto &&d1 = std::div(stats.seconds, 60);
418 		const auto &&d2 = std::div(d1.rem, 60);
419 		auto time_length = std::snprintf(r.time_played.data(), r.time_played.size(), "%d:%02d:%02d", d1.quot, d2.quot, d2.rem);
420 		auto tb = r.time_played.begin();
421 		std::replace(tb, std::next(tb, time_length), '1', '\x84');
422 	}
423 }
424 
prepare_row(numbered_row & r,const stats_info & stats,std::ostringstream & oss,const unsigned idx)425 void scores_menu_items::prepare_row(numbered_row &r, const stats_info &stats, std::ostringstream &oss, const unsigned idx)
426 {
427 	std::snprintf(r.line_number.data(), r.line_number.size(), "%u.", idx);
428 	auto b = r.line_number.begin();
429 	std::replace(b, std::next(b, 2), '1', '\x84');
430 	prepare_row(r, stats, oss);
431 }
432 
build_locale_stringstream()433 std::ostringstream scores_menu_items::build_locale_stringstream()
434 {
435 	const auto user_preferred_locale = []() {
436 		try {
437 			/* Use the user's locale if possible. */
438 			return std::locale("");
439 		} catch (std::runtime_error &) {
440 			/* Fall back to the default locale if the user's locale
441 			 * fails to parse.
442 			 */
443 			return std::locale();
444 		}
445 	}();
446 	std::ostringstream oss;
447 	oss.imbue(user_preferred_locale);
448 	return oss;
449 }
450 
prepare_scores(const all_scores & all_scores,std::ostringstream & oss)451 void scores_menu_items::prepare_scores(const all_scores &all_scores, std::ostringstream &oss)
452 {
453 	for (auto &&[idx, sr, si] : enumerate(zip(scores, all_scores.stats), 1u))
454 		prepare_row(sr, si, oss, idx);
455 	std::copy(std::begin(all_scores.cool_saying), std::prev(std::end(all_scores.cool_saying)), cool_saying.begin());
456 	cool_saying.back() = 0;
457 }
458 
scores_menu_items(const int citem,const all_scores & all_scores,const stats_info * const last_game_stats)459 scores_menu_items::scores_menu_items(const int citem, const all_scores &all_scores, const stats_info *const last_game_stats) :
460 	citem(citem)
461 {
462 	auto oss = build_locale_stringstream();
463 	prepare_scores(all_scores, oss);
464 	if (last_game_stats)
465 		prepare_row(last_game, *last_game_stats, oss);
466 	else
467 		last_game = {};
468 }
469 
470 struct scores_menu : scores_menu_items, window
471 {
scores_menudsx::__anon5f2de7980511::scores_menu472 	scores_menu(grs_canvas &src, int x, int y, int w, int h, int citem, const all_scores &scores, const stats_info *last_game) :
473 		scores_menu_items(citem, scores, last_game),
474 		window(src, x, y, w, h)
475 	{
476 	}
477 	virtual window_event_result event_handler(const d_event &) override;
478 	int get_update_looper();
479 };
480 
scores_draw_item(grs_canvas & canvas,const grs_font & cv_font,const score_items_context & shared_item_context,const unsigned shade,const font_y_scaled_float fspacy_y,const scores_menu_items::row & stats)481 static void scores_draw_item(grs_canvas &canvas, const grs_font &cv_font, const score_items_context &shared_item_context, const unsigned shade, const font_y_scaled_float fspacy_y, const scores_menu_items::row &stats)
482 {
483 	gr_set_fontcolor(canvas, BM_XRGB(shade, shade, shade), -1);
484 	if (!stats.name[0u])
485 	{
486 		gr_string(canvas, cv_font, shared_item_context.name, fspacy_y, TXT_EMPTY);
487 		return;
488 	}
489 	gr_string(canvas, cv_font, shared_item_context.name, fspacy_y, stats.name);
490 	scores_rputs(canvas, cv_font, shared_item_context.score, fspacy_y, stats.score.data());
491 
492 	gr_string(canvas, cv_font, shared_item_context.difficulty, fspacy_y, MENU_DIFFICULTY_TEXT(stats.diff_level));
493 
494 	scores_rputs(canvas, cv_font, shared_item_context.levels, fspacy_y, stats.levels.data());
495 	scores_rputs(canvas, cv_font, shared_item_context.time_played, fspacy_y, stats.time_played.data());
496 }
497 
event_handler(const d_event & event)498 window_event_result scores_menu::event_handler(const d_event &event)
499 {
500 	int k;
501 	const auto &&fspacx = FSPACX();
502 	const auto &&fspacy = FSPACY();
503 
504 	switch (event.type)
505 	{
506 		case EVENT_WINDOW_ACTIVATED:
507 			game_flush_inputs(Controls);
508 			break;
509 
510 		case EVENT_KEY_COMMAND:
511 			k = event_key_get(event);
512 			switch( k )	{
513 				case KEY_CTRLED+KEY_R:
514 					if (citem < 0)
515 					{
516 						// Reset scores...
517 						if (nm_messagebox_str(menu_title{nullptr}, nm_messagebox_tie(TXT_NO, TXT_YES), menu_subtitle{TXT_RESET_HIGH_SCORES}) == 1)
518 						{
519 							PHYSFS_delete(SCORES_FILENAME);
520 							all_scores scores{};
521 							assign_builtin_placeholder_scores(scores);
522 							auto oss = build_locale_stringstream();
523 							prepare_scores(scores, oss);
524 							return window_event_result::handled;
525 						}
526 					}
527 					return window_event_result::handled;
528 				case KEY_ENTER:
529 				case KEY_SPACEBAR:
530 				case KEY_ESC:
531 					return window_event_result::close;
532 			}
533 			break;
534 
535 		case EVENT_MOUSE_BUTTON_DOWN:
536 		case EVENT_MOUSE_BUTTON_UP:
537 			if (event_mouse_get_button(event) == MBTN_LEFT || event_mouse_get_button(event) == MBTN_RIGHT)
538 			{
539 				return window_event_result::close;
540 			}
541 			break;
542 
543 #if DXX_MAX_BUTTONS_PER_JOYSTICK
544 		case EVENT_JOYSTICK_BUTTON_DOWN:
545 			return window_event_result::close;
546 #endif
547 
548 		case EVENT_IDLE:
549 			timer_delay2(50);
550 			break;
551 
552 		case EVENT_WINDOW_DRAW:
553 
554 			{
555 				auto &canvas = w_canv;
556 			nm_draw_background(w_canv, 0, 0, w_canv.cv_bitmap.bm_w, w_canv.cv_bitmap.bm_h);
557 			auto &medium3_font = *MEDIUM3_FONT;
558 			const auto border_x = BORDERX;
559 			const auto border_y = BORDERY;
560 			gr_string(canvas, medium3_font, 0x8000, border_y, TXT_HIGH_SCORES);
561 			auto &game_font = *GAME_FONT;
562 			gr_set_fontcolor(canvas, BM_XRGB(28, 28, 28), -1);
563 			gr_printf(canvas, game_font, 0x8000, fspacy(16) + border_y, "\"%s\"  - %s", cool_saying.data(), static_cast<const char *>(scores[0].name));
564 			const font_x_scaled_float fspacx_line_number(fspacx(42) + border_x);
565 			const score_items_context shared_item_context(fspacx, border_x);
566 			gr_set_fontcolor(canvas, BM_XRGB(31, 26, 5), -1);
567 			const auto x_header = fspacx(56) + border_x;
568 			const auto fspacy_column_labels = fspacy(35) + border_y;
569 			gr_string(canvas, game_font, x_header, fspacy_column_labels, TXT_NAME);
570 			gr_string(canvas, game_font, x_header + fspacx(51), fspacy_column_labels, TXT_SCORE);
571 			gr_string(canvas, game_font, x_header + fspacx(96), fspacy_column_labels, TXT_SKILL);
572 			gr_string(canvas, game_font, x_header + fspacx(139), fspacy_column_labels, TXT_LEVELS);
573 			gr_string(canvas, game_font, x_header + fspacx(182), fspacy_column_labels, TXT_TIME);
574 
575 			if (citem < 0)
576 				gr_string(canvas, game_font, 0x8000, fspacy(125) + fspacy_column_labels, TXT_PRESS_CTRL_R);
577 
578 			for (const auto &&[idx, stat] : enumerate(scores))
579 			{
580 				const auto shade = (idx == citem)
581 					? get_update_looper()
582 					: 28 - idx * 2;
583 				const unsigned y = compute_score_y_coordinate(idx);
584 				const font_y_scaled_float fspacy_y(fspacy(y) + border_y);
585 				scores_draw_item(canvas, game_font, shared_item_context, shade, fspacy_y, stat);
586 				scores_rputs(canvas, game_font, fspacx_line_number, fspacy_y, stat.line_number.data());
587 			}
588 
589 			if (citem == MAX_HIGH_SCORES)
590 			{
591 				const auto shade = get_update_looper();
592 				scores_draw_item(canvas, game_font, shared_item_context, shade, fspacy(compute_score_y_coordinate(citem) + 8), last_game);
593 			}
594 			}
595 			break;
596 		case EVENT_WINDOW_CLOSE:
597 			break;
598 		default:
599 			break;
600 	}
601 	return window_event_result::ignored;
602 }
603 
get_update_looper()604 int scores_menu::get_update_looper()
605 {
606 	if (const auto t2 = timer_query(); t2 >= time_last_color_change + F1_0 / 128)
607 	{
608 		time_last_color_change = t2;
609 		if (++ looper >= fades.size())
610 			looper = 0;
611 	}
612 	const auto shade = 7 + fades[looper];
613 	return shade;
614 }
615 
scores_view(grs_canvas & canvas,const stats_info * const last_game,int citem)616 void scores_view(grs_canvas &canvas, const stats_info *const last_game, int citem)
617 {
618 	const auto &&fspacx290 = FSPACX(290);
619 	const auto &&fspacy170 = FSPACY(170);
620 	all_scores scores;
621 	scores_read(&scores);
622 	const auto border_x = get_border_x(canvas);
623 	const auto border_y = get_border_y(canvas);
624 	auto menu = window_create<scores_menu>(canvas, ((canvas.cv_bitmap.bm_w - fspacx290) / 2) - border_x, ((canvas.cv_bitmap.bm_h - fspacy170) / 2) - border_y, fspacx290 + (border_x * 2), fspacy170 + (border_y * 2), citem, scores, last_game);
625 	(void)menu;
626 
627 	newmenu_free_background();
628 
629 	set_screen_mode(SCREEN_MENU);
630 	show_menus();
631 }
632 
633 }
634 
scores_view_menu(grs_canvas & canvas)635 void scores_view_menu(grs_canvas &canvas)
636 {
637 	scores_view(canvas, nullptr, -1);
638 }
639 
640 }
641