290 lines
17 KiB
Go
290 lines
17 KiB
Go
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。
|
||
//
|