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。 //