diff --git a/Data/mupen64plus.ini b/Data/mupen64plus.ini index 55372af..a66751a 100644 --- a/Data/mupen64plus.ini +++ b/Data/mupen64plus.ini @@ -14149,6 +14149,18 @@ CRC=D68CEFC0 0B81D955 RefMD5=5AAC6E652C5CF1E37A466AC0073E88CA CountPerOp=1 +[2B2D6B295106C54216B7FC7A2F14346E] +GoodName=SmashRemix2.0.1 +CRC=34EE4749 1833C64C +RefMD5=5AAC6E652C5CF1E37A466AC0073E88CA +CountPerOp=1 + +[0CFDA9AB8E7DEE5B088385D2576369E6] +GoodName=SmashRemix2.0.1 (PAL60) +CRC=96D0976A 2FB7654F +RefMD5=5AAC6E652C5CF1E37A466AC0073E88CA +CountPerOp=1 + [B8D4B92E66A312708626B3216DE07A3A] GoodName=Snobow Kids (J) [!] CRC=84FC04FF B1253CE9 diff --git a/src/converter.cpp b/src/converter.cpp index 5ec90ac..58a97c0 100644 --- a/src/converter.cpp +++ b/src/converter.cpp @@ -351,6 +351,18 @@ bool convert_one(const std::string& krec_path, const std::string& output_path, emu.apply_deterministic_settings(); + // Apply GameShark cheats if requested + if (config.remove_music) { + m64p_cheat_code no_music[] = { + {0x81462BE2, 0x0000}, + }; + if (emu.apply_cheat("NoMusic", no_music, 1)) { + converter_log(LOG_INFO, "Applied 'Remove Background Music' cheat."); + } else { + converter_log(LOG_WARNING, "Warning: failed to apply music removal cheat."); + } + } + // Setup FFmpeg encoder config (video only, to temp file) // Encoder is opened lazily on first frame to match actual render dimensions FFmpegEncoder encoder; diff --git a/src/converter.h b/src/converter.h index 9d1397c..33f8fcf 100644 --- a/src/converter.h +++ b/src/converter.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include @@ -40,7 +41,9 @@ struct AppConfig { int aniso = 0; // 0=off, 2, 4, 8, 16 std::string encoder = "libx264"; // FFmpeg codec name bool batch = false; + std::vector batch_files; // selected files for batch mode bool verbose = false; + bool remove_music = false; }; // Get the directory containing the executable diff --git a/src/emulator.cpp b/src/emulator.cpp index ab76bf0..656f029 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -78,6 +78,9 @@ bool Emulator::load_core(const std::string& path) { RESOLVE(config_open_section, ptr_ConfigOpenSection, "ConfigOpenSection"); RESOLVE(config_set_parameter, ptr_ConfigSetParameter, "ConfigSetParameter"); + // Optional cheat API + core_add_cheat = (ptr_CoreAddCheat)GET_PROC(core_handle, "CoreAddCheat"); + // Optional RMG-K extension set_pif_callback_fn = (ptr_set_pif_sync_callback)GET_PROC(core_handle, "set_pif_sync_callback"); if (!set_pif_callback_fn) { @@ -333,6 +336,19 @@ void Emulator::apply_deterministic_settings() { // via GLideN64.ini in configure_gliden64(), called during init(). } +bool Emulator::apply_cheat(const char* name, m64p_cheat_code* codes, int num_codes) { + if (!core_add_cheat) { + fprintf(stderr, "Warning: CoreAddCheat not available in this core build\n"); + return false; + } + m64p_error ret = core_add_cheat(name, codes, num_codes); + if (ret != M64ERR_SUCCESS) { + fprintf(stderr, "Error: CoreAddCheat('%s') failed (error %d)\n", name, ret); + return false; + } + return true; +} + void Emulator::configure_controllers_for_replay(int num_players) { m64p_handle section; diff --git a/src/emulator.h b/src/emulator.h index 4e47ab9..61ccffb 100644 --- a/src/emulator.h +++ b/src/emulator.h @@ -101,6 +101,12 @@ struct pif { typedef void (*pif_sync_callback_t)(struct pif*); +// Cheat code structure +typedef struct { + uint32_t address; + int value; +} m64p_cheat_code; + } // extern "C" // Mupen64plus core function pointer types @@ -128,6 +134,10 @@ typedef m64p_error (*ptr_ConfigSetDefaultString)(m64p_handle, const char*, const typedef m64p_error (*ptr_PluginStartup)(m64p_dynlib_handle, void*, ptr_DebugCallback); typedef m64p_error (*ptr_PluginShutdown)(void); +// Cheat API +typedef m64p_error (*ptr_CoreAddCheat)(const char*, m64p_cheat_code*, int); +typedef m64p_error (*ptr_CoreCheatEnabled)(const char*, int); + // RMG-K extension typedef void (*ptr_set_pif_sync_callback)(pif_sync_callback_t); @@ -158,6 +168,7 @@ class Emulator { bool open_rom(const std::string& rom_path); bool attach_plugins(); void apply_deterministic_settings(); + bool apply_cheat(const char* name, m64p_cheat_code* codes, int num_codes); void configure_controllers_for_replay(int num_players); void set_pif_callback(pif_sync_callback_t callback); void set_frame_callback(m64p_frame_callback callback); @@ -186,6 +197,7 @@ class Emulator { ptr_ConfigOpenSection config_open_section = nullptr; ptr_ConfigSetParameter config_set_parameter = nullptr; ptr_set_pif_sync_callback set_pif_callback_fn = nullptr; + ptr_CoreAddCheat core_add_cheat = nullptr; ptr_ReadScreen2 read_screen2 = nullptr; bool verbose = false; diff --git a/src/frame_capture.cpp b/src/frame_capture.cpp index 553c3c4..dadd64a 100644 --- a/src/frame_capture.cpp +++ b/src/frame_capture.cpp @@ -41,7 +41,7 @@ static FFmpegConfig s_ff_config; static bool s_encoder_opened = false; static int s_captured_frames = 0; static int s_total_frames = 0; -static bool s_speed_limiter_disabled = false; +static bool s_speed_set = false; static ProgressCallback s_progress_callback; static std::atomic* s_cancel_flag = nullptr; @@ -194,7 +194,7 @@ void frame_capture_init(Emulator* emu, FFmpegEncoder* encoder, const FFmpegConfi s_encoder_opened = false; s_captured_frames = 0; s_total_frames = total_frames; - s_speed_limiter_disabled = false; + s_speed_set = false; // Keep buffers allocated across batch runs to avoid reallocation s_progress_callback = nullptr; s_cancel_flag = nullptr; @@ -235,10 +235,10 @@ void frame_capture_callback(unsigned int frame_index) { } // Disable speed limiter on first frame for maximum conversion speed - if (!s_speed_limiter_disabled && s_emu) { + if (!s_speed_set && s_emu) { int limiter = 0; // 0 = off s_emu->core_do_command(M64CMD_CORE_STATE_SET, M64CORE_SPEED_LIMITER, &limiter); - s_speed_limiter_disabled = true; + s_speed_set = true; } // Reset PIF sync flag for next frame diff --git a/src/gui_main.cpp b/src/gui_main.cpp index 88cf7f5..a07ead7 100644 --- a/src/gui_main.cpp +++ b/src/gui_main.cpp @@ -39,6 +39,9 @@ static HFONT g_font = nullptr; static HWND g_rom_path = nullptr; static HWND g_input_path = nullptr; static HWND g_batch_check = nullptr; +static HWND g_batch_list = nullptr; +static HWND g_batch_select_all = nullptr; +static HWND g_batch_deselect_all = nullptr; static HWND g_output_path = nullptr; static HWND g_resolution_combo = nullptr; static HWND g_quality_combo = nullptr; @@ -48,6 +51,7 @@ static HWND g_msaa_value = nullptr; static HWND g_aniso_slider = nullptr; static HWND g_aniso_value = nullptr; static HWND g_encoder_combo = nullptr; +static HWND g_remove_music_check = nullptr; static HWND g_verbose_check = nullptr; static HWND g_convert_btn = nullptr; static HWND g_cancel_btn = nullptr; @@ -240,6 +244,8 @@ static void SaveSettings() { WritePrivateProfileStringW(sec, L"Batch", batch ? L"1" : L"0", file); bool verbose = (SendMessageW(g_verbose_check, BM_GETCHECK, 0, 0) == BST_CHECKED); WritePrivateProfileStringW(sec, L"Verbose", verbose ? L"1" : L"0", file); + bool remove_music = (SendMessageW(g_remove_music_check, BM_GETCHECK, 0, 0) == BST_CHECKED); + WritePrivateProfileStringW(sec, L"RemoveMusic", remove_music ? L"1" : L"0", file); // Resolution (save index) int res_sel = (int)SendMessageW(g_resolution_combo, CB_GETCURSEL, 0, 0); @@ -302,6 +308,8 @@ static void LoadSettings() { SendMessageW(g_batch_check, BM_SETCHECK, batch ? BST_CHECKED : BST_UNCHECKED, 0); int verbose = GetPrivateProfileIntW(sec, L"Verbose", 0, file); SendMessageW(g_verbose_check, BM_SETCHECK, verbose ? BST_CHECKED : BST_UNCHECKED, 0); + int remove_music = GetPrivateProfileIntW(sec, L"RemoveMusic", 0, file); + SendMessageW(g_remove_music_check, BM_SETCHECK, remove_music ? BST_CHECKED : BST_UNCHECKED, 0); // Resolution int res_sel = GetPrivateProfileIntW(sec, L"Resolution", 1, file); @@ -410,10 +418,43 @@ static void CreateControls(HWND hwnd) { CreateBtn(hwnd, L"Browse...", IDC_INPUT_BROWSE, EDIT_X + EDIT_W + GAP, y, BTN_W, ROW_H); y += ROW_H + GAP; - g_batch_check = CreateCheck(hwnd, L"Batch mode (process all .krec in folder)", + g_batch_check = CreateCheck(hwnd, L"Batch mode (process .krec files in folder)", IDC_BATCH_CHECK, EDIT_X, y, EDIT_W, ROW_H); y += ROW_H + GAP; + // Batch file list (hidden by default, shown when batch mode is checked) + g_batch_select_all = CreateBtn(hwnd, L"Select All", IDC_BATCH_SELECT_ALL, + EDIT_X, y, 80, ROW_H); + g_batch_deselect_all = CreateBtn(hwnd, L"Deselect All", IDC_BATCH_DESELECT_ALL, + EDIT_X + 86, y, 90, ROW_H); + ShowWindow(g_batch_select_all, SW_HIDE); + ShowWindow(g_batch_deselect_all, SW_HIDE); + y += ROW_H + 2; + + g_batch_list = CreateWindowExW(WS_EX_CLIENTEDGE, WC_LISTVIEWW, L"", + WS_CHILD | LVS_REPORT | LVS_NOSORTHEADER | LVS_SHOWSELALWAYS, + EDIT_X, y, CLIENT_W - EDIT_X - MARGIN, 150, + hwnd, (HMENU)(INT_PTR)IDC_BATCH_LIST, nullptr, nullptr); + SendMessageW(g_batch_list, WM_SETFONT, (WPARAM)g_font, TRUE); + ListView_SetExtendedListViewStyle(g_batch_list, + LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT); + { + int list_w = CLIENT_W - EDIT_X - MARGIN - 24; // leave room for scrollbar + LVCOLUMNW col = {}; + col.mask = LVCF_TEXT | LVCF_WIDTH; + col.pszText = (LPWSTR)L"Filename"; + col.cx = list_w * 5 / 12; + SendMessageW(g_batch_list, LVM_INSERTCOLUMNW, 0, (LPARAM)&col); + col.pszText = (LPWSTR)L"Game"; + col.cx = list_w * 4 / 12; + SendMessageW(g_batch_list, LVM_INSERTCOLUMNW, 1, (LPARAM)&col); + col.pszText = (LPWSTR)L"Date"; + col.cx = list_w * 3 / 12; + SendMessageW(g_batch_list, LVM_INSERTCOLUMNW, 2, (LPARAM)&col); + } + ShowWindow(g_batch_list, SW_HIDE); + y += 150 + GAP; + CreateLabel(hwnd, L"Output:", MARGIN, y + 2, LBL_W, ROW_H); g_output_path = CreateEdit(hwnd, IDC_OUTPUT_PATH, EDIT_X, y, EDIT_W, ROW_H); CreateBtn(hwnd, L"Browse...", IDC_OUTPUT_BROWSE, EDIT_X + EDIT_W + GAP, y, BTN_W, ROW_H); @@ -497,6 +538,11 @@ static void CreateControls(HWND hwnd) { SendMessageW(g_aniso_value, WM_SETFONT, (WPARAM)g_font, TRUE); y += ROW_H + GAP + 4; + // --- Game-specific options --- + g_remove_music_check = CreateCheck(hwnd, L"Remove background music (Remix 2.0.1)", + IDC_REMOVE_MUSIC_CHECK, EDIT_X, y, 280, ROW_H); + y += ROW_H + GAP; + // --- Verbose + buttons --- g_verbose_check = CreateCheck(hwnd, L"Verbose logging", IDC_VERBOSE_CHECK, EDIT_X, y, 160, ROW_H); @@ -541,6 +587,110 @@ static void CreateControls(HWND hwnd) { SendMessageW(g_log_edit, WM_SETFONT, (WPARAM)g_font, TRUE); } +// --- Batch file list helpers --- + +struct KrecHeader { + std::string game; + uint32_t timestamp = 0; +}; + +static KrecHeader ReadKrecHeader(const std::string& path) { + KrecHeader hdr; + FILE* f = fopen(path.c_str(), "rb"); + if (!f) return hdr; + // Game name at offset 132, 128 bytes + char buf[128] = {}; + if (fseek(f, 132, SEEK_SET) == 0) + fread(buf, 1, 127, f); + buf[127] = 0; + hdr.game = buf; + // Timestamp at offset 260, 4 bytes + if (fseek(f, 260, SEEK_SET) == 0) + fread(&hdr.timestamp, 4, 1, f); + fclose(f); + return hdr; +} + +static void PopulateBatchList() { + ListView_DeleteAllItems(g_batch_list); + std::string dir = GetEditText(g_input_path); + if (dir.empty() || !fs::is_directory(dir)) return; + + // Collect entries with header info + struct KrecEntry { + std::wstring filename; + std::wstring game; + std::wstring date; + uint32_t timestamp; + }; + std::vector entries; + for (auto& entry : fs::directory_iterator(dir)) { + if (entry.is_regular_file() && entry.path().extension() == ".krec") { + KrecHeader hdr = ReadKrecHeader(entry.path().string()); + std::wstring date_str; + if (hdr.timestamp > 0) { + time_t t = (time_t)hdr.timestamp; + struct tm* tm = localtime(&t); + if (tm) { + char tbuf[64]; + strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M", tm); + date_str = Utf8ToWide(tbuf); + } + } + entries.push_back({entry.path().filename().wstring(), + Utf8ToWide(hdr.game), date_str, hdr.timestamp}); + } + } + // Sort newest first + std::sort(entries.begin(), entries.end(), + [](const KrecEntry& a, const KrecEntry& b) { + return a.timestamp > b.timestamp; + }); + + SendMessageW(g_batch_list, WM_SETREDRAW, FALSE, 0); + for (size_t i = 0; i < entries.size(); i++) { + LVITEMW item = {}; + item.mask = LVIF_TEXT; + item.iItem = (int)i; + item.pszText = (LPWSTR)entries[i].filename.c_str(); + SendMessageW(g_batch_list, LVM_INSERTITEMW, 0, (LPARAM)&item); + // Game name column + LVITEMW sub = {}; + sub.iItem = (int)i; + sub.iSubItem = 1; + sub.pszText = (LPWSTR)entries[i].game.c_str(); + SendMessageW(g_batch_list, LVM_SETITEMTEXTW, (WPARAM)i, (LPARAM)&sub); + // Date column + sub.iSubItem = 2; + sub.pszText = (LPWSTR)entries[i].date.c_str(); + SendMessageW(g_batch_list, LVM_SETITEMTEXTW, (WPARAM)i, (LPARAM)&sub); + ListView_SetCheckState(g_batch_list, (int)i, TRUE); + } + SendMessageW(g_batch_list, WM_SETREDRAW, TRUE, 0); + InvalidateRect(g_batch_list, nullptr, TRUE); +} + +static void UpdateBatchListVisibility() { + bool batch = (SendMessageW(g_batch_check, BM_GETCHECK, 0, 0) == BST_CHECKED); + int show = batch ? SW_SHOW : SW_HIDE; + ShowWindow(g_batch_list, show); + ShowWindow(g_batch_select_all, show); + ShowWindow(g_batch_deselect_all, show); + if (batch) { + PopulateBatchList(); + // Adjust output path to folder (strip filename if present) + std::string out = GetEditText(g_output_path); + if (!out.empty()) { + fs::path p(out); + if (!fs::is_directory(p) && p.has_parent_path()) { + SetEditText(g_output_path, p.parent_path().string()); + } + } + } else { + ListView_DeleteAllItems(g_batch_list); + } +} + // --- Worker thread --- static void WorkerThread(AppConfig config) { @@ -568,11 +718,7 @@ static void WorkerThread(AppConfig config) { std::vector krec_files; if (config.batch) { - for (auto& entry : fs::directory_iterator(config.input_path)) { - if (entry.is_regular_file() && entry.path().extension() == ".krec") { - krec_files.push_back(entry.path().string()); - } - } + krec_files = std::move(config.batch_files); } else { krec_files.push_back(config.input_path); } @@ -624,7 +770,24 @@ static AppConfig ReadConfig() { cfg.input_path = GetEditText(g_input_path); cfg.output_path = GetEditText(g_output_path); cfg.batch = (SendMessageW(g_batch_check, BM_GETCHECK, 0, 0) == BST_CHECKED); + if (cfg.batch) { + int count = ListView_GetItemCount(g_batch_list); + for (int i = 0; i < count; i++) { + if (ListView_GetCheckState(g_batch_list, i)) { + wchar_t buf[MAX_PATH] = {}; + LVITEMW lvi = {}; + lvi.iSubItem = 0; + lvi.pszText = buf; + lvi.cchTextMax = MAX_PATH; + SendMessageW(g_batch_list, LVM_GETITEMTEXTW, (WPARAM)i, (LPARAM)&lvi); + std::string filename = WideToUtf8(buf); + cfg.batch_files.push_back( + (fs::path(cfg.input_path) / filename).string()); + } + } + } cfg.verbose = (SendMessageW(g_verbose_check, BM_GETCHECK, 0, 0) == BST_CHECKED); + cfg.remove_music = (SendMessageW(g_remove_music_check, BM_GETCHECK, 0, 0) == BST_CHECKED); // Default paths relative to exe std::string exe_dir = get_exe_dir(); @@ -695,6 +858,10 @@ static void StartConversion() { MessageBoxW(g_hwnd, L"In batch mode, output must be an existing directory.", L"Validation Error", MB_ICONWARNING); return; } + if (cfg.batch && cfg.batch_files.empty()) { + MessageBoxW(g_hwnd, L"No files selected for conversion.", L"Validation Error", MB_ICONWARNING); + return; + } if (!cfg.batch && !fs::is_regular_file(cfg.input_path)) { MessageBoxW(g_hwnd, L"Input file does not exist.", L"Validation Error", MB_ICONWARNING); return; @@ -727,6 +894,7 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara case WM_CREATE: CreateControls(hwnd); LoadSettings(); + UpdateBatchListVisibility(); return 0; case WM_HSCROLL: { @@ -753,6 +921,19 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara return 0; } + // Batch checkbox toggled -> show/hide file list + if (id == IDC_BATCH_CHECK && code == BN_CLICKED) { + UpdateBatchListVisibility(); + return 0; + } + + // Input path lost focus while in batch mode -> repopulate list + if (id == IDC_INPUT_PATH && code == EN_KILLFOCUS) { + bool batch = (SendMessageW(g_batch_check, BM_GETCHECK, 0, 0) == BST_CHECKED); + if (batch) PopulateBatchList(); + return 0; + } + switch (id) { case IDC_ROM_BROWSE: { std::wstring dir = GetEditDir(g_rom_path); @@ -768,7 +949,10 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara if (batch) { auto f = BrowseFolder(hwnd, L"Select input folder", dir.empty() ? nullptr : dir.c_str()); - if (!f.empty()) SetWindowTextW(g_input_path, f.c_str()); + if (!f.empty()) { + SetWindowTextW(g_input_path, f.c_str()); + PopulateBatchList(); + } } else { auto f = BrowseFile(hwnd, L"Select .krec file", L"Krec Files (*.krec)\0*.krec\0All Files\0*.*\0", false, @@ -777,6 +961,18 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara } break; } + case IDC_BATCH_SELECT_ALL: { + int count = ListView_GetItemCount(g_batch_list); + for (int i = 0; i < count; i++) + ListView_SetCheckState(g_batch_list, i, TRUE); + break; + } + case IDC_BATCH_DESELECT_ALL: { + int count = ListView_GetItemCount(g_batch_list); + for (int i = 0; i < count; i++) + ListView_SetCheckState(g_batch_list, i, FALSE); + break; + } case IDC_OUTPUT_BROWSE: { std::wstring dir = GetEditDir(g_output_path); bool batch = (SendMessageW(g_batch_check, BM_GETCHECK, 0, 0) == BST_CHECKED); @@ -994,7 +1190,7 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) { // Calculate window size to fit client area const int CLIENT_W = 620; - const int CLIENT_H = 670; + const int CLIENT_H = 882; RECT rc = { 0, 0, CLIENT_W, CLIENT_H }; AdjustWindowRectEx(&rc, WS_OVERLAPPEDWINDOW & ~(WS_THICKFRAME | WS_MAXIMIZEBOX), FALSE, 0); diff --git a/src/gui_resources.h b/src/gui_resources.h index f67ccbf..fc4d87e 100644 --- a/src/gui_resources.h +++ b/src/gui_resources.h @@ -11,6 +11,9 @@ #define IDC_BATCH_CHECK 105 #define IDC_OUTPUT_PATH 106 #define IDC_OUTPUT_BROWSE 107 +#define IDC_BATCH_LIST 108 +#define IDC_BATCH_SELECT_ALL 109 +#define IDC_BATCH_DESELECT_ALL 110 #define IDC_RESOLUTION 201 #define IDC_QUALITY 202 @@ -21,6 +24,8 @@ #define IDC_ANISO_VALUE 208 #define IDC_ENCODER 209 +#define IDC_REMOVE_MUSIC_CHECK 301 + #define IDC_VERBOSE_CHECK 401 #define IDC_CONVERT_BTN 402 #define IDC_CANCEL_BTN 403