diff --git a/cmake/targets/linux.cmake b/cmake/targets/linux.cmake index 172736bd..197e9612 100644 --- a/cmake/targets/linux.cmake +++ b/cmake/targets/linux.cmake @@ -30,6 +30,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_SDL2 DUSK_OPENGL + DUSK_CONSOLE_POSIX # DUSK_OPENGL_LEGACY DUSK_LINUX DUSK_DISPLAY_SIZE_DYNAMIC diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index a4192a65..0b879e61 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -61,8 +61,9 @@ target_sources(${DUSK_BINARY_TARGET_NAME} # Subdirs add_subdirectory(assert) add_subdirectory(asset) -add_subdirectory(log) +add_subdirectory(console) add_subdirectory(display) +add_subdirectory(log) add_subdirectory(engine) add_subdirectory(entity) add_subdirectory(error) @@ -77,4 +78,4 @@ add_subdirectory(time) add_subdirectory(ui) add_subdirectory(network) add_subdirectory(util) -# add_subdirectory(thread) \ No newline at end of file +add_subdirectory(thread) \ No newline at end of file diff --git a/src/dusk/console/CMakeLists.txt b/src/dusk/console/CMakeLists.txt new file mode 100644 index 00000000..42ade831 --- /dev/null +++ b/src/dusk/console/CMakeLists.txt @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + console.c + consolecmd.c + consolevar.c +) + +# Subdirectories +add_subdirectory(cmd) \ No newline at end of file diff --git a/src/dusk/console/cmd/CMakeLists.txt b/src/dusk/console/cmd/CMakeLists.txt new file mode 100644 index 00000000..57c6ef05 --- /dev/null +++ b/src/dusk/console/cmd/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2025 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC +) \ No newline at end of file diff --git a/src/dusk/console/cmd/cmdalias.h b/src/dusk/console/cmd/cmdalias.h new file mode 100644 index 00000000..01380b28 --- /dev/null +++ b/src/dusk/console/cmd/cmdalias.h @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "console/console.h" +#include "input/input.h" +#include "util/string.h" + +void cmdAlias(const consolecmdexec_t *exec) { + if(exec->argc < 1) { + consolePrint("Expected 1 argument: "); + return; + } + + if(exec->argc == 1 || strlen(exec->argv[1]) == 0) { + // Removing the alias. + for(uint32_t i = 0; i < CONSOLE.aliasCount; i++) { + if(stringCompare(CONSOLE.aliases[i].alias, exec->argv[0]) != 0) { + continue; + } + + // Move all the later aliases down one. + if(CONSOLE.aliasCount - i - 1 > 0) { + memoryMove( + &CONSOLE.aliases[i], + &CONSOLE.aliases[i + 1], + (CONSOLE.aliasCount - i - 1) * sizeof(consolealias_t) + ); + } + CONSOLE.aliasCount--; + return; + } + + // Alias not found. + return; + } + + // Add alias + if(CONSOLE.aliasCount >= CONSOLE_ALIAS_MAX) { + consolePrint("Max aliases reached"); + return; + } + + // Create alias + consolealias_t *alias = &CONSOLE.aliases[CONSOLE.aliasCount++]; + stringCopy(alias->alias, exec->argv[0], CONSOLE_LINE_MAX); + stringCopy(alias->command, exec->argv[1], CONSOLE_LINE_MAX); + consolePrint("Added alias \"%s\" -> \"%s\"", exec->argv[0], exec->argv[1]); +} \ No newline at end of file diff --git a/src/dusk/console/cmd/cmdbind.h b/src/dusk/console/cmd/cmdbind.h new file mode 100644 index 00000000..5eafd5ce --- /dev/null +++ b/src/dusk/console/cmd/cmdbind.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "console/console.h" +#include "input/input.h" + +void cmdBind(const consolecmdexec_t *exec) { + if(exec->argc < 1) { + consolePrint("Expected 1 argument: argc == 1) { + consolePrint("TODO: Show binds"); + return; + } + + inputbutton_t button = inputButtonGetByName(exec->argv[0]); + if(button.type == INPUT_BUTTON_TYPE_NONE) { + consolePrint("Unknown button \"%s\"", exec->argv[0]); + return; + } + + inputBind(button, exec->argv[1]); +} \ No newline at end of file diff --git a/src/dusk/console/cmd/cmdecho.h b/src/dusk/console/cmd/cmdecho.h new file mode 100644 index 00000000..6bcd1593 --- /dev/null +++ b/src/dusk/console/cmd/cmdecho.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "console/console.h" + +void cmdEcho(const consolecmdexec_t *exec) { + if(exec->argc < 1) { + consolePrint("Expected 1 argument: "); + return; + } + + consolePrint("%s", exec->argv[0]); +} \ No newline at end of file diff --git a/src/dusk/console/cmd/cmdget.h b/src/dusk/console/cmd/cmdget.h new file mode 100644 index 00000000..6b6cb138 --- /dev/null +++ b/src/dusk/console/cmd/cmdget.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "console/console.h" + +void cmdGet(const consolecmdexec_t *exec) { + assertTrue( + exec->argc >= 1, + "Get command requires 1 argument." + ); + + for(uint32_t i = 0; i < CONSOLE.variableCount; i++) { + consolevar_t *var = &CONSOLE.variables[i]; + if(stringCompare(var->name, exec->argv[0]) != 0) continue; + consolePrint("%s", var->value); + return; + } + + consolePrint("Error: Variable '%s' not found.", exec->argv[0]); +} \ No newline at end of file diff --git a/src/dusk/console/cmd/cmdlist.h b/src/dusk/console/cmd/cmdlist.h new file mode 100644 index 00000000..a87d7afe --- /dev/null +++ b/src/dusk/console/cmd/cmdlist.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +// #include "console/cmd/cmdquit.h" +#include "console/cmd/cmdecho.h" +#include "console/cmd/cmdset.h" +#include "console/cmd/cmdget.h" +// #include "console/cmd/cmdbind.h" +// #include "console/cmd/cmdtoggleconsole.h" +// #include "console/cmd/cmdalias.h" + +X(ECHO, "echo", cmdEcho) +X(SET, "set", cmdSet) +X(GET, "get", cmdGet) \ No newline at end of file diff --git a/src/dusk/console/cmd/cmdquit.h b/src/dusk/console/cmd/cmdquit.h new file mode 100644 index 00000000..d1e3474d --- /dev/null +++ b/src/dusk/console/cmd/cmdquit.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "console/console.h" +#include "engine/engine.h" + +void cmdQuit(const consolecmdexec_t *exec) { + ENGINE.running = false; +} \ No newline at end of file diff --git a/src/dusk/console/cmd/cmdset.h b/src/dusk/console/cmd/cmdset.h new file mode 100644 index 00000000..64816452 --- /dev/null +++ b/src/dusk/console/cmd/cmdset.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "console/console.h" + +void cmdSet(const consolecmdexec_t *exec) { + assertTrue(exec->argc >= 2, "set command requires 2 arguments."); + + for(uint32_t i = 0; i < CONSOLE.variableCount; i++) { + consolevar_t *var = &CONSOLE.variables[i]; + if(stringCompare(var->name, exec->argv[0]) != 0) continue; + consoleVarSetValue(var, exec->argv[1]); + // consolePrint("%s %s", var->name, var->value); + for(i = 0; i < var->eventCount; i++) { + assertNotNull(var->events[i], "Event is NULL"); + var->events[i](var); + } + return; + } + + consolePrint("Error: Variable '%s' not found.", exec->argv[0]); +} \ No newline at end of file diff --git a/src/dusk/console/cmd/cmdtoggleconsole.h b/src/dusk/console/cmd/cmdtoggleconsole.h new file mode 100644 index 00000000..af366cd9 --- /dev/null +++ b/src/dusk/console/cmd/cmdtoggleconsole.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "console/console.h" + +void cmdToggleConsole(const consolecmdexec_t *exec) { + CONSOLE.visible = !CONSOLE.visible; +} \ No newline at end of file diff --git a/src/dusk/console/console.c b/src/dusk/console/console.c new file mode 100644 index 00000000..dc5d20a2 --- /dev/null +++ b/src/dusk/console/console.c @@ -0,0 +1,455 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "console.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" +#include "input/input.h" +#include "log/log.h" +#include "engine/engine.h" +#include "console/cmd/cmdset.h" +#include "console/cmd/cmdget.h" + +#include "display/shader/shaderunlit.h" +#include "display/text/text.h" +#include "display/spritebatch/spritebatch.h" + +console_t CONSOLE; + +void consoleInit() { + memoryZero(&CONSOLE, sizeof(console_t)); + + // Register vars + consoleRegVar("fps", "0", NULL); + + // Register cmds + CONSOLE.cmdGet = consoleRegCmd("get", cmdGet); + CONSOLE.cmdSet = consoleRegCmd("set", cmdSet); + + #define X(unused, command, function) \ + consoleRegCmd(command, function); + #include "console/cmd/cmdlist.h" + #undef X + + #ifdef DUSK_CONSOLE_POSIX + threadInit(&CONSOLE.thread, consoleInputThread); + threadMutexInit(&CONSOLE.execMutex); + threadStartRequest(&CONSOLE.thread); + #endif +} + +consolecmd_t * consoleRegCmd(const char_t *name, consolecmdfunc_t function) { + consolecmd_t *cmd = &CONSOLE.commands[CONSOLE.commandCount++]; + consoleCmdInit(cmd, name, function); + return cmd; +} + +consolevar_t * consoleRegVar( + const char_t *name, + const char_t *value, + consolevarchanged_t event +) { + consolevar_t *var; + + // Existing? + var = consoleVarGet(name); + if(var != NULL) return var; + + assertTrue( + CONSOLE.variableCount < CONSOLE_VARIABLES_MAX, + "Too many console variables registered." + ); + + // Create + var = &CONSOLE.variables[CONSOLE.variableCount++]; + consoleVarInitListener(var, name, value, event); + return var; +} + +consolevar_t * consoleVarGet(const char_t *name) { + assertNotNull(name, "name must not be NULL"); + for(uint32_t i = 0; i < CONSOLE.variableCount; i++) { + consolevar_t *var = &CONSOLE.variables[i]; + if(stringCompare(var->name, name) == 0) return var; + } + return NULL; +} + +void consolePrint(const char_t *message, ...) { + char_t buffer[CONSOLE_LINE_MAX]; + + va_list args; + va_start(args, message); + int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args); + va_end(args); + + // Move all lines back + memoryMove( + CONSOLE.line[0], + CONSOLE.line[1], + (CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX + ); + + // Copy the new line + memoryCopy( + CONSOLE.line[CONSOLE_HISTORY_MAX - 1], + buffer, + len + 1 + ); + logDebug("%s", buffer); +} + +void consoleExec(const char_t *line) { + #ifdef DUSK_CONSOLE_POSIX + threadMutexLock(&CONSOLE.execMutex); + #endif + + assertNotNull(line, "line must not be NULL"); + assertTrue( + CONSOLE.execBufferCount < CONSOLE_EXEC_BUFFER_MAX, + "Too many commands in the buffer." + ); + + char_t buffer[CONSOLE_LINE_MAX]; + size_t i = 0, j = 0; + char_t c; + consoleexecstate_t state = CONSOLE_EXEC_STATE_INITIAL; + consolecmdexec_t *exec = NULL; + + while(state != CONSOLE_EXEC_STATE_FULLY_PARSED) { + c = line[i]; + + switch(state) { + case CONSOLE_EXEC_STATE_INITIAL: + assertTrue(j == 0, "Buffer not empty?"); + + if(c == '\0') { + state = CONSOLE_EXEC_STATE_FULLY_PARSED; + break; + } + + if(stringIsWhitespace(c) || c == ';') { + i++; + continue; + } + + state = CONSOLE_EXEC_STATE_PARSE_CMD; + break; + + case CONSOLE_EXEC_STATE_PARSE_CMD: + if(stringIsWhitespace(c) || c == '\0' || c == ';') { + state = CONSOLE_EXEC_STATE_CMD_PARSED; + continue; + } + + if(c == '"') { + // Can't handle quotes within the command. + consolePrint("Invalid command"); + while(c != '\0' && c != ';') c = line[++i]; + continue; + } + + buffer[j++] = c; + i++; + + if(j >= CONSOLE_LINE_MAX) { + consolePrint("Command too long"); + state = CONSOLE_EXEC_STATE_FULLY_PARSED; + continue; + } + break; + + case CONSOLE_EXEC_STATE_CMD_PARSED: + if(j == 0) { + state = CONSOLE_EXEC_STATE_INITIAL; + continue; + } + + // Create exec + assertNull(exec, "Existing command parsing?"); + + exec = &CONSOLE.execBuffer[CONSOLE.execBufferCount]; + memoryZero(exec, sizeof(consolecmdexec_t)); + + buffer[j] = '\0'; + stringCopy(exec->command, buffer, CONSOLE_LINE_MAX); + state = CONSOLE_EXEC_STATE_FIND_ARG; + + j = 0;// Free up buffer + break; + + case CONSOLE_EXEC_STATE_FIND_ARG: + if(c == '\0' || c == ';') { + state = CONSOLE_EXEC_STATE_CMD_FINISHED; + continue; + } + + if(stringIsWhitespace(c)) { + i++; + continue; + } + + if(c == '"') { + state = CONSOLE_EXEC_STATE_PARSE_ARG_QUOTED; + i++; + } else { + state = CONSOLE_EXEC_STATE_PARSE_ARG; + } + break; + + case CONSOLE_EXEC_STATE_PARSE_ARG: + if(stringIsWhitespace(c) || c == '\0' || c == ';') { + state = CONSOLE_EXEC_STATE_ARG_PARSED; + continue; + } + + buffer[j++] = c; + i++; + + if(j >= CONSOLE_LINE_MAX) { + consolePrint("Arg too long"); + state = CONSOLE_EXEC_STATE_FULLY_PARSED; + continue; + } + break; + + case CONSOLE_EXEC_STATE_PARSE_ARG_QUOTED: + if(c == '"') { + state = CONSOLE_EXEC_STATE_ARG_PARSED; + i++; + continue; + } + + if(c == '\0' || c == ';') { + consolePrint("Unterminated quote"); + state = CONSOLE_EXEC_STATE_FULLY_PARSED; + continue; + } + + if(c == '\\') { + c = line[++i]; + + if(c == '\0' || c == ';') { + consolePrint("Unterminated quote"); + state = CONSOLE_EXEC_STATE_FULLY_PARSED; + continue; + } + } + + buffer[j++] = c; + i++; + + if(j >= CONSOLE_LINE_MAX) { + consolePrint("Arg too long"); + state = CONSOLE_EXEC_STATE_FULLY_PARSED; + continue; + } + break; + + case CONSOLE_EXEC_STATE_ARG_PARSED: + buffer[j] = '\0'; + stringCopy(exec->argv[exec->argc++], buffer, CONSOLE_LINE_MAX); + state = CONSOLE_EXEC_STATE_FIND_ARG; + j = 0;// Free up buffer + break; + + case CONSOLE_EXEC_STATE_CMD_FINISHED: + assertNotNull(exec, "No command found?"); + + // Now, is there a command that matches? + for(uint32_t k = 0; k < CONSOLE.commandCount; k++) { + consolecmd_t *cmd = &CONSOLE.commands[k]; + if(stringCompare(cmd->name, exec->command) != 0) continue; + exec->cmd = cmd; + break; + } + + if(exec->cmd == NULL) { + // Command wasn't found, is there a variable that matches? + for(uint32_t k = 0; k < CONSOLE.variableCount; k++) { + consolevar_t *var = &CONSOLE.variables[k]; + if(stringCompare(var->name, exec->command) != 0) continue; + + // Matching variable found, is this a GET or a SET? + if(exec->argc == 0) { + exec->cmd = CONSOLE.cmdGet; + stringCopy(exec->argv[0], exec->command, CONSOLE_LINE_MAX); + exec->argc = 1; + } else { + exec->cmd = CONSOLE.cmdSet; + stringCopy(exec->argv[1], exec->argv[0], CONSOLE_LINE_MAX); + stringCopy(exec->argv[0], exec->command, CONSOLE_LINE_MAX); + exec->argc = 2; + } + break; + } + + // Variable not found, is there an alias that matches? + bool_t aliasFound = false; + for(uint32_t k = 0; k < CONSOLE.aliasCount; k++) { + consolealias_t *alias = &CONSOLE.aliases[k]; + if(stringCompare(alias->alias, exec->command) != 0) continue; + + // Matching alias found, we unlock the mutex and recursively call + // consoleExec to handle the alias command. + #ifdef DUSK_CONSOLE_POSIX + threadMutexUnlock(&CONSOLE.execMutex); + #endif + consoleExec(alias->command); + #ifdef DUSK_CONSOLE_POSIX + threadMutexLock(&CONSOLE.execMutex); + #endif + + aliasFound = true; + break; + } + + if(!aliasFound && exec->cmd == NULL) { + consolePrint("Command \"%s\" not found", exec->command); + exec = NULL; + state = CONSOLE_EXEC_STATE_INITIAL; + break; + } + } + + // Prep for next command. + exec = NULL; + state = CONSOLE_EXEC_STATE_INITIAL; + CONSOLE.execBufferCount++; + break; + + default: + assertUnreachable("Invalid state."); + break; + } + } + + #ifdef DUSK_CONSOLE_POSIX + threadMutexUnlock(&CONSOLE.execMutex); + #endif +} + +void consoleUpdate() { + #ifdef DUSK_CONSOLE_POSIX + threadMutexLock(&CONSOLE.execMutex); + #endif + + // Toggle console + // if(inputPressed(INPUT_ACTION_CONSOLE)) { + // CONSOLE.visible = !CONSOLE.visible; + // } + + // Anything to exec? + if(CONSOLE.execBufferCount == 0) { + #ifdef DUSK_CONSOLE_POSIX + threadMutexUnlock(&CONSOLE.execMutex); + #endif + return; + } + + // Copy the exec buffer, this allows exec command to work + consolecmdexec_t execBuffer[CONSOLE_EXEC_BUFFER_MAX]; + uint32_t execBufferCount = CONSOLE.execBufferCount; + memoryCopy( + execBuffer, + CONSOLE.execBuffer, + sizeof(consolecmdexec_t) * execBufferCount + ); + + // Clear the exec buffer and unlock so new commands can be added while we + // process the current ones. + CONSOLE.execBufferCount = 0; + #ifdef DUSK_CONSOLE_POSIX + threadMutexUnlock(&CONSOLE.execMutex); + #endif + + // Exec pending buffer. + for(uint32_t i = 0; i < execBufferCount; i++) { + consolecmdexec_t *exec = &execBuffer[i]; + assertNotNull(exec->cmd, "Command execution has no command."); + exec->cmd->function(exec); + } +} + +errorret_t consoleDraw() { + if(!CONSOLE.visible) { + errorOk(); + } + + errorChain(shaderSetTexture( + &SHADER_UNLIT, SHADER_UNLIT_TEXTURE, &DEFAULT_FONT_TEXTURE + )); + errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, COLOR_WHITE)); + errorChain(textDraw( + 32, 32, + "Hello World", + COLOR_WHITE, + &DEFAULT_FONT_TILESET, + &DEFAULT_FONT_TEXTURE + )); + errorChain(spriteBatchFlush()); + errorOk(); +} + +void consoleDispose(void) { + #ifdef DUSK_CONSOLE_POSIX + threadStop(&CONSOLE.thread); + threadMutexDispose(&CONSOLE.execMutex); + #endif +} + +#ifdef DUSK_CONSOLE_POSIX + void consoleInputThread(thread_t *thread) { + assertNotNull(thread, "Thread cannot be NULL."); + + char_t line[CONSOLE_LINE_MAX]; + size_t cap = 0; + + struct pollfd pfd = { + .fd = STDIN_FILENO, + .events = POLLIN + }; + + while(!threadShouldStop(thread) && ENGINE.running) { + int32_t rc = poll(&pfd, 1, CONSOLE_POSIX_POLL_RATE); + + if(rc == 0) continue; + if(rc < 0) { + if(errno == EINTR) continue; // Interrupted by signal, retry + assertUnreachable("poll() failed with unexpected error."); + } + + // Check for errors or input + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) break; + if (!(pfd.revents & POLLIN)) { + pfd.revents = 0; + continue; + } + + // Read a line from stdin + if(!fgets(line, CONSOLE_LINE_MAX, stdin)) { + if (feof(stdin)) break; + clearerr(stdin); + continue; + } + + // Did we read a full line or did it get truncated? + size_t len = strlen(line); + int32_t fullLine = len > 0 && line[len - 1] == '\n'; + + // Strip trailing newline/CR + while(len && (line[len - 1] == '\n' || line[len - 1] == '\r')) { + line[--len] = '\0'; + } + + if(len > 0) consoleExec(line); + + pfd.revents = 0; + } + } +#endif \ No newline at end of file diff --git a/src/dusk/console/console.h b/src/dusk/console/console.h new file mode 100644 index 00000000..c6cc6538 --- /dev/null +++ b/src/dusk/console/console.h @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "consolevar.h" +#include "consolecmd.h" +#include "consolealias.h" +#include "error/error.h" + +#ifdef DUSK_CONSOLE_POSIX + #include "thread/thread.h" + #include + #include + #define CONSOLE_POSIX_POLL_RATE 75 +#endif + +typedef enum { + CONSOLE_EXEC_STATE_INITIAL, + CONSOLE_EXEC_STATE_PARSE_CMD, + CONSOLE_EXEC_STATE_CMD_PARSED, + + CONSOLE_EXEC_STATE_FIND_ARG, + CONSOLE_EXEC_STATE_PARSE_ARG, + CONSOLE_EXEC_STATE_PARSE_ARG_QUOTED, + CONSOLE_EXEC_STATE_ARG_PARSED, + + CONSOLE_EXEC_STATE_CMD_FINISHED, + CONSOLE_EXEC_STATE_FULLY_PARSED +} consoleexecstate_t; + +typedef struct { + consolecmd_t commands[CONSOLE_COMMANDS_MAX]; + uint32_t commandCount; + + consolevar_t variables[CONSOLE_VARIABLES_MAX]; + uint32_t variableCount; + + char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX]; + + consolecmdexec_t execBuffer[CONSOLE_EXEC_BUFFER_MAX]; + uint32_t execBufferCount; + + consolealias_t aliases[CONSOLE_ALIAS_MAX]; + uint32_t aliasCount; + + consolecmd_t *cmdGet; + consolecmd_t *cmdSet; + + bool_t visible; + + #ifdef DUSK_CONSOLE_POSIX + char_t inputBuffer[CONSOLE_LINE_MAX]; + thread_t thread; + threadmutex_t execMutex; + #endif +} console_t; + +extern console_t CONSOLE; + +/** + * Initializes the console. + */ +void consoleInit(); + +/** + * Registers a console command. + * + * @param name The name of the command. + * @param function The function to execute when the command is called. + * @return The registered command. + */ +consolecmd_t * consoleRegCmd(const char_t *name, consolecmdfunc_t function); + +/** + * Registers a console variable. + * + * @param name The name of the variable. + * @param value The initial value of the variable. + * @param event The event to register. + * @return The registered variable. + */ +consolevar_t * consoleRegVar( + const char_t *name, + const char_t *value, + consolevarchanged_t event +); + +/** + * Gets a console variable by name. + * + * @param name The name of the variable. + * @return The variable, or NULL if not found. + */ +consolevar_t * consoleVarGet(const char_t *name); + +/** + * Prints a message to the console. + * + * @param message The message to print. + */ +void consolePrint( + const char_t *message, + ... +); + +/** + * Executes a console command. This method is thread safe and can be called from + * any thread. + * + * @param line The line to execute. + */ +void consoleExec(const char_t *line); + +/** + * Processes the console's pending commands. + */ +void consoleUpdate(); + +/** + * Renders the console to the screen. This is in UI space. + * + * @return The error return value. + */ +errorret_t consoleDraw(); + +/** + * Disposes of the console. + */ +void consoleDispose(void); + +#ifdef DUSK_CONSOLE_POSIX + /** + * Input thread handler for posix input. + * + * @param thread The thread that is running. + */ + void consoleInputThread(thread_t *thread); +#endif \ No newline at end of file diff --git a/src/dusk/console/consolealias.h b/src/dusk/console/consolealias.h new file mode 100644 index 00000000..39aba099 --- /dev/null +++ b/src/dusk/console/consolealias.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "consolecmd.h" + +typedef struct { + char_t command[CONSOLE_LINE_MAX]; + char_t alias[CONSOLE_LINE_MAX]; +} consolealias_t; \ No newline at end of file diff --git a/src/dusk/console/consolecmd.c b/src/dusk/console/consolecmd.c new file mode 100644 index 00000000..f2923b9e --- /dev/null +++ b/src/dusk/console/consolecmd.c @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "consolecmd.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" + +void consoleCmdInit( + consolecmd_t *cmd, + const char_t *name, + consolecmdfunc_t function +) { + assertNotNull(cmd, "Command is NULL."); + assertNotNull(name, "Name is NULL."); + assertNotNull(function, "Function is NULL."); + assertStrLenMin(name, 1, "Name is empty."); + assertStrLenMax(name, CONSOLE_CMD_NAME_MAX, "Name is too long."); + + memoryZero(cmd, sizeof(consolecmd_t)); + stringCopy(cmd->name, name, CONSOLE_CMD_NAME_MAX); + cmd->function = function; +} \ No newline at end of file diff --git a/src/dusk/console/consolecmd.h b/src/dusk/console/consolecmd.h new file mode 100644 index 00000000..09a3c4f3 --- /dev/null +++ b/src/dusk/console/consolecmd.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include "consoledefs.h" + +typedef struct consolecmd_s consolecmd_t; + +typedef struct { + consolecmd_t *cmd; + char_t command[CONSOLE_LINE_MAX]; + char_t argv[CONSOLE_CMD_ARGC_MAX][CONSOLE_LINE_MAX]; + uint32_t argc; +} consolecmdexec_t; + +typedef void (*consolecmdfunc_t)(const consolecmdexec_t *exec); + +typedef struct consolecmd_s { + char_t name[CONSOLE_CMD_NAME_MAX]; + consolecmdfunc_t function; +} consolecmd_t; + +/** + * Initializes a console command. + * + * @param cmd Pointer to the console command. + * @param name The name of the command. + * @param function The function to execute when the command is called. + */ +void consoleCmdInit( + consolecmd_t *cmd, + const char_t *name, + consolecmdfunc_t function +); \ No newline at end of file diff --git a/src/dusk/console/consoledefs.h b/src/dusk/console/consoledefs.h new file mode 100644 index 00000000..fc84e47a --- /dev/null +++ b/src/dusk/console/consoledefs.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once + +#define CONSOLE_CMD_NAME_MAX 32 +#define CONSOLE_CMD_ARGC_MAX 16 + +#define CONSOLE_COMMANDS_MAX 32 +#define CONSOLE_VARIABLES_MAX 64 +#define CONSOLE_LINE_MAX 128 +#define CONSOLE_HISTORY_MAX 16 +#define CONSOLE_EXEC_BUFFER_MAX 32 +#define CONSOLE_ALIAS_MAX 32 + +#define CONSOLE_VAR_NAME_MAX 32 +#define CONSOLE_VAR_VALUE_MAX 128 +#define CONSOLE_VAR_EVENTS_MAX 8 diff --git a/src/dusk/console/consolevar.c b/src/dusk/console/consolevar.c new file mode 100644 index 00000000..e126c893 --- /dev/null +++ b/src/dusk/console/consolevar.c @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "consolevar.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" + +void consoleVarInit( + consolevar_t *var, + const char_t *name, + const char_t *value +) { + assertNotNull(var, "var must not be NULL"); + assertNotNull(name, "name must not be NULL"); + assertNotNull(value, "value must not be NULL"); + + assertStrLenMin(name, 1, "name must not be empty"); + assertStrLenMax(name, CONSOLE_VAR_NAME_MAX, "name is too long"); + assertStrLenMax(value, CONSOLE_VAR_VALUE_MAX, "value is too long"); + + memoryZero(var, sizeof(consolevar_t)); + stringCopy(var->name, name, CONSOLE_VAR_NAME_MAX); + stringCopy(var->value, value, CONSOLE_VAR_VALUE_MAX); +} + +void consoleVarInitListener( + consolevar_t *var, + const char_t *name, + const char_t *value, + consolevarchanged_t event +) { + consoleVarInit(var, name, value); + if(event) consoleVarListen(var, event); +} + +void consoleVarSetValue(consolevar_t *var, const char_t *value) { + assertNotNull(var, "var must not be NULL"); + assertNotNull(value, "value must not be NULL"); + assertStrLenMax(value, CONSOLE_VAR_VALUE_MAX, "value is too long"); + + stringCopy(var->value, value, CONSOLE_VAR_VALUE_MAX); + + uint8_t i = 0; + while (i < var->eventCount) { + var->events[i](var); + i++; + } +} + +void consoleVarListen(consolevar_t *var, consolevarchanged_t event) { + assertNotNull(var, "var must not be NULL"); + assertNotNull(event, "event must not be NULL"); + assertTrue( + var->eventCount < CONSOLE_VAR_EVENTS_MAX, + "Event count is too high" + ); + var->events[var->eventCount++] = event; +} + diff --git a/src/dusk/console/consolevar.h b/src/dusk/console/consolevar.h new file mode 100644 index 00000000..af6a9df1 --- /dev/null +++ b/src/dusk/console/consolevar.h @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include "consoledefs.h" + +typedef struct consolevar_s consolevar_t; + +typedef void (*consolevarchanged_t)(const consolevar_t *var); + +typedef struct consolevar_s { + char_t name[CONSOLE_VAR_NAME_MAX]; + char_t value[CONSOLE_VAR_VALUE_MAX]; + consolevarchanged_t events[CONSOLE_VAR_EVENTS_MAX]; + uint8_t eventCount; +} consolevar_t; + +/** + * Initializes a console variable. + * + * @param var Pointer to the console variable. + * @param name The name of the variable. + * @param value The initial value of the variable. + */ +void consoleVarInit( + consolevar_t *var, + const char_t *name, + const char_t *value +); + +/** + * Initializes a console variable with a listener. + * + * @param var Pointer to the console variable. + * @param name The name of the variable. + * @param value The initial value of the variable. + * @param event The event to register. + */ +void consoleVarInitListener( + consolevar_t *var, + const char_t *name, + const char_t *value, + consolevarchanged_t event +); + +/** + * Sets the value of a console variable. + * + * @param var Pointer to the console variable. + * @param value The new value of the variable. + */ +void consoleVarSetValue(consolevar_t *var, const char_t *value); + +/** + * Registers an event to be called when the value of a console variable changes. + * + * @param var Pointer to the console variable. + * @param event The event to register. + */ +void consoleVarListen(consolevar_t *var, consolevarchanged_t event); \ No newline at end of file diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index af8ba34d..9e0f1357 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -21,76 +21,10 @@ #include "game/game.h" #include "physics/physicsmanager.h" #include "network/network.h" -#include "network/networkinfo.h" #include "system/system.h" - -#include "display/mesh/cube.h" -#include "display/mesh/plane.h" +#include "console/console.h" engine_t ENGINE; -entityid_t phBoxEnt; -componentid_t phBoxPhys; - -float_t onlineSwapTime = FLT_MAX; - -void goOnline(); -void goOffline(); - -void onNetworkConnected(void *user) { - onlineSwapTime = TIME.time + 3.0f; - - networkinfo_t info = networkGetInfo(); - if(info.type == NETWORK_TYPE_IPV4) { - sceneLog( - "Connected to network with IPv4 address: " NETWORK_INFO_FORMAT_IPV4 "\n", - info.ipv4.ip[0], info.ipv4.ip[1], info.ipv4.ip[2], info.ipv4.ip[3] - ); - #ifdef DUSK_NETWORK_IPV6 - } else if(info.type == NETWORK_TYPE_IPV6) { - sceneLog( - "Connected to network with IPv6 address: " NETWORK_INFO_FORMAT_IPV6 "\n", - info.ipv6.ip[0], info.ipv6.ip[1], info.ipv6.ip[2], info.ipv6.ip[3], - info.ipv6.ip[4], info.ipv6.ip[5], info.ipv6.ip[6], info.ipv6.ip[7], - info.ipv6.ip[8], info.ipv6.ip[9], info.ipv6.ip[10], info.ipv6.ip[11], - info.ipv6.ip[12], info.ipv6.ip[13], info.ipv6.ip[14], info.ipv6.ip[15] - ); - #endif - } - - sceneLog("Network connected, I will disconnect at: %.2f1.\n", onlineSwapTime); -} - -void onNetworkFailed(errorret_t error, void *user) { - onlineSwapTime = TIME.time + 3.0f; - sceneLog("Failed to connect to network, will try again at %.2f1.\n", onlineSwapTime); -} - -void onNetworkDisconnected(errorret_t error, void *user) { - onlineSwapTime = TIME.time + 3.0f; - sceneLog("Network disconnected, will go online at %.2f1.\n", onlineSwapTime); - errorCatch(errorPrint(error)); -} - -void onNetworkDisconnectFinished(void *user) { - onlineSwapTime = TIME.time + 3.0f; - sceneLog("Finished disconnecting from network, will go online at %.2f1.\n", onlineSwapTime); -} - -void goOnline() { - sceneLog("Going online...\n"); - networkRequestConnection( - onNetworkConnected, - onNetworkFailed, - onNetworkDisconnected, - NULL - ); -} - -void goOffline() { - sceneLog("Going offline...\n"); - networkRequestDisconnection(onNetworkDisconnectFinished, NULL); -} - errorret_t engineInit(const int32_t argc, const char_t **argv) { memoryZero(&ENGINE, sizeof(engine_t)); @@ -101,6 +35,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { // Init systems. Order is important. errorChain(systemInit()); timeInit(); + consoleInit(); errorChain(inputInit()); errorChain(assetInit()); errorChain(localeManagerInit()); @@ -110,55 +45,9 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(sceneInit()); entityManagerInit(); physicsManagerInit(); - // errorChain(networkInit()); + errorChain(networkInit()); errorChain(gameInit()); - - sceneLog("Init done, going to queue online in 3 seconds...\n"); - onlineSwapTime = TIME.time + 3.0f; - - // Camera - entityid_t cam = entityManagerAdd(); - componentid_t camPos = entityAddComponent(cam, COMPONENT_TYPE_POSITION); - float_t distance = 6.0f; - entityPositionLookAt( - cam, camPos, - (vec3){ 0.0f, 1.0f, 0.0f }, - (vec3){ 0.0f, 1.0f, 0.0f }, - (vec3){ distance, distance, distance } - ); - componentid_t camCam = entityAddComponent(cam, COMPONENT_TYPE_CAMERA); - entityCameraSetZFar(cam, camCam, 100.0f); - - // Floor - entityid_t floorEnt = entityManagerAdd(); - componentid_t floorPos = entityAddComponent(floorEnt, COMPONENT_TYPE_POSITION); - componentid_t floorMesh = entityAddComponent(floorEnt, COMPONENT_TYPE_MESH); - componentid_t floorMat = entityAddComponent(floorEnt, COMPONENT_TYPE_MATERIAL); - componentid_t floorPhys = entityAddComponent(floorEnt, COMPONENT_TYPE_PHYSICS); - - entityPositionSetPosition(floorEnt, floorPos, (vec3){ -5.0f, 0.0f, -5.0f }); - entityPositionSetScale(floorEnt, floorPos, (vec3){ 10.0f, 1.0f, 10.0f }); - entityMeshSetMesh(floorEnt, floorMesh, &PLANE_MESH_SIMPLE); - entityMaterialGetShaderMaterial(floorEnt, floorMat)->unlit.color = COLOR_GREEN; - - entityphysics_t *floorPhysData = entityPhysicsGet(floorEnt, floorPhys); - floorPhysData->type = PHYSICS_BODY_STATIC; - floorPhysData->shape.type = PHYSICS_SHAPE_PLANE; - floorPhysData->shape.data.plane.normal[0] = 0.0f; - floorPhysData->shape.data.plane.normal[1] = 1.0f; - floorPhysData->shape.data.plane.normal[2] = 0.0f; - floorPhysData->shape.data.plane.distance = 0.0f; - - // Box - phBoxEnt = entityManagerAdd(); - componentid_t boxPos = entityAddComponent(phBoxEnt, COMPONENT_TYPE_POSITION); - componentid_t boxMesh = entityAddComponent(phBoxEnt, COMPONENT_TYPE_MESH); - componentid_t boxMat = entityAddComponent(phBoxEnt, COMPONENT_TYPE_MATERIAL); - phBoxPhys = entityAddComponent(phBoxEnt, COMPONENT_TYPE_PHYSICS); - - entityMeshSetMesh(phBoxEnt, boxMesh, &CUBE_MESH_SIMPLE); - entityMaterialGetShaderMaterial(phBoxEnt, boxMat)->unlit.color = COLOR_RED; - entityPositionSetPosition(phBoxEnt, boxPos, (vec3){ 0.0f, 4.0f, 0.0f }); + consolePrint("Engine initialized\n"); /* Run the init script. */ scriptcontext_t ctx; @@ -170,38 +59,18 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { } errorret_t engineUpdate(void) { - // errorChain(networkUpdate()); - + errorChain(networkUpdate()); timeUpdate(); inputUpdate(); - + consoleUpdate(); uiUpdate(); errorChain(sceneUpdate()); - - /* Reset the box to its start position on demand. */ - if(inputIsDown(INPUT_ACTION_ACCEPT)) { - componentid_t posComp = entityGetComponent(phBoxEnt, COMPONENT_TYPE_POSITION); - entityPositionSetPosition(phBoxEnt, posComp, (vec3){ 0.0f, 4.0f, 0.0f }); - entityPhysicsSetVelocity(phBoxEnt, phBoxPhys, (vec3){ 0.0f, 0.0f, 0.0f }); - } - - /* Step physics: positions are updated directly on POSITION components. */ physicsManagerUpdate(); - errorChain(gameUpdate()); errorChain(displayUpdate()); if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false; - if(TIME.time >= onlineSwapTime) { - onlineSwapTime = FLT_MAX; - if(NETWORK.state == NETWORK_STATE_CONNECTED) { - goOffline(); - } else { - goOnline(); - } - } - errorOk(); } @@ -210,13 +79,15 @@ void engineExit(void) { } errorret_t engineDispose(void) { - // errorChain(networkDispose()); sceneDispose(); errorChain(gameDispose()); + errorChain(networkDispose()); entityManagerDispose(); localeManagerDispose(); uiDispose(); + consoleDispose(); errorChain(displayDispose()); errorChain(assetDispose()); + errorOk(); } diff --git a/src/dusk/entity/entitymanager.c b/src/dusk/entity/entitymanager.c index 1b691b58..fc0fde5c 100644 --- a/src/dusk/entity/entitymanager.c +++ b/src/dusk/entity/entitymanager.c @@ -8,7 +8,7 @@ #include "entitymanager.h" #include "assert/assert.h" #include "util/memory.h" -#include "scene/scene.h" +#include "console/console.h" entitymanager_t ENTITY_MANAGER; @@ -19,7 +19,7 @@ void entityManagerInit(void) { sizeof(entityid_t) * COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX ); - sceneLog( + consolePrint( "Entity Manager size: %zu bytes (%.2f KB)\n", sizeof(entitymanager_t), sizeof(entitymanager_t) / 1024.0f diff --git a/src/dusk/scene/scene.c b/src/dusk/scene/scene.c index 6aaf9158..de8d54a9 100644 --- a/src/dusk/scene/scene.c +++ b/src/dusk/scene/scene.c @@ -15,56 +15,12 @@ #include "display/spritebatch/spritebatch.h" #include "display/text/text.h" #include "display/screen/screen.h" +#include "console/console.h" scene_t SCENE; -char_t SCENE_LOG[SCENE_LOG_SIZE]; - -void sceneLog(const char *fmt, ...) { - char temp[512]; - - // 1. Format input like printf - va_list args; - va_start(args, fmt); - vsnprintf(temp, sizeof(temp), fmt, args); - va_end(args); - - printf("%s", temp); - - // 2. Split into lines - char *lines[64]; - int line_count = 0; - - char *ptr = temp; - while (*ptr && line_count < 64) { - lines[line_count++] = ptr; - - char *nl = strchr(ptr, '\n'); - if (!nl) break; - - *nl = '\0'; - ptr = nl + 1; - } - - // 3. Prepend lines in reverse order (so final order is correct) - for (int i = 0; i < line_count; i++) { - char new_log[SCENE_LOG_SIZE]; - - snprintf(new_log, sizeof(new_log), "%s\n%s", lines[i], SCENE_LOG); - - // Copy back safely - strncpy(SCENE_LOG, new_log, SCENE_LOG_SIZE - 1); - SCENE_LOG[SCENE_LOG_SIZE - 1] = '\0'; - } -} - errorret_t sceneInit(void) { memoryZero(&SCENE, sizeof(scene_t)); - - memoryZero(SCENE_LOG, sizeof(SCENE_LOG)); - sceneLog("Init\n"); - - errorOk(); } @@ -177,17 +133,8 @@ errorret_t sceneRender(void) { errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj)); errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view)); errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model)); - // errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_0TEXTURE, &DEFAULT_FONT_TEXTURE)); - // errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, COLOR_WHITE)); - errorChain(textDraw( - 32, 32, - // "Hello World", - SCENE_LOG, - COLOR_WHITE, - &DEFAULT_FONT_TILESET, - &DEFAULT_FONT_TEXTURE - )); - errorChain(spriteBatchFlush()); + + errorChain(consoleDraw()); errorOk(); } diff --git a/src/dusk/scene/scene.h b/src/dusk/scene/scene.h index bf949761..6e0a079d 100644 --- a/src/dusk/scene/scene.h +++ b/src/dusk/scene/scene.h @@ -14,10 +14,6 @@ typedef struct { extern scene_t SCENE; -#define SCENE_LOG_SIZE 1024 -extern char_t SCENE_LOG[SCENE_LOG_SIZE]; -void sceneLog(const char *fmt, ...); - /** * Initialize the scene subsystem. *