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);