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
+5
View File
@@ -0,0 +1,5 @@
add_subdirectory(botcraft)
if(BOTCRAFT_BUILD_TESTS_ONLINE AND BOTCRAFT_COMPRESSION)
add_subdirectory(botcraft_online)
endif()
add_subdirectory(protocolCraft)
+41
View File
@@ -0,0 +1,41 @@
project(botcraft_tests)
set(SRC_FILES
src/aabb.cpp
src/behaviour_tree.cpp
src/blackboard.cpp
src/blockstate.cpp
src/world.cpp
src/init.cpp
)
add_executable(${PROJECT_NAME} ${SRC_FILES})
set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 17)
target_link_libraries(${PROJECT_NAME} PRIVATE Catch2::Catch2WithMain)
target_link_libraries(${PROJECT_NAME} PRIVATE botcraft)
set_target_properties(${PROJECT_NAME} PROPERTIES FOLDER Tests)
# Output the test executable next to the examples and library files
if(MSVC)
# To avoid having folder for each configuration when building with Visual
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_DEBUG "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_RELEASE "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_MINSIZEREL "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_DEBUG "${BOTCRAFT_OUTPUT_DIR}/lib")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${BOTCRAFT_OUTPUT_DIR}/lib")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO "${BOTCRAFT_OUTPUT_DIR}/lib")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_MINSIZEREL "${BOTCRAFT_OUTPUT_DIR}/lib")
set_property(TARGET ${PROJECT_NAME} PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/bin")
else()
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/lib")
endif(MSVC)
catch_discover_tests(${PROJECT_NAME} WORKING_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/bin")
+46
View File
@@ -0,0 +1,46 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <botcraft/Game/Physics/AABB.hpp>
using namespace Botcraft;
TEST_CASE("Constructor")
{
AABB aabb(Vector3<double>(0.5, 0.5, 0.5), Vector3<double>(0.5, 0.5, 0.5));
REQUIRE_THAT(aabb.GetMin().x, Catch::Matchers::WithinAbs(0.0, 1e-8));
REQUIRE_THAT(aabb.GetMin().y, Catch::Matchers::WithinAbs(0.0, 1e-8));
REQUIRE_THAT(aabb.GetMin().z, Catch::Matchers::WithinAbs(0.0, 1e-8));
REQUIRE_THAT(aabb.GetMax().x, Catch::Matchers::WithinAbs(1.0, 1e-8));
REQUIRE_THAT(aabb.GetMax().y, Catch::Matchers::WithinAbs(1.0, 1e-8));
REQUIRE_THAT(aabb.GetMax().z, Catch::Matchers::WithinAbs(1.0, 1e-8));
}
TEST_CASE("Translate")
{
AABB aabb(Vector3<double>(0.5, 0.5, 0.5), Vector3<double>(0.5, 0.5, 0.5));
aabb.Translate(Vector3<double>(1.0, 1.0, 1.0));
REQUIRE_THAT(aabb.GetMin().x, Catch::Matchers::WithinAbs(1.0, 1e-8));
REQUIRE_THAT(aabb.GetMin().y, Catch::Matchers::WithinAbs(1.0, 1e-8));
REQUIRE_THAT(aabb.GetMin().z, Catch::Matchers::WithinAbs(1.0, 1e-8));
REQUIRE_THAT(aabb.GetMax().x, Catch::Matchers::WithinAbs(2.0, 1e-8));
REQUIRE_THAT(aabb.GetMax().y, Catch::Matchers::WithinAbs(2.0, 1e-8));
REQUIRE_THAT(aabb.GetMax().z, Catch::Matchers::WithinAbs(2.0, 1e-8));
}
TEST_CASE("Inflate")
{
auto inflate = GENERATE(0.1, -0.1, 1e-6, -1e-6);
AABB aabb(Vector3<double>(0.5, 0.5, 0.5), Vector3<double>(0.5, 0.5, 0.5));
aabb.Inflate(inflate);
REQUIRE_THAT(aabb.GetMin().x, Catch::Matchers::WithinAbs(0.0 - inflate, 1e-8));
REQUIRE_THAT(aabb.GetMin().y, Catch::Matchers::WithinAbs(0.0 - inflate, 1e-8));
REQUIRE_THAT(aabb.GetMin().z, Catch::Matchers::WithinAbs(0.0 - inflate, 1e-8));
REQUIRE_THAT(aabb.GetMax().x, Catch::Matchers::WithinAbs(1.0 + inflate, 1e-8));
REQUIRE_THAT(aabb.GetMax().y, Catch::Matchers::WithinAbs(1.0 + inflate, 1e-8));
REQUIRE_THAT(aabb.GetMax().z, Catch::Matchers::WithinAbs(1.0 + inflate, 1e-8));
}
@@ -0,0 +1,655 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <botcraft/AI/BehaviourTree.hpp>
using namespace Botcraft;
Status TestLeaf(int& c)
{
c += 1;
return Status::Success;
}
Status TestLeafWithArg(int& c, const int i)
{
c += i;
return Status::Success;
}
TEST_CASE("LeafTree")
{
int i = 0;
SECTION("Function")
{
auto tree = Builder<int>()
.leaf(TestLeaf);
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 1);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 2);
}
SECTION("NamedFunction")
{
auto tree = Builder<int>()
.leaf("leaf", TestLeaf);
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 1);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 2);
}
SECTION("FunctionWithArg")
{
auto tree = Builder<int>()
.leaf(TestLeafWithArg, 2);
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 2);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 4);
}
SECTION("NamedFunctionWithArg")
{
auto tree = Builder<int>()
.leaf("leaf", TestLeafWithArg, 2);
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 2);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 4);
}
SECTION("std::bind")
{
auto tree = Builder<int>()
.leaf(std::bind(TestLeafWithArg, std::placeholders::_1, 2));
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 2);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 4);
}
SECTION("Namedstd::bind")
{
auto tree = Builder<int>()
.leaf("leaf", std::bind(TestLeafWithArg, std::placeholders::_1, 2));
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 2);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 4);
}
SECTION("Lambda")
{
auto tree = Builder<int>()
.leaf([](int& i) { i += 1; return Status::Success; });
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 1);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 2);
}
SECTION("NamedLambda")
{
auto tree = Builder<int>()
.leaf("leaf", [](int& i) { i += 1; return Status::Success; });
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 1);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 2);
}
}
template<class Context>
class CustomDecorator : public Decorator<Context>
{
using Decorator<Context>::Decorator;
public:
virtual Status TickImpl(Context& context) const override
{
this->TickChild(context);
return Status::Failure;
}
};
TEST_CASE("Decorator")
{
int i = 0;
SECTION("Inverter")
{
auto tree = Builder<int>()
.inverter().leaf([](int& i) { i += 1; return Status::Success; });
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Failure);
CHECK(i == 1);
CHECK(tree->Tick(i) == Status::Failure);
CHECK(i == 2);
}
SECTION("NamedInverter")
{
auto tree = Builder<int>()
.inverter("inverter").leaf([](int& i) { i += 1; return Status::Success; });
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Failure);
CHECK(i == 1);
}
SECTION("Succeeder")
{
auto tree = Builder<int>()
.succeeder().leaf([](int& i) { i += 1; return Status::Failure; });
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 1);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 2);
}
SECTION("NamedSucceeder")
{
auto tree = Builder<int>()
.succeeder("succeeder").leaf([](int& i) { i += 1; return Status::Failure; });
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 1);
}
SECTION("Repeater")
{
auto tree = Builder<int>()
.repeater(3).leaf([](int& i) { i += 1; return Status::Success; });
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 3);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 6);
i = 0;
tree = Builder<int>()
.repeater(0).leaf([](int& i) { i += 1; return i > 10 ? Status::Success : Status::Failure; });
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 11);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 12);
}
SECTION("NamedRepeater")
{
auto tree = Builder<int>()
.repeater("repeater", 3).leaf([](int& i) { i += 1; return Status::Success; });
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 3);
}
SECTION("CustomDecorator")
{
auto tree = Builder<int>()
.decorator<CustomDecorator<int>>().leaf([](int& i) { i += 1; return Status::Success; });
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Failure);
CHECK(i == 1);
CHECK(tree->Tick(i) == Status::Failure);
CHECK(i == 2);
}
SECTION("NamedCustomDecorator")
{
auto tree = Builder<int>()
.decorator<CustomDecorator<int>>("custom decorator").leaf([](int& i) { i += 1; return Status::Success; });
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Failure);
CHECK(i == 1);
}
}
template<typename Context>
class CustomComposite : public Composite<Context>
{
using Composite<Context>::Composite;
public:
virtual Status TickImpl(Context& context) const override
{
for (size_t i = 0; i < this->GetNumChildren(); ++i)
{
this->TickChild(context, i);
}
return Status::Success;
}
};
TEST_CASE("Composite")
{
int i = 0;
SECTION("Sequence")
{
auto tree = Builder<int>()
.sequence()
.leaf([](int& i) { i += 1; return Status::Success; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Success; }) // Never reached
.end();
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Failure);
CHECK(i == 2);
CHECK(tree->Tick(i) == Status::Failure);
CHECK(i == 4);
i = 0;
tree = Builder<int>()
.sequence()
.leaf([](int& i) { i += 1; return Status::Success; })
.leaf([](int& i) { i += 1; return Status::Success; })
.leaf([](int& i) { i += 1; return Status::Success; })
.end();
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 3);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 6);
}
SECTION("NamedSequence")
{
auto tree = Builder<int>()
.sequence("sequence")
.leaf([](int& i) { i += 1; return Status::Success; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Success; }) // Never reached
.end();
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Failure);
CHECK(i == 2);
}
SECTION("Empty")
{
auto tree = Builder<int>()
.sequence() // empty sequence is useless but valid
.end();
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 0);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 0);
}
SECTION("NamedEmpty")
{
auto tree = Builder<int>()
.sequence("sequence") // empty sequence is useless but valid
.end();
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 0);
}
SECTION("Selector")
{
auto tree = Builder<int>()
.selector()
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Success; })
.leaf([](int& i) { i += 1; return Status::Success; }) // Never reached
.end();
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 3);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 6);
i = 0;
tree = Builder<int>()
.selector()
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.end();
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Failure);
CHECK(i == 4);
CHECK(tree->Tick(i) == Status::Failure);
CHECK(i == 8);
}
SECTION("NamedSelector")
{
auto tree = Builder<int>()
.selector("selector")
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Success; })
.leaf([](int& i) { i += 1; return Status::Success; }) // Never reached
.end();
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 3);
}
SECTION("CustomSelector")
{
auto tree = Builder<int>()
.composite<CustomComposite<int>>()
.leaf([](int& i) { i += 1; return Status::Success; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Success; })
.end();
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 3);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 6);
}
SECTION("NamedCustomSelector")
{
auto tree = Builder<int>()
.composite<CustomComposite<int>>("custom composite")
.leaf([](int& i) { i += 1; return Status::Success; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Success; })
.end();
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 3);
}
}
TEST_CASE("Subtree")
{
auto subtree = Builder<int>("tree")
.sequence()
.leaf([](int& i) { i += 1; return Status::Success; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Success; }) // Never reached
.end();
auto tree = Builder<int>()
.selector()
.tree(subtree)
.tree(subtree)
.leaf([](int& i) { i += 1; return Status::Success; })
.end();
int i = 0;
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 5);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 10);
}
TEST_CASE("Exceptions")
{
int i = 0;
SECTION("Anonymous")
{
auto tree = Builder<int>()
.inverter().sequence()
.selector()
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.succeeder().leaf([](int& i) { i += 1; return Status::Success; })
.end()
.selector()
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.repeater(2).composite<CustomComposite<int>>()
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; throw std::runtime_error("Exception to catch"); return Status::Failure; })
.end()
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Success; })
.end()
.end();
CHECK_FALSE(tree == nullptr);
CHECK_THROWS(tree->Tick(i));
CHECK(i == 11);
try
{
tree->Tick(i);
}
catch (const std::exception& ex)
{
CHECK_THAT(ex.what(), Catch::Matchers::Equals(
std::string("In Inverter\n") +
"In Sequence while Ticking child 1\n" +
"In Selector while Ticking child 2\n" +
"In Repeater\n" +
"In CustomComposite while Ticking child 3\n" +
"Exception to catch")
);
}
}
SECTION("Named")
{
auto tree = Builder<int>("tree")
.inverter("inverter").sequence("sequence")
.selector("selector 0")
.leaf("leaf 00", [](int& i) { i += 1; return Status::Failure; })
.leaf("leaf 01", [](int& i) { i += 1; return Status::Failure; })
.leaf("leaf 02", [](int& i) { i += 1; return Status::Failure; })
.leaf("leaf 03", [](int& i) { i += 1; return Status::Failure; })
.succeeder("succeeder").leaf("leaf 04", [](int& i) { i += 1; return Status::Success; })
.end()
.selector("selector 1")
.leaf("leaf 10", [](int& i) { i += 1; return Status::Failure; })
.leaf("leaf 11", [](int& i) { i += 1; return Status::Failure; })
.repeater("repeater", 2).composite<CustomComposite<int>>("custom composite")
.leaf("leaf 20", [](int& i) { i += 1; return Status::Failure; })
.leaf("leaf 21", [](int& i) { i += 1; return Status::Failure; })
.leaf("leaf 22", [](int& i) { i += 1; return Status::Failure; })
.leaf("leaf 23", [](int& i) { i += 1; throw std::runtime_error("Exception to catch"); return Status::Failure; })
.end()
.leaf("leaf 30", [](int& i) { i += 1; return Status::Failure; })
.leaf("leaf 31", [](int& i) { i += 1; return Status::Success; })
.end()
.end();
CHECK_FALSE(tree == nullptr);
try
{
tree->Tick(i);
}
catch (const std::exception& ex)
{
CHECK_THAT(ex.what(), Catch::Matchers::Equals(
std::string("In tree \"tree\"\n") +
"In \"inverter\" (Inverter)\n" +
"In \"sequence\" (Sequence) while Ticking child 1\n" +
"In \"selector 1\" (Selector) while Ticking child 2\n" +
"In \"repeater\" (Repeater)\n" +
"In \"custom composite\" (CustomComposite) while Ticking child 3\n" +
"In leaf \"leaf 23\"\n" +
"Exception to catch")
);
}
}
}
TEST_CASE("Nested")
{
int i = 0;
SECTION("Composite in Composite")
{
auto tree = Builder<int>()
.sequence()
.selector()
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Success; })
.end()
.selector()
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.composite<CustomComposite<int>>()
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.end()
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Success; })
.end()
.end();
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 11);
}
SECTION("Composite in decorator")
{
auto tree = Builder<int>()
.inverter().selector()
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.inverter().composite<CustomComposite<int>>()
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.end()
.leaf([](int& i) { i += 1; return Status::Failure; })
.leaf([](int& i) { i += 1; return Status::Failure; })
.end();
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 8);
}
SECTION("Decorator in decorator")
{
auto tree = Builder<int>()
.inverter().decorator<CustomDecorator<int>>().repeater(3).leaf([](int& i) { i += 1; return Status::Success; });
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Success);
CHECK(i == 3);
}
SECTION("Decorator in Composite")
{
auto tree = Builder<int>()
.sequence()
.inverter().leaf([](int& i) { i += 1; return Status::Failure; })
.repeater(3).leaf([](int& i) { i += 1; return Status::Success; })
.succeeder().leaf([](int& i) { i += 1; return Status::Failure; })
.decorator<CustomDecorator<int>>().leaf([](int& i) { i += 1; return Status::Success; })
.end();
CHECK_FALSE(tree == nullptr);
CHECK(tree->Tick(i) == Status::Failure);
CHECK(i == 6);
}
}
struct CustomContext
{
void OnNodeStartTick()
{
num_start_tick += 1;
}
void OnNodeEndTick(const Status s)
{
num_end_tick += 1;
if (s == Status::Success)
{
num_success_tick += 1;
}
else if (s == Status::Failure)
{
num_failure_tick += 1;
}
}
void OnNodeTickChild(const size_t index)
{
num_child_tick += 1;
}
int num_start_tick = 0;
int num_end_tick = 0;
int num_success_tick = 0;
int num_failure_tick = 0;
int num_child_tick = 0;
};
TEST_CASE("NodeCallbacks")
{
auto tree = Builder<CustomContext>()
.sequence()
.selector()
.leaf([](CustomContext& c) { return Status::Failure; })
.leaf([](CustomContext& c) { return Status::Failure; })
.leaf([](CustomContext& c) { return Status::Success; })
.leaf([](CustomContext& c) { return Status::Failure; })
.end()
.inverter().leaf([](CustomContext& c) { return Status::Failure; })
.leaf([](CustomContext& c) { return Status::Success; })
.end();
CustomContext context;
const Status s = tree->Tick(context);
CHECK(s == Status::Success);
CHECK(context.num_start_tick == 9);
CHECK(context.num_end_tick == 9);
CHECK(context.num_success_tick == 6);
CHECK(context.num_failure_tick == 3);
CHECK(context.num_child_tick == 8);
}
+79
View File
@@ -0,0 +1,79 @@
#include <catch2/catch_test_macros.hpp>
#include <botcraft/AI/Blackboard.hpp>
using namespace Botcraft;
TEST_CASE("Blackboard Read/Write values")
{
Blackboard blackboard;
REQUIRE_NOTHROW(blackboard.Set("hello", std::string("world")));
REQUIRE_NOTHROW(blackboard.Get<std::string>("hello"));
REQUIRE(blackboard.Get<std::string>("hello") == "world");
REQUIRE_NOTHROW(blackboard.Set("hello", 3));
REQUIRE_THROWS(blackboard.Get<std::string>("hello"));
REQUIRE(blackboard.Get<int>("hello") == 3);
REQUIRE_NOTHROW(blackboard.Copy("hello", "world"));
REQUIRE(blackboard.Get<int>("world") == 3);
REQUIRE_THROWS(blackboard.Copy("hallo", "world"));
REQUIRE_NOTHROW(blackboard.Erase("world"));
REQUIRE_THROWS(blackboard.Get<int>("world"));
NotifyOnEndUseRef<int> wrapped_ref = blackboard.GetRef<int>("hello", 42);
int& ref = wrapped_ref.ref();
REQUIRE(ref == 3);
ref = 42;
REQUIRE(ref == 42);
REQUIRE(blackboard.Get<int>("hello") == 42);
REQUIRE_NOTHROW(blackboard.Reset());
REQUIRE_THROWS(blackboard.Get<int>("hello"));
REQUIRE_NOTHROW(blackboard.Reset({ { "hello", 42 } }));
REQUIRE(blackboard.Get<int>("hello") == 42);
}
class TestBlackboardObserver : public BlackboardObserver
{
public:
void OnReset() override { is_reset = true; }
void OnValueChanged(const std::string& key, const std::any& value) override { is_value_changed = true; }
void OnValueRemoved(const std::string& key) override { is_value_removed = true; }
bool is_reset = false;
bool is_value_changed = false;
bool is_value_removed = false;
};
TEST_CASE("Blackboard notifications")
{
Blackboard blackboard;
TestBlackboardObserver observer;
blackboard.Subscribe(&observer);
REQUIRE_FALSE(observer.is_reset);
REQUIRE_FALSE(observer.is_value_changed);
REQUIRE_FALSE(observer.is_value_removed);
blackboard.Set<std::string>("hello", "world");
REQUIRE(observer.is_value_changed);
observer.is_value_changed = false;
{
const NotifyOnEndUseRef<std::string> wrapped_ref = blackboard.GetRef<std::string>("hello");
std::string& ref = wrapped_ref.ref();
ref = "world!";
}
REQUIRE(blackboard.Get<std::string>("hello") == "world!");
REQUIRE(observer.is_value_changed);
blackboard.Erase("hello");
REQUIRE(observer.is_value_removed);
blackboard.Reset();
REQUIRE(observer.is_reset);
}
+178
View File
@@ -0,0 +1,178 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <botcraft/Game/World/Blockstate.hpp>
using namespace Botcraft;
TEST_CASE("Testing mining time calculation")
{
BlockstateProperties blockstate_properties;
SECTION("air")
{
blockstate_properties.hardness = -1.0f;
blockstate_properties.any_tool_harvest = false;
blockstate_properties.best_tools = {
};
Blockstate blockstate(blockstate_properties);
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::None, ToolMaterial::None), Catch::Matchers::WithinAbs(-1.0, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Axe, ToolMaterial::Wood), Catch::Matchers::WithinAbs(-1.0, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Shears, ToolMaterial::None), Catch::Matchers::WithinAbs(-1.0, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Sword, ToolMaterial::Diamond), Catch::Matchers::WithinAbs(-1.0, 0.04));
}
SECTION("water")
{
blockstate_properties.hardness = 100.0f;
blockstate_properties.any_tool_harvest = false;
blockstate_properties.water = true;
blockstate_properties.best_tools = {
};
Blockstate blockstate(blockstate_properties);
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::None, ToolMaterial::None), Catch::Matchers::WithinAbs(-1.0, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Axe, ToolMaterial::Wood), Catch::Matchers::WithinAbs(-1.0, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Shears, ToolMaterial::None), Catch::Matchers::WithinAbs(-1.0, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Sword, ToolMaterial::Diamond), Catch::Matchers::WithinAbs(-1.0, 0.04));
}
SECTION("obsidian")
{
blockstate_properties.hardness = 50.0f;
blockstate_properties.any_tool_harvest = false;
blockstate_properties.best_tools = {
BestTool {
ToolType::Pickaxe, //tool_type
ToolMaterial::Diamond, //tool_material
1.0f //multiplier
}
};
Blockstate blockstate(blockstate_properties);
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::None, ToolMaterial::None), Catch::Matchers::WithinAbs(250.0, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Axe, ToolMaterial::Wood), Catch::Matchers::WithinAbs(250.0, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Shears, ToolMaterial::None), Catch::Matchers::WithinAbs(250.0, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Shovel, ToolMaterial::Gold), Catch::Matchers::WithinAbs(250.0, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Sword, ToolMaterial::Diamond), Catch::Matchers::WithinAbs(250.0, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Pickaxe, ToolMaterial::Wood), Catch::Matchers::WithinAbs(125.0, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Pickaxe, ToolMaterial::Gold), Catch::Matchers::WithinAbs(20.85, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Pickaxe, ToolMaterial::Diamond), Catch::Matchers::WithinAbs(9.4, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Pickaxe, ToolMaterial::Diamond, 5), Catch::Matchers::WithinAbs(2.25, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Pickaxe, ToolMaterial::Diamond, 0, 2), Catch::Matchers::WithinAbs(6.7, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Pickaxe, ToolMaterial::Diamond, 0, 0, 0, false), Catch::Matchers::WithinAbs(46.9, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Pickaxe, ToolMaterial::Diamond, 0, 0, 0, true, true), Catch::Matchers::WithinAbs(46.9, 0.04));
}
SECTION("ender_chest")
{
BlockstateProperties blockstate_properties;
blockstate_properties.hardness = 22.5f;
blockstate_properties.any_tool_harvest = false;
blockstate_properties.best_tools = {
BestTool {
ToolType::Pickaxe, //tool_type
ToolMaterial::Wood, //tool_material
1.0f //multiplier
}
};
Blockstate blockstate(blockstate_properties);
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::None, ToolMaterial::None), Catch::Matchers::WithinAbs(112.5, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Axe, ToolMaterial::Wood), Catch::Matchers::WithinAbs(112.5, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Shears, ToolMaterial::None), Catch::Matchers::WithinAbs(112.5, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Shovel, ToolMaterial::Stone), Catch::Matchers::WithinAbs(112.5, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Sword, ToolMaterial::Iron), Catch::Matchers::WithinAbs(112.5, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Pickaxe, ToolMaterial::Wood), Catch::Matchers::WithinAbs(16.9, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Pickaxe, ToolMaterial::Gold), Catch::Matchers::WithinAbs(2.85, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Pickaxe, ToolMaterial::Diamond), Catch::Matchers::WithinAbs(4.25, 0.04));
}
SECTION("chest")
{
BlockstateProperties blockstate_properties;
blockstate_properties.hardness = 2.5f;
blockstate_properties.any_tool_harvest = true;
blockstate_properties.best_tools = {
BestTool {
ToolType::Axe, //tool_type
ToolMaterial::Wood, //tool_material
1.0f //multiplier
}
};
Blockstate blockstate(blockstate_properties);
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::None, ToolMaterial::None), Catch::Matchers::WithinAbs(3.75, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Axe, ToolMaterial::Wood), Catch::Matchers::WithinAbs(1.9, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Axe, ToolMaterial::Gold), Catch::Matchers::WithinAbs(0.35, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Shears, ToolMaterial::None), Catch::Matchers::WithinAbs(3.75, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Sword, ToolMaterial::Gold), Catch::Matchers::WithinAbs(3.75, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Shovel, ToolMaterial::Stone), Catch::Matchers::WithinAbs(3.75, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Pickaxe, ToolMaterial::Wood), Catch::Matchers::WithinAbs(3.75, 0.04));
}
SECTION("dirt")
{
BlockstateProperties blockstate_properties;
blockstate_properties.hardness = 0.5f;
blockstate_properties.any_tool_harvest = true;
blockstate_properties.best_tools = {
BestTool {
ToolType::Shovel, //tool_type
ToolMaterial::Wood, //tool_material
1.0f //multiplier
}
};
Blockstate blockstate(blockstate_properties);
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::None, ToolMaterial::None), Catch::Matchers::WithinAbs(0.75, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Axe, ToolMaterial::Wood), Catch::Matchers::WithinAbs(0.75, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Shears, ToolMaterial::None), Catch::Matchers::WithinAbs(0.75, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Sword, ToolMaterial::Gold), Catch::Matchers::WithinAbs(0.75, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Pickaxe, ToolMaterial::Wood), Catch::Matchers::WithinAbs(0.75, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Shovel, ToolMaterial::Wood), Catch::Matchers::WithinAbs(0.4, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Shovel, ToolMaterial::Gold), Catch::Matchers::WithinAbs(0.1, 0.04));
#if PROTOCOL_VERSION > 578 /* > 1.15.2 */
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Shovel, ToolMaterial::Netherite), Catch::Matchers::WithinAbs(0.1, 0.04));
#endif
}
SECTION("leaves")
{
BlockstateProperties blockstate_properties;
blockstate_properties.hardness = 0.2f;
blockstate_properties.any_tool_harvest = true;
blockstate_properties.best_tools = {
BestTool {
ToolType::Shears, //tool_type
ToolMaterial::None, //tool_material
15.0f //multiplier
},
BestTool {
ToolType::Sword, //tool_type
ToolMaterial::None, //tool_material
1.5f //multiplier
},
BestTool {
ToolType::Hoe, //tool_type
ToolMaterial::Wood, //tool_material
1.0f //multiplier
},
};
Blockstate blockstate(blockstate_properties);
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::None, ToolMaterial::None), Catch::Matchers::WithinAbs(0.3, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Shears, ToolMaterial::None), Catch::Matchers::WithinAbs(0.0, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Sword, ToolMaterial::Gold), Catch::Matchers::WithinAbs(0.2, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Sword, ToolMaterial::Diamond), Catch::Matchers::WithinAbs(0.2, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Hoe, ToolMaterial::Wood), Catch::Matchers::WithinAbs(0.15, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Hoe, ToolMaterial::Stone), Catch::Matchers::WithinAbs(0.1, 0.04));
REQUIRE_THAT(blockstate.GetMiningTimeSeconds(ToolType::Hoe, ToolMaterial::Diamond), Catch::Matchers::WithinAbs(0.0, 0.04));
}
}
+18
View File
@@ -0,0 +1,18 @@
#include <catch2/reporters/catch_reporter_event_listener.hpp>
#include <catch2/reporters/catch_reporter_registrars.hpp>
#include <botcraft/Utilities/Logger.hpp>
class testRunListener : public Catch::EventListenerBase
{
public:
using Catch::EventListenerBase::EventListenerBase;
void testRunStarting(Catch::TestRunInfo const&) override
{
// Set log level to warning before starting the tests
Botcraft::Logger::GetInstance().SetLogLevel(Botcraft::LogLevel::Warning);
}
};
CATCH_REGISTER_LISTENER(testRunListener)
+228
View File
@@ -0,0 +1,228 @@
#include <catch2/catch_test_macros.hpp>
#include <botcraft/Game/AssetsManager.hpp>
#include <botcraft/Game/World/World.hpp>
#include <botcraft/Game/World/Biome.hpp>
using namespace Botcraft;
TEST_CASE("Add/Remove chunks")
{
World world = World(false);
#if PROTOCOL_VERSION < 719 /* < 1.16 */
const Dimension dimension = Dimension::Overworld;
#else
const std::string dimension = "minecraft:overworld";
#endif
#if PROTOCOL_VERSION > 756 /* > 1.17.1 */
world.SetDimensionMinY(dimension, 0);
world.SetDimensionHeight(dimension, 256);
#endif
world.SetCurrentDimension(dimension);
REQUIRE(world.GetChunks()->size() == 0);
world.LoadChunk(0, 0, dimension);
REQUIRE(world.GetChunks()->size() == 1);
world.UnloadChunk(0, 0);
REQUIRE(world.GetChunks()->size() == 0);
world.LoadChunk(0, 0, dimension);
world.LoadChunk(0, 1, dimension);
REQUIRE(world.GetChunks()->size() == 2);
world.UnloadAllChunks();
REQUIRE(world.GetChunks()->size() == 0);
}
TEST_CASE("Set/Get blocks")
{
World world = World(false);
#if PROTOCOL_VERSION < 719 /* < 1.16 */
const Dimension dimension = Dimension::Overworld;
#else
const std::string dimension = "minecraft:overworld";
#endif
#if PROTOCOL_VERSION > 756 /* > 1.17.1 */
world.SetDimensionMinY(dimension, 0);
world.SetDimensionHeight(dimension, 256);
#endif
world.SetCurrentDimension(dimension);
#if PROTOCOL_VERSION < 347 /* < 1.13 */
const BlockstateId id = { 1,0 };
#else
const BlockstateId id = 1;
#endif
// Does nothing: chunk not loaded
world.SetBlock(Position(0, 0, 0), id);
REQUIRE(world.GetBlock(Position(0, 0, 0)) == nullptr);
world.LoadChunk(0, 0, dimension);
world.SetBlock(Position(0, 0, 0), id);
REQUIRE(world.GetBlock(Position(0, 0, 0)) != nullptr);
}
TEST_CASE("Set/Get biomes")
{
World world = World(false);
#if PROTOCOL_VERSION < 719 /* < 1.16 */
const Dimension dimension = Dimension::Overworld;
#else
const std::string dimension = "minecraft:overworld";
#endif
#if PROTOCOL_VERSION > 756 /* > 1.17.1 */
world.SetDimensionMinY(dimension, 0);
world.SetDimensionHeight(dimension, 256);
#endif
world.SetCurrentDimension(dimension);
// Does nothing: chunk not loaded
#if PROTOCOL_VERSION < 552 /* < 1.15 */
world.SetBiome(0, 0, 0);
#else
world.SetBiome(0, 0, 0, 0);
#endif
REQUIRE(world.GetBiome(Position(0, 0, 0)) == nullptr);
world.LoadChunk(0, 0, dimension);
#if PROTOCOL_VERSION < 552 /* < 1.15 */
world.SetBiome(0, 0, 0);
#else
world.SetBiome(0, 0, 0, 0);
#endif
REQUIRE(world.GetBiome(Position(0, 0, 0)) != nullptr);
REQUIRE(world.GetBiome(Position(0, 0, 0))->GetName() == AssetsManager::getInstance().GetBiome(0)->GetName());
}
#if USE_GUI
TEST_CASE("Neighbour chunk update")
{
World world = World(false);
#if PROTOCOL_VERSION < 719 /* < 1.16 */
const Dimension dimension = Dimension::Overworld;
#else
const std::string dimension = "minecraft:overworld";
#endif
#if PROTOCOL_VERSION > 756 /* > 1.17.1 */
world.SetDimensionMinY(dimension, 0);
world.SetDimensionHeight(dimension, 256);
#endif
world.SetCurrentDimension(dimension);
world.LoadChunk(0, 0, dimension);
world.LoadChunk(0, 1, dimension);
#if PROTOCOL_VERSION < 347 /* < 1.13 */
const BlockstateId id = { 1,0 };
#else
const BlockstateId id = 1;
#endif
world.SetBlock(Position(0, 0, CHUNK_WIDTH - 1), id);
{
auto world_terrain = world.GetChunks();
REQUIRE(world_terrain->at({ 0,0 }).GetBlock(Position(0, 0, CHUNK_WIDTH - 1)) != nullptr);
REQUIRE(world_terrain->at({ 0,0 }).GetBlock(Position(0, 0, CHUNK_WIDTH - 1))->GetId() == id);
REQUIRE(world_terrain->at({ 0,1 }).GetBlock(Position(0, 0, -1)) != nullptr);
REQUIRE(world_terrain->at({ 0,1 }).GetBlock(Position(0, 0, -1))->GetId() == id);
}
}
#endif
TEST_CASE("Shared world")
{
std::thread thread1 = std::thread([]() {});
std::thread thread2 = std::thread([]() {});
const std::thread::id thread_id1 = thread1.get_id();
const std::thread::id thread_id2 = thread2.get_id();
thread1.join();
thread2.join();
World world = World(true);
#if PROTOCOL_VERSION < 719 /* < 1.16 */
const Dimension dimension = Dimension::Overworld;
#else
const std::string dimension = "minecraft:overworld";
#endif
#if PROTOCOL_VERSION > 756 /* > 1.17.1 */
world.SetDimensionMinY(dimension, 0);
world.SetDimensionHeight(dimension, 256);
#endif
world.SetCurrentDimension(dimension);
REQUIRE(world.GetChunks()->size() == 0);
SECTION("Load same chunk")
{
world.LoadChunk(0, 0, dimension, thread_id1);
world.LoadChunk(0, 0, dimension, thread_id2);
REQUIRE(world.GetChunks()->size() == 1);
world.UnloadChunk(0, 0, thread_id1);
REQUIRE(world.GetChunks()->size() == 1);
world.UnloadChunk(0, 0, thread_id1);
REQUIRE(world.GetChunks()->size() == 1);
world.UnloadChunk(0, 0, thread_id2);
REQUIRE(world.GetChunks()->size() == 0);
}
SECTION("Load different chunks")
{
world.LoadChunk(0, 0, dimension, thread_id1);
world.LoadChunk(0, 1, dimension, thread_id2);
REQUIRE(world.GetChunks()->size() == 2);
world.UnloadAllChunks(thread_id1);
REQUIRE(world.GetChunks()->size() == 1);
world.UnloadAllChunks(thread_id1);
REQUIRE(world.GetChunks()->size() == 1);
world.UnloadAllChunks(thread_id2);
REQUIRE(world.GetChunks()->size() == 0);
}
}
TEST_CASE("Set/Get lights")
{
World world = World(false);
#if PROTOCOL_VERSION < 719 /* < 1.16 */
const Dimension dimension = Dimension::Overworld;
#else
const std::string dimension = "minecraft:overworld";
#endif
#if PROTOCOL_VERSION > 756 /* > 1.17.1 */
world.SetDimensionMinY(dimension, 0);
world.SetDimensionHeight(dimension, 256);
#endif
world.SetCurrentDimension(dimension);
// Does nothing: chunk not loaded
world.SetBlockLight(Position(0, 0, 0), 12);
CHECK(world.GetBlockLight(Position(0, 0, 0)) == 0);
world.SetSkyLight(Position(0, 0, 0), 6);
CHECK(world.GetSkyLight(Position(0, 0, 0)) == 0);
world.LoadChunk(0, 0, dimension);
world.SetBlockLight(Position(0, 0, 0), 12);
CHECK(world.GetBlockLight(Position(0, 0, 0)) == 12);
world.SetBlockLight(Position(1, 0, 0), 6);
CHECK(world.GetBlockLight(Position(0, 0, 0)) == 12);
CHECK(world.GetBlockLight(Position(1, 0, 0)) == 6);
world.SetSkyLight(Position(0, 0, 0), 12);
CHECK(world.GetSkyLight(Position(0, 0, 0)) == 12);
world.SetSkyLight(Position(1, 0, 0), 6);
CHECK(world.GetSkyLight(Position(0, 0, 0)) == 12);
CHECK(world.GetSkyLight(Position(1, 0, 0)) == 6);
}
@@ -0,0 +1,92 @@
project(botcraft_online_tests)
set(HDR_FILES
include/MinecraftServer.hpp
include/TestManager.hpp
include/Utils.hpp
)
set(SRC_FILES
src/main.cpp
src/MinecraftServer.cpp
src/TestManager.cpp
src/tests/base_tasks.cpp
src/tests/dig.cpp
src/tests/entities.cpp
src/tests/inventory.cpp
src/tests/pathfinding.cpp
src/tests/physics.cpp
src/tests/self-tests.cpp
src/tests/world.cpp
)
add_executable(${PROJECT_NAME} ${HDR_FILES} ${SRC_FILES})
set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 17)
target_link_libraries(${PROJECT_NAME} PRIVATE Catch2::Catch2)
target_link_libraries(${PROJECT_NAME} PRIVATE botcraft)
target_link_libraries(${PROJECT_NAME} PRIVATE subprocess)
target_include_directories(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
get_filename_component(BOTCRAFT_ROOT_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
target_compile_definitions(${PROJECT_NAME} PRIVATE BASE_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/src/tests")
if(BOTCRAFT_COMPRESSION)
target_compile_definitions(${PROJECT_NAME} PRIVATE USE_COMPRESSION=1)
endif(BOTCRAFT_COMPRESSION)
set_target_properties(${PROJECT_NAME} PROPERTIES FOLDER Tests)
# To have a nice files structure in Visual Studio
if(MSVC)
foreach(source IN LISTS HDR_FILES)
get_filename_component(source_path_header "${source}" PATH)
string(REPLACE "include" "Header Files" source_path_header "${source_path_header}")
string(REPLACE "/" "\\" source_path_msvc "${source_path_header}")
source_group("${source_path_msvc}" FILES "${source}")
endforeach()
foreach(source IN LISTS SRC_FILES)
get_filename_component(source_path "${source}" PATH)
string(REPLACE "src" "Source Files" source_path "${source_path}")
string(REPLACE "/" "\\" source_path_msvc "${source_path}")
source_group("${source_path_msvc}" FILES "${source}")
endforeach()
endif()
# Output the test executable next to the examples and library files
if(MSVC)
# To avoid having folder for each configuration when building with Visual
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_DEBUG "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_RELEASE "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_MINSIZEREL "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_DEBUG "${BOTCRAFT_OUTPUT_DIR}/lib")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${BOTCRAFT_OUTPUT_DIR}/lib")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO "${BOTCRAFT_OUTPUT_DIR}/lib")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_MINSIZEREL "${BOTCRAFT_OUTPUT_DIR}/lib")
set_property(TARGET ${PROJECT_NAME} PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/bin")
else()
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/lib")
endif(MSVC)
# Manual add_test instead of catch_discover_tests because otherwise each test is launched by a separate process
# i.e. a separate server instance. And we want everything to happen on the same server.
add_test(
NAME ${PROJECT_NAME}
COMMAND $<TARGET_FILE:${PROJECT_NAME}>
WORKING_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/bin"
)
# Copy runtime files to bin output folder
if(NOT IS_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/bin/test_server_files/runtime")
message(STATUS "Copying runtime online test files to bin folder...")
endif()
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/runtime" DESTINATION "${BOTCRAFT_OUTPUT_DIR}/bin/test_server_files")
@@ -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);
}
Binary file not shown.
@@ -0,0 +1,352 @@
#include "MinecraftServer.hpp"
#include <subprocess/subprocess.h>
#include <botcraft/Utilities/Logger.hpp>
#include <botcraft/Utilities/SleepUtilities.hpp>
#include <botcraft/Version.hpp>
#include <stdexcept>
#include <array>
#include <regex>
#include <fstream>
const std::filesystem::path server_relative_files_path = "../../test_server_files";
MinecraftServerOptions MinecraftServer::options;
MinecraftServer::MinecraftServer()
{
running = false;
const std::filesystem::path current_path = std::filesystem::current_path();
const std::filesystem::path exe_path = std::filesystem::path(options.argv0).is_absolute() ? std::filesystem::path(options.argv0) : (current_path / options.argv0);
server_path = exe_path.parent_path() / "test_servers" / game_version;
if (std::filesystem::exists(server_path))
{
std::filesystem::remove_all(server_path);
}
std::filesystem::create_directories(server_path);
server_path = std::filesystem::canonical(server_path);
InitServerFolder(server_path);
// Change current path to launch server in the dedicated folder
std::filesystem::current_path(server_path);
const std::string jar_path = (server_relative_files_path / "server_jars" / ("server_" + game_version + ".jar")).string();
const char* command_line[] = { "java", "-Xmx1024M", "-Xms1024M", "-jar", jar_path.c_str(), "nogui", NULL};
subprocess = std::make_unique<subprocess_s>();
int result = subprocess_create(command_line,
subprocess_option_combined_stdout_stderr |
subprocess_option_inherit_environment |
subprocess_option_enable_async |
subprocess_option_no_window |
subprocess_option_search_user_path,
subprocess.get()
);
// Restore current path to previous value
std::filesystem::current_path(current_path);
if (result != 0)
{
LOG_FATAL("Error starting wrapped server process");
throw std::runtime_error("Error starting wrapped server process");
}
running = true;
read_thread = std::thread(&MinecraftServer::InternalThreadRead, this);
}
MinecraftServer::~MinecraftServer()
{
// Ask the server to stop properly
SendLine("stop");
// Wait for the process to finish
if (subprocess_join(subprocess.get(), NULL) != 0)
{
LOG_FATAL("Error joining wrapped server process, killing it");
Kill();
}
running = false;
if (subprocess_destroy(subprocess.get()) != 0)
{
LOG_FATAL("Error destroying wrapped server process");
}
subprocess.reset();
if (read_thread.joinable())
{
read_thread.join();
}
}
MinecraftServer& MinecraftServer::GetInstance()
{
static MinecraftServer instance;
return instance;
}
void MinecraftServer::Initialize()
{
if (!running)
{
LOG_WARNING("Trying to initialize a server that is not running");
return;
}
// Make sure the server is running and ready
// (5 minutes should be enough for the server to start even on a potato)
WaitLine(".*: Done \\([\\d.,]+s\\)! For help, type \"help\".*", 300000);
// Set gamerules
InitServerGamerules();
// Set time to day
SendLine("time set day");
WaitLine(".*: Set the time to \\d+.*", 5000);
}
std::string MinecraftServer::WaitLine(const long long int timeout_ms)
{
return WaitLine("", timeout_ms)[0];
}
std::vector<std::string> MinecraftServer::WaitLine(const std::string& regex, const long long int timeout_ms)
{
if (!running)
{
return { "" };
}
std::chrono::steady_clock::time_point time_check = std::chrono::steady_clock::now();
const std::chrono::steady_clock::time_point start = time_check;
std::vector<std::string> output;
if (Botcraft::Utilities::WaitForCondition([&]()
{
std::unique_lock<std::mutex> lock(internal_read_mutex);
if (lines.size() > 0)
{
const std::string line = lines.front();
lines.pop();
if (regex.empty())
{
LOG_DEBUG("Line caught (no regex):\n" << line);
output = { line };
return true;
}
std::smatch match;
if (std::regex_match(line, match, std::regex(regex)))
{
output.resize(match.size());
for (size_t i = 0; i < match.size(); ++i)
{
output[i] = match[i].str();
}
LOG_DEBUG("Line caught using regex [" << regex << "]:\n" << line);
return true;
}
}
if (timeout_ms == 0)
{
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now - time_check).count() > 30)
{
time_check = now;
LOG_WARNING("Waiting for a server line matching [" << regex << "] since " << std::chrono::duration_cast<std::chrono::seconds>(now - start).count() << " seconds. Infinite loop?");
}
}
return false;
}, timeout_ms))
{
return output;
}
if (regex.empty())
{
LOG_FATAL("Timeout waiting for server to output a line");
throw std::runtime_error("Timeout waiting for server to output a line");
}
else
{
LOG_FATAL("Timeout waiting for server to output a line matching [" << regex << "]");
throw std::runtime_error("Timeout waiting for server to output a line matching [" + regex + "]");
}
}
void MinecraftServer::SendLine(const std::string& input)
{
if (!running)
{
return;
}
FILE* p_stdin = subprocess_stdin(subprocess.get());
LOG_DEBUG("Sending to server:\n" << input);
std::fputs(input.back() == '\n' ? input.c_str() : (input + '\n').c_str(), p_stdin);
std::fflush(p_stdin);
}
std::filesystem::path MinecraftServer::GetStructurePath() const
{
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
return server_path / "world" / "generated" / "minecraft" / "structures";
#else
return server_path / "world" / "structures";
#endif
}
void MinecraftServer::Kill()
{
int result = subprocess_terminate(subprocess.get());
if (result != 0)
{
LOG_FATAL("Error killing wrapped server process");
}
running = false;
}
void MinecraftServer::InternalThreadRead()
{
Botcraft::Logger::GetInstance().RegisterThread("read");
std::array<char, 256> buffer;
std::string line = "";
unsigned int bytes_read = 0;
while ((bytes_read = subprocess_read_stdout(subprocess.get(), buffer.data(), sizeof(buffer))))
{
line += std::string(buffer.data(), bytes_read);
// Remove windows \r stuff if any
line.erase(std::remove(line.begin(), line.end(), '\r'), line.end());
size_t newline_pos = 0;
while ((newline_pos = line.find('\n')) != std::string::npos)
{
std::unique_lock<std::mutex> lock(internal_read_mutex);
lines.push(line.substr(0, newline_pos));
LOG_DEBUG("[SERVER] - " << lines.back());
line = line.substr(newline_pos + 1);
}
}
}
void MinecraftServer::InitServerFolder(const std::filesystem::path& path)
{
// Bash script to relaunch the server manually
#if _WIN32
if (std::ofstream bash = std::ofstream(path / "start.cmd"))
#else
if (std::ofstream bash = std::ofstream(path / "start.sh"))
#endif
{
bash << "java -Xmx1024M -Xms1024M -jar " << server_relative_files_path.string() << "/server_jars/server_" << game_version << ".jar nogui\n";
}
else
{
LOG_FATAL("Unable to create start bash script");
throw std::runtime_error("Unable to create start bash script");
}
// Accept the EULA
if (std::ofstream eula = std::ofstream(path / "eula.txt"))
{
eula << "eula=true\n";
}
else
{
LOG_FATAL("Unable to create eula.txt");
throw std::runtime_error("Unable to create eula.txt");
}
// Setting the value of all non default properties
if (std::ofstream server_props = std::ofstream(path / "server.properties"))
{
#if PROTOCOL_VERSION < 477 /* < 1.14 */
server_props << "difficulty=3" << "\n";
#else
server_props << "difficulty=hard" << "\n";
#endif
server_props << "enable-command-block=true" << "\n";
#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
server_props << "enforce-secure-profile=false" << "\n";
#endif
// Everyone joins as creative and bots will be manually set to survival upon join
server_props << "force-gamemode=true" << "\n";
#if PROTOCOL_VERSION < 477 /* < 1.14 */
server_props << "gamemode=1" << "\n";
#else
server_props << "gamemode=creative" << "\n";
#endif
server_props << "generate-structures=false" << "\n";
// generator-settings should be there but it doesn't work on all
// supported versions see https://bugs.mojang.com/browse/MC-195468
// Set using custom level.dat file instead
server_props << "level-seed=botcraft" << "\n";
server_props << "level-type=flat" << "\n";
server_props << "motd=Botcraft test server" << "\n";
#if USE_COMPRESSION
server_props << "network-compression-threshold=256" << "\n";
#else
// This should technically never happen as we need
// compression support for protocolCraft to read
// structures NBT files
server_props << "network-compression-threshold=-1" << "\n";
#endif
server_props << "online-mode=false" << "\n";
server_props << "spawn-monsters=false" << "\n";
server_props << "spawn-protection=0" << "\n";
server_props << "view-distance=" << options.view_distance << "\n";
}
else
{
LOG_FATAL("Unable to create server.properties");
throw std::runtime_error("Unable to create server.properties");
}
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
std::filesystem::create_directories(path / "world" / "generated" / "minecraft");
#else
std::filesystem::create_directories(path / "world");
#endif
// Setup level.dat
std::filesystem::copy_file(path / server_relative_files_path / "runtime" / "level.dat", path / "world" / "level.dat");
// Setup structures files
std::filesystem::copy(
path / server_relative_files_path / "runtime" / "structures",
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
path / "world" / "generated" / "minecraft" / "structures",
#else
path / "world" / "structures",
#endif
std::filesystem::copy_options::recursive);
}
void MinecraftServer::SetGamerule(const std::string& gamerule, const std::string& value)
{
SendLine("gamerule " + gamerule + " " + value);
WaitLine(".*: Game ?rule " + gamerule + " (?:is now set to:|has been updated to) " + value + ".*", 5000);
}
void MinecraftServer::InitServerGamerules()
{
SetGamerule("announceAdvancements", "false");
#if PROTOCOL_VERSION > 485 /* > 1.14.2 */
SetGamerule("disableRaids", "true");
#endif
SetGamerule("doDaylightCycle", "false");
SetGamerule("doFireTick", "false");
#if PROTOCOL_VERSION > 498 /* > 1.14.4 */
SetGamerule("doInsomnia", "false");
#endif
SetGamerule("doMobSpawning", "false");
#if PROTOCOL_VERSION > 575 /* > 1.15.1 */
SetGamerule("doPatrolSpawning", "false");
SetGamerule("doTraderSpawning", "false");
#endif
SetGamerule("doWeatherCycle", "false");
#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
SetGamerule("doWardenSpawning", "false");
#endif
SetGamerule("mobGriefing", "false");
SetGamerule("randomTickSpeed", "0");
SetGamerule("spawnRadius", "0");
SetGamerule("spectatorsGenerateChunks", "true");
}
@@ -0,0 +1,709 @@
#include "TestManager.hpp"
#include "MinecraftServer.hpp"
#include <catch2/catch_test_case_info.hpp>
#include <protocolCraft/Types/NBT/NBT.hpp>
#include <botcraft/Network/NetworkManager.hpp>
#include <botcraft/Game/Entities/EntityManager.hpp>
#include <botcraft/Game/Entities/LocalPlayer.hpp>
#include <botcraft/Utilities/Logger.hpp>
#include <fstream>
#include <sstream>
#include <regex>
std::string ReplaceCharacters(const std::string& in, const std::vector<std::pair<char, std::string>>& replacements = { {'"', "\\\""}, {'\n', "\\n"} });
TestManager::TestManager()
{
current_offset = {spacing_x, 2, 2 * spacing_z };
current_test_index = 0;
bot_index = 0;
}
TestManager::~TestManager()
{
}
TestManager& TestManager::GetInstance()
{
static TestManager instance;
return instance;
}
const Botcraft::Position& TestManager::GetCurrentOffset() const
{
return current_offset;
}
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
void TestManager::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) const
#else
void TestManager::SetBlock(const std::string& name, const Botcraft::Position& pos, const int block_variant, const std::map<std::string, std::string>& metadata, const bool escape_metadata) const
#endif
{
MakeSureLoaded(pos);
std::stringstream command;
command
<< "setblock" << " "
<< pos.x << " "
<< pos.y << " "
<< pos.z << " "
<< ((name.size() > 10 && name.substr(0, 10) == "minecraft:") ? "" : "minecraft:") << name;
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
if (!blockstates.empty())
{
command << "[";
int index = 0;
for (const auto& [k, v] : blockstates)
{
command << k << "=";
if (v.find(' ') != std::string::npos ||
v.find(',') != std::string::npos ||
v.find(':') != std::string::npos)
{
// Make sure the whole string is between quotes and all internal quotes are escaped
command << "\"" << ReplaceCharacters(v) << "\"";
}
else
{
command << v;
}
command << (index == blockstates.size() - 1 ? "" : ",");
index += 1;
}
command << "]";
}
#else
command << " "
<< block_variant << " "
<< "replace" << " ";
#endif
if (!metadata.empty())
{
command << "{";
int index = 0;
for (const auto& [k, v] : metadata)
{
command << k << ":";
if (escape_metadata &&
(v.find(' ') != std::string::npos ||
v.find(',') != std::string::npos ||
v.find(':') != std::string::npos)
)
{
// Make sure the whole string is between quotes and all internal quotes are escaped
command << "\"" << ReplaceCharacters(v) << "\"";
}
else
{
command << v;
}
command << (index == metadata.size() - 1 ? "" : ",");
index += 1;
}
command << "}";
}
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
command << " replace";
#endif
MinecraftServer::GetInstance().SendLine(command.str());
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
MinecraftServer::GetInstance().WaitLine(
".*: Changed the block at "
+ std::to_string(pos.x) + ", "
+ std::to_string(pos.y) + ", "
+ std::to_string(pos.z) + ".*", 5000);
#else
MinecraftServer::GetInstance().WaitLine(".*: Block placed.*", 5000);
#endif
}
void TestManager::CreateBook(const Botcraft::Position& pos, const std::vector<std::string>& pages, const std::string& facing, const std::string& title, const std::string& author, const std::vector<std::string>& description)
{
int facing_id = 0;
if (facing == "south")
{
facing_id = 0;
}
else if (facing == "west")
{
facing_id = 1;
}
else if (facing == "north")
{
facing_id = 2;
}
else if (facing == "east")
{
facing_id = 3;
}
std::stringstream command;
command
#if PROTOCOL_VERSION < 477 /* < 1.14 */
<< "summon" << " "
<< "minecraft:item_frame" << " "
#else
<< "setblock" << " "
#endif
<< pos.x << " "
<< pos.y << " "
<< pos.z << " "
#if PROTOCOL_VERSION < 477 /* < 1.14 */
<< "{Facing:" << facing_id << ","
<< "Item:{"
#else
<< "minecraft:lectern"
<< "[facing=" << facing << ",has_book=true]"
<< "{"
<< "Book:{"
#endif
<< "id:\"written_book\"" << ","
<< "Count:1" << ","
<< "tag:{"
<< "pages:[";
for (size_t i = 0; i < pages.size(); ++i)
{
command
<< "\"{"
<< "\\\"text\\\"" << ":" << "\\\"" << ReplaceCharacters(pages[i], { {'\n', "\\\\n"}, {'"', "\\\\\\\""} }) << "\\\""
<< "}\"" << ((i < pages.size() - 1) ? "," : "");
}
command << "]";
if (!title.empty())
{
command << ","
<< "title" << ":" << "\"" << ReplaceCharacters(title) << "\"";
}
if (!author.empty())
{
command << ","
<< "author" << ":" << "\"" << ReplaceCharacters(author) << "\"";
}
if (!description.empty())
{
command << "," << "display" << ":"
<< "{Lore:[";
for (size_t i = 0; i < description.size(); ++i)
{
command
<< "\""
#if PROTOCOL_VERSION < 735 /* < 1.16 */
// Just a list of strings is working before 1.16(?)
<< ReplaceCharacters(description[i])
#else
// After, a JSON text element is required
<< "{" << "\\\"text\\\"" << ":" << "\\\"" << ReplaceCharacters(description[i], { { '\n', "\\\\n"}, { '"', "\\\\\\\"" } }) << "\\\"" << "}"
#endif
<< "\"" << ((i < description.size() - 1) ? "," : "");
}
command << "]}";
}
command
<< "}" // tag
<< "}" // Item
<< "}"; // Main
MinecraftServer::GetInstance().SendLine(command.str());
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */ && PROTOCOL_VERSION < 477 /* < 1.14 */
MinecraftServer::GetInstance().WaitLine(".*?: Summoned new Item Frame.*", 5000);
#elif PROTOCOL_VERSION == 340 /* 1.12.2 */
MinecraftServer::GetInstance().WaitLine(".*?: Object successfully summoned.*", 5000);
#else
MinecraftServer::GetInstance().WaitLine(
".*: Changed the block at "
+ std::to_string(pos.x) + ", "
+ std::to_string(pos.y) + ", "
+ std::to_string(pos.z) + ".*", 5000);
#endif
}
void TestManager::Teleport(const std::string& name, const Botcraft::Vector3<double>& pos, const float yaw, const float pitch) const
{
std::stringstream command;
command
<< "teleport" << " "
<< name << " "
<< pos.x << " "
<< pos.y << " "
<< pos.z << " "
<< yaw << " "
<< pitch;
MinecraftServer::GetInstance().SendLine(command.str());
MinecraftServer::GetInstance().WaitLine(".*?Teleported " + name + " to.*", 5000);
}
Botcraft::Position TestManager::GetStructureSize(const std::string& filename) const
{
std::string no_space_filename = ReplaceCharacters(filename, { {' ', "_"} });
// If there is a # in the file name, only use what's before (useful to have multiple tests sharing the same structure file)
size_t split_index = no_space_filename.find('#');
if (split_index != std::string::npos)
{
no_space_filename = no_space_filename.substr(0, split_index);
}
const std::filesystem::path filepath = MinecraftServer::GetInstance().GetStructurePath() / (no_space_filename + ".nbt");
if (!std::filesystem::exists(filepath))
{
return GetStructureSize("_default");
}
std::ifstream file(filepath.string(), std::ios::in | std::ios::binary);
file.unsetf(std::ios::skipws);
ProtocolCraft::NBT::Value nbt;
file >> nbt;
file.close();
// TODO: deal with recursive structures (structures containing structure blocks) ?
return nbt["size"].as_list_of<int>();
}
void TestManager::CreateTPSign(const Botcraft::Position& src, const Botcraft::Vector3<double>& dst, const std::vector<std::string>& texts, const std::string& facing, const TestSucess success) const
{
Botcraft::Position offset_target;
int block_rotation = 0;
if (facing == "north")
{
block_rotation = 0;
offset_target = dst + Botcraft::Position(0, 0, -1);
}
else if (facing == "south")
{
block_rotation = 8;
offset_target = dst + Botcraft::Position(0, 0, 1);
}
else if (facing == "east")
{
block_rotation = 12;
offset_target = dst + Botcraft::Position(-1, 0, 0);
}
else if (facing == "west")
{
block_rotation = 4;
offset_target = dst + Botcraft::Position(1, 0, 0);
}
std::string text_color;
switch (success)
{
case TestSucess::None:
text_color = "black";
break;
case TestSucess::Success:
text_color = "dark_green";
break;
case TestSucess::Failure:
text_color = "red";
break;
case TestSucess::ExpectedFailure:
text_color = "gold";
break;
}
#if PROTOCOL_VERSION < 763 /* < 1.20 */
std::map<std::string, std::string> lines;
#else
std::vector<std::string> lines;
lines.reserve(4);
#endif
for (size_t i = 0; i < std::min(texts.size(), static_cast<size_t>(4)); ++i)
{
std::stringstream line;
line
<< "{"
<< "\"text\":\"" << texts[i] << "\"" << ",";
if (i == 0)
{
line
<< "\"underlined\"" << ":" << "false" << ","
<< "\"color\"" << ":" << "\"" << "black" << "\"" << ","
<< "\"clickEvent\"" << ":" << "{"
<< "\"action\"" << ":" << "\"run_command\"" << ","
<< "\"value\"" << ":" << "\""
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
<< "teleport @s" << " " << offset_target.x << " " << offset_target.y << " " << offset_target.z
<< " " << "facing" << " " << dst.x << " " << dst.y << " " << dst.z
#else
<< "teleport @s" << " " << dst.x << " " << dst.y << " " << dst.z
#endif
<< "\""
<< "}";
}
else
{
line
<< "\"underlined\"" << ":" << "true" << ","
<< "\"color\"" << ":" << "\"" << text_color << "\"";
}
line << "}";
#if PROTOCOL_VERSION < 763 /* < 1.20 */
lines.insert({ "Text" + std::to_string(i + 1), line.str() });
#else
lines.push_back(line.str());
#endif
}
// There is a bug in version 1.14 (and prereleases) that requires all 4 lines
// to be specified or the server can't read the text. See: https://bugs.mojang.com/browse/MC-144316
#if PROTOCOL_VERSION > 459 /* > 1.13.2 */ && PROTOCOL_VERSION < 478 /* < 1.14.1 */
for (size_t i = texts.size(); i < 4ULL; ++i)
{
lines.insert({ "Text" + std::to_string(i + 1), "{\"text\":\"\"}" });
}
#endif
#if PROTOCOL_VERSION > 762 /* > 1.19.4 */ // In 1.20 texts are sent as a list with exactly 4 elements
std::stringstream text_content;
text_content << "{\"messages\":[";
for (size_t i = 0; i < 4ULL; ++i)
{
if (i < lines.size())
{
text_content << "\"" << ReplaceCharacters(lines[i]) << "\"";
}
else
{
text_content << "\"" << ReplaceCharacters("{\"text\":\"\"}") << "\"";
}
if (i != 3ULL)
{
text_content << ",";
}
}
text_content << "]}";
const std::string text_content_str = text_content.str();
#endif
SetBlock(
#if PROTOCOL_VERSION < 393 /* < 1.13 */
"standing_sign",
#elif PROTOCOL_VERSION < 477 /* < 1.14 */
"sign",
#else
"oak_sign",
#endif
src,
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
{
{ "rotation", std::to_string(block_rotation) }
},
#else
block_rotation,
#endif
#if PROTOCOL_VERSION < 763 /* < 1.20 */
lines
#else
{
{ "is_waxed", "true" },
{ "front_text", text_content_str },
{ "back_text", text_content_str },
},
false
#endif
);
}
void TestManager::LoadStructure(const std::string& filename, const Botcraft::Position& pos, const Botcraft::Position& load_offset) const
{
std::string no_space_filename = ReplaceCharacters(filename, { {' ', "_"} });
// If there is a # in the file name, only use what's before (useful to have multiple tests sharing the same structure file)
size_t split_index = no_space_filename.find('#');
if (split_index != std::string::npos)
{
no_space_filename = no_space_filename.substr(0, split_index);
}
const std::string& loaded = std::filesystem::exists(MinecraftServer::GetInstance().GetStructurePath() / (no_space_filename + ".nbt")) ?
no_space_filename :
"_default";
SetBlock(
"structure_block",
pos,
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
{},
#else
0,
#endif
{
{"mode", "LOAD"},
{"name", loaded},
{"posX", std::to_string(load_offset.x)},
{"posY", std::to_string(load_offset.y)},
{"posZ", std::to_string(load_offset.z)},
{"showboundingbox", "1"}
}
);
SetBlock("redstone_block", pos + Botcraft::Position(0, 1, 0));
}
void TestManager::MakeSureLoaded(const Botcraft::Position& pos) const
{
std::shared_ptr<Botcraft::World> world = chunk_loader->GetWorld();
if (chunk_loader->GetWorld()->IsLoaded(pos))
{
return;
}
Teleport(chunk_loader_name, pos);
// Wait for bot to load center and corner view_distance blocks
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
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 (!world->IsLoaded(Botcraft::Position(wait_loaded[i].first, pos.y, wait_loaded[i].second)))
{
return false;
}
}
return true;
}, 15000))
{
LOG_ERROR("Timeout waiting " << chunk_loader_name << " to load surroundings from " << chunk_loader->GetLocalPlayer()->GetPosition());
for (size_t i = 0; i < wait_loaded.size(); ++i)
{
const Botcraft::Position pos(wait_loaded[i].first, pos.y, wait_loaded[i].second);
LOG_ERROR(pos << " is" << (world->IsLoaded(pos) ? "" : " not") << " loaded");
}
std::stringstream loaded;
loaded << "\n";
for (const auto& c : *world->GetChunks())
{
loaded << c.first.first << "\t" << c.first.second << ";";
}
LOG_ERROR("Loaded chunks: " << loaded.str());
throw std::runtime_error("Timeout waiting " + chunk_loader_name + " to load surroundings");
}
}
void TestManager::SetGameMode(const std::string& name, const Botcraft::GameType gamemode) const
{
std::string gamemode_string;
switch (gamemode)
{
case Botcraft::GameType::Survival:
gamemode_string = "survival";
break;
case Botcraft::GameType::Creative:
gamemode_string = "creative";
break;
case Botcraft::GameType::Adventure:
gamemode_string = "adventure";
break;
case Botcraft::GameType::Spectator:
gamemode_string = "spectator";
break;
default:
return;
}
std::stringstream command;
command
<< "gamemode" << " "
<< gamemode_string << " "
<< name;
MinecraftServer::GetInstance().SendLine(command.str());
MinecraftServer::GetInstance().WaitLine(".*? Set " + name + "'s game mode to.*", 5000);
}
void TestManager::testRunStarting(Catch::TestRunInfo const& test_run_info)
{
// Make sure the server is running and ready before the first test run
MinecraftServer::GetInstance().Initialize();
// Retrieve header size
header_size = GetStructureSize("_header_running");
chunk_loader = GetBot(chunk_loader_name, Botcraft::GameType::Spectator);
}
void TestManager::testCaseStarting(Catch::TestCaseInfo const& test_info)
{
// Retrieve test structure size
current_test_size = GetStructureSize(test_info.name);
LoadStructure("_header_running", Botcraft::Position(current_offset.x, 0, spacing_z - header_size.z));
current_offset.z = 2 * spacing_z;
}
void TestManager::testCasePartialStarting(Catch::TestCaseInfo const& test_info, uint64_t part_number)
{
// Load header
current_header_position = current_offset - Botcraft::Position(0, 2, 0);
LoadStructure("_header_running", current_header_position);
current_offset.z += header_size.z;
// Load test structure
LoadStructure(test_info.name, current_offset + Botcraft::Position(0, -1, 0), Botcraft::Position(0, 1, 0));
current_test_case_failures.clear();
section_stack = { std::filesystem::path(test_info.lineInfo.file).stem().string() };
}
void TestManager::sectionStarting(Catch::SectionInfo const& section_info)
{
section_stack.push_back(section_info.name);
}
void TestManager::assertionEnded(Catch::AssertionStats const& assertion_stats)
{
if (!assertion_stats.assertionResult.succeeded())
{
const std::filesystem::path file_path = std::filesystem::relative(assertion_stats.assertionResult.getSourceInfo().file, BASE_SOURCE_DIR);
current_test_case_failures.push_back(
ReplaceCharacters(file_path.string(), {{'\\', "/"}}) + ":" +
std::to_string(assertion_stats.assertionResult.getSourceInfo().line) + "\n\n" +
assertion_stats.assertionResult.getExpressionInMacro() + "\n\n"
"with expansion:" + "\n" +
assertion_stats.assertionResult.getExpandedExpression()
);
}
}
void TestManager::testCasePartialEnded(Catch::TestCaseStats const& test_case_stats, uint64_t part_number)
{
const bool passed = test_case_stats.totals.assertions.allPassed();
// Replace header with proper test result
LoadStructure(passed ? "_header_success" : (test_case_stats.testInfo->okToFail() ? "_header_expected_fail" : "_header_fail"), current_header_position);
// Create TP sign for the partial that just ended
CreateTPSign(
Botcraft::Position(-2 * (current_test_index + 1), 2, -2 * part_number - 5),
Botcraft::Vector3(current_offset.x, 2, current_offset.z - 1),
section_stack, "north", passed ? TestSucess::Success : (test_case_stats.testInfo->okToFail() ? TestSucess::ExpectedFailure : TestSucess::Failure)
);
// Create back to spawn sign for the section that just ended
CreateTPSign(
Botcraft::Position(current_offset.x, 2, current_offset.z - 1),
Botcraft::Vector3(-2 * (current_test_index + 1), 2, -2 * static_cast<int>(part_number) - 5),
section_stack, "south", passed ? TestSucess::Success : (test_case_stats.testInfo->okToFail() ? TestSucess::ExpectedFailure : TestSucess::Failure)
);
if (!passed)
{
CreateBook(
Botcraft::Position(current_offset.x + 1, 2, current_offset.z - header_size.z),
current_test_case_failures,
"north",
test_case_stats.testInfo->name + "#" + std::to_string(part_number),
"Botcraft Test Framework",
section_stack
);
}
current_offset.z += current_test_size.z + spacing_z;
// Kill all items that could exist on the floor
MinecraftServer::GetInstance().SendLine("kill @e[type=item]");
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
// In 1.12.2 server sends one line per entity killed so we can't wait without knowing
// how many there were. Just assume the command worked
MinecraftServer::GetInstance().WaitLine(".*?: (?:Killed|No entity was found).*", 5000);
#endif
}
void TestManager::testCaseEnded(Catch::TestCaseStats const& test_case_stats)
{
const bool passed = test_case_stats.totals.assertions.allPassed();
LoadStructure(passed ? "_header_success" : (test_case_stats.testInfo->okToFail() ? "_header_expected_fail" : "_header_fail"), Botcraft::Position(current_offset.x, 0, spacing_z - header_size.z));
// Create Sign to TP to current test
CreateTPSign(
Botcraft::Position(-2 * (current_test_index + 1), 2, -2),
Botcraft::Vector3(current_offset.x, 2, spacing_z - 1),
{ std::filesystem::path(test_case_stats.testInfo->lineInfo.file).stem().string(), test_case_stats.testInfo->name },
"north", passed ? TestSucess::Success : (test_case_stats.testInfo->okToFail() ? TestSucess::ExpectedFailure : TestSucess::Failure)
);
// Create sign to TP to TP back to spawn
CreateTPSign(
Botcraft::Position(current_offset.x, 2, spacing_z - 1),
Botcraft::Vector3(-2 * (current_test_index + 1), 2, -2),
{ std::filesystem::path(test_case_stats.testInfo->lineInfo.file).stem().string(), test_case_stats.testInfo->name },
"south", passed ? TestSucess::Success : (test_case_stats.testInfo->okToFail() ? TestSucess::ExpectedFailure : TestSucess::Failure)
);
current_test_index += 1;
current_offset.x += std::max(current_test_size.x, header_size.x) + spacing_x;
}
void TestManager::testRunEnded(Catch::TestRunStats const& test_run_info)
{
if (chunk_loader)
{
chunk_loader->Disconnect();
}
}
void TestManagerListener::testRunStarting(Catch::TestRunInfo const& test_run_info)
{
TestManager::GetInstance().testRunStarting(test_run_info);
}
void TestManagerListener::testCaseStarting(Catch::TestCaseInfo const& test_info)
{
TestManager::GetInstance().testCaseStarting(test_info);
}
void TestManagerListener::testCasePartialStarting(Catch::TestCaseInfo const& test_info, uint64_t part_number)
{
TestManager::GetInstance().testCasePartialStarting(test_info, part_number);
}
void TestManagerListener::sectionStarting(Catch::SectionInfo const& section_info)
{
TestManager::GetInstance().sectionStarting(section_info);
}
void TestManagerListener::assertionEnded(Catch::AssertionStats const& assertion_stats)
{
TestManager::GetInstance().assertionEnded(assertion_stats);
}
void TestManagerListener::testCasePartialEnded(Catch::TestCaseStats const& test_case_stats, uint64_t part_number)
{
TestManager::GetInstance().testCasePartialEnded(test_case_stats, part_number);
}
void TestManagerListener::testCaseEnded(Catch::TestCaseStats const& test_case_stats)
{
TestManager::GetInstance().testCaseEnded(test_case_stats);
}
void TestManagerListener::testRunEnded(Catch::TestRunStats const& test_run_info)
{
TestManager::GetInstance().testRunEnded(test_run_info);
}
CATCH_REGISTER_LISTENER(TestManagerListener)
std::string ReplaceCharacters(const std::string& in, const std::vector<std::pair<char, std::string>>& replacements)
{
std::string output;
output.reserve(in.size());
for (size_t i = 0; i < in.size(); ++i)
{
bool found = false;
for (size_t j = 0; j < replacements.size(); ++j)
{
if (replacements[j].first == in[i])
{
output += replacements[j].second;
found = true;
break;
}
}
if (!found)
{
output += in[i];
}
}
return output;
}
@@ -0,0 +1,36 @@
#include <catch2/catch_session.hpp>
#include <botcraft/Utilities/Logger.hpp>
#include "MinecraftServer.hpp"
int main(int argc, char* argv[])
{
MinecraftServer::options.argv0 = argv[0];
Botcraft::Logger::GetInstance().RegisterThread("main");
Catch::Session session;
// Parse the command line
int ret = session.applyCommandLine(argc, argv);
if (ret != 0)
{
return ret;
}
// Adapt botcraft logger to catch loglevel
switch (session.config().verbosity())
{
case Catch::Verbosity::Quiet:
Botcraft::Logger::GetInstance().SetLogLevel(Botcraft::LogLevel::Fatal);
break;
case Catch::Verbosity::Normal:
Botcraft::Logger::GetInstance().SetLogLevel(Botcraft::LogLevel::Warning);
break;
case Catch::Verbosity::High:
Botcraft::Logger::GetInstance().SetLogLevel(Botcraft::LogLevel::Debug);
break;
}
return session.run();
}
@@ -0,0 +1,80 @@
#include "TestManager.hpp"
#include "MinecraftServer.hpp"
#include "Utils.hpp"
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <botcraft/AI/SimpleBehaviourClient.hpp>
#include <botcraft/AI/Tasks/BaseTasks.hpp>
#include <sstream>
TEST_CASE("say")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
bot->SyncAction(5000, Botcraft::Say, "Hello, world!");
const std::string& botname = bot->GetNetworkManager()->GetMyName();
REQUIRE_NOTHROW(MinecraftServer::GetInstance().WaitLine(".*: (?:\\[Not Secure\\] )?[[<]" + botname + "[>\\]] Hello, world!.*", 5000));
}
bool IsLitRedstoneLamp(const Botcraft::Blockstate* block)
{
if (block == nullptr)
{
return false;
}
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
return block->GetName() == "minecraft:redstone_lamp" && block->GetVariableValue("lit") == "true";
#else
return block->GetName() == "minecraft:lit_redstone_lamp";
#endif
}
TEST_CASE("interact")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
const Botcraft::Position lever = TestManager::GetInstance().GetCurrentOffset() + Botcraft::Position(1, 1, 1);
const Botcraft::Position lamp = lever - Botcraft::Position(0, 1, 0);
// Wait for at least one tick because otherwise for some versions the server sometimes
// does not register the new position (? not sure about what happens here)
Botcraft::Utilities::WaitForCondition([]() -> bool
{
return false;
}, 100);
bot->SyncAction(5000, Botcraft::InteractWithBlock, lever, Botcraft::PlayerDiggingFace::Up, true);
std::shared_ptr<Botcraft::World> world = bot->GetWorld();
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
const Botcraft::Blockstate* block = world->GetBlock(lamp);
return IsLitRedstoneLamp(block);
}, 5000));
}
TEST_CASE("get day time")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
auto day_time = GENERATE(1000, 5000, 10000, 15000, 20000, 23500);
// Sent time set command
MinecraftServer::GetInstance().SendLine("time set " + std::to_string(day_time));
MinecraftServer::GetInstance().WaitLine(".*: Set the time to " + std::to_string(day_time) + ".*", 5000);
Botcraft::Utilities::WaitForCondition([&]()
{
return bot->GetDayTime() == day_time;
}, 5000);
CHECK(bot->GetDayTime() == day_time);
// Reset the time to day
MinecraftServer::GetInstance().SendLine("time set day");
MinecraftServer::GetInstance().WaitLine(".*: Set the time to 1000.*", 5000);
}
@@ -0,0 +1,194 @@
#include "TestManager.hpp"
#include "MinecraftServer.hpp"
#include "Utils.hpp"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <botcraft/AI/SimpleBehaviourClient.hpp>
#include <botcraft/AI/Tasks/DigTask.hpp>
#include <botcraft/Game/Inventory/InventoryManager.hpp>
void TestDig(std::unique_ptr<Botcraft::SimpleBehaviourClient>& bot, const Botcraft::Position& pos, const double time_s)
{
// Wait to be on ground
Botcraft::Utilities::WaitForCondition([&]()
{
return bot->GetLocalPlayer()->GetOnGround();
}, 5000);
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
bot->SyncAction(30000, Botcraft::Dig, pos, true, Botcraft::PlayerDiggingFace::North);
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
// Check the block is now air
std::shared_ptr<Botcraft::World> world = bot->GetWorld();
CHECK(Botcraft::Utilities::WaitForCondition([&]()
{
const Botcraft::Blockstate* block = world->GetBlock(pos);
return block == nullptr || block->IsAir();
}, 5000));
const double elapsed_time = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() / 1000.0;
// We are rather large here to compensate lags from delays in tree swapping/executing
CHECK_THAT(elapsed_time, Catch::Matchers::WithinAbs(time_s, 0.2));
}
TEST_CASE("dig pickaxe")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(1,0,1));
const std::string& botname = bot->GetNetworkManager()->GetMyName();
const Botcraft::Position chest = TestManager::GetInstance().GetCurrentOffset() + Botcraft::Position(0, 0, 2);
const Botcraft::Position stone = chest + Botcraft::Position(1, 0, 0);
const Botcraft::Position iron = stone + Botcraft::Position(1, 0, 0);
SECTION("no haste")
{
SECTION("golden pick")
{
GiveItem(bot, "golden_pickaxe", "Golden Pickaxe");
TestDig(bot, chest, 3.75);
TestDig(bot, stone, 0.2);
TestDig(bot, iron, 2.1);
}
SECTION("diamond pick")
{
GiveItem(bot, "diamond_pickaxe", "Diamond Pickaxe");
TestDig(bot, chest, 3.75);
TestDig(bot, stone, 0.3);
TestDig(bot, iron, 0.95);
}
}
SECTION("haste")
{
// Haste 2 is given using amplifier 1
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
MinecraftServer::GetInstance().SendLine("effect give " + botname + " haste 99999 1");
#else
MinecraftServer::GetInstance().SendLine("effect " + botname + " haste 99999 1");
#endif
MinecraftServer::GetInstance().WaitLine(".*?: (?:Applied effect Haste|Given Haste \\(ID [0-9]+\\)(?: \\* [0-9]+)?) to " + botname + ".*", 5000);
CHECK(Botcraft::Utilities::WaitForCondition([&]() {
for (const Botcraft::EntityEffect& effect : bot->GetLocalPlayer()->GetEffects())
{
if (effect.type == Botcraft::EntityEffectType::Haste)
{
return true;
}
}
return false;
}, 5000));
SECTION("golden pick")
{
GiveItem(bot, "golden_pickaxe", "Golden Pickaxe");
TestDig(bot, chest, 2.7);
TestDig(bot, stone, 0.15);
TestDig(bot, iron, 1.5);
}
SECTION("diamond pick")
{
GiveItem(bot, "diamond_pickaxe", "Diamond Pickaxe");
TestDig(bot, chest, 2.7);
TestDig(bot, stone, 0.25);
TestDig(bot, iron, 0.7);
}
}
}
TEST_CASE("dig underwater")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(1, 0, 1));
const std::string& botname = bot->GetNetworkManager()->GetMyName();
const Botcraft::Position dirt = TestManager::GetInstance().GetCurrentOffset() + Botcraft::Position(0, 0, 2);
const Botcraft::Position stone = dirt + Botcraft::Position(1, 0, 0);
const Botcraft::Position iron = stone + Botcraft::Position(1, 0, 0);
SECTION("aqua affinity")
{
GiveItem(bot, "diamond_pickaxe", "Diamond Pickaxe");
#if PROTOCOL_VERSION < 393 /* < 1.13 */
MinecraftServer::GetInstance().SendLine("replaceitem entity " + botname + " slot.armor.head minecraft:diamond_helmet 1 0 {ench:[{id:" + std::to_string(static_cast<int>(Botcraft::EnchantmentID::AquaAffinity)) + ",lvl:1}]}");
#elif PROTOCOL_VERSION < 755 /* < 1.17 */
MinecraftServer::GetInstance().SendLine("replaceitem entity " + botname + " armor.head minecraft:diamond_helmet{Enchantments:[{id:aqua_affinity,lvl:1}]} 1");
#else
MinecraftServer::GetInstance().SendLine("item replace entity " + botname + " armor.head with minecraft:diamond_helmet{Enchantments:[{id:aqua_affinity,lvl:1}]} 1");
#endif
CHECK(Botcraft::Utilities::WaitForCondition([&]() -> bool
{
return !bot->GetInventoryManager()->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_HEAD_ARMOR).IsEmptySlot();
}, 5000));
TestDig(bot, dirt, 0.75);
TestDig(bot, stone, 0.3);
TestDig(bot, iron, 0.95);
}
SECTION("haste")
{
// Haste 2 is given using amplifier 1
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
MinecraftServer::GetInstance().SendLine("effect give " + botname + " haste 99999 1");
#else
MinecraftServer::GetInstance().SendLine("effect " + botname + " haste 99999 1");
#endif
MinecraftServer::GetInstance().WaitLine(".*?: (?:Applied effect Haste|Given Haste \\(ID [0-9]+\\)(?: \\* [0-9]+)?) to " + botname + ".*", 5000);
CHECK(Botcraft::Utilities::WaitForCondition([&]() {
for (const Botcraft::EntityEffect& effect : bot->GetLocalPlayer()->GetEffects())
{
if (effect.type == Botcraft::EntityEffectType::Haste)
{
return true;
}
}
return false;
}, 5000));
SECTION("golden pick")
{
GiveItem(bot, "golden_pickaxe", "Golden Pickaxe");
TestDig(bot, dirt, 2.7);
TestDig(bot, stone, 0.7);
TestDig(bot, iron, 7.45);
}
SECTION("diamond pick")
{
GiveItem(bot, "diamond_pickaxe", "Diamond Pickaxe");
TestDig(bot, dirt, 2.7);
TestDig(bot, stone, 1.05);
TestDig(bot, iron, 3.35);
}
}
}
TEST_CASE("dig shears")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(1, 0, 1));
const Botcraft::Position dirt = TestManager::GetInstance().GetCurrentOffset() + Botcraft::Position(0, 0, 2);
const Botcraft::Position leaves = dirt + Botcraft::Position(1, 0, 0);
const Botcraft::Position cobweb = leaves + Botcraft::Position(1, 0, 0);
SECTION("shears")
{
GiveItem(bot, "shears", "Shears");
TestDig(bot, dirt, 0.75);
TestDig(bot, leaves, 0.0);
TestDig(bot, cobweb, 0.4);
}
SECTION("sword")
{
GiveItem(bot, "iron_sword", "Iron Sword");
TestDig(bot, dirt, 0.75);
TestDig(bot, leaves, 0.2);
TestDig(bot, cobweb, 0.4);
}
}
@@ -0,0 +1,150 @@
#include "TestManager.hpp"
#include "MinecraftServer.hpp"
#include "Utils.hpp"
#include <botcraft/Game/Entities/EntityManager.hpp>
#include <botcraft/Game/Entities/entities/Entity.hpp>
#include <botcraft/Game/Entities/entities/monster/CreeperEntity.hpp>
#include <botcraft/Game/Inventory/InventoryManager.hpp>
#include <botcraft/AI/Tasks/EntitiesTasks.hpp>
#include <catch2/catch_test_macros.hpp>
#include <sstream>
TEST_CASE("summoning")
{
const std::vector<std::string> types = {
"cow",
"horse",
"villager",
"witch"
};
std::unique_ptr<Botcraft::ManagersClient> bot = SetupTestBot();
const Botcraft::Vector3<double> pos = Botcraft::Vector3<double>(1.5, 0, 1.5) + TestManager::GetInstance().GetCurrentOffset();
for (const auto& t : types)
{
SECTION(t)
{
std::stringstream command;
command
<< "summon" << " "
<< t << " "
<< pos.x << " "
<< pos.y << " "
<< pos.z << " "
<< "{NoAI:1b,PersistenceRequired:1b}";
MinecraftServer::GetInstance().SendLine(command.str());
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
MinecraftServer::GetInstance().WaitLine(".*?: Summoned new.*", 5000);
#else
MinecraftServer::GetInstance().WaitLine(".*?: Object successfully summoned.*", 5000);
#endif
std::shared_ptr<Botcraft::EntityManager> entity_manager = bot->GetEntityManager();
std::shared_ptr<Botcraft::Entity> entity;
// Wait for the entity to be registered on bot side
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
auto entities = entity_manager->GetEntities();
for (const auto& [k, v] : *entities)
{
if (v->GetPosition().SqrDist(pos) < 0.2)
{
entity = v;
return true;
}
}
return false;
}, 5000));
REQUIRE(entity != nullptr);
CHECK(entity->GetName() == t);
}
}
}
TEST_CASE("entity interact")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
const std::string& botname = bot->GetNetworkManager()->GetMyName();
const Botcraft::Vector3<double> pos = Botcraft::Vector3<double>(1.5, 0, 1.5) + TestManager::GetInstance().GetCurrentOffset();
// Give flint and steel to bot
MinecraftServer::GetInstance().SendLine("give " + botname + " flint_and_steel");
MinecraftServer::GetInstance().WaitLine(".*?: (?:Given|Gave 1) \\[Flint and Steel\\](?: \\* 1)? to " + botname + ".*", 5000);
Botcraft::Utilities::WaitForCondition([&]()
{
return !bot->GetInventoryManager()->GetHotbarSelected().IsEmptySlot();
}, 5000);
// Wait for the entity to be registered on bot side
std::shared_ptr<Botcraft::EntityManager> entity_manager = bot->GetEntityManager();
std::shared_ptr<Botcraft::Entity> entity;
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
auto entities = entity_manager->GetEntities();
for (const auto& [k, v] : *entities)
{
if (v->GetPosition().SqrDist(pos) < 0.2)
{
entity = v;
return true;
}
}
return false;
}, 5000));
REQUIRE(entity != nullptr);
CHECK(entity->GetName() == "creeper");
bot->SyncAction(5000, Botcraft::InteractEntity, entity->GetEntityID(), Botcraft::Hand::Main, true);
// Wait for the creeper to be ignited
std::shared_ptr<Botcraft::CreeperEntity> creeper = std::dynamic_pointer_cast<Botcraft::CreeperEntity>(entity);
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
return creeper->GetDataIsIgnited();
}, 5000));
// Wait for the creeper to explode
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
return entity_manager->GetEntity(creeper->GetEntityID()) == nullptr;
}, 5000));
}
TEST_CASE("player names")
{
// Setup two bots and check their names are present in the "tab list"
std::unique_ptr<Botcraft::ManagersClient> bot = SetupTestBot();
std::shared_ptr<Botcraft::EntityManager> entity_manager = bot->GetEntityManager();
CHECK(bot->GetNetworkManager()->GetMyName() == bot->GetPlayerName(entity_manager->GetLocalPlayer()->GetUUID()));
const Botcraft::Vector3<double> bot_pos = entity_manager->GetLocalPlayer()->GetPosition();
std::unique_ptr<Botcraft::ManagersClient> bot2 = SetupTestBot();
std::shared_ptr<Botcraft::Entity> bot2_entity;
// Find bot2 in bot entities
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
for (const auto& [id, e] : *entity_manager->GetEntities())
{
if (id == bot2->GetLocalPlayer()->GetEntityID())
{
bot2_entity = e;
return true;
}
}
return false;
}, 5000));
// Check bot sees bot2 with it's name
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
return bot->GetPlayerName(bot2_entity->GetUUID()) == bot2->GetNetworkManager()->GetMyName();
}, 5000));
bot2->Disconnect();
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
return bot->GetPlayerName(bot2_entity->GetUUID()) == "";
}, 5000));
}
@@ -0,0 +1,387 @@
#include "TestManager.hpp"
#include "MinecraftServer.hpp"
#include "Utils.hpp"
#include <catch2/catch_test_macros.hpp>
#include <botcraft/AI/Tasks/InventoryTasks.hpp>
#include <botcraft/AI/Tasks/EntitiesTasks.hpp>
#include <botcraft/Game/Inventory/InventoryManager.hpp>
#include <botcraft/Game/World/World.hpp>
#include <botcraft/Game/Entities/EntityManager.hpp>
#include <botcraft/Game/Entities/entities/Entity.hpp>
#include <botcraft/Game/Entities/entities/item/ItemEntity.hpp>
#include <botcraft/Game/Entities/entities/npc/VillagerEntity.hpp>
#include <array>
std::string GetItemName(const ProtocolCraft::Slot& slot)
{
if (slot.IsEmptySlot())
{
return "";
}
return Botcraft::AssetsManager::getInstance().GetItem(slot.GetItemID())->GetName();
}
TEST_CASE("receive item")
{
std::unique_ptr<Botcraft::ManagersClient> bot = SetupTestBot();
CHECK(GiveItem(bot, "minecraft:stick", "Stick", 1));
CHECK(GiveItem(bot, "minecraft:stick", "Stick", 1));
const std::shared_ptr<Botcraft::InventoryManager> inventory_manager = bot->GetInventoryManager();
REQUIRE_FALSE(inventory_manager->GetHotbarSelected().IsEmptySlot());
}
TEST_CASE("swap slots")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
CHECK(GiveItem(bot, "minecraft:stick", "Stick", 5));
CHECK(GiveItem(bot, "minecraft:diamond_pickaxe", "Diamond Pickaxe", 1));
bot->SyncAction(5000, Botcraft::SwapItemsInContainer, Botcraft::Window::PLAYER_INVENTORY_INDEX, Botcraft::Window::INVENTORY_HOTBAR_START, Botcraft::Window::INVENTORY_HOTBAR_START + 1);
const std::shared_ptr<Botcraft::InventoryManager> inventory_manager = bot->GetInventoryManager();
CHECK(GetItemName(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_HOTBAR_START)) == "minecraft:diamond_pickaxe");
CHECK(GetItemName(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_HOTBAR_START + 1)) == "minecraft:stick");
CHECK(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_HOTBAR_START + 1).GetItemCount() == 5);
}
TEST_CASE("drop items")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
CHECK(GiveItem(bot, "minecraft:stick", "Stick", 5));
const std::shared_ptr<Botcraft::InventoryManager> inventory_manager = bot->GetInventoryManager();
const std::shared_ptr<Botcraft::EntityManager> entity_manager = bot->GetEntityManager();
const Botcraft::Vector3<double> position = Botcraft::Vector3<double>(0.5, 0.0, 0.5) + TestManager::GetInstance().GetCurrentOffset();
SECTION("drop 1")
{
// Drop 1 stick
bot->SyncAction(5000, Botcraft::DropItemsFromContainer, Botcraft::Window::PLAYER_INVENTORY_INDEX, Botcraft::Window::INVENTORY_HOTBAR_START, 4);
// We still have 4 sticks
CHECK(GetItemName(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_HOTBAR_START)) == "minecraft:stick");
CHECK(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_HOTBAR_START).GetItemCount() == 4);
// There is a stick entity on the floor
CHECK(Botcraft::Utilities::WaitForCondition([&]()
{
auto entities = entity_manager->GetEntities();
for (const auto& [id, e] : *entities)
{
// If this is an item entity and closer than 3.5 blocks
if (e->GetType() == Botcraft::ItemEntity::GetClassType() &&
e->GetPosition().SqrDist(position) < 12.25)
{
std::shared_ptr<Botcraft::ItemEntity> floor_items = std::dynamic_pointer_cast<Botcraft::ItemEntity>(e);
return
GetItemName(floor_items->GetDataItem()) == "minecraft:stick" &&
floor_items->GetDataItem().GetItemCount() == 1;
}
}
return false;
}, 5000));
}
SECTION("drop all")
{
// Drop all sticks
bot->SyncAction(5000, Botcraft::DropItemsFromContainer, Botcraft::Window::PLAYER_INVENTORY_INDEX, Botcraft::Window::INVENTORY_HOTBAR_START, 0);
// We have nothing now
CHECK(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_HOTBAR_START).IsEmptySlot());
// There is a stick entity on the floor
CHECK(Botcraft::Utilities::WaitForCondition([&]()
{
auto entities = entity_manager->GetEntities();
for (const auto& [id, e] : *entities)
{
// If this is an item entity and closer than 3.5 blocks
if (e->GetType() == Botcraft::ItemEntity::GetClassType() &&
e->GetPosition().SqrDist(position) < 12.25)
{
std::shared_ptr<Botcraft::ItemEntity> floor_items = std::dynamic_pointer_cast<Botcraft::ItemEntity>(e);
return
GetItemName(floor_items->GetDataItem()) == "minecraft:stick" &&
floor_items->GetDataItem().GetItemCount() == 5;
}
}
return false;
}, 5000));
}
}
TEST_CASE("put one item")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
const std::shared_ptr<Botcraft::InventoryManager> inventory_manager = bot->GetInventoryManager();
CHECK(GiveItem(bot, "minecraft:stick", "Stick", 5));
for (int i = 1; i < 5; ++i)
{
bot->SyncAction(5000, Botcraft::PutOneItemInContainerSlot, Botcraft::Window::PLAYER_INVENTORY_INDEX, Botcraft::Window::INVENTORY_HOTBAR_START, Botcraft::Window::INVENTORY_HOTBAR_START + i);
}
for (int i = 0; i < 5; ++i)
{
CHECK(GetItemName(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_HOTBAR_START + i)) == "minecraft:stick");
CHECK(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_HOTBAR_START + i).GetItemCount() == 1);
}
}
TEST_CASE("set in hand")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
CHECK(GiveItem(bot, "minecraft:stick", "Stick", 1));
CHECK(GiveItem(bot, "minecraft:diamond_pickaxe", "Diamond Pickaxe", 1));
bot->SyncAction(5000, Botcraft::SetItemInHand, "minecraft:diamond_pickaxe", Botcraft::Hand::Right);
const std::shared_ptr<Botcraft::InventoryManager> inventory_manager = bot->GetInventoryManager();
{
REQUIRE(GetItemName(inventory_manager->GetHotbarSelected()) == "minecraft:diamond_pickaxe");
}
}
TEST_CASE("place block")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
const std::shared_ptr<Botcraft::World> world = bot->GetWorld();
CHECK(GiveItem(bot, "minecraft:diamond_block", "Block of Diamond", 1));
Botcraft::Position pos;
SECTION("no mid air")
{
pos = TestManager::GetInstance().GetCurrentOffset() + Botcraft::Position(1, 0, 1);
bot->SyncAction(5000, Botcraft::PlaceBlock, "minecraft:diamond_block", pos, Botcraft::PlayerDiggingFace::Up, true, false);
}
SECTION("mid air")
{
pos = TestManager::GetInstance().GetCurrentOffset() + Botcraft::Position(1, 1, 1);
bot->SyncAction(5000, Botcraft::PlaceBlock, "minecraft:diamond_block", pos, Botcraft::PlayerDiggingFace::Up, true, true);
}
SECTION("automatic face detection")
{
pos = TestManager::GetInstance().GetCurrentOffset() + Botcraft::Position(1, 0, 1);
bot->SyncAction(5000, Botcraft::PlaceBlock, "minecraft:diamond_block", pos, std::nullopt, true, false);
}
const Botcraft::Blockstate* block = world->GetBlock(pos);
REQUIRE(block != nullptr);
REQUIRE(block->GetName() == "minecraft:diamond_block");
}
TEST_CASE("eat")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
CHECK(GiveItem(bot, "minecraft:golden_apple", "Golden Apple", 1));
bot->SyncAction(5000, Botcraft::Eat, "minecraft:golden_apple", true);
const std::shared_ptr<Botcraft::InventoryManager> inventory_manager = bot->GetInventoryManager();
// Check we don't have a golden apple in main or off hand
REQUIRE(inventory_manager->GetHotbarSelected().IsEmptySlot());
REQUIRE(inventory_manager->GetOffHand().IsEmptySlot());
}
TEST_CASE("container")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
const std::shared_ptr<Botcraft::InventoryManager> inventory_manager = bot->GetInventoryManager();
const Botcraft::Position chest = TestManager::GetInstance().GetCurrentOffset() + Botcraft::Position(1, 0, 1);
CHECK(GiveItem(bot, "minecraft:stick", "Stick", 5));
bot->SyncAction(5000, Botcraft::OpenContainer, chest);
short container_id = inventory_manager->GetFirstOpenedWindowId();
std::shared_ptr<Botcraft::Window> container = inventory_manager->GetWindow(container_id);
REQUIRE(container_id != -1);
SECTION("put 1")
{
bot->SyncAction(5000, Botcraft::PutOneItemInContainerSlot, container_id, container->GetFirstPlayerInventorySlot() + Botcraft::Window::INVENTORY_HOTBAR_START - Botcraft::Window::INVENTORY_STORAGE_START, 0);
bot->SyncAction(5000, Botcraft::CloseContainer, container_id);
#if PROTOCOL_VERSION < 755 /* < 1.17 */
// If <1.17 we need to wait for the server to send the
// updated player inventory content after closing the container
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
if (GetItemName(inventory_manager->GetHotbarSelected()) == "minecraft:stick" &&
inventory_manager->GetHotbarSelected().GetItemCount() == 4)
{
return true;
}
return false;
}, 5000));
#else
REQUIRE(GetItemName(inventory_manager->GetHotbarSelected()) == "minecraft:stick");
REQUIRE(inventory_manager->GetHotbarSelected().GetItemCount() == 4);
#endif
container_id = -1;
bot->SyncAction(5000, Botcraft::OpenContainer, chest);
container_id = inventory_manager->GetFirstOpenedWindowId();
container = inventory_manager->GetWindow(container_id);
REQUIRE(container_id != -1);
REQUIRE(GetItemName(container->GetSlot(0)) == "minecraft:stick");
REQUIRE(container->GetSlot(0).GetItemCount() == 1);
}
SECTION("put all")
{
bot->SyncAction(5000, Botcraft::SwapItemsInContainer, container_id, container->GetFirstPlayerInventorySlot() + Botcraft::Window::INVENTORY_HOTBAR_START - Botcraft::Window::INVENTORY_STORAGE_START, 0);
bot->SyncAction(5000, Botcraft::CloseContainer, container_id);
#if PROTOCOL_VERSION < 755 /* < 1.17 */
// If <1.17 we need to wait for the server to send the
// updated player inventory content after closing the container
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
if (inventory_manager->GetHotbarSelected().IsEmptySlot())
{
return true;
}
return false;
}, 5000));
#else
REQUIRE(inventory_manager->GetHotbarSelected().IsEmptySlot());
#endif
container_id = -1;
bot->SyncAction(5000, Botcraft::OpenContainer, chest);
container_id = inventory_manager->GetFirstOpenedWindowId();
container = inventory_manager->GetWindow(container_id);
REQUIRE(container_id != -1);
REQUIRE(GetItemName(container->GetSlot(0)) == "minecraft:stick");
REQUIRE(container->GetSlot(0).GetItemCount() == 5);
}
bot->SyncAction(5000, Botcraft::CloseContainer, container_id);
REQUIRE(inventory_manager->GetFirstOpenedWindowId() == -1);
}
#if PROTOCOL_VERSION > 451 /* > 1.13.2 */
TEST_CASE("trade")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
const std::shared_ptr<Botcraft::InventoryManager> inventory_manager = bot->GetInventoryManager();
const std::shared_ptr<Botcraft::EntityManager> entity_manager = bot->GetEntityManager();
const Botcraft::Vector3<double> pos = Botcraft::Vector3<double>(1.5, 0, 1.5) + TestManager::GetInstance().GetCurrentOffset();
std::shared_ptr<Botcraft::Entity> entity;
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
auto entities = entity_manager->GetEntities();
for (const auto& [k, v] : *entities)
{
if (v->GetPosition().SqrDist(pos) < 0.2 &&
v->GetType() == Botcraft::VillagerEntity::GetClassType())
{
entity = v;
return true;
}
}
return false;
}, 5000));
REQUIRE(entity != nullptr);
REQUIRE(GiveItem(bot, "minecraft:stick", "Stick", 1));
bot->SyncAction(5000, Botcraft::InteractEntity, entity->GetEntityID(), Botcraft::Hand::Right, true);
bot->SyncAction(5000, Botcraft::TradeName, "minecraft:stick", false, -1);
bot->SyncAction(5000, Botcraft::TradeName, "minecraft:enchanted_book", true, -1);
bot->SyncAction(5000, Botcraft::CloseContainer, -1);
// Wait for the player inventory to update
CHECK(Botcraft::Utilities::WaitForCondition([&]()
{
return !inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_STORAGE_START).IsEmptySlot();
}, 5000));
bot->SyncAction(5000, Botcraft::SetItemInHand, "minecraft:enchanted_book", Botcraft::Hand::Right);
REQUIRE(GetItemName(inventory_manager->GetHotbarSelected()) == "minecraft:enchanted_book");
REQUIRE(inventory_manager->GetHotbarSelected().GetItemCount() == 1);
}
#endif
TEST_CASE("craft")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
const std::shared_ptr<Botcraft::InventoryManager> inventory_manager = bot->GetInventoryManager();
const Botcraft::Position table = TestManager::GetInstance().GetCurrentOffset() + Botcraft::Position(1, 0, 1);
CHECK(GiveItem(bot, "minecraft:diamond_block", "Block of Diamond", 1));
const std::array<std::string, 3> empty = { "", "", "" };
const std::array<std::array<std::string, 3>, 3> decraft_recipe = {
empty,
empty,
{"minecraft:diamond_block", "", ""}
};
bot->SyncAction(5000, Botcraft::CraftNamed, decraft_recipe, true);
REQUIRE(GetItemName(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_STORAGE_START)) == "minecraft:diamond");
REQUIRE(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_STORAGE_START).GetItemCount() == 9);
bot->SyncAction(5000, Botcraft::OpenContainer, table);
short container_id = inventory_manager->GetFirstOpenedWindowId();
std::shared_ptr<Botcraft::Window> container = inventory_manager->GetWindow(container_id);
REQUIRE(container_id != -1);
const std::array<std::string, 3> diamond_line = { "minecraft:diamond", "minecraft:diamond", "minecraft:diamond" };
const std::array<std::array<std::string, 3>, 3> craft_recipe = {
diamond_line,
diamond_line,
diamond_line
};
bot->SyncAction(5000, Botcraft::CraftNamed, craft_recipe, false);
bot->SyncAction(5000, Botcraft::CloseContainer, -1);
#if PROTOCOL_VERSION < 755 /* < 1.17 */
// If <1.17 we need to wait for the server to send the
// updated player inventory content after closing the container
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
if (GetItemName(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_STORAGE_START)) == "minecraft:diamond_block" &&
inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_STORAGE_START).GetItemCount() == 1)
{
return true;
}
return false;
}, 5000));
#else
REQUIRE(GetItemName(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_STORAGE_START)) == "minecraft:diamond_block");
REQUIRE(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_STORAGE_START).GetItemCount() == 1);
#endif
}
TEST_CASE("sort inventory")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>();
const std::shared_ptr<Botcraft::InventoryManager> inventory_manager = bot->GetInventoryManager();
CHECK(GiveItem(bot, "minecraft:stick", "Stick", 5));
for (int i = 1; i < 5; ++i)
{
bot->SyncAction(5000, Botcraft::PutOneItemInContainerSlot, Botcraft::Window::PLAYER_INVENTORY_INDEX, Botcraft::Window::INVENTORY_HOTBAR_START, Botcraft::Window::INVENTORY_HOTBAR_START + i);
}
bot->SyncAction(5000, Botcraft::SortInventory);
CHECK(GetItemName(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_HOTBAR_START)) == "minecraft:stick");
CHECK(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_HOTBAR_START).GetItemCount() == 5);
for (int i = 1; i < 5; ++i)
{
CHECK(inventory_manager->GetPlayerInventory()->GetSlot(Botcraft::Window::INVENTORY_HOTBAR_START + i).IsEmptySlot());
}
}
@@ -0,0 +1,323 @@
#include "TestManager.hpp"
#include "Utils.hpp"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <botcraft/AI/SimpleBehaviourClient.hpp>
#include <botcraft/AI/Tasks/PathfindingTask.hpp>
bool SameBlock(const Botcraft::Vector3<double>& p1, const Botcraft::Vector3<double>& p2)
{
return std::floor(p1.x) == std::floor(p2.x) &&
std::floor(p1.y) == std::floor(p2.y) &&
std::floor(p1.z) == std::floor(p2.z);
}
TEST_CASE("simple pathfinding")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(2.5, 2.01, 2.5));
std::vector<std::pair<std::string, Botcraft::Vector3<double>>> directions = {
{"south", Botcraft::Position(0, 0, 2)},
{"north", Botcraft::Position(0, 0, -2)},
{"east", Botcraft::Position(2, 0, 0)},
{"west", Botcraft::Position(-2, 0, 0)},
};
std::shared_ptr<Botcraft::LocalPlayer> local_player = bot->GetLocalPlayer();
const Botcraft::Vector3<double> init_position = local_player->GetPosition();
for (size_t i = 0; i < directions.size(); ++i)
{
SECTION(directions[i].first)
{
bot->SyncAction(5000, Botcraft::GoTo, Botcraft::Position(std::floor(init_position.x), std::floor(init_position.y), std::floor(init_position.z)) + directions[i].second, 0, 0, 0, false, false, 1.0f);
CHECK(SameBlock(local_player->GetPosition(), init_position + directions[i].second));
}
}
}
TEST_CASE("jump pathfinding")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(1.5, 1.01, 1.5));
std::vector<std::pair<std::string, Botcraft::Vector3<double>>> directions = {
{"south", Botcraft::Position(0, 1, 1)},
{"north", Botcraft::Position(0, 1, -1)},
{"east", Botcraft::Position(1, 1, 0)},
{"west", Botcraft::Position(-1, 1, 0)},
};
std::shared_ptr<Botcraft::LocalPlayer> local_player = bot->GetLocalPlayer();
const Botcraft::Vector3<double> init_position = local_player->GetPosition();
for (size_t i = 0; i < directions.size(); ++i)
{
SECTION(directions[i].first)
{
bot->SyncAction(5000, Botcraft::GoTo, Botcraft::Position(std::floor(init_position.x), std::floor(init_position.y), std::floor(init_position.z)) + directions[i].second, 0, 0, 0, true, false, 1.0f);
CHECK(SameBlock(local_player->GetPosition(), init_position + directions[i].second));
}
}
}
TEST_CASE("gap pathfinding")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(2.5, 2.01, 2.5));
std::vector<std::pair<std::string, Botcraft::Vector3<double>>> directions = {
{"south", Botcraft::Position(0, 0, 2)},
{"north", Botcraft::Position(0, 0, -2)},
{"east", Botcraft::Position(2, 0, 0)},
{"west", Botcraft::Position(-2, 0, 0)},
};
std::shared_ptr<Botcraft::LocalPlayer> local_player = bot->GetLocalPlayer();
const Botcraft::Vector3<double> init_position = local_player->GetPosition();
for (size_t i = 0; i < directions.size(); ++i)
{
SECTION(directions[i].first)
{
bot->SyncAction(5000, Botcraft::GoTo, Botcraft::Position(std::floor(init_position.x), std::floor(init_position.y), std::floor(init_position.z)) + directions[i].second, 0, 0, 0, true, false, 1.0f);
CHECK(SameBlock(local_player->GetPosition(), init_position + directions[i].second));
}
}
}
TEST_CASE("climb pathfinding")
{
std::vector<std::pair<std::string, Botcraft::Vector3<double>>> directions = {
{"south", Botcraft::Position(0, 4, -2)},
{"north", Botcraft::Position(0, 4, 2)},
{"east", Botcraft::Position(-2, 4, 0)},
{"west", Botcraft::Position(2, 4, 0)},
};
const Botcraft::Position& current_offset = TestManager::GetInstance().GetCurrentOffset();
for (size_t i = 0; i < directions.size(); ++i)
{
SECTION(directions[i].first)
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(2.5, 5.01, 2.5) - directions[i].second);
std::shared_ptr<Botcraft::LocalPlayer> local_player = bot->GetLocalPlayer();
const Botcraft::Vector3<double> init_position = local_player->GetPosition();
REQUIRE(Botcraft::Utilities::WaitForCondition([&]() -> bool
{
return local_player->GetOnGround();
}, 2000));
bot->SyncAction(10000, Botcraft::GoTo, Botcraft::Position(std::floor(init_position.x), std::floor(init_position.y), std::floor(init_position.z)) + directions[i].second, 0, 0, 0, true, false, 1.0f);
CHECK(SameBlock(local_player->GetPosition(), init_position + directions[i].second));
}
}
}
TEST_CASE("fall pathfinding")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(2.5, 5.01, 2.5));
std::vector<std::pair<std::string, Botcraft::Vector3<double>>> directions = {
{"south", Botcraft::Position(0, -4, 2)},
{"north", Botcraft::Position(0, -4, -2)},
{"east", Botcraft::Position(2, -4, 0)},
{"west", Botcraft::Position(-2, -4, 0)},
};
std::shared_ptr<Botcraft::LocalPlayer> local_player = bot->GetLocalPlayer();
const Botcraft::Vector3<double> init_position = local_player->GetPosition();
for (size_t i = 0; i < directions.size(); ++i)
{
SECTION(directions[i].first)
{
bot->SyncAction(10000, Botcraft::GoTo, Botcraft::Position(std::floor(init_position.x), std::floor(init_position.y), std::floor(init_position.z)) + directions[i].second, 0, 0, 0, true, false, 1.0f);
CHECK(SameBlock(local_player->GetPosition(), init_position + directions[i].second));
}
}
}
TEST_CASE("full pathfinding")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot;
SECTION("survival")
{
bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(0.0, 1.01, 0.0));
}
SECTION("creative")
{
bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(0.0, 1.01, 0.0), Botcraft::GameType::Creative);
}
const Botcraft::Position delta(2, 3, 24);
std::shared_ptr<Botcraft::LocalPlayer> local_player = bot->GetLocalPlayer();
const Botcraft::Vector3<double> init_position = local_player->GetPosition();
bot->SyncAction(120000, Botcraft::GoTo, Botcraft::Position(std::floor(init_position.x), std::floor(init_position.y), std::floor(init_position.z)) + delta, 0, 0, 0, true, false, 1.0f);
CHECK(SameBlock(local_player->GetPosition(), init_position + delta));
CHECK_THAT(local_player->GetHealth(), Catch::Matchers::WithinAbs(20.0, 0.01));
}
TEST_CASE("hazardous pathfinding")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(1.0, 1.01, 0.0));
const Botcraft::Position delta(0, 4, 2);
std::shared_ptr<Botcraft::LocalPlayer> local_player = bot->GetLocalPlayer();
const Botcraft::Vector3<double> init_position = local_player->GetPosition();
bot->SyncAction(60000, Botcraft::GoTo, Botcraft::Position(std::floor(init_position.x), std::floor(init_position.y), std::floor(init_position.z)) + delta, 0, 0, 0, true, false, 1.0f);
CHECK(SameBlock(local_player->GetPosition(), init_position + delta));
CHECK_THAT(local_player->GetHealth(), Catch::Matchers::WithinAbs(20.0, 0.01));
}
TEST_CASE("water walking pathfinding")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(2.0, 2.01, 0.0));
const Botcraft::Position delta(0, 0, 6);
const Botcraft::Position string_control_check_delta(0, 2, 3);
std::shared_ptr<Botcraft::LocalPlayer> local_player = bot->GetLocalPlayer();
std::shared_ptr<Botcraft::World> world = bot->GetWorld();
const Botcraft::Vector3<double> init_position = local_player->GetPosition();
Botcraft::Position init_position_int = Botcraft::Position(std::floor(init_position.x), std::floor(init_position.y), std::floor(init_position.z));
bot->SyncAction(60000, Botcraft::GoTo, init_position_int + delta, 0, 0, 0, true, false, 1.0f);
CHECK(SameBlock(local_player->GetPosition(), init_position + delta));
const Botcraft::Blockstate* should_not_be_air = world->GetBlock(init_position_int + string_control_check_delta);
REQUIRE_FALSE(should_not_be_air == nullptr);
CHECK_FALSE(should_not_be_air->IsAir());
}
TEST_CASE("ladder walking pathfinding")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(2.0, 2.01, 0.0));
const Botcraft::Position delta(0, 0, 6);
const Botcraft::Position string_control_check_delta(0, 2, 3);
std::shared_ptr<Botcraft::LocalPlayer> local_player = bot->GetLocalPlayer();
std::shared_ptr<Botcraft::World> world = bot->GetWorld();
const Botcraft::Vector3<double> init_position = local_player->GetPosition();
const Botcraft::Position init_position_int = Botcraft::Position(std::floor(init_position.x), std::floor(init_position.y), std::floor(init_position.z));
bot->SyncAction(60000, Botcraft::GoTo, init_position_int + delta, 0, 0, 0, true, false, 1.0f);
CHECK(SameBlock(local_player->GetPosition(), init_position + delta));
const Botcraft::Blockstate* should_not_be_air = world->GetBlock(init_position_int + string_control_check_delta);
REQUIRE_FALSE(should_not_be_air == nullptr);
CHECK_FALSE(should_not_be_air->IsAir());
}
TEST_CASE("speed pathfinding")
{
std::unique_ptr<Botcraft::SimpleBehaviourClient> bot = SetupTestBot<Botcraft::SimpleBehaviourClient>(Botcraft::Vector3<double>(0.0, 1.01, 0.0));
std::shared_ptr<Botcraft::LocalPlayer> local_player = bot->GetLocalPlayer();
const std::string& botname = bot->GetNetworkManager()->GetMyName();
bool sprint = false;
int speed_effect = 0;
float botcraft_speed_factor = 1.0f;
// Expected time is an estimate to check consistency with previous versions. It's not consistent with distance/theoretical speed
// This is mainly due to the way we travel from block to block instead of from start to goal directly
float expected_time_s = 0.0f;
SECTION("no speed")
{
speed_effect = 0;
botcraft_speed_factor = 1.0f;
SECTION("walk")
{
sprint = false;
expected_time_s = 5.7f;
}
SECTION("sprint")
{
sprint = true;
expected_time_s = 4.5f;
}
}
SECTION("botcraft cheaty speed")
{
speed_effect = 0;
botcraft_speed_factor = 1.5f;
SECTION("walk")
{
sprint = false;
expected_time_s = 3.9f;
}
SECTION("sprint")
{
sprint = true;
expected_time_s = 3.1f;
}
}
SECTION("speed I")
{
speed_effect = 1;
botcraft_speed_factor = 1.0f;
SECTION("walk")
{
sprint = false;
expected_time_s = 4.8f;
}
SECTION("sprint")
{
sprint = true;
expected_time_s = 3.8f;
}
}
SECTION("speed II")
{
speed_effect = 2;
botcraft_speed_factor = 1.0f;
SECTION("walk")
{
sprint = false;
expected_time_s = 4.1f;
}
SECTION("sprint")
{
sprint = true;
expected_time_s = 3.2f;
}
}
if (speed_effect > 0)
{
// Speed N is given using N-1
#if PROTOCOL_VERSION > 340 /* > 1.12.2 */
MinecraftServer::GetInstance().SendLine("effect give " + botname + " speed 99999 " + std::to_string(speed_effect - 1));
#else
MinecraftServer::GetInstance().SendLine("effect " + botname + " speed 99999 " + std::to_string(speed_effect - 1));
#endif
MinecraftServer::GetInstance().WaitLine(".*?: (?:Applied effect Speed|Given Speed \\(ID [0-9]+\\)(?: \\* [0-9]+)?) to " + botname + ".*", 5000);
CHECK(Botcraft::Utilities::WaitForCondition([&]() {
for (const Botcraft::EntityEffect& effect : local_player->GetEffects())
{
if (effect.type == Botcraft::EntityEffectType::Speed)
{
return true;
}
}
return false;
}, 5000));
}
const Botcraft::Position delta(0, 0, 24);
const Botcraft::Vector3<double> init_position = local_player->GetPosition();
const std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
bot->SyncAction(10000, Botcraft::GoTo, Botcraft::Position(std::floor(init_position.x), std::floor(init_position.y), std::floor(init_position.z)) + delta, 0, 0, 0, true, sprint, botcraft_speed_factor);
CHECK(SameBlock(local_player->GetPosition(), init_position + delta));
const float time_taken = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count() / 1000.0f;
CHECK_THAT(time_taken, Catch::Matchers::WithinAbs(expected_time_s, 0.2));
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,45 @@
#include "TestManager.hpp"
#include "MinecraftServer.hpp"
#include <botcraft/Game/ManagersClient.hpp>
#include <catch2/catch_test_macros.hpp>
TEST_CASE("current offset")
{
CHECK(TestManager::GetInstance().GetCurrentOffset() != Botcraft::Position());
}
TEST_CASE("commands")
{
MinecraftServer::GetInstance().SendLine("say hello");
std::vector<std::string> captured;
// \\w because we want the regex string to be \w
REQUIRE_NOTHROW(captured = MinecraftServer::GetInstance().WaitLine(".*: (?:\\[Not Secure\\] )?[[<]Server[>\\]] \\b(\\w+)\\b.*", 5000));
REQUIRE(captured.size() == 2);
CHECK(captured[0].substr(captured[0].size() - 5) == "hello");
CHECK(captured[1] == "hello");
}
TEST_CASE("create block")
{
const Botcraft::Position anvil_position = TestManager::GetInstance().GetCurrentOffset() + Botcraft::Position(1, 0, 1);
REQUIRE_NOTHROW(TestManager::GetInstance().SetBlock("anvil", anvil_position));
}
TEST_CASE("create book")
{
const Botcraft::Position& pos = TestManager::GetInstance().GetCurrentOffset();
#if PROTOCOL_VERSION < 477 /* < 1.14 */
CHECK_NOTHROW(TestManager::GetInstance().SetBlock("anvil", pos + Botcraft::Position(1, 0, 1)));
#endif
REQUIRE_NOTHROW(TestManager::GetInstance().CreateBook(pos + Botcraft::Position(1, 0, 0), { "hello", "", "hello page 3" }, "north", "Title", "author", { "desc1", "desc2" }));
}
TEST_CASE("teleport bot")
{
std::unique_ptr<Botcraft::ManagersClient> bot;
std::string botname;
REQUIRE_NOTHROW(bot = TestManager::GetInstance().GetBot(botname, Botcraft::GameType::Adventure));
CHECK_NOTHROW(TestManager::GetInstance().Teleport(botname, TestManager::GetInstance().GetCurrentOffset() + Botcraft::Vector3<double>(1, 2, 1)));
}
@@ -0,0 +1,175 @@
#include "TestManager.hpp"
#include "MinecraftServer.hpp"
#include "Utils.hpp"
#include <catch2/catch_test_macros.hpp>
#include <botcraft/Game/AssetsManager.hpp>
#if PROTOCOL_VERSION > 760 /* > 1.19.2 */
#include <sstream>
#endif
TEST_CASE("block names")
{
std::unique_ptr<Botcraft::ManagersClient> bot = SetupTestBot();
std::vector<std::pair<Botcraft::Position, std::string> > pos_names = {
#if PROTOCOL_VERSION < 393 /* < 1.13 */
{Botcraft::Position(0, 0, 1), "minecraft:stained_glass"},
{Botcraft::Position(1, 0, 0), "minecraft:magma"},
{Botcraft::Position(1, 0, 1), "minecraft:wool"},
{Botcraft::Position(1, 0, 2), "minecraft:melon_block"},
#else
{Botcraft::Position(0, 0, 1), "minecraft:white_stained_glass"},
{Botcraft::Position(1, 0, 0), "minecraft:magma_block"},
{Botcraft::Position(1, 0, 1), "minecraft:pink_wool"},
{Botcraft::Position(1, 0, 2), "minecraft:melon"},
#endif
{Botcraft::Position(0, 0, 0), "minecraft:cobblestone"},
{Botcraft::Position(0, 0, 2), "minecraft:bedrock"},
{Botcraft::Position(0, 0, 3), "minecraft:bookshelf"},
{Botcraft::Position(1, 0, 3), "minecraft:glass"}
};
const Botcraft::Position base_position = TestManager::GetInstance().GetCurrentOffset();
for (size_t i = 0; i < pos_names.size(); ++i)
{
const Botcraft::Blockstate* block;
block = bot->GetWorld()->GetBlock(base_position + pos_names[i].first);
CHECK(block != nullptr);
if (block)
{
CHECK(block->GetName() == pos_names[i].second);
}
}
}
TEST_CASE("block states")
{
std::unique_ptr<Botcraft::ManagersClient> bot = SetupTestBot();
std::vector<std::tuple<Botcraft::Position, std::string, std::string, std::string> > pos_block_variables_values = {
#if PROTOCOL_VERSION < 393 /* < 1.13 */
{Botcraft::Position(0, 0, 0), "minecraft:stone_slab", "half", "bottom"},
{Botcraft::Position(0, 0, 1), "minecraft:stone_slab", "half", "top"},
#elif PROTOCOL_VERSION < 477 /* < 1.14 */
{Botcraft::Position(0, 0, 0), "minecraft:stone_slab", "type", "bottom"},
{Botcraft::Position(0, 0, 1), "minecraft:stone_slab", "type", "top"},
#else
{Botcraft::Position(0, 0, 0), "minecraft:smooth_stone_slab", "type", "bottom"},
{Botcraft::Position(0, 0, 1), "minecraft:smooth_stone_slab", "type", "top"},
#endif
#if PROTOCOL_VERSION < 393 /* < 1.13 */
{Botcraft::Position(0, 0, 2), "minecraft:redstone_torch", "facing", "up"},
{Botcraft::Position(1, 0, 1), "minecraft:unlit_redstone_torch", "facing", "south"}
#else
{Botcraft::Position(0, 0, 2), "minecraft:redstone_torch", "lit", "true"},
{Botcraft::Position(1, 0, 1), "minecraft:redstone_wall_torch", "facing", "south"},
{Botcraft::Position(1, 0, 1), "minecraft:redstone_wall_torch", "lit", "false"}
#endif
};
const Botcraft::Position base_position = TestManager::GetInstance().GetCurrentOffset();
for (size_t i = 0; i < pos_block_variables_values.size(); ++i)
{
const Botcraft::Blockstate* block;
block = bot->GetWorld()->GetBlock(base_position + std::get<0>(pos_block_variables_values[i]));
CHECK(block != nullptr);
if (block)
{
CHECK(block->GetName() == std::get<1>(pos_block_variables_values[i]));
CHECK(block->GetVariableValue(std::get<2>(pos_block_variables_values[i])) == std::get<3>(pos_block_variables_values[i]));
}
}
}
// Biome names is sometimes wrong in 1.19.4 because of additional cherry_grove that
// is only present when experimental is activated
#if PROTOCOL_VERSION == 762 /* 1.19.4 */
TEST_CASE("biomes", "[!mayfail]")
#else
TEST_CASE("biomes")
#endif
{
std::unique_ptr<Botcraft::ManagersClient> bot = SetupTestBot();
SECTION("base biome")
{
const Botcraft::Biome* biome;
REQUIRE_NOTHROW(biome = bot->GetWorld()->GetBiome(TestManager::GetInstance().GetCurrentOffset()));
REQUIRE(biome != nullptr);
CHECK(biome->GetName() == "plains");
}
#if PROTOCOL_VERSION > 760 /* > 1.19.2 */
SECTION("changed biome")
{
const Botcraft::Position start_pos = TestManager::GetInstance().GetCurrentOffset();
const Botcraft::Position end_pos = start_pos + Botcraft::Position(2, 2, 2);
std::stringstream command;
command
<< "fillbiome" << " "
<< start_pos.x << " "
<< start_pos.y << " "
<< start_pos.z << " "
<< end_pos.x << " "
<< end_pos.y << " "
<< end_pos.z << " "
<< "desert";
MinecraftServer::GetInstance().SendLine(command.str());
MinecraftServer::GetInstance().WaitLine(".* biome (?:entry/)?entries set between .*", 5000);
const Botcraft::Biome* biome;
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
biome = bot->GetWorld()->GetBiome(start_pos);
return biome != nullptr && biome->GetName() == "desert";
}, 5000));
REQUIRE(Botcraft::Utilities::WaitForCondition([&]()
{
biome = bot->GetWorld()->GetBiome(end_pos);
return biome != nullptr && biome->GetName() == "desert";
}, 5000));
}
#endif
}
#if PROTOCOL_VERSION < 763 /* < 1.20 */
TEST_CASE("block entity")
#else
TEST_CASE("block entity 1_20")
#endif
{
std::unique_ptr<Botcraft::ManagersClient> bot = SetupTestBot();
ProtocolCraft::NBT::Value nbt;
REQUIRE_NOTHROW(nbt = bot->GetWorld()->GetBlockEntityData(TestManager::GetInstance().GetCurrentOffset() + Botcraft::Position(1, 0, 1)));
REQUIRE(nbt.HasData());
std::vector<std::string> expected_lines = { "Hello", "world", "", "!" };
#if PROTOCOL_VERSION > 762 /* > 1.19.4 */
std::vector<std::string> expected_lines_back = { "Hello", "back", "", "!" };
#endif
for (size_t i = 0; i < expected_lines.size(); ++i)
{
#if PROTOCOL_VERSION < 763 /* < 1.20 */
const std::string& line = nbt["Text" + std::to_string(i+1)].get<std::string>();
const ProtocolCraft::Json::Value content = ProtocolCraft::Json::Parse(line);
CHECK(content["text"].get_string() == expected_lines[i]);
#else
const std::string& front_line = nbt["front_text"]["messages"].as_list_of<std::string>()[i];
const std::string& back_line = nbt["back_text"]["messages"].as_list_of<std::string>()[i];
#if PROTOCOL_VERSION < 765 /* < 1.20.3 */
const ProtocolCraft::Json::Value front_content = ProtocolCraft::Json::Parse(front_line);
const ProtocolCraft::Json::Value back_content = ProtocolCraft::Json::Parse(back_line);
CHECK(front_content["text"].get_string() == expected_lines[i]);
CHECK(back_content["text"].get_string() == expected_lines_back[i]);
#else
CHECK(front_line == "\"" + expected_lines[i] + "\"");
CHECK(back_line == "\"" + expected_lines_back[i] + "\"");
#endif
#endif
}
}
@@ -0,0 +1,38 @@
project(protocolCraft_tests)
set(SRC_FILES
src/json.cpp
src/nbt.cpp
)
add_executable(${PROJECT_NAME} ${SRC_FILES})
set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 17)
target_link_libraries(${PROJECT_NAME} PRIVATE Catch2::Catch2WithMain)
target_link_libraries(${PROJECT_NAME} PRIVATE protocolCraft)
set_target_properties(${PROJECT_NAME} PROPERTIES FOLDER Tests)
if(BOTCRAFT_COMPRESSION)
target_compile_definitions(${PROJECT_NAME} PRIVATE USE_COMPRESSION=1)
endif(BOTCRAFT_COMPRESSION)
# Output the test executable next to the examples and library files
if(MSVC)
# To avoid having folder for each configuration when building with Visual
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_DEBUG "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_RELEASE "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_MINSIZEREL "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_DEBUG "${BOTCRAFT_OUTPUT_DIR}/lib")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${BOTCRAFT_OUTPUT_DIR}/lib")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO "${BOTCRAFT_OUTPUT_DIR}/lib")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_MINSIZEREL "${BOTCRAFT_OUTPUT_DIR}/lib")
else()
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/lib")
endif(MSVC)
catch_discover_tests(${PROJECT_NAME} WORKING_DIRECTORY "${BOTCRAFT_OUTPUT_DIR}/bin")
+935
View File
@@ -0,0 +1,935 @@
#include <catch2/catch_test_macros.hpp>
#include <deque>
#include <list>
#include "protocolCraft/Utilities/Json.hpp"
using namespace ProtocolCraft;
TEST_CASE("Constructors and is")
{
Json::Value j;
SECTION("empty")
{
CHECK(j.is<std::monostate>());
}
SECTION("Object")
{
j = Json::Object();
CHECK(j.is<Json::Object>());
}
SECTION("Array")
{
j = Json::Array();
CHECK(j.is<Json::Array>());
j = std::vector<int>(4, 0);
CHECK(j.is<Json::Array>());
j = std::array<int, 4>();
CHECK(j.is<Json::Array>());
j = std::array<Json::Object, 4>();
CHECK(j.is<Json::Array>());
}
SECTION("std::string")
{
j = "s";
CHECK(j.is<std::string>());
j = std::string();
CHECK(j.is<std::string>());
}
SECTION("bool")
{
j = true;
CHECK(j.is<bool>());
j = false;
CHECK(j.is<bool>());
}
SECTION("long long int")
{
j = 3LL;
CHECK(j.is<long long int>());
j = -3LL;
CHECK(j.is<long long int>());
j = 3;
CHECK(j.is<long long int>());
j = static_cast<short>(3);
CHECK(j.is<long long int>());
j = static_cast<char>(3);
CHECK(j.is<long long int>());
enum class TestEnum
{
A = 1,
B = 2
};
j = TestEnum::A;
CHECK(j.is<long long int>());
}
SECTION("unsigned long long int")
{
j = 3ULL;
CHECK(j.is<unsigned long long int>());
j = 3U;
CHECK(j.is<unsigned long long int>());
j = static_cast<unsigned short>(3);
CHECK(j.is<unsigned long long int>());
j = static_cast<unsigned char>(3);
CHECK(j.is<unsigned long long int>());
}
SECTION("double")
{
j = 3.14;
CHECK(j.is<double>());
j = -3.14;
CHECK(j.is<double>());
j = 3.14f;
CHECK(j.is<double>());
j = -3.14f;
CHECK(j.is<double>());
}
SECTION("is_integer")
{
j = 3LL;
CHECK(j.is_integer());
j = 3ULL;
CHECK(j.is_integer());
j = 3;
CHECK(j.is_integer());
j = 3.1;
CHECK_FALSE(j.is_integer());
j = 3.1f;
CHECK_FALSE(j.is_integer());
j = "a";
CHECK_FALSE(j.is_integer());
j = true;
CHECK_FALSE(j.is_integer());
}
SECTION("is_number")
{
j = 3LL;
CHECK(j.is_number());
j = 3ULL;
CHECK(j.is_number());
j = 3;
CHECK(j.is_number());
j = 3.1;
CHECK(j.is_number());
j = 3.1f;
CHECK(j.is_number());
j = "a";
CHECK_FALSE(j.is_number());
j = true;
CHECK_FALSE(j.is_number());
}
}
TEST_CASE("size()")
{
Json::Value j;
SECTION("empty")
{
CHECK(j.size() == 0);
}
SECTION("Object")
{
j = Json::Object();
CHECK(j.size() == 0);
}
SECTION("Array")
{
j = Json::Array();
CHECK(j.size() == 0);
j = std::vector<int>(4, 0);
CHECK(j.size() == 4);
}
SECTION("std::string")
{
j = "s";
CHECK_THROWS(j.size());
}
SECTION("bool")
{
j = true;
CHECK_THROWS(j.size());
}
SECTION("long long int")
{
j = 3LL;
CHECK_THROWS(j.size());
}
SECTION("unsigned long long int")
{
j = 3ULL;
CHECK_THROWS(j.size());
}
SECTION("double")
{
j = 3.14;
CHECK_THROWS(j.size());
}
}
TEST_CASE("get")
{
Json::Value j;
SECTION("empty")
{
CHECK_THROWS(j.get<bool>());
CHECK_THROWS(j.get<Json::Object>());
CHECK_THROWS(j.get<Json::Array>());
CHECK_THROWS(j.get<long long int>());
CHECK_THROWS(j.get<unsigned long long int>());
CHECK_THROWS(j.get<std::string>());
CHECK_THROWS(j.get<char>());
CHECK_THROWS(j.get<double>());
CHECK_THROWS(j.get<float>());
}
SECTION("Object")
{
j = { {"a", 1}, {"b", 2} };
CHECK_THROWS(j.get<bool>());
CHECK_NOTHROW(j.get<Json::Object>());
CHECK_THROWS(j.get<Json::Array>());
CHECK_THROWS(j.get<long long int>());
CHECK_THROWS(j.get<unsigned long long int>());
CHECK_THROWS(j.get<std::string>());
CHECK_THROWS(j.get<char>());
CHECK_THROWS(j.get<double>());
CHECK_THROWS(j.get<float>());
CHECK(j.get<Json::Object>().size() == 2);
j.get<Json::Object>()["c"] = 3;
CHECK(j.get<Json::Object>().size() == 3);
}
SECTION("Array")
{
j = { 1,2,3 };
CHECK_THROWS(j.get<bool>());
CHECK_THROWS(j.get<Json::Object>());
CHECK_NOTHROW(j.get<Json::Array>());
CHECK_THROWS(j.get<long long int>());
CHECK_THROWS(j.get<unsigned long long int>());
CHECK_THROWS(j.get<std::string>());
CHECK_THROWS(j.get<char>());
CHECK_THROWS(j.get<double>());
CHECK_THROWS(j.get<float>());
CHECK(j.get<Json::Array>().size() == 3);
j.get<Json::Array>().push_back(3);
CHECK(j.get<Json::Array>().size() == 4);
}
SECTION("std::string")
{
j = "Hello";
CHECK_THROWS(j.get<bool>());
CHECK_THROWS(j.get<Json::Object>());
CHECK_THROWS(j.get<Json::Array>());
CHECK_THROWS(j.get<long long int>());
CHECK_THROWS(j.get<unsigned long long int>());
CHECK_NOTHROW(j.get<std::string>());
CHECK_THROWS(j.get<char>());
CHECK_THROWS(j.get<double>());
CHECK_THROWS(j.get<float>());
CHECK(j.get<std::string>() == "Hello");
j.get<std::string>() += " world!";
CHECK(j.get<std::string>() == "Hello world!");
}
SECTION("bool")
{
j = true;
CHECK_NOTHROW(j.get<bool>());
CHECK_THROWS(j.get<Json::Object>());
CHECK_THROWS(j.get<Json::Array>());
CHECK_THROWS(j.get<long long int>());
CHECK_THROWS(j.get<unsigned long long int>());
CHECK_THROWS(j.get<std::string>());
CHECK_THROWS(j.get<char>());
CHECK_THROWS(j.get<double>());
CHECK_THROWS(j.get<float>());
CHECK(j.get<bool>() == true);
}
SECTION("long long int")
{
j = -1;
CHECK_THROWS(j.get<bool>());
CHECK_THROWS(j.get<Json::Object>());
CHECK_THROWS(j.get<Json::Array>());
CHECK_NOTHROW(j.get<long long int>());
CHECK_NOTHROW(j.get<unsigned long long int>());
CHECK_THROWS(j.get<std::string>());
CHECK_NOTHROW(j.get<char>());
CHECK_NOTHROW(j.get<double>());
CHECK_NOTHROW(j.get<float>());
CHECK(j.get<long long int>() == -1LL);
CHECK(j.get<int>() == -1);
CHECK(j.get<unsigned int>() == static_cast<unsigned int>(-1));
CHECK(j.get<double>() == -1.0);
}
SECTION("unsigned long long int")
{
j = 1U;
CHECK_THROWS(j.get<bool>());
CHECK_THROWS(j.get<Json::Object>());
CHECK_THROWS(j.get<Json::Array>());
CHECK_NOTHROW(j.get<long long int>());
CHECK_NOTHROW(j.get<unsigned long long int>());
CHECK_THROWS(j.get<std::string>());
CHECK_NOTHROW(j.get<char>());
CHECK_NOTHROW(j.get<double>());
CHECK_NOTHROW(j.get<float>());
CHECK(j.get<unsigned long long int>() == 1U);
CHECK(j.get<unsigned int>() == 1U);
CHECK(j.get<int>() == 1);
CHECK(j.get<double>() == 1.0);
}
SECTION("double")
{
j = 1.0;
CHECK_THROWS(j.get<bool>());
CHECK_THROWS(j.get<Json::Object>());
CHECK_THROWS(j.get<Json::Array>());
CHECK_NOTHROW(j.get<long long int>());
CHECK_NOTHROW(j.get<unsigned long long int>());
CHECK_THROWS(j.get<std::string>());
CHECK_NOTHROW(j.get<char>());
CHECK_NOTHROW(j.get<double>());
CHECK_NOTHROW(j.get<float>());
CHECK(j.get<double>() == 1.0);
CHECK(j.get<float>() == 1.0);
CHECK(j.get<char>() == 1);
}
SECTION("get_number")
{
j = 1.0;
CHECK(j.get_number<double>() == 1.0);
CHECK(j.get_number<int>() == 1);
j = 1ULL;
CHECK(j.get_number<double>() == 1.0);
CHECK(j.get_number<unsigned long long int>() == 1);
j = 1LL;
CHECK(j.get_number<double>() == 1.0);
CHECK(j.get_number<long long int>() == 1);
j = true;
CHECK_THROWS(j.get_number<double>());
CHECK_THROWS(j.get_number<unsigned long long int>());
}
}
TEST_CASE("Array")
{
Json::Value j;
SECTION("push_back")
{
j.push_back("foo");
j.push_back(1);
j.push_back(true);
CHECK(j.is<Json::Array>());
CHECK(j.size() == 3);
CHECK(j[2].get<bool>() == true);
}
SECTION("initializer list")
{
j = { "foo", 1, true };
CHECK(j.is<Json::Array>());
CHECK(j.size() == 3);
CHECK(j[2].get<bool>() == true);
}
SECTION("from std::vector")
{
j = std::vector<int>({ 1,2,3 });
CHECK(j.is<Json::Array>());
CHECK(j.size() == 3);
CHECK(j[2].get<int>() == 3);
}
SECTION("from std::deque")
{
j = std::deque<int>({ 1,2,3 });
CHECK(j.is<Json::Array>());
CHECK(j.size() == 3);
CHECK(j[2].get<int>() == 3);
}
SECTION("from std::list")
{
j = std::list<int>({ 1,2,3 });
CHECK(j.is<Json::Array>());
CHECK(j.size() == 3);
CHECK(j[2].get<int>() == 3);
}
}
TEST_CASE("Object")
{
Json::Value j;
SECTION("Cast from empty")
{
j["pi"] = 3.141;
j["happy"] = true;
j["name"] = "Niels";
j["nothing"] = nullptr;
j["answer"]["everything"] = 42;
j["list"] = { 1, 0, 2 };
j["object"] = { {"currency", "USD"}, {"value", 42.99} };
CHECK(j.is<Json::Object>());
CHECK(j.size() == 7);
CHECK(j["happy"].is<bool>());
CHECK(j["name"].is<std::string>());
CHECK(j["nothing"].is<std::monostate>());
CHECK(j["answer"].is<Json::Object>());
CHECK(j["answer"].size() == 1);
CHECK(j["answer"]["everything"].is<long long int>());
CHECK(j["answer"]["everything"].get<long long int>() == 42);
CHECK(j["list"].is<Json::Array>());
CHECK(j["list"].size() == 3);
CHECK(j["object"].is<Json::Object>());
CHECK(j["object"].size() == 2);
}
SECTION("initializer list")
{
j =
{
{"pi", 3.141},
{"happy", true},
{"name", "Niels"},
{"nothing", nullptr},
{
"answer", {
{"everything", 42}
}
},
{"list", {1, 0, 2}},
{
"object", {
{"currency", "USD"},
{"value", 42.99}
}
}
};
CHECK(j.is<Json::Object>());
CHECK(j.size() == 7);
CHECK(j["happy"].is<bool>());
CHECK(j["name"].is<std::string>());
CHECK(j["nothing"].is<std::monostate>());
CHECK(j["answer"].is<Json::Object>());
CHECK(j["answer"].size() == 1);
CHECK(j["answer"]["everything"].is<long long int>());
CHECK(j["answer"]["everything"].get<long long int>() == 42);
CHECK(j["list"].is<Json::Array>());
CHECK(j["list"].size() == 3);
CHECK(j["object"].is<Json::Object>());
CHECK(j["object"].size() == 2);
}
}
TEST_CASE("Dump")
{
Json::Value j;
SECTION("Null")
{
j = { };
CHECK(j.Dump() == "null");
CHECK(j.Dump(0) == "null");
CHECK(j.Dump(4) == "null");
CHECK(j.Dump(1, '\t') == "null");
j = nullptr;
CHECK(j.Dump() == "null");
CHECK(j.Dump(0) == "null");
CHECK(j.Dump(4) == "null");
CHECK(j.Dump(1, '\t') == "null");
}
SECTION("String")
{
j = "";
CHECK(j.Dump() == "\"\"");
CHECK(j.Dump(0) == "\"\"");
CHECK(j.Dump(4) == "\"\"");
CHECK(j.Dump(1, '\t') == "\"\"");
j = "hello, \"world\"!";
CHECK(j.Dump() == "\"hello, \\\"world\\\"!\"");
CHECK(j.Dump(0) == "\"hello, \\\"world\\\"!\"");
CHECK(j.Dump(4) == "\"hello, \\\"world\\\"!\"");
CHECK(j.Dump(1, '\t') == "\"hello, \\\"world\\\"!\"");
j = "\"\n\"";
CHECK(j.Dump() == "\"\\\"\\n\\\"\"");
CHECK(j.Dump(0) == "\"\\\"\\n\\\"\"");
CHECK(j.Dump(4) == "\"\\\"\\n\\\"\"");
CHECK(j.Dump(1, '\t') == "\"\\\"\\n\\\"\"");
}
SECTION("Bool")
{
j = true;
CHECK(j.Dump() == "true");
CHECK(j.Dump(0) == "true");
CHECK(j.Dump(4) == "true");
CHECK(j.Dump(1, '\t') == "true");
j = false;
CHECK(j.Dump() == "false");
CHECK(j.Dump(0) == "false");
CHECK(j.Dump(4) == "false");
CHECK(j.Dump(1, '\t') == "false");
}
SECTION("Int")
{
j = 1;
CHECK(j.Dump() == "1");
CHECK(j.Dump(0) == "1");
CHECK(j.Dump(4) == "1");
CHECK(j.Dump(1, '\t') == "1");
j = std::numeric_limits<long long int>::max();
CHECK(j.Dump() == std::to_string(std::numeric_limits<long long int>::max()));
CHECK(j.Dump(0) == std::to_string(std::numeric_limits<long long int>::max()));
CHECK(j.Dump(4) == std::to_string(std::numeric_limits<long long int>::max()));
CHECK(j.Dump(1, '\t') == std::to_string(std::numeric_limits<long long int>::max()));
j = std::numeric_limits<long long int>::min();
CHECK(j.Dump() == std::to_string(std::numeric_limits<long long int>::min()));
CHECK(j.Dump(0) == std::to_string(std::numeric_limits<long long int>::min()));
CHECK(j.Dump(4) == std::to_string(std::numeric_limits<long long int>::min()));
CHECK(j.Dump(1, '\t') == std::to_string(std::numeric_limits<long long int>::min()));
}
SECTION("UInt")
{
j = 1u;
CHECK(j.Dump() == "1");
CHECK(j.Dump(0) == "1");
CHECK(j.Dump(4) == "1");
CHECK(j.Dump(1, '\t') == "1");
j = std::numeric_limits<unsigned long long int>::max();
CHECK(j.Dump() == std::to_string(std::numeric_limits<unsigned long long int>::max()));
CHECK(j.Dump(0) == std::to_string(std::numeric_limits<unsigned long long int>::max()));
CHECK(j.Dump(4) == std::to_string(std::numeric_limits<unsigned long long int>::max()));
CHECK(j.Dump(1, '\t') == std::to_string(std::numeric_limits<unsigned long long int>::max()));
}
SECTION("Double")
{
j = 1.0;
CHECK(j.Dump() == "1.0");
CHECK(j.Dump(0) == "1.0");
CHECK(j.Dump(4) == "1.0");
CHECK(j.Dump(1, '\t') == "1.0");
j = -1.0;
CHECK(j.Dump() == "-1.0");
CHECK(j.Dump(0) == "-1.0");
CHECK(j.Dump(4) == "-1.0");
CHECK(j.Dump(1, '\t') == "-1.0");
}
SECTION("Array")
{
j = Json::Array();
CHECK(j.Dump() == "[]");
CHECK(j.Dump(0) == "[]");
CHECK(j.Dump(4) == "[]");
CHECK(j.Dump(4, '\t') == "[]");
j = { "a", 1, true, 2.0, { {}, nullptr} };
CHECK(j.Dump() == "[\"a\",1,true,2.0,[null,null]]");
CHECK(j.Dump(0) ==
"[\n"
"\"a\",\n"
"1,\n"
"true,\n"
"2.0,\n"
"[\n"
"null,\n"
"null\n"
"]\n"
"]");
CHECK(j.Dump(4) ==
"[\n"
" \"a\",\n"
" 1,\n"
" true,\n"
" 2.0,\n"
" [\n"
" null,\n"
" null\n"
" ]\n"
"]");
CHECK(j.Dump(1, '\t') ==
"[\n"
"\t\"a\",\n"
"\t1,\n"
"\ttrue,\n"
"\t2.0,\n"
"\t[\n"
"\t\tnull,\n"
"\t\tnull\n"
"\t]\n"
"]"
);
}
SECTION("Object")
{
j = Json::Object();
CHECK(j.Dump() == "{}");
CHECK(j.Dump(0) == "{}");
CHECK(j.Dump(4) == "{}");
CHECK(j.Dump(1, '\t') == "{}");
j = { { "a", { "b", 1 } }, { "c", "hello"} };
CHECK(j.Dump() == "{\"a\":{\"b\":1},\"c\":\"hello\"}");
CHECK(j.Dump(0) ==
"{\n"
"\"a\": {\n"
"\"b\": 1\n"
"},\n"
"\"c\": \"hello\"\n"
"}"
);
CHECK(j.Dump(4) ==
"{\n"
" \"a\": {\n"
" \"b\": 1\n"
" },\n"
" \"c\": \"hello\"\n"
"}"
);
CHECK(j.Dump(1, '\t') ==
"{\n"
"\t\"a\": {\n"
"\t\t\"b\": 1\n"
"\t},\n"
"\t\"c\": \"hello\"\n"
"}"
);
}
}
TEST_CASE("Parse")
{
std::string s;
Json::Value result;
SECTION("Empty")
{
s = "null";
result = Json::Parse(s);
CHECK(result.is<std::monostate>());
}
SECTION("Bool")
{
s = "true";
result = Json::Parse(s);
CHECK(result.is<bool>());
CHECK(result.get<bool>() == true);
s = "false";
result = Json::Parse(s);
CHECK(result.is<bool>());
CHECK(result.get<bool>() == false);
}
SECTION("String")
{
s = "\"\"";
result = Json::Parse(s);
CHECK(result.is<std::string>());
CHECK(result.get<std::string>() == "");
s = "\"hello\"";
result = Json::Parse(s);
CHECK(result.is<std::string>());
CHECK(result.get<std::string>() == "hello");
s = "\"hel\\\"lo\"";
result = Json::Parse(s);
CHECK(result.is<std::string>());
CHECK(result.get<std::string>() == "hel\\\"lo");
s = "\"\\u000a\"";
result = Json::Parse(s);
CHECK(result.is<std::string>());
CHECK(result.get<std::string>() == "\n");
s = "\"\\u000A\"";
result = Json::Parse(s);
CHECK(result.is<std::string>());
CHECK(result.get<std::string>() == "\n");
s = "\"\\u0009\"";
result = Json::Parse(s);
CHECK(result.is<std::string>());
CHECK(result.get<std::string>() == "\t");
s = "\"\\u12\"";
CHECK_THROWS(Json::Parse(s));
s = "\"\t\"";
CHECK_THROWS(Json::Parse(s));
s = "\"\n\"";
CHECK_THROWS(Json::Parse(s));
s = "\"\r\"";
CHECK_THROWS(Json::Parse(s));
s = "\"\b\"";
CHECK_THROWS(Json::Parse(s));
s = "\"\f\"";
CHECK_THROWS(Json::Parse(s));
}
SECTION("Number")
{
s = "0.1";
result = Json::Parse(s);
CHECK(result.is<double>());
CHECK(result.get<double>() == 0.1);
s = "147e12";
result = Json::Parse(s);
CHECK(result.is<double>());
CHECK(result.get<double>() == 147e12);
s = "147E+12";
result = Json::Parse(s);
CHECK(result.is<double>());
CHECK(result.get<double>() == 147e12);
s = "-147E-12";
result = Json::Parse(s);
CHECK(result.is<double>());
CHECK(result.get<double>() == -147e-12);
s = "1";
result = Json::Parse(s);
CHECK(result.is_integer());
CHECK(result.is<unsigned long long int>());
CHECK(result.get<unsigned long long int>() == 1);
s = "-1";
result = Json::Parse(s);
CHECK(result.is_integer());
CHECK(result.is<long long int>());
CHECK(result.get<long long int>() == -1);
s = "0e+1";
result = Json::Parse(s);
CHECK(result.is<double>());
CHECK(result.get<double>() == 0.0);
s = "-0.000000000000000000000000000000000000000000000000000000000000000000000000000001";
result = Json::Parse(s);
CHECK(result.is<double>());
CHECK(result.get<double>() == -0.000000000000000000000000000000000000000000000000000000000000000000000000000001);
s = "5708990770823839207320493820740630171355185152";
result = Json::Parse(s);
CHECK(result.is<double>());
CHECK(result.get<double>() == 5.7089907708238395e+45);
s = "+1";
CHECK_THROWS(Json::Parse(s));
s = "0";
result = Json::Parse(s);
CHECK(result.is<unsigned long long int>());
CHECK(result.get<unsigned long long int>() == 0);
s = "007";
CHECK_THROWS(Json::Parse(s));
s = "1e";
CHECK_THROWS(Json::Parse(s));
s = "-01";
CHECK_THROWS(Json::Parse(s));
s = "0.e1";
CHECK_THROWS(Json::Parse(s));
s = "1+2";
CHECK_THROWS(Json::Parse(s));
s = "1e+-2";
CHECK_THROWS(Json::Parse(s));
s = "-.1";
CHECK_THROWS(Json::Parse(s));
s = "2.e1";
CHECK_THROWS(Json::Parse(s));
}
SECTION("Array")
{
s = "[]";
result = Json::Parse(s);
CHECK(result.is<Json::Array>());
CHECK(result.size() == 0);
s = "[]\n";
result = Json::Parse(s);
CHECK(result.is<Json::Array>());
CHECK(result.size() == 0);
s = "[ ]";
result = Json::Parse(s);
CHECK(result.is<Json::Array>());
CHECK(result.size() == 0);
s = "[ \t ]";
result = Json::Parse(s);
CHECK(result.is<Json::Array>());
CHECK(result.size() == 0);
s = "[1,\t2,3.0,4 ,\n 5,\"hello\"]";
result = Json::Parse(s);
CHECK(result.is<Json::Array>());
CHECK(result.size() == 6);
CHECK(result[5].get<std::string>() == "hello");
s = "[,]";
CHECK_THROWS(Json::Parse(s));
s = "[1,2,]";
CHECK_THROWS(Json::Parse(s));
}
SECTION("Object")
{
s = "{}";
result = Json::Parse(s);
CHECK(result.is<Json::Object>());
CHECK(result.size() == 0);
s = "{}\n";
result = Json::Parse(s);
CHECK(result.is<Json::Object>());
CHECK(result.size() == 0);
s = "{ }";
result = Json::Parse(s);
CHECK(result.is<Json::Object>());
CHECK(result.size() == 0);
s = "{ }";
result = Json::Parse(s);
CHECK(result.is<Json::Object>());
CHECK(result.size() == 0);
s = "{\"a\": 1, \"b\": 42.0, \"c\": \t\t\t\"hello\",\n\n \"d\": [1,2,3,\"world\"]\n\n\n}";
result = Json::Parse(s);
CHECK(result.is<Json::Object>());
CHECK(result.size() == 4);
CHECK(result["d"][3].get<std::string>() == "world");
s = "{}more content";
CHECK_THROWS(Json::Parse(s));
s = "{,}";
CHECK_THROWS(Json::Parse(s));
s = "{\"a\": 1, }";
CHECK_THROWS(Json::Parse(s));
s = "{\"a\": 1, }";
CHECK_NOTHROW(Json::Parse(s, true));
CHECK(Json::Parse(s, true).is<std::monostate>());
}
}
TEST_CASE("ToJsonAndBack")
{
std::string input;
input = "null";
CHECK(input == Json::Parse(input).Dump());
input = "2.0";
CHECK(input == Json::Parse(input).Dump());
input = "[\"a\",1,{\"b\":\"c\"}]";
CHECK(input == Json::Parse(input).Dump());
input = "{}";
CHECK(input == Json::Parse(input).Dump());
}
+443
View File
@@ -0,0 +1,443 @@
#include <catch2/catch_test_macros.hpp>
#include "protocolCraft/Types/NBT/NBT.hpp"
using namespace ProtocolCraft;
TEST_CASE("Empty NBT")
{
NBT::Value n;
std::vector<unsigned char> data = { 0x00 };
ReadIterator iter = data.begin();
size_t length = data.size();
SECTION("Empty")
{
CHECK_FALSE(n.HasData());
}
SECTION("Read Empty")
{
n = ReadData<NBT::Value>(iter, length);
CHECK_FALSE(n.HasData());
CHECK_THROWS(n["a"]);
}
SECTION("Serialization")
{
std::vector<unsigned char> serialized;
serialized.reserve(data.size());
WriteData<NBT::Value>(n, serialized);
CHECK(serialized == data);
}
}
TEST_CASE("Tag short only")
{
std::vector<unsigned char> data = { 0x02, 0x00, 0x09, 0x73,0x68,0x6F, 0x72, 0x74, 0x54, 0x65, 0x73, 0x74, 0x7F, 0xFF };
ReadIterator iter = data.begin();
size_t length = data.size();
SECTION("Not a valid NBT")
{
NBT::Value n;
CHECK_THROWS(n.Read(iter, length));
}
SECTION("Valid short NamedTag")
{
NBT::Tag t = ReadData<NBT::Tag>(iter, length);
CHECK(t.GetName() == "shortTest");
CHECK(t.is<short>());
CHECK(t.get<NBT::TagShort>() == 32767);
CHECK(t.get<short>() == 32767);
CHECK_THROWS(t.get<NBT::TagCompound>());
CHECK_THROWS(t.get<NBT::TagList>());
CHECK_THROWS(t.get<NBT::TagInt>());
CHECK_THROWS(t["A"]);
CHECK_THROWS(t.size());
CHECK_THROWS(t.as_list_of<short>());
CHECK_THROWS(t.as_list_of<NBT::TagCompound>());
Json::Value check_serialization = { { "name", "shortTest"}, {"type", "TagShort"}, {"content", 32767} };
CHECK(t.Serialize().Dump() == check_serialization.Dump());
}
}
TEST_CASE("test nbt")
{
std::vector<unsigned char> data = {
0x0A, // TagCompound
0x00, 0x0B, // Name length
0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, // Name
0x08, // TagString
0x00, 0x04, // Name length
0x6E, 0x61, 0x6D, 0x65, // Name
0x00, 0x09, // String length
0x42, 0x61, 0x6E, 0x61, 0x6E, 0x72, 0x61, 0x6D, 0x61, // String content
0x00 // TagEnd
};
ReadIterator iter = data.begin();
size_t length = data.size();
NBT::Value nbt = ReadData<NBT::Value>(iter, length);
CHECK(nbt.HasData());
CHECK(nbt.is<NBT::TagCompound>());
CHECK(nbt.size() == 1);
CHECK(nbt.GetName() == "hello world");
CHECK(nbt["name"].is<NBT::TagString>());
CHECK(nbt["name"].get<NBT::TagString>() == "Bananrama");
std::vector<unsigned char> serialized;
serialized.reserve(data.size());
WriteData<NBT::Value>(nbt, serialized);
CHECK(serialized == data);
}
TEST_CASE("Uncompressed bigtest nbt")
{
std::vector<unsigned char> data = {
0x0A, 0x00, 0x05, 0x4C, 0x65, 0x76, 0x65, 0x6C, 0x04, 0x00, 0x08, 0x6C, 0x6F, 0x6E, 0x67, 0x54, 0x65, 0x73, 0x74, 0x7F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x02, 0x00, 0x09, 0x73, 0x68, 0x6F, 0x72, 0x74, 0x54, 0x65, 0x73, 0x74, 0x7F, 0xFF, 0x08,
0x00, 0x0A, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x54, 0x65, 0x73, 0x74, 0x00, 0x29, 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x20, 0x57,
0x4F, 0x52, 0x4C, 0x44, 0x20, 0x54, 0x48, 0x49, 0x53, 0x20, 0x49, 0x53, 0x20, 0x41, 0x20, 0x54, 0x45, 0x53, 0x54, 0x20, 0x53,
0x54, 0x52, 0x49, 0x4E, 0x47, 0x20, 0xC3, 0x85, 0xC3, 0x84, 0xC3, 0x96, 0x21, 0x05, 0x00, 0x09, 0x66, 0x6C, 0x6F, 0x61, 0x74,
0x54, 0x65, 0x73, 0x74, 0x3E, 0xFF, 0x18, 0x32, 0x03, 0x00, 0x07, 0x69, 0x6E, 0x74, 0x54, 0x65, 0x73, 0x74, 0x7F, 0xFF, 0xFF,
0xFF, 0x0A, 0x00, 0x14, 0x6E, 0x65, 0x73, 0x74, 0x65, 0x64, 0x20, 0x63, 0x6F, 0x6D, 0x70, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x74,
0x65, 0x73, 0x74, 0x0A, 0x00, 0x03, 0x68, 0x61, 0x6D, 0x08, 0x00, 0x04, 0x6E, 0x61, 0x6D, 0x65, 0x00, 0x06, 0x48, 0x61, 0x6D,
0x70, 0x75, 0x73, 0x05, 0x00, 0x05, 0x76, 0x61, 0x6C, 0x75, 0x65, 0x3F, 0x40, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x03, 0x65, 0x67,
0x67, 0x08, 0x00, 0x04, 0x6E, 0x61, 0x6D, 0x65, 0x00, 0x07, 0x45, 0x67, 0x67, 0x62, 0x65, 0x72, 0x74, 0x05, 0x00, 0x05, 0x76,
0x61, 0x6C, 0x75, 0x65, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x0F, 0x6C, 0x69, 0x73, 0x74, 0x54, 0x65, 0x73, 0x74,
0x20, 0x28, 0x6C, 0x6F, 0x6E, 0x67, 0x29, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x09, 0x00, 0x13, 0x6C, 0x69, 0x73, 0x74, 0x54, 0x65, 0x73, 0x74,
0x20, 0x28, 0x63, 0x6F, 0x6D, 0x70, 0x6F, 0x75, 0x6E, 0x64, 0x29, 0x0A, 0x00, 0x00, 0x00, 0x02, 0x08, 0x00, 0x04, 0x6E, 0x61,
0x6D, 0x65, 0x00, 0x0F, 0x43, 0x6F, 0x6D, 0x70, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x74, 0x61, 0x67, 0x20, 0x23, 0x30, 0x04, 0x00,
0x0A, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x2D, 0x6F, 0x6E, 0x00, 0x00, 0x01, 0x26, 0x52, 0x37, 0xD5, 0x8D, 0x00, 0x08,
0x00, 0x04, 0x6E, 0x61, 0x6D, 0x65, 0x00, 0x0F, 0x43, 0x6F, 0x6D, 0x70, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x74, 0x61, 0x67, 0x20,
0x23, 0x31, 0x04, 0x00, 0x0A, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x2D, 0x6F, 0x6E, 0x00, 0x00, 0x01, 0x26, 0x52, 0x37,
0xD5, 0x8D, 0x00, 0x01, 0x00, 0x08, 0x62, 0x79, 0x74, 0x65, 0x54, 0x65, 0x73, 0x74, 0x7F, 0x07, 0x00, 0x65, 0x62, 0x79, 0x74,
0x65, 0x41, 0x72, 0x72, 0x61, 0x79, 0x54, 0x65, 0x73, 0x74, 0x20, 0x28, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74,
0x20, 0x31, 0x30, 0x30, 0x30, 0x20, 0x76, 0x61, 0x6C, 0x75, 0x65, 0x73, 0x20, 0x6F, 0x66, 0x20, 0x28, 0x6E, 0x2A, 0x6E, 0x2A,
0x32, 0x35, 0x35, 0x2B, 0x6E, 0x2A, 0x37, 0x29, 0x25, 0x31, 0x30, 0x30, 0x2C, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x69, 0x6E,
0x67, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x6E, 0x3D, 0x30, 0x20, 0x28, 0x30, 0x2C, 0x20, 0x36, 0x32, 0x2C, 0x20, 0x33, 0x34,
0x2C, 0x20, 0x31, 0x36, 0x2C, 0x20, 0x38, 0x2C, 0x20, 0x2E, 0x2E, 0x2E, 0x29, 0x29, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x3E, 0x22,
0x10, 0x08, 0x0A, 0x16, 0x2C, 0x4C, 0x12, 0x46, 0x20, 0x04, 0x56, 0x4E, 0x50, 0x5C, 0x0E, 0x2E, 0x58, 0x28, 0x02, 0x4A, 0x38,
0x30, 0x32, 0x3E, 0x54, 0x10, 0x3A, 0x0A, 0x48, 0x2C, 0x1A, 0x12, 0x14, 0x20, 0x36, 0x56, 0x1C, 0x50, 0x2A, 0x0E, 0x60, 0x58,
0x5A, 0x02, 0x18, 0x38, 0x62, 0x32, 0x0C, 0x54, 0x42, 0x3A, 0x3C, 0x48, 0x5E, 0x1A, 0x44, 0x14, 0x52, 0x36, 0x24, 0x1C, 0x1E,
0x2A, 0x40, 0x60, 0x26, 0x5A, 0x34, 0x18, 0x06, 0x62, 0x00, 0x0C, 0x22, 0x42, 0x08, 0x3C, 0x16, 0x5E, 0x4C, 0x44, 0x46, 0x52,
0x04, 0x24, 0x4E, 0x1E, 0x5C, 0x40, 0x2E, 0x26, 0x28, 0x34, 0x4A, 0x06, 0x30, 0x00, 0x3E, 0x22, 0x10, 0x08, 0x0A, 0x16, 0x2C,
0x4C, 0x12, 0x46, 0x20, 0x04, 0x56, 0x4E, 0x50, 0x5C, 0x0E, 0x2E, 0x58, 0x28, 0x02, 0x4A, 0x38, 0x30, 0x32, 0x3E, 0x54, 0x10,
0x3A, 0x0A, 0x48, 0x2C, 0x1A, 0x12, 0x14, 0x20, 0x36, 0x56, 0x1C, 0x50, 0x2A, 0x0E, 0x60, 0x58, 0x5A, 0x02, 0x18, 0x38, 0x62,
0x32, 0x0C, 0x54, 0x42, 0x3A, 0x3C, 0x48, 0x5E, 0x1A, 0x44, 0x14, 0x52, 0x36, 0x24, 0x1C, 0x1E, 0x2A, 0x40, 0x60, 0x26, 0x5A,
0x34, 0x18, 0x06, 0x62, 0x00, 0x0C, 0x22, 0x42, 0x08, 0x3C, 0x16, 0x5E, 0x4C, 0x44, 0x46, 0x52, 0x04, 0x24, 0x4E, 0x1E, 0x5C,
0x40, 0x2E, 0x26, 0x28, 0x34, 0x4A, 0x06, 0x30, 0x00, 0x3E, 0x22, 0x10, 0x08, 0x0A, 0x16, 0x2C, 0x4C, 0x12, 0x46, 0x20, 0x04,
0x56, 0x4E, 0x50, 0x5C, 0x0E, 0x2E, 0x58, 0x28, 0x02, 0x4A, 0x38, 0x30, 0x32, 0x3E, 0x54, 0x10, 0x3A, 0x0A, 0x48, 0x2C, 0x1A,
0x12, 0x14, 0x20, 0x36, 0x56, 0x1C, 0x50, 0x2A, 0x0E, 0x60, 0x58, 0x5A, 0x02, 0x18, 0x38, 0x62, 0x32, 0x0C, 0x54, 0x42, 0x3A,
0x3C, 0x48, 0x5E, 0x1A, 0x44, 0x14, 0x52, 0x36, 0x24, 0x1C, 0x1E, 0x2A, 0x40, 0x60, 0x26, 0x5A, 0x34, 0x18, 0x06, 0x62, 0x00,
0x0C, 0x22, 0x42, 0x08, 0x3C, 0x16, 0x5E, 0x4C, 0x44, 0x46, 0x52, 0x04, 0x24, 0x4E, 0x1E, 0x5C, 0x40, 0x2E, 0x26, 0x28, 0x34,
0x4A, 0x06, 0x30, 0x00, 0x3E, 0x22, 0x10, 0x08, 0x0A, 0x16, 0x2C, 0x4C, 0x12, 0x46, 0x20, 0x04, 0x56, 0x4E, 0x50, 0x5C, 0x0E,
0x2E, 0x58, 0x28, 0x02, 0x4A, 0x38, 0x30, 0x32, 0x3E, 0x54, 0x10, 0x3A, 0x0A, 0x48, 0x2C, 0x1A, 0x12, 0x14, 0x20, 0x36, 0x56,
0x1C, 0x50, 0x2A, 0x0E, 0x60, 0x58, 0x5A, 0x02, 0x18, 0x38, 0x62, 0x32, 0x0C, 0x54, 0x42, 0x3A, 0x3C, 0x48, 0x5E, 0x1A, 0x44,
0x14, 0x52, 0x36, 0x24, 0x1C, 0x1E, 0x2A, 0x40, 0x60, 0x26, 0x5A, 0x34, 0x18, 0x06, 0x62, 0x00, 0x0C, 0x22, 0x42, 0x08, 0x3C,
0x16, 0x5E, 0x4C, 0x44, 0x46, 0x52, 0x04, 0x24, 0x4E, 0x1E, 0x5C, 0x40, 0x2E, 0x26, 0x28, 0x34, 0x4A, 0x06, 0x30, 0x00, 0x3E,
0x22, 0x10, 0x08, 0x0A, 0x16, 0x2C, 0x4C, 0x12, 0x46, 0x20, 0x04, 0x56, 0x4E, 0x50, 0x5C, 0x0E, 0x2E, 0x58, 0x28, 0x02, 0x4A,
0x38, 0x30, 0x32, 0x3E, 0x54, 0x10, 0x3A, 0x0A, 0x48, 0x2C, 0x1A, 0x12, 0x14, 0x20, 0x36, 0x56, 0x1C, 0x50, 0x2A, 0x0E, 0x60,
0x58, 0x5A, 0x02, 0x18, 0x38, 0x62, 0x32, 0x0C, 0x54, 0x42, 0x3A, 0x3C, 0x48, 0x5E, 0x1A, 0x44, 0x14, 0x52, 0x36, 0x24, 0x1C,
0x1E, 0x2A, 0x40, 0x60, 0x26, 0x5A, 0x34, 0x18, 0x06, 0x62, 0x00, 0x0C, 0x22, 0x42, 0x08, 0x3C, 0x16, 0x5E, 0x4C, 0x44, 0x46,
0x52, 0x04, 0x24, 0x4E, 0x1E, 0x5C, 0x40, 0x2E, 0x26, 0x28, 0x34, 0x4A, 0x06, 0x30, 0x00, 0x3E, 0x22, 0x10, 0x08, 0x0A, 0x16,
0x2C, 0x4C, 0x12, 0x46, 0x20, 0x04, 0x56, 0x4E, 0x50, 0x5C, 0x0E, 0x2E, 0x58, 0x28, 0x02, 0x4A, 0x38, 0x30, 0x32, 0x3E, 0x54,
0x10, 0x3A, 0x0A, 0x48, 0x2C, 0x1A, 0x12, 0x14, 0x20, 0x36, 0x56, 0x1C, 0x50, 0x2A, 0x0E, 0x60, 0x58, 0x5A, 0x02, 0x18, 0x38,
0x62, 0x32, 0x0C, 0x54, 0x42, 0x3A, 0x3C, 0x48, 0x5E, 0x1A, 0x44, 0x14, 0x52, 0x36, 0x24, 0x1C, 0x1E, 0x2A, 0x40, 0x60, 0x26,
0x5A, 0x34, 0x18, 0x06, 0x62, 0x00, 0x0C, 0x22, 0x42, 0x08, 0x3C, 0x16, 0x5E, 0x4C, 0x44, 0x46, 0x52, 0x04, 0x24, 0x4E, 0x1E,
0x5C, 0x40, 0x2E, 0x26, 0x28, 0x34, 0x4A, 0x06, 0x30, 0x00, 0x3E, 0x22, 0x10, 0x08, 0x0A, 0x16, 0x2C, 0x4C, 0x12, 0x46, 0x20,
0x04, 0x56, 0x4E, 0x50, 0x5C, 0x0E, 0x2E, 0x58, 0x28, 0x02, 0x4A, 0x38, 0x30, 0x32, 0x3E, 0x54, 0x10, 0x3A, 0x0A, 0x48, 0x2C,
0x1A, 0x12, 0x14, 0x20, 0x36, 0x56, 0x1C, 0x50, 0x2A, 0x0E, 0x60, 0x58, 0x5A, 0x02, 0x18, 0x38, 0x62, 0x32, 0x0C, 0x54, 0x42,
0x3A, 0x3C, 0x48, 0x5E, 0x1A, 0x44, 0x14, 0x52, 0x36, 0x24, 0x1C, 0x1E, 0x2A, 0x40, 0x60, 0x26, 0x5A, 0x34, 0x18, 0x06, 0x62,
0x00, 0x0C, 0x22, 0x42, 0x08, 0x3C, 0x16, 0x5E, 0x4C, 0x44, 0x46, 0x52, 0x04, 0x24, 0x4E, 0x1E, 0x5C, 0x40, 0x2E, 0x26, 0x28,
0x34, 0x4A, 0x06, 0x30, 0x00, 0x3E, 0x22, 0x10, 0x08, 0x0A, 0x16, 0x2C, 0x4C, 0x12, 0x46, 0x20, 0x04, 0x56, 0x4E, 0x50, 0x5C,
0x0E, 0x2E, 0x58, 0x28, 0x02, 0x4A, 0x38, 0x30, 0x32, 0x3E, 0x54, 0x10, 0x3A, 0x0A, 0x48, 0x2C, 0x1A, 0x12, 0x14, 0x20, 0x36,
0x56, 0x1C, 0x50, 0x2A, 0x0E, 0x60, 0x58, 0x5A, 0x02, 0x18, 0x38, 0x62, 0x32, 0x0C, 0x54, 0x42, 0x3A, 0x3C, 0x48, 0x5E, 0x1A,
0x44, 0x14, 0x52, 0x36, 0x24, 0x1C, 0x1E, 0x2A, 0x40, 0x60, 0x26, 0x5A, 0x34, 0x18, 0x06, 0x62, 0x00, 0x0C, 0x22, 0x42, 0x08,
0x3C, 0x16, 0x5E, 0x4C, 0x44, 0x46, 0x52, 0x04, 0x24, 0x4E, 0x1E, 0x5C, 0x40, 0x2E, 0x26, 0x28, 0x34, 0x4A, 0x06, 0x30, 0x00,
0x3E, 0x22, 0x10, 0x08, 0x0A, 0x16, 0x2C, 0x4C, 0x12, 0x46, 0x20, 0x04, 0x56, 0x4E, 0x50, 0x5C, 0x0E, 0x2E, 0x58, 0x28, 0x02,
0x4A, 0x38, 0x30, 0x32, 0x3E, 0x54, 0x10, 0x3A, 0x0A, 0x48, 0x2C, 0x1A, 0x12, 0x14, 0x20, 0x36, 0x56, 0x1C, 0x50, 0x2A, 0x0E,
0x60, 0x58, 0x5A, 0x02, 0x18, 0x38, 0x62, 0x32, 0x0C, 0x54, 0x42, 0x3A, 0x3C, 0x48, 0x5E, 0x1A, 0x44, 0x14, 0x52, 0x36, 0x24,
0x1C, 0x1E, 0x2A, 0x40, 0x60, 0x26, 0x5A, 0x34, 0x18, 0x06, 0x62, 0x00, 0x0C, 0x22, 0x42, 0x08, 0x3C, 0x16, 0x5E, 0x4C, 0x44,
0x46, 0x52, 0x04, 0x24, 0x4E, 0x1E, 0x5C, 0x40, 0x2E, 0x26, 0x28, 0x34, 0x4A, 0x06, 0x30, 0x00, 0x3E, 0x22, 0x10, 0x08, 0x0A,
0x16, 0x2C, 0x4C, 0x12, 0x46, 0x20, 0x04, 0x56, 0x4E, 0x50, 0x5C, 0x0E, 0x2E, 0x58, 0x28, 0x02, 0x4A, 0x38, 0x30, 0x32, 0x3E,
0x54, 0x10, 0x3A, 0x0A, 0x48, 0x2C, 0x1A, 0x12, 0x14, 0x20, 0x36, 0x56, 0x1C, 0x50, 0x2A, 0x0E, 0x60, 0x58, 0x5A, 0x02, 0x18,
0x38, 0x62, 0x32, 0x0C, 0x54, 0x42, 0x3A, 0x3C, 0x48, 0x5E, 0x1A, 0x44, 0x14, 0x52, 0x36, 0x24, 0x1C, 0x1E, 0x2A, 0x40, 0x60,
0x26, 0x5A, 0x34, 0x18, 0x06, 0x62, 0x00, 0x0C, 0x22, 0x42, 0x08, 0x3C, 0x16, 0x5E, 0x4C, 0x44, 0x46, 0x52, 0x04, 0x24, 0x4E,
0x1E, 0x5C, 0x40, 0x2E, 0x26, 0x28, 0x34, 0x4A, 0x06, 0x30, 0x06, 0x00, 0x0A, 0x64, 0x6F, 0x75, 0x62, 0x6C, 0x65, 0x54, 0x65,
0x73, 0x74, 0x3F, 0xDF, 0x8F, 0x6B, 0xBB, 0xFF, 0x6A, 0x5E, 0x00 // data from https://wiki.vg/NBT#bigtest.nbt (decompressed)
};
ReadIterator iter = data.begin();
size_t length = data.size();
NBT::Value nbt = ReadData<NBT::Value>(iter, length);
SECTION("General")
{
CHECK(nbt.HasData());
CHECK(nbt.GetName() == "Level");
CHECK(nbt.is<NBT::TagCompound>());
CHECK(nbt.size() == 11);
}
SECTION("Compound")
{
CHECK(nbt["nested compound test"].is<NBT::TagCompound>());
CHECK(nbt["nested compound test"].size() == 2);
}
SECTION("Int")
{
CHECK(nbt["intTest"].is<NBT::TagInt>());
CHECK(nbt["intTest"].get<int>() == 2147483647);
CHECK(nbt["intTest"].get<NBT::TagInt>() == 2147483647);
}
SECTION("Byte")
{
CHECK(nbt["byteTest"].is<NBT::TagByte>());
CHECK(nbt["byteTest"].get<NBT::TagByte>() == 127);
}
SECTION("String")
{
CHECK(nbt["stringTest"].is<NBT::TagString>());
CHECK(nbt["stringTest"].get<NBT::TagString>().size() == 41);
CHECK(nbt["stringTest"].get<NBT::TagString>().substr(0, 11) == "HELLO WORLD");
}
SECTION("List")
{
CHECK(nbt["listTest (long)"].is<NBT::TagList>());
CHECK(nbt["listTest (long)"].is_list_of<NBT::TagLong>());
CHECK(nbt["listTest (long)"].get<NBT::TagList>().size() == 5);
CHECK(nbt["listTest (long)"].as_list_of<NBT::TagLong>()[3] == 14);
CHECK(nbt["listTest (long)"].get<NBT::TagList>().is_of<NBT::TagLong>());
CHECK(nbt["listTest (compound)"].is<NBT::TagList>());
CHECK(nbt["listTest (compound)"].get<NBT::TagList>().size() == 2);
}
SECTION("Double")
{
CHECK(nbt["doubleTest"].is<NBT::TagDouble>());
CHECK(nbt["doubleTest"].get<NBT::TagDouble>() == 0.49312871321823148);
}
SECTION("Float")
{
CHECK(nbt["floatTest"].is<NBT::TagFloat>());
CHECK(nbt["floatTest"].get<NBT::TagFloat>() == 0.49823147058486938f);
}
SECTION("Long")
{
CHECK(nbt["longTest"].is<NBT::TagLong>());
CHECK(nbt["longTest"].get<NBT::TagLong>() == 9223372036854775807L);
}
SECTION("ByteArray")
{
CHECK(nbt["byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))"].is<NBT::TagByteArray>());
CHECK(nbt["byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))"].get<NBT::TagByteArray>().size() == 1000);
CHECK(nbt["byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))"].get<NBT::TagByteArray>()[2] == 34);
}
SECTION("Short")
{
CHECK(nbt["shortTest"].is<NBT::TagShort>());
CHECK(nbt["shortTest"].get<NBT::TagShort>() == 32767);
}
SECTION("Nested")
{
CHECK(nbt["nested compound test"]["egg"]["name"].is<std::string>());
CHECK(nbt["nested compound test"]["egg"]["name"].get<std::string>() == "Eggbert");
CHECK(nbt["nested compound test"]["ham"]["value"].is<float>());
CHECK(nbt["nested compound test"]["ham"]["value"].get<float>() == 0.75f);
CHECK(nbt["listTest (compound)"].as_list_of<NBT::TagCompound>()[1]["created-on"].is<NBT::TagLong>());
CHECK(nbt["listTest (compound)"].as_list_of<NBT::TagCompound>()[1]["created-on"].get<NBT::TagLong>() == 1264099775885L);
}
}
TEST_CASE("Compressed bigtest nbt")
{
std::vector<unsigned char> data = {
0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xED, 0x54, 0xCF, 0x4F, 0x1A, 0x41, 0x14, 0x7E, 0xC2, 0x02, 0xCB,
0x96, 0x82, 0xB1, 0xC4, 0x10, 0x63, 0xCC, 0xAB, 0xB5, 0x84, 0xA5, 0xDB, 0xCD, 0x42, 0x11, 0x89, 0xB1, 0x88, 0x16, 0x2C, 0x9A,
0x0D, 0x1A, 0xD8, 0xA8, 0x31, 0x86, 0xB8, 0x2B, 0xC3, 0x82, 0x2E, 0xBB, 0x66, 0x77, 0xB0, 0xF1, 0xD4, 0x4B, 0x7B, 0x6C, 0x7A,
0xEB, 0x3F, 0xD3, 0x23, 0x7F, 0x43, 0xCF, 0xBD, 0xF6, 0xBF, 0xA0, 0xC3, 0x2F, 0x7B, 0x69, 0xCF, 0xBD, 0xF0, 0x32, 0xC9, 0xF7,
0xE6, 0xBD, 0x6F, 0xE6, 0x7B, 0x6F, 0x26, 0x79, 0x02, 0x04, 0x54, 0x72, 0x4F, 0x2C, 0x0E, 0x78, 0xCB, 0xB1, 0x4D, 0x8D, 0x78,
0xF4, 0xE3, 0x70, 0x62, 0x3E, 0x08, 0x7B, 0x1D, 0xC7, 0xA5, 0x93, 0x18, 0x0F, 0x82, 0x47, 0xDD, 0xEE, 0x84, 0x02, 0x62, 0xB5,
0xA2, 0xAA, 0xC7, 0x78, 0x76, 0x5C, 0x57, 0xCB, 0xA8, 0x55, 0x0F, 0x1B, 0xC8, 0xD6, 0x1E, 0x6A, 0x95, 0x86, 0x86, 0x0D, 0xAD,
0x7E, 0x58, 0x7B, 0x8F, 0x83, 0xCF, 0x83, 0x4F, 0x83, 0x6F, 0xCF, 0x03, 0x10, 0x6E, 0x5B, 0x8E, 0x3E, 0xBE, 0xA5, 0x38, 0x4C,
0x64, 0xFD, 0x10, 0xEA, 0xDA, 0x74, 0xA6, 0x23, 0x40, 0xDC, 0x66, 0x2E, 0x69, 0xE1, 0xB5, 0xD3, 0xBB, 0x73, 0xFA, 0x76, 0x0B,
0x29, 0xDB, 0x0B, 0xE0, 0xEF, 0xE8, 0x3D, 0x1E, 0x38, 0x5B, 0xEF, 0x11, 0x08, 0x56, 0xF5, 0xDE, 0x5D, 0xDF, 0x0B, 0x40, 0xE0,
0x5E, 0xB7, 0xFA, 0x64, 0xB7, 0x04, 0x00, 0x8C, 0x41, 0x4C, 0x73, 0xC6, 0x08, 0x55, 0x4C, 0xD3, 0x20, 0x2E, 0x7D, 0xA4, 0xC0,
0xC8, 0xC2, 0x10, 0xB3, 0xBA, 0xDE, 0x58, 0x0B, 0x53, 0xA3, 0xEE, 0x44, 0x8E, 0x45, 0x03, 0x30, 0xB1, 0x27, 0x53, 0x8C, 0x4C,
0xF1, 0xE9, 0x14, 0xA3, 0x53, 0x8C, 0x85, 0xE1, 0xD9, 0x9F, 0xE3, 0xB3, 0xF2, 0x44, 0x81, 0xA5, 0x7C, 0x33, 0xDD, 0xD8, 0xBB,
0xC7, 0xAA, 0x75, 0x13, 0x5F, 0x28, 0x1C, 0x08, 0xD7, 0x2E, 0xD1, 0x59, 0x3F, 0xAF, 0x1D, 0x1B, 0x60, 0x21, 0x59, 0xDF, 0xFA,
0xF1, 0x05, 0xFE, 0xC1, 0xCE, 0xFC, 0x9D, 0xBD, 0x00, 0xBC, 0xF1, 0x40, 0xC9, 0xF8, 0x85, 0x42, 0x40, 0x46, 0xFE, 0x9E, 0xEB,
0xEA, 0x0F, 0x93, 0x3A, 0x68, 0x87, 0x60, 0xBB, 0xEB, 0x32, 0x37, 0xA3, 0x28, 0x0A, 0x8E, 0xBB, 0xF5, 0xD0, 0x69, 0x63, 0xCA,
0x4E, 0xDB, 0xE9, 0xEC, 0xE6, 0xE6, 0x2B, 0x3B, 0xBD, 0x25, 0xBE, 0x64, 0x49, 0x09, 0x3D, 0xAA, 0xBB, 0x94, 0xFD, 0x18, 0x7E,
0xE8, 0xD2, 0x0E, 0xDA, 0x6F, 0x15, 0x4C, 0xB1, 0x68, 0x3E, 0x2B, 0xE1, 0x9B, 0x9C, 0x84, 0x99, 0xBC, 0x84, 0x05, 0x09, 0x65,
0x59, 0x16, 0x45, 0x00, 0xFF, 0x2F, 0x28, 0xAE, 0x2F, 0xF2, 0xC2, 0xB2, 0xA4, 0x2E, 0x1D, 0x20, 0x77, 0x5A, 0x3B, 0xB9, 0x8C,
0xCA, 0xE7, 0x29, 0xDF, 0x51, 0x41, 0xC9, 0x16, 0xB5, 0xC5, 0x6D, 0xA1, 0x2A, 0xAD, 0x2C, 0xC5, 0x31, 0x7F, 0xBA, 0x7A, 0x92,
0x8E, 0x5E, 0x9D, 0x5F, 0xF8, 0x12, 0x05, 0x23, 0x1B, 0xD1, 0xF6, 0xB7, 0x77, 0xAA, 0xCD, 0x95, 0x72, 0xBC, 0x9E, 0xDF, 0x58,
0x5D, 0x4B, 0x97, 0xAE, 0x92, 0x17, 0xB9, 0x44, 0xD0, 0x80, 0xC8, 0xFA, 0x3E, 0xBF, 0xB3, 0xDC, 0x54, 0xCB, 0x07, 0x75, 0x6E,
0xA3, 0xB6, 0x76, 0x59, 0x92, 0x93, 0xA9, 0xDC, 0x51, 0x50, 0x99, 0x6B, 0xCC, 0x35, 0xE6, 0x1A, 0xFF, 0x57, 0x23, 0x08, 0x42,
0xCB, 0xE9, 0x1B, 0xD6, 0x78, 0xC2, 0xEC, 0xFE, 0xFC, 0x7A, 0xFB, 0x7D, 0x78, 0xD3, 0x84, 0xDF, 0xD4, 0xF2, 0xA4, 0xFB, 0x08,
0x06, 0x00, 0x00 // data from https://wiki.vg/NBT#bigtest.nbt
};
ReadIterator iter = data.begin();
size_t length = data.size();
#if !USE_COMPRESSION
CHECK_THROWS(ReadData<NBT::Value>(iter, length));
#else
NBT::Value nbt = ReadData<NBT::Value>(iter, length);
SECTION("General")
{
CHECK(nbt.HasData());
CHECK(nbt.GetName() == "Level");
CHECK(nbt.is<NBT::TagCompound>());
CHECK(nbt.size() == 11);
}
SECTION("Compound")
{
CHECK(nbt["nested compound test"].is<NBT::TagCompound>());
CHECK(nbt["nested compound test"].size() == 2);
}
SECTION("Int")
{
CHECK(nbt["intTest"].is<NBT::TagInt>());
CHECK(nbt["intTest"].get<int>() == 2147483647);
CHECK(nbt["intTest"].get<NBT::TagInt>() == 2147483647);
}
SECTION("Byte")
{
CHECK(nbt["byteTest"].is<NBT::TagByte>());
CHECK(nbt["byteTest"].get<NBT::TagByte>() == 127);
}
SECTION("String")
{
CHECK(nbt["stringTest"].is<NBT::TagString>());
CHECK(nbt["stringTest"].get<NBT::TagString>().size() == 41);
CHECK(nbt["stringTest"].get<NBT::TagString>().substr(0, 11) == "HELLO WORLD");
}
SECTION("List")
{
CHECK(nbt["listTest (long)"].is<NBT::TagList>());
CHECK(nbt["listTest (long)"].is_list_of<NBT::TagLong>());
CHECK(nbt["listTest (long)"].get<NBT::TagList>().size() == 5);
CHECK(nbt["listTest (long)"].as_list_of<NBT::TagLong>()[3] == 14);
CHECK(nbt["listTest (long)"].get<NBT::TagList>().is_of<NBT::TagLong>());
CHECK(nbt["listTest (compound)"].is<NBT::TagList>());
CHECK(nbt["listTest (compound)"].get<NBT::TagList>().size() == 2);
}
SECTION("Double")
{
CHECK(nbt["doubleTest"].is<NBT::TagDouble>());
CHECK(nbt["doubleTest"].get<NBT::TagDouble>() == 0.49312871321823148);
}
SECTION("Float")
{
CHECK(nbt["floatTest"].is<NBT::TagFloat>());
CHECK(nbt["floatTest"].get<NBT::TagFloat>() == 0.49823147058486938f);
}
SECTION("Long")
{
CHECK(nbt["longTest"].is<NBT::TagLong>());
CHECK(nbt["longTest"].get<NBT::TagLong>() == 9223372036854775807L);
}
SECTION("ByteArray")
{
CHECK(nbt["byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))"].is<NBT::TagByteArray>());
CHECK(nbt["byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))"].get<NBT::TagByteArray>().size() == 1000);
CHECK(nbt["byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))"].get<NBT::TagByteArray>()[2] == 34);
}
SECTION("Short")
{
CHECK(nbt["shortTest"].is<NBT::TagShort>());
CHECK(nbt["shortTest"].get<NBT::TagShort>() == 32767);
}
SECTION("Nested")
{
CHECK(nbt["nested compound test"]["egg"]["name"].is<std::string>());
CHECK(nbt["nested compound test"]["egg"]["name"].get<std::string>() == "Eggbert");
CHECK(nbt["nested compound test"]["ham"]["value"].is<float>());
CHECK(nbt["nested compound test"]["ham"]["value"].get<float>() == 0.75f);
CHECK(nbt["listTest (compound)"].as_list_of<NBT::TagCompound>()[1]["created-on"].is<NBT::TagLong>());
CHECK(nbt["listTest (compound)"].as_list_of<NBT::TagCompound>()[1]["created-on"].get<NBT::TagLong>() == 1264099775885L);
}
#endif
}
TEST_CASE("Unnamed NBT")
{
std::vector<unsigned char> data = {
0x0A, // TagCompound
#if PROTOCOL_VERSION < 764 /* < 1.20.2 */
0x00, 0x0B, // Name length
0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, // Name
#endif
0x08, // TagString
0x00, 0x04, // Name length
0x6E, 0x61, 0x6D, 0x65, // Name
0x00, 0x09, // String length
0x42, 0x61, 0x6E, 0x61, 0x6E, 0x72, 0x61, 0x6D, 0x61, // String content
0x00 // TagEnd
};
ReadIterator iter = data.begin();
size_t length = data.size();
NBT::Value nbt = ReadData<NBT::UnnamedValue>(iter, length);
CHECK(nbt.HasData());
CHECK(nbt.is<NBT::TagCompound>());
CHECK(nbt.size() == 1);
CHECK(nbt.GetName() == "");
CHECK(nbt["name"].is<NBT::TagString>());
CHECK(nbt["name"].get<NBT::TagString>() == "Bananrama");
std::vector<unsigned char> serialized;
serialized.reserve(data.size());
WriteData<NBT::UnnamedValue>(nbt, serialized);
// Name has been removed
#if PROTOCOL_VERSION < 764 /* < 1.20.2 */
CHECK(serialized.size() == data.size() - 11);
#else
CHECK(serialized == data);
#endif
}