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,19 @@
project(7_WorldEaterExample)
set(${PROJECT_NAME}_SOURCE_FILES
${PROJECT_SOURCE_DIR}/include/WorldEaterClient.hpp
${PROJECT_SOURCE_DIR}/include/WorldEaterSubTrees.hpp
${PROJECT_SOURCE_DIR}/include/WorldEaterTasks.hpp
${PROJECT_SOURCE_DIR}/include/WorldEaterUtilities.hpp
${PROJECT_SOURCE_DIR}/src/main.cpp
${PROJECT_SOURCE_DIR}/src/WorldEaterClient.cpp
${PROJECT_SOURCE_DIR}/src/WorldEaterSubTrees.cpp
${PROJECT_SOURCE_DIR}/src/WorldEaterTasks.cpp
${PROJECT_SOURCE_DIR}/src/WorldEaterUtilities.cpp
)
set(${PROJECT_NAME}_INCLUDE_FOLDERS
${PROJECT_SOURCE_DIR}/include
)
add_example("${${PROJECT_NAME}_INCLUDE_FOLDERS}" "${${PROJECT_NAME}_SOURCE_FILES}")
@@ -0,0 +1,28 @@
#pragma once
#include "botcraft/AI/SimpleBehaviourClient.hpp"
#include <string>
/// @brief Very simple extension of Botcraft::SimpleBehaviourClient.
/// The only addition is a Handle function to react to a specific
/// word in the chat, which will trigger the pause of the bots.
class WorldEaterClient : public Botcraft::SimpleBehaviourClient
{
public:
WorldEaterClient(const std::string& trigger_word, const bool use_renderer_);
~WorldEaterClient();
protected:
#if PROTOCOL_VERSION < 759 /* < 1.19 */
virtual void Handle(ProtocolCraft::ClientboundChatPacket& msg) override;
#else
virtual void Handle(ProtocolCraft::ClientboundPlayerChatPacket& msg) override;
virtual void Handle(ProtocolCraft::ClientboundSystemChatPacket& msg) override;
#endif
void ProcessChatMsg(const std::string& msg);
private:
std::string word;
};
@@ -0,0 +1,14 @@
#pragma once
#include <botcraft/AI/Tasks/AllTasks.hpp>
#include <botcraft/AI/SimpleBehaviourClient.hpp>
std::shared_ptr<Botcraft::BehaviourTree<Botcraft::SimpleBehaviourClient>> FullTree();
std::shared_ptr<Botcraft::BehaviourTree<Botcraft::SimpleBehaviourClient>> IsDeadTree();
std::shared_ptr<Botcraft::BehaviourTree<Botcraft::SimpleBehaviourClient>> InitTree();
std::shared_ptr<Botcraft::BehaviourTree<Botcraft::SimpleBehaviourClient>> BaseCampResupplyTree();
std::shared_ptr<Botcraft::BehaviourTree<Botcraft::SimpleBehaviourClient>> MainTree();
std::shared_ptr<Botcraft::BehaviourTree<Botcraft::SimpleBehaviourClient>> ActionLoopTree();
std::shared_ptr<Botcraft::BehaviourTree<Botcraft::SimpleBehaviourClient>> CompletionTree();
std::shared_ptr<Botcraft::BehaviourTree<Botcraft::SimpleBehaviourClient>> GoToTopLadderTree();
@@ -0,0 +1,87 @@
#pragma once
#include <botcraft/AI/SimpleBehaviourClient.hpp>
/// @brief Initialize all the blackboard values required to run the tree
/// @param client The client performing the action
/// @return Success if everything was initialize properly, failure otherwise
Botcraft::Status Init(Botcraft::SimpleBehaviourClient& client);
/// @brief Get the next action in the action queue
/// @param client The client performing the action
/// @return Failure if there was no action in the queue, Success otherwise
Botcraft::Status GetNextAction(Botcraft::SimpleBehaviourClient& client);
/// @brief Execute the currently loaded action
/// @param client The client performing the action
/// @return Success if the block was placed/removed, Failure otherwise
Botcraft::Status ExecuteAction(Botcraft::SimpleBehaviourClient& client);
/// @brief Check if the action queue is empty
/// @param client The client performing the action
/// @return Success if the action queue is empty, Failure otherwise
Botcraft::Status CheckActionsDone(Botcraft::SimpleBehaviourClient& client);
/// @brief Check all layers above the current one to see if we didn't forget a block. This should not happen,
/// but in case it happens for any reason, we want to make sure we detect it and go back up.
/// @param client The client performing the action
/// @return Always returns Success, Eater.current_layer might be updated
Botcraft::Status ValidateCurrentLayer(Botcraft::SimpleBehaviourClient& client);
/// @brief Remove the first action in the action queue
/// @param client The client performing the action
/// @return Failure if the queue is full, Success otherwise
Botcraft::Status PopAction(Botcraft::SimpleBehaviourClient& client);
/// @brief Check if there is some empty slots in the inventory
/// @param client The client performing the action
/// @return Success if the inventory has no more empty slot, Failure otherwise
Botcraft::Status IsInventoryFull(Botcraft::SimpleBehaviourClient& client);
/// @brief Clean the inventory by throwing on the floor all the undesired items and put some lava on top
/// @param client The client performing the action
/// @return Success if the items were thrown, the lava placed and retrieved, Failure otherwise
Botcraft::Status CleanInventory(Botcraft::SimpleBehaviourClient& client);
/// @brief Decrement current layer index
/// @param client The client performing the action
/// @return Success if not done, Failure if we passed the last layer
Botcraft::Status MoveToNextLayer(Botcraft::SimpleBehaviourClient& client);
/// @brief Plan actions to prepare for the current layer (place ladders)
/// @param client The client performing the action
/// @return Always return Success
Botcraft::Status PrepareLayer(Botcraft::SimpleBehaviourClient& client);
/// @brief Plan actions to fill all fluids from the current layer with solid blocks
/// @param client The client performing the action
/// @return Always return Success
Botcraft::Status PlanLayerFluids(Botcraft::SimpleBehaviourClient& client);
/// @brief Plan actions to replace all non walkable blocks (non solid/hazardous) from the current layer with solid ones
/// @param client The client performing the action
/// @return Always return Success
Botcraft::Status PlanLayerNonWalkable(Botcraft::SimpleBehaviourClient& client);
/// @brief Plan actions to remove all blocks from the current layer
/// @param client The client performing the action
/// @return Always return Success
Botcraft::Status PlanLayerBlocks(Botcraft::SimpleBehaviourClient& client);
/// @brief Drop all unnecessary items at basecamp
/// @param client The client performing the action
/// @param all_items If true, all items will be thrown (including tools and food)
/// @return Success if nothing bad happened during dropping
Botcraft::Status BaseCampDropItems(Botcraft::SimpleBehaviourClient& client, const bool all_items);
/// @brief Pick all necessary items from basecamp
/// @param client The client performing the action
/// @return Success if nothing bad happened during picking
Botcraft::Status BaseCampPickItems(Botcraft::SimpleBehaviourClient& client);
/// @brief Check if there is a tool of type tool_type in the inventory
/// @param client The client performing the action
/// @param tool_type Tool type to search for
/// @param min_durability Number of time we can use the tool before it breaks
/// @return Success if a tool with >min_durability durability was found, Failure otherwise
Botcraft::Status HasToolInInventory(Botcraft::SimpleBehaviourClient& client, const Botcraft::ToolType tool_type, const int min_durability = 0);
@@ -0,0 +1,68 @@
#pragma once
#include <unordered_set>
#include <unordered_map>
#include <vector>
#include <string>
#include <botcraft/Game/Vector3.hpp>
#include <botcraft/Game/World/World.hpp>
#include <botcraft/AI/SimpleBehaviourClient.hpp>
/// @brief Split a string into subcomponents
/// @param s String to split
/// @param delimiter Delimiter character
/// @return An unordered set of substrings in s
std::unordered_set<std::string> SplitString(const std::string& s, const char delimiter);
/// @brief Find all interest points in the surroundings and load them in the blackboard
/// @param client The client performing the action
/// @return True if everything was found, false otherwise
bool IdentifyBaseCampLayout(Botcraft::SimpleBehaviourClient& client);
/// @brief Helper function. Get all blocks between start and end positions. Assumes start.y == end.y
/// @param world World to query
/// @param start Start position
/// @param end End position
/// @param layer Y position of the current layer
/// @param fluids If true, will gather fluid blocks
/// @param solids If true, will gather solid blocks
/// @return A set of positions respecting the given constraints
std::unordered_set<Botcraft::Position> CollectBlocks(const std::shared_ptr<Botcraft::World> world, const Botcraft::Position& start, const Botcraft::Position& end, const int layer, const bool fluids, const bool solids);
/// @brief Helper function. Group a set of positions on the same layer in connected components
/// @param start Start block of the layer
/// @param end End block of the layer
/// @param start_point Starting point on the layer (typically the access ladder)
/// @param client Client used to check for pathfinding reachability
/// @param positions Positions to process
/// @param additional_neighbours If set, will be filled with additional neighbours for each components (i.e. blocks that are not directly next to each other but still reachable from each other by pathfinding algorithm)
/// @return A vector of connected components
std::vector<std::unordered_set<Botcraft::Position>> GroupBlocksInComponents(const Botcraft::Position& start, const Botcraft::Position& end, const Botcraft::Position& start_point, const Botcraft::BehaviourClient& client, const std::unordered_set<Botcraft::Position>& positions, std::unordered_map<Botcraft::Position, std::unordered_set<Botcraft::Position>>* additional_neighbours);
/// @brief Given a set of connected components, get a list of blocks to add to link them together
/// @param components The components (i.e. group of blocks) to link together
/// @param start_point Starting point on the layer (typically the access ladder)
/// @param bound_min Min bound of the working area (to be sure we don't add block outside of it)
/// @param bound_max Max bound of the working area (to be sure we don't add block outside of it)
/// @return An ordered vector of blocks to add to the layer to link all components together
std::vector<Botcraft::Position> GetBlocksToAdd(const std::vector<std::unordered_set<Botcraft::Position>>& components, const Botcraft::Position& start_point, const Botcraft::Position& bound_min, const Botcraft::Position& bound_max);
/// @brief Merge sets of positions into one set
/// @param components Connected components positions
/// @param additional_blocks Additional blocks to link the components together
/// @return One set of all sets merged
std::unordered_set<Botcraft::Position> FlattenComponentsAndAdditionalBlocks(const std::vector<std::unordered_set<Botcraft::Position>>& components, const std::vector<Botcraft::Position>& additional_blocks);
/// @brief Order all given positions such as we go one by one until we reach exit_block without ever "cutting the branch we are sitting on"
/// @param blocks All block to order. Intentional copy as we need to remove the blocks one by one once processed
/// @param start_block Block to start with
/// @param orientation Orientation of the work area compared to the entry point (North, South, East or West), used to "optimize" the ordering along one axis rather than the other and minimize travel time
/// @param additional_neighbours Potential additional neighbours for some blocks (i.e. blocks that are not direct neighbours but that we can reach by pathfinding)
/// @return Ordered positions to follow, should end with exit_block
std::vector<Botcraft::Position> ComputeBlockOrder(std::unordered_set<Botcraft::Position> blocks, const Botcraft::Position& start_block, const Botcraft::Direction orientation, const std::unordered_map<Botcraft::Position, std::unordered_set<Botcraft::Position>>& additional_neighbours = {});
/// @brief A function to check if a block could turn into falling entity if updated (sand/gravel)
/// @param block_name Block name to check
/// @return True if sand/gravel or similar, false otherwise
bool CouldFallIfUpdated(const std::string& block_name);
@@ -0,0 +1,76 @@
#include <botcraft/AI/Tasks/BaseTasks.hpp>
#include <botcraft/AI/Tasks/PathfindingTask.hpp>
#include <botcraft/Game/Vector3.hpp>
#include "WorldEaterClient.hpp"
using namespace Botcraft;
WorldEaterClient::WorldEaterClient(const std::string& trigger_word, const bool use_renderer_) : SimpleBehaviourClient(use_renderer_)
{
word = trigger_word;
}
WorldEaterClient::~WorldEaterClient()
{
}
#if PROTOCOL_VERSION < 759 /* < 1.19 */
void WorldEaterClient::Handle(ProtocolCraft::ClientboundChatPacket& msg)
{
ManagersClient::Handle(msg);
ProcessChatMsg(msg.GetMessage().GetText());
}
#else
void WorldEaterClient::Handle(ProtocolCraft::ClientboundPlayerChatPacket& msg)
{
ManagersClient::Handle(msg);
#if PROTOCOL_VERSION == 759 /* 1.19 */
ProcessChatMsg(msg.GetSignedContent().GetText());
#elif PROTOCOL_VERSION == 760 /* 1.19.1/2 */
ProcessChatMsg(msg.GetMessage_().GetSignedBody().GetContent().GetPlain());
#else
ProcessChatMsg(msg.GetBody().GetContent());
#endif
}
void WorldEaterClient::Handle(ProtocolCraft::ClientboundSystemChatPacket& msg)
{
ManagersClient::Handle(msg);
ProcessChatMsg(msg.GetContent().GetText());
}
#endif
void WorldEaterClient::ProcessChatMsg(const std::string& msg)
{
if (msg != word)
{
return;
}
Position out_position;
try
{
out_position = blackboard.Get<Position>("Eater.out_position");
}
catch (const std::exception&)
{
// Probably not initialized yet, do nothing except warn user
LOG_WARNING("out_position not found in the blackboard, can't abort. Is the bot initialized?");
return;
}
const std::string& bot_name = network_manager->GetMyName();
SetBehaviourTree(Builder<SimpleBehaviourClient>("Completion Tree")
.sequence()
.leaf("Go to out pos", GoTo, out_position + Position(0, 1, 0), 0, 0, 0, true, true, 1.0f)
.leaf("Notify", Say, network_manager->GetMyName() + " out")
.leaf("Set should be closed", [](SimpleBehaviourClient& c) { c.SetShouldBeClosed(true); return Status::Success; })
.leaf("Set null tree", [](SimpleBehaviourClient& c) { c.SetBehaviourTree(nullptr); return Status::Success; })
.end());
}
@@ -0,0 +1,152 @@
#include "WorldEaterSubTrees.hpp"
#include "WorldEaterTasks.hpp"
#include <botcraft/AI/Tasks/AllTasks.hpp>
using namespace Botcraft;
std::shared_ptr<BehaviourTree<SimpleBehaviourClient>> FullTree()
{
std::shared_ptr<BehaviourTree<SimpleBehaviourClient>> go_to_top_ladder = GoToTopLadderTree();
return Builder<SimpleBehaviourClient>("Full Tree")
.sequence()
.tree(InitTree())
.tree(BaseCampResupplyTree())
// Make sure we go near the ladder position first,
// to avoid pathfinding through the other bots
// working area
.tree(go_to_top_ladder)
.tree(MainTree())
.tree(go_to_top_ladder)
// If we exited main tree because of an error, clean inventory before going back to basecamp
.leaf("Clean inventory", CleanInventory)
.tree(CompletionTree())
.end();
}
std::shared_ptr<BehaviourTree<SimpleBehaviourClient>> InitTree()
{
return Builder<SimpleBehaviourClient>("Init Tree")
.selector()
.leaf("Is init?", CheckBlackboardBoolData, "Eater.init")
.selector()
.leaf("Init", Init)
// If init failed, stop the behaviour
.leaf("Stop behaviour", [](SimpleBehaviourClient& c) { c.SetBehaviourTree(nullptr); c.Yield(); return Status::Success; })
.end()
.end();
}
std::shared_ptr<BehaviourTree<SimpleBehaviourClient>> BaseCampResupplyTree()
{
return Builder<SimpleBehaviourClient>("Basecamp Resupply Tree")
.sequence()
.leaf("Drop items", BaseCampDropItems, false)
.leaf("Pick items", BaseCampPickItems)
.leaf("Has lava", HasItemInInventory, "minecraft:lava_bucket", 1)
.leaf("Has shears", HasToolInInventory, ToolType::Shears, 2)
.leaf("Has hoe", HasToolInInventory, ToolType::Hoe, 2)
.leaf("Has shovel", HasToolInInventory, ToolType::Shovel, 2)
.leaf("Has axe", HasToolInInventory, ToolType::Axe, 2)
.leaf("Has food", HasItemInInventory, "minecraft:golden_carrot", 64)
.leaf("Has ladder", HasItemInInventory, "minecraft:ladder", 64)
.leaf(CopyBlackboardData, "Eater.temp_block", "HasItemInInventory.item_name")
.leaf(SetBlackboardData<int>, "HasItemInInventory.quantity", 5*64)
.leaf("Has temp block", HasItemInInventoryBlackboard)
.leaf("Has pickaxe", HasToolInInventory, ToolType::Pickaxe, 2)
.end();
}
std::shared_ptr<BehaviourTree<SimpleBehaviourClient>> MainTree()
{
std::shared_ptr<BehaviourTree<SimpleBehaviourClient>> action_loop_tree = ActionLoopTree();
return Builder<SimpleBehaviourClient>("Main Tree")
.repeater("Layer loop", 0).inverter() // repeater(0) + inverter --> repeat until it fails
.sequence("Layer loop body")
.leaf("Validate current layer", ValidateCurrentLayer)
.leaf("Prepare layer", PrepareLayer)
.tree(action_loop_tree)
// CheckActionsDone will exit this loop early if all the
// actions weren't done during the internal loop
.leaf("Check done", CheckActionsDone)
.leaf("Plan layer non walkable", PlanLayerNonWalkable)
.tree(action_loop_tree)
.leaf("Check done", CheckActionsDone)
.leaf("Plan layer fluids", PlanLayerFluids)
.tree(action_loop_tree)
.leaf("Check done", CheckActionsDone)
.leaf("Plan layer blocks", PlanLayerBlocks)
.tree(action_loop_tree)
.leaf("Check done", CheckActionsDone)
.leaf("Move to next layer", MoveToNextLayer)
.end();
}
std::shared_ptr<BehaviourTree<SimpleBehaviourClient>> ActionLoopTree()
{
return Builder<SimpleBehaviourClient>("Action Loop Tree")
.sequence()
// Count the number of failed action performed in a row, reset after every successful action
.leaf("Reset failure counter", SetBlackboardData<int>, "Eater.failed_loop", 0)
// Repeat the whole loop until it fails.
// The last leaf makes sure it fails only
// if the internal loop failed three times in
// a row.
.repeater(0).inverter()
.sequence()
// Exit early if action queue is empty
.inverter().leaf("Check done", CheckActionsDone)
.selector()
// Try to perform next action
.sequence("Action loop body")
.selector("Eat")
.inverter().leaf(IsHungry, 15) // Try to optimize golden carrots eating pattern with 15 threshold
.leaf(Eat, "minecraft:golden_carrot", true)
.end()
.selector("Clean inventory")
.inverter().leaf(IsInventoryFull)
.leaf(CleanInventory)
.end()
.inverter().leaf(IsInventoryFull) // If inventory is still full after cleaning, stop loop and go back to basecamp to empty it
.leaf("Get next action", GetNextAction)
.leaf("Execute action", ExecuteAction)
// If the action succeeded, reset failure counter to 0
.leaf("Reset failure counter", SetBlackboardData<int>, "Eater.failed_loop", 0)
.leaf("Pop action", PopAction)
.end()
// If action failed, wait for ~1 sec and increment failure counter
.sequence()
.repeater(100).leaf("Sleep", Yield)
.leaf("Increment failure counter", [](SimpleBehaviourClient& c) { c.GetBlackboard().Set<int>("Eater.failed_loop", c.GetBlackboard().Get<int>("Eater.failed_loop") + 1); return Status::Success; })
.end()
.end()
// If failure counter is equal to 3, break out of the loop
.leaf("Exit loop if 3 failures", [](SimpleBehaviourClient& c) { return c.GetBlackboard().Get<int>("Eater.failed_loop") == 3 ? Status::Failure : Status::Success; })
.end()
.end();
}
std::shared_ptr<BehaviourTree<SimpleBehaviourClient>> CompletionTree()
{
return Builder<SimpleBehaviourClient>("Completion Tree")
.sequence()
.leaf("Check done", CheckBlackboardBoolData, "Eater.done")
.leaf("Drop all items", BaseCampDropItems, true)
.leaf("Go to out pos", [](SimpleBehaviourClient& c) { LOG_INFO("Go to out position" << c.GetBlackboard().Get<Position>("Eater.out_position")); return GoTo(c, c.GetBlackboard().Get<Position>("Eater.out_position") + Position(0, 1, 0), 0, 0); })
.leaf("Notify", Say, "It's over. It's done.")
.leaf("Set should be closed", [](SimpleBehaviourClient& c) { c.SetShouldBeClosed(true); return Status::Success; })
.leaf("Set null tree", [](SimpleBehaviourClient& c) { c.SetBehaviourTree(nullptr); return Status::Success; })
.end();
}
std::shared_ptr<Botcraft::BehaviourTree<Botcraft::SimpleBehaviourClient>> GoToTopLadderTree()
{
return Builder<SimpleBehaviourClient>("Go To Top Ladder Tree")
.sequence()
.leaf(CopyBlackboardData, "Eater.ladder", "GoTo.goal")
.leaf(SetBlackboardData<int>, "GoTo.dist_tolerance", 4)
.leaf(SetBlackboardData<int>, "GoTo.min_end_dist", 1)
.leaf(SetBlackboardData<int>, "GoTo.min_end_dist_xz", 1)
.leaf("Go to ladder", GoToBlackboard)
.end();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,568 @@
#include <botcraft/AI/Tasks/PathfindingTask.hpp>
#include "WorldEaterUtilities.hpp"
using namespace Botcraft;
std::unordered_set<std::string> SplitString(const std::string& s, const char delimiter)
{
std::unordered_set<std::string> tokens;
std::string token;
std::istringstream tokenStream(s);
while (std::getline(tokenStream, token, delimiter))
{
tokens.insert(token);
}
return tokens;
}
bool IdentifyBaseCampLayout(SimpleBehaviourClient& client)
{
Blackboard& blackboard = client.GetBlackboard();
const int bot_index = blackboard.Get<int>("Eater.bot_index");
std::unordered_map<std::string, std::string> block_item_mapping = {
{ "minecraft:red_concrete", "drop" },
{ "minecraft:lime_concrete", "out" },
{ "minecraft:orange_shulker_box", "lava_bucket" },
{ "minecraft:magenta_shulker_box", "shears" },
{ "minecraft:purple_shulker_box", "hoe" },
{ "minecraft:blue_shulker_box", "shovel" },
{ "minecraft:light_blue_shulker_box", "axe" },
{ "minecraft:yellow_shulker_box", "golden_carrots" },
{ "minecraft:brown_shulker_box", "ladder" },
{ "minecraft:black_shulker_box", "temp_block" },
{ "minecraft:cyan_shulker_box", "pickaxe" }
};
std::unordered_map<std::string, std::vector<Position>> found_blocks;
for (const auto& [k, v] : block_item_mapping)
{
found_blocks[k] = {};
}
std::shared_ptr<World> world = client.GetWorld();
Position current_position;
// Loop through all loaded blocks to find the one we are looking for
{
auto all_chunks = world->GetChunks();
for (const auto& [coords, chunk] : *all_chunks)
{
for (int y = chunk.GetMinY(); y < chunk.GetMinY() + chunk.GetHeight(); ++y)
{
current_position.y = y;
for (int z = 0; z < CHUNK_WIDTH; ++z)
{
current_position.z = z;
for (int x = 0; x < CHUNK_WIDTH; ++x)
{
current_position.x = x;
const Blockstate* blockstate = chunk.GetBlock(current_position);
if (blockstate == nullptr)
{
continue;
}
if (const auto it = found_blocks.find(blockstate->GetName()); it != found_blocks.end())
{
const Position world_coordinates(
coords.first * CHUNK_WIDTH + x,
y,
coords.second * CHUNK_WIDTH + z
);
it->second.push_back(world_coordinates);
if (bot_index == 0)
{
LOG_INFO(blockstate->GetName() << "(" << block_item_mapping.at(blockstate->GetName()) << ") found at " << world_coordinates);
}
}
}
}
}
}
}
// Sort all positions in a consistent way for all bots
for (auto& [_, v] : found_blocks)
{
std::sort(v.begin(), v.end(), [&](const Position& a, const Position& b)
{
return a.x < b.x ||
(a.x == b.x && a.y < b.y) ||
(a.x == b.x && a.y == b.y && a.z < b.z);
});
}
// Assign dedicated positions for each bot based on index
for (const auto& [k, v] : block_item_mapping)
{
const std::vector<Position>& positions = found_blocks.at(k);
if (positions.size() == 0)
{
if (bot_index == 0)
{
LOG_ERROR("Can't find any " << k << ". I can't work without " << v);
}
return false;
}
blackboard.Set<Position>("Eater." + v + "_position", positions[bot_index % positions.size()]);
}
return true;
}
std::unordered_set<Position> CollectBlocks(const std::shared_ptr<World> world, const Position& start, const Position& end, const int layer, const bool fluids, const bool solids)
{
std::unordered_set<Position> output;
Position p(0, layer, 0);
for (int x = start.x; x <= end.x; ++x)
{
p.x = x;
for (int z = start.z; z <= end.z; ++z)
{
p.z = z;
const Blockstate* blockstate = world->GetBlock(p);
if (blockstate == nullptr || blockstate->IsAir())
{
continue;
}
if (// if fluid, we are not interested in the corner blocks as they can't "leak" into the working area
(fluids && (blockstate->IsFluid() || blockstate->IsWaterlogged()) && ((x != start.x && x != end.x) || (z != start.z && z != end.z))) ||
// if solid
(solids && blockstate->IsSolid()) ||
// not fluid not solid and non walkable
(!fluids && !solids && !blockstate->IsWaterlogged() && (!blockstate->IsFluid() || blockstate->IsHazardous()) && (!blockstate->IsSolid() || blockstate->IsHazardous())) ||
// not fluid not solid and gravity hazard
(!fluids && !solids && CouldFallIfUpdated(blockstate->GetName())))
{
output.insert(Position(x, layer, z));
}
}
}
return output;
}
std::vector<std::unordered_set<Position>> GroupBlocksInComponents(const Position& start, const Position& end, const Position& start_point, const BehaviourClient& client, const std::unordered_set<Position>& positions, std::unordered_map<Position, std::unordered_set<Position>>* additional_neighbours)
{
std::vector<std::unordered_set<Position>> components;
std::unordered_map<Position, int> components_index;
for (const Position& b : positions)
{
components_index[b] = -1;
}
// For all block elements
for (const auto& p : positions)
{
// If we already set a component to this position, skip it
if (components_index[p] != -1)
{
continue;
}
// Otherwise add all neighbours we can find to the current component
std::unordered_set<Position> current_component;
std::unordered_set<Position> neighbours = { p };
while (neighbours.size() > 0)
{
// Take first element in current neighbours
const Position current_pos = *neighbours.begin();
neighbours.erase(current_pos);
// This neighbour is already in a component
if (components_index[current_pos] != -1)
{
continue;
}
current_component.insert(current_pos);
components_index[current_pos] = components.size();
if (current_pos.x > start.x)
{
const Position west = current_pos + Position(-1, 0, 0);
if (const auto it = components_index.find(west); it != components_index.end() && it->second == -1)
{
neighbours.insert(west);
}
}
if (current_pos.x < end.x)
{
const Position east = current_pos + Position(1, 0, 0);
if (const auto it = components_index.find(east); it != components_index.end() && it->second == -1)
{
neighbours.insert(east);
}
}
if (current_pos.z > start.z)
{
const Position north = current_pos + Position(0, 0, -1);
if (const auto it = components_index.find(north); it != components_index.end() && it->second == -1)
{
neighbours.insert(north);
}
}
if (current_pos.z < end.z)
{
const Position south = current_pos + Position(0, 0, 1);
if (const auto it = components_index.find(south); it != components_index.end() && it->second == -1)
{
neighbours.insert(south);
}
}
}
if (current_component.size() == 0)
{
continue;
}
// If additional neighbours are not asked, don't try to merge
// components using pathfinding
if (additional_neighbours == nullptr)
{
components.push_back(current_component);
continue;
}
// Check if we can pathfind from one of the previous components
// to this one and merge them if we can.
// We do not allow the 1-wide gaps during the pathfinding search because of the
// case illustrated below:
// __A__ __B__
// | | | |
// __C__
// | |
// when standing on and having to break B, the bot would pathfind to the nearest block
// which would be C (because jumping has a penalty in the pathfinding search). So it
// goes down to C, breaks B and that's it. It can't get back on top of A to continue the
// current layer. Disabling 1-wide jumps solves this by making A and B always adjacent,
// i.e. closer to each other than C that is at a 2 blocks distance.
bool merged = false;
for (size_t i = 0; i < components.size(); ++i)
{
const Position pathfinding_start = *components[i].begin() + Position(0, 1, 0);
const Position pathfinding_end = *current_component.begin() + Position(0, 1, 0);
const std::vector<Position> path = FindPath(client, pathfinding_start, pathfinding_end, 0, 0, false, false);
const std::vector<Position> reversed_path = FindPath(client, pathfinding_end, pathfinding_start, 0, 0, false, false);
// If we can pathfind from start to end (both ways to prevent cliff falls that would only allow one-way travel)
if (path.back() == pathfinding_end && reversed_path.back() == pathfinding_start)
{
merged = true;
// Add link between the two components as non adjacent neighbours
Position path_block_component1 = pathfinding_start + Position(0, -1, 0);
for (size_t j = 0; j < path.size(); ++j)
{
// If the pathfinding crosses the boundaries of the work area anywhere else than the ladder,
// cancel the merging of the components
if ((path[j].x < start.x || path[j].x > end.x || path[j].z < start.z || path[j].z > end.z) &&
path[j].x != start_point.x && path[j].z != start_point.z)
{
merged = false;
break;
}
if (additional_neighbours != nullptr)
{
// If component index of the current path position is equal to the one currently compared to (i)
auto it = components_index.find(path[j] + Position(0, -1, 0));
if (it != components_index.end() && it->second == i)
{
path_block_component1 = path[j] + Position(0, -1, 0);
}
}
}
Position path_block_component2 = pathfinding_end + Position(0, -1, 0);
for (size_t j = 0; j < reversed_path.size(); ++j)
{
// If the pathfinding crosses the boundaries of the work area anywhere else than the ladder,
// cancel the merging of the components
if ((reversed_path[j].x < start.x || reversed_path[j].x > end.x || reversed_path[j].z < start.z || reversed_path[j].z > end.z) &&
reversed_path[j].x != start_point.x && reversed_path[j].z != start_point.z)
{
merged = false;
break;
}
if (additional_neighbours != nullptr)
{
// If component index of the current path position is equal to the current one to add (components.size())
auto it = components_index.find(reversed_path[j] + Position(0, -1, 0));
if (it != components_index.end() && it->second == components.size())
{
path_block_component2 = reversed_path[j] + Position(0, -1, 0);
}
}
}
if (merged)
{
for (const Position& p : current_component)
{
components_index[p] = i;
}
components[i].insert(current_component.begin(), current_component.end());
if (additional_neighbours != nullptr)
{
(*additional_neighbours)[path_block_component1].insert(path_block_component2);
(*additional_neighbours)[path_block_component2].insert(path_block_component1);
}
}
}
}
if (!merged)
{
components.push_back(current_component);
}
}
return components;
}
std::vector<Position> GetBlocksToAdd(const std::vector<std::unordered_set<Position>>& components, const Position& start_point, const Position& min_bound, const Position& max_bound)
{
if (components.empty())
{
return {};
}
std::unordered_set<int> components_already_processed = { -1 };
std::unordered_set<int> components_to_process;
// Add all components to "to_process" vector
for (int i = 0; i < components.size(); ++i)
{
components_to_process.insert(i);
}
std::vector<Position> output;
output.reserve(components_to_process.size()); // rough estimate of ~1:1 ratio block present/block to add
while (components_to_process.size() > 0)
{
float min_dist = std::numeric_limits<float>::max();
int argmin_component_idx = -1;
Position from;
Position to;
// Look for the "closest" remaining component
// from the one we already have
for (const int c1 : components_already_processed)
{
for (const int c2 : components_to_process)
{
for (const Position& p1 : c1 != -1 ? components[c1] : std::unordered_set<Position>{ start_point })
{
for (const Position& p2 : components[c2])
{
const float dist = std::abs(p1.x - p2.x) + std::abs(p1.z - p2.z);
if (dist < min_dist)
{
min_dist = dist;
argmin_component_idx = c2;
from = p1;
to = p2;
}
}
}
}
}
// Straight line to get back into the working area
// as quickly as possible
while (from != to && (
from.x < min_bound.x ||
from.x > max_bound.x ||
from.z < min_bound.z ||
from.z > max_bound.z))
{
if (from.x < min_bound.x)
{
from.x += 1;
}
else if (from.z < min_bound.z)
{
from.z += 1;
}
else if (from.x > max_bound.x)
{
from.x -= 1;
}
else if (from.z > max_bound.z)
{
from.z -= 1;
}
if (from != to)
{
output.push_back(from);
}
}
// Dumb 2D staircase between from and to
while (from != to)
{
if (std::abs(to.x - from.x) > std::abs(to.z - from.z))
{
from.x += from.x < to.x ? 1 : -1;
}
else
{
from.z += from.z < to.z ? 1 : -1;
}
if (from != to)
{
output.push_back(from);
}
}
components_already_processed.insert(argmin_component_idx);
// Remove start point after first time
components_already_processed.erase(-1);
components_to_process.erase(argmin_component_idx);
}
return output;
}
std::unordered_set<Position> FlattenComponentsAndAdditionalBlocks(const std::vector<std::unordered_set<Position>>& components, const std::vector<Position>& additional_blocks)
{
std::unordered_set<Position> output;
for (const std::unordered_set<Position>& v : components)
{
output.insert(v.begin(), v.end());
}
output.insert(additional_blocks.begin(), additional_blocks.end());
return output;
}
std::vector<Position> ComputeBlockOrder(std::unordered_set<Position> blocks, const Position& start_block, const Direction orientation, const std::unordered_map<Position, std::unordered_set<Position>>& additional_neighbours)
{
struct SortingNode
{
Position pos;
std::vector<SortingNode> children;
};
// Should not happen as at least start_block should be in blocks
if (blocks.size() == 0)
{
return { };
}
blocks.erase(start_block);
SortingNode root{ start_block };
std::queue<SortingNode*> to_process;
to_process.push(&root);
std::array<Position, 4> potential_neighbours;
if (orientation == Direction::East || orientation == Direction::West)
{
potential_neighbours = {
Position(-1, 0, 0), // west
Position(1, 0, 0), // east
Position(0, 0, -1), // north
Position(0, 0, 1) // south
};
}
else
{
potential_neighbours = {
Position(0, 0, -1), // north
Position(0, 0, 1), // south
Position(-1, 0, 0), // west
Position(1, 0, 0) // east
};
}
while (blocks.size() > 0)
{
if (to_process.empty())
{
LOG_ERROR("to_process queue empty while ordering blocks to process. This should not happen (?)");
return {};
}
SortingNode* current_node = to_process.front();
// If there are some additional neighbours to this node, check them and the direct neighbours
if (const auto it = additional_neighbours.find(current_node->pos); it != additional_neighbours.end())
{
for (const Position& potential_neighbour : it->second)
{
if (blocks.find(potential_neighbour) != blocks.end())
{
SortingNode neighbour;
neighbour.pos = potential_neighbour;
current_node->children.emplace_back(neighbour);
}
}
for (const Position& offset : potential_neighbours)
{
const Position potential_neighbour = current_node->pos + offset;
// Only add it if it's in blocks AND we didn't already added it as a potential neighbour
if (blocks.find(potential_neighbour) != blocks.end() && it->second.find(potential_neighbour) == it->second.end())
{
SortingNode neighbour;
neighbour.pos = potential_neighbour;
current_node->children.emplace_back(neighbour);
}
}
}
// Else only check the direct neighbours
else
{
for (const Position& offset : potential_neighbours)
{
const Position potential_neighbour = current_node->pos + offset;
if (blocks.find(potential_neighbour) != blocks.end())
{
SortingNode neighbour;
neighbour.pos = potential_neighbour;
current_node->children.emplace_back(neighbour);
}
}
}
for (size_t i = 0; i < current_node->children.size(); ++i)
{
to_process.push(current_node->children.data() + i);
blocks.erase(current_node->children[i].pos);
}
to_process.pop();
}
// Recursive function to add all children, then current node
// This allows to be sure that we will never "cut the branch
// we are sitting on" preventing getting back to start_point
const std::function<std::vector<Position>(const SortingNode& n)> get_sorted_positions =
[&get_sorted_positions](const SortingNode& n) -> std::vector<Position>
{
std::vector<Position> output;
for (const SortingNode& c : n.children)
{
const std::vector<Position> children_sorted = get_sorted_positions(c);
output.insert(output.end(), children_sorted.begin(), children_sorted.end());
}
output.push_back(n.pos);
return output;
};
return get_sorted_positions(root);
}
bool CouldFallIfUpdated(const std::string& block_name)
{
const static std::unordered_set<std::string> gravity_blocks = {
"minecraft:sand",
"minecraft:gravel",
"minecraft:red_sand",
#if PROTOCOL_VERSION > 762 /* > 1.19.4 */
"minecraft:suspicious_sand",
"minecraft:suspicious_gravel",
#endif
};
return gravity_blocks.find(block_name) != gravity_blocks.end();
}
@@ -0,0 +1,283 @@
#include <iostream>
#include <string>
#include <botcraft/Game/Vector3.hpp>
#include <botcraft/Game/World/World.hpp>
#include <botcraft/AI/SimpleBehaviourClient.hpp>
#include <botcraft/Utilities/Logger.hpp>
#include <botcraft/Utilities/SleepUtilities.hpp>
#include "WorldEaterSubTrees.hpp"
#include "WorldEaterClient.hpp"
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: 16\n"
<< "\t--numworld\tNumber of shared world used by bots, less worlds saves RAM, but can be slower if shared between too many bots, default: 4\n"
<< "\t--start\t3 ints, offset for the first block, default: -64 -59 832\n"
<< "\t--end\t3 ints, offset for the last block, default: 63 80 959\n"
<< "\t--tempblock\tname of the temp block, must be a full solid block, default: minecraft:basalt\n"
<< "\t--spared\tcomma-separated list of blocks you don't want the bots to break, default: minecraft:spawner,minecraft:torch\n"
<< "\t--collected\tcomma-separated list of items you want the bot to store (be careful to use item names instead of blocks if tools don't have silk touch), default: minecraft:diamond_ore,minecraft:deepslate_diamond_ore\n"
<< "\t--stopword\tif someone says this word in chat, all bots will go back to basecamp and stop working. Use it if you need to pause the process to be able to resume it, default:banana\n"
<< std::endl;
}
struct Args
{
bool help = false;
std::string address = "127.0.0.1:25565";
int num_bot = 16;
int num_world = 4;
Botcraft::Position start = Botcraft::Position(-64, -59, 832);
Botcraft::Position end = Botcraft::Position(63, 80, 959);
std::string temp_block = "minecraft:basalt";
std::string spared_blocks = "minecraft:spawner,minecraft:torch";
std::string collected_blocks = "minecraft:diamond_ore,minecraft:deepslate_diamond_ore";
std::string stopword = "banana";
int return_code = 0;
};
Args ParseCommandLine(int argc, char* argv[]);
int main(int argc, char* argv[])
{
try
{
// Init logging, log everything >= Info, to console and file
Botcraft::Logger::GetInstance().SetLogLevel(Botcraft::LogLevel::Info);
Botcraft::Logger::GetInstance().SetFilename("world-eater.logs");
// Add a name to this thread for logging
Botcraft::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", "BotDog", "Botrefois",
"Botomate", "Botaku", "Botaubus", "Bothentique"
};
const std::shared_ptr<Botcraft::BehaviourTree<Botcraft::SimpleBehaviourClient>> eater_behaviour_tree = FullTree();
std::vector<std::shared_ptr<Botcraft::World> > shared_worlds(args.num_world);
for (int i = 0; i < args.num_world; ++i)
{
shared_worlds[i] = std::make_shared<Botcraft::World>(true);
}
std::vector<std::string> names(args.num_bot);
std::vector<std::shared_ptr<WorldEaterClient> > clients(args.num_bot);
for (int i = 0; i < args.num_bot; ++i)
{
// Get a unique name for this bot
names[i] = i < base_names.size() ? base_names[i] : (base_names[i % base_names.size()] + std::to_string(i / base_names.size()));
// Create the bot client and connect to the server
clients[i] = std::make_shared<WorldEaterClient>(args.stopword, false);
clients[i]->SetSharedWorld(shared_worlds[i % args.num_world]);
clients[i]->SetAutoRespawn(true);
clients[i]->Connect(args.address, names[i], false);
// Start behaviour thread and set active tree
clients[i]->StartBehaviour();
clients[i]->SetBehaviourTree(eater_behaviour_tree, {
{ "Eater.bot_index" , i },
{ "Eater.num_bot", args.num_bot },
{ "Eater.start_block", args.start },
{ "Eater.end_block", args.end },
{ "Eater.temp_block", args.temp_block },
{ "Eater.spared_blocks", args.spared_blocks },
{ "Eater.collected_blocks", args.collected_blocks }
});
// Wait 2 seconds between each bot to avoid having too many bots loading all the chunks at the same time
Botcraft::Utilities::SleepFor(std::chrono::seconds(2));
}
std::vector<std::thread> behaviours_threads(args.num_bot);
for (int i = 0; i < args.num_bot; ++i)
{
behaviours_threads[i] = std::thread(&Botcraft::SimpleBehaviourClient::RunBehaviourUntilClosed, clients[i]);
// Start all the behaviours with a 2 seconds interval
Botcraft::Utilities::SleepFor(std::chrono::seconds(2));
}
// Wait for all the bots to disconnect (meaning the job is done if everything worked properly)
for (int i = 0; i < args.num_bot; ++i)
{
behaviours_threads[i].join();
}
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 == "--start")
{
if (i + 3 < argc)
{
int x = std::stoi(argv[++i]);
int y = std::stoi(argv[++i]);
int z = std::stoi(argv[++i]);
args.start = Botcraft::Position(x, y, z);
}
else
{
LOG_FATAL("--start requires 3 arguments");
args.return_code = 1;
return args;
}
}
else if (arg == "--end")
{
if (i + 3 < argc)
{
int x = std::stoi(argv[++i]);
int y = std::stoi(argv[++i]);
int z = std::stoi(argv[++i]);
args.end = Botcraft::Position(x, y, z);
}
else
{
LOG_FATAL("--end 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;
}
}
else if (arg == "--spared")
{
if (i + 1 < argc)
{
args.spared_blocks = argv[++i];
}
else
{
LOG_FATAL("--spared requires an argument");
args.return_code = 1;
return args;
}
}
else if (arg == "--collected")
{
if (i + 1 < argc)
{
args.collected_blocks = argv[++i];
}
else
{
LOG_FATAL("--collected requires an argument");
args.return_code = 1;
return args;
}
}
else if (arg == "--stopword")
{
if (i + 1 < argc)
{
args.stopword = argv[++i];
}
else
{
LOG_FATAL("--stopword requires an argument");
args.return_code = 1;
return args;
}
}
}
return args;
}