achievements
[carveJwlIkooP6JGAAIwe30JlM.git] / highscores.h
1 /*
2 * Copyright (C) 2021-2022 Mt.ZERO Software, Harry Godden - All Rights Reserved
3 */
4
5 #ifndef HIGHSCORES_H
6 #define HIGHSCORES_H
7
8 #include "vg/vg_store.h"
9 #include "vg/vg_stdint.h"
10 #include "world_info.h"
11
12 /*
13 * Designed to be used across client and server,
14 * for the client its only storing the local users records, for server its
15 * storing many.
16 */
17
18 typedef struct highscore highscore;
19 typedef struct highscore_record highscore_record;
20 typedef struct highscore_track_table highscore_track_table;
21 typedef struct highscore_database highscore_database;
22 typedef struct highscore_playerinfo highscore_playerinfo;
23
24 #pragma pack(push,1)
25
26 struct highscore_playerinfo
27 {
28 char nickname[16];
29 u64 playerid;
30
31 union
32 {
33 aatree_pool_node aapn;
34 aatree_node aa_playerid;
35 };
36 };
37
38 struct highscore_record
39 {
40 u16 trackid, points, time, reserved0;
41 u64 playerid;
42 u32 datetime;
43 u32 reserved1;
44
45 union
46 {
47 struct
48 {
49 aatree_node points,
50 time,
51 playerid,
52 datetime;
53 }
54 aa;
55
56 aatree_pool_node pool;
57 };
58 };
59
60 struct highscore_track_table
61 {
62 aatree_ptr root_points,
63 root_time,
64 root_playerid,
65 root_datetime;
66
67 u32 reserved[12];
68 };
69
70 struct highscore_database
71 {
72 highscore_track_table tracks[ 128 ];
73
74 aatree_ptr pool_head, playerinfo_head;
75 u32 entry_capacity,
76 playerinfo_capacity, playerinfo_root;
77
78 u32 reserved[59];
79 };
80
81 #pragma pack(pop)
82
83 VG_STATIC struct highscore_system
84 {
85 highscore_database dbheader;
86 aatree aainfo,
87 aainfo_points,
88 aainfo_time,
89 aainfo_playerid,
90 aainfo_datetime,
91 aainfo_playerinfo_playerid,
92 aainfo_playerinfo;
93
94 void *data,
95 *playerinfo_data;
96
97 u32 pool_size, playerinfo_pool_size;
98 }
99 highscore_system;
100
101 VG_STATIC int highscore_cmp_points( void *a, void *b )
102 {
103 highscore_record *pa = a, *pb = b;
104 return (int)pa->points - (int)pb->points;
105 }
106
107 VG_STATIC int highscore_cmp_datetime( void *a, void *b )
108 {
109 highscore_record *pa = a, *pb = b;
110
111 if( pa->datetime == pb->datetime ) return 0;
112 return pa->datetime < pb->datetime? 1: -1;
113 }
114
115 VG_STATIC int highscore_cmp_time( void *a, void *b )
116 {
117 highscore_record *pa = a, *pb = b;
118 return (int)pb->time - (int)pa->time;
119 }
120
121 VG_STATIC int highscore_cmp_playerid( void *a, void *b )
122 {
123 highscore_record *pa = a, *pb = b;
124 if( pa->playerid == pb->playerid ) return 0;
125 return pa->playerid < pb->playerid? -1: 1;
126 }
127
128 VG_STATIC int highscore_cmp_playerinfo_playerid( void *a, void *b )
129 {
130 highscore_playerinfo *pa = a, *pb = b;
131 if( pa->playerid == pb->playerid ) return 0;
132 return pa->playerid < pb->playerid? -1: 1;
133 }
134
135 VG_STATIC void highscores_create_db(void)
136 {
137 struct highscore_system *sys = &highscore_system;
138
139 vg_info( "Initializing database nodes\n" );
140 memset( &sys->dbheader, 0, sizeof(highscore_database) );
141
142 sys->dbheader.pool_head = aatree_init_pool( &sys->aainfo, sys->pool_size );
143 sys->dbheader.entry_capacity = sys->pool_size;
144
145 for( int i=0; i<vg_list_size(sys->dbheader.tracks); i++ )
146 {
147 highscore_track_table *table = &sys->dbheader.tracks[i];
148 table->root_points = AATREE_PTR_NIL;
149 table->root_playerid = AATREE_PTR_NIL;
150 table->root_time = AATREE_PTR_NIL;
151 table->root_datetime = AATREE_PTR_NIL;
152 }
153
154 /* Initialize secondary db */
155 sys->dbheader.playerinfo_head = aatree_init_pool(
156 &sys->aainfo_playerinfo,
157 sys->playerinfo_pool_size );
158 sys->dbheader.playerinfo_capacity = sys->playerinfo_pool_size;
159 sys->dbheader.playerinfo_root = AATREE_PTR_NIL;
160 }
161
162 VG_STATIC int highscores_read(void)
163 {
164 struct highscore_system *sys = &highscore_system;
165
166 FILE *fp = fopen( ".aadb", "rb" );
167 if( fp )
168 {
169 vg_info( "Loading existing database\n" );
170
171 u64 count = fread( &sys->dbheader, sizeof(highscore_database), 1, fp );
172
173 if( count != 1 )
174 {
175 vg_error( "Unexpected EOF reading database header\n" );
176 return 0;
177 }
178
179 count = fread( sys->data, sizeof(highscore_record),
180 sys->pool_size, fp );
181
182 if( count != sys->pool_size )
183 {
184 vg_error( "Unexpected EOF reading database contents;"
185 " %lu records of %u were read\n", count, sys->pool_size );
186 return 0;
187 }
188
189 count = fread( sys->playerinfo_data, sizeof(highscore_playerinfo),
190 sys->playerinfo_pool_size, fp );
191
192 if( count != sys->playerinfo_pool_size )
193 {
194 vg_error( "Unexpected EOF reading playerinfo contents;"
195 " %lu records of %u were read\n", count,
196 sys->playerinfo_pool_size );
197 return 0;
198 }
199
200 fclose( fp );
201 return 1;
202 }
203 else
204 {
205 vg_low( "No existing database found (.aadb)\n" );
206 return 0;
207 }
208 }
209
210 VG_STATIC void highscores_init( u32 pool_size, u32 playerinfo_pool_size )
211 {
212 struct highscore_system *sys = &highscore_system;
213
214 sys->data = vg_linear_alloc( vg_mem.rtmemory,
215 pool_size*sizeof(highscore_record) );
216
217 sys->playerinfo_data =
218 vg_linear_alloc( vg_mem.rtmemory,
219 playerinfo_pool_size * sizeof(highscore_playerinfo) );
220
221 memset( sys->data, 0, pool_size*sizeof(highscore_record) );
222 memset( sys->playerinfo_data, 0,
223 playerinfo_pool_size*sizeof(highscore_playerinfo) );
224
225
226 /* This is ugly.. too bad! */
227 sys->aainfo.base = highscore_system.data;
228 sys->aainfo.stride = sizeof(highscore_record);
229 sys->aainfo.offset = offsetof(highscore_record,pool);
230 sys->aainfo.p_cmp = NULL;
231
232 sys->aainfo_datetime.base = highscore_system.data;
233 sys->aainfo_datetime.stride = sizeof(highscore_record);
234 sys->aainfo_datetime.offset = offsetof(highscore_record,aa.datetime);
235 sys->aainfo_datetime.p_cmp = highscore_cmp_datetime;
236
237 sys->aainfo_points.base = highscore_system.data;
238 sys->aainfo_points.stride = sizeof(highscore_record);
239 sys->aainfo_points.offset = offsetof(highscore_record,aa.points);
240 sys->aainfo_points.p_cmp = highscore_cmp_points;
241
242 sys->aainfo_time.base = highscore_system.data;
243 sys->aainfo_time.stride = sizeof(highscore_record);
244 sys->aainfo_time.offset = offsetof(highscore_record,aa.time);
245 sys->aainfo_time.p_cmp = highscore_cmp_time;
246
247 sys->aainfo_playerid.base = highscore_system.data;
248 sys->aainfo_playerid.stride = sizeof(highscore_record);
249 sys->aainfo_playerid.offset = offsetof(highscore_record,aa.playerid);
250 sys->aainfo_playerid.p_cmp = highscore_cmp_playerid;
251
252 sys->aainfo_playerinfo_playerid.base = highscore_system.playerinfo_data;
253 sys->aainfo_playerinfo_playerid.stride = sizeof(highscore_playerinfo);
254 sys->aainfo_playerinfo_playerid.offset =
255 offsetof(highscore_playerinfo,aa_playerid);
256 sys->aainfo_playerinfo_playerid.p_cmp = highscore_cmp_playerinfo_playerid;
257
258 sys->aainfo_playerinfo.base = highscore_system.playerinfo_data;
259 sys->aainfo_playerinfo.stride = sizeof(highscore_playerinfo);
260 sys->aainfo_playerinfo.offset = offsetof(highscore_playerinfo,aapn);
261 sys->aainfo_playerinfo.p_cmp = NULL;
262
263 sys->playerinfo_pool_size = playerinfo_pool_size;
264 sys->pool_size = pool_size;
265 }
266
267 VG_STATIC int highscores_serialize_all(void)
268 {
269 struct highscore_system *sys = &highscore_system;
270 vg_info( "Serializing database\n" );
271
272 FILE *fp = fopen( ".aadb", "wb" );
273
274 if( !fp )
275 {
276 vg_error( "Could not open .aadb\n" );
277 return 0;
278 }
279
280 fwrite( &sys->dbheader, sizeof(highscore_database), 1, fp );
281 fwrite( sys->data, sizeof(highscore_record),
282 sys->dbheader.entry_capacity, fp );
283 fwrite( sys->playerinfo_data, sizeof(highscore_playerinfo),
284 sys->dbheader.playerinfo_capacity, fp );
285
286 fclose( fp );
287 return 1;
288 }
289
290 VG_STATIC highscore_record *highscore_find_user_record( u64 playerid, u32 trackid )
291 {
292 struct highscore_system *sys = &highscore_system;
293
294 highscore_track_table *table = &sys->dbheader.tracks[trackid];
295 highscore_record temp;
296 temp.playerid = playerid;
297
298 aatree_ptr find =
299 aatree_find( &sys->aainfo_playerid, table->root_playerid, &temp );
300
301 if( find == AATREE_PTR_NIL )
302 return NULL;
303
304 return aatree_get_data( &sys->aainfo_playerid, find );
305 }
306
307 VG_STATIC aatree_ptr highscores_push_record( highscore_record *record )
308 {
309 struct highscore_system *sys = &highscore_system;
310
311 vg_low( "Inserting record into database for track %hu\n",record->trackid );
312
313 if( record->trackid >= vg_list_size(sys->dbheader.tracks) ){
314 vg_error( "TrackID out of range (%hu>=%d)\n", record->trackid,
315 vg_list_size(sys->dbheader.tracks) );
316
317 return AATREE_PTR_NIL;
318 }
319
320 /* Search for existing record on this track */
321 highscore_track_table *table = &sys->dbheader.tracks[record->trackid];
322 aatree_ptr existing = aatree_find( &sys->aainfo_playerid,
323 table->root_playerid,
324 record );
325
326 if( existing != AATREE_PTR_NIL ){
327 highscore_record *crecord = aatree_get_data( &sys->aainfo_playerid,
328 existing );
329
330 if( crecord->time < record->time ||
331 (crecord->time == record->time && crecord->points > record->points))
332 {
333 vg_low( "Not overwriting better score\n" );
334 return existing;
335 }
336
337 vg_low( "Freeing existing record for player %lu\n", record->playerid );
338 table->root_playerid = aatree_del( &sys->aainfo_playerid, existing );
339 table->root_datetime = aatree_del( &sys->aainfo_datetime, existing );
340 table->root_points = aatree_del( &sys->aainfo_points, existing );
341 table->root_time = aatree_del( &sys->aainfo_time, existing );
342
343 aatree_pool_free( &sys->aainfo, existing, &sys->dbheader.pool_head );
344 }
345
346 aatree_ptr index =
347 aatree_pool_alloc( &sys->aainfo, &sys->dbheader.pool_head );
348
349 if( index == AATREE_PTR_NIL )
350 {
351 vg_error( "Database records are over capacity!\n" );
352 return index;
353 }
354
355 highscore_record *dst = aatree_get_data( &sys->aainfo, index );
356 memset( dst, 0, sizeof(highscore_record) );
357
358 dst->trackid = record->trackid;
359 dst->datetime = record->datetime;
360 dst->playerid = record->playerid;
361 dst->points = record->points;
362 dst->time = record->time;
363
364 table->root_time =
365 aatree_insert( &sys->aainfo_time, table->root_time, index );
366 table->root_datetime =
367 aatree_insert( &sys->aainfo_datetime, table->root_datetime, index );
368 table->root_playerid =
369 aatree_insert( &sys->aainfo_playerid, table->root_playerid, index );
370 table->root_points =
371 aatree_insert( &sys->aainfo_points, table->root_points, index );
372
373 return index;
374 }
375
376 VG_STATIC aatree_ptr highscore_set_user_nickname( u64 steamid, char nick[16] )
377 {
378 char name[17];
379 for( int i=0; i<16; i++ )
380 name[i] = nick[i];
381 name[16] = '\0';
382
383 vg_low( "Updating %lu's nickname -> %s\n", steamid, name );
384
385 struct highscore_system *sys = &highscore_system;
386
387 highscore_playerinfo temp;
388 temp.playerid = steamid;
389
390 aatree_ptr record = aatree_find( &sys->aainfo_playerinfo_playerid,
391 sys->dbheader.playerinfo_root,
392 &temp );
393 highscore_playerinfo *info;
394
395 if( record != AATREE_PTR_NIL )
396 {
397 info = aatree_get_data( &sys->aainfo_playerinfo, record );
398 }
399 else
400 {
401 record = aatree_pool_alloc( &sys->aainfo_playerinfo,
402 &sys->dbheader.playerinfo_head );
403
404 if( record == AATREE_PTR_NIL )
405 {
406 vg_error( "Player info database is over capacity!\n" );
407 return AATREE_PTR_NIL;
408 }
409
410 info = aatree_get_data( &sys->aainfo_playerinfo, record );
411 memset( info, 0, sizeof(highscore_playerinfo) );
412
413 info->playerid = steamid;
414 sys->dbheader.playerinfo_root = aatree_insert(
415 &sys->aainfo_playerinfo_playerid,
416 sys->dbheader.playerinfo_root, record );
417 }
418
419 for( int i=0; i<16; i++ )
420 info->nickname[i] = nick[i];
421
422 return AATREE_PTR_NIL;
423 }
424
425 /* Get the length of a string, bounded by '\0' or len, whichever is first */
426 VG_STATIC int highscore_strlen( const char *str, int len )
427 {
428 int str_length;
429 for( str_length=0; str_length<len; str_length++ )
430 if( !str[str_length] )
431 return str_length;
432
433 return str_length;
434 }
435
436 /* Print the string(max length:len) centered into buf (has width:width) */
437 VG_STATIC void highscore_strc( char *buf, const char *str, int len, int width )
438 {
439 int str_length = highscore_strlen( str, len ),
440 offs = (width-str_length)/2;
441
442 for( int i=0; i<str_length; i++ )
443 {
444 int j=i+offs;
445
446 if( j >= width )
447 return;
448
449 buf[j] = str[i];
450 }
451 }
452
453 /* Print the string(max length:len) left aligned into buf */
454 VG_STATIC void highscore_strl( char *buf, const char *str, int len )
455 {
456 for( int i=0; i<len; i++ )
457 {
458 if( !str[i] )
459 return;
460
461 buf[i] = str[i];
462 }
463 }
464
465 /* Print the string (max length:len) right aligned into buf (has width:width) */
466 VG_STATIC void highscore_strr( char *buf, const char *str, int len, int width )
467 {
468 int str_length = highscore_strlen( str, len );
469
470 for( int i=0; i<len; i++ )
471 {
472 if( !str[i] )
473 return;
474
475 buf[width-str_length+i] = str[i];
476 }
477 }
478
479 /* Print integer (padded with: alt), right aligned into buf(width: len)
480 * returns number of digits (not including alt), that were written to buf */
481 VG_STATIC int highscore_intr( char *buf, int value, int len, char alt )
482 {
483 int i=0;
484 while(value){
485 if( i>=len )
486 return i;
487
488 buf[ len-1 - (i ++) ] = '0' + (value % 10);
489 value /= 10;
490 }
491
492 for( ;i<len; i ++ )
493 buf[ len-1 - i ] = alt;
494
495 return i;
496 }
497
498 /* Print integer into buffer with max length len
499 * retuns the number of digits written to buf */
500 VG_STATIC int highscore_intl( char *buf, int value, int len )
501 {
502 char temp[32];
503
504 int i=0;
505 while(value){
506 if( i>=len )
507 break;
508
509 temp[ i ++ ] = '0' + (value % 10);
510 value /= 10;
511 }
512
513 if( i>len )
514 i = len;
515
516 for( int j=0; j<i; j ++ )
517 buf[j] = temp[ i-1-j ];
518
519 return i;
520 }
521
522 /* Clear buffer with length using clr character */
523 VG_STATIC void highscore_clear( char *buf, char clr, int length )
524 {
525 for( int i=0; i<length; i++ )
526 buf[i] = clr;
527 }
528
529 /*
530 Megapark Green
531 --------------------------
532 #| Player | Time | Pts
533 1|aaaabbbbcc 5:23.32 30000
534 2| jef 0:20.34 10000
535 3|aaabbbcccl 2:30.45 20000
536 4|
537 5|
538 6|
539 7|
540 8|
541 9|
542 10|
543 */
544
545 /* Generate a highscores board in text form, the width is always 27. Buffer
546 * must be (count+3)*27 in size. */
547 VG_STATIC void highscores_board_generate( char *buf, u32 id, u32 count )
548 {
549 int w=27;
550 highscore_clear( buf, ' ', (count+3)*w );
551
552 struct track_info *inf = &track_infos[id];
553 struct highscore_system *sys = &highscore_system;
554
555 highscore_track_table *table = &sys->dbheader.tracks[ id ];
556 aatree_ptr it = aatree_kth( &sys->aainfo_time, table->root_time, 0 );
557
558 highscore_strc ( buf+w*0, inf->name, w,w );
559 highscore_clear( buf+w*1, '-', w );
560 highscore_strl ( buf+w*2, " #| Player | Time ", 27 );
561
562 for( int i=0; i<count; i++ )
563 {
564 char *line = buf+w*(3+i);
565 highscore_intr( line, i+1, 2, ' ' );
566 line[2] = '|';
567
568 if( it == AATREE_PTR_NIL )
569 continue;
570
571 highscore_record *record = aatree_get_data( &sys->aainfo_time, it );
572 highscore_playerinfo temp;
573 temp.playerid = record->playerid;
574
575 aatree_ptr info_ptr = aatree_find( &sys->aainfo_playerinfo_playerid,
576 sys->dbheader.playerinfo_root,
577 &temp );
578
579 /* Player name */
580 if( info_ptr == AATREE_PTR_NIL )
581 highscore_strl( line+3, "unknown", 16 );
582 else
583 {
584 highscore_playerinfo *inf = aatree_get_data(
585 &sys->aainfo_playerinfo_playerid, info_ptr );
586
587 highscore_strl( line+3, inf->nickname, 16 );
588
589 /* yep, this is fucking awesome! */
590 if( inf->playerid == 0x8297744501001001 ||
591 inf->playerid == 0x1ec4620101001001 ||
592 inf->playerid == 0x0110000145749782 ||
593 inf->playerid == 0x011000010162c41e )
594 {
595 i --;
596 /* FIXME: Clear line, or yknow, do it properly */
597 }
598 }
599
600 u16 centiseconds = record->time,
601 seconds = centiseconds / 100,
602 minutes = seconds / 60;
603
604 centiseconds %= 100;
605 seconds %= 60;
606 minutes %= 60;
607
608 if( minutes > 9 ) minutes = 9;
609
610 /* Timer */
611 highscore_intr( line+20, minutes, 1, '0' );
612 line[21] = ':';
613 highscore_intr( line+22, seconds, 2, '0' );
614 line[24] = '.';
615 highscore_intr( line+25, centiseconds, 2, '0' );
616
617 #if 0
618 /* Score */
619 highscore_intl( line+22, record->points, 5 );
620 #endif
621 it = aatree_next( &sys->aainfo_time, it );
622 }
623 }
624
625 /* Print string out to file using newlines. Count is number of records
626 * ( this requires a buffer of (count+3)*27 size */
627 VG_STATIC void highscores_board_printf( FILE *fp, const char *buf, u32 count )
628 {
629 int w=27;
630
631 for( int i=0; i<count+3; i++ )
632 fprintf( fp, "%.27s\n", &buf[i*w] );
633 }
634
635 #endif /* HIGHSCORES_H */