Subversion Repositories Games.Descent

Rev

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
}