Merge branch 'master' (early part) into medusa

This commit is contained in:
Vicki Pfau 2023-02-15 01:50:55 -08:00
commit 47de096841
49 changed files with 1874 additions and 122 deletions

View File

@ -41,15 +41,21 @@ Misc:
0.11.0: (Future)
Features:
- New unlicensed GB mappers: NT (older types 1 and 2), Li Cheng
- New unlicensed GB mappers: NT (older types 1 and 2), Li Cheng, GGB-81
- Debugger: Add range watchpoints
Emulation fixes:
- GB Serialize: Don't write BGP/OBP when loading SCGB state (fixes mgba.io/i/2694)
Other fixes:
- Qt: Manually split filename to avoid overzealous splitting (fixes mgba.io/i/2681)
- Qt: Expand criteria for tag branch names (fixes mgba.io/i/2679)
- Qt: Fix scanning specific e-Reader dotcodes (fixes mgba.io/i/2693)
- Res: Fix species name location in Ruby/Sapphire revs 1/2 (fixes mgba.io/i/2685)
- VFS: Fix minizip write returning 0 on success instead of size
Misc:
- GB Serialize: Add missing savestate support for MBC6 and NT (newer)
- macOS: Add category to plist (closes mgba.io/i/2691)
- macOS: Fix modern build with libepoxy (fixes mgba.io/i/2700)
- Qt: Keep track of current pslette preset name (fixes mgba.io/i/2680)
0.10.0: (2022-10-11)
Features:

View File

@ -33,7 +33,7 @@ if(NOT MSVC)
# mingw32 likes to complain about using the "wrong" format strings despite them actually working
set(WARNING_FLAGS "${WARNING_FLAGS} -Wno-format")
endif()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WARNING_FLAGS} -Werror=implicit-function-declaration")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WARNING_FLAGS} -Werror=implicit-function-declaration -Werror=implicit-int")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS}")
else()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_CRT_SECURE_NO_WARNINGS /wd4003 /wd4244 /wd4146 /wd4267 /Zc:preprocessor-")
@ -735,8 +735,12 @@ if (USE_LZMA)
endif()
if(USE_EPOXY)
list(APPEND FEATURE_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/opengl/gl.c ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/opengl/gles2.c)
list(APPEND FEATURE_DEFINES BUILD_GL BUILD_GLES2 BUILD_GLES3)
if(NOT APPLE OR NOT MACOSX_SDK VERSION_GREATER 10.14)
list(APPEND FEATURE_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/opengl/gl.c)
list(APPEND FEATURE_DEFINES BUILD_GL)
endif()
list(APPEND FEATURE_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/opengl/gles2.c)
list(APPEND FEATURE_DEFINES BUILD_GLES2 BUILD_GLES3)
list(APPEND FEATURES EPOXY)
include_directories(AFTER ${EPOXY_INCLUDE_DIRS})
link_directories(${EPOXY_LIBRARY_DIRS})

View File

@ -51,6 +51,7 @@ The following mappers are fully supported:
- MBC2
- MBC3
- MBC3+RTC
- MBC30
- MBC5
- MBC5+Rumble
- MBC7
@ -71,6 +72,7 @@ The following mappers are partially supported:
- Sachen MMC2 (missing alternate wiring support)
- BBD (missing logo switching)
- Hitek (missing logo switching)
- GGB-81 (missing logo switching)
- Li Cheng (missing logo switching)
### Planned features

30
include/mgba-util/sfo.h Normal file
View File

@ -0,0 +1,30 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef SFO_H
#define SFO_H
#include <mgba-util/common.h>
CXX_GUARD_START
#include <mgba-util/table.h>
void SfoInit(struct Table* sfo);
static inline void SfoDeinit(struct Table* sfo) {
HashTableDeinit(sfo);
}
struct VFile;
bool SfoWrite(struct Table* sfo, struct VFile* vf);
bool SfoAddU32Value(struct Table* sfo, const char* name, uint32_t value);
bool SfoAddStrValue(struct Table* sfo, const char* name, const char* value);
bool SfoSetTitle(struct Table* sfo, const char* title);
CXX_GUARD_END
#endif

View File

@ -47,6 +47,7 @@ enum GBMemoryBankControllerType {
GB_UNL_BBD = 0x220,
GB_UNL_HITEK = 0x221,
GB_UNL_LI_CHENG = 0x222,
GB_UNL_GGB81 = 0x223,
GB_UNL_SACHEN_MMC1 = 0x230,
GB_UNL_SACHEN_MMC2 = 0x231,
};

View File

