using System.Collections.Concurrent; using System.Runtime.CompilerServices; using Ananta.SDK.Network; using Ananta.SDK.Serialization; using Auto = Ananta.Server.RpcTypes.Client4229938.Auto; using Ananta.Server.Configuration; using Ananta.Server.Protocol.Client4229938; using Ananta.Server.RpcTypes.Client4229938; using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game; using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene; namespace Ananta.Server.Handlers.Game; /// /// 梅卡妮卡(Mechanika,spirit 15021017)的召唤能力 —— 机械羊。 /// /// 2026-09-27 17:2x 定稿(用户拍板): /// · 一次 E 在玩家左前 / 右前 / 背后各放一只(默认 3 只,旋钮可调)。 /// · 取消 10 米判定:不再有"范围内已有羊就走念力"这条,想召就召。 /// · 取消数量上限:不再有 hard cap(旋钮 mechanikaSheepMaxAlive,0 = 不限制)。 /// · 羊不会移动 —— 这是客户端结构决定的,不是 bug: /// 模板 40120651 的 AI 40213004 行为树是 Behavior_EmptyBehavior(字面"无战斗AI"), /// 服务端推的 SyncEnemyBattleMoveTowardUnit 全被忽略(实机 150+ 条,坐标 3 分钟零变化; /// 客户端 AskUnitMoveActionSimple 仍在正常上报这只羊,说明它在场只是不动)。 /// 带真树(MeikaShip)的羊模板模型是透明的,客户端里也没有这棵树 ⇒ 换模板救不了。 /// ⇒ 跟随/移动相关代码已整块删除,羊就是定点召唤物。 /// /// 取证(2026-09-27 读 DRMK-PS_09_7k-v0.5 + 客户端配置表): /// · SkillConfig 51937722「梅卡定制召唤羊」SkillCastTypeTag=2。 /// · SkillJumpConfig 97011851:return !IsHaveEnemyByDistance(40980002, 10); → 跳到 51937722; /// 另一条 97011078return true → 跳到 51934941。 /// ⛑⛑ 但这两个是 tag=2,不是可释放的根技能,且没有任何 jump 指向它们、 /// 也没有 FightSkillConfig 行把 15021017 写进 SpiritId ⇒ /// 这版客户端里梅卡妮卡的羊没有任何原生 E 入口(捐赠端的 E 绑定是他自己加的)。 /// 现做法:借 51939201(tag=6 的空壳占位技能)当 E 的壳子,服务端接住后召羊。 /// 详见 GameplaySettings.MechanikaSheepActiveSkillId 与硬规则 201。 /// /// 模板:AgentConfig 40120651「羊」GeneralModelId 86952761(有模型)、 /// AI 40213004、ShowHPBar=True(血条用 UnitStateConfig.HideHp 收掉)。 /// ⛔ 40980002 / 40120907「梅卡羊」模型 86952527 在这版客户端里是透明的,别再默认。 /// internal sealed partial class GameRouter { private const uint MechanikaSpirit4229938 = 15_021_017; private const uint MechanikaSummonSkill4229938 = 51_937_722; // 梅卡定制召唤羊 /// 「梅卡定制念力」—— 51937721/51937722 的 SkillReplace 就挂在这个根技能上。 private const uint MechanikaMindRootSkill4229938 = 51_937_721; /// 「通用念力砸击连招01_特殊可破坏物」—— 原来 10 米内有羊时 97011078 跳到这里。 private const uint MechanikaMindStrikeSkill4229938 = 51_934_941; /// /// 「混厄召唤师召唤精灵1」—— E 槽的壳子。 /// ⛑⛑ 为什么是它:E 槽只认 SkillCastTypeTag=6。它 tag=6 且是空壳占位技能 /// (51939200–51939204 五个字段几乎全空、不召唤任何东西、只出现在 SkillConfig.json) /// ⇒ 借它的壳发一次 AskUseSkill,没有任何副作用。实机已验证客户端肯发。 /// private const uint MechanikaSheepCastProxySkill4229938 = 51_939_201; /// 「梅卡羊传送」—— 带 MeikaShip 树的模板唯一的技能(现用模板用不上,仅登记用)。 private const uint MechanikaSheepTeleportSkill4229938 = 51_984_701; /// 羊落点离玩家的距离(米)。沿用原来的 2.5 m,避免直接怼在玩家身上。 private const float MechanikaSheepDropMeters4229938 = 2.5f; /// /// 羊落地时朝向再转多少度。默认 180。 /// ⛑ 2026-09-27 17:3x 用户实机:按"落点方位角"直接给 facing 时羊全是屁股对着玩家 /// (落点在左前,羊也朝左前 = 背对站在中心的玩家)⇒ 统一再转 180° 让它们面朝玩家。 /// ⛑ 只改朝向,不动落点:落点仍由 决定。 /// private const float MechanikaSheepFacingOffsetDeg4229938 = 180f; private static readonly ConditionalWeakTable Mechanika4229938 = new(); private sealed class MechanikaRuntime4229938 { internal readonly SemaphoreSlim Gate = new(1, 1); internal readonly ConcurrentDictionary States = new(); internal ulong Owner; internal int Generation; } private sealed class MechanikaSheepState4229938 { internal long ActionUntil; internal int SkillInstance; } private static bool MechanikaReady4229938(WorldEntryState? state) => state is { Ready: true, CombatProfilePublished: true, ActiveSpiritTemplateId: MechanikaSpirit4229938, PendingSwitchTemplateId: 0, PendingTeleportId: 0 } && state.ActiveSpiritUnitId != 0; /// /// 梅卡妮卡的"羊技能族"白名单。⛑ 这四个 id 不在任何 FightSkillConfig 行里, /// 所以 weapon.AllowsSkill 一定判 false —— 必须在到达那条校验之前放行, /// 否则客户端发来的 51937722 / 51934941 / 51939201 会被 /// [COMBAT] reject unknown-skill 吃掉。 /// internal static bool AllowsMechanikaSheepSkill4229938(uint templateId, uint skillId) => templateId == MechanikaSpirit4229938 && (skillId == MechanikaMindRootSkill4229938 || skillId == MechanikaSummonSkill4229938 || skillId == MechanikaMindStrikeSkill4229938 || skillId == MechanikaSheepCastProxySkill4229938); /// /// 一次按键放一批机械羊(默认 3 只:左前 45° / 右前 −45° / 背后 180°)。 /// 调用方:AskUseSkill(SkillId=51937722)、51937721、51939201(当前 E 走的那个) /// 与流派 ActiveSkill 兜底。 /// /// ⛑ 2026-09-27:10 米判定与数量上限都已按用户要求删除 —— 按一次出一批,可以一直出。 /// 想加安全闸就配 gameplay.mechanikaSheepMaxAlive(0 = 不限制)。 /// internal static async Task<(bool Ok, string Message)> SummonMechanikaSheep4229938(TcpSession session) { var state = GetStateIfExists(session); if (!MechanikaReady4229938(state) || !state!.HasLastReportedPlayerTransform) return (false, "请先切换到梅卡妮卡,进入场景并移动一下。"); var runtime = Mechanika4229938.GetValue(session, _ => new()); await runtime.Gate.WaitAsync(CancellationToken.None); try { if (!MechanikaReady4229938(state)) return (false, "角色正在切换或传送。"); var gameplay = PrivateServerConfigStore.Current.Gameplay; var maxAlive = gameplay.MechanikaSheepMaxAlive; if (maxAlive > 0 && runtime.States.Count >= maxAlive) return (true, $"机械羊数量已达上限 {maxAlive} 只(gameplay.mechanikaSheepMaxAlive;置 0 不限制)。"); runtime.Owner = state.ActiveSpiritUnitId; runtime.Generation = state.WorldEntryControlGeneration; var p = state.HasLastReportedPlayerTransform ? state.LastReportedPlayerPosition : state.WorldEntryCreateHeroPosition; if (!float.IsFinite(p.X + p.Y + p.Z)) return (false, "角色坐标无效。"); var facing = state.LastReportedPlayerRotation.Y; if (!float.IsFinite(facing)) facing = 0f; // 落点角度(度)。默认 [45, -45, 180] = 左前 / 右前 / 背后。 var angles = gameplay.MechanikaSheepSpawnAngles is { Length: > 0 } a ? a : new[] { 45, -45, 180 }; var perCast = gameplay.MechanikaSheepPerCast > 0 ? gameplay.MechanikaSheepPerCast : angles.Length; var spawnPath = gameplay.MechanikaSheepSpawnPath; var useHandwritten = string.Equals(spawnPath, "handwritten", StringComparison.OrdinalIgnoreCase); var spawned = 0; for (var i = 0; i < perCast; i++) { // 硬规则 181:facing 是度数,喂 sin/cos 前必须转弧度。 var offset = angles[i % angles.Length]; var yaw = (facing + offset) * MathF.PI / 180f; var position = new Vec3(p.X + MathF.Sin(yaw) * MechanikaSheepDropMeters4229938, p.Y, p.Z + MathF.Cos(yaw) * MechanikaSheepDropMeters4229938); // 朝向 = 落点方位角 + 180°(面朝玩家),并归一到 [0,360):玩家 facing 可能为负。 var sheepFacing = ((facing + offset + MechanikaSheepFacingOffsetDeg4229938) % 360f + 360f) % 360f; ulong id; if (useHandwritten) { id = GameAdminBridge.NextRetirableNpcUid(); } else { var result = await GameAdminBridge.SpawnAgentAsync( gameplay.MechanikaSheepAgentId, position.X, position.Y, position.Z, sheepFacing, hostile: true, // AgentConfig 40120651 在 Hostile 桶里,校验要求 true engage: false, // 羊不进敌人账本:它不是拿来打的 forceEnemyRelation: false, // 不能把玩家的羊标成玩家的敌人 campOverride: (byte)gameplay.MechanikaSheepCamp, treeNameOverride: gameplay.MechanikaSheepTreeName, occurOverride: gameplay.MechanikaSheepOccurId); if (!result.Ok) { session.Log.Warn($"[MECHANIKA] spawn #{i} failed: {result.Message}"); continue; } id = result.Uid; await ActivateMechanikaSheepAsync(session, id); } // ★ 血条:AgentConfig 40120651.ShowHPBar=True(客户端配置,服务端改不了), // 唯一可写的开关是 UnitStateConfig.HideHp —— 机械狗就是靠它把血条收掉的 // (GameRouter.HackerSummon.cs)。详见 GameplaySettings.MechanikaSheepHideHpBar。 if (gameplay.MechanikaSheepHideHpBar) await HideMechanikaSheepHpBarAsync(session, id); runtime.States[id] = new MechanikaSheepState4229938(); if (useHandwritten) { try { await SendMechanikaSheepAgentAsync(session, id, runtime.Owner, position, sheepFacing); } catch { runtime.States.TryRemove(id, out _); throw; } } spawned++; } if (spawned == 0) return (false, "机械羊生成失败(见上一行 [MECHANIKA] spawn 日志)。"); session.Log.Info($"[MECHANIKA] summoned sheep={spawned} template={gameplay.MechanikaSheepAgentId} " + $"path={spawnPath} owner={runtime.Owner} angles=[{string.Join(",", angles)}] " + $"alive={runtime.States.Count} at=({p.X:F2},{p.Y:F2},{p.Z:F2}) facing={facing:F2}"); return (true, $"已在身边放下 {spawned} 只机械羊(左前/右前/背后),场上共 {runtime.States.Count} 只。"); } finally { runtime.Gate.Release(); } } /// /// 生成之后的"激活批次"。GameplaySettings.MechanikaSheepActivate: /// "battleAiS"(默认)只切战斗树、不锁玩家;"full" 走 GM 那套(会攻击玩家);"none" 不发。 /// ⛑ 1.5 s 后重发一次是照 ReEngageSpawnedEnemyAsync 的节奏 —— 第一批可能在客户端 /// 还没把这个单位初始化完就被吃掉了。 /// private static async Task ActivateMechanikaSheepAsync(TcpSession session, ulong id) { var mode = PrivateServerConfigStore.Current.Gameplay.MechanikaSheepActivate; if (string.Equals(mode, "none", StringComparison.OrdinalIgnoreCase)) return; if (string.Equals(mode, "full", StringComparison.OrdinalIgnoreCase)) { var state = GetStateIfExists(session); var playerUnit = state is not null && state.WorldEntryControlUnit != 0 ? state.WorldEntryControlUnit : state?.ActiveSpiritUnitId ?? 0; await EngageSpawnedEnemyAsync(session, id, playerUnit); _ = ReEngageSpawnedEnemyAsync(session, id); session.Log.Info($"[MECHANIKA] activate uid={id} mode=full (GM 那套;羊会攻击玩家)"); return; } try { await session.NotifyAsync(MethodId.SyncBattleAiS, UxSerializer.Serialize(new SceneMethods.SyncBattleAiS { agentId = id, value = true }), CancellationToken.None); await session.NotifyAsync(MethodId.SyncEnemyDetect, UxSerializer.Serialize(new GameMethods.EnemyDetectData4229938 { Id = id, Stealth = 0 }), CancellationToken.None); session.Log.Info($"[MECHANIKA] activate uid={id} mode=battleAiS (切战斗树,不锁玩家)"); } catch (Exception ex) { session.Log.Warn($"[MECHANIKA] activate failed uid={id}: {ex.Message}"); return; } // 1.5 s 后补一次(对齐 ReEngageSpawnedEnemyAsync 的节奏)。 _ = Task.Run(async () => { try { await Task.Delay(1500, CancellationToken.None); if (!SessionAlive4229938(session)) return; await session.NotifyAsync(MethodId.SyncBattleAiS, UxSerializer.Serialize(new SceneMethods.SyncBattleAiS { agentId = id, value = true }), CancellationToken.None); session.Log.Info($"[MECHANIKA] re-activate uid={id} mode=battleAiS"); } catch (Exception ex) { session.Log.Warn($"[MECHANIKA] re-activate failed uid={id}: {ex.Message}"); } }); } /// /// 收掉羊头顶的血条(SyncAddUnitState + UnitStateConfig.HideHp = 10266)。 /// 与 GameRouter.HackerSummon.cs 给机械狗做的完全同一招。 /// ⛑ 1.5 s 后重发一次:spawn 批是几十个包,AddUnitState 可能比单位在客户端建好更早到, /// 那就白发了(ReassertHiddenHpBarAsync / ReEngageSpawnedEnemyAsync 同样延 1.5 s)。 /// private static async Task HideMechanikaSheepHpBarAsync(TcpSession session, ulong id) { async Task Send() { await session.NotifyAsync(MethodId.SyncAddUnitState, UxSerializer.Serialize(new GameMethods.SyncAddUnitStateArgs4229938 { Id = id, State = LocalEnemyUnitStateHideHp, EffectFreezeState = 0u, }), CancellationToken.None); } try { await Send(); session.Log.Info($"[MECHANIKA] hide hp bar uid={id} state=HideHp({LocalEnemyUnitStateHideHp})"); } catch (Exception ex) { session.Log.Warn($"[MECHANIKA] hide hp bar failed uid={id}: {ex.Message}"); return; } _ = Task.Run(async () => { try { await Task.Delay(1500, CancellationToken.None); if (!SessionAlive4229938(session)) return; await Send(); session.Log.Info($"[MECHANIKA] reassert hide hp bar uid={id}"); } catch (Exception ex) { session.Log.Warn($"[MECHANIKA] reassert hide hp bar failed uid={id}: {ex.Message}"); } }); } /// /// 刷一只羊(mechanikaSheepSpawnPath="handwritten" 时走这条)。 /// 序列照抄 GameRouter.Civilians.cs 的 /// SendLeftBehindAgentAsync(SyncRaidBattleUnitAgent 通道,实机验证过的那条)。 /// ⛑ 阵营:硬规则 132 —— 友方 NPC 一律不发 SyncForceEnemyRelation、不登记进敌人账本。 /// private static async Task SendMechanikaSheepAgentAsync(TcpSession session, ulong id, ulong owner, Vec3 position, float facing) { var gameplay = PrivateServerConfigStore.Current.Gameplay; var ai = new SceneMethods.LinkAIAgentInfo { Uid = id, Nickname = "梅卡羊", Fashion = null }; var syncInfo = new SceneMethods.AgentSyncClientInfo { isApproachNpc = true, TriggerLeaveEvent = true, approachDistance = 300, LeaveDistance = 600, sitIndex = -1, SpoonAgentId = -1, treeName = string.IsNullOrWhiteSpace(gameplay.MechanikaSheepTreeName) ? "Main_Common" : gameplay.MechanikaSheepTreeName, AISetting = 40_213_004, stimIDList = [], spawnEffectId = [], indoorList = [], roomIds = [], returnPoiActionIds = [], AIAgentInfo = ai, }; await session.NotifyAsync(MethodId.SyncLogicAgentEnter, UxSerializer.Serialize(WorldCodec.LogicAgentEnter(id)), CancellationToken.None); await session.NotifyAsync(MethodId.SyncManagedLogicAgent, UxSerializer.Serialize(WorldCodec.ManagedLogicAgent(id, Profile.PlayerPid, 0)), CancellationToken.None); await session.NotifyAsync(MethodId.SyncRaidBattleUnitAgent, UxSerializer.Serialize(new SceneMethods.RaidBattleUnitAgent { SkillId = -1, HSummonIndex = -1, SpoonAgentId = -1, FashionIdList = [], VehicleIndex = -1, IsBorn = true, BattleAiS = true, agentSyncClientInfo = syncInfo, WeaponId = 98005506u, SpawnType = 1, AIAgentInfo = ai, Id = id, TemplateId = gameplay.MechanikaSheepAgentId, Position = new Auto.UXVector3 { X = position.X, Y = position.Y, Z = position.Z }, FacingDirection = facing, ManagedPid = Profile.PlayerPid, GroundData = new Auto.MoveActionGroundData { LocalPos = new Auto.UXVector3() }, }), CancellationToken.None); await session.NotifyAsync(MethodId.SyncUnitOccur, UxSerializer.Serialize(new SceneMethods.SyncUnitOccur { enemyId = id, occurId = gameplay.MechanikaSheepOccurId, dullSecs = 0f, position = new Auto.UXVector3 { X = position.X, Y = position.Y, Z = position.Z }, rotateY = facing, }), CancellationToken.None); await session.NotifyAsync(MethodId.SyncAgentCampInfo, UxSerializer.Serialize(new SceneMethods.SyncAgentCampInfo { pid = id, camp = (byte)gameplay.MechanikaSheepCamp }), CancellationToken.None); await session.NotifyAsync(MethodId.SyncAttachBattleModule, UxSerializer.Serialize(new SceneMethods.SyncAttachBattleModule { unitId = id }), CancellationToken.None); await session.NotifyAsync(MethodId.SyncUnitAttrs, UxSerializer.Serialize(new SceneMethods.SyncUnitAttrs { unitId = id, attrs = new Dictionary { [1] = 100f }, }), CancellationToken.None); await session.NotifyAsync(MethodId.SyncUnitHp, UxSerializer.Serialize(new SceneMethods.SyncUnitHp { unitId = id, hp = 100f }), CancellationToken.None); await session.NotifyAsync(MethodId.IGameSceneToClient_SyncAgentForceGo, UxSerializer.Serialize(new SceneMethods.SyncAgentForceGo4229938 { id = id, isForceGo = true }), CancellationToken.None); } /// /// 羊自己放的原生技能(MeikaShip 只列了传送 51984701):只登记生命周期, /// 绝扣不到玩家头上,也绝不伪造伤害/命中。 /// ⛑ 现用的 40120651 行为树是 Behavior_EmptyBehavior,实际上不会放技能; /// 这条留着是为了万一换成带树的模板时,不把羊的技能记到玩家连招账上。 /// internal static async Task RecordMechanikaSkill4229938(TcpSession session, SceneMethods.SkillUseData request) { if (!Mechanika4229938.TryGetValue(session, out var runtime)) return false; if (!runtime.States.TryGetValue(request.Releaser, out var sheep) || request.SkillId != MechanikaSheepTeleportSkill4229938) return false; var state = GetStateIfExists(session); if (!MechanikaReady4229938(state) || runtime.Generation != state!.WorldEntryControlGeneration || runtime.Owner != state.ActiveSpiritUnitId) return false; sheep.SkillInstance = request.SkillInstanceId; sheep.ActionUntil = Environment.TickCount64 + 5000; session.Log.Info($"[MECHANIKA] native-skill entity={request.Releaser} skill={request.SkillId}; player resources untouched"); await Task.CompletedTask; return true; } /// 技能收尾:把冷冻再延长一点点,避免刚传送走的羊被别处逻辑打断。 internal static void RecordMechanikaSkillEnd4229938(TcpSession session, SceneMethods.ReportSkillEnd report) { if (!Mechanika4229938.TryGetValue(session, out var runtime)) return; if (!runtime.States.TryGetValue(report.unitId, out var sheep) || report.skillId != sheep.SkillInstance) return; sheep.ActionUntil = Environment.TickCount64 + 500; } private static async Task ClearMechanikaSheepUnderGate4229938(TcpSession session, MechanikaRuntime4229938 runtime) { var ids = runtime.States.Keys.ToArray(); runtime.States.Clear(); runtime.Owner = 0; // 世界已经换代 ⇒ 旧 id 不要再往新世界送 Leave。 if (GetStateIfExists(session)?.WorldEntryControlGeneration != runtime.Generation) return; foreach (var id in ids) await session.NotifyAsync(MethodId.SyncLogicAgentLeave, UxSerializer.Serialize(WorldCodec.LogicAgentLeave(id)), CancellationToken.None); session.Log.Info($"[MECHANIKA] recalled count={ids.Length}"); } /// 对外统一召回入口(切角色 / 切场景)。 internal static async Task StopMechanikaSheep4229938(TcpSession session) { if (!Mechanika4229938.TryGetValue(session, out var runtime)) return; await runtime.Gate.WaitAsync(CancellationToken.None); try { await ClearMechanikaSheepUnderGate4229938(session, runtime); } finally { runtime.Gate.Release(); } } }