└── addons └── fluid_htn ├── fluid_htn.gd ├── tasks ├── compound_tasks │ ├── task_root.gd │ ├── i_compound_task.gd │ ├── pause_plan_task.gd │ ├── compound_task.gd │ ├── sequence.gd │ └── selector.gd ├── i_task.gd ├── primitive_tasks │ ├── i_primitive_task.gd │ └── primitive_task.gd └── other_tasks │ └── slot.gd ├── plugin.cfg ├── contexts ├── partial_plan_entry.gd ├── base_context.gd └── i_context.gd ├── domain_builder.gd ├── conditions ├── i_condition.gd └── func_condition.gd ├── effects ├── i_effect.gd └── action_effect.gd ├── i_domain.gd ├── operators ├── i_operator.gd └── func_operator.gd ├── planners ├── plan.gd ├── default_planner_state.gd ├── i_planner_state.gd └── planner.gd ├── htn.gd ├── debug ├── error.gd └── decomposition_log_entry.gd ├── domain.gd └── base_domain_builder.gd /addons/fluid_htn/fluid_htn.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | func _enter_tree(): 5 | pass 6 | 7 | func _exit_tree(): 8 | pass 9 | -------------------------------------------------------------------------------- /addons/fluid_htn/tasks/compound_tasks/task_root.gd: -------------------------------------------------------------------------------- 1 | class_name HtnTaskRoot 2 | extends HtnSelector 3 | 4 | func _init(name: String) -> void: 5 | super._init(name) 6 | -------------------------------------------------------------------------------- /addons/fluid_htn/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="fluid_htn" 4 | description="https://github.com/fnaith/godot-fluid-hierarchical-task-network" 5 | author="fnaith" 6 | version="0.4" 7 | script="fluid_htn.gd" 8 | -------------------------------------------------------------------------------- /addons/fluid_htn/contexts/partial_plan_entry.gd: -------------------------------------------------------------------------------- 1 | class_name HtnPartialPlanEntry 2 | extends RefCounted 3 | 4 | var task: HtnICompoundTask 5 | var task_index: int 6 | 7 | func _init(t: HtnICompoundTask, i: int) -> void: 8 | task = t 9 | task_index = i 10 | -------------------------------------------------------------------------------- /addons/fluid_htn/domain_builder.gd: -------------------------------------------------------------------------------- 1 | class_name HtnDomainBuilder 2 | extends HtnBaseDomainBuilder 3 | 4 | #region CONSTRUCTION 5 | 6 | func _init(context_script: Script, domain_name: String) -> void: 7 | super._init(context_script, domain_name) 8 | 9 | #endregion 10 | -------------------------------------------------------------------------------- /addons/fluid_htn/conditions/i_condition.gd: -------------------------------------------------------------------------------- 1 | class_name HtnICondition 2 | extends RefCounted 3 | 4 | func get_name() -> String: 5 | assert(false, "Don't use HtnICondition.get_name") 6 | return "" 7 | 8 | func is_valid(_ctx: HtnIContext) -> bool: 9 | assert(false, "Don't use HtnICondition.is_valid") 10 | return false 11 | -------------------------------------------------------------------------------- /addons/fluid_htn/effects/i_effect.gd: -------------------------------------------------------------------------------- 1 | class_name HtnIEffect 2 | extends RefCounted 3 | 4 | func get_name() -> String: 5 | assert(false, "Don't use HtnIEffect.get_name") 6 | return "" 7 | 8 | func get_type() -> Htn.EffectType: 9 | assert(false, "Don't use HtnIEffect.get_type") 10 | return Htn.EffectType.PLAN_AND_EXECUTE 11 | 12 | func apply(_ctx: HtnIContext) -> bool: 13 | assert(false, "Don't use HtnIEffect.apply") 14 | return false 15 | -------------------------------------------------------------------------------- /addons/fluid_htn/i_domain.gd: -------------------------------------------------------------------------------- 1 | class_name HtnIDomain 2 | extends RefCounted 3 | 4 | func get_root() -> HtnTaskRoot: 5 | assert(false, "Don't use HtnIDomain.get_root") 6 | return null 7 | 8 | func add_subtask(_parent: HtnICompoundTask, _subtask: HtnITask) -> void: 9 | assert(false, "Don't use HtnIDomain.add_subtask") 10 | 11 | func add_slot(_parent: HtnICompoundTask, _slot: HtnSlot) -> void: 12 | assert(false, "Don't use HtnIDomain.add_slot") 13 | -------------------------------------------------------------------------------- /addons/fluid_htn/operators/i_operator.gd: -------------------------------------------------------------------------------- 1 | class_name HtnIOperator 2 | extends RefCounted 3 | 4 | func update(_ctx: HtnIContext) -> Htn.TaskStatus: 5 | assert(false, "Don't use HtnIOperator.update") 6 | return Htn.TaskStatus.SUCCESS 7 | 8 | func stop(_ctx: HtnIContext) -> bool: 9 | assert(false, "Don't use HtnIOperator.stop") 10 | return false 11 | 12 | func aborted(_ctx: HtnIContext) -> bool: 13 | assert(false, "Don't use HtnIOperator.aborted") 14 | return false 15 | -------------------------------------------------------------------------------- /addons/fluid_htn/planners/plan.gd: -------------------------------------------------------------------------------- 1 | class_name HtnPlan 2 | extends RefCounted 3 | 4 | var _queue: Array[HtnITask] = [] 5 | var _valid: bool = true 6 | 7 | func invalidate() -> void: 8 | _valid = false 9 | 10 | func is_valid() -> bool: 11 | return _valid 12 | 13 | func copy(other: HtnPlan) -> void: 14 | _queue = other._queue 15 | _valid = true 16 | 17 | func is_empty() -> bool: 18 | return _queue.is_empty() 19 | 20 | func size() -> int: 21 | return _queue.size() 22 | 23 | func clear() -> void: 24 | _queue.clear() 25 | 26 | func enqueue(task: HtnITask) -> void: 27 | _queue.append(task) 28 | 29 | func dequeue() -> HtnITask: 30 | return _queue.pop_front() 31 | 32 | func peek() -> HtnITask: 33 | return _queue.front() 34 | -------------------------------------------------------------------------------- /addons/fluid_htn/htn.gd: -------------------------------------------------------------------------------- 1 | class_name Htn 2 | extends Object 3 | 4 | ## The state our context can be in. This is essentially planning or execution state. 5 | enum ContextState { 6 | PLANNING = 0, 7 | EXECUTING = 1, 8 | } 9 | 10 | enum LogEntryType { 11 | TASK = 0, 12 | CONDITION = 1, 13 | EFFECT = 2, 14 | } 15 | 16 | enum EffectType { 17 | PLAN_AND_EXECUTE = 0, 18 | PLAN_ONLY = 1, 19 | PERMANENT = 2, 20 | } 21 | 22 | enum TaskStatus { 23 | CONTINUE = 0, 24 | SUCCESS = 1, 25 | FAILURE = 2, 26 | } 27 | 28 | enum TaskType { 29 | PRIMITIVE = 0, 30 | COMPOUND = 1, 31 | PAUSE_PLAN = 2, 32 | SLOT = 3, 33 | } 34 | 35 | enum DecompositionStatus { 36 | SUCCEEDED = 0, 37 | PARTIAL = 1, 38 | FAILED = 2, 39 | REJECTED = 3, 40 | } 41 | -------------------------------------------------------------------------------- /addons/fluid_htn/planners/default_planner_state.gd: -------------------------------------------------------------------------------- 1 | class_name HtnDefaultPlannerState 2 | extends HtnIPlannerState 3 | 4 | func _init() -> void: 5 | super._init() 6 | 7 | #region PROPERTIES 8 | 9 | var _current_task: HtnITask 10 | var _plan: HtnPlan = HtnPlan.new() 11 | var _last_status: Htn.TaskStatus 12 | 13 | func get_current_task() -> HtnITask: 14 | return _current_task 15 | func set_current_task(current_task: HtnITask) -> void: 16 | _current_task = current_task 17 | 18 | func get_plan() -> HtnPlan: 19 | return _plan 20 | func set_plan(plan: HtnPlan) -> void: 21 | _plan = plan 22 | 23 | func get_last_status() -> Htn.TaskStatus: 24 | return _last_status 25 | func set_last_status(last_status: Htn.TaskStatus) -> void: 26 | _last_status = last_status 27 | 28 | #endregion 29 | -------------------------------------------------------------------------------- /addons/fluid_htn/debug/error.gd: -------------------------------------------------------------------------------- 1 | class_name HtnError 2 | extends Object 3 | 4 | static var _message: String = "" 5 | 6 | static var _under_testing: bool = false 7 | static var _reset_message_count: int = 0 8 | static var _assert_count: int = 0 9 | 10 | static func set_message(message: String) -> void: 11 | _message = message 12 | if !_under_testing: 13 | assert(false, _message) 14 | 15 | #region for testing 16 | 17 | static func set_under_testing(under_testing: bool) -> void: 18 | _under_testing = under_testing 19 | 20 | static func reset_message() -> void: 21 | _message = "" 22 | _reset_message_count += 1 23 | 24 | static func get_message() -> String: 25 | return _message 26 | 27 | static func get_reset_message_count() -> int: 28 | return _reset_message_count 29 | 30 | static func add_assert(condition: bool) -> void: 31 | assert(condition) 32 | _assert_count += 1 33 | 34 | static func get_assert_count() -> int: 35 | return _assert_count 36 | 37 | #endregion 38 | -------------------------------------------------------------------------------- /addons/fluid_htn/tasks/compound_tasks/i_compound_task.gd: -------------------------------------------------------------------------------- 1 | class_name HtnICompoundTask 2 | extends HtnITask 3 | 4 | #region checking task type 5 | 6 | func get_type() -> Htn.TaskType: 7 | return Htn.TaskType.COMPOUND 8 | 9 | #endregion 10 | 11 | func get_subtasks() -> Array[HtnITask]: 12 | assert(false, "Don't use HtnICompoundTask.get_subtasks") 13 | return [] 14 | func add_subtask(_subtask: HtnITask) -> HtnICompoundTask: 15 | assert(false, "Don't use HtnICompoundTask.add_subtask") 16 | return null 17 | 18 | ## Decompose the task onto the tasks to process queue, mind it's depth first 19 | func decompose(_ctx: HtnIContext, _start_index: int,\ 20 | _result: HtnPlan) -> Htn.DecompositionStatus: 21 | assert(false, "Don't use HtnICompoundTask.decompose") 22 | return Htn.DecompositionStatus.FAILED 23 | 24 | ## The Decompose All interface is a tag to signify that this compound task type intends to 25 | ## decompose all its subtasks. 26 | ## For a task to support Pause Plan tasks, needed for partial planning, it must be 27 | ## a decompose-all compound task type. 28 | func is_decompose_all() -> bool: 29 | return false 30 | -------------------------------------------------------------------------------- /addons/fluid_htn/debug/decomposition_log_entry.gd: -------------------------------------------------------------------------------- 1 | class_name HtnDecompositionLogEntry 2 | extends RefCounted 3 | 4 | var _name: String 5 | var _description: String 6 | var _depth: int 7 | var _entry_type: Htn.LogEntryType 8 | var _entry: Variant 9 | 10 | static func depth_to_string(depth: int, indent: String = "\t") -> String: 11 | return indent.repeat(depth) + "- " 12 | 13 | func _init(name: String, description: String, depth: int, entry_type: Htn.LogEntryType,\ 14 | entry: Variant) -> void: 15 | _name = name 16 | _description = description 17 | _depth = depth 18 | _entry_type = entry_type 19 | _entry = entry 20 | 21 | func get_name() -> String: 22 | return _name 23 | func set_name(name: String) -> void: 24 | _name = name 25 | 26 | func get_description() -> String: 27 | return _description 28 | func set_description(description: String) -> void: 29 | _description = description 30 | 31 | func get_depth() -> int: 32 | return _depth 33 | func set_depth(depth: int) -> void: 34 | _depth = depth 35 | 36 | func get_entry() -> Variant: 37 | return _entry 38 | func set_entry(entry: Variant) -> void: 39 | _entry = entry 40 | -------------------------------------------------------------------------------- /addons/fluid_htn/tasks/i_task.gd: -------------------------------------------------------------------------------- 1 | class_name HtnITask 2 | extends RefCounted 3 | 4 | #region checking task type 5 | 6 | func get_type() -> Htn.TaskType: 7 | assert(false, "Don't use HtnITask.get_type") 8 | return Htn.TaskType.PRIMITIVE 9 | 10 | #endregion 11 | 12 | ## Used for debugging and identification purposes 13 | func get_name() -> String: 14 | assert(false, "Don't use HtnITask.get_name") 15 | return "" 16 | func set_name(_name: String) -> void: 17 | assert(false, "Don't use HtnITask.set_name") 18 | 19 | ## The parent of this task in the hierarchy 20 | func get_parent() -> HtnICompoundTask: 21 | assert(false, "Don't use HtnITask.get_parent") 22 | return null 23 | func set_parent(_parent: HtnICompoundTask) -> void: 24 | assert(false, "Don't use HtnITask.set_parent") 25 | 26 | ## The conditions that must be satisfied for this task to pass as valid. 27 | func get_conditions() -> Array[HtnICondition]: 28 | assert(false, "Don't use HtnITask.get_conditions") 29 | return [] 30 | 31 | ## Add a new condition to the task. 32 | func add_condition(_condition: HtnICondition) -> HtnITask: 33 | assert(false, "Don't use HtnITask.add_condition") 34 | return null 35 | 36 | ## Check the task's preconditions, returns true if all preconditions are valid. 37 | func is_valid(_ctx: HtnIContext) -> bool: 38 | assert(false, "Don't use HtnITask.is_valid") 39 | return false 40 | 41 | func on_is_valid_failed(_ctx: HtnIContext) -> Htn.DecompositionStatus: 42 | assert(false, "Don't use HtnITask.on_is_valid_failed") 43 | return 0 44 | -------------------------------------------------------------------------------- /addons/fluid_htn/conditions/func_condition.gd: -------------------------------------------------------------------------------- 1 | class_name HtnFuncCondition 2 | extends HtnICondition 3 | 4 | #region default callback 5 | 6 | static var _DEFAULT_FUNC: Callable = func (_ctx: HtnIContext) -> bool: 7 | return false 8 | 9 | #endregion 10 | 11 | #region checking context type 12 | 13 | var _context_script: Script 14 | var _expected_context_type: bool = true 15 | 16 | ## check context type, but only in debug builds 17 | 18 | func is_context_script(ctx: HtnIContext) -> bool: 19 | _expected_context_type = false if null == ctx else ctx.is_script(_context_script) 20 | return true 21 | 22 | #endregion 23 | 24 | #region FIELDS 25 | 26 | var _fn = _DEFAULT_FUNC 27 | var _name: String 28 | 29 | #endregion 30 | 31 | #region CONSTRUCTION 32 | 33 | func _init(context_class: Script, name: String, fn = null) -> void: 34 | _context_script = context_class 35 | _name = name 36 | _fn = fn 37 | 38 | #endregion 39 | 40 | #region PROPERTIES 41 | 42 | func get_name() -> String: 43 | return _name 44 | 45 | #endregion 46 | 47 | #region VALIDITY 48 | 49 | func is_valid(ctx: HtnIContext) -> bool: 50 | assert(is_context_script(ctx)) 51 | if _expected_context_type: 52 | var result = (false if null == _fn else _fn.call(ctx)) 53 | if ctx.is_log_decomposition(): 54 | var ok = "True" if result else "False" 55 | ctx.log_condition(_name, "FuncCondition.IsValid:%s" % ok, ctx.get_current_decomposition_depth() + 1, self) 56 | return result 57 | HtnError.set_message("Unexpected context type!") 58 | return false 59 | 60 | #endregion 61 | -------------------------------------------------------------------------------- /addons/fluid_htn/tasks/primitive_tasks/i_primitive_task.gd: -------------------------------------------------------------------------------- 1 | class_name HtnIPrimitiveTask 2 | extends HtnITask 3 | 4 | #region checking task type 5 | 6 | func get_type() -> Htn.TaskType: 7 | return Htn.TaskType.PRIMITIVE 8 | 9 | #endregion 10 | 11 | ## Executing conditions are validated before every call to Operator.Update(...) 12 | func get_executing_conditions() -> Array[HtnICondition]: 13 | assert(false, "Don't use HtnIPrimitiveTask.get_executing_conditions") 14 | return [] 15 | 16 | ## Add a new executing condition to the primitive task. This will be checked before 17 | ## every call to Operator.Update(...) 18 | func add_executing_condition(_condition: HtnICondition) -> HtnITask: 19 | assert(false, "Don't use HtnIPrimitiveTask.add_executing_condition") 20 | return null 21 | 22 | func get_operator() -> HtnIOperator: 23 | assert(false, "Don't use HtnIPrimitiveTask.get_operator") 24 | return null 25 | func set_operator(_operator: HtnIOperator) -> bool: 26 | assert(false, "Don't use HtnIPrimitiveTask.get_operator") 27 | return false 28 | 29 | func get_effects() -> Array[HtnIEffect]: 30 | assert(false, "Don't use HtnIPrimitiveTask.get_effects") 31 | return [] 32 | func add_effect(_effect: HtnIEffect) -> HtnITask: 33 | assert(false, "Don't use HtnIPrimitiveTask.add_effect") 34 | return null 35 | func apply_effects(_ctx: HtnIContext) -> void: 36 | assert(false, "Don't use HtnIPrimitiveTask.add_effect") 37 | 38 | func stop(_ctx: HtnIContext) -> bool: 39 | assert(false, "Don't use HtnIPrimitiveTask.stop") 40 | return false 41 | 42 | func aborted(_ctx: HtnIContext) -> bool: 43 | assert(false, "Don't use HtnIPrimitiveTask.aborted") 44 | return false 45 | -------------------------------------------------------------------------------- /addons/fluid_htn/effects/action_effect.gd: -------------------------------------------------------------------------------- 1 | class_name HtnActionEffect 2 | extends HtnIEffect 3 | 4 | #region default callback 5 | 6 | static var _DEFAULT_ACT: Callable = func (_ctx: HtnIContext, _effect_type: Htn.EffectType) -> void: 7 | return 8 | 9 | #endregion 10 | 11 | #region checking context type 12 | 13 | var _context_script: Script 14 | var _expected_context_type: bool = true 15 | 16 | ## check context type, but only in debug builds 17 | 18 | func is_context_script(ctx: HtnIContext) -> bool: 19 | _expected_context_type = false if null == ctx else ctx.is_script(_context_script) 20 | return true 21 | 22 | #endregion 23 | 24 | #region FIELDS 25 | 26 | var _name: String 27 | var _type: Htn.EffectType 28 | var _action = _DEFAULT_ACT 29 | 30 | #endregion 31 | 32 | #region CONSTRUCTION 33 | 34 | func _init(context_script: Script, name: String, type: Htn.EffectType, action) -> void: 35 | _context_script = context_script 36 | _name = name 37 | _type = type 38 | _action = action 39 | 40 | #endregion 41 | 42 | #region PROPERTIES 43 | 44 | func get_name() -> String: 45 | return _name 46 | 47 | func get_type() -> Htn.EffectType: 48 | return _type 49 | 50 | #endregion 51 | 52 | #region FUNCTIONALITY 53 | 54 | func apply(ctx: HtnIContext) -> bool: 55 | assert(is_context_script(ctx)) 56 | if _expected_context_type: 57 | if ctx.is_log_decomposition(): 58 | ctx.log_effect(_name, "ActionEffect.Apply:%s" % ["PlanAndExecute" if _type == Htn.EffectType.PLAN_AND_EXECUTE else 59 | "PlanOnly" if _type == Htn.EffectType.PLAN_ONLY else 60 | "Permanent" if _type == Htn.EffectType.PERMANENT else 61 | "???"], ctx.get_current_decomposition_depth() + 1, self) 62 | if null != _action: 63 | _action.call(ctx, _type) 64 | return true 65 | return false 66 | HtnError.set_message("Unexpected context type!") 67 | return false 68 | 69 | #endregion 70 | -------------------------------------------------------------------------------- /addons/fluid_htn/tasks/compound_tasks/pause_plan_task.gd: -------------------------------------------------------------------------------- 1 | class_name HtnPausePlanTask 2 | extends HtnITask 3 | 4 | #region PROPERTIES 5 | 6 | var _name: String 7 | var _parent: HtnICompoundTask 8 | var _conditions: Array[HtnICondition] = [] 9 | var _effects: Array[HtnIEffect] = [] 10 | 11 | func get_name() -> String: 12 | return _name 13 | func set_name(name: String) -> void: 14 | _name = name 15 | 16 | func get_parent() -> HtnICompoundTask: 17 | return _parent 18 | func set_parent(parent: HtnICompoundTask) -> void: 19 | _parent = parent 20 | 21 | func get_conditions() -> Array[HtnICondition]: 22 | return _conditions 23 | 24 | func get_effects() -> Array[HtnIEffect]: 25 | return _effects 26 | 27 | #endregion 28 | 29 | func _init(name: String = "") -> void: 30 | _name = name 31 | 32 | #region checking task type 33 | 34 | func get_type() -> Htn.TaskType: 35 | return Htn.TaskType.PAUSE_PLAN 36 | 37 | #endregion 38 | 39 | #region VALIDITY 40 | 41 | func on_is_valid_failed(_ctx: HtnIContext) -> Htn.DecompositionStatus: 42 | return Htn.DecompositionStatus.FAILED 43 | 44 | #endregion 45 | 46 | #region ADDERS 47 | 48 | func add_condition(_condition: HtnICondition) -> HtnITask: 49 | HtnError.set_message("Pause Plan tasks does not support conditions.") 50 | return null 51 | 52 | func add_effects(_effect: HtnIEffect) -> HtnITask: 53 | HtnError.set_message("Pause Plan tasks does not support effects.") 54 | return null 55 | 56 | #endregion 57 | 58 | #region FUNCTIONALITY 59 | 60 | func apply_effects(_ctx: HtnIContext) -> void: 61 | pass 62 | 63 | #endregion 64 | 65 | #region VALIDITY 66 | 67 | func is_valid(ctx: HtnIContext) -> bool: 68 | if ctx.is_log_decomposition(): 69 | _log(ctx, "PausePlanTask.IsValid:Success!") 70 | 71 | return true 72 | 73 | #endregion 74 | 75 | #region LOGGING 76 | 77 | func _log(ctx: HtnIContext, description: String) -> void: 78 | ctx.log_task(_name, description, ctx.get_current_decomposition_depth(), self) 79 | 80 | #endregion 81 | -------------------------------------------------------------------------------- /addons/fluid_htn/operators/func_operator.gd: -------------------------------------------------------------------------------- 1 | class_name HtnFuncOperator 2 | extends HtnIOperator 3 | 4 | #region default callback 5 | 6 | static var _DEFAULT_FUNC: Callable = func (_ctx: HtnIContext) -> Htn.TaskStatus: 7 | return Htn.TaskStatus.SUCCESS 8 | static var _DEFAULT_ACT: Callable = func (_ctx: HtnIContext) -> void: 9 | return 10 | 11 | #endregion 12 | 13 | #region checking context type 14 | 15 | var _context_script: Script 16 | var _expected_context_type: bool = true 17 | 18 | ## check context type, but only in debug builds 19 | 20 | func is_context_script(ctx: HtnIContext) -> bool: 21 | _expected_context_type = false if null == ctx else ctx.is_script(_context_script) 22 | return true 23 | 24 | #endregion 25 | 26 | #region FIELDS 27 | 28 | var _fn = _DEFAULT_FUNC 29 | var _fn_stop = _DEFAULT_ACT 30 | var _fn_aborted = _DEFAULT_ACT 31 | 32 | #endregion 33 | 34 | #region CONSTRUCTION 35 | 36 | func _init(context_script: Script, fn, fn_stop = null, fn_aborted = null) -> void: 37 | _context_script = context_script 38 | _fn = fn 39 | _fn_stop = fn_stop 40 | _fn_aborted = fn_aborted 41 | 42 | #endregion 43 | 44 | #region FUNCTIONALITY 45 | 46 | func update(ctx: HtnIContext) -> Htn.TaskStatus: 47 | assert(is_context_script(ctx)) 48 | if _expected_context_type: 49 | return Htn.TaskStatus.FAILURE if null == _fn else _fn.call(ctx) 50 | HtnError.set_message("Unexpected context type!") 51 | return Htn.TaskStatus.FAILURE 52 | 53 | func stop(ctx: HtnIContext) -> bool: 54 | assert(is_context_script(ctx)) 55 | if _expected_context_type: 56 | if null != _fn_stop: 57 | _fn_stop.call(ctx) 58 | return true 59 | return false 60 | HtnError.set_message("Unexpected context type!") 61 | return false 62 | 63 | func aborted(ctx: HtnIContext) -> bool: 64 | assert(is_context_script(ctx)) 65 | if _expected_context_type: 66 | if null != _fn_aborted: 67 | _fn_aborted.call(ctx) 68 | return true 69 | return false 70 | HtnError.set_message("Unexpected context type!") 71 | return false 72 | 73 | #endregion 74 | -------------------------------------------------------------------------------- /addons/fluid_htn/tasks/other_tasks/slot.gd: -------------------------------------------------------------------------------- 1 | class_name HtnSlot 2 | extends HtnITask 3 | 4 | #region PROPERTIES 5 | 6 | var _slot_id: int 7 | var _name: String 8 | var _parent: HtnICompoundTask 9 | var _conditions: Array[HtnICondition] = [] 10 | var _subtask: HtnICompoundTask = null 11 | 12 | func get_slot_id() -> int: 13 | return _slot_id 14 | func set_slot_id(slot_id: int) -> void: 15 | _slot_id = slot_id 16 | 17 | func get_name() -> String: 18 | return _name 19 | func set_name(name: String) -> void: 20 | _name = name 21 | 22 | func get_parent() -> HtnICompoundTask: 23 | return _parent 24 | func set_parent(parent: HtnICompoundTask) -> void: 25 | _parent = parent 26 | 27 | func get_conditions() -> Array[HtnICondition]: 28 | return _conditions 29 | 30 | func get_subtask() -> HtnICompoundTask: 31 | return _subtask 32 | 33 | #endregion 34 | 35 | func _init(slot_id: int, name: String) -> void: 36 | _slot_id = slot_id 37 | _name = name 38 | 39 | #region checking task type 40 | 41 | func get_type() -> Htn.TaskType: 42 | return Htn.TaskType.SLOT 43 | 44 | #endregion 45 | 46 | #region VALIDITY 47 | 48 | func on_is_valid_failed(_ctx: HtnIContext) -> Htn.DecompositionStatus: 49 | return Htn.DecompositionStatus.FAILED 50 | 51 | #endregion 52 | 53 | #region ADDERS 54 | 55 | func add_condition(_condition: HtnICondition) -> HtnITask: 56 | HtnError.set_message("Slot tasks does not support conditions.") 57 | return null 58 | 59 | #endregion 60 | 61 | #region SET / REMOVE 62 | 63 | func set_subtask(subtask: HtnICompoundTask) -> bool: 64 | if null != _subtask: 65 | return false 66 | _subtask = subtask 67 | return true 68 | 69 | func clear_subtask() -> void: 70 | _subtask = null 71 | 72 | #endregion 73 | 74 | #region DECOMPOSITION 75 | 76 | func decompose(ctx: HtnIContext, start_index: int,\ 77 | result: HtnPlan) -> Htn.DecompositionStatus: 78 | if null != _subtask: 79 | return _subtask.decompose(ctx, start_index, result) 80 | 81 | result.invalidate() 82 | return Htn.DecompositionStatus.FAILED 83 | 84 | #endregion 85 | 86 | #region VALIDITY 87 | 88 | func is_valid(ctx: HtnIContext) -> bool: 89 | var result = (null != _subtask) 90 | 91 | if ctx.is_log_decomposition(): 92 | var ok = "Success" if result else "Failed" 93 | _log(ctx, "Slot.IsValid:%s!" % ok) 94 | 95 | return result 96 | 97 | #endregion 98 | 99 | #region LOGGING 100 | 101 | func _log(ctx: HtnIContext, description: String) -> void: 102 | ctx.log_task(_name, description, ctx.get_current_decomposition_depth(), self) 103 | 104 | #endregion 105 | -------------------------------------------------------------------------------- /addons/fluid_htn/planners/i_planner_state.gd: -------------------------------------------------------------------------------- 1 | class_name HtnIPlannerState 2 | extends RefCounted 3 | 4 | func _init() -> void: 5 | on_new_plan = null 6 | on_replace_plan = null 7 | on_new_task = null 8 | on_new_task_condition_failed = null 9 | on_stop_current_task = null 10 | on_current_task_completed_successfully = null 11 | on_apply_effect = null 12 | on_current_task_failed = null 13 | on_current_task_continues = null 14 | on_current_task_executing_condition_failed = null 15 | 16 | #region PROPERTIES 17 | 18 | func get_current_task() -> HtnITask: 19 | assert(false, "Don't use HtnIPlannerState.get_current_task") 20 | return null 21 | func set_current_task(_current_task: HtnITask) -> void: 22 | assert(false, "Don't use HtnIPlannerState.set_current_task") 23 | 24 | func get_plan() -> HtnPlan: 25 | assert(false, "Don't use HtnIPlannerState.get_plan") 26 | return null 27 | func set_plan(_plan: HtnPlan) -> void: 28 | assert(false, "Don't use HtnIPlannerState.set_plan") 29 | 30 | func get_last_status() -> Htn.TaskStatus: 31 | assert(false, "Don't use HtnIPlannerState.get_last_status") 32 | return Htn.TaskStatus.FAILURE 33 | func set_last_status(_last_status: Htn.TaskStatus) -> void: 34 | assert(false, "Don't use HtnIPlannerState.set_last_status") 35 | 36 | #endregion 37 | 38 | #region CALLBACKS 39 | 40 | ## OnNewPlan(newPlan) is called when we found a new plan, and there is no 41 | ## old plan to replace. 42 | var on_new_plan = func (_new_plan: HtnPlan): 43 | pass 44 | 45 | ## OnReplacePlan(oldPlan, currentTask, newPlan) is called when we're about to replace the 46 | ## current plan with a new plan. 47 | var on_replace_plan = func (_old_plan: HtnPlan, _current_task: HtnITask, _new_plan: HtnPlan): 48 | pass 49 | 50 | ## OnNewTask(task) is called after we popped a new task off the current plan. 51 | var on_new_task = func (_task: HtnITask): 52 | pass 53 | 54 | ## OnNewTaskConditionFailed(task, failedCondition) is called when we failed to 55 | ## validate a condition on a new task. 56 | var on_new_task_condition_failed = func (_task: HtnITask, _failed_condition: HtnICondition): 57 | pass 58 | 59 | ## OnStopCurrentTask(task) is called when the currently running task was stopped 60 | ## forcefully. 61 | var on_stop_current_task = func (_task: HtnIPrimitiveTask): 62 | pass 63 | 64 | ## OnCurrentTaskCompletedSuccessfully(task) is called when the currently running task 65 | ## completes successfully, and before its effects are applied. 66 | var on_current_task_completed_successfully = func (_task: HtnIPrimitiveTask): 67 | pass 68 | 69 | ## OnApplyEffect(effect) is called for each effect of the type PlanAndExecute on a 70 | ## completed task. 71 | var on_apply_effect = func (_effect: HtnIEffect): 72 | pass 73 | 74 | ## OnCurrentTaskFailed(task) is called when the currently running task fails to complete. 75 | var on_current_task_failed = func (_task: HtnIPrimitiveTask): 76 | pass 77 | 78 | ## OnCurrentTaskContinues(task) is called every tick that a currently running task 79 | ## needs to continue. 80 | var on_current_task_continues = func (_task: HtnIPrimitiveTask): 81 | pass 82 | 83 | ## OnCurrentTaskExecutingConditionFailed(task, condition) is called if an Executing Condition 84 | ## fails. The Executing Conditions are checked before every call to task.Operator.Update(...). 85 | var on_current_task_executing_condition_failed = func (_task: HtnIPrimitiveTask, _condition: HtnICondition): 86 | pass 87 | 88 | #endregion 89 | -------------------------------------------------------------------------------- /addons/fluid_htn/tasks/compound_tasks/compound_task.gd: -------------------------------------------------------------------------------- 1 | class_name HtnCompoundTask 2 | extends HtnICompoundTask 3 | 4 | #region PROPERTIES 5 | 6 | var _name: String 7 | var _parent: HtnICompoundTask 8 | var _conditions: Array[HtnICondition] = [] 9 | var _subtasks: Array[HtnITask] = [] 10 | 11 | func get_name() -> String: 12 | return _name 13 | func set_name(name: String) -> void: 14 | _name = name 15 | 16 | func get_parent() -> HtnICompoundTask: 17 | return _parent 18 | func set_parent(parent: HtnICompoundTask) -> void: 19 | _parent = parent 20 | 21 | func get_conditions() -> Array[HtnICondition]: 22 | return _conditions 23 | 24 | func get_subtasks() -> Array[HtnITask]: 25 | return _subtasks 26 | 27 | #endregion 28 | 29 | #region VALIDITY 30 | 31 | func on_is_valid_failed(_ctx: HtnIContext) -> Htn.DecompositionStatus: 32 | return Htn.DecompositionStatus.FAILED 33 | 34 | #endregion 35 | 36 | #region ADDERS 37 | 38 | func add_condition(condition: HtnICondition) -> HtnITask: 39 | _conditions.append(condition) 40 | return self 41 | 42 | func add_subtask(subtask: HtnITask) -> HtnICompoundTask: 43 | _subtasks.append(subtask) 44 | return self 45 | 46 | #endregion 47 | 48 | #region DECOMPOSITION 49 | 50 | func decompose(ctx: HtnIContext, start_index: int,\ 51 | result: HtnPlan) -> Htn.DecompositionStatus: 52 | if ctx.is_log_decomposition(): 53 | ctx.set_current_decomposition_depth(ctx.get_current_decomposition_depth() + 1) 54 | var status = on_decompose(ctx, start_index, result) 55 | if ctx.is_log_decomposition(): 56 | ctx.set_current_decomposition_depth(ctx.get_current_decomposition_depth() - 1) 57 | return status 58 | 59 | func on_decompose(_ctx: HtnIContext, _start_index: int,\ 60 | _result: HtnPlan) -> Htn.DecompositionStatus: 61 | assert(false, "Don't use HtnCompoundTask.on_decompose") 62 | return 0 63 | 64 | func on_decompose_task(_ctx: HtnIContext, _task: HtnITask, _task_index: int,\ 65 | _old_stack_depth: Array[int], _result: HtnPlan) -> Htn.DecompositionStatus: 66 | assert(false, "Don't use HtnCompoundTask.on_decompose_task") 67 | return 0 68 | 69 | func on_decompose_primitive_task(_ctx: HtnIContext, _task: HtnIPrimitiveTask, _task_index: int,\ 70 | _old_stack_depth: Array[int], _result: HtnPlan) -> void: 71 | assert(false, "Don't use HtnCompoundTask.on_decompose_primitive_task") 72 | 73 | func on_decompose_compound_task(_ctx: HtnIContext, _task: HtnICompoundTask, _task_index: int,\ 74 | _old_stack_depth: Array[int], _result: HtnPlan) -> Htn.DecompositionStatus: 75 | assert(false, "Don't use HtnCompoundTask.on_decompose_compound_task") 76 | return 0 77 | 78 | func on_decompose_slot(_ctx: HtnIContext, _task: HtnSlot, _task_index: int,\ 79 | _old_stack_depth: Array[int], _result: HtnPlan) -> Htn.DecompositionStatus: 80 | assert(false, "Don't use HtnCompoundTask.on_decompose_slot") 81 | return 0 82 | 83 | #endregion 84 | 85 | #region VALIDITY 86 | 87 | func is_valid(ctx: HtnIContext) -> bool: 88 | for condition in _conditions: 89 | var result = condition.is_valid(ctx) 90 | if ctx.is_log_decomposition(): 91 | var s1 = "Success" if result else "Failed" 92 | var s2 = "" if result else " not" 93 | _log(ctx, "CompoundTask.IsValid:%s:%s is%s valid!" % [s1, condition.get_name(), s2]) 94 | if !result: 95 | return false 96 | return true 97 | 98 | #endregion 99 | 100 | #region LOGGING 101 | 102 | func _log(ctx: HtnIContext, description: String) -> void: 103 | ctx.log_task(_name, description, ctx.get_current_decomposition_depth(), self) 104 | 105 | #endregion 106 | -------------------------------------------------------------------------------- /addons/fluid_htn/tasks/primitive_tasks/primitive_task.gd: -------------------------------------------------------------------------------- 1 | class_name HtnPrimitiveTask 2 | extends HtnIPrimitiveTask 3 | 4 | #region PROPERTIES 5 | 6 | var _name: String 7 | var _parent: HtnICompoundTask 8 | var _conditions: Array[HtnICondition] = [] 9 | var _executing_conditions: Array[HtnICondition] = [] 10 | var _operator: HtnIOperator 11 | var _effects: Array[HtnIEffect] = [] 12 | 13 | func get_name() -> String: 14 | return _name 15 | func set_name(name: String) -> void: 16 | _name = name 17 | 18 | func get_parent() -> HtnICompoundTask: 19 | return _parent 20 | func set_parent(parent: HtnICompoundTask) -> void: 21 | _parent = parent 22 | 23 | func get_conditions() -> Array[HtnICondition]: 24 | return _conditions 25 | 26 | func get_executing_conditions() -> Array[HtnICondition]: 27 | return _executing_conditions 28 | 29 | func get_operator() -> HtnIOperator: 30 | return _operator 31 | 32 | func get_effects() -> Array[HtnIEffect]: 33 | return _effects 34 | 35 | #endregion 36 | 37 | func _init(name: String) -> void: 38 | _name = name 39 | 40 | #region VALIDITY 41 | 42 | func on_is_valid_failed(_ctx: HtnIContext) -> Htn.DecompositionStatus: 43 | return Htn.DecompositionStatus.FAILED 44 | 45 | #endregion 46 | 47 | #region ADDERS 48 | 49 | func add_condition(condition: HtnICondition) -> HtnITask: 50 | _conditions.append(condition) 51 | return self 52 | 53 | func add_executing_condition(condition: HtnICondition) -> HtnITask: 54 | _executing_conditions.append(condition) 55 | return self 56 | 57 | func add_effect(effect: HtnIEffect) -> HtnITask: 58 | _effects.append(effect) 59 | return self 60 | 61 | #endregion 62 | 63 | #region SETTERS 64 | 65 | func set_operator(operator: HtnIOperator) -> bool: 66 | if null != _operator: 67 | HtnError.set_message("A Primitive Task can only contain a single Operator!") 68 | return false 69 | _operator = operator 70 | return true 71 | 72 | #endregion 73 | 74 | #region FUNCTIONALITY 75 | 76 | func apply_effects(ctx: HtnIContext) -> void: 77 | if Htn.ContextState.PLANNING == ctx.get_context_state(): 78 | if ctx.is_log_decomposition(): 79 | _log(ctx, "PrimitiveTask.ApplyEffects") 80 | if ctx.is_log_decomposition(): 81 | ctx.set_current_decomposition_depth(ctx.get_current_decomposition_depth() + 1) 82 | for effect in _effects: 83 | effect.apply(ctx) 84 | if ctx.is_log_decomposition(): 85 | ctx.set_current_decomposition_depth(ctx.get_current_decomposition_depth() - 1) 86 | 87 | func stop(ctx: HtnIContext) -> bool: 88 | if null != _operator: 89 | _operator.stop(ctx) 90 | return true 91 | return false 92 | 93 | func aborted(ctx: HtnIContext) -> bool: 94 | if null != _operator: 95 | _operator.aborted(ctx) 96 | return true 97 | return false 98 | 99 | #endregion 100 | 101 | #region VALIDITY 102 | 103 | func is_valid(ctx: HtnIContext) -> bool: 104 | if ctx.is_log_decomposition(): 105 | _log(ctx, "PrimitiveTask.IsValid check") 106 | 107 | for condition in _conditions: 108 | if ctx.is_log_decomposition(): 109 | ctx.set_current_decomposition_depth(ctx.get_current_decomposition_depth() + 1) 110 | 111 | var result = condition.is_valid(ctx) 112 | 113 | if ctx.is_log_decomposition(): 114 | ctx.set_current_decomposition_depth(ctx.get_current_decomposition_depth() - 1) 115 | var s1 = "Success" if result else "Failed" 116 | var s2 = "" if result else " not" 117 | _log(ctx, "PrimitiveTask.IsValid:%s:%s is%s valid!" % [s1, condition.get_name(), s2]) 118 | 119 | if !result: 120 | if ctx.is_log_decomposition(): 121 | _log(ctx, "PrimitiveTask.IsValid:Failed:Preconditions not met!") 122 | return false 123 | 124 | if ctx.is_log_decomposition(): 125 | _log(ctx, "PrimitiveTask.IsValid:Success!") 126 | 127 | return true 128 | 129 | #endregion 130 | 131 | #region LOGGING 132 | 133 | func _log(ctx: HtnIContext, description: String) -> void: 134 | ctx.log_task(_name, description, ctx.get_current_decomposition_depth() + 1, self) 135 | 136 | #endregion 137 | -------------------------------------------------------------------------------- /addons/fluid_htn/contexts/base_context.gd: -------------------------------------------------------------------------------- 1 | class_name HtnBaseContext 2 | extends HtnIContext 3 | 4 | var LogEntry = preload("res://addons/fluid_htn/debug/decomposition_log_entry.gd") 5 | 6 | #region PROPERTIES 7 | 8 | var _initialized: bool = false 9 | var _dirty: bool = false 10 | var _context_state: Htn.ContextState = Htn.ContextState.EXECUTING 11 | var _current_decomposition_depth: int = 0 12 | var _planner_state: HtnIPlannerState 13 | var _method_traversal_record: Array[int] = [] 14 | var _last_mtr: Array[int] = [] 15 | var _mtr_debug: Array[String] = [] 16 | var _last_mtr_debug: Array[String] = [] 17 | var _debug_mtr: bool = false 18 | var _decomposition_log: Array[HtnDecompositionLogEntry] = [] 19 | var _log_decomposition: bool = false 20 | var _partial_plan_queue: Array[HtnPartialPlanEntry] = [] 21 | var _paused_partial_plan: bool = false 22 | 23 | var _world_state: PackedByteArray = PackedByteArray() 24 | var _world_state_change_stack: Array[Array] = [] 25 | 26 | func is_initialized() -> bool: 27 | return _initialized 28 | 29 | func is_dirty() -> bool: 30 | return _dirty 31 | func set_dirty(dirty: bool) -> void: 32 | _dirty = dirty 33 | 34 | func get_context_state() -> Htn.ContextState: 35 | return _context_state 36 | func set_context_state(context_state: Htn.ContextState) -> void: 37 | _context_state = context_state 38 | 39 | func get_current_decomposition_depth() -> int: 40 | return _current_decomposition_depth 41 | func set_current_decomposition_depth(i: int) -> void: 42 | _current_decomposition_depth = i 43 | 44 | func get_planner_state() -> HtnIPlannerState: 45 | return _planner_state 46 | 47 | func get_method_traversal_record() -> Array[int]: 48 | return _method_traversal_record 49 | func set_method_traversal_record(a: Array[int]) -> void: 50 | _method_traversal_record = a 51 | 52 | func get_last_mtr() -> Array[int]: 53 | return _last_mtr 54 | 55 | func get_mtr_debug() -> Array[String]: 56 | return _mtr_debug 57 | func set_mtr_debug(a: Array[String]) -> void: 58 | _mtr_debug = a 59 | 60 | func get_last_mtr_debug() -> Array[String]: 61 | return _last_mtr_debug 62 | func set_last_mtr_debug(a: Array[String]) -> void: 63 | _last_mtr_debug = a 64 | 65 | func is_debug_mtr() -> bool: 66 | return _debug_mtr 67 | 68 | func get_decomposition_log() -> Array[HtnDecompositionLogEntry]: 69 | return _decomposition_log 70 | func set_decomposition_log(a: Array[HtnDecompositionLogEntry]) -> void: 71 | _decomposition_log = a 72 | 73 | func is_log_decomposition() -> bool: 74 | return _log_decomposition 75 | 76 | func get_partial_plan_queue() -> Array[HtnPartialPlanEntry]: 77 | return _partial_plan_queue 78 | func set_partial_plan_queue(a: Array[HtnPartialPlanEntry]) -> void: 79 | _partial_plan_queue = a 80 | 81 | func has_paused_partial_plan() -> bool: 82 | return _paused_partial_plan 83 | func set_paused_partial_plan(b: bool) -> void: 84 | _paused_partial_plan = b 85 | 86 | func get_world_state() -> PackedByteArray: 87 | return _world_state 88 | 89 | func get_world_state_change_stack() -> Array[Array]: 90 | return _world_state_change_stack 91 | 92 | #endregion 93 | 94 | #region INITIALIZATION 95 | 96 | func init() -> void: 97 | var length = _world_state.size() 98 | if _world_state_change_stack.size() < length: 99 | _world_state_change_stack.resize(length) 100 | for i in length: 101 | _world_state_change_stack[i] = [] 102 | 103 | _initialized = true 104 | 105 | #endregion 106 | 107 | #region STATE HANDLING 108 | 109 | func has_state(state: int, byte_value: int) -> bool: 110 | return byte_value == get_state(state) 111 | 112 | func get_state(state: int) -> int: 113 | if Htn.ContextState.EXECUTING == _context_state: 114 | return _world_state[state] 115 | 116 | var stack = _world_state_change_stack[state] 117 | if stack.is_empty(): 118 | return _world_state[state] 119 | 120 | return stack.back()[1] 121 | 122 | func set_state(state: int, byte_value: int, set_as_dirty: bool = true,\ 123 | e: Htn.EffectType = Htn.EffectType.PERMANENT) -> void: 124 | assert(0 <= byte_value and byte_value <= 255) 125 | if Htn.ContextState.EXECUTING == _context_state: 126 | # Prevent setting the world state dirty if we're not changing anything. 127 | if byte_value == _world_state[state]: 128 | return 129 | 130 | _world_state[state] = byte_value 131 | if set_as_dirty: 132 | # When a state change during execution, we need to mark the context dirty for replanning! 133 | _dirty = true 134 | else: 135 | _world_state_change_stack[state].append([e, byte_value]) 136 | 137 | #endregion 138 | 139 | #region STATE STACK HANDLING 140 | 141 | func get_world_state_change_depth() -> Array[int]: 142 | var length = _world_state_change_stack.size() 143 | var stack_depth: Array[int] = [] 144 | stack_depth.resize(length) 145 | 146 | for i in length: 147 | var stack = _world_state_change_stack[i] 148 | stack_depth[i] = 0 if null == stack else stack.size() 149 | 150 | return stack_depth 151 | 152 | func trim_for_execution() -> void: 153 | if Htn.ContextState.EXECUTING == _context_state: 154 | HtnError.set_message("Can not trim a context when in execution mode") 155 | return 156 | 157 | for stack in _world_state_change_stack: 158 | while !stack.is_empty() and Htn.EffectType.PERMANENT != stack.back()[0]: 159 | stack.pop_back() 160 | 161 | func trim_to_stack_depth(stack_depth: Array[int]) -> void: 162 | if Htn.ContextState.EXECUTING == _context_state: 163 | HtnError.set_message("Can not trim a context when in execution mode") 164 | return 165 | 166 | var length = stack_depth.size() 167 | for i in length: 168 | var stack = _world_state_change_stack[i] 169 | while stack_depth[i] < stack.size(): 170 | stack.pop_back() 171 | 172 | #endregion 173 | 174 | #region STATE RESET 175 | 176 | func reset() -> void: 177 | _method_traversal_record.clear() 178 | _last_mtr.clear() 179 | 180 | if _debug_mtr: 181 | _mtr_debug.clear() 182 | _last_mtr_debug.clear() 183 | 184 | _initialized = false 185 | 186 | #endregion 187 | 188 | #region DECOMPOSITION LOGGING 189 | 190 | func log_task(name: String, description: String, depth: int, task: HtnITask) -> void: 191 | if !is_log_decomposition(): 192 | return 193 | var entry = LogEntry.new(name, description, depth, Htn.LogEntryType.TASK, task) 194 | _decomposition_log.append(entry) 195 | 196 | func log_condition(name: String, description: String, depth: int, condition: HtnICondition) -> void: 197 | if !is_log_decomposition(): 198 | return 199 | var entry = LogEntry.new(name, description, depth, Htn.LogEntryType.CONDITION, condition) 200 | _decomposition_log.append(entry) 201 | 202 | func log_effect(name: String, description: String, depth: int, effect: HtnIEffect): 203 | if !is_log_decomposition(): 204 | return 205 | var entry = LogEntry.new(name, description, depth, Htn.LogEntryType.EFFECT, effect) 206 | _decomposition_log.append(entry) 207 | 208 | #endregion 209 | -------------------------------------------------------------------------------- /addons/fluid_htn/contexts/i_context.gd: -------------------------------------------------------------------------------- 1 | class_name HtnIContext 2 | extends RefCounted 3 | 4 | #region checking context type 5 | 6 | static var _script_to_inheritance_set: Dictionary = {} 7 | 8 | ## traverse and collect the inheritance tree of given script into a set 9 | static func _build_inheritance_script_set(script: Script) -> Dictionary: 10 | var script_set = {} 11 | while null != script: 12 | script_set[script] = true 13 | script = script.get_base_script() 14 | return script_set 15 | 16 | ## for any new script, build a inheritance set for type checking 17 | static func _register_script(script: Script) -> bool: 18 | if !_script_to_inheritance_set.has(script): 19 | _script_to_inheritance_set[script] = _build_inheritance_script_set(script) 20 | return true 21 | 22 | ## register script, but only in debug builds 23 | func _init() -> void: 24 | assert(_register_script(get_script())) 25 | 26 | func is_script(script: Script) -> bool: 27 | var inheritance_set = _script_to_inheritance_set[get_script()] 28 | return inheritance_set.has(script) 29 | 30 | #endregion 31 | 32 | func is_initialized() -> bool: 33 | assert(false, "Don't use HtnIContext.is_initialized") 34 | return false 35 | 36 | func is_dirty() -> bool: 37 | assert(false, "Don't use HtnIContext.is_dirty") 38 | return false 39 | func set_dirty(_b: bool) -> void: 40 | assert(false, "Don't use HtnIContext.set_dirty") 41 | 42 | func get_context_state() -> Htn.ContextState: 43 | assert(false, "Don't use HtnIContext.get_context_state") 44 | return 0 45 | func set_context_state(_context_state: Htn.ContextState) -> void: 46 | assert(false, "Don't use HtnIContext.set_context_state") 47 | 48 | func get_current_decomposition_depth() -> int: 49 | assert(false, "Don't use HtnIContext.get_current_decomposition_depth") 50 | return 0 51 | func set_current_decomposition_depth(_i: int) -> void: 52 | assert(false, "Don't use HtnIContext.set_current_decomposition_depth") 53 | 54 | func get_planner_state() -> HtnIPlannerState: 55 | assert(false, "Don't use HtnIContext.get_planner_state") 56 | return null 57 | 58 | ## The Method Traversal Record is used while decomposing a domain and 59 | ## records the valid decomposition indices as we go through our 60 | ## decomposition process. 61 | ## It "should" be enough to only record decomposition traversal in Selectors. 62 | ## This can be used to compare LastMTR with the MTR, and reject 63 | ## a new plan early if it is of lower priority than the last plan. 64 | ## It is the user's responsibility to set the instance of the MTR, so that 65 | ## the user is free to use pooled instances, or whatever optimization they 66 | ## see fit. 67 | func get_method_traversal_record() -> Array[int]: 68 | assert(false, "Don't use HtnIContext.get_method_traversal_record") 69 | return [] 70 | func set_method_traversal_record(_a: Array[int]) -> void: 71 | assert(false, "Don't use HtnIContext.set_method_traversal_record") 72 | 73 | func get_mtr_debug() -> Array[String]: 74 | assert(false, "Don't use HtnIContext.get_mtr_debug") 75 | return [] 76 | func set_mtr_debug(_a: Array[String]) -> void: 77 | assert(false, "Don't use HtnIContext.set_mtr_debug") 78 | 79 | ## The Method Traversal Record that was recorded for the currently 80 | ## running plan. 81 | ## If a plan completes successfully, this should be cleared. 82 | ## It is the user's responsibility to set the instance of the MTR, so that 83 | ## the user is free to use pooled instances, or whatever optimization they 84 | ## see fit. 85 | func get_last_mtr() -> Array[int]: 86 | assert(false, "Don't use HtnIContext.get_last_mtr") 87 | return [] 88 | 89 | func get_last_mtr_debug() -> Array[String]: 90 | assert(false, "Don't use HtnIContext.get_last_mtr_debug") 91 | return [] 92 | func set_last_mtr_debug(_a: Array[String]) -> void: 93 | assert(false, "Don't use HtnIContext.set_last_mtr_debug") 94 | 95 | ## Whether the planning system should collect debug information about our Method Traversal Record. 96 | func is_debug_mtr() -> bool: 97 | assert(false, "Don't use HtnIContext.is_debug_mtr") 98 | return false 99 | 100 | func get_decomposition_log() -> Array[HtnDecompositionLogEntry]: # Queue[] 101 | assert(false, "Don't use HtnIContext.get_decomposition_log") 102 | return [] 103 | func set_decomposition_log(_a: Array[HtnDecompositionLogEntry]) -> void: 104 | assert(false, "Don't use HtnIContext.set_decomposition_log") 105 | 106 | ## Whether our planning system should log our decomposition. Specially condition success vs failure. 107 | func is_log_decomposition() -> bool: 108 | assert(false, "Don't use HtnIContext.is_log_decomposition") 109 | return false 110 | 111 | func get_partial_plan_queue() -> Array[HtnPartialPlanEntry]: # Queue[] 112 | assert(false, "Don't use HtnIContext.get_partial_plan_queue") 113 | return [] 114 | func set_partial_plan_queue(_a: Array[HtnPartialPlanEntry]) -> void: 115 | assert(false, "Don't use HtnIContext.set_partial_plan_queue") 116 | 117 | func has_paused_partial_plan() -> bool: 118 | assert(false, "Don't use HtnIContext.has_paused_partial_plan") 119 | return false 120 | func set_paused_partial_plan(_b: bool) -> void: 121 | assert(false, "Don't use HtnIContext.set_paused_partial_plan") 122 | 123 | func get_world_state() -> PackedByteArray: 124 | assert(false, "Don't use HtnIContext.get_world_state") 125 | return PackedByteArray() 126 | 127 | ## A stack of changes applied to each world state entry during planning. 128 | ## This is necessary if one wants to support planner-only and plan&execute effects. 129 | func get_world_state_change_stack() -> Array[Array]: # Array[Stack[Pair[EffectType, byte]] 130 | assert(false, "Don't use HtnIContext.get_world_state_change_stack") 131 | return [] 132 | 133 | ## Reset the context state to default values. 134 | func reset() -> void: 135 | assert(false, "Don't use HtnIContext.reset") 136 | 137 | func trim_for_execution() -> void: 138 | assert(false, "Don't use trim_for_execution.trim_for_execution") 139 | func trim_to_stack_depth(_stack_depth: Array[int]) -> void: 140 | assert(false, "Don't use HtnIContext.trim_to_stack_depth") 141 | 142 | func has_state(_state: int, _byte_value: int) -> bool: 143 | assert(false, "Don't use HtnIContext.has_state") 144 | return false 145 | func get_state(_state: int) -> int: 146 | assert(false, "Don't use HtnIContext.get_state") 147 | return 0 148 | func set_state(_state: int, _byte_value: int, _set_as_dirty: bool = true,\ 149 | _e: Htn.EffectType = Htn.EffectType.PERMANENT) -> void: 150 | assert(false, "Don't use HtnIContext.set_state") 151 | 152 | func get_world_state_change_depth() -> Array[int]: 153 | assert(false, "Don't use HtnIContext.get_world_state_change_depth") 154 | return [] 155 | 156 | func log_task(_name: String, _description: String, _depth: int, _task: HtnITask) -> void: 157 | assert(false, "Don't use HtnIContext.log_task") 158 | func log_condition(_name: String, _description: String, _depth: int,\ 159 | _condition: HtnICondition) -> void: 160 | assert(false, "Don't use HtnIContext.log_condition") 161 | func log_effect(_name: String, _description: String, _depth: int, _effect: HtnIEffect): 162 | assert(false, "Don't use HtnIContext.log_effect") 163 | -------------------------------------------------------------------------------- /addons/fluid_htn/tasks/compound_tasks/sequence.gd: -------------------------------------------------------------------------------- 1 | class_name HtnSequence 2 | extends HtnCompoundTask 3 | 4 | #region FIELDS 5 | 6 | var _plan: HtnPlan = HtnPlan.new() 7 | 8 | #endregion 9 | 10 | func _init(name: String = "") -> void: 11 | _name = name 12 | 13 | func is_decompose_all() -> bool: 14 | return true 15 | 16 | #region VALIDITY 17 | 18 | func is_valid(ctx: HtnIContext) -> bool: 19 | # Check that our preconditions are valid first. 20 | if !super.is_valid(ctx): 21 | if ctx.is_log_decomposition(): 22 | _log(ctx, "Sequence.IsValid:Failed:Preconditions not met!") 23 | 24 | return false 25 | 26 | # Selector requires there to be subtasks to successfully select from. 27 | if _subtasks.is_empty(): 28 | if ctx.is_log_decomposition(): 29 | _log(ctx, "Sequence.IsValid:Failed:No sub-tasks!") 30 | 31 | return false 32 | 33 | if ctx.is_log_decomposition(): 34 | _log(ctx, "Sequence.IsValid:Success!") 35 | 36 | return true 37 | 38 | #endregion 39 | 40 | #region DECOMPOSITION 41 | 42 | ## In a Sequence decomposition, all sub-tasks must be valid and successfully decomposed in order for the Sequence to 43 | ## be successfully decomposed. 44 | func on_decompose(ctx: HtnIContext, start_index: int, result: HtnPlan) -> Htn.DecompositionStatus: 45 | _plan.clear() 46 | 47 | var old_stack_depth = ctx.get_world_state_change_depth() 48 | 49 | var length = _subtasks.size() 50 | for task_index in range(start_index, length): 51 | var task = _subtasks[task_index] 52 | 53 | if ctx.is_log_decomposition(): 54 | var name = task.get_name() 55 | _log(ctx, "Sequence.OnDecompose:Task index: %d: %s" % [task_index, name]) 56 | 57 | var status = on_decompose_task(ctx, task, task_index, old_stack_depth, result) 58 | match status: 59 | Htn.DecompositionStatus.REJECTED, Htn.DecompositionStatus.FAILED, Htn.DecompositionStatus.PARTIAL: 60 | return status 61 | 62 | result.copy(_plan) 63 | return Htn.DecompositionStatus.FAILED if result.is_empty() else Htn.DecompositionStatus.SUCCEEDED 64 | 65 | func on_decompose_task(ctx: HtnIContext, task: HtnITask, task_index: int, old_stack_depth: Array[int], result: HtnPlan) -> Htn.DecompositionStatus: 66 | if !task.is_valid(ctx): 67 | if ctx.is_log_decomposition(): 68 | _log(ctx, "Sequence.OnDecomposeTask:Failed:Task %s.IsValid returned false!" % task.get_name()) 69 | 70 | _plan.clear() 71 | ctx.trim_to_stack_depth(old_stack_depth) 72 | result.copy(_plan) 73 | return task.on_is_valid_failed(ctx) 74 | 75 | if Htn.TaskType.COMPOUND == task.get_type(): 76 | var compound_task: HtnICompoundTask = task 77 | return on_decompose_compound_task(ctx, compound_task, task_index, old_stack_depth, result) 78 | 79 | if Htn.TaskType.PRIMITIVE == task.get_type(): 80 | var primitive_task: HtnIPrimitiveTask = task 81 | on_decompose_primitive_task(ctx, primitive_task, task_index, old_stack_depth, result) 82 | 83 | elif Htn.TaskType.PAUSE_PLAN == task.get_type(): 84 | if ctx.is_log_decomposition(): 85 | _log(ctx, "Sequence.OnDecomposeTask:Return partial plan at index %d!" % task_index) 86 | 87 | ctx.set_paused_partial_plan(true) 88 | var entry = HtnPartialPlanEntry.new(self, task_index + 1) 89 | ctx.get_partial_plan_queue().append(entry) 90 | 91 | result.copy(_plan) 92 | return Htn.DecompositionStatus.PARTIAL 93 | 94 | elif Htn.TaskType.SLOT == task.get_type(): 95 | var slot: HtnSlot = task 96 | return on_decompose_slot(ctx, slot, task_index, old_stack_depth, result) 97 | 98 | result.copy(_plan) 99 | var s = Htn.DecompositionStatus.FAILED if result.is_empty() else Htn.DecompositionStatus.SUCCEEDED 100 | 101 | if ctx.is_log_decomposition(): 102 | var s1 = "Succeeded" if s == Htn.DecompositionStatus.SUCCEEDED else "Failed" 103 | _log(ctx, "Sequence.OnDecomposeTask:%s!" % s1) 104 | 105 | return s 106 | 107 | func on_decompose_primitive_task(ctx: HtnIContext, task: HtnIPrimitiveTask, _task_index: int, _old_stack_depth: Array[int], result: HtnPlan) -> void: 108 | # We don't add MTR tracking on sequences for primary sub-tasks, since they will always be included, so they're irrelevant to MTR tracking. 109 | 110 | if ctx.is_log_decomposition(): 111 | _log(ctx, "Sequence.OnDecomposeTask:Pushed %s to plan!" % task.get_name()) 112 | 113 | task.apply_effects(ctx) 114 | _plan.enqueue(task) 115 | result.copy(_plan) 116 | 117 | func on_decompose_compound_task(ctx: HtnIContext, task: HtnICompoundTask, task_index: int, old_stack_depth: Array[int], result: HtnPlan) -> Htn.DecompositionStatus: 118 | var sub_plan: HtnPlan = HtnPlan.new() 119 | var status = task.decompose(ctx, 0, sub_plan) 120 | 121 | # If result is null, that means the entire planning procedure should cancel. 122 | if Htn.DecompositionStatus.REJECTED == status: 123 | if ctx.is_log_decomposition(): 124 | _log(ctx, "Sequence.OnDecomposeCompoundTask:%s: Decomposing %s was rejected." % ["Rejected", task.get_name()]) 125 | 126 | _plan.clear() 127 | ctx.trim_to_stack_depth(old_stack_depth) 128 | 129 | result.invalidate() 130 | return Htn.DecompositionStatus.REJECTED 131 | 132 | # If the decomposition failed 133 | if Htn.DecompositionStatus.FAILED == status: 134 | if ctx.is_log_decomposition(): 135 | _log(ctx, "Sequence.OnDecomposeCompoundTask:%s: Decomposing %s failed." % ["Failed", task.get_name()]) 136 | 137 | _plan.clear() 138 | ctx.trim_to_stack_depth(old_stack_depth) 139 | result.copy(_plan) 140 | return Htn.DecompositionStatus.FAILED 141 | 142 | while !sub_plan.is_empty(): 143 | var p = sub_plan.dequeue() 144 | 145 | if ctx.is_log_decomposition(): 146 | _log(ctx, "Sequence.OnDecomposeCompoundTask:Decomposing %s:Pushed %s to plan!" % [task.get_name(), p.get_name()]) 147 | 148 | _plan.enqueue(p) 149 | 150 | if ctx.has_paused_partial_plan(): 151 | if ctx.is_log_decomposition(): 152 | _log(ctx, "Sequence.OnDecomposeCompoundTask:Return partial plan at index %d!" % task_index) 153 | 154 | if task_index < _subtasks.size() - 1: 155 | var entry = HtnPartialPlanEntry.new(self, task_index + 1) 156 | ctx.get_partial_plan_queue().append(entry) 157 | 158 | result.copy(_plan) 159 | return Htn.DecompositionStatus.PARTIAL 160 | 161 | result.copy(_plan) 162 | 163 | if ctx.is_log_decomposition(): 164 | _log(ctx, "Sequence.OnDecomposeCompoundTask:Succeeded!") 165 | 166 | return Htn.DecompositionStatus.SUCCEEDED 167 | 168 | func on_decompose_slot(ctx: HtnIContext, task: HtnSlot, task_index: int, old_stack_depth: Array[int], result: HtnPlan) -> Htn.DecompositionStatus: 169 | var sub_plan: HtnPlan = HtnPlan.new() 170 | var status = task.decompose(ctx, 0, sub_plan) 171 | 172 | # If result is null, that means the entire planning procedure should cancel. 173 | if Htn.DecompositionStatus.REJECTED == status: 174 | if ctx.is_log_decomposition(): 175 | _log(ctx, "Sequence.OnDecomposeSlot:%s: Decomposing %s was rejected." % ["Rejected", task.get_name()]) 176 | 177 | _plan.clear() 178 | ctx.trim_to_stack_depth(old_stack_depth) 179 | 180 | result.invalidate() 181 | return Htn.DecompositionStatus.REJECTED 182 | 183 | # If the decomposition failed 184 | if Htn.DecompositionStatus.FAILED == status: 185 | if ctx.is_log_decomposition(): 186 | _log(ctx, "Sequence.OnDecomposeSlot:%s: Decomposing %s failed." % ["Failed", task.get_name()]) 187 | 188 | _plan.clear() 189 | ctx.trim_to_stack_depth(old_stack_depth) 190 | result.copy(_plan) 191 | return Htn.DecompositionStatus.FAILED 192 | 193 | while !sub_plan.is_empty(): 194 | var p = sub_plan.dequeue() 195 | 196 | if ctx.is_log_decomposition(): 197 | _log(ctx, "Sequence.OnDecomposeSlot:Decomposing %s:Pushed %s to plan!" % [task.get_name(), p.get_name()]) 198 | 199 | _plan.enqueue(p) 200 | 201 | if ctx.has_paused_partial_plan(): 202 | if ctx.is_log_decomposition(): 203 | _log(ctx, "Sequence.OnDecomposeSlot:Return partial plan at index %d!" % task_index) 204 | 205 | if task_index < _subtasks.size() - 1: 206 | var entry = HtnPartialPlanEntry.new(self, task_index + 1) 207 | ctx.get_partial_plan_queue().append(entry) 208 | 209 | result.copy(_plan) 210 | return Htn.DecompositionStatus.PARTIAL 211 | 212 | result.copy(_plan) 213 | 214 | if ctx.is_log_decomposition(): 215 | _log(ctx, "Sequence.OnDecomposeSlot:Succeeded!") 216 | 217 | return Htn.DecompositionStatus.SUCCEEDED 218 | 219 | #endregion 220 | -------------------------------------------------------------------------------- /addons/fluid_htn/domain.gd: -------------------------------------------------------------------------------- 1 | class_name HtnDomain 2 | extends HtnIDomain 3 | 4 | #region FIELDS 5 | 6 | var _slots: Dictionary = {} 7 | var _root: HtnTaskRoot 8 | 9 | #endregion 10 | 11 | #region CONSTRUCTION 12 | 13 | func _init(name: String) -> void: 14 | _root = HtnTaskRoot.new(name) 15 | 16 | #endregion 17 | 18 | #region PROPERTIES 19 | 20 | func get_root() -> HtnTaskRoot: 21 | return _root 22 | 23 | #endregion 24 | 25 | #region HIERARCHY HANDLING 26 | 27 | func add_subtask(parent: HtnICompoundTask, subtask: HtnITask) -> void: 28 | if parent == subtask: 29 | HtnError.set_message("Parent-task and Sub-task can't be the same instance!") 30 | return 31 | 32 | parent.add_subtask(subtask) 33 | subtask.set_parent(parent) 34 | 35 | func add_slot(parent: HtnICompoundTask, slot: HtnSlot) -> void: 36 | if _slots.has(slot.get_slot_id()): 37 | HtnError.set_message("This slot id already exist in the domain definition!") 38 | return 39 | 40 | parent.add_subtask(slot) 41 | slot.set_parent(parent) 42 | 43 | _slots[slot.get_slot_id()] = slot 44 | 45 | #endregion 46 | 47 | #region PLANNING 48 | 49 | func find_plan(ctx: HtnIContext, plan: HtnPlan) -> Htn.DecompositionStatus: 50 | if null == ctx: 51 | HtnError.set_message("Context was not existed!") 52 | return Htn.DecompositionStatus.FAILED 53 | 54 | if !ctx.is_initialized(): 55 | HtnError.set_message("Context was not initialized!") 56 | return Htn.DecompositionStatus.FAILED 57 | 58 | ctx.set_context_state(Htn.ContextState.PLANNING) 59 | 60 | plan.invalidate() 61 | var status = Htn.DecompositionStatus.REJECTED 62 | 63 | # We first check whether we have a stored start task. This is true 64 | # if we had a partial plan pause somewhere in our plan, and we now 65 | # want to continue where we left off. 66 | # If this is the case, we don't erase the MTR, but continue building it. 67 | # However, if we have a partial plan, but LastMTR is not 0, that means 68 | # that the partial plan is still running, but something triggered a replan. 69 | # When this happens, we have to plan from the domain root (we're not 70 | # continuing the current plan), so that we're open for other plans to replace 71 | # the running partial plan. 72 | if ctx.has_paused_partial_plan() and ctx.get_last_mtr().is_empty(): 73 | status = _on_paused_partial_plan(ctx, plan, status) 74 | else: 75 | status = _on_replan_during_partial_planning(ctx, plan, status) 76 | 77 | # If this MTR equals the last MTR, then we need to double-check whether we ended up 78 | # just finding the exact same plan. During decomposition each compound task can't check 79 | # for equality, only for less than, so this case needs to be treated after the fact. 80 | if _has_found_same_plan(ctx): 81 | plan.invalidate() 82 | status = Htn.DecompositionStatus.REJECTED 83 | 84 | if _has_decomposition_succeeded(status): 85 | # Apply permanent world state changes to the actual world state used during plan execution. 86 | _apply_permanent_world_state_stack_changes(ctx) 87 | else: 88 | # Clear away any changes that might have been applied to the stack 89 | # No changes should be made or tracked further when the plan failed. 90 | _clear_world_state_stack_changes(ctx) 91 | 92 | ctx.set_context_state(Htn.ContextState.EXECUTING) 93 | return status 94 | 95 | ## We first check whether we have a stored start task. This is true 96 | ## if we had a partial plan pause somewhere in our plan, and we now 97 | ## want to continue where we left off. 98 | ## If this is the case, we don't erase the MTR, but continue building it. 99 | ## However, if we have a partial plan, but LastMTR is not 0, that means 100 | ## that the partial plan is still running, but something triggered a replan. 101 | ## When this happens, we have to plan from the domain root (we're not 102 | ## continuing the current plan), so that we're open for other plans to replace 103 | ## the running partial plan. 104 | func _on_replan_during_partial_planning(ctx: HtnIContext, plan: HtnPlan, status: Htn.DecompositionStatus) -> Htn.DecompositionStatus: 105 | var last_partial_plan_queue = _cache_last_partial_plan(ctx) 106 | 107 | _clear_method_traversal_record(ctx) 108 | 109 | # Replan through decomposition of the hierarchy 110 | status = _root.decompose(ctx, 0, plan) 111 | 112 | if _has_decomposition_failed(status): 113 | _restore_last_partial_plan(ctx, last_partial_plan_queue, status) 114 | 115 | return status 116 | 117 | ## If there is a paused partial plan, we cache it to a last partial plan queue. 118 | ## This is useful when we want to perform a replan, but don't know yet if it will 119 | ## win over the current plan. 120 | func _cache_last_partial_plan(ctx: HtnIContext) -> Variant: # Array[HtnPartialPlanEntry] 121 | if !ctx.has_paused_partial_plan(): 122 | return null 123 | 124 | ctx.HasPausedPartialPlan = false 125 | var last_partial_plan_queue: Array[HtnPartialPlanEntry] = [] 126 | 127 | while !ctx.get_partial_plan_queue().is_empty(): 128 | last_partial_plan_queue.append(ctx.get_partial_plan_queue().pop_front()) 129 | 130 | return last_partial_plan_queue 131 | 132 | ## If we failed to find a new plan, we have to restore the old plan, 133 | ## if it was a partial plan. 134 | func _restore_last_partial_plan(ctx: HtnIContext, last_partial_plan_queue: Variant, status: Htn.DecompositionStatus) -> void: 135 | if null == last_partial_plan_queue: 136 | return 137 | 138 | ctx.set_paused_partial_plan(true) 139 | ctx.get_partial_plan_queue().clear() 140 | 141 | while !last_partial_plan_queue.is_empty(): 142 | ctx.get_partial_plan_queue().append(last_partial_plan_queue.pop_front()) 143 | 144 | ## We only erase the MTR if we start from the root task of the domain. 145 | func _clear_method_traversal_record(ctx: HtnIContext) -> void: 146 | ctx.get_method_traversal_record().clear() 147 | 148 | if ctx.is_debug_mtr(): 149 | ctx.get_mtr_debug().clear() 150 | 151 | ## If decomposition status is failed or rejected, the replan failed. 152 | func _has_decomposition_failed(status: Htn.DecompositionStatus) -> bool: 153 | return status == Htn.DecompositionStatus.REJECTED or status == Htn.DecompositionStatus.FAILED 154 | 155 | ## If decomposition status is failed or rejected, the replan failed. 156 | func _has_decomposition_succeeded(status: Htn.DecompositionStatus) -> bool: 157 | return status == Htn.DecompositionStatus.SUCCEEDED or status == Htn.DecompositionStatus.PARTIAL 158 | 159 | ## We first check whether we have a stored start task. This is true 160 | ## if we had a partial plan pause somewhere in our plan, and we now 161 | ## want to continue where we left off. 162 | ## If this is the case, we don't erase the MTR, but continue building it. 163 | func _on_paused_partial_plan(ctx: HtnIContext, plan: HtnPlan, status: Htn.DecompositionStatus) -> Htn.DecompositionStatus: 164 | ctx.set_paused_partial_plan(false) 165 | while !ctx.get_partial_plan_queue().is_empty(): 166 | var kvp = ctx.get_partial_plan_queue().pop_front() 167 | if !plan.is_valid(): 168 | status = kvp.task.decompose(ctx, kvp.task_index, plan) 169 | else: 170 | var sub_plan = HtnPlan.new() 171 | status = kvp.task.decompose(ctx, kvp.task_index, sub_plan) 172 | if _has_decomposition_succeeded(status): 173 | _enqueue_to_existing_plan(plan, sub_plan) 174 | 175 | # While continuing a partial plan, we might encounter 176 | # a new pause. 177 | if ctx.has_paused_partial_plan(): 178 | break 179 | 180 | # If we failed to continue the paused partial plan, 181 | # then we have to start planning from the root. 182 | if _has_decomposition_failed(status): 183 | _clear_method_traversal_record(ctx) 184 | 185 | status = _root.decompose(ctx, 0, plan) 186 | 187 | return status 188 | 189 | ## Enqueues the sub plan's queue onto the existing plan 190 | func _enqueue_to_existing_plan(plan: HtnPlan, sub_plan: HtnPlan) -> void: 191 | while !sub_plan.is_empty(): 192 | plan.enqueue(sub_plan.dequeue()) 193 | 194 | ## If this MTR equals the last MTR, then we need to double-check whether we ended up 195 | ## just finding the exact same plan. During decomposition each compound task can't check 196 | ## for equality, only for less than, so this case needs to be treated after the fact. 197 | func _has_found_same_plan(ctx: HtnIContext) -> bool: 198 | var is_mtrs_equal = ctx.get_method_traversal_record().size() == ctx.get_last_mtr().size() 199 | if is_mtrs_equal: 200 | var length = ctx.get_method_traversal_record().size() 201 | for i in length: 202 | if ctx.get_method_traversal_record()[i] < ctx.get_last_mtr()[i]: 203 | is_mtrs_equal = false 204 | break 205 | 206 | return is_mtrs_equal 207 | 208 | return false 209 | 210 | ## Apply permanent world state changes to the actual world state used during plan execution. 211 | func _apply_permanent_world_state_stack_changes(ctx: HtnIContext) -> void: 212 | # Trim away any plan-only or plan&execute effects from the world state change stack, that only 213 | # permanent effects on the world state remains now that the planning is done. 214 | ctx.trim_for_execution() 215 | 216 | var length = ctx.get_world_state_change_stack().size() 217 | for i in length: 218 | var stack = ctx.get_world_state_change_stack()[i] 219 | if null != stack and !stack.is_empty(): 220 | ctx.get_world_state()[i] = stack.back()[1] 221 | stack.clear() 222 | 223 | ## Clear away any changes that might have been applied to the stack 224 | func _clear_world_state_stack_changes(ctx: HtnIContext) -> void: 225 | var length = ctx.get_world_state_change_stack().size() 226 | for i in length: 227 | var stack = ctx.get_world_state_change_stack()[i] 228 | if null != stack and !stack.is_empty(): 229 | stack.clear() 230 | 231 | #endregion 232 | 233 | #region SLOTS 234 | 235 | ## At runtime, set a sub-domain to the slot with the given id. 236 | ## This can be used with Smart Objects, to extend the behavior 237 | ## of an agent at runtime. 238 | func try_set_slot_domain(slot_id: int, sub_domain: HtnIDomain) -> bool: 239 | var slot = _slots.get(slot_id, null) 240 | if null != slot: 241 | return slot.set_subtask(sub_domain.get_root()) 242 | 243 | return false 244 | 245 | ## At runtime, clear the sub-domain from the slot with the given id. 246 | ## This can be used with Smart Objects, to extend the behavior 247 | ## of an agent at runtime. 248 | func clear_slot(slot_id: int) -> void: 249 | var slot = _slots.get(slot_id, null) 250 | if null != slot: 251 | slot.clear_subtask() 252 | 253 | #endregion 254 | -------------------------------------------------------------------------------- /addons/fluid_htn/tasks/compound_tasks/selector.gd: -------------------------------------------------------------------------------- 1 | class_name HtnSelector 2 | extends HtnCompoundTask 3 | 4 | #region FIELDS 5 | 6 | var _plan: HtnPlan = HtnPlan.new() 7 | 8 | #endregion 9 | 10 | func _init(name: String = "") -> void: 11 | _name = name 12 | 13 | #region VALIDITY 14 | 15 | func is_valid(ctx: HtnIContext) -> bool: 16 | # Check that our preconditions are valid first. 17 | if !super.is_valid(ctx): 18 | if ctx.is_log_decomposition(): 19 | _log(ctx, "Selector.IsValid:Failed:Preconditions not met!") 20 | 21 | return false 22 | 23 | # Selector requires there to be at least one sub-task to successfully select from. 24 | if _subtasks.is_empty(): 25 | if ctx.is_log_decomposition(): 26 | _log(ctx, "Selector.IsValid:Failed:No sub-tasks!") 27 | 28 | return false 29 | 30 | if ctx.is_log_decomposition(): 31 | _log(ctx, "Selector.IsValid:Success!") 32 | 33 | return true 34 | 35 | func beats_last_mtr(ctx: HtnIContext, task_index: int, current_decomposition_index: int) -> bool: 36 | # If the last plan's traversal record for this decomposition layer 37 | # has a smaller index than the current task index we're about to 38 | # decompose, then the new decomposition can't possibly beat the 39 | # running plan, so we cancel finding a new plan. 40 | if ctx.get_last_mtr()[current_decomposition_index] < task_index: 41 | # But, if any of the earlier records beat the record in LastMTR, we're still good, as we're on a higher priority branch. 42 | # This ensures that [0,0,1] can beat [0,1,0] 43 | var length = ctx.get_method_traversal_record().size() 44 | for i in length: 45 | var diff = ctx.get_method_traversal_record()[i] - ctx.get_last_mtr()[i] 46 | 47 | if diff < 0: 48 | return true 49 | 50 | if 0 < diff: 51 | # We should never really be able to get here, but just in case. 52 | return false 53 | 54 | return false 55 | 56 | return true 57 | 58 | #endregion 59 | 60 | #region DECOMPOSITION 61 | 62 | ## In a Selector decomposition, just a single sub-task must be valid and successfully decompose for the Selector to be 63 | ## successfully decomposed. 64 | func on_decompose(ctx: HtnIContext, start_index: int,\ 65 | result: HtnPlan) -> Htn.DecompositionStatus: 66 | _plan.clear() 67 | 68 | for task_index in range(start_index, _subtasks.size()): 69 | var task = _subtasks[task_index] 70 | 71 | if ctx.is_log_decomposition(): 72 | var name = task.get_name() 73 | _log(ctx, "Selector.OnDecompose:Task index: %d: %s" % [task_index, name]) 74 | 75 | # If the last plan is still running, we need to check whether the 76 | # new decomposition can possibly beat it. 77 | if !ctx.get_last_mtr().is_empty(): 78 | if ctx.get_method_traversal_record().size() < ctx.get_last_mtr().size(): 79 | var current_decomposition_index = ctx.get_method_traversal_record().size() 80 | if !beats_last_mtr(ctx, task_index, current_decomposition_index): 81 | ctx.get_method_traversal_record().append(-1) 82 | if ctx.is_debug_mtr(): 83 | ctx.get_mtr_debug().append("REPLAN FAIL %s" % task.get_name()) 84 | 85 | if ctx.is_log_decomposition(): 86 | _log(ctx, "Selector.OnDecompose:Rejected:Index %d is beat by last method traversal record!" % current_decomposition_index) 87 | 88 | result.invalidate() 89 | return Htn.DecompositionStatus.REJECTED 90 | 91 | var status = on_decompose_task(ctx, task, task_index, [], result) 92 | match status: 93 | Htn.DecompositionStatus.REJECTED, Htn.DecompositionStatus.SUCCEEDED, Htn.DecompositionStatus.PARTIAL: 94 | return status 95 | Htn.DecompositionStatus.FAILED, _: 96 | continue 97 | 98 | result.copy(_plan) 99 | return Htn.DecompositionStatus.FAILED if result.is_empty() else Htn.DecompositionStatus.SUCCEEDED 100 | 101 | func on_decompose_task(ctx: HtnIContext, task: HtnITask, task_index: int, _old_stack_depth: Array[int], result: HtnPlan) -> Htn.DecompositionStatus: 102 | if !task.is_valid(ctx): 103 | if ctx.is_log_decomposition(): 104 | _log(ctx, "Selector.OnDecomposeTask:Failed:Task %s.IsValid returned false!" % task.get_name()) 105 | 106 | result.copy(_plan) 107 | return task.on_is_valid_failed(ctx) 108 | 109 | if Htn.TaskType.COMPOUND == task.get_type(): 110 | var compound_task: HtnICompoundTask = task 111 | return on_decompose_compound_task(ctx, compound_task, task_index, [], result) 112 | 113 | if Htn.TaskType.PRIMITIVE == task.get_type(): 114 | var primitive_task: HtnIPrimitiveTask = task 115 | on_decompose_primitive_task(ctx, primitive_task, task_index, [], result) 116 | 117 | if Htn.TaskType.SLOT == task.get_type(): 118 | var slot: HtnSlot = task 119 | return on_decompose_slot(ctx, slot, task_index, [], result) 120 | 121 | result.copy(_plan) 122 | var status = Htn.DecompositionStatus.FAILED if result.is_empty() else Htn.DecompositionStatus.SUCCEEDED 123 | 124 | if ctx.is_log_decomposition(): 125 | _log(ctx, "Selector.OnDecomposeTask:%s!" % ["Succeeded" if status == Htn.DecompositionStatus.SUCCEEDED else 126 | "PARTIAL" if status == Htn.DecompositionStatus.PARTIAL else 127 | "Failed" if status == Htn.DecompositionStatus.FAILED else 128 | "Rejected" if status == Htn.DecompositionStatus.REJECTED else 129 | "???"]) 130 | 131 | return status 132 | 133 | func on_decompose_primitive_task(ctx: HtnIContext, task: HtnIPrimitiveTask, task_index: int, _old_stack_depth: Array[int], result: HtnPlan) -> void: 134 | # We need to record the task index before we decompose the task, 135 | # so that the traversal record is set up in the right order. 136 | ctx.get_method_traversal_record().append(task_index) 137 | if ctx.is_debug_mtr(): 138 | ctx.get_mtr_debug().append(task.get_name()) 139 | 140 | if ctx.is_log_decomposition(): 141 | _log(ctx, "Selector.OnDecomposeTask:Pushed %s to plan!" % task.get_name()) 142 | 143 | task.apply_effects(ctx) 144 | _plan.enqueue(task) 145 | result.copy(_plan) 146 | 147 | func on_decompose_compound_task(ctx: HtnIContext, task: HtnICompoundTask, task_index: int, _old_stack_depth: Array[int], result: HtnPlan) -> Htn.DecompositionStatus: 148 | # We need to record the task index before we decompose the task, 149 | # so that the traversal record is set up in the right order. 150 | ctx.get_method_traversal_record().append(task_index) 151 | 152 | if ctx.is_debug_mtr(): 153 | ctx.get_mtr_debug().append(task.get_name()) 154 | 155 | var sub_plan: HtnPlan = HtnPlan.new() 156 | var status = task.decompose(ctx, 0, sub_plan) 157 | 158 | # If status is rejected, that means the entire planning procedure should cancel. 159 | if Htn.DecompositionStatus.REJECTED == status: 160 | if ctx.is_log_decomposition(): 161 | _log(ctx, "Selector.OnDecomposeCompoundTask:%s: Decomposing %s was rejected." % ["Rejected", task.get_name()]) 162 | 163 | result.invalidate() 164 | return Htn.DecompositionStatus.REJECTED 165 | 166 | # If the decomposition failed 167 | if Htn.DecompositionStatus.FAILED == status: 168 | # Remove the taskIndex if it failed to decompose. 169 | ctx.get_method_traversal_record().pop_back() 170 | if ctx.is_debug_mtr(): 171 | ctx.get_mtr_debug().pop_back() 172 | 173 | if ctx.is_log_decomposition(): 174 | _log(ctx, "Selector.OnDecomposeCompoundTask:%s: Decomposing %s failed." % ["Failed", task.get_name()]) 175 | 176 | result.copy(_plan) 177 | return Htn.DecompositionStatus.FAILED 178 | 179 | while !sub_plan.is_empty(): 180 | var p = sub_plan.dequeue() 181 | if ctx.is_log_decomposition(): 182 | _log(ctx, "Selector.OnDecomposeCompoundTask:Decomposing %s:Pushed %s to plan!" % [task.get_name(), p.get_name()]) 183 | 184 | _plan.enqueue(p) 185 | 186 | if ctx.has_paused_partial_plan(): 187 | if ctx.is_log_decomposition(): 188 | _log(ctx, "Selector.OnDecomposeCompoundTask:Return partial plan at index %d!" % task_index) 189 | 190 | result.copy(_plan) 191 | return Htn.DecompositionStatus.PARTIAL 192 | 193 | result.copy(_plan) 194 | var s = Htn.DecompositionStatus.FAILED if result.is_empty() else Htn.DecompositionStatus.SUCCEEDED 195 | 196 | if ctx.is_log_decomposition(): 197 | var s1 = "Succeeded" if s == Htn.DecompositionStatus.SUCCEEDED else "Failed" 198 | _log(ctx, "Selector.OnDecomposeCompoundTask:%s!" % s1) 199 | 200 | return s 201 | 202 | func on_decompose_slot(ctx: HtnIContext, task: HtnSlot, task_index: int, _old_stack_depth: Array[int], result: HtnPlan) -> Htn.DecompositionStatus: 203 | # We need to record the task index before we decompose the task, 204 | # so that the traversal record is set up in the right order. 205 | ctx.get_method_traversal_record().append(task_index) 206 | 207 | if ctx.is_debug_mtr(): 208 | ctx.get_mtr_debug().append(task.get_name()) 209 | 210 | var sub_plan: HtnPlan = HtnPlan.new() 211 | var status = task.decompose(ctx, 0, sub_plan) 212 | 213 | # If status is rejected, that means the entire planning procedure should cancel. 214 | if Htn.DecompositionStatus.REJECTED == status: 215 | if ctx.is_log_decomposition(): 216 | _log(ctx, "Selector.OnDecomposeSlot:%s: Decomposing %s was rejected." % ["Rejected", task.get_name()]) 217 | 218 | result.invalidate() 219 | return Htn.DecompositionStatus.REJECTED 220 | 221 | # If the decomposition failed 222 | if Htn.DecompositionStatus.FAILED == status: 223 | # Remove the taskIndex if it failed to decompose. 224 | ctx.get_method_traversal_record().pop_back() 225 | if ctx.is_debug_mtr(): 226 | ctx.get_mtr_debug().pop_back() 227 | 228 | if ctx.is_log_decomposition(): 229 | _log(ctx, "Selector.OnDecomposeSlot:%s: Decomposing %s failed." % ["Failed", task.get_name()]) 230 | 231 | result.invalidate() 232 | return Htn.DecompositionStatus.FAILED 233 | 234 | while !sub_plan.is_empty(): 235 | var p = sub_plan.dequeue() 236 | 237 | if ctx.is_log_decomposition(): 238 | _log(ctx, "Selector.OnDecomposeSlot:Decomposing %s:Pushed %s to plan!" % [task.get_name(), p.get_name()]) 239 | 240 | _plan.enqueue(p) 241 | 242 | if ctx.has_paused_partial_plan(): 243 | if ctx.is_log_decomposition(): 244 | _log(ctx, "Selector.OnDecomposeSlot:Return partial plan!") 245 | 246 | result.copy(_plan) 247 | return Htn.DecompositionStatus.PARTIAL 248 | 249 | result.copy(_plan) 250 | var s = Htn.DecompositionStatus.FAILED if result.is_empty() else Htn.DecompositionStatus.SUCCEEDED 251 | 252 | if ctx.is_log_decomposition(): 253 | var s1 = "Succeeded" if result else "Failed" 254 | _log(ctx, "Selector.OnDecomposeSlot:%s!" % s1) 255 | 256 | return s 257 | 258 | #endregion 259 | -------------------------------------------------------------------------------- /addons/fluid_htn/base_domain_builder.gd: -------------------------------------------------------------------------------- 1 | class_name HtnBaseDomainBuilder 2 | extends RefCounted 3 | 4 | #region FIELDS 5 | 6 | var _context_script: Script 7 | var _domain: HtnDomain 8 | var _pointers: Array[HtnITask] = [] 9 | 10 | #endregion 11 | 12 | #region CONSTRUCTION 13 | 14 | func _init(context_script: Script, domain_name: String): 15 | _context_script = context_script 16 | _domain = HtnDomain.new(domain_name) 17 | _pointers.append(_domain.get_root()) 18 | 19 | #endregion 20 | 21 | #region PROPERTIES 22 | 23 | func get_pointer() -> HtnITask: 24 | if _pointers.is_empty(): 25 | return null 26 | 27 | return _pointers.back() 28 | 29 | #endregion 30 | 31 | #region HIERARCHY HANDLING 32 | 33 | ## Compound tasks are where HTN get their “hierarchical” nature. You can think of a compound task as 34 | ## a high level task that has multiple ways of being accomplished. There are primarily two types of 35 | ## compound tasks. Selectors and Sequencers. A Selector must be able to decompose a single sub-task, 36 | ## while a Sequence must be able to decompose all its sub-tasks successfully for itself to have decomposed 37 | ## successfully. There is nothing stopping you from extending this toolset with RandomSelect, UtilitySelect, 38 | ## etc. These tasks are decomposed until we're left with only Primitive Tasks, which represent a final plan. 39 | ## Compound tasks are comprised of a set of subtasks and a set of conditions. 40 | ## http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter12_Exploring_HTN_Planners_through_Example.pdf 41 | func compound_task(compound_task_script: Script, name: String) -> HtnDomainBuilder: 42 | var parent = compound_task_script.new() 43 | return add_subtask(name, parent) 44 | 45 | ## Compound tasks are where HTN get their “hierarchical” nature. You can think of a compound task as 46 | ## a high level task that has multiple ways of being accomplished. There are primarily two types of 47 | ## compound tasks. Selectors and Sequencers. A Selector must be able to decompose a single sub-task, 48 | ## while a Sequence must be able to decompose all its sub-tasks successfully for itself to have decomposed 49 | ## successfully. There is nothing stopping you from extending this toolset with RandomSelect, UtilitySelect, 50 | ## etc. These tasks are decomposed until we're left with only Primitive Tasks, which represent a final plan. 51 | ## Compound tasks are comprised of a set of subtasks and a set of conditions. 52 | ## http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter12_Exploring_HTN_Planners_through_Example.pdf 53 | func add_subtask(name: String, task: HtnICompoundTask) -> HtnDomainBuilder: 54 | if null != task: 55 | var pointer = get_pointer() 56 | if Htn.TaskType.COMPOUND == pointer.get_type(): 57 | var compound_task: HtnICompoundTask = pointer 58 | task.set_name(name) 59 | _domain.add_subtask(compound_task, task) 60 | _pointers.append(task) 61 | else: 62 | HtnError.set_message("Pointer is not a compound task type. Did you forget an End() after a Primitive Task Action was defined?") 63 | return null 64 | else: 65 | HtnError.set_message("task") 66 | return null 67 | 68 | return self 69 | 70 | ## Primitive tasks represent a single step that can be performed by our AI. A set of primitive tasks is 71 | ## the plan that we are ultimately getting out of the HTN. Primitive tasks are comprised of an operator, 72 | ## a set of effects, a set of conditions and a set of executing conditions. 73 | ## http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter12_Exploring_HTN_Planners_through_Example.pdf 74 | func primitive_task(primitive_task_script: Script, name: String) -> HtnDomainBuilder: 75 | var pointer = get_pointer() 76 | if Htn.TaskType.COMPOUND == pointer.get_type(): 77 | var compound_task: HtnICompoundTask = pointer 78 | var parent = primitive_task_script.new(name) 79 | _domain.add_subtask(compound_task, parent) 80 | _pointers.append(parent) 81 | else: 82 | HtnError.set_message("Pointer is not a compound task type. Did you forget an End() after a Primitive Task Action was defined?") 83 | return null 84 | 85 | return self 86 | 87 | ## Partial planning is one of the most powerful features of HTN. In simplest terms, it allows 88 | ## the planner the ability to not fully decompose a complete plan. HTN is able to do this because 89 | ## it uses forward decomposition or forward search to find plans. That is, the planner starts with 90 | ## the current world state and plans forward in time from that. This allows the planner to only 91 | ## plan ahead a few steps. 92 | ## http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter12_Exploring_HTN_Planners_through_Example.pdf 93 | func pause_plan_task() -> HtnDomainBuilder: 94 | var pointer = get_pointer() 95 | if Htn.TaskType.COMPOUND == pointer.get_type() and pointer.is_decompose_all(): 96 | var compound_task: HtnICompoundTask = pointer 97 | var parent = HtnPausePlanTask.new("Pause Plan") 98 | _domain.add_subtask(compound_task, parent) 99 | else: 100 | HtnError.set_message("Pointer is not a decompose-all compound task type, like a Sequence. Maybe you tried to Pause Plan a Selector, or forget an End() after a Primitive Task Action was defined?") 101 | return null 102 | 103 | return self 104 | #endregion 105 | 106 | #region COMPOUND TASKS 107 | 108 | ## A compound task that requires all sub-tasks to be valid. 109 | ## Sub-tasks can be sequences, selectors or actions. 110 | func sequence(name: String) -> HtnDomainBuilder: 111 | return compound_task(HtnSequence, name) 112 | 113 | ## A compound task that requires a single sub-task to be valid. 114 | ## Sub-tasks can be sequences, selectors or actions. 115 | func select(name: String) -> HtnDomainBuilder: 116 | return compound_task(HtnSelector, name) 117 | 118 | #endregion 119 | 120 | #region PRIMITIVE TASKS 121 | 122 | ## A primitive task that can contain conditions, an operator and effects. 123 | func action(name: String) -> HtnDomainBuilder: 124 | return primitive_task(HtnPrimitiveTask, name) 125 | 126 | #endregion 127 | 128 | #region CONDITIONS 129 | 130 | ## A precondition is a boolean statement required for the parent task to validate. 131 | func condition(name: String, condition: Callable) -> HtnDomainBuilder: 132 | var cond = HtnFuncCondition.new(_context_script, name, condition) 133 | get_pointer().add_condition(cond) 134 | 135 | return self 136 | 137 | ## An executing condition is a boolean statement validated before every call to the current 138 | ## primitive task's operator update tick. It's only supported inside primitive tasks / Actions. 139 | ## Note that this condition is never validated during planning, only during execution. 140 | func executing_condition(name: String, condition: Callable) -> HtnDomainBuilder: 141 | var pointer = get_pointer() 142 | if Htn.TaskType.PRIMITIVE == pointer.get_type(): 143 | var task: HtnIPrimitiveTask = pointer 144 | var cond = HtnFuncCondition.new(_context_script, name, condition) 145 | task.add_executing_condition(cond) 146 | else: 147 | HtnError.set_message("Tried to add an Executing Condition, but the Pointer is not a Primitive Task!") 148 | return null 149 | 150 | return self 151 | 152 | #endregion 153 | 154 | #region OPERATORS 155 | 156 | ## The operator of an Action / primitive task. 157 | func do(action, force_stop_action = null) -> HtnDomainBuilder: 158 | var pointer = get_pointer() 159 | if Htn.TaskType.PRIMITIVE == pointer.get_type(): 160 | var task: HtnIPrimitiveTask = pointer 161 | var op = HtnFuncOperator.new(_context_script, action, force_stop_action) 162 | task.set_operator(op) 163 | else: 164 | HtnError.set_message("Tried to add an Operator, but the Pointer is not a Primitive Task!") 165 | return null 166 | 167 | return self 168 | 169 | #endregion 170 | 171 | #region EFFECTS 172 | 173 | ## Effects can be added to an Action / primitive task. 174 | func effect(name: String, effect_type: Htn.EffectType, action: Callable) -> HtnDomainBuilder: 175 | var pointer = get_pointer() 176 | if Htn.TaskType.PRIMITIVE == pointer.get_type(): 177 | var task: HtnIPrimitiveTask = pointer 178 | var effect = HtnActionEffect.new(_context_script, name, effect_type, action) 179 | task.add_effect(effect) 180 | else: 181 | HtnError.set_message("Tried to add an Effect, but the Pointer is not a Primitive Task!") 182 | return null 183 | 184 | return self 185 | 186 | #endregion 187 | 188 | #region OTHER OPERANDS 189 | 190 | ## Every task encapsulation must end with a call to End(), otherwise subsequent calls will be applied wrong. 191 | func end() -> HtnDomainBuilder: 192 | _pointers.pop_back() 193 | return self 194 | 195 | ## We can splice multiple domains together, allowing us to define reusable sub-domains. 196 | func splice(domain: HtnDomain) -> HtnDomainBuilder: 197 | var pointer = get_pointer() 198 | if Htn.TaskType.COMPOUND == pointer.get_type(): 199 | var compound_task: HtnICompoundTask = pointer 200 | _domain.add_subtask(compound_task, domain.get_root()) 201 | else: 202 | HtnError.set_message("Pointer is not a compound task type. Did you forget an End()?") 203 | return null 204 | 205 | return self 206 | 207 | ## The identifier associated with a slot can be used to splice 208 | ## sub-domains onto the domain, and remove them, at runtime. 209 | ## Use TrySetSlotDomain and ClearSlot on the domain instance at 210 | ## runtime to manage this feature. SlotId can typically be implemented 211 | ## as an enum. 212 | func slot(slot_id: int) -> HtnDomainBuilder: 213 | var pointer = get_pointer() 214 | if Htn.TaskType.COMPOUND == pointer.get_type(): 215 | var compound_task: HtnICompoundTask = pointer 216 | var slot = HtnSlot.new(slot_id, "Slot %d" % slot_id) 217 | _domain.add_slot(compound_task, slot) 218 | else: 219 | HtnError.set_message("Pointer is not a compound task type. Did you forget an End()?") 220 | return null 221 | 222 | return self 223 | 224 | ## We can add a Pause Plan when in a sequence in our domain definition, 225 | ## and this will give us partial planning. 226 | ## It means that we can tell our planner to only plan up to a certain point, 227 | ## then stop. If the partial plan completes execution successfully, the next 228 | ## time we try to find a plan, we will continue planning where we left off. 229 | ## Typical use cases is to split after we navigate toward a location, since 230 | ## this is often time consuming, it's hard to predict the world state when 231 | ## we have reached the destination, and thus there's little point wasting 232 | ## milliseconds on planning further into the future at that point. We might 233 | ## still want to plan what to do when reaching the destination, however, and 234 | ## this is where partial plans come into play. 235 | func pause_plan() -> HtnDomainBuilder: 236 | return pause_plan_task() 237 | 238 | ## Build the designed domain and return a domain instance. 239 | func build() -> HtnDomain: 240 | var pointer = get_pointer() 241 | if pointer != _domain.get_root(): 242 | HtnError.set_message("The domain definition lacks one or more End() statements. Pointer is '%s', but expected '%s'." % [pointer.get_name(), _domain.get_root().get_name()]) 243 | return null 244 | 245 | _pointers.clear() 246 | 247 | return _domain 248 | 249 | #endregion 250 | -------------------------------------------------------------------------------- /addons/fluid_htn/planners/planner.gd: -------------------------------------------------------------------------------- 1 | ## A planner is a responsible for handling the management of finding plans in a domain, replan when the state of the 2 | ## running plan 3 | ## demands it, or look for a new potential plan if the world state gets dirty. 4 | class_name HtnPlanner 5 | extends RefCounted 6 | 7 | #region TICK PLAN 8 | 9 | ## Call this with a domain and context instance to have the planner manage plan and task handling for the domain at 10 | ## runtime. 11 | ## If the plan completes or fails, the planner will find a new plan, or if the context is marked dirty, the planner 12 | ## will attempt 13 | ## a replan to see whether we can find a better plan now that the state of the world has changed. 14 | ## This planner can also be used as a blueprint for writing a custom planner. 15 | func tick(domain: HtnIDomain, ctx: HtnIContext, allow_immediate_replan: bool = true) -> void: 16 | if null == ctx: 17 | HtnError.set_message("Context was not existed!") 18 | return 19 | if null == domain: 20 | HtnError.set_message("Domain was not existed!") 21 | return 22 | if !ctx.is_initialized(): 23 | HtnError.set_message("Context was not initialized!") 24 | return 25 | 26 | var decomposition_status = Htn.DecompositionStatus.FAILED 27 | var is_trying_to_replace_plan = false 28 | # Check whether state has changed or the current plan has finished running. 29 | # and if so, try to find a new plan. 30 | if _should_find_new_plan(ctx): 31 | decomposition_status = _try_find_new_plan(domain, ctx, decomposition_status) 32 | is_trying_to_replace_plan = !ctx.get_planner_state().get_plan().is_empty() 33 | 34 | # If the plan has more tasks, we try to select the next one. 35 | if _can_select_next_task_in_plan(ctx): 36 | # Select the next task, but check whether the conditions of the next task failed to validate. 37 | if !_select_next_task_in_plan(domain, ctx): 38 | return 39 | 40 | # If the current task is a primitive task, we try to tick its operator. 41 | var current_task = ctx.get_planner_state().get_current_task() 42 | if null != current_task and Htn.TaskType.PRIMITIVE == current_task.get_type(): 43 | var task: HtnIPrimitiveTask = current_task 44 | if !_try_tick_primitive_task_operator(domain, ctx, task, allow_immediate_replan): 45 | return 46 | 47 | # Check whether the planner failed to find a plan 48 | if _has_failed_to_find_plan(is_trying_to_replace_plan, decomposition_status, ctx): 49 | ctx.get_planner_state().set_last_status(Htn.TaskStatus.FAILURE) 50 | 51 | ## Check whether state has changed or the current plan has finished running. 52 | ## and if so, try to find a new plan. 53 | func _should_find_new_plan(ctx: HtnIContext) -> bool: 54 | return ctx.is_dirty() or (null == ctx.get_planner_state().get_current_task() and ctx.get_planner_state().get_plan().is_empty()) 55 | 56 | func _try_find_new_plan(domain: HtnDomain, ctx: HtnIContext, decomposition_status: Htn.DecompositionStatus) -> Htn.DecompositionStatus: 57 | var last_partial_plan_queue = _prepare_dirty_world_state_for_replan(ctx) 58 | 59 | var new_plan = HtnPlan.new() 60 | decomposition_status = domain.find_plan(ctx, new_plan) 61 | 62 | if _has_found_new_plan(decomposition_status): 63 | _on_found_new_plan(ctx, new_plan) 64 | elif null != last_partial_plan_queue: 65 | _restore_last_partial_plan(ctx, last_partial_plan_queue) 66 | _restore_last_method_traversal_record(ctx) 67 | 68 | return decomposition_status 69 | 70 | ## If we're simply re-evaluating whether to replace the current plan because 71 | ## some world state got dirty, then we do not intend to continue a partial plan 72 | ## right now, but rather see whether the world state changed to a degree where 73 | ## we should pursue a better plan. 74 | func _prepare_dirty_world_state_for_replan(ctx: HtnIContext) -> Variant: # Queue 75 | if !ctx.is_dirty(): 76 | return null 77 | 78 | ctx.set_dirty(false) 79 | 80 | var last_partial_plan = _cache_last_partial_plan(ctx) 81 | if null == last_partial_plan: 82 | return null 83 | 84 | # We also need to ensure that the last mtr is up to date with the on-going MTR of the partial plan, 85 | # so that any new potential plan that is decomposing from the domain root has to beat the currently 86 | # running partial plan. 87 | _copy_mtr_to_last_mtr(ctx) 88 | 89 | return last_partial_plan 90 | 91 | func _cache_last_partial_plan(ctx: HtnIContext) -> Variant: # Queue 92 | if !ctx.has_paused_partial_plan(): 93 | return null 94 | 95 | ctx.set_paused_partial_plan(false) 96 | var last_partial_plan_queue: Array[HtnPartialPlanEntry] = [] 97 | 98 | while !ctx.get_partial_plan_queue().is_empty(): 99 | last_partial_plan_queue.append(ctx.get_partial_plan_queue().pop_front()) 100 | 101 | return last_partial_plan_queue 102 | 103 | func _restore_last_partial_plan(ctx: HtnIContext, last_partial_plan_queue: Array[HtnPartialPlanEntry]) -> void: 104 | ctx.set_paused_partial_plan(true) 105 | ctx.get_partial_plan_queue().clear() 106 | 107 | while !last_partial_plan_queue.is_empty(): 108 | ctx.get_partial_plan_queue().append(last_partial_plan_queue.pop_front()) 109 | 110 | func _has_found_new_plan(decomposition_status: Htn.DecompositionStatus) -> bool: 111 | return decomposition_status == Htn.DecompositionStatus.SUCCEEDED or\ 112 | decomposition_status == Htn.DecompositionStatus.PARTIAL 113 | 114 | func _on_found_new_plan(ctx: HtnIContext, new_plan: HtnPlan) -> void: 115 | var planner_state = ctx.get_planner_state() 116 | var plan = planner_state.get_plan() 117 | var current_task = planner_state.get_current_task() 118 | if null != planner_state.on_replace_plan and (!plan.is_empty() or null != current_task): 119 | planner_state.on_replace_plan.call(plan, current_task, new_plan) 120 | elif null != planner_state.on_new_plan and plan.is_empty(): 121 | planner_state.on_new_plan.call(new_plan) 122 | 123 | plan.clear() 124 | while !new_plan.is_empty(): 125 | plan.enqueue(new_plan.dequeue()) 126 | 127 | # If a task was running from the previous plan, we stop it. 128 | if null != current_task and Htn.TaskType.PRIMITIVE == current_task.get_type(): 129 | var t: HtnIPrimitiveTask = current_task 130 | if null != planner_state.on_stop_current_task: 131 | planner_state.on_stop_current_task.call(t) 132 | t.stop(ctx) 133 | planner_state.set_current_task(null) 134 | 135 | # Copy the MTR into our LastMTR to represent the current plan's decomposition record 136 | # that must be beat to replace the plan. 137 | _copy_mtr_to_last_mtr(ctx) 138 | 139 | ## Copy the MTR into our LastMTR to represent the current plan's decomposition record 140 | ## that must be beat to replace the plan. 141 | func _copy_mtr_to_last_mtr(ctx: HtnIContext) -> void: 142 | if null != ctx.get_method_traversal_record(): 143 | ctx.get_last_mtr().clear() 144 | for record in ctx.get_method_traversal_record(): 145 | ctx.get_last_mtr().append(record) 146 | 147 | if ctx.is_debug_mtr(): 148 | ctx.get_last_mtr_debug().clear() 149 | for record in ctx.get_mtr_debug(): 150 | ctx.get_last_mtr_debug().append(record) 151 | 152 | ## Copy the Last MTR back into our MTR. This is done during rollback when a new plan 153 | ## failed to beat the last plan. 154 | func _restore_last_method_traversal_record(ctx: HtnIContext) -> void: 155 | if !ctx.get_last_mtr().is_empty(): 156 | ctx.get_method_traversal_record().clear() 157 | for record in ctx.get_last_mtr(): 158 | ctx.get_method_traversal_record().append(record) 159 | ctx.get_last_mtr().clear() 160 | 161 | if !ctx.is_debug_mtr(): 162 | return 163 | 164 | ctx.get_mtr_debug().clear() 165 | for record in ctx.get_last_mtr_debug(): 166 | ctx.get_mtr_debug().append(record) 167 | ctx.get_last_mtr_debug().clear() 168 | 169 | ## If current task is null, we need to verify that the plan has more tasks queued. 170 | func _can_select_next_task_in_plan(ctx: HtnIContext) -> bool: 171 | var planner_state = ctx.get_planner_state() 172 | return null == planner_state.get_current_task() and !planner_state.get_plan().is_empty() 173 | 174 | ## Dequeues the next task of the plan and checks its conditions. If a condition fails, we require a replan. 175 | func _select_next_task_in_plan(domain: HtnDomain, ctx: HtnIContext) -> bool: 176 | var planner_state = ctx.get_planner_state() 177 | var plan = planner_state.get_plan() 178 | var current_task = plan.dequeue() 179 | planner_state.set_current_task(current_task) 180 | if null != current_task: 181 | if null != planner_state.on_new_task: 182 | planner_state.on_new_task.call(current_task) 183 | 184 | return _is_conditions_valid(ctx) 185 | 186 | return true 187 | 188 | ## While we have a valid primitive task running, we should tick it each tick of the plan execution. 189 | func _try_tick_primitive_task_operator(domain: HtnDomain, ctx: HtnIContext, task: HtnIPrimitiveTask, allow_immediate_replan: bool) -> bool: 190 | var planner_state = ctx.get_planner_state() 191 | if null != task.get_operator(): 192 | if !_is_executing_conditions_valid(domain, ctx, task, allow_immediate_replan): 193 | return false 194 | 195 | var last_status = task.get_operator().update(ctx) 196 | planner_state.set_last_status(last_status) 197 | 198 | # If the operation finished successfully, we set task to null so that we dequeue the next task in the plan the following tick. 199 | if Htn.TaskStatus.SUCCESS == last_status: 200 | _on_operator_finished_successfully(domain, ctx, task, allow_immediate_replan) 201 | return true 202 | 203 | # If the operation failed to finish, we need to fail the entire plan, so that we will replan the next tick. 204 | if Htn.TaskStatus.FAILURE == last_status: 205 | _fail_entire_plan(domain, ctx, task, allow_immediate_replan) 206 | return true 207 | 208 | # Otherwise the operation isn't done yet and need to continue. 209 | if null != planner_state.on_current_task_continues: 210 | planner_state.on_current_task_continues.call(task) 211 | return true 212 | 213 | # This should not really happen if a domain is set up properly. 214 | task.aborted(ctx) 215 | planner_state.set_current_task(null) 216 | planner_state.set_last_status(Htn.TaskStatus.FAILURE) 217 | return true 218 | 219 | ## Ensure conditions are valid when a new task is selected from the plan 220 | func _is_conditions_valid(ctx: HtnIContext) -> bool: 221 | var planner_state = ctx.get_planner_state() 222 | var current_task = planner_state.get_current_task() 223 | for condition in current_task.get_conditions(): 224 | # If a condition failed, then the plan failed to progress! A replan is required. 225 | if !condition.is_valid(ctx): 226 | if null != planner_state.on_new_task_condition_failed: 227 | planner_state.on_new_task_condition_failed.call(current_task, condition) 228 | _abort_task(ctx, current_task) 229 | 230 | return false 231 | 232 | return true 233 | 234 | ## When a task is aborted (due to failed condition checks), 235 | ## we prepare the context for a replan next tick. 236 | func _abort_task(ctx: HtnIContext, task: HtnIPrimitiveTask) -> void: 237 | if null != task: 238 | task.aborted(ctx) 239 | _clear_plan_for_replan(ctx) 240 | 241 | ## If the operation finished successfully, we set task to null so that we dequeue the next task in the plan the following tick. 242 | func _on_operator_finished_successfully(domain: HtnDomain, ctx: HtnIContext, task: HtnIPrimitiveTask, allow_immediate_replan: bool) -> void: 243 | var planner_state = ctx.get_planner_state() 244 | if null != planner_state.on_current_task_completed_successfully: 245 | planner_state.on_current_task_completed_successfully.call(task) 246 | 247 | # All effects that is a result of running this task should be applied when the task is a success. 248 | for effect in task.get_effects(): 249 | if Htn.EffectType.PLAN_AND_EXECUTE == effect.get_type(): 250 | if null != planner_state.on_apply_effect: 251 | planner_state.on_apply_effect.call(effect) 252 | effect.apply(ctx) 253 | 254 | planner_state.set_current_task(null) 255 | if planner_state.get_plan().is_empty(): 256 | ctx.get_last_mtr().clear() 257 | 258 | if ctx.is_debug_mtr(): 259 | ctx.get_last_mtr_debug().clear() 260 | 261 | ctx.set_dirty(false) 262 | 263 | if allow_immediate_replan: 264 | tick(domain, ctx, false) 265 | 266 | ## Ensure executing conditions are valid during plan execution 267 | func _is_executing_conditions_valid(domain: HtnDomain, ctx: HtnIContext, task: HtnIPrimitiveTask, allow_immediate_replan: bool) -> bool: 268 | var on_current_task_executing_condition_failed = ctx.get_planner_state().on_current_task_executing_condition_failed 269 | for condition in task.get_executing_conditions(): 270 | # If a condition failed, then the plan failed to progress! A replan is required. 271 | if !condition.is_valid(ctx): 272 | if null != on_current_task_executing_condition_failed: 273 | on_current_task_executing_condition_failed.call(task, condition) 274 | 275 | _abort_task(ctx, task) 276 | 277 | if allow_immediate_replan: 278 | tick(domain, ctx, false) 279 | 280 | return false 281 | 282 | return true 283 | 284 | ## If the operation failed to finish, we need to fail the entire plan, so that we will replan the next tick. 285 | func _fail_entire_plan(domain: HtnDomain, ctx: HtnIContext, task: HtnIPrimitiveTask, allow_immediate_replan: bool) -> void: 286 | var planner_state = ctx.get_planner_state() 287 | if null != planner_state.on_current_task_failed: 288 | planner_state.on_current_task_failed.call(task) 289 | 290 | task.aborted(ctx) 291 | _clear_plan_for_replan(ctx) 292 | 293 | if allow_immediate_replan: 294 | tick(domain, ctx, false) 295 | 296 | ## Prepare the planner state and context for a clean replan 297 | func _clear_plan_for_replan(ctx: HtnIContext) -> void: 298 | var planner_state = ctx.get_planner_state() 299 | planner_state.set_current_task(null) 300 | planner_state.get_plan().clear() 301 | 302 | ctx.get_last_mtr().clear() 303 | 304 | if ctx.is_debug_mtr(): 305 | ctx.get_last_mtr_debug().clear() 306 | 307 | ctx.set_paused_partial_plan(false) 308 | ctx.get_partial_plan_queue().clear() 309 | ctx.set_dirty(false) 310 | 311 | ## If current task is null, and plan is empty, and we're not trying to replace the current plan, and decomposition failed or was rejected, then the planner failed to find a plan. 312 | func _has_failed_to_find_plan(is_trying_to_replace_plan: bool, decomposition_status: Htn.DecompositionStatus, ctx: HtnIContext) -> bool: 313 | var planner_state = ctx.get_planner_state() 314 | var current_task = planner_state.get_current_task() 315 | var plan = planner_state.get_plan() 316 | return null == current_task and plan.is_empty() and !is_trying_to_replace_plan and\ 317 | (Htn.DecompositionStatus.FAILED == decomposition_status or Htn.DecompositionStatus.REJECTED == decomposition_status) 318 | 319 | #endregion 320 | 321 | #region RESET 322 | 323 | func reset(ctx: HtnIContext) -> void: 324 | var planner_state = ctx.get_planner_state() 325 | var current_task = planner_state.get_current_task() 326 | planner_state.get_plan().clear() 327 | 328 | if null != current_task and Htn.TaskType.PRIMITIVE == current_task.get_type(): 329 | var task: HtnIPrimitiveTask = current_task 330 | task.stop(ctx) 331 | 332 | _clear_plan_for_replan(ctx) 333 | 334 | #endregion 335 | --------------------------------------------------------------------------------