using Ananta.Server.Configuration; namespace Ananta.Server.Gameplay; /// /// Protagonist traversal policy. /// All editable ids live in config/private-server.json under gameplay.webTraversal. /// This class only answers gameplay questions for the handlers. /// internal static class WebTraversal { private static WebTraversalSettings Settings => PrivateServerConfigStore.Current.Gameplay.WebTraversal; internal static uint MaleProtagonistId => Settings.MaleProtagonistId; internal static uint FemaleProtagonistId => Settings.FemaleProtagonistId; internal static uint FeiSuoBuff => Settings.PersistentGrappleBuffId; // Build 4229938 web traversal split: // - Swing/WallRush are client-owned action lifecycle buffs and must NOT be permanent in the login snapshot. // - GrappleDiveDamage is a safe, long-lived capability modifier and can be part of the resident kit. internal const uint SwingBuff4229938 = 52_853_761; internal const uint WallRushBuff4229938 = 52_853_762; internal const uint GrappleDiveDamageBuff4229938 = 52_959_114; private static readonly uint[] ClientLifecycleBuffs4229938 = [SwingBuff4229938, WallRushBuff4229938]; private static readonly uint[] ResidentBuildWebBuffs4229938 = [GrappleDiveDamageBuff4229938]; internal static int GrappleRearmDelayMs => Settings.GrappleRearmDelayMs; internal static IReadOnlyList ForbidStates => Settings.ForbidStateIds; /// /// 主角原生飞索能力门(C 键定点飞跃)。 /// /// AbilityLimitConfig.HasFeisuoAbility = 3013(dump.cs:1026235),挂上后客户端 /// 追加 State_HasFeisuoAbility = 2318(dump.cs:1226889)并放开 C 键与原生目标查询。 /// 这一行带 GamePlayTags[] / ButtonEnum[] / QueryId ⇒ 是能力开关,不是「限制」。 /// /// ⛑⛑ 光有飞索 buff()打不开客户端的原生飞索 HUD:本服日志里 /// AskEnterFeiSuoCrouch 命中恒为 0,即客户端从来没进过飞索蹲伏态。必须再发一次 /// SyncPaokuLimitChange(68974941)(见 GameRouter.Profiles.PublishProtagonistGrapnelGate4229938)。 /// internal const uint DefaultFeisuoAbilityLimitId = 3013; internal static uint FeisuoAbilityLimitId => Settings.FeisuoAbilityLimitId != 0 ? Settings.FeisuoAbilityLimitId : DefaultFeisuoAbilityLimitId; /// /// 暗杀能力门 AbilityLimitConfig.UnlockAssassin = 5051(dump.cs:1026213) /// → 挂 State_UnlockAssassin = 1333(dump.cs:1226857)。 /// /// ⛑⛑ 与飞索门 3013 同表同构但互相独立:只发 3013 的话客户端能进飞索蹲伏态 /// (AskEnterFeiSuoCrouch 正常上报),但 StealthManager 的 /// CrouchAssassinationUpdate 不启用 ⇒ 从不发 ReportCanAssassinateEnemies /// ⇒ 服务端就算回了 SyncEnemyCanAssassinate 也没用(2026-09-26 17:4x 实机验证: /// AskEnterFeiSuoCrouch 命中 19 次,ReportCanAssassinateEnemies 命中 0 次)。 /// 两者都通过同一个 SyncPaokuLimitChange(68974941) 下发。 /// internal const uint DefaultAssassinationAbilityLimitId = 5051; internal static uint AssassinationAbilityLimitId => Settings.AssassinationAbilityLimitId != 0 ? Settings.AssassinationAbilityLimitId : DefaultAssassinationAbilityLimitId; /// 是否下发暗杀能力门。关掉即回滚(HUD 无暗杀提示)。 internal static bool PushAssassinationAbilityGate => Settings.PushAssassinationAbilityGate; // ------------------------------------------------------------------ // 璎珑(Yinglong / 15021040)专属「虐杀原形跑酷」门。 // // 取证链(2026-10-01,全部用我们自己的 dump + ConfigDump,不依赖捐赠端): // · AbilityLimitConfig.Id=3040 Name=KillingPrototype Desc="开启樱珑八步赶蝉(史晨阳)" // → GamePlayTags=[1306] = State_CanKilling(GameplayTagConfig)。 // · 反向门 Id=3032 ForbidKilling「禁止璎珑跑酷」→ 1331 State_ForbidKilling(本服从不发)。 // · 动作态:ParkourState 10062「璎珑跑酷」(zone 1062 P_Killing)、10081「八步赶蝉jump」; // Killing tag 族 574 Rush / 575 Jump / 577 Edge / 2502 ClimbRun / 2503 ClimbStay // ⇒ 能力本身覆盖平地疾跑与跳跃,不是只有攀爬。 // · 服务端此前只发 3013/5051,且 PublishProtagonistGrapnelGate4229938 有 IsProtagonist 闸 // ⇒ 3040 一次都没下发过(全量日志 abilities=[3013,5051] ×351,ParkourState 10062 命中 0 次)。 // ------------------------------------------------------------------ /// AbilityLimitConfig.KillingPrototype 的 id —— 3040。填 0 = 用内置默认 3040。 internal const uint DefaultKillingAbilityLimitId = 3040; internal static uint KillingAbilityLimitId => Settings.KillingAbilityLimitId != 0 ? Settings.KillingAbilityLimitId : DefaultKillingAbilityLimitId; /// 唯一拿到这张门的角色 —— 15021040 璎珑(Yinglong)。填 0 = 用内置默认。 internal const uint DefaultKillingSpiritId = 15021040; internal static uint KillingSpiritId => Settings.KillingSpiritId != 0 ? Settings.KillingSpiritId : DefaultKillingSpiritId; /// 是否下发璎珑跑酷门。关掉即回滚(回到只有基础爬墙跑/空中冲刺)。 internal static bool PushKillingAbilityGate => Settings.PushKillingAbilityGate; /// /// 角色 → 专属能力门列表(AbilityLimitConfig.Id),走同一条 SyncPaokuLimitChange。 /// /// 这是上面那三个「killing*」单值开关的通用化替代:一个角色可以带多张门。 /// 当前用途:15021040 璎珑 → 3040 虐杀原形跑酷;15020990 → 5040 连跳 + 5052 大跳。 /// 优先级:字典里有该角色就用字典,查不到才回落 killing* 单值配置。 /// /// ⛑ 只给配进来的角色发:其余角色即便挂上 tag,也未必有对应动画数据 /// (例:Killing 需要 MovementModeType.Yinglong(10) 的 ABP)。 /// ⛑ 空的字典 = 只走旧的 killing 单值路径,行为与改动前一致 ⇒ 回滚安全。 /// internal static IReadOnlyList AbilityGatesForSpirit(uint templateId) { if (Settings.SpiritAbilityGates.TryGetValue(templateId, out var ids) && ids is { Length: > 0 }) return ids; return PushKillingAbilityGate && templateId == KillingSpiritId ? new[] { KillingAbilityLimitId } : []; } /// ★ 跑酷状态 → 临时能力门(gameplay.webTraversal.parkourStateGates)。 /// /// 严格跟状态:进入该 ParkourState 才下发,离开就 isAdd=false 撤回。 /// 与 (角色常驻门)是两条独立的路径, /// 同一个 limitId 可以同时出现在两边(常驻下发一次 + 状态进入时再下发一次,客户端按集合去重)。 /// 留空字典 = 整层不生效 ⇒ 回滚安全。 internal static IReadOnlyList ParkourStateGateIds(uint parkourState) => Settings.ParkourStateGates.TryGetValue(parkourState, out var ids) && ids is { Length: > 0 } ? ids : []; /// 是否配置了任何「跑酷状态 → 门」映射。空字典时整个 handler 分支直接跳过(零开销)。 internal static bool HasParkourStateGates => Settings.ParkourStateGates.Count > 0; /// 跑酷门是否在离开状态时撤回。"onLeave" = 严格跟状态;其余(默认 "session")= 只发不撤。 /// 见 gameplay.webTraversal.parkourStateGateRelease 的注释(瞬时态下 onLeave 会抖动到不可用)。 internal static bool ReleaseParkourGatesOnLeave => string.Equals(Settings.ParkourStateGateRelease, "onLeave", StringComparison.OrdinalIgnoreCase); /// 飞索 + 暗杀两张能力门,一起通过 SyncPaokuLimitChange 下发。 internal static uint[] AbilityLimitIds { get { var ids = new List { FeisuoAbilityLimitId }; if (PushAssassinationAbilityGate) ids.Add(AssassinationAbilityLimitId); return ids.ToArray(); } } /// 是否下发原生飞索能力门。关掉即回滚(回到只有 buff、没有 C 键白圈的状态)。 internal static bool PushFeisuoAbilityGate => Settings.PushFeisuoAbilityGate; /// /// 是否回应蹲伏暗杀裁决(ReportCanAssassinateEnemies → SyncEnemyCanAssassinate)。 /// 关掉即回滚到「HUD 没有暗杀提示」的状态。 /// internal static bool AnswerCrouchAssassination => Settings.AnswerCrouchAssassination; /// /// ★ 主动播种 SyncEnemyCanAssassinate —— 不等客户端上报(硬规则 192)。 /// /// 客户端的候选筛选是自举死锁:PreCheckCanAss 要求目标 /// BattleDataModule.canAssassinate(0xB1) == true,而它只能由这条 S2C 写入; /// 这条 S2C 名义上又是 ReportCanAssassinateEnemies 的回应,上报却必须先过 /// canAssassinate。不主动播种 ⇒ 循环永远启动不了。 /// internal static bool PushAssassinationSeed => Settings.PushAssassinationSeed; /// 播种粗筛半径(米)。 internal static float AssassinationSeedRadiusMeters => MathF.Max(0.5f, Settings.AssassinationSeedRadiusMeters); /// 播种扫描间隔(毫秒)。 internal static int AssassinationSeedIntervalMs => Math.Clamp(Settings.AssassinationSeedIntervalMs, 100, 10_000); /// Resident build/web buffs, published only when the config keeps them on. /// These carry real damage bonuses but one of them is the only non-hidden entry in the /// whole capability set, which is what puts a permanent icon on the HUD. private static uint[] ResidentBuildBuffs => Settings.PushResidentBuildBuffs ? ResidentBuildWebBuffs4229938 : []; /// Capability buffs every controllable spirit gets, protagonists included. /// See gameplay.webTraversal.commonCapabilityBuffIds. internal static IReadOnlyList CommonCapabilityBuffIds => Settings.CommonCapabilityBuffIds; /// Per-spirit overrides appended after . /// Empty by default — CharacterCapabilities already covers this data-driven. internal static IReadOnlyList SpiritExtraBuffIds(uint templateId) => Settings.SpiritCapabilityBuffIds.TryGetValue(templateId, out var ids) ? ids : []; internal static IReadOnlyList InitialCapabilityBuffIds => Combine(Settings.SharedBuffIds, new uint[] { Settings.PersistentGrappleBuffId }, Settings.MaleExtraBuffIds, ResidentBuildBuffs, CommonCapabilityBuffIds); internal static IReadOnlyList FemaleCapabilityBuffIds => Combine(Settings.SharedBuffIds, new uint[] { Settings.PersistentGrappleBuffId }, Settings.FemaleExtraBuffIds, ResidentBuildBuffs, CommonCapabilityBuffIds); internal static bool IsProtagonist(uint templateId) => templateId == Settings.MaleProtagonistId || templateId == Settings.FemaleProtagonistId; internal static IReadOnlyList CapabilityBuffIds(uint templateId) { if (templateId == Settings.MaleProtagonistId) return InitialCapabilityBuffIds; if (templateId == Settings.FemaleProtagonistId) return FemaleCapabilityBuffIds; // Non-protagonist spirits used to get an empty kit, which is why the whole exploration // kit vanished the moment you switched characters. They now receive the common kit plus // any per-spirit override; CharacterCapabilities layers authored unlocks (Dila's // telekinetic flight, the hacker set, ...) on top of this in Profiles.cs. return Combine(CommonCapabilityBuffIds, SpiritExtraBuffIds(templateId)); } internal static bool IsClientLifecycleBuff(uint buffId) => ClientLifecycleBuffs4229938.Contains(buffId); internal static bool IsProtected(uint buffId) => Settings.SharedBuffIds.Contains(buffId) || Settings.MaleExtraBuffIds.Contains(buffId) || Settings.FemaleExtraBuffIds.Contains(buffId) || ResidentBuildWebBuffs4229938.Contains(buffId); internal static bool IsWebBuff(uint buffId) => buffId == Settings.PersistentGrappleBuffId || IsProtected(buffId) || IsClientLifecycleBuff(buffId); private static uint[] Combine(params IReadOnlyCollection[] groups) => groups.SelectMany(x => x).Distinct().ToArray(); }