using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
// System.Diagnostics をまるごと using すると Debug が UnityEngine.Debug と
// ぶつかるので、要る型だけ別名で入れる
using Process = System.Diagnostics.Process;

namespace ServiceSim.EditorTools
{
    /// <summary>
    /// 段階B（依頼 v7。2026-10-23）：18回まとめて回して、台帳に1行ずつ残す。
    ///
    /// 回すもの:
    ///   サイトの条件 6 つ（③ fam_p1・⑦ fam_p5 × ロボット 0・1・2 台）× 種 0・1・2 ＝ 18 回。
    ///   決まりは固定版のまま。条件ごとに .asset へ入れた値は、ExperimentRunner と SiteValues が
    ///   回し終わりに必ず元へ戻す（途中で止めても戻す）。
    ///
    /// 回す順番:
    ///   ③ロボット2台 種0 → ⑦ロボット2台 種0 → 残りを条件・種の順。
    ///   **最初の2回で失敗したら、残りは回さずに止める**。
    ///
    /// 回しっぱなしにするための作り:
    ///   ・1回ごとに Play を出入りする（前の回の人・ロボット・NavMesh を持ち越さない）。
    ///     Play をまたいで消えないよう、進みは EditorPrefs に置く
    ///   ・1回の実時間の上限は ExperimentRunner.maxWallSecondsPerRun（40 分）。
    ///     超えた回は書き出して stopped で台帳に残し、次へ進む
    ///   ・回しているあいだは caffeinate で Mac のスリープを止める（終わったら戻す）
    ///   ・途中で止めても、もう一度押せば **台帳を見て** まだ回していない回から続ける
    ///
    /// シミュレーションの動きは1行も変えない。入れたのは「まとめて回す仕組み」と
    /// 「台帳への書き出し」だけ。
    /// </summary>
    public static class ServiceSimStageB
    {
        // 店の形の場所は ServiceSimSetup に1つだけ置く
        // （画面なしの実行ファイルも同じ所を見る。依頼 v12 の 2 章・2026-10-26）
        const string P1Path = ServiceSimSetup.P1LayoutPath;
        const string P5Path = ServiceSimSetup.P5LayoutPath;

        /// <summary>1回の実時間の上限（秒）。依頼 v7 の 3</summary>
        // 1回の実時間の上限。値は ExperimentRunner に1つだけ置く
        // （画面なしの実行ファイルと同じ値を使うため。依頼 v12 の 2 章・2026-10-26）
        const float MaxWallSecondsPerRun = ExperimentRunner.DefaultMaxWallSecondsPerRun;

        /// <summary>最初の何回かが失敗したら、残りを回さずに止める</summary>
        const int FailFastRuns = 2;

        const string LogFileName = "run_all_log.txt";

        /// <summary>
        /// まとめて回すときの名札（依頼 v11 の 2・3 B 3）。
        /// 出力フォルダの名前に入り、台帳の「もう回したか」の照合もこの名札ごとに分ける。
        ///   stageB      … 段階Bの18回（基準データを作る回）
        ///   split_check … プロジェクトを分けたあとの「同じ結果が出るか」の確かめ
        /// 名札を分けてあるので、split_check を回しても段階Bの18回が「済」にならない
        /// </summary>
        const string LabelStageB = "stageB";
        const string LabelSplitCheck = "split_check";

        /// <summary>
        /// 同じ種でずれる元の切り分け（依頼 v11 の 5 章）。
        /// 名札を分けてあるので、段階Bの18回とも split_check とも混ざらない
        /// </summary>
        const string LabelDetCheck = "det_check";

        /// <summary>
        /// 道探しの枠（pathfindingIterationsPerFrame）が結果を動かすかの確かめ（2026-10-26）。
        /// 段階Bの18回（⑦）で待ちが短く出たので、枠 100 と 5000 で同じ回を比べる
        /// </summary>
        const string LabelPathCheck = "path_check";

        /// <summary>
        /// ロボットどうしのすれ違い（依頼 v13 の 依頼1）。
        /// すれ違いの走り方を入れた回と切った回で名札を分けるので、
        /// 出力フォルダ名も台帳の照合も混ざらない（段階Bの18回も「済」にならない）
        /// </summary>
        const string LabelPassOn = "pass_on";
        const string LabelPassOff = "pass_off";

        /// <summary>
        /// このプロジェクトの Unity Version Control のリポジトリ名に入っているはずの字。
        /// cm status が返したリポジトリ名にこれが無ければ、
        /// 1つ上のワークスペース（Robotics_UX_New）の版を見てしまっているので回さない（依頼 v11 の 2）
        /// </summary>
        const string ExpectRepoName = "ServiceSim";

        /// <summary>EditorPrefs にためるログの区切り（1文字。行の中に出ない字を使う）</summary>
        const char LogSep = '\u001f';

        // ---- Play をまたいで持つ進み（EditorPrefs）----
        const string KeyActive = "ServiceSim.StageB.active";
        const string KeyIndex = "ServiceSim.StageB.index";
        const string KeyConds = "ServiceSim.StageB.conds";     // "2:0;5:0;..."
        const string KeyFolders = "ServiceSim.StageB.folders";  // ";" 区切り
        const string KeyUnityCs = "ServiceSim.StageB.unityCs";
        const string KeyPending = "ServiceSim.StageB.pending";
        const string KeyLog = "ServiceSim.StageB.log";
        const string KeyRunStart = "ServiceSim.StageB.runStart";
        const string KeyCaffeinate = "ServiceSim.StageB.caffeinate";
        const string KeyBatchStart = "ServiceSim.StageB.batchStart";
        const string KeyLabel = "ServiceSim.StageB.label";

        // ---------------- メニュー ----------------

        [MenuItem("ServiceSim/Stage B/Run All（18回）", false, 200)]
        public static void RunAll()
        {
            List<int> condIdx; List<int> seeds;
            BuildOrder(out condIdx, out seeds);
            StartBatch("段階B", LabelStageB, condIdx, seeds, true);
        }

