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

289 lines
17 KiB
Go
Raw Permalink Normal View History

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