Files

193 lines
9.9 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>
/// 巡卫官(里希 / 15021021)盘查链路的最后一步 —— <b>开罚单出结果</b>。
///
/// <code>
/// 客户端 PoliceExamineManager.lua:1163 gPoliceJobManager.cs:AskPoliceFine(pid, fineIdList)
/// ⇒ C2S AskPoliceFineNpc(63548238)
/// ⇒ 服务端必须回 S2C ShowReceiveRewardDetail(64145515) 且 Reason == ItemReason.PoliceFine(180001)
/// ⇒ GameToClientImpl.lua:266 gPoliceJobManager:OnDropPoliceJobExp(msg.Reward)
/// ⇒ PoliceExamineManager.lua:618 填 fineMoney / JobExpInfo
/// ⇒ PoliceExamineManager.lua:601 ShowFineResult(罚款结果面板)
/// </code>
///
/// ⛑⛑ <b>为什么之前"开罚单没结果"</b>:<c>AskPoliceFineNpc</c> 既没进
/// <c>Enabled4229938MethodIds</c>(硬规则 94)也没 handler ⇒ 客户端 await 卡死,
/// 服务端从来没回过掉落帧 ⇒ <c>needShowFineResult=true</c> 但 <c>OnDropPoliceJobExp</c>
/// 永远不触发 ⇒ <c>ShowFineResult</c> 一次都没被调用。
///
/// ⛑ 反编译陷阱(又是同一条,见已作废结论):
/// <c>GameToClientImpl.lua:266</c> 里写的是 <c>if msg.Reason ~= ItemReason.PoliceFine then</c>,
/// 字面读是"非罚款才回调",但同一函数里 <c>if msg.Reason ~= Mall or msg.Reason ~= MallBundle</c>
/// 恒为真会无条件 return ⇒ 字面读整段逻辑自相矛盾 ⇒ 反编译器把 <c>==</c> 翻成了 <c>~=</c>。
/// 真意:<b>Reason == PoliceFine 时</b>才走 <c>OnDropPoliceJobExp</c>。
///
/// <c>ItemReason.PoliceFine = 180001</c>(dump.cs:1737137,enum ItemReason TypeDefIndex:32206)。
///
/// 回滚 = 删本文件 + 从 <c>GameRouter.cs</c> 白名单删掉 <c>MethodId.AskPoliceFineNpc</c>。
/// </summary>
internal sealed partial class GameRouter
{
/// <summary><c>ItemReason.PoliceFine</c>(dump.cs enum ItemReason)。唯一能让罚款结果弹出来的 Reason。</summary>
internal const int ItemReasonPoliceFine = 180001;
/// <summary>
/// <c>UrbanJobJobClassConfig.Police</c> —— NCCA Officer / Detective / Inspector 的 JobClass
/// (<c>UrbanJobConfig.json</c> 里 Id 301/302/303 三条 JobClass 全是 11300003,
/// 里希 <c>DefaultUrbanJob=301</c>)。<c>JobExpInfo</c> 的 key 就是它。
/// </summary>
internal const uint PoliceJobClass = 11300003;
// =================================================================================
// ① C2S AskPoliceFineNpc(63548238)
// =================================================================================
/// <summary>
/// 玩家在盘查面板里选完罪名、点「开罚单」时由 <b>C# 侧</b>发出来的那条。
///
/// 形状:<c>AskPoliceFine(ulong pid, object data)</c>(dump.cs:673124),data 是 <c>int[]</c>。
/// 极性 CSharpOnly ⇒ lua 没有 writer 可抄、count 编码不确定 ⇒ <b>不硬解析</b>,
/// 只把前 8 字节当 u64 pid 记日志,其余原样打 hex 留作取证。
///
/// ⛑ 用 <c>HandlerPacketKind.Both</c>:INVOKE / NOTIFY 都不能漏。
/// INVOKE 不回执 ⇒ 客户端 await 到超时(硬规则 94 的同一类坑)。
/// </summary>
[Handler(MethodId.AskPoliceFineNpc, HandlerPacketKind.Both)]
private async Task AskPoliceFineNpc(Connection conn, UxRpcMessage msg)
{
var hex = msg.Body.Length > 0 ? Convert.ToHexString(msg.Body) : "(empty)";
ulong pid = msg.Body.Length >= 8
? BinaryPrimitives.ReadUInt64LittleEndian(msg.Body.AsSpan(0, 8))
: 0;
// 先回执:这一条比什么都重要(INVOKE 不回 = 面板点不动)。
await conn.ReturnEmptyOkAsync(msg);
msg.Context.Session.Log.Info(
$"[POLICE-FINE] AskPoliceFineNpc(63548238) pid={pid} bytes={msg.Body.Length} hex={hex}");
// 立刻回掉落帧:Reason=PoliceFine ⇒ 客户端弹罚款结果。
var (ok, message) = await PushFineRewardAsync(msg.Context.Session, DefaultFineMoney, DefaultFineJobExp);
msg.Context.Session.Log.Info($"[POLICE-FINE] ShowReceiveRewardDetail(64145515) → ok={ok} {message}");
}
internal const double DefaultFineMoney = 500;
internal const uint DefaultFineJobExp = 50;
// =================================================================================
// ② S2C ShowReceiveRewardDetail(64145515) —— 手搓字节
// =================================================================================
/// <summary>
/// 推一帧 <c>ShowReceiveRewardDetail</c>(64145515, LuaOnly),Reason = PoliceFine(180001)。
///
/// 载荷(照 <c>Auto.Reader[308]</c> / <c>Auto.Reader[500]</c>,<c>RPCDeserializeAuto.lua:37641 / 37521</c>):
/// <code>
/// int32 Reason = 180001
/// uint32 RewardTemplate = 0
/// list FirstItemInfo = FF + int32 count + uint32* (空)
/// cplx ExtraInfo = 00(null —— 必须 null!DropManager.lua:126 见 ExtraInfo 非空
/// 就会拿 ExtraInfo.Pos 判"掉落物在地上"并 return,罚款结果就没了)
/// dict Reward = FF + int32 count + { byte key, cplx RewardDetail }
/// </code>
/// <c>RewardDetail</c>(Reader[500])28 个字段全按声明顺序写,空的集合一律 <c>FF + int32 0</c>
/// (<c>Serializer.ReadList</c> / <c>ReadDict</c> = peek byte FF + <b>原始 int32</b> count,不带 +1 偏置;
/// 只有 <c>ReadList7Bit</c> / <c>ReadDict7Bit</c> 才走 Int7 +1 偏置)。
/// <c>SpiritTalentExpInfo</c> 是 ReadComplex ⇒ 写 00。
///
/// ⛑⛑ 关键判据(<c>PoliceExamineManager.lua:618</c>):
/// <code>
/// fineMoney = Σ RewardDetail.Money
/// if RewardDetail.JobExpInfo[Police] then JobExpInfo = RewardDetail.JobExpInfo end
/// if JobExpInfo or fineMoney &lt;= 0 then ShowFineResult() end
/// </code>
/// ⇒ <b>JobExpInfo 里必须命中 Police(11300003)</b>,否则 money&gt;0 时永远不弹。
/// 所以这里 money 与 exp 都给,双保险。
/// </summary>
internal static async Task<(bool Ok, string Message)> PushFineRewardAsync(
TcpSession session, double money, uint jobExp, CancellationToken token = default)
{
var bytes = BuildFineRewardWire(money, jobExp);
await session.NotifyAsync(MethodId.ShowReceiveRewardDetail, bytes, token);
return (true, $"ShowReceiveRewardDetail(64145515) Reason=PoliceFine(180001) "
+ $"money={money} jobExp[{PoliceJobClass}]={jobExp} bytes={bytes.Length}");
}
/// <summary>照 <c>Auto.Reader[308]</c> / <c>Auto.Reader[500]</c> 手搓 <c>ShowReceiveRewardDetail</c>。</summary>
internal static byte[] BuildFineRewardWire(double money, uint jobExp)
{
var w = new UxWriter();
// ---- Reader[308] ----
w.I32(ItemReasonPoliceFine);
w.U32(0); // RewardTemplate = 0 ⇒ DropManager 回落 DEFAULT_SHOW_TYPE
EmptyList(w); // FirstItemInfo : list<uint32>
w.U8(0x00); // ExtraInfo : null(必须!见上面注释)
// ---- Reward : dict<byte, RewardDetail> ----
w.U8(0xFF);
w.I32(1);
w.U8(1); // key = 1
w.U8(0xFF); // complex present
// ---- Reader[500] RewardDetail ----
EmptyDict(w); // 1 DropIdCnt dict<uint32,uint32>
w.U32(0); // 2 ReasonTextId
w.Bool(false); // 3 FactionMerge
w.F64(money); // 4 Money
w.F64(0); // 5 Gold
w.F64(0); // 6 BindingGold
w.F64(0); // 7 FreeGold
EmptyList(w); // 8 Items list<Reader[1172]>
EmptyList(w); // 9 UrbanAbilityInfo list<Reader[1430]>
EmptyList(w); // 10 UrbanAbilities list<int32>
EmptyDict(w); // 11 AbilityExpInfo dict<uint32,uint32>
EmptyDict(w); // 12 FanInfo dict<uint32,int32>
EmptyDict(w); // 13 FactionDispositionInfo dict<uint32,int32>
EmptyDict(w); // 14 FactionInfluenceInfo dict<uint32,int32>
// 15 JobExpInfo dict<uint32,uint32> —— ★罚款结果能不能弹就看这里
w.U8(0xFF);
w.I32(1);
w.U32(PoliceJobClass);
w.U32(jobExp);
EmptyDict(w); // 16 NpcFavors dict<uint32,uint32>
EmptyDict(w); // 17 OriginalNpcFavors dict<uint32,uint32>
w.F32(0); // 18 Popularity float
w.U32(0); // 19 EyeCoinRewardCount
w.U32(0); // 20 CommonSpiritTalentExp
w.U8(0x00); // 21 SpiritTalentExpInfo complex → null
EmptyList(w); // 22 WeaponList list<uint32>
EmptyList(w); // 23 RumorIds list<uint32>
EmptyDict(w); // 24 BattlePassExpInfo dict<uint32,uint32>
EmptyDict(w); // 25 ExtractionShooterInfo dict<uint32,uint32>
EmptyDict(w); // 26 SeasonProgressInfo dict<uint32,uint32>
EmptyList(w); // 27 FishingFishRewards list<Reader[1074]>
EmptyList(w); // 28 FishingGearRewards list<uint32>
return w.ToArray();
}
/// <summary><c>Serializer.ReadList</c> 的空集合:peek FF + <b>原始 int32</b> count = 0。</summary>
private static void EmptyList(UxWriter w)
{
w.U8(0xFF);
w.I32(0);
}
/// <summary><c>Serializer.ReadDict</c> 的空字典:peek FF + <b>原始 int32</b> count = 0。</summary>
private static void EmptyDict(UxWriter w)
{
w.U8(0xFF);
w.I32(0);
}
}