From c68ee859eae107316d2149e6d3762384ccf27ea9 Mon Sep 17 00:00:00 2001 From: djoslin0 Date: Sun, 22 Jun 2025 02:07:15 -0700 Subject: [PATCH] Add mod development mode (#851) With mod development mode on you can press the L bind while paused to reload the active mods. This reload will rescan the directories for the active modes and thus refresh their file caches. Mod development mode also enables live lua module reloading. Any time a lua module is updated, coop will live reload the functions that changed and do its best to maintain the previous variable states. --------- Co-authored-by: MysterD --- autogen/lua_definitions/structs.lua | 2 + data/dynos.c.h | 1 + data/dynos.cpp.h | 1 + data/dynos_c.cpp | 4 + data/dynos_gfx_init.cpp | 55 +-- docs/lua/structs.md | 2 + lang/Czech.ini | 2 + lang/Dutch.ini | 2 + lang/English.ini | 2 + lang/French.ini | 2 + lang/German.ini | 2 + lang/Italian.ini | 2 + lang/Japanese.ini | 2 + lang/Polish.ini | 2 + lang/Portuguese.ini | 2 + lang/Russian.ini | 2 + lang/Spanish.ini | 2 + src/game/ingame_menu.c | 3 + src/pc/configfile.c | 2 + src/pc/configfile.h | 1 + src/pc/djui/djui.c | 12 + src/pc/djui/djui.h | 1 + src/pc/djui/djui_panel_host_settings.c | 4 + src/pc/djui/djui_panel_menu_options.c | 3 + src/pc/lua/smlua.c | 27 +- src/pc/lua/smlua.h | 2 +- src/pc/lua/smlua_cobject_autogen.c | 16 +- src/pc/lua/smlua_hooks.c | 47 +++ src/pc/lua/smlua_hooks.h | 1 + src/pc/lua/smlua_live_reload.c | 460 +++++++++++++++++++++++++ src/pc/lua/smlua_live_reload.h | 6 + src/pc/lua/smlua_require.c | 4 +- src/pc/lua/smlua_require.h | 2 + src/pc/lua/smlua_utils.c | 18 + src/pc/mods/mod.c | 81 +++++ src/pc/mods/mod.h | 4 + src/pc/network/network.c | 29 ++ src/pc/network/network.h | 2 + 38 files changed, 772 insertions(+), 40 deletions(-) create mode 100644 src/pc/lua/smlua_live_reload.c create mode 100644 src/pc/lua/smlua_live_reload.h diff --git a/autogen/lua_definitions/structs.lua b/autogen/lua_definitions/structs.lua index 24adcccff..be9dc46a9 100644 --- a/autogen/lua_definitions/structs.lua +++ b/autogen/lua_definitions/structs.lua @@ -1277,6 +1277,8 @@ --- @class ModFile --- @field public cachedPath string --- @field public dataHash integer[] +--- @field public isLoadedLuaModule boolean +--- @field public modifiedTimestamp integer --- @field public relativePath string --- @field public wroteBytes integer diff --git a/data/dynos.c.h b/data/dynos.c.h index bbfc0d74c..9fd738463 100644 --- a/data/dynos.c.h +++ b/data/dynos.c.h @@ -32,6 +32,7 @@ const char* dynos_pack_get_name(s32 index); bool dynos_pack_get_enabled(s32 index); void dynos_pack_set_enabled(s32 index, bool value); bool dynos_pack_get_exists(s32 index); +void dynos_generate_mod_pack(char* modPath); void dynos_generate_packs(const char* directory); // -- geos -- // diff --git a/data/dynos.cpp.h b/data/dynos.cpp.h index 98b29f0e3..c79326a22 100644 --- a/data/dynos.cpp.h +++ b/data/dynos.cpp.h @@ -1013,6 +1013,7 @@ void DynOS_Gfx_ModShutdown(); typedef s64 (*RDConstantFunc)(const String& _Arg, bool* found); u32 DynOS_Lua_RememberVariable(GfxData* aGfxData, void* aPtr, const String& token); +void DynOS_Gfx_GenerateModPacks(char* modPath); void DynOS_Gfx_GeneratePacks(const char* directory); s64 DynOS_RecursiveDescent_Parse(const char* expr, bool* success, RDConstantFunc func); void DynOS_Read_Source(GfxData *aGfxData, const SysPath &aFilename); diff --git a/data/dynos_c.cpp b/data/dynos_c.cpp index 5b59d32f8..850f4006d 100644 --- a/data/dynos_c.cpp +++ b/data/dynos_c.cpp @@ -101,6 +101,10 @@ bool dynos_pack_get_exists(s32 index) { return false; } +void dynos_generate_mod_pack(char* modPath) { + DynOS_Gfx_GenerateModPacks(modPath); +} + void dynos_generate_packs(const char* directory) { DynOS_Gfx_GeneratePacks(directory); } diff --git a/data/dynos_gfx_init.cpp b/data/dynos_gfx_init.cpp index 3e4ac2145..d098bfdf4 100644 --- a/data/dynos_gfx_init.cpp +++ b/data/dynos_gfx_init.cpp @@ -3,9 +3,37 @@ extern "C" { #include "pc/loading.h" } +#define MOD_PATH_LEN 1024 + +void DynOS_Gfx_GenerateModPacks(char* modPath) { + // If pack folder exists, generate bins + SysPath _LevelPackFolder = fstring("%s/levels", modPath); + if (fs_sys_dir_exists(_LevelPackFolder.c_str())) { + DynOS_Lvl_GeneratePack(_LevelPackFolder); + } + + SysPath _ActorPackFolder = fstring("%s/actors", modPath); + if (fs_sys_dir_exists(_ActorPackFolder.c_str())) { + DynOS_Actor_GeneratePack(_ActorPackFolder); + } + + SysPath _BehaviorPackFolder = fstring("%s/data", modPath); + if (fs_sys_dir_exists(_BehaviorPackFolder.c_str())) { + DynOS_Bhv_GeneratePack(_BehaviorPackFolder); + } + + SysPath _TexturePackFolder = fstring("%s", modPath); + SysPath _TexturePackOutputFolder = fstring("%s/textures", modPath); + if (fs_sys_dir_exists(_TexturePackFolder.c_str())) { + DynOS_Tex_GeneratePack(_TexturePackFolder, _TexturePackOutputFolder, true); + } +} + void DynOS_Gfx_GeneratePacks(const char* directory) { if (configSkipPackGeneration) { return; } - + + static char sModPath[MOD_PATH_LEN] = ""; + LOADING_SCREEN_MUTEX( loading_screen_reset_progress_bar(); snprintf(gCurrLoadingSegment.str, 256, "Generating DynOS Packs In Path:\n\\#808080\\%s", directory); @@ -25,28 +53,11 @@ void DynOS_Gfx_GeneratePacks(const char* directory) { if (SysPath(dir->d_name) == ".") continue; if (SysPath(dir->d_name) == "..") continue; - // If pack folder exists, generate bins - SysPath _LevelPackFolder = fstring("%s/%s/levels", directory, dir->d_name); - if (fs_sys_dir_exists(_LevelPackFolder.c_str())) { - DynOS_Lvl_GeneratePack(_LevelPackFolder); - } - - SysPath _ActorPackFolder = fstring("%s/%s/actors", directory, dir->d_name); - if (fs_sys_dir_exists(_ActorPackFolder.c_str())) { - DynOS_Actor_GeneratePack(_ActorPackFolder); - } - - SysPath _BehaviorPackFolder = fstring("%s/%s/data", directory, dir->d_name); - if (fs_sys_dir_exists(_BehaviorPackFolder.c_str())) { - DynOS_Bhv_GeneratePack(_BehaviorPackFolder); - } - - SysPath _TexturePackFolder = fstring("%s/%s", directory, dir->d_name); - SysPath _TexturePackOutputFolder = fstring("%s/%s/textures", directory, dir->d_name); - if (fs_sys_dir_exists(_TexturePackFolder.c_str())) { - DynOS_Tex_GeneratePack(_TexturePackFolder, _TexturePackOutputFolder, true); - } + // build mod path + snprintf(sModPath, MOD_PATH_LEN, "%s/%s", directory, dir->d_name); + // generate packs + DynOS_Gfx_GenerateModPacks(sModPath); LOADING_SCREEN_MUTEX(gCurrLoadingSegment.percentage = (f32) i / (f32) pathCount); } diff --git a/docs/lua/structs.md b/docs/lua/structs.md index 6d3714eab..65bf417a1 100644 --- a/docs/lua/structs.md +++ b/docs/lua/structs.md @@ -1940,6 +1940,8 @@ | ----- | ---- | ------ | | cachedPath | `string` | read-only | | dataHash | `Array` <`integer`> | read-only | +| isLoadedLuaModule | `boolean` | read-only | +| modifiedTimestamp | `integer` | read-only | | relativePath | `string` | read-only | | wroteBytes | `integer` | read-only | diff --git a/lang/Czech.ini b/lang/Czech.ini index a5be025a5..04921974d 100644 --- a/lang/Czech.ini +++ b/lang/Czech.ini @@ -240,6 +240,7 @@ SKIP_INTRO_CUTSCENE = "Přeskočit intro" ENABLE_CHEATS = "Zapnout cheaty" BUBBLE_ON_DEATH = "Bublina při smrti" NAMETAGS = "Nametags" +MOD_DEV_MODE = "Režim vývoje modů" BOUNCY_BOUNDS_ON_CAP = "Zapnuto (Omezeno)" BOUNCY_BOUNDS_ON = "Zapnuto" BOUNCY_BOUNDS_OFF = "Vypnuto" @@ -321,6 +322,7 @@ DEBUG = "Debug" LANGUAGE = "Jazyk" COOP_COMPATIBILITY = "Povolit kompatibilitu sm64ex-coop" R_BUTTON = "Tlačítko R - Možnosti" +L_BUTTON = "Tlačítko L - Znovu načíst aktivní mody" [INFORMATION] INFORMATION_TITLE = "INFORMACE" diff --git a/lang/Dutch.ini b/lang/Dutch.ini index 04828b1a6..25d3c6a0b 100644 --- a/lang/Dutch.ini +++ b/lang/Dutch.ini @@ -240,6 +240,7 @@ SKIP_INTRO_CUTSCENE = "Introductie film overslaan" ENABLE_CHEATS = "Cheats aan zetten" BUBBLE_ON_DEATH = "Bubbelen op dood" NAMETAGS = "Nametags" +MOD_DEV_MODE = "Modontwikkelingsmodus" BOUNCY_BOUNDS_ON_CAP = "Aan (Begrensd)" BOUNCY_BOUNDS_ON = "Aan" BOUNCY_BOUNDS_OFF = "Uit" @@ -321,6 +322,7 @@ DEBUG = "Debug" LANGUAGE = "Taal" COOP_COMPATIBILITY = "Schakel sm64ex-coop compatibiliteit in" R_BUTTON = "R-knop - Opties" +L_BUTTON = "L-knop - Actieve mods opnieuw laden" [INFORMATION] INFORMATION_TITLE = "INFORMATIE" diff --git a/lang/English.ini b/lang/English.ini index a301f02de..cc18a5cbb 100644 --- a/lang/English.ini +++ b/lang/English.ini @@ -240,6 +240,7 @@ SKIP_INTRO_CUTSCENE = "Skip Intro Cutscene" ENABLE_CHEATS = "Enable Cheats" BUBBLE_ON_DEATH = "Bubble On Death" NAMETAGS = "Nametags" +MOD_DEV_MODE = "Mod Development Mode" BOUNCY_BOUNDS_ON_CAP = "On (Capped)" BOUNCY_BOUNDS_ON = "On" BOUNCY_BOUNDS_OFF = "Off" @@ -321,6 +322,7 @@ DEBUG = "Debug" LANGUAGE = "Language" COOP_COMPATIBILITY = "Enable sm64ex-coop Compatibility" R_BUTTON = "R Button - Options" +L_BUTTON = "L Button - Reload Active Mods" [INFORMATION] INFORMATION_TITLE = "INFO" diff --git a/lang/French.ini b/lang/French.ini index 5e03453a0..29b567025 100644 --- a/lang/French.ini +++ b/lang/French.ini @@ -240,6 +240,7 @@ SKIP_INTRO_CUTSCENE = "Passer la cinématique d'intro" ENABLE_CHEATS = "Activer le mode triche" BUBBLE_ON_DEATH = "Bulles (mort)" NAMETAGS = "Afficher Pseudos" +MOD_DEV_MODE = "Mode de développement de mods" BOUNCY_BOUNDS_ON_CAP = "Activé (Limité)" BOUNCY_BOUNDS_ON = "Activé" BOUNCY_BOUNDS_OFF = "Désactivé" @@ -321,6 +322,7 @@ DEBUG = "Débogage" LANGUAGE = "Langue" COOP_COMPATIBILITY = "Activer la compatibilité sm64ex-coop" R_BUTTON = "Bouton R - Options" +L_BUTTON = "Bouton L - Recharger les mods actifs" [INFORMATION] INFORMATION_TITLE = "INFORMATION" diff --git a/lang/German.ini b/lang/German.ini index 97f2f3251..f5aa499fa 100644 --- a/lang/German.ini +++ b/lang/German.ini @@ -240,6 +240,7 @@ SKIP_INTRO_CUTSCENE = "Intro überspringen" ENABLE_CHEATS = "Cheats aktivieren" BUBBLE_ON_DEATH = "Base beim Tod" NAMETAGS = "Nametags" +MOD_DEV_MODE = "Mod-Entwicklungsmodus" BOUNCY_BOUNDS_ON_CAP = "An (Gedrosselt)" BOUNCY_BOUNDS_ON = "An" BOUNCY_BOUNDS_OFF = "Aus" @@ -321,6 +322,7 @@ DEBUG = "Debug" LANGUAGE = "Sprache" COOP_COMPATIBILITY = "Aktiviere sm64ex-coop Kompatibilität" R_BUTTON = "R-Taste - Optionen" +L_BUTTON = "L-Taste - Aktive Mods neu laden" [INFORMATION] INFORMATION_TITLE = "INFORMATION" diff --git a/lang/Italian.ini b/lang/Italian.ini index f28c7f732..08084d7e6 100644 --- a/lang/Italian.ini +++ b/lang/Italian.ini @@ -238,6 +238,7 @@ SKIP_INTRO_CUTSCENE = "Salta la intro iniziale" ENABLE_CHEATS = "Abilita i trucchi" BUBBLE_ON_DEATH = "Bolla alla morte" NAMETAGS = "Nametags" +MOD_DEV_MODE = "Modalità sviluppo mod" BOUNCY_BOUNDS_ON_CAP = "Acceso (Limitato)" BOUNCY_BOUNDS_ON = "Acceso" BOUNCY_BOUNDS_OFF = "Spento" @@ -319,6 +320,7 @@ DEBUG = "Debug" LANGUAGE = "Lingua" COOP_COMPATIBILITY = "Abilita la compatibilità sm64ex-coop" R_BUTTON = "Pulsante R - Opzioni" +L_BUTTON = "Pulsante L - Ricarica mod attivi" [INFORMATION] INFORMATION_TITLE = "INFORMAZIONE" diff --git a/lang/Japanese.ini b/lang/Japanese.ini index e51d98e4d..1d192fded 100644 --- a/lang/Japanese.ini +++ b/lang/Japanese.ini @@ -241,6 +241,7 @@ SKIP_INTRO_CUTSCENE = "イントロをスキップ" ENABLE_CHEATS = "チートを有効にする" BUBBLE_ON_DEATH = "やられた時にシャボンで復活" NAMETAGS = "ネームタグを有効にする" +MOD_DEV_MODE = "MOD開発モード" BOUNCY_BOUNDS_ON_CAP = "オン(制限付き)" BOUNCY_BOUNDS_ON = "オン" BOUNCY_BOUNDS_OFF = "オフ" @@ -322,6 +323,7 @@ DEBUG = "デバッグ" LANGUAGE = "言語" COOP_COMPATIBILITY = "sm64ex-coopとの互換性を有効にする" R_BUTTON = "Rボタン - 設定" +L_BUTTON = "Lボタン - アクティブなMODを再読み込み" [INFORMATION] INFORMATION_TITLE = "INFO" diff --git a/lang/Polish.ini b/lang/Polish.ini index f38821a7d..be3d99f38 100644 --- a/lang/Polish.ini +++ b/lang/Polish.ini @@ -240,6 +240,7 @@ SKIP_INTRO_CUTSCENE = "Pomiń Przerywnik Intro" ENABLE_CHEATS = "Włącz Kody" BUBBLE_ON_DEATH = "Bańka po Śmierci" NAMETAGS = "Identyfikatory" +MOD_DEV_MODE = "Tryb deweloperski modów" BOUNCY_BOUNDS_ON_CAP = "Wł. (Ograniczone)" BOUNCY_BOUNDS_ON = "Włączone" BOUNCY_BOUNDS_OFF = "Wyłączone" @@ -321,6 +322,7 @@ DEBUG = "Debugowanie" LANGUAGE = "Język" COOP_COMPATIBILITY = "Włącz kompatybilność z sm64ex-coop" R_BUTTON = "Przycisk R - Opcje" +L_BUTTON = "Przycisk L - Przeładuj aktywne mody" [INFORMATION] INFORMATION_TITLE = "INFORMACJA" diff --git a/lang/Portuguese.ini b/lang/Portuguese.ini index 0d3f3ee06..1be23e351 100644 --- a/lang/Portuguese.ini +++ b/lang/Portuguese.ini @@ -240,6 +240,7 @@ SKIP_INTRO_CUTSCENE = "Pular cena de introdução" ENABLE_CHEATS = "Ativar trapaças" BUBBLE_ON_DEATH = "Bolha após a morte" NAMETAGS = "Etiquetas" +MOD_DEV_MODE = "Modo de desenvolvimento de mods" BOUNCY_BOUNDS_ON_CAP = "Ativado (Limitado)" BOUNCY_BOUNDS_ON = "Ativado" BOUNCY_BOUNDS_OFF = "Desativado" @@ -321,6 +322,7 @@ DEBUG = "Debug" LANGUAGE = "Idioma" COOP_COMPATIBILITY = "Ativar a compatibilidade com sm64ex-coop" R_BUTTON = "Botão R - Opções" +L_BUTTON = "Botão L - Recarregar mods ativos" [INFORMATION] INFORMATION_TITLE = "INFORMAÇÃO" diff --git a/lang/Russian.ini b/lang/Russian.ini index 47a392687..9b9232980 100644 --- a/lang/Russian.ini +++ b/lang/Russian.ini @@ -239,6 +239,7 @@ SKIP_INTRO_CUTSCENE = "Пропустить вступительный роли ENABLE_CHEATS = "Включить читы" BUBBLE_ON_DEATH = "Пузырик при смерти" NAMETAGS = "Этикетки" +MOD_DEV_MODE = "Режим разработки модов" BOUNCY_BOUNDS_ON_CAP = "Вкл. (Ограничено)" BOUNCY_BOUNDS_ON = "Вкл" BOUNCY_BOUNDS_OFF = "Выкл" @@ -320,6 +321,7 @@ DEBUG = "Отладка" LANGUAGE = "Язык" COOP_COMPATIBILITY = "Включить совместимость sm64ex-coop" R_BUTTON = "Кнопка R - Опции" +L_BUTTON = "Кнопка L - Перезагрузить активные моды" [INFORMATION] INFORMATION_TITLE = "INFORMATION" diff --git a/lang/Spanish.ini b/lang/Spanish.ini index 4d92bfa5a..919a7b087 100644 --- a/lang/Spanish.ini +++ b/lang/Spanish.ini @@ -240,6 +240,7 @@ SKIP_INTRO_CUTSCENE = "Saltar cinemática de introducción" ENABLE_CHEATS = "Habilitar trucos" BUBBLE_ON_DEATH = "Burbuja al morir" NAMETAGS = "Etiquetas de nombre" +MOD_DEV_MODE = "Modo de desarrollo de mods" BOUNCY_BOUNDS_ON_CAP = "Encendido (Limitado)" BOUNCY_BOUNDS_ON = "Encendido" BOUNCY_BOUNDS_OFF = "Apagado" @@ -321,6 +322,7 @@ DEBUG = "Depuración" LANGUAGE = "Idioma" COOP_COMPATIBILITY = "Habilitar la compatibilidad con sm64ex-coop" R_BUTTON = "Botón R - Opciones" +L_BUTTON = "Botón L - Recargar mods activos" [INFORMATION] INFORMATION_TITLE = "INFORMACIÓN" diff --git a/src/game/ingame_menu.c b/src/game/ingame_menu.c index 709e3d253..6aed98165 100644 --- a/src/game/ingame_menu.c +++ b/src/game/ingame_menu.c @@ -3168,6 +3168,9 @@ s16 render_pause_courses_and_castle(void) { if (gPlayer1Controller->buttonPressed & R_TRIG) { djui_panel_pause_create(NULL); } + if ((gPlayer1Controller->buttonPressed & L_TRIG) && network_allow_mod_dev_mode()) { + network_mod_dev_mode_reload(); + } return 0; } diff --git a/src/pc/configfile.c b/src/pc/configfile.c index 3477ab40c..a557f8c57 100644 --- a/src/pc/configfile.c +++ b/src/pc/configfile.c @@ -177,6 +177,7 @@ unsigned int configPlayerInteraction = 1; unsigned int configPlayerKnockbackStrength = 25; unsigned int configStayInLevelAfterStar = 0; bool configNametags = true; +bool configModDevMode = false; unsigned int configBouncyLevelBounds = 0; bool configSkipIntro = 0; bool configPauseAnywhere = false; @@ -334,6 +335,7 @@ static const struct ConfigOption options[] = { {.name = "coop_player_knockback_strength", .type = CONFIG_TYPE_UINT, .uintValue = &configPlayerKnockbackStrength}, {.name = "coop_stay_in_level_after_star", .type = CONFIG_TYPE_UINT, .uintValue = &configStayInLevelAfterStar}, {.name = "coop_nametags", .type = CONFIG_TYPE_BOOL, .boolValue = &configNametags}, + {.name = "coop_mod_dev_mode", .type = CONFIG_TYPE_BOOL, .boolValue = &configModDevMode}, {.name = "coop_bouncy_bounds", .type = CONFIG_TYPE_UINT, .uintValue = &configBouncyLevelBounds}, {.name = "skip_intro", .type = CONFIG_TYPE_BOOL, .boolValue = &configSkipIntro}, {.name = "pause_anywhere", .type = CONFIG_TYPE_BOOL, .boolValue = &configPauseAnywhere}, diff --git a/src/pc/configfile.h b/src/pc/configfile.h index f7503a050..1614da193 100644 --- a/src/pc/configfile.h +++ b/src/pc/configfile.h @@ -135,6 +135,7 @@ extern unsigned int configPlayerInteraction; extern unsigned int configPlayerKnockbackStrength; extern unsigned int configStayInLevelAfterStar; extern bool configNametags; +extern bool configModDevMode; extern unsigned int configBouncyLevelBounds; extern bool configSkipIntro; extern bool configPauseAnywhere; diff --git a/src/pc/djui/djui.c b/src/pc/djui/djui.c index bee49721c..f4a22f760 100644 --- a/src/pc/djui/djui.c +++ b/src/pc/djui/djui.c @@ -21,6 +21,7 @@ static Gfx* sSavedDisplayListHead = NULL; struct DjuiRoot* gDjuiRoot = NULL; struct DjuiText* gDjuiPauseOptions = NULL; +struct DjuiText* gDjuiModReload = NULL; static struct DjuiText* sDjuiLuaError = NULL; static u32 sDjuiLuaErrorTimeout = 0; bool gDjuiInMainMenu = true; @@ -39,8 +40,10 @@ void djui_shutdown(void) { sSavedDisplayListHead = NULL; if (gDjuiPauseOptions) djui_base_destroy(&gDjuiPauseOptions->base); + if (gDjuiModReload) djui_base_destroy(&gDjuiModReload->base); if (sDjuiLuaError) djui_base_destroy(&sDjuiLuaError->base); gDjuiPauseOptions = NULL; + gDjuiModReload = NULL; sDjuiLuaError = NULL; sDjuiLuaErrorTimeout = 0; @@ -89,6 +92,15 @@ void djui_init(void) { djui_base_set_location(&gDjuiPauseOptions->base, 0, 16); djui_text_set_alignment(gDjuiPauseOptions, DJUI_HALIGN_CENTER, DJUI_VALIGN_CENTER); + gDjuiModReload = djui_text_create(&sDjuiRootBehind->base, DLANG(MISC, L_BUTTON)); + djui_text_set_drop_shadow(gDjuiModReload, 0, 0, 0, 255); + djui_base_set_color(&gDjuiModReload->base, 255, 32, 32, 255); + djui_base_set_size_type(&gDjuiModReload->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE); + djui_base_set_size(&gDjuiModReload->base, 1.0f, 32); + djui_base_set_location(&gDjuiModReload->base, 0, 64); + djui_text_set_alignment(gDjuiModReload, DJUI_HALIGN_CENTER, DJUI_VALIGN_CENTER); + djui_base_set_visible(&gDjuiModReload->base, false); + sDjuiLuaError = djui_text_create(&gDjuiRoot->base, ""); djui_base_set_size_type(&sDjuiLuaError->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE); djui_base_set_size(&sDjuiLuaError->base, 1.0f, 32); diff --git a/src/pc/djui/djui.h b/src/pc/djui/djui.h index 1263a5518..da793bb23 100644 --- a/src/pc/djui/djui.h +++ b/src/pc/djui/djui.h @@ -38,6 +38,7 @@ extern struct DjuiRoot* gDjuiRoot; extern struct DjuiText* gDjuiPauseOptions; +extern struct DjuiText* gDjuiModReload; extern bool gDjuiInMainMenu; extern bool gDjuiInPlayerMenu; extern bool gDjuiDisabled; diff --git a/src/pc/djui/djui_panel_host_settings.c b/src/pc/djui/djui_panel_host_settings.c index 068a3b319..4cb003790 100644 --- a/src/pc/djui/djui_panel_host_settings.c +++ b/src/pc/djui/djui_panel_host_settings.c @@ -10,6 +10,7 @@ static unsigned int sKnockbackIndex = 0; struct DjuiInputbox* sPlayerAmount = NULL; +static bool sFalse = false; static void djui_panel_host_settings_knockback_change(UNUSED struct DjuiBase* caller) { switch (sKnockbackIndex) { @@ -69,6 +70,9 @@ void djui_panel_host_settings_create(struct DjuiBase* caller) { djui_checkbox_create(body, DLANG(HOST_SETTINGS, BUBBLE_ON_DEATH), &configBubbleDeath, NULL); djui_checkbox_create(body, DLANG(HOST_SETTINGS, NAMETAGS), &configNametags, NULL); + struct DjuiCheckbox* chkDevMode = djui_checkbox_create(body, DLANG(HOST_SETTINGS, MOD_DEV_MODE), (configNetworkSystem == NS_SOCKET) ? &configModDevMode : &sFalse, NULL); + djui_base_set_enabled(&chkDevMode->base, configNetworkSystem == NS_SOCKET); + struct DjuiRect* rect1 = djui_rect_container_create(body, 32); { struct DjuiText* text1 = djui_text_create(&rect1->base, DLANG(HOST_SETTINGS, AMOUNT_OF_PLAYERS)); diff --git a/src/pc/djui/djui_panel_menu_options.c b/src/pc/djui/djui_panel_menu_options.c index ed5518538..994b4e58f 100644 --- a/src/pc/djui/djui_panel_menu_options.c +++ b/src/pc/djui/djui_panel_menu_options.c @@ -109,6 +109,9 @@ static void djui_panel_menu_options_djui_setting_change(UNUSED struct DjuiBase* djui_text_set_font(gDjuiPauseOptions, gDjuiFonts[configDjuiThemeFont == 0 ? FONT_NORMAL : FONT_ALIASED]); djui_text_set_text(gDjuiPauseOptions, DLANG(MISC, R_BUTTON)); + + djui_text_set_font(gDjuiModReload, gDjuiFonts[configDjuiThemeFont == 0 ? FONT_NORMAL : FONT_ALIASED]); + djui_text_set_text(gDjuiModReload, DLANG(MISC, L_BUTTON)); } gDjuiChangingTheme = false; diff --git a/src/pc/lua/smlua.c b/src/pc/lua/smlua.c index 49dc4c7aa..b93b5679a 100644 --- a/src/pc/lua/smlua.c +++ b/src/pc/lua/smlua.c @@ -1,5 +1,6 @@ #include "smlua.h" #include "pc/lua/smlua_require.h" +#include "pc/lua/smlua_live_reload.h" #include "game/hardcoded.h" #include "pc/mods/mods.h" #include "pc/mods/mods_utils.h" @@ -192,8 +193,10 @@ static bool smlua_check_binary_header(struct ModFile *file) { return false; } -void smlua_load_script(struct Mod* mod, struct ModFile* file, u16 remoteIndex, bool isModInit) { - if (!smlua_check_binary_header(file)) return; +int smlua_load_script(struct Mod* mod, struct ModFile* file, u16 remoteIndex, bool isModInit) { + int rc = LUA_OK; + if (!smlua_check_binary_header(file)) { return LUA_ERRMEM; } + lua_State* L = gLuaState; s32 prevTop = lua_gettop(L); @@ -207,7 +210,7 @@ void smlua_load_script(struct Mod* mod, struct ModFile* file, u16 remoteIndex, b LOG_LUA("Failed to load lua script '%s': File not found.", file->cachedPath); gLuaInitializingScript = 0; lua_settop(L, prevTop); - return; + return LUA_ERRFILE; } f_seek(f, 0, SEEK_END); @@ -217,7 +220,7 @@ void smlua_load_script(struct Mod* mod, struct ModFile* file, u16 remoteIndex, b LOG_LUA("Failed to load lua script '%s': Cannot allocate buffer.", file->cachedPath); gLuaInitializingScript = 0; lua_settop(L, prevTop); - return; + return LUA_ERRMEM; } f_rewind(f); @@ -225,18 +228,19 @@ void smlua_load_script(struct Mod* mod, struct ModFile* file, u16 remoteIndex, b LOG_LUA("Failed to load lua script '%s': Unexpected early end of file.", file->cachedPath); gLuaInitializingScript = 0; lua_settop(L, prevTop); - return; + return LUA_ERRFILE; } f_close(f); f_delete(f); - if (luaL_loadbuffer(L, buffer, length, file->cachedPath) != LUA_OK) { // only run on success + rc = luaL_loadbuffer(L, buffer, length, file->cachedPath); + if (rc != LUA_OK) { // only run on success LOG_LUA("Failed to load lua script '%s'.", file->cachedPath); LOG_LUA("%s", smlua_to_string(L, lua_gettop(L))); gLuaInitializingScript = 0; free(buffer); lua_settop(L, prevTop); - return; + return rc; } free(buffer); @@ -284,18 +288,21 @@ void smlua_load_script(struct Mod* mod, struct ModFile* file, u16 remoteIndex, b lua_pop(L, 1); LOG_LUA("mod environment not found"); lua_settop(L, prevTop); - return; + return LUA_ERRRUN; } lua_setupvalue(L, -2, 1); // set _ENV } // run chunks LOG_INFO("Executing '%s'", file->relativePath); - if (smlua_pcall(L, 0, 1, 0) != LUA_OK) { + rc = smlua_pcall(L, 0, 1, 0); + if (rc != LUA_OK) { LOG_LUA("Failed to execute lua script '%s'.", file->cachedPath); } gLuaInitializingScript = 0; + + return rc; } void smlua_init(void) { @@ -370,6 +377,8 @@ void smlua_update(void) { lua_State* L = gLuaState; if (L == NULL) { return; } + if (network_allow_mod_dev_mode()) { smlua_live_reload_update(L); } + audio_sample_destroy_pending_copies(); smlua_call_event_hooks(HOOK_UPDATE); diff --git a/src/pc/lua/smlua.h b/src/pc/lua/smlua.h index be424df40..ddaf22182 100644 --- a/src/pc/lua/smlua.h +++ b/src/pc/lua/smlua.h @@ -47,7 +47,7 @@ int smlua_error_handler(UNUSED lua_State* L); int smlua_pcall(lua_State* L, int nargs, int nresults, int errfunc); void smlua_exec_file(const char* path); void smlua_exec_str(const char* str); -void smlua_load_script(struct Mod* mod, struct ModFile* file, u16 remoteIndex, bool isModInit); +int smlua_load_script(struct Mod* mod, struct ModFile* file, u16 remoteIndex, bool isModInit); void smlua_init(void); void smlua_update(void); diff --git a/src/pc/lua/smlua_cobject_autogen.c b/src/pc/lua/smlua_cobject_autogen.c index 9c0ed8f08..8a8d49574 100644 --- a/src/pc/lua/smlua_cobject_autogen.c +++ b/src/pc/lua/smlua_cobject_autogen.c @@ -1598,14 +1598,16 @@ static struct LuaObjectField sModAudioSampleCopiesFields[LUA_MOD_AUDIO_SAMPLE_CO // { "sound", LVT_???, offsetof(struct ModAudioSampleCopies, sound), false, LOT_???, 1, sizeof(ma_sound) }, <--- UNIMPLEMENTED }; -#define LUA_MOD_FILE_FIELD_COUNT 4 +#define LUA_MOD_FILE_FIELD_COUNT 6 static struct LuaObjectField sModFileFields[LUA_MOD_FILE_FIELD_COUNT] = { - { "cachedPath", LVT_STRING_P, offsetof(struct ModFile, cachedPath), true, LOT_NONE, 1, sizeof(char*) }, - { "dataHash", LVT_U8, offsetof(struct ModFile, dataHash), true, LOT_NONE, 16, sizeof(u8) }, -// { "fp", LVT_???, offsetof(struct ModFile, fp), true, LOT_???, 1, sizeof(FILE*) }, <--- UNIMPLEMENTED - { "relativePath", LVT_STRING, offsetof(struct ModFile, relativePath), true, LOT_NONE, 1, sizeof(char) }, -// { "size", LVT_???, offsetof(struct ModFile, size), true, LOT_???, 1, sizeof(size_t) }, <--- UNIMPLEMENTED - { "wroteBytes", LVT_U64, offsetof(struct ModFile, wroteBytes), true, LOT_NONE, 1, sizeof(u64) }, + { "cachedPath", LVT_STRING_P, offsetof(struct ModFile, cachedPath), true, LOT_NONE, 1, sizeof(char*) }, + { "dataHash", LVT_U8, offsetof(struct ModFile, dataHash), true, LOT_NONE, 16, sizeof(u8) }, +// { "fp", LVT_???, offsetof(struct ModFile, fp), true, LOT_???, 1, sizeof(FILE*) }, <--- UNIMPLEMENTED + { "isLoadedLuaModule", LVT_BOOL, offsetof(struct ModFile, isLoadedLuaModule), true, LOT_NONE, 1, sizeof(bool) }, + { "modifiedTimestamp", LVT_U64, offsetof(struct ModFile, modifiedTimestamp), true, LOT_NONE, 1, sizeof(u64) }, + { "relativePath", LVT_STRING, offsetof(struct ModFile, relativePath), true, LOT_NONE, 1, sizeof(char) }, +// { "size", LVT_???, offsetof(struct ModFile, size), true, LOT_???, 1, sizeof(size_t) }, <--- UNIMPLEMENTED + { "wroteBytes", LVT_U64, offsetof(struct ModFile, wroteBytes), true, LOT_NONE, 1, sizeof(u64) }, }; #define LUA_MODE_TRANSITION_INFO_FIELD_COUNT 6 diff --git a/src/pc/lua/smlua_hooks.c b/src/pc/lua/smlua_hooks.c index 56c2115d1..f08e156d3 100644 --- a/src/pc/lua/smlua_hooks.c +++ b/src/pc/lua/smlua_hooks.c @@ -1456,6 +1456,53 @@ void smlua_call_mod_menu_element_hook(struct LuaHookedModMenuElement* hooked, in // misc // ////////// +static void smlua_hook_replace_function_reference(lua_State* L, int* hookedReference, int oldReference, int newReference) { + lua_rawgeti(L, LUA_REGISTRYINDEX, *hookedReference); // stack: ..., hookedFunc + int hookedIdx = lua_gettop(L); + + lua_rawgeti(L, LUA_REGISTRYINDEX, oldReference); // stack: ..., hookedFunc, oldFunc + int oldIdx = lua_gettop(L); + + if (lua_rawequal(L, hookedIdx, oldIdx)) { + luaL_unref(L, LUA_REGISTRYINDEX, *hookedReference); + *hookedReference = newReference; + } + + lua_pop(L, 2); +} + +void smlua_hook_replace_function_references(lua_State* L, int oldReference, int newReference) { + for (int i = 0; i < HOOK_MAX; i++) { + struct LuaHookedEvent* hooked = &sHookedEvents[i]; + for (int j = 0; j < hooked->count; j++) { + smlua_hook_replace_function_reference(L, &hooked->reference[j], oldReference, newReference); + } + } + + for (int i = 0; i < sHookedMarioActionsCount; i++) { + struct LuaHookedMarioAction* hooked = &sHookedMarioActions[i]; + for (int j = 0; j < ACTION_HOOK_MAX; j++) { + smlua_hook_replace_function_reference(L, &hooked->actionHookRefs[j], oldReference, newReference); + } + } + + for (int i = 0; i < sHookedChatCommandsCount; i++) { + struct LuaHookedChatCommand* hooked = &sHookedChatCommands[i]; + smlua_hook_replace_function_reference(L, &hooked->reference, oldReference, newReference); + } + + for (int i = 0; i < gHookedModMenuElementsCount; i++) { + struct LuaHookedModMenuElement* hooked = &gHookedModMenuElements[i]; + smlua_hook_replace_function_reference(L, &hooked->reference, oldReference, newReference); + } + + for (int i = 0; i < sHookedBehaviorsCount; i++) { + struct LuaHookedBehavior* hooked = &sHookedBehaviors[i]; + smlua_hook_replace_function_reference(L, &hooked->initReference, oldReference, newReference); + smlua_hook_replace_function_reference(L, &hooked->loopReference, oldReference, newReference); + } +} + void smlua_clear_hooks(void) { for (int i = 0; i < HOOK_MAX; i++) { struct LuaHookedEvent* hooked = &sHookedEvents[i]; diff --git a/src/pc/lua/smlua_hooks.h b/src/pc/lua/smlua_hooks.h index 280bd129d..3862d3d65 100644 --- a/src/pc/lua/smlua_hooks.h +++ b/src/pc/lua/smlua_hooks.h @@ -155,6 +155,7 @@ bool smlua_subcommand_exists(const char* maincommand, const char* subcommand); void smlua_call_mod_menu_element_hook(struct LuaHookedModMenuElement* hooked, int index); +void smlua_hook_replace_function_references(lua_State* L, int oldReference, int newReference); void smlua_clear_hooks(void); void smlua_bind_hooks(void); diff --git a/src/pc/lua/smlua_live_reload.c b/src/pc/lua/smlua_live_reload.c new file mode 100644 index 000000000..d8add9c15 --- /dev/null +++ b/src/pc/lua/smlua_live_reload.c @@ -0,0 +1,460 @@ +#include +#include "smlua.h" +#include "pc/mods/mods.h" +#include "pc/mods/mods_utils.h" + +#define LIVE_RELOAD_TICK_COUNT 15 + +typedef struct UpvalRecord { + char *funcKeyStr; // the table key under which the function lived + int funcKeyRef; // registry ref for the key + char *name; // the upvalue's name + int ref; // registry reference to the upvalue's value + const void *id; // the opaque upvalue‐cell pointer + int type; // the Lua type code of that value (LUA_T*) + struct UpvalRecord *next; +} UpvalRecord; + +typedef struct UpvalReference { + char *name; + int reference; + bool active; + bool isOld; + struct UpvalReference *next; +} UpvalReference; + +static UpvalReference *sUpvalReferences = NULL; + +static void upval_references_free(lua_State *L) { + UpvalReference *ref = sUpvalReferences; + while (ref) { + UpvalReference *next = ref->next; + + if (ref->name) { + free(ref->name); + } + + if (!ref->active) { + luaL_unref(L, LUA_REGISTRYINDEX, ref->reference); + } + + free(ref); + ref = next; + } + sUpvalReferences = NULL; +} + +static int upval_references_deduplicate(lua_State *L, char* name, int reference, bool isOld) { + // push the candidate function onto the stack + lua_rawgeti(L, LUA_REGISTRYINDEX, reference); // stack: ..., newFunc + int newIdx = lua_gettop(L); + + // iterate existing refs, compare each stored function + for (UpvalReference *ref = sUpvalReferences; ref; ref = ref->next) { + lua_rawgeti(L, LUA_REGISTRYINDEX, ref->reference); // stack: ..., newFunc, existFunc + int existIdx = lua_gettop(L); + + // raw-equal tests pointer identity for tables/functions + if (lua_rawequal(L, newIdx, existIdx)) { + lua_pop(L, 2); // pop existFunc and newFunc + luaL_unref(L, LUA_REGISTRYINDEX, reference); + return ref->reference; + } + + lua_pop(L, 1); // pop only existFunc, leave newFunc for next iteration + } + + lua_pop(L, 1); // pop newFunc + + // allocate + UpvalReference *ref = malloc(sizeof(struct UpvalReference)); + ref->name = name ? strdup(name) : NULL; + ref->reference = reference; + ref->active = false; + ref->isOld = isOld; + ref->next = sUpvalReferences; + sUpvalReferences = ref; + + return reference; +} + +static void upval_references_mark_active(int reference) { + for (UpvalReference *ref = sUpvalReferences; ref; ref = ref->next) { + if (ref->reference == reference) { + ref->active = true; + } + } +} + +static void upvalues_free(lua_State *L, UpvalRecord *upvalsHead) { + UpvalRecord *cur = upvalsHead; + while (cur) { + UpvalRecord *next = cur->next; + + // free the string + if (cur->funcKeyStr) { + free(cur->funcKeyStr); + } + + // unref the upvalue's value + luaL_unref(L, LUA_REGISTRYINDEX, cur->ref); + + // free the upvalue name + free(cur->name); + + // finally free the record itself + free(cur); + + cur = next; + } +} + +static void upvalues_collect_from_function(lua_State *L, UpvalRecord **upvalsHead, bool isOld) { + LUA_STACK_CHECK_BEGIN(L); + + // stack: ..., key, val + if (lua_type(L, -1) != LUA_TFUNCTION) { + return; + } + + // read key string + static char sFuncKeyStr[128] = ""; + const char *kstr = lua_tostring(L, -2); + if (kstr) { + snprintf(sFuncKeyStr, 128, "%s", kstr); + } + // read key ref + lua_pushvalue(L, -1); // duplicate the function closure (val) + // stack: ..., key, val, val_copy + + int refRegistered = luaL_ref(L, LUA_REGISTRYINDEX); + // luaL_ref pops the copy, leaving the original 'val' in place + // stack: ..., key, val + + int funcKeyRef = upval_references_deduplicate(L, sFuncKeyStr, refRegistered, isOld); + + int fnIdx = lua_gettop(L); // the stack index of the reference to the function we're processing + + // walk its upvalues + for (int uv = 1;; uv++) { + const char *uvName = lua_getupvalue(L, fnIdx, uv); + if (!uvName) { break; } + + // get the upvalue‐cell identifier + const void *upvalId = lua_upvalueid(L, fnIdx, uv); + + // get its type + int uvType = lua_type(L, -1); + + // now on top of the stack is the upvalue's 'value' + // we pin it in the registry + lua_pushvalue(L, -1); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); + // pop the original + lua_pop(L, 1); + + // allocate a new record + UpvalRecord *rec = malloc(sizeof(struct UpvalRecord)); + rec->funcKeyStr = kstr ? strdup(sFuncKeyStr) : NULL; + rec->funcKeyRef = funcKeyRef; + rec->name = strdup(uvName); + rec->ref = ref; + rec->id = upvalId; + rec->type = uvType; + rec->next = *upvalsHead; + *upvalsHead = rec; + } + + LUA_STACK_CHECK_END(L); +} + +static void upvalues_collect(lua_State *L, UpvalRecord **upvalsHead, int moduleIdx, bool isOld) { + LUA_STACK_CHECK_BEGIN(L); + + // make moduleIdx absolute so pushes don't shift it + int absMod = lua_absindex(L, moduleIdx); + + // iterate module metatable + if (lua_getmetatable(L, absMod)) { + lua_pushnil(L); // first key + while (lua_next(L, -2) != 0) { + upvalues_collect_from_function(L, upvalsHead, isOld); + lua_pop(L, 1); // pop val, keep key + } + lua_pop(L, 1); // pop metatable + } + + // iterate module table + lua_pushnil(L); + while (lua_next(L, absMod) != 0) { + upvalues_collect_from_function(L, upvalsHead, isOld); + lua_pop(L, 1); // pop val, keep key + } + + LUA_STACK_CHECK_END(L); +} + +const UpvalRecord *upvalues_find(UpvalRecord *searchList, UpvalRecord *searchFor) { + const UpvalRecord *best = NULL; + int best_score = 0; + + for (const UpvalRecord *cur = searchList; cur; cur = cur->next) { + // check if upval names match + if (strcmp(cur->name, searchFor->name) != 0) { continue; } + + // if function names match too, we found it + if (cur->funcKeyStr && searchFor->funcKeyStr && strcmp(cur->funcKeyStr, searchFor->funcKeyStr) == 0) { + return (UpvalRecord *)cur; + } + + // if types match, that's a pretty good indicator + int score = (cur->type == searchFor->type) ? 2 : 1; + if (score > best_score) { + best = cur; + best_score = score; + } + } + + return best; +} + +static void upval_record_push_key(lua_State *L, const UpvalRecord *rec) { + if (rec->funcKeyStr) { + lua_pushstring(L, rec->funcKeyStr); + } else { + lua_rawgeti(L, LUA_REGISTRYINDEX, rec->funcKeyRef); + } +} + +static void upvalues_join(lua_State *L, UpvalRecord *upvalsOld, UpvalRecord *upvalsNew, int moduleIdxOld, int moduleIdxNew) { + int absOld = lua_absindex(L, moduleIdxOld); + int absNew = lua_absindex(L, moduleIdxNew); + + for (UpvalRecord *newrec = upvalsNew; newrec; newrec = newrec->next) { + const UpvalRecord *oldrec = upvalues_find(upvalsOld, newrec); + if (!oldrec) { continue; } + + // push closures + upval_record_push_key(L, newrec); // key + lua_gettable(L, absNew); // new closure + int idxNewFn = lua_gettop(L); + + upval_record_push_key(L, oldrec); + lua_gettable(L, absOld); // old closure + int idxOldFn = lua_gettop(L); + + // only look through functions + if (!lua_isfunction(L, idxNewFn) || !lua_isfunction(L, idxOldFn)) { + lua_pop(L, 2); + continue; + } + + // locate upvalue slot on each + int slotNew = -1; + int slotOld = -1; + + // find new slot + for (int i = 1; ; ++i) { + if (!lua_upvalueid(L, idxNewFn, i)) { break; } + if (lua_upvalueid(L, idxNewFn, i) == newrec->id) { + slotNew = i; + break; + } + } + + // find old slot + for (int i = 1; ; ++i) { + if (!lua_upvalueid(L, idxOldFn, i)) { break; } + if (lua_upvalueid(L, idxOldFn, i) == oldrec->id) { + slotOld = i; + break; + } + } + + // join the two upvalues + if (slotNew > 0 && slotOld > 0) { + lua_upvaluejoin(L, idxNewFn, slotNew, idxOldFn, slotOld); + } + + lua_pop(L, 2); // pop both closures + } +} + +static void upvalues_replace_hooks(lua_State *L) { + for (UpvalReference *newRef = sUpvalReferences; newRef; newRef = newRef->next) { + if (newRef->isOld) { continue; } + if (!newRef->name) { continue; } + + for (UpvalReference *oldRef = newRef; oldRef; oldRef = oldRef->next) { + if (!oldRef->isOld) { continue; } + if (!oldRef->name) { continue; } + + if (strcmp(newRef->name, oldRef->name) == 0) { + smlua_hook_replace_function_references(L, oldRef->reference, newRef->reference); + newRef->active = true; + break; + } + } + } +} + +static void upvalues_print(UpvalRecord *upvalsHead) { + for (const UpvalRecord *cur = upvalsHead; cur; cur = cur->next) { + LOG_INFO("upval: %s, %s, %d, %p", cur->funcKeyStr ? cur->funcKeyStr : "(non-string)", cur->name, cur->ref, cur->id); + } +} + +static void overwrite_module_functions(lua_State *L, int dstIdx, int srcIdx) { + srcIdx = lua_absindex(L, srcIdx); + dstIdx = lua_absindex(L, dstIdx); + + lua_pushnil(L); // first key for iteration + while (lua_next(L, srcIdx) != 0) { + // stack: ..., dstTable?, srcTable?, key, newVal + + int typeNew = lua_type(L, -1); + + // lookup oldVal = dstTable[key] + lua_pushvalue(L, -2); // copy key + lua_gettable(L, dstIdx); // push oldVal + int typeOld = lua_type(L, -1); + + bool shouldOverride = + (typeNew == LUA_TFUNCTION && typeOld == LUA_TFUNCTION) || + (typeNew != LUA_TNIL && typeOld == LUA_TNIL); + + if (shouldOverride) { + int idxNewVal = lua_gettop(L) - 1; // index of newVal + + // overwrite oldMod[key] = newVal + lua_pushvalue(L, -3); // key + lua_pushvalue(L, idxNewVal); // newVal + lua_settable(L, dstIdx); // dstTable[key] = newVal + } + + // pop oldVal and newVal (leave key for next lua_next) + lua_pop(L, 2); + } +} + +static void smlua_reload_module(lua_State *L, struct Mod* mod, struct ModFile *file) { + LUA_STACK_CHECK_BEGIN(L); + + // only handle loaded Lua modules + if (!file->isLoadedLuaModule) { return; } + + // build registry key for this mod's loaded table + char registryKey[SYS_MAX_PATH + 16]; + snprintf(registryKey, sizeof(registryKey), "mod_loaded_%s", mod->relativePath); + + // get per-mod "loaded" table + lua_getfield(L, LUA_REGISTRYINDEX, registryKey); // ..., loadedTable + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return; + } + + // get the old module table: loadedTable[file->relativePath] + lua_getfield(L, -1, file->relativePath); // ..., loadedTable, oldMod + if (!lua_istable(L, -1)) { + lua_pop(L, 2); + return; + } + int moduleIdxOld = lua_gettop(L); + + // load & execute the new script -> pushes new module table + struct ModFile *prevFile = gLuaActiveModFile; + gLuaActiveModFile = file; + int rc = smlua_load_script(mod, file, mod->index, false); // ..., loadedTable, oldMod, newMod + gLuaActiveModFile = prevFile; + + // exit on error + if (rc != LUA_OK) { + lua_pop(L, 3); + return; + } + + int moduleIdxNew = lua_gettop(L); + + // merge functions and join upvalues + if (lua_istable(L, moduleIdxOld) && lua_istable(L, moduleIdxNew)) { + // collect old upvals + UpvalRecord *upvalsOld = NULL; + upvalues_collect(L, &upvalsOld, moduleIdxOld, true); + + // collect new upvals + UpvalRecord *upvalsNew = NULL; + upvalues_collect(L, &upvalsNew, moduleIdxNew, false); + + // join upvals + upvalues_join(L, upvalsOld, upvalsNew, moduleIdxOld, moduleIdxNew); + + // replace hooks + upvalues_replace_hooks(L); + + // free upval collections + upvalues_free(L, upvalsOld); + upvalues_free(L, upvalsNew); + + // free upval references + upval_references_free(L); + + // overwrite any functions in oldMod with newMod equivalents + overwrite_module_functions(L, moduleIdxOld, moduleIdxNew); + + // now do the same for metatables + if (lua_getmetatable(L, moduleIdxNew)) { // pushes newMod's metatable + int mtNewIdx = lua_gettop(L); + if (lua_getmetatable(L, moduleIdxOld)) { // pushes oldMod's metatable + int mtOldIdx = lua_gettop(L); + overwrite_module_functions(L, mtOldIdx, mtNewIdx); + lua_pop(L, 1); // pop oldMod's mt + } + lua_pop(L, 1); // pop newMod's mt + } + + // cleanup: replace newMod on stack with oldMod as return value + lua_pushvalue(L, moduleIdxOld); // duplicate oldMod + lua_replace(L, moduleIdxNew); // overwrite newMod slot with oldMod + lua_pop(L, 3); // pop loadedTable and extra oldMod + } else { + lua_pop(L, 3); // pop loadedTable, oldMod, newMod + } + + LUA_STACK_CHECK_END(L); +} + +void smlua_live_reload_update(lua_State* L) { + // only refresh every LIVE_RELOAD_TICK_COUNT ticks + static int refreshTimer = 0; + refreshTimer++; + if ((refreshTimer % LIVE_RELOAD_TICK_COUNT) != 0) { return; } + + // cache the active mod/file + struct Mod* prevMod = gLuaActiveMod; + struct ModFile* prevModFile = gLuaActiveModFile; + + // search for mod files to update + for (int i = 0; i < gActiveMods.entryCount; i++) { + struct Mod *mod = gActiveMods.entries[i]; + gLuaActiveMod = mod; + + for (int j = 0; j < mod->fileCount; j++) { + struct ModFile* file = &mod->files[j]; + + // check modified time + u64 timestamp = mod_get_file_mtime_seconds(file); + if (timestamp <= file->modifiedTimestamp) { continue; } + + // update modified time and reload the module + file->modifiedTimestamp = timestamp; + gLuaActiveModFile = file; + smlua_reload_module(L, mod, file); + } + + } + + // restore previous active mod/file + gLuaActiveMod = prevMod; + gLuaActiveModFile = prevModFile; +} diff --git a/src/pc/lua/smlua_live_reload.h b/src/pc/lua/smlua_live_reload.h new file mode 100644 index 000000000..e70764c74 --- /dev/null +++ b/src/pc/lua/smlua_live_reload.h @@ -0,0 +1,6 @@ +#ifndef SMLUA_LIVE_RELOAD_H +#define SMLUA_LIVE_RELOAD_H + +void smlua_live_reload_update(lua_State* L); + +#endif \ No newline at end of file diff --git a/src/pc/lua/smlua_require.c b/src/pc/lua/smlua_require.c index 813a7430b..96552339a 100644 --- a/src/pc/lua/smlua_require.c +++ b/src/pc/lua/smlua_require.c @@ -4,7 +4,6 @@ #include "pc/mods/mods_utils.h" #include "pc/fs/fmem.h" - // table to track loaded modules per mod static void smlua_init_mod_loaded_table(lua_State* L, const char* modPath) { // Create a unique registry key for this mod's loaded table @@ -136,6 +135,9 @@ static int smlua_custom_require(lua_State* L) { struct ModFile* prevModFile = gLuaActiveModFile; s32 prevTop = lua_gettop(L); + // tag it as a loaded lua module + file->isLoadedLuaModule = true; + // load and execute gLuaActiveModFile = file; smlua_load_script(activeMod, file, activeMod->index, false); diff --git a/src/pc/lua/smlua_require.h b/src/pc/lua/smlua_require.h index 489fe1dc5..dcb8891cb 100644 --- a/src/pc/lua/smlua_require.h +++ b/src/pc/lua/smlua_require.h @@ -3,7 +3,9 @@ #include "smlua.h" +void smlua_require_update(lua_State* L); void smlua_bind_custom_require(lua_State* L); +void smlua_reload_module(lua_State *L, struct Mod* mod, struct ModFile *file); void smlua_init_require_system(void); #endif \ No newline at end of file diff --git a/src/pc/lua/smlua_utils.c b/src/pc/lua/smlua_utils.c index 1d52a4532..4dd773ca0 100644 --- a/src/pc/lua/smlua_utils.c +++ b/src/pc/lua/smlua_utils.c @@ -748,6 +748,24 @@ void smlua_dump_table(int index) { lua_State* L = gLuaState; printf("--------------\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", + lua_tostring(L, -2), + lua_typename(L, lua_type(L, -1))); + } + else { + printf("[meta] %s - %s\n", + lua_typename(L, lua_type(L, -2)), + lua_typename(L, lua_type(L, -1))); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + // table is in the stack at index 't' lua_pushnil(L); // first key while (lua_next(L, index) != 0) { diff --git a/src/pc/mods/mod.c b/src/pc/mods/mod.c index f9c4871e9..d0acabd5c 100644 --- a/src/pc/mods/mod.c +++ b/src/pc/mods/mod.c @@ -8,6 +8,39 @@ #include "pc/utils/md5.h" #include "pc/debuglog.h" #include "pc/fs/fmem.h" +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif + +u64 mod_get_file_mtime_seconds(struct ModFile* file) { +#ifdef _WIN32 + WIN32_FILE_ATTRIBUTE_DATA fad; + if (!GetFileAttributesExA(file->cachedPath, GetFileExInfoStandard, &fad)) { + // error; you could also GetLastError() here + return 0; + } + // FILETIME is 100-ns intervals since 1601-01-01 UTC + ULARGE_INTEGER ull; + ull.LowPart = fad.ftLastWriteTime.dwLowDateTime; + ull.HighPart = fad.ftLastWriteTime.dwHighDateTime; + + const u64 EPOCH_DIFF = 116444736000000000ULL; // 100-ns from 1601 to 1970 + u64 time100ns = ull.QuadPart; + return (time100ns - EPOCH_DIFF) / 10000000ULL; // to seconds +#else + struct stat st; + if (stat(file->cachedPath, &st) != 0) { + // error; errno is set + return 0; + } + return (u64)st.st_mtime; +#endif +} size_t mod_get_lua_size(struct Mod* mod) { if (!mod) { return 0; } @@ -145,6 +178,7 @@ void mod_activate(struct Mod* mod) { // activate dynos models for (int i = 0; i < mod->fileCount; i++) { struct ModFile* file = &mod->files[i]; + file->modifiedTimestamp = mod_get_file_mtime_seconds(file); mod_cache_add(mod, file, false); // forcefully update md5 hash @@ -485,6 +519,52 @@ static void mod_extract_fields(struct Mod* mod) { fclose(f); } +bool mod_refresh_files(struct Mod* mod) { + if (!mod) { return false; } + + // clear files + if (mod->files) { + for (int j = 0; j < mod->fileCount; j++) { + struct ModFile* file = &mod->files[j]; + if (file->fp != NULL) { + f_close(file->fp); + f_delete(file->fp); + file->fp = NULL; + } + if (file->cachedPath != NULL) { + free((char*)file->cachedPath); + file->cachedPath = NULL; + } + } + } + + if (mod->files != NULL) { + free(mod->files); + mod->files = NULL; + } + + mod->fileCount = 0; + mod->fileCapacity = 0; + mod->size = 0; + + // generate packs + dynos_generate_mod_pack(mod->basePath); + + // read files + if (!mod_load_files(mod, mod->name, mod->basePath)) { + LOG_ERROR("Failed to load mod files for '%s'", mod->name); + return false; + } + + // update cache + for (int i = 0; i < mod->fileCount; i++) { + struct ModFile* file = &mod->files[i]; + mod_cache_add(mod, file, true); + } + + return true; +} + bool mod_load(struct Mods* mods, char* basePath, char* modName) { bool valid = false; @@ -530,6 +610,7 @@ bool mod_load(struct Mods* mods, char* basePath, char* modName) { return false; } mods->entries[modIndex] = calloc(1, sizeof(struct Mod)); + struct Mod* mod = mods->entries[modIndex]; if (mod == NULL) { LOG_ERROR("Failed to allocate mod!"); diff --git a/src/pc/mods/mod.h b/src/pc/mods/mod.h index 092396857..bfca4026c 100644 --- a/src/pc/mods/mod.h +++ b/src/pc/mods/mod.h @@ -15,6 +15,8 @@ struct Mods; struct ModFile { char relativePath[SYS_MAX_PATH]; size_t size; + u64 modifiedTimestamp; + bool isLoadedLuaModule; FILE* fp; u64 wroteBytes; @@ -44,9 +46,11 @@ struct Mod { u8 customBehaviorIndex; }; +u64 mod_get_file_mtime_seconds(struct ModFile* file); size_t mod_get_lua_size(struct Mod* mod); void mod_activate(struct Mod* mod); void mod_clear(struct Mod* mod); +bool mod_refresh_files(struct Mod* mod); bool mod_load(struct Mods* mods, char* basePath, char* modName); #endif \ No newline at end of file diff --git a/src/pc/network/network.c b/src/pc/network/network.c index ea9de270e..ef6ec3ec1 100644 --- a/src/pc/network/network.c +++ b/src/pc/network/network.c @@ -184,6 +184,8 @@ bool network_init(enum NetworkType inNetworkType, bool reconnecting) { } #endif + djui_base_set_visible(&gDjuiModReload->base, network_allow_mod_dev_mode()); + LOG_INFO("initialized"); return true; @@ -637,6 +639,33 @@ static inline void color_set(Color color, u8 r, u8 g, u8 b) { color[2] = b; } +bool network_allow_mod_dev_mode(void) { + return (configModDevMode && gNetworkSystem == &gNetworkSystemSocket && gNetworkType == NT_SERVER); +} + +void network_mod_dev_mode_reload(void) { + network_rehost_begin(); + + for (int i = 0; i < gLocalMods.entryCount; i++) { + struct Mod* mod = gLocalMods.entries[i]; + if (mod->enabled) { + mod_refresh_files(mod); + } + } + + djui_lua_error_clear(); + + LOG_CONSOLE(" "); + LOG_CONSOLE("==================================================="); + LOG_CONSOLE("==================================================="); + LOG_CONSOLE("==================================================="); + LOG_CONSOLE("===================== REFRESH ====================="); + LOG_CONSOLE("==================================================="); + LOG_CONSOLE("==================================================="); + LOG_CONSOLE("==================================================="); +} + + void network_shutdown(bool sendLeaving, bool exiting, bool popup, bool reconnecting) { smlua_call_event_hooks(HOOK_ON_EXIT); diff --git a/src/pc/network/network.h b/src/pc/network/network.h index 5437ee941..1ba8e6320 100644 --- a/src/pc/network/network.h +++ b/src/pc/network/network.h @@ -126,6 +126,8 @@ void network_reset_reconnect_and_rehost(void); void network_reconnect_begin(void); bool network_is_reconnecting(void); void network_rehost_begin(void); +bool network_allow_mod_dev_mode(void); +void network_mod_dev_mode_reload(void); void network_update(void); void network_shutdown(bool sendLeaving, bool exiting, bool popup, bool reconnecting);