d2f8d878edee8763179e2ab57d8d35b3ff4310ce
[vg.git] / vg_console.h
1 /* Copyright (C) 2021-2023 Harry Godden (hgn) - All Rights Reserved */
2
3 #ifndef VG_CONSOLE_H
4 #define VG_CONSOLE_H
5
6 #ifndef VG_GAME
7 #define VG_GAME
8 #endif
9
10 #include "vg/vg_imgui.h"
11 #include "vg/vg_log.h"
12
13 #define VG_VAR_F32( NAME, ... ) \
14 { u32 flags=0x00; __VA_ARGS__ ;\
15 vg_console_reg_var( #NAME, &NAME, k_var_dtype_f32, flags ); }
16
17 #define VG_VAR_I32( NAME, ... ) \
18 { u32 flags=0x00; __VA_ARGS__ ;\
19 vg_console_reg_var( #NAME, &NAME, k_var_dtype_i32, flags ); }
20
21 #define VG_VAR_PERSISTENT 0x1
22 #define VG_VAR_CHEAT 0x2
23
24 typedef struct vg_var vg_var;
25 typedef struct vg_cmd vg_cmd;
26
27 struct vg_console{
28 struct vg_var{
29 void *data;
30 const char *name;
31
32 enum vg_var_dtype{
33 k_var_dtype_i32,
34 k_var_dtype_u32,
35 k_var_dtype_f32
36 }
37 data_type;
38
39 u32 flags;
40 }
41 vars[ 128 ];
42
43 struct vg_cmd{
44 int (*function)( int argc, char const *argv[] );
45 void (*poll_suggest)( int argc, char const *argv[] );
46 const char *name;
47 }
48 functions[ 32 ];
49
50 struct {
51 const char *str;
52 int len;
53
54 u32 lev_score;
55 }
56 suggestions[12];
57 u32 suggestion_count;
58 int suggestion_select,
59 suggestion_pastepos,
60 suggestion_maxlen;
61
62 u32 var_count, function_count;
63
64 char input[96],
65 input_copy[96];
66
67 char history[32][96];
68 int history_last, history_pos, history_count;
69
70 int enabled;
71 }
72 vg_console;
73
74
75 VG_STATIC void _vg_console_draw( void );
76 void _vg_console_println( const char *str );
77 VG_STATIC int _vg_console_list( int argc, char const *argv[] );
78 VG_STATIC void _vg_console_init(void);
79 VG_STATIC void _vg_console_write_persistent(void);
80 VG_STATIC void _vg_console_free(void);
81 VG_STATIC void vg_execute_console_input( const char *cmd );
82
83 /*
84 * Console interface
85 */
86 VG_STATIC void console_history_get( char* buf, int entry_num );
87 VG_STATIC int _vg_console_enabled(void);
88 VG_STATIC void console_proc_key( SDL_Keysym ev );
89
90 /*
91 * Implementation
92 */
93 VG_STATIC int _vg_console_enabled(void)
94 {
95 return vg_console.enabled;
96 }
97
98 VG_STATIC
99 void vg_console_reg_var( const char *alias, void *ptr, enum vg_var_dtype type,
100 u32 flags )
101 {
102 if( vg_console.var_count > vg_list_size(vg_console.vars) )
103 vg_fatal_error( "Too many vars registered" );
104
105 vg_var *var = &vg_console.vars[ vg_console.var_count ++ ];
106 var->name = alias;
107 var->data = ptr;
108 var->data_type = type;
109 var->flags = flags;
110
111 vg_info( "Console variable '%s' registered\n", alias );
112 }
113
114 VG_STATIC
115 void vg_console_reg_cmd( const char *alias,
116 int (*function)(int argc, const char *argv[]),
117 void (*poll_suggest)(int argc, const char *argv[]) )
118 {
119 if( vg_console.function_count > vg_list_size(vg_console.functions) )
120 vg_fatal_error( "Too many functions registered" );
121
122 vg_cmd *cmd = &vg_console.functions[ vg_console.function_count ++ ];
123
124 cmd->function = function;
125 cmd->poll_suggest = poll_suggest;
126 cmd->name = alias;
127
128 vg_info( "Console function '%s' registered\n", alias );
129 }
130
131 VG_STATIC int _vg_console_list( int argc, char const *argv[] )
132 {
133 for( int i=0; i<vg_console.function_count; i ++ ){
134 struct vg_cmd *cmd = &vg_console.functions[ i ];
135 vg_info( "* %s\n", cmd->name );
136 }
137
138 for( int i=0; i<vg_console.var_count; i ++ ){
139 struct vg_var *cv = &vg_console.vars[ i ];
140 vg_info( "%s\n", cv->name );
141 }
142
143 return 0;
144 }
145
146 int _test_break( int argc, const char *argv[] )
147 {
148 vg_fatal_error( "Test crash from main, after loading (console)" );
149 return 0;
150 }
151
152 int _vg_console_exec( int argc, const char *argv[] )
153 {
154 if( argc < 1 ) return 0;
155
156 char path[256];
157 strcpy( path, "cfg/" );
158 strncat( path, argv[0], 250 );
159
160 FILE *fp = fopen( path, "r" );
161 if( fp ){
162 char line[256];
163
164 while( fgets( line, sizeof( line ), fp ) ){
165 line[ strcspn( line, "\r\n#" ) ] = 0x00;
166
167 if( line[0] != 0x00 ){
168 vg_execute_console_input( line );
169 }
170 }
171
172 fclose( fp );
173 }
174 else{
175 vg_error( "Could not open '%s'\n", path );
176 }
177
178 return 0;
179 }
180
181 VG_STATIC void _vg_console_init(void)
182 {
183 vg_console_reg_cmd( "list", _vg_console_list, NULL );
184 vg_console_reg_cmd( "crash", _test_break, NULL );
185 vg_console_reg_cmd( "exec", _vg_console_exec, NULL );
186 }
187
188 VG_STATIC void vg_console_load_autos(void)
189 {
190 _vg_console_exec( 1, (const char *[]){ "auto.conf" } );
191 }
192
193 VG_STATIC void _vg_console_write_persistent(void)
194 {
195 FILE *fp = fopen( "cfg/auto.conf", "w" );
196
197 for( int i=0; i<vg_console.var_count; i ++ ){
198 struct vg_var *cv = &vg_console.vars[i];
199
200 if( cv->flags & VG_VAR_PERSISTENT ){
201 switch( cv->data_type ){
202 case k_var_dtype_i32:
203 fprintf( fp, "%s %d\n", cv->name, *(i32 *)(cv->data) );
204 break;
205 case k_var_dtype_u32:
206 fprintf( fp, "%s %u\n", cv->name, *(u32 *)(cv->data) );
207 break;
208 case k_var_dtype_f32:
209 fprintf( fp, "%s %.5f\n", cv->name, *(float *)(cv->data ) );
210 break;
211 }
212 }
213 }
214
215 fclose( fp );
216 }
217
218 VG_STATIC void _vg_console_free(void)
219 {
220 _vg_console_write_persistent();
221 }
222
223 /*
224 * splits src into tokens and fills out args as pointers to those tokens
225 * returns number of tokens
226 * dst must be as long as src
227 */
228 VG_STATIC int vg_console_tokenize( const char *src, char *dst,
229 const char *args[8] )
230 {
231 int arg_count = 0,
232 in_token = 0;
233
234 for( int i=0;; i ++ ){
235 if( src[i] ){
236 if( src[i] == ' ' || src[i] == '\t' ){
237 if( in_token )
238 dst[i] = '\0';
239
240 in_token = 0;
241
242 if( arg_count == 8 )
243 break;
244 }
245 else{
246 dst[i] = src[i];
247
248 if( !in_token ){
249 args[ arg_count ++ ] = &dst[i];
250 in_token = 1;
251 }
252 }
253 }
254 else{
255 dst[i] = '\0';
256 break;
257 }
258 }
259
260 return arg_count;
261 }
262
263 VG_STATIC vg_var *vg_console_match_var( const char *kw )
264 {
265 for( int i=0; i<vg_console.var_count; i ++ ){
266 struct vg_var *cv = &vg_console.vars[ i ];
267 if( !strcmp( cv->name, kw ) ){
268 return cv;
269 }
270 }
271
272 return NULL;
273 }
274
275 VG_STATIC vg_cmd *vg_console_match_cmd( const char *kw )
276 {
277 for( int i=0; i<vg_console.function_count; i ++ ){
278 struct vg_cmd *cmd = &vg_console.functions[ i ];
279 if( !strcmp( cmd->name, kw ) ){
280 return cmd;
281 }
282 }
283
284 return NULL;
285 }
286
287 VG_STATIC void vg_execute_console_input( const char *cmd )
288 {
289 char temp[512];
290 char const *args[8];
291 int arg_count = vg_console_tokenize( cmd, temp, args );
292
293 if( arg_count == 0 )
294 return;
295
296 vg_var *cv = vg_console_match_var( args[0] );
297 vg_cmd *fn = vg_console_match_cmd( args[0] );
298
299 assert( !(cv && fn) );
300
301 if( cv ){
302 /* Cvar Matched, try get value */
303 if( arg_count >= 2 ){
304 if( (cv->data_type == k_var_dtype_u32) ||
305 (cv->data_type == k_var_dtype_i32) )
306 {
307 int *ptr = cv->data;
308 *ptr = atoi( args[1] );
309 }
310 else if( cv->data_type == k_var_dtype_f32 ){
311 float *ptr = cv->data;
312 *ptr = atof( args[1] );
313 }
314 }
315 else{
316 if( cv->data_type == k_var_dtype_i32 )
317 vg_info( "= %d\n", *((int *)cv->data) );
318 else if( cv->data_type == k_var_dtype_u32 )
319 vg_info( "= %u\n", *((u32 *)cv->data) );
320 else if( cv->data_type == k_var_dtype_f32 )
321 vg_info( "= %.4f\n", *((float *)cv->data) );
322 }
323
324 return;
325 }
326 else if( fn ){
327 fn->function( arg_count-1, args+1 );
328 return;
329 }
330
331 vg_error( "No command/var named '%s'. Use 'list' to view all\n", args[0] );
332 }
333
334 u32 str_lev_distance( const char *s1, const char *s2 )
335 {
336 u32 m = strlen( s1 ),
337 n = strlen( s2 );
338
339 if( m==0 ) return n;
340 if( n==0 ) return m;
341
342 assert( n+1 <= 256 );
343
344 u32 costs[ 256 ];
345
346 for( u32 k=0; k<=n; k++ )
347 costs[k] = k;
348
349 u32 i = 0;
350 for( u32 i=0; i<m; i++ ){
351 costs[0] = i+1;
352
353 u32 corner = i;
354
355 for( u32 j=0; j<n; j++ ){
356 u32 upper = costs[j+1];
357
358 if( s1[i] == s2[j] )
359 costs[ j+1 ] = corner;
360 else{
361 u32 t = (upper < corner)? upper: corner;
362 costs[j+1] = ((costs[j] < t)? costs[j]: t) + 1;
363 }
364
365 corner = upper;
366 }
367 }
368
369 return costs[n];
370 }
371
372 u32 str_lcs( const char *s1, const char *s2 )
373 {
374 u32 m = VG_MIN( 31, strlen( s1 ) ),
375 n = VG_MIN( 31, strlen( s2 ) );
376
377 int suff[32][32],
378 result = 0;
379
380 for( int i=0; i<=m; i++ ){
381 for( int j=0; j<=n; j++ ){
382 if( i == 0 || j == 0 )
383 suff[i][j] = 0;
384 else if( s1[i-1] == s2[j-1] ){
385 suff[i][j] = suff[i-1][j-1] + 1;
386 result = VG_MAX( result, suff[i][j] );
387 }
388 else
389 suff[i][j] = 0;
390 }
391 }
392
393 return result;
394 }
395
396 /* str must not fuckoff ever! */
397 VG_STATIC void console_suggest_score_text( const char *str, const char *input,
398 int minscore )
399 {
400 /* filter duplicates */
401 for( int i=0; i<vg_console.suggestion_count; i++ )
402 if( !strcmp( vg_console.suggestions[i].str, str ) )
403 return;
404
405 /* calc score */
406 u32 score = str_lcs( str, input );
407
408 if( score < minscore )
409 return;
410
411 int best_pos = vg_console.suggestion_count;
412 for( int j=best_pos-1; j>=0; j -- )
413 if( score > vg_console.suggestions[j].lev_score )
414 best_pos = j;
415
416 /* insert if good score */
417 if( best_pos < vg_list_size( vg_console.suggestions ) ){
418 int start = VG_MIN( vg_console.suggestion_count,
419 vg_list_size( vg_console.suggestions )-1 );
420 for( int j=start; j>best_pos; j -- )
421 vg_console.suggestions[j] = vg_console.suggestions[j-1];
422
423 vg_console.suggestions[ best_pos ].str = str;
424 vg_console.suggestions[ best_pos ].len = strlen( str );
425 vg_console.suggestions[ best_pos ].lev_score = score;
426
427 if( vg_console.suggestion_count <
428 vg_list_size( vg_console.suggestions ) )
429 vg_console.suggestion_count ++;
430 }
431 }
432
433 VG_STATIC void console_update_suggestions(void)
434 {
435 if( vg_ui.focused_control_type != k_ui_control_textbox ||
436 vg_ui.textbuf != vg_console.input )
437 return;
438
439 vg_console.suggestion_count = 0;
440 vg_console.suggestion_select = -1;
441 vg_console.suggestion_maxlen = 0;
442
443 /*
444 * - must be typing something
445 * - must be at the end
446 * - prev char must not be a whitespace
447 * - cursors should match
448 */
449
450 if( vg_ui.textbox.cursor_pos == 0 ) return;
451 if( vg_ui.textbox.cursor_pos != vg_ui.textbox.cursor_user ) return;
452 if( vg_console.input[ vg_ui.textbox.cursor_pos ] != '\0' ) return;
453
454 if( (vg_console.input[ vg_ui.textbox.cursor_pos -1 ] == ' ') ||
455 (vg_console.input[ vg_ui.textbox.cursor_pos -1 ] == '\t') )
456 return;
457
458 char temp[128];
459 const char *args[8];
460
461 int token_count = vg_console_tokenize( vg_console.input, temp, args );
462 if( !token_count ) return;
463 vg_console.suggestion_pastepos = args[token_count-1]-temp;
464
465 /* Score all our commands and cvars */
466 if( token_count == 1 ){
467 for( int i=0; i<vg_console.var_count; i++ ){
468 vg_var *cvar = &vg_console.vars[i];
469 console_suggest_score_text( cvar->name, args[0], 1 );
470 }
471
472 for( int i=0; i<vg_console.function_count; i++ ){
473 vg_cmd *cmd = &vg_console.functions[i];
474 console_suggest_score_text( cmd->name, args[0], 1 );
475 }
476 }
477 else{
478 vg_cmd *cmd = vg_console_match_cmd( args[0] );
479 vg_var *var = vg_console_match_var( args[0] );
480
481 assert( !( cmd && var ) );
482
483 if( cmd )
484 if( cmd->poll_suggest )
485 cmd->poll_suggest( token_count-1, &args[1] );
486 }
487
488 /* some post processing */
489 for( int i=0; i<vg_console.suggestion_count; i++ ){
490 vg_console.suggestion_maxlen = VG_MAX( vg_console.suggestion_maxlen,
491 vg_console.suggestions[i].len );
492
493 if( vg_console.suggestions[i].lev_score <
494 vg_console.suggestions[0].lev_score/2 )
495 {
496 vg_console.suggestion_count = i;
497 return;
498 }
499 }
500 }
501
502 /*
503 * Suggestion controls
504 */
505 VG_STATIC void _console_fetch_suggestion(void)
506 {
507 char *target = &vg_console.input[ vg_console.suggestion_pastepos ];
508
509 if( vg_console.suggestion_select == -1 ){
510 strcpy( target, vg_console.input_copy );
511 _ui_textbox_move_cursor( &vg_ui.textbox.cursor_user,
512 &vg_ui.textbox.cursor_pos, 10000, 1 );
513 }
514 else{
515 strncpy( target,
516 vg_console.suggestions[ vg_console.suggestion_select ].str,
517 vg_list_size( vg_console.input )-1 );
518
519 _ui_textbox_move_cursor( &vg_ui.textbox.cursor_user,
520 &vg_ui.textbox.cursor_pos, 10000, 1 );
521 _ui_textbox_put_char( ' ' );
522 }
523 }
524
525 VG_STATIC void _console_suggest_store_normal(void)
526 {
527 if( vg_console.suggestion_select == -1 ){
528 char *target = &vg_console.input[ vg_console.suggestion_pastepos ];
529 strcpy( vg_console.input_copy, target );
530 }
531 }
532
533 VG_STATIC void _console_suggest_next(void)
534 {
535 if( vg_console.suggestion_count ){
536 _console_suggest_store_normal();
537
538 vg_console.suggestion_select ++;
539
540 if( vg_console.suggestion_select >= vg_console.suggestion_count )
541 vg_console.suggestion_select = -1;
542
543 _console_fetch_suggestion();
544 }
545 }
546
547 VG_STATIC void _console_suggest_prev(void)
548 {
549 if( vg_console.suggestion_count ){
550 _console_suggest_store_normal();
551
552 vg_console.suggestion_select --;
553
554 if( vg_console.suggestion_select < -1 )
555 vg_console.suggestion_select = vg_console.suggestion_count-1;
556
557 _console_fetch_suggestion();
558 }
559 }
560
561 VG_STATIC void _vg_console_on_update( char *buf, u32 len )
562 {
563 if( buf == vg_console.input ){
564 console_update_suggestions();
565 }
566 }
567
568 VG_STATIC void console_history_get( char* buf, int entry_num )
569 {
570 if( !vg_console.history_count )
571 return;
572
573 int offset = VG_MIN( entry_num, vg_console.history_count -1 ),
574 pick = (vg_console.history_last - offset) %
575 vg_list_size( vg_console.history );
576 strcpy( buf, vg_console.history[ pick ] );
577 }
578
579 VG_STATIC void _vg_console_on_up( char *buf, u32 len )
580 {
581 if( buf == vg_console.input ){
582 vg_console.history_pos =
583 VG_MAX
584 (
585 0,
586 VG_MIN
587 (
588 vg_console.history_pos+1,
589 VG_MIN
590 (
591 vg_list_size( vg_console.history ),
592 vg_console.history_count - 1
593 )
594 )
595 );
596
597 console_history_get( vg_console.input, vg_console.history_pos );
598 _ui_textbox_move_cursor( &vg_ui.textbox.cursor_user,
599 &vg_ui.textbox.cursor_pos,
600 vg_list_size(vg_console.input)-1, 1);
601 }
602 }
603
604 VG_STATIC void _vg_console_on_down( char *buf, u32 len )
605 {
606 if( buf == vg_console.input ){
607 vg_console.history_pos = VG_MAX( 0, vg_console.history_pos-1 );
608 console_history_get( vg_console.input, vg_console.history_pos );
609
610 _ui_textbox_move_cursor( &vg_ui.textbox.cursor_user,
611 &vg_ui.textbox.cursor_pos,
612 vg_list_size(vg_console.input)-1, 1 );
613 }
614 }
615
616 VG_STATIC void _vg_console_on_enter( char *buf, u32 len )
617 {
618 if( buf == vg_console.input ){
619 if( !strlen( vg_console.input ) )
620 return;
621
622 vg_info( "%s\n", vg_console.input );
623
624 if( strcmp( vg_console.input,
625 vg_console.history[ vg_console.history_last ]) )
626 {
627 vg_console.history_last = ( vg_console.history_last + 1) %
628 vg_list_size(vg_console.history );
629 vg_console.history_count =
630 VG_MIN( vg_list_size( vg_console.history ),
631 vg_console.history_count + 1 );
632 strcpy( vg_console.history[ vg_console.history_last ],
633 vg_console.input );
634 }
635
636 vg_console.history_pos = -1;
637 vg_execute_console_input( vg_console.input );
638 _ui_textbox_move_cursor( &vg_ui.textbox.cursor_user,
639 &vg_ui.textbox.cursor_pos, -10000, 1 );
640
641 vg_console.input[0] = '\0';
642 console_update_suggestions();
643 }
644 }
645
646 VG_STATIC void _vg_console_draw(void)
647 {
648 if( !vg_console.enabled ) return;
649
650 SDL_AtomicLock( &log_print_sl );
651
652 int ptr = vg_log.buffer_line_current;
653 int const fh = 14, log_lines = 32;
654 int console_lines = VG_MIN( log_lines, vg_log.buffer_line_count );
655
656 ui_rect rect_log = { 0, 0, vg.window_x, log_lines*fh },
657 rect_input = { 0, log_lines*fh + 1, vg.window_x, fh*2 },
658 rect_line = { 0, 0, vg.window_x, fh };
659
660 /*
661 * log
662 */
663 u32 bg_colour = (ui_colour( k_ui_bg )&0x00ffffff)|0x9f000000;
664
665 ui_fill( rect_log, bg_colour );
666 rect_line[1] = rect_log[1]+rect_log[3]-fh;
667
668 for( int i=0; i<console_lines; i ++ ){
669 ptr --;
670
671 if( ptr < 0 ) ptr = vg_list_size( vg_log.buffer )-1;
672
673 ui_text( rect_line, vg_log.buffer[ptr], 1, k_ui_align_left, 0 );
674 rect_line[1] -= fh;
675 }
676
677 /*
678 * Input area
679 */
680
681 struct ui_textbox_callbacks callbacks = {
682 .up = _vg_console_on_up,
683 .down = _vg_console_on_down,
684 .change = _vg_console_on_update,
685 .enter = _vg_console_on_enter
686 };
687 ui_textbox( rect_input, vg_console.input, vg_list_size(vg_console.input),
688 UI_TEXTBOX_AUTOFOCUS, &callbacks );
689
690 /*
691 * suggestions
692 */
693 if( vg_console.suggestion_count ){
694 ui_rect rect_suggest;
695 rect_copy( rect_input, rect_suggest );
696
697 rect_suggest[0] += 6 + UI_GLYPH_SPACING_X*vg_console.suggestion_pastepos;
698 rect_suggest[1] += rect_input[3];
699 rect_suggest[2] = UI_GLYPH_SPACING_X * vg_console.suggestion_maxlen;
700 rect_suggest[3] = vg_console.suggestion_count * fh;
701
702 ui_fill( rect_suggest, bg_colour );
703
704 rect_suggest[3] = fh;
705
706 for( int i=0; i<vg_console.suggestion_count; i ++ ){
707 u32 text_colour;
708 if( i == vg_console.suggestion_select ){
709 ui_fill( rect_suggest, ui_colour( k_ui_orange ) );
710 text_colour = ui_colourcont( k_ui_orange );
711 }
712 else text_colour = ui_colourcont( k_ui_bg );
713
714 ui_text( rect_suggest, vg_console.suggestions[i].str, 1,
715 k_ui_align_left, text_colour );
716
717 rect_suggest[1] += fh;
718 }
719 }
720
721 SDL_AtomicUnlock( &log_print_sl );
722 }
723
724
725 #endif /* VG_CONSOLE_H */