        /// <summary>
        /// プロジェクトを分けたあとの「同じ結果が出るか」の確かめ（依頼 v11 の 3 B 3）。
        ///
        /// 段階Bの18回のうち ③ ロボット1台・種1 だけを 2 回回す。
        /// 同じ種で同じ数字が出るか（2回が同じか）と、
        /// 基準データ v2 の同じ回（20261025_stageB_cs144_fam_p1_r1_s1）と同じかを見る。
        ///
        /// 名札が split_check なので、出力フォルダは
        ///   {日付}_split_check_cs{版}_fam_p1_r1_s1（2回めは末尾に _2）
        /// になり、段階Bの18回とは混ざらない。
        /// 台帳の「もう回したか」も名札ごとなので、
        /// この確かめのあとで Run All を押しても18回とも「まだ」のまま。
        /// 回し方・記録・条件の中身は段階Bとまったく同じ
        /// </summary>
        [MenuItem("ServiceSim/Stage B/移せたかの確かめ（③1台・種1 を2回）", false, 203)]
        public static void RunSplitCheck()
        {
            // 条件 1 = ③（fam_p1）・ロボット 1 台（unity_conditions の並び）。種は 1
            List<int> condIdx = new List<int> { 1, 1 };
            List<int> seeds = new List<int> { 1, 1 };
            StartBatch("移せたかの確かめ", LabelSplitCheck, condIdx, seeds, false);
        }

        /// <summary>
        /// 同じ種でずれる元の切り分け（依頼 v11 の 5 章）。③1台・種1 を2回回す。
        ///
        /// どの試しになるかは ExperimentPlan の det_check の切り替えで決まります
        /// （試し1 = ExperimentPlan.pathIterationsPerFrame、試し2 = detCheckSyncPeoplePaths、
        ///   試し4 = detCheckNoAvoidance）。ここは回すだけで、切り替えは触りません。
        ///
        /// 出力フォルダは {日付}_det_check_cs{版}_fam_p1_r1_s1（2回めは _2）。
        /// 名札が det_check なので、段階Bの18回とも split_check とも混ざりません。
        /// 入れた切り替えは run_info.json の det_check に残ります
        /// </summary>
        [MenuItem("ServiceSim/Stage B/ずれの切り分け（③1台・種1 を2回）", false, 204)]
        public static void RunDetCheck()
        {
            List<int> condIdx = new List<int> { 1, 1 };
            List<int> seeds = new List<int> { 1, 1 };
            StartBatch("ずれの切り分け", LabelDetCheck, condIdx, seeds, false);
        }

        // ---------------- すれ違い（依頼 v13 の 依頼1）----------------

        /// <summary>
        /// すれ違いの確かめ（依頼 v13 の 依頼1）。③（fam_p1）ロボット2台・スタッフ4・種1 を1回。
        ///
        /// ロボット1台の条件ではすれ違いが起きないので2台で回す。
        /// 押した方（入／切）に合わせて RobotSpec.passByEnabled を書いてから回すので、
        /// 切り替えと名札が食い違わない。
        /// 出力フォルダは {日付}_pass_on_cs{版}_fam_p1_r2_s1（切は pass_off）。
        /// 入れた値は run_info.json の pass_by に残る
        /// </summary>
        [MenuItem("ServiceSim/Stage B/すれ違い（③2台・種1）/入で1回回す", false, 230)]
        public static void RunPassOn() { RunPassCheck(true); }

        [MenuItem("ServiceSim/Stage B/すれ違い（③2台・種1）/切で1回回す", false, 231)]
        public static void RunPassOff() { RunPassCheck(false); }

        [MenuItem("ServiceSim/Stage B/すれ違い（③2台・種1）/いまの設定を見る", false, 232)]
        public static void PassByShow()
        {
            RobotSpec spec = LoadRobotSpec();
            if (spec != null) Debug.Log("[ServiceSim] すれ違い: " + PassByNote(spec));
        }

        static void RunPassCheck(bool on)
        {
            RobotSpec spec = LoadRobotSpec();
            if (spec == null) return;

            if (spec.passByEnabled != on)
            {
                spec.passByEnabled = on;
                EditorUtility.SetDirty(spec);
                AssetDatabase.SaveAssets();
            }
            Debug.Log("[ServiceSim] すれ違いの走り方を " + (on ? "入" : "切") + " にしました\n  " +
                      PassByNote(spec));

            // 条件 2 = ③（fam_p1）・ロボット 2 台（unity_conditions の並び）。種は 1
            List<int> condIdx = new List<int> { 2 };
            List<int> seeds = new List<int> { 1 };
            StartBatch("すれ違いの確かめ（" + (on ? "入" : "切") + "）",
                       on ? LabelPassOn : LabelPassOff, condIdx, seeds, false);
        }

        /// <summary>いまのすれ違いの設定を、人が読める1行にする</summary>
        static string PassByNote(RobotSpec spec)
        {
            if (spec == null) return "（RobotSpec を読めません）";
            if (!spec.passByEnabled) return "切（cs:3 と同じ動き）";
            return "入 / 見る距離 " + spec.passByDistanceM + " m / すれ違い中の速さ " +
                   spec.passBySpeedMps + " m/秒 / 体どうしの間 " + spec.passBySeparationM +
                   " m（右へ " + spec.PassByShiftM.ToString("0.##") + " m 寄る）";
        }

        static RobotSpec LoadRobotSpec()
        {
            RobotSpec s = AssetDatabase.LoadAssetAtPath<RobotSpec>(ServiceSimSetup.RobotSpecPath);
            if (s == null) Debug.LogError("[ServiceSim] RobotSpec が見つかりません");
            return s;
        }

        /// <summary>
        /// 道探しの枠が結果を動かすかの確かめ（2026-10-26）。
        /// ⑦（fam_p5）ロボット0台・種0・1・2 を3回回す。
        ///
        /// 段階Bの18回（cs:7）で、⑦が移す前（cs:144）より9回とも待ちが短く出た。
        /// 昼の切り分けで入れた枠 5000 が、エディタを閉じるまで残っていた疑いがあるため、
        /// 枠 100 と 5000 で同じ3回を回して並べる。ロボット0台でも出ているので、
        /// ロボットではなく人（スタッフ）の動きを見る条件を選んでいる。
        ///
        /// 枠は ExperimentPlan.pathIterationsPerFrame で決まる（切り替えメニューで変える）。
        /// 入った値は run_info.json の path_iterations_per_frame に出るので、
        /// あとから「どちらの枠で回したか」が必ず分かる。
        /// 名札が path_check なので、段階Bの18回とは混ざらない
        /// </summary>
        [MenuItem("ServiceSim/Stage B/道探しの枠の確かめ（⑦0台・種0〜2）", false, 205)]
        public static void RunPathCheck()
        {
            // 条件 3 = ⑦（fam_p5）・ロボット 0 台（unity_conditions の並び）
            List<int> condIdx = new List<int> { 3, 3, 3 };
            List<int> seeds = new List<int> { 0, 1, 2 };
            StartBatch("道探しの枠の確かめ", LabelPathCheck, condIdx, seeds, false);
        }

