Files

132 lines
7.2 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 System.Buffers.Binary;
using Ananta.SDK.Network;
using Ananta.SDK.Rpc;
using Ananta.SDK.Serialization;
using Ananta.Server.Protocol.Client4229938;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// Urban-job (职业) handshakes — the gate every job app walks through before its own panel opens.
///
/// Why this file exists: the courier app's start button never got past the "领取驾照" screen because
/// the RPC behind it was not served at all. <c>DeliveryAPPBeginPanelStore.OnStartClick</c> sends
/// <c>AskStartJob</c> as an INVOKE and only opens the order board from inside its callback, so an
/// unhandled method is not a silent no-op — the callback never fires and the button looks dead
/// (hard rule 94). Same shape for <c>AskQuitJob</c> (离职) and <c>AskAcceptTask</c> (the 晋升 button
/// on the job template, <c>DeliveryAPPBeginPanelStore.OnPromoteClick</c>).
///
/// ⛑ Client-side polarity check (this is why these return bare Ok rather than a payload):
/// every caller compares against <c>LTConfig.MessageConfig.Ok</c> and treats a MATCH as the error
/// branch — <c>if errorId == Ok then DisplayServerMessageId(errorId); return end</c>. So the success
/// path is any code that is NOT Ok, which is what <c>ReturnEmptyOkAsync</c> emits (0).
///
/// These are deliberately stateless: the client only needs the ack to advance. Persisting an
/// "on duty" flag would be dead weight until a real job-progress model exists.
/// </summary>
internal sealed partial class GameRouter
{
/// <summary>JobClass of the courier career; the only class this build of the app asks for.</summary>
private const uint UrbanJobClassDelivery = 11300005u;
// -----------------------------------------------------------------------------------------
// ⛑⛑ 2026-09-29 修正:AskStartJob / AskQuitJob **不是 void**。
// 客户端 `midToReturnMessageReader[63441623 / 63990693]` 都是
// `Base.ReadComplex(reader, Auto.Reader[435])`,而原实现走 `ReturnEmptyOkAsync`
// —— 那个是 **0 字节载荷**(Connection.cs:32 `Array.Empty<byte>()`)⇒ 客户端读空 complex。
// 实测 [JOB] 每个会话 4 次左右(入职/离职按钮),属于稳定触发。
//
// Reader[435] 的 wire(逐层从 `RPCDeserializeAuto.lua` 钉死,共 33 字节):
// FF complex presence(SerializeObjectMarkNull 实测是 0xFF,
// 反编译常量写的 0 是**假的**,按记忆 §5 以实测包体为准)
// Job u32
// Exp u32
// Level u8
// RegisterTime u32
// UnregisterTime u32
// TalentInfo = Auto.Dispatch[1521]:
// Dispatch 读 1 字节 tp,要求 == 0xFF,再用 `Concrete[1521][tp]` 选 reader;
// Concrete[1521] 只有 [1]/[2] 与 __index 兜底 ⇒ [0xFF] 走 __index ⇒ Auto.Reader[1521]
// ⇒ 等价于 `FF` + Reader[1521]
// Reader[1521] = TalentPoint u32 + 两个 ReadDict(u32 → complex)
// ⛑ ReadDict 计数是 **Int32、无 +1 偏置**(RPCDeserializeBase.lua:162)
// ⇒ 空字典 = `FF 00000000`(5 字节),不是 Int7 的 `FF 01`
// ⛑ 回滚:把两个 handler 的 return 换回 `conn.ReturnEmptyOkAsync(msg)`。
// -----------------------------------------------------------------------------------------
private static byte[] BuildJobInfoPayload(uint jobClass, byte level, uint registerTime, uint unregisterTime)
{
var w = new UxWriter();
w.U8(0xFF); // Complex[435] presence
w.U32(jobClass); // Job
w.U32(0); // Exp
w.U8(level); // Level
w.U32(registerTime); // RegisterTime
w.U32(unregisterTime); // UnregisterTime
// TalentInfo = Auto.Dispatch[1521]
w.U8(0xFF); // presence
w.U32(0); // TalentPoint
w.U8(0xFF); w.I32(0); // UnlockTalentInfoDict(空字典)
w.U8(0xFF); w.I32(0); // TalentTreeRecordDict(空字典)
return w.ToArray();
}
private static uint NowUnixSeconds()
=> (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
/// <summary>
/// AskStartJob(63441623) — IN(u32 jobClassId) → <c>Reader[435]</c> 的 JobInfo。
/// Sent by the "领取驾照 / 开始接单" button of every job app. The callback is what opens the
/// panel, so answering is the whole feature.
/// </summary>
[Handler(MethodId.AskStartJob, HandlerPacketKind.Invoke)]
private async Task AskStartJob(Connection conn, UxRpcMessage msg)
{
// ★ 捐赠端 GameRouter.CareerApps.cs:48-49:里希开始职业 ⇒ 发布警察运行时
// (S002 root + 职业 + F 按钮)。没有这一步,整条盘查链根本不会启动。
var s0 = GetStateIfExists(conn.Session);
if (s0 is not null && s0.ActiveSpiritTemplateId == LixiSpirit4229938)
await PublishLixiPolice4229938(conn.Session, s0.ActiveSpiritTemplateId);
var jobClass = JobClassOf(msg);
var payload = BuildJobInfoPayload(jobClass, 1, NowUnixSeconds(), 0u);
conn.Log.Info($"[JOB] startJob class={jobClass} " +
$"{(jobClass == UrbanJobClassDelivery ? "(delivery)" : "(other)")} " +
$"-> jobInfo {payload.Length}B");
await conn.ReturnRawAsync(msg, payload);
}
/// <summary>
/// AskQuitJob(63990693) — IN(u32 jobClassId) → <c>Reader[435]</c> 的 JobInfo。
/// The 离职 button; the panel closes itself from the callback, so leaving this unhandled would
/// strand the confirm dialog.
/// </summary>
[Handler(MethodId.AskQuitJob, HandlerPacketKind.Invoke)]
private async Task AskQuitJob(Connection conn, UxRpcMessage msg)
{
// ★ 捐赠端 GameRouter.CareerApps.cs:里希结束职业 ⇒ StopLixiPolice。
var s0 = GetStateIfExists(conn.Session);
if (s0 is not null && s0.ActiveSpiritTemplateId == LixiSpirit4229938)
await StopLixiPolice4229938(conn.Session);
var jobClass = JobClassOf(msg);
var payload = BuildJobInfoPayload(jobClass, 0, 0u, NowUnixSeconds());
conn.Log.Info($"[JOB] quitJob class={jobClass} -> jobInfo {payload.Length}B");
await conn.ReturnRawAsync(msg, payload);
}
/// <summary>
/// AskAcceptTask(63422116) — IN(u32 taskId), void. Reached by the job template's 晋升 button
/// (<c>OnPromoteClick</c>) once the promotion task is in <c>Accepted</c>. There is no task
/// catalogue behind this build, so the task id is echoed for the log and nothing is persisted.
/// </summary>
[Handler(MethodId.AskAcceptTask, HandlerPacketKind.Invoke)]
private Task AskAcceptTask(Connection conn, UxRpcMessage msg)
{
var taskId = JobClassOf(msg);
conn.Log.Info($"[JOB] acceptTask id={taskId} (no task catalog — ack only)");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>All three bodies are a single u32, written with <c>WritePrimitive</c> (no marker).</summary>
private static uint JobClassOf(UxRpcMessage msg)
=> msg.Body.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(msg.Body) : 0u;
}