1260 lines
44 KiB
C++
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 ¤t;
|
|
const nlohmann::json ⌖
|
|
};
|
|
|
|
bool Blackboard::is_satisfied_by(const nlohmann::json ¤t,
|
|
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);
|
|
}
|
|
}
|
|
}
|