Initial commit

This commit is contained in:
Astatin3
2024-04-30 22:07:50 -06:00
commit 8565caa62a
8463 changed files with 4915934 additions and 0 deletions
@@ -0,0 +1,14 @@
project(4_MapCreatorExample)
set(${PROJECT_NAME}_SOURCE_FILES
${PROJECT_SOURCE_DIR}/include/MapCreationTasks.hpp
${PROJECT_SOURCE_DIR}/include/CustomBehaviourTree.hpp
${PROJECT_SOURCE_DIR}/src/MapCreationTasks.cpp
${PROJECT_SOURCE_DIR}/src/main.cpp
)
set(${PROJECT_NAME}_INCLUDE_FOLDERS
${PROJECT_SOURCE_DIR}/include
)
add_example("${${PROJECT_NAME}_INCLUDE_FOLDERS}" "${${PROJECT_NAME}_SOURCE_FILES}")
@@ -0,0 +1,31 @@
#pragma once
#include "botcraft/AI/BehaviourTree.hpp"
/// @brief A Decorator that ticks its child until
/// it successes. If ticked n times with no
/// success, return failure.
/// @tparam Context The tree context type
template<class Context>
class RepeatUntilSuccess : public Botcraft::Decorator<Context>
{
public:
RepeatUntilSuccess(const std::string& s, const size_t n_) : Botcraft::Decorator<Context>(s), n(n_) {}
protected:
virtual Botcraft::Status TickImpl(Context& context) const override
{
for (size_t i = 0; i < n; ++i)
{
Botcraft::Status child_return = this->TickChild(context);
if (child_return == Botcraft::Status::Success)
{
return Botcraft::Status::Success;
}
}
return Botcraft::Status::Failure;
}
private:
size_t n;
};
@@ -0,0 +1,60 @@
#pragma once
#include <string>
#include "botcraft/AI/BehaviourClient.hpp"
#include "botcraft/AI/Status.hpp"
#include "botcraft/Game/Vector3.hpp"
/// @brief Check all blocks nearby for chests. Save the found positions in World.ChestsPos
/// @param c The client performing the action
/// @return Always return Success
Botcraft::Status GetAllChestsAround(Botcraft::BehaviourClient& c);
/// @brief Search in nearby chests for a stack of food, put it in the inventory when found.
/// @param c The client performing the action
/// @param food_name The food item name
/// @return Success if the given food is in inventory, Failure otherwise
Botcraft::Status GetSomeFood(Botcraft::BehaviourClient& c, const std::string& food_name);
/// @brief Get list of blocks in the inventory (ignoring the offhand), store it in the blackboard at Inventory.block_list
/// @param c The client performing the action
/// @return Success if at least one block, Failure otherwise
Botcraft::Status GetBlocksAvailableInInventory(Botcraft::BehaviourClient& c);
/// @brief Take or deposit items in nearby chests
/// @param c The client performing the action
/// @param food_name Food item you don't want to take/deposit
/// @param take_from_chest If true, take items from chests, if false deposit items
/// @return Success if all the items were deposited/the inventory is full, Failure otherwise
Botcraft::Status SwapChestsInventory(Botcraft::BehaviourClient& c, const std::string& food_name, const bool take_from_chest);
/// @brief Read block list in blackboard at Inventory.block_list and fill in NextTask.action, NextTask.pos, NextTask.face and NextTask.item
/// @param c The client performing the action
/// @return success if a task was found, failure otherwise
Botcraft::Status FindNextTask(Botcraft::BehaviourClient& c);
/// @brief Read the blackboard NextTask.XXX values, and perform the given task
/// @param c The client performing the action
/// @return Success if the task was correctly executed, failure otherwise
Botcraft::Status ExecuteNextTask(Botcraft::BehaviourClient& c);
/// @brief Check if the whole structure is built, check in the blackboard for CheckCompletion.(print_details, print_errors and full_check) to know if details should be displayed in the console
/// @param c The client performing the action
/// @return Success if the structure is fully built, failure otherwise
Botcraft::Status CheckCompletion(Botcraft::BehaviourClient& c);
/// @brief Write a message in the console, prefixed with bot name
/// @param c The client performing the action
/// @param msg The string to write in the console
/// @return Always return success
Botcraft::Status WarnConsole(Botcraft::BehaviourClient& c, const std::string& msg);
/// @brief Loads a NBT file (unzipped) and store the target structure in the blackboard of the given client
/// @param c The client performing the action
/// @param path The path to the unzipped NBT file
/// @param offset Starting offset position to build the structure
/// @param temp_block Minecraft item/block name used as scaffolding block in the NBT
/// @param log_info Whether or not the loading information should be logged
/// @return Success if the file was correctly loaded, failure otherwise
Botcraft::Status LoadNBT(Botcraft::BehaviourClient& c, const std::string& path, const Botcraft::Position& offset, const std::string& temp_block, const bool log_info);
@@ -0,0 +1,924 @@
#include "MapCreationTasks.hpp"
#include "botcraft/Network/NetworkManager.hpp"
#include "botcraft/Game/AssetsManager.hpp"
#include "botcraft/Game/Entities/EntityManager.hpp"
#include "botcraft/Game/Entities/LocalPlayer.hpp"
#include "botcraft/Game/World/World.hpp"
#include "botcraft/Game/Vector3.hpp"
#include "botcraft/Game/Inventory/InventoryManager.hpp"
#include "botcraft/Game/Inventory/Window.hpp"
#include "botcraft/AI/Tasks/AllTasks.hpp"
#include "botcraft/Utilities/Logger.hpp"
#include <fstream>
#include <iostream>
#include <iterator>
#include <random>
#include <set>
#include <unordered_set>
using namespace Botcraft;
using namespace ProtocolCraft;
Status GetAllChestsAround(BehaviourClient& c)
{
std::vector<Position> chests_pos;
std::shared_ptr<LocalPlayer> local_player = c.GetLocalPlayer();
std::shared_ptr<World> world = c.GetWorld();
const Position player_position(
static_cast<int>(std::floor(local_player->GetX())),
static_cast<int>(std::floor(local_player->GetY())),
static_cast<int>(std::floor(local_player->GetZ()))
);
Position checked_position;
{
auto all_chunks = world->GetChunks();
for (auto it = all_chunks->begin(); it != all_chunks->end(); ++it)
{
for (int x = 0; x < CHUNK_WIDTH; ++x)
{
checked_position.x = x;
for (int y = 0; y < it->second.GetHeight(); ++y)
{
checked_position.y = y + it->second.GetMinY();
for (int z = 0; z < CHUNK_WIDTH; ++z)
{
checked_position.z = z;
const Blockstate* block = it->second.GetBlock(checked_position);
if (block != nullptr && block->GetName() == "minecraft:chest")
{
const Position world_position(
it->first.first * CHUNK_WIDTH + x,
y + world->GetMinY(),
it->first.second * CHUNK_WIDTH + z
);
chests_pos.push_back(world_position);
}
}
}
}
}
}
c.GetBlackboard().Set("World.ChestsPos", chests_pos);
return Status::Success;
}
Status GetSomeFood(BehaviourClient& c, const std::string& food_name)
{
std::shared_ptr<InventoryManager> inventory_manager = c.GetInventoryManager();
GetAllChestsAround(c);
const std::vector<Position>& chests = c.GetBlackboard().Get<std::vector<Position>>("World.ChestsPos");
std::vector<size_t> chests_indices(chests.size());
for (size_t i = 0; i < chests.size(); ++i)
{
chests_indices[i] = i;
}
std::mt19937 random_engine = std::mt19937(static_cast<unsigned int>(std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count()));
std::shuffle(chests_indices.begin(), chests_indices.end(), random_engine);
short container_id;
bool item_taken = false;
for (size_t index = 0; index < chests.size(); ++index)
{
const size_t i = chests_indices[index];
// If we can't open this chest for a reason
if (OpenContainer(c, chests[i]) == Status::Failure)
{
continue;
}
short player_dst = -1;
while (true)
{
std::vector<short> slots_src;
container_id = inventory_manager->GetFirstOpenedWindowId();
if (container_id == -1)
{
continue;
}
const std::shared_ptr<Window> container = inventory_manager->GetWindow(container_id);
if (container == nullptr)
{
continue;
}
const short first_player_index = container->GetFirstPlayerInventorySlot();
player_dst = first_player_index + 9 * 3;
const std::map<short, Slot> slots = container->GetSlots();
slots_src.reserve(slots.size());
for (const auto& [id, slot] : slots)
{
// Chest is src
if (id >= 0
&& id < first_player_index
&& !slot.IsEmptySlot()
&& AssetsManager::getInstance().Items().at(slot.GetItemID())->GetName() == food_name
)
{
slots_src.push_back(id);
}
}
if (slots_src.size() > 0)
{
// Select a random slot in both src and dst
const int src_index = slots_src.size() == 1 ? 0 : std::uniform_int_distribution<int>(0, static_cast<int>(slots_src.size()) - 1)(random_engine);
// Try to swap the items
if (SwapItemsInContainer(c, container_id, slots_src[src_index], player_dst) == Status::Success)
{
item_taken = true;
break;
}
}
// This means the chest doesn't have any food
else
{
break;
}
}
CloseContainer(c, container_id);
if (!item_taken)
{
continue;
}
// Wait until player inventory is updated after the container is closed
auto start = std::chrono::steady_clock::now();
while (AssetsManager::getInstance().Items().at(inventory_manager->GetPlayerInventory()->GetSlot(Window::INVENTORY_HOTBAR_START).GetItemID())->GetName() != food_name)
{
if (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count() >= 10000)
{
LOG_WARNING("Something went wrong trying to get food from chest (Timeout).");
return Status::Failure;
}
c.Yield();
}
// No need to continue loooking in the other chests
break;
}
return item_taken ? Status::Success : Status::Failure;
}
Status GetBlocksAvailableInInventory(BehaviourClient& c)
{
std::shared_ptr<InventoryManager> inventory_manager = c.GetInventoryManager();
std::set<std::string> blocks_in_inventory;
for (const auto& [idx, slot] : inventory_manager->GetPlayerInventory()->GetSlots())
{
if (idx >= Window::INVENTORY_STORAGE_START &&
idx < Window::INVENTORY_OFFHAND_INDEX &&
!slot.IsEmptySlot())
{
blocks_in_inventory.insert(AssetsManager::getInstance().Items().at(slot.GetItemID())->GetName());
}
}
c.GetBlackboard().Set("Inventory.block_list", blocks_in_inventory);
return blocks_in_inventory.size() > 0 ? Status::Success : Status::Failure;
}
Status SwapChestsInventory(BehaviourClient& c, const std::string& food_name, const bool take_from_chest)
{
std::shared_ptr<InventoryManager> inventory_manager = c.GetInventoryManager();
GetAllChestsAround(c);
const std::vector<Position>& chests = c.GetBlackboard().Get<std::vector<Position>>("World.ChestsPos");
std::vector<size_t> chest_indices(chests.size());
for (size_t i = 0; i < chests.size(); ++i)
{
chest_indices[i] = i;
}
std::mt19937 random_engine(static_cast<unsigned int>(std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count()));
while (true)
{
// We checked all the chests
if (chest_indices.size() == 0)
{
return Status::Success;
}
// Select a chest
size_t chest_index_index = chest_indices.size() == 1 ? 0 : std::uniform_int_distribution<int>(0, static_cast<int>(chest_indices.size()) - 1)(random_engine);
size_t chest_index = chest_indices[chest_index_index];
// If we can't open this chest for a reason
if (OpenContainer(c, chests[chest_index]) == Status::Failure)
{
continue;
}
// Find possible swaps
const short container_id = inventory_manager->GetFirstOpenedWindowId();
if (container_id == -1)
{
continue;
}
const std::shared_ptr<Window> container = inventory_manager->GetWindow(container_id);
if (container == nullptr)
{
continue;
}
const short first_player_index = (static_cast<int>(container->GetType()) + 1) * 9;
const std::map<short, Slot> slots = container->GetSlots();
std::vector<short> slots_src;
slots_src.reserve(slots.size());
std::vector<short> slots_dst;
slots_dst.reserve(slots.size());
for (const auto& [idx, slot] : slots)
{
// If take, chest is src
if (idx >= 0
&& idx < first_player_index
&& take_from_chest
&& !slot.IsEmptySlot()
&& AssetsManager::getInstance().Items().at(slot.GetItemID())->GetName() != food_name)
{
slots_src.push_back(idx);
}
// If take, player is dst
else if (idx >= first_player_index
&& take_from_chest
&& slot.IsEmptySlot())
{
slots_dst.push_back(idx);
}
// If !take, chest is dst
else if (idx >= 0
&& idx < first_player_index
&& !take_from_chest
&& slot.IsEmptySlot())
{
slots_dst.push_back(idx);
}
// If !take, player is src
else if (idx >= first_player_index
&& !take_from_chest
&& !slot.IsEmptySlot()
&& AssetsManager::getInstance().Items().at(slot.GetItemID())->GetName() != food_name)
{
slots_src.push_back(idx);
}
}
Status swap_success = Status::Failure;
int dst_index = -1;
int src_index = -1;
if (slots_src.size() > 0 &&
slots_dst.size() > 0)
{
// Select a random slot in both src and dst
dst_index = slots_dst.size() == 1 ? 0 : std::uniform_int_distribution<int>(0, static_cast<int>(slots_dst.size()) - 1)(random_engine);
src_index = slots_src.size() == 1 ? 0 : std::uniform_int_distribution<int>(0, static_cast<int>(slots_src.size()) - 1)(random_engine);
// Try to swap the items
swap_success = SwapItemsInContainer(c, container_id, slots_src[src_index], slots_dst[dst_index]);
}
// Close the chest
CloseContainer(c, container_id);
// The chest was empty/full, remove it from the list
if ((take_from_chest && slots_src.size() == 0) ||
(!take_from_chest && slots_dst.size() == 0))
{
chest_indices.erase(chest_indices.begin() + chest_index_index);
continue;
}
// The player inventory was full/empty, end the function
else if ((take_from_chest && slots_dst.size() == 0) ||
(!take_from_chest && slots_src.size() == 0))
{
return Status::Success;
}
if (swap_success == Status::Failure)
{
continue;
}
// Wait for the confirmation from the server
auto start = std::chrono::steady_clock::now();
const short checked_slot_index = (take_from_chest ? slots_dst[dst_index] : slots_src[src_index]) - first_player_index + Window::INVENTORY_STORAGE_START;
while (true)
{
if (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count() >= 10000)
{
LOG_WARNING("Something went wrong trying to get items from chest (Timeout).");
return Status::Failure;
}
const Slot slot = inventory_manager->GetPlayerInventory()->GetSlot(checked_slot_index);
if ((take_from_chest && !slot.IsEmptySlot()) ||
(!take_from_chest && slot.IsEmptySlot()))
{
break;
}
c.Yield();
}
}
return Status::Success;
}
Status FindNextTask(BehaviourClient& c)
{
Blackboard& blackboard = c.GetBlackboard();
std::shared_ptr<EntityManager> entity_manager = c.GetEntityManager();
std::shared_ptr<World> world = c.GetWorld();
const Position& start = blackboard.Get<Position>("Structure.start");
const Position& end = blackboard.Get<Position>("Structure.end");
const std::vector<std::vector<std::vector<short> > >& target = blackboard.Get<std::vector<std::vector<std::vector<short> > > >("Structure.target");
const std::map<short, std::string>& palette = blackboard.Get<std::map<short, std::string> >("Structure.palette");
const std::set<std::string>& available = blackboard.Get<std::set<std::string> >("Inventory.block_list");
std::mt19937 random_engine(static_cast<unsigned int>(std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count()));
Position start_pos;
start_pos.x = std::min(end.x, std::max(start.x, static_cast<int>(std::floor(entity_manager->GetLocalPlayer()->GetX()))));
start_pos.y = std::min(end.y, std::max(start.y, static_cast<int>(std::floor(entity_manager->GetLocalPlayer()->GetY()))));
start_pos.z = std::min(end.z, std::max(start.z, static_cast<int>(std::floor(entity_manager->GetLocalPlayer()->GetZ()))));
std::unordered_set<Position> explored;
std::unordered_set<Position> to_explore;
const std::vector<Position> neighbour_offsets({ Position(0, 1, 0), Position(0, -1, 0),
Position(0, 0, 1), Position(0, 0, -1),
Position(1, 0, 0), Position(-1, 0, 0) });
to_explore.insert(start_pos);
std::vector<Position> pos_candidates;
std::vector<std::string> item_candidates;
std::vector<PlayerDiggingFace> face_candidates;
while (!to_explore.empty())
{
// For each candidate, check if
// 1) the target is not air
// 2) we have the correct block in the inventory
// 3) it is currently a free space
// 4) it has a block under or next to it so we can put the new block
// OR
// 1) the placed block is not air
// 2) it does not match the desired build
// 3) it has a free block under or next to it so we can dig it
for (auto it = to_explore.begin(); it != to_explore.end(); ++it)
{
const Position pos = *it;
const int target_palette = target[pos.x - start.x][pos.y - start.y][pos.z - start.z];
const std::string& target_name = palette.at(target_palette);
const Blockstate* blockstate = world->GetBlock(pos);
if (blockstate == nullptr)
{
#if PROTOCOL_VERSION < 347 /* < 1.13 */
blockstate = AssetsManager::getInstance().Blockstates().at(0).at(0).get();
#else
blockstate = AssetsManager::getInstance().Blockstates().at(0).get();
#endif
}
// Empty space requiring block placement
if (target_palette != -1
&& blockstate->IsAir()
&& available.find(target_name) != available.end())
{
for (int i = 0; i < neighbour_offsets.size(); ++i)
{
const Blockstate* neighbour_blockstate = world->GetBlock(pos + neighbour_offsets[i]);
if (neighbour_blockstate != nullptr && !neighbour_blockstate->IsAir())
{
pos_candidates.push_back(pos);
item_candidates.push_back(target_name);
face_candidates.push_back(static_cast<PlayerDiggingFace>(i));
break;
}
}
}
// Wrong block requiring digging
else if ((target_palette != -1 && !blockstate->IsAir() && target_name != blockstate->GetName())
|| (target_palette == -1 && !blockstate->IsAir()))
{
for (int i = 0; i < neighbour_offsets.size(); ++i)
{
const Blockstate* neighbour_block = world->GetBlock(pos + neighbour_offsets[i]);
if (neighbour_block != nullptr && !neighbour_block->IsAir())
{
pos_candidates.push_back(pos);
item_candidates.push_back("");
face_candidates.push_back((PlayerDiggingFace)i);
break;
}
}
}
}
// If we have at least one candidate
if (pos_candidates.size() > 0)
{
// Get the position of all other players
std::vector<Vector3<double> > other_player_pos;
{
auto entities = entity_manager->GetEntities();
for (const auto& [id, entity] : *entities)
{
if (entity->GetType() == EntityType::Player)
{
other_player_pos.push_back(entity ->GetPosition());
}
}
}
// Get all the candidates that are as far as possible from all
// the other players
std::vector<int> max_dist_indices;
double max_dist = 0.0;
for (int i = 0; i < pos_candidates.size(); ++i)
{
double dist = 0.0;
for (int j = 0; j < other_player_pos.size(); ++j)
{
dist += std::abs(pos_candidates[i].x - other_player_pos[j].x) +
std::abs(pos_candidates[i].y - other_player_pos[j].y) +
std::abs(pos_candidates[i].z - other_player_pos[j].z);
if (dist > max_dist)
{
max_dist_indices.clear();
max_dist = dist;
}
if (dist == max_dist)
{
max_dist_indices.push_back(i);
}
}
}
// Select one randomly if multiple possibilities
int selected_index = max_dist_indices.size() == 1 ? 0 : max_dist_indices[std::uniform_int_distribution<int>(0, static_cast<int>(max_dist_indices.size()) - 1)(random_engine)];
blackboard.Set<std::string>("NextTask.action", item_candidates[selected_index].empty() ? "Dig" : "Place");
blackboard.Set("NextTask.block_position", pos_candidates[selected_index]);
blackboard.Set("NextTask.face", face_candidates[selected_index]);
if (!item_candidates[selected_index].empty())
{
blackboard.Set("NextTask.item", item_candidates[selected_index]);
}
return Status::Success;
}
explored.insert(to_explore.begin(), to_explore.end());
std::unordered_set<Position> neighbours;
for (auto it = to_explore.begin(); it != to_explore.end(); ++it)
{
for (int i = 0; i < neighbour_offsets.size(); ++i)
{
const Position p = *it + neighbour_offsets[i];
if (p.x < start.x ||
p.x > end.x ||
p.y < start.y ||
p.y > end.y ||
p.z < start.z ||
p.z > end.z)
{
continue;
}
if (explored.find(p) == explored.end())
{
neighbours.insert(p);
}
}
}
to_explore = neighbours;
}
return Status::Failure;
}
Status ExecuteNextTask(BehaviourClient& c)
{
Blackboard& b = c.GetBlackboard();
const std::string& action = b.Get<std::string>("NextTask.action");
const Position& block_position = b.Get<Position>("NextTask.block_position");
const PlayerDiggingFace face = b.Get<PlayerDiggingFace>("NextTask.face");
if (action == "Dig")
{
return Dig(c, block_position, true, face);
}
else if (action == "Place")
{
const std::string& item_name = b.Get<std::string>("NextTask.item");
return PlaceBlock(c, item_name, block_position, face, true, false);
}
LOG_WARNING("Unknown task in ExecuteNextTask");
return Status::Failure;
}
Status CheckCompletion(BehaviourClient& c)
{
Blackboard& blackboard = c.GetBlackboard();
std::shared_ptr<World> world = c.GetWorld();
Position world_pos;
Position target_pos;
int additional_blocks = 0;
int wrong_blocks = 0;
int missing_blocks = 0;
const Position& start = blackboard.Get<Position>("Structure.start");
const Position& end = blackboard.Get<Position>("Structure.end");
const std::vector<std::vector<std::vector<short> > >& target = blackboard.Get<std::vector<std::vector<std::vector<short> > > >("Structure.target");
const std::map<short, std::string>& palette = blackboard.Get<std::map<short, std::string> >("Structure.palette");
const bool log_details = blackboard.Get<bool>("CheckCompletion.log_details", false);
const bool log_errors = blackboard.Get<bool>("CheckCompletion.log_errors", false);
const bool full_check = blackboard.Get<bool>("CheckCompletion.full_check", false);
//Reset values for the next time
blackboard.Set("CheckCompletion.log_details", false);
blackboard.Set("CheckCompletion.log_errors", false);
blackboard.Set("CheckCompletion.full_check", false);
for (int x = start.x; x <= end.x; ++x)
{
world_pos.x = x;
target_pos.x = x - start.x;
for (int y = start.y; y <= end.y; ++y)
{
world_pos.y = y;
target_pos.y = y - start.y;
for (int z = start.z; z <= end.z; ++z)
{
world_pos.z = z;
target_pos.z = z - start.z;
const short target_id = target[target_pos.x][target_pos.y][target_pos.z];
const Blockstate* blockstate = world->GetBlock(world_pos);
if (blockstate == nullptr)
{
if (target_id != -1)
{
if (!full_check)
{
return Status::Failure;
}
missing_blocks++;
if (log_details && missing_blocks < 100) // Don't print more than 100 missing blocks
{
LOG_INFO("Missing " << palette.at(target_id) << " in " << world_pos);
}
}
continue;
}
if (target_id == -1)
{
if (!blockstate->IsAir())
{
if (!full_check)
{
return Status::Failure;
}
additional_blocks++;
if (log_details)
{
LOG_INFO("Additional " << blockstate->GetName() << " in " << world_pos);
}
}
}
else
{
if (blockstate->IsAir())
{
if (!full_check)
{
return Status::Failure;
}
missing_blocks++;
if (log_details)
{
LOG_INFO("Missing " << palette.at(target_id) << " in " << world_pos);
}
}
else
{
const std::string& target_name = palette.at(target_id);
if (blockstate->GetName() != target_name)
{
if (!full_check)
{
return Status::Failure;
}
wrong_blocks++;
if (log_details)
{
LOG_INFO("Wrong " << blockstate->GetName() << " instead of " << target_name << " in " << world_pos);
}
}
}
}
}
}
}
if (log_errors)
{
LOG_INFO("Wrong blocks: " << wrong_blocks);
LOG_INFO("Missing blocks: " << missing_blocks);
LOG_INFO("Additional blocks: " << additional_blocks);
}
return (missing_blocks + additional_blocks + wrong_blocks == 0) ? Status::Success : Status::Failure;
}
Status WarnConsole(BehaviourClient& c, const std::string& msg)
{
LOG_WARNING("[" << c.GetNetworkManager()->GetMyName() << "]: " << msg);
return Status::Success;
}
Status LoadNBT(BehaviourClient& c, const std::string& path, const Position& offset, const std::string& temp_block, const bool log_info)
{
NBT::Value loaded_file;
try
{
std::ifstream infile(path, std::ios_base::binary);
infile.unsetf(std::ios::skipws);
infile >> loaded_file;
infile.close();
}
catch (const std::exception& e)
{
LOG_ERROR("Error loading NBT file " << e.what());
return Status::Failure;
}
std::map<short, std::string> palette;
palette[-1] = "minecraft:air";
short id_temp_block = -1;
std::map<short, int> num_blocks_used;
if (!loaded_file.contains("palette") ||
!loaded_file["palette"].is_list_of<NBT::TagCompound>())
{
LOG_ERROR("Error loading NBT file, no palette TagCompound found");
return Status::Failure;
}
const std::vector<NBT::TagCompound>& palette_list = loaded_file["palette"].as_list_of<NBT::TagCompound>();
for (int i = 0; i < palette_list.size(); ++i)
{
const std::string& block_name = palette_list[i]["Name"].get<std::string>();
palette[i] = block_name;
num_blocks_used[i] = 0;
if (block_name == temp_block)
{
id_temp_block = i;
}
}
Position min(std::numeric_limits<int>().max(), std::numeric_limits<int>().max(), std::numeric_limits<int>().max());
Position max(std::numeric_limits<int>().min(), std::numeric_limits<int>().min(), std::numeric_limits<int>().min());
if (!loaded_file.contains("blocks") ||
!loaded_file["blocks"].is_list_of<NBT::TagCompound>())
{
LOG_ERROR("Error loading NBT file, no blocks TagCompound found");
return Status::Failure;
}
const std::vector<NBT::TagCompound>& block_tag = loaded_file["blocks"].as_list_of<NBT::TagCompound>();
for (const auto& c : block_tag)
{
const std::vector<int>& pos_list = c["pos"].as_list_of<int>();
const int x = pos_list[0];
const int y = pos_list[1];
const int z = pos_list[2];
if (x < min.x)
{
min.x = x;
}
if (y < min.y)
{
min.y = y;
}
if (z < min.z)
{
min.z = z;
}
if (x > max.x)
{
max.x = x;
}
if (y > max.y)
{
max.y = y;
}
if (z > max.z)
{
max.z = z;
}
}
Position size = max - min + Position(1, 1, 1);
Position start = offset;
Position end = offset + size - Position(1, 1, 1);
if (log_info)
{
LOG_INFO("Start: " << start << " | " << "End: " << end);
}
// Fill the target area with air (-1)
std::vector<std::vector<std::vector<short> > > target(size.x, std::vector<std::vector<short> >(size.y, std::vector<short>(size.z, -1)));
// Read all block to place
for (const auto& c : block_tag)
{
const int state = c["state"].get<int>();
const std::vector<int>& pos_list = c["pos"].as_list_of<int>();
const int x = pos_list[0];
const int y = pos_list[1];
const int z = pos_list[2];
target[x - min.x][y - min.y][z - min.z] = state;
num_blocks_used[state] += 1;
}
if (id_temp_block == -1)
{
LOG_WARNING("Can't find the given temp block " << temp_block << " in the palette");
}
else
{
int removed_layers = 0;
// Check the bottom Y layers, if only
// air or temp block, the layer can be removed
while (true)
{
bool is_removable = true;
int num_temp_block = 0;
for (int x = 0; x < size.x; ++x)
{
for (int z = 0; z < size.z; z++)
{
if (target[x][0][z] == id_temp_block)
{
num_temp_block += 1;
}
if (target[x][0][z] != -1 &&
target[x][0][z] != id_temp_block)
{
is_removable = false;
break;
}
if (!is_removable)
{
break;
}
}
}
if (!is_removable)
{
break;
}
for (int x = 0; x < size.x; ++x)
{
target[x].erase(target[x].begin());
}
num_blocks_used[id_temp_block] -= num_temp_block;
removed_layers++;
size.y -= 1;
end.y -= 1;
}
if (log_info)
{
LOG_INFO("Removed the bottom " << removed_layers << " layer" << (removed_layers > 1 ? "s" : ""));
}
}
if (log_info)
{
LOG_INFO("Total size: " << size);
std::stringstream needed;
needed << "Block needed:\n";
for (auto it = num_blocks_used.begin(); it != num_blocks_used.end(); ++it)
{
needed << "\t" << palette[it->first] << "\t\t" << it->second << "\n";
}
LOG_INFO(needed.rdbuf());
// Check if some block can't be placed (flying blocks)
std::stringstream flyings;
flyings << "Flying blocks, you might have to place them yourself:\n";
Position target_pos;
const std::vector<Position> neighbour_offsets({ Position(0, 1, 0), Position(0, -1, 0),
Position(0, 0, 1), Position(0, 0, -1),
Position(1, 0, 0), Position(-1, 0, 0) });
for (int x = 0; x < size.x; ++x)
{
target_pos.x = x;
// If this block is on the floor, it's ok
for (int y = 1; y < size.y; ++y)
{
target_pos.y = y;
for (int z = 0; z < size.z; ++z)
{
target_pos.z = z;
const short target_id = target[target_pos.x][target_pos.y][target_pos.z];
if (target_id != -1)
{
// Check all target neighbours
bool has_neighbour = false;
for (int i = 0; i < neighbour_offsets.size(); ++i)
{
const Position neighbour_pos = target_pos + neighbour_offsets[i];
if (neighbour_pos.x >= 0 && neighbour_pos.x < size.x &&
neighbour_pos.y >= 0 && neighbour_pos.y < size.y &&
neighbour_pos.z >= 0 && neighbour_pos.z < size.z &&
target[neighbour_pos.x][neighbour_pos.y][neighbour_pos.z] != -1)
{
has_neighbour = true;
break;
}
}
if (!has_neighbour)
{
flyings << start + target_pos << "\t" << palette[target_id] << "\n";
}
}
}
}
}
LOG_WARNING(flyings.rdbuf());
}
Blackboard& blackboard = c.GetBlackboard();
blackboard.Set("Structure.start", start);
blackboard.Set("Structure.end", end);
blackboard.Set("Structure.target", target);
blackboard.Set("Structure.palette", palette);
blackboard.Set("Structure.loaded", true);
return Status::Success;
}
@@ -0,0 +1,370 @@
#include <iostream>
#include <string>
#include "botcraft/Game/World/World.hpp"
#include "botcraft/AI/BehaviourTree.hpp"
#include "botcraft/AI/Tasks/AllTasks.hpp"
#include "botcraft/AI/SimpleBehaviourClient.hpp"
#include "botcraft/Utilities/Logger.hpp"
#include "botcraft/Utilities/SleepUtilities.hpp"
#include "CustomBehaviourTree.hpp"
#include "MapCreationTasks.hpp"
using namespace Botcraft;
void ShowHelp(const char* argv0)
{
std::cout << "Usage: " << argv0 << " <options>\n"
<< "Options:\n"
<< "\t-h, --help\tShow this help message\n"
<< "\t--address\tAddress of the server you want to connect to, default: 127.0.0.1:25565\n"
<< "\t--numbot\tNumber of parallel bot to start, default: 5\n"
<< "\t--numworld\tNumber of parallel world to use, less worlds saves RAM, but slows the bots down, default: 1\n"
<< "\t--nbt\tnbt filename to load, default: empty\n"
<< "\t--offset\t3 ints, offset for the first block, default: 0 0 0\n"
<< "\t--tempblock\tname of the scaffolding block, default: minecraft:slime_block\n"
<< std::endl;
}
struct Args
{
bool help = false;
std::string address = "127.0.0.1:25565";
int num_bot = 5;
int num_world = 1;
std::string nbt_file = "";
Position offset = Position(0, 0, 0);
std::string temp_block = "minecraft:slime_block";
int return_code = 0;
};
Args ParseCommandLine(int argc, char* argv[]);
std::shared_ptr<BehaviourTree<SimpleBehaviourClient>> GenerateMapArtCreatorTree(const std::string& food_name,
const std::string& nbt_path, const Position& offset, const std::string& temp_block, const bool detailed);
int main(int argc, char* argv[])
{
try
{
// Init logging, log everything >= Info, only to console, no file
Logger::GetInstance().SetLogLevel(LogLevel::Info);
Logger::GetInstance().SetFilename("");
// Add a name to this thread for logging
Logger::GetInstance().RegisterThread("main");
Args args;
if (argc == 1)
{
LOG_WARNING("No command arguments. Using default options.");
ShowHelp(argv[0]);
}
else
{
args = ParseCommandLine(argc, argv);
if (args.help)
{
ShowHelp(argv[0]);
return 0;
}
if (args.return_code != 0)
{
return args.return_code;
}
}
const std::vector<std::string> base_names = { "BotAuFeu", "Botager", "Botiron", "BotEnTouche", "BotDeVin", "BotAuxRoses", "BotronMinet", "Botmobile", "Botman", "Botentiel" };
auto map_art_detailed_behaviour_tree = GenerateMapArtCreatorTree("minecraft:golden_carrot", args.nbt_file, args.offset, args.temp_block, true);
auto map_art_behaviour_tree = GenerateMapArtCreatorTree("minecraft:golden_carrot", args.nbt_file, args.offset, args.temp_block, false);
std::vector<std::shared_ptr<World> > shared_worlds(args.num_world);
for (int i = 0; i < args.num_world; i++)
{
shared_worlds[i] = std::make_shared<World>(true);
}
std::vector<std::string> names(args.num_bot);
std::vector<std::shared_ptr<SimpleBehaviourClient> > clients(args.num_bot);
for (int i = 0; i < args.num_bot; ++i)
{
names[i] = base_names[i] + (i < base_names.size() ? "" : ("_" + std::to_string(i / base_names.size())));
clients[i] = std::make_shared<SimpleBehaviourClient>(false);
clients[i]->SetSharedWorld(shared_worlds[i % args.num_world]);
clients[i]->SetAutoRespawn(true);
clients[i]->Connect(args.address, names[i], false);
clients[i]->StartBehaviour();
clients[i]->SetBehaviourTree(i == 0 ? map_art_detailed_behaviour_tree : map_art_behaviour_tree);
}
std::map<int, std::chrono::steady_clock::time_point> restart_time;
std::chrono::steady_clock::time_point next_time_display = std::chrono::steady_clock::now() + std::chrono::minutes(2);
while (true)
{
// Check if any client should be closed, and schedule the restart 10 seconds later
for (int i = 0; i < clients.size(); ++i)
{
if (clients[i]->GetShouldBeClosed())
{
clients[i]->Disconnect();
clients[i].reset();
clients[i] = std::make_shared<SimpleBehaviourClient>(false);
clients[i]->SetSharedWorld(shared_worlds[i % args.num_world]);
clients[i]->SetAutoRespawn(true);
clients[i]->Connect(args.address, names[i], false);
// Restart client[i] in 10 seconds
LOG_INFO(names[i] << " has been stopped. Scheduling a restart in 10 seconds...");
restart_time[i] = std::chrono::steady_clock::now() + std::chrono::seconds(10);
}
}
// Check if any scheduled restart should happen
auto now = std::chrono::steady_clock::now();
for (auto it = restart_time.begin(); it != restart_time.end(); )
{
if (now > it->second)
{
LOG_INFO("Restarting " << names[it->first] << "...");
clients[it->first]->StartBehaviour();
clients[it->first]->SetBehaviourTree(map_art_behaviour_tree);
restart_time.erase(it++);
}
else
{
++it;
}
}
// Check if we should display progress details
if (now > next_time_display)
{
Blackboard& blackboard = clients[0]->GetBlackboard();
// Set blackboard values to true so the next check completion will print the details
blackboard.Set<bool>("CheckCompletion.print_details", true);
blackboard.Set<bool>("CheckCompletion.print_errors", true);
blackboard.Set<bool>("CheckCompletion.full_check", true);
next_time_display += std::chrono::minutes(2);
}
std::chrono::steady_clock::time_point end = now + std::chrono::milliseconds(10);
for (int i = 0; i < clients.size(); ++i)
{
clients[i]->BehaviourStep();
}
Utilities::SleepUntil(end);
}
return 0;
}
catch (std::exception& e)
{
LOG_FATAL("Exception: " << e.what());
return 1;
}
catch (...)
{
LOG_FATAL("Unknown exception");
return 2;
}
}
Args ParseCommandLine(int argc, char* argv[])
{
Args args;
for (int i = 1; i < argc; ++i)
{
std::string arg = argv[i];
if (arg == "-h" || arg == "--help")
{
ShowHelp(argv[0]);
args.help = true;
return args;
}
else if (arg == "--address")
{
if (i + 1 < argc)
{
args.address = argv[++i];
}
else
{
LOG_FATAL("--address requires an argument");
args.return_code = 1;
return args;
}
}
else if (arg == "--numbot")
{
if (i + 1 < argc)
{
args.num_bot = std::stoi(argv[++i]);
}
else
{
LOG_FATAL("--numbot requires an argument");
args.return_code = 1;
return args;
}
}
else if (arg == "--numworld")
{
if (i + 1 < argc)
{
args.num_world = std::stoi(argv[++i]);
}
else
{
LOG_FATAL("--numworld requires an argument");
args.return_code = 1;
return args;
}
}
else if (arg == "--nbt")
{
if (i + 1 < argc)
{
args.nbt_file = argv[++i];
}
else
{
LOG_FATAL("--nbt requires an argument");
args.return_code = 1;
return args;
}
}
else if (arg == "--offset")
{
if (i + 3 < argc)
{
int x = std::stoi(argv[++i]);
int y = std::stoi(argv[++i]);
int z = std::stoi(argv[++i]);
args.offset = Position(x, y, z);
}
else
{
LOG_FATAL("--offset requires 3 arguments");
args.return_code = 1;
return args;
}
}
else if (arg == "--tempblock")
{
if (i + 1 < argc)
{
args.temp_block = argv[++i];
}
else
{
LOG_FATAL("--tempblock requires an argument");
args.return_code = 1;
return args;
}
}
}
return args;
}
std::shared_ptr<BehaviourTree<SimpleBehaviourClient>> GenerateMapArtCreatorTree(const std::string& food_name,
const std::string& nbt_path, const Position& offset, const std::string& temp_block, const bool detailed)
{
auto loading_tree = Builder<SimpleBehaviourClient>("loading")
.selector()
// Check if the structure is already loaded
.leaf(CheckBlackboardBoolData, "Structure.loaded")
// Otherwise load it
.leaf("load NBT file", LoadNBT, nbt_path, offset, temp_block, detailed)
.end();
auto completion_tree = Builder<SimpleBehaviourClient>("completion check")
.succeeder().sequence()
.leaf("check completion", CheckCompletion)
.leaf(WarnConsole, "Task fully completed!")
.repeater(0).inverter().leaf(Yield)
.end();
auto disconnect_subtree = Builder<SimpleBehaviourClient>("disconnect")
.sequence()
.leaf("disconnect", Disconnect)
.repeater(0).inverter().leaf(Yield)
.end();
auto eat_subtree = Builder<SimpleBehaviourClient>("eat")
.selector()
// If hungry
.inverter().leaf(IsHungry, 20)
// Get some food, then eat
.sequence()
.selector()
.leaf(SetItemInHand, food_name, Hand::Left)
.sequence()
.leaf("get food", GetSomeFood, food_name)
.leaf(SetItemInHand, food_name, Hand::Left)
.end()
.leaf(WarnConsole, "Can't find food anywhere!")
.end()
.selector()
.leaf(Eat, food_name, true)
.inverter().leaf(WarnConsole, "Can't eat!")
// If we are here, hungry and can't eat --> Disconnect
.tree(disconnect_subtree)
.end()
.end()
.end();
auto getinventory_tree = Builder<SimpleBehaviourClient>("list blocks in inventory")
// List all blocks in the inventory
.selector()
.leaf("get block in inventory", GetBlocksAvailableInInventory)
// If no block found, get some in neighbouring chests
.sequence()
.selector()
.leaf("get some blocks from chests", SwapChestsInventory, food_name, true)
.inverter().leaf(WarnConsole, "Can't swap with chests, will wait before retrying.")
// If the previous task failed, maybe chests were just
// not loaded yet, sleep for ~1 second
.inverter().repeater(100).leaf(Yield)
.end()
.selector()
.leaf("get block in inventory after swapping", GetBlocksAvailableInInventory)
.inverter().leaf(WarnConsole, "No more block in chests, I will stop here.")
.tree(disconnect_subtree)
.end()
.end()
.end();
auto placeblock_tree = Builder<SimpleBehaviourClient>("place block")
.selector()
// Try to perform a task 5 times
.decorator<RepeatUntilSuccess<SimpleBehaviourClient>>(5).selector()
.sequence()
.leaf("find next task", FindNextTask)
.leaf("execute next task", ExecuteNextTask)
.end()
// If the previous task failed, sleep for ~1 second
// before retrying to get an action
.inverter().repeater(100).leaf(Yield)
.end()
// If failed 5 times, put all blocks in chests to
// randomize available blocks for next time
.leaf("dump all items in chest", SwapChestsInventory, food_name, false)
.end();
return Builder<SimpleBehaviourClient>("main")
// Main sequence of actions
.sequence()
.tree(loading_tree)
.tree(completion_tree)
.tree(eat_subtree)
.tree(getinventory_tree)
.tree(placeblock_tree)
.end();
}