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 <myster@d>
This commit is contained in:
djoslin0 2025-06-22 02:07:15 -07:00 committed by GitHub
parent 01fd935807
commit c68ee859ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 772 additions and 40 deletions

View file

@ -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

View file

@ -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 -- //

View file

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

View file

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

View file

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

View file

@ -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 |

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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;
}

View file

@ -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},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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];

View file

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

View file

@ -0,0 +1,460 @@
#include <stdbool.h>
#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 upvaluecell 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 upvaluecell 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;
}

View file

@ -0,0 +1,6 @@
#ifndef SMLUA_LIVE_RELOAD_H
#define SMLUA_LIVE_RELOAD_H
void smlua_live_reload_update(lua_State* L);
#endif

View file

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

View file

@ -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

View file

@ -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) {

View file

@ -8,6 +8,39 @@
#include "pc/utils/md5.h"
#include "pc/debuglog.h"
#include "pc/fs/fmem.h"
#include <stdint.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <sys/stat.h>
#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!");

View file

@ -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

View file

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

View file

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