Initial commit

This commit is contained in:
Astatin3
2024-04-30 22:07:50 -06:00
commit 8565caa62a
8463 changed files with 4915934 additions and 0 deletions
@@ -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);
}