/*
* 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 "arm_intrinsic.h"
#include "util.h"
#include "drivers/sha.h"
#include "arm11/drivers/hid.h"
#include "drivers/lgy.h"
#include "arm11/drivers/lgyfb.h"
#include "arm11/console.h"
#include "arm11/fmt.h"
#include "drivers/gfx.h"
#include "fs.h"
#include "fsutil.h"
#include "inih/ini.h"
#include "arm11/filebrowser.h"
#include "arm11/drivers/lcd.h"
#include "arm11/gpu_cmd_lists.h"
#include "arm11/drivers/mcu.h"
#include "arm11/acf.h"
#include "arm11/atp.h"
#include "arm11/cheat.h"
#include "kernel.h"
#include "kevent.h"
#include "arm11/drivers/codec.h"
#define OAF_WORK_DIR "sdmc:/3ds/open_agb_firm"
#define OAF_SAVE_DIR "saves" // Relative to work dir.
#define INI_BUF_SIZE (1024u)
#define DEFAULT_CONFIG "[general]\n" \
"backlight=100\n" \
"directBoot=false\n" \
"useGbaDb=true\n\n" \
"[video]\n" \
"scaler=1\n" \
"gbaGamma=2.2\n" \
"lcdGamma=1.54\n" \
"contrast=1.0\n" \
"brightness=0.0\n\n" \
"[advanced]\n" \
"saveOverride=false\n" \
"defaultSave=14"
#define SAVE_POLICY_GBADB 0
#define SAVE_POLICY_FIRM 1
#define SAVE_POLICY_SRAM 2
#define SAVE_POLICY_POPUP 3
#define SAVE_POLICY_SIZE 4
#define CHEAT_INUSE_ADDR (0x3007FE0u)
typedef struct
{
// [general]
u8 backlight; // Both LCDs.
bool directBoot;
bool useGbaDb;
// [video]
u8 scaler; // 0 = 1:1, 1 = bilinear (GPU) x1.5, 2 = matrix (hardware) x1.5.
float gbaGamma;
float lcdGamma;
float contrast;
float brightness;
// [game]
u8 saveSlot;
// TODO: Per-game save type override.
// [advanced]
bool saveOverride;
u16 defaultSave;
// [boost]
u8 savePolicy;
u8 cheatMode;
u16 cheatKeys;
} OafConfig;
typedef struct
{
char name[200];
char serial[4];
u8 sha1[20];
u32 attr;
} GameDbEntry;
// Default config.
static OafConfig g_oafConfig =
{
// [general]
100, // backlight
false, // directBoot
true, // useGbaDb
// [video]
2, // scaler
2.2f, // gbaGamma
1.54f, // lcdGamma
1.f, // contrast
0.f, // brightness
// [game]
0, // saveSlot
// [advanced]
false, // saveOverride
14 // defaultSave
// [boost]
, SAVE_POLICY_GBADB // savePolicy
, CHEAT_MODE_DISABLED// cheatMode
, KEY_L|KEY_R|KEY_DDOWN|KEY_SELECT
};
static KHandle g_frameReadyEvent = 0;
static u16 detect_cheatKey = 0;
// --------------------------
// code for oaf config page
// --------------------------
atp_text_t config_help[] =
{
"全局参数编辑操作指引",
"~ ~ ~ ~ ~ ~ ~",
"-修改后的全局参数保存在config.ini ",
"保存修改时,存档方案的修改不会保存",
"-金手指组合键为下方向+L+R+SELECT",
" 也可以使用HOME键直接代替组合键 ",
"~ ~ ~ ~ ~ ~ ~",
"上下方向键 切换参数项目",
"左右方向键 翻页",
"L键和R键 调整当前参数",
"A键 应用修改退出",
"B键 放弃修改退出",
"X键 应用修改保存退出", // X做保存,选中后走A键流程
};
atp_text_t CONFIG_OUTPUT = "[general]\n" \
"backlight=%d\n" \
"directBoot=%s\n" \
"useGbaDb=true\n\n" \
"[video]\n" \
"scaler=%d\n" \
"gbaGamma=2.2\n" \
"lcdGamma=1.54\n" \
"contrast=1.0\n" \
"brightness=0.0\n\n" \
"[advanced]\n" \
"saveOverride=false\n" \
"defaultSave=14\n\n" \
"[boost]\n" \
"cheatMode=%d\n";
extern atp_error_t help_page( atp_text_t *wording, atp_counter_t length );
int get_cheat_mode()
{
return g_oafConfig.cheatMode;
}
#define LIGHT_MIN (MCU_getSystemModel() > 3 ? 16 : 20)
#define LIGHT_MAX (MCU_getSystemModel() > 3 ? 142 : 117)
#define SCALER_SIZE 4
static atp_error_t config_item( atp_callerdata_t, atp_counter_t index, atp_itemcfg_t *cfg )
{
const char *scaler_val[] = {"上屏无缩放", "上屏GPU放大", "上屏DMA放大", "下屏无缩放"};
const char *savetype_name[] = {"和卡带序列号一致", "读取ROM的特定标记", "汉化带SRAM补丁", "自行决定"};
const char *cheatmode_name[] = {"关闭金手指", "全程激活", "组合键单次激活", "组合键激活/关闭"};
static char buf[16];
cfg->extra_text_color = ATP_COLOR_GREEN;
if( index == 0 )
{
ee_snprintf(buf, sizeof(buf), "%d", g_oafConfig.backlight);
cfg->text = "屏幕亮度";
cfg->extra_text = buf;
if( g_oafConfig.backlight == LIGHT_MIN || g_oafConfig.backlight == LIGHT_MAX )
cfg->extra_text_color = ATP_COLOR_RED;
}
else if( index == 1 )
{
cfg->text = "GAME BOY画面";
cfg->extra_text = g_oafConfig.directBoot ? "跳过" : "显示";
}
else if( index == 2 )
{
cfg->text = "画面输出";
cfg->extra_text = scaler_val[g_oafConfig.scaler];
}
else if( index == 3 )
{
cfg->text = "存档方案";
cfg->extra_text = savetype_name[g_oafConfig.savePolicy];
}
else if( index == 4 )
{
cfg->text = "激活金手指";
cfg->extra_text = cheatmode_name[g_oafConfig.cheatMode];
}
cfg->value = index;
return ATP_SUCCESS;
}
static atp_pageopt_t config_adjust( atp_callerdata_t, atp_counter_t index, atp_boolean_t x, atp_boolean_t, atp_boolean_t l, atp_boolean_t r, atp_boolean_t start, atp_boolean_t )
{
if( l || r )
{
if( index == 0 )
{
u8 light = g_oafConfig.backlight;
if( l )
{
light -= 10;
if( light % 10 ) light += 10 - light%10;
}
else if( r )
{
light += 10;
if( light % 10 ) light -= light % 10;
}
if( light > LIGHT_MAX ) light = LIGHT_MAX;
else if( light < LIGHT_MIN ) light = LIGHT_MIN;
g_oafConfig.backlight = light;
}
else if( index == 1 ) g_oafConfig.directBoot = !g_oafConfig.directBoot;
else if( index == 2 ) g_oafConfig.scaler = ( SCALER_SIZE + ( g_oafConfig.scaler+(l?-1:1) ) ) % SCALER_SIZE;
else if( index == 3 ) g_oafConfig.savePolicy = ( SAVE_POLICY_SIZE + ( g_oafConfig.savePolicy+(l?-1:1) ) ) % SAVE_POLICY_SIZE;
else if( index == 4 ) g_oafConfig.cheatMode = ( CHEAT_MODE_SIZE + (g_oafConfig.cheatMode+(l?-1:1) ) ) % CHEAT_MODE_SIZE;
}
else if( start )
{
atp_tips("", NULL);
return ATP_POWER_OFF == help_page( config_help, sizeof(config_help)/sizeof(atp_text_t) )
? ATP_POWER_OFF : ATP_PAGE_REFRESH;
}
else if( x )
{
int len = strlen(CONFIG_OUTPUT) + 20;
char *data = malloc( len );
if( data == NULL ) return ATP_PAGE_DOSELECT; // only ignore this save
len = ee_snprintf(
data, len, CONFIG_OUTPUT,
g_oafConfig.backlight, g_oafConfig.directBoot ? "true":"false", g_oafConfig.scaler, g_oafConfig.cheatMode
);
fsQuickWrite("config.ini", data, len);
free( data );
return ATP_PAGE_DOSELECT;
}
return ATP_PAGE_UPDATE;
}
atp_error_t oaf_config_page()
{
u8 base[sizeof(g_oafConfig)];
static char title[210];
memcpy( base, &g_oafConfig, sizeof(g_oafConfig) );
OafConfig *prev = (OafConfig*)&base[0];
ee_snprintf(
title, sizeof(title),
"参数配置 当前电量:%3d%%"
"每次开机存档方案重置为“和卡带序列号一致”,这个方案会优先使用游戏最后一次启动时设置的存档类型", MCU_getBatteryLevel());
atp_error_t res = atp_select( title, 5, config_item, config_adjust, NULL, 0, 0, NULL );
if( res == ATP_NO_ACTION )
{
memcpy( &g_oafConfig, prev, sizeof(g_oafConfig) );
return ATP_SUCCESS;
}
else if( res == ATP_SUCCESS )
{
if( prev->backlight != g_oafConfig.backlight )
GFX_setBrightness(g_oafConfig.backlight, g_oafConfig.backlight);
}
return res;
}
// cache the savetype
static u16 from_savetype_cache( char file[512] )
{
FILINFO fi;
int len = strlen( file );
file[len-1] = 'c';
file[len-2] = 't';
file[len-3] = 's';
u16 retval;
if( fStat(file, &fi) == RES_OK )
{
u8 dat[8];
if( RES_OK == fsQuickRead(file, dat, 8 ) )
{
retval = (u16)dat[0];
}
else retval = 0xffff;
}
else retval = 0xffff;
file[len-1] = 'a';
file[len-2] = 'b';
file[len-3] = 'g';
return retval;
}
static void savetype_cache_store( char file[512], u16 savetype )
{
int len = strlen( file );
file[len-1] = 'c';
file[len-2] = 't';
file[len-3] = 's';
u8 dat[8];
dat[0] = (u8)savetype;
fsQuickWrite(file, dat, sizeof(dat));
file[len-1] = 'a';
file[len-2] = 'b';
file[len-3] = 'g';
}
static atp_error_t custom_savetype( atp_callerdata_t, atp_counter_t index, atp_itemcfg_t *cfg )
{
static const char* options[] = {
"EEPROM 8k (0, 1)",
"EEPROM 64k (2, 3)",
"Flash 512k RTC (4, 6, 8)",
"Flash 512k (5, 7, 9)",
"Flash 1m RTC (10, 12)",
"Flash 1m (11, 13)",
"SRAM 256k (14)",
"None (15)"
};
static const u8 cursorSaveTypeLut[8] = {0, 2, 8, 9, 10, 11, 14, 15};
cfg->text = options[index];
cfg->value = cursorSaveTypeLut[index];
return ATP_SUCCESS;
}
static u32 fixRomPadding(u32 romFileSize)
{
// Pad unused ROM area with 0xFFs (trimmed ROMs).
// Smallest retail ROM chip is 8 Mbit (1 MiB).
u32 romSize = nextPow2(romFileSize);
if(romSize < 0x100000u) romSize = 0x100000u;
memset((void*)(ROM_LOC + romFileSize), 0xFFFFFFFFu, romSize - romFileSize);
if(romSize > 0x100000u) // >1 MiB.
{
// Fake "open bus" padding.
u32 padding = (ROM_LOC + romSize) / 2;
padding = __pkhbt(padding, padding + 1, 16); // Copy lower half + 1 to upper half.
for(uintptr_t i = ROM_LOC + romSize; i < ROM_LOC + MAX_ROM_SIZE; i += 4)
{
*(u32*)i = padding;
padding = __uadd16(padding, 0x00020002u); // Unsigned parallel halfword-wise addition.
}
}
else
{
// ROM mirroring (Classic NES Series/possibly others with 8 Mbit ROM).
// Mirror ROM across the entire 32 MiB area.
for(uintptr_t i = ROM_LOC + romSize; i < ROM_LOC + MAX_ROM_SIZE; i += romSize)
{
//memcpy((void*)i, (void*)(i - romSize), romSize); // 0x23A15DD
memcpy((void*)i, (void*)ROM_LOC, romSize); // 0x237109B
}
}
return romSize;
}
static Result loadGbaRom(const char *const path, u32 *const romSizeOut)
{
Result res;
FHandle f;
if((res = fOpen(&f, path, FA_OPEN_EXISTING | FA_READ)) == RES_OK)
{
u32 fileSize;
if((fileSize = fSize(f)) <= MAX_ROM_SIZE)
{
u8 *ptr = (u8*)ROM_LOC;
u32 read;
while((res = fRead(f, ptr, 0x100000u, &read)) == RES_OK && read == 0x100000u)
ptr += 0x100000u;
fClose(f);
// use the gbaatm cheat
if( g_oafConfig.cheatMode != CHEAT_MODE_DISABLED && info_current_cheat(NULL, NULL) != CCHT_NOT_INIT )
{
char gamecode[5];
memcpy( gamecode, ROM_LOC+0xac, 4 );
gamecode[4] = '\0';
acl_open_lib( "gba.acl" );
acl_select_game( gamecode, 0, NULL );
apply_cheat( g_oafConfig.cheatMode, fileSize, g_oafConfig.cheatKeys, CHEAT_INUSE_ADDR, &fileSize );
acl_close_lib();
fini_current_cheat();
}
*romSizeOut = fixRomPadding(fileSize);
}
else
{
res = RES_ROM_TOO_BIG;
fClose(f);
}
}
return res;
}
static u16 checkSaveOverride(u32 gameCode) // Save type overrides for modern homebrew.
{
if( gameCode == 0 )
return 0xFF;
switch (gameCode & 0xFFu)
{
case '1': return SAVE_TYPE_EEPROM_64k; // Homebrew using EEPROM.
case '2': return SAVE_TYPE_SRAM_256k; // Homebrew using SRAM.
case '3': return SAVE_TYPE_FLASH_512k_PSC_RTC; // Homebrew using FLASH-64.
case '4': return SAVE_TYPE_FLASH_1m_MRX_RTC; // Homebrew using FLASH-128.
case 'F': return SAVE_TYPE_EEPROM_8k; // Classic NES Series.
case 'S': return SAVE_TYPE_SRAM_256k; // Homebrew using SRAM (Butano games).
}
if( g_oafConfig.savePolicy != SAVE_POLICY_GBADB ) // skip the mapping
return 0xFF;
Result res;
FHandle f;
GameDbEntry instance;
u32* code;
u32 similarCode = 0xFFFFFFu & gameCode;
u16 similar = 0;
if((res = fOpen(&f, "gba_db.bin", FA_OPEN_EXISTING | FA_READ)) == RES_OK)
{
int count = fSize(f) / sizeof( GameDbEntry );
fLseek(f, 0);
for( int i=0; i < count; ++i )
{
if((res = fRead(f, &instance, sizeof(GameDbEntry), NULL)) != RES_OK) break;
code = (u32*)instance.serial;
if( (*code & 0xFFFFFFu) == similarCode ) {
if( *code == gameCode ){
fClose(f);
return instance.attr & 0xFu;
}
else if( similar == 0 ){
similar = instance.attr & 0xFu;
}
}
}
debug_printf("serial not found: %08lx\n", gameCode);
fClose(f);
}
return similar ? similar : 0xFF;
}
static u16 detectSaveType(u32 romSize)
{
const u32 *romPtr = (u32*)ROM_LOC;
u16 saveType;
if( g_oafConfig.savePolicy == SAVE_POLICY_SRAM )
{
return SAVE_TYPE_SRAM_256k;
}
if((saveType = checkSaveOverride(romPtr[0xAC / 4])) != 0xFF)
{
debug_printf("Serial in override list.\n"
"saveType: %u\n", saveType);
return saveType;
}
// Code based on: https://github.com/Gericom/GBARunner2/blob/master/arm9/source/save/Save.vram.cpp
romPtr += 0xE4 / 4; // Skip headers.
const u16 defaultSave = g_oafConfig.defaultSave;
if(defaultSave > SAVE_TYPE_NONE)
saveType = SAVE_TYPE_NONE;
else
saveType = defaultSave;
for(; romPtr < (u32*)(ROM_LOC + romSize); romPtr++)
{
u32 tmp = *romPtr;
// "EEPR" "FLAS" "SRAM"
if(tmp == 0x52504545u || tmp == 0x53414C46u || tmp == 0x4D415253u)
{
static const struct
{
const char *str;
u16 saveType;
} saveTypeLut[25] =
{
// EEPROM
// Assume common sizes for popular games to aid ROM hacks.
{"EEPROM_V111", SAVE_TYPE_EEPROM_8k},
{"EEPROM_V120", SAVE_TYPE_EEPROM_8k},
{"EEPROM_V121", SAVE_TYPE_EEPROM_64k},
{"EEPROM_V122", SAVE_TYPE_EEPROM_8k},
{"EEPROM_V124", SAVE_TYPE_EEPROM_64k},
{"EEPROM_V125", SAVE_TYPE_EEPROM_8k},
{"EEPROM_V126", SAVE_TYPE_EEPROM_8k},
// FLASH
// Assume they all have RTC.
{"FLASH_V120", SAVE_TYPE_FLASH_512k_PSC_RTC},
{"FLASH_V121", SAVE_TYPE_FLASH_512k_PSC_RTC},
{"FLASH_V123", SAVE_TYPE_FLASH_512k_PSC_RTC},
{"FLASH_V124", SAVE_TYPE_FLASH_512k_PSC_RTC},
{"FLASH_V125", SAVE_TYPE_FLASH_512k_PSC_RTC},
{"FLASH_V126", SAVE_TYPE_FLASH_512k_PSC_RTC},
{"FLASH512_V130", SAVE_TYPE_FLASH_512k_PSC_RTC},
{"FLASH512_V131", SAVE_TYPE_FLASH_512k_PSC_RTC},
{"FLASH512_V133", SAVE_TYPE_FLASH_512k_PSC_RTC},
{"FLASH1M_V102", SAVE_TYPE_FLASH_1m_MRX_RTC},
{"FLASH1M_V103", SAVE_TYPE_FLASH_1m_MRX_RTC},
// FRAM & SRAM
{"SRAM_F_V100", SAVE_TYPE_SRAM_256k},
{"SRAM_F_V102", SAVE_TYPE_SRAM_256k},
{"SRAM_F_V103", SAVE_TYPE_SRAM_256k},
{"SRAM_V110", SAVE_TYPE_SRAM_256k},
{"SRAM_V111", SAVE_TYPE_SRAM_256k},
{"SRAM_V112", SAVE_TYPE_SRAM_256k},
{"SRAM_V113", SAVE_TYPE_SRAM_256k}
};
for(u32 i = 0; i < 25; i++)
{
const char *const str = saveTypeLut[i].str;
u16 tmpSaveType = saveTypeLut[i].saveType;
if(memcmp(romPtr, str, strlen(str)) == 0)
{
if(tmpSaveType == SAVE_TYPE_EEPROM_8k || tmpSaveType == SAVE_TYPE_EEPROM_64k)
{
// If ROM bigger than 16 MiB --> SAVE_TYPE_EEPROM_8k_2 or SAVE_TYPE_EEPROM_64k_2.
if(romSize > 0x1000000) tmpSaveType++;
}
debug_printf("SDK save string: %s\n"
"saveType: %u\n", str, tmpSaveType);
return tmpSaveType;
}
}
}
}
debug_printf("saveType: %u\n", saveType);
return saveType;
}
// Search for entry with first u64 of the SHA1 = x using binary search.
static Result searchGbaDb(u64 x, GameDbEntry *const db, s32 *const entryPos)
{
debug_printf("Database search: '%016" PRIX64 "'\n", __builtin_bswap64(x));
Result res;
FHandle f;
if((res = fOpen(&f, "gba_db.bin", FA_OPEN_EXISTING | FA_READ)) == RES_OK)
{
s32 l = 0;
s32 r = fSize(f) / sizeof(GameDbEntry) - 1; // TODO: Check for 0!
while(1)
{
const s32 mid = l + (r - l) / 2;
if((res = fLseek(f, sizeof(GameDbEntry) * mid)) != RES_OK) break;
if((res = fRead(f, db, sizeof(GameDbEntry), NULL)) != RES_OK) break;
const u64 tmp = *(u64*)db->sha1; // Unaligned access.
if(tmp == x)
{
*entryPos = mid; // TODO: Remove.
break;
}
if(r <= l)
{
debug_printf("Not found!");
res = RES_NOT_FOUND;
break;
}
if(tmp > x) r = mid - 1;
else l = mid + 1;
}
fClose(f);
}
return res;
}
static u16 getSaveType(u32 romSize, const char *const savePath)
{
FILINFO fi;
const bool saveOverride = g_oafConfig.saveOverride;
const u16 autoSaveType = detectSaveType(romSize);
//const bool saveExists = fStat(savePath, &fi) == RES_OK;
u64 sha1[3];
sha((u32*)ROM_LOC, romSize, (u32*)sha1, SHA_IN_BIG | SHA_1_MODE, SHA_OUT_BIG);
Result res;
GameDbEntry dbEntry;
s32 dbPos = -1;
u16 saveType = SAVE_TYPE_NONE;
res = searchGbaDb(*sha1, &dbEntry, &dbPos);
if(res == RES_OK) saveType = dbEntry.attr & 0xFu;
else if(!saveOverride && res == RES_NOT_FOUND) return autoSaveType;
else if(res != RES_NOT_FOUND)
{
//ee_puts("Could not access gba_db.bin! Press any button to continue.");
printErrorWaitInput(res, 0);
return autoSaveType;
}
//debug_printf("saveType: %u\n", saveType);
if(saveOverride)
{
u8 cursor;
static const u8 saveTypeCursorLut[16] = {0, 0, 1, 1, 2, 3, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7};
if(!g_oafConfig.useGbaDb || res == RES_NOT_FOUND)
cursor = saveTypeCursorLut[autoSaveType];
else cursor = saveTypeCursorLut[saveType];
atp_itemval_t target_type;
atp_error_t err = atp_select(
"请选择此游戏的存档类型",
8,
custom_savetype,
NULL, NULL,
(atp_counter_t)cursor, 1, &target_type
);
if( err == ATP_POWER_OFF ) goto end;
/*consoleClear();
ee_printf("==Save Type Override Menu==\n"
"Save file: %s\n"
"Save type (autodetected): %u\n"
"Save type (from gba_db.bin): ", (saveExists ? "Found" : "Not found"), autoSaveType);
if(res == RES_NOT_FOUND)
ee_puts("Not found");
else
ee_printf("%u\n", saveType);
ee_puts("\n"
"=Save Types=\n"
" EEPROM 8k (0, 1)\n"
" EEPROM 64k (2, 3)\n"
" Flash 512k RTC (4, 6, 8)\n"
" Flash 512k (5, 7, 9)\n"
" Flash 1m RTC (10, 12)\n"
" Flash 1m (11, 13)\n"
" SRAM 256k (14)\n"
" None (15)\n\n"
"=Controls=\n"
"Up/Down: Navigate\n"
"A: Select\n"
"X: Delete save file");
static const u8 saveTypeCursorLut[16] = {0, 0, 1, 1, 2, 3, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7};
u8 oldCursor = 0;
u8 cursor;
if(!g_oafConfig.useGbaDb || res == RES_NOT_FOUND)
cursor = saveTypeCursorLut[autoSaveType];
else
cursor = saveTypeCursorLut[saveType];
while(1)
{
ee_printf("\x1b[%u;H ", oldCursor + 6);
ee_printf("\x1b[%u;H>", cursor + 6);
oldCursor = cursor;
u32 kDown;
do
{
GFX_waitForVBlank0();
hidScanInput();
if(hidGetExtraKeys(0) & (KEY_POWER_HELD | KEY_POWER)) goto end;
kDown = hidKeysDown();
} while(kDown == 0);
if((kDown & KEY_DUP) && cursor > 0) cursor--;
else if((kDown & KEY_DDOWN) && cursor < 7) cursor++;
else if(kDown & KEY_X)
{
fUnlink(savePath);
ee_printf("\x1b[1;11HDeleted ");
}
else if(kDown & KEY_A) break;
}
//static const u8 cursorSaveTypeLut[8] = {0, 2, 8, 9, 10, 11, 14, 15};*/
saveType = (u16)target_type;// cursorSaveTypeLut[cursor];
if(saveType == SAVE_TYPE_EEPROM_8k || saveType == SAVE_TYPE_EEPROM_64k)
{
// If ROM bigger than 16 MiB --> SAVE_TYPE_EEPROM_8k_2 or SAVE_TYPE_EEPROM_64k_2.
if(romSize > 0x1000000) saveType++;
}
}
end:
return saveType;
}
static void adjustGammaTableForGba(void)
{
const float gbaGamma = g_oafConfig.gbaGamma;
const float lcdGamma = g_oafConfig.lcdGamma;
const float contrast = g_oafConfig.contrast;
const float brightness = g_oafConfig.brightness;
for(u32 i = 0; i < 256; i++)
{
// Credits for this algo go to Extrems.
// Originally from Game Boy Interface Standard Edition for the GameCube.
u32 res = powf(powf(contrast, gbaGamma) * powf((float)i / 255.0f + brightness / contrast, gbaGamma),
1.0f / lcdGamma) * 255.0f;
// Same adjustment for red/green/blue.
REG_LCD_PDC0_GTBL_FIFO = res<<16 | res<<8 | res;
REG_LCD_PDC1_GTBL_FIFO = res<<16 | res<<8 | res;
}
}
/**
static Result dumpFrameTex(void)
{
// 512x-512 (hight negative to flip vertically).
// Pixels at offset 0x40.
alignas(4) static const u8 bmpHeader[54] =
{
0x42, 0x4D, 0x40, 0x00, 0x0C, 0x00, 0x00, 0x00,
0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xFE,
0xFF, 0xFF, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x13, 0x0B,
0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
//GX_displayTransfer((u32*)0x18200000, 160u<<16 | 256u, (u32*)0x18400000, 160u<<16 | 256u, 1u<<12 | 1u<<8);
GFX_waitForPPF();
//fsQuickWrite("sdmc:/lgyfb_dbg_frame.bgr", (void*)0x18400000, 256 * 160 * 3);
GX_displayTransfer((u32*)0x18200000, 240u<<16 | 512u, (u32*)0x18400040, 240u<<16 | 512u, 1u<<12 | 1u<<8);
GFX_waitForPPF();
memcpy((void*)0x18400000, bmpHeader, sizeof(bmpHeader));
return fsQuickWrite("texture_dump.bmp", (void*)0x18400000, 0x40 + 512 * 512 * 3);
}*/
static void gbaGfxHandler(void *args)
{
const KHandle event = (KHandle)args;
while(1)
{
if(waitForEvent(event) != KRES_OK) break;
clearEvent(event);
// Rotate the frame using the GPU.
// 240x160: TODO.
// 360x240: about 0.623620315 ms.
static bool inited = false;
u32 listSize;
const u32 *list;
if(inited == false)
{
inited = true;
listSize = sizeof(gbaGpuInitList);
list = (u32*)gbaGpuInitList;
}
else
{
listSize = sizeof(gbaGpuList2);
list = (u32*)gbaGpuList2;
}
GX_processCommandList(listSize, list);
GFX_waitForP3D();
// 地址0x18180000保存的是400x240大小的贴图数据rgb888(序列:↑↘↑)
// 地址0x18200000保存的是512x512大小的GBA的240x160贴图数据(序列:→↙→)
// 所以GPU做的事情就是将18200000的数据逆时针旋转90°
if( g_oafConfig.scaler == 0 )
{
int base_offset = 96; // 3ds高240, gba高160,居中需要平移40个像素,rgb888每个像素3字节,共120字节
// 但是,gpu的transfer engine只支持16字节对齐的地址。所以只能选择16和3的公约数,就是48,96,144,192,240
GX_displayTransfer((u32*)(0x18180000 + 16*240*3), 368u<<16 | 240u,
GFX_getFramebuffer(SCREEN_TOP) + 16*240*3 + base_offset, 368u<<16 | 240u, 1u<<12 | 1u<<8);
}
else if( g_oafConfig.scaler == 3 )
{
int base_offset = 144;
GX_displayTransfer((u32*)(0x18180000 + 40*240*3), 320u<<16 | 240u,
GFX_getFramebuffer(SCREEN_BOT) + base_offset, 320u<<16 | 240u, 1u<<12 | 1u<<8);
}
else GX_displayTransfer((u32*)(0x18180000 + 16*240*3), 368u<<16 | 240u,
GFX_getFramebuffer(SCREEN_TOP) + 16*240*3, 368u<<16 | 240u, 1u<<12 | 1u<<8);
GFX_waitForPPF();
GFX_swapFramebufs();
//if(hidKeysDown() == (KEY_Y | KEY_SELECT)) dumpFrameTex();
CODEC_soundSwitchOutput();
}
taskExit();
}
static int cfgIniCallback(void* user, const char* section, const char* name, const char* value)
{
OafConfig *const config = (OafConfig*)user;
if(strcmp(section, "general") == 0)
{
if(strcmp(name, "backlight") == 0)
config->backlight = (u8)strtoul(value, NULL, 10);
else if(strcmp(name, "directBoot") == 0)
config->directBoot = (strcmp(value, "false") == 0 ? false : true);
else if(strcmp(name, "useGbaDb") == 0)
config->useGbaDb = (strcmp(value, "true") == 0 ? true : false);
}
else if(strcmp(section, "video") == 0)
{
if(strcmp(name, "scaler") == 0)
config->scaler = (u8)strtoul(value, NULL, 10);
else if(strcmp(name, "gbaGamma") == 0)
config->gbaGamma = str2float(value);
else if(strcmp(name, "lcdGamma") == 0)
config->lcdGamma = str2float(value);
else if(strcmp(name, "contrast") == 0)
config->contrast = str2float(value);
else if(strcmp(name, "brightness") == 0)
config->brightness = str2float(value);
}
else if(strcmp(section, "game") == 0)
{
if(strcmp(name, "saveSlot") == 0)
config->saveSlot = (u8)strtoul(value, NULL, 10);
}
else if(strcmp(section, "advanced") == 0)
{
if(strcmp(name, "saveOverride") == 0)
config->saveOverride = (strcmp(value, "false") == 0 ? false : true);
if(strcmp(name, "defaultSave") == 0)
config->defaultSave = (u16)strtoul(value, NULL, 10);
}
else if(strcmp(section, "boost") == 0)
{
if( strcmp(name, "cheatMode") == 0 )
config->cheatMode = (u8)strtoul(value, NULL, 10);
if( config->cheatMode >= CHEAT_MODE_SIZE ) config->cheatMode = CHEAT_MODE_DISABLED;
}
else return 0; // Error.
return 1; // 1 is no error? Really?
}
static Result parseOafConfig(const char *const path, const bool writeDefaultCfg)
{
char *iniBuf = (char*)calloc(INI_BUF_SIZE, 1);
if(iniBuf == NULL) return RES_OUT_OF_MEM;
Result res = fsQuickRead(path, iniBuf, INI_BUF_SIZE - 1);
if(res == RES_OK) ini_parse_string(iniBuf, cfgIniCallback, &g_oafConfig);
else if(writeDefaultCfg)
{
const char *const defaultConfig = DEFAULT_CONFIG;
res = fsQuickWrite(path, defaultConfig, strlen(defaultConfig));
}
free(iniBuf);
return res;
}
static Result showFileBrowser(char romAndSavePath[512])
{
Result res;
char *lastDir = (char*)calloc(512, 1);
if(lastDir != NULL)
{
do
{
// Get last ROM launch path.
if((res = fsLoadPathFromFile("lastdir.txt", lastDir)) != FR_OK)
{
if(res == RES_FR_NO_FILE) strcpy(lastDir, "sdmc:/");
else break;
}
// Show file browser.
*romAndSavePath = '\0';
if((res = browseFiles(lastDir, romAndSavePath)) == RES_FR_NO_PATH)
{
// Second chance in case the last dir has been deleted.
strcpy(lastDir, "sdmc:/");
if((res = browseFiles(lastDir, romAndSavePath)) != RES_OK) break;
}
else if(res != RES_OK) break;
size_t cmpLen = strrchr(romAndSavePath, '/') - romAndSavePath;
if((size_t)(strchr(romAndSavePath, '/') - romAndSavePath) == cmpLen) cmpLen++; // Keep the first '/'.
if(cmpLen < 512)
{
if(cmpLen < strlen(lastDir) || strncmp(lastDir, romAndSavePath, cmpLen) != 0)
{
strncpy(lastDir, romAndSavePath, cmpLen);
lastDir[cmpLen] = '\0';
res = fsQuickWrite("lastdir.txt", lastDir, cmpLen + 1);
}
}
} while(0);
free(lastDir);
}
else res = RES_OUT_OF_MEM;
return res;
}
static void rom2GameCfgPath(char romPath[512])
{
// Extract the file name and change the extension.
// For cfg2SavePath() we need to reserve 2 extra bytes/chars.
char tmpSaveFileName[256];
safeStrcpy(tmpSaveFileName, strrchr(romPath, '/') + 1, 256 - 2);
strcpy(tmpSaveFileName + strlen(tmpSaveFileName) - 4, ".ini");
// Construct the new path.
strcpy(romPath, OAF_SAVE_DIR "/");
strcat(romPath, tmpSaveFileName);
}
static void gameCfg2SavePath(char cfgPath[512], const u8 saveSlot)
{
if(saveSlot > 9)
{
*cfgPath = '\0'; // Prevent using the ROM as save file.
return;
}
static char numberedExt[7] = {'.', 'X', '.', 's', 'a', 'v', '\0'};
// Change the extension.
// This relies on rom2GameCfgPath() to reserve 2 extra bytes/chars.
numberedExt[1] = '0' + saveSlot;
strcpy(cfgPath + strlen(cfgPath) - 4, (saveSlot == 0 ? ".sav" : numberedExt));
}
Result oafParseConfigEarly(void)
{
Result res;
do
{
// Create the work dir and switch to it.
if((res = fsMakePath(OAF_WORK_DIR)) != RES_OK && res != RES_FR_EXIST) break;
if((res = fChdir(OAF_WORK_DIR)) != RES_OK) break;
// Create the saves folder.
if((res = fMkdir(OAF_SAVE_DIR)) != RES_OK && res != RES_FR_EXIST) break;
if((res = acf_initialize("wqy11.fnt")) != RES_OK ) break;
// Parse the config.
res = parseOafConfig("config.ini", true);
} while(0);
return res;
}
u8 oafGetBacklightConfig(void)
{
return g_oafConfig.backlight;
}
Result oafInitAndRun(void)
{
Result res;
char *const filePath = (char*)calloc(512, 1);
if(filePath != NULL)
{
do
{
// Try to load the ROM path from autoboot.txt.
// If this file doesn't exist show the file browser.
if((res = fsLoadPathFromFile("autoboot.txt", filePath)) == RES_FR_NO_FILE)
{
if((res = showFileBrowser(filePath)) != RES_OK || *filePath == '\0') break;
ee_puts("Loading...");
}
else if(res != RES_OK) break;
// Load the ROM file.
u32 romSize;
if((res = loadGbaRom(filePath, &romSize)) != RES_OK) break;
// Load the per-game config.
rom2GameCfgPath(filePath);
if((res = parseOafConfig(filePath, false)) != RES_OK && res != RES_FR_NO_FILE) break;
// Adjust the path for the save file and get save type.
u16 saveType = from_savetype_cache(filePath);
if( saveType > SAVE_TYPE_MASK || g_oafConfig.savePolicy >= SAVE_POLICY_FIRM )
{
if( g_oafConfig.savePolicy == SAVE_POLICY_POPUP )
g_oafConfig.saveOverride = true;
else g_oafConfig.saveOverride = g_oafConfig.useGbaDb = false;
if(g_oafConfig.useGbaDb || g_oafConfig.saveOverride)
saveType = getSaveType(romSize, filePath);
else saveType = detectSaveType(romSize);
savetype_cache_store(filePath, saveType);
}
gameCfg2SavePath(filePath, g_oafConfig.saveSlot);
// Prepare ARM9 for GBA mode + save loading.
if((res = LGY_prepareGbaMode(g_oafConfig.directBoot, saveType, filePath)) == RES_OK)
{
//ee_puts("\x1b[2J");
detect_cheatKey = (g_oafConfig.cheatMode == CHEAT_MODE_ENABYKEY || g_oafConfig.cheatMode == CHEAT_MODE_KEYONOFF) ? g_oafConfig.cheatKeys : 0;
#ifdef NDEBUG
// Force black and turn the backlight off on the bottom screen.
// Don't turn the backlight off on 2DS (1 panel).
if( g_oafConfig.scaler==3 ) GFX_setForceBlack(true, false);
else GFX_setForceBlack(false, true);
if(MCU_getSystemModel() != 3) GFX_powerOffBacklights(g_oafConfig.scaler==3 ? GFX_BLIGHT_TOP : GFX_BLIGHT_BOT);
#endif
// Initialize the legacy frame buffer and frame handler.
const KHandle frameReadyEvent = createEvent(false);
{
LGYFB_init(frameReadyEvent, g_oafConfig.scaler); // 这里把GBA的输出转换成0x18200000处512x512大小的纹理
patchGbaGpuCmdList(g_oafConfig.scaler);
if( g_oafConfig.scaler == 3u ) GFX_setFramebufFmt(GFX_BGR8, GFX_BGR8);
}
createTask(0x800, 3, gbaGfxHandler, (void*)frameReadyEvent);
g_frameReadyEvent = frameReadyEvent;
// Adjust gamma table and sync LgyFb start with LCD VBlank.
adjustGammaTableForGba();
GFX_waitForVBlank0();
LGY_switchMode();
}
} while(0);
}
else res = RES_OUT_OF_MEM;
free(filePath);
return res;
}
void oafUpdate(void)
{
if( detect_cheatKey && (hidGetExtraKeys(0) & KEY_HOME) ) // press home as cheat keys
{
vu16 *hid_set = (vu16*)0x10141112;
vu16 *hid_mode = (vu16*)0x10141110;
*hid_mode = detect_cheatKey;
*hid_set = ~detect_cheatKey;
}
else LGY_handleOverrides();
waitForEvent(g_frameReadyEvent);
}
void oafFinish(void)
{
LGYFB_deinit();
if(g_frameReadyEvent != 0)
{
deleteEvent(g_frameReadyEvent); // gbaGfxHandler() will automatically terminate.
g_frameReadyEvent = 0;
}
LGY_deinit();
}