This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

192 lines
9.9 KiB
C#
Raw Permalink Normal View History

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);
}
}