Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Py4GWCoreLib/BottingTree.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def Reset(self):
self.headless_heroai.reset()
if self._service_steps:
self._service_trees = [
(step_name, self._coerce_runtime_tree(subtree_or_builder))
(step_name, BehaviorTree.resolve_tree(subtree_or_builder))
for step_name, subtree_or_builder in self._service_steps
]
self._rebuild_root_tree()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Public hero setup facade for modular party loading and UI."""
"""Public hero setup facade for BottingTree party loading and UI."""
from __future__ import annotations

from .hero_setup_model import (
Expand Down Expand Up @@ -41,6 +41,7 @@ def __getattr__(name: str):
globals()[name] = value
return value


__all__ = [
"DEFAULT_HERO_PRIORITY",
"default_hero_config",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Account-scoped hero team setup model for modular party loading."""
"""Account-scoped hero team setup model for BottingTree party loading."""
from __future__ import annotations

import json
Expand All @@ -8,8 +8,6 @@

from Py4GWCoreLib import Player

from .paths import modular_settings_root


HERO_CATALOG = [
(0, "Empty"),
Expand Down Expand Up @@ -92,6 +90,22 @@ def _build_default_hero_priority() -> list[int]:
DEFAULT_HERO_PRIORITY = _build_default_hero_priority()


def _project_root() -> str:
try:
import Py4GW

root = str(Py4GW.Console.get_projects_path() or "").strip()
if root:
return os.path.normpath(root)
except Exception:
pass
return os.path.normpath(os.getcwd())


def _botting_tree_settings_root() -> str:
return os.path.join(_project_root(), "Settings", "BottingTree")


def safe_account_key() -> str:
try:
account_email = str(Player.GetAccountEmail() or "").strip()
Expand All @@ -104,7 +118,7 @@ def safe_account_key() -> str:


def hero_config_path(account_key: str | None = None) -> str:
configs_dir = os.path.join(modular_settings_root(), "configs")
configs_dir = os.path.join(_botting_tree_settings_root(), "configs")
os.makedirs(configs_dir, exist_ok=True)
return os.path.join(configs_dir, f"{account_key or safe_account_key()}.json")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""PyImGui controls for modular hero team setup."""
"""PyImGui controls for BottingTree hero team setup."""
from __future__ import annotations

import PyImGui
Expand Down Expand Up @@ -28,15 +28,6 @@ def _ensure_loaded() -> None:
_ui_loaded = True


def _ui_input_text(label: str, value: str, max_len: int = 256) -> str:
try:
result = PyImGui.input_text(label, str(value), 0)
except Exception:
result = PyImGui.input_text(label, str(value))
text = str(result[1]) if isinstance(result, tuple) and len(result) == 2 else str(result)
return text[: int(max_len)] if int(max_len) > 0 else text


def _begin_child(child_id: str, height: int = 290, border: bool = True) -> bool:
try:
h = int(height)
Expand Down
141 changes: 43 additions & 98 deletions Py4GWCoreLib/botting_tree_src/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,95 +115,18 @@ def SetCurrentTree(
elif reset:
self.Reset()

def _build_sequence_from_children(
self,
children: Sequence[object],
name: str = 'MainRoutine',
) -> BehaviorTree:
return BehaviorTree(
BehaviorTree.SequenceNode(
name=name,
children=[
BehaviorTree.SubtreeNode(
name=f'{name} Step {index + 1}',
subtree_fn=lambda node, child=child: self._coerce_runtime_tree(child),
)
for index, child in enumerate(children)
],
)
@staticmethod
def _mark_current_step(step_name: str) -> BehaviorTree.Node:
def _mark(node: BehaviorTree.Node, step_name: str = step_name) -> BehaviorTree.NodeState:
node.blackboard['current_step_name'] = step_name
return BehaviorTree.NodeState.SUCCESS

return BehaviorTree.ActionNode(
name=f'MarkCurrentStep({step_name})',
action_fn=_mark,
aftercast_ms=0,
)

def _build_named_planner_tree(
self,
steps: Sequence[tuple[str, Callable[[], object] | object]],
start_from: str | None = None,
name: str = 'PlannerSequence',
repeat: bool = False,
) -> BehaviorTree:
if not steps:
return BehaviorTree(BehaviorTree.SequenceNode(name=name, children=[]))

step_names = [step_name for step_name, _ in steps]
start_index = 0
if start_from is not None:
if start_from not in step_names:
raise ValueError(f"Unknown planner step '{start_from}'. Valid values: {', '.join(step_names)}")
start_index = step_names.index(start_from)

def _as_tree(subtree_or_builder: Callable[[], object] | object) -> BehaviorTree:
subtree = subtree_or_builder() if callable(subtree_or_builder) else subtree_or_builder
if isinstance(subtree, BehaviorTree):
return subtree
if isinstance(subtree, BehaviorTree.Node):
return BehaviorTree(subtree)
if hasattr(subtree, 'root') and hasattr(subtree, 'tick') and hasattr(subtree, 'reset'):
return cast(BehaviorTree, subtree)
raise TypeError(f'Planner step returned invalid type {type(subtree).__name__}.')

def _mark_current_step(step_name: str) -> BehaviorTree.Node:
def _mark(node: BehaviorTree.Node, step_name: str = step_name) -> BehaviorTree.NodeState:
node.blackboard['current_step_name'] = step_name
return BehaviorTree.NodeState.SUCCESS

return BehaviorTree.ActionNode(
name=f'MarkCurrentStep({step_name})',
action_fn=_mark,
aftercast_ms=0,
)

children: list[BehaviorTree.Node] = [
BehaviorTree.SequenceNode(
name=f'Step: {step_name}',
children=[
_mark_current_step(step_name),
BehaviorTree.SubtreeNode(
name=step_name,
subtree_fn=lambda node, subtree_or_builder=subtree_or_builder: _as_tree(subtree_or_builder),
),
],
)
for step_name, subtree_or_builder in steps[start_index:]
]
if repeat:
full_pass = self._build_named_planner_tree(steps, start_from=None, name=f'{name} Full Pass', repeat=False)
children.append(
BehaviorTree.RepeaterForeverNode(
full_pass.root,
name='Loop: restart routine',
)
)
return BehaviorTree(BehaviorTree.SequenceNode(name=name, children=children))

def _coerce_runtime_tree(self, subtree_or_builder: Callable[[], object] | object) -> BehaviorTree:
subtree = subtree_or_builder() if callable(subtree_or_builder) else subtree_or_builder
if isinstance(subtree, BehaviorTree):
return subtree
if isinstance(subtree, BehaviorTree.Node):
return BehaviorTree(subtree)
if hasattr(subtree, 'root') and hasattr(subtree, 'tick') and hasattr(subtree, 'reset'):
return cast(BehaviorTree, subtree)
raise TypeError(f'Service step returned invalid type {type(subtree).__name__}.')

def SetMainRoutine(
self,
routine: BehaviorTree | BehaviorTree.Node | Callable[[], object] | Sequence[object] | None,
Expand All @@ -215,7 +138,7 @@ def SetMainRoutine(
if routine is None:
self.SetPlannerTree(None)
elif callable(routine):
self.SetPlannerTree(self._coerce_runtime_tree(routine))
self.SetPlannerTree(BehaviorTree.resolve_tree(routine))
elif isinstance(routine, RuntimeSequence) and not isinstance(routine, (str, bytes)):
routine_items = list(routine)
if routine_items and all(
Expand All @@ -233,9 +156,15 @@ def SetMainRoutine(
self._planner_steps = []
self._planner_sequence_name = name
self.planner_repeat = False
self.SetPlannerTree(self._build_sequence_from_children(routine_items, name=name))
self.SetPlannerTree(
BehaviorTree.build_sequence(
routine_items,
name=name,
step_name_fn=lambda index, _child: f'{name} Step {index + 1}',
)
)
else:
self.SetPlannerTree(self._coerce_runtime_tree(routine))
self.SetPlannerTree(BehaviorTree.resolve_tree(routine))

if auto_start:
self.Start()
Expand All @@ -252,7 +181,15 @@ def SetNamedPlannerSteps(
self._planner_steps = list(steps)
self._planner_sequence_name = name
self.planner_repeat = repeat
self._set_planner_tree(self._build_named_planner_tree(self._planner_steps, start_from=start_from, name=name, repeat=repeat))
self._set_planner_tree(
BehaviorTree.build_named_sequence(
self._planner_steps,
start_from=start_from,
name=name,
before_step=self._mark_current_step,
repeat=repeat,
)
)
self.EnsurePartyWipeRecoveryService(
default_step_name=lambda: (self.GetNamedPlannerStepNames() or [None])[0],
)
Expand Down Expand Up @@ -289,12 +226,15 @@ def RestartFromNamedPlannerStep(
if not self._planner_steps:
return False
sequence_name = name or self._planner_sequence_name
self._set_planner_tree(self._build_named_planner_tree(
self._planner_steps,
start_from=step_name,
name=sequence_name,
repeat=self.planner_repeat,
))
self._set_planner_tree(
BehaviorTree.build_named_sequence(
self._planner_steps,
start_from=step_name,
name=sequence_name,
before_step=self._mark_current_step,
repeat=self.planner_repeat,
)
)
self.Reset()
if auto_start:
self.Start()
Expand All @@ -308,7 +248,12 @@ def BuildAllSequences(
if not self._planner_steps:
return self._build_default_planner_tree()
sequence_name = name or self._planner_sequence_name
return self._build_named_planner_tree(self._planner_steps, start_from=start_from, name=sequence_name)
return BehaviorTree.build_named_sequence(
self._planner_steps,
start_from=start_from,
name=sequence_name,
before_step=self._mark_current_step,
)

def RestartFromSequence(
self,
Expand Down
6 changes: 3 additions & 3 deletions Py4GWCoreLib/botting_tree_src/upkeep.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ def SetServiceTrees(
):
self._service_steps = list(steps)
self._service_trees = [
(step_name, self._coerce_runtime_tree(subtree_or_builder))
(step_name, BehaviorTree.resolve_tree(subtree_or_builder))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s needed for ownership, not because that specific line is dramatically smarter.

self._coerce_runtime_tree(...) was BottingTree-owned coercion logic: “take a tree/node/builder and normalize it into a BehaviorTree.” But after the unification, that semantic belongs to BehaviorTree, not BottingTree.

for step_name, subtree_or_builder in self._service_steps
]
self._rebuild_root_tree()

def AddServiceTree(self, name: str, subtree_or_builder: Callable[[], object] | object):
self._service_steps.append((name, subtree_or_builder))
self._service_trees.append((name, self._coerce_runtime_tree(subtree_or_builder)))
self._service_trees.append((name, BehaviorTree.resolve_tree(subtree_or_builder)))
self._rebuild_root_tree()

def ClearServiceTrees(self):
Expand Down Expand Up @@ -69,7 +69,7 @@ def EnsurePartyWipeRecoveryService(
if service_name != 'PartyWipeRecoveryService':
continue
self._service_steps[index] = (service_name, subtree_or_builder)
self._service_trees[index] = (service_name, self._coerce_runtime_tree(subtree_or_builder))
self._service_trees[index] = (service_name, BehaviorTree.resolve_tree(subtree_or_builder))
self._rebuild_root_tree()
return
self.AddServiceTree('PartyWipeRecoveryService', subtree_or_builder)
22 changes: 2 additions & 20 deletions Py4GWCoreLib/modular/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,27 @@
from __future__ import annotations

from .json_bt_compiler import CANONICAL_STEP_TYPES
from .json_bt_compiler import CompiledRecipeStep
from .json_bt_compiler import JsonBTCompilerContext
from .json_bt_compiler import LEGACY_STEP_TYPES
from .json_bt_compiler import RecipeCompileError
from .json_bt_compiler import RecipeStepMetadata
from .json_bt_compiler import UnknownRecipeStepType
from .json_bt_compiler import audit_recipe_vocabulary
from .json_bt_compiler import compile_file_to_bt
from .json_bt_compiler import compile_recipe_to_bt
from .json_bt_compiler import compile_recipe_step_to_bt
from .json_bt_compiler import compile_recipe_steps_to_bt
from .json_bt_compiler import compile_step_to_bt
from .json_bt_compiler import compile_recipe_steps_to_named_planner_steps
from .json_bt_compiler import get_json_bt_step_types
from .json_bt_compiler import load_recipe
from .json_bt_compiler import recipe_step_metadata
from .runner import BTRecipeRunner
from .runner import RecipeSpec
from .runner import specs_from_campaign_rows

__all__ = [
"CANONICAL_STEP_TYPES",
"CompiledRecipeStep",
"JsonBTCompilerContext",
"LEGACY_STEP_TYPES",
"RecipeCompileError",
"RecipeStepMetadata",
"UnknownRecipeStepType",
"audit_recipe_vocabulary",
"compile_file_to_bt",
"compile_recipe_to_bt",
"compile_recipe_step_to_bt",
"compile_recipe_steps_to_bt",
"compile_step_to_bt",
"compile_recipe_steps_to_named_planner_steps",
"get_json_bt_step_types",
"load_recipe",
"recipe_step_metadata",
"BTRecipeRunner",
"RecipeSpec",
"specs_from_campaign_rows",
]

__version__ = "2.0.0"
Loading