This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

221 lines
13 KiB
C#
Raw Permalink Normal View History

using Ananta.Server.Configuration;
namespace Ananta.Server.Gameplay;
/// <summary>
/// Protagonist traversal policy.
/// All editable ids live in config/private-server.json under gameplay.webTraversal.
/// This class only answers gameplay questions for the handlers.
/// </summary>
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<uint> ForbidStates => Settings.ForbidStateIds;
/// <summary>
/// 主角原生飞索能力门(C 键定点飞跃)。
///
/// <c>AbilityLimitConfig.HasFeisuoAbility = 3013</c>(<c>dump.cs:1026235</c>),挂上后客户端
/// 追加 <c>State_HasFeisuoAbility = 2318</c>(<c>dump.cs:1226889</c>)并放开 C 键与原生目标查询。
/// 这一行带 <c>GamePlayTags[] / ButtonEnum[] / QueryId</c> ⇒ 是<b>能力开关</b>,不是「限制」。
///
/// ⛑⛑ 光有飞索 buff(<see cref="FeiSuoBuff"/>)打不开客户端的原生飞索 HUD:本服日志里
/// <c>AskEnterFeiSuoCrouch</c> 命中恒为 0,即客户端从来没进过飞索蹲伏态。必须再发一次
/// <c>SyncPaokuLimitChange</c>(68974941)(见 <c>GameRouter.Profiles.PublishProtagonistGrapnelGate4229938</c>)。
/// </summary>
internal const uint DefaultFeisuoAbilityLimitId = 3013;
internal static uint FeisuoAbilityLimitId
=> Settings.FeisuoAbilityLimitId != 0 ? Settings.FeisuoAbilityLimitId : DefaultFeisuoAbilityLimitId;
/// <summary>
/// 暗杀能力门 <c>AbilityLimitConfig.UnlockAssassin = 5051</c>(<c>dump.cs:1026213</c>)
/// → 挂 <c>State_UnlockAssassin = 1333</c>(<c>dump.cs:1226857</c>)。
///
/// ⛑⛑ 与飞索门 3013 <b>同表同构但互相独立</b>:只发 3013 的话客户端能进飞索蹲伏态
/// (<c>AskEnterFeiSuoCrouch</c> 正常上报),但 <c>StealthManager</c> 的
/// <c>CrouchAssassinationUpdate</c> 不启用 ⇒ <b>从不发 ReportCanAssassinateEnemies</b>
/// ⇒ 服务端就算回了 SyncEnemyCanAssassinate 也没用(2026-09-26 17:4x 实机验证:
/// AskEnterFeiSuoCrouch 命中 19 次,ReportCanAssassinateEnemies 命中 0 次)。
/// 两者都通过同一个 <c>SyncPaokuLimitChange</c>(68974941) 下发。
/// </summary>
internal const uint DefaultAssassinationAbilityLimitId = 5051;
internal static uint AssassinationAbilityLimitId
=> Settings.AssassinationAbilityLimitId != 0 ? Settings.AssassinationAbilityLimitId : DefaultAssassinationAbilityLimitId;
/// <summary>是否下发暗杀能力门。关掉即回滚(HUD 无暗杀提示)。</summary>
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 次)。
// ------------------------------------------------------------------
/// <summary><c>AbilityLimitConfig.KillingPrototype</c> 的 id —— 3040。填 0 = 用内置默认 3040。</summary>
internal const uint DefaultKillingAbilityLimitId = 3040;
internal static uint KillingAbilityLimitId
=> Settings.KillingAbilityLimitId != 0 ? Settings.KillingAbilityLimitId : DefaultKillingAbilityLimitId;
/// <summary>唯一拿到这张门的角色 —— 15021040 璎珑(Yinglong)。填 0 = 用内置默认。</summary>
internal const uint DefaultKillingSpiritId = 15021040;
internal static uint KillingSpiritId
=> Settings.KillingSpiritId != 0 ? Settings.KillingSpiritId : DefaultKillingSpiritId;
/// <summary>是否下发璎珑跑酷门。关掉即回滚(回到只有基础爬墙跑/空中冲刺)。</summary>
internal static bool PushKillingAbilityGate => Settings.PushKillingAbilityGate;
/// <summary>
/// 角色 → 专属能力门列表(<c>AbilityLimitConfig.Id</c>),走同一条 <c>SyncPaokuLimitChange</c>。
///
/// 这是上面那三个「killing*」单值开关的<b>通用化替代</b>:一个角色可以带多张门。
/// 当前用途:15021040 璎珑 → 3040 虐杀原形跑酷;15020990 → 5040 连跳 + 5052 大跳。
/// 优先级:<b>字典里有该角色就用字典</b>,查不到才回落 killing* 单值配置。
///
/// ⛑ 只给配进来的角色发:其余角色即便挂上 tag,也未必有对应动画数据
/// (例:Killing 需要 <c>MovementModeType.Yinglong</c>(10) 的 ABP)。
/// ⛑ 空的字典 = 只走旧的 killing 单值路径,行为与改动前一致 ⇒ 回滚安全。
/// </summary>
internal static IReadOnlyList<uint> AbilityGatesForSpirit(uint templateId)
{
if (Settings.SpiritAbilityGates.TryGetValue(templateId, out var ids) && ids is { Length: > 0 })
return ids;
return PushKillingAbilityGate && templateId == KillingSpiritId
? new[] { KillingAbilityLimitId }
: [];
}
/// <summary>★ 跑酷状态 → 临时能力门(<c>gameplay.webTraversal.parkourStateGates</c>)。
///
/// <b>严格跟状态</b>:进入该 ParkourState 才下发,离开就 <c>isAdd=false</c> 撤回。
/// 与 <see cref="AbilityGatesForSpirit"/>(角色常驻门)是两条独立的路径,
/// 同一个 limitId 可以同时出现在两边(常驻下发一次 + 状态进入时再下发一次,客户端按集合去重)。
/// 留空字典 = 整层不生效 ⇒ 回滚安全。</summary>
internal static IReadOnlyList<uint> ParkourStateGateIds(uint parkourState)
=> Settings.ParkourStateGates.TryGetValue(parkourState, out var ids) && ids is { Length: > 0 }
? ids
: [];
/// <summary>是否配置了任何「跑酷状态 → 门」映射。空字典时整个 handler 分支直接跳过(零开销)。</summary>
internal static bool HasParkourStateGates => Settings.ParkourStateGates.Count > 0;
/// <summary>跑酷门是否在离开状态时撤回。<c>"onLeave"</c> = 严格跟状态;其余(默认 "session")= 只发不撤。
/// 见 <c>gameplay.webTraversal.parkourStateGateRelease</c> 的注释(瞬时态下 onLeave 会抖动到不可用)。</summary>
internal static bool ReleaseParkourGatesOnLeave
=> string.Equals(Settings.ParkourStateGateRelease, "onLeave", StringComparison.OrdinalIgnoreCase);
/// <summary>飞索 + 暗杀两张能力门,一起通过 SyncPaokuLimitChange 下发。</summary>
internal static uint[] AbilityLimitIds
{
get
{
var ids = new List<uint> { FeisuoAbilityLimitId };
if (PushAssassinationAbilityGate)
ids.Add(AssassinationAbilityLimitId);
return ids.ToArray();
}
}
/// <summary>是否下发原生飞索能力门。关掉即回滚(回到只有 buff、没有 C 键白圈的状态)。</summary>
internal static bool PushFeisuoAbilityGate => Settings.PushFeisuoAbilityGate;
/// <summary>
/// 是否回应蹲伏暗杀裁决(<c>ReportCanAssassinateEnemies</c> → <c>SyncEnemyCanAssassinate</c>)。
/// 关掉即回滚到「HUD 没有暗杀提示」的状态。
/// </summary>
internal static bool AnswerCrouchAssassination => Settings.AnswerCrouchAssassination;
/// <summary>
/// ★ 主动播种 <c>SyncEnemyCanAssassinate</c> —— 不等客户端上报(硬规则 192)。
///
/// 客户端的候选筛选是<b>自举死锁</b>:<c>PreCheckCanAss</c> 要求目标
/// <c>BattleDataModule.canAssassinate</c>(0xB1) == true,而它只能由这条 S2C 写入;
/// 这条 S2C 名义上又是 <c>ReportCanAssassinateEnemies</c> 的回应,上报却必须先过
/// <c>canAssassinate</c>。不主动播种 ⇒ 循环永远启动不了。
/// </summary>
internal static bool PushAssassinationSeed => Settings.PushAssassinationSeed;
/// <summary>播种粗筛半径(米)。</summary>
internal static float AssassinationSeedRadiusMeters
=> MathF.Max(0.5f, Settings.AssassinationSeedRadiusMeters);
/// <summary>播种扫描间隔(毫秒)。</summary>
internal static int AssassinationSeedIntervalMs
=> Math.Clamp(Settings.AssassinationSeedIntervalMs, 100, 10_000);
/// <summary>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.</summary>
private static uint[] ResidentBuildBuffs
=> Settings.PushResidentBuildBuffs ? ResidentBuildWebBuffs4229938 : [];
/// <summary>Capability buffs every controllable spirit gets, protagonists included.
/// See <c>gameplay.webTraversal.commonCapabilityBuffIds</c>.</summary>
internal static IReadOnlyList<uint> CommonCapabilityBuffIds => Settings.CommonCapabilityBuffIds;
/// <summary>Per-spirit overrides appended after <see cref="CommonCapabilityBuffIds"/>.
/// Empty by default — <c>CharacterCapabilities</c> already covers this data-driven.</summary>
internal static IReadOnlyList<uint> SpiritExtraBuffIds(uint templateId)
=> Settings.SpiritCapabilityBuffIds.TryGetValue(templateId, out var ids) ? ids : [];
internal static IReadOnlyList<uint> InitialCapabilityBuffIds
=> Combine(Settings.SharedBuffIds, new uint[] { Settings.PersistentGrappleBuffId }, Settings.MaleExtraBuffIds, ResidentBuildBuffs, CommonCapabilityBuffIds);
internal static IReadOnlyList<uint> 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<uint> 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<uint>[] groups)
=> groups.SelectMany(x => x).Distinct().ToArray();
}