osb/source/game/StarBehaviorDatabase.cpp
2024-03-15 21:28:11 +11:00

299 lines
11 KiB
C++

#include "StarBehaviorDatabase.hpp"
#include "StarAssets.hpp"
#include "StarRoot.hpp"
#include "StarJsonExtra.hpp"
namespace Star {
EnumMap<NodeParameterType> const NodeParameterTypeNames {
{NodeParameterType::Json, "json"},
{NodeParameterType::Entity, "entity"},
{NodeParameterType::Position, "position"},
{NodeParameterType::Vec2, "vec2"},
{NodeParameterType::Number, "number"},
{NodeParameterType::Bool, "bool"},
{NodeParameterType::List, "list"},
{NodeParameterType::Table, "table"},
{NodeParameterType::String, "string"}
};
NodeParameterValue nodeParameterValueFromJson(Json const& json) {
if (auto key = json.optString("key"))
return *key;
else
return json.get("value");
}
Json jsonFromNodeParameter(NodeParameter const& parameter) {
JsonObject json {
{"type", NodeParameterTypeNames.getRight(parameter.first)}
};
if (auto key = parameter.second.maybe<String>())
json.set("key", *key);
else
json.set("value", parameter.second.get<Json>());
return json;
}
NodeParameter jsonToNodeParameter(Json const& json) {
NodeParameterType type = NodeParameterTypeNames.getLeft(json.getString("type"));
if (auto key = json.optString("key"))
return {type, *key};
else
return {type, json.opt("value").value(Json())};
}
Json nodeOutputToJson(NodeOutput const& output) {
return JsonObject {
{"type", NodeParameterTypeNames.getRight(output.first)},
{"key", jsonFromMaybe<String>(output.second.first, [](String const& s) { return Json(s); })},
{"ephemeral", output.second.second}
};
}
NodeOutput jsonToNodeOutput(Json const& json) {
return {
NodeParameterTypeNames.getLeft(json.getString("type")),
{jsonToMaybe<String>(json.get("key"), [](Json const& j) { return j.toString(); }), json.optBool("ephemeral").value(false)}
};
}
EnumMap<BehaviorNodeType> const BehaviorNodeTypeNames {
{BehaviorNodeType::Action, "Action"},
{BehaviorNodeType::Decorator, "Decorator"},
{BehaviorNodeType::Composite, "Composite"},
{BehaviorNodeType::Module, "Module"}
};
EnumMap<CompositeType> const CompositeTypeNames {
{CompositeType::Sequence, "Sequence"},
{CompositeType::Selector, "Selector"},
{CompositeType::Parallel, "Parallel"},
{CompositeType::Dynamic, "Dynamic"},
{CompositeType::Randomize, "Randomize"}
};
void applyTreeParameters(StringMap<NodeParameter>& nodeParameters, StringMap<NodeParameterValue> const& treeParameters) {
for (auto& p : nodeParameters) {
NodeParameter& parameter = p.second;
parameter.second = replaceBehaviorTag(parameter.second, treeParameters);
}
}
NodeParameterValue replaceBehaviorTag(NodeParameterValue const& parameter, StringMap<NodeParameterValue> const& treeParameters) {
Maybe<String> strVal = parameter.maybe<String>();
if (!strVal && parameter.get<Json>().isType(Json::Type::String))
strVal = parameter.get<Json>().toString();
if (strVal) {
String key = *strVal;
if (key.beginsWith('<') && key.endsWith('>')) {
String treeKey = key.substr(1, key.size() - 2);
if (auto replace = treeParameters.maybe(treeKey)) {
return *replace;
} else {
throw StarException(strf("No parameter specified for tag '{}'", key));
}
}
}
return parameter;
}
Maybe<String> replaceOutputBehaviorTag(Maybe<String> const& output, StringMap<NodeParameterValue> const& treeParameters) {
if (auto out = output) {
if (out->beginsWith('<') && out->endsWith('>')) {
if (auto replace = treeParameters.maybe(out->substr(1, out->size() - 2))) {
if (auto key = replace->maybe<String>())
return *key;
else if (replace->get<Json>().isType(Json::Type::String))
return replace->get<Json>().toString();
else
return {};
} else {
throw StarException(strf("No parameter specified for tag '{}'", *out));
}
}
}
return output;
}
// TODO: This is temporary until BehaviorState can handle valueType:value pairs
void parseNodeParameters(JsonObject& parameters) {
for (auto& p : parameters)
p.second = p.second.opt("key").orMaybe(p.second.opt("value")).value(Json());
}
ActionNode::ActionNode(String name, StringMap<NodeParameter> parameters, StringMap<NodeOutput> output)
: name(std::move(name)), parameters(std::move(parameters)), output(std::move(output)) { }
DecoratorNode::DecoratorNode(String const& name, StringMap<NodeParameter> parameters, BehaviorNodeConstPtr child)
: name(name), parameters(parameters), child(child) { }
SequenceNode::SequenceNode(List<BehaviorNodeConstPtr> children) : children(children) { }
SelectorNode::SelectorNode(List<BehaviorNodeConstPtr> children) : children(children) { }
ParallelNode::ParallelNode(StringMap<NodeParameter> parameters, List<BehaviorNodeConstPtr> children) : children(children) {
int s = parameters.get("success").second.get<Json>().optInt().value(-1);
succeed = s == -1 ? children.size() : s;
int f = parameters.get("fail").second.get<Json>().optInt().value(-1);
fail = f == -1 ? children.size() : f;
}
DynamicNode::DynamicNode(List<BehaviorNodeConstPtr> children) : children(children) { }
RandomizeNode::RandomizeNode(List<BehaviorNodeConstPtr> children) : children(children) { }
BehaviorTree::BehaviorTree(String const& name, StringSet scripts, JsonObject const& parameters)
: name(name), scripts(scripts), parameters(parameters) { }
BehaviorDatabase::BehaviorDatabase() {
auto assets = Root::singleton().assets();
auto& nodeFiles = assets->scanExtension("nodes");
assets->queueJsons(nodeFiles);
for (String const& file : nodeFiles) {
try {
Json nodes = assets->json(file);
for (auto& node : nodes.toObject()) {
StringMap<NodeParameter> parameters;
for (auto p : node.second.getObject("properties", {}))
parameters.set(p.first, jsonToNodeParameter(p.second));
m_nodeParameters.set(node.first, parameters);
StringMap<NodeOutput> output;
for (auto p : node.second.getObject("output", {}))
output.set(p.first, jsonToNodeOutput(p.second));
m_nodeOutput.set(node.first, output);
}
} catch (StarException const& e) {
throw StarException(strf("Could not load nodes file \'{}\'", file), e);
}
}
auto& behaviorFiles = assets->scanExtension("behavior");
assets->queueJsons(behaviorFiles);
for (auto const& file : behaviorFiles) {
try {
auto config = assets->json(file);
auto name = config.getString("name");
if (m_configs.contains(name))
throw StarException(strf("Duplicate behavior tree \'{}\'", name));
m_configs[name] = config;
} catch (StarException const& e) {
throw StarException(strf("Could not load behavior file \'{}\'", file), e);
}
}
for (auto& pair : m_configs) {
if (!m_behaviors.contains(pair.first))
loadTree(pair.first);
}
}
BehaviorTreeConstPtr BehaviorDatabase::behaviorTree(String const& name) const {
if (!m_behaviors.contains(name))
throw StarException(strf("No such behavior tree \'{}\'", name));
return m_behaviors.get(name);
}
BehaviorTreeConstPtr BehaviorDatabase::buildTree(Json const& config, StringMap<NodeParameterValue> const& overrides) const {
StringSet scripts = jsonToStringSet(config.get("scripts", JsonArray()));
auto tree = BehaviorTree(config.getString("name"), scripts, config.getObject("parameters", {}));
StringMap<NodeParameterValue> parameters;
for (auto p : config.getObject("parameters", {}))
parameters.set(p.first, p.second);
for (auto p : overrides)
parameters.set(p.first, p.second);
BehaviorNodeConstPtr root = behaviorNode(config.get("root"), parameters, tree);
tree.root = root;
return std::make_shared<BehaviorTree>(std::move(tree));
}
Json BehaviorDatabase::behaviorConfig(String const& name) const {
if (!m_configs.contains(name))
throw StarException(strf("No such behavior tree \'{}\'", name));
return m_configs.get(name);
}
void BehaviorDatabase::loadTree(String const& name) {
m_behaviors.set(name, buildTree(m_configs.get(name)));
}
CompositeNode BehaviorDatabase::compositeNode(Json const& config, StringMap<NodeParameter> parameters, StringMap<NodeParameterValue> const& treeParameters, BehaviorTree& tree) const {
List<BehaviorNodeConstPtr> children = config.getArray("children", {}).transformed([this,treeParameters,&tree](Json const& child) {
return behaviorNode(child, treeParameters, tree);
});
CompositeType type = CompositeTypeNames.getLeft(config.getString("name"));
if (type == CompositeType::Sequence)
return SequenceNode(children);
else if (type == CompositeType::Selector)
return SelectorNode(children);
else if (type == CompositeType::Parallel)
return ParallelNode(parameters, children);
else if (type == CompositeType::Dynamic)
return DynamicNode(children);
else if (type == CompositeType::Randomize)
return RandomizeNode(children);
// above statement needs to be exhaustive
throw StarException(strf("Composite node type '{}' could not be created from JSON", CompositeTypeNames.getRight(type)));
}
BehaviorNodeConstPtr BehaviorDatabase::behaviorNode(Json const& json, StringMap<NodeParameterValue> const& treeParameters, BehaviorTree& tree) const {
BehaviorNodeType type = BehaviorNodeTypeNames.getLeft(json.getString("type"));
auto name = json.getString("name");
auto parameterConfig = json.getObject("parameters", {});
if (type == BehaviorNodeType::Module) {
// merge in module parameters to a copy of the treeParameters to propagate
// tree parameters into the sub-tree, but allow modules to override
auto moduleParameters = treeParameters;
for (auto p : parameterConfig)
moduleParameters.set(p.first, replaceBehaviorTag(nodeParameterValueFromJson(p.second), treeParameters));
BehaviorTree module = *buildTree(m_configs.get(name), moduleParameters);
tree.scripts.addAll(module.scripts);
tree.functions.addAll(module.functions);
return module.root;
}
StringMap<NodeParameter> parameters = m_nodeParameters.get(name);
for (auto& p : parameters)
p.second.second = parameterConfig.maybe(p.first).apply(nodeParameterValueFromJson).value(p.second.second);
applyTreeParameters(parameters, treeParameters);
if (type == BehaviorNodeType::Action) {
tree.functions.add(name);
Json outputConfig = json.getObject("output", {});
StringMap<NodeOutput> output = m_nodeOutput.get(name);
for (auto& p : output)
p.second.second.first = replaceOutputBehaviorTag(outputConfig.optString(p.first).orMaybe(p.second.second.first), treeParameters);
return make_shared<BehaviorNode>(ActionNode(name, parameters, output));
} else if (type == BehaviorNodeType::Decorator) {
tree.functions.add(name);
BehaviorNodeConstPtr child = behaviorNode(json.get("child"), treeParameters, tree);
return make_shared<BehaviorNode>(DecoratorNode(name, parameters, child));
} else if (type == BehaviorNodeType::Composite) {
return make_shared<BehaviorNode>(compositeNode(json, parameters, treeParameters, tree));
}
// above statement must be exhaustive
throw StarException(strf("Behavior node type '{}' could not be created from JSON", BehaviorNodeTypeNames.getRight(type)));
}
}