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
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.EnemyStance.cs
T

67 lines
4.3 KiB
C#
Raw Normal View History

using Ananta.SDK.Network;
using Ananta.SDK.Serialization;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Protocol.Client4229938;
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// 敌人战斗姿态握手(2026-09-26 Wave 2,原生战斗管线复活)—— 移植 7k-v0.5
/// <c>GameRouter.EnemyAi.cs:21-49 SendEnemyCombatStanceAsync / ClearEnemyGunAim4229938</c>,
/// 逐行对齐,唯一差异:我们的 <c>TcpSession</c> 没有 <c>ClosedToken</c>,一律
/// <c>CancellationToken.None</c>(与 GameAdminBridge 既有风格一致)。
///
/// 这条握手是「客户端原生受击/战斗 locomotion」的激活钥匙之一:
/// <c>SyncBattleAnimSet</c> 让客户端把单位切到指定 AnimationSetConfig(空手 325/327、
/// 远程 337/338/340),原生 hurt/stagger 反应才有对应动画集可播;
/// <c>SyncAgentChangeWeapon</c> 负责卸/换武器(melee 空手 = weaponId 0)。
/// 7K 对 template 模式 / 无动画集可换的模板<b>整段跳过</b>,我们保持同样的判据,
/// 所以 spawnCombatMode=off / template 时本文件一个包都不会发。
/// </summary>
internal sealed partial class GameRouter
{
/// <summary>7K 原判据:melee 模式 + 已卸武器 + 有空手技能 = 空手单位。</summary>
private static bool IsUnarmedEnemyProfile4229938(EnemyAiProfile4229938? profile)
=> profile is { CombatMode: "melee", Weapon: 0, UnarmedAttacks.Count: > 0 };
/// <summary>
/// 清空上半身持枪瞄准(移植 7K <c>ClearEnemyGunAim4229938</c>,消费方注释原文保留)。
/// 两个包都是裸 u64(8B,无 marker)—— 7K 用 <c>new UxWriter().U64(id)</c>,照抄。
/// </summary>
private static async Task ClearEnemyGunAim4229938(TcpSession session, ulong id)
{
// Native 127C7F20 -> GunAimModule_Npc.Stop(pid): upper-body aim is
// independent of the weapon model and the selected battle locomotion set.
var payload = new UxWriter().U64(id).ToArray();
await session.NotifyAsync(MethodId.IGameSceneToClient_SyncEndShootingState, payload, CancellationToken.None);
// Native 127704B0 removes shooting tag 919, sends inward signal 1850,
// and exits the shooting skill transition when the state-tree is active.
await session.NotifyAsync(MethodId.IGameSceneToClient_SyncEndShootGameplayTag, payload, CancellationToken.None);
}
/// <summary>
/// 战斗姿态握手(移植 7K <c>SendEnemyCombatStanceAsync</c>)。
/// 判据与 7K 完全一致:profile null → 跳过;非空手且 BattleAnimSetTag==0 → 跳过
/// (template / 持武器近战模板 7K 本来一个包都不发)。
/// </summary>
internal static async Task SendEnemyCombatStanceAsync(TcpSession session, ulong id, EnemyAiProfile4229938? profile)
{
if (profile is null) return;
var unarmed = IsUnarmedEnemyProfile4229938(profile);
if (!unarmed && profile.BattleAnimSetTag == 0) return;
if (unarmed) await ClearEnemyGunAim4229938(session, id);
// Weapon removal alone does not replace the battle movement action set.
await session.NotifyAsync(MethodId.SyncAgentChangeWeapon, UxSerializer.Serialize(
new SceneMethods.EnemyAiWeapon4229938 { agentId = id, weaponId = profile.Weapon }), CancellationToken.None);
// ApplyBattleAnimSetTag returns immediately for the same cached tag.
// During the bounded loading warmups, remove/re-add the override so it
// cannot remain a cached no-op after a late weapon/module initialization.
if (unarmed && profile.BattleAnimSetTag != 0)
await session.NotifyAsync(MethodId.IGameSceneToClient_SyncBattleAnimSet, UxSerializer.Serialize(
new SceneMethods.EnemyAiBattleAnim4229938 { id = id, animSetTag = 0 }), CancellationToken.None);
await session.NotifyAsync(MethodId.IGameSceneToClient_SyncBattleAnimSet, UxSerializer.Serialize(
new SceneMethods.EnemyAiBattleAnim4229938 { id = id, animSetTag = profile.BattleAnimSetTag }), CancellationToken.None);
session.Log.Info($"[ENEMY-STANCE] uid={id} mode={profile.CombatMode} unarmed={unarmed} weapon={profile.Weapon} animSet={profile.BattleAnimSetTag} attacks={string.Join(',', profile.Attacks)}");
}
}