/* * This file is part of open_agb_firm * Copyright (C) 2021 derrek, profi200 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include "types.h" #include "error_codes.h" #include "fs.h" #include "util.h" #include "arm11/drivers/hid.h" #include "arm11/console.h" #include "arm11/fmt.h" #include "drivers/gfx.h" #include "arm11/acf.h" #include "arm11/atp.h" #include "arm11/acl.h" #include "arm11/cheat.h" #define screenClean() memset(consoleGet()->frameBuffer, 0, CWIDTH*CHEIGHT*sizeof(uint16_t)) // Notes on these settings: // MAX_ENT_BUF_SIZE should be big enough to hold the average file/dir name length * MAX_DIR_ENTRIES. #define MAX_ENT_BUF_SIZE (1024u * 196) // 196 KiB. #define MAX_DIR_ENTRIES (999u) #define DIR_READ_BLOCKS (10u) #define CWIDTH 320u #define CHEIGHT 240u #define ENT_TYPE_FILE (0) #define ENT_TYPE_DIR (1) typedef struct { u32 num; // Total number of entries. char entBuf[MAX_ENT_BUF_SIZE]; // Format: char entryType; char name[X]; // null terminated. char *ptrs[MAX_DIR_ENTRIES]; // For fast sorting. } DirList; int dlistCompare(const void *a, const void *b) { const char *entA = *(char**)a; const char *entB = *(char**)b; // Compare the entry type. Dirs have priority over files. if(*entA != *entB) return (int)*entB - *entA; // Compare the string. int res; do { res = *++entA - *++entB; } while(res == 0 && *entA != '\0' && *entB != '\0'); return res; } static Result scanDir(const char *const path, DirList *const dList, const char *const filter, const char *match, int *index) { FILINFO *const fis = (FILINFO*)malloc(sizeof(FILINFO) * DIR_READ_BLOCKS); if(fis == NULL) return RES_OUT_OF_MEM; dList->num = 0; Result res; DHandle dh; u32 received = 0; if((res = fOpenDir(&dh, path)) == RES_OK) { u32 read; // Number of entries read by fReadDir(). u32 numEntries = 0; // Total number of processed entries. u32 entBufPos = 0; // Entry buffer position/number of bytes used. const u32 filterLen = strlen(filter); do { if((res = fReadDir(dh, fis, DIR_READ_BLOCKS, &read)) != RES_OK) break; received = numEntries + read; read = (read <= MAX_DIR_ENTRIES - numEntries ? read : MAX_DIR_ENTRIES - numEntries); for(u32 i = 0; i < read; i++) { const char entType = (fis[i].fattrib & AM_DIR ? ENT_TYPE_DIR : ENT_TYPE_FILE); const u32 nameLen = strlen(fis[i].fname); if(entType == ENT_TYPE_FILE) { if(nameLen <= filterLen || strcmp(filter, fis[i].fname + nameLen - filterLen) != 0) continue; } // nameLen does not include the entry type and NULL termination. if(entBufPos + nameLen + 2 > MAX_ENT_BUF_SIZE) { res = RES_PATH_TOO_LONG; goto scanEnd; } char *const entry = &dList->entBuf[entBufPos]; *entry = entType; safeStrcpy(&entry[1], fis[i].fname, 256); dList->ptrs[numEntries++] = entry; entBufPos += nameLen + 2; } } while(read == DIR_READ_BLOCKS); scanEnd: dList->num = numEntries; fCloseDir(dh); } free(fis); qsort(dList->ptrs, dList->num, sizeof(char*), dlistCompare); if( match != NULL ) { for( u32 i=0; i < dList->num; ++i ) { if( strcmp(match, &dList->ptrs[i][1]) == 0 ) { *index = i; break; } } } return received > MAX_DIR_ENTRIES ? RES_OUT_OF_RANGE : res; } static Result rom_get_serial( const char *file, char serial[5] ) { FHandle f; Result res; u32 readed; serial[0] = serial[4] = '\0'; if( RES_OK != (res=fOpen(&f, file, FA_OPEN_EXISTING | FA_READ)) ) { return res; } if( RES_OK != (res=fLseek(f, 0xac)) ) { fClose( f ); return res; } if( RES_OK != (res=fRead(f, serial, 4, &readed)) || readed != 4 ) { fClose(f); return res; } fClose(f); return RES_OK; } static atp_text_t folder_help[] = { "操 作 说 明", "~ ~ ~ ~ ~ ~ ~", "蓝色             目录", "白色             游戏", "~ ~ ~ ~ ~ ~ ~", "上下方向键   切换选中文件或目录", "左右方向键          翻页", "A键      查看目录或启动游戏", "B键          上层文件夹", "X键            金手指", "Y键           暂不使用", "START        查看说明", "SELECT       系统设置" }; static atp_text_t cheat_off_help[] = { "未启用金手指", "如需开启,请进入系统设置菜单(SELECT键进入)", "在“激活金手指”选项中启用金手指功能" }; static atp_error_t display_folder( atp_callerdata_t data, atp_counter_t index, atp_itemcfg_t *config ) { void* *dat = (void **)data; DirList *const dList = (DirList*)dat[0]; u8 type = *dList->ptrs[index]; if( type == ENT_TYPE_DIR ) config->text_color = ATP_COLOR_BLUE; config->text = &dList->ptrs[index][1]; config->value = index; return ATP_SUCCESS; } #define DECLARE_ERROR_PAGE(name, message) static atp_error_t name (atp_callerdata_t, atp_counter_t, atp_linecfg_t *c) \ { \ c->text = message; \ c->text_align = ATP_PLACEMENT_CENTER; \ c->text_color = ATP_COLOR_RED; \ return ATP_SUCCESS; \ } DECLARE_ERROR_PAGE( display_openlib, "打开金手指文件出错" ) DECLARE_ERROR_PAGE( display_selcht, "查找金手指配置出错" ) DECLARE_ERROR_PAGE( display_noserial, "提取游戏识别码失败" ) DECLARE_ERROR_PAGE( display_nocheat, "找不到对应的金手指配置" ) DECLARE_ERROR_PAGE( display_empty, "没有合适的文件" ) DECLARE_ERROR_PAGE( display_toolong, "路径过长,改名或移动文件后再试" ) DECLARE_ERROR_PAGE( display_pathfull, "游戏或目录过量,最多显示999个" ) DECLARE_ERROR_PAGE( display_longname, "文件名总计过长,只显示前面的文件" ) static atp_error_t display_help( atp_callerdata_t table, atp_counter_t index, atp_linecfg_t *config ) { atp_text_t *list = (atp_text_t*)table; config->text_align = ATP_PLACEMENT_CENTER; config->text_color = index == 0 ? ATP_COLOR_MAGENTA : ATP_COLOR_LIGHT; config->text = list[index]; return ATP_SUCCESS; } atp_error_t help_page( atp_text_t *wording, atp_counter_t length ) { atp_tips( NULL, "返回:按A/B" ); atp_error_t res = atp_show( length, display_help, (atp_callerdata_t)wording ); atp_tips( NULL, "指引:按START" ); return res; } static atp_error_t disp_str( atp_callerdata_t data, atp_counter_t, atp_linecfg_t *cfg ) { cfg->text = (atp_text_t)data; return ATP_SUCCESS; } /*void log( const char *fmt, ... ) { char buf[512]; ee_puts("\x1b[2J"); va_list args; va_start(args, fmt); u32 res = ee_vsnprintf(buf, 512, fmt, args); va_end(args); ee_puts( buf ); uint32_t down; do{ GFX_waitForVBlank0(); hidScanInput(); down = hidKeysDown(); }while ( down == 0 ); }*/ static atp_error_t select_region( atp_callerdata_t, atp_counter_t index, atp_itemcfg_t *config ) { static char text[16]; acl_region_t sreg; acl_chtid_t sid, id; if( ACHTLIB_SUCCESS != acl_query_cheat_set(index, &sid, &sreg) ) { config->text = "无效数据"; config->value = index; return ATP_SUCCESS; } char *t; switch( sreg ) { case 'C': t = "官方中文"; break; case 'J': t = "日文版"; break; case 'E': t = "英文版"; break; case 'F': t = "法语版"; break; case 'S': t = "西班牙语"; break; case 'I': t = "意大利语"; break; case 'D': t = "德语版"; break; case 'K': t = "韩文版"; break; case 'X': case 'P': t = "欧洲语种"; break; default: t = "其他语种"; break; } ee_sprintf(text, "%c-%s", 'A'+(char)index, t); if( CCHT_NOT_INIT == info_current_cheat( &id, NULL ) ) id = 0; config->text = text; config->value = index; // show saved information if( sid == id ) { config->extra_text = "已启用"; config->extra_text_color = ATP_COLOR_GREEN; } return ATP_SUCCESS; } static atp_error_t select_holes( atp_callerdata_t, atp_counter_t index, atp_itemcfg_t *cfg ) { acl_text_t text; acl_error_t res = acl_entry_get_label(index, &text); if( res == ACHTLIB_SUCCESS ) cfg->text = text; else cfg->text = "无效数据"; cfg->value = 1+index; // show saved information acl_entryid_t id; if( CCHT_OK == get_current_cheat( index, &id ) && ENT_USING(id) ) { cfg->extra_text = "开"; cfg->extra_text_color = ATP_COLOR_GREEN; } return ATP_SUCCESS; } static atp_error_t select_onoff( atp_callerdata_t data, atp_counter_t index, atp_itemcfg_t *cfg ) { acl_entryid_t eid = (acl_entryid_t)data; eid &= 0xffff; if( index == 0 ) { cfg->text = "不开启"; cfg->value = eid; } else if( index == 1 ) { cfg->text = "开启"; cfg->value = 1<<16 | eid; } else { cfg->text = "调整数值开启[结果未知]"; cfg->value = 0; } // show saved information if( CCHT_OK == include_current_cheat((acl_entryid_t)cfg->value) ) { cfg->extra_text = "选中"; cfg->extra_text_color = ATP_COLOR_GREEN; } return ATP_SUCCESS; } ALWAYS_INLINE u16 calc_step( u16 value ) { u16 n = value / 50; if( n == 0 ) return 1; else { u16 base = 1; while( n > 10 ) { n = n/10; base *= 10; } if( n < 5 ) return base * 5; else return base * 10; } } static atp_error_t step_provider( atp_callerdata_t mixid, atp_counter_t index, atp_itemcfg_t *cfg ) { static char number[8]; u32 mix = (u32)mixid; u16 max = mix & 0xffff; u16 step = mix >> 16; u16 res = step * (index+1); ee_snprintf( number, 8, "%d", res < max ? res : max ); cfg->text = number; cfg->value = res < max ? res : max; return ATP_SUCCESS; } static atp_error_t handle_onoff_entry( acl_entryid_t id, acl_elemlen_t codelen, atp_itemval_t *item ) { #define DEFAULT_ONOFF_HANDLER atp_select("选择此项目对应的设置", 2, select_onoff, NULL, (atp_callerdata_t)id, 0, 0, item); #define ONOFF_SAFE_CALLACL( statement ) if( ACHTLIB_SUCCESS != statement ) return DEFAULT_ONOFF_HANDLER; if( codelen > 4 )// more than 2 address return DEFAULT_ONOFF_HANDLER; u16 targetval = 0; acl_armcode_t code; // one address if( codelen == 2 ) { ONOFF_SAFE_CALLACL( acl_entry_get_armcode(1, &code) ); targetval = code & 0xff; } // two addresses else if( codelen == 4 ) { u32 addr0, addr1; ONOFF_SAFE_CALLACL( acl_entry_get_armcode(0, &addr0) ); ONOFF_SAFE_CALLACL( acl_entry_get_armcode(2, &addr1) ); if( (addr0 & 1) || (addr0 | 1) != addr1 ) // valid u16 address return DEFAULT_ONOFF_HANDLER; ONOFF_SAFE_CALLACL( acl_entry_get_armcode(1, &code) ); targetval = code & 0xff; ONOFF_SAFE_CALLACL( acl_entry_get_armcode(3, &code) ); targetval |= (code & 0xff) << 8; } // more than two address, should not overwrite values else return DEFAULT_ONOFF_HANDLER; atp_error_t res = atp_select( "选择此项目对应的设置", 3, select_onoff, NULL, targetval << 16 | id, 0, 0, item ); if( res == ATP_SUCCESS && *item == 0 ) { char title[24]; ee_snprintf( title, sizeof(title), "默认值:%d", targetval ); u16 step = calc_step( targetval ); atp_counter_t n = (targetval-1+step)/step; res = atp_select(title, n, step_provider, NULL, (atp_callerdata_t)(step<<16|targetval), n, 0, item ); if( res == ATP_SUCCESS && *item != targetval ) overwrite_current_cheat( 1<<16|id, *item ); *item = 1<<16 | id; } return res; } static atp_error_t select_keys( atp_callerdata_t data, atp_counter_t index, atp_itemcfg_t *cfg ) { acl_text_t text; if( index == 0 ) { cfg->text = "不开启"; } else { acl_error_t res = acl_entry_get_label(index-1, &text); if( res == ACHTLIB_SUCCESS ) cfg->text = text; else cfg->text = "无效数据"; } acl_entryid_t eid = (acl_entryid_t)data; cfg->value = index<<16 | eid; if( CCHT_OK == include_current_cheat((acl_entryid_t)cfg->value) ) { cfg->extra_text = "选中"; cfg->extra_text_color = ATP_COLOR_GREEN; } return ATP_SUCCESS; } extern atp_error_t oaf_config_page(); #define DIRBUFFSIZE 512 #define WAIT_ON_ACT( act ) ( ATP_POWER_OFF == (act) ) ? ATP_POWER_OFF : ATP_PAGE_REFRESH #define WAIT_ON_ERRPAGE( page ) WAIT_ON_ACT( atp_show(1, (page), NULL) ) static atp_pageopt_t serve_on_key( atp_callerdata_t data, atp_counter_t index, atp_boolean_t x, atp_boolean_t y, atp_boolean_t l, atp_boolean_t r, atp_boolean_t start, atp_boolean_t select ) { if( start ) { atp_tips( "", NULL ); return WAIT_ON_ACT( help_page( folder_help, sizeof(folder_help)/sizeof(atp_text_t) ) ); } else if( select ) { return WAIT_ON_ACT( oaf_config_page() ); } else if( x ) { extern int get_cheat_mode(); if( CHEAT_MODE_DISABLED == get_cheat_mode() ) { return WAIT_ON_ACT( help_page( cheat_off_help, sizeof(cheat_off_help)/sizeof(atp_text_t) ) ); } void* *dat = (void **)data; DirList const *dList = (DirList*)dat[0]; char *path = (char*)dat[1]; const char *file = &dList->ptrs[index][1]; int pathlen = strlen(path); int filelen = strlen(file); if( pathlen + filelen > DIRBUFFSIZE - 1) { return WAIT_ON_ERRPAGE( display_toolong ); } else if( ENT_TYPE_DIR == dList->ptrs[index][0] ) { return WAIT_ON_ERRPAGE( display_empty ); } path[pathlen] = '/'; safeStrcpy( path+pathlen+1, file, DIRBUFFSIZE - filelen ); #define RECOVER_PATH path[pathlen] = '\0' // 确定game serial char serial[5]; if( RES_OK != rom_get_serial(path, serial) ) { RECOVER_PATH; return WAIT_ON_ERRPAGE( display_noserial ); } else RECOVER_PATH; acl_count_t len; if( ACHTLIB_SUCCESS != acl_open_lib( "gba.acl" ) ) { return WAIT_ON_ERRPAGE( display_openlib ); } if( ACHTLIB_SUCCESS != acl_select_game( serial, 0, &len ) ) { acl_close_lib(); return WAIT_ON_ERRPAGE( display_selcht ); } if( len == 0 ) { acl_close_lib(); return WAIT_ON_ERRPAGE( display_nocheat ); } // 显示配置页面 atp_error_t res; atp_itemval_t item; acl_elemlen_t cnt; #define DISP_DONE 0 #define DISP_REGION 1 #define DISP_HOLES 2 #define DISP_KEYS 3 #define SAFE_CALLACL( r ) if( ACHTLIB_SUCCESS!=r ) \ {\ res = WAIT_ON_ERRPAGE( display_selcht );\ break;\ } uint8_t status = DISP_REGION; atp_counter_t defi = 0; acl_entryid_t eid = 0; atp_tips(NULL, "确定A/取消B"); while( status != DISP_DONE ) { if( status == DISP_REGION ) { res = atp_select("请谨慎使用金手指!金手指可能会引起卡顿、死机、损坏存档等现象。           " "选择一个金手指配置:", len, select_region, NULL, NULL, defi, 0, &item ); if( res == ATP_SUCCESS ) { defi = item; acl_chtid_t sid; acl_elemlen_t len; u32 ccid, cclen; SAFE_CALLACL( acl_query_cheat_set((acl_index_t)item, &sid, NULL ) ); SAFE_CALLACL( acl_select_cheat_set( sid ) ); SAFE_CALLACL( acl_select_entry(0, &len) ); if( CCHT_OK == info_current_cheat(&ccid, &cclen) ) { // SKIP the init process when id and len is the same if( ccid != sid || cclen != len ) init_current_cheat( sid, len ); } else init_current_cheat( sid, len ); eid = 0; status = DISP_HOLES; } else if( res == ATP_NO_ACTION) { status = DISP_DONE; } else break; } else if( status == DISP_HOLES ) { SAFE_CALLACL( acl_select_entry(0, &cnt) ); res = atp_select("选择一个金手指项目", cnt, select_holes, NULL, NULL, eid > 0 ? (eid&0xffff)-1 : 0, 0, &item); if( res == ATP_SUCCESS ) { eid = (acl_entryid_t)item; status = DISP_KEYS; } else if( res == ATP_NO_ACTION ) { status = DISP_REGION; } else break; } else // DISP_KEYS { SAFE_CALLACL( acl_select_entry( eid, &cnt ) ); if( cnt == 0 ) { SAFE_CALLACL( acl_select_entry( 1<<16 | eid, &cnt ) ); res = handle_onoff_entry( eid, cnt, &item ); } else { res = atp_select("选择此项目对应的设置", cnt, select_keys, NULL, (atp_callerdata_t)eid, 0, 0, &item); } if( res == ATP_SUCCESS ) { put_current_cheat( (acl_entryid_t)item ); status = DISP_HOLES; } else if( res == ATP_NO_ACTION ) { status = DISP_HOLES; } else break; } } acl_close_lib(); atp_tips( NULL, "指引:按START" ); return WAIT_ON_ACT( res ); } #ifndef NDEBUG else if( y ) { extern u8 dump_patched_rom; dump_patched_rom = 1; } #endif return ATP_PAGE_NOOPTION; } #define PATH_SAME 0 #define PATH_PUSH 1 #define PATH_POP 2 #define DIRUP { \ char *tmpPathPtr = curDir + pathLen; \ while(*--tmpPathPtr != '/'); \ if(*(tmpPathPtr - 1) == ':') tmpPathPtr++; \ *tmpPathPtr = '\0'; \ path_recover = PATH_POP; \ upFrom = tmpPathPtr + 1; \ } Result browseFiles(const char *const basePath, char selected[512]) { if(basePath == NULL || selected == NULL) return RES_INVALID_ARG; // TODO: Check if the base path is empty. char *curDir = (char*)malloc(DIRBUFFSIZE); if(curDir == NULL) return RES_OUT_OF_MEM; safeStrcpy(curDir, basePath, 512); DirList *const dList = (DirList*)malloc(sizeof(DirList)); if(dList == NULL) return RES_OUT_OF_MEM; Result res; if((res = scanDir(curDir, dList, ".gba", NULL, NULL)) != RES_OK) { if( res == RES_OUT_OF_RANGE ) atp_show(1, display_pathfull, NULL); else if( res == RES_PATH_TOO_LONG ) atp_show(1, display_longname, NULL ); else goto end; } const char *upFrom = NULL; int selecting = 0; while( 1 ) { atp_itemval_t value; atp_error_t error; int path_recover = PATH_SAME; const u32 count = dList->num; u32 pathLen = strlen(curDir); if( count > 0 ) { GFX_waitForVBlank0(); hidScanInput(); atp_tips( NULL, "指引:按START" ); void *cust[2] = {dList, curDir}; error = atp_select( curDir, count, display_folder, serve_on_key, (atp_callerdata_t)cust, selecting, 0, &value ); } else error = atp_show( 1, display_empty, NULL ); if( error == ATP_POWER_OFF ) { res = error; break; } else if( error == ATP_SUCCESS ) {// 进入目录或文件 if( count == 0 ) { DIRUP; } else { const char *fname = &dList->ptrs[value][1]; u32 namelen = strlen( fname ) + 1; if( namelen + pathLen > DIRBUFFSIZE-1 ) { atp_show( 1, display_toolong, NULL ); continue; } curDir[pathLen++] = '/'; safeStrcpy( curDir+pathLen, fname, DIRBUFFSIZE - namelen ); if( *dList->ptrs[value] == ENT_TYPE_FILE ) { safeStrcpy( selected, curDir, 512 ); break; } else { path_recover = PATH_PUSH; selecting = 0; upFrom = NULL; } } } else if( error == ATP_NO_ACTION ) {// 上层目录 if( strcmp(curDir, FS_DRIVE_NAMES) == 0 ) { atp_tips("没有上层目录", NULL); help_page( folder_help, sizeof(folder_help)/sizeof(atp_text_t) ); atp_tips("", NULL); } else DIRUP; } if( path_recover ) { if(RES_OK != (res = scanDir(curDir, dList, ".gba", upFrom, &selecting)) ) { if( res == RES_OUT_OF_RANGE ) atp_show( 1, display_pathfull, NULL ); // give a warning for user, and keep show the list else if( res == RES_PATH_TOO_LONG ) atp_show( 1, display_longname, NULL ); else break; } path_recover = PATH_SAME; } } end: free(dList); free(curDir); // Clear screen. screenClean(); return res; }