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/pkg/protocol/methods/enemy_combat_methods.go
T

290 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package methods
// ========================================================================
// Local-enemy combat contracts (build 4229938).
//
// The enemies this server cares about right now are NOT spawned by the server.
// They are created client-side (GmUtils.CreateClientAgent /
// LuaUtils.CreateClientAgentByCfg) and live in the client's LocalUnitMgr, so the
// CLIENT owns their HP, their death model and their animation layer. Nothing
// drives that model: the client's own combat sim never damages a local unit, and
// the server did not know the unit existed. The result the player sees is an
// enemy that absorbs every bullet without flinching and never dies.
//
// These contracts let the server act as the missing authority over a unit it did
// not spawn: on an accepted hit it broadcasts the hurt VFX, the HP ratio and a
// one-shot stagger; at zero HP it runs the down-pose / death-beat / loot-drop /
// despawn sequence.
//
// Field ORDER is the wire, so every shape below records which 4229938 artifact it
// was read from. Two independent sources are used because half of these methods
// are Lua-handled (verifiable from the client's own reader table) and half are
// CSharpOnly (verifiable only from the IL2CPP dump):
//
// SyncUnitAttacked 68299892 dump SyncUnitAttacked(u64,u64,DamageData)
// + dump UX.Game.DamageData (18 fields)
// SyncUnitHurtEffect 68349329 Lua reader midToReader[68349329] -> 7 flat fields
// SyncEnemyPlayAction 68039914 dump SyncEnemyPlayAction(u64,u32,f32) -> 3 flat fields
// SyncEnemyPrepareDie 68485751 dump SyncEnemyPrepareDie(u64, EnemyDieInfo)
// + dump UX.Game.EnemyDieInfo field list
// SyncEnemyEndItemDrop 68876406 Lua reader midToReader[68876406] -> 4 flat fields
// SyncRemoveNpc 68446556 Lua reader midToReader[68446556] -> 3 flat fields
//
// Nothing here is invented: where a shape could not be verified it is left out
// rather than guessed.
//
// SyncUnitAttacked(68299892) used to be one of those omissions, and that omission was a
// mistake: the dump declares it as SyncUnitAttacked(ulong id, ulong attackerId, DamageData
// hpChange) with an 18-field DamageData, so the 4-primitive shape the LY-3.3 reference
// server used (u64,u64,u32,f32) cannot fit this build — but "cannot be wrong" is not the
// same as "should not be sent". It is the only hit notification the native combat code
// consumes, and without it a unit never enters the hurt state machine at all: HP can be
// driven to zero and the unit will still just stand there. It is implemented below from
// the dump's own field list, not from LY-3.3's. See DamageData4229938.
// ========================================================================
//
// Single-u64 enemy/unit reference used by the client's C2S fall handshake
// (AskEnemyStartFall / AskEnemyEndFall / AskEnemyFallGround). The dump gives these
// three methods one ulong parameter, so the argument is a bare u64 and this
// contract is used only to read that argument back defensively.
//
//
// SyncUnitHurtEffect(68349329) — hurt VFX on the target.
// Wire shape read from the client's own Lua reader, which flattens the call into
// 7 primitives with NO complex header (RPCDeserializeAuto.lua midToReader[68349329],
// export option LuaAndCSharp):
// hurterId u64, attackerId u64, hurtId u32, skillId u32,
// hurtEffectFacePos UXVector3(3 x f32), hurtImpact u32, hurtLeftOrRightHit u32.
// The dump confirms the same 7 parameters on
// UX.Game.IGameSceneToClient.SyncUnitHurtEffect, so this is a flat argument list,
// not a packed class: Inline is required, a 0xFF marker would shift every field.
// `hurtId` is the hit's HurtEffectId (the client reported 55004611 for firearm
// hits); `hurtImpact` 1 = normal impact, matching the client's own expectation.
//
// UX.Game.EntityType — dump shows `public int value__`, so FOUR bytes on the wire.
type EntityType4229938 int32
const (
EntityType4229938_None EntityType4229938 = 0
EntityType4229938_Player EntityType4229938 = 1
EntityType4229938_Enemy EntityType4229938 = 2
EntityType4229938_Npc EntityType4229938 = 4
EntityType4229938_Creation EntityType4229938 = 6
EntityType4229938_RaidVehicle EntityType4229938 = 7
)
// UX.Game.DamageEffect — dump shows `public ushort value__`, i.e. TWO bytes on the
// wire, not four. Getting this wrong shifts everything after it. The dump's field offsets
// confirm it: SpecialEffects sits at 0x2C and ClientHitPosition at 0x30, i.e. a 2-byte field
// followed by 2 bytes of memory padding — padding that does NOT exist on the wire.
type DamageEffect4229938 uint16
const (
DamageEffect4229938_Normal DamageEffect4229938 = 0
DamageEffect4229938_Crit DamageEffect4229938 = 1
DamageEffect4229938_Miss DamageEffect4229938 = 2
DamageEffect4229938_Resist DamageEffect4229938 = 4
DamageEffect4229938_Invincible DamageEffect4229938 = 8
DamageEffect4229938_ForceKill DamageEffect4229938 = 16
DamageEffect4229938_Block DamageEffect4229938 = 32
DamageEffect4229938_RealDamage DamageEffect4229938 = 64
DamageEffect4229938_FallDamage DamageEffect4229938 = 128
DamageEffect4229938_DotDamage DamageEffect4229938 = 256
DamageEffect4229938_DestructibleCreationDamage DamageEffect4229938 = 512
)
// UX.Game.DamageData — the SyncUnitAttacked payload, field-for-field in dump
// declaration order (Namespace UX.Game, TypeDefIndex 32296). Eighteen fields.
// This is THE packet the client's native combat code listens to. Everything else the
// server can send about a hit (SyncUnitHp, SyncEnemyPlayAction, SyncUnitHurtEffect) is
// peripheral: SyncUnitHp only moves the bar, SyncEnemyPlayAction never reaches a handler
// in this build (CSharpOnly, and the fastpatch's injected reader still never fired —
// Player.log has zero playAction lines for a session where the server sent nine of them),
// and SyncUnitHurtEffect is pure VFX. A unit that never receives SyncUnitAttacked never
// enters the client's hurt state machine, which is why an enemy could be shot ten times,
// reach HP 0 and still just stand there before vanishing.
// SyncUnitAttacked(68299892) — authoritative hit reaction.
// Dump signature: SyncUnitAttacked(ulong id, ulong attackerId, DamageData hpChange). Two
// bare primitives followed by a complex class, so this argument wrapper is flat while the
// nested payload keeps its 0xFF complex marker (same framing as SyncEnemyPrepareDie).
//
// SyncEnemyPlayAction(68039914) — plays a one-shot animation on a unit.
// The dump gives the exact three parameters, entity-first and flat:
// SyncEnemyPlayAction(ulong enemyId, uint actionId, float time).
// Action ids verified in-game against this build: 67149 = stagger, 88300 = down.
// The client's Lua handler (GameSceneToClientImpl.SyncEnemyPlayAction) resolves the
// unit through gCS.SceneDataMgr.GetUnit(enemyId) — the same global lookup that also
// serves LocalUnitMgr entries — then calls AnimControllerManager.PlayAction. That
// is what makes this contract usable on a locally generated enemy.
// `time` does NOT control the animation length on this build; pass 0 and let the
// client pick the clip length via AnimationManager.AnimatorGetAnimationTime.
//
//
// UX.Game.DieType — int-backed enum (the dump shows `public int value__`), so it is
// FOUR bytes on the wire. The LY-3.3 reference server declared this field as a byte,
// which shifts every following field of EnemyDieInfo and is one reason its death beat
// never landed on this build.
//
// ⛑⛑ 必须 :short。RPCSerializeAuto.lua 的 Auto.WriteEnemyDieInfo 最后两项是
// Base.WritePrimitive(writer, Base.CheckEnum(val.DieType, 151, 0), writer.WriteInt16, 0)
// Base.WritePrimitive(writer, val.WeaponId, writer.WriteUInt32, 0)
// 即线上是 Int16,而 UxContract.cs:122 的枚举分支按 Enum.GetUnderlyingType 写 —— 默认
// 底层是 Int32,会多出 2 个字节,DieType 之后的 WeaponId 整体错位、包尾多 2 字节。
// SyncEnemyPrepareDie(68485751) 是 CSharpOnly、没有 Lua reader,native 解析失败就等于
// 这个包被丢掉:客户端永远不知道单位已死,尸体继续走本地受击管线 —— 这正是
// 「死亡动画刚起手就被后续攻击打断」的根因(2026-09-23 18:0x)。
// 全项目只有 LocalEnemyCombat.cs 的两处 PrepareDie 用到它,改成 short 无副作用。
type DieType4229938 int16
const (
DieType4229938_None DieType4229938 = 0
DieType4229938_Damage DieType4229938 = 1
DieType4229938_FallOffCliff DieType4229938 = 2
DieType4229938_Clear DieType4229938 = 3
DieType4229938_Limbo DieType4229938 = 4
DieType4229938_FourWheelVehicleBurst DieType4229938 = 5
DieType4229938_VehicleLoseControl DieType4229938 = 6
DieType4229938_MotorCollide DieType4229938 = 7
DieType4229938_Drowning DieType4229938 = 8
DieType4229938_DesignerScriptChangeHp DieType4229938 = 9
DieType4229938_GangGM DieType4229938 = 10
DieType4229938_CreationDestructible DieType4229938 = 11
DieType4229938_DOTDamage DieType4229938 = 12
DieType4229938_Suicide DieType4229938 = 1000
DieType4229938_ParentDieSuicide DieType4229938 = 1001
DieType4229938_SpoonSuicide DieType4229938 = 1002
DieType4229938_VehicleTransformTimeoutSuicide DieType4229938 = 1003
DieType4229938_DesignerScriptSuicide DieType4229938 = 1004
DieType4229938_GMSuicide DieType4229938 = 1005
DieType4229938_VampirizeSuicide DieType4229938 = 1006
DieType4229938_HideAndSeekSuicide DieType4229938 = 1007
DieType4229938_HitFlyTimeoutSuicide DieType4229938 = 1008
DieType4229938_HitFlyTooHighSuicide DieType4229938 = 1009
DieType4229938_FallInWaterSuicide DieType4229938 = 1010
DieType4229938_PuppetTimeoutSuicide DieType4229938 = 1011
DieType4229938_GhostMouseSummonPuppetSuicide DieType4229938 = 1012
)
//
// UX.Game.EnemyDieInfo — the SyncEnemyPrepareDie payload, field-for-field in
// declaration order exactly as the IL2CPP dump lists it (Namespace UX.Game,
// TypeDefIndex 32297). Eleven fields, NOT ten: the trailing WeaponId is easy to
// miss and its absence truncates the payload. DieType is the int-backed enum above.
// Because SyncEnemyPrepareDie's parameter is a real class (not a flattened argument
// list), this contract is deliberately NOT Inline: the client reads it behind the
// normal 0xFF complex marker, which UxSerializer emits for any non-Inline contract.
// HasDieAnimation=true asks for the death animation. DelayDestroyDistance=0 keeps
// the corpse until the despawn packet rather than letting the client age it out.
// Killer carries the player pid so the client can attribute the kill.
//
//
// SyncEnemyPrepareDie(68485751) — death beat + killer attribution.
// Dump signature: SyncEnemyPrepareDie(ulong id, EnemyDieInfo dieInfo). The ulong id is
// a bare primitive parameter and dieInfo is a complex class, so this argument wrapper
// is flat while the nested payload keeps its complex 0xFF marker (UxObjectAttribute).
//
//
// SyncEnemyPoiseRate(68696896) — stance/架势 ratio push.
// Wire shape read from the client's own generated Lua reader (midToReader[68696896]):
// id u64, ratio single. Two flat primitives, no complex marker, no list/dict.
// ⚠️ The client-side handler for this build is NOT confirmed: the IL2CPP dump has no
// `GameSceneToClientImpl-SyncEnemyPoiseRate0` entry (unlike SyncEnemyPrepareDie, which
// has IDMAP 48141), so the C# side is an empty shell. A Lua-side impl may still exist.
// This contract exists so a single probe frame can settle it — see the poise probe note
// in GameRouter.LocalEnemyCombat.cs.
//
//
// SyncEnemyDetectStatus(68126766) — the enemy's alertness state, stored client-side as
// unitDataSet.detectionState and read by EnemyHUDCtrl.
// Wire shape copied from the client's own Lua reader (midToReader[68126766]):
// id u64, stealthValue f32, state u8 (13 bytes).
// Its export option is LuaAndCSharp — exactly the shape 硬规则 89 warns about: the C# side may
// be a shell while the Lua side is the real consumer, so this is settled by sending it, never
// by reading the dump.
// state is UX.Game.EnemyDetectState: None=0, Idle=1, Suspect=2, Alert=3, Fight=4,
// Wander=5, Search=6, GoHome=7 — a 1:1 match with the behaviour slots of the battle AI, i.e.
// Fight is the one that maps onto Behavior_Battle.
// Why it matters: a spawned enemy stays at None until this lands, and EnemyHUDCtrl keys the
// enemy HUD (the HP bar, plus the alertness meter that hangs off it) on detectionState — so
// the bar never binds without it. Note this is a different packet from SyncEnemyDetect,
// which the engagement batch already sends and which does not touch detectionState.
//
//
// SyncEnemyEndItemDrop(68876406) — loot drop after the fall sequence.
// Wire shape read from the client's own Lua reader (midToReader[68876406], LuaOnly),
// four flat fields with no complex header:
// enemyInstanceId u64, bindItemsIndex i32, itemRotation UXVector3, position UXVector3.
// We send bindItemsIndex = -1 ("no bound item table") with both vectors zeroed but for
// the corpse position; the point of the packet here is the tail of the despawn
// sequence, not the loot itself.
//
//
// SyncRemoveNpc(68446556) — remove an NPC from the scene.
// Wire shape read from the client's own Lua reader (midToReader[68446556]):
// npcId i32, taskId u32, enemyId u64.
// The client declares this method LuaOnly AND ships the reader above, but the retail
// Lua never implements it. Handlers are resolved by name — RPCDeserializeBase does
// sidToImpl[floor(mid / 1000000)][midToName[mid]], i.e. 68446556 -> 68 ->
// GameSceneToClientImpl["SyncRemoveNpc"] — so in the stock build the lookup returned
// nil and the packet was dropped on arrival without a single log line.
// That makes this method the best available carrier for removing a CLIENT-SPAWNED
// enemy: it costs nothing to send, it cannot collide with an existing client handler,
// and its argument list already carries the pid. The fastpatch in
// Drmk.Proxy/proxy/tools/generate_client_config_patch.js installs the missing Lua impl,
// which is the code that finally destroys the unit.
//
// ---------------------------------------------------------------------------------------------
// 虚弱 / 硬直 闭环 + 尸体清理 (2026-09-23)
//
// 这一组契约来自 kimika 捐赠端(可信方,其 GameRouter.EnemyGroggy.cs / Enemy.cs 有一整套
// 闭环实现),在此之前我们一条都没接,后果是客户端的状态机没有出口:
// * 破防后我们没有推 SyncEnemyDisarmState(true),也没有把架势条推到 0,客户端的虚弱状态
// 进不去也出不来;
// * ReportUnitStandUp / ReportAgentDiePerfEnd 是 **Invoke**,客户端 await 服务端回执,
// 我们没有 handler ⇒ 客户端的起身 / 死亡表现流程挂起 ⇒ 敌人"卡住";
// * 尸体只发了 SyncEnemyPrepareDie(播动画),从未发 SyncClearSpoonEnemies(拆尸体),
// 于是敌人死了还站在场上,后续命中因为账本已删而无人结算 ⇒ "打不死的幽灵"。
//
// 全部按捐赠端的 wire 形状定义([UxContract(Inline = true)] + 裸标量),集合用 Int7 偏置。
// ---------------------------------------------------------------------------------------------
//
// SyncClearSpoonEnemies(68237191) — 真正拆除尸体的包。
// 捐赠端注释(GameRouter.Enemy.cs:888):SyncEnemyPrepareDie 只要求客户端"播死亡",
// 尸体由 SyncClearSpoonEnemies 拆掉;没有它,单位会带着一条空血条一直站着。
// 客户端入口 gCS.BattleManager.CheckClearDelayDeath(enemyIds[i])。
//
//
// SyncForceEnemyRelation(68617877) — 强制敌对关系:IGameSceneToClient.SyncForceEnemyRelation
// (ulong me, ulong other)。捐赠端在 spawn 的第 6 步发它,用于让锁定 / HUD 把该单位当敌人。
// 我方此前只有 SyncAgentCampInfo(camp=4),缺这一步,可能是 HUD 不绑血条的另一半原因。
//
//
// AskAgentEnterStiff(67352229) / AskAgentExitStiff(67581079) — 客户端上报单位进入 / 退出硬直。
// 序列化(ClientToGameSceneDelegate.lua):单个 UInt64 agentid。Notify,无需回执,
// 但必须被"接住":这是客户端告诉我们它的动画图进入硬直的方式,AI 不能在硬直里继续挥。
//
//
// ReportUnitStandUp(67329238) — 单位起身报告,Invoke,客户端阻塞等回执。
// 序列化(ClientToGameSceneDelegate.lua:1890):UInt64 unitid, UInt32 stiffid, Single stifftime。
// 这是"单位站起来了"的权威信号,也是虚弱窗口正常关闭的通路。不回就是卡住。
//
//
// ReportEnemyPrepareFinish(67427299) — 客户端报告死亡准备流程走完(Invoke,需回执)。
// 捐赠端用它立即触发 SyncClearSpoonEnemies。
//
//
// ReportAgentDiePerfEnd(67638502) — 客户端报告死亡表现播完(Invoke,需回执)。
// 序列化(ClientToGameSceneDelegate.lua:3628):单个 UInt64 id。
//
//
// SyncAddUnitState(68026128) — 把单位打进某个 UnitStateConfig 状态。
// 字段顺序照抄捐赠端 7k-v0.5
// Drmk.RpcTypes/Client4229938/Methods/GameScene/CombatMethods.cs:155
// (其注释:"native inline u64 + u32 + u32"):id / state / effectFreezeState,全部裸标量,无复杂头。
// ⛑⛑ state 是 UnitStateConfig.DeadS = 2,不是 UnitStateAtlasConfig.Death = 3
// —— 后者只是个图集 atlas,发错了单位不会真的进死亡行为模块(捐赠端原注释)。
// 用途:死亡序列里在 SyncEnemyPrepareDie 之后发一次,让客户端把单位标记为已死,
// 从而停掉它原生的受击管线。没有这一包,玩家的后续连招会一直把倒地动画顶掉。
// 见 EnemyCombatSettings.PushDeadUnitState。
//