using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.AI;
namespace ServiceSim.EditorTools
{
///
/// サイトの JSON から建てた店(JsonStoreLayout)を確かめるメニュー。
///
/// することは「建てて、正しく建ったかを確かめる」まで。回さない(段階1)。
/// 建てるところと画面の撮影は ServiceSim/Build Store をそのまま呼ぶ(作り直さない)。
/// ここで足すのは、JSON と突き合わせる確かめだけ:
/// 1. 卓の数・席の数・細い通路の本数と幅(summary.json とも突き合わせる)
/// 2. 止まる位置・座る位置・席に着く点・場所の点が NavMesh に乗るか
/// 3. 受け渡し口の前から各卓の止まる位置までの道のりと、JSON の dist の照合(CSV)
/// 4. 席に入れない卓(席に着く点へ、入口から歩いて行けるか)
/// 書き出し先は ExperimentPlan.screenshotDir(Build Store と同じ)。
///
public static class ServiceSimJsonStore
{
const string P1Path = "Assets/ServiceSim/Store/StoreLayout_p1_efficient_corner.asset";
const string P5Path = "Assets/ServiceSim/Store/StoreLayout_p5_crammed.asset";
/// 道のりを測るときに、点から NavMesh を探す半径(m)
const float SampleRadius = 0.5f;
/// 点が NavMesh に乗っているとみなす距離(m)
const float OnMeshRadius = 0.2f;
/// 差がこれを超えた卓を報告に並べる
const float BigDiffPct = 30f;
[MenuItem("ServiceSim/JSON Store/③ p1_efficient_corner を建てて確かめる", false, 30)]
public static void CheckP1() { Run(P1Path); }
[MenuItem("ServiceSim/JSON Store/⑦ p5_crammed を建てて確かめる", false, 31)]
public static void CheckP5() { Run(P5Path); }
[MenuItem("ServiceSim/JSON Store/③⑦ を続けて確かめる", false, 32)]
public static void CheckBoth() { if (Run(P1Path)) Run(P5Path); }
const string SimParamsPath = "Assets/ServiceSim/Experiment/SimParams.asset";
///
/// サイトの試算の値(serving-robot-params.json)を読んで、中身と版の番号を確かめる(v5)。
/// 回さない。読めるかどうかと、店の形の版と合っているかだけを見る。
///
[MenuItem("ServiceSim/JSON Store/試算の値を読んで確かめる(v5)", false, 33)]
public static void CheckSimParams()
{
SimParams sp = AssetDatabase.LoadAssetAtPath(SimParamsPath);
if (sp == null)
{
Debug.LogError("[ServiceSim] SimParams が見つかりません: " + SimParamsPath);
return;
}
StringBuilder sb = new StringBuilder();
sb.AppendLine("# 試算の値の読み込み(v5・回していない)");
sb.AppendLine("作った日時: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
sb.AppendLine();
// 店の形の版と突き合わせる(③⑦の両方)
string[] layouts = { P1Path, P5Path };
for (int i = 0; i < layouts.Length; i++)
{
JsonStoreLayout layout = AssetDatabase.LoadAssetAtPath(layouts[i]);
if (layout == null || layout.layoutJson == null) continue;
string err;
JsonValue root = JsonValue.Parse(layout.layoutJson.text, out err);
string gen = root != null ? root["meta"]["generation"].S("") : "(読めない)";
bool ok = sp.Load(gen);
sb.AppendLine("## " + layout.layoutFileName + " と突き合わせ → " +
(ok ? "合っている" : "合っていない"));
sb.AppendLine(" 店の形の版 (meta.generation): " + gen);
sb.AppendLine(sp.Report());
sb.AppendLine();
}
// 読む値(site_rule)を並べて、何を使うかを見えるようにする
sp.Load(null);
string[] keys = { "guide_s", "pay_s", "call_s", "staff_place_s", "robot_handoff_s",
"clear_s", "wipe_s", "load_s", "guest_load_s", "rescue_s",
"staff_speed", "cold_min", "dish_extra_m", "max_extra_min" };
sb.AppendLine("## Unity が使う値(role=site_rule)");
for (int i = 0; i < keys.Length; i++)
sb.AppendLine(" " + keys[i] + " = " + F(sp.SiteRule(keys[i], float.NaN)));
float[] fc = sp.SiteRuleArray("fragile_cook", null);
sb.AppendLine(" fragile_cook = " + (fc == null ? "(無い)"
: F(fc[0]) + " / " + F(fc[1]) + " / " + F(fc[2])));
sb.AppendLine();
sb.AppendLine("## 店ごとの値(stores)");
string[] stores = { "fam_p1", "fam_p5" };
for (int i = 0; i < stores.Length; i++)
{
string k = stores[i];
sb.AppendLine(" " + k + ": 卓 " + F(sp.StoreValue(k, "tables", 0f)) +
" / peak_min " + F(sp.StoreValue(k, "peak_min", 0f)) +
" / 来客(真ん中)" + sp.StoreMiddle(k, "arrivals", 0) + " 組毎時" +
" / スタッフ(真ん中)" + sp.StoreMiddle(k, "staff", 0) + " 人" +
" / eat_min " + sp.StoreTriangle(k, "eat_min", Vector3.zero) +
" / call_rate " + F(sp.StoreValue(k, "call_rate", 0f)) +
" / bus_trips " + F(sp.StoreValue(k, "bus_trips", 0f)) +
" / fragile " + F(sp.StoreValue(k, "fragile", 0f)));
}
sb.AppendLine();
sb.AppendLine("## 出す数字の名前と意味(meta.metrics)");
List mk = sp.MetricKeys();
for (int i = 0; i < mk.Count; i++)
sb.AppendLine(" " + mk[i] + ": " + sp.MetricNote(mk[i]));
ExperimentPlan plan = AssetDatabase.LoadAssetAtPath(ServiceSimSetup.ExperimentPlanPath);
string outDir = plan != null ? plan.screenshotDir : null;
if (!string.IsNullOrEmpty(outDir))
{
try
{
Directory.CreateDirectory(outDir);
File.WriteAllText(Path.Combine(outDir,
DateTime.Now.ToString("yyyyMMdd") + "_sim_params_check.txt"), sb.ToString());
}
catch (Exception e) { Debug.LogError("[ServiceSim] 書けません: " + e.Message); }
}
Debug.Log("[ServiceSim] 試算の値の読み込み\n" + sb.ToString());
}
static bool Run(string assetPath)
{
JsonStoreLayout layout = AssetDatabase.LoadAssetAtPath(assetPath);
if (layout == null)
{
Debug.LogError("[ServiceSim] 店の形が見つかりません: " + assetPath);
return false;
}
if (EditorSceneManager.GetActiveScene().path != ServiceSimSetup.ScenePath)
{
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return false;
EditorSceneManager.OpenScene(ServiceSimSetup.ScenePath);
}
StoreBuilder builder = UnityEngine.Object.FindObjectOfType();
NavMeshBaker baker = UnityEngine.Object.FindObjectOfType();
ExperimentPlan plan = AssetDatabase.LoadAssetAtPath(ServiceSimSetup.ExperimentPlanPath);
if (builder == null || baker == null || plan == null)
{
Debug.LogError("[ServiceSim] StoreBuilder / NavMeshBaker / ExperimentPlan が見つかりません。"
+ "先に ServiceSim/Setup を実行してください");
return false;
}
// 建てるのは Build Store にまかせる(組み立て・ベイク・既存の確かめ・撮影)
builder.layout = layout;
baker.layout = layout;
ServiceSimMenu.BuildStore();
builder = UnityEngine.Object.FindObjectOfType();
baker = UnityEngine.Object.FindObjectOfType();
if (builder == null || baker == null || builder.Plan == null)
{
Debug.LogError("[ServiceSim] 建てられませんでした。Console の [ServiceSim] の行を見てください");
return false;
}
if (builder.layout != layout)
{
Debug.LogError("[ServiceSim] Build Store のあとで店の形が " + builder.layout +
" に戻っています。確かめを中止しました");
return false;
}
string err;
JsonValue root = JsonValue.Parse(layout.layoutJson != null ? layout.layoutJson.text : "", out err);
if (root == null)
{
Debug.LogError("[ServiceSim] JSON を読めません: " + err);
return false;
}
string outDir = plan.screenshotDir;
try { Directory.CreateDirectory(outDir); }
catch (Exception e)
{
Debug.LogError("[ServiceSim] 書き出し先を作れません: " + outDir + "\n" + e.Message);
return false;
}
string stem = DateTime.Now.ToString("yyyyMMdd") + "_" + layout.layoutFileName;
string csvPath = Path.Combine(outDir, stem + "_table_dist.csv");
string txtPath = Path.Combine(outDir, stem + "_json_check.txt");
NavMeshQueryFilter people = baker.FilterFor(baker.PeopleAgentTypeId);
// v5b: 道順はサイトの routes を使う。NavMesh は区画を足さない段階1 と同じ。
// 点から点の区間だけを NavMesh でつないで確かめる
RobotSpec robotSpec = AssetDatabase.LoadAssetAtPath(ServiceSimSetup.RobotSpecPath);
NavMeshQueryFilter robot = baker.FilterFor(baker.RobotAgentTypeId);
_snapRadius = robotSpec != null ? robotSpec.Radius * 4f : SampleRadius;
StringBuilder report = new StringBuilder();
report.AppendLine("# JSON から建てた店の確かめ(段階1・回していない)");
report.AppendLine("店の形: " + layout.name + " / " + layout.StoreKindName);
report.AppendLine("作った日時: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
report.AppendLine();
report.AppendLine(layout.FrontFitReport());
report.AppendLine();
report.AppendLine(CountReport(builder, root, layout));
report.AppendLine();
report.AppendLine(OnMeshReport(builder, robot, people));
report.AppendLine();
report.AppendLine(DistReport(builder, root, robot, csvPath));
report.AppendLine();
report.AppendLine(SeatReachReport(builder, people));
try { File.WriteAllText(txtPath, report.ToString()); }
catch (Exception e) { Debug.LogError("[ServiceSim] 報告を書けません: " + e.Message); }
Debug.Log("[ServiceSim] JSON Store の確かめ おわり(" + layout.layoutFileName + ")\n"
+ report.ToString()
+ "\n 書き出し: " + txtPath + "\n " + csvPath);
return true;
}
// ---------------- 1. 数 ----------------
static string CountReport(StoreBuilder builder, JsonValue root, JsonStoreLayout layout)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("## 1. 数");
int tables = builder.Tables.Count;
int seats = 0;
for (int i = 0; i < builder.Tables.Count; i++) seats += builder.Tables[i].seatCount;
int jsonTables = root["tables"].Count;
int jsonSeats = 0;
for (int i = 0; i < jsonTables; i++) jsonSeats += root["tables"][i]["seats"].I(0);
int sumTables = -1, sumSeats = -1;
string err;
JsonValue sum = layout.summaryJson != null
? JsonValue.Parse(layout.summaryJson.text, out err) : null;
if (sum != null)
for (int i = 0; i < sum.Count; i++)
if (sum[i]["file"].S("") == layout.layoutFileName)
{
sumTables = sum[i]["tables"].I(-1);
sumSeats = sum[i]["seats"].I(-1);
}
sb.AppendLine(" 卓: Unity " + tables + " / JSON " + jsonTables +
" / summary.json " + sumTables + " " + Same(tables, jsonTables, sumTables));
sb.AppendLine(" 席: Unity " + seats + " / JSON " + jsonSeats +
" / summary.json " + sumSeats + " " + Same(seats, jsonSeats, sumSeats));
int narrow = 0;
StringBuilder widths = new StringBuilder();
for (int i = 0; i < builder.Aisles.Count; i++)
{
StoreBuilder.AisleInfo a = builder.Aisles[i];
if (!a.narrow) continue;
narrow++;
if (widths.Length > 0) widths.Append(" / ");
widths.Append(a.name).Append(" ").Append(F(a.width)).Append(" m");
}
sb.AppendLine(" 細い通路: " + narrow + " 本(" + widths + ")");
sb.AppendLine(" 組み立てた幅 (plan.aisleWidth): " + F(builder.BuiltAisleWidth) + " m");
sb.AppendLine(" 外寸: " + F(builder.OuterSize.x) + " × " + F(builder.OuterSize.y) + " m" +
" / JSON outer " + F(root["outer"]["w"].F(0f)) + " × " + F(root["outer"]["d"].F(0f)) + " m");
return sb.ToString().TrimEnd();
}
static string Same(int a, int b, int c)
{
bool ok = a == b && (c < 0 || a == c);
return ok ? "一致" : "← ちがう";
}
// ---------------- 2. 点が NavMesh に乗るか ----------------
static string OnMeshReport(StoreBuilder builder, NavMeshQueryFilter robot, NavMeshQueryFilter people)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("## 2. 点が NavMesh の上に乗るか(乗らないものだけ並べる)");
List offDelivery = new List();
List offSpot = new List();
List offSeat = new List();
List offPlace = new List();
StorePlan plan = builder.Plan;
for (int i = 0; i < plan.seats.Count; i++)
{
PlanSeat s = plan.seats[i];
string tid = "T" + s.tableId.ToString("00");
if (!OnMesh(s.deliveryPoint, robot)) offDelivery.Add(tid + " " + P(s.deliveryPoint));
for (int k = 0; k < s.spots.Count; k++)
if (!OnMesh(s.spots[k].point, people))
offSpot.Add(tid + " " + s.spots[k].name + " " + P(s.spots[k].point));
for (int k = 0; k < s.seatPoints.Count; k++)
if (!OnMesh(s.seatPoints[k], people))
offSeat.Add(tid + "#" + (k + 1) + " " + P(s.seatPoints[k]));
}
for (int i = 0; i < plan.places.Count; i++)
{
PlanPlace p = plan.places[i];
if (p.solid) continue; // 部屋そのものの点は通れない箱の中
bool forRobot = p.kind == PlaceKind.FoodPass || p.kind == PlaceKind.DishReturn ||
p.kind == PlaceKind.RobotStandby;
if (!OnMesh(p.point, forRobot ? robot : people))
offPlace.Add(p.name + " " + P(p.point) + (forRobot ? "(ロボット用)" : "(人用)"));
}
sb.AppendLine(" 止まる位置 (deliveryPoint, ロボット用): 乗らない " + offDelivery.Count +
" / " + plan.seats.Count);
Lines(sb, offDelivery);
sb.AppendLine(" 席に着く点 (spots, 人用): 乗らない " + offSpot.Count);
Lines(sb, offSpot);
sb.AppendLine(" 座る位置 (seatPoints, 人用): 乗らない " + offSeat.Count +
" ※ 座面やベンチの上の点は NavMesh に乗らないのがふつう(見た目に使う点)");
Lines(sb, offSeat, 8);
sb.AppendLine(" 場所の点 (places): 乗らない " + offPlace.Count);
Lines(sb, offPlace);
return sb.ToString().TrimEnd();
}
static void Lines(StringBuilder sb, List items, int max = 40)
{
for (int i = 0; i < items.Count && i < max; i++) sb.AppendLine(" - " + items[i]);
if (items.Count > max) sb.AppendLine(" - ほか " + (items.Count - max) + " 件");
}
static bool OnMesh(Vector3 p, NavMeshQueryFilter filter)
{
NavMeshHit hit;
return NavMesh.SamplePosition(p, out hit, OnMeshRadius, filter);
}
// ---------------- 3. 道のりの照合 ----------------
/// 区間の NavMesh の道のりが、まっすぐの長さのこの倍を超えたら並べる
const float SegmentDetourLimit = 1.05f;
static float _snapRadius = SampleRadius;
///
/// 3. 道順の照合(v5b)。
/// サイトが決めた道順(曲がり角の点)を、Unity の NavMesh でつなぎ直して確かめる。
/// ・道順の長さ(点を結んだ長さ)と JSON の dist
/// ・点から点の区間が NavMesh で完全につながるか
/// ・区間の NavMesh の道のりが、まっすぐの長さの 1.05 倍を超えないか
/// ・use(normal / narrow)= via_narrow
///
static string DistReport(StoreBuilder builder, JsonValue root, NavMeshQueryFilter robot, string csvPath)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("## 3. 道順の照合(受け渡し口の前 → 止まる位置。サイトの routes をたどる)");
RouteBook book = builder.Routes;
if (book == null)
{
sb.AppendLine(" routes がありません(v5b の版の JSON を写してください)");
return sb.ToString().TrimEnd();
}
sb.AppendLine(" 道順: " + book.Summary());
sb.AppendLine(" 止まる位置を面へ寄せる半径: " + F(_snapRadius) + " m");
Dictionary byId = new Dictionary();
JsonValue tables = root["tables"];
for (int i = 0; i < tables.Count; i++)
{
int id = NumberFromId(tables[i]["id"].S(""), i + 1);
if (!byId.ContainsKey(id)) byId[id] = tables[i];
}
StringBuilder csv = new StringBuilder();
csv.AppendLine("table,category,aisle,use,json_dist_m,route_len_m,unity_path_m," +
"diff_m,diff_pct,via_narrow,segments,bad_segments,status");
List brokenSeg = new List();
List detourSeg = new List();
List big = new List();
List diffs = new List();
int narrowTables = 0, okTables = 0;
float sumAbs = 0f, maxAbs = 0f; string maxAt = "";
StorePlan plan = builder.Plan;
for (int i = 0; i < plan.seats.Count; i++)
{
PlanSeat s = plan.seats[i];
string tid = "T" + s.tableId.ToString("00");
JsonValue t;
string category = "", aisle = "";
float jsonDist = -1f;
if (byId.TryGetValue(s.tableId, out t))
{
jsonDist = t["dist"].F(-1f);
category = t["category"].S("");
aisle = t["aisle"].S("");
}
RouteBook.Route r = book.FromPass(s.tableId);
if (r == null)
{
brokenSeg.Add(tid + " 道順が無い");
csv.AppendLine(tid + "," + category + "," + aisle + ",,,,,,,,,,道順が無い");
continue;
}
bool narrow = r.IsNarrow;
if (narrow) narrowTables++;
float routeLen = r.PolylineLength;
float unityLen = 0f;
int bad = 0;
bool allConnected = true;
for (int k = 1; k < r.points.Count; k++)
{
Vector3 a = r.points[k - 1], b = r.points[k];
string st;
float len = PathLen(a, b, robot, out st);
float straight = Vector3.Distance(a, b);
if (len < 0f)
{
allConnected = false; bad++;
brokenSeg.Add(tid + " #" + k + " " + P(a) + "→" + P(b) + " " + st);
continue;
}
unityLen += len;
if (straight > 0.05f && len > straight * SegmentDetourLimit)
{
bad++;
detourSeg.Add(tid + " #" + k + " まっすぐ " + F(straight) + " m → NavMesh " +
F(len) + " m(" + F(len / straight) + " 倍)");
}
}
string status = allConnected ? "ok" : "つながらない区間あり";
if (allConnected)
{
okTables++;
float diff = unityLen - jsonDist;
float pct = jsonDist > 0.0005f ? diff / jsonDist * 100f : 0f;
diffs.Add(diff);
sumAbs += Mathf.Abs(diff);
if (Mathf.Abs(diff) > maxAbs) { maxAbs = Mathf.Abs(diff); maxAt = tid; }
if (Mathf.Abs(pct) > BigDiffPct)
big.Add(tid + " JSON " + F(jsonDist) + " m / Unity " + F(unityLen) + " m(" + F(pct) + "%)");
csv.AppendLine(tid + "," + category + "," + aisle + "," + r.use + "," +
F(jsonDist) + "," + F(routeLen) + "," + F(unityLen) + "," +
F(diff) + "," + F(pct) + "," + (narrow ? "1" : "0") + "," +
(r.points.Count - 1) + "," + bad + ",ok");
}
else
{
csv.AppendLine(tid + "," + category + "," + aisle + "," + r.use + "," +
F(jsonDist) + "," + F(routeLen) + ",," + ",," +
(narrow ? "1" : "0") + "," + (r.points.Count - 1) + "," + bad + "," + status);
}
}
try { File.WriteAllText(csvPath, csv.ToString()); }
catch (Exception e) { sb.AppendLine(" CSV を書けません: " + e.Message); }
sb.AppendLine(" すべての区間がつながった卓: " + okTables + " / " + plan.seats.Count);
if (diffs.Count > 0)
{
float mean = 0f;
for (int i = 0; i < diffs.Count; i++) mean += diffs[i];
mean /= diffs.Count;
sb.AppendLine(" 差(Unity − JSON)の平均: " + F(mean) + " m");
sb.AppendLine(" 差の大きさの平均: " + F(sumAbs / diffs.Count) + " m / 最大: " +
F(maxAbs) + " m(" + maxAt + ")");
}
sb.AppendLine(" 差が " + F(BigDiffPct) + "% を超えた卓: " + big.Count);
Lines(sb, big);
sb.AppendLine(" via_narrow(use=narrow)の卓: " + narrowTables + " / " + plan.seats.Count);
sb.AppendLine(" つながらない区間: " + brokenSeg.Count);
Lines(sb, brokenSeg);
sb.AppendLine(" まっすぐの " + F(SegmentDetourLimit) + " 倍を超えた区間: " + detourSeg.Count);
Lines(sb, detourSeg);
sb.AppendLine(" CSV: " + csvPath);
return sb.ToString().TrimEnd();
}
/// その絞り込みで道のりを測る。完全につながらなければ -1
static float PathLen(Vector3 from, Vector3 to, NavMeshQueryFilter filter, out string status)
{
NavMeshHit hitFrom, hitTo;
if (!NavMesh.SamplePosition(from, out hitFrom, _snapRadius, filter))
{ status = "出発点が面の上にない"; return -1f; }
if (!NavMesh.SamplePosition(to, out hitTo, _snapRadius, filter))
{ status = "行き先が面の上にない"; return -1f; }
NavMeshPath path = new NavMeshPath();
if (!NavMesh.CalculatePath(hitFrom.position, hitTo.position, filter, path))
{ status = "経路を引けない"; return -1f; }
if (path.status != NavMeshPathStatus.PathComplete)
{
status = path.status == NavMeshPathStatus.PathPartial ? "途中まで" : "無効";
return -1f;
}
float len = 0f;
Vector3[] c = path.corners;
for (int i = 1; i < c.Length; i++) len += Vector3.Distance(c[i - 1], c[i]);
status = "ok";
return len;
}
// ---------------- 4. 席に入れない卓 ----------------
static string SeatReachReport(StoreBuilder builder, NavMeshQueryFilter people)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("## 4. 席に入れない卓(入口から、席に着く点まで歩けるか)");
Vector3 from = builder.PointOf(PlaceKind.Entrance);
sb.AppendLine(" 出発点 (Entrance): " + P(from));
List bad = new List();
int checkedSpots = 0;
StorePlan plan = builder.Plan;
for (int i = 0; i < plan.seats.Count; i++)
{
PlanSeat s = plan.seats[i];
if (s.spots.Count == 0)
{
bad.Add("T" + s.tableId.ToString("00") + " 席に着く点が無い");
continue;
}
bool any = false;
for (int k = 0; k < s.spots.Count; k++)
{
checkedSpots++;
string status;
float len = NavMeshBaker.PathLength(from, s.spots[k].point, SampleRadius, people, out status);
if (len >= 0f) { any = true; break; }
}
if (!any) bad.Add("T" + s.tableId.ToString("00") + " どの席に着く点へも歩けない");
}
sb.AppendLine(" 確かめた点: " + checkedSpots + " / 卓 " + plan.seats.Count);
sb.AppendLine(" 入れない卓: " + bad.Count);
Lines(sb, bad);
return sb.ToString().TrimEnd();
}
// ---------------- 小さな道具 ----------------
static int NumberFromId(string id, int fallback)
{
if (string.IsNullOrEmpty(id)) return fallback;
int n = 0; bool any = false;
for (int i = 0; i < id.Length; i++)
{
char c = id[i];
if (c < '0' || c > '9') continue;
n = n * 10 + (c - '0');
any = true;
}
return any ? n : fallback;
}
static string P(Vector3 v)
{
return "(" + F(v.x) + ", " + F(v.z) + ")";
}
static string F(float v)
{
return v.ToString("0.##", CultureInfo.InvariantCulture);
}
}
}