        /// <summary>
        /// まとめて回し始める（Run All と、移せたかの確かめで共通）。
        ///
        /// skipDone が true なら、同じ名札・同じ版・同じ条件・同じ種の行が台帳にある回を飛ばす。
        /// 確かめは同じ条件を 2 回回すので false にする（飛ばさない）。
        /// 回し方（1回ごとに Play を出入りする・実時間の上限・caffeinate）は同じ
        /// </summary>
        static void StartBatch(string title, string label,
                               List<int> condIdx, List<int> seeds, bool skipDone)
        {
            ExperimentRunner runner; ExperimentPlan plan;
            if (!ServiceSimRunSupport.Prepare(out runner, out plan)) return;

            // --- 押す前の確かめ（合わなければ回し始めない）---
            if (!CheckBeforeRun(plan, label, out string why))
            {
                Debug.LogError("[ServiceSim] " + title + "：回し始めません\n" + why);
                return;
            }

            SimParams sp = LoadParams();
            if (sp == null) return;
            if (sp.Conditions.Count != 6)
            {
                Debug.LogError("[ServiceSim] " + title + "：unity_conditions が 6 件ではありません（" +
                               sp.Conditions.Count + " 件）。params を確かめてください");
                return;
            }

            string ledger = ServiceSimLedger.PathOf(plan);
            string day = DateTime.Now.ToString("yyyyMMdd");

            // --- 回し始めの版（照合キーに入れるので先に読む。依頼 v8 の 3）---
            string cs, pendingNote;
            ReadChangeset(out cs, out pendingNote);

            // 照合は「店・台数・種・版」。出力フォルダ名（日付入り）では照らし合わせない。
            // 名札ごとに分けるので、確かめの回が段階Bの18回を「済」にしない（依頼 v11）
            HashSet<string> done = skipDone
                ? ServiceSimLedger.DoneKeys(ledger, label)
                : new HashSet<string>();

            List<string> conds = new List<string>();
            List<string> folders = new List<string>();
            int skipped = 0;
            for (int i = 0; i < condIdx.Count; i++)
            {
                SimParams.Condition c = sp.ConditionAt(condIdx[i]);
                if (c == null)
                {
                    Debug.LogError("[ServiceSim] " + title + "：条件 " + condIdx[i] + " がありません");
                    return;
                }
                if (done.Contains(ServiceSimLedger.RunKey(c.store, c.robots, seeds[i], cs)))
                { skipped++; continue; }
                conds.Add(condIdx[i] + ":" + seeds[i]);
                folders.Add(FreeFolderName(plan, day, label, cs, c, seeds[i], folders));
            }

            if (conds.Count == 0)
            {
                Debug.Log("[ServiceSim] " + title + "：この版（" + cs + "）の " + condIdx.Count +
                          " 回はぜんぶ台帳にあります（" + skipped + " 回を飛ばしました）。" +
                          "\n  やり直すときは台帳の行を見て、エイイチに相談してください" +
                          "\n  台帳: " + ledger);
                return;
            }

            EditorPrefs.SetBool(KeyActive, true);
            EditorPrefs.SetInt(KeyIndex, 0);
            EditorPrefs.SetString(KeyConds, string.Join(";", conds.ToArray()));
            EditorPrefs.SetString(KeyFolders, string.Join(";", folders.ToArray()));
            EditorPrefs.SetString(KeyUnityCs, cs);
            EditorPrefs.SetString(KeyPending, pendingNote);
            EditorPrefs.SetString(KeyLog, "");
            EditorPrefs.SetString(KeyBatchStart, day);
            EditorPrefs.SetString(KeyLabel, label);

            StartCaffeinate();

            Debug.Log("[ServiceSim] " + title + "：" + conds.Count + " / " + condIdx.Count + " 回を回します" +
                      (skipped > 0 ? "（台帳にある " + skipped + " 回は飛ばします）" : "") +
                      "\n  名札: " + label +
                      "\n  Unity の版: " + cs + (pendingNote.Length > 0 ? "（" + pendingNote + "）" : "") +
                      "\n  台帳: " + ledger +
                      "\n  1回の実時間の上限: " + (MaxWallSecondsPerRun / 60f).ToString("0") + " 分" +
                      "\n  1回ごとに Play を出入りします。止めたいときは、Play を抜けたところで" +
                      " ServiceSim/Stage B/いまの回で止める を押してください" +
                      "\n  ずれの切り分けの切り替え: " + DetCheckNote(plan) +
                      "\n  すれ違い（依頼 v13）: " + PassByNote(LoadRobotSpec()) +
                      "\n  置き場所: " + string.Join(" / ", folders.ToArray()));

            StartNext();
        }

