#pragma once #include "../game/vars.h" #include "string.h" #include #include #include #include #include #include #include #include enum CONSOLE_ROUTE : U8 { CONSOLE_ROUTE_NONE = 0, CONSOLE_ROUTE_VIEW = 1 << 0, CONSOLE_ROUTE_STDOUT = 1 << 1, CONSOLE_ROUTE_BOTH = CONSOLE_ROUTE_VIEW | CONSOLE_ROUTE_STDOUT }; inline constexpr U64 CONSOLE_BYTES_MAX = 10 * 1024 * 1024; struct CONSOLE_DATA_RING { LIST slots{64}; U32 first{}; U32 count{}; U64 max_bytes{CONSOLE_BYTES_MAX}; U64 revision{}; U64 total_pushed{}; U64 bytes{}; static U64 line_bytes( const STR& line ) { return (U64)line.size + 1; } STR& operator[]( U32 logical_idx ) { U32 physical_idx = ( first + logical_idx ) % slots.capacity; return slots.data[physical_idx]; } const STR& operator[]( U32 logical_idx ) const { U32 physical_idx = ( first + logical_idx ) % slots.capacity; return slots.data[physical_idx]; } void grow() { U32 new_capacity = slots.capacity ? slots.capacity * 2 : 64; LIST grown{new_capacity}; for( U32 idx = 0; idx < count; ++idx ) grown.push( (*this)[idx] ); slots = grown; first = 0; } void pop_front() { if( !count ) return; STR& oldest = slots.data[first]; bytes -= line_bytes( oldest ); oldest = STR(); first = ( first + 1 ) % slots.capacity; --count; if( !count ) first = 0; } U8 make_room_for( U64 required_bytes ) { if( required_bytes > max_bytes ) return 0; U8 removed = 0; while( count && bytes > max_bytes - required_bytes ) { pop_front(); removed = 1; } if( removed ) ++revision; return bytes <= max_bytes - required_bytes; } void push( const STR& src ) { if( !max_bytes ) return; U64 src_bytes = line_bytes( src ); if( src_bytes > max_bytes ) { U32 keep = max_bytes > 1 ? (U32)( max_bytes - 1 ) : 0; keep = keep < src.size ? keep : src.size; STR clipped( keep, src.data + src.size - keep ); push( clipped ); return; } if( !make_room_for( src_bytes ) ) return; if( count == slots.capacity ) grow(); U32 tail = ( first + count ) % slots.capacity; if( tail < slots.size ) slots[tail] = src; else slots.push( src ); ++count; ++total_pushed; bytes += src_bytes; } void clear() { slots.clear(); slots.reserve( 64 ); bytes = 0; count = 0; first = 0; } }; struct CONSOLE_STDOUT_CAPTURE { I32 saved_stdout{-1}; I32 read_fd{-1}; U8 active{}; }; struct CONSOLE_DATA { CONSOLE_STDOUT_CAPTURE stdout_capture{}; CONSOLE_DATA_RING lines{}; STR partial_stdout_line{}; U8 pending_carriage{}; LIST history{}; U64 revision{}; }; struct CONSOLE_COMMAND { U8 unterminated; LIST args; }; inline U8 console_append_partial_bytes( CONSOLE_DATA* state, const char* text, U32 length ) { if( !state || !text ) return 0; if( !length ) return 1; STR& partial = state->partial_stdout_line; U64 required_bytes = (U64)partial.size + (U64)length + 1; if( !state->lines.make_room_for( required_bytes ) ) return 0; for( U32 idx = 0; idx < length; ++idx ) partial.push( text[idx] ); return 1; } inline void console_clear_partial_line( CONSOLE_DATA* state ) { if( !state ) return; STR& partial = state->partial_stdout_line; partial.size = 0; if( partial.data ) partial.data[0] = 0; } inline I32 console_clear_output( CONSOLE_DATA* state ) { if( !state ) return 0; state->lines.clear(); console_clear_partial_line( state ); state->pending_carriage = 0; ++state->revision; return 1; } extern I32 console_clear_cb( CVAR* self, LIST cmdline ); inline void console_finish_partial_line( CONSOLE_DATA* state ) { if( !state ) return; state->lines.push( state->partial_stdout_line ); console_clear_partial_line( state ); } inline STR console_command_token( const char* text, U32 start, U32 end ) { STR token; token.reserve( end - start + 1 ); for( U32 idx = start; idx < end; ++idx ) { if( text[idx] == '"' ) continue; token.push( text[idx] ); } return token; } inline void console_parse_command( const char* text, CONSOLE_COMMAND* command ) { if( !command ) return; command->args.clear(); command->unterminated = 0; if( !text ) return; U32 token_start = 0; U8 in_string = 0; U32 cursor = 0; while( text[cursor] ) { char c = text[cursor]; if( c == '\"' ) in_string ^= 1; if( c == ' ' && !in_string ) { if( cursor > token_start ) { command->args.push( console_command_token( text, token_start, cursor ) ); } do { ++cursor; } while( text[cursor] == ' ' ); token_start = cursor; continue; } ++cursor; } if( cursor > token_start ) { command->args.push( console_command_token( text, token_start, cursor ) ); } command->unterminated = in_string; } inline void console_append_text( CONSOLE_DATA* state, const char* text, U32 len ) { if( !state || !text || !len ) return; ++state->revision; for( U32 idx = 0; idx < len; ++idx ) { char c = text[idx]; if( state->pending_carriage ) { state->pending_carriage = 0; if( c == '\n' ) { console_finish_partial_line( state ); continue; } console_clear_partial_line( state ); } if( c == '\r' ) { state->pending_carriage = 1; continue; } if( c == '\n' ) { console_finish_partial_line( state ); continue; } if( c == '\t' ) { console_append_partial_bytes( state, " ", 2 ); continue; } console_append_partial_bytes( state, &c, 1 ); } } inline void console_write_fd( I32 fd, const char* text, U32 len ) { if( fd < 0 || !text ) return; U32 written = 0; while( written < len ) { ssize_t result = write( fd, text + written, len - written ); if( result > 0 ) { written += (U32)result; continue; } if( result < 0 && errno == EINTR ) continue; break; } } inline void console_write_original_stdout( CONSOLE_DATA* state, const char* text, U32 len ) { I32 fd = state && state->stdout_capture.active ? state->stdout_capture.saved_stdout : STDOUT_FILENO; console_write_fd( fd, text, len ); } inline void console_write( CONSOLE_DATA* state, U8 routes, const char* text, U32 len ) { if( !text || !len ) return; if( routes & CONSOLE_ROUTE_VIEW ) console_append_text( state, text, len ); if( routes & CONSOLE_ROUTE_STDOUT ) console_write_original_stdout( state, text, len ); } inline void console_print( CONSOLE_DATA* state, U8 routes, const char* fmt, ... ) { if( !state || !fmt ) return; va_list args; va_list count_args; va_start( args, fmt ); va_copy( count_args, args ); I32 len = vsnprintf( 0, 0, fmt, count_args ); va_end( count_args ); if( len < 0 ) { va_end( args ); return; } STR text; text.reserve( (U32)len + 1 ); vsnprintf( text.data, (U32)len + 1, fmt, args ); va_end( args ); text.size = (U32)len; text.data[text.size] = 0; console_write( state, routes, text.data, text.size ); } inline U8 console_parse_i32( const STR& text, I32* output ) { if( !output || !text.data || !text.size ) return 0; char* end = 0; errno = 0; long parsed = strtol( text.data, &end, 10 ); if( errno == ERANGE || end == text.data || *end || parsed < INT_MIN || parsed > INT_MAX ) return 0; *output = (I32)parsed; return 1; } inline U8 console_parse_f32( const STR& text, F32* output ) { if( !output || !text.data || !text.size ) return 0; char* end = 0; errno = 0; F32 parsed = strtof( text.data, &end ); if( errno == ERANGE || end == text.data || *end || !isfinite( parsed ) ) return 0; *output = parsed; return 1; } inline U8 console_parse_color_component( const STR& text, F32* output ) { I32 integer = 0; if( console_parse_i32( text, &integer ) ) { if( integer < 0 || integer > 255 ) return 0; *output = (F32)integer / 255.f; return 1; } F32 decimal = 0.f; if( !console_parse_f32( text, &decimal ) || decimal < 0.f || decimal > 1.f ) return 0; *output = decimal; return 1; } inline U8 console_execute_cvar( CONSOLE_DATA* state, CVAR* cvar, LIST& args ) { if( cvar->type != CVAR_TYPE_FN && args.size == 0 ) return 0; switch( cvar->type ) { case CVAR_TYPE_FN: { if( var_call( cvar, args ) ) return 1; console_print( state, CONSOLE_ROUTE_BOTH, "%s", "[error] failed to run cvar\n" ); return 0; } case CVAR_TYPE_INT: { I32 value = 0; if( args.size != 1 || !console_parse_i32( args[0], &value ) ) break; var_set( cvar, value ); return 1; } case CVAR_TYPE_FLOAT: { F32 value = 0.f; if( args.size != 1 || !console_parse_f32( args[0], &value ) ) break; var_set( cvar, value ); return 1; } break; case CVAR_TYPE_COLOR: { if( args.size == 1 ) { F32 value = 0.f; if( !console_parse_color_component( args[0], &value ) ) break; var_set( cvar, value ); return 1; } if( args.size != 3 && args.size != 4 ) break; F32 components[4] = { cvar->clr_v.r, cvar->clr_v.g, cvar->clr_v.b, cvar->clr_v.a }; for( U32 idx = 0; idx < args.size; ++idx ) { if( !console_parse_color_component( args[idx], &components[idx] ) ) break; if( idx + 1 != args.size ) continue; var_set( cvar, CLR( components[0], components[1], components[2], components[3] ) ); return 1; } } break; case CVAR_TYPE_STRING: { if( args.size != 1 || args[0].size >= sizeof( cvar->str_v ) ) break; var_set( cvar, args[0] ); return 1; } default: { console_print( state, CONSOLE_ROUTE_BOTH, "%s", "[error] unhandled cvar type\n" ); return 0; } } console_print( state, CONSOLE_ROUTE_BOTH, "[error] failed to set %s\n", cvar->name.data ); return 0; } inline U8 console_print_i32_help( CONSOLE_DATA* state, CVAR* cvar ) { if( !state || !cvar || cvar->type != CVAR_TYPE_INT ) return 0; if( cvar->flags & CVAR_FLAG_MINMAX ) { console_print( state, CONSOLE_ROUTE_BOTH, "%s - %s. default(%d) current(%d) min(%d) max(%d)\n", cvar->name.data, cvar->desc.data, cvar->default_v.i_v, cvar->i_v, cvar->min_v.i_v, cvar->max_v.i_v ); } else { console_print( state, CONSOLE_ROUTE_BOTH, "%s - %s. default(%d) current(%d)\n", cvar->name.data, cvar->desc.data, cvar->default_v.i_v, cvar->i_v ); } return 1; } inline U8 console_print_f32_help( CONSOLE_DATA* state, CVAR* cvar ) { if( !state || !cvar || cvar->type != CVAR_TYPE_FLOAT ) return 0; if( cvar->flags & CVAR_FLAG_MINMAX ) { console_print( state, CONSOLE_ROUTE_BOTH, "%s - %s. default(%f) current(%f) min(%f) max(%f)\n", cvar->name.data, cvar->desc.data, cvar->default_v.fl_v, cvar->fl_v, cvar->min_v.fl_v, cvar->max_v.fl_v ); } else { console_print( state, CONSOLE_ROUTE_BOTH, "%s - %s. default(%f) current(%f)\n", cvar->name.data, cvar->desc.data, cvar->default_v.fl_v, cvar->fl_v ); } return 1; } inline U8 console_print_cvar_help( CONSOLE_DATA* state, CVAR* cvar ) { switch( cvar->type ) { case CVAR_TYPE_FN: return 1; case CVAR_TYPE_INT: return console_print_i32_help( state, cvar ); case CVAR_TYPE_FLOAT: return console_print_f32_help( state, cvar ); case CVAR_TYPE_COLOR: { console_print( state, CONSOLE_ROUTE_BOTH, "%s - %s. default(%f %f %f %f) current(%f %f %f %f)\n", cvar->name.data, cvar->desc.data, cvar->default_v.clr_v.r, cvar->default_v.clr_v.g, cvar->default_v.clr_v.b, cvar->default_v.clr_v.a, cvar->clr_v.r, cvar->clr_v.g, cvar->clr_v.b, cvar->clr_v.a ); } break; case CVAR_TYPE_STRING: { console_print( state, CONSOLE_ROUTE_BOTH, "%s - %s. default(%s) current(%s)\n", cvar->name.data, cvar->desc.data, cvar->default_v.str_v, cvar->str_v ); } break; default: { console_print( state, CONSOLE_ROUTE_BOTH, "%s", "[error] unhandled cvar type\n" ); return 0; } } return 1; } inline U8 console_execute_command( CONSOLE_DATA* state, CONSOLE_COMMAND* command ) { if( !command || !command->args.size ) return 1; if( command->unterminated ) { console_print( state, CONSOLE_ROUTE_BOTH, "%s", "[error] unterminated string\n" ); return 0; } STR command_name = command->args.pop_front(); CVAR* cvar = var_find( command_name.data ); if( !cvar ) { console_print( state, CONSOLE_ROUTE_BOTH, "[error] unknown command: %s\n", command_name.data ); return 0; } if( !command->args.size && cvar->type != CVAR_TYPE_FN ) return console_print_cvar_help( state, cvar ); return console_execute_cvar( state, cvar, command->args ); } inline U8 console_execute_segment( CONSOLE_DATA* state, const char* text, U32 length ) { if( !length ) return 1; STR segment( length, text ); CONSOLE_COMMAND command{}; console_parse_command( segment.data, &command ); return console_execute_command( state, &command ); } inline void console_execute( CONSOLE_DATA* state, const char* cmd ) { if( !state || !cmd ) return; U32 command_start = 0; U8 in_string = 0; for( U32 cursor = 0;; ++cursor ) { char c = cmd[cursor]; if( c == '"' ) in_string ^= 1; U8 at_end = !c; U8 at_separator = c == ';' && !in_string; if( !at_end && !at_separator ) continue; if( !console_execute_segment( state, cmd + command_start, cursor - command_start ) ) return; if( at_end ) return; command_start = cursor + 1; } } inline U8 console_submit( CONSOLE_DATA* state, const char* cmd ) { if( !state || !cmd || !cmd[0] ) return 0; STR submitted( "%s", cmd ); state->history.push( submitted ); console_print( state, CONSOLE_ROUTE_BOTH, "] %s\n", submitted.data ); console_execute( state, submitted.data ); return 1; } inline U8 console_capture_stdout_begin( CONSOLE_DATA* state ) { if( !state ) return 0; CONSOLE_STDOUT_CAPTURE& capture = state->stdout_capture; if( capture.active ) return 1; I32 saved_stdout = dup( STDOUT_FILENO ); if( saved_stdout < 0 ) return 0; I32 pipe_fds[2]; if( pipe( pipe_fds ) ) { close( saved_stdout ); return 0; } I32 flags = fcntl( pipe_fds[0], F_GETFL, 0 ); if( flags < 0 || fcntl( pipe_fds[0], F_SETFL, flags | O_NONBLOCK ) < 0 ) { close( pipe_fds[0] ); close( pipe_fds[1] ); close( saved_stdout ); return 0; } fflush( stdout ); setvbuf( stdout, 0, _IONBF, 0 ); if( dup2( pipe_fds[1], STDOUT_FILENO ) < 0 ) { close( pipe_fds[0] ); close( pipe_fds[1] ); close( saved_stdout ); return 0; } close( pipe_fds[1] ); capture.saved_stdout = saved_stdout; capture.read_fd = pipe_fds[0]; capture.active = 1; return 1; } inline void console_pump_stdout( CONSOLE_DATA* state ) { if( !state ) return; CONSOLE_STDOUT_CAPTURE& capture = state->stdout_capture; if( !capture.active || capture.read_fd < 0 ) return; char buffer[4096]; for( ;; ) { ssize_t count = read( capture.read_fd, buffer, sizeof( buffer ) ); if( count > 0 ) { console_write_fd( capture.saved_stdout, buffer, (U32)count ); console_append_text( state, buffer, (U32)count ); continue; } if( count < 0 && errno == EINTR ) continue; if( count < 0 && ( errno == EAGAIN || errno == EWOULDBLOCK ) ) break; break; } } inline void console_capture_stdout_end( CONSOLE_DATA* state ) { if( !state ) return; CONSOLE_STDOUT_CAPTURE& capture = state->stdout_capture; if( !capture.active ) return; fflush( stdout ); dup2( capture.saved_stdout, STDOUT_FILENO ); console_pump_stdout( state ); close( capture.read_fd ); close( capture.saved_stdout ); capture.read_fd = capture.saved_stdout = -1; capture.active = 0; }