#include "rc_internal.h" #include "rc_url.h" #include "rc_api_runtime.h" #include "rc_consoles.h" #include "rc_validate.h" #include #include #include #include #include /* memset */ #include #ifdef _MSC_VER /* windows build */ #define WIN32_LEAN_AND_MEAN #include #else #include #include #define stricmp strcasecmp #endif /* usage exmaple: * * ./validator.exe d "E:\RetroAchievements\dump" | sort > results.txt * grep -v ": OK" results.txt | grep -v "File: " | grep . */ static const char* flag_string(char type) { switch (type) { case RC_CONDITION_STANDARD: return ""; case RC_CONDITION_PAUSE_IF: return "PauseIf "; case RC_CONDITION_RESET_IF: return "ResetIf "; case RC_CONDITION_MEASURED_IF: return "MeasuredIf "; case RC_CONDITION_TRIGGER: return "Trigger "; case RC_CONDITION_MEASURED: return "Measured "; case RC_CONDITION_ADD_SOURCE: return "AddSource "; case RC_CONDITION_SUB_SOURCE: return "SubSource "; case RC_CONDITION_REMEMBER: return "Remember "; case RC_CONDITION_ADD_ADDRESS: return "AddAddress "; case RC_CONDITION_ADD_HITS: return "AddHits "; case RC_CONDITION_SUB_HITS: return "SubHits "; case RC_CONDITION_RESET_NEXT_IF: return "ResetNextIf "; case RC_CONDITION_AND_NEXT: return "AndNext "; case RC_CONDITION_OR_NEXT: return "OrNext "; default: return "Unknown"; } } static const char* type_string(char type) { switch (type) { case RC_OPERAND_ADDRESS: return "Mem"; case RC_OPERAND_DELTA: return "Delta"; case RC_OPERAND_CONST: return "Value"; case RC_OPERAND_FP: return "Float"; case RC_OPERAND_LUA: return "Lua"; case RC_OPERAND_PRIOR: return "Prior"; case RC_OPERAND_BCD: return "BCD"; case RC_OPERAND_INVERTED: return "Inverted"; case RC_OPERAND_RECALL: return "Recall"; default: return "Unknown"; } } static const char* size_string(char size) { switch (size) { case RC_MEMSIZE_8_BITS: return "8-bit"; case RC_MEMSIZE_16_BITS: return "16-bit"; case RC_MEMSIZE_24_BITS: return "24-bit"; case RC_MEMSIZE_32_BITS: return "32-bit"; case RC_MEMSIZE_LOW: return "Lower4"; case RC_MEMSIZE_HIGH: return "Upper4"; case RC_MEMSIZE_BIT_0: return "Bit0"; case RC_MEMSIZE_BIT_1: return "Bit1"; case RC_MEMSIZE_BIT_2: return "Bit2"; case RC_MEMSIZE_BIT_3: return "Bit3"; case RC_MEMSIZE_BIT_4: return "Bit4"; case RC_MEMSIZE_BIT_5: return "Bit5"; case RC_MEMSIZE_BIT_6: return "Bit6"; case RC_MEMSIZE_BIT_7: return "Bit7"; case RC_MEMSIZE_BITCOUNT: return "BitCount"; case RC_MEMSIZE_16_BITS_BE: return "16-bit BE"; case RC_MEMSIZE_24_BITS_BE: return "24-bit BE"; case RC_MEMSIZE_32_BITS_BE: return "32-bit BE"; case RC_MEMSIZE_VARIABLE: return "Variable"; default: return "Unknown"; } } static const char* operator_string(char oper) { switch (oper) { case RC_OPERATOR_NONE: return ""; case RC_OPERATOR_AND: return "&"; case RC_OPERATOR_XOR: return "^"; case RC_OPERATOR_MULT: return "*"; case RC_OPERATOR_DIV: return "/"; case RC_OPERATOR_MOD: return "%"; case RC_OPERATOR_ADD: return "+"; case RC_OPERATOR_SUB: return "-"; case RC_OPERATOR_EQ: return "="; case RC_OPERATOR_NE: return "!="; case RC_OPERATOR_GE: return ">="; case RC_OPERATOR_GT: return ">"; case RC_OPERATOR_LE: return "<="; case RC_OPERATOR_LT: return "<"; default: return "?"; } } static void append_condition(char result[], size_t result_size, const rc_condition_t* cond) { const char* flag = flag_string(cond->type); const char* src_type = type_string(cond->operand1.type); const char* tgt_type = type_string(cond->operand2.type); const char* cmp = operator_string(cond->oper); char val1[32], val2[32]; if (rc_operand_is_memref(&cond->operand1)) snprintf(val1, sizeof(val1), "%s 0x%06x", size_string(cond->operand1.size), cond->operand1.value.memref->address); else snprintf(val1, sizeof(val1), "0x%06x", cond->operand1.value.num); if (rc_operand_is_memref(&cond->operand2)) snprintf(val2, sizeof(val2), "%s 0x%06x", size_string(cond->operand2.size), cond->operand2.value.memref->address); else snprintf(val2, sizeof(val2), "0x%06x", cond->operand2.value.num); const size_t message_len = strlen(result); result += message_len; result_size -= message_len; if (cond->oper == RC_OPERATOR_NONE) snprintf(result, result_size, ": %s%s %s", flag, src_type, val1); else snprintf(result, result_size, ": %s%s %s %s %s %s", flag, src_type, val1, cmp, tgt_type, val2); } static void append_invalid_condition(char result[], size_t result_size, const rc_condset_t* condset) { if (strncmp(result, "Condition ", 10) == 0) { int index = atoi(&result[10]); const rc_condition_t* cond; for (cond = condset->conditions; cond; cond = cond->next) { if (--index == 0) { const size_t error_length = strlen(result); append_condition(result + error_length, result_size - error_length, cond); return; } } } } static void append_invalid_trigger_condition(char result[], size_t result_size, const rc_trigger_t* trigger) { if (strncmp(result, "Alt", 3) == 0) { int index = atoi(&result[3]); const rc_condset_t* condset; for (condset = trigger->alternative; condset; condset = condset->next) { if (--index == 0) { result += 4; result_size -= 4; while (isdigit(*result)) { ++result; --result_size; } ++result; --result_size; append_invalid_condition(result, result_size, condset); return; } } } else if (strncmp(result, "Core ", 5) == 0) { append_invalid_condition(result + 5, result_size - 5, trigger->requirement); } else { append_invalid_condition(result, result_size, trigger->requirement); } } static int validate_trigger(const char* trigger, char result[], size_t result_size, uint32_t console_id) { char* buffer; rc_trigger_t* compiled; int success = 0; int ret = rc_trigger_size(trigger); if (ret < 0) { snprintf(result, result_size, "%s", rc_error_str(ret)); return 0; } buffer = (char*)malloc(ret + 4); if (!buffer) { snprintf(result, result_size, "malloc failed"); return 0; } memset(buffer + ret, 0xCD, 4); compiled = rc_parse_trigger(buffer, trigger, NULL, 0); if (compiled == NULL) { snprintf(result, result_size, "parse failed"); } else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { snprintf(result, result_size, "write past end of buffer"); } else if (rc_validate_trigger_for_console(compiled, result, result_size, console_id)) { snprintf(result, result_size, "%d OK", ret); success = 1; } else { append_invalid_trigger_condition(result, result_size, compiled); } free(buffer); return success; } static int validate_leaderboard(const char* leaderboard, char result[], const size_t result_size, uint32_t console_id) { char* buffer; rc_lboard_t* compiled; int success = 0; int ret = rc_lboard_size(leaderboard); if (ret < 0) { /* generic problem parsing the leaderboard, attempt to report where */ const char* start = leaderboard; char part[4] = { 0,0,0,0 }; do { char* next = strstr(start, "::"); part[0] = toupper((int)start[0]); part[1] = toupper((int)start[1]); part[2] = toupper((int)start[2]); start += 4; if (strcmp(part, "VAL") == 0) { int ret2 = rc_value_size(start); if (ret2 == ret) { snprintf(result, result_size, "%s: %s", part, rc_error_str(ret)); return 0; } } else { int ret2 = rc_trigger_size(start); if (ret2 == ret) { snprintf(result, result_size, "%s: %s", part, rc_error_str(ret)); return 0; } } if (!next) break; start = next + 2; } while (1); snprintf(result, result_size, "%s", rc_error_str(ret)); return 0; } buffer = (char*)malloc(ret + 4); if (!buffer) { snprintf(result, result_size, "malloc failed"); return 0; } memset(buffer + ret, 0xCD, 4); compiled = rc_parse_lboard(buffer, leaderboard, NULL, 0); if (compiled == NULL) { snprintf(result, result_size, "parse failed"); } else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { snprintf(result, result_size, "write past end of buffer"); } else { snprintf(result, result_size, "STA: "); success = rc_validate_trigger_for_console(&compiled->start, result + 5, result_size - 5, console_id); if (!success) { append_invalid_trigger_condition(result + 5, result_size - 5, &compiled->start); } else { snprintf(result, result_size, "SUB: "); success = rc_validate_trigger_for_console(&compiled->submit, result + 5, result_size - 5, console_id); if (!success) { append_invalid_trigger_condition(result + 5, result_size - 5, &compiled->submit); } else { snprintf(result, result_size, "CAN: "); success = rc_validate_trigger_for_console(&compiled->cancel, result + 5, result_size - 5, console_id); if (!success) { append_invalid_trigger_condition(result + 5, result_size - 5, &compiled->cancel); } else { snprintf(result, result_size, "%d OK", ret); } } } } free(buffer); return success; } static int validate_macros(const rc_richpresence_t* richpresence, const char* script, char result[], const size_t result_size) { const uint16_t RC_FORMAT_UNKNOWN_MACRO = 103; /* enum not exposed by header */ rc_richpresence_display_t* display = richpresence->first_display; while (display != NULL) { rc_richpresence_display_part_t* part = display->display; while (part != NULL) { if (part->display_type == RC_FORMAT_UNKNOWN_MACRO) { /* include opening parenthesis to prevent partial match */ size_t macro_len = strchr(part->text, '(') - part->text + 1; /* find the display portion of the script */ const char* ptr = script; int line = 1; while (strncmp(ptr, "Display:", 8) != 0) { while (*ptr != '\n') ++ptr; ++line; ++ptr; } /* find the first matching reference to the unknown macro */ do { while (*ptr != '@') { if (*ptr == '\n') ++line; if (*ptr == '\0') { /* unexpected, but prevent potential infinite loop */ snprintf(result, result_size, "Unknown macro \"%.*s\"", (int)(macro_len - 1), part->text); return 0; } ++ptr; } ++ptr; if (strncmp(ptr, part->text, macro_len) == 0) { snprintf(result, result_size, "Line %d: Unknown macro \"%.*s\"", line, (int)(macro_len - 1), part->text); return 0; } } while (1); } part = part->next; } display = display->next; } return 1; } static int validate_richpresence(const char* script, char result[], const size_t result_size, uint32_t console_id) { char* buffer; rc_richpresence_t* compiled; int lines; int success = 0; int ret = rc_richpresence_size_lines(script, &lines); if (ret < 0) { snprintf(result, result_size, "Line %d: %s", lines, rc_error_str(ret)); return 0; } buffer = (char*)malloc(ret + 4); if (!buffer) { snprintf(result, result_size, "malloc failed"); return 0; } memset(buffer + ret, 0xCD, 4); compiled = rc_parse_richpresence(buffer, script, NULL, 0); if (compiled == NULL) { snprintf(result, result_size, "parse failed"); } else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { snprintf(result, result_size, "write past end of buffer"); } else { const rc_richpresence_display_t* display; int index = 1; for (display = compiled->first_display; display; display = display->next) { const size_t prefix_length = snprintf(result, result_size, "Display%d: ", index++); success = rc_validate_trigger_for_console(&display->trigger, result + prefix_length, result_size - prefix_length, console_id); if (!success) break; } if (success) success = rc_validate_memrefs_for_console(compiled->memrefs, result, result_size, console_id); if (success) success = validate_macros(compiled, script, result, result_size); if (success) snprintf(result, result_size, "%d OK", ret); } free(buffer); return success; } static void validate_richpresence_file(const char* richpresence_file, char result[], const size_t result_size) { char* file_contents; size_t file_size; FILE* file; fopen_s(&file, richpresence_file, "rb"); if (!file) { snprintf(result, result_size, "could not open file"); return; } fseek(file, 0, SEEK_END); file_size = ftell(file); fseek(file, 0, SEEK_SET); file_contents = (char*)malloc(file_size + 1); if (!file_contents) { snprintf(result, result_size, "malloc failed"); return; } fread(file_contents, 1, file_size, file); file_contents[file_size] = '\0'; fclose(file); validate_richpresence(file_contents, result, result_size, 0); free(file_contents); } static int validate_patchdata_file(const char* patchdata_file, const char* filename, int errors_only) { char* file_contents; size_t file_size; FILE* file; rc_api_fetch_game_data_response_t fetch_game_data_response; int result; size_t i; char file_title[256]; char buffer[256]; int success = 1; fopen_s(&file, patchdata_file, "rb"); if (!file) { printf("File: %s: could not open file\n", filename); return 0; } fseek(file, 0, SEEK_END); file_size = ftell(file); fseek(file, 0, SEEK_SET); file_contents = (char*)malloc(file_size + 1); fread(file_contents, 1, file_size, file); file_contents[file_size] = '\0'; fclose(file); /* rc_api_process_fetch_game_data_response expects the PatchData to be * a subobject, but the DLL strips that when writing to the RACache. * if it looks like the nested object, wrap it again */ if (strncmp(file_contents, "{\"ID\":", 6) == 0) { char* expanded_contents = (char*)malloc(file_size + 15); memcpy(expanded_contents, "{\"PatchData\":", 13); memcpy(&expanded_contents[13], file_contents, file_size); expanded_contents[file_size + 13] = '}'; expanded_contents[file_size + 14] = '\0'; free(file_contents); file_contents = expanded_contents; } result = rc_api_process_fetch_game_data_response(&fetch_game_data_response, file_contents); if (result != RC_OK) { if (fetch_game_data_response.response.error_message) printf("File: %s: %s\n", filename, fetch_game_data_response.response.error_message); else printf("File: %s: %s\n", filename, rc_error_str(result)); return 0; } free(file_contents); snprintf(file_title, sizeof(file_title), "File: %s: %s\n", filename, fetch_game_data_response.title); if (fetch_game_data_response.rich_presence_script && *fetch_game_data_response.rich_presence_script) { result = validate_richpresence(fetch_game_data_response.rich_presence_script, buffer, sizeof(buffer), fetch_game_data_response.console_id); success &= result; if (!result || !errors_only) { printf("%s", file_title); file_title[0] = '\0'; printf(" rich presence %d: %s\n", fetch_game_data_response.id, buffer); } } for (i = 0; i < fetch_game_data_response.num_achievements; ++i) { const char* trigger = fetch_game_data_response.achievements[i].definition; result = validate_trigger(trigger, buffer, sizeof(buffer), fetch_game_data_response.console_id); success &= result; if (!result || !errors_only) { if (file_title[0]) { printf("%s", file_title); file_title[0] = '\0'; } printf(" achievement %d%s: %s\n", fetch_game_data_response.achievements[i].id, (fetch_game_data_response.achievements[i].category == 3) ? "" : " (Unofficial)", buffer); } } for (i = 0; i < fetch_game_data_response.num_leaderboards; ++i) { result = validate_leaderboard(fetch_game_data_response.leaderboards[i].definition, buffer, sizeof(buffer), fetch_game_data_response.console_id); success &= result; if (!result || !errors_only) { if (file_title[0]) { printf("%s", file_title); file_title[0] = '\0'; } printf(" leaderboard %d: %s\n", fetch_game_data_response.leaderboards[i].id, buffer); } } rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); return success; } #ifdef _MSC_VER static void validate_patchdata_directory(const char* patchdata_directory, int errors_only) { WIN32_FIND_DATA fdFile; HANDLE hFind = NULL; int need_newline = 0; char filename[MAX_PATH]; snprintf(filename, sizeof(filename), "%s\\*.json", patchdata_directory); if ((hFind = FindFirstFile(filename, &fdFile)) == INVALID_HANDLE_VALUE) { printf("failed to open directory"); return; } do { if (!(fdFile.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { if (need_newline) { printf("\n"); need_newline = 0; } snprintf(filename, sizeof(filename), "%s\\%s", patchdata_directory, fdFile.cFileName); if (!validate_patchdata_file(filename, fdFile.cFileName, errors_only) || !errors_only) need_newline = 1; } } while(FindNextFile(hFind, &fdFile)); FindClose(hFind); } #else static void validate_patchdata_directory(const char* patchdata_directory, int errors_only) { struct dirent* entry; char* filename; size_t filename_len; char path[2048]; int need_newline = 0; DIR* dir = opendir(patchdata_directory); if (!dir) { printf("failed to open directory"); return; } while ((entry = readdir(dir)) != NULL) { filename = entry->d_name; filename_len = strlen(filename); if (filename_len > 5 && stricmp(&filename[filename_len - 5], ".json") == 0) { if (need_newline) { printf("\n"); need_newline = 0; } sprintf(path, "%s/%s", patchdata_directory, filename); if (!validate_patchdata_file(path, filename, errors_only) || !errors_only) need_newline = 1; } } closedir(dir); } #endif static int usage() { printf("validator [type] [data]\n" "\n" "where [type] is one of the following:\n" " a achievement, [data] = trigger definition\n" " l leaderboard, [data] = leaderboard definition\n" " r rich presence, [data] = path to rich presence script\n" " f patchdata file, [data] = path to patchdata json file\n" " d patchdata directory, [data] = path to directory containing one or more patchdata json files\n" " e same as 'd', but only reports errors\n" ); return 0; } int main(int argc, char* argv[]) { char buffer[256]; if (argc < 3) return usage(); switch (argv[1][0]) { case 'a': validate_trigger(argv[2], buffer, sizeof(buffer), 0); printf("Achievement: %s\n", buffer); break; case 'l': validate_leaderboard(argv[2], buffer, sizeof(buffer), 0); printf("Leaderboard: %s\n", buffer); break; case 'r': validate_richpresence_file(argv[2], buffer, sizeof(buffer)); printf("Rich Presence: %s\n", buffer); break; case 'f': validate_patchdata_file(argv[2], argv[2], 0); break; case 'd': printf("Directory: %s:\n", argv[2]); validate_patchdata_directory(argv[2], 0); break; case 'e': printf("Directory: %s:\n", argv[2]); validate_patchdata_directory(argv[2], 1); break; default: return usage(); } return 0; }