        [MenuItem("ServiceSim/Stage B/押す前の確かめ（回さない）", false, 201)]
        public static void CheckOnly()
        {
            ExperimentPlan plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(
                ServiceSimSetup.ExperimentPlanPath);
            if (plan == null) { Debug.LogError("[ServiceSim] ExperimentPlan が見つかりません"); return; }

            string why;
            bool ok = CheckBeforeRun(plan, out why);
            string ledger = ServiceSimLedger.PathOf(plan);
            HashSet<string> doneFolders = ServiceSimLedger.DoneFolders(ledger);

            StringBuilder sb = new StringBuilder();
            sb.AppendLine("[ServiceSim] 段階B：押す前の確かめ " + (ok ? "→ 回せます" : "→ 回せません"));
            if (!ok) sb.AppendLine(why);
            sb.AppendLine("  台帳: " + ledger);
            sb.AppendLine("  台帳にある回（ぜんぶの版）: " + doneFolders.Count + " 件");
            sb.AppendLine("  列（" + ServiceSimLedger.Columns.Length + " 列・台帳の版 " +
                          ServiceSimLedger.SchemaVersion + "）: " + ServiceSimLedger.HeaderLine);

            // --- 試算の版・店の形・レジの数（依頼 v10b の 3 B 2）---
            SimParams sp = LoadParams();
            if (sp == null)
            {
                sb.AppendLine("  試算の値を読めませんでした（回せません）");
                Debug.LogError(sb.ToString().TrimEnd());
                return;
            }
            sb.AppendLine("  試算の版 (meta.model): " + sp.Model +
                          " / 店の形の版 (meta.layout_generation): " + sp.LayoutGeneration);
            sb.AppendLine("  条件: " + sp.Conditions.Count + " 件 / レジ: " + RegistersNote(sp));
            if (sp.Problems.Count > 0)
            {
                ok = false;
                sb.AppendLine("  合わないもの: " + sp.Problems.Count + " 件");
                for (int i = 0; i < sp.Problems.Count; i++)
                    sb.AppendLine("    - " + sp.Problems[i]);
            }
            else sb.AppendLine("  合わないもの: なし");

            // --- 18回が「済」か「まだ」か（この版での照合。Run All と同じ見方）---
            string cs, pendingNote;
            ReadChangeset(out cs, out pendingNote);
            // 照合は名札（stageB）ごと。移せたかの確かめ（split_check）の回は数えない（依頼 v11）
            HashSet<string> doneKeys = ServiceSimLedger.DoneKeys(ledger, LabelStageB);

            List<int> condIdx; List<int> seeds;
            BuildOrder(out condIdx, out seeds);

            int todo = 0;
            StringBuilder list = new StringBuilder();
            for (int i = 0; i < condIdx.Count; i++)
            {
                SimParams.Condition c = sp.ConditionAt(condIdx[i]);
                if (c == null) continue;
                bool had = doneKeys.Contains(
                    ServiceSimLedger.RunKey(c.store, c.robots, seeds[i], cs));
                if (!had) todo++;
                list.AppendLine("    " + (i + 1).ToString("00") + ". " + c.store +
                                "・ロボット " + c.robots + " 台・種 " + seeds[i] +
                                " … " + (had ? "済（台帳にあるので飛ばします）" : "まだ"));
            }

            sb.AppendLine("  Unity の版: " + cs +
                          (pendingNote.Length > 0 ? "（" + pendingNote + "）" : "（保留なし）"));
            // 版が分からないまま回すと、台帳の unity_cs が「不明」になって
            // どの版で回したか追えなくなる（依頼 v11 の 2）
            if (cs.IndexOf("不明", StringComparison.Ordinal) >= 0)
            {
                ok = false;
                sb.AppendLine("    ・版が分かりません。このプロジェクトを Unity Version Control の" +
                              "ワークスペースにして、check-in してから回してください");
            }
            sb.AppendLine("  この版の18回（名札 " + LabelStageB + "）: まだ " + todo +
                          " 回 / 済 " + (condIdx.Count - todo) + " 回");
            sb.Append(list.ToString().TrimEnd('\n'));
            sb.AppendLine();
            sb.AppendLine("  移せたかの確かめ（名札 " + LabelSplitCheck +
                          "）は、この18回の「済／まだ」に入りません");

            if (ok) Debug.Log(sb.ToString().TrimEnd()); else Debug.LogError(sb.ToString().TrimEnd());
        }

        // ---------------- ずれの切り分けの切り替え（依頼 v11 の 5 章。試しだけ）----------------

        /// <summary>いま入っている切り替えを、人が読める1行にする</summary>
        static string DetCheckNote(ExperimentPlan plan)
        {
            if (plan == null) return "（ExperimentPlan を読めません）";
            // 道探しの枠は切り替えではなく「回すたびに入れる値」なので、いつでも出す（2026-10-26）
            string head = "道探しの枠 " + plan.pathIterationsPerFrame + " 回/コマ"
                          + (plan.pathIterationsPerFrame == 100 ? "（既定）" : "（既定は 100）");
            if (!plan.detCheckSyncPeoplePaths &&
                !plan.detCheckNoAvoidance && !plan.detCheckFrameFingerprint)
                return head + " / ほかの切り替えはぜんぶ切";
            return head +
                   " / 人の道を同期 " + (plan.detCheckSyncPeoplePaths ? "入" : "切") +
                   " / 局所回避を切る " + (plan.detCheckNoAvoidance ? "入" : "切") +
                   " / 1コマごとの指紋 " + (plan.detCheckFrameFingerprint ? "入" : "切");
        }

        static ExperimentPlan LoadPlan()
        {
            ExperimentPlan p = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(
                ServiceSimSetup.ExperimentPlanPath);
            if (p == null) Debug.LogError("[ServiceSim] ExperimentPlan が見つかりません");
            return p;
        }

        static void SetDetCheck(string title, int pathIterations, bool syncPaths,
                                bool noAvoidance, bool fingerprint)
        {
            ExperimentPlan p = LoadPlan();
            if (p == null) return;
            p.pathIterationsPerFrame = pathIterations > 0 ? pathIterations : 100;
            p.detCheckSyncPeoplePaths = syncPaths;
            p.detCheckNoAvoidance = noAvoidance;
            p.detCheckFrameFingerprint = fingerprint;
            EditorUtility.SetDirty(p);
            AssetDatabase.SaveAssets();
            Debug.Log("[ServiceSim] ずれの切り分け：" + title +
                      "\n  " + DetCheckNote(p) +
                      "\n  このまま ServiceSim/Stage B/ずれの切り分け（③1台・種1 を2回）を押してください");
        }

        [MenuItem("ServiceSim/Stage B/切り分けの設定/試し1：道探しの枠を 5000 にする", false, 220)]
        public static void DetCheck1() { SetDetCheck("試し1", 5000, false, false, true); }

        [MenuItem("ServiceSim/Stage B/切り分けの設定/試し2：人の道を同期で引く", false, 221)]
        public static void DetCheck2() { SetDetCheck("試し2", 5000, true, false, true); }

        [MenuItem("ServiceSim/Stage B/切り分けの設定/試し4：局所回避も切る（要るときだけ）", false, 222)]
        public static void DetCheck4() { SetDetCheck("試し4", 5000, true, true, true); }

        [MenuItem("ServiceSim/Stage B/切り分けの設定/ぜんぶ切る（枠 100・ほかは切）", false, 223)]
        public static void DetCheckOff() { SetDetCheck("ぜんぶ切る", 100, false, false, false); }

        [MenuItem("ServiceSim/Stage B/切り分けの設定/道探しの枠を 100 にする（既定）", false, 225)]
        public static void PathIter100() { SetPathIterations(100); }

        [MenuItem("ServiceSim/Stage B/切り分けの設定/道探しの枠を 5000 にする", false, 226)]
        public static void PathIter5000() { SetPathIterations(5000); }

