using Ananta.SDK.Rpc; using Ananta.Server.Gameplay; using Ananta.Server.Protocol.Client4229938; using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene; namespace Ananta.Server.Handlers.Game; /// /// Build-4229938 RPC-surface compatibility. Semantic handlers still win. Any other Invoke uses /// the exact generated return reader shape from the matching client Lua so callbacks receive a /// valid default value (empty list/dict, zero primitive, empty string, or non-null empty object) /// instead of a zero-byte body. /// internal sealed partial class GameRouter { private Task DefaultUnknownInvoke4229938(RpcContext ctx) { UnknownMethodLogger.Log(ctx.Session.Log.Scope, "INVOKE", ctx.MethodId, ctx.Body); if (DefaultReturnCatalog4229938.TryGet(ctx.MethodId, out var body, out var shape)) { ctx.Session.Log.Warn($"[RPC4229938] unimplemented {Ananta.SDK.Logging.RpcMethodNames.Display(ctx.MethodId)} -> typed-default {shape} {body.Length}b"); return ctx.ReturnAsync(body); } ctx.Session.Log.Warn($"[RPC4229938] no generated return reader for {Ananta.SDK.Logging.RpcMethodNames.Display(ctx.MethodId)} -> empty OK"); return ctx.ReturnEmptyOkAsync(); } private Task DefaultUnknownNotify4229938(RpcContext ctx) { UnknownMethodLogger.Log(ctx.Session.Log.Scope, "NOTIFY", ctx.MethodId, ctx.Body); // Notifies have no callback body. Accepting an unimplemented generated 4229938 notify is // safer than treating it as a protocol error; stateful methods get semantic handlers as // they are implemented, while telemetry/client-local signals remain harmless no-ops. ctx.Session.Log.Warn($"[RPC4229938] unimplemented notify {Ananta.SDK.Logging.RpcMethodNames.Display(ctx.MethodId)} -> accepted"); return Task.CompletedTask; } // High-frequency/UI RPCs seen in stock 4229938 are made explicit so they no longer appear as // unknown traffic. The server does not emulate production economy/social services here; it // returns the exact neutral value expected by the generated client callback. // AskTradeGetMarketList is handled for real by GameRouter.Trade.cs (synthesised market buckets); // it must not stay in this neutral list or registration reports a duplicate invoke handler. [Handler(MethodId.AskTradeGetHistoryPage, HandlerPacketKind.Invoke)] [Handler(MethodId.AskPlayerRankingSummary, HandlerPacketKind.Invoke)] [Handler(MethodId.AskPopularityPhoneFirstOpened, HandlerPacketKind.Invoke)] [Handler(MethodId.AskPopularityUIOpened, HandlerPacketKind.Invoke)] [Handler(MethodId.SyncOpenInspireHub, HandlerPacketKind.Invoke)] [Handler(MethodId.AskQueryInspireHubAllGamePlayRecommendData, HandlerPacketKind.Invoke)] [Handler(MethodId.AskQueryInspireHubAllGamePlayRankData, HandlerPacketKind.Invoke)] [Handler(MethodId.AskLinkInfos, HandlerPacketKind.Invoke)] [Handler(MethodId.GetLastMode, HandlerPacketKind.Invoke)] [Handler(MethodId.AskSwitchLinkMode, HandlerPacketKind.Invoke)] private Task KnownNeutralInvoke4229938(Connection conn, UxRpcMessage msg) { if (DefaultReturnCatalog4229938.TryGet(msg.MethodId, out var body, out var shape)) { conn.Log.Info($"[RPC4229938] neutral {Ananta.SDK.Logging.RpcMethodNames.Display(msg.MethodId)} -> {shape} {body.Length}b"); return msg.Context.ReturnAsync(body); } return conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.OnParkourStateChange, HandlerPacketKind.Invoke)] private async Task OnParkourStateChange4229938(Connection conn, UxRpcMessage msg) { var ctx = msg.Context; // ⛑ 哨兵用 uint? 而不是 0:ParkourState 10000「空状态」是合法值,0 会把它吞掉(硬规则 240)。 uint? parkourState = null; var payload = msg.Body; if (payload != null && payload.Length >= 4) { parkourState = BitConverter.ToUInt32(payload, 0); conn.Log.Info($"[PARKOUR] state={parkourState} len={payload.Length} hex={Convert.ToHexString(payload)}"); } await conn.ReturnEmptyOkAsync(msg); // 跑酷状态 → 临时能力门(严格跟状态:进才发、离开立刻撤)。未配置 = 整层跳过,零开销。 if (parkourState is { } s && WebTraversal.HasParkourStateGates) await SyncParkourStateGates4229938(ctx, s); } /// /// ★ gameplay.webTraversal.parkourStateGates:**严格跟状态**的临时能力门。 /// /// 客户端每上报一次 OnParkourStateChange(67897801) 就进一次,但只在 /// 0→1 / 1→0 的边沿各发一个 SyncPaokuLimitChange(68974941): /// 进入配置里的 ParkourState ⇒ isAdd=true;一旦离开 ⇒ 立刻 isAdd=false 撤回, /// 没有宽限期(用户 2026-10-01 23:3x 定:大跳 5052 走这条,连跳 5040 走常驻)。 /// /// 当前配置:"10081": [5052] —— ParkourState 10081「八步赶蝉jump」即璎珑的虐杀原形跑酷 /// (TOPIC §11.10:全量 604 份日志里只在下发 3040 门之后的两个会话出现过)。 /// /// ⛑ 高频 RPC(实测 ~1.2 次/秒),靠 WorldEntryState.ActiveParkourGateStates 去重, /// 否则每次上报都 add/remove 抖动。规则 238:这里不做 per-item 日志,只在边沿打一行。 /// async Task SyncParkourStateGates4229938(RpcContext ctx, uint parkourState) { var state = GetWorldState(ctx); var unitId = state.ActiveSpiritUnitId != 0 ? state.ActiveSpiritUnitId : Profile.InitialUnitId; if (unitId == 0) return; var active = state.ActiveParkourGateStates; // 换了单位(切角色)⇒ 旧单位上的门随之消失,记账重置;不补发 isAdd=false(旧单位已不在场)。 if (state.ActiveParkourGateUnitId != unitId) { active.Clear(); state.ActiveParkourGateUnitId = unitId; } // ① 已挂上、但与当前 state 不符的:撤回。 // ⛑ 只有 parkourStateGateRelease == "onLeave" 才撤。默认 "session" 模式跳过这一步 —— // 10081「八步赶蝉jump」是瞬时态(实机 add→remove 间隔 ~1s,同秒内还会来回), // 一撤就导致下一次触发前门已经没了(用户实测「第二次触发大跳就没了」)。 if (WebTraversal.ReleaseParkourGatesOnLeave) { List? revoked = null; foreach (var prev in active) { if (prev == parkourState) continue; var prevIds = WebTraversal.ParkourStateGateIds(prev); if (prevIds.Count == 0) continue; await ctx.NotifyAsync(MethodId.IGameSceneToClient_SyncPaokuLimitChange, new SceneMethods.SyncPaokuLimitChange4229938 { unitId = unitId, limitIds = prevIds.ToList(), isAdd = false, }); (revoked ??= []).Add(prev); ctx.Session.Log.Info($"[PARKOUR-GATE] unit={unitId} state={prev} remove=[{string.Join(",", prevIds)}] nowState={parkourState}"); } if (revoked != null) foreach (var prev in revoked) active.Remove(prev); } // ② 当前 state 命中配置且还没挂上:下发(Add 返回 true 才是新边沿) var addIds = WebTraversal.ParkourStateGateIds(parkourState); if (addIds.Count > 0 && active.Add(parkourState)) { await ctx.NotifyAsync(MethodId.IGameSceneToClient_SyncPaokuLimitChange, new SceneMethods.SyncPaokuLimitChange4229938 { unitId = unitId, limitIds = addIds.ToList(), isAdd = true, }); ctx.Session.Log.Info($"[PARKOUR-GATE] unit={unitId} state={parkourState} add=[{string.Join(",", addIds)}]"); } } }