Subversion Repositories Games.Descent

Rev

Blame | Last modification | View Log | Download | RSS feed

  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 <ctype.h>
  30.  
  31. #include "scores.h"
  32. #include "dxxerror.h"
  33. #include "pstypes.h"
  34. #include "window.h"
  35. #include "gr.h"
  36. #include "key.h"
  37. #include "mouse.h"
  38. #include "palette.h"
  39. #include "game.h"
  40. #include "gamefont.h"
  41. #include "u_mem.h"
  42. #include "newmenu.h"
  43. #include "menu.h"
  44. #include "player.h"
  45. #include "object.h"
  46. #include "screens.h"
  47. #include "gamefont.h"
  48. #include "mouse.h"
  49. #include "joy.h"
  50. #include "timer.h"
  51. #include "text.h"
  52. #include "strutil.h"
  53. #include "rbaudio.h"
  54. #include "physfsx.h"
  55. #include "compiler-range_for.h"
  56. #include "d_range.h"
  57.  
  58. #if DXX_USE_OGL
  59. #include "ogl_init.h"
  60. #endif
  61.  
  62. #define VERSION_NUMBER          1
  63. #define SCORES_FILENAME         "descent.hi"
  64. #define COOL_MESSAGE_LEN        50
  65. namespace dcx {
  66. constexpr std::integral_constant<unsigned, 10> MAX_HIGH_SCORES{};
  67. }
  68.  
  69. #if defined(DXX_BUILD_DESCENT_I)
  70. #define DXX_SCORE_STRUCT_PACK   __pack__
  71. #elif defined(DXX_BUILD_DESCENT_II)
  72. #define DXX_SCORE_STRUCT_PACK
  73. #endif
  74.  
  75. struct stats_info
  76. {
  77.         callsign_t name;
  78.         int             score;
  79.         sbyte   starting_level;
  80.         sbyte   ending_level;
  81.         sbyte   diff_level;
  82.         short   kill_ratio;             // 0-100
  83.         short   hostage_ratio;  //
  84.         int             seconds;                // How long it took in seconds...
  85. } DXX_SCORE_STRUCT_PACK;
  86.  
  87. struct all_scores
  88. {
  89.         char                    signature[3];                   // DHS
  90.         sbyte           version;                                // version
  91.         char                    cool_saying[COOL_MESSAGE_LEN];
  92.         stats_info      stats[MAX_HIGH_SCORES];
  93. } DXX_SCORE_STRUCT_PACK;
  94. #if defined(DXX_BUILD_DESCENT_I)
  95. static_assert(sizeof(all_scores) == 294, "high score size wrong");
  96. #elif defined(DXX_BUILD_DESCENT_II)
  97. static_assert(sizeof(all_scores) == 336, "high score size wrong");
  98. #endif
  99.  
  100. static void scores_read(all_scores *scores)
  101. {
  102.         int fsize;
  103.  
  104.         // clear score array...
  105.         *scores = {};
  106.  
  107.         RAIIPHYSFS_File fp{PHYSFS_openRead(SCORES_FILENAME)};
  108.         if (!fp)
  109.         {
  110.                 // No error message needed, code will work without a scores file
  111.                 strcpy(scores->cool_saying, TXT_REGISTER_DESCENT);
  112.                 scores->stats[0].name = "Parallax";
  113.                 scores->stats[1].name = "Matt";
  114.                 scores->stats[2].name = "Mike";
  115.                 scores->stats[3].name = "Adam";
  116.                 scores->stats[4].name = "Mark";
  117.                 scores->stats[5].name = "Jasen";
  118.                 scores->stats[6].name = "Samir";
  119.                 scores->stats[7].name = "Doug";
  120.                 scores->stats[8].name = "Dan";
  121.                 scores->stats[9].name = "Jason";
  122.  
  123.                 range_for (const int i, xrange(10u))
  124.                         scores->stats[i].score = (10-i)*1000;
  125.                 return;
  126.         }
  127.  
  128.         fsize = PHYSFS_fileLength(fp);
  129.  
  130.         if ( fsize != sizeof(all_scores) )      {
  131.                 return;
  132.         }
  133.         // Read 'em in...
  134.         PHYSFS_read(fp, scores, sizeof(all_scores), 1);
  135.         if ( (scores->version!=VERSION_NUMBER)||(scores->signature[0]!='D')||(scores->signature[1]!='H')||(scores->signature[2]!='S') ) {
  136.                 *scores = {};
  137.                 return;
  138.         }
  139. }
  140.  
  141. static void scores_write(all_scores *scores)
  142. {
  143.         RAIIPHYSFS_File fp{PHYSFS_openWrite(SCORES_FILENAME)};
  144.         if (!fp)
  145.         {
  146.                 nm_messagebox( TXT_WARNING, 1, TXT_OK, "%s\n'%s'", TXT_UNABLE_TO_OPEN, SCORES_FILENAME  );
  147.                 return;
  148.         }
  149.  
  150.         scores->signature[0]='D';
  151.         scores->signature[1]='H';
  152.         scores->signature[2]='S';
  153.         scores->version = VERSION_NUMBER;
  154.         PHYSFS_write(fp, scores,sizeof(all_scores), 1);
  155. }
  156.  
  157. static void int_to_string( int number, char *dest )
  158. {
  159.         int c;
  160.         char buffer[20],*p;
  161.  
  162.         const auto l = snprintf(buffer, sizeof(buffer), "%d", number);
  163.         if (l<=3) {
  164.                 // Don't bother with less than 3 digits
  165.                 memcpy(dest, buffer, 4);
  166.                 return;
  167.         }
  168.  
  169.         c = 0;
  170.         p=dest;
  171.         for (int i=l-1; i>=0; i-- ) {
  172.                 if (c==3) {
  173.                         *p++=',';
  174.                         c = 0;
  175.                 }
  176.                 c++;
  177.                 *p++ = buffer[i];
  178.         }
  179.         *p++ = '\0';
  180.         d_strrev(dest);
  181. }
  182.  
  183. static void scores_fill_struct(stats_info * stats)
  184. {
  185.         auto &Objects = LevelUniqueObjectState.Objects;
  186.         auto &vmobjptr = Objects.vmptr;
  187.         auto &plr = get_local_player();
  188.         stats->name = plr.callsign;
  189.         auto &player_info = get_local_plrobj().ctype.player_info;
  190.         stats->score = player_info.mission.score;
  191.         stats->ending_level = plr.level;
  192.         if (const auto robots_total = GameUniqueState.accumulated_robots)
  193.                 stats->kill_ratio = (plr.num_kills_total * 100) / robots_total;
  194.         else
  195.                 stats->kill_ratio = 0;
  196.  
  197.         if (const auto hostages_total = GameUniqueState.total_hostages)
  198.                 stats->hostage_ratio = (player_info.mission.hostages_rescued_total * 100) / hostages_total;
  199.         else
  200.                 stats->hostage_ratio = 0;
  201.  
  202.         stats->seconds = f2i(plr.time_total) + (plr.hours_total * 3600);
  203.  
  204.         stats->diff_level = GameUniqueState.Difficulty_level;
  205.         stats->starting_level = plr.starting_level;
  206. }
  207.  
  208. static inline const char *get_placement_slot_string(const unsigned position)
  209. {
  210.         switch(position)
  211.         {
  212.                 default:
  213.                         Int3();
  214.                         DXX_BOOST_FALLTHROUGH;
  215.                 case 0: return TXT_1ST;
  216.                 case 1: return TXT_2ND;
  217.                 case 2: return TXT_3RD;
  218.                 case 3: return TXT_4TH;
  219.                 case 4: return TXT_5TH;
  220.                 case 5: return TXT_6TH;
  221.                 case 6: return TXT_7TH;
  222.                 case 7: return TXT_8TH;
  223.                 case 8: return TXT_9TH;
  224.                 case 9: return TXT_10TH;
  225.         }
  226. }
  227.  
  228. void scores_maybe_add_player()
  229. {
  230.         auto &Objects = LevelUniqueObjectState.Objects;
  231.         auto &vmobjptr = Objects.vmptr;
  232.         int position;
  233.         all_scores scores;
  234.         stats_info last_game;
  235.  
  236.         if ((Game_mode & GM_MULTI) && !(Game_mode & GM_MULTI_COOP))
  237.                 return;
  238.  
  239.         scores_read(&scores);
  240.        
  241.         position = MAX_HIGH_SCORES;
  242.         auto &player_info = get_local_plrobj().ctype.player_info;
  243.         for (int i=0; i<MAX_HIGH_SCORES; i++ ) {
  244.                 if (player_info.mission.score > scores.stats[i].score)
  245.                 {
  246.                         position = i;
  247.                         break;
  248.                 }
  249.         }
  250.        
  251.         if ( position == MAX_HIGH_SCORES ) {
  252.                 scores_fill_struct( &last_game );
  253.         } else {
  254.                 if ( position==0 )      {
  255.                         std::array<char, sizeof(scores.cool_saying)> text1{};
  256.                         std::array<newmenu_item, 2> m{{
  257.                                 nm_item_text(TXT_COOL_SAYING),
  258.                                 nm_item_input(text1),
  259.                         }};
  260.                         newmenu_do( TXT_HIGH_SCORE, TXT_YOU_PLACED_1ST, m, unused_newmenu_subfunction, unused_newmenu_userdata );
  261.                         strcpy(scores.cool_saying, text1[0] ? text1.data() : "No comment");
  262.                 } else {
  263.                         nm_messagebox( TXT_HIGH_SCORE, 1, TXT_OK, "%s %s!", TXT_YOU_PLACED, get_placement_slot_string(position));
  264.                 }
  265.        
  266.                 // move everyone down...
  267.                 for ( int i=MAX_HIGH_SCORES-1; i>position; i-- ) {
  268.                         scores.stats[i] = scores.stats[i-1];
  269.                 }
  270.  
  271.                 scores_fill_struct( &scores.stats[position] );
  272.        
  273.                 scores_write(&scores);
  274.  
  275.         }
  276.         scores_view(&last_game, position);
  277. }
  278.  
  279. __attribute_nonnull()
  280. static void scores_rputs(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, char *const buffer)
  281. {
  282.         char *p;
  283.  
  284.         //replace the digit '1' with special wider 1
  285.         for (p=buffer;*p;p++)
  286.                 if (*p=='1') *p=(char)132; // Pierre-Marie Baty -- missing cast
  287.  
  288.         int w, h;
  289.         gr_get_string_size(cv_font, buffer, &w, &h, nullptr);
  290.         gr_string(canvas, cv_font, FSPACX(x) - w, FSPACY(y), buffer, w, h);
  291. }
  292.  
  293. __attribute_format_printf(5, 6)
  294. static void scores_rprintf(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const  format, ...)
  295. {
  296.         va_list args;
  297.         char buffer[128];
  298.  
  299.         va_start(args, format );
  300.         vsnprintf(buffer,sizeof(buffer),format,args);
  301.         va_end(args);
  302.         scores_rputs(canvas, cv_font, x, y, buffer);
  303. }
  304.  
  305. static void scores_draw_item(grs_canvas &canvas, const grs_font &cv_font, const unsigned i, stats_info *const stats)
  306. {
  307.         char buffer[20];
  308.  
  309.         int y;
  310.  
  311.         y = 77+i*9;
  312.  
  313.         if (i==0)
  314.                 y -= 8;
  315.  
  316.         if ( i==MAX_HIGH_SCORES )
  317.                 y += 8;
  318.         else
  319.                 scores_rprintf(canvas, cv_font, 57, y - 3, "%d.", i + 1);
  320.  
  321.         y -= 3;
  322.  
  323.         const auto &&fspacx = FSPACX();
  324.         const auto &&fspacx66 = fspacx(66);
  325.         const auto &&fspacy_y = FSPACY(y);
  326.         if (!stats->name[0u])
  327.         {
  328.                 gr_string(canvas, cv_font, fspacx66, fspacy_y, TXT_EMPTY);
  329.                 return;
  330.         }
  331.         gr_string(canvas, cv_font, fspacx66, fspacy_y, stats->name);
  332.         int_to_string(stats->score, buffer);
  333.         scores_rputs(canvas, cv_font, 149, y, buffer);
  334.  
  335.         gr_string(canvas, cv_font, fspacx(166), fspacy_y, MENU_DIFFICULTY_TEXT(stats->diff_level));
  336.  
  337.         if ( (stats->starting_level > 0 ) && (stats->ending_level > 0 ))
  338.                 scores_rprintf(canvas, cv_font, 232, y, "%d-%d", stats->starting_level, stats->ending_level);
  339.         else if ( (stats->starting_level < 0 ) && (stats->ending_level > 0 ))
  340.                 scores_rprintf(canvas, cv_font, 232, y, "S%d-%d", -stats->starting_level, stats->ending_level);
  341.         else if ( (stats->starting_level < 0 ) && (stats->ending_level < 0 ))
  342.                 scores_rprintf(canvas, cv_font, 232, y, "S%d-S%d", -stats->starting_level, -stats->ending_level);
  343.         else if ( (stats->starting_level > 0 ) && (stats->ending_level < 0 ))
  344.                 scores_rprintf(canvas, cv_font, 232, y, "%d-S%d", stats->starting_level, -stats->ending_level);
  345.  
  346.         {
  347.                 int h, m, s;
  348.                 h = stats->seconds/3600;
  349.                 s = stats->seconds%3600;
  350.                 m = s / 60;
  351.                 s = s % 60;
  352.                 scores_rprintf(canvas, cv_font, 276, y, "%d:%02d:%02d", h, m, s);
  353.         }
  354. }
  355.  
  356. struct scores_menu : ignore_window_pointer_t
  357. {
  358.         int                     citem;
  359.         fix64                   t1;
  360.         int                     looper;
  361.         all_scores      scores;
  362.         stats_info      last_game;
  363. };
  364.  
  365. static window_event_result scores_handler(window *wind,const d_event &event, scores_menu *menu)
  366. {
  367.         int k;
  368.         static const std::array<int8_t, 64> fades{{
  369.                 1,1,1,2,2,3,4,4,5,6,8,9,10,12,13,15,16,17,19,20,22,23,24,26,27,28,28,29,30,30,31,31,31,31,31,30,30,29,28,28,27,26,24,23,22,20,19,17,16,15,13,12,10,9,8,6,5,4,4,3,2,2,1,1
  370.         }};
  371.         const auto &&fspacx = FSPACX();
  372.         const auto &&fspacy = FSPACY();
  373.         int w = fspacx(290), h = fspacy(170);
  374.  
  375.         switch (event.type)
  376.         {
  377.                 case EVENT_WINDOW_ACTIVATED:
  378.                         game_flush_inputs();
  379.                         break;
  380.                        
  381.                 case EVENT_KEY_COMMAND:
  382.                         k = event_key_get(event);
  383.                         switch( k )     {
  384.                                 case KEY_CTRLED+KEY_R:         
  385.                                         if ( menu->citem < 0 )          {
  386.                                                 // Reset scores...
  387.                                                 if ( nm_messagebox( NULL, 2,  TXT_NO, TXT_YES, TXT_RESET_HIGH_SCORES )==1 )     {
  388.                                                         PHYSFS_delete(SCORES_FILENAME);
  389.                                                         scores_view(&menu->last_game, menu->citem);     // create new scores window
  390.                                                         return window_event_result::close;
  391.                                                 }
  392.                                         }
  393.                                         return window_event_result::handled;
  394.                                 case KEY_ENTER:
  395.                                 case KEY_SPACEBAR:
  396.                                 case KEY_ESC:
  397.                                         return window_event_result::close;
  398.                         }
  399.                         break;
  400.  
  401.                 case EVENT_MOUSE_BUTTON_DOWN:
  402.                 case EVENT_MOUSE_BUTTON_UP:
  403.                         if (event_mouse_get_button(event) == MBTN_LEFT || event_mouse_get_button(event) == MBTN_RIGHT)
  404.                         {
  405.                                 return window_event_result::close;
  406.                         }
  407.                         break;
  408.  
  409.                 case EVENT_JOYSTICK_BUTTON_DOWN:
  410.                         return window_event_result::close;
  411.  
  412.                 case EVENT_IDLE:
  413.                         timer_delay2(50);
  414.                         break;
  415.  
  416.                 case EVENT_WINDOW_DRAW:
  417.                         gr_set_default_canvas();
  418.                        
  419.                         nm_draw_background(*grd_curcanv, ((SWIDTH - w) / 2) - BORDERX, ((SHEIGHT - h) / 2) - BORDERY, ((SWIDTH - w) / 2) + w + BORDERX, ((SHEIGHT - h) / 2) + h + BORDERY);
  420.                        
  421.                         {
  422.                         auto &canvas = window_get_canvas(*wind);
  423.                         auto &medium3_font = *MEDIUM3_FONT;
  424.                         gr_string(canvas, medium3_font, 0x8000, fspacy(15), TXT_HIGH_SCORES);
  425.                         gr_set_fontcolor(canvas, BM_XRGB(31, 26, 5), -1);
  426.                         auto &game_font = *GAME_FONT;
  427.                         gr_string(canvas, game_font, fspacx( 71), fspacy(50), TXT_NAME);
  428.                         gr_string(canvas, game_font, fspacx(122), fspacy(50), TXT_SCORE);
  429.                         gr_string(canvas, game_font, fspacx(167), fspacy(50), TXT_SKILL);
  430.                         gr_string(canvas, game_font, fspacx(210), fspacy(50), TXT_LEVELS);
  431.                         gr_string(canvas, game_font, fspacx(253), fspacy(50), TXT_TIME);
  432.                        
  433.                         if ( menu->citem < 0 ) 
  434.                                 gr_string(canvas, game_font, 0x8000, fspacy(175), TXT_PRESS_CTRL_R);
  435.                        
  436.                         gr_set_fontcolor(canvas, BM_XRGB(28, 28, 28), -1);
  437.                        
  438.                         gr_printf(canvas, game_font, 0x8000, fspacy(31), "%c%s%c  - %s", 34, menu->scores.cool_saying, 34, static_cast<const char *>(menu->scores.stats[0].name));
  439.                        
  440.                         for (int i=0; i<MAX_HIGH_SCORES; i++ ) {
  441.                                 gr_set_fontcolor(canvas, BM_XRGB(28 - i * 2, 28 - i * 2, 28 - i * 2), -1);
  442.                                 scores_draw_item(canvas, game_font, i, &menu->scores.stats[i]);
  443.                         }
  444.                        
  445.                         if ( menu->citem > -1 ) {
  446.                                
  447.                                 gr_set_fontcolor(canvas, BM_XRGB(7 + fades[menu->looper], 7 + fades[menu->looper], 7 + fades[menu->looper]), -1);
  448.                                 if (timer_query() >= menu->t1+F1_0/128)
  449.                                 {
  450.                                         menu->t1 = timer_query();
  451.                                         menu->looper++;
  452.                                         if (menu->looper>63) menu->looper=0;
  453.                                 }
  454.  
  455.                                 scores_draw_item(canvas, game_font, menu->citem, menu->citem == MAX_HIGH_SCORES
  456.                                         ? &menu->last_game
  457.                                         : &menu->scores.stats[menu->citem]);
  458.                         }
  459.                         }
  460.                         break;
  461.                 case EVENT_WINDOW_CLOSE:
  462.                         d_free(menu);
  463.                         break;
  464.  
  465.                 default:
  466.                         break;
  467.         }
  468.         return window_event_result::ignored;
  469. }
  470.  
  471. void scores_view(stats_info *last_game, int citem)
  472. {
  473.         scores_menu *menu;
  474.  
  475.         MALLOC(menu, scores_menu, 1);
  476.         if (!menu)
  477.                 return;
  478.  
  479.         menu->citem = citem;
  480.         menu->t1 = timer_query();
  481.         menu->looper = 0;
  482.         if (last_game)
  483.                 menu->last_game = *last_game;
  484.  
  485.         newmenu_free_background();
  486.  
  487.         scores_read(&menu->scores);
  488.  
  489.         set_screen_mode(SCREEN_MENU);
  490.         show_menus();
  491.  
  492.         const auto &&fspacx320 = FSPACX(320);
  493.         const auto &&fspacy200 = FSPACY(200);
  494.         window_create(grd_curscreen->sc_canvas, (SWIDTH - fspacx320) / 2, (SHEIGHT - fspacy200) / 2, fspacx320, fspacy200,
  495.                                   scores_handler, menu);
  496. }
  497.