diff --git a/Externals/SlippiLib/SlippiGame.cpp b/Externals/SlippiLib/SlippiGame.cpp index 7892486653..166645bc3e 100644 --- a/Externals/SlippiLib/SlippiGame.cpp +++ b/Externals/SlippiLib/SlippiGame.cpp @@ -4,636 +4,613 @@ #include "SlippiGame.h" -namespace Slippi -{ - //********************************************************************** - //* Event Handlers - //********************************************************************** - // The read operators will read a value and increment the index so the next read - // will read in the correct location - uint8_t readByte(uint8_t* a, int& idx, uint32_t maxSize, uint8_t defaultValue) - { - if (idx >= (int)maxSize) - { - idx += 1; - return defaultValue; - } - - return a[idx++]; +namespace Slippi { +//********************************************************************** +//* Event Handlers +//********************************************************************** +// The read operators will read a value and increment the index so the next read +// will read in the correct location +uint8_t readByte(uint8_t *a, int &idx, uint32_t maxSize, uint8_t defaultValue) { + if (idx >= (int)maxSize) { + idx += 1; + return defaultValue; } - uint16_t readHalf(uint8_t* a, int& idx, uint32_t maxSize, uint16_t defaultValue) - { - if (idx >= (int)maxSize) - { - idx += 2; - return defaultValue; - } + return a[idx++]; +} - uint16_t value = a[idx] << 8 | a[idx + 1]; +uint16_t readHalf(uint8_t *a, int &idx, uint32_t maxSize, + uint16_t defaultValue) { + if (idx >= (int)maxSize) { idx += 2; - return value; + return defaultValue; } - uint32_t readWord(uint8_t* a, int& idx, uint32_t maxSize, uint32_t defaultValue) - { - if (idx >= (int)maxSize) - { - idx += 4; - return defaultValue; - } + uint16_t value = a[idx] << 8 | a[idx + 1]; + idx += 2; + return value; +} - uint32_t value = a[idx] << 24 | a[idx + 1] << 16 | a[idx + 2] << 8 | a[idx + 3]; +uint32_t readWord(uint8_t *a, int &idx, uint32_t maxSize, + uint32_t defaultValue) { + if (idx >= (int)maxSize) { idx += 4; - return value; + return defaultValue; } - float readFloat(uint8_t* a, int& idx, uint32_t maxSize, float defaultValue) - { - uint32_t bytes = readWord(a, idx, maxSize, *(uint32_t*)(&defaultValue)); - return *(float*)(&bytes); + uint32_t value = + a[idx] << 24 | a[idx + 1] << 16 | a[idx + 2] << 8 | a[idx + 3]; + idx += 4; + return value; +} + +float readFloat(uint8_t *a, int &idx, uint32_t maxSize, float defaultValue) { + uint32_t bytes = readWord(a, idx, maxSize, *(uint32_t *)(&defaultValue)); + return *(float *)(&bytes); +} + +void handleGameInit(Game *game, uint32_t maxSize) { + int idx = 0; + + // Read version number + for (int i = 0; i < 4; i++) { + game->version[i] = readByte(data, idx, maxSize, 0); } - void handleGameInit(Game* game, uint32_t maxSize) - { - int idx = 0; + // Read entire game info header + for (int i = 0; i < GAME_INFO_HEADER_SIZE; i++) { + game->settings.header[i] = readWord(data, idx, maxSize, 0); + } - // Read version number - for (int i = 0; i < 4; i++) - { - game->version[i] = readByte(data, idx, maxSize, 0); - } + // Load random seed + game->settings.randomSeed = readWord(data, idx, maxSize, 0); - // Read entire game info header - for (int i = 0; i < GAME_INFO_HEADER_SIZE; i++) - { - game->settings.header[i] = readWord(data, idx, maxSize, 0); - } + // Read UCF toggle bytes + bool shouldRead = game->version[0] >= 1; + for (int i = 0; i < UCF_TOGGLE_SIZE; i++) { + uint32_t value = shouldRead ? readWord(data, idx, maxSize, 0) : 0; + game->settings.ucfToggles[i] = value; + } - // Load random seed - game->settings.randomSeed = readWord(data, idx, maxSize, 0); - - // Read UCF toggle bytes - bool shouldRead = game->version[0] >= 1; - for (int i = 0; i < UCF_TOGGLE_SIZE; i++) - { - uint32_t value = shouldRead ? readWord(data, idx, maxSize, 0) : 0; - game->settings.ucfToggles[i] = value; - } - - // Read nametag for each player - std::array, 4> playerNametags; - for (int i = 0; i < 4; i++) - { - for (int j = 0; j < NAMETAG_SIZE; j++) - { - playerNametags[i][j] = readHalf(data, idx, maxSize, 0); - } - } - - // Read isPAL byte - game->settings.isPAL = readByte(data, idx, maxSize, 0); - - // Read isFrozenPS byte - game->settings.isFrozenPS = readByte(data, idx, maxSize, 0); - - // Pull header data into struct - int player1Pos = 24; // This is the index of the first players character info - std::array gameInfoHeader = game->settings.header; - for (int i = 0; i < 4; i++) - { - // this is the position in the array that this player's character info is - // stored - int pos = player1Pos + (9 * i); - - uint32_t playerInfo = gameInfoHeader[pos]; - uint8_t playerType = (playerInfo & 0x00FF0000) >> 16; - if (playerType == 0x3) - { - // Player type 3 is an empty slot - continue; - } - - PlayerSettings p; - - // Get player settings - p.controllerPort = i; - p.characterId = playerInfo >> 24; - p.playerType = playerType; - p.characterColor = playerInfo & 0xFF; - p.nametag = playerNametags[i]; - - // Add player settings to result - game->settings.players[i] = p; - } - - game->settings.stage = gameInfoHeader[3] & 0xFFFF; - - auto majorVersion = game->version[0]; - auto minorVersion = game->version[1]; - if (majorVersion > 3 || (majorVersion == 3 && minorVersion >= 1)) - { - // After version 3.1.0 we added a dynamic gecko loading process. These - // are needed before starting the game. areSettingsLoaded will be set - // to true when they are received - game->areSettingsLoaded = false; - } - else if (majorVersion > 1 || (majorVersion == 1 && minorVersion >= 6)) - { - // Indicate settings loaded immediately if after version 1.6.0 - // Sheik game info was added in this version and so we no longer - // need to wait - game->areSettingsLoaded = true; + // Read nametag for each player + std::array, 4> playerNametags; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < NAMETAG_SIZE; j++) { + playerNametags[i][j] = readHalf(data, idx, maxSize, 0); } } - void handleGeckoList(Game* game, uint32_t maxSize) - { - game->settings.geckoCodes.clear(); - game->settings.geckoCodes.insert(game->settings.geckoCodes.end(), data, data + maxSize); + // Read isPAL byte + game->settings.isPAL = readByte(data, idx, maxSize, 0); - // File is good to load + // Read isFrozenPS byte + game->settings.isFrozenPS = readByte(data, idx, maxSize, 0); + + // Read minorScene byte + game->settings.minorScene = readByte(data, idx, maxSize, 0); + + // Read majorScene byte + game->settings.majorScene = readByte(data, idx, maxSize, 0); + + // Read display name for each player + std::array, 4> playerDisplayNames; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < DISPLAY_NAME_SIZE; j++) { + playerDisplayNames[i][j] = readByte(data, idx, maxSize, 0); + } + } + + // Read connectCodes + std::array, 4> playerConnectCodes; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < CONNECT_CODE_SIZE; j++) { + playerConnectCodes[i][j] = readByte(data, idx, maxSize, 0); + } + } + + // Pull header data into struct + int player1Pos = 24; // This is the index of the first players character info + std::array gameInfoHeader = + game->settings.header; + for (int i = 0; i < 4; i++) { + // this is the position in the array that this player's character info is + // stored + int pos = player1Pos + (9 * i); + + uint32_t playerInfo = gameInfoHeader[pos]; + uint8_t playerType = (playerInfo & 0x00FF0000) >> 16; + if (playerType == 0x3) { + // Player type 3 is an empty slot + continue; + } + + PlayerSettings p; + + // Get player settings + p.controllerPort = i; + p.characterId = playerInfo >> 24; + p.playerType = playerType; + p.characterColor = playerInfo & 0xFF; + p.nametag = playerNametags[i]; + p.displayName = playerDisplayNames[i]; + p.connectCode = playerConnectCodes[i]; + + // Add player settings to result + game->settings.players[i] = p; + } + + game->settings.stage = gameInfoHeader[3] & 0xFFFF; + + auto majorVersion = game->version[0]; + auto minorVersion = game->version[1]; + if (majorVersion > 3 || (majorVersion == 3 && minorVersion >= 1)) { + // After version 3.1.0 we added a dynamic gecko loading process. These + // are needed before starting the game. areSettingsLoaded will be set + // to true when they are received + game->areSettingsLoaded = false; + } else if (majorVersion > 1 || (majorVersion == 1 && minorVersion >= 6)) { + // Indicate settings loaded immediately if after version 1.6.0 + // Sheik game info was added in this version and so we no longer + // need to wait game->areSettingsLoaded = true; } +} - void handleFrameStart(Game* game, uint32_t maxSize) - { - int idx = 0; +void handleGeckoList(Game *game, uint32_t maxSize) { + game->settings.geckoCodes.clear(); + game->settings.geckoCodes.insert(game->settings.geckoCodes.end(), data, + data + maxSize); - // Check frame count - int32_t frameCount = readWord(data, idx, maxSize, 0); - game->frameCount = frameCount; + // File is good to load + game->areSettingsLoaded = true; +} - auto frameUniquePtr = std::make_unique(); - FrameData* frame = frameUniquePtr.get(); +void handleFrameStart(Game *game, uint32_t maxSize) { + int idx = 0; - frame->frame = frameCount; - frame->randomSeedExists = true; - frame->randomSeed = readWord(data, idx, maxSize, 0); + // Check frame count + int32_t frameCount = readWord(data, idx, maxSize, 0); + game->frameCount = frameCount; - // Add frame to game. The frames are stored in multiple ways because - // for games with rollback, the same frame may be replayed multiple times + auto frameUniquePtr = std::make_unique(); + FrameData *frame = frameUniquePtr.get(); + + frame->frame = frameCount; + frame->randomSeedExists = true; + frame->randomSeed = readWord(data, idx, maxSize, 0); + + // Add frame to game. The frames are stored in multiple ways because + // for games with rollback, the same frame may be replayed multiple times + frame->numSinceStart = game->frames.size(); + game->frames.push_back(std::move(frameUniquePtr)); + game->framesByIndex[frameCount] = frame; +} + +void handlePreFrameUpdate(Game *game, uint32_t maxSize) { + int idx = 0; + + // Check frame count + int32_t frameCount = readWord(data, idx, maxSize, 0); + game->frameCount = frameCount; + + auto frameUniquePtr = std::make_unique(); + FrameData *frame = frameUniquePtr.get(); + bool isNewFrame = true; + + if (game->framesByIndex.count(frameCount)) { + // If this frame already exists, get the current frame + frame = game->frames.back().get(); + isNewFrame = false; + } + + frame->frame = frameCount; + + PlayerFrameData p; + + uint8_t playerSlot = readByte(data, idx, maxSize, 0); + uint8_t isFollower = readByte(data, idx, maxSize, 0); + + // Load random seed for player frame update + p.randomSeed = readWord(data, idx, maxSize, 0); + + // Load player data + p.animation = readHalf(data, idx, maxSize, 0); + p.locationX = readFloat(data, idx, maxSize, 0); + p.locationY = readFloat(data, idx, maxSize, 0); + p.facingDirection = readFloat(data, idx, maxSize, 0); + + // Controller information + p.joystickX = readFloat(data, idx, maxSize, 0); + p.joystickY = readFloat(data, idx, maxSize, 0); + p.cstickX = readFloat(data, idx, maxSize, 0); + p.cstickY = readFloat(data, idx, maxSize, 0); + p.trigger = readFloat(data, idx, maxSize, 0); + p.buttons = readWord(data, idx, maxSize, 0); + + // Raw controller information + p.physicalButtons = readHalf(data, idx, maxSize, 0); + p.lTrigger = readFloat(data, idx, maxSize, 0); + p.rTrigger = readFloat(data, idx, maxSize, 0); + + if (asmEvents[EVENT_PRE_FRAME_UPDATE] >= 59) { + p.joystickXRaw = readByte(data, idx, maxSize, 0); + } + + uint32_t noPercent = 0xFFFFFFFF; + p.percent = readFloat(data, idx, maxSize, *(float *)(&noPercent)); + + // Add player data to frame + std::unordered_map *target; + target = isFollower ? &frame->followers : &frame->players; + + // Set the player data for the player or follower + target->operator[](playerSlot) = p; + + // Add frame to game + if (isNewFrame) { frame->numSinceStart = game->frames.size(); game->frames.push_back(std::move(frameUniquePtr)); game->framesByIndex[frameCount] = frame; } +} - void handlePreFrameUpdate(Game* game, uint32_t maxSize) - { - int idx = 0; +void handlePostFrameUpdate(Game *game, uint32_t maxSize) { + int idx = 0; - // Check frame count - int32_t frameCount = readWord(data, idx, maxSize, 0); - game->frameCount = frameCount; + // Check frame count + int32_t frameCount = readWord(data, idx, maxSize, 0); - auto frameUniquePtr = std::make_unique(); - FrameData* frame = frameUniquePtr.get(); - bool isNewFrame = true; - - if (game->framesByIndex.count(frameCount)) - { - // If this frame already exists, get the current frame - frame = game->frames.back().get(); - isNewFrame = false; - } - - frame->frame = frameCount; - - PlayerFrameData p; - - uint8_t playerSlot = readByte(data, idx, maxSize, 0); - uint8_t isFollower = readByte(data, idx, maxSize, 0); - - // Load random seed for player frame update - p.randomSeed = readWord(data, idx, maxSize, 0); - - // Load player data - p.animation = readHalf(data, idx, maxSize, 0); - p.locationX = readFloat(data, idx, maxSize, 0); - p.locationY = readFloat(data, idx, maxSize, 0); - p.facingDirection = readFloat(data, idx, maxSize, 0); - - // Controller information - p.joystickX = readFloat(data, idx, maxSize, 0); - p.joystickY = readFloat(data, idx, maxSize, 0); - p.cstickX = readFloat(data, idx, maxSize, 0); - p.cstickY = readFloat(data, idx, maxSize, 0); - p.trigger = readFloat(data, idx, maxSize, 0); - p.buttons = readWord(data, idx, maxSize, 0); - - // Raw controller information - p.physicalButtons = readHalf(data, idx, maxSize, 0); - p.lTrigger = readFloat(data, idx, maxSize, 0); - p.rTrigger = readFloat(data, idx, maxSize, 0); - - if (asmEvents[EVENT_PRE_FRAME_UPDATE] >= 59) - { - p.joystickXRaw = readByte(data, idx, maxSize, 0); - } - - uint32_t noPercent = 0xFFFFFFFF; - p.percent = readFloat(data, idx, maxSize, *(float*)(&noPercent)); - - // Add player data to frame - std::unordered_map* target; - target = isFollower ? &frame->followers : &frame->players; - - // Set the player data for the player or follower - target->operator[](playerSlot) = p; - - // Add frame to game - if (isNewFrame) - { - frame->numSinceStart = game->frames.size(); - game->frames.push_back(std::move(frameUniquePtr)); - game->framesByIndex[frameCount] = frame; - } + FrameData *frame; + if (game->framesByIndex.count(frameCount)) { + // If this frame already exists, get the current frame + frame = game->frames.back().get(); } - void handlePostFrameUpdate(Game* game, uint32_t maxSize) - { - int idx = 0; + // As soon as a post frame update happens, we know we have received all the + // inputs This is used to determine if a frame is ready to be used for a + // replay (for mirroring) + frame->inputsFullyFetched = true; - // Check frame count - int32_t frameCount = readWord(data, idx, maxSize, 0); + uint8_t playerSlot = readByte(data, idx, maxSize, 0); + uint8_t isFollower = readByte(data, idx, maxSize, 0); - FrameData* frame; - if (game->framesByIndex.count(frameCount)) - { - // If this frame already exists, get the current frame - frame = game->frames.back().get(); - } + PlayerFrameData *p = + isFollower ? &frame->followers[playerSlot] : &frame->players[playerSlot]; - // As soon as a post frame update happens, we know we have received all the - // inputs This is used to determine if a frame is ready to be used for a - // replay (for mirroring) - frame->inputsFullyFetched = true; + p->internalCharacterId = readByte(data, idx, maxSize, 0); - uint8_t playerSlot = readByte(data, idx, maxSize, 0); - uint8_t isFollower = readByte(data, idx, maxSize, 0); - - PlayerFrameData* p = isFollower ? &frame->followers[playerSlot] : &frame->players[playerSlot]; - - p->internalCharacterId = readByte(data, idx, maxSize, 0); - - // Check if a player started as sheik and update - if (frameCount == GAME_FIRST_FRAME && p->internalCharacterId == GAME_SHEIK_INTERNAL_ID) - { - game->settings.players[playerSlot].characterId = GAME_SHEIK_EXTERNAL_ID; - } - - // Set settings loaded if this is the last character - if (frameCount == GAME_FIRST_FRAME) - { - uint8_t lastPlayerIndex = 0; - for (auto it = frame->players.begin(); it != frame->players.end(); ++it) - { - if (it->first <= lastPlayerIndex) - { - continue; - } - - lastPlayerIndex = it->first; - } - } + // Check if a player started as sheik and update + if (frameCount == GAME_FIRST_FRAME && + p->internalCharacterId == GAME_SHEIK_INTERNAL_ID) { + game->settings.players[playerSlot].characterId = GAME_SHEIK_EXTERNAL_ID; } - void handleFrameEnd(Game* game, uint32_t maxSize) { - int idx = 0; - - int32_t frameCount = readWord(data, idx, maxSize, 0); - int32_t lastFinalizedFrame = readWord(data, idx, maxSize, frameCount); - - game->lastFinalizedFrame = lastFinalizedFrame; - } - - void handleGameEnd(Game* game, uint32_t maxSize) - { - int idx = 0; - - game->winCondition = readByte(data, idx, maxSize, 0); - } - - // This function gets the position where the raw data starts - int getRawDataPosition(std::ifstream* f) - { - char buffer[2]; - f->seekg(0, std::ios::beg); - f->read(buffer, 2); - - if (buffer[0] == 0x36) - { - return 0; - } - - if (buffer[0] != '{') - { - // TODO: Do something here to cause an error - return 0; - } - - // TODO: Read ubjson file to find the "raw" element and return the start of it - // TODO: For now since raw is the first element the data will always start at - // 15 - return 15; - } - - uint32_t getRawDataLength(std::ifstream* f, int position, int fileSize) - { - if (position == 0) - { - return fileSize; - } - - char buffer[4]; - f->seekg(position - 4, std::ios::beg); - f->read(buffer, 4); - - uint8_t* byteBuf = (uint8_t*)&buffer[0]; - uint32_t length = byteBuf[0] << 24 | byteBuf[1] << 16 | byteBuf[2] << 8 | byteBuf[3]; - return length; - } - - std::unordered_map getMessageSizes(std::ifstream* f, int position) - { - char buffer[2]; - f->seekg(position, std::ios::beg); - f->read(buffer, 2); - if (buffer[0] != EVENT_PAYLOAD_SIZES) - { - return {}; - } - - int payloadLength = buffer[1]; - std::unordered_map messageSizes = { {EVENT_PAYLOAD_SIZES, payloadLength} }; - - std::vector messageSizesBuffer(payloadLength - 1); - f->read(&messageSizesBuffer[0], payloadLength - 1); - for (int i = 0; i < payloadLength - 1; i += 3) - { - uint8_t command = messageSizesBuffer[i]; - - // Extract the bytes in u8s. Without this the chars don't or together well - uint8_t byte1 = messageSizesBuffer[i + 1]; - uint8_t byte2 = messageSizesBuffer[i + 2]; - - uint16_t size = byte1 << 8 | byte2; - messageSizes[command] = size; - } - - return messageSizes; - } - - void SlippiGame::processData() - { - if (isProcessingComplete) - { - // If we have finished processing this file, return - return; - } - - // This function will process as much data as possible - int startPos = (int)file->tellg(); - file->seekg(startPos); - if (startPos == 0) - { - file->seekg(0, std::ios::end); - int len = (int)file->tellg(); - if (len < 2) - { - // If we can't read message sizes payload size yet, return - return; + // Set settings loaded if this is the last character + if (frameCount == GAME_FIRST_FRAME) { + uint8_t lastPlayerIndex = 0; + for (auto it = frame->players.begin(); it != frame->players.end(); ++it) { + if (it->first <= lastPlayerIndex) { + continue; } - int rawDataPos = getRawDataPosition(file.get()); - int rawDataLen = len - rawDataPos; - if (rawDataLen < 2) - { - // If we don't have enough raw data yet to read the replay file, return - // Reset to begining so that the startPos condition will be hit again - file->seekg(0); - return; - } - - startPos = rawDataPos; - - char buffer[2]; - file->seekg(startPos); - file->read(buffer, 2); - file->seekg(startPos); - auto messageSizesSize = (int)buffer[1]; - if (rawDataLen < messageSizesSize) - { - // If we haven't received the full payload sizes message, return - // Reset to begining so that the startPos condition will be hit again - file->seekg(0); - return; - } - - asmEvents = getMessageSizes(file.get(), rawDataPos); + lastPlayerIndex = it->first; } - // Read everything to the end + if (playerSlot >= lastPlayerIndex) { + game->areSettingsLoaded = true; + } + } +} + +void handleFrameEnd(Game *game, uint32_t maxSize) { + int idx = 0; + + int32_t frameCount = readWord(data, idx, maxSize, 0); + int32_t lastFinalizedFrame = readWord(data, idx, maxSize, frameCount); + + game->lastFinalizedFrame = lastFinalizedFrame; +} + +void handleGameEnd(Game *game, uint32_t maxSize) { + int idx = 0; + + game->winCondition = readByte(data, idx, maxSize, 0); +} + +// This function gets the position where the raw data starts +int getRawDataPosition(std::ifstream *f) { + char buffer[2]; + f->seekg(0, std::ios::beg); + f->read(buffer, 2); + + if (buffer[0] == 0x36) { + return 0; + } + + if (buffer[0] != '{') { + // TODO: Do something here to cause an error + return 0; + } + + // TODO: Read ubjson file to find the "raw" element and return the start of it + // TODO: For now since raw is the first element the data will always start at + // 15 + return 15; +} + +uint32_t getRawDataLength(std::ifstream *f, int position, int fileSize) { + if (position == 0) { + return fileSize; + } + + char buffer[4]; + f->seekg(position - 4, std::ios::beg); + f->read(buffer, 4); + + uint8_t *byteBuf = (uint8_t *)&buffer[0]; + uint32_t length = + byteBuf[0] << 24 | byteBuf[1] << 16 | byteBuf[2] << 8 | byteBuf[3]; + return length; +} + +std::unordered_map getMessageSizes(std::ifstream *f, + int position) { + char buffer[2]; + f->seekg(position, std::ios::beg); + f->read(buffer, 2); + if (buffer[0] != EVENT_PAYLOAD_SIZES) { + return {}; + } + + int payloadLength = buffer[1]; + std::unordered_map messageSizes = { + {EVENT_PAYLOAD_SIZES, payloadLength}}; + + std::vector messageSizesBuffer(payloadLength - 1); + f->read(&messageSizesBuffer[0], payloadLength - 1); + for (int i = 0; i < payloadLength - 1; i += 3) { + uint8_t command = messageSizesBuffer[i]; + + // Extract the bytes in u8s. Without this the chars don't or together well + uint8_t byte1 = messageSizesBuffer[i + 1]; + uint8_t byte2 = messageSizesBuffer[i + 2]; + + uint16_t size = byte1 << 8 | byte2; + messageSizes[command] = size; + } + + return messageSizes; +} + +void SlippiGame::processData() { + if (isProcessingComplete) { + // If we have finished processing this file, return + return; + } + + // This function will process as much data as possible + int startPos = (int)file->tellg(); + file->seekg(startPos); + if (startPos == 0) { file->seekg(0, std::ios::end); - int endPos = (int)file->tellg(); - int sizeToRead = endPos - startPos; - file->seekg(startPos); - // log << "Size to read: " << sizeToRead << "\n"; - // log << "Start Pos: " << startPos << "\n"; - // log << "End Pos: " << endPos << "\n\n"; - if (sizeToRead <= 0) - { + int len = (int)file->tellg(); + if (len < 2) { + // If we can't read message sizes payload size yet, return return; } - std::vector newData(sizeToRead); - file->read(&newData[0], sizeToRead); - - int newDataPos = 0; - while (newDataPos < sizeToRead) - { - auto command = newData[newDataPos]; - auto payloadSize = asmEvents[command]; - - // char buff[100]; - // snprintf(buff, sizeof(buff), "%x", command); - // log << "Command: " << buff << " | Payload Size: " << payloadSize << "\n"; - - auto remainingLen = sizeToRead - newDataPos; - if (remainingLen < ((int)payloadSize + 1)) - { - // Here we don't have enough data to read the whole payload - // Will be processed after getting more data (hopefully) - file->seekg(-remainingLen, std::ios::cur); - return; - } - - data = (uint8_t*)&newData[newDataPos + 1]; - - uint8_t isSplitComplete = false; - uint32_t outerPayloadSize = payloadSize; - - // Handle a split message, combining in until we possess the entire message - if (command == EVENT_SPLIT_MESSAGE) - { - if (shouldResetSplitMessageBuf) - { - splitMessageBuf.clear(); - shouldResetSplitMessageBuf = false; - } - - int _ = 0; - uint16_t blockSize = readHalf(&data[SPLIT_MESSAGE_INTERNAL_DATA_LEN], _, payloadSize, 0); - splitMessageBuf.insert(splitMessageBuf.end(), data, data + blockSize); - - isSplitComplete = data[SPLIT_MESSAGE_INTERNAL_DATA_LEN + 3]; - if (isSplitComplete) - { - // Transform this message into a different message - command = data[SPLIT_MESSAGE_INTERNAL_DATA_LEN + 2]; - data = &splitMessageBuf[0]; - payloadSize = asmEvents[command]; - shouldResetSplitMessageBuf = true; - } - } - - switch (command) - { - case EVENT_GAME_INIT: - handleGameInit(game.get(), payloadSize); - break; - case EVENT_GECKO_LIST: - handleGeckoList(game.get(), payloadSize); - break; - case EVENT_FRAME_START: - handleFrameStart(game.get(), payloadSize); - break; - case EVENT_PRE_FRAME_UPDATE: - handlePreFrameUpdate(game.get(), payloadSize); - break; - case EVENT_POST_FRAME_UPDATE: - handlePostFrameUpdate(game.get(), payloadSize); - break; - case EVENT_GAME_END: - handleGameEnd(game.get(), payloadSize); - isProcessingComplete = true; - break; - case 0x55: - // This is sort of a hack to prevent this functioning - // from processing the metadata as raw data. 0x55 is 'U' - // which is the first character after the raw data in the - // ubjson file format - // log.close(); - isProcessingComplete = true; - file->seekg(-remainingLen, std::ios::cur); - return; - } - - payloadSize = isSplitComplete ? outerPayloadSize : payloadSize; - newDataPos += payloadSize + 1; + int rawDataPos = getRawDataPosition(file.get()); + int rawDataLen = len - rawDataPos; + if (rawDataLen < 2) { + // If we don't have enough raw data yet to read the replay file, return + // Reset to begining so that the startPos condition will be hit again + file->seekg(0); + return; } + + startPos = rawDataPos; + + char buffer[2]; + file->seekg(startPos); + file->read(buffer, 2); + file->seekg(startPos); + auto messageSizesSize = (int)buffer[1]; + if (rawDataLen < messageSizesSize) { + // If we haven't received the full payload sizes message, return + // Reset to begining so that the startPos condition will be hit again + file->seekg(0); + return; + } + + asmEvents = getMessageSizes(file.get(), rawDataPos); } - std::unique_ptr SlippiGame::FromFile(std::string path) - { - auto result = std::make_unique(); - result->game = std::make_unique(); - result->path = path; + // Read everything to the end + file->seekg(0, std::ios::end); + int endPos = (int)file->tellg(); + int sizeToRead = endPos - startPos; + file->seekg(startPos); + // log << "Size to read: " << sizeToRead << "\n"; + // log << "Start Pos: " << startPos << "\n"; + // log << "End Pos: " << endPos << "\n\n"; + if (sizeToRead <= 0) { + return; + } + + std::vector newData(sizeToRead); + file->read(&newData[0], sizeToRead); + + int newDataPos = 0; + while (newDataPos < sizeToRead) { + auto command = newData[newDataPos]; + auto payloadSize = asmEvents[command]; + + // char buff[100]; + // snprintf(buff, sizeof(buff), "%x", command); + // log << "Command: " << buff << " | Payload Size: " << payloadSize << "\n"; + + auto remainingLen = sizeToRead - newDataPos; + if (remainingLen < ((int)payloadSize + 1)) { + // Here we don't have enough data to read the whole payload + // Will be processed after getting more data (hopefully) + file->seekg(-remainingLen, std::ios::cur); + return; + } + + data = (uint8_t *)&newData[newDataPos + 1]; + + uint8_t isSplitComplete = false; + uint32_t outerPayloadSize = payloadSize; + + // Handle a split message, combining in until we possess the entire message + if (command == EVENT_SPLIT_MESSAGE) { + if (shouldResetSplitMessageBuf) { + splitMessageBuf.clear(); + shouldResetSplitMessageBuf = false; + } + + int _ = 0; + uint16_t blockSize = + readHalf(&data[SPLIT_MESSAGE_INTERNAL_DATA_LEN], _, payloadSize, 0); + splitMessageBuf.insert(splitMessageBuf.end(), data, data + blockSize); + + isSplitComplete = data[SPLIT_MESSAGE_INTERNAL_DATA_LEN + 3]; + if (isSplitComplete) { + // Transform this message into a different message + command = data[SPLIT_MESSAGE_INTERNAL_DATA_LEN + 2]; + data = &splitMessageBuf[0]; + payloadSize = asmEvents[command]; + shouldResetSplitMessageBuf = true; + } + } + + switch (command) { + case EVENT_GAME_INIT: + handleGameInit(game.get(), payloadSize); + break; + case EVENT_GECKO_LIST: + handleGeckoList(game.get(), payloadSize); + break; + case EVENT_FRAME_START: + handleFrameStart(game.get(), payloadSize); + break; + case EVENT_PRE_FRAME_UPDATE: + handlePreFrameUpdate(game.get(), payloadSize); + break; + case EVENT_POST_FRAME_UPDATE: + handlePostFrameUpdate(game.get(), payloadSize); + break; + case EVENT_FRAME_END: + handleFrameEnd(game.get(), payloadSize); + break; + case EVENT_GAME_END: + handleGameEnd(game.get(), payloadSize); + isProcessingComplete = true; + break; + case 0x55: + // This is sort of a hack to prevent this functioning + // from processing the metadata as raw data. 0x55 is 'U' + // which is the first character after the raw data in the + // ubjson file format + // log.close(); + isProcessingComplete = true; + file->seekg(-remainingLen, std::ios::cur); + return; + } + + payloadSize = isSplitComplete ? outerPayloadSize : payloadSize; + newDataPos += payloadSize + 1; + } +} + +std::unique_ptr SlippiGame::FromFile(std::string path) { + auto result = std::make_unique(); + result->game = std::make_unique(); + result->path = path; #ifdef _WIN32 - // On Windows, we need to convert paths to std::wstring to deal with UTF-8 - std::wstring convertedPath = std::wstring_convert>().from_bytes(path); - result->file = std::make_unique(convertedPath, std::ios::in | std::ios::binary); + // On Windows, we need to convert paths to std::wstring to deal with UTF-8 + std::wstring convertedPath = + std::wstring_convert>().from_bytes(path); + result->file = std::make_unique( + convertedPath, std::ios::in | std::ios::binary); #else - result->file = std::make_unique(path, std::ios::in | std::ios::binary); + result->file = + std::make_unique(path, std::ios::in | std::ios::binary); #endif - // result->log.open("log.txt"); - if (!result->file->is_open()) - { - return nullptr; - } - - // int fileLength = (int)file.tellg(); - // int rawDataPos = getRawDataPosition(&file); - // uint32_t rawDataLength = getRawDataLength(&file, rawDataPos, fileLength); - // asmEvents = getMessageSizes(&file, rawDataPos); - - // std::vector rawData(rawDataLength); - // file.seekg(rawDataPos, std::ios::beg); - // file.read(&rawData[0], rawDataLength); - - // SlippiGame* result = processFile((uint8_t*)&rawData[0], rawDataLength); - - return std::move(result); + // result->log.open("log.txt"); + if (!result->file->is_open()) { + return nullptr; } - bool SlippiGame::IsProcessingComplete() - { - return isProcessingComplete; - } + // int fileLength = (int)file.tellg(); + // int rawDataPos = getRawDataPosition(&file); + // uint32_t rawDataLength = getRawDataLength(&file, rawDataPos, fileLength); + // asmEvents = getMessageSizes(&file, rawDataPos); - bool SlippiGame::AreSettingsLoaded() - { - processData(); - return game->areSettingsLoaded; - } + // std::vector rawData(rawDataLength); + // file.seekg(rawDataPos, std::ios::beg); + // file.read(&rawData[0], rawDataLength); - bool SlippiGame::DoesFrameExist(int32_t frame) - { - processData(); - return (bool)game->framesByIndex.count(frame); - } + // SlippiGame* result = processFile((uint8_t*)&rawData[0], rawDataLength); - std::array SlippiGame::GetVersion() - { - return game->version; - } + return std::move(result); +} - std::string SlippiGame::GetVersionString() - { - char version[30]; - sprintf(version, "%d.%d.%d", game->version[0], game->version[1], game->version[2]); +bool SlippiGame::IsProcessingComplete() { return isProcessingComplete; } - return std::string(version); - } +bool SlippiGame::AreSettingsLoaded() { + processData(); + return game->areSettingsLoaded; +} - FrameData* SlippiGame::GetFrame(int32_t frame) { - // Get the frame we want - return game->framesByIndex.at(frame); - } +bool SlippiGame::DoesFrameExist(int32_t frame) { + processData(); + return static_cast(game->framesByIndex.count(frame)); +} - FrameData* SlippiGame::GetFrameAt(uint32_t pos) - { - if (pos >= game->frames.size()) - { - return nullptr; - } - // Get the frame we want - return game->frames[pos].get(); - } +std::array SlippiGame::GetVersion() { return game->version; } - int32_t SlippiGame::GetLastFinalizedFrame() { - processData(); - return game->lastFinalizedFrame; - } +std::string SlippiGame::GetVersionString() { + char version[30]; + sprintf(version, "%d.%d.%d", game->version[0], game->version[1], + game->version[2]); - int32_t SlippiGame::GetLatestIndex() { - processData(); - return game->frameCount; - } + return std::string(version); +} - GameSettings* SlippiGame::GetSettings() - { - processData(); - return &game->settings; - } +FrameData *SlippiGame::GetFrame(int32_t frame) { + // Get the frame we want + return game->framesByIndex.at(frame); +} - bool SlippiGame::DoesPlayerExist(int8_t port) { - return game->settings.players.find(port) != game->settings.players.end(); +FrameData *SlippiGame::GetFrameAt(uint32_t pos) { + if (pos >= game->frames.size()) { + return nullptr; } + // Get the frame we want + return game->frames[pos].get(); +} - uint8_t SlippiGame::GetGameEndMethod() { - return game->winCondition; - } -} // namespace Slippi +int32_t SlippiGame::GetLastFinalizedFrame() { + processData(); + return game->lastFinalizedFrame; +} + +int32_t SlippiGame::GetLatestIndex() { + processData(); + return game->frameCount; +} + +GameSettings *SlippiGame::GetSettings() { + processData(); + return &game->settings; +} + +bool SlippiGame::DoesPlayerExist(int8_t port) { + return game->settings.players.find(port) != game->settings.players.end(); +} + +uint8_t SlippiGame::GetGameEndMethod() { return game->winCondition; } +} // namespace Slippi diff --git a/Externals/SlippiLib/SlippiGame.h b/Externals/SlippiLib/SlippiGame.h index a38e6ab95e..9d628e84e5 100644 --- a/Externals/SlippiLib/SlippiGame.h +++ b/Externals/SlippiLib/SlippiGame.h @@ -9,146 +9,148 @@ #include namespace Slippi { - const uint8_t EVENT_SPLIT_MESSAGE = 0x10; - const uint8_t EVENT_PAYLOAD_SIZES = 0x35; - const uint8_t EVENT_GAME_INIT = 0x36; - const uint8_t EVENT_PRE_FRAME_UPDATE = 0x37; - const uint8_t EVENT_POST_FRAME_UPDATE = 0x38; - const uint8_t EVENT_GAME_END = 0x39; - const uint8_t EVENT_FRAME_START = 0x3A; - const uint8_t EVENT_FRAME_END = 0x3C; - const uint8_t EVENT_GECKO_LIST = 0x3D; +const uint8_t EVENT_SPLIT_MESSAGE = 0x10; +const uint8_t EVENT_PAYLOAD_SIZES = 0x35; +const uint8_t EVENT_GAME_INIT = 0x36; +const uint8_t EVENT_PRE_FRAME_UPDATE = 0x37; +const uint8_t EVENT_POST_FRAME_UPDATE = 0x38; +const uint8_t EVENT_GAME_END = 0x39; +const uint8_t EVENT_FRAME_START = 0x3A; +const uint8_t EVENT_FRAME_END = 0x3C; +const uint8_t EVENT_GECKO_LIST = 0x3D; - const uint8_t GAME_INFO_HEADER_SIZE = 78; - const uint8_t UCF_TOGGLE_SIZE = 8; - const uint8_t NAMETAG_SIZE = 8; - const uint8_t DISPLAY_NAME_SIZE = 31; - const uint8_t CONNECT_CODE_SIZE = 10; - const int32_t GAME_FIRST_FRAME = -123; - const int32_t PLAYBACK_FIRST_SAVE = -122; - const uint8_t GAME_SHEIK_INTERNAL_ID = 0x7; - const uint8_t GAME_SHEIK_EXTERNAL_ID = 0x13; +const uint8_t GAME_INFO_HEADER_SIZE = 78; +const uint8_t UCF_TOGGLE_SIZE = 8; +const uint8_t NAMETAG_SIZE = 8; +const uint8_t DISPLAY_NAME_SIZE = 31; +const uint8_t CONNECT_CODE_SIZE = 10; +const int32_t GAME_FIRST_FRAME = -123; +const int32_t PLAYBACK_FIRST_SAVE = -122; +const uint8_t GAME_SHEIK_INTERNAL_ID = 0x7; +const uint8_t GAME_SHEIK_EXTERNAL_ID = 0x13; - const uint32_t SPLIT_MESSAGE_INTERNAL_DATA_LEN = 512; +const uint32_t SPLIT_MESSAGE_INTERNAL_DATA_LEN = 512; - static uint8_t* data; +static uint8_t *data; - typedef struct { - // Every player update has its own rng seed because it might change in between players - uint32_t randomSeed; +typedef struct { + // Every player update has its own rng seed because it might change in between + // players + uint32_t randomSeed; - uint8_t internalCharacterId; - uint16_t animation; - float locationX; - float locationY; - float facingDirection; - uint8_t stocks; - float percent; - float shieldSize; - uint8_t lastMoveHitId; - uint8_t comboCount; - uint8_t lastHitBy; + uint8_t internalCharacterId; + uint16_t animation; + float locationX; + float locationY; + float facingDirection; + uint8_t stocks; + float percent; + float shieldSize; + uint8_t lastMoveHitId; + uint8_t comboCount; + uint8_t lastHitBy; - //Controller information - float joystickX; - float joystickY; - float cstickX; - float cstickY; - float trigger; - uint32_t buttons; //This will include multiple "buttons" pressed on special buttons. For example I think pressing z sets 3 bits + // Controller information + float joystickX; + float joystickY; + float cstickX; + float cstickY; + float trigger; + uint32_t buttons; // This will include multiple "buttons" pressed on special + // buttons. For example I think pressing z sets 3 bits - //This is extra controller information - uint16_t physicalButtons; //A better representation of what a player is actually pressing - float lTrigger; - float rTrigger; + // This is extra controller information + uint16_t physicalButtons; // A better representation of what a player is + // actually pressing + float lTrigger; + float rTrigger; - uint8_t joystickXRaw; - } PlayerFrameData; + uint8_t joystickXRaw; +} PlayerFrameData; - typedef struct FrameData { - int32_t frame; - uint32_t numSinceStart; - bool randomSeedExists = false; - uint32_t randomSeed; - bool inputsFullyFetched = false; - std::unordered_map players; - std::unordered_map followers; - } FrameData; +typedef struct FrameData { + int32_t frame; + uint32_t numSinceStart; + bool randomSeedExists = false; + uint32_t randomSeed; + bool inputsFullyFetched = false; + std::unordered_map players; + std::unordered_map followers; +} FrameData; - typedef struct { - //Static data - uint8_t characterId; - uint8_t characterColor; - uint8_t playerType; - uint8_t controllerPort; - std::array nametag; - std::array displayName; - std::array connectCode; - } PlayerSettings; +typedef struct { + // Static data + uint8_t characterId; + uint8_t characterColor; + uint8_t playerType; + uint8_t controllerPort; + std::array nametag; + std::array displayName; + std::array connectCode; +} PlayerSettings; - typedef struct { - uint16_t stage; //Stage ID - uint32_t randomSeed; - std::array header; - std::array ucfToggles; - std::unordered_map players; - uint8_t isPAL; - uint8_t isFrozenPS; - uint8_t minorScene; - uint8_t majorScene; - std::vector geckoCodes; - } GameSettings; +typedef struct { + uint16_t stage; // Stage ID + uint32_t randomSeed; + std::array header; + std::array ucfToggles; + std::unordered_map players; + uint8_t isPAL; + uint8_t isFrozenPS; + uint8_t minorScene; + uint8_t majorScene; + std::vector geckoCodes; +} GameSettings; - typedef struct Game { - std::array version; - std::unordered_map framesByIndex; - std::vector> frames; - GameSettings settings; - bool areSettingsLoaded = false; +typedef struct Game { + std::array version; + std::unordered_map framesByIndex; + std::vector> frames; + GameSettings settings; + bool areSettingsLoaded = false; - int32_t frameCount; // Current/last frame count - int32_t lastFinalizedFrame = -124; + int32_t frameCount; // Current/last frame count + int32_t lastFinalizedFrame = -124; - //From OnGameEnd event - uint8_t winCondition; - } Game; + // From OnGameEnd event + uint8_t winCondition; +} Game; - // TODO: This shouldn't be static. Doesn't matter too much atm because we always - // TODO: only read one file at a time - static std::unordered_map asmEvents = { - { EVENT_GAME_INIT, 320 }, - { EVENT_PRE_FRAME_UPDATE, 58 }, - { EVENT_POST_FRAME_UPDATE, 33 }, - { EVENT_GAME_END, 1 }, - { EVENT_FRAME_START, 8 } - }; +// TODO: This shouldn't be static. Doesn't matter too much atm because we always +// TODO: only read one file at a time +static std::unordered_map asmEvents = { + {EVENT_GAME_INIT, 320}, + {EVENT_PRE_FRAME_UPDATE, 58}, + {EVENT_POST_FRAME_UPDATE, 33}, + {EVENT_GAME_END, 1}, + {EVENT_FRAME_START, 8}}; - class SlippiGame - { - public: - static std::unique_ptr FromFile(std::string path); - bool AreSettingsLoaded(); - bool DoesFrameExist(int32_t frame); - std::array GetVersion(); - std::string GetVersionString(); - FrameData* GetFrame(int32_t frame); - FrameData* GetFrameAt(uint32_t pos); - int32_t GetLastFinalizedFrame(); - int32_t GetLatestIndex(); - GameSettings* GetSettings(); - uint8_t GetGameEndMethod(); - bool DoesPlayerExist(int8_t port); - bool IsProcessingComplete(); - private: - std::unique_ptr game; - std::unique_ptr file; - std::vector rawData; - std::string path; - std::ofstream log; - std::vector splitMessageBuf; - bool shouldResetSplitMessageBuf = false; +class SlippiGame { +public: + static std::unique_ptr FromFile(std::string path); + bool AreSettingsLoaded(); + bool DoesFrameExist(int32_t frame); + std::array GetVersion(); + std::string GetVersionString(); + FrameData *GetFrame(int32_t frame); + FrameData *GetFrameAt(uint32_t pos); + int32_t GetLastFinalizedFrame(); + int32_t GetLatestIndex(); + GameSettings *GetSettings(); + uint8_t GetGameEndMethod(); + bool DoesPlayerExist(int8_t port); + bool IsProcessingComplete(); - bool isProcessingComplete = false; - void processData(); - }; -} +private: + std::unique_ptr game; + std::unique_ptr file; + std::vector rawData; + std::string path; + std::ofstream log; + std::vector splitMessageBuf; + bool shouldResetSplitMessageBuf = false; + + bool isProcessingComplete = false; + void processData(); +}; +} // namespace Slippi