com.badlogic.gdx.ai.btree.utils.BehaviorTreeParser Maven / Gradle / Ivy
/*******************************************************************************
* Copyright 2014 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.badlogic.gdx.ai.btree.utils;
import java.io.InputStream;
import java.io.Reader;
import com.badlogic.gdx.ai.btree.BehaviorTree;
import com.badlogic.gdx.ai.btree.Task;
import com.badlogic.gdx.ai.btree.annotation.TaskAttribute;
import com.badlogic.gdx.ai.btree.annotation.TaskConstraint;
import com.badlogic.gdx.ai.btree.branch.DynamicGuardSelector;
import com.badlogic.gdx.ai.btree.branch.Parallel;
import com.badlogic.gdx.ai.btree.branch.RandomSelector;
import com.badlogic.gdx.ai.btree.branch.RandomSequence;
import com.badlogic.gdx.ai.btree.branch.Selector;
import com.badlogic.gdx.ai.btree.branch.Sequence;
import com.badlogic.gdx.ai.btree.decorator.AlwaysFail;
import com.badlogic.gdx.ai.btree.decorator.AlwaysSucceed;
import com.badlogic.gdx.ai.btree.decorator.Include;
import com.badlogic.gdx.ai.btree.decorator.Invert;
import com.badlogic.gdx.ai.btree.decorator.Random;
import com.badlogic.gdx.ai.btree.decorator.Repeat;
import com.badlogic.gdx.ai.btree.decorator.SemaphoreGuard;
import com.badlogic.gdx.ai.btree.decorator.UntilFail;
import com.badlogic.gdx.ai.btree.decorator.UntilSuccess;
import com.badlogic.gdx.ai.btree.leaf.Failure;
import com.badlogic.gdx.ai.btree.leaf.Success;
import com.badlogic.gdx.ai.btree.leaf.Wait;
import com.badlogic.gdx.ai.utils.random.Distribution;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.ObjectMap.Entries;
import com.badlogic.gdx.utils.ObjectMap.Entry;
import com.badlogic.gdx.utils.ObjectSet;
import com.badlogic.gdx.utils.SerializationException;
import com.badlogic.gdx.utils.reflect.Annotation;
import com.badlogic.gdx.utils.reflect.ClassReflection;
import com.badlogic.gdx.utils.reflect.Field;
import com.badlogic.gdx.utils.reflect.ReflectionException;
/** A {@link BehaviorTree} parser.
*
* @author davebaol */
public class BehaviorTreeParser {
public static final int DEBUG_NONE = 0;
public static final int DEBUG_LOW = 1;
public static final int DEBUG_HIGH = 2;
public int debugLevel;
public DistributionAdapters distributionAdapters;
private DefaultBehaviorTreeReader btReader;
public BehaviorTreeParser () {
this(DEBUG_NONE);
}
public BehaviorTreeParser (DistributionAdapters distributionAdapters) {
this(distributionAdapters, DEBUG_NONE);
}
public BehaviorTreeParser (int debugLevel) {
this(new DistributionAdapters(), debugLevel);
}
public BehaviorTreeParser (DistributionAdapters distributionAdapters, int debugLevel) {
this(distributionAdapters, debugLevel, null);
}
public BehaviorTreeParser (DistributionAdapters distributionAdapters, int debugLevel, DefaultBehaviorTreeReader reader) {
this.distributionAdapters = distributionAdapters;
this.debugLevel = debugLevel;
btReader = reader == null ? new DefaultBehaviorTreeReader() : reader;
btReader.setParser(this);
}
/** Parses the given string.
* @param string the string to parse
* @param object the blackboard object. It can be {@code null}.
* @return the behavior tree
* @throws SerializationException if the string cannot be successfully parsed. */
public BehaviorTree parse (String string, E object) {
btReader.parse(string);
return createBehaviorTree(btReader.root, object);
}
/** Parses the given input stream.
* @param input the input stream to parse
* @param object the blackboard object. It can be {@code null}.
* @return the behavior tree
* @throws SerializationException if the input stream cannot be successfully parsed. */
public BehaviorTree parse (InputStream input, E object) {
btReader.parse(input);
return createBehaviorTree(btReader.root, object);
}
/** Parses the given file.
* @param file the file to parse
* @param object the blackboard object. It can be {@code null}.
* @return the behavior tree
* @throws SerializationException if the file cannot be successfully parsed. */
public BehaviorTree parse (FileHandle file, E object) {
btReader.parse(file);
return createBehaviorTree(btReader.root, object);
}
/** Parses the given reader.
* @param reader the reader to parse
* @param object the blackboard object. It can be {@code null}.
* @return the behavior tree
* @throws SerializationException if the reader cannot be successfully parsed. */
public BehaviorTree parse (Reader reader, E object) {
btReader.parse(reader);
return createBehaviorTree(btReader.root, object);
}
protected BehaviorTree createBehaviorTree (Task root, E object) {
if (debugLevel > BehaviorTreeParser.DEBUG_LOW) printTree(root, 0);
return new BehaviorTree(root, object);
}
protected static void printTree (Task task, int indent) {
for (int i = 0; i < indent; i++)
System.out.print(' ');
if (task.getGuard() != null) {
System.out.println("Guard");
indent = indent + 2;
printTree(task.getGuard(), indent);
for (int i = 0; i < indent; i++)
System.out.print(' ');
}
System.out.println(task.getClass().getSimpleName());
for (int i = 0; i < task.getChildCount(); i++) {
printTree(task.getChild(i), indent + 2);
}
}
public static class DefaultBehaviorTreeReader extends BehaviorTreeReader {
private static final ObjectMap DEFAULT_IMPORTS = new ObjectMap();
static {
Class>[] classes = new Class>[] {// @off - disable libgdx formatter
AlwaysFail.class,
AlwaysSucceed.class,
DynamicGuardSelector.class,
Failure.class,
Include.class,
Invert.class,
Parallel.class,
Random.class,
RandomSelector.class,
RandomSequence.class,
Repeat.class,
Selector.class,
SemaphoreGuard.class,
Sequence.class,
Success.class,
UntilFail.class,
UntilSuccess.class,
Wait.class
}; // @on - enable libgdx formatter
for (Class> c : classes) {
String fqcn = c.getName();
String cn = c.getSimpleName();
String alias = Character.toLowerCase(cn.charAt(0)) + (cn.length() > 1 ? cn.substring(1) : "");
DEFAULT_IMPORTS.put(alias, fqcn);
}
}
enum Statement {
Import("import") {
@Override
protected void enter (DefaultBehaviorTreeReader reader, String name, boolean isGuard) {
}
@Override
protected boolean attribute (DefaultBehaviorTreeReader reader, String name, Object value) {
if (!(value instanceof String)) reader.throwAttributeTypeException(this.name, name, "String");
reader.addImport(name, (String)value);
return true;
}
@Override
protected void exit (DefaultBehaviorTreeReader reader) {
return;
}
},
Subtree("subtree") {
@Override
protected void enter (DefaultBehaviorTreeReader reader, String name, boolean isGuard) {
}
@Override
protected boolean attribute (DefaultBehaviorTreeReader reader, String name, Object value) {
if (!name.equals("name")) reader.throwAttributeNameException(this.name, name, "name");
if (!(value instanceof String)) reader.throwAttributeTypeException(this.name, name, "String");
if ("".equals(value)) throw new GdxRuntimeException(this.name + ": the name connot be empty");
if (reader.subtreeName != null)
throw new GdxRuntimeException(this.name + ": the name has been already specified");
reader.subtreeName = (String)value;
return true;
}
@Override
protected void exit (DefaultBehaviorTreeReader reader) {
if (reader.subtreeName == null)
throw new GdxRuntimeException(this.name + ": the name has not been specified");
reader.switchToNewTree(reader.subtreeName);
reader.subtreeName = null;
}
},
Root("root") {
@Override
protected void enter (DefaultBehaviorTreeReader reader, String name, boolean isGuard) {
reader.subtreeName = ""; // the root tree has empty name
}
@Override
protected boolean attribute (DefaultBehaviorTreeReader reader, String name, Object value) {
reader.throwAttributeTypeException(this.name, name, null);
return true;
}
@Override
protected void exit (DefaultBehaviorTreeReader reader) {
reader.switchToNewTree(reader.subtreeName);
reader.subtreeName = null;
}
},
TreeTask(null) {
@Override
protected void enter (DefaultBehaviorTreeReader reader, String name, boolean isGuard) {
// Root tree is the default one
if (reader.currentTree == null) {
reader.switchToNewTree("");
reader.subtreeName = null;
}
reader.openTask(name, isGuard);
}
@Override
protected boolean attribute (DefaultBehaviorTreeReader reader, String name, Object value) {
StackedTask stackedTask = reader.getCurrentTask();
AttrInfo ai = stackedTask.metadata.attributes.get(name);
if (ai == null) return false;
boolean isNew = reader.encounteredAttributes.add(name);
if (!isNew) throw reader.stackedTaskException(stackedTask, "attribute '" + name + "' specified more than once");
Field attributeField = reader.getField(stackedTask.task.getClass(), ai.fieldName);
reader.setField(attributeField, stackedTask.task, value);
return true;
}
@Override
protected void exit (DefaultBehaviorTreeReader reader) {
if (!reader.isSubtreeRef) {
reader.checkRequiredAttributes(reader.getCurrentTask());
reader.encounteredAttributes.clear();
}
}
};
String name;
Statement(String name) {
this.name = name;
}
protected abstract void enter (DefaultBehaviorTreeReader reader, String name, boolean isGuard);
protected abstract boolean attribute (DefaultBehaviorTreeReader reader, String name, Object value);
protected abstract void exit (DefaultBehaviorTreeReader reader);
}
protected BehaviorTreeParser btParser;
ObjectMap, Metadata> metadataCache = new ObjectMap, Metadata>();
Task root;
String subtreeName;
Statement statement;
private int indent;
public DefaultBehaviorTreeReader () {
this(false);
}
public DefaultBehaviorTreeReader (boolean reportsComments) {
super(reportsComments);
}
public BehaviorTreeParser getParser () {
return btParser;
}
public void setParser (BehaviorTreeParser parser) {
this.btParser = parser;
}
@Override
public void parse (char[] data, int offset, int length) {
debug = btParser.debugLevel > BehaviorTreeParser.DEBUG_NONE;
root = null;
clear();
super.parse(data, offset, length);
// Pop all task from the stack and check their minimum number of children
popAndCheckMinChildren(0);
Subtree rootTree = subtrees.get("");
if (rootTree == null) throw new GdxRuntimeException("Missing root tree");
root = rootTree.rootTask;
if (root == null) throw new GdxRuntimeException("The tree must have at least the root task");
clear();
}
@Override
protected void startLine (int indent) {
if (btParser.debugLevel > BehaviorTreeParser.DEBUG_LOW)
System.out.println(lineNumber + ": <" + indent + ">");
this.indent = indent;
}
private Statement checkStatement (String name) {
if (name.equals(Statement.Import.name)) return Statement.Import;
if (name.equals(Statement.Subtree.name)) return Statement.Subtree;
if (name.equals(Statement.Root.name)) return Statement.Root;
return Statement.TreeTask;
}
@Override
protected void startStatement (String name, boolean isSubtreeReference, boolean isGuard) {
if (btParser.debugLevel > BehaviorTreeParser.DEBUG_LOW)
System.out.println((isGuard? " guard" : " task") + " name '" + name + "'");
this.isSubtreeRef = isSubtreeReference;
this.statement = isSubtreeReference ? Statement.TreeTask : checkStatement(name);
if (isGuard) {
if (statement != Statement.TreeTask)
throw new GdxRuntimeException(name + ": only tree's tasks can be guarded");
}
statement.enter(this, name, isGuard);
}
@Override
protected void attribute (String name, Object value) {
if (btParser.debugLevel > BehaviorTreeParser.DEBUG_LOW)
System.out.println(lineNumber + ": attribute '" + name + " : " + value + "'");
boolean validAttribute = statement.attribute(this, name, value);
if (!validAttribute) {
if (statement == Statement.TreeTask) {
throw stackedTaskException(getCurrentTask(), "unknown attribute '" + name + "'");
} else {
throw new GdxRuntimeException(statement.name + ": unknown attribute '" + name + "'");
}
}
}
private Field getField (Class> clazz, String name) {
try {
return ClassReflection.getField(clazz, name);
} catch (ReflectionException e) {
throw new GdxRuntimeException(e);
}
}
private void setField (Field field, Task task, Object value) {
field.setAccessible(true);
Object valueObject = castValue(field, value);
try {
field.set(task, valueObject);
} catch (ReflectionException e) {
throw new GdxRuntimeException(e);
}
}
private Object castValue (Field field, Object value) {
Class> type = field.getType();
Object ret = null;
if (value instanceof Number) {
Number numberValue = (Number)value;
if (type == int.class || type == Integer.class)
ret = numberValue.intValue();
else if (type == float.class || type == Float.class)
ret = numberValue.floatValue();
else if (type == long.class || type == Long.class)
ret = numberValue.longValue();
else if (type == double.class || type == Double.class)
ret = numberValue.doubleValue();
else if (type == short.class || type == Short.class)
ret = numberValue.shortValue();
else if (type == byte.class || type == Byte.class)
ret = numberValue.byteValue();
else if (ClassReflection.isAssignableFrom(Distribution.class, type)) {
@SuppressWarnings("unchecked")
Class distributionType = (Class)type;
ret = btParser.distributionAdapters.toDistribution("constant," + numberValue, distributionType);
}
} else if (value instanceof Boolean) {
if (type == boolean.class || type == Boolean.class) ret = value;
} else if (value instanceof String) {
String stringValue = (String)value;
if (type == String.class)
ret = value;
else if (type == char.class || type == Character.class) {
if (stringValue.length() != 1) throw new GdxRuntimeException("Invalid character '" + value + "'");
ret = Character.valueOf(stringValue.charAt(0));
} else if (ClassReflection.isAssignableFrom(Distribution.class, type)) {
@SuppressWarnings("unchecked")
Class distributionType = (Class)type;
ret = btParser.distributionAdapters.toDistribution(stringValue, distributionType);
} else if (ClassReflection.isAssignableFrom(Enum.class, type)) {
Enum>[] constants = (Enum>[])type.getEnumConstants();
for (int i = 0, n = constants.length; i < n; i++) {
Enum> e = constants[i];
if (e.name().equalsIgnoreCase(stringValue)) {
ret = e;
break;
}
}
}
}
if (ret == null) throwAttributeTypeException(getCurrentTask().name, field.getName(), type.getSimpleName());
return ret;
}
private void throwAttributeNameException (String statement, String name, String expectedName) {
String expected = " no attribute expected";
if (expectedName != null)
expected = "expected '" + expectedName + "' instead";
throw new GdxRuntimeException(statement + ": attribute '" + name + "' unknown; " + expected);
}
private void throwAttributeTypeException (String statement, String name, String expectedType) {
throw new GdxRuntimeException(statement + ": attribute '" + name + "' must be of type " + expectedType);
}
@Override
protected void endLine () {
}
@Override
protected void endStatement () {
statement.exit(this);
}
private void openTask (String name, boolean isGuard) {
try {
Task task;
if (isSubtreeRef) {
task = subtreeRootTaskInstance(name);
}
else {
String className = getImport(name);
if (className == null) className = name;
@SuppressWarnings("unchecked")
Task tmpTask = (Task)ClassReflection.newInstance(ClassReflection.forName(className));
task = tmpTask;
}
if (!currentTree.inited()) {
initCurrentTree(task, indent);
indent = 0;
} else if (!isGuard) {
StackedTask stackedTask = getPrevTask();
indent -= currentTreeStartIndent;
if (stackedTask.task == currentTree.rootTask) {
step = indent;
}
if (indent > currentDepth) {
stack.add(stackedTask); // push
} else if (indent <= currentDepth) {
// Pop tasks from the stack based on indentation
// and check their minimum number of children
int i = (currentDepth - indent) / step;
popAndCheckMinChildren(stack.size - i);
}
// Check the max number of children of the parent
StackedTask stackedParent = stack.peek();
int maxChildren = stackedParent.metadata.maxChildren;
if (stackedParent.task.getChildCount() >= maxChildren)
throw stackedTaskException(stackedParent, "max number of children exceeded ("
+ (stackedParent.task.getChildCount() + 1) + " > " + maxChildren + ")");
// Add child task to the parent
stackedParent.task.addChild(task);
}
updateCurrentTask(createStackedTask(name, task), indent, isGuard);
} catch (ReflectionException e) {
throw new GdxRuntimeException("Cannot parse behavior tree!!!", e);
}
}
private StackedTask createStackedTask (String name, Task task) {
Metadata metadata = findMetadata(task.getClass());
if (metadata == null)
throw new GdxRuntimeException(name + ": @TaskConstraint annotation not found in '" + task.getClass().getSimpleName()
+ "' class hierarchy");
return new StackedTask(lineNumber, name, task, metadata);
}
private Metadata findMetadata (Class> clazz) {
Metadata metadata = metadataCache.get(clazz);
if (metadata == null) {
Annotation tca = ClassReflection.getAnnotation(clazz, TaskConstraint.class);
if (tca != null) {
TaskConstraint taskConstraint = tca.getAnnotation(TaskConstraint.class);
ObjectMap taskAttributes = new ObjectMap();
Field[] fields = ClassReflection.getFields(clazz);
for (Field f : fields) {
Annotation a = f.getDeclaredAnnotation(TaskAttribute.class);
if (a != null) {
AttrInfo ai = new AttrInfo(f.getName(), a.getAnnotation(TaskAttribute.class));
taskAttributes.put(ai.name, ai);
}
}
metadata = new Metadata(taskConstraint.minChildren(), taskConstraint.maxChildren(), taskAttributes);
metadataCache.put(clazz, metadata);
}
}
return metadata;
}
protected static class StackedTask {
public int lineNumber;
public String name;
public Task task;
public Metadata metadata;
StackedTask (int lineNumber, String name, Task task, Metadata metadata) {
this.lineNumber = lineNumber;
this.name = name;
this.task = task;
this.metadata = metadata;
}
}
private static class Metadata {
int minChildren;
int maxChildren;
ObjectMap attributes;
/** Creates a {@code Metadata} for a task accepting from {@code minChildren} to {@code maxChildren} children and the given
* attributes.
* @param minChildren the minimum number of children (defaults to 0 if negative)
* @param maxChildren the maximum number of children (defaults to {@link Integer.MAX_VALUE} if negative)
* @param attributes the attributes */
Metadata (int minChildren, int maxChildren, ObjectMap attributes) {
this.minChildren = minChildren < 0 ? 0 : minChildren;
this.maxChildren = maxChildren < 0 ? Integer.MAX_VALUE : maxChildren;
this.attributes = attributes;
}
}
private static class AttrInfo {
String name;
String fieldName;
boolean required;
AttrInfo (String fieldName, TaskAttribute annotation) {
this(annotation.name(), fieldName, annotation.required());
}
AttrInfo (String name, String fieldName, boolean required) {
this.name = name == null || name.length() == 0 ? fieldName : name;
this.fieldName = fieldName;
this.required = required;
}
}
protected static class Subtree {
String name; // root tree must have no name
Task rootTask;
int referenceCount;
Subtree() {
this(null);
}
Subtree(String name) {
this.name = name;
this.rootTask = null;
this.referenceCount = 0;
}
public void init(Task rootTask) {
this.rootTask = rootTask;
}
public boolean inited() {
return rootTask != null;
}
public boolean isRootTree() {
return name == null || "".equals(name);
}
public Task rootTaskInstance () {
if (referenceCount++ == 0) {
return rootTask;
}
return rootTask.cloneTask();
}
}
ObjectMap userImports = new ObjectMap();
ObjectMap> subtrees = new ObjectMap>();
Subtree currentTree;
int currentTreeStartIndent;
int currentDepth;
int step;
boolean isSubtreeRef;
protected StackedTask prevTask;
protected StackedTask guardChain;
protected Array> stack = new Array>();
ObjectSet encounteredAttributes = new ObjectSet();
boolean isGuard;
StackedTask getLastStackedTask() {
return stack.peek();
}
StackedTask getPrevTask() {
return prevTask;
}
StackedTask getCurrentTask() {
return isGuard? guardChain : prevTask;
}
void updateCurrentTask(StackedTask stackedTask, int indent, boolean isGuard) {
this.isGuard = isGuard;
stackedTask.task.setGuard(guardChain == null ? null : guardChain.task);
if (isGuard) {
guardChain = stackedTask;
}
else {
prevTask = stackedTask;
guardChain = null;
currentDepth = indent;
}
}
void clear() {
prevTask = null;
guardChain = null;
currentTree = null;
userImports.clear();
subtrees.clear();
stack.clear();
encounteredAttributes.clear();
}
//
// Subtree
//
void switchToNewTree(String name) {
// Pop all task from the stack and check their minimum number of children
popAndCheckMinChildren(0);
this.currentTree = new Subtree(name);
Subtree oldTree = subtrees.put(name, currentTree);
if (oldTree != null)
throw new GdxRuntimeException("A subtree named '" + name + "' is already defined");
}
void initCurrentTree(Task rootTask, int startIndent) {
currentDepth = -1;
step = 1;
currentTreeStartIndent = startIndent;
this.currentTree.init(rootTask);
prevTask = null;
}
Task subtreeRootTaskInstance(String name) {
Subtree tree = subtrees.get(name);
if (tree == null)
throw new GdxRuntimeException("Undefined subtree with name '" + name + "'");
return tree.rootTaskInstance();
}
//
// Import
//
void addImport (String alias, String task) {
if (task == null) throw new GdxRuntimeException("import: missing task class name.");
if (alias == null) {
Class> clazz = null;
try {
clazz = ClassReflection.forName(task);
} catch (ReflectionException e) {
throw new GdxRuntimeException("import: class not found '" + task + "'");
}
alias = clazz.getSimpleName();
}
String className = getImport(alias);
if (className != null) throw new GdxRuntimeException("import: alias '" + alias + "' previously defined already.");
userImports.put(alias, task);
}
String getImport (String as) {
String className = DEFAULT_IMPORTS.get(as);
return className != null ? className : userImports.get(as);
}
//
// Integrity checks
//
private void popAndCheckMinChildren (int upToFloor) {
// Check the minimum number of children in prevTask
if (prevTask != null) checkMinChildren(prevTask);
// Check the minimum number of children while popping up to the specified floor
while (stack.size > upToFloor) {
StackedTask stackedTask = stack.pop();
checkMinChildren(stackedTask);
}
}
private void checkMinChildren (StackedTask stackedTask) {
// Check the minimum number of children
int minChildren = stackedTask.metadata.minChildren;
if (stackedTask.task.getChildCount() < minChildren)
throw stackedTaskException(stackedTask, "not enough children (" + stackedTask.task.getChildCount() + " < " + minChildren
+ ")");
}
private void checkRequiredAttributes (StackedTask stackedTask) {
// Check the minimum number of children
Entries entries = stackedTask.metadata.attributes.iterator();
while (entries.hasNext()) {
Entry entry = entries.next();
if (entry.value.required && !encounteredAttributes.contains(entry.key))
throw stackedTaskException(stackedTask, "missing required attribute '" + entry.key + "'");
}
}
private GdxRuntimeException stackedTaskException(StackedTask stackedTask, String message) {
return new GdxRuntimeException(stackedTask.name + " at line " + stackedTask.lineNumber + ": " + message);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy