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.
Files changed (79) hide show
  1. chartflow/__init__.py +28 -0
  2. chartflow/_verify_importexport.py +169 -0
  3. chartflow/edit/__init__.py +119 -0
  4. chartflow/edit/code_editor.py +1007 -0
  5. chartflow/edit/code_folder.py +523 -0
  6. chartflow/edit/completer.py +2652 -0
  7. chartflow/edit/context_analyzer.py +521 -0
  8. chartflow/edit/demo.py +469 -0
  9. chartflow/edit/line_number_area.py +548 -0
  10. chartflow/edit/minimap.py +581 -0
  11. chartflow/edit/python_editor.py +509 -0
  12. chartflow/edit/syntax_highlighter.py +291 -0
  13. chartflow/edit/test_editor.py +190 -0
  14. chartflow/flow/__init__.py +31 -0
  15. chartflow/flow/_demo.py +98 -0
  16. chartflow/flow/chart.py +1101 -0
  17. chartflow/flow/editor.py +375 -0
  18. chartflow/flow/flow.py +6 -0
  19. chartflow/flow/qchart.py +1250 -0
  20. chartflow/flow/test/__init__.py +0 -0
  21. chartflow/flow/test/test_chart.py +537 -0
  22. chartflow/flow/test_decorator_sync.py +102 -0
  23. chartflow/fsm/__init__.py +84 -0
  24. chartflow/fsm/decorators.py +395 -0
  25. chartflow/fsm/demo.py +381 -0
  26. chartflow/fsm/demo_pyqt6.py +881 -0
  27. chartflow/fsm/demo_tree.py +46 -0
  28. chartflow/fsm/logic.py +447 -0
  29. chartflow/fsm/meta.py +569 -0
  30. chartflow/fsm/scheduler.py +427 -0
  31. chartflow/fsm/stage.py +2079 -0
  32. chartflow/fsm/stdio.py +66 -0
  33. chartflow/fsm/test/__init__.py +7 -0
  34. chartflow/fsm/test/test_decorators.py +210 -0
  35. chartflow/fsm/test/test_events.py +202 -0
  36. chartflow/fsm/test/test_integration.py +596 -0
  37. chartflow/fsm/test/test_logic.py +371 -0
  38. chartflow/fsm/test/test_meta.py +192 -0
  39. chartflow/fsm/test/test_runtime_param.py +336 -0
  40. chartflow/fsm/test/test_scheduler.py +280 -0
  41. chartflow/fsm/test/test_stage.py +314 -0
  42. chartflow/fsm/test_behavior_events.py +263 -0
  43. chartflow/fsm/test_nested_parallel.py +80 -0
  44. chartflow/fsm/test_stage_spec.py +448 -0
  45. chartflow/fsm/utils.py +34 -0
  46. chartflow/node/__init__.py +66 -0
  47. chartflow/node/_node_base.py +727 -0
  48. chartflow/node/_node_interaction.py +488 -0
  49. chartflow/node/_node_interface.py +426 -0
  50. chartflow/node/_node_markers.py +648 -0
  51. chartflow/node/_node_ports.py +536 -0
  52. chartflow/node/_port_interface.py +111 -0
  53. chartflow/node/arrow_item.py +287 -0
  54. chartflow/node/color_utils.py +113 -0
  55. chartflow/node/connection_item.py +939 -0
  56. chartflow/node/demo.py +258 -0
  57. chartflow/node/demo_arrow_drag.py +46 -0
  58. chartflow/node/demo_decorator.py +63 -0
  59. chartflow/node/demo_ports.py +69 -0
  60. chartflow/node/enums.py +35 -0
  61. chartflow/node/example.py +84 -0
  62. chartflow/node/layout_utils.py +307 -0
  63. chartflow/node/main_window.py +196 -0
  64. chartflow/node/menu_bar.py +240 -0
  65. chartflow/node/node_canvas.py +1730 -0
  66. chartflow/node/node_item.py +754 -0
  67. chartflow/node/popmenu.py +472 -0
  68. chartflow/node/port_item.py +591 -0
  69. chartflow/node/qnode_editor.py +73 -0
  70. chartflow/node/selection_rect.py +53 -0
  71. chartflow/node/style_panel.py +615 -0
  72. chartflow/node/styles.py +63 -0
  73. chartflow/node/test_qnode.py +2336 -0
  74. chartflow/showcase.py +528 -0
  75. chartflow/theme.py +508 -0
  76. chartflow-0.1.dist-info/METADATA +23 -0
  77. chartflow-0.1.dist-info/RECORD +79 -0
  78. chartflow-0.1.dist-info/WHEEL +5 -0
  79. 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
+ ]