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;
///
/// 巡卫官(里希 / 15021021)盘查链路的最后一步 —— 开罚单出结果。
///
///
/// 客户端 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(罚款结果面板)
///
///
/// ⛑⛑ 为什么之前"开罚单没结果":AskPoliceFineNpc 既没进
/// Enabled4229938MethodIds(硬规则 94)也没 handler ⇒ 客户端 await 卡死,
/// 服务端从来没回过掉落帧 ⇒ needShowFineResult=true 但 OnDropPoliceJobExp
/// 永远不触发 ⇒ ShowFineResult 一次都没被调用。
///
/// ⛑ 反编译陷阱(又是同一条,见已作废结论):
/// GameToClientImpl.lua:266 里写的是 if msg.Reason ~= ItemReason.PoliceFine then,
/// 字面读是"非罚款才回调",但同一函数里 if msg.Reason ~= Mall or msg.Reason ~= MallBundle
/// 恒为真会无条件 return ⇒ 字面读整段逻辑自相矛盾 ⇒ 反编译器把 == 翻成了 ~=。
/// 真意:Reason == PoliceFine 时才走 OnDropPoliceJobExp。
///
/// ItemReason.PoliceFine = 180001(dump.cs:1737137,enum ItemReason TypeDefIndex:32206)。
///
/// 回滚 = 删本文件 + 从 GameRouter.cs 白名单删掉 MethodId.AskPoliceFineNpc。
///
internal sealed partial class GameRouter
{
/// ItemReason.PoliceFine(dump.cs enum ItemReason)。唯一能让罚款结果弹出来的 Reason。
internal const int ItemReasonPoliceFine = 180001;
///
/// UrbanJobJobClassConfig.Police —— NCCA Officer / Detective / Inspector 的 JobClass
/// (UrbanJobConfig.json 里 Id 301/302/303 三条 JobClass 全是 11300003,
/// 里希 DefaultUrbanJob=301)。JobExpInfo 的 key 就是它。
///
internal const uint PoliceJobClass = 11300003;
// =================================================================================
// ① C2S AskPoliceFineNpc(63548238)
// =================================================================================
///
/// 玩家在盘查面板里选完罪名、点「开罚单」时由 C# 侧发出来的那条。
///
/// 形状:AskPoliceFine(ulong pid, object data)(dump.cs:673124),data 是 int[]。
/// 极性 CSharpOnly ⇒ lua 没有 writer 可抄、count 编码不确定 ⇒ 不硬解析,
/// 只把前 8 字节当 u64 pid 记日志,其余原样打 hex 留作取证。
///
/// ⛑ 用 HandlerPacketKind.Both:INVOKE / NOTIFY 都不能漏。
/// INVOKE 不回执 ⇒ 客户端 await 到超时(硬规则 94 的同一类坑)。
///
[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) —— 手搓字节
// =================================================================================
///
/// 推一帧 ShowReceiveRewardDetail(64145515, LuaOnly),Reason = PoliceFine(180001)。
///
/// 载荷(照 Auto.Reader[308] / Auto.Reader[500],RPCDeserializeAuto.lua:37641 / 37521):
///
/// 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 }
///
/// RewardDetail(Reader[500])28 个字段全按声明顺序写,空的集合一律 FF + int32 0
/// (Serializer.ReadList / ReadDict = peek byte FF + 原始 int32 count,不带 +1 偏置;
/// 只有 ReadList7Bit / ReadDict7Bit 才走 Int7 +1 偏置)。
/// SpiritTalentExpInfo 是 ReadComplex ⇒ 写 00。
///
/// ⛑⛑ 关键判据(PoliceExamineManager.lua:618):
///
/// fineMoney = Σ RewardDetail.Money
/// if RewardDetail.JobExpInfo[Police] then JobExpInfo = RewardDetail.JobExpInfo end
/// if JobExpInfo or fineMoney <= 0 then ShowFineResult() end
///
/// ⇒ JobExpInfo 里必须命中 Police(11300003),否则 money>0 时永远不弹。
/// 所以这里 money 与 exp 都给,双保险。
///
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}");
}
/// 照 Auto.Reader[308] / Auto.Reader[500] 手搓 ShowReceiveRewardDetail。
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
w.U8(0x00); // ExtraInfo : null(必须!见上面注释)
// ---- Reward : dict ----
w.U8(0xFF);
w.I32(1);
w.U8(1); // key = 1
w.U8(0xFF); // complex present
// ---- Reader[500] RewardDetail ----
EmptyDict(w); // 1 DropIdCnt dict
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
EmptyList(w); // 9 UrbanAbilityInfo list
EmptyList(w); // 10 UrbanAbilities list
EmptyDict(w); // 11 AbilityExpInfo dict
EmptyDict(w); // 12 FanInfo dict
EmptyDict(w); // 13 FactionDispositionInfo dict
EmptyDict(w); // 14 FactionInfluenceInfo dict
// 15 JobExpInfo dict —— ★罚款结果能不能弹就看这里
w.U8(0xFF);
w.I32(1);
w.U32(PoliceJobClass);
w.U32(jobExp);
EmptyDict(w); // 16 NpcFavors dict
EmptyDict(w); // 17 OriginalNpcFavors dict
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
EmptyList(w); // 23 RumorIds list
EmptyDict(w); // 24 BattlePassExpInfo dict
EmptyDict(w); // 25 ExtractionShooterInfo dict
EmptyDict(w); // 26 SeasonProgressInfo dict
EmptyList(w); // 27 FishingFishRewards list
EmptyList(w); // 28 FishingGearRewards list
return w.ToArray();
}
/// Serializer.ReadList 的空集合:peek FF + 原始 int32 count = 0。
private static void EmptyList(UxWriter w)
{
w.U8(0xFF);
w.I32(0);
}
/// Serializer.ReadDict 的空字典:peek FF + 原始 int32 count = 0。
private static void EmptyDict(UxWriter w)
{
w.U8(0xFF);
w.I32(0);
}
}