#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include "MinecraftServer.hpp" /// @brief Singleton class that organizes the layout of the tests /// in the world and sets them up. class TestManager { private: TestManager(); ~TestManager(); TestManager(const TestManager&) = delete; TestManager(TestManager&&) = delete; TestManager& operator=(const TestManager&) = delete; /// @brief Spacing between tests static constexpr int spacing_x = 3; /// @brief Spacing between sections static constexpr int spacing_z = 3; public: static TestManager& GetInstance(); /// @brief current_offset getter /// @return The offset in the world of the currently running section const Botcraft::Position& GetCurrentOffset() const; #if PROTOCOL_VERSION > 340 /* > 1.12.2 */ void SetBlock(const std::string& name, const Botcraft::Position& pos, const std::map& blockstates = {}, const std::map& metadata = {}, const bool escape_metadata = true) const; #else void SetBlock(const std::string& name, const Botcraft::Position& pos, const int block_variant = 0, const std::map& metadata = {}, const bool escape_metadata = true) const; #endif /// @brief Create a book with given content at pos /// @param pos Position of the item frame/lectern /// @param pages Content of the pages of the book /// @param facing Orientation of the item frame/lectern (book is toward this direction) /// @param title Title of the book /// @param author Author of the book /// @param description Description of the book (minecraft tooltip) void CreateBook(const Botcraft::Position& pos, const std::vector& pages, const std::string& facing = "north", const std::string& title = "", const std::string& author = "", const std::vector& description = {}); void Teleport(const std::string& name, const Botcraft::Vector3& pos, const float yaw = 0.0f, const float pitch = 0.0f) const; template, bool> = true> std::unique_ptr GetBot(std::string& botname, int& id, Botcraft::Vector3& pos, const Botcraft::GameType gamemode = Botcraft::GameType::Survival) { std::unique_ptr client; if constexpr (std::is_same_v) { client = std::make_unique(); } else { client = std::make_unique(false); } botname = "botcraft_" + std::to_string(bot_index++); client->Connect("127.0.0.1:25565", botname); std::vector captured = MinecraftServer::GetInstance().WaitLine(".*?" + botname + ".*? logged in with entity id (\\d+) at \\(([\\d.,]+), ([\\d.,]+), ([\\d.,]+)\\).*", 5000); id = std::stoi(captured[1]); pos = Botcraft::Vector3(std::stod(captured[2]), std::stod(captured[3]), std::stod(captured[4])); MinecraftServer::GetInstance().WaitLine(".*?" + botname + " joined the game.*", 5000); if (gamemode != Botcraft::GameType::Creative) { SetGameMode(botname, gamemode); } if constexpr (std::is_same_v) { return client; } else { // If this is not a ConnectionClient, wait for a good amount of chunks to be loaded const int num_chunk_to_load = (MinecraftServer::options.view_distance + 1) * (MinecraftServer::options.view_distance + 1); if (!Botcraft::Utilities::WaitForCondition([&]() { std::shared_ptr world = client->GetWorld(); // If the client has not received a ClientboundGameProfilePacket yet world is still nullptr if (world != nullptr && world->GetChunks()->size() >= num_chunk_to_load) { return true; } return false; }, 10000)) { throw std::runtime_error(botname + " took too long to load world"); } if constexpr (std::is_base_of_v) { client->StartBehaviour(); } return client; } } template, bool> = true> std::unique_ptr GetBot(std::string& botname, const Botcraft::GameType gamemode = Botcraft::GameType::Survival) { int id; Botcraft::Vector3 pos; return GetBot(botname, id, pos, gamemode); } template, bool> = true> std::unique_ptr GetBot(std::string& botname, Botcraft::Vector3& pos, const Botcraft::GameType gamemode = Botcraft::GameType::Survival) { int id; return GetBot(botname, id, pos, gamemode); } template, bool> = true> std::unique_ptr GetBot(const Botcraft::GameType gamemode = Botcraft::GameType::Survival) { std::string botname; int id; Botcraft::Vector3 pos; return GetBot(botname, id, pos, gamemode); } private: enum class TestSucess { None, Success, Failure, ExpectedFailure }; /// @brief Read a NBT structure file and extract its size /// @return A X/Y/Z size Vector3 Botcraft::Position GetStructureSize(const std::string& filename) const; /// @brief Create a TP sign /// @param src Position of the sign. /// @param dst TP destination coordinates /// @param texts A list of strings to display on the sign /// @param facing The direction the sign will face /// @param success A TestSuccess result, will define wood and/or text color depending on minecraft version void CreateTPSign(const Botcraft::Position& src, const Botcraft::Vector3& dst, const std::vector& texts, const std::string& facing = "north", const TestSucess success = TestSucess::None) const; /// @brief Load a structure block into the world /// @param filename The structure block to load. If it doesn't exist, will use "_default" instead /// @param pos position of the structure block /// @param load_offset offset to load the structure to (w.r.t pos), default to (0,0,0) void LoadStructure(const std::string& filename, const Botcraft::Position& pos, const Botcraft::Position& load_offset = Botcraft::Position()) const; /// @brief Make sure a block is loaded on the server by teleporting the chunk_loader /// @param pos The position to load void MakeSureLoaded(const Botcraft::Position& pos) const; /// @brief Set a bot gamemode /// @param name Bot name /// @param gamemode Target game mode void SetGameMode(const std::string& name, const Botcraft::GameType gamemode) const; friend class TestManagerListener; void testRunStarting(Catch::TestRunInfo const& test_run_info); void testCaseStarting(Catch::TestCaseInfo const& test_info); void testCasePartialStarting(Catch::TestCaseInfo const& test_info, uint64_t part_number); void sectionStarting(Catch::SectionInfo const& section_info); void assertionEnded(Catch::AssertionStats const& assertion_stats); void testCasePartialEnded(Catch::TestCaseStats const& test_case_stats, uint64_t part_number); void testCaseEnded(Catch::TestCaseStats const& test_case_stats); void testRunEnded(Catch::TestRunStats const& test_run_info); private: /// @brief Offset in the world for current section/test Botcraft::Position current_offset; /// @brief Size of the header structure Botcraft::Position header_size; /// @brief Index of the next bot to be created int bot_index; /// @brief Bot used to load chunks before running commands on them std::unique_ptr chunk_loader; /// @brief Name of the bot used as chunk loader std::string chunk_loader_name; /// @brief Index of the current test in the world int current_test_index; /// @brief Size of the loaded structure for current test Botcraft::Position current_test_size; /// @brief All failed assertions in this test case so far std::vector current_test_case_failures; /// @brief Store names of all running (potentially) nested sections std::vector section_stack; /// @brief Position of the header for current section Botcraft::Position current_header_position; }; /// @brief Catch2 listener to get test events and pass them to the main singleton /// that can be easily accessed from inside the tests. Maybe there is an easier dedicated /// way to do it without a singleton but couldn't find it. class TestManagerListener : public Catch::EventListenerBase { using Catch::EventListenerBase::EventListenerBase; void testRunStarting(Catch::TestRunInfo const& test_run_info) override; void testCaseStarting(Catch::TestCaseInfo const& test_info) override; void testCasePartialStarting(Catch::TestCaseInfo const& test_info, uint64_t part_number) override; void sectionStarting(Catch::SectionInfo const& section_info) override; void assertionEnded(Catch::AssertionStats const& assertion_stats) override; void testCasePartialEnded(Catch::TestCaseStats const& test_case_stats, uint64_t part_number) override; void testCaseEnded(Catch::TestCaseStats const& test_case_stats) override; void testRunEnded(Catch::TestRunStats const& test_run_info) override; };