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);
string day = DateTime.Now.ToString("yyyyMMdd");
// --- 回し始めの版(照合キーに入れるので先に読む。依頼 v8 の 3)---
string cs, pendingNote;
ReadChangeset(out cs, out pendingNote);
// 照合は「店・台数・種・版」。出力フォルダ名(日付入り)では照らし合わせない
HashSet done = ServiceSimLedger.DoneKeys(ledger);
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]);
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, cs, c, seeds[i]));
}
if (conds.Count == 0)
{
Debug.Log("[ServiceSim] 段階B:この版(" + cs + ")の18回はぜんぶ台帳にあります(" +
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);
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);
}
///
/// 出力フォルダの名前(依頼 v8 の 3)。
/// {日付}_stageB_cs{版}_{店}_r{台数}_s{種}。
/// 版が分かるようにしておき、照合は台帳のキー(店・台数・種・版)で行う
///
static string FolderName(string day, string cs, SimParams.Condition c, int seed)
{
// "cs:139" の ":" はフォルダ名に向かないので外す
string ver = (cs ?? "").Replace("cs:", "").Replace(":", "").Replace("(", "").Replace(")", "");
if (ver.Length == 0) ver = "unknown";
return day + "_stageB_cs" + ver + "_" + c.store + "_r" + c.robots + "_s" + seed;
}
///
/// 書き出す先が既にあるときは上書きせず、末尾に _2・_3 … を付けた
/// 新しいフォルダ名を返す(依頼 v8 の 3)
///
static string FreeFolderName(ExperimentPlan plan, string day, string cs,
SimParams.Condition c, int seed)
{
string baseName = FolderName(day, cs, c, seed);
string name = baseName;
for (int n = 2; n < 100; n++)
{
if (!Directory.Exists(Path.Combine(plan.kioxiaOutputRoot, name))) 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(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))
{
// cm を呼べなかった。数が分からないので「不明」と書く(依頼 v8 の 3)
cs = "不明";
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 "))
{
string[] f = l.Split(' ');
if (f.Length > 1) cs = "cs:" + f[1];
continue;
}
if (l.StartsWith("PR ")) privateItems++;
else controlled++;
}
int all = controlled + privateItems;
if (all > 0)
pendingNote = "保留あり " + all + " 件(管理下 " + controlled +
" 件・置いてあるだけ " + privateItems + " 件)";
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;
}
}
}