diff --git a/Makefile b/Makefile index e1549b294..82f14d136 100644 --- a/Makefile +++ b/Makefile @@ -517,6 +517,10 @@ ifeq ($(DISCORD_SDK),1) SRC_DIRS += src/pc/discord endif +ifeq ($(WINDOWS_BUILD),0) + SRC_DIRS += src/pc/linenoise +endif + SRC_DIRS += src/pc/mumble ULTRA_SRC_DIRS := lib/src lib/src/math lib/asm lib/data diff --git a/autogen/convert_functions.py b/autogen/convert_functions.py index 19d9ad16b..b5151e4f7 100644 --- a/autogen/convert_functions.py +++ b/autogen/convert_functions.py @@ -35,6 +35,7 @@ in_files = [ "src/game/mario_step.h", "src/game/mario.h", "src/game/rumble_init.h", + "src/pc/commands.h", "src/pc/djui/djui_popup.h", "src/pc/network/network_utils.h", "src/pc/djui/djui_console.h", @@ -86,6 +87,7 @@ in_files = [ override_allowed_functions = { "src/audio/external.h": [ " play_", "fade", "current_background", "stop_", "sound_banks", "drop_queued_background_music", "set_sound_moving_speed", "background_music_default_volume", "get_sound_pan", "sound_get_level_intensity", "set_audio_muted" ], "src/game/rumble_init.h": [ "queue_rumble_", "reset_rumble_timers" ], + "src/pc/commands.h": [ "command_message_create" ], "src/pc/djui/djui_popup.h": [ "create" ], "src/pc/djui/djui_language.h": [ "djui_language_get" ], "src/pc/djui/djui_panel_menu.h": [ "djui_menu_get_rainbow_string_color" ], diff --git a/autogen/lua_definitions/constants.lua b/autogen/lua_definitions/constants.lua index 1d7dfb2dc..ccc981d75 100644 --- a/autogen/lua_definitions/constants.lua +++ b/autogen/lua_definitions/constants.lua @@ -2751,6 +2751,9 @@ DIALOG_COUNT = 170 --- @type DialogId --- | `DIALOG_169` --- | `DIALOG_COUNT` +--- @type integer +MAX_CONSOLE_INPUT_LENGTH = 500 + CONSOLE_MESSAGE_INFO = 0 --- @type ConsoleMessageLevel CONSOLE_MESSAGE_WARNING = 1 --- @type ConsoleMessageLevel CONSOLE_MESSAGE_ERROR = 2 --- @type ConsoleMessageLevel diff --git a/autogen/lua_definitions/functions.lua b/autogen/lua_definitions/functions.lua index 564f56b9e..04bc622ad 100644 --- a/autogen/lua_definitions/functions.lua +++ b/autogen/lua_definitions/functions.lua @@ -3789,6 +3789,12 @@ function update_character_anim_offset(m) -- ... end +--- @param message string +--- @param level? ConsoleMessageLevel +function command_message_create(message, level) + -- ... +end + --- @param message string --- Creates a `message` in the game's chat box function djui_chat_message_create(message) diff --git a/autogen/lua_definitions/manual.lua b/autogen/lua_definitions/manual.lua index 2ce161f8a..42ccb971b 100644 --- a/autogen/lua_definitions/manual.lua +++ b/autogen/lua_definitions/manual.lua @@ -126,6 +126,19 @@ function update_chat_command_description(command, description) -- ... end +--- @param command string The command to run. Should be easy to type +--- @param description string Should describe what the command does and how to use it +--- @param func fun(msg:string): boolean Run upon activating the command. Return `true` to confirm the command has succeeded +function hook_console_command(command, description, func) + -- ... +end + +--- @param command string The command to change the description of +--- @param description string The description to change to +function update_console_command_description(command, description) + -- ... +end + --- @param hookEventType LuaHookedEventType When a function should run --- @param func fun(...: any): any?, any? The function to run --- Different hooks can pass in different parameters and have different return values. Be sure to read the hooks guide for more information. @@ -407,13 +420,13 @@ end --- @param command string --- @vararg integer | string | Gfx | Texture | Vtx Parameters for the command --- Sets a display list command on the display list given. ---- +--- --- If `command` includes parameter specifiers (subsequences beginning with `%`), the additional arguments --- following `command` are converted and inserted in `command` replacing their respective specifiers. ---- +--- --- The number of provided parameters must be equal to the number of specifiers in `command`, --- and the order of parameters must be the same as the specifiers. ---- +--- --- The following specifiers are allowed: --- - `%i` for an `integer` parameter --- - `%s` for a `string` parameter diff --git a/data/dynos.cpp.h b/data/dynos.cpp.h index e0b699d9d..977726ea8 100644 --- a/data/dynos.cpp.h +++ b/data/dynos.cpp.h @@ -11,6 +11,7 @@ extern "C" { #include "game/moving_texture.h" #include "pc/djui/djui_console.h" #include "pc/fs/fmem.h" +#include "pc/debuglog.h" } #define FUNCTION_CODE (u32) 0x434E5546 @@ -730,13 +731,13 @@ T *CopyBytes(const T *aPtr, u64 aSize) { template void PrintNoNewLine(const char *aFmt, Args... aArgs) { - printf(aFmt, aArgs...); + log_to_terminal(aFmt, aArgs...); fflush(stdout); } template void Print(const char *aFmt, Args... aArgs) { - printf(aFmt, aArgs...); + log_to_terminal(aFmt, aArgs...); printf("\r\n"); fflush(stdout); } @@ -750,7 +751,7 @@ void PrintConsole(enum ConsoleMessageLevel level, const char *aFmt, Args... aArg template void PrintError(const char *aFmt, Args... aArgs) { - printf(aFmt, aArgs...); + log_to_terminal(aFmt, aArgs...); printf("\r\n"); fflush(stdout); PrintConsole(CONSOLE_MESSAGE_ERROR, aFmt, aArgs...); diff --git a/docs/lua/constants.md b/docs/lua/constants.md index cf00279f7..d38aa30a4 100644 --- a/docs/lua/constants.md +++ b/docs/lua/constants.md @@ -1136,6 +1136,7 @@
## [djui_console.h](#djui_console.h) +- MAX_CONSOLE_INPUT_LENGTH ### [enum ConsoleMessageLevel](#ConsoleMessageLevel) | Identifier | Value | diff --git a/docs/lua/examples/audio-test/main.lua b/docs/lua/examples/audio-test/main.lua index 8bae3477f..2986f6020 100644 --- a/docs/lua/examples/audio-test/main.lua +++ b/docs/lua/examples/audio-test/main.lua @@ -9,36 +9,36 @@ function on_stream_play(msg) if(msg == "load") then audioStream = audio_stream_load("music.mp3") audio_stream_set_looping(audioStream, true) - djui_chat_message_create("audio audioStream:" .. tostring(audioStream)); + command_message_create("audio audioStream:" .. tostring(audioStream)); end if(msg == "play") then audio_stream_play(audioStream, true, 1); - djui_chat_message_create("playing audio"); + command_message_create("playing audio"); end if(msg == "resume") then audio_stream_play(audioStream, false, 1); - djui_chat_message_create("resuming audio"); + command_message_create("resuming audio"); end if(msg == "pause") then audio_stream_pause(audioStream); - djui_chat_message_create("pausing audio"); + command_message_create("pausing audio"); end if(msg == "stop") then audio_stream_stop(audioStream); - djui_chat_message_create("stopping audio"); + command_message_create("stopping audio"); end if(msg == "destroy") then audio_stream_destroy(audioStream); - djui_chat_message_create("destroyed audio"); + command_message_create("destroyed audio"); end if(msg == "getpos") then - djui_chat_message_create("pos: " .. tostring(audio_stream_get_position(audioStream))); + command_message_create("pos: " .. tostring(audio_stream_get_position(audioStream))); end return true; @@ -48,7 +48,7 @@ function on_sample_play(msg) if(msg == "load") then audioSample = audio_sample_load("sample.mp3"); - djui_chat_message_create("audio audioStream:" .. tostring(audioSample)); + command_message_create("audio audioStream:" .. tostring(audioSample)); return true; end diff --git a/docs/lua/examples/bytestring-packet-example.lua b/docs/lua/examples/bytestring-packet-example.lua index 3c834d5f0..c7049e0fc 100644 --- a/docs/lua/examples/bytestring-packet-example.lua +++ b/docs/lua/examples/bytestring-packet-example.lua @@ -27,12 +27,12 @@ function send_example_1(byte_param, short_param, long_param, float_param, double network_send_bytestring(true, bytestring) - djui_chat_message_create('Sent bytestring packet example 1:') - djui_chat_message_create(' byte_param: ' .. byte_param) - djui_chat_message_create(' short_param: ' .. short_param) - djui_chat_message_create(' long_param: ' .. long_param) - djui_chat_message_create(' float_param: ' .. float_param) - djui_chat_message_create(' double_param: ' .. double_param) + command_message_create('Sent bytestring packet example 1:') + command_message_create(' byte_param: ' .. byte_param) + command_message_create(' short_param: ' .. short_param) + command_message_create(' long_param: ' .. long_param) + command_message_create(' float_param: ' .. float_param) + command_message_create(' double_param: ' .. double_param) end function on_packet_bytestring_receive_example_1(bytestring) @@ -55,11 +55,17 @@ function on_packet_bytestring_receive_example_1(bytestring) --------------------------------------- djui_chat_message_create('Received bytestring packet example 1:') + log_to_console('Received bytestring packet example 1:') djui_chat_message_create(' byte_param: ' .. byte_param) + log_to_console(' byte_param: ' .. byte_param) djui_chat_message_create(' short_param: ' .. short_param) + log_to_console(' short_param: ' .. short_param) djui_chat_message_create(' long_param: ' .. long_param) + log_to_console(' long_param: ' .. long_param) djui_chat_message_create(' float_param: ' .. float_param) + log_to_console(' float_param: ' .. float_param) djui_chat_message_create(' double_param: ' .. double_param) + log_to_console(' double_param: ' .. double_param) end --------------------------------------------------------------------------------------------------- @@ -76,9 +82,9 @@ function send_example_2(long_param, string_param) network_send_bytestring(true, bytestring) - djui_chat_message_create('Sent bytestring packet example 2:') - djui_chat_message_create(' byte_param: ' .. long_param) - djui_chat_message_create(' string_param: ' .. string_param) + command_message_create('Sent bytestring packet example 2:') + command_message_create(' byte_param: ' .. long_param) + command_message_create(' string_param: ' .. string_param) end function on_packet_bytestring_receive_example_2(bytestring) @@ -98,8 +104,11 @@ function on_packet_bytestring_receive_example_2(bytestring) --------------------------------------- djui_chat_message_create('Received bytestring packet example 2:') + log_to_console('Received bytestring packet example 2:') djui_chat_message_create(' long_param: ' .. long_param) + log_to_console(' long_param: ' .. long_param) djui_chat_message_create(' string_param: ' .. string_param) + log_to_console(' string_param: ' .. string_param) end --------------------------------------------------------------------------------------------------- diff --git a/docs/lua/examples/texture-override/main.lua b/docs/lua/examples/texture-override/main.lua index f40ddc50d..d757d9ce5 100644 --- a/docs/lua/examples/texture-override/main.lua +++ b/docs/lua/examples/texture-override/main.lua @@ -2,18 +2,6 @@ -- description: Run /matrix and a builtin texture name to replace with the digital rain -- deluxe: true -if SM64COOPDX_VERSION == nil then - local first = false - hook_event(HOOK_ON_LEVEL_INIT, function() - if not first then - first = true - play_sound(SOUND_MENU_CAMERA_BUZZ, gMarioStates[0].marioObj.header.gfx.cameraToObject) - djui_chat_message_create("\\#ff7f7f\\Matrix Code is not supported with sm64ex-coop\nas it uses sm64coopdx exclusive Lua functionality.\n\\#dcdcdc\\To use this mod, try out sm64coopdx at\n\\#7f7fff\\https://sm64coopdx.com") - end - end) - return -end - local sMatrixFrames = {} for i = 0, 10 do sMatrixFrames[i] = get_texture_info("matrix_" .. i) diff --git a/docs/lua/examples/water-level.lua b/docs/lua/examples/water-level.lua index 3b6f2a462..6a83edb06 100644 --- a/docs/lua/examples/water-level.lua +++ b/docs/lua/examples/water-level.lua @@ -3,24 +3,24 @@ local function on_get_command(msg) if not network_is_server() then - djui_chat_message_create("You need to be the host!") + command_message_create("You need to be the host!", CONSOLE_MESSAGE_ERROR) return true end - djui_chat_message_create(tostring(get_water_level(0))) - djui_chat_message_create(tostring(get_water_level(1))) + command_message_create(tostring(get_water_level(0))) + command_message_create(tostring(get_water_level(1))) return true end local function on_set_command(msg) if not network_is_server() then - djui_chat_message_create("You need to be the host!") + command_message_create("You need to be the host!", CONSOLE_MESSAGE_ERROR) return true end local num = tonumber(msg) if not num then - djui_chat_message_create("Not a number!") + command_message_create("Not a number!", CONSOLE_MESSAGE_ERROR) return true end diff --git a/docs/lua/functions-3.md b/docs/lua/functions-3.md index 0dcd65edb..d4ae7a48f 100644 --- a/docs/lua/functions-3.md +++ b/docs/lua/functions-3.md @@ -2661,6 +2661,33 @@ Updates Mario's current animation offset. This adjusts Mario's position based on
+--- +# functions from commands.h + +
+ + +## [command_message_create](#command_message_create) + +### Lua Example +`command_message_create(message, level)` + +### Parameters +| Field | Type | +| ----- | ---- | +| message | `string` | +| level | [enum ConsoleMessageLevel](constants.md#enum-ConsoleMessageLevel) | + +### Returns +- None + +### C Prototype +`void command_message_create(const char* message, OPTIONAL enum ConsoleMessageLevel level);` + +[:arrow_up_small:](#) + +
+ --- # functions from djui_chat_message.h diff --git a/docs/lua/functions.md b/docs/lua/functions.md index cd60e96f9..9282199d2 100644 --- a/docs/lua/functions.md +++ b/docs/lua/functions.md @@ -740,6 +740,11 @@
+- commands.h + - [command_message_create](functions-3.md#command_message_create) + +
+ - djui_chat_message.h - [djui_chat_message_create](functions-3.md#djui_chat_message_create) diff --git a/docs/lua/guides/hooks.md b/docs/lua/guides/hooks.md index 07d522650..2b891a77a 100644 --- a/docs/lua/guides/hooks.md +++ b/docs/lua/guides/hooks.md @@ -6,6 +6,7 @@ Hooks are a way for SM64 to trigger Lua code, whereas the functions listed in [f # Supported Hooks - [hook_behavior](#hook_behavior) - [hook_chat_command](#hook_chat_command) +- [hook_console_command](#hook_console_command) - [hook_event](#hook_event) - [hook_mario_action](#hook_mario_action) - [hook_on_sync_table_change](#hook_on_sync_table_change) @@ -54,7 +55,7 @@ id_bhvExample = hook_behavior(nil, OBJ_LIST_DEFAULT, true, bhv_example_init, bhv
## [hook_chat_command](#hook_chat_command) -`hook_chat_command()` allows Lua mods to react and respond to chat commands. Chat commands start with the `/` character. The function the mod passes to the hook should return `true` when the command was valid and `false` otherwise. +`hook_chat_command()` allows Lua mods to react and respond to chat commands. Chat commands start with the `/` character. The function the mod passes to the hook should return `true` when the command was valid and `false` otherwise. Use `command_message_create` to show any message to the user. Chat commands appear in the chat, console, and terminal. ### Parameters @@ -69,10 +70,10 @@ id_bhvExample = hook_behavior(nil, OBJ_LIST_DEFAULT, true, bhv_example_init, bhv ```lua function on_test_command(msg) if msg == "on" then - djui_chat_message_create("Test: enabled") + command_message_create("Test: enabled") return true elseif msg == "off" then - djui_chat_message_create("Test: disabled") + command_message_create("Test: disabled") return true end return false @@ -85,6 +86,38 @@ hook_chat_command("test", "[on|off] turn test on or off", on_hide_and_seek_comma
+## [hook_console_command](#hook_console_command) +`hook_console_command()` allows Lua mods to react and respond to console commands. The function the mod passes to the hook should return `true` when the command was valid and `false` otherwise. You should use `command_message_create` to show any messages to the user. Console messages only appear in the console and terminal. + +### Parameters + +| Field | Type | +| ----- | ---- | +| command | `string` | +| description | `string` | +| func | `Lua Function` (`string` message) -> `bool` | + +### Lua Example + +```lua +function on_test_command(msg) + if msg == "on" then + command_message_create("Test: enabled") + return true + elseif msg == "off" then + command_message_create("Test: disabled") + return true + end + return false +end + +hook_console_command("test", "[on|off] turn test on or off", on_test_command) +``` + +[:arrow_up_small:](#) + +
+ ## [hook_event](#hook_event) The lua functions sent to `hook_event()` will be automatically called by SM64 when certain events occur. @@ -143,7 +176,7 @@ The lua functions sent to `hook_event()` will be automatically called by SM64 wh | HOOK_ON_GEO_PROCESS | Called when a GeoLayout is processed **Note:** You must set the `hookProcess` field of the graph node to a non-zero value | [GraphNode](../structs.md#GraphNode) graphNode, `integer` matStackIndex | | HOOK_BEFORE_GEO_PROCESS | Called before a GeoLayout is processed **Note:** You must set the `hookProcess` field of the graph node to a non-zero value | [GraphNode](../structs.md#GraphNode) graphNode, `integer` matStackIndex | | HOOK_ON_GEO_PROCESS_CHILDREN | Called when the children of a GeoLayout node is processed **Note:** You must set the `hookProcess` field of the parent graph node to a non-zero value | [GraphNode](../structs.md#GraphNode) graphNode, `integer` matStackIndex | -| HOOK_MARIO_OVERRIDE_GEOMETRY_INPUTS | Called before running Mario's geometry input logic, return `false` to not run it. | [MarioState](../structs.md) m | +| HOOK_MARIO_OVERRIDE_GEOMETRY_INPUTS | Called before running Mario's geometry input logic, return `false` to not run it. | [MarioState](../structs.md) m | | HOOK_ON_INTERACTIONS | Called when the Mario interactions are processed | [MarioState](../structs.md#MarioState) mario | | HOOK_ALLOW_FORCE_WATER_ACTION | Called when executing a non-water action while under the water's surface, or vice versa. Return `false` to prevent the player from being forced out of the action at the water's surface | [MarioState](../structs.md#MarioState) mario, `boolean` isInWaterAction | | HOOK_BEFORE_WARP | Called before the local player warps. Return a table with `destLevel`, `destArea`, `destWarpNode`, to override the warp | `integer` destLevel, `integer` destArea, `integer` destWarpNode, `integer` arg | diff --git a/docs/lua/lua.md b/docs/lua/lua.md index 553659b33..a3ce9f45e 100644 --- a/docs/lua/lua.md +++ b/docs/lua/lua.md @@ -17,9 +17,9 @@ Save file locations:
## Tips -- When developing Lua mods, run the game from a console. Lua errors and logs will appear there, but only if the game is launched with the `--console` launch parameter. +- When developing Lua mods, open the console with `~` or `F1` to see Lua errors and warnings. - When a function requests a time parameter, it is almost if not always in frames. -- You can use the `print()` command when debugging. Your logs will show up in the console. +- You can use the `print()` command when debugging. Your logs will show up in the console and terminal. - You can create a folder within the mods folder containing multiple lua scripts as long as one script is called `main.lua`. Dynos actors can be placed inside this mod folder under `/actors/`.
@@ -30,7 +30,7 @@ Save file locations: - [Structs](structs.md) ### Guides -- [Setting up Visual Studio Code](guides/vs-code-setup.md) +- [Setting up Visual Studio Code](guides/vs-code-setup.md) - [Hooks](guides/hooks.md) - [gMarioStates](guides/mario-state.md) - [Behavior Object Lists](guides/object-lists.md) diff --git a/mods/arena/main.lua b/mods/arena/main.lua index 458fc786f..fceabc354 100644 --- a/mods/arena/main.lua +++ b/mods/arena/main.lua @@ -378,7 +378,7 @@ function on_arena_player_death(victimGlobalId, attackerGlobalId) if sAttacker.team ~= 0 then local teamScore = calculate_team_score(sAttacker.team) if teamScore >= gGameModes[gGlobalSyncTable.gameMode].scoreCap then - round_end() + round_end() end end end @@ -472,7 +472,7 @@ function on_gamemode_command(msg) end if msg == 'random' then - djui_chat_message_create("[Arena] Setting to random gamemode.") + command_message_create("[Arena] Setting to random gamemode.") sRandomizeMode = true round_end() sWaitTimer = 1 @@ -481,7 +481,7 @@ function on_gamemode_command(msg) end if setMode ~= nil then - djui_chat_message_create("[Arena] Setting game mode.") + command_message_create("[Arena] Setting game mode.") gGlobalSyncTable.gameMode = setMode sRandomizeMode = false round_end() @@ -490,7 +490,7 @@ function on_gamemode_command(msg) return true end - djui_chat_message_create("/arena \\#00ffff\\gamemode\\#ffff00\\ " .. string.format("[%s|random]\\#dcdcdc\\ sets gamemode", sGameModeShortTimes)) + command_message_create("/arena \\#00ffff\\gamemode\\#ffff00\\ " .. string.format("[%s|random]\\#dcdcdc\\ sets gamemode", sGameModeShortTimes)) return true end @@ -512,21 +512,21 @@ function on_level_command(msg) return true end - djui_chat_message_create("/arena \\#00ffff\\level\\#ffff00\\ " .. string.format("[%s]\\#dcdcdc\\ sets level", get_level_choices())) + command_message_create("/arena \\#00ffff\\level\\#ffff00\\ " .. string.format("[%s]\\#dcdcdc\\ sets level", get_level_choices())) return true end function on_jump_leniency_command(msg) local num = tonumber(msg) if not network_is_server and not network_is_moderator() then - djui_chat_message_create("\\#ffa0a0\\[Arena] You need to be a moderator to use this command.") + command_message_create("\\#ffa0a0\\[Arena] You need to be a moderator to use this command.") return true elseif num == nil then - djui_chat_message_create("\\#ffa0a0\\[Arena] Invalid number!") + command_message_create("\\#ffa0a0\\[Arena] Invalid number!") return true else gGlobalSyncTable.jumpLeniency = num - djui_chat_message_create("[Arena] The number of jump leniency frames has been set to " .. num) + command_message_create("[Arena] The number of jump leniency frames has been set to " .. num) return true end end @@ -545,7 +545,7 @@ local function on_arena_command(msg) return on_jump_leniency_command(args[2] or "") end - djui_chat_message_create("/arena \\#00ffff\\[gamemode|level|jump-leniency]") + command_message_create("/arena \\#00ffff\\[gamemode|level|jump-leniency]") return true end diff --git a/mods/character-select-coop/main.lua b/mods/character-select-coop/main.lua index e15e77b6b..57de78ddb 100644 --- a/mods/character-select-coop/main.lua +++ b/mods/character-select-coop/main.lua @@ -528,7 +528,7 @@ local function update_character_render_table() end end end - + if #characterTableRender > 0 then -- Get icons for category based on name similarity if category.icon1 == nil or category.icon2 == nil then @@ -902,7 +902,7 @@ local worldColor = { ambient = {r = 255, g = 255, b = 255} } local menuOffsetX = 0 -local menuOffsetY = 0 +local menuOffsetY = 0 local camScale = 1 local prevMusicToggle = 1 local prevVisualToggle = 1 @@ -919,7 +919,7 @@ local function mario_update(m) set_all_models() queueStorageFailsafe = false end - + local np = gNetworkPlayers[m.playerIndex] local p = gCSPlayers[m.playerIndex] @@ -952,7 +952,7 @@ local function mario_update(m) end end - if djui_hud_is_pause_menu_created() then + if djui_hud_is_pause_menu_created() then if prevBaseCharFrame ~= np.modelIndex then force_set_character(np.modelIndex) p.presetPalette = 0 @@ -978,7 +978,7 @@ local function mario_update(m) local charTable = characterTable[currChar] p.saveName = charTable.saveName p.currAlt = charTable.currAlt - + p.modelId = charTable[charTable.currAlt].model if charTable[charTable.currAlt].baseChar ~= nil then p.baseChar = charTable[charTable.currAlt].baseChar @@ -1316,9 +1316,9 @@ function set_model(o, model) if o.oOriginalModel == 0 then o.oOriginalModel = obj_get_model_id_extended(o) end - + local model = run_func_or_get_var(currReplace, o, o.oOriginalModel) - + if model ~= nil and visualToggle then o.oModelHasBeenReplaced = 1 if obj_has_model_extended(o, model) == 0 then @@ -1601,7 +1601,7 @@ local function on_hud_render() djui_hud_set_color(charColor.r*0.5 + 127, charColor.g*0.5 + 127, charColor.b*0.5 + 127, math.min(paletteTrans, 255)) djui_hud_print_text(paletteName, x, y, 0.5) end - + -- Render Background Wall local wallWidth = TEX_WALL_LEFT.width local wallHeight = TEX_WALL_LEFT.height @@ -1615,11 +1615,11 @@ local function on_hud_render() djui_hud_render_texture_auto_interpolated("wall-l", TEX_WALL_LEFT, x, y, wallScale, wallScale) djui_hud_set_color(playerPants.r, playerPants.g, playerPants.b, 255) djui_hud_render_texture_auto_interpolated("wall-r", TEX_WALL_RIGHT, x, y, wallScale, wallScale) - + -- Render Graffiti local graffiti = characterGraffiti[currChar] or TEX_GRAFFITI_DEFAULT - local graffitiWidthScale = 120/graffiti.width - local graffitiHeightScale = 120/graffiti.width + local graffitiWidthScale = 120/graffiti.width + local graffitiHeightScale = 120/graffiti.width djui_hud_set_color(255, 255, 255, 150) djui_hud_render_texture_auto_interpolated("graffiti", graffiti, wallMiddle - graffiti.width*0.5*graffitiWidthScale - menuOffsetX, height*0.5 - graffiti.height*0.5*graffitiHeightScale - menuOffsetY, graffitiWidthScale, graffitiHeightScale) @@ -1638,7 +1638,7 @@ local function on_hud_render() local scale = 0.35 local textScale = scale*1.5 local buttonSpacing = 32 - + if not gridMenu then -- Render Character List gridYOffset = lerp(gridYOffset, currCharRender*buttonSpacing, 0.1) @@ -1666,7 +1666,7 @@ local function on_hud_render() -- Name Screen djui_hud_set_color(charColor.r*0.5, charColor.g*0.5, charColor.b*0.5, 255) djui_hud_print_text(charName, x + 112*scale + segments*16*scale*0.5 - charNameLength*textScale*0.5, y + 32*scale, textScale) - + -- Bottom Info djui_hud_render_rect(x + 112*scale, y + 84*scale, segments*16*scale, scale) djui_hud_print_text(channel, x + 112*scale, y + 85*scale, 0.3*scale) @@ -1907,7 +1907,7 @@ local function on_hud_render() djui_hud_print_text(TEXT_VERSION, 2, height - 7, 0.4) local currMenu = gridMenu and MENU_BINDS_GRID or MENU_BINDS_DEFAULT if options == OPTIONS_MAIN then - currMenu = MENU_BINDS_OPTIONS + currMenu = MENU_BINDS_OPTIONS elseif options == OPTIONS_CREDITS then currMenu = MENU_BINDS_GRID end @@ -2091,7 +2091,7 @@ local function before_mario_update(m) until update_character_render_table() gearRotationTarget = gearRotationTarget + 0x10000/#characterCategories categoryOpenTimer = 150 - + play_sound(SOUND_MENU_CAMERA_TURN, cameraToObject) end ) @@ -2108,7 +2108,7 @@ local function before_mario_update(m) play_sound(SOUND_MENU_CAMERA_TURN, cameraToObject) end ) - + if not gridMenu then -- List Controls run_func_with_condition_and_cooldown(FUNC_INDEX_VERTICAL, @@ -2150,7 +2150,7 @@ local function before_mario_update(m) end ) end - + else -- Grid Controls run_func_with_condition_and_cooldown(FUNC_INDEX_VERTICAL, @@ -2186,7 +2186,7 @@ local function before_mario_update(m) play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject) end ) - + -- Alt switcher if #characterTable[currChar] > 1 then run_func_with_condition_and_cooldown(FUNC_INDEX_ALT, @@ -2392,14 +2392,14 @@ local function chat_command(msg) menu = not menu return true else - djui_chat_message_create(TEXT_PAUSE_UNAVAILABLE) + command_message_create(TEXT_PAUSE_UNAVAILABLE) return true end end -- Help Prompt Check if msg == "?" or msg == "help" then - djui_chat_message_create("Character Select's Avalible Commands:" .. + command_message_create("Character Select's Avalible Commands:" .. "\n\\#ffff33\\/char-select help\\#ffffff\\ - Returns Avalible Commands" .. "\n\\#ffff33\\/char-select menu\\#ffffff\\ - Opens the Menu" .. "\n\\#ffff33\\/char-select [name/num]\\#ffffff\\ - Switches to Character" .. @@ -2413,9 +2413,9 @@ local function chat_command(msg) return true end - -- Stop Character checks if API disallows it + -- Stop Character checks if API disallows it if not menu_is_allowed() or charBeingSet then - djui_chat_message_create("Character Cannot be Changed") + command_message_create("Character Cannot be Changed") return true end @@ -2426,7 +2426,7 @@ local function chat_command(msg) for a = 1, #characterTable[i] do if msg == string.lower(characterTable[i][a].name) or msg == saveName then force_set_character(i, msg ~= saveName and a or 1) - djui_chat_message_create('Character set to "' .. characterTable[i][characterTable[i].currAlt].name .. '" Successfully!') + command_message_create('Character set to "' .. characterTable[i][characterTable[i].currAlt].name .. '" Successfully!') return true end end @@ -2441,12 +2441,12 @@ local function chat_command(msg) altNum = altNum and altNum or 1 if charNum > 0 and charNum <= #characterTable and characterTable[charNum].locked ~= LOCKED_TRUE then force_set_character(charNum, altNum) - djui_chat_message_create('Character set to "' .. characterTable[charNum][altNum].name .. '" Successfully!') + command_message_create('Character set to "' .. characterTable[charNum][altNum].name .. '" Successfully!') return true end end - djui_chat_message_create("Character Not Found") + command_message_create("Character Not Found") return true end diff --git a/mods/day-night-cycle/main.lua b/mods/day-night-cycle/main.lua index badd1f056..ede86d262 100755 --- a/mods/day-night-cycle/main.lua +++ b/mods/day-night-cycle/main.lua @@ -333,12 +333,12 @@ end local function on_set_command(msg) if dayNightCycleApi.lockTime then play_sound(SOUND_MENU_CAMERA_BUZZ, gGlobalSoundSource) - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.") + command_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.", CONSOLE_MESSAGE_ERROR) return end if msg == "" then - djui_chat_message_create("/time \\#00ffff\\set\\#ffff00\\ [TIME]\\#dcdcdc\\ to set the time") + command_message_create("/time \\#00ffff\\set\\#ffff00\\ [TIME]\\#dcdcdc\\ to set the time") return end @@ -359,9 +359,9 @@ local function on_set_command(msg) local amount = tonumber(msg) if amount ~= nil then gGlobalSyncTable.time = amount * SECOND - djui_chat_message_create("[Day Night Cycle] Time set to " .. math_floor(gGlobalSyncTable.time / SECOND)) + command_message_create("[Day Night Cycle] Time set to " .. math_floor(gGlobalSyncTable.time / SECOND)) else - djui_chat_message_create(string.format("\\#ffa0a0\\[Day Night Cycle] Could not set time to '%s'", msg)) + command_message_create(string.format("\\#ffa0a0\\[Day Night Cycle] Could not set time to '%s'", msg), CONSOLE_MESSAGE_ERROR) end end @@ -373,13 +373,13 @@ end local function on_add_command(msg) if dayNightCycleApi.lockTime then play_sound(SOUND_MENU_CAMERA_BUZZ, gGlobalSoundSource) - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.") + command_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.", CONSOLE_MESSAGE_ERROR) return end local amount = tonumber(msg) if amount == nil then - djui_chat_message_create("/time \\#00ffff\\add\\#ffff00\\ [AMOUNT]\\#dcdcdc\\ to add to the time") + command_message_create("/time \\#00ffff\\add\\#ffff00\\ [AMOUNT]\\#dcdcdc\\ to add to the time") return end local oldTime = gGlobalSyncTable.time @@ -388,7 +388,7 @@ local function on_add_command(msg) update_mod_menu_element_inputbox(modMenuTimeModifier, msg) - djui_chat_message_create("[Day Night Cycle] Time set to " .. math_floor(gGlobalSyncTable.time / SECOND)) + command_message_create("[Day Night Cycle] Time set to " .. math_floor(gGlobalSyncTable.time / SECOND)) save_time() end @@ -397,13 +397,13 @@ end local function on_scale_command(msg) if dayNightCycleApi.lockTime then play_sound(SOUND_MENU_CAMERA_BUZZ, gGlobalSoundSource) - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.") + command_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.", CONSOLE_MESSAGE_ERROR) return end local scale = tonumber(msg) if scale == nil then - djui_chat_message_create("/time \\#00ffff\\scale\\#ffff00\\ [SCALE]\\#dcdcdc\\ to scale the rate at which time passes") + command_message_create("/time \\#00ffff\\scale\\#ffff00\\ [SCALE]\\#dcdcdc\\ to scale the rate at which time passes") return end gGlobalSyncTable.timeScale = scale @@ -411,13 +411,13 @@ local function on_scale_command(msg) update_mod_menu_element_slider(modMenuTimeScale, scale) - djui_chat_message_create("[Day Night Cycle] Time scale set to " .. scale) + command_message_create("[Day Night Cycle] Time scale set to " .. scale) save_time() end local function on_query_command() - djui_chat_message_create(string.format("[Day Night Cycle] Time is %d (%s), day %d", math_floor(gGlobalSyncTable.time / SECOND), get_time_string(gGlobalSyncTable.time), get_day_count())) + command_message_create(string.format("[Day Night Cycle] Time is %d (%s), day %d", math_floor(gGlobalSyncTable.time / SECOND), get_time_string(gGlobalSyncTable.time), get_day_count())) end local function on_24h_command() @@ -430,11 +430,11 @@ end local function on_sync_command() if dayNightCycleApi.lockTime then play_sound(SOUND_MENU_CAMERA_BUZZ, gGlobalSoundSource) - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.") + command_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.", CONSOLE_MESSAGE_ERROR) return end - djui_chat_message_create("[Day Night Cycle] Attempting to sync in-game time with real life time...") + command_message_create("[Day Night Cycle] Attempting to sync in-game time with real life time...") local dateTime = get_date_and_time() gGlobalSyncTable.time = get_day_count() * (MINUTE * 24) + (MINUTE * dateTime.hour) + (SECOND * dateTime.minute) @@ -446,25 +446,25 @@ end local function on_sync_sun_command() if dayNightCycleApi.lockTime then play_sound(SOUND_MENU_CAMERA_BUZZ, gGlobalSoundSource) - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.") + command_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.", CONSOLE_MESSAGE_ERROR) return end if dayNightCycleApi.lockSunHours then play_sound(SOUND_MENU_CAMERA_BUZZ, gGlobalSoundSource) - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] Changing sun hours has been locked by another mod.") + command_message_create("\\#ffa0a0\\[Day Night Cycle] Changing sun hours has been locked by another mod.", CONSOLE_MESSAGE_ERROR) return end syncSun = not syncSun mod_storage_save_bool("sync_sun", syncSun) if syncSun then - djui_chat_message_create("[Day Night Cycle] Syncing sunrise and sunset times to real life...") + command_message_create("[Day Night Cycle] Syncing sunrise and sunset times to real life...") local month = get_date_and_time().month + 1 set_sun_hours(gSunriseTimes[month], gSunsetTimes[month]) else - djui_chat_message_create("[Day Night Cycle] Resetting sunrise and sunset times...") + command_message_create("[Day Night Cycle] Resetting sunrise and sunset times...") set_sun_hours(HOUR_SUNRISE_START_BASE, HOUR_SUNSET_START_BASE) end @@ -475,13 +475,13 @@ end local function on_music_command() if dayNightCycleApi.lockTime then play_sound(SOUND_MENU_CAMERA_BUZZ, gGlobalSoundSource) - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.") + command_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.", CONSOLE_MESSAGE_ERROR) return end playNightMusic = not playNightMusic mod_storage_save_bool("night_music", playNightMusic) - djui_chat_message_create("[Day Night Cycle] Night music status: " .. on_or_off(playNightMusic)) + command_message_create("[Day Night Cycle] Night music status: " .. on_or_off(playNightMusic)) update_mod_menu_element_checkbox(modMenuMusic, playNightMusic) end @@ -489,13 +489,13 @@ end local function on_display_time_command() if dayNightCycleApi.lockTime then play_sound(SOUND_MENU_CAMERA_BUZZ, gGlobalSoundSource) - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.") + command_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.", CONSOLE_MESSAGE_ERROR) return end displayTime = not displayTime mod_storage_save_bool("display_time", displayTime) - djui_chat_message_create("[Day Night Cycle] Display time status: " .. on_or_off(displayTime)) + command_message_create("[Day Night Cycle] Display time status: " .. on_or_off(displayTime)) update_mod_menu_element_checkbox(modMenuDisplayTime, displayTime) end @@ -506,19 +506,19 @@ local function on_time_command(msg) if args[1] == "set" then if not network_is_server() then - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] You do not have permission to run /time set") + command_message_create("\\#ffa0a0\\[Day Night Cycle] You do not have permission to run /time set", CONSOLE_MESSAGE_ERROR) else on_set_command(args[2] or "") end elseif args[1] == "add" then if not network_is_server() then - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] You do not have permission to run /time add") + command_message_create("\\#ffa0a0\\[Day Night Cycle] You do not have permission to run /time add", CONSOLE_MESSAGE_ERROR) else on_add_command(args[2] or "") end elseif args[1] == "scale" then if not network_is_server() then - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] You do not have permission to run /time scale") + command_message_create("\\#ffa0a0\\[Day Night Cycle] You do not have permission to run /time scale", CONSOLE_MESSAGE_ERROR) else on_scale_command(args[2] or "") end @@ -528,7 +528,7 @@ local function on_time_command(msg) on_24h_command() elseif args[1] == "sync" then if not network_is_server() then - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] You do not have permission to run /time sync") + command_message_create("\\#ffa0a0\\[Day Night Cycle] You do not have permission to run /time sync", CONSOLE_MESSAGE_ERROR) else on_sync_command() end @@ -539,13 +539,13 @@ local function on_time_command(msg) elseif args[1] == "display-time" then on_display_time_command() elseif args[1] ~= nil then - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] Unrecognized command '" .. args[1] .. "'") + command_message_create("\\#ffa0a0\\[Day Night Cycle] Unrecognized command '" .. args[1] .. "'", CONSOLE_MESSAGE_ERROR) else if not network_is_server() then - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] You do not have permission to enable or disable Day Night Cycle") + command_message_create("\\#ffa0a0\\[Day Night Cycle] You do not have permission to enable or disable Day Night Cycle", CONSOLE_MESSAGE_ERROR) else gGlobalSyncTable.dncEnabled = not gGlobalSyncTable.dncEnabled - djui_chat_message_create("[Day Night Cycle] Status: " .. on_or_off(gGlobalSyncTable.dncEnabled)) + command_message_create("[Day Night Cycle] Status: " .. on_or_off(gGlobalSyncTable.dncEnabled)) end end @@ -586,7 +586,7 @@ end local function on_set_dnc_enabled(_, value) if dayNightCycleApi.lockTime then play_sound(SOUND_MENU_CAMERA_BUZZ, gGlobalSoundSource) - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.") + command_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.", CONSOLE_MESSAGE_ERROR) return end @@ -597,7 +597,7 @@ end local function on_set_time_scale(index, value) if dayNightCycleApi.lockTime then play_sound(SOUND_MENU_CAMERA_BUZZ, gGlobalSoundSource) - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.") + command_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.", CONSOLE_MESSAGE_ERROR) return end @@ -610,7 +610,7 @@ end local function on_set_time_modifier(_, value) if dayNightCycleApi.lockTime then play_sound(SOUND_MENU_CAMERA_BUZZ, gGlobalSoundSource) - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.") + command_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.", CONSOLE_MESSAGE_ERROR) return end @@ -621,7 +621,7 @@ end local function on_add_hour() if dayNightCycleApi.lockTime then play_sound(SOUND_MENU_CAMERA_BUZZ, gGlobalSoundSource) - djui_chat_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.") + command_message_create("\\#ffa0a0\\[Day Night Cycle] The Day Night Cycle settings have been locked by another mod.", CONSOLE_MESSAGE_ERROR) return end diff --git a/mods/hide-and-seek.lua b/mods/hide-and-seek.lua index d72890a6b..a05f329ea 100644 --- a/mods/hide-and-seek.lua +++ b/mods/hide-and-seek.lua @@ -389,31 +389,31 @@ end local function on_touch_tag_command() gGlobalSyncTable.touchTag = not gGlobalSyncTable.touchTag - djui_chat_message_create("Touch tag: " .. on_or_off(gGlobalSyncTable.touchTag)) + command_message_create("Touch tag: " .. on_or_off(gGlobalSyncTable.touchTag)) return true end local function on_hider_cap_command() gGlobalSyncTable.hiderCaps = not gGlobalSyncTable.hiderCaps - djui_chat_message_create("Hider Caps: " .. on_or_off(gGlobalSyncTable.hiderCaps)) + command_message_create("Hider Caps: " .. on_or_off(gGlobalSyncTable.hiderCaps)) return true end local function on_seeker_cap_command() gGlobalSyncTable.seekerCaps = not gGlobalSyncTable.seekerCaps - djui_chat_message_create("Seeker Caps: " .. on_or_off(gGlobalSyncTable.seekerCaps)) + command_message_create("Seeker Caps: " .. on_or_off(gGlobalSyncTable.seekerCaps)) return true end local function on_koopa_shell_command() gGlobalSyncTable.banKoopaShell = not gGlobalSyncTable.banKoopaShell - djui_chat_message_create("Koopa Shells: " .. on_or_off(not gGlobalSyncTable.banKoopaShell)) + command_message_create("Koopa Shells: " .. on_or_off(not gGlobalSyncTable.banKoopaShell)) return true end local function on_blj_command() gGlobalSyncTable.disableBLJ = not gGlobalSyncTable.disableBLJ - djui_chat_message_create("BLJS: " .. on_or_off(not gGlobalSyncTable.disableBLJ)) + command_message_create("BLJS: " .. on_or_off(not gGlobalSyncTable.disableBLJ)) return true end diff --git a/mods/sm74/main.lua b/mods/sm74/main.lua index 493f086a7..45d21b1d3 100644 --- a/mods/sm74/main.lua +++ b/mods/sm74/main.lua @@ -23,9 +23,9 @@ end local function on_swap_command() local np = gNetworkPlayers[0] if np.currAreaIndex == 1 then - djui_chat_message_create("Swapping to Extreme Edition") + command_message_create("Swapping to Extreme Edition") else - djui_chat_message_create("Swapping to normal edition") + command_message_create("Swapping to normal edition") end warp_to_level(np.currLevelNum, np.currAreaIndex ~ 3, np.currActNum) return true diff --git a/src/game/player_palette.c b/src/game/player_palette.c index 9300df262..e021010cc 100644 --- a/src/game/player_palette.c +++ b/src/game/player_palette.c @@ -1,6 +1,7 @@ #include #include #include "pc/ini.h" +#include "pc/debuglog.h" #include "pc/mods/mods.h" #include "pc/mods/mods_utils.h" #include "player_palette.h" @@ -118,7 +119,7 @@ void player_palettes_read(const char* palettesPath, bool appendPalettes) { if (!player_palette_init(palettesPath, path, appendPalettes)) { #ifdef DEVELOPMENT - printf("Failed to load palette '%s.ini'\n", path); + LOG_ERROR("Failed to load palette '%s.ini'\n", path); #endif continue; } @@ -140,7 +141,7 @@ void player_palettes_read(const char* palettesPath, bool appendPalettes) { gPresetPalettes[gPresetPaletteCount].palette = palette; gPresetPaletteCount++; #ifdef DEVELOPMENT - printf("Loaded palette '%s.ini'\n", path); + LOG_INFO("Loaded palette '%s.ini'\n", path); #endif if (gPresetPaletteCount >= MAX_PRESET_PALETTES) { break; } } @@ -160,7 +161,7 @@ void player_palette_export(char* name) { snprintf(ppath, SYS_MAX_PATH, "%s/%s.ini", palettesPath, name); fs_sys_mkdir(palettesPath); - printf("Saving palette as '%s.ini'\n", name); + LOG_INFO("Saving palette as '%s.ini'\n", name); FILE* file = fopen(ppath, "w"); fprintf(file, "[PALETTE]\n\ PANTS_R = %d\n\ @@ -224,7 +225,7 @@ bool player_palette_delete(const char* palettesPath, char* name, bool appendPale } if (remove(ppath) == 0) { - printf("Deleting palette '%s.ini'\n", name); + LOG_INFO("Deleting palette '%s.ini'\n", name); return true; } return false; diff --git a/src/pc/chat_commands.c b/src/pc/chat_commands.c deleted file mode 100644 index adb2bec1e..000000000 --- a/src/pc/chat_commands.c +++ /dev/null @@ -1,276 +0,0 @@ -#include "pc/network/network.h" -#include "pc/network/socket/socket.h" -#include "pc/lua/smlua_hooks.h" -#include "pc/djui/djui_language.h" -#include "pc/djui/djui_chat_message.h" -#include "chat_commands.h" -#include "pc/network/ban_list.h" -#include "pc/network/moderator_list.h" -#include "pc/debuglog.h" -#include "pc/lua/utils/smlua_level_utils.h" -#include "pc/mods/mods_utils.h" -#include "level_table.h" -#ifdef DEVELOPMENT -#include "pc/dev/chat.h" -#endif - -static enum ChatConfirmCommand sConfirming = CCC_NONE; -static u8 sConfirmPlayerIndex = 0; - -static struct NetworkPlayer* chat_get_network_player(const char* name) { - // check for id - for (s32 i = 0; i < MAX_PLAYERS; i++) { - if (!gNetworkPlayers[i].connected) { continue; } - char id[16] = { 0 }; - if (snprintf(id, 16, "%d", i) < 0) { - // do nothing - } - if (strcmp(id, name) == 0) { - return &gNetworkPlayers[i]; - } - } - - // check for name - for (s32 i = 0; i < MAX_PLAYERS; i++) { - if (!gNetworkPlayers[i].connected) { continue; } - if (strcmp(gNetworkPlayers[i].name, name) == 0) { - return &gNetworkPlayers[i]; - } - } - return NULL; -} - -static void chat_construct_player_message(struct NetworkPlayer* np, char* msg) { - char built[256] = { 0 }; - snprintf(built, 256, "\\#fff982\\"); - - char player[128] = { 0 }; - snprintf(player, 128, "%s%s\\#fff982\\", network_get_player_text_color_string(np->localIndex), np->name); - djui_language_replace(msg, &built[9], 256 - 9, '@', player); - djui_chat_message_create(built); -} - -bool exec_chat_command(char* command) { - struct NetworkPlayer* npl = &gNetworkPlayers[0]; - enum ChatConfirmCommand ccc = sConfirming; - sConfirming = CCC_NONE; - - if (ccc != CCC_NONE && strcmp("/confirm", command) == 0) { - struct NetworkPlayer* np = &gNetworkPlayers[sConfirmPlayerIndex]; - if (!np->connected) return true; - if (gNetworkType == NT_SERVER || npl->moderator) { - if (ccc == CCC_KICK) { - chat_construct_player_message(np, DLANG(CHAT, KICKING)); - if (gNetworkType == NT_SERVER) { - network_send_kick(np->localIndex, EKT_KICKED); - network_player_disconnected(np->localIndex); - } else { - network_send_chat_command(np->globalIndex, CCC_KICK); - } - return true; - } - } - if (gNetworkType == NT_SERVER || npl->moderator) { - if (ccc == CCC_BAN) { - chat_construct_player_message(np, DLANG(CHAT, BANNING)); - if (gNetworkType == NT_SERVER) { - network_send_kick(np->localIndex, EKT_BANNED); - ban_list_add(gNetworkSystem->get_id_str(np->localIndex), false); - network_player_disconnected(np->localIndex); - } else { - network_send_chat_command(np->globalIndex, CCC_BAN); - } - return true; - } - } - if (gNetworkType == NT_SERVER && ccc == CCC_PERMBAN) { - chat_construct_player_message(np, DLANG(CHAT, PERM_BANNING)); - network_send_kick(np->localIndex, EKT_BANNED); - ban_list_add(gNetworkSystem->get_id_str(np->localIndex), true); - network_player_disconnected(np->localIndex); - return true; - } - if (gNetworkType == NT_SERVER && ccc == CCC_MODERATOR) { - chat_construct_player_message(np, DLANG(CHAT, ADD_MODERATOR)); - np->moderator = true; - network_send_moderator(np->localIndex); - moderator_list_add(gNetworkSystem->get_id_str(np->localIndex), true); - return true; - } - } - - if (strcmp("/players", command) == 0) { - char line[128] = { 0 }; - snprintf(line, 127, "\\#fff982\\%s:\n", DLANG(CHAT, PLAYERS)); - djui_chat_message_create(line); - for (s32 i = 0; i < MAX_PLAYERS; i++) { - struct NetworkPlayer* np = &gNetworkPlayers[i]; - if (!np->connected) { continue; } - if (gNetworkSystem == &gNetworkSystemSocket) { - snprintf(line, 127, "\\#82f9ff\\%u\\#fff982\\ - %s%s\n", np->globalIndex, network_get_player_text_color_string(np->localIndex), np->name); - } else { - snprintf(line, 127, "\\#82f9ff\\%u\\#fff982\\ - \\#82f9ff\\%s\\#fff982\\ - %s%s\n", np->globalIndex, gNetworkSystem->get_id_str(np->localIndex), network_get_player_text_color_string(np->localIndex), np->name); - } - djui_chat_message_create(line); - } - return true; - } - - if (strcmp("/kick", command) == 0) { - djui_chat_message_create(DLANG(CHAT, PLAYER_NOT_FOUND)); - return true; - } - - if (str_starts_with(command, "/kick ")) { - if (gNetworkType != NT_SERVER && !npl->moderator) { - djui_chat_message_create(DLANG(CHAT, NO_PERMS)); - return true; - } - - struct NetworkPlayer* np = chat_get_network_player(&command[6]); - if (np == NULL) { - djui_chat_message_create(DLANG(CHAT, PLAYER_NOT_FOUND)); - return true; - } - - if (np->localIndex == 0) { - djui_chat_message_create(DLANG(CHAT, SELF_KICK)); - return true; - } - chat_construct_player_message(np, DLANG(CHAT, KICK_CONFIRM)); - sConfirming = CCC_KICK; - sConfirmPlayerIndex = np->localIndex; - - return true; - } - - if (strcmp("/ban", command) == 0) { - djui_chat_message_create(DLANG(CHAT, PLAYER_NOT_FOUND)); - return true; - } - - if (str_starts_with(command, "/ban ")) { - if (gNetworkType != NT_SERVER && !npl->moderator) { - djui_chat_message_create(DLANG(CHAT, NO_PERMS)); - return true; - } - - struct NetworkPlayer* np = chat_get_network_player(&command[5]); - if (np == NULL) { - djui_chat_message_create(DLANG(CHAT, PLAYER_NOT_FOUND)); - return true; - } - - if (np->localIndex == 0) { - djui_chat_message_create(DLANG(CHAT, SELF_BAN)); - return true; - } - chat_construct_player_message(np, DLANG(CHAT, BAN_CONFIRM)); - sConfirming = CCC_BAN; - sConfirmPlayerIndex = np->localIndex; - - return true; - } - - if (strcmp("/permban", command) == 0) { - djui_chat_message_create(DLANG(CHAT, PLAYER_NOT_FOUND)); - return true; - } - - if (str_starts_with(command, "/permban ")) { - if (gNetworkType != NT_SERVER && !npl->moderator) { - djui_chat_message_create(DLANG(CHAT, NO_PERMS)); - return true; - } - - struct NetworkPlayer* np = chat_get_network_player(&command[9]); - if (np == NULL) { - djui_chat_message_create(DLANG(CHAT, PLAYER_NOT_FOUND)); - return true; - } - - if (np->localIndex == 0) { - djui_chat_message_create(DLANG(CHAT, SELF_BAN)); - return true; - } - chat_construct_player_message(np, DLANG(CHAT, PERM_BAN_CONFIRM)); - sConfirming = CCC_PERMBAN; - sConfirmPlayerIndex = np->localIndex; - - return true; - } - - if (strcmp("/moderator", command) == 0) { - djui_chat_message_create(DLANG(CHAT, PLAYER_NOT_FOUND)); - return true; - } - - if (str_starts_with(command, "/moderator ")) { - if (gNetworkType != NT_SERVER) { - djui_chat_message_create(DLANG(CHAT, SERVER_ONLY)); - return true; - } - - struct NetworkPlayer* np = chat_get_network_player(&command[11]); - if (np == NULL) { - djui_chat_message_create(DLANG(CHAT, PLAYER_NOT_FOUND)); - return true; - } - - if (np->localIndex == 0) { - djui_chat_message_create(DLANG(CHAT, SELF_MOD)); - return true; - } - chat_construct_player_message(np, DLANG(CHAT, MOD_CONFIRM)); - sConfirming = CCC_MODERATOR; - sConfirmPlayerIndex = np->localIndex; - - return true; - } - - if (gServerSettings.nametags) { - if (strcmp("/nametags", command) == 0) { - djui_chat_message_create(DLANG(CHAT, NAMETAGS_MISSING_PARAMETERS)); - return true; - } - - if (str_starts_with(command, "/nametags ")) { - char *option = &command[10]; - if (strcmp("show-tag", option) == 0) { - gNametagsSettings.showSelfTag = !gNametagsSettings.showSelfTag; - } else if (strcmp("show-health", option) == 0) { - gNametagsSettings.showHealth = !gNametagsSettings.showHealth; - } - return true; - } - } - -#ifdef DEVELOPMENT - if (exec_dev_chat_command(command)) { - return true; - } -#endif - - return smlua_call_chat_command_hook(command); -} - -void display_chat_commands(void) { - djui_chat_message_create(DLANG(CHAT, PLAYERS_DESC)); - if (gNetworkType == NT_SERVER || gNetworkPlayers[0].moderator) { - djui_chat_message_create(DLANG(CHAT, KICK_DESC)); - djui_chat_message_create(DLANG(CHAT, BAN_DESC)); - - if (gNetworkType == NT_SERVER) { - djui_chat_message_create(DLANG(CHAT, PERM_BAN_DESC)); - djui_chat_message_create(DLANG(CHAT, MOD_DESC)); - } - } - if (gServerSettings.nametags) { - djui_chat_message_create(DLANG(CHAT, NAMETAGS_DESC)); - } -#ifdef DEVELOPMENT - dev_display_chat_commands(); -#endif - if (sConfirming != CCC_NONE) { djui_chat_message_create("/confirm"); } - smlua_display_chat_commands(); -} diff --git a/src/pc/chat_commands.h b/src/pc/chat_commands.h deleted file mode 100644 index 9614dbed8..000000000 --- a/src/pc/chat_commands.h +++ /dev/null @@ -1,7 +0,0 @@ -#ifndef CHAT_COMMANDS_H -#define CHAT_COMMANDS_H - -bool exec_chat_command(char* command); -void display_chat_commands(void); - -#endif \ No newline at end of file diff --git a/src/pc/cliopts.c b/src/pc/cliopts.c index f8bab9c2f..5d0ba1b9f 100644 --- a/src/pc/cliopts.c +++ b/src/pc/cliopts.c @@ -3,6 +3,7 @@ #include "pc_main.h" #include "platform.h" #include "macros.h" +#include "debuglog.h" #include #include @@ -13,28 +14,28 @@ struct CLIOptions gCLIOpts; static void print_help(void) { - printf("sm64coopdx\n"); + log_to_terminal("sm64coopdx\n"); #if defined(_WIN32) || defined(_WIN64) - printf("--console Enables the Windows console.\n"); + log_to_terminal("--console Enables the Windows console.\n"); #endif - printf("--savepath SAVEPATH Overrides the default save/config path ('!' expands to executable path).\n"); - printf("--configfile CONFIGNAME Saves the configuration file as CONFIGNAME.\n"); - printf("--hide-loading-screen Hides the loading screen before the menu boots up.\n"); - printf("--fullscreen Starts the game in full screen mode.\n"); - printf("--windowed Starts the game in windowed mode.\n"); - printf("--width WIDTH Sets the window width.\n"); - printf("--height HEIGHT Sets the window height.\n"); - printf("--skip-intro Skips the Peach and Lakitu intros when on a zero star save.\n"); - printf("--server PORT Starts the game and creates a new server on PORT.\n"); - printf("--client IP PORT Starts the game and joins an existing server.\n"); - printf("--coopnet PASSWORD Starts the game and creates a new CoopNet server.\n"); - printf("--playername PLAYERNAME Starts the game with a specific playername.\n"); - printf("--playercount PLAYERCOUNT Starts the game with a specific player count limit.\n"); - printf("--skip-update-check Skips the update check when loading the game.\n"); - printf("--no-discord Disables discord integration.\n"); - printf("--disable-mods Disables all mods that are already enabled.\n"); - printf("--enable-mod MODNAME Enables a mod.\n"); - printf("--headless Enable Headless mode."); + log_to_terminal("--savepath SAVEPATH Overrides the default save/config path ('!' expands to executable path).\n"); + log_to_terminal("--configfile CONFIGNAME Saves the configuration file as CONFIGNAME.\n"); + log_to_terminal("--hide-loading-screen Hides the loading screen before the menu boots up.\n"); + log_to_terminal("--fullscreen Starts the game in full screen mode.\n"); + log_to_terminal("--windowed Starts the game in windowed mode.\n"); + log_to_terminal("--width WIDTH Sets the window width.\n"); + log_to_terminal("--height HEIGHT Sets the window height.\n"); + log_to_terminal("--skip-intro Skips the Peach and Lakitu intros when on a zero star save.\n"); + log_to_terminal("--server PORT Starts the game and creates a new server on PORT.\n"); + log_to_terminal("--client IP PORT Starts the game and joins an existing server.\n"); + log_to_terminal("--coopnet PASSWORD Starts the game and creates a new CoopNet server.\n"); + log_to_terminal("--playername PLAYERNAME Starts the game with a specific playername.\n"); + log_to_terminal("--playercount PLAYERCOUNT Starts the game with a specific player count limit.\n"); + log_to_terminal("--skip-update-check Skips the update check when loading the game.\n"); + log_to_terminal("--no-discord Disables discord integration.\n"); + log_to_terminal("--disable-mods Disables all mods that are already enabled.\n"); + log_to_terminal("--enable-mod MODNAME Enables a mod.\n"); + log_to_terminal("--headless Enable Headless mode."); } static inline int arg_string(const char *name, const char *value, char *target, int maxLength) { diff --git a/src/pc/commands.c b/src/pc/commands.c new file mode 100644 index 000000000..9195b92e1 --- /dev/null +++ b/src/pc/commands.c @@ -0,0 +1,483 @@ +#include "pc/network/network.h" +#include "pc/network/socket/socket.h" +#include "pc/lua/smlua_hooks.h" +#include "pc/djui/djui_language.h" +#include "pc/djui/djui_chat_message.h" +#include "pc/djui/djui_console.h" +#include "commands.h" +#include "pc/network/ban_list.h" +#include "pc/network/moderator_list.h" +#include "pc/debuglog.h" +#include "pc/lua/utils/smlua_level_utils.h" +#include "pc/mods/mods_utils.h" +#include "pc/pc_main.h" +#include "level_table.h" +#ifdef DEVELOPMENT +#include "pc/dev/chat.h" +#endif + +static bool command_help(UNUSED const char* message); +static bool command_players(UNUSED const char* message); +static bool command_kick(const char* message); +static bool command_ban(const char* message); +static bool command_permaban(const char* message); +static bool command_mod(const char* message); +static bool command_confirm(UNUSED const char* message); +static bool command_nametags(const char* message); +static bool command_clear(UNUSED const char* message); +static bool command_quit(UNUSED const char* message); +static bool command_host(UNUSED const char* message); +static bool command_rehost(UNUSED const char* message); +static bool command_stop_hosting(UNUSED const char* message); +static bool command_disconnect(UNUSED const char* message); + +static struct Command sCommands[] = { + { + .command = "players", + .description = "PLAYERS_DESC", + .action = command_players, + .active = true, + .isChatCommand = true + }, + { + .command = "kick", + .description = "KICK_DESC", + .action = command_kick, + .active = true, + .isChatCommand = true + }, + { + .command = "ban", + .description = "BAN_DESC", + .action = command_ban, + .active = true, + .isChatCommand = true + }, + { + .command = "permaban", + .description = "PERM_BAN_DESC", + .action = command_permaban, + .active = true, + .isChatCommand = true + }, + { + .command = "moderator", + .description = "MOD_DESC", + .action = command_mod, + .active = true, + .isChatCommand = true + }, + { + .command = "confirm", + .description = "", + .action = command_confirm, + .active = false, + .isChatCommand = true + }, + { + .command = "nametags", + .description = "NAMETAGS_DESC", + .action = command_nametags, + .active = false, + .isChatCommand = true + }, + { + .command = "clear", + .description = "/clear - Clears the current console's text", + .action = command_clear, + .active = true, + .isChatCommand = false + }, + { + .command = "quit", + .description = "/quit - Quits the game", + .action = command_quit, + .active = true, + .isChatCommand = false + }, + { + .command = "host", + .description = "/host - Hosts a new game using your saved server settings and mod list", + .action = command_host, + .active = true, + .isChatCommand = false + }, + { + .command = "rehost", + .description = "/rehost - Rehosts a currently active game", + .action = command_rehost, + .active = false, + .isChatCommand = false + }, + { + .command = "stop-hosting", + .description = "/stop-hosting - Stop hosting a currently active game", + .action = command_stop_hosting, + .active = false, + .isChatCommand = false + }, + { + .command = "disconnect", + .description = "/disconnect - Disconnects from a currently active game", + .action = command_disconnect, + .active = false, + .isChatCommand = false + }, +}; +static unsigned int sCommandCount = sizeof(sCommands) / sizeof(struct Command); +static enum ChatConfirmCommand sConfirming = CCC_NONE; +static u8 sConfirmPlayerIndex = 0; + +static struct NetworkPlayer* chat_get_network_player(const char* name) { + // check for id + for (s32 i = 0; i < MAX_PLAYERS; i++) { + if (!gNetworkPlayers[i].connected) { continue; } + char id[16] = { 0 }; + if (snprintf(id, 16, "%d", i) < 0) { + // do nothing + } + if (strcmp(id, name) == 0) { + return &gNetworkPlayers[i]; + } + } + + // check for name + for (s32 i = 0; i < MAX_PLAYERS; i++) { + if (!gNetworkPlayers[i].connected) { continue; } + if (strcmp(gNetworkPlayers[i].name, name) == 0) { + return &gNetworkPlayers[i]; + } + } + return NULL; +} + +static void chat_construct_player_message(struct NetworkPlayer* np, char* msg) { + char built[256] = { 0 }; + snprintf(built, 256, "\\#fff982\\"); + + char player[128] = { 0 }; + snprintf(player, 128, "%s%s\\#fff982\\", network_get_player_text_color_string(np->localIndex), np->name); + djui_language_replace(msg, &built[9], 256 - 9, '@', player); + command_message_create(built, CONSOLE_MESSAGE_INFO); +} + +static bool command_help(UNUSED const char* message) { + for (unsigned int i = 0; i < sCommandCount; i++) { + if (!sCommands[i].active) continue; + if (!sCommands[i].isChatCommand && gDjuiChatBoxFocus) continue; + command_message_create(djui_language_get("CHAT", sCommands[i].description), CONSOLE_MESSAGE_INFO); + } +#ifdef DEVELOPMENT + dev_display_chat_commands(); +#endif + smlua_display_chat_commands(gDjuiConsoleFocus); + return true; +} + +static bool command_players(UNUSED const char* message) { + char line[128] = { 0 }; + snprintf(line, 127, "\\#fff982\\%s:\n", DLANG(CHAT, PLAYERS)); + command_message_create(line, CONSOLE_MESSAGE_INFO); + for (s32 i = 0; i < MAX_PLAYERS; i++) { + struct NetworkPlayer* np = &gNetworkPlayers[i]; + if (!np->connected) { continue; } + if (gNetworkSystem == &gNetworkSystemSocket) { + snprintf(line, 127, "\\#82f9ff\\%u\\#fff982\\ - %s%s\n", np->globalIndex, network_get_player_text_color_string(np->localIndex), np->name); + } else { + snprintf(line, 127, "\\#82f9ff\\%u\\#fff982\\ - \\#82f9ff\\%s\\#fff982\\ - %s%s\n", np->globalIndex, gNetworkSystem->get_id_str(np->localIndex), network_get_player_text_color_string(np->localIndex), np->name); + } + command_message_create(line, CONSOLE_MESSAGE_INFO); + } + return true; +} + +static bool command_kick(const char* message) { + struct NetworkPlayer* npl = &gNetworkPlayers[0]; + if (gNetworkType != NT_SERVER && !npl->moderator) { + command_message_create(DLANG(CHAT, NO_PERMS), CONSOLE_MESSAGE_ERROR); + return true; + } + + struct NetworkPlayer* np = chat_get_network_player(message); + if (np == NULL) { + command_message_create(DLANG(CHAT, PLAYER_NOT_FOUND), CONSOLE_MESSAGE_ERROR); + return true; + } + + if (np->localIndex == 0) { + command_message_create(DLANG(CHAT, SELF_KICK), CONSOLE_MESSAGE_ERROR); + return true; + } + chat_construct_player_message(np, DLANG(CHAT, KICK_CONFIRM)); + sConfirming = CCC_KICK; + sConfirmPlayerIndex = np->localIndex; + struct Command* confirmCommand = get_command("confirm"); + if (confirmCommand) confirmCommand->active = true; + return true; +} + +static bool command_ban(const char* message) { + struct NetworkPlayer* npl = &gNetworkPlayers[0]; + if (gNetworkType != NT_SERVER && !npl->moderator) { + command_message_create(DLANG(CHAT, NO_PERMS), CONSOLE_MESSAGE_ERROR); + return true; + } + + struct NetworkPlayer* np = chat_get_network_player(message); + if (np == NULL) { + command_message_create(DLANG(CHAT, PLAYER_NOT_FOUND), CONSOLE_MESSAGE_ERROR); + return true; + } + + if (np->localIndex == 0) { + command_message_create(DLANG(CHAT, SELF_BAN), CONSOLE_MESSAGE_ERROR); + return true; + } + chat_construct_player_message(np, DLANG(CHAT, BAN_CONFIRM)); + sConfirming = CCC_BAN; + sConfirmPlayerIndex = np->localIndex; + struct Command* confirmCommand = get_command("confirm"); + if (confirmCommand) confirmCommand->active = true; + return true; +} + +static bool command_permaban(const char* message) { + struct NetworkPlayer* npl = &gNetworkPlayers[0]; + if (gNetworkType != NT_SERVER && !npl->moderator) { + command_message_create(DLANG(CHAT, NO_PERMS), CONSOLE_MESSAGE_ERROR); + return true; + } + + struct NetworkPlayer* np = chat_get_network_player(message); + if (np == NULL) { + command_message_create(DLANG(CHAT, PLAYER_NOT_FOUND), CONSOLE_MESSAGE_ERROR); + return true; + } + + if (np->localIndex == 0) { + command_message_create(DLANG(CHAT, SELF_BAN), CONSOLE_MESSAGE_ERROR); + return true; + } + chat_construct_player_message(np, DLANG(CHAT, PERM_BAN_CONFIRM)); + sConfirming = CCC_PERMBAN; + sConfirmPlayerIndex = np->localIndex; + struct Command* confirmCommand = get_command("confirm"); + if (confirmCommand) confirmCommand->active = true; + return true; +} + +static bool command_mod(const char* message) { + if (gNetworkType != NT_SERVER) { + command_message_create(DLANG(CHAT, SERVER_ONLY), CONSOLE_MESSAGE_ERROR); + return true; + } + + struct NetworkPlayer* np = chat_get_network_player(message); + if (np == NULL) { + command_message_create(DLANG(CHAT, PLAYER_NOT_FOUND), CONSOLE_MESSAGE_ERROR); + return true; + } + + if (np->localIndex == 0) { + command_message_create(DLANG(CHAT, SELF_MOD), CONSOLE_MESSAGE_ERROR); + return true; + } + chat_construct_player_message(np, DLANG(CHAT, MOD_CONFIRM)); + sConfirming = CCC_MODERATOR; + sConfirmPlayerIndex = np->localIndex; + struct Command* confirmCommand = get_command("confirm"); + if (confirmCommand) confirmCommand->active = true; + return true; +} + +static bool command_confirm(UNUSED const char* message) { + // deactivate command + struct Command* confirmCommand = get_command("confirm"); + if (confirmCommand) confirmCommand->active = false; + + enum ChatConfirmCommand ccc = sConfirming; + sConfirming = CCC_NONE; + + struct NetworkPlayer* npl = &gNetworkPlayers[0]; + struct NetworkPlayer* np = &gNetworkPlayers[sConfirmPlayerIndex]; + if (!np->connected) return true; + if (gNetworkType == NT_SERVER || npl->moderator) { + if (ccc == CCC_KICK) { + chat_construct_player_message(np, DLANG(CHAT, KICKING)); + if (gNetworkType == NT_SERVER) { + network_send_kick(np->localIndex, EKT_KICKED); + network_player_disconnected(np->localIndex); + } else { + network_send_chat_command(np->globalIndex, CCC_KICK); + } + return true; + } + } + if (gNetworkType == NT_SERVER || npl->moderator) { + if (ccc == CCC_BAN) { + chat_construct_player_message(np, DLANG(CHAT, BANNING)); + if (gNetworkType == NT_SERVER) { + network_send_kick(np->localIndex, EKT_BANNED); + ban_list_add(gNetworkSystem->get_id_str(np->localIndex), false); + network_player_disconnected(np->localIndex); + } else { + network_send_chat_command(np->globalIndex, CCC_BAN); + } + return true; + } + } + if (gNetworkType == NT_SERVER && ccc == CCC_PERMBAN) { + chat_construct_player_message(np, DLANG(CHAT, PERM_BANNING)); + network_send_kick(np->localIndex, EKT_BANNED); + ban_list_add(gNetworkSystem->get_id_str(np->localIndex), true); + network_player_disconnected(np->localIndex); + return true; + } + if (gNetworkType == NT_SERVER && ccc == CCC_MODERATOR) { + chat_construct_player_message(np, DLANG(CHAT, ADD_MODERATOR)); + np->moderator = true; + network_send_moderator(np->localIndex); + moderator_list_add(gNetworkSystem->get_id_str(np->localIndex), true); + return true; + } + return false; +} + +static bool command_nametags(const char* message) { + if (strcmp("show-tag", message) == 0) { + gNametagsSettings.showSelfTag = !gNametagsSettings.showSelfTag; + } else if (strcmp("show-health", message) == 0) { + gNametagsSettings.showHealth = !gNametagsSettings.showHealth; + } + command_message_create(DLANG(CHAT, NAMETAGS_MISSING_PARAMETERS), CONSOLE_MESSAGE_ERROR); + return true; +} + +static bool command_clear(UNUSED const char* message) { + djui_console_clear(); + terminal_clear(); + return true; +} + +static bool command_quit(UNUSED const char* message) { + game_exit(); + return true; +} + +extern void djui_panel_do_host(bool reconnecting, bool playSound); +static bool command_host(UNUSED const char* message) { + djui_panel_do_host(false, true); + return true; +} + +static bool command_rehost(UNUSED const char* message) { + network_rehost_begin(); + return true; +} + +static bool command_stop_hosting(UNUSED const char* message) { + network_reset_reconnect_and_rehost(); + network_shutdown(true, false, false, false); + return true; +} + +static bool command_disconnect(UNUSED const char* message) { + network_reset_reconnect_and_rehost(); + network_shutdown(true, false, false, false); + return true; +} + +static void set_command_active(const char* name, bool active) { + struct Command* command = get_command(name); + if (command) command->active = active; +} + +struct Command* get_command(const char* name) { + for (unsigned int i = 0; i < sCommandCount; i++) { + if (strcmp(sCommands[i].command, name) == 0) { + return &sCommands[i]; + } + } + return NULL; +} + + +void command_message_create(const char* message, OPTIONAL enum ConsoleMessageLevel level) { + if (gDjuiChatBoxFocus) { + size_t newMsgLength = strlen(message) + 12; + char newMsg[newMsgLength]; + switch (level) { + case CONSOLE_MESSAGE_INFO: + snprintf(newMsg, newMsgLength, "\\#dcdcdc\\%s", message); + break; + case CONSOLE_MESSAGE_WARNING: + snprintf(newMsg, newMsgLength, "\\#ffffa0\\%s", message); + break; + case CONSOLE_MESSAGE_ERROR: + snprintf(newMsg, newMsgLength, "\\#ffa0a0\\%s", message); + break; + default: + snprintf(newMsg, newMsgLength, "\\#dcdcdc\\%s", message); + break; + } + djui_chat_message_create(newMsg); + } else { + djui_console_message_create(message, level); + char* colorCode; + switch (level) { + case CONSOLE_MESSAGE_WARNING: colorCode = "\x1b[33m"; break; + case CONSOLE_MESSAGE_ERROR: colorCode = "\x1b[31m"; break; + default: colorCode = "\x1b[0m"; break; + } + log_to_terminal("%s%s\x1b[0m\n", colorCode, message); + } +} + +void run_command(char* command) { + // directly set active state of certain commands + set_command_active("nametags", gServerSettings.nametags); + set_command_active("host", gDjuiInMainMenu); + set_command_active("rehost", gNetworkType == NT_SERVER && !gDjuiInMainMenu); + set_command_active("stop-hosting", gNetworkType == NT_SERVER && !gDjuiInMainMenu); + set_command_active("disconnect", gNetworkType == NT_CLIENT && !gDjuiInMainMenu); + + // directly check help command + if (strcmp(command, "help") == 0 || strcmp(command, "?") == 0 || strcmp(command, "") == 0) { + command_help(NULL); + return; + } + + // loop through builtin commands first + for (unsigned int i = 0; i < sCommandCount; i++) { + // sanity checks + if (sCommands[i].command[0] == '\0') continue; + if (!sCommands[i].action) continue; + if (!sCommands[i].active) continue; + if (!sCommands[i].isChatCommand && gDjuiChatBoxFocus) continue; + + // compare strings + size_t commandLength = strlen(sCommands[i].command); + if (!str_starts_with(command, sCommands[i].command)) continue; + if (command[commandLength] != '\0' && command[commandLength] != ' ') continue; + + // get args + char* arguments = command + commandLength; + if (*arguments != '\0') arguments++; + + // run action + if (sCommands[i].action(arguments)) return; + } + +#ifdef DEVELOPMENT + // check development commands + if (exec_dev_chat_command(command)) return; +#endif + + // check lua commands + if (smlua_call_chat_command_hook(command)) return; + + // no command exists, alert the user + char extendedUnknownCommandMessage[MAX_CONSOLE_INPUT_LENGTH]; + snprintf(extendedUnknownCommandMessage, sizeof(extendedUnknownCommandMessage), "%s (/help)", DLANG(CHAT, UNRECOGNIZED)); + command_message_create(extendedUnknownCommandMessage, CONSOLE_MESSAGE_INFO); +} diff --git a/src/pc/commands.h b/src/pc/commands.h new file mode 100644 index 000000000..997c6ae40 --- /dev/null +++ b/src/pc/commands.h @@ -0,0 +1,17 @@ +#pragma once + +#define MAX_COMMAND_LEN 128 +// only applies to builtin commands +#define MAX_COMMAND_DESC_LEN 512 + +struct Command { + char command[MAX_COMMAND_LEN]; + char description[MAX_COMMAND_DESC_LEN]; + bool (*action)(const char*); + bool active; + bool isChatCommand; +}; + +struct Command* get_command(const char* name); +void run_command(char* command); +void command_message_create(const char* message, OPTIONAL enum ConsoleMessageLevel level); \ No newline at end of file diff --git a/src/pc/configfile.c b/src/pc/configfile.c index 4e52b50b6..34664bb5f 100644 --- a/src/pc/configfile.c +++ b/src/pc/configfile.c @@ -879,7 +879,7 @@ void configfile_save(const char *filename) { return; } - printf("Saving configuration to '%s'\n", filename); + LOG_INFO("Saving configuration to '%s'\n", filename); for (unsigned int i = 0; i < ARRAY_LEN(options); i++) { const struct ConfigOption *option = &options[i]; diff --git a/src/pc/debuglog.h b/src/pc/debuglog.h index 04e238c14..d7f1a93a2 100644 --- a/src/pc/debuglog.h +++ b/src/pc/debuglog.h @@ -5,8 +5,11 @@ #include #include "pc/network/network.h" #include "pc/djui/djui_console.h" +#include "pc/terminal.h" -static void _debuglog_print_timestamp(void) { +#define MAX_LOG_SIZE 8192 + +static int _debuglog_print_timestamp(char* buffer, size_t bufferSize) { time_t ltime = time(NULL); #if defined(_WIN32) char* str = asctime(localtime(<ime)); @@ -15,32 +18,64 @@ static void _debuglog_print_timestamp(void) { localtime_r(<ime, <ime2); char* str = asctime(<ime2); #endif - printf("%.*s", (int)strlen(str) - 1, str); + return snprintf(buffer, bufferSize, "%.*s", (int)strlen(str) - 1, str); } -static void _debuglog_print_network_type(void) { - printf(" [%02d] ", (gNetworkPlayerLocal != NULL) ? gNetworkPlayerLocal->globalIndex : -1); +static int _debuglog_print_network_type(char* buffer, size_t bufferSize) { + return snprintf(buffer, bufferSize, " [%02d] ", (gNetworkPlayerLocal != NULL) ? gNetworkPlayerLocal->globalIndex : -1); } -static void _debuglog_print_log_type(const char* logType) { - printf("[%s] ", logType); +static int _debuglog_print_log_type(const char* logType, char* buffer, size_t bufferSize) { + return snprintf(buffer, bufferSize, "[%s] ", logType); } -static void _debuglog_print_short_filename(const char* filename) { +static int _debuglog_print_short_filename(const char* filename, char* buffer, size_t bufferSize) { const char* last = strrchr(filename, '/'); if (last != NULL) { - printf("%s: ", last + 1); + return snprintf(buffer, bufferSize, "%s: ", last + 1); } else { - printf("???: "); + return snprintf(buffer, bufferSize, "???: "); } } -static void _debuglog_print_log(const char* logType, char* filename) { - _debuglog_print_timestamp(); - _debuglog_print_network_type(); - _debuglog_print_log_type(logType); - _debuglog_print_short_filename(filename); +static inline void _debuglog_print_log(const char* color, const char* logType, const char* filename, const char* fmt, ...) { + char log[MAX_LOG_SIZE]; + size_t capacity = MAX_LOG_SIZE; + char* buffer = log; + + int len = 0; + + len = snprintf(buffer, capacity, "%s", color); + if (len < 0 || (size_t)len >= capacity) return; + buffer += len; capacity -= len; + + len = _debuglog_print_timestamp(buffer, capacity); + if (len < 0 || (size_t)len >= capacity) return; + buffer += len; capacity -= len; + + len = _debuglog_print_network_type(buffer, capacity); + if (len < 0 || (size_t)len >= capacity) return; + buffer += len; capacity -= len; + + len = _debuglog_print_log_type(logType, buffer, capacity); + if (len < 0 || (size_t)len >= capacity) return; + buffer += len; capacity -= len; + + len = _debuglog_print_short_filename(filename, buffer, capacity); + if (len < 0 || (size_t)len >= capacity) return; + buffer += len; capacity -= len; + + va_list args; + va_start(args, fmt); + + len = vsnprintf(buffer, capacity, fmt, args); + + va_end(args); + + if (len < 0) return; + + log_to_terminal("%s\x1b[0m\n", log); } #if defined(DISABLE_MODULE_LOG) @@ -48,9 +83,9 @@ static void _debuglog_print_log(const char* logType, char* filename) { #define LOG_INFO(...) #define LOG_ERROR(...) #else -#define LOG_DEBUG(...) (configDebugPrint ? ( _debuglog_print_log("DEBUG", __FILE__), printf(__VA_ARGS__), printf("\n") ) : 0) -#define LOG_INFO(...) ((configDebugInfo || gCLIOpts.headless) ? ( _debuglog_print_log("INFO", __FILE__), printf(__VA_ARGS__), printf("\n") ) : 0) -#define LOG_ERROR(...) (configDebugError ? ( _debuglog_print_log("ERROR", __FILE__), printf(__VA_ARGS__), printf("\n") ) : 0) +#define LOG_DEBUG(...) (configDebugPrint ? ( _debuglog_print_log("", "DEBUG", __FILE__, __VA_ARGS__) ) : 0) +#define LOG_INFO(...) ((configDebugInfo || gCLIOpts.headless) ? ( _debuglog_print_log("", "INFO", __FILE__, __VA_ARGS__) ) : 0) +#define LOG_ERROR(...) (configDebugError ? ( _debuglog_print_log("\x1b[31m", "ERROR", __FILE__, __VA_ARGS__) ) : 0) #endif #define LOG_CONSOLE(...) { snprintf(gDjuiConsoleTmpBuffer, CONSOLE_MAX_TMP_BUFFER, __VA_ARGS__), djui_console_message_create(gDjuiConsoleTmpBuffer, CONSOLE_MESSAGE_INFO); } diff --git a/src/pc/dev/chat.c b/src/pc/dev/chat.c index 7118c4665..b7d9130ed 100644 --- a/src/pc/dev/chat.c +++ b/src/pc/dev/chat.c @@ -3,7 +3,7 @@ #include "pc/lua/smlua_hooks.h" #include "pc/djui/djui_language.h" #include "pc/djui/djui_chat_message.h" -#include "pc/chat_commands.h" +#include "pc/commands.h" #include "pc/network/ban_list.h" #include "pc/network/moderator_list.h" #include "pc/debuglog.h" @@ -66,12 +66,12 @@ static s32 get_level_abbreviation_alt(const char *str) { } bool exec_dev_chat_command(char* command) { - if (strcmp("/warp", command) == 0) { + if (strcmp("warp", command) == 0) { djui_chat_message_create("Missing parameters: [LEVEL] [AREA] [ACT]"); return true; } - if (str_starts_with(command, "/warp ")) { + if (str_starts_with(command, "warp ")) { static const struct { const char *name; s32 num; } sLevelNumByName[] = { #undef STUB_LEVEL #undef DEFINE_LEVEL @@ -85,7 +85,7 @@ bool exec_dev_chat_command(char* command) { s32 act = 0; // Params - char *paramLevel = command + 6; + char *paramLevel = command + 5; if (*paramLevel == 0 || *paramLevel == ' ') { djui_chat_message_create("Missing parameters: [LEVEL]"); return true; @@ -161,23 +161,23 @@ bool exec_dev_chat_command(char* command) { return true; } - if (strcmp("/lua", command) == 0) { + if (strcmp("lua", command) == 0) { djui_chat_message_create("Missing parameter: [LUA]"); return true; } - if (str_starts_with(command, "/lua ")) { - smlua_exec_str(&command[5]); + if (str_starts_with(command, "lua ")) { + smlua_exec_str(&command[4]); return true; } - if (strcmp("/luaf", command) == 0) { + if (strcmp("luaf", command) == 0) { djui_chat_message_create("Missing parameter: [FILENAME]"); return true; } - if (str_starts_with(command, "/luaf ")) { - smlua_exec_file(&command[6]); + if (str_starts_with(command, "luaf ")) { + smlua_exec_file(&command[5]); return true; } @@ -185,8 +185,8 @@ bool exec_dev_chat_command(char* command) { } void dev_display_chat_commands(void) { - djui_chat_message_create("/warp [LEVEL] [AREA] [ACT] - Level can be either a numeric value or a shorthand name"); - djui_chat_message_create("/lua [LUA] - Execute Lua code from a string"); - djui_chat_message_create("/luaf [FILENAME] - Execute Lua code from a file"); + command_message_create("/warp [LEVEL] [AREA] [ACT] - Level can be either a numeric value or a shorthand name"); + command_message_create("/lua [LUA] - Execute Lua code from a string"); + command_message_create("/luaf [FILENAME] - Execute Lua code from a file"); } #endif \ No newline at end of file diff --git a/src/pc/discord/discord.c b/src/pc/discord/discord.c index c64a7b6d9..2ea1db767 100644 --- a/src/pc/discord/discord.c +++ b/src/pc/discord/discord.c @@ -46,7 +46,7 @@ void discord_fatal(int rc) { } static void get_oauth2_token_callback(UNUSED void* data, enum EDiscordResult result, struct DiscordOAuth2Token* token) { - LOG_INFO("> get_oauth2_token_callback returned %d", result); + LOG_INFO("get_oauth2_token_callback returned %d", result); if (result != DiscordResult_Ok) { return; } LOG_INFO("OAuth2 token: %s", token->access_token); } @@ -72,7 +72,7 @@ static void register_launch_command(void) { } static void on_current_user_update(UNUSED void* data) { - LOG_INFO("> on_current_user_update"); + LOG_INFO("on_current_user_update"); struct DiscordUser user = { 0 }; app.users->get_current_user(app.users, &user); @@ -95,7 +95,7 @@ static void on_current_user_update(UNUSED void* data) { } struct IDiscordUserEvents* discord_user_initialize(void) { - LOG_INFO("> discord_user_intitialize"); + LOG_INFO("discord_user_intitialize"); static struct IDiscordUserEvents events = { 0 }; events.on_current_user_update = on_current_user_update; return &events; diff --git a/src/pc/discord/discord_activity.c b/src/pc/discord/discord_activity.c index 36745701c..a7ef30d3e 100644 --- a/src/pc/discord/discord_activity.c +++ b/src/pc/discord/discord_activity.c @@ -16,12 +16,12 @@ static uint64_t sQueuedLobbyId = 0; static char sQueuedLobbyPassword[64] = ""; static void on_activity_update_callback(UNUSED void* data, enum EDiscordResult result) { - LOG_INFO("> on_activity_update_callback returned %d", result); + LOG_INFO("on_activity_update_callback returned %d", result); DISCORD_REQUIRE(result); } static void on_activity_join(UNUSED void* data, const char* secret) { - LOG_INFO("> on_activity_join, secret: %s", secret); + LOG_INFO("on_activity_join, secret: %s", secret); char *token; // extract lobby type @@ -52,12 +52,12 @@ static void on_activity_join(UNUSED void* data, const char* secret) { } static void on_activity_join_request_callback(UNUSED void* data, enum EDiscordResult result) { - LOG_INFO("> on_activity_join_request_callback returned %d", (int)result); + LOG_INFO("on_activity_join_request_callback returned %d", (int)result); DISCORD_REQUIRE(result); } static void on_activity_join_request(UNUSED void* data, struct DiscordUser* user) { - LOG_INFO("> on_activity_join_request from " DISCORD_ID_FORMAT, user->id); + LOG_INFO("on_activity_join_request from " DISCORD_ID_FORMAT, user->id); } static void strncat_len(char* destination, char* source, size_t destinationLength, size_t sourceLength) { diff --git a/src/pc/djui/djui_chat_box.c b/src/pc/djui/djui_chat_box.c index bddb52f6d..afd5b1613 100644 --- a/src/pc/djui/djui_chat_box.c +++ b/src/pc/djui/djui_chat_box.c @@ -3,7 +3,7 @@ #include #include "pc/network/network.h" #include "pc/lua/smlua_hooks.h" -#include "pc/chat_commands.h" +#include "pc/commands.h" #include "pc/configfile.h" #include "djui.h" #include "engine/math_util.h" @@ -139,13 +139,7 @@ static void djui_chat_box_input_enter(struct DjuiInputbox* chatInput) { if (strlen(chatInput->buffer) != 0) { sent_history_add_message(&sentHistory, chatInput->buffer); if (chatInput->buffer[0] == '/') { - if (strcmp(chatInput->buffer, "/help") == 0 || strcmp(chatInput->buffer, "/?") == 0 || strcmp(chatInput->buffer, "/") == 0) { - display_chat_commands(); - } else if (!exec_chat_command(chatInput->buffer)) { - char extendedUnknownCommandMessage[MAX_CHAT_MSG_LENGTH]; - snprintf(extendedUnknownCommandMessage, sizeof(extendedUnknownCommandMessage), "%s (/help)", DLANG(CHAT, UNRECOGNIZED)); - djui_chat_message_create(extendedUnknownCommandMessage); - } + run_command(chatInput->buffer + 1); } else { djui_chat_message_create_from(gNetworkPlayerLocal->globalIndex, chatInput->buffer); network_send_chat(chatInput->buffer, gNetworkPlayerLocal->globalIndex); @@ -417,7 +411,7 @@ static bool djui_chat_box_input_on_key_down(UNUSED struct DjuiBase* base, int sc sent_history_init(&sentHistory); if (gDjuiChatBox == NULL) { return false; } - + f32 pageAmount = gDjuiChatBox->chatContainer->base.elem.height * 3.0f / 4.0f; char previousText[MAX_CHAT_MSG_LENGTH]; diff --git a/src/pc/djui/djui_console.c b/src/pc/djui/djui_console.c index 3f6b1372a..0efbfd4ff 100644 --- a/src/pc/djui/djui_console.c +++ b/src/pc/djui/djui_console.c @@ -3,6 +3,7 @@ #include "djui.h" #include "djui_console.h" #include "pc/pc_main.h" +#include "pc/commands.h" #include "engine/math_util.h" #define MAX_CONSOLE_MESSAGES 500 @@ -12,6 +13,7 @@ bool gDjuiConsoleFocus = false; char gDjuiConsoleTmpBuffer[CONSOLE_MAX_TMP_BUFFER] = ""; u32 sDjuiConsoleMessages = 0; bool sDjuiConsoleQueueMessages = true; +bool sClearConsoleInput = false; struct ConsoleQueuedMessage { char* message; @@ -52,18 +54,25 @@ void djui_console_message_dequeue(void) { bool djui_console_render(struct DjuiBase* base) { struct DjuiConsole* console = (struct DjuiConsole*)base; - djui_base_set_size(base, gDjuiRoot->base.width.value, gDjuiRoot->base.height.value * 0.5f); + djui_base_set_size(&console->base, gDjuiRoot->base.width.value, gDjuiRoot->base.height.value * 0.5f); + djui_base_set_size(&console->rectContainer->base, gDjuiRoot->base.width.value, gDjuiRoot->base.height.value * 0.5f - 32); if (console->scrolling) { - f32 yMax = console->base.comp.height - console->flow->base.height.value; + f32 yMax = console->base.comp.height - console->flow->base.height.value - 32; f32 target = console->flow->base.y.value + (console->scrollY - console->flow->base.y.value) * (configSmoothScrolling ? 0.5f : 1.f); - console->flow->base.y.value = clamp(target, yMax, 0.f); + console->flow->base.y.value = clamp(target, yMax, 0.0f); if (target < yMax || 0.f < target) { console->scrollY = clamp(target, yMax, 0.f); if (target > 0.f) { gDjuiConsole->scrolling = false; } } } else { console->scrollY = console->flow->base.y.value; } + if (sClearConsoleInput) { + djui_inputbox_set_text(gDjuiConsole->inputbox, ""); + djui_inputbox_select_all(gDjuiConsole->inputbox); + sClearConsoleInput = false; + } + djui_rect_render(base); return true; } @@ -75,12 +84,13 @@ static void djui_console_destroy(struct DjuiBase* base) { void djui_console_toggle(void) { if (gDjuiConsole == NULL) { return; } + sClearConsoleInput = true; gDjuiConsoleFocus = !gDjuiConsoleFocus; djui_base_set_visible(&gDjuiConsole->base, gDjuiConsoleFocus); if (gDjuiConsoleFocus) { if (gDjuiChatBoxFocus) { djui_chat_box_toggle(); } - djui_interactable_set_input_focus(&gDjuiConsole->base); + djui_interactable_set_input_focus(&gDjuiConsole->inputbox->base); } else { djui_interactable_set_input_focus(NULL); } @@ -96,18 +106,33 @@ static void djui_console_on_scroll(UNUSED struct DjuiBase *base, UNUSED float x, if (gDjuiInputHeldShift) { y *= 3; } gDjuiConsole->scrollY -= y; - + if (!gDjuiConsole->scrolling) { gDjuiConsole->scrolling = y > 0 && gDjuiConsole->scrollY > yMax; } } -static bool djui_console_on_key_down(UNUSED struct DjuiBase* base, int scancode) { +static void djui_console_enter() { + char* buffer = gDjuiConsole->inputbox->buffer; + if (strcmp(buffer, "") == 0) return; + if (buffer[0] == '/') buffer++; + run_command(buffer); + sClearConsoleInput = true; +} + +static bool djui_console_on_key_down(struct DjuiBase* base, int scancode) { if (gDjuiConsole == NULL) { return false; } f32 yMax = gDjuiConsole->base.comp.height - gDjuiConsole->flow->base.height.value; f32 pageAmount = gDjuiConsole->base.comp.height * 3.0f / 4.0f; + for (int i = 0; i < MAX_BINDS; i++) { + if (scancode == (int)configKeyConsole[i]) { + djui_console_toggle(); + return true; + } + } + switch (scancode) { case SCANCODE_UP: gDjuiConsole->scrollY -= 15; @@ -121,8 +146,14 @@ static bool djui_console_on_key_down(UNUSED struct DjuiBase* base, int scancode) case SCANCODE_PAGE_DOWN: gDjuiConsole->scrollY += pageAmount; break; - case SCANCODE_ESCAPE: djui_console_toggle(); break; - default: break; + case SCANCODE_ENTER: + djui_console_enter(); + break; + case SCANCODE_ESCAPE: + djui_console_toggle(); + break; + default: + return djui_inputbox_on_key_down(base, scancode); } if (!gDjuiConsole->scrolling) { @@ -131,6 +162,23 @@ static bool djui_console_on_key_down(UNUSED struct DjuiBase* base, int scancode) return true; } +static void djui_console_on_text_input(struct DjuiBase* base, char* text) { + djui_inputbox_on_text_input(base, text); +} + +void djui_console_clear() { + if (gDjuiConsole == NULL) { return; } + + struct DjuiBase* cfBase = &gDjuiConsole->flow->base; + djui_base_destroy_children(cfBase); + + cfBase->height.value = 0; + cfBase->y.value = 0; + gDjuiConsole->scrollY = 0; + gDjuiConsole->scrolling = false; + sDjuiConsoleMessages = 0; +} + void djui_console_message_create(const char* message, enum ConsoleMessageLevel level) { if (sDjuiConsoleQueueMessages || !gDjuiConsole) { djui_console_message_queue(message, level); @@ -205,11 +253,13 @@ struct DjuiConsole* djui_console_create(void) { djui_base_set_padding(base, 0, 8, 8, 8); djui_base_set_visible(base, false); - djui_interactable_create(base, NULL); - djui_interactable_hook_key(base, djui_console_on_key_down, NULL); - djui_interactable_hook_scroll(base, djui_console_on_scroll); + struct DjuiRect* rectContainer = djui_rect_container_create(base, 0); + djui_base_set_alignment(&rectContainer->base, DJUI_HALIGN_LEFT, DJUI_VALIGN_TOP); + djui_base_set_size_type(&rectContainer->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE); + djui_base_set_size(&rectContainer->base, 1.0f, gDjuiRoot->base.height.value * 0.5f); + console->rectContainer = rectContainer; - struct DjuiFlowLayout* flow = djui_flow_layout_create(base); + struct DjuiFlowLayout* flow = djui_flow_layout_create(&rectContainer->base); struct DjuiBase* cfBase = &flow->base; djui_base_set_alignment(cfBase, DJUI_HALIGN_LEFT, DJUI_VALIGN_BOTTOM); djui_base_set_location(cfBase, 0, 0); @@ -223,6 +273,21 @@ struct DjuiConsole* djui_console_create(void) { cfBase->abandonAfterChildRenderFail = true; console->flow = flow; + struct DjuiInputbox* inputbox = djui_inputbox_create(base, MAX_CONSOLE_INPUT_LENGTH); + inputbox->base.interactable->update_style = NULL; + djui_base_set_border_color(&inputbox->base, 0, 0, 0, 0); + djui_base_set_color(&inputbox->base, 0, 0, 0, 0); + djui_base_set_size_type(&inputbox->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE); + djui_base_set_size(&inputbox->base, 1.0f, 32); + djui_base_set_alignment(&inputbox->base, DJUI_HALIGN_CENTER, DJUI_VALIGN_BOTTOM); + djui_interactable_hook_key(&inputbox->base, djui_console_on_key_down, djui_inputbox_on_key_up); + djui_interactable_hook_text_input(&inputbox->base, djui_console_on_text_input); + djui_interactable_hook_text_editing(&inputbox->base, djui_inputbox_on_text_editing); + djui_interactable_hook_scroll(&inputbox->base, djui_console_on_scroll); + djui_inputbox_set_text_color(inputbox, 255, 255, 255, 255); + inputbox->yOffset = 6; + console->inputbox = inputbox; + gDjuiConsole = console; return console; diff --git a/src/pc/djui/djui_console.h b/src/pc/djui/djui_console.h index e7dee2416..8bdeaf235 100644 --- a/src/pc/djui/djui_console.h +++ b/src/pc/djui/djui_console.h @@ -1,6 +1,8 @@ #pragma once #include "djui.h" +#define MAX_CONSOLE_INPUT_LENGTH 500 + enum ConsoleMessageLevel { CONSOLE_MESSAGE_INFO, CONSOLE_MESSAGE_WARNING, @@ -9,7 +11,9 @@ enum ConsoleMessageLevel { struct DjuiConsole { struct DjuiBase base; + struct DjuiRect* rectContainer; struct DjuiFlowLayout* flow; + struct DjuiInputbox* inputbox; bool scrolling; f32 scrollY; }; @@ -20,6 +24,7 @@ extern bool gDjuiConsoleFocus; extern char gDjuiConsoleTmpBuffer[]; void djui_console_message_dequeue(void); +void djui_console_clear(); void djui_console_message_create(const char* message, enum ConsoleMessageLevel level); /* |description|Toggles the visibility of the DJUI console|descriptionEnd| */ void djui_console_toggle(void); diff --git a/src/pc/djui/djui_inputbox.c b/src/pc/djui/djui_inputbox.c index da7fa59b0..da9b276a2 100644 --- a/src/pc/djui/djui_inputbox.c +++ b/src/pc/djui/djui_inputbox.c @@ -397,7 +397,7 @@ void djui_inputbox_on_text_input(struct DjuiBase *base, char* text) { inputbox->selection[1] = inputbox->selection[0]; sCursorBlink = 0; djui_inputbox_on_change(inputbox); - + inputbox->imePos = 0; if (inputbox->imeBuffer != NULL) { free(inputbox->imeBuffer); @@ -408,9 +408,9 @@ void djui_inputbox_on_text_input(struct DjuiBase *base, char* text) { void djui_inputbox_on_text_editing(struct DjuiBase *base, char* text, int cursorPos) { struct DjuiInputbox *inputbox = (struct DjuiInputbox *) base; inputbox->imePos = (u16)cursorPos; - + if (inputbox->imeBuffer != NULL) free(inputbox->imeBuffer); - + if (*text == '\0') { inputbox->imeBuffer = NULL; } @@ -420,7 +420,7 @@ void djui_inputbox_on_text_editing(struct DjuiBase *base, char* text, int cursor strcpy(copy,text); inputbox->imeBuffer = copy; } - + djui_inputbox_on_change(inputbox); } @@ -469,9 +469,9 @@ static void djui_inputbox_render_selection(struct DjuiInputbox* inputbox) { } sCursorBlink = (sCursorBlink + 1) % DJUI_INPUTBOX_MAX_BLINK; - + f32 renderX = x; - + u16 imePos = inputbox->imePos; if (imePos != 0) { char* ime = inputbox->imeBuffer; @@ -480,13 +480,13 @@ static void djui_inputbox_render_selection(struct DjuiInputbox* inputbox) { ime = djui_unicode_next_char(ime); } } - + // render only cursor when there is no selection width if (selection[0] == selection[1]) { if (sCursorBlink < DJUI_INPUTBOX_MID_BLINK && djui_interactable_is_input_focus(&inputbox->base)) { create_dl_translation_matrix(DJUI_MTX_PUSH, renderX - DJUI_INPUTBOX_CURSOR_WIDTH / 2.0f, -0.1f, 0); create_dl_scale_matrix(DJUI_MTX_NOPUSH, DJUI_INPUTBOX_CURSOR_WIDTH, 0.8f, 1.0f); - gDPSetEnvColor(gDisplayListHead++, 0, 0, 0, 255); + gDPSetEnvColor(gDisplayListHead++, inputbox->textColor.r, inputbox->textColor.g, inputbox->textColor.b, inputbox->textColor.a); gSPDisplayList(gDisplayListHead++, dl_djui_simple_rect); gSPPopMatrix(gDisplayListHead++, G_MTX_MODELVIEW); } @@ -559,7 +559,7 @@ static bool djui_inputbox_render(struct DjuiBase* base) { // translate position f32 translatedX = comp->x + inputbox->viewX; - f32 translatedY = comp->y + DJUI_INPUTBOX_YOFF; + f32 translatedY = comp->y + inputbox->yOffset; djui_gfx_position_translate(&translatedX, &translatedY); create_dl_translation_matrix(DJUI_MTX_PUSH, translatedX, translatedY, 0); @@ -584,7 +584,7 @@ static bool djui_inputbox_render(struct DjuiBase* base) { u16 selection[2] = { 0 }; selection[0] = fmin(inputbox->selection[0], inputbox->selection[1]); selection[1] = fmax(inputbox->selection[0], inputbox->selection[1]); - + // render text char* c = inputbox->buffer; f32 drawX = inputbox->viewX; @@ -593,7 +593,7 @@ static bool djui_inputbox_render(struct DjuiBase* base) { font->render_begin(); for (u16 i = 0; i < inputbox->bufferSize; i++) { - + //render composition text if (selection[0] == i && inputbox->imeBuffer != NULL) { char *ime = inputbox->imeBuffer; @@ -602,7 +602,7 @@ static bool djui_inputbox_render(struct DjuiBase* base) { ime = djui_unicode_next_char(ime); } } - + if (*c == '\0') { break; } // deal with seleciton color @@ -638,6 +638,7 @@ struct DjuiInputbox* djui_inputbox_create(struct DjuiBase* parent, u16 bufferSiz struct DjuiBase* base = &inputbox->base; inputbox->bufferSize = bufferSize; inputbox->buffer = calloc(bufferSize, sizeof(char)); + inputbox->yOffset = DJUI_INPUTBOX_YOFF; djui_base_init(parent, base, djui_inputbox_render, djui_inputbox_destroy); djui_base_set_size(base, 200, 32); diff --git a/src/pc/djui/djui_inputbox.h b/src/pc/djui/djui_inputbox.h index 7cc1f70d8..dcec5f6df 100644 --- a/src/pc/djui/djui_inputbox.h +++ b/src/pc/djui/djui_inputbox.h @@ -8,6 +8,7 @@ struct DjuiInputbox { u16 bufferSize; u16 selection[2]; f32 viewX; + f32 yOffset; struct DjuiColor textColor; void (*on_enter_press)(struct DjuiInputbox*); void (*on_escape_press)(struct DjuiInputbox*); diff --git a/src/pc/djui/djui_interactable.c b/src/pc/djui/djui_interactable.c index 96f3acc06..3fd1d6b9d 100644 --- a/src/pc/djui/djui_interactable.c +++ b/src/pc/djui/djui_interactable.c @@ -180,7 +180,7 @@ void djui_interactable_set_binding(struct DjuiBase* base) { } void djui_interactable_set_input_focus(struct DjuiBase* base) { - if (gDjuiConsoleFocus && base != &gDjuiConsole->base) { + if (gDjuiConsoleFocus && base != &gDjuiConsole->inputbox->base) { return; } @@ -199,8 +199,8 @@ bool djui_interactable_on_key_down(int scancode) { } bool keyFocused = (gInteractableFocus != NULL) - && (gInteractableFocus->interactable != NULL) - && (gInteractableFocus->interactable->on_key_down != NULL); + && (gInteractableFocus->interactable != NULL) + && (gInteractableFocus->interactable->on_key_down != NULL); if (keyFocused) { bool consume = gInteractableFocus->interactable->on_key_down(gInteractableFocus, scancode); @@ -217,10 +217,22 @@ bool djui_interactable_on_key_down(int scancode) { return true; } + if (!gDjuiChatBoxFocus) { + for (int i = 0; i < MAX_BINDS; i++) { + if (scancode == (int)configKeyConsole[i]) { + djui_console_toggle(); + return true; + } + } + } + if (gDjuiChatBox != NULL && !gDjuiChatBoxFocus) { bool pressChat = false; for (int i = 0; i < MAX_BINDS; i++) { - if (scancode == (int)configKeyChat[i]) { pressChat = true; } + if (scancode == (int)configKeyChat[i]) { + pressChat = true; + break; + } } if (pressChat && !gDjuiConsoleFocus) { @@ -264,7 +276,7 @@ bool djui_interactable_on_key_down(int scancode) { } } - if (gDjuiChatBoxFocus || djui_panel_is_active()) { + if (gDjuiConsoleFocus || gDjuiChatBoxFocus || djui_panel_is_active()) { switch (scancode) { case SCANCODE_UP: sKeyboardHoldDirection = PAD_HOLD_DIR_UP; return true; case SCANCODE_DOWN: sKeyboardHoldDirection = PAD_HOLD_DIR_DOWN; return true; @@ -278,13 +290,6 @@ bool djui_interactable_on_key_down(int scancode) { } void djui_interactable_on_key_up(int scancode) { - - if (!gDjuiChatBoxFocus) { - for (int i = 0; i < MAX_BINDS; i++) { - if (scancode == (int)configKeyConsole[i]) { djui_console_toggle(); break; } - } - } - if (gDjuiPlayerList != NULL || gDjuiModList != NULL) { for (int i = 0; i < MAX_BINDS; i++) { if (scancode == (int)configKeyPlayerList[i]) { @@ -424,12 +429,12 @@ void djui_interactable_update(void) { u16 mainButtons = PAD_BUTTON_A | PAD_BUTTON_B; if ((mouseButtons & MOUSE_BUTTON_1) && !(sLastMouseButtons & MOUSE_BUTTON_1) && !djui_cursor_inside_base(gInteractableFocus)) { // clicked outside of focus - if (!gDjuiChatBoxFocus) { + if (!gDjuiChatBoxFocus && !gDjuiConsoleFocus) { djui_interactable_set_input_focus(NULL); } } else if ((padButtons & mainButtons) && !(sLastInteractablePad.button & mainButtons)) { // pressed main face button - if (!gDjuiChatBoxFocus) { + if (!gDjuiChatBoxFocus && !gDjuiConsoleFocus) { djui_interactable_set_input_focus(NULL); } } else { diff --git a/src/pc/djui/djui_text.c b/src/pc/djui/djui_text.c index eb5643ac8..e26d0c259 100644 --- a/src/pc/djui/djui_text.c +++ b/src/pc/djui/djui_text.c @@ -287,6 +287,11 @@ static void djui_text_read_line(struct DjuiText* text, char** message, f32* line break; } + // check for tab + if (*c == '\t') { + charWidth = 4 * text->font->char_width(" "); + } + // check to see if this character would exceed size if (*lineWidth + charWidth >= maxLineWidth) { break; @@ -390,7 +395,9 @@ static void djui_text_render_line(struct DjuiText* text, char* c1, char* c2, f32 } f32 charWidth = text->font->char_width(c); - if (*c != '\n' && *c != ' ') { + if (*c == '\t') { + charWidth = 4 * text->font->char_width(" "); + } else if (*c != '\n' && *c != ' ') { djui_text_render_char(text, c); } diff --git a/src/pc/linenoise/linenoise.c b/src/pc/linenoise/linenoise.c new file mode 100644 index 000000000..db9b06cb6 --- /dev/null +++ b/src/pc/linenoise/linenoise.c @@ -0,0 +1,1762 @@ +/* linenoise.c -- guerrilla line editing library against the idea that a + * line editing lib needs to be 20,000 lines of C code. + * + * You can find the latest source code at: + * + * http://github.com/antirez/linenoise + * + * Does a number of crazy assumptions that happen to be true in 99.9999% of + * the 2010 UNIX computers around. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010-2023, Salvatore Sanfilippo + * Copyright (c) 2010-2013, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ------------------------------------------------------------------------ + * + * References: + * - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * - http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html + * + * Todo list: + * - Filter bogus Ctrl+ combinations. + * - Win32 support + * + * Bloat: + * - History search like Ctrl+r in readline? + * + * List of escape sequences used by this program, we do everything just + * with three sequences. In order to be so cheap we may have some + * flickering effect with some slow terminal, but the lesser sequences + * the more compatible. + * + * EL (Erase Line) + * Sequence: ESC [ n K + * Effect: if n is 0 or missing, clear from cursor to end of line + * Effect: if n is 1, clear from beginning of line to cursor + * Effect: if n is 2, clear entire line + * + * CUF (CUrsor Forward) + * Sequence: ESC [ n C + * Effect: moves cursor forward n chars + * + * CUB (CUrsor Backward) + * Sequence: ESC [ n D + * Effect: moves cursor backward n chars + * + * The following is used to get the terminal width if getting + * the width with the TIOCGWINSZ ioctl fails + * + * DSR (Device Status Report) + * Sequence: ESC [ 6 n + * Effect: reports the current cusor position as ESC [ n ; m R + * where n is the row and m is the column + * + * When multi line mode is enabled, we also use an additional escape + * sequence. However multi line editing is disabled by default. + * + * CUU (Cursor Up) + * Sequence: ESC [ n A + * Effect: moves cursor up of n chars. + * + * CUD (Cursor Down) + * Sequence: ESC [ n B + * Effect: moves cursor down of n chars. + * + * When linenoiseClearScreen() is called, two additional escape sequences + * are used in order to clear the screen and position the cursor at home + * position. + * + * CUP (Cursor position) + * Sequence: ESC [ H + * Effect: moves the cursor to upper left corner + * + * ED (Erase display) + * Sequence: ESC [ 2 J + * Effect: clear the whole screen + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "linenoise.h" + +#define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100 +#define LINENOISE_MAX_LINE 4096 +static char *unsupported_term[] = {"dumb","cons25","emacs",NULL}; +static linenoiseCompletionCallback *completionCallback = NULL; +static linenoiseHintsCallback *hintsCallback = NULL; +static linenoiseFreeHintsCallback *freeHintsCallback = NULL; +static char *linenoiseNoTTY(void); +static void refreshLineWithCompletion(struct linenoiseState *ls, linenoiseCompletions *lc, int flags); +static void refreshLineWithFlags(struct linenoiseState *l, int flags); + +static struct termios orig_termios; /* In order to restore at exit.*/ +static int maskmode = 0; /* Show "***" instead of input. For passwords. */ +static int rawmode = 0; /* For atexit() function to check if restore is needed*/ +static int mlmode = 0; /* Multi line mode. Default is single line. */ +static int atexit_registered = 0; /* Register atexit just 1 time. */ +static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN; +static int history_len = 0; +static char **history = NULL; + +/* =========================== UTF-8 support ================================ */ + +/* Return the number of bytes that compose the UTF-8 character starting at + * 'c'. This function assumes a valid UTF-8 encoding and handles the four + * standard byte patterns: + * 0xxxxxxx -> 1 byte (ASCII) + * 110xxxxx -> 2 bytes + * 1110xxxx -> 3 bytes + * 11110xxx -> 4 bytes */ +static int utf8ByteLen(char c) { + unsigned char uc = (unsigned char)c; + if ((uc & 0x80) == 0) return 1; /* 0xxxxxxx: ASCII */ + if ((uc & 0xE0) == 0xC0) return 2; /* 110xxxxx: 2-byte seq */ + if ((uc & 0xF0) == 0xE0) return 3; /* 1110xxxx: 3-byte seq */ + if ((uc & 0xF8) == 0xF0) return 4; /* 11110xxx: 4-byte seq */ + return 1; /* Fallback for invalid encoding, treat as single byte. */ +} + +/* Decode a UTF-8 sequence starting at 's' into a Unicode codepoint. + * Returns the codepoint value. Assumes valid UTF-8 encoding. */ +static uint32_t utf8DecodeChar(const char *s, size_t *len) { + unsigned char *p = (unsigned char *)s; + uint32_t cp; + + if ((*p & 0x80) == 0) { + *len = 1; + return *p; + } else if ((*p & 0xE0) == 0xC0) { + *len = 2; + cp = (*p & 0x1F) << 6; + cp |= (p[1] & 0x3F); + return cp; + } else if ((*p & 0xF0) == 0xE0) { + *len = 3; + cp = (*p & 0x0F) << 12; + cp |= (p[1] & 0x3F) << 6; + cp |= (p[2] & 0x3F); + return cp; + } else if ((*p & 0xF8) == 0xF0) { + *len = 4; + cp = (*p & 0x07) << 18; + cp |= (p[1] & 0x3F) << 12; + cp |= (p[2] & 0x3F) << 6; + cp |= (p[3] & 0x3F); + return cp; + } + *len = 1; + return *p; /* Fallback for invalid sequences. */ +} + +/* Check if codepoint is a variation selector (emoji style modifiers). */ +static int isVariationSelector(uint32_t cp) { + return cp == 0xFE0E || cp == 0xFE0F; /* Text/emoji style */ +} + +/* Check if codepoint is a skin tone modifier. */ +static int isSkinToneModifier(uint32_t cp) { + return cp >= 0x1F3FB && cp <= 0x1F3FF; +} + +/* Check if codepoint is Zero Width Joiner. */ +static int isZWJ(uint32_t cp) { + return cp == 0x200D; +} + +/* Check if codepoint is a Regional Indicator (for flag emoji). */ +static int isRegionalIndicator(uint32_t cp) { + return cp >= 0x1F1E6 && cp <= 0x1F1FF; +} + +/* Check if codepoint is a combining mark or other zero-width character. */ +static int isCombiningMark(uint32_t cp) { + return (cp >= 0x0300 && cp <= 0x036F) || /* Combining Diacriticals */ + (cp >= 0x1AB0 && cp <= 0x1AFF) || /* Combining Diacriticals Extended */ + (cp >= 0x1DC0 && cp <= 0x1DFF) || /* Combining Diacriticals Supplement */ + (cp >= 0x20D0 && cp <= 0x20FF) || /* Combining Diacriticals for Symbols */ + (cp >= 0xFE20 && cp <= 0xFE2F); /* Combining Half Marks */ +} + +/* Check if codepoint extends the previous character (doesn't start a new grapheme). */ +static int isGraphemeExtend(uint32_t cp) { + return isVariationSelector(cp) || isSkinToneModifier(cp) || + isZWJ(cp) || isCombiningMark(cp); +} + +/* Decode the UTF-8 codepoint ending at position 'pos' (exclusive) and + * return its value. Also sets *cplen to the byte length of the codepoint. */ +static uint32_t utf8DecodePrev(const char *buf, size_t pos, size_t *cplen) { + if (pos == 0) { + *cplen = 0; + return 0; + } + /* Scan backwards to find the start byte. */ + size_t i = pos; + do { + i--; + } while (i > 0 && (pos - i) < 4 && ((unsigned char)buf[i] & 0xC0) == 0x80); + *cplen = pos - i; + size_t dummy; + return utf8DecodeChar(buf + i, &dummy); +} + +/* Given a buffer and a position, return the byte length of the grapheme + * cluster before that position. A grapheme cluster includes: + * - The base character + * - Any following variation selectors, skin tone modifiers + * - ZWJ sequences (emoji joined by Zero Width Joiner) + * - Regional indicator pairs (flag emoji) */ +static size_t utf8PrevCharLen(const char *buf, size_t pos) { + if (pos == 0) return 0; + + size_t total = 0; + size_t curpos = pos; + + /* First, get the last codepoint. */ + size_t cplen; + uint32_t cp = utf8DecodePrev(buf, curpos, &cplen); + if (cplen == 0) return 0; + total += cplen; + curpos -= cplen; + + /* If we're at an extending character, we need to find what it extends. + * Keep going back through the grapheme cluster. */ + while (curpos > 0) { + size_t prevlen; + uint32_t prevcp = utf8DecodePrev(buf, curpos, &prevlen); + if (prevlen == 0) break; + + if (isZWJ(prevcp)) { + /* ZWJ joins two emoji. Include the ZWJ and continue to get + * the preceding character. */ + total += prevlen; + curpos -= prevlen; + /* Now get the character before ZWJ. */ + prevcp = utf8DecodePrev(buf, curpos, &prevlen); + if (prevlen == 0) break; + total += prevlen; + curpos -= prevlen; + cp = prevcp; + continue; /* Check if there's more extending before this. */ + } else if (isGraphemeExtend(cp)) { + /* Current cp is an extending character; include previous. */ + total += prevlen; + curpos -= prevlen; + cp = prevcp; + continue; + } else if (isRegionalIndicator(cp) && isRegionalIndicator(prevcp)) { + /* Two regional indicators form a flag. But we need to be careful: + * flags are always pairs, so only join if we're at an even boundary. + * For simplicity, just join one pair. */ + total += prevlen; + curpos -= prevlen; + break; + } else { + /* No more extending; we've found the start of the cluster. */ + break; + } + } + + return total; +} + +/* Given a buffer, position and total length, return the byte length of the + * grapheme cluster at the current position. */ +static size_t utf8NextCharLen(const char *buf, size_t pos, size_t len) { + if (pos >= len) return 0; + + size_t total = 0; + size_t curpos = pos; + + /* Get the first codepoint. */ + size_t cplen; + uint32_t cp = utf8DecodeChar(buf + curpos, &cplen); + total += cplen; + curpos += cplen; + + int isRI = isRegionalIndicator(cp); + + /* Consume any extending characters that follow. */ + while (curpos < len) { + size_t nextlen; + uint32_t nextcp = utf8DecodeChar(buf + curpos, &nextlen); + + if (isZWJ(nextcp) && curpos + nextlen < len) { + /* ZWJ: include it and the following character. */ + total += nextlen; + curpos += nextlen; + /* Get the character after ZWJ. */ + nextcp = utf8DecodeChar(buf + curpos, &nextlen); + total += nextlen; + curpos += nextlen; + continue; /* Check for more extending after the joined char. */ + } else if (isGraphemeExtend(nextcp)) { + /* Variation selector, skin tone, combining mark, etc. */ + total += nextlen; + curpos += nextlen; + continue; + } else if (isRI && isRegionalIndicator(nextcp)) { + /* Second regional indicator for a flag pair. */ + total += nextlen; + curpos += nextlen; + isRI = 0; /* Only pair once. */ + continue; + } else { + break; + } + } + + return total; +} + +/* Return the display width of a Unicode codepoint. This is a heuristic + * that works for most common cases: + * - Control chars and zero-width: 0 columns + * - Grapheme-extending chars (VS, skin tone, ZWJ): 0 columns + * - ASCII printable: 1 column + * - Wide chars (CJK, emoji, fullwidth): 2 columns + * - Everything else: 1 column + * + * This is not a full wcwidth() implementation, but a minimal heuristic + * that handles emoji and CJK characters reasonably well. */ +static int utf8CharWidth(uint32_t cp) { + /* Control characters and combining marks: zero width. */ + if (cp < 32 || (cp >= 0x7F && cp < 0xA0)) return 0; + if (isCombiningMark(cp)) return 0; + + /* Grapheme-extending characters: zero width. + * These modify the preceding character rather than taking space. */ + if (isVariationSelector(cp)) return 0; + if (isSkinToneModifier(cp)) return 0; + if (isZWJ(cp)) return 0; + + /* Wide character ranges - these display as 2 columns: + * - CJK Unified Ideographs and Extensions + * - Fullwidth forms + * - Various emoji ranges */ + if (cp >= 0x1100 && + (cp <= 0x115F || /* Hangul Jamo */ + cp == 0x2329 || cp == 0x232A || /* Angle brackets */ + (cp >= 0x231A && cp <= 0x231B) || /* Watch, Hourglass */ + (cp >= 0x23E9 && cp <= 0x23F3) || /* Various symbols */ + (cp >= 0x23F8 && cp <= 0x23FA) || /* Various symbols */ + (cp >= 0x25AA && cp <= 0x25AB) || /* Small squares */ + (cp >= 0x25B6 && cp <= 0x25C0) || /* Play/reverse buttons */ + (cp >= 0x25FB && cp <= 0x25FE) || /* Squares */ + (cp >= 0x2600 && cp <= 0x26FF) || /* Misc Symbols (sun, cloud, etc) */ + (cp >= 0x2700 && cp <= 0x27BF) || /* Dingbats (❤, ✂, etc) */ + (cp >= 0x2934 && cp <= 0x2935) || /* Arrows */ + (cp >= 0x2B05 && cp <= 0x2B07) || /* Arrows */ + (cp >= 0x2B1B && cp <= 0x2B1C) || /* Squares */ + cp == 0x2B50 || cp == 0x2B55 || /* Star, circle */ + (cp >= 0x2E80 && cp <= 0xA4CF && + cp != 0x303F) || /* CJK ... Yi */ + (cp >= 0xAC00 && cp <= 0xD7A3) || /* Hangul Syllables */ + (cp >= 0xF900 && cp <= 0xFAFF) || /* CJK Compatibility Ideographs */ + (cp >= 0xFE10 && cp <= 0xFE1F) || /* Vertical forms */ + (cp >= 0xFE30 && cp <= 0xFE6F) || /* CJK Compatibility Forms */ + (cp >= 0xFF00 && cp <= 0xFF60) || /* Fullwidth Forms */ + (cp >= 0xFFE0 && cp <= 0xFFE6) || /* Fullwidth Signs */ + (cp >= 0x1F1E6 && cp <= 0x1F1FF) || /* Regional Indicators (flags) */ + (cp >= 0x1F300 && cp <= 0x1F64F) || /* Misc Symbols and Emoticons */ + (cp >= 0x1F680 && cp <= 0x1F6FF) || /* Transport and Map Symbols */ + (cp >= 0x1F900 && cp <= 0x1F9FF) || /* Supplemental Symbols */ + (cp >= 0x1FA00 && cp <= 0x1FAFF) || /* Chess, Extended-A */ + (cp >= 0x20000 && cp <= 0x2FFFF))) /* CJK Extension B and beyond */ + return 2; + + return 1; /* Default: single width */ +} + +/* Calculate the display width of a UTF-8 string of 'len' bytes. + * This is used for cursor positioning in the terminal. + * Handles grapheme clusters: characters joined by ZWJ contribute 0 width + * after the first character in the sequence. */ +static size_t utf8StrWidth(const char *s, size_t len) { + size_t width = 0; + size_t i = 0; + int after_zwj = 0; /* Track if previous char was ZWJ */ + + while (i < len) { + size_t clen; + uint32_t cp = utf8DecodeChar(s + i, &clen); + + if (after_zwj) { + /* Character after ZWJ: don't add width, it's joined. + * But do check for extending chars after it. */ + after_zwj = 0; + } else { + width += utf8CharWidth(cp); + } + + /* Check if this is a ZWJ - next char will be joined. */ + if (isZWJ(cp)) { + after_zwj = 1; + } + + i += clen; + } + return width; +} + +/* Return the display width of a single UTF-8 character at position 's'. */ +static int utf8SingleCharWidth(const char *s, size_t len) { + if (len == 0) return 0; + size_t clen; + uint32_t cp = utf8DecodeChar(s, &clen); + return utf8CharWidth(cp); +} + +enum KEY_ACTION{ + KEY_NULL = 0, /* NULL */ + CTRL_A = 1, /* Ctrl+a */ + CTRL_B = 2, /* Ctrl-b */ + CTRL_C = 3, /* Ctrl-c */ + CTRL_D = 4, /* Ctrl-d */ + CTRL_E = 5, /* Ctrl-e */ + CTRL_F = 6, /* Ctrl-f */ + CTRL_H = 8, /* Ctrl-h */ + TAB = 9, /* Tab */ + CTRL_K = 11, /* Ctrl+k */ + CTRL_L = 12, /* Ctrl+l */ + ENTER = 13, /* Enter */ + CTRL_N = 14, /* Ctrl-n */ + CTRL_P = 16, /* Ctrl-p */ + CTRL_T = 20, /* Ctrl-t */ + CTRL_U = 21, /* Ctrl+u */ + CTRL_W = 23, /* Ctrl+w */ + ESC = 27, /* Escape */ + BACKSPACE = 127 /* Backspace */ +}; + +static void linenoiseAtExit(void); +int linenoiseHistoryAdd(const char *line); +#define REFRESH_CLEAN (1<<0) // Clean the old prompt from the screen +#define REFRESH_WRITE (1<<1) // Rewrite the prompt on the screen. +#define REFRESH_ALL (REFRESH_CLEAN|REFRESH_WRITE) // Do both. +static void refreshLine(struct linenoiseState *l); + +/* Debugging macro. */ +#if 0 +FILE *lndebug_fp = NULL; +#define lndebug(...) \ + do { \ + if (lndebug_fp == NULL) { \ + lndebug_fp = fopen("/tmp/lndebug.txt","a"); \ + fprintf(lndebug_fp, \ + "[%d %d %d] p: %d, rows: %d, rpos: %d, max: %d, oldmax: %d\n", \ + (int)l->len,(int)l->pos,(int)l->oldpos,plen,rows,rpos, \ + (int)l->oldrows,old_rows); \ + } \ + fprintf(lndebug_fp, ", " __VA_ARGS__); \ + fflush(lndebug_fp); \ + } while (0) +#else +#define lndebug(fmt, ...) +#endif + +/* ======================= Low level terminal handling ====================== */ + +/* Enable "mask mode". When it is enabled, instead of the input that + * the user is typing, the terminal will just display a corresponding + * number of asterisks, like "****". This is useful for passwords and other + * secrets that should not be displayed. */ +void linenoiseMaskModeEnable(void) { + maskmode = 1; +} + +/* Disable mask mode. */ +void linenoiseMaskModeDisable(void) { + maskmode = 0; +} + +/* Set if to use or not the multi line mode. */ +void linenoiseSetMultiLine(int ml) { + mlmode = ml; +} + +/* Return true if the terminal name is in the list of terminals we know are + * not able to understand basic escape sequences. */ +static int isUnsupportedTerm(void) { + char *term = getenv("TERM"); + int j; + + if (term == NULL) return 0; + for (j = 0; unsupported_term[j]; j++) + if (!strcasecmp(term,unsupported_term[j])) return 1; + return 0; +} + +/* Raw mode: 1960 magic shit. */ +static int enableRawMode(int fd) { + struct termios raw; + + /* Test mode: when LINENOISE_ASSUME_TTY is set, skip terminal setup. + * This allows testing via pipes without a real terminal. */ + if (getenv("LINENOISE_ASSUME_TTY")) { + rawmode = 1; + return 0; + } + + if (!isatty(STDIN_FILENO)) goto fatal; + if (!atexit_registered) { + atexit(linenoiseAtExit); + atexit_registered = 1; + } + if (tcgetattr(fd,&orig_termios) == -1) goto fatal; + + raw = orig_termios; /* modify the original mode */ + /* input modes: no break, no CR to NL, no parity check, no strip char, + * no start/stop output control. */ + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + /* output modes - disable post processing */ + raw.c_oflag &= ~(OPOST); + /* control modes - set 8 bit chars */ + raw.c_cflag |= (CS8); + /* local modes - choing off, canonical off, no extended functions, + * no signal chars (^Z,^C) */ + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + /* control chars - set return condition: min number of bytes and timer. + * We want read to return every single byte, without timeout. */ + raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ + + /* put terminal in raw mode after flushing */ + if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal; + rawmode = 1; + return 0; + +fatal: + errno = ENOTTY; + return -1; +} + +static void disableRawMode(int fd) { + /* Test mode: nothing to restore. */ + if (getenv("LINENOISE_ASSUME_TTY")) { + rawmode = 0; + return; + } + /* Don't even check the return value as it's too late. */ + if (rawmode && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1) + rawmode = 0; +} + +/* Use the ESC [6n escape sequence to query the horizontal cursor position + * and return it. On error -1 is returned, on success the position of the + * cursor. */ +static int getCursorPosition(int ifd, int ofd) { + char buf[32]; + int cols, rows; + unsigned int i = 0; + + /* Report cursor location */ + if (write(ofd, "\x1b[6n", 4) != 4) return -1; + + /* Read the response: ESC [ rows ; cols R */ + while (i < sizeof(buf)-1) { + if (read(ifd,buf+i,1) != 1) break; + if (buf[i] == 'R') break; + i++; + } + buf[i] = '\0'; + + /* Parse it. */ + if (buf[0] != ESC || buf[1] != '[') return -1; + if (sscanf(buf+2,"%d;%d",&rows,&cols) != 2) return -1; + return cols; +} + +/* Try to get the number of columns in the current terminal, or assume 80 + * if it fails. */ +static int getColumns(int ifd, int ofd) { + struct winsize ws; + + /* Test mode: use LINENOISE_COLS env var for fixed width. */ + char *cols_env = getenv("LINENOISE_COLS"); + if (cols_env) return atoi(cols_env); + + if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { + /* ioctl() failed. Try to query the terminal itself. */ + int start, cols; + + /* Get the initial position so we can restore it later. */ + start = getCursorPosition(ifd,ofd); + if (start == -1) goto failed; + + /* Go to right margin and get position. */ + if (write(ofd,"\x1b[999C",6) != 6) goto failed; + cols = getCursorPosition(ifd,ofd); + if (cols == -1) goto failed; + + /* Restore position. */ + if (cols > start) { + char seq[32]; + snprintf(seq,32,"\x1b[%dD",cols-start); + if (write(ofd,seq,strlen(seq)) == -1) { + /* Can't recover... */ + } + } + return cols; + } else { + return ws.ws_col; + } + +failed: + return 80; +} + +/* Clear the screen. Used to handle ctrl+l */ +void linenoiseClearScreen(void) { + if (write(STDOUT_FILENO,"\x1b[H\x1b[2J",7) <= 0) { + /* nothing to do, just to avoid warning. */ + } +} + +/* Beep, used for completion when there is nothing to complete or when all + * the choices were already shown. */ +static void linenoiseBeep(void) { + fprintf(stderr, "\x7"); + fflush(stderr); +} + +/* ============================== Completion ================================ */ + +/* Free a list of completion option populated by linenoiseAddCompletion(). */ +static void freeCompletions(linenoiseCompletions *lc) { + size_t i; + for (i = 0; i < lc->len; i++) + free(lc->cvec[i]); + if (lc->cvec != NULL) + free(lc->cvec); +} + +/* Called by completeLine() and linenoiseShow() to render the current + * edited line with the proposed completion. If the current completion table + * is already available, it is passed as second argument, otherwise the + * function will use the callback to obtain it. + * + * Flags are the same as refreshLine*(), that is REFRESH_* macros. */ +static void refreshLineWithCompletion(struct linenoiseState *ls, linenoiseCompletions *lc, int flags) { + /* Obtain the table of completions if the caller didn't provide one. */ + linenoiseCompletions ctable = { 0, NULL }; + if (lc == NULL) { + completionCallback(ls->buf,&ctable); + lc = &ctable; + } + + /* Show the edited line with completion if possible, or just refresh. */ + if (ls->completion_idx < lc->len) { + struct linenoiseState saved = *ls; + ls->len = ls->pos = strlen(lc->cvec[ls->completion_idx]); + ls->buf = lc->cvec[ls->completion_idx]; + refreshLineWithFlags(ls,flags); + ls->len = saved.len; + ls->pos = saved.pos; + ls->buf = saved.buf; + } else { + refreshLineWithFlags(ls,flags); + } + + /* Free the completions table if needed. */ + if (lc != &ctable) freeCompletions(&ctable); +} + +/* This is an helper function for linenoiseEdit*() and is called when the + * user types the key in order to complete the string currently in the + * input. + * + * The state of the editing is encapsulated into the pointed linenoiseState + * structure as described in the structure definition. + * + * If the function returns non-zero, the caller should handle the + * returned value as a byte read from the standard input, and process + * it as usually: this basically means that the function may return a byte + * read from the termianl but not processed. Otherwise, if zero is returned, + * the input was consumed by the completeLine() function to navigate the + * possible completions, and the caller should read for the next characters + * from stdin. */ +static int completeLine(struct linenoiseState *ls, int keypressed) { + linenoiseCompletions lc = { 0, NULL }; + int nwritten; + char c = keypressed; + + completionCallback(ls->buf,&lc); + if (lc.len == 0) { + linenoiseBeep(); + ls->in_completion = 0; + c = 0; + } else { + switch(c) { + case 9: /* tab */ + if (ls->in_completion == 0) { + ls->in_completion = 1; + ls->completion_idx = 0; + } else { + ls->completion_idx = (ls->completion_idx+1) % (lc.len+1); + if (ls->completion_idx == lc.len) linenoiseBeep(); + } + c = 0; + break; + case 27: /* escape */ + /* Re-show original buffer */ + if (ls->completion_idx < lc.len) refreshLine(ls); + ls->in_completion = 0; + c = 0; + break; + default: + /* Update buffer and return */ + if (ls->completion_idx < lc.len) { + nwritten = snprintf(ls->buf,ls->buflen,"%s", + lc.cvec[ls->completion_idx]); + ls->len = ls->pos = nwritten; + } + ls->in_completion = 0; + break; + } + + /* Show completion or original buffer */ + if (ls->in_completion && ls->completion_idx < lc.len) { + refreshLineWithCompletion(ls,&lc,REFRESH_ALL); + } else { + refreshLine(ls); + } + } + + freeCompletions(&lc); + return c; /* Return last read character */ +} + +/* Register a callback function to be called for tab-completion. */ +void linenoiseSetCompletionCallback(linenoiseCompletionCallback *fn) { + completionCallback = fn; +} + +/* Register a hits function to be called to show hits to the user at the + * right of the prompt. */ +void linenoiseSetHintsCallback(linenoiseHintsCallback *fn) { + hintsCallback = fn; +} + +/* Register a function to free the hints returned by the hints callback + * registered with linenoiseSetHintsCallback(). */ +void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *fn) { + freeHintsCallback = fn; +} + +/* This function is used by the callback function registered by the user + * in order to add completion options given the input string when the + * user typed . See the example.c source code for a very easy to + * understand example. */ +void linenoiseAddCompletion(linenoiseCompletions *lc, const char *str) { + size_t len = strlen(str); + char *copy, **cvec; + + copy = malloc(len+1); + if (copy == NULL) return; + memcpy(copy,str,len+1); + cvec = realloc(lc->cvec,sizeof(char*)*(lc->len+1)); + if (cvec == NULL) { + free(copy); + return; + } + lc->cvec = cvec; + lc->cvec[lc->len++] = copy; +} + +/* =========================== Line editing ================================= */ + +/* We define a very simple "append buffer" structure, that is an heap + * allocated string where we can append to. This is useful in order to + * write all the escape sequences in a buffer and flush them to the standard + * output in a single call, to avoid flickering effects. */ +struct abuf { + char *b; + int len; +}; + +static void abInit(struct abuf *ab) { + ab->b = NULL; + ab->len = 0; +} + +static void abAppend(struct abuf *ab, const char *s, int len) { + char *new = realloc(ab->b,ab->len+len); + + if (new == NULL) return; + memcpy(new+ab->len,s,len); + ab->b = new; + ab->len += len; +} + +static void abFree(struct abuf *ab) { + free(ab->b); +} + +/* Helper of refreshSingleLine() and refreshMultiLine() to show hints + * to the right of the prompt. Now uses display widths for proper UTF-8. */ +void refreshShowHints(struct abuf *ab, struct linenoiseState *l, int pwidth) { + char seq[64]; + size_t bufwidth = utf8StrWidth(l->buf, l->len); + if (hintsCallback && pwidth + bufwidth < l->cols) { + int color = -1, bold = 0; + char *hint = hintsCallback(l->buf,&color,&bold); + if (hint) { + size_t hintlen = strlen(hint); + size_t hintwidth = utf8StrWidth(hint, hintlen); + size_t hintmaxwidth = l->cols - (pwidth + bufwidth); + /* Truncate hint to fit, respecting UTF-8 boundaries. */ + if (hintwidth > hintmaxwidth) { + size_t i = 0, w = 0; + while (i < hintlen) { + size_t clen = utf8NextCharLen(hint, i, hintlen); + int cwidth = utf8SingleCharWidth(hint + i, clen); + if (w + cwidth > hintmaxwidth) break; + w += cwidth; + i += clen; + } + hintlen = i; + } + if (bold == 1 && color == -1) color = 37; + if (color != -1 || bold != 0) + snprintf(seq,64,"\033[%d;%d;49m",bold,color); + else + seq[0] = '\0'; + abAppend(ab,seq,strlen(seq)); + abAppend(ab,hint,hintlen); + if (color != -1 || bold != 0) + abAppend(ab,"\033[0m",4); + /* Call the function to free the hint returned. */ + if (freeHintsCallback) freeHintsCallback(hint); + } + } +} + +/* Single line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. + * + * Flags is REFRESH_* macros. The function can just remove the old + * prompt, just write it, or both. + * + * This function is UTF-8 aware and uses display widths (not byte counts) + * for cursor positioning and horizontal scrolling. */ +static void refreshSingleLine(struct linenoiseState *l, int flags) { + char seq[64]; + size_t pwidth = utf8StrWidth(l->prompt, l->plen); /* Prompt display width */ + int fd = l->ofd; + char *buf = l->buf; + size_t len = l->len; /* Byte length of buffer to display */ + size_t pos = l->pos; /* Byte position of cursor */ + size_t poscol; /* Display column of cursor */ + size_t lencol; /* Display width of buffer */ + struct abuf ab; + + /* Calculate the display width up to cursor and total display width. */ + poscol = utf8StrWidth(buf, pos); + lencol = utf8StrWidth(buf, len); + + /* Scroll the buffer horizontally if cursor is past the right edge. + * We need to trim full UTF-8 characters from the left until the + * cursor position fits within the terminal width. */ + while (pwidth + poscol >= l->cols) { + size_t clen = utf8NextCharLen(buf, 0, len); + int cwidth = utf8SingleCharWidth(buf, clen); + buf += clen; + len -= clen; + pos -= clen; + poscol -= cwidth; + lencol -= cwidth; + } + + /* Trim from the right if the line still doesn't fit. */ + while (pwidth + lencol > l->cols) { + size_t clen = utf8PrevCharLen(buf, len); + int cwidth = utf8SingleCharWidth(buf + len - clen, clen); + len -= clen; + lencol -= cwidth; + } + + abInit(&ab); + /* Cursor to left edge */ + snprintf(seq,sizeof(seq),"\r"); + abAppend(&ab,seq,strlen(seq)); + + if (flags & REFRESH_WRITE) { + /* Write the prompt and the current buffer content */ + abAppend(&ab,l->prompt,l->plen); + if (maskmode == 1) { + /* In mask mode, we output one '*' per UTF-8 character, not byte */ + size_t i = 0; + while (i < len) { + abAppend(&ab,"*",1); + i += utf8NextCharLen(buf, i, len); + } + } else { + abAppend(&ab,buf,len); + } + /* Show hints if any. */ + refreshShowHints(&ab,l,pwidth); + } + + /* Erase to right */ + snprintf(seq,sizeof(seq),"\x1b[0K"); + abAppend(&ab,seq,strlen(seq)); + + if (flags & REFRESH_WRITE) { + /* Move cursor to original position (using display column, not byte). */ + snprintf(seq,sizeof(seq),"\r\x1b[%dC", (int)(poscol+pwidth)); + abAppend(&ab,seq,strlen(seq)); + } + + if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ + abFree(&ab); +} + +/* Multi line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. + * + * Flags is REFRESH_* macros. The function can just remove the old + * prompt, just write it, or both. + * + * This function is UTF-8 aware and uses display widths for positioning. */ +static void refreshMultiLine(struct linenoiseState *l, int flags) { + char seq[64]; + size_t pwidth = utf8StrWidth(l->prompt, l->plen); /* Prompt display width */ + size_t bufwidth = utf8StrWidth(l->buf, l->len); /* Buffer display width */ + size_t poswidth = utf8StrWidth(l->buf, l->pos); /* Cursor display width */ + int rows = (pwidth+bufwidth+l->cols-1)/l->cols; /* rows used by current buf. */ + int rpos = l->oldrpos; /* cursor relative row from previous refresh. */ + int rpos2; /* rpos after refresh. */ + int col; /* column position, zero-based. */ + int old_rows = l->oldrows; + int fd = l->ofd, j; + struct abuf ab; + + l->oldrows = rows; + + /* First step: clear all the lines used before. To do so start by + * going to the last row. */ + abInit(&ab); + + if (flags & REFRESH_CLEAN) { + if (old_rows-rpos > 0) { + lndebug("go down %d", old_rows-rpos); + snprintf(seq,64,"\x1b[%dB", old_rows-rpos); + abAppend(&ab,seq,strlen(seq)); + } + + /* Now for every row clear it, go up. */ + for (j = 0; j < old_rows-1; j++) { + lndebug("clear+up"); + snprintf(seq,64,"\r\x1b[0K\x1b[1A"); + abAppend(&ab,seq,strlen(seq)); + } + } + + if (flags & REFRESH_ALL) { + /* Clean the top line. */ + lndebug("clear"); + snprintf(seq,64,"\r\x1b[0K"); + abAppend(&ab,seq,strlen(seq)); + } + + if (flags & REFRESH_WRITE) { + /* Write the prompt and the current buffer content */ + abAppend(&ab,l->prompt,l->plen); + if (maskmode == 1) { + /* In mask mode, output one '*' per UTF-8 character, not byte */ + size_t i = 0; + while (i < l->len) { + abAppend(&ab,"*",1); + i += utf8NextCharLen(l->buf, i, l->len); + } + } else { + abAppend(&ab,l->buf,l->len); + } + + /* Show hints if any. */ + refreshShowHints(&ab,l,pwidth); + + /* If we are at the very end of the screen with our prompt, we need to + * emit a newline and move the prompt to the first column. */ + if (l->pos && + l->pos == l->len && + (poswidth+pwidth) % l->cols == 0) + { + lndebug(""); + abAppend(&ab,"\n",1); + snprintf(seq,64,"\r"); + abAppend(&ab,seq,strlen(seq)); + rows++; + if (rows > (int)l->oldrows) l->oldrows = rows; + } + + /* Move cursor to right position. */ + rpos2 = (pwidth+poswidth+l->cols)/l->cols; /* Current cursor relative row */ + lndebug("rpos2 %d", rpos2); + + /* Go up till we reach the expected position. */ + if (rows-rpos2 > 0) { + lndebug("go-up %d", rows-rpos2); + snprintf(seq,64,"\x1b[%dA", rows-rpos2); + abAppend(&ab,seq,strlen(seq)); + } + + /* Set column. */ + col = (pwidth+poswidth) % l->cols; + lndebug("set col %d", 1+col); + if (col) + snprintf(seq,64,"\r\x1b[%dC", col); + else + snprintf(seq,64,"\r"); + abAppend(&ab,seq,strlen(seq)); + } + + lndebug("\n"); + l->oldpos = l->pos; + if (flags & REFRESH_WRITE) l->oldrpos = rpos2; + + if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ + abFree(&ab); +} + +/* Calls the two low level functions refreshSingleLine() or + * refreshMultiLine() according to the selected mode. */ +static void refreshLineWithFlags(struct linenoiseState *l, int flags) { + if (mlmode) + refreshMultiLine(l,flags); + else + refreshSingleLine(l,flags); +} + +/* Utility function to avoid specifying REFRESH_ALL all the times. */ +static void refreshLine(struct linenoiseState *l) { + refreshLineWithFlags(l,REFRESH_ALL); +} + +/* Hide the current line, when using the multiplexing API. */ +void linenoiseHide(struct linenoiseState *l) { + if (mlmode) + refreshMultiLine(l,REFRESH_CLEAN); + else + refreshSingleLine(l,REFRESH_CLEAN); +} + +/* Show the current line, when using the multiplexing API. */ +void linenoiseShow(struct linenoiseState *l) { + if (l->in_completion) { + refreshLineWithCompletion(l,NULL,REFRESH_WRITE); + } else { + refreshLineWithFlags(l,REFRESH_WRITE); + } +} + +/* Insert the character(s) 'c' of length 'clen' at cursor current position. + * This handles both single-byte ASCII and multi-byte UTF-8 sequences. + * + * On error writing to the terminal -1 is returned, otherwise 0. */ +int linenoiseEditInsert(struct linenoiseState *l, const char *c, size_t clen) { + if (l->len + clen <= l->buflen) { + if (l->len == l->pos) { + /* Append at end of line. */ + memcpy(l->buf+l->pos, c, clen); + l->pos += clen; + l->len += clen; + l->buf[l->len] = '\0'; + if ((!mlmode && + utf8StrWidth(l->prompt,l->plen)+utf8StrWidth(l->buf,l->len) < l->cols && + !hintsCallback)) { + /* Avoid a full update of the line in the trivial case: + * single-width char, no hints, fits in one line. */ + if (maskmode == 1) { + if (write(l->ofd,"*",1) == -1) return -1; + } else { + if (write(l->ofd,c,clen) == -1) return -1; + } + } else { + refreshLine(l); + } + } else { + /* Insert in the middle of the line. */ + memmove(l->buf+l->pos+clen, l->buf+l->pos, l->len-l->pos); + memcpy(l->buf+l->pos, c, clen); + l->len += clen; + l->pos += clen; + l->buf[l->len] = '\0'; + refreshLine(l); + } + } + return 0; +} + +/* Move cursor on the left. Moves by one UTF-8 character, not byte. */ +void linenoiseEditMoveLeft(struct linenoiseState *l) { + if (l->pos > 0) { + l->pos -= utf8PrevCharLen(l->buf, l->pos); + refreshLine(l); + } +} + +/* Move cursor on the right. Moves by one UTF-8 character, not byte. */ +void linenoiseEditMoveRight(struct linenoiseState *l) { + if (l->pos != l->len) { + l->pos += utf8NextCharLen(l->buf, l->pos, l->len); + refreshLine(l); + } +} + +/* Move cursor to the start of the line. */ +void linenoiseEditMoveHome(struct linenoiseState *l) { + if (l->pos != 0) { + l->pos = 0; + refreshLine(l); + } +} + +/* Move cursor to the end of the line. */ +void linenoiseEditMoveEnd(struct linenoiseState *l) { + if (l->pos != l->len) { + l->pos = l->len; + refreshLine(l); + } +} + +/* Substitute the currently edited line with the next or previous history + * entry as specified by 'dir'. */ +#define LINENOISE_HISTORY_NEXT 0 +#define LINENOISE_HISTORY_PREV 1 +void linenoiseEditHistoryNext(struct linenoiseState *l, int dir) { + if (history_len > 1) { + /* Update the current history entry before to + * overwrite it with the next one. */ + free(history[history_len - 1 - l->history_index]); + history[history_len - 1 - l->history_index] = strdup(l->buf); + /* Show the new entry */ + l->history_index += (dir == LINENOISE_HISTORY_PREV) ? 1 : -1; + if (l->history_index < 0) { + l->history_index = 0; + return; + } else if (l->history_index >= history_len) { + l->history_index = history_len-1; + return; + } + strncpy(l->buf,history[history_len - 1 - l->history_index],l->buflen); + l->buf[l->buflen-1] = '\0'; + l->len = l->pos = strlen(l->buf); + refreshLine(l); + } +} + +/* Delete the character at the right of the cursor without altering the cursor + * position. Basically this is what happens with the "Delete" keyboard key. + * Now handles multi-byte UTF-8 characters. */ +void linenoiseEditDelete(struct linenoiseState *l) { + if (l->len > 0 && l->pos < l->len) { + size_t clen = utf8NextCharLen(l->buf, l->pos, l->len); + memmove(l->buf+l->pos, l->buf+l->pos+clen, l->len-l->pos-clen); + l->len -= clen; + l->buf[l->len] = '\0'; + refreshLine(l); + } +} + +/* Backspace implementation. Deletes the UTF-8 character before the cursor. */ +void linenoiseEditBackspace(struct linenoiseState *l) { + if (l->pos > 0 && l->len > 0) { + size_t clen = utf8PrevCharLen(l->buf, l->pos); + memmove(l->buf+l->pos-clen, l->buf+l->pos, l->len-l->pos); + l->pos -= clen; + l->len -= clen; + l->buf[l->len] = '\0'; + refreshLine(l); + } +} + +/* Delete the previous word, maintaining the cursor at the start of the + * current word. Handles UTF-8 by moving character-by-character. */ +void linenoiseEditDeletePrevWord(struct linenoiseState *l) { + size_t old_pos = l->pos; + size_t diff; + + /* Skip spaces before the word (move backwards by UTF-8 chars). */ + while (l->pos > 0 && l->buf[l->pos-1] == ' ') + l->pos -= utf8PrevCharLen(l->buf, l->pos); + /* Skip non-space characters (move backwards by UTF-8 chars). */ + while (l->pos > 0 && l->buf[l->pos-1] != ' ') + l->pos -= utf8PrevCharLen(l->buf, l->pos); + diff = old_pos - l->pos; + memmove(l->buf+l->pos, l->buf+old_pos, l->len-old_pos+1); + l->len -= diff; + refreshLine(l); +} + +/* This function is part of the multiplexed API of Linenoise, that is used + * in order to implement the blocking variant of the API but can also be + * called by the user directly in an event driven program. It will: + * + * 1. Initialize the linenoise state passed by the user. + * 2. Put the terminal in RAW mode. + * 3. Show the prompt. + * 4. Return control to the user, that will have to call linenoiseEditFeed() + * each time there is some data arriving in the standard input. + * + * The user can also call linenoiseEditHide() and linenoiseEditShow() if it + * is required to show some input arriving asyncronously, without mixing + * it with the currently edited line. + * + * When linenoiseEditFeed() returns non-NULL, the user finished with the + * line editing session (pressed enter CTRL-D/C): in this case the caller + * needs to call linenoiseEditStop() to put back the terminal in normal + * mode. This will not destroy the buffer, as long as the linenoiseState + * is still valid in the context of the caller. + * + * The function returns 0 on success, or -1 if writing to standard output + * fails. If stdin_fd or stdout_fd are set to -1, the default is to use + * STDIN_FILENO and STDOUT_FILENO. + */ +int linenoiseEditStart(struct linenoiseState *l, int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt) { + /* Populate the linenoise state that we pass to functions implementing + * specific editing functionalities. */ + l->in_completion = 0; + l->ifd = stdin_fd != -1 ? stdin_fd : STDIN_FILENO; + l->ofd = stdout_fd != -1 ? stdout_fd : STDOUT_FILENO; + l->buf = buf; + l->buflen = buflen; + l->prompt = prompt; + l->plen = strlen(prompt); + l->oldpos = l->pos = 0; + l->len = 0; + + /* Enter raw mode. */ + if (enableRawMode(l->ifd) == -1) return -1; + + l->cols = getColumns(stdin_fd, stdout_fd); + l->oldrows = 0; + l->oldrpos = 1; /* Cursor starts on row 1. */ + l->history_index = 0; + + /* Buffer starts empty. */ + l->buf[0] = '\0'; + l->buflen--; /* Make sure there is always space for the nulterm */ + + /* If stdin is not a tty, stop here with the initialization. We + * will actually just read a line from standard input in blocking + * mode later, in linenoiseEditFeed(). */ + if (!isatty(l->ifd) && !getenv("LINENOISE_ASSUME_TTY")) return 0; + + /* The latest history entry is always our current buffer, that + * initially is just an empty string. */ + linenoiseHistoryAdd(""); + + if (write(l->ofd,prompt,l->plen) == -1) return -1; + return 0; +} + +char *linenoiseEditMore = "If you see this, you are misusing the API: when linenoiseEditFeed() is called, if it returns linenoiseEditMore the user is yet editing the line. See the README file for more information."; + +/* This function is part of the multiplexed API of linenoise, see the top + * comment on linenoiseEditStart() for more information. Call this function + * each time there is some data to read from the standard input file + * descriptor. In the case of blocking operations, this function can just be + * called in a loop, and block. + * + * The function returns linenoiseEditMore to signal that line editing is still + * in progress, that is, the user didn't yet pressed enter / CTRL-D. Otherwise + * the function returns the pointer to the heap-allocated buffer with the + * edited line, that the user should free with linenoiseFree(). + * + * On special conditions, NULL is returned and errno is populated: + * + * EAGAIN if the user pressed Ctrl-C + * ENOENT if the user pressed Ctrl-D + * + * Some other errno: I/O error. + */ +char *linenoiseEditFeed(struct linenoiseState *l) { + /* Not a TTY, pass control to line reading without character + * count limits. */ + if (!isatty(l->ifd) && !getenv("LINENOISE_ASSUME_TTY")) return linenoiseNoTTY(); + + char c; + int nread; + char seq[3]; + + nread = read(l->ifd,&c,1); + if (nread < 0) { + return (errno == EAGAIN || errno == EWOULDBLOCK) ? linenoiseEditMore : NULL; + } else if (nread == 0) { + return NULL; + } + + /* Only autocomplete when the callback is set. completeLine() + * returns the character to be handled next, or zero when the + * key was consumed to navigate completions. */ + if ((l->in_completion || c == 9 /* TAB */) && completionCallback != NULL) { + int retval = completeLine(l,c); + /* Read next character when 0 */ + if (retval == 0) return linenoiseEditMore; + c = retval; + } + + switch(c) { + case ENTER: /* enter */ + history_len--; + free(history[history_len]); + if (mlmode) linenoiseEditMoveEnd(l); + if (hintsCallback) { + /* Force a refresh without hints to leave the previous + * line as the user typed it after a newline. */ + linenoiseHintsCallback *hc = hintsCallback; + hintsCallback = NULL; + refreshLine(l); + hintsCallback = hc; + } + return strdup(l->buf); + case CTRL_C: /* ctrl-c */ + errno = EAGAIN; + return NULL; + case BACKSPACE: /* backspace */ + case 8: /* ctrl-h */ + linenoiseEditBackspace(l); + break; + case CTRL_D: /* ctrl-d, remove char at right of cursor, or if the + line is empty, act as end-of-file. */ + if (l->len > 0) { + linenoiseEditDelete(l); + } else { + history_len--; + free(history[history_len]); + errno = ENOENT; + return NULL; + } + break; + case CTRL_T: /* ctrl-t, swaps current character with previous. */ + /* Handle UTF-8: swap the two UTF-8 characters around cursor. */ + if (l->pos > 0 && l->pos < l->len) { + char tmp[32]; + size_t prevlen = utf8PrevCharLen(l->buf, l->pos); + size_t currlen = utf8NextCharLen(l->buf, l->pos, l->len); + size_t prevstart = l->pos - prevlen; + /* Copy current char to tmp, move previous char right, paste tmp. */ + memcpy(tmp, l->buf + l->pos, currlen); + memmove(l->buf + prevstart + currlen, l->buf + prevstart, prevlen); + memcpy(l->buf + prevstart, tmp, currlen); + if (l->pos + currlen <= l->len) l->pos += currlen; + refreshLine(l); + } + break; + case CTRL_B: /* ctrl-b */ + linenoiseEditMoveLeft(l); + break; + case CTRL_F: /* ctrl-f */ + linenoiseEditMoveRight(l); + break; + case CTRL_P: /* ctrl-p */ + linenoiseEditHistoryNext(l, LINENOISE_HISTORY_PREV); + break; + case CTRL_N: /* ctrl-n */ + linenoiseEditHistoryNext(l, LINENOISE_HISTORY_NEXT); + break; + case ESC: /* escape sequence */ + /* Read the next two bytes representing the escape sequence. + * Use two calls to handle slow terminals returning the two + * chars at different times. */ + if (read(l->ifd,seq,1) == -1) break; + if (read(l->ifd,seq+1,1) == -1) break; + + /* ESC [ sequences. */ + if (seq[0] == '[') { + if (seq[1] >= '0' && seq[1] <= '9') { + /* Extended escape, read additional byte. */ + if (read(l->ifd,seq+2,1) == -1) break; + if (seq[2] == '~') { + switch(seq[1]) { + case '3': /* Delete key. */ + linenoiseEditDelete(l); + break; + } + } + } else { + switch(seq[1]) { + case 'A': /* Up */ + linenoiseEditHistoryNext(l, LINENOISE_HISTORY_PREV); + break; + case 'B': /* Down */ + linenoiseEditHistoryNext(l, LINENOISE_HISTORY_NEXT); + break; + case 'C': /* Right */ + linenoiseEditMoveRight(l); + break; + case 'D': /* Left */ + linenoiseEditMoveLeft(l); + break; + case 'H': /* Home */ + linenoiseEditMoveHome(l); + break; + case 'F': /* End*/ + linenoiseEditMoveEnd(l); + break; + } + } + } + + /* ESC O sequences. */ + else if (seq[0] == 'O') { + switch(seq[1]) { + case 'H': /* Home */ + linenoiseEditMoveHome(l); + break; + case 'F': /* End*/ + linenoiseEditMoveEnd(l); + break; + } + } + break; + default: + /* Handle UTF-8 multi-byte sequences. When we receive the first byte + * of a multi-byte UTF-8 character, read the remaining bytes to + * complete the sequence before inserting. */ + { + char utf8[4]; + int utf8len = utf8ByteLen(c); + utf8[0] = c; + if (utf8len > 1) { + /* Read remaining bytes of the UTF-8 sequence. */ + int i; + for (i = 1; i < utf8len; i++) { + if (read(l->ifd, utf8+i, 1) != 1) break; + } + } + if (linenoiseEditInsert(l, utf8, utf8len)) return NULL; + } + break; + case CTRL_U: /* Ctrl+u, delete the whole line. */ + l->buf[0] = '\0'; + l->pos = l->len = 0; + refreshLine(l); + break; + case CTRL_K: /* Ctrl+k, delete from current to end of line. */ + l->buf[l->pos] = '\0'; + l->len = l->pos; + refreshLine(l); + break; + case CTRL_A: /* Ctrl+a, go to the start of the line */ + linenoiseEditMoveHome(l); + break; + case CTRL_E: /* ctrl+e, go to the end of the line */ + linenoiseEditMoveEnd(l); + break; + case CTRL_L: /* ctrl+l, clear screen */ + linenoiseClearScreen(); + refreshLine(l); + break; + case CTRL_W: /* ctrl+w, delete previous word */ + linenoiseEditDeletePrevWord(l); + break; + } + return linenoiseEditMore; +} + +/* This is part of the multiplexed linenoise API. See linenoiseEditStart() + * for more information. This function is called when linenoiseEditFeed() + * returns something different than NULL. At this point the user input + * is in the buffer, and we can restore the terminal in normal mode. */ +void linenoiseEditStop(struct linenoiseState *l) { + if (!isatty(l->ifd) && !getenv("LINENOISE_ASSUME_TTY")) return; + disableRawMode(l->ifd); + printf("\n"); +} + +/* This just implements a blocking loop for the multiplexed API. + * In many applications that are not event-drivern, we can just call + * the blocking linenoise API, wait for the user to complete the editing + * and return the buffer. */ +static char *linenoiseBlockingEdit(int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt) +{ + struct linenoiseState l; + + /* Editing without a buffer is invalid. */ + if (buflen == 0) { + errno = EINVAL; + return NULL; + } + + linenoiseEditStart(&l,stdin_fd,stdout_fd,buf,buflen,prompt); + char *res; + while((res = linenoiseEditFeed(&l)) == linenoiseEditMore); + linenoiseEditStop(&l); + return res; +} + +/* This special mode is used by linenoise in order to print scan codes + * on screen for debugging / development purposes. It is implemented + * by the linenoise_example program using the --keycodes option. */ +void linenoisePrintKeyCodes(void) { + char quit[4]; + + printf("Linenoise key codes debugging mode.\n" + "Press keys to see scan codes. Type 'quit' at any time to exit.\n"); + if (enableRawMode(STDIN_FILENO) == -1) return; + memset(quit,' ',4); + while(1) { + char c; + int nread; + + nread = read(STDIN_FILENO,&c,1); + if (nread <= 0) continue; + memmove(quit,quit+1,sizeof(quit)-1); /* shift string to left. */ + quit[sizeof(quit)-1] = c; /* Insert current char on the right. */ + if (memcmp(quit,"quit",sizeof(quit)) == 0) break; + + printf("'%c' %02x (%d) (type quit to exit)\n", + isprint(c) ? c : '?', (int)c, (int)c); + printf("\r"); /* Go left edge manually, we are in raw mode. */ + fflush(stdout); + } + disableRawMode(STDIN_FILENO); +} + +/* This function is called when linenoise() is called with the standard + * input file descriptor not attached to a TTY. So for example when the + * program using linenoise is called in pipe or with a file redirected + * to its standard input. In this case, we want to be able to return the + * line regardless of its length (by default we are limited to 4k). */ +static char *linenoiseNoTTY(void) { + char *line = NULL; + size_t len = 0, maxlen = 0; + + while(1) { + if (len == maxlen) { + if (maxlen == 0) maxlen = 16; + maxlen *= 2; + char *oldval = line; + line = realloc(line,maxlen); + if (line == NULL) { + if (oldval) free(oldval); + return NULL; + } + } + int c = fgetc(stdin); + if (c == EOF || c == '\n') { + if (c == EOF && len == 0) { + free(line); + return NULL; + } else { + line[len] = '\0'; + return line; + } + } else { + line[len] = c; + len++; + } + } +} + +/* The high level function that is the main API of the linenoise library. + * This function checks if the terminal has basic capabilities, just checking + * for a blacklist of stupid terminals, and later either calls the line + * editing function or uses dummy fgets() so that you will be able to type + * something even in the most desperate of the conditions. */ +char *linenoise(const char *prompt) { + char buf[LINENOISE_MAX_LINE]; + + if (!isatty(STDIN_FILENO) && !getenv("LINENOISE_ASSUME_TTY")) { + /* Not a tty: read from file / pipe. In this mode we don't want any + * limit to the line size, so we call a function to handle that. */ + return linenoiseNoTTY(); + } else if (isUnsupportedTerm()) { + size_t len; + + printf("%s",prompt); + fflush(stdout); + if (fgets(buf,LINENOISE_MAX_LINE,stdin) == NULL) return NULL; + len = strlen(buf); + while(len && (buf[len-1] == '\n' || buf[len-1] == '\r')) { + len--; + buf[len] = '\0'; + } + return strdup(buf); + } else { + char *retval = linenoiseBlockingEdit(STDIN_FILENO,STDOUT_FILENO,buf,LINENOISE_MAX_LINE,prompt); + return retval; + } +} + +/* This is just a wrapper the user may want to call in order to make sure + * the linenoise returned buffer is freed with the same allocator it was + * created with. Useful when the main program is using an alternative + * allocator. */ +void linenoiseFree(void *ptr) { + if (ptr == linenoiseEditMore) return; // Protect from API misuse. + free(ptr); +} + +/* ================================ History ================================= */ + +/* Free the history, but does not reset it. Only used when we have to + * exit() to avoid memory leaks are reported by valgrind & co. */ +static void freeHistory(void) { + if (history) { + int j; + + for (j = 0; j < history_len; j++) + free(history[j]); + free(history); + } +} + +/* At exit we'll try to fix the terminal to the initial conditions. */ +static void linenoiseAtExit(void) { + disableRawMode(STDIN_FILENO); + freeHistory(); +} + +/* This is the API call to add a new entry in the linenoise history. + * It uses a fixed array of char pointers that are shifted (memmoved) + * when the history max length is reached in order to remove the older + * entry and make room for the new one, so it is not exactly suitable for huge + * histories, but will work well for a few hundred of entries. + * + * Using a circular buffer is smarter, but a bit more complex to handle. */ +int linenoiseHistoryAdd(const char *line) { + char *linecopy; + + if (history_max_len == 0) return 0; + + /* Initialization on first call. */ + if (history == NULL) { + history = malloc(sizeof(char*)*history_max_len); + if (history == NULL) return 0; + memset(history,0,(sizeof(char*)*history_max_len)); + } + + /* Don't add duplicated lines. */ + if (history_len && !strcmp(history[history_len-1], line)) return 0; + + /* Add an heap allocated copy of the line in the history. + * If we reached the max length, remove the older line. */ + linecopy = strdup(line); + if (!linecopy) return 0; + if (history_len == history_max_len) { + free(history[0]); + memmove(history,history+1,sizeof(char*)*(history_max_len-1)); + history_len--; + } + history[history_len] = linecopy; + history_len++; + return 1; +} + +/* Set the maximum length for the history. This function can be called even + * if there is already some history, the function will make sure to retain + * just the latest 'len' elements if the new history length value is smaller + * than the amount of items already inside the history. */ +int linenoiseHistorySetMaxLen(int len) { + char **new; + + if (len < 1) return 0; + if (history) { + int tocopy = history_len; + + new = malloc(sizeof(char*)*len); + if (new == NULL) return 0; + + /* If we can't copy everything, free the elements we'll not use. */ + if (len < tocopy) { + int j; + + for (j = 0; j < tocopy-len; j++) free(history[j]); + tocopy = len; + } + memset(new,0,sizeof(char*)*len); + memcpy(new,history+(history_len-tocopy), sizeof(char*)*tocopy); + free(history); + history = new; + } + history_max_len = len; + if (history_len > history_max_len) + history_len = history_max_len; + return 1; +} + +/* Save the history in the specified file. On success 0 is returned + * otherwise -1 is returned. */ +int linenoiseHistorySave(const char *filename) { + mode_t old_umask = umask(S_IXUSR|S_IRWXG|S_IRWXO); + FILE *fp; + int j; + + fp = fopen(filename,"w"); + umask(old_umask); + if (fp == NULL) return -1; + fchmod(fileno(fp),S_IRUSR|S_IWUSR); + for (j = 0; j < history_len; j++) + fprintf(fp,"%s\n",history[j]); + fclose(fp); + return 0; +} + +/* Load the history from the specified file. If the file does not exist + * zero is returned and no operation is performed. + * + * If the file exists and the operation succeeded 0 is returned, otherwise + * on error -1 is returned. */ +int linenoiseHistoryLoad(const char *filename) { + FILE *fp = fopen(filename,"r"); + char buf[LINENOISE_MAX_LINE]; + + if (fp == NULL) return -1; + + while (fgets(buf,LINENOISE_MAX_LINE,fp) != NULL) { + char *p; + + p = strchr(buf,'\r'); + if (!p) p = strchr(buf,'\n'); + if (p) *p = '\0'; + linenoiseHistoryAdd(buf); + } + fclose(fp); + return 0; +} diff --git a/src/pc/linenoise/linenoise.h b/src/pc/linenoise/linenoise.h new file mode 100644 index 000000000..e56b62714 --- /dev/null +++ b/src/pc/linenoise/linenoise.h @@ -0,0 +1,114 @@ +/* linenoise.h -- VERSION 1.0 + * + * Guerrilla line editing library against the idea that a line editing lib + * needs to be 20,000 lines of C code. + * + * See linenoise.c for more information. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010-2023, Salvatore Sanfilippo + * Copyright (c) 2010-2013, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __LINENOISE_H +#define __LINENOISE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include /* For size_t. */ + +extern char *linenoiseEditMore; + +/* The linenoiseState structure represents the state during line editing. + * We pass this state to functions implementing specific editing + * functionalities. */ +struct linenoiseState { + int in_completion; /* The user pressed TAB and we are now in completion + * mode, so input is handled by completeLine(). */ + size_t completion_idx; /* Index of next completion to propose. */ + int ifd; /* Terminal stdin file descriptor. */ + int ofd; /* Terminal stdout file descriptor. */ + char *buf; /* Edited line buffer. */ + size_t buflen; /* Edited line buffer size. */ + const char *prompt; /* Prompt to display. */ + size_t plen; /* Prompt length. */ + size_t pos; /* Current cursor position. */ + size_t oldpos; /* Previous refresh cursor position. */ + size_t len; /* Current edited line length. */ + size_t cols; /* Number of columns in terminal. */ + size_t oldrows; /* Rows used by last refrehsed line (multiline mode) */ + int oldrpos; /* Cursor row from last refresh (for multiline clearing). */ + int history_index; /* The history index we are currently editing. */ +}; + +typedef struct linenoiseCompletions { + size_t len; + char **cvec; +} linenoiseCompletions; + +/* Non blocking API. */ +int linenoiseEditStart(struct linenoiseState *l, int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt); +char *linenoiseEditFeed(struct linenoiseState *l); +void linenoiseEditStop(struct linenoiseState *l); +void linenoiseHide(struct linenoiseState *l); +void linenoiseShow(struct linenoiseState *l); + +/* Blocking API. */ +char *linenoise(const char *prompt); +void linenoiseFree(void *ptr); + +/* Completion API. */ +typedef void(linenoiseCompletionCallback)(const char *, linenoiseCompletions *); +typedef char*(linenoiseHintsCallback)(const char *, int *color, int *bold); +typedef void(linenoiseFreeHintsCallback)(void *); +void linenoiseSetCompletionCallback(linenoiseCompletionCallback *); +void linenoiseSetHintsCallback(linenoiseHintsCallback *); +void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *); +void linenoiseAddCompletion(linenoiseCompletions *, const char *); + +/* History API. */ +int linenoiseHistoryAdd(const char *line); +int linenoiseHistorySetMaxLen(int len); +int linenoiseHistorySave(const char *filename); +int linenoiseHistoryLoad(const char *filename); + +/* Other utilities. */ +void linenoiseClearScreen(void); +void linenoiseSetMultiLine(int ml); +void linenoisePrintKeyCodes(void); +void linenoiseMaskModeEnable(void); +void linenoiseMaskModeDisable(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __LINENOISE_H */ diff --git a/src/pc/lua/smlua.h b/src/pc/lua/smlua.h index ddaf22182..f69cf4030 100644 --- a/src/pc/lua/smlua.h +++ b/src/pc/lua/smlua.h @@ -19,9 +19,9 @@ #include "pc/debuglog.h" #include "pc/djui/djui_console.h" -#define LOG_LUA(...) { if (!gSmLuaSuppressErrors) { printf("[LUA] "), printf(__VA_ARGS__), printf("\n"), smlua_mod_error(), snprintf(gDjuiConsoleTmpBuffer, CONSOLE_MAX_TMP_BUFFER, __VA_ARGS__), sys_swap_backslashes(gDjuiConsoleTmpBuffer), djui_console_message_create(gDjuiConsoleTmpBuffer, CONSOLE_MESSAGE_ERROR); } } -#define LOG_LUA_LINE(...) { if (!gSmLuaSuppressErrors) { printf("[LUA] "), printf(__VA_ARGS__), printf("\n"), smlua_mod_error(); snprintf(gDjuiConsoleTmpBuffer, CONSOLE_MAX_TMP_BUFFER, __VA_ARGS__), sys_swap_backslashes(gDjuiConsoleTmpBuffer), djui_console_message_create(gDjuiConsoleTmpBuffer, CONSOLE_MESSAGE_ERROR), smlua_logline(); } } -#define LOG_LUA_LINE_WARNING(...) { if (!gLuaActiveMod->showedScriptWarning) { gLuaActiveMod->showedScriptWarning = true; smlua_mod_warning(); snprintf(gDjuiConsoleTmpBuffer, CONSOLE_MAX_TMP_BUFFER, __VA_ARGS__), sys_swap_backslashes(gDjuiConsoleTmpBuffer), djui_console_message_create(gDjuiConsoleTmpBuffer, CONSOLE_MESSAGE_WARNING); } } +#define LOG_LUA(...) { if (!gSmLuaSuppressErrors) { log_to_terminal("\x1b[31m[LUA] "); log_to_terminal(__VA_ARGS__); log_to_terminal("\x1b[0m\n"); smlua_mod_error(); snprintf(gDjuiConsoleTmpBuffer, CONSOLE_MAX_TMP_BUFFER, __VA_ARGS__); sys_swap_backslashes(gDjuiConsoleTmpBuffer); djui_console_message_create(gDjuiConsoleTmpBuffer, CONSOLE_MESSAGE_ERROR); } } +#define LOG_LUA_LINE(...) { if (!gSmLuaSuppressErrors) { log_to_terminal("\x1b[31m[LUA] "); log_to_terminal(__VA_ARGS__); log_to_terminal("\x1b[0m\n"); smlua_mod_error(); snprintf(gDjuiConsoleTmpBuffer, CONSOLE_MAX_TMP_BUFFER, __VA_ARGS__); sys_swap_backslashes(gDjuiConsoleTmpBuffer); djui_console_message_create(gDjuiConsoleTmpBuffer, CONSOLE_MESSAGE_ERROR); smlua_logline(); } } +#define LOG_LUA_LINE_WARNING(...) { if (!gLuaActiveMod->showedScriptWarning) { gLuaActiveMod->showedScriptWarning = true; log_to_terminal("\x1b[33m[LUA] "); log_to_terminal(__VA_ARGS__); log_to_terminal("\x1b[0m\n"); smlua_mod_warning(); snprintf(gDjuiConsoleTmpBuffer, CONSOLE_MAX_TMP_BUFFER, __VA_ARGS__); sys_swap_backslashes(gDjuiConsoleTmpBuffer); djui_console_message_create(gDjuiConsoleTmpBuffer, CONSOLE_MESSAGE_WARNING); } } #ifdef DEVELOPMENT #define LUA_STACK_CHECK_BEGIN_NUM(state, n) int __LUA_STACK_TOP = lua_gettop(state) + (n) diff --git a/src/pc/lua/smlua_constants_autogen.c b/src/pc/lua/smlua_constants_autogen.c index de7da771c..546d33e96 100644 --- a/src/pc/lua/smlua_constants_autogen.c +++ b/src/pc/lua/smlua_constants_autogen.c @@ -1485,6 +1485,7 @@ char gSmluaConstants[] = "" "DIALOG_168=168\n" "DIALOG_169=169\n" "DIALOG_COUNT=170\n" +"MAX_CONSOLE_INPUT_LENGTH=500\n" "CONSOLE_MESSAGE_INFO=0\n" "CONSOLE_MESSAGE_WARNING=1\n" "CONSOLE_MESSAGE_ERROR=2\n" diff --git a/src/pc/lua/smlua_functions.c b/src/pc/lua/smlua_functions.c index ba79220a8..6a9441697 100644 --- a/src/pc/lua/smlua_functions.c +++ b/src/pc/lua/smlua_functions.c @@ -40,6 +40,52 @@ bool smlua_functions_valid_param_range(lua_State* L, int min, int max) { return true; } + /////////// + // print // +/////////// + +int smlua_func_print(lua_State *L) { + int top = lua_gettop(L); + + // calculate total length first + size_t totalLen = 0; + for (int i = 1; i <= top; i++) { + size_t len; + luaL_tolstring(L, i, &len); + totalLen += len; + if (i > 1) totalLen += 1; + } + + // allocate string + char* completeString = malloc(totalLen + 1); + if (!completeString) return 0; + + size_t pos = 0; + + // copy string + for (int i = 1; i <= top; i++) { + size_t len; + const char* str = luaL_tolstring(L, i, &len); + + if (i > 1) { + completeString[pos] = '\t'; + pos += 1; + } + + memcpy(completeString + pos, str, len); + pos += len; + } + + completeString[pos] = '\0'; + + // print to terminal and console + log_to_terminal("%s\n", completeString); + djui_console_message_create(completeString, CONSOLE_MESSAGE_INFO); + + free(completeString); + return 1; +} + /////////// // table // /////////// @@ -806,6 +852,13 @@ int smlua_func_log_to_console(lua_State* L) { } djui_console_message_create(message, level); + char* colorCode; + switch (level) { + case CONSOLE_MESSAGE_WARNING: colorCode = "\x1b[33m"; break; + case CONSOLE_MESSAGE_ERROR: colorCode = "\x1b[31m"; break; + default: colorCode = "\x1b[0m"; break; + } + log_to_terminal("%s%s\x1b[0m\n", colorCode, message); return 1; } @@ -1013,6 +1066,7 @@ void smlua_bind_functions(void) { lua_State* L = gLuaState; // misc + smlua_bind_function(L, "print", smlua_func_print); smlua_bind_function(L, "table_copy", smlua_func_table_copy); smlua_bind_function(L, "table_deepcopy", smlua_func_table_deepcopy); smlua_bind_function(L, "init_mario_after_warp", smlua_func_init_mario_after_warp); diff --git a/src/pc/lua/smlua_functions_autogen.c b/src/pc/lua/smlua_functions_autogen.c index a41d6f864..5288e8a69 100644 --- a/src/pc/lua/smlua_functions_autogen.c +++ b/src/pc/lua/smlua_functions_autogen.c @@ -12,6 +12,7 @@ #include "src/game/mario_step.h" #include "src/game/mario.h" #include "src/game/rumble_init.h" +#include "src/pc/commands.h" #include "src/pc/djui/djui_popup.h" #include "src/pc/network/network_utils.h" #include "src/pc/djui/djui_console.h" @@ -12125,6 +12126,32 @@ int smlua_func_update_character_anim_offset(lua_State* L) { return 1; } + //////////////// + // commands.h // +//////////////// + +int smlua_func_command_message_create(lua_State* L) { + if (L == NULL) { return 0; } + + int top = lua_gettop(L); + if (top < 1 || top > 2) { + LOG_LUA_LINE("Improper param count for '%s': Expected between %u and %u, Received %u", "command_message_create", 1, 2, top); + return 0; + } + + const char* message = smlua_to_string(L, 1); + if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 1, "command_message_create"); return 0; } + int level = (int) 0; + if (top >= 2) { + level = smlua_to_integer(L, 2); + if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 2, "command_message_create"); return 0; } + } + + command_message_create(message, level); + + return 1; +} + ///////////////////////// // djui_chat_message.h // ///////////////////////// @@ -37519,6 +37546,9 @@ void smlua_bind_functions_autogen(void) { smlua_bind_function(L, "get_character_anim", smlua_func_get_character_anim); smlua_bind_function(L, "update_character_anim_offset", smlua_func_update_character_anim_offset); + // commands.h + smlua_bind_function(L, "command_message_create", smlua_func_command_message_create); + // djui_chat_message.h smlua_bind_function(L, "djui_chat_message_create", smlua_func_djui_chat_message_create); diff --git a/src/pc/lua/smlua_hooks.c b/src/pc/lua/smlua_hooks.c index 0984c8223..09ad1eeb3 100644 --- a/src/pc/lua/smlua_hooks.c +++ b/src/pc/lua/smlua_hooks.c @@ -14,7 +14,7 @@ #include "pc/network/network.h" #include "pc/network/network_player.h" #include "pc/network/socket/socket.h" -#include "pc/chat_commands.h" +#include "pc/commands.h" #include "pc/pc_main.h" #include "pc/djui/djui_lua_profiler.h" #include "pc/djui/djui_panel.h" @@ -1009,62 +1009,72 @@ bool smlua_call_behavior_hook(const BehaviorScript** behavior, struct Object* ob // hooked chat command // ///////////////////////// -struct LuaHookedChatCommand { +struct LuaHookedCommand { char* command; char* description; int reference; struct Mod* mod; struct ModFile* modFile; + bool isConsoleCommand; }; #define MAX_HOOKED_CHAT_COMMANDS 512 -static struct LuaHookedChatCommand sHookedChatCommands[MAX_HOOKED_CHAT_COMMANDS] = { 0 }; +static struct LuaHookedCommand sHookedChatCommands[MAX_HOOKED_CHAT_COMMANDS] = { 0 }; static int sHookedChatCommandsCount = 0; -int smlua_hook_chat_command(lua_State* L) { +int smlua_hook_command_internal(lua_State* L, bool isConsoleCommand) { if (L == NULL) { return 0; } if (!smlua_functions_valid_param_count(L, 3)) { return 0; } if (gLuaLoadingMod == NULL) { - LOG_LUA_LINE("hook_chat_command() can only be called on load."); + LOG_LUA_LINE("%s can only be called on load.", isConsoleCommand ? "hook_console_command()" : "hook_chat_command()"); return 0; } if (sHookedChatCommandsCount >= MAX_HOOKED_CHAT_COMMANDS) { - LOG_LUA_LINE("Hooked chat command exceeded maximum references!"); + LOG_LUA_LINE("Hooked command exceeded maximum references!"); return 0; } const char* command = smlua_to_string(L, 1); if (command == NULL || strlen(command) == 0 || !gSmLuaConvertSuccess) { - LOG_LUA_LINE("Hook chat command: tried to hook invalid command"); + LOG_LUA_LINE("Hook command: tried to hook invalid command"); return 0; } const char* description = smlua_to_string(L, 2); if (description == NULL || strlen(description) == 0 || !gSmLuaConvertSuccess) { - LOG_LUA_LINE("Hook chat command: tried to hook invalid description"); + LOG_LUA_LINE("Hook command: tried to hook invalid description"); return 0; } int ref = luaL_ref(L, LUA_REGISTRYINDEX); if (ref == -1) { - LOG_LUA_LINE("Hook chat command: tried to hook undefined function '%s'", command); + LOG_LUA_LINE("Hook command: tried to hook undefined function '%s'", command); return 0; } - struct LuaHookedChatCommand* hooked = &sHookedChatCommands[sHookedChatCommandsCount]; + struct LuaHookedCommand* hooked = &sHookedChatCommands[sHookedChatCommandsCount]; hooked->command = strdup(command); hooked->description = strdup(description); hooked->reference = ref; hooked->mod = gLuaActiveMod; hooked->modFile = gLuaActiveModFile; + hooked->isConsoleCommand = isConsoleCommand; sHookedChatCommandsCount++; return 1; } +int smlua_hook_chat_command(lua_State* L) { + return smlua_hook_command_internal(L, false); +} + +int smlua_hook_console_command(lua_State* L) { + return smlua_hook_command_internal(L, true); +} + int smlua_update_chat_command_description(lua_State* L) { if (L == NULL) { return 0; } if (!smlua_functions_valid_param_count(L, 2)) { return 0; } @@ -1082,8 +1092,8 @@ int smlua_update_chat_command_description(lua_State* L) { } for (int i = 0; i < sHookedChatCommandsCount; i++) { - struct LuaHookedChatCommand* hook = &sHookedChatCommands[i]; - if (!strcmp(hook->command, command)) { + struct LuaHookedCommand* hook = &sHookedChatCommands[i]; + if (!hook->isConsoleCommand && !strcmp(hook->command, command)) { if (hook->description) { free(hook->description); } @@ -1096,19 +1106,50 @@ int smlua_update_chat_command_description(lua_State* L) { return 0; } +int smlua_update_console_command_description(lua_State* L) { + if (L == NULL) { return 0; } + if (!smlua_functions_valid_param_count(L, 2)) { return 0; } + + const char* command = smlua_to_string(L, 1); + if (command == NULL || strlen(command) == 0 || !gSmLuaConvertSuccess) { + LOG_LUA_LINE("Update console command: tried to update invalid command"); + return 0; + } + + const char* description = smlua_to_string(L, 2); + if (description == NULL || strlen(description) == 0 || !gSmLuaConvertSuccess) { + LOG_LUA_LINE("Update console command: tried to update invalid description"); + return 0; + } + + for (int i = 0; i < sHookedChatCommandsCount; i++) { + struct LuaHookedCommand* hook = &sHookedChatCommands[i]; + if (hook->isConsoleCommand && !strcmp(hook->command, command)) { + if (hook->description) { + free(hook->description); + } + hook->description = strdup(description); + return 1; + } + } + + LOG_LUA_LINE("Update console command: could not find command to update"); + return 0; +} + bool smlua_call_chat_command_hook(char* command) { lua_State* L = gLuaState; if (L == NULL) { return false; } for (int i = 0; i < sHookedChatCommandsCount; i++) { - struct LuaHookedChatCommand* hook = &sHookedChatCommands[i]; + struct LuaHookedCommand* hook = &sHookedChatCommands[i]; + // compare strings size_t commandLength = strlen(hook->command); - for (size_t j = 0; j < commandLength; j++) { - if (hook->command[j] != command[j + 1]) { - goto NEXT_HOOK; - } - } + if (strncmp(hook->command, command, commandLength) != 0) goto NEXT_HOOK; - char* params = &command[commandLength + 1]; + // make sure we aren't running a console command + if (hook->isConsoleCommand && !gDjuiConsoleFocus) goto NEXT_HOOK; + + char* params = &command[commandLength]; if (*params != '\0' && *params != ' ') { goto NEXT_HOOK; } @@ -1145,12 +1186,13 @@ NEXT_HOOK:; return false; } -void smlua_display_chat_commands(void) { +void smlua_display_chat_commands(bool isConsole) { for (int i = 0; i < sHookedChatCommandsCount; i++) { - struct LuaHookedChatCommand* hook = &sHookedChatCommands[i]; + struct LuaHookedCommand* hook = &sHookedChatCommands[i]; + if (!isConsole && hook->isConsoleCommand) continue; char msg[256] = { 0 }; snprintf(msg, 256, "/%s %s", hook->command, hook->description); - djui_chat_message_create(msg); + command_message_create(msg, CONSOLE_MESSAGE_INFO); } } @@ -1207,6 +1249,7 @@ char** smlua_get_chat_player_list(void) { return sortedPlayers; } +// this needs a rewrite, actually all these funcs needs a rewrite, actually, the whole autocomplete system needs a rewrite char** smlua_get_chat_maincommands_list(void) { #if defined(DEVELOPMENT) s32 defaultCmdsCount = 11; @@ -1227,7 +1270,8 @@ char** smlua_get_chat_maincommands_list(void) { } char** commands = (char**) malloc((sHookedChatCommandsCount + defaultCmdsCountNew + 1) * sizeof(char*)); for (s32 i = 0; i < sHookedChatCommandsCount; i++) { - struct LuaHookedChatCommand* hook = &sHookedChatCommands[i]; + struct LuaHookedCommand* hook = &sHookedChatCommands[i]; + if (hook->isConsoleCommand) continue; commands[i] = strdup(hook->command); } for (s32 i = 0; i < defaultCmdsCount; i++) { @@ -1251,7 +1295,7 @@ char** smlua_get_chat_subcommands_list(const char* maincommand) { } for (s32 i = 0; i < sHookedChatCommandsCount; i++) { - struct LuaHookedChatCommand* hook = &sHookedChatCommands[i]; + struct LuaHookedCommand* hook = &sHookedChatCommands[i]; if (strcmp(hook->command, maincommand) == 0) { char* noColorsDesc = djui_text_get_uncolored_string(NULL, strlen(hook->description) + 1, hook->description); char* startSubcommands = strstr(noColorsDesc, "["); @@ -1793,7 +1837,7 @@ void smlua_hook_replace_function_references(lua_State* L, int oldReference, int } for (int i = 0; i < sHookedChatCommandsCount; i++) { - struct LuaHookedChatCommand* hooked = &sHookedChatCommands[i]; + struct LuaHookedCommand* hooked = &sHookedChatCommands[i]; smlua_hook_replace_function_reference(L, &hooked->reference, oldReference, newReference); } @@ -1829,7 +1873,7 @@ void smlua_clear_hooks(void) { sHookedMarioActionsCount = 0; for (int i = 0; i < sHookedChatCommandsCount; i++) { - struct LuaHookedChatCommand* hooked = &sHookedChatCommands[i]; + struct LuaHookedCommand* hooked = &sHookedChatCommands[i]; if (hooked->command != NULL) { free(hooked->command); } hooked->command = NULL; @@ -1896,6 +1940,7 @@ void smlua_bind_hooks(void) { smlua_bind_function(L, "hook_event", smlua_hook_event); smlua_bind_function(L, "hook_mario_action", smlua_hook_mario_action); smlua_bind_function(L, "hook_chat_command", smlua_hook_chat_command); + smlua_bind_function(L, "hook_console_command", smlua_hook_console_command); smlua_bind_function(L, "hook_on_sync_table_change", smlua_hook_on_sync_table_change); smlua_bind_function(L, "hook_behavior", smlua_hook_behavior); smlua_bind_function(L, "hook_mod_menu_text", smlua_hook_mod_menu_text); @@ -1904,6 +1949,7 @@ void smlua_bind_hooks(void) { smlua_bind_function(L, "hook_mod_menu_slider", smlua_hook_mod_menu_slider); smlua_bind_function(L, "hook_mod_menu_inputbox", smlua_hook_mod_menu_inputbox); smlua_bind_function(L, "update_chat_command_description", smlua_update_chat_command_description); + smlua_bind_function(L, "update_console_command_description", smlua_update_console_command_description); smlua_bind_function(L, "update_mod_menu_element_name", smlua_update_mod_menu_element_name); smlua_bind_function(L, "update_mod_menu_element_checkbox", smlua_update_mod_menu_element_checkbox); smlua_bind_function(L, "update_mod_menu_element_slider", smlua_update_mod_menu_element_slider); diff --git a/src/pc/lua/smlua_hooks.h b/src/pc/lua/smlua_hooks.h index c814c73e9..a54da76dd 100644 --- a/src/pc/lua/smlua_hooks.h +++ b/src/pc/lua/smlua_hooks.h @@ -177,7 +177,7 @@ bool smlua_call_action_hook(enum LuaActionHookType hookType, struct MarioState* u32 smlua_get_action_interaction_type(struct MarioState* m); bool smlua_call_chat_command_hook(char* command); -void smlua_display_chat_commands(void); +void smlua_display_chat_commands(bool isConsole); char** smlua_get_chat_player_list(void); char** smlua_get_chat_maincommands_list(void); char** smlua_get_chat_subcommands_list(const char* maincommand); diff --git a/src/pc/lua/smlua_utils.c b/src/pc/lua/smlua_utils.c index 3ccda2a7f..af1685a64 100644 --- a/src/pc/lua/smlua_utils.c +++ b/src/pc/lua/smlua_utils.c @@ -743,33 +743,33 @@ const char* smlua_lnt_to_str(struct LSTNetworkType* lnt) { void smlua_dump_stack(void) { lua_State* L = gLuaState; int top = lua_gettop(L); - printf("--------------\n"); + log_to_terminal("--------------\n"); for (int i = 1; i <= top; i++) { - printf("%d\t%s\t", i, luaL_typename(L, i)); + log_to_terminal("%d\t%s\t", i, luaL_typename(L, i)); switch (lua_type(L, i)) { case LUA_TNUMBER: - printf("%g\n", lua_tonumber(L, i)); + log_to_terminal("%g\n", lua_tonumber(L, i)); break; case LUA_TSTRING: - printf("%s\n", lua_tostring(L, i)); + log_to_terminal("%s\n", lua_tostring(L, i)); break; case LUA_TBOOLEAN: - printf("%s\n", (lua_toboolean(L, i) ? "true" : "false")); + log_to_terminal("%s\n", (lua_toboolean(L, i) ? "true" : "false")); break; case LUA_TNIL: - printf("%s\n", "nil"); + log_to_terminal("%s\n", "nil"); break; default: - printf("%p\n", lua_topointer(L, i)); + log_to_terminal("%p\n", lua_topointer(L, i)); break; } } - printf("--------------\n"); + log_to_terminal("--------------\n"); } void smlua_dump_globals(void) { lua_State* L = gLuaState; - printf("--------------\n"); + log_to_terminal("--------------\n"); lua_pushglobaltable(L); // table is in the stack at index 't' @@ -777,12 +777,12 @@ void smlua_dump_globals(void) { while (lua_next(L, -2) != 0) { // uses 'key' (at index -2) and 'value' (at index -1) if (lua_type(L, -2) == LUA_TSTRING) { - printf("%s - %s\n", + log_to_terminal("%s - %s\n", lua_tostring(L, -2), lua_typename(L, lua_type(L, -1))); } else { - printf("%s - %s\n", + log_to_terminal("%s - %s\n", lua_typename(L, lua_type(L, -2)), lua_typename(L, lua_type(L, -1))); } @@ -790,23 +790,23 @@ void smlua_dump_globals(void) { lua_pop(L, 1); } lua_pop(L, 1); // remove global table(-1) - printf("--------------\n"); + log_to_terminal("--------------\n"); } void smlua_dump_table(int index) { lua_State* L = gLuaState; - printf("--------------\n"); + log_to_terminal("--------------\n"); if (lua_getmetatable(L, index)) { lua_pushnil(L); // first key while (lua_next(L, -2) != 0) { if (lua_type(L, -2) == LUA_TSTRING) { - printf("[meta] %s - %s\n", + log_to_terminal("[meta] %s - %s\n", lua_tostring(L, -2), lua_typename(L, lua_type(L, -1))); } else { - printf("[meta] %s - %s\n", + log_to_terminal("[meta] %s - %s\n", lua_typename(L, lua_type(L, -2)), lua_typename(L, lua_type(L, -1))); } @@ -820,19 +820,19 @@ void smlua_dump_table(int index) { while (lua_next(L, index) != 0) { // uses 'key' (at index -2) and 'value' (at index -1) if (lua_type(L, -2) == LUA_TSTRING) { - printf("%s - %s\n", + log_to_terminal("%s - %s\n", lua_tostring(L, -2), lua_typename(L, lua_type(L, -1))); } else { - printf("%s - %s\n", + log_to_terminal("%s - %s\n", lua_typename(L, lua_type(L, -2)), lua_typename(L, lua_type(L, -1))); } // removes 'value'; keeps 'key' for next iteration lua_pop(L, 1); } - printf("--------------\n"); + log_to_terminal("--------------\n"); } void smlua_logline(void) { diff --git a/src/pc/pc_main.c b/src/pc/pc_main.c index 91f689b6d..e97dd29ec 100644 --- a/src/pc/pc_main.c +++ b/src/pc/pc_main.c @@ -66,6 +66,8 @@ #include "pc/discord/discord.h" #endif +#include "pc/terminal.h" + #include "pc/mumble/mumble.h" #if defined(_WIN32) || defined(_WIN64) @@ -529,7 +531,7 @@ int main(int argc, char *argv[]) { } else #endif { - printf("ERROR: could not find valid vanilla us sm64 rom in game's user folder\n"); + LOG_ERROR("Could not find valid vanilla us sm64 rom in game's user folder\n"); return 0; } } @@ -602,6 +604,9 @@ int main(int argc, char *argv[]) { network_init(NT_NONE, false); } + // initialize terminal + terminal_init(); + // main loop while (true) { debug_context_reset(); @@ -611,6 +616,7 @@ int main(int argc, char *argv[]) { discord_update(); #endif mumble_update(); + terminal_update(); #ifdef DEBUG fflush(stdout); fflush(stderr); diff --git a/src/pc/terminal.c b/src/pc/terminal.c new file mode 100644 index 000000000..c42ba00f0 --- /dev/null +++ b/src/pc/terminal.c @@ -0,0 +1,102 @@ +#include +#if !defined(_WIN32) && !defined(_WIN64) +#include +#include +#include +#include "linenoise/linenoise.h" +#endif +#include "djui/djui_console.h" +#include "commands.h" +#include "pc_main.h" +#include "terminal.h" + +#define TERMINAL_BUFFER_SIZE 1024 + +#if !defined(_WIN32) && !defined(_WIN64) +static struct linenoiseState sLinenoiseState; +#endif +static char sTerminalInput[TERMINAL_BUFFER_SIZE] = { 0 }; +static bool sTerminalInitialized = false; +static bool sTerminalActive = false; + +void log_to_terminal(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + +#if !defined(_WIN32) && !defined(_WIN64) + if (sTerminalActive) linenoiseHide(&sLinenoiseState); +#endif + + vprintf(fmt, args); + +#if !defined(_WIN32) && !defined(_WIN64) + if (sTerminalActive) linenoiseShow(&sLinenoiseState); +#endif + + va_end(args); +} + +static void terminal_stop() { +#if !defined(_WIN32) && !defined(_WIN64) + if (!sTerminalInitialized) return; + linenoiseEditStop(&sLinenoiseState); + sTerminalActive = false; +#endif +} + +void terminal_init() { +#if !defined(_WIN32) && !defined(_WIN64) + if (!isatty(STDIN_FILENO)) { + sTerminalActive = false; + sTerminalInitialized = false; + return; + } + + if (tcgetpgrp(STDIN_FILENO) != getpgrp()) { + sTerminalActive = false; + sTerminalInitialized = false; + return; + } + + linenoiseEditStart(&sLinenoiseState, -1, -1, sTerminalInput, sizeof(sTerminalInput), "> "); + sTerminalInitialized = true; + sTerminalActive = true; +#endif +} + +void terminal_update() { +#if !defined(_WIN32) && !defined(_WIN64) + if (!sTerminalInitialized) return; + struct timeval tv = {0L, 0L}; + fd_set fds; + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + + if (select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv) <= 0) return; + + char* input = linenoiseEditFeed(&sLinenoiseState); + + if (input == NULL) { + linenoiseEditStop(&sLinenoiseState); + game_exit(); + } else if (input != linenoiseEditMore) { + terminal_stop(); + + if (input[0] != '\0') { + run_command(input); + linenoiseHistoryAdd(input); + } + + free(input); + + terminal_init(); + } + #endif +} + +void terminal_clear() { +#if !defined(_WIN32) && !defined(_WIN64) + if (!sTerminalInitialized) return; + linenoiseClearScreen(); +#endif +} \ No newline at end of file diff --git a/src/pc/terminal.h b/src/pc/terminal.h new file mode 100644 index 000000000..af43aad4e --- /dev/null +++ b/src/pc/terminal.h @@ -0,0 +1,9 @@ +#pragma once +#include "src/pc/thread.h" + +extern struct ThreadHandle gTerminalThread; + +void log_to_terminal(const char* fmt, ...); +void terminal_init(); +void terminal_update(); +void terminal_clear(); \ No newline at end of file diff --git a/src/pc/update_checker.c b/src/pc/update_checker.c index 77d21abb5..4b1cf1d19 100644 --- a/src/pc/update_checker.c +++ b/src/pc/update_checker.c @@ -10,6 +10,7 @@ #include "pc/djui/djui.h" #include "pc/network/version.h" #include "pc/loading.h" +#include "pc/debuglog.h" #define URL "https://raw.githubusercontent.com/coop-deluxe/sm64coopdx/refs/heads/main/src/pc/network/version.h" #define VERSION_IDENTIFIER "#define SM64COOPDX_VERSION \"" @@ -73,7 +74,7 @@ void get_version_remote(void) { // initialize WinINet HINTERNET hInternet = InternetOpenA("sm64coopdx", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0); if (!hInternet) { - printf("Failed to check for updates!\n"); + LOG_ERROR("Failed to check for updates!"); InternetCloseHandle(hInternet); return; } @@ -81,7 +82,7 @@ void get_version_remote(void) { // open the URL HINTERNET hUrl = InternetOpenUrlA(hInternet, URL, NULL, 0, INTERNET_FLAG_RELOAD, 0); if (!hUrl) { - printf("Failed to check for updates!\n"); + LOG_ERROR("Failed to check for updates!"); InternetCloseHandle(hInternet); InternetCloseHandle(hUrl); return; @@ -95,7 +96,7 @@ void get_version_remote(void) { // read data from the URL, making room in the buffer for the null-terminator DWORD bytesRead; if (!InternetReadFile(hUrl, buffer, sizeof(buffer) - 1, &bytesRead)) { - printf("Failed to check for updates!\n"); + LOG_ERROR("Failed to check for updates!"); InternetCloseHandle(hInternet); InternetCloseHandle(hUrl); return; @@ -112,7 +113,7 @@ void get_version_remote(void) { // initialize libcurl CURL *curl = curl_easy_init(); if (!curl || curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) { - printf("Failed to check for updates!\n"); + LOG_ERROR("Failed to check for updates!"); return; } @@ -126,7 +127,7 @@ void get_version_remote(void) { // perform the request CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { - printf("Failed to check for updates!\n"); + LOG_ERROR("Failed to check for updates!"); curl_easy_cleanup(curl); return; }