/* * Portions of this file are copyright Rebirth contributors and licensed as * described in COPYING.txt. * Portions of this file are copyright Parallax Software and licensed * according to the Parallax license below. * See COPYING.txt for license details. THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX SOFTWARE CORPORATION ("PARALLAX"). PARALLAX, IN DISTRIBUTING THE CODE TO END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS IN USING, DISPLAYING, AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE FREE PURPOSES. IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES. THE END-USER UNDERSTANDS AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE. COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION. ALL RIGHTS RESERVED. */ /* * * Inferno High Scores and Statistics System * */ #include #include #include #include #include "scores.h" #include "dxxerror.h" #include "pstypes.h" #include "window.h" #include "gr.h" #include "key.h" #include "mouse.h" #include "palette.h" #include "game.h" #include "gamefont.h" #include "u_mem.h" #include "newmenu.h" #include "menu.h" #include "player.h" #include "object.h" #include "screens.h" #include "gamefont.h" #include "mouse.h" #include "joy.h" #include "timer.h" #include "text.h" #include "strutil.h" #include "rbaudio.h" #include "physfsx.h" #include "compiler-range_for.h" #include "d_levelstate.h" #include "d_range.h" #if DXX_USE_OGL #include "ogl_init.h" #endif #define VERSION_NUMBER 1 #define SCORES_FILENAME "descent.hi" #define COOL_MESSAGE_LEN 50 namespace dcx { constexpr std::integral_constant MAX_HIGH_SCORES{}; } namespace dsx { namespace { #if defined(DXX_BUILD_DESCENT_I) #define DXX_SCORE_STRUCT_PACK __pack__ #elif defined(DXX_BUILD_DESCENT_II) #define DXX_SCORE_STRUCT_PACK #endif struct stats_info { callsign_t name; int score; sbyte starting_level; sbyte ending_level; sbyte diff_level; short kill_ratio; // 0-100 short hostage_ratio; // int seconds; // How long it took in seconds... } DXX_SCORE_STRUCT_PACK; struct all_scores { char signature[3]; // DHS sbyte version; // version char cool_saying[COOL_MESSAGE_LEN]; stats_info stats[MAX_HIGH_SCORES]; } DXX_SCORE_STRUCT_PACK; #if defined(DXX_BUILD_DESCENT_I) static_assert(sizeof(all_scores) == 294, "high score size wrong"); #elif defined(DXX_BUILD_DESCENT_II) static_assert(sizeof(all_scores) == 336, "high score size wrong"); #endif void scores_view(const stats_info *last_game, int citem); static void scores_read(all_scores *scores) { int fsize; // clear score array... *scores = {}; RAIIPHYSFS_File fp{PHYSFS_openRead(SCORES_FILENAME)}; if (!fp) { // No error message needed, code will work without a scores file strcpy(scores->cool_saying, TXT_REGISTER_DESCENT); scores->stats[0].name = "Parallax"; scores->stats[1].name = "Matt"; scores->stats[2].name = "Mike"; scores->stats[3].name = "Adam"; scores->stats[4].name = "Mark"; scores->stats[5].name = "Jasen"; scores->stats[6].name = "Samir"; scores->stats[7].name = "Doug"; scores->stats[8].name = "Dan"; scores->stats[9].name = "Jason"; range_for (const int i, xrange(10u)) scores->stats[i].score = (10-i)*1000; return; } fsize = PHYSFS_fileLength(fp); if ( fsize != sizeof(all_scores) ) { return; } // Read 'em in... PHYSFS_read(fp, scores, sizeof(all_scores), 1); if ( (scores->version!=VERSION_NUMBER)||(scores->signature[0]!='D')||(scores->signature[1]!='H')||(scores->signature[2]!='S') ) { *scores = {}; return; } } static void scores_write(all_scores *scores) { RAIIPHYSFS_File fp{PHYSFS_openWrite(SCORES_FILENAME)}; if (!fp) { nm_messagebox(menu_title{TXT_WARNING}, 1, TXT_OK, "%s\n'%s'", TXT_UNABLE_TO_OPEN, SCORES_FILENAME); return; } scores->signature[0]='D'; scores->signature[1]='H'; scores->signature[2]='S'; scores->version = VERSION_NUMBER; PHYSFS_write(fp, scores,sizeof(all_scores), 1); } } } namespace dcx { namespace { static void int_to_string( int number, char *dest ) { int c; char buffer[20],*p; const auto l = snprintf(buffer, sizeof(buffer), "%d", number); if (l<=3) { // Don't bother with less than 3 digits memcpy(dest, buffer, 4); return; } c = 0; p=dest; for (int i=l-1; i>=0; i-- ) { if (c==3) { *p++=','; c = 0; } c++; *p++ = buffer[i]; } *p++ = '\0'; d_strrev(dest); } } } namespace dsx { namespace { static void scores_fill_struct(stats_info * stats) { auto &Objects = LevelUniqueObjectState.Objects; auto &vmobjptr = Objects.vmptr; auto &plr = get_local_player(); stats->name = plr.callsign; auto &player_info = get_local_plrobj().ctype.player_info; stats->score = player_info.mission.score; stats->ending_level = plr.level; if (const auto robots_total = GameUniqueState.accumulated_robots) stats->kill_ratio = (plr.num_kills_total * 100) / robots_total; else stats->kill_ratio = 0; if (const auto hostages_total = GameUniqueState.total_hostages) stats->hostage_ratio = (player_info.mission.hostages_rescued_total * 100) / hostages_total; else stats->hostage_ratio = 0; stats->seconds = f2i(plr.time_total) + (plr.hours_total * 3600); stats->diff_level = GameUniqueState.Difficulty_level; stats->starting_level = plr.starting_level; } } } namespace dcx { namespace { static inline const char *get_placement_slot_string(const unsigned position) { switch(position) { default: Int3(); DXX_BOOST_FALLTHROUGH; case 0: return TXT_1ST; case 1: return TXT_2ND; case 2: return TXT_3RD; case 3: return TXT_4TH; case 4: return TXT_5TH; case 5: return TXT_6TH; case 6: return TXT_7TH; case 7: return TXT_8TH; case 8: return TXT_9TH; case 9: return TXT_10TH; } } } } namespace dsx { void scores_maybe_add_player() { auto &Objects = LevelUniqueObjectState.Objects; auto &vmobjptr = Objects.vmptr; all_scores scores; stats_info last_game; if ((Game_mode & GM_MULTI) && !(Game_mode & GM_MULTI_COOP)) return; scores_read(&scores); auto &player_info = get_local_plrobj().ctype.player_info; const auto predicate = [player_mission_score = player_info.mission.score](const stats_info &stats) { return player_mission_score > stats.score; }; const auto begin_score_stats = std::begin(scores.stats); const auto end_score_stats = std::end(scores.stats); const auto iter_position = std::find_if(begin_score_stats, end_score_stats, predicate); const auto position = std::distance(begin_score_stats, iter_position); stats_info *const ptr_last_game = (iter_position == end_score_stats) ? &last_game : nullptr; if (ptr_last_game) { scores_fill_struct(ptr_last_game); } else { if (iter_position == begin_score_stats) { std::array text1{}; std::array m{{ nm_item_text(TXT_COOL_SAYING), nm_item_input(text1), }}; newmenu_do2(menu_title{TXT_HIGH_SCORE}, menu_subtitle{TXT_YOU_PLACED_1ST}, m, unused_newmenu_subfunction, unused_newmenu_userdata); strcpy(scores.cool_saying, text1[0] ? text1.data() : "No comment"); } else { nm_messagebox(menu_title{TXT_HIGH_SCORE}, 1, TXT_OK, "%s %s!", TXT_YOU_PLACED, get_placement_slot_string(position)); } // move everyone down... std::move_backward(iter_position, std::prev(end_score_stats), end_score_stats); scores_fill_struct(iter_position); scores_write(&scores); } scores_view(ptr_last_game, position); } } namespace dcx { namespace { __attribute_nonnull() static void scores_rputs(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, char *const buffer) { char *p; //replace the digit '1' with special wider 1 for (p=buffer;*p;p++) if (*p=='1') *p=132; int w, h; gr_get_string_size(cv_font, buffer, &w, &h, nullptr); gr_string(canvas, cv_font, FSPACX(x) - w, FSPACY(y), buffer, w, h); } __attribute_format_printf(5, 6) static void scores_rprintf(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const format, ...) { va_list args; char buffer[128]; va_start(args, format ); vsnprintf(buffer,sizeof(buffer),format,args); va_end(args); scores_rputs(canvas, cv_font, x, y, buffer); } } } namespace dsx { namespace { static void scores_draw_item(grs_canvas &canvas, const grs_font &cv_font, const unsigned i, const stats_info *const stats) { char buffer[20]; int y; y = 77+i*9; if (i==0) y -= 8; if ( i==MAX_HIGH_SCORES ) y += 8; else scores_rprintf(canvas, cv_font, 57, y - 3, "%d.", i + 1); y -= 3; const auto &&fspacx = FSPACX(); const auto &&fspacx66 = fspacx(66); const auto &&fspacy_y = FSPACY(y); if (!stats->name[0u]) { gr_string(canvas, cv_font, fspacx66, fspacy_y, TXT_EMPTY); return; } gr_string(canvas, cv_font, fspacx66, fspacy_y, stats->name); int_to_string(stats->score, buffer); scores_rputs(canvas, cv_font, 149, y, buffer); gr_string(canvas, cv_font, fspacx(166), fspacy_y, MENU_DIFFICULTY_TEXT(stats->diff_level)); if ( (stats->starting_level > 0 ) && (stats->ending_level > 0 )) scores_rprintf(canvas, cv_font, 232, y, "%d-%d", stats->starting_level, stats->ending_level); else if ( (stats->starting_level < 0 ) && (stats->ending_level > 0 )) scores_rprintf(canvas, cv_font, 232, y, "S%d-%d", -stats->starting_level, stats->ending_level); else if ( (stats->starting_level < 0 ) && (stats->ending_level < 0 )) scores_rprintf(canvas, cv_font, 232, y, "S%d-S%d", -stats->starting_level, -stats->ending_level); else if ( (stats->starting_level > 0 ) && (stats->ending_level < 0 )) scores_rprintf(canvas, cv_font, 232, y, "%d-S%d", stats->starting_level, -stats->ending_level); { int h, m, s; h = stats->seconds/3600; s = stats->seconds%3600; m = s / 60; s = s % 60; scores_rprintf(canvas, cv_font, 276, y, "%d:%02d:%02d", h, m, s); } } struct scores_menu : window { const int citem; fix64 t1; int looper = 0; all_scores scores; const stats_info last_game; scores_menu(grs_canvas &src, int x, int y, int w, int h, int citem, const stats_info *last_game) : window(src, x, y, w, h), citem(citem), t1(timer_query()), last_game(last_game ? *last_game : stats_info{}) { } virtual window_event_result event_handler(const d_event &) override; }; window_event_result scores_menu::event_handler(const d_event &event) { int k; const auto &&fspacx = FSPACX(); const auto &&fspacy = FSPACY(); int w = fspacx(290), h = fspacy(170); switch (event.type) { case EVENT_WINDOW_ACTIVATED: game_flush_inputs(Controls); break; case EVENT_KEY_COMMAND: k = event_key_get(event); switch( k ) { case KEY_CTRLED+KEY_R: if (citem < 0) { // Reset scores... if (nm_messagebox_str(menu_title{nullptr}, nm_messagebox_tie(TXT_NO, TXT_YES), menu_subtitle{TXT_RESET_HIGH_SCORES}) == 1) { PHYSFS_delete(SCORES_FILENAME); scores_view(&last_game, citem); // create new scores window return window_event_result::close; } } return window_event_result::handled; case KEY_ENTER: case KEY_SPACEBAR: case KEY_ESC: return window_event_result::close; } break; case EVENT_MOUSE_BUTTON_DOWN: case EVENT_MOUSE_BUTTON_UP: if (event_mouse_get_button(event) == MBTN_LEFT || event_mouse_get_button(event) == MBTN_RIGHT) { return window_event_result::close; } break; #if DXX_MAX_BUTTONS_PER_JOYSTICK case EVENT_JOYSTICK_BUTTON_DOWN: return window_event_result::close; #endif case EVENT_IDLE: timer_delay2(50); break; case EVENT_WINDOW_DRAW: gr_set_default_canvas(); nm_draw_background(*grd_curcanv, ((SWIDTH - w) / 2) - BORDERX, ((SHEIGHT - h) / 2) - BORDERY, ((SWIDTH - w) / 2) + w + BORDERX, ((SHEIGHT - h) / 2) + h + BORDERY); { auto &canvas = w_canv; auto &medium3_font = *MEDIUM3_FONT; gr_string(canvas, medium3_font, 0x8000, fspacy(15), TXT_HIGH_SCORES); gr_set_fontcolor(canvas, BM_XRGB(31, 26, 5), -1); auto &game_font = *GAME_FONT; gr_string(canvas, game_font, fspacx( 71), fspacy(50), TXT_NAME); gr_string(canvas, game_font, fspacx(122), fspacy(50), TXT_SCORE); gr_string(canvas, game_font, fspacx(167), fspacy(50), TXT_SKILL); gr_string(canvas, game_font, fspacx(210), fspacy(50), TXT_LEVELS); gr_string(canvas, game_font, fspacx(253), fspacy(50), TXT_TIME); if (citem < 0) gr_string(canvas, game_font, 0x8000, fspacy(175), TXT_PRESS_CTRL_R); gr_set_fontcolor(canvas, BM_XRGB(28, 28, 28), -1); gr_printf(canvas, game_font, 0x8000, fspacy(31), "%c%s%c - %s", 34, scores.cool_saying, 34, static_cast(scores.stats[0].name)); for (int i=0; i -1) { gr_set_fontcolor(canvas, BM_XRGB(7 + fades[looper], 7 + fades[looper], 7 + fades[looper]), -1); if (timer_query() >= t1 + F1_0 / 128) { t1 = timer_query(); looper++; if (looper > 63) looper = 0; } scores_draw_item(canvas, game_font, citem, citem == MAX_HIGH_SCORES ? &last_game : &scores.stats[citem]); } } break; case EVENT_WINDOW_CLOSE: break; default: break; } return window_event_result::ignored; } void scores_view(const stats_info *const last_game, int citem) { const auto &&fspacx320 = FSPACX(320); const auto &&fspacy200 = FSPACY(200); auto menu = window_create(grd_curscreen->sc_canvas, (SWIDTH - fspacx320) / 2, (SHEIGHT - fspacy200) / 2, fspacx320, fspacy200, citem, last_game); newmenu_free_background(); scores_read(&menu->scores); set_screen_mode(SCREEN_MENU); show_menus(); } } void scores_view_menu() { scores_view(nullptr, -1); } }