|
|
|
|
@ -5,12 +5,15 @@
|
|
|
|
|
*/ |
|
|
|
|
#include "pfile.h" |
|
|
|
|
|
|
|
|
|
#include <sstream> |
|
|
|
|
#include <string> |
|
|
|
|
#include <unordered_map> |
|
|
|
|
|
|
|
|
|
#include <fmt/compile.h> |
|
|
|
|
|
|
|
|
|
#include "codec.h" |
|
|
|
|
#include "engine.h" |
|
|
|
|
#include "engine/load_file.hpp" |
|
|
|
|
#include "init.h" |
|
|
|
|
#include "loadsave.h" |
|
|
|
|
#include "menu.h" |
|
|
|
|
@ -188,55 +191,263 @@ bool ArchiveContainsGame(MpqArchive &hsArchive)
|
|
|
|
|
return IsHeaderValid(hdr); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
HeroCompareResult CompareSaves(const std::string &actualSavePath, const std::string &referenceSavePath) |
|
|
|
|
class MemoryBuffer : public std::basic_streambuf<char> { |
|
|
|
|
public: |
|
|
|
|
MemoryBuffer(char *data, size_t byteCount) |
|
|
|
|
{ |
|
|
|
|
setg(data, data, data + byteCount); |
|
|
|
|
setp(data, data + byteCount); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
struct CompareInfo { |
|
|
|
|
std::unique_ptr<byte[]> &data; |
|
|
|
|
size_t currentPosition; |
|
|
|
|
size_t size; |
|
|
|
|
bool isTownLevel; |
|
|
|
|
bool dataExists; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
struct CompareCounter { |
|
|
|
|
int reference; |
|
|
|
|
int actual; |
|
|
|
|
int max() |
|
|
|
|
{ |
|
|
|
|
return std::max(reference, actual); |
|
|
|
|
} |
|
|
|
|
void checkIfDataExists(int count, CompareInfo &compareInfoReference, CompareInfo &compareInfoActual) |
|
|
|
|
{ |
|
|
|
|
if (reference == count) |
|
|
|
|
compareInfoReference.dataExists = false; |
|
|
|
|
if (actual == count) |
|
|
|
|
compareInfoActual.dataExists = false; |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
inline bool string_ends_with(std::string const &value, std::string const &ending) |
|
|
|
|
{ |
|
|
|
|
if (ending.size() > value.size()) |
|
|
|
|
return false; |
|
|
|
|
return std::equal(ending.rbegin(), ending.rend(), value.rbegin()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void CreateDetailDiffs(string_view prefix, string_view memoryMapFile, CompareInfo &compareInfoReference, CompareInfo &compareInfoActual, std::unordered_map<std::string, size_t> &foundDiffs) |
|
|
|
|
{ |
|
|
|
|
std::vector<std::string> possibleFileNamesToCheck; |
|
|
|
|
possibleFileNamesToCheck.emplace_back("hero"); |
|
|
|
|
possibleFileNamesToCheck.emplace_back("game"); |
|
|
|
|
possibleFileNamesToCheck.emplace_back("additionalMissiles"); |
|
|
|
|
// Note: Detail diffs are currently only supported in unit tests
|
|
|
|
|
std::string memoryMapFileAssetName = StrCat(paths::BasePath(), "/test/fixtures/memory_map/", memoryMapFile, ".txt"); |
|
|
|
|
|
|
|
|
|
SDL_RWops *handle = SDL_RWFromFile(memoryMapFileAssetName.c_str(), "r"); |
|
|
|
|
if (handle == nullptr) { |
|
|
|
|
app_fatal(StrCat("MemoryMapFile ", memoryMapFile, " is missing")); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
size_t readBytes = SDL_RWsize(handle); |
|
|
|
|
std::unique_ptr<byte[]> memoryMapFileData { new byte[readBytes] }; |
|
|
|
|
SDL_RWread(handle, memoryMapFileData.get(), readBytes, 1); |
|
|
|
|
|
|
|
|
|
MemoryBuffer buffer(reinterpret_cast<char *>(memoryMapFileData.get()), readBytes); |
|
|
|
|
std::istream reader(&buffer); |
|
|
|
|
|
|
|
|
|
std::unordered_map<std::string, CompareCounter> counter; |
|
|
|
|
|
|
|
|
|
auto getCounter = [&](const std::string &counterAsString) { |
|
|
|
|
auto it = counter.find(counterAsString); |
|
|
|
|
if (it != counter.end()) |
|
|
|
|
return it->second; |
|
|
|
|
int countFromMapFile = std::stoi(counterAsString); |
|
|
|
|
return CompareCounter { countFromMapFile, countFromMapFile }; |
|
|
|
|
}; |
|
|
|
|
auto addDiff = [&](const std::string &diffKey) { |
|
|
|
|
auto it = foundDiffs.find(diffKey); |
|
|
|
|
if (it == foundDiffs.end()) { |
|
|
|
|
foundDiffs.insert_or_assign(diffKey, 1); |
|
|
|
|
} else { |
|
|
|
|
foundDiffs.insert_or_assign(diffKey, it->second + 1); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
auto compareBytes = [&](size_t countBytes) { |
|
|
|
|
if (compareInfoReference.dataExists && compareInfoReference.currentPosition + countBytes > compareInfoReference.size) |
|
|
|
|
app_fatal(StrCat("Comparsion failed. Too less bytes in reference to compare. Location: ", prefix)); |
|
|
|
|
if (compareInfoActual.dataExists && compareInfoActual.currentPosition + countBytes > compareInfoActual.size) |
|
|
|
|
app_fatal(StrCat("Comparsion failed. Too less bytes in actual to compare. Location: ", prefix)); |
|
|
|
|
bool result = true; |
|
|
|
|
if (compareInfoReference.dataExists && compareInfoActual.dataExists) |
|
|
|
|
result = memcmp(compareInfoReference.data.get() + compareInfoReference.currentPosition, compareInfoActual.data.get() + compareInfoActual.currentPosition, countBytes) == 0; |
|
|
|
|
if (compareInfoReference.dataExists) |
|
|
|
|
compareInfoReference.currentPosition += countBytes; |
|
|
|
|
if (compareInfoActual.dataExists) |
|
|
|
|
compareInfoActual.currentPosition += countBytes; |
|
|
|
|
return result; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
auto read32BitInt = [&](CompareInfo &compareInfo, bool useLE) { |
|
|
|
|
int32_t value = 0; |
|
|
|
|
if (!compareInfo.dataExists) |
|
|
|
|
return value; |
|
|
|
|
if (compareInfo.currentPosition + sizeof(value) > compareInfo.size) |
|
|
|
|
app_fatal("read32BitInt failed. Too less bytes to read."); |
|
|
|
|
memcpy(&value, compareInfo.data.get() + compareInfo.currentPosition, sizeof(value)); |
|
|
|
|
if (useLE) |
|
|
|
|
value = SDL_SwapLE32(value); |
|
|
|
|
else |
|
|
|
|
value = SDL_SwapBE32(value); |
|
|
|
|
return value; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
std::string line; |
|
|
|
|
while (std::getline(reader, line)) { |
|
|
|
|
if (line.size() > 0 && line[line.size() - 1] == '\r') |
|
|
|
|
line.resize(line.size() - 1); |
|
|
|
|
if (line.size() == 0) |
|
|
|
|
continue; |
|
|
|
|
std::stringstream lineStream(line); |
|
|
|
|
std::string command; |
|
|
|
|
std::getline(lineStream, command, ' '); |
|
|
|
|
|
|
|
|
|
bool dataExistsReference = compareInfoReference.dataExists; |
|
|
|
|
bool dataExistsActual = compareInfoActual.dataExists; |
|
|
|
|
|
|
|
|
|
if (string_ends_with(command, "_HF")) { |
|
|
|
|
if (!gbIsHellfire) |
|
|
|
|
continue; |
|
|
|
|
command.resize(command.size() - 3); |
|
|
|
|
} |
|
|
|
|
if (string_ends_with(command, "_DA")) { |
|
|
|
|
if (gbIsHellfire) |
|
|
|
|
continue; |
|
|
|
|
command.resize(command.size() - 3); |
|
|
|
|
} |
|
|
|
|
if (string_ends_with(command, "_DL")) { |
|
|
|
|
if (compareInfoReference.isTownLevel && compareInfoActual.isTownLevel) |
|
|
|
|
continue; |
|
|
|
|
if (compareInfoReference.isTownLevel) |
|
|
|
|
compareInfoReference.dataExists = false; |
|
|
|
|
if (compareInfoActual.isTownLevel) |
|
|
|
|
compareInfoActual.dataExists = false; |
|
|
|
|
command.resize(command.size() - 3); |
|
|
|
|
} |
|
|
|
|
if (command == "R" || command == "LT" || command == "LC" || command == "LC_LE") { |
|
|
|
|
std::string bitsAsString; |
|
|
|
|
std::getline(lineStream, bitsAsString, ' '); |
|
|
|
|
std::string comment; |
|
|
|
|
std::getline(lineStream, comment); |
|
|
|
|
|
|
|
|
|
size_t bytes = static_cast<size_t>(std::stoi(bitsAsString) / 8); |
|
|
|
|
|
|
|
|
|
if (command == "LT") { |
|
|
|
|
int32_t valueReference = read32BitInt(compareInfoReference, false); |
|
|
|
|
int32_t valueActual = read32BitInt(compareInfoActual, false); |
|
|
|
|
assert(sizeof(valueReference) == bytes); |
|
|
|
|
compareInfoReference.isTownLevel = valueReference == 0; |
|
|
|
|
compareInfoActual.isTownLevel = valueActual == 0; |
|
|
|
|
} |
|
|
|
|
if (command == "LC" || command == "LC_LE") { |
|
|
|
|
int32_t valueReference = read32BitInt(compareInfoReference, command == "LC_LE"); |
|
|
|
|
int32_t valueActual = read32BitInt(compareInfoActual, command == "LC_LE"); |
|
|
|
|
assert(sizeof(valueReference) == bytes); |
|
|
|
|
counter.insert_or_assign(comment, CompareCounter { valueReference, valueActual }); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!compareBytes(bytes)) { |
|
|
|
|
std::string diffKey = StrCat(prefix, ".", comment); |
|
|
|
|
addDiff(diffKey); |
|
|
|
|
} |
|
|
|
|
} else if (command == "M") { |
|
|
|
|
std::string countAsString; |
|
|
|
|
std::getline(lineStream, countAsString, ' '); |
|
|
|
|
std::string bitsAsString; |
|
|
|
|
std::getline(lineStream, bitsAsString, ' '); |
|
|
|
|
std::string comment; |
|
|
|
|
std::getline(lineStream, comment); |
|
|
|
|
|
|
|
|
|
CompareCounter count = getCounter(countAsString); |
|
|
|
|
size_t bytes = static_cast<size_t>(std::stoi(bitsAsString) / 8); |
|
|
|
|
for (int i = 0; i < count.max(); i++) { |
|
|
|
|
count.checkIfDataExists(i, compareInfoReference, compareInfoActual); |
|
|
|
|
if (!compareBytes(bytes)) { |
|
|
|
|
std::string diffKey = StrCat(prefix, ".", comment); |
|
|
|
|
addDiff(diffKey); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else if (command == "C") { |
|
|
|
|
std::string countAsString; |
|
|
|
|
std::getline(lineStream, countAsString, ' '); |
|
|
|
|
std::string subMemoryMapFile; |
|
|
|
|
std::getline(lineStream, subMemoryMapFile, ' '); |
|
|
|
|
std::string comment; |
|
|
|
|
std::getline(lineStream, comment); |
|
|
|
|
|
|
|
|
|
CompareCounter count = getCounter(countAsString); |
|
|
|
|
subMemoryMapFile.erase(std::remove(subMemoryMapFile.begin(), subMemoryMapFile.end(), '\r'), subMemoryMapFile.end()); |
|
|
|
|
for (int i = 0; i < count.max(); i++) { |
|
|
|
|
count.checkIfDataExists(i, compareInfoReference, compareInfoActual); |
|
|
|
|
std::string subPrefix = StrCat(prefix, ".", comment); |
|
|
|
|
CreateDetailDiffs(subPrefix, subMemoryMapFile, compareInfoReference, compareInfoActual, foundDiffs); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
compareInfoReference.dataExists = dataExistsReference; |
|
|
|
|
compareInfoActual.dataExists = dataExistsActual; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
struct CompareTargets { |
|
|
|
|
std::string fileName; |
|
|
|
|
std::string memoryMapFileName; |
|
|
|
|
bool isTownLevel; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
HeroCompareResult CompareSaves(const std::string &actualSavePath, const std::string &referenceSavePath, bool logDetails) |
|
|
|
|
{ |
|
|
|
|
std::vector<CompareTargets> possibleFileToCheck; |
|
|
|
|
possibleFileToCheck.push_back({ "hero", "hero", false }); |
|
|
|
|
possibleFileToCheck.push_back({ "game", "game", false }); |
|
|
|
|
possibleFileToCheck.push_back({ "additionalMissiles", "additionalMissiles", false }); |
|
|
|
|
char szPerm[MaxMpqPathSize]; |
|
|
|
|
for (int i = 0; GetPermSaveNames(i, szPerm); i++) { |
|
|
|
|
possibleFileNamesToCheck.emplace_back(szPerm); |
|
|
|
|
possibleFileToCheck.push_back({ std::string(szPerm), "level", i == 0 }); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::int32_t error; |
|
|
|
|
auto actualArchive = *MpqArchive::Open(actualSavePath.c_str(), error); |
|
|
|
|
auto referenceArchive = *MpqArchive::Open(referenceSavePath.c_str(), error); |
|
|
|
|
|
|
|
|
|
for (const auto &fileName : possibleFileNamesToCheck) { |
|
|
|
|
size_t fileSizeActual; |
|
|
|
|
auto fileDataActual = ReadArchive(actualArchive, fileName.c_str(), &fileSizeActual); |
|
|
|
|
size_t fileSizeReference; |
|
|
|
|
auto fileDataReference = ReadArchive(referenceArchive, fileName.c_str(), &fileSizeReference); |
|
|
|
|
bool compareResult = true; |
|
|
|
|
std::string message; |
|
|
|
|
for (const auto &compareTarget : possibleFileToCheck) { |
|
|
|
|
size_t fileSizeActual = 0; |
|
|
|
|
auto fileDataActual = ReadArchive(actualArchive, compareTarget.fileName.c_str(), &fileSizeActual); |
|
|
|
|
size_t fileSizeReference = 0; |
|
|
|
|
auto fileDataReference = ReadArchive(referenceArchive, compareTarget.fileName.c_str(), &fileSizeReference); |
|
|
|
|
if (fileDataActual.get() == nullptr && fileDataReference.get() == nullptr) { |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
if (fileSizeActual != fileSizeReference) { |
|
|
|
|
return { HeroCompareResult::Difference, StrCat("file \"", fileName, "\" is different size. Expected: ", fileSizeReference, "Actual: ", fileSizeActual) }; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const byte *reference = fileDataReference.get(); |
|
|
|
|
const byte *referenceEnd = reference + fileSizeActual; |
|
|
|
|
const byte *actual = fileDataActual.get(); |
|
|
|
|
while (reference != referenceEnd) { |
|
|
|
|
if (*reference != *actual) { |
|
|
|
|
const size_t diffSize = std::min<size_t>(16, referenceEnd - reference); |
|
|
|
|
return { |
|
|
|
|
HeroCompareResult::Difference, |
|
|
|
|
fmt::format( |
|
|
|
|
"file \"{}\" is different at byte {}:\n" |
|
|
|
|
"Expected: ... {:02X} ...\n" |
|
|
|
|
" Actual: ... {:02X} ...", |
|
|
|
|
fileName, reference - fileDataReference.get(), |
|
|
|
|
fmt::join(reference, reference + diffSize, " "), |
|
|
|
|
fmt::join(actual, actual + diffSize, " ")) |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
++reference; |
|
|
|
|
++actual; |
|
|
|
|
if (fileSizeActual == fileSizeReference && memcmp(fileDataReference.get(), fileDataActual.get(), fileSizeActual) == 0) |
|
|
|
|
continue; |
|
|
|
|
compareResult = false; |
|
|
|
|
if (!message.empty()) |
|
|
|
|
message.append("\n"); |
|
|
|
|
if (fileSizeActual != fileSizeReference) |
|
|
|
|
StrAppend(message, "file \"", compareTarget.fileName, "\" is different size. Expected: ", fileSizeReference, " Actual: ", fileSizeActual); |
|
|
|
|
else |
|
|
|
|
StrAppend(message, "file \"", compareTarget.fileName, "\" has different content."); |
|
|
|
|
if (!logDetails) |
|
|
|
|
continue; |
|
|
|
|
std::unordered_map<std::string, size_t> foundDiffs; |
|
|
|
|
CompareInfo compareInfoReference = { fileDataReference, 0, fileSizeReference, compareTarget.isTownLevel, fileSizeReference != 0 }; |
|
|
|
|
CompareInfo compareInfoActual = { fileDataActual, 0, fileSizeActual, compareTarget.isTownLevel, fileSizeActual != 0 }; |
|
|
|
|
CreateDetailDiffs(compareTarget.fileName, compareTarget.memoryMapFileName, compareInfoReference, compareInfoActual, foundDiffs); |
|
|
|
|
if (compareInfoReference.currentPosition != fileSizeReference) |
|
|
|
|
app_fatal(StrCat("Comparsion failed. Uncompared bytes in reference. File: ", compareTarget.fileName)); |
|
|
|
|
if (compareInfoActual.currentPosition != fileSizeActual) |
|
|
|
|
app_fatal(StrCat("Comparsion failed. Uncompared bytes in actual. File: ", compareTarget.fileName)); |
|
|
|
|
for (auto entry : foundDiffs) { |
|
|
|
|
StrAppend(message, "\nDiff found in ", entry.first, " count: ", entry.second); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return { HeroCompareResult::Same, {} }; |
|
|
|
|
return { compareResult ? HeroCompareResult::Same : HeroCompareResult::Difference, message }; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void pfile_write_hero(MpqWriter &saveWriter, bool writeGameData) |
|
|
|
|
@ -309,7 +520,7 @@ void pfile_write_hero_demo(int demo)
|
|
|
|
|
pfile_write_hero(saveWriter, true); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
HeroCompareResult pfile_compare_hero_demo(int demo) |
|
|
|
|
HeroCompareResult pfile_compare_hero_demo(int demo, bool logDetails) |
|
|
|
|
{ |
|
|
|
|
std::string referenceSavePath = GetSavePath(gSaveNumber, StrCat("demo_", demo, "_reference_")); |
|
|
|
|
|
|
|
|
|
@ -322,7 +533,7 @@ HeroCompareResult pfile_compare_hero_demo(int demo)
|
|
|
|
|
pfile_write_hero(saveWriter, true); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return CompareSaves(actualSavePath, referenceSavePath); |
|
|
|
|
return CompareSaves(actualSavePath, referenceSavePath, logDetails); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void sfile_write_stash() |
|
|
|
|
|