        /// <summary>
        /// 道探しの枠だけを入れ直す（2026-10-26）。
        /// ほかの切り替えは触らない。回すたびに ExperimentRunner がこの値を入れるので、
        /// エディタの中に前の値が残らない
        /// </summary>
        static void SetPathIterations(int n)
        {
            ExperimentPlan p = LoadPlan();
            if (p == null) return;
            p.pathIterationsPerFrame = n > 0 ? n : 100;
            EditorUtility.SetDirty(p);
            AssetDatabase.SaveAssets();
            Debug.Log("[ServiceSim] 道探しの枠を " + p.pathIterationsPerFrame +
                      " 回/コマ にしました（既定は 100）" +
                      "\n  " + DetCheckNote(p) +
                      "\n  回すたびにこの値が入り、run_info.json の" +
                      " path_iterations_per_frame に実際の値が出ます");
        }

        [MenuItem("ServiceSim/Stage B/切り分けの設定/いまの設定を見る", false, 224)]
        public static void DetCheckShow()
        {
            ExperimentPlan p = LoadPlan();
            if (p != null) Debug.Log("[ServiceSim] ずれの切り分けの切り替え: " + DetCheckNote(p));
        }

        /// <summary>6条件のレジの数を短く書く（ぜんぶ同じなら1つだけ）</summary>
        static string RegistersNote(SimParams sp)
        {
            if (sp.Conditions.Count == 0) return "（条件なし）";
            int first = sp.Conditions[0].registers;
            bool same = true;
            for (int i = 1; i < sp.Conditions.Count; i++)
                if (sp.Conditions[i].registers != first) { same = false; break; }
            if (same) return first + " つ（6条件とも同じ）";

            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < sp.Conditions.Count; i++)
            {
                if (i > 0) sb.Append(" / ");
                sb.Append(sp.Conditions[i].registers);
            }
            return "ばらばら: " + sb;
        }

        [MenuItem("ServiceSim/Stage B/いまの回で止める", false, 202)]
        public static void StopBatch()
        {
            if (!EditorPrefs.GetBool(KeyActive, false))
            {
                Debug.Log("[ServiceSim] 段階B：いま回していません");
                return;
            }
            Finish("エイイチが止めました");
        }

        [MenuItem("ServiceSim/Stage B/台帳を開く", false, 210)]
        public static void OpenLedger()
        {
            ExperimentPlan plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(
                ServiceSimSetup.ExperimentPlanPath);
            if (plan == null) return;
            string p = ServiceSimLedger.PathOf(plan);
            if (!File.Exists(p)) { Debug.LogWarning("[ServiceSim] まだありません: " + p); return; }
            EditorUtility.RevealInFinder(p);
        }

        // ---------------- 押す前の確かめ ----------------

        static bool CheckBeforeRun(ExperimentPlan plan, out string why)
        {
            return CheckBeforeRun(plan, null, out why);
        }

        /// <summary>
        /// 押す前の確かめ。label に名札を渡すと、
        /// 測る回（stageB / split_check）では「ずれの切り分けの切り替え」が
        /// 入ったままかどうかも見る（依頼 v11 の 5 章）。
        /// 切り替えが入ったまま測る回を回すと、黙って動きが変わった数字が
        /// 台帳に入ってしまうので、そこで止める
        /// </summary>
        static bool CheckBeforeRun(ExperimentPlan plan, string label, out string why)
        {
            StringBuilder sb = new StringBuilder();

            if (label != null && label != LabelDetCheck && plan != null &&
                (plan.detCheckSyncPeoplePaths || plan.detCheckNoAvoidance))
                sb.AppendLine("  ・ずれの切り分けの切り替えが入ったままです（" + DetCheckNote(plan) + "）。" +
                              "測る回は回せません。ServiceSim/Stage B/切り分けの設定/" +
                              "ぜんぶ切る を押してください");

            // 1倍で回す切り替え（SimHud）が入っていると、その回は見る専用で
            // 記録を書き出さない。測る回では必ず止める（依頼 v13 の 依頼2・2026-10-26）
            SimHud hud = UnityEngine.Object.FindObjectOfType<SimHud>();
            if (hud != null && hud.realTimeSpeed)
                sb.AppendLine("  ・SimHud の「1倍で回す」が入っています。" +
                              "その回は見る専用で記録を書き出さないので、測る回は回せません。" +
                              "シーンの SimHud で Real Time Speed を切ってください");

            if (!Directory.Exists(plan.kioxiaVolumePath))
                sb.AppendLine("  ・KIOXIA がつながっていません: " + plan.kioxiaVolumePath);

            if (!Directory.Exists(plan.kioxiaOutputRoot))
                sb.AppendLine("  ・置き場所がありません: " + plan.kioxiaOutputRoot +
                              "（MASTER の保存先です。新しく作らずに確かめてください）");

            string ledger = ServiceSimLedger.PathOf(plan);
            string headerWhy;
            if (!ServiceSimLedger.CanAppend(ledger, out headerWhy))
                sb.AppendLine("  ・" + headerWhy.Replace("\n", "\n  "));

            StoreBuilder builder = UnityEngine.Object.FindObjectOfType<StoreBuilder>();
            NavMeshBaker baker = UnityEngine.Object.FindObjectOfType<NavMeshBaker>();
            if (builder == null || baker == null)
                sb.AppendLine("  ・StoreBuilder / NavMeshBaker が見つかりません。ServiceSim/Setup を実行してください");

            why = sb.ToString().TrimEnd();
            return why.Length == 0;
        }

        // ---------------- 18回の並び ----------------

        /// <summary>
        /// ③ロボット2台 種0 → ⑦ロボット2台 種0 → 残りを条件・種の順。
        /// 条件の番号は unity_conditions の並び（0..5）
        /// </summary>
        static void BuildOrder(out List<int> conds, out List<int> seeds)
        {
            conds = new List<int>();
            seeds = new List<int>();

            // 最初の2回（この2回で失敗したら残りを回さない）
            Add(conds, seeds, 2, 0);   // ③ ロボット2台 種0
            Add(conds, seeds, 5, 0);   // ⑦ ロボット2台 種0

            int[] allSeeds = { 0, 1, 2 };
            for (int c = 0; c < 6; c++)
                for (int s = 0; s < allSeeds.Length; s++)
                {
                    if (c == 2 && allSeeds[s] == 0) continue;   // もう入れた
                    if (c == 5 && allSeeds[s] == 0) continue;
                    Add(conds, seeds, c, allSeeds[s]);
                }
        }

        static void Add(List<int> conds, List<int> seeds, int c, int s)
        {
            conds.Add(c);
            seeds.Add(s);
        }