@ -19,6 +19,12 @@
#include <mgba-util/png-io.h>
#include <mgba-util/vfs.h>
#ifdef PSP2
#include <psp2/io/stat.h>
#elif defined(__3DS__)
#include <mgba-util/platform/3ds/3ds-vfs.h>
#endif
#include <sys/time.h>
mLOG_DECLARE_CATEGORY(GUI_RUNNER);
@ -450,11 +456,7 @@ void mGUIRun(struct mGUIRunner* runner, const char* path) {
runner->setup(runner);
}
if (runner->config.port && runner->keySources) {
mLOG(GUI_RUNNER, DEBUG, "Loading key sources for %s...", runner->config.port);
size_t i;
for (i = 0; runner->keySources[i].id; ++i) {
mInputMapLoad(&runner->core->inputMap, runner->keySources[i].id, mCoreConfigGetInput(&runner->config));
}
mGUILoadInputMaps(runner);
}
mLOG(GUI_RUNNER, DEBUG, "Reseting...");
runner->core->reset(runner->core);
@ -745,11 +747,7 @@ void mGUIRun(struct mGUIRunner* runner, const char* path) {
void mGUIRunloop(struct mGUIRunner* runner) {
if (runner->keySources) {
mLOG(GUI_RUNNER, DEBUG, "Loading key sources for %s...", runner->config.port);
size_t i;
for (i = 0; runner->keySources[i].id; ++i) {
mInputMapLoad(&runner->params.keyMap, runner->keySources[i].id, mCoreConfigGetInput(&runner->config));
}
mGUILoadInputMaps(runner);
}
while (!runner->running || runner->running(runner)) {
char path[PATH_MAX];
@ -770,6 +768,52 @@ void mGUIRunloop(struct mGUIRunner* runner) {
}
}
void mGUILoadInputMaps(struct mGUIRunner* runner) {
mLOG(GUI_RUNNER, DEBUG, "Loading key sources for %s...", runner->config.port);
size_t i;
for (i = 0; runner->keySources[i].id; ++i) {
mInputMapLoad(&runner->params.keyMap, runner->keySources[i].id, mCoreConfigGetInput(&runner->config));
}
}
#if defined(__3DS__) || defined(PSP2)
bool mGUIGetRom(struct mGUIRunner* runner, char* out, size_t outLength) {
#ifdef PSP2
int fd = open("app0:/filename", O_RDONLY);
strcpy(out, "app0:/");
#elif defined(__3DS__)
int fd = open("romfs:/filename", O_RDONLY);
strcpy(out, "romfs:/");
#endif
if (fd < 0) {
return false;
}
size_t len = strlen(out);
ssize_t size = read(fd, out + len, outLength - len);
if (size > 0 && out[len + size - 1] == '\n') {
out[len + size - 1] = '\0';
}
close(fd);
if (size <= 0) {
return false;
}
char basedir[64];
mCoreConfigDirectory(basedir, sizeof(basedir));
strlcat(basedir, "/forwarders", sizeof(basedir));
#ifdef PSP2
sceIoMkdir(basedir, 0777);
#elif defined(__3DS__)
FSUSER_CreateDirectory(sdmcArchive, fsMakePath(PATH_ASCII, basedir), 0);
#endif
mCoreConfigSetValue(&runner->config, "savegamePath", basedir);
mCoreConfigSetValue(&runner->config, "savestatePath", basedir);
mCoreConfigSetValue(&runner->config, "screenshotPath", basedir);
mCoreConfigSetValue(&runner->config, "cheatsPath", basedir);
return true;
}
#endif
#ifndef DISABLE_THREADING
THREAD_ENTRY mGUIAutosaveThread(void* context) {
struct mGUIAutosaveContext* autosave = context;

View File

@ -95,9 +95,14 @@ struct mGUIRunner {
void mGUIInit(struct mGUIRunner*, const char* port);
void mGUIDeinit(struct mGUIRunner*);
void mGUILoadInputMaps(struct mGUIRunner* runner);
void mGUIRun(struct mGUIRunner*, const char* path);
void mGUIRunloop(struct mGUIRunner*);
#if defined(__3DS__) || defined(PSP2)
bool mGUIGetRom(struct mGUIRunner* runner, char* out, size_t outLength);
#endif
#ifndef DISABLE_THREADING
THREAD_ENTRY mGUIAutosaveThread(void* context);
#endif

View File

@ -78,14 +78,7 @@ static void _updateMatch(const char* key, const char* value, void* user) {
return;
}
const char* item = &key[dotLoc + 1];
struct Table* out = user;
struct mUpdate* update = HashTableLookup(out, match->channel);
if (!update) {
update = calloc(1, sizeof(*update));
HashTableInsert(out, match->channel, update);
}
_updateUpdate(update, item, value);
_updateUpdate(match->out, item, value);
}
bool mUpdaterInit(struct mUpdaterContext* context, const char* manifest) {

View File

@ -754,7 +754,7 @@ void GBIODeserialize(struct GB* gb, const struct GBSerializedState* state) {
gb->video.renderer->writeVideoRegister(gb->video.renderer, GB_REG_SCX, state->io[GB_REG_SCX]);
gb->video.renderer->writeVideoRegister(gb->video.renderer, GB_REG_WY, state->io[GB_REG_WY]);
gb->video.renderer->writeVideoRegister(gb->video.renderer, GB_REG_WX, state->io[GB_REG_WX]);
if (gb->model & GB_MODEL_SGB) {
if (gb->model == GB_MODEL_SGB) {
gb->video.renderer->writeVideoRegister(gb->video.renderer, GB_REG_BGP, state->io[GB_REG_BGP]);
gb->video.renderer->writeVideoRegister(gb->video.renderer, GB_REG_OBP0, state->io[GB_REG_OBP0]);
gb->video.renderer->writeVideoRegister(gb->video.renderer, GB_REG_OBP1, state->io[GB_REG_OBP1]);

View File

@ -126,7 +126,7 @@ static struct {
{"SAM2", GB_UNL_SACHEN_MMC2},
{"ROCK", GB_MBC_AUTODETECT}, // TODO
{"NGHK", GB_MBC_AUTODETECT}, // TODO
{"GB81", GB_MBC_AUTODETECT}, // TODO
{"GB81", GB_UNL_GGB81},
{"TPP1", GB_MBC_AUTODETECT}, // TODO
{NULL, GB_MBC_AUTODETECT},
@ -211,11 +211,17 @@ static enum GBMemoryBankControllerType _detectUnlMBC(const uint8_t* mem, size_t
return GB_UNL_BBD;
}
break;
case 0x79f34594: // DATA.
case 0x7e8c539b: // TD-SOFT
return GB_UNL_GGB81;
case 0x20d092e2:
case 0xd2b57657:
if (cart->type == 0x01) { // Make sure we're not using a "fixed" version
return GB_UNL_LI_CHENG;
}
if ((0x8000 << cart->romSize) != size) {
return GB_UNL_LI_CHENG;
}
break;
}
@ -476,6 +482,11 @@ void GBMBCInit(struct GB* gb) {
case GB_UNL_LI_CHENG:
gb->memory.mbcWrite = _GBLiCheng;
break;
case GB_UNL_GGB81:
gb->memory.mbcWrite = _GBGGB81;
gb->memory.mbcRead = _GBGGB81Read;
gb->memory.mbcReadBank1 = true;
break;
case GB_UNL_SACHEN_MMC1:
gb->memory.mbcWrite = _GBSachen;
gb->memory.mbcRead = _GBSachenMMC1Read;

View File

@ -36,6 +36,7 @@ void _GBNTNew(struct GB* gb, uint16_t address, uint8_t value);
void _GBBBD(struct GB* gb, uint16_t address, uint8_t value);
void _GBHitek(struct GB* gb, uint16_t address, uint8_t value);
void _GBLiCheng(struct GB* gb, uint16_t address, uint8_t value);
void _GBGGB81(struct GB* gb, uint16_t address, uint8_t value);
void _GBSachen(struct GB* gb, uint16_t address, uint8_t value);
uint8_t _GBMBC2Read(struct GBMemory*, uint16_t address);
@ -50,6 +51,7 @@ uint8_t _GBHuC3Read(struct GBMemory*, uint16_t address);
uint8_t _GBPKJDRead(struct GBMemory*, uint16_t address);
uint8_t _GBBBDRead(struct GBMemory*, uint16_t address);
uint8_t _GBHitekRead(struct GBMemory*, uint16_t address);
uint8_t _GBGGB81Read(struct GBMemory*, uint16_t address);
uint8_t _GBSachenMMC1Read(struct GBMemory*, uint16_t address);
uint8_t _GBSachenMMC2Read(struct GBMemory*, uint16_t address);

View File

@ -362,6 +362,37 @@ uint8_t _GBHitekRead(struct GBMemory* memory, uint16_t address) {
}
}
static const uint8_t _ggb81DataReordering[8][8] = {
{ 0, 1, 2, 3, 4, 5, 6, 7 },
{ 0, 2, 1, 3, 4, 6, 5, 7 },
{ 0, 6, 5, 3, 4, 2, 1, 7 },
{ 0, 5, 1, 3, 4, 2, 6, 7 },
{ 0, 5, 2, 3, 4, 1, 6, 7 },
{ 0, 2, 6, 3, 4, 5, 1, 7 },
{ 0, 1, 6, 3, 4, 2, 5, 7 },
{ 0, 2, 5, 3, 4, 6, 1, 7 },
};
void _GBGGB81(struct GB* gb, uint16_t address, uint8_t value) {
struct GBMemory* memory = &gb->memory;
switch (address & 0xF0FF) {
case 0x2001:
memory->mbcState.bbd.dataSwapMode = value & 0x07;
break;
}
_GBMBC5(gb, address, value);
}
uint8_t _GBGGB81Read(struct GBMemory* memory, uint16_t address) {
switch (address >> 14) {
case 0:
default:
return memory->romBank[address & (GB_SIZE_CART_BANK0 - 1)];
case 1:
return _reorderBits(memory->romBank[address & (GB_SIZE_CART_BANK0 - 1)], _ggb81DataReordering[memory->mbcState.bbd.dataSwapMode]);
}
}
void _GBLiCheng(struct GB* gb, uint16_t address, uint8_t value) {
if (address > 0x2100 && address < 0x3000) {
return;

View File

@ -816,6 +816,7 @@ void GBMemorySerialize(const struct GB* gb, struct GBSerializedState* state) {
break;
case GB_UNL_BBD:
case GB_UNL_HITEK:
case GB_UNL_GGB81:
state->memory.bbd.dataSwapMode = memory->mbcState.bbd.dataSwapMode;
state->memory.bbd.bankSwapMode = memory->mbcState.bbd.bankSwapMode;
break;
@ -978,6 +979,7 @@ void GBMemoryDeserialize(struct GB* gb, const struct GBSerializedState* state) {
break;
case GB_UNL_BBD:
case GB_UNL_HITEK:
case GB_UNL_GGB81:
memory->mbcState.bbd.dataSwapMode = state->memory.bbd.dataSwapMode & 0x7;
memory->mbcState.bbd.bankSwapMode = state->memory.bbd.bankSwapMode & 0x7;
break;

View File

@ -695,6 +695,7 @@ static const struct GBCartridgeOverride _overrides[] = {
{ 0xBC75D7B8, GB_MODEL_AUTODETECT, GB_UNL_NT_NEW, { 0 } }, // Pokemon - Mewtwo Strikes Back
{ 0xFF0B60CC, GB_MODEL_AUTODETECT, GB_UNL_NT_NEW, { 0 } }, // Shuma Baolong 02 4
{ 0x14A992A6, GB_MODEL_AUTODETECT, GB_UNL_NT_NEW, { 0 } }, // /Street Fighter Zero 4
{ 0x3EF5AFB2, GB_MODEL_AUTODETECT, GB_UNL_LI_CHENG, { 0 } }, // Pokemon Jade Version (Telefang Speed bootleg)
{ 0, 0, 0, { 0 } }
};

View File

@ -1,4 +1,4 @@
set(USE_VFS_3DS ON CACHE BOOL "Use 3DS-specific file support")
set(USE_VFS_3DS OFF CACHE BOOL "Use 3DS-specific file support")
mark_as_advanced(USE_VFS_3DS)
find_program(3DSLINK 3dslink)

View File

@ -817,7 +817,16 @@ THREAD_ENTRY _core2Test(void* context) {
UNUSED(context);
}
int main() {
int main(int argc, char* argv[]) {
char initialPath[PATH_MAX] = { 0 };
if (argc > 1) {
strncpy(initialPath, argv[1], sizeof(PATH_MAX));
} else {
u8 hmac[0x20];
memset(hmac, 0, sizeof(hmac));
APT_ReceiveDeliverArg(initialPath, sizeof(initialPath), hmac, NULL, NULL);
}
rotation.d.sample = _sampleRotation;
rotation.d.readTiltX = _readTiltX;
rotation.d.readTiltY = _readTiltY;
@ -1047,9 +1056,29 @@ int main() {
_map3DSKey(&runner.params.keyMap, KEY_CSTICK_UP, mGUI_INPUT_INCREASE_BRIGHTNESS);
_map3DSKey(&runner.params.keyMap, KEY_CSTICK_DOWN, mGUI_INPUT_DECREASE_BRIGHTNESS);
mGUIRunloop(&runner);
Result res = romfsInit();
bool useRomfs = false;
if (R_SUCCEEDED(res)) {
useRomfs = mGUIGetRom(&runner, initialPath, sizeof(initialPath));
if (!useRomfs) {
romfsExit();
_cleanup();
return 1;
}
}
if (initialPath[0] == '/' || useRomfs) {
mGUILoadInputMaps(&runner);
mGUIRun(&runner, initialPath);
} else {
mGUIRunloop(&runner);
}
mGUIDeinit(&runner);
if (useRomfs) {
romfsExit();
}
_cleanup();
return 0;
}

View File

@ -153,6 +153,8 @@ static enum GUIKeyboardStatus _keyboardRun(struct GUIKeyboardParams* keyboard) {
}
int main() {
char initialPath[PATH_MAX] = { 0 };
vita2d_init();
struct GUIFont* font = GUIFontCreate();
struct mGUIRunner runner = {
@ -278,7 +280,13 @@ int main() {
mPSP2MapKey(&runner.params.keyMap, SCE_CTRL_SQUARE, mGUI_INPUT_SCREEN_MODE);
scePowerSetArmClockFrequency(444);
mGUIRunloop(&runner);
if (mGUIGetRom(&runner, initialPath, sizeof(initialPath))) {
mGUILoadInputMaps(&runner);
mGUIRun(&runner, initialPath);
} else {
mGUIRunloop(&runner);
}
vita2d_fini();
mGUIDeinit(&runner);

View File

@ -136,7 +136,7 @@ QUrl ApplicationUpdater::parseManifest(const QByteArray& manifest) {
QString ApplicationUpdater::destination() const {
QFileInfo path(updateInfo().url.path());
QDir dir(ConfigController::configDir());
QDir dir(ConfigController::cacheDir());
// QFileInfo::completeSuffix will eat all .'s in the filename...including
// ones in the version string, turning mGBA-1.0.0-win32.7z into
// 0.0-win32.7z instead of the intended .7z

View File

@ -95,6 +95,11 @@ set(SOURCE_FILES
Display.cpp
DisplayGL.cpp
DisplayQt.cpp
ForwarderController.cpp
ForwarderGenerator.cpp
ForwarderGenerator3DS.cpp
ForwarderGeneratorVita.cpp
ForwarderView.cpp
FrameView.cpp
GBAApp.cpp
GIFView.cpp
@ -154,6 +159,7 @@ set(UI_FILES
CheatsView.ui
DebuggerConsole.ui
DolphinConnector.ui
ForwarderView.ui
FrameView.ui
GIFView.ui
IOViewer.ui

View File

@ -382,3 +382,7 @@ const QString& ConfigController::configDir() {
}
return s_configDir;
}
const QString& ConfigController::cacheDir() {
return configDir();
}

View File

@ -105,6 +105,7 @@ public:
void usage(const char* arg0) const;
static const QString& configDir();
static const QString& cacheDir();
static bool isPortable();
public slots:

View File

@ -910,7 +910,10 @@ void CoreController::scanCard(const QString& path) {
if (!file.open(QIODevice::ReadOnly)) {
return;
}
m_eReaderData = file.read(2912);
QByteArray eReaderData = file.read(2912);
if (eReaderData.isEmpty()) {
return;
}
file.seek(0);
QStringList lines;
@ -932,6 +935,7 @@ void CoreController::scanCard(const QString& path) {
}
}
scanCards(lines);
m_eReaderData = eReaderData;
} else if (image.size() == QSize(989, 44) || image.size() == QSize(639, 44)) {
const uchar* bits = image.constBits();
size_t size;

View File

@ -31,6 +31,7 @@ void CoreManager::setMultiplayerController(MultiplayerController* multiplayer) {
CoreController* CoreManager::loadGame(const QString& path) {
QFileInfo info(path);
if (!info.isReadable()) {
// Open specific file in archive
QString fname = info.fileName();
QString base = info.path();
if (base.endsWith("/") || base.endsWith(QDir::separator())) {
@ -40,13 +41,8 @@ CoreController* CoreManager::loadGame(const QString& path) {
if (dir) {
VFile* vf = dir->openFile(dir, fname.toUtf8().constData(), O_RDONLY);
if (vf) {
struct VFile* vfclone = VFileMemChunk(NULL, vf->size(vf));
uint8_t buffer[2048];
ssize_t read;
while ((read = vf->read(vf, buffer, sizeof(buffer))) > 0) {
vfclone->write(vfclone, buffer, read);
}
vf->close(vf);
struct VFile* vfclone = VFileDevice::openMemory(vf->size(vf));
VFileDevice::copyFile(vf, vfclone);
vf = vfclone;
}
dir->close(dir);
@ -59,15 +55,16 @@ CoreController* CoreManager::loadGame(const QString& path) {
VFile* vf = nullptr;
VDir* archive = VDirOpenArchive(path.toUtf8().constData());
if (archive) {
// Open first file in archive
VFile* vfOriginal = VDirFindFirst(archive, [](VFile* vf) {
return mCoreIsCompatible(vf) != mPLATFORM_NONE;
});
if (vfOriginal) {
ssize_t size = vfOriginal->size(vfOriginal);
if (size > 0) {
void* mem = vfOriginal->map(vfOriginal, size, MAP_READ);
vf = VFileMemChunk(mem, size);
vfOriginal->unmap(vfOriginal, mem, size);
struct VFile* vfclone = VFileDevice::openMemory(vfOriginal->size(vfOriginal));
VFileDevice::copyFile(vfOriginal, vfclone);
vf = vfclone;
}
vfOriginal->close(vfOriginal);
}
@ -75,6 +72,7 @@ CoreController* CoreManager::loadGame(const QString& path) {
}
QDir dir(info.dir());
if (!vf) {
// Open bare file
vf = VFileOpen(info.canonicalFilePath().toUtf8().constData(), O_RDONLY);
}
return loadGame(vf, info.fileName(), dir.canonicalPath());

View File

@ -22,7 +22,7 @@ namespace DiscordCoordinator {
static bool s_gameRunning = false;
static bool s_inited = false;
static QString s_title;
static QByteArray s_title;
static void updatePresence() {
if (!s_inited) {
@ -30,7 +30,7 @@ static void updatePresence() {
}
if (s_gameRunning) {
DiscordRichPresence discordPresence{};
discordPresence.details = s_title.toUtf8().constData();
discordPresence.details = s_title.constData();
discordPresence.instance = 1;
discordPresence.largeImageKey = "mgba";
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
@ -74,8 +74,10 @@ void gameStarted(std::shared_ptr<CoreController> controller) {
s_title = controller->thread()->core->dirs.baseName;
QString dbTitle = controller->dbTitle();
if (!dbTitle.isNull()) {
s_title = dbTitle;
s_title = dbTitle.toUtf8();
}
// Non-const QByteArrays are null-terminated so we don't need to append null even after truncation
s_title.truncate(128);
updatePresence();
}

View File

@ -0,0 +1,118 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ForwarderController.h"
#include <QFileInfo>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include "ConfigController.h"
#include <mgba/core/version.h>
#include <mgba/feature/updater.h>
using namespace QGBA;
ForwarderController::ForwarderController(QObject* parent)
: QObject(parent)
, m_netman(new QNetworkAccessManager(this))
{
m_netman->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
connect(this, &ForwarderController::buildFailed, this, [this]() {
m_inProgress = false;
});
connect(this, &ForwarderController::buildComplete, this, [this]() {
m_inProgress = false;
});
}
void ForwarderController::startBuild(const QString& outFilename) {
if (m_inProgress) {
return;
}
m_inProgress = true;
m_outFilename = outFilename;
downloadManifest();
}
void ForwarderController::downloadManifest() {
QNetworkReply* reply = m_netman->get(QNetworkRequest(QUrl("https://mgba.io/latest.ini")));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
gotManifest(reply);
});
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
connect(reply, &QNetworkReply::errorOccurred, this, [this, reply]() {
#else
connect(reply, qOverload<>(&QNetworkReply::error), this, [this, reply]() {
#endif
emit buildFailed();
});
}
void ForwarderController::gotManifest(QNetworkReply* reply) {
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
emit buildFailed();
return;
}
QByteArray manifest = reply->readAll();
QString platform = m_generator->systemName();
mUpdaterContext context;
if (!mUpdaterInit(&context, manifest.constData())) {
emit buildFailed();
return;
}
QString bucket = QLatin1String(mUpdaterGetBucket(&context));
mUpdate update;
mUpdaterGetUpdateForChannel(&context, platform.toUtf8().constData(), m_channel.toUtf8().constData(), &update);
downloadBuild({bucket + update.path});
mUpdaterDeinit(&context);
}
void ForwarderController::downloadBuild(const QUrl& url) {
QString extension(QFileInfo(url.path()).suffix());
// TODO: cache this
QString configDir(ConfigController::cacheDir());
m_sourceFile.setFileName(QString("%1/%2-%3-%4.%5").arg(configDir)
.arg(projectName)
.arg(m_generator->systemName())
.arg(channel())
.arg(extension));
if (!m_sourceFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
emit buildFailed();
return;
}
QNetworkReply* reply = m_netman->get(QNetworkRequest(url));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
gotBuild(reply);
});
connect(reply, &QNetworkReply::readyRead, this, [this, reply]() {
QByteArray data = reply->readAll();
m_sourceFile.write(data);
});
}
void ForwarderController::gotBuild(QNetworkReply* reply) {
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
emit buildFailed();
return;
}
QByteArray data = reply->readAll();
m_sourceFile.write(data);
m_sourceFile.close();
if (!m_generator->rebuild(m_sourceFile.fileName(), m_outFilename)) {
emit buildFailed();
} else {
emit buildComplete();
}
m_sourceFile.remove();
}

View File

@ -0,0 +1,54 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#pragma once
#include <QFile>
#include <QObject>
#include "ForwarderGenerator.h"
#include <memory>
class QNetworkAccessManager;
class QNetworkReply;
namespace QGBA {
class ForwarderController : public QObject {
Q_OBJECT
public:
ForwarderController(QObject* parent = nullptr);
void setGenerator(std::unique_ptr<ForwarderGenerator>&& generator) { m_generator = std::move(generator); }
ForwarderGenerator* generator() { return m_generator.get(); }
QString channel() const { return m_channel; }
public slots:
void startBuild(const QString& outFilename);
signals:
void buildComplete();
void buildFailed();
private slots:
void gotManifest(QNetworkReply*);
void gotBuild(QNetworkReply*);
private:
void downloadManifest();
void downloadBuild(const QUrl&);
QString m_channel{"dev"};
QString m_outFilename;
QNetworkAccessManager* m_netman;
std::unique_ptr<ForwarderGenerator> m_generator;
QFile m_sourceFile;
bool m_inProgress = false;
};
}

View File

@ -0,0 +1,74 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ForwarderGenerator.h"
#include <QCryptographicHash>
#include <QFile>
#include "ForwarderGenerator3DS.h"
#include "ForwarderGeneratorVita.h"
using namespace QGBA;
std::unique_ptr<ForwarderGenerator> ForwarderGenerator::createForSystem(System system) {
switch (system) {
case System::N3DS:
return std::make_unique<ForwarderGenerator3DS>();
case System::VITA:
return std::make_unique<ForwarderGeneratorVita>();
}
return nullptr;
}
ForwarderGenerator::ForwarderGenerator(int imageTypes, QObject* parent)
: QObject(parent)
{
m_images.resize(imageTypes);
}
void ForwarderGenerator::setImage(int index, const QImage& image) {
if (index < 0 || index >= m_images.count()) {
return;
}
m_images[index] = image;
}
QImage ForwarderGenerator::image(int index) const {
if (index >= m_images.size()) {
return {};
}
return m_images[index];
}
QByteArray ForwarderGenerator::hashRom() const {
if (m_romPath.isEmpty()) {
return {};
}
QFile romFile(m_romPath);
if (!romFile.open(QIODevice::ReadOnly)) {
return {};
}
QCryptographicHash hash(QCryptographicHash::Sha256);
if (!hash.addData(&romFile)) {
return {};
}
return hash.result();
}
QString ForwarderGenerator::systemName(ForwarderGenerator::System system) {
switch (system) {
case ForwarderGenerator::System::N3DS:
return QLatin1String("3ds");
case ForwarderGenerator::System::VITA:
return QLatin1String("vita");
}
return {};
}

View File

@ -0,0 +1,57 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#pragma once
#include <QByteArray>
#include <QImage>
#include <QObject>
#include <QString>
#include <QVector>
#include <memory>
namespace QGBA {
class ForwarderGenerator : public QObject {
Q_OBJECT
public:
enum class System {
N3DS,
VITA,
};
static std::unique_ptr<ForwarderGenerator> createForSystem(System);
void setTitle(const QString& title) { m_title = title; }
void setRom(const QString& path) { m_romPath = path; }
void setImage(int index, const QImage&);
QString title() const { return m_title; }
QString rom() const { return m_romPath; }
QImage image(int index) const;
QByteArray hashRom() const;
virtual QList<QPair<QString, QSize>> imageTypes() const = 0;
virtual System system() const = 0;
QString systemName() const { return systemName(system()); }
virtual QString extension() const = 0;
static QString systemName(System);
virtual bool rebuild(const QString& source, const QString& target) = 0;
protected:
ForwarderGenerator(int imageTypes, QObject* parent = nullptr);
private:
QString m_title;
QString m_romPath;
QVector<QImage> m_images;
};
}

View File

@ -0,0 +1,24 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ForwarderGenerator3DS.h"
using namespace QGBA;
ForwarderGenerator3DS::ForwarderGenerator3DS()
: ForwarderGenerator(2)
{
}
QList<QPair<QString, QSize>> ForwarderGenerator3DS::imageTypes() const {
return {
{ tr("Icon"), QSize(48, 48) },
{ tr("Banner"), QSize(256, 128) }
};
}
bool ForwarderGenerator3DS::rebuild(const QString& source, const QString& target) {
return false;
}

View File

@ -0,0 +1,25 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#pragma once
#include "ForwarderGenerator.h"
namespace QGBA {
class ForwarderGenerator3DS final : public ForwarderGenerator {
Q_OBJECT
public:
ForwarderGenerator3DS();
QList<QPair<QString, QSize>> imageTypes() const override;
System system() const override { return System::N3DS; }
QString extension() const override { return QLatin1String("cia"); }
bool rebuild(const QString& source, const QString& target) override;
};
}

View File

@ -0,0 +1,187 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ForwarderGeneratorVita.h"
#include <QFileInfo>
#include <QTemporaryFile>
#include "VFileDevice.h"
#include <mgba-util/sfo.h>
#include <mgba-util/vfs.h>
using namespace QGBA;
ForwarderGeneratorVita::ForwarderGeneratorVita()
: ForwarderGenerator(3)
{
}
QList<QPair<QString, QSize>> ForwarderGeneratorVita::imageTypes() const {
return {
{ tr("Bubble"), QSize(128, 128) },
{ tr("Background"), QSize(840, 500) },
{ tr("Startup"), QSize(280, 158) }
};
}
bool ForwarderGeneratorVita::rebuild(const QString& source, const QString& target) {
QString vpk = dumpVpk(source);
if (vpk.isNull()) {
return false;
}
QFile vpkFile(vpk);
VDir* outdir = VDirOpenZip(target.toLocal8Bit().constData(), O_WRONLY | O_CREAT | O_TRUNC);
if (outdir && !copyAssets(vpk, outdir)) {
outdir->close(outdir);
outdir = nullptr;
}
vpkFile.remove();
if (!outdir) {
return false;
}
VFile* sfo = outdir->openFile(outdir, "sce_sys/param.sfo", O_WRONLY);
writeSfo(sfo);
sfo->close(sfo);
QFileInfo info(rom());
QByteArray buffer(info.fileName().toUtf8());
VFile* filename = outdir->openFile(outdir, "filename", O_WRONLY);
filename->write(filename, buffer.constData(), buffer.size());
filename->close(filename);
VFile* romfileOut = outdir->openFile(outdir, buffer.constData(), O_WRONLY);
VFileDevice romfileIn(rom(), QIODevice::ReadOnly);
VFileDevice::copyFile(romfileIn, romfileOut);
romfileIn.close();
romfileOut->close(romfileOut);
if (!image(0).isNull()) {
injectImage(outdir, "sce_sys/icon0.png", 0);
}
if (!image(1).isNull()) {
injectImage(outdir, "sce_sys/livearea/contents/bg.png", 1);
}
if (!image(2).isNull()) {
injectImage(outdir, "sce_sys/livearea/contents/startup.png", 2);
}
outdir->close(outdir);
return true;
}
QString ForwarderGeneratorVita::dumpVpk(const QString& archive) {
bool gotFile = false;
VDir* inArchive = VFileDevice::openArchive(archive);
if (!inArchive) {
return {};
}
for (VDirEntry* dirent = inArchive->listNext(inArchive); dirent; dirent = inArchive->listNext(inArchive)) {
if (dirent->type(dirent) != VFS_FILE) {
continue;
}
QString filename(dirent->name(dirent));
if (!filename.endsWith(".vpk")) {
continue;
}
VFile* outfile = VFileOpen("tmp.vpk", O_WRONLY | O_TRUNC | O_CREAT);
VFile* vpk = inArchive->openFile(inArchive, dirent->name(dirent), O_RDONLY);
VFileDevice::copyFile(vpk, outfile);
vpk->close(vpk);
outfile->close(outfile);
gotFile = true;
break;
}
inArchive->close(inArchive);
if (gotFile) {
return QLatin1String("tmp.vpk");
}
return {};
}
bool ForwarderGeneratorVita::copyAssets(const QString& vpk, VDir* outdir) {
VDir* indir = VDirOpenZip(vpk.toLocal8Bit().constData(), O_RDONLY);
if (!indir) {
return false;
}
bool ok = true;
for (VDirEntry* dirent = indir->listNext(indir); dirent; dirent = indir->listNext(indir)) {
if (dirent->name(dirent) == QLatin1String("sce_sys/param.sfo")) {
continue;
}
if (dirent->name(dirent) == QLatin1String("sce_sys/icon0.png") && !image(0).isNull()) {
continue;
}
if (dirent->name(dirent) == QLatin1String("sce_sys/livearea/contents/bg.png") && !image(1).isNull()) {
continue;
}
if (dirent->name(dirent) == QLatin1String("sce_sys/livearea/contents/startup.png") && !image(2).isNull()) {
continue;
}
if (dirent->type(dirent) != VFS_FILE) {
continue;
}
VFile* infile = indir->openFile(indir, dirent->name(dirent), O_RDONLY);
if (!infile) {
ok = false;
break;
}
VFile* outfile = outdir->openFile(outdir, dirent->name(dirent), O_WRONLY);
if (!outfile) {
infile->close(infile);
ok = false;
break;
}
VFileDevice::copyFile(infile, outfile);
infile->close(infile);
outfile->close(outfile);
}
indir->close(indir);
return ok;
}
QString ForwarderGeneratorVita::makeSerial() const {
QByteArray hash = hashRom();
quint32 hashBits = (hash[0] << 24) | (hash[1] << 16) | (hash[2] << 8) | hash[3];
QString serial("MFXXXXXXX");
for (int i = 0; i < 7; ++i) {
static const char alphabet[37] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
serial[i + 2] = alphabet[hashBits % 36];
hashBits /= 36;
}
return serial;
}
void ForwarderGeneratorVita::writeSfo(VFile* out) {
Table sfo;
QByteArray serial(makeSerial().toLocal8Bit());
QByteArray titleBytes(title().toUtf8());
SfoInit(&sfo);
SfoSetTitle(&sfo, titleBytes.constData());
SfoAddStrValue(&sfo, "TITLE_ID", serial.constData());
SfoWrite(&sfo, out);
SfoDeinit(&sfo);
}
void ForwarderGeneratorVita::injectImage(VDir* out, const char* name, int index) {
VFile* outfile = out->openFile(out, name, O_WRONLY);
VFileDevice outdev(outfile);
image(index).convertToFormat(QImage::Format_Indexed8).save(&outdev, "PNG");
}

View File

@ -0,0 +1,35 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#pragma once
#include "ForwarderGenerator.h"
struct VDir;
struct VFile;
namespace QGBA {
class ForwarderGeneratorVita final : public ForwarderGenerator {
Q_OBJECT
public:
ForwarderGeneratorVita();
QList<QPair<QString, QSize>> imageTypes() const override;
System system() const override { return System::VITA; }
QString extension() const override { return QLatin1String("vpk"); }
bool rebuild(const QString& source, const QString& target) override;
private:
QString dumpVpk(const QString& archive);
bool copyAssets(const QString& vpk, VDir* out);
QString makeSerial() const;
void writeSfo(VFile* out);
void injectImage(VDir* out, const char* name, int index);
};
}

View File

@ -0,0 +1,159 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/ */
#include "ForwarderView.h"
#include <QMessageBox>
#include <QResizeEvent>
#include "ForwarderGenerator.h"
#include "GBAApp.h"
#include "utils.h"
using namespace QGBA;
ForwarderView::ForwarderView(QWidget* parent)
: QDialog(parent)
{
m_ui.setupUi(this);
connectBrowseButton(m_ui.romBrowse, m_ui.romFilename, tr("Select ROM file"), false, romFilters());
connectBrowseButton(m_ui.outputBrowse, m_ui.outputFilename, tr("Select output filename"), true);
connectBrowseButton(m_ui.baseBrowse, m_ui.baseFilename, tr("Select base file"));
connect(m_ui.romFilename, &QLineEdit::textChanged, this, &ForwarderView::validate);
connect(m_ui.outputFilename, &QLineEdit::textChanged, this, &ForwarderView::validate);
connect(m_ui.baseFilename, &QLineEdit::textChanged, this, &ForwarderView::validate);
connect(m_ui.title, &QLineEdit::textChanged, this, &ForwarderView::validate);
connect(m_ui.baseType, qOverload<int>(&QComboBox::currentIndexChanged), this, &ForwarderView::validate);
connect(m_ui.imageSelect, qOverload<int>(&QComboBox::currentIndexChanged), this, &ForwarderView::setActiveImage);
connect(m_ui.imageBrowse, &QAbstractButton::clicked, this, &ForwarderView::selectImage);
connect(&m_controller, &ForwarderController::buildComplete, this, &QDialog::accept);
connect(&m_controller, &ForwarderController::buildFailed, this, [this]() {
QMessageBox* error = new QMessageBox(QMessageBox::Critical, tr("Build failed"),
tr("Failed to build forwarder"),
QMessageBox::Ok, this, Qt::Sheet);
error->setAttribute(Qt::WA_DeleteOnClose);
error->show();
});
connect(m_ui.system3DS, &QAbstractButton::clicked, this, [this]() {
setSystem(ForwarderGenerator::System::N3DS);
});
connect(m_ui.systemVita, &QAbstractButton::clicked, this, [this]() {
setSystem(ForwarderGenerator::System::VITA);
});
m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
connect(m_ui.buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, [this]() {
m_controller.generator()->setRom(m_ui.romFilename->text());
m_controller.startBuild(m_ui.outputFilename->text());
});
}
void ForwarderView::build() {
if (!m_controller.generator()) {
return;
}
m_controller.generator()->setTitle(m_ui.title->text());
m_controller.generator()->setRom(m_ui.romFilename->text());
m_controller.startBuild(m_ui.outputFilename->text());
}
void ForwarderView::validate() {
bool valid = true;
if (m_ui.romFilename->text().isEmpty()) {
valid = false;
} else if (!QFileInfo(m_ui.romFilename->text()).exists()) {
valid = false;
}
if (m_ui.outputFilename->text().isEmpty()) {
valid = false;
}
if (m_ui.title->text().isEmpty()) {
valid = false;
}
if (!m_ui.system->checkedButton()) {
valid = false;
}
if (m_ui.baseType->currentIndex() != 1) {
valid = false;
}
m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid);
}
void ForwarderView::setSystem(ForwarderGenerator::System system) {
m_controller.setGenerator(ForwarderGenerator::createForSystem(system));
auto types = m_controller.generator()->imageTypes();
m_images.clear();
m_images.resize(types.count());
m_ui.imageSelect->clear();
for (const auto& pair : types) {
m_ui.imageSelect->addItem(pair.first);
}
m_ui.imageSelect->setEnabled(true);
m_ui.imagePreview->setEnabled(true);
m_ui.imageBrowse->setEnabled(true);
m_ui.imagesLabel->setEnabled(true);
m_ui.preferredLabel->setEnabled(true);
m_ui.preferredWidth->setEnabled(true);
m_ui.preferredX->setEnabled(true);
m_ui.preferredHeight->setEnabled(true);
}
void ForwarderView::connectBrowseButton(QAbstractButton* button, QLineEdit* lineEdit, const QString& title, bool save, const QString& filter) {
connect(button, &QAbstractButton::clicked, lineEdit, [this, lineEdit, save, title, filter]() {
QString filename;
if (save) {
filename = GBAApp::app()->getSaveFileName(this, title, filter);
} else {
filename = GBAApp::app()->getOpenFileName(this, title, filter);
}
if (filename.isEmpty()) {
return;
}
lineEdit->setText(filename);
});
}
void ForwarderView::selectImage() {
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select an image"), {});
if (filename.isEmpty()) {
return;
}
QImage image(filename);
if (image.isNull()) {
return;
}
image = image.scaled(m_activeSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
m_ui.imagePreview->setPixmap(QPixmap::fromImage(image));
m_ui.useDefaultImage->setChecked(false);
m_controller.generator()->setImage(m_currentImage, image);
}
void ForwarderView::setActiveImage(int index) {
if (index < 0) {
m_currentImage = -1;
m_activeSize = QSize();
return;
}
if (!m_controller.generator()) {
return;
}
auto types = m_controller.generator()->imageTypes();
if (index >= types.count()) {
return;
}
m_currentImage = index;
m_activeSize = types[index].second;
m_ui.preferredWidth->setText(QString::number(m_activeSize.width()));
m_ui.preferredHeight->setText(QString::number(m_activeSize.height()));
m_ui.imagePreview->setMaximumSize(m_activeSize);
m_ui.imagePreview->setPixmap(QPixmap::fromImage(m_controller.generator()->image(index)));
}

View File

@ -0,0 +1,42 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#pragma once
#include "ForwarderController.h"
#include "ForwarderGenerator.h"
#include <QImage>
#include <QVector>
#include "ui_ForwarderView.h"
namespace QGBA {
class ForwarderView : public QDialog {
Q_OBJECT
public:
ForwarderView(QWidget* parent = nullptr);
private slots:
void build();
void validate();
private:
void setSystem(ForwarderGenerator::System);
void connectBrowseButton(QAbstractButton* button, QLineEdit* lineEdit, const QString& title, bool save = false, const QString& filter = {});
void selectImage();
void setActiveImage(int);
ForwarderController m_controller;
QVector<QImage> m_images;
int m_currentImage;
QSize m_activeSize;
Ui::ForwarderView m_ui;
};
}

View File

@ -0,0 +1,426 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QGBA::ForwarderView</class>
<widget class="QDialog" name="QGBA::ForwarderView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>710</width>
<height>465</height>
</rect>
</property>
<property name="windowTitle">
<string>Create forwarder</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>System</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item alignment="Qt::AlignHCenter">
<widget class="QRadioButton" name="system3DS">
<property name="text">
<string>3DS</string>
</property>
<attribute name="buttonGroup">
<string notr="true">system</string>
</attribute>
</widget>
</item>
<item alignment="Qt::AlignHCenter">
<widget class="QRadioButton" name="systemVita">
<property name="text">
<string>Vita</string>
</property>
<attribute name="buttonGroup">
<string notr="true">system</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Files</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>ROM file:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="romFilename"/>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="romBrowse">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Output filename:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="outputFilename"/>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="outputBrowse">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Forwarder base:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="baseType">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Latest stable verison</string>
</property>
</item>
<item>
<property name="text">
<string>Latest development build</string>
</property>
</item>
<item>
<property name="text">
<string>Specific file</string>
</property>
</item>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_6">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Base file:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="baseFilename">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QPushButton" name="baseBrowse">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" rowspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Presentation</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Title:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="title"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="imagesLabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Images:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="imageSelect">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="useDefaultImage">
<property name="text">
<string>Use default image</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,1,0" columnstretch="0,1,0">
<item row="0" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QLabel" name="imagePreview">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>128</width>
<height>128</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="2">
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0">
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="2,1,0,1,0">
<item alignment="Qt::AlignRight">
<widget class="QLabel" name="preferredLabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Preferred size:</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignRight">
<widget class="QLabel" name="preferredWidth">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string notr="true">0</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignHCenter">
<widget class="QLabel" name="preferredX">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string notr="true">×</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="preferredHeight">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string notr="true">0</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="imageBrowse">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Select image file</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>QGBA::ForwarderView</receiver>
<slot>deleteLater()</slot>
<hints>
<hint type="sourcelabel">
<x>354</x>
<y>439</y>
</hint>
<hint type="destinationlabel">
<x>354</x>
<y>232</y>
</hint>
</hints>
</connection>
</connections>
<buttongroups>
<buttongroup name="system"/>
</buttongroups>
</ui>

View File

@ -40,6 +40,7 @@ static const QList<GBMemoryBankControllerType> s_mbcList{
GB_UNL_NT_NEW,
GB_UNL_BBD,
GB_UNL_HITEK,
GB_UNL_GGB81,
GB_UNL_LI_CHENG,
GB_UNL_SACHEN_MMC1,
GB_UNL_SACHEN_MMC2,
@ -97,6 +98,7 @@ QString GameBoy::mbcName(GBMemoryBankControllerType mbc) {
s_mbcNames[GB_UNL_PKJD] = tr("Pokémon Jade/Diamond");
s_mbcNames[GB_UNL_BBD] = tr("BBD");
s_mbcNames[GB_UNL_HITEK] = tr("Hitek");
s_mbcNames[GB_UNL_GGB81] = tr("GGB-81");
s_mbcNames[GB_UNL_LI_CHENG] = tr("Li Cheng");
s_mbcNames[GB_UNL_SACHEN_MMC1] = tr("Sachen (MMC1)");
s_mbcNames[GB_UNL_SACHEN_MMC2] = tr("Sachen (MMC2)");

View File

@ -296,9 +296,14 @@ SettingsView::SettingsView(ConfigController* controller, InputController* inputC
}
const GBColorPreset* colorPresets;
QString usedPreset = m_controller->getQtOption("gb.pal").toString();
size_t nPresets = GBColorPresetList(&colorPresets);
for (size_t i = 0; i < nPresets; ++i) {
m_ui.colorPreset->addItem(QString(colorPresets[i].name));
QString presetName(colorPresets[i].name);
m_ui.colorPreset->addItem(presetName);
if (usedPreset == presetName) {
m_ui.colorPreset->setCurrentIndex(i);
}
}
connect(m_ui.colorPreset, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [this, colorPresets](int n) {
const GBColorPreset* preset = &colorPresets[n];
@ -642,6 +647,7 @@ void SettingsView::updateConfig() {
m_controller->setOption(color.toUtf8().constData(), m_gbColors[colorId] & ~0xFF000000);
}
m_controller->setQtOption("gb.pal", m_ui.colorPreset->currentText());
int gbColors = GB_COLORS_CGB;
if (m_ui.gbColor->isChecked()) {

View File

@ -172,8 +172,8 @@ VFile* VFileDevice::open(const QString& path, int mode) {
return VFileOpen(path.toUtf8().constData(), mode);
}
VFile* VFileDevice::openMemory() {
return VFileMemChunk(nullptr, 0);
VFile* VFileDevice::openMemory(quint64 size) {
return VFileMemChunk(nullptr, size);
}
VDir* VFileDevice::openDir(const QString& path) {
@ -184,6 +184,19 @@ VDir* VFileDevice::openArchive(const QString& path) {
return VDirOpenArchive(path.toUtf8().constData());
}
bool VFileDevice::copyFile(VFile* input, VFile* output) {
uint8_t buffer[0x800];
input->seek(input, 0, SEEK_SET);
output->seek(output, 0, SEEK_SET);
ssize_t size;
while ((size = input->read(input, buffer, sizeof(buffer))) > 0) {
output->write(output, buffer, size);
}
return size >= 0;
}
VFileAbstractWrapper::VFileAbstractWrapper(QIODevice* iodev)
: m_iodev(iodev)
{

View File

@ -38,10 +38,12 @@ public:
static VFile* wrap(QBuffer*, QIODevice::OpenMode);
static VFile* open(const QString& path, int mode);
static VFile* openMemory();
static VFile* openMemory(quint64 size = 0);
static VDir* openDir(const QString& path);
static VDir* openArchive(const QString& path);
static bool copyFile(VFile* input, VFile* output);
protected:
virtual qint64 readData(char* data, qint64 maxSize) override;
virtual qint64 writeData(const char* data, qint64 maxSize) override;

View File

@ -30,6 +30,7 @@
#include "Display.h"
#include "DolphinConnector.h"
#include "CoreController.h"
#include "ForwarderView.h"
#include "FrameView.h"
#include "GBAApp.h"
#include "GDBController.h"
@ -337,69 +338,6 @@ void Window::saveConfig() {
m_config->write();
}
QString Window::getFilters() const {
QStringList filters;
QStringList formats;
#ifdef M_CORE_GBA
QStringList gbaFormats{
"*.gba",
#if defined(USE_LIBZIP) || defined(USE_MINIZIP)
"*.zip",
#endif
#ifdef USE_LZMA
"*.7z",
#endif
#ifdef USE_ELF
"*.elf",
#endif
"*.agb",
"*.mb",
"*.rom",
"*.bin"};
formats.append(gbaFormats);
filters.append(tr("Game Boy Advance ROMs (%1)").arg(gbaFormats.join(QChar(' '))));
#endif
#ifdef M_CORE_DS
QStringList dsFormats{
"*.nds",
"*.srl",
#if defined(USE_LIBZIP) || defined(USE_ZLIB)
"*.zip",
#endif
#ifdef USE_LZMA
"*.7z",
#endif
"*.rom",
"*.bin"};
formats.append(dsFormats);
filters.append(tr("DS ROMs (%1)").arg(dsFormats.join(QChar(' '))));
#endif
#ifdef M_CORE_GB
QStringList gbFormats{
"*.gb",
"*.gbc",
"*.sgb",
#if defined(USE_LIBZIP) || defined(USE_MINIZIP)
"*.zip",
#endif
#ifdef USE_LZMA
"*.7z",
#endif
"*.rom",
"*.bin"};
formats.append(gbFormats);
filters.append(tr("Game Boy ROMs (%1)").arg(gbFormats.join(QChar(' '))));
#endif
formats.removeDuplicates();
filters.prepend(tr("All ROMs (%1)").arg(formats.join(QChar(' '))));
filters.append(tr("%1 Video Logs (*.mvl)").arg(projectName));
return filters.join(";;");
}
QString Window::getFiltersArchive() const {
QStringList filters;
@ -416,7 +354,7 @@ QString Window::getFiltersArchive() const {
}
void Window::selectROM() {
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select ROM"), getFilters());
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select ROM"), romFilters(true));
if (!filename.isEmpty()) {
setController(m_manager->loadGame(filename), filename);
}
@ -459,7 +397,7 @@ void Window::addDirToLibrary() {
#endif
void Window::replaceROM() {
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select ROM"), getFilters());
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select ROM"), romFilters());
if (!filename.isEmpty()) {
m_controller->replaceGame(filename);
}
@ -1755,6 +1693,8 @@ void Window::setupMenu(QMenuBar* menubar) {
m_actions.addAction(tr("Scripting..."), "scripting", this, &Window::scriptingOpen, "tools");
#endif
m_actions.addAction(tr("Create forwarder..."), "createForwarder", openTView<ForwarderView>(), "tools");
m_actions.addSeparator("tools");
m_actions.addAction(tr("Settings..."), "settings", this, &Window::openSettingsWindow, "tools")->setRole(Action::Role::SETTINGS);
m_actions.addAction(tr("Make portable"), "makePortable", this, &Window::tryMakePortable, "tools");

View File

@ -187,7 +187,6 @@ private:
void updateTitle(float fps = -1);
QString getFilters() const;
QString getFiltersArchive() const;
CoreManager* m_manager;

View File

@ -5,6 +5,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "utils.h"
#include <mgba/core/version.h>
#include <QCoreApplication>
#include <QObject>
namespace QGBA {
@ -59,4 +62,69 @@ bool convertAddress(const QHostAddress* input, Address* output) {
return true;
}
QString romFilters(bool includeMvl) {
QStringList filters;
QStringList formats;
#ifdef M_CORE_GBA
QStringList gbaFormats{
"*.gba",
#if defined(USE_LIBZIP) || defined(USE_MINIZIP)
"*.zip",
#endif
#ifdef USE_LZMA
"*.7z",
#endif
#ifdef USE_ELF
"*.elf",
#endif
"*.agb",
"*.mb",
"*.rom",
"*.bin"};
formats.append(gbaFormats);
filters.append(QCoreApplication::translate("QGBA", "Game Boy Advance ROMs (%1)", nullptr).arg(gbaFormats.join(QChar(' '))));
#endif
#ifdef M_CORE_DS
QStringList dsFormats{
"*.nds",
"*.srl",
#if defined(USE_LIBZIP) || defined(USE_ZLIB)
"*.zip",
#endif
#ifdef USE_LZMA
"*.7z",
#endif
"*.rom",
"*.bin"};
formats.append(dsFormats);
filters.append(QCoreApplication::translate("QGBA", "DS ROMs (%1)").arg(dsFormats.join(QChar(' '))));
#endif
#ifdef M_CORE_GB
QStringList gbFormats{
"*.gb",
"*.gbc",
"*.sgb",
#if defined(USE_LIBZIP) || defined(USE_MINIZIP)
"*.zip",
#endif
#ifdef USE_LZMA
"*.7z",
#endif
"*.rom",
"*.bin"};
formats.append(gbFormats);
filters.append(QCoreApplication::translate("QGBA", "Game Boy ROMs (%1)", nullptr).arg(gbFormats.join(QChar(' '))));
#endif
formats.removeDuplicates();
filters.prepend(QCoreApplication::translate("QGBA", "All ROMs (%1)", nullptr).arg(formats.join(QChar(' '))));
if (includeMvl) {
filters.append(QCoreApplication::translate("QGBA", "%1 Video Logs (*.mvl)", nullptr).arg(projectName));
}
return filters.join(";;");
}
}

View File

@ -77,4 +77,6 @@ constexpr const T& clamp(const T& v, const T& lo, const T& hi) {
}
#endif
QString romFilters(bool includeMvl = false);
}

View File

@ -1061,10 +1061,7 @@ int main(int argc, char* argv[]) {
}
if (argc > 1) {
size_t i;
for (i = 0; runner.keySources[i].id; ++i) {
mInputMapLoad(&runner.params.keyMap, runner.keySources[i].id, mCoreConfigGetInput(&runner.config));
}
mGUILoadInputMaps(&runner);
mGUIRun(&runner, argv[1]);
} else {
mGUIRunloop(&runner);

View File

@ -653,10 +653,7 @@ int main(int argc, char* argv[]) {
}
if (argc > 1) {
size_t i;
for (i = 0; runner.keySources[i].id; ++i) {
mInputMapLoad(&runner.params.keyMap, runner.keySources[i].id, mCoreConfigGetInput(&runner.config));
}
mGUILoadInputMaps(&runner);
mGUIRun(&runner, argv[1]);
} else {
mGUIRunloop(&runner);

View File

@ -21,6 +21,7 @@ set(SOURCE_FILES
patch-ups.c
png-io.c
ring-fifo.c
sfo.c
text-codec.c)
set(GUI_FILES
@ -31,6 +32,7 @@ set(GUI_FILES
gui/menu.c)
set(TEST_FILES
test/sfo.c
test/string-parser.c
test/string-utf8.c
test/table.c

228
src/util/sfo.c Normal file
View File

@ -0,0 +1,228 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* This code is loosely based on vita-mksfoex.c from the vitasdk
* Copyright (c) 2015 Sergi Granell
* Copyright (c) 2015 Danielle Church
* Used under the MIT license
*
* Which itself is based on mksfoex.c from the pspsdk
* Copyright (c) 2005 adresd
* Copyright (c) 2005 Marcus R. Brown
* Copyright (c) 2005 James Forshaw
* Copyright (c) 2005 John Kelley
* Copyright (c) 2005 Jesper Svennevid
* Used under the BSD 3-clause license
*/
#include <mgba-util/common.h>
#include <mgba-util/table.h>
#include <mgba-util/vfs.h>
#define PSF_MAGIC 0x46535000
#define PSF_VERSION 0x00000101
struct SfoHeader {
uint32_t magic;
uint32_t version;
uint32_t keyofs;
uint32_t valofs;
uint32_t count;
};
struct SfoEntry {
uint16_t nameofs;
uint8_t alignment;
uint8_t type;
uint32_t valsize;
uint32_t totalsize;
uint32_t dataofs;
};
enum PSFType {
PSF_TYPE_BIN = 0,
PSF_TYPE_STR = 2,
PSF_TYPE_U32 = 4,
};
struct SfoEntryContainer {
const char* name;
enum PSFType type;
union {
const char* str;
uint32_t u32;
} data;
uint32_t size;
};
static struct SfoEntryContainer sfoDefaults[] = {
{ "APP_VER", PSF_TYPE_STR, { .str = "00.00" } },
{ "ATTRIBUTE", PSF_TYPE_U32, { .u32 = 0x8000 } },
{ "ATTRIBUTE2", PSF_TYPE_U32, { .u32 = 0 } },
{ "ATTRIBUTE_MINOR", PSF_TYPE_U32, { .u32 = 0x10 } },
{ "BOOT_FILE", PSF_TYPE_STR, { .str = ""}, 0x20 },
{ "CATEGORY", PSF_TYPE_STR, { .str = "gd" } },
{ "CONTENT_ID", PSF_TYPE_STR, { .str = "" }, 0x30 },
{ "EBOOT_APP_MEMSIZE", PSF_TYPE_U32, { .u32 = 0 } },
{ "EBOOT_ATTRIBUTE", PSF_TYPE_U32, { .u32 = 0 } },
{ "EBOOT_PHY_MEMSIZE", PSF_TYPE_U32, { .u32 = 0 } },
{ "LAREA_TYPE", PSF_TYPE_U32, { .u32 = 0 } },
{ "NP_COMMUNICATION_ID", PSF_TYPE_STR, { .str = "" }, 0x10 },
{ "PARENTAL_LEVEL", PSF_TYPE_U32, { .u32 = 0 } },
{ "PSP2_DISP_VER", PSF_TYPE_STR, { .str = "00.000" } },
{ "PSP2_SYSTEM_VER", PSF_TYPE_U32, { .u32 = 0 } },
{ "STITLE", PSF_TYPE_STR, { .str = "Homebrew" }, 52 },
{ "TITLE", PSF_TYPE_STR, { .str = "Homebrew" }, 0x80 },
{ "TITLE_ID", PSF_TYPE_STR, { .str = "ABCD99999" } },
{ "VERSION", PSF_TYPE_STR, { .str = "00.00" } },
};
bool SfoAddStrValue(struct Table* sfo, const char* name, const char* value) {
struct SfoEntryContainer* entry = HashTableLookup(sfo, name);
if (!entry) {
entry = calloc(1, sizeof(*entry));
if (!entry) {
return false;
}
entry->name = name;
HashTableInsert(sfo, name, entry);
}
entry->type = PSF_TYPE_STR;
entry->data.str = value;
return true;
}
bool SfoAddU32Value(struct Table* sfo, const char* name, uint32_t value) {
struct SfoEntryContainer* entry = HashTableLookup(sfo, name);
if (!entry) {
entry = calloc(1, sizeof(*entry));
if (!entry) {
return false;
}
entry->name = name;
HashTableInsert(sfo, name, entry);
}
entry->type = PSF_TYPE_U32;
entry->data.u32 = value;
return true;
}
bool SfoSetTitle(struct Table* sfo, const char* title) {
return SfoAddStrValue(sfo, "TITLE", title) && SfoAddStrValue(sfo, "STITLE", title);
}
void SfoInit(struct Table* sfo) {
HashTableInit(sfo, 32, free);
size_t i;
for (i = 0; i < sizeof(sfoDefaults) / sizeof(sfoDefaults[0]); ++i) {
struct SfoEntryContainer* entry = calloc(1, sizeof(*entry));
memcpy(entry, &sfoDefaults[i], sizeof(*entry));
HashTableInsert(sfo, entry->name, entry);
}
}
#define ALIGN4(X) (((X) + 3) & ~3)
static int _sfoSort(const void* a, const void* b) {
const struct SfoEntryContainer* ea = a;
const struct SfoEntryContainer* eb = b;
return strcmp(ea->name, eb->name);
}
bool SfoWrite(struct Table* sfo, struct VFile* vf) {
struct SfoHeader header;
size_t count = HashTableSize(sfo);
STORE_32LE(PSF_MAGIC, 0, &header.magic);
STORE_32LE(PSF_VERSION, 0, &header.version);
STORE_32LE(count, 0, &header.count);
struct TableIterator iter;
if (!TableIteratorStart(sfo, &iter)) {
return false;
}
struct SfoEntryContainer* sortedEntries = calloc(count, sizeof(struct SfoEntryContainer));
uint32_t keysSize = 0;
uint32_t dataSize = 0;
size_t i = 0;
do {
memcpy(&sortedEntries[i], TableIteratorGetValue(sfo, &iter), sizeof(struct SfoEntryContainer));
keysSize += strlen(sortedEntries[i].name) + 1;
if (!sortedEntries[i].size) {
switch (sortedEntries[i].type) {
case PSF_TYPE_STR:
sortedEntries[i].size = strlen(sortedEntries[i].data.str) + 1;
break;
case PSF_TYPE_U32:
sortedEntries[i].size = 4;
break;
}
}
dataSize += ALIGN4(sortedEntries[i].size);
++i;
} while (TableIteratorNext(sfo, &iter));
keysSize = ALIGN4(keysSize);
qsort(sortedEntries, count, sizeof(struct SfoEntryContainer), _sfoSort);
uint32_t keysOffset = 0;
uint32_t dataOffset = 0;
char* keys = calloc(1, keysSize);
char* data = calloc(1, dataSize);
struct SfoEntry* entries = calloc(count, sizeof(struct SfoEntry));
for (i = 0; i < count; ++i) {
STORE_16LE(keysOffset, 0, &entries[i].nameofs);
STORE_32LE(dataOffset, 0, &entries[i].dataofs);
entries[i].alignment = 4;
entries[i].type = sortedEntries[i].type;
strcpy(&keys[keysOffset], sortedEntries[i].name);
keysOffset += strlen(sortedEntries[i].name) + 1;
if (sortedEntries[i].type == PSF_TYPE_U32) {
STORE_32LE(4, 0, &entries[i].valsize);
STORE_32LE(4, 0, &entries[i].totalsize);
STORE_32LE(sortedEntries[i].data.u32, dataOffset, data);
dataOffset += 4;
} else {
STORE_32LE(ALIGN4(sortedEntries[i].size), 0, &entries[i].totalsize);
memset(&data[dataOffset], 0, ALIGN4(sortedEntries[i].size));
if (sortedEntries[i].data.str) {
STORE_32LE(strlen(sortedEntries[i].data.str) + 1, 0, &entries[i].valsize);
strncpy(&data[dataOffset], sortedEntries[i].data.str, sortedEntries[i].size);
} else {
STORE_32LE(sortedEntries[i].size, 0, &entries[i].valsize);
}
dataOffset += ALIGN4(sortedEntries[i].size);
}
}
if (keysSize != ALIGN4(keysOffset) || dataSize != dataOffset) {
abort();
}
free(sortedEntries);
STORE_32LE(count * sizeof(struct SfoEntry) + sizeof(header), 0, &header.keyofs);
STORE_32LE(count * sizeof(struct SfoEntry) + sizeof(header) + keysSize, 0, &header.valofs);
vf->write(vf, &header, sizeof(header));
vf->write(vf, entries, sizeof(entries[0]) * count);
vf->write(vf, keys, keysSize);
vf->write(vf, data, dataSize);
free(entries);
free(keys);
free(data);
return true;
}

107
src/util/test/sfo.c Normal file
View File

@ -0,0 +1,107 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "util/test/suite.h"
#include <mgba-util/sfo.h>
#include <mgba-util/vfs.h>
const char defaultMksfoex[] = {
0x00, 0x50, 0x53, 0x46, 0x01, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00,
0x30, 0x02, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x02,
0x06, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x08, 0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0x08, 0x00, 0x00, 0x00, 0x12, 0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x04, 0x04,
0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
0x2d, 0x00, 0x04, 0x02, 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0x14, 0x00, 0x00, 0x00, 0x37, 0x00, 0x04, 0x02, 0x03, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x40, 0x00, 0x04, 0x02,
0x01, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00,
0x4b, 0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0x68, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x04, 0x04,
0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00,
0x7f, 0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0x74, 0x00, 0x00, 0x00, 0x8a, 0x00, 0x04, 0x02, 0x01, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x9e, 0x00, 0x04, 0x04,
0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00,
0xad, 0x00, 0x04, 0x02, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
0x8c, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0xcb, 0x00, 0x04, 0x02,
0x09, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00,
0xd2, 0x00, 0x04, 0x02, 0x09, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
0xcc, 0x00, 0x00, 0x00, 0xd8, 0x00, 0x04, 0x02, 0x0a, 0x00, 0x00, 0x00,
0x0c, 0x00, 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0xe1, 0x00, 0x04, 0x02,
0x06, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00,
0x41, 0x50, 0x50, 0x5f, 0x56, 0x45, 0x52, 0x00, 0x41, 0x54, 0x54, 0x52,
0x49, 0x42, 0x55, 0x54, 0x45, 0x00, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42,
0x55, 0x54, 0x45, 0x32, 0x00, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55,
0x54, 0x45, 0x5f, 0x4d, 0x49, 0x4e, 0x4f, 0x52, 0x00, 0x42, 0x4f, 0x4f,
0x54, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x00, 0x43, 0x41, 0x54, 0x45, 0x47,
0x4f, 0x52, 0x59, 0x00, 0x43, 0x4f, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f,
0x49, 0x44, 0x00, 0x45, 0x42, 0x4f, 0x4f, 0x54, 0x5f, 0x41, 0x50, 0x50,
0x5f, 0x4d, 0x45, 0x4d, 0x53, 0x49, 0x5a, 0x45, 0x00, 0x45, 0x42, 0x4f,
0x4f, 0x54, 0x5f, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45,
0x00, 0x45, 0x42, 0x4f, 0x4f, 0x54, 0x5f, 0x50, 0x48, 0x59, 0x5f, 0x4d,
0x45, 0x4d, 0x53, 0x49, 0x5a, 0x45, 0x00, 0x4c, 0x41, 0x52, 0x45, 0x41,
0x5f, 0x54, 0x59, 0x50, 0x45, 0x00, 0x4e, 0x50, 0x5f, 0x43, 0x4f, 0x4d,
0x4d, 0x55, 0x4e, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x49,
0x44, 0x00, 0x50, 0x41, 0x52, 0x45, 0x4e, 0x54, 0x41, 0x4c, 0x5f, 0x4c,
0x45, 0x56, 0x45, 0x4c, 0x00, 0x50, 0x53, 0x50, 0x32, 0x5f, 0x44, 0x49,
0x53, 0x50, 0x5f, 0x56, 0x45, 0x52, 0x00, 0x50, 0x53, 0x50, 0x32, 0x5f,
0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x56, 0x45, 0x52, 0x00, 0x53,
0x54, 0x49, 0x54, 0x4c, 0x45, 0x00, 0x54, 0x49, 0x54, 0x4c, 0x45, 0x00,
0x54, 0x49, 0x54, 0x4c, 0x45, 0x5f, 0x49, 0x44, 0x00, 0x56, 0x45, 0x52,
0x53, 0x49, 0x4f, 0x4e, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x2e, 0x30,
0x30, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x67, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x2e, 0x30, 0x30, 0x30, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x6f, 0x6d, 0x65, 0x62, 0x72, 0x65, 0x77,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x6f, 0x6d, 0x65,
0x62, 0x72, 0x65, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x41, 0x42, 0x43, 0x44, 0x39, 0x39, 0x39, 0x39,
0x39, 0x00, 0x00, 0x00, 0x30, 0x30, 0x2e, 0x30, 0x30, 0x00, 0x00, 0x00
};
M_TEST_DEFINE(defaultSfo) {
struct VFile* vf = VFileMemChunk(NULL, 0);
struct Table sfo;
SfoInit(&sfo);
SfoWrite(&sfo, vf);
SfoDeinit(&sfo);
assert_int_equal(vf->size(vf), sizeof(defaultMksfoex));
void* buffer = vf->map(vf, sizeof(defaultMksfoex), MAP_READ);
assert_memory_equal(defaultMksfoex, buffer, sizeof(defaultMksfoex));
vf->unmap(vf, buffer, sizeof(defaultMksfoex));
vf->close(vf);
}
M_TEST_SUITE_DEFINE(Sfo,
cmocka_unit_test(defaultSfo),
)

View File

@ -593,7 +593,11 @@ ssize_t _vfzRead(struct VFile* vf, void* buffer, size_t size) {
ssize_t _vfzWrite(struct VFile* vf, const void* buffer, size_t size) {
struct VFileZip* vfz = (struct VFileZip*) vf;
return zipWriteInFileInZip(vfz->z, buffer, size);
int res = zipWriteInFileInZip(vfz->z, buffer, size);
if (res != ZIP_OK) {
return res;
}
return size;
}
void* _vfzMap(struct VFile* vf, size_t size, int flags) {