2026-09-15 18:13:46 +07:00
|
|
|
|
using Ananta.SDK.Rpc;
|
2026-10-29 02:44:24 +07:00
|
|
|
|
using Ananta.Server.Gameplay;
|
2026-09-15 18:13:46 +07:00
|
|
|
|
using Ananta.Server.Protocol.Client4229938;
|
2026-10-29 02:44:24 +07:00
|
|
|
|
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
|
2026-09-15 18:13:46 +07:00
|
|
|
|
|
|
|
|
|
|
namespace Ananta.Server.Handlers.Game;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 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.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
internal sealed partial class GameRouter
|
|
|
|
|
|
{
|
|
|
|
|
|
private Task DefaultUnknownInvoke4229938(RpcContext ctx)
|
|
|
|
|
|
{
|
2026-10-29 02:44:24 +07:00
|
|
|
|
UnknownMethodLogger.Log(ctx.Session.Log.Scope, "INVOKE", ctx.MethodId, ctx.Body);
|
2026-09-15 18:13:46 +07:00
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-10-29 02:44:24 +07:00
|
|
|
|
UnknownMethodLogger.Log(ctx.Session.Log.Scope, "NOTIFY", ctx.MethodId, ctx.Body);
|
2026-09-15 18:13:46 +07:00
|
|
|
|
// 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.
|
2026-10-29 02:44:24 +07:00
|
|
|
|
// 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.
|
2026-09-15 18:13:46 +07:00
|
|
|
|
[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)]
|
2026-10-29 02:44:24 +07:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// ★ <c>gameplay.webTraversal.parkourStateGates</c>:**严格跟状态**的临时能力门。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 客户端每上报一次 <c>OnParkourStateChange</c>(67897801) 就进一次,但只在
|
|
|
|
|
|
/// <b>0→1 / 1→0 的边沿</b>各发一个 <c>SyncPaokuLimitChange</c>(68974941):
|
|
|
|
|
|
/// 进入配置里的 ParkourState ⇒ <c>isAdd=true</c>;一旦离开 ⇒ 立刻 <c>isAdd=false</c> 撤回,
|
|
|
|
|
|
/// <b>没有宽限期</b>(用户 2026-10-01 23:3x 定:大跳 5052 走这条,连跳 5040 走常驻)。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 当前配置:<c>"10081": [5052]</c> —— ParkourState 10081「八步赶蝉jump」即璎珑的虐杀原形跑酷
|
|
|
|
|
|
/// (TOPIC §11.10:全量 604 份日志里只在下发 3040 门之后的两个会话出现过)。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// ⛑ 高频 RPC(实测 ~1.2 次/秒),靠 <c>WorldEntryState.ActiveParkourGateStates</c> 去重,
|
|
|
|
|
|
/// 否则每次上报都 add/remove 抖动。规则 238:这里不做 per-item 日志,只在边沿打一行。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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<uint>? 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)}]");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-15 18:13:46 +07:00
|
|
|
|
}
|