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

453 lines
24 KiB
C#
Raw Permalink Normal View History

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;
/// <summary>
/// 梅卡妮卡(Mechanika,spirit <b>15021017</b>)的召唤能力 —— <b>机械羊</b>。
///
/// <b>2026-09-27 17:2x 定稿(用户拍板):</b>
/// · 一次 E 在玩家<b>左前 / 右前 / 背后</b>各放一只(默认 3 只,旋钮可调)。
/// · <b>取消 10 米判定</b>:不再有"范围内已有羊就走念力"这条,想召就召。
/// · <b>取消数量上限</b>:不再有 hard cap(旋钮 <c>mechanikaSheepMaxAlive</c>,0 = 不限制)。
/// · <b>羊不会移动</b> —— 这是客户端结构决定的,不是 bug:
/// 模板 40120651 的 AI 40213004 行为树是 <c>Behavior_EmptyBehavior</c>(字面"无战斗AI"),
/// 服务端推的 <c>SyncEnemyBattleMoveTowardUnit</c> 全被忽略(实机 150+ 条,坐标 3 分钟零变化;
/// 客户端 <c>AskUnitMoveActionSimple</c> 仍在正常上报这只羊,说明它<b>在场</b>只是不动)。
/// 带真树(<c>MeikaShip</c>)的羊模板模型是透明的,客户端里也没有这棵树 ⇒ 换模板救不了。
/// ⇒ <b>跟随/移动相关代码已整块删除</b>,羊就是定点召唤物。
///
/// <b>取证(2026-09-27 读 <c>DRMK-PS_09_7k-v0.5</c> + 客户端配置表):</b>
/// · <c>SkillConfig 51937722</c>「梅卡定制召唤羊」SkillCastTypeTag=2。
/// · <c>SkillJumpConfig 97011851</c>:<c>return !IsHaveEnemyByDistance(40980002, 10);</c> → 跳到 51937722;
/// 另一条 <c>97011078</c><c>return true</c> → 跳到 51934941。
/// ⛑⛑ 但这两个是 <b>tag=2,不是可释放的根技能</b>,且没有任何 jump 指向它们、
/// 也没有 FightSkillConfig 行把 15021017 写进 SpiritId ⇒
/// <b>这版客户端里梅卡妮卡的羊没有任何原生 E 入口</b>(捐赠端的 E 绑定是他自己加的)。
/// 现做法:借 <b>51939201</b>(tag=6 的空壳占位技能)当 E 的壳子,服务端接住后召羊。
/// 详见 <c>GameplaySettings.MechanikaSheepActiveSkillId</c> 与硬规则 201。
///
/// <b>模板:</b><c>AgentConfig 40120651</c>「羊」GeneralModelId 86952761(<b>有模型</b>)、
/// AI 40213004、ShowHPBar=True(血条用 <c>UnitStateConfig.HideHp</c> 收掉)。
/// ⛔ 40980002 / 40120907「梅卡羊」模型 86952527 <b>在这版客户端里是透明的</b>,别再默认。
/// </summary>
internal sealed partial class GameRouter
{
private const uint MechanikaSpirit4229938 = 15_021_017;
private const uint MechanikaSummonSkill4229938 = 51_937_722; // 梅卡定制召唤羊
/// <summary>「梅卡定制念力」—— 51937721/51937722 的 <c>SkillReplace</c> 就挂在这个根技能上。</summary>
private const uint MechanikaMindRootSkill4229938 = 51_937_721;
/// <summary>「通用念力砸击连招01_特殊可破坏物」—— 原来 10 米内有羊时 <c>97011078</c> 跳到这里。</summary>
private const uint MechanikaMindStrikeSkill4229938 = 51_934_941;
/// <summary>
/// 「混厄召唤师召唤精灵1」—— <b>E 槽的壳子</b>。
/// ⛑⛑ 为什么是它:E 槽只认 <c>SkillCastTypeTag=6</c>。它 tag=6 且是<b>空壳占位技能</b>
/// (51939200–51939204 五个字段几乎全空、不召唤任何东西、只出现在 SkillConfig.json)
/// ⇒ 借它的壳发一次 AskUseSkill,<b>没有任何副作用</b>。实机已验证客户端肯发。
/// </summary>
private const uint MechanikaSheepCastProxySkill4229938 = 51_939_201;
/// <summary>「梅卡羊传送」—— 带 MeikaShip 树的模板唯一的技能(现用模板用不上,仅登记用)。</summary>
private const uint MechanikaSheepTeleportSkill4229938 = 51_984_701;
/// <summary>羊落点离玩家的距离(米)。沿用原来的 2.5 m,避免直接怼在玩家身上。</summary>
private const float MechanikaSheepDropMeters4229938 = 2.5f;
/// <summary>
/// 羊<b>落地时朝向</b>再转多少度。默认 <b>180</b>。
/// ⛑ 2026-09-27 17:3x 用户实机:按"落点方位角"直接给 facing 时羊全是<b>屁股对着玩家</b>
/// (落点在左前,羊也朝左前 = 背对站在中心的玩家)⇒ 统一再转 180° 让它们面朝玩家。
/// ⛑ 只改朝向,<b>不动落点</b>:落点仍由 <see cref="MechanikaSheepSpawnAngles"/> 决定。
/// </summary>
private const float MechanikaSheepFacingOffsetDeg4229938 = 180f;
private static readonly ConditionalWeakTable<TcpSession, MechanikaRuntime4229938> Mechanika4229938 = new();
private sealed class MechanikaRuntime4229938
{
internal readonly SemaphoreSlim Gate = new(1, 1);
internal readonly ConcurrentDictionary<ulong, MechanikaSheepState4229938> 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;
/// <summary>
/// 梅卡妮卡的"羊技能族"白名单。⛑ 这四个 id <b>不在任何 FightSkillConfig 行里</b>,
/// 所以 <c>weapon.AllowsSkill</c> 一定判 false —— 必须在到达那条校验<b>之前</b>放行,
/// 否则客户端发来的 51937722 / 51934941 / 51939201 会被
/// <c>[COMBAT] reject unknown-skill</c> 吃掉。
/// </summary>
internal static bool AllowsMechanikaSheepSkill4229938(uint templateId, uint skillId)
=> templateId == MechanikaSpirit4229938 &&
(skillId == MechanikaMindRootSkill4229938 ||
skillId == MechanikaSummonSkill4229938 ||
skillId == MechanikaMindStrikeSkill4229938 ||
skillId == MechanikaSheepCastProxySkill4229938);
/// <summary>
/// 一次按键放一批机械羊(默认 3 只:左前 45° / 右前 −45° / 背后 180°)。
/// 调用方:<c>AskUseSkill(SkillId=51937722)</c>、<c>51937721</c>、<c>51939201</c>(当前 E 走的那个)
/// 与流派 <c>ActiveSkill</c> 兜底。
///
/// ⛑ 2026-09-27:<b>10 米判定与数量上限都已按用户要求删除</b> —— 按一次出一批,可以一直出。
/// 想加安全闸就配 <c>gameplay.mechanikaSheepMaxAlive</c>(0 = 不限制)。
/// </summary>
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 是<b>度数</b>,喂 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(); }
}
/// <summary>
/// 生成之后的"激活批次"。<c>GameplaySettings.MechanikaSheepActivate</c>:
/// "battleAiS"(默认)只切战斗树、<b>不锁玩家</b>;"full" 走 GM 那套(会攻击玩家);"none" 不发。
/// ⛑ 1.5 s 后重发一次是照 <c>ReEngageSpawnedEnemyAsync</c> 的节奏 —— 第一批可能在客户端
/// 还没把这个单位初始化完就被吃掉了。
/// </summary>
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}");
}
});
}
/// <summary>
/// 收掉羊头顶的血条(<c>SyncAddUnitState</c> + <c>UnitStateConfig.HideHp = 10266</c>)。
/// 与 <c>GameRouter.HackerSummon.cs</c> 给机械狗做的<b>完全同一招</b>。
/// ⛑ 1.5 s 后重发一次:spawn 批是几十个包,AddUnitState 可能比单位在客户端建好更早到,
/// 那就白发了(<c>ReassertHiddenHpBarAsync</c> / <c>ReEngageSpawnedEnemyAsync</c> 同样延 1.5 s)。
/// </summary>
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}");
}
});
}
/// <summary>
/// 刷一只羊(<c>mechanikaSheepSpawnPath="handwritten"</c> 时走这条)。
/// 序列照抄 <c>GameRouter.Civilians.cs</c> 的
/// <c>SendLeftBehindAgentAsync</c>(<c>SyncRaidBattleUnitAgent</c> 通道,实机验证过的那条)。
/// ⛑ 阵营:硬规则 132 —— 友方 NPC 一律不发 <c>SyncForceEnemyRelation</c>、不登记进敌人账本。
/// </summary>
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<uint, float> { [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);
}
/// <summary>
/// 羊自己放的原生技能(MeikaShip 只列了传送 51984701):只登记生命周期,
/// <b>绝扣不到玩家头上</b>,也绝不伪造伤害/命中。
/// ⛑ 现用的 40120651 行为树是 <c>Behavior_EmptyBehavior</c>,实际上不会放技能;
/// 这条留着是为了万一换成带树的模板时,不把羊的技能记到玩家连招账上。
/// </summary>
internal static async Task<bool> 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;
}
/// <summary>技能收尾:把冷冻再延长一点点,避免刚传送走的羊被别处逻辑打断。</summary>
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}");
}
/// <summary>对外统一召回入口(切角色 / 切场景)。</summary>
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(); }
}
}