        /// <summary>
        /// 出力フォルダの名前（依頼 v8 の 3・v11 の 2）。
        /// {日付}_{名札}_cs{版}_{店}_r{台数}_s{種}。
        /// 版が分かるようにしておき、照合は台帳のキー（店・台数・種・版）で行う。
        ///
        /// 版の字は "ServiceSim/cs:1" の形で来るので（依頼 v11 の 2）、
        /// フォルダ名に使えない "/" と ":" を外して "cs1" にする。
        /// 前のプロジェクトの "cs:144" も同じく "cs144" になり、名前の形は変わらない
        /// </summary>
        static string FolderName(string day, string label, string cs, SimParams.Condition c, int seed)
        {
            string ver = (cs ?? "")
                .Replace(ServiceSimLedger.CsPrefix, "")   // "ServiceSim/" を外す
                .Replace("cs:", "")
                .Replace(":", "").Replace("/", "")
                .Replace("（", "").Replace("）", "");
            if (ver.Length == 0) ver = "unknown";
            return day + "_" + label + "_cs" + ver + "_" + c.store + "_r" + c.robots + "_s" + seed;
        }

        /// <summary>
        /// 書き出す先が既にあるときは上書きせず、末尾に _2・_3 … を付けた
        /// 新しいフォルダ名を返す（依頼 v8 の 3）。
        /// 同じ条件を2回回す確かめ（依頼 v11）では、2回めがこれで _2 になる
        /// </summary>
        static string FreeFolderName(ExperimentPlan plan, string day, string label, string cs,
                                     SimParams.Condition c, int seed, List<string> taken)
        {
            string baseName = FolderName(day, label, cs, c, seed);
            string name = baseName;
            for (int n = 2; n < 100; n++)
            {
                // taken は、この並びでもう使うことに決めた名前（まだフォルダは無い）。
                // 同じ条件を2回回すときに、2回めが同じ名前にならないようにする（依頼 v11）
                bool free = !Directory.Exists(Path.Combine(plan.kioxiaOutputRoot, name)) &&
                            (taken == null || !taken.Contains(name));
                if (free) return name;
                name = baseName + "_" + n;
            }
            return name;
        }

        // ---------------- 1回ずつ回す ----------------

        static void StartNext()
        {
            if (!EditorPrefs.GetBool(KeyActive, false)) return;

            string[] conds = Split(EditorPrefs.GetString(KeyConds, ""));
            string[] folders = Split(EditorPrefs.GetString(KeyFolders, ""));
            int index = EditorPrefs.GetInt(KeyIndex, 0);

            if (index >= conds.Length || index >= folders.Length) { Finish("ぜんぶ終わりました"); return; }

            ExperimentRunner runner; ExperimentPlan plan;
            if (!ServiceSimRunSupport.Prepare(out runner, out plan))
            {
                Finish("Play の下ごしらえができませんでした");
                return;
            }

            SimParams sp = LoadParams();
            if (sp == null) { Finish("params を読めませんでした"); return; }
            if (runner.simParams == null) runner.simParams = sp;

            string[] parts = conds[index].Split(':');
            int condIndex = int.Parse(parts[0], CultureInfo.InvariantCulture);
            int seed = int.Parse(parts[1], CultureInfo.InvariantCulture);

            SimParams.Condition cond = sp.ConditionAt(condIndex);
            if (cond == null) { Finish("条件 " + condIndex + " がありません"); return; }

            string assetPath = cond.store == "fam_p1" ? P1Path : cond.store == "fam_p5" ? P5Path : null;
            JsonStoreLayout layout = assetPath != null
                ? AssetDatabase.LoadAssetAtPath<JsonStoreLayout>(assetPath) : null;
            if (layout == null) { Finish("店の形が見つかりません: " + cond.store); return; }

            StoreBuilder builder = UnityEngine.Object.FindObjectOfType<StoreBuilder>();
            NavMeshBaker baker = UnityEngine.Object.FindObjectOfType<NavMeshBaker>();
            if (builder == null || baker == null) { Finish("StoreBuilder / NavMeshBaker がありません"); return; }

            builder.layout = layout;
            baker.layout = layout;
            EditorUtility.SetDirty(builder);
            EditorUtility.SetDirty(baker);

            ExperimentRunner.Job job = new ExperimentRunner.Job();
            job.siteRun = true;
            job.conditionIndex = condIndex;
            job.storeKey = cond.store;
            job.robots = cond.robots;
            job.seed = seed;
            job.aisleWidthIndex = layout.aisleWidthIndex;
            job.crowd = CrowdLevel.Normal;
            job.applyDeliveryOffset = false;
            job.folder = Path.Combine(plan.kioxiaOutputRoot, folders[index]);

            runner.SetSingleJob(job);
            runner.timeMode = ExperimentRunner.TimeMode.FixedStep;
            runner.captureDeltaTimeOverride = 0f;
            runner.maxWallSecondsPerRun = MaxWallSecondsPerRun;
            // 回し始めの版と、保留中の変更があったかを run_info に残す（依頼 v7 の 2）
            runner.unityCs = EditorPrefs.GetString(KeyUnityCs, "");
            runner.pendingNote = EditorPrefs.GetString(KeyPending, "");
            runner.runLabel = "stageB";
            runner.viewMode = false;
            runner.runOnPlay = true;
            EditorUtility.SetDirty(runner);
            EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());

