diff --git a/CHANGES b/CHANGES index d28e64d8b..80786cc0c 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,7 @@ Features: - 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 crop SGB borders off screen (closes mgba.io/i/1204) + - Cheats: Add support for loading Libretro-style cht files Emulation fixes: - GBA: All IRQs have 7 cycle delay (fixes mgba.io/i/539, mgba.io/i/1208) - GBA: Reset now reloads multiboot ROMs diff --git a/include/mgba-util/string.h b/include/mgba-util/string.h index 7e9465719..fe5f48f59 100644 --- a/include/mgba-util/string.h +++ b/include/mgba-util/string.h @@ -39,6 +39,8 @@ const char* hex4(const char* line, uint8_t* out); void rtrim(char* string); +ssize_t parseQuotedString(const char* unparsed, ssize_t unparsedLen, char* parsed, ssize_t parsedLen); + CXX_GUARD_END #endif diff --git a/include/mgba/core/cheats.h b/include/mgba/core/cheats.h index 32c7cad42..dbc94199f 100644 --- a/include/mgba/core/cheats.h +++ b/include/mgba/core/cheats.h @@ -100,6 +100,8 @@ void mCheatRemoveSet(struct mCheatDevice*, struct mCheatSet*); bool mCheatParseFile(struct mCheatDevice*, struct VFile*); bool mCheatSaveFile(struct mCheatDevice*, struct VFile*); +bool mCheatParseLibretroFile(struct mCheatDevice*, struct VFile*); + #if !defined(MINIMAL_CORE) || MINIMAL_CORE < 2 void mCheatAutosave(struct mCheatDevice*); #endif diff --git a/src/core/cheats.c b/src/core/cheats.c index 464ebfc39..92b6aa778 100644 --- a/src/core/cheats.c +++ b/src/core/cheats.c @@ -9,7 +9,8 @@ #include #include -#define MAX_LINE_LENGTH 128 +#define MAX_LINE_LENGTH 512 +#define MAX_CHEATS 1000 const uint32_t M_CHEAT_DEVICE_ID = 0xABADC0DE; @@ -191,6 +192,12 @@ bool mCheatParseFile(struct mCheatDevice* device, struct VFile* vf) { break; default: 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->enabled = !nextDisabled; nextDisabled = false; @@ -211,6 +218,112 @@ bool mCheatParseFile(struct mCheatDevice* device, struct VFile* vf) { 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) { static const char lineStart[3] = "# "; static const char lineEnd = '\n'; diff --git a/src/platform/qt/CheatsView.cpp b/src/platform/qt/CheatsView.cpp index 315edcae1..4a631c932 100644 --- a/src/platform/qt/CheatsView.cpp +++ b/src/platform/qt/CheatsView.cpp @@ -109,14 +109,14 @@ bool CheatsView::eventFilter(QObject* object, QEvent* event) { } 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()) { m_model.loadFile(filename); } } 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()) { m_model.saveFile(filename); } diff --git a/src/util/string.c b/src/util/string.c index cef28e5cd..f3835f04b 100644 --- a/src/util/string.c +++ b/src/util/string.c @@ -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 * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -373,3 +373,64 @@ void rtrim(char* string) { --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; +} \ No newline at end of file diff --git a/src/util/test/string-parser.c b/src/util/test/string-parser.c new file mode 100644 index 000000000..38650d0c9 --- /dev/null +++ b/src/util/test/string-parser.c @@ -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 + +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))