#!/usr/bin/env bash # Run one exported client process and seal its report after exit. # This script owns exactly one child PID; it never pkills other clients. set -euo pipefail cd "$(dirname "$0")/.." app_path="${MT_PLAYABLE_APP:-$PWD/build/export-native-trees-20260911/mtgodot-poc.app}" mode="playable" config_path="" output_dir="${MT_PLAYABLE_OUTPUT:-$PWD/build/playable/run-$(date +%Y%m%d-%H%M%S)-$$}" timeout_seconds="${MT_PLAYABLE_TIMEOUT_SECONDS:-180}" allow_gameplay=0 usage() { cat <<'EOF' 用法: script/run_client_gate.sh --app APP --config CONFIG [选项] 选项: --output DIR 本次运行目录,必须不存在或为空 --mode MODE 默认 playable --timeout-seconds N 墙钟超时,默认 180 --allow-gameplay 允许专用测试账号发送移动/战斗/拾取请求 EOF } while [ "$#" -gt 0 ]; do case "$1" in --app) app_path="$2"; shift 2 ;; --config) config_path="$2"; shift 2 ;; --output) output_dir="$2"; shift 2 ;; --mode) mode="$2"; shift 2 ;; --timeout-seconds) timeout_seconds="$2"; shift 2 ;; --allow-gameplay) allow_gameplay=1; shift ;; --help) usage; exit 0 ;; *) echo "未知选项: $1" >&2; usage >&2; exit 2 ;; esac done if [ -z "$config_path" ] || [ ! -f "$config_path" ]; then echo "配置不存在;不会启动客户端:$config_path" >&2 exit 2 fi if [ ! -x "$app_path/Contents/MacOS/mtgodot-poc" ]; then echo "找不到可执行包:$app_path" >&2 exit 2 fi if ! [[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]]; then echo "timeout-seconds 必须是正整数" >&2 exit 2 fi if [ -e "$output_dir" ] && [ "$(find "$output_dir" -mindepth 1 -maxdepth 1 -print -quit)" ]; then echo "输出目录已有内容,拒绝复用历史报告:$output_dir" >&2 exit 2 fi mkdir -p "$output_dir" log="$output_dir/client.log" client_report="$output_dir/client-report.json" final_report="$output_dir/report.json" events="$output_dir/events.jsonl" if ! MT_PLAYABLE_VALIDATE_CONFIG="$config_path" godot --headless --path project --script playable_config_gate.gd >"$output_dir/config.log" 2>&1; then echo "配置校验失败;不会启动客户端:$output_dir/config.log" >&2 exit 2 fi char_slot="$(node -e 'const fs=require("node:fs"); const p=process.argv[1]; const d=JSON.parse(fs.readFileSync(p,"utf8")); process.stdout.write(String(d.character_slot));' "$config_path")" sha256_of() { if [ -f "$1" ]; then shasum -a 256 "$1" | awk '{print $1}'; else echo ""; fi } engine="$app_path/Contents/MacOS/mtgodot-poc" pck="$app_path/Contents/Resources/mtgodot-poc.pck" dylib="$(find "$app_path/Contents/Frameworks" -maxdepth 1 -type f -name '*.dylib' -print -quit)" export MT_TEST_MODE="$mode" export MT_PLAYABLE_CONFIG="$config_path" export MT_TEST_REPORT="$client_report" export MT_TEST_EVENTS="$events" export MT_TEST_OUTPUT="$output_dir" export MT_PROTOCOL="classic" export MT_AUTOLOGIN=1 export MT_CHAR_SLOT="$char_slot" export MT_PLAYABLE_RUN_ID="$(basename "$output_dir")" export MT_BUILD_ENGINE_SHA256="$(sha256_of "$engine")" export MT_BUILD_EXTENSION_SHA256="$(sha256_of "$dylib")" export MT_BUILD_PCK_SHA256="$(sha256_of "$pck")" file_description="$(file "$engine")" case "$file_description" in *arm64*) export MT_BUILD_ARCH="arm64" ;; *x86_64*) export MT_BUILD_ARCH="x86_64" ;; *) export MT_BUILD_ARCH="unknown" ;; esac if [ "$allow_gameplay" -eq 1 ]; then export MT_PLAYABLE_ALLOW_GAMEPLAY=1; else export MT_PLAYABLE_ALLOW_GAMEPLAY=0; fi start_epoch="$(date +%s)" set +e env -u MT_ASSETS "$engine" >"$log" 2>&1 & child_pid=$! timed_out=0 while kill -0 "$child_pid" 2>/dev/null; do now="$(date +%s)" if [ $((now - start_epoch)) -ge "$timeout_seconds" ]; then timed_out=1 kill -TERM "$child_pid" 2>/dev/null || true for _ in 1 2 3 4 5 6 7 8 9 10; do kill -0 "$child_pid" 2>/dev/null || break sleep 1 done kill -KILL "$child_pid" 2>/dev/null || true break fi sleep 1 done wait "$child_pid" app_status=$? set -e if [ "$timed_out" -eq 1 ]; then app_status=124; fi set +e node script/validate_playable_report.mjs --report "$client_report" --output "$final_report" \ --log "$log" --process-code "$app_status" --require-pass gate_status=$? set -e if [ "$app_status" -ne 0 ] || [ "$gate_status" -ne 0 ]; then echo "PLAYABLE GATE: FAIL exit=$app_status report=$final_report log=$log" >&2 exit 1 fi echo "PLAYABLE GATE: PASS report=$final_report log=$log"