#include "catch.hpp" #include #include #include #include #include #include #include "Command.h" #include "CommandManager.h" #include "CommandSerializer.h" #include "ConfigLoader.h" #include "GameConfig.h" #include "ReplayRecorder.h" #include "Simulation.h" namespace { std::string readFile(const std::string& path) { std::ifstream stream(path, std::ios::in | std::ios::binary); std::ostringstream buffer; buffer << stream.rdbuf(); return buffer.str(); } std::string tempOutputDir() { return (QDir::tempPath() + "/dota_factory_replay_test").toStdString(); } } // namespace // --------------------------------------------------------------------------- // Command serialization // --------------------------------------------------------------------------- TEST_CASE("serializeCommand: plain placement", "[replay]") { PlaceBuildingCommand command; command.type = BuildingType::Miner; command.anchor = QPoint(-3, 5); command.rotation = Rotation::East; REQUIRE(serializeCommand(command) == "place miner -3 5 E"); } TEST_CASE("serializeCommand: placement with recipe", "[replay]") { PlaceBuildingCommand command; command.type = BuildingType::Miner; command.anchor = QPoint(-2, 0); command.rotation = Rotation::North; command.recipeId = "mine_iron_ore"; REQUIRE(serializeCommand(command) == "place miner -2 0 N recipe mine_iron_ore"); } TEST_CASE("serializeCommand: placement with ship layout", "[replay]") { PlaceBuildingCommand command; command.type = BuildingType::Shipyard; command.anchor = QPoint(-3, 0); command.rotation = Rotation::East; ShipLayoutConfig layout; layout.placedModules.push_back(PlacedModule{"weapon_basic", QPoint(1, 2), Rotation::South}); command.shipLayout = layout; REQUIRE(serializeCommand(command) == "place shipyard -3 0 E layout 1 weapon_basic 1 2 S"); } TEST_CASE("serializeCommand: splitter filters are length-prefixed", "[replay]") { SetSplitterFiltersCommand command; command.tile = QPoint(4, 7); command.filterA = { ItemType{"iron_ore"}, ItemType{"copper_ore"} }; command.filterB = { ItemType{"coal"} }; REQUIRE(serializeCommand(command) == "splitterfilters 4 7 2 iron_ore copper_ore 1 coal"); } TEST_CASE("serializeCommand: demolish / rotate / schematic / clearbelt", "[replay]") { DemolishCommand demolish; demolish.id = 12; REQUIRE(serializeCommand(demolish) == "demolish 12"); RotateInPlaceCommand rotate; rotate.id = 7; rotate.newRotation = Rotation::West; REQUIRE(serializeCommand(rotate) == "rotate 7 W"); ApplySchematicChoiceCommand schematic; schematic.choiceIndex = 2; REQUIRE(serializeCommand(schematic) == "schematic 2"); ClearBeltTilesCommand clear; clear.tiles = { QPoint(1, 2), QPoint(3, 4) }; REQUIRE(serializeCommand(clear) == "clearbelt 2 1 2 3 4"); } // --------------------------------------------------------------------------- // ReplayRecorder file output // --------------------------------------------------------------------------- TEST_CASE("ReplayRecorder writes a well-formed file", "[replay]") { ReplayRecorder recorder(CONFIG_DIR, tempOutputDir()); recorder.startNewRun(42u, 0x1122334455667788ull); PlaceBuildingCommand place; place.type = BuildingType::Miner; place.anchor = QPoint(-3, 0); place.rotation = Rotation::East; recorder.recordCommand(5, place, 0xabcdef0123456789ull); recorder.recordChecksum(30, 0x0ffffffffffffff0ull); const std::string path = recorder.currentFilePath(); REQUIRE_FALSE(path.empty()); recorder.close(); const std::string content = readFile(path); // Header. REQUIRE(content.find("# dota_factory replay") != std::string::npos); REQUIRE(content.find("version 1") != std::string::npos); REQUIRE(content.find("seed 42") != std::string::npos); REQUIRE(content.find("config_hash ") != std::string::npos); REQUIRE(content.find("---") != std::string::npos); // Initial + per-command + periodic checksums. REQUIRE(content.find("# checksum 0 1122334455667788") != std::string::npos); REQUIRE(content.find("5 place miner -3 0 E") != std::string::npos); REQUIRE(content.find("# checksum 5 abcdef0123456789") != std::string::npos); REQUIRE(content.find("# checksum 30 0ffffffffffffff0") != std::string::npos); QFile::remove(QString::fromStdString(path)); } TEST_CASE("CommandManager records commands and an initial checksum on drain", "[replay]") { Simulation sim(ConfigLoader::loadFromDirectory(CONFIG_DIR), 7u); CommandManager manager(sim); std::unique_ptr recorder = std::make_unique(CONFIG_DIR, tempOutputDir()); ReplayRecorder* recorderPtr = recorder.get(); // setRecorder opens the file and writes the header + the tick-0 checksum. manager.setRecorder(std::move(recorder)); const std::string path = recorderPtr->currentFilePath(); REQUIRE_FALSE(path.empty()); std::shared_ptr place = std::make_shared(); place->type = BuildingType::Miner; place->anchor = QPoint(-3, 0); manager.enqueue(place); manager.drain(); const std::string content = readFile(path); REQUIRE(content.find("seed 7") != std::string::npos); REQUIRE(content.find("# checksum 0 ") != std::string::npos); // Drained at tick 0 (no ticks have run), so the command line is tagged tick 0. REQUIRE(content.find("0 place miner -3 0 E") != std::string::npos); QFile::remove(QString::fromStdString(path)); } TEST_CASE("ReplayRecorder startNewRun rolls to a new file", "[replay]") { ReplayRecorder recorder(CONFIG_DIR, tempOutputDir()); recorder.startNewRun(1u, 0ull); const std::string first = recorder.currentFilePath(); recorder.startNewRun(2u, 0ull); const std::string second = recorder.currentFilePath(); REQUIRE(first != second); REQUIRE(second.find("_2.replay") != std::string::npos); recorder.close(); } TEST_CASE("CommandManager rolls the replay file on a Reset command", "[replay]") { Simulation sim(ConfigLoader::loadFromDirectory(CONFIG_DIR), 1u); CommandManager manager(sim); std::unique_ptr recorder = std::make_unique(CONFIG_DIR, tempOutputDir()); ReplayRecorder* recorderPtr = recorder.get(); manager.setRecorder(std::move(recorder)); const std::string firstPath = recorderPtr->currentFilePath(); std::shared_ptr reset = std::make_shared(); reset->config = std::make_shared(ConfigLoader::loadFromDirectory(CONFIG_DIR)); reset->seed = 999u; manager.enqueue(reset); manager.drain(); const std::string secondPath = recorderPtr->currentFilePath(); REQUIRE(firstPath != secondPath); REQUIRE(secondPath.find("_999.replay") != std::string::npos); manager.setRecorder(nullptr); QFile::remove(QString::fromStdString(firstPath)); QFile::remove(QString::fromStdString(secondPath)); }