Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.MetroGadget.cs

214 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 重新数一遍。");
}
}