chartflow 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chartflow/__init__.py +28 -0
- chartflow/_verify_importexport.py +169 -0
- chartflow/edit/__init__.py +119 -0
- chartflow/edit/code_editor.py +1007 -0
- chartflow/edit/code_folder.py +523 -0
- chartflow/edit/completer.py +2652 -0
- chartflow/edit/context_analyzer.py +521 -0
- chartflow/edit/demo.py +469 -0
- chartflow/edit/line_number_area.py +548 -0
- chartflow/edit/minimap.py +581 -0
- chartflow/edit/python_editor.py +509 -0
- chartflow/edit/syntax_highlighter.py +291 -0
- chartflow/edit/test_editor.py +190 -0
- chartflow/flow/__init__.py +31 -0
- chartflow/flow/_demo.py +98 -0
- chartflow/flow/chart.py +1101 -0
- chartflow/flow/editor.py +375 -0
- chartflow/flow/flow.py +6 -0
- chartflow/flow/qchart.py +1250 -0
- chartflow/flow/test/__init__.py +0 -0
- chartflow/flow/test/test_chart.py +537 -0
- chartflow/flow/test_decorator_sync.py +102 -0
- chartflow/fsm/__init__.py +84 -0
- chartflow/fsm/decorators.py +395 -0
- chartflow/fsm/demo.py +381 -0
- chartflow/fsm/demo_pyqt6.py +881 -0
- chartflow/fsm/demo_tree.py +46 -0
- chartflow/fsm/logic.py +447 -0
- chartflow/fsm/meta.py +569 -0
- chartflow/fsm/scheduler.py +427 -0
- chartflow/fsm/stage.py +2079 -0
- chartflow/fsm/stdio.py +66 -0
- chartflow/fsm/test/__init__.py +7 -0
- chartflow/fsm/test/test_decorators.py +210 -0
- chartflow/fsm/test/test_events.py +202 -0
- chartflow/fsm/test/test_integration.py +596 -0
- chartflow/fsm/test/test_logic.py +371 -0
- chartflow/fsm/test/test_meta.py +192 -0
- chartflow/fsm/test/test_runtime_param.py +336 -0
- chartflow/fsm/test/test_scheduler.py +280 -0
- chartflow/fsm/test/test_stage.py +314 -0
- chartflow/fsm/test_behavior_events.py +263 -0
- chartflow/fsm/test_nested_parallel.py +80 -0
- chartflow/fsm/test_stage_spec.py +448 -0
- chartflow/fsm/utils.py +34 -0
- chartflow/node/__init__.py +66 -0
- chartflow/node/_node_base.py +727 -0
- chartflow/node/_node_interaction.py +488 -0
- chartflow/node/_node_interface.py +426 -0
- chartflow/node/_node_markers.py +648 -0
- chartflow/node/_node_ports.py +536 -0
- chartflow/node/_port_interface.py +111 -0
- chartflow/node/arrow_item.py +287 -0
- chartflow/node/color_utils.py +113 -0
- chartflow/node/connection_item.py +939 -0
- chartflow/node/demo.py +258 -0
- chartflow/node/demo_arrow_drag.py +46 -0
- chartflow/node/demo_decorator.py +63 -0
- chartflow/node/demo_ports.py +69 -0
- chartflow/node/enums.py +35 -0
- chartflow/node/example.py +84 -0
- chartflow/node/layout_utils.py +307 -0
- chartflow/node/main_window.py +196 -0
- chartflow/node/menu_bar.py +240 -0
- chartflow/node/node_canvas.py +1730 -0
- chartflow/node/node_item.py +754 -0
- chartflow/node/popmenu.py +472 -0
- chartflow/node/port_item.py +591 -0
- chartflow/node/qnode_editor.py +73 -0
- chartflow/node/selection_rect.py +53 -0
- chartflow/node/style_panel.py +615 -0
- chartflow/node/styles.py +63 -0
- chartflow/node/test_qnode.py +2336 -0
- chartflow/showcase.py +528 -0
- chartflow/theme.py +508 -0
- chartflow-0.1.dist-info/METADATA +23 -0
- chartflow-0.1.dist-info/RECORD +79 -0
- chartflow-0.1.dist-info/WHEEL +5 -0
- chartflow-0.1.dist-info/top_level.txt +1 -0
chartflow/fsm/stage.py
ADDED
|
@@ -0,0 +1,2079 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
"""
|
|
3
|
+
qfsm Stage Module - Base class for finite state machines
|
|
4
|
+
|
|
5
|
+
This module provides the Stage base class with automatic state discovery,
|
|
6
|
+
behavior tree support, and recursive state execution.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from colorama import init as colorama_init, Fore, Style
|
|
10
|
+
from PyQt6.QtWidgets import QApplication
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from typing import Any, ClassVar, Self
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
|
|
17
|
+
from chartflow.fsm.meta import StageMachineMeta
|
|
18
|
+
from chartflow.fsm.scheduler import Scheduler
|
|
19
|
+
from chartflow.fsm.utils import EventSystem, GetES
|
|
20
|
+
from chartflow.fsm import stdio as std
|
|
21
|
+
|
|
22
|
+
import inspect
|
|
23
|
+
import ast
|
|
24
|
+
|
|
25
|
+
# Color support
|
|
26
|
+
try:
|
|
27
|
+
colorama_init(autoreset=True)
|
|
28
|
+
_COLOR_AVAILABLE = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
_COLOR_AVAILABLE = False
|
|
31
|
+
# Dummy color classes for when colorama is not available
|
|
32
|
+
class _DummyColor:
|
|
33
|
+
def __getattr__(self, name: str) -> str:
|
|
34
|
+
return ''
|
|
35
|
+
Fore = _DummyColor()
|
|
36
|
+
Style = _DummyColor()
|
|
37
|
+
|
|
38
|
+
# Module-level cache for runtime paramable check results
|
|
39
|
+
# Key: id(method) -> bool
|
|
40
|
+
_RUNTIME_PARAMABLE_CACHE = {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _getMethodCacheKey(_method):
|
|
44
|
+
"""
|
|
45
|
+
获取方法的缓存键。
|
|
46
|
+
|
|
47
|
+
对于 bound method,使用 (id(obj), func_name) 作为键
|
|
48
|
+
对于普通函数,使用 id(method) 作为键
|
|
49
|
+
"""
|
|
50
|
+
# 尝试获取 __func__ 属性(bound method 有这个属性)
|
|
51
|
+
if hasattr(_method, '__func__'):
|
|
52
|
+
# 这是一个 bound method
|
|
53
|
+
# 获取绑定的对象
|
|
54
|
+
if hasattr(_method, '__self__'):
|
|
55
|
+
return (id(_method.__self__), id(_method.__func__))
|
|
56
|
+
# 普通函数或其他可调用对象
|
|
57
|
+
return id(_method)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _checkMethodAcceptsParam(_method):
|
|
61
|
+
"""
|
|
62
|
+
检查方法是否接受至少一个参数(可以接受inst参数)。
|
|
63
|
+
|
|
64
|
+
使用inspect检查方法的参数,如果方法接受以下情况之一,返回True:
|
|
65
|
+
- 至少一个位置参数(除了self)
|
|
66
|
+
- *args
|
|
67
|
+
- **kwargs
|
|
68
|
+
|
|
69
|
+
结果会被缓存到模块级别的 _RUNTIME_PARAMABLE_CACHE 中。
|
|
70
|
+
同时也会尝试设置到方法的 __runtime_paramable__ 属性(如果可能)。
|
|
71
|
+
|
|
72
|
+
优先级:
|
|
73
|
+
1. 方法上的 __runtime_paramable__ 属性(如果存在)
|
|
74
|
+
2. 模块级缓存
|
|
75
|
+
3. 重新计算
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
_method: 要检查的方法
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
bool: 如果方法可以接受inst参数则返回True
|
|
82
|
+
"""
|
|
83
|
+
# 首先检查是否已经有 __runtime_paramable__ 属性
|
|
84
|
+
# 这允许用户或测试覆盖检查结果
|
|
85
|
+
if hasattr(_method, '__runtime_paramable__'):
|
|
86
|
+
return _method.__runtime_paramable__
|
|
87
|
+
|
|
88
|
+
# 获取缓存键
|
|
89
|
+
_cacheKey = _getMethodCacheKey(_method)
|
|
90
|
+
|
|
91
|
+
# 检查模块级缓存
|
|
92
|
+
if _cacheKey in _RUNTIME_PARAMABLE_CACHE:
|
|
93
|
+
return _RUNTIME_PARAMABLE_CACHE[_cacheKey]
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
_sig = inspect.signature(_method)
|
|
97
|
+
_params = list(_sig.parameters.values())
|
|
98
|
+
|
|
99
|
+
# 判断是否是 bound method(有 __self__ 属性)
|
|
100
|
+
_isBoundMethod = hasattr(_method, '__self__')
|
|
101
|
+
|
|
102
|
+
# 参数处理逻辑:
|
|
103
|
+
# - 对于 bound method:签名中已经不包含 self,直接检查所有参数
|
|
104
|
+
# - 对于普通函数:跳过第一个参数(self/cls)
|
|
105
|
+
if not _isBoundMethod and _params:
|
|
106
|
+
# 普通函数,跳过第一个参数(self/cls)
|
|
107
|
+
_params = _params[1:]
|
|
108
|
+
# 对于 bound method,不需要跳过任何参数,因为签名已经不包含 self
|
|
109
|
+
|
|
110
|
+
# 检查是否有可接受inst的参数
|
|
111
|
+
_canAccept = False
|
|
112
|
+
for _p in _params:
|
|
113
|
+
# 检查是否是VAR_POSITIONAL (*args)
|
|
114
|
+
if _p.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
115
|
+
_canAccept = True
|
|
116
|
+
break
|
|
117
|
+
# 检查是否是VAR_KEYWORD (**kwargs)
|
|
118
|
+
if _p.kind == inspect.Parameter.VAR_KEYWORD:
|
|
119
|
+
_canAccept = True
|
|
120
|
+
break
|
|
121
|
+
# 检查是否是POSITIONAL_OR_KEYWORD 或 KEYWORD_ONLY
|
|
122
|
+
if _p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY):
|
|
123
|
+
_canAccept = True
|
|
124
|
+
break
|
|
125
|
+
# 检查是否是POSITIONAL_ONLY
|
|
126
|
+
if _p.kind == inspect.Parameter.POSITIONAL_ONLY:
|
|
127
|
+
_canAccept = True
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
# 尝试设置 __runtime_paramable__ 属性(如果可能)
|
|
131
|
+
try:
|
|
132
|
+
setattr(_method, '__runtime_paramable__', _canAccept)
|
|
133
|
+
except (AttributeError, TypeError):
|
|
134
|
+
# 对于某些无法设置属性的对象,忽略
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
# 缓存到模块级缓存
|
|
138
|
+
_RUNTIME_PARAMABLE_CACHE[_cacheKey] = _canAccept
|
|
139
|
+
return _canAccept
|
|
140
|
+
|
|
141
|
+
except (ValueError, TypeError):
|
|
142
|
+
# 如果无法获取签名,默认假设可以接受参数
|
|
143
|
+
_RUNTIME_PARAMABLE_CACHE[_cacheKey] = True
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _invokeStateMethod(_method, _inst):
|
|
148
|
+
"""
|
|
149
|
+
调用状态方法,根据方法签名自动决定是否传递inst参数。
|
|
150
|
+
|
|
151
|
+
注意:_method 应该是 bound method(从实例上获取的方法),
|
|
152
|
+
这样 self 已经自动绑定,我们只需要决定是否传递 inst。
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
_method: 状态方法(bound method)
|
|
156
|
+
_inst: 要传递的实例
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
方法的返回值
|
|
160
|
+
"""
|
|
161
|
+
if _checkMethodAcceptsParam(_method):
|
|
162
|
+
return _method(_inst)
|
|
163
|
+
else:
|
|
164
|
+
return _method()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class _NamedNode:
|
|
168
|
+
"""
|
|
169
|
+
Base class for named nodes in the FSM hierarchy.
|
|
170
|
+
|
|
171
|
+
Provides basic naming and identification functionality.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
NAME: ClassVar[str] = ""
|
|
175
|
+
|
|
176
|
+
def __init__(self) -> None:
|
|
177
|
+
self._uid: int = 0
|
|
178
|
+
self._name: str = self.NAME
|
|
179
|
+
self.__parent: _NamedNode | None = None
|
|
180
|
+
self.__children: list[_NamedNode] = []
|
|
181
|
+
|
|
182
|
+
def __repr__(self) -> str:
|
|
183
|
+
return f"{self.__class__.__name__}({self._name})"
|
|
184
|
+
|
|
185
|
+
def __hash__(self) -> int:
|
|
186
|
+
return hash((self.__class__, self._uid))
|
|
187
|
+
|
|
188
|
+
def __eq__(self, other: object) -> bool:
|
|
189
|
+
if not isinstance(other, _NamedNode):
|
|
190
|
+
return NotImplemented
|
|
191
|
+
return self._uid == other._uid and self.__class__ == other.__class__
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def parent(self) -> _NamedNode | None:
|
|
195
|
+
"""Get parent node."""
|
|
196
|
+
return self.__parent
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def _children(self) -> list[_NamedNode]:
|
|
200
|
+
"""Get child nodes."""
|
|
201
|
+
return self.__children
|
|
202
|
+
|
|
203
|
+
def _add_child(self, child: _NamedNode) -> None:
|
|
204
|
+
"""Add a child node."""
|
|
205
|
+
if child.__parent is not None:
|
|
206
|
+
child.__parent._remove_child(child)
|
|
207
|
+
self.__children.append(child)
|
|
208
|
+
child.__parent = self
|
|
209
|
+
|
|
210
|
+
def _remove_child(self, child: _NamedNode) -> None:
|
|
211
|
+
"""Remove a child node."""
|
|
212
|
+
if child in self.__children:
|
|
213
|
+
self.__children.remove(child)
|
|
214
|
+
child.__parent = None
|
|
215
|
+
|
|
216
|
+
class BlackBoard(object):
|
|
217
|
+
"""Data container that can be bound to a Stage instance.
|
|
218
|
+
|
|
219
|
+
Users can freely add any attributes to store shared data.
|
|
220
|
+
"""
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
class Stage(_NamedNode, metaclass=StageMachineMeta):
|
|
224
|
+
"""
|
|
225
|
+
Base class for finite state machines with automatic state discovery.
|
|
226
|
+
|
|
227
|
+
Features:
|
|
228
|
+
- Automatic state discovery via StageMachineMeta
|
|
229
|
+
- Recursive state execution with configurable limit
|
|
230
|
+
- Behavior tree support (sequence, selector, parallel)
|
|
231
|
+
- Event system integration
|
|
232
|
+
- Lifecycle hooks (onLoading, onChanged)
|
|
233
|
+
|
|
234
|
+
Class Attributes:
|
|
235
|
+
NAME: The display name for this stage type
|
|
236
|
+
LOG: Whether to log state transitions
|
|
237
|
+
LIMIT: Maximum recursive calls per tick (default: 24)
|
|
238
|
+
|
|
239
|
+
Example:
|
|
240
|
+
class MyStage(Stage):
|
|
241
|
+
NAME = "MyStage"
|
|
242
|
+
|
|
243
|
+
def idle(self):
|
|
244
|
+
if self.should_work:
|
|
245
|
+
return "working"
|
|
246
|
+
|
|
247
|
+
def working(self):
|
|
248
|
+
if self.work_done:
|
|
249
|
+
return "idle"
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
# Class attributes
|
|
253
|
+
NAME: ClassVar[str] = "Stage"
|
|
254
|
+
LOG: ClassVar[bool] = True
|
|
255
|
+
LIMIT: ClassVar[int] = 24
|
|
256
|
+
|
|
257
|
+
# Class-level state tracking (populated by metaclass)
|
|
258
|
+
__states__: ClassVar[list[str]] = []
|
|
259
|
+
__recursive__: ClassVar[set[str]] = set()
|
|
260
|
+
__listens__: ClassVar[dict[str, list[tuple[str, tuple[str, ...]]]]] = {}
|
|
261
|
+
__behaviors__: ClassVar[dict[str, dict[str, Any]]] = {}
|
|
262
|
+
IGNORES: ClassVar[list[str]] = []
|
|
263
|
+
|
|
264
|
+
def __init__(self, inst: Any = None) -> None:
|
|
265
|
+
"""
|
|
266
|
+
Initialize the Stage.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
inst: The bound instance (e.g., game entity, component owner)
|
|
270
|
+
"""
|
|
271
|
+
super().__init__()
|
|
272
|
+
|
|
273
|
+
# Instance attributes
|
|
274
|
+
self._inst: Any = inst or BlackBoard()
|
|
275
|
+
self._current: str | None = None
|
|
276
|
+
self._recursive_left: int = self.LIMIT
|
|
277
|
+
self._states_length: int = len(self.__states__)
|
|
278
|
+
self._uid: int = StageMachineMeta.requireId(self.NAME)
|
|
279
|
+
self._name: str = f"{self.NAME}{self._uid}"
|
|
280
|
+
|
|
281
|
+
# Event system
|
|
282
|
+
self._eda: EventSystem = GetES()
|
|
283
|
+
|
|
284
|
+
# Behavior tree context
|
|
285
|
+
self._bhead: str | None = None
|
|
286
|
+
self._bhcontext: dict[str, Any] = {}
|
|
287
|
+
|
|
288
|
+
# Initialize to first state if available
|
|
289
|
+
if self.__states__:
|
|
290
|
+
self._current = self.__states__[0]
|
|
291
|
+
|
|
292
|
+
self.__login_event__()
|
|
293
|
+
self.onLoading(self._inst)
|
|
294
|
+
|
|
295
|
+
def __login_event__(self):
|
|
296
|
+
for attr in dir(self):
|
|
297
|
+
if attr.startswith("_") or attr in StageMachineMeta.IGNORES:
|
|
298
|
+
continue
|
|
299
|
+
val = getattr(self, attr)
|
|
300
|
+
if callable(val) and hasattr(val, "_listen_event"):
|
|
301
|
+
if not isinstance(val._listen_event, str):
|
|
302
|
+
raise TypeError(f"[Inner System Load]: method '{attr}'._listen_event get type '{type(val._listen_event)}', which ecpected str.")
|
|
303
|
+
self.listenFor(val._listen_event, val)
|
|
304
|
+
|
|
305
|
+
def __del__(self):
|
|
306
|
+
for attr in dir(self):
|
|
307
|
+
if attr.startswith("_") or attr in StageMachineMeta.IGNORES:
|
|
308
|
+
continue
|
|
309
|
+
val = getattr(self, attr)
|
|
310
|
+
if callable(val) and hasattr(val, "_listen_event"):
|
|
311
|
+
if not isinstance(val._listen_event, str):
|
|
312
|
+
raise TypeError(
|
|
313
|
+
f"[Inner System Load]: method '{attr}'._listen_event get type '{type(val._listen_event)}', which ecpected str.")
|
|
314
|
+
self.cancelListen(val._listen_event, val)
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def uid(self) -> int:
|
|
318
|
+
"""Get unique identifier."""
|
|
319
|
+
return self._uid
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def name(self) -> str:
|
|
323
|
+
"""Get full name with uid."""
|
|
324
|
+
return self._name
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def current(self) -> str | None:
|
|
328
|
+
"""Get current state name."""
|
|
329
|
+
return self._current
|
|
330
|
+
|
|
331
|
+
@current.setter
|
|
332
|
+
def current(self, value: str | None) -> None:
|
|
333
|
+
"""Set current state with validation."""
|
|
334
|
+
if value is not None and value not in self.__states__:
|
|
335
|
+
raise ValueError(f"Invalid state: {value}")
|
|
336
|
+
self._current = value
|
|
337
|
+
|
|
338
|
+
def onLoading(self, inst: Any) -> None:
|
|
339
|
+
"""
|
|
340
|
+
Lifecycle hook called after initialization.
|
|
341
|
+
|
|
342
|
+
Override this to perform setup after the stage is loaded.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
inst: The bound instance
|
|
346
|
+
"""
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
def onChanged(self, src: str | None, dst: str | None, inst: Any) -> None:
|
|
350
|
+
"""
|
|
351
|
+
Lifecycle hook called when state changes.
|
|
352
|
+
|
|
353
|
+
Override this to react to state transitions.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
src: Source state name (None if initial)
|
|
357
|
+
dst: Destination state name (None if terminal)
|
|
358
|
+
inst: The bound instance
|
|
359
|
+
"""
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
def handleStage(self, log: bool = True) -> str | None:
|
|
363
|
+
"""
|
|
364
|
+
Execute the current state and handle transitions.
|
|
365
|
+
|
|
366
|
+
This is the main execution method that:
|
|
367
|
+
1. Calls the current state method
|
|
368
|
+
2. Handles return values (state transitions)
|
|
369
|
+
3. Supports recursive state execution
|
|
370
|
+
4. Handles behavior tree composites
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
log: Whether to log state transitions
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
The final state after execution, or None if no states
|
|
377
|
+
|
|
378
|
+
Example:
|
|
379
|
+
>>> stage = MyStage()
|
|
380
|
+
>>> stage.handleStage() # Execute one tick
|
|
381
|
+
"""
|
|
382
|
+
if not self._current or self._states_length == 0:
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
# Reset recursive counter at start of tick
|
|
386
|
+
self._recursive_left = self.LIMIT
|
|
387
|
+
|
|
388
|
+
# Execute current state (with recursion support)
|
|
389
|
+
result = self._execute_state(self._current, log)
|
|
390
|
+
|
|
391
|
+
return result
|
|
392
|
+
|
|
393
|
+
def _execute_state(self, state_name: str, log: bool, pre_result: Any = None, send_enter: bool = True) -> str | None:
|
|
394
|
+
"""
|
|
395
|
+
Execute a single state and handle its result.
|
|
396
|
+
This is the entry point for ALL state execution.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
state_name: Name of state to execute
|
|
400
|
+
log: Whether to log transitions
|
|
401
|
+
pre_result: Optional pre-computed result (avoids double execution)
|
|
402
|
+
send_enter: Whether to send 'enter' event (default True)
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Final state after execution
|
|
406
|
+
"""
|
|
407
|
+
if self._recursive_left <= 0:
|
|
408
|
+
# Limit reached, stop recursion
|
|
409
|
+
return state_name
|
|
410
|
+
|
|
411
|
+
self._recursive_left -= 1
|
|
412
|
+
|
|
413
|
+
# Check if we have behavior tree context that needs to be resumed
|
|
414
|
+
# This happens when a sequence/selector/parallel paused and _current was set to a child
|
|
415
|
+
if self._bhead is not None and state_name != self._bhead and not hasattr(self, '_resuming_behavior'):
|
|
416
|
+
# We have active behavior tree context and current state is not the root
|
|
417
|
+
# Check if current state is a child of the behavior tree
|
|
418
|
+
head_behavior = self.__behaviors__.get(self._bhead)
|
|
419
|
+
if head_behavior and state_name in head_behavior.get('children', []):
|
|
420
|
+
# Current state is a child of the active behavior tree
|
|
421
|
+
# Resume behavior tree execution from the root
|
|
422
|
+
# Set flag to prevent recursive resuming
|
|
423
|
+
self._resuming_behavior = True
|
|
424
|
+
try:
|
|
425
|
+
result = self._execute_state(self._bhead, log, pre_result=None, send_enter=False)
|
|
426
|
+
finally:
|
|
427
|
+
if hasattr(self, '_resuming_behavior'):
|
|
428
|
+
delattr(self, '_resuming_behavior')
|
|
429
|
+
return result
|
|
430
|
+
|
|
431
|
+
# Get the state method
|
|
432
|
+
state_method = getattr(self, state_name, None)
|
|
433
|
+
if state_method is None or not callable(state_method):
|
|
434
|
+
return state_name
|
|
435
|
+
|
|
436
|
+
# Check if this is a behavior tree composite
|
|
437
|
+
behavior_info = self.__behaviors__.get(state_name)
|
|
438
|
+
if behavior_info:
|
|
439
|
+
# Behavior tree composite - let it handle execution
|
|
440
|
+
# It will eventually call back to _execute_state for child states
|
|
441
|
+
behavior_result = self._handle_behavior(state_name, behavior_info, log)
|
|
442
|
+
# Handle the result through _handle_result to ensure proper state transitions
|
|
443
|
+
# But only if it's a string (state transition) and not a special behavior result
|
|
444
|
+
if isinstance(behavior_result, str):
|
|
445
|
+
return self._handle_result(state_name, behavior_result, log)
|
|
446
|
+
# 行为树完成(True/False),触发 behavior_completed 事件
|
|
447
|
+
if behavior_result is True or behavior_result is False:
|
|
448
|
+
self._trigger_behavior_completed_if_needed()
|
|
449
|
+
return behavior_result
|
|
450
|
+
|
|
451
|
+
# Send 'enter' event before executing the state (if requested)
|
|
452
|
+
# This is the ONLY place where 'enter' event is triggered for regular states
|
|
453
|
+
if send_enter:
|
|
454
|
+
self.pushEvent("enter", {
|
|
455
|
+
"inst": self,
|
|
456
|
+
"state": state_name
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
# Use pre-computed result if provided, otherwise execute the state method
|
|
460
|
+
try:
|
|
461
|
+
if pre_result is not None:
|
|
462
|
+
result = pre_result
|
|
463
|
+
else:
|
|
464
|
+
result = _invokeStateMethod(state_method, self._inst)
|
|
465
|
+
except Exception as e:
|
|
466
|
+
# Log error but don't crash
|
|
467
|
+
if log and self.LOG:
|
|
468
|
+
print(f"[{self._name}] Error in state '{state_name}': {e}")
|
|
469
|
+
return state_name
|
|
470
|
+
|
|
471
|
+
# Handle result - this may call _transition_to for state changes
|
|
472
|
+
handled_result = self._handle_result(state_name, result, log)
|
|
473
|
+
|
|
474
|
+
# For behavior tree children: preserve None (RUN) semantics
|
|
475
|
+
# _handle_result converts None to state_name, but we need None for RUN
|
|
476
|
+
if self._bhead is not None and result is None and handled_result == state_name:
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
return handled_result
|
|
480
|
+
|
|
481
|
+
def _get_state_result(self, state_name: str) -> Any:
|
|
482
|
+
"""
|
|
483
|
+
Execute a state method and return its raw result (for behavior tree logic).
|
|
484
|
+
|
|
485
|
+
Unlike _execute_state which returns the handled result (next state name),
|
|
486
|
+
this method returns the actual return value from the state method.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
state_name: Name of state to execute
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Raw return value from the state method
|
|
493
|
+
"""
|
|
494
|
+
state_method = getattr(self, state_name, None)
|
|
495
|
+
if state_method is None or not callable(state_method):
|
|
496
|
+
return None
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
return _invokeStateMethod(state_method, self._inst)
|
|
500
|
+
except Exception:
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
def _handle_result(
|
|
504
|
+
self,
|
|
505
|
+
state_name: str,
|
|
506
|
+
result: Any,
|
|
507
|
+
log: bool
|
|
508
|
+
) -> str | None:
|
|
509
|
+
"""
|
|
510
|
+
Handle the return value from a state method.
|
|
511
|
+
ALL state transitions go through _transition_to.
|
|
512
|
+
|
|
513
|
+
Return value meanings:
|
|
514
|
+
- str: Transition to named state (calls _transition_to)
|
|
515
|
+
- True/None: Success, stay in current state
|
|
516
|
+
- False: Failure (for behavior trees)
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
state_name: Current state name
|
|
520
|
+
result: Return value from state method
|
|
521
|
+
log: Whether to log
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
Final state after handling result
|
|
525
|
+
"""
|
|
526
|
+
# String result = state transition - use _transition_to
|
|
527
|
+
if isinstance(result, str):
|
|
528
|
+
if result in self.__states__ or result in self.__behaviors__:
|
|
529
|
+
# Check if transition was already handled by behavior tree BREAK
|
|
530
|
+
# If suppress flag is set, the transition already happened in _execute_sequence/_execute_selector/_execute_parallel
|
|
531
|
+
if getattr(self, "_suppress_changed", False):
|
|
532
|
+
# Transition already handled, just return the result
|
|
533
|
+
# Clear the flag since we're consuming it
|
|
534
|
+
delattr(self, "_suppress_changed")
|
|
535
|
+
# But if target is recursive state, we need to execute it
|
|
536
|
+
if result in self.__recursive__ and self._recursive_left > 0:
|
|
537
|
+
return self._execute_state(result, log)
|
|
538
|
+
return result
|
|
539
|
+
|
|
540
|
+
# Skip if target state is the same as current state AND state is not recursive
|
|
541
|
+
# This prevents duplicate events when behavior tree completes and returns parent name
|
|
542
|
+
# But allows recursive states to re-execute themselves
|
|
543
|
+
if result == state_name and state_name not in self.__recursive__:
|
|
544
|
+
return state_name
|
|
545
|
+
|
|
546
|
+
# Transition to new state - _transition_to handles changed event
|
|
547
|
+
transitioned = self._transition_to(result, state_name, log, result=result)
|
|
548
|
+
|
|
549
|
+
# Only execute new state if it's marked as recursive
|
|
550
|
+
if transitioned and transitioned in self.__recursive__ and self._recursive_left > 0:
|
|
551
|
+
return self._execute_state(transitioned, log)
|
|
552
|
+
|
|
553
|
+
return transitioned
|
|
554
|
+
else:
|
|
555
|
+
# Unknown state, log warning
|
|
556
|
+
if log and self.LOG:
|
|
557
|
+
print(f"[{self._name}] Unknown state: {result}")
|
|
558
|
+
return state_name
|
|
559
|
+
|
|
560
|
+
# None or True = success, stay in state
|
|
561
|
+
if result is None or result is True:
|
|
562
|
+
# Check if recursive state should continue (recursive execution, not transition)
|
|
563
|
+
if state_name in self.__recursive__ and self._recursive_left > 0:
|
|
564
|
+
return self._execute_state(state_name, log)
|
|
565
|
+
|
|
566
|
+
# No behavior tree context: trigger changed event even when staying in same state
|
|
567
|
+
# This ensures consistency - ALL state execution goes through _transition_to
|
|
568
|
+
if self._bhead is None:
|
|
569
|
+
return self._transition_to(state_name, state_name, log, event_type="-", result=result)
|
|
570
|
+
|
|
571
|
+
return state_name
|
|
572
|
+
|
|
573
|
+
# False = failure (for behavior trees)
|
|
574
|
+
if result is False:
|
|
575
|
+
return False # type: ignore[return-value]
|
|
576
|
+
|
|
577
|
+
# Unknown result type, stay in current state
|
|
578
|
+
return state_name
|
|
579
|
+
|
|
580
|
+
def _handle_behavior(
|
|
581
|
+
self,
|
|
582
|
+
state_name: str,
|
|
583
|
+
behavior_info: dict[str, Any],
|
|
584
|
+
log: bool
|
|
585
|
+
) -> str | None:
|
|
586
|
+
"""
|
|
587
|
+
Handle behavior tree composite execution with RUN support.
|
|
588
|
+
|
|
589
|
+
Supports:
|
|
590
|
+
- sequence: Execute all children in order, fail if any fails
|
|
591
|
+
- selector: Execute children until one succeeds
|
|
592
|
+
- parallel: Execute all children, succeed if any succeeds
|
|
593
|
+
|
|
594
|
+
RUN (None) semantics:
|
|
595
|
+
- Child returns None: pause execution, stay in current state, resume next tick
|
|
596
|
+
- Uses _bhead and _bhcontext to track execution state across ticks
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
state_name: Name of behavior state
|
|
600
|
+
behavior_info: Behavior configuration dict
|
|
601
|
+
log: Whether to log
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
Result state, False for failure, or None for RUN
|
|
605
|
+
"""
|
|
606
|
+
_children = behavior_info.get('children', [])
|
|
607
|
+
composite: str | None = behavior_info.get('composite')
|
|
608
|
+
|
|
609
|
+
# Normalize children to list (handle string case for simple behavior)
|
|
610
|
+
if isinstance(_children, str):
|
|
611
|
+
children: list[str] = [_children]
|
|
612
|
+
else:
|
|
613
|
+
children: list[str] = _children
|
|
614
|
+
|
|
615
|
+
if not children:
|
|
616
|
+
return state_name
|
|
617
|
+
|
|
618
|
+
# Check if resuming: state has context stored in _bhcontext
|
|
619
|
+
is_resuming = state_name in self._bhcontext
|
|
620
|
+
|
|
621
|
+
# If resuming but sequence already completed (index >= len(children)), restart
|
|
622
|
+
if is_resuming and composite:
|
|
623
|
+
ctx = self._bhcontext.get(state_name)
|
|
624
|
+
if ctx:
|
|
625
|
+
idx = ctx.get("index", 0)
|
|
626
|
+
if idx >= len(children):
|
|
627
|
+
# Sequence completed, restart from beginning
|
|
628
|
+
is_resuming = False
|
|
629
|
+
del self._bhcontext[state_name]
|
|
630
|
+
# Keep the stack and history for context
|
|
631
|
+
|
|
632
|
+
# Calculate indent level based on behavior stack depth
|
|
633
|
+
# Root behavior: indent_level = 0 (0 spaces)
|
|
634
|
+
# Nested behaviors: indent_level = stack_depth (2, 4, 6... spaces)
|
|
635
|
+
context_stack: list[str] = self._bhcontext.get("stack", [])
|
|
636
|
+
if state_name == self._bhead:
|
|
637
|
+
# Root level behavior (first entry or resuming)
|
|
638
|
+
indent_level = 0
|
|
639
|
+
else:
|
|
640
|
+
# Nested behavior: indent based on stack depth
|
|
641
|
+
indent_level = len(context_stack)
|
|
642
|
+
indent = " " * indent_level
|
|
643
|
+
|
|
644
|
+
bold_name = f"{Style.BRIGHT}{self._name}{Style.RESET_ALL}" if _COLOR_AVAILABLE else self._name
|
|
645
|
+
# Root behavior prints [name], nested behaviors don't
|
|
646
|
+
prefix = f"[{bold_name}] " if indent_level == 0 else ""
|
|
647
|
+
|
|
648
|
+
if self._bhead is None:
|
|
649
|
+
self._bhead = state_name
|
|
650
|
+
if not self._bhcontext:
|
|
651
|
+
self._bhcontext = {"stack": [], "history": []}
|
|
652
|
+
|
|
653
|
+
# Send 'enter' event for behavior tree composite node
|
|
654
|
+
# This allows visualizers to show the node before execution starts
|
|
655
|
+
if not is_resuming:
|
|
656
|
+
self.pushEvent("enter", {
|
|
657
|
+
"inst": self,
|
|
658
|
+
"state": state_name,
|
|
659
|
+
"type": "behavior_parent"
|
|
660
|
+
})
|
|
661
|
+
# Process Qt events immediately so visualizers update before potential long operations
|
|
662
|
+
try:
|
|
663
|
+
app = QApplication.instance()
|
|
664
|
+
if app:
|
|
665
|
+
app.processEvents()
|
|
666
|
+
except ImportError:
|
|
667
|
+
pass
|
|
668
|
+
|
|
669
|
+
# Execute parent state's method body ONLY on first entry (not when resuming from RUN)
|
|
670
|
+
# When resuming, we skip parent and continue from where we left off
|
|
671
|
+
if not is_resuming:
|
|
672
|
+
state_method = getattr(self, state_name, None)
|
|
673
|
+
if state_method is not None and callable(state_method):
|
|
674
|
+
try:
|
|
675
|
+
parent_result = state_method(self._inst)
|
|
676
|
+
except Exception as e:
|
|
677
|
+
if log and self.LOG:
|
|
678
|
+
print(f"[{self._name}] Error in behavior parent '{state_name}': {e}")
|
|
679
|
+
parent_result = None
|
|
680
|
+
|
|
681
|
+
# Handle parent result
|
|
682
|
+
# - String: transition to that state (override behavior tree)
|
|
683
|
+
# - True/None: continue to execute children
|
|
684
|
+
# - False: for selector, try children; for others, fail immediately
|
|
685
|
+
if isinstance(parent_result, str):
|
|
686
|
+
self._reset_behavior_context()
|
|
687
|
+
# Use _transition_to for state transition (triggers changed event)
|
|
688
|
+
return self._transition_to(parent_result, state_name, log, event_type="behavior", result=parent_result)
|
|
689
|
+
if parent_result is False and composite != 'selector':
|
|
690
|
+
# Parent failed, behavior fails immediately (except selector which tries children)
|
|
691
|
+
self._bhcontext["__print_result__"] = "FAIL"
|
|
692
|
+
self._bhcontext["__print_parent__"] = state_name
|
|
693
|
+
self._reset_behavior_context()
|
|
694
|
+
return False
|
|
695
|
+
# Success: store print info and continue to children
|
|
696
|
+
self._bhcontext["__print_result__"] = "OK"
|
|
697
|
+
self._bhcontext["__print_parent__"] = state_name
|
|
698
|
+
self._bhcontext["__print_composite__"] = composite
|
|
699
|
+
self._bhcontext["__print_children__"] = children
|
|
700
|
+
# Note: parent execution result is printed by _transition_print
|
|
701
|
+
else:
|
|
702
|
+
# No parent method, just store print info
|
|
703
|
+
self._bhcontext["__print_result__"] = "OK"
|
|
704
|
+
self._bhcontext["__print_parent__"] = state_name
|
|
705
|
+
self._bhcontext["__print_composite__"] = composite
|
|
706
|
+
self._bhcontext["__print_children__"] = children
|
|
707
|
+
# Note: parent execution result is printed by _transition_print
|
|
708
|
+
# Note: resuming behavior tree doesn't print anything to keep output clean
|
|
709
|
+
|
|
710
|
+
match composite:
|
|
711
|
+
case 'sequence':
|
|
712
|
+
return self._execute_sequence(state_name, children, log)
|
|
713
|
+
case 'selector':
|
|
714
|
+
return self._execute_selector(state_name, children, log)
|
|
715
|
+
case 'parallel':
|
|
716
|
+
return self._execute_parallel(state_name, children, log)
|
|
717
|
+
case _:
|
|
718
|
+
# Simple behavior - execute child behavior tree if applicable
|
|
719
|
+
_child = children[0] if children else None
|
|
720
|
+
if _child is None:
|
|
721
|
+
self._reset_behavior_context()
|
|
722
|
+
return state_name
|
|
723
|
+
|
|
724
|
+
# Check if child is a behavior tree node
|
|
725
|
+
_child_behavior = self.__behaviors__.get(_child)
|
|
726
|
+
if _child_behavior is not None:
|
|
727
|
+
# Child is behavior tree - execute it
|
|
728
|
+
_result = self._handle_behavior(_child, _child_behavior, log)
|
|
729
|
+
# Only reset context if child completed (not RUN/None)
|
|
730
|
+
if _result is not None:
|
|
731
|
+
# 检查 BREAK 是否已被处理,如果是则不重置上下文(保留标记)
|
|
732
|
+
if not getattr(self, '_break_handled', False):
|
|
733
|
+
# Send completion event for root behavior before reset
|
|
734
|
+
if self._bhead == state_name:
|
|
735
|
+
_history = self._bhcontext.get("history", [])
|
|
736
|
+
# Record simple behavior result based on child result
|
|
737
|
+
_history.append((state_name, "success" if _result is not False else "fail"))
|
|
738
|
+
self.pushEvent("behavior_completed", {
|
|
739
|
+
"success": _result is not False,
|
|
740
|
+
"history": _history.copy(),
|
|
741
|
+
"failed_node": _child if _result is False else None
|
|
742
|
+
})
|
|
743
|
+
self._reset_behavior_context()
|
|
744
|
+
return _result
|
|
745
|
+
else:
|
|
746
|
+
# Child is regular state - transition and execute via _transition_to
|
|
747
|
+
# _transition_to triggers changed event
|
|
748
|
+
self._reset_behavior_context()
|
|
749
|
+
return self._transition_to(_child, state_name, log, event_type="behavior", result=_child)
|
|
750
|
+
|
|
751
|
+
def _reset_behavior_context(self) -> None:
|
|
752
|
+
"""Reset behavior tree execution context."""
|
|
753
|
+
self._bhead = None
|
|
754
|
+
self._bhcontext = {}
|
|
755
|
+
|
|
756
|
+
def _trigger_behavior_completed_if_needed(self) -> None:
|
|
757
|
+
"""Trigger behavior_completed event if __behavior_return__ mark exists."""
|
|
758
|
+
if not self._bhcontext:
|
|
759
|
+
return
|
|
760
|
+
behavior_return = self._bhcontext.get("__behavior_return__")
|
|
761
|
+
if behavior_return is None:
|
|
762
|
+
return
|
|
763
|
+
# 获取保存的数据
|
|
764
|
+
history = self._bhcontext.get("__behavior_history__", [])
|
|
765
|
+
success = self._bhcontext.get("__behavior_success__")
|
|
766
|
+
failed_node = self._bhcontext.get("__behavior_failed_node__")
|
|
767
|
+
if history is not None and success is not None:
|
|
768
|
+
event_data = {
|
|
769
|
+
"inst": self,
|
|
770
|
+
"success": success,
|
|
771
|
+
"history": history
|
|
772
|
+
}
|
|
773
|
+
if failed_node:
|
|
774
|
+
event_data["failed_node"] = failed_node
|
|
775
|
+
self.pushEvent("behavior_completed", event_data)
|
|
776
|
+
# 重置上下文
|
|
777
|
+
self._reset_behavior_context()
|
|
778
|
+
|
|
779
|
+
def _execute_sequence(self, parent_name: str, children: list[str], log: bool) -> str | None:
|
|
780
|
+
"""
|
|
781
|
+
Execute children in sequence (all must succeed).
|
|
782
|
+
|
|
783
|
+
RUN (None) handling:
|
|
784
|
+
- If a child returns None, pause and resume from the same child next tick
|
|
785
|
+
- Uses _bhcontext to track current index across ticks
|
|
786
|
+
|
|
787
|
+
Args:
|
|
788
|
+
parent_name: Name of the parent behavior state
|
|
789
|
+
children: List of child state names
|
|
790
|
+
log: Whether to log
|
|
791
|
+
|
|
792
|
+
Returns:
|
|
793
|
+
Last successful state, False if any fails, or None if RUN
|
|
794
|
+
"""
|
|
795
|
+
max_len = getattr(self, '__max_state_len__', 0)
|
|
796
|
+
bold_name = f"{Style.BRIGHT}{self._name}{Style.RESET_ALL}" if _COLOR_AVAILABLE else self._name
|
|
797
|
+
context_stack: list[str] = self._bhcontext.get("stack", [])
|
|
798
|
+
|
|
799
|
+
# Check if this is first entry or resuming
|
|
800
|
+
is_first_entry = parent_name not in self._bhcontext
|
|
801
|
+
|
|
802
|
+
# Get or initialize sequence context
|
|
803
|
+
if is_first_entry:
|
|
804
|
+
seq_context = {"index": 0}
|
|
805
|
+
self._bhcontext[parent_name] = seq_context
|
|
806
|
+
# First time: push to stack for calculating children's indent
|
|
807
|
+
if parent_name not in context_stack:
|
|
808
|
+
context_stack.append(parent_name)
|
|
809
|
+
else:
|
|
810
|
+
seq_context = self._bhcontext[parent_name]
|
|
811
|
+
# Resuming: ensure stack contains this parent at the top
|
|
812
|
+
if parent_name not in context_stack:
|
|
813
|
+
context_stack.append(parent_name)
|
|
814
|
+
else:
|
|
815
|
+
# Remove any items above this parent
|
|
816
|
+
while context_stack and context_stack[-1] != parent_name:
|
|
817
|
+
context_stack.pop()
|
|
818
|
+
|
|
819
|
+
# Calculate indent based on nesting depth
|
|
820
|
+
# Root's children: 2 spaces, nested children: 4, 6... spaces
|
|
821
|
+
indent_level = len(context_stack)
|
|
822
|
+
child_indent = " " * indent_level if indent_level > 0 else " "
|
|
823
|
+
result_indent = " " * (indent_level - 1) if indent_level > 1 else ""
|
|
824
|
+
|
|
825
|
+
current_index = seq_context["index"]
|
|
826
|
+
|
|
827
|
+
# Execute from current index
|
|
828
|
+
while current_index < len(children):
|
|
829
|
+
child = children[current_index]
|
|
830
|
+
|
|
831
|
+
if child not in self.__states__ and child not in self.__behaviors__:
|
|
832
|
+
if log and self.LOG:
|
|
833
|
+
padded = child.ljust(max_len)
|
|
834
|
+
print(f"{child_indent}-> {padded} [{Fore.RED}UNKNOWN - FAIL{Fore.RESET}]")
|
|
835
|
+
self._reset_behavior_context()
|
|
836
|
+
return False # type: ignore[return-value]
|
|
837
|
+
|
|
838
|
+
# Check if child is a behavior composite
|
|
839
|
+
child_behavior = self.__behaviors__.get(child)
|
|
840
|
+
is_child_behavior = child_behavior is not None
|
|
841
|
+
|
|
842
|
+
# Transition to child state - triggers CHANGED event
|
|
843
|
+
# This ensures every node execution follows: parent -> child transition
|
|
844
|
+
# Avoid duplicate transition if already at this child (from behavior resume)
|
|
845
|
+
if self._current != child:
|
|
846
|
+
self._bhcontext["_transitioning_to"] = child
|
|
847
|
+
try:
|
|
848
|
+
self._transition_to(child, parent_name if is_first_entry else self._current, log, event_type="behavior")
|
|
849
|
+
finally:
|
|
850
|
+
del self._bhcontext["_transitioning_to"]
|
|
851
|
+
|
|
852
|
+
# Check if child is a behavior composite - if so, let _handle_behavior execute it
|
|
853
|
+
# to avoid double execution (once in _get_state_result, once in _handle_behavior)
|
|
854
|
+
if is_child_behavior:
|
|
855
|
+
# For behavior children, directly call _handle_behavior to avoid double execution
|
|
856
|
+
executed_result = self._handle_behavior(child, child_behavior, log)
|
|
857
|
+
actual_result = executed_result
|
|
858
|
+
|
|
859
|
+
# Handle string result (BREAK) from behavior child
|
|
860
|
+
# Use _transition_to for state transition (triggers changed event)
|
|
861
|
+
if isinstance(actual_result, str):
|
|
862
|
+
# Send compisited event for composite node interruption
|
|
863
|
+
self.pushEvent("compisited", {
|
|
864
|
+
"inst": self,
|
|
865
|
+
"name": parent_name,
|
|
866
|
+
"child": child,
|
|
867
|
+
"result": None
|
|
868
|
+
})
|
|
869
|
+
# Set suppress flag to prevent parent behavior nodes from firing CHANGED
|
|
870
|
+
# Context cleanup will be handled by _transition_to
|
|
871
|
+
self._suppress_changed = True
|
|
872
|
+
# Transition to new state - _transition_to handles context reset
|
|
873
|
+
return self._transition_to(actual_result, child, log, result=actual_result)
|
|
874
|
+
else:
|
|
875
|
+
# Only print -> child on first entry, not on resume
|
|
876
|
+
# (Resume is handled by _execute_state which will call us back if needed)
|
|
877
|
+
if is_first_entry and log and self.LOG:
|
|
878
|
+
# Store first entry flag for _transition_print
|
|
879
|
+
self._bhcontext["__is_first_entry__"] = True
|
|
880
|
+
|
|
881
|
+
# Process Qt events before executing child to handle pending UI updates
|
|
882
|
+
try:
|
|
883
|
+
app = QApplication.instance()
|
|
884
|
+
if app:
|
|
885
|
+
app.processEvents()
|
|
886
|
+
except ImportError:
|
|
887
|
+
pass
|
|
888
|
+
|
|
889
|
+
# Execute state to get result
|
|
890
|
+
actual_result = self._get_state_result(child)
|
|
891
|
+
|
|
892
|
+
# String result means state transition (BREAK) - use _transition_to
|
|
893
|
+
if isinstance(actual_result, str):
|
|
894
|
+
# Store print info
|
|
895
|
+
self._bhcontext["__print_result__"] = "BREAK"
|
|
896
|
+
# Send compisited event
|
|
897
|
+
self.pushEvent("compisited", {
|
|
898
|
+
"inst": self,
|
|
899
|
+
"name": parent_name,
|
|
900
|
+
"child": child,
|
|
901
|
+
"result": None
|
|
902
|
+
})
|
|
903
|
+
# 标记行为树完成(BREAK 情况),但不触发 behavior_completed(在 _transition_to 中统一触发)
|
|
904
|
+
# 获取历史记录(对于嵌套行为树,保留完整历史)
|
|
905
|
+
history = self._bhcontext.get("history", [])
|
|
906
|
+
history.append((child, "break"))
|
|
907
|
+
# 设置根行为树的完成标记(如果当前是根行为树或嵌套行为树)
|
|
908
|
+
if self._bhead is not None:
|
|
909
|
+
self._bhcontext["__behavior_history__"] = history.copy()
|
|
910
|
+
self._bhcontext["__behavior_success__"] = True # BREAK 视为成功
|
|
911
|
+
self._bhcontext["__behavior_return__"] = parent_name
|
|
912
|
+
# 设置标记表示 BREAK 已处理,防止外层行为树重复处理
|
|
913
|
+
# 使用实例变量而不是 bhcontext,因为 bhcontext 会被 _transition_to 重置
|
|
914
|
+
self._break_handled = True
|
|
915
|
+
# Set suppress flag to prevent parent behavior nodes from firing CHANGED
|
|
916
|
+
self._suppress_changed = True
|
|
917
|
+
# Transition to new state - _transition_to handles context reset and behavior_completed
|
|
918
|
+
return self._transition_to(actual_result, child, log, result=actual_result)
|
|
919
|
+
|
|
920
|
+
# Execute state with pre-computed result (only if not None/False)
|
|
921
|
+
# For None/False, we handle directly without calling _execute_state again
|
|
922
|
+
if actual_result not in (None, False):
|
|
923
|
+
# _execute_state triggers enter event
|
|
924
|
+
# Set _resuming_behavior to prevent _execute_state from re-resuming the behavior tree
|
|
925
|
+
self._resuming_behavior = True
|
|
926
|
+
try:
|
|
927
|
+
self._execute_state(child, log, pre_result=actual_result, send_enter=True)
|
|
928
|
+
finally:
|
|
929
|
+
delattr(self, '_resuming_behavior')
|
|
930
|
+
|
|
931
|
+
# Note: changed event and onChanged lifecycle hook are triggered in _transition_to
|
|
932
|
+
# when transitioning to child state. No need to trigger here.
|
|
933
|
+
|
|
934
|
+
# RUN (None): pause execution, resume next tick
|
|
935
|
+
# But if this is the last child, treat it as success and return child name
|
|
936
|
+
if actual_result is None:
|
|
937
|
+
self._bhcontext["__print_result__"] = "RUN"
|
|
938
|
+
self._bhcontext["__print_child__"] = child
|
|
939
|
+
# Update _current to the child that returned RUN
|
|
940
|
+
# This ensures next tick resumes from this child, not parent
|
|
941
|
+
self._current = child
|
|
942
|
+
# If last child returned None, complete sequence with this child
|
|
943
|
+
if current_index == len(children) - 1:
|
|
944
|
+
# Last child - complete the sequence
|
|
945
|
+
seq_context["index"] = current_index + 1
|
|
946
|
+
break
|
|
947
|
+
# Not last child - pause and resume next tick
|
|
948
|
+
seq_context["index"] = current_index
|
|
949
|
+
return None
|
|
950
|
+
|
|
951
|
+
# False result means failure
|
|
952
|
+
if actual_result is False:
|
|
953
|
+
# Store print info for _transition_print to use
|
|
954
|
+
self._bhcontext["__print_result__"] = "FAIL"
|
|
955
|
+
self._bhcontext["__print_child__"] = child
|
|
956
|
+
self._bhcontext["__print_parent__"] = parent_name
|
|
957
|
+
# Record failure in history
|
|
958
|
+
history = self._bhcontext.get("history", [])
|
|
959
|
+
history.append((child, "fail"))
|
|
960
|
+
history.append((parent_name, "fail"))
|
|
961
|
+
# Send compisited event for composite node exit
|
|
962
|
+
self.pushEvent("compisited", {
|
|
963
|
+
"inst": self,
|
|
964
|
+
"name": parent_name,
|
|
965
|
+
"child": child,
|
|
966
|
+
"result": False
|
|
967
|
+
})
|
|
968
|
+
# Pop from stack and clean up
|
|
969
|
+
if context_stack and context_stack[-1] == parent_name:
|
|
970
|
+
context_stack.pop()
|
|
971
|
+
if parent_name in self._bhcontext:
|
|
972
|
+
del self._bhcontext[parent_name]
|
|
973
|
+
# If this was the root behavior, send completion event and trigger return to bhead
|
|
974
|
+
_bhead = self._bhead
|
|
975
|
+
if _bhead == parent_name:
|
|
976
|
+
self.pushEvent("behavior_completed", {
|
|
977
|
+
"success": False,
|
|
978
|
+
"history": history.copy(),
|
|
979
|
+
"failed_node": child
|
|
980
|
+
})
|
|
981
|
+
# Only trigger return to bhead if this is the root behavior
|
|
982
|
+
# (not if it's a child of parallel/another composite)
|
|
983
|
+
self._reset_behavior_context()
|
|
984
|
+
self._bhcontext["__behavior_return__"] = _bhead
|
|
985
|
+
self._bhcontext["__behavior_failed__"] = True
|
|
986
|
+
return self._transition_to(_bhead, child, log, event_type="behavior", result=False)
|
|
987
|
+
return False # type: ignore[return-value]
|
|
988
|
+
|
|
989
|
+
# Success - store print info
|
|
990
|
+
self._bhcontext["__print_result__"] = "OK"
|
|
991
|
+
self._bhcontext["__print_child__"] = child
|
|
992
|
+
|
|
993
|
+
# Record success in history
|
|
994
|
+
history = self._bhcontext.get("history", [])
|
|
995
|
+
history.append((child, "success"))
|
|
996
|
+
|
|
997
|
+
# Move to next child
|
|
998
|
+
current_index += 1
|
|
999
|
+
seq_context["index"] = current_index
|
|
1000
|
+
|
|
1001
|
+
# Update _current to the child we just executed
|
|
1002
|
+
self._current = child
|
|
1003
|
+
|
|
1004
|
+
# Pop from stack for clean resume state
|
|
1005
|
+
if context_stack and context_stack[-1] == parent_name:
|
|
1006
|
+
context_stack.pop()
|
|
1007
|
+
|
|
1008
|
+
# If this was the last child, handle completion immediately
|
|
1009
|
+
# instead of returning None (which would pause and cause
|
|
1010
|
+
# resume logic to re-execute the behavior tree)
|
|
1011
|
+
if current_index >= len(children):
|
|
1012
|
+
# Last child completed - handle completion directly
|
|
1013
|
+
# Store print info for _transition_print
|
|
1014
|
+
self._bhcontext["__print_result__"] = "OK"
|
|
1015
|
+
self._bhcontext["__print_parent__"] = parent_name
|
|
1016
|
+
|
|
1017
|
+
# Record parent success in history
|
|
1018
|
+
history.append((parent_name, "success"))
|
|
1019
|
+
|
|
1020
|
+
# Send compisited event for composite node success
|
|
1021
|
+
self.pushEvent("compisited", {
|
|
1022
|
+
"inst": self,
|
|
1023
|
+
"name": parent_name,
|
|
1024
|
+
"child": children[-1] if children else None,
|
|
1025
|
+
"result": True
|
|
1026
|
+
})
|
|
1027
|
+
|
|
1028
|
+
# Pop from stack and clean up
|
|
1029
|
+
if context_stack and context_stack[-1] == parent_name:
|
|
1030
|
+
context_stack.pop()
|
|
1031
|
+
if parent_name in self._bhcontext:
|
|
1032
|
+
del self._bhcontext[parent_name]
|
|
1033
|
+
|
|
1034
|
+
# 标记行为树完成,但不触发 behavior_completed(在 _transition_to 中统一触发)
|
|
1035
|
+
if self._bhead is not None:
|
|
1036
|
+
self._bhcontext["__behavior_history__"] = history.copy()
|
|
1037
|
+
self._bhcontext["__behavior_success__"] = True
|
|
1038
|
+
self._bhcontext["__behavior_return__"] = parent_name
|
|
1039
|
+
|
|
1040
|
+
# Update _current to parent_name explicitly via _transition_to
|
|
1041
|
+
# This ensures the next tick will execute from the parent (restarting the sequence)
|
|
1042
|
+
# We call _transition_to with event_type="-" to trigger the changed event
|
|
1043
|
+
# The _suppress_changed flag check in _transition_to will handle duplicate prevention
|
|
1044
|
+
return self._transition_to(parent_name, child, log, event_type="-", result=parent_name)
|
|
1045
|
+
|
|
1046
|
+
# Not the last child - pause execution, resume next tick
|
|
1047
|
+
return None
|
|
1048
|
+
|
|
1049
|
+
# All children completed successfully (reached naturally via loop completion)
|
|
1050
|
+
# This section is now handled above when the last child completes
|
|
1051
|
+
# Store print info for _transition_print
|
|
1052
|
+
self._bhcontext["__print_result__"] = "OK"
|
|
1053
|
+
self._bhcontext["__print_parent__"] = parent_name
|
|
1054
|
+
|
|
1055
|
+
# Record parent success in history
|
|
1056
|
+
history = self._bhcontext.get("history", [])
|
|
1057
|
+
history.append((parent_name, "success"))
|
|
1058
|
+
|
|
1059
|
+
# Send compisited event for composite node success
|
|
1060
|
+
self.pushEvent("compisited", {
|
|
1061
|
+
"inst": self,
|
|
1062
|
+
"name": parent_name,
|
|
1063
|
+
"child": children[-1] if children else None,
|
|
1064
|
+
"result": True
|
|
1065
|
+
})
|
|
1066
|
+
|
|
1067
|
+
# Pop from stack and clean up
|
|
1068
|
+
if context_stack and context_stack[-1] == parent_name:
|
|
1069
|
+
context_stack.pop()
|
|
1070
|
+
if parent_name in self._bhcontext:
|
|
1071
|
+
del self._bhcontext[parent_name]
|
|
1072
|
+
|
|
1073
|
+
# 标记行为树完成,但不触发 behavior_completed(在 _transition_to 中统一触发)
|
|
1074
|
+
# 保存历史记录供后续使用(只要是在行为树中就保存)
|
|
1075
|
+
if self._bhead is not None:
|
|
1076
|
+
self._bhcontext["__behavior_history__"] = history.copy()
|
|
1077
|
+
self._bhcontext["__behavior_success__"] = True
|
|
1078
|
+
# 不在这里重置 bhcontext,让 _transition_to 来处理
|
|
1079
|
+
# 临时标记:告诉 _transition_to 这是行为树内部完成
|
|
1080
|
+
self._bhcontext["__behavior_return__"] = parent_name
|
|
1081
|
+
|
|
1082
|
+
return parent_name
|
|
1083
|
+
|
|
1084
|
+
def _execute_selector(self, parent_name: str, children: list[str], log: bool) -> str | None:
|
|
1085
|
+
"""
|
|
1086
|
+
Execute children until one succeeds.
|
|
1087
|
+
|
|
1088
|
+
RUN (None) handling:
|
|
1089
|
+
- If a child returns None, pause and resume from the same child next tick
|
|
1090
|
+
- If a child returns True/success, the selector succeeds
|
|
1091
|
+
- If a child returns False/failure, try the next child
|
|
1092
|
+
|
|
1093
|
+
Args:
|
|
1094
|
+
parent_name: Name of the parent behavior state
|
|
1095
|
+
children: List of child state names
|
|
1096
|
+
log: Whether to log
|
|
1097
|
+
|
|
1098
|
+
Returns:
|
|
1099
|
+
First successful state, False if all fail, or None if RUN
|
|
1100
|
+
"""
|
|
1101
|
+
max_len = getattr(self, '__max_state_len__', 0)
|
|
1102
|
+
bold_name = f"{Style.BRIGHT}{self._name}{Style.RESET_ALL}" if _COLOR_AVAILABLE else self._name
|
|
1103
|
+
context_stack: list[str] = self._bhcontext.get("stack", [])
|
|
1104
|
+
|
|
1105
|
+
# Check if this is first entry or resuming
|
|
1106
|
+
is_first_entry = parent_name not in self._bhcontext
|
|
1107
|
+
|
|
1108
|
+
# Get or initialize selector context
|
|
1109
|
+
if is_first_entry:
|
|
1110
|
+
sel_context = {"index": 0}
|
|
1111
|
+
self._bhcontext[parent_name] = sel_context
|
|
1112
|
+
# First time: push to stack
|
|
1113
|
+
context_stack.append(parent_name)
|
|
1114
|
+
else:
|
|
1115
|
+
sel_context = self._bhcontext[parent_name]
|
|
1116
|
+
# Resuming: ensure stack contains this parent at the top
|
|
1117
|
+
if parent_name not in context_stack:
|
|
1118
|
+
context_stack.append(parent_name)
|
|
1119
|
+
else:
|
|
1120
|
+
while context_stack and context_stack[-1] != parent_name:
|
|
1121
|
+
context_stack.pop()
|
|
1122
|
+
|
|
1123
|
+
# Calculate indent based on nesting depth
|
|
1124
|
+
# Root's children: 2 spaces, nested children: 4, 6... spaces
|
|
1125
|
+
indent_level = len(context_stack)
|
|
1126
|
+
child_indent = " " * indent_level if indent_level > 0 else " "
|
|
1127
|
+
result_indent = " " * (indent_level - 1) if indent_level > 1 else ""
|
|
1128
|
+
|
|
1129
|
+
current_index = sel_context["index"]
|
|
1130
|
+
|
|
1131
|
+
# Execute from current index
|
|
1132
|
+
while current_index < len(children):
|
|
1133
|
+
child = children[current_index]
|
|
1134
|
+
|
|
1135
|
+
if child not in self.__states__ and child not in self.__behaviors__:
|
|
1136
|
+
if log and self.LOG:
|
|
1137
|
+
padded = child.ljust(max_len)
|
|
1138
|
+
print(f"{child_indent}-> {padded} [{Fore.YELLOW}UNKNOWN - SKIP{Fore.RESET}]")
|
|
1139
|
+
current_index += 1
|
|
1140
|
+
sel_context["index"] = current_index
|
|
1141
|
+
continue
|
|
1142
|
+
|
|
1143
|
+
# Check if child is a behavior composite
|
|
1144
|
+
child_behavior = self.__behaviors__.get(child)
|
|
1145
|
+
is_child_behavior = child_behavior is not None
|
|
1146
|
+
|
|
1147
|
+
# Transition to child state - triggers CHANGED event
|
|
1148
|
+
# Avoid duplicate transition if already at this child (from behavior resume)
|
|
1149
|
+
if self._current != child:
|
|
1150
|
+
self._bhcontext["_transitioning_to"] = child
|
|
1151
|
+
try:
|
|
1152
|
+
self._transition_to(child, parent_name if is_first_entry else self._current, log, event_type="behavior")
|
|
1153
|
+
finally:
|
|
1154
|
+
del self._bhcontext["_transitioning_to"]
|
|
1155
|
+
|
|
1156
|
+
# Check if child is a behavior composite - if so, let _handle_behavior execute it
|
|
1157
|
+
# NOTE: _handle_behavior sends its own enter event, so we don't send one here for behavior children
|
|
1158
|
+
if is_child_behavior:
|
|
1159
|
+
# For behavior children, directly call _handle_behavior to avoid double execution
|
|
1160
|
+
executed_result = self._handle_behavior(child, child_behavior, log)
|
|
1161
|
+
actual_result = executed_result
|
|
1162
|
+
|
|
1163
|
+
# Handle string result (BREAK) from behavior child
|
|
1164
|
+
# Use _transition_to for state transition (triggers changed event)
|
|
1165
|
+
if isinstance(actual_result, str):
|
|
1166
|
+
# Send compisited event
|
|
1167
|
+
self.pushEvent("compisited", {
|
|
1168
|
+
"inst": self,
|
|
1169
|
+
"name": parent_name,
|
|
1170
|
+
"child": child,
|
|
1171
|
+
"result": None
|
|
1172
|
+
})
|
|
1173
|
+
# Set suppress flag to prevent parent behavior nodes from firing CHANGED
|
|
1174
|
+
# Context cleanup will be handled by _transition_to
|
|
1175
|
+
self._suppress_changed = True
|
|
1176
|
+
# Transition to new state - _transition_to handles context reset
|
|
1177
|
+
return self._transition_to(actual_result, child, log, result=actual_result)
|
|
1178
|
+
else:
|
|
1179
|
+
# Only print -> child on first entry, not on resume
|
|
1180
|
+
# Store first entry flag in bhcontext for _transition_print
|
|
1181
|
+
if is_first_entry:
|
|
1182
|
+
self._bhcontext["__is_first_entry__"] = True
|
|
1183
|
+
|
|
1184
|
+
# Execute state to get result
|
|
1185
|
+
actual_result = self._get_state_result(child)
|
|
1186
|
+
|
|
1187
|
+
# String result means state transition (BREAK) - use _transition_to
|
|
1188
|
+
if isinstance(actual_result, str):
|
|
1189
|
+
# Store print info
|
|
1190
|
+
self._bhcontext["__print_result__"] = "BREAK"
|
|
1191
|
+
# Send compisited event
|
|
1192
|
+
self.pushEvent("compisited", {
|
|
1193
|
+
"inst": self,
|
|
1194
|
+
"name": parent_name,
|
|
1195
|
+
"child": child,
|
|
1196
|
+
"result": None
|
|
1197
|
+
})
|
|
1198
|
+
# Set suppress flag to prevent parent behavior nodes from firing CHANGED
|
|
1199
|
+
# Context cleanup will be handled by _transition_to
|
|
1200
|
+
self._suppress_changed = True
|
|
1201
|
+
# Transition to new state - _transition_to handles context reset
|
|
1202
|
+
return self._transition_to(actual_result, child, log, result=actual_result)
|
|
1203
|
+
|
|
1204
|
+
# Execute state with pre-computed result
|
|
1205
|
+
# _execute_state triggers enter event
|
|
1206
|
+
# Set _resuming_behavior to prevent _execute_state from re-resuming the behavior tree
|
|
1207
|
+
self._resuming_behavior = True
|
|
1208
|
+
try:
|
|
1209
|
+
executed_result = self._execute_state(child, log, pre_result=actual_result, send_enter=True)
|
|
1210
|
+
finally:
|
|
1211
|
+
if hasattr(self, '_resuming_behavior'):
|
|
1212
|
+
delattr(self, '_resuming_behavior')
|
|
1213
|
+
|
|
1214
|
+
# Use executed_result for behavior tree children, actual_result for regular states
|
|
1215
|
+
effective_result = executed_result if is_child_behavior else actual_result
|
|
1216
|
+
|
|
1217
|
+
# Note: changed event and onChanged lifecycle hook are triggered in _transition_to
|
|
1218
|
+
# when transitioning to child state. No need to trigger here.
|
|
1219
|
+
|
|
1220
|
+
# RUN (None): pause execution, resume next tick
|
|
1221
|
+
# But if this is the last child, treat it as success and return child name
|
|
1222
|
+
if effective_result is None:
|
|
1223
|
+
self._bhcontext["__print_result__"] = "RUN"
|
|
1224
|
+
# If last child returned None, complete selector with this child
|
|
1225
|
+
if current_index == len(children) - 1:
|
|
1226
|
+
# Last child - complete the selector (treat as success)
|
|
1227
|
+
sel_context["index"] = current_index + 1
|
|
1228
|
+
# Record success in history
|
|
1229
|
+
history = self._bhcontext.get("history", [])
|
|
1230
|
+
history.append((child, "success"))
|
|
1231
|
+
history.append((parent_name, "success"))
|
|
1232
|
+
# Send compisited event for composite node success
|
|
1233
|
+
self.pushEvent("compisited", {
|
|
1234
|
+
"inst": self,
|
|
1235
|
+
"name": parent_name,
|
|
1236
|
+
"child": child,
|
|
1237
|
+
"result": True
|
|
1238
|
+
})
|
|
1239
|
+
# Clean up
|
|
1240
|
+
if context_stack and context_stack[-1] == parent_name:
|
|
1241
|
+
context_stack.pop()
|
|
1242
|
+
if parent_name in self._bhcontext:
|
|
1243
|
+
del self._bhcontext[parent_name]
|
|
1244
|
+
# 标记行为树完成,但不触发 behavior_completed(在 _transition_to 中统一触发)
|
|
1245
|
+
if self._bhead is not None:
|
|
1246
|
+
self._bhcontext["__behavior_history__"] = history.copy()
|
|
1247
|
+
self._bhcontext["__behavior_success__"] = True
|
|
1248
|
+
# 不在这里重置 bhcontext,让 _transition_to 来处理
|
|
1249
|
+
self._bhcontext["__behavior_return__"] = parent_name
|
|
1250
|
+
return child
|
|
1251
|
+
# Not last child - pause and resume next tick
|
|
1252
|
+
sel_context["index"] = current_index
|
|
1253
|
+
return None
|
|
1254
|
+
|
|
1255
|
+
# Success (non-False, non-None, non-str result)
|
|
1256
|
+
if effective_result is not False:
|
|
1257
|
+
self._bhcontext["__print_result__"] = "OK"
|
|
1258
|
+
self._bhcontext["__print_child__"] = child
|
|
1259
|
+
# Record success in history
|
|
1260
|
+
history = self._bhcontext.get("history", [])
|
|
1261
|
+
history.append((child, "success"))
|
|
1262
|
+
history.append((parent_name, "success"))
|
|
1263
|
+
# Send compisited event for composite node success
|
|
1264
|
+
self.pushEvent("compisited", {
|
|
1265
|
+
"inst": self,
|
|
1266
|
+
"name": parent_name,
|
|
1267
|
+
"child": child,
|
|
1268
|
+
"result": True
|
|
1269
|
+
})
|
|
1270
|
+
# Clean up
|
|
1271
|
+
if context_stack and context_stack[-1] == parent_name:
|
|
1272
|
+
context_stack.pop()
|
|
1273
|
+
if parent_name in self._bhcontext:
|
|
1274
|
+
del self._bhcontext[parent_name]
|
|
1275
|
+
# 标记行为树完成,但不触发 behavior_completed(在 _transition_to 中统一触发)
|
|
1276
|
+
if self._bhead is not None:
|
|
1277
|
+
self._bhcontext["__behavior_history__"] = history.copy()
|
|
1278
|
+
self._bhcontext["__behavior_success__"] = True
|
|
1279
|
+
# 不在这里重置 bhcontext,让 _transition_to 来处理
|
|
1280
|
+
self._bhcontext["__behavior_return__"] = parent_name
|
|
1281
|
+
# Set suppress flag to prevent duplicate CHANGED event
|
|
1282
|
+
# The transition to child was already triggered inside the selector
|
|
1283
|
+
self._suppress_changed = True
|
|
1284
|
+
return child
|
|
1285
|
+
|
|
1286
|
+
# Failure: try next child
|
|
1287
|
+
self._bhcontext["__print_result__"] = "FAIL"
|
|
1288
|
+
self._bhcontext["__print_child__"] = child
|
|
1289
|
+
# Record failure in history
|
|
1290
|
+
history = self._bhcontext.get("history", [])
|
|
1291
|
+
history.append((child, "fail"))
|
|
1292
|
+
current_index += 1
|
|
1293
|
+
sel_context["index"] = current_index
|
|
1294
|
+
|
|
1295
|
+
# All failed
|
|
1296
|
+
# Store print info for _transition_print
|
|
1297
|
+
self._bhcontext["__print_result__"] = "ALL FAILED"
|
|
1298
|
+
self._bhcontext["__print_parent__"] = parent_name
|
|
1299
|
+
|
|
1300
|
+
# Record parent failure in history
|
|
1301
|
+
history = self._bhcontext.get("history", [])
|
|
1302
|
+
history.append((parent_name, "fail"))
|
|
1303
|
+
|
|
1304
|
+
# Send compisited event for composite node failure
|
|
1305
|
+
self.pushEvent("compisited", {
|
|
1306
|
+
"inst": self,
|
|
1307
|
+
"name": parent_name,
|
|
1308
|
+
"child": None, # All children failed
|
|
1309
|
+
"result": False
|
|
1310
|
+
})
|
|
1311
|
+
|
|
1312
|
+
# Clean up
|
|
1313
|
+
if context_stack and context_stack[-1] == parent_name:
|
|
1314
|
+
context_stack.pop()
|
|
1315
|
+
if parent_name in self._bhcontext:
|
|
1316
|
+
del self._bhcontext[parent_name]
|
|
1317
|
+
# 标记行为树失败完成,但不触发 behavior_completed(在 _transition_to 中统一触发)
|
|
1318
|
+
if self._bhead is not None:
|
|
1319
|
+
self._bhcontext["__behavior_history__"] = history.copy()
|
|
1320
|
+
self._bhcontext["__behavior_success__"] = False
|
|
1321
|
+
self._bhcontext["__behavior_failed_node__"] = parent_name
|
|
1322
|
+
# 不在这里重置 bhcontext,让 _transition_to 来处理
|
|
1323
|
+
self._bhcontext["__behavior_return__"] = parent_name
|
|
1324
|
+
self._bhcontext["__behavior_failed__"] = True
|
|
1325
|
+
|
|
1326
|
+
return False # type: ignore[return-value]
|
|
1327
|
+
|
|
1328
|
+
def _execute_parallel(self, parent_name: str, children: list[str], log: bool) -> str | None:
|
|
1329
|
+
"""
|
|
1330
|
+
Execute all children (any can succeed).
|
|
1331
|
+
|
|
1332
|
+
RUN (None) handling:
|
|
1333
|
+
- If any child returns None, the parallel returns None (RUN)
|
|
1334
|
+
- Tracks which children have completed across ticks
|
|
1335
|
+
|
|
1336
|
+
Args:
|
|
1337
|
+
parent_name: Name of the parent behavior state
|
|
1338
|
+
children: List of child state names
|
|
1339
|
+
log: Whether to log
|
|
1340
|
+
|
|
1341
|
+
Returns:
|
|
1342
|
+
First successful state, False if all fail, or None if RUN
|
|
1343
|
+
"""
|
|
1344
|
+
max_len = getattr(self, '__max_state_len__', 0)
|
|
1345
|
+
bold_name = f"{Style.BRIGHT}{self._name}{Style.RESET_ALL}" if _COLOR_AVAILABLE else self._name
|
|
1346
|
+
context_stack: list[str] = self._bhcontext.get("stack", [])
|
|
1347
|
+
|
|
1348
|
+
# Check if this is first entry or resuming
|
|
1349
|
+
is_first_entry = parent_name not in self._bhcontext
|
|
1350
|
+
|
|
1351
|
+
# Get or initialize parallel context
|
|
1352
|
+
if is_first_entry:
|
|
1353
|
+
para_context = {"history": [False] * len(children), "completed": 0, "entered": [False] * len(children)}
|
|
1354
|
+
self._bhcontext[parent_name] = para_context
|
|
1355
|
+
context_stack.append(parent_name)
|
|
1356
|
+
else:
|
|
1357
|
+
para_context = self._bhcontext[parent_name]
|
|
1358
|
+
# Resuming: ensure stack contains this parent at the top
|
|
1359
|
+
if parent_name not in context_stack:
|
|
1360
|
+
context_stack.append(parent_name)
|
|
1361
|
+
else:
|
|
1362
|
+
while context_stack and context_stack[-1] != parent_name:
|
|
1363
|
+
context_stack.pop()
|
|
1364
|
+
|
|
1365
|
+
# Calculate indent based on nesting depth
|
|
1366
|
+
# Root's children: 2 spaces, nested children: 4, 6... spaces
|
|
1367
|
+
indent_level = len(context_stack)
|
|
1368
|
+
child_indent = " " * indent_level if indent_level > 0 else " "
|
|
1369
|
+
result_indent = " " * (indent_level - 1) if indent_level > 1 else ""
|
|
1370
|
+
|
|
1371
|
+
completed_history = para_context["history"]
|
|
1372
|
+
any_running = False
|
|
1373
|
+
any_success = False
|
|
1374
|
+
last_result: str | None = None
|
|
1375
|
+
|
|
1376
|
+
for i, child in enumerate(children):
|
|
1377
|
+
# Skip already completed children
|
|
1378
|
+
if completed_history[i]:
|
|
1379
|
+
continue
|
|
1380
|
+
|
|
1381
|
+
if child not in self.__states__ and child not in self.__behaviors__:
|
|
1382
|
+
if log and self.LOG:
|
|
1383
|
+
padded = child.ljust(max_len)
|
|
1384
|
+
print(f"{child_indent}-> {padded} [{Fore.YELLOW}UNKNOWN - SKIP{Fore.RESET}]")
|
|
1385
|
+
completed_history[i] = True
|
|
1386
|
+
para_context["completed"] += 1
|
|
1387
|
+
continue
|
|
1388
|
+
|
|
1389
|
+
# Check if child is a behavior composite (like _execute_sequence does)
|
|
1390
|
+
child_behavior = self.__behaviors__.get(child)
|
|
1391
|
+
is_child_behavior = child_behavior is not None
|
|
1392
|
+
|
|
1393
|
+
# Transition to child state - triggers CHANGED event
|
|
1394
|
+
# For parallel, all children transition from parent (concurrent execution)
|
|
1395
|
+
# Avoid duplicate transition if:
|
|
1396
|
+
# 1. Already at this child (from behavior resume)
|
|
1397
|
+
# 2. Child is a behavior tree that has already started (has its own context)
|
|
1398
|
+
_already_started = is_child_behavior and child in self._bhcontext
|
|
1399
|
+
if self._current != child and not _already_started:
|
|
1400
|
+
self._bhcontext["_transitioning_to"] = child
|
|
1401
|
+
try:
|
|
1402
|
+
# Parallel children are concurrent: each branches from the
|
|
1403
|
+
# parent, so every child transition is parent -> child
|
|
1404
|
+
# (root -> a, root -> b), never child -> sibling (a -> b).
|
|
1405
|
+
# Using _current here would chain children like a sequence
|
|
1406
|
+
# and corrupt both the 'changed' event source and the log.
|
|
1407
|
+
self._transition_to(child, parent_name, log, event_type="behavior")
|
|
1408
|
+
finally:
|
|
1409
|
+
del self._bhcontext["_transitioning_to"]
|
|
1410
|
+
|
|
1411
|
+
# Check if child is a behavior composite - if so, let _handle_behavior execute it
|
|
1412
|
+
# NOTE: _handle_behavior sends its own enter event, so we don't send one here for behavior children
|
|
1413
|
+
if is_child_behavior:
|
|
1414
|
+
# For behavior children, directly call _handle_behavior to avoid double execution
|
|
1415
|
+
executed_result = self._handle_behavior(child, child_behavior, log)
|
|
1416
|
+
actual_result = executed_result
|
|
1417
|
+
else:
|
|
1418
|
+
# Only print -> child on first entry, not on resume
|
|
1419
|
+
# Store first entry flag in bhcontext for _transition_print
|
|
1420
|
+
if is_first_entry:
|
|
1421
|
+
self._bhcontext["__is_first_entry__"] = True
|
|
1422
|
+
|
|
1423
|
+
# Execute state to get result
|
|
1424
|
+
actual_result = self._get_state_result(child)
|
|
1425
|
+
last_result = actual_result
|
|
1426
|
+
|
|
1427
|
+
# Execute state with pre-computed result
|
|
1428
|
+
# _execute_state triggers enter event
|
|
1429
|
+
executed_result = self._execute_state(child, log, pre_result=actual_result, send_enter=True)
|
|
1430
|
+
|
|
1431
|
+
# Use executed_result for behavior tree children, actual_result for regular states
|
|
1432
|
+
effective_result = executed_result if is_child_behavior else actual_result
|
|
1433
|
+
|
|
1434
|
+
# String result handling:
|
|
1435
|
+
# - For regular states: state transition (BREAK) to a new state
|
|
1436
|
+
# - For behavior tree children: if returning self name, it's completion not BREAK
|
|
1437
|
+
if isinstance(effective_result, str):
|
|
1438
|
+
# 检查 BREAK 是否已被内层行为树处理
|
|
1439
|
+
if getattr(self, '_break_handled', False):
|
|
1440
|
+
# BREAK 已处理,触发外层的 compisited,然后返回
|
|
1441
|
+
self._break_handled = False
|
|
1442
|
+
# 先触发外层 parallel 的 compisited
|
|
1443
|
+
|
|
1444
|
+
self.pushEvent("compisited", {
|
|
1445
|
+
"inst": self,
|
|
1446
|
+
"name": parent_name,
|
|
1447
|
+
"child": child,
|
|
1448
|
+
"result": True
|
|
1449
|
+
})
|
|
1450
|
+
# 再触发 behavior_completed
|
|
1451
|
+
has_data = hasattr(self, '_behavior_completion_data')
|
|
1452
|
+
data_truthy = bool(self._behavior_completion_data) if has_data else False
|
|
1453
|
+
if has_data and data_truthy:
|
|
1454
|
+
event_data = {
|
|
1455
|
+
"inst": self,
|
|
1456
|
+
"success": self._behavior_completion_data.get("success"),
|
|
1457
|
+
"history": self._behavior_completion_data.get("history")
|
|
1458
|
+
}
|
|
1459
|
+
failed_node = self._behavior_completion_data.get("failed_node")
|
|
1460
|
+
if failed_node:
|
|
1461
|
+
event_data["failed_node"] = failed_node
|
|
1462
|
+
self.pushEvent("behavior_completed", event_data)
|
|
1463
|
+
self._behavior_completion_data = None
|
|
1464
|
+
return effective_result
|
|
1465
|
+
# Behavior tree child returning its own name = completion, not BREAK
|
|
1466
|
+
if is_child_behavior and effective_result == child:
|
|
1467
|
+
# Treat as successful completion, not state transition
|
|
1468
|
+
completed_history[i] = True
|
|
1469
|
+
para_context["completed"] += 1
|
|
1470
|
+
any_success = True
|
|
1471
|
+
exec_history.append((child, "success"))
|
|
1472
|
+
last_result = effective_result
|
|
1473
|
+
# Continue to next child (don't return, process as success)
|
|
1474
|
+
else:
|
|
1475
|
+
# BREAK: transition to a different state
|
|
1476
|
+
# Store print info
|
|
1477
|
+
self._bhcontext["__print_result__"] = "BREAK"
|
|
1478
|
+
# Send compisited event for composite node interruption
|
|
1479
|
+
self.pushEvent("compisited", {
|
|
1480
|
+
"inst": self,
|
|
1481
|
+
"name": parent_name,
|
|
1482
|
+
"child": child,
|
|
1483
|
+
"result": None
|
|
1484
|
+
})
|
|
1485
|
+
# 标记行为树完成(BREAK 情况),但不触发 behavior_completed(在 _transition_to 中统一触发)
|
|
1486
|
+
# 获取历史记录(对于嵌套行为树,保留完整历史)
|
|
1487
|
+
history = self._bhcontext.get("history", [])
|
|
1488
|
+
history.append((child, "break"))
|
|
1489
|
+
# 设置根行为树的完成标记(如果当前是根行为树或嵌套行为树)
|
|
1490
|
+
if self._bhead is not None:
|
|
1491
|
+
self._bhcontext["__behavior_history__"] = history.copy()
|
|
1492
|
+
self._bhcontext["__behavior_success__"] = True # BREAK 视为成功
|
|
1493
|
+
self._bhcontext["__behavior_return__"] = parent_name
|
|
1494
|
+
# 触发外层 behavior 的 compisited(对于嵌套行为树)
|
|
1495
|
+
|
|
1496
|
+
self.pushEvent("compisited", {
|
|
1497
|
+
"inst": self,
|
|
1498
|
+
"name": parent_name,
|
|
1499
|
+
"child": child,
|
|
1500
|
+
"result": True
|
|
1501
|
+
})
|
|
1502
|
+
# Set suppress flag to prevent parent behavior nodes from firing CHANGED
|
|
1503
|
+
self._suppress_changed = True
|
|
1504
|
+
# Transition to new state - _transition_to handles context reset and behavior_completed
|
|
1505
|
+
return self._transition_to(effective_result, child, log, result=effective_result)
|
|
1506
|
+
|
|
1507
|
+
# Behavior tree child completed (treated as success above)
|
|
1508
|
+
if is_child_behavior and effective_result == child:
|
|
1509
|
+
continue
|
|
1510
|
+
|
|
1511
|
+
# Note: changed event and onChanged lifecycle hook are triggered in _transition_to
|
|
1512
|
+
# when transitioning to child state. No need to trigger here.
|
|
1513
|
+
|
|
1514
|
+
# RUN (None): mark as running
|
|
1515
|
+
if effective_result is None:
|
|
1516
|
+
self._bhcontext["__print_result__"] = "RUN"
|
|
1517
|
+
any_running = True
|
|
1518
|
+
continue
|
|
1519
|
+
|
|
1520
|
+
# Mark as completed
|
|
1521
|
+
completed_history[i] = True
|
|
1522
|
+
para_context["completed"] += 1
|
|
1523
|
+
|
|
1524
|
+
# Track success and record in history
|
|
1525
|
+
exec_history = self._bhcontext.get("history", [])
|
|
1526
|
+
if effective_result is not False:
|
|
1527
|
+
any_success = True
|
|
1528
|
+
exec_history.append((child, "success"))
|
|
1529
|
+
self._bhcontext["__print_result__"] = "OK"
|
|
1530
|
+
self._bhcontext["__print_child__"] = child
|
|
1531
|
+
else:
|
|
1532
|
+
exec_history.append((child, "fail"))
|
|
1533
|
+
self._bhcontext["__print_result__"] = "FAIL"
|
|
1534
|
+
self._bhcontext["__print_child__"] = child
|
|
1535
|
+
|
|
1536
|
+
# If any child is still running, return None
|
|
1537
|
+
if any_running:
|
|
1538
|
+
return None
|
|
1539
|
+
|
|
1540
|
+
# Build completion history from loop results
|
|
1541
|
+
# Store print info for _transition_print
|
|
1542
|
+
if any_success:
|
|
1543
|
+
self._bhcontext["__print_result__"] = "OK"
|
|
1544
|
+
else:
|
|
1545
|
+
self._bhcontext["__print_result__"] = "ALL FAILED"
|
|
1546
|
+
self._bhcontext["__print_parent__"] = parent_name
|
|
1547
|
+
|
|
1548
|
+
# Record parent completion
|
|
1549
|
+
exec_history = self._bhcontext.get("history", [])
|
|
1550
|
+
exec_history.append((parent_name, "success" if any_success else "fail"))
|
|
1551
|
+
|
|
1552
|
+
# Send compisited event for composite node completion
|
|
1553
|
+
# Find the first successful child for parallel success case
|
|
1554
|
+
success_child = None
|
|
1555
|
+
if any_success:
|
|
1556
|
+
for i, child in enumerate(children):
|
|
1557
|
+
if completed_history[i] and i < len(children):
|
|
1558
|
+
# Check if this child was successful
|
|
1559
|
+
_result = self._get_state_result(child)
|
|
1560
|
+
if _result is not False:
|
|
1561
|
+
success_child = child
|
|
1562
|
+
break
|
|
1563
|
+
|
|
1564
|
+
self.pushEvent("compisited", {
|
|
1565
|
+
"inst": self,
|
|
1566
|
+
"name": parent_name,
|
|
1567
|
+
"child": success_child, # None if all failed
|
|
1568
|
+
"result": any_success
|
|
1569
|
+
})
|
|
1570
|
+
|
|
1571
|
+
# Clean up
|
|
1572
|
+
if context_stack and context_stack[-1] == parent_name:
|
|
1573
|
+
context_stack.pop()
|
|
1574
|
+
if parent_name in self._bhcontext:
|
|
1575
|
+
del self._bhcontext[parent_name]
|
|
1576
|
+
# 标记行为树完成,但不触发 behavior_completed(在 _transition_to 中统一触发)
|
|
1577
|
+
if self._bhead is not None:
|
|
1578
|
+
self._bhcontext["__behavior_history__"] = exec_history.copy()
|
|
1579
|
+
self._bhcontext["__behavior_success__"] = any_success
|
|
1580
|
+
# 不在这里重置 bhcontext,让 _transition_to 来处理
|
|
1581
|
+
self._bhcontext["__behavior_return__"] = parent_name
|
|
1582
|
+
if not any_success:
|
|
1583
|
+
self._bhcontext["__behavior_failed__"] = True
|
|
1584
|
+
|
|
1585
|
+
# Restore _current to parent_name so next tick executes from the behavior tree root
|
|
1586
|
+
# not from the last child that was executed
|
|
1587
|
+
self._current = parent_name
|
|
1588
|
+
|
|
1589
|
+
# Return True to indicate success, not the child state name
|
|
1590
|
+
# This prevents _handle_result from treating it as a state transition
|
|
1591
|
+
return True if any_success else False # type: ignore[return-value]
|
|
1592
|
+
|
|
1593
|
+
def _transition_print(self, new_state: str, old_state: str, log: bool, event_type: str) -> None:
|
|
1594
|
+
"""
|
|
1595
|
+
统一处理所有状态切换的输出打印。
|
|
1596
|
+
只能在 _transition_to 中调用。
|
|
1597
|
+
与老版本兼容的输出格式。
|
|
1598
|
+
|
|
1599
|
+
Args:
|
|
1600
|
+
new_state: 新状态名
|
|
1601
|
+
old_state: 旧状态名
|
|
1602
|
+
log: 是否打印
|
|
1603
|
+
event_type: 事件类型
|
|
1604
|
+
"""
|
|
1605
|
+
if not log or not self.LOG:
|
|
1606
|
+
return
|
|
1607
|
+
|
|
1608
|
+
# 检查是否是跳转到行为树外部(BREAK)
|
|
1609
|
+
is_break_to_external = (
|
|
1610
|
+
self._bhead is not None and
|
|
1611
|
+
new_state not in self.__behaviors__ and
|
|
1612
|
+
new_state not in self._get_all_behavior_children()
|
|
1613
|
+
)
|
|
1614
|
+
|
|
1615
|
+
# 检查是否需要抑制日志(BREAK 冒泡时),但如果是跳转到外部则不抑制
|
|
1616
|
+
if getattr(self, "_suppress_changed", False) and not is_break_to_external:
|
|
1617
|
+
return
|
|
1618
|
+
|
|
1619
|
+
# 老版本输出格式: std.info(tag, message, extra)
|
|
1620
|
+
# tag: self._name + ".fbm" (普通状态机) 或 ... (行为树内部)
|
|
1621
|
+
# message: old_state -> new_state
|
|
1622
|
+
# extra: ... (递归状态)
|
|
1623
|
+
|
|
1624
|
+
# 确定 tag: 如果在行为树中且不是跳转到外部,使用 ...
|
|
1625
|
+
if self._bhead is not None and not is_break_to_external:
|
|
1626
|
+
tag = "..."
|
|
1627
|
+
else:
|
|
1628
|
+
tag = f"{self._name}.fbm"
|
|
1629
|
+
|
|
1630
|
+
# message: old_state -> new_state
|
|
1631
|
+
message = f"{old_state} -> {new_state}"
|
|
1632
|
+
|
|
1633
|
+
# extra: 递归标记 (如果新状态是递归状态)
|
|
1634
|
+
extra = ""
|
|
1635
|
+
if new_state in self.__recursive__:
|
|
1636
|
+
extra = "..."
|
|
1637
|
+
|
|
1638
|
+
# 输出
|
|
1639
|
+
if extra:
|
|
1640
|
+
std.info(tag, message, extra)
|
|
1641
|
+
else:
|
|
1642
|
+
std.info(tag, message)
|
|
1643
|
+
|
|
1644
|
+
def _get_all_behavior_children(self) -> set[str]:
|
|
1645
|
+
"""获取所有行为树的所有子节点。"""
|
|
1646
|
+
children = set()
|
|
1647
|
+
for behavior in self.__behaviors__.values():
|
|
1648
|
+
behavior_children = behavior.get("children", [])
|
|
1649
|
+
if isinstance(behavior_children, str):
|
|
1650
|
+
children.add(behavior_children)
|
|
1651
|
+
else:
|
|
1652
|
+
children.update(behavior_children)
|
|
1653
|
+
return children
|
|
1654
|
+
|
|
1655
|
+
def _transition_to(
|
|
1656
|
+
self,
|
|
1657
|
+
new_state: str,
|
|
1658
|
+
old_state: str,
|
|
1659
|
+
log: bool,
|
|
1660
|
+
event_type: str = "-",
|
|
1661
|
+
result: Any = None
|
|
1662
|
+
) -> str | None:
|
|
1663
|
+
"""
|
|
1664
|
+
Transition to a new state. This is the ONLY place where 'changed' event is triggered.
|
|
1665
|
+
|
|
1666
|
+
Args:
|
|
1667
|
+
new_state: State to transition to
|
|
1668
|
+
old_state: Current state name
|
|
1669
|
+
log: Whether to log
|
|
1670
|
+
event_type: Event type for changed event (e.g., "behavior", "-")
|
|
1671
|
+
result: Result value to include in changed event
|
|
1672
|
+
|
|
1673
|
+
Returns:
|
|
1674
|
+
New state name or None
|
|
1675
|
+
"""
|
|
1676
|
+
# 统一打印输出
|
|
1677
|
+
self._transition_print(new_state, old_state, log, event_type)
|
|
1678
|
+
|
|
1679
|
+
if new_state not in self.__states__ and new_state not in self.__behaviors__:
|
|
1680
|
+
return old_state
|
|
1681
|
+
|
|
1682
|
+
self._current = new_state
|
|
1683
|
+
|
|
1684
|
+
# Check if this is a behavior tree internal child transition
|
|
1685
|
+
_bt_target = self._bhcontext.get("_transitioning_to") if self._bhcontext else None
|
|
1686
|
+
_is_bt_child_transition = _bt_target is not None and new_state == _bt_target
|
|
1687
|
+
|
|
1688
|
+
# Check if CHANGED event should be suppressed (for behavior tree BREAK bubbling)
|
|
1689
|
+
_suppress_changed = getattr(self, "_suppress_changed", False)
|
|
1690
|
+
|
|
1691
|
+
# 检查是否是行为树返回标记
|
|
1692
|
+
behavior_return = self._bhcontext.get("__behavior_return__") if self._bhcontext else None
|
|
1693
|
+
|
|
1694
|
+
# 在重置之前保存 behavior_completed 所需的数据
|
|
1695
|
+
# 使用实例变量来跨多次 _transition_to 调用保存数据
|
|
1696
|
+
if self._bhcontext:
|
|
1697
|
+
if self._bhcontext.get("__behavior_history__") is not None:
|
|
1698
|
+
self._behavior_completion_data = {
|
|
1699
|
+
"history": self._bhcontext.get("__behavior_history__"),
|
|
1700
|
+
"success": self._bhcontext.get("__behavior_success__"),
|
|
1701
|
+
"failed_node": self._bhcontext.get("__behavior_failed_node__")
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
# 从实例变量恢复数据(如果存在)
|
|
1705
|
+
behavior_history = None
|
|
1706
|
+
behavior_success = None
|
|
1707
|
+
behavior_failed_node = None
|
|
1708
|
+
if hasattr(self, '_behavior_completion_data') and self._behavior_completion_data:
|
|
1709
|
+
behavior_history = self._behavior_completion_data.get("history")
|
|
1710
|
+
behavior_success = self._behavior_completion_data.get("success")
|
|
1711
|
+
behavior_failed_node = self._behavior_completion_data.get("failed_node")
|
|
1712
|
+
|
|
1713
|
+
# Reset behavior context on state transition
|
|
1714
|
+
# But preserve it if transitioning to a behavior tree or internal child transition
|
|
1715
|
+
# If suppressing CHANGED (BREAK from child), always reset context
|
|
1716
|
+
root = None
|
|
1717
|
+
# 检查是否是行为树完成后的跳转(通过 __behavior_return__ 标记)
|
|
1718
|
+
is_behavior_completion = behavior_return is not None
|
|
1719
|
+
if (self._bhead is not None or is_behavior_completion) and new_state not in self.__behaviors__ and not _is_bt_child_transition:
|
|
1720
|
+
root = self._bhead if self._bhead is not None else behavior_return
|
|
1721
|
+
self._reset_behavior_context()
|
|
1722
|
+
|
|
1723
|
+
# Call lifecycle hook (always called for state tracking)
|
|
1724
|
+
self.onChanged(old_state, new_state, self._inst)
|
|
1725
|
+
|
|
1726
|
+
# Trigger changed event - ONLY place where this event is fired
|
|
1727
|
+
# But suppress for behavior tree internal bubbling transitions
|
|
1728
|
+
if not _suppress_changed or root:
|
|
1729
|
+
# 如果是行为树返回,触发两次 changed 事件:
|
|
1730
|
+
# 1. 子节点 -> bhead(行为树 parent)
|
|
1731
|
+
# 2. bhead -> new_state(实际目标)
|
|
1732
|
+
if behavior_return and behavior_return != old_state:
|
|
1733
|
+
# BREAK 情况:直接触发一次 CHANGED 事件:子节点 -> new_state
|
|
1734
|
+
|
|
1735
|
+
event_data = {
|
|
1736
|
+
"inst": self,
|
|
1737
|
+
"old": old_state,
|
|
1738
|
+
"new": new_state,
|
|
1739
|
+
"type": event_type
|
|
1740
|
+
}
|
|
1741
|
+
if result is not None:
|
|
1742
|
+
event_data["result"] = result
|
|
1743
|
+
self.pushEvent("changed", event_data)
|
|
1744
|
+
|
|
1745
|
+
# 在 compisited 事件之后触发 behavior_completed
|
|
1746
|
+
# 使用之前保存的数据
|
|
1747
|
+
# 只在根行为树阶段触发(通过检查 behavior_return 是否是根行为树)
|
|
1748
|
+
# 根行为树的判断:behavior_return 不在任何子行为树的 children 中
|
|
1749
|
+
is_root_behavior = True
|
|
1750
|
+
if behavior_return:
|
|
1751
|
+
for behavior_name, behavior_info in self.__behaviors__.items():
|
|
1752
|
+
children = behavior_info.get("children", [])
|
|
1753
|
+
if isinstance(children, str):
|
|
1754
|
+
children = [children]
|
|
1755
|
+
if behavior_return in children:
|
|
1756
|
+
# behavior_return 是某个行为树的子节点,不是根
|
|
1757
|
+
is_root_behavior = False
|
|
1758
|
+
break
|
|
1759
|
+
if is_root_behavior and behavior_history is not None and behavior_success is not None:
|
|
1760
|
+
|
|
1761
|
+
event_data = {
|
|
1762
|
+
"inst": self,
|
|
1763
|
+
"success": behavior_success,
|
|
1764
|
+
"history": behavior_history
|
|
1765
|
+
}
|
|
1766
|
+
if behavior_failed_node:
|
|
1767
|
+
event_data["failed_node"] = behavior_failed_node
|
|
1768
|
+
self.pushEvent("behavior_completed", event_data)
|
|
1769
|
+
# 清理实例变量,避免重复触发
|
|
1770
|
+
self._behavior_completion_data = None
|
|
1771
|
+
|
|
1772
|
+
# 清理标记
|
|
1773
|
+
if self._bhcontext and "__behavior_return__" in self._bhcontext:
|
|
1774
|
+
del self._bhcontext["__behavior_return__"]
|
|
1775
|
+
else:
|
|
1776
|
+
# 普通情况
|
|
1777
|
+
event_data = {
|
|
1778
|
+
"inst": self,
|
|
1779
|
+
"old": old_state,
|
|
1780
|
+
"new": new_state,
|
|
1781
|
+
"type": event_type
|
|
1782
|
+
}
|
|
1783
|
+
if result is not None:
|
|
1784
|
+
event_data["result"] = result
|
|
1785
|
+
self.pushEvent("changed", event_data)
|
|
1786
|
+
|
|
1787
|
+
return new_state
|
|
1788
|
+
|
|
1789
|
+
@property
|
|
1790
|
+
def uid(self) -> int:
|
|
1791
|
+
"""Unique instance identifier."""
|
|
1792
|
+
return self._uid
|
|
1793
|
+
|
|
1794
|
+
@property
|
|
1795
|
+
def name(self) -> str:
|
|
1796
|
+
return self._name
|
|
1797
|
+
|
|
1798
|
+
@property
|
|
1799
|
+
def inst(self) -> Any:
|
|
1800
|
+
return self._inst
|
|
1801
|
+
|
|
1802
|
+
@property
|
|
1803
|
+
def current_state(self) -> str | None:
|
|
1804
|
+
"""Get the current state name."""
|
|
1805
|
+
return self._current
|
|
1806
|
+
|
|
1807
|
+
@property
|
|
1808
|
+
def states(self) -> list[str]:
|
|
1809
|
+
"""Get list of all available states."""
|
|
1810
|
+
return self.__states__.copy()
|
|
1811
|
+
|
|
1812
|
+
@property
|
|
1813
|
+
def recursive_states(self) -> set[str]:
|
|
1814
|
+
"""Get set of recursive state names."""
|
|
1815
|
+
return self.__recursive__.copy()
|
|
1816
|
+
|
|
1817
|
+
@property
|
|
1818
|
+
def now(self) -> int:
|
|
1819
|
+
"""Get current scheduler tick (read-only)."""
|
|
1820
|
+
try:
|
|
1821
|
+
return Scheduler().tick
|
|
1822
|
+
except Exception:
|
|
1823
|
+
return 0
|
|
1824
|
+
|
|
1825
|
+
def goto(self, state_name: str) -> bool:
|
|
1826
|
+
"""
|
|
1827
|
+
Manually transition to a state.
|
|
1828
|
+
|
|
1829
|
+
Args:
|
|
1830
|
+
state_name: Target state name
|
|
1831
|
+
|
|
1832
|
+
Returns:
|
|
1833
|
+
True if transition succeeded
|
|
1834
|
+
"""
|
|
1835
|
+
if state_name not in self.__states__ and state_name not in self.__behaviors__:
|
|
1836
|
+
return False
|
|
1837
|
+
|
|
1838
|
+
old_state = self._current
|
|
1839
|
+
self._current = state_name
|
|
1840
|
+
self.onChanged(old_state, state_name, self._inst)
|
|
1841
|
+
return True
|
|
1842
|
+
|
|
1843
|
+
# ========================================================================
|
|
1844
|
+
# Event Methods
|
|
1845
|
+
# ========================================================================
|
|
1846
|
+
|
|
1847
|
+
def listenFor(self, event: str, callback: Any, *sources: str) -> Self:
|
|
1848
|
+
"""
|
|
1849
|
+
Register an event listener.
|
|
1850
|
+
|
|
1851
|
+
Args:
|
|
1852
|
+
event: Event name to listen for
|
|
1853
|
+
callback: Function to call when event occurs
|
|
1854
|
+
*sources: Optional source filters
|
|
1855
|
+
|
|
1856
|
+
Returns:
|
|
1857
|
+
self for method chaining
|
|
1858
|
+
"""
|
|
1859
|
+
self._eda.listenFor(event, callback, *sources)
|
|
1860
|
+
return self
|
|
1861
|
+
|
|
1862
|
+
def cancelListen(self, event: str, callback: Any | None = None) -> bool:
|
|
1863
|
+
"""
|
|
1864
|
+
Cancel an event listener.
|
|
1865
|
+
|
|
1866
|
+
Args:
|
|
1867
|
+
event: Event name to stop listening for
|
|
1868
|
+
callback: Specific callback to remove (None = all)
|
|
1869
|
+
|
|
1870
|
+
Returns:
|
|
1871
|
+
True if listener was removed
|
|
1872
|
+
"""
|
|
1873
|
+
return self._eda.cancelListen(event, callback)
|
|
1874
|
+
|
|
1875
|
+
def pushEvent(self, event: str, data: Any = None, *targets: str) -> int:
|
|
1876
|
+
"""
|
|
1877
|
+
Push an event to listeners.
|
|
1878
|
+
|
|
1879
|
+
Args:
|
|
1880
|
+
event: Event name to push
|
|
1881
|
+
data: Event data
|
|
1882
|
+
*targets: Optional target filters
|
|
1883
|
+
|
|
1884
|
+
Returns:
|
|
1885
|
+
Number of listeners that received the event
|
|
1886
|
+
"""
|
|
1887
|
+
# print("stage: ", event, data)
|
|
1888
|
+
return self._eda.pushEvent(event, data, *targets)
|
|
1889
|
+
|
|
1890
|
+
@classmethod
|
|
1891
|
+
def extractNodes(cls) -> Dict[str, Dict[str, Any]]:
|
|
1892
|
+
"""
|
|
1893
|
+
静态分析当前 Stage 类的所有状态方法,提取:
|
|
1894
|
+
- 文档字符串 (doc)
|
|
1895
|
+
- 函数体源代码 (code)
|
|
1896
|
+
- 所有静态返回的目标状态 (targets)
|
|
1897
|
+
- 装饰器列表 (decorates)
|
|
1898
|
+
|
|
1899
|
+
适用于 Python 3.8+ (已针对 3.10+ 优化)
|
|
1900
|
+
"""
|
|
1901
|
+
result = {}
|
|
1902
|
+
states = cls.__states__
|
|
1903
|
+
|
|
1904
|
+
try:
|
|
1905
|
+
source_file = inspect.getsourcefile(cls)
|
|
1906
|
+
if not source_file:
|
|
1907
|
+
raise FileNotFoundError(f"Source file not found for {cls.__name__}")
|
|
1908
|
+
|
|
1909
|
+
with open(source_file, 'r', encoding='utf-8') as f:
|
|
1910
|
+
source = f.read()
|
|
1911
|
+
except Exception as e:
|
|
1912
|
+
raise RuntimeError(f"Cannot access source code: {e}") from e
|
|
1913
|
+
|
|
1914
|
+
try:
|
|
1915
|
+
module_ast = ast.parse(source)
|
|
1916
|
+
except SyntaxError as e:
|
|
1917
|
+
raise RuntimeError(f"Syntax error in source file: {e}") from e
|
|
1918
|
+
|
|
1919
|
+
# 找到当前类定义
|
|
1920
|
+
target_class_node = None
|
|
1921
|
+
for node in ast.walk(module_ast):
|
|
1922
|
+
if isinstance(node, ast.ClassDef) and node.name == cls.__name__:
|
|
1923
|
+
target_class_node = node
|
|
1924
|
+
break
|
|
1925
|
+
|
|
1926
|
+
if not target_class_node:
|
|
1927
|
+
raise RuntimeError(f"Class {cls.__name__} not found in source file")
|
|
1928
|
+
|
|
1929
|
+
# 遍历类中的所有方法
|
|
1930
|
+
for node in target_class_node.body:
|
|
1931
|
+
if isinstance(node, ast.FunctionDef) and node.name in states:
|
|
1932
|
+
state_name = node.name
|
|
1933
|
+
|
|
1934
|
+
# 【核心修复点 2】从源文本提取代码体
|
|
1935
|
+
code = cls._extract_function_body(source, node)
|
|
1936
|
+
|
|
1937
|
+
# 提取文档字符串
|
|
1938
|
+
doc = ast.get_docstring(node) or ""
|
|
1939
|
+
|
|
1940
|
+
# 提取目标状态
|
|
1941
|
+
targets = cls._extract_static_targets(node, state_name)
|
|
1942
|
+
|
|
1943
|
+
# 提取非 property 装饰器列表
|
|
1944
|
+
decorates = []
|
|
1945
|
+
for decorator in node.decorator_list:
|
|
1946
|
+
# 排除 property 装饰器
|
|
1947
|
+
if isinstance(decorator, ast.Name) and decorator.id == 'property':
|
|
1948
|
+
continue
|
|
1949
|
+
# 排除 property() 调用
|
|
1950
|
+
elif isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Name) and decorator.func.id == 'property':
|
|
1951
|
+
continue
|
|
1952
|
+
|
|
1953
|
+
# 获取装饰器名称 (处理链式调用如 @dec1 @dec2)
|
|
1954
|
+
decor_name = cls._get_decorator_name(decorator)
|
|
1955
|
+
if decor_name:
|
|
1956
|
+
decorates.append(decor_name)
|
|
1957
|
+
|
|
1958
|
+
result[state_name] = {
|
|
1959
|
+
"doc": doc,
|
|
1960
|
+
"code": code,
|
|
1961
|
+
"targets": targets,
|
|
1962
|
+
"decorates": decorates
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
return result
|
|
1966
|
+
|
|
1967
|
+
@staticmethod
|
|
1968
|
+
def _extract_function_body(source: str, func_node: ast.FunctionDef) -> str:
|
|
1969
|
+
"""
|
|
1970
|
+
从源码字符串中提取函数体,去除 'def ...:' 行和公共缩进。
|
|
1971
|
+
适用于 Python 3.8+ (依赖 end_lineno)
|
|
1972
|
+
"""
|
|
1973
|
+
start_line = func_node.lineno
|
|
1974
|
+
|
|
1975
|
+
# Python 3.8+ 保证有 end_lineno
|
|
1976
|
+
if not hasattr(func_node, 'end_lineno'):
|
|
1977
|
+
raise RuntimeError("AST node missing end_lineno. Requires Python 3.8+")
|
|
1978
|
+
|
|
1979
|
+
end_line = func_node.end_lineno
|
|
1980
|
+
|
|
1981
|
+
lines = source.splitlines()
|
|
1982
|
+
|
|
1983
|
+
# 边界检查 (行号从 1 开始,列表索引从 0 开始)
|
|
1984
|
+
if start_line > len(lines) or end_line > len(lines):
|
|
1985
|
+
return ""
|
|
1986
|
+
|
|
1987
|
+
# 截取函数定义行到结束行的所有内容
|
|
1988
|
+
func_lines = lines[start_line - 1: end_line]
|
|
1989
|
+
|
|
1990
|
+
# 去掉第一行 (def ...)
|
|
1991
|
+
body_lines = func_lines[1:]
|
|
1992
|
+
|
|
1993
|
+
if not body_lines:
|
|
1994
|
+
return ""
|
|
1995
|
+
|
|
1996
|
+
# 计算公共缩进
|
|
1997
|
+
first_body_line = body_lines[0]
|
|
1998
|
+
indent = len(first_body_line) - len(first_body_line.lstrip())
|
|
1999
|
+
|
|
2000
|
+
cleaned_lines = []
|
|
2001
|
+
for line in body_lines:
|
|
2002
|
+
if line.strip() == "":
|
|
2003
|
+
cleaned_lines.append("")
|
|
2004
|
+
elif len(line) >= indent:
|
|
2005
|
+
cleaned_lines.append(line[indent:])
|
|
2006
|
+
else:
|
|
2007
|
+
# 防止缩进异常导致截断错误
|
|
2008
|
+
cleaned_lines.append(line)
|
|
2009
|
+
|
|
2010
|
+
return "\n".join(cleaned_lines).rstrip()
|
|
2011
|
+
|
|
2012
|
+
@staticmethod
|
|
2013
|
+
def _extract_static_targets(func_node: ast.FunctionDef, current_state: str) -> List[str]:
|
|
2014
|
+
"""
|
|
2015
|
+
使用 AST 静态分析函数中所有 return 的字符串字面量目标。
|
|
2016
|
+
兼容 Python 3.8+ (ast.Constant)
|
|
2017
|
+
"""
|
|
2018
|
+
targets = set()
|
|
2019
|
+
|
|
2020
|
+
def _visit(node, depth=0):
|
|
2021
|
+
if isinstance(node, ast.Return):
|
|
2022
|
+
value = node.value
|
|
2023
|
+
if value is None:
|
|
2024
|
+
return
|
|
2025
|
+
|
|
2026
|
+
# Python 3.8+ 统一使用 ast.Constant
|
|
2027
|
+
if isinstance(value, ast.Constant):
|
|
2028
|
+
if isinstance(value.value, str):
|
|
2029
|
+
target = value.value
|
|
2030
|
+
if target != current_state:
|
|
2031
|
+
targets.add(target)
|
|
2032
|
+
# 兼容性兜底 (理论上 3.10+ 不需要,但保留以防万一)
|
|
2033
|
+
elif hasattr(ast, 'Str') and isinstance(value, ast.Str):
|
|
2034
|
+
if value.s != current_state:
|
|
2035
|
+
targets.add(value.s)
|
|
2036
|
+
|
|
2037
|
+
elif isinstance(node, ast.If):
|
|
2038
|
+
for child in node.body:
|
|
2039
|
+
_visit(child, depth + 1)
|
|
2040
|
+
for child in node.orelse:
|
|
2041
|
+
_visit(child, depth + 1)
|
|
2042
|
+
|
|
2043
|
+
elif isinstance(node, ast.IfExp): # 三元表达式
|
|
2044
|
+
_visit(node.body, depth + 1)
|
|
2045
|
+
_visit(node.orelse, depth + 1)
|
|
2046
|
+
|
|
2047
|
+
# 递归遍历子节点
|
|
2048
|
+
for child in ast.iter_child_nodes(node):
|
|
2049
|
+
_visit(child, depth + 1)
|
|
2050
|
+
|
|
2051
|
+
# 从函数体开始递归
|
|
2052
|
+
for stmt in func_node.body:
|
|
2053
|
+
_visit(stmt)
|
|
2054
|
+
|
|
2055
|
+
return sorted(list(targets))
|
|
2056
|
+
|
|
2057
|
+
@staticmethod
|
|
2058
|
+
def _get_decorator_name(decorator: ast.expr) -> str | None:
|
|
2059
|
+
"""
|
|
2060
|
+
从 AST 装饰器节点提取名称字符串。
|
|
2061
|
+
支持简单名称、属性访问 (obj.method) 和带参数的调用 (func(arg))。
|
|
2062
|
+
"""
|
|
2063
|
+
if isinstance(decorator, ast.Name):
|
|
2064
|
+
return decorator.id
|
|
2065
|
+
elif isinstance(decorator, ast.Attribute):
|
|
2066
|
+
# 处理 obj.attr 形式
|
|
2067
|
+
return decorator.attr
|
|
2068
|
+
elif isinstance(decorator, ast.Call):
|
|
2069
|
+
# 处理 func(arg) 形式,只取函数名
|
|
2070
|
+
if isinstance(decorator.func, ast.Name):
|
|
2071
|
+
return decorator.func.id
|
|
2072
|
+
elif isinstance(decorator.func, ast.Attribute):
|
|
2073
|
+
return decorator.func.attr
|
|
2074
|
+
return None
|
|
2075
|
+
|
|
2076
|
+
__all__ = [
|
|
2077
|
+
'Stage',
|
|
2078
|
+
'_NamedNode',
|
|
2079
|
+
]
|