#include "Client/Replay.h" #include "Contents/Configuration/ServiceConfiguration.h" #include #include #include #include #include #include // GLOBAL: gl_24549 int g_replayAnimationScaling = 0; // GLOBAL: gl_24630 static int g_replayFlushCounter = 0; // STRING: st_16831 static const char g_replayHeaderText[] = "<< Ragnarok Replay File Version 0.%02d >>\r\n \xA1\xD8 Do not modify file! "; // STRING: st_16832 static const char g_replayHeaderPrefix[] = "\r\n"; #pragma pack(push, 1) struct ReplayTaggedRecordHeader { /* 0000 */ std::uint16_t id; /* 0002 */ std::uint32_t bytes; }; #pragma pack(pop) static_assert(sizeof(ReplayTaggedRecordHeader) == 6); #pragma pack(push, 1) struct ReplayRecordHeader { /* 0000 */ std::uint32_t frame; /* 0004 */ std::uint32_t tick; /* 0008 */ std::uint16_t bytes; }; #pragma pack(pop) static_assert(sizeof(ReplayRecordHeader) == 10); // OFFSET: 0x006DA340 // FUNCTION: fn_06623 std::vector BuildReplayTaggedRecord(std::uint16_t id, const void* data, std::uint32_t bytes) { std::vector record(bytes + 6u, 0); const ReplayTaggedRecordHeader header{id, bytes}; std::memcpy(record.data(), &header, sizeof(header)); if (bytes != 0) { std::memcpy(record.data() + sizeof(header), data, bytes); } return record; } // OFFSET: 0x00B83920 // FUNCTION: fn_19301 ReplayCachedPacket::ReplayCachedPacket(const ReplayCachedPacket& other) : enabled(other.enabled), bytes(other.bytes) { } // OFFSET: 0x00B839D0 // FUNCTION: fn_19302 ReplaySection::ReplaySection() : id(0), bytes(0), fileOffset(0) { } // OFFSET: 0x00B83D00 // FUNCTION: fn_19305 void ReplaySection::Clear() { this->id = 0; this->bytes = 0; this->fileOffset = 0; } // OFFSET: 0x00B83D20 // FUNCTION: fn_19306 ReplaySection::~ReplaySection() { this->id = 0; this->bytes = 0; this->fileOffset = 0; } // OFFSET: 0x00B83540 // FUNCTION: fn_19300 Replay::Replay() : cipherIndex(0), cipherBase(0), cipherKey(0), unknown_01F4(0), unknown_01F8(0) { this->state = 1; this->baseFrame = 0; this->started = 0; this->frame = 0; this->elapsed = 0; this->speed = 1.0f; std::memset(&this->header, 0, sizeof(this->header)); std::memset(&this->header.options, 1, sizeof(this->header.options)); this->ResetFile(); this->previousTick = timeGetTime(); this->unknown_01F8 = 0; this->unknown_01F4 = 0; this->cachedPackets.resize(3143, ReplayCachedPacket{}); this->cachedPackets[2459].enabled = true; this->cachedPackets[470].enabled = true; } // OFFSET: 0x00B855B0 // FUNCTION: fn_19326 bool Replay::WriteSection(int id, std::vector& bytes, int event) { if (bytes.empty()) { return false; } if (id <= 0 || id >= 26 || bytes.size() == 0 || bytes.data() == nullptr) { return false; } ReplaySection& section = this->sections[id]; if (event == 14) { section.id = static_cast(id); section.bytes = bytes.size(); section.fileOffset = this->fileOffset; this->fileOffset += bytes.size(); } if (section.bytes != 0) { this->cipherBase = this->header.key0 >> 5; this->cipherIndex = 0; this->cipherKey = this->header.key1 >> 3; for (std::uint32_t index = 0; index < section.bytes / 4; ++index) { std::uint32_t word; std::memcpy(&word, bytes.data() + index * 4, sizeof(word)); word ^= this->cipherKey * (++this->cipherIndex + this->cipherBase); std::memcpy(bytes.data() + index * 4, &word, sizeof(word)); } } std::fseek(this->file, static_cast(section.fileOffset), SEEK_SET); return std::fwrite(bytes.data(), section.bytes, 1, this->file) != 0; } // OFFSET: 0x00B857E0 // FUNCTION: fn_19327 int Replay::WriteHeader(int event) { this->header.records = this->records; this->header.fileOffset = this->fileOffset; this->header.closingSignature = 0x52414705; this->header.signature = 0x52414705; switch (event) { case 11: { this->sections[25].id = 111; bool written; { std::vector metadata = this->BuildMetadata(); written = this->WriteSection(2, metadata, 14); } if (!written) { return 0; } char text[100] = {}; std::sprintf(text, g_replayHeaderText, 5); std::sprintf(text + 95, g_replayHeaderPrefix); std::fseek(this->file, 0, SEEK_SET); std::fwrite(text, sizeof(text), 1, this->file); std::fwrite(&this->header, 12, 1, this->file); return 1; } case 12: { if (++g_replayFlushCounter != 1200) { return 1; } this->header.closingSignature = 0x51404605; this->header.signature = 0x51404605; bool written; { std::vector metadata = this->BuildMetadata(); written = this->WriteSection(2, metadata, 15); } if (!written) { return 0; } std::fseek(this->file, 112, SEEK_SET); std::fwrite(this->sections, sizeof(this->sections), 1, this->file); g_replayFlushCounter = 0; return 1; } case 13: { bool written; { std::vector metadata = this->BuildMetadata(); written = this->WriteSection(2, metadata, 15); } if (!written) { return 0; } std::fseek(this->file, 112, SEEK_SET); std::fwrite(this->sections, sizeof(this->sections), 1, this->file); return 1; } default: return 0; } } // OFFSET: 0x00B85A20 // FUNCTION: fn_19328 std::vector Replay::BuildMetadata() { std::vector metadata; { const auto record = BuildReplayTaggedRecord(950, nullptr, 0); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(961, &this->header.service, sizeof(this->header.service)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(962, &this->header.serviceType, sizeof(this->header.serviceType)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(963, &this->header.unknown_0014, sizeof(this->header.unknown_0014)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(964, this->header.characterName, sizeof(this->header.characterName)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(965, this->header.mapName, sizeof(this->header.mapName)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(966, &this->header.accountId, sizeof(this->header.accountId)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(967, &this->header.cellX, sizeof(this->header.cellX)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(968, &this->header.cellY, sizeof(this->header.cellY)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(969, &this->header.unknown_0078, sizeof(this->header.unknown_0078)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(970, &this->header.duration, sizeof(this->header.duration)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(971, &this->header.records, sizeof(this->header.records)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(972, &this->header.fileOffset, sizeof(this->header.fileOffset)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(973, &this->header.options, sizeof(this->header.options)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(960, &this->header.closingSignature, sizeof(this->header.closingSignature)); metadata.insert(metadata.end(), record.begin(), record.end()); } { const auto record = BuildReplayTaggedRecord(951, nullptr, 0); metadata.insert(metadata.end(), record.begin(), record.end()); } return metadata; } // OFFSET: 0x00B875E0 // FUNCTION: fn_19354 Replay& Replay::Instance() { // GLOBAL: gl_24631 static Replay g_replay; return g_replay; } // OFFSET: 0x00B83D50 // FUNCTION: fn_19307 bool Replay::MatchesService(int mode) const { if (mode == 2) { const int type = this->header.serviceType; return type == 0 || type == 13 || type == 1 || type == 2; } return g_serviceMode == this->header.serviceType; } // OFFSET: 0x00B83D90 // FUNCTION: fn_19308 int Replay::AdvanceFrame() { if (!g_replayAnimationScaling) { ++this->frame; return 1; } const std::uint32_t now = timeGetTime(); if (this->state != 6) { ++this->frame; this->elapsed += static_cast(static_cast(now - this->previousTick) * this->speed); if (static_cast(this->elapsed) >= static_cast(this->header.duration)) { this->previousTick = now; this->elapsed = this->header.duration; this->state = 6; return 0; } } this->previousTick = now; return 1; } // OFFSET: 0x00B83E10 // FUNCTION: fn_19309 void Replay::ResetPlayback() { if (this->state != 3 && this->state != 8) { this->records = 0; this->frame = 0; this->elapsed = 0; } } // OFFSET: 0x00B83E40 // FUNCTION: fn_19310 bool Replay::Cipher(std::uint32_t bytes, void* data) { if (!data || !bytes) { return false; } auto* words = static_cast(data); this->cipherBase = this->header.key0 >> 5; this->cipherIndex = 0; this->cipherKey = this->header.key1 >> 3; const int count = bytes / 4; for (int index = 0; index < count; ++index) { ++this->cipherIndex; words[index] ^= (this->cipherBase + this->cipherIndex) * this->cipherKey; } return true; } // OFFSET: 0x00B83F20 // FUNCTION: fn_19312 void Replay::CloseFile() { if (this->file) { if (this->fileMode == 31) { this->WriteHeader(13); } std::fclose(this->file); this->file = nullptr; } } // OFFSET: 0x00B84340 // FUNCTION: fn_19314 ReplayTime Replay::Duration() const { const unsigned seconds = static_cast(this->header.duration) / 1000; const unsigned minutes = seconds / 60; const unsigned hours = minutes / 60; ReplayTime time; time.hours = static_cast(hours); time.minutes = static_cast(minutes % 60); time.seconds = static_cast(seconds % 60); return time; } // OFFSET: 0x00B843A0 // FUNCTION: fn_19315 int Replay::FindSection(int id) const { if (id < 1 || id > 25) { return -1; } if (this->sections[id].id == id) { return id; } for (int index = 0; index < 26; ++index) { if (this->sections[index].id == id) { return index; } } return -1; } // OFFSET: 0x00B84620 // FUNCTION: fn_19318 int Replay::SpeedLevel() const { if (this->speed == 0.25f) { return 1; } if (this->speed == 0.5f) { return 2; } if (this->speed == 1.0f) { return 3; } if (this->speed == 2.0f) { return 4; } if (this->speed == 4.0f) { return 5; } if (this->speed == 8.0f) { return 6; } if (this->speed == 16.0f) { return 7; } return 0; } // OFFSET: 0x00B846D0 // FUNCTION: fn_19319 void Replay::ResetFile() { this->header.Reset(); this->records = 0; this->fileMode = -1; this->file = nullptr; this->fileOffset = 0; } // OFFSET: 0x00B84930 // FUNCTION: fn_19320 std::vector Replay::ReadSection(int id) { std::vector data; if (id >= 1 && id <= 25) { const int index = this->FindSection(id); if (index != -1 && this->sections[index].fileOffset != 0 && this->sections[index].bytes != 0) { const ReplaySection& section = this->sections[index]; data.resize(section.bytes); std::fseek(this->file, static_cast(section.fileOffset), SEEK_SET); std::fread(data.data(), section.bytes, 1, this->file); this->Cipher(section.bytes, data.data()); } } return data; } // OFFSET: 0x00B84E40 // FUNCTION: fn_19322 std::uint32_t Replay::ParseMetadata(const std::vector& data) { if (data.empty()) { return 0xFFFFFFFF; } const unsigned char* buffer = data.data(); const std::uint32_t size = static_cast(data.size()); std::uint32_t offset = 6; std::uint16_t signature = 0; std::memcpy(&signature, buffer, sizeof(signature)); if (signature != 0x3B6) { return 0xFFFFFFFF; } if (size > 6) { do { std::uint16_t id = 0; std::uint32_t bytes = 0; std::memcpy(&id, buffer + offset, sizeof(id)); std::memcpy(&bytes, buffer + offset + 2, sizeof(bytes)); offset += 6; switch (id) { case 0x3B7: return offset; case 0x3C0: std::memcpy(&this->header.closingSignature, buffer + offset, bytes); offset += bytes; if (this->header.signature != this->header.closingSignature) { std::uint32_t fileOffset = this->fileOffset; ReplayRecordHeader record{}; int count = static_cast(this->header.records) - 1; if (count > 0) { do { std::fseek(this->file, static_cast(fileOffset), SEEK_SET); std::fread(&record, sizeof(record), 1, this->file); fileOffset += record.bytes + sizeof(record); --count; } while (count != 0); } this->header.duration = record.tick; } break; case 0x3C1: std::memcpy(&this->header.service, buffer + offset, bytes); offset += bytes; break; case 0x3C2: std::memcpy(&this->header.serviceType, buffer + offset, bytes); offset += bytes; break; case 0x3C3: std::memcpy(&this->header.unknown_0014, buffer + offset, bytes); offset += bytes; break; case 0x3C4: std::memcpy(this->header.characterName, buffer + offset, bytes); offset += bytes; break; case 0x3C5: std::memcpy(this->header.mapName, buffer + offset, bytes); offset += bytes; break; case 0x3C6: std::memcpy(&this->header.accountId, buffer + offset, bytes); offset += bytes; break; case 0x3C7: std::memcpy(&this->header.cellX, buffer + offset, bytes); offset += bytes; break; case 0x3C8: std::memcpy(&this->header.cellY, buffer + offset, bytes); offset += bytes; break; case 0x3C9: std::memcpy(&this->header.unknown_0078, buffer + offset, bytes); offset += bytes; break; case 0x3CA: std::memcpy(&this->header.duration, buffer + offset, bytes); offset += bytes; break; case 0x3CB: std::memcpy(&this->header.records, buffer + offset, bytes); offset += bytes; break; case 0x3CC: std::memcpy(&this->header.fileOffset, buffer + offset, bytes); offset += bytes; break; case 0x3CD: std::memcpy(&this->header.options, buffer + offset, bytes); offset += bytes; break; default: offset += bytes; break; } } while (offset < size); } return offset; } // OFFSET: 0x00B85350 // FUNCTION: fn_19323 bool Replay::ReadRecord(unsigned char* buffer, std::uint16_t& id, bool force) { if (!force && this->state != 7) { return false; } ReplayRecordHeader record{}; bool result = false; std::fseek(this->file, static_cast(this->fileOffset), SEEK_SET); std::fread(&record, sizeof(record), 1, this->file); if (this->header.records > this->records && this->elapsed >= record.tick) { std::fread(buffer, record.bytes, 1, this->file); this->Cipher(record.bytes, buffer); std::memcpy(&id, buffer, sizeof(id)); this->fileOffset += record.bytes + sizeof(record); result = true; ++this->records; } return result; } // OFFSET: 0x00B854C0 // FUNCTION: fn_19324 bool Replay::AllowsEvent(int event) const { if (!g_replayAnimationScaling) { return false; } if (event == 271 || event == 510 || event == 201 || event == 286 || event == 580) { return false; } if (event == 195) { return this->header.options.Get(ReplayRecordOption::Equip) == 0; } if (event == 194) { return this->header.options.Get(ReplayRecordOption::Item) == 0; } if (event == 196) { return this->header.options.Get(ReplayRecordOption::Skill) == 0; } if (event == 199) { return this->header.options.Get(ReplayRecordOption::Party) == 0; } if (event == 192) { return this->header.options.Get(ReplayRecordOption::Status) == 0; } return true; } // OFFSET: 0x00B85580 // FUNCTION: fn_19325 void Replay::Rewind() { this->records = 0; this->fileOffset = this->sections[this->FindSection(1)].fileOffset; } // OFFSET: 0x00B83EC0 // FUNCTION: fn_19311 void ReplayFileList::Clear() { this->selected = -1; this->files.erase(this->files.begin(), this->files.end()); } // OFFSET: 0x00B84B60 // FUNCTION: fn_19321 void ReplayFileList::Refresh() { this->Clear(); char directory[260] = {}; GetCurrentDirectoryA(sizeof(directory), directory); std::string pattern(directory, std::strlen(directory)); pattern.append("\\Replay\\*.rrf", 13); WIN32_FIND_DATAA found; HANDLE search = FindFirstFileA(pattern.c_str(), &found); if (search != INVALID_HANDLE_VALUE) { BOOL more = TRUE; while (more) { if ((found.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { const std::size_t length = std::strlen(found.cFileName); const char* extension = found.cFileName + length - 4; if (extension[0] == '.' && extension[1] == 'r' && extension[2] == 'r' && extension[3] == 'f') { ReplayFileEntry entry = {}; std::memcpy(entry.name, found.cFileName, length); entry.name[length] = '\0'; entry.unknown_0000 = 0; if (length == 23) { char number[6] = {}; std::memcpy(number, entry.name + 15, 4); entry.unknown_0000 = std::atoi(number); } this->files.push_back(entry); } } more = FindNextFileA(search, &found); } } FindClose(search); } // OFFSET: 0x00B84300 // FUNCTION: fn_19313 const char* ReplayFileList::FileName(int index) const { if (static_cast(index) >= this->files.size()) { return nullptr; } return this->files[index].name; } // OFFSET: 0x00B86AF0 // FUNCTION: fn_19336 char* ReplayFileList::SetPath(const char* value) { if (!value || !this->pathEnabled) { return nullptr; } std::memset(this->path, 0, sizeof(this->path)); std::memcpy(this->path, value, std::strlen(value)); return this->path; } // OFFSET: 0x00B870D0 // FUNCTION: fn_19338 bool ReplayFileList::Select(int index) { if (index < 0) { return false; } const int count = static_cast(this->files.size()); if (index >= count) { index = count - 1; } this->selected = index; return true; }