            EditorPrefs.SetString(KeyRunStart, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture));

            Debug.Log("[ServiceSim] " + BatchLabel() + " " + (index + 1) + "/" + conds.Length + " を回します" +
                      "\n  条件 " + condIndex + "：" + cond.Id + "（" + cond.store +
                      "・ロボット " + cond.robots + " 台）/ 種 " + seed +
                      "\n  置き場所: " + job.folder);

            EditorApplication.EnterPlaymode();
        }

        /// <summary>Play を抜けたら、その回を台帳に足して次へ</summary>
        [InitializeOnLoadMethod]
        static void Hook()
        {
            EditorApplication.playModeStateChanged -= OnPlayModeChanged;
            EditorApplication.playModeStateChanged += OnPlayModeChanged;
        }

        static void OnPlayModeChanged(PlayModeStateChange state)
        {
            if (state != PlayModeStateChange.EnteredEditMode) return;
            if (!EditorPrefs.GetBool(KeyActive, false)) return;

            // Play を抜けた直後にそのまま Play へ入れないので、1 コマ置く
            EditorApplication.delayCall += AfterRun;
        }

        static void AfterRun()
        {
            if (!EditorPrefs.GetBool(KeyActive, false)) return;

            ExperimentPlan plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(
                ServiceSimSetup.ExperimentPlanPath);
            if (plan == null) { Finish("ExperimentPlan が見つかりません"); return; }

            string[] conds = Split(EditorPrefs.GetString(KeyConds, ""));
            string[] folders = Split(EditorPrefs.GetString(KeyFolders, ""));
            int index = EditorPrefs.GetInt(KeyIndex, 0);
            if (index >= folders.Length) { Finish("ぜんぶ終わりました"); return; }

            string folder = folders[index];
            string runDir = Path.Combine(plan.kioxiaOutputRoot, folder);

            float wallS = 0f;
            long ticks;
            if (long.TryParse(EditorPrefs.GetString(KeyRunStart, "0"),
                              NumberStyles.Integer, CultureInfo.InvariantCulture, out ticks) && ticks > 0)
                wallS = (float)(DateTime.UtcNow - new DateTime(ticks, DateTimeKind.Utc)).TotalSeconds;

            string status;
            string line = WriteLedgerRow(plan, conds[index], folder, runDir, wallS, out status);

            // ログに1行ためる
            string log = EditorPrefs.GetString(KeyLog, "");
            EditorPrefs.SetString(KeyLog, log.Length > 0 ? log + LogSep + line : line);

            Debug.Log("[ServiceSim] " + BatchLabel() + " " + (index + 1) + "/" + conds.Length + " おわり: " + line);

            // 最初の2回で失敗したら、残りは回さない
            if (status == "failed" && index + 1 <= FailFastRuns)
            {
                Finish("最初の " + FailFastRuns + " 回で失敗したので、残りは回しません（" + folder + "）");
                return;
            }

            EditorPrefs.SetInt(KeyIndex, index + 1);
            if (index + 1 >= conds.Length) { Finish("ぜんぶ終わりました"); return; }

            StartNext();
        }

        // ---------------- 台帳に1行 ----------------
        /// <summary>
        /// その回の台帳の1行を足す。
        ///
        /// 行を作るのは **LedgerRow**（Experiment/LedgerRow.cs）1つだけ。
        /// ここは Editor でしか取れない値（.asset・EditorPrefs）を詰めて渡すだけ。
        /// 夜の画面なしの実行（HeadlessRun）も同じ LedgerRow を通る
        /// （依頼 v12 の 2〜3 章・2026-10-26）
        /// </summary>
        static string WriteLedgerRow(ExperimentPlan plan, string condSeed, string folder,
                                     string runDir, float wallS, out string status)
        {
            SimParams sp = LoadParams();
            string[] parts = condSeed.Split(':');
            int condIndex = int.Parse(parts[0], CultureInfo.InvariantCulture);
            int seed = int.Parse(parts[1], CultureInfo.InvariantCulture);

            LedgerRow.Inputs inp = new LedgerRow.Inputs();
            inp.simParams = sp;
            inp.cond = sp != null ? sp.ConditionAt(condIndex) : null;
            inp.robotSpec = AssetDatabase.LoadAssetAtPath<RobotSpec>(ServiceSimSetup.RobotSpecPath);
            inp.peopleSpec = AssetDatabase.LoadAssetAtPath<PeopleSpec>(ServiceSimSetup.PeopleSpecPath);
            inp.unityCs = EditorPrefs.GetString(KeyUnityCs, "");
            inp.runFolder = folder;
            inp.runDir = runDir;
            inp.seed = seed;
            inp.wallS = wallS;
            inp.ledgerPath = ServiceSimLedger.PathOf(plan);

            return LedgerRow.BuildAndAppend(inp, out status);
        }

        // ---------------- 終わり ----------------

        static void Finish(string whyDone)
        {
            string[] conds = Split(EditorPrefs.GetString(KeyConds, ""));
            int index = EditorPrefs.GetInt(KeyIndex, 0);
            string log = EditorPrefs.GetString(KeyLog, "");

            StopCaffeinate();

            ExperimentPlan plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(
                ServiceSimSetup.ExperimentPlanPath);

            StringBuilder sb = new StringBuilder();
            sb.AppendLine("[ServiceSim] " + BatchLabel() + "：" + whyDone);
            sb.AppendLine("  回した: " + index + " / " + conds.Length + " 回");
            sb.AppendLine("  台帳: " + (plan != null ? ServiceSimLedger.PathOf(plan) : "-"));
            sb.AppendLine("  一覧（出力フォルダ / status / 実時間 / 入店待ち / ロボットが運んだ割合）:");
            string[] lines = log.Length > 0
                ? log.Split(LogSep) : new string[0];
            for (int i = 0; i < lines.Length; i++) sb.AppendLine("    " + (i + 1) + ". " + lines[i]);

            string text = sb.ToString().TrimEnd();
            Debug.Log(text);

            // run_all_log.txt（service_sim/unity/ の中）
            if (plan != null && Directory.Exists(plan.kioxiaOutputRoot))
            {
                try
                {
                    string p = Path.Combine(plan.kioxiaOutputRoot, LogFileName);
                    string head = "=== " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") +
                                  " " + BatchLabel() + "（Unity " + EditorPrefs.GetString(KeyUnityCs, "") + "）===\n";
                    File.AppendAllText(p, head + text + "\n\n", new UTF8Encoding(false));
                    Debug.Log("[ServiceSim] 一覧を書きました: " + p);
                }
                catch (Exception e)
                {
                    Debug.LogWarning("[ServiceSim] run_all_log.txt を書けませんでした: " + e.Message);
                }
            }

            EditorPrefs.SetBool(KeyActive, false);
            EditorPrefs.DeleteKey(KeyConds);
            EditorPrefs.DeleteKey(KeyFolders);
            EditorPrefs.DeleteKey(KeyIndex);
            EditorPrefs.DeleteKey(KeyLog);
            EditorPrefs.DeleteKey(KeyRunStart);
        }

        // ---------------- スリープを止める ----------------

        static void StartCaffeinate()
        {
            StopCaffeinate();
            try
            {
                Process p = new Process();
                p.StartInfo.FileName = "/usr/bin/caffeinate";
                p.StartInfo.Arguments = "-dimsu";
                p.StartInfo.UseShellExecute = false;
                p.StartInfo.CreateNoWindow = true;
                p.Start();
                EditorPrefs.SetInt(KeyCaffeinate, p.Id);
                Debug.Log("[ServiceSim] スリープを止めました（caffeinate pid " + p.Id + "）");
            }
            catch (Exception e)
            {
                Debug.LogWarning("[ServiceSim] caffeinate を起動できませんでした（スリープしないよう手で設定してください）: "
                                 + e.Message);
            }
        }

        static void StopCaffeinate()
        {
            int pid = EditorPrefs.GetInt(KeyCaffeinate, 0);
            if (pid <= 0) return;
            EditorPrefs.DeleteKey(KeyCaffeinate);
            try
            {
                Process p = Process.GetProcessById(pid);
                if (p != null && !p.HasExited) { p.Kill(); Debug.Log("[ServiceSim] スリープの設定を戻しました"); }
            }
            catch (Exception)
            {
                // もう終わっている
            }
        }

        // ---------------- 小さな道具 ----------------

        /// <summary>
        /// 回し始めの changeset 番号と、保留中の変更があるか。
        ///
        /// cm status --machinereadable の1行めが
        ///   STATUS 1 ServiceSim/ServiceSim studio_unity148@unity
        /// なので、そこから版の番号を取り、2行めから先を数える
        /// （PR＝置いてあるだけ も数える）。
        ///
        /// 版の字は ServiceSimLedger.CsPrefix を頭に付けて "ServiceSim/cs:1" の形にする
        /// （依頼 v11 の 2）。このプロジェクトは changeset の番号が 1 から振り直されるので、
        /// 前のプロジェクトの "cs:144" などと台帳の中で混ざらないようにするため
        /// </summary>
        // BuildHeadless（依頼 v12 の 1 章）も同じ関数で版を取る。
        // 版の取り方が2通りに分かれると、台帳の unity_cs と
        // build_info.json の unity_cs が食い違うので、入り口は1つにする
        public static void ReadChangeset(out string cs, out string pendingNote)
        {
            cs = "";
            pendingNote = "";

            string outp = RunCm("status --machinereadable");
            if (string.IsNullOrEmpty(outp))
            {
                // cm を呼べなかった。数が分からないので「不明」と書く（依頼 v8 の 3）
                cs = ServiceSimLedger.CsPrefix + "不明";
                pendingNote = "不明（cm status を読めませんでした）";
                return;
            }

            // 保留の数は Unity Version Control の「保留中の変更」と同じ数え方にする。
            // 置いてあるだけ（PR＝Private）も、あの一覧には出るので **数に入れる**
            // （前は PR を外していたので、2件あったのに空になっていた）
            int controlled = 0, privateItems = 0;
            string[] lines = outp.Split('\n');
            for (int i = 0; i < lines.Length; i++)
            {
                string l = lines[i].Trim();
                if (l.Length == 0) continue;
                if (l.StartsWith("STATUS "))
                {
                    // STATUS {版} {リポジトリ} {サーバー}
                    string[] f = l.Split(' ');
                    if (f.Length > 1) cs = ServiceSimLedger.CsPrefix + "cs:" + f[1];

                    // このプロジェクトのワークスペースから答えが返ってきたかを確かめる
                    // （依頼 v11 の 2）。
                    // ServiceSim/ がまだ自分のワークスペースになっていないと、
                    // cm は1つ上（/Users/shimazueiichi/Robotics_UX）の古いワークスペースの
                    // 版（Robotics_UX_New の cs:37 など）を返す。
                    // その番号を台帳に書くと、どの版で回したのか分からなくなるので弾く
                    string repo = f.Length > 2 ? f[2] : "";
                    if (repo.IndexOf(ExpectRepoName, StringComparison.Ordinal) < 0)
                    {
                        Debug.LogError("[ServiceSim] cm が別のワークスペースの版を返しました: " + l +
                                       "\n  このプロジェクト（" + Directory.GetCurrentDirectory() +
                                       "）を Unity Version Control のワークスペースにしてください。" +
                                       "\n  いまのままだと、台帳の unity_cs に1つ上の" +
                                       "ワークスペースの番号が入ってしまいます");
                        cs = ServiceSimLedger.CsPrefix + "不明";
                        pendingNote = "不明（cm が " + repo + " の版を返しました。" +
                                      "このプロジェクトがワークスペースになっていません）";
                        return;
                    }
                    continue;
                }
                if (l.StartsWith("PR ")) privateItems++;
                else controlled++;
            }

            int all = controlled + privateItems;
            if (all > 0)
                pendingNote = "保留あり " + all + " 件（管理下 " + controlled +
                              " 件・置いてあるだけ " + privateItems + " 件）";
            if (cs.Length == 0) cs = ServiceSimLedger.CsPrefix + "不明";
        }

        static string RunCm(string args)
        {
            string[] paths = { "/usr/local/bin/cm", "/opt/homebrew/bin/cm", "cm" };
            for (int i = 0; i < paths.Length; i++)
            {
                try
                {
                    Process p = new Process();
                    p.StartInfo.FileName = paths[i];
                    p.StartInfo.Arguments = args;
                    p.StartInfo.WorkingDirectory = Directory.GetCurrentDirectory();
                    p.StartInfo.UseShellExecute = false;
                    p.StartInfo.RedirectStandardOutput = true;
                    p.StartInfo.RedirectStandardError = true;
                    p.StartInfo.CreateNoWindow = true;
                    p.Start();
                    string outp = p.StandardOutput.ReadToEnd();
                    p.WaitForExit(15000);
                    if (!string.IsNullOrEmpty(outp)) return outp;
                }
                catch (Exception)
                {
                    // 次の場所を試す
                }
            }
            return null;
        }

        /// <summary>
        /// いま回している並びの名札（ログに出す。依頼 v11 の 2）。
        /// StartBatch が EditorPrefs に入れた名札を読む。無ければ段階B扱い
        /// </summary>
        static string BatchLabel()
        {
            string l = EditorPrefs.GetString(KeyLabel, LabelStageB);
            return l == LabelStageB ? "段階B" : l;
        }

        static string[] Split(string s)
        {
            if (string.IsNullOrEmpty(s)) return new string[0];
            return s.Split(';');
        }

        static SimParams LoadParams()
        {
            SimParams sp = AssetDatabase.LoadAssetAtPath<SimParams>(ServiceSimSetup.SimParamsPath);
            if (sp == null)
            {
                Debug.LogError("[ServiceSim] SimParams が見つかりません: " + ServiceSimSetup.SimParamsPath);
                return null;
            }
            sp.Load("");
            if (sp.Conditions.Count == 0)
            {
                Debug.LogError("[ServiceSim] unity_conditions を読めません");
                return null;
            }
            return sp;
        }
    }
}
