17#define MOD_MAX_SHADOW_ATTEMPTS 32
18#define MOD_RELOAD_STABLE_MS 500u
19#define MOD_RELOAD_POLL_MS 1000u
23static mod_vec loaded_mods;
24static char mods_dir[MAX_PATH];
25static DWORD last_reload_scan_ms = 0;
26static unsigned long shadow_counter = 0;
27static uintptr_t hook_owner_counter = 0;
37 .config = base_ctx->
config,
50 .write_time = find_data->ftLastWriteTime,
51 .size_high = find_data->nFileSizeHigh,
52 .size_low = find_data->nFileSizeLow,
57 *out = sdsnew(mods_dir);
72 return info_fn ? info_fn() :
NULL;
86 "Mod: %s v%s by %s (%s)",
109 *mod->
context = mod_context(base_ctx);
113#define MOD_WITH_OWNER(mod, call) \
115 void *previous_owner = DTTR_Core_HookSetOwner((mod)->hook_owner); \
117 DTTR_Core_HookSetOwner(previous_owner); \
120#define MOD_DISPATCH(field, ...) \
122 for (size_t i = 0; i < kv_size(loaded_mods); i++) { \
123 loaded_mod *mod = &kv_A(loaded_mods, i); \
125 MOD_WITH_OWNER(mod, mod->field(__VA_ARGS__)); \
130#define MOD_OPTIONAL_EXPORTS(X) \
131 X(tick, DTTR_Mods_TickFn, "DTTR_Mod_Tick") \
132 X(event, DTTR_Mods_EventFn, "DTTR_Mod_Event") \
133 X(info, DTTR_Mods_InfoFn, "DTTR_Mod_Info") \
134 X(late_init, DTTR_Mods_LateInitFn, "DTTR_Mod_LateInit") \
135 X(before_unload, DTTR_Mods_BeforeUnloadFn, "DTTR_Mod_BeforeUnload") \
136 X(frame_begin, DTTR_Mods_FrameBeginFn, "DTTR_Mod_FrameBegin") \
137 X(before_game_frame, DTTR_Mods_BeforeGameFrameFn, "DTTR_Mod_BeforeGameFrame") \
138 X(after_game_frame, DTTR_Mods_AfterGameFrameFn, "DTTR_Mod_AfterGameFrame") \
139 X(before_present, DTTR_Mods_BeforePresentFn, "DTTR_Mod_BeforePresent") \
140 X(after_present, DTTR_Mods_AfterPresentFn, "DTTR_Mod_AfterPresent") \
141 X(frame_end, DTTR_Mods_FrameEndFn, "DTTR_Mod_FrameEnd") \
142 X(imgui_begin, DTTR_Mods_ImGuiBeginFn, "DTTR_Mod_ImGuiBegin") \
143 X(imgui_end, DTTR_Mods_ImGuiEndFn, "DTTR_Mod_ImGuiEnd") \
144 X(overlay_visible_changed, \
145 DTTR_Mods_OverlayVisibleChangedFn, \
146 "DTTR_Mod_OverlayVisibleChanged") \
147 X(window_created, DTTR_Mods_WindowCreatedFn, "DTTR_Mod_WindowCreated") \
148 X(window_resized, DTTR_Mods_WindowResizedFn, "DTTR_Mod_WindowResized") \
149 X(window_destroying, DTTR_Mods_WindowDestroyingFn, "DTTR_Mod_WindowDestroying") \
150 X(graphics_device_created, \
151 DTTR_Mods_GraphicsDeviceCreatedFn, \
152 "DTTR_Mod_GraphicsDeviceCreated") \
153 X(graphics_device_lost, \
154 DTTR_Mods_GraphicsDeviceLostFn, \
155 "DTTR_Mod_GraphicsDeviceLost") \
156 X(graphics_device_restored, \
157 DTTR_Mods_GraphicsDeviceRestoredFn, \
158 "DTTR_Mod_GraphicsDeviceRestored") \
159 X(graphics_device_destroying, \
160 DTTR_Mods_GraphicsDeviceDestroyingFn, \
161 "DTTR_Mod_GraphicsDeviceDestroying") \
162 X(before_event, DTTR_Mods_BeforeEventFn, "DTTR_Mod_BeforeEvent") \
163 X(after_event, DTTR_Mods_AfterEventFn, "DTTR_Mod_AfterEvent") \
164 X(input_mode_changed, DTTR_Mods_InputModeChangedFn, "DTTR_Mod_InputModeChanged") \
165 X(render_game, DTTR_Mods_RenderGameFn, "DTTR_Mod_RenderGame") \
166 X(render, DTTR_Mods_RenderFn, "DTTR_Mod_Render") \
167 X(should_advance_game_frame, \
168 DTTR_Mods_ShouldAdvanceGameFrameFn, \
169 "DTTR_Mod_ShouldAdvanceGameFrame") \
170 X(game_frame_advanced, DTTR_Mods_GameFrameAdvancedFn, "DTTR_Mod_GameFrameAdvanced") \
171 X(game_frame_blocked, DTTR_Mods_GameFrameBlockedFn, "DTTR_Mod_GameFrameBlocked")
183 for (
size_t i = 0; i < kv_size(loaded_mods); i++) {
184 if (strcmp(kv_A(loaded_mods, i).filename, filename) == 0) {
214 "Refusing to unload mod %s because one or more hooks could not be restored",
226 if (index < 0 || (
size_t)index >= kv_size(loaded_mods)) {
231 const size_t last = kv_size(loaded_mods) - 1;
232 if ((
size_t)index < last) {
234 &kv_A(loaded_mods, index),
235 &kv_A(loaded_mods, index + 1),
236 (last - (
size_t)index) *
sizeof(kv_A(loaded_mods, 0))
245 const DWORD process_id = GetCurrentProcessId();
248 sds shadow_name = sdscatprintf(
251 (
unsigned long)process_id,
259 sds shadow_path =
NULL;
260 const bool made_shadow_path =
make_mod_path(&shadow_path, shadow_name);
261 sdsfree(shadow_name);
262 if (!made_shadow_path) {
263 sdsfree(shadow_path);
272 sdsfree(shadow_path);
273 if (!copied_shadow_path) {
281 if (GetLastError() != ERROR_FILE_EXISTS) {
290#define LOAD_OPTIONAL_EXPORT(field, fn_type, symbol) \
291 mod->field = (fn_type)GetProcAddress(mod->handle, symbol);
295#undef LOAD_OPTIONAL_EXPORT
299 const char *filename,
300 const char *source_path,
304 memset(out, 0,
sizeof(*out));
305 out->
hook_owner = (
void *)(++hook_owner_counter);
314 "Failed to copy mod DLL for hot reload: %s (error %lu)",
324 out->
handle = LoadLibraryA(load_path);
326 DTTR_LOG_WARN(
"Failed to load mod DLL: %s (error %lu)", filename, GetLastError());
336 "Mod %s missing required exports "
337 "(DTTR_Mod_Init/DTTR_Mod_Cleanup) - skipping",
397 const char *filename,
398 const char *source_path,
401 if (kv_size(loaded_mods) >=
MODS_MAX) {
407 if (!
prepare_mod(filename, source_path, source_file, &mod)) {
420 loaded_mod *old_mod = &kv_A(loaded_mods, index);
435 kv_A(loaded_mods, index) = new_mod;
444 for (
int i = (
int)kv_size(loaded_mods) - 1; i >= 0; i--) {
455 while (kv_size(loaded_mods) > 0) {
456 const int i = (int)kv_size(loaded_mods) - 1;
466 const WIN32_FIND_DATAA *find_data,
471 if (find_data->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
480 DTTR_LOG_INFO(
"Mod disabled, skipping: %s", find_data->cFileName);
484 sds source_path =
NULL;
486 sdsfree(source_path);
491 const int index =
find_mod(find_data->cFileName);
493 load_mod(find_data->cFileName, source_path, &source_file);
494 const int new_index =
find_mod(find_data->cFileName);
495 if (new_index >= 0) {
496 seen[new_index] =
true;
499 sdsfree(source_path);
504 bool restart_scan =
false;
507 restart_scan =
reload_mod(index, source_path, &source_file);
510 sdsfree(source_path);
518 sds search_pattern =
NULL;
520 sdsfree(search_pattern);
524 WIN32_FIND_DATAA find_data;
525 HANDLE find_handle = FindFirstFileA(search_pattern, &find_data);
526 sdsfree(search_pattern);
528 if (find_handle == INVALID_HANDLE_VALUE) {
537 const DWORD now_ms = GetTickCount();
538 bool restart_scan =
false;
545 }
while (FindNextFileA(find_handle, &find_data));
547 FindClose(find_handle);
560 if (!resolved_mods_dir
562 sdsfree(resolved_mods_dir);
566 const DWORD attrs = GetFileAttributesA(resolved_mods_dir);
567 if (attrs == INVALID_FILE_ATTRIBUTES || !(attrs & FILE_ATTRIBUTE_DIRECTORY)) {
568 DTTR_LOG_INFO(
"No mods directory found at %s - skipping", resolved_mods_dir);
569 sdsfree(resolved_mods_dir);
573 const bool copied =
DTTR_Path_CopySds(mods_dir,
sizeof(mods_dir), resolved_mods_dir);
574 sdsfree(resolved_mods_dir);
583 const DWORD now_ms = GetTickCount();
588 last_reload_scan_ms = now_ms;
600 last_reload_scan_ms = GetTickCount();
602 DTTR_LOG_INFO(
"Loaded %d mod(s)", (
int)kv_size(loaded_mods));
608 for (
size_t i = 0; i < kv_size(loaded_mods); i++) {
687 for (
size_t i = 0; i < kv_size(loaded_mods); i++) {
694 bool consumed =
false;
717 for (
size_t i = 0; i < kv_size(loaded_mods); i++) {
723 bool should_advance =
true;
725 if (!should_advance) {
742 for (
size_t i = 0; i < kv_size(loaded_mods); i++) {
743 if (kv_A(loaded_mods, i).render_game) {
764 return kv_size(loaded_mods);
768 if (index >= kv_size(loaded_mods)) {
777 if (index >= kv_size(loaded_mods)) {
781 return GetTickCount() - kv_A(loaded_mods, index).loaded_at_ms;
790 kv_destroy(loaded_mods);
791 kv_init(loaded_mods);
793 last_reload_scan_ms = 0;
void void DWORD HANDLE event
DTTR_Graphics_COM_DirectDrawSurface7 DWORD flags void NULL
#define DTTR_MODS_SHADOW_PREFIX
bool DTTR_Config_IsModDisabled(const DTTR_Config *config, const char *mod_filename)
#define DTTR_FATAL(error_message,...)
#define DTTR_LOG_WARN(...)
#define DTTR_LOG_INFO(...)
void(* DTTR_Mods_CleanupFn)()
union SDL_Event SDL_Event
bool(* DTTR_Mods_EventFn)(const SDL_Event *event)
bool(* DTTR_Mods_InitFn)(const DTTR_Mods_Context *ctx)
const DTTR_Mods_Info *(* DTTR_Mods_InfoFn)()
bool DTTR_Path_CopyString(char *out, size_t out_size, const char *value)
bool DTTR_Path_CopySds(char *out, size_t out_size, sds value)
bool DTTR_Path_AppendSegment(sds *path, const char *segment, char separator)
bool DTTR_Core_HookDetachOwnerChecked(void *owner)
void * DTTR_Core_HookSetOwner(void *owner)
const DTTR_Mods_Context * dttr_sidecar_context()
char dttr_exe_hash[DTTR_EXE_HASH_LENGTH+1]
char dttr_loader_dir[MAX_PATH]
SDL_Window * dttr_graphics_get_window()
void dttr_mods_render_game(const DTTR_Mods_RenderGameContext *ctx)
void dttr_mods_input_mode_changed(const DTTR_Mods_InputContext *ctx)
static void log_mod_info(const char *filename, const DTTR_Mods_Info *info)
static void attempt_hot_reload_mods()
#define MOD_RELOAD_POLL_MS
static void set_mod_display_name(loaded_mod *mod, const DTTR_Mods_Info *info)
void dttr_mods_overlay_visible_changed(bool visible)
static void destroy_mod_context(loaded_mod *mod)
void dttr_mods_render(const DTTR_Mods_RenderContext *ctx)
static void load_mod(const char *filename, const char *source_path, const mod_file_id *source_file)
void dttr_mods_window_destroying(const DTTR_Mods_WindowContext *ctx)
static bool resolve_mods_dir()
void dttr_mods_after_game_frame(const DTTR_Mods_FrameContext *ctx)
static void remove_all_mods(bool log_deleted)
void dttr_mods_imgui_end(const DTTR_Mods_RenderContext *ctx)
static void delete_shadow_copy(loaded_mod *mod)
static void scan_mods(bool initial_scan)
void dttr_mods_frame_begin(const DTTR_Mods_FrameContext *ctx)
static bool refresh_mod_context(loaded_mod *mod, const DTTR_Mods_Context *base_ctx)
static bool make_mod_path(sds *out, const char *filename)
#define MOD_MAX_SHADOW_ATTEMPTS
void dttr_mods_before_present(const DTTR_Mods_PresentContext *ctx)
size_t dttr_mods_loaded_count()
static const DTTR_Mods_Info * get_mod_info(DTTR_Mods_InfoFn info_fn)
void dttr_mods_late_init()
bool dttr_mods_handle_event(const SDL_Event *event)
void dttr_mods_graphics_device_destroying(const DTTR_Mods_GraphicsContext *ctx)
void dttr_mods_after_present(const DTTR_Mods_PresentContext *ctx)
void dttr_mods_window_resized(const DTTR_Mods_WindowContext *ctx)
void dttr_mods_imgui_begin(const DTTR_Mods_RenderContext *ctx)
static bool dispatch_event_until_consumed(const SDL_Event *event, bool before_event)
bool dttr_mods_has_render_game()
static bool prepare_mod(const char *filename, const char *source_path, const mod_file_id *source_file, loaded_mod *out)
#define MOD_DISPATCH(field,...)
static bool file_id_equal(const mod_file_id *lhs, const mod_file_id *rhs)
DWORD dttr_mods_loaded_elapsed_ms(size_t index)
static bool make_shadow_path(loaded_mod *mod)
static mod_file_id make_mod_file_id(const WIN32_FIND_DATAA *find_data)
static void remove_missing_mods(const bool *seen, bool initial_scan)
static bool is_shadow_mod(const char *filename)
static void remove_mod_at(int index)
#define MOD_WITH_OWNER(mod, call)
static int find_mod(const char *filename)
void dttr_mods_frame_end(const DTTR_Mods_FrameContext *ctx)
static bool should_reload_now(loaded_mod *mod, const mod_file_id *source_file, DWORD now_ms)
void dttr_mods_game_frame_blocked()
typedef kvec_t(loaded_mod)
bool dttr_mods_should_advance_game_frame()
static void unload_mod(loaded_mod *mod)
void dttr_mods_graphics_device_created(const DTTR_Mods_GraphicsContext *ctx)
void dttr_mods_window_created(const DTTR_Mods_WindowContext *ctx)
static bool reload_mod(int index, const char *source_path, const mod_file_id *source_file)
#define MOD_OPTIONAL_EXPORTS(X)
static bool scan_mod_file(const WIN32_FIND_DATAA *find_data, DWORD now_ms, bool initial_scan, bool *seen)
const char * dttr_mods_loaded_name(size_t index)
#define MOD_RELOAD_STABLE_MS
bool dttr_mods_before_event(const SDL_Event *event)
static void log_mod_deleted(const loaded_mod *mod)
void dttr_mods_before_game_frame(const DTTR_Mods_FrameContext *ctx)
void dttr_mods_game_frame_advanced()
static void load_optional_exports(loaded_mod *mod)
static bool init_mod(loaded_mod *mod)
void dttr_mods_graphics_device_restored(const DTTR_Mods_GraphicsContext *ctx)
void dttr_mods_graphics_device_lost(const DTTR_Mods_GraphicsContext *ctx)
void dttr_mods_after_event(const SDL_Event *event, bool consumed)
#define LOAD_OPTIONAL_EXPORT(field, fn_type, symbol)
bool dttr_mods_hot_reload_enabled()
DTTR_Core_Context runtime
const DTTR_Mods_API * api
DTTR_Mods_Context * context
DTTR_Mods_CleanupFn cleanup
DTTR_Mods_ShouldAdvanceGameFrameFn should_advance_game_frame
char display_name[MAX_PATH]
DTTR_Mods_BeforeEventFn before_event
char shadow_path[MAX_PATH]
char source_path[MAX_PATH]
DTTR_Mods_BeforeUnloadFn before_unload