Files
ogre-prototype/src/gamedata/CharacterAIModule.cpp

1260 lines
44 KiB
C++

#include <iostream>
#include <nlohmann/json.hpp>
#include <Ogre.h>
#include "goap.h"
#include "CharacterManagerModule.h"
#include "PlayerActionModule.h"
#include "CharacterModule.h"
#include "items.h"
#include "CharacterAIModule.h"
#include <tracy/Tracy.hpp>
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<Blackboard> *action;
ActionExec(PlanExecData &data,
const goap::BaseAction<Blackboard> *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<struct ActionExec *> 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<Blackboard> {
int node;
WalkToAction(int node, int cost)
: goap::BaseAction<Blackboard>(
"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<ActionNodeList>();
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<ActionNodeList>();
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<Blackboard> {
int node;
float radius;
Ogre::String action;
RunActionNode(int node, const Ogre::String &action,
const Blackboard &prereq,
const Blackboard &effects, int cost)
: goap::BaseAction<Blackboard>(
"Use(" + action + "," +
Ogre::StringConverter::toString(
node) +
")",
prereq, effects, cost)
, node(node)
, action(action)
{
const ActionNodeList &alist =
ECS::get<ActionNodeList>();
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<ActionNodeList>();
const Ogre::Vector3 &nodePosition =
alist.dynamicNodes[node].position;
const Ogre::Vector3 &npcPosition = state.getPosition();
return (npcPosition.squaredDistance(nodePosition) <
radius * radius);
}
};
std::vector<goap::BaseAction<Blackboard> *> 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<CharacterModule>()
.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<ActionNodeList>()
.dynamicNodes[wtaction->node]
.radius;
targetPosition = ECS::get<ActionNodeList>()
.dynamicNodes[wtaction->node]
.position;
Ogre::Vector3 direction =
(targetPosition - npc.position).normalisedCopy();
targetPosition -= direction * radius;
}
public:
ActionExecWalk(ActionExec::PlanExecData &data,
goap::BaseAction<Blackboard> *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<const ActionNodeActions::RunActionNode
*>(action);
OgreAssert(false, "activate");
}
public:
ActionExecUse(ActionExec::PlanExecData &data,
goap::BaseAction<Blackboard> *action)
: ActionExec(data, action)
{
}
};
ActionNodeActions(int node, const Blackboard &prereq, int cost)
{
ZoneScoped;
OgreAssert(
node < ECS::get<ActionNodeList>().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<ActionNodeList>().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>();
Ogre::String effectName = "";
#if 0
if (action == "sit") {
const Ogre::String &nodeName =
props["name"].get<Ogre::String>();
effectName = "is_" + nodeName + "_seated";
} else if (action == "use") {
const Ogre::String &nodeName =
props["name"].get<Ogre::String>();
effectName = "is_" + nodeName + "_used";
}
#endif
// const Ogre::String &nodeName =
// props["name"].get<Ogre::String>();
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<Ogre::String>() == "") {
} 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<goap::BaseAction<Blackboard> *> 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<Blackboard> *action)
: ActionExec(data, action)
, delay(0.0f)
{
}
};
CharacterAIModule::CharacterAIModule(flecs::world &ecs)
{
static std::mutex ecs_mutex;
ecs.module<CharacterAIModule>();
ecs.import <CharacterManagerModule>();
ecs.import <PlayerActionModule>();
ecs.component<Blackboard>();
ecs.component<TownAI>().on_add([](flecs::entity e, TownAI &ai) {
std::lock_guard<std::mutex> lock(ecs_mutex);
ai.mutex = std::make_shared<std::mutex>();
std::lock_guard<std::mutex> 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<Blackboard>(
adata.name, adata.prereq, adata.effects,
adata.cost));
ai.planner = std::make_shared<goap::BasePlanner<
Blackboard, goap::BaseAction<Blackboard> > >();
});
ecs.system<TownAI, TownNPCs>("CreateBlackboards")
.kind(flecs::OnUpdate)
.each([this](flecs::entity town, TownAI &ai,
const TownNPCs &npcs) {
ZoneScopedN("CreateBlackboards");
std::lock_guard<std::mutex> lock(ecs_mutex);
OgreAssert(npcs.npcs.size() > 0, "npcs not crated");
createBlackboards(town, npcs, ai);
});
ecs.system<ActionNodeList, TownAI, TownNPCs>("UpdateDynamicActions")
.kind(flecs::OnUpdate)
.each([](flecs::entity e, ActionNodeList &alist, TownAI &ai,
TownNPCs &npcs) {
ZoneScopedN("UpdateDynamicActions");
std::lock_guard<std::mutex> 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<ActionNodeList>()
.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<ActionNodeList, TownAI, TownNPCs>("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<std::mutex> lock(ecs_mutex);
ECS::get_mut<ActionNodeList>().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<float, std::milli> elapsed =
end - start;
std::cout << what << " " << elapsed.count()
<< std::endl;
}
};
ecs.system<TownNPCs>("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<CharacterBase>()) {
Ogre::SceneNode *n =
ECS::get<CharacterModule>()
.characterNodes.at(
npc.e);
npc.position = n->_getDerivedPosition();
}
}
});
ecs.system<ActionNodeList, TownAI, TownNPCs>("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<std::mutex> lock(
ecs_mutex);
updateBlackboards(town, alist, npcs,
ai);
}
Ogre::Root::getSingleton()
.getWorkQueue()
->addMainThreadTask([this, town,
&alist]() {
ZoneScopedN(
"UpdateBlackboards::MainThread");
town.modified<TownAI>();
town.modified<TownNPCs>();
ECS::modified<ActionNodeList>();
});
});
});
ecs.system<TownAI, TownNPCs>("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<std::mutex> lock(ecs_mutex);
buildPlans(town, npcs, ai);
Ogre::Root::getSingleton()
.getWorkQueue()
->addMainThreadTask([this, town]() {
ZoneScopedN(
"PlanAI::MainThread");
town.modified<TownAI>();
});
});
});
static std::unordered_map<int, struct PlanExec> plan_exec;
ecs.system<const EngineData, TownNPCs, TownAI>("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<TownAI>();
}
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<goap::BaseAction<Blackboard> *> 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<PlanTask> 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<std::mutex> 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<std::mutex> 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<int>();
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<int>() >
20;
})
->addRule(
"hungry",
[](const nlohmann::json &data)
-> bool {
return data["needs_hunger"]
.get<int>() >
2000;
})
->addRule(
"thirsty",
[](const nlohmann::json &data)
-> bool {
return data["needs_thirst"]
.get<int>() >
1000;
})
->addRule(
"toilet",
[](const nlohmann::json &data)
-> bool {
return data["needs_toilet"]
.get<int>() >
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<std::mutex> 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>();
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<std::mutex>())
, bits(0)
, mask(0)
{
}
std::unordered_map<std::string, size_t> Blackboard::mapping;
Blackboard::Blackboard(const nlohmann::json &stats)
: Blackboard()
{
populate(stats, mapping);
}
void Blackboard::populate(const nlohmann::json &stats,
std::unordered_map<std::string, size_t> &mapping)
{
if (stats.empty())
return;
for (auto &[key, value] : stats.items()) {
if (value.is_number_integer()) {
int val = value.get<int>();
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<std::mutex> 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<std::mutex> 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<std::mutex> lock(*mutex);
_actionRefResize(count);
}
void Blackboard::actionRefAddAction(goap::BaseAction<Blackboard> *action)
{
std::lock_guard<std::mutex> 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<goap::BaseAction<Blackboard> *> &actions)
{
std::lock_guard<std::mutex> lock(*mutex);
_actionRefAddActions(actions);
}
void Blackboard::_actionRefAddActions(
const std::vector<goap::BaseAction<Blackboard> *> &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<Blackboard> **actions,
int count)
{
int i;
std::lock_guard<std::mutex> 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 &current;
const nlohmann::json &target;
};
bool Blackboard::is_satisfied_by(const nlohmann::json &current,
const nlohmann::json &target, float epsilon)
{
std::deque<ComparePair> 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<float>() - tgt.get<float>()) >=
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<std::mutex> lock(*mutex);
this->position = position;
}
void Blackboard::query_ai()
{
std::lock_guard<std::mutex> lock(*mutex);
TownAI &ai = town.get_mut<TownAI>();
const TownNPCs &npcs = town.get<TownNPCs>();
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<CharacterBase>()) {
Ogre::SceneNode *n =
ECS::get<CharacterModule>().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<ActionNodeList>();
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);
}
}
}