1488 lines
56 KiB
Python
1488 lines
56 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate a Pet Game behavior .cpp file from a JSON behavior tree.
|
|
|
|
The generator intentionally targets the lightweight IPetContext based behavior
|
|
classes used by the demo7 pet game stages. It supports the node subset present
|
|
in example.json:
|
|
|
|
Parallel, Repeat, Sequence, IfElse, Check, Listen, Log, Wait
|
|
|
|
Unknown nodes are emitted as ESP_LOGW statements so the generated C++ remains
|
|
buildable while making missing mappings visible during runtime.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import re
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Iterable
|
|
|
|
|
|
DEFAULT_CLASS_NAME = "JsonBehaviorRunner"
|
|
DEFAULT_HEADER = "json_behavior_runner.h"
|
|
DEFAULT_CPP = "json_behavior_runner.cpp"
|
|
DEFAULT_NAMESPACE = "sweekar::demo7"
|
|
DEFAULT_EVENT_BUS = "../../../../09_core/event_bus/event_bus.h"
|
|
|
|
EVENT_RE = re.compile(r"^EVT_[A-Z0-9_]+$")
|
|
MATH_RANDOM_RE = re.compile(r"^Math\.random\(\)\s*<\s*([0-9]*\.?[0-9]+)\s*$")
|
|
INTERNAL_EVENT_RE = re.compile(r"^EVT_INTERNAL_[A-Z0-9_]+$")
|
|
|
|
# PetInternalEventType 中定义的伪装事件(0x8000+),不在 EventType 枚举中,但共用同一传递通道
|
|
PET_INTERNAL_EVENT_NAMES: list[str] = [
|
|
"EVT_INTERNAL_BRAIN_FULLNESS_WARNING",
|
|
"EVT_INTERNAL_BRAIN_FULLNESS_CRITICAL",
|
|
"EVT_INTERNAL_BRAIN_CLEANLINESS_WARNING",
|
|
"EVT_INTERNAL_BRAIN_CLEANLINESS_CRITICAL",
|
|
"EVT_INTERNAL_BRAIN_HAPPINESS_WARNING",
|
|
"EVT_INTERNAL_BRAIN_HAPPINESS_CRITICAL",
|
|
"EVT_INTERNAL_BRAIN_ENERGY_WARNING",
|
|
"EVT_INTERNAL_BRAIN_ENERGY_CRITICAL",
|
|
]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Node:
|
|
node_id: str
|
|
name: str
|
|
args: dict[str, Any]
|
|
children: list["Node"]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ListenBranch:
|
|
event: str
|
|
children: list[Node]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class IdleBranch:
|
|
base_nodes: list[Node] # played immediately on enter / script-done (base idle display)
|
|
wait_seconds: float # main idle loop wait time
|
|
random_seconds: float # random jitter on wait
|
|
random_nodes: list[Node] # played when random idle timer fires (mood picker etc.)
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Convert a JSON behavior tree into a pet behavior C++ source file.",
|
|
)
|
|
parser.add_argument(
|
|
"input",
|
|
nargs="?",
|
|
default="example.json",
|
|
type=Path,
|
|
help="Input JSON behavior tree. Defaults to ./example.json.",
|
|
)
|
|
parser.add_argument(
|
|
"-o",
|
|
"--output",
|
|
type=Path,
|
|
default=None,
|
|
help=f"Output .cpp path. Defaults to ./{DEFAULT_CPP}.",
|
|
)
|
|
parser.add_argument(
|
|
"--output-header",
|
|
type=Path,
|
|
default=None,
|
|
help=f"Output .h path. Defaults to ./{DEFAULT_HEADER}.",
|
|
)
|
|
parser.add_argument(
|
|
"--event-bus",
|
|
type=Path,
|
|
default=None,
|
|
help=f"Path to event_bus.h. Defaults to {DEFAULT_EVENT_BUS}.",
|
|
)
|
|
parser.add_argument(
|
|
"--header",
|
|
default=DEFAULT_HEADER,
|
|
help=f"Header included by the generated .cpp. Defaults to {DEFAULT_HEADER}.",
|
|
)
|
|
parser.add_argument(
|
|
"--class-name",
|
|
default=DEFAULT_CLASS_NAME,
|
|
help=f"Behavior class name. Defaults to {DEFAULT_CLASS_NAME}.",
|
|
)
|
|
parser.add_argument(
|
|
"--namespace",
|
|
default=DEFAULT_NAMESPACE,
|
|
help=f"C++ namespace, separated with ::. Defaults to {DEFAULT_NAMESPACE}.",
|
|
)
|
|
parser.add_argument(
|
|
"--tag",
|
|
default=None,
|
|
help="ESP log tag. Defaults to the class name.",
|
|
)
|
|
parser.add_argument(
|
|
"--force",
|
|
action="store_true",
|
|
help="Overwrite the output file if it already exists.",
|
|
)
|
|
parser.add_argument(
|
|
"--condition",
|
|
action="append",
|
|
default=[],
|
|
metavar="CHECK=CPP_EXPR",
|
|
help=(
|
|
"Bind a Check value to a C++ expression, for example "
|
|
"--condition 'emotionHigh=ctx->GetEnergy() > 50'. Can be used multiple times."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Print generated C++ to stdout instead of writing a file.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def load_tree(path: Path) -> tuple[dict[str, Any], Node]:
|
|
with path.open("r", encoding="utf-8") as f:
|
|
document = json.load(f)
|
|
|
|
if not isinstance(document, dict):
|
|
raise ValueError("JSON root must be an object")
|
|
|
|
root_value = document.get("root")
|
|
if not isinstance(root_value, dict):
|
|
raise ValueError("JSON must contain an object field named 'root'")
|
|
|
|
return document, parse_node(root_value)
|
|
|
|
|
|
def parse_node(value: dict[str, Any]) -> Node:
|
|
name = value.get("name")
|
|
if not isinstance(name, str) or not name:
|
|
raise ValueError(f"Node is missing a non-empty string name: {value!r}")
|
|
|
|
node_id = str(value.get("$id") or value.get("id") or name)
|
|
|
|
args = value.get("args", {})
|
|
if args is None:
|
|
args = {}
|
|
if not isinstance(args, dict):
|
|
raise ValueError(f"Node {node_id} args must be an object")
|
|
|
|
raw_children = value.get("children", [])
|
|
if raw_children is None:
|
|
raw_children = []
|
|
if not isinstance(raw_children, list):
|
|
raise ValueError(f"Node {node_id} children must be an array")
|
|
|
|
children = []
|
|
for child in raw_children:
|
|
if not isinstance(child, dict):
|
|
raise ValueError(f"Node {node_id} contains a non-object child: {child!r}")
|
|
children.append(parse_node(child))
|
|
|
|
return Node(node_id=node_id, name=name, args=args, children=children)
|
|
|
|
|
|
def split_behavior(root: Node) -> tuple[IdleBranch | None, list[ListenBranch], list[Node]]:
|
|
"""Separate startup/tick work from event listeners.
|
|
|
|
Root Parallel nodes commonly contain one repeatable idle branch and multiple
|
|
Listen branches. Other root shapes are still accepted and treated as idle
|
|
body nodes.
|
|
"""
|
|
|
|
candidates = root.children if root.name == "Parallel" else [root]
|
|
idle_candidates: list[Node] = []
|
|
listen_branches: list[ListenBranch] = []
|
|
extra_nodes: list[Node] = []
|
|
|
|
for child in candidates:
|
|
if child.name == "Listen":
|
|
event = child.args.get("event")
|
|
if not isinstance(event, str) or not EVENT_RE.match(event):
|
|
raise ValueError(f"Listen node {child.node_id} has invalid event: {event!r}")
|
|
listen_branches.append(ListenBranch(event=event, children=child.children))
|
|
elif child.name == "Repeat":
|
|
idle_candidates.extend(child.children)
|
|
else:
|
|
extra_nodes.append(child)
|
|
|
|
if not idle_candidates and extra_nodes:
|
|
idle_candidates = extra_nodes
|
|
extra_nodes = []
|
|
|
|
idle_branch = None
|
|
if idle_candidates:
|
|
flat = flatten_single_sequence(idle_candidates)
|
|
base_nodes, wait_seconds, random_seconds, random_nodes = split_idle_body(flat)
|
|
idle_branch = IdleBranch(
|
|
base_nodes=base_nodes,
|
|
wait_seconds=wait_seconds,
|
|
random_seconds=random_seconds,
|
|
random_nodes=random_nodes,
|
|
)
|
|
|
|
return idle_branch, listen_branches, extra_nodes
|
|
|
|
|
|
def flatten_single_sequence(nodes: list[Node]) -> list[Node]:
|
|
if len(nodes) == 1 and nodes[0].name == "Sequence":
|
|
return nodes[0].children
|
|
return nodes
|
|
|
|
|
|
def find_wait_window(nodes: Iterable[Node]) -> tuple[float, float]:
|
|
for node in walk_nodes(nodes):
|
|
if node.name == "Wait":
|
|
return number_arg(node, "time", 1.0), number_arg(node, "random", 0.0)
|
|
return 1.0, 0.0
|
|
|
|
|
|
def split_idle_body(nodes: list[Node]) -> tuple[list[Node], float, float, list[Node]]:
|
|
"""Split an idle Sequence into (base_nodes, wait_sec, random_sec, random_nodes).
|
|
|
|
Scans top-level nodes for the first Wait. Everything before it becomes the
|
|
base-idle content (played immediately on enter / script-done); everything
|
|
after it becomes the random-idle content (played when the timer fires).
|
|
"""
|
|
base: list[Node] = []
|
|
random_nodes: list[Node] = []
|
|
wait_s = 1.0
|
|
random_s = 0.0
|
|
found_wait = False
|
|
for node in nodes:
|
|
if not found_wait and node.name == "Wait":
|
|
wait_s = number_arg(node, "time", 1.0)
|
|
random_s = number_arg(node, "random", 0.0)
|
|
found_wait = True
|
|
elif not found_wait:
|
|
base.append(node)
|
|
else:
|
|
random_nodes.append(node)
|
|
return base, wait_s, random_s, random_nodes
|
|
|
|
|
|
def walk_nodes(nodes: Iterable[Node]) -> Iterable[Node]:
|
|
for node in nodes:
|
|
yield node
|
|
yield from walk_nodes(node.children)
|
|
|
|
|
|
def number_arg(node: Node, key: str, default: float) -> float:
|
|
value = node.args.get(key, default)
|
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
raise ValueError(f"{node.name} node {node.node_id} arg {key!r} must be a number")
|
|
if not math.isfinite(float(value)) or float(value) < 0:
|
|
raise ValueError(f"{node.name} node {node.node_id} arg {key!r} must be >= 0")
|
|
return float(value)
|
|
|
|
|
|
def generate_cpp(
|
|
document: dict[str, Any],
|
|
root: Node,
|
|
*,
|
|
header: str,
|
|
class_name: str,
|
|
namespace: str,
|
|
tag: str,
|
|
condition_bindings: dict[str, str] | None = None,
|
|
) -> str:
|
|
condition_bindings = condition_bindings or {}
|
|
idle_branch, listen_branches, extra_nodes = split_behavior(root)
|
|
namespace_parts = [part for part in namespace.split("::") if part]
|
|
if not namespace_parts:
|
|
raise ValueError("namespace must not be empty")
|
|
|
|
out: list[str] = []
|
|
emit = out.append
|
|
|
|
emit(f'#include "{escape_cpp_string(header)}"')
|
|
emit('#include "esp_log.h"')
|
|
emit('#include "esp_random.h"')
|
|
emit('#include "esp_timer.h"')
|
|
emit("")
|
|
emit(f'static const char* TAG = "{escape_cpp_string(tag)}";')
|
|
emit("")
|
|
for part in namespace_parts:
|
|
emit(f"namespace {part} {{")
|
|
emit("")
|
|
emit("namespace {")
|
|
emit(f"// Generated from behavior tree: {cpp_comment(str(document.get('name', '<unnamed>')))}")
|
|
emit(f"// Description: {cpp_comment(str(document.get('desc', '')))}")
|
|
emit("constexpr int64_t kUsPerSecond = 1000000;")
|
|
emit("")
|
|
emit("int64_t SecondsToUs(float seconds) {")
|
|
emit(" return static_cast<int64_t>(seconds * kUsPerSecond);")
|
|
emit("}")
|
|
emit("")
|
|
emit("bool RandomLessThan(float threshold) {")
|
|
emit(" if (threshold <= 0.0f) {")
|
|
emit(" return false;")
|
|
emit(" }")
|
|
emit(" if (threshold >= 1.0f) {")
|
|
emit(" return true;")
|
|
emit(" }")
|
|
emit(" return (esp_random() % 10000) < static_cast<uint32_t>(threshold * 10000.0f);")
|
|
emit("}")
|
|
emit("")
|
|
emit("int64_t WaitWindowUs(float base_seconds, float random_seconds) {")
|
|
emit(" int64_t delay_us = SecondsToUs(base_seconds);")
|
|
emit(" if (random_seconds > 0.0f) {")
|
|
emit(" delay_us += esp_random() % static_cast<uint32_t>(SecondsToUs(random_seconds) + 1);")
|
|
emit(" }")
|
|
emit(" return delay_us;")
|
|
emit("}")
|
|
emit("")
|
|
emit("bool EvaluateCondition(IPetContext* ctx, const char* expression) {")
|
|
emit(" (void)ctx;")
|
|
emit(" ESP_LOGW(TAG, \"No C++ binding for Check expression: %s\", expression);")
|
|
emit(" return false;")
|
|
emit("}")
|
|
emit("")
|
|
emit("} // namespace")
|
|
emit("")
|
|
|
|
emit(f"void {class_name}::OnEnter(IPetContext* ctx) {{")
|
|
if idle_branch:
|
|
emit(" PlayBaseIdle(ctx);")
|
|
emit_idle_schedule(idle_branch, emit, " ")
|
|
emit(" phase_ = Phase::WAITING_RANDOM_IDLE;")
|
|
else:
|
|
emit(" (void)ctx;")
|
|
emit(' ESP_LOGI(TAG, "Enter generated behavior with no idle branch");')
|
|
emit(" phase_ = Phase::PLAYING_BASE_IDLE;")
|
|
emit("}")
|
|
emit("")
|
|
|
|
emit(f"void {class_name}::OnExit(IPetContext* ctx) {{")
|
|
emit(" ctx->StopScript();")
|
|
emit(" next_random_idle_us_ = 0;")
|
|
emit(" phase_ = Phase::PLAYING_BASE_IDLE;")
|
|
emit("}")
|
|
emit("")
|
|
|
|
emit(f"bool {class_name}::HandleEvent(IPetContext* ctx, const Event& evt) {{")
|
|
if listen_branches:
|
|
emit(" switch (evt.type) {")
|
|
for branch in listen_branches:
|
|
emit(f" case EventType::{branch.event}:")
|
|
emit(f' ESP_LOGI(TAG, "Handle event: {escape_cpp_string(branch.event)}");')
|
|
emit_node_list(branch.children, emit, " ", condition_bindings)
|
|
emit(" return true;")
|
|
emit("")
|
|
emit(" case EventType::EVT_SCRIPT_DONE:")
|
|
emit(' ESP_LOGI(TAG, "Generated behavior script done");')
|
|
if idle_branch:
|
|
emit(" PlayBaseIdle(ctx);")
|
|
emit_idle_schedule(idle_branch, emit, " ")
|
|
emit(" phase_ = Phase::COOLDOWN_AFTER_RANDOM;")
|
|
emit(" return true;")
|
|
emit("")
|
|
emit(" default:")
|
|
emit(" return false;")
|
|
emit(" }")
|
|
else:
|
|
emit(" (void)ctx;")
|
|
emit(" (void)evt;")
|
|
emit(" return false;")
|
|
emit("}")
|
|
emit("")
|
|
|
|
emit(f"void {class_name}::OnTick(IPetContext* ctx) {{")
|
|
if idle_branch:
|
|
emit(" if (!IsRandomIdleDue()) {")
|
|
emit(" return;")
|
|
emit(" }")
|
|
emit(" PlayRandomIdle(ctx);")
|
|
emit(" phase_ = Phase::WAITING_RANDOM_IDLE;")
|
|
else:
|
|
emit(" (void)ctx;")
|
|
emit("}")
|
|
emit("")
|
|
|
|
emit(f"void {class_name}::PlayBaseIdle(IPetContext* ctx) {{")
|
|
if idle_branch and idle_branch.base_nodes:
|
|
emit_node_list(idle_branch.base_nodes, emit, " ", condition_bindings)
|
|
else:
|
|
emit(" (void)ctx;")
|
|
emit("}")
|
|
emit("")
|
|
|
|
emit(f"void {class_name}::PlayRandomIdle(IPetContext* ctx) {{")
|
|
if idle_branch and idle_branch.random_nodes:
|
|
emit_node_list(idle_branch.random_nodes, emit, " ", condition_bindings)
|
|
else:
|
|
emit(" (void)ctx;")
|
|
emit("}")
|
|
emit("")
|
|
|
|
emit(f"void {class_name}::ScheduleRandomIdle(int64_t delay_us) {{")
|
|
emit(" next_random_idle_us_ = esp_timer_get_time() + delay_us;")
|
|
emit("}")
|
|
emit("")
|
|
|
|
emit(f"bool {class_name}::IsRandomIdleDue() const {{")
|
|
emit(" if (phase_ != Phase::WAITING_RANDOM_IDLE && phase_ != Phase::COOLDOWN_AFTER_RANDOM) {")
|
|
emit(" return false;")
|
|
emit(" }")
|
|
emit(" return next_random_idle_us_ > 0 && esp_timer_get_time() >= next_random_idle_us_;")
|
|
emit("}")
|
|
emit("")
|
|
|
|
for part in reversed(namespace_parts):
|
|
emit(f"}} // namespace {part}")
|
|
emit("")
|
|
|
|
if extra_nodes:
|
|
emit("// The following root nodes were not emitted because they are outside Repeat/Listen:")
|
|
for node in extra_nodes:
|
|
emit(f"// - {cpp_comment(node.name)} ({cpp_comment(node.node_id)})")
|
|
|
|
return "\n".join(out)
|
|
|
|
|
|
def emit_idle_schedule(branch: IdleBranch, emit: Any, indent: str) -> None:
|
|
emit(
|
|
f"{indent}ScheduleRandomIdle(WaitWindowUs({float_literal(branch.wait_seconds)}, "
|
|
f"{float_literal(branch.random_seconds)}));"
|
|
)
|
|
|
|
|
|
def emit_node_list(nodes: list[Node], emit: Any, indent: str, condition_bindings: dict[str, str]) -> None:
|
|
for node in nodes:
|
|
emit_node(node, emit, indent, condition_bindings)
|
|
|
|
|
|
def emit_node(node: Node, emit: Any, indent: str, condition_bindings: dict[str, str]) -> None:
|
|
if node.name in {"Sequence", "Repeat", "Parallel"}:
|
|
emit_node_list(node.children, emit, indent, condition_bindings)
|
|
return
|
|
|
|
if node.name == "Log":
|
|
level = str(node.args.get("level", "info")).lower()
|
|
message = str(node.args.get("message", ""))
|
|
log_macro = {
|
|
"debug": "ESP_LOGD",
|
|
"info": "ESP_LOGI",
|
|
"warn": "ESP_LOGW",
|
|
"warning": "ESP_LOGW",
|
|
"error": "ESP_LOGE",
|
|
}.get(level, "ESP_LOGI")
|
|
emit(f'{indent}{log_macro}(TAG, "{escape_cpp_string(message)}");')
|
|
return
|
|
|
|
if node.name == "Wait":
|
|
time_seconds = number_arg(node, "time", 0.0)
|
|
random_seconds = number_arg(node, "random", 0.0)
|
|
emit(
|
|
f"{indent}ScheduleRandomIdle(WaitWindowUs({float_literal(time_seconds)}, "
|
|
f"{float_literal(random_seconds)}));"
|
|
)
|
|
return
|
|
|
|
if node.name == "IfElse":
|
|
if not node.children:
|
|
emit(f'{indent}ESP_LOGW(TAG, "IfElse node {escape_cpp_string(node.node_id)} has no condition");')
|
|
return
|
|
|
|
condition = condition_expr(node.children[0], condition_bindings)
|
|
then_nodes = flatten_single_sequence(node.children[1:2])
|
|
else_nodes = flatten_single_sequence(node.children[2:3])
|
|
|
|
emit(f"{indent}if ({condition}) {{")
|
|
emit_node_list(then_nodes, emit, indent + " ", condition_bindings)
|
|
if else_nodes:
|
|
emit(f"{indent}}} else {{")
|
|
emit_node_list(else_nodes, emit, indent + " ", condition_bindings)
|
|
emit(f"{indent}}}")
|
|
return
|
|
|
|
if node.name == "Check":
|
|
emit(f"{indent}(void)({condition_expr(node, condition_bindings)});")
|
|
return
|
|
|
|
if node.name == "Listen":
|
|
emit(f'{indent}ESP_LOGW(TAG, "Nested Listen node is ignored: {escape_cpp_string(node.node_id)}");')
|
|
return
|
|
|
|
if node.name == "PlayScript":
|
|
script = node.args.get("script") or node.args.get("name") or node.args.get("path")
|
|
if not isinstance(script, str) or not script:
|
|
emit(f'{indent}ESP_LOGW(TAG, "PlayScript node {escape_cpp_string(node.node_id)} is missing script");')
|
|
else:
|
|
emit(f'{indent}ctx->PlayScript("{escape_cpp_string(script)}");')
|
|
return
|
|
|
|
emit(f'{indent}ESP_LOGW(TAG, "Unsupported behavior node: {escape_cpp_string(node.name)}");')
|
|
|
|
|
|
def condition_expr(node: Node, condition_bindings: dict[str, str]) -> str:
|
|
if node.name != "Check":
|
|
return "false"
|
|
|
|
value = node.args.get("value", "")
|
|
if isinstance(value, bool):
|
|
return "true" if value else "false"
|
|
if not isinstance(value, str):
|
|
return "false"
|
|
|
|
value = value.strip()
|
|
match = MATH_RANDOM_RE.match(value)
|
|
if match:
|
|
return f"RandomLessThan({float_literal(float(match.group(1)))})"
|
|
|
|
if value in {"true", "false"}:
|
|
return value
|
|
|
|
if value in condition_bindings:
|
|
return f"({condition_bindings[value]})"
|
|
|
|
return f'EvaluateCondition(ctx, "{escape_cpp_string(value)}")'
|
|
|
|
|
|
def parse_condition_bindings(items: list[str]) -> dict[str, str]:
|
|
bindings: dict[str, str] = {}
|
|
for item in items:
|
|
if "=" not in item:
|
|
raise ValueError(f"--condition must use CHECK=CPP_EXPR format: {item!r}")
|
|
key, expression = item.split("=", 1)
|
|
key = key.strip()
|
|
expression = expression.strip()
|
|
if not key or not expression:
|
|
raise ValueError(f"--condition must include both CHECK and CPP_EXPR: {item!r}")
|
|
bindings[key] = expression
|
|
return bindings
|
|
|
|
|
|
def float_literal(value: float) -> str:
|
|
text = f"{value:.6g}"
|
|
if "e" not in text.lower() and "." not in text:
|
|
text += ".0"
|
|
return f"{text}f"
|
|
|
|
|
|
def escape_cpp_string(value: str) -> str:
|
|
return (
|
|
value.replace("\\", "\\\\")
|
|
.replace('"', '\\"')
|
|
.replace("\n", "\\n")
|
|
.replace("\r", "\\r")
|
|
.replace("\t", "\\t")
|
|
)
|
|
|
|
|
|
def cpp_comment(value: str) -> str:
|
|
return value.replace("*/", "* /").replace("\n", " ")
|
|
|
|
|
|
def parse_event_types(event_bus_path: Path) -> list[str]:
|
|
text = event_bus_path.read_text(encoding="utf-8")
|
|
match = re.search(r"enum\s+class\s+EventType\s*:[^{]+{(?P<body>.*?)};", text, re.S)
|
|
if not match:
|
|
raise ValueError(f"Cannot find enum class EventType in {event_bus_path}")
|
|
|
|
body = re.sub(r"//.*", "", match.group("body"))
|
|
events: list[str] = []
|
|
for item in body.split(","):
|
|
name = item.split("=", 1)[0].strip()
|
|
if EVENT_RE.match(name):
|
|
events.append(name)
|
|
|
|
if not events:
|
|
raise ValueError(f"No EVT_* entries found in {event_bus_path}")
|
|
return events
|
|
|
|
|
|
def collect_listen_events(node: Node) -> list[str]:
|
|
events: list[str] = []
|
|
if node.name == "Listen":
|
|
event = node.args.get("event")
|
|
if isinstance(event, str):
|
|
events.append(event)
|
|
for child in node.children:
|
|
events.extend(collect_listen_events(child))
|
|
return events
|
|
|
|
|
|
def validate_listen_events(root: Node, event_names: list[str]) -> None:
|
|
valid = set(event_names)
|
|
# PetInternalEventType 的值(0x8000+)不在 EventType 枚举里,但运行时通过 static_cast 共用同一通道,跳过验证
|
|
invalid = sorted({
|
|
event for event in collect_listen_events(root)
|
|
if event not in valid and not INTERNAL_EVENT_RE.match(event)
|
|
})
|
|
if invalid:
|
|
raise ValueError(
|
|
"Listen event(s) not present in EventType: "
|
|
+ ", ".join(invalid)
|
|
+ ". Update example.json or event_bus.h first."
|
|
)
|
|
|
|
|
|
def namespace_open(namespace: str) -> list[str]:
|
|
parts = [part for part in namespace.split("::") if part]
|
|
if not parts:
|
|
raise ValueError("namespace must not be empty")
|
|
return [f"namespace {part} {{" for part in parts]
|
|
|
|
|
|
def namespace_close(namespace: str) -> list[str]:
|
|
parts = [part for part in namespace.split("::") if part]
|
|
return [f"}} // namespace {part}" for part in reversed(parts)]
|
|
|
|
|
|
def generate_runner_header(namespace: str) -> str:
|
|
out: list[str] = [
|
|
"#pragma once",
|
|
"",
|
|
"#include <stddef.h>",
|
|
"#include <stdint.h>",
|
|
"#include <vector>",
|
|
'#include "11_apps/pet_game_app/pet_state_interfaces.h"',
|
|
"",
|
|
"struct cJSON;",
|
|
"",
|
|
]
|
|
out.extend(namespace_open(namespace))
|
|
out.extend([
|
|
"",
|
|
"class JsonBehaviorRunner {",
|
|
"public:",
|
|
" JsonBehaviorRunner(const char* json_data, size_t json_size);",
|
|
" ~JsonBehaviorRunner();",
|
|
"",
|
|
" JsonBehaviorRunner(const JsonBehaviorRunner&) = delete;",
|
|
" JsonBehaviorRunner& operator=(const JsonBehaviorRunner&) = delete;",
|
|
"",
|
|
" void OnEnter(IPetContext* ctx);",
|
|
" void OnExit(IPetContext* ctx);",
|
|
" bool HandleEvent(IPetContext* ctx, const Event& evt);",
|
|
" void OnTick(IPetContext* ctx);",
|
|
"",
|
|
"private:",
|
|
" struct Frame {",
|
|
" const cJSON* children = nullptr;",
|
|
" int index = 0;",
|
|
" bool loop = false;",
|
|
" };",
|
|
"",
|
|
" bool EnsureLoaded();",
|
|
" const cJSON* FindIdleRoot() const;",
|
|
" const cJSON* FindListenBranch(EventType event_type) const;",
|
|
"",
|
|
" void StartIdle(IPetContext* ctx);",
|
|
" void RunIdleUntilBlocked(IPetContext* ctx);",
|
|
" bool ExecuteImmediate(IPetContext* ctx, const cJSON* node);",
|
|
" void ExecuteImmediateChildren(IPetContext* ctx, const cJSON* children);",
|
|
" void ExecuteLeaf(IPetContext* ctx, const cJSON* node);",
|
|
" bool TryPlayScript(IPetContext* ctx, const char* script, const char* source);",
|
|
"",
|
|
" bool EvaluateCondition(IPetContext* ctx, const cJSON* node);",
|
|
" void ScheduleWait(const cJSON* wait_node);",
|
|
" void ApplyMessageAction(IPetContext* ctx, const char* message);",
|
|
"",
|
|
" const char* json_data_ = nullptr;",
|
|
" size_t json_size_ = 0;",
|
|
" cJSON* document_ = nullptr;",
|
|
" const cJSON* root_ = nullptr;",
|
|
" std::vector<Frame> idle_stack_;",
|
|
" int64_t next_wakeup_us_ = 0;",
|
|
" bool script_playing_ = false;",
|
|
"};",
|
|
"",
|
|
])
|
|
out.extend(namespace_close(namespace))
|
|
out.append("")
|
|
return "\n".join(out)
|
|
|
|
|
|
def generate_event_type_name_cases(event_names: list[str]) -> list[str]:
|
|
out: list[str] = []
|
|
for event_name in event_names:
|
|
out.append(f" case EventType::{event_name}:")
|
|
out.append(f' return "{event_name}";')
|
|
out.append(" // ------ PetInternalEventType (\u5927\u8111\u4ee3\u8c22\u4fe1\u53f7, 0x8000+) ------")
|
|
for event_name in PET_INTERNAL_EVENT_NAMES:
|
|
out.append(f" case static_cast<EventType>({event_name}):")
|
|
out.append(f' return "{event_name}";')
|
|
return out
|
|
|
|
|
|
def generate_runner_cpp(*, header: str, namespace: str, event_names: list[str], tag: str) -> str:
|
|
out: list[str] = [
|
|
f'#include "{escape_cpp_string(header)}"',
|
|
"",
|
|
"#include <string.h>",
|
|
"#include <stdio.h>",
|
|
'#include "cJSON.h"',
|
|
'#include "esp_log.h"',
|
|
'#include "esp_random.h"',
|
|
'#include "esp_timer.h"',
|
|
'#include "11_apps/pet_game_app/pet_types.h"',
|
|
"",
|
|
f'static const char* TAG = "{escape_cpp_string(tag)}";',
|
|
"",
|
|
]
|
|
out.extend(namespace_open(namespace))
|
|
out.extend([
|
|
"",
|
|
"namespace {",
|
|
"",
|
|
"constexpr int64_t kUsPerSecond = 1000000;",
|
|
"constexpr int64_t kFallbackDelayUs = 1000000;",
|
|
"",
|
|
"const cJSON* GetObject(const cJSON* object, const char* key) {",
|
|
" const cJSON* item = cJSON_GetObjectItemCaseSensitive(object, key);",
|
|
" return cJSON_IsObject(item) ? item : nullptr;",
|
|
"}",
|
|
"",
|
|
"const cJSON* GetArray(const cJSON* object, const char* key) {",
|
|
" const cJSON* item = cJSON_GetObjectItemCaseSensitive(object, key);",
|
|
" return cJSON_IsArray(item) ? item : nullptr;",
|
|
"}",
|
|
"",
|
|
"const char* GetString(const cJSON* object, const char* key) {",
|
|
" const cJSON* item = cJSON_GetObjectItemCaseSensitive(object, key);",
|
|
" return cJSON_IsString(item) ? item->valuestring : nullptr;",
|
|
"}",
|
|
"",
|
|
"double GetNumber(const cJSON* object, const char* key, double default_value) {",
|
|
" const cJSON* item = cJSON_GetObjectItemCaseSensitive(object, key);",
|
|
" return cJSON_IsNumber(item) ? item->valuedouble : default_value;",
|
|
"}",
|
|
"",
|
|
"const char* NodeName(const cJSON* node) {",
|
|
' const char* name = GetString(node, "name");',
|
|
' return name ? name : "";',
|
|
"}",
|
|
"",
|
|
"bool NameIs(const cJSON* node, const char* expected) {",
|
|
" return strcmp(NodeName(node), expected) == 0;",
|
|
"}",
|
|
"",
|
|
"const cJSON* NodeArgs(const cJSON* node) {",
|
|
' return GetObject(node, "args");',
|
|
"}",
|
|
"",
|
|
"const cJSON* NodeChildren(const cJSON* node) {",
|
|
' return GetArray(node, "children");',
|
|
"}",
|
|
"",
|
|
"bool StringContains(const char* text, const char* needle) {",
|
|
" return text && strstr(text, needle) != nullptr;",
|
|
"}",
|
|
"",
|
|
"bool RandomLessThan(double threshold) {",
|
|
" if (threshold <= 0.0) {",
|
|
" return false;",
|
|
" }",
|
|
" if (threshold >= 1.0) {",
|
|
" return true;",
|
|
" }",
|
|
" return (esp_random() % 10000) < static_cast<uint32_t>(threshold * 10000.0);",
|
|
"}",
|
|
"",
|
|
"bool StringEquals(const char* left, const char* right) {",
|
|
" return left && right && strcmp(left, right) == 0;",
|
|
"}",
|
|
"",
|
|
"int GetPhysioValue(IPetContext* ctx, const char* attr) {",
|
|
' if (StringEquals(attr, "fullness") || StringEquals(attr, "hunger")) {',
|
|
" return ctx->GetFullness();",
|
|
" }",
|
|
' if (StringEquals(attr, "cleanliness")) {',
|
|
" return ctx->GetCleanliness();",
|
|
" }",
|
|
' if (StringEquals(attr, "energy")) {',
|
|
" return ctx->GetEnergy();",
|
|
" }",
|
|
' if (StringEquals(attr, "happiness") || StringEquals(attr, "mood")) {',
|
|
" return ctx->GetHappiness();",
|
|
" }",
|
|
' ESP_LOGW(TAG, "Unknown physiology attr: %s", attr ? attr : "<null>");',
|
|
" return 0;",
|
|
"}",
|
|
"",
|
|
"void AddPhysioValue(IPetContext* ctx, const char* attr, int amount) {",
|
|
' if (StringEquals(attr, "fullness") || StringEquals(attr, "hunger")) {',
|
|
" ctx->AddFullness(amount);",
|
|
" return;",
|
|
" }",
|
|
' if (StringEquals(attr, "cleanliness")) {',
|
|
" ctx->AddCleanliness(amount);",
|
|
" return;",
|
|
" }",
|
|
' if (StringEquals(attr, "energy")) {',
|
|
" ctx->AddEnergy(amount);",
|
|
" return;",
|
|
" }",
|
|
' if (StringEquals(attr, "happiness") || StringEquals(attr, "mood")) {',
|
|
" ctx->AddHappiness(amount);",
|
|
" return;",
|
|
" }",
|
|
' ESP_LOGW(TAG, "Unknown physiology attr: %s", attr ? attr : "<null>");',
|
|
"}",
|
|
"",
|
|
"void SetPhysioValue(IPetContext* ctx, const char* attr, int target) {",
|
|
" int current = GetPhysioValue(ctx, attr);",
|
|
" int delta = target - current;",
|
|
" if (delta != 0) {",
|
|
" AddPhysioValue(ctx, attr, delta);",
|
|
" }",
|
|
"}",
|
|
"",
|
|
"bool CompareInt(int left, const char* op, int right) {",
|
|
' if (!op || StringEquals(op, "<=")) {',
|
|
" return left <= right;",
|
|
" }",
|
|
' if (StringEquals(op, "<")) {',
|
|
" return left < right;",
|
|
" }",
|
|
' if (StringEquals(op, ">=")) {',
|
|
" return left >= right;",
|
|
" }",
|
|
' if (StringEquals(op, ">")) {',
|
|
" return left > right;",
|
|
" }",
|
|
' if (StringEquals(op, "==") || StringEquals(op, "=")) {',
|
|
" return left == right;",
|
|
" }",
|
|
' if (StringEquals(op, "!=")) {',
|
|
" return left != right;",
|
|
" }",
|
|
' ESP_LOGW(TAG, "Unknown comparison op: %s", op);',
|
|
" return false;",
|
|
"}",
|
|
"",
|
|
"bool CompareFloat(float left, const char* op, float right) {",
|
|
' if (!op || StringEquals(op, "<=")) {',
|
|
" return left <= right;",
|
|
" }",
|
|
' if (StringEquals(op, "<")) {',
|
|
" return left < right;",
|
|
" }",
|
|
' if (StringEquals(op, ">=")) {',
|
|
" return left >= right;",
|
|
" }",
|
|
' if (StringEquals(op, ">")) {',
|
|
" return left > right;",
|
|
" }",
|
|
' if (StringEquals(op, "==") || StringEquals(op, "=")) {',
|
|
" return left == right;",
|
|
" }",
|
|
' if (StringEquals(op, "!=")) {',
|
|
" return left != right;",
|
|
" }",
|
|
' ESP_LOGW(TAG, "Unknown comparison op: %s", op);',
|
|
" return false;",
|
|
"}",
|
|
"",
|
|
"const char* EventTypeName(EventType type) {",
|
|
" switch (type) {",
|
|
])
|
|
out.extend(generate_event_type_name_cases(event_names))
|
|
out.extend([
|
|
" default:",
|
|
' return "";',
|
|
" }",
|
|
"}",
|
|
"",
|
|
"} // namespace",
|
|
"",
|
|
"JsonBehaviorRunner::JsonBehaviorRunner(const char* json_data, size_t json_size)",
|
|
" : json_data_(json_data), json_size_(json_size) {}",
|
|
"",
|
|
"JsonBehaviorRunner::~JsonBehaviorRunner() {",
|
|
" if (document_) {",
|
|
" cJSON_Delete(document_);",
|
|
" document_ = nullptr;",
|
|
" }",
|
|
"}",
|
|
"",
|
|
"void JsonBehaviorRunner::OnEnter(IPetContext* ctx) {",
|
|
" if (!EnsureLoaded()) {",
|
|
" return;",
|
|
" }",
|
|
' ESP_LOGI(TAG, "Enter JSON behavior");',
|
|
" script_playing_ = false;",
|
|
" StartIdle(ctx);",
|
|
"}",
|
|
"",
|
|
"void JsonBehaviorRunner::OnExit(IPetContext* ctx) {",
|
|
" ctx->StopScript();",
|
|
" idle_stack_.clear();",
|
|
" next_wakeup_us_ = 0;",
|
|
" script_playing_ = false;",
|
|
"}",
|
|
"",
|
|
"bool JsonBehaviorRunner::HandleEvent(IPetContext* ctx, const Event& evt) {",
|
|
" if (!EnsureLoaded()) {",
|
|
" return false;",
|
|
" }",
|
|
"",
|
|
" if (evt.type == EventType::EVT_SCRIPT_DONE) {",
|
|
" script_playing_ = false;",
|
|
" if (idle_stack_.empty()) {",
|
|
' ESP_LOGD(TAG, "[BT] Script done, idle stack empty — restarting idle");',
|
|
" StartIdle(ctx);",
|
|
" }",
|
|
" return false;",
|
|
" }",
|
|
"",
|
|
" const cJSON* listen = FindListenBranch(evt.type);",
|
|
" if (!listen) {",
|
|
" return false;",
|
|
" }",
|
|
"",
|
|
" const cJSON* listen_args = NodeArgs(listen);",
|
|
' const char* abort_type = listen_args ? GetString(listen_args, "abortType") : nullptr;',
|
|
' const bool is_interrupt = abort_type && strcmp(abort_type, "interrupt") == 0;',
|
|
"",
|
|
" if (is_interrupt) {",
|
|
' ESP_LOGI(TAG, "[BT] Interrupt: aborting idle for %s", EventTypeName(evt.type));',
|
|
" ctx->StopScript();",
|
|
" script_playing_ = false;",
|
|
" idle_stack_.clear();",
|
|
" next_wakeup_us_ = 0;",
|
|
" }",
|
|
"",
|
|
' ESP_LOGI(TAG, "Handle JSON behavior event: %s", EventTypeName(evt.type));',
|
|
" ExecuteImmediateChildren(ctx, NodeChildren(listen));",
|
|
" return true;",
|
|
"}",
|
|
"",
|
|
"void JsonBehaviorRunner::OnTick(IPetContext* ctx) {",
|
|
" if (script_playing_) {",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
" if (next_wakeup_us_ <= 0 || esp_timer_get_time() < next_wakeup_us_) {",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
" next_wakeup_us_ = 0;",
|
|
" RunIdleUntilBlocked(ctx);",
|
|
"}",
|
|
"",
|
|
"bool JsonBehaviorRunner::EnsureLoaded() {",
|
|
" if (document_) {",
|
|
" return root_ != nullptr;",
|
|
" }",
|
|
"",
|
|
" if (!json_data_ || json_size_ == 0) {",
|
|
' ESP_LOGE(TAG, "Behavior JSON data is empty");',
|
|
" return false;",
|
|
" }",
|
|
"",
|
|
" document_ = cJSON_ParseWithLength(json_data_, json_size_);",
|
|
" if (!document_) {",
|
|
' ESP_LOGE(TAG, "Failed to parse behavior JSON");',
|
|
" return false;",
|
|
" }",
|
|
"",
|
|
' root_ = GetObject(document_, "root");',
|
|
" if (!root_) {",
|
|
' ESP_LOGE(TAG, "Behavior JSON has no object field named root");',
|
|
" return false;",
|
|
" }",
|
|
"",
|
|
' const char* behavior_name = GetString(document_, "name");',
|
|
' ESP_LOGI(TAG, "Loaded behavior tree: %s", behavior_name ? behavior_name : "<unnamed>");',
|
|
" return true;",
|
|
"}",
|
|
"",
|
|
"const cJSON* JsonBehaviorRunner::FindIdleRoot() const {",
|
|
" if (!root_) {",
|
|
" return nullptr;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(root_, "Repeat")) {',
|
|
" return root_;",
|
|
" }",
|
|
"",
|
|
" const cJSON* children = NodeChildren(root_);",
|
|
" if (!children) {",
|
|
" return root_;",
|
|
" }",
|
|
"",
|
|
" const int size = cJSON_GetArraySize(children);",
|
|
" for (int i = 0; i < size; ++i) {",
|
|
" const cJSON* child = cJSON_GetArrayItem(children, i);",
|
|
' if (NameIs(child, "Repeat")) {',
|
|
" return child;",
|
|
" }",
|
|
" }",
|
|
"",
|
|
" return root_;",
|
|
"}",
|
|
"",
|
|
"const cJSON* JsonBehaviorRunner::FindListenBranch(EventType event_type) const {",
|
|
" const char* event_name = EventTypeName(event_type);",
|
|
" if (!event_name[0] || !root_) {",
|
|
" return nullptr;",
|
|
" }",
|
|
"",
|
|
" const cJSON* children = NodeChildren(root_);",
|
|
" if (!children) {",
|
|
" return nullptr;",
|
|
" }",
|
|
"",
|
|
" const int size = cJSON_GetArraySize(children);",
|
|
" for (int i = 0; i < size; ++i) {",
|
|
" const cJSON* child = cJSON_GetArrayItem(children, i);",
|
|
' if (!NameIs(child, "Listen")) {',
|
|
" continue;",
|
|
" }",
|
|
"",
|
|
" const cJSON* args = NodeArgs(child);",
|
|
' const char* event = args ? GetString(args, "event") : nullptr;',
|
|
" if (event && strcmp(event, event_name) == 0) {",
|
|
" return child;",
|
|
" }",
|
|
" }",
|
|
"",
|
|
" return nullptr;",
|
|
"}",
|
|
"",
|
|
"void JsonBehaviorRunner::StartIdle(IPetContext* ctx) {",
|
|
" idle_stack_.clear();",
|
|
" next_wakeup_us_ = 0;",
|
|
"",
|
|
" const cJSON* idle_root = FindIdleRoot();",
|
|
" const cJSON* children = NodeChildren(idle_root);",
|
|
" if (!children) {",
|
|
" ExecuteLeaf(ctx, idle_root);",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
" Frame frame;",
|
|
" frame.children = children;",
|
|
" frame.index = 0;",
|
|
' frame.loop = NameIs(idle_root, "Repeat");',
|
|
" idle_stack_.push_back(frame);",
|
|
" RunIdleUntilBlocked(ctx);",
|
|
"}",
|
|
"",
|
|
"void JsonBehaviorRunner::RunIdleUntilBlocked(IPetContext* ctx) {",
|
|
" while (!idle_stack_.empty()) {",
|
|
" Frame& frame = idle_stack_.back();",
|
|
" const int size = cJSON_GetArraySize(frame.children);",
|
|
"",
|
|
" if (frame.index >= size) {",
|
|
" if (frame.loop) {",
|
|
" frame.index = 0;",
|
|
" } else {",
|
|
" idle_stack_.pop_back();",
|
|
" }",
|
|
" continue;",
|
|
" }",
|
|
"",
|
|
" const cJSON* node = cJSON_GetArrayItem(frame.children, frame.index);",
|
|
' if (NameIs(node, "Wait")) {',
|
|
" ScheduleWait(node);",
|
|
" frame.index++;",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "Sequence") || NameIs(node, "Parallel") || NameIs(node, "Repeat")) {',
|
|
" frame.index++;",
|
|
" const cJSON* children = NodeChildren(node);",
|
|
" if (children) {",
|
|
" Frame child_frame;",
|
|
" child_frame.children = children;",
|
|
" child_frame.index = 0;",
|
|
' child_frame.loop = NameIs(node, "Repeat");',
|
|
" idle_stack_.push_back(child_frame);",
|
|
" }",
|
|
" continue;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "IfElse")) {',
|
|
" frame.index++;",
|
|
" const cJSON* children = NodeChildren(node);",
|
|
" const cJSON* condition = children ? cJSON_GetArrayItem(children, 0) : nullptr;",
|
|
" const int branch_index = EvaluateCondition(ctx, condition) ? 1 : 2;",
|
|
" const cJSON* branch = children ? cJSON_GetArrayItem(children, branch_index) : nullptr;",
|
|
" const cJSON* branch_children = NodeChildren(branch);",
|
|
" if (branch_children) {",
|
|
" Frame branch_frame;",
|
|
" branch_frame.children = branch_children;",
|
|
" branch_frame.index = 0;",
|
|
" branch_frame.loop = false;",
|
|
" idle_stack_.push_back(branch_frame);",
|
|
" } else if (branch) {",
|
|
" ExecuteLeaf(ctx, branch);",
|
|
" }",
|
|
" continue;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "Selector")) {',
|
|
" frame.index++;",
|
|
" const cJSON* sel_children = NodeChildren(node);",
|
|
" if (!sel_children) { continue; }",
|
|
" const int sel_size = cJSON_GetArraySize(sel_children);",
|
|
" for (int s = 0; s < sel_size; ++s) {",
|
|
" const cJSON* candidate = cJSON_GetArrayItem(sel_children, s);",
|
|
' if (NameIs(candidate, "Sequence")) {',
|
|
" const cJSON* seq_ch = NodeChildren(candidate);",
|
|
" if (!seq_ch) continue;",
|
|
" const cJSON* first = cJSON_GetArrayItem(seq_ch, 0);",
|
|
" if (!EvaluateCondition(ctx, first)) continue;",
|
|
" Frame sel_frame;",
|
|
" sel_frame.children = seq_ch;",
|
|
" sel_frame.index = 1;",
|
|
" sel_frame.loop = false;",
|
|
" idle_stack_.push_back(sel_frame);",
|
|
" break;",
|
|
" } else {",
|
|
" ExecuteLeaf(ctx, candidate);",
|
|
" break;",
|
|
" }",
|
|
" }",
|
|
" if (script_playing_) return;",
|
|
" continue;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "PlayScript")) {',
|
|
' if (script_playing_) {',
|
|
' ESP_LOGD(TAG, "[BT] Pause idle tree: waiting current script to finish before next PlayScript");',
|
|
" return;",
|
|
" }",
|
|
"",
|
|
" ExecuteLeaf(ctx, node);",
|
|
" if (script_playing_) {",
|
|
" frame.index++;",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
" frame.index++;",
|
|
" continue;",
|
|
" }",
|
|
"",
|
|
" ExecuteLeaf(ctx, node);",
|
|
" frame.index++;",
|
|
" }",
|
|
"}",
|
|
"",
|
|
"bool JsonBehaviorRunner::ExecuteImmediate(IPetContext* ctx, const cJSON* node) {",
|
|
" if (!node) return false;",
|
|
"",
|
|
' if (NameIs(node, "Sequence")) {',
|
|
" const cJSON* children = NodeChildren(node);",
|
|
" if (!children) return true;",
|
|
" const int size = cJSON_GetArraySize(children);",
|
|
" for (int i = 0; i < size; ++i) {",
|
|
" if (!ExecuteImmediate(ctx, cJSON_GetArrayItem(children, i))) return false;",
|
|
" }",
|
|
" return true;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "Selector")) {',
|
|
" const cJSON* children = NodeChildren(node);",
|
|
" if (!children) return false;",
|
|
" const int size = cJSON_GetArraySize(children);",
|
|
" for (int i = 0; i < size; ++i) {",
|
|
" if (ExecuteImmediate(ctx, cJSON_GetArrayItem(children, i))) return true;",
|
|
" }",
|
|
" return false;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "Parallel") || NameIs(node, "Repeat") || NameIs(node, "Listen")) {',
|
|
" const cJSON* children = NodeChildren(node);",
|
|
" if (!children) return true;",
|
|
" const int size = cJSON_GetArraySize(children);",
|
|
" for (int i = 0; i < size; ++i) {",
|
|
" ExecuteImmediate(ctx, cJSON_GetArrayItem(children, i));",
|
|
" }",
|
|
" return true;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "IfElse")) {',
|
|
" const cJSON* children = NodeChildren(node);",
|
|
" const cJSON* condition = children ? cJSON_GetArrayItem(children, 0) : nullptr;",
|
|
" const int branch_index = EvaluateCondition(ctx, condition) ? 1 : 2;",
|
|
" return ExecuteImmediate(ctx, children ? cJSON_GetArrayItem(children, branch_index) : nullptr);",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "CheckPhysio") || NameIs(node, "CheckUtility") || NameIs(node, "Check")) {',
|
|
" return EvaluateCondition(ctx, node);",
|
|
" }",
|
|
"",
|
|
" ExecuteLeaf(ctx, node);",
|
|
" return true;",
|
|
"}",
|
|
"",
|
|
"void JsonBehaviorRunner::ExecuteImmediateChildren(IPetContext* ctx, const cJSON* children) {",
|
|
" if (!children) return;",
|
|
" const int size = cJSON_GetArraySize(children);",
|
|
" for (int i = 0; i < size; ++i) {",
|
|
" ExecuteImmediate(ctx, cJSON_GetArrayItem(children, i));",
|
|
" }",
|
|
"}",
|
|
"",
|
|
"void JsonBehaviorRunner::ExecuteLeaf(IPetContext* ctx, const cJSON* node) {",
|
|
" if (!node) {",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
' ESP_LOGD(TAG, "[BT] Execute: %s", NodeName(node));',
|
|
"",
|
|
" const cJSON* args = NodeArgs(node);",
|
|
' if (NameIs(node, "Log")) {',
|
|
' const char* message = args ? GetString(args, "message") : nullptr;',
|
|
' ESP_LOGI(TAG, "%s", message ? message : "");',
|
|
" ApplyMessageAction(ctx, message);",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "Wait")) {',
|
|
" ScheduleWait(node);",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "Check")) {',
|
|
" (void)EvaluateCondition(ctx, node);",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "AddPhysio")) {',
|
|
' const char* attr = args ? GetString(args, "attr") : nullptr;',
|
|
' const int amount = static_cast<int>(args ? GetNumber(args, "amount", 0.0) : 0.0);',
|
|
' ESP_LOGD(TAG, "[BT] AddPhysio %s %+d (before=%d)", attr ? attr : "?", amount, GetPhysioValue(ctx, attr));',
|
|
" AddPhysioValue(ctx, attr, amount);",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "SetPhysio")) {',
|
|
' const char* attr = args ? GetString(args, "attr") : nullptr;',
|
|
' const int value = static_cast<int>(args ? GetNumber(args, "value", 0.0) : 0.0);',
|
|
' ESP_LOGD(TAG, "[BT] SetPhysio %s => %d (before=%d)", attr ? attr : "?", value, GetPhysioValue(ctx, attr));',
|
|
" SetPhysioValue(ctx, attr, value);",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "PlayScript")) {',
|
|
' const char* script = args ? GetString(args, "script") : nullptr;',
|
|
" if (!script && args) {",
|
|
' script = GetString(args, "path");',
|
|
" }",
|
|
" if (!script && args) {",
|
|
' script = GetString(args, "name");',
|
|
" }",
|
|
" if (script) {",
|
|
' (void)TryPlayScript(ctx, script, "PlayScript");',
|
|
" } else {",
|
|
' ESP_LOGW(TAG, "PlayScript node is missing script/path/name");',
|
|
" }",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "RequestModeSwitch")) {',
|
|
' const char* mode = args ? GetString(args, "mode") : nullptr;',
|
|
' if (mode && strcmp(mode, "MODE_MENU") == 0) {',
|
|
" ctx->RequestModeSwitch(PetModeType::MODE_MENU);",
|
|
' } else if (mode && strcmp(mode, "MODE_IDLE") == 0) {',
|
|
" ctx->RequestModeSwitch(PetModeType::MODE_IDLE);",
|
|
' } else if (mode && strcmp(mode, "MODE_SLEEPING") == 0) {',
|
|
" ctx->RequestModeSwitch(PetModeType::MODE_SLEEPING);",
|
|
" } else {",
|
|
' ESP_LOGW(TAG, "Unsupported RequestModeSwitch mode: %s", mode ? mode : "<null>");',
|
|
" }",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
' ESP_LOGW(TAG, "Unsupported JSON behavior node: %s", NodeName(node));',
|
|
"}",
|
|
"",
|
|
"bool JsonBehaviorRunner::EvaluateCondition(IPetContext* ctx, const cJSON* node) {",
|
|
" if (!node) {",
|
|
" return false;",
|
|
" }",
|
|
"",
|
|
" const cJSON* args = NodeArgs(node);",
|
|
"",
|
|
' if (NameIs(node, "CheckPhysio")) {',
|
|
' const char* attr = args ? GetString(args, "attr") : nullptr;',
|
|
' const char* op = args ? GetString(args, "op") : nullptr;',
|
|
' const int target = static_cast<int>(args ? GetNumber(args, "value", 0.0) : 0.0);',
|
|
" bool result = CompareInt(GetPhysioValue(ctx, attr), op, target);",
|
|
' ESP_LOGD(TAG, "[BT] CheckPhysio %s %s %d => current=%d result=%s",',
|
|
' attr ? attr : "?", op ? op : "<=", target,',
|
|
' GetPhysioValue(ctx, attr), result ? "true" : "false");',
|
|
" return result;",
|
|
" }",
|
|
"",
|
|
' if (NameIs(node, "CheckUtility")) {',
|
|
' const char* attr = args ? GetString(args, "attr") : nullptr;',
|
|
' const char* op = args ? GetString(args, "op") : nullptr;',
|
|
' const float thr = static_cast<float>(args ? GetNumber(args, "value", 0.0) : 0.0);',
|
|
" if (!attr) {",
|
|
' ESP_LOGW(TAG, "[BT] CheckUtility missing attr");',
|
|
" return false;",
|
|
" }",
|
|
" const BehaviorUtility u = ctx->GetBehaviorUtilities();",
|
|
" float actual = 0.0f;",
|
|
' if (StringEquals(attr, "hungry")) { actual = u.hungry; }',
|
|
' else if (StringEquals(attr, "sleepy")) { actual = u.sleepy; }',
|
|
' else if (StringEquals(attr, "dirty")) { actual = u.dirty; }',
|
|
' else if (StringEquals(attr, "laugh")) { actual = u.laugh; }',
|
|
' else if (StringEquals(attr, "curious")) { actual = u.curious; }',
|
|
' else if (StringEquals(attr, "sad")) { actual = u.sad; }',
|
|
" else {",
|
|
' ESP_LOGW(TAG, "[BT] CheckUtility unknown attr: %s", attr);',
|
|
" return false;",
|
|
" }",
|
|
" bool result = CompareFloat(actual, op, thr);",
|
|
' ESP_LOGD(TAG, "[BT] CheckUtility %s %s %.2f => actual=%.2f result=%s",',
|
|
' attr, op ? op : ">=", thr, actual, result ? "true" : "false");',
|
|
" return result;",
|
|
" }",
|
|
"",
|
|
' if (!NameIs(node, "Check")) {',
|
|
" return false;",
|
|
" }",
|
|
"",
|
|
' const char* value = args ? GetString(args, "value") : nullptr;',
|
|
" if (!value) {",
|
|
" return false;",
|
|
" }",
|
|
"",
|
|
" double threshold = 0.0;",
|
|
' if (sscanf(value, "Math.random() < %lf", &threshold) == 1) {',
|
|
" return RandomLessThan(threshold);",
|
|
" }",
|
|
"",
|
|
' if (strcmp(value, "energyHigh") == 0 || strcmp(value, "emotionHigh") == 0) {',
|
|
" return ctx->GetEnergy() >= 50;",
|
|
" }",
|
|
"",
|
|
' if (strcmp(value, "hungerHigh") == 0) {',
|
|
" return ctx->GetHunger() >= 50;",
|
|
" }",
|
|
"",
|
|
' if (strcmp(value, "true") == 0) {',
|
|
" return true;",
|
|
" }",
|
|
"",
|
|
' ESP_LOGW(TAG, "Unknown Check expression: %s", value);',
|
|
" return false;",
|
|
"}",
|
|
"",
|
|
"void JsonBehaviorRunner::ScheduleWait(const cJSON* wait_node) {",
|
|
" const cJSON* args = NodeArgs(wait_node);",
|
|
' const double base_seconds = args ? GetNumber(args, "time", 0.0) : 0.0;',
|
|
' const double random_seconds = args ? GetNumber(args, "random", 0.0) : 0.0;',
|
|
" int64_t delay_us = static_cast<int64_t>(base_seconds * kUsPerSecond);",
|
|
"",
|
|
" if (random_seconds > 0.0) {",
|
|
" const uint32_t random_us = static_cast<uint32_t>(random_seconds * kUsPerSecond);",
|
|
" delay_us += esp_random() % (random_us + 1);",
|
|
" }",
|
|
"",
|
|
" if (delay_us <= 0) {",
|
|
" delay_us = kFallbackDelayUs;",
|
|
" }",
|
|
"",
|
|
" next_wakeup_us_ = esp_timer_get_time() + delay_us;",
|
|
"}",
|
|
"",
|
|
"void JsonBehaviorRunner::ApplyMessageAction(IPetContext* ctx, const char* message) {",
|
|
" if (!message) {",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
' if (StringContains(message, "OPEN_MENU")) {',
|
|
" ctx->RequestModeSwitch(PetModeType::MODE_MENU);",
|
|
" return;",
|
|
" }",
|
|
"",
|
|
' if (StringContains(message, "PET_IDLE")) {',
|
|
' (void)TryPlayScript(ctx, "baby/idle/look_around", "Log/PET_IDLE");',
|
|
' } else if (StringContains(message, "PET_CURIOUS")) {',
|
|
' (void)TryPlayScript(ctx, "baby/interaction/curious", "Log/PET_CURIOUS");',
|
|
' } else if (StringContains(message, "PET_SHY")) {',
|
|
' (void)TryPlayScript(ctx, "baby/interaction/shy", "Log/PET_SHY");',
|
|
' } else if (StringContains(message, "PET_HURT")) {',
|
|
' (void)TryPlayScript(ctx, "baby/interaction/hurt", "Log/PET_HURT");',
|
|
' } else if (StringContains(message, "PET_HAPPY")) {',
|
|
' (void)TryPlayScript(ctx, "baby/interaction/happy", "Log/PET_HAPPY");',
|
|
' } else if (StringContains(message, "PET_CLINGY")) {',
|
|
' (void)TryPlayScript(ctx, "baby/interaction/clingy", "Log/PET_CLINGY");',
|
|
" }",
|
|
"}",
|
|
"",
|
|
"bool JsonBehaviorRunner::TryPlayScript(IPetContext* ctx, const char* script, const char* source) {",
|
|
" if (!script || !script[0]) {",
|
|
" return false;",
|
|
" }",
|
|
"",
|
|
" if (script_playing_) {",
|
|
' ESP_LOGD(TAG, "[BT] Skip PlayScript while another script is running: source=%s script=%s",',
|
|
' source ? source : "<unknown>", script);',
|
|
" return false;",
|
|
" }",
|
|
"",
|
|
" script_playing_ = true;",
|
|
' ESP_LOGD(TAG, "[BT] PlayScript => %s (source=%s)", script, source ? source : "<unknown>");',
|
|
" ctx->PlayScript(script);",
|
|
" return true;",
|
|
"}",
|
|
"",
|
|
])
|
|
out.extend(namespace_close(namespace))
|
|
out.append("")
|
|
return "\n".join(out)
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
script_dir = Path(__file__).resolve().parent
|
|
input_path = args.input.resolve() if args.input.is_absolute() else (script_dir / args.input).resolve()
|
|
output_cpp_path = args.output.resolve() if args.output else input_path.with_name(DEFAULT_CPP)
|
|
output_header_path = (
|
|
args.output_header.resolve() if args.output_header else input_path.with_name(DEFAULT_HEADER)
|
|
)
|
|
event_bus_path = (
|
|
args.event_bus.resolve()
|
|
if args.event_bus
|
|
else (script_dir / DEFAULT_EVENT_BUS).resolve()
|
|
)
|
|
|
|
try:
|
|
document, root = load_tree(input_path)
|
|
event_names = parse_event_types(event_bus_path)
|
|
validate_listen_events(root, event_names)
|
|
cpp = generate_runner_cpp(
|
|
header=args.header,
|
|
namespace=args.namespace,
|
|
tag=args.tag or args.class_name,
|
|
event_names=event_names,
|
|
)
|
|
header = generate_runner_header(args.namespace)
|
|
|
|
if args.dry_run:
|
|
print(cpp)
|
|
print(header, file=sys.stderr)
|
|
return 0
|
|
|
|
for path in (output_cpp_path, output_header_path):
|
|
if path.exists() and not args.force:
|
|
print(f"Refusing to overwrite existing file: {path}", file=sys.stderr)
|
|
print("Use --force to overwrite it.", file=sys.stderr)
|
|
return 2
|
|
|
|
output_cpp_path.parent.mkdir(parents=True, exist_ok=True)
|
|
output_header_path.parent.mkdir(parents=True, exist_ok=True)
|
|
output_cpp_path.write_text(cpp, encoding="utf-8")
|
|
output_header_path.write_text(header, encoding="utf-8")
|
|
print(f"Generated {output_cpp_path}")
|
|
print(f"Generated {output_header_path}")
|
|
return 0
|
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
print(f"error: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|