diff --git a/dep/libchdr/include/dr_libs/dr_flac.h b/dep/libchdr/include/dr_libs/dr_flac.h index 0aeee497a..0c43eed7f 100644 --- a/dep/libchdr/include/dr_libs/dr_flac.h +++ b/dep/libchdr/include/dr_libs/dr_flac.h @@ -1,6 +1,6 @@ /* FLAC audio decoder. Choice of public domain or MIT-0. See license statements at the end of this file. -dr_flac - v0.12.37 - 2022-02-12 +dr_flac - v0.12.39 - 2022-09-17 David Reid - mackron@gmail.com @@ -210,6 +210,9 @@ Build Options #define DR_FLAC_NO_SIMD Disables SIMD optimizations (SSE on x86/x64 architectures, NEON on ARM architectures). Use this if you are having compatibility issues with your compiler. +#define DR_FLAC_NO_WCHAR + Disables all functions ending with `_w`. Use this if your compiler does not provide wchar.h. Not required if DR_FLAC_NO_STDIO is also defined. + Notes @@ -232,7 +235,7 @@ extern "C" { #define DRFLAC_VERSION_MAJOR 0 #define DRFLAC_VERSION_MINOR 12 -#define DRFLAC_VERSION_REVISION 37 +#define DRFLAC_VERSION_REVISION 39 #define DRFLAC_VERSION_STRING DRFLAC_XSTRINGIFY(DRFLAC_VERSION_MAJOR) "." DRFLAC_XSTRINGIFY(DRFLAC_VERSION_MINOR) "." DRFLAC_XSTRINGIFY(DRFLAC_VERSION_REVISION) #include /* For size_t. */ @@ -383,15 +386,13 @@ typedef enum drflac_seek_origin_current } drflac_seek_origin; -/* Packing is important on this structure because we map this directly to the raw data within the SEEKTABLE metadata block. */ -#pragma pack(2) +/* The order of members in this structure is important because we map this directly to the raw data within the SEEKTABLE metadata block. */ typedef struct { drflac_uint64 firstPCMFrame; drflac_uint64 flacFrameOffset; /* The offset from the first byte of the header of the first frame. */ drflac_uint16 pcmFrameCount; } drflac_seekpoint; -#pragma pack() typedef struct { @@ -1280,15 +1281,13 @@ typedef struct const char* pRunningData; } drflac_cuesheet_track_iterator; -/* Packing is important on this structure because we map this directly to the raw data within the CUESHEET metadata block. */ -#pragma pack(4) +/* The order of members here is important because we map this directly to the raw data within the CUESHEET metadata block. */ typedef struct { drflac_uint64 offset; drflac_uint8 index; drflac_uint8 reserved[3]; } drflac_cuesheet_track_index; -#pragma pack() typedef struct { @@ -1363,9 +1362,15 @@ DRFLAC_API drflac_bool32 drflac_next_cuesheet_track(drflac_cuesheet_track_iterat I am using "__inline__" only when we're compiling in strict ANSI mode. */ #if defined(__STRICT_ANSI__) - #define DRFLAC_INLINE __inline__ __attribute__((always_inline)) + #define DRFLAC_GNUC_INLINE_HINT __inline__ #else - #define DRFLAC_INLINE inline __attribute__((always_inline)) + #define DRFLAC_GNUC_INLINE_HINT inline + #endif + + #if (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 2)) || defined(__clang__) + #define DRFLAC_INLINE DRFLAC_GNUC_INLINE_HINT __attribute__((always_inline)) + #else + #define DRFLAC_INLINE DRFLAC_GNUC_INLINE_HINT #endif #elif defined(__WATCOMC__) #define DRFLAC_INLINE __inline @@ -1509,9 +1514,7 @@ static DRFLAC_INLINE drflac_bool32 drflac_has_sse41(void) { #if defined(DRFLAC_SUPPORT_SSE41) #if (defined(DRFLAC_X64) || defined(DRFLAC_X86)) && !defined(DRFLAC_NO_SSE41) - #if defined(DRFLAC_X64) - return DRFLAC_TRUE; /* 64-bit targets always support SSE4.1. */ - #elif (defined(_M_IX86_FP) && _M_IX86_FP == 2) || defined(__SSE4_1__) + #if defined(__SSE4_1__) || defined(__AVX__) return DRFLAC_TRUE; /* If the compiler is allowed to freely generate SSE41 code we can assume support. */ #else #if defined(DRFLAC_NO_CPUID) @@ -1576,18 +1579,21 @@ static DRFLAC_INLINE drflac_bool32 drflac_has_sse41(void) extern __inline drflac_uint64 _watcom_bswap64(drflac_uint64); #pragma aux _watcom_bswap16 = \ "xchg al, ah" \ - parm [ax] \ - modify [ax]; + parm [ax] \ + value [ax] \ + modify nomemory; #pragma aux _watcom_bswap32 = \ - "bswap eax" \ - parm [eax] \ - modify [eax]; + "bswap eax" \ + parm [eax] \ + value [eax] \ + modify nomemory; #pragma aux _watcom_bswap64 = \ "bswap eax" \ "bswap edx" \ "xchg eax,edx" \ parm [eax edx] \ - modify [eax edx]; + value [eax edx] \ + modify nomemory; #endif @@ -1688,6 +1694,10 @@ typedef drflac_int32 drflac_result; #define DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE 9 #define DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE 10 +#define DRFLAC_SEEKPOINT_SIZE_IN_BYTES 18 +#define DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES 36 +#define DRFLAC_CUESHEET_TRACK_INDEX_SIZE_IN_BYTES 12 + #define drflac_align(x, a) ((((x) + (a) - 1) / (a)) * (a)) @@ -2690,6 +2700,10 @@ static drflac_bool32 drflac__find_and_seek_to_next_sync_code(drflac_bs* bs) #if defined(__WATCOMC__) && defined(__386__) #define DRFLAC_IMPLEMENT_CLZ_WATCOM #endif +#ifdef __MRC__ +#include +#define DRFLAC_IMPLEMENT_CLZ_MRC +#endif static DRFLAC_INLINE drflac_uint32 drflac__clz_software(drflac_cache_t x) { @@ -2730,6 +2744,8 @@ static DRFLAC_INLINE drflac_bool32 drflac__is_lzcnt_supported(void) /* Fast compile time check for ARM. */ #if defined(DRFLAC_HAS_LZCNT_INTRINSIC) && defined(DRFLAC_ARM) && (defined(__ARM_ARCH) && __ARM_ARCH >= 5) return DRFLAC_TRUE; +#elif defined(__MRC__) + return DRFLAC_TRUE; #else /* If the compiler itself does not support the intrinsic then we'll need to return false. */ #ifdef DRFLAC_HAS_LZCNT_INTRINSIC @@ -2839,6 +2855,15 @@ static DRFLAC_INLINE drflac_uint32 drflac__clz_msvc(drflac_cache_t x) #ifdef DRFLAC_IMPLEMENT_CLZ_WATCOM static __inline drflac_uint32 drflac__clz_watcom (drflac_uint32); +#ifdef DRFLAC_IMPLEMENT_CLZ_WATCOM_LZCNT +/* Use the LZCNT instruction (only available on some processors since the 2010s). */ +#pragma aux drflac__clz_watcom_lzcnt = \ + "db 0F3h, 0Fh, 0BDh, 0C0h" /* lzcnt eax, eax */ \ + parm [eax] \ + value [eax] \ + modify nomemory; +#else +/* Use the 386+-compatible implementation. */ #pragma aux drflac__clz_watcom = \ "bsr eax, eax" \ "xor eax, 31" \ @@ -2846,6 +2871,7 @@ static __inline drflac_uint32 drflac__clz_watcom (drflac_uint32); value [eax] \ modify exact [eax] nomemory; #endif +#endif static DRFLAC_INLINE drflac_uint32 drflac__clz(drflac_cache_t x) { @@ -2857,8 +2883,12 @@ static DRFLAC_INLINE drflac_uint32 drflac__clz(drflac_cache_t x) { #ifdef DRFLAC_IMPLEMENT_CLZ_MSVC return drflac__clz_msvc(x); +#elif defined(DRFLAC_IMPLEMENT_CLZ_WATCOM_LZCNT) + return drflac__clz_watcom_lzcnt(x); #elif defined(DRFLAC_IMPLEMENT_CLZ_WATCOM) return (x == 0) ? sizeof(x)*8 : drflac__clz_watcom(x); +#elif defined(__MRC__) + return __cntlzw(x); #else return drflac__clz_software(x); #endif @@ -4420,7 +4450,7 @@ static drflac_bool32 drflac__decode_samples_with_residual__rice__neon_32(drflac_ const drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; - riceParamMask = ~((~0UL) << riceParam); + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); riceParamMask128 = vdupq_n_u32(riceParamMask); riceParam128 = vdupq_n_s32(riceParam); @@ -4606,10 +4636,13 @@ static drflac_bool32 drflac__decode_samples_with_residual__rice__neon_64(drflac_ int32x4_t riceParam128; int64x1_t shift64; uint32x4_t one128; + int64x2_t prediction128 = { 0 }; + uint32x4_t zeroCountPart128; + uint32x4_t riceParamPart128; const drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; - riceParamMask = ~((~0UL) << riceParam); + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); riceParamMask128 = vdupq_n_u32(riceParamMask); riceParam128 = vdupq_n_s32(riceParam); @@ -4686,10 +4719,6 @@ static drflac_bool32 drflac__decode_samples_with_residual__rice__neon_64(drflac_ /* For this version we are doing one sample at a time. */ while (pDecodedSamples < pDecodedSamplesEnd) { - int64x2_t prediction128 = vdupq_n_s64(0); - uint32x4_t zeroCountPart128; - uint32x4_t riceParamPart128; - if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[0], &riceParamParts[0]) || !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[1], &riceParamParts[1]) || !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[2], &riceParamParts[2]) || @@ -6437,7 +6466,7 @@ static void drflac__free_from_callbacks(void* p, const drflac_allocation_callbac } -static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_meta_proc onMeta, void* pUserData, void* pUserDataMD, drflac_uint64* pFirstFramePos, drflac_uint64* pSeektablePos, drflac_uint32* pSeektableSize, drflac_allocation_callbacks* pAllocationCallbacks) +static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_meta_proc onMeta, void* pUserData, void* pUserDataMD, drflac_uint64* pFirstFramePos, drflac_uint64* pSeektablePos, drflac_uint32* pSeekpointCount, drflac_allocation_callbacks* pAllocationCallbacks) { /* We want to keep track of the byte position in the stream of the seektable. At the time of calling this function we know that @@ -6497,32 +6526,37 @@ static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, d seektableSize = blockSize; if (onMeta) { + drflac_uint32 seekpointCount; drflac_uint32 iSeekpoint; void* pRawData; - pRawData = drflac__malloc_from_callbacks(blockSize, pAllocationCallbacks); + seekpointCount = blockSize/DRFLAC_SEEKPOINT_SIZE_IN_BYTES; + + pRawData = drflac__malloc_from_callbacks(seekpointCount * sizeof(drflac_seekpoint), pAllocationCallbacks); if (pRawData == NULL) { return DRFLAC_FALSE; } - if (onRead(pUserData, pRawData, blockSize) != blockSize) { - drflac__free_from_callbacks(pRawData, pAllocationCallbacks); - return DRFLAC_FALSE; - } - - metadata.pRawData = pRawData; - metadata.rawDataSize = blockSize; - metadata.data.seektable.seekpointCount = blockSize/sizeof(drflac_seekpoint); - metadata.data.seektable.pSeekpoints = (const drflac_seekpoint*)pRawData; - - /* Endian swap. */ - for (iSeekpoint = 0; iSeekpoint < metadata.data.seektable.seekpointCount; ++iSeekpoint) { + /* We need to read seekpoint by seekpoint and do some processing. */ + for (iSeekpoint = 0; iSeekpoint < seekpointCount; ++iSeekpoint) { drflac_seekpoint* pSeekpoint = (drflac_seekpoint*)pRawData + iSeekpoint; + + if (onRead(pUserData, pSeekpoint, DRFLAC_SEEKPOINT_SIZE_IN_BYTES) != DRFLAC_SEEKPOINT_SIZE_IN_BYTES) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + /* Endian swap. */ pSeekpoint->firstPCMFrame = drflac__be2host_64(pSeekpoint->firstPCMFrame); pSeekpoint->flacFrameOffset = drflac__be2host_64(pSeekpoint->flacFrameOffset); pSeekpoint->pcmFrameCount = drflac__be2host_16(pSeekpoint->pcmFrameCount); } + metadata.pRawData = pRawData; + metadata.rawDataSize = blockSize; + metadata.data.seektable.seekpointCount = seekpointCount; + metadata.data.seektable.pSeekpoints = (const drflac_seekpoint*)pRawData; + onMeta(pUserDataMD, &metadata); drflac__free_from_callbacks(pRawData, pAllocationCallbacks); @@ -6607,9 +6641,15 @@ static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, d void* pRawData; const char* pRunningData; const char* pRunningDataEnd; + size_t bufferSize; drflac_uint8 iTrack; drflac_uint8 iIndex; + void* pTrackData; + /* + This needs to be loaded in two passes. The first pass is used to calculate the size of the memory allocation + we need for storing the necessary data. The second pass will fill that buffer with usable data. + */ pRawData = drflac__malloc_from_callbacks(blockSize, pAllocationCallbacks); if (pRawData == NULL) { return DRFLAC_FALSE; @@ -6630,38 +6670,91 @@ static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, d metadata.data.cuesheet.leadInSampleCount = drflac__be2host_64(*(const drflac_uint64*)pRunningData); pRunningData += 8; metadata.data.cuesheet.isCD = (pRunningData[0] & 0x80) != 0; pRunningData += 259; metadata.data.cuesheet.trackCount = pRunningData[0]; pRunningData += 1; - metadata.data.cuesheet.pTrackData = pRunningData; + metadata.data.cuesheet.pTrackData = NULL; /* Will be filled later. */ - /* Check that the cuesheet tracks are valid before passing it to the callback */ - for (iTrack = 0; iTrack < metadata.data.cuesheet.trackCount; ++iTrack) { - drflac_uint8 indexCount; - drflac_uint32 indexPointSize; + /* Pass 1: Calculate the size of the buffer for the track data. */ + { + const char* pRunningDataSaved = pRunningData; /* Will be restored at the end in preparation for the second pass. */ - if (pRunningDataEnd - pRunningData < 36) { - drflac__free_from_callbacks(pRawData, pAllocationCallbacks); - return DRFLAC_FALSE; + bufferSize = metadata.data.cuesheet.trackCount * DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES; + + for (iTrack = 0; iTrack < metadata.data.cuesheet.trackCount; ++iTrack) { + drflac_uint8 indexCount; + drflac_uint32 indexPointSize; + + if (pRunningDataEnd - pRunningData < DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + /* Skip to the index point count */ + pRunningData += 35; + + indexCount = pRunningData[0]; + pRunningData += 1; + + bufferSize += indexCount * sizeof(drflac_cuesheet_track_index); + + /* Quick validation check. */ + indexPointSize = indexCount * DRFLAC_CUESHEET_TRACK_INDEX_SIZE_IN_BYTES; + if (pRunningDataEnd - pRunningData < (drflac_int64)indexPointSize) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + pRunningData += indexPointSize; } - /* Skip to the index point count */ - pRunningData += 35; - indexCount = pRunningData[0]; pRunningData += 1; - indexPointSize = indexCount * sizeof(drflac_cuesheet_track_index); - if (pRunningDataEnd - pRunningData < (drflac_int64)indexPointSize) { - drflac__free_from_callbacks(pRawData, pAllocationCallbacks); - return DRFLAC_FALSE; - } - - /* Endian swap. */ - for (iIndex = 0; iIndex < indexCount; ++iIndex) { - drflac_cuesheet_track_index* pTrack = (drflac_cuesheet_track_index*)pRunningData; - pRunningData += sizeof(drflac_cuesheet_track_index); - pTrack->offset = drflac__be2host_64(pTrack->offset); - } + pRunningData = pRunningDataSaved; } + /* Pass 2: Allocate a buffer and fill the data. Validation was done in the step above so can be skipped. */ + { + char* pRunningTrackData; + + pTrackData = drflac__malloc_from_callbacks(bufferSize, pAllocationCallbacks); + if (pTrackData == NULL) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + pRunningTrackData = (char*)pTrackData; + + for (iTrack = 0; iTrack < metadata.data.cuesheet.trackCount; ++iTrack) { + drflac_uint8 indexCount; + + DRFLAC_COPY_MEMORY(pRunningTrackData, pRunningData, DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES); + pRunningData += DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES-1; /* Skip forward, but not beyond the last byte in the CUESHEET_TRACK block which is the index count. */ + pRunningTrackData += DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES-1; + + /* Grab the index count for the next part. */ + indexCount = pRunningData[0]; + pRunningData += 1; + pRunningTrackData += 1; + + /* Extract each track index. */ + for (iIndex = 0; iIndex < indexCount; ++iIndex) { + drflac_cuesheet_track_index* pTrackIndex = (drflac_cuesheet_track_index*)pRunningTrackData; + + DRFLAC_COPY_MEMORY(pRunningTrackData, pRunningData, DRFLAC_CUESHEET_TRACK_INDEX_SIZE_IN_BYTES); + pRunningData += DRFLAC_CUESHEET_TRACK_INDEX_SIZE_IN_BYTES; + pRunningTrackData += sizeof(drflac_cuesheet_track_index); + + pTrackIndex->offset = drflac__be2host_64(pTrackIndex->offset); + } + } + + metadata.data.cuesheet.pTrackData = pTrackData; + } + + /* The original data is no longer needed. */ + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + pRawData = NULL; + onMeta(pUserDataMD, &metadata); - drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + drflac__free_from_callbacks(pTrackData, pAllocationCallbacks); + pTrackData = NULL; } } break; @@ -6700,7 +6793,7 @@ static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, d drflac__free_from_callbacks(pRawData, pAllocationCallbacks); return DRFLAC_FALSE; } - metadata.data.picture.mime = pRunningData; pRunningData += metadata.data.picture.mimeLength; + metadata.data.picture.mime = pRunningData; pRunningData += metadata.data.picture.mimeLength; metadata.data.picture.descriptionLength = drflac__be2host_32_ptr_unaligned(pRunningData); pRunningData += 4; /* Need space for the rest of the block */ @@ -6708,7 +6801,7 @@ static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, d drflac__free_from_callbacks(pRawData, pAllocationCallbacks); return DRFLAC_FALSE; } - metadata.data.picture.description = pRunningData; pRunningData += metadata.data.picture.descriptionLength; + metadata.data.picture.description = pRunningData; pRunningData += metadata.data.picture.descriptionLength; metadata.data.picture.width = drflac__be2host_32_ptr_unaligned(pRunningData); pRunningData += 4; metadata.data.picture.height = drflac__be2host_32_ptr_unaligned(pRunningData); pRunningData += 4; metadata.data.picture.colorDepth = drflac__be2host_32_ptr_unaligned(pRunningData); pRunningData += 4; @@ -6791,9 +6884,9 @@ static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, d } } - *pSeektablePos = seektablePos; - *pSeektableSize = seektableSize; - *pFirstFramePos = runningFilePos; + *pSeektablePos = seektablePos; + *pSeekpointCount = seektableSize / DRFLAC_SEEKPOINT_SIZE_IN_BYTES; + *pFirstFramePos = runningFilePos; return DRFLAC_TRUE; } @@ -7823,11 +7916,11 @@ static drflac* drflac_open_with_metadata_private(drflac_read_proc onRead, drflac drflac_uint32 wholeSIMDVectorCountPerChannel; drflac_uint32 decodedSamplesAllocationSize; #ifndef DR_FLAC_NO_OGG - drflac_oggbs oggbs; + drflac_oggbs* pOggbs = NULL; #endif drflac_uint64 firstFramePos; drflac_uint64 seektablePos; - drflac_uint32 seektableSize; + drflac_uint32 seekpointCount; drflac_allocation_callbacks allocationCallbacks; drflac* pFlac; @@ -7881,18 +7974,21 @@ static drflac* drflac_open_with_metadata_private(drflac_read_proc onRead, drflac /* There's additional data required for Ogg streams. */ if (init.container == drflac_container_ogg) { allocationSize += sizeof(drflac_oggbs); - } - DRFLAC_ZERO_MEMORY(&oggbs, sizeof(oggbs)); - if (init.container == drflac_container_ogg) { - oggbs.onRead = onRead; - oggbs.onSeek = onSeek; - oggbs.pUserData = pUserData; - oggbs.currentBytePos = init.oggFirstBytePos; - oggbs.firstBytePos = init.oggFirstBytePos; - oggbs.serialNumber = init.oggSerial; - oggbs.bosPageHeader = init.oggBosHeader; - oggbs.bytesRemainingInPage = 0; + pOggbs = (drflac_oggbs*)drflac__malloc_from_callbacks(sizeof(*pOggbs), &allocationCallbacks); + if (pOggbs == NULL) { + return NULL; /*DRFLAC_OUT_OF_MEMORY;*/ + } + + DRFLAC_ZERO_MEMORY(pOggbs, sizeof(*pOggbs)); + pOggbs->onRead = onRead; + pOggbs->onSeek = onSeek; + pOggbs->pUserData = pUserData; + pOggbs->currentBytePos = init.oggFirstBytePos; + pOggbs->firstBytePos = init.oggFirstBytePos; + pOggbs->serialNumber = init.oggSerial; + pOggbs->bosPageHeader = init.oggBosHeader; + pOggbs->bytesRemainingInPage = 0; } #endif @@ -7901,9 +7997,9 @@ static drflac* drflac_open_with_metadata_private(drflac_read_proc onRead, drflac consist of only a single heap allocation. To this, the size of the seek table needs to be known, which we determine when reading and decoding the metadata. */ - firstFramePos = 42; /* <-- We know we are at byte 42 at this point. */ - seektablePos = 0; - seektableSize = 0; + firstFramePos = 42; /* <-- We know we are at byte 42 at this point. */ + seektablePos = 0; + seekpointCount = 0; if (init.hasMetadataBlocks) { drflac_read_proc onReadOverride = onRead; drflac_seek_proc onSeekOverride = onSeek; @@ -7913,20 +8009,26 @@ static drflac* drflac_open_with_metadata_private(drflac_read_proc onRead, drflac if (init.container == drflac_container_ogg) { onReadOverride = drflac__on_read_ogg; onSeekOverride = drflac__on_seek_ogg; - pUserDataOverride = (void*)&oggbs; + pUserDataOverride = (void*)pOggbs; } #endif - if (!drflac__read_and_decode_metadata(onReadOverride, onSeekOverride, onMeta, pUserDataOverride, pUserDataMD, &firstFramePos, &seektablePos, &seektableSize, &allocationCallbacks)) { + if (!drflac__read_and_decode_metadata(onReadOverride, onSeekOverride, onMeta, pUserDataOverride, pUserDataMD, &firstFramePos, &seektablePos, &seekpointCount, &allocationCallbacks)) { + #ifndef DR_FLAC_NO_OGG + drflac__free_from_callbacks(pOggbs, &allocationCallbacks); + #endif return NULL; } - allocationSize += seektableSize; + allocationSize += seekpointCount * sizeof(drflac_seekpoint); } pFlac = (drflac*)drflac__malloc_from_callbacks(allocationSize, &allocationCallbacks); if (pFlac == NULL) { + #ifndef DR_FLAC_NO_OGG + drflac__free_from_callbacks(pOggbs, &allocationCallbacks); + #endif return NULL; } @@ -7936,8 +8038,12 @@ static drflac* drflac_open_with_metadata_private(drflac_read_proc onRead, drflac #ifndef DR_FLAC_NO_OGG if (init.container == drflac_container_ogg) { - drflac_oggbs* pInternalOggbs = (drflac_oggbs*)((drflac_uint8*)pFlac->pDecodedSamples + decodedSamplesAllocationSize + seektableSize); - *pInternalOggbs = oggbs; + drflac_oggbs* pInternalOggbs = (drflac_oggbs*)((drflac_uint8*)pFlac->pDecodedSamples + decodedSamplesAllocationSize + (seekpointCount * sizeof(drflac_seekpoint))); + DRFLAC_COPY_MEMORY(pInternalOggbs, pOggbs, sizeof(*pOggbs)); + + /* At this point the pOggbs object has been handed over to pInternalOggbs and can be freed. */ + drflac__free_from_callbacks(pOggbs, &allocationCallbacks); + pOggbs = NULL; /* The Ogg bistream needs to be layered on top of the original bitstream. */ pFlac->bs.onRead = drflac__on_read_ogg; @@ -7961,7 +8067,7 @@ static drflac* drflac_open_with_metadata_private(drflac_read_proc onRead, drflac { /* If we have a seektable we need to load it now, making sure we move back to where we were previously. */ if (seektablePos != 0) { - pFlac->seekpointCount = seektableSize / sizeof(*pFlac->pSeekpoints); + pFlac->seekpointCount = seekpointCount; pFlac->pSeekpoints = (drflac_seekpoint*)((drflac_uint8*)pFlac->pDecodedSamples + decodedSamplesAllocationSize); DRFLAC_ASSERT(pFlac->bs.onSeek != NULL); @@ -7969,18 +8075,20 @@ static drflac* drflac_open_with_metadata_private(drflac_read_proc onRead, drflac /* Seek to the seektable, then just read directly into our seektable buffer. */ if (pFlac->bs.onSeek(pFlac->bs.pUserData, (int)seektablePos, drflac_seek_origin_start)) { - if (pFlac->bs.onRead(pFlac->bs.pUserData, pFlac->pSeekpoints, seektableSize) == seektableSize) { - /* Endian swap. */ - drflac_uint32 iSeekpoint; - for (iSeekpoint = 0; iSeekpoint < pFlac->seekpointCount; ++iSeekpoint) { + drflac_uint32 iSeekpoint; + + for (iSeekpoint = 0; iSeekpoint < seekpointCount; iSeekpoint += 1) { + if (pFlac->bs.onRead(pFlac->bs.pUserData, pFlac->pSeekpoints + iSeekpoint, DRFLAC_SEEKPOINT_SIZE_IN_BYTES) == DRFLAC_SEEKPOINT_SIZE_IN_BYTES) { + /* Endian swap. */ pFlac->pSeekpoints[iSeekpoint].firstPCMFrame = drflac__be2host_64(pFlac->pSeekpoints[iSeekpoint].firstPCMFrame); pFlac->pSeekpoints[iSeekpoint].flacFrameOffset = drflac__be2host_64(pFlac->pSeekpoints[iSeekpoint].flacFrameOffset); pFlac->pSeekpoints[iSeekpoint].pcmFrameCount = drflac__be2host_16(pFlac->pSeekpoints[iSeekpoint].pcmFrameCount); + } else { + /* Failed to read the seektable. Pretend we don't have one. */ + pFlac->pSeekpoints = NULL; + pFlac->seekpointCount = 0; + break; } - } else { - /* Failed to read the seektable. Pretend we don't have one. */ - pFlac->pSeekpoints = NULL; - pFlac->seekpointCount = 0; } /* We need to seek back to where we were. If this fails it's a critical error. */ @@ -8029,7 +8137,9 @@ static drflac* drflac_open_with_metadata_private(drflac_read_proc onRead, drflac #ifndef DR_FLAC_NO_STDIO #include +#ifndef DR_FLAC_NO_WCHAR #include /* For wcslen(), wcsrtombs() */ +#endif /* drflac_result_from_errno() is only used for fopen() and wfopen() so putting it inside DR_WAV_NO_STDIO for now. If something else needs this later we can move it out. */ #include @@ -8495,6 +8605,7 @@ fallback, so if you notice your compiler not detecting this properly I'm happy t #endif #endif +#ifndef DR_FLAC_NO_WCHAR static drflac_result drflac_wfopen(FILE** ppFile, const wchar_t* pFilePath, const wchar_t* pOpenMode, const drflac_allocation_callbacks* pAllocationCallbacks) { if (ppFile != NULL) { @@ -8523,10 +8634,23 @@ static drflac_result drflac_wfopen(FILE** ppFile, const wchar_t* pFilePath, cons } #else /* - Use fopen() on anything other than Windows. Requires a conversion. This is annoying because fopen() is locale specific. The only real way I can - think of to do this is with wcsrtombs(). Note that wcstombs() is apparently not thread-safe because it uses a static global mbstate_t object for - maintaining state. I've checked this with -std=c89 and it works, but if somebody get's a compiler error I'll look into improving compatibility. + Use fopen() on anything other than Windows. Requires a conversion. This is annoying because + fopen() is locale specific. The only real way I can think of to do this is with wcsrtombs(). Note + that wcstombs() is apparently not thread-safe because it uses a static global mbstate_t object for + maintaining state. I've checked this with -std=c89 and it works, but if somebody get's a compiler + error I'll look into improving compatibility. */ + + /* + Some compilers don't support wchar_t or wcsrtombs() which we're using below. In this case we just + need to abort with an error. If you encounter a compiler lacking such support, add it to this list + and submit a bug report and it'll be added to the library upstream. + */ + #if defined(__DJGPP__) + { + /* Nothing to do here. This will fall through to the error check below. */ + } + #else { mbstate_t mbs; size_t lenMB; @@ -8568,6 +8692,7 @@ static drflac_result drflac_wfopen(FILE** ppFile, const wchar_t* pFilePath, cons drflac__free_from_callbacks(pFilePathMB, pAllocationCallbacks); } + #endif if (*ppFile == NULL) { return DRFLAC_ERROR; @@ -8576,6 +8701,7 @@ static drflac_result drflac_wfopen(FILE** ppFile, const wchar_t* pFilePath, cons return DRFLAC_SUCCESS; } +#endif static size_t drflac__on_read_stdio(void* pUserData, void* bufferOut, size_t bytesToRead) { @@ -8608,6 +8734,7 @@ DRFLAC_API drflac* drflac_open_file(const char* pFileName, const drflac_allocati return pFlac; } +#ifndef DR_FLAC_NO_WCHAR DRFLAC_API drflac* drflac_open_file_w(const wchar_t* pFileName, const drflac_allocation_callbacks* pAllocationCallbacks) { drflac* pFlac; @@ -8625,6 +8752,7 @@ DRFLAC_API drflac* drflac_open_file_w(const wchar_t* pFileName, const drflac_all return pFlac; } +#endif DRFLAC_API drflac* drflac_open_file_with_metadata(const char* pFileName, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) { @@ -8644,6 +8772,7 @@ DRFLAC_API drflac* drflac_open_file_with_metadata(const char* pFileName, drflac_ return pFlac; } +#ifndef DR_FLAC_NO_WCHAR DRFLAC_API drflac* drflac_open_file_with_metadata_w(const wchar_t* pFileName, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) { drflac* pFlac; @@ -8661,6 +8790,7 @@ DRFLAC_API drflac* drflac_open_file_with_metadata_w(const wchar_t* pFileName, dr return pFlac; } +#endif #endif /* DR_FLAC_NO_STDIO */ static size_t drflac__on_read_memory(void* pUserData, void* bufferOut, size_t bytesToRead) @@ -11928,6 +12058,18 @@ DRFLAC_API drflac_bool32 drflac_next_cuesheet_track(drflac_cuesheet_track_iterat /* REVISION HISTORY ================ +v0.12.39 - 2022-09-17 + - Fix compilation with DJGPP. + - Fix compilation error with Visual Studio 2019 and the ARM build. + - Fix an error with SSE 4.1 detection. + - Add support for disabling wchar_t with DR_WAV_NO_WCHAR. + - Improve compatibility with compilers which lack support for explicit struct packing. + - Improve compatibility with low-end and embedded hardware by reducing the amount of stack + allocation when loading an Ogg encapsulated file. + +v0.12.38 - 2022-04-10 + - Fix compilation error on older versions of GCC. + v0.12.37 - 2022-02-12 - Improve ARM detection. diff --git a/dep/libchdr/include/libchdr/chd.h b/dep/libchdr/include/libchdr/chd.h index 2b85da793..0beed4903 100644 --- a/dep/libchdr/include/libchdr/chd.h +++ b/dep/libchdr/include/libchdr/chd.h @@ -48,6 +48,7 @@ extern "C" { #include #include +#include /*************************************************************************** @@ -204,6 +205,9 @@ extern "C" { #define CHD_CODEC_NONE 0 #define CHD_CODEC_ZLIB CHD_MAKE_TAG('z','l','i','b') +#define CHD_CODEC_LZMA CHD_MAKE_TAG('l','z','m','a') +#define CHD_CODEC_HUFFMAN CHD_MAKE_TAG('h','u','f','f') +#define CHD_CODEC_FLAC CHD_MAKE_TAG('f','l','a','c') /* general codecs with CD frontend */ #define CHD_CODEC_CD_ZLIB CHD_MAKE_TAG('c','d','z','l') #define CHD_CODEC_CD_LZMA CHD_MAKE_TAG('c','d','l','z') @@ -253,6 +257,7 @@ extern "C" { /* CHD open values */ #define CHD_OPEN_READ 1 #define CHD_OPEN_READWRITE 2 +#define CHD_OPEN_TRANSFER_FILE 4 /* Freeing of the FILE* is now libchdr's responsibility if open was successful */ /* error types */ enum _chd_error @@ -370,7 +375,8 @@ struct _chd_verify_result /* chd_error chd_create_file(core_file *file, UINT64 logicalbytes, UINT32 hunkbytes, UINT32 compression, chd_file *parent); */ /* open an existing CHD file */ -CHD_EXPORT chd_error chd_open_file(core_file *file, int mode, chd_file *parent, chd_file **chd); +CHD_EXPORT chd_error chd_open_core_file(core_file *file, int mode, chd_file *parent, chd_file **chd); +CHD_EXPORT chd_error chd_open_file(FILE *file, int mode, chd_file *parent, chd_file **chd); CHD_EXPORT chd_error chd_open(const char *filename, int mode, chd_file *parent, chd_file **chd); /* precache underlying file */ @@ -394,7 +400,10 @@ CHD_EXPORT const char *chd_error_string(chd_error err); CHD_EXPORT const chd_header *chd_get_header(chd_file *chd); /* read CHD header data from file into the pointed struct */ +CHD_EXPORT chd_error chd_read_header_core_file(core_file *file, chd_header *header); +CHD_EXPORT chd_error chd_read_header_file(FILE *file, chd_header *header); CHD_EXPORT chd_error chd_read_header(const char *filename, chd_header *header); +CHD_EXPORT bool chd_is_matching_parent(const chd_header* header, const chd_header* parent_header); diff --git a/dep/libchdr/include/libchdr/coretypes.h b/dep/libchdr/include/libchdr/coretypes.h index 30ff9d0ab..805359b5a 100644 --- a/dep/libchdr/include/libchdr/coretypes.h +++ b/dep/libchdr/include/libchdr/coretypes.h @@ -4,8 +4,21 @@ #include #include +#ifdef USE_LIBRETRO_VFS +#include +#endif + #define ARRAY_LENGTH(x) (sizeof(x)/sizeof(x[0])) +#if defined(__PS3__) || defined(__PSL1GHT__) +#undef UINT32 +#undef UINT16 +#undef UINT8 +#undef INT32 +#undef INT16 +#undef INT8 +#endif + typedef uint64_t UINT64; typedef uint32_t UINT32; typedef uint16_t UINT16; @@ -16,33 +29,50 @@ typedef int32_t INT32; typedef int16_t INT16; typedef int8_t INT8; -#define core_file FILE -#define core_fopen(file) fopen(file, "rb") +typedef struct chd_core_file { + /* + * arbitrary pointer to data the implementation uses to implement the below functions + */ + void *argp; -#if defined(__WIN32__) || defined(_WIN32) || defined(WIN32) || defined(__WIN64__) - #define core_fseek _fseeki64 - #define core_ftell _ftelli64 -#elif defined(_LARGEFILE_SOURCE) && defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64 - #define core_fseek fseeko64 - #define core_ftell ftello64 -#elif defined(__PS3__) && !defined(__PSL1GHT__) || defined(__SWITCH__) - #define core_fseek(x,y,z) fseek(x,(off_t)y,z) - #define core_ftell(x) (off_t)ftell(x) -#else - #define core_fseek fseeko - #define core_ftell ftello -#endif -#define core_fread(fc, buff, len) fread(buff, 1, len, fc) -#define core_fclose fclose + /* + * return the size of a given file as a 64-bit unsigned integer. + * the position of the file pointer after calling this function is + * undefined because many implementations will seek to the end of the + * file and call ftell. + * + * on error, (UINT64)-1 is returned. + */ + UINT64(*fsize)(struct chd_core_file*); -static inline UINT64 core_fsize(core_file *f) + /* + * should match the behavior of fread, except the FILE* argument at the end + * will be replaced with a struct chd_core_file*. + */ + size_t(*fread)(void*,size_t,size_t,struct chd_core_file*); + + // closes the given file. + int (*fclose)(struct chd_core_file*); + + // fseek clone + int (*fseek)(struct chd_core_file*, INT64, int); +} core_file; + +static inline int core_fclose(core_file *fp) { + return fp->fclose(fp); +} + +static inline size_t core_fread(core_file *fp, void *ptr, size_t len) { + return fp->fread(ptr, 1, len, fp); +} + +static inline int core_fseek(core_file* fp, INT64 offset, int whence) { + return fp->fseek(fp, offset, whence); +} + +static inline UINT64 core_fsize(core_file *fp) { - UINT64 rv; - UINT64 p = core_ftell(f); - core_fseek(f, 0, SEEK_END); - rv = core_ftell(f); - core_fseek(f, p, SEEK_SET); - return rv; + return fp->fsize(fp); } #endif diff --git a/dep/libchdr/src/libchdr_chd.c b/dep/libchdr/src/libchdr_chd.c index 8b939005e..8c65d7459 100644 --- a/dep/libchdr/src/libchdr_chd.c +++ b/dep/libchdr/src/libchdr_chd.c @@ -42,6 +42,7 @@ #include #include #include +#include #include #include @@ -227,6 +228,12 @@ struct _lzma_codec_data lzma_allocator allocator; }; +typedef struct _huff_codec_data huff_codec_data; +struct _huff_codec_data +{ + struct huffman_decoder* decoder; +}; + /* codec-private data for the CDZL codec */ typedef struct _cdzl_codec_data cdzl_codec_data; struct _cdzl_codec_data { @@ -249,6 +256,14 @@ struct _cdlz_codec_data { uint8_t* buffer; }; +/* codec-private data for the FLAC codec */ +typedef struct _flac_codec_data flac_codec_data; +struct _flac_codec_data { + /* internal state */ + int native_endian; + flac_decoder decoder; +}; + /* codec-private data for the CDFL codec */ typedef struct _cdfl_codec_data cdfl_codec_data; struct _cdfl_codec_data { @@ -267,7 +282,6 @@ struct _chd_file UINT32 cookie; /* cookie, should equal COOKIE_VALUE */ core_file * file; /* handle to the open core file */ - UINT8 owns_file; /* flag indicating if this file should be closed on chd_close() */ chd_header header; /* header, extracted from file */ chd_file * parent; /* pointer to parent file, or NULL */ @@ -286,6 +300,9 @@ struct _chd_file const codec_interface * codecintf[4]; /* interface to the codec */ zlib_codec_data zlib_codec_data; /* zlib codec data */ + lzma_codec_data lzma_codec_data; /* lzma codec data */ + huff_codec_data huff_codec_data; /* huff codec data */ + flac_codec_data flac_codec_data; /* flac codec data */ cdzl_codec_data cdzl_codec_data; /* cdzl codec data */ cdlz_codec_data cdlz_codec_data; /* cdlz codec data */ cdfl_codec_data cdfl_codec_data; /* cdfl codec data */ @@ -309,6 +326,14 @@ static const UINT8 nullsha1[CHD_SHA1_BYTES] = { 0 }; PROTOTYPES ***************************************************************************/ +/* core_file wrappers over stdio */ +static core_file *core_stdio_fopen(char const *path); +static UINT64 core_stdio_fsize(core_file *file); +static size_t core_stdio_fread(void *ptr, size_t size, size_t nmemb, core_file *file); +static int core_stdio_fclose(core_file *file); +static int core_stdio_fclose_nonowner(core_file *file); // alternate fclose used by chd_open_file +static int core_stdio_fseek(core_file* file, INT64 offset, int whence); + /* internal header operations */ static chd_error header_validate(const chd_header *header); static chd_error header_read(chd_file *chd, chd_header *header); @@ -338,6 +363,16 @@ static chd_error lzma_codec_init(void *codec, uint32_t hunkbytes); static void lzma_codec_free(void *codec); static chd_error lzma_codec_decompress(void *codec, const uint8_t *src, uint32_t complen, uint8_t *dest, uint32_t destlen); +/* huff compression codec */ +static chd_error huff_codec_init(void *codec, uint32_t hunkbytes); +static void huff_codec_free(void *codec); +static chd_error huff_codec_decompress(void *codec, const uint8_t *src, uint32_t complen, uint8_t *dest, uint32_t destlen); + +/* flac compression codec */ +static chd_error flac_codec_init(void *codec, uint32_t hunkbytes); +static void flac_codec_free(void *codec); +static chd_error flac_codec_decompress(void *codec, const uint8_t *src, uint32_t complen, uint8_t *dest, uint32_t destlen); + /* cdzl compression codec */ static chd_error cdzl_codec_init(void* codec, uint32_t hunkbytes); static void cdzl_codec_free(void* codec); @@ -749,24 +784,121 @@ static chd_error cdzl_codec_decompress(void *codec, const uint8_t *src, uint32_t return CHDERR_NONE; } +/*************************************************************************** + * HUFFMAN DECOMPRESSOR + *************************************************************************** + */ + +static chd_error huff_codec_init(void* codec, uint32_t hunkbytes) +{ + huff_codec_data* huff_codec = (huff_codec_data*) codec; + huff_codec->decoder = create_huffman_decoder(256, 16); + return CHDERR_NONE; +} + +static void huff_codec_free(void *codec) +{ + huff_codec_data* huff_codec = (huff_codec_data*) codec; + delete_huffman_decoder(huff_codec->decoder); +} + +static chd_error huff_codec_decompress(void *codec, const uint8_t *src, uint32_t complen, uint8_t *dest, uint32_t destlen) +{ + huff_codec_data* huff_codec = (huff_codec_data*) codec; + struct bitstream* bitbuf = create_bitstream(src, complen); + + // first import the tree + enum huffman_error err = huffman_import_tree_huffman(huff_codec->decoder, bitbuf); + if (err != HUFFERR_NONE) + { + free(bitbuf); + return err; + } + + // then decode the data + for (uint32_t cur = 0; cur < destlen; cur++) + dest[cur] = huffman_decode_one(huff_codec->decoder, bitbuf); + bitstream_flush(bitbuf); + chd_error result = bitstream_overflow(bitbuf) ? CHDERR_DECOMPRESSION_ERROR : CHDERR_NONE; + + free(bitbuf); + return result; +} + /*************************************************************************** * CD FLAC DECOMPRESSOR *************************************************************************** */ /*------------------------------------------------------ - * cdfl_codec_blocksize - return the optimal block size + * flac_codec_blocksize - return the optimal block size *------------------------------------------------------ */ -static uint32_t cdfl_codec_blocksize(uint32_t bytes) +static uint32_t flac_codec_blocksize(uint32_t bytes) { /* determine FLAC block size, which must be 16-65535 * clamp to 2k since that's supposed to be the sweet spot */ - uint32_t hunkbytes = bytes / 4; - while (hunkbytes > 2048) - hunkbytes /= 2; - return hunkbytes; + uint32_t blocksize = bytes / 4; + while (blocksize > 2048) + blocksize /= 2; + return blocksize; +} + +static chd_error flac_codec_init(void *codec, uint32_t hunkbytes) +{ + uint16_t native_endian = 0; + flac_codec_data *flac = (flac_codec_data*)codec; + + /* make sure the CHD's hunk size is an even multiple of the sample size */ + if (hunkbytes % 4 != 0) + return CHDERR_CODEC_ERROR; + + /* determine whether we want native or swapped samples */ + *(uint8_t *)(&native_endian) = 1; + flac->native_endian = (native_endian & 1); + + /* flac decoder init */ + if (flac_decoder_init(&flac->decoder)) + return CHDERR_OUT_OF_MEMORY; + + return CHDERR_NONE; +} + +static void flac_codec_free(void *codec) +{ + flac_codec_data *flac = (flac_codec_data*)codec; + flac_decoder_free(&flac->decoder); +} + +static chd_error flac_codec_decompress(void *codec, const uint8_t *src, uint32_t complen, uint8_t *dest, uint32_t destlen) +{ + flac_codec_data *flac = (flac_codec_data*)codec; + int swap_endian; + + if (src[0] == 'L') + swap_endian = !flac->native_endian; + else if (src[0] == 'B') + swap_endian = flac->native_endian; + else + return CHDERR_DECOMPRESSION_ERROR; + + if (!flac_decoder_reset(&flac->decoder, 44100, 2, flac_codec_blocksize(destlen), src + 1, complen - 1)) + return CHDERR_DECOMPRESSION_ERROR; + if (!flac_decoder_decode_interleaved(&flac->decoder, (int16_t *)(dest), destlen/4, swap_endian)) + return CHDERR_DECOMPRESSION_ERROR; + flac_decoder_finish(&flac->decoder); + + return CHDERR_NONE; +} + +static uint32_t cdfl_codec_blocksize(uint32_t bytes) +{ + // for CDs it seems that CD_MAX_SECTOR_DATA is the right target + uint32_t blocksize = bytes / 4; + while (blocksize > CD_MAX_SECTOR_DATA) + blocksize /= 2; + return blocksize; } static chd_error cdfl_codec_init(void *codec, uint32_t hunkbytes) @@ -904,6 +1036,39 @@ static const codec_interface codec_interfaces[] = NULL }, + /* V5 lzma compression */ + { + CHD_CODEC_LZMA, + "lzma (LZMA)", + FALSE, + lzma_codec_init, + lzma_codec_free, + lzma_codec_decompress, + NULL + }, + + /* V5 huffman compression */ + { + CHD_CODEC_HUFFMAN, + "Huffman", + FALSE, + huff_codec_init, + huff_codec_free, + huff_codec_decompress, + NULL + }, + + /* V5 flac compression */ + { + CHD_CODEC_FLAC, + "flac (FLAC)", + FALSE, + flac_codec_init, + flac_codec_free, + flac_codec_decompress, + NULL + }, + /* V5 CD zlib compression */ { CHD_CODEC_CD_ZLIB, @@ -1185,6 +1350,8 @@ static chd_error decompress_v5_map(chd_file* chd, chd_header* header) if (!chd_compressed(header)) { header->rawmap = (uint8_t*)malloc(rawmapsize); + if (header->rawmap == NULL) + return CHDERR_OUT_OF_MEMORY; core_fseek(chd->file, header->mapoffset, SEEK_SET); result = core_fread(chd->file, header->rawmap, rawmapsize); return CHDERR_NONE; @@ -1202,10 +1369,18 @@ static chd_error decompress_v5_map(chd_file* chd, chd_header* header) /* now read the map */ compressed_ptr = (uint8_t*)malloc(sizeof(uint8_t) * mapbytes); + if (compressed_ptr == NULL) + return CHDERR_OUT_OF_MEMORY; core_fseek(chd->file, header->mapoffset + 16, SEEK_SET); result = core_fread(chd->file, compressed_ptr, mapbytes); bitbuf = create_bitstream(compressed_ptr, sizeof(uint8_t) * mapbytes); header->rawmap = (uint8_t*)malloc(rawmapsize); + if (header->rawmap == NULL) + { + free(compressed_ptr); + free(bitbuf); + return CHDERR_OUT_OF_MEMORY; + } /* first decode the compression types */ decoder = create_huffman_decoder(16, 8); @@ -1343,7 +1518,30 @@ static inline void map_extract_old(const UINT8 *base, map_entry *entry, UINT32 h chd_open_file - open a CHD file for access -------------------------------------------------*/ -CHD_EXPORT chd_error chd_open_file(core_file *file, int mode, chd_file *parent, chd_file **chd) +CHD_EXPORT chd_error chd_open_file(FILE *file, int mode, chd_file *parent, chd_file **chd) { + core_file *stream = malloc(sizeof(core_file)); + if (!stream) + return CHDERR_OUT_OF_MEMORY; + stream->argp = file; + stream->fsize = core_stdio_fsize; + stream->fread = core_stdio_fread; + stream->fclose = core_stdio_fclose_nonowner; + stream->fseek = core_stdio_fseek; + + chd_error err = chd_open_core_file(stream, mode, parent, chd); + if (err != CHDERR_NONE) + return err; + + // swap out the fclose so that we close it on chd clost + stream->fclose = core_stdio_fclose; + return CHDERR_NONE; +} + +/*------------------------------------------------- + chd_open_core_file - open a CHD file for access +-------------------------------------------------*/ + +CHD_EXPORT chd_error chd_open_core_file(core_file *file, int mode, chd_file *parent, chd_file **chd) { chd_file *newchd = NULL; chd_error err; @@ -1492,6 +1690,18 @@ CHD_EXPORT chd_error chd_open_file(core_file *file, int mode, chd_file *parent, codec = &newchd->zlib_codec_data; break; + case CHD_CODEC_LZMA: + codec = &newchd->lzma_codec_data; + break; + + case CHD_CODEC_HUFFMAN: + codec = &newchd->huff_codec_data; + break; + + case CHD_CODEC_FLAC: + codec = &newchd->flac_codec_data; + break; + case CHD_CODEC_CD_ZLIB: codec = &newchd->cdzl_codec_data; break; @@ -1532,33 +1742,33 @@ cleanup: CHD_EXPORT chd_error chd_precache(chd_file* chd) { - return chd_precache_progress(chd, NULL, NULL); + return chd_precache_progress(chd, NULL, NULL); } -CHD_EXPORT chd_error chd_precache_progress(chd_file *chd, void(*progress)(size_t pos, size_t total, void* param), void* param) +CHD_EXPORT chd_error chd_precache_progress(chd_file* chd, void(*progress)(size_t pos, size_t total, void* param), void* param) { #define PRECACHE_CHUNK_SIZE 16 * 1024 * 1024 -#ifdef _MSC_VER - size_t size, done, req_count, count, last_update_done, update_interval; -#else - ssize_t size, done, req_count, count, last_update_done, update_interval; -#endif + size_t count; + UINT64 size, done, req_count, last_update_done, update_interval; - if (chd->file_cache == NULL) - { - core_fseek(chd->file, 0, SEEK_END); - size = core_ftell(chd->file); - if (size <= 0) - return CHDERR_INVALID_DATA; - chd->file_cache = malloc(size); - if (chd->file_cache == NULL) + if (chd->file_cache == NULL) + { + size = core_fsize(chd->file); + if ((INT64)size <= 0) + return CHDERR_INVALID_DATA; + + if (size > SIZE_MAX) return CHDERR_OUT_OF_MEMORY; - core_fseek(chd->file, 0, SEEK_SET); + + chd->file_cache = malloc(size); + if (chd->file_cache == NULL) + return CHDERR_OUT_OF_MEMORY; + core_fseek(chd->file, 0, SEEK_SET); done = 0; - last_update_done = 0; - update_interval = ((size + 99) / 100); + last_update_done = 0; + update_interval = ((size + 99) / 100); while (done < size) { @@ -1566,8 +1776,8 @@ CHD_EXPORT chd_error chd_precache_progress(chd_file *chd, void(*progress)(size_t if (req_count > PRECACHE_CHUNK_SIZE) req_count = PRECACHE_CHUNK_SIZE; - count = core_fread(chd->file, chd->file_cache + done, req_count); - if (count != req_count) + count = core_fread(chd->file, chd->file_cache + (size_t)done, (size_t)req_count); + if (count != (size_t)req_count) { free(chd->file_cache); chd->file_cache = NULL; @@ -1581,9 +1791,9 @@ CHD_EXPORT chd_error chd_precache_progress(chd_file *chd, void(*progress)(size_t progress(done, size, param); } } - } + } - return CHDERR_NONE; + return CHDERR_NONE; } /*------------------------------------------------- @@ -1614,7 +1824,7 @@ CHD_EXPORT chd_error chd_open(const char *filename, int mode, chd_file *parent, } /* open the file */ - file = core_fopen(filename); + file = core_stdio_fopen(filename); if (file == 0) { err = CHDERR_FILE_NOT_FOUND; @@ -1622,12 +1832,7 @@ CHD_EXPORT chd_error chd_open(const char *filename, int mode, chd_file *parent, } /* now open the CHD */ - err = chd_open_file(file, mode, parent, chd); - if (err != CHDERR_NONE) - goto cleanup; - - /* we now own this file */ - (*chd)->owns_file = TRUE; + return chd_open_core_file(file, mode, parent, chd); cleanup: if ((err != CHDERR_NONE) && (file != NULL)) @@ -1664,18 +1869,30 @@ CHD_EXPORT void chd_close(chd_file *chd) switch (chd->codecintf[i]->compression) { - case CHD_CODEC_CD_LZMA: - codec = &chd->cdlz_codec_data; - break; - case CHD_CODEC_ZLIB: codec = &chd->zlib_codec_data; break; + case CHD_CODEC_LZMA: + codec = &chd->lzma_codec_data; + break; + + case CHD_CODEC_HUFFMAN: + codec = &chd->huff_codec_data; + break; + + case CHD_CODEC_FLAC: + codec = &chd->flac_codec_data; + break; + case CHD_CODEC_CD_ZLIB: codec = &chd->cdzl_codec_data; break; + case CHD_CODEC_CD_LZMA: + codec = &chd->cdlz_codec_data; + break; + case CHD_CODEC_CD_FLAC: codec = &chd->cdfl_codec_data; break; @@ -1709,7 +1926,7 @@ CHD_EXPORT void chd_close(chd_file *chd) free(chd->map); /* close the file */ - if (chd->owns_file && chd->file != NULL) + if (chd->file != NULL) core_fclose(chd->file); #ifdef NEED_CACHE_HUNK @@ -1798,37 +2015,68 @@ CHD_EXPORT const chd_header *chd_get_header(chd_file *chd) chd_read_header - read CHD header data from file into the pointed struct -------------------------------------------------*/ -CHD_EXPORT chd_error chd_read_header(const char *filename, chd_header *header) +CHD_EXPORT chd_error chd_read_header_core_file(core_file* file, chd_header* header) { - chd_error err = CHDERR_NONE; chd_file chd; - - /* punt if NULL */ - if (filename == NULL || header == NULL) - EARLY_EXIT(err = CHDERR_INVALID_PARAMETER); - - /* open the file */ - chd.file = core_fopen(filename); - if (chd.file == NULL) - EARLY_EXIT(err = CHDERR_FILE_NOT_FOUND); + chd.file = file; /* attempt to read the header */ - err = header_read(&chd, header); + const chd_error err = header_read(&chd, header); if (err != CHDERR_NONE) - EARLY_EXIT(err); + return err; /* validate the header */ - err = header_validate(header); - if (err != CHDERR_NONE) - EARLY_EXIT(err); + return header_validate(header); +} -cleanup: - if (chd.file != NULL) - core_fclose(chd.file); +CHD_EXPORT chd_error chd_read_header_file(FILE *file, chd_header *header) +{ + core_file stream; + stream.argp = file; + stream.fsize = core_stdio_fsize; + stream.fread = core_stdio_fread; + stream.fclose = core_stdio_fclose_nonowner; + stream.fseek = core_stdio_fseek; + return chd_read_header_core_file(&stream, header); +} + +CHD_EXPORT chd_error chd_read_header(const char *filename, chd_header *header) +{ + if (filename == NULL) + return CHDERR_INVALID_PARAMETER; + + core_file* file = core_stdio_fopen(filename); + if (file == NULL) + return CHDERR_FILE_NOT_FOUND; + + chd_error err = chd_read_header_core_file(file, header); + + core_fclose(file); return err; } +CHD_EXPORT bool chd_is_matching_parent(const chd_header* header, const chd_header* parent_header) +{ + /* check MD5 if it isn't empty */ + if (memcmp(nullmd5, header->parentmd5, sizeof(header->parentmd5)) != 0 && + memcmp(nullmd5, parent_header->md5, sizeof(parent_header->md5)) != 0 && + memcmp(parent_header->md5, header->parentmd5, sizeof(header->parentmd5)) != 0) + { + return false; + } + + /* check SHA1 if it isn't empty */ + if (memcmp(nullsha1, header->parentsha1, sizeof(header->parentsha1)) != 0 && + memcmp(nullsha1, parent_header->sha1, sizeof(parent_header->sha1)) != 0 && + memcmp(parent_header->sha1, header->parentsha1, sizeof(header->parentsha1)) != 0) + { + return false; + } + + return true; +} + /*************************************************************************** CORE DATA READ/WRITE ***************************************************************************/ @@ -2096,6 +2344,8 @@ static chd_error header_read(chd_file *chd, chd_header *header) header->logicalbytes = (UINT64)header->obsolete_cylinders * (UINT64)header->obsolete_heads * (UINT64)header->obsolete_sectors * (UINT64)seclen; header->hunkbytes = seclen * header->obsolete_hunksize; header->unitbytes = header_guess_unitbytes(chd); + if (header->unitbytes == 0) + return CHDERR_INVALID_DATA; header->unitcount = (header->logicalbytes + header->unitbytes - 1) / header->unitbytes; header->metaoffset = 0; } @@ -2110,6 +2360,8 @@ static chd_error header_read(chd_file *chd, chd_header *header) memcpy(header->parentmd5, &rawheader[60], CHD_MD5_BYTES); header->hunkbytes = get_bigendian_uint32(&rawheader[76]); header->unitbytes = header_guess_unitbytes(chd); + if (header->unitbytes == 0) + return CHDERR_INVALID_DATA; header->unitcount = (header->logicalbytes + header->unitbytes - 1) / header->unitbytes; memcpy(header->sha1, &rawheader[80], CHD_SHA1_BYTES); memcpy(header->parentsha1, &rawheader[100], CHD_SHA1_BYTES); @@ -2123,6 +2375,8 @@ static chd_error header_read(chd_file *chd, chd_header *header) header->metaoffset = get_bigendian_uint64(&rawheader[36]); header->hunkbytes = get_bigendian_uint32(&rawheader[44]); header->unitbytes = header_guess_unitbytes(chd); + if (header->unitbytes == 0) + return CHDERR_INVALID_DATA; header->unitcount = (header->logicalbytes + header->unitbytes - 1) / header->unitbytes; memcpy(header->sha1, &rawheader[48], CHD_SHA1_BYTES); memcpy(header->parentsha1, &rawheader[68], CHD_SHA1_BYTES); @@ -2141,8 +2395,12 @@ static chd_error header_read(chd_file *chd, chd_header *header) header->mapoffset = get_bigendian_uint64(&rawheader[40]); header->metaoffset = get_bigendian_uint64(&rawheader[48]); header->hunkbytes = get_bigendian_uint32(&rawheader[56]); + if (header->hunkbytes == 0) + return CHDERR_INVALID_DATA; header->hunkcount = (header->logicalbytes + header->hunkbytes - 1) / header->hunkbytes; header->unitbytes = get_bigendian_uint32(&rawheader[60]); + if (header->unitbytes == 0) + return CHDERR_INVALID_DATA; header->unitcount = (header->logicalbytes + header->unitbytes - 1) / header->unitbytes; memcpy(header->sha1, &rawheader[84], CHD_SHA1_BYTES); memcpy(header->parentsha1, &rawheader[104], CHD_SHA1_BYTES); @@ -2382,18 +2640,30 @@ static chd_error hunk_read_into_memory(chd_file *chd, UINT32 hunknum, UINT8 *des return CHDERR_READ_ERROR; switch (chd->codecintf[rawmap[0]]->compression) { - case CHD_CODEC_CD_LZMA: - codec = &chd->cdlz_codec_data; - break; - case CHD_CODEC_ZLIB: codec = &chd->zlib_codec_data; break; + case CHD_CODEC_LZMA: + codec = &chd->lzma_codec_data; + break; + + case CHD_CODEC_HUFFMAN: + codec = &chd->huff_codec_data; + break; + + case CHD_CODEC_FLAC: + codec = &chd->flac_codec_data; + break; + case CHD_CODEC_CD_ZLIB: codec = &chd->cdzl_codec_data; break; + case CHD_CODEC_CD_LZMA: + codec = &chd->cdlz_codec_data; + break; + case CHD_CODEC_CD_FLAC: codec = &chd->cdfl_codec_data; break; @@ -2625,10 +2895,6 @@ static chd_error zlib_codec_init(void *codec, uint32_t hunkbytes) else err = CHDERR_NONE; - /* handle an error */ - if (err != CHDERR_NONE) - free(data); - return err; } @@ -2768,3 +3034,87 @@ static void zlib_allocator_free(voidpf opaque) if (alloc->allocptr[i]) free(alloc->allocptr[i]); } + +/*------------------------------------------------- + core_stdio_fopen - core_file wrapper over fopen +-------------------------------------------------*/ +static core_file *core_stdio_fopen(char const *path) { + core_file *file = malloc(sizeof(core_file)); + if (!file) + return NULL; + if (!(file->argp = fopen(path, "rb"))) { + free(file); + return NULL; + } + file->fsize = core_stdio_fsize; + file->fread = core_stdio_fread; + file->fclose = core_stdio_fclose; + file->fseek = core_stdio_fseek; + return file; +} + +/*------------------------------------------------- + core_stdio_fsize - core_file function for + getting file size with stdio +-------------------------------------------------*/ +static UINT64 core_stdio_fsize(core_file *file) { +#if defined USE_LIBRETRO_VFS + #define core_stdio_fseek_impl fseek + #define core_stdio_ftell_impl ftell +#elif defined(__WIN32__) || defined(_WIN32) || defined(WIN32) || defined(__WIN64__) + #define core_stdio_fseek_impl _fseeki64 + #define core_stdio_ftell_impl _ftelli64 +#elif defined(_LARGEFILE_SOURCE) && defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64 + #define core_stdio_fseek_impl fseeko64 + #define core_stdio_ftell_impl ftello64 +#elif defined(__PS3__) && !defined(__PSL1GHT__) || defined(__SWITCH__) || defined(__vita__) + #define core_stdio_fseek_impl(x,y,z) fseek(x,(off_t)y,z) + #define core_stdio_ftell_impl(x) (off_t)ftell(x) +#else + #define core_stdio_fseek_impl fseeko + #define core_stdio_ftell_impl ftello +#endif + FILE *fp; + UINT64 p, rv; + fp = (FILE*)file->argp; + + p = core_stdio_ftell_impl(fp); + core_stdio_fseek_impl(fp, 0, SEEK_END); + rv = core_stdio_ftell_impl(fp); + core_stdio_fseek_impl(fp, p, SEEK_SET); + return rv; +} + +/*------------------------------------------------- + core_stdio_fread - core_file wrapper over fread +-------------------------------------------------*/ +static size_t core_stdio_fread(void *ptr, size_t size, size_t nmemb, core_file *file) { + return fread(ptr, size, nmemb, (FILE*)file->argp); +} + +/*------------------------------------------------- + core_stdio_fclose - core_file wrapper over fclose +-------------------------------------------------*/ +static int core_stdio_fclose(core_file *file) { + int err = fclose((FILE*)file->argp); + if (err == 0) + free(file); + return err; +} + +/*------------------------------------------------- + core_stdio_fclose_nonowner - don't call fclose because + we don't own the underlying file, but do free the + core_file because libchdr did allocate that itself. +-------------------------------------------------*/ +static int core_stdio_fclose_nonowner(core_file *file) { + free(file); + return 0; +} + +/*------------------------------------------------- + core_stdio_fseek - core_file wrapper over fclose +-------------------------------------------------*/ +static int core_stdio_fseek(core_file* file, INT64 offset, int whence) { + return core_stdio_fseek_impl((FILE*)file->argp, offset, whence); +} diff --git a/dep/libchdr/src/libchdr_flac.c b/dep/libchdr/src/libchdr_flac.c index 0582be84f..dce22c723 100644 --- a/dep/libchdr/src/libchdr_flac.c +++ b/dep/libchdr/src/libchdr_flac.c @@ -60,9 +60,10 @@ int flac_decoder_init(flac_decoder *decoder) void flac_decoder_free(flac_decoder* decoder) { - if ((decoder != NULL) && (decoder->decoder != NULL)) + if ((decoder != NULL) && (decoder->decoder != NULL)) { drflac_close(decoder->decoder); - decoder->decoder = NULL; + decoder->decoder = NULL; + } } /*------------------------------------------------- diff --git a/dep/libchdr/src/libchdr_huffman.c b/dep/libchdr/src/libchdr_huffman.c index 45c02d8b7..5162b957a 100644 --- a/dep/libchdr/src/libchdr_huffman.c +++ b/dep/libchdr/src/libchdr_huffman.c @@ -212,6 +212,8 @@ enum huffman_error huffman_import_tree_rle(struct huffman_decoder* decoder, stru else { int repcount = bitstream_read(bitbuf, numbits) + 3; + if (repcount + curnode > decoder->numcodes) + return HUFFERR_INVALID_DATA; while (repcount--) decoder->huffnode[curnode++].numbits = nodebits; } diff --git a/dep/rcheevos/include/rc_api_editor.h b/dep/rcheevos/include/rc_api_editor.h index 2bda50325..46880d582 100644 --- a/dep/rcheevos/include/rc_api_editor.h +++ b/dep/rcheevos/include/rc_api_editor.h @@ -44,6 +44,7 @@ rc_api_fetch_code_notes_response_t; int rc_api_init_fetch_code_notes_request(rc_api_request_t* request, const rc_api_fetch_code_notes_request_t* api_params); int rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response, const char* server_response); +int rc_api_process_fetch_code_notes_server_response(rc_api_fetch_code_notes_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response); /* --- Update Code Note --- */ @@ -76,6 +77,7 @@ rc_api_update_code_note_response_t; int rc_api_init_update_code_note_request(rc_api_request_t* request, const rc_api_update_code_note_request_t* api_params); int rc_api_process_update_code_note_response(rc_api_update_code_note_response_t* response, const char* server_response); +int rc_api_process_update_code_note_server_response(rc_api_update_code_note_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_update_code_note_response(rc_api_update_code_note_response_t* response); /* --- Update Achievement --- */ @@ -121,6 +123,7 @@ rc_api_update_achievement_response_t; int rc_api_init_update_achievement_request(rc_api_request_t* request, const rc_api_update_achievement_request_t* api_params); int rc_api_process_update_achievement_response(rc_api_update_achievement_response_t* response, const char* server_response); +int rc_api_process_update_achievement_server_response(rc_api_update_achievement_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_update_achievement_response(rc_api_update_achievement_response_t* response); /* --- Update Leaderboard --- */ @@ -170,6 +173,7 @@ rc_api_update_leaderboard_response_t; int rc_api_init_update_leaderboard_request(rc_api_request_t* request, const rc_api_update_leaderboard_request_t* api_params); int rc_api_process_update_leaderboard_response(rc_api_update_leaderboard_response_t* response, const char* server_response); +int rc_api_process_update_leaderboard_server_response(rc_api_update_leaderboard_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_update_leaderboard_response(rc_api_update_leaderboard_response_t* response); /* --- Fetch Badge Range --- */ @@ -199,6 +203,7 @@ rc_api_fetch_badge_range_response_t; int rc_api_init_fetch_badge_range_request(rc_api_request_t* request, const rc_api_fetch_badge_range_request_t* api_params); int rc_api_process_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response, const char* server_response); +int rc_api_process_fetch_badge_range_server_response(rc_api_fetch_badge_range_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response); /* --- Add Game Hash --- */ @@ -238,6 +243,7 @@ rc_api_add_game_hash_response_t; int rc_api_init_add_game_hash_request(rc_api_request_t* request, const rc_api_add_game_hash_request_t* api_params); int rc_api_process_add_game_hash_response(rc_api_add_game_hash_response_t* response, const char* server_response); +int rc_api_process_add_game_hash_server_response(rc_api_add_game_hash_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_add_game_hash_response(rc_api_add_game_hash_response_t* response); #ifdef __cplusplus diff --git a/dep/rcheevos/include/rc_api_info.h b/dep/rcheevos/include/rc_api_info.h index 7979cc391..7d3a31607 100644 --- a/dep/rcheevos/include/rc_api_info.h +++ b/dep/rcheevos/include/rc_api_info.h @@ -64,6 +64,7 @@ rc_api_fetch_achievement_info_response_t; int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params); int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response); +int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achievement_info_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response); /* --- Fetch Leaderboard Info --- */ @@ -135,6 +136,7 @@ rc_api_fetch_leaderboard_info_response_t; int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params); int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response); +int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboard_info_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response); /* --- Fetch Games List --- */ @@ -173,6 +175,7 @@ rc_api_fetch_games_list_response_t; int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params); int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response); +int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response); #ifdef __cplusplus diff --git a/dep/rcheevos/include/rc_api_request.h b/dep/rcheevos/include/rc_api_request.h index 8ba482a8f..81c855fd3 100644 --- a/dep/rcheevos/include/rc_api_request.h +++ b/dep/rcheevos/include/rc_api_request.h @@ -3,6 +3,8 @@ #include "rc_error.h" +#include + #ifdef __cplusplus extern "C" { #endif @@ -10,15 +12,25 @@ extern "C" { /** * A block of memory for variable length data (like strings and arrays). */ -typedef struct rc_api_buffer_t { +typedef struct rc_api_buffer_chunk_t { /* The current location where data is being written */ char* write; /* The first byte past the end of data where writing cannot occur */ char* end; + /* The first byte of the data */ + char* start; /* The next block in the allocated memory chain */ - struct rc_api_buffer_t* next; - /* The buffer containing the data. The actual size may be larger than 256 bytes for buffers allocated in - * the next chain. The 256 byte size specified is for the initial allocation within the container object. */ + struct rc_api_buffer_chunk_t* next; +} +rc_api_buffer_chunk_t; + +/** + * A preallocated block of memory for variable length data (like strings and arrays). + */ +typedef struct rc_api_buffer_t { + /* The chunk data (will point at the local data member) */ + struct rc_api_buffer_chunk_t chunk; + /* Small chunk of memory pre-allocated for the chunk */ char data[256]; } rc_api_buffer_t; @@ -31,6 +43,8 @@ typedef struct rc_api_request_t { const char* url; /* Additional query args that should be sent via a POST command. If null, GET may be used */ const char* post_data; + /* The HTTP Content-Type of the POST data. */ + const char* content_type; /* Storage for the url and post_data */ rc_api_buffer_t buffer; @@ -41,7 +55,7 @@ rc_api_request_t; * Common attributes for all server responses. */ typedef struct rc_api_response_t { - /* Server-provided success indicator (non-zero on failure) */ + /* Server-provided success indicator (non-zero on success, zero on failure) */ int succeeded; /* Server-provided message associated to the failure */ const char* error_message; @@ -56,6 +70,15 @@ void rc_api_destroy_request(rc_api_request_t* request); void rc_api_set_host(const char* hostname); void rc_api_set_image_host(const char* hostname); +typedef struct rc_api_server_response_t { + /* Pointer to the data returned from the server */ + const char* body; + /* Length of data returned from the server (Content-Length) */ + size_t body_length; + /* HTTP status code returned from the server */ + int http_status_code; +} rc_api_server_response_t; + #ifdef __cplusplus } #endif diff --git a/dep/rcheevos/include/rc_api_runtime.h b/dep/rcheevos/include/rc_api_runtime.h index 68f56fd51..4534ff182 100644 --- a/dep/rcheevos/include/rc_api_runtime.h +++ b/dep/rcheevos/include/rc_api_runtime.h @@ -59,6 +59,7 @@ rc_api_resolve_hash_response_t; int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params); int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response); +int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response); /* --- Fetch Game Data --- */ @@ -155,6 +156,7 @@ rc_api_fetch_game_data_response_t; int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params); int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response); +int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response); /* --- Ping --- */ @@ -185,6 +187,7 @@ rc_api_ping_response_t; int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params); int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response); +int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_ping_response(rc_api_ping_response_t* response); /* --- Award Achievement --- */ @@ -214,6 +217,8 @@ typedef struct rc_api_award_achievement_response_t { unsigned awarded_achievement_id; /* The updated player score */ unsigned new_player_score; + /* The updated player softcore score */ + unsigned new_player_score_softcore; /* The number of achievements the user has not yet unlocked for this game * (in hardcore/non-hardcore per hardcore flag in request) */ unsigned achievements_remaining; @@ -225,6 +230,7 @@ rc_api_award_achievement_response_t; int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params); int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response); +int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response); /* --- Submit Leaderboard Entry --- */ @@ -282,6 +288,7 @@ rc_api_submit_lboard_entry_response_t; int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params); int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response); +int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response); #ifdef __cplusplus diff --git a/dep/rcheevos/include/rc_api_user.h b/dep/rcheevos/include/rc_api_user.h index 758842557..8f96044c8 100644 --- a/dep/rcheevos/include/rc_api_user.h +++ b/dep/rcheevos/include/rc_api_user.h @@ -33,6 +33,8 @@ typedef struct rc_api_login_response_t { const char* api_token; /* The current score of the player */ unsigned score; + /* The current softcore score of the player */ + unsigned score_softcore; /* The number of unread messages waiting for the player on the web site */ unsigned num_unread_messages; /* The preferred name to display for the player */ @@ -45,6 +47,7 @@ rc_api_login_response_t; int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params); int rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response); +int rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_login_response(rc_api_login_response_t* response); /* --- Start Session --- */ @@ -73,6 +76,7 @@ rc_api_start_session_response_t; int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_request_t* api_params); int rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response); +int rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response); /* --- Fetch User Unlocks --- */ @@ -108,6 +112,7 @@ rc_api_fetch_user_unlocks_response_t; int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params); int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response); +int rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response); #ifdef __cplusplus diff --git a/dep/rcheevos/include/rc_client.h b/dep/rcheevos/include/rc_client.h new file mode 100644 index 000000000..40e9a977a --- /dev/null +++ b/dep/rcheevos/include/rc_client.h @@ -0,0 +1,584 @@ +#ifndef RC_CLIENT_H +#define RC_CLIENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "rc_api_request.h" +#include "rc_error.h" + +#include +#include +#include + +/* implementation abstracted in rc_client_internal.h */ +typedef struct rc_client_t rc_client_t; +typedef struct rc_client_async_handle_t rc_client_async_handle_t; + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +/** + * Callback used to read num_bytes bytes from memory starting at address into buffer. + * Returns the number of bytes read. A return value of 0 indicates the address was invalid. + */ +typedef uint32_t (*rc_client_read_memory_func_t)(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client); + +/** + * Internal method passed to rc_client_server_call_t to process the server response. + */ +typedef void (*rc_client_server_callback_t)(const rc_api_server_response_t* server_response, void* callback_data); + +/** + * Callback used to issue a request to the server. + */ +typedef void (*rc_client_server_call_t)(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); + +/** + * Generic callback for asynchronous eventing. + */ +typedef void (*rc_client_callback_t)(int result, const char* error_message, rc_client_t* client, void* userdata); + +/** + * Callback for logging or displaying a message. + */ +typedef void (*rc_client_message_callback_t)(const char* message, const rc_client_t* client); + +/** + * Marks an async process as aborted. The associated callback will not be called. + */ +void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle); + +/*****************************************************************************\ +| Runtime | +\*****************************************************************************/ + +/** + * Creates a new rc_client_t object. + */ +rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function); + +/** + * Releases resources associated to a rc_client_t object. + * Pointer will no longer be valid after making this call. + */ +void rc_client_destroy(rc_client_t* client); + +/** + * Sets whether hardcore is enabled (on by default). + * Can be called with a game loaded. + * Enabling hardcore with a game loaded will raise an RC_CLIENT_EVENT_RESET + * event. Processing will be disabled until rc_client_reset is called. + */ +void rc_client_set_hardcore_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether hardcore is enabled (on by default). + */ +int rc_client_get_hardcore_enabled(const rc_client_t* client); + +/** + * Sets whether encore mode is enabled (off by default). + * Evaluated when loading a game. Has no effect while a game is loaded. + */ +void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether encore mode is enabled (off by default). + */ +int rc_client_get_encore_mode_enabled(const rc_client_t* client); + +/** + * Sets whether unofficial achievements should be loaded. + * Evaluated when loading a game. Has no effect while a game is loaded. + */ +void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether unofficial achievements should be loaded. + */ +int rc_client_get_unofficial_enabled(const rc_client_t* client); + +/** + * Sets whether spectator mode is enabled (off by default). + * If enabled, events for achievement unlocks and leaderboard submissions will be + * raised, but server calls to actually perform the unlock/submit will not occur. + * Can be modified while a game is loaded. Evaluated at unlock/submit time. + * Cannot be modified if disabled before a game is loaded. + */ +void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether spectator mode is enabled (off by default). + */ +int rc_client_get_spectator_mode_enabled(const rc_client_t* client); + +/** + * Attaches client-specific data to the runtime. + */ +void rc_client_set_userdata(rc_client_t* client, void* userdata); + +/** + * Gets the client-specific data attached to the runtime. + */ +void* rc_client_get_userdata(const rc_client_t* client); + +/** + * Sets the name of the server to use. + */ +void rc_client_set_host(const rc_client_t* client, const char* hostname); + +/*****************************************************************************\ +| Logging | +\*****************************************************************************/ + +/** + * Sets the logging level and provides a callback to be called to do the logging. + */ +void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback); +enum +{ + RC_CLIENT_LOG_LEVEL_NONE = 0, + RC_CLIENT_LOG_LEVEL_ERROR = 1, + RC_CLIENT_LOG_LEVEL_WARN = 2, + RC_CLIENT_LOG_LEVEL_INFO = 3, + RC_CLIENT_LOG_LEVEL_VERBOSE = 4 +}; + +/*****************************************************************************\ +| User | +\*****************************************************************************/ + +/** + * Attempt to login a user. + */ +rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client, + const char* username, const char* password, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Attempt to login a user. + */ +rc_client_async_handle_t* rc_client_begin_login_with_token(rc_client_t* client, + const char* username, const char* token, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Logout the user. + */ +void rc_client_logout(rc_client_t* client); + +typedef struct rc_client_user_t { + const char* display_name; + const char* username; + const char* token; + uint32_t score; + uint32_t score_softcore; + uint32_t num_unread_messages; +} rc_client_user_t; + +/** + * Gets information about the logged in user. Will return NULL if the user is not logged in. + */ +const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client); + +/** + * Gets the URL for the user's profile picture. + * Returns RC_OK on success. + */ +int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size); + +typedef struct rc_client_user_game_summary_t +{ + uint32_t num_core_achievements; + uint32_t num_unofficial_achievements; + uint32_t num_unlocked_achievements; + uint32_t num_unsupported_achievements; + + uint32_t points_core; + uint32_t points_unlocked; +} rc_client_user_game_summary_t; + +/** + * Gets a breakdown of the number of achievements in the game, and how many the user has unlocked. + * Used for the "You have unlocked X of Y achievements" message shown when the game starts. + */ +void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary); + +/*****************************************************************************\ +| Game | +\*****************************************************************************/ + +/** + * Start loading an unidentified game. + */ +rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* client, + uint32_t console_id, const char* file_path, + const uint8_t* data, size_t data_size, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Start loading a game. + */ +rc_client_async_handle_t* rc_client_begin_load_game(rc_client_t* client, const char* hash, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Unloads the current game. + */ +void rc_client_unload_game(rc_client_t* client); + +typedef struct rc_client_game_t { + uint32_t id; + uint32_t console_id; + const char* title; + const char* hash; + const char* badge_name; +} rc_client_game_t; + +/** + * Get information about the current game. Returns NULL if no game is loaded. + */ +const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client); + +/** + * Gets the URL for the game image. + * Returns RC_OK on success. + */ +int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size); + +/** + * Changes the active disc in a multi-disc game. + */ +rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); + +/*****************************************************************************\ +| Subsets | +\*****************************************************************************/ + +typedef struct rc_client_subset_t { + uint32_t id; + const char* title; + char badge_name[16]; + + uint32_t num_achievements; + uint32_t num_leaderboards; +} rc_client_subset_t; + +const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id); + +/*****************************************************************************\ +| Achievements | +\*****************************************************************************/ + +enum { + RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE = 0, /* unprocessed */ + RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE = 1, /* eligible to trigger */ + RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED = 2, /* earned by user */ + RC_CLIENT_ACHIEVEMENT_STATE_DISABLED = 3 /* not supported by this version of the runtime */ +}; + +enum { + RC_CLIENT_ACHIEVEMENT_CATEGORY_NONE = 0, + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE = (1 << 0), + RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL = (1 << 1), + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE | RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL +}; + +enum { + RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN = 0, + RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED = 1, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED = 2, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED = 3, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL = 4, + RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED = 5, + RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE = 6, + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE = 7 +}; + +enum { + RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE = 0, + RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE = (1 << 0), + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE = (1 << 1), + RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH = RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE | RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE +}; + +typedef struct rc_client_achievement_t { + const char* title; + const char* description; + char badge_name[8]; + char measured_progress[24]; + float measured_percent; + uint32_t id; + uint32_t points; + time_t unlock_time; + uint8_t state; + uint8_t category; + uint8_t bucket; + uint8_t unlocked; +} rc_client_achievement_t; + +/** + * Get information about an achievement. Returns NULL if not found. + */ +const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* client, uint32_t id); + +/** + * Gets the URL for the achievement image. + * Returns RC_OK on success. + */ +int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size); + +typedef struct rc_client_achievement_bucket_t { + rc_client_achievement_t** achievements; + uint32_t num_achievements; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} rc_client_achievement_bucket_t; + +typedef struct rc_client_achievement_list_t { + rc_client_achievement_bucket_t* buckets; + uint32_t num_buckets; +} rc_client_achievement_list_t; + +enum { + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE = 0, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS = 1 +}; + +/** + * Creates a list of achievements matching the specified category and grouping. + * Returns an allocated list that must be free'd by calling rc_client_destroy_achievement_list. + */ +rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping); + +/** + * Destroys a list allocated by rc_client_get_achievement_list. + */ +void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list); + +/*****************************************************************************\ +| Leaderboards | +\*****************************************************************************/ + +enum { + RC_CLIENT_LEADERBOARD_STATE_INACTIVE = 0, + RC_CLIENT_LEADERBOARD_STATE_ACTIVE = 1, + RC_CLIENT_LEADERBOARD_STATE_TRACKING = 2, + RC_CLIENT_LEADERBOARD_STATE_DISABLED = 3 +}; + +typedef struct rc_client_leaderboard_t { + const char* title; + const char* description; + const char* tracker_value; + uint32_t id; + uint8_t state; + uint8_t lower_is_better; +} rc_client_leaderboard_t; + +/** + * Get information about a leaderboard. Returns NULL if not found. + */ +const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id); + +typedef struct rc_client_leaderboard_tracker_t { + char display[24]; + uint32_t id; +} rc_client_leaderboard_tracker_t; + +typedef struct rc_client_leaderboard_bucket_t { + rc_client_leaderboard_t** leaderboards; + uint32_t num_leaderboards; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} rc_client_leaderboard_bucket_t; + +typedef struct rc_client_leaderboard_list_t { + rc_client_leaderboard_bucket_t* buckets; + uint32_t num_buckets; +} rc_client_leaderboard_list_t; + +enum { + RC_CLIENT_LEADERBOARD_BUCKET_UNKNOWN = 0, + RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE = 1, + RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE = 2, + RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED = 3, + RC_CLIENT_LEADERBOARD_BUCKET_ALL = 4 +}; + +enum { + RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE = 0, + RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING = 1 +}; + +/** + * Creates a list of leaderboards matching the specified grouping. + * Returns an allocated list that must be free'd by calling rc_client_destroy_leaderboard_list. + */ +rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping); + +/** + * Destroys a list allocated by rc_client_get_leaderboard_list. + */ +void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list); + +typedef struct rc_client_leaderboard_entry_t { + const char* user; + char display[24]; + time_t submitted; + uint32_t rank; + uint32_t index; +} rc_client_leaderboard_entry_t; + +typedef struct rc_client_leaderboard_entry_list_t { + rc_client_leaderboard_entry_t* entries; + uint32_t num_entries; + int32_t user_index; +} rc_client_leaderboard_entry_list_t; + +typedef void (*rc_client_fetch_leaderboard_entries_callback_t)(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata); + +/** + * Fetches a list of leaderboard entries from the server. + * Callback receives an allocated list that must be free'd by calling rc_client_destroy_leaderboard_entry_list. + */ +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id, + uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + +/** + * Fetches a list of leaderboard entries from the server containing the logged-in user. + * Callback receives an allocated list that must be free'd by calling rc_client_destroy_leaderboard_entry_list. + */ +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id, + uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + +/** + * Gets the URL for the profile picture of the user associated to a leaderboard entry. + * Returns RC_OK on success. + */ +int rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size); + +/** + * Destroys a list allocated by rc_client_begin_fetch_leaderboard_entries or rc_client_begin_fetch_leaderboard_entries_around_user. + */ +void rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list); + +/*****************************************************************************\ +| Rich Presence | +\*****************************************************************************/ + +/** + * Gets the current rich presence message. + * Returns the number of characters written to buffer. + */ +size_t rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size); + +/*****************************************************************************\ +| Processing | +\*****************************************************************************/ + +enum { + RC_CLIENT_EVENT_TYPE_NONE = 0, + RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED = 1, /* [achievement] was earned by the player */ + RC_CLIENT_EVENT_LEADERBOARD_STARTED = 2, /* [leaderboard] attempt has started */ + RC_CLIENT_EVENT_LEADERBOARD_FAILED = 3, /* [leaderboard] attempt failed */ + RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED = 4, /* [leaderboard] attempt submitted */ + RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW = 5, /* [achievement] challenge indicator should be shown */ + RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE = 6, /* [achievement] challenge indicator should be hidden */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW = 7, /* progress indicator should be shown for [achievement] */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE = 8, /* progress indicator should be hidden */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE = 9, /* progress indicator should be updated to reflect new badge/progress for [achievement] */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW = 10, /* [leaderboard_tracker] should be shown */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE = 11, /* [leaderboard_tracker] should be hidden */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE = 12, /* [leaderboard_tracker] updated */ + RC_CLIENT_EVENT_RESET = 13, /* emulated system should be reset (as the result of enabling hardcore) */ + RC_CLIENT_EVENT_GAME_COMPLETED = 14, /* all achievements for the game have been earned */ + RC_CLIENT_EVENT_SERVER_ERROR = 15 /* an API response returned a [server_error] and will not be retried */ +}; + +typedef struct rc_client_server_error_t +{ + const char* error_message; + const char* api; +} rc_client_server_error_t; + +typedef struct rc_client_event_t +{ + uint32_t type; + + rc_client_achievement_t* achievement; + rc_client_leaderboard_t* leaderboard; + rc_client_leaderboard_tracker_t* leaderboard_tracker; + rc_client_server_error_t* server_error; + +} rc_client_event_t; + +/** + * Callback used to notify the client when certain events occur. + */ +typedef void (*rc_client_event_handler_t)(const rc_client_event_t* event, rc_client_t* client); + +/** + * Provides a callback for event handling. + */ +void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler); + +/** + * Provides a callback for reading memory. + */ +void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler); + +/** + * Determines if there are any active achievements/leaderboards/rich presence that need processing. + */ +int rc_client_is_processing_required(rc_client_t* client); + +/** + * Processes achievements for the current frame. + */ +void rc_client_do_frame(rc_client_t* client); + +/** + * Processes the periodic queue. + * Called internally by rc_client_do_frame. + * Should be explicitly called if rc_client_do_frame is not being called because emulation is paused. + */ +void rc_client_idle(rc_client_t* client); + +/** + * Informs the runtime that the emulator has been reset. Will reset all achievements and leaderboards + * to their initial state (includes hiding indicators/trackers). + */ +void rc_client_reset(rc_client_t* client); + +/** + * Gets the number of bytes needed to serialized the runtime state. + */ +size_t rc_client_progress_size(rc_client_t* client); + +/** + * Serializes the runtime state into a buffer. + * Returns RC_OK on success, or an error indicator. + */ +int rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer); + +/** + * Deserializes the runtime state from a buffer. + * Returns RC_OK on success, or an error indicator. + */ +int rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_RUNTIME_H */ diff --git a/dep/rcheevos/include/rc_consoles.h b/dep/rcheevos/include/rc_consoles.h index 21c369c0a..0dd9c1ee1 100644 --- a/dep/rcheevos/include/rc_consoles.h +++ b/dep/rcheevos/include/rc_consoles.h @@ -10,6 +10,7 @@ extern "C" { \*****************************************************************************/ enum { + RC_CONSOLE_UNKNOWN = 0, RC_CONSOLE_MEGA_DRIVE = 1, RC_CONSOLE_NINTENDO_64 = 2, RC_CONSOLE_SUPER_NINTENDO = 3, @@ -85,6 +86,11 @@ enum { RC_CONSOLE_ARCADIA_2001 = 73, RC_CONSOLE_INTERTON_VC_4000 = 74, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER = 75, + RC_CONSOLE_PC_ENGINE_CD = 76, + RC_CONSOLE_ATARI_JAGUAR_CD = 77, + RC_CONSOLE_NINTENDO_DSI = 78, + RC_CONSOLE_TI83 = 79, + RC_CONSOLE_UZEBOX = 80, RC_CONSOLE_HUBS = 100, RC_CONSOLE_EVENTS = 101 diff --git a/dep/rcheevos/include/rc_error.h b/dep/rcheevos/include/rc_error.h index 91a98c11c..b80bc0bab 100644 --- a/dep/rcheevos/include/rc_error.h +++ b/dep/rcheevos/include/rc_error.h @@ -36,7 +36,13 @@ enum { RC_INVALID_MEASURED_TARGET = -23, RC_INVALID_COMPARISON = -24, RC_INVALID_STATE = -25, - RC_INVALID_JSON = -26 + RC_INVALID_JSON = -26, + RC_API_FAILURE = -27, + RC_LOGIN_REQUIRED = -28, + RC_NO_GAME_LOADED = -29, + RC_HARDCORE_DISABLED = -30, + RC_ABORTED = -31, + RC_NO_RESPONSE = -32 }; const char* rc_error_str(int ret); diff --git a/dep/rcheevos/include/rc_hash.h b/dep/rcheevos/include/rc_hash.h index b5fd59bab..ba9ea1c02 100644 --- a/dep/rcheevos/include/rc_hash.h +++ b/dep/rcheevos/include/rc_hash.h @@ -27,20 +27,20 @@ extern "C" { /* data for rc_hash_iterate */ - struct rc_hash_iterator + typedef struct rc_hash_iterator { - uint8_t* buffer; + const uint8_t* buffer; size_t buffer_size; uint8_t consoles[12]; int index; const char* path; - }; + } rc_hash_iterator_t; /* initializes a rc_hash_iterator * - path must be provided * - if buffer and buffer_size are provided, path may be a filename (i.e. for something extracted from a zip file) */ - void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, uint8_t* buffer, size_t buffer_size); + void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, const uint8_t* buffer, size_t buffer_size); /* releases resources associated to a rc_hash_iterator */ @@ -92,11 +92,12 @@ extern "C" { /* ===================================================== */ - #define RC_HASH_CDTRACK_FIRST_DATA ((uint32_t)-1) - #define RC_HASH_CDTRACK_LAST ((uint32_t)-2) - #define RC_HASH_CDTRACK_LARGEST ((uint32_t)-3) + #define RC_HASH_CDTRACK_FIRST_DATA ((uint32_t)-1) /* the first data track (skip audio tracks) */ + #define RC_HASH_CDTRACK_LAST ((uint32_t)-2) /* the last data/audio track */ + #define RC_HASH_CDTRACK_LARGEST ((uint32_t)-3) /* the largest data/audio track */ + #define RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION ((uint32_t)-4) /* the first data/audio track of the second session */ - /* opens a track from the specified file. track 0 indicates the largest data track should be opened. + /* opens a track from the specified file. see the RC_HASH_CDTRACK_ defines for special tracks. * returns a handle to be passed to the other functions, or NULL if the track could not be opened. */ typedef void* (*rc_hash_cdreader_open_track_handler)(const char* path, uint32_t track); diff --git a/dep/rcheevos/include/rc_runtime.h b/dep/rcheevos/include/rc_runtime.h index 6ad5f0266..d7a25c707 100644 --- a/dep/rcheevos/include/rc_runtime.h +++ b/dep/rcheevos/include/rc_runtime.h @@ -88,9 +88,12 @@ typedef struct rc_runtime_t { rc_value_t* variables; rc_value_t** next_variable; + + char owns_self; } rc_runtime_t; +rc_runtime_t* rc_runtime_alloc(void); void rc_runtime_init(rc_runtime_t* runtime); void rc_runtime_destroy(rc_runtime_t* runtime); @@ -121,7 +124,8 @@ enum { RC_RUNTIME_EVENT_LBOARD_TRIGGERED, RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, RC_RUNTIME_EVENT_LBOARD_DISABLED, - RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED + RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, + RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED }; typedef struct rc_runtime_event_t { diff --git a/dep/rcheevos/include/rc_runtime_types.h b/dep/rcheevos/include/rc_runtime_types.h index c446ab730..dc3ab60c9 100644 --- a/dep/rcheevos/include/rc_runtime_types.h +++ b/dep/rcheevos/include/rc_runtime_types.h @@ -56,6 +56,7 @@ enum { RC_MEMSIZE_32_BITS_BE, RC_MEMSIZE_FLOAT, RC_MEMSIZE_MBF32, + RC_MEMSIZE_MBF32_LE, RC_MEMSIZE_VARIABLE }; @@ -169,7 +170,8 @@ enum { RC_OPERATOR_NONE, RC_OPERATOR_MULT, RC_OPERATOR_DIV, - RC_OPERATOR_AND + RC_OPERATOR_AND, + RC_OPERATOR_XOR }; typedef struct rc_condition_t rc_condition_t; @@ -198,6 +200,9 @@ struct rc_condition_t { /* Whether or not the condition evaluated true on the last check */ char is_true; + + /* Unique identifier of optimized comparator to use */ + char optimized_comparator; }; /*****************************************************************************\ diff --git a/dep/rcheevos/src/rapi/rc_api_common.c b/dep/rcheevos/src/rapi/rc_api_common.c index c703af752..7fdc7beb1 100644 --- a/dep/rcheevos/src/rapi/rc_api_common.c +++ b/dep/rcheevos/src/rapi/rc_api_common.c @@ -10,7 +10,9 @@ #include #define RETROACHIEVEMENTS_HOST "https://retroachievements.org" -#define RETROACHIEVEMENTS_IMAGE_HOST "http://i.retroachievements.org" +#define RETROACHIEVEMENTS_IMAGE_HOST "https://media.retroachievements.org" +#define RETROACHIEVEMENTS_HOST_NONSSL "http://retroachievements.org" +#define RETROACHIEVEMENTS_IMAGE_HOST_NONSSL "http://media.retroachievements.org" static char* g_host = NULL; static char* g_imagehost = NULL; @@ -18,157 +20,174 @@ static char* g_imagehost = NULL; /* --- rc_json --- */ -static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen); -static int rc_json_parse_array(const char** json_ptr, rc_json_field_t* field); +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen); +static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field); -static int rc_json_parse_field(const char** json_ptr, rc_json_field_t* field) { +static int rc_json_match_char(rc_json_iterator_t* iterator, char c) +{ + if (iterator->json < iterator->end && *iterator->json == c) { + ++iterator->json; + return 1; + } + + return 0; +} + +static void rc_json_skip_whitespace(rc_json_iterator_t* iterator) +{ + while (iterator->json < iterator->end && isspace((unsigned char)*iterator->json)) + ++iterator->json; +} + +static int rc_json_find_closing_quote(rc_json_iterator_t* iterator) +{ + while (iterator->json < iterator->end) { + if (*iterator->json == '"') + return 1; + + if (*iterator->json == '\\') { + ++iterator->json; + if (iterator->json == iterator->end) + return 0; + } + + if (*iterator->json == '\0') + return 0; + + ++iterator->json; + } + + return 0; +} + +static int rc_json_parse_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { int result; - field->value_start = *json_ptr; + if (iterator->json >= iterator->end) + return RC_INVALID_JSON; - switch (**json_ptr) + field->value_start = iterator->json; + + switch (*iterator->json) { case '"': /* quoted string */ - ++(*json_ptr); - while (**json_ptr != '"') { - if (**json_ptr == '\\') - ++(*json_ptr); - - if (**json_ptr == '\0') - return RC_INVALID_JSON; - - ++(*json_ptr); - } - ++(*json_ptr); + ++iterator->json; + if (!rc_json_find_closing_quote(iterator)) + return RC_INVALID_JSON; + ++iterator->json; break; case '-': case '+': /* signed number */ - ++(*json_ptr); + ++iterator->json; /* fallthrough to number */ case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': /* number */ - do { - ++(*json_ptr); - } while (**json_ptr >= '0' && **json_ptr <= '9'); - if (**json_ptr == '.') { - do { - ++(*json_ptr); - } while (**json_ptr >= '0' && **json_ptr <= '9'); + while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9') + ++iterator->json; + + if (rc_json_match_char(iterator, '.')) { + while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9') + ++iterator->json; } break; case '[': /* array */ - result = rc_json_parse_array(json_ptr, field); + result = rc_json_parse_array(iterator, field); if (result != RC_OK) - return result; + return result; break; case '{': /* object */ - result = rc_json_parse_object(json_ptr, NULL, 0, &field->array_size); + result = rc_json_parse_object(iterator, NULL, 0, &field->array_size); if (result != RC_OK) return result; break; default: /* non-quoted text [true,false,null] */ - if (!isalpha((unsigned char)**json_ptr)) + if (!isalpha((unsigned char)*iterator->json)) return RC_INVALID_JSON; - do { - ++(*json_ptr); - } while (isalnum((unsigned char)**json_ptr)); + while (iterator->json < iterator->end && isalnum((unsigned char)*iterator->json)) + ++iterator->json; break; } - field->value_end = *json_ptr; + field->value_end = iterator->json; return RC_OK; } -static int rc_json_parse_array(const char** json_ptr, rc_json_field_t* field) { +static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field) { rc_json_field_t unused_field; - const char* json = *json_ptr; int result; - if (*json != '[') + if (!rc_json_match_char(iterator, '[')) return RC_INVALID_JSON; - ++json; field->array_size = 0; - if (*json != ']') { - do - { - while (isspace((unsigned char)*json)) - ++json; - result = rc_json_parse_field(&json, &unused_field); - if (result != RC_OK) - return result; + if (rc_json_match_char(iterator, ']')) /* empty array */ + return RC_OK; - ++field->array_size; + do + { + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*json)) - ++json; + result = rc_json_parse_field(iterator, &unused_field); + if (result != RC_OK) + return result; - if (*json != ',') - break; + ++field->array_size; - ++json; - } while (1); + rc_json_skip_whitespace(iterator); + } while (rc_json_match_char(iterator, ',')); - if (*json != ']') - return RC_INVALID_JSON; - } + if (!rc_json_match_char(iterator, ']')) + return RC_INVALID_JSON; - *json_ptr = ++json; return RC_OK; } -static int rc_json_get_next_field(rc_json_object_field_iterator_t* iterator) { - const char* json = iterator->json; +static int rc_json_get_next_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*json)) - ++json; - - if (*json != '"') + if (!rc_json_match_char(iterator, '"')) return RC_INVALID_JSON; - iterator->field.name = ++json; - while (*json != '"') { - if (!*json) + field->name = iterator->json; + while (iterator->json < iterator->end && *iterator->json != '"') { + if (!*iterator->json) return RC_INVALID_JSON; - ++json; + ++iterator->json; } - iterator->name_len = json - iterator->field.name; - ++json; - while (isspace((unsigned char)*json)) - ++json; - - if (*json != ':') + if (iterator->json == iterator->end) return RC_INVALID_JSON; - ++json; + field->name_len = iterator->json - field->name; + ++iterator->json; - while (isspace((unsigned char)*json)) - ++json; + rc_json_skip_whitespace(iterator); - if (rc_json_parse_field(&json, &iterator->field) < 0) + if (!rc_json_match_char(iterator, ':')) return RC_INVALID_JSON; - while (isspace((unsigned char)*json)) - ++json; + rc_json_skip_whitespace(iterator); + + if (rc_json_parse_field(iterator, field) < 0) + return RC_INVALID_JSON; + + rc_json_skip_whitespace(iterator); - iterator->json = json; return RC_OK; } -static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen) { - rc_json_object_field_iterator_t iterator; - const char* json = *json_ptr; +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen) { size_t i; unsigned num_fields = 0; + rc_json_field_t field; int result; if (fields_seen) @@ -177,60 +196,105 @@ static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, for (i = 0; i < field_count; ++i) fields[i].value_start = fields[i].value_end = NULL; - if (*json != '{') + if (!rc_json_match_char(iterator, '{')) return RC_INVALID_JSON; - ++json; - if (*json == '}') { - *json_ptr = ++json; + if (rc_json_match_char(iterator, '}')) /* empty object */ return RC_OK; - } - - memset(&iterator, 0, sizeof(iterator)); - iterator.json = json; do { - result = rc_json_get_next_field(&iterator); + result = rc_json_get_next_field(iterator, &field); if (result != RC_OK) return result; for (i = 0; i < field_count; ++i) { - if (!fields[i].value_start && strncmp(fields[i].name, iterator.field.name, iterator.name_len) == 0 && - fields[i].name[iterator.name_len] == '\0') { - fields[i].value_start = iterator.field.value_start; - fields[i].value_end = iterator.field.value_end; - fields[i].array_size = iterator.field.array_size; + if (!fields[i].value_start && fields[i].name_len == field.name_len && + memcmp(fields[i].name, field.name, field.name_len) == 0) { + fields[i].value_start = field.value_start; + fields[i].value_end = field.value_end; + fields[i].array_size = field.array_size; break; } } ++num_fields; - if (*iterator.json != ',') - break; - ++iterator.json; - } while (1); + } while (rc_json_match_char(iterator, ',')); - if (*iterator.json != '}') + if (!rc_json_match_char(iterator, '}')) return RC_INVALID_JSON; if (fields_seen) *fields_seen = num_fields; - *json_ptr = ++iterator.json; return RC_OK; } -int rc_json_get_next_object_field(rc_json_object_field_iterator_t* iterator) { - if (*iterator->json != ',' && *iterator->json != '{') +int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + if (!rc_json_match_char(iterator, ',') && !rc_json_match_char(iterator, '{')) return 0; - ++iterator->json; - return (rc_json_get_next_field(iterator) == RC_OK); + return (rc_json_get_next_field(iterator, field) == RC_OK); } -int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_json_field_t* fields, size_t field_count) { +int rc_json_get_object_string_length(const char* json) { + const char* json_start = json; + + rc_json_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = json; + iterator.end = json + (1024 * 1024 * 1024); /* arbitrary 1GB limit on JSON response */ + + rc_json_parse_object(&iterator, NULL, 0, NULL); + + return (int)(iterator.json - json_start); +} + +static int rc_json_extract_html_error(rc_api_response_t* response, const rc_api_server_response_t* server_response) { + const char* json = server_response->body; + const char* end = json; + + const char* title_start = strstr(json, ""); + if (title_start) { + title_start += 7; + if (isdigit((int)*title_start)) { + const char* title_end = strstr(title_start + 7, ""); + if (title_end) { + char* dst = rc_buf_reserve(&response->buffer, (title_end - title_start) + 1); + response->error_message = dst; + memcpy(dst, title_start, title_end - title_start); + dst += (title_end - title_start); + *dst++ = '\0'; + rc_buf_consume(&response->buffer, response->error_message, dst); + response->succeeded = 0; + return RC_INVALID_JSON; + } + } + } + + while (*end && *end != '\n' && end - json < 200) + ++end; + + if (end > json && end[-1] == '\r') + --end; + + if (end > json) { + char* dst = rc_buf_reserve(&response->buffer, (end - json) + 1); + response->error_message = dst; + memcpy(dst, json, end - json); + dst += (end - json); + *dst++ = '\0'; + rc_buf_consume(&response->buffer, response->error_message, dst); + } + + response->succeeded = 0; + return RC_INVALID_JSON; +} + +int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count) { + int result; + #ifndef NDEBUG if (field_count < 2) return RC_INVALID_STATE; @@ -240,37 +304,28 @@ int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_jso return RC_INVALID_STATE; #endif - if (*json == '{') { - int result = rc_json_parse_object(&json, fields, field_count, NULL); + response->error_message = NULL; + + if (!server_response || !server_response->body || !*server_response->body) { + response->succeeded = 0; + return RC_NO_RESPONSE; + } + + if (*server_response->body != '{') { + result = rc_json_extract_html_error(response, server_response); + } + else { + rc_json_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = server_response->body; + iterator.end = server_response->body + server_response->body_length; + result = rc_json_parse_object(&iterator, fields, field_count, NULL); rc_json_get_optional_string(&response->error_message, response, &fields[1], "Error", NULL); rc_json_get_optional_bool(&response->succeeded, &fields[0], "Success", 1); - - return result; } - response->error_message = NULL; - - if (*json) { - const char* end = json; - while (*end && *end != '\n' && end - json < 200) - ++end; - - if (end > json && end[-1] == '\r') - --end; - - if (end > json) { - char* dst = rc_buf_reserve(&response->buffer, (end - json) + 1); - response->error_message = dst; - memcpy(dst, json, end - json); - dst += (end - json); - *dst++ = '\0'; - rc_buf_consume(&response->buffer, response->error_message, dst); - } - } - - response->succeeded = 0; - return RC_INVALID_JSON; + return result; } static int rc_json_missing_field(rc_api_response_t* response, const rc_json_field_t* field) { @@ -293,42 +348,48 @@ static int rc_json_missing_field(rc_api_response_t* response, const rc_json_fiel } int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name) { - const char* json = field->value_start; + rc_json_iterator_t iterator; + #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif - if (!json) + if (!field->value_start) return rc_json_missing_field(response, field); - return (rc_json_parse_object(&json, fields, field_count, &field->array_size) == RC_OK); + memset(&iterator, 0, sizeof(iterator)); + iterator.json = field->value_start; + iterator.end = field->value_end; + return (rc_json_parse_object(&iterator, fields, field_count, &field->array_size) == RC_OK); } -static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_field_t* iterator) { - if (!iterator->array_size) +static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); + + if (iterator->json >= iterator->end) return 0; - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + if (rc_json_parse_field(iterator, field) != RC_OK) + return 0; - rc_json_parse_field(&iterator->value_start, field); + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); - ++iterator->value_start; /* skip , or ] */ - - --iterator->array_size; return 1; } int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { - rc_json_field_t iterator; + rc_json_iterator_t iterator; + rc_json_field_t array; rc_json_field_t value; unsigned* entry; - if (!rc_json_get_required_array(num_entries, &iterator, response, field, field_name)) + if (!rc_json_get_required_array(num_entries, &array, response, field, field_name)) return RC_MISSING_VALUE; if (*num_entries) { @@ -338,6 +399,10 @@ int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, r value.name = field_name; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array.value_start; + iterator.end = array.value_end; + entry = *entries; while (rc_json_get_array_entry_value(&value, &iterator)) { if (!rc_json_get_unum(entry, &value, field_name)) @@ -353,10 +418,12 @@ int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, r return RC_OK; } -int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif if (!field->value_start || *field->value_start != '[') { @@ -364,28 +431,27 @@ int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, return rc_json_missing_field(response, field); } - memcpy(iterator, field, sizeof(*iterator)); - ++iterator->value_start; /* skip [ */ + memcpy(array_field, field, sizeof(*array_field)); + ++array_field->value_start; /* skip [ */ *num_entries = field->array_size; return 1; } -int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_field_t* iterator) { - if (!iterator->array_size) +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); + + if (iterator->json >= iterator->end) return 0; - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + if (rc_json_parse_object(iterator, fields, field_count, NULL) != RC_OK) + return 0; - rc_json_parse_object(&iterator->value_start, fields, field_count, NULL); + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); - ++iterator->value_start; /* skip , or ] */ - - --iterator->array_size; return 1; } @@ -451,6 +517,8 @@ int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_ #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif if (!src) { @@ -562,6 +630,8 @@ int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_na #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif if (!src) { @@ -613,6 +683,8 @@ int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* fi #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif if (!src) { @@ -654,11 +726,13 @@ int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif if (*field->value_start == '\"') { memset(&tm, 0, sizeof(tm)); - if (sscanf(field->value_start + 1, "%d-%d-%d %d:%d:%d", + if (sscanf_s(field->value_start + 1, "%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) { tm.tm_mon--; /* 0-based */ tm.tm_year -= 1900; /* 1900 based */ @@ -669,9 +743,11 @@ int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* * timezone conversion twice and manually removing the difference */ { time_t local_timet = mktime(&tm); - struct tm* gmt_tm = gmtime(&local_timet); - time_t skewed_timet = mktime(gmt_tm); /* applies local time adjustment second time */ - time_t tz_offset = skewed_timet - local_timet; + time_t skewed_timet, tz_offset; + struct tm gmt_tm; + gmtime_s(&gmt_tm, &local_timet); + skewed_timet = mktime(&gmt_tm); /* applies local time adjustment second time */ + tz_offset = skewed_timet - local_timet; *out = local_timet - tz_offset; } @@ -696,6 +772,8 @@ int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_n #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif if (src) { @@ -731,31 +809,32 @@ int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_js /* --- rc_buf --- */ void rc_buf_init(rc_api_buffer_t* buffer) { - buffer->write = &buffer->data[0]; - buffer->end = &buffer->data[sizeof(buffer->data)]; - buffer->next = NULL; + buffer->chunk.write = buffer->chunk.start = &buffer->data[0]; + buffer->chunk.end = &buffer->data[sizeof(buffer->data)]; + buffer->chunk.next = NULL; } void rc_buf_destroy(rc_api_buffer_t* buffer) { + rc_api_buffer_chunk_t *chunk; #ifdef DEBUG_BUFFERS int count = 0; int wasted = 0; int total = 0; #endif - /* first buffer is not allocated */ - buffer = buffer->next; + /* first chunk is not allocated. skip it. */ + chunk = buffer->chunk.next; /* deallocate any additional buffers */ - while (buffer) { - rc_api_buffer_t* next = buffer->next; + while (chunk) { + rc_api_buffer_chunk_t* next = chunk->next; #ifdef DEBUG_BUFFERS - total += (int)(buffer->end - buffer->data); - wasted += (int)(buffer->end - buffer->write); + total += (int)(chunk->end - chunk->data); + wasted += (int)(chunk->end - chunk->write); ++count; #endif - free(buffer); - buffer = next; + free(chunk); + chunk = next; } #ifdef DEBUG_BUFFERS @@ -765,47 +844,50 @@ void rc_buf_destroy(rc_api_buffer_t* buffer) { } char* rc_buf_reserve(rc_api_buffer_t* buffer, size_t amount) { + rc_api_buffer_chunk_t* chunk = &buffer->chunk; size_t remaining; - while (buffer) { - remaining = buffer->end - buffer->write; + while (chunk) { + remaining = chunk->end - chunk->write; if (remaining >= amount) - return buffer->write; + return chunk->write; - if (!buffer->next) { - /* allocate a chunk of memory that is a multiple of 256-bytes. casting it to an rc_api_buffer_t will - * effectively unbound the data field, so use write and end pointers to track how data is being used. + if (!chunk->next) { + /* allocate a chunk of memory that is a multiple of 256-bytes. the first 32 bytes will be associated + * to the chunk header, and the remaining will be used for data. */ - const size_t buffer_prefix_size = sizeof(rc_api_buffer_t) - sizeof(buffer->data); - const size_t alloc_size = (amount + buffer_prefix_size + 0xFF) & ~0xFF; - buffer->next = (rc_api_buffer_t*)malloc(alloc_size); - if (!buffer->next) + const size_t chunk_header_size = sizeof(rc_api_buffer_chunk_t); + const size_t alloc_size = (chunk_header_size + amount + 0xFF) & ~0xFF; + chunk->next = (rc_api_buffer_chunk_t*)malloc(alloc_size); + if (!chunk->next) break; - buffer->next->write = buffer->next->data; - buffer->next->end = buffer->next->write + (alloc_size - buffer_prefix_size); - buffer->next->next = NULL; + chunk->next->start = (char*)chunk->next + chunk_header_size; + chunk->next->write = chunk->next->start; + chunk->next->end = (char*)chunk->next + alloc_size; + chunk->next->next = NULL; } - buffer = buffer->next; + chunk = chunk->next; } return NULL; } void rc_buf_consume(rc_api_buffer_t* buffer, const char* start, char* end) { + rc_api_buffer_chunk_t* chunk = &buffer->chunk; do { - if (buffer->write == start) { - size_t offset = (end - buffer->data); + if (chunk->write == start) { + size_t offset = (end - chunk->start); offset = (offset + 7) & ~7; - buffer->write = &buffer->data[offset]; + chunk->write = &chunk->start[offset]; - if (buffer->write > buffer->end) - buffer->write = buffer->end; + if (chunk->write > chunk->end) + chunk->write = chunk->end; break; } - buffer = buffer->next; - } while (buffer); + chunk = chunk->next; + } while (chunk); } void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount) { @@ -814,6 +896,18 @@ void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount) { return (void*)ptr; } +char* rc_buf_strncpy(rc_api_buffer_t* buffer, const char* src, size_t len) { + char* dst = rc_buf_reserve(buffer, len + 1); + memcpy(dst, src, len); + dst[len] = '\0'; + rc_buf_consume(buffer, dst, dst + len + 2); + return dst; +} + +char* rc_buf_strcpy(rc_api_buffer_t* buffer, const char* src) { + return rc_buf_strncpy(buffer, src, strlen(src)); +} + void rc_api_destroy_request(rc_api_request_t* request) { rc_buf_destroy(&request->buffer); } @@ -828,13 +922,13 @@ void rc_api_format_md5(char checksum[33], const unsigned char digest[16]) { /* --- rc_url_builder --- */ void rc_url_builder_init(rc_api_url_builder_t* builder, rc_api_buffer_t* buffer, size_t estimated_size) { - rc_api_buffer_t* used_buffer; + rc_api_buffer_chunk_t* used_buffer; memset(builder, 0, sizeof(*builder)); builder->buffer = buffer; builder->write = builder->start = rc_buf_reserve(buffer, estimated_size); - used_buffer = buffer; + used_buffer = &buffer->chunk; while (used_buffer && used_buffer->write != builder->write) used_buffer = used_buffer->next; @@ -857,7 +951,7 @@ static int rc_url_builder_reserve(rc_api_url_builder_t* builder, size_t amount) if (remaining < amount) { const size_t used = builder->write - builder->start; const size_t current_size = builder->end - builder->start; - const size_t buffer_prefix_size = sizeof(rc_api_buffer_t) - sizeof(builder->buffer->data); + const size_t buffer_prefix_size = sizeof(rc_api_buffer_chunk_t); char* new_start; size_t new_size = (current_size < 256) ? 256 : current_size * 2; do { @@ -1054,6 +1148,16 @@ static void rc_api_update_host(char** host, const char* hostname) { void rc_api_set_host(const char* hostname) { rc_api_update_host(&g_host, hostname); + + if (!hostname) { + /* also clear out the image hostname */ + rc_api_set_image_host(NULL); + } + else if (strcmp(hostname, RETROACHIEVEMENTS_HOST_NONSSL) == 0) { + /* if just pointing at the non-HTTPS host, explicitly use the default image host + * so it doesn't try to use the web host directly */ + rc_api_set_image_host(RETROACHIEVEMENTS_IMAGE_HOST_NONSSL); + } } void rc_api_set_image_host(const char* hostname) { diff --git a/dep/rcheevos/src/rapi/rc_api_common.h b/dep/rcheevos/src/rapi/rc_api_common.h index 9e2dc53b0..99b17c6d6 100644 --- a/dep/rcheevos/src/rapi/rc_api_common.h +++ b/dep/rcheevos/src/rapi/rc_api_common.h @@ -10,10 +10,13 @@ extern "C" { #endif +#define RC_CONTENT_TYPE_URLENCODED "application/x-www-form-urlencoded" + typedef struct rc_api_url_builder_t { char* write; char* start; char* end; + /* pointer to a preallocated rc_api_buffer_t */ rc_api_buffer_t* buffer; int result; } @@ -23,22 +26,24 @@ void rc_url_builder_init(rc_api_url_builder_t* builder, rc_api_buffer_t* buffer, void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len); const char* rc_url_builder_finalize(rc_api_url_builder_t* builder); +#define RC_JSON_NEW_FIELD(n) {NULL,NULL,n,sizeof(n)-1,0} + typedef struct rc_json_field_t { - const char* name; const char* value_start; const char* value_end; + const char* name; + size_t name_len; unsigned array_size; } rc_json_field_t; -typedef struct rc_json_object_field_iterator_t { - rc_json_field_t field; +typedef struct rc_json_iterator_t { const char* json; - size_t name_len; + const char* end; } -rc_json_object_field_iterator_t; +rc_json_iterator_t; -int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_json_field_t* fields, size_t field_count); +int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count); int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_field_t* field, const char* field_name); int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_name); int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* field_name); @@ -55,15 +60,18 @@ int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_js int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name); int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); -int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); -int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_field_t* iterator); -int rc_json_get_next_object_field(rc_json_object_field_iterator_t* iterator); +int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator); +int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field); +int rc_json_get_object_string_length(const char* json); void rc_buf_init(rc_api_buffer_t* buffer); void rc_buf_destroy(rc_api_buffer_t* buffer); char* rc_buf_reserve(rc_api_buffer_t* buffer, size_t amount); void rc_buf_consume(rc_api_buffer_t* buffer, const char* start, char* end); void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount); +char* rc_buf_strcpy(rc_api_buffer_t* buffer, const char* src); +char* rc_buf_strncpy(rc_api_buffer_t* buffer, const char* src, size_t len); void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str); void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int value); diff --git a/dep/rcheevos/src/rapi/rc_api_editor.c b/dep/rcheevos/src/rapi/rc_api_editor.c index 3d2bd7401..ea4cbe19b 100644 --- a/dep/rcheevos/src/rapi/rc_api_editor.c +++ b/dep/rcheevos/src/rapi/rc_api_editor.c @@ -22,12 +22,24 @@ int rc_api_init_fetch_code_notes_request(rc_api_request_t* request, const rc_api rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response, const char* server_response) { - rc_json_field_t iterator; + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_code_notes_server_response(response, &response_obj); +} + +int rc_api_process_fetch_code_notes_server_response(rc_api_fetch_code_notes_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t array_field; + rc_json_iterator_t iterator; rc_api_code_note_t* note; const char* address_str; const char* last_author = ""; @@ -36,25 +48,25 @@ int rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"CodeNotes"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("CodeNotes") }; rc_json_field_t note_fields[] = { - {"Address"}, - {"User"}, - {"Note"} + RC_JSON_NEW_FIELD("Address"), + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Note") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; - if (!rc_json_get_required_array(&response->num_notes, &iterator, &response->response, &fields[2], "CodeNotes")) + if (!rc_json_get_required_array(&response->num_notes, &array_field, &response->response, &fields[2], "CodeNotes")) return RC_MISSING_VALUE; if (response->num_notes) { @@ -62,15 +74,21 @@ int rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* if (!response->notes) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + note = response->notes; while (rc_json_get_array_entry_object(note_fields, sizeof(note_fields) / sizeof(note_fields[0]), &iterator)) { /* an empty note represents a record that was deleted on the server */ /* a note set to '' also represents a deleted note (remnant of a bug) */ /* NOTE: len will include the quotes */ - len = note_fields[2].value_end - note_fields[2].value_start; - if (len == 2 || (len == 4 && note_fields[2].value_start[1] == '\'' && note_fields[2].value_start[2] == '\'')) { - --response->num_notes; - continue; + if (note_fields[2].value_start) { + len = note_fields[2].value_end - note_fields[2].value_start; + if (len == 2 || (len == 4 && note_fields[2].value_start[1] == '\'' && note_fields[2].value_start[2] == '\'')) { + --response->num_notes; + continue; + } } if (!rc_json_get_required_string(&address_str, &response->response, ¬e_fields[0], "Address")) @@ -123,27 +141,38 @@ int rc_api_init_update_code_note_request(rc_api_request_t* request, const rc_api rc_url_builder_append_str_param(&builder, "n", api_params->note); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_update_code_note_response(rc_api_update_code_note_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_update_code_note_server_response(response, &response_obj); +} + +int rc_api_process_update_code_note_server_response(rc_api_update_code_note_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error") /* unused fields - {"GameID"}, - {"Address"}, - {"Note"} + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("Address"), + RC_JSON_NEW_FIELD("Note") */ }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -206,23 +235,34 @@ int rc_api_init_update_achievement_request(rc_api_request_t* request, const rc_a rc_url_builder_append_str_param(&builder, "h", buffer); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_update_achievement_response(rc_api_update_achievement_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_update_achievement_server_response(response, &response_obj); +} + +int rc_api_process_update_achievement_server_response(rc_api_update_achievement_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"AchievementID"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("AchievementID") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -297,23 +337,34 @@ int rc_api_init_update_leaderboard_request(rc_api_request_t* request, const rc_a rc_url_builder_append_str_param(&builder, "h", buffer); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_update_leaderboard_response(rc_api_update_leaderboard_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_update_leaderboard_server_response(response, &response_obj); +} + +int rc_api_process_update_leaderboard_server_response(rc_api_update_leaderboard_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"LeaderboardID"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("LeaderboardID") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -338,24 +389,37 @@ int rc_api_init_fetch_badge_range_request(rc_api_request_t* request, const rc_ap rc_url_builder_append_str_param(&builder, "r", "badgeiter"); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + (void)api_params; return builder.result; } int rc_api_process_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_badge_range_server_response(response, &response_obj); +} + +int rc_api_process_fetch_badge_range_server_response(rc_api_fetch_badge_range_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"FirstBadge"}, - {"NextBadge"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("FirstBadge"), + RC_JSON_NEW_FIELD("NextBadge") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -399,33 +463,44 @@ int rc_api_init_add_game_hash_request(rc_api_request_t* request, const rc_api_ad rc_url_builder_append_str_param(&builder, "d", api_params->hash_description); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_add_game_hash_response(rc_api_add_game_hash_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_add_game_hash_server_response(response, &response_obj); +} + +int rc_api_process_add_game_hash_server_response(rc_api_add_game_hash_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"Response"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") }; rc_json_field_t response_fields[] = { - {"GameID"} + RC_JSON_NEW_FIELD("GameID") /* unused fields - {"MD5"}, - {"ConsoleID"}, - {"GameTitle"}, - {"Success"} + RC_JSON_NEW_FIELD("MD5"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("GameTitle"), + RC_JSON_NEW_FIELD("Success") */ }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; diff --git a/dep/rcheevos/src/rapi/rc_api_info.c b/dep/rcheevos/src/rapi/rc_api_info.c index 7dc758a1c..0ee4de79c 100644 --- a/dep/rcheevos/src/rapi/rc_api_info.c +++ b/dep/rcheevos/src/rapi/rc_api_info.c @@ -27,45 +27,57 @@ int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_url_builder_append_unum_param(&builder, "c", api_params->count); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_achievement_info_server_response(response, &response_obj); +} + +int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achievement_info_response_t* response, const rc_api_server_response_t* server_response) { rc_api_achievement_awarded_entry_t* entry; - rc_json_field_t iterator; + rc_json_field_t array_field; + rc_json_iterator_t iterator; unsigned timet; int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"AchievementID"}, - {"Response"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("AchievementID"), + RC_JSON_NEW_FIELD("Response") /* unused fields - {"Offset"}, - {"Count"}, - {"FriendsOnly"}, + RC_JSON_NEW_FIELD("Offset"), + RC_JSON_NEW_FIELD("Count"), + RC_JSON_NEW_FIELD("FriendsOnly") * unused fields */ }; rc_json_field_t response_fields[] = { - {"NumEarned"}, - {"TotalPlayers"}, - {"GameID"}, - {"RecentWinner"} /* array */ + RC_JSON_NEW_FIELD("NumEarned"), + RC_JSON_NEW_FIELD("TotalPlayers"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("RecentWinner") /* array */ }; rc_json_field_t entry_fields[] = { - {"User"}, - {"DateAwarded"} + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("DateAwarded") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -81,7 +93,7 @@ int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info if (!rc_json_get_required_unum(&response->game_id, &response->response, &response_fields[2], "GameID")) return RC_MISSING_VALUE; - if (!rc_json_get_required_array(&response->num_recently_awarded, &iterator, &response->response, &response_fields[3], "RecentWinner")) + if (!rc_json_get_required_array(&response->num_recently_awarded, &array_field, &response->response, &response_fields[3], "RecentWinner")) return RC_MISSING_VALUE; if (response->num_recently_awarded) { @@ -89,6 +101,10 @@ int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info if (!response->recently_awarded) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + entry = response->recently_awarded; while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) @@ -130,57 +146,69 @@ int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_url_builder_append_unum_param(&builder, "c", api_params->count); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_leaderboard_info_server_response(response, &response_obj); +} + +int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboard_info_response_t* response, const rc_api_server_response_t* server_response) { rc_api_lboard_info_entry_t* entry; - rc_json_field_t iterator; + rc_json_field_t array_field; + rc_json_iterator_t iterator; unsigned timet; int result; size_t len; char format[16]; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"LeaderboardData"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("LeaderboardData") }; rc_json_field_t leaderboarddata_fields[] = { - {"LBID"}, - {"LBFormat"}, - {"LowerIsBetter"}, - {"LBTitle"}, - {"LBDesc"}, - {"LBMem"}, - {"GameID"}, - {"LBAuthor"}, - {"LBCreated"}, - {"LBUpdated"}, - {"Entries"} /* array */ + RC_JSON_NEW_FIELD("LBID"), + RC_JSON_NEW_FIELD("LBFormat"), + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("LBTitle"), + RC_JSON_NEW_FIELD("LBDesc"), + RC_JSON_NEW_FIELD("LBMem"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("LBAuthor"), + RC_JSON_NEW_FIELD("LBCreated"), + RC_JSON_NEW_FIELD("LBUpdated"), + RC_JSON_NEW_FIELD("Entries") /* array */ /* unused fields - {"GameTitle"}, - {"ConsoleID"}, - {"ConsoleName"}, - {"ForumTopicID"}, - {"GameIcon"}, + RC_JSON_NEW_FIELD("GameTitle"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("ConsoleName"), + RC_JSON_NEW_FIELD("ForumTopicID"), + RC_JSON_NEW_FIELD("GameIcon") * unused fields */ }; rc_json_field_t entry_fields[] = { - {"User"}, - {"Rank"}, - {"Index"}, - {"Score"}, - {"DateSubmitted"} + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("Index"), + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("DateSubmitted") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -218,7 +246,7 @@ int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info response->format = RC_FORMAT_VALUE; } - if (!rc_json_get_required_array(&response->num_entries, &iterator, &response->response, &leaderboarddata_fields[10], "Entries")) + if (!rc_json_get_required_array(&response->num_entries, &array_field, &response->response, &leaderboarddata_fields[10], "Entries")) return RC_MISSING_VALUE; if (response->num_entries) { @@ -226,6 +254,10 @@ int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info if (!response->entries) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + entry = response->entries; while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) @@ -270,26 +302,38 @@ int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_games_list_server_response(response, &response_obj); +} + +int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response) { rc_api_game_list_entry_t* entry; - rc_json_object_field_iterator_t iterator; + rc_json_iterator_t iterator; + rc_json_field_t field; int result; char* end; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"Response"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -308,13 +352,14 @@ int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* memset(&iterator, 0, sizeof(iterator)); iterator.json = fields[2].value_start; + iterator.end = fields[2].value_end; entry = response->entries; - while (rc_json_get_next_object_field(&iterator)) { - entry->id = strtol(iterator.field.name, &end, 10); + while (rc_json_get_next_object_field(&iterator, &field)) { + entry->id = strtol(field.name, &end, 10); - iterator.field.name = ""; - if (!rc_json_get_string(&entry->name, &response->response.buffer, &iterator.field, "")) + field.name = ""; + if (!rc_json_get_string(&entry->name, &response->response.buffer, &field, "")) return RC_MISSING_VALUE; ++entry; diff --git a/dep/rcheevos/src/rapi/rc_api_runtime.c b/dep/rcheevos/src/rapi/rc_api_runtime.c index e42db3cf1..7bac4ec53 100644 --- a/dep/rcheevos/src/rapi/rc_api_runtime.c +++ b/dep/rcheevos/src/rapi/rc_api_runtime.c @@ -24,22 +24,33 @@ int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_res rc_url_builder_append_str_param(&builder, "r", "gameid"); rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_resolve_hash_server_response(response, &response_obj); +} + +int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"GameID"}, + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("GameID") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -65,17 +76,30 @@ int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_ if (rc_api_url_build_dorequest(&builder, "patch", api_params->username, api_params->api_token)) { rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_game_data_server_response(response, &response_obj); +} + +int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response) { rc_api_achievement_definition_t* achievement; rc_api_leaderboard_definition_t* leaderboard; - rc_json_field_t iterator; + rc_json_field_t array_field; + rc_json_iterator_t iterator; const char* str; const char* last_author = ""; + const char* last_author_field = ""; size_t last_author_len = 0; size_t len; unsigned timet; @@ -83,52 +107,52 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r char format[16]; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"PatchData"} /* nested object */ + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("PatchData") /* nested object */ }; rc_json_field_t patchdata_fields[] = { - {"ID"}, - {"Title"}, - {"ConsoleID"}, - {"ImageIcon"}, - {"RichPresencePatch"}, - {"Achievements"}, /* array */ - {"Leaderboards"} /* array */ + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("ImageIcon"), + RC_JSON_NEW_FIELD("RichPresencePatch"), + RC_JSON_NEW_FIELD("Achievements"), /* array */ + RC_JSON_NEW_FIELD("Leaderboards") /* array */ /* unused fields - {"ForumTopicID"}, - {"Flags"}, + RC_JSON_NEW_FIELD("ForumTopicID"), + RC_JSON_NEW_FIELD("Flags") * unused fields */ }; rc_json_field_t achievement_fields[] = { - {"ID"}, - {"Title"}, - {"Description"}, - {"Flags"}, - {"Points"}, - {"MemAddr"}, - {"Author"}, - {"BadgeName"}, - {"Created"}, - {"Modified"} + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("Description"), + RC_JSON_NEW_FIELD("Flags"), + RC_JSON_NEW_FIELD("Points"), + RC_JSON_NEW_FIELD("MemAddr"), + RC_JSON_NEW_FIELD("Author"), + RC_JSON_NEW_FIELD("BadgeName"), + RC_JSON_NEW_FIELD("Created"), + RC_JSON_NEW_FIELD("Modified") }; rc_json_field_t leaderboard_fields[] = { - {"ID"}, - {"Title"}, - {"Description"}, - {"Mem"}, - {"Format"}, - {"LowerIsBetter"}, - {"Hidden"} + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("Description"), + RC_JSON_NEW_FIELD("Mem"), + RC_JSON_NEW_FIELD("Format"), + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("Hidden") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -174,7 +198,7 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r if (!response->rich_presence_script) response->rich_presence_script = ""; - if (!rc_json_get_required_array(&response->num_achievements, &iterator, &response->response, &patchdata_fields[5], "Achievements")) + if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[5], "Achievements")) return RC_MISSING_VALUE; if (response->num_achievements) { @@ -182,6 +206,10 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r if (!response->achievements) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + achievement = response->achievements; while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) { if (!rc_json_get_required_unum(&achievement->id, &response->response, &achievement_fields[0], "ID")) @@ -199,8 +227,8 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r if (!rc_json_get_required_string(&achievement->badge_name, &response->response, &achievement_fields[7], "BadgeName")) return RC_MISSING_VALUE; - len = achievement_fields[7].value_end - achievement_fields[7].value_start; - if (len == last_author_len && memcmp(achievement_fields[7].value_start, last_author, len) == 0) { + len = achievement_fields[6].value_end - achievement_fields[6].value_start; + if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) { achievement->author = last_author; } else { @@ -208,6 +236,7 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r return RC_MISSING_VALUE; last_author = achievement->author; + last_author_field = achievement_fields[6].value_start; last_author_len = len; } @@ -222,7 +251,7 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r } } - if (!rc_json_get_required_array(&response->num_leaderboards, &iterator, &response->response, &patchdata_fields[6], "Leaderboards")) + if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[6], "Leaderboards")) return RC_MISSING_VALUE; if (response->num_leaderboards) { @@ -230,6 +259,10 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r if (!response->leaderboards) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + leaderboard = response->leaderboards; while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) { if (!rc_json_get_required_unum(&leaderboard->id, &response->response, &leaderboard_fields[0], "ID")) @@ -284,21 +317,32 @@ int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_reques rc_url_builder_append_str_param(&builder, "m", api_params->rich_presence); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_ping_server_response(response, &response_obj); +} + +int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response) { rc_json_field_t fields[] = { - {"Success"}, - {"Error"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - return rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); } void rc_api_destroy_ping_response(rc_api_ping_response_t* response) { @@ -337,25 +381,37 @@ int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_ap rc_url_builder_append_str_param(&builder, "v", buffer); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_award_achievement_server_response(response, &response_obj); +} + +int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"Score"}, - {"AchievementID"}, - {"AchievementsRemaining"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("SoftcoreScore"), + RC_JSON_NEW_FIELD("AchievementID"), + RC_JSON_NEW_FIELD("AchievementsRemaining") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -373,8 +429,9 @@ int rc_api_process_award_achievement_response(rc_api_award_achievement_response_ } rc_json_get_optional_unum(&response->new_player_score, &fields[2], "Score", 0); - rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[3], "AchievementID", 0); - rc_json_get_optional_unum(&response->achievements_remaining, &fields[4], "AchievementsRemaining", (unsigned)-1); + rc_json_get_optional_unum(&response->new_player_score_softcore, &fields[3], "SoftcoreScore", 0); + rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[4], "AchievementID", 0); + rc_json_get_optional_unum(&response->achievements_remaining, &fields[5], "AchievementsRemaining", (unsigned)-1); return RC_OK; } @@ -416,67 +473,79 @@ int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_ rc_url_builder_append_str_param(&builder, "v", buffer); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_submit_lboard_entry_server_response(response, &response_obj); +} + +int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response) { rc_api_lboard_entry_t* entry; - rc_json_field_t iterator; + rc_json_field_t array_field; + rc_json_iterator_t iterator; const char* str; int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"Response"} /* nested object */ + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") /* nested object */ }; rc_json_field_t response_fields[] = { - {"Score"}, - {"BestScore"}, - {"RankInfo"}, /* nested object */ - {"TopEntries"} /* array */ + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("BestScore"), + RC_JSON_NEW_FIELD("RankInfo"), /* nested object */ + RC_JSON_NEW_FIELD("TopEntries") /* array */ /* unused fields - {"LBData"}, / * array * / - {"ScoreFormatted"}, - {"TopEntriesFriends"}, / * array * / + RC_JSON_NEW_FIELD("LBData"), / * array * / + RC_JSON_NEW_FIELD("ScoreFormatted"), + RC_JSON_NEW_FIELD("TopEntriesFriends") / * array * / * unused fields */ }; /* unused fields rc_json_field_t lbdata_fields[] = { - {"Format"}, - {"LeaderboardID"}, - {"GameID"}, - {"Title"}, - {"LowerIsBetter"} + RC_JSON_NEW_FIELD("Format"), + RC_JSON_NEW_FIELD("LeaderboardID"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("LowerIsBetter") }; * unused fields */ rc_json_field_t entry_fields[] = { - {"User"}, - {"Rank"}, - {"Score"} + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("Score") /* unused fields - {"DateSubmitted"}, + RC_JSON_NEW_FIELD("DateSubmitted") * unused fields */ }; rc_json_field_t rank_info_fields[] = { - {"Rank"}, - {"NumEntries"} + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("NumEntries") /* unused fields - {"LowerIsBetter"}, - {"UserRank"}, + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("UserRank") * unused fields */ }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -495,7 +564,7 @@ int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_respo return RC_MISSING_VALUE; response->num_entries = (unsigned)atoi(str); - if (!rc_json_get_required_array(&response->num_top_entries, &iterator, &response->response, &response_fields[3], "TopEntries")) + if (!rc_json_get_required_array(&response->num_top_entries, &array_field, &response->response, &response_fields[3], "TopEntries")) return RC_MISSING_VALUE; if (response->num_top_entries) { @@ -503,6 +572,10 @@ int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_respo if (!response->top_entries) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + entry = response->top_entries; while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) diff --git a/dep/rcheevos/src/rapi/rc_api_user.c b/dep/rcheevos/src/rapi/rc_api_user.c index e5181ba95..4a13b8df2 100644 --- a/dep/rcheevos/src/rapi/rc_api_user.c +++ b/dep/rcheevos/src/rapi/rc_api_user.c @@ -1,6 +1,8 @@ #include "rc_api_user.h" #include "rc_api_common.h" +#include "../rcheevos/rc_version.h" + #include /* --- Login --- */ @@ -25,26 +27,38 @@ int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_requ return RC_INVALID_STATE; request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_login_server_response(response, &response_obj); +} + +int rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"User"}, - {"Token"}, - {"Score"}, - {"Messages"}, - {"DisplayName"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Token"), + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("SoftcoreScore"), + RC_JSON_NEW_FIELD("Messages"), + RC_JSON_NEW_FIELD("DisplayName") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -54,9 +68,10 @@ int rc_api_process_login_response(rc_api_login_response_t* response, const char* return RC_MISSING_VALUE; rc_json_get_optional_unum(&response->score, &fields[4], "Score", 0); - rc_json_get_optional_unum(&response->num_unread_messages, &fields[5], "Messages", 0); + rc_json_get_optional_unum(&response->score_softcore, &fields[5], "SoftcoreScore", 0); + rc_json_get_optional_unum(&response->num_unread_messages, &fields[6], "Messages", 0); - rc_json_get_optional_string(&response->display_name, &response->response, &fields[6], "DisplayName", response->username); + rc_json_get_optional_string(&response->display_name, &response->response, &fields[7], "DisplayName", response->username); return RC_OK; } @@ -86,22 +101,34 @@ int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_st */ rc_url_builder_append_unum_param(&builder, "a", 3); rc_url_builder_append_unum_param(&builder, "m", api_params->game_id); + rc_url_builder_append_str_param(&builder, "l", RCHEEVOS_VERSION_STRING); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_start_session_server_response(response, &response_obj); +} + +int rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response) { rc_json_field_t fields[] = { - {"Success"}, - {"Error"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - return rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); } void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response) { @@ -120,27 +147,38 @@ int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_a rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_user_unlocks_server_response(response, &response_obj); +} + +int rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"UserUnlocks"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("UserUnlocks") /* unused fields - { "GameID" }, - { "HardcoreMode" } + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("HardcoreMode") * unused fields */ }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; diff --git a/dep/rcheevos/src/rcheevos/alloc.c b/dep/rcheevos/src/rcheevos/alloc.c index c361eecee..ac2a07bc0 100644 --- a/dep/rcheevos/src/rcheevos/alloc.c +++ b/dep/rcheevos/src/rcheevos/alloc.c @@ -159,6 +159,17 @@ void rc_destroy_parse_state(rc_parse_state_t* parse) } } +unsigned rc_djb2(const char* input) +{ + unsigned result = 5381; + char c; + + while ((c = *input++) != '\0') + result = ((result << 5) + result) + c; /* result = result * 33 + c */ + + return result; +} + const char* rc_error_str(int ret) { switch (ret) { @@ -189,7 +200,12 @@ const char* rc_error_str(int ret) case RC_INVALID_COMPARISON: return "Invalid comparison"; case RC_INVALID_STATE: return "Invalid state"; case RC_INVALID_JSON: return "Invalid JSON"; - + case RC_API_FAILURE: return "API call failed"; + case RC_LOGIN_REQUIRED: return "Login required"; + case RC_NO_GAME_LOADED: return "No game loaded"; + case RC_HARDCORE_DISABLED: return "Hardcore disabled"; + case RC_ABORTED: return "Aborted"; + case RC_NO_RESPONSE: return "No response"; default: return "Unknown error"; } } diff --git a/dep/rcheevos/src/rcheevos/compat.c b/dep/rcheevos/src/rcheevos/compat.c index 877c64ddb..0ba7601ad 100644 --- a/dep/rcheevos/src/rcheevos/compat.c +++ b/dep/rcheevos/src/rcheevos/compat.c @@ -3,6 +3,8 @@ #include #include +#ifdef RC_C89_HELPERS + int rc_strncasecmp(const char* left, const char* right, size_t length) { while (length) @@ -44,20 +46,94 @@ char* rc_strdup(const char* str) { const size_t length = strlen(str); char* buffer = (char*)malloc(length + 1); - memcpy(buffer, str, length + 1); + if (buffer) + memcpy(buffer, str, length + 1); return buffer; } int rc_snprintf(char* buffer, size_t size, const char* format, ...) { - int result; - va_list args; + int result; + va_list args; - va_start(args, format); - /* assume buffer is large enough and ignore size */ - (void)size; - result = vsprintf(buffer, format, args); - va_end(args); + va_start(args, format); - return result; +#ifdef __STDC_WANT_SECURE_LIB__ + result = vsprintf_s(buffer, size, format, args); +#else + /* assume buffer is large enough and ignore size */ + (void)size; + result = vsprintf(buffer, format, args); +#endif + + va_end(args); + + return result; } + +#endif + +#ifndef __STDC_WANT_SECURE_LIB__ + +struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer) +{ + struct tm* tm = gmtime(timer); + memcpy(buf, tm, sizeof(*tm)); + return buf; +} + +#endif + +#ifndef RC_NO_THREADS +#ifdef _WIN32 + +/* https://gist.github.com/roxlu/1c1af99f92bafff9d8d9 */ + +#define WIN32_LEAN_AND_MEAN +#include + +void rc_mutex_init(rc_mutex_t* mutex) +{ + /* default security, not owned by calling thread, unnamed */ + mutex->handle = CreateMutex(NULL, FALSE, NULL); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + CloseHandle(mutex->handle); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + WaitForSingleObject(mutex->handle, 0xFFFFFFFF); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + ReleaseMutex(mutex->handle); +} + +#else + +void rc_mutex_init(rc_mutex_t* mutex) +{ + pthread_mutex_init(mutex, NULL); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + pthread_mutex_destroy(mutex); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + pthread_mutex_lock(mutex); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + pthread_mutex_unlock(mutex); +} + +#endif +#endif /* RC_NO_THREADS */ diff --git a/dep/rcheevos/src/rcheevos/condition.c b/dep/rcheevos/src/rcheevos/condition.c index 03aa0484e..55a8084e7 100644 --- a/dep/rcheevos/src/rcheevos/condition.c +++ b/dep/rcheevos/src/rcheevos/condition.c @@ -1,6 +1,91 @@ #include "rc_internal.h" #include +#include + +static int rc_test_condition_compare(unsigned value1, unsigned value2, char oper) { + switch (oper) { + case RC_OPERATOR_EQ: return value1 == value2; + case RC_OPERATOR_NE: return value1 != value2; + case RC_OPERATOR_LT: return value1 < value2; + case RC_OPERATOR_LE: return value1 <= value2; + case RC_OPERATOR_GT: return value1 > value2; + case RC_OPERATOR_GE: return value1 >= value2; + default: return 1; + } +} + +static char rc_condition_determine_comparator(const rc_condition_t* self) { + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_NE: + case RC_OPERATOR_LT: + case RC_OPERATOR_LE: + case RC_OPERATOR_GT: + case RC_OPERATOR_GE: + break; + + default: + /* not a comparison. should not be getting compared. but if it is, legacy behavior was to return 1 */ + return RC_PROCESSING_COMPARE_ALWAYS_TRUE; + } + + if ((self->operand1.type == RC_OPERAND_ADDRESS || self->operand1.type == RC_OPERAND_DELTA) && + !self->operand1.value.memref->value.is_indirect && !rc_operand_is_float(&self->operand1)) { + /* left side is an integer memory reference */ + int needs_translate = (self->operand1.size != self->operand1.value.memref->value.size); + + if (self->operand2.type == RC_OPERAND_CONST) { + /* right side is a constant */ + if (self->operand1.type == RC_OPERAND_ADDRESS) + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_CONST; + + return needs_translate ? RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED : RC_PROCESSING_COMPARE_DELTA_TO_CONST; + } + else if ((self->operand2.type == RC_OPERAND_ADDRESS || self->operand2.type == RC_OPERAND_DELTA) && + !self->operand2.value.memref->value.is_indirect && !rc_operand_is_float(&self->operand2)) { + /* right side is an integer memory reference */ + const int is_same_memref = (self->operand1.value.memref == self->operand2.value.memref); + needs_translate |= (self->operand2.size != self->operand2.value.memref->value.size); + + if (self->operand1.type == RC_OPERAND_ADDRESS) { + if (self->operand2.type == RC_OPERAND_ADDRESS) { + if (is_same_memref && !needs_translate) { + /* comparing a memref to itself, will evaluate to a constant */ + return rc_test_condition_compare(0, 0, self->oper) ? RC_PROCESSING_COMPARE_ALWAYS_TRUE : RC_PROCESSING_COMPARE_ALWAYS_FALSE; + } + + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF; + } + + assert(self->operand2.type == RC_OPERAND_DELTA); + + if (is_same_memref) { + /* delta comparison is optimized to compare with itself (for detecting change) */ + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_DELTA; + } + } + else { + assert(self->operand1.type == RC_OPERAND_DELTA); + + if (self->operand2.type == RC_OPERAND_ADDRESS) { + if (is_same_memref) { + /* delta comparison is optimized to compare with itself (for detecting change) */ + return needs_translate ? RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED : RC_PROCESSING_COMPARE_DELTA_TO_MEMREF; + } + } + } + } + } + + if (self->operand1.type == RC_OPERAND_CONST && self->operand2.type == RC_OPERAND_CONST) { + /* comparing constants will always generate a constant result */ + return rc_test_condition_compare(self->operand1.value.num, self->operand2.value.num, self->oper) ? + RC_PROCESSING_COMPARE_ALWAYS_TRUE : RC_PROCESSING_COMPARE_ALWAYS_FALSE; + } + + return RC_PROCESSING_COMPARE_DEFAULT; +} static int rc_parse_operator(const char** memaddr) { const char* oper = *memaddr; @@ -50,6 +135,10 @@ static int rc_parse_operator(const char** memaddr) { ++(*memaddr); return RC_OPERATOR_AND; + case '^': + ++(*memaddr); + return RC_OPERATOR_XOR; + case '\0':/* end of string */ case '_': /* next condition */ case 'S': /* next condset */ @@ -71,6 +160,7 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse self->current_hits = 0; self->is_true = 0; self->pause = 0; + self->optimized_comparator = RC_PROCESSING_COMPARE_DEFAULT; if (*aux != 0 && aux[1] == ':') { switch (*aux) { @@ -135,6 +225,7 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse case RC_OPERATOR_MULT: case RC_OPERATOR_DIV: case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: /* modifying operators are only valid on modifying statements */ if (can_modify) break; @@ -209,6 +300,9 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse self->required_hits = 0; } + if (parse->buffer != 0) + self->optimized_comparator = rc_condition_determine_comparator(self); + *memaddr = aux; return self; } @@ -228,12 +322,203 @@ int rc_condition_is_combining(const rc_condition_t* self) { } } +static int rc_test_condition_compare_memref_to_const(rc_condition_t* self) { + const unsigned value1 = self->operand1.value.memref->value.value; + const unsigned value2 = self->operand2.value.num; + assert(self->operand1.size == self->operand1.value.memref->value.size); + return rc_test_condition_compare(value1, value2, self->oper); +} + +static int rc_test_condition_compare_delta_to_const(rc_condition_t* self) { + const rc_memref_value_t* memref1 = &self->operand1.value.memref->value; + const unsigned value1 = (memref1->changed) ? memref1->prior : memref1->value; + const unsigned value2 = self->operand2.value.num; + assert(self->operand1.size == self->operand1.value.memref->value.size); + return rc_test_condition_compare(value1, value2, self->oper); +} + +static int rc_test_condition_compare_memref_to_memref(rc_condition_t* self) { + const unsigned value1 = self->operand1.value.memref->value.value; + const unsigned value2 = self->operand2.value.memref->value.value; + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + return rc_test_condition_compare(value1, value2, self->oper); +} + +static int rc_test_condition_compare_memref_to_delta(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + + if (memref->changed) + return rc_test_condition_compare(memref->value, memref->prior, self->oper); + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_delta_to_memref(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + + if (memref->changed) + return rc_test_condition_compare(memref->prior, memref->value, self->oper); + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_memref_to_const_transformed(rc_condition_t* self) { + rc_typed_value_t value1; + const unsigned value2 = self->operand2.value.num; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = self->operand1.value.memref->value.value; + rc_transform_memref_value(&value1, self->operand1.size); + + return rc_test_condition_compare(value1.value.u32, value2, self->oper); +} + +static int rc_test_condition_compare_delta_to_const_transformed(rc_condition_t* self) { + rc_typed_value_t value1; + const rc_memref_value_t* memref1 = &self->operand1.value.memref->value; + const unsigned value2 = self->operand2.value.num; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = (memref1->changed) ? memref1->prior : memref1->value; + rc_transform_memref_value(&value1, self->operand1.size); + + return rc_test_condition_compare(value1.value.u32, value2, self->oper); +} + +static int rc_test_condition_compare_memref_to_memref_transformed(rc_condition_t* self) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = self->operand1.value.memref->value.value; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = self->operand2.value.memref->value.value; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); +} + +static int rc_test_condition_compare_memref_to_delta_transformed(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + + if (memref->changed) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = memref->value; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = memref->prior; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); + } + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_delta_to_memref_transformed(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + + if (memref->changed) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = memref->prior; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = memref->value; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); + } + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state) { rc_typed_value_t value1, value2; - rc_evaluate_operand(&value1, &self->operand1, eval_state); - if (eval_state->add_value.type != RC_VALUE_TYPE_NONE) + if (eval_state->add_value.type != RC_VALUE_TYPE_NONE) { + /* if there's an accumulator, we can't use the optimized comparators */ + rc_evaluate_operand(&value1, &self->operand1, eval_state); rc_typed_value_add(&value1, &eval_state->add_value); + } else { + /* use an optimized comparator whenever possible */ + switch (self->optimized_comparator) { + case RC_PROCESSING_COMPARE_MEMREF_TO_CONST: + return rc_test_condition_compare_memref_to_const(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_DELTA: + return rc_test_condition_compare_memref_to_delta(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF: + return rc_test_condition_compare_memref_to_memref(self); + case RC_PROCESSING_COMPARE_DELTA_TO_CONST: + return rc_test_condition_compare_delta_to_const(self); + case RC_PROCESSING_COMPARE_DELTA_TO_MEMREF: + return rc_test_condition_compare_delta_to_memref(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED: + return rc_test_condition_compare_memref_to_const_transformed(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED: + return rc_test_condition_compare_memref_to_delta_transformed(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED: + return rc_test_condition_compare_memref_to_memref_transformed(self); + case RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED: + return rc_test_condition_compare_delta_to_const_transformed(self); + case RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED: + return rc_test_condition_compare_delta_to_memref_transformed(self); + case RC_PROCESSING_COMPARE_ALWAYS_TRUE: + return 1; + case RC_PROCESSING_COMPARE_ALWAYS_FALSE: + return 0; + default: + rc_evaluate_operand(&value1, &self->operand1, eval_state); + break; + } + } rc_evaluate_operand(&value2, &self->operand2, eval_state); @@ -260,5 +545,11 @@ void rc_evaluate_condition_value(rc_typed_value_t* value, rc_condition_t* self, rc_typed_value_convert(&amount, RC_VALUE_TYPE_UNSIGNED); value->value.u32 &= amount.value.u32; break; + + case RC_OPERATOR_XOR: + rc_typed_value_convert(value, RC_VALUE_TYPE_UNSIGNED); + rc_typed_value_convert(&amount, RC_VALUE_TYPE_UNSIGNED); + value->value.u32 ^= amount.value.u32; + break; } } diff --git a/dep/rcheevos/src/rcheevos/condset.c b/dep/rcheevos/src/rcheevos/condset.c index c7c7e6dd6..e77a23581 100644 --- a/dep/rcheevos/src/rcheevos/condset.c +++ b/dep/rcheevos/src/rcheevos/condset.c @@ -84,6 +84,7 @@ rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, in switch ((*next)->oper) { case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: case RC_OPERATOR_DIV: case RC_OPERATOR_MULT: case RC_OPERATOR_NONE: @@ -209,8 +210,7 @@ static int rc_test_condset_internal(rc_condset_t* self, int processing_pause, rc case RC_CONDITION_SUB_SOURCE: rc_evaluate_condition_value(&value, condition, eval_state); - rc_typed_value_convert(&value, RC_VALUE_TYPE_SIGNED); - value.value.i32 = -value.value.i32; + rc_typed_value_negate(&value); rc_typed_value_add(&eval_state->add_value, &value); eval_state->add_address = 0; continue; diff --git a/dep/rcheevos/src/rcheevos/consoleinfo.c b/dep/rcheevos/src/rcheevos/consoleinfo.c index 9f2665d7e..0ef0089e8 100644 --- a/dep/rcheevos/src/rcheevos/consoleinfo.c +++ b/dep/rcheevos/src/rcheevos/consoleinfo.c @@ -39,6 +39,9 @@ const char* rc_console_name(int console_id) case RC_CONSOLE_ATARI_JAGUAR: return "Atari Jaguar"; + case RC_CONSOLE_ATARI_JAGUAR_CD: + return "Atari Jaguar CD"; + case RC_CONSOLE_ATARI_LYNX: return "Atari Lynx"; @@ -132,6 +135,9 @@ const char* rc_console_name(int console_id) case RC_CONSOLE_NINTENDO_DS: return "Nintendo DS"; + case RC_CONSOLE_NINTENDO_DSI: + return "Nintendo DSi"; + case RC_CONSOLE_NINTENDO_3DS: return "Nintendo 3DS"; @@ -156,6 +162,9 @@ const char* rc_console_name(int console_id) case RC_CONSOLE_PC_ENGINE: return "PC Engine"; + case RC_CONSOLE_PC_ENGINE_CD: + return "PC Engine CD"; + case RC_CONSOLE_PLAYSTATION: return "PlayStation"; @@ -198,9 +207,15 @@ const char* rc_console_name(int console_id) case RC_CONSOLE_THOMSONTO8: return "Thomson TO8"; + case RC_CONSOLE_TI83: + return "TI-83"; + case RC_CONSOLE_TIC80: return "TIC-80"; + case RC_CONSOLE_UZEBOX: + return "Uzebox"; + case RC_CONSOLE_VECTREX: return "Vectrex"; @@ -435,6 +450,13 @@ static const rc_memory_region_t _rc_memory_regions_gameboy_advance[] = { }; static const rc_memory_regions_t rc_memory_regions_gameboy_advance = { _rc_memory_regions_gameboy_advance, 2 }; +/* ===== GameCube ===== */ +/* https://wiibrew.org/wiki/Memory_map */ +static const rc_memory_region_t _rc_memory_regions_gamecube[] = { + { 0x00000000U, 0x017FFFFF, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_gamecube = { _rc_memory_regions_gamecube, 1 }; + /* ===== Game Gear ===== */ /* http://www.smspower.org/Development/MemoryMap */ static const rc_memory_region_t _rc_memory_regions_game_gear[] = { @@ -518,6 +540,15 @@ static const rc_memory_region_t _rc_memory_regions_megadrive[] = { }; static const rc_memory_regions_t rc_memory_regions_megadrive = { _rc_memory_regions_megadrive, 2 }; +/* ===== MegaDrive 32X (Genesis 32X) ===== */ +/* http://devster.monkeeh.com/sega/32xguide1.txt */ +static const rc_memory_region_t _rc_memory_regions_megadrive_32x[] = { + { 0x000000U, 0x00FFFFU, 0x00FF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Main MegaDrive RAM */ + { 0x010000U, 0x04FFFFU, 0x06000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "32X RAM"}, /* Additional 32X RAM */ + { 0x050000U, 0x05FFFFU, 0x00000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_megadrive_32x = { _rc_memory_regions_megadrive_32x, 3 }; + /* ===== MSX ===== */ /* https://www.msx.org/wiki/The_Memory */ /* MSX only has 64KB of addressable RAM, of which 32KB is reserved for the system/BIOS. @@ -540,6 +571,35 @@ static const rc_memory_region_t _rc_memory_regions_neo_geo_pocket[] = { }; static const rc_memory_regions_t rc_memory_regions_neo_geo_pocket = { _rc_memory_regions_neo_geo_pocket, 1 }; +/* ===== Neo Geo CD ===== */ +/* https://wiki.neogeodev.org/index.php?title=68k_memory_map */ +/* NeoCD exposes $000000-$1FFFFF as System RAM, but it seems like only the WORKRAM section is used. + * This is consistent with http://www.hardmvs.fr/manuals/NeoGeoProgrammersGuide.pdf (page25), which says: + * + * Furthermore, the NEO-GEO provides addresses 100000H-10FFFFH as a work area, out of which the + * addresses 10F300H-10FFFFH are reserved exclusively for use by the system program. Therefore, + * every game is to use addresses 100000H-10F2FFH. + * + * Also note that PRG files (game ROM) can be loaded anywhere else in the $000000-$1FFFFF range. + * AoF3 illustrates this pretty clearly: https://wiki.neogeodev.org/index.php?title=IPL_file + * + * PROG_CD.PRG,0,0 + * PROG_CDX.PRG,0,058000 + * CNV_NM.PRG,0,0C0000 + * FIX_DATA.PRG,0,0FD000 + * OBJACTLK.PRG,0,130000 + * SSEL_CNV.PRG,0,15A000 + * SSEL_BAK.PRG,0,16F000 + * HITMSG.PRG,0,170000 + * SSEL_SPR.PRG,0,19D000 + */ +static const rc_memory_region_t _rc_memory_regions_neo_geo_cd[] = { + { 0x000000U, 0x00F2FFU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* NOTE: some BIOS settings are exposed through the reserved RAM: https://wiki.neogeodev.org/index.php?title=68k_ASM_defines */ + { 0x00F300U, 0x00FFFFU, 0x0010F300U, RC_MEMORY_TYPE_SYSTEM_RAM, "Reserved RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_neo_geo_cd = { _rc_memory_regions_neo_geo_cd, 2 }; + /* ===== Nintendo Entertainment System ===== */ /* https://wiki.nesdev.com/w/index.php/CPU_memory_map */ static const rc_memory_region_t _rc_memory_regions_nes[] = { @@ -587,6 +647,13 @@ static const rc_memory_region_t _rc_memory_regions_nintendo_ds[] = { }; static const rc_memory_regions_t rc_memory_regions_nintendo_ds = { _rc_memory_regions_nintendo_ds, 1 }; +/* ===== Nintendo DSi ===== */ +/* https://problemkaputt.de/gbatek.htm#dsiiomap */ +static const rc_memory_region_t _rc_memory_regions_nintendo_dsi[] = { + { 0x000000U, 0xFFFFFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_nintendo_dsi = { _rc_memory_regions_nintendo_dsi, 1 }; + /* ===== Oric ===== */ static const rc_memory_region_t _rc_memory_regions_oric[] = { /* actual size depends on machine type - up to 64KB */ @@ -605,11 +672,18 @@ static const rc_memory_regions_t rc_memory_regions_pc8800 = { _rc_memory_regions /* http://www.archaicpixels.com/Memory_Map */ static const rc_memory_region_t _rc_memory_regions_pc_engine[] = { { 0x000000U, 0x001FFFU, 0x1F0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_pc_engine = { _rc_memory_regions_pc_engine, 1 }; + +/* ===== PC Engine CD===== */ +/* http://www.archaicpixels.com/Memory_Map */ +static const rc_memory_region_t _rc_memory_regions_pc_engine_cd[] = { + { 0x000000U, 0x001FFFU, 0x1F0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, { 0x002000U, 0x011FFFU, 0x100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "CD RAM" }, { 0x012000U, 0x041FFFU, 0x0D0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Super System Card RAM" }, { 0x042000U, 0x0427FFU, 0x1EE000U, RC_MEMORY_TYPE_SAVE_RAM, "CD Battery-backed RAM" } }; -static const rc_memory_regions_t rc_memory_regions_pc_engine = { _rc_memory_regions_pc_engine, 4 }; +static const rc_memory_regions_t rc_memory_regions_pc_engine_cd = { _rc_memory_regions_pc_engine_cd, 4 }; /* ===== PC-FX ===== */ /* http://daifukkat.su/pcfx/data/memmap.html */ @@ -711,6 +785,13 @@ static const rc_memory_region_t _rc_memory_regions_thomson_to8[] = { }; static const rc_memory_regions_t rc_memory_regions_thomson_to8 = { _rc_memory_regions_thomson_to8, 1 }; +/* ===== TI-83 ===== */ +/* https://tutorials.eeems.ca/ASMin28Days/lesson/day03.html#mem */ +static const rc_memory_region_t _rc_memory_regions_ti83[] = { + { 0x000000U, 0x007FFFU, 0x008000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_ti83 = { _rc_memory_regions_ti83, 1 }; + /* ===== TIC-80 ===== */ /* https://github.com/nesbox/TIC-80/wiki/RAM */ static const rc_memory_region_t _rc_memory_regions_tic80[] = { @@ -727,6 +808,13 @@ static const rc_memory_region_t _rc_memory_regions_tic80[] = { }; static const rc_memory_regions_t rc_memory_regions_tic80 = { _rc_memory_regions_tic80, 10 }; +/* ===== Uzebox ===== */ +/* https://uzebox.org/index.php */ +static const rc_memory_region_t _rc_memory_regions_uzebox[] = { + { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_uzebox = { _rc_memory_regions_uzebox, 1 }; + /* ===== Vectrex ===== */ /* https://roadsidethoughts.com/vectrex/vectrex-memory-map.htm */ static const rc_memory_region_t _rc_memory_regions_vectrex[] = { @@ -761,6 +849,14 @@ static const rc_memory_region_t _rc_memory_regions_wasm4[] = { }; static const rc_memory_regions_t rc_memory_regions_wasm4 = { _rc_memory_regions_wasm4, 1 }; +/* ===== Wii ===== */ +/* https://wiibrew.org/wiki/Memory_map */ +static const rc_memory_region_t _rc_memory_regions_wii[] = { + { 0x00000000U, 0x017FFFFF, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x01800000U, 0x057FFFFF, 0x90000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_wii = { _rc_memory_regions_wii, 2 }; + /* ===== WonderSwan ===== */ /* http://daifukkat.su/docs/wsman/#ovr_memmap */ static const rc_memory_region_t _rc_memory_regions_wonderswan[] = { @@ -810,6 +906,7 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) return &rc_memory_regions_atari7800; case RC_CONSOLE_ATARI_JAGUAR: + case RC_CONSOLE_ATARI_JAGUAR_CD: return &rc_memory_regions_atari_jaguar; case RC_CONSOLE_ATARI_LYNX: @@ -840,6 +937,9 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) case RC_CONSOLE_GAMEBOY_ADVANCE: return &rc_memory_regions_gameboy_advance; + case RC_CONSOLE_GAMECUBE: + return &rc_memory_regions_gamecube; + case RC_CONSOLE_GAME_GEAR: return &rc_memory_regions_game_gear; @@ -856,17 +956,20 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) return &rc_memory_regions_master_system; case RC_CONSOLE_MEGA_DRIVE: - case RC_CONSOLE_SEGA_32X: - /* NOTE: 32x adds an extra 512KB of memory (256KB RAM + 256KB VRAM) to the - * Genesis, but we currently don't support it. */ return &rc_memory_regions_megadrive; + case RC_CONSOLE_SEGA_32X: + return &rc_memory_regions_megadrive_32x; + case RC_CONSOLE_MSX: return &rc_memory_regions_msx; case RC_CONSOLE_NEOGEO_POCKET: return &rc_memory_regions_neo_geo_pocket; + case RC_CONSOLE_NEO_GEO_CD: + return &rc_memory_regions_neo_geo_cd; + case RC_CONSOLE_NINTENDO: return &rc_memory_regions_nes; @@ -876,6 +979,9 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) case RC_CONSOLE_NINTENDO_DS: return &rc_memory_regions_nintendo_ds; + case RC_CONSOLE_NINTENDO_DSI: + return &rc_memory_regions_nintendo_dsi; + case RC_CONSOLE_ORIC: return &rc_memory_regions_oric; @@ -885,6 +991,9 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) case RC_CONSOLE_PC_ENGINE: return &rc_memory_regions_pc_engine; + case RC_CONSOLE_PC_ENGINE_CD: + return &rc_memory_regions_pc_engine_cd; + case RC_CONSOLE_PCFX: return &rc_memory_regions_pcfx; @@ -921,9 +1030,15 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) case RC_CONSOLE_THOMSONTO8: return &rc_memory_regions_thomson_to8; + case RC_CONSOLE_TI83: + return &rc_memory_regions_ti83; + case RC_CONSOLE_TIC80: return &rc_memory_regions_tic80; + case RC_CONSOLE_UZEBOX: + return &rc_memory_regions_uzebox; + case RC_CONSOLE_VECTREX: return &rc_memory_regions_vectrex; @@ -933,6 +1048,9 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) case RC_CONSOLE_WASM4: return &rc_memory_regions_wasm4; + case RC_CONSOLE_WII: + return &rc_memory_regions_wii; + case RC_CONSOLE_WONDERSWAN: return &rc_memory_regions_wonderswan; diff --git a/dep/rcheevos/src/rcheevos/memref.c b/dep/rcheevos/src/rcheevos/memref.c index f4ea6ac28..fff2a6bca 100644 --- a/dep/rcheevos/src/rcheevos/memref.c +++ b/dep/rcheevos/src/rcheevos/memref.c @@ -95,6 +95,7 @@ int rc_parse_memref(const char** memaddr, char* size, unsigned* address) { switch (*aux++) { case 'f': case 'F': *size = RC_MEMSIZE_FLOAT; break; case 'm': case 'M': *size = RC_MEMSIZE_MBF32; break; + case 'l': case 'L': *size = RC_MEMSIZE_MBF32_LE; break; default: return RC_INVALID_FP_OPERAND; @@ -201,6 +202,21 @@ static void rc_transform_memref_mbf32(rc_typed_value_t* value) { value->type = RC_VALUE_TYPE_FLOAT; } +static void rc_transform_memref_mbf32_le(rc_typed_value_t* value) { + /* decodes a Microsoft Binary Format float */ + /* Locomotive BASIC (CPC) uses MBF40, but in little endian format */ + const unsigned mantissa = value->value.u32 & 0x007FFFFF; + const int exponent = (int)(value->value.u32 >> 24) - 129; + const int sign = (value->value.u32 & 0x00800000); + + if (mantissa == 0 && exponent == -129) + value->value.f32 = (sign) ? -0.0f : 0.0f; + else + value->value.f32 = rc_build_float(mantissa, exponent, sign); + + value->type = RC_VALUE_TYPE_FLOAT; +} + static const unsigned char rc_bits_set[16] = { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 }; void rc_transform_memref_value(rc_typed_value_t* value, char size) { @@ -293,6 +309,10 @@ void rc_transform_memref_value(rc_typed_value_t* value, char size) { rc_transform_memref_mbf32(value); break; + case RC_MEMSIZE_MBF32_LE: + rc_transform_memref_mbf32_le(value); + break; + default: break; } @@ -319,6 +339,7 @@ static const unsigned rc_memref_masks[] = { 0xffffffff, /* RC_MEMSIZE_32_BITS_BE */ 0xffffffff, /* RC_MEMSIZE_FLOAT */ 0xffffffff, /* RC_MEMSIZE_MBF32 */ + 0xffffffff, /* RC_MEMSIZE_MBF32_LE */ 0xffffffff /* RC_MEMSIZE_VARIABLE */ }; @@ -354,6 +375,7 @@ static const char rc_memref_shared_sizes[] = { RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_32_BITS_BE */ RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_FLOAT */ RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32 */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32_LE */ RC_MEMSIZE_32_BITS /* RC_MEMSIZE_VARIABLE */ }; @@ -365,7 +387,7 @@ char rc_memref_shared_size(char size) { return rc_memref_shared_sizes[index]; } -static unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* ud) { +unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* ud) { if (!peek) return 0; @@ -422,7 +444,7 @@ void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memref_t** memrefs) *memrefs = 0; } -static unsigned rc_get_memref_value_value(rc_memref_value_t* memref, int operand_type) { +static unsigned rc_get_memref_value_value(const rc_memref_value_t* memref, int operand_type) { switch (operand_type) { /* most common case explicitly first, even though it could be handled by default case. diff --git a/dep/rcheevos/src/rcheevos/operand.c b/dep/rcheevos/src/rcheevos/operand.c index 1501607ff..f8abbbd7b 100644 --- a/dep/rcheevos/src/rcheevos/operand.c +++ b/dep/rcheevos/src/rcheevos/operand.c @@ -59,6 +59,8 @@ static int rc_parse_operand_lua(rc_operand_t* self, const char** memaddr, rc_par self->value.luafunc = luaL_ref(parse->L, LUA_REGISTRYINDEX); } +#else + (void)parse; #endif /* RC_DISABLE_LUA */ self->type = RC_OPERAND_LUA; @@ -300,6 +302,7 @@ int rc_operand_is_float_memref(const rc_operand_t* self) { switch (self->size) { case RC_MEMSIZE_FLOAT: case RC_MEMSIZE_MBF32: + case RC_MEMSIZE_MBF32_LE: return 1; default: @@ -319,6 +322,13 @@ int rc_operand_is_memref(const rc_operand_t* self) { } } +int rc_operand_is_float(const rc_operand_t* self) { + if (self->type == RC_OPERAND_FP) + return 1; + + return rc_operand_is_float_memref(self); +} + unsigned rc_transform_operand_value(unsigned value, const rc_operand_t* self) { switch (self->type) { diff --git a/dep/rcheevos/src/rcheevos/rc_client.c b/dep/rcheevos/src/rcheevos/rc_client.c new file mode 100644 index 000000000..76b7bf127 --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_client.c @@ -0,0 +1,4669 @@ +#include "rc_client_internal.h" + +#include "rc_api_info.h" +#include "rc_api_runtime.h" +#include "rc_api_user.h" +#include "rc_consoles.h" +#include "rc_internal.h" +#include "rc_hash.h" + +#include "../rapi/rc_api_common.h" + +#include + +#define RC_CLIENT_UNKNOWN_GAME_ID (uint32_t)-1 +#define RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS (10 * 60) /* ten minutes */ + +/* clock_t can wrap. For most cases, we won't have to worry about it because it won't + * overflow until after over a month of runtime. But some cases can overflow in as short as + * 36 minutes. Use substraction as a secondary check to ensure an overflow hasn't occurred. */ +#define RC_CLIENT_CLOCK_IS_BEFORE(clk, cmp_clk) (clk < cmp_clk && (cmp_clk - clk) > 0) + +struct rc_client_async_handle_t { + uint8_t aborted; +}; + +typedef struct rc_client_generic_callback_data_t { + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + rc_client_async_handle_t async_handle; +} rc_client_generic_callback_data_t; + +typedef struct rc_client_pending_media_t +{ + const char* file_path; + uint8_t* data; + size_t data_size; + rc_client_callback_t callback; + void* callback_userdata; +} rc_client_pending_media_t; + +typedef struct rc_client_load_state_t +{ + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + + rc_client_game_info_t* game; + rc_client_subset_info_t* subset; + rc_client_game_hash_t* hash; + + rc_hash_iterator_t hash_iterator; + rc_client_pending_media_t* pending_media; + + uint32_t* hardcore_unlocks; + uint32_t* softcore_unlocks; + uint32_t num_hardcore_unlocks; + uint32_t num_softcore_unlocks; + + rc_client_async_handle_t async_handle; + + uint8_t progress; + uint8_t outstanding_requests; + uint8_t hash_console_id; +} rc_client_load_state_t; + +static void rc_client_begin_fetch_game_data(rc_client_load_state_t* callback_data); +static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game); +static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message); +static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, const char* hash, const char* file_path); +static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, clock_t now); +static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset); +static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game); +static void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard); +static void rc_client_reschedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* callback, clock_t when); + +/* ===== Construction/Destruction ===== */ + +static void rc_client_dummy_event_handler(const rc_client_event_t* event, rc_client_t* client) +{ +} + +rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function) +{ + rc_client_t* client = (rc_client_t*)calloc(1, sizeof(rc_client_t)); + if (!client) + return NULL; + + client->state.hardcore = 1; + + client->callbacks.read_memory = read_memory_function; + client->callbacks.server_call = server_call_function; + client->callbacks.event_handler = rc_client_dummy_event_handler; + rc_client_set_legacy_peek(client, RC_CLIENT_LEGACY_PEEK_AUTO); + + rc_mutex_init(&client->state.mutex); + + rc_buf_init(&client->state.buffer); + + return client; +} + +void rc_client_destroy(rc_client_t* client) +{ + if (!client) + return; + + rc_client_unload_game(client); + + rc_buf_destroy(&client->state.buffer); + + rc_mutex_destroy(&client->state.mutex); + + free(client); +} + +/* ===== Logging ===== */ + +static rc_client_t* g_hash_client = NULL; + +static void rc_client_log_hash_message(const char* message) { + rc_client_log_message(g_hash_client, message); +} + +void rc_client_log_message(const rc_client_t* client, const char* message) +{ + if (client->callbacks.log_call) + client->callbacks.log_call(message, client); +} + +static void rc_client_log_message_va(const rc_client_t* client, const char* format, va_list args) +{ + if (client->callbacks.log_call) { + char buffer[2048]; + +#ifdef __STDC_WANT_SECURE_LIB__ + vsprintf_s(buffer, sizeof(buffer), format, args); +#elif __STDC_VERSION__ >= 199901L /* vsnprintf requires c99 */ + vsnprintf(buffer, sizeof(buffer), format, args); +#else /* c89 doesn't have a size-limited vsprintf function - assume the buffer is large enough */ + vsprintf(buffer, format, args); +#endif + + client->callbacks.log_call(buffer, client); + } +} + +#ifdef RC_NO_VARIADIC_MACROS + +void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +#else + +void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...) +{ + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); +} + +#endif /* RC_NO_VARIADIC_MACROS */ + +void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback) +{ + client->callbacks.log_call = callback; + client->state.log_level = callback ? level : RC_CLIENT_LOG_LEVEL_NONE; +} + +/* ===== Common ===== */ + +static int rc_client_async_handle_aborted(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + int aborted; + + rc_mutex_lock(&client->state.mutex); + aborted = async_handle->aborted; + rc_mutex_unlock(&client->state.mutex); + + return aborted; +} + +void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + if (async_handle && client) { + rc_mutex_lock(&client->state.mutex); + async_handle->aborted = 1; + rc_mutex_unlock(&client->state.mutex); + } +} + +static const char* rc_client_server_error_message(int* result, int http_status_code, const rc_api_response_t* response) +{ + if (!response->succeeded) { + if (*result == RC_OK) { + *result = RC_API_FAILURE; + if (!response->error_message) + return "Unexpected API failure with no error message"; + } + + if (response->error_message) + return response->error_message; + } + + if (*result != RC_OK) + return rc_error_str(*result); + + return NULL; +} + +static void rc_client_raise_server_error_event(rc_client_t* client, const char* api, const char* error_message) +{ + rc_client_server_error_t server_error; + rc_client_event_t client_event; + + server_error.api = api; + server_error.error_message = error_message; + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_SERVER_ERROR; + client_event.server_error = &server_error; + + client->callbacks.event_handler(&client_event, client); +} + +static int rc_client_should_retry(const rc_api_server_response_t* server_response) +{ + switch (server_response->http_status_code) { + case 502: /* 502 Bad Gateway */ + /* nginx connection pool full. retry */ + return 1; + + case 503: /* 503 Service Temporarily Unavailable */ + /* site is in maintenance mode. retry */ + return 1; + + case 429: /* 429 Too Many Requests */ + /* too many unlocks occurred at the same time */ + return 1; + + default: + return 0; + } +} + +static int rc_client_get_image_url(char buffer[], size_t buffer_size, int image_type, const char* image_name) +{ + rc_api_fetch_image_request_t image_request; + rc_api_request_t request; + int result; + + if (!buffer) + return RC_INVALID_STATE; + + memset(&image_request, 0, sizeof(image_request)); + image_request.image_type = image_type; + image_request.image_name = image_name; + result = rc_api_init_fetch_image_request(&request, &image_request); + if (result == RC_OK) + snprintf(buffer, buffer_size, "%s", request.url); + + rc_api_destroy_request(&request); + return result; +} + +/* ===== User ===== */ + +static void rc_client_login_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_generic_callback_data_t* login_callback_data = (rc_client_generic_callback_data_t*)callback_data; + rc_client_t* client = login_callback_data->client; + rc_api_login_response_t login_response; + rc_client_load_state_t* load_state; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(client, &login_callback_data->async_handle)) { + RC_CLIENT_LOG_VERBOSE(client, "Login aborted"); + free(login_callback_data); + return; + } + + if (client->state.user == RC_CLIENT_USER_STATE_NONE) { + /* logout was called */ + if (login_callback_data->callback) + login_callback_data->callback(RC_ABORTED, "Login aborted", client, login_callback_data->callback_userdata); + + free(login_callback_data); + return; + } + + result = rc_api_process_login_server_response(&login_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &login_response.response); + if (error_message) { + rc_mutex_lock(&client->state.mutex); + client->state.user = RC_CLIENT_USER_STATE_NONE; + load_state = client->state.load; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_ERR_FORMATTED(client, "Login failed: %s", error_message); + if (login_callback_data->callback) + login_callback_data->callback(result, error_message, client, login_callback_data->callback_userdata); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_begin_fetch_game_data(load_state); + } + else { + client->user.username = rc_buf_strcpy(&client->state.buffer, login_response.username); + + if (strcmp(login_response.username, login_response.display_name) == 0) + client->user.display_name = client->user.username; + else + client->user.display_name = rc_buf_strcpy(&client->state.buffer, login_response.display_name); + + client->user.token = rc_buf_strcpy(&client->state.buffer, login_response.api_token); + client->user.score = login_response.score; + client->user.score_softcore = login_response.score_softcore; + client->user.num_unread_messages = login_response.num_unread_messages; + + rc_mutex_lock(&client->state.mutex); + client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; + load_state = client->state.load; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "%s logged in successfully", login_response.display_name); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_begin_fetch_game_data(load_state); + + if (login_callback_data->callback) + login_callback_data->callback(RC_OK, NULL, client, login_callback_data->callback_userdata); + } + + rc_api_destroy_login_response(&login_response); + free(login_callback_data); +} + +static rc_client_async_handle_t* rc_client_begin_login(rc_client_t* client, + const rc_api_login_request_t* login_request, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_generic_callback_data_t* callback_data; + rc_api_request_t request; + int result = rc_api_init_login_request(&request, login_request); + const char* error_message = rc_error_str(result); + + if (result == RC_OK) { + rc_mutex_lock(&client->state.mutex); + + if (client->state.user == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) { + error_message = "Login already in progress"; + result = RC_INVALID_STATE; + } + client->state.user = RC_CLIENT_USER_STATE_LOGIN_REQUESTED; + + rc_mutex_unlock(&client->state.mutex); + } + + if (result != RC_OK) { + callback(result, error_message, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_generic_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + + client->callbacks.server_call(&request, rc_client_login_callback, callback_data, client); + rc_api_destroy_request(&request); + + return &callback_data->async_handle; +} + +rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client, + const char* username, const char* password, rc_client_callback_t callback, void* callback_userdata) +{ + rc_api_login_request_t login_request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!username || !username[0]) { + callback(RC_INVALID_STATE, "username is required", client, callback_userdata); + return NULL; + } + + if (!password || !password[0]) { + callback(RC_INVALID_STATE, "password is required", client, callback_userdata); + return NULL; + } + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = username; + login_request.password = password; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with password)", username); + return rc_client_begin_login(client, &login_request, callback, callback_userdata); +} + +rc_client_async_handle_t* rc_client_begin_login_with_token(rc_client_t* client, + const char* username, const char* token, rc_client_callback_t callback, void* callback_userdata) +{ + rc_api_login_request_t login_request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!username || !username[0]) { + callback(RC_INVALID_STATE, "username is required", client, callback_userdata); + return NULL; + } + + if (!token || !token[0]) { + callback(RC_INVALID_STATE, "token is required", client, callback_userdata); + return NULL; + } + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = username; + login_request.api_token = token; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with token)", username); + return rc_client_begin_login(client, &login_request, callback, callback_userdata); +} + +void rc_client_logout(rc_client_t* client) +{ + rc_client_load_state_t* load_state; + + if (!client) + return; + + switch (client->state.user) { + case RC_CLIENT_USER_STATE_LOGGED_IN: + RC_CLIENT_LOG_INFO_FORMATTED(client, "Logging %s out", client->user.display_name); + break; + + case RC_CLIENT_USER_STATE_LOGIN_REQUESTED: + RC_CLIENT_LOG_INFO(client, "Aborting login"); + break; + } + + rc_mutex_lock(&client->state.mutex); + + client->state.user = RC_CLIENT_USER_STATE_NONE; + memset(&client->user, 0, sizeof(client->user)); + + load_state = client->state.load; + + rc_mutex_unlock(&client->state.mutex); + + rc_client_unload_game(client); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_load_error(load_state, RC_ABORTED, "Login aborted"); +} + +const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client) +{ + return (client && client->state.user == RC_CLIENT_USER_STATE_LOGGED_IN) ? &client->user : NULL; +} + +int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size) +{ + if (!user) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, user->display_name); +} + +static void rc_client_subset_get_user_game_summary(const rc_client_subset_info_t* subset, + rc_client_user_game_summary_t* summary, const uint8_t unlock_bit) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + switch (achievement->public_.category) { + case RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE: + ++summary->num_core_achievements; + summary->points_core += achievement->public_.points; + + if (achievement->public_.unlocked & unlock_bit) { + ++summary->num_unlocked_achievements; + summary->points_unlocked += achievement->public_.points; + } + else if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) { + ++summary->num_unsupported_achievements; + } + + break; + + case RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL: + ++summary->num_unofficial_achievements; + break; + + default: + continue; + } + } +} + +void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary) +{ + const uint8_t unlock_bit = (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + if (!summary) + return; + + memset(summary, 0, sizeof(*summary)); + if (!client || !client->game) + return; + + rc_mutex_lock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ + + rc_client_subset_get_user_game_summary(client->game->subsets, summary, unlock_bit); + + rc_mutex_unlock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ +} + +/* ===== Game ===== */ + +static void rc_client_free_game(rc_client_game_info_t* game) +{ + rc_runtime_destroy(&game->runtime); + + rc_buf_destroy(&game->buffer); + + free(game); +} + +static void rc_client_free_load_state(rc_client_load_state_t* load_state) +{ + if (load_state->game) + rc_client_free_game(load_state->game); + + if (load_state->hardcore_unlocks) + free(load_state->hardcore_unlocks); + if (load_state->softcore_unlocks) + free(load_state->softcore_unlocks); + + free(load_state); +} + +static void rc_client_begin_load_state(rc_client_load_state_t* load_state, uint8_t state, uint8_t num_requests) +{ + rc_mutex_lock(&load_state->client->state.mutex); + + load_state->progress = state; + load_state->outstanding_requests += num_requests; + + rc_mutex_unlock(&load_state->client->state.mutex); +} + +static int rc_client_end_load_state(rc_client_load_state_t* load_state) +{ + int remaining_requests = 0; + int aborted = 0; + + rc_mutex_lock(&load_state->client->state.mutex); + + if (load_state->outstanding_requests > 0) + --load_state->outstanding_requests; + remaining_requests = load_state->outstanding_requests; + + if (load_state->client->state.load != load_state) + aborted = 1; + + rc_mutex_unlock(&load_state->client->state.mutex); + + if (aborted) { + /* we can't actually free the load_state itself if there are any outstanding requests + * or their callbacks will try to use the free'd memory. As they call end_load_state, + * the outstanding_requests count will reach zero and the memory will be free'd then. */ + if (remaining_requests == 0) { + /* if one of the callbacks called rc_client_load_error, progress will be set to + * RC_CLIENT_LOAD_STATE_UNKNOWN. There's no need to call the callback with RC_ABORTED + * in that case, as it will have already been called with something more appropriate. */ + if (load_state->progress != RC_CLIENT_LOAD_STATE_UNKNOWN_GAME && load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", load_state->client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + } + + return -1; + } + + return remaining_requests; +} + +static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message) +{ + int remaining_requests = 0; + + rc_mutex_lock(&load_state->client->state.mutex); + + load_state->progress = RC_CLIENT_LOAD_STATE_UNKNOWN_GAME; + if (load_state->client->state.load == load_state) + load_state->client->state.load = NULL; + + remaining_requests = load_state->outstanding_requests; + + rc_mutex_unlock(&load_state->client->state.mutex); + + if (load_state->callback) + load_state->callback(result, error_message, load_state->client, load_state->callback_userdata); + + /* we can't actually free the load_state itself if there are any outstanding requests + * or their callbacks will try to use the free'd memory. as they call end_load_state, + * the outstanding_requests count will reach zero and the memory will be free'd then. */ + if (remaining_requests == 0) + rc_client_free_load_state(load_state); +} + +static void rc_client_load_aborted(rc_client_load_state_t* load_state) +{ + /* prevent callback from being called when manually aborted */ + load_state->callback = NULL; + + /* mark the game as no longer being loaded */ + rc_client_load_error(load_state, RC_ABORTED, NULL); + + /* decrement the async counter and potentially free the load_state object */ + rc_client_end_load_state(load_state); +} + +static void rc_client_invalidate_memref_achievements(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref) +{ + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_DISABLED) + continue; + + if (rc_trigger_contains_memref(achievement->trigger, memref)) { + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled achievement %u. Invalid address %06X", achievement->public_.id, memref->address); + } + } + } +} + +static void rc_client_invalidate_memref_leaderboards(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref) +{ + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED) + continue; + + if (rc_trigger_contains_memref(&leaderboard->lboard->start, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_trigger_contains_memref(&leaderboard->lboard->cancel, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_trigger_contains_memref(&leaderboard->lboard->submit, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_value_contains_memref(&leaderboard->lboard->value, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else + continue; + + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled leaderboard %u. Invalid address %06X", leaderboard->public_.id, memref->address); + } + } +} + +static void rc_client_validate_addresses(rc_client_game_info_t* game, rc_client_t* client) +{ + const rc_memory_regions_t* regions = rc_console_memory_regions(game->public_.console_id); + const uint32_t max_address = (regions && regions->num_regions > 0) ? + regions->region[regions->num_regions - 1].end_address : 0xFFFFFFFF; + uint8_t buffer[8]; + uint32_t total_count = 0; + uint32_t invalid_count = 0; + + rc_memref_t** last_memref = &game->runtime.memrefs; + rc_memref_t* memref = game->runtime.memrefs; + for (; memref; memref = memref->next) { + if (!memref->value.is_indirect) { + total_count++; + + if (memref->address > max_address || + client->callbacks.read_memory(memref->address, buffer, 1, client) == 0) { + /* invalid address, remove from chain so we don't have to evaluate it in the future. + * it's still there, so anything referencing it will always fetch 0. */ + *last_memref = memref->next; + + rc_client_invalidate_memref_achievements(game, client, memref); + rc_client_invalidate_memref_leaderboards(game, client, memref); + + invalid_count++; + continue; + } + } + + last_memref = &memref->next; + } + + game->max_valid_address = max_address; + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "%u/%u memory addresses valid", total_count - invalid_count, total_count); +} + +static void rc_client_update_legacy_runtime_achievements(rc_client_game_info_t* game, uint32_t active_count) +{ + if (active_count > 0) { + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* stop; + rc_runtime_trigger_t* trigger; + + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + + if (active_count <= game->runtime.trigger_capacity) { + if (active_count != 0) + memset(game->runtime.triggers, 0, active_count * sizeof(rc_runtime_trigger_t)); + } + else { + if (game->runtime.triggers) + free(game->runtime.triggers); + + game->runtime.trigger_capacity = active_count; + game->runtime.triggers = (rc_runtime_trigger_t*)calloc(1, active_count * sizeof(rc_runtime_trigger_t)); + if (!game->runtime.triggers) { + /* Unexpected, no callback available, just fail */ + break; + } + } + + trigger = game->runtime.triggers; + achievement = subset->achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { + trigger->id = achievement->public_.id; + memcpy(trigger->md5, achievement->md5, 16); + trigger->trigger = achievement->trigger; + ++trigger; + } + } + } + } + + game->runtime.trigger_count = active_count; +} + +static uint32_t rc_client_subset_count_active_achievements(const rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + uint32_t active_count = 0; + + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + ++active_count; + } + + return active_count; +} + +static void rc_client_update_active_achievements(rc_client_game_info_t* game) +{ + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + active_count += rc_client_subset_count_active_achievements(subset); + } + + rc_client_update_legacy_runtime_achievements(game, active_count); +} + +static uint32_t rc_client_subset_toggle_hardcore_achievements(rc_client_subset_info_t* subset, rc_client_t* client, uint8_t active_bit) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + uint32_t active_count = 0; + + for (; achievement < stop; ++achievement) { + if ((achievement->public_.unlocked & active_bit) == 0) { + switch (achievement->public_.state) { + case RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED: + case RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE: + rc_reset_trigger(achievement->trigger); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + ++active_count; + break; + + case RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE: + ++active_count; + break; + } + } + else if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE || + achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE) { + + /* if it's active despite being unlocked, and we're in encore mode, leave it active */ + if (client->state.encore_mode) { + ++active_count; + continue; + } + + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + achievement->public_.unlock_time = (active_bit == RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) ? + achievement->unlock_time_hardcore : achievement->unlock_time_softcore; + + if (achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) { + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + client_event.achievement = &achievement->public_; + client->callbacks.event_handler(&client_event, client); + } + } + } + + return active_count; +} + +static void rc_client_toggle_hardcore_achievements(rc_client_game_info_t* game, rc_client_t* client, uint8_t active_bit) +{ + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + active_count += rc_client_subset_toggle_hardcore_achievements(subset, client, active_bit); + } + + rc_client_update_legacy_runtime_achievements(game, active_count); +} + +static void rc_client_activate_achievements(rc_client_game_info_t* game, rc_client_t* client) +{ + const uint8_t active_bit = (client->state.encore_mode) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE : (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_client_toggle_hardcore_achievements(game, client, active_bit); +} + +static void rc_client_activate_leaderboards(rc_client_game_info_t* game, rc_client_t* client) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + unsigned active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + if (client->state.hardcore) { + rc_reset_lboard(leaderboard->lboard); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + ++active_count; + } + break; + + default: + if (client->state.hardcore) + ++active_count; + else + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + break; + } + } + } + + if (active_count > 0) { + rc_runtime_lboard_t* lboard; + + if (active_count <= game->runtime.lboard_capacity) { + if (active_count != 0) + memset(game->runtime.lboards, 0, active_count * sizeof(rc_runtime_lboard_t)); + } + else { + if (game->runtime.lboards) + free(game->runtime.lboards); + + game->runtime.lboard_capacity = active_count; + game->runtime.lboards = (rc_runtime_lboard_t*)calloc(1, active_count * sizeof(rc_runtime_lboard_t)); + } + + lboard = game->runtime.lboards; + + subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_ACTIVE || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) { + lboard->id = leaderboard->public_.id; + memcpy(lboard->md5, leaderboard->md5, 16); + lboard->lboard = leaderboard->lboard; + ++lboard; + } + } + } + } + + game->runtime.lboard_count = active_count; +} + +static void rc_client_deactivate_leaderboards(rc_client_game_info_t* game, rc_client_t* client) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + rc_client_release_leaderboard_tracker(client->game, leaderboard); + /* fallthrough to default */ + default: + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + break; + } + } + } + + game->runtime.lboard_count = 0; +} + +static void rc_client_apply_unlocks(rc_client_subset_info_t* subset, uint32_t* unlocks, uint32_t num_unlocks, uint8_t mode) +{ + rc_client_achievement_info_t* start = subset->achievements; + rc_client_achievement_info_t* stop = start + subset->public_.num_achievements; + rc_client_achievement_info_t* scan; + unsigned i; + + for (i = 0; i < num_unlocks; ++i) { + uint32_t id = unlocks[i]; + for (scan = start; scan < stop; ++scan) { + if (scan->public_.id == id) { + scan->public_.unlocked |= mode; + + if (scan == start) + ++start; + else if (scan + 1 == stop) + --stop; + break; + } + } + } +} + +static void rc_client_activate_game(rc_client_load_state_t* load_state) +{ + rc_client_t* client = load_state->client; + + rc_mutex_lock(&client->state.mutex); + load_state->progress = (client->state.load == load_state) ? + RC_CLIENT_LOAD_STATE_DONE : RC_CLIENT_LOAD_STATE_UNKNOWN_GAME; + client->state.load = NULL; + rc_mutex_unlock(&client->state.mutex); + + if (load_state->progress != RC_CLIENT_LOAD_STATE_DONE) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else if ((!load_state->softcore_unlocks || !load_state->hardcore_unlocks) && + client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) { + /* unlocks not available - assume malloc failed */ + if (load_state->callback) + load_state->callback(RC_INVALID_STATE, "Unlock arrays were not allocated", client, load_state->callback_userdata); + } + else { + if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) { + rc_client_apply_unlocks(load_state->subset, load_state->softcore_unlocks, + load_state->num_softcore_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + rc_client_apply_unlocks(load_state->subset, load_state->hardcore_unlocks, + load_state->num_hardcore_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + } + + rc_mutex_lock(&client->state.mutex); + if (client->state.load == NULL) + client->game = load_state->game; + rc_mutex_unlock(&client->state.mutex); + + if (client->game != load_state->game) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else { + /* if a change media request is pending, kick it off */ + rc_client_pending_media_t* pending_media; + + rc_mutex_lock(&load_state->client->state.mutex); + pending_media = load_state->pending_media; + load_state->pending_media = NULL; + rc_mutex_unlock(&load_state->client->state.mutex); + + if (pending_media) { + rc_client_begin_change_media(client, pending_media->file_path, + pending_media->data, pending_media->data_size, pending_media->callback, pending_media->callback_userdata); + if (pending_media->data) + free(pending_media->data); + free((void*)pending_media->file_path); + free(pending_media); + } + + /* client->game must be set before calling this function so it can query the console_id */ + rc_client_validate_addresses(load_state->game, client); + + rc_client_activate_achievements(load_state->game, client); + rc_client_activate_leaderboards(load_state->game, client); + + if (load_state->hash->hash[0] != '[') { + if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_LOCKED) { + /* schedule the periodic ping */ + rc_client_scheduled_callback_data_t* callback_data = rc_buf_alloc(&load_state->game->buffer, sizeof(rc_client_scheduled_callback_data_t)); + memset(callback_data, 0, sizeof(*callback_data)); + callback_data->callback = rc_client_ping; + callback_data->related_id = load_state->game->public_.id; + callback_data->when = clock() + 30 * CLOCKS_PER_SEC; + rc_client_schedule_callback(client, callback_data); + } + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Game %u loaded, hardcode %s%s", load_state->game->public_.id, + client->state.hardcore ? "enabled" : "disabled", + (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) ? ", spectating" : ""); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Subset %u loaded", load_state->subset->public_.id); + } + + if (load_state->callback) + load_state->callback(RC_OK, NULL, client, load_state->callback_userdata); + + /* detach the game object so it doesn't get freed by free_load_state */ + load_state->game = NULL; + } + } + + rc_client_free_load_state(load_state); +} + +static void rc_client_start_session_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_api_start_session_response_t start_session_response; + int outstanding_requests; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(load_state->client, &load_state->async_handle)) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while starting session"); + return; + } + + result = rc_api_process_start_session_server_response(&start_session_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &start_session_response.response); + outstanding_requests = rc_client_end_load_state(load_state); + + if (error_message) { + rc_client_load_error(callback_data, result, error_message); + } + else if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + if (outstanding_requests == 0) + rc_client_activate_game(load_state); + } + + rc_api_destroy_start_session_response(&start_session_response); +} + +static void rc_client_unlocks_callback(const rc_api_server_response_t* server_response, void* callback_data, int mode) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; + int outstanding_requests; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(load_state->client, &load_state->async_handle)) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while fetching unlocks"); + return; + } + + result = rc_api_process_fetch_user_unlocks_server_response(&fetch_user_unlocks_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_user_unlocks_response.response); + outstanding_requests = rc_client_end_load_state(load_state); + + if (error_message) { + rc_client_load_error(callback_data, result, error_message); + } + else if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + if (mode == RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) { + const size_t array_size = fetch_user_unlocks_response.num_achievement_ids * sizeof(uint32_t); + load_state->num_hardcore_unlocks = fetch_user_unlocks_response.num_achievement_ids; + load_state->hardcore_unlocks = (uint32_t*)malloc(array_size); + if (load_state->hardcore_unlocks) + memcpy(load_state->hardcore_unlocks, fetch_user_unlocks_response.achievement_ids, array_size); + } + else { + const size_t array_size = fetch_user_unlocks_response.num_achievement_ids * sizeof(uint32_t); + load_state->num_softcore_unlocks = fetch_user_unlocks_response.num_achievement_ids; + load_state->softcore_unlocks = (uint32_t*)malloc(array_size); + if (load_state->softcore_unlocks) + memcpy(load_state->softcore_unlocks, fetch_user_unlocks_response.achievement_ids, array_size); + } + + if (outstanding_requests == 0) + rc_client_activate_game(load_state); + } + + rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); +} + +static void rc_client_hardcore_unlocks_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_unlocks_callback(server_response, callback_data, RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE); +} + +static void rc_client_softcore_unlocks_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_unlocks_callback(server_response, callback_data, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); +} + +static void rc_client_begin_start_session(rc_client_load_state_t* load_state) +{ + rc_api_start_session_request_t start_session_params; + rc_api_fetch_user_unlocks_request_t unlock_params; + rc_client_t* client = load_state->client; + rc_api_request_t start_session_request; + rc_api_request_t hardcore_unlock_request; + rc_api_request_t softcore_unlock_request; + int result; + + memset(&start_session_params, 0, sizeof(start_session_params)); + start_session_params.username = client->user.username; + start_session_params.api_token = client->user.token; + start_session_params.game_id = load_state->hash->game_id; + + result = rc_api_init_start_session_request(&start_session_request, &start_session_params); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + } + else { + memset(&unlock_params, 0, sizeof(unlock_params)); + unlock_params.username = client->user.username; + unlock_params.api_token = client->user.token; + unlock_params.game_id = load_state->hash->game_id; + unlock_params.hardcore = 1; + + result = rc_api_init_fetch_user_unlocks_request(&hardcore_unlock_request, &unlock_params); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + } + else { + unlock_params.hardcore = 0; + + result = rc_api_init_fetch_user_unlocks_request(&softcore_unlock_request, &unlock_params); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + } + else { + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_STARTING_SESSION, 3); + + /* TODO: create single server request to do all three of these */ + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Starting session for game %u", start_session_params.game_id); + client->callbacks.server_call(&start_session_request, rc_client_start_session_callback, load_state, client); + client->callbacks.server_call(&hardcore_unlock_request, rc_client_hardcore_unlocks_callback, load_state, client); + client->callbacks.server_call(&softcore_unlock_request, rc_client_softcore_unlocks_callback, load_state, client); + + rc_api_destroy_request(&softcore_unlock_request); + } + + rc_api_destroy_request(&hardcore_unlock_request); + } + + rc_api_destroy_request(&start_session_request); + } +} + +static void rc_client_copy_achievements(rc_client_load_state_t* load_state, + rc_client_subset_info_t* subset, + const rc_api_achievement_definition_t* achievement_definitions, uint32_t num_achievements) +{ + const rc_api_achievement_definition_t* read; + const rc_api_achievement_definition_t* stop; + rc_client_achievement_info_t* achievements; + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* scan; + rc_api_buffer_t* buffer; + rc_parse_state_t parse; + const char* memaddr; + size_t size; + int trigger_size; + + subset->achievements = NULL; + subset->public_.num_achievements = num_achievements; + + if (num_achievements == 0) + return; + + stop = achievement_definitions + num_achievements; + + /* if not testing unofficial, filter them out */ + if (!load_state->client->state.unofficial_enabled) { + for (read = achievement_definitions; read < stop; ++read) { + if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) + --num_achievements; + } + + subset->public_.num_achievements = num_achievements; + + if (num_achievements == 0) + return; + } + + /* preallocate space for achievements */ + size = 24 /* assume average title length of 24 */ + + 48 /* assume average description length of 48 */ + + sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2 /* trigger container */ + + sizeof(rc_condition_t) * 8 /* assume average trigger length of 8 conditions */ + + sizeof(rc_client_achievement_info_t); + rc_buf_reserve(&load_state->game->buffer, size * num_achievements); + + /* allocate the achievement array */ + size = sizeof(rc_client_achievement_info_t) * num_achievements; + buffer = &load_state->game->buffer; + achievement = achievements = rc_buf_alloc(buffer, size); + memset(achievements, 0, size); + + /* copy the achievement data */ + for (read = achievement_definitions; read < stop; ++read) { + if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE && !load_state->client->state.unofficial_enabled) + continue; + + achievement->public_.title = rc_buf_strcpy(buffer, read->title); + achievement->public_.description = rc_buf_strcpy(buffer, read->description); + snprintf(achievement->public_.badge_name, sizeof(achievement->public_.badge_name), "%s", read->badge_name); + achievement->public_.id = read->id; + achievement->public_.points = read->points; + achievement->public_.category = (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) ? + RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; + + memaddr = read->definition; + rc_runtime_checksum(memaddr, achievement->md5); + + trigger_size = rc_trigger_size(memaddr); + if (trigger_size < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", trigger_size, read->id); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + } + else { + /* populate the item, using the communal memrefs pool */ + rc_init_parse_state(&parse, rc_buf_reserve(buffer, trigger_size), NULL, 0); + parse.first_memref = &load_state->game->runtime.memrefs; + parse.variables = &load_state->game->runtime.variables; + achievement->trigger = RC_ALLOC(rc_trigger_t, &parse); + rc_parse_trigger_internal(achievement->trigger, &memaddr, &parse); + + if (parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", parse.offset, read->id); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + } + else { + rc_buf_consume(buffer, parse.buffer, (char*)parse.buffer + parse.offset); + achievement->trigger->memrefs = NULL; /* memrefs managed by runtime */ + } + + rc_destroy_parse_state(&parse); + } + + achievement->created_time = read->created; + achievement->updated_time = read->updated; + + scan = achievement; + while (scan > achievements) { + --scan; + if (strcmp(scan->author, read->author) == 0) { + achievement->author = scan->author; + break; + } + } + if (!achievement->author) + achievement->author = rc_buf_strcpy(buffer, read->author); + + ++achievement; + } + + subset->achievements = achievements; +} + +static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state, + rc_client_subset_info_t* subset, + const rc_api_leaderboard_definition_t* leaderboard_definitions, uint32_t num_leaderboards) +{ + const rc_api_leaderboard_definition_t* read; + const rc_api_leaderboard_definition_t* stop; + rc_client_leaderboard_info_t* leaderboards; + rc_client_leaderboard_info_t* leaderboard; + rc_api_buffer_t* buffer; + rc_parse_state_t parse; + const char* memaddr; + const char* ptr; + size_t size; + int lboard_size; + + subset->leaderboards = NULL; + subset->public_.num_leaderboards = num_leaderboards; + + if (num_leaderboards == 0) + return; + + /* preallocate space for achievements */ + size = 24 /* assume average title length of 24 */ + + 48 /* assume average description length of 48 */ + + sizeof(rc_lboard_t) /* lboard container */ + + (sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2) * 3 /* start/submit/cancel */ + + (sizeof(rc_value_t) + sizeof(rc_condset_t)) /* value */ + + sizeof(rc_condition_t) * 4 * 4 /* assume average of 4 conditions in each start/submit/cancel/value */ + + sizeof(rc_client_leaderboard_info_t); + rc_buf_reserve(&load_state->game->buffer, size * num_leaderboards); + + /* allocate the achievement array */ + size = sizeof(rc_client_leaderboard_info_t) * num_leaderboards; + buffer = &load_state->game->buffer; + leaderboard = leaderboards = rc_buf_alloc(buffer, size); + memset(leaderboards, 0, size); + + /* copy the achievement data */ + read = leaderboard_definitions; + stop = read + num_leaderboards; + do { + leaderboard->public_.title = rc_buf_strcpy(buffer, read->title); + leaderboard->public_.description = rc_buf_strcpy(buffer, read->description); + leaderboard->public_.id = read->id; + leaderboard->public_.lower_is_better = read->lower_is_better; + leaderboard->format = (uint8_t)read->format; + leaderboard->hidden = (uint8_t)read->hidden; + + memaddr = read->definition; + rc_runtime_checksum(memaddr, leaderboard->md5); + + ptr = strstr(memaddr, "VAL:"); + if (ptr != NULL) { + /* calculate the DJB2 hash of the VAL portion of the string*/ + uint32_t hash = 5381; + ptr += 4; /* skip 'VAL:' */ + while (*ptr && (ptr[0] != ':' || ptr[1] != ':')) + hash = (hash << 5) + hash + *ptr++; + leaderboard->value_djb2 = hash; + } + + lboard_size = rc_lboard_size(memaddr); + if (lboard_size < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", lboard_size, read->id); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + } + else { + /* populate the item, using the communal memrefs pool */ + rc_init_parse_state(&parse, rc_buf_reserve(buffer, lboard_size), NULL, 0); + parse.first_memref = &load_state->game->runtime.memrefs; + parse.variables = &load_state->game->runtime.variables; + leaderboard->lboard = RC_ALLOC(rc_lboard_t, &parse); + rc_parse_lboard_internal(leaderboard->lboard, memaddr, &parse); + + if (parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", parse.offset, read->id); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + } + else { + rc_buf_consume(buffer, parse.buffer, (char*)parse.buffer + parse.offset); + leaderboard->lboard->memrefs = NULL; /* memrefs managed by runtime */ + } + + rc_destroy_parse_state(&parse); + } + + ++leaderboard; + ++read; + } while (read < stop); + + subset->leaderboards = leaderboards; +} + +static const char* rc_client_subset_extract_title(rc_client_game_info_t* game, const char* title) +{ + const char* subset_prefix = strstr(title, "[Subset - "); + if (subset_prefix) { + const char* start = subset_prefix + 10; + const char* stop = strstr(start, "]"); + const size_t len = stop - start; + char* result = (char*)rc_buf_alloc(&game->buffer, len + 1); + + memcpy(result, start, len); + result[len] = '\0'; + return result; + } + + return NULL; +} + +static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_api_fetch_game_data_response_t fetch_game_data_response; + int outstanding_requests; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(load_state->client, &load_state->async_handle)) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while fetching game data"); + return; + } + + result = rc_api_process_fetch_game_data_server_response(&fetch_game_data_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_game_data_response.response); + + outstanding_requests = rc_client_end_load_state(load_state); + + if (error_message) { + rc_client_load_error(load_state, result, error_message); + } + else if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + rc_client_subset_info_t* subset; + + subset = (rc_client_subset_info_t*)rc_buf_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t)); + memset(subset, 0, sizeof(*subset)); + subset->public_.id = fetch_game_data_response.id; + subset->active = 1; + snprintf(subset->public_.badge_name, sizeof(subset->public_.badge_name), "%s", fetch_game_data_response.image_name); + load_state->subset = subset; + + if (load_state->game->public_.console_id != RC_CONSOLE_UNKNOWN && + fetch_game_data_response.console_id != load_state->game->public_.console_id) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Data for game %u is for console %u, expecting console %u", + fetch_game_data_response.id, fetch_game_data_response.console_id, load_state->game->public_.console_id); + } + + /* kick off the start session request while we process the game data */ + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_STARTING_SESSION, 1); + if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + /* we can't unlock achievements without a session, lock spectator mode for the game */ + load_state->client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_LOCKED; + } + else { + rc_client_begin_start_session(load_state); + } + + /* process the game data */ + rc_client_copy_achievements(load_state, subset, + fetch_game_data_response.achievements, fetch_game_data_response.num_achievements); + rc_client_copy_leaderboards(load_state, subset, + fetch_game_data_response.leaderboards, fetch_game_data_response.num_leaderboards); + + if (!load_state->game->subsets) { + /* core set */ + rc_mutex_lock(&load_state->client->state.mutex); + load_state->game->public_.title = rc_buf_strcpy(&load_state->game->buffer, fetch_game_data_response.title); + load_state->game->subsets = subset; + load_state->game->public_.badge_name = subset->public_.badge_name; + load_state->game->public_.console_id = fetch_game_data_response.console_id; + rc_mutex_unlock(&load_state->client->state.mutex); + + subset->public_.title = load_state->game->public_.title; + + if (fetch_game_data_response.rich_presence_script && fetch_game_data_response.rich_presence_script[0]) { + result = rc_runtime_activate_richpresence(&load_state->game->runtime, fetch_game_data_response.rich_presence_script, NULL, 0); + if (result != RC_OK) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing rich presence", result); + } + } + } + else { + rc_client_subset_info_t* scan; + + /* subset - extract subset title */ + subset->public_.title = rc_client_subset_extract_title(load_state->game, fetch_game_data_response.title); + if (!subset->public_.title) { + const char* core_subset_title = rc_client_subset_extract_title(load_state->game, load_state->game->public_.title); + if (core_subset_title) { + rc_client_subset_info_t* scan = load_state->game->subsets; + for (; scan; scan = scan->next) { + if (scan->public_.title == load_state->game->public_.title) { + scan->public_.title = core_subset_title; + break; + } + } + } + + subset->public_.title = rc_buf_strcpy(&load_state->game->buffer, fetch_game_data_response.title); + } + + /* append to subset list */ + scan = load_state->game->subsets; + while (scan->next) + scan = scan->next; + scan->next = subset; + } + + outstanding_requests = rc_client_end_load_state(load_state); + if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + if (outstanding_requests == 0) + rc_client_activate_game(load_state); + } + } + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) +{ + rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_client_t* client = load_state->client; + rc_api_request_t request; + int result; + + if (load_state->hash->game_id == 0) { + char hash[33]; + + if (rc_hash_iterate(hash, &load_state->hash_iterator)) { + /* found another hash to try */ + load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1]; + rc_client_load_game(load_state, hash, NULL); + return; + } + + if (load_state->game->media_hash && + load_state->game->media_hash->game_hash && + load_state->game->media_hash->game_hash->next) { + /* multiple hashes were tried, create a CSV */ + struct rc_client_game_hash_t* game_hash = load_state->game->media_hash->game_hash; + int count = 1; + char* ptr; + size_t size; + + size = strlen(game_hash->hash) + 1; + while (game_hash->next) { + game_hash = game_hash->next; + size += strlen(game_hash->hash) + 1; + count++; + } + + ptr = (char*)rc_buf_alloc(&load_state->game->buffer, size); + ptr += size - 1; + *ptr = '\0'; + game_hash = load_state->game->media_hash->game_hash; + do { + const size_t hash_len = strlen(game_hash->hash); + ptr -= hash_len; + memcpy(ptr, game_hash->hash, hash_len); + + game_hash = game_hash->next; + if (!game_hash) + break; + + ptr--; + *ptr = ','; + } while (1); + + load_state->game->public_.hash = ptr; + load_state->game->public_.console_id = RC_CONSOLE_UNKNOWN; + } else { + /* only a single hash was tried, capture it */ + load_state->game->public_.console_id = load_state->hash_console_id; + load_state->game->public_.hash = load_state->hash->hash; + } + + load_state->game->public_.title = "Unknown Game"; + load_state->game->public_.badge_name = ""; + client->game = load_state->game; + load_state->game = NULL; + + rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game"); + return; + } + + if (load_state->hash->hash[0] != '[') { + load_state->game->public_.id = load_state->hash->game_id; + load_state->game->public_.hash = load_state->hash->hash; + } + + /* done with the hashing code, release the global pointer */ + g_hash_client = NULL; + + rc_mutex_lock(&client->state.mutex); + result = client->state.user; + if (result == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) + load_state->progress = RC_CLIENT_LOAD_STATE_AWAIT_LOGIN; + rc_mutex_unlock(&client->state.mutex); + + switch (result) { + case RC_CLIENT_USER_STATE_LOGGED_IN: + break; + + case RC_CLIENT_USER_STATE_LOGIN_REQUESTED: + /* do nothing, this function will be called again after login completes */ + return; + + default: + rc_client_load_error(load_state, RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED)); + return; + } + + memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); + fetch_game_data_request.username = client->user.username; + fetch_game_data_request.api_token = client->user.token; + fetch_game_data_request.game_id = load_state->hash->game_id; + + result = rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + return; + } + + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_FETCHING_GAME_DATA, 1); + + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Fetching data for game %u", fetch_game_data_request.game_id); + client->callbacks.server_call(&request, rc_client_fetch_game_data_callback, load_state, client); + rc_api_destroy_request(&request); +} + +static void rc_client_identify_game_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_client_t* client = load_state->client; + rc_api_resolve_hash_response_t resolve_hash_response; + int outstanding_requests; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(client, &load_state->async_handle)) { + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted during game identification"); + return; + } + + result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response); + + if (error_message) { + rc_client_end_load_state(load_state); + rc_client_load_error(load_state, result, error_message); + } + else { + /* hash exists outside the load state - always update it */ + load_state->hash->game_id = resolve_hash_response.game_id; + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + + /* have to call end_load_state after updating hash in case the load_state gets free'd */ + outstanding_requests = rc_client_end_load_state(load_state); + if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + rc_client_begin_fetch_game_data(load_state); + } + } + + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash) +{ + rc_client_game_hash_t* game_hash; + + rc_mutex_lock(&client->state.mutex); + game_hash = client->hashes; + while (game_hash) { + if (strcasecmp(game_hash->hash, hash) == 0) + break; + + game_hash = game_hash->next; + } + + if (!game_hash) { + game_hash = rc_buf_alloc(&client->state.buffer, sizeof(rc_client_game_hash_t)); + memset(game_hash, 0, sizeof(*game_hash)); + snprintf(game_hash->hash, sizeof(game_hash->hash), "%s", hash); + game_hash->game_id = RC_CLIENT_UNKNOWN_GAME_ID; + game_hash->next = client->hashes; + client->hashes = game_hash; + } + rc_mutex_unlock(&client->state.mutex); + + return game_hash; +} + +static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, + const char* hash, const char* file_path) +{ + rc_client_t* client = load_state->client; + rc_client_game_hash_t* old_hash; + + if (client->state.load == NULL) { + rc_client_unload_game(client); + client->state.load = load_state; + + if (load_state->game == NULL) { + load_state->game = (rc_client_game_info_t*)calloc(1, sizeof(*load_state->game)); + if (!load_state->game) { + if (load_state->callback) + load_state->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + return NULL; + } + + rc_buf_init(&load_state->game->buffer); + rc_runtime_init(&load_state->game->runtime); + } + } + else if (client->state.load != load_state) { + /* previous load was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + return NULL; + } + + old_hash = load_state->hash; + load_state->hash = rc_client_find_game_hash(client, hash); + + if (file_path) { + rc_client_media_hash_t* media_hash = + (rc_client_media_hash_t*)rc_buf_alloc(&load_state->game->buffer, sizeof(*media_hash)); + media_hash->game_hash = load_state->hash; + media_hash->path_djb2 = rc_djb2(file_path); + media_hash->next = load_state->game->media_hash; + load_state->game->media_hash = media_hash; + } + else if (load_state->game->media_hash && load_state->game->media_hash->game_hash == old_hash) { + load_state->game->media_hash->game_hash = load_state->hash; + } + + if (load_state->hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) { + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + int result; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = hash; + + result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + return NULL; + } + + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_IDENTIFYING_GAME, 1); + + client->callbacks.server_call(&request, rc_client_identify_game_callback, load_state, client); + + rc_api_destroy_request(&request); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + + rc_client_begin_fetch_game_data(load_state); + } + + return &load_state->async_handle; +} + +rc_hash_iterator_t* rc_client_get_load_state_hash_iterator(rc_client_t* client) +{ + if (client && client->state.load) + return &client->state.load->hash_iterator; + + return NULL; +} + +rc_client_async_handle_t* rc_client_begin_load_game(rc_client_t* client, const char* hash, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_load_state_t* load_state; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!hash || !hash[0]) { + callback(RC_INVALID_STATE, "hash is required", client, callback_userdata); + return NULL; + } + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + + return rc_client_load_game(load_state, hash, NULL); +} + +rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* client, + uint32_t console_id, const char* file_path, + const uint8_t* data, size_t data_size, + rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_load_state_t* load_state; + char hash[33]; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (data) { + if (file_path) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p (%s)", data_size, data, file_path); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p", data_size, data); + } + } + else if (file_path) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %s", file_path); + } + else { + callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata); + return NULL; + } + + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + g_hash_client = client; + rc_hash_init_error_message_callback(rc_client_log_hash_message); + rc_hash_init_verbose_message_callback(rc_client_log_hash_message); + } + + if (!file_path) + file_path = "?"; + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + + if (console_id == RC_CONSOLE_UNKNOWN) { + rc_hash_initialize_iterator(&load_state->hash_iterator, file_path, data, data_size); + + if (!rc_hash_iterate(hash, &load_state->hash_iterator)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + + load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1]; + } + else { + /* ASSERT: hash_iterator->index and hash_iterator->consoles[0] will be 0 from calloc */ + load_state->hash_console_id = console_id; + + if (data != NULL) { + if (!rc_hash_generate_from_buffer(hash, console_id, data, data_size)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + } + else { + if (!rc_hash_generate_from_file(hash, console_id, file_path)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + } + } + + return rc_client_load_game(load_state, hash, file_path); +} + +static void rc_client_game_mark_ui_to_be_hidden(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + rc_client_subset_info_t* subset; + + for (subset = game->subsets; subset; subset = subset->next) { + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE && + achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) + rc_client_release_leaderboard_tracker(game, leaderboard); + } + } + + rc_client_hide_progress_tracker(client, game); +} + +void rc_client_unload_game(rc_client_t* client) +{ + rc_client_game_info_t* game; + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + if (!client) + return; + + rc_mutex_lock(&client->state.mutex); + + game = client->game; + client->game = NULL; + client->state.load = NULL; + + if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) + client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_ON; + + if (game != NULL) + rc_client_game_mark_ui_to_be_hidden(client, game); + + last = &client->state.scheduled_callbacks; + do { + next = *last; + if (!next) + break; + + /* remove rich presence ping scheduled event for game */ + if (next->callback == rc_client_ping && game && next->related_id == game->public_.id) { + *last = next->next; + continue; + } + + last = &next->next; + } while (1); + + rc_mutex_unlock(&client->state.mutex); + + if (game != NULL) { + rc_client_raise_pending_events(client, game); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unloading game %u", game->public_.id); + rc_client_free_game(game); + } +} + +static void rc_client_change_media(rc_client_t* client, const rc_client_game_hash_t* game_hash, rc_client_callback_t callback, void* callback_userdata) +{ + if (game_hash->game_id == client->game->public_.id) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to valid media for game %u: %s", game_hash->game_id, game_hash->hash); + } + else if (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) { + RC_CLIENT_LOG_INFO(client, "Switching to unknown media"); + } + else if (game_hash->game_id == 0) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to unrecognized media: %s", game_hash->hash); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to known media for game %u: %s", game_hash->game_id, game_hash->hash); + } + + client->game->public_.hash = game_hash->hash; + callback(RC_OK, NULL, client, callback_userdata); +} + +static void rc_client_identify_changed_media_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_client_t* client = load_state->client; + rc_api_resolve_hash_response_t resolve_hash_response; + + int result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response); + + if (rc_client_async_handle_aborted(client, &load_state->async_handle)) { + RC_CLIENT_LOG_VERBOSE(client, "Media change aborted"); + /* if lookup succeeded, still capture the new hash */ + if (result == RC_OK) + load_state->hash->game_id = resolve_hash_response.game_id; + } + else if (client->game != load_state->game) { + /* loaded game changed. return success regardless of result */ + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else if (error_message) { + load_state->callback(result, error_message, client, load_state->callback_userdata); + } + else { + load_state->hash->game_id = resolve_hash_response.game_id; + + if (resolve_hash_response.game_id == 0 && client->state.hardcore) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling hardcore for unidentified media: %s", load_state->hash->hash); + rc_client_set_hardcore_enabled(client, 0); + client->game->public_.hash = load_state->hash->hash; /* do still update the loaded hash */ + load_state->callback(RC_HARDCORE_DISABLED, "Hardcore disabled. Unidentified media inserted.", client, load_state->callback_userdata); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + rc_client_change_media(client, load_state->hash, load_state->callback, load_state->callback_userdata); + } + } + + free(load_state); + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_game_hash_t* game_hash = NULL; + rc_client_media_hash_t* media_hash; + rc_client_game_info_t* game; + rc_client_pending_media_t* pending_media = NULL; + uint32_t path_djb2; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!data && !file_path) { + callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata); + return NULL; + } + + rc_mutex_lock(&client->state.mutex); + if (client->state.load) { + game = client->state.load->game; + if (game->public_.console_id == 0) { + /* still waiting for game data */ + pending_media = client->state.load->pending_media; + if (pending_media) { + if (pending_media->data) + free(pending_media->data); + free((void*)pending_media->file_path); + free(pending_media); + } + + pending_media = (rc_client_pending_media_t*)calloc(1, sizeof(*pending_media)); + if (!pending_media) { + rc_mutex_unlock(&client->state.mutex); + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + pending_media->file_path = strdup(file_path); + pending_media->callback = callback; + pending_media->callback_userdata = callback_userdata; + if (data && data_size) { + pending_media->data_size = data_size; + pending_media->data = (uint8_t*)malloc(data_size); + if (!pending_media->data) { + rc_mutex_unlock(&client->state.mutex); + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + memcpy(pending_media->data, data, data_size); + } + + client->state.load->pending_media = pending_media; + } + } + else { + game = client->game; + } + rc_mutex_unlock(&client->state.mutex); + + if (!game) { + callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); + return NULL; + } + + /* still waiting for game data */ + if (pending_media) + return NULL; + + /* check to see if we've already hashed this file */ + path_djb2 = rc_djb2(file_path); + rc_mutex_lock(&client->state.mutex); + for (media_hash = game->media_hash; media_hash; media_hash = media_hash->next) { + if (media_hash->path_djb2 == path_djb2) { + game_hash = media_hash->game_hash; + break; + } + } + rc_mutex_unlock(&client->state.mutex); + + if (!game_hash) { + char hash[33]; + int result; + + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + g_hash_client = client; + rc_hash_init_error_message_callback(rc_client_log_hash_message); + rc_hash_init_verbose_message_callback(rc_client_log_hash_message); + } + + if (data != NULL) + result = rc_hash_generate_from_buffer(hash, game->public_.console_id, data, data_size); + else + result = rc_hash_generate_from_file(hash, game->public_.console_id, file_path); + + g_hash_client = NULL; + + if (!result) { + /* when changing discs, if the disc is not supported by the system, allow it. this is + * primarily for games that support user-provided audio CDs, but does allow using discs + * from other systems for games that leverage user-provided discs. */ + strcpy_s(hash, sizeof(hash), "[NO HASH]"); + } + + game_hash = rc_client_find_game_hash(client, hash); + + media_hash = (rc_client_media_hash_t*)rc_buf_alloc(&game->buffer, sizeof(*media_hash)); + media_hash->game_hash = game_hash; + media_hash->path_djb2 = path_djb2; + + rc_mutex_lock(&client->state.mutex); + media_hash->next = game->media_hash; + game->media_hash = media_hash; + rc_mutex_unlock(&client->state.mutex); + + if (!result) { + rc_client_change_media(client, game_hash, callback, callback_userdata); + return NULL; + } + } + + if (game_hash->game_id != RC_CLIENT_UNKNOWN_GAME_ID) { + rc_client_change_media(client, game_hash, callback, callback_userdata); + return NULL; + } + else { + /* call the server to make sure the hash is valid for the loaded game */ + rc_client_load_state_t* callback_data; + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + int result; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = game_hash->hash; + + result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request); + if (result != RC_OK) { + callback(result, rc_error_str(result), client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_load_state_t*)calloc(1, sizeof(rc_client_load_state_t)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->client = client; + callback_data->hash = game_hash; + callback_data->game = game; + + client->callbacks.server_call(&request, rc_client_identify_changed_media_callback, callback_data, client); + + rc_api_destroy_request(&request); + + return &callback_data->async_handle; + } +} + +const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client) +{ + return (client && client->game) ? &client->game->public_ : NULL; +} + +int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size) +{ + if (!game) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_GAME, game->badge_name); +} + +/* ===== Subsets ===== */ + +void rc_client_begin_load_subset(rc_client_t* client, uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata) +{ + char buffer[32]; + rc_client_load_state_t* load_state; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return; + } + + if (!client->game) { + callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); + return; + } + + snprintf(buffer, sizeof(buffer), "[SUBSET%u]", subset_id); + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return; + } + + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + load_state->game = client->game; + load_state->hash = rc_client_find_game_hash(client, buffer); + load_state->hash->game_id = subset_id; + client->state.load = load_state; + + rc_client_begin_fetch_game_data(load_state); +} + +const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->public_.id == subset_id) + return &subset->public_; + } + + return NULL; +} + +/* ===== Achievements ===== */ + +static void rc_client_update_achievement_display_information(rc_client_t* client, rc_client_achievement_info_t* achievement, time_t recent_unlock_time) +{ + uint8_t new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN; + uint32_t new_measured_value = 0; + + if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) + return; + + achievement->public_.measured_progress[0] = '\0'; + + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) { + /* achievement unlocked */ + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + } + else { + /* active achievement */ + new_bucket = (achievement->public_.category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL) ? + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + + if (achievement->trigger) { + if (achievement->trigger->measured_target) { + if (achievement->trigger->measured_value == RC_MEASURED_UNKNOWN) { + /* value hasn't been initialized yet, leave progress string empty */ + } + else if (achievement->trigger->measured_value == 0) { + /* value is 0, leave progress string empty. update progress to 0.0 */ + achievement->public_.measured_percent = 0.0; + } + else { + /* clamp measured value at target (can't get more than 100%) */ + new_measured_value = (achievement->trigger->measured_value > achievement->trigger->measured_target) ? + achievement->trigger->measured_target : achievement->trigger->measured_value; + + achievement->public_.measured_percent = ((float)new_measured_value * 100) / (float)achievement->trigger->measured_target; + + if (!achievement->trigger->measured_as_percent) { + snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress), + "%u/%u", new_measured_value, achievement->trigger->measured_target); + } + else if (achievement->public_.measured_percent >= 1.0) { + snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress), + "%u%%", (uint32_t)achievement->public_.measured_percent); + } + } + } + + if (achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE; + else if (achievement->public_.measured_percent >= 80.0) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE; + } + } + + if (new_bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED && achievement->public_.unlock_time >= recent_unlock_time) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED; + + achievement->public_.bucket = new_bucket; +} + +static const char* rc_client_get_achievement_bucket_label(uint8_t bucket_type) +{ + switch (bucket_type) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: return "Locked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: return "Unlocked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: return "Unsupported"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: return "Unofficial"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: return "Recently Unlocked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: return "Active Challenges"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: return "Almost There"; + default: return "Unknown"; + } +} + +static const char* rc_client_get_subset_achievement_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + const char** ptr; + const char* label; + char* new_label; + size_t new_label_len; + + switch (bucket_type) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: ptr = &subset->locked_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: ptr = &subset->unlocked_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: ptr = &subset->unofficial_label; break; + default: return rc_client_get_achievement_bucket_label(bucket_type); + } + + if (*ptr) + return *ptr; + + label = rc_client_get_achievement_bucket_label(bucket_type); + new_label_len = strlen(subset->public_.title) + strlen(label) + 4; + new_label = (char*)rc_buf_alloc(&game->buffer, new_label_len); + snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label); + + *ptr = new_label; + return new_label; +} + +static int rc_client_compare_achievement_unlock_times(const void* a, const void* b) +{ + const rc_client_achievement_t* unlock_a = *(const rc_client_achievement_t**)a; + const rc_client_achievement_t* unlock_b = *(const rc_client_achievement_t**)b; + return (int)(unlock_b->unlock_time - unlock_a->unlock_time); +} + +static uint8_t rc_client_map_bucket(uint8_t bucket, int grouping) +{ + if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE) { + switch (bucket) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: + return RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + + case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: + case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: + return RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + + default: + return bucket; + } + } + + return bucket; +} + +rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* stop; + rc_client_achievement_t** bucket_achievements; + rc_client_achievement_t** achievement_ptr; + rc_client_achievement_bucket_t* bucket_ptr; + rc_client_achievement_list_t* list; + rc_client_subset_info_t* subset; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t bucket_counts[16]; + uint32_t num_buckets; + uint32_t num_achievements; + size_t buckets_size; + uint8_t bucket_type; + uint32_t num_subsets = 0; + uint32_t i, j; + const uint8_t shared_bucket_order[] = { + RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE, + RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE + }; + const uint8_t subset_bucket_order[] = { + RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED + }; + const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + + if (!client || !client->game) + return calloc(1, sizeof(rc_client_achievement_list_t)); + + memset(&bucket_counts, 0, sizeof(bucket_counts)); + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + num_subsets++; + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category) { + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + bucket_counts[rc_client_map_bucket(achievement->public_.bucket, grouping)]++; + } + } + } + + num_buckets = 0; + num_achievements = 0; + for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) { + if (bucket_counts[i]) { + int needs_split = 0; + + num_achievements += bucket_counts[i]; + + if (num_subsets > 1) { + for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) { + if (subset_bucket_order[j] == i) { + needs_split = 1; + break; + } + } + } + + if (!needs_split) { + ++num_buckets; + continue; + } + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category) { + if (rc_client_map_bucket(achievement->public_.bucket, grouping) == i) { + ++num_buckets; + break; + } + } + } + } + } + } + + buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_achievement_bucket_t)); + + list = (rc_client_achievement_list_t*)malloc(list_size + buckets_size + num_achievements * sizeof(rc_client_achievement_t*)); + bucket_ptr = list->buckets = (rc_client_achievement_bucket_t*)((uint8_t*)list + list_size); + achievement_ptr = (rc_client_achievement_t**)((uint8_t*)bucket_ptr + buckets_size); + + if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS) { + for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) { + bucket_type = shared_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_achievements = achievement_ptr; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category && + rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) { + *achievement_ptr++ = &achievement->public_; + } + } + } + + if (achievement_ptr > bucket_achievements) { + bucket_ptr->achievements = bucket_achievements; + bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements); + bucket_ptr->subset_id = 0; + bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type); + bucket_ptr->bucket_type = bucket_type; + + if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED) + qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_unlock_times); + + ++bucket_ptr; + } + } + } + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { + bucket_type = subset_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_achievements = achievement_ptr; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category && + rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) { + *achievement_ptr++ = &achievement->public_; + } + } + + if (achievement_ptr > bucket_achievements) { + bucket_ptr->achievements = bucket_achievements; + bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements); + bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0; + bucket_ptr->bucket_type = bucket_type; + + if (num_subsets > 1) + bucket_ptr->label = rc_client_get_subset_achievement_bucket_label(bucket_type, client->game, subset); + else + bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type); + + ++bucket_ptr; + } + } + } + + rc_mutex_unlock(&client->state.mutex); + + list->num_buckets = (uint32_t)(bucket_ptr - list->buckets); + return list; +} + +void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list) +{ + if (list) + free(list); +} + +static const rc_client_achievement_t* rc_client_subset_get_achievement_info( + rc_client_t* client, rc_client_subset_info_t* subset, uint32_t id) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + if (achievement->public_.id == id) { + const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + rc_mutex_lock((rc_mutex_t*)(&client->state.mutex)); + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + rc_mutex_unlock((rc_mutex_t*)(&client->state.mutex)); + return &achievement->public_; + } + } + + return NULL; +} + +const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* client, uint32_t id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + const rc_client_achievement_t* achievement = rc_client_subset_get_achievement_info(client, subset, id); + if (achievement != NULL) + return achievement; + } + + return NULL; +} + +int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size) +{ + const int image_type = (state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) ? + RC_IMAGE_TYPE_ACHIEVEMENT : RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED; + + if (!achievement || !achievement->badge_name[0]) + return rc_client_get_image_url(buffer, buffer_size, image_type, "00000"); + + return rc_client_get_image_url(buffer, buffer_size, image_type, achievement->badge_name); +} + +typedef struct rc_client_award_achievement_callback_data_t +{ + uint32_t id; + uint32_t retry_count; + uint8_t hardcore; + const char* game_hash; + time_t unlock_time; + rc_client_t* client; + rc_client_scheduled_callback_data_t* scheduled_callback_data; +} rc_client_award_achievement_callback_data_t; + +static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data); + +static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, clock_t now) +{ + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)callback_data->data; + + rc_client_award_achievement_server_call(ach_data); +} + +static void rc_client_award_achievement_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)callback_data; + rc_api_award_achievement_response_t award_achievement_response; + + int result = rc_api_process_award_achievement_server_response(&award_achievement_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &award_achievement_response.response); + + if (error_message) { + if (award_achievement_response.response.error_message && !rc_client_should_retry(server_response)) { + /* actual error from server */ + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s", ach_data->id, error_message); + rc_client_raise_server_error_event(ach_data->client, "award_achievement", award_achievement_response.response.error_message); + } + else if (ach_data->retry_count++ == 0) { + /* first retry is immediate */ + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying immediately", ach_data->id, error_message); + rc_client_award_achievement_server_call(ach_data); + return; + } + else { + /* double wait time between each attempt until we hit a maximum delay of two minutes */ + /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/ + const uint32_t delay = (ach_data->retry_count > 7) ? 120 : (1 << (ach_data->retry_count - 1)); + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying in %u seconds", ach_data->id, error_message, delay); + + if (!ach_data->scheduled_callback_data) { + ach_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*ach_data->scheduled_callback_data)); + if (!ach_data->scheduled_callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Failed to allocate scheduled callback data for reattempt to unlock achievement %u", ach_data->id); + rc_client_raise_server_error_event(ach_data->client, "award_achievement", rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + ach_data->scheduled_callback_data->callback = rc_client_award_achievement_retry; + ach_data->scheduled_callback_data->data = ach_data; + ach_data->scheduled_callback_data->related_id = ach_data->id; + } + + ach_data->scheduled_callback_data->when = clock() + delay * CLOCKS_PER_SEC; + + rc_client_schedule_callback(ach_data->client, ach_data->scheduled_callback_data); + return; + } + } + else { + ach_data->client->user.score = award_achievement_response.new_player_score; + ach_data->client->user.score_softcore = award_achievement_response.new_player_score_softcore; + + if (award_achievement_response.awarded_achievement_id != ach_data->id) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Awarded achievement %u instead of %u", award_achievement_response.awarded_achievement_id, error_message); + } + else { + if (award_achievement_response.response.error_message) { + /* previously unlocked achievements are returned as a success with an error message */ + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u: %s", ach_data->id, award_achievement_response.response.error_message); + } + else if (ach_data->retry_count) { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded after %u attempts, new score: %u", + ach_data->id, ach_data->retry_count + 1, + ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded, new score: %u", + ach_data->id, + ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore); + } + + if (award_achievement_response.achievements_remaining == 0) { + rc_client_subset_info_t* subset; + for (subset = ach_data->client->game->subsets; subset; subset = subset->next) { + if (subset->mastery == RC_CLIENT_MASTERY_STATE_NONE && + rc_client_subset_get_achievement_info(ach_data->client, subset, ach_data->id)) { + if (subset->public_.id == ach_data->client->game->public_.id) { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Game %u %s", ach_data->client->game->public_.id, + ach_data->client->state.hardcore ? "mastered" : "completed"); + subset->mastery = RC_CLIENT_MASTERY_STATE_PENDING; + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Subset %u %s", ach_data->client->game->public_.id, + ach_data->client->state.hardcore ? "mastered" : "completed"); + + /* TODO: subset mastery notification */ + subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN; + } + } + } + } + } + } + + if (ach_data->scheduled_callback_data) + free(ach_data->scheduled_callback_data); + free(ach_data); +} + +static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data) +{ + rc_api_award_achievement_request_t api_params; + rc_api_request_t request; + int result; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = ach_data->client->user.username; + api_params.api_token = ach_data->client->user.token; + api_params.achievement_id = ach_data->id; + api_params.hardcore = ach_data->hardcore; + api_params.game_hash = ach_data->game_hash; + + result = rc_api_init_award_achievement_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error constructing unlock request for achievement %u: %s", ach_data->id, rc_error_str(result)); + free(ach_data); + return; + } + + ach_data->client->callbacks.server_call(&request, rc_client_award_achievement_callback, ach_data, ach_data->client); + + rc_api_destroy_request(&request); +} + +static void rc_client_award_achievement(rc_client_t* client, rc_client_achievement_info_t* achievement) +{ + rc_client_award_achievement_callback_data_t* callback_data; + + rc_mutex_lock(&client->state.mutex); + + if (client->state.hardcore) { + achievement->public_.unlock_time = achievement->unlock_time_hardcore = time(NULL); + if (achievement->unlock_time_softcore == 0) + achievement->unlock_time_softcore = achievement->unlock_time_hardcore; + + /* adjust score now - will get accurate score back from server */ + client->user.score += achievement->public_.points; + } + else { + achievement->public_.unlock_time = achievement->unlock_time_softcore = time(NULL); + + /* adjust score now - will get accurate score back from server */ + client->user.score_softcore += achievement->public_.points; + } + + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + achievement->public_.unlocked |= (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_mutex_unlock(&client->state.mutex); + + /* can't unlock unofficial achievements on the server */ + if (achievement->public_.category != RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unlocked unofficial achievement %u: %s", achievement->public_.id, achievement->public_.title); + return; + } + + /* don't actually unlock achievements when spectating */ + if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated achievement %u: %s", achievement->public_.id, achievement->public_.title); + return; + } + + callback_data = (rc_client_award_achievement_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for unlocking achievement %u", achievement->public_.id); + rc_client_raise_server_error_event(client, "award_achievement", rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + callback_data->client = client; + callback_data->id = achievement->public_.id; + callback_data->hardcore = client->state.hardcore; + callback_data->game_hash = client->game->public_.hash; + callback_data->unlock_time = achievement->public_.unlock_time; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Awarding achievement %u: %s", achievement->public_.id, achievement->public_.title); + rc_client_award_achievement_server_call(callback_data); +} + +static void rc_client_subset_reset_achievements(rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + if (trigger->state == RC_TRIGGER_STATE_PRIMED) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + + rc_reset_trigger(trigger); + } +} + +static void rc_client_reset_achievements(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_reset_achievements(subset); +} + +/* ===== Leaderboards ===== */ + +static const rc_client_leaderboard_t* rc_client_subset_get_leaderboard_info(const rc_client_subset_info_t* subset, uint32_t id) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.id == id) + return &leaderboard->public_; + } + + return NULL; +} + +const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + const rc_client_leaderboard_t* leaderboard = rc_client_subset_get_leaderboard_info(subset, id); + if (leaderboard != NULL) + return leaderboard; + } + + return NULL; +} + +static const char* rc_client_get_leaderboard_bucket_label(uint8_t bucket_type) +{ + switch (bucket_type) { + case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: return "Inactive"; + case RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE: return "Active"; + case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: return "Unsupported"; + case RC_CLIENT_LEADERBOARD_BUCKET_ALL: return "All"; + default: return "Unknown"; + } +} + +static const char* rc_client_get_subset_leaderboard_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + const char** ptr; + const char* label; + char* new_label; + size_t new_label_len; + + switch (bucket_type) { + case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: ptr = &subset->inactive_label; break; + case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break; + case RC_CLIENT_LEADERBOARD_BUCKET_ALL: ptr = &subset->all_label; break; + default: return rc_client_get_achievement_bucket_label(bucket_type); + } + + if (*ptr) + return *ptr; + + label = rc_client_get_leaderboard_bucket_label(bucket_type); + new_label_len = strlen(subset->public_.title) + strlen(label) + 4; + new_label = (char*)rc_buf_alloc(&game->buffer, new_label_len); + snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label); + + *ptr = new_label; + return new_label; +} + +static uint8_t rc_client_get_leaderboard_bucket(const rc_client_leaderboard_info_t* leaderboard, int grouping) +{ + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ? + RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE; + + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + return RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED; + + default: + return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ? + RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE; + } +} + +rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + rc_client_leaderboard_t** bucket_leaderboards; + rc_client_leaderboard_t** leaderboard_ptr; + rc_client_leaderboard_bucket_t* bucket_ptr; + rc_client_leaderboard_list_t* list; + rc_client_subset_info_t* subset; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t bucket_counts[8]; + uint32_t num_buckets; + uint32_t num_leaderboards; + size_t buckets_size; + uint8_t bucket_type; + uint32_t num_subsets = 0; + uint32_t i, j; + const uint8_t shared_bucket_order[] = { + RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE + }; + const uint8_t subset_bucket_order[] = { + RC_CLIENT_LEADERBOARD_BUCKET_ALL, + RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE, + RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED + }; + + if (!client || !client->game) + return calloc(1, sizeof(rc_client_leaderboard_list_t)); + + memset(&bucket_counts, 0, sizeof(bucket_counts)); + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + num_subsets++; + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->hidden) + continue; + + leaderboard->bucket = rc_client_get_leaderboard_bucket(leaderboard, grouping); + bucket_counts[leaderboard->bucket]++; + } + } + + num_buckets = 0; + num_leaderboards = 0; + for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) { + if (bucket_counts[i]) { + int needs_split = 0; + + num_leaderboards += bucket_counts[i]; + + if (num_subsets > 1) { + for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) { + if (subset_bucket_order[j] == i) { + needs_split = 1; + break; + } + } + } + + if (!needs_split) { + ++num_buckets; + continue; + } + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == i) { + ++num_buckets; + break; + } + } + } + } + } + + buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_leaderboard_bucket_t)); + + list = (rc_client_leaderboard_list_t*)malloc(list_size + buckets_size + num_leaderboards * sizeof(rc_client_leaderboard_t*)); + bucket_ptr = list->buckets = (rc_client_leaderboard_bucket_t*)((uint8_t*)list + list_size); + leaderboard_ptr = (rc_client_leaderboard_t**)((uint8_t*)bucket_ptr + buckets_size); + + if (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING) { + for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) { + bucket_type = shared_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_leaderboards = leaderboard_ptr; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == bucket_type && !leaderboard->hidden) + *leaderboard_ptr++ = &leaderboard->public_; + } + } + + if (leaderboard_ptr > bucket_leaderboards) { + bucket_ptr->leaderboards = bucket_leaderboards; + bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards); + bucket_ptr->subset_id = 0; + bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type); + bucket_ptr->bucket_type = bucket_type; + ++bucket_ptr; + } + } + } + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { + bucket_type = subset_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_leaderboards = leaderboard_ptr; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == bucket_type && !leaderboard->hidden) + *leaderboard_ptr++ = &leaderboard->public_; + } + + if (leaderboard_ptr > bucket_leaderboards) { + bucket_ptr->leaderboards = bucket_leaderboards; + bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards); + bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0; + bucket_ptr->bucket_type = bucket_type; + + if (num_subsets > 1) + bucket_ptr->label = rc_client_get_subset_leaderboard_bucket_label(bucket_type, client->game, subset); + else + bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type); + + ++bucket_ptr; + } + } + } + + rc_mutex_unlock(&client->state.mutex); + + list->num_buckets = (uint32_t)(bucket_ptr - list->buckets); + return list; +} + +void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list) +{ + if (list) + free(list); +} + +static void rc_client_allocate_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker; + rc_client_leaderboard_tracker_info_t* available_tracker = NULL; + + for (tracker = game->leaderboard_trackers; tracker; tracker = tracker->next) { + if (tracker->reference_count == 0) { + if (available_tracker == NULL && tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE) + available_tracker = tracker; + + continue; + } + + if (tracker->value_djb2 != leaderboard->value_djb2 || tracker->format != leaderboard->format) + continue; + + if (tracker->raw_value != leaderboard->value) { + /* if the value comes from tracking hits, we can't assume the trackers started in the + * same frame, so we can't share the tracker */ + if (tracker->value_from_hits) + continue; + + /* value has changed. prepare an update event */ + tracker->raw_value = leaderboard->value; + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } + + /* attach to the existing tracker */ + ++tracker->reference_count; + tracker->pending_events &= ~RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE; + leaderboard->tracker = tracker; + leaderboard->public_.tracker_value = tracker->public_.display; + return; + } + + if (!available_tracker) { + rc_client_leaderboard_tracker_info_t** next = &game->leaderboard_trackers; + + available_tracker = (rc_client_leaderboard_tracker_info_t*)rc_buf_alloc(&game->buffer, sizeof(*available_tracker)); + memset(available_tracker, 0, sizeof(*available_tracker)); + available_tracker->public_.id = 1; + + for (tracker = *next; tracker; next = &tracker->next, tracker = *next) + available_tracker->public_.id++; + + *next = available_tracker; + } + + /* update the claimed tracker */ + available_tracker->reference_count = 1; + available_tracker->value_djb2 = leaderboard->value_djb2; + available_tracker->format = leaderboard->format; + available_tracker->raw_value = leaderboard->value; + available_tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW; + available_tracker->value_from_hits = rc_value_from_hits(&leaderboard->lboard->value); + leaderboard->tracker = available_tracker; + leaderboard->public_.tracker_value = available_tracker->public_.display; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; +} + +static void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker; + leaderboard->tracker = NULL; + + if (tracker && --tracker->reference_count == 0) { + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } +} + +static void rc_client_update_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker; + if (tracker && tracker->raw_value != leaderboard->value) { + tracker->raw_value = leaderboard->value; + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } +} + +typedef struct rc_client_submit_leaderboard_entry_callback_data_t +{ + uint32_t id; + int32_t score; + uint32_t retry_count; + const char* game_hash; + time_t submit_time; + rc_client_t* client; + rc_client_scheduled_callback_data_t* scheduled_callback_data; +} rc_client_submit_leaderboard_entry_callback_data_t; + +static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data); + +static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, clock_t now) +{ + rc_client_submit_leaderboard_entry_callback_data_t* lboard_data = + (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data->data; + + rc_client_submit_leaderboard_entry_server_call(lboard_data); +} + +static void rc_client_submit_leaderboard_entry_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_submit_leaderboard_entry_callback_data_t* lboard_data = + (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data; + rc_api_submit_lboard_entry_response_t submit_lboard_entry_response; + + int result = rc_api_process_submit_lboard_entry_server_response(&submit_lboard_entry_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &submit_lboard_entry_response.response); + + if (error_message) { + if (submit_lboard_entry_response.response.error_message && !rc_client_should_retry(server_response)) { + /* actual error from server */ + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s", lboard_data->id, error_message); + rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", submit_lboard_entry_response.response.error_message); + } + else if (lboard_data->retry_count++ == 0) { + /* first retry is immediate */ + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying immediately", lboard_data->id, error_message); + rc_client_submit_leaderboard_entry_server_call(lboard_data); + return; + } + else { + /* double wait time between each attempt until we hit a maximum delay of two minutes */ + /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/ + const uint32_t delay = (lboard_data->retry_count > 7) ? 120 : (1 << (lboard_data->retry_count - 1)); + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying in %u seconds", lboard_data->id, error_message, delay); + + if (!lboard_data->scheduled_callback_data) { + lboard_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*lboard_data->scheduled_callback_data)); + if (!lboard_data->scheduled_callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Failed to allocate scheduled callback data for reattempt to submit entry for leaderboard %u", lboard_data->id); + rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + lboard_data->scheduled_callback_data->callback = rc_client_submit_leaderboard_entry_retry; + lboard_data->scheduled_callback_data->data = lboard_data; + lboard_data->scheduled_callback_data->related_id = lboard_data->id; + } + + lboard_data->scheduled_callback_data->when = clock() + delay * CLOCKS_PER_SEC; + + rc_client_schedule_callback(lboard_data->client, lboard_data->scheduled_callback_data); + return; + } + } + else { + /* TODO: raise event for scoreboard (if retry_count < 2) */ + + /* not currently doing anything with the response */ + if (lboard_data->retry_count) { + RC_CLIENT_LOG_INFO_FORMATTED(lboard_data->client, "Leaderboard %u submission %d completed after %u attempts", + lboard_data->id, lboard_data->score, lboard_data->retry_count); + } + } + + if (lboard_data->scheduled_callback_data) + free(lboard_data->scheduled_callback_data); + free(lboard_data); +} + +static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data) +{ + rc_api_submit_lboard_entry_request_t api_params; + rc_api_request_t request; + int result; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = lboard_data->client->user.username; + api_params.api_token = lboard_data->client->user.token; + api_params.leaderboard_id = lboard_data->id; + api_params.score = lboard_data->score; + api_params.game_hash = lboard_data->game_hash; + + result = rc_api_init_submit_lboard_entry_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error constructing submit leaderboard entry for leaderboard %u: %s", lboard_data->id, rc_error_str(result)); + return; + } + + lboard_data->client->callbacks.server_call(&request, rc_client_submit_leaderboard_entry_callback, lboard_data, lboard_data->client); + + rc_api_destroy_request(&request); +} + +static void rc_client_submit_leaderboard_entry(rc_client_t* client, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_submit_leaderboard_entry_callback_data_t* callback_data; + + /* don't actually submit leaderboard entries when spectating */ + if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated %s (%d) for leaderboard %u: %s", + leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title); + return; + } + + callback_data = (rc_client_submit_leaderboard_entry_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for submitting entry for leaderboard %u", leaderboard->public_.id); + rc_client_raise_server_error_event(client, "submit_lboard_entry", rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + callback_data->client = client; + callback_data->id = leaderboard->public_.id; + callback_data->score = leaderboard->value; + callback_data->game_hash = client->game->public_.hash; + callback_data->submit_time = time(NULL); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Submitting %s (%d) for leaderboard %u: %s", + leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title); + rc_client_submit_leaderboard_entry_server_call(callback_data); +} + +static void rc_client_subset_reset_leaderboards(rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (!lboard) + continue; + + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + rc_client_release_leaderboard_tracker(game, leaderboard); + /* fallthrough to default */ + default: + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + rc_reset_lboard(lboard); + break; + } + } +} + +static void rc_client_reset_leaderboards(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_reset_leaderboards(client->game, subset); +} + +typedef struct rc_client_fetch_leaderboard_entries_callback_data_t { + rc_client_t* client; + rc_client_fetch_leaderboard_entries_callback_t callback; + void* callback_userdata; + uint32_t leaderboard_id; + rc_client_async_handle_t async_handle; +} rc_client_fetch_leaderboard_entries_callback_data_t; + +static void rc_client_fetch_leaderboard_entries_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_fetch_leaderboard_entries_callback_data_t* lbinfo_callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)callback_data; + rc_client_t* client = lbinfo_callback_data->client; + rc_api_fetch_leaderboard_info_response_t lbinfo_response; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(client, &lbinfo_callback_data->async_handle)) { + RC_CLIENT_LOG_VERBOSE(client, "Fetch leaderbord entries aborted"); + free(lbinfo_callback_data); + return; + } + + result = rc_api_process_fetch_leaderboard_info_server_response(&lbinfo_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &lbinfo_response.response); + if (error_message) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch leaderboard %u info failed: %s", lbinfo_callback_data->leaderboard_id, error_message); + lbinfo_callback_data->callback(result, error_message, NULL, client, lbinfo_callback_data->callback_userdata); + } + else { + rc_client_leaderboard_entry_list_t* list; + const size_t list_size = sizeof(*list) + sizeof(rc_client_leaderboard_entry_t) * lbinfo_response.num_entries; + size_t needed_size = list_size; + unsigned i; + + for (i = 0; i < lbinfo_response.num_entries; i++) + needed_size += strlen(lbinfo_response.entries[i].username) + 1; + + list = (rc_client_leaderboard_entry_list_t*)malloc(needed_size); + if (!list) { + lbinfo_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, lbinfo_callback_data->callback_userdata); + } + else { + rc_client_leaderboard_entry_t* entry = list->entries = (rc_client_leaderboard_entry_t*)((uint8_t*)list + sizeof(*list)); + char* user = (char*)((uint8_t*)list + list_size); + const rc_api_lboard_info_entry_t* lbentry = lbinfo_response.entries; + const rc_api_lboard_info_entry_t* stop = lbentry + lbinfo_response.num_entries; + const size_t logged_in_user_len = strlen(client->user.display_name) + 1; + list->user_index = -1; + + for (; lbentry < stop; ++lbentry, ++entry) { + const size_t len = strlen(lbentry->username) + 1; + entry->user = user; + memcpy(user, lbentry->username, len); + user += len; + + if (len == logged_in_user_len && memcmp(entry->user, client->user.display_name, len) == 0) + list->user_index = (int)(entry - list->entries); + + entry->index = lbentry->index; + entry->rank = lbentry->rank; + entry->submitted = lbentry->submitted; + + rc_format_value(entry->display, sizeof(entry->display), lbentry->score, lbinfo_response.format); + } + + list->num_entries = lbinfo_response.num_entries; + + lbinfo_callback_data->callback(RC_OK, NULL, list, client, lbinfo_callback_data->callback_userdata); + } + } + + rc_api_destroy_fetch_leaderboard_info_response(&lbinfo_response); + free(lbinfo_callback_data); +} + +static rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_info(rc_client_t* client, + const rc_api_fetch_leaderboard_info_request_t* lbinfo_request, + rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_client_fetch_leaderboard_entries_callback_data_t* callback_data; + rc_api_request_t request; + int result; + const char* error_message; + + result = rc_api_init_fetch_leaderboard_info_request(&request, lbinfo_request); + + if (result != RC_OK) { + error_message = rc_error_str(result); + callback(result, error_message, NULL, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->leaderboard_id = lbinfo_request->leaderboard_id; + + client->callbacks.server_call(&request, rc_client_fetch_leaderboard_entries_callback, callback_data, client); + rc_api_destroy_request(&request); + + return &callback_data->async_handle; +} + +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id, + uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_api_fetch_leaderboard_info_request_t lbinfo_request; + + memset(&lbinfo_request, 0, sizeof(lbinfo_request)); + lbinfo_request.leaderboard_id = leaderboard_id; + lbinfo_request.first_entry = first_entry; + lbinfo_request.count = count; + + return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata); +} + +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id, + uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_api_fetch_leaderboard_info_request_t lbinfo_request; + + memset(&lbinfo_request, 0, sizeof(lbinfo_request)); + lbinfo_request.leaderboard_id = leaderboard_id; + lbinfo_request.username = client->user.username; + lbinfo_request.count = count; + + if (!lbinfo_request.username) { + callback(RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED), NULL, client, callback_userdata); + return NULL; + } + + return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata); +} + +void rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list) +{ + if (list) + free(list); +} + +int rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size) +{ + if (!entry) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, entry->user); +} + +/* ===== Rich Presence ===== */ + +static void rc_client_ping_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_t* client = (rc_client_t*)callback_data; + rc_api_ping_response_t response; + + int result = rc_api_process_ping_server_response(&response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &response.response); + if (error_message) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Ping response error: %s", error_message); + } + + rc_api_destroy_ping_response(&response); +} + +static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, clock_t now) +{ + rc_api_ping_request_t api_params; + rc_api_request_t request; + char buffer[256]; + int result; + + rc_runtime_get_richpresence(&client->game->runtime, buffer, sizeof(buffer), + client->state.legacy_peek, client, NULL); + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = client->user.username; + api_params.api_token = client->user.token; + api_params.game_id = client->game->public_.id; + api_params.rich_presence = buffer; + + result = rc_api_init_ping_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Error generating ping request: %s", rc_error_str(result)); + } + else { + client->callbacks.server_call(&request, rc_client_ping_callback, client, client); + } + + callback_data->when = now + 120 * CLOCKS_PER_SEC; + rc_client_schedule_callback(client, callback_data); +} + +size_t rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size) +{ + int result; + + if (!client || !client->game || !buffer) + return 0; + + result = rc_runtime_get_richpresence(&client->game->runtime, buffer, (unsigned)buffer_size, + client->state.legacy_peek, client, NULL); + + if (result == 0) + result = snprintf(buffer, buffer_size, "Playing %s", client->game->public_.title); + + return result; +} + +/* ===== Processing ===== */ + +void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler) +{ + if (client) + client->callbacks.event_handler = handler; +} + +void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler) +{ + if (client) + client->callbacks.read_memory = handler; +} + +static void rc_client_invalidate_processing_memref(rc_client_t* client) +{ + rc_memref_t** next_memref = &client->game->runtime.memrefs; + rc_memref_t* memref; + + /* if processing_memref is not set, this occurred following a pointer chain. ignore it. */ + if (!client->state.processing_memref) + return; + + /* invalid memref. remove from chain so we don't have to evaluate it in the future. + * it's still there, so anything referencing it will always fetch the current value. */ + while ((memref = *next_memref) != NULL) { + if (memref == client->state.processing_memref) { + *next_memref = memref->next; + break; + } + next_memref = &memref->next; + } + + rc_client_invalidate_memref_achievements(client->game, client, client->state.processing_memref); + rc_client_invalidate_memref_leaderboards(client->game, client, client->state.processing_memref); + + client->state.processing_memref = NULL; +} + +static unsigned rc_client_peek_le(unsigned address, unsigned num_bytes, void* ud) +{ + rc_client_t* client = (rc_client_t*)ud; + unsigned value = 0; + uint32_t num_read = 0; + + /* if we know the address is out of range, and it's part of a pointer chain + * (processing_memref is null), don't bother processing it. */ + if (address > client->game->max_valid_address && !client->state.processing_memref) + return 0; + + if (num_bytes <= sizeof(value)) { + num_read = client->callbacks.read_memory(address, (uint8_t*)&value, num_bytes, client); + if (num_read == num_bytes) + return value; + } + + if (num_read < num_bytes) + rc_client_invalidate_processing_memref(client); + + return 0; +} + +static unsigned rc_client_peek(unsigned address, unsigned num_bytes, void* ud) +{ + rc_client_t* client = (rc_client_t*)ud; + uint8_t buffer[4]; + uint32_t num_read = 0; + + /* if we know the address is out of range, and it's part of a pointer chain + * (processing_memref is null), don't bother processing it. */ + if (address > client->game->max_valid_address && !client->state.processing_memref) + return 0; + + switch (num_bytes) { + case 1: + num_read = client->callbacks.read_memory(address, buffer, 1, client); + if (num_read == 1) + return buffer[0]; + break; + case 2: + num_read = client->callbacks.read_memory(address, buffer, 2, client); + if (num_read == 2) + return buffer[0] | (buffer[1] << 8); + break; + case 3: + num_read = client->callbacks.read_memory(address, buffer, 3, client); + if (num_read == 3) + return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16); + break; + case 4: + num_read = client->callbacks.read_memory(address, buffer, 4, client); + if (num_read == 4) + return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24); + break; + default: + break; + } + + if (num_read < num_bytes) + rc_client_invalidate_processing_memref(client); + + return 0; +} + +void rc_client_set_legacy_peek(rc_client_t* client, int method) +{ + if (method == RC_CLIENT_LEGACY_PEEK_AUTO) { + uint8_t buffer[4] = { 1,0,0,0 }; + method = (*((uint32_t*)buffer) == 1) ? + RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS : RC_CLIENT_LEGACY_PEEK_CONSTRUCTED; + } + + client->state.legacy_peek = (method == RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS) ? + rc_client_peek_le : rc_client_peek; +} + +int rc_client_is_processing_required(rc_client_t* client) +{ + if (!client || !client->game) + return 0; + + if (client->game->runtime.trigger_count || client->game->runtime.lboard_count) + return 1; + + return (client->game->runtime.richpresence && client->game->runtime.richpresence->richpresence); +} + +static void rc_client_update_memref_values(rc_client_t* client) +{ + rc_memref_t* memref = client->game->runtime.memrefs; + unsigned value; + int invalidated_memref = 0; + + for (; memref; memref = memref->next) { + if (memref->value.is_indirect) + continue; + + client->state.processing_memref = memref; + + value = rc_peek_value(memref->address, memref->value.size, client->state.legacy_peek, client); + + if (client->state.processing_memref) { + rc_update_memref_value(&memref->value, value); + } + else { + /* if the peek function cleared the processing_memref, the memref was invalidated */ + invalidated_memref = 1; + } + } + + client->state.processing_memref = NULL; + + if (invalidated_memref) + rc_client_update_active_achievements(client->game); +} + +static void rc_client_do_frame_process_achievements(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + int old_state, new_state; + unsigned old_measured_value; + + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + old_measured_value = trigger->measured_value; + old_state = trigger->state; + new_state = rc_evaluate_trigger(trigger, client->state.legacy_peek, client, NULL); + + /* if the measured value changed and the achievement hasn't triggered, show a progress indicator */ + if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN && + trigger->measured_value <= trigger->measured_target && + rc_trigger_state_active(new_state) && new_state != RC_TRIGGER_STATE_WAITING) { + + /* only show a popup for the achievement closest to triggering */ + float progress = (float)trigger->measured_value / (float)trigger->measured_target; + + if (trigger->measured_as_percent) { + /* if reporting the measured value as a percentage, only show the popup if the percentage changes */ + const unsigned old_percent = (unsigned)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); + const unsigned new_percent = (unsigned)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); + if (old_percent == new_percent) + progress = -1.0; + } + + if (progress > client->game->progress_tracker.progress) { + client->game->progress_tracker.progress = progress; + client->game->progress_tracker.achievement = achievement; + client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_UPDATE; + } + } + + /* if the state hasn't changed, there won't be any events raised */ + if (new_state == old_state) + continue; + + /* raise a CHALLENGE_INDICATOR_HIDE event when changing from PRIMED to anything else */ + if (old_state == RC_TRIGGER_STATE_PRIMED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + + /* raise events for each of the possible new states */ + if (new_state == RC_TRIGGER_STATE_TRIGGERED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED; + else if (new_state == RC_TRIGGER_STATE_PRIMED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW; + + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } +} + +static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) +{ + /* ASSERT: this should only be called if the mutex is held */ + + if (game->progress_tracker.hide_callback && + game->progress_tracker.hide_callback->when && + game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) { + rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, 0); + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER; + } +} + +static void rc_client_progress_tracker_timer_elapsed(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, clock_t now) +{ + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + + rc_mutex_lock(&client->state.mutex); + if (client->game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) { + client->game->progress_tracker.hide_callback->when = 0; + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE; + } + rc_mutex_unlock(&client->state.mutex); + + if (client_event.type) + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_do_frame_update_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) +{ + if (!game->progress_tracker.hide_callback) { + game->progress_tracker.hide_callback = (rc_client_scheduled_callback_data_t*) + rc_buf_alloc(&game->buffer, sizeof(rc_client_scheduled_callback_data_t)); + memset(game->progress_tracker.hide_callback, 0, sizeof(rc_client_scheduled_callback_data_t)); + game->progress_tracker.hide_callback->callback = rc_client_progress_tracker_timer_elapsed; + } + + if (game->progress_tracker.hide_callback->when == 0) + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW; + else + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE; + + rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, clock() + 2 * CLOCKS_PER_SEC); +} + +static void rc_client_raise_progress_tracker_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + switch (game->progress_tracker.action) { + case RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW; + break; + case RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE; + break; + default: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE; + break; + } + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE; + + client_event.achievement = &game->progress_tracker.achievement->public_; + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_raise_achievement_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + rc_client_event_t client_event; + time_t recent_unlock_time = 0; + + memset(&client_event, 0, sizeof(client_event)); + + for (; achievement < stop; ++achievement) { + if (achievement->pending_events == RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE) + continue; + + /* kick off award achievement request first */ + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) { + rc_client_award_achievement(client, achievement); + client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS; + } + + /* update display state */ + if (recent_unlock_time == 0) + recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + + /* raise events */ + client_event.achievement = &achievement->public_; + + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + client->callbacks.event_handler(&client_event, client); + } + else if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW; + client->callbacks.event_handler(&client_event, client); + } + + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + client->callbacks.event_handler(&client_event, client); + } + + /* clear pending flags */ + achievement->pending_events = RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE; + } +} + +static void rc_client_raise_mastery_event(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_GAME_COMPLETED; + + subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN; + + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_do_frame_process_leaderboards(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + int old_state, new_state; + + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + default: + if (!lboard) + continue; + + break; + } + + old_state = lboard->state; + new_state = rc_evaluate_lboard(lboard, &leaderboard->value, client->state.legacy_peek, client, NULL); + + switch (new_state) { + case RC_LBOARD_STATE_STARTED: /* leaderboard is running */ + if (old_state != RC_LBOARD_STATE_STARTED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED; + rc_client_allocate_leaderboard_tracker(client->game, leaderboard); + } + else { + rc_client_update_leaderboard_tracker(client->game, leaderboard); + } + break; + + case RC_LBOARD_STATE_CANCELED: + if (old_state != RC_LBOARD_STATE_CANCELED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + rc_client_release_leaderboard_tracker(client->game, leaderboard); + } + break; + + case RC_LBOARD_STATE_TRIGGERED: + if (old_state != RC_RUNTIME_EVENT_LBOARD_TRIGGERED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED; + + if (old_state != RC_LBOARD_STATE_STARTED) + rc_client_allocate_leaderboard_tracker(client->game, leaderboard); + else + rc_client_update_leaderboard_tracker(client->game, leaderboard); + + rc_client_release_leaderboard_tracker(client->game, leaderboard); + } + break; + } + + if (leaderboard->pending_events) + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD; + } +} + +static void rc_client_raise_leaderboard_tracker_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_leaderboard_tracker_info_t* tracker = game->leaderboard_trackers; + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + tracker = game->leaderboard_trackers; + for (; tracker; tracker = tracker->next) { + if (tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE) + continue; + + client_event.leaderboard_tracker = &tracker->public_; + + /* update display text for new trackers or updated trackers */ + if (tracker->pending_events & (RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW | RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE)) + rc_format_value(tracker->public_.display, sizeof(tracker->public_.display), tracker->raw_value, tracker->format); + + if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE) { + if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) { + /* request to show and hide in the same frame - ignore the event */ + } + else { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE; + client->callbacks.event_handler(&client_event, client); + } + } + else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW; + client->callbacks.event_handler(&client_event, client); + } + else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE) { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE; + client->callbacks.event_handler(&client_event, client); + } + + tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE; + } +} + +static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + for (; leaderboard < leaderboard_stop; ++leaderboard) { + if (leaderboard->pending_events == RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE) + continue; + + client_event.leaderboard = &leaderboard->public_; + + if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u canceled: %s", leaderboard->public_.id, leaderboard->public_.title); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_FAILED; + client->callbacks.event_handler(&client_event, client); + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED) { + /* kick off submission request before raising event */ + rc_client_submit_leaderboard_entry(client, leaderboard); + + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + client->callbacks.event_handler(&client_event, client); + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u started: %s", leaderboard->public_.id, leaderboard->public_.title); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_STARTED; + client->callbacks.event_handler(&client_event, client); + } + + leaderboard->pending_events = RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE; + } +} + +static void rc_client_reset_pending_events(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + + client->game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE; + + for (subset = client->game->subsets; subset; subset = subset->next) + subset->pending_events = RC_CLIENT_SUBSET_PENDING_EVENT_NONE; +} + +static void rc_client_subset_raise_pending_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + /* raise any pending achievement events */ + if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT) + rc_client_raise_achievement_events(client, subset); + + /* raise any pending leaderboard events */ + if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD) + rc_client_raise_leaderboard_events(client, subset); + + /* raise mastery event if pending */ + if (subset->mastery == RC_CLIENT_MASTERY_STATE_PENDING) + rc_client_raise_mastery_event(client, subset); +} + +static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_subset_info_t* subset; + + /* raise tracker events before leaderboard events so formatted values are updated for leaderboard events */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER) + rc_client_raise_leaderboard_tracker_events(client, game); + + for (subset = game->subsets; subset; subset = subset->next) + rc_client_subset_raise_pending_events(client, subset); + + /* raise progress tracker events after achievement events so formatted values are updated for tracker event */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER) + rc_client_raise_progress_tracker_events(client, game); + + /* if any achievements were unlocked, resync the active achievements list */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS) { + rc_mutex_lock(&client->state.mutex); + rc_client_update_active_achievements(game); + rc_mutex_unlock(&client->state.mutex); + } + + game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE; +} + +void rc_client_do_frame(rc_client_t* client) +{ + if (!client) + return; + + if (client->game && !client->game->waiting_for_reset) { + rc_runtime_richpresence_t* richpresence; + rc_client_subset_info_t* subset; + + rc_mutex_lock(&client->state.mutex); + + rc_client_reset_pending_events(client); + + rc_client_update_memref_values(client); + rc_update_variables(client->game->runtime.variables, client->state.legacy_peek, client, NULL); + + client->game->progress_tracker.progress = 0.0; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active) + rc_client_do_frame_process_achievements(client, subset); + } + if (client->game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER) + rc_client_do_frame_update_progress_tracker(client, client->game); + + if (client->state.hardcore) { + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active) + rc_client_do_frame_process_leaderboards(client, subset); + } + } + + richpresence = client->game->runtime.richpresence; + if (richpresence && richpresence->richpresence) + rc_update_richpresence(richpresence->richpresence, client->state.legacy_peek, client, NULL); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); + } + + rc_client_idle(client); +} + +void rc_client_idle(rc_client_t* client) +{ + rc_client_scheduled_callback_data_t* scheduled_callback; + + if (!client) + return; + + scheduled_callback = client->state.scheduled_callbacks; + if (scheduled_callback) { + const clock_t now = clock(); + + do { + rc_mutex_lock(&client->state.mutex); + scheduled_callback = client->state.scheduled_callbacks; + if (scheduled_callback) { + if (RC_CLIENT_CLOCK_IS_BEFORE(now, scheduled_callback->when)) { + /* not time for next callback yet, ignore it */ + scheduled_callback = NULL; + } + else { + /* remove the callback from the queue while we process it. callback can requeue if desired */ + client->state.scheduled_callbacks = scheduled_callback->next; + } + } + rc_mutex_unlock(&client->state.mutex); + + if (!scheduled_callback) + break; + + scheduled_callback->callback(scheduled_callback, client, now); + } while (1); + } +} + +void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback) +{ + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + rc_mutex_lock(&client->state.mutex); + + last = &client->state.scheduled_callbacks; + do { + next = *last; + if (!next || RC_CLIENT_CLOCK_IS_BEFORE(scheduled_callback->when, next->when)) { + scheduled_callback->next = next; + *last = scheduled_callback; + break; + } + + last = &next->next; + } while (1); + + rc_mutex_unlock(&client->state.mutex); +} + +static void rc_client_reschedule_callback(rc_client_t* client, + rc_client_scheduled_callback_data_t* callback, clock_t when) +{ + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + /* ASSERT: this should only be called if the mutex is held */ + + callback->when = when; + + last = &client->state.scheduled_callbacks; + do { + next = *last; + + if (next == callback) { + if (when == 0) { + /* request to unschedule the callback */ + *last = next->next; + next->next = NULL; + break; + } + + if (!next->next) { + /* end of list, just append it */ + break; + } + + if (RC_CLIENT_CLOCK_IS_BEFORE(when, next->next->when)) { + /* already in the correct place */ + break; + } + + /* remove from current position - will insert later */ + *last = next->next; + next->next = NULL; + continue; + } + + if (!next || RC_CLIENT_CLOCK_IS_BEFORE(when, next->when)) { + /* insert here */ + callback->next = next; + *last = callback; + break; + } + + last = &next->next; + } while (1); +} + +static void rc_client_reset_richpresence(rc_client_t* client) +{ + rc_runtime_richpresence_t* richpresence = client->game->runtime.richpresence; + if (richpresence && richpresence->richpresence) + rc_reset_richpresence(richpresence->richpresence); +} + +static void rc_client_reset_variables(rc_client_t* client) +{ + rc_value_t* variable = client->game->runtime.variables; + for (; variable; variable = variable->next) + rc_reset_value(variable); +} + +static void rc_client_reset_all(rc_client_t* client) +{ + rc_client_reset_achievements(client); + rc_client_reset_leaderboards(client); + rc_client_reset_richpresence(client); + rc_client_reset_variables(client); +} + +void rc_client_reset(rc_client_t* client) +{ + rc_client_game_hash_t* game_hash; + if (!client || !client->game) + return; + + game_hash = rc_client_find_game_hash(client, client->game->public_.hash); + if (game_hash && game_hash->game_id != client->game->public_.id) { + /* current media is not for loaded game. unload game */ + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling runtime. Reset with non-game media loaded: %u (%s)", + (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) ? 0 : game_hash->game_id, game_hash->hash); + rc_client_unload_game(client); + return; + } + + RC_CLIENT_LOG_INFO(client, "Resetting runtime"); + + rc_mutex_lock(&client->state.mutex); + + client->game->waiting_for_reset = 0; + rc_client_reset_pending_events(client); + + rc_client_hide_progress_tracker(client, client->game); + rc_client_reset_all(client); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); +} + +size_t rc_client_progress_size(rc_client_t* client) +{ + size_t result; + + if (!client || !client->game) + return 0; + + rc_mutex_lock(&client->state.mutex); + result = rc_runtime_progress_size(&client->game->runtime, NULL); + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +int rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer) +{ + int result; + + if (!client || !client->game) + return RC_NO_GAME_LOADED; + + if (!buffer) + return RC_INVALID_STATE; + + rc_mutex_lock(&client->state.mutex); + result = rc_runtime_serialize_progress(buffer, &client->game->runtime, NULL); + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +static void rc_client_subset_before_deserialize_progress(rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + + /* flag any visible challenge indicators to be hidden */ + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (trigger && trigger->state == RC_TRIGGER_STATE_PRIMED && + achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + + /* flag any visible trackers to be hidden */ + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (lboard && lboard->state == RC_LBOARD_STATE_STARTED && + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) { + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD; + } + } +} + +static void rc_client_subset_after_deserialize_progress(rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + + /* flag any challenge indicators that should be shown */ + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + if (trigger->state == RC_TRIGGER_STATE_PRIMED) { + /* if it's already shown, just keep it. otherwise flag it to be shown */ + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) { + achievement->pending_events &= ~RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + } + else { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + /* ASSERT: only active achievements are serialized, so we don't have to worry about + * deserialization deactiving them. */ + } + + /* flag any trackers that need to be shown */ + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (!lboard || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_INACTIVE || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED) + continue; + + if (lboard->state == RC_LBOARD_STATE_STARTED) { + leaderboard->value = (int)lboard->value.value.value; + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; + + /* if it's already being tracked, just update tracker. otherwise, allocate one */ + if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + rc_client_update_leaderboard_tracker(game, leaderboard); + } + else { + rc_client_allocate_leaderboard_tracker(game, leaderboard); + } + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + /* deallocate the tracker (don't actually raise the failed event) */ + leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + rc_client_release_leaderboard_tracker(game, leaderboard); + } + } +} + +int rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client || !client->game) + return RC_NO_GAME_LOADED; + + rc_mutex_lock(&client->state.mutex); + + rc_client_reset_pending_events(client); + + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_before_deserialize_progress(subset); + + rc_client_hide_progress_tracker(client, client->game); + + if (!serialized) { + rc_client_reset_all(client); + result = RC_OK; + } + else { + result = rc_runtime_deserialize_progress(&client->game->runtime, serialized, NULL); + } + + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_after_deserialize_progress(client->game, subset); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); + + return result; +} + +/* ===== Toggles ===== */ + +static void rc_client_enable_hardcore(rc_client_t* client) +{ + client->state.hardcore = 1; + + if (client->game) { + rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE); + rc_client_activate_leaderboards(client->game, client); + + /* disable processing until the client acknowledges the reset event by calling rc_runtime_reset() */ + RC_CLIENT_LOG_INFO(client, "Hardcore enabled, waiting for reset"); + client->game->waiting_for_reset = 1; + } + else { + RC_CLIENT_LOG_INFO(client, "Hardcore enabled"); + } +} + +static void rc_client_disable_hardcore(rc_client_t* client) +{ + client->state.hardcore = 0; + RC_CLIENT_LOG_INFO(client, "Hardcore disabled"); + + if (client->game) { + rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + rc_client_deactivate_leaderboards(client->game, client); + } +} + +void rc_client_set_hardcore_enabled(rc_client_t* client, int enabled) +{ + int changed = 0; + + if (!client) + return; + + rc_mutex_lock(&client->state.mutex); + + enabled = enabled ? 1 : 0; + if (client->state.hardcore != enabled) { + if (enabled) + rc_client_enable_hardcore(client); + else + rc_client_disable_hardcore(client); + + changed = 1; + } + + rc_mutex_unlock(&client->state.mutex); + + /* events must be raised outside of lock */ + if (changed && client->game) { + if (enabled) { + /* if enabling hardcore, notify client that a reset is requested */ + if (client->game->waiting_for_reset) { + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_RESET; + client->callbacks.event_handler(&client_event, client); + } + } + else { + /* if disabling hardcore, leaderboards will be deactivated. raise events for hiding trackers */ + rc_client_raise_pending_events(client, client->game); + } + } +} + +int rc_client_get_hardcore_enabled(const rc_client_t* client) +{ + return client && client->state.hardcore; +} + +void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled) +{ + if (client) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unofficial %s", enabled ? "enabled" : "disabled"); + client->state.unofficial_enabled = enabled ? 1 : 0; + } +} + +int rc_client_get_unofficial_enabled(const rc_client_t* client) +{ + return client && client->state.unofficial_enabled; +} + +void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled) +{ + if (client) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Encore mode %s", enabled ? "enabled" : "disabled"); + client->state.encore_mode = enabled ? 1 : 0; + } +} + +int rc_client_get_encore_mode_enabled(const rc_client_t* client) +{ + return client && client->state.encore_mode; +} + +void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled) +{ + if (client) { + if (!enabled && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) { + RC_CLIENT_LOG_WARN(client, "Spectator mode cannot be disabled if it was enabled prior to loading game."); + return; + } + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectator mode %s", enabled ? "enabled" : "disabled"); + client->state.spectator_mode = enabled ? RC_CLIENT_SPECTATOR_MODE_ON : RC_CLIENT_SPECTATOR_MODE_OFF; + } +} + +int rc_client_get_spectator_mode_enabled(const rc_client_t* client) +{ + return client && (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) ? 0 : 1; +} + +void rc_client_set_userdata(rc_client_t* client, void* userdata) +{ + if (client) + client->callbacks.client_data = userdata; +} + +void* rc_client_get_userdata(const rc_client_t* client) +{ + return client ? client->callbacks.client_data : NULL; +} + +void rc_client_set_host(const rc_client_t* client, const char* hostname) +{ + /* if empty, just pass NULL */ + if (hostname && !hostname[0]) + hostname = NULL; + + /* clear the image host so it'll use the custom host for images too */ + rc_api_set_image_host(NULL); + + /* set the custom host */ + if (hostname && client) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Using host: %s", hostname); + } + rc_api_set_host(hostname); +} diff --git a/dep/rcheevos/src/rcheevos/rc_client_internal.h b/dep/rcheevos/src/rcheevos/rc_client_internal.h new file mode 100644 index 000000000..9d0dd9f99 --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_client_internal.h @@ -0,0 +1,290 @@ +#ifndef RC_CLIENT_INTERNAL_H +#define RC_CLIENT_INTERNAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "rc_client.h" + +#include "rc_compat.h" +#include "rc_runtime.h" +#include "rc_runtime_types.h" + +typedef struct rc_client_callbacks_t { + rc_client_read_memory_func_t read_memory; + rc_client_event_handler_t event_handler; + rc_client_server_call_t server_call; + rc_client_message_callback_t log_call; + + void* client_data; +} rc_client_callbacks_t; + +struct rc_client_scheduled_callback_data_t; +typedef void (*rc_client_scheduled_callback_t)(struct rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, clock_t now); + +typedef struct rc_client_scheduled_callback_data_t +{ + clock_t when; + unsigned related_id; + rc_client_scheduled_callback_t callback; + void* data; + struct rc_client_scheduled_callback_data_t* next; +} rc_client_scheduled_callback_data_t; + +void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback); + +enum { + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE = 0, + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED = (1 << 1), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW = (1 << 2), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE = (1 << 3), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_UPDATE = (1 << 4) /* not a real event, just triggers update */ +}; + +typedef struct rc_client_achievement_info_t { + rc_client_achievement_t public_; + + rc_trigger_t* trigger; + uint8_t md5[16]; + + time_t unlock_time_hardcore; + time_t unlock_time_softcore; + + uint8_t pending_events; + + const char* author; + time_t created_time; + time_t updated_time; +} rc_client_achievement_info_t; + +enum { + RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE, + RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW, + RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE, + RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE +}; + +typedef struct rc_client_progress_tracker_t { + rc_client_achievement_info_t* achievement; + float progress; + + rc_client_scheduled_callback_data_t* hide_callback; + uint8_t action; +} rc_client_progress_tracker_t; + +enum { + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE = 0, + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE = (1 << 1), + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW = (1 << 2), + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE = (1 << 3) +}; + +typedef struct rc_client_leaderboard_tracker_info_t { + rc_client_leaderboard_tracker_t public_; + struct rc_client_leaderboard_tracker_info_t* next; + int raw_value; + + uint32_t value_djb2; + + uint8_t format; + uint8_t pending_events; + uint8_t reference_count; + uint8_t value_from_hits; +} rc_client_leaderboard_tracker_info_t; + +enum { + RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE = 0, + RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED = (1 << 1), + RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED = (1 << 2), + RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED = (1 << 3) +}; + +typedef struct rc_client_leaderboard_info_t { + rc_client_leaderboard_t public_; + + rc_lboard_t* lboard; + uint8_t md5[16]; + + rc_client_leaderboard_tracker_info_t* tracker; + + uint32_t value_djb2; + int value; + + uint8_t format; + uint8_t pending_events; + uint8_t bucket; + uint8_t hidden; +} rc_client_leaderboard_info_t; + +enum { + RC_CLIENT_SUBSET_PENDING_EVENT_NONE = 0, + RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT = (1 << 1), + RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD = (1 << 2) +}; + +enum { + RC_CLIENT_GAME_PENDING_EVENT_NONE = 0, + RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER = (1 << 1), + RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS = (1 << 2), + RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER = (1 << 3) +}; + +typedef struct rc_client_subset_info_t { + rc_client_subset_t public_; + + rc_client_achievement_info_t* achievements; + rc_client_leaderboard_info_t* leaderboards; + + struct rc_client_subset_info_t* next; + + const char* all_label; + const char* inactive_label; + const char* locked_label; + const char* unlocked_label; + const char* unofficial_label; + const char* unsupported_label; + + uint8_t active; + uint8_t mastery; + uint8_t pending_events; +} rc_client_subset_info_t; + +typedef struct rc_client_game_hash_t { + char hash[33]; + uint32_t game_id; + struct rc_client_game_hash_t* next; +} rc_client_game_hash_t; + +rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash); + +typedef struct rc_client_media_hash_t { + rc_client_game_hash_t* game_hash; + struct rc_client_media_hash_t* next; + uint32_t path_djb2; +} rc_client_media_hash_t; + +typedef struct rc_client_game_info_t { + rc_client_game_t public_; + rc_client_leaderboard_tracker_info_t* leaderboard_trackers; + rc_client_progress_tracker_t progress_tracker; + + rc_client_subset_info_t* subsets; + + rc_client_media_hash_t* media_hash; + + rc_runtime_t runtime; + + uint32_t max_valid_address; + + uint8_t waiting_for_reset; + uint8_t pending_events; + + rc_api_buffer_t buffer; +} rc_client_game_info_t; + +enum { + RC_CLIENT_LOAD_STATE_NONE, + RC_CLIENT_LOAD_STATE_IDENTIFYING_GAME, + RC_CLIENT_LOAD_STATE_AWAIT_LOGIN, + RC_CLIENT_LOAD_STATE_FETCHING_GAME_DATA, + RC_CLIENT_LOAD_STATE_STARTING_SESSION, + RC_CLIENT_LOAD_STATE_DONE, + RC_CLIENT_LOAD_STATE_UNKNOWN_GAME +}; + +enum { + RC_CLIENT_USER_STATE_NONE, + RC_CLIENT_USER_STATE_LOGIN_REQUESTED, + RC_CLIENT_USER_STATE_LOGGED_IN +}; + +enum { + RC_CLIENT_MASTERY_STATE_NONE, + RC_CLIENT_MASTERY_STATE_PENDING, + RC_CLIENT_MASTERY_STATE_SHOWN +}; + +enum { + RC_CLIENT_SPECTATOR_MODE_OFF, + RC_CLIENT_SPECTATOR_MODE_ON, + RC_CLIENT_SPECTATOR_MODE_LOCKED +}; + +struct rc_client_load_state_t; + +typedef struct rc_client_state_t { + rc_mutex_t mutex; + rc_api_buffer_t buffer; + + rc_client_scheduled_callback_data_t* scheduled_callbacks; + + uint8_t hardcore; + uint8_t encore_mode; + uint8_t spectator_mode; + uint8_t unofficial_enabled; + uint8_t log_level; + uint8_t user; + + struct rc_client_load_state_t* load; + rc_memref_t* processing_memref; + + rc_peek_t legacy_peek; +} rc_client_state_t; + +struct rc_client_t { + rc_client_game_info_t* game; + rc_client_game_hash_t* hashes; + + rc_client_user_t user; + + rc_client_callbacks_t callbacks; + + rc_client_state_t state; +}; + +#ifdef RC_NO_VARIADIC_MACROS + void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...); +#else + void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...); + #define RC_CLIENT_LOG_ERR_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_WARN_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_INFO_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_VERBOSE_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) rc_client_log_message_formatted(client, format, __VA_ARGS__); } +#endif + +void rc_client_log_message(const rc_client_t* client, const char* message); +#define RC_CLIENT_LOG_ERR(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_WARN(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_INFO(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_VERBOSE(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) rc_client_log_message(client, message); } + +/* internals pulled from runtime.c */ +void rc_runtime_checksum(const char* memaddr, unsigned char* md5); +int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref); +int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref); +/* end runtime.c internals */ + +/* helper functions for unit tests */ +struct rc_hash_iterator; +struct rc_hash_iterator* rc_client_get_load_state_hash_iterator(rc_client_t* client); +/* end helper functions for unit tests */ + +enum { + RC_CLIENT_LEGACY_PEEK_AUTO, + RC_CLIENT_LEGACY_PEEK_CONSTRUCTED, + RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS +}; + +void rc_client_set_legacy_peek(rc_client_t* client, int method); + +void rc_client_begin_load_subset(rc_client_t* client, uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_CLIENT_INTERNAL_H */ diff --git a/dep/rcheevos/src/rcheevos/rc_compat.h b/dep/rcheevos/src/rcheevos/rc_compat.h index 62c362f47..e22f9b84e 100644 --- a/dep/rcheevos/src/rcheevos/rc_compat.h +++ b/dep/rcheevos/src/rcheevos/rc_compat.h @@ -13,6 +13,8 @@ extern "C" { /* MinGW redefinitions */ +#define RC_NO_VARIADIC_MACROS 1 + #elif defined(_MSC_VER) /* Visual Studio redefinitions */ @@ -30,6 +32,9 @@ extern "C" { #elif __STDC_VERSION__ < 199901L /* C89 redefinitions */ +#define RC_C89_HELPERS 1 + +#define RC_NO_VARIADIC_MACROS 1 #ifndef snprintf extern int rc_snprintf(char* buffer, size_t size, const char* format, ...); @@ -53,6 +58,40 @@ extern "C" { #endif /* __STDC_VERSION__ < 199901L */ +#ifndef __STDC_WANT_SECURE_LIB__ + /* _CRT_SECURE_NO_WARNINGS redefinitions */ + #define strcpy_s(dest, sz, src) strcpy(dest, src) + #define sscanf_s sscanf + + /* NOTE: Microsoft secure gmtime_s parameter order differs from C11 standard */ + #include + extern struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer); + #define gmtime_s rc_gmtime_s +#endif + +#ifdef RC_NO_THREADS + typedef int rc_mutex_t; + + #define rc_mutex_init(mutex) + #define rc_mutex_destroy(mutex) + #define rc_mutex_lock(mutex) + #define rc_mutex_unlock(mutex) +#else + #ifdef _WIN32 + typedef struct rc_mutex_t { + void* handle; /* HANDLE is defined as "void*" */ + } rc_mutex_t; + #else + #include + typedef pthread_mutex_t rc_mutex_t; + #endif + + void rc_mutex_init(rc_mutex_t* mutex); + void rc_mutex_destroy(rc_mutex_t* mutex); + void rc_mutex_lock(rc_mutex_t* mutex); + void rc_mutex_unlock(rc_mutex_t* mutex); +#endif + #ifdef __cplusplus } #endif diff --git a/dep/rcheevos/src/rcheevos/rc_internal.h b/dep/rcheevos/src/rcheevos/rc_internal.h index c98f71309..e58ca53de 100644 --- a/dep/rcheevos/src/rcheevos/rc_internal.h +++ b/dep/rcheevos/src/rcheevos/rc_internal.h @@ -36,6 +36,9 @@ RC_ALLOW_ALIGN(char) #define RC_ALLOC(t, p) ((t*)rc_alloc((p)->buffer, &(p)->offset, sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) #define RC_ALLOC_SCRATCH(t, p) ((t*)rc_alloc_scratch((p)->buffer, &(p)->offset, sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) +/* force alignment to 4 bytes on 32-bit systems, or 8 bytes on 64-bit systems */ +#define RC_ALIGN(n) (((n) + (sizeof(void*)-1)) & ~(sizeof(void*)-1)) + typedef struct rc_scratch_buffer { struct rc_scratch_buffer* next; int offset; @@ -83,6 +86,8 @@ typedef struct { } rc_typed_value_t; +#define RC_MEASURED_UNKNOWN 0xFFFFFFFF + typedef struct { rc_typed_value_t add_value;/* AddSource/SubSource */ int add_hits; /* AddHits */ @@ -130,6 +135,8 @@ void* rc_alloc(void* pointer, int* offset, int size, int alignment, rc_scratch_t void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset); char* rc_alloc_str(rc_parse_state_t* parse, const char* text, int length); +unsigned rc_djb2(const char* input); + rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, unsigned address, char size, char is_indirect); int rc_parse_memref(const char** memaddr, char* size, unsigned* address); void rc_update_memref_values(rc_memref_t* memref, rc_peek_t peek, void* ud); @@ -138,6 +145,7 @@ unsigned rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_stat char rc_memref_shared_size(char size); unsigned rc_memref_mask(char size); void rc_transform_memref_value(rc_typed_value_t* value, char size); +unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* ud); void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_parse_state_t* parse); int rc_trigger_state_active(int state); @@ -146,6 +154,22 @@ rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, in int rc_test_condset(rc_condset_t* self, rc_eval_state_t* eval_state); void rc_reset_condset(rc_condset_t* self); +enum { + RC_PROCESSING_COMPARE_DEFAULT = 0, + RC_PROCESSING_COMPARE_MEMREF_TO_CONST, + RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, + RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, + RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, + RC_PROCESSING_COMPARE_DELTA_TO_CONST, + RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, + RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED, + RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, + RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED, + RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, + RC_PROCESSING_COMPARE_ALWAYS_TRUE, + RC_PROCESSING_COMPARE_ALWAYS_FALSE +}; + rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, int is_indirect); int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state); void rc_evaluate_condition_value(rc_typed_value_t* value, rc_condition_t* self, rc_eval_state_t* eval_state); @@ -154,10 +178,12 @@ int rc_condition_is_combining(const rc_condition_t* self); int rc_parse_operand(rc_operand_t* self, const char** memaddr, int is_indirect, rc_parse_state_t* parse); void rc_evaluate_operand(rc_typed_value_t* value, rc_operand_t* self, rc_eval_state_t* eval_state); int rc_operand_is_float_memref(const rc_operand_t* self); +int rc_operand_is_float(const rc_operand_t* self); void rc_parse_value_internal(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse); int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t peek, void* ud, lua_State* L); void rc_reset_value(rc_value_t* self); +int rc_value_from_hits(rc_value_t* self); rc_value_t* rc_alloc_helper_variable(const char* memaddr, int memaddr_len, rc_parse_state_t* parse); void rc_update_variables(rc_value_t* variable, rc_peek_t peek, void* ud, lua_State* L); @@ -165,6 +191,7 @@ void rc_typed_value_convert(rc_typed_value_t* value, char new_type); void rc_typed_value_add(rc_typed_value_t* value, const rc_typed_value_t* amount); void rc_typed_value_multiply(rc_typed_value_t* value, const rc_typed_value_t* amount); void rc_typed_value_divide(rc_typed_value_t* value, const rc_typed_value_t* amount); +void rc_typed_value_negate(rc_typed_value_t* value); int rc_typed_value_compare(const rc_typed_value_t* value1, const rc_typed_value_t* value2, char oper); void rc_typed_value_from_memref_value(rc_typed_value_t* value, const rc_memref_value_t* memref); diff --git a/dep/rcheevos/src/rcheevos/rc_libretro.c b/dep/rcheevos/src/rcheevos/rc_libretro.c new file mode 100644 index 000000000..eab44ef1b --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_libretro.c @@ -0,0 +1,816 @@ +/* This file provides a series of functions for integrating RetroAchievements with libretro. + * These functions will be called by a libretro frontend to validate certain expected behaviors + * and simplify mapping core data to the RAIntegration DLL. + * + * Originally designed to be shared between RALibretro and RetroArch, but will simplify + * integrating with any other frontends. + */ + +#include "rc_libretro.h" + +#include "rc_consoles.h" +#include "rc_compat.h" + +#include +#include + +/* internal helper functions in hash.c */ +extern void* rc_file_open(const char* path); +extern void rc_file_seek(void* file_handle, int64_t offset, int origin); +extern int64_t rc_file_tell(void* file_handle); +extern size_t rc_file_read(void* file_handle, void* buffer, int requested_bytes); +extern void rc_file_close(void* file_handle); +extern int rc_path_compare_extension(const char* path, const char* ext); +extern int rc_hash_error(const char* message); + + +static rc_libretro_message_callback rc_libretro_verbose_message_callback = NULL; + +/* a value that starts with a comma is a CSV. + * if it starts with an exclamation point, it's everything but the provided value. + * if it starts with an exclamntion point followed by a comma, it's everything but the CSV values. + * values are case-insensitive */ +typedef struct rc_disallowed_core_settings_t +{ + const char* library_name; + const rc_disallowed_setting_t* disallowed_settings; +} rc_disallowed_core_settings_t; + +static const rc_disallowed_setting_t _rc_disallowed_bsnes_settings[] = { + { "bsnes_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_cap32_settings[] = { + { "cap32_autorun", "disabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_dolphin_settings[] = { + { "dolphin_cheats_enabled", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_duckstation_settings[] = { + { "duckstation_CDROM.LoadImagePatches", "true" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_ecwolf_settings[] = { + { "ecwolf-invulnerability", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_fbneo_settings[] = { + { "fbneo-allow-patched-romsets", "enabled" }, + { "fbneo-cheat-*", "!,Disabled,0 - Disabled" }, + { "fbneo-dipswitch-*", "Universe BIOS*" }, + { "fbneo-neogeo-mode", "UNIBIOS" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_fceumm_settings[] = { + { "fceumm_region", ",PAL,Dendy" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_gpgx_settings[] = { + { "genesis_plus_gx_lock_on", ",action replay (pro),game genie" }, + { "genesis_plus_gx_region_detect", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_gpgx_wide_settings[] = { + { "genesis_plus_gx_wide_lock_on", ",action replay (pro),game genie" }, + { "genesis_plus_gx_wide_region_detect", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_mesen_settings[] = { + { "mesen_region", ",PAL,Dendy" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_mesen_s_settings[] = { + { "mesen-s_region", "PAL" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_pcsx_rearmed_settings[] = { + { "pcsx_rearmed_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_picodrive_settings[] = { + { "picodrive_region", ",Europe,Japan PAL" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_ppsspp_settings[] = { + { "ppsspp_cheats", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_quasi88_settings[] = { + { "q88_cpu_clock", ",1,2" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_smsplus_settings[] = { + { "smsplus_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_snes9x_settings[] = { + { "snes9x_gfx_clip", "disabled" }, + { "snes9x_gfx_transp", "disabled" }, + { "snes9x_layer_*", "disabled" }, + { "snes9x_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_vice_settings[] = { + { "vice_autostart", "disabled" }, /* autostart dictates initial load and reset from menu */ + { "vice_reset", "!autostart" }, /* reset dictates behavior when pressing reset button (END) */ + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_virtual_jaguar_settings[] = { + { "virtualjaguar_pal", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_core_settings_t rc_disallowed_core_settings[] = { + { "bsnes-mercury", _rc_disallowed_bsnes_settings }, + { "cap32", _rc_disallowed_cap32_settings }, + { "dolphin-emu", _rc_disallowed_dolphin_settings }, + { "DuckStation", _rc_disallowed_duckstation_settings }, + { "ecwolf", _rc_disallowed_ecwolf_settings }, + { "FCEUmm", _rc_disallowed_fceumm_settings }, + { "FinalBurn Neo", _rc_disallowed_fbneo_settings }, + { "Genesis Plus GX", _rc_disallowed_gpgx_settings }, + { "Genesis Plus GX Wide", _rc_disallowed_gpgx_wide_settings }, + { "Mesen", _rc_disallowed_mesen_settings }, + { "Mesen-S", _rc_disallowed_mesen_s_settings }, + { "PPSSPP", _rc_disallowed_ppsspp_settings }, + { "PCSX-ReARMed", _rc_disallowed_pcsx_rearmed_settings }, + { "PicoDrive", _rc_disallowed_picodrive_settings }, + { "QUASI88", _rc_disallowed_quasi88_settings }, + { "SMS Plus GX", _rc_disallowed_smsplus_settings }, + { "Snes9x", _rc_disallowed_snes9x_settings }, + { "VICE x64", _rc_disallowed_vice_settings }, + { "Virtual Jaguar", _rc_disallowed_virtual_jaguar_settings }, + { NULL, NULL } +}; + +static int rc_libretro_string_equal_nocase_wildcard(const char* test, const char* value) { + char c1, c2; + while ((c1 = *test++)) { + if (tolower(c1) != tolower(c2 = *value++)) + return (c2 == '*'); + } + + return (*value == '\0'); +} + +static int rc_libretro_match_value(const char* val, const char* match) { + /* if value starts with a comma, it's a CSV list of potential matches */ + if (*match == ',') { + do { + const char* ptr = ++match; + size_t size; + + while (*match && *match != ',') + ++match; + + size = match - ptr; + if (val[size] == '\0') { + if (memcmp(ptr, val, size) == 0) { + return 1; + } + else { + char buffer[128]; + memcpy(buffer, ptr, size); + buffer[size] = '\0'; + if (rc_libretro_string_equal_nocase_wildcard(buffer, val)) + return 1; + } + } + } while (*match == ','); + + return 0; + } + + /* a leading exclamation point means the provided value(s) are not forbidden (are allowed) */ + if (*match == '!') + return !rc_libretro_match_value(val, &match[1]); + + /* just a single value, attempt to match it */ + return rc_libretro_string_equal_nocase_wildcard(val, match); +} + +int rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value) { + const char* key; + size_t key_len; + + for (; disallowed_settings->setting; ++disallowed_settings) { + key = disallowed_settings->setting; + key_len = strlen(key); + + if (key[key_len - 1] == '*') { + if (memcmp(setting, key, key_len - 1) == 0) { + if (rc_libretro_match_value(value, disallowed_settings->value)) + return 0; + } + } + else { + if (memcmp(setting, key, key_len + 1) == 0) { + if (rc_libretro_match_value(value, disallowed_settings->value)) + return 0; + } + } + } + + return 1; +} + +const rc_disallowed_setting_t* rc_libretro_get_disallowed_settings(const char* library_name) { + const rc_disallowed_core_settings_t* core_filter = rc_disallowed_core_settings; + size_t library_name_length; + + if (!library_name || !library_name[0]) + return NULL; + + library_name_length = strlen(library_name) + 1; + while (core_filter->library_name) { + if (memcmp(core_filter->library_name, library_name, library_name_length) == 0) + return core_filter->disallowed_settings; + + ++core_filter; + } + + return NULL; +} + +typedef struct rc_disallowed_core_systems_t +{ + const char* library_name; + const int disallowed_consoles[4]; +} rc_disallowed_core_systems_t; + +static const rc_disallowed_core_systems_t rc_disallowed_core_systems[] = { + /* https://github.com/libretro/Mesen-S/issues/8 */ + { "Mesen-S", { RC_CONSOLE_GAMEBOY, RC_CONSOLE_GAMEBOY_COLOR, 0 }}, + { NULL, { 0 } } +}; + +int rc_libretro_is_system_allowed(const char* library_name, int console_id) { + const rc_disallowed_core_systems_t* core_filter = rc_disallowed_core_systems; + size_t library_name_length; + size_t i; + + if (!library_name || !library_name[0]) + return 1; + + library_name_length = strlen(library_name) + 1; + while (core_filter->library_name) { + if (memcmp(core_filter->library_name, library_name, library_name_length) == 0) { + for (i = 0; i < sizeof(core_filter->disallowed_consoles) / sizeof(core_filter->disallowed_consoles[0]); ++i) { + if (core_filter->disallowed_consoles[i] == console_id) + return 0; + } + break; + } + + ++core_filter; + } + + return 1; +} + +unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, unsigned address, unsigned* avail) { + unsigned i; + + for (i = 0; i < regions->count; ++i) { + const size_t size = regions->size[i]; + if (address < size) { + if (regions->data[i] == NULL) + break; + + if (avail) + *avail = (unsigned)(size - address); + + return ®ions->data[i][address]; + } + + address -= (unsigned)size; + } + + if (avail) + *avail = 0; + + return NULL; +} + +unsigned char* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, unsigned address) { + return rc_libretro_memory_find_avail(regions, address, NULL); +} + +uint32_t rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, unsigned address, + uint8_t* buffer, uint32_t num_bytes) { + unsigned i; + uint32_t avail; + + for (i = 0; i < regions->count; ++i) { + const size_t size = regions->size[i]; + if (address < size) { + if (regions->data[i] == NULL) + break; + + avail = (unsigned)(size - address); + if (avail < num_bytes) + return avail; + + memcpy(buffer, ®ions->data[i][address], num_bytes); + return num_bytes; + } + + address -= (unsigned)size; + } + + return 0; +} + +void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback) { + rc_libretro_verbose_message_callback = callback; +} + +static void rc_libretro_verbose(const char* message) { + if (rc_libretro_verbose_message_callback) + rc_libretro_verbose_message_callback(message); +} + +static const char* rc_memory_type_str(int type) { + switch (type) + { + case RC_MEMORY_TYPE_SAVE_RAM: + return "SRAM"; + case RC_MEMORY_TYPE_VIDEO_RAM: + return "VRAM"; + case RC_MEMORY_TYPE_UNUSED: + return "UNUSED"; + default: + break; + } + + return "SYSTEM RAM"; +} + +static void rc_libretro_memory_register_region(rc_libretro_memory_regions_t* regions, int type, + unsigned char* data, size_t size, const char* description) { + if (size == 0) + return; + + if (regions->count == (sizeof(regions->size) / sizeof(regions->size[0]))) { + rc_libretro_verbose("Too many memory memory regions to register"); + return; + } + + if (!data && regions->count > 0 && !regions->data[regions->count - 1]) { + /* extend null region */ + regions->size[regions->count - 1] += size; + } + else if (data && regions->count > 0 && + data == (regions->data[regions->count - 1] + regions->size[regions->count - 1])) { + /* extend non-null region */ + regions->size[regions->count - 1] += size; + } + else { + /* create new region */ + regions->data[regions->count] = data; + regions->size[regions->count] = size; + ++regions->count; + } + + regions->total_size += size; + + if (rc_libretro_verbose_message_callback) { + char message[128]; + snprintf(message, sizeof(message), "Registered 0x%04X bytes of %s at $%06X (%s)", (unsigned)size, + rc_memory_type_str(type), (unsigned)(regions->total_size - size), description); + rc_libretro_verbose_message_callback(message); + } +} + +static void rc_libretro_memory_init_without_regions(rc_libretro_memory_regions_t* regions, + rc_libretro_get_core_memory_info_func get_core_memory_info) { + /* no regions specified, assume system RAM followed by save RAM */ + char description[64]; + rc_libretro_core_memory_info_t info; + + snprintf(description, sizeof(description), "offset 0x%06x", 0); + + get_core_memory_info(RETRO_MEMORY_SYSTEM_RAM, &info); + if (info.size) + rc_libretro_memory_register_region(regions, RC_MEMORY_TYPE_SYSTEM_RAM, info.data, info.size, description); + + get_core_memory_info(RETRO_MEMORY_SAVE_RAM, &info); + if (info.size) + rc_libretro_memory_register_region(regions, RC_MEMORY_TYPE_SAVE_RAM, info.data, info.size, description); +} + +static const struct retro_memory_descriptor* rc_libretro_memory_get_descriptor(const struct retro_memory_map* mmap, unsigned real_address, size_t* offset) +{ + const struct retro_memory_descriptor* desc = mmap->descriptors; + const struct retro_memory_descriptor* end = desc + mmap->num_descriptors; + + for (; desc < end; desc++) { + if (desc->select == 0) { + /* if select is 0, attempt to explcitly match the address */ + if (real_address >= desc->start && real_address < desc->start + desc->len) { + *offset = real_address - desc->start; + return desc; + } + } + else { + /* otherwise, attempt to match the address by matching the select bits */ + /* address is in the block if (addr & select) == (start & select) */ + if (((desc->start ^ real_address) & desc->select) == 0) { + /* get the relative offset of the address from the start of the memory block */ + unsigned reduced_address = real_address - (unsigned)desc->start; + + /* remove any bits from the reduced_address that correspond to the bits in the disconnect + * mask and collapse the remaining bits. this code was copied from the mmap_reduce function + * in RetroArch. i'm not exactly sure how it works, but it does. */ + unsigned disconnect_mask = (unsigned)desc->disconnect; + while (disconnect_mask) { + const unsigned tmp = (disconnect_mask - 1) & ~disconnect_mask; + reduced_address = (reduced_address & tmp) | ((reduced_address >> 1) & ~tmp); + disconnect_mask = (disconnect_mask & (disconnect_mask - 1)) >> 1; + } + + /* calculate the offset within the descriptor */ + *offset = reduced_address; + + /* sanity check - make sure the descriptor is large enough to hold the target address */ + if (reduced_address < desc->len) + return desc; + } + } + } + + *offset = 0; + return NULL; +} + +static void rc_libretro_memory_init_from_memory_map(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, + const rc_memory_regions_t* console_regions) { + char description[64]; + unsigned i; + unsigned char* region_start; + unsigned char* desc_start; + size_t desc_size; + size_t offset; + + for (i = 0; i < console_regions->num_regions; ++i) { + const rc_memory_region_t* console_region = &console_regions->region[i]; + size_t console_region_size = console_region->end_address - console_region->start_address + 1; + unsigned real_address = console_region->real_address; + unsigned disconnect_size = 0; + + while (console_region_size > 0) { + const struct retro_memory_descriptor* desc = rc_libretro_memory_get_descriptor(mmap, real_address, &offset); + if (!desc) { + if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { + snprintf(description, sizeof(description), "Could not map region starting at $%06X", + real_address - console_region->real_address + console_region->start_address); + rc_libretro_verbose(description); + } + + if (disconnect_size && console_region_size > disconnect_size) { + rc_libretro_memory_register_region(regions, console_region->type, NULL, disconnect_size, "null filler"); + console_region_size -= disconnect_size; + real_address += disconnect_size; + disconnect_size = 0; + continue; + } + + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size, "null filler"); + break; + } + + snprintf(description, sizeof(description), "descriptor %u, offset 0x%06X%s", + (unsigned)(desc - mmap->descriptors) + 1, (int)offset, desc->ptr ? "" : " [no pointer]"); + + if (desc->ptr) { + desc_start = (uint8_t*)desc->ptr + desc->offset; + region_start = desc_start + offset; + } + else { + region_start = NULL; + } + + desc_size = desc->len - offset; + if (desc->disconnect && desc_size > desc->disconnect) { + /* if we need to extract a disconnect bit, the largest block we can read is up to + * the next time that bit flips */ + /* https://stackoverflow.com/questions/12247186/find-the-lowest-set-bit */ + disconnect_size = (desc->disconnect & -((int)desc->disconnect)); + desc_size = disconnect_size - (real_address & (disconnect_size - 1)); + } + + if (console_region_size > desc_size) { + if (desc_size == 0) { + if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { + snprintf(description, sizeof(description), "Could not map region starting at $%06X", + real_address - console_region->real_address + console_region->start_address); + rc_libretro_verbose(description); + } + + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size, "null filler"); + console_region_size = 0; + } + else { + rc_libretro_memory_register_region(regions, console_region->type, region_start, desc_size, description); + console_region_size -= desc_size; + real_address += (unsigned)desc_size; + } + } + else { + rc_libretro_memory_register_region(regions, console_region->type, region_start, console_region_size, description); + console_region_size = 0; + } + } + } +} + +static unsigned rc_libretro_memory_console_region_to_ram_type(int region_type) { + switch (region_type) + { + case RC_MEMORY_TYPE_SAVE_RAM: + return RETRO_MEMORY_SAVE_RAM; + case RC_MEMORY_TYPE_VIDEO_RAM: + return RETRO_MEMORY_VIDEO_RAM; + default: + break; + } + + return RETRO_MEMORY_SYSTEM_RAM; +} + +static void rc_libretro_memory_init_from_unmapped_memory(rc_libretro_memory_regions_t* regions, + rc_libretro_get_core_memory_info_func get_core_memory_info, const rc_memory_regions_t* console_regions) { + char description[64]; + unsigned i, j; + rc_libretro_core_memory_info_t info; + size_t offset; + + for (i = 0; i < console_regions->num_regions; ++i) { + const rc_memory_region_t* console_region = &console_regions->region[i]; + const size_t console_region_size = console_region->end_address - console_region->start_address + 1; + const unsigned type = rc_libretro_memory_console_region_to_ram_type(console_region->type); + unsigned base_address = 0; + + for (j = 0; j <= i; ++j) { + const rc_memory_region_t* console_region2 = &console_regions->region[j]; + if (rc_libretro_memory_console_region_to_ram_type(console_region2->type) == type) { + base_address = console_region2->start_address; + break; + } + } + offset = console_region->start_address - base_address; + + get_core_memory_info(type, &info); + + if (offset < info.size) { + info.size -= offset; + + if (info.data) { + snprintf(description, sizeof(description), "offset 0x%06X", (int)offset); + info.data += offset; + } + else { + snprintf(description, sizeof(description), "null filler"); + } + } + else { + if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { + snprintf(description, sizeof(description), "Could not map region starting at $%06X", console_region->start_address); + rc_libretro_verbose(description); + } + + info.data = NULL; + info.size = 0; + } + + if (console_region_size > info.size) { + /* want more than what is available, take what we can and null fill the rest */ + rc_libretro_memory_register_region(regions, console_region->type, info.data, info.size, description); + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size - info.size, "null filler"); + } + else { + /* only take as much as we need */ + rc_libretro_memory_register_region(regions, console_region->type, info.data, console_region_size, description); + } + } +} + +int rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, + rc_libretro_get_core_memory_info_func get_core_memory_info, int console_id) { + const rc_memory_regions_t* console_regions = rc_console_memory_regions(console_id); + rc_libretro_memory_regions_t new_regions; + int has_valid_region = 0; + unsigned i; + + if (!regions) + return 0; + + memset(&new_regions, 0, sizeof(new_regions)); + + if (console_regions == NULL || console_regions->num_regions == 0) + rc_libretro_memory_init_without_regions(&new_regions, get_core_memory_info); + else if (mmap && mmap->num_descriptors != 0) + rc_libretro_memory_init_from_memory_map(&new_regions, mmap, console_regions); + else + rc_libretro_memory_init_from_unmapped_memory(&new_regions, get_core_memory_info, console_regions); + + /* determine if any valid regions were found */ + for (i = 0; i < new_regions.count; i++) { + if (new_regions.data[i]) { + has_valid_region = 1; + break; + } + } + + memcpy(regions, &new_regions, sizeof(*regions)); + return has_valid_region; +} + +void rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions) { + memset(regions, 0, sizeof(*regions)); +} + +void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, + const char* m3u_path, rc_libretro_get_image_path_func get_image_path) { + char image_path[1024]; + char* m3u_contents; + char* ptr; + int64_t file_len; + void* file_handle; + int index = 0; + + memset(hash_set, 0, sizeof(*hash_set)); + + if (!rc_path_compare_extension(m3u_path, "m3u")) + return; + + file_handle = rc_file_open(m3u_path); + if (!file_handle) + { + rc_hash_error("Could not open playlist"); + return; + } + + rc_file_seek(file_handle, 0, SEEK_END); + file_len = rc_file_tell(file_handle); + rc_file_seek(file_handle, 0, SEEK_SET); + + m3u_contents = (char*)malloc((size_t)file_len + 1); + rc_file_read(file_handle, m3u_contents, (int)file_len); + m3u_contents[file_len] = '\0'; + + rc_file_close(file_handle); + + ptr = m3u_contents; + do + { + /* ignore whitespace */ + while (isspace((int)*ptr)) + ++ptr; + + if (*ptr == '#') + { + /* ignore comment unless it's the special SAVEDISK extension */ + if (memcmp(ptr, "#SAVEDISK:", 10) == 0) + { + /* get the path to the save disk from the frontend, assign it a bogus hash so + * it doesn't get hashed later */ + if (get_image_path(index, image_path, sizeof(image_path))) + { + const char save_disk_hash[33] = "[SAVE DISK]"; + rc_libretro_hash_set_add(hash_set, image_path, -1, save_disk_hash); + ++index; + } + } + } + else + { + /* non-empty line, tally a file */ + ++index; + } + + /* find the end of the line */ + while (*ptr && *ptr != '\n') + ++ptr; + + } while (*ptr); + + free(m3u_contents); + + if (hash_set->entries_count > 0) + { + /* at least one save disk was found. make sure the core supports the #SAVEDISK: extension by + * asking for the last expected disk. if it's not found, assume no #SAVEDISK: support */ + if (!get_image_path(index - 1, image_path, sizeof(image_path))) + hash_set->entries_count = 0; + } +} + +void rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set) { + if (hash_set->entries) + free(hash_set->entries); + memset(hash_set, 0, sizeof(*hash_set)); +} + +static unsigned rc_libretro_djb2(const char* input) +{ + unsigned result = 5381; + char c; + + while ((c = *input++) != '\0') + result = ((result << 5) + result) + c; /* result = result * 33 + c */ + + return result; +} + +void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, + const char* path, int game_id, const char hash[33]) { + const unsigned path_djb2 = (path != NULL) ? rc_libretro_djb2(path) : 0; + struct rc_libretro_hash_entry_t* entry = NULL; + struct rc_libretro_hash_entry_t* scan; + struct rc_libretro_hash_entry_t* stop = hash_set->entries + hash_set->entries_count;; + + if (path_djb2) + { + /* attempt to match the path */ + for (scan = hash_set->entries; scan < stop; ++scan) + { + if (scan->path_djb2 == path_djb2) + { + entry = scan; + break; + } + } + } + + if (!entry) + { + /* entry not found, allocate a new one */ + if (hash_set->entries_size == 0) + { + hash_set->entries_size = 4; + hash_set->entries = (struct rc_libretro_hash_entry_t*) + malloc(hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t)); + } + else if (hash_set->entries_count == hash_set->entries_size) + { + hash_set->entries_size += 4; + hash_set->entries = (struct rc_libretro_hash_entry_t*)realloc(hash_set->entries, + hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t)); + } + + entry = hash_set->entries + hash_set->entries_count++; + } + + /* update the entry */ + entry->path_djb2 = path_djb2; + entry->game_id = game_id; + memcpy(entry->hash, hash, sizeof(entry->hash)); +} + +const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path) +{ + const unsigned path_djb2 = rc_libretro_djb2(path); + struct rc_libretro_hash_entry_t* scan = hash_set->entries; + struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count; + for (; scan < stop; ++scan) + { + if (scan->path_djb2 == path_djb2) + return scan->hash; + } + + return NULL; +} + +int rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash) +{ + struct rc_libretro_hash_entry_t* scan = hash_set->entries; + struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count; + for (; scan < stop; ++scan) + { + if (memcmp(scan->hash, hash, sizeof(scan->hash)) == 0) + return scan->game_id; + } + + return 0; +} diff --git a/dep/rcheevos/src/rcheevos/rc_libretro.h b/dep/rcheevos/src/rcheevos/rc_libretro.h new file mode 100644 index 000000000..089631bae --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_libretro.h @@ -0,0 +1,92 @@ +#ifndef RC_LIBRETRO_H +#define RC_LIBRETRO_H + +/* this file comes from the libretro repository, which is not an explicit submodule. + * the integration must set up paths appropriately to find it. */ +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/*****************************************************************************\ +| Disallowed Settings | +\*****************************************************************************/ + +typedef struct rc_disallowed_setting_t +{ + const char* setting; + const char* value; +} rc_disallowed_setting_t; + +const rc_disallowed_setting_t* rc_libretro_get_disallowed_settings(const char* library_name); +int rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value); +int rc_libretro_is_system_allowed(const char* library_name, int console_id); + +/*****************************************************************************\ +| Memory Mapping | +\*****************************************************************************/ + +/* specifies a function to call for verbose logging */ +typedef void (*rc_libretro_message_callback)(const char*); +void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback); + +#define RC_LIBRETRO_MAX_MEMORY_REGIONS 32 +typedef struct rc_libretro_memory_regions_t +{ + unsigned char* data[RC_LIBRETRO_MAX_MEMORY_REGIONS]; + size_t size[RC_LIBRETRO_MAX_MEMORY_REGIONS]; + size_t total_size; + unsigned count; +} rc_libretro_memory_regions_t; + +typedef struct rc_libretro_core_memory_info_t +{ + unsigned char* data; + size_t size; +} rc_libretro_core_memory_info_t; + +typedef void (*rc_libretro_get_core_memory_info_func)(unsigned id, rc_libretro_core_memory_info_t* info); + +int rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, + rc_libretro_get_core_memory_info_func get_core_memory_info, int console_id); +void rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions); + +unsigned char* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, unsigned address); +unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, unsigned address, unsigned* avail); +uint32_t rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, unsigned address, uint8_t* buffer, uint32_t num_bytes); + +/*****************************************************************************\ +| Disk Identification | +\*****************************************************************************/ + +typedef struct rc_libretro_hash_entry_t +{ + uint32_t path_djb2; + int game_id; + char hash[33]; +} rc_libretro_hash_entry_t; + +typedef struct rc_libretro_hash_set_t +{ + struct rc_libretro_hash_entry_t* entries; + uint16_t entries_count; + uint16_t entries_size; +} rc_libretro_hash_set_t; + +typedef int (*rc_libretro_get_image_path_func)(unsigned index, char* buffer, size_t buffer_size); + +void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, + const char* m3u_path, rc_libretro_get_image_path_func get_image_path); +void rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set); + +void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, + const char* path, int game_id, const char hash[33]); +const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path); +int rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_LIBRETRO_H */ diff --git a/dep/rcheevos/src/rcheevos/rc_validate.c b/dep/rcheevos/src/rcheevos/rc_validate.c new file mode 100644 index 000000000..1751b890e --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_validate.c @@ -0,0 +1,847 @@ +#include "rc_validate.h" + +#include "rc_compat.h" +#include "rc_consoles.h" +#include "rc_internal.h" + +#include +#include + +static int rc_validate_memref(const rc_memref_t* memref, char result[], const size_t result_size, int console_id, unsigned max_address) +{ + if (memref->address > max_address) { + snprintf(result, result_size, "Address %04X out of range (max %04X)", memref->address, max_address); + return 0; + } + + switch (console_id) { + case RC_CONSOLE_NINTENDO: + if (memref->address >= 0x0800 && memref->address <= 0x1FFF) { + snprintf(result, result_size, "Mirror RAM may not be exposed by emulator (address %04X)", memref->address); + return 0; + } + break; + + case RC_CONSOLE_GAMEBOY: + case RC_CONSOLE_GAMEBOY_COLOR: + if (memref->address >= 0xE000 && memref->address <= 0xFDFF) { + snprintf(result, result_size, "Echo RAM may not be exposed by emulator (address %04X)", memref->address); + return 0; + } + break; + + case RC_CONSOLE_PLAYSTATION: + if (memref->address <= 0xFFFF) { + snprintf(result, result_size, "Kernel RAM may not be initialized without real BIOS (address %04X)", memref->address); + return 0; + } + break; + } + + return 1; +} + +int rc_validate_memrefs(const rc_memref_t* memref, char result[], const size_t result_size, unsigned max_address) +{ + while (memref) { + if (!rc_validate_memref(memref, result, result_size, 0, max_address)) + return 0; + + memref = memref->next; + } + + return 1; +} + +static unsigned rc_console_max_address(int console_id) +{ + const rc_memory_regions_t* memory_regions; + memory_regions = rc_console_memory_regions(console_id); + if (memory_regions && memory_regions->num_regions > 0) + return memory_regions->region[memory_regions->num_regions - 1].end_address; + + return 0xFFFFFFFF; +} + +int rc_validate_memrefs_for_console(const rc_memref_t* memref, char result[], const size_t result_size, int console_id) +{ + const unsigned max_address = rc_console_max_address(console_id); + while (memref) { + if (!rc_validate_memref(memref, result, result_size, console_id, max_address)) + return 0; + + memref = memref->next; + } + + return 1; +} + +static unsigned rc_max_value(const rc_operand_t* operand) +{ + if (operand->type == RC_OPERAND_CONST) + return operand->value.num; + + if (!rc_operand_is_memref(operand)) + return 0xFFFFFFFF; + + switch (operand->size) { + case RC_MEMSIZE_BIT_0: + case RC_MEMSIZE_BIT_1: + case RC_MEMSIZE_BIT_2: + case RC_MEMSIZE_BIT_3: + case RC_MEMSIZE_BIT_4: + case RC_MEMSIZE_BIT_5: + case RC_MEMSIZE_BIT_6: + case RC_MEMSIZE_BIT_7: + return 1; + + case RC_MEMSIZE_LOW: + case RC_MEMSIZE_HIGH: + return 0xF; + + case RC_MEMSIZE_BITCOUNT: + return 8; + + case RC_MEMSIZE_8_BITS: + return (operand->type == RC_OPERAND_BCD) ? 165 : 0xFF; + + case RC_MEMSIZE_16_BITS: + case RC_MEMSIZE_16_BITS_BE: + return (operand->type == RC_OPERAND_BCD) ? 16665 : 0xFFFF; + + case RC_MEMSIZE_24_BITS: + case RC_MEMSIZE_24_BITS_BE: + return (operand->type == RC_OPERAND_BCD) ? 1666665 : 0xFFFFFF; + + default: + return (operand->type == RC_OPERAND_BCD) ? 166666665 : 0xFFFFFFFF; + } +} + +static unsigned rc_scale_value(unsigned value, char oper, const rc_operand_t* operand) +{ + switch (oper) { + case RC_OPERATOR_MULT: + { + unsigned long long scaled = ((unsigned long long)value) * rc_max_value(operand); + if (scaled > 0xFFFFFFFF) + return 0xFFFFFFFF; + + return (unsigned)scaled; + } + + case RC_OPERATOR_DIV: + { + const unsigned min_val = (operand->type == RC_OPERAND_CONST) ? operand->value.num : 1; + return value / min_val; + } + + case RC_OPERATOR_AND: + return rc_max_value(operand); + + case RC_OPERATOR_XOR: + return value | rc_max_value(operand); + + default: + return value; + } +} + +static int rc_validate_get_condition_index(const rc_condset_t* condset, const rc_condition_t* condition) +{ + int index = 1; + const rc_condition_t* scan; + for (scan = condset->conditions; scan != NULL; scan = scan->next) + { + if (scan == condition) + return index; + + ++index; + } + + return 0; +} + +static int rc_validate_range(unsigned min_val, unsigned max_val, char oper, unsigned max, char result[], const size_t result_size) +{ + switch (oper) { + case RC_OPERATOR_AND: + if (min_val > max) { + snprintf(result, result_size, "Mask has more bits than source"); + return 0; + } + else if (min_val == 0 && max_val == 0) { + snprintf(result, result_size, "Result of mask always 0"); + return 0; + } + break; + + case RC_OPERATOR_EQ: + if (min_val > max) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + break; + + case RC_OPERATOR_NE: + if (min_val > max) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + break; + + case RC_OPERATOR_GE: + if (min_val > max) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + if (max_val == 0) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + break; + + case RC_OPERATOR_GT: + if (min_val >= max) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + break; + + case RC_OPERATOR_LE: + if (min_val >= max) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + break; + + case RC_OPERATOR_LT: + if (min_val > max) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + if (max_val == 0) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + break; + } + + return 1; +} + +int rc_validate_condset_internal(const rc_condset_t* condset, char result[], const size_t result_size, int console_id, unsigned max_address) +{ + const rc_condition_t* cond; + char buffer[128]; + unsigned max_val; + int index = 1; + unsigned long long add_source_max = 0; + int in_add_hits = 0; + int in_add_address = 0; + int is_combining = 0; + + if (!condset) { + *result = '\0'; + return 1; + } + + for (cond = condset->conditions; cond; cond = cond->next, ++index) { + unsigned max = rc_max_value(&cond->operand1); + const int is_memref1 = rc_operand_is_memref(&cond->operand1); + const int is_memref2 = rc_operand_is_memref(&cond->operand2); + + if (!in_add_address) { + if (is_memref1 && !rc_validate_memref(cond->operand1.value.memref, buffer, sizeof(buffer), console_id, max_address)) { + snprintf(result, result_size, "Condition %d: %s", index, buffer); + return 0; + } + if (is_memref2 && !rc_validate_memref(cond->operand2.value.memref, buffer, sizeof(buffer), console_id, max_address)) { + snprintf(result, result_size, "Condition %d: %s", index, buffer); + return 0; + } + } + else { + in_add_address = 0; + } + + switch (cond->type) { + case RC_CONDITION_ADD_SOURCE: + max = rc_scale_value(max, cond->oper, &cond->operand2); + add_source_max += max; + is_combining = 1; + continue; + + case RC_CONDITION_SUB_SOURCE: + max = rc_scale_value(max, cond->oper, &cond->operand2); + if (add_source_max < max) /* potential underflow - may be expected */ + add_source_max = 0xFFFFFFFF; + is_combining = 1; + continue; + + case RC_CONDITION_ADD_ADDRESS: + if (cond->operand1.type == RC_OPERAND_DELTA || cond->operand1.type == RC_OPERAND_PRIOR) { + snprintf(result, result_size, "Condition %d: Using pointer from previous frame", index); + return 0; + } + in_add_address = 1; + is_combining = 1; + continue; + + case RC_CONDITION_ADD_HITS: + case RC_CONDITION_SUB_HITS: + in_add_hits = 1; + is_combining = 1; + break; + + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + case RC_CONDITION_RESET_NEXT_IF: + is_combining = 1; + break; + + default: + if (in_add_hits) { + if (cond->required_hits == 0) { + snprintf(result, result_size, "Condition %d: Final condition in AddHits chain must have a hit target", index); + return 0; + } + + in_add_hits = 0; + } + + is_combining = 0; + break; + } + + /* if we're in an add source chain, check for overflow */ + if (add_source_max) { + const unsigned long long overflow = add_source_max + max; + if (overflow > 0xFFFFFFFFUL) + max = 0xFFFFFFFF; + else + max += (unsigned)add_source_max; + } + + /* check for comparing two differently sized memrefs */ + max_val = rc_max_value(&cond->operand2); + if (max_val != max && add_source_max == 0 && is_memref1 && is_memref2) { + snprintf(result, result_size, "Condition %d: Comparing different memory sizes", index); + return 0; + } + + /* if either side is a memref, or there's a running add source chain, check for impossible comparisons */ + if (is_memref1 || is_memref2 || add_source_max) { + const size_t prefix_length = snprintf(result, result_size, "Condition %d: ", index); + + unsigned min_val; + switch (cond->operand2.type) { + case RC_OPERAND_CONST: + min_val = cond->operand2.value.num; + break; + + case RC_OPERAND_FP: + min_val = (int)cond->operand2.value.dbl; + + /* cannot compare an integer memory reference to a non-integral floating point value */ + /* assert: is_memref1 (because operand2==FP means !is_memref2) */ + if (!add_source_max && !rc_operand_is_float_memref(&cond->operand1) && + (float)min_val != cond->operand2.value.dbl) { + snprintf(result + prefix_length, result_size - prefix_length, "Comparison is never true"); + return 0; + } + + break; + + default: + min_val = 0; + + /* cannot compare an integer memory reference to a non-integral floating point value */ + /* assert: is_memref2 (because operand1==FP means !is_memref1) */ + if (cond->operand1.type == RC_OPERAND_FP && !add_source_max && !rc_operand_is_float_memref(&cond->operand2) && + (float)((int)cond->operand1.value.dbl) != cond->operand1.value.dbl) { + snprintf(result + prefix_length, result_size - prefix_length, "Comparison is never true"); + return 0; + } + + break; + } + + if (rc_operand_is_float(&cond->operand2) && rc_operand_is_float(&cond->operand1)) { + /* both sides are floats, don't validate range*/ + } else if (!rc_validate_range(min_val, max_val, cond->oper, max, result + prefix_length, result_size - prefix_length)) { + return 0; + } + } + + add_source_max = 0; + } + + if (is_combining) { + snprintf(result, result_size, "Final condition type expects another condition to follow"); + return 0; + } + + *result = '\0'; + return 1; +} + +int rc_validate_condset(const rc_condset_t* condset, char result[], const size_t result_size, unsigned max_address) +{ + return rc_validate_condset_internal(condset, result, result_size, 0, max_address); +} + +int rc_validate_condset_for_console(const rc_condset_t* condset, char result[], const size_t result_size, int console_id) +{ + const unsigned max_address = rc_console_max_address(console_id); + return rc_validate_condset_internal(condset, result, result_size, console_id, max_address); +} + +static int rc_validate_is_combining_condition(const rc_condition_t* condition) +{ + switch (condition->type) + { + case RC_CONDITION_ADD_ADDRESS: + case RC_CONDITION_ADD_HITS: + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + case RC_CONDITION_RESET_NEXT_IF: + case RC_CONDITION_SUB_HITS: + case RC_CONDITION_SUB_SOURCE: + return 1; + + default: + return 0; + } +} + +static const rc_condition_t* rc_validate_next_non_combining_condition(const rc_condition_t* condition) +{ + int is_combining = rc_validate_is_combining_condition(condition); + for (condition = condition->next; condition != NULL; condition = condition->next) + { + if (rc_validate_is_combining_condition(condition)) + is_combining = 1; + else if (is_combining) + is_combining = 0; + else + return condition; + } + + return NULL; +} + +static int rc_validate_get_opposite_comparison(int oper) +{ + switch (oper) + { + case RC_OPERATOR_EQ: return RC_OPERATOR_NE; + case RC_OPERATOR_NE: return RC_OPERATOR_EQ; + case RC_OPERATOR_LT: return RC_OPERATOR_GE; + case RC_OPERATOR_LE: return RC_OPERATOR_GT; + case RC_OPERATOR_GT: return RC_OPERATOR_LE; + case RC_OPERATOR_GE: return RC_OPERATOR_LT; + default: return oper; + } +} + +static const rc_operand_t* rc_validate_get_comparison(const rc_condition_t* condition, int* comparison, unsigned* value) +{ + if (rc_operand_is_memref(&condition->operand1)) + { + if (condition->operand2.type != RC_OPERAND_CONST) + return NULL; + + *comparison = condition->oper; + *value = condition->operand2.value.num; + return &condition->operand1; + } + + if (condition->operand1.type != RC_OPERAND_CONST) + return NULL; + + if (!rc_operand_is_memref(&condition->operand2)) + return NULL; + + *comparison = rc_validate_get_opposite_comparison(condition->oper); + *value = condition->operand1.value.num; + return &condition->operand2; +} + +enum { + RC_OVERLAP_NONE = 0, + RC_OVERLAP_CONFLICTING, + RC_OVERLAP_REDUNDANT, + RC_OVERLAP_DEFER +}; + +static int rc_validate_comparison_overlap(int comparison1, unsigned value1, int comparison2, unsigned value2) +{ + /* NOTE: this only cares if comp2 conflicts with comp1. + * If comp1 conflicts with comp2, we'll catch that later (return RC_OVERLAP_NONE for now) */ + switch (comparison2) + { + case RC_OPERATOR_EQ: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a == 1 | a == 1 && a == 2 | a == 2 && a == 1 */ + /* redundant conflict conflict */ + return (value1 == value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a == 1 | a <= 1 && a == 2 | a <= 2 && a == 1 */ + /* defer conflict defer */ + return (value1 < value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a == 1 | a >= 1 && a == 2 | a >= 2 && a == 1 */ + /* defer defer conflict */ + return (value1 > value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a == 1 | a != 1 && a == 2 | a != 2 && a == 1 */ + /* conflict defer defer */ + return (value1 == value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_LT: /* a < 1 && a == 1 | a < 1 && a == 2 | a < 2 && a == 1 */ + /* conflict conflict defer */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a == 1 | a > 1 && a == 2 | a > 2 && a == 1 */ + /* conflict defer conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + } + break; + + case RC_OPERATOR_NE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a != 1 | a == 1 && a != 2 | a == 2 && a != 1 */ + /* conflict redundant redundant */ + return (value1 == value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_REDUNDANT; + case RC_OPERATOR_LE: /* a <= 1 && a != 1 | a <= 1 && a != 2 | a <= 2 && a != 1 */ + /* none redundant none */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_GE: /* a >= 1 && a != 1 | a >= 1 && a != 2 | a >= 2 && a != 1 */ + /* none none redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a != 1 | a != 1 && a != 2 | a != 2 && a != 1 */ + /* redundant none none */ + return (value1 == value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a != 1 | a < 1 && a != 2 | a < 2 && a != 1 */ + /* redundant redundant none */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a != 1 | a > 1 && a != 2 | a > 2 && a != 1 */ + /* redundant none redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_LT: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a < 1 | a == 1 && a < 2 | a == 2 && a < 1 */ + /* conflict redundant conflict */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a < 1 | a <= 1 && a < 2 | a <= 2 && a < 1 */ + /* defer redundant defer */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a < 1 | a >= 1 && a < 2 | a >= 2 && a < 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a < 1 | a != 1 && a < 2 | a != 2 && a < 1 */ + /* defer none defer */ + return (value1 >= value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a < 1 | a < 1 && a < 2 | a < 2 && a < 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a < 1 | a > 1 && a < 2 | a > 2 && a < 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_LE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a <= 1 | a == 1 && a <= 2 | a == 2 && a <= 1 */ + /* redundant redundant conflict */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a <= 1 | a <= 1 && a <= 2 | a <= 2 && a <= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a <= 1 | a >= 1 && a <= 2 | a >= 2 && a <= 1 */ + /* none none conflict */ + return (value1 > value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a <= 1 | a != 1 && a <= 2 | a != 2 && a <= 1 */ + /* none none defer */ + return (value1 > value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a <= 1 | a < 1 && a <= 2 | a < 2 && a <= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a <= 1 | a > 1 && a <= 2 | a > 2 && a <= 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_GT: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a > 1 | a == 1 && a > 2 | a == 2 && a > 1 */ + /* conflict conflict redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a > 1 | a <= 1 && a > 2 | a <= 2 && a > 1 */ + /* conflict conflict defer */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a > 1 | a >= 1 && a > 2 | a >= 2 && a > 1 */ + /* defer defer redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a > 1 | a != 1 && a > 2 | a != 2 && a > 1 */ + /* defer defer none */ + return (value1 <= value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a > 1 | a < 1 && a > 2 | a < 2 && a > 1 */ + /* conflict conflict none */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a > 1 | a > 1 && a > 2 | a > 2 && a > 1 */ + /* redundant defer redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + } + break; + + case RC_OPERATOR_GE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a >= 1 | a == 1 && a >= 2 | a == 2 && a >= 1 */ + /* redundant conflict redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a >= 1 | a <= 1 && a >= 2 | a <= 2 && a >= 1 */ + /* none conflict none */ + return (value1 < value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GE: /* a >= 1 && a >= 1 | a >= 1 && a >= 2 | a >= 2 && a >= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a >= 1 | a != 1 && a >= 2 | a != 2 && a >= 1 */ + /* none defer none */ + return (value1 < value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a >= 1 | a < 1 && a >= 2 | a < 2 && a >= 1 */ + /* conflict conflict none */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a >= 1 | a > 1 && a >= 2 | a > 2 && a >= 1 */ + /* redundant defer redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + } + break; + } + + return RC_OVERLAP_NONE; +} + +static int rc_validate_conflicting_conditions(const rc_condset_t* conditions, const rc_condset_t* compare_conditions, + const char* prefix, const char* compare_prefix, char result[], const size_t result_size) +{ + int comparison1, comparison2; + unsigned value1, value2; + const rc_operand_t* operand1; + const rc_operand_t* operand2; + const rc_condition_t* compare_condition; + const rc_condition_t* condition; + int overlap; + + /* empty group */ + if (conditions == NULL || compare_conditions == NULL) + return 1; + + /* outer loop is the source conditions */ + for (condition = conditions->conditions; condition != NULL; + condition = rc_validate_next_non_combining_condition(condition)) + { + /* hits can be captured at any time, so any potential conflict will not be conflicting at another time */ + if (condition->required_hits) + continue; + + operand1 = rc_validate_get_comparison(condition, &comparison1, &value1); + if (!operand1) + continue; + + switch (condition->type) + { + case RC_CONDITION_PAUSE_IF: + if (conditions != compare_conditions) + break; + /* fallthrough */ + case RC_CONDITION_RESET_IF: + comparison1 = rc_validate_get_opposite_comparison(comparison1); + break; + default: + if (rc_validate_is_combining_condition(condition)) + continue; + break; + } + + /* inner loop is the potentially conflicting conditions */ + for (compare_condition = compare_conditions->conditions; compare_condition != NULL; + compare_condition = rc_validate_next_non_combining_condition(compare_condition)) + { + if (compare_condition == condition) + continue; + + if (compare_condition->required_hits) + continue; + + operand2 = rc_validate_get_comparison(compare_condition, &comparison2, &value2); + if (!operand2 || operand2->value.memref->address != operand1->value.memref->address || + operand2->size != operand1->size || operand2->type != operand1->type) + continue; + + switch (compare_condition->type) + { + case RC_CONDITION_PAUSE_IF: + if (conditions != compare_conditions) + break; + /* fallthrough */ + case RC_CONDITION_RESET_IF: + comparison2 = rc_validate_get_opposite_comparison(comparison2); + break; + default: + if (rc_validate_is_combining_condition(compare_condition)) + continue; + break; + } + + overlap = rc_validate_comparison_overlap(comparison1, value1, comparison2, value2); + switch (overlap) + { + case RC_OVERLAP_CONFLICTING: + if (compare_condition->type == RC_CONDITION_PAUSE_IF || condition->type == RC_CONDITION_PAUSE_IF) + { + /* ignore PauseIf conflicts between groups, unless both conditions are PauseIfs */ + if (conditions != compare_conditions && compare_condition->type != condition->type) + continue; + } + break; + + case RC_OVERLAP_REDUNDANT: + if (prefix != compare_prefix && strcmp(compare_prefix, "Core") == 0) + { + /* if the alt condition is more restrictive than the core condition, ignore it */ + if (rc_validate_comparison_overlap(comparison2, value2, comparison1, value1) != RC_OVERLAP_REDUNDANT) + continue; + } + + if (compare_condition->type == RC_CONDITION_PAUSE_IF || condition->type == RC_CONDITION_PAUSE_IF) + { + /* ignore PauseIf redundancies between groups */ + if (conditions != compare_conditions) + continue; + + /* if the PauseIf is less restrictive than the other condition, it's just a guard. ignore it */ + if (rc_validate_comparison_overlap(comparison2, value2, comparison1, value1) != RC_OVERLAP_REDUNDANT) + continue; + + /* PauseIf redundant with ResetIf is a conflict (both are inverted comparisons) */ + if (compare_condition->type == RC_CONDITION_RESET_IF || condition->type == RC_CONDITION_RESET_IF) + overlap = RC_OVERLAP_CONFLICTING; + } + else if (compare_condition->type == RC_CONDITION_RESET_IF && condition->type != RC_CONDITION_RESET_IF) + { + /* only ever report the redundancy on the non-ResetIf condition. The ResetIf is allowed to + * fire when the non-ResetIf condition is not true. */ + continue; + } + else if (compare_condition->type == RC_CONDITION_MEASURED_IF || condition->type == RC_CONDITION_MEASURED_IF) + { + /* ignore MeasuredIf redundancies between groups */ + if (conditions != compare_conditions) + continue; + + if (compare_condition->type == RC_CONDITION_MEASURED_IF && condition->type != RC_CONDITION_MEASURED_IF) + { + /* only ever report the redundancy on the non-MeasuredIf condition. The MeasuredIf provides + * additional functionality. */ + continue; + } + } + else if (compare_condition->type == RC_CONDITION_TRIGGER || condition->type == RC_CONDITION_TRIGGER) + { + /* Trigger is allowed to be redundant with non-trigger conditions as there may be limits that start a + * challenge that are furhter reduced for the completion of the challenge */ + if (compare_condition->type != condition->type) + continue; + } + break; + + default: + continue; + } + + if (compare_prefix && *compare_prefix) + { + snprintf(result, result_size, "%s Condition %d: %s with %s Condition %d", + compare_prefix, rc_validate_get_condition_index(compare_conditions, compare_condition), + (overlap == RC_OVERLAP_REDUNDANT) ? "Redundant" : "Conflicts", + prefix, rc_validate_get_condition_index(conditions, condition)); + } + else + { + snprintf(result, result_size, "Condition %d: %s with Condition %d", + rc_validate_get_condition_index(compare_conditions, compare_condition), + (overlap == RC_OVERLAP_REDUNDANT) ? "Redundant" : "Conflicts", + rc_validate_get_condition_index(conditions, condition)); + } + return 0; + } + } + + return 1; +} + +static int rc_validate_trigger_internal(const rc_trigger_t* trigger, char result[], const size_t result_size, int console_id, unsigned max_address) +{ + const rc_condset_t* alt; + int index; + + if (!trigger->alternative) + { + if (!rc_validate_condset_internal(trigger->requirement, result, result_size, console_id, max_address)) + return 0; + + return rc_validate_conflicting_conditions(trigger->requirement, trigger->requirement, "", "", result, result_size); + } + + snprintf(result, result_size, "Core "); + if (!rc_validate_condset_internal(trigger->requirement, result + 5, result_size - 5, console_id, max_address)) + return 0; + + /* compare core to itself */ + if (!rc_validate_conflicting_conditions(trigger->requirement, trigger->requirement, "Core", "Core", result, result_size)) + return 0; + + index = 1; + for (alt = trigger->alternative; alt; alt = alt->next, ++index) { + char altname[16]; + const size_t prefix_length = snprintf(result, result_size, "Alt%d ", index); + if (!rc_validate_condset_internal(alt, result + prefix_length, result_size - prefix_length, console_id, max_address)) + return 0; + + /* compare alt to itself */ + snprintf(altname, sizeof(altname), "Alt%d", index); + if (!rc_validate_conflicting_conditions(alt, alt, altname, altname, result, result_size)) + return 0; + + /* compare alt to core */ + if (!rc_validate_conflicting_conditions(trigger->requirement, alt, "Core", altname, result, result_size)) + return 0; + + /* compare core to alt */ + if (!rc_validate_conflicting_conditions(alt, trigger->requirement, altname, "Core", result, result_size)) + return 0; + } + + *result = '\0'; + return 1; +} + +int rc_validate_trigger(const rc_trigger_t* trigger, char result[], const size_t result_size, unsigned max_address) +{ + return rc_validate_trigger_internal(trigger, result, result_size, 0, max_address); +} + +int rc_validate_trigger_for_console(const rc_trigger_t* trigger, char result[], const size_t result_size, int console_id) +{ + const unsigned max_address = rc_console_max_address(console_id); + return rc_validate_trigger_internal(trigger, result, result_size, console_id, max_address); +} diff --git a/dep/rcheevos/src/rcheevos/rc_validate.h b/dep/rcheevos/src/rcheevos/rc_validate.h new file mode 100644 index 000000000..ba7df325e --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_validate.h @@ -0,0 +1,26 @@ +#ifndef RC_VALIDATE_H +#define RC_VALIDATE_H + +#include "rc_runtime_types.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int rc_validate_memrefs(const rc_memref_t* memref, char result[], const size_t result_size, unsigned max_address); + +int rc_validate_condset(const rc_condset_t* condset, char result[], const size_t result_size, unsigned max_address); +int rc_validate_trigger(const rc_trigger_t* trigger, char result[], const size_t result_size, unsigned max_address); + +int rc_validate_memrefs_for_console(const rc_memref_t* memref, char result[], const size_t result_size, int console_id); + +int rc_validate_condset_for_console(const rc_condset_t* condset, char result[], const size_t result_size, int console_id); +int rc_validate_trigger_for_console(const rc_trigger_t* trigger, char result[], const size_t result_size, int console_id); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_VALIDATE_H */ diff --git a/dep/rcheevos/src/rcheevos/rc_version.h b/dep/rcheevos/src/rcheevos/rc_version.h new file mode 100644 index 000000000..cdf857d5f --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_version.h @@ -0,0 +1,29 @@ +#ifndef RC_VERSION_H +#define RC_VERSION_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define RCHEEVOS_VERSION_MAJOR 10 +#define RCHEEVOS_VERSION_MINOR 7 +#define RCHEEVOS_VERSION_PATCH 0 + +#define RCHEEVOS_MAKE_VERSION(major, minor, patch) (major * 1000000 + minor * 1000 + patch) +#define RCHEEVOS_VERSION RCHEEVOS_MAKE_VERSION(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR, RCHEEVOS_VERSION_PATCH) + +#define RCHEEVOS_MAKE_STRING(num) #num +#define RCHEEVOS_MAKE_VERSION_STRING(major, minor, patch) RCHEEVOS_MAKE_STRING(major) "." RCHEEVOS_MAKE_STRING(minor) "." RCHEEVOS_MAKE_STRING(patch) +#define RCHEEVOS_MAKE_VERSION_STRING_SHORT(major, minor) RCHEEVOS_MAKE_STRING(major) "." RCHEEVOS_MAKE_STRING(minor) + +#if RCHEEVOS_VERSION_PATCH > 0 + #define RCHEEVOS_VERSION_STRING RCHEEVOS_MAKE_VERSION_STRING(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR, RCHEEVOS_VERSION_PATCH) +#else + #define RCHEEVOS_VERSION_STRING RCHEEVOS_MAKE_VERSION_STRING_SHORT(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR) +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* RC_VERSION_H */ diff --git a/dep/rcheevos/src/rcheevos/richpresence.c b/dep/rcheevos/src/rcheevos/richpresence.c index 68d54490c..3ad6bd991 100644 --- a/dep/rcheevos/src/rcheevos/richpresence.c +++ b/dep/rcheevos/src/rcheevos/richpresence.c @@ -280,7 +280,6 @@ static void rc_rebalance_richpresence_lookup(rc_richpresence_lookup_item_t** roo { rc_richpresence_lookup_item_t** items; rc_scratch_buffer_t* buffer; - const int alignment = sizeof(rc_richpresence_lookup_item_t*); int index; int size; @@ -293,7 +292,7 @@ static void rc_rebalance_richpresence_lookup(rc_richpresence_lookup_item_t** roo size = count * sizeof(rc_richpresence_lookup_item_t*); buffer = &parse->scratch.buffer; do { - const int aligned_offset = (buffer->offset + alignment - 1) & ~(alignment - 1); + const int aligned_offset = RC_ALIGN(buffer->offset); const int remaining = sizeof(buffer->buffer) - aligned_offset; if (remaining >= size) { @@ -534,10 +533,11 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, display = nextline; display_line = parse->lines_read; + /* scan as long as we find conditional lines or full line comments */ do { line = nextline; nextline = rc_parse_line(line, &endline, parse); - } while (*line == '?'); + } while (*line == '?' || (line[0] == '/' && line[1] == '/')); } line = nextline; @@ -557,38 +557,48 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, nextline = rc_parse_line(line, &endline, parse); - while (*line == '?') { - /* conditional display: ?trigger?string */ - ptr = ++line; - while (ptr < endline && *ptr != '?') - ++ptr; + do { + if (line[0] == '?') { + /* conditional display: ?trigger?string */ + ptr = ++line; + while (ptr < endline && *ptr != '?') + ++ptr; - if (ptr < endline) { - *nextdisplay = rc_parse_richpresence_display_internal(ptr + 1, endline, parse, firstlookup); - if (parse->offset < 0) - return; - trigger = &((*nextdisplay)->trigger); - rc_parse_trigger_internal(trigger, &line, parse); - trigger->memrefs = 0; - if (parse->offset < 0) - return; - if (parse->buffer) - nextdisplay = &((*nextdisplay)->next); + if (ptr < endline) { + *nextdisplay = rc_parse_richpresence_display_internal(ptr + 1, endline, parse, firstlookup); + if (parse->offset < 0) + return; + trigger = &((*nextdisplay)->trigger); + rc_parse_trigger_internal(trigger, &line, parse); + trigger->memrefs = 0; + if (parse->offset < 0) + return; + if (parse->buffer) + nextdisplay = &((*nextdisplay)->next); + } + } + else if (line[0] != '/' || line[1] != '/') { + break; } line = nextline; nextline = rc_parse_line(line, &endline, parse); - } + } while (1); /* non-conditional display: string */ *nextdisplay = rc_parse_richpresence_display_internal(line, endline, parse, firstlookup); if (*nextdisplay) { hasdisplay = 1; nextdisplay = &((*nextdisplay)->next); - } - /* restore the parser state */ - parse->lines_read = lines_read; + /* restore the parser state */ + parse->lines_read = lines_read; + } + else { + /* this should only happen if the line is blank. + * expect parse->offset to be RC_MISSING_DISPLAY_STRING and leave parse->lines_read + * on the current line for error tracking. */ + } } /* finalize */ diff --git a/dep/rcheevos/src/rcheevos/runtime.c b/dep/rcheevos/src/rcheevos/runtime.c index 32f079a5b..a5a66abd6 100644 --- a/dep/rcheevos/src/rcheevos/runtime.c +++ b/dep/rcheevos/src/rcheevos/runtime.c @@ -9,6 +9,17 @@ #define RC_RICHPRESENCE_DISPLAY_BUFFER_SIZE 256 +rc_runtime_t* rc_runtime_alloc(void) { + rc_runtime_t* self = malloc(sizeof(rc_runtime_t)); + + if (self) { + rc_runtime_init(self); + self->owns_self = 1; + } + + return self; +} + void rc_runtime_init(rc_runtime_t* self) { memset(self, 0, sizeof(rc_runtime_t)); self->next_memref = &self->memrefs; @@ -48,9 +59,13 @@ void rc_runtime_destroy(rc_runtime_t* self) { self->next_memref = 0; self->memrefs = 0; + + if (self->owns_self) { + free(self); + } } -static void rc_runtime_checksum(const char* memaddr, unsigned char* md5) { +void rc_runtime_checksum(const char* memaddr, unsigned char* md5) { md5_state_t state; md5_init(&state); md5_append(&state, (unsigned char*)memaddr, (int)strlen(memaddr)); @@ -231,7 +246,7 @@ int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, unsigned id } if (rc_trigger_state_active(trigger->state)) { - *measured_value = trigger->measured_value; + *measured_value = (trigger->measured_value == RC_MEASURED_UNKNOWN) ? 0 : trigger->measured_value; *measured_target = trigger->measured_target; } else { @@ -257,7 +272,7 @@ int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, unsigned } /* cap the value at the target so we can count past the target: "107 >= 100" */ - value = trigger->measured_value; + value = (trigger->measured_value == RC_MEASURED_UNKNOWN) ? 0 : trigger->measured_value; if (value > trigger->measured_target) value = trigger->measured_target; @@ -534,6 +549,7 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha for (i = self->trigger_count - 1; i >= 0; --i) { rc_trigger_t* trigger = self->triggers[i].trigger; int old_state, new_state; + unsigned old_measured_value; if (!trigger) continue; @@ -552,13 +568,13 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha continue; } + old_measured_value = trigger->measured_value; old_state = trigger->state; new_state = rc_evaluate_trigger(trigger, peek, ud, L); - /* the trigger state doesn't actually change to RESET, RESET just serves as a notification. + /* trigger->state doesn't actually change to RESET, RESET just serves as a notification. * handle the notification, then look at the actual state */ - if (new_state == RC_TRIGGER_STATE_RESET) - { + if (new_state == RC_TRIGGER_STATE_RESET) { runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_RESET; runtime_event.id = self->triggers[i].id; event_handler(&runtime_event); @@ -566,6 +582,32 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha new_state = trigger->state; } + /* if the measured value changed and the achievement hasn't triggered, send a notification */ + if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN && + trigger->measured_target != 0 && trigger->measured_value <= trigger->measured_target && + new_state != RC_TRIGGER_STATE_TRIGGERED && + new_state != RC_TRIGGER_STATE_INACTIVE && new_state != RC_TRIGGER_STATE_WAITING) { + + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED; + runtime_event.id = self->triggers[i].id; + + if (trigger->measured_as_percent) { + /* if reporting measured value as a percentage, only send the notification if the percentage changes */ + unsigned old_percent = (unsigned)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); + unsigned new_percent = (unsigned)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); + if (old_percent != new_percent) { + runtime_event.value = new_percent; + event_handler(&runtime_event); + } + } + else { + runtime_event.value = trigger->measured_value; + event_handler(&runtime_event); + } + + runtime_event.value = 0; /* achievement loop expects this to stay at 0 */ + } + /* if the state hasn't changed, there won't be any events raised */ if (new_state == old_state) continue; @@ -685,13 +727,8 @@ void rc_runtime_reset(rc_runtime_t* self) { rc_reset_lboard(self->lboards[i].lboard); } - if (self->richpresence && self->richpresence->richpresence) { - rc_richpresence_display_t* display = self->richpresence->richpresence->first_display; - while (display != 0) { - rc_reset_trigger(&display->trigger); - display = display->next; - } - } + if (self->richpresence && self->richpresence->richpresence) + rc_reset_richpresence(self->richpresence->richpresence); for (variable = self->variables; variable; variable = variable->next) rc_reset_value(variable); @@ -712,7 +749,7 @@ static int rc_condset_contains_memref(const rc_condset_t* condset, const rc_memr return 0; } -static int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref) { +int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref) { rc_condset_t* condset; if (!value) return 0; @@ -725,7 +762,7 @@ static int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* return 0; } -static int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref) { +int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref) { rc_condset_t* condset; if (!trigger) return 0; diff --git a/dep/rcheevos/src/rcheevos/runtime_progress.c b/dep/rcheevos/src/rcheevos/runtime_progress.c index cebe981c3..32353faab 100644 --- a/dep/rcheevos/src/rcheevos/runtime_progress.c +++ b/dep/rcheevos/src/rcheevos/runtime_progress.c @@ -17,7 +17,7 @@ #define RC_RUNTIME_CHUNK_DONE 0x454E4F44 /* DONE */ typedef struct rc_runtime_progress_t { - rc_runtime_t* runtime; + const rc_runtime_t* runtime; int offset; unsigned char* buffer; @@ -81,17 +81,6 @@ static int rc_runtime_progress_match_md5(rc_runtime_progress_t* progress, unsign return result; } -static unsigned rc_runtime_progress_djb2(const char* input) -{ - unsigned result = 5381; - char c; - - while ((c = *input++) != '\0') - result = ((result << 5) + result) + c; /* result = result * 33 + c */ - - return result; -} - static void rc_runtime_progress_start_chunk(rc_runtime_progress_t* progress, unsigned chunk_id) { rc_runtime_progress_write_uint(progress, chunk_id); @@ -120,7 +109,7 @@ static void rc_runtime_progress_end_chunk(rc_runtime_progress_t* progress) } } -static void rc_runtime_progress_init(rc_runtime_progress_t* progress, rc_runtime_t* runtime, lua_State* L) +static void rc_runtime_progress_init(rc_runtime_progress_t* progress, const rc_runtime_t* runtime, lua_State* L) { memset(progress, 0, sizeof(rc_runtime_progress_t)); progress->runtime = runtime; @@ -352,7 +341,7 @@ static int rc_runtime_progress_write_variables(rc_runtime_progress_t* progress) for (variable = progress->runtime->variables; variable; variable = variable->next) { - unsigned djb2 = rc_runtime_progress_djb2(variable->name); + unsigned djb2 = rc_djb2(variable->name); rc_runtime_progress_write_uint(progress, djb2); rc_runtime_progress_write_variable(progress, variable); @@ -418,7 +407,7 @@ static int rc_runtime_progress_read_variables(rc_runtime_progress_t* progress) count = 0; for (variable = progress->runtime->variables; variable; variable = variable->next) { pending_variables[count].variable = variable; - pending_variables[count].djb2 = rc_runtime_progress_djb2(variable->name); + pending_variables[count].djb2 = rc_djb2(variable->name); ++count; } @@ -751,7 +740,7 @@ int rc_runtime_progress_size(const rc_runtime_t* runtime, lua_State* L) rc_runtime_progress_t progress; int result; - rc_runtime_progress_init(&progress, (rc_runtime_t*)runtime, L); + rc_runtime_progress_init(&progress, runtime, L); result = rc_runtime_progress_serialize_internal(&progress); if (result != RC_OK) @@ -764,7 +753,10 @@ int rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, lua { rc_runtime_progress_t progress; - rc_runtime_progress_init(&progress, (rc_runtime_t*)runtime, L); + if (!buffer) + return RC_INVALID_STATE; + + rc_runtime_progress_init(&progress, runtime, L); progress.buffer = (unsigned char*)buffer; return rc_runtime_progress_serialize_internal(&progress); @@ -782,6 +774,11 @@ int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const unsigned char* int seen_rich_presence = 0; int result = RC_OK; + if (!serialized) { + rc_runtime_reset(runtime); + return RC_INVALID_STATE; + } + rc_runtime_progress_init(&progress, runtime, L); progress.buffer = (unsigned char*)serialized; diff --git a/dep/rcheevos/src/rcheevos/trigger.c b/dep/rcheevos/src/rcheevos/trigger.c index dda918b53..6061ab8ee 100644 --- a/dep/rcheevos/src/rcheevos/trigger.c +++ b/dep/rcheevos/src/rcheevos/trigger.c @@ -42,8 +42,8 @@ void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_pars *next = 0; *memaddr = aux; - self->measured_value = 0; self->measured_target = parse->measured_target; + self->measured_value = parse->measured_target ? RC_MEASURED_UNKNOWN : 0; self->measured_as_percent = parse->measured_as_percent; self->state = RC_TRIGGER_STATE_WAITING; self->has_hits = 0; @@ -197,14 +197,6 @@ int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* self->measured_value = eval_state.measured_value.value.u32; } - /* if the state is WAITING and the trigger is ready to fire, ignore it and reset the hit counts */ - /* otherwise, if the state is WAITING, proceed to activating the trigger */ - if (self->state == RC_TRIGGER_STATE_WAITING && ret) { - rc_reset_trigger(self); - self->has_hits = 0; - return RC_TRIGGER_STATE_WAITING; - } - /* if any ResetIf condition was true, reset the hit counts */ if (eval_state.was_reset) { /* if the measured value came from a hit count, reset it. do this before calling @@ -247,6 +239,13 @@ int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* is_primed = 0; } else if (ret) { + /* if the state is WAITING and the trigger is ready to fire, ignore it and reset the hit counts */ + if (self->state == RC_TRIGGER_STATE_WAITING) { + rc_reset_trigger(self); + self->has_hits = 0; + return RC_TRIGGER_STATE_WAITING; + } + /* trigger was triggered */ self->state = RC_TRIGGER_STATE_TRIGGERED; return RC_TRIGGER_STATE_TRIGGERED; @@ -284,6 +283,9 @@ void rc_reset_trigger(rc_trigger_t* self) { rc_reset_trigger_hitcounts(self); self->state = RC_TRIGGER_STATE_WAITING; - self->measured_value = 0; + + if (self->measured_target) + self->measured_value = RC_MEASURED_UNKNOWN; + self->has_hits = 0; } diff --git a/dep/rcheevos/src/rcheevos/value.c b/dep/rcheevos/src/rcheevos/value.c index 70444c104..54784caab 100644 --- a/dep/rcheevos/src/rcheevos/value.c +++ b/dep/rcheevos/src/rcheevos/value.c @@ -111,6 +111,7 @@ void rc_parse_legacy_value(rc_value_t* self, const char** memaddr, rc_parse_stat case RC_OPERATOR_MULT: case RC_OPERATOR_DIV: case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: case RC_OPERATOR_NONE: break; @@ -284,6 +285,20 @@ void rc_reset_value(rc_value_t* self) { self->value.changed = 0; } +int rc_value_from_hits(rc_value_t* self) +{ + rc_condset_t* condset = self->conditions; + for (; condset != NULL; condset = condset->next) { + rc_condition_t* condition = condset->conditions; + for (; condition != NULL; condition = condition->next) { + if (condition->type == RC_CONDITION_MEASURED) + return (condition->required_hits != 0); + } + } + + return 0; +} + void rc_init_parse_state_variables(rc_parse_state_t* parse, rc_value_t** variables) { parse->variables = variables; *variables = 0; @@ -427,6 +442,26 @@ static rc_typed_value_t* rc_typed_value_convert_into(rc_typed_value_t* dest, con return dest; } +void rc_typed_value_negate(rc_typed_value_t* value) { + switch (value->type) + { + case RC_VALUE_TYPE_UNSIGNED: + rc_typed_value_convert(value, RC_VALUE_TYPE_SIGNED); + /* fallthrough to RC_VALUE_TYPE_SIGNED */ + + case RC_VALUE_TYPE_SIGNED: + value->value.i32 = -(value->value.i32); + break; + + case RC_VALUE_TYPE_FLOAT: + value->value.f32 = -(value->value.f32); + break; + + default: + break; + } +} + void rc_typed_value_add(rc_typed_value_t* value, const rc_typed_value_t* amount) { rc_typed_value_t converted; diff --git a/dep/rcheevos/src/rhash/cdreader.c b/dep/rcheevos/src/rhash/cdreader.c index 9f464b1bb..c0f5c88a2 100644 --- a/dep/rcheevos/src/rhash/cdreader.c +++ b/dep/rcheevos/src/rhash/cdreader.c @@ -22,6 +22,7 @@ struct cdrom_t void* file_handle; /* the file handle for reading the track data */ int sector_size; /* the size of each sector in the track data */ int sector_header_size; /* the offset to the raw data within a sector block */ + int raw_data_size; /* the amount of raw data within a sector block */ int64_t file_track_offset;/* the offset of the track data within the file */ int track_first_sector; /* the first absolute sector associated to the track (includes pregap) */ int track_pregap_sectors; /* the number of pregap sectors */ @@ -58,6 +59,7 @@ static void cdreader_determine_sector_size(struct cdrom_t* cdrom) cdrom->sector_size = 0; cdrom->sector_header_size = 0; + cdrom->raw_data_size = 2048; rc_file_seek(cdrom->file_handle, toc_sector * 2352 + cdrom->file_track_offset, SEEK_SET); if (rc_file_read(cdrom->file_handle, header, sizeof(header)) < sizeof(header)) @@ -122,6 +124,8 @@ static void* cdreader_open_bin_track(const char* path, uint32_t track) return NULL; cdrom = (struct cdrom_t*)calloc(1, sizeof(*cdrom)); + if (!cdrom) + return NULL; cdrom->file_handle = file_handle; #ifndef NDEBUG cdrom->track_id = track; @@ -219,6 +223,7 @@ static int cdreader_open_bin(struct cdrom_t* cdrom, const char* path, const char { cdrom->sector_size = 2352; cdrom->sector_header_size = 0; + cdrom->raw_data_size = 2352; /* no header or footer data on audio tracks */ } } @@ -282,6 +287,7 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) char* bin_filename = NULL; char *ptr, *ptr2, *end; int done = 0; + int session = 1; size_t num_read = 0; struct cdrom_t* cdrom = NULL; @@ -339,7 +345,7 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) ++ptr; /* convert mm:ss:ff to sector count */ - sscanf(ptr, "%d:%d:%d", &m, &s, &f); + sscanf_s(ptr, "%d:%d:%d", &m, &s, &f); sector_offset = ((m * 60) + s) * 75 + f; if (current_track.first_sector == -1) @@ -389,6 +395,13 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) done = 1; break; } + + if (track == RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION && session == 2) + { + track = current_track.id; + done = 1; + break; + } } } else if (strncasecmp(ptr, "TRACK ", 6) == 0) @@ -465,6 +478,20 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) if (ptr2 - ptr < (int)sizeof(current_track.filename)) memcpy(current_track.filename, ptr, ptr2 - ptr); } + else if (strncasecmp(ptr, "REM ", 4) == 0) + { + ptr += 4; + while (*ptr == ' ') + ++ptr; + + if (strncasecmp(ptr, "SESSION ", 8) == 0) + { + ptr += 8; + while (*ptr == ' ') + ++ptr; + session = atoi(ptr); + } + } while (*ptr && *ptr != '\n') ++ptr; @@ -694,8 +721,8 @@ static void* cdreader_open_gdi_track(const char* path, uint32_t track) largest_track_size = track_size; largest_track = current_track; largest_track_lba = lba; - strcpy(largest_track_file, file); - strcpy(largest_track_sector_size, sector_size); + strcpy_s(largest_track_file, sizeof(largest_track_file), file); + strcpy_s(largest_track_sector_size, sizeof(largest_track_sector_size), sector_size); } } } @@ -723,8 +750,8 @@ static void* cdreader_open_gdi_track(const char* path, uint32_t track) if (largest_track != 0 && largest_track != current_track) { current_track = largest_track; - strcpy(file, largest_track_file); - strcpy(sector_size, largest_track_sector_size); + strcpy_s(file, sizeof(file), largest_track_file); + strcpy_s(sector_size, sizeof(sector_size), largest_track_sector_size); lba = largest_track_lba; } @@ -794,18 +821,18 @@ static size_t cdreader_read_sector(void* track_handle, uint32_t sector, void* bu sector_start = (int64_t)(sector - cdrom->track_first_sector) * cdrom->sector_size + cdrom->sector_header_size + cdrom->file_track_offset; - while (requested_bytes > 2048) + while (requested_bytes > (size_t)cdrom->raw_data_size) { rc_file_seek(cdrom->file_handle, sector_start, SEEK_SET); - num_read = rc_file_read(cdrom->file_handle, buffer_ptr, 2048); + num_read = rc_file_read(cdrom->file_handle, buffer_ptr, cdrom->raw_data_size); total_read += num_read; - if (num_read < 2048) + if (num_read < (size_t)cdrom->raw_data_size) return total_read; - buffer_ptr += 2048; + buffer_ptr += cdrom->raw_data_size; sector_start += cdrom->sector_size; - requested_bytes -= 2048; + requested_bytes -= cdrom->raw_data_size; } rc_file_seek(cdrom->file_handle, sector_start, SEEK_SET); @@ -844,7 +871,7 @@ void rc_hash_get_default_cdreader(struct rc_hash_cdreader* cdreader) cdreader->first_track_sector = cdreader_first_track_sector; } -void rc_hash_init_default_cdreader() +void rc_hash_init_default_cdreader(void) { struct rc_hash_cdreader cdreader; rc_hash_get_default_cdreader(&cdreader); diff --git a/dep/rcheevos/src/rhash/hash.c b/dep/rcheevos/src/rhash/hash.c index 1c983146b..e93423b86 100644 --- a/dep/rcheevos/src/rhash/hash.c +++ b/dep/rcheevos/src/rhash/hash.c @@ -48,7 +48,13 @@ static struct rc_hash_filereader* filereader = NULL; static void* filereader_open(const char* path) { +#if defined(__STDC_WANT_SECURE_LIB__) + FILE* fp; + fopen_s(&fp, path, "rb"); + return fp; +#else return fopen(path, "rb"); +#endif } static void filereader_seek(void* file_handle, int64_t offset, int origin) @@ -223,12 +229,17 @@ static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uns { uint8_t buffer[2048], *tmp; int sector; + unsigned num_sectors = 0; size_t filename_length; const char* slash; if (!track_handle) return 0; + /* we start at the root. don't need to explicitly find it */ + if (*path == '\\') + ++path; + filename_length = strlen(path); slash = strrchr(path, '\\'); if (slash) @@ -247,6 +258,8 @@ static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uns } else { + unsigned logical_block_size; + /* find the cd information */ if (!rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle) + 16, buffer, 256)) return 0; @@ -255,6 +268,15 @@ static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uns * https://www.cdroller.com/htm/readdata.html */ sector = buffer[156 + 2] | (buffer[156 + 3] << 8) | (buffer[156 + 4] << 16); + + /* if the table of contents spans more than one sector, it's length of section will exceed it's logical block size */ + logical_block_size = (buffer[128] | (buffer[128 + 1] << 8)); /* logical block size */ + if (logical_block_size == 0) { + num_sectors = 1; + } else { + num_sectors = (buffer[156 + 10] | (buffer[156 + 11] << 8) | (buffer[156 + 12] << 16) | (buffer[156 + 13] << 24)); /* length of section */ + num_sectors /= logical_block_size; + } } /* fetch and process the directory record */ @@ -262,21 +284,34 @@ static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uns return 0; tmp = buffer; - while (tmp < buffer + sizeof(buffer)) + do { - if (!*tmp) - return 0; + if (tmp >= buffer + sizeof(buffer) || !*tmp) + { + /* end of this path table block. if the path table spans multiple sectors, keep scanning */ + if (num_sectors > 1) + { + --num_sectors; + if (rc_cd_read_sector(track_handle, ++sector, buffer, sizeof(buffer))) + { + tmp = buffer; + continue; + } + } + break; + } /* filename is 33 bytes into the record and the format is "FILENAME;version" or "DIRECTORY" */ - if ((tmp[33 + filename_length] == ';' || tmp[33 + filename_length] == '\0') && + if ((tmp[32] == filename_length || tmp[33 + filename_length] == ';') && strncasecmp((const char*)(tmp + 33), path, filename_length) == 0) { sector = tmp[2] | (tmp[3] << 8) | (tmp[4] << 16); if (verbose_message_callback) { - snprintf((char*)buffer, sizeof(buffer), "Found %s at sector %d", path, sector); - verbose_message_callback((const char*)buffer); + char message[128]; + snprintf(message, sizeof(message), "Found %s at sector %d", path, sector); + verbose_message_callback(message); } if (size) @@ -287,7 +322,7 @@ static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uns /* the first byte of the record is the length of the record */ tmp += *tmp; - } + } while (1); return 0; } @@ -347,6 +382,34 @@ int rc_path_compare_extension(const char* path, const char* ext) /* ===================================================== */ +static void rc_hash_byteswap16(uint8_t* buffer, const uint8_t* stop) +{ + uint32_t* ptr = (uint32_t*)buffer; + const uint32_t* stop32 = (const uint32_t*)stop; + while (ptr < stop32) + { + uint32_t temp = *ptr; + temp = (temp & 0xFF00FF00) >> 8 | + (temp & 0x00FF00FF) << 8; + *ptr++ = temp; + } +} + +static void rc_hash_byteswap32(uint8_t* buffer, const uint8_t* stop) +{ + uint32_t* ptr = (uint32_t*)buffer; + const uint32_t* stop32 = (const uint32_t*)stop; + while (ptr < stop32) + { + uint32_t temp = *ptr; + temp = (temp & 0xFF000000) >> 24 | + (temp & 0x00FF0000) >> 8 | + (temp & 0x0000FF00) << 8 | + (temp & 0x000000FF) << 24; + *ptr++ = temp; + } +} + static int rc_hash_finalize(md5_state_t* md5, char hash[33]) { md5_byte_t digest[16]; @@ -415,6 +478,9 @@ static int rc_hash_cd_file(md5_state_t* md5, void* track_handle, uint32_t sector verbose_message_callback(message); } + if (size < (unsigned)num_read) + size = (unsigned)num_read; + do { md5_append(md5, buffer, (int)num_read); @@ -684,6 +750,142 @@ static int rc_hash_text(char hash[33], const uint8_t* buffer, size_t buffer_size return rc_hash_finalize(&md5, hash); } +/* helper variable only used for testing */ +const char* _rc_hash_jaguar_cd_homebrew_hash = NULL; + +static int rc_hash_jaguar_cd(char hash[33], const char* path) +{ + uint8_t buffer[2352]; + char message[128]; + void* track_handle; + md5_state_t md5; + int byteswapped = 0; + unsigned size = 0; + unsigned offset = 0; + unsigned sector = 0; + unsigned remaining; + unsigned i; + + /* Jaguar CD header is in the first sector of the first data track OF THE SECOND SESSION. + * The first track must be an audio track, but may be a warning message or actual game audio */ + track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* The header is an unspecified distance into the first sector, but usually two bytes in. + * It consists of 64 bytes of "TAIR" or "ATRI" repeating, depending on whether or not the data + * is byteswapped. Then another 32 byte that reads "ATARI APPROVED DATA HEADER ATRI " + * (possibly byteswapped). Then a big-endian 32-bit value for the address where the boot code + * should be loaded, and a second big-endian 32-bit value for the size of the boot code. */ + sector = rc_cd_first_track_sector(track_handle); + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + + for (i = 64; i < sizeof(buffer) - 32 - 4 * 3; i++) + { + if (memcmp(&buffer[i], "TARA IPARPVODED TA AEHDAREA RT I", 32) == 0) + { + byteswapped = 1; + offset = i + 32 + 4; + size = (buffer[offset] << 16) | (buffer[offset + 1] << 24) | (buffer[offset + 2]) | (buffer[offset + 3] << 8); + break; + } + else if (memcmp(&buffer[i], "ATARI APPROVED DATA HEADER ATRI ", 32) == 0) + { + byteswapped = 0; + offset = i + 32 + 4; + size = (buffer[offset] << 24) | (buffer[offset + 1] << 16) | (buffer[offset + 2] << 8) | (buffer[offset + 3]); + break; + } + } + + if (size == 0) /* did not see ATARI APPROVED DATA HEADER */ + { + rc_cd_close_track(track_handle); + return rc_hash_error("Not a Jaguar CD"); + } + + i = 0; /* only loop once */ + do + { + md5_init(&md5); + + offset += 4; + + if (verbose_message_callback) + { + snprintf(message, sizeof(message), "Hashing boot executable (%u bytes starting at %u bytes into sector %u)", size, offset, sector); + rc_hash_verbose(message); + } + + if (size > MAX_BUFFER_SIZE) + size = MAX_BUFFER_SIZE; + + do + { + if (byteswapped) + rc_hash_byteswap16(buffer, &buffer[sizeof(buffer)]); + + remaining = sizeof(buffer) - offset; + if (remaining >= size) + { + md5_append(&md5, &buffer[offset], size); + size = 0; + break; + } + + md5_append(&md5, &buffer[offset], remaining); + size -= remaining; + offset = 0; + } while (rc_cd_read_sector(track_handle, ++sector, buffer, sizeof(buffer)) == sizeof(buffer)); + + rc_cd_close_track(track_handle); + + if (size > 0) + return rc_hash_error("Not enough data"); + + rc_hash_finalize(&md5, hash); + + /* homebrew games all seem to have the same boot executable and store the actual game code in track 2. + * if we generated something other than the homebrew hash, return it. assume all homebrews are byteswapped. */ + if (strcmp(hash, "254487b59ab21bc005338e85cbf9fd2f") != 0 || !byteswapped) { + if (_rc_hash_jaguar_cd_homebrew_hash == NULL || strcmp(hash, _rc_hash_jaguar_cd_homebrew_hash) != 0) + return 1; + } + + /* if we've already been through the loop a second time, just return the hash */ + if (i == 1) + return 1; + ++i; + + if (verbose_message_callback) + { + snprintf(message, sizeof(message), "Potential homebrew at sector %u, checking for KART data in track 2", sector); + rc_hash_verbose(message); + } + + track_handle = rc_cd_open_track(path, 2); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* track 2 of the homebrew code has the 64 bytes or ATRI followed by 32 bytes of "ATARI APPROVED DATA HEADER ATRI!", + * then 64 bytes of KART repeating. */ + sector = rc_cd_first_track_sector(track_handle); + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + if (memcmp(&buffer[0x5E], "RT!IRTKA", 8) != 0) + return rc_hash_error("Homebrew executable not found in track 2"); + + /* found KART data*/ + if (verbose_message_callback) + { + snprintf(message, sizeof(message), "Found KART data in track 2"); + rc_hash_verbose(message); + } + + offset = 0xA6; + size = (buffer[offset] << 16) | (buffer[offset + 1] << 24) | (buffer[offset + 2]) | (buffer[offset + 3] << 8); + } while (1); +} + static int rc_hash_lynx(char hash[33], const uint8_t* buffer, size_t buffer_size) { /* if the file contains a header, ignore it */ @@ -698,6 +900,70 @@ static int rc_hash_lynx(char hash[33], const uint8_t* buffer, size_t buffer_size return rc_hash_buffer(hash, buffer, buffer_size); } +static int rc_hash_neogeo_cd(char hash[33], const char* path) +{ + char buffer[1024], *ptr; + void* track_handle; + uint32_t sector; + unsigned size; + md5_state_t md5; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* https://wiki.neogeodev.org/index.php?title=IPL_file, https://wiki.neogeodev.org/index.php?title=PRG_file + * IPL file specifies data to be loaded before the game starts. PRG files are the executable code + */ + sector = rc_cd_find_file_sector(track_handle, "IPL.TXT", &size); + if (!sector) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Not a NeoGeo CD game disc"); + } + + if (rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)) == 0) + { + rc_cd_close_track(track_handle); + return 0; + } + + md5_init(&md5); + + buffer[sizeof(buffer) - 1] = '\0'; + ptr = &buffer[0]; + do + { + char* start = ptr; + while (*ptr && *ptr != '.') + ++ptr; + + if (strncasecmp(ptr, ".PRG", 4) == 0) + { + ptr += 4; + *ptr++ = '\0'; + + sector = rc_cd_find_file_sector(track_handle, start, &size); + if (!sector || !rc_hash_cd_file(&md5, track_handle, sector, NULL, size, start)) + { + char error[128]; + rc_cd_close_track(track_handle); + snprintf(error, sizeof(error), "Could not read %.16s", start); + return rc_hash_error(error); + } + } + + while (*ptr && *ptr != '\n') + ++ptr; + if (*ptr != '\n') + break; + ++ptr; + } while (*ptr != '\0' && *ptr != '\x1a'); + + rc_cd_close_track(track_handle); + return rc_hash_finalize(&md5, hash); +} + static int rc_hash_nes(char hash[33], const uint8_t* buffer, size_t buffer_size) { /* if the file contains a header, ignore it */ @@ -719,34 +985,6 @@ static int rc_hash_nes(char hash[33], const uint8_t* buffer, size_t buffer_size) return rc_hash_buffer(hash, buffer, buffer_size); } -static void rc_hash_v64_to_z64(uint8_t* buffer, const uint8_t* stop) -{ - uint32_t* ptr = (uint32_t*)buffer; - const uint32_t* stop32 = (const uint32_t*)stop; - while (ptr < stop32) - { - uint32_t temp = *ptr; - temp = (temp & 0xFF00FF00) >> 8 | - (temp & 0x00FF00FF) << 8; - *ptr++ = temp; - } -} - -static void rc_hash_n64_to_z64(uint8_t* buffer, const uint8_t* stop) -{ - uint32_t* ptr = (uint32_t*)buffer; - const uint32_t* stop32 = (const uint32_t*)stop; - while (ptr < stop32) - { - uint32_t temp = *ptr; - temp = (temp & 0xFF000000) >> 24 | - (temp & 0x00FF0000) >> 8 | - (temp & 0x0000FF00) << 8 | - (temp & 0x000000FF) << 24; - *ptr++ = temp; - } -} - static int rc_hash_n64(char hash[33], const char* path) { uint8_t* buffer; @@ -787,6 +1025,9 @@ static int rc_hash_n64(char hash[33], const char* path) rc_hash_verbose("converting n64 to z64"); is_n64 = 1; } + else if (buffer[0] == 0xE8 || buffer[0] == 0x22) /* ndd format (don't byteswap) */ + { + } else { free(buffer); @@ -818,9 +1059,9 @@ static int rc_hash_n64(char hash[33], const char* path) rc_file_read(file_handle, buffer, (int)buffer_size); if (is_v64) - rc_hash_v64_to_z64(buffer, stop); + rc_hash_byteswap16(buffer, stop); else if (is_n64) - rc_hash_n64_to_z64(buffer, stop); + rc_hash_byteswap32(buffer, stop); md5_append(&md5, buffer, (int)buffer_size); remaining -= buffer_size; @@ -832,9 +1073,9 @@ static int rc_hash_n64(char hash[33], const char* path) stop = buffer + remaining; if (is_v64) - rc_hash_v64_to_z64(buffer, stop); + rc_hash_byteswap16(buffer, stop); else if (is_n64) - rc_hash_n64_to_z64(buffer, stop); + rc_hash_byteswap32(buffer, stop); md5_append(&md5, buffer, (int)remaining); } @@ -957,6 +1198,121 @@ static int rc_hash_nintendo_ds(char hash[33], const char* path) return rc_hash_finalize(&md5, hash); } +static int rc_hash_gamecube(char hash[33], const char* path) +{ + md5_state_t md5; + void* file_handle; + const uint32_t BASE_HEADER_SIZE = 0x2440; + const uint32_t MAX_HEADER_SIZE = 1024 * 1024; + uint32_t apploader_header_size, apploader_body_size, apploader_trailer_size, header_size; + uint8_t quad_buffer[4]; + uint8_t addr_buffer[0xD8]; + uint8_t* buffer; + uint32_t dol_offset; + uint32_t dol_offsets[18]; + uint32_t dol_sizes[18]; + uint32_t dol_buf_size = 0; + uint32_t ix; + + file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); + + /* Verify Gamecube */ + rc_file_seek(file_handle, 0x1c, SEEK_SET); + rc_file_read(file_handle, quad_buffer, 4); + if (quad_buffer[0] != 0xC2|| quad_buffer[1] != 0x33 || quad_buffer[2] != 0x9F || quad_buffer[3] != 0x3D) + { + rc_file_close(file_handle); + return rc_hash_error("Not a Gamecube disc"); + } + + /* GetApploaderSize */ + rc_file_seek(file_handle, BASE_HEADER_SIZE + 0x14, SEEK_SET); + apploader_header_size = 0x20; + rc_file_read(file_handle, quad_buffer, 4); + apploader_body_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + rc_file_read(file_handle, quad_buffer, 4); + apploader_trailer_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + header_size = BASE_HEADER_SIZE + apploader_header_size + apploader_body_size + apploader_trailer_size; + if (header_size > MAX_HEADER_SIZE) header_size = MAX_HEADER_SIZE; + + /* Hash headers */ + buffer = (uint8_t*)malloc(header_size); + if (!buffer) + { + rc_file_close(file_handle); + return rc_hash_error("Could not allocate temporary buffer"); + } + rc_file_seek(file_handle, 0, SEEK_SET); + rc_file_read(file_handle, buffer, header_size); + md5_init(&md5); + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing %u byte header", header_size); + verbose_message_callback(message); + } + md5_append(&md5, buffer, header_size); + + /* GetBootDOLOffset + * Base header size is guaranteed larger than 0x423 therefore buffer contains dol_offset right now + */ + dol_offset = (buffer[0x420] << 24) | (buffer[0x421] << 16) | (buffer[0x422] << 8) | buffer[0x423]; + free(buffer); + + /* Find offsetsand sizes for the 7 main.dol code segments and 11 main.dol data segments */ + rc_file_seek(file_handle, dol_offset, SEEK_SET); + rc_file_read(file_handle, addr_buffer, 0xD8); + for (ix = 0; ix < 18; ix++) + { + dol_offsets[ix] = + (addr_buffer[0x0 + ix * 4] << 24) | + (addr_buffer[0x1 + ix * 4] << 16) | + (addr_buffer[0x2 + ix * 4] << 8) | + addr_buffer[0x3 + ix * 4]; + dol_sizes[ix] = + (addr_buffer[0x90 + ix * 4] << 24) | + (addr_buffer[0x91 + ix * 4] << 16) | + (addr_buffer[0x92 + ix * 4] << 8) | + addr_buffer[0x93 + ix * 4]; + dol_buf_size = (dol_sizes[ix] > dol_buf_size) ? dol_sizes[ix] : dol_buf_size; + } + + /* Iterate through the 18 main.dol segments and hash each */ + buffer = (uint8_t*)malloc(dol_buf_size); + if (!buffer) + { + rc_file_close(file_handle); + return rc_hash_error("Could not allocate temporary buffer"); + } + for (ix = 0; ix < 18; ix++) + { + if (dol_sizes[ix] == 0) + continue; + rc_file_seek(file_handle, dol_offsets[ix], SEEK_SET); + rc_file_read(file_handle, buffer, dol_sizes[ix]); + if (verbose_message_callback) + { + char message[128]; + if (ix < 7) + snprintf(message, sizeof(message), "Hashing %u byte main.dol code segment %u", dol_sizes[ix], ix); + else + snprintf(message, sizeof(message), "Hashing %u byte main.dol data segment %u", dol_sizes[ix], ix - 7); + verbose_message_callback(message); + } + md5_append(&md5, buffer, dol_sizes[ix]); + } + + /* Finalize */ + rc_file_close(file_handle); + free(buffer); + + return rc_hash_finalize(&md5, hash); +} + static int rc_hash_pce(char hash[33], const uint8_t* buffer, size_t buffer_size) { /* if the file contains a header, ignore it (expect ROM data to be multiple of 128KB) */ @@ -1290,7 +1646,7 @@ static int rc_hash_find_playstation_executable(void* track_handle, const char* b if (strncmp(ptr, cdrom_prefix, cdrom_prefix_len) == 0) ptr += cdrom_prefix_len; - if (*ptr == '\\') + while (*ptr == '\\') ++ptr; start = ptr; @@ -1454,18 +1810,30 @@ static int rc_hash_psp(char hash[33], const char* path) */ sector = rc_cd_find_file_sector(track_handle, "PSP_GAME\\PARAM.SFO", &size); if (!sector) + { + rc_cd_close_track(track_handle); return rc_hash_error("Not a PSP game disc"); + } md5_init(&md5); if (!rc_hash_cd_file(&md5, track_handle, sector, NULL, size, "PSP_GAME\\PARAM.SFO")) + { + rc_cd_close_track(track_handle); return 0; + } sector = rc_cd_find_file_sector(track_handle, "PSP_GAME\\SYSDIR\\EBOOT.BIN", &size); if (!sector) + { + rc_cd_close_track(track_handle); return rc_hash_error("Could not find primary executable"); + } if (!rc_hash_cd_file(&md5, track_handle, sector, NULL, size, "PSP_GAME\\SYSDIR\\EBOOT.BIN")) + { + rc_cd_close_track(track_handle); return 0; + } rc_cd_close_track(track_handle); return rc_hash_finalize(&md5, hash); @@ -1527,7 +1895,11 @@ static struct rc_buffered_file rc_buffered_file; static void* rc_file_open_buffered_file(const char* path) { struct rc_buffered_file* handle = (struct rc_buffered_file*)malloc(sizeof(struct rc_buffered_file)); - memcpy(handle, &rc_buffered_file, sizeof(rc_buffered_file)); + (void)path; + + if (handle) + memcpy(handle, &rc_buffered_file, sizeof(rc_buffered_file)); + return handle; } @@ -1631,7 +2003,9 @@ int rc_hash_generate_from_buffer(char hash[33], int console_id, const uint8_t* b case RC_CONSOLE_SEGA_32X: case RC_CONSOLE_SG1000: case RC_CONSOLE_SUPERVISION: + case RC_CONSOLE_TI83: case RC_CONSOLE_TIC80: + case RC_CONSOLE_UZEBOX: case RC_CONSOLE_VECTREX: case RC_CONSOLE_VIRTUAL_BOY: case RC_CONSOLE_WASM4: @@ -1659,6 +2033,7 @@ int rc_hash_generate_from_buffer(char hash[33], int console_id, const uint8_t* b case RC_CONSOLE_NINTENDO_64: case RC_CONSOLE_NINTENDO_DS: + case RC_CONSOLE_NINTENDO_DSI: return rc_hash_file_from_buffer(hash, console_id, buffer, buffer_size); } } @@ -1922,7 +2297,9 @@ int rc_hash_generate_from_file(char hash[33], int console_id, const char* path) case RC_CONSOLE_SEGA_32X: case RC_CONSOLE_SG1000: case RC_CONSOLE_SUPERVISION: + case RC_CONSOLE_TI83: case RC_CONSOLE_TIC80: + case RC_CONSOLE_UZEBOX: case RC_CONSOLE_VECTREX: case RC_CONSOLE_VIRTUAL_BOY: case RC_CONSOLE_WASM4: @@ -1946,6 +2323,7 @@ int rc_hash_generate_from_file(char hash[33], int console_id, const char* path) case RC_CONSOLE_ATARI_7800: case RC_CONSOLE_ATARI_LYNX: case RC_CONSOLE_NINTENDO: + case RC_CONSOLE_PC_ENGINE: case RC_CONSOLE_SUPER_NINTENDO: /* additional logic whole-file hash - buffer then call rc_hash_generate_from_buffer */ return rc_hash_buffered_file(hash, console_id, path); @@ -1959,13 +2337,29 @@ int rc_hash_generate_from_file(char hash[33], int console_id, const char* path) case RC_CONSOLE_ARCADE: return rc_hash_arcade(hash, path); + case RC_CONSOLE_ATARI_JAGUAR_CD: + return rc_hash_jaguar_cd(hash, path); + + case RC_CONSOLE_DREAMCAST: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_dreamcast(hash, path); + + case RC_CONSOLE_GAMECUBE: + return rc_hash_gamecube(hash, path); + + case RC_CONSOLE_NEO_GEO_CD: + return rc_hash_neogeo_cd(hash, path); + case RC_CONSOLE_NINTENDO_64: return rc_hash_n64(hash, path); case RC_CONSOLE_NINTENDO_DS: + case RC_CONSOLE_NINTENDO_DSI: return rc_hash_nintendo_ds(hash, path); - case RC_CONSOLE_PC_ENGINE: + case RC_CONSOLE_PC_ENGINE_CD: if (rc_path_compare_extension(path, "cue") || rc_path_compare_extension(path, "chd")) return rc_hash_pce_cd(hash, path); @@ -1995,12 +2389,6 @@ int rc_hash_generate_from_file(char hash[33], int console_id, const char* path) case RC_CONSOLE_PSP: return rc_hash_psp(hash, path); - case RC_CONSOLE_DREAMCAST: - if (rc_path_compare_extension(path, "m3u")) - return rc_hash_generate_from_playlist(hash, console_id, path); - - return rc_hash_dreamcast(hash, path); - case RC_CONSOLE_SEGA_CD: case RC_CONSOLE_SATURN: if (rc_path_compare_extension(path, "m3u")) @@ -2077,7 +2465,7 @@ static void rc_hash_initialize_dsk_iterator(struct rc_hash_iterator* iterator, c rc_hash_iterator_append_console(iterator, RC_CONSOLE_APPLE_II); } -void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, uint8_t* buffer, size_t buffer_size) +void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, const uint8_t* buffer, size_t buffer_size) { int need_path = !buffer; @@ -2108,6 +2496,15 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } break; + case '8': + /* http://tibasicdev.wikidot.com/file-extensions */ + if (rc_path_compare_extension(ext, "83g") || + rc_path_compare_extension(ext, "83p")) + { + iterator->consoles[0] = RC_CONSOLE_TI83; + } + break; + case 'a': if (rc_path_compare_extension(ext, "a78")) { @@ -2162,9 +2559,11 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* iterator->consoles[1] = RC_CONSOLE_PLAYSTATION_2; iterator->consoles[2] = RC_CONSOLE_DREAMCAST; iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ - iterator->consoles[4] = RC_CONSOLE_PC_ENGINE; + iterator->consoles[4] = RC_CONSOLE_PC_ENGINE_CD; iterator->consoles[5] = RC_CONSOLE_3DO; iterator->consoles[6] = RC_CONSOLE_PCFX; + iterator->consoles[7] = RC_CONSOLE_NEO_GEO_CD; + iterator->consoles[8] = RC_CONSOLE_ATARI_JAGUAR_CD; need_path = 1; } else if (rc_path_compare_extension(ext, "chd")) @@ -2173,7 +2572,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* iterator->consoles[1] = RC_CONSOLE_PLAYSTATION_2; iterator->consoles[2] = RC_CONSOLE_DREAMCAST; iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ - iterator->consoles[4] = RC_CONSOLE_PC_ENGINE; + iterator->consoles[4] = RC_CONSOLE_PC_ENGINE_CD; iterator->consoles[5] = RC_CONSOLE_3DO; iterator->consoles[6] = RC_CONSOLE_PCFX; need_path = 1; @@ -2199,7 +2598,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } else if (rc_path_compare_extension(ext, "d64")) { - iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; + iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; } else if (rc_path_compare_extension(ext, "d88")) { @@ -2219,7 +2618,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } else if (rc_path_compare_extension(ext, "fd")) { - iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ } break; @@ -2330,7 +2729,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } else if (rc_path_compare_extension(ext, "nds")) { - iterator->consoles[0] = RC_CONSOLE_NINTENDO_DS; + iterator->consoles[0] = RC_CONSOLE_NINTENDO_DS; /* ASSERT: handles both DS and DSi */ } else if (rc_path_compare_extension(ext, "n64") || rc_path_compare_extension(ext, "ndd")) @@ -2412,6 +2811,13 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } break; + case 'u': + if (rc_path_compare_extension(ext, "uze")) + { + iterator->consoles[0] = RC_CONSOLE_UZEBOX; + } + break; + case 'v': if (rc_path_compare_extension(ext, "vb")) { diff --git a/dep/rcheevos/src/rhash/md5.c b/dep/rcheevos/src/rhash/md5.c index c35d96c5e..f3a520566 100644 --- a/dep/rcheevos/src/rhash/md5.c +++ b/dep/rcheevos/src/rhash/md5.c @@ -52,6 +52,7 @@ */ #include "md5.h" +#include #include #undef BYTE_ORDER /* 1 = big-endian, -1 = little-endian, 0 = unknown */ @@ -161,7 +162,7 @@ md5_process(md5_state_t *pms, const md5_byte_t *data /*[64]*/) * On little-endian machines, we can process properly aligned * data without copying it. */ - if (!((data - (const md5_byte_t *)0) & 3)) { + if (!((ptrdiff_t)data & 3)) { /* data are properly aligned */ X = (const md5_word_t *)data; } else { diff --git a/src/core/host_display.cpp b/src/core/host_display.cpp index 5da2f140d..18cd5785d 100644 --- a/src/core/host_display.cpp +++ b/src/core/host_display.cpp @@ -675,7 +675,7 @@ bool HostDisplay::WriteScreenshotToFile(std::string filename, bool internal_reso u32 pixels_stride; GPUTexture::Format pixels_format; if (!RenderScreenshot(width, height, - Common::Rectangle::FromExtents(draw_top, draw_left, draw_width, draw_height), &pixels, + Common::Rectangle::FromExtents(draw_left, draw_top, draw_width, draw_height), &pixels, &pixels_stride, &pixels_format)) { Log_ErrorPrintf("Failed to render %ux%u screenshot", width, height); diff --git a/src/duckstation-qt/translations/duckstation-qt_es-es.ts b/src/duckstation-qt/translations/duckstation-qt_es-es.ts index 7d16afdcb..193ff528e 100644 --- a/src/duckstation-qt/translations/duckstation-qt_es-es.ts +++ b/src/duckstation-qt/translations/duckstation-qt_es-es.ts @@ -296,62 +296,62 @@ Token de inicio de sesión generado en %2. Achievements - + Loading state Cargando estado - + Resuming state Reanudando estado - + Hardcore mode disabled by state switch. El cambio de estados guardados ha desactivado el modo «hardcore». - + Hardcore mode will be enabled on system reset. El modo «hardcore» se activará al reiniciar el sistema. - + Confirm Hardcore Mode Confirmar el modo «hardcore» - + {0} cannot be performed while hardcore mode is active. Do you want to disable hardcore mode? {0} will be cancelled if you select No. No se puede utilizar la función de {0} mientras el modo «hardcore» esté activo. ¿Deseas desactivar el modo «hardcore»? Se cancelará la acción de {0} si seleccionas No. - + Hardcore mode is now enabled. El modo «hardcore» está activado. - + Hardcore mode is now disabled. El modo «hardcore» está desactivado. - + {} (Hardcore Mode) {} (modo «hardcore») - + You have earned {} of {} achievements, and {} of {} points. Has conseguido {} de {} logros y {} de {} puntos. - + This game has no achievements. No hay logros para este juego. - + Your Score: {} (Best: {}) Leaderboard Position: {} of {} Tu puntuación: {} (mejor: {}) @@ -752,77 +752,81 @@ Posición en la tabla de puntuaciones: {} de {} Forzar el modo analógico al reiniciar - Forces the controller to analog mode when the console is reset/powered on. May cause issues with games, so it is recommended to leave this option off. - Fuerza el modo analógico en el mando cuando la consola se reinicie/encienda. Puede causar problemas con los juegos, así que se recomienda dejar esta opción desactivada. + Fuerza el modo analógico en el mando cuando la consola se reinicie/encienda. Puede causar problemas con los juegos, así que se recomienda dejar esta opción desactivada. - + + Forces the controller to analog mode when the console is reset/powered on. + Fuerza el modo analógico en el mando cuando la consola se reinicie/encienda. + + + Use Analog Sticks for D-Pad in Digital Mode Usar los joysticks analógicos como cruceta en el modo digital - + Allows you to use the analog sticks to control the d-pad in digital mode, as well as the buttons. Permite usar los joysticks analógicos para controlar la cruceta y los botones en el modo digital. - + Analog Deadzone Zona muerta del joystick analógico - + Sets the analog stick deadzone, i.e. the fraction of the stick movement which will be ignored. Establece la zona muerta del joystick analógico, es decir, cuánto movimiento del joystick se ignorará. - + Analog Sensitivity Sensibilidad analógica - + Sets the analog stick axis scaling factor. A value between 130% and 140% is recommended when using recent controllers, e.g. DualShock 4, Xbox One Controller. Establece el factor de escalado para los ejes de los joysticks analógicos. Se recomienda un valor entre 130 % y 140 % cuando se usen mandos modernos, como el DualShock 4 o el mando de Xbox One. - + Button/Trigger Deadzone Zona muerta de botón/gatillo - + Sets the deadzone for activating buttons/triggers, i.e. the fraction of the trigger which will be ignored. Establece la zona de inactividad para activar botones o gatillos, es decir, la distancia de la pulsación que será ignorada. - + Vibration Bias Fuerza de vibración - + Sets the rumble bias value. If rumble in some games is too weak or not functioning, try increasing this value. Indica la medida de la vibración. Si la vibración en algunos juegos es débil o no funciona, intenta incrementar este valor. - + Invert Left Stick Invertir joystick izquierdo - + Inverts the direction of the left analog stick. Invierte la dirección del joystick analógico izquierdo. - + Invert Right Stick Invertir joystick derecho - + Inverts the direction of the right analog stick. Invierte la dirección del joystick analógico derecho. @@ -905,17 +909,17 @@ Posición en la tabla de puntuaciones: {} de {} AudioBackend - + Null (No Output) Nulo (sin salida) - + Cubeb Cubeb - + XAudio2 XAudio2 @@ -1325,17 +1329,17 @@ Posición en la tabla de puntuaciones: {} de {} CPUExecutionMode - + Interpreter (Slowest) Intérprete (el más lento) - + Cached Interpreter (Faster) Intérprete en caché (más rápido) - + Recompiler (Fastest) Recompilador (el más rápido) @@ -1343,17 +1347,17 @@ Posición en la tabla de puntuaciones: {} de {} CPUFastmemMode - + Disabled (Slowest) Deshabilitado (lo más lento) - + MMap (Hardware, Fastest, 64-Bit Only) MMap (por hardware, el más rápido, solo para 64 bits) - + LUT (Faster) LUT (más rápido) @@ -1914,7 +1918,7 @@ Posición en la tabla de puntuaciones: {} de {} CommonHostInterface - + Invalid version %u (%s version %u) Versión %u inválida (versión %s %u) @@ -3413,50 +3417,55 @@ Esta acción no puede deshacerse. ControllerType - + None Ninguno - + Digital Controller Mando digital - + Analog Controller (DualShock) Mando analógico (DualShock) - + Analog Joystick Palancas analógicas - + PlayStation Mouse PlayStation Mouse - + NeGcon NeGcon - + Analog Controller Mando analógico - + GunCon GunCon + + + Not Connected + Sin conectar + CoverDownloadDialog @@ -3997,21 +4006,26 @@ Este archivo puede llegar a ocupar varios gigabytes, así que ten cuidado con el Other Otra + + + Non-PS1 + Ajena a PS1 + DisplayAlignment - + Left / Top Superior izquierda - + Center Centrada - + Right / Bottom Inferior derecha @@ -4019,17 +4033,17 @@ Este archivo puede llegar a ocupar varios gigabytes, así que ten cuidado con el DisplayAspectRatio - + Auto (Game Native) Automática (nativa del juego) - + Auto (Match Window) Automática (ajustada a la ventana) - + Custom Personalizada @@ -4037,17 +4051,17 @@ Este archivo puede llegar a ocupar varios gigabytes, así que ten cuidado con el DisplayCropMode - + None No recortar - + Only Overscan Area Solo el área de sobrebarrido - + All Borders Todos los bordes @@ -4409,7 +4423,7 @@ Este archivo puede llegar a ocupar varios gigabytes, así que ten cuidado con el No se han encontrado estados guardados para continuar. - + Game ID: %1 Game Title: %2 Achievements: %5 (%6) @@ -4422,7 +4436,7 @@ Logros: %5 (%6) - + %n points %n punto @@ -4430,27 +4444,27 @@ Logros: %5 (%6) - + Rich presence inactive or unsupported. «Rich Presence» inactiva o no compatible. - + Game not loaded or no RetroAchievements available. No se ha cargado un juego o RetroAchievements no está disponible. - + %1x%2 %1 × %2 - + Game: %1 FPS Juego: %1 FPS - + Video: %1 FPS (%2%) Vídeo: %1 FPS (%2 %) @@ -5058,17 +5072,17 @@ Logros: %5 (%6) GPUDownsampleMode - + Disabled Deshabilitado - + Box (Downsample 3D/Smooth All) Cuadro (reducir resolución 3D/suavizar todo) - + Adaptive (Preserve 3D/Smooth 2D) Adaptable (preservar 3D/suavizar 2D) @@ -5076,27 +5090,27 @@ Logros: %5 (%6) GPURenderer - + Hardware (D3D11) Hardware (D3D11) - + Hardware (D3D12) Hardware (D3D12) - + Hardware (Vulkan) Hardware (Vulkan) - + Hardware (OpenGL) Hardware (OpenGL) - + Software Software @@ -5207,37 +5221,37 @@ Logros: %5 (%6) GPUTextureFilter - + Nearest-Neighbor Vecino más cercano - + Bilinear Bilineal - + Bilinear (No Edge Blending) Bilineal (sin unión de bordes) - + JINC2 (Slow) JINC2 (lento) - + JINC2 (Slow, No Edge Blending) JINC2 (lento, sin unión de bordes) - + xBR (Very Slow) xBR (muy lento) - + xBR (Very Slow, No Edge Blending) xBR (muy lento, sin unión de bordes) @@ -5245,72 +5259,72 @@ Logros: %5 (%6) GameList - + Disc Disco - + PS-EXE PS-EXE - + Playlist Lista de reproducción - + PSF PSF - + Never Nunca - + Today Hoy - + Yesterday Ayer - + {}h {}m {} h {} min - + {}h {}m {}s {} h {} min {} s - + {}m {}s {} min {} s - + {}s {} s - + None Ninguno - + {} hours {} horas - + {} minutes {} minutos @@ -5318,32 +5332,32 @@ Logros: %5 (%6) GameListCompatibilityRating - + Unknown Desconocido - + Doesn't Boot No inicia - + Crashes In Intro Cuelga al inicio - + Crashes In-Game Cuelga en juego - + Graphical/Audio Issues Problemas audiovisuales - + No Issues Sin problemas @@ -5545,97 +5559,97 @@ La búsqueda recursiva lleva más tiempo, pero identificará los archivos que es GameSettingsTrait - + Force Interpreter Forzar intérprete - + Force Software Renderer Forzar renderizado por software - + Force Software Renderer For Readbacks Forzar el renderizador por software para cotejar - + Force Interlacing Forzar entrelazado - + Disable True Color Deshabilitar color verdadero - + Disable Upscaling Deshabilitar escalado de resolución - + Disable Scaled Dithering Deshabilitar escalado de tramado - + Disallow Forcing NTSC Timings Deshabilitar forzado de velocidad NTSC - + Disable Widescreen Deshabilitar pantalla panorámica - + Disable PGXP Deshabilitar PGXP - + Disable PGXP Culling Deshabilitar «culling» de la PGXP - + Disable PGXP Depth Buffer Deshabilitar búfer de profundidad de la PGXP - + Force PGXP Vertex Cache Forzar caché de vértices de la PGXP - + Force PGXP CPU Mode Forzar modo CPU de la PGXP - + Force Recompiler LUT Fastmem Forzar fastmem LUT del recompilador - + Force Recompiler Memory Exceptions Forzar excepciones de memoria del recompilador - + Disable PGXP Perspective Correct Textures Deshabilitar corrección de perspectiva de texturas de la PGXP - + Disable PGXP Perspective Correct Colors Deshabilitar corrección de perspectiva de colores de la PGXP - + Force Recompiler ICache Forzar ICache del recompilador @@ -6950,7 +6964,7 @@ La búsqueda recursiva lleva más tiempo, pero identificará los archivos que es &Display - &Pantalla + Ima&gen @@ -7286,7 +7300,7 @@ La búsqueda recursiva lleva más tiempo, pero identificará los archivos que es - + Cheat Manager Administrador de trucos @@ -7622,51 +7636,51 @@ Esta acción no se puede deshacer. QDarkStyle - + Confirm Shutdown Confirmar apagado - + Are you sure you want to shut down the virtual machine? ¿Seguro que quieres apagar la máquina virtual? - + Save State For Resume Estado para continuar - - - - + + + + Memory Card Not Found Memory Card no encontrada - + Memory card '%1' does not exist. Do you want to create an empty memory card? La Memory Card «%1» no existe. ¿Quieres crear una Memory Card vacía? - + Failed to create memory card '%1' Fallo al crear la Memory Card «%1» - - + + Memory card '%1' could not be found. Try starting the game and saving to create it. No se ha encontrado la Memory Card «%1». Intenta iniciar el juego y guardar una partida para crearla. - + Do not show again No mostrar otra vez - + Using cheats can have unpredictable effects on games, causing crashes, graphical glitches, and corrupted saves. By using the cheat manager, you agree that it is an unsupported configuration, and we will not provide you with any assistance when games break. Cheats persist through save states even after being disabled, please remember to reset/reboot the game after turning off any codes. @@ -7679,17 +7693,17 @@ Los trucos persistirán a través de los estados de guardado incluso después de ¿Seguro que quieres continuar? - + Updater Error Error de actualización - + <p>Sorry, you are trying to update a DuckStation version which is not an official GitHub release. To prevent incompatibilities, the auto-updater is only enabled on official builds.</p><p>To obtain an official build, please follow the instructions under "Downloading and Running" at the link below:</p><p><a href="https://github.com/stenzek/duckstation/">https://github.com/stenzek/duckstation/</a></p> <p>Estás intentando actualizar a una versión de DuckStation no oficial de GitHub. Para evitar incompatibilidades, el actualizador automático solo buscará compilaciones oficiales.</p><p>Para obtener una versión oficial, sigue las instrucciones en el apartado «Downloading and Running» en el siguiente enlace:</p><p><a href="https://github.com/stenzek/duckstation/">https://github.com/stenzek/duckstation/</a></p> - + Automatic updating is not supported on the current platform. Las actualizaciones automáticas no son compatibles con la plataforma actual. @@ -8023,32 +8037,32 @@ Los trucos persistirán a través de los estados de guardado incluso después de MemoryCardType - + No Memory Card No utilizar una Memory Card - + Shared Between All Games Compartida entre todos los juegos - + Separate Card Per Game (Serial) Memory Card individual para cada juego (por número de serie) - + Separate Card Per Game (Title) Memory Card individual para cada juego (por título) - + Separate Card Per Game (File Title) Memory Card individual para cada juego (por nombre de archivo) - + Non-Persistent Card (Do Not Save) Memory Card no persistente (no guardar) @@ -8056,22 +8070,22 @@ Los trucos persistirán a través de los estados de guardado incluso después de MultitapMode - + Disabled Deshabilitado - + Enable on Port 1 Only Habilitar solamente en el puerto 1 - + Enable on Port 2 Only Habilitar solamente en el puerto 2 - + Enable on Ports 1 and 2 Habilitar en los puertos 1 y 2 @@ -8161,17 +8175,17 @@ Los trucos persistirán a través de los estados de guardado incluso después de Renderizador de OpenGL no disponible, tu hardware o tus controladores no son lo suficientemente modernos. Se requiere de OpenGL 3.1 o de OpenGL ES 3.0. - + System reset. Reiniciando sistema. - + Loading state from '%s' failed. Resetting. Fallo al cargar el estado «%s». Reiniciando. - + Saving state to '%s' failed. Fallo al guardar el estado «%s». @@ -8196,37 +8210,37 @@ Los trucos persistirán a través de los estados de guardado incluso después de Se ha desactivado la función de rebobinado porque la predicción de latencia está activada. - + Switching to %s%s GPU renderer. Cambiando al renderizador de GPU %s%s. - + Switching to %s audio backend. Cambiando al motor de audio %s. - + Switching to %s CPU execution mode. Cambiando al modo de ejecución de CPU %s. - + Recompiler options changed, flushing all blocks. Las opciones del recompilador han cambiado, limpiando los bloques. - + PGXP enabled, recompiling all blocks. PGXP habilitado, recompilando todos los bloques. - + PGXP disabled, recompiling all blocks. PGXP deshabilitado, recompilando todos los bloques. - + Switching to %s renderer... Cambiando al renderizador %s... @@ -8281,84 +8295,84 @@ Los trucos persistirán a través de los estados de guardado incluso después de La Memory Card %u está presente en el sistema, pero no en el estado guardado. Quitando Memory Card. - + CD image preloading not available for multi-disc image '%s' La precarga de imagen de CD no está disponible para la imagen de varios discos «%s» - + Precaching CD image failed, it may be unreliable. Fallo al precachear la imagen de CD, el sistema podría ser inestable. - + CPU clock speed is set to %u%% (%u / %u). This may result in instability. La velocidad de reloj de la CPU está establecida en %u%% (%u / %u). Podría haber inestabilidades. - + CD-ROM read speedup set to %ux (effective speed %ux). This may result in instability. Aceleración de lectura del CDROM establecida en %ux (velocidad efectiva %ux). Podría haber inestabilidades. - + CD-ROM seek speedup set to instant. This may result in instability. Aceleración de búsqueda de CD-ROM establecida como instantánea. Podría haber inestabilidades. - + CD-ROM seek speedup set to %ux. This may result in instability. Aceleración de búsqueda de CD-ROM establecida en %ux. Podría haber inestabilidades. - + Failed to initialize %s renderer, falling back to software renderer. Fallo al inicializar el renderizador %s, cambiando al renderizador por software. - + WARNING: CPU overclock (%u%%) was different in save state (%u%%). AVISO: la velocidad de «overclocking» de la CPU (%u %%) es diferente a la del estado guardado (%u %%). - + Failed to open CD image from save state '%s': %s. Using existing image '%s', this may result in instability. Fallo al abrir la imagen de CD del estado guardado «%s»: %s. Usando la imagen existente «%s», podría haber inestabilidades. - + Failed to open disc image '%s': %s. Fallo al abrir la imagen de disco «%s»: %s. - + Failed to switch to subimage %u in '%s': %s. Error al cambiar la subimagen %u en «%s»: %s. - + Switched to sub-image %s (%u) in '%s'. Cambiado a la subimagen %s (%u) en «%s». - + Inserted disc '%s' (%s). Disco «%s» introducido (%s). - - + + Failed to load post processing shader chain. Error al cargar la cadena de sombreadores de posprocesamiento. - + No cheats are loaded. No hay trucos cargados. - + %n cheats are now active. %n truco está habilitado. @@ -8366,7 +8380,7 @@ Los trucos persistirán a través de los estados de guardado incluso después de - + %n cheats are now inactive. %n truco está deshabilitado. @@ -8441,12 +8455,12 @@ Los trucos persistirán a través de los estados de guardado incluso después de Reemplazos de textura recargados. - + Failed to save undo load state. Fallo al guardar la acción para deshacer la carga del estado. - + Rewinding is not enabled. El rebobinado no está habilitado. @@ -8484,37 +8498,37 @@ Los trucos persistirán a través de los estados de guardado incluso después de Audio de CD activado. - + Started dumping audio to '%s'. Comenzando a volcar audio en «%s». - + Failed to start dumping audio to '%s'. Fallo al iniciar el volcado de audio en «%s». - + Stopped dumping audio. Volcado de audio finalizado. - + Screenshot file '%s' already exists. El archivo de captura «%s» ya existe. - + Failed to save screenshot to '%s' Fallo al guardar la captura «%s» - + Screenshot saved to '%s'. Captura de pantalla guardada en «%s». - + Controller in port %u (%s) is not supported for %s. Supported controllers: %s Please configure a supported controller from the list above. @@ -8523,32 +8537,32 @@ Mandos soportados: %s Selecciona un control de la lista superior. - + Failed to load cheats from '%s'. Fallo al cargar trucos de «%s». - + Swapped memory card ports. Both ports have a memory card. Intercambiados los puertos de Memory Card. Ambos tienen una Memory Card. - + Swapped memory card ports. Port 2 has a memory card, Port 1 is empty. Intercambiados los puertos de Memory Card. El puerto 2 contiene una Memory Card y el puerto 1 está vacío. - + Swapped memory card ports. Port 1 has a memory card, Port 2 is empty. Intercambiados los puertos de Memory Card. El puerto 1 contiene una Memory Card y el puerto 2 está vacío. - + Swapped memory card ports. Neither port has a memory card. Intercambiados los puertos de Memory Card. Ninguno tiene una Memory Card. - + Saved %n cheats to '%s'. Se ha guardado %n truco en «%s». @@ -8556,42 +8570,42 @@ Selecciona un control de la lista superior. - + Widescreen hack is now enabled, and aspect ratio is set to %s. Hack para pantallas panorámicas habilitado, relación de aspecto configurada en %s. - + Widescreen hack is now disabled, and aspect ratio is set to %s. Hack para pantallas panorámicas deshabilitado, relación de aspecto configurada en %s. - + Failed to save cheat list to '%s' Fallo al guardar la lista de trucos en «%s» - + Loading state from '{}'... Cargando estado de «{}»... - + Save State Guardar estado - + State saved to '{}'. Estado guardado en «{}». - + This save state was created with a different BIOS version or patch options. This may cause stability issues. Este estado de guardado se creó con una versión de la BIOS o con opciones de parche distintas. Podría haber problemas de estabilidad. - + %n cheats are enabled. This may result in instability. %n truco habilitado. Podría haber inestabilidades. @@ -8599,127 +8613,127 @@ Selecciona un control de la lista superior. - + Deleted cheat list '%s'. Lista de trucos «%s» borrada. - + Cheat '%s' enabled. Truco «%s» habilitado. - + Cheat '%s' disabled. Truco «%s» deshabilitado. - + Applied cheat '%s'. Aplicado truco «%s». - + Cheat '%s' is already enabled. El truco «%s» ya está activado. - + Post-processing is now enabled. Posprocesamiento habilitado. - + Post-processing is now disabled. Posprocesamiento deshabilitado. - + Failed to load post-processing shader chain. Fallo al cargar la cadena de sombreadores de posprocesamiento. - + Post-processing shaders reloaded. Sombreadores de posprocesamiento recargados. - + CPU interpreter forced by game settings. Intérprete de CPU forzado por la configuración del juego. - + Software renderer forced by game settings. Renderizado por software forzado por la configuración del juego. - + Using software renderer for readbacks based on game settings. Utilizando renderizador por software para cotejar por lo indicado en la configuración del juego. - + Interlacing forced by game settings. Entrelazado forzado por la configuración del juego. - + True color disabled by game settings. Color verdadero deshabilitado por la configuración del juego. - + Upscaling disabled by game settings. Escalado deshabilitado por la configuración del juego. - + Scaled dithering disabled by game settings. Escalado de tramado deshabilitado por la configuración del juego. - + Widescreen disabled by game settings. Pantalla panorámica deshabilitada por la configuración del juego. - + Forcing NTSC Timings disallowed by game settings. Forzado de velocidad NTSC deshabilitado por la configuración del juego. - + PGXP geometry correction disabled by game settings. Corrección de geometría de la PGXP deshabilitada por la configuración del juego. - + PGXP culling disabled by game settings. «Culling» de la PGXP deshabilitado por la configuración del juego. - + PGXP perspective corrected textures disabled by game settings. Corrección de perspectiva de texturas de la PGXP deshabilitada por la configuración del juego. - + PGXP perspective corrected colors disabled by game settings. Corrección de perspectiva de colores de la PGXP deshabilitada por la configuración del juego. - + PGXP vertex cache forced by game settings. Caché de vértices de la PGXP forzada por la configuración del juego. - + PGXP CPU mode forced by game settings. Modo CPU de la PGXP forzado por la configuración del juego. - + PGXP Depth Buffer disabled by game settings. Búfer de profundidad de la PGXP deshabilitado por la configuración del juego. @@ -8917,29 +8931,29 @@ La URL era: %1 QtHost - - - + + + Error Error - + File '%1' does not exist. El archivo «%1» no existe. - + The specified save state does not exist. El estado de guardado indicado no existe. - + Cannot use no-gui mode, because no boot filename was specified. No se puede utilizar el modo «no-gui» porque no se ha especificado un nombre del archivo de arranque. - + Cannot use batch mode, because no boot filename was specified. No se puede utilizar el modo por lotes porque no se ha especificado un nombre del archivo de arranque. @@ -9227,68 +9241,68 @@ La URL era: %1 System - + Failed to load %s BIOS. Fallo al cargar la BIOS %s. - - + + Error Error - + Failed to load save state file '{}' for booting. Fallo al arrancar con el archivo de estado «{}». - + Incorrect BIOS image size Tamaño de imagen de la BIOS incorrecto - + Save state is incompatible: minimum version is %u but state is version %u. El estado guardado es incompatible: la versión mínima soportada es %u, pero el estado es para la versión %u. - + Save state is incompatible: maximum version is %u but state is version %u. El estado guardado es incompatible: la versión máxima soportada es %u, pero el estado es para la versión %u. - + Failed to open CD image '%s' used by save state: %s. Fallo al abrir la imagen de CD «%s» del estado guardado %s. - + Failed to switch to subimage %u in CD image '%s' used by save state: %s. Fallo al cambiar la subimagen %u en el CD «%s» utilizado por el estado guardado %s. - + Per-game memory card cannot be used for slot %u as the running game has no code. Using shared card instead. No puede usarse una Memory Card individual para el juego en la ranura %u ya que el juego no tiene un código. Se usará una Memory Card compartida. - + Per-game memory card cannot be used for slot %u as the running game has no title. Using shared card instead. No puede usarse una Memory Card individual para el juego en la ranura %u ya que el juego no tiene un título. Se usará una Memory Card compartida. - + Per-game memory card cannot be used for slot %u as the running game has no path. Using shared card instead. No puede usarse una Memory Card individual para el juego en la ranura %u ya que el juego no tiene una ruta. Se usará una Memory Card compartida. - + Game changed, reloading memory cards. Juego cambiado, volviendo a cargar las Memory Cards. - + You are attempting to run a libcrypt protected game without an SBI file: %s: %s @@ -9309,7 +9323,7 @@ Revisa las instrucciones del archivo README sobre como agregar un archivo SBI. ¿Quieres continuar? - + You are attempting to run a libcrypt protected game without an SBI file: %s: %s diff --git a/src/duckstation-qt/translations/duckstation-qt_pt-br.ts b/src/duckstation-qt/translations/duckstation-qt_pt-br.ts index ac64aa075..e61b1c187 100644 --- a/src/duckstation-qt/translations/duckstation-qt_pt-br.ts +++ b/src/duckstation-qt/translations/duckstation-qt_pt-br.ts @@ -215,7 +215,7 @@ função, considere fazer sua conta em:<a href="https://retroachievement Enable Hardcore Mode - Ativar modo dificílimo + Ativar modo hardcore Enable Hardcode Mode @@ -359,57 +359,57 @@ Token gerado %2. Achievements - + Loading state Carregando estado - + Resuming state Retomando estado - + Hardcore mode disabled by state switch. Modo dificílimo desligado pelo estado salvo. - + Hardcore mode will be enabled on system reset. Modo dificílimo será ligado assim que o sistema for reiniciado. - + Confirm Hardcore Mode Confirmar modo dificílimo - + {0} cannot be performed while hardcore mode is active. Do you want to disable hardcore mode? {0} will be cancelled if you select No. {0} não é possível no momento enquanto o modo dificílimo estiver ligado. Deseja desativar o dificílimo? {0} será cancelado se você escolher Não. - + Hardcore mode is now enabled. O modo Hardcore está ativado. - + Hardcore mode is now disabled. O modo Hardcore está desligado. - + {} (Hardcore Mode) {} (Modo dificílimo) - + You have earned {} of {} achievements, and {} of {} points. Você ganhou {} de {} conquistas e {} de {} pontos. - + This game has no achievements. Este jogo não possui conquistas. @@ -422,7 +422,7 @@ Token gerado %2. As tabelas de classificação estão desligadas porque o modo dificílimo está desativado. - + Your Score: {} (Best: {}) Leaderboard Position: {} of {} Sua pontuação: {} (Melhor: : {}) @@ -886,52 +886,57 @@ Posição nos placares de lideres: {} de {} Inverter Esquerda/Direita + Cima/Baixo - + + Forces the controller to analog mode when the console is reset/powered on. + Força o controle a entrar no modo analógico quando o console é reiniciado/ligado. + + + Analog Deadzone Zona morta do analógico - + Sets the analog stick deadzone, i.e. the fraction of the stick movement which will be ignored. Define a zona morta do analógico, por exemplo:. a fração do movimento do analógico que será ignorada. - + Analog Sensitivity Sensibilidade do analógico - + Sets the analog stick axis scaling factor. A value between 130% and 140% is recommended when using recent controllers, e.g. DualShock 4, Xbox One Controller. Define o fator de escala do eixo do analógico. Um valor entre 130% e 140% é recomendável quando estiver usando controles mais recentes, por exemplo: Dualshock 4 e controles de Xbox One. - + Button/Trigger Deadzone Botão/Gatilho zona morta - + Sets the deadzone for activating buttons/triggers, i.e. the fraction of the trigger which will be ignored. Define a zona morta para acionamento de botões e gatilhos, ou seja, a fração do gatilho que será ignorada. - + Invert Left Stick Inverter analógico esquerdo - + Inverts the direction of the left analog stick. Inverte a direção do controle analógico esquerdo. - + Invert Right Stick Inverter analógico direito - + Inverts the direction of the right analog stick. Inverte a direção do controle analógico direito. @@ -1027,9 +1032,8 @@ Posição nos placares de lideres: {} de {} Forçar modo analógico ao reiniciar - Forces the controller to analog mode when the console is reset/powered on. May cause issues with games, so it is recommended to leave this option off. - Força os controles a ficarem no modo analógico quando o console é reiniciado ou religado. Pode causar problemas em alguns jogos, portanto considere deixar esta opção desligada. + Força os controles a ficarem no modo analógico quando o console é reiniciado ou religado. Pode causar problemas em alguns jogos, portanto considere deixar esta opção desligada. Enable Analog Mode on Reset @@ -1040,12 +1044,12 @@ Posição nos placares de lideres: {} de {} Ativa o modo analógico automaticamente quando o console é reiniciado / desligado. - + Use Analog Sticks for D-Pad in Digital Mode Usar analógicos como D-Pad no modo digital - + Allows you to use the analog sticks to control the d-pad in digital mode, as well as the buttons. Te permite usar os analógicos como um direcional (D-Pad) no modo digital, assim como os botões. @@ -1058,12 +1062,12 @@ Posição nos placares de lideres: {} de {} Define o fator de escala do eixo do analógico. Um valor entre 1.30 e 1.40 é recomendável quando estiver usando controles mais recentes, por exemplo: Dualshock 4 e controles de Xbox One. - + Vibration Bias Vibração - + Sets the rumble bias value. If rumble in some games is too weak or not functioning, try increasing this value. Define valores de vibração. Se a vibração em alguns jogos estiver fraca ou não funcionar, tente aumentar estes valores. @@ -1242,17 +1246,17 @@ Posição nos placares de lideres: {} de {} AudioBackend - + Null (No Output) Mudo - + Cubeb Cubed - + XAudio2 XAudio2 @@ -1741,17 +1745,17 @@ Posição nos placares de lideres: {} de {} Interpretador (Mais Lento) - + Interpreter (Slowest) Interpretador (mais lento) - + Cached Interpreter (Faster) Interpretador armazenado (rápido) - + Recompiler (Fastest) Recompilador (mais rápido) @@ -1759,17 +1763,17 @@ Posição nos placares de lideres: {} de {} CPUFastmemMode - + Disabled (Slowest) Desativado (Lento) - + MMap (Hardware, Fastest, 64-Bit Only) MMap (hardware, mais rápido) - + LUT (Faster) LUT (rápido) @@ -2416,7 +2420,7 @@ Posição nos placares de lideres: {} de {} O estado atual será salvo. - + Invalid version %u (%s version %u) Versão inválida %u (%s versão %u) @@ -4238,24 +4242,24 @@ Esta ação não poderá ser desfeita. ControllerType - + None Nenhum - + Digital Controller Controle digital - + Analog Controller (DualShock) Controle analógico (dualshock) - + Analog Joystick Controle analógico @@ -4265,24 +4269,24 @@ Esta ação não poderá ser desfeita. - + PlayStation Mouse Mouse Playstation - + NeGcon NeGcon - + Analog Controller Controle analógico - + GunCon GunCon @@ -4894,21 +4898,26 @@ This file can be several gigabytes, so be aware of SSD wear. Other Outros + + + Non-PS1 + + DisplayAlignment - + Left / Top Topo superior - + Center Centro - + Right / Bottom Esquerda inferior @@ -4916,17 +4925,17 @@ This file can be several gigabytes, so be aware of SSD wear. DisplayAspectRatio - + Auto (Game Native) Auto (resolução nativa) - + Auto (Match Window) Auto (corresponder a janela) - + Custom Personalizado @@ -4934,17 +4943,17 @@ This file can be several gigabytes, so be aware of SSD wear. DisplayCropMode - + None Nenhuma - + Only Overscan Area Somente área renderizada - + All Borders Todas as bordas @@ -5382,7 +5391,7 @@ This file can be several gigabytes, so be aware of SSD wear. Salvamento rápido não encontrado. - + Game ID: %1 Game Title: %2 Achievements: %5 (%6) @@ -5395,7 +5404,7 @@ Conquistas: %5 (%6) - + %n points %n pontos @@ -5403,27 +5412,27 @@ Conquistas: %5 (%6) - + Rich presence inactive or unsupported. Presença rica do Discord inativa ou não suportada. - + Game not loaded or no RetroAchievements available. Jogo não carregado ou sem conquistas disponíveis. - + %1x%2 %1x%2 - + Game: %1 FPS Jogo: %1 FPS - + Video: %1 FPS (%2%) Vídeo: %1 FPS (%2%) @@ -5672,8 +5681,8 @@ Conquistas: %5 (%6) Rewind for %n frame(s), lasting %1 second(s) will require up to %2MB of RAM and %3MB of VRAM. - Retroceder por % quadros, durará %1 segundo(s) que exigirá pouco mais de %2MB de RAM e %3MB de V-RAM. - + Retroceder por %n quadro, durará %1 segundo o que exigirá pouco mais de %2MB de RAM e %3MB de V-RAM. + Retroceder por %n quadros, durará %1 segundo(s) que exigirá pouco mais de %2MB de RAM e %3MB de V-RAM. @@ -6092,17 +6101,17 @@ Conquistas: %5 (%6) GPUDownsampleMode - + Disabled Desativado - + Box (Downsample 3D/Smooth All) Misto (Reduz 3D / Suaviza tudo) - + Adaptive (Preserve 3D/Smooth 2D) Adaptativo (Preserva o 3D / Suaviza 2D) @@ -6110,27 +6119,27 @@ Conquistas: %5 (%6) GPURenderer - + Hardware (D3D11) Placa de vídeo (D3D11) - + Hardware (D3D12) Placa de vídeo (D3D12) - + Hardware (Vulkan) Placa de vídeo (Vulkan) - + Hardware (OpenGL) Placa de vídeo (OpenGL) - + Software Software @@ -6553,32 +6562,32 @@ Conquistas: %5 (%6) GPUTextureFilter - + Nearest-Neighbor Nearest-Neighbor - + Bilinear Bi-linear - + JINC2 (Slow) JINC2 (Lento) - + JINC2 (Slow, No Edge Blending) JINC2 (Lento, sem AA) - + xBR (Very Slow) xBR (Muito lento) - + xBR (Very Slow, No Edge Blending) xBR (Muito lento sem AA) @@ -6587,7 +6596,7 @@ Conquistas: %5 (%6) JINC2 - + Bilinear (No Edge Blending) Bi-linear (sem AA) @@ -6607,72 +6616,72 @@ Conquistas: %5 (%6) GameList - + Disc Disco - + PS-EXE - + Playlist Lista de reprodução - + PSF - + Never Nunca - + Today Hoje - + Yesterday Ontem - + {}h {}m {}h {}m - + {}h {}m {}s {}h {}m {}s - + {}m {}s {}m {}s - + {}s {}s - + None Nenhum - + {} hours {} horas - + {} minutes {} minutos @@ -6680,32 +6689,32 @@ Conquistas: %5 (%6) GameListCompatibilityRating - + Unknown Desconhecido - + Doesn't Boot Não funciona - + Crashes In Intro Quebra logo no início - + Crashes In-Game Quebra durante o jogo - + Graphical/Audio Issues Problemas de áudio e vídeo - + No Issues Sem problemas @@ -7457,12 +7466,12 @@ This will download approximately 4 megabytes over your current internet connecti GameSettingsTrait - + Force Interpreter Forçar interpretador - + Force Software Renderer Forçar renderização por software @@ -7471,57 +7480,57 @@ This will download approximately 4 megabytes over your current internet connecti Ativar Entrelaçamento - + Force Software Renderer For Readbacks Forçar modo software para releituras - + Force Interlacing Forçar o entrelaçamento - + Disable True Color Desativar cor real - + Disable Upscaling Desativar escalonamento - + Disable Scaled Dithering Desativar dithering escalonado - + Disallow Forcing NTSC Timings Desativa os temporizadores em NTSC - + Disable Widescreen Desativar ajuste de tela panorâmica - + Disable PGXP Desativar PGXP - + Disable PGXP Culling Desativar correção de curvas - + Disable PGXP Perspective Correct Textures Desativar correção de texturas de perspectiva PGXP - + Disable PGXP Perspective Correct Colors Desativar correção de cores de perspectiva PGXP @@ -7530,22 +7539,22 @@ This will download approximately 4 megabytes over your current internet connecti Desligar correção de textura (PGXP) - + Disable PGXP Depth Buffer Desligar PGXP modo eixo Z - + Force PGXP Vertex Cache Forçar armazenamento em modo PGXP - + Force PGXP CPU Mode Força o PGXP em modo CPU - + Force Recompiler LUT Fastmem Forçar recompilador LUT fastmen @@ -7554,12 +7563,12 @@ This will download approximately 4 megabytes over your current internet connecti Desativar Modo Analógico Forçado ao Reiniciar - + Force Recompiler Memory Exceptions Forçar exceções de memória do recompilador - + Force Recompiler ICache Força recompilador em modo armazenado (ICache) @@ -9682,7 +9691,7 @@ Você deseja que este arquivo seja carregado ou que seja reiniciado novamente? - + Cheat Manager Gerenciador de trapaças @@ -9852,51 +9861,51 @@ Esta ação não poderá ser desfeita. Escuro - + Confirm Shutdown Confirmar desligamento - + Are you sure you want to shut down the virtual machine? Tem certeza de que deseja desligar a máquina virtual? - + Save State For Resume Salvar estado e continuar - - - - + + + + Memory Card Not Found Cartão de memória não encontrado - + Memory card '%1' does not exist. Do you want to create an empty memory card? Cartão de memória '%1' não existe, você deseja criar um cartão de memória vazio? - + Failed to create memory card '%1' Falha ao criar cartão de memória '%1' - - + + Memory card '%1' could not be found. Try starting the game and saving to create it. Cartão de memória '%1' não encotrado. Experimente iniciar o jogo e salvá-lo para que ele seja criado. - + Do not show again Não mostrar de novo - + Using cheats can have unpredictable effects on games, causing crashes, graphical glitches, and corrupted saves. By using the cheat manager, you agree that it is an unsupported configuration, and we will not provide you with any assistance when games break. Cheats persist through save states even after being disabled, please remember to reset/reboot the game after turning off any codes. @@ -9909,17 +9918,17 @@ As trapaças ficam guardadas no estado de salvamento rápido mesmo após serem d Tem certeza de que deseja continuar? - + Updater Error Erro na atualização - + <p>Sorry, you are trying to update a DuckStation version which is not an official GitHub release. To prevent incompatibilities, the auto-updater is only enabled on official builds.</p><p>To obtain an official build, please follow the instructions under "Downloading and Running" at the link below:</p><p><a href="https://github.com/stenzek/duckstation/">https://github.com/stenzek/duckstation/</a></p> <p>Desculpe, mas você está tentando atualizar uma versão não oficial do Duckstation. Para evitarmos imcompatibilidade, o atualizador automático só poderá ser usado nas versões oficiais! </p><p>Para obtê-las, siga as instruções de como e onde no link "Baixando e rodando" conforme abaixo:</p><p><a href="https://github.com/stenzek/duckstation/">https://github.com/stenzek/duckstation/</a></p> - + Automatic updating is not supported on the current platform. Atualizações automáticas não são compatíveis com a plataforma atual. @@ -10297,22 +10306,22 @@ Tem certeza de que deseja continuar? MemoryCardType - + No Memory Card Sem cartão de memória - + Shared Between All Games Compartilhada entre jogos - + Separate Card Per Game (Serial) Cartão separado por jogo (Serial) - + Separate Card Per Game (Title) Cartão separado por jogo (título) @@ -10325,12 +10334,12 @@ Tem certeza de que deseja continuar? Separar cartão por jogo (título do jogo) - + Separate Card Per Game (File Title) Separar cartão por jogo (título do arquivo) - + Non-Persistent Card (Do Not Save) Cartão não persistente (não salvar) @@ -10338,17 +10347,17 @@ Tem certeza de que deseja continuar? MultitapMode - + Disabled Desativado - + Enable on Port 1 Only Ativar somente na porta 1 - + Enable on Port 2 Only Ativar somente na porta 2 @@ -10357,7 +10366,7 @@ Tem certeza de que deseja continuar? Ativar somente na Porta 1 - + Enable on Ports 1 and 2 Ativar nas portas 1 e 2 @@ -10483,7 +10492,7 @@ Tem certeza de que deseja continuar? OSDMessage - + System reset. Sistema reiniciado. @@ -10492,12 +10501,12 @@ Tem certeza de que deseja continuar? Estado carregado de '%s'... - + Loading state from '%s' failed. Resetting. Carregamento de estado '%s'.falhou. reiniciando. - + Saving state to '%s' failed. Falha ao salvar estado em '%s'. @@ -10526,7 +10535,7 @@ Tem certeza de que deseja continuar? Função de retrocesso desligada porque o avanço rápido está ligado. - + Recompiler options changed, flushing all blocks. As opções do recompilador foram alteradas, limpando todos os blocos. @@ -10539,17 +10548,17 @@ Tem certeza de que deseja continuar? Fastmem não disponível nesta plataforma, LUT será usado no lugar. - + Switching to %s%s GPU renderer. Mudando renderizador de GPU para %s%s. - + Switching to %s audio backend. Mudando tipo de saída de som para %s. - + Switching to %s CPU execution mode. Mudando para modo de execução %s. @@ -10570,24 +10579,24 @@ Tem certeza de que deseja continuar? Icache desativado, limpando blocos. - + PGXP enabled, recompiling all blocks. PGXP ativado, reconstruindo blocos. - + PGXP disabled, recompiling all blocks. PGXP desativado, reconstruindo blocos. - + Switching to %s renderer... Mudando para %s... - - + + Failed to load post processing shader chain. Falha ao carregar shader escolhido. @@ -10600,7 +10609,7 @@ Tem certeza de que deseja continuar? Limitador de Velocidade Desativado. - + %n cheats are now active. @@ -10608,7 +10617,7 @@ Tem certeza de que deseja continuar? - + %n cheats are now inactive. @@ -10651,12 +10660,12 @@ Tem certeza de que deseja continuar? Texturas personalizadas recarregadas. - + Failed to save undo load state. Falha ao desfazer carregar estado. - + Rewinding is not enabled. O retrocesso não está habilitado. @@ -10741,37 +10750,37 @@ Tem certeza de que deseja continuar? Perfil de controle carregado de '%s' - + Started dumping audio to '%s'. Iniciado despejo de áudio em '%s'. - + Failed to start dumping audio to '%s'. Falha ao iniciar despejo de áudio em '%s'. - + Stopped dumping audio. Despejo de áudio terminado. - + Screenshot file '%s' already exists. Captura de tela '%s' já existe. - + Failed to save screenshot to '%s' Falha ao salvar captura em '%s' - + Screenshot saved to '%s'. Captura de tela salva em '%s'. - + Controller in port %u (%s) is not supported for %s. Supported controllers: %s Please configure a supported controller from the list above. @@ -10786,12 +10795,12 @@ Please configure a supported controller from the list above. Usando perfil de controle '%s'. - + Failed to load cheats from '%s'. Falha ao carregar '%s' trapaças. - + %n cheats are enabled. This may result in instability. %n trapaças ativadas. Isso pode resultar em instabilidade. @@ -10799,32 +10808,32 @@ Please configure a supported controller from the list above. - + Widescreen hack is now enabled, and aspect ratio is set to %s. Ajuste de tela panorâmica ligado, e a proporção da imagem está definida como %s. - + Widescreen hack is now disabled, and aspect ratio is set to %s. Ajuste de tela panorâmica desligado, e a proporção da imagem está definida como %s. - + Swapped memory card ports. Both ports have a memory card. Portas de cartão de memória trocadas. ambas já contém cartão. - + Swapped memory card ports. Port 2 has a memory card, Port 1 is empty. Portas de cartão de memória trocadas. porta 2 tem um cartão de memória, porta 1 vazia. - + Swapped memory card ports. Port 1 has a memory card, Port 2 is empty. Portas de cartão de memória trocadas. porta 1 tem um cartão de memória, porta 2 vazia. - + Swapped memory card ports. Neither port has a memory card. Portas de cartão de memória trocadas. nenhuma das portas possui cartão de memória. @@ -10841,27 +10850,27 @@ Please configure a supported controller from the list above. Trapaça %u.salva para '%s'. - + Deleted cheat list '%s'. Trapaça '%s' apagada da lista. - + Cheat '%s' enabled. Trapaça '%s' ativada. - + Cheat '%s' disabled. Trapaça '%s' desativada. - + Failed to save cheat list to '%s' Falha ao salvar lista de trapaças para '%s' - + No cheats are loaded. Nenhuma trapaça foi carregada. @@ -10923,7 +10932,7 @@ Please configure a supported controller from the list above. - + Saved %n cheats to '%s'. Salvo %n trapaça em '%s'. @@ -10931,97 +10940,97 @@ Please configure a supported controller from the list above. - + Applied cheat '%s'. Trapaça aplicada '%s'. - + Cheat '%s' is already enabled. Trapaça '%s' já está ativada - + Post-processing is now enabled. Pós-processamento ligado. - + Post-processing is now disabled. Pós-processamento desligado. - + Failed to load post-processing shader chain. Falha ao carregar texturas de pós-processamento. - + Post-processing shaders reloaded. Texturas de pós-processamento recarregadas. - + CPU interpreter forced by game settings. Configurado o interpretador por CPU pela configuração individual. - + Software renderer forced by game settings. Renderização por software forçado pelas configurações individuais. - + Using software renderer for readbacks based on game settings. Usando o renderizador por software para releituras com base nas configurações individuais. - + Interlacing forced by game settings. Entrelaçamento forçado pela configuração individual. - + True color disabled by game settings. Efeito cor real desativado pelas configurações individuais. - + Upscaling disabled by game settings. Escalonamento desativado pelas configurações individuais. - + Scaled dithering disabled by game settings. Dithering escalonado desligado pelas configurações individuais. - + Widescreen disabled by game settings. Ajuste de tela panorâmica desativado pelas configurações. - + Forcing NTSC Timings disallowed by game settings. Temporizadores NTSC não permitidos pela configuração individual. - + PGXP geometry correction disabled by game settings. Correção geométrica desativada pelas configurações individuais. - + PGXP culling disabled by game settings. Correção de curvas desativada pela configuração individual. - + PGXP perspective corrected textures disabled by game settings. Perspectiva PGXP corrigiu as texturas desativadas pelas configurações por jogo. - + PGXP perspective corrected colors disabled by game settings. Perspectiva PGXP corrigiu as cores desativadas pelas configurações por jogo. @@ -11030,17 +11039,17 @@ Please configure a supported controller from the list above. Correção de curvas desligada pela configuração individual. - + PGXP vertex cache forced by game settings. Vértice armazenado forçado pelas configurações individuais. - + PGXP CPU mode forced by game settings. PGXP em modo CPU forçado pelas configurações individuais. - + PGXP Depth Buffer disabled by game settings. PGXP modo eixo Z desativado pelas configurações individuais. @@ -11092,12 +11101,12 @@ Please configure a supported controller from the list above. Cartão de memória %u presente no sistema mas não no estado salvo, removendo cartão. - + CD image preloading not available for multi-disc image '%s' O pré-carregamento da imagem do CD não está disponível para jogos de múltiplos discos '%s' - + Precaching CD image failed, it may be unreliable. Pré-alocação de disco falhou, pode ser que a imagem esteja danificada. @@ -11106,72 +11115,72 @@ Please configure a supported controller from the list above. Falha ao aplicar modificação ppf '%1s', usando imagem limpa. - + Loading state from '{}'... Estado carregado de '{}'... - + Save State Salvar estado - + State saved to '{}'. Estado salvo em '{}'. - + CPU clock speed is set to %u%% (%u / %u). This may result in instability. Velocidade do CPU foi mudada para %u%% isto resultará em instabilidades. - + CD-ROM read speedup set to %ux (effective speed %ux). This may result in instability. Leitura do CD-rom acelerada para %ux (velocidade apropriada %u). pode resultar em instabilidades. - + CD-ROM seek speedup set to instant. This may result in instability. Aumento de velocidade de busca do CD-ROM definida para instantânea. isso pode resultar em instabilidade. - + CD-ROM seek speedup set to %ux. This may result in instability. Aumento de velocidade de busca do CD-ROM definida para % ux. isso pode resultar em instabilidade. - + Failed to initialize %s renderer, falling back to software renderer. Falha ao inicializar o renderizador %s , retornando para renderizador por software. - + This save state was created with a different BIOS version or patch options. This may cause stability issues. O salvamento automático foi criado com uma versão do BIOS diferente ou modificada. Isto acarretará em problemas. - + WARNING: CPU overclock (%u%%) was different in save state (%u%%). ATENÇÃO: Aumento da velocidade (%u%%) era diferente do que no seu save anterior (%u%%). - + Failed to open CD image from save state '%s': %s. Using existing image '%s', this may result in instability. Falha ao abrir imagem do estado salvo '%s': %s. usando imagem existente '%s', isto, resultará em instabilidades. - + Failed to open disc image '%s': %s. Falha ao abrir o disco '%s': %s. - + Failed to switch to subimage %u in '%s': %s. Falha ao trocar para disco %u em '%s': %s. - + Switched to sub-image %s (%u) in '%s'. Mudado para segunda imagem %s (%u) em '%s'. @@ -11184,7 +11193,7 @@ Please configure a supported controller from the list above. Falha ao abrir o disco '%s'. - + Inserted disc '%s' (%s). Disco inserido '%s' (%s). @@ -11519,29 +11528,29 @@ The URL was: %1 QtHost - - - + + + Error Erro - + File '%1' does not exist. O arquivo '%1' não existe. - + The specified save state does not exist. O dado de salvamento não existe. - + Cannot use no-gui mode, because no boot filename was specified. Não é possível usar o modo no-gui, porque nenhum parâmetro de inicialização foi configurado. - + Cannot use batch mode, because no boot filename was specified. Não é possível usar este modo porque nenhum parâmetro de inicialização foi configurado. @@ -12113,53 +12122,53 @@ The saves will not be recoverable. Estado salvo incompatível: versão do mesmo esperada %u não a versão %u. - + Failed to load %s BIOS. Falha ao carregar %s BIOS. - - + + Error Erro - + Failed to load save state file '{}' for booting. Falha ao carregar o dado salvo no compartimento '{}'. - + Incorrect BIOS image size Tamanho da imagem do BIOS incorreta - + Save state is incompatible: minimum version is %u but state is version %u. Estado salvo incompatível: versão esperada %u não versão %u. - + Save state is incompatible: maximum version is %u but state is version %u. Estado salvo incompatível: versão esperada %u não versão %u. - + Failed to open CD image '%s' used by save state: %s. Falha ao abrir imagem do CD '%s' usado pelo estado salvo: %s. - + Failed to switch to subimage %u in CD image '%s' used by save state: %s. Falha ao trocar disco %u do CD '%s' usado pelo estado salvo: %s. - + Per-game memory card cannot be used for slot %u as the running game has no path. Using shared card instead. O cartão de memória individual não pôde ser usado no compartimento %u caminho não configurado. usando cartão compartilhado. - + You are attempting to run a libcrypt protected game without an SBI file: %s: %s @@ -12192,13 +12201,13 @@ Seu despejo está incompleto, você deve adicionar o arquivo SBI para rodá-lo c Falha ao abrir estado salvo: '%s'. - + Per-game memory card cannot be used for slot %u as the running game has no code. Using shared card instead. Caminho para o cartão de memória no compartimento %u não pôde ser usado o jogo iniciado não possui um códido válido. Será usado um cartão compartilhado. - + Per-game memory card cannot be used for slot %u as the running game has no title. Using shared card instead. Caminho para o cartão de memória no compartimento %u não pôde ser usado o jogo iniciado não possui um nome válido. Será usado um cartão compartilhado. @@ -12208,12 +12217,12 @@ o jogo iniciado não possui um nome válido. Será usado um cartão compartilhad Caminho para o Cartão de Memória %u incorreto, usando o padrão. - + Game changed, reloading memory cards. Jogo trocado, recarregando cartões de memória. - + You are attempting to run a libcrypt protected game without an SBI file: %s: %s diff --git a/src/frontend-common/achievements.cpp b/src/frontend-common/achievements.cpp index 940f039af..5797e2366 100644 --- a/src/frontend-common/achievements.cpp +++ b/src/frontend-common/achievements.cpp @@ -104,7 +104,7 @@ static void SendPingCallback(s32 status_code, std::string content_type, Common:: static void UnlockAchievementCallback(s32 status_code, std::string content_type, Common::HTTPDownloader::Request::Data data); static void SubmitLeaderboardCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data); + Common::HTTPDownloader::Request::Data data, u32 lboard_id); static bool s_active = false; static bool s_logged_in = false; @@ -137,7 +137,6 @@ static std::string s_rich_presence_string; static Common::Timer s_last_ping_time; static u32 s_last_queried_lboard = 0; -static u32 s_submitting_lboard_id = 0; static std::optional> s_lboard_entries; template @@ -356,7 +355,6 @@ void Achievements::ClearGameInfo(bool clear_achievements, bool clear_leaderboard } s_last_queried_lboard = 0; - s_submitting_lboard_id = 0; s_lboard_entries.reset(); } @@ -682,7 +680,8 @@ void Achievements::FrameUpdate() #ifdef WITH_RAINTEGRATION if (IsUsingRAIntegration()) { - RA_DoAchievementsFrame(); + if (!System::IsPaused()) + RA_DoAchievementsFrame(); return; } #endif @@ -692,7 +691,8 @@ void Achievements::FrameUpdate() if (HasActiveGame()) { std::unique_lock lock(s_achievements_mutex); - rc_runtime_do_frame(&s_rcheevos_runtime, &CheevosEventHandler, &PeekMemory, nullptr, nullptr); + if (!System::IsPaused()) + rc_runtime_do_frame(&s_rcheevos_runtime, &CheevosEventHandler, &PeekMemory, nullptr, nullptr); UpdateRichPresence(); if (!IsTestModeActive()) @@ -1741,7 +1741,7 @@ void Achievements::UnlockAchievementCallback(s32 status_code, std::string conten } void Achievements::SubmitLeaderboardCallback(s32 status_code, std::string content_type, - Common::HTTPDownloader::Request::Data data) + Common::HTTPDownloader::Request::Data data, u32 lboard_id) { if (!System::IsValid()) return; @@ -1755,11 +1755,7 @@ void Achievements::SubmitLeaderboardCallback(s32 status_code, std::string conten // Force the next leaderboard query to repopulate everything, just in case the user wants to see their new score s_last_queried_lboard = 0; - // RA API doesn't send us the leaderboard ID back.. hopefully we don't submit two at once :/ - if (s_submitting_lboard_id == 0) - return; - - const Leaderboard* lb = GetLeaderboardByID(std::exchange(s_submitting_lboard_id, 0u)); + const Leaderboard* lb = GetLeaderboardByID(lboard_id); if (!lb || !FullscreenUI::IsInitialized() || !g_settings.achievements_notifications) return; @@ -1870,17 +1866,16 @@ void Achievements::SubmitLeaderboard(u32 leaderboard_id, int value) return; } - std::unique_lock lock(s_achievements_mutex); - - s_submitting_lboard_id = leaderboard_id; - RAPIRequest request; request.username = s_username.c_str(); request.api_token = s_api_token.c_str(); request.game_hash = s_game_hash.c_str(); request.leaderboard_id = leaderboard_id; request.score = value; - request.Send(SubmitLeaderboardCallback); + request.Send( + [leaderboard_id](s32 status_code, const std::string& content_type, Common::HTTPDownloader::Request::Data data) { + SubmitLeaderboardCallback(status_code, content_type, std::move(data), leaderboard_id); + }); } void Achievements::AchievementPrimed(u32 achievement_id) diff --git a/src/frontend-common/platform_misc_unix.cpp b/src/frontend-common/platform_misc_unix.cpp index 0033b1ebc..c9dd0ad57 100644 --- a/src/frontend-common/platform_misc_unix.cpp +++ b/src/frontend-common/platform_misc_unix.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "common/log.h" +#include "common/scoped_guard.h" #include "common/string.h" #include "input_manager.h" #include "platform_misc.h" @@ -41,68 +42,77 @@ static bool SetScreensaverInhibitX11(bool inhibit, const WindowInfo& wi) #elif defined(USE_DBUS) #include -bool ChangeScreenSaverStateDBus(const bool inhibit_requested, const char* program_name, const char* reason) +static bool SetScreensaverInhibitDBus(const bool inhibit_requested, const char* program_name, const char* reason) { static dbus_uint32_t s_cookie; - // "error_dbus" doesn't need to be cleared in the end with "dbus_message_unref" at least if there is - // no error set, since calling "dbus_error_free" reinitializes it like "dbus_error_init" after freeing. - DBusError error_dbus; - dbus_error_init(&error_dbus); + const char* bus_method = (inhibit_requested) ? "Inhibit" : "UnInhibit"; + DBusError error; DBusConnection* connection = nullptr; + static DBusConnection* s_comparison_connection; DBusMessage* message = nullptr; DBusMessage* response = nullptr; - // Initialized here because initializations should be before "goto" statements. - const char* bus_method = (inhibit_requested) ? "Inhibit" : "UnInhibit"; - // "dbus_bus_get" gets a pointer to the same connection in libdbus, if exists, without creating a new connection. - // this doesn't need to be deleted, except if there's an error then calling "dbus_connection_unref", to free it, - // might be better so a new connection is established on the next try. - if (!(connection = dbus_bus_get(DBUS_BUS_SESSION, &error_dbus)) || (dbus_error_is_set(&error_dbus))) - goto cleanup; - if (!(message = dbus_message_new_method_call("org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", - "org.freedesktop.ScreenSaver", bus_method))) - goto cleanup; - // Initialize an append iterator for the message, gets freed with the message. DBusMessageIter message_itr; + + ScopedGuard cleanup = [&]() { + if (dbus_error_is_set(&error)) + { + Log_ErrorPrintf("SetScreensaverInhibitDBus error: %s", error.message); + dbus_error_free(&error); + } + if (message) + dbus_message_unref(message); + if (response) + dbus_message_unref(response); + }; + + dbus_error_init(&error); + // Calling dbus_bus_get() after the first time returns a pointer to the existing connection. + connection = dbus_bus_get(DBUS_BUS_SESSION, &error); + if (!connection || (dbus_error_is_set(&error))) + return false; + if (s_comparison_connection != connection) + { + dbus_connection_set_exit_on_disconnect(connection, false); + s_cookie = 0; + s_comparison_connection = connection; + } + message = dbus_message_new_method_call("org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", + "org.freedesktop.ScreenSaver", bus_method); + if (!message) + return false; + // Initialize an append iterator for the message, gets freed with the message. dbus_message_iter_init_append(message, &message_itr); if (inhibit_requested) { + // Guard against repeat inhibitions which would add extra inhibitors each generating a different cookie. + if (s_cookie) + return false; // Append process/window name. if (!dbus_message_iter_append_basic(&message_itr, DBUS_TYPE_STRING, &program_name)) - goto cleanup; + return false; // Append reason for inhibiting the screensaver. if (!dbus_message_iter_append_basic(&message_itr, DBUS_TYPE_STRING, &reason)) - goto cleanup; + return false; } else { // Only Append the cookie. if (!dbus_message_iter_append_basic(&message_itr, DBUS_TYPE_UINT32, &s_cookie)) - goto cleanup; + return false; } // Send message and get response. - if (!(response = - dbus_connection_send_with_reply_and_block(connection, message, DBUS_TIMEOUT_USE_DEFAULT, &error_dbus)) || - dbus_error_is_set(&error_dbus)) - goto cleanup; + response = dbus_connection_send_with_reply_and_block(connection, message, DBUS_TIMEOUT_USE_DEFAULT, &error); + if (!response || dbus_error_is_set(&error)) + return false; + s_cookie = 0; if (inhibit_requested) { // Get the cookie from the response message. - if (!dbus_message_get_args(response, &error_dbus, DBUS_TYPE_UINT32, &s_cookie, DBUS_TYPE_INVALID)) - goto cleanup; + if (!dbus_message_get_args(response, &error, DBUS_TYPE_UINT32, &s_cookie, DBUS_TYPE_INVALID) || + dbus_error_is_set(&error)) + return false; } - dbus_message_unref(message); - dbus_message_unref(response); return true; -cleanup: - if (dbus_error_is_set(&error_dbus)) - dbus_error_free(&error_dbus); - if (connection) - dbus_connection_unref(connection); - if (message) - dbus_message_unref(message); - if (response) - dbus_message_unref(response); - return false; } #endif @@ -110,7 +120,7 @@ cleanup: static bool SetScreensaverInhibit(bool inhibit) { #ifdef USE_DBUS - return ChangeScreenSaverStateDBus(inhibit, "DuckStation", "DuckStation VM is running."); + return SetScreensaverInhibitDBus(inhibit, "DuckStation", "DuckStation VM is running."); #else std::optional wi(Host::GetTopLevelWindowInfo()); diff --git a/src/util/cd_image_chd.cpp b/src/util/cd_image_chd.cpp index adbfa61bc..076f59bc2 100644 --- a/src/util/cd_image_chd.cpp +++ b/src/util/cd_image_chd.cpp @@ -7,14 +7,19 @@ #include "cd_image.h" #include "cd_subchannel_replacement.h" + #include "common/align.h" #include "common/assert.h" #include "common/error.h" #include "common/file_system.h" #include "common/log.h" +#include "common/path.h" #include "common/platform.h" +#include "common/string_util.h" + #include "fmt/format.h" #include "libchdr/chd.h" + #include #include #include @@ -22,6 +27,7 @@ #include #include #include + Log_SetChannel(CDImageCHD); static std::optional ParseTrackModeString(const char* str) @@ -46,6 +52,7 @@ static std::optional ParseTrackModeString(const char* str) return std::nullopt; } +namespace { class CDImageCHD : public CDImage { public: @@ -66,12 +73,13 @@ private: enum : u32 { CHD_CD_SECTOR_DATA_SIZE = 2352 + 96, - CHD_CD_TRACK_ALIGNMENT = 4 + CHD_CD_TRACK_ALIGNMENT = 4, + MAX_PARENTS = 32 // Surely someone wouldn't be insane enough to go beyond this... }; + chd_file* OpenCHD(const char* filename, FileSystem::ManagedCFilePtr fp, Common::Error* error, u32 recursion_level); bool ReadHunk(u32 hunk_index); - std::FILE* m_fp = nullptr; chd_file* m_chd = nullptr; u32 m_hunk_size = 0; u32 m_sectors_per_hunk = 0; @@ -82,6 +90,7 @@ private: CDSubChannelReplacement m_sbi; }; +} // namespace CDImageCHD::CDImageCHD() = default; @@ -89,15 +98,102 @@ CDImageCHD::~CDImageCHD() { if (m_chd) chd_close(m_chd); - if (m_fp) - std::fclose(m_fp); +} + +chd_file* CDImageCHD::OpenCHD(const char* filename, FileSystem::ManagedCFilePtr fp, Common::Error* error, + u32 recursion_level) +{ + chd_file* chd; + chd_error err = chd_open_file(fp.get(), CHD_OPEN_READ | CHD_OPEN_TRANSFER_FILE, nullptr, &chd); + if (err == CHDERR_NONE) + { + // fp is now managed by libchdr + fp.release(); + return chd; + } + else if (err != CHDERR_REQUIRES_PARENT) + { + Log_ErrorPrintf("Failed to open CHD '%s': %s", filename, chd_error_string(err)); + if (error) + error->SetMessage(chd_error_string(err)); + return nullptr; + } + + if (recursion_level >= MAX_PARENTS) + { + Log_ErrorPrintf("Failed to open CHD '%s': Too many parent files", filename); + if (error) + error->SetMessage("Too many parent files"); + return nullptr; + } + + // Need to get the sha1 to look for. + chd_header header; + err = chd_read_header_file(fp.get(), &header); + if (err != CHDERR_NONE) + { + Log_ErrorPrintf("Failed to read CHD header '%s': %s", filename, chd_error_string(err)); + if (error) + error->SetMessage(chd_error_string(err)); + return nullptr; + } + + // Find a chd with a matching sha1 in the same directory. + // Have to do *.* and filter on the extension manually because Linux is case sensitive. + // We _could_ memoize the CHD headers here, but is anyone actually going to nest CHDs that deep? + chd_file* parent_chd = nullptr; + const std::string parent_dir(Path::GetDirectory(filename)); + FileSystem::FindResultsArray parent_files; + FileSystem::FindFiles(parent_dir.c_str(), "*.*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES, &parent_files); + for (FILESYSTEM_FIND_DATA& fd : parent_files) + { + if (StringUtil::EndsWithNoCase(Path::GetExtension(fd.FileName), ".chd")) + continue; + + auto parent_fp = + FileSystem::OpenManagedSharedCFile(fd.FileName.c_str(), "rb", FileSystem::FileShareMode::DenyWrite); + if (!parent_fp) + continue; + + chd_header parent_header; + err = chd_read_header_file(parent_fp.get(), &parent_header); + if (err != CHDERR_NONE || !chd_is_matching_parent(&header, &parent_header)) + continue; + + // Match! Open this one. + if ((parent_chd = OpenCHD(fd.FileName.c_str(), std::move(parent_fp), error, recursion_level + 1)) != nullptr) + { + Log_DevPrintf(fmt::format("Found parent CHD '{}' for '{}'.", Path::GetFileName(fd.FileName), Path::GetFileName(filename)).c_str()); + break; + } + } + if (!parent_chd) + { + Log_ErrorPrintf("Failed to open CHD '%s': Failed to find parent CHD, it must be in the same directory.", filename); + if (error) + error->SetMessage("Failed to find parent CHD, it must be in the same directory."); + return nullptr; + } + + // Now try re-opening with the parent. + err = chd_open_file(fp.get(), CHD_OPEN_READ | CHD_OPEN_TRANSFER_FILE, parent_chd, &chd); + if (err != CHDERR_NONE) + { + Log_ErrorPrintf("Failed to open CHD '%s': %s", filename, chd_error_string(err)); + if (error) + error->SetMessage(chd_error_string(err)); + return nullptr; + } + + // fp now owned by libchdr + fp.release(); + return chd; } bool CDImageCHD::Open(const char* filename, Common::Error* error) { - Assert(!m_fp); - m_fp = FileSystem::OpenCFile(filename, "rb"); - if (!m_fp) + auto fp = FileSystem::OpenManagedSharedCFile(filename, "rb", FileSystem::FileShareMode::DenyWrite); + if (!fp) { Log_ErrorPrintf("Failed to open CHD '%s': errno %d", filename, errno); if (error) @@ -106,15 +202,9 @@ bool CDImageCHD::Open(const char* filename, Common::Error* error) return false; } - chd_error err = chd_open_file(m_fp, CHD_OPEN_READ, nullptr, &m_chd); - if (err != CHDERR_NONE) - { - Log_ErrorPrintf("Failed to open CHD '%s': %s", filename, chd_error_string(err)); - if (error) - error->SetMessage(chd_error_string(err)); - + m_chd = OpenCHD(filename, std::move(fp), error, 0); + if (!m_chd) return false; - } const chd_header* header = chd_get_header(m_chd); m_hunk_size = header->hunkbytes; @@ -146,8 +236,8 @@ bool CDImageCHD::Open(const char* filename, Common::Error* error) u32 metadata_length; int track_num = 0, frames = 0, pregap_frames = 0, postgap_frames = 0; - err = chd_get_metadata(m_chd, CDROM_TRACK_METADATA2_TAG, num_tracks, metadata_str, sizeof(metadata_str), - &metadata_length, nullptr, nullptr); + chd_error err = chd_get_metadata(m_chd, CDROM_TRACK_METADATA2_TAG, num_tracks, metadata_str, sizeof(metadata_str), + &metadata_length, nullptr, nullptr); if (err == CHDERR_NONE) { if (std::sscanf(metadata_str, CDROM_TRACK_METADATA2_FORMAT, &track_num, type_str, subtype_str, &frames,