well yeah i guess
[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
222 /* This is ugly.. too bad! */
223 sys->aainfo.base = highscore_system.data;
224 sys->aainfo.stride = sizeof(highscore_record);
225 sys->aainfo.offset = offsetof(highscore_record,pool);
226 sys->aainfo.p_cmp = NULL;
227
228 sys->aainfo_datetime.base = highscore_system.data;
229 sys->aainfo_datetime.stride = sizeof(highscore_record);
230 sys->aainfo_datetime.offset = offsetof(highscore_record,aa.datetime);
231 sys->aainfo_datetime.p_cmp = highscore_cmp_datetime;
232
233 sys->aainfo_points.base = highscore_system.data;
234 sys->aainfo_points.stride = sizeof(highscore_record);
235 sys->aainfo_points.offset = offsetof(highscore_record,aa.points);
236 sys->aainfo_points.p_cmp = highscore_cmp_points;
237
238 sys->aainfo_time.base = highscore_system.data;
239 sys->aainfo_time.stride = sizeof(highscore_record);
240 sys->aainfo_time.offset = offsetof(highscore_record,aa.time);
241 sys->aainfo_time.p_cmp = highscore_cmp_time;
242
243 sys->aainfo_playerid.base = highscore_system.data;
244 sys->aainfo_playerid.stride = sizeof(highscore_record);
245 sys->aainfo_playerid.offset = offsetof(highscore_record,aa.playerid);
246 sys->aainfo_playerid.p_cmp = highscore_cmp_playerid;
247
248 sys->aainfo_playerinfo_playerid.base = highscore_system.playerinfo_data;
249 sys->aainfo_playerinfo_playerid.stride = sizeof(highscore_playerinfo);
250 sys->aainfo_playerinfo_playerid.offset =
251 offsetof(highscore_playerinfo,aa_playerid);
252 sys->aainfo_playerinfo_playerid.p_cmp = highscore_cmp_playerinfo_playerid;
253
254 sys->aainfo_playerinfo.base = highscore_system.playerinfo_data;
255 sys->aainfo_playerinfo.stride = sizeof(highscore_playerinfo);
256 sys->aainfo_playerinfo.offset = offsetof(highscore_playerinfo,aapn);
257 sys->aainfo_playerinfo.p_cmp = NULL;
258
259 sys->playerinfo_pool_size = playerinfo_pool_size;
260 sys->pool_size = pool_size;
261 }
262
263 VG_STATIC int highscores_serialize_all(void)
264 {
265 struct highscore_system *sys = &highscore_system;
266 vg_info( "Serializing database\n" );
267
268 FILE *fp = fopen( ".aadb", "wb" );
269
270 if( !fp )
271 {
272 vg_error( "Could not open .aadb\n" );
273 return 0;
274 }
275
276 fwrite( &sys->dbheader, sizeof(highscore_database), 1, fp );
277 fwrite( sys->data, sizeof(highscore_record),
278 sys->dbheader.entry_capacity, fp );
279 fwrite( sys->playerinfo_data, sizeof(highscore_playerinfo),
280 sys->dbheader.playerinfo_capacity, fp );
281
282 fclose( fp );
283 return 1;
284 }
285
286 VG_STATIC highscore_record *highscore_find_user_record( u64 playerid, u32 trackid )
287 {
288 struct highscore_system *sys = &highscore_system;
289
290 highscore_track_table *table = &sys->dbheader.tracks[trackid];
291 highscore_record temp;
292 temp.playerid = playerid;
293
294 aatree_ptr find =
295 aatree_find( &sys->aainfo_playerid, table->root_playerid, &temp );
296
297 if( find == AATREE_PTR_NIL )
298 return NULL;
299
300 return aatree_get_data( &sys->aainfo_playerid, find );
301 }
302
303 VG_STATIC aatree_ptr highscores_push_record( highscore_record *record )
304 {
305 struct highscore_system *sys = &highscore_system;
306
307 vg_low( "Inserting record into database for track %hu\n",record->trackid );
308
309 if( record->trackid >= vg_list_size(sys->dbheader.tracks) )
310 {
311 vg_error( "TrackID out of range (%hu>=%d)\n", record->trackid,
312 vg_list_size(sys->dbheader.tracks) );
313
314 return AATREE_PTR_NIL;
315 }
316
317 /* Search for existing record on this track */
318 highscore_track_table *table = &sys->dbheader.tracks[record->trackid];
319 aatree_ptr existing = aatree_find( &sys->aainfo_playerid,
320 table->root_playerid,
321 record );
322
323 if( existing != AATREE_PTR_NIL )
324 {
325 highscore_record *crecord = aatree_get_data( &sys->aainfo_playerid,
326 existing );
327
328 if( crecord->time < record->time ||
329 (crecord->time == record->time && crecord->points > record->points))
330 {
331 vg_low( "Not overwriting better score\n" );
332 return existing;
333 }
334
335 vg_low( "Freeing existing record for player %lu\n", record->playerid );
336 table->root_playerid = aatree_del( &sys->aainfo_playerid, existing );
337 table->root_datetime = aatree_del( &sys->aainfo_datetime, existing );
338 table->root_points = aatree_del( &sys->aainfo_points, existing );
339 table->root_time = aatree_del( &sys->aainfo_time, existing );
340
341 aatree_pool_free( &sys->aainfo, existing, &sys->dbheader.pool_head );
342 }
343
344 aatree_ptr index =
345 aatree_pool_alloc( &sys->aainfo, &sys->dbheader.pool_head );
346
347 if( index == AATREE_PTR_NIL )
348 {
349 vg_error( "Database records are over capacity!\n" );
350 return index;
351 }
352
353 highscore_record *dst = aatree_get_data( &sys->aainfo, index );
354 memset( dst, 0, sizeof(highscore_record) );
355
356 dst->trackid = record->trackid;
357 dst->datetime = record->datetime;
358 dst->playerid = record->playerid;
359 dst->points = record->points;
360 dst->time = record->time;
361
362 table->root_time =
363 aatree_insert( &sys->aainfo_time, table->root_time, index );
364 table->root_datetime =
365 aatree_insert( &sys->aainfo_datetime, table->root_datetime, index );
366 table->root_playerid =
367 aatree_insert( &sys->aainfo_playerid, table->root_playerid, index );
368 table->root_points =
369 aatree_insert( &sys->aainfo_points, table->root_points, index );
370
371 return index;
372 }
373
374 VG_STATIC aatree_ptr highscore_set_user_nickname( u64 steamid, char nick[16] )
375 {
376 char name[17];
377 for( int i=0; i<16; i++ )
378 name[i] = nick[i];
379 name[16] = '\0';
380
381 vg_low( "Updating %lu's nickname -> %s\n", steamid, name );
382
383 struct highscore_system *sys = &highscore_system;
384
385 highscore_playerinfo temp;
386 temp.playerid = steamid;
387
388 aatree_ptr record = aatree_find( &sys->aainfo_playerinfo_playerid,
389 sys->dbheader.playerinfo_root,
390 &temp );
391 highscore_playerinfo *info;
392
393 if( record != AATREE_PTR_NIL )
394 {
395 info = aatree_get_data( &sys->aainfo_playerinfo, record );
396 }
397 else
398 {
399 record = aatree_pool_alloc( &sys->aainfo_playerinfo,
400 &sys->dbheader.playerinfo_head );
401
402 if( record == AATREE_PTR_NIL )
403 {
404 vg_error( "Player info database is over capacity!\n" );
405 return AATREE_PTR_NIL;
406 }
407
408 info = aatree_get_data( &sys->aainfo_playerinfo, record );
409 memset( info, 0, sizeof(highscore_playerinfo) );
410
411 info->playerid = steamid;
412 sys->dbheader.playerinfo_root = aatree_insert(
413 &sys->aainfo_playerinfo_playerid,
414 sys->dbheader.playerinfo_root, record );
415 }
416
417 for( int i=0; i<16; i++ )
418 info->nickname[i] = nick[i];
419
420 return AATREE_PTR_NIL;
421 }
422
423 /* Get the length of a string, bounded by '\0' or len, whichever is first */
424 VG_STATIC int highscore_strlen( const char *str, int len )
425 {
426 int str_length;
427 for( str_length=0; str_length<len; str_length++ )
428 if( !str[str_length] )
429 return str_length;
430
431 return str_length;
432 }
433
434 /* Print the string(max length:len) centered into buf (has width:width) */
435 VG_STATIC void highscore_strc( char *buf, const char *str, int len, int width )
436 {
437 int str_length = highscore_strlen( str, len ),
438 offs = (width-str_length)/2;
439
440 for( int i=0; i<str_length; i++ )
441 {
442 int j=i+offs;
443
444 if( j >= width )
445 return;
446
447 buf[j] = str[i];
448 }
449 }
450
451 /* Print the string(max length:len) left aligned into buf */
452 VG_STATIC void highscore_strl( char *buf, const char *str, int len )
453 {
454 for( int i=0; i<len; i++ )
455 {
456 if( !str[i] )
457 return;
458
459 buf[i] = str[i];
460 }
461 }
462
463 /* Print the string (max length:len) right aligned into buf (has width:width) */
464 VG_STATIC void highscore_strr( char *buf, const char *str, int len, int width )
465 {
466 int str_length = highscore_strlen( str, len );
467
468 for( int i=0; i<len; i++ )
469 {
470 if( !str[i] )
471 return;
472
473 buf[width-str_length+i] = str[i];
474 }
475 }
476
477 /* Print integer (padded with: alt), right aligned into buf(width: len) */
478 VG_STATIC void highscore_intr( char *buf, int value, int len, char alt )
479 {
480 int i=0;
481 while(value)
482 {
483 if( i>=len )
484 return;
485
486 buf[ len-1 - (i ++) ] = '0' + (value % 10);
487 value /= 10;
488 }
489
490 for( ;i<len; i ++ )
491 buf[ len-1 - i ] = alt;
492 }
493
494 /* Print integer into buffer with max length len */
495 VG_STATIC void highscore_intl( char *buf, int value, int len )
496 {
497 char temp[32];
498
499 int i=0;
500 while(value)
501 {
502 if( i>=len )
503 break;
504
505 temp[ i ++ ] = '0' + (value % 10);
506 value /= 10;
507 }
508
509 if( i>len )
510 i = len;
511
512 for( int j=0; j<i; j ++ )
513 {
514 buf[j] = temp[ i-1-j ];
515 }
516 }
517
518 /* Clear buffer with length using clr character */
519 VG_STATIC void highscore_clear( char *buf, char clr, int length )
520 {
521 for( int i=0; i<length; i++ )
522 buf[i] = clr;
523 }
524
525 /*
526 Megapark Green
527 --------------------------
528 #| Player | Time | Pts
529 1|aaaabbbbcc 5:23.32 30000
530 2| jef 0:20.34 10000
531 3|aaabbbcccl 2:30.45 20000
532 4|
533 5|
534 6|
535 7|
536 8|
537 9|
538 10|
539 */
540
541 /* Generate a highscores board in text form, the width is always 27. Buffer
542 * must be (count+3)*27 in size. */
543 VG_STATIC void highscores_board_generate( char *buf, u32 id, u32 count )
544 {
545 int w=27;
546 highscore_clear( buf, ' ', (count+3)*w );
547
548 struct track_info *inf = &track_infos[id];
549 struct highscore_system *sys = &highscore_system;
550
551 highscore_track_table *table = &sys->dbheader.tracks[ id ];
552 aatree_ptr it = aatree_kth( &sys->aainfo_time, table->root_time, 0 );
553
554 highscore_strc ( buf+w*0, inf->name, w,w );
555 highscore_clear( buf+w*1, '-', w );
556 highscore_strl ( buf+w*2, " #| Player | Time ", 27 );
557
558 for( int i=0; i<count; i++ )
559 {
560 char *line = buf+w*(3+i);
561 highscore_intr( line, i+1, 2, ' ' );
562 line[2] = '|';
563
564 if( it == AATREE_PTR_NIL )
565 continue;
566
567 highscore_record *record = aatree_get_data( &sys->aainfo_time, it );
568 highscore_playerinfo temp;
569 temp.playerid = record->playerid;
570
571 aatree_ptr info_ptr = aatree_find( &sys->aainfo_playerinfo_playerid,
572 sys->dbheader.playerinfo_root,
573 &temp );
574
575 /* Player name */
576 if( info_ptr == AATREE_PTR_NIL )
577 highscore_strl( line+3, "unknown", 16 );
578 else
579 {
580 highscore_playerinfo *inf = aatree_get_data(
581 &sys->aainfo_playerinfo_playerid, info_ptr );
582
583 highscore_strl( line+3, inf->nickname, 16 );
584 }
585
586 u16 centiseconds = record->time,
587 seconds = centiseconds / 100,
588 minutes = seconds / 60;
589
590 centiseconds %= 100;
591 seconds %= 60;
592 minutes %= 60;
593
594 if( minutes > 9 ) minutes = 9;
595
596 /* Timer */
597 highscore_intr( line+20, minutes, 1, '0' );
598 line[21] = ':';
599 highscore_intr( line+22, seconds, 2, '0' );
600 line[24] = '.';
601 highscore_intr( line+25, centiseconds, 2, '0' );
602
603 #if 0
604 /* Score */
605 highscore_intl( line+22, record->points, 5 );
606 #endif
607 it = aatree_next( &sys->aainfo_time, it );
608 }
609 }
610
611 /* Print string out to file using newlines. Count is number of records
612 * ( this requires a buffer of (count+3)*27 size */
613 VG_STATIC void highscores_board_printf( FILE *fp, const char *buf, u32 count )
614 {
615 int w=27;
616
617 for( int i=0; i<count+3; i++ )
618 fprintf( fp, "%.27s\n", &buf[i*w] );
619 }
620
621 #endif /* HIGHSCORES_H */