using Ananta.SDK.Network;
using Ananta.SDK.Rpc;
using Ananta.Server.ClientData;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Configuration;
using Ananta.Server.Protocol.Client4229938;
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
namespace Ananta.Server.Handlers.Game;
///
/// AskMetroGadgetIds(67809129) — the missing half of the metro carriages.
///
/// What the client does: it builds the LightRail prefab on its own (see the captured hierarchy
/// LightRail_4_405/carriage1|2/Seats/Gadgets_888_LightRailChair03_*), then asks the server
/// which gadget instance ids belong to that train. Every SpoonSlotComponent on a seat needs a
/// gadget hold before it yields a "sit down" button, and the hold comes from this reply.
///
/// Before 2026-09-23 this method had no handler at all, so the answer was the typed-default empty
/// list (see the log line "unimplemented AskMetroGadgetIds -> typed-default list7-empty 2b"), which
/// is why the seats inside our carriages stay inert while AnantaTrainSeatLoader.exe can wake them
/// up by calling SpoonSlotComponent.Init() directly through frida.
///
/// Option A (implemented here): hand back invented but stable ids from the server's own 9.5e12
/// gadget band. The client only needs an id to bind the local seat slot to. If seats still stay
/// dead, the fallback is option B — really spawn the chairs through SyncGadgetAdd(68843189) with
/// the LightRailChair pathIds (843740076 / 1058671153 / 267309514 / 1181678445, iScale must be 0)
/// and return those instance ids instead.
///
/// Rollback: world.content.metroGadgets.enabled = false.
///
internal sealed partial class GameRouter
{
[Handler(MethodId.AskMetroGadgetIds)]
private async Task OnAskMetroGadgetIds(Connection conn, UxRpcMessage msg)
{
var cfg = PrivateServerConfigStore.Current.World.Content.MetroGadgets;
if (!msg.TryGetArgs(out var args) || args is null)
{
conn.Log.Warn($"[METROG] AskMetroGadgetIds undecodable bytes={msg.Body.Length} " +
$"hex={(msg.Body.Length > 0 ? System.Convert.ToHexString(msg.Body, 0, System.Math.Min(msg.Body.Length, 16)) : "-")}");
await conn.ReturnAsync(msg, new List());
return;
}
var metroId = args.metroId;
// One answer entry per carriage, each holding exactly that carriage's seat count:
// the client walks its own carriages in order and hands entry[i] to carriage[i], and a
// count mismatch silently drops the whole carriage (verified 2026-09-23 — our first version
// answered 6 ids for every carriage and only carriage1, which really has 6 seats, woke up).
var perCarriage = new List();
var composition = "";
if (cfg.UseTrainConfig
&& global::Ananta.Server.ClientData.Client4229938.MetroCarriageCatalog4229938.TryGetCarriageTemplates(metroId, out var templates)
&& templates.Length > 0)
{
var unknown = new System.Collections.Generic.List();
var borrowed = new System.Collections.Generic.List();
foreach (var template in templates)
{
var known = cfg.SeatsByCarriageTemplate.TryGetValue(template, out var configured);
int seats;
if (known)
{
seats = configured;
}
else
{
unknown.Add(template);
// 同一车型(Vehicle01/Head、Vehicle03/End …)的座位数应当一致 —— 没采过的模板
// 先借用同车型已配好的那个值,比无脑退化成 defaultCarriageSeats 更接近真值。
var cls = RailCarriageCatalog4229938.LayoutClassOf((uint)template);
var sibling = cls is null ? null : FindSiblingSeats(cfg, cls, conn);
if (sibling is not null)
borrowed.Add(template);
seats = sibling ?? cfg.DefaultCarriageSeats;
}
perCarriage.Add(System.Math.Clamp(seats, 0, 64));
}
composition = "tpl[" + string.Join(",", templates) + "] seats[" + string.Join(",", perCarriage) + "]";
// ⛔ 座位数只对得上「编组模板 id」在 seatsByCarriageTemplate 里有条目的车厢。
// 查不到就退化成 defaultCarriageSeats(默认 6)。
// 实测 2026-09-27 15:13:1/2/3 号线的车(tpl 10,2,2,2,3 与 11,7,7,7,8)
// 全部命中这条退化路径,只有 4 号线的 tpl 4/5/9 是真实值。
//
// ★★ 2026-09-28 受控对照 + 症状订正(原来这里写"整节车厢被客户端静默丢弃、座位按不出坐下",
// 与客户端实际报出来的东西不符,会把人带偏):
// · 客户端唯一喊出来的错是 **MGAGadgets 槽**(`Metro/Gadget.json data mismatch with
// MGAGadgets slot, the slot count is 1 and the gadget count is 0`),
// 调用栈是 `LoadMoveGroundArea() → OnMoveGroundAreaLoaded → ActivateMGAGadgets()`
// —— 车厢自己的 MoveGroundArea(MGA)gadget 没绑上,不是座位。
// · `mismatch with **Seats** slot` 那条串在 3 个会话里命中 **0 次** ⇒ 座位检查从未触发。
// · 一局内对照:metroId=400..405(4 号线,模板已配)被问 8 次,MGA 报错 **0**;
// 1/2/3 号线(模板未配)报 50 条。三局日志里报错点名的列车**一辆 4 号线都没有**。
// · 线上本身干净:38 次 INVOKE / 38 次 RETURN,全 error=0,延迟 5–10ms,
// body 267B(5 车厢×6)或 145B(3 车厢 4+6+6)⇒ 不是协议问题。
// ⇒ 模板未配 ⇒ 退化的 6 与真值不符 ⇒ 该车厢的 gadget infos 被丢 ⇒ MGA 拿到 0 个。
// 所以"必须把真座位数补上"这个结论不变,但**症状描述以 MGA 为准**。
// 真值只在 prefab 里(任何 JSON 都没有),只能进游戏数,或用 defaultCarriageSeats
// 做零编译 A/B 反推。见硬规则 212 备注。
// ⇒ 这里必须喊出来,否则日志看起来"一切正常"。
if (unknown.Count > 0)
{
var names = unknown.Select(t => RailCarriageCatalog4229938.Describe((uint)t));
conn.Log.Warn($"[METROG] metroId={metroId} 编组模板 {string.Join(",", unknown)} " +
$"(车型 {string.Join(", ", names)})在 metroGadgets.seatsByCarriageTemplate " +
$"里没有条目 ⇒ " +
(borrowed.Count > 0
? $"{string.Join(",", borrowed)} 借用了同车型已配值," +
$"其余退化成 defaultCarriageSeats={cfg.DefaultCarriageSeats}"
: $"退化成 defaultCarriageSeats={cfg.DefaultCarriageSeats}") +
$"。客户端实际报的是 MGAGadgets 槽对不上" +
$"(Metro/Gadget.json data mismatch with MGAGadgets slot, slot count is 1 / " +
$"gadget count is 0,经 LoadMoveGroundArea → ActivateMGAGadgets 触发;" +
$"Seats 槽那条报错 3 个会话里一次都没出现过)。" +
$"补法:进游戏站到该线路的车上,跑 TrainSeatLoader 的 trainhier," +
$"数每节车厢 Seats 下的 LightRailChair 个数,按模板 id 填进配置" +
$"(13 个模板按车型归并只有 8 种布局,采一趟 1/2 号线就能覆盖 Vehicle01 的头/中/尾)。");
}
}
else
{
var carriages = System.Math.Clamp(cfg.Carriages, 0, 32);
var flat = System.Math.Clamp(cfg.SeatsPerCarriage, 0, 64);
for (var i = 0; i < carriages; i++)
perCarriage.Add(flat);
composition = $"flat {carriages}x{flat}";
}
if (!cfg.Enabled || cfg.LogOnly || cfg.IdBase == 0UL || perCarriage.Count == 0 || perCarriage.All(n => n == 0))
{
conn.Log.Info($"[METROG] AskMetroGadgetIds metroId={metroId} -> empty " +
$"(enabled={cfg.Enabled} logOnly={cfg.LogOnly} {composition})");
await conn.ReturnAsync(msg, new List());
return;
}
// Deterministic: the same train always gets the same ids, so a repeated ask (the client
// re-asks every few seconds while a train is near) never reshuffles the seats.
var result = new List(perCarriage.Count);
ulong total = 0UL;
for (var c = 0; c < perCarriage.Count; c++)
{
var seats = perCarriage[c];
var inner = new List(seats);
for (var s = 0; s < seats; s++)
inner.Add(cfg.IdBase
+ (ulong)((long)metroId * 10000L)
+ (ulong)(c * 100)
+ (ulong)s);
total += (ulong)seats;
result.Add(new SceneMethods.MetroCarriageGadgetInfos
{
InnerGadgetIds = inner,
OuterGadgetIds = cfg.IncludeOuter ? new List(inner) : new List(),
});
}
var firstIds = result[0].InnerGadgetIds;
var firstId = firstIds is { Count: > 0 } ? firstIds[0].ToString() : "-";
conn.Log.Info($"[METROG] AskMetroGadgetIds metroId={metroId} -> {result.Count} carriage(s), {total} seat id(s), " +
$"{composition}, first={firstId} outer={cfg.IncludeOuter}");
await conn.ReturnAsync(msg, result);
}
private static readonly HashSet WarnedSeatConflicts = new();
private static readonly object SeatConflictLock = new();
///
/// 同车型(同一 RailCarriageConfig 布局类)里有没有已经配好座位数的兄弟模板?
/// 有就借用它的值 —— 比退化成 defaultCarriageSeats 更接近真值。
/// ⛑ 必须按 key 升序取第一个:Dictionary 的遍历顺序没有保证,同车型存在
/// 多个已配值时会取到随机的那个(见 )。
///
private static int? FindSiblingSeats(MetroGadgetSettings cfg, string layoutClass, Connection conn)
{
int? first = null;
var distinct = new SortedSet();
foreach (var kv in cfg.SeatsByCarriageTemplate.OrderBy(k => k.Key))
{
if (RailCarriageCatalog4229938.LayoutClassOf((uint)kv.Key) != layoutClass)
continue;
distinct.Add(kv.Value);
first ??= kv.Value;
}
if (distinct.Count > 1)
WarnSeatConflict(layoutClass, distinct, conn);
return first;
}
///
/// 同一布局类里配了不一样的座位数 —— 一定是某次实测数错了,喊出来(每条只喊一次)。
/// 2026-09-27 现况:Vehicle03/End 同时有 tpl4=4 与 tpl9=6,而两者
/// TemplateName(Vehicle03/CarriageOnEnd) 与 ViewModelName(View_Blue_End)
/// 完全相同,只差 IsRevert(镜像)⇒ 座位数本该一样,现在必有一个是错的。
///
private static void WarnSeatConflict(string layoutClass, SortedSet values, Connection conn)
{
lock (SeatConflictLock)
{
if (!WarnedSeatConflicts.Add(layoutClass))
return;
}
conn.Log.Warn("[METROG] ⛔ 座位表自相矛盾:布局类 " + layoutClass +
" 在 metroGadgets.seatsByCarriageTemplate 里配了 " + string.Join(" / ", values) +
" 多个不同值 —— 同一车型的车厢座位数本该一致,必有一次实测数错," +
"当前只按模板 id 升序取第一个用。请进游戏跑 TrainSeatLoader 的 trainhier 重新数一遍。");
}
}