From 0d01a4c2140541ce7c087ae5345fb37c42ab1c01 Mon Sep 17 00:00:00 2001 From: austin Date: Fri, 30 May 2025 18:05:40 -0400 Subject: [PATCH] macro pathfinding scaffolding --- docs/src/progress/renderertodo.md | 1 + .../server/ai/blackboard/BlackboardKeys.java | 5 + .../checks/spatial/TargetRangeCheckNode.java | 6 +- .../nodes/macro/MacroCharacterGoalNode.java | 35 ++++ .../ai/nodes/plan/MacroPathfindingNode.java | 155 ++++++++++++++++++ .../character/goals/CharacterGoalTree.java | 8 + .../ai/trees/creature/MacroMoveToTree.java | 69 ++++++++ .../macro/character/goal/CharacterGoal.java | 4 + .../simulation/chara/CharaSimulation.java | 9 + 9 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 src/main/java/electrosphere/server/ai/nodes/plan/MacroPathfindingNode.java create mode 100644 src/main/java/electrosphere/server/ai/trees/creature/MacroMoveToTree.java diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index 4498ed93..96608bbc 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -2084,6 +2084,7 @@ Remove potential collision engine footgun Synchronized time-of-day between server and client Skybox reflects time of day Standardize data sourcing in MacroTemporalData +Macro pathfinding scaffolding diff --git a/src/main/java/electrosphere/server/ai/blackboard/BlackboardKeys.java b/src/main/java/electrosphere/server/ai/blackboard/BlackboardKeys.java index 9927cf18..933923ee 100644 --- a/src/main/java/electrosphere/server/ai/blackboard/BlackboardKeys.java +++ b/src/main/java/electrosphere/server/ai/blackboard/BlackboardKeys.java @@ -45,6 +45,11 @@ public class BlackboardKeys { */ public static final String BUILDING_MATERIAL_CURRENT = "buildingMaterialCurrent"; + /** + * The macro object that is being targeted + */ + public static final String MACRO_TARGET = "macroTarget"; + /** * The type of item to try to acquire */ diff --git a/src/main/java/electrosphere/server/ai/nodes/checks/spatial/TargetRangeCheckNode.java b/src/main/java/electrosphere/server/ai/nodes/checks/spatial/TargetRangeCheckNode.java index b88cf643..238aa436 100644 --- a/src/main/java/electrosphere/server/ai/nodes/checks/spatial/TargetRangeCheckNode.java +++ b/src/main/java/electrosphere/server/ai/nodes/checks/spatial/TargetRangeCheckNode.java @@ -6,7 +6,7 @@ import electrosphere.entity.Entity; import electrosphere.entity.EntityUtils; import electrosphere.server.ai.blackboard.Blackboard; import electrosphere.server.ai.nodes.AITreeNode; -import electrosphere.server.macro.structure.VirtualStructure; +import electrosphere.server.macro.spatial.MacroObject; /** * Checks if the target is inside a given range of the entity @@ -44,8 +44,8 @@ public class TargetRangeCheckNode implements AITreeNode { targetPos = (Vector3d)targetRaw; } else if(targetRaw instanceof Entity){ targetPos = EntityUtils.getPosition((Entity)targetRaw); - } else if(targetRaw instanceof VirtualStructure){ - targetPos = ((VirtualStructure)targetRaw).getPos(); + } else if(targetRaw instanceof MacroObject macroObject){ + targetPos = macroObject.getPos(); } else { throw new Error("Unsupported target type " + targetRaw); } diff --git a/src/main/java/electrosphere/server/ai/nodes/macro/MacroCharacterGoalNode.java b/src/main/java/electrosphere/server/ai/nodes/macro/MacroCharacterGoalNode.java index 6278a2f9..9029432e 100644 --- a/src/main/java/electrosphere/server/ai/nodes/macro/MacroCharacterGoalNode.java +++ b/src/main/java/electrosphere/server/ai/nodes/macro/MacroCharacterGoalNode.java @@ -12,6 +12,8 @@ import electrosphere.server.ai.nodes.checks.spatial.BeginStructureNode; import electrosphere.server.macro.character.Character; import electrosphere.server.macro.character.goal.CharacterGoal; import electrosphere.server.macro.character.goal.CharacterGoal.CharacterGoalType; +import electrosphere.server.macro.region.MacroRegion; +import electrosphere.server.macro.spatial.MacroObject; import electrosphere.server.macro.structure.VirtualStructure; /** @@ -84,6 +86,13 @@ public class MacroCharacterGoalNode implements AITreeNode { } blackboard.put(BlackboardKeys.GOAL_ITEM_ACQUISITION_TARGET, targetRaw); } break; + case MOVE_TO_MACRO_STRUCT: { + Object targetRaw = goal.getTarget(); + if(!(targetRaw instanceof VirtualStructure) && !(targetRaw instanceof MacroRegion)){ + throw new Error("Unsupported type! " + targetRaw); + } + MacroCharacterGoalNode.setMacroTarget(blackboard, (MacroObject)goal.getTarget()); + } break; } if(type == goal.getType()){ return AITreeNodeResult.SUCCESS; @@ -91,4 +100,30 @@ public class MacroCharacterGoalNode implements AITreeNode { return AITreeNodeResult.FAILURE; } + /** + * Sets the macro object target for the entity + * @param blackboard The blackboard + * @param object The macro object to target + */ + public static void setMacroTarget(Blackboard blackboard, MacroObject object){ + blackboard.put(BlackboardKeys.MACRO_TARGET, object); + } + + /** + * Checks if the blackboard has a object target + * @param blackboard The blackboard + */ + public static boolean hasMacroTarget(Blackboard blackboard){ + return blackboard.has(BlackboardKeys.MACRO_TARGET); + } + + /** + * Gets the object target in the blackboard + * @param blackboard The blackboard + * @return The object if it exists, null otherwise + */ + public static MacroObject getMacroTarget(Blackboard blackboard){ + return (MacroObject)blackboard.get(BlackboardKeys.MACRO_TARGET); + } + } diff --git a/src/main/java/electrosphere/server/ai/nodes/plan/MacroPathfindingNode.java b/src/main/java/electrosphere/server/ai/nodes/plan/MacroPathfindingNode.java new file mode 100644 index 00000000..8c0fbcb3 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/plan/MacroPathfindingNode.java @@ -0,0 +1,155 @@ +package electrosphere.server.ai.nodes.plan; + +import org.joml.Vector3d; + +import electrosphere.engine.Globals; +import electrosphere.entity.Entity; +import electrosphere.entity.EntityUtils; +import electrosphere.server.ai.AI; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.datacell.Realm; +import electrosphere.server.datacell.interfaces.PathfindingManager; +import electrosphere.server.macro.spatial.path.MacroPathNode; +import electrosphere.server.macro.structure.VirtualStructure; +import electrosphere.server.pathfinding.recast.PathingProgressiveData; + +/** + * A node that uses macro pathfinding structures to accelerate calculating the pathfinding + */ +public class MacroPathfindingNode implements AITreeNode { + + + /** + * The value used to check if the entity is close to a pathing point horizontally + */ + public static final double CLOSENESS_CHECK_BOUND_HORIZONTAL = 0.3f; + + /** + * The value used to check if the entity is close to a pathing point vertically + */ + public static final double CLOSENESS_CHECK_BOUND_VERTICAL = 0.7f; + + /** + * The blackboard key to lookup the target entity under + */ + private String targetEntityKey; + + /** + * + * @param targetEntityKey + */ + public static PathfindingNode createPathEntity(String targetEntityKey){ + PathfindingNode rVal = new PathfindingNode(); + rVal.targetEntityKey = targetEntityKey; + return rVal; + } + + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard){ + //make sure that the solved pathfinding data is for the point we want + if(PathfindingNode.hasPathfindingData(blackboard)){ + PathingProgressiveData pathingProgressiveData = PathfindingNode.getPathfindingData(blackboard); + Vector3d actualPoint = pathingProgressiveData.getGoal(); + Object targetRaw = blackboard.get(this.targetEntityKey); + Vector3d targetPos = null; + if(targetRaw == null){ + throw new Error("Target undefined!"); + } + if(targetRaw instanceof MacroPathNode macroNode){ + targetPos = macroNode.getPosition(); + } else { + throw new Error("Unsupported target type " + targetRaw); + } + if(actualPoint.distance(targetPos) > CLOSENESS_CHECK_BOUND_HORIZONTAL){ + PathfindingNode.clearPathfindingData(blackboard); + PathfindingNode.clearPathfindingPoint(blackboard); + } + } + + //create a path if we don't already have one + if(!PathfindingNode.hasPathfindingData(blackboard)){ + Object targetRaw = blackboard.get(this.targetEntityKey); + Vector3d targetPos = null; + if(targetRaw == null){ + throw new Error("Target undefined!"); + } + if(targetRaw instanceof Vector3d){ + targetPos = (Vector3d)targetRaw; + } else if(targetRaw instanceof Entity){ + targetPos = EntityUtils.getPosition((Entity)targetRaw); + } else if(targetRaw instanceof VirtualStructure){ + targetPos = ((VirtualStructure)targetRaw).getPos(); + } else { + throw new Error("Unsupported target type " + targetRaw); + } + + Realm realm = Globals.serverState.realmManager.getEntityRealm(entity); + PathfindingManager pathfindingManager = realm.getPathfindingManager(); + + Vector3d entityPos = EntityUtils.getPosition(entity); + + PathingProgressiveData pathingProgressiveData = pathfindingManager.findPathAsync(entityPos, targetPos); + PathfindingNode.setPathfindingData(blackboard, pathingProgressiveData); + } + + if(!PathfindingNode.hasPathfindingData(blackboard)){ + throw new Error("Failed to find path! Unhandled"); + } + + //check if the path has been found + PathingProgressiveData pathingProgressiveData = PathfindingNode.getPathfindingData(blackboard); + if(!pathingProgressiveData.isReady()){ + AI.getAI(entity).setStatus("Thinking about pathing"); + return AITreeNodeResult.RUNNING; + } + + + Vector3d entityPos = EntityUtils.getPosition(entity); + + Vector3d currentPathPos = null; + if(pathingProgressiveData.getCurrentPoint() < pathingProgressiveData.getPoints().size()){ + currentPathPos = pathingProgressiveData.getPoints().get(pathingProgressiveData.getCurrentPoint()); + } + double vertDist = Math.abs(currentPathPos.y - entityPos.y); + double horizontalDist = Math.sqrt((currentPathPos.x - entityPos.x) * (currentPathPos.x - entityPos.x) + (currentPathPos.z - entityPos.z) * (currentPathPos.z - entityPos.z)); + while( + currentPathPos != null && + vertDist < CLOSENESS_CHECK_BOUND_VERTICAL && + horizontalDist < CLOSENESS_CHECK_BOUND_HORIZONTAL && + pathingProgressiveData.getCurrentPoint() < pathingProgressiveData.getPoints().size() - 1 + ){ + pathingProgressiveData.setCurrentPoint(pathingProgressiveData.getCurrentPoint() + 1); + currentPathPos = pathingProgressiveData.getPoints().get(pathingProgressiveData.getCurrentPoint()); + } + + //if we're close enough to the final pathing point, always path to actual final point + if( + vertDist < CLOSENESS_CHECK_BOUND_VERTICAL && + horizontalDist < CLOSENESS_CHECK_BOUND_HORIZONTAL && + pathingProgressiveData.getCurrentPoint() == pathingProgressiveData.getPoints().size() - 1 + ){ + Object targetRaw = blackboard.get(this.targetEntityKey); + Vector3d targetPos = null; + if(targetRaw == null){ + throw new Error("Target undefined!"); + } + if(targetRaw instanceof Vector3d){ + targetPos = (Vector3d)targetRaw; + } else if(targetRaw instanceof Entity){ + targetPos = EntityUtils.getPosition((Entity)targetRaw); + } else if(targetRaw instanceof VirtualStructure){ + targetPos = ((VirtualStructure)targetRaw).getPos(); + } else { + throw new Error("Unsupported target type " + targetRaw); + } + currentPathPos = targetPos; + } + + PathfindingNode.setPathfindingPoint(blackboard, currentPathPos); + + return AITreeNodeResult.SUCCESS; + } + +} diff --git a/src/main/java/electrosphere/server/ai/trees/character/goals/CharacterGoalTree.java b/src/main/java/electrosphere/server/ai/trees/character/goals/CharacterGoalTree.java index 1458b1d0..3a95ff37 100644 --- a/src/main/java/electrosphere/server/ai/trees/character/goals/CharacterGoalTree.java +++ b/src/main/java/electrosphere/server/ai/trees/character/goals/CharacterGoalTree.java @@ -11,6 +11,7 @@ import electrosphere.server.ai.nodes.meta.collections.SelectorNode; import electrosphere.server.ai.nodes.meta.collections.SequenceNode; import electrosphere.server.ai.nodes.meta.debug.PublishStatusNode; import electrosphere.server.ai.nodes.meta.decorators.RunnerNode; +import electrosphere.server.ai.trees.creature.MacroMoveToTree; import electrosphere.server.ai.trees.creature.resource.AcquireItemTree; import electrosphere.server.ai.trees.struct.BuildStructureTree; import electrosphere.server.macro.character.goal.CharacterGoal.CharacterGoalType; @@ -53,6 +54,13 @@ public class CharacterGoalTree { new PublishStatusNode("Acquire building material"), //try to find building materials AcquireItemTree.create(BlackboardKeys.GOAL_ITEM_ACQUISITION_TARGET) + ), + + //character's goal is to move to a macro virtual structure + new SequenceNode( + MacroCharacterGoalNode.create(CharacterGoalType.MOVE_TO_MACRO_STRUCT), + new PublishStatusNode("Move to macro structure"), + MacroMoveToTree.create(BlackboardKeys.MACRO_TARGET) ) ) ); diff --git a/src/main/java/electrosphere/server/ai/trees/creature/MacroMoveToTree.java b/src/main/java/electrosphere/server/ai/trees/creature/MacroMoveToTree.java new file mode 100644 index 00000000..b2d84985 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/trees/creature/MacroMoveToTree.java @@ -0,0 +1,69 @@ +package electrosphere.server.ai.trees.creature; + +import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree.MovementRelativeFacing; +import electrosphere.server.ai.blackboard.BlackboardKeys; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.ai.nodes.actions.move.FaceTargetNode; +import electrosphere.server.ai.nodes.actions.move.MoveStartNode; +import electrosphere.server.ai.nodes.actions.move.MoveStopNode; +import electrosphere.server.ai.nodes.checks.spatial.TargetRangeCheckNode; +import electrosphere.server.ai.nodes.meta.DataDeleteNode; +import electrosphere.server.ai.nodes.meta.collections.SelectorNode; +import electrosphere.server.ai.nodes.meta.collections.SequenceNode; +import electrosphere.server.ai.nodes.meta.decorators.RunnerNode; +import electrosphere.server.ai.nodes.meta.decorators.SucceederNode; +import electrosphere.server.ai.nodes.plan.PathfindingNode; + +/** + * Tree that moves an entity to a macro structure + */ +public class MacroMoveToTree { + + /** + * Name of the tree + */ + public static final String TREE_NAME = "MacroMoveTo"; + + /** + * Default distance to be within + */ + static final double DEFAULT_DIST = 0.5f; + + /** + * Creates a move-to-target tree + * @param targetKey The key to lookup the target under + * @return The root node of the move-to-target tree + */ + public static AITreeNode create(String targetKey){ + return MacroMoveToTree.create(DEFAULT_DIST, targetKey); + } + + /** + * Creates a move-to-target tree + * @param dist The target distance to be within + * @param targetKey The key to lookup the target under + * @return The root node of the move-to-target tree + */ + public static AITreeNode create(double dist, String targetKey){ + if(dist < PathfindingNode.CLOSENESS_CHECK_BOUND_HORIZONTAL){ + throw new Error("Dist less than minimal amount! " + dist); + } + return new SelectorNode( + new SequenceNode( + //check if in range of target + new TargetRangeCheckNode(dist, targetKey), + new DataDeleteNode(BlackboardKeys.PATHFINDING_POINT), + new DataDeleteNode(BlackboardKeys.PATHFINDING_DATA), + //if in range, stop moving fowards and return SUCCESS + new SucceederNode(new MoveStopNode()) + ), + + //not in range of target, keep moving towards it + new SequenceNode( + PathfindingNode.createPathEntity(targetKey), + new FaceTargetNode(BlackboardKeys.PATHFINDING_POINT), + new RunnerNode(new MoveStartNode(MovementRelativeFacing.FORWARD)) + ) + ); + } +} diff --git a/src/main/java/electrosphere/server/macro/character/goal/CharacterGoal.java b/src/main/java/electrosphere/server/macro/character/goal/CharacterGoal.java index 4fd32a09..4a2b1f0c 100644 --- a/src/main/java/electrosphere/server/macro/character/goal/CharacterGoal.java +++ b/src/main/java/electrosphere/server/macro/character/goal/CharacterGoal.java @@ -26,6 +26,10 @@ public class CharacterGoal extends CharacterData { * Acquire an item */ ACQUIRE_ITEM, + /** + * Move to a macro structure + */ + MOVE_TO_MACRO_STRUCT, } /** diff --git a/src/main/java/electrosphere/server/simulation/chara/CharaSimulation.java b/src/main/java/electrosphere/server/simulation/chara/CharaSimulation.java index a2ef7e72..72d51367 100644 --- a/src/main/java/electrosphere/server/simulation/chara/CharaSimulation.java +++ b/src/main/java/electrosphere/server/simulation/chara/CharaSimulation.java @@ -13,6 +13,7 @@ import electrosphere.server.macro.character.data.CharacterDataStrings; import electrosphere.server.macro.character.goal.CharacterGoal; import electrosphere.server.macro.character.goal.CharacterGoal.CharacterGoalType; import electrosphere.server.macro.civilization.town.Town; +import electrosphere.server.macro.region.MacroRegion; import electrosphere.server.macro.structure.VirtualStructure; import electrosphere.server.macro.utils.StructurePlacementUtils; import electrosphere.server.macro.utils.StructureRepairUtils; @@ -40,6 +41,14 @@ public class CharaSimulation { if(CharaSimulation.checkTownGoals(realm, chara)){ return; } + //send a character on a walk + // if(chara.getId() == 3){ + // Town hometown = CharacterUtils.getHometown(realm.getMacroData(), chara); + // if(hometown != null){ + // MacroRegion target = hometown.getFarmPlots(realm.getMacroData()).get(0); + // CharacterGoal.setCharacterGoal(chara, new CharacterGoal(CharacterGoalType.MOVE_TO_MACRO_STRUCT, target)); + // } + // } } /**