#include #include #include #include "goap.h" #include "CharacterManagerModule.h" #include "PlayerActionModule.h" #include "CharacterModule.h" #include "items.h" #include "CharacterAIModule.h" #include namespace ECS { struct ActionExec { struct PlanExecData { TownNPCs::NPCData &npc; Blackboard &bb; nlohmann::json &memory; }; enum { OK = 0, BUSY, ERROR }; TownNPCs::NPCData &npc; Blackboard &bb; nlohmann::json &memory; bool complete; bool active; const goap::BaseAction *action; ActionExec(PlanExecData &data, const goap::BaseAction *action) : npc(data.npc) , bb(data.bb) , memory(data.memory) , complete(false) , active(false) , action(action) { } ActionExec(const ActionExec &other) : npc(other.npc) , bb(other.bb) , memory(other.memory) , complete(other.complete) , active(other.active) , action(other.action) { } ActionExec &operator=(const ActionExec &other) { npc = other.npc; bb = other.bb; memory = other.memory; complete = other.complete; active = other.active; action = other.action; return *this; } private: virtual int update(float delta) = 0; virtual void finish(int result) = 0; virtual void activate() = 0; public: int operator()(float delta) { if (!active) activate(); int ret = update(delta); if (ret != BUSY) finish(ret); return ret; } }; struct PlanExec { enum { OK = 0, BUSY, ERROR }; std::vector action_exec; int index; PlanExec() : index(-1) { } int operator()(float delta) { if (index == -1) index = 0; int rc = (*action_exec[index])(delta); if (rc == ActionExec::ERROR) return ERROR; if (action_exec[index]->complete) index++; if (index >= action_exec.size()) return OK; return BUSY; } }; struct ActionNodeActions { struct WalkToAction : public goap::BaseAction { int node; WalkToAction(int node, int cost) : goap::BaseAction( "WalkTo(" + Ogre::StringConverter::toString( node) + ")", { { { "at_object", 0 } } }, { { { "at_object", 1 } } }, cost) , node(node) { } bool can_run(const Blackboard &state, bool debug = false) override { return (state.distance_to(prereq) == 0); } void _plan_effects(Blackboard &state) override { const ActionNodeList &alist = ECS::get(); state.apply(effects); const Ogre::Vector3 &nodePosition = alist.dynamicNodes[node].position; state.setPosition(nodePosition); } int get_cost(const Blackboard &bb) const override { int ret = m_cost; if (!bb.town.is_valid()) { ret += 1000000; goto out; } { const ActionNodeList &alist = ECS::get(); const Ogre::Vector3 &nodePosition = alist.dynamicNodes[node].position; const Ogre::Vector3 &npcPosition = bb.getPosition(); float dist = npcPosition.squaredDistance( nodePosition); ret += (int)Ogre::Math::Ceil(dist); } out: return ret; } }; struct RunActionNode : public goap::BaseAction { int node; float radius; Ogre::String action; RunActionNode(int node, const Ogre::String &action, const Blackboard &prereq, const Blackboard &effects, int cost) : goap::BaseAction( "Use(" + action + "," + Ogre::StringConverter::toString( node) + ")", prereq, effects, cost) , node(node) , action(action) { const ActionNodeList &alist = ECS::get(); radius = alist.dynamicNodes[node].radius; } bool can_run(const Blackboard &state, bool debug = false) override { bool pre = (state.distance_to(prereq) == 0); if (!pre) return pre; const ActionNodeList &alist = ECS::get(); const Ogre::Vector3 &nodePosition = alist.dynamicNodes[node].position; const Ogre::Vector3 &npcPosition = state.getPosition(); return (npcPosition.squaredDistance(nodePosition) < radius * radius); } }; std::vector *> m_actions; public: struct ActionExecWalk : ActionExec { private: Ogre::Vector3 targetPosition; float radius; int update(float delta) override { if (npc.e.is_valid()) { Ogre::SceneNode *n = ECS::get() .characterNodes.at(npc.e); Ogre::Vector3 position = n->_getDerivedPosition(); if (position.squaredDistance(targetPosition) >= radius * radius) { if (npc.e.is_valid()) n->_setDerivedPosition( targetPosition); npc.position = targetPosition; return BUSY; } } else { if (npc.position.squaredDistance( targetPosition) >= radius * radius) { npc.position = targetPosition; return BUSY; } } return OK; } void finish(int rc) override { if (rc == OK) bb.apply(action->effects); } void activate() override { const ActionNodeActions::WalkToAction *wtaction = static_cast< const ActionNodeActions::WalkToAction *>( action); radius = ECS::get() .dynamicNodes[wtaction->node] .radius; targetPosition = ECS::get() .dynamicNodes[wtaction->node] .position; Ogre::Vector3 direction = (targetPosition - npc.position).normalisedCopy(); targetPosition -= direction * radius; } public: ActionExecWalk(ActionExec::PlanExecData &data, goap::BaseAction *action) : ActionExec(data, action) { } }; struct ActionExecUse : ActionExec { private: int update(float delta) override { OgreAssert(false, "update"); return OK; } void finish(int rc) override { if (rc == OK) bb.apply(action->effects); OgreAssert(false, "finish"); } void activate() override { const ActionNodeActions::RunActionNode *runaction = static_cast(action); OgreAssert(false, "activate"); } public: ActionExecUse(ActionExec::PlanExecData &data, goap::BaseAction *action) : ActionExec(data, action) { } }; ActionNodeActions(int node, const Blackboard &prereq, int cost) { ZoneScoped; OgreAssert( node < ECS::get().dynamicNodes.size(), "bad node " + Ogre::StringConverter::toString(node)); auto paction = OGRE_NEW WalkToAction(node, 10000); m_actions.push_back(paction); nlohmann::json jactionPrereq = nlohmann::json::object(); nlohmann::json jactionEffect = nlohmann::json::object(); jactionPrereq["at_object"] = 1; const nlohmann::json props = ECS::get().dynamicNodes[node].props; OgreAssert(!props.is_null(), "bad node " + Ogre::StringConverter::toString(node)); Ogre::String prefix = "goap_prereq_"; for (auto it = props.begin(); it != props.end(); it++) { if (it.key().substr(0, prefix.length()) == prefix) { Ogre::String key = it.key().substr(prefix.length()); jactionPrereq[key] = it.value(); } } Ogre::String prefix2 = "goap_effect_"; for (auto it = props.begin(); it != props.end(); it++) { if (it.key().substr(0, prefix2.length()) == prefix2) { Ogre::String key = it.key().substr(prefix2.length()); jactionPrereq[key] = it.value(); } } OgreAssert(props.find("action") != props.end(), "bad action" + props.dump(4)); const Ogre::String &action = props["action"].get(); Ogre::String effectName = ""; #if 0 if (action == "sit") { const Ogre::String &nodeName = props["name"].get(); effectName = "is_" + nodeName + "_seated"; } else if (action == "use") { const Ogre::String &nodeName = props["name"].get(); effectName = "is_" + nodeName + "_used"; } #endif // const Ogre::String &nodeName = // props["name"].get(); Ogre::String nodeID = Ogre::StringConverter::toString(node); effectName = "is_used"; if (effectName.length() > 0) { jactionPrereq[effectName] = 0; jactionEffect[effectName] = 1; } // FIXME: add this to Blender goap_prereq_ and goap_effect_ variables if (action == "sit") { jactionPrereq["is_seated"] = 0; jactionEffect["is_seated"] = 1; } else if (action == "use") { OgreAssert(props["tags"].is_array(), "bad formed tags"); const nlohmann::json &tags = props["tags"]; if (tags.size() == 1 && tags[0].get() == "") { } else { bool have_bits = false; if (std::find(tags.begin(), tags.end(), "dance-pole") != tags.end()) { jactionPrereq["is_pole_dancing"] = 0; jactionEffect["is_pole_dancing"] = 1; have_bits = true; } if (std::find(tags.begin(), tags.end(), "toilet") != tags.end()) { jactionPrereq["toilet"] = 1; jactionEffect["toilet"] = 0; have_bits = true; } if (!have_bits) { ZoneScopedN("Use"); std::cout << "use: " << props.dump(4) << std::endl; // OgreAssert(false, "props: " + props.dump(4)); OgreAssert(tags.size() == 0, "Some tags: " + props.dump(4)); } } } else { OgreAssert(false, "props: " + props.dump(4)); } Blackboard actionPrereq({ jactionPrereq }); Blackboard actionEffect({ jactionEffect }); if (!prereq.is_valid()) actionPrereq.apply(prereq); m_actions.push_back(OGRE_NEW RunActionNode( node, action, actionPrereq, actionEffect, cost)); if (effectName == "") { std::cout << props.dump(4) << std::endl; std::cout << "Prereq" << std::endl; actionPrereq.dump_bits(); std::cout << "Effect" << std::endl; actionEffect.dump_bits(); OgreAssert(false, "action"); } } std::vector *> getActions() const { return m_actions; } }; struct ActionExecCommon : ActionExec { private: float delay; int update(float delta) override { delay -= delta; if (delay > 0.0f) return BUSY; else return OK; } void finish(int rc) override { if (rc == OK) bb.apply(action->effects); } void activate() override { ZoneScoped; ZoneTextF("%s", action->get_name().c_str()); delay = 1.0f; } public: ActionExecCommon(ActionExec::PlanExecData &data, goap::BaseAction *action) : ActionExec(data, action) , delay(0.0f) { } }; CharacterAIModule::CharacterAIModule(flecs::world &ecs) { static std::mutex ecs_mutex; ecs.module(); ecs.import (); ecs.import (); ecs.component(); ecs.component().on_add([](flecs::entity e, TownAI &ai) { std::lock_guard lock(ecs_mutex); ai.mutex = std::make_shared(); std::lock_guard lock2(*ai.mutex); ai.goals.push_back( { "HealthGoal", { nlohmann::json::object({ { "healthy", 1 } }) } }); ai.goals.push_back( { "NotHungryGoal", { { { "hungry", 0 } } } }); ai.goals.push_back( { "NotThirstyGoal", { { { "thirsty", 0 } } } }); ai.goals.push_back( { "SatisfyToiletNeedGoal", { { { "toilet", 0 } } } }); struct ActionData { Ogre::String name; Blackboard prereq; Blackboard effects; int cost; }; struct ActionData actionData[] = { #if 0 { "WalkTo", { { { "at_object", 0 } } }, { { { "at_object", 1 } } }, 1000 }, { "Sit#", { { { "at_object", 1 }, { "is_seated", 0 } } }, { { { "is_seated", 1 } } }, 10 }, #endif { "EatFoodSeated", { { { "have_food", 1 }, { "is_seated", 1 }, { "hungry", 1 } } }, { { { "healthy", 1 }, { "hungry", 0 } } }, 10 }, { "EatFood", { { { "have_food", 1 }, { "hungry", 1 } } }, { { { "healthy", 1 }, { "hungry", 0 } } }, 2000 }, { "DrinkWaterSeated", { { { "have_water", 1 }, { "is_seated", 1 }, { "thirsty", 1 } } }, { { { "thirsty", 0 } } }, 10 }, { "DrinkWater", { { { "have_water", 1 }, { "thirsty", 1 }, { "is_seated", 0 } } }, { { { "thirsty", 0 } } }, 2000 }, { "EatMedicineSeated", { { { "have_medicine", 1 }, { "healthy", 0 }, { "is_seated", 1 } } }, { { { "healthy", 1 } } }, 100 }, { "EatMedicine", { { { "have_medicine", 1 }, { "healthy", 0 }, { "is_seated", 0 } } }, { { { "healthy", 1 } } }, 10000 }, #if 0 { "UseToilet", { { { "toilet", 1 } } }, { { { "toilet", 0 } } }, 100 }, #endif { "GetFood", { { { "have_food", 0 } } }, { { { "have_food", 1 } } }, 1000 }, { "GetWater", { { { "have_water", 0 } } }, { { { "have_water", 1 } } }, 1000 }, { "GetMedicine", { { { "have_medicine", 0 } } }, { { { "have_medicine", 1 } } }, 1000 }, }; for (const auto &adata : actionData) ai.actions.push_back( OGRE_NEW goap::BaseAction( adata.name, adata.prereq, adata.effects, adata.cost)); ai.planner = std::make_shared > >(); }); ecs.system("CreateBlackboards") .kind(flecs::OnUpdate) .each([this](flecs::entity town, TownAI &ai, const TownNPCs &npcs) { ZoneScopedN("CreateBlackboards"); std::lock_guard lock(ecs_mutex); OgreAssert(npcs.npcs.size() > 0, "npcs not crated"); createBlackboards(town, npcs, ai); }); ecs.system("UpdateDynamicActions") .kind(flecs::OnUpdate) .each([](flecs::entity e, ActionNodeList &alist, TownAI &ai, TownNPCs &npcs) { ZoneScopedN("UpdateDynamicActions"); std::lock_guard lock(ecs_mutex); OgreAssert(npcs.npcs.size() > 0, "npcs not crated"); if (ai.nodeActions.size() > 0) return; if (alist.dynamicNodes.size() == 0) ECS::get_mut() .updateDynamicNodes(); OgreAssert(alist.nodes.size() > 0, "bad nodes"); if (alist.dynamicNodes.size() == 0) return; OgreAssert(alist.dynamicNodes.size() > 0, "bad dynamic nodes"); int nodeIndex; for (nodeIndex = 0; nodeIndex < alist.dynamicNodes.size(); nodeIndex++) { ActionNodeActions aactions( nodeIndex, Blackboard( { nlohmann::json::object() }), 10); ai.nodeActions[nodeIndex] = aactions.getActions(); OgreAssert(ai.nodeActions[nodeIndex].size() > 0, "bad action count"); } OgreAssert(ai.nodeActions.size() > 0, "no dynamic actions?"); }); ecs.system("UpdateDynamicNodes") .kind(flecs::OnUpdate) .interval(0.1f) .each([this](flecs::entity town, ActionNodeList &alist, TownAI &ai, TownNPCs &npcs) { ZoneScopedN("UpdateDynamicNodes"); OgreAssert(npcs.npcs.size() > 0, "npcs not crated"); std::lock_guard lock(ecs_mutex); ECS::get_mut().updateDynamicNodes(); }); struct MeasureTime { std::chrono::system_clock::time_point start; std::string what; MeasureTime(const std::string &s) : start(std::chrono::high_resolution_clock::now()) , what(s) { } ~MeasureTime() { auto end = std::chrono::high_resolution_clock::now(); std::chrono::duration elapsed = end - start; std::cout << what << " " << elapsed.count() << std::endl; } }; ecs.system("UpdateNPCPositions") .kind(flecs::OnUpdate) .each([](flecs::entity e, TownNPCs &npcs) { ZoneScopedN("UpdateNPCPositions"); OgreAssert(npcs.npcs.size() > 0, "npcs not crated"); for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) { auto &npc = npcs.npcs.at(it->first); if (npc.e.is_valid() && npc.e.has()) { Ogre::SceneNode *n = ECS::get() .characterNodes.at( npc.e); npc.position = n->_getDerivedPosition(); } } }); ecs.system("UpdateBlackboards") .kind(flecs::OnUpdate) .interval(0.1f) .each([this](flecs::entity town, ActionNodeList &alist, TownAI &ai, const TownNPCs &npcs) { ZoneScopedN("UpdateBlackboards"); OgreAssert(npcs.npcs.size() > 0, "npcs not crated"); Ogre::Root::getSingleton().getWorkQueue()->addTask([this, town, &alist, npcs, &ai]() { { ZoneScopedN( "UpdateBlackboards::Thread"); std::lock_guard lock( ecs_mutex); updateBlackboards(town, alist, npcs, ai); } Ogre::Root::getSingleton() .getWorkQueue() ->addMainThreadTask([this, town, &alist]() { ZoneScopedN( "UpdateBlackboards::MainThread"); town.modified(); town.modified(); ECS::modified(); }); }); }); ecs.system("PlanAI") .kind(flecs::OnUpdate) .interval(0.5f) .each([&](flecs::entity town, TownAI &ai, const TownNPCs &npcs) { ZoneScopedN("PlanAI"); OgreAssert(npcs.npcs.size() > 0, "npcs not crated"); OgreAssert(ai.blackboards.size() > 0, "blackboards not crated"); OgreAssert(ai.memory.size() > 0, "memory not crated"); Ogre::Root::getSingleton().getWorkQueue()->addTask([this, town, npcs, &ai]() { ZoneScopedN("PlanAI::Thread"); std::lock_guard lock(ecs_mutex); buildPlans(town, npcs, ai); Ogre::Root::getSingleton() .getWorkQueue() ->addMainThreadTask([this, town]() { ZoneScopedN( "PlanAI::MainThread"); town.modified(); }); }); }); static std::unordered_map plan_exec; ecs.system("RunPLAN") .kind(flecs::OnUpdate) .each([&](flecs::entity town, const EngineData &eng, TownNPCs &npcs, TownAI &ai) { ZoneScopedN("RunPLAN"); OgreAssert(npcs.npcs.size() > 0, "npcs not crated"); OgreAssert(ai.blackboards.size() > 0, "blackboards not crated"); OgreAssert(ai.memory.size() > 0, "memory not crated"); for (const auto &plans : ai.plans) { if (plan_exec.find(plans.first) != plan_exec.end()) { int rc = plan_exec[plans.first]( eng.delta); if (rc != PlanExec::BUSY) { plan_exec.erase(plans.first); ai.plans[plans.first].erase( ai.plans[plans.first] .begin()); } continue; } // std::cout << "NPC: " << plans.first; // std::cout << " Plans: " << plans.second.size(); for (const auto &plan : plans.second) { struct PlanExec pexec; if (plan.plan.size() == 0) continue; // std::cout << " Goal: "; plan.goal->goal.dump_bits(); for (const auto &action : plan.plan) { TownNPCs::NPCData &npc = npcs.npcs.at( plans.first); Blackboard &bb = ai.blackboards.at( plans.first); nlohmann::json &mem = ai.memory.at( plans.first); ActionExec::PlanExecData data({ npc, bb, mem, }); // TODO: executor factory is needed if (action->get_name().substr( 0, 4) == "Walk") { ActionExec *e = OGRE_NEW ActionNodeActions::ActionExecWalk( data, action); pexec.action_exec .push_back(e); } else if (action->get_name() .substr(0, 4) == "Use(") { ActionExec *e = OGRE_NEW ActionNodeActions::ActionExecUse( data, action); pexec.action_exec .push_back(e); } else { // std::cout // << action->get_name() // << " "; ActionExec *e = OGRE_NEW ActionExecCommon( data, action); pexec.action_exec .push_back(e); } // std::cout << action->get_name() // << " "; } plan_exec[plans.first] = pexec; break; } //std::cout << std::endl; } }); } void CharacterAIModule::createAI(flecs::entity town) { town.add(); } struct PlanTask { Blackboard blackboard; TownAI::goal_t goal; TownAI::Plan plan; TownAI::planner_t *planner; bool operator()() { auto buildPlan = [this](Blackboard &blackboard, const TownAI::goal_t &goal, TownAI::Plan &plan) -> bool { if (goal.is_reached(blackboard)) return false; plan.goal = &goal; std::vector *> path; int actionCount = blackboard.getActionsCount(); auto actionsData = blackboard.getActionsData(); path.resize(actionCount * actionCount); int path_length = planner->plan( blackboard, goal, actionsData, actionCount, path.data(), path.size()); if (path_length > 0) { plan.goal = &goal; plan.plan.insert(plan.plan.end(), path.begin(), path.begin() + path_length); return true; } return false; }; return buildPlan(blackboard, goal, plan); } PlanTask(Blackboard &blackboard, const TownAI::goal_t &goal, TownAI::planner_t *planner) : blackboard(blackboard) , goal(goal) , planner(planner) { } }; static std::deque plan_tasks; void CharacterAIModule::buildPlans(flecs::entity town, const TownNPCs &npcs, TownAI &ai) { ZoneScopedN("buildPlans"); OgreAssert(town.is_valid(), "Bad town entity"); std::lock_guard lock(*ai.mutex); auto planner = ai.planner; if (plan_tasks.size() > 0) { bool created = (plan_tasks.front())(); if (created) { ZoneTextF("%d: Goal: %s", plan_tasks.front().blackboard.index, plan_tasks.front().goal.get_name().c_str()); { std::cout << plan_tasks.front().blackboard.index << " "; std::cout << "Goal: " << plan_tasks.front().goal.get_name(); plan_tasks.front().goal.goal.dump_bits(); std::cout << std::endl; std::cout << "Path: "; for (auto &action : plan_tasks.front().plan.plan) { OgreAssert(action, "No action"); std::cout << action->get_name() + " "; } std::cout << " size: " << plan_tasks.front().plan.plan.size() << std::endl; } ai.plans[plan_tasks.front().blackboard.index].push_back( plan_tasks.front().plan); } plan_tasks.pop_front(); } else for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) { if (ai.blackboards.find(it->first) == ai.blackboards.end()) continue; auto &bb = ai.blackboards.at(it->first); /* if there are plans, skip until these get discarded */ if (ai.plans.find(it->first) != ai.plans.end() && ai.plans.at(it->first).size() > 0) continue; const auto &npc = npcs.npcs.at(it->first); int index = it->first; ai.plans[index] = {}; bb.query_ai(); for (const auto &goal : ai.goals) plan_tasks.emplace_back(bb, goal, ai.planner.get()); } } void CharacterAIModule::createBlackboards(flecs::entity town, const TownNPCs &npcs, TownAI &ai) { ZoneScopedN("createBlackboards"); OgreAssert(town.is_valid(), "Bad town entity"); std::lock_guard lock(*ai.mutex); for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) { if (ai.blackboards.find(it->first) == ai.blackboards.end()) { int strength = 10; int dexterity = 10; int health = 100; int stamina = 100; int sex = it->second.props["sex"].get(); if (sex == 0) { // male strength += 10; dexterity += 10; } // FIXME: use separate "memory" for stats // Do not keep actual stats in blackboard nlohmann::json memory; memory["strength"] = strength; memory["dexterity"] = dexterity; memory["health"] = health; memory["stamina"] = stamina; memory["needs_hunger"] = 0; memory["needs_thirst"] = 0; memory["needs_toilet"] = 0; nlohmann::json bb; bb["have_water"] = 0; bb["have_food"] = 0; bb["have_medicine"] = 0; bb["at_object"] = 0; ai.memory[it->first] = memory; ai.blackboards[it->first] = Blackboard(bb); ai.blackboards[it->first].index = it->first; ai.blackboards.at(it->first).town = town; // FIXME: do this once ai.blackboards.at(it->first).actionRefResize(0); ai.blackboards.at(it->first).actionRefAddActions( ai.actions); ai.conditions = TownAI::BitEvolutionBuilder() .addRule( "healthy", [](const nlohmann::json &data) -> bool { return data["health"] .get() > 20; }) ->addRule( "hungry", [](const nlohmann::json &data) -> bool { return data["needs_hunger"] .get() > 2000; }) ->addRule( "thirsty", [](const nlohmann::json &data) -> bool { return data["needs_thirst"] .get() > 1000; }) ->addRule( "toilet", [](const nlohmann::json &data) -> bool { return data["needs_toilet"] .get() > 1500; }) ->build(); ai.memoryUpdates = TownAI::MemoryUpdateBuilder() .addUpdate( "health", [](int index, const std::string &name, int value) -> int { if (value < 50) return value + 1; else return value; }) .addUpdate("needs_hunger", [](int index, const std::string &name, int value) -> int { return value + 1; }) .addUpdate("needs_thirst", [](int index, const std::string &name, int value) -> int { return value + 1; }) .addUpdate("needs_toilet", [](int index, const std::string &name, int value) -> int { return value + 1; }) .build(); } } } void CharacterAIModule::updateBlackboards(flecs::entity town, ActionNodeList &alist, const TownNPCs &npcs, TownAI &ai) { ZoneScopedN("updateBlackboards"); std::lock_guard lock(*ai.mutex); OgreAssert(town.is_valid(), "Bad town entity"); alist.build(); for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) { if (ai.blackboards.find(it->first) == ai.blackboards.end()) continue; ai.blackboards.at(it->first).index = it->first; ai.blackboards.at(it->first).town = town; ai.blackboards.at(it->first).setPosition( npcs.npcs.at(it->first).position); for (auto e : ai.memoryUpdates) { std::string key = e.name; auto &memory = ai.memory.at(it->first); if (memory.find(key) == memory.end()) memory[key] = 0; int value = memory[key].get(); int new_value = e.update(it->first, key, value); if (value != new_value) memory[key] = new_value; } auto &bb = ai.blackboards.at(it->first); bb.updateBits(ai, ai.memory.at(it->first), it->second.props); } } void Blackboard::_actionRefResize(int count) { if (count >= actionRef.size()) { int allocate = count; if (allocate < 1000) allocate = 1000; actionRef.resize(allocate); } OgreAssert(count < actionRef.size(), "out of memory"); actionRefCount = count; actionRefPtr = count; } Blackboard::Blackboard() : object(-1) , index(-1) , actionRefCount(0) , mutex(std::make_shared()) , bits(0) , mask(0) { } std::unordered_map Blackboard::mapping; Blackboard::Blackboard(const nlohmann::json &stats) : Blackboard() { populate(stats, mapping); } void Blackboard::populate(const nlohmann::json &stats, std::unordered_map &mapping) { if (stats.empty()) return; for (auto &[key, value] : stats.items()) { if (value.is_number_integer()) { int val = value.get(); if (val == 0 || val == 1) { // Update mapping if key is new if (mapping.find(key) == mapping.end()) { size_t next_bit = mapping.size(); if (next_bit < 64) { mapping[key] = next_bit; } else { OgreAssert(false, "Out of bits"); continue; } } size_t bit_idx = mapping[key]; bits.set(bit_idx, val == 1); mask.set(bit_idx); } } } } bool Blackboard::operator==(const Blackboard &other) const { OgreAssert(mask != 0 && other.mask != 0, "blackboard not prepared"); return (bits & other.mask) == (other.bits & other.mask); } bool Blackboard::operator!=(const Blackboard &other) const { return !(*this == other); } void Blackboard::apply(const Blackboard &other) { std::lock_guard lock(*mutex); // stats.update(other.stats); // 1. Clear bits in 'values' that are about to be overwritten (where effect mask is 1) // 2. OR the result with the effect values (filtered by the effect mask) bits = (bits & ~other.mask) | (other.bits & other.mask); // 3. Update our mask to include any new state definitions introduced by the effect mask |= other.mask; } Ogre::String Blackboard::dumpActions() { std::lock_guard lock(*mutex); Ogre::String ret; ret += "Actions:\n"; int count = 0; for (count = 0; count < actionRefCount; count++) { auto &action = actionRef[count]; ret += "name: " + action->get_name() + "\n"; ret += "\tprereq:\n"; action->prereq.dump_bits(); ret += "\teffects:\n"; action->effects.dump_bits(); } return ret; } void Blackboard::printActions() { std::cout << dumpActions() << std::endl; } void Blackboard::actionRefResize(int count) { std::lock_guard lock(*mutex); _actionRefResize(count); } void Blackboard::actionRefAddAction(goap::BaseAction *action) { std::lock_guard lock(*mutex); if (actionRef.size() <= actionRefCount + 16) actionRef.resize(actionRefCount + 32); OgreAssert(actionRefPtr < actionRef.size(), "out of memory"); OgreAssert(action, "bad action"); actionRef[actionRefPtr++] = action; actionRefCount++; } void Blackboard::actionRefAddActions( const std::vector *> &actions) { std::lock_guard lock(*mutex); _actionRefAddActions(actions); } void Blackboard::_actionRefAddActions( const std::vector *> &actions) { if (actionRef.size() <= actionRefCount + actions.size()) actionRef.resize(actionRefCount + actions.size() * 2); for (const auto &action : actions) { OgreAssert(actionRefPtr < actionRef.size(), "out of memory"); OgreAssert(action, "bad action"); actionRef[actionRefPtr++] = action; actionRefCount++; } } void Blackboard::actionRefAddActions(goap::BaseAction **actions, int count) { int i; std::lock_guard lock(*mutex); if (actionRef.size() <= actionRefCount + count) actionRef.resize(actionRefCount + count * 2); for (i = 0; i < count; i++) { OgreAssert(actionRefPtr < actionRef.size(), "out of memory"); OgreAssert(actions[i], "bad action"); actionRef[actionRefPtr++] = actions[i]; actionRefCount++; } } void Blackboard::updateBits(const TownAI &ai, const nlohmann::json &memory, const nlohmann::json &props) { for (const auto &mcond : ai.conditions) { if (mapping.find(mcond.key) == mapping.end()) populate(nlohmann::json::object({ { mcond.key, 0 } }), mapping); bits.set(mapping[mcond.key], mcond.condition(memory)); } } struct ComparePair { const nlohmann::json ¤t; const nlohmann::json ⌖ }; bool Blackboard::is_satisfied_by(const nlohmann::json ¤t, const nlohmann::json &target, float epsilon) { std::deque queue; queue.push_back({ current, target }); while (!queue.empty()) { ComparePair pair = queue.front(); queue.pop_front(); const nlohmann::json &curr = pair.current; const nlohmann::json &tgt = pair.target; if (curr.type() != tgt.type() && !(curr.is_number() && tgt.is_number())) return false; if (tgt.is_object()) for (auto it = tgt.begin(); it != tgt.end(); ++it) { auto found = curr.find(it.key()); if (found == curr.end()) return false; queue.push_back({ *found, it.value() }); } else if (tgt.is_array()) { if (curr.size() != tgt.size()) return false; for (int i = 0; i < tgt.size(); ++i) queue.push_back({ curr[i], tgt[i] }); } else if (tgt.is_number_float() || curr.is_number_float()) { if (std::abs(curr.get() - tgt.get()) >= epsilon) return false; } else if (curr != tgt) return false; } return true; } int Blackboard::distance_to(const Blackboard &goal) const { int distance = 0; OgreAssert(mask != 0 && goal.mask != 0, "blackboard not prepared"); return ((bits ^ goal.bits) & goal.mask).count(); } void Blackboard::setPosition(const Ogre::Vector3 &position) { std::lock_guard lock(*mutex); this->position = position; } void Blackboard::query_ai() { std::lock_guard lock(*mutex); TownAI &ai = town.get_mut(); const TownNPCs &npcs = town.get(); const float distance = 10000.0f; Ogre::Vector3 position(0, 0, 0); if (npcs.npcs.at(index).e.is_valid() && npcs.npcs.at(index).e.has()) { Ogre::SceneNode *n = ECS::get().characterNodes.at( npcs.npcs.at(index).e); position = n->_getDerivedPosition(); } else from_json(npcs.npcs.at(index).props["position"], position); this->position = position; ActionNodeList &alist = ECS::get_mut(); alist.query_ai(position, distance, points, distances); _actionRefResize(ai.actions.size()); int nodeActionCount = 0; for (size_t point : points) { Ogre::Vector3 &p = alist.dynamicNodes[point].position; float radius = alist.dynamicNodes[point].radius; float distance = p.squaredDistance(position); if (object >= 0 && (size_t)object == point && distance > radius * radius) { object = -1; bits.set(mapping["at_object"], false); } else if (object >= 0 && (size_t)object == point && distance <= radius * radius) bits.set(mapping["at_object"], true); /* some nodes do not have usable actions */ if (ai.nodeActions[point].size() > 0) { OgreAssert(ai.nodeActions[point].size() > 0, "bad node actions count " + alist.dynamicNodes[point].props.dump( 4)); _actionRefAddActions(ai.nodeActions[point]); nodeActionCount += ai.nodeActions[point].size(); } nlohmann::json nodes = nlohmann::json::array(); nlohmann::json j = alist.dynamicNodes[point].props; j["global_position_x"] = p.x; j["global_position_y"] = p.y; j["global_position_z"] = p.z; nodes.push_back(j); } } }