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 { /// /// 段階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行も変えない。入れたのは「まとめて回す仕組み」と /// 「台帳への書き出し」だけ。 /// public static class ServiceSimStageB { const string P1Path = "Assets/ServiceSim/Store/StoreLayout_p1_efficient_corner.asset"; const string P5Path = "Assets/ServiceSim/Store/StoreLayout_p5_crammed.asset"; /// 1回の実時間の上限(秒)。依頼 v7 の 3 const float MaxWallSecondsPerRun = 40f * 60f; /// 最初の何回かが失敗したら、残りを回さずに止める const int FailFastRuns = 2; const string LogFileName = "run_all_log.txt"; /// EditorPrefs にためるログの区切り(1文字。行の中に出ない字を使う) 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"; // ---------------- メニュー ---------------- [MenuItem("ServiceSim/Stage B/Run All(18回)", false, 200)] public static void RunAll() { ExperimentRunner runner; ExperimentPlan plan; if (!ServiceSimRunSupport.Prepare(out runner, out plan)) return; // --- 押す前の確かめ(合わなければ回し始めない)--- if (!CheckBeforeRun(plan, out string why)) { Debug.LogError("[ServiceSim] 段階B:回し始めません\n" + why); return; } SimParams sp = LoadParams(); if (sp == null) return; if (sp.Conditions.Count != 6) { Debug.LogError("[ServiceSim] 段階B:unity_conditions が 6 件ではありません(" + sp.Conditions.Count + " 件)。params を確かめてください"); return; } // --- 18回の並びを作る --- List condIdx; List seeds; BuildOrder(out condIdx, out seeds); string ledger = ServiceSimLedger.PathOf(plan); HashSet done = ServiceSimLedger.DoneFolders(ledger); string day = DateTime.Now.ToString("yyyyMMdd"); List conds = new List(); List folders = new List(); int skipped = 0; for (int i = 0; i < condIdx.Count; i++) { SimParams.Condition c = sp.ConditionAt(condIdx[i]); string folder = FolderName(day, c, seeds[i]); if (done.Contains(folder)) { skipped++; continue; } conds.Add(condIdx[i] + ":" + seeds[i]); folders.Add(folder); } if (conds.Count == 0) { Debug.Log("[ServiceSim] 段階B:18回ぜんぶ台帳にあります(" + skipped + " 回を飛ばしました)。" + "\n やり直すときは台帳の行を見て、エイイチに相談してください" + "\n 台帳: " + ledger); return; } // --- 回し始めの記録 --- string cs, pendingNote; ReadChangeset(out cs, out pendingNote); 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); StartCaffeinate(); Debug.Log("[ServiceSim] 段階B:" + conds.Count + " / 18 回を回します" + (skipped > 0 ? "(台帳にある " + skipped + " 回は飛ばします)" : "") + "\n Unity の版: " + cs + (pendingNote.Length > 0 ? "(" + pendingNote + ")" : "") + "\n 台帳: " + ledger + "\n 1回の実時間の上限: " + (MaxWallSecondsPerRun / 60f).ToString("0") + " 分" + "\n 1回ごとに Play を出入りします。止めたいときは、Play を抜けたところで" + " ServiceSim/Stage B/いまの回で止める を押してください"); StartNext(); } [MenuItem("ServiceSim/Stage B/押す前の確かめ(回さない)", false, 201)] public static void CheckOnly() { ExperimentPlan plan = AssetDatabase.LoadAssetAtPath( ServiceSimSetup.ExperimentPlanPath); if (plan == null) { Debug.LogError("[ServiceSim] ExperimentPlan が見つかりません"); return; } string why; bool ok = CheckBeforeRun(plan, out why); string ledger = ServiceSimLedger.PathOf(plan); HashSet done = ServiceSimLedger.DoneFolders(ledger); StringBuilder sb = new StringBuilder(); sb.AppendLine("[ServiceSim] 段階B:押す前の確かめ " + (ok ? "→ 回せます" : "→ 回せません")); if (!ok) sb.AppendLine(why); sb.AppendLine(" 台帳: " + ledger); sb.AppendLine(" 台帳にある回: " + done.Count + " 件"); sb.AppendLine(" 列(" + ServiceSimLedger.Columns.Length + "): " + ServiceSimLedger.HeaderLine); if (ok) Debug.Log(sb.ToString().TrimEnd()); else Debug.LogError(sb.ToString().TrimEnd()); } [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( 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) { StringBuilder sb = new StringBuilder(); 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(); NavMeshBaker baker = UnityEngine.Object.FindObjectOfType(); if (builder == null || baker == null) sb.AppendLine(" ・StoreBuilder / NavMeshBaker が見つかりません。ServiceSim/Setup を実行してください"); why = sb.ToString().TrimEnd(); return why.Length == 0; } // ---------------- 18回の並び ---------------- /// /// ③ロボット2台 種0 → ⑦ロボット2台 種0 → 残りを条件・種の順。 /// 条件の番号は unity_conditions の並び(0..5) /// static void BuildOrder(out List conds, out List seeds) { conds = new List(); seeds = new List(); // 最初の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 conds, List seeds, int c, int s) { conds.Add(c); seeds.Add(s); } static string FolderName(string day, SimParams.Condition c, int seed) { // 台帳の照合キー。回し直しても同じ名前になるよう、時刻は入れない return day + "_stageB_" + c.store + "_r" + c.robots + "_s" + seed; } // ---------------- 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(assetPath) : null; if (layout == null) { Finish("店の形が見つかりません: " + cond.store); return; } StoreBuilder builder = UnityEngine.Object.FindObjectOfType(); NavMeshBaker baker = UnityEngine.Object.FindObjectOfType(); 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] 段階B " + (index + 1) + "/" + conds.Length + " を回します" + "\n 条件 " + condIndex + ":" + cond.Id + "(" + cond.store + "・ロボット " + cond.robots + " 台)/ 種 " + seed + "\n 置き場所: " + job.folder); EditorApplication.EnterPlaymode(); } /// Play を抜けたら、その回を台帳に足して次へ [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( 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] 段階B " + (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.json / run_info.json から台帳の1行を作って足す。 /// 戻り値はログ用の短い1行。status も返す /// 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); SimParams.Condition cond = sp != null ? sp.ConditionAt(condIndex) : null; JsonValue sum = ServiceSimLedger.ReadSummary(runDir); JsonValue info = ReadJson(Path.Combine(runDir, "run_info.json")); bool stoppedEarly = info != null && info["stopped_early"].B(false); status = sum == null ? "failed" : (stoppedEarly ? "stopped" : "completed"); RobotSpec rs = AssetDatabase.LoadAssetAtPath(ServiceSimSetup.RobotSpecPath); PeopleSpec ps = AssetDatabase.LoadAssetAtPath(ServiceSimSetup.PeopleSpecPath); JsonValue simLike = sum != null ? sum["sim_like"] : null; JsonValue siteM = sum != null ? sum["site_metrics"] : null; JsonValue siteRun = sum != null ? sum["site_run"] : null; JsonValue trips = sum != null ? sum["trips"] : null; JsonValue stops = sum != null ? sum["stops"] : null; Dictionary row = new Dictionary(); row["timestamp"] = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"); row["run_folder"] = folder; row["status"] = status; row["unity_cs"] = EditorPrefs.GetString(KeyUnityCs, ""); row["layout_generation"] = sp != null ? sp.LayoutGeneration : ""; row["params_model"] = sp != null ? sp.Model : ""; if (cond != null) { row["store"] = cond.store; row["staff"] = ServiceSimLedger.Num(cond.staff); row["robots"] = ServiceSimLedger.Num(cond.robots); row["arrivals_per_h"] = ServiceSimLedger.Num(cond.arrivalsPerHour); row["mode"] = cond.mode; row["trays"] = ServiceSimLedger.Num(cond.trays); row["fragile_by"] = cond.fragileBy; } row["seed"] = ServiceSimLedger.Num(seed); // 決まり(回したときの設定)。run_info にあればそれを、無ければ .asset から row["staff_first_yield"] = info != null && !info["staff_first_yield"].IsNull ? ServiceSimLedger.Flag(info["staff_first_yield"].B(false)) : ServiceSimLedger.Flag(ps != null && ps.staffFirstYield); row["yield_order_enabled"] = info != null && !info["yield_order_enabled"].IsNull ? ServiceSimLedger.Flag(info["yield_order_enabled"].B(false)) : ServiceSimLedger.Flag(rs != null && rs.yieldOrderEnabled); row["one_robot_per_aisle"] = info != null && !info["one_robot_per_aisle"].IsNull ? ServiceSimLedger.Flag(info["one_robot_per_aisle"].B(false)) : ServiceSimLedger.Flag(rs != null && rs.oneRobotPerAisle); row["robot_first_wait_s"] = Val(simLike, "robot_first_wait_s"); row["everyone_left"] = siteRun != null && !siteRun["everyone_left"].IsNull ? ServiceSimLedger.Flag(siteRun["everyone_left"].B(false)) : ""; row["total_minutes"] = Val(siteRun, "total_minutes"); row["wall_seconds"] = info != null && !info["wall_seconds"].IsNull ? Val(info, "wall_seconds") : ServiceSimLedger.Num(wallS); row["entry_wait_mean"] = Val(siteM, "entry_wait_mean"); row["staff_wait_mean"] = Val(siteM, "staff_wait_mean"); row["dish_wait_mean"] = Val(siteM, "dish_wait_mean"); row["cold_share"] = Val(siteM, "cold_share"); row["robot_share"] = Val(siteM, "robot_share"); row["staff_util"] = Val(siteM, "staff_util"); row["robot_util"] = Val(siteM, "robot_util"); row["walk_km_per_staff"] = Val(siteM, "walk_km_per_staff"); // サイトの試算(同じ条件の site_result) if (cond != null) { row["site_entry_wait_mean"] = Site(cond, "entry_wait_mean"); row["site_staff_wait_mean"] = Site(cond, "staff_wait_mean"); row["site_dish_wait_mean"] = Site(cond, "dish_wait_mean"); row["site_robot_share"] = Site(cond, "robot_share"); } // サイトの unity_measures と同じ意味で Unity が測った値。 // pause_rate・stuck_rate は hop_m(role=site_only)が要るので空欄にする row["robot_speed_mps"] = Val(trips, "robot_speed_mps"); row["pause_rate"] = ""; row["pause_s_mean"] = Val(trips, "pause_s_mean"); row["stuck_rate"] = ""; row["trips"] = Val(trips, "trips"); row["timeouts"] = Val(trips, "timeouts"); row["stuck_count"] = Val(stops, "stuck_count"); row["stop_s_total"] = Val(stops, "stop_s_total"); row["corner_stop_time_share"] = Val(simLike, "corner_stop_time_share"); row["gave_up"] = Val(simLike, "gave_up"); row["warped"] = Val(simLike, "warped"); row["table_freed"] = Val(simLike, "table_freed"); row["rescues"] = Val(siteM, "rescues"); row["late_staff_dishes"] = Val(simLike, "late_staff_dishes"); row["staff_yield_share_of_staff_time"] = Val(simLike, "staff_yield_share_of_staff_time"); row["ledger_schema_version"] = ServiceSimLedger.Num(ServiceSimLedger.SchemaVersion); string ledger = ServiceSimLedger.PathOf(plan); string why; if (!ServiceSimLedger.Append(ledger, row, out why)) Debug.LogWarning("[ServiceSim] 台帳に足しませんでした: " + why); return folder + " / " + status + " / 実時間 " + (wallS / 60f).ToString("0.0") + " 分" + " / 入店待ち " + Or(row, "entry_wait_mean") + " 分" + " / ロボットが運んだ割合 " + Or(row, "robot_share"); } static string Val(JsonValue v, string key) { if (v == null || v.IsNull) return ""; JsonValue x = v[key]; if (x.IsNull) return ""; return ServiceSimLedger.Num(x.F(0f)); } static string Site(SimParams.Condition c, string key) { float f; return c != null && c.siteResult.TryGetValue(key, out f) ? ServiceSimLedger.Num(f) : ""; } static string Or(Dictionary row, string key) { string v; return row.TryGetValue(key, out v) && v.Length > 0 ? v : "-"; } static JsonValue ReadJson(string path) { if (!File.Exists(path)) return null; try { string err; JsonValue v = JsonValue.Parse(File.ReadAllText(path), out err); return v != null && v.IsObject ? v : null; } catch (Exception) { return null; } } // ---------------- 終わり ---------------- 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( ServiceSimSetup.ExperimentPlanPath); StringBuilder sb = new StringBuilder(); sb.AppendLine("[ServiceSim] 段階B:" + 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") + " 段階B(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) { // もう終わっている } } // ---------------- 小さな道具 ---------------- /// /// 回し始めの changeset 番号と、保留中の変更があるか。 /// /// cm status --machinereadable の1行めが /// STATUS 139 Robotics_UX_New/... studio_unity148@unity /// なので、そこから版の番号を取り、2行めから先を数える /// (PR=置いてあるだけ は数えない)。 /// static void ReadChangeset(out string cs, out string pendingNote) { cs = ""; pendingNote = ""; string outp = RunCm("status --machinereadable"); if (!string.IsNullOrEmpty(outp)) { int n = 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 ")) { string[] f = l.Split(' '); if (f.Length > 1) cs = "cs:" + f[1]; continue; } if (l.StartsWith("PR ")) continue; // 置いてあるだけ n++; } if (n > 0) pendingNote = "保留あり " + n + " 件"; } if (cs.Length == 0) cs = "(読めません)"; } 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; } static string[] Split(string s) { if (string.IsNullOrEmpty(s)) return new string[0]; return s.Split(';'); } static SimParams LoadParams() { SimParams sp = AssetDatabase.LoadAssetAtPath(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; } } }