open_agb_firm/source/arm11/open_agb_firm.c

378 lines
9.4 KiB
C

/*
* This file is part of open_agb_firm
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
*/
#include <stdlib.h>
#include <string.h>
#include "types.h"
#include "util.h"
#include "arm11/fast_rom_padding.h"
#include "oaf_error_codes.h"
#include "fs.h"
#include "arm11/fmt.h"
#include "arm11/drivers/mcu.h"
#include "drivers/gfx.h"
#include "arm11/drivers/hid.h"
#include "fsutil.h"
#include "arm11/filebrowser.h"
#include "arm11/config.h"
#include "arm11/save_type.h"
#include "arm11/patch.h"
#include "arm11/drivers/codec.h"
#include "drivers/lgy_common.h"
#include "arm11/oaf_video.h"
#include "arm11/drivers/lgy11.h"
#include "kernel.h"
#include "kevent.h"
static KHandle g_frameReadyEvent = 0;
static u32 fixRomPadding(const u32 romFileSize)
{
// Pad unused ROM area with 0xFFs (trimmed ROMs).
// Smallest retail ROM chip is 8 Mbit (1 MiB).
u32 romSize = nextPow2(romFileSize);
romSize = (romSize < 0x100000 ? 0x100000 : romSize);
const uintptr_t romLoc = LGY_ROM_LOC;
memset((void*)(romLoc + romFileSize), 0xFF, romSize - romFileSize);
u32 mirroredSize = romSize;
if(romSize == 0x100000) // 1 MiB.
{
// ROM mirroring for Classic NES Series/others with 8 Mbit ROM.
// The ROM is mirrored exactly 4 times.
// Thanks to endrift for discovering this.
mirroredSize = 0x400000; // 4 MiB.
uintptr_t mirrorLoc = romLoc + romSize;
do
{
memcpy((void*)mirrorLoc, (void*)romLoc, romSize);
mirrorLoc += romSize;
} while(mirrorLoc < romLoc + mirroredSize);
}
// Fake "open bus" padding.
if(romSize < LGY_MAX_ROM_SIZE)
makeOpenBusPaddingFast((u32*)(romLoc + mirroredSize));
// We don't return the mirrored size because the db hashes are over unmirrored dumps.
return romSize;
}
static Result loadGbaRom(const char *const path, u32 *const romSizeOut)
{
FHandle f;
Result res = fOpen(&f, path, FA_OPEN_EXISTING | FA_READ);
if(res == RES_OK)
{
u32 fileSize = fSize(f);
if(fileSize > LGY_MAX_ROM_SIZE)
{
fileSize = LGY_MAX_ROM_SIZE;
ee_puts("Warning: ROM file is too big. Expect crashes.");
}
u32 read;
res = fRead(f, (void*)LGY_ROM_LOC, fileSize, &read);
fClose(f);
if(read == fileSize) *romSizeOut = fixRomPadding(fileSize);
}
return res;
}
void changeBacklight(s16 amount)
{
u8 min, max;
if(MCU_getSystemModel() >= 4)
{
min = 16;
max = 142;
}
else
{
min = 20;
max = 117;
}
s16 newVal = g_oafConfig.backlight + amount;
newVal = (newVal > max ? max : newVal);
newVal = (newVal < min ? min : newVal);
g_oafConfig.backlight = (u8)newVal;
GFX_setLcdLuminance(newVal);
}
static void updateBacklight(void)
{
// Check for special button combos.
const u32 kHeld = hidKeysHeld();
static bool backlightOn = true;
if(hidKeysDown() && kHeld)
{
// Adjust LCD brightness up.
const s16 steps = g_oafConfig.backlightSteps;
if(kHeld == (KEY_X | KEY_DUP))
changeBacklight(steps);
// Adjust LCD brightness down.
if(kHeld == (KEY_X | KEY_DDOWN))
changeBacklight(-steps);
// Disable backlight switching in debug builds on 2DS.
const GfxBl lcd = (MCU_getSystemModel() != SYS_MODEL_2DS ? GFX_BL_TOP : GFX_BL_BOT);
#ifndef NDEBUG
if(lcd != GFX_BL_BOT)
#endif
{
// Turn off backlight.
if(backlightOn && kHeld == (KEY_X | KEY_DLEFT))
{
backlightOn = false;
GFX_powerOffBacklight(lcd);
}
// Turn on backlight.
if(!backlightOn && kHeld == (KEY_X | KEY_DRIGHT))
{
backlightOn = true;
GFX_powerOnBacklight(lcd);
}
}
}
}
static Result showFileBrowser(char romAndSavePath[512])
{
Result res;
char *lastDir = (char*)calloc(512, 1);
if(lastDir != NULL)
{
do
{
// Get last ROM launch path.
res = fsLoadPathFromFile("lastdir.txt", lastDir);
if(res != RES_OK)
{
if(res == RES_FR_NO_FILE) strcpy(lastDir, "sdmc:/");
else break;
}
// Show file browser.
*romAndSavePath = '\0';
res = browseFiles(lastDir, romAndSavePath);
if(res == RES_FR_NO_PATH)
{
// Second chance in case the last dir has been deleted.
strcpy(lastDir, "sdmc:/");
res = browseFiles(lastDir, romAndSavePath);
if(res != 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])
{
if (g_oafConfig.useSavesFolder)
{
// Extract the file name and change the extension.
// For cfg2SavePath() we need to reserve 2 extra bytes/chars.
char tmpIniFileName[256];
safeStrcpy(tmpIniFileName, strrchr(romPath, '/') + 1, 256 - 2);
strcpy(tmpIniFileName + strlen(tmpIniFileName) - 4, ".ini");
// Construct the new path.
strcpy(romPath, OAF_SAVE_DIR "/");
strcat(romPath, tmpIniFileName);
}
else
{
// Change the extension to .ini.
strcpy(romPath + strlen(romPath) - 4, ".ini");
}
}
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.
res = fsMakePath(OAF_WORK_DIR);
if(res != RES_OK && res != RES_FR_EXIST) break;
res = fChdir(OAF_WORK_DIR);
if(res != RES_OK) break;
// Create the saves folder.
res = fMkdir(OAF_SAVE_DIR);
if(res != RES_OK && res != RES_FR_EXIST) break;
// Create screenshots folder.
res = fMkdir(OAF_SCREENSHOT_DIR);
if(res != RES_OK && res != RES_FR_EXIST) break;
// Parse the config.
res = parseOafConfig("config.ini", &g_oafConfig, true);
} while(0);
return res;
}
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.
res = fsLoadPathFromFile("autoboot.txt", filePath);
if(res == RES_FR_NO_FILE)
{
res = showFileBrowser(filePath);
if(res != RES_OK || *filePath == '\0') break;
ee_puts("Loading...");
}
else if(res != RES_OK) break;
//make copy of rom path
char *const romFilePath = (char*)calloc(strlen(filePath)+1, 1);
if(romFilePath == NULL) { res = RES_OUT_OF_MEM; break; }
strcpy(romFilePath, filePath);
// Load the ROM file.
u32 romSize;
res = loadGbaRom(filePath, &romSize);
if(res != RES_OK) break;
// Load the per-game config.
rom2GameCfgPath(filePath);
res = parseOafConfig(filePath, &g_oafConfig, false);
if(res != RES_OK && res != RES_FR_NO_FILE) break;
// Adjust the path for the save file and get save type.
gameCfg2SavePath(filePath, g_oafConfig.saveSlot);
u16 saveType;
if(g_oafConfig.saveType != 0xFF)
saveType = g_oafConfig.saveType;
else if(g_oafConfig.useGbaDb || g_oafConfig.saveOverride)
saveType = getSaveType(&g_oafConfig, romSize, filePath);
else
saveType = detectSaveType(romSize, g_oafConfig.defaultSave);
patchRom(romFilePath, &romSize);
free(romFilePath);
// Set audio output and volume.
CODEC_setAudioOutput(g_oafConfig.audioOut);
CODEC_setVolumeOverride(g_oafConfig.volume);
// Prepare ARM9 for GBA mode + save loading.
res = LGY_prepareGbaMode(g_oafConfig.directBoot, saveType, filePath);
if(res == RES_OK)
{
// Initialize video output (frame capture, post processing ect.).
g_frameReadyEvent = OAF_videoInit();
// Setup button overrides.
const u32 *const maps = g_oafConfig.buttonMaps;
u16 overrides = 0;
for(unsigned i = 0; i < 10; i++)
if(maps[i] != 0) overrides |= 1u<<i;
LGY11_selectInput(overrides);
// Sync LgyCap start with LCD VBlank.
GFX_waitForVBlank0();
LGY11_switchMode();
}
} while(0);
}
else res = RES_OUT_OF_MEM;
free(filePath);
return res;
}
void oafUpdate(void)
{
const u32 *const maps = g_oafConfig.buttonMaps;
const u32 kHeld = hidKeysHeld();
u16 pressed = 0;
for(unsigned i = 0; i < 10; i++)
{
if((kHeld & maps[i]) != 0)
pressed |= 1u<<i;
}
LGY11_setInputState(pressed);
CODEC_runHeadphoneDetection();
updateBacklight();
waitForEvent(g_frameReadyEvent);
clearEvent(g_frameReadyEvent);
}
void oafFinish(void)
{
// frameReadyEvent deleted by this function.
OAF_videoExit();
g_frameReadyEvent = 0;
LGY11_deinit();
}