diff --git a/src/gamedata/CharacterAIModule.cpp b/src/gamedata/CharacterAIModule.cpp index ecf4845..28f0799 100644 --- a/src/gamedata/CharacterAIModule.cpp +++ b/src/gamedata/CharacterAIModule.cpp @@ -164,16 +164,16 @@ public: } Blackboard actionPrereq({ jactionPrereq }); Blackboard actionEffect({ jactionEffect }); - if (!prereq.stats.is_null()) + 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; - std::cout << actionPrereq.stats.dump(4) << std::endl; + actionPrereq.dump_bits(); std::cout << "Effect" << std::endl; - std::cout << actionEffect.stats.dump(4) << std::endl; + actionEffect.dump_bits(); OgreAssert(false, "action"); } } @@ -241,7 +241,7 @@ CharacterAIModule::CharacterAIModule(flecs::world &ecs) 2000 }, #endif { "EatMedicine", - { { { "have_medicine", 1 }, { "healty", 0 } } }, + { { { "have_medicine", 1 }, { "healthy", 0 } } }, { { { "healthy", 1 } } }, 100 }, { "UseToilet", @@ -402,91 +402,90 @@ 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) { OgreAssert(town.is_valid(), "Bad town entity"); std::lock_guard lock(*ai.mutex); auto planner = ai.planner; - auto buildPlan = [planner](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; - }; - 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] = {}; - for (const auto &goal : ai.goals) { - struct TownAI::Plan plan; - bool created = buildPlan(bb, goal, plan); -#if 0 - std::cout << "blackboard: " - << bb.stats.dump(4) - << std::endl; - std::cout << "goal: " - << goal.goal.stats.dump(4) - << std::endl; -#endif -#if 0 - std::cout << "Actions: " << std::endl; - for (auto &action : actions) { - std::cout << "name: " - << action->get_name() - << std::endl; - std::cout - << "\tprereq:\n" - << action->prereq.stats - .dump(4) - << std::endl; - std::cout - << "\teffects:\n" - << action->effects.stats - .dump(4) - << std::endl; - } -#endif -#if 1 - if (created) { - std::cout << bb.index << " "; - std::cout << "Goal: " << goal.get_name(); - std::cout << std::endl; - std::cout << "Path: "; - for (auto &action : plan.plan) { - OgreAssert(action, "No action"); - std::cout << action->get_name() + " "; - } - std::cout << " size: " << plan.plan.size() - << std::endl; - ai.plans[it->first].push_back(plan); - OgreAssert(false, "plan"); + if (plan_tasks.size() > 0) { + bool created = (plan_tasks.front())(); + if (created) { + 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() + " "; } -#endif + std::cout << " size: " + << plan_tasks.front().plan.plan.size() + << std::endl; + ai.plans[plan_tasks.front().blackboard.index].push_back( + plan_tasks.front().plan); + // OgreAssert(false, "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] = {}; + for (const auto &goal : ai.goals) + plan_tasks.emplace_back(bb, goal, + ai.planner.get()); } - } } void CharacterAIModule::createBlackboards(flecs::entity town, @@ -536,15 +535,7 @@ void CharacterAIModule::updateBlackboardsBits(flecs::entity town, { OgreAssert(town.is_valid(), "Bad town entity"); std::lock_guard lock(*ai.mutex); - struct UpdateBit { - Ogre::String checkValue; - int recover; - int minValue; - int maxValue; - int threshold; - Ogre::String writeValue; - }; - struct UpdateBit updateBits[] = { + std::vector updateBits = { { "health", 0, 0, 100, 20, "healthy" }, { "needs_hunger", 1, 0, 10000, 2000, "hungry" }, { "needs_thirst", 1, 0, 10000, 1000, "thirsty" }, @@ -553,39 +544,16 @@ void CharacterAIModule::updateBlackboardsBits(flecs::entity town, for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) { if (ai.blackboards.find(it->first) == ai.blackboards.end()) continue; - auto &stats = ai.blackboards.at(it->first).stats; - auto &object = ai.blackboards.at(it->first).object; + auto &bb = ai.blackboards.at(it->first); + bb.commit(); + } + 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; - for (const auto &bits : updateBits) { - int value = stats[bits.checkValue].get(); - int maxValue = bits.maxValue; - int minValue = bits.minValue; - int threshold = bits.threshold; - if (it->second.props.find(bits.checkValue + "_max") != - it->second.props.end()) - maxValue = - it->second - .props[bits.checkValue + "_max"] - .get(); - if (it->second.props.find(bits.checkValue + - "_threshold") != - it->second.props.end()) - threshold = it->second - .props[bits.checkValue + - "_threshold"] - .get(); - value += bits.recover; - if (value > maxValue) - value = maxValue; - if (value >= threshold) - stats[bits.writeValue] = 1; - else - stats[bits.writeValue] = 0; - if (value < bits.minValue) - value = bits.minValue; - stats[bits.checkValue] = value; - } + auto &bb = ai.blackboards.at(it->first); + bb.updateBits(updateBits, it->second.props); } } @@ -598,32 +566,52 @@ void CharacterAIModule::updateBlackboards(flecs::entity town, for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) { if (ai.blackboards.find(it->first) == ai.blackboards.end()) continue; - auto &stats = ai.blackboards.at(it->first).stats; - auto &object = ai.blackboards.at(it->first).object; ai.blackboards.at(it->first).index = it->first; ai.blackboards.at(it->first).town = town; auto &bb = ai.blackboards.at(it->first); bb.query_ai(); -#if 0 - OgreAssert(nodeActionCount > 0 || - points.size() == 0, - "no node actions and no points"); - if (nodeActionCount == 0) { - std::cout << "nodes:" - << alist.nodes.size() << " " - << alist.dynamicNodes.size() - << std::endl; - std::cout << "points: " << points.size() - << std::endl; - std::cout << "position: " << position - << std::endl; - } - OgreAssert(nodeActionCount > 0, - "no node actions"); -#endif bb.fixupBooleanKeys(); } + prepareActions(ai); +} + +void CharacterAIModule::prepareActions(TownAI &ai) +{ + // baking actions + for (auto &action : ai.actions) { + action->effects.commit(); + action->prereq.commit(); + } + for (auto &naction : ai.nodeActions) + for (auto &action : naction.second) { + action->effects.commit(); + action->prereq.commit(); + } +#if 0 + for (const auto &action : ai.actions) { + std::cout << action->get_name() << std::endl; + std::cout << "effects: " << std::endl; + action->effects.dump_bits(); + std::cout << "prereq: " << std::endl; + action->prereq.dump_bits(); + } + for (auto &naction : ai.nodeActions) + for (const auto &action : naction.second) { + std::cout << action->get_name() << std::endl; + std::cout << action->get_name() << std::endl; + std::cout << "effects: " << std::endl; + action->effects.dump_bits(); + std::cout << "prereq: " << std::endl; + action->prereq.dump_bits(); + } + std::cout << "end dump" << std::endl; + std::cout << "dump" << std::endl; + for (auto &m : Blackboard::mapping) + std::cout << m.first << ": " << m.second << std::endl; + std::cout << "end dump" << std::endl; + // OgreAssert(false, "baking actions"); +#endif } void Blackboard::_actionRefResize(int count) @@ -645,18 +633,58 @@ Blackboard::Blackboard() , index(-1) , actionRefCount(0) , mutex(std::make_shared()) + , bits(0) + , mask(0) { } +std::unordered_map Blackboard::mapping; Blackboard::Blackboard(const nlohmann::json &stats) : Blackboard() { this->stats = stats; + 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 { - return is_satisfied_by(this->stats, other.stats); + OgreAssert(mask != 0 && other.mask != 0, "blackboard not prepared"); +#if 0 + if (mask == 0 || other.mask == 0) + return is_satisfied_by(this->stats, other.stats); + else +#endif + return (bits & other.mask) == (other.bits & other.mask); } bool Blackboard::operator!=(const Blackboard &other) const @@ -668,6 +696,12 @@ 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() @@ -759,6 +793,37 @@ void Blackboard::actionRefAddActions(goap::BaseAction **actions, } } +void Blackboard::updateBits(const std::vector &updateBits, + const nlohmann::json &props) +{ + for (const auto &mbits : updateBits) { + // OgreAssert(mapping.find(mbits.checkValue) != mapping.end(), + // "No key " + mbits.checkValue); + OgreAssert(mapping.find(mbits.writeValue) != mapping.end(), + "No key " + mbits.writeValue); + + int value = stats[mbits.checkValue].get(); + int maxValue = mbits.maxValue; + int minValue = mbits.minValue; + int threshold = mbits.threshold; + if (props.find(mbits.checkValue + "_max") != props.end()) + maxValue = props[mbits.checkValue + "_max"].get(); + if (props.find(mbits.checkValue + "_threshold") != props.end()) + threshold = props[mbits.checkValue + "_threshold"] + .get(); + value += mbits.recover; + if (value > maxValue) + value = maxValue; + if (value >= threshold) + stats[mbits.writeValue] = 1; + else + stats[mbits.writeValue] = 0; + if (value < mbits.minValue) + value = mbits.minValue; + stats[mbits.checkValue] = value; + } +} + struct ComparePair { const nlohmann::json ¤t; const nlohmann::json ⌖ @@ -802,7 +867,9 @@ bool Blackboard::is_satisfied_by(const nlohmann::json ¤t, 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(); +#if 0 OgreAssert(goal.stats.is_object(), "Not an object:\n" + goal.stats.dump(4)); for (auto it = goal.stats.begin(); it != goal.stats.end(); ++it) { @@ -829,6 +896,7 @@ int Blackboard::distance_to(const Blackboard &goal) const } } return distance; +#endif } void Blackboard::setPosition(const Ogre::Vector3 &position) diff --git a/src/gamedata/CharacterAIModule.h b/src/gamedata/CharacterAIModule.h index 262f170..d37fd83 100644 --- a/src/gamedata/CharacterAIModule.h +++ b/src/gamedata/CharacterAIModule.h @@ -9,13 +9,23 @@ namespace ECS { struct Blackboard { - nlohmann::json stats; + struct UpdateBit { + Ogre::String checkValue; + int recover; + int minValue; + int maxValue; + int threshold; + Ogre::String writeValue; + }; int object; int index; flecs::entity town; std::shared_ptr mutex; + static std::unordered_map mapping; private: + nlohmann::json stats; + std::bitset<64> bits, mask; std::vector *> actionRef; int actionRefCount; int actionRefPtr; @@ -25,6 +35,7 @@ private: void _actionRefResize(int count); void _actionRefAddActions( const std::vector *> &actions); + void populate(const nlohmann::json &stats, std::unordered_map& mapping); public: Blackboard(); @@ -41,14 +52,32 @@ public: const std::vector *> &actions); void actionRefAddActions(goap::BaseAction **actions, int count); + const goap::BaseAction *const *getActionsData() const + { + return actionRef.data(); + } goap::BaseAction **getActionsData() { return actionRef.data(); } - int getActionsCount() + int getActionsCount() const { return actionRefCount; } + void commit() + { + populate(stats, mapping); + } + void dump_bits() const + { + std::cout << "bits: " << bits << std::endl; + std::cout << "mask: " << mask << std::endl; + } + bool is_valid() const + { + return !stats.is_null() && mask != 0; + } + void updateBits(const std::vector &updateBits, const nlohmann::json &props); private: static bool is_satisfied_by(const nlohmann::json ¤t, @@ -79,6 +108,7 @@ struct TownAI { planner; std::unordered_map blackboards; typedef goap::BasePlanner >::BaseGoal goal_t; + typedef goap::BasePlanner > planner_t; struct Plan { const goal_t *goal; std::vector *> plan; @@ -94,6 +124,7 @@ struct CharacterAIModule { void createBlackboards(flecs::entity town, const TownNPCs &npcs, TownAI &ai); void updateBlackboardsBits(flecs::entity town, ActionNodeList &alist, const TownNPCs &npcs, TownAI &ai); void updateBlackboards(flecs::entity town, const ActionNodeList &alist, const TownNPCs &npcs, TownAI &ai); + void prepareActions(TownAI &ai); }; }