214 lines
12 KiB
C#
214 lines
12 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 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
|
||
/// <c>LightRail_4_405/carriage1|2/Seats/Gadgets_888_LightRailChair03_*</c>), 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: <c>world.content.metroGadgets.enabled = false</c>.
|
||
/// </summary>
|
||
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<SceneMethods.AskMetroGadgetIdsArgs4229938>(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<SceneMethods.MetroCarriageGadgetInfos>());
|
||
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<int>();
|
||
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<int>();
|
||
var borrowed = new System.Collections.Generic.List<int>();
|
||
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<SceneMethods.MetroCarriageGadgetInfos>());
|
||
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<SceneMethods.MetroCarriageGadgetInfos>(perCarriage.Count);
|
||
ulong total = 0UL;
|
||
for (var c = 0; c < perCarriage.Count; c++)
|
||
{
|
||
var seats = perCarriage[c];
|
||
var inner = new List<ulong>(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<ulong>(inner) : new List<ulong>(),
|
||
});
|
||
}
|
||
|
||
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<string> WarnedSeatConflicts = new();
|
||
private static readonly object SeatConflictLock = new();
|
||
|
||
/// <summary>
|
||
/// 同车型(同一 <c>RailCarriageConfig</c> 布局类)里有没有已经配好座位数的兄弟模板?
|
||
/// 有就借用它的值 —— 比退化成 defaultCarriageSeats 更接近真值。
|
||
/// ⛑ <b>必须按 key 升序取第一个</b>:<c>Dictionary</c> 的遍历顺序没有保证,同车型存在
|
||
/// 多个已配值时会取到随机的那个(见 <see cref="WarnSeatConflict"/>)。
|
||
/// </summary>
|
||
private static int? FindSiblingSeats(MetroGadgetSettings cfg, string layoutClass, Connection conn)
|
||
{
|
||
int? first = null;
|
||
var distinct = new SortedSet<int>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 同一布局类里配了<b>不一样</b>的座位数 —— 一定是某次实测数错了,喊出来(每条只喊一次)。
|
||
/// 2026-09-27 现况:Vehicle03/End 同时有 tpl4=4 与 tpl9=6,而两者
|
||
/// <c>TemplateName</c>(Vehicle03/CarriageOnEnd) 与 <c>ViewModelName</c>(View_Blue_End)
|
||
/// <b>完全相同</b>,只差 <c>IsRevert</c>(镜像)⇒ 座位数本该一样,现在必有一个是错的。
|
||
/// </summary>
|
||
private static void WarnSeatConflict(string layoutClass, SortedSet<int> values, Connection conn)
|
||
{
|
||
lock (SeatConflictLock)
|
||
{
|
||
if (!WarnedSeatConflicts.Add(layoutClass))
|
||
return;
|
||
}
|
||
conn.Log.Warn("[METROG] ⛔ 座位表自相矛盾:布局类 " + layoutClass +
|
||
" 在 metroGadgets.seatsByCarriageTemplate 里配了 " + string.Join(" / ", values) +
|
||
" 多个不同值 —— 同一车型的车厢座位数本该一致,必有一次实测数错," +
|
||
"当前只按模板 id 升序取第一个用。请进游戏跑 TrainSeatLoader 的 trainhier 重新数一遍。");
|
||
}
|
||
}
|