using Ananta.Server.ClientData.Client4229938; using Ananta.Server.Configuration; namespace Ananta.Server.Gameplay; /// /// Character-owned capability buffs that unlock stock client modules. /// /// Ownership source: UrbanBadgeConfig.json (ConfigDump_v3) — rows that set both /// Buff[] and a non-zero FightspiritId. Of the 109 controllable spirits only 9 /// own anything; see _role_capability_scan.txt for the full list. /// /// Everything else in that table is either a numeric passive (damage/HP/driving modifiers), /// a skill variant, or an OnlyServer progress marker — none of them add exploration /// verbs, so they are deliberately not published here. /// /// Keep this list small: BuffConfig (4123 rows) also contains executable NPC, boss and /// timeline actions that must never be published as player unlocks. /// internal static class CharacterCapabilities { /// /// UrbanJobConfig 401 Professional Hacker. Authored per character in /// FightSpiritConfig.DefaultUrbanJob — Seymour(15021023) and 赛默迪斯科球人(15022089) /// carry it. Data-driven on purpose: the donor build hard-codes three spirit ids and /// gets one of them wrong (15022061 is jobless). /// internal const uint HackerUrbanJob = 401; /// Jobless marker from UrbanJobConfig; 0 is never a valid job id. internal const uint JoblessUrbanJob = 100; internal const uint DilaSpiritId = 15_020_997; /// /// 白靖 Bai Jing(spirit 15021039)。原生职业 = 赛车手(class 11300002 / job 4 Racer)。 /// 见 GameRouter.BaiJingTime.cs 文件头的完整取证。 /// internal const uint BaiJingSpiritId = 15_021_039; /// /// BuffConfig 52959495 「可以激活斯安威斯坦」— Duration -1、IsHidden、Description「职业标记buff」。 /// 斯安威斯坦的能力门闸:客户端职业技能按钮(BattleMgr 一侧)查的就是这个 buff, /// 不下发它,玩家就算挂着赛车手职业也看不到 / 按不动那个按钮。 /// internal const uint BaiJingTimeScaleGateBuff = 52_959_495; /// /// BuffConfig 52959496 「斯安威斯坦」— Duration 5s、Description「子弹时间」、EffectIds [53500525]。 /// 真正生效那一份(5 秒子弹时间)。⛑ 它不进常驻清单: /// 常驻清单的含义是「永久能力标记」,而这个是有时长的技能状态, /// 由 GameRouter.BaiJingTime.cs 在玩家按下按钮时现发(Permanent=false)。 /// 塞进常驻会让玩家一登录就永久挂着子弹时间。 /// internal const uint BaiJingTimeScaleActiveBuff = 52_959_496; /// /// BuffConfig 52606175 — 名字写的是「移动射击时造成伤害增加15%」,但它是 /// 徽章 19002851「You Talkin' to Me?」(Cab Driver 出租车司机系列 19002850/51/52) /// 唯一挂在身上的 buff,而捐赠端 7k-v0.5 把白靖的 ResidentBuffIds 就写成 /// [52606175] + BadgeIds = [19002850, 19002851, 19002852]。 /// /// ⛑ 为什么仍然下发它:客户端 DriveControlDriverStore.ShowProfessionSkillButton 查的 /// BuffConfig.TimeScaleBadgeBuff 我们读不到字面量(LuaGen/ClientConfig.lua 的键名是 /// 混淆过的哈希),52959495 与 52606175 都是候选: /// · 52959495「可以激活斯安威斯坦」Duration -1,且出现在 UberSimConfig.TimeStopPara /// (TimeStopPara 的另一条 52980302 名字直接写着「货车职业徽章…(子弹时间持续时间提高)」) /// ⇒ TimeStopPara 里的 BuffId 都是徽章 buff,这一点偏向 52959495 = TimeScaleBadgeBuff; /// · 但 52606175 是唯一字面意义上的「badge buff」。 /// 两个都下发即可覆盖两种可能;52606175 本身是一个隐藏的 +15% 移动射击伤害,无副作用。 /// internal const uint BaiJingCabDriverBadgeBuff = 52_606_175; /// /// BuffConfig 52606125 「可以念力飞行」— permanent, hidden client-side capability flag. /// Badge 19002141 "Zero-Gravity Allure", quality 4. /// internal const uint DilaFloatFlightBuff = 52_606_125; /// /// Hacker capability set, "safe" shape — the proven round-1 baseline (2026-09-20, /// "middle-mouse entry visible but grey"). Kept selectable because the round-1 experiments /// showed the middle-mouse GadgetManager entry can disappear on wider sets; switching /// gameplay.hackerAbilitySet back to "safe" restores this exact shape without a rebuild. /// /// Observed behaviour per set (all measured on the real client, Seymour 15021023): /// | set | middle-mouse hack entry | /// |---------------------------------------|-------------------------| /// | 33/167/168 (this one, no battery) | visible but grey | /// | 33/167/168 + 49/50 | gone | /// | donor's 9 (33/134/135/137/161/167/168/178/179) | gone | /// | donor's 9 + 52899024 | gone | /// internal static readonly uint[] HackerBuffsSafe = [ 52_606_133, // 黑客骇入能力总开关 — without it every other one stays inert. 52_606_167, // 黑客-骇入车流载具能力 52_606_168, // 骇入车流载具-加速技能 ]; /// /// Hacker capability set, "full" shape — everything job 401 can own (2026-09-20 explicit /// request: unlock ALL of Seymour's hacker abilities, drone and spider included). /// /// Layout (BuffConfig names, cross-checked against donor_7k + ConfigDump_v3): /// · 52606133 黑客骇入能力总开关 — global gate, same as safe. /// · 52606134/135/137/161 — atmosphere-NPC hack actions (distract / rebel / press). /// · 52606167/168 — traffic-vehicle hacking + its rush skill. /// · 52606178/179 — HackerConfig.HackNPCAbilityBuff[6..7] (EnemyDistract / EnemyRebel); /// the ordinary-NPC flags alone yield an empty enemy menu, so these ship together. /// · 52606149 蜘蛛机器人 / 52606150 黑客无人机 — the two EonBug-app skill entries. /// ⛑️ NOT in donor_7k. They are what makes the spider/drone menu items cast /// (HackScriptFunc.SpiderSkill/DroneSkill → UseSkillByPid 51938181/51938183); the same /// two skills are authored on Seymour's own FightSpiritConfig row as TempSkill, so the /// battle pipeline accepts them (IsKnownSkill covers SkillConfig). /// Known risk from round 1: publishing 49/50 coincided with the middle-mouse entry /// disappearing. The EonBug APP path (HackerAppPanelStore) does not consult these buffs /// at all, so if that regression shows up again the app still works — flip /// hackerAbilitySet to "safe" and the trio comes back. /// internal static readonly uint[] HackerBuffsFull = [ 52_606_133, 52_606_134, 52_606_135, 52_606_137, 52_606_161, 52_606_167, 52_606_168, 52_606_178, 52_606_179, 52_606_149, // 蜘蛛机器人 (UrbanBadgeConfig 19001108, job-401 award) 52_606_150, // 黑客无人机 (UrbanBadgeConfig 19001109, job-401 award) 52_606_170, // 黑客-俄罗斯方块大停电能力 (2026-09-23 补;Donor Blackout §6.1 要的就是这条, // 名字在 BuffConfig 里逐字对得上)。注意别和 T0-12 的 // 52606118「可以横梁刺杀」混 —— 那是完全不同的能力。 ]; /// /// Talent-tree ability buffs (see gameplay.hackerTalentBuffIds). Configurable rather than /// authored here because they are not yet a proven shape — T1-6 only lands the C2S round-trip, /// so publishing them here is the flat replacement, not an actual talent grant. /// private static IReadOnlyList TalentBuffIds => PrivateServerConfigStore.Current.Gameplay.HackerTalentBuffIds; /// How many talent buffs actually landed, for the publish log. internal static int TalentBuffCount => TalentBuffIds.Count; /// HackerHackTypeConfig prices hacks in battery points; the donor build seeds 6/6. internal const uint HackerBatteryCapacity = 6; /// /// BuffConfig 52606233 「潜水服buff」— 不会掉氧气,且会更快的恢复氧气。 /// Published to every controllable spirit (gameplay.divingMode == "on") because /// diving is a client-global capability, not a character one: the gate lives in /// DiveManager.CheckIsEnableDiving() and oxygen is a player attribute, so every role /// needs the same suit. Id is configurable so an id that turns out to be wrong on a future /// build can be swapped without a rebuild. /// internal const uint DivingSuitBuff = 52_606_233; /// /// Character-independent capability buffs: the diving suit when gameplay.divingMode is /// "on", plus the Cat Express support buffs (gameplay.deliverySupportBuffIds). /// /// The courier half is here for one reason: RefreshCallFunctionListView filters a support /// button out unless the player carries its UberSimOrderSupportConfig.UnlockBuff /// (52800660 叫车 / 52800661 无人机). Publishing them with the regular spirit buff snapshot means /// they are present in the app and re-published on every character switch — no dedicated S2C. /// Set the list to [] to restore the pre-2026-09-24 buff snapshot byte-for-byte. /// internal static IReadOnlyList GlobalCapabilityBuffIds { get { var gameplay = PrivateServerConfigStore.Current.Gameplay; List ids = []; if (string.Equals(gameplay.DivingMode, "on", StringComparison.OrdinalIgnoreCase)) { var suit = gameplay.DivingSuitBuffId; if (suit != 0) ids.Add(suit); } foreach (var id in gameplay.DeliverySupportBuffIds ?? []) { if (id != 0 && !ids.Contains(id)) ids.Add(id); } return ids; } } /// Authored urban job of a controllable spirit; 0 normalizes to Jobless(100). internal static uint UrbanJobOf(uint templateId) { var job = ClientConfigRepository.Characters() .FirstOrDefault(x => x.TemplateId == templateId)?.DefaultUrbanJob ?? 0; return job == 0 ? JoblessUrbanJob : job; } /// True when the character's authored job grants the hacking kit. internal static bool CanHack(uint templateId) => UrbanJobOf(templateId) == HackerUrbanJob; /// /// Capability buffs for a character. selects between the /// safe trio and the full 11-buff hacker set (see / /// ); the caller reads it from /// gameplay.hackerAbilitySet so the shape stays runtime-switchable. /// internal static IReadOnlyList BuffIds(uint templateId, bool fullHackerKit = false) { var traversal = WebTraversal.CapabilityBuffIds(templateId); IEnumerable characterCapabilities = templateId switch { DilaSpiritId => new uint[] { DilaFloatFlightBuff }, // 斯安威斯坦的能力门闸(永久标记,不是技能本体 —— 见常量注释) // + 出租车司机徽章 19002851 的 buff(对齐捐赠端 ResidentBuffIds,见常量注释) // + 排查旋钮 gameplay.baiJingTimeScaleProbeBuffs(默认空,见配置注释)。 BaiJingSpiritId => new uint[] { BaiJingTimeScaleGateBuff, BaiJingCabDriverBadgeBuff } .Concat(PrivateServerConfigStore.Current.Gameplay.BaiJingTimeScaleProbeBuffs ?? Array.Empty()), _ => Array.Empty() }; // Diving suit is role-independent: it rides the same snapshot as the traversal kit so it // needs no extra packet and survives a role switch (the snapshot is republished per switch). characterCapabilities = characterCapabilities.Concat(GlobalCapabilityBuffIds); if (CanHack(templateId)) { characterCapabilities = characterCapabilities.Concat(fullHackerKit ? HackerBuffsFull : HackerBuffsSafe); // Talent layer rides the same switch as the rest of the widened set, so flipping // hackerAbilitySet to "safe" rolls back to the proven round-1 shape in one edit. if (fullHackerKit) characterCapabilities = characterCapabilities.Concat(TalentBuffIds); } return traversal.Concat(characterCapabilities).Distinct().ToArray(); } }