mirror of
https://github.com/Astatin3/meteorbot-old.git
synced 2026-06-09 08:38:07 -06:00
Initial commit
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
|
||||
struct subprocess_s;
|
||||
|
||||
struct MinecraftServerOptions
|
||||
{
|
||||
std::string argv0 = "";
|
||||
int view_distance = 5;
|
||||
};
|
||||
|
||||
/// @brief Singleton wrapper around a Minecraft server subprocess instance.
|
||||
/// Built to be used in a single-threaded way (to follow catch2 requirements).
|
||||
class MinecraftServer
|
||||
{
|
||||
private:
|
||||
MinecraftServer();
|
||||
~MinecraftServer();
|
||||
MinecraftServer(const MinecraftServer&) = delete;
|
||||
MinecraftServer(MinecraftServer&&) = delete;
|
||||
MinecraftServer& operator=(const MinecraftServer&) = delete;
|
||||
|
||||
public:
|
||||
static MinecraftServerOptions options;
|
||||
static MinecraftServer& GetInstance();
|
||||
|
||||
void Initialize();
|
||||
|
||||
/// @brief Wait for the subprocess to write a line to stdout or stderr. Thread safe
|
||||
/// @param timeout_ms If != 0 and no line received after timeout_ms, will throw an exception
|
||||
/// @return The received line content
|
||||
std::string WaitLine(const long long int timeout_ms = 0);
|
||||
|
||||
/// @brief Wait for the subprocess to write a line matching regex to stdout or stderr.
|
||||
/// @param regex A regex pattern of the line to match
|
||||
/// @param timeout_ms If != 0 and no matching line received after timeout_ms, will throw an exception
|
||||
/// @return First element is the full line, other are all the captured groups from the regex
|
||||
std::vector<std::string> WaitLine(const std::string& regex, const long long int timeout_ms = 0);
|
||||
|
||||
/// @brief Write a line to subprocess stdin. Not thread-safe.
|
||||
/// @param input Line to write
|
||||
void SendLine(const std::string& input);
|
||||
|
||||
/// @brief Path to the folder holding the structure files
|
||||
/// @return The path in which the server loads the structures
|
||||
std::filesystem::path GetStructurePath() const;
|
||||
|
||||
private:
|
||||
/// @brief Terminate the subprocess
|
||||
void Kill();
|
||||
/// @brief Internal function used to retrieve stdout and stderr
|
||||
void InternalThreadRead();
|
||||
/// @brief Prepare a folder to host a server
|
||||
void InitServerFolder(const std::filesystem::path& path);
|
||||
/// @brief Set a gamerule value on the server
|
||||
void SetGamerule(const std::string& gamerule, const std::string& value);
|
||||
/// @brief Set all non default gamerules on the server
|
||||
void InitServerGamerules();
|
||||
|
||||
private:
|
||||
/// @brief Thread running the stdout reading loop
|
||||
std::thread read_thread;
|
||||
/// @brief Internal mutex for the std::queue lines
|
||||
std::mutex internal_read_mutex;
|
||||
/// @brief Queue of lines generated by the subprocess
|
||||
std::queue<std::string> lines;
|
||||
|
||||
/// @brief Server subprocess handle
|
||||
std::unique_ptr<subprocess_s> subprocess;
|
||||
|
||||
bool running;
|
||||
|
||||
std::filesystem::path server_path;
|
||||
};
|
||||
@@ -0,0 +1,236 @@
|
||||
#pragma once
|
||||
|
||||
#include <catch2/reporters/catch_reporter_event_listener.hpp>
|
||||
#include <catch2/reporters/catch_reporter_registrars.hpp>
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
|
||||
#include <botcraft/AI/BehaviourClient.hpp>
|
||||
#include <botcraft/Game/Vector3.hpp>
|
||||
#include <botcraft/Game/Enums.hpp>
|
||||
#include <botcraft/Game/ManagersClient.hpp>
|
||||
#include <botcraft/Game/World/World.hpp>
|
||||
#include <botcraft/Utilities/SleepUtilities.hpp>
|
||||
|
||||
#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<std::string, std::string>& blockstates = {}, const std::map<std::string, std::string>& 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<std::string, std::string>& 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<std::string>& pages, const std::string& facing = "north", const std::string& title = "", const std::string& author = "", const std::vector<std::string>& description = {});
|
||||
|
||||
void Teleport(const std::string& name, const Botcraft::Vector3<double>& pos, const float yaw = 0.0f, const float pitch = 0.0f) const;
|
||||
|
||||
template<class ClientType = Botcraft::ManagersClient,
|
||||
std::enable_if_t<std::is_base_of_v<Botcraft::ConnectionClient, ClientType>, bool> = true>
|
||||
std::unique_ptr<ClientType> GetBot(std::string& botname, int& id, Botcraft::Vector3<double>& pos, const Botcraft::GameType gamemode = Botcraft::GameType::Survival)
|
||||
{
|
||||
std::unique_ptr<ClientType> client;
|
||||
if constexpr (std::is_same_v<ClientType, Botcraft::ConnectionClient>)
|
||||
{
|
||||
client = std::make_unique<ClientType>();
|
||||
}
|
||||
else
|
||||
{
|
||||
client = std::make_unique<ClientType>(false);
|
||||
}
|
||||
|
||||
botname = "botcraft_" + std::to_string(bot_index++);
|
||||
client->Connect("127.0.0.1:25565", botname);
|
||||
|
||||
std::vector<std::string> 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<ClientType, Botcraft::ConnectionClient>)
|
||||
{
|
||||
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<Botcraft::World> 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<Botcraft::BehaviourClient, ClientType>)
|
||||
{
|
||||
client->StartBehaviour();
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
template<class ClientType = Botcraft::ManagersClient,
|
||||
std::enable_if_t<std::is_base_of_v<Botcraft::ConnectionClient, ClientType>, bool> = true>
|
||||
std::unique_ptr<ClientType> GetBot(std::string& botname, const Botcraft::GameType gamemode = Botcraft::GameType::Survival)
|
||||
{
|
||||
int id;
|
||||
Botcraft::Vector3<double> pos;
|
||||
return GetBot<ClientType>(botname, id, pos, gamemode);
|
||||
}
|
||||
|
||||
|
||||
template<class ClientType = Botcraft::ManagersClient,
|
||||
std::enable_if_t<std::is_base_of_v<Botcraft::ConnectionClient, ClientType>, bool> = true>
|
||||
std::unique_ptr<ClientType> GetBot(std::string& botname, Botcraft::Vector3<double>& pos, const Botcraft::GameType gamemode = Botcraft::GameType::Survival)
|
||||
{
|
||||
int id;
|
||||
return GetBot<ClientType>(botname, id, pos, gamemode);
|
||||
}
|
||||
|
||||
template<class ClientType = Botcraft::ManagersClient,
|
||||
std::enable_if_t<std::is_base_of_v<Botcraft::ConnectionClient, ClientType>, bool> = true>
|
||||
std::unique_ptr<ClientType> GetBot(const Botcraft::GameType gamemode = Botcraft::GameType::Survival)
|
||||
{
|
||||
std::string botname;
|
||||
int id;
|
||||
Botcraft::Vector3<double> pos;
|
||||
return GetBot<ClientType>(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<double>& dst, const std::vector<std::string>& 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<Botcraft::ManagersClient> 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<std::string> current_test_case_failures;
|
||||
|
||||
/// @brief Store names of all running (potentially) nested sections
|
||||
std::vector<std::string> 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;
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#include "TestManager.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <botcraft/AI/TemplatedBehaviourClient.hpp>
|
||||
#include <botcraft/AI/SimpleBehaviourClient.hpp>
|
||||
#include <botcraft/Game/ManagersClient.hpp>
|
||||
#include <botcraft/Game/Vector3.hpp>
|
||||
#include <botcraft/Game/Entities/EntityManager.hpp>
|
||||
#include <botcraft/Game/Entities/LocalPlayer.hpp>
|
||||
|
||||
template<class ClientType = Botcraft::ManagersClient>
|
||||
std::unique_ptr<ClientType> SetupTestBot(const Botcraft::Vector3<double>& offset = { 0,0,0 }, const Botcraft::GameType gamemode = Botcraft::GameType::Survival)
|
||||
{
|
||||
std::string botname;
|
||||
std::unique_ptr<ClientType> bot = TestManager::GetInstance().GetBot<ClientType>(botname, gamemode);
|
||||
|
||||
const Botcraft::Vector3<double> pos = offset + TestManager::GetInstance().GetCurrentOffset();
|
||||
TestManager::GetInstance().Teleport(botname, pos);
|
||||
|
||||
// Wait for bot to register teleportation
|
||||
std::shared_ptr<Botcraft::LocalPlayer> local_player = bot->GetLocalPlayer();
|
||||
if (!Botcraft::Utilities::WaitForCondition([&]()
|
||||
{
|
||||
return local_player->GetPosition().SqrDist(pos) < 1.0;
|
||||
}, 5000))
|
||||
{
|
||||
throw std::runtime_error("Timeout waiting " + botname + " to register teleportation");
|
||||
}
|
||||
|
||||
// Wait for bot to load center and corner view_distance blocks
|
||||
std::shared_ptr<Botcraft::World> world = bot->GetWorld();
|
||||
const int chunk_x = static_cast<int>(std::floor(pos.x / static_cast<double>(Botcraft::CHUNK_WIDTH)));
|
||||
const int chunk_z = static_cast<int>(std::floor(pos.z / static_cast<double>(Botcraft::CHUNK_WIDTH)));
|
||||
// -1 because sometimes corner chunks are not sent, depending on where you are on the current chunk
|
||||
const int view_distance = MinecraftServer::options.view_distance - 1;
|
||||
std::vector<std::pair<int, int>> wait_loaded = {
|
||||
{chunk_x * Botcraft::CHUNK_WIDTH, chunk_z * Botcraft::CHUNK_WIDTH},
|
||||
{(chunk_x + view_distance) * Botcraft::CHUNK_WIDTH - 1, (chunk_z + view_distance) * Botcraft::CHUNK_WIDTH - 1},
|
||||
{(chunk_x - view_distance) * Botcraft::CHUNK_WIDTH, (chunk_z + view_distance) * Botcraft::CHUNK_WIDTH - 1},
|
||||
{(chunk_x + view_distance) * Botcraft::CHUNK_WIDTH - 1, (chunk_z - view_distance) * Botcraft::CHUNK_WIDTH},
|
||||
{(chunk_x - view_distance) * Botcraft::CHUNK_WIDTH, (chunk_z - view_distance) * Botcraft::CHUNK_WIDTH}
|
||||
};
|
||||
|
||||
if (!Botcraft::Utilities::WaitForCondition([&]() {
|
||||
for (size_t i = 0; i < wait_loaded.size(); ++i)
|
||||
{
|
||||
if (!bot->GetWorld()->IsLoaded(Botcraft::Position(wait_loaded[i].first, 2, wait_loaded[i].second)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, 15000))
|
||||
{
|
||||
throw std::runtime_error("Timeout waiting " + botname + " to load surroundings");
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
#include <botcraft/Game/AssetsManager.hpp>
|
||||
#include <botcraft/Game/Inventory/InventoryManager.hpp>
|
||||
#include <botcraft/Game/Inventory/Window.hpp>
|
||||
|
||||
template<class ClientType = Botcraft::SimpleBehaviourClient>
|
||||
bool GiveItem(std::unique_ptr<ClientType>& bot, const std::string& item_name, const std::string& item_pretty_name, const int quantity = 1)
|
||||
{
|
||||
const std::shared_ptr<Botcraft::InventoryManager> inventory_manager = bot->GetInventoryManager();
|
||||
const Botcraft::Item* item = Botcraft::AssetsManager::getInstance().GetItem(Botcraft::AssetsManager::getInstance().GetItemID(item_name));
|
||||
short receiving_slot = -1;
|
||||
const std::map<short, ProtocolCraft::Slot> slots = inventory_manager->GetPlayerInventory()->GetSlots();
|
||||
for (short i = Botcraft::Window::INVENTORY_HOTBAR_START; i < Botcraft::Window::INVENTORY_OFFHAND_INDEX; ++i)
|
||||
{
|
||||
if (slots.at(i).IsEmptySlot() ||
|
||||
(item->GetId() == slots.at(i).GetItemID() && item->GetStackSize() >= slots.at(i).GetItemCount() + quantity))
|
||||
{
|
||||
receiving_slot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// No slot available in the hotbar, check the main inventory
|
||||
if (receiving_slot == -1)
|
||||
{
|
||||
for (short i = Botcraft::Window::INVENTORY_STORAGE_START; i < Botcraft::Window::INVENTORY_HOTBAR_START; ++i)
|
||||
{
|
||||
if (slots.at(i).IsEmptySlot())
|
||||
{
|
||||
receiving_slot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (receiving_slot == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string& botname = bot->GetNetworkManager()->GetMyName();
|
||||
MinecraftServer::GetInstance().SendLine("give " + botname + " " + item_name + " " + std::to_string(quantity));
|
||||
MinecraftServer::GetInstance().WaitLine(".*?: (?:Given|Gave " + std::to_string(quantity) + ") \\[" + item_pretty_name + "\\](?: \\* " + std::to_string(quantity) + ")? to " + botname + ".*", 5000);
|
||||
return Botcraft::Utilities::WaitForCondition([&]()
|
||||
{
|
||||
return !bot->GetInventoryManager()->GetPlayerInventory()->GetSlot(receiving_slot).IsEmptySlot();
|
||||
}, 5000);
|
||||
}
|
||||
Reference in New Issue
Block a user