#!/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', '')))}") 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(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(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(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.*?)};", 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 ", "#include ", "#include ", '#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 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({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 ", "#include ", '#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(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 : "");', " 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 : "");', "}", "", "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 : "");', " 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(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(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 : "");', " }", " 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(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(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(base_seconds * kUsPerSecond);", "", " if (random_seconds > 0.0) {", " const uint32_t random_us = static_cast(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 : "", script);', " return false;", " }", "", " script_playing_ = true;", ' ESP_LOGD(TAG, "[BT] PlayScript => %s (source=%s)", script, source ? source : "");', " 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())