193 lines
9.9 KiB
C#
193 lines
9.9 KiB
C#
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 <= 0 then ShowFineResult() end
|
||
/// </code>
|
||
/// ⇒ <b>JobExpInfo 里必须命中 Police(11300003)</b>,否则 money>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);
|
||
}
|
||
}
|