From 3a47eb5cde11a16db148f22fb6e269773e1ac2e2 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 18 Jan 2023 09:52:38 +0100 Subject: [PATCH 01/91] Add USB Debugging through UART --- CMakeLists.txt | 7 +++++++ src/debug.c | 4 ++++ src/main.c | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c839c1..a86135c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,3 +126,10 @@ target_link_libraries(sd2psx set_target_properties(sd2psx PROPERTIES PICO_TARGET_LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/memmap_custom.ld) pico_add_extra_outputs(sd2psx) + +set(DEBUG_USB_UART OFF CACHE BOOL "Activate UART over USB for debugging") + +if(DEBUG_USB_UART) + add_definitions(-DDEBUG_USB_UART) + pico_enable_stdio_usb(sd2psx ENABLED) +endif() diff --git a/src/debug.c b/src/debug.c index 6a260fd..6ff0393 100644 --- a/src/debug.c +++ b/src/debug.c @@ -32,6 +32,10 @@ void __time_critical_func(debug_printf)(const char *format, ...) { vsnprintf(buf, sizeof(buf), format, args); va_end(args); +#if DEBUG_USB_UART + printf("%s", buf); +#endif + for (char *c = buf; *c; ++c) debug_put(*c); } diff --git a/src/main.c b/src/main.c index 750de0c..2494352 100644 --- a/src/main.c +++ b/src/main.c @@ -54,7 +54,12 @@ int main() { input_init(); check_bootloader_reset(); +#if DEBUG_USB_UART + stdio_usb_init(); + sleep_ms(2000); +#else stdio_uart_init_full(UART_PERIPH, UART_BAUD, UART_TX, UART_RX); +#endif printf("prepare...\n"); int mhz = 240; From 8978615b0ce2bc37f7a7b80c2f5d19eee496abd7 Mon Sep 17 00:00:00 2001 From: BBsan Date: Wed, 18 Jan 2023 10:05:57 +0100 Subject: [PATCH 02/91] Add USB Debug UF2 --- .github/workflows/cmake.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index b0bf0cf..a46d53c 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -34,6 +34,19 @@ jobs: - name: Build # Build your program with the given configuration run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + + - name: Configure CMake for Debug USB + # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: cmake -B ${{github.workspace}}/build_usb -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DDEBUG_USB_UART:BOOL=ON + + - name: Build Debug USB + # Build your program with the given configuration + run: cmake --build ${{github.workspace}}/build_usb --config ${{env.BUILD_TYPE}} + + - name: Rename Debug USB uf2 + run: mv build_usb/sd2psx.uf2 build/sd2psx_usb_debug.uf2 + - uses: marvinpinto/action-automatic-releases@v1.2.1 # uses: marvinpinto/action-automatic-releases@919008cf3f741b179569b7a6fb4d8860689ab7f0 @@ -46,3 +59,4 @@ jobs: files: | build/*.uf2 + From 1b37d13899b91674a34e3fc4ad92b8fd20038995 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 18 Jan 2023 18:42:56 +0100 Subject: [PATCH 03/91] put char when writing to uart --- src/debug.c | 4 ---- src/main.c | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/debug.c b/src/debug.c index 6ff0393..6a260fd 100644 --- a/src/debug.c +++ b/src/debug.c @@ -32,10 +32,6 @@ void __time_critical_func(debug_printf)(const char *format, ...) { vsnprintf(buf, sizeof(buf), format, args); va_end(args); -#if DEBUG_USB_UART - printf("%s", buf); -#endif - for (char *c = buf; *c; ++c) debug_put(*c); } diff --git a/src/main.c b/src/main.c index 2494352..27e6a7a 100644 --- a/src/main.c +++ b/src/main.c @@ -44,6 +44,9 @@ static void debug_task(void) { if (ch == '\n') uart_putc_raw(UART_PERIPH, '\r'); uart_putc_raw(UART_PERIPH, ch); + #if DEBUG_USB_UART + putchar(ch); + #endif } else { break; } From 9c3ac6e24062157a0c83038c046ddf6a35d5bf19 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Thu, 19 Jan 2023 16:33:33 +0100 Subject: [PATCH 04/91] wip-ps2 exploit --- CMakeLists.txt | 1 + src/arduino_wrapper/sd.cpp | 4 ++ src/gui.c | 85 +++++++++++++++++++++++++++--- src/main.c | 2 + src/ps2/ps2_exploit.c | 105 +++++++++++++++++++++++++++++++++++++ src/ps2/ps2_exploit.h | 23 ++++++++ src/ps2/ps2_memory_card.c | 3 ++ src/sd.h | 3 +- 8 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 src/ps2/ps2_exploit.c create mode 100644 src/ps2/ps2_exploit.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a86135c..5e67ab4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ add_executable(sd2psx src/ps2/ps2_cardman.c src/ps2/ps2_pio_qspi.c src/ps2/ps2_psram.c + src/ps2/ps2_exploit.c src/wear_leveling/wear_leveling.c src/wear_leveling/wear_leveling_rp2040_flash.c diff --git a/src/arduino_wrapper/sd.cpp b/src/arduino_wrapper/sd.cpp index 2e9396c..643241d 100644 --- a/src/arduino_wrapper/sd.cpp +++ b/src/arduino_wrapper/sd.cpp @@ -105,3 +105,7 @@ extern "C" int sd_mkdir(const char *path) { extern "C" int sd_exists(const char *path) { return sd.exists(path); } + +extern "C" int sd_filesize(int fd) { + return files[fd].fileSize();; +} \ No newline at end of file diff --git a/src/gui.c b/src/gui.c index ddb4075..0535f78 100644 --- a/src/gui.c +++ b/src/gui.c @@ -17,15 +17,16 @@ #include "ps2/ps2_memory_card.h" #include "ps2/ps2_cardman.h" #include "ps2/ps2_dirty.h" +#include "ps2/ps2_exploit.h" #include "ui_theme_mono.h" /* Displays the line at the bottom for long pressing buttons */ -static lv_obj_t *g_navbar, *g_progress_bar, *g_progress_text, *g_activity_frame; +static lv_obj_t *g_navbar, *g_progress_bar, *g_exploit_bar, *g_progress_text, *g_exploit_text, *g_activity_frame; -static lv_obj_t *scr_switch_nag, *scr_card_switch, *scr_main, *scr_menu, *scr_freepsxboot, *menu, *main_page; +static lv_obj_t *scr_switch_nag, *scr_card_switch, *scr_exploit, *scr_main, *scr_menu, *scr_freepsxboot, *menu, *main_page; static lv_style_t style_inv; -static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl, *lbl_civ_err; +static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl, *lbl_civ_err, *lbl_exploit_err; static int have_oled; static int switching_card; @@ -188,6 +189,23 @@ static void reload_card_cb(int progress) { gui_tick(); } +static void load_exploit_cb(int progress) { + static lv_point_t line_points[2] = { {0, DISPLAY_HEIGHT/2}, {0, DISPLAY_HEIGHT/2} }; + static int prev_progress; + + progress += 5; + if (progress/5 == prev_progress/5) + return; + printf("Current progress %d\n", progress); + prev_progress = progress; + line_points[1].x = DISPLAY_WIDTH * progress / 100; + lv_line_set_points(g_exploit_bar, line_points, 2); + + lv_label_set_text(g_exploit_text, ps2_exploit_get_deploy_text()); + + gui_tick(); +} + static void evt_scr_main(lv_event_t *event) { if (event->code == LV_EVENT_KEY) { uint32_t key = lv_indev_get_key(lv_indev_get_act()); @@ -318,6 +336,26 @@ static void evt_do_civ_deploy(lv_event_t *event) { } } +static void evt_do_exploit_deploy(lv_event_t *event) +{ + (void)event; + UI_GOTO_SCREEN(scr_exploit); + + ps2_exploit_set_progress_cb(load_exploit_cb); + + gui_tick(); + + int ret = ps2_exploit_deploy(); + /*if (ret == 0) { + lv_label_set_text(lbl_exploit_err, "Success!\n"); + } else { + lv_label_set_text(lbl_exploit_err, ps2_exploit_error(ret)); + }*/ + ps2_exploit_set_progress_cb(NULL); +// UI_GOTO_SCREEN(scr_main); + +} + static void evt_switch_to_ps1(lv_event_t *event) { (void)event; @@ -446,6 +484,28 @@ static void create_cardswitch_screen(void) { lv_label_set_text(g_progress_text, "Read XXX kB/s"); } +static void create_exploit_screen(void) { + printf("Creating Exploit Screen\n"); + + scr_exploit = ui_scr_create(); + + ui_header_create(scr_exploit, "Writing Exploit"); + + static lv_style_t style_progress; + lv_style_init(&style_progress); + lv_style_set_line_width(&style_progress, 12); + lv_style_set_line_color(&style_progress, lv_palette_main(LV_PALETTE_BLUE)); + + g_exploit_bar = lv_line_create(scr_exploit); + lv_obj_set_width(g_exploit_bar, DISPLAY_WIDTH); + lv_obj_add_style(g_exploit_bar, &style_progress, 0); + + g_exploit_text= lv_label_create(scr_exploit); + lv_obj_set_align(g_exploit_text, LV_ALIGN_TOP_LEFT); + lv_obj_set_pos(g_exploit_text, 0, DISPLAY_HEIGHT-9); + lv_label_set_text(g_exploit_text, "Write XXX kB/s"); +} + static void create_switch_nag_screen(void) { scr_switch_nag = ui_scr_create(); @@ -543,9 +603,16 @@ static void create_menu_screen(void) { { /* exploit install */ lv_obj_t *exploit_install_page = ui_menu_subpage_create(menu, "Install EXPLOIT.bin"); - { - // TODO: handle exploit install - } + /*{ + cont = ui_menu_cont_create(exploit_install_page); + ui_label_create(cont, ""); + cont = ui_menu_cont_create(exploit_install_page); + lbl_exploit_err = ui_label_create(cont, ""); + + cont = ui_menu_cont_create_nav(exploit_install_page); + ui_label_create(cont, "Back"); + lv_obj_add_event_cb(cont, evt_go_back, LV_EVENT_CLICKED, NULL); + }*/ /* deploy submenu */ lv_obj_t *civ_page = ui_menu_subpage_create(menu, "Deploy CIV.bin"); @@ -567,7 +634,9 @@ static void create_menu_screen(void) { cont = ui_menu_cont_create_nav(ps2_page); ui_label_create_grow_scroll(cont, "Install EXPLOIT.bin"); ui_label_create(cont, " >"); - ui_menu_set_load_page_event(menu, cont, exploit_install_page); +// ui_menu_set_load_page_event(menu, cont, exploit_install_page); + lv_obj_add_event_cb(cont, evt_do_exploit_deploy, LV_EVENT_CLICKED, NULL); + cont = ui_menu_cont_create_nav(ps2_page); ui_label_create_grow(cont, "Deploy CIV.bin"); @@ -617,6 +686,7 @@ static void create_ui(void) { create_main_screen(); create_menu_screen(); create_cardswitch_screen(); + create_exploit_screen(); create_switch_nag_screen(); create_freepsxboot_screen(); @@ -692,6 +762,7 @@ void gui_do_ps2_card_switch(void) { } void gui_task(void) { + input_update_display(g_navbar); if (settings_get_mode() == MODE_PS1) { diff --git a/src/main.c b/src/main.c index 27e6a7a..f047724 100644 --- a/src/main.c +++ b/src/main.c @@ -23,6 +23,7 @@ #include "ps2/ps2_dirty.h" #include "ps2/ps2_cardman.h" #include "ps2/ps2_psram.h" +#include "ps2/ps2_exploit.h" /* reboot to bootloader if either button is held on startup to make the device easier to flash when assembled inside case */ @@ -109,6 +110,7 @@ int main() { sd_init(); ps2_cardman_init(); ps2_dirty_init(); + ps2_exploit_init(); gui_init(); multicore_launch_core1(ps2_memory_card_main); diff --git a/src/ps2/ps2_exploit.c b/src/ps2/ps2_exploit.c new file mode 100644 index 0000000..5662d0e --- /dev/null +++ b/src/ps2/ps2_exploit.c @@ -0,0 +1,105 @@ +#include "ps2_exploit.h" + +#include +#include +#include +#include +#include +#include + +#include "flashmap.h" +#include "hardware/flash.h" +#include "hardware/regs/addressmap.h" +#include "hardware/sync.h" +#include "pico/time.h" +#include "pico/types.h" +#include "sd.h" + +#define CARD_SIZE (8 * 1024 * 1024) + +bool card_available = false; +static exploit_cb_t exploit_cb; +static uint64_t exploitprog_start; +static size_t exploitprog_pos; +static int exploitprog_wr; + +void ps2_exploit_init(void) { + if ((*(char *)(FLASH_OFF_PS2EXP + XIP_BASE)) != 0xFF) { + card_available = true; + debug_printf("Using exploit card.\n"); + } +} + +char *ps2_exploit_error(int rc) { + switch (rc) { + case 0: return "No error"; + case PS2_EXPLOIT_DEPLOY_NOFILE: return "exploit.bin not\nfound\n"; + case PS2_EXPLOIT_DEPLOY_OPEN: return "Cannot open\nexploit.bin\n"; + case PS2_EXPLOIT_DEPLOY_READ: return "Cannot read\nexploit.bin\n"; + default: return "Unknown error"; + } +} + +int ps2_exploit_deploy(void) { + uint8_t buf[256] = {0}; + + if (!sd_exists("exploit.bin")) + return PS2_EXPLOIT_DEPLOY_NOFILE; + + int fd = sd_open("exploit.bin", O_RDONLY); + if (fd < 0) + return PS2_EXPLOIT_DEPLOY_OPEN; + + if (sd_filesize(fd) == CARD_SIZE) { + size_t erase_offset = FLASH_OFF_PS2EXP; + uint32_t offset = FLASH_OFF_PS2EXP; + + exploitprog_start = time_us_64(); + + while (erase_offset < FLASH_OFF_PS2EXP + CARD_SIZE) { + uint32_t ints = save_and_disable_interrupts(); + flash_range_erase(erase_offset, 65536); + + printf("Erase %d, %d Bytes\n", erase_offset, 65536); + restore_interrupts(ints); + + erase_offset += 65536; + + exploitprog_pos = (erase_offset - FLASH_OFF_PS2EXP) / 2; + + if (exploit_cb) + exploit_cb(50 * (erase_offset - FLASH_OFF_PS2EXP) / CARD_SIZE); + } + + while (sd_read(fd, buf, sizeof(buf)) == sizeof(buf)) { + uint32_t ints = save_and_disable_interrupts(); + flash_range_program(offset, buf, sizeof(buf)); + + restore_interrupts(ints); + exploitprog_pos = (CARD_SIZE + offset - FLASH_OFF_PS2EXP) / 2; + + if (exploit_cb) + exploit_cb(50 + 50 * (offset - FLASH_OFF_PS2EXP) / CARD_SIZE); + offset += 256; + } + } + + sd_close(fd); + return 0; +} + +void ps2_exploit_set_progress_cb(exploit_cb_t func) +{ + exploit_cb = func; +} + + +char *ps2_exploit_get_deploy_text(void) { + static char progress[32]; + + snprintf(progress, sizeof(progress), "%s %.2f kB/s", "Wr", 1000000.0 * exploitprog_pos / (time_us_64() - exploitprog_start) / 1024); + + printf("%s", progress); + + return progress; +} diff --git a/src/ps2/ps2_exploit.h b/src/ps2/ps2_exploit.h new file mode 100644 index 0000000..2ab4c81 --- /dev/null +++ b/src/ps2/ps2_exploit.h @@ -0,0 +1,23 @@ +#pragma once + +#include + + +typedef void (*exploit_cb_t)(int); + +void ps2_exploit_init(void); +void ps2_exploit_read(void); +char *ps2_exploit_error(int rc); +int ps2_exploit_deploy(void); +void ps2_exploit_set_progress_cb(exploit_cb_t func); +char *ps2_exploit_get_deploy_text(void); + + +enum { + PS2_EXPLOIT_DEPLOY_NOFILE = 1, + PS2_EXPLOIT_DEPLOY_OPEN, + PS2_EXPLOIT_DEPLOY_READ, +}; + + + diff --git a/src/ps2/ps2_memory_card.c b/src/ps2/ps2_memory_card.c index 72ca331..982da52 100644 --- a/src/ps2/ps2_memory_card.c +++ b/src/ps2/ps2_memory_card.c @@ -14,6 +14,7 @@ #include "ps2_psram.h" #include "ps2_pio_qspi.h" +#include #include // #define DEBUG_MC_PROTOCOL @@ -24,6 +25,7 @@ int byte_count; volatile int reset; int ignore; uint8_t flag; +bool bootup_done = false; typedef struct { uint32_t offset; @@ -403,4 +405,5 @@ void ps2_memory_card_enter(void) { {} mc_enter_request = mc_enter_response = 0; memcard_running = 1; + bootup_done = true; } diff --git a/src/sd.h b/src/sd.h index d7fca8d..c18ab89 100644 --- a/src/sd.h +++ b/src/sd.h @@ -9,5 +9,6 @@ void sd_flush(int fd); int sd_read(int fd, void *buf, size_t count); int sd_write(int fd, void *buf, size_t count); int sd_seek(int fd, uint64_t pos); +int sd_filesize(int fd); int sd_mkdir(const char *path); -int sd_exists(const char *path); +int sd_exists(const char *path); \ No newline at end of file From 1fd3cbff292bc61f99601aa51f30aee09385d6fa Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Fri, 20 Jan 2023 08:20:04 +0100 Subject: [PATCH 05/91] Peroperly adding exploit screen --- src/gui.c | 49 ++++++++++++++++++++++++++++++------------- src/main.c | 2 +- src/ps2/ps2_exploit.c | 11 +++++----- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/gui.c b/src/gui.c index 0535f78..3b25204 100644 --- a/src/gui.c +++ b/src/gui.c @@ -26,13 +26,15 @@ static lv_obj_t *g_navbar, *g_progress_bar, *g_exploit_bar, *g_progress_text, *g static lv_obj_t *scr_switch_nag, *scr_card_switch, *scr_exploit, *scr_main, *scr_menu, *scr_freepsxboot, *menu, *main_page; static lv_style_t style_inv; -static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl, *lbl_civ_err, *lbl_exploit_err; +static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl, *lbl_civ_err; static int have_oled; static int switching_card; static uint64_t switching_card_timeout; static int terminated; +static bool installing_exploit; + #define COLOR_FG lv_color_white() #define COLOR_BG lv_color_black() @@ -339,20 +341,8 @@ static void evt_do_civ_deploy(lv_event_t *event) { static void evt_do_exploit_deploy(lv_event_t *event) { (void)event; - UI_GOTO_SCREEN(scr_exploit); - ps2_exploit_set_progress_cb(load_exploit_cb); - - gui_tick(); - - int ret = ps2_exploit_deploy(); - /*if (ret == 0) { - lv_label_set_text(lbl_exploit_err, "Success!\n"); - } else { - lv_label_set_text(lbl_exploit_err, ps2_exploit_error(ret)); - }*/ - ps2_exploit_set_progress_cb(NULL); -// UI_GOTO_SCREEN(scr_main); + installing_exploit = true; } @@ -374,6 +364,25 @@ static void evt_switch_to_ps2(lv_event_t *event) { terminated = 1; } +static void gui_install_exploit() +{ + UI_GOTO_SCREEN(scr_exploit); + + ps2_exploit_set_progress_cb(load_exploit_cb); + + gui_tick(); + + //int ret = + (void)ps2_exploit_deploy(); + /*if (ret == 0) { + lv_label_set_text(lbl_exploit_err, "Success!\n"); + } else { + lv_label_set_text(lbl_exploit_err, ps2_exploit_error(ret)); + }*/ + ps2_exploit_set_progress_cb(NULL); + UI_GOTO_SCREEN(scr_main); +} + static void create_main_screen(void) { lv_obj_t *lbl; @@ -602,7 +611,7 @@ static void create_menu_screen(void) { lv_obj_t *ps2_page = ui_menu_subpage_create(menu, "PS2 Settings"); { /* exploit install */ - lv_obj_t *exploit_install_page = ui_menu_subpage_create(menu, "Install EXPLOIT.bin"); + //lv_obj_t *exploit_install_page = ui_menu_subpage_create(menu, "Install EXPLOIT.bin"); /*{ cont = ui_menu_cont_create(exploit_install_page); ui_label_create(cont, ""); @@ -732,6 +741,8 @@ void gui_init(void) { lv_disp_set_theme(disp, th); create_ui(); + + installing_exploit = false; } void gui_do_ps1_card_switch(void) { @@ -761,6 +772,7 @@ void gui_do_ps2_card_switch(void) { input_flush(); } + void gui_task(void) { input_update_display(g_navbar); @@ -802,6 +814,13 @@ void gui_task(void) { gui_do_ps2_card_switch(); } + if (installing_exploit && !input_is_any_down()) + { + installing_exploit = false; + gui_install_exploit(); + } + + if (ps2_dirty_activity) { input_flush(); lv_obj_clear_flag(g_activity_frame, LV_OBJ_FLAG_HIDDEN); diff --git a/src/main.c b/src/main.c index f047724..626ffef 100644 --- a/src/main.c +++ b/src/main.c @@ -106,11 +106,11 @@ int main() { printf("starting in PS2 mode\n"); keystore_init(); + ps2_exploit_init(); psram_init(); sd_init(); ps2_cardman_init(); ps2_dirty_init(); - ps2_exploit_init(); gui_init(); multicore_launch_core1(ps2_memory_card_main); diff --git a/src/ps2/ps2_exploit.c b/src/ps2/ps2_exploit.c index 5662d0e..0c6f587 100644 --- a/src/ps2/ps2_exploit.c +++ b/src/ps2/ps2_exploit.c @@ -26,7 +26,7 @@ static int exploitprog_wr; void ps2_exploit_init(void) { if ((*(char *)(FLASH_OFF_PS2EXP + XIP_BASE)) != 0xFF) { card_available = true; - debug_printf("Using exploit card.\n"); + printf("Using exploit card.\n"); } } @@ -55,12 +55,12 @@ int ps2_exploit_deploy(void) { uint32_t offset = FLASH_OFF_PS2EXP; exploitprog_start = time_us_64(); + exploitprog_wr = 0; while (erase_offset < FLASH_OFF_PS2EXP + CARD_SIZE) { uint32_t ints = save_and_disable_interrupts(); flash_range_erase(erase_offset, 65536); - printf("Erase %d, %d Bytes\n", erase_offset, 65536); restore_interrupts(ints); erase_offset += 65536; @@ -70,6 +70,7 @@ int ps2_exploit_deploy(void) { if (exploit_cb) exploit_cb(50 * (erase_offset - FLASH_OFF_PS2EXP) / CARD_SIZE); } + exploitprog_wr = 1; while (sd_read(fd, buf, sizeof(buf)) == sizeof(buf)) { uint32_t ints = save_and_disable_interrupts(); @@ -82,6 +83,8 @@ int ps2_exploit_deploy(void) { exploit_cb(50 + 50 * (offset - FLASH_OFF_PS2EXP) / CARD_SIZE); offset += 256; } + exploitprog_wr = -1; + debug_printf("Wrote new exploit image, took %d s\n", (uint32_t)((time_us_64() - exploitprog_start)/1000000)); } sd_close(fd); @@ -97,9 +100,7 @@ void ps2_exploit_set_progress_cb(exploit_cb_t func) char *ps2_exploit_get_deploy_text(void) { static char progress[32]; - snprintf(progress, sizeof(progress), "%s %.2f kB/s", "Wr", 1000000.0 * exploitprog_pos / (time_us_64() - exploitprog_start) / 1024); - - printf("%s", progress); + snprintf(progress, sizeof(progress), "%s %.2f kB/s", exploitprog_wr == 0 ? "Er" : "Wr", 1000000.0 * exploitprog_pos / (time_us_64() - exploitprog_start) / 1024); return progress; } From 2e90fdfa28106f160ba0840c0b8ea8afe83606c2 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Fri, 20 Jan 2023 10:31:04 +0100 Subject: [PATCH 06/91] wip - read from flash --- src/main.c | 4 +++- src/ps2/ps2_memory_card.c | 43 +++++++++++++++++++++++++++++++++--- src/ps2/ps2_memory_card.h | 3 +++ src/ps2/ps2_memory_card.in.c | 8 +++---- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/main.c b/src/main.c index 626ffef..5ec2702 100644 --- a/src/main.c +++ b/src/main.c @@ -115,9 +115,11 @@ int main() { multicore_launch_core1(ps2_memory_card_main); + ps2_memory_card_enter_flash(); + printf("Starting memory card... "); uint64_t start = time_us_64(); - gui_do_ps2_card_switch(); + //gui_do_ps2_card_switch(); uint64_t end = time_us_64(); printf("DONE! (%d us)\n", (int)(end - start)); diff --git a/src/ps2/ps2_memory_card.c b/src/ps2/ps2_memory_card.c index 982da52..bd356b2 100644 --- a/src/ps2/ps2_memory_card.c +++ b/src/ps2/ps2_memory_card.c @@ -1,4 +1,5 @@ #include "hardware/gpio.h" +#include "hardware/regs/addressmap.h" #include "hardware/timer.h" #include "hardware/flash.h" #include "hardware/dma.h" @@ -6,6 +7,7 @@ #include "config.h" #include "ps2_mc_spi.pio.h" +#include "flashmap.h" #include "debug.h" #include "keystore.h" #include "des.h" @@ -17,6 +19,7 @@ #include #include + // #define DEBUG_MC_PROTOCOL uint64_t us_startup; @@ -25,7 +28,7 @@ int byte_count; volatile int reset; int ignore; uint8_t flag; -bool bootup_done = false; +bool flash_mode = false; typedef struct { uint32_t offset; @@ -50,6 +53,29 @@ uint32_t readptr, writeptr; static volatile int mc_exit_request, mc_exit_response, mc_enter_request, mc_enter_response; static uint8_t hostkey[9]; +static inline void __time_critical_func(read_mc)(uint32_t addr, void *buf, size_t sz) { + debug_printf("Read at address %d size %d - ", addr, sz); + if (flash_mode) { + debug_printf("Flash\n"); + memcpy(buf, (void*)(XIP_BASE + FLASH_OFF_PS2EXP + addr), sz); + ps2_dirty_unlock(); + } else { + debug_printf("PSRAM\n"); + psram_read_dma(addr, buf, sz); + } +} + +static inline void __time_critical_func(write_mc)(uint32_t addr, void *buf, size_t sz) { + debug_printf("Wrote at address %d size %d\n", addr, sz); + + if (!flash_mode) { + psram_write(addr, buf, sz); + } else { + ps2_dirty_unlock(); + debug_printf("Ignore writing to exploit.\n"); + } +} + static inline void __time_critical_func(RAM_pio_sm_drain_tx_fifo)(PIO pio, uint sm) { uint instr = (pio->sm[sm].shiftctrl & PIO_SM0_SHIFTCTRL_AUTOPULL_BITS) ? pio_encode_out(pio_null, 32) : pio_encode_pull(false, false); @@ -397,7 +423,9 @@ void ps2_memory_card_exit(void) { } void ps2_memory_card_enter(void) { - if (memcard_running) + if (flash_mode) { + ps2_memory_card_exit(); + } else if (memcard_running) return; mc_enter_request = 1; @@ -405,5 +433,14 @@ void ps2_memory_card_enter(void) { {} mc_enter_request = mc_enter_response = 0; memcard_running = 1; - bootup_done = true; + flash_mode = false; +} + +void ps2_memory_card_enter_flash(void) { + mc_enter_request = 1; + while (!mc_enter_response) + {} + mc_enter_request = mc_enter_response = 0; + memcard_running = 1; + flash_mode = true; } diff --git a/src/ps2/ps2_memory_card.h b/src/ps2/ps2_memory_card.h index d3aa9b5..688f35e 100644 --- a/src/ps2/ps2_memory_card.h +++ b/src/ps2/ps2_memory_card.h @@ -1,5 +1,8 @@ #pragma once +#include + void ps2_memory_card_main(void); void ps2_memory_card_enter(void); +void ps2_memory_card_enter_flash(void); void ps2_memory_card_exit(void); diff --git a/src/ps2/ps2_memory_card.in.c b/src/ps2/ps2_memory_card.in.c index 9a5ef82..ce6c118 100644 --- a/src/ps2/ps2_memory_card.in.c +++ b/src/ps2/ps2_memory_card.in.c @@ -71,7 +71,7 @@ if (ch == 0x11) { ps2_dirty_lockout_renew(); /* the spinlock will be unlocked by the DMA irq once all data is tx'd */ ps2_dirty_lock(); - psram_read_dma(read_sector * 512, &readtmp, 512+4); + read_mc(read_sector * 512, &readtmp, 512+4); // dma_channel_wait_for_finish_blocking(0); // dma_channel_wait_for_finish_blocking(1); } @@ -158,7 +158,7 @@ if (ch == 0x11) { ps2_dirty_lockout_renew(); /* the spinlock will be unlocked by the DMA irq once all data is tx'd */ ps2_dirty_lock(); - psram_read_dma(read_sector * 512, &readtmp, 512+4); + read_mc(read_sector * 512, &readtmp, 512+4); // TODO: remove this if safe // must make sure the dma completes for first byte before we start reading below dma_channel_wait_for_finish_blocking(PIO_SPI_DMA_RX_CHAN); @@ -213,7 +213,7 @@ if (ch == 0x11) { if (write_sector * 512 + 512 <= CARD_SIZE) { ps2_dirty_lockout_renew(); ps2_dirty_lock(); - psram_write(write_sector * 512, writetmp, 512); + write_mc(write_sector * 512, writetmp, 512); ps2_dirty_mark(write_sector); ps2_dirty_unlock(); #ifdef DEBUG_MC_PROTOCOL @@ -239,7 +239,7 @@ if (ch == 0x11) { ps2_dirty_lockout_renew(); ps2_dirty_lock(); for (int i = 0; i < ERASE_SECTORS; ++i) { - psram_write((erase_sector + i) * 512, readtmp.buf, 512); + write_mc((erase_sector + i) * 512, readtmp.buf, 512); ps2_dirty_mark(erase_sector + i); } ps2_dirty_unlock(); From fac3041becc7b3f4ee20ac18a5aa1af5240e51c3 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Fri, 20 Jan 2023 11:25:38 +0100 Subject: [PATCH 07/91] Fix wrong pointer to buffer --- src/main.c | 2 +- src/ps2/ps2_memory_card.c | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main.c b/src/main.c index 5ec2702..543c97e 100644 --- a/src/main.c +++ b/src/main.c @@ -119,7 +119,7 @@ int main() { printf("Starting memory card... "); uint64_t start = time_us_64(); - //gui_do_ps2_card_switch(); + gui_do_ps2_card_switch(); uint64_t end = time_us_64(); printf("DONE! (%d us)\n", (int)(end - start)); diff --git a/src/ps2/ps2_memory_card.c b/src/ps2/ps2_memory_card.c index bd356b2..2e64d8e 100644 --- a/src/ps2/ps2_memory_card.c +++ b/src/ps2/ps2_memory_card.c @@ -54,19 +54,17 @@ static volatile int mc_exit_request, mc_exit_response, mc_enter_request, mc_ente static uint8_t hostkey[9]; static inline void __time_critical_func(read_mc)(uint32_t addr, void *buf, size_t sz) { - debug_printf("Read at address %d size %d - ", addr, sz); if (flash_mode) { - debug_printf("Flash\n"); - memcpy(buf, (void*)(XIP_BASE + FLASH_OFF_PS2EXP + addr), sz); + // Skip first 4 Bytes in Buffer, as they are the prefix + memcpy((buf + 4), (void*)(addr + XIP_BASE + FLASH_OFF_PS2EXP), sz); ps2_dirty_unlock(); } else { - debug_printf("PSRAM\n"); psram_read_dma(addr, buf, sz); } } static inline void __time_critical_func(write_mc)(uint32_t addr, void *buf, size_t sz) { - debug_printf("Wrote at address %d size %d\n", addr, sz); + debug_printf("Write at address %d size %d\n", addr, sz); if (!flash_mode) { psram_write(addr, buf, sz); From 0f227de7a25c61b60cb24b60aeb792dfb8c129d9 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 23 Jan 2023 11:19:01 +0100 Subject: [PATCH 08/91] Add exploit "BOOT" card and activate Boot --- src/gui.c | 23 +++++++++++++++++++---- src/main.c | 3 ++- src/ps2/ps2_cardman.c | 35 +++++++++++++++++++++++++---------- src/ps2/ps2_exploit.c | 4 ++++ src/ps2/ps2_exploit.h | 2 ++ src/settings.c | 15 +++++++++++++++ src/settings.h | 4 ++++ 7 files changed, 71 insertions(+), 15 deletions(-) diff --git a/src/gui.c b/src/gui.c index 3b25204..2cd0eec 100644 --- a/src/gui.c +++ b/src/gui.c @@ -26,7 +26,7 @@ static lv_obj_t *g_navbar, *g_progress_bar, *g_exploit_bar, *g_progress_text, *g static lv_obj_t *scr_switch_nag, *scr_card_switch, *scr_exploit, *scr_main, *scr_menu, *scr_freepsxboot, *menu, *main_page; static lv_style_t style_inv; -static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl, *lbl_civ_err; +static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl, *lbl_civ_err, *lbl_autoboot; static int have_oled; static int switching_card; @@ -327,6 +327,15 @@ static void evt_go_back(lv_event_t *event) { lv_event_stop_bubbling(event); } +static void evt_ps2_autoboot(lv_event_t *event) { + bool current = settings_get_ps2_autoboot(); + if (ps2_exploit_is_available()) { + settings_set_ps2_autoboot(!current); + lv_label_set_text(lbl_autoboot, !current ? "Yes" : "No"); + } + lv_event_stop_bubbling(event); +} + static void evt_do_civ_deploy(lv_event_t *event) { (void)event; @@ -637,8 +646,10 @@ static void create_menu_screen(void) { } cont = ui_menu_cont_create_nav(ps2_page); - ui_label_create_grow_scroll(cont, "Autoboot exploit"); - ui_label_create(cont, " No"); + ui_label_create_grow_scroll(cont, "Autoboot"); + lbl_autoboot = ui_label_create(cont, settings_get_ps2_autoboot() ? " Yes" : " No"); + lv_obj_add_event_cb(cont, evt_ps2_autoboot, LV_EVENT_CLICKED, NULL); + cont = ui_menu_cont_create_nav(ps2_page); ui_label_create_grow_scroll(cont, "Install EXPLOIT.bin"); @@ -803,7 +814,11 @@ void gui_task(void) { if (displayed_card_idx != ps2_cardman_get_idx() || displayed_card_channel != ps2_cardman_get_channel()) { displayed_card_idx = ps2_cardman_get_idx(); displayed_card_channel = ps2_cardman_get_channel(); - snprintf(card_idx_s, sizeof(card_idx_s), "%d", displayed_card_idx); + if (displayed_card_idx == 0) { + snprintf(card_idx_s, sizeof(card_idx_s), "BOOT"); + } else { + snprintf(card_idx_s, sizeof(card_idx_s), "%d", displayed_card_idx); + } snprintf(card_channel_s, sizeof(card_channel_s), "%d", displayed_card_channel); lv_label_set_text(scr_main_idx_lbl, card_idx_s); lv_label_set_text(scr_main_channel_lbl, card_channel_s); diff --git a/src/main.c b/src/main.c index 543c97e..38b9b18 100644 --- a/src/main.c +++ b/src/main.c @@ -115,7 +115,8 @@ int main() { multicore_launch_core1(ps2_memory_card_main); - ps2_memory_card_enter_flash(); + if (settings_get_ps2_autoboot() && ps2_exploit_is_available()) + ps2_memory_card_enter_flash(); printf("Starting memory card... "); uint64_t start = time_us_64(); diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index e46db8e..cbc6059 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -1,5 +1,6 @@ #include "ps2_cardman.h" +#include #include #include @@ -17,6 +18,7 @@ static uint8_t flushbuf[BLOCK_SIZE]; static int fd = -1; #define IDX_MIN 1 +#define IDX_BOOT 0 #define CHAN_MIN 1 #define CHAN_MAX 8 @@ -28,12 +30,17 @@ static size_t cardprog_pos; static int cardprog_wr; void ps2_cardman_init(void) { - card_idx = settings_get_ps2_card(); - if (card_idx < IDX_MIN) - card_idx = IDX_MIN; - card_chan = settings_get_ps2_channel(); - if (card_chan < CHAN_MIN || card_chan > CHAN_MAX) + if (settings_get_ps2_autoboot()) { + card_idx = IDX_BOOT; card_chan = CHAN_MIN; + } else { + card_idx = settings_get_ps2_card(); + if (card_idx < IDX_MIN) + card_idx = IDX_MIN; + card_chan = settings_get_ps2_channel(); + if (card_chan < CHAN_MIN || card_chan > CHAN_MAX) + card_chan = CHAN_MIN; + } } int ps2_cardman_write_sector(int sector, void *buf512) { @@ -56,7 +63,10 @@ void ps2_cardman_flush(void) { static void ensuredirs(void) { char cardpath[32]; - snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/Card%d", card_idx); + if (settings_get_ps2_autoboot() && ps2_exploit_is_available()) + snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/BOOT"); + else + snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/Card%d", card_idx); sd_mkdir("MemoryCards"); sd_mkdir("MemoryCards/PS2"); @@ -206,13 +216,18 @@ void ps2_cardman_open(void) { char path[64]; ensuredirs(); - snprintf(path, sizeof(path), "MemoryCards/PS2/Card%d/Card%d-%d.mcd", card_idx, card_idx, card_chan); + if (settings_get_ps2_autoboot() && ps2_exploit_is_available()) + snprintf(path, sizeof(path), "MemoryCards/PS2/BOOT/Card-%d.mcd", card_chan); + else + { + snprintf(path, sizeof(path), "MemoryCards/PS2/Card%d/Card%d-%d.mcd", card_idx, card_idx, card_chan); + /* this is ok to do on every boot because it wouldn't update if the value is the same as currently stored */ + settings_set_ps2_card(card_idx); + settings_set_ps2_channel(card_chan); + } printf("Switching to card path = %s\n", path); - /* this is ok to do on every boot because it wouldn't update if the value is the same as currently stored */ - settings_set_ps2_card(card_idx); - settings_set_ps2_channel(card_chan); if (!sd_exists(path)) { cardprog_wr = 1; diff --git a/src/ps2/ps2_exploit.c b/src/ps2/ps2_exploit.c index 0c6f587..4cb3e03 100644 --- a/src/ps2/ps2_exploit.c +++ b/src/ps2/ps2_exploit.c @@ -104,3 +104,7 @@ char *ps2_exploit_get_deploy_text(void) { return progress; } + +bool ps2_exploit_is_available(void) { + return (*(char *)(FLASH_OFF_PS2EXP + XIP_BASE)) != 0xFF; +} diff --git a/src/ps2/ps2_exploit.h b/src/ps2/ps2_exploit.h index 2ab4c81..6e14224 100644 --- a/src/ps2/ps2_exploit.h +++ b/src/ps2/ps2_exploit.h @@ -1,6 +1,7 @@ #pragma once #include +#include typedef void (*exploit_cb_t)(int); @@ -11,6 +12,7 @@ char *ps2_exploit_error(int rc); int ps2_exploit_deploy(void); void ps2_exploit_set_progress_cb(exploit_cb_t func); char *ps2_exploit_get_deploy_text(void); +bool ps2_exploit_is_available(void); enum { diff --git a/src/settings.c b/src/settings.c index 85dfa2f..6705feb 100644 --- a/src/settings.c +++ b/src/settings.c @@ -24,6 +24,7 @@ typedef struct { } settings_t; #define SETTINGS_VERSION_MAGIC (0xABCD0002) +#define SETTINGS_FLAGS_AUTOBOOT (0b1) _Static_assert(sizeof(settings_t) == 16, "unexpected padding in the settings structure"); @@ -122,3 +123,17 @@ void settings_set_mode(int mode) { SETTINGS_UPDATE_FIELD(sys_flags); } } + +bool settings_get_ps2_autoboot(void) { + return (settings.ps2_flags & SETTINGS_FLAGS_AUTOBOOT); +} + +void settings_set_ps2_autoboot(bool autoboot) { + if (((settings.ps2_flags & SETTINGS_FLAGS_AUTOBOOT) != 0) != autoboot) { + if (autoboot) + settings.ps2_flags |= SETTINGS_FLAGS_AUTOBOOT; + else + settings.ps2_flags &= ~SETTINGS_FLAGS_AUTOBOOT; + SETTINGS_UPDATE_FIELD(ps2_flags); + } +} \ No newline at end of file diff --git a/src/settings.h b/src/settings.h index 46d436e..c396b8b 100644 --- a/src/settings.h +++ b/src/settings.h @@ -1,5 +1,7 @@ #pragma once +#include + void settings_init(void); int settings_get_ps1_card(void); @@ -19,3 +21,5 @@ enum { int settings_get_mode(void); void settings_set_mode(int mode); +bool settings_get_ps2_autoboot(void); +void settings_set_ps2_autoboot(bool autoboot); \ No newline at end of file From 8453576ba8fe155582b3df44a419e0f137bfc385 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 18 Jan 2023 09:52:38 +0100 Subject: [PATCH 09/91] Add USB Debugging through UART --- src/debug.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/debug.c b/src/debug.c index 6a260fd..6ff0393 100644 --- a/src/debug.c +++ b/src/debug.c @@ -32,6 +32,10 @@ void __time_critical_func(debug_printf)(const char *format, ...) { vsnprintf(buf, sizeof(buf), format, args); va_end(args); +#if DEBUG_USB_UART + printf("%s", buf); +#endif + for (char *c = buf; *c; ++c) debug_put(*c); } From 18352b9d2cbcf55cc7dfcd9e2c848414921d6e00 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 18 Jan 2023 18:42:56 +0100 Subject: [PATCH 10/91] put char when writing to uart --- src/debug.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/debug.c b/src/debug.c index 6ff0393..6a260fd 100644 --- a/src/debug.c +++ b/src/debug.c @@ -32,10 +32,6 @@ void __time_critical_func(debug_printf)(const char *format, ...) { vsnprintf(buf, sizeof(buf), format, args); va_end(args); -#if DEBUG_USB_UART - printf("%s", buf); -#endif - for (char *c = buf; *c; ++c) debug_put(*c); } From 686a4d6c01938c620a7f38cfdda6bb6873004677 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Thu, 19 Jan 2023 16:33:33 +0100 Subject: [PATCH 11/91] wip-ps2 exploit --- CMakeLists.txt | 1 + src/arduino_wrapper/sd.cpp | 4 ++ src/gui.c | 85 +++++++++++++++++++++++++++--- src/main.c | 2 + src/ps2/ps2_exploit.c | 105 +++++++++++++++++++++++++++++++++++++ src/ps2/ps2_exploit.h | 23 ++++++++ src/ps2/ps2_memory_card.c | 3 ++ src/sd.h | 3 +- 8 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 src/ps2/ps2_exploit.c create mode 100644 src/ps2/ps2_exploit.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b97a0c6..c97ee5b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ add_executable(sd2psx src/ps2/ps2_cardman.c src/ps2/ps2_pio_qspi.c src/ps2/ps2_psram.c + src/ps2/ps2_exploit.c src/wear_leveling/wear_leveling.c src/wear_leveling/wear_leveling_rp2040_flash.c diff --git a/src/arduino_wrapper/sd.cpp b/src/arduino_wrapper/sd.cpp index 2e9396c..643241d 100644 --- a/src/arduino_wrapper/sd.cpp +++ b/src/arduino_wrapper/sd.cpp @@ -105,3 +105,7 @@ extern "C" int sd_mkdir(const char *path) { extern "C" int sd_exists(const char *path) { return sd.exists(path); } + +extern "C" int sd_filesize(int fd) { + return files[fd].fileSize();; +} \ No newline at end of file diff --git a/src/gui.c b/src/gui.c index 7e36c58..cd2e9c7 100644 --- a/src/gui.c +++ b/src/gui.c @@ -18,15 +18,16 @@ #include "ps2/ps2_memory_card.h" #include "ps2/ps2_cardman.h" #include "ps2/ps2_dirty.h" +#include "ps2/ps2_exploit.h" #include "ui_theme_mono.h" /* Displays the line at the bottom for long pressing buttons */ -static lv_obj_t *g_navbar, *g_progress_bar, *g_progress_text, *g_activity_frame; +static lv_obj_t *g_navbar, *g_progress_bar, *g_exploit_bar, *g_progress_text, *g_exploit_text, *g_activity_frame; -static lv_obj_t *scr_switch_nag, *scr_card_switch, *scr_main, *scr_menu, *scr_freepsxboot, *menu, *main_page; +static lv_obj_t *scr_switch_nag, *scr_card_switch, *scr_exploit, *scr_main, *scr_menu, *scr_freepsxboot, *menu, *main_page; static lv_style_t style_inv; -static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl, *src_main_title_lbl, *lbl_civ_err; +static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl, *src_main_title_lbl, *lbl_civ_err, *lbl_exploit_err; static int have_oled; static int switching_card; @@ -190,6 +191,23 @@ static void reload_card_cb(int progress) { gui_tick(); } +static void load_exploit_cb(int progress) { + static lv_point_t line_points[2] = { {0, DISPLAY_HEIGHT/2}, {0, DISPLAY_HEIGHT/2} }; + static int prev_progress; + + progress += 5; + if (progress/5 == prev_progress/5) + return; + printf("Current progress %d\n", progress); + prev_progress = progress; + line_points[1].x = DISPLAY_WIDTH * progress / 100; + lv_line_set_points(g_exploit_bar, line_points, 2); + + lv_label_set_text(g_exploit_text, ps2_exploit_get_deploy_text()); + + gui_tick(); +} + static void evt_scr_main(lv_event_t *event) { if (event->code == LV_EVENT_KEY) { uint32_t key = lv_indev_get_key(lv_indev_get_act()); @@ -320,6 +338,26 @@ static void evt_do_civ_deploy(lv_event_t *event) { } } +static void evt_do_exploit_deploy(lv_event_t *event) +{ + (void)event; + UI_GOTO_SCREEN(scr_exploit); + + ps2_exploit_set_progress_cb(load_exploit_cb); + + gui_tick(); + + int ret = ps2_exploit_deploy(); + /*if (ret == 0) { + lv_label_set_text(lbl_exploit_err, "Success!\n"); + } else { + lv_label_set_text(lbl_exploit_err, ps2_exploit_error(ret)); + }*/ + ps2_exploit_set_progress_cb(NULL); +// UI_GOTO_SCREEN(scr_main); + +} + static void evt_switch_to_ps1(lv_event_t *event) { (void)event; @@ -448,6 +486,28 @@ static void create_cardswitch_screen(void) { lv_label_set_text(g_progress_text, "Read XXX kB/s"); } +static void create_exploit_screen(void) { + printf("Creating Exploit Screen\n"); + + scr_exploit = ui_scr_create(); + + ui_header_create(scr_exploit, "Writing Exploit"); + + static lv_style_t style_progress; + lv_style_init(&style_progress); + lv_style_set_line_width(&style_progress, 12); + lv_style_set_line_color(&style_progress, lv_palette_main(LV_PALETTE_BLUE)); + + g_exploit_bar = lv_line_create(scr_exploit); + lv_obj_set_width(g_exploit_bar, DISPLAY_WIDTH); + lv_obj_add_style(g_exploit_bar, &style_progress, 0); + + g_exploit_text= lv_label_create(scr_exploit); + lv_obj_set_align(g_exploit_text, LV_ALIGN_TOP_LEFT); + lv_obj_set_pos(g_exploit_text, 0, DISPLAY_HEIGHT-9); + lv_label_set_text(g_exploit_text, "Write XXX kB/s"); +} + static void create_switch_nag_screen(void) { scr_switch_nag = ui_scr_create(); @@ -545,9 +605,16 @@ static void create_menu_screen(void) { { /* exploit install */ lv_obj_t *exploit_install_page = ui_menu_subpage_create(menu, "Install EXPLOIT.bin"); - { - // TODO: handle exploit install - } + /*{ + cont = ui_menu_cont_create(exploit_install_page); + ui_label_create(cont, ""); + cont = ui_menu_cont_create(exploit_install_page); + lbl_exploit_err = ui_label_create(cont, ""); + + cont = ui_menu_cont_create_nav(exploit_install_page); + ui_label_create(cont, "Back"); + lv_obj_add_event_cb(cont, evt_go_back, LV_EVENT_CLICKED, NULL); + }*/ /* deploy submenu */ lv_obj_t *civ_page = ui_menu_subpage_create(menu, "Deploy CIV.bin"); @@ -569,7 +636,9 @@ static void create_menu_screen(void) { cont = ui_menu_cont_create_nav(ps2_page); ui_label_create_grow_scroll(cont, "Install EXPLOIT.bin"); ui_label_create(cont, " >"); - ui_menu_set_load_page_event(menu, cont, exploit_install_page); +// ui_menu_set_load_page_event(menu, cont, exploit_install_page); + lv_obj_add_event_cb(cont, evt_do_exploit_deploy, LV_EVENT_CLICKED, NULL); + cont = ui_menu_cont_create_nav(ps2_page); ui_label_create_grow(cont, "Deploy CIV.bin"); @@ -619,6 +688,7 @@ static void create_ui(void) { create_main_screen(); create_menu_screen(); create_cardswitch_screen(); + create_exploit_screen(); create_switch_nag_screen(); create_freepsxboot_screen(); @@ -700,6 +770,7 @@ void gui_do_ps2_card_switch(void) { } void gui_task(void) { + input_update_display(g_navbar); if (settings_get_mode() == MODE_PS1) { diff --git a/src/main.c b/src/main.c index dbca379..cc21626 100644 --- a/src/main.c +++ b/src/main.c @@ -25,6 +25,7 @@ #include "ps2/ps2_dirty.h" #include "ps2/ps2_cardman.h" #include "ps2/ps2_psram.h" +#include "ps2/ps2_exploit.h" /* reboot to bootloader if either button is held on startup to make the device easier to flash when assembled inside case */ @@ -112,6 +113,7 @@ int main() { sd_init(); ps2_cardman_init(); ps2_dirty_init(); + ps2_exploit_init(); gui_init(); multicore_launch_core1(ps2_memory_card_main); diff --git a/src/ps2/ps2_exploit.c b/src/ps2/ps2_exploit.c new file mode 100644 index 0000000..5662d0e --- /dev/null +++ b/src/ps2/ps2_exploit.c @@ -0,0 +1,105 @@ +#include "ps2_exploit.h" + +#include +#include +#include +#include +#include +#include + +#include "flashmap.h" +#include "hardware/flash.h" +#include "hardware/regs/addressmap.h" +#include "hardware/sync.h" +#include "pico/time.h" +#include "pico/types.h" +#include "sd.h" + +#define CARD_SIZE (8 * 1024 * 1024) + +bool card_available = false; +static exploit_cb_t exploit_cb; +static uint64_t exploitprog_start; +static size_t exploitprog_pos; +static int exploitprog_wr; + +void ps2_exploit_init(void) { + if ((*(char *)(FLASH_OFF_PS2EXP + XIP_BASE)) != 0xFF) { + card_available = true; + debug_printf("Using exploit card.\n"); + } +} + +char *ps2_exploit_error(int rc) { + switch (rc) { + case 0: return "No error"; + case PS2_EXPLOIT_DEPLOY_NOFILE: return "exploit.bin not\nfound\n"; + case PS2_EXPLOIT_DEPLOY_OPEN: return "Cannot open\nexploit.bin\n"; + case PS2_EXPLOIT_DEPLOY_READ: return "Cannot read\nexploit.bin\n"; + default: return "Unknown error"; + } +} + +int ps2_exploit_deploy(void) { + uint8_t buf[256] = {0}; + + if (!sd_exists("exploit.bin")) + return PS2_EXPLOIT_DEPLOY_NOFILE; + + int fd = sd_open("exploit.bin", O_RDONLY); + if (fd < 0) + return PS2_EXPLOIT_DEPLOY_OPEN; + + if (sd_filesize(fd) == CARD_SIZE) { + size_t erase_offset = FLASH_OFF_PS2EXP; + uint32_t offset = FLASH_OFF_PS2EXP; + + exploitprog_start = time_us_64(); + + while (erase_offset < FLASH_OFF_PS2EXP + CARD_SIZE) { + uint32_t ints = save_and_disable_interrupts(); + flash_range_erase(erase_offset, 65536); + + printf("Erase %d, %d Bytes\n", erase_offset, 65536); + restore_interrupts(ints); + + erase_offset += 65536; + + exploitprog_pos = (erase_offset - FLASH_OFF_PS2EXP) / 2; + + if (exploit_cb) + exploit_cb(50 * (erase_offset - FLASH_OFF_PS2EXP) / CARD_SIZE); + } + + while (sd_read(fd, buf, sizeof(buf)) == sizeof(buf)) { + uint32_t ints = save_and_disable_interrupts(); + flash_range_program(offset, buf, sizeof(buf)); + + restore_interrupts(ints); + exploitprog_pos = (CARD_SIZE + offset - FLASH_OFF_PS2EXP) / 2; + + if (exploit_cb) + exploit_cb(50 + 50 * (offset - FLASH_OFF_PS2EXP) / CARD_SIZE); + offset += 256; + } + } + + sd_close(fd); + return 0; +} + +void ps2_exploit_set_progress_cb(exploit_cb_t func) +{ + exploit_cb = func; +} + + +char *ps2_exploit_get_deploy_text(void) { + static char progress[32]; + + snprintf(progress, sizeof(progress), "%s %.2f kB/s", "Wr", 1000000.0 * exploitprog_pos / (time_us_64() - exploitprog_start) / 1024); + + printf("%s", progress); + + return progress; +} diff --git a/src/ps2/ps2_exploit.h b/src/ps2/ps2_exploit.h new file mode 100644 index 0000000..2ab4c81 --- /dev/null +++ b/src/ps2/ps2_exploit.h @@ -0,0 +1,23 @@ +#pragma once + +#include + + +typedef void (*exploit_cb_t)(int); + +void ps2_exploit_init(void); +void ps2_exploit_read(void); +char *ps2_exploit_error(int rc); +int ps2_exploit_deploy(void); +void ps2_exploit_set_progress_cb(exploit_cb_t func); +char *ps2_exploit_get_deploy_text(void); + + +enum { + PS2_EXPLOIT_DEPLOY_NOFILE = 1, + PS2_EXPLOIT_DEPLOY_OPEN, + PS2_EXPLOIT_DEPLOY_READ, +}; + + + diff --git a/src/ps2/ps2_memory_card.c b/src/ps2/ps2_memory_card.c index 72ca331..982da52 100644 --- a/src/ps2/ps2_memory_card.c +++ b/src/ps2/ps2_memory_card.c @@ -14,6 +14,7 @@ #include "ps2_psram.h" #include "ps2_pio_qspi.h" +#include #include // #define DEBUG_MC_PROTOCOL @@ -24,6 +25,7 @@ int byte_count; volatile int reset; int ignore; uint8_t flag; +bool bootup_done = false; typedef struct { uint32_t offset; @@ -403,4 +405,5 @@ void ps2_memory_card_enter(void) { {} mc_enter_request = mc_enter_response = 0; memcard_running = 1; + bootup_done = true; } diff --git a/src/sd.h b/src/sd.h index d7fca8d..c18ab89 100644 --- a/src/sd.h +++ b/src/sd.h @@ -9,5 +9,6 @@ void sd_flush(int fd); int sd_read(int fd, void *buf, size_t count); int sd_write(int fd, void *buf, size_t count); int sd_seek(int fd, uint64_t pos); +int sd_filesize(int fd); int sd_mkdir(const char *path); -int sd_exists(const char *path); +int sd_exists(const char *path); \ No newline at end of file From 0cadad11204e395857d0011abb93d7d67e7f5b82 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Fri, 20 Jan 2023 08:20:04 +0100 Subject: [PATCH 12/91] Peroperly adding exploit screen --- src/gui.c | 52 ++++++++++++++++++++++++++----------------- src/main.c | 2 +- src/ps2/ps2_exploit.c | 11 ++++----- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/gui.c b/src/gui.c index cd2e9c7..f310a51 100644 --- a/src/gui.c +++ b/src/gui.c @@ -27,13 +27,14 @@ static lv_obj_t *g_navbar, *g_progress_bar, *g_exploit_bar, *g_progress_text, *g static lv_obj_t *scr_switch_nag, *scr_card_switch, *scr_exploit, *scr_main, *scr_menu, *scr_freepsxboot, *menu, *main_page; static lv_style_t style_inv; -static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl, *src_main_title_lbl, *lbl_civ_err, *lbl_exploit_err; +static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl,*src_main_title_lbl, *lbl_civ_err, *lbl_exploit_err; static int have_oled; static int switching_card; static uint64_t switching_card_timeout; static int terminated; static bool refresh_gui; +static bool installing_exploit; #define COLOR_FG lv_color_white() #define COLOR_BG lv_color_black() @@ -341,20 +342,8 @@ static void evt_do_civ_deploy(lv_event_t *event) { static void evt_do_exploit_deploy(lv_event_t *event) { (void)event; - UI_GOTO_SCREEN(scr_exploit); - - ps2_exploit_set_progress_cb(load_exploit_cb); - - gui_tick(); - int ret = ps2_exploit_deploy(); - /*if (ret == 0) { - lv_label_set_text(lbl_exploit_err, "Success!\n"); - } else { - lv_label_set_text(lbl_exploit_err, ps2_exploit_error(ret)); - }*/ - ps2_exploit_set_progress_cb(NULL); -// UI_GOTO_SCREEN(scr_main); + installing_exploit = true; } @@ -376,6 +365,25 @@ static void evt_switch_to_ps2(lv_event_t *event) { terminated = 1; } +static void gui_install_exploit() +{ + UI_GOTO_SCREEN(scr_exploit); + + ps2_exploit_set_progress_cb(load_exploit_cb); + + gui_tick(); + + //int ret = + (void)ps2_exploit_deploy(); + /*if (ret == 0) { + lv_label_set_text(lbl_exploit_err, "Success!\n"); + } else { + lv_label_set_text(lbl_exploit_err, ps2_exploit_error(ret)); + }*/ + ps2_exploit_set_progress_cb(NULL); + UI_GOTO_SCREEN(scr_main); +} + static void create_main_screen(void) { lv_obj_t *lbl; @@ -604,7 +612,7 @@ static void create_menu_screen(void) { lv_obj_t *ps2_page = ui_menu_subpage_create(menu, "PS2 Settings"); { /* exploit install */ - lv_obj_t *exploit_install_page = ui_menu_subpage_create(menu, "Install EXPLOIT.bin"); + //lv_obj_t *exploit_install_page = ui_menu_subpage_create(menu, "Install EXPLOIT.bin"); /*{ cont = ui_menu_cont_create(exploit_install_page); ui_label_create(cont, ""); @@ -735,11 +743,7 @@ void gui_init(void) { create_ui(); refresh_gui = false; -} - -void gui_request_refresh(void) -{ - refresh_gui = true; + installing_exploit = false; } void gui_do_ps1_card_switch(void) { @@ -769,6 +773,7 @@ void gui_do_ps2_card_switch(void) { input_flush(); } + void gui_task(void) { input_update_display(g_navbar); @@ -828,6 +833,13 @@ void gui_task(void) { gui_do_ps2_card_switch(); } + if (installing_exploit && !input_is_any_down()) + { + installing_exploit = false; + gui_install_exploit(); + } + + if (ps2_dirty_activity) { input_flush(); lv_obj_clear_flag(g_activity_frame, LV_OBJ_FLAG_HIDDEN); diff --git a/src/main.c b/src/main.c index cc21626..69df08e 100644 --- a/src/main.c +++ b/src/main.c @@ -109,11 +109,11 @@ int main() { printf("starting in PS2 mode\n"); keystore_init(); + ps2_exploit_init(); psram_init(); sd_init(); ps2_cardman_init(); ps2_dirty_init(); - ps2_exploit_init(); gui_init(); multicore_launch_core1(ps2_memory_card_main); diff --git a/src/ps2/ps2_exploit.c b/src/ps2/ps2_exploit.c index 5662d0e..0c6f587 100644 --- a/src/ps2/ps2_exploit.c +++ b/src/ps2/ps2_exploit.c @@ -26,7 +26,7 @@ static int exploitprog_wr; void ps2_exploit_init(void) { if ((*(char *)(FLASH_OFF_PS2EXP + XIP_BASE)) != 0xFF) { card_available = true; - debug_printf("Using exploit card.\n"); + printf("Using exploit card.\n"); } } @@ -55,12 +55,12 @@ int ps2_exploit_deploy(void) { uint32_t offset = FLASH_OFF_PS2EXP; exploitprog_start = time_us_64(); + exploitprog_wr = 0; while (erase_offset < FLASH_OFF_PS2EXP + CARD_SIZE) { uint32_t ints = save_and_disable_interrupts(); flash_range_erase(erase_offset, 65536); - printf("Erase %d, %d Bytes\n", erase_offset, 65536); restore_interrupts(ints); erase_offset += 65536; @@ -70,6 +70,7 @@ int ps2_exploit_deploy(void) { if (exploit_cb) exploit_cb(50 * (erase_offset - FLASH_OFF_PS2EXP) / CARD_SIZE); } + exploitprog_wr = 1; while (sd_read(fd, buf, sizeof(buf)) == sizeof(buf)) { uint32_t ints = save_and_disable_interrupts(); @@ -82,6 +83,8 @@ int ps2_exploit_deploy(void) { exploit_cb(50 + 50 * (offset - FLASH_OFF_PS2EXP) / CARD_SIZE); offset += 256; } + exploitprog_wr = -1; + debug_printf("Wrote new exploit image, took %d s\n", (uint32_t)((time_us_64() - exploitprog_start)/1000000)); } sd_close(fd); @@ -97,9 +100,7 @@ void ps2_exploit_set_progress_cb(exploit_cb_t func) char *ps2_exploit_get_deploy_text(void) { static char progress[32]; - snprintf(progress, sizeof(progress), "%s %.2f kB/s", "Wr", 1000000.0 * exploitprog_pos / (time_us_64() - exploitprog_start) / 1024); - - printf("%s", progress); + snprintf(progress, sizeof(progress), "%s %.2f kB/s", exploitprog_wr == 0 ? "Er" : "Wr", 1000000.0 * exploitprog_pos / (time_us_64() - exploitprog_start) / 1024); return progress; } From 891210d674398cbcdaf1a8a85918f17a8331c5e5 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Fri, 20 Jan 2023 10:31:04 +0100 Subject: [PATCH 13/91] wip - read from flash --- src/main.c | 4 +++- src/ps2/ps2_memory_card.c | 43 +++++++++++++++++++++++++++++++++--- src/ps2/ps2_memory_card.h | 3 +++ src/ps2/ps2_memory_card.in.c | 8 +++---- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/main.c b/src/main.c index 69df08e..54bff42 100644 --- a/src/main.c +++ b/src/main.c @@ -118,9 +118,11 @@ int main() { multicore_launch_core1(ps2_memory_card_main); + ps2_memory_card_enter_flash(); + printf("Starting memory card... "); uint64_t start = time_us_64(); - gui_do_ps2_card_switch(); + //gui_do_ps2_card_switch(); uint64_t end = time_us_64(); printf("DONE! (%d us)\n", (int)(end - start)); diff --git a/src/ps2/ps2_memory_card.c b/src/ps2/ps2_memory_card.c index 982da52..bd356b2 100644 --- a/src/ps2/ps2_memory_card.c +++ b/src/ps2/ps2_memory_card.c @@ -1,4 +1,5 @@ #include "hardware/gpio.h" +#include "hardware/regs/addressmap.h" #include "hardware/timer.h" #include "hardware/flash.h" #include "hardware/dma.h" @@ -6,6 +7,7 @@ #include "config.h" #include "ps2_mc_spi.pio.h" +#include "flashmap.h" #include "debug.h" #include "keystore.h" #include "des.h" @@ -17,6 +19,7 @@ #include #include + // #define DEBUG_MC_PROTOCOL uint64_t us_startup; @@ -25,7 +28,7 @@ int byte_count; volatile int reset; int ignore; uint8_t flag; -bool bootup_done = false; +bool flash_mode = false; typedef struct { uint32_t offset; @@ -50,6 +53,29 @@ uint32_t readptr, writeptr; static volatile int mc_exit_request, mc_exit_response, mc_enter_request, mc_enter_response; static uint8_t hostkey[9]; +static inline void __time_critical_func(read_mc)(uint32_t addr, void *buf, size_t sz) { + debug_printf("Read at address %d size %d - ", addr, sz); + if (flash_mode) { + debug_printf("Flash\n"); + memcpy(buf, (void*)(XIP_BASE + FLASH_OFF_PS2EXP + addr), sz); + ps2_dirty_unlock(); + } else { + debug_printf("PSRAM\n"); + psram_read_dma(addr, buf, sz); + } +} + +static inline void __time_critical_func(write_mc)(uint32_t addr, void *buf, size_t sz) { + debug_printf("Wrote at address %d size %d\n", addr, sz); + + if (!flash_mode) { + psram_write(addr, buf, sz); + } else { + ps2_dirty_unlock(); + debug_printf("Ignore writing to exploit.\n"); + } +} + static inline void __time_critical_func(RAM_pio_sm_drain_tx_fifo)(PIO pio, uint sm) { uint instr = (pio->sm[sm].shiftctrl & PIO_SM0_SHIFTCTRL_AUTOPULL_BITS) ? pio_encode_out(pio_null, 32) : pio_encode_pull(false, false); @@ -397,7 +423,9 @@ void ps2_memory_card_exit(void) { } void ps2_memory_card_enter(void) { - if (memcard_running) + if (flash_mode) { + ps2_memory_card_exit(); + } else if (memcard_running) return; mc_enter_request = 1; @@ -405,5 +433,14 @@ void ps2_memory_card_enter(void) { {} mc_enter_request = mc_enter_response = 0; memcard_running = 1; - bootup_done = true; + flash_mode = false; +} + +void ps2_memory_card_enter_flash(void) { + mc_enter_request = 1; + while (!mc_enter_response) + {} + mc_enter_request = mc_enter_response = 0; + memcard_running = 1; + flash_mode = true; } diff --git a/src/ps2/ps2_memory_card.h b/src/ps2/ps2_memory_card.h index d3aa9b5..688f35e 100644 --- a/src/ps2/ps2_memory_card.h +++ b/src/ps2/ps2_memory_card.h @@ -1,5 +1,8 @@ #pragma once +#include + void ps2_memory_card_main(void); void ps2_memory_card_enter(void); +void ps2_memory_card_enter_flash(void); void ps2_memory_card_exit(void); diff --git a/src/ps2/ps2_memory_card.in.c b/src/ps2/ps2_memory_card.in.c index 9a5ef82..ce6c118 100644 --- a/src/ps2/ps2_memory_card.in.c +++ b/src/ps2/ps2_memory_card.in.c @@ -71,7 +71,7 @@ if (ch == 0x11) { ps2_dirty_lockout_renew(); /* the spinlock will be unlocked by the DMA irq once all data is tx'd */ ps2_dirty_lock(); - psram_read_dma(read_sector * 512, &readtmp, 512+4); + read_mc(read_sector * 512, &readtmp, 512+4); // dma_channel_wait_for_finish_blocking(0); // dma_channel_wait_for_finish_blocking(1); } @@ -158,7 +158,7 @@ if (ch == 0x11) { ps2_dirty_lockout_renew(); /* the spinlock will be unlocked by the DMA irq once all data is tx'd */ ps2_dirty_lock(); - psram_read_dma(read_sector * 512, &readtmp, 512+4); + read_mc(read_sector * 512, &readtmp, 512+4); // TODO: remove this if safe // must make sure the dma completes for first byte before we start reading below dma_channel_wait_for_finish_blocking(PIO_SPI_DMA_RX_CHAN); @@ -213,7 +213,7 @@ if (ch == 0x11) { if (write_sector * 512 + 512 <= CARD_SIZE) { ps2_dirty_lockout_renew(); ps2_dirty_lock(); - psram_write(write_sector * 512, writetmp, 512); + write_mc(write_sector * 512, writetmp, 512); ps2_dirty_mark(write_sector); ps2_dirty_unlock(); #ifdef DEBUG_MC_PROTOCOL @@ -239,7 +239,7 @@ if (ch == 0x11) { ps2_dirty_lockout_renew(); ps2_dirty_lock(); for (int i = 0; i < ERASE_SECTORS; ++i) { - psram_write((erase_sector + i) * 512, readtmp.buf, 512); + write_mc((erase_sector + i) * 512, readtmp.buf, 512); ps2_dirty_mark(erase_sector + i); } ps2_dirty_unlock(); From baf7afa8b11387deac12dfabb3f2f6063157de45 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Fri, 20 Jan 2023 11:25:38 +0100 Subject: [PATCH 14/91] Fix wrong pointer to buffer --- src/main.c | 2 +- src/ps2/ps2_memory_card.c | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main.c b/src/main.c index 54bff42..113bfa7 100644 --- a/src/main.c +++ b/src/main.c @@ -122,7 +122,7 @@ int main() { printf("Starting memory card... "); uint64_t start = time_us_64(); - //gui_do_ps2_card_switch(); + gui_do_ps2_card_switch(); uint64_t end = time_us_64(); printf("DONE! (%d us)\n", (int)(end - start)); diff --git a/src/ps2/ps2_memory_card.c b/src/ps2/ps2_memory_card.c index bd356b2..2e64d8e 100644 --- a/src/ps2/ps2_memory_card.c +++ b/src/ps2/ps2_memory_card.c @@ -54,19 +54,17 @@ static volatile int mc_exit_request, mc_exit_response, mc_enter_request, mc_ente static uint8_t hostkey[9]; static inline void __time_critical_func(read_mc)(uint32_t addr, void *buf, size_t sz) { - debug_printf("Read at address %d size %d - ", addr, sz); if (flash_mode) { - debug_printf("Flash\n"); - memcpy(buf, (void*)(XIP_BASE + FLASH_OFF_PS2EXP + addr), sz); + // Skip first 4 Bytes in Buffer, as they are the prefix + memcpy((buf + 4), (void*)(addr + XIP_BASE + FLASH_OFF_PS2EXP), sz); ps2_dirty_unlock(); } else { - debug_printf("PSRAM\n"); psram_read_dma(addr, buf, sz); } } static inline void __time_critical_func(write_mc)(uint32_t addr, void *buf, size_t sz) { - debug_printf("Wrote at address %d size %d\n", addr, sz); + debug_printf("Write at address %d size %d\n", addr, sz); if (!flash_mode) { psram_write(addr, buf, sz); From fbbd2d52df528a1af4bed4cad8a8d6429af89783 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 23 Jan 2023 11:19:01 +0100 Subject: [PATCH 15/91] Add exploit "BOOT" card and activate Boot --- src/gui.c | 23 +++++++++++++++++++---- src/main.c | 3 ++- src/ps2/ps2_cardman.c | 35 +++++++++++++++++++++++++---------- src/ps2/ps2_exploit.c | 4 ++++ src/ps2/ps2_exploit.h | 2 ++ src/settings.c | 15 +++++++++++++++ src/settings.h | 4 ++++ 7 files changed, 71 insertions(+), 15 deletions(-) diff --git a/src/gui.c b/src/gui.c index f310a51..d95df0d 100644 --- a/src/gui.c +++ b/src/gui.c @@ -27,7 +27,7 @@ static lv_obj_t *g_navbar, *g_progress_bar, *g_exploit_bar, *g_progress_text, *g static lv_obj_t *scr_switch_nag, *scr_card_switch, *scr_exploit, *scr_main, *scr_menu, *scr_freepsxboot, *menu, *main_page; static lv_style_t style_inv; -static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl,*src_main_title_lbl, *lbl_civ_err, *lbl_exploit_err; +static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl,*src_main_title_lbl, *lbl_civ_err, *lbl_exploit_err, *lbl_autoboot; static int have_oled; static int switching_card; @@ -328,6 +328,15 @@ static void evt_go_back(lv_event_t *event) { lv_event_stop_bubbling(event); } +static void evt_ps2_autoboot(lv_event_t *event) { + bool current = settings_get_ps2_autoboot(); + if (ps2_exploit_is_available()) { + settings_set_ps2_autoboot(!current); + lv_label_set_text(lbl_autoboot, !current ? "Yes" : "No"); + } + lv_event_stop_bubbling(event); +} + static void evt_do_civ_deploy(lv_event_t *event) { (void)event; @@ -638,8 +647,10 @@ static void create_menu_screen(void) { } cont = ui_menu_cont_create_nav(ps2_page); - ui_label_create_grow_scroll(cont, "Autoboot exploit"); - ui_label_create(cont, " No"); + ui_label_create_grow_scroll(cont, "Autoboot"); + lbl_autoboot = ui_label_create(cont, settings_get_ps2_autoboot() ? " Yes" : " No"); + lv_obj_add_event_cb(cont, evt_ps2_autoboot, LV_EVENT_CLICKED, NULL); + cont = ui_menu_cont_create_nav(ps2_page); ui_label_create_grow_scroll(cont, "Install EXPLOIT.bin"); @@ -822,7 +833,11 @@ void gui_task(void) { if (displayed_card_idx != ps2_cardman_get_idx() || displayed_card_channel != ps2_cardman_get_channel()) { displayed_card_idx = ps2_cardman_get_idx(); displayed_card_channel = ps2_cardman_get_channel(); - snprintf(card_idx_s, sizeof(card_idx_s), "%d", displayed_card_idx); + if (displayed_card_idx == 0) { + snprintf(card_idx_s, sizeof(card_idx_s), "BOOT"); + } else { + snprintf(card_idx_s, sizeof(card_idx_s), "%d", displayed_card_idx); + } snprintf(card_channel_s, sizeof(card_channel_s), "%d", displayed_card_channel); lv_label_set_text(scr_main_idx_lbl, card_idx_s); lv_label_set_text(scr_main_channel_lbl, card_channel_s); diff --git a/src/main.c b/src/main.c index 113bfa7..422432e 100644 --- a/src/main.c +++ b/src/main.c @@ -118,7 +118,8 @@ int main() { multicore_launch_core1(ps2_memory_card_main); - ps2_memory_card_enter_flash(); + if (settings_get_ps2_autoboot() && ps2_exploit_is_available()) + ps2_memory_card_enter_flash(); printf("Starting memory card... "); uint64_t start = time_us_64(); diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index e46db8e..cbc6059 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -1,5 +1,6 @@ #include "ps2_cardman.h" +#include #include #include @@ -17,6 +18,7 @@ static uint8_t flushbuf[BLOCK_SIZE]; static int fd = -1; #define IDX_MIN 1 +#define IDX_BOOT 0 #define CHAN_MIN 1 #define CHAN_MAX 8 @@ -28,12 +30,17 @@ static size_t cardprog_pos; static int cardprog_wr; void ps2_cardman_init(void) { - card_idx = settings_get_ps2_card(); - if (card_idx < IDX_MIN) - card_idx = IDX_MIN; - card_chan = settings_get_ps2_channel(); - if (card_chan < CHAN_MIN || card_chan > CHAN_MAX) + if (settings_get_ps2_autoboot()) { + card_idx = IDX_BOOT; card_chan = CHAN_MIN; + } else { + card_idx = settings_get_ps2_card(); + if (card_idx < IDX_MIN) + card_idx = IDX_MIN; + card_chan = settings_get_ps2_channel(); + if (card_chan < CHAN_MIN || card_chan > CHAN_MAX) + card_chan = CHAN_MIN; + } } int ps2_cardman_write_sector(int sector, void *buf512) { @@ -56,7 +63,10 @@ void ps2_cardman_flush(void) { static void ensuredirs(void) { char cardpath[32]; - snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/Card%d", card_idx); + if (settings_get_ps2_autoboot() && ps2_exploit_is_available()) + snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/BOOT"); + else + snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/Card%d", card_idx); sd_mkdir("MemoryCards"); sd_mkdir("MemoryCards/PS2"); @@ -206,13 +216,18 @@ void ps2_cardman_open(void) { char path[64]; ensuredirs(); - snprintf(path, sizeof(path), "MemoryCards/PS2/Card%d/Card%d-%d.mcd", card_idx, card_idx, card_chan); + if (settings_get_ps2_autoboot() && ps2_exploit_is_available()) + snprintf(path, sizeof(path), "MemoryCards/PS2/BOOT/Card-%d.mcd", card_chan); + else + { + snprintf(path, sizeof(path), "MemoryCards/PS2/Card%d/Card%d-%d.mcd", card_idx, card_idx, card_chan); + /* this is ok to do on every boot because it wouldn't update if the value is the same as currently stored */ + settings_set_ps2_card(card_idx); + settings_set_ps2_channel(card_chan); + } printf("Switching to card path = %s\n", path); - /* this is ok to do on every boot because it wouldn't update if the value is the same as currently stored */ - settings_set_ps2_card(card_idx); - settings_set_ps2_channel(card_chan); if (!sd_exists(path)) { cardprog_wr = 1; diff --git a/src/ps2/ps2_exploit.c b/src/ps2/ps2_exploit.c index 0c6f587..4cb3e03 100644 --- a/src/ps2/ps2_exploit.c +++ b/src/ps2/ps2_exploit.c @@ -104,3 +104,7 @@ char *ps2_exploit_get_deploy_text(void) { return progress; } + +bool ps2_exploit_is_available(void) { + return (*(char *)(FLASH_OFF_PS2EXP + XIP_BASE)) != 0xFF; +} diff --git a/src/ps2/ps2_exploit.h b/src/ps2/ps2_exploit.h index 2ab4c81..6e14224 100644 --- a/src/ps2/ps2_exploit.h +++ b/src/ps2/ps2_exploit.h @@ -1,6 +1,7 @@ #pragma once #include +#include typedef void (*exploit_cb_t)(int); @@ -11,6 +12,7 @@ char *ps2_exploit_error(int rc); int ps2_exploit_deploy(void); void ps2_exploit_set_progress_cb(exploit_cb_t func); char *ps2_exploit_get_deploy_text(void); +bool ps2_exploit_is_available(void); enum { diff --git a/src/settings.c b/src/settings.c index 85dfa2f..6705feb 100644 --- a/src/settings.c +++ b/src/settings.c @@ -24,6 +24,7 @@ typedef struct { } settings_t; #define SETTINGS_VERSION_MAGIC (0xABCD0002) +#define SETTINGS_FLAGS_AUTOBOOT (0b1) _Static_assert(sizeof(settings_t) == 16, "unexpected padding in the settings structure"); @@ -122,3 +123,17 @@ void settings_set_mode(int mode) { SETTINGS_UPDATE_FIELD(sys_flags); } } + +bool settings_get_ps2_autoboot(void) { + return (settings.ps2_flags & SETTINGS_FLAGS_AUTOBOOT); +} + +void settings_set_ps2_autoboot(bool autoboot) { + if (((settings.ps2_flags & SETTINGS_FLAGS_AUTOBOOT) != 0) != autoboot) { + if (autoboot) + settings.ps2_flags |= SETTINGS_FLAGS_AUTOBOOT; + else + settings.ps2_flags &= ~SETTINGS_FLAGS_AUTOBOOT; + SETTINGS_UPDATE_FIELD(ps2_flags); + } +} \ No newline at end of file diff --git a/src/settings.h b/src/settings.h index 46d436e..c396b8b 100644 --- a/src/settings.h +++ b/src/settings.h @@ -1,5 +1,7 @@ #pragma once +#include + void settings_init(void); int settings_get_ps1_card(void); @@ -19,3 +21,5 @@ enum { int settings_get_mode(void); void settings_set_mode(int mode); +bool settings_get_ps2_autoboot(void); +void settings_set_ps2_autoboot(bool autoboot); \ No newline at end of file From 420d03e07ae38c6c4deb979d99b050d1a76f0eb6 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 23 Jan 2023 11:42:36 +0100 Subject: [PATCH 16/91] Re-Add Force Refresh --- src/gui.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gui.c b/src/gui.c index d95df0d..2f4de55 100644 --- a/src/gui.c +++ b/src/gui.c @@ -27,7 +27,7 @@ static lv_obj_t *g_navbar, *g_progress_bar, *g_exploit_bar, *g_progress_text, *g static lv_obj_t *scr_switch_nag, *scr_card_switch, *scr_exploit, *scr_main, *scr_menu, *scr_freepsxboot, *menu, *main_page; static lv_style_t style_inv; -static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl,*src_main_title_lbl, *lbl_civ_err, *lbl_exploit_err, *lbl_autoboot; +static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl,*src_main_title_lbl, *lbl_civ_err, *lbl_autoboot; static int have_oled; static int switching_card; @@ -757,6 +757,11 @@ void gui_init(void) { installing_exploit = false; } +void gui_request_refresh(void) +{ + refresh_gui = true; +} + void gui_do_ps1_card_switch(void) { printf("switching the card now!\n"); From 0bc06355bd92d10af1a10532d86ecae420ae2750 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 29 Jan 2023 14:17:37 +0100 Subject: [PATCH 17/91] integrate boot iamge, remove deploy dialog (preliminary) --- CMakeLists.txt | 4 +++- ps2boot/CMakeLists.txt | 15 +++++++++++++++ ps2boot/bootcard.bin | Bin 0 -> 1048576 bytes src/gui.c | 5 ++--- src/ps2/ps2_cardman.c | 36 ++++++++++++++++++++++++++--------- src/ps2/ps2_cardman.h | 11 ++++++++++- src/ps2/ps2_exploit.c | 8 ++++++++ src/ps2/ps2_exploit.h | 4 +++- src/ps2/ps2_memory_card.c | 4 +++- src/ps2/ps2_memory_card.in.c | 15 +++++++++++---- 10 files changed, 82 insertions(+), 20 deletions(-) create mode 100644 ps2boot/CMakeLists.txt create mode 100755 ps2boot/bootcard.bin diff --git a/CMakeLists.txt b/CMakeLists.txt index c97ee5b..0ea08e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,7 @@ set(LV_CONF_PATH ${CMAKE_CURRENT_SOURCE_DIR}/src/lv_conf.h CACHE STRING "" FORCE add_subdirectory(ext/lvgl EXCLUDE_FROM_ALL) add_subdirectory(database) +add_subdirectory(ps2boot) add_executable(sd2psx @@ -131,9 +132,10 @@ target_link_libraries(sd2psx hardware_dma lvgl::lvgl gamedb + ps2boot ) -add_dependencies(sd2psx gamedb) +add_dependencies(sd2psx gamedb ps2boot) set_target_properties(sd2psx PROPERTIES PICO_TARGET_LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/memmap_custom.ld) pico_add_extra_outputs(sd2psx) diff --git a/ps2boot/CMakeLists.txt b/ps2boot/CMakeLists.txt new file mode 100644 index 0000000..f8760e9 --- /dev/null +++ b/ps2boot/CMakeLists.txt @@ -0,0 +1,15 @@ +set(STAGE1_PS2_BOOT "bootcard.bin") +set(STAGE1_PS2_BOOT_OBJ "${CMAKE_CURRENT_BINARY_DIR}/bootcard.o") + +add_custom_command(OUTPUT "${STAGE1_PS2_BOOT_OBJ}" + COMMAND ${CMAKE_OBJCOPY} ARGS --input-target=binary --output-target=elf32-littlearm --binary-architecture arm --rename-section .data=.rodata ${STAGE1_PS2_BOOT} ${STAGE1_PS2_BOOT_OBJ} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Generating ps2 stage 1 boot obj") + +add_custom_target(ps2bootobjs DEPENDS "${STAGE1_PS2_BOOT_OBJ}") + +add_library(ps2boot INTERFACE) +add_dependencies(ps2boot ps2bootobjs) + +target_link_libraries(ps2boot INTERFACE ${STAGE1_PS2_BOOT_OBJ}) + diff --git a/ps2boot/bootcard.bin b/ps2boot/bootcard.bin new file mode 100755 index 0000000000000000000000000000000000000000..d34d8cd40f9939c41d6a9d529274dd8e916521d2 GIT binary patch literal 1048576 zcmeF)2Urv9x;Oku2%#sT7cmJEf(1xa45$d91#~H(B3L1GRGOlIq7YiJV88}eh^T;y z0Tly^K&UnZY@mV=QNcp9fr^s%jxP7R&f0sQz0dc3*E#QcKjylAPG*wKJaa!Ylk%V7 z((te?WVfX=$cq9(!#8dr+xu+vC$qvghWczGPdA%kMmM7)KL#`!As~nZ!61kV%y=&I zb9Ow8;(p%z@vr845=xMOkP$L9|Aktb$oAbvy4EAe5prjDq#a{5DeCWbznUR0ihrago6mM0Z7*!3FA#*Gl&Aw zAO>s!TR|+?2DSsZUW*eCI3NKef+Ua(QUDjEf*oKd*adcjG_VKoKsu1FcOLZfKDfOf z8~_JFCddMZz+rF%90kX~agYsi03VzHx!@!?1x|xJkPiyL8E_Vy1BKu`5CG}ALt)() z;Pypu36y}#KnO}f8Mp$jf^u*TRDeoQ1w^14)PP!02kJosxDIZBo8T6>4I056AYFGe zj9b86a1Y!E55Pmv3fjOU@EAM+?VtlZ1)ZP^h`}?^4W5G+;3en*z2Fsi4c>saK)T)> z*yaJaJqU)tJMbQS03X38Fbqb(XYd7l1*6~__zuQ^1i&?F927tU8Gr$@01M=RJWv3N zK)UW<0}iI~fB;m18c+usKoe*IZJ+~mfgYFu^ua_x1SCKP24E5}1QcKdsK6LZ22;RP zFb$XhQ(y*YfDWdE8DJ(b2eSYJSO7~f8_WT7!LRm9$2Ks{1h&8q*aHXP2w1=gID`3M z0r(9p1dD(Ra0PB)F<1hY0(Y7@CAOr9|V9v5Cnq3 zTCfhR2O;2(+YkZM8^A^o2{wVvAPPi-7_bFw1+ico*bd@AJm7!?kO-1MGDrbjkP3Ey zonRN(4bs3Kzys+Z1MCI+zt@lgKMAyRDvoX0@a`f)Pg!t4;sLA za0A=~x4>=C2=0I;@M|0H!tHn9K6n5gf>zK59)ZW;31|l$;3?WH!D7{FBQ3HD=S`_?MS9)$r%>j{enb`oDMl+s$`e?r3kibj4D4$3?%MJ3ju) zR6yGQKTi6;_rJ`qme2S%``>oF|9_S2uQ-;z{{I+$?|iMrxBmb0>;0dn|H%f8zyAM|!u&;M|E>T3aJ>I`4fwnYCKiQ1&u!~w z?p;aRdVIk56dHF0xk|u2qxBkSn%`>lL(koGe(0d#VYdLv>I;U8RZDQL=VCL|H0kej z8?Sqrm=eN-ohurr-BG=bEs1wJET{d*i@3j|*;=>0KfT|- zcWPhxoay_|j$DrU?f72Vb2xvvoc!ltj*x%m62E5rK8E&7+Y1BX2RG>lSU8DEGcD;H z>HRG~=E0Oa0byz&Km58M?*1_l5VREsYeyj6#Tj|JVe0?$7oih(nq+f>FjGj+7bD9#b+? z`Y^m?p1f*EPMq!j;8Hnj*s3|8o-PDqaUM-zRXQSc%E_iI0DO3uU)dkDxf@O5cBVrjg zQeliFsKRp+jSV4Y1o=#Opusj2DBC-Lt2|@Tp8ZeC@x#`k% zDI*x3A-x8f2J4bOM%w0Sgh}YP66i%(?qkW1E=Ek9p>4}Su(ZE%lz3#)bS$);siS}x z5O%}7^+pI{m%${U;kG>mttJCwJGwl=B$zt0e}ByRACLL@*c$!-EE@%Vib9lsERX31 zE&UhElrBrU+<3%~&SWaU>;Ya_ymN0*8$JHO}k~fk=0^qpqFPx?mfSM`6P%NPS zie!5@C5dHB2^{1*JWhHn{p|0bZ8Bwx{=qta?x)haXTo!&{Uwjom=LA=iw+0DBZ>40 z(%Y6CI8UFUlaX5{rc5%*QcoT+Vy;DA6XheIW}4FH?6mibK#*74#1iQ_s+U5BdD7{x z(01uG1`ccK<37Xv@-QvEE$t)e0FQBlzxMpiR7Z@!z69sH2H26p_w$c*o^<-xdGFV>bl!jOK>qQ%Abp)c;OsXK zULT}CLX!@vFie7BCJYWR{Htl{b0kA>Qbpk8`2|k(%7`~xh6$JSp{;tMW)z(&Ut$sy zFN9O)5Ne3h^@?r3}QvB_obY z9o^L*%acZ37dgydqF%lnaZ{8yE_a{r!gAei5z<9+TMh&D=&4 zP%zc<=qry{p2I^m$(JcQvL`**twGqRPI3cLZiL&5lgL{Ykfkr(DxFrE=_By69e!Du z-0LN~K5U-FwlW3uHk+j+!Zt2}O0?$DyUof^$x@4P%jK;}ix9)mAcCYgIw{`UbxdR{ z>oQr6SUh~C!CWq*GbY-R zM5l%a(v1iXgQUVl9jrD!l|Yr{%XsH&#x;27hw^JpuHw22hlNzCK5KILYWy{0v&^nS zZTUDiK}9KHd67pYL3}NHvq_rJ-Hx97h^gT^;aXIZIuggApa`g^1L8@A?cvP#L9-G1 z#Zk#|q|A&WmP|7nBd*b(IB-caDeI7;Mjqxn{k>mxpo=Xf^YK@f_MpF@P@;un^(dR6 zgKW)0aZ1C?T)WKb)k<=V??JZEY72G+FcqIyG#SgUNP;Rrk3)t90z*}cC$lT z$@Z#%v_Y?~u&xN+mS+0nq99(*?9`K)oadzvJe_@`a&i1ov)m$4YF{y~-;0r0GTG~* zrdCFRhq37xJtj_%F7z=DwBWu|wc4n7Z{#hWbBxKe$+DYHEgNQ#8H*cCwx_#IKGL@d zCFwEXsG@NZ-UVZ5b8q^H>Dh=*r*P)Hlht9wS-czdIO`2dsBP_}UilB{iqS^AF|+jL zaYSh&E>^{cU-J5kxx+aTe2}SvJP1@A%_t z51T`t)r|YCF1s>QnIjiRbh0OSqm?k8O>#ZPGpgwVnoL7j7NsNn^F-G~md`4so!%GZ zNE`jNW0Z2cR+S~Dt@gM})oGbisC@mfzES#qdVBUHWp|Vp4oInbOS_PDTrG12O`W zxQSglu|$1dZGY4nw;YAbpEJqbaj*Ndi9PwP-}2IMh={NpuNby{fbDjM*Dxsb)pCe2 zV=t=o8%_HrVY?e2w-_Ffh}=B`f>!46s}n7Y)bb*_cwxtKj%6KBZ`CSt$va$`Kc<1h z%bLkxh$>^nLEPWSZj{;BMQ{P50N}e`Q+?7CRv>-ERGGXUP$g{BJ+N+`l zi$!*iddlqH2xS@8X6swUTp=;Ba8SF%j$P=3y-n>RG6+`a5^X!0EK!0&R4F^`2v4b2 zVZ|OB3U}i@$IwX_ZNx!k!3eB#1I(mv>spx%&c%MkF&;2WgmGn zv*M2hy`?D-t7wx?tJjte+E^=w(L{DHls~YiU{)1b6dlk)Z)G%L!*MoM=pe+g!;V6G zmgUSfibQuRv1L zbi8?bNUqF&p4Rd^@^iI_(+bvIn<5wI_l{yfaQqY>FZa?Wn4*BH3iu zd6g8tYmhiO)=Q}$T2npLdqKxs<%%V-H~7@}h5Vw3o6>j70e z&KK>G++?AoGbFH>GGnWc*Ltov{T)^itP{u(stP(7x%>n1q~f9^j8}q#i_k*mkpQ3=&H2n0Ie@V6m0< zU9^R~a<*mAYErMZu~T2%7x=1yf+{^QB2%w{j;{5pr^loB4l=zOC8`p+3SZLu^@w`3 z!Nx7xTMf2JB)Xn~lGzqxRD`c%;FhA~!aN*LU0_6UmaIS-x=ltUu1HeMt6gt25W04H zAX%(hPx+GxF+DMpRA| z5k;+45MbTZ3(Q32acGabm#zCtioz~(i!#6Rix6FP{{?Y81D)-Nq0>-A;qlJ>3?U;d zlA0P`g={Fi;mx|9cBp7D(5xo7VRFG|{0wa?!2=b=@TG#g8KvxAL03{&UQ$d+CJY z=A)iR8dcm?t@8+0EyTn}0z34gF}rV=Iy8|ymg^BLxgx44;qn6xo5pjQ+Dfj z+SglGGW`Sdv=T^nu>643LT_~I0J~5JyDL~e$4gXLj=@I7HBd$+DA}6cx85CsWoa}s z*W=8>rf*KMYE}|UH>#GfONimM(Ea)-B08&unDi{}s1`M&P+_14gR>Z1Yn@3D?@zxs zomWA;Y3ybbLR6sabz1aw z&jR>gajaC*InI>{iM$zkM3-mAY~xitI@L9?xW`0G(TR=ru#gkjnMZZH>^9{7RvNd) zt~rse@1dSot885st(2Xj(HKOSIvA5GLCTtgyzO~D(UIPsN~Oork43-CR|?-#8xk*T zg(H=nG91Z5-=I0BIdpM}vIA_TgsMp60YwJ8wI;i0Ai%;$tKCczAEzi}_M0W_(Ak|h z_^r{~Ax$)E3^CVokRcYZTuL1aEopACc@J2{q&^OL!5kO+qfz7}>d`#T_kaP>ky+RR zPcJug336-%eW?gVM_-)7k5iaLBkhu3lo^qT&byb0|E&Fxh>@krD2QyX#ho(VgtNk- zXR&miX^npLc%)+1HZw0yO&!HQNqn@>LQ?jbGnFbNdMN#Rq_y{HBcE02CGwZhjUaVw>!7!=}>z9X9+)o=qw=*ER=|k@vpck zn-fVIl4^8sLRZ3Iu?EUPQjK(>RRki*v0em8B3MQ^&FsBK$C>xa9f}dA%SLgM3GBFI zBmM%}COty=s;R~^)JbMqb}zJPJ0%9w60p1t zMwX`{!rfTicIAzTdq2bM_;y%kJoO6iX0os|EJXM*XSKBvJC{CX?bNWBU>8K3ncxud*+|D8u?Xg9$PrN+Q(tv9 zJjlyuh-x&=oO68dwUlDxPZF!LU{E z8k;i9a-jku!Y2}w6j`MUi0+CA8y`m_tVN8GXRJNM6W9e}GbY!fV~P!GYqnM=-sW3^ zJi9H{rX!5%us&{nH1VSZPwT)MYlexm1tJuYm?05!{qn_$e02Q0WJV^!zeojQ7~K zVF`y2VLYoyVJJ&H6z|r>NyF-PHQ7`lOIt1Y1SfoAshlq#6I5L%to93V$%TaAlC2SuQ*T*KkJ5KHei`F>-y04{%IC2D0EF^-D~hw%v)`4*YiGObIi?-Q%L$PfGfS_od0}0g=#gR@h0koePgI#t zZEZT?r9+ZQm-9L`Dw()3I!eNh#brlSN_NNy8F{6W1YEc2P)3}Uws;qXi_^ZbL|q$2 ztsXuRwS$Hpq)dN-D?R*+}e?1s&QS z&m%}!{A@WS?t$I&v{^~U2Q06rQYYgWoo3yIn{YGRUbSe9wcM=sylYHe?(L3Wu6mwS z%*&vtXP#P@cNIR%KcIWU+myiw>w&%=rmoA(O4wnmIvHc|U85S`qLdYEala?bU*md0 zuX0Z9A(7;!jZU1Em*C!d?FFg|I?t!(P;Ki;HypfnNbJ^S?2h-a@Bp5)(}&)0D`^=b z#CE@~Yy+8j!}}KPmW{~ua^Qj)Y?_SAIafm(O5fGu`ftKz>WqhEW6{|>)r-cmo_Xn5 z_B7hUAejh~>wPpOh;VagZHu>pX1p2_n)Sf@Wp&WTu2t~9Z=)nUls?CGsl(FF6-!Cy zhWR?rT@j@+A}Yv;oR?i!Bje&DZ)BMBthne(G&kUt|=!C!clhN zL*bhoH?|(xNE!==zUk-W+d$s>`2JVIWd|5&t;^ho4R zJ7_yHS0bSy$@}J04odU3|L))a!uOv4A^&U9`;YnGZh5n$;}g{m|3A(DwkOu_Ua=y9 zJ!yxmanWXj#d!?hwRhi7eBrn1@k%|_)7}5w`5yw`?@Ep5f8+Vzc>Xt@|NSu&{*`h5 z$ZyBuq&;S0*`Jc{zDgRsk|84#k$VFeEy}MLJuDu)@|6Kkq$*x69 zp__3{D2@N<)_oBFscSQLZxakrF!JcAD=f|7i2j;ni2s5;*q?&1r8tQHD&)Am65P-D zuhvxD>1dU{9pbbh^v~{taOscAY^bfKBcjG@Dk5G=T3nv6u%Wtpa=NGYaWW5SHubs|z z-s0SApKb53#i6$#yWr%Olf9<~PeEF!GmktdAhC4jYI&b5+F?cR6HcJYS6)jNvPM}U zjA&<$g4cQ?H?N;tLfmnbpTxhc$lzSVa`}^g)4-|EN&iN3L2{RQh(e=G>3W@3*+zs_ zMY)B~Alw@zWiT7ihtA2Aa(U+(N`(crCQqf|-dqM%(L*`j4DVrq&+BqA#>DAVT3;s| ztD4Y65PJxwTBWs1!@Up)_f(G6M_FXCzDvVBt5T0ih2bF(?)4z_TceUF#7smHo3bcl z0;2xJ#{-f{jaiC{*%-Wl{=UpFaIyL!3v<=h&DAFP`ogF$o5g5 zl@e9%`=g-18~-reV?nqVJoqErbM*Kb?xA*DlrH~sxR?Di+%@{ z;h|u>evBShttU(}R~=s{Lc{XwVHc7Xf&93L$(*#x2Xyx&GtIb zL)6t{XgzOwtm)aZPQ&nQW{Q|~gvdMFjVrTWLcP*YO77i~Uan}w6PsNxj;mI)c#O1q z;+V_o|Wk~gJ+l3DPlslS2xnn zVREq(C1pI5(q$~+JQ^P{DG&cbylR8ZZi8@-h_Nus!dNKFerUPb*zwpy*Cx$#BBRyP zr67|TJ2GOBknPRyzTzp@Vf<(YT|lOVHe|J9!;?O{mas5Wy{U5WQPh8R%+#)(p#_O) zkt}MA4*Z!%Z0W+mPmt`bOXZZf1Xpn)s@`NPZWSm56V1(5L$Y@@icnshk(R5Htsne~ zJLJKWiO6zq_3!9CMT*AoVjAr7xy|!ju@HW8uv5W)0ebSYX_dN-FOYzdA=iB4ra-0?@B*uw0FhbxYJ@7qP-r^ zpk?WW{6y8FggoPB`L$s;({HBUjTidoyv{r#7}JOlXk;ZE}zWsKs+l=wA7w(y_mFtWb@EmEG3pI#Edj5H@${vFQv-7 zl$_a>hqJh5fMVM{OCDTNkz1VntFF6;j=%_)a-Gm-XyNN2Sb50ppfQUfxQAp%jXF7C6^V7t_M-!(S!F8mSf zZG&KMl36SSd+9jB*-WaS@&Qjdt2aJq4ozWzNbBBVe0p{17Mp%+McNDIWU8`@IAV9s zW{XXOT4+YogzE4%8+6bp#{rVPqmb<3a(IN^YeP!B7s@ixHJ8!@69@WbDG@pKS2N3U zBDyR^%FCKVnkGQBr>|`oi$_u(rs?p8AlsWAH%seIlDyVMqOjoFo+*+zKWVf_iuW8Y zRJvK3f`484CSAL#i(Rzb)xPox#T6@#332Jv^jPN;lIRlU)srB1M=Q`QNhR7Zx9Csd zUh~gzkNG3q3zmj^LlExGb16g6i7KOQO`O;==4uuL(mmbuCw>`$4MZwdb~q%7Z#_}f zu7>mFV{fvD0bIK(ok^`Cc?{Micn+`n~hV< z4x^teiW#w%XOv;Gi>nR}Q?%M1hA7d*GMmOU^cl#sDiJ$On(iGXAx(1Lk>P=lth9sC z$rjjb{U9?^q_*+NzHjhnWo0Ow_Dv(*G8*V-(cND3gm`=YAgxg%pCq|ZLE5q&QOJnC zvPCIlYbB(6GvzD z@DT1rOfV6WCu%{srv%|%WW19MGYrph5iL4eY_lJ=9~FY93Q{IM_-a(pW`BHtRf?ql zLL>4WzBYq!uTmhChI_jzi**E>CzYJX6W9)HU5GNf=M%C@=!*_R2&iYh^io1hgH|?6luqyw|ZQj zXI)ZM9Cop&EAvOVw{Sr^gnM<{kvNNpLiNszj6h-7{YYxgQS64o&(7Ygw3zQjf&FGR z4U;uLsu-878Irg07*ih`g?d>@J^7^MMODGP+hisWA4T?R-Coi;6)V z>S|*(V_aIP;=IJ=nG-}>Ru*nEs6(k3mdHar^CIHLU@O0$MLosPM!DM6#VcNNjkh!M zxM1{8WHv_?zQ zJ;z{qS1-}iLJYPl>Swy=(;Mx*+S~4I8f}d$b8ge}Z%)Gy??t##O@);Z@43xI9;t`i1)I4Ft2d*_)|}~1hH$pb3@z+&e+J7cDgl8D|`3Z^PdY=TuADH$`e5&g2@m19~)mxy>`fE`g~X6!StkkP7>y=)+$ z&q8Z1#Ctw*&ccai2^V(gB*qUegm{lD8p}hpI1ZbMEQS2*j)gQue5U(mRskvGKgN4H z{08(ti}&z)IX&>E042(Z!Pu-=4K9m3ctAM`5V2xSc^`m0?^6_h{+) zcu6Xsh>;`?U?exixA_@vwM6(jcOhD*SEp;jpau#fS%?t2$Q=TSLhlU(nMp`>lIfj! z4JXq(UoKV1NnaktA+X{UMo#&6SVOp{H6q`OWk&?%q}zU~?D)&h9w4sy+9*`JoE4;d zyDiVM@g7BH(sb`L#CtLk;!~=fD>xQxeEiRNFYFKT-nWP!@t!$HLxy+-mAa|3VO`!* zF||gKIPe_ey%w$BqY&?fLcCYVogcrDaC=sq+wEuqeRbw8i1$px`P2?RLqX#8Gu~t4 zZHcyutafR z*P}lsNods_#wlTrB7sQcgv%(CaMeHOC-RZ>@MKD)O?C&t##nDPpM=b6}4 zt9XqrGJY7_6=_q&(P(&OK_H0n38mG({2+Q=gwT%_^fsMXCFXo~fp{;rN^4SM^%**Y zeaoaGTN>|OSYX8T!b#&j>^X?{Dx~pV@sU^C7N+-#8%acE(kl|Cbl4T`?cZA)u2YO- zMjZ=6Td9ZJHJK}Fx7ExwR`UtqXO5sUqY`W4iz6L*P5j9p2~=j-*k;FJU!1_IFL*5sFUMS8(N=2yjLcT_e$MHw*8Fv z+yw=35bqV^jf`VXrZ0-#)mZC;K^FTm-b~_^N#ng_Pa(v6mbYfA#d$-#m!`>MmrCP3 z{2ZJR86rk_b!~-sZ(yW24&uEO>RXvlf;OE_i1!@V=Usz%Pk)S#mWYON*@>0Aa_v~5 z5bw$0>P?5@$Xm3<8qVB_+8Ilz+NecI!%=oKtflduoP?t?U@Dh6R6WJ7VG!ayiVB&+ zpiw3*tSVT8Z&OR@AK4DZ#z)wsN7F zO3h;2;R}aSTfGYKubx;MFUheGaLPu^D^evgubMlg`QA*{s%j$BZ6X%kz(a+2Dh;4K zZN(M^;}l1v}ej1oA!k35g^3 zK99W{lh_H=wKsmej38z4FXT{P-KF^+&e*N{x;}OCT!vXpccDA3xsC6(bj;1zQ_k3t z?CrA{&t~C;JW&S4C3A1y*#)moo0rsj*3$>;t!oE*ikbc{jg>@$s#->jG~b(_Xz`SF z(qd7M|5c6ay_lKlhwu^^8wxw_9proGwN)n@h@Tha9JM8BI~Z))AyGJ+5pTM}VnzH4 z3oqKCEbdv_7s&Upx(hWM$THrBv}rceT+x9K7tEk>a4v*Lb!EX!?mHf>2 zRxI60DuR5^72zon#gOkM=Ec<2sJO@{8>OdrCl_vme2)^vow72_<h7f@lKD1nULJY$}t!ElNj*Zod@SEBnX>EfZT2$UmD!~ zKKys*cmHqKC;k0rax^sl_uqSJ|Jo4hzyJHsACYL_uP2U=|4J43TmS#w-?M%--uU0{ z|4^T`VdFml{#CcX*!a)+KL*YP9JnUn*YAG`6LESQXFPnj#7p%ZE`|>mSUz~;XF<~W z^Eh7S-YI|k=^T3Fchh;_Hdd}!xP6uTvqh(zGm_RnKVx~=u<$^`%0)SuOBIdQDW*if z{5G4T{c&?6CEjr%3kAO({57EAmvYi6eWV3t{%a4I)aKQBtR(;BOej z{C%bm4?!NmPei4UPrid7%j~3c2uS2_2(m_c-2)O~bDx|#q$eBtxevXqp7g1Hr0QPC zov)(vuC zmh{_x+{-&CUtgrR*CuVCs#vpsDJ9TxyOwD4w(e8gjpO5Z(Y^_#*(59e+t~8w+GFWH zZ#!0PEYnk8v$FL>gfThqT*Jw8=$F`M?R8qPN-dUcydf{bc47cjerX(w%FZ%+}p za?K)gvk5~x=d>zSxu=Vwm145PKY6~g_i4U$UnrldvTHH_>r?M%ezBi8*PEcWti-M13~t$|?=+sK&v`QkUbx>Sn@`7c-BFnE%m@otjYy-! z+v)d7FKY(dWpB=S67WQz>w9P^hP+lF^jUhLD{fUhnuL@vDw%9bC zCdHIoPM9}0;M(MaC(caVluWbDP_@PNP2{H;SwvQTen5Mv{J;sDF(O`f<&>(t&t>bH zHAjjv2Fz56`;VO6l4d-CcC(--{Z(9g=L@@I6%k?L`eLH+J_@>)FPMB%SqZ*m(+uaRr zwXUCNky<|GrU@%xqu_wqt@=?~z|A4MZF8?)E?<8};lyKSwUw*h1sxrGCfx6R*DKsS z;yXvhu(5mFIFqZB#5)@&uKzZF>)q#9P(Bxr)G?n&Qrhnu zuYbnBo~ZEHv6)n*ManM;?YMo=_1W^zCoxRDt>rF*M-=4GWNv+(xY(9?Af^4-#Cgo_ zq>LR!cO-LI13l{^PImY|r@epMpy8)oEjnsAws_!7-GllkTOEDA|s~=aS->v7~@h{(9+vMJB>3pxg zj`^*p!^RY0*B6C+-(NI^b1>taIFeek`bq(1_OTn+y`e?@FVYT8-glnhKf3Za`Hq1v zd5<`xNtvtqly{pZzdZ2el^VtLK=o`L4?UGN-pZ||DeDS}FC7v#6|CNVaZj@P=M2>5 z^jqbW>k5tK-m-oMJx@vr4(nUYSZcwC^WHu{O$~86r}7$6NXYHCKN$KpJK&(v$tg#o zK4#?m>`^{FVWH@G0jb{2{D4=(J;ycgoF}p?1Nv0k@4lHZ-{)$KS$|`9>;`dJxAuX? z2K!Uf=1kagmqd7Wd%n{LUoxg<`=gQXqGd^M0vFwRbvesiw{>BJQbD<6%F%Z&S$j=c zw(ONxSNADxzcBgMl=T)9983?`M_N!0My~49*%Ujhb$Q#_UGKUwyHRgRk^9SK_a`oO z*}d&h-{!JzR#;RFdck6rwQkyl&+VUBb=Wzn?t2<)=Qk9l9E$n=+3d@{ONl`en#`=b z+Mj+Ko4lNOD@XUq7KWdC-}8#PGtCAbYrj7mon(PI`{-T!;i@LtI=0oBDi^<8*@XtQ zuj+I4v?D67FtM7`D)P_K-RooZ>ip}VS23fV{^;ve$k8j1p14PS+0GvHaB}~ z!Rr*<;9brp$%(in82bxxsm2?d=tR`B#4}zi9?P9wvaSBw7Of|f*WPzMRURcbtM0KY zrDJ;P{V%h6+on%RY@9+fS`({kaH&a+8n~)cyoDv(I8f5=LrMEKcC9a4NSKW#vppR5 z%~80uVOz4prfy^9;rENyE?^dCKb^Mkef0jx9;o8|!J?aA@01U3ovOYG-O+nP`Gb+> z?#L(hnMTuc6N@6B#QN(la@*AYg;<^${>~P^i5_@PwotBF?#tP;rw;9Ut|duj*tV&? z?dQEvs`CAa5U2LJ2VbtMeDE$<+gtX=)e@^!yv4r5kDLMxt?rb^?d`nHe3-E(y;$&g zH1NoXSg%37TgfIT`Kir=t9A*SZ>v*lPo8r&vU6E5Oq-^7M#WPds_o zGs@)jZfda;O(VXrKk!bj>{!K|w4+~^$=>o~e`=Y#_r>{m$II(w6je(m9Uis*@ZrF0 znz&=p*d$M9j&IoZL(S~GR9;Ept7^31>0`UJv;zmq8hw*)zft&9RZXMpnfvk0X1(^K zDmbq5%1>5e&WU$e$2Zn)task%uUEG(@?Y#fNp-j9@R%#!(6TLEBj(iW!yg;koAe!+ zuhJ(>5QT?6&}Hhs9k|JjZ8&#u^cBUs&vEGRfrsxoMs;^XW>C#q8#AKQHY?JcbL<0# zZ{&|mcxv`l+uH0{+$Ixq^ZmsOq6gAc_uLv&ymc9u-=3p1BYEAhuk(-}Sv2@^_sXO2$H+W97f zW%}&Sw{2yw&}W9JrU4yW3gX2V9v)`!El(e+Z`dt{tI5@R_Pig}?qwgA;7I(lAveg= zwoTbFug;o#p?SwS%`{`p7Z0xLmtBnAI>*sxgW3ISUa7lm?52r#XTEo)hO?G`8Z}@yQ#V@ zXLr`?by+z@59GQg+3K$Ez9zZai>hr?Yi_?sFC6p>VNm8 z#?>bkv5lvMCQV6WVnOJXvdfD z^1@Gh=ryJ4TffCLHjjL{ut1exrL4Af)_m4Fou?0O+k2NkU%c8V>`QDJzGGJT)WaHK zl*&q(2K$tAEm3|4pY?wGnlhzkSz6k3x8BMJ+gdVg*-eem8b?VRvWbzEH0Yxmskv=GgV+(}p}H?^l%UD??=3)<3;iq8uxe zGBsZAN%3z%#ZR|Y9rt~(#bWKE11a6?+4aGVcGL3o`ldIdlP)>$jz?>__t{-e(3G4A z$er*}{*m9h$mW#uwg`hdlXjaMT_P)(h&{tNw}ar|d-jBf;Tm;anun_Lqh9QaOIez^ zdXs(itd4%^I5Kmi-Kmx`B@_RL&W4ww>jPq*$*-21p4Z^q%_%WA+_SUIyzmyWNBb`3 z?Ezx*op&4d>drfS>EVR9lOql9W(T~f*}d88U`_8wiTXmxj4d$>4e|4`?(Tg&H|Oos z-S6MZ*FDk{E}rH!>N_>PB|N2j?32ln_|s0EC5C>&=7uZjNwpJy4X1+Vh&bTw&uRkku#Ssg^q@KAFf&KlDTNE-d&ReeQIxy z>c`kzIUOcHduPp!(Pw0{l#=@{6DB@w;S|lgcREP&c+$o1SEF*S=;EGkzNx|{H(7Xg zD~%nkTT6dvaU416=wI$2{PHY0ffbU1B`#FIzO4!BY|L7_Y5Um03O~jC!7E+?AE!oHuIoOL6thI|Md?&A zY4`kDQAg?z1bo{3Hq*%8UZhzPac5oUH?JK<#E_Z4y*uew|6$#hCt6b$T`JhnGJ&+R zXJ6ortJ5YQX}dXR?g8VNmd2H)7VCNEJ5i($B{wH1{KF4<$NwB9ms5Nl5^?q7~gBz{TH9Mmagx+nl!WO+B)Xe^DZA-^V>JZMQCil za6G_#%H_3tyO<8QmRC;Q9*fzl>D$+Ts4m^Bc&16)zM*uV-p6UL=lA4=f7#N|6+BJ; zu`Tyl^~+;~wI?U^QcQdEJ}r4f+?@UR%689Mxe@m3h*k9S3AiV;8ZUWvg?8yn>;?SP z6*RBMvN5NXb1E-RL(?28AJ-RX;`UJvF`rk+vS=J|?bqd6-#*WxVW(&~v{r?Tx#7n|lOMXx`r ztJZsI!j>MZkH?HoB@WB^yK!1&pBxGZc&JxV#e-@E3gy#M6}apT$PjFXlOovT-`y{*)3d$o4bMjyYAUv);k zT+)+r_g`SuKHRqieRH}svd1v+o2to|+6{V(ECXtH_&&4E6`!(U+)_!$n5>S8E_GdS zvuEJdYKMpKO&^JOOg#3;$?cTc>_xwMJ+Zy>cx=TyLu9sOzZ3TEqX^DpU~g3fHdRm=I6qW)w`6DPWfqT^W$LcdG&or!f3|wyL+HnB z=AI1+-)3HHDa{I<_MZGavAk&QRij?|H<`kht;4ruTUAEp3Ns%aVZ69M81d+7ZAHQ! z@>F8}&a*l`Ek4!!zI3xJj$~Qr*jyyW;Yi<-F8a z|1aL&0xGU%T^ropxHnFa;O@bLy9P~=5Hz?$aDuy&5Fof)f@?@{2@oW>y9Q10;Q#KN z^WF2Ed++@Jomn%(T21d=Rr}TFeX4pvR~<=Y#8PNE(zP&TA*Ie_)}FC>ul4#SJj)!b zoSpknz94{H+x<8KMx;`jN?6)kTwgycTu@5l$1gfTmHdu)Hm6s$87uyVJ&ivp#Mv7& z-en=j=LIKD_9sF-wFgMc<5JAau6;V~dy=?>{h8c>A>}Sl`$p8T{WDG8^&Vnb+Z^Av zZPe!Ea1PmHAoP8B*HJh~7}c+i&}qwT!D(aO ztwfSWuyt_|t8FvCnP|Y)$C>`$4(Y25r4}i{PBVO87rXj8RyiCRO`#IOhpNaGh8d#2 zTGiUlla!wuh}sK(k~k*yR6oy-V{8aNr*uV*aEeV50Ek~Gf1-Evn?eg7Hv3INkvm+* zP*^hkyu^ia=l+?-?rP~8Nr2l4A7(4I7yEV<>MAYlxu}O1h4g&88IRkivOVW7MwcQb zym1)qnquOv0R+17H z^?r^I8%3z(h}mN~jiqHNnUN|VAtR$7;*96FFI^r`Az*}6Uq>X(s{c~+|HvJGpY8Bt z&gbQ;gQZuvhfl*pjDJ-&MOsf2d;gkBd+WwNoIv|9&x6)+al!*4#1h}iAYekRyNk7# zR-_kdrqR;KXcwq&LKPUWe3fzIXfUlhp#-h66K(eYRo{;FrkMi?_O7B%UGZB7NBGYL ziT1{a>g(1L#drZckFzlj3by7hwd?5pQr<^azgqY_Z_Yv;TJ=FEVr72Mw#Tl@A2(T~ zM(ixf9>Kt5Adp-4wpR!uj3xYvVDA^Uiem<&=F$OWzbefObpye<%Ra7U2b0Q&0k-q{ zpRqpaCpsl2s#iX-Uh&wf@q0$cViI|rqrZM1_DhaJzE01rl$O&zQUd9jO6kM1^pw)d zG>3-~(G)O1mClJUs!yl_tuxq^Ao#X%D{Qu5NcUb6BhZmsC;uJ(o8P+ed$~EmRb`{Q ztHT9Ij(*AWQEFrJ-)0i5OCt*^sIbuj%@6oHMU*qXPLZ$(9f?d;^b>47nD;Mve_%0e zfx%1m5MB4$Ft>Jca#nqpD?7cCDQ2bD`nen8sj|_!(rk;NAGgx?1i4QtL7xaY>?g5o zl5qPiEPTiZjQIJ4wQ2pFo+2AH&CSk;xF8I_snKy&@l0U>sf+1J$zMlYxmxb{f8sYLKzgBRmgWw|fspKj$Yu8u@}E#Y>B0oLAdL$7xDz z@Fip^X=NQqv4cl;#hxef*HnABEl3gaVJOc~>;jXT=szE=ax-0~?c@dcc{DHc$TZk7 zP0jnYX%Y{fm|-w!_N&wmMXVhd*-Q4)=zWuKVpV#d1)G|7bz-U*ZDJ`rXF|AT#_Z1b zXsPctS7sv2xOJFg$3H(S8w}1Dia23GEib;;@B3k^PQQTB;os+A#A-L@+bcLcZVfFo z(#g&H`Qgnt?MU_)15)ghAlu`7JDsnwV;b+vczfmBs=dm{aOI7hE}oiTolx;H%9-n+ zC);~a(yg_>lFVyY59jjDnbt5e~&&6E$$np1s95y@q*Z2ENy}{A$(>#~bxS=qn z?GMMVjt(8n4Lr}jkVy<$UN|vw(SW%6q2uKHL>Kzgs7fBGzL z_6mefwqh`w4F*?v2c@!*eLdJD9%PbrYN!*Kls3~>)r0TqN!-~Hfn4N}N zGUMxi`P}W3hHoWqBNln_H)ISDd80E5>4+*#ua7mVJ`bytW*?BO&gIz4eMU}+E*;q0 zhA?~wF^sZO%<|J<;X!`m4m|gG=a_uLwj&l@;hWvdnqp&$AKK@vp!7h0c>!0f!;+#< zw9YQdf0djQ|0sQxLjFlWt7`X7#>7a&e9&q`Jt>oqbx)_!ULxv4wCKc%3pG;?5xctf zA|Vy!sP^YWV*Y*Wj6zQg=9lk3z4*S*Bl)3CHvKGB>^jq^n>D)~T31aaac-@;MUrY+ z?cpqO<~~t3g`eq}P>=DY1}Q^f?|zI_Yl8VWsikM>5$9*A95a3+tf= zCu9w!mhY7}bV|f}Uuz`^`(%xqBSzcwbr*2g6#Jf|3`o&I`8DW#&N~XK8LE^ zrpH70M)&6qI7v9}Bz|bC9F7pPkuVH@(;Rwokygw7&BKhtZIB7c^L?QBvU5!NcZ%~} zvThL`CF^U{W&^Fdu`nf{k9UQB{dDL9)hHF+#dg&=83=d!NGWzPh#DkOETt-2GWm^X zl-8-gmM+#Q*CTLWUJ=Bq%x0lC#Xjam@B`CYt!O;PVaUTxrceAQvsFB@&&a*;b`b&(ZY{)HvwK=T);eT1;$;qY zS0+Ck2(N!f^TULjw(fJaweWQG(c(LMgzkk-zvkALf;RyQ_>G@qHzW_<3|CFr*k3wD zg^#$(B_9>=2{?&F$A(=PODXMp5emYRqqJ$*VSJm%dIA(>G*K>779y$P%3Lx(`ibhb zH-kMLO5T6@$^SgG)wb7hCFTtdT3XBFw3|A?-Q|Q+ibe62E|e@~C=@gEgZWf+v?qU_ z5yHA?TrT;ukkNxZM_bxk?I)cs;!Bt7Y~N)bV*#6CSJ-9-b6Q644>PA5*CeTJkh~$6 z2@?LW`}E;4Y9r_h*4R~(7O~hjn>S=+Zi~U!>C#E#nk*rwxjb27j=tX`1?Cf@-3#pW zgZn<~8Z(*ql!kXMYVX%Pd1fxYe-|oTQ1x@xlNJZ9hJC<3HsB%F3XuLy2&A5|N2Gq43c zk?+Yw+WQb{?H`(1(0VIgM!0mTS+N$2&Sb*s72fwG2L1iIWQ#Txo*%bN}a>< zPgFZh#>tFB{Ef&r+I?mbLy*>})^h0S<`b3Yl)pVsnV!ALVPW$9xAS{v!ViryeEY`I`?abJNt?yCakg

-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�p9EN20Epo#009`| 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
bfx1j3x?$M_Q}m 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 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ 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 literal 0 HcmV?d00001 diff --git a/src/gui.c b/src/gui.c index 33ae133..361be66 100644 --- a/src/gui.c +++ b/src/gui.c @@ -632,13 +632,12 @@ static void create_menu_screen(void) { lbl_autoboot = ui_label_create(cont, settings_get_ps2_autoboot() ? " Yes" : " No"); lv_obj_add_event_cb(cont, evt_ps2_autoboot, LV_EVENT_CLICKED, NULL); - +/* cont = ui_menu_cont_create_nav(ps2_page); ui_label_create_grow_scroll(cont, "Install EXPLOIT.bin"); ui_label_create(cont, " >"); -// ui_menu_set_load_page_event(menu, cont, exploit_install_page); lv_obj_add_event_cb(cont, evt_do_exploit_deploy, LV_EVENT_CLICKED, NULL); - +*/ cont = ui_menu_cont_create_nav(ps2_page); ui_label_create_grow(cont, "Deploy CIV.bin"); diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index cbc6059..ddba293 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -1,6 +1,7 @@ #include "ps2_cardman.h" #include +#include #include #include @@ -11,7 +12,8 @@ #include "hardware/timer.h" -#define CARD_SIZE (8 * 1024 * 1024) + +#define PS2_DEFAULT_CARD_SIZE PS2_CARD_SIZE_8M #define BLOCK_SIZE (512) static uint8_t flushbuf[BLOCK_SIZE]; @@ -24,6 +26,7 @@ static int fd = -1; static int card_idx; static int card_chan; +static uint32_t card_size; static cardman_cb_t cardman_cb; static uint64_t cardprog_start; static size_t cardprog_pos; @@ -239,15 +242,18 @@ void ps2_cardman_open(void) { printf("create new image at %s... ", path); cardprog_start = time_us_64(); - for (size_t pos = 0; pos < CARD_SIZE; pos += BLOCK_SIZE) { - genblock(pos, flushbuf); + for (size_t pos = 0; pos < PS2_DEFAULT_CARD_SIZE; pos += BLOCK_SIZE) { + if (PS2_DEFAULT_CARD_SIZE == PS2_CARD_SIZE_8M) + genblock(pos, flushbuf); + else + memset(flushbuf, 0xFF, sizeof(flushbuf)/sizeof(flushbuf[0])); if (sd_write(fd, flushbuf, BLOCK_SIZE) != BLOCK_SIZE) fatal("cannot init memcard"); psram_write(pos, flushbuf, BLOCK_SIZE); cardprog_pos = pos; if (cardman_cb) - cardman_cb(100 * pos / CARD_SIZE); + cardman_cb(100 * pos / PS2_DEFAULT_CARD_SIZE); } sd_flush(fd); @@ -255,7 +261,7 @@ void ps2_cardman_open(void) { printf("OK!\n"); printf("took = %.2f s; SD write speed = %.2f kB/s\n", (end - cardprog_start) / 1e6, - 1000000.0 * CARD_SIZE / (end - cardprog_start) / 1024); + 1000000.0 * PS2_DEFAULT_CARD_SIZE / (end - cardprog_start) / 1024); } else { cardprog_wr = 0; fd = sd_open(path, O_RDWR); @@ -263,23 +269,31 @@ void ps2_cardman_open(void) { if (fd < 0) fatal("cannot open card"); + card_size = sd_filesize(fd); + if ((card_size != PS2_CARD_SIZE_512K) + && (card_size != PS2_CARD_SIZE_1M) + && (card_size != PS2_CARD_SIZE_2M) + && (card_size != PS2_CARD_SIZE_4M) + && (card_size != PS2_CARD_SIZE_8M)) + fatal("Card %d Channel %d is corrupted", card_idx, card_chan); + /* read 8 megs of card image */ - printf("reading card.... "); + printf("reading card (%lu KB).... ", (uint32_t)(card_size / 1024)); cardprog_start = time_us_64(); - for (size_t pos = 0; pos < CARD_SIZE; pos += BLOCK_SIZE) { + for (size_t pos = 0; pos < card_size; pos += BLOCK_SIZE) { if (sd_read(fd, flushbuf, BLOCK_SIZE) != BLOCK_SIZE) fatal("cannot read memcard"); psram_write(pos, flushbuf, BLOCK_SIZE); cardprog_pos = pos; if (cardman_cb) - cardman_cb(100 * pos / CARD_SIZE); + cardman_cb(100 * pos / card_size); } uint64_t end = time_us_64(); printf("OK!\n"); printf("took = %.2f s; SD read speed = %.2f kB/s\n", (end - cardprog_start) / 1e6, - 1000000.0 * CARD_SIZE / (end - cardprog_start) / 1024); + 1000000.0 * card_size / (end - cardprog_start) / 1024); } } @@ -335,3 +349,7 @@ char *ps2_cardman_get_progress_text(void) { return progress; } + +uint32_t ps2_cardman_get_card_size(void) { + return card_size; +} \ No newline at end of file diff --git a/src/ps2/ps2_cardman.h b/src/ps2/ps2_cardman.h index 5586c73..d6fd745 100644 --- a/src/ps2/ps2_cardman.h +++ b/src/ps2/ps2_cardman.h @@ -1,5 +1,13 @@ #pragma once +#include + +#define PS2_CARD_SIZE_8M (8 * 1024 * 1024) +#define PS2_CARD_SIZE_4M (4 * 1024 * 1024) +#define PS2_CARD_SIZE_2M (2 * 1024 * 1024) +#define PS2_CARD_SIZE_1M (1024 * 1024) +#define PS2_CARD_SIZE_512K (512 * 1024) + void ps2_cardman_init(void); int ps2_cardman_write_sector(int sector, void *buf512); void ps2_cardman_flush(void); @@ -7,6 +15,7 @@ void ps2_cardman_open(void); void ps2_cardman_close(void); int ps2_cardman_get_idx(void); int ps2_cardman_get_channel(void); +uint32_t ps2_cardman_get_card_size(void); void ps2_cardman_next_channel(void); void ps2_cardman_prev_channel(void); @@ -16,4 +25,4 @@ void ps2_cardman_prev_idx(void); typedef void (*cardman_cb_t)(int); void ps2_cardman_set_progress_cb(cardman_cb_t func); -char *ps2_cardman_get_progress_text(void); +char *ps2_cardman_get_progress_text(void); \ No newline at end of file diff --git a/src/ps2/ps2_exploit.c b/src/ps2/ps2_exploit.c index 4cb3e03..4120018 100644 --- a/src/ps2/ps2_exploit.c +++ b/src/ps2/ps2_exploit.c @@ -13,6 +13,7 @@ #include "hardware/sync.h" #include "pico/time.h" #include "pico/types.h" +#include "pico/platform.h" #include "sd.h" #define CARD_SIZE (8 * 1024 * 1024) @@ -23,6 +24,8 @@ static uint64_t exploitprog_start; static size_t exploitprog_pos; static int exploitprog_wr; +extern const char _binary_bootcard_bin_start, _binary_bootcard_bin_size; + void ps2_exploit_init(void) { if ((*(char *)(FLASH_OFF_PS2EXP + XIP_BASE)) != 0xFF) { card_available = true; @@ -108,3 +111,8 @@ char *ps2_exploit_get_deploy_text(void) { bool ps2_exploit_is_available(void) { return (*(char *)(FLASH_OFF_PS2EXP + XIP_BASE)) != 0xFF; } + +void __time_critical_func(ps2_exploit_read)(uint32_t addr, void *buf, size_t sz) { + memcpy((buf + 4), (void*)(&_binary_bootcard_bin_start + addr), sz); +} + diff --git a/src/ps2/ps2_exploit.h b/src/ps2/ps2_exploit.h index 6e14224..ee9d2ae 100644 --- a/src/ps2/ps2_exploit.h +++ b/src/ps2/ps2_exploit.h @@ -2,18 +2,20 @@ #include #include +#include typedef void (*exploit_cb_t)(int); void ps2_exploit_init(void); -void ps2_exploit_read(void); char *ps2_exploit_error(int rc); int ps2_exploit_deploy(void); void ps2_exploit_set_progress_cb(exploit_cb_t func); char *ps2_exploit_get_deploy_text(void); bool ps2_exploit_is_available(void); +void ps2_exploit_read(uint32_t addr, void *buf, size_t sz); + enum { PS2_EXPLOIT_DEPLOY_NOFILE = 1, diff --git a/src/ps2/ps2_memory_card.c b/src/ps2/ps2_memory_card.c index 2e64d8e..7cf27e8 100644 --- a/src/ps2/ps2_memory_card.c +++ b/src/ps2/ps2_memory_card.c @@ -15,6 +15,8 @@ #include "ps2_dirty.h" #include "ps2_psram.h" #include "ps2_pio_qspi.h" +#include "ps2_cardman.h" +#include "ps2_exploit.h" #include #include @@ -56,7 +58,7 @@ static uint8_t hostkey[9]; static inline void __time_critical_func(read_mc)(uint32_t addr, void *buf, size_t sz) { if (flash_mode) { // Skip first 4 Bytes in Buffer, as they are the prefix - memcpy((buf + 4), (void*)(addr + XIP_BASE + FLASH_OFF_PS2EXP), sz); + ps2_exploit_read(addr, buf, sz); ps2_dirty_unlock(); } else { psram_read_dma(addr, buf, sz); diff --git a/src/ps2/ps2_memory_card.in.c b/src/ps2/ps2_memory_card.in.c index ce6c118..f29391d 100644 --- a/src/ps2/ps2_memory_card.in.c +++ b/src/ps2/ps2_memory_card.in.c @@ -1,3 +1,5 @@ +#include +#include #define XOR8(a) (a[0] ^ a[1] ^ a[2] ^ a[3] ^ a[4] ^ a[5] ^ a[6] ^ a[7]) #define ARG8(a) a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7] @@ -67,7 +69,7 @@ if (ch == 0x11) { send(0x2B); recv(); (void)ck; // TODO: validate checksum read_sector = raw.addr; - if (read_sector * 512 + 512 <= CARD_SIZE) { + if (read_sector * 512 + 512 <= ps2_cardman_get_card_size()) { ps2_dirty_lockout_renew(); /* the spinlock will be unlocked by the DMA irq once all data is tx'd */ ps2_dirty_lock(); @@ -84,8 +86,13 @@ if (ch == 0x11) { } else if (ch == 0x26) { /* GET_SPECS ? */ send(0x2B); recv(); + uint32_t sector_count = (flash_mode) ? PS2_CARD_SIZE_1M / 512 : (uint32_t)(ps2_cardman_get_card_size() / 512); uint8_t specs[] = { 0x00, 0x02, ERASE_SECTORS, 0x00, 0x00, 0x40, 0x00, 0x00 }; + specs[4] = (uint8_t)(sector_count & 0xFF); + specs[5] = (uint8_t)((sector_count >> 8) & 0xFF); + specs[6] = (uint8_t)((sector_count >> 16) & 0xFF); + specs[7] = (uint8_t)((sector_count >> 24) & 0xFF); send(specs[0]); recv(); send(specs[1]); recv(); @@ -154,7 +161,7 @@ if (ch == 0x11) { if (readptr == sizeof(readtmp.buf)) { /* a game may read more than one 528-byte sector in a sequence of read ops, e.g. re4 */ ++read_sector; - if (read_sector * 512 + 512 <= CARD_SIZE) { + if (read_sector * 512 + 512 <= ps2_cardman_get_card_size()) { ps2_dirty_lockout_renew(); /* the spinlock will be unlocked by the DMA irq once all data is tx'd */ ps2_dirty_lock(); @@ -210,7 +217,7 @@ if (ch == 0x11) { /* commit for read/write? */ if (is_write) { is_write = 0; - if (write_sector * 512 + 512 <= CARD_SIZE) { + if (write_sector * 512 + 512 <= ps2_cardman_get_card_size()) { ps2_dirty_lockout_renew(); ps2_dirty_lock(); write_mc(write_sector * 512, writetmp, 512); @@ -234,7 +241,7 @@ if (ch == 0x11) { send(term); } else if (ch == 0x82) { /* do erase */ - if (erase_sector * 512 + 512 * ERASE_SECTORS <= CARD_SIZE) { + if (erase_sector * 512 + 512 * ERASE_SECTORS <= ps2_cardman_get_card_size()) { memset(readtmp.buf, 0xFF, 512); ps2_dirty_lockout_renew(); ps2_dirty_lock(); From a47bfe124038a113a0a6494cca4c789d868d30e0 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 29 Jan 2023 17:16:46 +0100 Subject: [PATCH 18/91] Build also develop --- .github/workflows/cmake.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index a46d53c..bad0016 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -2,9 +2,9 @@ name: CMake on: push: - branches: [ "main" ] + branches: [ "main", "develop" ] pull_request: - branches: [ "main" ] + branches: [ "main", "develop" ] env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) From e3b0a824cb82c4ec38af30d7b1e8fd5dbbbc7184 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 30 Jan 2023 06:59:00 +0100 Subject: [PATCH 19/91] Get Bootcard during build --- ps2boot/CMakeLists.txt | 4 ++++ ps2boot/bootcard.bin | Bin 1048576 -> 0 bytes 2 files changed, 4 insertions(+) delete mode 100755 ps2boot/bootcard.bin diff --git a/ps2boot/CMakeLists.txt b/ps2boot/CMakeLists.txt index f8760e9..f54124c 100644 --- a/ps2boot/CMakeLists.txt +++ b/ps2boot/CMakeLists.txt @@ -1,6 +1,10 @@ set(STAGE1_PS2_BOOT "bootcard.bin") set(STAGE1_PS2_BOOT_OBJ "${CMAKE_CURRENT_BINARY_DIR}/bootcard.o") +file(DOWNLOAD "https://github.com/sd2psx/OSDSYS-Launcher/releases/download/latest/first-stage.mcd" SHOW_PROGRESS) + +file(RENAME "first-stage.mcd" "${STAGE1_PS2_BOOT}") + add_custom_command(OUTPUT "${STAGE1_PS2_BOOT_OBJ}" COMMAND ${CMAKE_OBJCOPY} ARGS --input-target=binary --output-target=elf32-littlearm --binary-architecture arm --rename-section .data=.rodata ${STAGE1_PS2_BOOT} ${STAGE1_PS2_BOOT_OBJ} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} diff --git a/ps2boot/bootcard.bin b/ps2boot/bootcard.bin deleted file mode 100755 index d34d8cd40f9939c41d6a9d529274dd8e916521d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1048576 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

-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�p9EN20Epo#009`| zqGr+r+l#^6NT{O%ba9{zFjoQa8v27LINsp7koc3^pcu5@5}-b~4*+N{D2qt~tTVbe zYM44eO9BIEVKSg>O#trC4d@p&cuxl&vkUk6#~=U9|M+)v4+U$z?H(EQlMa>#>JI^r z5RnDz^Itmt?lYW6s1T*VXM)P$*DK&P+%9l@nK~E`_I7yv*W3dTC|wTF`ltRAfX4d( z;LlM8AP>~tg9ns@Tu=?hs0zd+LLw9hNgx2R4L_3s017J+NrC~e7UJ*kUxKxOl!#2V z2KokKAG8nT#CW)Gh(J*#36NLG1|Y`s0?>F&06$&=co5G85XGZ{?SjBVur0q51`xy( z0L1Ym0CGGNKp0OB6jYJ}=PgXKe6Ni0EXfP%4`g}~2S zqc%2$BEvcKE;I?`(>DMf8UQ>-0U&@9J-vc7(20ivkWk5jxub)Wf;pAKMFId+C>_Wd zU=Cz~0H`g46fRs|9%yq85pWk&2-*PZ4S=BlG@Jk_Apiy138-OVP-<8T^!{FojsWZj zX*_`%W(tsi_Q7+A48{f#76j^&2B>wTLHy8gJ_YqbfO?SMXgLu;d||+75g-6I9c>>C z!aV>Lkub&v&*+rmrIv_>u)^d4fD{d2cM5ZBFi0O0-s09GLY>15x$mmq-agZlsh=Q03L%>lV8o)y*w z`YZuM1pQ%!$v_IhasP-n6pXhNFWe@u4xWO!sQm9@3vY+lCMt*pFqiQB!gE&%=B-i& zzytG-TnXm163lreFE|#=c_oOCN)UI;Am)}q%q@e*XMj9iw+vzqQ~?lApfpef4H`f#VG5Cmzy|pk z#M6I?f6)JR{Ehz=fAqh_pTYANM8XLh%me11JS^QyJcZkm1^NQd1z1~ueEa>!Hx354 zkJPYWC_I0(ARZ-19sqDWQcJvqvcfDO@HilY*r57j+y5Tx31D^nXRKoaLX2?C09UpQ zaJ*9i(87BF70fj%A1e&yk8A)i|3B8z)4$1;5V&kO?%=Z7o#42@0kLLU z2m$cu{@`5~@1JB=2!ILl?J)hHSb}l?!yQ((VBLe<0Td?v z;SG>O{>QxG`7hi`{}Z>=PJUeaCPn}PKT00Vd6Gz9VxH7pcLO$Dzn=>+&XlL6KU#tM}l0D^PSnV`T0=ntF+ z+x|HozDAe>ui<<(2G$@eY!dPh|MUOUXDhreLEo>zoZN$-3*eXsKO6AEWy044f8q}E z$G^ut2#^v)0caH%A)Ul%pikf&Lt+Zf_iG?0InW(|Hc%_jfOSv|0M6Lp{2@UA1&FGFu)#M2V#RS9J+K+C+wd( z;JyM-d+_rOs1x)N298Mt`{DEFzihIEz~_p;Y!dt*%s-F*!zL^`RuVHXriFia`o}p#}>-;pR5DIb&Eyyd>FddL5f}j}S zdhE~o4P3jTfm{Wzcl16}P<}l_`z@R&3UdCQE4U1>S~)R5EE!L_mMpE>fcIH-g2;Jmp4;)K1*_}_HF`~K2p^*_)BUwiy# zZeS;ANBB#Z6(|qnauC0*f7Wa8-2X53-UL3X^7{Wj_s%4l>=y_+P}B)Cfz)ImTGZ51 z852aRXc19qtu;na+$xH#Sho>S>DQ1%v}&zO1fp0mAc~3$L_~{gsiJkMCZb^7!=fxA z_y2jGdrv~HJ``Tx{=Q$o|JT7+oS8Fc&a>~&J#W>AE4d0s%|ldLhhyFCy8TOfSXJc- z*i2TWG0&Y*m#mCgDj#V5xnu-c0T_5;uWMp;-}SETGd z%4RI3?PGq7_hgsCHr9{z>UTEkYdM8ll>EdneuMpzD84Uqjw4k@ki#dv`aDL>uN}3P z9FEUDk>9Ukj;P|Tbau7IRBPX4Dy`G1#xFguopd+Cp5CbDwxyIaQ>{l*6rHKoESRg$ zOgL3m9_7!Z(`-HOEgpA^g}M67l#{FS-OASMs1Y}oIW06w#S?oI7*89!)>O@j&NWrV z2;|)W3n4gayg{62CM#R9R!$YA?fXjhrc{qCvIisou2=CZrI3yf_KmJr@rhD+W&-*D z8?3X!qn}3E*)!D|SbZ1K>&&Xgc=Qa)LOr5PwLY%0y4F_q;3V=miZrS@ksj|-dER&z z&DC?uWDT17m~1MGwdk>x=!_fpMObP*MKoK_3ra>-S$b^s)6xrRiC3;e`)tMfVQTVY zf8At}qr*`1Ej8Ct<2~{QbCecU);gcmXSD~g^ZreI$WHc3I%BT}E}TT~e)i z_+71+-jqtOTEC3Aag9qoc9q`jozgpC;sS5ZgLJ>I)+q>gxsJP&Q0Y*6P>9dKIO6^f zyjN>qRc1v}G zz7ji74$3ad^^d9?ns(D}y>B<=f<>VuNYAt=Vu8&=9&Z~ome=Ba?+Z@-y;)(My zKWNqK4mGd-$fXmM{r;$1x_#H=VJmS;!nv9s@RcW55f$X%llCohGk5R3ABlp4gEp z3ZWlYZF7ll=Ml`OluV^3kD`e@mA&(}k)2Grl+E3fPyBQC(v9YQSsd#)>o++CX zeVaG#rp?u^m$p|CS7o~w_Dx5DE3M96hGS2)2%k&wS-mgEEA|Q(PuX8IKxn9HfoNu&~1W^ zH1=#$dY^WY8^c~L(LC(YZKxw^S8=K9g}KCuNUxd)Vg5X36y_$QXj)wc)t2`_d8yAJ z*~~&`k|>T&!gz1DjHdBTt~&<%3^xwI7>To)sszTmo_OD)^wn=SB9GPYyw5d`l3|`Y zOU;8U%7VUU|2W8!m00Cq>?c~@f;eMrY$v1-`+kpL-|u1U`z^-4-$U5i}u3M+Al<7DRWi{=2z4or&E~6m7oMlP}f}SpBGup_A+K)$c;sqs1P3Yt{tJF`6(B4kL}&4-MN`qdWt>sdF)ZN^Emg z--Rgv;+}$?3fLTqKg|7Gu$SF395T$${JC9gsHZOPQ2jwvI~w8Fdvm>CqmCp0AH`@+ zD({rd*$wLZG}@rb8}<28|NRtvZ_(bUTbTeJwiXr9^71088if5>J-@7=_MXMgEL(+} zEKS?X)VvaNQOr?sq7cR&b9+zB(`_5=D@xV6Ec#KAj%QlWL0dz2pr2P|q~@NO*VV_J zv3d?1Z^Yh9(^O>g9HcFTd8nQEn_D|pr9;KH(f^%_a~;}&%9ra@+l6-p6_;|HN2$1| zvK~b}TfDR_Z&7tfopGeK?(CMg@%pJdB9WZYig5;ca0_;*QNN0)|Da;$2D>;`fjFo= zn*&e>Cl65aveFA}%B@gKyO_>8!zF<8zz{{7Iy)Wvx zN~6jb^}WiMS6nY&lx{!DY~g$vw^P2v5z<1p8vRxrYe?)r;asxJxxmJ`Pf`4q3lP8A z_3_5zkza_fbG$0U7M!LyqfjSC;58TdtMY;Kpg202Qab%}P#?1rL-F3m_lM#&j91m} zxy`FKoqV+2Z~Lv}wSBVCHgHapCe`j$KcL#ZYV)cu2-Cf2_x4Pp)pdC^8}D1u@B9U? zbbk?5*C$hiwOdtv9`@jpGivBys#a@X?9CsK@>hF9WA&UpLiaX=X3h8#KqH0oN4S1gBYPD}BlYp~yX_k!Zm2X&b7 z8OM`ad#UDnktKIa8I?j0S^%pgJ)L%T0 zJdIMSr5$ijo@o-J4FH+3%AAMHJib7h;FQTNw0 zhUoo9+CKwv{-Ci0>r~bE>3$EbT=jVvMHA!2xI(f%{neI5@S#S_1ER9@&pxxQl&lc zf4g3tjjH@nZ4%=P`r)b;%CEw2jzfE{YEgB;Tl?0~cb%_#aZ~xO^0^81a3Rhsm$`#= z-nZ*DwK^~Fve7lJsPkEj38>pK`y^dHO`$K*{*`_f%{~Wp`+V#hEy5u4@E530DlQ9k zUUY@0+G7ggB^%pGg{i_v{{Y<-L9W+;!SQUg3KwA}OPfMET=bVX7uMn8thB)$78*K? z!b1!7y_3_iw>*7FS?n5&ZP;fnv=hT9Yp9x|7U5A|D>4u|@;1oJ}W z!%E~w5mmHsd9J``RfZiq)ZC@!{tjB|s&mkG-H)}>Dm90xKwVOEn1v~;QPrn;X-cC$ zC2{UE3Hhemj)lBTx6?v3$0;SN3H_U6Ek%E`sSez2jGa>l5#}{&%)lI}g)A?R5nda@ zYC||}2%` zu81B)--&Uvw8lmsj(oxIMk0UEAL_m^)#}O8sKceLA)IkHWaxgZKl+BM`b^BrL)iJx zP2=W}pVxZsh(1rX_ZY_Yei%!TpH_)8j|vIAYMimU?z3vlKx>^dr5*2a-y{*E73DBfX{dbrG~{^&WGix-jOE zYCaW5eJex%gt1tE_ods4xr*u&)bG=z+No+i4mBnU$v#lEQProY@m%#O`_VMq1+f!} zbS>?VJ2I=R9z(-riTWR6FHzmwT4nVdN@Z2`Kk)5WdG~%$E-9qPPUO=X-9BMsK)7sR zi$fi_!?8x+dq5oKV{R~?sAGPl?8x~C`*(1V?{-(e@LhK|P>NnSggqI=yQ)Hcj>oWm z^yTlM-XmTuRCYS@VG{CSxZc-O@rY2(e1|6CY;64e2u+)hJ6wpvr1@?5O}H$MIzlu~ z&6m*r^c)Cj!JMbA!_7eNUWGCIgSz61S_H5PlS2n)xTtA=iS*OE6I<$R*{zZGJKgpoaW3}t{ z)OVd!J5p^eTi+`~`zz~Tuj>4FjioAn>ihAqU7Q|{x;$FlR~nmiQnpieV7*gT(bA#n z2tH?_Z&wvT<%9a&fq~D51wN+&pAQdwt_pm{+M}EO@qy1O%O1m@%KOQZ}8cFIzDvvA1?oR|5y0G!v7Wi zuke3`|111o;r|N%SNOle{}ukP@PCE>EBs&K{|f(C_`kyc75=aAe}(@m{9ob!3jbI5 zzrz0&{;%+Vh5sx3U*Z1>|5y0G!v7Wiuke3`|111o;r|N%SNOle{}ukP@PCE>EBs&K z{|f(C_`kyc75=aAe}(@m{9ob!3jdc#_`kycZ4XUi|BUeEDEwdeB#kw}ZC{t~O6B8H z`L_H|a)Z;t{}ui({HhB7SNOle{}ukP@&m7UY95<(H+k1JUEEsX{|f(C_`kycjmD0E z@A8Bt{9m8PF8p8N|0>q7@PCE>EBs%~FNOab2llZ0SRZDM<+PT1pZTNd_58`>4>+)5(1df( zFlmVV`&JTaI~!}natgI5 z`H5ltrc{M*A8grp* zk)3cUG_-iq-3WVnqwAhNq?B@IhMlRDqUcP_6B04XnwfB_tUSt}NvGLV7Cn=S$K7Hj zD0^ng$;CQ%wx!vxJ4W1C=Csf#70)Ql)0{@?nN4(P7FpDsC>hzEkBSf+H6KBoXC^CK zu~tqMrS1Dl_NG*iEV2h9|E^c@E2WT*4^igzDn3yP&rC#X={J;i!lR$Y+Ss01jIvPQ zMJSXYs%nf!&!8-)7C#9kiV^o{tQv8?4tZt|P9l$^NTZq)>G@D9&zlcLbFDJxNJ`cq zx?{4bEY?Dqh?DTYRoPWpJF{(t|J_Q(HMLf93WiP+{*G*)G`9Exs;U%VZN(a=|sz0ildV$ z1v%!?Ef&5TiuX3YKNPQ6k0eH89^Xhy-DF1-rRfnj`Di;$qKDn&!d9xG#cpyQSrxR% zO*W>f34H0%^pKmP2f>iT4gC`wiJ zc{H~nIirRSrfS4x?t&;CUmtfSWBoH$&)Fk%Z$p^oH5AXd$X4r>WJSA;HIDsYCe|;N zO`$xxy8&saPo`l|_}`(J@V^_?x;R4D)D>eLMEBs&K{|f(C_`kyc75=aAe}(@m{9ob!3jbI5zrz0&{;%+Vh5sx3U*Z1>|5y0G z!v7Wiuke3`|111o;r|N%SNOle{}ukP@PCE>EBs&K{|f(C_`kyc75=aAe}(@m{9ob! z3jbI5zrz0&{;%+Vh5sx3U*Z1>|5y0G!v7Wiuke3`|111o;r|N%SNOle{}ukP@PCE> zEBs&K{|f(C_`kyc75=aAf0a+dNt)G4v`VpboOW7e;QgeU=xg_w1pHuA zdjDx94mA%fqI(8rP&OERIpEjaH!uc&+zz!YD5CoptchF%SBG;JgyAPVO3e+58x*Fw z4s_aTa@2POR_rp(h^_F$)eIjnb}}dpKFu;0e_+biXn%={%{wvM``uRf3QtB;r1X{E zYG1DORXYARIKCX0J>>?cX>LOOwygAO^@JPOMT9i#CQH-UnZd!kzrm2av z8%iX=SK5U9*zOLWuk3{Idu8Vd{ARm5w{B-Ut@vKChrs7`;lmsj7rMzAX~k87|MW#} z9!;xr;J-SYR(`ADzgqaeZVmj)kA{Ew3glS~`31+`Hz=l55$d+3xlGLq!qI7jw$K31 z6xu?@4VkeuaBz>h@64v56l#cr;}oX5;bVUGf^rHs3?aKAk9swvs82%$?bQ$w{;%+V z>5(?!{|f(C_`kycEpslUoK%%vJa3*|jCn!aS!ss*$L+$)eK}!v7Wiuke2};Ama=zs1f;=qIMyxtObFQUAe&xofs&J3gDp zSy>Ax=;8sb-(b2*bDQEL6IL8^)_COyM4P5IXMw?_s)tuL5t@U!@e!1)OjA`4 zTQOH@H0(v_;RWq#jkmC-TUhEl4r}6Wgr(TB9F_`0hZSuNaaa>!?*oJrp{1CQtGLXj zJX*Ry$yGR8@SVzw;q-?(*X>`@13cOUY$hwxU`WlVOIF6La++QjkIuvV|Ao3T=PYF8 zGj+L@wP3QQiz?{(x-dOmS4zw4;$Q)lIct#LiiP|1d@YL(w1(q-G5D9%8y-2bj9tpwl7;|Zws9k1n*jjf0a z)_F6U@Eff2(rL}-75=aAe}(@$yU}Ae3;(ws{h8)PJ9+4bh5s9-s^rhyRFTRP;r|N% zSNOle|F!7G0g7{KQB@*?g#Ro2U*Z1>|5y0G*feOt+ESmn3jcSG@P8ZIX)wuN%&2&B4uyFyjz{?gB{#hsdo}&*v9Hw=8za<9-&4TZdMVl1L$A-?l2YgFV128& zrEHZBhx)f<(d=_D2RI*lXp6A#@^BLO6jow?sE8_BGJP8>@Y%*bTgMJ{FQMlC4qEDF z;|5y0G>LM5K*9-qwjlloef8Qtm@6FS3+IBJ0_ro4`AM3-c>p7|W zd!PBE>Gk}{5v8NZvil(FCx+1%q#-NEN&^>3$=Ro%?}Z1xwtmd7d#o>*()7=k z6IOqG`XT#8gRfVHUYR?-`haxBi{B)Fy8XhXzZiA-$z$I;X70$wY3ICuZ{?rj^KX3i zm&e?B)A2baXXjjf@!IV_PVc?xw=X8HJYv)c%i>B$Sl~nuA)ihP@gS{ zJnDeWHyo_HF{F6r+rc$2#^*$|S~Ji81;6Us-3gj+zTLGGif^6?&Upg7z6khx+ub9; z*_&FYe7Af5x;XHwEPH4yT!r6Eov+@vM!`{3 zWvKk#hn=Yuja5_exa;6Qvj5<*@TISKlVNZrL*Pp0I`NQ21&R2|{=)wj{x`g$3jbU9 z-@^YE{&xY!k5rLO6K?uTgh=v)^pf*3sWQ!A34BUjQ1Ah zl&JR&`u)g~c;!0q7^{XTS($SrWn<3n&>4R$bASbP75`D~#?5J@p4mi)W`PyboG2OD zohO||snP29Ddx{RwN``*6Q?2!hejpHK39k1Kz1@@Wc5jKxW&=AI$ZVp5IG4IekGl8 zqYA%$$L@un!1_V4-dzd}O*y$p&*?*oo!ORROC~E@k*1m=+B8Djd|@|j=K5*Uc9B12 z&Io#9N2(};~+-?gd6V76DcDnV6S{Nci8v{}%qY@V|xs zE&Ok_lKg-7?|aGr_Ick6IoV~q$A1R@`+y%+2>zS1?f=?qC;#0q@0U3N<-1+}!hh#H z`#<;!Hn#A;GtS%cseRcq`+fP$=4G#(`_i`krrv$x&D(yv@|v%YbDllc z>~sCLlPZtheB>e4ko7++UH(z?SJr(~eqS=CXxq5!wq18kBKhVyy{2CH&}WBly1xF8 zk!Ke4y?$8T=gSuM{_Oa~8An{cSL<&tTQm3afmdEpe{t2)e+&Ox_}{|+7XG*JzlHxT{BPlZ3;$dA-@^YE{gCBBP;Lu`Q_lq`(*-Eg1r@Z{f+5gFnJ*Yhp#|X1w}Cm|2IhPlnDcEl z^ibPqdaw;#`nC$RrI_EA7V5U-F|uC*f61cyk}@zp4?;P{ql)!;G5)54qkT-{Ha9Ww z8@K~vTj_v%g5cj+aXBR9fCY3yHN8(b&}#ca9)cZ>suapbpNacytc>hlG`* z;#T?UPZ{dFn|2ySW_3-L}@V|xsE&T6nDvP!F zW5*dc?n`M$&F2aHI*-j;W$CfQXG8bWbN6+cgG<)bPEN7~rq|AEq721Vjgqa{wl;pF zxWDSN+5<>BHCPXMdn=05`wHA#*dQ&RBYW*_e z#x*YW*j0ML!^K*~OYeY*3#?QPXgx^x`)ZwnaF^@2JDP{Q)l0{~IO6^fyhnK0&#)2y z(9Bu-b2TYjsaVt-w|8O{EADR0=k7kdo1En3h?~lD#oSG{B1^X2S7HasLD?mrzudH& zc7^{f{O`Ck&|1E?O*zpu^tmhiZ{dHNdF{ggPW8li*`j#iiXFZYV}s&`d!K6@B^yol zEbJ2&S(KGvzW8yFB`dMRg?)9RAV{B|E@XfLB_Xzg=9>%`kV(j}pgnhpUvG2DC z`+g5#FWuWmS9^bBC^YMM3eTdwFtqjy(OAlyRpL}*uP#i(DWkpw1ojfJ=3O_GD6*Q% zV-#vhfMdQlH5H8lw;M2`S;-2F9f(V@`duiL2uG`_HETj50)Bgh!u1wCo1n0rD2qM= zy{U6;nx5F^s_|3!-!f75HriJzJhkL9eoj`crFBn6eW#HDg_} zrZGhCH`4wYIFI_Eu>|J~s_)bN-heP(hZ8xW1UbXYbDi2a9n6_CD3r)X zpZf!vinEW@=$8OxOO7P!8N<&?iCmhF9x>yt46QRw%hY!^h3~Q;SMf_aF{>46!`Y^r zY)oUG|A4E~mrM))I~i+2`7Lr&C~M(=lkmTV|BX3b3SA@7mu_oOGS;g1KZO4+{BPlZ z3;!ECin>2g{lnpyo9z6#D)4!D;Pd#vXJzC2^e->8$|}X*aQxNa?=t+|g1?9GM`8T^ z5B&Q+@xPH^;r|N%xA$rL&+qxd;~#A}@yu&C?{!ea=YMUSX+Lq|-1n}nKJlq{XB~9w zEp=CIoBG!A``vWoilUR;x1GF0@0oe?_A7hdZ`URUojB>4saH2&`j1C9FHNkOx4v@j zV-KvIJms2YFMavTMYG0N9sT#&bNVc+{>5HpbB6u+@~c~Ze$Ajsv+r;^{&48)xy!~j zv|gM$<&N{eJn3K6r~U2uS09dCoBv>D32axq1BX)Bd{u zt2b?}-sdN$Iz3N#X!Nu97M$>PZCQQq)9xQMy#D;t9~}1O?X$;P;qz~rbjZ((CYMZo zq2bm2)-M0z?TqIRd~3p6Eq!WlIX)aatL5R-j(?!-ij(V}%w1Wt?49wCZp(i3aN++7 z|Cc^lRd?yKy>5ST%fBuySaR|X;r|N%SNOle{}ukP@PCE>EBs&K{|f(C_`kyc75=aA ze}(@m{9ob!3jbI5zrz0&{;%+Vh5sx3U*Z1>|5y0G!v7Wiuke3`|111o;r|N%SNOle z{}ukP@PCE>EBs&K{|f(C_`kyc75=aAe}(@m{9ob!3jbI5zrz0&{;%+Vh5sx3U*Z1> z|5y0G!v7Wiuke3`|111o;r|N%SNOle{}ukP@PCE>OO(2-EK*I|+*pxARf$!WJ#
bfx1j3x?$M_Q}m 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 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ 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 From 32a9d53e12810260dd76b6a07f03818a701c20a9 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 30 Jan 2023 07:04:48 +0100 Subject: [PATCH 20/91] Fix download --- ps2boot/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ps2boot/CMakeLists.txt b/ps2boot/CMakeLists.txt index f54124c..2eb3ce0 100644 --- a/ps2boot/CMakeLists.txt +++ b/ps2boot/CMakeLists.txt @@ -1,9 +1,9 @@ set(STAGE1_PS2_BOOT "bootcard.bin") set(STAGE1_PS2_BOOT_OBJ "${CMAKE_CURRENT_BINARY_DIR}/bootcard.o") -file(DOWNLOAD "https://github.com/sd2psx/OSDSYS-Launcher/releases/download/latest/first-stage.mcd" SHOW_PROGRESS) - -file(RENAME "first-stage.mcd" "${STAGE1_PS2_BOOT}") +file(DOWNLOAD "https://github.com/sd2psx/OSDSYS-Launcher/releases/download/0.9/first-stage.mcd" + "${CMAKE_CURRENT_SOURCE_DIR}/${STAGE1_PS2_BOOT}" + SHOW_PROGRESS) add_custom_command(OUTPUT "${STAGE1_PS2_BOOT_OBJ}" COMMAND ${CMAKE_OBJCOPY} ARGS --input-target=binary --output-target=elf32-littlearm --binary-architecture arm --rename-section .data=.rodata ${STAGE1_PS2_BOOT} ${STAGE1_PS2_BOOT_OBJ} From 2f52863ee814f10f972075a4445a15d3d1e97245 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 30 Jan 2023 08:41:54 +0100 Subject: [PATCH 21/91] Start implementing text game name --- src/ps2/ps2_cardman.c | 19 +++++++++++++++++++ src/ps2/ps2_cardman.h | 6 +++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index ddba293..57aa795 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -24,10 +24,16 @@ static int fd = -1; #define CHAN_MIN 1 #define CHAN_MAX 8 +#define MAX_GAME_NAME_LENGTH (127) +#define MAX_PREFIX_LENGTH (4) +#define MAX_GAME_ID_LENGTH (16) + static int card_idx; static int card_chan; static uint32_t card_size; static cardman_cb_t cardman_cb; +static char card_game_id[MAX_GAME_ID_LENGTH]; +static const char* card_game_name; static uint64_t cardprog_start; static size_t cardprog_pos; static int cardprog_wr; @@ -44,6 +50,7 @@ void ps2_cardman_init(void) { if (card_chan < CHAN_MIN || card_chan > CHAN_MAX) card_chan = CHAN_MIN; } + memset((void*)card_game_id, 0, MAX_GAME_ID_LENGTH); } int ps2_cardman_write_sector(int sector, void *buf512) { @@ -352,4 +359,16 @@ char *ps2_cardman_get_progress_text(void) { uint32_t ps2_cardman_get_card_size(void) { return card_size; +} + +void ps2_cardman_set_gameid(const char* game_id) { + strlcpy(card_game_id, game_id, MAX_GAME_ID_LENGTH); +} + +const char* ps2_cardman_get_gameid(void) { + return card_game_id; +} + +const char* ps2_cardman_get_gamename(void) { + return card_game_name; } \ No newline at end of file diff --git a/src/ps2/ps2_cardman.h b/src/ps2/ps2_cardman.h index d6fd745..967a136 100644 --- a/src/ps2/ps2_cardman.h +++ b/src/ps2/ps2_cardman.h @@ -25,4 +25,8 @@ void ps2_cardman_prev_idx(void); typedef void (*cardman_cb_t)(int); void ps2_cardman_set_progress_cb(cardman_cb_t func); -char *ps2_cardman_get_progress_text(void); \ No newline at end of file +char *ps2_cardman_get_progress_text(void); + +void ps2_cardman_set_gameid(const char* game_id); +const char* ps2_cardman_get_gameid(void); +const char* ps2_cardman_get_gamename(void); \ No newline at end of file From d90ad3799702aed970bee2369715b79a20704af6 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Tue, 31 Jan 2023 07:02:15 +0100 Subject: [PATCH 22/91] Remove exploit deployment --- src/gui.c | 51 +---------------------- src/ps2/ps2_exploit.c | 94 ------------------------------------------- src/ps2/ps2_exploit.h | 20 --------- 3 files changed, 2 insertions(+), 163 deletions(-) diff --git a/src/gui.c b/src/gui.c index 361be66..c319e95 100644 --- a/src/gui.c +++ b/src/gui.c @@ -192,23 +192,6 @@ static void reload_card_cb(int progress) { gui_tick(); } -static void load_exploit_cb(int progress) { - static lv_point_t line_points[2] = { {0, DISPLAY_HEIGHT/2}, {0, DISPLAY_HEIGHT/2} }; - static int prev_progress; - - progress += 5; - if (progress/5 == prev_progress/5) - return; - printf("Current progress %d\n", progress); - prev_progress = progress; - line_points[1].x = DISPLAY_WIDTH * progress / 100; - lv_line_set_points(g_exploit_bar, line_points, 2); - - lv_label_set_text(g_exploit_text, ps2_exploit_get_deploy_text()); - - gui_tick(); -} - static void evt_scr_main(lv_event_t *event) { if (event->code == LV_EVENT_KEY) { uint32_t key = lv_indev_get_key(lv_indev_get_act()); @@ -330,10 +313,8 @@ static void evt_go_back(lv_event_t *event) { static void evt_ps2_autoboot(lv_event_t *event) { bool current = settings_get_ps2_autoboot(); - if (ps2_exploit_is_available()) { - settings_set_ps2_autoboot(!current); - lv_label_set_text(lbl_autoboot, !current ? "Yes" : "No"); - } + settings_set_ps2_autoboot(!current); + lv_label_set_text(lbl_autoboot, !current ? "Yes" : "No"); lv_event_stop_bubbling(event); } @@ -348,14 +329,6 @@ static void evt_do_civ_deploy(lv_event_t *event) { } } -static void evt_do_exploit_deploy(lv_event_t *event) -{ - (void)event; - - installing_exploit = true; - -} - static void evt_switch_to_ps1(lv_event_t *event) { (void)event; @@ -374,19 +347,6 @@ static void evt_switch_to_ps2(lv_event_t *event) { terminated = 1; } -static void gui_install_exploit() -{ - UI_GOTO_SCREEN(scr_exploit); - - ps2_exploit_set_progress_cb(load_exploit_cb); - - gui_tick(); - - (void)ps2_exploit_deploy(); - ps2_exploit_set_progress_cb(NULL); - UI_GOTO_SCREEN(scr_main); -} - static void create_main_screen(void) { lv_obj_t *lbl; @@ -833,13 +793,6 @@ void gui_task(void) { gui_do_ps2_card_switch(); } - if (installing_exploit && !input_is_any_down()) - { - installing_exploit = false; - gui_install_exploit(); - } - - if (ps2_dirty_activity) { input_flush(); lv_obj_clear_flag(g_activity_frame, LV_OBJ_FLAG_HIDDEN); diff --git a/src/ps2/ps2_exploit.c b/src/ps2/ps2_exploit.c index 4120018..77b3384 100644 --- a/src/ps2/ps2_exploit.c +++ b/src/ps2/ps2_exploit.c @@ -16,102 +16,8 @@ #include "pico/platform.h" #include "sd.h" -#define CARD_SIZE (8 * 1024 * 1024) - -bool card_available = false; -static exploit_cb_t exploit_cb; -static uint64_t exploitprog_start; -static size_t exploitprog_pos; -static int exploitprog_wr; - extern const char _binary_bootcard_bin_start, _binary_bootcard_bin_size; -void ps2_exploit_init(void) { - if ((*(char *)(FLASH_OFF_PS2EXP + XIP_BASE)) != 0xFF) { - card_available = true; - printf("Using exploit card.\n"); - } -} - -char *ps2_exploit_error(int rc) { - switch (rc) { - case 0: return "No error"; - case PS2_EXPLOIT_DEPLOY_NOFILE: return "exploit.bin not\nfound\n"; - case PS2_EXPLOIT_DEPLOY_OPEN: return "Cannot open\nexploit.bin\n"; - case PS2_EXPLOIT_DEPLOY_READ: return "Cannot read\nexploit.bin\n"; - default: return "Unknown error"; - } -} - -int ps2_exploit_deploy(void) { - uint8_t buf[256] = {0}; - - if (!sd_exists("exploit.bin")) - return PS2_EXPLOIT_DEPLOY_NOFILE; - - int fd = sd_open("exploit.bin", O_RDONLY); - if (fd < 0) - return PS2_EXPLOIT_DEPLOY_OPEN; - - if (sd_filesize(fd) == CARD_SIZE) { - size_t erase_offset = FLASH_OFF_PS2EXP; - uint32_t offset = FLASH_OFF_PS2EXP; - - exploitprog_start = time_us_64(); - exploitprog_wr = 0; - - while (erase_offset < FLASH_OFF_PS2EXP + CARD_SIZE) { - uint32_t ints = save_and_disable_interrupts(); - flash_range_erase(erase_offset, 65536); - - restore_interrupts(ints); - - erase_offset += 65536; - - exploitprog_pos = (erase_offset - FLASH_OFF_PS2EXP) / 2; - - if (exploit_cb) - exploit_cb(50 * (erase_offset - FLASH_OFF_PS2EXP) / CARD_SIZE); - } - exploitprog_wr = 1; - - while (sd_read(fd, buf, sizeof(buf)) == sizeof(buf)) { - uint32_t ints = save_and_disable_interrupts(); - flash_range_program(offset, buf, sizeof(buf)); - - restore_interrupts(ints); - exploitprog_pos = (CARD_SIZE + offset - FLASH_OFF_PS2EXP) / 2; - - if (exploit_cb) - exploit_cb(50 + 50 * (offset - FLASH_OFF_PS2EXP) / CARD_SIZE); - offset += 256; - } - exploitprog_wr = -1; - debug_printf("Wrote new exploit image, took %d s\n", (uint32_t)((time_us_64() - exploitprog_start)/1000000)); - } - - sd_close(fd); - return 0; -} - -void ps2_exploit_set_progress_cb(exploit_cb_t func) -{ - exploit_cb = func; -} - - -char *ps2_exploit_get_deploy_text(void) { - static char progress[32]; - - snprintf(progress, sizeof(progress), "%s %.2f kB/s", exploitprog_wr == 0 ? "Er" : "Wr", 1000000.0 * exploitprog_pos / (time_us_64() - exploitprog_start) / 1024); - - return progress; -} - -bool ps2_exploit_is_available(void) { - return (*(char *)(FLASH_OFF_PS2EXP + XIP_BASE)) != 0xFF; -} - void __time_critical_func(ps2_exploit_read)(uint32_t addr, void *buf, size_t sz) { memcpy((buf + 4), (void*)(&_binary_bootcard_bin_start + addr), sz); } diff --git a/src/ps2/ps2_exploit.h b/src/ps2/ps2_exploit.h index ee9d2ae..50f8310 100644 --- a/src/ps2/ps2_exploit.h +++ b/src/ps2/ps2_exploit.h @@ -1,27 +1,7 @@ #pragma once #include -#include #include -typedef void (*exploit_cb_t)(int); - -void ps2_exploit_init(void); -char *ps2_exploit_error(int rc); -int ps2_exploit_deploy(void); -void ps2_exploit_set_progress_cb(exploit_cb_t func); -char *ps2_exploit_get_deploy_text(void); -bool ps2_exploit_is_available(void); - void ps2_exploit_read(uint32_t addr, void *buf, size_t sz); - - -enum { - PS2_EXPLOIT_DEPLOY_NOFILE = 1, - PS2_EXPLOIT_DEPLOY_OPEN, - PS2_EXPLOIT_DEPLOY_READ, -}; - - - From fa2bde3efea5ff0c836bfd1ca64edbcb099902c1 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Tue, 31 Jan 2023 07:02:29 +0100 Subject: [PATCH 23/91] Fix using other cards after boot --- src/ps2/ps2_cardman.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index ddba293..56df6cc 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -66,7 +66,7 @@ void ps2_cardman_flush(void) { static void ensuredirs(void) { char cardpath[32]; - if (settings_get_ps2_autoboot() && ps2_exploit_is_available()) + if (IDX_BOOT == card_idx) snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/BOOT"); else snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/Card%d", card_idx); @@ -219,7 +219,7 @@ void ps2_cardman_open(void) { char path[64]; ensuredirs(); - if (settings_get_ps2_autoboot() && ps2_exploit_is_available()) + if (IDX_BOOT == card_idx) snprintf(path, sizeof(path), "MemoryCards/PS2/BOOT/Card-%d.mcd", card_chan); else { From d087a0d28b7382d3e0212cacc88233e151b85fad Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Tue, 31 Jan 2023 07:02:51 +0100 Subject: [PATCH 24/91] Remove check for exploit available (it's always available) --- src/main.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main.c b/src/main.c index 422432e..3d67ebf 100644 --- a/src/main.c +++ b/src/main.c @@ -109,7 +109,6 @@ int main() { printf("starting in PS2 mode\n"); keystore_init(); - ps2_exploit_init(); psram_init(); sd_init(); ps2_cardman_init(); @@ -118,7 +117,7 @@ int main() { multicore_launch_core1(ps2_memory_card_main); - if (settings_get_ps2_autoboot() && ps2_exploit_is_available()) + if (settings_get_ps2_autoboot()) ps2_memory_card_enter_flash(); printf("Starting memory card... "); From 93e23c2bbfeb1bbab529a328dc3e6ac362df8615 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Tue, 31 Jan 2023 08:56:22 +0100 Subject: [PATCH 25/91] Fix Min Index --- src/ps2/ps2_cardman.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index 56df6cc..7858461 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -323,10 +323,11 @@ void ps2_cardman_next_idx(void) { } void ps2_cardman_prev_idx(void) { + int minIndex = (settings_get_ps2_autoboot() ? IDX_BOOT : IDX_MIN); card_idx -= 1; card_chan = CHAN_MIN; - if (card_idx < IDX_MIN) - card_idx = IDX_MIN; + if (card_idx < minIndex) + card_idx = minIndex; } int ps2_cardman_get_idx(void) { From dfb82860ead4be0c3b4cbb0c12b9f2ab25e7ccb6 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Thu, 2 Feb 2023 06:46:54 +0100 Subject: [PATCH 26/91] Refactor game name stuff in own library --- CMakeLists.txt | 2 + src/game_names/CMakelists.txt | 5 + src/game_names/game_names.c | 167 ++++++++++++++++++++++++++++++++++ src/game_names/game_names.h | 3 + src/ps1/ps1_cardman.c | 129 +------------------------- 5 files changed, 181 insertions(+), 125 deletions(-) create mode 100644 src/game_names/CMakelists.txt create mode 100644 src/game_names/game_names.c create mode 100644 src/game_names/game_names.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ea08e3..a7f188f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ add_subdirectory(ext/lvgl EXCLUDE_FROM_ALL) add_subdirectory(database) add_subdirectory(ps2boot) +add_subdirectory(src/game_names) add_executable(sd2psx @@ -132,6 +133,7 @@ target_link_libraries(sd2psx hardware_dma lvgl::lvgl gamedb + game_names ps2boot ) diff --git a/src/game_names/CMakelists.txt b/src/game_names/CMakelists.txt new file mode 100644 index 0000000..6d50861 --- /dev/null +++ b/src/game_names/CMakelists.txt @@ -0,0 +1,5 @@ +add_library(game_names STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/game_names.c) + +target_include_directories(game_names PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}) \ No newline at end of file diff --git a/src/game_names/game_names.c b/src/game_names/game_names.c new file mode 100644 index 0000000..90e8f62 --- /dev/null +++ b/src/game_names/game_names.c @@ -0,0 +1,167 @@ + +#include "game_names.h" + +#include +#include +#include +#include +#include + +#include "../debug.h" +#include "../sd.h" +#include "../settings.h" + +#define MAX_GAME_NAME_LENGTH (127) +#define MAX_PREFIX_LENGTH (4) +#define MAX_GAME_ID_LENGTH (16) + +extern const char _binary_gamedbps1_dat_start, _binary_gamedbps1_dat_size; +// extern const char _binary_gamedbps2_dat_start, _binary_gamedbps2_dat_size; + +static int card_idx; +static int card_chan; +static char card_game_id[MAX_GAME_ID_LENGTH]; + +static bool game_names_sanity_check_game_id(const char* const game_id) { + uint8_t i = 0U; + + char splittable_game_id[MAX_GAME_ID_LENGTH]; + strlcpy(splittable_game_id, game_id, MAX_GAME_ID_LENGTH); + char* prefix = strtok(splittable_game_id, "-"); + char* id = strtok(NULL, "-"); + + while (prefix[i] != 0x00) { + if (!isalpha((int)prefix[i])) { + return false; + } + i++; + } + if (i == 0) { + return false; + } else { + i = 0; + } + + while (prefix[i] != 0x00) { + if (!isdigit((int)id[i])) { + return false; + } + i++; + } + + return (i > 0); +} + +static uint32_t game_names_char_array_to_uint32(const char in[4]) { +#if BIG_ENDIAN + char inter[4] = {in[3], in[2], in[1], in[0]}; +#else + char* inter = in; +#endif + return *(uint32_t*)inter; +} + +static uint32_t game_names_find_prefix_offset(uint32_t numericPrefix, const char* const db_start) { + uint32_t offset = 0; + + const char* pointer = db_start; + + while (offset == 0) { + uint32_t currentprefix = game_names_char_array_to_uint32(pointer), currentoffset = game_names_char_array_to_uint32(&pointer[4]); + + if (currentprefix == numericPrefix) { + offset = currentoffset; + } + if ((currentprefix == 0U) && (currentoffset == 0U)) { + break; + } + pointer += 8; + } + + return offset; +} + +const char* game_names_get_game_name(const char* const id) { + char prefixString[MAX_PREFIX_LENGTH + 1] = {}; + char idString[10] = {}; + const char* card_game_name = NULL; + + // const char* const db_start = settings_get_mode() == MODE_PS1 ? &_binary_gamedbps1_dat_start : &_binary_gamedbps2_dat_start; + // const char* const db_size = settings_get_mode() == MODE_PS1 ? &_binary_gamedbps1_dat_size : &_binary_gamedbps2_dat_size; + + const char* const db_start = &_binary_gamedbps1_dat_start; + const char* const db_size = &_binary_gamedbps1_dat_size; + + uint32_t numericPrefix = 0, prefixOffset = 0, currentId = 0, numericId = 0; + + if (game_names_sanity_check_game_id(id)) { + char* copy = strdup(id); + char* split = strtok(copy, "-"); + + if (strlen(split) > 0) { + strlcpy(prefixString, split, MAX_PREFIX_LENGTH + 1); + for (uint8_t i = 0; i < MAX_PREFIX_LENGTH; i++) { + prefixString[i] = toupper((unsigned char)prefixString[i]); + } + numericPrefix = game_names_char_array_to_uint32(prefixString); + } + + split = strtok(NULL, "-"); + + if (strlen(split) > 0) { + strlcpy(idString, split, 11); + numericId = atoi(idString); + } + + prefixOffset = game_names_find_prefix_offset(numericPrefix, db_start); + + if (prefixOffset < (size_t)db_size) { + uint32_t offset = prefixOffset; + do { + currentId = game_names_char_array_to_uint32(&(db_start)[offset]); + if (currentId == numericId) { + uint32_t name_offset = game_names_char_array_to_uint32(&(db_start)[offset + 4]); + + debug_printf("Found ID - Name Offset: %d, Parent ID: %d\n", (int)name_offset, + (int)game_names_char_array_to_uint32(&(db_start)[offset + 8])); + + snprintf(card_game_id, MAX_GAME_ID_LENGTH, "%s-%0*d", prefixString, (int)strlen(idString), + (int)game_names_char_array_to_uint32(&(db_start)[offset + 8])); + + debug_printf("Parent ID: %s\n", card_game_id); + + if ((name_offset < (size_t)db_size) && (db_start + name_offset) != 0x00) { + card_game_name = (db_start + name_offset); + debug_printf("Name:%s\n", card_game_name); + + return card_game_name; + } else { + return NULL; + } + } + offset += 12; + } while (currentId != 0); + } + } + + return NULL; +} + +const char* game_names_name_from_file(char* path, size_t size, int channel) { + int dir, it; + char filename[MAX_GAME_NAME_LENGTH] = {}; + + dir = sd_open(path, O_RDONLY); + if (dir >= 0) { + it = sd_iterate_dir(dir, it); + while (it != -1) { + int i = MAX_GAME_NAME_LENGTH - 1; + sd_get_name(it, filename, MAX_GAME_NAME_LENGTH); + while ((filename[i] == 0x0) && (i > 0)) + i--; + if ((i > 4) && (filename[i--] == 't') && (filename[i--] == 'x') && (filename[i--] == 't')) { + + } + } + } +} \ No newline at end of file diff --git a/src/game_names/game_names.h b/src/game_names/game_names.h new file mode 100644 index 0000000..8485640 --- /dev/null +++ b/src/game_names/game_names.h @@ -0,0 +1,3 @@ +#pragma once + +const char* game_names_get_game_name(const char* const id); \ No newline at end of file diff --git a/src/ps1/ps1_cardman.c b/src/ps1/ps1_cardman.c index f5e80eb..32b7b42 100644 --- a/src/ps1/ps1_cardman.c +++ b/src/ps1/ps1_cardman.c @@ -11,6 +11,8 @@ #include "bigmem.h" #include "ps1_empty_card.h" +#include "game_names.h" + #include "hardware/timer.h" #define CARD_SIZE (128 * 1024) @@ -23,137 +25,13 @@ static int fd = -1; #define CHAN_MIN 1 #define CHAN_MAX 8 -#define MAX_GAME_NAME_LENGTH (127) -#define MAX_PREFIX_LENGTH (4) #define MAX_GAME_ID_LENGTH (16) -extern const char _binary_gamedbps1_dat_start, _binary_gamedbps1_dat_size; - static int card_idx; static int card_chan; static char card_game_id[MAX_GAME_ID_LENGTH]; static const char* card_game_name; -static bool ps1_cardman_sanity_check_game_id(const char* const game_id) { - uint8_t i = 0U; - - char splittable_game_id[MAX_GAME_ID_LENGTH]; - strlcpy(splittable_game_id, game_id, MAX_GAME_ID_LENGTH); - char* prefix = strtok(splittable_game_id, "-"); - char* id = strtok(NULL, "-"); - - while (prefix[i] != 0x00) { - if (!isalpha((int)prefix[i])) { - return false; - } - i++; - } - if (i == 0) { - return false; - } else { - i = 0; - } - - while (prefix[i] != 0x00) { - if (!isdigit((int)id[i])) { - return false; - } - i++; - } - - return (i > 0); -} - -static uint32_t ps1_cardman_char_array_to_uint32(const char in[4]) { -#if BIG_ENDIAN - char inter[4] = {in[3], in[2], in[1], in[0]}; -#else - char* inter = in; -#endif - return *(uint32_t*)inter; -} - -static uint32_t ps1_cardman_find_prefix_offset(uint32_t numericPrefix) { - uint32_t offset = 0; - - const char* pointer = &_binary_gamedbps1_dat_start; - - while (offset == 0) { - uint32_t currentprefix = ps1_cardman_char_array_to_uint32(pointer), currentoffset = ps1_cardman_char_array_to_uint32(&pointer[4]); - - if (currentprefix == numericPrefix) { - offset = currentoffset; - } - if ((currentprefix == 0U) && (currentoffset == 0U)) { - break; - } - pointer += 8; - } - - return offset; -} - -static bool ps1_cardman_update_game_data(const char* const id) { - char prefixString[MAX_PREFIX_LENGTH + 1] = {}; - char idString[10] = {}; - - uint32_t numericPrefix = 0, prefixOffset = 0, currentId = 0, numericId = 0; - - if (ps1_cardman_sanity_check_game_id(id)) { - char* copy = strdup(id); - char* split = strtok(copy, "-"); - - if (strlen(split) > 0) { - strlcpy(prefixString, split, MAX_PREFIX_LENGTH + 1); - for (uint8_t i = 0; i < MAX_PREFIX_LENGTH; i++) { - prefixString[i] = toupper((unsigned char)prefixString[i]); - } - numericPrefix = ps1_cardman_char_array_to_uint32(prefixString); - } - - split = strtok(NULL, "-"); - - if (strlen(split) > 0) { - strlcpy(idString, split, 11); - numericId = atoi(idString); - } - - prefixOffset = ps1_cardman_find_prefix_offset(numericPrefix); - - if (prefixOffset < (size_t)&_binary_gamedbps1_dat_size) { - uint32_t offset = prefixOffset; - do { - currentId = ps1_cardman_char_array_to_uint32(&(&_binary_gamedbps1_dat_start)[offset]); - if (currentId == numericId) { - uint32_t name_offset = ps1_cardman_char_array_to_uint32(&(&_binary_gamedbps1_dat_start)[offset + 4]); - - debug_printf("Found ID - Name Offset: %d, Parent ID: %d\n", (int)name_offset, - (int)ps1_cardman_char_array_to_uint32(&(&_binary_gamedbps1_dat_start)[offset + 8])); - - snprintf(card_game_id, MAX_GAME_ID_LENGTH, "%s-%0*d", prefixString, (int)strlen(idString), - (int)ps1_cardman_char_array_to_uint32(&(&_binary_gamedbps1_dat_start)[offset + 8])); - - debug_printf("Parent ID: %s\n", card_game_id); - - if ((name_offset < (size_t)&_binary_gamedbps1_dat_size) && (&_binary_gamedbps1_dat_start + name_offset) != 0x00) { - card_game_name = (&_binary_gamedbps1_dat_start + name_offset); - debug_printf("Name:%s\n", card_game_name); - - return true; - } - else - { - return false; - } - } - offset += 12; - } while (currentId != 0); - } - } - - return false; -} - void ps1_cardman_init(void) { card_idx = settings_get_ps1_card(); if (card_idx < IDX_MIN) @@ -308,7 +186,8 @@ int ps1_cardman_get_channel(void) { } void ps1_cardman_set_gameid(const char* game_id) { - if (!ps1_cardman_update_game_data(game_id)) + card_game_name = game_names_get_game_name(game_id); + if (!card_game_name) { strlcpy(card_game_id, game_id, sizeof(card_game_id)); card_game_name = NULL; From 3e068f138d5f91d0b1596c61c4085e928f6ec6b0 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Thu, 2 Feb 2023 06:47:45 +0100 Subject: [PATCH 27/91] Add additional SD functions to iterate over files / dirs --- src/arduino_wrapper/sd.cpp | 16 ++++++++++++++++ src/sd.h | 5 ++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/arduino_wrapper/sd.cpp b/src/arduino_wrapper/sd.cpp index 643241d..76400c0 100644 --- a/src/arduino_wrapper/sd.cpp +++ b/src/arduino_wrapper/sd.cpp @@ -108,4 +108,20 @@ extern "C" int sd_exists(const char *path) { extern "C" int sd_filesize(int fd) { return files[fd].fileSize();; +} + +extern "C" int sd_iterate_dir(int dir, int it) { + if (it == -1) { + for (it = 0; it < NUM_FILES; ++it) + if (!files[it].isOpen()) + break; + } + if (!files[it].openNext(&files[dir], O_RDONLY)) { + it = -1; + } + return it; +} + +extern "C" size_t sd_get_name(int fd, char* name, size_t size) { + return files[fd].getName(name, size); } \ No newline at end of file diff --git a/src/sd.h b/src/sd.h index c18ab89..0985955 100644 --- a/src/sd.h +++ b/src/sd.h @@ -11,4 +11,7 @@ int sd_write(int fd, void *buf, size_t count); int sd_seek(int fd, uint64_t pos); int sd_filesize(int fd); int sd_mkdir(const char *path); -int sd_exists(const char *path); \ No newline at end of file +int sd_exists(const char *path); + +int sd_iterate_dir(int dir, int it); +size_t sd_get_name(int fd, char* name, size_t size); \ No newline at end of file From 0c052414b8c186825fcf8ef9e0a9f976a3d9628e Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Thu, 2 Feb 2023 06:48:05 +0100 Subject: [PATCH 28/91] Add PS2 database (commented out for now) --- database/CMakeLists.txt | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/database/CMakeLists.txt b/database/CMakeLists.txt index 48a444f..047af72 100644 --- a/database/CMakeLists.txt +++ b/database/CMakeLists.txt @@ -4,6 +4,9 @@ find_package (Python COMPONENTS Interpreter) set(GAMEDB_PS1_BIN "gamedbps1.dat") set(GAMEDB_PS1_OBJ "${CMAKE_CURRENT_BINARY_DIR}/gamedbps1.o") +set(GAMEDB_PS2_BIN "gamedbps2.dat") +set(GAMEDB_PS2_OBJ "${CMAKE_CURRENT_BINARY_DIR}/gamedbps2.o") + add_custom_command(OUTPUT "${GAMEDB_PS1_OBJ}" COMMAND ${Python_EXECUTABLE} ARGS parse_db.py ps1 COMMAND ${CMAKE_OBJCOPY} ARGS --input-target=binary --output-target=elf32-littlearm --binary-architecture arm --rename-section .data=.rodata ${GAMEDB_PS1_BIN} ${GAMEDB_PS1_OBJ} @@ -11,10 +14,23 @@ add_custom_command(OUTPUT "${GAMEDB_PS1_OBJ}" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Generating game db binary for ps1") -add_custom_target(gamedbobjs DEPENDS "${GAMEDB_PS1_OBJ}") +#add_custom_command(OUTPUT "${GAMEDB_PS2_OBJ}" +# COMMAND ${Python_EXECUTABLE} ARGS parse_db.py ps2 +# COMMAND ${CMAKE_OBJCOPY} ARGS --input-target=binary --output-target=elf32-littlearm --binary-architecture arm --rename-section .data=.rodata ${GAMEDB_PS2_BIN} ${GAMEDB_PS2_OBJ} +# COMMAND ${CMAKE_COMMAND} ARGS -E remove_directory ps2 +# WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +# COMMENT "Generating game db binary for ps2") + +add_custom_target(gamedbobjs DEPENDS + "${GAMEDB_PS1_OBJ}" + #"${GAMEDB_PS2_OBJ}" + ) add_library(gamedb INTERFACE) add_dependencies(gamedb gamedbobjs) -target_link_libraries(gamedb INTERFACE ${GAMEDB_PS1_OBJ}) +target_link_libraries(gamedb INTERFACE + ${GAMEDB_PS1_OBJ} + #${GAMEDB_PS2_OBJ} + ) From 9d62c9c9524ffd8644a08851c54b37157a2c25b6 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Thu, 2 Feb 2023 06:48:54 +0100 Subject: [PATCH 29/91] Start adding game name functionality to ps2 --- src/ps2/ps2_cardman.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index c90f948..9ceb142 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -371,5 +371,8 @@ const char* ps2_cardman_get_gameid(void) { } const char* ps2_cardman_get_gamename(void) { + if (!*card_game_name) { + + } return card_game_name; } \ No newline at end of file From caf9b2c4eeb9583235b242a29d98cd9afc54acf0 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Thu, 2 Feb 2023 06:51:13 +0100 Subject: [PATCH 30/91] Remove create exploit screen --- src/gui.c | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/gui.c b/src/gui.c index c319e95..79ef65a 100644 --- a/src/gui.c +++ b/src/gui.c @@ -457,28 +457,6 @@ static void create_cardswitch_screen(void) { lv_label_set_text(g_progress_text, "Read XXX kB/s"); } -static void create_exploit_screen(void) { - printf("Creating Exploit Screen\n"); - - scr_exploit = ui_scr_create(); - - ui_header_create(scr_exploit, "Writing Exploit"); - - static lv_style_t style_progress; - lv_style_init(&style_progress); - lv_style_set_line_width(&style_progress, 12); - lv_style_set_line_color(&style_progress, lv_palette_main(LV_PALETTE_BLUE)); - - g_exploit_bar = lv_line_create(scr_exploit); - lv_obj_set_width(g_exploit_bar, DISPLAY_WIDTH); - lv_obj_add_style(g_exploit_bar, &style_progress, 0); - - g_exploit_text= lv_label_create(scr_exploit); - lv_obj_set_align(g_exploit_text, LV_ALIGN_TOP_LEFT); - lv_obj_set_pos(g_exploit_text, 0, DISPLAY_HEIGHT-9); - lv_label_set_text(g_exploit_text, "Write XXX kB/s"); -} - static void create_switch_nag_screen(void) { scr_switch_nag = ui_scr_create(); @@ -647,7 +625,6 @@ static void create_ui(void) { create_main_screen(); create_menu_screen(); create_cardswitch_screen(); - create_exploit_screen(); create_switch_nag_screen(); create_freepsxboot_screen(); From 0bbb5a3201f018bcb6874e7b3e1956b583d84bde Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Thu, 2 Feb 2023 06:51:41 +0100 Subject: [PATCH 31/91] Remove commented code --- src/gui.c | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/gui.c b/src/gui.c index 79ef65a..6a7548b 100644 --- a/src/gui.c +++ b/src/gui.c @@ -570,13 +570,6 @@ static void create_menu_screen(void) { lbl_autoboot = ui_label_create(cont, settings_get_ps2_autoboot() ? " Yes" : " No"); lv_obj_add_event_cb(cont, evt_ps2_autoboot, LV_EVENT_CLICKED, NULL); -/* - cont = ui_menu_cont_create_nav(ps2_page); - ui_label_create_grow_scroll(cont, "Install EXPLOIT.bin"); - ui_label_create(cont, " >"); - lv_obj_add_event_cb(cont, evt_do_exploit_deploy, LV_EVENT_CLICKED, NULL); -*/ - cont = ui_menu_cont_create_nav(ps2_page); ui_label_create_grow(cont, "Deploy CIV.bin"); ui_label_create(cont, ">"); From 2eb340e1d132fbcae117f2e1dac227c9ab92e180 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Thu, 2 Feb 2023 06:54:27 +0100 Subject: [PATCH 32/91] Simplify settings --- src/settings.c | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/settings.c b/src/settings.c index 6705feb..d14598c 100644 --- a/src/settings.c +++ b/src/settings.c @@ -129,11 +129,7 @@ bool settings_get_ps2_autoboot(void) { } void settings_set_ps2_autoboot(bool autoboot) { - if (((settings.ps2_flags & SETTINGS_FLAGS_AUTOBOOT) != 0) != autoboot) { - if (autoboot) - settings.ps2_flags |= SETTINGS_FLAGS_AUTOBOOT; - else - settings.ps2_flags &= ~SETTINGS_FLAGS_AUTOBOOT; - SETTINGS_UPDATE_FIELD(ps2_flags); - } + if (autoboot != settings_get_ps2_autoboot()) + settings.ps2_flags ^= SETTINGS_FLAGS_AUTOBOOT; + SETTINGS_UPDATE_FIELD(ps2_flags); } \ No newline at end of file From 9be1e611b7645276ab9a27d64fbb33524560bee5 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Thu, 2 Feb 2023 06:54:36 +0100 Subject: [PATCH 33/91] Remove comments and debug --- src/ps2/ps2_memory_card.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/ps2/ps2_memory_card.c b/src/ps2/ps2_memory_card.c index 7cf27e8..e075362 100644 --- a/src/ps2/ps2_memory_card.c +++ b/src/ps2/ps2_memory_card.c @@ -57,7 +57,6 @@ static uint8_t hostkey[9]; static inline void __time_critical_func(read_mc)(uint32_t addr, void *buf, size_t sz) { if (flash_mode) { - // Skip first 4 Bytes in Buffer, as they are the prefix ps2_exploit_read(addr, buf, sz); ps2_dirty_unlock(); } else { @@ -66,13 +65,10 @@ static inline void __time_critical_func(read_mc)(uint32_t addr, void *buf, size_ } static inline void __time_critical_func(write_mc)(uint32_t addr, void *buf, size_t sz) { - debug_printf("Write at address %d size %d\n", addr, sz); - if (!flash_mode) { psram_write(addr, buf, sz); } else { ps2_dirty_unlock(); - debug_printf("Ignore writing to exploit.\n"); } } From aa22b998727bc103cef1f7c73921315fcb910be5 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Thu, 2 Feb 2023 06:54:47 +0100 Subject: [PATCH 34/91] cleanup --- src/gui.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/gui.c b/src/gui.c index 6a7548b..e48824f 100644 --- a/src/gui.c +++ b/src/gui.c @@ -699,9 +699,7 @@ void gui_do_ps2_card_switch(void) { input_flush(); } - void gui_task(void) { - input_update_display(g_navbar); if (settings_get_mode() == MODE_PS1) { From 3c32e6f6310a1c3ec294f887cc6b48623389dca8 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 6 Feb 2023 06:59:56 +0100 Subject: [PATCH 35/91] Change CMake structure --- CMakeLists.txt | 6 +++--- src/game_names/CMakelists.txt | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 src/game_names/CMakelists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index a7f188f..1904252 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,8 +19,6 @@ add_subdirectory(ext/lvgl EXCLUDE_FROM_ALL) add_subdirectory(database) add_subdirectory(ps2boot) -add_subdirectory(src/game_names) - add_executable(sd2psx src/main.c @@ -48,12 +46,15 @@ add_executable(sd2psx src/ps2/ps2_psram.c src/ps2/ps2_exploit.c + src/game_names/game_names.c + src/wear_leveling/wear_leveling.c src/wear_leveling/wear_leveling_rp2040_flash.c src/arduino_wrapper/sd.cpp src/arduino_wrapper/SPI.cpp + ext/ESP8266SdFat/src/common/FmtNumber.cpp ext/ESP8266SdFat/src/common/FsCache.cpp ext/ESP8266SdFat/src/common/FsDateTime.cpp @@ -133,7 +134,6 @@ target_link_libraries(sd2psx hardware_dma lvgl::lvgl gamedb - game_names ps2boot ) diff --git a/src/game_names/CMakelists.txt b/src/game_names/CMakelists.txt deleted file mode 100644 index 6d50861..0000000 --- a/src/game_names/CMakelists.txt +++ /dev/null @@ -1,5 +0,0 @@ -add_library(game_names STATIC - ${CMAKE_CURRENT_SOURCE_DIR}/game_names.c) - -target_include_directories(game_names PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}) \ No newline at end of file From 8acf5f03c2d9869abc4c8e45078f567eff465a9d Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 6 Feb 2023 07:02:33 +0100 Subject: [PATCH 36/91] Update game name processing --- src/game_names/game_names.c | 219 +++++++++++++++++++++++++++--------- src/game_names/game_names.h | 19 +++- 2 files changed, 186 insertions(+), 52 deletions(-) diff --git a/src/game_names/game_names.c b/src/game_names/game_names.c index 90e8f62..945e703 100644 --- a/src/game_names/game_names.c +++ b/src/game_names/game_names.c @@ -1,32 +1,43 @@ #include "game_names.h" + #include #include +#include +#include #include #include #include +#include "pico/platform.h" + + #include "../debug.h" #include "../sd.h" #include "../settings.h" #define MAX_GAME_NAME_LENGTH (127) -#define MAX_PREFIX_LENGTH (4) +#define MAX_PREFIX_LENGTH (5) #define MAX_GAME_ID_LENGTH (16) +#define MAX_STRING_ID_LENGTH (10) +#define MAX_PATH_LENGTH (64) extern const char _binary_gamedbps1_dat_start, _binary_gamedbps1_dat_size; // extern const char _binary_gamedbps2_dat_start, _binary_gamedbps2_dat_size; -static int card_idx; -static int card_chan; -static char card_game_id[MAX_GAME_ID_LENGTH]; +typedef struct { + size_t offset; + uint32_t game_id; + uint32_t parent_id; + const char* name; +} game_lookup; -static bool game_names_sanity_check_game_id(const char* const game_id) { +bool __time_critical_func(game_names_sanity_check_title_id)(const char* const title_id) { uint8_t i = 0U; char splittable_game_id[MAX_GAME_ID_LENGTH]; - strlcpy(splittable_game_id, game_id, MAX_GAME_ID_LENGTH); + strlcpy(splittable_game_id, title_id, MAX_GAME_ID_LENGTH); char* prefix = strtok(splittable_game_id, "-"); char* id = strtok(NULL, "-"); @@ -81,87 +92,193 @@ static uint32_t game_names_find_prefix_offset(uint32_t numericPrefix, const char return offset; } -const char* game_names_get_game_name(const char* const id) { - char prefixString[MAX_PREFIX_LENGTH + 1] = {}; +static game_lookup build_game_lookup(const char* const db_start, const size_t db_size, const size_t offset) { + game_lookup game = {}; + size_t name_offset; + game.game_id = game_names_char_array_to_uint32(&(db_start)[offset]); + game.offset = offset; + game.parent_id = game_names_char_array_to_uint32(&(db_start)[offset + 8]); + name_offset = game_names_char_array_to_uint32(&(db_start)[offset + 4]); + if ((name_offset < db_size) && ((db_start)[name_offset] != 0x00)) + game.name = &((db_start)[name_offset]); + else + game.name = NULL; + + return game; +} + +static bool file_has_extension(const char* const file, const char* const extension) { + int file_index = strlen(file) - 1; + int ext_index = strlen(extension) - 1; + while ((file_index >= 0) && (ext_index >= 0)) + if (file[file_index--] != extension[ext_index--]) + return false; + + return true; +} + +static void file_remove_extension(char* const file) { + int file_index = strlen(file) - 1; + while ((file_index >= 0) && (file[file_index] != '.')) { + file[file_index--] = 0x00; + } + if (file_index >= 0) + file[file_index] = 0x00; +} + +static game_lookup find_game_lookup(const char* game_id) { + char prefixString[MAX_PREFIX_LENGTH] = {}; char idString[10] = {}; - const char* card_game_name = NULL; + uint32_t numeric_id = 0, numeric_prefix = 0; // const char* const db_start = settings_get_mode() == MODE_PS1 ? &_binary_gamedbps1_dat_start : &_binary_gamedbps2_dat_start; // const char* const db_size = settings_get_mode() == MODE_PS1 ? &_binary_gamedbps1_dat_size : &_binary_gamedbps2_dat_size; const char* const db_start = &_binary_gamedbps1_dat_start; - const char* const db_size = &_binary_gamedbps1_dat_size; + const size_t db_size = (size_t)&_binary_gamedbps1_dat_size; + + uint32_t prefixOffset = 0; + game_lookup ret = { + .game_id = 0U, + .parent_id = 0U, + .name = NULL + }; - uint32_t numericPrefix = 0, prefixOffset = 0, currentId = 0, numericId = 0; - if (game_names_sanity_check_game_id(id)) { - char* copy = strdup(id); + if (game_id != NULL && game_id[0]) { + char* copy = strdup(game_id); char* split = strtok(copy, "-"); if (strlen(split) > 0) { - strlcpy(prefixString, split, MAX_PREFIX_LENGTH + 1); - for (uint8_t i = 0; i < MAX_PREFIX_LENGTH; i++) { + strlcpy(prefixString, split, MAX_PREFIX_LENGTH); + for (uint8_t i = 0; i < MAX_PREFIX_LENGTH - 1; i++) { prefixString[i] = toupper((unsigned char)prefixString[i]); } - numericPrefix = game_names_char_array_to_uint32(prefixString); } split = strtok(NULL, "-"); if (strlen(split) > 0) { strlcpy(idString, split, 11); - numericId = atoi(idString); + numeric_id = atoi(idString); } + } + + numeric_prefix = game_names_char_array_to_uint32(prefixString); - prefixOffset = game_names_find_prefix_offset(numericPrefix, db_start); + if (numeric_id != 0) { + + prefixOffset = game_names_find_prefix_offset(numeric_prefix, db_start); - if (prefixOffset < (size_t)db_size) { + if (prefixOffset < db_size) { uint32_t offset = prefixOffset; + game_lookup game; do { - currentId = game_names_char_array_to_uint32(&(db_start)[offset]); - if (currentId == numericId) { - uint32_t name_offset = game_names_char_array_to_uint32(&(db_start)[offset + 4]); + game = build_game_lookup(db_start, db_size, offset); - debug_printf("Found ID - Name Offset: %d, Parent ID: %d\n", (int)name_offset, - (int)game_names_char_array_to_uint32(&(db_start)[offset + 8])); + if (game.game_id == numeric_id) { + ret = game; + debug_printf("Found ID - Name Offset: %d, Parent ID: %d\n", (int)game.name, game.parent_id); + debug_printf("Name:%s\n", game.name); - snprintf(card_game_id, MAX_GAME_ID_LENGTH, "%s-%0*d", prefixString, (int)strlen(idString), - (int)game_names_char_array_to_uint32(&(db_start)[offset + 8])); + } + offset += 12; + } while ((game.game_id != 0) && (offset < db_size) && (ret.game_id == 0)); + } + } - debug_printf("Parent ID: %s\n", card_game_id); + return ret; +} - if ((name_offset < (size_t)db_size) && (db_start + name_offset) != 0x00) { - card_game_name = (db_start + name_offset); - debug_printf("Name:%s\n", card_game_name); +void __time_critical_func(game_names_extract_title_id)(const uint8_t* const in_title_id, char* const out_title_id, const size_t in_title_id_length, const size_t out_buffer_size) { + uint16_t idx_in_title = 0, idx_out_title = 0; - return card_game_name; - } else { - return NULL; - } - } - offset += 12; - } while (currentId != 0); + while ( (in_title_id[idx_in_title] != 0x00) + && (idx_in_title < in_title_id_length) + && (idx_out_title < out_buffer_size) ) { + if ((in_title_id[idx_in_title] == ';') || (in_title_id[idx_in_title] == 0x00)) { + out_title_id[idx_out_title++] = 0x00; + break; + } else if ((in_title_id[idx_in_title] == '\\') || (in_title_id[idx_in_title] == '/') || (in_title_id[idx_in_title] == ':')) { + idx_out_title = 0; + } else if (in_title_id[idx_in_title] == '_') { + out_title_id[idx_out_title++] = '-'; + } else if (in_title_id[idx_in_title] != '.') { + out_title_id[idx_out_title++] = in_title_id[idx_in_title]; + } else { } + idx_in_title++; } +} + +void game_names_get_name_by_folder(const char* const folder, char* const game_name) { + if (game_names_sanity_check_title_id(folder)) { + game_lookup game; + + game = find_game_lookup(folder); - return NULL; + if (game.game_id != 0) { + strlcpy(game_name, game.name, MAX_GAME_NAME_LENGTH); + } + } else { + int dir_fd, it_fd = -1; + char filename[MAX_GAME_NAME_LENGTH] = {}; + char dir[64]; + if (settings_get_mode() == MODE_PS1) { + snprintf(dir, sizeof(dir), "MemoryCards/PS1/%s", folder); + } else { + snprintf(dir, sizeof(dir), "MemoryCards/PS2/%s", folder); + } + + dir_fd = sd_open(dir, O_RDONLY); + if (dir_fd >= 0) { + it_fd = sd_iterate_dir(dir_fd, it_fd); + + while (it_fd != -1) { + sd_get_name(it_fd, filename, MAX_GAME_NAME_LENGTH); + + if (file_has_extension(filename, ".txt")) { + file_remove_extension(filename); + strlcpy(game_name, filename, MAX_GAME_NAME_LENGTH); + break; + } + it_fd = sd_iterate_dir(dir_fd, it_fd); + } + if (it_fd != -1) + sd_close(it_fd); + sd_close(dir_fd); + } + } } -const char* game_names_name_from_file(char* path, size_t size, int channel) { - int dir, it; - char filename[MAX_GAME_NAME_LENGTH] = {}; +void game_names_get_parent(const char* const game_id, char* const parent_id) { + char prefixString[MAX_PREFIX_LENGTH] = {}; + char idString[10] = {}; + game_lookup game; - dir = sd_open(path, O_RDONLY); - if (dir >= 0) { - it = sd_iterate_dir(dir, it); - while (it != -1) { - int i = MAX_GAME_NAME_LENGTH - 1; - sd_get_name(it, filename, MAX_GAME_NAME_LENGTH); - while ((filename[i] == 0x0) && (i > 0)) - i--; - if ((i > 4) && (filename[i--] == 't') && (filename[i--] == 'x') && (filename[i--] == 't')) { + if (game_id != NULL && game_id[0]) { + char* copy = strdup(game_id); + char* split = strtok(copy, "-"); + if (strlen(split) > 0) { + strlcpy(prefixString, split, MAX_PREFIX_LENGTH); + for (uint8_t i = 0; i < MAX_PREFIX_LENGTH - 1; i++) { + prefixString[i] = toupper((unsigned char)prefixString[i]); } } + split = strtok(NULL, "-"); + + if (strlen(split) > 0) { + strlcpy(idString, split, 11); + } } -} \ No newline at end of file + + game = find_game_lookup(game_id); + + if (game.game_id != 0) { + snprintf(parent_id, MAX_GAME_ID_LENGTH, "%s-%0*d", prefixString, (int)strlen(idString), (int)game.parent_id); + + debug_printf("Parent ID: %s\n", parent_id); + } +} + diff --git a/src/game_names/game_names.h b/src/game_names/game_names.h index 8485640..a1dedfb 100644 --- a/src/game_names/game_names.h +++ b/src/game_names/game_names.h @@ -1,3 +1,20 @@ #pragma once -const char* game_names_get_game_name(const char* const id); \ No newline at end of file +#include +#include +#include + +void game_names_extract_title_id(const uint8_t* const in_title_id, char* const out_title_id, const size_t in_title_id_length, const size_t out_buffer_size); +bool game_names_sanity_check_title_id(const char* const title_id); + +void game_names_get_name_by_folder(const char* const folder, char* const game_name); +void game_names_get_parent(const char* const game_id, char* const parent_id); + +void game_names_update_card_dir(const char* const dir); +void game_names_init(void); + +void game_names_set_game_id(const char* const game_id); +const char* game_names_get_game_id(void); + +const char* game_names_get_game_name(void); +const char* game_names_name_from_file(char* path, int channel); From 110c730f726a4173bb61c98f0cb88f174f91ac1a Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 6 Feb 2023 07:02:49 +0100 Subject: [PATCH 37/91] Fix ODE Man --- src/ps1/ps1_odeman.c | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/ps1/ps1_odeman.c b/src/ps1/ps1_odeman.c index 0bc0e41..c21cd44 100644 --- a/src/ps1/ps1_odeman.c +++ b/src/ps1/ps1_odeman.c @@ -5,15 +5,12 @@ #include "ps1_odeman.h" #include "debug.h" +#include #include #include #define CARD_SWITCH_DELAY_MS (250) - - -void ps1_odeman_init(void) { - -} +#define MAX_GAME_ID_LENGTH (16) void ps1_odeman_task(void) { uint8_t ode_command = ps1_memory_card_get_ode_command(); @@ -28,7 +25,7 @@ void ps1_odeman_task(void) { const char *game_id; game_id = ps1_memory_card_get_game_id(); debug_printf("Received Game ID: %s\n", game_id); - ps1_cardman_set_gameid(game_id); + ps1_cardman_set_ode_idx(game_id); break; } case MCP_NXT_CARD: @@ -54,10 +51,8 @@ void ps1_odeman_task(void) { sleep_ms(CARD_SWITCH_DELAY_MS); // This delay is required, so ODE can register the card change - ps1_cardman_open(); ps1_memory_card_enter(); gui_request_refresh(); } - -} \ No newline at end of file +} From e6785b0863ffdbc7e06e73a1868f24f8dceed8a5 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 6 Feb 2023 07:03:11 +0100 Subject: [PATCH 38/91] Store folder name in cardman --- src/ps1/ps1_cardman.c | 77 +++++++++++++++++++++++-------------------- src/ps1/ps1_cardman.h | 6 ++-- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/ps1/ps1_cardman.c b/src/ps1/ps1_cardman.c index 32b7b42..06ce32f 100644 --- a/src/ps1/ps1_cardman.c +++ b/src/ps1/ps1_cardman.c @@ -1,6 +1,7 @@ #include "ps1_cardman.h" #include +#include #include #include #include @@ -11,7 +12,7 @@ #include "bigmem.h" #include "ps1_empty_card.h" -#include "game_names.h" +#include "game_names/game_names.h" #include "hardware/timer.h" @@ -29,8 +30,7 @@ static int fd = -1; static int card_idx; static int card_chan; -static char card_game_id[MAX_GAME_ID_LENGTH]; -static const char* card_game_name; +static char folder_name[MAX_GAME_ID_LENGTH]; void ps1_cardman_init(void) { card_idx = settings_get_ps1_card(); @@ -39,7 +39,8 @@ void ps1_cardman_init(void) { card_chan = settings_get_ps1_channel(); if (card_chan < CHAN_MIN || card_chan > CHAN_MAX) card_chan = CHAN_MIN; - memset(card_game_id, 0, sizeof(card_game_id)); + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); + } int ps1_cardman_write_sector(int sector, void *buf512) { @@ -62,12 +63,9 @@ void ps1_cardman_flush(void) { static void ensuredirs(void) { char cardpath[32]; - if ((card_game_id[0] != 0x00) && (card_idx < IDX_MIN) ) { - snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS1/%s", card_game_id); - } else { - snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS1/Card%d", card_idx); - } - + + snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS1/%s", folder_name); + sd_mkdir("MemoryCards"); sd_mkdir("MemoryCards/PS1"); sd_mkdir(cardpath); @@ -85,12 +83,10 @@ static void genblock(size_t pos, void *buf) { void ps1_cardman_open(void) { char path[64]; - ensuredirs(); - if ((card_game_id[0] != 0x00) && (card_idx < IDX_MIN)) { - snprintf(path, sizeof(path), "MemoryCards/PS1/%s/%s-%d.mcd", card_game_id, card_game_id, card_chan); - } else { - snprintf(path, sizeof(path), "MemoryCards/PS1/Card%d/Card%d-%d.mcd", card_idx, card_idx, card_chan); + + snprintf(path, sizeof(path), "MemoryCards/PS1/%s/%s-%d.mcd", folder_name, folder_name, card_chan); + if (card_idx != IDX_GAMEID) { /* this is ok to do on every boot because it wouldn't update if the value is the same as currently stored */ settings_set_ps1_card(card_idx); settings_set_ps1_channel(card_chan); @@ -165,16 +161,35 @@ void ps1_cardman_prev_channel(void) { void ps1_cardman_next_idx(void) { card_idx += 1; card_chan = CHAN_MIN; + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); + + if (card_idx == IDX_GAMEID) { + const char* const game_id = ps1_memory_card_get_game_id(); + if ((game_id != NULL) && (game_id[0])) + snprintf(folder_name, sizeof(folder_name), "%s", game_id); + else + card_idx = 1; + } + } void ps1_cardman_prev_idx(void) { card_idx -= 1; card_chan = CHAN_MIN; - if ((card_idx == IDX_GAMEID) - && (card_game_id[0] == 0x00)) - card_idx = IDX_MIN; - else if (card_idx < IDX_GAMEID) + if (card_idx < IDX_GAMEID) card_idx = IDX_GAMEID; + + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); + + if (card_idx == IDX_GAMEID) { + char parent_id[MAX_GAME_ID_LENGTH]; + const char* const game_id = ps1_memory_card_get_game_id(); + game_names_get_parent(game_id, parent_id); + if ((game_id != NULL) && (game_id[0])) + snprintf(folder_name, sizeof(folder_name), "%s", parent_id); + else + card_idx = IDX_MIN; + } } int ps1_cardman_get_idx(void) { @@ -185,26 +200,16 @@ int ps1_cardman_get_channel(void) { return card_chan; } -void ps1_cardman_set_gameid(const char* game_id) { - card_game_name = game_names_get_game_name(game_id); - if (!card_game_name) - { - strlcpy(card_game_id, game_id, sizeof(card_game_id)); - card_game_name = NULL; - card_idx = IDX_MIN; - card_chan = CHAN_MIN; - } - else - { +void ps1_cardman_set_ode_idx(const char* const card_game_id) { + if (card_game_id[0]) { + char parent_id[MAX_GAME_ID_LENGTH]; + game_names_get_parent(card_game_id, parent_id); + snprintf(folder_name, sizeof(folder_name), "%s", parent_id); card_idx = IDX_GAMEID; card_chan = CHAN_MIN; } } -const char* ps1_cardman_get_gameid(void) { - return card_game_id; -} - -const char* ps1_cardman_get_gamename(void) { - return card_game_name; +const char* ps1_cardman_get_folder_name(void) { + return folder_name; } \ No newline at end of file diff --git a/src/ps1/ps1_cardman.h b/src/ps1/ps1_cardman.h index 16f3c3f..9ac14d4 100644 --- a/src/ps1/ps1_cardman.h +++ b/src/ps1/ps1_cardman.h @@ -7,12 +7,10 @@ void ps1_cardman_open(void); void ps1_cardman_close(void); int ps1_cardman_get_idx(void); int ps1_cardman_get_channel(void); +const char* ps1_cardman_get_folder_name(void); void ps1_cardman_next_channel(void); void ps1_cardman_prev_channel(void); void ps1_cardman_next_idx(void); void ps1_cardman_prev_idx(void); - -void ps1_cardman_set_gameid(const char* game_id); -const char* ps1_cardman_get_gameid(void); -const char* ps1_cardman_get_gamename(void); +void ps1_cardman_set_ode_idx(const char* const card_game_id); From e3e77e9bce3ab1e8712c209da13280962f1ea872 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 6 Feb 2023 07:04:12 +0100 Subject: [PATCH 39/91] Move Game ID processing stuff into game names --- src/ps1/ps1_memory_card.c | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/src/ps1/ps1_memory_card.c b/src/ps1/ps1_memory_card.c index f0a3f48..bd44e5a 100644 --- a/src/ps1/ps1_memory_card.c +++ b/src/ps1/ps1_memory_card.c @@ -2,14 +2,15 @@ #include "hardware/timer.h" #include "pico/platform.h" #include "string.h" +#include #include "config.h" #include "ps1_mc_spi.pio.h" #include "debug.h" #include "bigmem.h" #include "ps1_dirty.h" -#include -#include +#include "ps1/ps1_memory_card.h" +#include "game_names/game_names.h" #define card_image bigmem.ps1.card_image @@ -34,30 +35,7 @@ static pio_t cmd_reader, dat_writer; static volatile int mc_exit_request, mc_exit_response, mc_enter_request, mc_enter_response; -static void __time_critical_func(clean_title_id)(const uint8_t* const in_title_id, char* const out_title_id, const size_t in_title_id_length, const size_t out_buffer_size) { - uint16_t idx_in_title = 0, idx_out_title = 0; - - while ( (in_title_id[idx_in_title] != 0x00) - && (idx_in_title < in_title_id_length) - && (idx_out_title < out_buffer_size) ) { - if ((in_title_id[idx_in_title] == ';') || (in_title_id[idx_in_title] == 0x00)) { - out_title_id[idx_out_title++] = 0x00; - break; - } else if ((in_title_id[idx_in_title] == '\\') || (in_title_id[idx_in_title] == '/') || (in_title_id[idx_in_title] == ':')) { - idx_out_title = 0; - } else if (in_title_id[idx_in_title] == '_') { - out_title_id[idx_out_title++] = '-'; - } else if (in_title_id[idx_in_title] != '.') { - out_title_id[idx_out_title++] = in_title_id[idx_in_title]; - } else { - } - idx_in_title++; - } -} - static void __time_critical_func(reset_pio)(void) { - // debug_printf("!!\n"); - pio_set_sm_mask_enabled(pio0, (1 << cmd_reader.sm) | (1 << dat_writer.sm), false); pio_restart_sm_mask(pio0, (1 << cmd_reader.sm) | (1 << dat_writer.sm)); @@ -196,8 +174,11 @@ static int __time_critical_func(mc_do_state)(uint8_t ch) { } else if (cmd == 0x21) { // MCP Game ID if (byte_count == game_id_length + 4) { - clean_title_id(&payload[4], received_game_id, game_id_length, sizeof(received_game_id)); - mc_pro_command = MCP_GAME_ID; + game_names_extract_title_id(&payload[4], received_game_id, game_id_length, sizeof(received_game_id)); + if (game_names_sanity_check_title_id(received_game_id)) + mc_pro_command = MCP_GAME_ID; + else + memset(received_game_id, 0, sizeof(received_game_id)); } switch (byte_count) { case 2: memset(received_game_id, 0, sizeof(received_game_id)); return 0x00; @@ -369,8 +350,6 @@ void ps1_memory_card_enter(void) { mc_enter_request = mc_enter_response = 0; memcard_running = 1; mc_pro_command = 0; - game_id_length = sizeof(received_game_id); - memset(received_game_id, 0, sizeof(received_game_id)); } void ps1_memory_card_reset_ode_command(void) { From 1b47bc46e8ff7c599989e78f730c2051f66f1957 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 6 Feb 2023 07:04:33 +0100 Subject: [PATCH 40/91] Update name handling --- src/gui.c | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/gui.c b/src/gui.c index c319e95..ae1775f 100644 --- a/src/gui.c +++ b/src/gui.c @@ -1,5 +1,6 @@ #include "gui.h" +#include #include #include @@ -739,29 +740,35 @@ void gui_task(void) { static int displayed_card_channel = -1; static char card_idx_s[8]; static char card_channel_s[8]; + char card_name[127]; + const char* folder_name = NULL; if (displayed_card_idx != ps1_cardman_get_idx() || displayed_card_channel != ps1_cardman_get_channel() || refresh_gui) { displayed_card_idx = ps1_cardman_get_idx(); displayed_card_channel = ps1_cardman_get_channel(); + folder_name = ps1_cardman_get_folder_name(); + snprintf(card_channel_s, sizeof(card_channel_s), "%d", displayed_card_channel); - if (displayed_card_idx == 0) { - const char* id = ps1_cardman_get_gameid(); - const char* name = ps1_cardman_get_gamename(); - lv_label_set_text(scr_main_idx_lbl, id); - if (name[0] != 0x00) - { - lv_label_set_text(src_main_title_lbl, name); - } - else - { - lv_label_set_text(src_main_title_lbl, ""); - } - } - else { + + if (displayed_card_idx > 0) { snprintf(card_idx_s, sizeof(card_idx_s), "%d", displayed_card_idx); lv_label_set_text(scr_main_idx_lbl, card_idx_s); + } else { + lv_label_set_text(scr_main_idx_lbl, folder_name); + } + + memset(card_name, 0, sizeof(card_name)); + game_names_get_name_by_folder(folder_name, card_name); + + if (card_name[0]) + { + lv_label_set_text(src_main_title_lbl, card_name); + } + else + { lv_label_set_text(src_main_title_lbl, ""); } + lv_label_set_text(scr_main_channel_lbl, card_channel_s); } From ce531e825bfa85a53e07e4235ce8c54af4a43d1f Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 6 Feb 2023 08:09:26 +0100 Subject: [PATCH 41/91] Switch bootcard to latest selected card on button press Only use one bootcard channel named "BootCard.mcb" --- src/ps2/ps2_cardman.c | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index 7858461..b2baf6a 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -220,7 +220,7 @@ void ps2_cardman_open(void) { ensuredirs(); if (IDX_BOOT == card_idx) - snprintf(path, sizeof(path), "MemoryCards/PS2/BOOT/Card-%d.mcd", card_chan); + snprintf(path, sizeof(path), "MemoryCards/PS2/BOOT/BootCard.mcd"); else { snprintf(path, sizeof(path), "MemoryCards/PS2/Card%d/Card%d-%d.mcd", card_idx, card_idx, card_chan); @@ -306,28 +306,42 @@ void ps2_cardman_close(void) { } void ps2_cardman_next_channel(void) { - card_chan += 1; - if (card_chan > CHAN_MAX) - card_chan = CHAN_MIN; + if (card_idx != IDX_BOOT) { + card_chan += 1; + if (card_chan > CHAN_MAX) + card_chan = CHAN_MIN; + } } void ps2_cardman_prev_channel(void) { - card_chan -= 1; - if (card_chan < CHAN_MIN) - card_chan = CHAN_MAX; + if (card_idx != IDX_BOOT) { + card_chan -= 1; + if (card_chan < CHAN_MIN) + card_chan = CHAN_MAX; + } } void ps2_cardman_next_idx(void) { - card_idx += 1; - card_chan = CHAN_MIN; + if (card_idx != IDX_BOOT) { + card_idx += 1; + card_chan = CHAN_MIN; + } else { + card_idx = settings_get_ps2_card(); + card_chan = settings_get_ps2_channel(); + } } void ps2_cardman_prev_idx(void) { - int minIndex = (settings_get_ps2_autoboot() ? IDX_BOOT : IDX_MIN); - card_idx -= 1; - card_chan = CHAN_MIN; - if (card_idx < minIndex) - card_idx = minIndex; + if (card_idx != IDX_BOOT) { + int minIndex = (settings_get_ps2_autoboot() ? IDX_BOOT : IDX_MIN); + card_idx -= 1; + card_chan = CHAN_MIN; + if (card_idx < minIndex) + card_idx = minIndex; + } else { + card_idx = settings_get_ps2_card(); + card_chan = settings_get_ps2_channel(); + } } int ps2_cardman_get_idx(void) { From 844fc0efe35307caa559e979656ce763126ad410 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 6 Feb 2023 17:24:53 +0100 Subject: [PATCH 42/91] Disable channels for boot card --- src/gui.c | 46 +++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/src/gui.c b/src/gui.c index e48824f..7959f7f 100644 --- a/src/gui.c +++ b/src/gui.c @@ -23,11 +23,11 @@ #include "ui_theme_mono.h" /* Displays the line at the bottom for long pressing buttons */ -static lv_obj_t *g_navbar, *g_progress_bar, *g_exploit_bar, *g_progress_text, *g_exploit_text, *g_activity_frame; +static lv_obj_t *g_navbar, *g_progress_bar, *g_progress_text, *g_activity_frame; -static lv_obj_t *scr_switch_nag, *scr_card_switch, *scr_exploit, *scr_main, *scr_menu, *scr_freepsxboot, *menu, *main_page; +static lv_obj_t *scr_switch_nag, *scr_card_switch, *scr_main, *scr_menu, *scr_freepsxboot, *menu, *main_page; static lv_style_t style_inv; -static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl,*src_main_title_lbl, *lbl_civ_err, *lbl_autoboot, *lbl_autoboot; +static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl,*src_main_title_lbl, *lbl_civ_err, *lbl_autoboot, *lbl_channel; static int have_oled; static int switching_card; @@ -209,10 +209,11 @@ static void evt_scr_main(lv_event_t *event) { // TODO: if there was a card op recently (1s timeout?), should refuse to switch // TODO: ps1 support here if (key == INPUT_KEY_PREV || key == INPUT_KEY_NEXT || key == INPUT_KEY_BACK || key == INPUT_KEY_ENTER) { + uint8_t prevChannel, prevIdx; if (settings_get_mode() == MODE_PS1) { - ps1_memory_card_exit(); - ps1_cardman_close(); - + prevChannel = ps1_cardman_get_channel(); + prevIdx = ps1_cardman_get_idx(); + switch (key) { case INPUT_KEY_PREV: ps1_cardman_prev_channel(); @@ -227,11 +228,15 @@ static void evt_scr_main(lv_event_t *event) { ps1_cardman_next_idx(); break; } - - printf("new PS1 card=%d chan=%d\n", ps1_cardman_get_idx(), ps1_cardman_get_channel()); + if ((prevChannel != ps1_cardman_get_channel()) || (prevIdx != ps1_cardman_get_idx())) { + ps1_memory_card_exit(); + ps1_cardman_close(); + switching_card = 1; + printf("new PS1 card=%d chan=%d\n", ps1_cardman_get_idx(), ps1_cardman_get_channel()); + } } else { - ps2_memory_card_exit(); - ps2_cardman_close(); + prevChannel = ps2_cardman_get_channel(); + prevIdx = ps2_cardman_get_idx(); switch (key) { case INPUT_KEY_PREV: @@ -248,10 +253,17 @@ static void evt_scr_main(lv_event_t *event) { break; } - printf("new PS2 card=%d chan=%d\n", ps2_cardman_get_idx(), ps2_cardman_get_channel()); + if ((prevChannel != ps2_cardman_get_channel()) || (prevIdx != ps2_cardman_get_idx())) { + ps2_memory_card_exit(); + ps2_cardman_close(); + switching_card = 1; + printf("new PS2 card=%d chan=%d\n", ps2_cardman_get_idx(), ps2_cardman_get_channel()); + } + } + + if (switching_card == 1) { + switching_card_timeout = time_us_64() + 1500 * 1000; } - switching_card = 1; - switching_card_timeout = time_us_64() + 1500 * 1000; } } @@ -369,7 +381,7 @@ static void create_main_screen(void) { scr_main_idx_lbl = ui_label_create_at(scr_main, 0, 24, ""); lv_obj_set_align(scr_main_idx_lbl, LV_ALIGN_TOP_RIGHT); - ui_label_create_at(scr_main, 0, 32, "Channel"); + lbl_channel = ui_label_create_at(scr_main, 0, 32, "Channel"); scr_main_channel_lbl = ui_label_create_at(scr_main, 0, 32, ""); lv_obj_set_align(scr_main_channel_lbl, LV_ALIGN_TOP_RIGHT); @@ -748,10 +760,14 @@ void gui_task(void) { displayed_card_channel = ps2_cardman_get_channel(); if (displayed_card_idx == 0) { snprintf(card_idx_s, sizeof(card_idx_s), "BOOT"); + snprintf(card_channel_s, sizeof(card_channel_s), " "); + lv_label_set_text(lbl_channel, ""); + } else { snprintf(card_idx_s, sizeof(card_idx_s), "%d", displayed_card_idx); + snprintf(card_channel_s, sizeof(card_channel_s), "%d", displayed_card_channel); + lv_label_set_text(lbl_channel, "Channel"); } - snprintf(card_channel_s, sizeof(card_channel_s), "%d", displayed_card_channel); lv_label_set_text(scr_main_idx_lbl, card_idx_s); lv_label_set_text(scr_main_channel_lbl, card_channel_s); } From 606a52042d0c1d33ca20ceff123a90669734f646 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Tue, 7 Feb 2023 06:42:12 +0100 Subject: [PATCH 43/91] Switch to first card after boot also on short press --- src/ps2/ps2_cardman.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index b2baf6a..6369e7d 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -310,6 +310,9 @@ void ps2_cardman_next_channel(void) { card_chan += 1; if (card_chan > CHAN_MAX) card_chan = CHAN_MIN; + } else { + card_idx = settings_get_ps2_card(); + card_chan = settings_get_ps2_channel(); } } @@ -318,6 +321,9 @@ void ps2_cardman_prev_channel(void) { card_chan -= 1; if (card_chan < CHAN_MIN) card_chan = CHAN_MAX; + } else { + card_idx = settings_get_ps2_card(); + card_chan = settings_get_ps2_channel(); } } From c7c81df05f76417821577bc645d8c514d0ae01d0 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Tue, 7 Feb 2023 14:32:16 +0100 Subject: [PATCH 44/91] Add version --- CMakeLists.txt | 16 +++++++++++++++- src/gui.c | 5 +++++ src/main.c | 2 ++ src/version.h | 4 ++++ template/version.c | 3 +++ 5 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/version.h create mode 100644 template/version.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ea08e3..e00d107 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,13 +14,25 @@ endif() pico_sdk_init() +find_package(Git QUIET) + +if(Git_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") + execute_process(COMMAND ${GIT_EXECUTABLE} fetch --all --tags --force) + execute_process(COMMAND ${GIT_EXECUTABLE} describe --exclude latest + OUTPUT_VARIABLE SD2PSX_VERSION) + string(REGEX REPLACE "\n$" "" SD2PSX_VERSION "${SD2PSX_VERSION}") +else() + set(SD2PSX_VERSION "Dev") +endif() + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/template/version.c ${CMAKE_CURRENT_BINARY_DIR}/version.c @ONLY) + set(LV_CONF_PATH ${CMAKE_CURRENT_SOURCE_DIR}/src/lv_conf.h CACHE STRING "" FORCE) add_subdirectory(ext/lvgl EXCLUDE_FROM_ALL) add_subdirectory(database) add_subdirectory(ps2boot) - add_executable(sd2psx src/main.c src/debug.c @@ -90,6 +102,8 @@ add_executable(sd2psx ext/ESP8266SdFat/src/SdCard/SdSpiCard.cpp ext/fnv/hash_64a.c + + ${CMAKE_CURRENT_BINARY_DIR}/version.c ) add_library(ssd1306 STATIC ext/pico-ssd1306/ssd1306.c) diff --git a/src/gui.c b/src/gui.c index 7959f7f..8dbba0f 100644 --- a/src/gui.c +++ b/src/gui.c @@ -2,6 +2,7 @@ #include #include +#include #include "config.h" #include "lvgl.h" @@ -611,6 +612,10 @@ static void create_menu_screen(void) { ui_label_create_grow(cont, "Display"); ui_label_create(cont, ">"); ui_menu_set_load_page_event(menu, cont, display_page); + + cont = ui_menu_cont_create_nav(main_page); + ui_label_create_grow_scroll(cont, "Version"); + ui_label_create(cont, sd2psx_version); } ui_menu_set_page(menu, main_page); diff --git a/src/main.c b/src/main.c index 3d67ebf..094803e 100644 --- a/src/main.c +++ b/src/main.c @@ -15,6 +15,7 @@ #include "sd.h" #include "keystore.h" #include "settings.h" +#include "version.h" #include "ps1/ps1_memory_card.h" #include "ps1/ps1_dirty.h" @@ -125,6 +126,7 @@ int main() { gui_do_ps2_card_switch(); uint64_t end = time_us_64(); printf("DONE! (%d us)\n", (int)(end - start)); + printf("SD2PSX Version %s\n", sd2psx_version); while (1) { debug_task(); diff --git a/src/version.h b/src/version.h new file mode 100644 index 0000000..5d3e05f --- /dev/null +++ b/src/version.h @@ -0,0 +1,4 @@ + +#pragma once + +extern const char* sd2psx_version; \ No newline at end of file diff --git a/template/version.c b/template/version.c new file mode 100644 index 0000000..4c89c49 --- /dev/null +++ b/template/version.c @@ -0,0 +1,3 @@ +#include "version.h" + +const char* sd2psx_version = "@SD2PSX_VERSION@"; From e6294ed6e53ecc533eb078cf08f9124e03116f8c Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 8 Feb 2023 07:10:34 +0100 Subject: [PATCH 45/91] Add separate version page --- CMakeLists.txt | 14 +------------- src/gui.c | 25 ++++++++++++++++++++++--- src/main.c | 2 +- src/version.h | 4 ---- src/version/CMakeLists.txt | 22 ++++++++++++++++++++++ src/version/template/version.c | 5 +++++ src/version/version.h | 6 ++++++ template/version.c | 3 --- 8 files changed, 57 insertions(+), 24 deletions(-) delete mode 100644 src/version.h create mode 100644 src/version/CMakeLists.txt create mode 100644 src/version/template/version.c create mode 100644 src/version/version.h delete mode 100644 template/version.c diff --git a/CMakeLists.txt b/CMakeLists.txt index e00d107..275aa4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,24 +14,12 @@ endif() pico_sdk_init() -find_package(Git QUIET) - -if(Git_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") - execute_process(COMMAND ${GIT_EXECUTABLE} fetch --all --tags --force) - execute_process(COMMAND ${GIT_EXECUTABLE} describe --exclude latest - OUTPUT_VARIABLE SD2PSX_VERSION) - string(REGEX REPLACE "\n$" "" SD2PSX_VERSION "${SD2PSX_VERSION}") -else() - set(SD2PSX_VERSION "Dev") -endif() - -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/template/version.c ${CMAKE_CURRENT_BINARY_DIR}/version.c @ONLY) - set(LV_CONF_PATH ${CMAKE_CURRENT_SOURCE_DIR}/src/lv_conf.h CACHE STRING "" FORCE) add_subdirectory(ext/lvgl EXCLUDE_FROM_ALL) add_subdirectory(database) add_subdirectory(ps2boot) +add_subdirectory(src/version) add_executable(sd2psx src/main.c diff --git a/src/gui.c b/src/gui.c index 8dbba0f..8037d46 100644 --- a/src/gui.c +++ b/src/gui.c @@ -2,7 +2,6 @@ #include #include -#include #include "config.h" #include "lvgl.h" @@ -21,6 +20,8 @@ #include "ps2/ps2_dirty.h" #include "ps2/ps2_exploit.h" +#include "version/version.h" + #include "ui_theme_mono.h" /* Displays the line at the bottom for long pressing buttons */ @@ -590,6 +591,22 @@ static void create_menu_screen(void) { lv_obj_add_event_cb(cont, evt_do_civ_deploy, LV_EVENT_CLICKED, NULL); } + /* Info submenu */ + lv_obj_t *info_page = ui_menu_subpage_create(menu, "Info"); + { + cont = ui_menu_cont_create_nav(info_page); + ui_label_create_grow_scroll(cont, "Version"); + ui_label_create(cont, sd2psx_version); + + cont = ui_menu_cont_create_nav(info_page); + ui_label_create_grow_scroll(cont, "Commit"); + ui_label_create(cont, sd2psx_commit); + + cont = ui_menu_cont_create_nav(info_page); + ui_label_create_grow_scroll(cont, "Debug"); + ui_label_create(cont, DEBUG_USB_UART ? "Yes" : "No"); + } + /* Main menu */ main_page = ui_menu_subpage_create(menu, NULL); { @@ -614,8 +631,10 @@ static void create_menu_screen(void) { ui_menu_set_load_page_event(menu, cont, display_page); cont = ui_menu_cont_create_nav(main_page); - ui_label_create_grow_scroll(cont, "Version"); - ui_label_create(cont, sd2psx_version); + ui_label_create_grow_scroll(cont, "Info"); + ui_label_create(cont, ">"); + ui_menu_set_load_page_event(menu, cont, info_page); + } ui_menu_set_page(menu, main_page); diff --git a/src/main.c b/src/main.c index 094803e..5748564 100644 --- a/src/main.c +++ b/src/main.c @@ -15,7 +15,7 @@ #include "sd.h" #include "keystore.h" #include "settings.h" -#include "version.h" +#include "version/version.h" #include "ps1/ps1_memory_card.h" #include "ps1/ps1_dirty.h" diff --git a/src/version.h b/src/version.h deleted file mode 100644 index 5d3e05f..0000000 --- a/src/version.h +++ /dev/null @@ -1,4 +0,0 @@ - -#pragma once - -extern const char* sd2psx_version; \ No newline at end of file diff --git a/src/version/CMakeLists.txt b/src/version/CMakeLists.txt new file mode 100644 index 0000000..f0de497 --- /dev/null +++ b/src/version/CMakeLists.txt @@ -0,0 +1,22 @@ + +find_package(Git QUIET) + +if(Git_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../.git") + execute_process(COMMAND ${GIT_EXECUTABLE} fetch --all --tags --force) + execute_process(COMMAND ${GIT_EXECUTABLE} describe --exclude latest --abbrev=0 + OUTPUT_VARIABLE SD2PSX_VERSION) + execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD + OUTPUT_VARIABLE SD2PSX_COMMIT) + execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD + OUTPUT_VARIABLE SD2PSX_BRANCH) + string(REGEX REPLACE "\n$" "" SD2PSX_VERSION "${SD2PSX_VERSION}") + string(REGEX REPLACE "\n$" "" SD2PSX_COMMIT "${SD2PSX_COMMIT}") + string(REGEX REPLACE "\n$" "" SD2PSX_BRANCH "${SD2PSX_BRANCH}") +else() + set(SD2PSX_VERSION "None") + set(SD2PSX_COMMIT "None") + set(SD2PSX_BRANCH "None") +endif() + + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/template/version.c ${CMAKE_CURRENT_BINARY_DIR}/version.c @ONLY) diff --git a/src/version/template/version.c b/src/version/template/version.c new file mode 100644 index 0000000..271ac60 --- /dev/null +++ b/src/version/template/version.c @@ -0,0 +1,5 @@ +#include "version/version.h" + +const char* sd2psx_version = "@SD2PSX_VERSION@"; +const char* sd2psx_commit = "@SD2PSX_COMMIT@"; +const char* sd2psx_branch = "@SD2PSX_BRANCH@"; diff --git a/src/version/version.h b/src/version/version.h new file mode 100644 index 0000000..dd4e696 --- /dev/null +++ b/src/version/version.h @@ -0,0 +1,6 @@ + +#pragma once + +extern const char* sd2psx_version; +extern const char* sd2psx_commit; +extern const char* sd2psx_branch; diff --git a/template/version.c b/template/version.c deleted file mode 100644 index 4c89c49..0000000 --- a/template/version.c +++ /dev/null @@ -1,3 +0,0 @@ -#include "version.h" - -const char* sd2psx_version = "@SD2PSX_VERSION@"; From 787e33afe31dd29efd8ee4c99688db9bdc522f47 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 8 Feb 2023 07:12:42 +0100 Subject: [PATCH 46/91] Add Version to workflow --- .github/workflows/cmake.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index ee2b052..38f99c2 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -46,13 +46,22 @@ jobs: - name: Rename Debug USB uf2 run: mv build_usb/sd2psx.uf2 build/sd2psx_usb_debug.uf2 - + + - name: Generate Version + run: | + git fetch --all --tags + git describe --exclude latest | sed 's/\(.*\)-.*/\1/' + SD2PSX_VERSION=$(git describe --exclude latest | sed 's/\(.*\)-.*/\1/') + echo "SD2PSX_VERSION=${SD2PSX_VERSION}" >> $GITHUB_ENV + echo "${SD2PSX_VERSION}" + shell: bash - uses: marvinpinto/action-automatic-releases@v1.2.1 if: github.event_name != 'pull_request' # uses: marvinpinto/action-automatic-releases@919008cf3f741b179569b7a6fb4d8860689ab7f0 with: # GitHub secret token + title: "${{ env.SD2PSX_VERSION }}" repo_token: "${{ secrets.GITHUB_TOKEN }}" automatic_release_tag: latest prerelease: true From 791ace6395174527b64288a1afd9eaa5084aafa6 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 8 Feb 2023 07:20:44 +0100 Subject: [PATCH 47/91] Fix Build --- .gitignore | 3 ++- CMakeLists.txt | 4 ++-- src/version/CMakeLists.txt | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index ad9a446..a1b56f4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ build .vscode keys.c .cache -gamedbps*.dat \ No newline at end of file +gamedbps*.dat +src/version/version.c \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 275aa4e..776b353 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,8 @@ add_executable(sd2psx src/arduino_wrapper/sd.cpp src/arduino_wrapper/SPI.cpp + src/version/version.c + ext/ESP8266SdFat/src/common/FmtNumber.cpp ext/ESP8266SdFat/src/common/FsCache.cpp ext/ESP8266SdFat/src/common/FsDateTime.cpp @@ -90,8 +92,6 @@ add_executable(sd2psx ext/ESP8266SdFat/src/SdCard/SdSpiCard.cpp ext/fnv/hash_64a.c - - ${CMAKE_CURRENT_BINARY_DIR}/version.c ) add_library(ssd1306 STATIC ext/pico-ssd1306/ssd1306.c) diff --git a/src/version/CMakeLists.txt b/src/version/CMakeLists.txt index f0de497..4692288 100644 --- a/src/version/CMakeLists.txt +++ b/src/version/CMakeLists.txt @@ -1,7 +1,7 @@ find_package(Git QUIET) -if(Git_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../.git") +if(Git_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../../.git") execute_process(COMMAND ${GIT_EXECUTABLE} fetch --all --tags --force) execute_process(COMMAND ${GIT_EXECUTABLE} describe --exclude latest --abbrev=0 OUTPUT_VARIABLE SD2PSX_VERSION) @@ -19,4 +19,4 @@ else() endif() -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/template/version.c ${CMAKE_CURRENT_BINARY_DIR}/version.c @ONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/template/version.c ${CMAKE_CURRENT_SOURCE_DIR}/version.c @ONLY) From 17777f52c8c3c0fd98213b7050be37d3251f1f86 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 8 Feb 2023 07:35:45 +0100 Subject: [PATCH 48/91] Fix non Debug version --- src/gui.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/gui.c b/src/gui.c index 8037d46..1b14170 100644 --- a/src/gui.c +++ b/src/gui.c @@ -604,7 +604,11 @@ static void create_menu_screen(void) { cont = ui_menu_cont_create_nav(info_page); ui_label_create_grow_scroll(cont, "Debug"); - ui_label_create(cont, DEBUG_USB_UART ? "Yes" : "No"); +#ifdef DEBUG_USB_UART + ui_label_create(cont, "Yes"); +#else + ui_label_create(cont, "No"); +#endif } /* Main menu */ From a5d2bd9a3b8c9c5a50b4e5ee42062171b280d77d Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 8 Feb 2023 07:47:08 +0100 Subject: [PATCH 49/91] Try to get version during checkout --- .github/workflows/cmake.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 38f99c2..6c5c164 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -21,6 +21,15 @@ jobs: - uses: actions/checkout@v3 with: submodules: 'recursive' + fetch-depth: '0' + run: | + git fetch --prune --tags + git tag --list + git fetch --all --tags + SD2PSX_VERSION=$(git describe --exclude latest | sed 's/\(.*\)-.*/\1/') + echo "SD2PSX_VERSION=${SD2PSX_VERSION}" >> $GITHUB_ENV + echo "${SD2PSX_VERSION}" + - name: add build essential run: sudo apt update && sudo apt install -y build-essential gcc-arm-none-eabi @@ -47,15 +56,6 @@ jobs: - name: Rename Debug USB uf2 run: mv build_usb/sd2psx.uf2 build/sd2psx_usb_debug.uf2 - - name: Generate Version - run: | - git fetch --all --tags - git describe --exclude latest | sed 's/\(.*\)-.*/\1/' - SD2PSX_VERSION=$(git describe --exclude latest | sed 's/\(.*\)-.*/\1/') - echo "SD2PSX_VERSION=${SD2PSX_VERSION}" >> $GITHUB_ENV - echo "${SD2PSX_VERSION}" - shell: bash - - uses: marvinpinto/action-automatic-releases@v1.2.1 if: github.event_name != 'pull_request' # uses: marvinpinto/action-automatic-releases@919008cf3f741b179569b7a6fb4d8860689ab7f0 From 95c9c49c08fd245263eb5bd5322fee4487de6c69 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 8 Feb 2023 07:48:45 +0100 Subject: [PATCH 50/91] CI --- .github/workflows/cmake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 6c5c164..686a364 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -22,7 +22,7 @@ jobs: with: submodules: 'recursive' fetch-depth: '0' - run: | + - run: | git fetch --prune --tags git tag --list git fetch --all --tags From 6b79a486b2336adb2c62743b8561d235e43321d5 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Fri, 10 Feb 2023 09:45:43 +0100 Subject: [PATCH 51/91] Add PS2 game naming based on txt file --- src/gui.c | 21 +++++++++++++++++++-- src/ps2/ps2_cardman.c | 32 +++++++++++++++++--------------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/gui.c b/src/gui.c index e5acb14..76d169c 100644 --- a/src/gui.c +++ b/src/gui.c @@ -743,13 +743,14 @@ void gui_do_ps2_card_switch(void) { void gui_task(void) { input_update_display(g_navbar); + char card_name[127]; + const char* folder_name = NULL; + if (settings_get_mode() == MODE_PS1) { static int displayed_card_idx = -1; static int displayed_card_channel = -1; static char card_idx_s[8]; static char card_channel_s[8]; - char card_name[127]; - const char* folder_name = NULL; if (displayed_card_idx != ps1_cardman_get_idx() || displayed_card_channel != ps1_cardman_get_channel() || refresh_gui) { displayed_card_idx = ps1_cardman_get_idx(); @@ -793,6 +794,8 @@ void gui_task(void) { if (displayed_card_idx != ps2_cardman_get_idx() || displayed_card_channel != ps2_cardman_get_channel()) { displayed_card_idx = ps2_cardman_get_idx(); displayed_card_channel = ps2_cardman_get_channel(); + folder_name = ps2_cardman_get_folder_name(); + if (displayed_card_idx == 0) { snprintf(card_idx_s, sizeof(card_idx_s), "BOOT"); snprintf(card_channel_s, sizeof(card_channel_s), " "); @@ -804,6 +807,20 @@ void gui_task(void) { lv_label_set_text(lbl_channel, "Channel"); } lv_label_set_text(scr_main_idx_lbl, card_idx_s); + + + memset(card_name, 0, sizeof(card_name)); + game_names_get_name_by_folder(folder_name, card_name); + + if (card_name[0]) + { + lv_label_set_text(src_main_title_lbl, card_name); + } + else + { + lv_label_set_text(src_main_title_lbl, ""); + } + lv_label_set_text(scr_main_channel_lbl, card_channel_s); } diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index 4c7a66d..4c8e93b 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -32,8 +32,7 @@ static int card_idx; static int card_chan; static uint32_t card_size; static cardman_cb_t cardman_cb; -static char card_game_id[MAX_GAME_ID_LENGTH]; -static const char* card_game_name; +static char folder_name[MAX_GAME_ID_LENGTH]; static uint64_t cardprog_start; static size_t cardprog_pos; static int cardprog_wr; @@ -50,7 +49,7 @@ void ps2_cardman_init(void) { if (card_chan < CHAN_MIN || card_chan > CHAN_MAX) card_chan = CHAN_MIN; } - memset((void*)card_game_id, 0, MAX_GAME_ID_LENGTH); + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } int ps2_cardman_write_sector(int sector, void *buf512) { @@ -76,7 +75,8 @@ static void ensuredirs(void) { if (IDX_BOOT == card_idx) snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/BOOT"); else - snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/Card%d", card_idx); + snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/%s", folder_name); + sd_mkdir("MemoryCards"); sd_mkdir("MemoryCards/PS2"); @@ -226,11 +226,12 @@ void ps2_cardman_open(void) { char path[64]; ensuredirs(); + if (IDX_BOOT == card_idx) snprintf(path, sizeof(path), "MemoryCards/PS2/BOOT/BootCard.mcd"); else { - snprintf(path, sizeof(path), "MemoryCards/PS2/Card%d/Card%d-%d.mcd", card_idx, card_idx, card_chan); + snprintf(path, sizeof(path), "MemoryCards/PS2/%s/%s-%d.mcd", folder_name, folder_name, card_chan); /* this is ok to do on every boot because it wouldn't update if the value is the same as currently stored */ settings_set_ps2_card(card_idx); settings_set_ps2_channel(card_chan); @@ -321,6 +322,7 @@ void ps2_cardman_next_channel(void) { card_idx = settings_get_ps2_card(); card_chan = settings_get_ps2_channel(); } + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } void ps2_cardman_prev_channel(void) { @@ -332,6 +334,7 @@ void ps2_cardman_prev_channel(void) { card_idx = settings_get_ps2_card(); card_chan = settings_get_ps2_channel(); } + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } void ps2_cardman_next_idx(void) { @@ -342,6 +345,7 @@ void ps2_cardman_next_idx(void) { card_idx = settings_get_ps2_card(); card_chan = settings_get_ps2_channel(); } + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } void ps2_cardman_prev_idx(void) { @@ -355,6 +359,11 @@ void ps2_cardman_prev_idx(void) { card_idx = settings_get_ps2_card(); card_chan = settings_get_ps2_channel(); } + if (card_idx != IDX_BOOT) + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); + else + snprintf(folder_name, sizeof(folder_name), "BOOT"); + } int ps2_cardman_get_idx(void) { @@ -383,16 +392,9 @@ uint32_t ps2_cardman_get_card_size(void) { } void ps2_cardman_set_gameid(const char* game_id) { - strlcpy(card_game_id, game_id, MAX_GAME_ID_LENGTH); + strlcpy(folder_name, game_id, MAX_GAME_ID_LENGTH); } -const char* ps2_cardman_get_gameid(void) { - return card_game_id; +const char* ps2_cardman_get_folder_name(void) { + return folder_name; } - -const char* ps2_cardman_get_gamename(void) { - if (!*card_game_name) { - - } - return card_game_name; -} \ No newline at end of file From edb31ad498b8a7e113595528c1edc9aa55ded0f5 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 22 Feb 2023 08:34:03 +0100 Subject: [PATCH 52/91] Fix Bootcard Naming --- src/ps2/ps2_cardman.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index 4c8e93b..57e5f57 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -41,6 +41,7 @@ void ps2_cardman_init(void) { if (settings_get_ps2_autoboot()) { card_idx = IDX_BOOT; card_chan = CHAN_MIN; + snprintf(folder_name, sizeof(folder_name), "BOOT"); } else { card_idx = settings_get_ps2_card(); if (card_idx < IDX_MIN) @@ -48,8 +49,8 @@ void ps2_cardman_init(void) { card_chan = settings_get_ps2_channel(); if (card_chan < CHAN_MIN || card_chan > CHAN_MAX) card_chan = CHAN_MIN; + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } - snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } int ps2_cardman_write_sector(int sector, void *buf512) { @@ -72,11 +73,8 @@ void ps2_cardman_flush(void) { static void ensuredirs(void) { char cardpath[32]; - if (IDX_BOOT == card_idx) - snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/BOOT"); - else - snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/%s", folder_name); - + + snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/%s", folder_name); sd_mkdir("MemoryCards"); sd_mkdir("MemoryCards/PS2"); @@ -228,7 +226,7 @@ void ps2_cardman_open(void) { ensuredirs(); if (IDX_BOOT == card_idx) - snprintf(path, sizeof(path), "MemoryCards/PS2/BOOT/BootCard.mcd"); + snprintf(path, sizeof(path), "MemoryCards/PS2/%s/BootCard.mcd", folder_name); else { snprintf(path, sizeof(path), "MemoryCards/PS2/%s/%s-%d.mcd", folder_name, folder_name, card_chan); From b5eccca8219f9329be3bf7c93c0067704c195f23 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 22 Feb 2023 08:34:44 +0100 Subject: [PATCH 53/91] Add game_names --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 776b353..5f94f84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ add_executable(sd2psx src/arduino_wrapper/SPI.cpp src/version/version.c + src/game_names/game_names.c ext/ESP8266SdFat/src/common/FmtNumber.cpp ext/ESP8266SdFat/src/common/FsCache.cpp From 0559050be4c7a7f1d59a19dedee2e5db86e95635 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 22 Feb 2023 08:36:40 +0100 Subject: [PATCH 54/91] Move version.c into build folder as it is generated each build --- CMakeLists.txt | 2 +- src/version/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 776b353..d463a62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,7 +53,7 @@ add_executable(sd2psx src/arduino_wrapper/sd.cpp src/arduino_wrapper/SPI.cpp - src/version/version.c + ${CMAKE_CURRENT_BINARY_DIR}/src/version/version.c ext/ESP8266SdFat/src/common/FmtNumber.cpp ext/ESP8266SdFat/src/common/FsCache.cpp diff --git a/src/version/CMakeLists.txt b/src/version/CMakeLists.txt index 4692288..e997708 100644 --- a/src/version/CMakeLists.txt +++ b/src/version/CMakeLists.txt @@ -19,4 +19,4 @@ else() endif() -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/template/version.c ${CMAKE_CURRENT_SOURCE_DIR}/version.c @ONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/template/version.c ${CMAKE_CURRENT_BINARY_DIR}/version.c @ONLY) From 5d5a2854321abdaeb91e70b848cd5f94f366602f Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 19 Mar 2023 17:21:15 +0100 Subject: [PATCH 55/91] Fix versioning --- .github/workflows/cmake.yml | 16 ++++++++++++---- src/version/CMakeLists.txt | 5 ++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 686a364..9ded941 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -26,9 +26,17 @@ jobs: git fetch --prune --tags git tag --list git fetch --all --tags - SD2PSX_VERSION=$(git describe --exclude latest | sed 's/\(.*\)-.*/\1/') - echo "SD2PSX_VERSION=${SD2PSX_VERSION}" >> $GITHUB_ENV - echo "${SD2PSX_VERSION}" + TAG=$(git describe --tags --exact-match HEAD --exclude=latest) || true + if [ $TAG ] + then + echo "SD2PSX_VERSION=${TAG}" >> $GITHUB_ENV + echo "SD2PSX_RLS_TAG=latest" >> $GITHUB_ENV + echo "${TAG}" + else + echo "SD2PSX_VERSION=nightly" >> $GITHUB_ENV + echo "SD2PSX_RLS_TAG=nightly" >> $GITHUB_ENV + echo "nightly" + fi - name: add build essential run: sudo apt update && sudo apt install -y build-essential gcc-arm-none-eabi @@ -63,7 +71,7 @@ jobs: # GitHub secret token title: "${{ env.SD2PSX_VERSION }}" repo_token: "${{ secrets.GITHUB_TOKEN }}" - automatic_release_tag: latest + automatic_release_tag: ${{ env.SD2PSX_RLS_TAG }} prerelease: true # Assets to upload to the release files: | diff --git a/src/version/CMakeLists.txt b/src/version/CMakeLists.txt index e997708..d941dc9 100644 --- a/src/version/CMakeLists.txt +++ b/src/version/CMakeLists.txt @@ -3,7 +3,7 @@ find_package(Git QUIET) if(Git_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../../.git") execute_process(COMMAND ${GIT_EXECUTABLE} fetch --all --tags --force) - execute_process(COMMAND ${GIT_EXECUTABLE} describe --exclude latest --abbrev=0 + execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --exact-match HEAD --exclude=latest OUTPUT_VARIABLE SD2PSX_VERSION) execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD OUTPUT_VARIABLE SD2PSX_COMMIT) @@ -12,6 +12,9 @@ if(Git_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../../.git") string(REGEX REPLACE "\n$" "" SD2PSX_VERSION "${SD2PSX_VERSION}") string(REGEX REPLACE "\n$" "" SD2PSX_COMMIT "${SD2PSX_COMMIT}") string(REGEX REPLACE "\n$" "" SD2PSX_BRANCH "${SD2PSX_BRANCH}") + if("${SD2PSX_VERSION}" STREQUAL "") + set(SD2PSX_VERSION "${SD2PSX_BRANCH}#${SD2PSX_COMMIT}") + endif() else() set(SD2PSX_VERSION "None") set(SD2PSX_COMMIT "None") From 7ac0d520656b4f76224b32494166ecfc16b1dac6 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 19 Mar 2023 17:30:04 +0100 Subject: [PATCH 56/91] Release name for untagged releases "nightly-COMMIT_HASH" --- .github/workflows/cmake.yml | 2 +- src/version/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 9ded941..e9cb84d 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -33,7 +33,7 @@ jobs: echo "SD2PSX_RLS_TAG=latest" >> $GITHUB_ENV echo "${TAG}" else - echo "SD2PSX_VERSION=nightly" >> $GITHUB_ENV + echo "SD2PSX_VERSION=nightly-$(git rev-parse --short HEAD)" >> $GITHUB_ENV echo "SD2PSX_RLS_TAG=nightly" >> $GITHUB_ENV echo "nightly" fi diff --git a/src/version/CMakeLists.txt b/src/version/CMakeLists.txt index d941dc9..64cfc41 100644 --- a/src/version/CMakeLists.txt +++ b/src/version/CMakeLists.txt @@ -13,7 +13,7 @@ if(Git_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../../.git") string(REGEX REPLACE "\n$" "" SD2PSX_COMMIT "${SD2PSX_COMMIT}") string(REGEX REPLACE "\n$" "" SD2PSX_BRANCH "${SD2PSX_BRANCH}") if("${SD2PSX_VERSION}" STREQUAL "") - set(SD2PSX_VERSION "${SD2PSX_BRANCH}#${SD2PSX_COMMIT}") + set(SD2PSX_VERSION "nightly-${SD2PSX_COMMIT}") endif() else() set(SD2PSX_VERSION "None") From c16d70e09d6e227facda6f433271fa694799c433 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 19 Mar 2023 17:39:38 +0100 Subject: [PATCH 57/91] Exclude nightly for describe --- .github/workflows/cmake.yml | 2 +- src/version/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index e9cb84d..4d9b570 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -26,7 +26,7 @@ jobs: git fetch --prune --tags git tag --list git fetch --all --tags - TAG=$(git describe --tags --exact-match HEAD --exclude=latest) || true + TAG=$(git describe --tags --exact-match HEAD --exclude=latest --exclude=nightly) || true if [ $TAG ] then echo "SD2PSX_VERSION=${TAG}" >> $GITHUB_ENV diff --git a/src/version/CMakeLists.txt b/src/version/CMakeLists.txt index 64cfc41..45324cf 100644 --- a/src/version/CMakeLists.txt +++ b/src/version/CMakeLists.txt @@ -3,7 +3,7 @@ find_package(Git QUIET) if(Git_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../../.git") execute_process(COMMAND ${GIT_EXECUTABLE} fetch --all --tags --force) - execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --exact-match HEAD --exclude=latest + execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --exact-match HEAD --exclude=latest --exclude=nightly OUTPUT_VARIABLE SD2PSX_VERSION) execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD OUTPUT_VARIABLE SD2PSX_COMMIT) From 782577d530bda8894f4bf268cf2bdf5769c1735c Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 20 Mar 2023 08:20:51 +0100 Subject: [PATCH 58/91] Update Exploit Loader to also work on PSX DVR --- ps2boot/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ps2boot/CMakeLists.txt b/ps2boot/CMakeLists.txt index 2eb3ce0..dd84710 100644 --- a/ps2boot/CMakeLists.txt +++ b/ps2boot/CMakeLists.txt @@ -1,7 +1,7 @@ set(STAGE1_PS2_BOOT "bootcard.bin") set(STAGE1_PS2_BOOT_OBJ "${CMAKE_CURRENT_BINARY_DIR}/bootcard.o") -file(DOWNLOAD "https://github.com/sd2psx/OSDSYS-Launcher/releases/download/0.9/first-stage.mcd" +file(DOWNLOAD "https://github.com/sd2psx/OSDSYS-Launcher/releases/download/0.9.1/first-stage.mcd" "${CMAKE_CURRENT_SOURCE_DIR}/${STAGE1_PS2_BOOT}" SHOW_PROGRESS) From 0db45cf3ca9971d848d943fa64bcd7a5e70dbf5f Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 20 Mar 2023 12:10:52 +0100 Subject: [PATCH 59/91] Keep object files updated - update databases only once a day --- database/CMakeLists.txt | 8 ++++---- database/parse_db.py | 7 ++++--- ps2boot/CMakeLists.txt | 12 +++++++++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/database/CMakeLists.txt b/database/CMakeLists.txt index 48a444f..8c62051 100644 --- a/database/CMakeLists.txt +++ b/database/CMakeLists.txt @@ -1,14 +1,15 @@ find_package (Python COMPONENTS Interpreter) +string(TIMESTAMP date "%Y%m%d") set(GAMEDB_PS1_BIN "gamedbps1.dat") -set(GAMEDB_PS1_OBJ "${CMAKE_CURRENT_BINARY_DIR}/gamedbps1.o") +set(GAMEDB_PS1_OBJ "${CMAKE_CURRENT_BINARY_DIR}/gamedbps1_${date}.o") add_custom_command(OUTPUT "${GAMEDB_PS1_OBJ}" - COMMAND ${Python_EXECUTABLE} ARGS parse_db.py ps1 + COMMAND ${Python_EXECUTABLE} ARGS ${CMAKE_CURRENT_SOURCE_DIR}/parse_db.py ps1 ${CMAKE_CURRENT_BINARY_DIR} COMMAND ${CMAKE_OBJCOPY} ARGS --input-target=binary --output-target=elf32-littlearm --binary-architecture arm --rename-section .data=.rodata ${GAMEDB_PS1_BIN} ${GAMEDB_PS1_OBJ} COMMAND ${CMAKE_COMMAND} ARGS -E remove_directory ps1 - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT "Generating game db binary for ps1") add_custom_target(gamedbobjs DEPENDS "${GAMEDB_PS1_OBJ}") @@ -17,4 +18,3 @@ add_library(gamedb INTERFACE) add_dependencies(gamedb gamedbobjs) target_link_libraries(gamedb INTERFACE ${GAMEDB_PS1_OBJ}) - diff --git a/database/parse_db.py b/database/parse_db.py index d44bb05..b3fe403 100644 --- a/database/parse_db.py +++ b/database/parse_db.py @@ -74,7 +74,7 @@ def createGameList(name_to_serials): import xml.etree.ElementTree as ET import re -def createDbFile(rootdir): +def createDbFile(rootdir, outputdir): dirname = rootdir.split("/")[-1] if len(dirname) < 1: dirname = rootdir.split("/")[-2] @@ -129,7 +129,7 @@ def createDbFile(rootdir): game_name_to_offset[gamename] = offset offset = offset + len(gamename) + 1 - with open("gamedb{}.dat".format(dirname), "wb") as out: + with open("{}/gamedb{}.dat".format(outputdir, dirname), "wb") as out: # First: write prefix Indices in the format # 4 Byte: Index Chars, padded with ws in the end # 4 Byte: Index Offset within dat @@ -174,8 +174,9 @@ def downloadDat(path): import argparse parser = argparse.ArgumentParser() parser.add_argument("dirname") +parser.add_argument("outputdir") args = parser.parse_args() downloadDat(args.dirname) -createDbFile(args.dirname) \ No newline at end of file +createDbFile(args.dirname, args.outputdir) \ No newline at end of file diff --git a/ps2boot/CMakeLists.txt b/ps2boot/CMakeLists.txt index dd84710..730384c 100644 --- a/ps2boot/CMakeLists.txt +++ b/ps2boot/CMakeLists.txt @@ -1,17 +1,23 @@ +set(STAGE1_PS2_BOOT_VERSION "0.9.1") + set(STAGE1_PS2_BOOT "bootcard.bin") set(STAGE1_PS2_BOOT_OBJ "${CMAKE_CURRENT_BINARY_DIR}/bootcard.o") -file(DOWNLOAD "https://github.com/sd2psx/OSDSYS-Launcher/releases/download/0.9.1/first-stage.mcd" - "${CMAKE_CURRENT_SOURCE_DIR}/${STAGE1_PS2_BOOT}" + +if(NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/${STAGE1_PS2_BOOT_VERSION}/${STAGE1_PS2_BOOT}) + file(DOWNLOAD "https://github.com/sd2psx/OSDSYS-Launcher/releases/download/${STAGE1_PS2_BOOT_VERSION}/first-stage.mcd" + "${CMAKE_CURRENT_BINARY_DIR}/${STAGE1_PS2_BOOT_VERSION}/${STAGE1_PS2_BOOT}" SHOW_PROGRESS) +endif() add_custom_command(OUTPUT "${STAGE1_PS2_BOOT_OBJ}" COMMAND ${CMAKE_OBJCOPY} ARGS --input-target=binary --output-target=elf32-littlearm --binary-architecture arm --rename-section .data=.rodata ${STAGE1_PS2_BOOT} ${STAGE1_PS2_BOOT_OBJ} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${STAGE1_PS2_BOOT_VERSION} COMMENT "Generating ps2 stage 1 boot obj") add_custom_target(ps2bootobjs DEPENDS "${STAGE1_PS2_BOOT_OBJ}") + add_library(ps2boot INTERFACE) add_dependencies(ps2boot ps2bootobjs) From 2cd42e8dce3bd256795261c02f64bf8c67eae572 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 9 Apr 2023 13:18:50 +0200 Subject: [PATCH 60/91] Use cmake script to generate version.c --- src/version/CMakeLists.txt | 32 +++++++------------------------- src/version/script.cmake | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 25 deletions(-) create mode 100644 src/version/script.cmake diff --git a/src/version/CMakeLists.txt b/src/version/CMakeLists.txt index 45324cf..d805f0d 100644 --- a/src/version/CMakeLists.txt +++ b/src/version/CMakeLists.txt @@ -1,25 +1,7 @@ - -find_package(Git QUIET) - -if(Git_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../../.git") - execute_process(COMMAND ${GIT_EXECUTABLE} fetch --all --tags --force) - execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --exact-match HEAD --exclude=latest --exclude=nightly - OUTPUT_VARIABLE SD2PSX_VERSION) - execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD - OUTPUT_VARIABLE SD2PSX_COMMIT) - execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD - OUTPUT_VARIABLE SD2PSX_BRANCH) - string(REGEX REPLACE "\n$" "" SD2PSX_VERSION "${SD2PSX_VERSION}") - string(REGEX REPLACE "\n$" "" SD2PSX_COMMIT "${SD2PSX_COMMIT}") - string(REGEX REPLACE "\n$" "" SD2PSX_BRANCH "${SD2PSX_BRANCH}") - if("${SD2PSX_VERSION}" STREQUAL "") - set(SD2PSX_VERSION "nightly-${SD2PSX_COMMIT}") - endif() -else() - set(SD2PSX_VERSION "None") - set(SD2PSX_COMMIT "None") - set(SD2PSX_BRANCH "None") -endif() - - -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/template/version.c ${CMAKE_CURRENT_BINARY_DIR}/version.c @ONLY) +add_custom_target(version_generator ALL + COMMAND ${CMAKE_COMMAND} + -D SCRIPT_TEMPLATE=${CMAKE_CURRENT_SOURCE_DIR}/template/version.c + -D SCRIPT_WORKING_DIR=${CMAKE_CURRENT_SOURCE_DIR} + -D SCRIPT_OUTPUT_FILE=${CMAKE_CURRENT_BINARY_DIR}/version.c + -P ${CMAKE_CURRENT_SOURCE_DIR}/script.cmake + SOURCES ${CMAKE_CURRENT_BINARY_DIR}/version.c) \ No newline at end of file diff --git a/src/version/script.cmake b/src/version/script.cmake new file mode 100644 index 0000000..7fe3aa3 --- /dev/null +++ b/src/version/script.cmake @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.12) + +find_package(Git QUIET) + +if(Git_FOUND AND EXISTS "${SCRIPT_WORKING_DIR}/../../.git") + execute_process(COMMAND ${GIT_EXECUTABLE} fetch --all --tags --force WORKING_DIRECTORY ${SCRIPT_WORKING_DIR}) + execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --exact-match HEAD --exclude=latest --exclude=nightly + OUTPUT_VARIABLE SD2PSX_VERSION WORKING_DIRECTORY ${SCRIPT_WORKING_DIR}) + execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD + OUTPUT_VARIABLE SD2PSX_COMMIT WORKING_DIRECTORY ${SCRIPT_WORKING_DIR}) + execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD + OUTPUT_VARIABLE SD2PSX_BRANCH WORKING_DIRECTORY ${SCRIPT_WORKING_DIR}) + string(REGEX REPLACE "\n$" "" SD2PSX_VERSION "${SD2PSX_VERSION}") + string(REGEX REPLACE "\n$" "" SD2PSX_COMMIT "${SD2PSX_COMMIT}") + string(REGEX REPLACE "\n$" "" SD2PSX_BRANCH "${SD2PSX_BRANCH}") + if("${SD2PSX_VERSION}" STREQUAL "") + set(SD2PSX_VERSION "nightly") + endif() +else() + set(SD2PSX_VERSION "None") + set(SD2PSX_COMMIT "None") + set(SD2PSX_BRANCH "None") +endif() + +file(READ ${SCRIPT_TEMPLATE} template_file) + +string(REPLACE "@SD2PSX_VERSION@" ${SD2PSX_VERSION} template_file "${template_file}") +string(REPLACE "@SD2PSX_COMMIT@" ${SD2PSX_COMMIT} template_file "${template_file}") +string(REPLACE "@SD2PSX_BRANCH@" ${SD2PSX_BRANCH} template_file "${template_file}") + +file(WRITE ${SCRIPT_OUTPUT_FILE} "${template_file}") From 0af6db3b035d738086244e1c499cbcd4bca85e1b Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 9 Apr 2023 13:22:11 +0200 Subject: [PATCH 61/91] Remove fetch --all and add origin --- src/version/script.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version/script.cmake b/src/version/script.cmake index 7fe3aa3..f7a0e0e 100644 --- a/src/version/script.cmake +++ b/src/version/script.cmake @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.12) find_package(Git QUIET) if(Git_FOUND AND EXISTS "${SCRIPT_WORKING_DIR}/../../.git") - execute_process(COMMAND ${GIT_EXECUTABLE} fetch --all --tags --force WORKING_DIRECTORY ${SCRIPT_WORKING_DIR}) + execute_process(COMMAND ${GIT_EXECUTABLE} fetch origin --tags --force WORKING_DIRECTORY ${SCRIPT_WORKING_DIR}) execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --exact-match HEAD --exclude=latest --exclude=nightly OUTPUT_VARIABLE SD2PSX_VERSION WORKING_DIRECTORY ${SCRIPT_WORKING_DIR}) execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD From 8599c3ae718279bde21f238d4472ac49ec41c94d Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 9 Apr 2023 19:48:37 +0200 Subject: [PATCH 62/91] Generate Version file after each clean --- CMakeLists.txt | 3 +-- src/version/CMakeLists.txt | 15 +++++++++------ src/version/template/version.c | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d463a62..a04ffc3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,8 +53,6 @@ add_executable(sd2psx src/arduino_wrapper/sd.cpp src/arduino_wrapper/SPI.cpp - ${CMAKE_CURRENT_BINARY_DIR}/src/version/version.c - ext/ESP8266SdFat/src/common/FmtNumber.cpp ext/ESP8266SdFat/src/common/FsCache.cpp ext/ESP8266SdFat/src/common/FsDateTime.cpp @@ -135,6 +133,7 @@ target_link_libraries(sd2psx lvgl::lvgl gamedb ps2boot + sd2psx_version ) add_dependencies(sd2psx gamedb ps2boot) diff --git a/src/version/CMakeLists.txt b/src/version/CMakeLists.txt index d805f0d..50d65bd 100644 --- a/src/version/CMakeLists.txt +++ b/src/version/CMakeLists.txt @@ -1,7 +1,10 @@ -add_custom_target(version_generator ALL +add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/version.c COMMAND ${CMAKE_COMMAND} - -D SCRIPT_TEMPLATE=${CMAKE_CURRENT_SOURCE_DIR}/template/version.c - -D SCRIPT_WORKING_DIR=${CMAKE_CURRENT_SOURCE_DIR} - -D SCRIPT_OUTPUT_FILE=${CMAKE_CURRENT_BINARY_DIR}/version.c - -P ${CMAKE_CURRENT_SOURCE_DIR}/script.cmake - SOURCES ${CMAKE_CURRENT_BINARY_DIR}/version.c) \ No newline at end of file + -D SCRIPT_TEMPLATE=${CMAKE_CURRENT_SOURCE_DIR}/template/version.c + -D SCRIPT_WORKING_DIR=${CMAKE_CURRENT_SOURCE_DIR} + -D SCRIPT_OUTPUT_FILE=${CMAKE_CURRENT_BINARY_DIR}/version.c + -P ${CMAKE_CURRENT_SOURCE_DIR}/script.cmake) + +add_library(sd2psx_version STATIC ${CMAKE_CURRENT_BINARY_DIR}/version.c) +target_include_directories(sd2psx_version PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + diff --git a/src/version/template/version.c b/src/version/template/version.c index 271ac60..2eb645b 100644 --- a/src/version/template/version.c +++ b/src/version/template/version.c @@ -1,4 +1,4 @@ -#include "version/version.h" +#include "version.h" const char* sd2psx_version = "@SD2PSX_VERSION@"; const char* sd2psx_commit = "@SD2PSX_COMMIT@"; From f8b6f8cfb52174f5d71c038eb3b243e9ab36b266 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 10 Apr 2023 12:33:11 +0200 Subject: [PATCH 63/91] Generate C file on each build, regardless of changed content --- src/version/CMakeLists.txt | 6 +++--- src/version/script.cmake | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/version/CMakeLists.txt b/src/version/CMakeLists.txt index 50d65bd..94dc7e5 100644 --- a/src/version/CMakeLists.txt +++ b/src/version/CMakeLists.txt @@ -1,10 +1,10 @@ -add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/version.c +add_custom_target(sd2psx_version_generator ALL COMMAND ${CMAKE_COMMAND} -D SCRIPT_TEMPLATE=${CMAKE_CURRENT_SOURCE_DIR}/template/version.c -D SCRIPT_WORKING_DIR=${CMAKE_CURRENT_SOURCE_DIR} -D SCRIPT_OUTPUT_FILE=${CMAKE_CURRENT_BINARY_DIR}/version.c - -P ${CMAKE_CURRENT_SOURCE_DIR}/script.cmake) + -P ${CMAKE_CURRENT_SOURCE_DIR}/script.cmake + BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/version.c) add_library(sd2psx_version STATIC ${CMAKE_CURRENT_BINARY_DIR}/version.c) target_include_directories(sd2psx_version PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - diff --git a/src/version/script.cmake b/src/version/script.cmake index f7a0e0e..c128def 100644 --- a/src/version/script.cmake +++ b/src/version/script.cmake @@ -5,11 +5,14 @@ find_package(Git QUIET) if(Git_FOUND AND EXISTS "${SCRIPT_WORKING_DIR}/../../.git") execute_process(COMMAND ${GIT_EXECUTABLE} fetch origin --tags --force WORKING_DIRECTORY ${SCRIPT_WORKING_DIR}) execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --exact-match HEAD --exclude=latest --exclude=nightly - OUTPUT_VARIABLE SD2PSX_VERSION WORKING_DIRECTORY ${SCRIPT_WORKING_DIR}) + OUTPUT_VARIABLE SD2PSX_VERSION WORKING_DIRECTORY ${SCRIPT_WORKING_DIR} + OUTPUT_STRIP_TRAILING_WHITESPACE) execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD - OUTPUT_VARIABLE SD2PSX_COMMIT WORKING_DIRECTORY ${SCRIPT_WORKING_DIR}) + OUTPUT_VARIABLE SD2PSX_COMMIT WORKING_DIRECTORY ${SCRIPT_WORKING_DIR} + OUTPUT_STRIP_TRAILING_WHITESPACE) execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD - OUTPUT_VARIABLE SD2PSX_BRANCH WORKING_DIRECTORY ${SCRIPT_WORKING_DIR}) + OUTPUT_VARIABLE SD2PSX_BRANCH WORKING_DIRECTORY ${SCRIPT_WORKING_DIR} + OUTPUT_STRIP_TRAILING_WHITESPACE) string(REGEX REPLACE "\n$" "" SD2PSX_VERSION "${SD2PSX_VERSION}") string(REGEX REPLACE "\n$" "" SD2PSX_COMMIT "${SD2PSX_COMMIT}") string(REGEX REPLACE "\n$" "" SD2PSX_BRANCH "${SD2PSX_BRANCH}") From 92d7b85c254bffc4a751bafff5a701537a811ee2 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 10 Apr 2023 12:37:28 +0200 Subject: [PATCH 64/91] Simplify build script --- .github/workflows/cmake.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 4d9b570..33dec3a 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -23,9 +23,7 @@ jobs: submodules: 'recursive' fetch-depth: '0' - run: | - git fetch --prune --tags - git tag --list - git fetch --all --tags + git fetch origin --tags --force TAG=$(git describe --tags --exact-match HEAD --exclude=latest --exclude=nightly) || true if [ $TAG ] then From 397621ceea7a3b02720db2a37e2e7e62f8e2afa7 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 6 Aug 2023 11:32:14 +0200 Subject: [PATCH 65/91] Clean up --- CMakeLists.txt | 1 + src/game_names/game_names.c | 2 +- src/game_names/game_names.h | 2 +- src/ps2/ps2_cardman.h | 3 ++- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f1a23ae..f5b4c62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,7 @@ add_executable(sd2psx src/ps2/ps2_psram.c src/ps2/ps2_exploit.c + src/game_names/game_names.c src/wear_leveling/wear_leveling.c src/wear_leveling/wear_leveling_rp2040_flash.c diff --git a/src/game_names/game_names.c b/src/game_names/game_names.c index 3aefa73..e88e97d 100644 --- a/src/game_names/game_names.c +++ b/src/game_names/game_names.c @@ -33,7 +33,7 @@ typedef struct { const char* name; } game_lookup; -bool __time_critical_func(game_names_sanity_check_title_id)(const char* const title_id) { +bool game_names_sanity_check_title_id(const char* const title_id) { uint8_t i = 0U; char splittable_game_id[MAX_GAME_ID_LENGTH]; diff --git a/src/game_names/game_names.h b/src/game_names/game_names.h index a1dedfb..4d05a38 100644 --- a/src/game_names/game_names.h +++ b/src/game_names/game_names.h @@ -5,7 +5,7 @@ #include void game_names_extract_title_id(const uint8_t* const in_title_id, char* const out_title_id, const size_t in_title_id_length, const size_t out_buffer_size); -bool game_names_sanity_check_title_id(const char* const title_id); +bool __time_critical_func(game_names_sanity_check_title_id)(const char* const title_id); void game_names_get_name_by_folder(const char* const folder, char* const game_name); void game_names_get_parent(const char* const game_id, char* const parent_id); diff --git a/src/ps2/ps2_cardman.h b/src/ps2/ps2_cardman.h index 967a136..27d478f 100644 --- a/src/ps2/ps2_cardman.h +++ b/src/ps2/ps2_cardman.h @@ -29,4 +29,5 @@ char *ps2_cardman_get_progress_text(void); void ps2_cardman_set_gameid(const char* game_id); const char* ps2_cardman_get_gameid(void); -const char* ps2_cardman_get_gamename(void); \ No newline at end of file +const char* ps2_cardman_get_gamename(void); +const char* ps2_cardman_get_folder_name(void); \ No newline at end of file From bff4ad634078bbd5e69ac1393f300c74a07122b7 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 6 Sep 2023 08:21:54 +0200 Subject: [PATCH 66/91] Move MC card emulation related stuff into separate folder --- CMakeLists.txt | 4 ++-- src/gui.c | 2 +- src/main.c | 2 +- src/ps2/{ => card_emu}/ps2_mc_spi.pio | 0 src/ps2/{ => card_emu}/ps2_memory_card.c | 10 +++++----- src/ps2/{ => card_emu}/ps2_memory_card.h | 0 src/ps2/{ => card_emu}/ps2_memory_card.in.c | 0 7 files changed, 9 insertions(+), 9 deletions(-) rename src/ps2/{ => card_emu}/ps2_mc_spi.pio (100%) rename src/ps2/{ => card_emu}/ps2_memory_card.c (98%) rename src/ps2/{ => card_emu}/ps2_memory_card.h (100%) rename src/ps2/{ => card_emu}/ps2_memory_card.in.c (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index f5b4c62..0f07a72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,7 @@ add_executable(sd2psx src/ps1/ps1_empty_card.c src/ps1/ps1_odeman.c - src/ps2/ps2_memory_card.c + src/ps2/card_emu/ps2_memory_card.c src/ps2/ps2_dirty.c src/ps2/ps2_cardman.c src/ps2/ps2_pio_qspi.c @@ -115,7 +115,7 @@ target_include_directories(sd2psx PUBLIC ) pico_generate_pio_header(sd2psx ${CMAKE_CURRENT_LIST_DIR}/src/ps1/ps1_mc_spi.pio) -pico_generate_pio_header(sd2psx ${CMAKE_CURRENT_LIST_DIR}/src/ps2/ps2_mc_spi.pio) +pico_generate_pio_header(sd2psx ${CMAKE_CURRENT_LIST_DIR}/src/ps2/card_emu/ps2_mc_spi.pio) pico_generate_pio_header(sd2psx ${CMAKE_CURRENT_LIST_DIR}/src/ps2/ps2_qspi.pio) target_link_libraries(sd2psx diff --git a/src/gui.c b/src/gui.c index 76d169c..65799c6 100644 --- a/src/gui.c +++ b/src/gui.c @@ -16,7 +16,7 @@ #include "ps1/ps1_odeman.h" #include "ps1/ps1_memory_card.h" -#include "ps2/ps2_memory_card.h" +#include "ps2/card_emu/ps2_memory_card.h" #include "ps2/ps2_cardman.h" #include "ps2/ps2_dirty.h" #include "ps2/ps2_exploit.h" diff --git a/src/main.c b/src/main.c index 5748564..12ba55e 100644 --- a/src/main.c +++ b/src/main.c @@ -22,8 +22,8 @@ #include "ps1/ps1_cardman.h" #include "ps1/ps1_odeman.h" -#include "ps2/ps2_memory_card.h" #include "ps2/ps2_dirty.h" +#include "ps2/card_emu/ps2_memory_card.h" #include "ps2/ps2_cardman.h" #include "ps2/ps2_psram.h" #include "ps2/ps2_exploit.h" diff --git a/src/ps2/ps2_mc_spi.pio b/src/ps2/card_emu/ps2_mc_spi.pio similarity index 100% rename from src/ps2/ps2_mc_spi.pio rename to src/ps2/card_emu/ps2_mc_spi.pio diff --git a/src/ps2/ps2_memory_card.c b/src/ps2/card_emu/ps2_memory_card.c similarity index 98% rename from src/ps2/ps2_memory_card.c rename to src/ps2/card_emu/ps2_memory_card.c index e075362..9b37f71 100644 --- a/src/ps2/ps2_memory_card.c +++ b/src/ps2/card_emu/ps2_memory_card.c @@ -12,11 +12,11 @@ #include "keystore.h" #include "des.h" -#include "ps2_dirty.h" -#include "ps2_psram.h" -#include "ps2_pio_qspi.h" -#include "ps2_cardman.h" -#include "ps2_exploit.h" +#include "../ps2_dirty.h" +#include "../ps2_psram.h" +#include "../ps2_pio_qspi.h" +#include "../ps2_cardman.h" +#include "../ps2_exploit.h" #include #include diff --git a/src/ps2/ps2_memory_card.h b/src/ps2/card_emu/ps2_memory_card.h similarity index 100% rename from src/ps2/ps2_memory_card.h rename to src/ps2/card_emu/ps2_memory_card.h diff --git a/src/ps2/ps2_memory_card.in.c b/src/ps2/card_emu/ps2_memory_card.in.c similarity index 100% rename from src/ps2/ps2_memory_card.in.c rename to src/ps2/card_emu/ps2_memory_card.in.c From a138eaac00374191fde5b86042fec5514bcba075 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sat, 9 Sep 2023 19:27:47 +0200 Subject: [PATCH 67/91] First commands WiP --- src/ps2/card_emu/ps2_memory_card.c | 15 +- src/ps2/card_emu/ps2_memory_card.in.c | 429 ++++++++++++++------------ 2 files changed, 234 insertions(+), 210 deletions(-) diff --git a/src/ps2/card_emu/ps2_memory_card.c b/src/ps2/card_emu/ps2_memory_card.c index 9b37f71..d2684c4 100644 --- a/src/ps2/card_emu/ps2_memory_card.c +++ b/src/ps2/card_emu/ps2_memory_card.c @@ -316,15 +316,18 @@ static void __time_critical_func(mc_main_loop)(void) { recvfirst(); if (cmd == 0x81) { + void (*send)(uint8_t) = NULL; if (probe_clock()) { -#define send mc_respond_fast -#include "ps2_memory_card.in.c" -#undef send + send = &mc_respond_fast; +//#define send mc_respond_fast +//#include "ps2_memory_card.in.c" +//#undef send } else { -#define send mc_respond_slow -#include "ps2_memory_card.in.c" -#undef send + send = &mc_respond_slow; +//#define send mc_respond_slow +//#undef send } +#include "ps2_memory_card.in.c" } else { // not for us continue; diff --git a/src/ps2/card_emu/ps2_memory_card.in.c b/src/ps2/card_emu/ps2_memory_card.in.c index f29391d..8ab7d31 100644 --- a/src/ps2/card_emu/ps2_memory_card.in.c +++ b/src/ps2/card_emu/ps2_memory_card.in.c @@ -4,7 +4,7 @@ #define ARG8(a) a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7] /* resp to 0x81 */ -send(0xFF); +(*send)(0xFF); /* sub cmd */ recv(); @@ -15,11 +15,11 @@ if (ch != 0x42 && ch != 0x43) #endif if (ch == 0x11) { - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (ch == 0x12) { - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (ch == 0x21) { /* set address for erase */ union { @@ -27,15 +27,15 @@ if (ch == 0x11) { uint32_t addr; } raw; uint8_t ck; - send(0xFF); recv(); raw.a[0] = cmd; - send(0xFF); recv(); raw.a[1] = cmd; - send(0xFF); recv(); raw.a[2] = cmd; - send(0xFF); recv(); raw.a[3] = cmd; - send(0xFF); recv(); ck = cmd; - send(0x2B); recv(); + (*send)(0xFF); recv(); raw.a[0] = cmd; + (*send)(0xFF); recv(); raw.a[1] = cmd; + (*send)(0xFF); recv(); raw.a[2] = cmd; + (*send)(0xFF); recv(); raw.a[3] = cmd; + (*send)(0xFF); recv(); ck = cmd; + (*send)(0x2B); recv(); (void)ck; // TODO: validate checksum erase_sector = raw.addr; - send(term); + (*send)(term); } else if (cmd == 0x22) { /* set address for write */ union { @@ -43,17 +43,17 @@ if (ch == 0x11) { uint32_t addr; } raw; uint8_t ck; - send(0xFF); recv(); raw.a[0] = cmd; - send(0xFF); recv(); raw.a[1] = cmd; - send(0xFF); recv(); raw.a[2] = cmd; - send(0xFF); recv(); raw.a[3] = cmd; - send(0xFF); recv(); ck = cmd; - send(0x2B); recv(); + (*send)(0xFF); recv(); raw.a[0] = cmd; + (*send)(0xFF); recv(); raw.a[1] = cmd; + (*send)(0xFF); recv(); raw.a[2] = cmd; + (*send)(0xFF); recv(); raw.a[3] = cmd; + (*send)(0xFF); recv(); ck = cmd; + (*send)(0x2B); recv(); (void)ck; // TODO: validate checksum write_sector = raw.addr; is_write = 1; writeptr = 0; - send(term); + (*send)(term); } else if (ch == 0x23) { /* set address for read */ union { @@ -61,12 +61,12 @@ if (ch == 0x11) { uint32_t addr; } raw; uint8_t ck; - send(0xFF); recv(); raw.a[0] = cmd; - send(0xFF); recv(); raw.a[1] = cmd; - send(0xFF); recv(); raw.a[2] = cmd; - send(0xFF); recv(); raw.a[3] = cmd; - send(0xFF); recv(); ck = cmd; - send(0x2B); recv(); + (*send)(0xFF); recv(); raw.a[0] = cmd; + (*send)(0xFF); recv(); raw.a[1] = cmd; + (*send)(0xFF); recv(); raw.a[2] = cmd; + (*send)(0xFF); recv(); raw.a[3] = cmd; + (*send)(0xFF); recv(); ck = cmd; + (*send)(0x2B); recv(); (void)ck; // TODO: validate checksum read_sector = raw.addr; if (read_sector * 512 + 512 <= ps2_cardman_get_card_size()) { @@ -82,10 +82,10 @@ if (ch == 0x11) { eccptr = &readtmp.buf[512]; memset(eccptr, 0, 16); - send(term); + (*send)(term); } else if (ch == 0x26) { /* GET_SPECS ? */ - send(0x2B); recv(); + (*send)(0x2B); recv(); uint32_t sector_count = (flash_mode) ? PS2_CARD_SIZE_1M / 512 : (uint32_t)(ps2_cardman_get_card_size() / 512); uint8_t specs[] = { 0x00, 0x02, ERASE_SECTORS, 0x00, 0x00, 0x40, 0x00, 0x00 }; @@ -94,32 +94,32 @@ if (ch == 0x11) { specs[6] = (uint8_t)((sector_count >> 16) & 0xFF); specs[7] = (uint8_t)((sector_count >> 24) & 0xFF); - send(specs[0]); recv(); - send(specs[1]); recv(); - send(specs[2]); recv(); - send(specs[3]); recv(); - send(specs[4]); recv(); - send(specs[5]); recv(); - send(specs[6]); recv(); - send(specs[7]); recv(); - send(XOR8(specs)); recv(); - send(term); + (*send)(specs[0]); recv(); + (*send)(specs[1]); recv(); + (*send)(specs[2]); recv(); + (*send)(specs[3]); recv(); + (*send)(specs[4]); recv(); + (*send)(specs[5]); recv(); + (*send)(specs[6]); recv(); + (*send)(specs[7]); recv(); + (*send)(XOR8(specs)); recv(); + (*send)(term); } else if (ch == 0x27) { /* SET_TERMINATOR */ - send(0xFF); + (*send)(0xFF); recv(); term = cmd; - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (ch == 0x28) { /* GET_TERMINATOR */ - send(0x2B); recv(); - send(term); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); recv(); + (*send)(term); } else if (ch == 0x42) { /* write data */ uint8_t sz; - send(0xFF); recv(); sz = cmd; - send(0xFF); + (*send)(0xFF); recv(); sz = cmd; + (*send)(0xFF); #ifdef DEBUG_MC_PROTOCOL debug_printf("> %02X %02X\n", ch, sz); @@ -135,20 +135,20 @@ if (ch == 0x11) { ++writeptr; } ck ^= b; - send(0xFF); + (*send)(0xFF); } // this should be checksum? recv(); uint8_t ck2 = cmd; (void)ck2; // TODO: validate checksum - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (ch == 0x43) { /* read data */ uint8_t sz; - send(0xFF); recv(); sz = cmd; - send(0x2B); recv(); + (*send)(0xFF); recv(); sz = cmd; + (*send)(0x2B); recv(); #ifdef DEBUG_MC_PROTOCOL debug_printf("> %02X %02X\n", ch, sz); @@ -179,7 +179,7 @@ if (ch == 0x11) { if (readptr < sizeof(readtmp.buf)) { b = readtmp.buf[readptr]; - send(b); + (*send)(b); if (readptr <= 512) { uint8_t c = Table[b]; @@ -206,13 +206,13 @@ if (ch == 0x11) { } else { ++readptr; } - } else send(b); + } else (*send)(b); ck ^= b; recv(); } - send(ck); recv(); - send(term); + (*send)(ck); recv(); + (*send)(term); } else if (ch == 0x81) { /* commit for read/write? */ if (is_write) { @@ -237,8 +237,8 @@ if (ch == 0x11) { #endif } - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (ch == 0x82) { /* do erase */ if (erase_sector * 512 + 512 * ERASE_SECTORS <= ps2_cardman_get_card_size()) { @@ -254,266 +254,287 @@ if (ch == 0x11) { debug_printf("ER 0x%08X\n", erase_sector * 512); #endif } - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (ch == 0xBF) { - send(0xFF); recv(); - send(0x2B); recv(); - send(term); + (*send)(0xFF); recv(); + (*send)(0x2B); recv(); + (*send)(term); } else if (ch == 0xF3) { - send(0xFF); recv(); - send(0x2B); recv(); - send(term); + (*send)(0xFF); recv(); + (*send)(0x2B); recv(); + (*send)(term); } else if ((ch == 0xF7) && ps2_magicgate) { // TODO: it fails to get detected at all when ps2_magicgate==0, check if it's intentional /* SIO_MEMCARD_KEY_SELECT */ - send(0xFF); recv(); - send(0x2B); recv(); - send(term); + (*send)(0xFF); recv(); + (*send)(0x2B); recv(); + (*send)(term); } else if ((ch == 0xF0) && ps2_magicgate) { /* auth stuff */ - send(0xFF); + (*send)(0xFF); recv(); int subcmd = cmd; if (subcmd == 0) { /* probe support ? */ - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 1) { debug_printf("iv : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(iv)); /* get IV */ - send(0x2B); recv(); - send(iv[7]); recv(); - send(iv[6]); recv(); - send(iv[5]); recv(); - send(iv[4]); recv(); - send(iv[3]); recv(); - send(iv[2]); recv(); - send(iv[1]); recv(); - send(iv[0]); recv(); - send(XOR8(iv)); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(iv[7]); recv(); + (*send)(iv[6]); recv(); + (*send)(iv[5]); recv(); + (*send)(iv[4]); recv(); + (*send)(iv[3]); recv(); + (*send)(iv[2]); recv(); + (*send)(iv[1]); recv(); + (*send)(iv[0]); recv(); + (*send)(XOR8(iv)); recv(); + (*send)(term); } else if (subcmd == 2) { debug_printf("seed : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(seed)); /* get seed */ - send(0x2B); recv(); - send(seed[7]); recv(); - send(seed[6]); recv(); - send(seed[5]); recv(); - send(seed[4]); recv(); - send(seed[3]); recv(); - send(seed[2]); recv(); - send(seed[1]); recv(); - send(seed[0]); recv(); - send(XOR8(seed)); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(seed[7]); recv(); + (*send)(seed[6]); recv(); + (*send)(seed[5]); recv(); + (*send)(seed[4]); recv(); + (*send)(seed[3]); recv(); + (*send)(seed[2]); recv(); + (*send)(seed[1]); recv(); + (*send)(seed[0]); recv(); + (*send)(XOR8(seed)); recv(); + (*send)(term); } else if (subcmd == 3) { /* dummy 3 */ - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 4) { debug_printf("nonce : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(nonce)); /* get nonce */ - send(0x2B); recv(); - send(nonce[7]); recv(); - send(nonce[6]); recv(); - send(nonce[5]); recv(); - send(nonce[4]); recv(); - send(nonce[3]); recv(); - send(nonce[2]); recv(); - send(nonce[1]); recv(); - send(nonce[0]); recv(); - send(XOR8(nonce)); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(nonce[7]); recv(); + (*send)(nonce[6]); recv(); + (*send)(nonce[5]); recv(); + (*send)(nonce[4]); recv(); + (*send)(nonce[3]); recv(); + (*send)(nonce[2]); recv(); + (*send)(nonce[1]); recv(); + (*send)(nonce[0]); recv(); + (*send)(XOR8(nonce)); recv(); + (*send)(term); } else if (subcmd == 5) { /* dummy 5 */ - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 6) { /* MechaChallenge3 */ - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge3[7] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge3[6] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge3[5] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge3[4] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge3[3] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge3[2] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge3[1] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge3[0] = cmd; /* TODO: checksum below */ - send(0xFF); recv(); - send(0x2B); recv(); - send(term); + (*send)(0xFF); recv(); + (*send)(0x2B); recv(); + (*send)(term); debug_printf("MechaChallenge3 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(MechaChallenge3)); } else if (subcmd == 7) { /* MechaChallenge2 */ - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge2[7] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge2[6] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge2[5] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge2[4] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge2[3] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge2[2] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge2[1] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge2[0] = cmd; /* TODO: checksum below */ - send(0xFF); recv(); - send(0x2B); recv(); - send(term); + (*send)(0xFF); recv(); + (*send)(0x2B); recv(); + (*send)(term); debug_printf("MechaChallenge2 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(MechaChallenge2)); } else if (subcmd == 8) { /* dummy 8 */ - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 9) { /* dummy 9 */ - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 0xA) { /* dummy A */ - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 0xB) { /* MechaChallenge1 */ - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge1[7] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge1[6] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge1[5] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge1[4] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge1[3] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge1[2] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge1[1] = cmd; - send(0xFF); + (*send)(0xFF); recv(); MechaChallenge1[0] = cmd; /* TODO: checksum below */ - send(0xFF); recv(); - send(0x2B); recv(); - send(term); + (*send)(0xFF); recv(); + (*send)(0x2B); recv(); + (*send)(term); debug_printf("MechaChallenge1 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(MechaChallenge1)); } else if (subcmd == 0xC) { /* dummy C */ - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 0xD) { /* dummy D */ - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 0xE) { /* dummy E */ generateResponse(); debug_printf("CardResponse1 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(CardResponse1)); debug_printf("CardResponse2 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(CardResponse2)); debug_printf("CardResponse3 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(CardResponse3)); - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 0xF) { /* CardResponse1 */ - send(0x2B); recv(); - send(CardResponse1[7]); recv(); - send(CardResponse1[6]); recv(); - send(CardResponse1[5]); recv(); - send(CardResponse1[4]); recv(); - send(CardResponse1[3]); recv(); - send(CardResponse1[2]); recv(); - send(CardResponse1[1]); recv(); - send(CardResponse1[0]); recv(); - send(XOR8(CardResponse1)); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(CardResponse1[7]); recv(); + (*send)(CardResponse1[6]); recv(); + (*send)(CardResponse1[5]); recv(); + (*send)(CardResponse1[4]); recv(); + (*send)(CardResponse1[3]); recv(); + (*send)(CardResponse1[2]); recv(); + (*send)(CardResponse1[1]); recv(); + (*send)(CardResponse1[0]); recv(); + (*send)(XOR8(CardResponse1)); recv(); + (*send)(term); } else if (subcmd == 0x10) { /* dummy 10 */ - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 0x11) { /* CardResponse2 */ - send(0x2B); recv(); - send(CardResponse2[7]); recv(); - send(CardResponse2[6]); recv(); - send(CardResponse2[5]); recv(); - send(CardResponse2[4]); recv(); - send(CardResponse2[3]); recv(); - send(CardResponse2[2]); recv(); - send(CardResponse2[1]); recv(); - send(CardResponse2[0]); recv(); - send(XOR8(CardResponse2)); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(CardResponse2[7]); recv(); + (*send)(CardResponse2[6]); recv(); + (*send)(CardResponse2[5]); recv(); + (*send)(CardResponse2[4]); recv(); + (*send)(CardResponse2[3]); recv(); + (*send)(CardResponse2[2]); recv(); + (*send)(CardResponse2[1]); recv(); + (*send)(CardResponse2[0]); recv(); + (*send)(XOR8(CardResponse2)); recv(); + (*send)(term); } else if (subcmd == 0x12) { /* dummy 12 */ - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 0x13) { /* CardResponse3 */ - send(0x2B); recv(); - send(CardResponse3[7]); recv(); - send(CardResponse3[6]); recv(); - send(CardResponse3[5]); recv(); - send(CardResponse3[4]); recv(); - send(CardResponse3[3]); recv(); - send(CardResponse3[2]); recv(); - send(CardResponse3[1]); recv(); - send(CardResponse3[0]); recv(); - send(XOR8(CardResponse3)); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(CardResponse3[7]); recv(); + (*send)(CardResponse3[6]); recv(); + (*send)(CardResponse3[5]); recv(); + (*send)(CardResponse3[4]); recv(); + (*send)(CardResponse3[3]); recv(); + (*send)(CardResponse3[2]); recv(); + (*send)(CardResponse3[1]); recv(); + (*send)(CardResponse3[0]); recv(); + (*send)(XOR8(CardResponse3)); recv(); + (*send)(term); } else if (subcmd == 0x14) { /* dummy 14 */ - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else { debug_printf("unknown %02X -> %02X\n", ch, subcmd); } +} else if (ch == 0xA0) +{ + /* SD2PSXMAN Command */ + (*send)(0xFF); recv(); + int subcmd = cmd; + debug_printf("SD2PSXMAN %02X -> %02X\n", ch, subcmd); + (*send)(0x00); recv(); // Accept command + + switch(subcmd) { + case 0x20: // Ping Command + (*send)(0x01); recv(); // Protocol version + (*send)(0x01); recv(); // Product ID - 1 == SD2PSX + (*send)(0x27); recv(); // SD2PSX is available + (*send)(0xFF); recv(); // Terminate + break; + case 0x21: // Game ID command + (*send)(0x00); recv(); // Accept command + + + } + } else if ((ch == 0xF1 || ch == 0xF2) && ps2_magicgate) { /* session key encrypt */ - send(0xFF); + (*send)(0xFF); recv(); int subcmd = cmd; if (subcmd == 0x50 || subcmd == 0x40) { - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 0x51 || subcmd == 0x41) { - /* host sends key to us */ + /* host (*send)s key to us */ for (size_t i = 0; i < sizeof(hostkey); ++i) { - send(0xFF); + (*send)(0xFF); recv(); hostkey[i] = cmd; } - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 0x52 || subcmd == 0x42) { /* now we encrypt/decrypt the key */ - send(0x2B); recv(); - send(term); + (*send)(0x2B); recv(); + (*send)(term); } else if (subcmd == 0x53 || subcmd == 0x43) { - send(0x2B); recv(); - /* we send key to the host */ + (*send)(0x2B); recv(); + /* we (*send) key to the host */ for (size_t i = 0; i < sizeof(hostkey); ++i) { - send(hostkey[i]); + (*send)(hostkey[i]); recv(); } - send(term); + (*send)(term); } else { debug_printf("!! unknown subcmd %02X -> %02X\n", 0xF2, subcmd); } From 0e3d81997a05845920f9f730616797f8da0d526b Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 29 Oct 2023 19:45:46 +0100 Subject: [PATCH 68/91] Start refactoring card communication for better readability. --- src/ps2/card_emu/ps2_mc_spi.pio | 83 ++--- src/ps2/card_emu/ps2_memory_card.c | 196 ++++++++-- src/ps2/card_emu/ps2_memory_card.in.c | 500 ++++++++++++-------------- 3 files changed, 436 insertions(+), 343 deletions(-) diff --git a/src/ps2/card_emu/ps2_mc_spi.pio b/src/ps2/card_emu/ps2_mc_spi.pio index 386c58a..0ed0716 100644 --- a/src/ps2/card_emu/ps2_mc_spi.pio +++ b/src/ps2/card_emu/ps2_mc_spi.pio @@ -5,6 +5,7 @@ .define PUBLIC PIN_PSX_CLK 18 .define PUBLIC PIN_PSX_CMD 19 .define PUBLIC PIN_PSX_DAT 20 +.define PUBLIC PIN_PSX_SPD_SEL 10 .program cmd_reader wait 0 gpio PIN_PSX_SEL ; wait for SEL @@ -18,6 +19,7 @@ ; wait for SEL wait 0 gpio PIN_PSX_SEL .wrap_target +wait_pull: ; wait for the arm core to give us a byte to send pull block @@ -25,6 +27,18 @@ set pins, 0 [31] set pins, 1 + jmp pin send_fast + + ; Sending slow + set x, 7 +sendbit: + ; wait for falling clock edge + wait 1 gpio PIN_PSX_CLK + wait 0 gpio PIN_PSX_CLK + out pins 1 [2] + jmp x-- sendbit + jmp wait_pull +send_fast: ; we need to output bits one clock early due to rp2040 input/output delay ; so output the first bit here and the rest after the clock goes low out pins 1 @@ -38,33 +52,17 @@ out pins 1 [9] .wrap -.program dat_writer_slow - ; wait for SEL - wait 0 gpio PIN_PSX_SEL -.wrap_target - ; wait for the arm core to give us a byte to send - pull block - - ; pulse ACK - set pins, 0 [31] - set pins, 1 - - set x, 7 -sendbit: - ; wait for falling clock edge - wait 1 gpio PIN_PSX_CLK - wait 0 gpio PIN_PSX_CLK - out pins 1 [2] - jmp x-- sendbit -.wrap - .program clock_probe - wait 0 gpio PIN_PSX_SEL - wait 0 gpio PIN_PSX_CLK - nop [2] ; wait 24ns - this will get us into high clock phase for 25mhz, stay in low clock phase for 8mhz and under - in pins 1 + wait 0 gpio PIN_PSX_SEL + wait 0 gpio PIN_PSX_CLK [2] ; wait 24ns - this will get us into high clock phase for 25mhz, stay in low clock phase for 8mhz and under + jmp PIN setpin + set pins 0 +.wrap_target infloop: jmp infloop +setpin: + set pins 1 +.wrap % c-sdk { @@ -90,6 +88,7 @@ static inline void dat_writer_program_init(PIO pio, uint sm, uint offset) { sm_config_set_out_pins(&c, PIN_PSX_DAT, 1); sm_config_set_set_pins(&c, PIN_PSX_ACK, 1); + sm_config_set_jmp_pin(&c, PIN_PSX_SPD_SEL); /* configure ACK pin for output */ pio_sm_set_pins_with_mask(pio, sm, 1 << PIN_PSX_ACK, 1 << PIN_PSX_ACK); @@ -104,6 +103,7 @@ static inline void dat_writer_program_init(PIO pio, uint sm, uint offset) { /* SEL and CLK used as "wait" inputs only */ pio_sm_set_consecutive_pindirs(pio, sm, PIN_PSX_SEL, 1, false); pio_sm_set_consecutive_pindirs(pio, sm, PIN_PSX_CLK, 1, false); +// pio_sm_set_consecutive_pindirs(pio, sm, PIN_PSX_SPD_SEL, 1, false); /* shift OSR to right, autopull every 8 bits */ sm_config_set_out_shift(&c, true, true, 8); @@ -112,43 +112,24 @@ static inline void dat_writer_program_init(PIO pio, uint sm, uint offset) { pio_sm_init(pio, sm, offset, &c); } -static inline void dat_writer_slow_program_init(PIO pio, uint sm, uint offset) { - pio_sm_config c = dat_writer_slow_program_get_default_config(offset); - - sm_config_set_out_pins(&c, PIN_PSX_DAT, 1); - sm_config_set_set_pins(&c, PIN_PSX_ACK, 1); - - /* configure ACK pin for output */ - pio_sm_set_pins_with_mask(pio, sm, 1 << PIN_PSX_ACK, 1 << PIN_PSX_ACK); - pio_sm_set_consecutive_pindirs(pio, sm, PIN_PSX_ACK, 1, true); - pio_gpio_init(pio, PIN_PSX_ACK); - - /* configure DAT pin for output */ - pio_sm_set_pins_with_mask(pio, sm, 1 << PIN_PSX_DAT, 1 << PIN_PSX_DAT); - pio_sm_set_consecutive_pindirs(pio, sm, PIN_PSX_DAT, 1, true); - pio_gpio_init(pio, PIN_PSX_DAT); - - /* SEL and CLK used as "wait" inputs only */ - pio_sm_set_consecutive_pindirs(pio, sm, PIN_PSX_SEL, 1, false); - pio_sm_set_consecutive_pindirs(pio, sm, PIN_PSX_CLK, 1, false); - - /* shift OSR to right, autopull every 8 bits */ - sm_config_set_out_shift(&c, true, true, 8); - sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); - - pio_sm_init(pio, sm, offset, &c); -} static inline void clock_probe_program_init(PIO pio, uint sm, uint offset) { pio_sm_config c = clock_probe_program_get_default_config(offset); + pio_gpio_init(pio, PIN_PSX_SPD_SEL); - sm_config_set_in_pins(&c, PIN_PSX_CLK); pio_sm_set_consecutive_pindirs(pio, sm, PIN_PSX_SEL, 1, false); pio_sm_set_consecutive_pindirs(pio, sm, PIN_PSX_CLK, 1, false); + pio_sm_set_consecutive_pindirs(pio, sm, PIN_PSX_SPD_SEL, 1, true); + /* shift ISR to right, autopush every bit */ + sm_config_set_in_pins(&c, PIN_PSX_CLK); sm_config_set_in_shift(&c, true, true, 1); sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_RX); + sm_config_set_jmp_pin(&c, PIN_PSX_CLK); + sm_config_set_set_pins(&c, PIN_PSX_SPD_SEL, 1); + + pio_sm_set_pins_with_mask(pio, sm, 0, 1 << PIN_PSX_SPD_SEL); sm_config_set_clkdiv_int_frac(&c, 2, 0); diff --git a/src/ps2/card_emu/ps2_memory_card.c b/src/ps2/card_emu/ps2_memory_card.c index d2684c4..97f73d7 100644 --- a/src/ps2/card_emu/ps2_memory_card.c +++ b/src/ps2/card_emu/ps2_memory_card.c @@ -18,7 +18,10 @@ #include "../ps2_cardman.h" #include "../ps2_exploit.h" +#include "game_names/game_names.h" + #include +#include #include @@ -32,12 +35,20 @@ int ignore; uint8_t flag; bool flash_mode = false; +static char received_game_id[0x10] = { 0 }; + typedef struct { uint32_t offset; uint32_t sm; } pio_t; -pio_t cmd_reader, dat_writer, dat_writer_slow, clock_probe; +enum { + RECEIVE_RESET, + RECEIVE_EXIT, + RECEIVE_OK +}; + +pio_t cmd_reader, dat_writer, clock_probe; #define ERASE_SECTORS 16 #define CARD_SIZE (8 * 1024 * 1024) @@ -81,20 +92,18 @@ static inline void __time_critical_func(RAM_pio_sm_drain_tx_fifo)(PIO pio, uint } static void __time_critical_func(reset_pio)(void) { - pio_set_sm_mask_enabled(pio0, (1 << cmd_reader.sm) | (1 << dat_writer.sm) | (1 << dat_writer_slow.sm) | (1 << clock_probe.sm), false); - pio_restart_sm_mask(pio0, (1 << cmd_reader.sm) | (1 << dat_writer.sm) | (1 << dat_writer_slow.sm) | (1 << clock_probe.sm)); + pio_set_sm_mask_enabled(pio0, (1 << cmd_reader.sm) | (1 << dat_writer.sm) | (1 << clock_probe.sm), false); + pio_restart_sm_mask(pio0, (1 << cmd_reader.sm) | (1 << dat_writer.sm) | (1 << clock_probe.sm)); pio_sm_exec(pio0, cmd_reader.sm, pio_encode_jmp(cmd_reader.offset)); pio_sm_exec(pio0, dat_writer.sm, pio_encode_jmp(dat_writer.offset)); - pio_sm_exec(pio0, dat_writer_slow.sm, pio_encode_jmp(dat_writer_slow.offset)); pio_sm_exec(pio0, clock_probe.sm, pio_encode_jmp(clock_probe.offset)); pio_sm_clear_fifos(pio0, cmd_reader.sm); RAM_pio_sm_drain_tx_fifo(pio0, dat_writer.sm); - RAM_pio_sm_drain_tx_fifo(pio0, dat_writer_slow.sm); pio_sm_clear_fifos(pio0, clock_probe.sm); - pio_enable_sm_mask_in_sync(pio0, (1 << cmd_reader.sm) | (1 << dat_writer.sm) | (1 << dat_writer_slow.sm) | (1 << clock_probe.sm)); + pio_enable_sm_mask_in_sync(pio0, (1 << cmd_reader.sm) | (1 << dat_writer.sm) | (1 << clock_probe.sm)); reset = 1; } @@ -106,11 +115,13 @@ static void __time_critical_func(init_pio)(void) { gpio_set_dir(PIN_PSX_CLK, 0); gpio_set_dir(PIN_PSX_CMD, 0); gpio_set_dir(PIN_PSX_DAT, 0); + gpio_set_dir(PIN_PSX_SPD_SEL, true); gpio_disable_pulls(PIN_PSX_ACK); gpio_disable_pulls(PIN_PSX_SEL); gpio_disable_pulls(PIN_PSX_CLK); gpio_disable_pulls(PIN_PSX_CMD); gpio_disable_pulls(PIN_PSX_DAT); + gpio_disable_pulls(PIN_PSX_SPD_SEL); cmd_reader.offset = pio_add_program(pio0, &cmd_reader_program); cmd_reader.sm = pio_claim_unused_sm(pio0, true); @@ -118,15 +129,12 @@ static void __time_critical_func(init_pio)(void) { dat_writer.offset = pio_add_program(pio0, &dat_writer_program); dat_writer.sm = pio_claim_unused_sm(pio0, true); - dat_writer_slow.offset = pio_add_program(pio0, &dat_writer_slow_program); - dat_writer_slow.sm = pio_claim_unused_sm(pio0, true); clock_probe.offset = pio_add_program(pio0, &clock_probe_program); clock_probe.sm = pio_claim_unused_sm(pio0, true); cmd_reader_program_init(pio0, cmd_reader.sm, cmd_reader.offset); dat_writer_program_init(pio0, dat_writer.sm, dat_writer.offset); - dat_writer_slow_program_init(pio0, dat_writer_slow.sm, dat_writer_slow.offset); clock_probe_program_init(pio0, clock_probe.sm, clock_probe.offset); } @@ -136,6 +144,23 @@ static void __time_critical_func(card_deselected)(uint gpio, uint32_t event_mask } } +#define receiveOrNextCmd(cmd) if(receive(cmd)==RECEIVE_RESET) continue + +static inline __attribute__((always_inline)) uint8_t receive(uint8_t* cmd) { + while ( + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && + 1) { + if (reset) + return RECEIVE_RESET; + } + (*cmd) = (pio_sm_get(pio0, cmd_reader.sm) >> 24); + return RECEIVE_OK; +} + #define recv() do { \ while ( \ pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ @@ -150,6 +175,23 @@ static void __time_critical_func(card_deselected)(uint gpio, uint32_t event_mask cmd = (uint8_t) (pio_sm_get(pio0, cmd_reader.sm) >> 24); \ } while (0); +static inline __attribute__((always_inline)) uint8_t receiveFirst(uint8_t* cmd) { + while ( + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && + 1) { + if (reset) + return RECEIVE_RESET; + if (mc_exit_request) + return RECEIVE_EXIT; + } + (*cmd) = (pio_sm_get(pio0, cmd_reader.sm) >> 24); + return RECEIVE_OK; +} + #define recvfirst() do { \ while ( \ pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ @@ -166,17 +208,10 @@ static void __time_critical_func(card_deselected)(uint gpio, uint32_t event_mask cmd = (uint8_t) (pio_sm_get(pio0, cmd_reader.sm) >> 24); \ } while (0); -static inline uint32_t __time_critical_func(probe_clock)(void) { - return pio_sm_get_blocking(pio0, clock_probe.sm); -} - -static inline void __time_critical_func(mc_respond_fast)(uint8_t ch) { +static inline void __time_critical_func(mc_respond)(uint8_t ch) { pio_sm_put_blocking(pio0, dat_writer.sm, ch); } -static inline void __time_critical_func(mc_respond_slow)(uint8_t ch) { - pio_sm_put_blocking(pio0, dat_writer_slow.sm, ch); -} static uint8_t Table[] = { 0x00, 0x87, 0x96, 0x11, 0xa5, 0x22, 0x33, 0xb4,0xb4, 0x33, 0x22, 0xa5, 0x11, 0x96, 0x87, 0x00, @@ -303,37 +338,136 @@ static void __time_critical_func(generateResponse)() { } static void __time_critical_func(mc_main_loop)(void) { + bool next = false, exit = false; while (1) { uint8_t cmd, ch; -NEXTCMD: while (!reset && !reset && !reset && !reset && !reset) { if (mc_exit_request) goto EXIT_REQUEST; } reset = 0; - recvfirst(); + //recvfirst(); + switch (receiveFirst(&cmd)) { + case RECEIVE_EXIT: + exit = true; + break; + case RECEIVE_RESET: + next = true; + break; + default: + break; + } + if (next) + continue; + if (exit) + break; if (cmd == 0x81) { - void (*send)(uint8_t) = NULL; - if (probe_clock()) { - send = &mc_respond_fast; -//#define send mc_respond_fast -//#include "ps2_memory_card.in.c" -//#undef send - } else { - send = &mc_respond_slow; -//#define send mc_respond_slow -//#undef send - } #include "ps2_memory_card.in.c" + } else if (cmd == 0x8A) { + if (ch == 0xA0) { + /* SD2PSXMAN Command */ + int subcmd = cmd; + uint8_t game_id_length; + uint8_t input_buff[0xFF] = { 0 }; + mc_respond(0xFF); receiveOrNextCmd + (&cmd); + debug_printf("SD2PSXMAN %02X -> %02X\n", ch, subcmd); + mc_respond(0x00); receiveOrNextCmd + (&cmd); // Accept command + + switch(subcmd) { + case 0x20: // Ping Command + mc_respond(0x01); receiveOrNextCmd + (&cmd); // Protocol version + mc_respond(0x01); receiveOrNextCmd + (&cmd); // Product ID - 1 == SD2PSX + mc_respond(0x27); receiveOrNextCmd + (&cmd); // SD2PSX is available + mc_respond(0xFF); receiveOrNextCmd + (&cmd); // Terminate + break; + case 0x21: // Game ID command + memset(received_game_id, 0, sizeof(received_game_id)); + mc_respond(0x00); receiveOrNextCmd + (&cmd); // Accept command + mc_respond(0x00); receiveOrNextCmd + (&cmd); // Padding + game_id_length = cmd; + for (uint8_t i; (i < game_id_length) && (i < 0x10); i++) { + receiveOrNextCmd + (&cmd); // Receive Game ID char by char + input_buff[i] = cmd; + mc_respond(cmd); + } + game_names_extract_title_id(input_buff, received_game_id, game_id_length, sizeof(received_game_id)); + break; + case 0x22: // Change Channel command + mc_respond(0x00); receiveOrNextCmd + (&cmd); // Accept command + subcmd = cmd; + mc_respond(0x00); receiveOrNextCmd + (&cmd); + + switch (subcmd) { + case 0x00: + // ps2_cardman_set_channel(cmd); todo: Implement! + break; + case 0x01: + ps2_cardman_next_channel(); + break; + case 0x02: + ps2_cardman_prev_channel(); + break; + default: + break; + } + mc_respond(0x00); receiveOrNextCmd + (&cmd); + mc_respond(0x00); receiveOrNextCmd + (&cmd); + mc_respond(0xFF); + break; + case 0x23: // Change Slot command + mc_respond(0x00); receiveOrNextCmd + (&cmd); // Accept command + subcmd = cmd; + mc_respond(0x00); receiveOrNextCmd + (&cmd); + + switch (subcmd) { + case 0x00: + // ps2_cardman_set_idx(cmd); todo: Implement! + break; + case 0x01: + ps2_cardman_next_idx(); + break; + case 0x02: + ps2_cardman_prev_idx(); + break; + default: + break; + } + mc_respond(0x00); receiveOrNextCmd + (&cmd); + mc_respond(0x00); receiveOrNextCmd + (&cmd); + mc_respond(0xFF); + break; + default: + break; + + } + + } } else { // not for us continue; } - } + } EXIT_REQUEST: mc_exit_response = 1; } diff --git a/src/ps2/card_emu/ps2_memory_card.in.c b/src/ps2/card_emu/ps2_memory_card.in.c index 8ab7d31..acbc504 100644 --- a/src/ps2/card_emu/ps2_memory_card.in.c +++ b/src/ps2/card_emu/ps2_memory_card.in.c @@ -4,10 +4,10 @@ #define ARG8(a) a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7] /* resp to 0x81 */ -(*send)(0xFF); +mc_respond(0xFF); /* sub cmd */ -recv(); +receiveOrNextCmd(&cmd); ch = cmd; #ifdef DEBUG_MC_PROTOCOL if (ch != 0x42 && ch != 0x43) @@ -15,11 +15,11 @@ if (ch != 0x42 && ch != 0x43) #endif if (ch == 0x11) { - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (ch == 0x12) { - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (ch == 0x21) { /* set address for erase */ union { @@ -27,15 +27,15 @@ if (ch == 0x11) { uint32_t addr; } raw; uint8_t ck; - (*send)(0xFF); recv(); raw.a[0] = cmd; - (*send)(0xFF); recv(); raw.a[1] = cmd; - (*send)(0xFF); recv(); raw.a[2] = cmd; - (*send)(0xFF); recv(); raw.a[3] = cmd; - (*send)(0xFF); recv(); ck = cmd; - (*send)(0x2B); recv(); + mc_respond(0xFF); receiveOrNextCmd(&raw.a[0]); + mc_respond(0xFF); receiveOrNextCmd(&raw.a[1]); + mc_respond(0xFF); receiveOrNextCmd(&raw.a[2]); + mc_respond(0xFF); receiveOrNextCmd(&raw.a[3]); + mc_respond(0xFF); receiveOrNextCmd(&ck); + mc_respond(0x2B); receiveOrNextCmd(&cmd); (void)ck; // TODO: validate checksum erase_sector = raw.addr; - (*send)(term); + mc_respond(term); } else if (cmd == 0x22) { /* set address for write */ union { @@ -43,17 +43,17 @@ if (ch == 0x11) { uint32_t addr; } raw; uint8_t ck; - (*send)(0xFF); recv(); raw.a[0] = cmd; - (*send)(0xFF); recv(); raw.a[1] = cmd; - (*send)(0xFF); recv(); raw.a[2] = cmd; - (*send)(0xFF); recv(); raw.a[3] = cmd; - (*send)(0xFF); recv(); ck = cmd; - (*send)(0x2B); recv(); + mc_respond(0xFF); receiveOrNextCmd(&raw.a[0]); + mc_respond(0xFF); receiveOrNextCmd(&raw.a[1]); + mc_respond(0xFF); receiveOrNextCmd(&raw.a[2]); + mc_respond(0xFF); receiveOrNextCmd(&raw.a[3]); + mc_respond(0xFF); receiveOrNextCmd(&ck); + mc_respond(0x2B); receiveOrNextCmd(&cmd); (void)ck; // TODO: validate checksum write_sector = raw.addr; is_write = 1; writeptr = 0; - (*send)(term); + mc_respond(term); } else if (ch == 0x23) { /* set address for read */ union { @@ -61,12 +61,12 @@ if (ch == 0x11) { uint32_t addr; } raw; uint8_t ck; - (*send)(0xFF); recv(); raw.a[0] = cmd; - (*send)(0xFF); recv(); raw.a[1] = cmd; - (*send)(0xFF); recv(); raw.a[2] = cmd; - (*send)(0xFF); recv(); raw.a[3] = cmd; - (*send)(0xFF); recv(); ck = cmd; - (*send)(0x2B); recv(); + mc_respond(0xFF); receiveOrNextCmd(&raw.a[0]); + mc_respond(0xFF); receiveOrNextCmd(&raw.a[1]); + mc_respond(0xFF); receiveOrNextCmd(&raw.a[2]); + mc_respond(0xFF); receiveOrNextCmd(&raw.a[3]); + mc_respond(0xFF); receiveOrNextCmd(&ck); + mc_respond(0x2B); receiveOrNextCmd(&cmd); (void)ck; // TODO: validate checksum read_sector = raw.addr; if (read_sector * 512 + 512 <= ps2_cardman_get_card_size()) { @@ -82,10 +82,10 @@ if (ch == 0x11) { eccptr = &readtmp.buf[512]; memset(eccptr, 0, 16); - (*send)(term); + mc_respond(term); } else if (ch == 0x26) { /* GET_SPECS ? */ - (*send)(0x2B); recv(); + mc_respond(0x2B); receiveOrNextCmd(&cmd); uint32_t sector_count = (flash_mode) ? PS2_CARD_SIZE_1M / 512 : (uint32_t)(ps2_cardman_get_card_size() / 512); uint8_t specs[] = { 0x00, 0x02, ERASE_SECTORS, 0x00, 0x00, 0x40, 0x00, 0x00 }; @@ -94,32 +94,32 @@ if (ch == 0x11) { specs[6] = (uint8_t)((sector_count >> 16) & 0xFF); specs[7] = (uint8_t)((sector_count >> 24) & 0xFF); - (*send)(specs[0]); recv(); - (*send)(specs[1]); recv(); - (*send)(specs[2]); recv(); - (*send)(specs[3]); recv(); - (*send)(specs[4]); recv(); - (*send)(specs[5]); recv(); - (*send)(specs[6]); recv(); - (*send)(specs[7]); recv(); - (*send)(XOR8(specs)); recv(); - (*send)(term); + mc_respond(specs[0]); receiveOrNextCmd(&cmd); + mc_respond(specs[1]); receiveOrNextCmd(&cmd); + mc_respond(specs[2]); receiveOrNextCmd(&cmd); + mc_respond(specs[3]); receiveOrNextCmd(&cmd); + mc_respond(specs[4]); receiveOrNextCmd(&cmd); + mc_respond(specs[5]); receiveOrNextCmd(&cmd); + mc_respond(specs[6]); receiveOrNextCmd(&cmd); + mc_respond(specs[7]); receiveOrNextCmd(&cmd); + mc_respond(XOR8(specs)); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (ch == 0x27) { /* SET_TERMINATOR */ - (*send)(0xFF); - recv(); term = cmd; - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0xFF); + receiveOrNextCmd(&term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (ch == 0x28) { /* GET_TERMINATOR */ - (*send)(0x2B); recv(); - (*send)(term); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (ch == 0x42) { /* write data */ uint8_t sz; - (*send)(0xFF); recv(); sz = cmd; - (*send)(0xFF); + mc_respond(0xFF); receiveOrNextCmd(&sz); + mc_respond(0xFF); #ifdef DEBUG_MC_PROTOCOL debug_printf("> %02X %02X\n", ch, sz); @@ -129,26 +129,26 @@ if (ch == 0x11) { uint8_t b; for (int i = 0; i < sz; ++i) { - recv(); b = cmd; + receiveOrNextCmd(&b); if (writeptr < sizeof(writetmp)) { writetmp[writeptr] = b; ++writeptr; } ck ^= b; - (*send)(0xFF); + mc_respond(0xFF); } // this should be checksum? - recv(); + receiveOrNextCmd(&cmd); uint8_t ck2 = cmd; (void)ck2; // TODO: validate checksum - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (ch == 0x43) { /* read data */ uint8_t sz; - (*send)(0xFF); recv(); sz = cmd; - (*send)(0x2B); recv(); + mc_respond(0xFF); receiveOrNextCmd(&sz); + mc_respond(0x2B); receiveOrNextCmd(&cmd); #ifdef DEBUG_MC_PROTOCOL debug_printf("> %02X %02X\n", ch, sz); @@ -179,7 +179,7 @@ if (ch == 0x11) { if (readptr < sizeof(readtmp.buf)) { b = readtmp.buf[readptr]; - (*send)(b); + mc_respond(b); if (readptr <= 512) { uint8_t c = Table[b]; @@ -206,13 +206,13 @@ if (ch == 0x11) { } else { ++readptr; } - } else (*send)(b); + } else mc_respond(b); ck ^= b; - recv(); + receiveOrNextCmd(&cmd); } - (*send)(ck); recv(); - (*send)(term); + mc_respond(ck); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (ch == 0x81) { /* commit for read/write? */ if (is_write) { @@ -237,8 +237,8 @@ if (ch == 0x11) { #endif } - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (ch == 0x82) { /* do erase */ if (erase_sector * 512 + 512 * ERASE_SECTORS <= ps2_cardman_get_card_size()) { @@ -254,287 +254,265 @@ if (ch == 0x11) { debug_printf("ER 0x%08X\n", erase_sector * 512); #endif } - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (ch == 0xBF) { - (*send)(0xFF); recv(); - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0xFF); receiveOrNextCmd(&cmd); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (ch == 0xF3) { - (*send)(0xFF); recv(); - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0xFF); receiveOrNextCmd(&cmd); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if ((ch == 0xF7) && ps2_magicgate) { // TODO: it fails to get detected at all when ps2_magicgate==0, check if it's intentional /* SIO_MEMCARD_KEY_SELECT */ - (*send)(0xFF); recv(); - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0xFF); receiveOrNextCmd(&cmd); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if ((ch == 0xF0) && ps2_magicgate) { /* auth stuff */ - (*send)(0xFF); - recv(); - int subcmd = cmd; + mc_respond(0xFF); + int subcmd; + receiveOrNextCmd(&subcmd); if (subcmd == 0) { /* probe support ? */ - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 1) { debug_printf("iv : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(iv)); /* get IV */ - (*send)(0x2B); recv(); - (*send)(iv[7]); recv(); - (*send)(iv[6]); recv(); - (*send)(iv[5]); recv(); - (*send)(iv[4]); recv(); - (*send)(iv[3]); recv(); - (*send)(iv[2]); recv(); - (*send)(iv[1]); recv(); - (*send)(iv[0]); recv(); - (*send)(XOR8(iv)); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(iv[7]); receiveOrNextCmd(&cmd); + mc_respond(iv[6]); receiveOrNextCmd(&cmd); + mc_respond(iv[5]); receiveOrNextCmd(&cmd); + mc_respond(iv[4]); receiveOrNextCmd(&cmd); + mc_respond(iv[3]); receiveOrNextCmd(&cmd); + mc_respond(iv[2]); receiveOrNextCmd(&cmd); + mc_respond(iv[1]); receiveOrNextCmd(&cmd); + mc_respond(iv[0]); receiveOrNextCmd(&cmd); + mc_respond(XOR8(iv)); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 2) { debug_printf("seed : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(seed)); /* get seed */ - (*send)(0x2B); recv(); - (*send)(seed[7]); recv(); - (*send)(seed[6]); recv(); - (*send)(seed[5]); recv(); - (*send)(seed[4]); recv(); - (*send)(seed[3]); recv(); - (*send)(seed[2]); recv(); - (*send)(seed[1]); recv(); - (*send)(seed[0]); recv(); - (*send)(XOR8(seed)); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(seed[7]); receiveOrNextCmd(&cmd); + mc_respond(seed[6]); receiveOrNextCmd(&cmd); + mc_respond(seed[5]); receiveOrNextCmd(&cmd); + mc_respond(seed[4]); receiveOrNextCmd(&cmd); + mc_respond(seed[3]); receiveOrNextCmd(&cmd); + mc_respond(seed[2]); receiveOrNextCmd(&cmd); + mc_respond(seed[1]); receiveOrNextCmd(&cmd); + mc_respond(seed[0]); receiveOrNextCmd(&cmd); + mc_respond(XOR8(seed)); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 3) { /* dummy 3 */ - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 4) { debug_printf("nonce : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(nonce)); /* get nonce */ - (*send)(0x2B); recv(); - (*send)(nonce[7]); recv(); - (*send)(nonce[6]); recv(); - (*send)(nonce[5]); recv(); - (*send)(nonce[4]); recv(); - (*send)(nonce[3]); recv(); - (*send)(nonce[2]); recv(); - (*send)(nonce[1]); recv(); - (*send)(nonce[0]); recv(); - (*send)(XOR8(nonce)); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(nonce[7]); receiveOrNextCmd(&cmd); + mc_respond(nonce[6]); receiveOrNextCmd(&cmd); + mc_respond(nonce[5]); receiveOrNextCmd(&cmd); + mc_respond(nonce[4]); receiveOrNextCmd(&cmd); + mc_respond(nonce[3]); receiveOrNextCmd(&cmd); + mc_respond(nonce[2]); receiveOrNextCmd(&cmd); + mc_respond(nonce[1]); receiveOrNextCmd(&cmd); + mc_respond(nonce[0]); receiveOrNextCmd(&cmd); + mc_respond(XOR8(nonce)); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 5) { /* dummy 5 */ - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 6) { /* MechaChallenge3 */ - (*send)(0xFF); - recv(); MechaChallenge3[7] = cmd; - (*send)(0xFF); - recv(); MechaChallenge3[6] = cmd; - (*send)(0xFF); - recv(); MechaChallenge3[5] = cmd; - (*send)(0xFF); - recv(); MechaChallenge3[4] = cmd; - (*send)(0xFF); - recv(); MechaChallenge3[3] = cmd; - (*send)(0xFF); - recv(); MechaChallenge3[2] = cmd; - (*send)(0xFF); - recv(); MechaChallenge3[1] = cmd; - (*send)(0xFF); - recv(); MechaChallenge3[0] = cmd; + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[7]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[6]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[5]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[4]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[3]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[2]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[1]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[0]); /* TODO: checksum below */ - (*send)(0xFF); recv(); - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0xFF); receiveOrNextCmd(&cmd); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); debug_printf("MechaChallenge3 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(MechaChallenge3)); } else if (subcmd == 7) { /* MechaChallenge2 */ - (*send)(0xFF); - recv(); MechaChallenge2[7] = cmd; - (*send)(0xFF); - recv(); MechaChallenge2[6] = cmd; - (*send)(0xFF); - recv(); MechaChallenge2[5] = cmd; - (*send)(0xFF); - recv(); MechaChallenge2[4] = cmd; - (*send)(0xFF); - recv(); MechaChallenge2[3] = cmd; - (*send)(0xFF); - recv(); MechaChallenge2[2] = cmd; - (*send)(0xFF); - recv(); MechaChallenge2[1] = cmd; - (*send)(0xFF); - recv(); MechaChallenge2[0] = cmd; + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[7]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[6]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[5]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[4]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[3]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[2]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[1]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[0]); /* TODO: checksum below */ - (*send)(0xFF); recv(); - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0xFF); receiveOrNextCmd(&cmd); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); debug_printf("MechaChallenge2 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(MechaChallenge2)); } else if (subcmd == 8) { /* dummy 8 */ - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 9) { /* dummy 9 */ - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 0xA) { /* dummy A */ - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 0xB) { /* MechaChallenge1 */ - (*send)(0xFF); - recv(); MechaChallenge1[7] = cmd; - (*send)(0xFF); - recv(); MechaChallenge1[6] = cmd; - (*send)(0xFF); - recv(); MechaChallenge1[5] = cmd; - (*send)(0xFF); - recv(); MechaChallenge1[4] = cmd; - (*send)(0xFF); - recv(); MechaChallenge1[3] = cmd; - (*send)(0xFF); - recv(); MechaChallenge1[2] = cmd; - (*send)(0xFF); - recv(); MechaChallenge1[1] = cmd; - (*send)(0xFF); - recv(); MechaChallenge1[0] = cmd; + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[7]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[6]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[5]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[4]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[3]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[2]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[1]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[0]); /* TODO: checksum below */ - (*send)(0xFF); recv(); - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0xFF); receiveOrNextCmd(&cmd); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); debug_printf("MechaChallenge1 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(MechaChallenge1)); } else if (subcmd == 0xC) { /* dummy C */ - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 0xD) { /* dummy D */ - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 0xE) { /* dummy E */ generateResponse(); debug_printf("CardResponse1 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(CardResponse1)); debug_printf("CardResponse2 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(CardResponse2)); debug_printf("CardResponse3 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(CardResponse3)); - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 0xF) { /* CardResponse1 */ - (*send)(0x2B); recv(); - (*send)(CardResponse1[7]); recv(); - (*send)(CardResponse1[6]); recv(); - (*send)(CardResponse1[5]); recv(); - (*send)(CardResponse1[4]); recv(); - (*send)(CardResponse1[3]); recv(); - (*send)(CardResponse1[2]); recv(); - (*send)(CardResponse1[1]); recv(); - (*send)(CardResponse1[0]); recv(); - (*send)(XOR8(CardResponse1)); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(CardResponse1[7]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse1[6]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse1[5]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse1[4]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse1[3]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse1[2]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse1[1]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse1[0]); receiveOrNextCmd(&cmd); + mc_respond(XOR8(CardResponse1)); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 0x10) { /* dummy 10 */ - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 0x11) { /* CardResponse2 */ - (*send)(0x2B); recv(); - (*send)(CardResponse2[7]); recv(); - (*send)(CardResponse2[6]); recv(); - (*send)(CardResponse2[5]); recv(); - (*send)(CardResponse2[4]); recv(); - (*send)(CardResponse2[3]); recv(); - (*send)(CardResponse2[2]); recv(); - (*send)(CardResponse2[1]); recv(); - (*send)(CardResponse2[0]); recv(); - (*send)(XOR8(CardResponse2)); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(CardResponse2[7]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse2[6]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse2[5]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse2[4]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse2[3]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse2[2]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse2[1]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse2[0]); receiveOrNextCmd(&cmd); + mc_respond(XOR8(CardResponse2)); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 0x12) { /* dummy 12 */ - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 0x13) { /* CardResponse3 */ - (*send)(0x2B); recv(); - (*send)(CardResponse3[7]); recv(); - (*send)(CardResponse3[6]); recv(); - (*send)(CardResponse3[5]); recv(); - (*send)(CardResponse3[4]); recv(); - (*send)(CardResponse3[3]); recv(); - (*send)(CardResponse3[2]); recv(); - (*send)(CardResponse3[1]); recv(); - (*send)(CardResponse3[0]); recv(); - (*send)(XOR8(CardResponse3)); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(CardResponse3[7]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse3[6]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse3[5]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse3[4]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse3[3]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse3[2]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse3[1]); receiveOrNextCmd(&cmd); + mc_respond(CardResponse3[0]); receiveOrNextCmd(&cmd); + mc_respond(XOR8(CardResponse3)); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 0x14) { /* dummy 14 */ - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else { debug_printf("unknown %02X -> %02X\n", ch, subcmd); } -} else if (ch == 0xA0) -{ - /* SD2PSXMAN Command */ - (*send)(0xFF); recv(); - int subcmd = cmd; - debug_printf("SD2PSXMAN %02X -> %02X\n", ch, subcmd); - (*send)(0x00); recv(); // Accept command - - switch(subcmd) { - case 0x20: // Ping Command - (*send)(0x01); recv(); // Protocol version - (*send)(0x01); recv(); // Product ID - 1 == SD2PSX - (*send)(0x27); recv(); // SD2PSX is available - (*send)(0xFF); recv(); // Terminate - break; - case 0x21: // Game ID command - (*send)(0x00); recv(); // Accept command - - - } - } else if ((ch == 0xF1 || ch == 0xF2) && ps2_magicgate) { - /* session key encrypt */ - (*send)(0xFF); - recv(); int subcmd = cmd; + /* session key encrypt */ + mc_respond(0xFF); + receiveOrNextCmd(&subcmd); if (subcmd == 0x50 || subcmd == 0x40) { - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 0x51 || subcmd == 0x41) { - /* host (*send)s key to us */ + /* host mc_responds key to us */ for (size_t i = 0; i < sizeof(hostkey); ++i) { - (*send)(0xFF); - recv(); - hostkey[i] = cmd; + mc_respond(0xFF); + receiveOrNextCmd(&hostkey[i]); } - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 0x52 || subcmd == 0x42) { /* now we encrypt/decrypt the key */ - (*send)(0x2B); recv(); - (*send)(term); + mc_respond(0x2B); receiveOrNextCmd(&cmd); + mc_respond(term); } else if (subcmd == 0x53 || subcmd == 0x43) { - (*send)(0x2B); recv(); - /* we (*send) key to the host */ + mc_respond(0x2B); receiveOrNextCmd(&cmd); + /* we mc_respond key to the host */ for (size_t i = 0; i < sizeof(hostkey); ++i) { - (*send)(hostkey[i]); - recv(); + mc_respond(hostkey[i]); + receiveOrNextCmd(&cmd); } - (*send)(term); + mc_respond(term); } else { debug_printf("!! unknown subcmd %02X -> %02X\n", 0xF2, subcmd); } From a3e92297fd6eac97248fdb7f7ebf9af50a7f05d8 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 30 Oct 2023 09:33:46 +0100 Subject: [PATCH 69/91] small fixes --- src/ps2/card_emu/ps2_memory_card.c | 503 +++++++++++--------------- src/ps2/card_emu/ps2_memory_card.in.c | 97 ++--- 2 files changed, 269 insertions(+), 331 deletions(-) diff --git a/src/ps2/card_emu/ps2_memory_card.c b/src/ps2/card_emu/ps2_memory_card.c index 97f73d7..fb689e6 100644 --- a/src/ps2/card_emu/ps2_memory_card.c +++ b/src/ps2/card_emu/ps2_memory_card.c @@ -1,30 +1,27 @@ +#include "../ps2_cardman.h" +#include "../ps2_dirty.h" +#include "../ps2_exploit.h" +#include "../ps2_pio_qspi.h" +#include "../ps2_psram.h" +#include "config.h" +#include "debug.h" +#include "des.h" +#include "flashmap.h" +#include "game_names/game_names.h" +#include "hardware/dma.h" +#include "hardware/flash.h" #include "hardware/gpio.h" #include "hardware/regs/addressmap.h" #include "hardware/timer.h" -#include "hardware/flash.h" -#include "hardware/dma.h" +#include "keystore.h" #include "pico/platform.h" - -#include "config.h" #include "ps2_mc_spi.pio.h" -#include "flashmap.h" -#include "debug.h" -#include "keystore.h" -#include "des.h" - -#include "../ps2_dirty.h" -#include "../ps2_psram.h" -#include "../ps2_pio_qspi.h" -#include "../ps2_cardman.h" -#include "../ps2_exploit.h" - -#include "game_names/game_names.h" +// #include "ps2_mx4sio.pio.h" #include #include #include - // #define DEBUG_MC_PROTOCOL uint64_t us_startup; @@ -35,23 +32,19 @@ int ignore; uint8_t flag; bool flash_mode = false; -static char received_game_id[0x10] = { 0 }; +static char received_game_id[0x10] = {0}; typedef struct { uint32_t offset; uint32_t sm; } pio_t; -enum { - RECEIVE_RESET, - RECEIVE_EXIT, - RECEIVE_OK -}; +enum { RECEIVE_RESET, RECEIVE_EXIT, RECEIVE_OK }; -pio_t cmd_reader, dat_writer, clock_probe; +pio_t cmd_reader, dat_writer, clock_probe; #define ERASE_SECTORS 16 -#define CARD_SIZE (8 * 1024 * 1024) +#define CARD_SIZE (8 * 1024 * 1024) uint8_t term = 0xFF; uint32_t read_sector, write_sector, erase_sector; @@ -84,8 +77,7 @@ static inline void __time_critical_func(write_mc)(uint32_t addr, void *buf, size } static inline void __time_critical_func(RAM_pio_sm_drain_tx_fifo)(PIO pio, uint sm) { - uint instr = (pio->sm[sm].shiftctrl & PIO_SM0_SHIFTCTRL_AUTOPULL_BITS) ? pio_encode_out(pio_null, 32) : - pio_encode_pull(false, false); + uint instr = (pio->sm[sm].shiftctrl & PIO_SM0_SHIFTCTRL_AUTOPULL_BITS) ? pio_encode_out(pio_null, 32) : pio_encode_pull(false, false); while (!pio_sm_is_tx_fifo_empty(pio, sm)) { pio_sm_exec(pio, sm, instr); } @@ -129,7 +121,6 @@ static void __time_critical_func(init_pio)(void) { dat_writer.offset = pio_add_program(pio0, &dat_writer_program); dat_writer.sm = pio_claim_unused_sm(pio0, true); - clock_probe.offset = pio_add_program(pio0, &clock_probe_program); clock_probe.sm = pio_claim_unused_sm(pio0, true); @@ -144,125 +135,100 @@ static void __time_critical_func(card_deselected)(uint gpio, uint32_t event_mask } } -#define receiveOrNextCmd(cmd) if(receive(cmd)==RECEIVE_RESET) continue - -static inline __attribute__((always_inline)) uint8_t receive(uint8_t* cmd) { - while ( - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && - 1) { - if (reset) - return RECEIVE_RESET; - } +static inline __attribute__((always_inline)) uint8_t receive(uint8_t *cmd) { + while (pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && 1) { + if (reset) + return RECEIVE_RESET; + } (*cmd) = (pio_sm_get(pio0, cmd_reader.sm) >> 24); return RECEIVE_OK; } -#define recv() do { \ - while ( \ - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ - 1) { \ - if (reset) \ - goto NEXTCMD; \ - } \ - cmd = (uint8_t) (pio_sm_get(pio0, cmd_reader.sm) >> 24); \ -} while (0); - -static inline __attribute__((always_inline)) uint8_t receiveFirst(uint8_t* cmd) { - while ( - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && - 1) { - if (reset) +#define recv() \ + do { \ + while (pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && 1) { \ + if (reset) \ + goto NEXTCMD; \ + } \ + cmd = (uint8_t)(pio_sm_get(pio0, cmd_reader.sm) >> 24); \ + } while (0); + +#define receiveOrNextCmd(cmd) \ + if (receive(cmd) == RECEIVE_RESET) \ + continue + +static inline __attribute__((always_inline)) uint8_t receiveFirst(uint8_t *cmd) { + while (pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && 1) { + if (reset) return RECEIVE_RESET; if (mc_exit_request) return RECEIVE_EXIT; - } + } (*cmd) = (pio_sm_get(pio0, cmd_reader.sm) >> 24); return RECEIVE_OK; } -#define recvfirst() do { \ - while ( \ - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ - 1) { \ - if (reset) \ - goto NEXTCMD; \ - if (mc_exit_request) \ - goto EXIT_REQUEST; \ - } \ - cmd = (uint8_t) (pio_sm_get(pio0, cmd_reader.sm) >> 24); \ -} while (0); +#define recvfirst() \ + do { \ + while (pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ + pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && 1) { \ + if (reset) \ + goto NEXTCMD; \ + if (mc_exit_request) \ + goto EXIT_REQUEST; \ + } \ + cmd = (uint8_t)(pio_sm_get(pio0, cmd_reader.sm) >> 24); \ + } while (0); static inline void __time_critical_func(mc_respond)(uint8_t ch) { pio_sm_put_blocking(pio0, dat_writer.sm, ch); } - static uint8_t Table[] = { - 0x00, 0x87, 0x96, 0x11, 0xa5, 0x22, 0x33, 0xb4,0xb4, 0x33, 0x22, 0xa5, 0x11, 0x96, 0x87, 0x00, - 0xc3, 0x44, 0x55, 0xd2, 0x66, 0xe1, 0xf0, 0x77,0x77, 0xf0, 0xe1, 0x66, 0xd2, 0x55, 0x44, 0xc3, - 0xd2, 0x55, 0x44, 0xc3, 0x77, 0xf0, 0xe1, 0x66,0x66, 0xe1, 0xf0, 0x77, 0xc3, 0x44, 0x55, 0xd2, - 0x11, 0x96, 0x87, 0x00, 0xb4, 0x33, 0x22, 0xa5,0xa5, 0x22, 0x33, 0xb4, 0x00, 0x87, 0x96, 0x11, - 0xe1, 0x66, 0x77, 0xf0, 0x44, 0xc3, 0xd2, 0x55,0x55, 0xd2, 0xc3, 0x44, 0xf0, 0x77, 0x66, 0xe1, - 0x22, 0xa5, 0xb4, 0x33, 0x87, 0x00, 0x11, 0x96,0x96, 0x11, 0x00, 0x87, 0x33, 0xb4, 0xa5, 0x22, - 0x33, 0xb4, 0xa5, 0x22, 0x96, 0x11, 0x00, 0x87,0x87, 0x00, 0x11, 0x96, 0x22, 0xa5, 0xb4, 0x33, - 0xf0, 0x77, 0x66, 0xe1, 0x55, 0xd2, 0xc3, 0x44,0x44, 0xc3, 0xd2, 0x55, 0xe1, 0x66, 0x77, 0xf0, - 0xf0, 0x77, 0x66, 0xe1, 0x55, 0xd2, 0xc3, 0x44,0x44, 0xc3, 0xd2, 0x55, 0xe1, 0x66, 0x77, 0xf0, - 0x33, 0xb4, 0xa5, 0x22, 0x96, 0x11, 0x00, 0x87,0x87, 0x00, 0x11, 0x96, 0x22, 0xa5, 0xb4, 0x33, - 0x22, 0xa5, 0xb4, 0x33, 0x87, 0x00, 0x11, 0x96,0x96, 0x11, 0x00, 0x87, 0x33, 0xb4, 0xa5, 0x22, - 0xe1, 0x66, 0x77, 0xf0, 0x44, 0xc3, 0xd2, 0x55,0x55, 0xd2, 0xc3, 0x44, 0xf0, 0x77, 0x66, 0xe1, - 0x11, 0x96, 0x87, 0x00, 0xb4, 0x33, 0x22, 0xa5,0xa5, 0x22, 0x33, 0xb4, 0x00, 0x87, 0x96, 0x11, - 0xd2, 0x55, 0x44, 0xc3, 0x77, 0xf0, 0xe1, 0x66,0x66, 0xe1, 0xf0, 0x77, 0xc3, 0x44, 0x55, 0xd2, - 0xc3, 0x44, 0x55, 0xd2, 0x66, 0xe1, 0xf0, 0x77,0x77, 0xf0, 0xe1, 0x66, 0xd2, 0x55, 0x44, 0xc3, - 0x00, 0x87, 0x96, 0x11, 0xa5, 0x22, 0x33, 0xb4,0xb4, 0x33, 0x22, 0xa5, 0x11, 0x96, 0x87, 0x00 -}; - -void calcECC(uint8_t *ecc, const uint8_t *data) -{ - int i, c; - - ecc[0] = ecc[1] = ecc[2] = 0; - - for (i = 0 ; i < 0x80 ; i ++) { - c = Table[data[i]]; - - ecc[0] ^= c; - if (c & 0x80) { - ecc[1] ^= ~i; - ecc[2] ^= i; - } - } - ecc[0] = ~ecc[0]; - ecc[0] &= 0x77; - - ecc[1] = ~ecc[1]; - ecc[1] &= 0x7f; - - ecc[2] = ~ecc[2]; - ecc[2] &= 0x7f; - - return; + 0x00, 0x87, 0x96, 0x11, 0xa5, 0x22, 0x33, 0xb4, 0xb4, 0x33, 0x22, 0xa5, 0x11, 0x96, 0x87, 0x00, 0xc3, 0x44, 0x55, 0xd2, 0x66, 0xe1, 0xf0, 0x77, 0x77, 0xf0, + 0xe1, 0x66, 0xd2, 0x55, 0x44, 0xc3, 0xd2, 0x55, 0x44, 0xc3, 0x77, 0xf0, 0xe1, 0x66, 0x66, 0xe1, 0xf0, 0x77, 0xc3, 0x44, 0x55, 0xd2, 0x11, 0x96, 0x87, 0x00, + 0xb4, 0x33, 0x22, 0xa5, 0xa5, 0x22, 0x33, 0xb4, 0x00, 0x87, 0x96, 0x11, 0xe1, 0x66, 0x77, 0xf0, 0x44, 0xc3, 0xd2, 0x55, 0x55, 0xd2, 0xc3, 0x44, 0xf0, 0x77, + 0x66, 0xe1, 0x22, 0xa5, 0xb4, 0x33, 0x87, 0x00, 0x11, 0x96, 0x96, 0x11, 0x00, 0x87, 0x33, 0xb4, 0xa5, 0x22, 0x33, 0xb4, 0xa5, 0x22, 0x96, 0x11, 0x00, 0x87, + 0x87, 0x00, 0x11, 0x96, 0x22, 0xa5, 0xb4, 0x33, 0xf0, 0x77, 0x66, 0xe1, 0x55, 0xd2, 0xc3, 0x44, 0x44, 0xc3, 0xd2, 0x55, 0xe1, 0x66, 0x77, 0xf0, 0xf0, 0x77, + 0x66, 0xe1, 0x55, 0xd2, 0xc3, 0x44, 0x44, 0xc3, 0xd2, 0x55, 0xe1, 0x66, 0x77, 0xf0, 0x33, 0xb4, 0xa5, 0x22, 0x96, 0x11, 0x00, 0x87, 0x87, 0x00, 0x11, 0x96, + 0x22, 0xa5, 0xb4, 0x33, 0x22, 0xa5, 0xb4, 0x33, 0x87, 0x00, 0x11, 0x96, 0x96, 0x11, 0x00, 0x87, 0x33, 0xb4, 0xa5, 0x22, 0xe1, 0x66, 0x77, 0xf0, 0x44, 0xc3, + 0xd2, 0x55, 0x55, 0xd2, 0xc3, 0x44, 0xf0, 0x77, 0x66, 0xe1, 0x11, 0x96, 0x87, 0x00, 0xb4, 0x33, 0x22, 0xa5, 0xa5, 0x22, 0x33, 0xb4, 0x00, 0x87, 0x96, 0x11, + 0xd2, 0x55, 0x44, 0xc3, 0x77, 0xf0, 0xe1, 0x66, 0x66, 0xe1, 0xf0, 0x77, 0xc3, 0x44, 0x55, 0xd2, 0xc3, 0x44, 0x55, 0xd2, 0x66, 0xe1, 0xf0, 0x77, 0x77, 0xf0, + 0xe1, 0x66, 0xd2, 0x55, 0x44, 0xc3, 0x00, 0x87, 0x96, 0x11, 0xa5, 0x22, 0x33, 0xb4, 0xb4, 0x33, 0x22, 0xa5, 0x11, 0x96, 0x87, 0x00}; + +void calcECC(uint8_t *ecc, const uint8_t *data) { + int i, c; + + ecc[0] = ecc[1] = ecc[2] = 0; + + for (i = 0; i < 0x80; i++) { + c = Table[data[i]]; + + ecc[0] ^= c; + if (c & 0x80) { + ecc[1] ^= ~i; + ecc[2] ^= i; + } + } + ecc[0] = ~ecc[0]; + ecc[0] &= 0x77; + + ecc[1] = ~ecc[1]; + ecc[1] &= 0x7f; + + ecc[2] = ~ecc[2]; + ecc[2] &= 0x7f; + + return; } // keysource and key are self generated values -uint8_t keysource[] = { 0xf5, 0x80, 0x95, 0x3c, 0x4c, 0x84, 0xa9, 0xc0 }; -uint8_t dex_key[16] = { 0x17, 0x39, 0xd3, 0xbc, 0xd0, 0x2c, 0x18, 0x07, 0x4b, 0x17, 0xf0, 0xea, 0xc4, 0x66, 0x30, 0xf9 }; -uint8_t cex_key[16] = { 0x06, 0x46, 0x7a, 0x6c, 0x5b, 0x9b, 0x82, 0x77, 0x0d, 0xdf, 0xe9, 0x7e, 0x24, 0x5b, 0x9f, 0xca }; +uint8_t keysource[] = {0xf5, 0x80, 0x95, 0x3c, 0x4c, 0x84, 0xa9, 0xc0}; +uint8_t dex_key[16] = {0x17, 0x39, 0xd3, 0xbc, 0xd0, 0x2c, 0x18, 0x07, 0x4b, 0x17, 0xf0, 0xea, 0xc4, 0x66, 0x30, 0xf9}; +uint8_t cex_key[16] = {0x06, 0x46, 0x7a, 0x6c, 0x5b, 0x9b, 0x82, 0x77, 0x0d, 0xdf, 0xe9, 0x7e, 0x24, 0x5b, 0x9f, 0xca}; uint8_t *key = cex_key; static uint8_t iv[8]; @@ -275,72 +241,68 @@ static uint8_t CardResponse1[8]; static uint8_t CardResponse2[8]; static uint8_t CardResponse3[8]; -static void __time_critical_func(desEncrypt)(void *key, void *data) -{ - DesContext dc; - desInit(&dc, (uint8_t *) key, 8); - desEncryptBlock(&dc, (uint8_t *) data, (uint8_t *) data); +static void __time_critical_func(desEncrypt)(void *key, void *data) { + DesContext dc; + desInit(&dc, (uint8_t *)key, 8); + desEncryptBlock(&dc, (uint8_t *)data, (uint8_t *)data); } -static void __time_critical_func(desDecrypt)(void *key, void *data) -{ - DesContext dc; - desInit(&dc, (uint8_t *) key, 8); - desDecryptBlock(&dc, (uint8_t *) data, (uint8_t *) data); +static void __time_critical_func(desDecrypt)(void *key, void *data) { + DesContext dc; + desInit(&dc, (uint8_t *)key, 8); + desDecryptBlock(&dc, (uint8_t *)data, (uint8_t *)data); } -static void __time_critical_func(doubleDesEncrypt)(void *key, void *data) -{ - desEncrypt(key, data); - desDecrypt(&((uint8_t *) key)[8], data); - desEncrypt(key, data); +static void __time_critical_func(doubleDesEncrypt)(void *key, void *data) { + desEncrypt(key, data); + desDecrypt(&((uint8_t *)key)[8], data); + desEncrypt(key, data); } -static void __time_critical_func(doubleDesDecrypt)(void *key, void *data) -{ - desDecrypt(key, data); - desEncrypt(&((uint8_t *) key)[8], data); - desDecrypt(key, data); +static void __time_critical_func(doubleDesDecrypt)(void *key, void *data) { + desDecrypt(key, data); + desEncrypt(&((uint8_t *)key)[8], data); + desDecrypt(key, data); } -static void __time_critical_func(xor_bit)(const void* a, const void* b, void* Result, size_t Length) { - size_t i; - for (i = 0; i < Length; i++) { - ((uint8_t*)Result)[i] = ((uint8_t*)a)[i] ^ ((uint8_t*)b)[i]; - } +static void __time_critical_func(xor_bit)(const void *a, const void *b, void *Result, size_t Length) { + size_t i; + for (i = 0; i < Length; i++) { + ((uint8_t *)Result)[i] = ((uint8_t *)a)[i] ^ ((uint8_t *)b)[i]; + } } static void __time_critical_func(generateIvSeedNonce)() { - for (int i = 0; i < 8; i++) { - iv[i] = 0x42; - seed[i] = keysource[i] ^ iv[i]; - nonce[i] = 0x42; - } + for (int i = 0; i < 8; i++) { + iv[i] = 0x42; + seed[i] = keysource[i] ^ iv[i]; + nonce[i] = 0x42; + } } static void __time_critical_func(generateResponse)() { - doubleDesDecrypt(key, MechaChallenge1); - uint8_t random[8] = { 0 }; - xor_bit(MechaChallenge1, ps2_civ, random, 8); + doubleDesDecrypt(key, MechaChallenge1); + uint8_t random[8] = {0}; + xor_bit(MechaChallenge1, ps2_civ, random, 8); - // MechaChallenge2 and MechaChallenge3 let's the card verify the console + // MechaChallenge2 and MechaChallenge3 let's the card verify the console - xor_bit(nonce, ps2_civ, CardResponse1, 8); + xor_bit(nonce, ps2_civ, CardResponse1, 8); - doubleDesEncrypt(key, CardResponse1); + doubleDesEncrypt(key, CardResponse1); - xor_bit(random, CardResponse1, CardResponse2, 8); - doubleDesEncrypt(key, CardResponse2); + xor_bit(random, CardResponse1, CardResponse2, 8); + doubleDesEncrypt(key, CardResponse2); - uint8_t CardKey[] = { 'M', 'e', 'c', 'h', 'a', 'P', 'w', 'n' }; - xor_bit(CardKey, CardResponse2, CardResponse3, 8); - doubleDesEncrypt(key, CardResponse3); + uint8_t CardKey[] = {'M', 'e', 'c', 'h', 'a', 'P', 'w', 'n'}; + xor_bit(CardKey, CardResponse2, CardResponse3, 8); + doubleDesEncrypt(key, CardResponse3); } static void __time_critical_func(mc_main_loop)(void) { - bool next = false, exit = false; while (1) { uint8_t cmd, ch; + NEXTCMD: while (!reset && !reset && !reset && !reset && !reset) { if (mc_exit_request) @@ -348,125 +310,103 @@ static void __time_critical_func(mc_main_loop)(void) { } reset = 0; - //recvfirst(); - switch (receiveFirst(&cmd)) { - case RECEIVE_EXIT: - exit = true; - break; - case RECEIVE_RESET: - next = true; - break; - default: - break; - } - if (next) - continue; - if (exit) + // recvfirst(); + uint8_t received = receiveFirst(&cmd); + + if (received == RECEIVE_EXIT) break; + if (received == RECEIVE_RESET) + continue; if (cmd == 0x81) { #include "ps2_memory_card.in.c" } else if (cmd == 0x8A) { - if (ch == 0xA0) { - /* SD2PSXMAN Command */ - int subcmd = cmd; - uint8_t game_id_length; - uint8_t input_buff[0xFF] = { 0 }; - mc_respond(0xFF); receiveOrNextCmd - (&cmd); - debug_printf("SD2PSXMAN %02X -> %02X\n", ch, subcmd); - mc_respond(0x00); receiveOrNextCmd - (&cmd); // Accept command - - switch(subcmd) { - case 0x20: // Ping Command - mc_respond(0x01); receiveOrNextCmd - (&cmd); // Protocol version - mc_respond(0x01); receiveOrNextCmd - (&cmd); // Product ID - 1 == SD2PSX - mc_respond(0x27); receiveOrNextCmd - (&cmd); // SD2PSX is available - mc_respond(0xFF); receiveOrNextCmd - (&cmd); // Terminate - break; - case 0x21: // Game ID command - memset(received_game_id, 0, sizeof(received_game_id)); - mc_respond(0x00); receiveOrNextCmd - (&cmd); // Accept command - mc_respond(0x00); receiveOrNextCmd - (&cmd); // Padding - game_id_length = cmd; - for (uint8_t i; (i < game_id_length) && (i < 0x10); i++) { - receiveOrNextCmd - (&cmd); // Receive Game ID char by char - input_buff[i] = cmd; - mc_respond(cmd); - } - game_names_extract_title_id(input_buff, received_game_id, game_id_length, sizeof(received_game_id)); - break; - case 0x22: // Change Channel command - mc_respond(0x00); receiveOrNextCmd - (&cmd); // Accept command - subcmd = cmd; - mc_respond(0x00); receiveOrNextCmd - (&cmd); - - switch (subcmd) { - case 0x00: - // ps2_cardman_set_channel(cmd); todo: Implement! - break; - case 0x01: - ps2_cardman_next_channel(); - break; - case 0x02: - ps2_cardman_prev_channel(); - break; - default: - break; - } - mc_respond(0x00); receiveOrNextCmd - (&cmd); - mc_respond(0x00); receiveOrNextCmd - (&cmd); + mc_respond(0xFF); receiveOrNextCmd(&cmd); + + if (cmd == 0xA0) { + /* SD2PSXMAN Command */ + int subcmd = cmd; + uint8_t game_id_length; + uint8_t input_buff[0xFF] = {0}; mc_respond(0xFF); - break; - case 0x23: // Change Slot command - mc_respond(0x00); receiveOrNextCmd - (&cmd); // Accept command - subcmd = cmd; - mc_respond(0x00); receiveOrNextCmd - (&cmd); - + receiveOrNextCmd(&cmd); + debug_printf("SD2PSXMAN %02X -> %02X\n", cmd, subcmd); + mc_respond(0x00); + receiveOrNextCmd(&cmd); // Accept command + switch (subcmd) { - case 0x00: - // ps2_cardman_set_idx(cmd); todo: Implement! + case 0x20: // Ping Command + mc_respond(0x01); + receiveOrNextCmd(&cmd); // Protocol version + mc_respond(0x01); + receiveOrNextCmd(&cmd); // Product ID - 1 == SD2PSX + mc_respond(0x27); + receiveOrNextCmd(&cmd); // SD2PSX is available + mc_respond(0xFF); + receiveOrNextCmd(&cmd); // Terminate break; - case 0x01: - ps2_cardman_next_idx(); + case 0x21: // Game ID command + memset(received_game_id, 0, sizeof(received_game_id)); + mc_respond(0x00); + receiveOrNextCmd(&cmd); // Accept command + mc_respond(0x00); + receiveOrNextCmd(&cmd); // Padding + game_id_length = cmd; + for (uint8_t i = 0; (i < game_id_length) && (i < 0x10); i++) { + receiveOrNextCmd(&input_buff[i]); // Receive Game ID char by char + mc_respond(input_buff[i]); + } + game_names_extract_title_id(input_buff, received_game_id, game_id_length, sizeof(received_game_id)); break; - case 0x02: - ps2_cardman_prev_idx(); + case 0x22: // Change Channel command + mc_respond(0x00); + receiveOrNextCmd(&cmd); // Accept command + subcmd = cmd; + mc_respond(0x00); + receiveOrNextCmd(&cmd); + + switch (subcmd) { + case 0x00: + // ps2_cardman_set_channel(cmd); todo: Implement! + break; + case 0x01: ps2_cardman_next_channel(); break; + case 0x02: ps2_cardman_prev_channel(); break; + default: break; + } + mc_respond(0x00); + receiveOrNextCmd(&cmd); + mc_respond(0x00); + receiveOrNextCmd(&cmd); + mc_respond(0xFF); break; - default: + case 0x23: // Change Slot command + mc_respond(0x00); + receiveOrNextCmd(&cmd); // Accept command + subcmd = cmd; + mc_respond(0x00); + receiveOrNextCmd(&cmd); + + switch (subcmd) { + case 0x00: + // ps2_cardman_set_idx(cmd); todo: Implement! + break; + case 0x01: ps2_cardman_next_idx(); break; + case 0x02: ps2_cardman_prev_idx(); break; + default: break; + } + mc_respond(0x00); + receiveOrNextCmd(&cmd); + mc_respond(0x00); + receiveOrNextCmd(&cmd); + mc_respond(0xFF); break; + default: break; } - mc_respond(0x00); receiveOrNextCmd - (&cmd); - mc_respond(0x00); receiveOrNextCmd - (&cmd); - mc_respond(0xFF); - break; - default: - break; - - } - - } + } } else { // not for us continue; } - } EXIT_REQUEST: mc_exit_response = 1; @@ -474,8 +414,7 @@ static void __time_critical_func(mc_main_loop)(void) { static void __no_inline_not_in_flash_func(mc_main)(void) { while (1) { - while (!mc_enter_request) - {} + while (!mc_enter_request) {} mc_enter_response = 1; mc_main_loop(); @@ -493,10 +432,10 @@ static void __time_critical_func(RAM_gpio_default_irq_handler)(void) { uint core = get_core_num(); gpio_irq_callback_t callback = callbacks[core]; io_irq_ctrl_hw_t *irq_ctrl_base = core ? &iobank0_hw->proc1_irq_ctrl : &iobank0_hw->proc0_irq_ctrl; - for (uint gpio = 0; gpio < NUM_BANK0_GPIOS; gpio+=8) { + for (uint gpio = 0; gpio < NUM_BANK0_GPIOS; gpio += 8) { uint32_t events8 = irq_ctrl_base->ints[gpio >> 3u]; // note we assume events8 is 0 for non-existent GPIO - for(uint i=gpio;events8 && i %02X\n", ch, subcmd); } } else if ((ch == 0xF1 || ch == 0xF2) && ps2_magicgate) { - int subcmd = cmd; /* session key encrypt */ mc_respond(0xFF); - receiveOrNextCmd(&subcmd); + receiveOrNextCmd(&cmd); + int subcmd = cmd; if (subcmd == 0x50 || subcmd == 0x40) { mc_respond(0x2B); receiveOrNextCmd(&cmd); mc_respond(term); @@ -497,7 +497,8 @@ if (ch == 0x11) { /* host mc_responds key to us */ for (size_t i = 0; i < sizeof(hostkey); ++i) { mc_respond(0xFF); - receiveOrNextCmd(&hostkey[i]); + receiveOrNextCmd(&cmd); + hostkey[i] = cmd; } mc_respond(0x2B); receiveOrNextCmd(&cmd); mc_respond(term); From d3fa29fb0ba9b27167fbc845c5aff528e73c60af Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 30 Oct 2023 17:11:51 +0100 Subject: [PATCH 70/91] Fix receiveFirst --- src/ps2/card_emu/ps2_memory_card.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ps2/card_emu/ps2_memory_card.c b/src/ps2/card_emu/ps2_memory_card.c index fb689e6..1318bf2 100644 --- a/src/ps2/card_emu/ps2_memory_card.c +++ b/src/ps2/card_emu/ps2_memory_card.c @@ -302,12 +302,14 @@ static void __time_critical_func(generateResponse)() { static void __time_critical_func(mc_main_loop)(void) { while (1) { uint8_t cmd, ch; - NEXTCMD: while (!reset && !reset && !reset && !reset && !reset) { - if (mc_exit_request) - goto EXIT_REQUEST; + if (mc_exit_request) { + mc_exit_response = 1; + return; + } } + reset = 0; // recvfirst(); @@ -408,8 +410,6 @@ static void __time_critical_func(mc_main_loop)(void) { continue; } } -EXIT_REQUEST: - mc_exit_response = 1; } static void __no_inline_not_in_flash_func(mc_main)(void) { From 8c04ebe6c6b95ff3bf98a8fed33911efaf5e30be Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 8 Nov 2023 09:29:44 +0100 Subject: [PATCH 71/91] Re-factor CMake --- CMakeLists.txt | 159 ++++++++++++++++------------------------- ext/CMakeLists.txt | 63 ++++++++++++++++ src/ps1/CMakeLists.txt | 18 +++++ src/ps2/CMakeLists.txt | 28 ++++++++ 4 files changed, 169 insertions(+), 99 deletions(-) create mode 100644 ext/CMakeLists.txt create mode 100644 src/ps1/CMakeLists.txt create mode 100644 src/ps2/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f07a72..de3873f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,146 +1,107 @@ cmake_minimum_required(VERSION 3.12) - set(PICO_SDK_PATH ${CMAKE_CURRENT_SOURCE_DIR}/ext/pico-sdk) include(pico_sdk_import.cmake) -project(pico_examples C CXX ASM) +project(SD2PSX LANGUAGES C CXX ASM) + set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 17) # set(PICO_COPY_TO_RAM 1) pico_sdk_init() -set(LV_CONF_PATH ${CMAKE_CURRENT_SOURCE_DIR}/src/lv_conf.h CACHE STRING "" FORCE) -add_subdirectory(ext/lvgl EXCLUDE_FROM_ALL) +# Add all subdirectories for sub-targets add_subdirectory(database) add_subdirectory(ps2boot) add_subdirectory(src/version) +add_subdirectory(src/ps2) +add_subdirectory(src/ps1) +add_subdirectory(ext/) + +# SD2PSX Main Lib add_executable(sd2psx src/main.c - src/debug.c - src/gui.c - src/input.c - src/ui_menu.c - src/ui_theme_mono.c - src/des.c - src/keystore.c - src/settings.c - src/bigmem.c - src/oled.c - - src/ps1/ps1_cardman.c - src/ps1/ps1_dirty.c - src/ps1/ps1_memory_card.c - src/ps1/ps1_empty_card.c - src/ps1/ps1_odeman.c - - src/ps2/card_emu/ps2_memory_card.c - src/ps2/ps2_dirty.c - src/ps2/ps2_cardman.c - src/ps2/ps2_pio_qspi.c - src/ps2/ps2_psram.c - src/ps2/ps2_exploit.c src/game_names/game_names.c src/wear_leveling/wear_leveling.c src/wear_leveling/wear_leveling_rp2040_flash.c - src/arduino_wrapper/sd.cpp - src/arduino_wrapper/SPI.cpp - - ext/ESP8266SdFat/src/common/FmtNumber.cpp - ext/ESP8266SdFat/src/common/FsCache.cpp - ext/ESP8266SdFat/src/common/FsDateTime.cpp - ext/ESP8266SdFat/src/common/FsName.cpp - ext/ESP8266SdFat/src/common/FsStructs.cpp - ext/ESP8266SdFat/src/common/FsUtf.cpp - ext/ESP8266SdFat/src/common/upcase.cpp - ext/ESP8266SdFat/src/ExFatLib/ExFatDbg.cpp - ext/ESP8266SdFat/src/ExFatLib/ExFatFile.cpp - ext/ESP8266SdFat/src/ExFatLib/ExFatFilePrint.cpp - ext/ESP8266SdFat/src/ExFatLib/ExFatFileWrite.cpp - ext/ESP8266SdFat/src/ExFatLib/ExFatFormatter.cpp - ext/ESP8266SdFat/src/ExFatLib/ExFatName.cpp - ext/ESP8266SdFat/src/ExFatLib/ExFatPartition.cpp - ext/ESP8266SdFat/src/ExFatLib/ExFatVolume.cpp - ext/ESP8266SdFat/src/FatLib/FatDbg.cpp - ext/ESP8266SdFat/src/FatLib/FatFile.cpp - ext/ESP8266SdFat/src/FatLib/FatFileLFN.cpp - ext/ESP8266SdFat/src/FatLib/FatFilePrint.cpp - ext/ESP8266SdFat/src/FatLib/FatFileSFN.cpp - ext/ESP8266SdFat/src/FatLib/FatFormatter.cpp - ext/ESP8266SdFat/src/FatLib/FatName.cpp - ext/ESP8266SdFat/src/FatLib/FatPartition.cpp - ext/ESP8266SdFat/src/FatLib/FatVolume.cpp - ext/ESP8266SdFat/src/FreeStack.cpp - ext/ESP8266SdFat/src/FsLib/FsFile.cpp - ext/ESP8266SdFat/src/FsLib/FsNew.cpp - ext/ESP8266SdFat/src/FsLib/FsVolume.cpp - ext/ESP8266SdFat/src/iostream/istream.cpp - ext/ESP8266SdFat/src/iostream/ostream.cpp - ext/ESP8266SdFat/src/iostream/StdioStream.cpp - ext/ESP8266SdFat/src/iostream/StreamBaseClass.cpp - ext/ESP8266SdFat/src/MinimumSerial.cpp - ext/ESP8266SdFat/src/SdCard/SdCardInfo.cpp - ext/ESP8266SdFat/src/SdCard/SdSpiCard.cpp - ext/fnv/hash_64a.c ) -add_library(ssd1306 STATIC ext/pico-ssd1306/ssd1306.c) -target_link_libraries(ssd1306 pico_stdlib hardware_i2c) - target_compile_definitions( sd2psx PUBLIC PICO_XOSC_STARTUP_DELAY_MULTIPLIER=64 PICO_FLASH_SIZE_BYTES=16777216 - USE_SPI_ARRAY_TRANSFER=1 -) - -target_compile_options( - sd2psx PRIVATE - -Wall -Wextra - -fno-jump-tables ) target_include_directories(sd2psx PUBLIC - src/arduino_wrapper - - ext/pico-ssd1306 - ext/ESP8266SdFat/src - ext/ESP8266SdFat/extras/attic ext/fnv ) -pico_generate_pio_header(sd2psx ${CMAKE_CURRENT_LIST_DIR}/src/ps1/ps1_mc_spi.pio) -pico_generate_pio_header(sd2psx ${CMAKE_CURRENT_LIST_DIR}/src/ps2/card_emu/ps2_mc_spi.pio) -pico_generate_pio_header(sd2psx ${CMAKE_CURRENT_LIST_DIR}/src/ps2/ps2_qspi.pio) - target_link_libraries(sd2psx - ssd1306 - pico_stdlib - pico_multicore - hardware_pio - hardware_spi - hardware_i2c - hardware_flash - hardware_dma - lvgl::lvgl - gamedb - ps2boot - sd2psx_version + PRIVATE + pico_stdlib + pico_multicore + hardware_pio + hardware_i2c + hardware_flash + gamedb + ps2boot + sd2psx_version + sd2psx_common + ps1_card + ps2_card + sd_fat ) add_dependencies(sd2psx gamedb ps2boot) set_target_properties(sd2psx PROPERTIES PICO_TARGET_LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/memmap_custom.ld) +# Common Lib + +add_library(sd2psx_common STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/src/debug.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/gui.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/input.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/ui_menu.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/ui_theme_mono.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/des.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/keystore.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/settings.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/bigmem.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/oled.c) + +target_include_directories(sd2psx_common + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + PRIVATE ) + +target_link_libraries(sd2psx_common + PUBLIC + lvgl::lvgl + PRIVATE + pico_platform_headers + ssd1306 + hardware_flash) + +target_compile_options(sd2psx_common + PUBLIC + -Wall -Wextra + -fno-jump-tables) + +target_compile_definitions(sd2psx_common PUBLIC + USE_SPI_ARRAY_TRANSFER=1) + +set_target_properties(sd2psx_common PROPERTIES PICO_TARGET_LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/memmap_custom.ld) + pico_add_extra_outputs(sd2psx) set(DEBUG_USB_UART OFF CACHE BOOL "Activate UART over USB for debugging") if(DEBUG_USB_UART) - add_definitions(-DDEBUG_USB_UART) - pico_enable_stdio_usb(sd2psx ENABLED) + target_compile_definitions(sd2psx_common PUBLIC -DDEBUG_USB_UART) + pico_enable_stdio_usb(sd2psx ON) endif() diff --git a/ext/CMakeLists.txt b/ext/CMakeLists.txt new file mode 100644 index 0000000..e5a586f --- /dev/null +++ b/ext/CMakeLists.txt @@ -0,0 +1,63 @@ + +add_library(sd_fat STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/common/FmtNumber.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/common/FsCache.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/common/FsDateTime.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/common/FsName.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/common/FsStructs.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/common/FsUtf.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/common/upcase.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/ExFatLib/ExFatDbg.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/ExFatLib/ExFatFile.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/ExFatLib/ExFatFilePrint.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/ExFatLib/ExFatFileWrite.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/ExFatLib/ExFatFormatter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/ExFatLib/ExFatName.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/ExFatLib/ExFatPartition.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/ExFatLib/ExFatVolume.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/FatLib/FatDbg.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/FatLib/FatFile.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/FatLib/FatFileLFN.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/FatLib/FatFilePrint.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/FatLib/FatFileSFN.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/FatLib/FatFormatter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/FatLib/FatName.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/FatLib/FatPartition.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/FatLib/FatVolume.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/FreeStack.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/FsLib/FsFile.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/FsLib/FsNew.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/FsLib/FsVolume.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/iostream/istream.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/iostream/ostream.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/iostream/StdioStream.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/iostream/StreamBaseClass.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/MinimumSerial.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/SdCard/SdCardInfo.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src/SdCard/SdSpiCard.cpp + ${PROJECT_SOURCE_DIR}/src/arduino_wrapper/sd.cpp + ${PROJECT_SOURCE_DIR}/src/arduino_wrapper/SPI.cpp +) + +target_include_directories(sd_fat + PRIVATE + ${PROJECT_SOURCE_DIR}/src/ + ${PROJECT_SOURCE_DIR}/src/arduino_wrapper + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/src + ${CMAKE_CURRENT_SOURCE_DIR}/ESP8266SdFat/extras/attic) + +target_link_libraries(sd_fat + PUBLIC + pico_stdlib + hardware_spi) + +target_link_libraries(sd_fat PRIVATE sd2psx_common) + + +add_library(ssd1306 STATIC ${CMAKE_CURRENT_SOURCE_DIR}/pico-ssd1306/ssd1306.c) +target_link_libraries(ssd1306 pico_stdlib hardware_i2c) +target_include_directories(ssd1306 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/pico-ssd1306) + +set(LV_CONF_PATH ${PROJECT_SOURCE_DIR}/src/lv_conf.h CACHE STRING "" FORCE) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/lvgl EXCLUDE_FROM_ALL) \ No newline at end of file diff --git a/src/ps1/CMakeLists.txt b/src/ps1/CMakeLists.txt new file mode 100644 index 0000000..4dadd61 --- /dev/null +++ b/src/ps1/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(ps1_card STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/ps1_cardman.c + ${CMAKE_CURRENT_SOURCE_DIR}/ps1_dirty.c + ${CMAKE_CURRENT_SOURCE_DIR}/ps1_memory_card.c + ${CMAKE_CURRENT_SOURCE_DIR}/ps1_empty_card.c + ${CMAKE_CURRENT_SOURCE_DIR}/ps1_odeman.c +) + +target_include_directories(ps1_card PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(ps1_card + PRIVATE + sd2psx_common + pico_platform_headers + pico_stdlib + hardware_pio) + +pico_generate_pio_header(ps1_card ${CMAKE_CURRENT_LIST_DIR}/ps1_mc_spi.pio) diff --git a/src/ps2/CMakeLists.txt b/src/ps2/CMakeLists.txt new file mode 100644 index 0000000..b276698 --- /dev/null +++ b/src/ps2/CMakeLists.txt @@ -0,0 +1,28 @@ + +add_library(ps2_card STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/card_emu/ps2_memory_card.c + ${CMAKE_CURRENT_SOURCE_DIR}/card_emu/ps2_mc_commands.c + ${CMAKE_CURRENT_SOURCE_DIR}/card_emu/ps2_mc_auth.c + ${CMAKE_CURRENT_SOURCE_DIR}/ps2_dirty.c + ${CMAKE_CURRENT_SOURCE_DIR}/ps2_cardman.c + ${CMAKE_CURRENT_SOURCE_DIR}/ps2_pio_qspi.c + ${CMAKE_CURRENT_SOURCE_DIR}/ps2_psram.c + ${CMAKE_CURRENT_SOURCE_DIR}/ps2_exploit.c + ) + +target_include_directories(ps2_card + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(ps2_card PRIVATE + sd2psx_common + pico_stdlib + pico_multicore + hardware_sync + hardware_pio + hardware_flash + hardware_dma) + + +pico_generate_pio_header(ps2_card ${CMAKE_CURRENT_LIST_DIR}/card_emu/ps2_mc_spi.pio) +pico_generate_pio_header(ps2_card ${CMAKE_CURRENT_LIST_DIR}/ps2_qspi.pio) From ff537a40c2de6c7ad0c55f57cfe66723c5040ff4 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 8 Nov 2023 10:00:34 +0100 Subject: [PATCH 72/91] Major PS2 refactoring --- src/ps2/card_emu/ps2_mc_auth.c | 505 +++++++++++++++++++++++++ src/ps2/card_emu/ps2_mc_auth.h | 9 + src/ps2/card_emu/ps2_mc_commands.c | 367 ++++++++++++++++++ src/ps2/card_emu/ps2_mc_commands.h | 38 ++ src/ps2/card_emu/ps2_mc_internal.h | 38 ++ src/ps2/card_emu/ps2_memory_card.c | 187 +++------ src/ps2/card_emu/ps2_memory_card.h | 2 +- src/ps2/card_emu/ps2_memory_card.in.c | 524 -------------------------- src/ps2/ps2_cardman.c | 2 +- src/ps2/ps2_dirty.c | 2 + src/ps2/ps2_exploit.h | 2 +- 11 files changed, 1016 insertions(+), 660 deletions(-) create mode 100644 src/ps2/card_emu/ps2_mc_auth.c create mode 100644 src/ps2/card_emu/ps2_mc_auth.h create mode 100644 src/ps2/card_emu/ps2_mc_commands.c create mode 100644 src/ps2/card_emu/ps2_mc_commands.h create mode 100644 src/ps2/card_emu/ps2_mc_internal.h delete mode 100644 src/ps2/card_emu/ps2_memory_card.in.c diff --git a/src/ps2/card_emu/ps2_mc_auth.c b/src/ps2/card_emu/ps2_mc_auth.c new file mode 100644 index 0000000..806d03f --- /dev/null +++ b/src/ps2/card_emu/ps2_mc_auth.c @@ -0,0 +1,505 @@ + +#include "ps2_mc_auth.h" + +#include + +#include "debug.h" +#include "des.h" +#include "keystore.h" +#include "ps2_mc_internal.h" + +// keysource and key are self generated values +uint8_t keysource[] = {0xf5, 0x80, 0x95, 0x3c, 0x4c, 0x84, 0xa9, 0xc0}; +uint8_t dex_key[16] = {0x17, 0x39, 0xd3, 0xbc, 0xd0, 0x2c, 0x18, 0x07, 0x4b, 0x17, 0xf0, 0xea, 0xc4, 0x66, 0x30, 0xf9}; +uint8_t cex_key[16] = {0x06, 0x46, 0x7a, 0x6c, 0x5b, 0x9b, 0x82, 0x77, 0x0d, 0xdf, 0xe9, 0x7e, 0x24, 0x5b, 0x9f, 0xca}; +uint8_t *key = cex_key; + +uint8_t iv[8]; +uint8_t seed[8]; +uint8_t nonce[8]; +uint8_t MechaChallenge3[8]; +uint8_t MechaChallenge2[8]; +uint8_t MechaChallenge1[8]; +uint8_t CardResponse1[8]; +uint8_t CardResponse2[8]; +uint8_t CardResponse3[8]; +uint8_t hostkey[9]; + +void __time_critical_func(desEncrypt)(void *key, void *data) { + DesContext dc; + desInit(&dc, (uint8_t *)key, 8); + desEncryptBlock(&dc, (uint8_t *)data, (uint8_t *)data); +} + +void __time_critical_func(desDecrypt)(void *key, void *data) { + DesContext dc; + desInit(&dc, (uint8_t *)key, 8); + desDecryptBlock(&dc, (uint8_t *)data, (uint8_t *)data); +} + +void __time_critical_func(doubleDesEncrypt)(void *key, void *data) { + desEncrypt(key, data); + desDecrypt(&((uint8_t *)key)[8], data); + desEncrypt(key, data); +} + +void __time_critical_func(doubleDesDecrypt)(void *key, void *data) { + desDecrypt(key, data); + desEncrypt(&((uint8_t *)key)[8], data); + desDecrypt(key, data); +} + +void __time_critical_func(xor_bit)(const void *a, const void *b, void *Result, size_t Length) { + size_t i; + for (i = 0; i < Length; i++) { + ((uint8_t *)Result)[i] = ((uint8_t *)a)[i] ^ ((uint8_t *)b)[i]; + } +} + +void __time_critical_func(generateIvSeedNonce)() { + for (int i = 0; i < 8; i++) { + iv[i] = 0x42; + seed[i] = keysource[i] ^ iv[i]; + nonce[i] = 0x42; + } +} + +void __time_critical_func(generateResponse)() { + doubleDesDecrypt(key, MechaChallenge1); + uint8_t random[8] = {0}; + xor_bit(MechaChallenge1, ps2_civ, random, 8); + + // MechaChallenge2 and MechaChallenge3 let's the card verify the console + + xor_bit(nonce, ps2_civ, CardResponse1, 8); + + doubleDesEncrypt(key, CardResponse1); + + xor_bit(random, CardResponse1, CardResponse2, 8); + doubleDesEncrypt(key, CardResponse2); + + uint8_t CardKey[] = {'M', 'e', 'c', 'h', 'a', 'P', 'w', 'n'}; + xor_bit(CardKey, CardResponse2, CardResponse3, 8); + doubleDesEncrypt(key, CardResponse3); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_probe(void) { + uint8_t _; + /* probe support ? */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_getIv(void) { + uint8_t _; + debug_printf("iv : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(iv)); + + /* get IV */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(iv[7]); + receiveOrNextCmd(&_); + mc_respond(iv[6]); + receiveOrNextCmd(&_); + mc_respond(iv[5]); + receiveOrNextCmd(&_); + mc_respond(iv[4]); + receiveOrNextCmd(&_); + mc_respond(iv[3]); + receiveOrNextCmd(&_); + mc_respond(iv[2]); + receiveOrNextCmd(&_); + mc_respond(iv[1]); + receiveOrNextCmd(&_); + mc_respond(iv[0]); + receiveOrNextCmd(&_); + mc_respond(XOR8(iv)); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_getSeed(void) { + uint8_t _; + debug_printf("seed : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(seed)); + + /* get seed */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(seed[7]); + receiveOrNextCmd(&_); + mc_respond(seed[6]); + receiveOrNextCmd(&_); + mc_respond(seed[5]); + receiveOrNextCmd(&_); + mc_respond(seed[4]); + receiveOrNextCmd(&_); + mc_respond(seed[3]); + receiveOrNextCmd(&_); + mc_respond(seed[2]); + receiveOrNextCmd(&_); + mc_respond(seed[1]); + receiveOrNextCmd(&_); + mc_respond(seed[0]); + receiveOrNextCmd(&_); + mc_respond(XOR8(seed)); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_dummy3(void) { + uint8_t _; + /* dummy 3 */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_getNonce(void) { + uint8_t _; + debug_printf("nonce : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(nonce)); + + /* get nonce */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(nonce[7]); + receiveOrNextCmd(&_); + mc_respond(nonce[6]); + receiveOrNextCmd(&_); + mc_respond(nonce[5]); + receiveOrNextCmd(&_); + mc_respond(nonce[4]); + receiveOrNextCmd(&_); + mc_respond(nonce[3]); + receiveOrNextCmd(&_); + mc_respond(nonce[2]); + receiveOrNextCmd(&_); + mc_respond(nonce[1]); + receiveOrNextCmd(&_); + mc_respond(nonce[0]); + receiveOrNextCmd(&_); + mc_respond(XOR8(nonce)); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_dummy5(void) { + uint8_t _; + /* dummy 5 */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_mechaChallenge3(void) { + uint8_t _; + /* MechaChallenge3 */ + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[7]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[6]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[5]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[4]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[3]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[2]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[1]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge3[0]); + /* TODO: checksum below */ + mc_respond(0xFF); + receiveOrNextCmd(&_); + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); + + debug_printf("MechaChallenge3 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(MechaChallenge3)); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_mechaChallenge2(void) { + uint8_t _ = 0U; + /* MechaChallenge2 */ + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[7]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[6]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[5]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[4]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[3]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[2]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[1]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge2[0]); + /* TODO: checksum below */ + mc_respond(0xFF); + receiveOrNextCmd(&_); + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); + + debug_printf("MechaChallenge2 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(MechaChallenge2)); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_dummy8(void) { + uint8_t _ = 0U; + /* dummy 8 */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_dummy9(void) { + uint8_t _ = 0U; + /* dummy 9 */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_dummyA(void) { + uint8_t _ = 0U; + /* dummy A */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_mechaChallenge1(void) { + uint8_t _ = 0; + /* MechaChallenge1 */ + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[7]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[6]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[5]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[4]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[3]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[2]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[1]); + mc_respond(0xFF); + receiveOrNextCmd(&MechaChallenge1[0]); + /* TODO: checksum below */ + mc_respond(0xFF); + receiveOrNextCmd(&_); + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); + + debug_printf("MechaChallenge1 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(MechaChallenge1)); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_dummyC(void) { + uint8_t _ = 0; + /* dummy C */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_dummyD(void) { + uint8_t _ = 0; + /* dummy D */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_dummyE(void) { + uint8_t _ = 0; + /* dummy E */ + generateResponse(); + debug_printf("CardResponse1 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(CardResponse1)); + debug_printf("CardResponse2 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(CardResponse2)); + debug_printf("CardResponse3 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(CardResponse3)); + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_cardResponse1(void) { + uint8_t _ = 0; + /* CardResponse1 */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(CardResponse1[7]); + receiveOrNextCmd(&_); + mc_respond(CardResponse1[6]); + receiveOrNextCmd(&_); + mc_respond(CardResponse1[5]); + receiveOrNextCmd(&_); + mc_respond(CardResponse1[4]); + receiveOrNextCmd(&_); + mc_respond(CardResponse1[3]); + receiveOrNextCmd(&_); + mc_respond(CardResponse1[2]); + receiveOrNextCmd(&_); + mc_respond(CardResponse1[1]); + receiveOrNextCmd(&_); + mc_respond(CardResponse1[0]); + receiveOrNextCmd(&_); + mc_respond(XOR8(CardResponse1)); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_dummy10(void) { + uint8_t _ = 0; + /* dummy 10 */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_cardResponse2(void) { + uint8_t _ = 0; + /* CardResponse2 */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(CardResponse2[7]); + receiveOrNextCmd(&_); + mc_respond(CardResponse2[6]); + receiveOrNextCmd(&_); + mc_respond(CardResponse2[5]); + receiveOrNextCmd(&_); + mc_respond(CardResponse2[4]); + receiveOrNextCmd(&_); + mc_respond(CardResponse2[3]); + receiveOrNextCmd(&_); + mc_respond(CardResponse2[2]); + receiveOrNextCmd(&_); + mc_respond(CardResponse2[1]); + receiveOrNextCmd(&_); + mc_respond(CardResponse2[0]); + receiveOrNextCmd(&_); + mc_respond(XOR8(CardResponse2)); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_dummy12(void) { + uint8_t _ = 0; + /* dummy 12 */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_cardResponse3(void) { + uint8_t _ = 0; + /* CardResponse3 */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(CardResponse3[7]); + receiveOrNextCmd(&_); + mc_respond(CardResponse3[6]); + receiveOrNextCmd(&_); + mc_respond(CardResponse3[5]); + receiveOrNextCmd(&_); + mc_respond(CardResponse3[4]); + receiveOrNextCmd(&_); + mc_respond(CardResponse3[3]); + receiveOrNextCmd(&_); + mc_respond(CardResponse3[2]); + receiveOrNextCmd(&_); + mc_respond(CardResponse3[1]); + receiveOrNextCmd(&_); + mc_respond(CardResponse3[0]); + receiveOrNextCmd(&_); + mc_respond(XOR8(CardResponse3)); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_auth_dummy14(void) { + uint8_t _ = 0; + /* dummy 14 */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_sessionKeyEncr(void) { + uint8_t _ = 0; + uint8_t subcmd = 0; + /* session key encrypt */ + mc_respond(0xFF); + receiveOrNextCmd(&subcmd); + if (subcmd == 0x50 || subcmd == 0x40) { + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); + } else if (subcmd == 0x51 || subcmd == 0x41) { + /* host mc_responds key to us */ + for (size_t i = 0; i < sizeof(hostkey); ++i) { + mc_respond(0xFF); + receiveOrNextCmd(&hostkey[i]); + } + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); + } else if (subcmd == 0x52 || subcmd == 0x42) { + /* now we encrypt/decrypt the key */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); + } else if (subcmd == 0x53 || subcmd == 0x43) { + mc_respond(0x2B); + receiveOrNextCmd(&_); + /* we mc_respond key to the host */ + for (size_t i = 0; i < sizeof(hostkey); ++i) { + mc_respond(hostkey[i]); + receiveOrNextCmd(&_); + } + mc_respond(term); + } else { + debug_printf("!! unknown subcmd %02X -> %02X\n", 0xF2, subcmd); + } +} + +inline __attribute__((always_inline)) void ps2_mc_auth(void) { + uint8_t subcmd = 0; + mc_respond(0xFF); + + receiveOrNextCmd(&subcmd); + // debug_printf("MC Auth: %02X\n", subcmd); + switch (subcmd) { + case 0x0: ps2_mc_auth_probe(); break; + case 0x1: ps2_mc_auth_getIv(); break; + case 0x2: ps2_mc_auth_getSeed(); break; + case 0x3: ps2_mc_auth_dummy3(); break; + case 0x4: ps2_mc_auth_getNonce(); break; + case 0x5: ps2_mc_auth_dummy5(); break; + case 0x6: ps2_mc_auth_mechaChallenge3(); break; + case 0x7: ps2_mc_auth_mechaChallenge2(); break; + case 0x8: ps2_mc_auth_dummy8(); break; + case 0x9: ps2_mc_auth_dummy9(); break; + case 0xA: ps2_mc_auth_dummyA(); break; + case 0xB: ps2_mc_auth_mechaChallenge1(); break; + case 0xC: ps2_mc_auth_dummyC(); break; + case 0xD: ps2_mc_auth_dummyD(); break; + case 0xE: ps2_mc_auth_dummyE(); break; + case 0xF: ps2_mc_auth_cardResponse1(); break; + case 0x10: ps2_mc_auth_dummy10(); break; + case 0x11: ps2_mc_auth_cardResponse2(); break; + case 0x12: ps2_mc_auth_dummy12(); break; + case 0x13: ps2_mc_auth_cardResponse3(); break; + case 0x14: ps2_mc_auth_dummy14(); break; + default: + // debug_printf("unknown %02X -> %02X\n", ch, subcmd); + break; + } +} diff --git a/src/ps2/card_emu/ps2_mc_auth.h b/src/ps2/card_emu/ps2_mc_auth.h new file mode 100644 index 0000000..d4cf9ac --- /dev/null +++ b/src/ps2/card_emu/ps2_mc_auth.h @@ -0,0 +1,9 @@ +#pragma once + +#include "pico/platform.h" + +extern void ps2_mc_auth(void); +extern void ps2_mc_sessionKeyEncr(void); + + +void __time_critical_func(generateIvSeedNonce)(void); diff --git a/src/ps2/card_emu/ps2_mc_commands.c b/src/ps2/card_emu/ps2_mc_commands.c new file mode 100644 index 0000000..572127a --- /dev/null +++ b/src/ps2/card_emu/ps2_mc_commands.c @@ -0,0 +1,367 @@ +#include "ps2_mc_commands.h" + +#include +#include + +#include "hardware/dma.h" +#include "pico/platform.h" +#include "ps2_cardman.h" +#include "ps2_dirty.h" +#include "ps2_mc_internal.h" +#include "ps2_pio_qspi.h" + +uint32_t read_sector, write_sector, erase_sector; +struct { + uint32_t prefix; + uint8_t buf[528]; +} readtmp; +uint8_t writetmp[528]; +int is_write, is_dma_read; +uint32_t readptr, writeptr; +uint8_t *eccptr; + +inline __attribute__((always_inline)) void ps2_mc_cmd_0x11(void) { + uint8_t _ = 0U; + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_0x12(void) { + uint8_t _ = 0U; + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_setEraseAddress(void) { + uint8_t _ = 0; + /* set address for erase */ + union { + uint8_t a[4]; + uint32_t addr; + } raw; + uint8_t ck; + mc_respond(0xFF); + receiveOrNextCmd(&raw.a[0]); + mc_respond(0xFF); + receiveOrNextCmd(&raw.a[1]); + mc_respond(0xFF); + receiveOrNextCmd(&raw.a[2]); + mc_respond(0xFF); + receiveOrNextCmd(&raw.a[3]); + mc_respond(0xFF); + receiveOrNextCmd(&ck); + mc_respond(0x2B); + receiveOrNextCmd(&_); + (void)ck; // TODO: validate checksum + erase_sector = raw.addr; + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_setWriteAddress(void) { + uint8_t _ = 0; + /* set address for write */ + union { + uint8_t a[4]; + uint32_t addr; + } raw; + uint8_t ck; + mc_respond(0xFF); + receiveOrNextCmd(&raw.a[0]); + mc_respond(0xFF); + receiveOrNextCmd(&raw.a[1]); + mc_respond(0xFF); + receiveOrNextCmd(&raw.a[2]); + mc_respond(0xFF); + receiveOrNextCmd(&raw.a[3]); + mc_respond(0xFF); + receiveOrNextCmd(&ck); + mc_respond(0x2B); + receiveOrNextCmd(&_); + (void)ck; // TODO: validate checksum + write_sector = raw.addr; + is_write = 1; + writeptr = 0; + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_setReadAddress(void) { + uint8_t _ = 0U; + /* set address for read */ + union { + uint8_t a[4]; + uint32_t addr; + } raw; + uint8_t ck; + mc_respond(0xFF); + receiveOrNextCmd(&raw.a[0]); + mc_respond(0xFF); + receiveOrNextCmd(&raw.a[1]); + mc_respond(0xFF); + receiveOrNextCmd(&raw.a[2]); + mc_respond(0xFF); + receiveOrNextCmd(&raw.a[3]); + mc_respond(0xFF); + receiveOrNextCmd(&ck); + mc_respond(0x2B); + receiveOrNextCmd(&_); + (void)ck; // TODO: validate checksum + read_sector = raw.addr; + if (read_sector * 512 + 512 <= ps2_cardman_get_card_size()) { + ps2_dirty_lockout_renew(); + /* the spinlock will be unlocked by the DMA irq once all data is tx'd */ + ps2_dirty_lock(); + read_mc(read_sector * 512, &readtmp, 512 + 4); + // dma_channel_wait_for_finish_blocking(0); + // dma_channel_wait_for_finish_blocking(1); + } + readptr = 0; + + eccptr = &readtmp.buf[512]; + memset(eccptr, 0, 16); + + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_getSpecs(void) { + uint8_t _ = 0; + /* GET_SPECS ? */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + uint32_t sector_count = (flash_mode) ? PS2_CARD_SIZE_1M / 512 : (uint32_t)(ps2_cardman_get_card_size() / 512); + + uint8_t specs[] = {0x00, 0x02, ERASE_SECTORS, 0x00, 0x00, 0x40, 0x00, 0x00}; + specs[4] = (uint8_t)(sector_count & 0xFF); + specs[5] = (uint8_t)((sector_count >> 8) & 0xFF); + specs[6] = (uint8_t)((sector_count >> 16) & 0xFF); + specs[7] = (uint8_t)((sector_count >> 24) & 0xFF); + + mc_respond(specs[0]); + receiveOrNextCmd(&_); + mc_respond(specs[1]); + receiveOrNextCmd(&_); + mc_respond(specs[2]); + receiveOrNextCmd(&_); + mc_respond(specs[3]); + receiveOrNextCmd(&_); + mc_respond(specs[4]); + receiveOrNextCmd(&_); + mc_respond(specs[5]); + receiveOrNextCmd(&_); + mc_respond(specs[6]); + receiveOrNextCmd(&_); + mc_respond(specs[7]); + receiveOrNextCmd(&_); + mc_respond(XOR8(specs)); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_setTerminator(void) { + uint8_t _ = 0U; + /* SET_TERMINATOR */ + mc_respond(0xFF); + receiveOrNextCmd(&term); + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_getTerminator(void) { + uint8_t _ = 0U; + /* GET_TERMINATOR */ + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_writeData(void) { + uint8_t ck2 = 0U; + uint8_t _ = 0U; + /* write data */ + uint8_t sz; + mc_respond(0xFF); + receiveOrNextCmd(&sz); + mc_respond(0xFF); + +#ifdef DEBUG_MC_PROTOCOL + debug_printf("> %02X %02X\n", ch, sz); +#endif + + uint8_t ck = 0; + uint8_t b; + + for (int i = 0; i < sz; ++i) { + receiveOrNextCmd(&b); + if (writeptr < sizeof(writetmp)) { + writetmp[writeptr] = b; + ++writeptr; + } + ck ^= b; + mc_respond(0xFF); + } + // this should be checksum? + receiveOrNextCmd(&ck2); + (void)ck2; // TODO: validate checksum + + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_readData(void) { + uint8_t _ = 0U; + /* read data */ + uint8_t sz; + mc_respond(0xFF); + receiveOrNextCmd(&sz); + mc_respond(0x2B); + receiveOrNextCmd(&_); + +#ifdef DEBUG_MC_PROTOCOL + debug_printf("> %02X %02X\n", ch, sz); +#endif + + uint8_t ck = 0; + uint8_t b = 0xFF; + + for (int i = 0; i < sz; ++i) { + if (readptr == sizeof(readtmp.buf)) { + /* a game may read more than one 528-byte sector in a sequence of read ops, e.g. re4 */ + ++read_sector; + if (read_sector * 512 + 512 <= ps2_cardman_get_card_size()) { + ps2_dirty_lockout_renew(); + /* the spinlock will be unlocked by the DMA irq once all data is tx'd */ + ps2_dirty_lock(); + read_mc(read_sector * 512, &readtmp, 512 + 4); + // TODO: remove this if safe + // must make sure the dma completes for first byte before we start reading below + dma_channel_wait_for_finish_blocking(PIO_SPI_DMA_RX_CHAN); + dma_channel_wait_for_finish_blocking(PIO_SPI_DMA_TX_CHAN); + } + readptr = 0; + + eccptr = &readtmp.buf[512]; + memset(eccptr, 0, 16); + } + + if (readptr < sizeof(readtmp.buf)) { + b = readtmp.buf[readptr]; + mc_respond(b); + + if (readptr <= 512) { + uint8_t c = Table[b]; + eccptr[0] ^= c; + if (c & 0x80) { + eccptr[1] ^= ~(readptr & 0x7F); + eccptr[2] ^= (readptr & 0x7F); + } + + ++readptr; + + if ((readptr & 0x7F) == 0) { + eccptr[0] = ~eccptr[0]; + eccptr[0] &= 0x77; + + eccptr[1] = ~eccptr[1]; + eccptr[1] &= 0x7f; + + eccptr[2] = ~eccptr[2]; + eccptr[2] &= 0x7f; + + eccptr += 3; + } + } else { + ++readptr; + } + } else + mc_respond(b); + ck ^= b; + receiveOrNextCmd(&_); + } + + mc_respond(ck); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_commitData(void) { + uint8_t _ = 0; + /* commit for read/write? */ + if (is_write) { + is_write = 0; + if (write_sector * 512 + 512 <= ps2_cardman_get_card_size()) { + ps2_dirty_lockout_renew(); + ps2_dirty_lock(); + write_mc(write_sector * 512, writetmp, 512); + ps2_dirty_mark(write_sector); + ps2_dirty_unlock(); +#ifdef DEBUG_MC_PROTOCOL + debug_printf("WR 0x%08X : %02X %02X .. %08X %08X %08X\n", write_sector * 512, writetmp[0], writetmp[1], *(uint32_t *)&writetmp[512], + *(uint32_t *)&writetmp[516], *(uint32_t *)&writetmp[520]); +#endif + } + } else { +#ifdef DEBUG_MC_PROTOCOL + debug_printf("RD 0x%08X : %02X %02X .. %08X %08X %08X\n", read_sector * 512, readtmp.buf[0], readtmp.buf[1], *(uint32_t *)&readtmp.buf[512], + *(uint32_t *)&readtmp.buf[516], *(uint32_t *)&readtmp.buf[520]); +#endif + } + + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_erase(void) { + uint8_t _ = 0U; + /* do erase */ + if (erase_sector * 512 + 512 * ERASE_SECTORS <= ps2_cardman_get_card_size()) { + memset(readtmp.buf, 0xFF, 512); + ps2_dirty_lockout_renew(); + ps2_dirty_lock(); + for (int i = 0; i < ERASE_SECTORS; ++i) { + write_mc((erase_sector + i) * 512, readtmp.buf, 512); + ps2_dirty_mark(erase_sector + i); + } + ps2_dirty_unlock(); +#ifdef DEBUG_MC_PROTOCOL + debug_printf("ER 0x%08X\n", erase_sector * 512); +#endif + } + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_0xBF(void) { + uint8_t _ = 0U; + mc_respond(0xFF); + receiveOrNextCmd(&_); + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_0xF3(void) { + uint8_t _ = 0U; + mc_respond(0xFF); + receiveOrNextCmd(&_); + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} + +inline __attribute__((always_inline)) void ps2_mc_cmd_keySelect( + void) { // TODO: it fails to get detected at all when ps2_magicgate==0, check if it's intentional + uint8_t _ = 0U; + /* SIO_MEMCARD_KEY_SELECT */ + mc_respond(0xFF); + receiveOrNextCmd(&_); + mc_respond(0x2B); + receiveOrNextCmd(&_); + mc_respond(term); +} diff --git a/src/ps2/card_emu/ps2_mc_commands.h b/src/ps2/card_emu/ps2_mc_commands.h new file mode 100644 index 0000000..b715920 --- /dev/null +++ b/src/ps2/card_emu/ps2_mc_commands.h @@ -0,0 +1,38 @@ +#pragma once + +#define PS2_SIO2_CMD_0x11 0x11 +#define PS2_SIO2_CMD_0x12 0x12 +#define PS2_SIO2_CMD_SET_ERASE_ADDRESS 0x21 +#define PS2_SIO2_CMD_SET_WRITE_ADDRESS 0x22 +#define PS2_SIO2_CMD_SET_READ_ADDRESS 0x23 +#define PS2_SIO2_CMD_GET_SPECS 0x26 +#define PS2_SIO2_CMD_SET_TERMINATOR 0x27 +#define PS2_SIO2_CMD_GET_TERMINATOR 0x28 +#define PS2_SIO2_CMD_WRITE_DATA 0x42 +#define PS2_SIO2_CMD_READ_DATA 0x43 +#define PS2_SIO2_CMD_COMMIT_DATA 0x81 +#define PS2_SIO2_CMD_ERASE 0x82 +#define PS2_SIO2_CMD_BF 0xBF +#define PS2_SIO2_CMD_F3 0xF3 +#define PS2_SIO2_CMD_KEY_SELECT 0xF7 +#define PS2_SIO2_CMD_AUTH 0xF0 +#define PS2_SIO2_CMD_SESSION_KEY_0 0xF1 +#define PS2_SIO2_CMD_SESSION_KEY_1 0xF2 + + +extern void ps2_mc_cmd_0x11(void); +extern void ps2_mc_cmd_0x12(void); +extern void ps2_mc_cmd_setEraseAddress(void); +extern void ps2_mc_cmd_setWriteAddress(void); +extern void ps2_mc_cmd_setReadAddress(void); +extern void ps2_mc_cmd_getSpecs(void); +extern void ps2_mc_cmd_setTerminator(void); +extern void ps2_mc_cmd_getTerminator(void); +extern void ps2_mc_cmd_writeData(void); +extern void ps2_mc_cmd_readData(void); +extern void ps2_mc_cmd_commitData(void); +extern void ps2_mc_cmd_erase(void); +extern void ps2_mc_cmd_0xBF(void); +extern void ps2_mc_cmd_0xF3(void); +extern void ps2_mc_cmd_keySelect(void); + diff --git a/src/ps2/card_emu/ps2_mc_internal.h b/src/ps2/card_emu/ps2_mc_internal.h new file mode 100644 index 0000000..020993b --- /dev/null +++ b/src/ps2/card_emu/ps2_mc_internal.h @@ -0,0 +1,38 @@ +#pragma once + +#include "../ps2_exploit.h" +#include "../ps2_dirty.h" +#include "../ps2_psram.h" + + +#include +#include + + +#define ERASE_SECTORS 16 +#define CARD_SIZE (8 * 1024 * 1024) + +#define XOR8(a) (a[0] ^ a[1] ^ a[2] ^ a[3] ^ a[4] ^ a[5] ^ a[6] ^ a[7]) +#define ARG8(a) a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7] + +enum { RECEIVE_RESET, RECEIVE_EXIT, RECEIVE_OK }; + +extern uint8_t term; +extern uint32_t read_sector, write_sector, erase_sector; +extern uint8_t writetmp[528]; +extern int is_write, is_dma_read; +extern uint32_t readptr, writeptr; +extern uint8_t *eccptr; +extern bool flash_mode; + +extern uint8_t Table[]; + +extern uint8_t receive(uint8_t *cmd); +extern uint8_t receiveFirst(uint8_t *cmd); +extern void __time_critical_func(mc_respond)(uint8_t ch); +extern void __time_critical_func(read_mc)(uint32_t addr, void *buf, size_t sz); +extern void __time_critical_func(write_mc)(uint32_t addr, void *buf, size_t sz); + +#define receiveOrNextCmd(cmd) \ + if (receive(cmd) == RECEIVE_RESET) \ + return diff --git a/src/ps2/card_emu/ps2_memory_card.c b/src/ps2/card_emu/ps2_memory_card.c index 1318bf2..5653b83 100644 --- a/src/ps2/card_emu/ps2_memory_card.c +++ b/src/ps2/card_emu/ps2_memory_card.c @@ -15,8 +15,10 @@ #include "hardware/timer.h" #include "keystore.h" #include "pico/platform.h" +#include "ps2_mc_auth.h" +#include "ps2_mc_commands.h" +#include "ps2_mc_internal.h" #include "ps2_mc_spi.pio.h" -// #include "ps2_mx4sio.pio.h" #include #include @@ -26,10 +28,8 @@ uint64_t us_startup; -int byte_count; volatile int reset; -int ignore; -uint8_t flag; + bool flash_mode = false; static char received_game_id[0x10] = {0}; @@ -39,27 +39,12 @@ typedef struct { uint32_t sm; } pio_t; -enum { RECEIVE_RESET, RECEIVE_EXIT, RECEIVE_OK }; - pio_t cmd_reader, dat_writer, clock_probe; - -#define ERASE_SECTORS 16 -#define CARD_SIZE (8 * 1024 * 1024) - uint8_t term = 0xFF; -uint32_t read_sector, write_sector, erase_sector; -struct { - uint32_t prefix; - uint8_t buf[528]; -} readtmp; -uint8_t *eccptr; -uint8_t writetmp[528]; -int is_write, is_dma_read; -uint32_t readptr, writeptr; + static volatile int mc_exit_request, mc_exit_response, mc_enter_request, mc_enter_response; -static uint8_t hostkey[9]; -static inline void __time_critical_func(read_mc)(uint32_t addr, void *buf, size_t sz) { +inline void __time_critical_func(read_mc)(uint32_t addr, void *buf, size_t sz) { if (flash_mode) { ps2_exploit_read(addr, buf, sz); ps2_dirty_unlock(); @@ -68,7 +53,7 @@ static inline void __time_critical_func(read_mc)(uint32_t addr, void *buf, size_ } } -static inline void __time_critical_func(write_mc)(uint32_t addr, void *buf, size_t sz) { +inline void __time_critical_func(write_mc)(uint32_t addr, void *buf, size_t sz) { if (!flash_mode) { psram_write(addr, buf, sz); } else { @@ -135,7 +120,7 @@ static void __time_critical_func(card_deselected)(uint gpio, uint32_t event_mask } } -static inline __attribute__((always_inline)) uint8_t receive(uint8_t *cmd) { +inline __attribute__((always_inline)) uint8_t receive(uint8_t *cmd) { while (pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && 1) { if (reset) @@ -145,21 +130,7 @@ static inline __attribute__((always_inline)) uint8_t receive(uint8_t *cmd) { return RECEIVE_OK; } -#define recv() \ - do { \ - while (pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && 1) { \ - if (reset) \ - goto NEXTCMD; \ - } \ - cmd = (uint8_t)(pio_sm_get(pio0, cmd_reader.sm) >> 24); \ - } while (0); - -#define receiveOrNextCmd(cmd) \ - if (receive(cmd) == RECEIVE_RESET) \ - continue - -static inline __attribute__((always_inline)) uint8_t receiveFirst(uint8_t *cmd) { +inline __attribute__((always_inline)) uint8_t receiveFirst(uint8_t *cmd) { while (pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && 1) { if (reset) @@ -171,23 +142,11 @@ static inline __attribute__((always_inline)) uint8_t receiveFirst(uint8_t *cmd) return RECEIVE_OK; } -#define recvfirst() \ - do { \ - while (pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && \ - pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && pio_sm_is_rx_fifo_empty(pio0, cmd_reader.sm) && 1) { \ - if (reset) \ - goto NEXTCMD; \ - if (mc_exit_request) \ - goto EXIT_REQUEST; \ - } \ - cmd = (uint8_t)(pio_sm_get(pio0, cmd_reader.sm) >> 24); \ - } while (0); - -static inline void __time_critical_func(mc_respond)(uint8_t ch) { +inline void __time_critical_func(mc_respond)(uint8_t ch) { pio_sm_put_blocking(pio0, dat_writer.sm, ch); } -static uint8_t Table[] = { +uint8_t Table[] = { 0x00, 0x87, 0x96, 0x11, 0xa5, 0x22, 0x33, 0xb4, 0xb4, 0x33, 0x22, 0xa5, 0x11, 0x96, 0x87, 0x00, 0xc3, 0x44, 0x55, 0xd2, 0x66, 0xe1, 0xf0, 0x77, 0x77, 0xf0, 0xe1, 0x66, 0xd2, 0x55, 0x44, 0xc3, 0xd2, 0x55, 0x44, 0xc3, 0x77, 0xf0, 0xe1, 0x66, 0x66, 0xe1, 0xf0, 0x77, 0xc3, 0x44, 0x55, 0xd2, 0x11, 0x96, 0x87, 0x00, 0xb4, 0x33, 0x22, 0xa5, 0xa5, 0x22, 0x33, 0xb4, 0x00, 0x87, 0x96, 0x11, 0xe1, 0x66, 0x77, 0xf0, 0x44, 0xc3, 0xd2, 0x55, 0x55, 0xd2, 0xc3, 0x44, 0xf0, 0x77, @@ -225,83 +184,9 @@ void calcECC(uint8_t *ecc, const uint8_t *data) { return; } -// keysource and key are self generated values -uint8_t keysource[] = {0xf5, 0x80, 0x95, 0x3c, 0x4c, 0x84, 0xa9, 0xc0}; -uint8_t dex_key[16] = {0x17, 0x39, 0xd3, 0xbc, 0xd0, 0x2c, 0x18, 0x07, 0x4b, 0x17, 0xf0, 0xea, 0xc4, 0x66, 0x30, 0xf9}; -uint8_t cex_key[16] = {0x06, 0x46, 0x7a, 0x6c, 0x5b, 0x9b, 0x82, 0x77, 0x0d, 0xdf, 0xe9, 0x7e, 0x24, 0x5b, 0x9f, 0xca}; -uint8_t *key = cex_key; - -static uint8_t iv[8]; -static uint8_t seed[8]; -static uint8_t nonce[8]; -static uint8_t MechaChallenge3[8]; -static uint8_t MechaChallenge2[8]; -static uint8_t MechaChallenge1[8]; -static uint8_t CardResponse1[8]; -static uint8_t CardResponse2[8]; -static uint8_t CardResponse3[8]; - -static void __time_critical_func(desEncrypt)(void *key, void *data) { - DesContext dc; - desInit(&dc, (uint8_t *)key, 8); - desEncryptBlock(&dc, (uint8_t *)data, (uint8_t *)data); -} - -static void __time_critical_func(desDecrypt)(void *key, void *data) { - DesContext dc; - desInit(&dc, (uint8_t *)key, 8); - desDecryptBlock(&dc, (uint8_t *)data, (uint8_t *)data); -} - -static void __time_critical_func(doubleDesEncrypt)(void *key, void *data) { - desEncrypt(key, data); - desDecrypt(&((uint8_t *)key)[8], data); - desEncrypt(key, data); -} - -static void __time_critical_func(doubleDesDecrypt)(void *key, void *data) { - desDecrypt(key, data); - desEncrypt(&((uint8_t *)key)[8], data); - desDecrypt(key, data); -} - -static void __time_critical_func(xor_bit)(const void *a, const void *b, void *Result, size_t Length) { - size_t i; - for (i = 0; i < Length; i++) { - ((uint8_t *)Result)[i] = ((uint8_t *)a)[i] ^ ((uint8_t *)b)[i]; - } -} - -static void __time_critical_func(generateIvSeedNonce)() { - for (int i = 0; i < 8; i++) { - iv[i] = 0x42; - seed[i] = keysource[i] ^ iv[i]; - nonce[i] = 0x42; - } -} - -static void __time_critical_func(generateResponse)() { - doubleDesDecrypt(key, MechaChallenge1); - uint8_t random[8] = {0}; - xor_bit(MechaChallenge1, ps2_civ, random, 8); - - // MechaChallenge2 and MechaChallenge3 let's the card verify the console - - xor_bit(nonce, ps2_civ, CardResponse1, 8); - - doubleDesEncrypt(key, CardResponse1); - - xor_bit(random, CardResponse1, CardResponse2, 8); - doubleDesEncrypt(key, CardResponse2); - - uint8_t CardKey[] = {'M', 'e', 'c', 'h', 'a', 'P', 'w', 'n'}; - xor_bit(CardKey, CardResponse2, CardResponse3, 8); - doubleDesEncrypt(key, CardResponse3); -} - static void __time_critical_func(mc_main_loop)(void) { while (1) { - uint8_t cmd, ch; + uint8_t cmd = 0; while (!reset && !reset && !reset && !reset && !reset) { if (mc_exit_request) { @@ -309,21 +194,53 @@ static void __time_critical_func(mc_main_loop)(void) { return; } } - reset = 0; // recvfirst(); uint8_t received = receiveFirst(&cmd); - if (received == RECEIVE_EXIT) + if (received == RECEIVE_EXIT) { + mc_exit_response = 1; break; + } if (received == RECEIVE_RESET) continue; if (cmd == 0x81) { -#include "ps2_memory_card.in.c" + /* resp to 0x81 */ + mc_respond(0xFF); + + /* sub cmd */ + receiveOrNextCmd(&cmd); + + switch (cmd) { + case PS2_SIO2_CMD_0x11: ps2_mc_cmd_0x11(); break; + case PS2_SIO2_CMD_0x12: ps2_mc_cmd_0x12(); break; + case PS2_SIO2_CMD_SET_ERASE_ADDRESS: ps2_mc_cmd_setEraseAddress(); break; + case PS2_SIO2_CMD_SET_WRITE_ADDRESS: ps2_mc_cmd_setWriteAddress(); break; + case PS2_SIO2_CMD_SET_READ_ADDRESS: ps2_mc_cmd_setReadAddress(); break; + case PS2_SIO2_CMD_GET_SPECS: ps2_mc_cmd_getSpecs(); break; + case PS2_SIO2_CMD_SET_TERMINATOR: ps2_mc_cmd_setTerminator(); break; + case PS2_SIO2_CMD_GET_TERMINATOR: ps2_mc_cmd_getTerminator(); break; + case PS2_SIO2_CMD_WRITE_DATA: ps2_mc_cmd_writeData(); break; + case PS2_SIO2_CMD_READ_DATA: ps2_mc_cmd_readData(); break; + case PS2_SIO2_CMD_COMMIT_DATA: ps2_mc_cmd_commitData(); break; + case PS2_SIO2_CMD_ERASE: ps2_mc_cmd_erase(); break; + case PS2_SIO2_CMD_BF: ps2_mc_cmd_0xBF(); break; + case PS2_SIO2_CMD_F3: ps2_mc_cmd_0xF3(); break; + case PS2_SIO2_CMD_KEY_SELECT: ps2_mc_cmd_keySelect(); break; + case PS2_SIO2_CMD_AUTH: + if (ps2_magicgate) + ps2_mc_auth(); + break; + case PS2_SIO2_CMD_SESSION_KEY_0: + case PS2_SIO2_CMD_SESSION_KEY_1: ps2_mc_sessionKeyEncr(); break; + default: debug_printf("Unknown Subcommand: %02x\n", cmd); break; + } } else if (cmd == 0x8A) { - mc_respond(0xFF); receiveOrNextCmd(&cmd); + /* Get sub-command for sd2psx */ + mc_respond(0xFF); + receiveOrNextCmd(&cmd); if (cmd == 0xA0) { /* SD2PSXMAN Command */ @@ -371,8 +288,12 @@ static void __time_critical_func(mc_main_loop)(void) { case 0x00: // ps2_cardman_set_channel(cmd); todo: Implement! break; - case 0x01: ps2_cardman_next_channel(); break; - case 0x02: ps2_cardman_prev_channel(); break; + case 0x01: + // ps2_cardman_next_channel(); Implement logic! + break; + case 0x02: + // ps2_cardman_prev_channel(); Implement logic! + break; default: break; } mc_respond(0x00); diff --git a/src/ps2/card_emu/ps2_memory_card.h b/src/ps2/card_emu/ps2_memory_card.h index 688f35e..ca00cfb 100644 --- a/src/ps2/card_emu/ps2_memory_card.h +++ b/src/ps2/card_emu/ps2_memory_card.h @@ -1,6 +1,6 @@ #pragma once -#include + void ps2_memory_card_main(void); void ps2_memory_card_enter(void); diff --git a/src/ps2/card_emu/ps2_memory_card.in.c b/src/ps2/card_emu/ps2_memory_card.in.c deleted file mode 100644 index dd3ed41..0000000 --- a/src/ps2/card_emu/ps2_memory_card.in.c +++ /dev/null @@ -1,524 +0,0 @@ -#include -#include -#define XOR8(a) (a[0] ^ a[1] ^ a[2] ^ a[3] ^ a[4] ^ a[5] ^ a[6] ^ a[7]) -#define ARG8(a) a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7] - -/* resp to 0x81 */ -mc_respond(0xFF); - -/* sub cmd */ -receiveOrNextCmd(&cmd); -ch = cmd; -#ifdef DEBUG_MC_PROTOCOL -if (ch != 0x42 && ch != 0x43) - debug_printf("> %02X\n", ch); -#endif - -if (ch == 0x11) { - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); -} else if (ch == 0x12) { - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); -} else if (ch == 0x21) { - /* set address for erase */ - union { - uint8_t a[4]; - uint32_t addr; - } raw; - uint8_t ck; - mc_respond(0xFF); receiveOrNextCmd(&cmd); raw.a[0] = cmd; - mc_respond(0xFF); receiveOrNextCmd(&cmd); raw.a[1] = cmd; - mc_respond(0xFF); receiveOrNextCmd(&cmd); raw.a[2] = cmd; - mc_respond(0xFF); receiveOrNextCmd(&cmd); raw.a[3] = cmd; - mc_respond(0xFF); receiveOrNextCmd(&cmd); ck = cmd; - mc_respond(0x2B); receiveOrNextCmd(&cmd); - (void)ck; // TODO: validate checksum - erase_sector = raw.addr; - mc_respond(term); -} else if (cmd == 0x22) { - /* set address for write */ - union { - uint8_t a[4]; - uint32_t addr; - } raw; - uint8_t ck; - mc_respond(0xFF); receiveOrNextCmd(&cmd); raw.a[0] = cmd; - mc_respond(0xFF); receiveOrNextCmd(&cmd); raw.a[1] = cmd; - mc_respond(0xFF); receiveOrNextCmd(&cmd); raw.a[2] = cmd; - mc_respond(0xFF); receiveOrNextCmd(&cmd); raw.a[3] = cmd; - mc_respond(0xFF); receiveOrNextCmd(&cmd); ck = cmd; - mc_respond(0x2B); receiveOrNextCmd(&cmd); - (void)ck; // TODO: validate checksum - write_sector = raw.addr; - is_write = 1; - writeptr = 0; - mc_respond(term); -} else if (ch == 0x23) { - /* set address for read */ - union { - uint8_t a[4]; - uint32_t addr; - } raw; - uint8_t ck; - mc_respond(0xFF); receiveOrNextCmd(&cmd); raw.a[0] = cmd; - mc_respond(0xFF); receiveOrNextCmd(&cmd); raw.a[1] = cmd; - mc_respond(0xFF); receiveOrNextCmd(&cmd); raw.a[2] = cmd; - mc_respond(0xFF); receiveOrNextCmd(&cmd); raw.a[3] = cmd; - mc_respond(0xFF); receiveOrNextCmd(&cmd); ck = cmd; - mc_respond(0x2B); receiveOrNextCmd(&cmd); - (void)ck; // TODO: validate checksum - read_sector = raw.addr; - if (read_sector * 512 + 512 <= ps2_cardman_get_card_size()) { - ps2_dirty_lockout_renew(); - /* the spinlock will be unlocked by the DMA irq once all data is tx'd */ - ps2_dirty_lock(); - read_mc(read_sector * 512, &readtmp, 512+4); - // dma_channel_wait_for_finish_blocking(0); - // dma_channel_wait_for_finish_blocking(1); - } - readptr = 0; - - eccptr = &readtmp.buf[512]; - memset(eccptr, 0, 16); - - mc_respond(term); -} else if (ch == 0x26) { - /* GET_SPECS ? */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - uint32_t sector_count = (flash_mode) ? PS2_CARD_SIZE_1M / 512 : (uint32_t)(ps2_cardman_get_card_size() / 512); - - uint8_t specs[] = { 0x00, 0x02, ERASE_SECTORS, 0x00, 0x00, 0x40, 0x00, 0x00 }; - specs[4] = (uint8_t)(sector_count & 0xFF); - specs[5] = (uint8_t)((sector_count >> 8) & 0xFF); - specs[6] = (uint8_t)((sector_count >> 16) & 0xFF); - specs[7] = (uint8_t)((sector_count >> 24) & 0xFF); - - mc_respond(specs[0]); receiveOrNextCmd(&cmd); - mc_respond(specs[1]); receiveOrNextCmd(&cmd); - mc_respond(specs[2]); receiveOrNextCmd(&cmd); - mc_respond(specs[3]); receiveOrNextCmd(&cmd); - mc_respond(specs[4]); receiveOrNextCmd(&cmd); - mc_respond(specs[5]); receiveOrNextCmd(&cmd); - mc_respond(specs[6]); receiveOrNextCmd(&cmd); - mc_respond(specs[7]); receiveOrNextCmd(&cmd); - mc_respond(XOR8(specs)); receiveOrNextCmd(&cmd); - mc_respond(term); -} else if (ch == 0x27) { - /* SET_TERMINATOR */ - mc_respond(0xFF); - receiveOrNextCmd(&cmd); term = cmd; - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); -} else if (ch == 0x28) { - /* GET_TERMINATOR */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); receiveOrNextCmd(&cmd); - mc_respond(term); -} else if (ch == 0x42) { - /* write data */ - uint8_t sz; - mc_respond(0xFF); receiveOrNextCmd(&cmd); sz = cmd; - mc_respond(0xFF); - -#ifdef DEBUG_MC_PROTOCOL - debug_printf("> %02X %02X\n", ch, sz); -#endif - - uint8_t ck = 0; - uint8_t b; - - for (int i = 0; i < sz; ++i) { - receiveOrNextCmd(&cmd); b = cmd; - if (writeptr < sizeof(writetmp)) { - writetmp[writeptr] = b; - ++writeptr; - } - ck ^= b; - mc_respond(0xFF); - } - // this should be checksum? - receiveOrNextCmd(&cmd); - uint8_t ck2 = cmd; - (void)ck2; // TODO: validate checksum - - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); -} else if (ch == 0x43) { - /* read data */ - uint8_t sz; - mc_respond(0xFF); receiveOrNextCmd(&cmd); sz = cmd; - mc_respond(0x2B); receiveOrNextCmd(&cmd); - -#ifdef DEBUG_MC_PROTOCOL - debug_printf("> %02X %02X\n", ch, sz); -#endif - - uint8_t ck = 0; - uint8_t b = 0xFF; - - for (int i = 0; i < sz; ++i) { - if (readptr == sizeof(readtmp.buf)) { - /* a game may read more than one 528-byte sector in a sequence of read ops, e.g. re4 */ - ++read_sector; - if (read_sector * 512 + 512 <= ps2_cardman_get_card_size()) { - ps2_dirty_lockout_renew(); - /* the spinlock will be unlocked by the DMA irq once all data is tx'd */ - ps2_dirty_lock(); - read_mc(read_sector * 512, &readtmp, 512+4); - // TODO: remove this if safe - // must make sure the dma completes for first byte before we start reading below - dma_channel_wait_for_finish_blocking(PIO_SPI_DMA_RX_CHAN); - dma_channel_wait_for_finish_blocking(PIO_SPI_DMA_TX_CHAN); - } - readptr = 0; - - eccptr = &readtmp.buf[512]; - memset(eccptr, 0, 16); - } - - if (readptr < sizeof(readtmp.buf)) { - b = readtmp.buf[readptr]; - mc_respond(b); - - if (readptr <= 512) { - uint8_t c = Table[b]; - eccptr[0] ^= c; - if (c & 0x80) { - eccptr[1] ^= ~(readptr & 0x7F); - eccptr[2] ^= (readptr & 0x7F); - } - - ++readptr; - - if ((readptr & 0x7F) == 0) { - eccptr[0] = ~eccptr[0]; - eccptr[0] &= 0x77; - - eccptr[1] = ~eccptr[1]; - eccptr[1] &= 0x7f; - - eccptr[2] = ~eccptr[2]; - eccptr[2] &= 0x7f; - - eccptr += 3; - } - } else { - ++readptr; - } - } else mc_respond(b); - ck ^= b; - receiveOrNextCmd(&cmd); - } - - mc_respond(ck); receiveOrNextCmd(&cmd); - mc_respond(term); -} else if (ch == 0x81) { - /* commit for read/write? */ - if (is_write) { - is_write = 0; - if (write_sector * 512 + 512 <= ps2_cardman_get_card_size()) { - ps2_dirty_lockout_renew(); - ps2_dirty_lock(); - write_mc(write_sector * 512, writetmp, 512); - ps2_dirty_mark(write_sector); - ps2_dirty_unlock(); -#ifdef DEBUG_MC_PROTOCOL - debug_printf("WR 0x%08X : %02X %02X .. %08X %08X %08X\n", - write_sector * 512, writetmp[0], writetmp[1], - *(uint32_t*)&writetmp[512], *(uint32_t*)&writetmp[516], *(uint32_t*)&writetmp[520]); -#endif - } - } else { -#ifdef DEBUG_MC_PROTOCOL - debug_printf("RD 0x%08X : %02X %02X .. %08X %08X %08X\n", - read_sector * 512, readtmp.buf[0], readtmp.buf[1], - *(uint32_t*)&readtmp.buf[512], *(uint32_t*)&readtmp.buf[516], *(uint32_t*)&readtmp.buf[520]); -#endif - } - - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); -} else if (ch == 0x82) { - /* do erase */ - if (erase_sector * 512 + 512 * ERASE_SECTORS <= ps2_cardman_get_card_size()) { - memset(readtmp.buf, 0xFF, 512); - ps2_dirty_lockout_renew(); - ps2_dirty_lock(); - for (int i = 0; i < ERASE_SECTORS; ++i) { - write_mc((erase_sector + i) * 512, readtmp.buf, 512); - ps2_dirty_mark(erase_sector + i); - } - ps2_dirty_unlock(); -#ifdef DEBUG_MC_PROTOCOL - debug_printf("ER 0x%08X\n", erase_sector * 512); -#endif - } - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); -} else if (ch == 0xBF) { - mc_respond(0xFF); receiveOrNextCmd(&cmd); - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); -} else if (ch == 0xF3) { - mc_respond(0xFF); receiveOrNextCmd(&cmd); - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); -} else if ((ch == 0xF7) && ps2_magicgate) { // TODO: it fails to get detected at all when ps2_magicgate==0, check if it's intentional - /* SIO_MEMCARD_KEY_SELECT */ - mc_respond(0xFF); receiveOrNextCmd(&cmd); - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); -} else if ((ch == 0xF0) && ps2_magicgate) { - /* auth stuff */ - mc_respond(0xFF); - receiveOrNextCmd(&cmd); - int subcmd = cmd; - if (subcmd == 0) { - /* probe support ? */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 1) { - debug_printf("iv : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(iv)); - - /* get IV */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(iv[7]); receiveOrNextCmd(&cmd); - mc_respond(iv[6]); receiveOrNextCmd(&cmd); - mc_respond(iv[5]); receiveOrNextCmd(&cmd); - mc_respond(iv[4]); receiveOrNextCmd(&cmd); - mc_respond(iv[3]); receiveOrNextCmd(&cmd); - mc_respond(iv[2]); receiveOrNextCmd(&cmd); - mc_respond(iv[1]); receiveOrNextCmd(&cmd); - mc_respond(iv[0]); receiveOrNextCmd(&cmd); - mc_respond(XOR8(iv)); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 2) { - debug_printf("seed : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(seed)); - - /* get seed */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(seed[7]); receiveOrNextCmd(&cmd); - mc_respond(seed[6]); receiveOrNextCmd(&cmd); - mc_respond(seed[5]); receiveOrNextCmd(&cmd); - mc_respond(seed[4]); receiveOrNextCmd(&cmd); - mc_respond(seed[3]); receiveOrNextCmd(&cmd); - mc_respond(seed[2]); receiveOrNextCmd(&cmd); - mc_respond(seed[1]); receiveOrNextCmd(&cmd); - mc_respond(seed[0]); receiveOrNextCmd(&cmd); - mc_respond(XOR8(seed)); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 3) { - /* dummy 3 */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 4) { - debug_printf("nonce : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(nonce)); - - /* get nonce */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(nonce[7]); receiveOrNextCmd(&cmd); - mc_respond(nonce[6]); receiveOrNextCmd(&cmd); - mc_respond(nonce[5]); receiveOrNextCmd(&cmd); - mc_respond(nonce[4]); receiveOrNextCmd(&cmd); - mc_respond(nonce[3]); receiveOrNextCmd(&cmd); - mc_respond(nonce[2]); receiveOrNextCmd(&cmd); - mc_respond(nonce[1]); receiveOrNextCmd(&cmd); - mc_respond(nonce[0]); receiveOrNextCmd(&cmd); - mc_respond(XOR8(nonce)); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 5) { - /* dummy 5 */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 6) { - /* MechaChallenge3 */ - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge3[7] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge3[6] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge3[5] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge3[4] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge3[3] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge3[2] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge3[1] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge3[0] = cmd; - /* TODO: checksum below */ - mc_respond(0xFF); receiveOrNextCmd(&cmd); - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - - debug_printf("MechaChallenge3 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(MechaChallenge3)); - } else if (subcmd == 7) { - /* MechaChallenge2 */ - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge2[7] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge2[6] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge2[5] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge2[4] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge2[3] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge2[2] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge2[1] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge2[0] = cmd; - /* TODO: checksum below */ - mc_respond(0xFF); receiveOrNextCmd(&cmd); - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - - debug_printf("MechaChallenge2 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(MechaChallenge2)); - } else if (subcmd == 8) { - /* dummy 8 */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 9) { - /* dummy 9 */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 0xA) { - /* dummy A */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 0xB) { - /* MechaChallenge1 */ - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge1[7] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge1[6] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge1[5] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge1[4] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge1[3] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge1[2] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge1[1] = cmd; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); MechaChallenge1[0] = cmd; - /* TODO: checksum below */ - mc_respond(0xFF); receiveOrNextCmd(&cmd); - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - - debug_printf("MechaChallenge1 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(MechaChallenge1)); - } else if (subcmd == 0xC) { - /* dummy C */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 0xD) { - /* dummy D */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 0xE) { - /* dummy E */ - generateResponse(); - debug_printf("CardResponse1 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(CardResponse1)); - debug_printf("CardResponse2 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(CardResponse2)); - debug_printf("CardResponse3 : %02X %02X %02X %02X %02X %02X %02X %02X\n", ARG8(CardResponse3)); - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 0xF) { - /* CardResponse1 */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(CardResponse1[7]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse1[6]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse1[5]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse1[4]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse1[3]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse1[2]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse1[1]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse1[0]); receiveOrNextCmd(&cmd); - mc_respond(XOR8(CardResponse1)); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 0x10) { - /* dummy 10 */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 0x11) { - /* CardResponse2 */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(CardResponse2[7]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse2[6]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse2[5]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse2[4]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse2[3]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse2[2]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse2[1]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse2[0]); receiveOrNextCmd(&cmd); - mc_respond(XOR8(CardResponse2)); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 0x12) { - /* dummy 12 */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 0x13) { - /* CardResponse3 */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(CardResponse3[7]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse3[6]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse3[5]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse3[4]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse3[3]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse3[2]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse3[1]); receiveOrNextCmd(&cmd); - mc_respond(CardResponse3[0]); receiveOrNextCmd(&cmd); - mc_respond(XOR8(CardResponse3)); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 0x14) { - /* dummy 14 */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else { - debug_printf("unknown %02X -> %02X\n", ch, subcmd); - } -} else if ((ch == 0xF1 || ch == 0xF2) && ps2_magicgate) { - /* session key encrypt */ - mc_respond(0xFF); - receiveOrNextCmd(&cmd); - int subcmd = cmd; - if (subcmd == 0x50 || subcmd == 0x40) { - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 0x51 || subcmd == 0x41) { - /* host mc_responds key to us */ - for (size_t i = 0; i < sizeof(hostkey); ++i) { - mc_respond(0xFF); - receiveOrNextCmd(&cmd); - hostkey[i] = cmd; - } - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 0x52 || subcmd == 0x42) { - /* now we encrypt/decrypt the key */ - mc_respond(0x2B); receiveOrNextCmd(&cmd); - mc_respond(term); - } else if (subcmd == 0x53 || subcmd == 0x43) { - mc_respond(0x2B); receiveOrNextCmd(&cmd); - /* we mc_respond key to the host */ - for (size_t i = 0; i < sizeof(hostkey); ++i) { - mc_respond(hostkey[i]); - receiveOrNextCmd(&cmd); - } - mc_respond(term); - } else { - debug_printf("!! unknown subcmd %02X -> %02X\n", 0xF2, subcmd); - } -} else { - debug_printf("!! unknown %02X\n", ch); -} - -#undef XOR8 diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index b841ccc..1fc3237 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -1,6 +1,6 @@ #include "ps2_cardman.h" -#include +#include "ps2_exploit.h" #include #include #include diff --git a/src/ps2/ps2_dirty.c b/src/ps2/ps2_dirty.c index ee68330..94e9261 100644 --- a/src/ps2/ps2_dirty.c +++ b/src/ps2/ps2_dirty.c @@ -6,6 +6,8 @@ #define dirty_heap bigmem.ps2.dirty_heap #define dirty_map bigmem.ps2.dirty_map +#include +#include #include spin_lock_t *ps2_dirty_spin_lock; diff --git a/src/ps2/ps2_exploit.h b/src/ps2/ps2_exploit.h index 50f8310..26d9337 100644 --- a/src/ps2/ps2_exploit.h +++ b/src/ps2/ps2_exploit.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include From 4f60239795bb4daf26249dfa639fca86de075e90 Mon Sep 17 00:00:00 2001 From: qnox32 Date: Wed, 8 Nov 2023 16:29:45 -0500 Subject: [PATCH 73/91] initial sd2psxman support --- src/main.c | 3 + src/ps2/CMakeLists.txt | 2 + src/ps2/card_emu/ps2_mc_commands.h | 2 + src/ps2/card_emu/ps2_memory_card.c | 117 ++++++--------------- src/ps2/card_emu/ps2_memory_card.h | 3 +- src/ps2/card_emu/ps2_sd2psxman.c | 72 +++++++++++++ src/ps2/card_emu/ps2_sd2psxman.h | 10 ++ src/ps2/card_emu/ps2_sd2psxman_commands.c | 121 ++++++++++++++++++++++ src/ps2/card_emu/ps2_sd2psxman_commands.h | 25 +++++ src/ps2/ps2_cardman.c | 26 +++++ src/ps2/ps2_cardman.h | 3 + 11 files changed, 295 insertions(+), 89 deletions(-) create mode 100644 src/ps2/card_emu/ps2_sd2psxman.c create mode 100644 src/ps2/card_emu/ps2_sd2psxman.h create mode 100644 src/ps2/card_emu/ps2_sd2psxman_commands.c create mode 100644 src/ps2/card_emu/ps2_sd2psxman_commands.h diff --git a/src/main.c b/src/main.c index 12ba55e..fe2440c 100644 --- a/src/main.c +++ b/src/main.c @@ -28,6 +28,8 @@ #include "ps2/ps2_psram.h" #include "ps2/ps2_exploit.h" +#include "ps2/card_emu/ps2_sd2psxman.h" + /* reboot to bootloader if either button is held on startup to make the device easier to flash when assembled inside case */ static void check_bootloader_reset(void) { @@ -130,6 +132,7 @@ int main() { while (1) { debug_task(); + ps2_sd2psxman_task(); ps2_dirty_task(); gui_task(); input_task(); diff --git a/src/ps2/CMakeLists.txt b/src/ps2/CMakeLists.txt index b276698..5fc6164 100644 --- a/src/ps2/CMakeLists.txt +++ b/src/ps2/CMakeLists.txt @@ -1,5 +1,7 @@ add_library(ps2_card STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/card_emu/ps2_sd2psxman.c + ${CMAKE_CURRENT_SOURCE_DIR}/card_emu/ps2_sd2psxman_commands.c ${CMAKE_CURRENT_SOURCE_DIR}/card_emu/ps2_memory_card.c ${CMAKE_CURRENT_SOURCE_DIR}/card_emu/ps2_mc_commands.c ${CMAKE_CURRENT_SOURCE_DIR}/card_emu/ps2_mc_auth.c diff --git a/src/ps2/card_emu/ps2_mc_commands.h b/src/ps2/card_emu/ps2_mc_commands.h index b715920..565704b 100644 --- a/src/ps2/card_emu/ps2_mc_commands.h +++ b/src/ps2/card_emu/ps2_mc_commands.h @@ -1,5 +1,7 @@ #pragma once +#define PS2_SIO2_CMD_IDENTIFIER 0x81 + #define PS2_SIO2_CMD_0x11 0x11 #define PS2_SIO2_CMD_0x12 0x12 #define PS2_SIO2_CMD_SET_ERASE_ADDRESS 0x21 diff --git a/src/ps2/card_emu/ps2_memory_card.c b/src/ps2/card_emu/ps2_memory_card.c index 5653b83..a40d150 100644 --- a/src/ps2/card_emu/ps2_memory_card.c +++ b/src/ps2/card_emu/ps2_memory_card.c @@ -24,6 +24,8 @@ #include #include +#include "ps2_sd2psxman_commands.h" + // #define DEBUG_MC_PROTOCOL uint64_t us_startup; @@ -206,7 +208,7 @@ static void __time_critical_func(mc_main_loop)(void) { if (received == RECEIVE_RESET) continue; - if (cmd == 0x81) { + if (cmd == PS2_SIO2_CMD_IDENTIFIER) { /* resp to 0x81 */ mc_respond(0xFF); @@ -237,94 +239,25 @@ static void __time_critical_func(mc_main_loop)(void) { case PS2_SIO2_CMD_SESSION_KEY_1: ps2_mc_sessionKeyEncr(); break; default: debug_printf("Unknown Subcommand: %02x\n", cmd); break; } - } else if (cmd == 0x8A) { - /* Get sub-command for sd2psx */ - mc_respond(0xFF); + } else if (cmd == PS2_SD2PSXMAN_CMD_IDENTIFIER) { + /* resp to 0x8B */ + mc_respond(0xAA); + + /* sub cmd */ receiveOrNextCmd(&cmd); - if (cmd == 0xA0) { - /* SD2PSXMAN Command */ - int subcmd = cmd; - uint8_t game_id_length; - uint8_t input_buff[0xFF] = {0}; - mc_respond(0xFF); - receiveOrNextCmd(&cmd); - debug_printf("SD2PSXMAN %02X -> %02X\n", cmd, subcmd); - mc_respond(0x00); - receiveOrNextCmd(&cmd); // Accept command - - switch (subcmd) { - case 0x20: // Ping Command - mc_respond(0x01); - receiveOrNextCmd(&cmd); // Protocol version - mc_respond(0x01); - receiveOrNextCmd(&cmd); // Product ID - 1 == SD2PSX - mc_respond(0x27); - receiveOrNextCmd(&cmd); // SD2PSX is available - mc_respond(0xFF); - receiveOrNextCmd(&cmd); // Terminate - break; - case 0x21: // Game ID command - memset(received_game_id, 0, sizeof(received_game_id)); - mc_respond(0x00); - receiveOrNextCmd(&cmd); // Accept command - mc_respond(0x00); - receiveOrNextCmd(&cmd); // Padding - game_id_length = cmd; - for (uint8_t i = 0; (i < game_id_length) && (i < 0x10); i++) { - receiveOrNextCmd(&input_buff[i]); // Receive Game ID char by char - mc_respond(input_buff[i]); - } - game_names_extract_title_id(input_buff, received_game_id, game_id_length, sizeof(received_game_id)); - break; - case 0x22: // Change Channel command - mc_respond(0x00); - receiveOrNextCmd(&cmd); // Accept command - subcmd = cmd; - mc_respond(0x00); - receiveOrNextCmd(&cmd); - - switch (subcmd) { - case 0x00: - // ps2_cardman_set_channel(cmd); todo: Implement! - break; - case 0x01: - // ps2_cardman_next_channel(); Implement logic! - break; - case 0x02: - // ps2_cardman_prev_channel(); Implement logic! - break; - default: break; - } - mc_respond(0x00); - receiveOrNextCmd(&cmd); - mc_respond(0x00); - receiveOrNextCmd(&cmd); - mc_respond(0xFF); - break; - case 0x23: // Change Slot command - mc_respond(0x00); - receiveOrNextCmd(&cmd); // Accept command - subcmd = cmd; - mc_respond(0x00); - receiveOrNextCmd(&cmd); - - switch (subcmd) { - case 0x00: - // ps2_cardman_set_idx(cmd); todo: Implement! - break; - case 0x01: ps2_cardman_next_idx(); break; - case 0x02: ps2_cardman_prev_idx(); break; - default: break; - } - mc_respond(0x00); - receiveOrNextCmd(&cmd); - mc_respond(0x00); - receiveOrNextCmd(&cmd); - mc_respond(0xFF); - break; - default: break; - } + switch (cmd) + { + case SD2PSXMAN_PING: ps2_sd2psxman_ping(); break; + case SD2PSXMAN_GET_STATUS: ps2_sd2psxman_get_status(); break; + case SD2PSXMAN_GET_CARD: ps2_sd2psxman_get_card(); break; + case SD2PSXMAN_SET_CARD: ps2_sd2psxman_set_card(); break; + case SD2PSXMAN_GET_CHANNEL: ps2_sd2psxman_get_channel(); break; + case SD2PSXMAN_SET_CHANNEL: ps2_sd2psxman_set_channel(); break; + case SD2PSXMAN_GET_GAMEID: ps2_sd2psxman_get_gameid(); break; + case SD2PSXMAN_SET_GAMEID: ps2_sd2psxman_set_gameid(); break; + + default: debug_printf("Unknown Subcommand: %02x\n", cmd); break; } } else { // not for us @@ -435,3 +368,13 @@ void ps2_memory_card_enter_flash(void) { memcard_running = 1; flash_mode = true; } + +void ps2_memory_card_set_reset(void) { + + /*Temp fix: When switching card / channels (via buttons or cmd) it's possible it + * exits from recvfirst() with reset == 0. Upon re-entering mc_main_loop after + * completing the switch it will be stuck waiting for reset == 1, causing + * the next command to be dropped. To avoid this deselect must be invoked by + * issuing a dummy command, reinsterting the card, or reset must be manually set to 1 */ + reset = 1; +} \ No newline at end of file diff --git a/src/ps2/card_emu/ps2_memory_card.h b/src/ps2/card_emu/ps2_memory_card.h index ca00cfb..dc7cc61 100644 --- a/src/ps2/card_emu/ps2_memory_card.h +++ b/src/ps2/card_emu/ps2_memory_card.h @@ -1,8 +1,7 @@ #pragma once - - void ps2_memory_card_main(void); void ps2_memory_card_enter(void); void ps2_memory_card_enter_flash(void); void ps2_memory_card_exit(void); +void ps2_memory_card_set_reset(void); \ No newline at end of file diff --git a/src/ps2/card_emu/ps2_sd2psxman.c b/src/ps2/card_emu/ps2_sd2psxman.c new file mode 100644 index 0000000..5d42ab1 --- /dev/null +++ b/src/ps2/card_emu/ps2_sd2psxman.c @@ -0,0 +1,72 @@ +#include +#include "ps2/ps2_cardman.h" +#include "ps2/card_emu/ps2_memory_card.h" +#include "ps2/card_emu/ps2_sd2psxman.h" +#include "gui.h" + +#include "pico/time.h" +#include "ps2/card_emu/ps2_sd2psxman_commands.h" + +volatile uint8_t sd2psxman_cmd; +volatile uint8_t sd2psxman_mode; +volatile uint16_t sd2psxman_cnum; +char sd2psxman_gameid[251] = {'S', 'L', 'U', 'S', '-', '2', '0', '5', '5', '4', '\0'}; + +void ps2_sd2psxman_task(void) +{ + if (sd2psxman_cmd != 0) { + + uint16_t prev_card = ps2_cardman_get_idx(); + uint8_t prev_chan = ps2_cardman_get_channel(); + + switch (sd2psxman_cmd) { + + case SD2PSXMAN_SET_CARD: + if (sd2psxman_mode == SD2PSXMAN_MODE_NUM) { + ps2_cardman_set_idx(sd2psxman_cnum); + debug_printf("set num idx\n"); + } else if (sd2psxman_mode == SD2PSXMAN_MODE_NEXT) { + ps2_cardman_next_idx(); + debug_printf("set next idx\n"); + } else if (sd2psxman_mode == SD2PSXMAN_MODE_PREV) { + ps2_cardman_prev_idx(); + debug_printf("set prev idx\n"); + } + break; + + case SD2PSXMAN_SET_CHANNEL: + if (sd2psxman_mode == SD2PSXMAN_MODE_NUM) { + ps2_cardman_set_channel(sd2psxman_cnum); + debug_printf("set num channel\n"); + } else if (sd2psxman_mode == SD2PSXMAN_MODE_NEXT) { + ps2_cardman_next_channel(); + debug_printf("set next channel\n"); + } else if (sd2psxman_mode == SD2PSXMAN_MODE_PREV) { + ps2_cardman_prev_channel(); + debug_printf("set prev channel\n"); + } + break; + + case SD2PSXMAN_SET_GAMEID: + //TODO: + break; + + default: + break; + } + + if (prev_card != ps2_cardman_get_idx() || prev_chan != ps2_cardman_get_channel()) { + //close old card + ps2_memory_card_exit(); + ps2_cardman_close(); + + //open new card + ps2_cardman_open(); + ps2_memory_card_set_reset(); //temp fix + ps2_memory_card_enter(); + gui_request_refresh(); + } + + sd2psxman_cmd = 0; + } +} \ No newline at end of file diff --git a/src/ps2/card_emu/ps2_sd2psxman.h b/src/ps2/card_emu/ps2_sd2psxman.h new file mode 100644 index 0000000..6f22058 --- /dev/null +++ b/src/ps2/card_emu/ps2_sd2psxman.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +extern volatile uint8_t sd2psxman_cmd; +extern volatile uint8_t sd2psxman_mode; +extern volatile uint16_t sd2psxman_cnum; +extern char sd2psxman_gameid[251]; + +void ps2_sd2psxman_task(void); \ No newline at end of file diff --git a/src/ps2/card_emu/ps2_sd2psxman_commands.c b/src/ps2/card_emu/ps2_sd2psxman_commands.c new file mode 100644 index 0000000..1942f8b --- /dev/null +++ b/src/ps2/card_emu/ps2_sd2psxman_commands.c @@ -0,0 +1,121 @@ +#include + +#include "ps2_cardman.h" +#include "ps2_dirty.h" +#include "ps2_mc_internal.h" +#include "ps2_pio_qspi.h" + +#include "ps2_sd2psxman.h" +#include "ps2_sd2psxman_commands.h" + +inline __attribute__((always_inline)) void ps2_sd2psxman_ping(void) +{ + uint8_t cmd; + mc_respond(0x0); receiveOrNextCmd(&cmd); //reserved byte + mc_respond(0x1); receiveOrNextCmd(&cmd); //protocol version + mc_respond(0x1); receiveOrNextCmd(&cmd); //product ID + mc_respond(0x1); receiveOrNextCmd(&cmd); //product revision number + mc_respond(term); + debug_printf("received SD2PSXMAN_PING\n"); +} + +inline __attribute__((always_inline)) void ps2_sd2psxman_get_status(void) +{ + uint8_t cmd; + //TODO + debug_printf("received SD2PSXMAN_GET_STATUS\n"); +} + +inline __attribute__((always_inline)) void ps2_sd2psxman_get_card(void) +{ + uint8_t cmd; + int card = ps2_cardman_get_idx(); + mc_respond(0x0); receiveOrNextCmd(&cmd); //reserved byte + mc_respond(card >> 8); receiveOrNextCmd(&cmd); //card upper 8 bits + mc_respond(card & 0xff); receiveOrNextCmd(&cmd); //card lower 8 bits + mc_respond(term); + debug_printf("received SD2PSXMAN_GET_CARD\n"); +} + +inline __attribute__((always_inline)) void ps2_sd2psxman_set_card(void) +{ + uint8_t cmd; + mc_respond(0x0); receiveOrNextCmd(&cmd); //reserved byte + mc_respond(0x0); receiveOrNextCmd(&cmd); //mode + sd2psxman_mode = cmd; + mc_respond(0x0); receiveOrNextCmd(&cmd); //card upper 8 bits + sd2psxman_cnum = cmd << 8; + mc_respond(0x0); receiveOrNextCmd(&cmd); //card lower 8 bits + sd2psxman_cnum |= cmd; + mc_respond(term); + + debug_printf("received SD2PSXMAN_SET_CARD mode: %i, num: %i\n", sd2psxman_mode, sd2psxman_cnum); + + sd2psxman_cmd = SD2PSXMAN_SET_CARD; //set after setting mode and cnum +} + +inline __attribute__((always_inline)) void ps2_sd2psxman_get_channel(void) +{ + uint8_t cmd; + int chan = ps2_cardman_get_channel(); + mc_respond(0x0); receiveOrNextCmd(&cmd); //reserved byte + mc_respond(chan >> 8); receiveOrNextCmd(&cmd); //channel upper 8 bits + mc_respond(chan & 0xff); receiveOrNextCmd(&cmd); //channel lower 8 bits + mc_respond(term); + debug_printf("received SD2PSXMAN_GET_CHANNEL\n"); +} + +inline __attribute__((always_inline)) void ps2_sd2psxman_set_channel(void) +{ + uint8_t cmd; + mc_respond(0x0); receiveOrNextCmd(&cmd); //reserved byte + mc_respond(0x0); receiveOrNextCmd(&cmd); //mode + sd2psxman_mode = cmd; + mc_respond(0x0); receiveOrNextCmd(&cmd); //channel upper 8 bits + sd2psxman_cnum = cmd << 8; + mc_respond(0x0); receiveOrNextCmd(&cmd); //channel lower 8 bits + sd2psxman_cnum |= cmd; + mc_respond(term); + + debug_printf("received SD2PSXMAN_SET_CHANNEL mode: %i, num: %i\n", sd2psxman_mode, sd2psxman_cnum); + + sd2psxman_cmd = SD2PSXMAN_SET_CHANNEL; //set after setting mode and cnum +} + +inline __attribute__((always_inline)) void ps2_sd2psxman_get_gameid(void) +{ + uint8_t cmd; + uint8_t gameid_len = strlen(sd2psxman_gameid) + 1; //+1 null terminator + mc_respond(0x0); receiveOrNextCmd(&cmd); //reserved byte + mc_respond(gameid_len); receiveOrNextCmd(&cmd); //gameid length + + for (int i = 0; i < gameid_len; i++) { + mc_respond(sd2psxman_gameid[i]); receiveOrNextCmd(&cmd); //gameid + } + + for (int i = 0; i < (250 - gameid_len); i++) { + mc_respond(0xff); receiveOrNextCmd(&cmd); //padding + } + + mc_respond(term); + + debug_printf("received SD2PSXMAN_GET_GAMEID\n"); +} + +inline __attribute__((always_inline)) void ps2_sd2psxman_set_gameid(void) +{ + uint8_t cmd; + uint8_t gameid_len; + mc_respond(0x0); receiveOrNextCmd(&cmd); //reserved byte + mc_respond(0x0); receiveOrNextCmd(&cmd); //gameid length + gameid_len = cmd; + + for (int i = 0; i < gameid_len; i++) { + mc_respond(0x0); receiveOrNextCmd(&cmd); //gameid + sd2psxman_gameid[i] = cmd; + } + + mc_respond(term); + + debug_printf("received SD2PSXMAN_SET_GAMEID len %i, id: %s\n", gameid_len, sd2psxman_gameid); +} \ No newline at end of file diff --git a/src/ps2/card_emu/ps2_sd2psxman_commands.h b/src/ps2/card_emu/ps2_sd2psxman_commands.h new file mode 100644 index 0000000..126e712 --- /dev/null +++ b/src/ps2/card_emu/ps2_sd2psxman_commands.h @@ -0,0 +1,25 @@ +#pragma once + +#define PS2_SD2PSXMAN_CMD_IDENTIFIER 0x8B + +#define SD2PSXMAN_PING 0x1 +#define SD2PSXMAN_GET_STATUS 0x2 +#define SD2PSXMAN_GET_CARD 0x3 +#define SD2PSXMAN_SET_CARD 0x4 +#define SD2PSXMAN_GET_CHANNEL 0x5 +#define SD2PSXMAN_SET_CHANNEL 0x6 +#define SD2PSXMAN_GET_GAMEID 0x7 +#define SD2PSXMAN_SET_GAMEID 0x8 + +#define SD2PSXMAN_MODE_NUM 0x0 +#define SD2PSXMAN_MODE_NEXT 0x1 +#define SD2PSXMAN_MODE_PREV 0x2 + +extern void ps2_sd2psxman_ping(void); +extern void ps2_sd2psxman_get_status(void); +extern void ps2_sd2psxman_get_card(void); +extern void ps2_sd2psxman_set_card(void); +extern void ps2_sd2psxman_get_channel(void); +extern void ps2_sd2psxman_set_channel(void); +extern void ps2_sd2psxman_get_gameid(void); +extern void ps2_sd2psxman_set_gameid(void); \ No newline at end of file diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index 1fc3237..edb9269 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -307,6 +307,18 @@ void ps2_cardman_close(void) { fd = -1; } +void ps2_cardman_set_channel(uint16_t chan_num) { + if (card_idx != IDX_BOOT) { + if (chan_num <= CHAN_MAX && chan_num >= CHAN_MIN) { + card_chan = chan_num; + } + } else { + card_idx = settings_get_ps2_card(); + card_chan = settings_get_ps2_channel(); + } + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); +} + void ps2_cardman_next_channel(void) { if (card_idx != IDX_BOOT) { card_chan += 1; @@ -331,6 +343,20 @@ void ps2_cardman_prev_channel(void) { snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } +void ps2_cardman_set_idx(uint16_t idx_num) { + if (card_idx != IDX_BOOT) { + if (idx_num >= IDX_MIN && idx_num <= 0xFFFF) { + card_idx = idx_num; + card_chan = CHAN_MIN; + } + } else { + card_idx = settings_get_ps2_card(); + card_chan = settings_get_ps2_channel(); + } + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); + +} + void ps2_cardman_next_idx(void) { if (card_idx != IDX_BOOT) { card_idx += 1; diff --git a/src/ps2/ps2_cardman.h b/src/ps2/ps2_cardman.h index 27d478f..c028be9 100644 --- a/src/ps2/ps2_cardman.h +++ b/src/ps2/ps2_cardman.h @@ -17,8 +17,11 @@ int ps2_cardman_get_idx(void); int ps2_cardman_get_channel(void); uint32_t ps2_cardman_get_card_size(void); +void ps2_cardman_set_channel(uint16_t num); void ps2_cardman_next_channel(void); void ps2_cardman_prev_channel(void); + +void ps2_cardman_set_idx(uint16_t num); void ps2_cardman_next_idx(void); void ps2_cardman_prev_idx(void); From 205600f1690ae2cf2428e3b78d66981cf812187f Mon Sep 17 00:00:00 2001 From: qnox32 Date: Wed, 8 Nov 2023 20:09:58 -0500 Subject: [PATCH 74/91] added unmount bootcard command fixed gui not refreshing from cmds in ps2mode fixed reset bug --- src/gui.c | 4 +++- src/ps2/card_emu/ps2_memory_card.c | 15 +++------------ src/ps2/card_emu/ps2_memory_card.h | 3 +-- src/ps2/card_emu/ps2_sd2psxman.c | 10 ++++++++-- src/ps2/card_emu/ps2_sd2psxman_commands.c | 11 +++++++++++ src/ps2/card_emu/ps2_sd2psxman_commands.h | 5 ++++- 6 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/gui.c b/src/gui.c index 65799c6..ba1f4fe 100644 --- a/src/gui.c +++ b/src/gui.c @@ -791,7 +791,7 @@ void gui_task(void) { static int displayed_card_channel = -1; static char card_idx_s[8]; static char card_channel_s[8]; - if (displayed_card_idx != ps2_cardman_get_idx() || displayed_card_channel != ps2_cardman_get_channel()) { + if (displayed_card_idx != ps2_cardman_get_idx() || displayed_card_channel != ps2_cardman_get_channel() || refresh_gui) { displayed_card_idx = ps2_cardman_get_idx(); displayed_card_channel = ps2_cardman_get_channel(); folder_name = ps2_cardman_get_folder_name(); @@ -822,6 +822,8 @@ void gui_task(void) { } lv_label_set_text(scr_main_channel_lbl, card_channel_s); + + refresh_gui = false; } if (switching_card && switching_card_timeout < time_us_64() && !input_is_any_down()) { diff --git a/src/ps2/card_emu/ps2_memory_card.c b/src/ps2/card_emu/ps2_memory_card.c index a40d150..0cdc711 100644 --- a/src/ps2/card_emu/ps2_memory_card.c +++ b/src/ps2/card_emu/ps2_memory_card.c @@ -256,7 +256,7 @@ static void __time_critical_func(mc_main_loop)(void) { case SD2PSXMAN_SET_CHANNEL: ps2_sd2psxman_set_channel(); break; case SD2PSXMAN_GET_GAMEID: ps2_sd2psxman_get_gameid(); break; case SD2PSXMAN_SET_GAMEID: ps2_sd2psxman_set_gameid(); break; - + case SD2PSXMAN_UNMOUNT_BOOTCARD: ps2_sd2psxman_unmount_bootcard(); break; default: debug_printf("Unknown Subcommand: %02x\n", cmd); break; } } else { @@ -270,7 +270,8 @@ static void __no_inline_not_in_flash_func(mc_main)(void) { while (1) { while (!mc_enter_request) {} mc_enter_response = 1; - + + reset_pio(); mc_main_loop(); } } @@ -367,14 +368,4 @@ void ps2_memory_card_enter_flash(void) { mc_enter_request = mc_enter_response = 0; memcard_running = 1; flash_mode = true; -} - -void ps2_memory_card_set_reset(void) { - - /*Temp fix: When switching card / channels (via buttons or cmd) it's possible it - * exits from recvfirst() with reset == 0. Upon re-entering mc_main_loop after - * completing the switch it will be stuck waiting for reset == 1, causing - * the next command to be dropped. To avoid this deselect must be invoked by - * issuing a dummy command, reinsterting the card, or reset must be manually set to 1 */ - reset = 1; } \ No newline at end of file diff --git a/src/ps2/card_emu/ps2_memory_card.h b/src/ps2/card_emu/ps2_memory_card.h index dc7cc61..4c857c9 100644 --- a/src/ps2/card_emu/ps2_memory_card.h +++ b/src/ps2/card_emu/ps2_memory_card.h @@ -3,5 +3,4 @@ void ps2_memory_card_main(void); void ps2_memory_card_enter(void); void ps2_memory_card_enter_flash(void); -void ps2_memory_card_exit(void); -void ps2_memory_card_set_reset(void); \ No newline at end of file +void ps2_memory_card_exit(void); \ No newline at end of file diff --git a/src/ps2/card_emu/ps2_sd2psxman.c b/src/ps2/card_emu/ps2_sd2psxman.c index 5d42ab1..c8e3ad9 100644 --- a/src/ps2/card_emu/ps2_sd2psxman.c +++ b/src/ps2/card_emu/ps2_sd2psxman.c @@ -51,10 +51,16 @@ void ps2_sd2psxman_task(void) //TODO: break; + case SD2PSXMAN_UNMOUNT_BOOTCARD: + if (ps2_cardman_get_idx() == 0) { + ps2_cardman_next_idx(); + } + break; + default: break; } - + if (prev_card != ps2_cardman_get_idx() || prev_chan != ps2_cardman_get_channel()) { //close old card ps2_memory_card_exit(); @@ -62,7 +68,7 @@ void ps2_sd2psxman_task(void) //open new card ps2_cardman_open(); - ps2_memory_card_set_reset(); //temp fix + //ps2_memory_card_set_reset(); //temp fix ps2_memory_card_enter(); gui_request_refresh(); } diff --git a/src/ps2/card_emu/ps2_sd2psxman_commands.c b/src/ps2/card_emu/ps2_sd2psxman_commands.c index 1942f8b..1ebeb0e 100644 --- a/src/ps2/card_emu/ps2_sd2psxman_commands.c +++ b/src/ps2/card_emu/ps2_sd2psxman_commands.c @@ -118,4 +118,15 @@ inline __attribute__((always_inline)) void ps2_sd2psxman_set_gameid(void) mc_respond(term); debug_printf("received SD2PSXMAN_SET_GAMEID len %i, id: %s\n", gameid_len, sd2psxman_gameid); +} + +inline __attribute__((always_inline)) void ps2_sd2psxman_unmount_bootcard(void) +{ + uint8_t cmd; + mc_respond(0x0); receiveOrNextCmd(&cmd); //reserved byte + mc_respond(term); + + debug_printf("received SD2PSXMAN_UNMOUNT_BOOTCARD\n"); + + sd2psxman_cmd = SD2PSXMAN_UNMOUNT_BOOTCARD; } \ No newline at end of file diff --git a/src/ps2/card_emu/ps2_sd2psxman_commands.h b/src/ps2/card_emu/ps2_sd2psxman_commands.h index 126e712..35d960f 100644 --- a/src/ps2/card_emu/ps2_sd2psxman_commands.h +++ b/src/ps2/card_emu/ps2_sd2psxman_commands.h @@ -11,6 +11,8 @@ #define SD2PSXMAN_GET_GAMEID 0x7 #define SD2PSXMAN_SET_GAMEID 0x8 +#define SD2PSXMAN_UNMOUNT_BOOTCARD 0x30 + #define SD2PSXMAN_MODE_NUM 0x0 #define SD2PSXMAN_MODE_NEXT 0x1 #define SD2PSXMAN_MODE_PREV 0x2 @@ -22,4 +24,5 @@ extern void ps2_sd2psxman_set_card(void); extern void ps2_sd2psxman_get_channel(void); extern void ps2_sd2psxman_set_channel(void); extern void ps2_sd2psxman_get_gameid(void); -extern void ps2_sd2psxman_set_gameid(void); \ No newline at end of file +extern void ps2_sd2psxman_set_gameid(void); +extern void ps2_sd2psxman_unmount_bootcard(void); \ No newline at end of file From 49df629f382a565cb7acec9d8273fea9a596e81a Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sat, 11 Nov 2023 12:10:56 +0100 Subject: [PATCH 75/91] Minor cleanup --- src/ps2/card_emu/ps2_sd2psxman.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ps2/card_emu/ps2_sd2psxman.c b/src/ps2/card_emu/ps2_sd2psxman.c index c8e3ad9..6af1a6a 100644 --- a/src/ps2/card_emu/ps2_sd2psxman.c +++ b/src/ps2/card_emu/ps2_sd2psxman.c @@ -3,6 +3,7 @@ #include "ps2/card_emu/ps2_memory_card.h" #include "ps2/card_emu/ps2_sd2psxman.h" #include "gui.h" +#include "debug.h" #include "pico/time.h" #include "ps2/card_emu/ps2_sd2psxman_commands.h" @@ -10,7 +11,7 @@ volatile uint8_t sd2psxman_cmd; volatile uint8_t sd2psxman_mode; volatile uint16_t sd2psxman_cnum; -char sd2psxman_gameid[251] = {'S', 'L', 'U', 'S', '-', '2', '0', '5', '5', '4', '\0'}; +char sd2psxman_gameid[251] = { 0x00 }; void ps2_sd2psxman_task(void) { @@ -68,7 +69,6 @@ void ps2_sd2psxman_task(void) //open new card ps2_cardman_open(); - //ps2_memory_card_set_reset(); //temp fix ps2_memory_card_enter(); gui_request_refresh(); } From d322900f25d9cea99fc5f61f34d644c3485fb334 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Mon, 13 Nov 2023 16:39:29 +0100 Subject: [PATCH 76/91] Switch card using GUI --- src/ps2/card_emu/ps2_sd2psxman.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ps2/card_emu/ps2_sd2psxman.c b/src/ps2/card_emu/ps2_sd2psxman.c index 6af1a6a..7f6bf7e 100644 --- a/src/ps2/card_emu/ps2_sd2psxman.c +++ b/src/ps2/card_emu/ps2_sd2psxman.c @@ -68,8 +68,8 @@ void ps2_sd2psxman_task(void) ps2_cardman_close(); //open new card - ps2_cardman_open(); - ps2_memory_card_enter(); + gui_do_ps2_card_switch(); + //ps2_memory_card_enter(); gui_request_refresh(); } From dd6e4f0f2dccbf6558cf5aff379349b640412bfc Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Wed, 6 Dec 2023 17:16:09 +0100 Subject: [PATCH 77/91] Update PS2 game name script --- database/get_and_parse_hdldb.py | 113 ++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 database/get_and_parse_hdldb.py diff --git a/database/get_and_parse_hdldb.py b/database/get_and_parse_hdldb.py new file mode 100644 index 0000000..2920612 --- /dev/null +++ b/database/get_and_parse_hdldb.py @@ -0,0 +1,113 @@ +import requests +import csv +from unidecode import unidecode + +class GameId: + name = "" + id = "" + prefix = "" + parent_id = "" + def __init__(self, name, id, parent_id=None): + self.name = unidecode(name) + separator = "_" + if "-" in id: + separator = "-" + self.id = id.split(separator)[1].replace(".", "") + self.prefix = id.split(separator)[0] + if parent_id: + self.parent_id = parent_id.split(separator)[1] + else: + self.parent_id = self.id + def __str__(self): + return "Prefix " + self.prefix + " Id " + self.id + " Name " + self.name + " Parent " + self.parent_id + + def __lt__(self, o): + return self.name < o.name + + +def writeSortedGameList(outfile, prefixes, games_count, games_sorted, gamenames): + term = 0 + # Calculate general offsets + game_ids_offset = (len(prefixes) + 1) * 8 + game_names_base_offset = game_ids_offset + (games_count * 12) + (len(prefixes) * 12) + prefix_offset = game_ids_offset + + offset = game_names_base_offset + game_name_to_offset = {} + # Calculate offset for each game name + for gamename in gamenames: + game_name_to_offset[gamename] = offset + offset = offset + len(gamename) + 1 + # First: write prefix Indices in the format + # 4 Byte: Index Chars, padded with ws in the end + # 4 Byte: Index Offset within dat + for prefix in games_sorted: + adjustedPrefix = prefix + if len(prefix) < 4: + adjustedPrefix = prefix + (4 - len(prefix) ) * " " + outfile.write(adjustedPrefix.encode('ascii')) + outfile.write(prefix_offset.to_bytes(4, 'big')) + prefix_offset = prefix_offset + (len(games_sorted[prefix]) + 1) * 12 + outfile.write(term.to_bytes(8, 'big')) + # Next: write game entries for each index in the format: + # 4 Byte: Game ID without prefix, Big Endian + # 4 Byte: Offset to game name, Big Endian + # 4 Byte: Parent Game ID - if multi disc this is equal to Game ID + for prefix in games_sorted: + for game in games_sorted[prefix]: + #print(game) + outfile.write(int(game.id).to_bytes(4, 'big')) + outfile.write(game_name_to_offset[game.name].to_bytes(4, 'big')) + outfile.write(int(game.parent_id).to_bytes(4, 'big')) + outfile.write(term.to_bytes(12, 'big')) + # Last: write null terminated game names + for game in game_name_to_offset: + outfile.write(game.encode('ascii')) + outfile.write(term.to_bytes(1, 'big')) + +def getGamesHDLBatchInstaller() -> ([], [], {}, int): + prefixes = [] + gamenames = [] + games_sorted = {} + games_count = 0 + + url = "https://github.com/israpps/HDL-Batch-installer/raw/main/Database/gamename.csv" + + r = requests.get(url, allow_redirects=True) + + if r.status_code == 200: + lines = r.text.split("\n") + csv_reader = csv.reader(lines, delimiter=";") + for row in csv_reader: + if len(row) == 2: + id = row[0] + title = row[1] + game = GameId(row[1], row[0]) + try: + if int(game.id) > 0: + # Create Prefix list and game name list + # Create dict that contains all games sorted by prefix + if game.prefix not in prefixes: + prefixes.append(game.prefix) + if game.name not in gamenames: + gamenames.append(game.name) + if not game.prefix in games_sorted: + games_sorted[game.prefix] = [] + games_sorted[game.prefix].append(game) + games_count += 1 + except ValueError: + print(f"{game} not parsed") + continue + return (prefixes, gamenames, games_sorted, games_count) + + +prefixes = [] +gamenames = [] +games_sorted = {} +games_count = 0 + + +with open("gamedbps2.dat", "wb") as out: + (prefixes, gamenames, games_sorted, games_count) = getGamesHDLBatchInstaller() + writeSortedGameList(out, prefixes, games_count, games_sorted, gamenames) + From 8e82f06f35b309941a2bdeaab803dbdf63268ad6 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sat, 30 Dec 2023 12:00:53 +0100 Subject: [PATCH 78/91] Increase space for App in linker script --- memmap_custom.ld | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/memmap_custom.ld b/memmap_custom.ld index 8eb3eda..e46168f 100644 --- a/memmap_custom.ld +++ b/memmap_custom.ld @@ -23,7 +23,7 @@ MEMORY { - FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048k + FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 7168k RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 256k SCRATCH_X(rwx) : ORIGIN = 0x20040000, LENGTH = 4k SCRATCH_Y(rwx) : ORIGIN = 0x20041000, LENGTH = 4k From 8b4bf380ac237c6514742ef30802e26e6b18abe9 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sat, 30 Dec 2023 12:01:35 +0100 Subject: [PATCH 79/91] Build PS2 game database binary file --- database/CMakeLists.txt | 23 +++++++++++++++++++++-- database/db_obj_builder.cmake | 20 ++++++++++++++++++-- database/get_and_parse_hdldb.py | 1 + 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/database/CMakeLists.txt b/database/CMakeLists.txt index f8c5cdf..b31d666 100644 --- a/database/CMakeLists.txt +++ b/database/CMakeLists.txt @@ -1,6 +1,7 @@ +# PS1 set(GAMEDB_PS1_OBJ "${CMAKE_CURRENT_BINARY_DIR}/gamedbps1.o") -add_custom_target(gamedbobjs ALL +add_custom_target(gamedbobjs_ps1 ALL COMMAND ${CMAKE_COMMAND} -D OUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR} -D PYTHON_SCRIPT=${CMAKE_CURRENT_SOURCE_DIR}/parse_db.py @@ -11,6 +12,24 @@ add_custom_target(gamedbobjs ALL BYPRODUCTS ${GAMEDB_PS1_OBJ}) add_library(gamedb INTERFACE) -add_dependencies(gamedb gamedbobjs) +add_dependencies(gamedb gamedbobjs_ps1) target_link_libraries(gamedb INTERFACE ${GAMEDB_PS1_OBJ}) + +# PS2 + +set(GAMEDB_PS2_OBJ "${CMAKE_CURRENT_BINARY_DIR}/gamedbps2.o") + +add_custom_target(gamedbobjs_ps2 ALL + COMMAND ${CMAKE_COMMAND} + -D OUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR} + -D PYTHON_SCRIPT=${CMAKE_CURRENT_SOURCE_DIR}/get_and_parse_hdldb.py + -D CMAKE_OBJCOPY=${CMAKE_OBJCOPY} + -D SYSTEM=ps2 + -P ${CMAKE_CURRENT_SOURCE_DIR}/db_obj_builder.cmake + VERBATIM + BYPRODUCTS ${GAMEDB_PS1_OBJ}) + +add_dependencies(gamedb gamedbobjs_ps2) + +target_link_libraries(gamedb INTERFACE ${GAMEDB_PS2_OBJ}) diff --git a/database/db_obj_builder.cmake b/database/db_obj_builder.cmake index 7f3194d..773eddc 100644 --- a/database/db_obj_builder.cmake +++ b/database/db_obj_builder.cmake @@ -2,7 +2,18 @@ string(TIMESTAMP date "%Y%m%d") if(NOT EXISTS "${OUTPUT_DIR}/gamedb${SYSTEM}_${date}.o") -find_package (Python COMPONENTS Interpreter) +find_package (Python3 COMPONENTS Interpreter) +execute_process (COMMAND "${Python3_EXECUTABLE}" -m venv "${OUTPUT_DIR}/db_builder") + +# Here is the trick +## update the environment with VIRTUAL_ENV variable (mimic the activate script) +set (ENV{VIRTUAL_ENV} "${OUTPUT_DIR}/db_builder") +## change the context of the search +set (Python3_FIND_VIRTUALENV FIRST) +## unset Python3_EXECUTABLE because it is also an input variable (see documentation, Artifacts Specification section) +unset (Python3_EXECUTABLE) +## Launch a new search +find_package (Python3 COMPONENTS Interpreter Development) file(GLOB files "${OUTPUT_DIR}/gamedb${SYSTEM}_*") foreach(file ${files}) @@ -14,7 +25,12 @@ set(GAMEDB_${SYSTEM}_BIN "gamedb${SYSTEM}.dat") set(GAMEDB_${SYSTEM}_OBJ "${OUTPUT_DIR}/gamedb${SYSTEM}_${date}.o") execute_process( - COMMAND ${Python_EXECUTABLE} ${PYTHON_SCRIPT} ${SYSTEM} ${OUTPUT_DIR} + COMMAND ${Python3_EXECUTABLE} -m pip install requests unidecode + WORKING_DIRECTORY ${OUTPUT_DIR} + OUTPUT_QUIET +) +execute_process( + COMMAND ${Python3_EXECUTABLE} ${PYTHON_SCRIPT} ${SYSTEM} ${OUTPUT_DIR} WORKING_DIRECTORY ${OUTPUT_DIR} OUTPUT_QUIET ) diff --git a/database/get_and_parse_hdldb.py b/database/get_and_parse_hdldb.py index 2920612..665b552 100644 --- a/database/get_and_parse_hdldb.py +++ b/database/get_and_parse_hdldb.py @@ -75,6 +75,7 @@ def getGamesHDLBatchInstaller() -> ([], [], {}, int): r = requests.get(url, allow_redirects=True) + if r.status_code == 200: lines = r.text.split("\n") csv_reader = csv.reader(lines, delimiter=";") From 0f00ae52ef8e9e1bbeef0643bce85fc2dc4a7975 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sat, 30 Dec 2023 12:02:33 +0100 Subject: [PATCH 80/91] Add PS2 game names --- src/game_names/game_names.c | 13 +++++++------ src/game_names/game_names.h | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/game_names/game_names.c b/src/game_names/game_names.c index e88e97d..745c6ff 100644 --- a/src/game_names/game_names.c +++ b/src/game_names/game_names.c @@ -24,7 +24,7 @@ #define MAX_PATH_LENGTH (64) extern const char _binary_gamedbps1_dat_start, _binary_gamedbps1_dat_size; -// extern const char _binary_gamedbps2_dat_start, _binary_gamedbps2_dat_size; +extern const char _binary_gamedbps2_dat_start, _binary_gamedbps2_dat_size; typedef struct { size_t offset; @@ -33,7 +33,7 @@ typedef struct { const char* name; } game_lookup; -bool game_names_sanity_check_title_id(const char* const title_id) { +bool __time_critical_func(game_names_sanity_check_title_id)(const char* const title_id) { uint8_t i = 0U; char splittable_game_id[MAX_GAME_ID_LENGTH]; @@ -131,11 +131,11 @@ static game_lookup find_game_lookup(const char* game_id) { char idString[10] = {}; uint32_t numeric_id = 0, numeric_prefix = 0; - // const char* const db_start = settings_get_mode() == MODE_PS1 ? &_binary_gamedbps1_dat_start : &_binary_gamedbps2_dat_start; - // const char* const db_size = settings_get_mode() == MODE_PS1 ? &_binary_gamedbps1_dat_size : &_binary_gamedbps2_dat_size; + const char* const db_start = settings_get_mode() == MODE_PS1 ? &_binary_gamedbps1_dat_start : &_binary_gamedbps2_dat_start; + const char* const db_size = settings_get_mode() == MODE_PS1 ? &_binary_gamedbps1_dat_size : &_binary_gamedbps2_dat_size; - const char* const db_start = &_binary_gamedbps1_dat_start; - const size_t db_size = (size_t)&_binary_gamedbps1_dat_size; + //const char* const db_start = &_binary_gamedbps1_dat_start; + //const size_t db_size = (size_t)&_binary_gamedbps1_dat_size; uint32_t prefixOffset = 0; game_lookup ret = { @@ -212,6 +212,7 @@ void __time_critical_func(game_names_extract_title_id)(const uint8_t* const in_t } void game_names_get_name_by_folder(const char* const folder, char* const game_name) { + strlcpy(game_name, "", MAX_GAME_NAME_LENGTH); if (game_names_sanity_check_title_id(folder)) { game_lookup game; diff --git a/src/game_names/game_names.h b/src/game_names/game_names.h index 4d05a38..a1dedfb 100644 --- a/src/game_names/game_names.h +++ b/src/game_names/game_names.h @@ -5,7 +5,7 @@ #include void game_names_extract_title_id(const uint8_t* const in_title_id, char* const out_title_id, const size_t in_title_id_length, const size_t out_buffer_size); -bool __time_critical_func(game_names_sanity_check_title_id)(const char* const title_id); +bool game_names_sanity_check_title_id(const char* const title_id); void game_names_get_name_by_folder(const char* const folder, char* const game_name); void game_names_get_parent(const char* const game_id, char* const parent_id); From e4cdfe37b18bc096eb7f4d91ce035a5c72b86454 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sat, 30 Dec 2023 12:03:13 +0100 Subject: [PATCH 81/91] Rename all functions --- src/ps2/card_emu/ps2_sd2psxman_commands.c | 36 +++++++++++++++-------- src/ps2/card_emu/ps2_sd2psxman_commands.h | 18 ++++++------ 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/ps2/card_emu/ps2_sd2psxman_commands.c b/src/ps2/card_emu/ps2_sd2psxman_commands.c index 1ebeb0e..03ca2f1 100644 --- a/src/ps2/card_emu/ps2_sd2psxman_commands.c +++ b/src/ps2/card_emu/ps2_sd2psxman_commands.c @@ -8,7 +8,11 @@ #include "ps2_sd2psxman.h" #include "ps2_sd2psxman_commands.h" -inline __attribute__((always_inline)) void ps2_sd2psxman_ping(void) +#include "game_names/game_names.h" + +#include "debug.h" + +inline __attribute__((always_inline)) void ps2_sd2psxman_cmds_ping(void) { uint8_t cmd; mc_respond(0x0); receiveOrNextCmd(&cmd); //reserved byte @@ -19,14 +23,14 @@ inline __attribute__((always_inline)) void ps2_sd2psxman_ping(void) debug_printf("received SD2PSXMAN_PING\n"); } -inline __attribute__((always_inline)) void ps2_sd2psxman_get_status(void) +inline __attribute__((always_inline)) void ps2_sd2psxman_cmds_get_status(void) { uint8_t cmd; //TODO debug_printf("received SD2PSXMAN_GET_STATUS\n"); } -inline __attribute__((always_inline)) void ps2_sd2psxman_get_card(void) +inline __attribute__((always_inline)) void ps2_sd2psxman_cmds_get_card(void) { uint8_t cmd; int card = ps2_cardman_get_idx(); @@ -37,7 +41,7 @@ inline __attribute__((always_inline)) void ps2_sd2psxman_get_card(void) debug_printf("received SD2PSXMAN_GET_CARD\n"); } -inline __attribute__((always_inline)) void ps2_sd2psxman_set_card(void) +inline __attribute__((always_inline)) void ps2_sd2psxman_cmds_set_card(void) { uint8_t cmd; mc_respond(0x0); receiveOrNextCmd(&cmd); //reserved byte @@ -54,7 +58,7 @@ inline __attribute__((always_inline)) void ps2_sd2psxman_set_card(void) sd2psxman_cmd = SD2PSXMAN_SET_CARD; //set after setting mode and cnum } -inline __attribute__((always_inline)) void ps2_sd2psxman_get_channel(void) +inline __attribute__((always_inline)) void ps2_sd2psxman_cmds_get_channel(void) { uint8_t cmd; int chan = ps2_cardman_get_channel(); @@ -65,7 +69,7 @@ inline __attribute__((always_inline)) void ps2_sd2psxman_get_channel(void) debug_printf("received SD2PSXMAN_GET_CHANNEL\n"); } -inline __attribute__((always_inline)) void ps2_sd2psxman_set_channel(void) +inline __attribute__((always_inline)) void ps2_sd2psxman_cmds_set_channel(void) { uint8_t cmd; mc_respond(0x0); receiveOrNextCmd(&cmd); //reserved byte @@ -82,7 +86,7 @@ inline __attribute__((always_inline)) void ps2_sd2psxman_set_channel(void) sd2psxman_cmd = SD2PSXMAN_SET_CHANNEL; //set after setting mode and cnum } -inline __attribute__((always_inline)) void ps2_sd2psxman_get_gameid(void) +inline __attribute__((always_inline)) void ps2_sd2psxman_cmds_get_gameid(void) { uint8_t cmd; uint8_t gameid_len = strlen(sd2psxman_gameid) + 1; //+1 null terminator @@ -94,7 +98,7 @@ inline __attribute__((always_inline)) void ps2_sd2psxman_get_gameid(void) } for (int i = 0; i < (250 - gameid_len); i++) { - mc_respond(0xff); receiveOrNextCmd(&cmd); //padding + mc_respond(0x0); receiveOrNextCmd(&cmd); //padding } mc_respond(term); @@ -102,25 +106,33 @@ inline __attribute__((always_inline)) void ps2_sd2psxman_get_gameid(void) debug_printf("received SD2PSXMAN_GET_GAMEID\n"); } -inline __attribute__((always_inline)) void ps2_sd2psxman_set_gameid(void) +inline __attribute__((always_inline)) void ps2_sd2psxman_cmds_set_gameid(void) { uint8_t cmd; uint8_t gameid_len; + uint8_t received_id[252] = { 0 }; + char sanitized_game_id[11] = { 0 }; mc_respond(0x0); receiveOrNextCmd(&cmd); //reserved byte mc_respond(0x0); receiveOrNextCmd(&cmd); //gameid length gameid_len = cmd; for (int i = 0; i < gameid_len; i++) { mc_respond(0x0); receiveOrNextCmd(&cmd); //gameid - sd2psxman_gameid[i] = cmd; + received_id[i] = cmd; } mc_respond(term); - debug_printf("received SD2PSXMAN_SET_GAMEID len %i, id: %s\n", gameid_len, sd2psxman_gameid); + game_names_extract_title_id(received_id, sanitized_game_id, gameid_len, sizeof(sanitized_game_id)); + if (game_names_sanity_check_title_id(sanitized_game_id)) { + ps2_sd2psxman_set_gameid(sanitized_game_id); + sd2psxman_cmd = SD2PSXMAN_SET_GAMEID; + } + + debug_printf("received SD2PSXMAN_SET_GAMEID len %i, id: %s\n", gameid_len, sanitized_game_id); } -inline __attribute__((always_inline)) void ps2_sd2psxman_unmount_bootcard(void) +inline __attribute__((always_inline)) void ps2_sd2psxman_cmds_unmount_bootcard(void) { uint8_t cmd; mc_respond(0x0); receiveOrNextCmd(&cmd); //reserved byte diff --git a/src/ps2/card_emu/ps2_sd2psxman_commands.h b/src/ps2/card_emu/ps2_sd2psxman_commands.h index 35d960f..3c995d5 100644 --- a/src/ps2/card_emu/ps2_sd2psxman_commands.h +++ b/src/ps2/card_emu/ps2_sd2psxman_commands.h @@ -17,12 +17,12 @@ #define SD2PSXMAN_MODE_NEXT 0x1 #define SD2PSXMAN_MODE_PREV 0x2 -extern void ps2_sd2psxman_ping(void); -extern void ps2_sd2psxman_get_status(void); -extern void ps2_sd2psxman_get_card(void); -extern void ps2_sd2psxman_set_card(void); -extern void ps2_sd2psxman_get_channel(void); -extern void ps2_sd2psxman_set_channel(void); -extern void ps2_sd2psxman_get_gameid(void); -extern void ps2_sd2psxman_set_gameid(void); -extern void ps2_sd2psxman_unmount_bootcard(void); \ No newline at end of file +extern void ps2_sd2psxman_cmds_ping(void); +extern void ps2_sd2psxman_cmds_get_status(void); +extern void ps2_sd2psxman_cmds_get_card(void); +extern void ps2_sd2psxman_cmds_set_card(void); +extern void ps2_sd2psxman_cmds_get_channel(void); +extern void ps2_sd2psxman_cmds_set_channel(void); +extern void ps2_sd2psxman_cmds_get_gameid(void); +extern void ps2_sd2psxman_cmds_set_gameid(void); +extern void ps2_sd2psxman_cmds_unmount_bootcard(void); \ No newline at end of file From f5442ebe0e6f9a5eb8f51510c6896909a1730b8f Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sat, 30 Dec 2023 12:03:29 +0100 Subject: [PATCH 82/91] Implement set game ID --- src/ps2/card_emu/ps2_sd2psxman.c | 50 ++++++++++++++++++-------------- src/ps2/card_emu/ps2_sd2psxman.h | 4 ++- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/ps2/card_emu/ps2_sd2psxman.c b/src/ps2/card_emu/ps2_sd2psxman.c index 7f6bf7e..9496374 100644 --- a/src/ps2/card_emu/ps2_sd2psxman.c +++ b/src/ps2/card_emu/ps2_sd2psxman.c @@ -1,27 +1,27 @@ -#include -#include "ps2/ps2_cardman.h" -#include "ps2/card_emu/ps2_memory_card.h" #include "ps2/card_emu/ps2_sd2psxman.h" -#include "gui.h" -#include "debug.h" +#include +#include + +#include "debug.h" +#include "gui.h" #include "pico/time.h" +#include "ps2/card_emu/ps2_memory_card.h" #include "ps2/card_emu/ps2_sd2psxman_commands.h" +#include "ps2/ps2_cardman.h" volatile uint8_t sd2psxman_cmd; volatile uint8_t sd2psxman_mode; volatile uint16_t sd2psxman_cnum; -char sd2psxman_gameid[251] = { 0x00 }; +char sd2psxman_gameid[251] = {0x00}; -void ps2_sd2psxman_task(void) -{ +void ps2_sd2psxman_task(void) { if (sd2psxman_cmd != 0) { - uint16_t prev_card = ps2_cardman_get_idx(); uint8_t prev_chan = ps2_cardman_get_channel(); - + ps2_cardman_state_t prev_state = ps2_cardman_get_state(); + switch (sd2psxman_cmd) { - case SD2PSXMAN_SET_CARD: if (sd2psxman_mode == SD2PSXMAN_MODE_NUM) { ps2_cardman_set_idx(sd2psxman_cnum); @@ -47,32 +47,40 @@ void ps2_sd2psxman_task(void) debug_printf("set prev channel\n"); } break; - - case SD2PSXMAN_SET_GAMEID: - //TODO: - break; + case SD2PSXMAN_SET_GAMEID: + ps2_cardman_set_gameid(sd2psxman_gameid); + debug_printf("set next channel\n"); + break; case SD2PSXMAN_UNMOUNT_BOOTCARD: if (ps2_cardman_get_idx() == 0) { ps2_cardman_next_idx(); } break; - default: - break; + default: break; } - if (prev_card != ps2_cardman_get_idx() || prev_chan != ps2_cardman_get_channel()) { - //close old card + if (prev_card != ps2_cardman_get_idx() || prev_chan != ps2_cardman_get_channel() || (prev_state != ps2_cardman_get_state()) || + (SD2PSXMAN_SET_GAMEID == sd2psxman_cmd)) { + // close old card ps2_memory_card_exit(); ps2_cardman_close(); - //open new card + // open new card gui_do_ps2_card_switch(); - //ps2_memory_card_enter(); + // ps2_memory_card_enter(); gui_request_refresh(); } sd2psxman_cmd = 0; } +} + +void __time_critical_func(ps2_sd2psxman_set_gameid)(const char* const game_id) { + snprintf(sd2psxman_gameid, sizeof(sd2psxman_gameid), "%s", game_id); +} + +const char* ps2_sd2psxman_get_gameid(void) { + return sd2psxman_gameid; } \ No newline at end of file diff --git a/src/ps2/card_emu/ps2_sd2psxman.h b/src/ps2/card_emu/ps2_sd2psxman.h index 6f22058..a960097 100644 --- a/src/ps2/card_emu/ps2_sd2psxman.h +++ b/src/ps2/card_emu/ps2_sd2psxman.h @@ -7,4 +7,6 @@ extern volatile uint8_t sd2psxman_mode; extern volatile uint16_t sd2psxman_cnum; extern char sd2psxman_gameid[251]; -void ps2_sd2psxman_task(void); \ No newline at end of file +void ps2_sd2psxman_task(void); +void ps2_sd2psxman_set_gameid(const char* const game_id); +const char* ps2_sd2psxman_get_gameid(void); \ No newline at end of file From b30cb8dc37cdd716d186d928fff250862692e5dd Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sat, 30 Dec 2023 12:03:49 +0100 Subject: [PATCH 83/91] Use renamed functions, add game id variable --- src/ps2/card_emu/ps2_memory_card.c | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/ps2/card_emu/ps2_memory_card.c b/src/ps2/card_emu/ps2_memory_card.c index 0cdc711..0076758 100644 --- a/src/ps2/card_emu/ps2_memory_card.c +++ b/src/ps2/card_emu/ps2_memory_card.c @@ -23,6 +23,7 @@ #include #include #include +#include #include "ps2_sd2psxman_commands.h" @@ -34,8 +35,6 @@ volatile int reset; bool flash_mode = false; -static char received_game_id[0x10] = {0}; - typedef struct { uint32_t offset; uint32_t sm; @@ -198,11 +197,11 @@ static void __time_critical_func(mc_main_loop)(void) { } reset = 0; - // recvfirst(); uint8_t received = receiveFirst(&cmd); if (received == RECEIVE_EXIT) { mc_exit_response = 1; + printf("Received EXIT\n"); break; } if (received == RECEIVE_RESET) @@ -248,15 +247,15 @@ static void __time_critical_func(mc_main_loop)(void) { switch (cmd) { - case SD2PSXMAN_PING: ps2_sd2psxman_ping(); break; - case SD2PSXMAN_GET_STATUS: ps2_sd2psxman_get_status(); break; - case SD2PSXMAN_GET_CARD: ps2_sd2psxman_get_card(); break; - case SD2PSXMAN_SET_CARD: ps2_sd2psxman_set_card(); break; - case SD2PSXMAN_GET_CHANNEL: ps2_sd2psxman_get_channel(); break; - case SD2PSXMAN_SET_CHANNEL: ps2_sd2psxman_set_channel(); break; - case SD2PSXMAN_GET_GAMEID: ps2_sd2psxman_get_gameid(); break; - case SD2PSXMAN_SET_GAMEID: ps2_sd2psxman_set_gameid(); break; - case SD2PSXMAN_UNMOUNT_BOOTCARD: ps2_sd2psxman_unmount_bootcard(); break; + case SD2PSXMAN_PING: ps2_sd2psxman_cmds_ping(); break; + case SD2PSXMAN_GET_STATUS: ps2_sd2psxman_cmds_get_status(); break; + case SD2PSXMAN_GET_CARD: ps2_sd2psxman_cmds_get_card(); break; + case SD2PSXMAN_SET_CARD: ps2_sd2psxman_cmds_set_card(); break; + case SD2PSXMAN_GET_CHANNEL: ps2_sd2psxman_cmds_get_channel(); break; + case SD2PSXMAN_SET_CHANNEL: ps2_sd2psxman_cmds_set_channel(); break; + case SD2PSXMAN_GET_GAMEID: ps2_sd2psxman_cmds_get_gameid(); break; + case SD2PSXMAN_SET_GAMEID: ps2_sd2psxman_cmds_set_gameid(); break; + case SD2PSXMAN_UNMOUNT_BOOTCARD: ps2_sd2psxman_cmds_unmount_bootcard(); break; default: debug_printf("Unknown Subcommand: %02x\n", cmd); break; } } else { From e52cd5fbdbaf983db7ce96f2c37b112913d8199c Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sat, 30 Dec 2023 12:04:25 +0100 Subject: [PATCH 84/91] Implement Game ID output and states --- src/gui.c | 125 +++++++--------- src/ps2/ps2_cardman.c | 335 ++++++++++++++++++++++-------------------- src/ps2/ps2_cardman.h | 11 +- 3 files changed, 233 insertions(+), 238 deletions(-) diff --git a/src/gui.c b/src/gui.c index ba1f4fe..38c29bd 100644 --- a/src/gui.c +++ b/src/gui.c @@ -5,32 +5,28 @@ #include #include "config.h" -#include "lvgl.h" #include "input.h" -#include "ui_menu.h" #include "keystore.h" -#include "settings.h" +#include "lvgl.h" #include "oled.h" - #include "ps1/ps1_cardman.h" -#include "ps1/ps1_odeman.h" #include "ps1/ps1_memory_card.h" - +#include "ps1/ps1_odeman.h" #include "ps2/card_emu/ps2_memory_card.h" #include "ps2/ps2_cardman.h" #include "ps2/ps2_dirty.h" #include "ps2/ps2_exploit.h" - -#include "version/version.h" - +#include "settings.h" +#include "ui_menu.h" #include "ui_theme_mono.h" +#include "version/version.h" /* Displays the line at the bottom for long pressing buttons */ static lv_obj_t *g_navbar, *g_progress_bar, *g_progress_text, *g_activity_frame; static lv_obj_t *scr_switch_nag, *scr_card_switch, *scr_main, *scr_menu, *scr_freepsxboot, *menu, *main_page; static lv_style_t style_inv; -static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl,*src_main_title_lbl, *lbl_civ_err, *lbl_autoboot, *lbl_channel; +static lv_obj_t *scr_main_idx_lbl, *scr_main_channel_lbl, *src_main_title_lbl, *lbl_civ_err, *lbl_autoboot, *lbl_channel; static int have_oled; static int switching_card; @@ -39,25 +35,25 @@ static int terminated; static bool refresh_gui; static bool installing_exploit; -#define COLOR_FG lv_color_white() -#define COLOR_BG lv_color_black() +#define COLOR_FG lv_color_white() +#define COLOR_BG lv_color_black() -static lv_obj_t* ui_scr_create(void) { - lv_obj_t * obj = lv_obj_create(NULL); +static lv_obj_t *ui_scr_create(void) { + lv_obj_t *obj = lv_obj_create(NULL); lv_obj_add_flag(obj, LV_OBJ_FLAG_EVENT_BUBBLE); lv_group_add_obj(lv_group_get_default(), obj); return obj; } /* create a navigatable UI menu container, so that the item (label) inside can be selected and clicked */ -static lv_obj_t* ui_menu_cont_create_nav(lv_obj_t *parent) { - lv_obj_t* cont = ui_menu_cont_create(parent); +static lv_obj_t *ui_menu_cont_create_nav(lv_obj_t *parent) { + lv_obj_t *cont = ui_menu_cont_create(parent); lv_obj_add_flag(cont, LV_OBJ_FLAG_EVENT_BUBBLE); lv_group_add_obj(lv_group_get_default(), cont); return cont; } -static lv_obj_t* ui_menu_subpage_create(lv_obj_t *menu, const char* title) { +static lv_obj_t *ui_menu_subpage_create(lv_obj_t *menu, const char *title) { lv_obj_t *page = ui_menu_page_create(menu, title); lv_obj_add_flag(page, LV_OBJ_FLAG_EVENT_BUBBLE); lv_group_add_obj(lv_group_get_default(), page); @@ -65,7 +61,7 @@ static lv_obj_t* ui_menu_subpage_create(lv_obj_t *menu, const char* title) { return page; } -static lv_obj_t* ui_label_create(lv_obj_t *parent, const char *text) { +static lv_obj_t *ui_label_create(lv_obj_t *parent, const char *text) { lv_obj_t *label = lv_label_create(parent); lv_label_set_text(label, text); return label; @@ -78,7 +74,7 @@ static lv_obj_t *ui_label_create_at(lv_obj_t *parent, int x, int y, const char * return label; } -static lv_obj_t* ui_label_create_grow(lv_obj_t *parent, const char *text) { +static lv_obj_t *ui_label_create_grow(lv_obj_t *parent, const char *text) { lv_obj_t *label = ui_label_create(parent, text); lv_obj_set_flex_grow(label, 1); return label; @@ -97,13 +93,13 @@ static void ui_make_scrollable(lv_obj_t *cont, lv_obj_t *label) { lv_obj_add_event_cb(cont, scrollable_label, LV_EVENT_DEFOCUSED, label); } -static lv_obj_t* ui_label_create_grow_scroll(lv_obj_t *parent, const char *text) { +static lv_obj_t *ui_label_create_grow_scroll(lv_obj_t *parent, const char *text) { lv_obj_t *label = ui_label_create_grow(parent, text); ui_make_scrollable(parent, label); return label; } -static lv_obj_t* ui_header_create(lv_obj_t *parent, const char *text) { +static lv_obj_t *ui_header_create(lv_obj_t *parent, const char *text) { lv_obj_t *lbl = lv_label_create(parent); lv_obj_set_align(lbl, LV_ALIGN_TOP_MID); lv_obj_add_style(lbl, &style_inv, 0); @@ -117,8 +113,8 @@ static void flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t if (have_oled) { oled_clear(); - for(int y = area->y1; y <= area->y2; y++) { - for(int x = area->x1; x <= area->x2; x++) { + for (int y = area->y1; y <= area->y2; y++) { + for (int x = area->x1; x <= area->x2; x++) { if (color_p->full) oled_draw_pixel(x, y); color_p++; @@ -160,7 +156,7 @@ static void create_nav(void) { g_activity_frame = lv_line_create(lv_layer_top()); lv_obj_add_style(g_activity_frame, &style_frame, 0); - static lv_point_t line_points[5] = { {0,0}, {DISPLAY_WIDTH,0}, {DISPLAY_WIDTH,DISPLAY_HEIGHT}, {0,DISPLAY_HEIGHT}, {0,0} }; + static lv_point_t line_points[5] = {{0, 0}, {DISPLAY_WIDTH, 0}, {DISPLAY_WIDTH, DISPLAY_HEIGHT}, {0, DISPLAY_HEIGHT}, {0, 0}}; lv_line_set_points(g_activity_frame, line_points, 5); lv_obj_add_flag(g_activity_frame, LV_OBJ_FLAG_HIDDEN); @@ -181,10 +177,10 @@ static void gui_tick(void) { } static void reload_card_cb(int progress) { - static lv_point_t line_points[2] = { {0, DISPLAY_HEIGHT/2}, {0, DISPLAY_HEIGHT/2} }; + static lv_point_t line_points[2] = {{0, DISPLAY_HEIGHT / 2}, {0, DISPLAY_HEIGHT / 2}}; static int prev_progress; progress += 5; - if (progress/5 == prev_progress/5) + if (progress / 5 == prev_progress / 5) return; prev_progress = progress; line_points[1].x = DISPLAY_WIDTH * progress / 100; @@ -216,20 +212,12 @@ static void evt_scr_main(lv_event_t *event) { if (settings_get_mode() == MODE_PS1) { prevChannel = ps1_cardman_get_channel(); prevIdx = ps1_cardman_get_idx(); - + switch (key) { - case INPUT_KEY_PREV: - ps1_cardman_prev_channel(); - break; - case INPUT_KEY_NEXT: - ps1_cardman_next_channel(); - break; - case INPUT_KEY_BACK: - ps1_cardman_prev_idx(); - break; - case INPUT_KEY_ENTER: - ps1_cardman_next_idx(); - break; + case INPUT_KEY_PREV: ps1_cardman_prev_channel(); break; + case INPUT_KEY_NEXT: ps1_cardman_next_channel(); break; + case INPUT_KEY_BACK: ps1_cardman_prev_idx(); break; + case INPUT_KEY_ENTER: ps1_cardman_next_idx(); break; } if ((prevChannel != ps1_cardman_get_channel()) || (prevIdx != ps1_cardman_get_idx())) { ps1_memory_card_exit(); @@ -242,18 +230,10 @@ static void evt_scr_main(lv_event_t *event) { prevIdx = ps2_cardman_get_idx(); switch (key) { - case INPUT_KEY_PREV: - ps2_cardman_prev_channel(); - break; - case INPUT_KEY_NEXT: - ps2_cardman_next_channel(); - break; - case INPUT_KEY_BACK: - ps2_cardman_prev_idx(); - break; - case INPUT_KEY_ENTER: - ps2_cardman_next_idx(); - break; + case INPUT_KEY_PREV: ps2_cardman_prev_channel(); break; + case INPUT_KEY_NEXT: ps2_cardman_next_channel(); break; + case INPUT_KEY_BACK: ps2_cardman_prev_idx(); break; + case INPUT_KEY_ENTER: ps2_cardman_next_idx(); break; } if ((prevChannel != ps2_cardman_get_channel()) || (prevIdx != ps2_cardman_get_idx())) { @@ -268,7 +248,6 @@ static void evt_scr_main(lv_event_t *event) { switching_card_timeout = time_us_64() + 1500 * 1000; } } - } } @@ -378,7 +357,6 @@ static void create_main_screen(void) { ui_header_create(scr_main, "PS2 Memory Card"); } - ui_label_create_at(scr_main, 0, 24, "Card"); scr_main_idx_lbl = ui_label_create_at(scr_main, 0, 24, ""); @@ -468,7 +446,7 @@ static void create_cardswitch_screen(void) { g_progress_text = lv_label_create(scr_card_switch); lv_obj_set_align(g_progress_text, LV_ALIGN_TOP_LEFT); - lv_obj_set_pos(g_progress_text, 0, DISPLAY_HEIGHT-9); + lv_obj_set_pos(g_progress_text, 0, DISPLAY_HEIGHT - 9); lv_label_set_text(g_progress_text, "Read XXX kB/s"); } @@ -639,7 +617,6 @@ static void create_menu_screen(void) { ui_label_create_grow_scroll(cont, "Info"); ui_label_create(cont, ">"); ui_menu_set_load_page_event(menu, cont, info_page); - } ui_menu_set_page(menu, main_page); @@ -708,8 +685,7 @@ void gui_init(void) { installing_exploit = false; } -void gui_request_refresh(void) -{ +void gui_request_refresh(void) { refresh_gui = true; } @@ -744,7 +720,7 @@ void gui_task(void) { input_update_display(g_navbar); char card_name[127]; - const char* folder_name = NULL; + const char *folder_name = NULL; if (settings_get_mode() == MODE_PS1) { static int displayed_card_idx = -1; @@ -756,7 +732,7 @@ void gui_task(void) { displayed_card_idx = ps1_cardman_get_idx(); displayed_card_channel = ps1_cardman_get_channel(); folder_name = ps1_cardman_get_folder_name(); - + snprintf(card_channel_s, sizeof(card_channel_s), "%d", displayed_card_channel); if (displayed_card_idx > 0) { @@ -769,12 +745,9 @@ void gui_task(void) { memset(card_name, 0, sizeof(card_name)); game_names_get_name_by_folder(folder_name, card_name); - if (card_name[0]) - { + if (card_name[0]) { lv_label_set_text(src_main_title_lbl, card_name); - } - else - { + } else { lv_label_set_text(src_main_title_lbl, ""); } @@ -789,35 +762,39 @@ void gui_task(void) { } else { static int displayed_card_idx = -1; static int displayed_card_channel = -1; + static ps2_cardman_state_t cardman_state = PS2_CM_STATE_NORMAL; static char card_idx_s[8]; static char card_channel_s[8]; - if (displayed_card_idx != ps2_cardman_get_idx() || displayed_card_channel != ps2_cardman_get_channel() || refresh_gui) { + if (displayed_card_idx != ps2_cardman_get_idx() || displayed_card_channel != ps2_cardman_get_channel() || cardman_state != ps2_cardman_get_state() || + refresh_gui) { displayed_card_idx = ps2_cardman_get_idx(); displayed_card_channel = ps2_cardman_get_channel(); folder_name = ps2_cardman_get_folder_name(); + cardman_state = ps2_cardman_get_state(); - if (displayed_card_idx == 0) { + if (PS2_CM_STATE_BOOT == cardman_state) { snprintf(card_idx_s, sizeof(card_idx_s), "BOOT"); snprintf(card_channel_s, sizeof(card_channel_s), " "); lv_label_set_text(lbl_channel, ""); - + lv_label_set_text(scr_main_idx_lbl, card_idx_s); + } else if (PS2_CM_STATE_GAMEID == cardman_state) { + lv_label_set_text(scr_main_idx_lbl, folder_name); + snprintf(card_channel_s, sizeof(card_channel_s), "%d", displayed_card_channel); + lv_label_set_text(lbl_channel, "Channel"); + lv_label_set_text(scr_main_idx_lbl, folder_name); } else { snprintf(card_idx_s, sizeof(card_idx_s), "%d", displayed_card_idx); snprintf(card_channel_s, sizeof(card_channel_s), "%d", displayed_card_channel); lv_label_set_text(lbl_channel, "Channel"); + lv_label_set_text(scr_main_idx_lbl, card_idx_s); } - lv_label_set_text(scr_main_idx_lbl, card_idx_s); - memset(card_name, 0, sizeof(card_name)); game_names_get_name_by_folder(folder_name, card_name); - if (card_name[0]) - { + if (card_name[0]) { lv_label_set_text(src_main_title_lbl, card_name); - } - else - { + } else { lv_label_set_text(src_main_title_lbl, ""); } diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index edb9269..5d6c816 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -1,32 +1,32 @@ #include "ps2_cardman.h" -#include "ps2_exploit.h" #include #include #include -#include "sd.h" +#include "card_emu/ps2_sd2psxman.h" +#include "card_emu/ps2_sd2psxman_commands.h" #include "debug.h" +#include "game_names/game_names.h" +#include "hardware/timer.h" +#include "ps2_exploit.h" #include "ps2_psram.h" +#include "sd.h" #include "settings.h" -#include "hardware/timer.h" - - -#define PS2_DEFAULT_CARD_SIZE PS2_CARD_SIZE_8M -#define BLOCK_SIZE (512) +#define PS2_DEFAULT_CARD_SIZE PS2_CARD_SIZE_8M +#define BLOCK_SIZE (512) static uint8_t flushbuf[BLOCK_SIZE]; static int fd = -1; -#define IDX_MIN 1 -#define IDX_BOOT 0 +#define IDX_MIN 1 #define CHAN_MIN 1 #define CHAN_MAX 8 -#define MAX_GAME_NAME_LENGTH (127) -#define MAX_PREFIX_LENGTH (4) -#define MAX_GAME_ID_LENGTH (16) +#define MAX_GAME_NAME_LENGTH (127) +#define MAX_PREFIX_LENGTH (4) +#define MAX_GAME_ID_LENGTH (16) static int card_idx; static int card_chan; @@ -37,9 +37,12 @@ static uint64_t cardprog_start; static size_t cardprog_pos; static int cardprog_wr; +static ps2_cardman_state_t cardman_state; + void ps2_cardman_init(void) { if (settings_get_ps2_autoboot()) { - card_idx = IDX_BOOT; + card_idx = PS2_CARD_IDX_SPECIAL; + cardman_state = PS2_CM_STATE_BOOT; card_chan = CHAN_MIN; snprintf(folder_name, sizeof(folder_name), "BOOT"); } else { @@ -69,7 +72,7 @@ void ps2_cardman_flush(void) { static void ensuredirs(void) { char cardpath[32]; - + snprintf(cardpath, sizeof(cardpath), "MemoryCards/PS2/%s", folder_name); sd_mkdir("MemoryCards"); @@ -81,113 +84,72 @@ static void ensuredirs(void) { } static const uint8_t block0[384] = { - 0x53, 0x6F, 0x6E, 0x79, 0x20, 0x50, 0x53, 0x32, 0x20, 0x4D, 0x65, 0x6D, 0x6F, 0x72, 0x79, 0x20, - 0x43, 0x61, 0x72, 0x64, 0x20, 0x46, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x20, 0x31, 0x2E, 0x32, 0x2E, - 0x30, 0x2E, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x10, 0x00, 0x00, 0xFF, - 0x00, 0x20, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0xC7, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0x03, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0x02, 0x2B, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, - 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x41, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF -}; + 0x53, 0x6F, 0x6E, 0x79, 0x20, 0x50, 0x53, 0x32, 0x20, 0x4D, 0x65, 0x6D, 0x6F, 0x72, 0x79, 0x20, 0x43, 0x61, 0x72, 0x64, 0x20, 0x46, 0x6F, 0x72, 0x6D, 0x61, + 0x74, 0x20, 0x31, 0x2E, 0x32, 0x2E, 0x30, 0x2E, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x10, 0x00, 0x00, 0xFF, 0x00, 0x20, 0x00, 0x00, + 0x29, 0x00, 0x00, 0x00, 0xC7, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x02, 0x2B, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x41, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}; static const uint8_t block2000[128] = { - 0x09, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x0D, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, - 0x15, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x19, 0x00, 0x00, 0x00, 0x1A, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, - 0x1D, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, - 0x21, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, - 0x25, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00 -}; + 0x09, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, + 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x1A, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x00, 0x1D, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00}; static const uint8_t blockA400[512] = { - 0x27, 0x84, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x31, 0x0C, 0x18, 0x0A, 0xE6, 0x07, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x31, 0x0C, 0x18, 0x0A, 0xE6, 0x07, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x2E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 -}; + 0x27, 0x84, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x31, 0x0C, 0x18, 0x0A, 0xE6, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, + 0x31, 0x0C, 0x18, 0x0A, 0xE6, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; static const uint8_t blockA600[512] = { - 0x26, 0xA4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x31, 0x0C, 0x18, 0x0A, 0xE6, 0x07, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x31, 0x0C, 0x18, 0x0A, 0xE6, 0x07, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x2E, 0x2E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 -}; - + 0x26, 0xA4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x31, 0x0C, 0x18, 0x0A, 0xE6, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, + 0x31, 0x0C, 0x18, 0x0A, 0xE6, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x2E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; static void genblock(size_t pos, void *vbuf) { uint8_t *buf = vbuf; @@ -199,7 +161,7 @@ static void genblock(size_t pos, void *vbuf) { } else if (pos == 0x2000) { memcpy(buf, block2000, sizeof(block2000)); } else if (pos >= 0x2400 && pos < 0xA400) { - for (size_t i = 0; i < BLOCK_SIZE/4; ++i) { + for (size_t i = 0; i < BLOCK_SIZE / 4; ++i) { uint32_t val = 0x7FFFFFFF; memcpy(&buf[i * 4], &val, sizeof(val)); } @@ -221,10 +183,9 @@ void ps2_cardman_open(void) { ensuredirs(); - if (IDX_BOOT == card_idx) + if (PS2_CM_STATE_BOOT == cardman_state) snprintf(path, sizeof(path), "MemoryCards/PS2/%s/BootCard.mcd", folder_name); - else - { + else { snprintf(path, sizeof(path), "MemoryCards/PS2/%s/%s-%d.mcd", folder_name, folder_name, card_chan); /* this is ok to do on every boot because it wouldn't update if the value is the same as currently stored */ settings_set_ps2_card(card_idx); @@ -233,7 +194,6 @@ void ps2_cardman_open(void) { printf("Switching to card path = %s\n", path); - if (!sd_exists(path)) { cardprog_wr = 1; fd = sd_open(path, O_RDWR | O_CREAT | O_TRUNC); @@ -248,7 +208,7 @@ void ps2_cardman_open(void) { if (PS2_DEFAULT_CARD_SIZE == PS2_CARD_SIZE_8M) genblock(pos, flushbuf); else - memset(flushbuf, 0xFF, sizeof(flushbuf)/sizeof(flushbuf[0])); + memset(flushbuf, 0xFF, sizeof(flushbuf) / sizeof(flushbuf[0])); if (sd_write(fd, flushbuf, BLOCK_SIZE) != BLOCK_SIZE) fatal("cannot init memcard"); psram_write(pos, flushbuf, BLOCK_SIZE); @@ -262,8 +222,7 @@ void ps2_cardman_open(void) { uint64_t end = time_us_64(); printf("OK!\n"); - printf("took = %.2f s; SD write speed = %.2f kB/s\n", (end - cardprog_start) / 1e6, - 1000000.0 * PS2_DEFAULT_CARD_SIZE / (end - cardprog_start) / 1024); + printf("took = %.2f s; SD write speed = %.2f kB/s\n", (end - cardprog_start) / 1e6, 1000000.0 * PS2_DEFAULT_CARD_SIZE / (end - cardprog_start) / 1024); } else { cardprog_wr = 0; fd = sd_open(path, O_RDWR); @@ -272,11 +231,8 @@ void ps2_cardman_open(void) { fatal("cannot open card"); card_size = sd_filesize(fd); - if ((card_size != PS2_CARD_SIZE_512K) - && (card_size != PS2_CARD_SIZE_1M) - && (card_size != PS2_CARD_SIZE_2M) - && (card_size != PS2_CARD_SIZE_4M) - && (card_size != PS2_CARD_SIZE_8M)) + if ((card_size != PS2_CARD_SIZE_512K) && (card_size != PS2_CARD_SIZE_1M) && (card_size != PS2_CARD_SIZE_2M) && (card_size != PS2_CARD_SIZE_4M) && + (card_size != PS2_CARD_SIZE_8M)) fatal("Card %d Channel %d is corrupted", card_idx, card_chan); /* read 8 megs of card image */ @@ -294,8 +250,7 @@ void ps2_cardman_open(void) { uint64_t end = time_us_64(); printf("OK!\n"); - printf("took = %.2f s; SD read speed = %.2f kB/s\n", (end - cardprog_start) / 1e6, - 1000000.0 * card_size / (end - cardprog_start) / 1024); + printf("took = %.2f s; SD read speed = %.2f kB/s\n", (end - cardprog_start) / 1e6, 1000000.0 * card_size / (end - cardprog_start) / 1024); } } @@ -308,7 +263,7 @@ void ps2_cardman_close(void) { } void ps2_cardman_set_channel(uint16_t chan_num) { - if (card_idx != IDX_BOOT) { + if ((PS2_CM_STATE_NORMAL == cardman_state) || (PS2_CM_STATE_GAMEID == cardman_state)) { if (chan_num <= CHAN_MAX && chan_num >= CHAN_MIN) { card_chan = chan_num; } @@ -320,80 +275,135 @@ void ps2_cardman_set_channel(uint16_t chan_num) { } void ps2_cardman_next_channel(void) { - if (card_idx != IDX_BOOT) { + if ((PS2_CM_STATE_NORMAL == cardman_state) || (PS2_CM_STATE_GAMEID == cardman_state)) { card_chan += 1; if (card_chan > CHAN_MAX) card_chan = CHAN_MIN; } else { card_idx = settings_get_ps2_card(); card_chan = settings_get_ps2_channel(); + cardman_state = PS2_CM_STATE_NORMAL; } snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } void ps2_cardman_prev_channel(void) { - if (card_idx != IDX_BOOT) { + if ((PS2_CM_STATE_NORMAL == cardman_state) || (PS2_CM_STATE_GAMEID == cardman_state)) { card_chan -= 1; if (card_chan < CHAN_MIN) card_chan = CHAN_MAX; } else { card_idx = settings_get_ps2_card(); card_chan = settings_get_ps2_channel(); + cardman_state = PS2_CM_STATE_NORMAL; } snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } void ps2_cardman_set_idx(uint16_t idx_num) { - if (card_idx != IDX_BOOT) { - if (idx_num >= IDX_MIN && idx_num <= 0xFFFF) { + if (PS2_CM_STATE_NORMAL == cardman_state) { + if ((idx_num >= IDX_MIN) && (idx_num <= UINT16_MAX)) { card_idx = idx_num; card_chan = CHAN_MIN; } + } else if (PS2_CM_STATE_GAMEID == cardman_state) { + } else { card_idx = settings_get_ps2_card(); card_chan = settings_get_ps2_channel(); } snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); +} +static void ps2_cardman_special_idx(int newIndx) { + char parent_id[MAX_GAME_ID_LENGTH] = { 0x00 }; + game_names_get_parent(sd2psxman_gameid, parent_id); + debug_printf("Parent ID is %s, State is %i, new Index: %i\n", parent_id, cardman_state, newIndx); + if (PS2_CM_STATE_NORMAL == cardman_state) { + if (parent_id[0]) { + card_idx = PS2_CARD_IDX_SPECIAL; + cardman_state = PS2_CM_STATE_GAMEID; + card_chan = CHAN_MIN; + snprintf(folder_name, sizeof(folder_name), "%s", parent_id); + } else if (settings_get_ps2_autoboot()) { + card_idx = PS2_CARD_IDX_SPECIAL; + cardman_state = PS2_CM_STATE_BOOT; + card_chan = CHAN_MIN; + snprintf(folder_name, sizeof(folder_name), "BOOT"); + } else { + cardman_state = PS2_CM_STATE_NORMAL; + card_idx = IDX_MIN; + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); + } + } else if (PS2_CM_STATE_BOOT == cardman_state) { + if ((newIndx > PS2_CARD_IDX_SPECIAL) && (parent_id[0])) { + card_idx = PS2_CARD_IDX_SPECIAL; + cardman_state = PS2_CM_STATE_GAMEID; + card_chan = CHAN_MIN; + snprintf(folder_name, sizeof(folder_name), "%s", parent_id); + } else { + card_idx = settings_get_ps2_card(); + card_chan = settings_get_ps2_channel(); + cardman_state = PS2_CM_STATE_NORMAL; + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); + } + } else if (PS2_CM_STATE_GAMEID == cardman_state) { + if ((newIndx < PS2_CARD_IDX_SPECIAL) && (settings_get_ps2_autoboot())) { + // Prev Pressed and Boot available + card_idx = PS2_CARD_IDX_SPECIAL; + cardman_state = PS2_CM_STATE_BOOT; + card_chan = CHAN_MIN; + snprintf(folder_name, sizeof(folder_name), "BOOT"); + } else { + card_idx = settings_get_ps2_card(); + card_chan = settings_get_ps2_channel(); + cardman_state = PS2_CM_STATE_NORMAL; + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); + } + } } void ps2_cardman_next_idx(void) { - if (card_idx != IDX_BOOT) { - card_idx += 1; - card_chan = CHAN_MIN; + int newIdx = card_idx + 1; + if (PS2_CM_STATE_NORMAL != cardman_state) { + ps2_cardman_special_idx(newIdx); } else { - card_idx = settings_get_ps2_card(); - card_chan = settings_get_ps2_channel(); + card_idx = (newIdx > (int)UINT16_MAX) ? UINT16_MAX : newIdx; + card_chan = CHAN_MIN; + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } - snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } void ps2_cardman_prev_idx(void) { - if (card_idx != IDX_BOOT) { - int minIndex = (settings_get_ps2_autoboot() ? IDX_BOOT : IDX_MIN); - card_idx -= 1; - card_chan = CHAN_MIN; - if (card_idx < minIndex) - card_idx = minIndex; + int newIdx = card_idx - 1; + if ((PS2_CM_STATE_NORMAL != cardman_state) || (PS2_CARD_IDX_SPECIAL == newIdx)) { + ps2_cardman_special_idx(newIdx); } else { - card_idx = settings_get_ps2_card(); - card_chan = settings_get_ps2_channel(); - } - if (card_idx != IDX_BOOT) + card_idx = newIdx; + card_chan = CHAN_MIN; snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); - else - snprintf(folder_name, sizeof(folder_name), "BOOT"); - + } } -int ps2_cardman_get_idx(void) { - return card_idx; +int ps2_cardman_get_idx(void) { + return (cardman_state == PS2_CM_STATE_NORMAL) ? card_idx : PS2_CARD_IDX_SPECIAL; } int ps2_cardman_get_channel(void) { return card_chan; } +void ps2_cardman_set_gameid(const char *const card_game_id) { + if (card_game_id[0]) { + char parent_id[MAX_GAME_ID_LENGTH]; + game_names_get_parent(card_game_id, parent_id); + snprintf(folder_name, sizeof(folder_name), "%s", parent_id); + card_idx = PS2_CARD_IDX_SPECIAL; + cardman_state = PS2_CM_STATE_GAMEID; + card_chan = CHAN_MIN; + } +} + void ps2_cardman_set_progress_cb(cardman_cb_t func) { cardman_cb = func; } @@ -401,8 +411,7 @@ void ps2_cardman_set_progress_cb(cardman_cb_t func) { char *ps2_cardman_get_progress_text(void) { static char progress[32]; - snprintf(progress, sizeof(progress), "%s %.2f kB/s", cardprog_wr ? "Wr" : "Rd", - 1000000.0 * cardprog_pos / (time_us_64() - cardprog_start) / 1024); + snprintf(progress, sizeof(progress), "%s %.2f kB/s", cardprog_wr ? "Wr" : "Rd", 1000000.0 * cardprog_pos / (time_us_64() - cardprog_start) / 1024); return progress; } @@ -411,10 +420,10 @@ uint32_t ps2_cardman_get_card_size(void) { return card_size; } -void ps2_cardman_set_gameid(const char* game_id) { - strlcpy(folder_name, game_id, MAX_GAME_ID_LENGTH); -} - -const char* ps2_cardman_get_folder_name(void) { +const char *ps2_cardman_get_folder_name(void) { return folder_name; } + +ps2_cardman_state_t ps2_cardman_get_state(void) { + return cardman_state; +} \ No newline at end of file diff --git a/src/ps2/ps2_cardman.h b/src/ps2/ps2_cardman.h index c028be9..705cc8b 100644 --- a/src/ps2/ps2_cardman.h +++ b/src/ps2/ps2_cardman.h @@ -8,6 +8,14 @@ #define PS2_CARD_SIZE_1M (1024 * 1024) #define PS2_CARD_SIZE_512K (512 * 1024) +#define PS2_CARD_IDX_SPECIAL 0 + +typedef enum { + PS2_CM_STATE_BOOT, + PS2_CM_STATE_GAMEID, + PS2_CM_STATE_NORMAL +} ps2_cardman_state_t; + void ps2_cardman_init(void); int ps2_cardman_write_sector(int sector, void *buf512); void ps2_cardman_flush(void); @@ -33,4 +41,5 @@ char *ps2_cardman_get_progress_text(void); void ps2_cardman_set_gameid(const char* game_id); const char* ps2_cardman_get_gameid(void); const char* ps2_cardman_get_gamename(void); -const char* ps2_cardman_get_folder_name(void); \ No newline at end of file +const char* ps2_cardman_get_folder_name(void); +ps2_cardman_state_t ps2_cardman_get_state(void); From 1ee8a9f8937758772164e6f55a68a84365669c77 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sat, 30 Dec 2023 12:15:15 +0100 Subject: [PATCH 85/91] Cherry Pick Ecc Table --- src/ps2/card_emu/ps2_mc_commands.c | 2 +- src/ps2/card_emu/ps2_mc_internal.h | 2 +- src/ps2/card_emu/ps2_memory_card.c | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ps2/card_emu/ps2_mc_commands.c b/src/ps2/card_emu/ps2_mc_commands.c index 572127a..38de5d8 100644 --- a/src/ps2/card_emu/ps2_mc_commands.c +++ b/src/ps2/card_emu/ps2_mc_commands.c @@ -253,7 +253,7 @@ inline __attribute__((always_inline)) void ps2_mc_cmd_readData(void) { mc_respond(b); if (readptr <= 512) { - uint8_t c = Table[b]; + uint8_t c = EccTable[b]; eccptr[0] ^= c; if (c & 0x80) { eccptr[1] ^= ~(readptr & 0x7F); diff --git a/src/ps2/card_emu/ps2_mc_internal.h b/src/ps2/card_emu/ps2_mc_internal.h index 020993b..9ddfd02 100644 --- a/src/ps2/card_emu/ps2_mc_internal.h +++ b/src/ps2/card_emu/ps2_mc_internal.h @@ -25,7 +25,7 @@ extern uint32_t readptr, writeptr; extern uint8_t *eccptr; extern bool flash_mode; -extern uint8_t Table[]; +extern uint8_t EccTable[]; extern uint8_t receive(uint8_t *cmd); extern uint8_t receiveFirst(uint8_t *cmd); diff --git a/src/ps2/card_emu/ps2_memory_card.c b/src/ps2/card_emu/ps2_memory_card.c index 0076758..740497b 100644 --- a/src/ps2/card_emu/ps2_memory_card.c +++ b/src/ps2/card_emu/ps2_memory_card.c @@ -147,7 +147,7 @@ inline void __time_critical_func(mc_respond)(uint8_t ch) { pio_sm_put_blocking(pio0, dat_writer.sm, ch); } -uint8_t Table[] = { +uint8_t EccTable[] = { 0x00, 0x87, 0x96, 0x11, 0xa5, 0x22, 0x33, 0xb4, 0xb4, 0x33, 0x22, 0xa5, 0x11, 0x96, 0x87, 0x00, 0xc3, 0x44, 0x55, 0xd2, 0x66, 0xe1, 0xf0, 0x77, 0x77, 0xf0, 0xe1, 0x66, 0xd2, 0x55, 0x44, 0xc3, 0xd2, 0x55, 0x44, 0xc3, 0x77, 0xf0, 0xe1, 0x66, 0x66, 0xe1, 0xf0, 0x77, 0xc3, 0x44, 0x55, 0xd2, 0x11, 0x96, 0x87, 0x00, 0xb4, 0x33, 0x22, 0xa5, 0xa5, 0x22, 0x33, 0xb4, 0x00, 0x87, 0x96, 0x11, 0xe1, 0x66, 0x77, 0xf0, 0x44, 0xc3, 0xd2, 0x55, 0x55, 0xd2, 0xc3, 0x44, 0xf0, 0x77, @@ -165,7 +165,7 @@ void calcECC(uint8_t *ecc, const uint8_t *data) { ecc[0] = ecc[1] = ecc[2] = 0; for (i = 0; i < 0x80; i++) { - c = Table[data[i]]; + c = EccTable[data[i]]; ecc[0] ^= c; if (c & 0x80) { From b67c8c9ff4699b9d78c4073fae550b49e64fe105 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sat, 30 Dec 2023 14:13:35 +0100 Subject: [PATCH 86/91] Cherry Pick Workflow from Upstream --- .github/workflows/cmake.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 33dec3a..134704f 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -2,9 +2,9 @@ name: CMake on: push: - branches: [ "main", "develop" ] + branches: '*' pull_request: - branches: [ "main", "develop" ] + branches: 'v*' env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) @@ -62,15 +62,22 @@ jobs: - name: Rename Debug USB uf2 run: mv build_usb/sd2psx.uf2 build/sd2psx_usb_debug.uf2 + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: SD2PSX-${{ github.sha }} + path: | + build/*.uf2 + - uses: marvinpinto/action-automatic-releases@v1.2.1 - if: github.event_name != 'pull_request' + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') #only release on tag or master branch # uses: marvinpinto/action-automatic-releases@919008cf3f741b179569b7a6fb4d8860689ab7f0 with: # GitHub secret token title: "${{ env.SD2PSX_VERSION }}" repo_token: "${{ secrets.GITHUB_TOKEN }}" automatic_release_tag: ${{ env.SD2PSX_RLS_TAG }} - prerelease: true + prerelease: ${{ !startsWith(github.ref, 'refs/tags/v') }} # Assets to upload to the release files: | build/*.uf2 From ecaa01302211e42981372ae9da95908f44ad319d Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sat, 30 Dec 2023 14:13:55 +0100 Subject: [PATCH 87/91] Cherry Pick settings --- src/ps1/ps1_cardman.c | 2 -- src/ps2/ps2_cardman.c | 17 +++-------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/ps1/ps1_cardman.c b/src/ps1/ps1_cardman.c index 9943b7b..06ca8d3 100644 --- a/src/ps1/ps1_cardman.c +++ b/src/ps1/ps1_cardman.c @@ -35,8 +35,6 @@ static char folder_name[MAX_GAME_ID_LENGTH]; void ps1_cardman_init(void) { card_idx = settings_get_ps1_card(); card_chan = settings_get_ps1_channel(); - if (card_chan < CHAN_MIN || card_chan > CHAN_MAX) - card_chan = CHAN_MIN; snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index 5d6c816..e66f82d 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -20,10 +20,6 @@ static uint8_t flushbuf[BLOCK_SIZE]; static int fd = -1; -#define IDX_MIN 1 -#define CHAN_MIN 1 -#define CHAN_MAX 8 - #define MAX_GAME_NAME_LENGTH (127) #define MAX_PREFIX_LENGTH (4) #define MAX_GAME_ID_LENGTH (16) @@ -301,16 +297,9 @@ void ps2_cardman_prev_channel(void) { } void ps2_cardman_set_idx(uint16_t idx_num) { - if (PS2_CM_STATE_NORMAL == cardman_state) { - if ((idx_num >= IDX_MIN) && (idx_num <= UINT16_MAX)) { - card_idx = idx_num; - card_chan = CHAN_MIN; - } - } else if (PS2_CM_STATE_GAMEID == cardman_state) { - - } else { - card_idx = settings_get_ps2_card(); - card_chan = settings_get_ps2_channel(); + if ((idx_num >= IDX_MIN) && (idx_num <= UINT16_MAX)) { + card_idx = idx_num; + card_chan = CHAN_MIN; } snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } From eb4f33f95d6ec676f38d99db4e71b11b900ffff9 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 31 Dec 2023 21:58:17 +0100 Subject: [PATCH 88/91] Fix Switching from GameID to Boot and back --- src/gui.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui.c b/src/gui.c index 38c29bd..31aa70e 100644 --- a/src/gui.c +++ b/src/gui.c @@ -226,6 +226,7 @@ static void evt_scr_main(lv_event_t *event) { printf("new PS1 card=%d chan=%d\n", ps1_cardman_get_idx(), ps1_cardman_get_channel()); } } else { + ps2_cardman_state_t prevState = ps2_cardman_get_state(); prevChannel = ps2_cardman_get_channel(); prevIdx = ps2_cardman_get_idx(); @@ -236,7 +237,7 @@ static void evt_scr_main(lv_event_t *event) { case INPUT_KEY_ENTER: ps2_cardman_next_idx(); break; } - if ((prevChannel != ps2_cardman_get_channel()) || (prevIdx != ps2_cardman_get_idx())) { + if ((prevChannel != ps2_cardman_get_channel()) || (prevIdx != ps2_cardman_get_idx()) || (prevState != ps2_cardman_get_state())) { ps2_memory_card_exit(); ps2_cardman_close(); switching_card = 1; From 250416fb0a8bbf4f11ae9c0b5a8661c2526b6624 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 31 Dec 2023 21:58:46 +0100 Subject: [PATCH 89/91] Fix issues for missing game ids in database --- src/game_names/game_names.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/game_names/game_names.c b/src/game_names/game_names.c index 745c6ff..1c5b4ee 100644 --- a/src/game_names/game_names.c +++ b/src/game_names/game_names.c @@ -1,7 +1,6 @@ #include "game_names.h" - #include #include #include @@ -73,11 +72,11 @@ static uint32_t game_names_char_array_to_uint32(const char in[4]) { #pragma GCC diagnostic pop static uint32_t game_names_find_prefix_offset(uint32_t numericPrefix, const char* const db_start) { - uint32_t offset = 0; + uint32_t offset = UINT32_MAX; const char* pointer = db_start; - while (offset == 0) { + while (offset == UINT32_MAX) { uint32_t currentprefix = game_names_char_array_to_uint32(pointer), currentoffset = game_names_char_array_to_uint32(&pointer[4]); if (currentprefix == numericPrefix) { @@ -134,9 +133,6 @@ static game_lookup find_game_lookup(const char* game_id) { const char* const db_start = settings_get_mode() == MODE_PS1 ? &_binary_gamedbps1_dat_start : &_binary_gamedbps2_dat_start; const char* const db_size = settings_get_mode() == MODE_PS1 ? &_binary_gamedbps1_dat_size : &_binary_gamedbps2_dat_size; - //const char* const db_start = &_binary_gamedbps1_dat_start; - //const size_t db_size = (size_t)&_binary_gamedbps1_dat_size; - uint32_t prefixOffset = 0; game_lookup ret = { .game_id = 0U, @@ -174,7 +170,7 @@ static game_lookup find_game_lookup(const char* game_id) { uint32_t offset = prefixOffset; game_lookup game; do { - game = build_game_lookup(db_start, db_size, offset); + game = build_game_lookup(db_start, (size_t)db_size, offset); if (game.game_id == numeric_id) { ret = game; @@ -280,6 +276,8 @@ void game_names_get_parent(const char* const game_id, char* const parent_id) { snprintf(parent_id, MAX_GAME_ID_LENGTH, "%s-%0*d", prefixString, (int)strlen(idString), (int)game.parent_id); debug_printf("Parent ID: %s\n", parent_id); + } else { + strlcpy(parent_id, game_id, MAX_GAME_ID_LENGTH); } } From 0c46b7969e4784e9eaf7da06cb5472946d38203f Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 31 Dec 2023 22:14:33 +0100 Subject: [PATCH 90/91] Cleanup includes --- src/ps2/card_emu/ps2_mc_commands.c | 1 - src/ps2/card_emu/ps2_memory_card.c | 10 ---------- src/ps2/card_emu/ps2_sd2psxman.c | 4 ++-- src/ps2/card_emu/ps2_sd2psxman_commands.c | 2 -- src/ps2/ps2_cardman.c | 2 -- 5 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/ps2/card_emu/ps2_mc_commands.c b/src/ps2/card_emu/ps2_mc_commands.c index 38de5d8..6645333 100644 --- a/src/ps2/card_emu/ps2_mc_commands.c +++ b/src/ps2/card_emu/ps2_mc_commands.c @@ -4,7 +4,6 @@ #include #include "hardware/dma.h" -#include "pico/platform.h" #include "ps2_cardman.h" #include "ps2_dirty.h" #include "ps2_mc_internal.h" diff --git a/src/ps2/card_emu/ps2_memory_card.c b/src/ps2/card_emu/ps2_memory_card.c index 740497b..f794e3d 100644 --- a/src/ps2/card_emu/ps2_memory_card.c +++ b/src/ps2/card_emu/ps2_memory_card.c @@ -1,17 +1,8 @@ -#include "../ps2_cardman.h" #include "../ps2_dirty.h" #include "../ps2_exploit.h" -#include "../ps2_pio_qspi.h" #include "../ps2_psram.h" -#include "config.h" #include "debug.h" -#include "des.h" -#include "flashmap.h" -#include "game_names/game_names.h" -#include "hardware/dma.h" -#include "hardware/flash.h" #include "hardware/gpio.h" -#include "hardware/regs/addressmap.h" #include "hardware/timer.h" #include "keystore.h" #include "pico/platform.h" @@ -22,7 +13,6 @@ #include #include -#include #include #include "ps2_sd2psxman_commands.h" diff --git a/src/ps2/card_emu/ps2_sd2psxman.c b/src/ps2/card_emu/ps2_sd2psxman.c index 9496374..8d81ccd 100644 --- a/src/ps2/card_emu/ps2_sd2psxman.c +++ b/src/ps2/card_emu/ps2_sd2psxman.c @@ -1,15 +1,15 @@ #include "ps2/card_emu/ps2_sd2psxman.h" #include -#include #include "debug.h" #include "gui.h" -#include "pico/time.h" #include "ps2/card_emu/ps2_memory_card.h" #include "ps2/card_emu/ps2_sd2psxman_commands.h" #include "ps2/ps2_cardman.h" +#include "pico/platform.h" + volatile uint8_t sd2psxman_cmd; volatile uint8_t sd2psxman_mode; volatile uint16_t sd2psxman_cnum; diff --git a/src/ps2/card_emu/ps2_sd2psxman_commands.c b/src/ps2/card_emu/ps2_sd2psxman_commands.c index 03ca2f1..e739ee5 100644 --- a/src/ps2/card_emu/ps2_sd2psxman_commands.c +++ b/src/ps2/card_emu/ps2_sd2psxman_commands.c @@ -1,9 +1,7 @@ #include #include "ps2_cardman.h" -#include "ps2_dirty.h" #include "ps2_mc_internal.h" -#include "ps2_pio_qspi.h" #include "ps2_sd2psxman.h" #include "ps2_sd2psxman_commands.h" diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index e66f82d..c246713 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -5,11 +5,9 @@ #include #include "card_emu/ps2_sd2psxman.h" -#include "card_emu/ps2_sd2psxman_commands.h" #include "debug.h" #include "game_names/game_names.h" #include "hardware/timer.h" -#include "ps2_exploit.h" #include "ps2_psram.h" #include "sd.h" #include "settings.h" From 8fc8be386e90c43105b577b8e46d386eb7a64cb8 Mon Sep 17 00:00:00 2001 From: BBsan2k Date: Sun, 4 Feb 2024 10:27:51 +0100 Subject: [PATCH 91/91] Game ID fixes --- src/game_names/game_names.c | 4 ++-- src/ps2/ps2_cardman.c | 16 ++++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/game_names/game_names.c b/src/game_names/game_names.c index 1c5b4ee..a3d82de 100644 --- a/src/game_names/game_names.c +++ b/src/game_names/game_names.c @@ -166,7 +166,7 @@ static game_lookup find_game_lookup(const char* game_id) { prefixOffset = game_names_find_prefix_offset(numeric_prefix, db_start); - if (prefixOffset < db_size) { + if (prefixOffset < (size_t)db_size) { uint32_t offset = prefixOffset; game_lookup game; do { @@ -179,7 +179,7 @@ static game_lookup find_game_lookup(const char* game_id) { } offset += 12; - } while ((game.game_id != 0) && (offset < db_size) && (ret.game_id == 0)); + } while ((game.game_id != 0) && (offset < (size_t)db_size) && (ret.game_id == 0)); } } diff --git a/src/ps2/ps2_cardman.c b/src/ps2/ps2_cardman.c index c246713..00bb85f 100644 --- a/src/ps2/ps2_cardman.c +++ b/src/ps2/ps2_cardman.c @@ -277,8 +277,8 @@ void ps2_cardman_next_channel(void) { card_idx = settings_get_ps2_card(); card_chan = settings_get_ps2_channel(); cardman_state = PS2_CM_STATE_NORMAL; + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } - snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } void ps2_cardman_prev_channel(void) { @@ -290,8 +290,8 @@ void ps2_cardman_prev_channel(void) { card_idx = settings_get_ps2_card(); card_chan = settings_get_ps2_channel(); cardman_state = PS2_CM_STATE_NORMAL; + snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } - snprintf(folder_name, sizeof(folder_name), "Card%d", card_idx); } void ps2_cardman_set_idx(uint16_t idx_num) { @@ -381,13 +381,17 @@ int ps2_cardman_get_channel(void) { } void ps2_cardman_set_gameid(const char *const card_game_id) { + char new_folder_name[MAX_GAME_ID_LENGTH]; if (card_game_id[0]) { char parent_id[MAX_GAME_ID_LENGTH]; game_names_get_parent(card_game_id, parent_id); - snprintf(folder_name, sizeof(folder_name), "%s", parent_id); - card_idx = PS2_CARD_IDX_SPECIAL; - cardman_state = PS2_CM_STATE_GAMEID; - card_chan = CHAN_MIN; + snprintf(new_folder_name, sizeof(new_folder_name), "%s", parent_id); + if (strcmp(new_folder_name, folder_name) != 0) { + card_idx = PS2_CARD_IDX_SPECIAL; + cardman_state = PS2_CM_STATE_GAMEID; + card_chan = CHAN_MIN; + snprintf(folder_name, sizeof(folder_name), "%s", parent_id); + } } }