Details | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
1 | pmbaty | 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 | } |