boards now pickable
[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 {
315 vg_error( "TrackID out of range (%hu>=%d)\n", record->trackid,
316 vg_list_size(sys->dbheader.tracks) );
317
318 return AATREE_PTR_NIL;
319 }
320
321 /* Search for existing record on this track */
322 highscore_track_table *table = &sys->dbheader.tracks[record->trackid];
323 aatree_ptr existing = aatree_find( &sys->aainfo_playerid,
324 table->root_playerid,
325 record );
326
327 if( existing != AATREE_PTR_NIL )
328 {
329 highscore_record *crecord = aatree_get_data( &sys->aainfo_playerid,
330 existing );
331
332 if( crecord->time < record->time ||
333 (crecord->time == record->time && crecord->points > record->points))
334 {
335 vg_low( "Not overwriting better score\n" );
336 return existing;
337 }
338
339 vg_low( "Freeing existing record for player %lu\n", record->playerid );
340 table->root_playerid = aatree_del( &sys->aainfo_playerid, existing );
341 table->root_datetime = aatree_del( &sys->aainfo_datetime, existing );
342 table->root_points = aatree_del( &sys->aainfo_points, existing );
343 table->root_time = aatree_del( &sys->aainfo_time, existing );
344
345 aatree_pool_free( &sys->aainfo, existing, &sys->dbheader.pool_head );
346 }
347
348 aatree_ptr index =
349 aatree_pool_alloc( &sys->aainfo, &sys->dbheader.pool_head );
350
351 if( index == AATREE_PTR_NIL )
352 {
353 vg_error( "Database records are over capacity!\n" );
354 return index;
355 }
356
357 highscore_record *dst = aatree_get_data( &sys->aainfo, index );
358 memset( dst, 0, sizeof(highscore_record) );
359
360 dst->trackid = record->trackid;
361 dst->datetime = record->datetime;
362 dst->playerid = record->playerid;
363 dst->points = record->points;
364 dst->time = record->time;
365
366 table->root_time =
367 aatree_insert( &sys->aainfo_time, table->root_time, index );
368 table->root_datetime =
369 aatree_insert( &sys->aainfo_datetime, table->root_datetime, index );
370 table->root_playerid =
371 aatree_insert( &sys->aainfo_playerid, table->root_playerid, index );
372 table->root_points =
373 aatree_insert( &sys->aainfo_points, table->root_points, index );
374
375 return index;
376 }
377
378 VG_STATIC aatree_ptr highscore_set_user_nickname( u64 steamid, char nick[16] )
379 {
380 char name[17];
381 for( int i=0; i<16; i++ )
382 name[i] = nick[i];
383 name[16] = '\0';
384
385 vg_low( "Updating %lu's nickname -> %s\n", steamid, name );
386
387 struct highscore_system *sys = &highscore_system;
388
389 highscore_playerinfo temp;
390 temp.playerid = steamid;
391
392 aatree_ptr record = aatree_find( &sys->aainfo_playerinfo_playerid,
393 sys->dbheader.playerinfo_root,
394 &temp );
395 highscore_playerinfo *info;
396
397 if( record != AATREE_PTR_NIL )
398 {
399 info = aatree_get_data( &sys->aainfo_playerinfo, record );
400 }
401 else
402 {
403 record = aatree_pool_alloc( &sys->aainfo_playerinfo,
404 &sys->dbheader.playerinfo_head );
405
406 if( record == AATREE_PTR_NIL )
407 {
408 vg_error( "Player info database is over capacity!\n" );
409 return AATREE_PTR_NIL;
410 }
411
412 info = aatree_get_data( &sys->aainfo_playerinfo, record );
413 memset( info, 0, sizeof(highscore_playerinfo) );
414
415 info->playerid = steamid;
416 sys->dbheader.playerinfo_root = aatree_insert(
417 &sys->aainfo_playerinfo_playerid,
418 sys->dbheader.playerinfo_root, record );
419 }
420
421 for( int i=0; i<16; i++ )
422 info->nickname[i] = nick[i];
423
424 return AATREE_PTR_NIL;
425 }
426
427 /* Get the length of a string, bounded by '\0' or len, whichever is first */
428 VG_STATIC int highscore_strlen( const char *str, int len )
429 {
430 int str_length;
431 for( str_length=0; str_length<len; str_length++ )
432 if( !str[str_length] )
433 return str_length;
434
435 return str_length;
436 }
437
438 /* Print the string(max length:len) centered into buf (has width:width) */
439 VG_STATIC void highscore_strc( char *buf, const char *str, int len, int width )
440 {
441 int str_length = highscore_strlen( str, len ),
442 offs = (width-str_length)/2;
443
444 for( int i=0; i<str_length; i++ )
445 {
446 int j=i+offs;
447
448 if( j >= width )
449 return;
450
451 buf[j] = str[i];
452 }
453 }
454
455 /* Print the string(max length:len) left aligned into buf */
456 VG_STATIC void highscore_strl( char *buf, const char *str, int len )
457 {
458 for( int i=0; i<len; i++ )
459 {
460 if( !str[i] )
461 return;
462
463 buf[i] = str[i];
464 }
465 }
466
467 /* Print the string (max length:len) right aligned into buf (has width:width) */
468 VG_STATIC void highscore_strr( char *buf, const char *str, int len, int width )
469 {
470 int str_length = highscore_strlen( str, len );
471
472 for( int i=0; i<len; i++ )
473 {
474 if( !str[i] )
475 return;
476
477 buf[width-str_length+i] = str[i];
478 }
479 }
480
481 /* Print integer (padded with: alt), right aligned into buf(width: len)
482 * returns number of digits (not including alt), that were written to buf */
483 VG_STATIC int highscore_intr( char *buf, int value, int len, char alt )
484 {
485 int i=0;
486 while(value){
487 if( i>=len )
488 return i;
489
490 buf[ len-1 - (i ++) ] = '0' + (value % 10);
491 value /= 10;
492 }
493
494 for( ;i<len; i ++ )
495 buf[ len-1 - i ] = alt;
496
497 return i;
498 }
499
500 /* Print integer into buffer with max length len
501 * retuns the number of digits written to buf */
502 VG_STATIC int highscore_intl( char *buf, int value, int len )
503 {
504 char temp[32];
505
506 int i=0;
507 while(value){
508 if( i>=len )
509 break;
510
511 temp[ i ++ ] = '0' + (value % 10);
512 value /= 10;
513 }
514
515 if( i>len )
516 i = len;
517
518 for( int j=0; j<i; j ++ )
519 buf[j] = temp[ i-1-j ];
520
521 return i;
522 }
523
524 /* Clear buffer with length using clr character */
525 VG_STATIC void highscore_clear( char *buf, char clr, int length )
526 {
527 for( int i=0; i<length; i++ )
528 buf[i] = clr;
529 }
530
531 /*
532 Megapark Green
533 --------------------------
534 #| Player | Time | Pts
535 1|aaaabbbbcc 5:23.32 30000
536 2| jef 0:20.34 10000
537 3|aaabbbcccl 2:30.45 20000
538 4|
539 5|
540 6|
541 7|
542 8|
543 9|
544 10|
545 */
546
547 /* Generate a highscores board in text form, the width is always 27. Buffer
548 * must be (count+3)*27 in size. */
549 VG_STATIC void highscores_board_generate( char *buf, u32 id, u32 count )
550 {
551 int w=27;
552 highscore_clear( buf, ' ', (count+3)*w );
553
554 struct track_info *inf = &track_infos[id];
555 struct highscore_system *sys = &highscore_system;
556
557 highscore_track_table *table = &sys->dbheader.tracks[ id ];
558 aatree_ptr it = aatree_kth( &sys->aainfo_time, table->root_time, 0 );
559
560 highscore_strc ( buf+w*0, inf->name, w,w );
561 highscore_clear( buf+w*1, '-', w );
562 highscore_strl ( buf+w*2, " #| Player | Time ", 27 );
563
564 for( int i=0; i<count; i++ )
565 {
566 char *line = buf+w*(3+i);
567 highscore_intr( line, i+1, 2, ' ' );
568 line[2] = '|';
569
570 if( it == AATREE_PTR_NIL )
571 continue;
572
573 highscore_record *record = aatree_get_data( &sys->aainfo_time, it );
574 highscore_playerinfo temp;
575 temp.playerid = record->playerid;
576
577 aatree_ptr info_ptr = aatree_find( &sys->aainfo_playerinfo_playerid,
578 sys->dbheader.playerinfo_root,
579 &temp );
580
581 /* Player name */
582 if( info_ptr == AATREE_PTR_NIL )
583 highscore_strl( line+3, "unknown", 16 );
584 else
585 {
586 highscore_playerinfo *inf = aatree_get_data(
587 &sys->aainfo_playerinfo_playerid, info_ptr );
588
589 highscore_strl( line+3, inf->nickname, 16 );
590
591 /* yep, this is fucking awesome! */
592 if( inf->playerid == 0x8297744501001001 ||
593 inf->playerid == 0x1ec4620101001001 ||
594 inf->playerid == 0x0110000145749782 ||
595 inf->playerid == 0x011000010162c41e )
596 {
597 i --;
598 /* FIXME: Clear line, or yknow, do it properly */
599 }
600 }
601
602 u16 centiseconds = record->time,
603 seconds = centiseconds / 100,
604 minutes = seconds / 60;
605
606 centiseconds %= 100;
607 seconds %= 60;
608 minutes %= 60;
609
610 if( minutes > 9 ) minutes = 9;
611
612 /* Timer */
613 highscore_intr( line+20, minutes, 1, '0' );
614 line[21] = ':';
615 highscore_intr( line+22, seconds, 2, '0' );
616 line[24] = '.';
617 highscore_intr( line+25, centiseconds, 2, '0' );
618
619 #if 0
620 /* Score */
621 highscore_intl( line+22, record->points, 5 );
622 #endif
623 it = aatree_next( &sys->aainfo_time, it );
624 }
625 }
626
627 /* Print string out to file using newlines. Count is number of records
628 * ( this requires a buffer of (count+3)*27 size */
629 VG_STATIC void highscores_board_printf( FILE *fp, const char *buf, u32 count )
630 {
631 int w=27;
632
633 for( int i=0; i<count+3; i++ )
634 fprintf( fp, "%.27s\n", &buf[i*w] );
635 }
636
637 #endif /* HIGHSCORES_H */