mirror of https://github.com/mgba-emu/mgba.git
Core: Add support for loading Libretro-style cht files
This commit is contained in:
parent
2d04d03d32
commit
3f044a5791
1
CHANGES
1
CHANGES
|
@ -15,6 +15,7 @@ Features:
|
||||||
- Switch: Option to use built-in brightness sensor for Boktai
|
- Switch: Option to use built-in brightness sensor for Boktai
|
||||||
- Ports: Ability to enable or disable all SGB features (closes mgba.io/i/1205)
|
- Ports: Ability to enable or disable all SGB features (closes mgba.io/i/1205)
|
||||||
- Ports: Ability to crop SGB borders off screen (closes mgba.io/i/1204)
|
- Ports: Ability to crop SGB borders off screen (closes mgba.io/i/1204)
|
||||||
|
- Cheats: Add support for loading Libretro-style cht files
|
||||||
Emulation fixes:
|
Emulation fixes:
|
||||||
- GBA: All IRQs have 7 cycle delay (fixes mgba.io/i/539, mgba.io/i/1208)
|
- GBA: All IRQs have 7 cycle delay (fixes mgba.io/i/539, mgba.io/i/1208)
|
||||||
- GBA: Reset now reloads multiboot ROMs
|
- GBA: Reset now reloads multiboot ROMs
|
||||||
|
|
|
@ -39,6 +39,8 @@ const char* hex4(const char* line, uint8_t* out);
|
||||||
|
|
||||||
void rtrim(char* string);
|
void rtrim(char* string);
|
||||||
|
|
||||||
|
ssize_t parseQuotedString(const char* unparsed, ssize_t unparsedLen, char* parsed, ssize_t parsedLen);
|
||||||
|
|
||||||
CXX_GUARD_END
|
CXX_GUARD_END
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -100,6 +100,8 @@ void mCheatRemoveSet(struct mCheatDevice*, struct mCheatSet*);
|
||||||
bool mCheatParseFile(struct mCheatDevice*, struct VFile*);
|
bool mCheatParseFile(struct mCheatDevice*, struct VFile*);
|
||||||
bool mCheatSaveFile(struct mCheatDevice*, struct VFile*);
|
bool mCheatSaveFile(struct mCheatDevice*, struct VFile*);
|
||||||
|
|
||||||
|
bool mCheatParseLibretroFile(struct mCheatDevice*, struct VFile*);
|
||||||
|
|
||||||
#if !defined(MINIMAL_CORE) || MINIMAL_CORE < 2
|
#if !defined(MINIMAL_CORE) || MINIMAL_CORE < 2
|
||||||
void mCheatAutosave(struct mCheatDevice*);
|
void mCheatAutosave(struct mCheatDevice*);
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
#include <mgba-util/string.h>
|
#include <mgba-util/string.h>
|
||||||
#include <mgba-util/vfs.h>
|
#include <mgba-util/vfs.h>
|
||||||
|
|
||||||
#define MAX_LINE_LENGTH 128
|
#define MAX_LINE_LENGTH 512
|
||||||
|
#define MAX_CHEATS 1000
|
||||||
|
|
||||||
const uint32_t M_CHEAT_DEVICE_ID = 0xABADC0DE;
|
const uint32_t M_CHEAT_DEVICE_ID = 0xABADC0DE;
|
||||||
|
|
||||||
|
@ -191,6 +192,12 @@ bool mCheatParseFile(struct mCheatDevice* device, struct VFile* vf) {
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (!set) {
|
if (!set) {
|
||||||
|
if (strncmp(cheat, "cheats = ", 9) == 0) {
|
||||||
|
// This is in libretro format, switch over to that parser
|
||||||
|
vf->seek(vf, 0, SEEK_SET);
|
||||||
|
StringListDeinit(&directives);
|
||||||
|
return mCheatParseLibretroFile(device, vf);
|
||||||
|
}
|
||||||
set = device->createSet(device, NULL);
|
set = device->createSet(device, NULL);
|
||||||
set->enabled = !nextDisabled;
|
set->enabled = !nextDisabled;
|
||||||
nextDisabled = false;
|
nextDisabled = false;
|
||||||
|
@ -211,6 +218,112 @@ bool mCheatParseFile(struct mCheatDevice* device, struct VFile* vf) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool mCheatParseLibretroFile(struct mCheatDevice* device, struct VFile* vf) {
|
||||||
|
char cheat[MAX_LINE_LENGTH];
|
||||||
|
char parsed[MAX_LINE_LENGTH];
|
||||||
|
struct mCheatSet* set = NULL;
|
||||||
|
unsigned long i = 0;
|
||||||
|
bool startFound = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
ssize_t bytesRead = vf->readline(vf, cheat, sizeof(cheat));
|
||||||
|
if (bytesRead == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (bytesRead < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cheat[0] == '\n') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (strncmp(cheat, "cheat", 5) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char* underscore = strchr(&cheat[5], '_');
|
||||||
|
if (!underscore) {
|
||||||
|
if (!startFound && cheat[5] == 's') {
|
||||||
|
startFound = true;
|
||||||
|
char* eq = strchr(&cheat[6], '=');
|
||||||
|
if (!eq) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
++eq;
|
||||||
|
while (isspace((int) eq[0])) {
|
||||||
|
if (eq[0] == '\0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
++eq;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* end;
|
||||||
|
unsigned long nCheats = strtoul(eq, &end, 10);
|
||||||
|
if (end[0] != '\0' && !isspace(end[0])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nCheats > MAX_CHEATS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (nCheats > mCheatSetsSize(&device->cheats)) {
|
||||||
|
struct mCheatSet* newSet = device->createSet(device, NULL);
|
||||||
|
if (!newSet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mCheatAddSet(device, newSet);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char* underscore2;
|
||||||
|
i = strtoul(&cheat[5], &underscore2, 10);
|
||||||
|
if (underscore2 != underscore) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
++underscore;
|
||||||
|
char* eq = strchr(underscore, '=');
|
||||||
|
if (!eq) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
++eq;
|
||||||
|
while (isspace((int) eq[0])) {
|
||||||
|
if (eq[0] == '\0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
++eq;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i >= mCheatSetsSize(&device->cheats)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
set = *mCheatSetsGetPointer(&device->cheats, i);
|
||||||
|
|
||||||
|
if (strncmp(underscore, "desc", 4) == 0) {
|
||||||
|
parseQuotedString(eq, strlen(eq), parsed, sizeof(parsed));
|
||||||
|
mCheatSetRename(set, parsed);
|
||||||
|
} else if (strncmp(underscore, "enable", 6) == 0) {
|
||||||
|
set->enabled = strncmp(eq, "true\n", 5) == 0;
|
||||||
|
} else if (strncmp(underscore, "code", 4) == 0) {
|
||||||
|
parseQuotedString(eq, strlen(eq), parsed, sizeof(parsed));
|
||||||
|
char* cur = parsed;
|
||||||
|
char* next;
|
||||||
|
while ((next = strchr(cur, '+'))) {
|
||||||
|
next[0] = '\0';
|
||||||
|
mCheatAddLine(set, cur, 0);
|
||||||
|
cur = &next[1];
|
||||||
|
}
|
||||||
|
mCheatAddLine(set, cur, 0);
|
||||||
|
|
||||||
|
for (++i; i < mCheatSetsSize(&device->cheats); ++i) {
|
||||||
|
struct mCheatSet* newSet = *mCheatSetsGetPointer(&device->cheats, i);
|
||||||
|
newSet->copyProperties(newSet, set);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool mCheatSaveFile(struct mCheatDevice* device, struct VFile* vf) {
|
bool mCheatSaveFile(struct mCheatDevice* device, struct VFile* vf) {
|
||||||
static const char lineStart[3] = "# ";
|
static const char lineStart[3] = "# ";
|
||||||
static const char lineEnd = '\n';
|
static const char lineEnd = '\n';
|
||||||
|
|
|
@ -109,14 +109,14 @@ bool CheatsView::eventFilter(QObject* object, QEvent* event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void CheatsView::load() {
|
void CheatsView::load() {
|
||||||
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select cheats file"), tr(("Cheats file (*.cheats *.cht *.clt)")));
|
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select cheats file"), tr(("Cheats file (*.cheats *.cht)")));
|
||||||
if (!filename.isEmpty()) {
|
if (!filename.isEmpty()) {
|
||||||
m_model.loadFile(filename);
|
m_model.loadFile(filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CheatsView::save() {
|
void CheatsView::save() {
|
||||||
QString filename = GBAApp::app()->getSaveFileName(this, tr("Select cheats file"), tr(("Cheats file (*.cheats *.cht *.clt)")));
|
QString filename = GBAApp::app()->getSaveFileName(this, tr("Select cheats file"), tr(("Cheats file (*.cheats)")));
|
||||||
if (!filename.isEmpty()) {
|
if (!filename.isEmpty()) {
|
||||||
m_model.saveFile(filename);
|
m_model.saveFile(filename);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright (c) 2013-2015 Jeffrey Pfau
|
/* Copyright (c) 2013-2019 Jeffrey Pfau
|
||||||
*
|
*
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
@ -373,3 +373,64 @@ void rtrim(char* string) {
|
||||||
--end;
|
--end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ssize_t parseQuotedString(const char* unparsed, ssize_t unparsedLen, char* parsed, ssize_t parsedLen) {
|
||||||
|
memset(parsed, 0, parsedLen);
|
||||||
|
bool escaped = false;
|
||||||
|
char start = '\0';
|
||||||
|
ssize_t len = 0;
|
||||||
|
ssize_t i;
|
||||||
|
for (i = 0; i < unparsedLen && len < parsedLen; ++i) {
|
||||||
|
if (i == 0) {
|
||||||
|
switch (unparsed[0]) {
|
||||||
|
case '"':
|
||||||
|
case '\'':
|
||||||
|
start = unparsed[0];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (escaped) {
|
||||||
|
switch (unparsed[i]) {
|
||||||
|
case 'n':
|
||||||
|
parsed[len] = '\n';
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
parsed[len] = '\r';
|
||||||
|
break;
|
||||||
|
case '\\':
|
||||||
|
parsed[len] = '\\';
|
||||||
|
break;
|
||||||
|
case '\'':
|
||||||
|
parsed[len] = '\'';
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
parsed[len] = '"';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
escaped = false;
|
||||||
|
++len;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (unparsed[i] == start) {
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
switch (unparsed[i]) {
|
||||||
|
case '\\':
|
||||||
|
escaped = true;
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
case '\r':
|
||||||
|
return len;
|
||||||
|
default:
|
||||||
|
parsed[len] = unparsed[i];
|
||||||
|
++len;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
/* Copyright (c) 2013-2019 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/string.h>
|
||||||
|
|
||||||
|
M_TEST_DEFINE(nullString) {
|
||||||
|
const char* unparsed = "";
|
||||||
|
char parsed[2];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, 1, parsed, sizeof(parsed)), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(emptyString2) {
|
||||||
|
const char* unparsed = "\"\"";
|
||||||
|
char parsed[2];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed) + 1, parsed, sizeof(parsed)), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(emptyString) {
|
||||||
|
const char* unparsed = "''";
|
||||||
|
char parsed[2];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed) + 1, parsed, sizeof(parsed)), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(plainString) {
|
||||||
|
const char* unparsed = "\"plain\"";
|
||||||
|
char parsed[32];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed) + 1, parsed, sizeof(parsed)), strlen("plain"));
|
||||||
|
assert_string_equal(parsed, "plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(plainString2) {
|
||||||
|
const char* unparsed = "\"plain\"";
|
||||||
|
char parsed[32];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed), parsed, sizeof(parsed)), strlen("plain"));
|
||||||
|
assert_string_equal(parsed, "plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(trailingString) {
|
||||||
|
const char* unparsed = "\"trailing\"T";
|
||||||
|
char parsed[32];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed) + 1, parsed, sizeof(parsed)), strlen("trailing"));
|
||||||
|
assert_string_equal(parsed, "trailing");
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(leadingString) {
|
||||||
|
const char* unparsed = "L\"leading\"";
|
||||||
|
char parsed[32];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed) + 1, parsed, sizeof(parsed)), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(backslashString) {
|
||||||
|
const char* unparsed = "\"back\\\\slash\"";
|
||||||
|
char parsed[32];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed) + 1, parsed, sizeof(parsed)), strlen("back\\slash"));
|
||||||
|
assert_string_equal(parsed, "back\\slash");
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(doubleBackslashString) {
|
||||||
|
const char* unparsed = "\"back\\\\\\\\slash\"";
|
||||||
|
char parsed[32];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed) + 1, parsed, sizeof(parsed)), strlen("back\\\\slash"));
|
||||||
|
assert_string_equal(parsed, "back\\\\slash");
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(escapeCharsString) {
|
||||||
|
const char* unparsed = "\"\\\"\\'\\n\\r\\\\\"";
|
||||||
|
char parsed[32];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed) + 1, parsed, sizeof(parsed)), strlen("\"'\n\r\\"));
|
||||||
|
assert_string_equal(parsed, "\"'\n\r\\");
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(invalidEscapeCharString) {
|
||||||
|
const char* unparsed = "\"\\z\"";
|
||||||
|
char parsed[32];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed) + 1, parsed, sizeof(parsed)), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(overflowString) {
|
||||||
|
const char* unparsed = "\"longstring\"";
|
||||||
|
char parsed[4];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed) + 1, parsed, sizeof(parsed)), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(unclosedString) {
|
||||||
|
const char* unparsed = "\"forever";
|
||||||
|
char parsed[32];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed) + 1, parsed, sizeof(parsed)), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(unclosedString2) {
|
||||||
|
const char* unparsed = "\"forever";
|
||||||
|
char parsed[32];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, 4, parsed, sizeof(parsed)), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(unclosedBackslashString) {
|
||||||
|
const char* unparsed = "\"backslash\\";
|
||||||
|
char parsed[32];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed), parsed, sizeof(parsed)), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(unclosedBackslashString2) {
|
||||||
|
const char* unparsed = "\"backslash\\";
|
||||||
|
char parsed[32];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed) + 1, parsed, sizeof(parsed)), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_DEFINE(highCharacterString) {
|
||||||
|
const char* unparsed = "\"\200\"";
|
||||||
|
char parsed[32];
|
||||||
|
assert_int_equal(parseQuotedString(unparsed, strlen(unparsed) + 1, parsed, sizeof(parsed)), 1);
|
||||||
|
assert_string_equal(parsed, "\200");
|
||||||
|
}
|
||||||
|
|
||||||
|
M_TEST_SUITE_DEFINE(StringParser,
|
||||||
|
cmocka_unit_test(nullString),
|
||||||
|
cmocka_unit_test(emptyString),
|
||||||
|
cmocka_unit_test(emptyString2),
|
||||||
|
cmocka_unit_test(plainString),
|
||||||
|
cmocka_unit_test(plainString2),
|
||||||
|
cmocka_unit_test(leadingString),
|
||||||
|
cmocka_unit_test(trailingString),
|
||||||
|
cmocka_unit_test(backslashString),
|
||||||
|
cmocka_unit_test(doubleBackslashString),
|
||||||
|
cmocka_unit_test(escapeCharsString),
|
||||||
|
cmocka_unit_test(invalidEscapeCharString),
|
||||||
|
cmocka_unit_test(overflowString),
|
||||||
|
cmocka_unit_test(unclosedString),
|
||||||
|
cmocka_unit_test(unclosedString2),
|
||||||
|
cmocka_unit_test(unclosedBackslashString),
|
||||||
|
cmocka_unit_test(unclosedBackslashString2),
|
||||||
|
cmocka_unit_test(highCharacterString))
|
Loading…
Reference in New Issue