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(); }
}
}