206 lines
11 KiB
Go
206 lines
11 KiB
Go
package routers
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
)
|
||
|
||
var _ = sync.Mutex{}
|
||
var _ = context.Background
|
||
|
||
//
|
||
// Attack AI — action layer for SERVER-SPAWNED enemies (GameAdminBridge.SpawnAgentAsync).
|
||
// Why this exists: BattleAiS only switches the unit onto its combat behaviour tree; it is
|
||
// not a decision loop, and the client's tree on this build does not close the distance or
|
||
// open an attack on its own for a server-spawned unit. So the server issues bounded native
|
||
// commands — move / turn / cast — and the client owns the animation, the movement and the
|
||
// hit detection. Nothing here teleports an enemy or fabricates a hit: every packet is a
|
||
// request the client's own systems execute.
|
||
// Compatibility scope, stated up front: this is NOT a reconstruction of the retail behaviour
|
||
// trees. Complex navigation, vehicle/seat skills, grabs/executions and co-op sequences are
|
||
// out of scope. Short-range approach can be blocked by geometry.
|
||
// Damage: intentionally zero. Enemy hits on the player are visual feedback only (see
|
||
// ApplyEnemyPlayerHitFeedbackAsync) — no HP, no death, no resource packets.
|
||
//
|
||
// ---- 移动节奏(2026-09-23:修「走一步停一下」的抽搐)----
|
||
//
|
||
// SyncEnemyBattleMoveTowardUnit 是**有限位移**:客户端只走 MoveTime 秒就停在那儿等下一条。
|
||
// 原来 MoveTime=0.65s 而派发间隔 1000ms ⇒ 走 0.65s、干站 0.35s,看起来就是一步一顿。
|
||
// 所以这两个值必须配对:间隔 ≥ 时长(短了会打断),又不能长太多(长了就是停顿)。
|
||
// 都从 PrivateServerConfig 读,便于实机微调,不用重编译。
|
||
// 单条移动命令让客户端走多久(秒)。
|
||
func GameRouter_EnemyAiActions_EnemyAiMoveSecondsNow(ctx context.Context, ) float32 {
|
||
// Router implementation for GameRouter_EnemyAiActions_EnemyAiMoveSecondsNow
|
||
return 0
|
||
}
|
||
|
||
// 派发间隔:默认贴着时长 +50ms;配置值更大时以配置为准。
|
||
func GameRouter_EnemyAiActions_EnemyAiMoveIntervalMsNow(ctx context.Context, moveSeconds float32) int32 {
|
||
// Router implementation for GameRouter_EnemyAiActions_EnemyAiMoveIntervalMsNow
|
||
return 0
|
||
}
|
||
|
||
//
|
||
// 每次派发把估算锚点推进多少米。1.2 m 是 MoveTime=0.65s 时调出来的,按时长等比放大,
|
||
// 免得「估算还在 5m 外、实体其实已经贴脸」导致它一直发移动命令原地挤。
|
||
//
|
||
func GameRouter_EnemyAiActions_EnemyAiMoveStepNow(ctx context.Context, moveSeconds float32) float32 {
|
||
// Router implementation for GameRouter_EnemyAiActions_EnemyAiMoveStepNow
|
||
return 0
|
||
}
|
||
|
||
//
|
||
// 攻击后的冻结时长改为可配。原来是硬编码 6000/6500 ms —— 打一下原地站 6.5 秒,
|
||
// 这也是"走一步停一下"里那段"停"的来源之一(玩家看到的就是打完杵着不动)。
|
||
// 配置缺项时退回硬编码默认值,避免老配置直接把敌人变成连击机关枪。
|
||
//
|
||
func GameRouter_EnemyAiActions_EnemyAiCastBusyMsNow(ctx context.Context, ) int32 {
|
||
// Router implementation for GameRouter_EnemyAiActions_EnemyAiCastBusyMsNow
|
||
return 0
|
||
}
|
||
|
||
func GameRouter_EnemyAiActions_EnemyAiCastIntervalMsNow(ctx context.Context, ) int32 {
|
||
// Router implementation for GameRouter_EnemyAiActions_EnemyAiCastIntervalMsNow
|
||
return 0
|
||
}
|
||
|
||
//
|
||
// 日志密度:前 3 条必打,之后每 N 条打一条(N 来自配置 EnemyAiVerboseLogEvery)。
|
||
// 原来只打前 3 条 —— 实机上敌人一次接敌要走十几条移动命令,后面全被吞掉,
|
||
// 排查「一步一停」时完全看不到节奏。
|
||
//
|
||
func GameRouter_EnemyAiActions_EnemyAiShouldLog(ctx context.Context, count int32) bool {
|
||
// Router implementation for GameRouter_EnemyAiActions_EnemyAiShouldLog
|
||
return true
|
||
}
|
||
|
||
// Resolved once per template: the behaviour set's own attacks when it has any.
|
||
// 2026-09-26 Wave 2:改走 EnemyAiCatalog4229938.ResolveSpawnProfile ——
|
||
// 战斗模式开启时这里拿到的就是模式套件(melee 空手 / ranged 远程),与 spawn 时
|
||
// 绑的技能槽、换的武器、推的动画集是同一份解析,不会出现「手拿空手动画集、
|
||
// 甩的却是模板武器技能」的错位。off 时 = Find 本体,旧行为不变。
|
||
func GameRouter_EnemyAiActions_EnemyAiAttacksFor(ctx context.Context, templateId uint32) []any {
|
||
// Router implementation for GameRouter_EnemyAiActions_EnemyAiAttacksFor
|
||
return nil
|
||
}
|
||
|
||
// Chinese name of one skill in a template's pool, for logs. "" when unknown.
|
||
// ⛑ Wave 2:anames 只覆盖模板原配攻击集;模式套件(空手/远程池)查不到名字,
|
||
// 日志里就是空串 —— 只影响可读性,不影响行为。
|
||
func GameRouter_EnemyAiActions_EnemyAiAttackName(ctx context.Context, templateId uint32, skillId uint32) string {
|
||
// Router implementation for GameRouter_EnemyAiActions_EnemyAiAttackName
|
||
return ""
|
||
}
|
||
|
||
func GameRouter_EnemyAiActions_RunEnemyAttackAiAsync(ctx context.Context, session *GameSession) any {
|
||
// Router implementation for GameRouter_EnemyAiActions_RunEnemyAttackAiAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// Fires the queued control-state releases that have come due. Several casts can queue
|
||
// deadlines that land in the same poll, so every due deadline collapses into one batch —
|
||
// otherwise a boss swinging twice in a row would send the same 24 packets four times.
|
||
//
|
||
func GameRouter_EnemyAiActions_DrainPendingUnlocksAsync(ctx context.Context, session *GameSession, state any, now int64) any {
|
||
// Router implementation for GameRouter_EnemyAiActions_DrainPendingUnlocksAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// ★ 蹲伏暗杀播种(硬规则 192)—— 主动下发 SyncEnemyCanAssassinate(68218494)。
|
||
// 为什么要这一段:客户端的候选筛选是自举死锁。
|
||
// FeiSuoCrouchManager.CheckCanCrouchAss(0x14CB11E0) →
|
||
// PreCheckCanAss(0x131F6700) 要求目标的 BattleDataModule.canAssassinate
|
||
// (私有字段,偏移 0xB1)== true;该字段全 .text 唯一写入点就是这条 S2C 的接收函数
|
||
// (0x127D102F mov byte ptr [rax+0xB1], bl,set_CanAssassinate 零调用者);
|
||
// 而这条 S2C 名义上是 ReportCanAssassinateEnemies(67161256) 的回应,
|
||
// 上报却必须先过 canAssassinate ⇒ 没有任何敌人天生带标记 ⇒ 循环永远启动不了
|
||
// (实机:AskEnterFeiSuoCrouch 19 次 / ReportCanAssassinateEnemies 0 次)。
|
||
// 官方用 Spoon 剧本动作 L18.Spoon.EnemySetCanAssassinate(dump.cs:3194822) 播种,
|
||
// 我们没有 Spoon 系统 ⇒ 用这条主动下发替代。
|
||
// ⛑⛑ 只在蹲伏期间播:离开蹲伏态由 RevokeAllAssassinationGrants 全部撤销。
|
||
// ⛑ 每个敌人每次蹲伏只播一次(AssassinationSeededAgents):播完客户端若判定
|
||
// 它不合格,会上报一个不含它的名单 ⇒ 上报路径撤销它;只看
|
||
// AssassinationGrantedAgents 的话播种会以为「没授权过」而重播,
|
||
// 撤销→播种→撤销来回甩。离开半径后重新靠近才会再播。
|
||
//
|
||
func GameRouter_EnemyAiActions_SeedAssassinationCandidatesAsync(ctx context.Context, session *GameSession, state any, now int64) any {
|
||
// Router implementation for GameRouter_EnemyAiActions_SeedAssassinationCandidatesAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
// 0 when the player is too far, unknown, or the enemy is busy dying.
|
||
func GameRouter_EnemyAiActions_EnemyAiSelectTarget(ctx context.Context, enemy any, state any) uint64 {
|
||
// Router implementation for GameRouter_EnemyAiActions_EnemyAiSelectTarget
|
||
return 0
|
||
}
|
||
|
||
// Engagement batch, re-sent only when the lock actually changes — spamming it
|
||
// resets the client's action layer and cuts every animation short.
|
||
func GameRouter_EnemyAiActions_RefreshEnemyEngageAsync(ctx context.Context, session *GameSession, uid uint64, state any, now int64) any {
|
||
// Router implementation for GameRouter_EnemyAiActions_RefreshEnemyEngageAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
func GameRouter_EnemyAiActions_RefreshEnemyActionAsync(ctx context.Context, session *GameSession, uid uint64, state any, now int64) any {
|
||
// Router implementation for GameRouter_EnemyAiActions_RefreshEnemyActionAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 出招前让单位面向目标。三种方式由 gameplay.aether.enemyAiTurnPacket 选
|
||
// (AetherSettings.EnemyAiTurnPacket):
|
||
// target(默认)= SyncUnitSmoothFaceToTarget,把目标单位甩给客户端算 ——
|
||
// 转得准,因为客户端知道自己的真实朝向;
|
||
// smooth = 旧的 SyncUnitSmoothFace(fromAngle, changeAngle),服务端按估算
|
||
// 朝向算增量 —— 估算一旦过期就会转歪(实机上就是"转 90° 打空气");
|
||
// off = 完全不发转向包(对照用:若技能都带 NoRotation=true,此时应当朝哪打哪)。
|
||
// ⛑ 单位是度(记忆 2026-09-21.md:1066:Facing 与 EulerAngles.y 同值),
|
||
// 一开始怀疑过的"度/弧度搞反"已经排除了。
|
||
//
|
||
func GameRouter_EnemyAiActions_TurnEnemyToAsync(ctx context.Context, session *GameSession, uid uint64, targetId uint64, smooth any) any {
|
||
// Router implementation for GameRouter_EnemyAiActions_TurnEnemyToAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
// 踱步两段之间随机停顿多久(毫秒),避免所有敌人同步齐步走。
|
||
func GameRouter_EnemyAiActions_RandomPause(ctx context.Context, cfg any) int32 {
|
||
// Router implementation for GameRouter_EnemyAiActions_RandomPause
|
||
return 0
|
||
}
|
||
|
||
//
|
||
// 哨兵踱步:玩家不在仇恨范围(30m)时让敌人「走一段 → 停一会 → 倒着走回来」。
|
||
// 原来这段时间是完全不动的(EnemyAiSelectTarget 给不出目标 ⇒ RefreshEnemyActionAsync 直接
|
||
// return),敌人从生成到玩家走近一直杵着。这里复用同一条 BattleMove 通道,靠
|
||
// WalkFront / WalkBack 交替让位移互相抵消 —— 它只会原地来回踱步,不会一路漂到玩家身上。
|
||
// ⛑️ 服务端拿不到真实坐标(客户端不回传),所以 AiPosition 是估算;估算漂太远会让实体和
|
||
// 锚点对不上,因此用 HomePosition 做保险绳:离出生点超过 leash 就强制往回走。
|
||
//
|
||
func GameRouter_EnemyAiActions_DispatchEnemyPatrolStepAsync(ctx context.Context, session *GameSession, uid uint64, state any, now int64, cfg any, moveSeconds float32, moveInterval int32, moveStep float32) any {
|
||
// Router implementation for GameRouter_EnemyAiActions_DispatchEnemyPatrolStepAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// Waits out the cast animation and then settles the hit on the player.
|
||
// Fire-and-forget on purpose: the AI worker must keep polling, and a session that disappears
|
||
// during the wait must not surface as an AI failure.
|
||
//
|
||
func GameRouter_EnemyAiActions_ApplyEnemyHitToPlayerDelayedAsync(ctx context.Context, session *GameSession, state any, uid uint64, skillId uint32) any {
|
||
// Router implementation for GameRouter_EnemyAiActions_ApplyEnemyHitToPlayerDelayedAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// C2S end-of-move report. Carries a pid and nothing else — no token, no success flag —
|
||
// so it is logged as a diagnostic acknowledgement and never treated as proof of arrival
|
||
// or as a reason to shorten the move interval.
|
||
//
|
||
func GameRouter_EnemyAiActions_ReportEnemyBattleMoveEnd(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_EnemyAiActions_ReportEnemyBattleMoveEnd
|
||
var zero any; return zero
|
||
}
|
||
|