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