特殊武器机制:刀、电击枪、霰弹枪、左轮、连发、消音器、开镜与客户端弹道特效
武器与射击 追踪了普通枪械主攻从按键到伤害的主路径。本页补全其余分支:同一个 CWeaponCSBaseGun 如何靠数据开关变成连发枪、消音枪、左轮或狙击枪;不走子弹路径的刀;伤害类型特殊的电击枪;自成一类、逐发装填的霰弹枪;以及服务器发出 TE_FireBullets 之后,其他客户端如何在本地重建枪声、弹孔、曳光和穿墙特效。这些分支共用“武器模式 m_weaponMode + 属性查询”这一套机制,所以任一数据源缺失(例如本快照中物品 schema 的加载路径,见 物品与经济)都会同时波及多种武器;本页最后一节据此回答审计未决项 U2。
相关页面:武器与射击 · 物品与经济 · 玩家 · 移动 · 延迟补偿 · 输入与预测 · 审计
相关源码文件#
game/shared/cstrike15/weapon_csbasegun.cpp、game/shared/cstrike15/weapon_csbasegun.h:连发、消音器、开镜、左轮副攻的分派game/shared/cstrike15/weapon_csbase.cpp、game/shared/cstrike15/weapon_csbase.h:按键分派、左轮扳机延迟、动画事件、精度惩罚game/shared/cstrike15/cs_weapon_parse.cpp:武器属性访问器GET_WEAPON_ATTR_FUNCgame/shared/cstrike15/weapon_knife.cpp、game/shared/cstrike15/weapon_taser.cppgame/shared/cstrike15/weapon_nova.cpp、game/shared/cstrike15/weapon_xm1014.cpp、game/shared/cstrike15/weapon_m3.cpp、game/shared/cstrike15/weapon_sawedoff.cpp:逐发装填霰弹枪game/shared/cstrike15/fx_cs_shared.cpp:FX_FireBulletsgame/server/cstrike15/te_shotgun_shot.cpp、game/client/cstrike15/c_te_shotgun_shot.cpp:TE_FireBullets的发送端与接收端game/shared/cstrike15/cs_player_shared.cpp:FireBullet中的弹孔、曳光、穿墙特效分支game/client/cstrike15/fx_cs_impacts.cpp、game/client/cstrike15/fx_cs_knifeslash.cpp、game/client/cstrike15/fx_cs_weaponfx.cpp、game/client/cstrike15/fx_cs_muzzleflash.cpp:客户端效果回调
1. 一个枪类,多种行为:数据开关与武器模式#
除霰弹枪、刀、电击枪、沙鹰、双枪等少数有独立源文件的类之外,大部分枪械实体类只是 CWeaponCSBaseGun 的“简单派生”注册,C++ 层没有专门子类;Famas、Glock、AWP、Mag-7 都在这张表中:game/shared/cstrike15/weapon_csbasegun.cpp:47-80。R8 左轮甚至没有自己的武器 ID,weapon_revolver 与 weapon_deagle 都映射到 WEAPON_DEAGLE:game/shared/cstrike15/cs_weapon_parse.cpp:55-58。
行为差异来自 CCSWeaponInfo 上的属性访问器。宏 GET_WEAPON_ATTR_FUNC 为每个属性生成一个函数:nAlt 非 0 时改查带 alt 后缀的属性名(即副模式取值);查找时先看武器自身 CEconItemView 上的物品属性,查不到再回退到武器脚本里的旧字段:game/shared/cstrike15/cs_weapon_parse.cpp:1130-1148、game/shared/cstrike15/cs_weapon_parse.cpp:1060-1093。
| 能力 | 判定函数 | 属性名 | 证据 |
|---|---|---|---|
| 连发模式 | WeaponHasBurst → HasBurstMode | has burst mode | game/shared/cstrike15/weapon_csbasegun.h:70、game/shared/cstrike15/cs_weapon_parse.cpp:1204 |
| 可装卸消音器 | HasSilencer | has silencer | game/shared/cstrike15/weapon_csbase.h:236-239、game/shared/cstrike15/cs_weapon_parse.cpp:1153 |
| 左轮 | IsRevolver | is revolver | game/shared/cstrike15/weapon_csbasegun.h:75、game/shared/cstrike15/cs_weapon_parse.cpp:1205 |
| 开镜档位 | GetZoomLevels / HasZoom | zoom levels | game/shared/cstrike15/weapon_csbase.h:185、game/shared/cstrike15/weapon_csbasegun.cpp:902-905、game/shared/cstrike15/cs_weapon_parse.cpp:1213 |
| 射击后退镜 | DoesUnzoomAfterShot | unzoom after shot | game/shared/cstrike15/weapon_csbasegun.h:76、game/shared/cstrike15/cs_weapon_parse.cpp:1210 |
| 每次射击的弹丸数 | GetBullets | bullets | game/shared/cstrike15/cs_weapon_parse.cpp:1154 |
| 全自动 | IsFullAuto | is full auto | game/shared/cstrike15/cs_weapon_parse.cpp:1152 |
这些能力共用一个网络化、参与预测的字段 m_weaponMode(Primary_Mode / Secondary_Mode):game/shared/cstrike15/weapon_csbase.cpp:305、game/shared/cstrike15/weapon_csbase.cpp:328。最大移速、散布等都按当前模式读取对应数值:game/shared/cstrike15/weapon_csbase.h:183、game/shared/cstrike15/weapon_csbase.h:337。Secondary_Mode 的含义因武器而异:
| 武器 | Secondary_Mode 表示 | 置位 / 复位 |
|---|---|---|
| 可开镜武器 | 已开镜 | 开镜与退镜 game/shared/cstrike15/weapon_csbasegun.cpp:368-370、game/shared/cstrike15/weapon_csbasegun.cpp:415-417;换弹与收枪时复位 game/shared/cstrike15/weapon_csbasegun.cpp:815-819、game/shared/cstrike15/weapon_csbasegun.cpp:859-863 |
| 连发武器 | 连发模式 | game/shared/cstrike15/weapon_csbasegun.cpp:538-549 |
| 消音武器 | 已装消音器 | 出生时 game/shared/cstrike15/weapon_csbase.cpp:3087-3088;装卸动画事件完成时 game/shared/cstrike15/weapon_csbase.cpp:2885-2901 |
| 左轮 | 副攻(连扇) | game/shared/cstrike15/weapon_csbasegun.cpp:593-597、game/shared/cstrike15/weapon_csbasegun.cpp:892-895、game/shared/cstrike15/weapon_csbase.cpp:1180-1191 |
2. 副攻键的分派顺序#
CWeaponCSBase::ItemPostFrame 按 IN_ATTACK → IN_ZOOM → IN_ATTACK2 → IN_RELOAD 的优先级分派,副攻只在 m_flNextSecondaryAttack 到期时触发:game/shared/cstrike15/weapon_csbase.cpp:943-964。缩放键对左轮等同副攻,对可开镜武器和刀调用 CallSecondaryAttack:game/shared/cstrike15/weapon_csbase.cpp:1038-1048。进入 CWeaponCSBaseGun::SecondaryAttack 后,按下列顺序只走一个分支:
证据:开镜分支 game/shared/cstrike15/weapon_csbasegun.cpp:361-534;连发分支被 #ifndef CLIENT_DLL 包裹 game/shared/cstrike15/weapon_csbasegun.cpp:535-553;消音器分支受本文件定义为 1 的 DETACHABLE_SILENCER 控制 game/shared/cstrike15/weapon_csbasegun.cpp:23、game/shared/cstrike15/weapon_csbasegun.cpp:554-592;左轮分支在设置自己的冷却后 return game/shared/cstrike15/weapon_csbasegun.cpp:593-613;其余分支最后统一把副攻冷却设为 0.3 秒 game/shared/cstrike15/weapon_csbasegun.cpp:615-620。
3. 刀#
CKnife 直接派生自 CWeaponCSBase,网络表为空,主攻与副攻都调用同一个 SwingOrStab:game/shared/cstrike15/weapon_knife.h:29、game/shared/cstrike15/weapon_knife.cpp:95-101、game/shared/cstrike15/weapon_knife.cpp:255-285。
| 攻击 | 射程 | 命中后:下次主攻 / 副攻 | 未命中:下次主攻 / 副攻 | 正面伤害 | 背刺伤害 |
|---|---|---|---|---|---|
| 主攻(划) | 48 | 0.5 / 0.5 秒 | 0.4 / 0.5 秒 | 首刀 40,连续划 25 | 90 |
| 副攻(刺) | 32 | 1.1 / 1.1 秒 | 1.0 / 1.0 秒 | 65 | 180 |
证据:射程常量与选择 game/shared/cstrike15/weapon_knife.cpp:29-30、game/shared/cstrike15/weapon_knife.cpp:326;间隔(持盾时各加 0.7 秒)game/shared/cstrike15/weapon_knife.cpp:367-387;伤害 game/shared/cstrike15/weapon_knife.cpp:420-452。
执行顺序:
- 前置条件:主攻走通用的
ItemPostFrame_ProcessPrimaryAttack,冻结期、拆弹中或非STATE_ACTIVE时不出刀game/shared/cstrike15/weapon_csbase.cpp:975-982;副攻在SecondaryAttack中自行检查拆弹与冻结期game/shared/cstrike15/weapon_knife.cpp:274。两者在服务端都以对应射程开启沿射线的延迟补偿game/shared/cstrike15/weapon_knife.cpp:260-267、game/shared/cstrike15/weapon_knife.cpp:276-283。 - 追踪:先做射线追踪;未命中时改用 32×32×36 的“头部”包围盒追踪,命中世界或无实体时用
FindHullIntersection求近似最佳交点game/shared/cstrike15/weapon_knife.cpp:32-33、game/shared/cstrike15/weapon_knife.cpp:210-252、game/shared/cstrike15/weapon_knife.cpp:328-353。服务端另以 42 点伤害对触发器追踪,用于打碎玻璃game/shared/cstrike15/weapon_knife.cpp:335-339。 - 首刀判定(仅服务端):
m_flNextPrimaryAttack + 0.4 < curtime时视为“新一轮”首刀game/shared/cstrike15/weapon_knife.cpp:357-365。 - 背刺判定:目标是玩家时,取攻击者指向目标的水平视线单位向量,与目标朝向的水平分量做点积,大于 0.475 即为背刺
game/shared/cstrike15/weapon_knife.cpp:404-418。换算成角度,攻击者需位于目标背后约 ±61.6° 的扇区内(由 acos(0.475) 推算)。 - 结算(仅服务端):伤害乘以开发用 ConVar
knife_damage_scale(百分比,默认 100)game/shared/cstrike15/weapon_knife.cpp:308-311、game/shared/cstrike15/weapon_knife.cpp:450-451;以DMG_SLASH | DMG_NEVERGIB直接DispatchTraceAttack+ApplyMultiDamagegame/shared/cstrike15/weapon_knife.cpp:463-470。刀不经过FX_FireBullets,因此不受第 11 节物品定义索引问题影响。 - 表现:按预测规则播放命中/挥空声
game/shared/cstrike15/weapon_knife.cpp:472-485、game/shared/cstrike15/weapon_knife.cpp:509-524;派发KnifeSlash效果,服务端从 PAS 过滤器中去掉攻击者(攻击者本地已预测)game/shared/cstrike15/weapon_knife.cpp:487-507;客户端回调按挥砍方向的垂直向量贴刀痕,命中静态道具时走专门路径game/client/cstrike15/fx_cs_knifeslash.cpp:19-77。服务端用m_swingLeft交替第三人称左右划动画game/shared/cstrike15/weapon_knife.cpp:526-538。
其他:拔刀后固定 1.0 秒才能攻击,以抹平不同刀模的部署动画差异 game/shared/cstrike15/weapon_knife.cpp:157-189;刀不可丢弃 game/shared/cstrike15/weapon_knife.cpp:547-550;军备竞赛的 weapon_knifegg 继承 CKnife game/shared/cstrike15/weapon_knife.cpp:557-566;出刀会清除军备竞赛出生保护 game/shared/cstrike15/weapon_knife.cpp:321-323、game/server/cstrike15/cs_player.cpp:14090-14094。
4. 电击枪(Zeus)#
CWeaponTaser 派生自 CWeaponCSBaseGun,主攻就是 CSBaseGunFire,所以弹道、散布、延迟补偿与普通枪械相同,也同样受第 11 节影响:game/shared/cstrike15/weapon_taser.cpp:25-49、game/shared/cstrike15/weapon_taser.cpp:75-91。差异集中在伤害类型和一次性使用:
| 方面 | 行为 | 证据 |
|---|---|---|
| 伤害类型 | FireBullet 发现当前武器是电击枪时,把 DMG_BULLET 换成 DMG_SHOCK | DMG_NEVERGIB | game/shared/cstrike15/cs_player_shared.cpp:1728-1733 |
| 受击表现 | 受害者 TraceAttack 对 DMG_SHOCK 不出血;客户端撞击效果对电击不贴弹孔;客户端布娃娃受击不喷血 | game/server/cstrike15/cs_player.cpp:5472-5475、game/client/cstrike15/fx_cs_impacts.cpp:31-35、game/client/cstrike15/c_cs_player.cpp:806-812 |
| 击杀表现 | 服务端置位网络变量 m_bKilledByTaser;客户端周期性对布娃娃双手施加随机力并播放死亡音效 | game/server/cstrike15/cs_player.cpp:2597-2601、game/server/cstrike15/cs_player.cpp:484、game/client/cstrike15/c_cs_player.cpp:744-770、game/client/cstrike15/c_cs_player.cpp:5915-5935 |
| 一次性使用 | 仅服务端:弹药耗尽且开火 0.5 秒后丢下并删除武器;空枪切走时丢下 | game/shared/cstrike15/weapon_taser.cpp:117-143 |
| 购买限制 | mp_weapons_allow_zeus 为 0 禁买,大于 0 时限制每回合购买次数(默认 1) | game/shared/cstrike15/cs_player_shared.cpp:2898-2909、game/shared/cstrike15/cs_gamerules.cpp:2273-2276 |
| 生日彩蛋 | IsCSGOBirthday() 时换成派对喇叭声与彩纸枪口效果 | game/shared/cstrike15/weapon_taser.cpp:82-115 |
5. 霰弹枪:逐发装填与弹丸#
weapon_nova、weapon_xm1014、weapon_m3、weapon_sawedoff 是四个几乎逐行相同的类,都直接派生自 CWeaponCSBase,自己调用 FX_FireBullets,因而没有连发、消音、开镜、左轮这些 CWeaponCSBaseGun 分支:game/shared/cstrike15/weapon_nova.cpp:26-50、game/shared/cstrike15/weapon_xm1014.cpp:27、game/shared/cstrike15/weapon_m3.cpp:25、game/shared/cstrike15/weapon_sawedoff.cpp:26。Mag-7 则是 CWeaponCSBaseGun 的简单派生 game/shared/cstrike15/weapon_csbasegun.cpp:62,推断它走普通的整匣 DefaultReload game/shared/cstrike15/weapon_csbasegun.cpp:797-831(推断)。
开火(以 Nova 为例):水下只播空膛声并延迟 0.15 秒;弹匣为空时先尝试 Reload() 再播空膛声;否则播动画、枪口闪光,调用 FX_FireBullets,把 m_reloadState 清零(打断装填),累加精度惩罚、调用(已被删空的)Recoil、扣一发:game/shared/cstrike15/weapon_nova.cpp:80-160。XM1014 只在空膛延迟(0.25 秒)和最后一发后的空闲时间上不同:game/shared/cstrike15/weapon_xm1014.cpp:104、game/shared/cstrike15/weapon_xm1014.cpp:142。
逐发装填由网络化、参与预测的 2 位字段 m_reloadState 驱动 game/shared/cstrike15/weapon_nova.cpp:52-66:
证据:Reload 三态逻辑,射击冷却未结束时不开始装填 game/shared/cstrike15/weapon_nova.cpp:163-222;WeaponIdle 在空闲计时到期时反复调用 Reload,弹满或无备弹时结束 game/shared/cstrike15/weapon_nova.cpp:225-269;开火清零状态 game/shared/cstrike15/weapon_nova.cpp:149。由于状态 1→2 时把空闲计时推后 0.5 秒,按代码推算约每 0.5 秒装入一发(推断,未运行验证)。
弹丸数与散布:FX_FireBullets 按 GetBullets() 循环,上限断言为 16;所有弹丸共享同一个“精度偏移”(x0, y0),每颗另有预先生成的“散布偏移”,每颗都以 4 次穿透上限调用 FireBullet:game/shared/cstrike15/fx_cs_shared.cpp:438-480、game/shared/cstrike15/fx_cs_shared.cpp:548-568。同一批弹丸的爆头表现合并见 武器与射击。客户端在整批弹丸期间把撞击声路由到 ShotgunImpactSoundGroup,300 单位内同名撞击声只播一次:game/shared/cstrike15/fx_cs_shared.cpp:89-126、game/shared/cstrike15/fx_cs_shared.cpp:374、game/shared/cstrike15/fx_cs_shared.cpp:579。
6. R8 左轮:扳机延迟与连扇#
主攻(按住扳机):每次处理主攻时先把副攻冷却推后 0.25 秒;当前动画不是 ACT_VM_HAULBACK(扳机后拉)时,重置“延迟开火就绪时间”并开始后拉动画,本帧不开火;之后切到 Primary_Mode,直到 IsPostponFireReadyTimeElapsed() 才真正 PrimaryAttack;空枪时额外延迟 0.5 秒:game/shared/cstrike15/weapon_csbase.cpp:999-1027。开火后副攻冷却设为副模式射速的 1.7 倍:game/shared/cstrike15/weapon_csbase.cpp:1030-1035。
延迟从哪里来:就绪时间不是代码常量,而是由视图模型动画事件 AE_WPN_PRIMARYATTACK 的参数字符串给出秒数;同时向射手以外、且不在第一人称观战此人的玩家播放 Weapon_Revolver.Prepare 预警声:game/shared/cstrike15/weapon_csbase.cpp:2961-2997。该字段网络化并参与预测:game/shared/cstrike15/weapon_csbase.h:351-354、game/shared/cstrike15/weapon_csbase.cpp:315、game/shared/cstrike15/weapon_csbase.cpp:335、game/shared/cstrike15/weapon_csbase.cpp:366。因此延迟时长属于模型内容,不在仓库中。
副攻(连扇):冻结期、拆弹中或非 STATE_ACTIVE 时拒绝;本次按键内已开过火(m_iShotsFired > 0)则不连发;空枪时也要先后拉并等待就绪时间:game/shared/cstrike15/weapon_csbase.cpp:1050-1100。随后 CWeaponCSBaseGun::SecondaryAttack 的左轮分支触发 weapon_fire 事件并以副模式开火:game/shared/cstrike15/weapon_csbasegun.cpp:593-613。
其他:松开所有武器键时取消后拉并回到 Secondary_Mode game/shared/cstrike15/weapon_csbase.cpp:1180-1191;空枪时左轮播放 ACT_VM_DRYFIRE 并按射速延迟两种攻击 game/shared/cstrike15/weapon_csbasegun.cpp:700-706;副攻使用 ACT_VM_SECONDARYATTACK 动画 game/shared/cstrike15/weapon_csbasegun.cpp:715-722。FX_FireBullets 中为“R8 副攻”准备的更集中的散布曲线以 nItemDefIndex == 64 为条件 game/shared/cstrike15/fx_cs_shared.cpp:420-425、game/shared/cstrike15/fx_cs_shared.cpp:455-460,在本快照中不可达,见第 11 节。
7. 连发模式(Famas / Glock)#
- 切换:只在服务端编译,弹出提示文本并播放切换音效,翻转
m_bBurstMode与m_weaponMode:game/shared/cstrike15/weapon_csbasegun.cpp:535-553。m_bBurstMode通过网络表同步到客户端game/shared/cstrike15/weapon_csbase.cpp:314、game/shared/cstrike15/weapon_csbase.cpp:334;客户端不在本地预测这次切换,而是等待网络更新(推断)。 - 第一发:连发模式下用属性钩子
cycletime_when_in_burst_mode改写射速,把剩余发数m_iBurstShotsRemaining设为 2,并以钩子time_between_burst_shots计算下一发时间,然后以m_weaponMode(即副模式)调用CSBaseGunFire:game/shared/cstrike15/weapon_csbasegun.cpp:320-336。下一发时间的基值是“当前时间”,间隔完全来自属性;若没有该属性,间隔为 0(推断)。 - 后两发:
ItemPostFrame在时间到达时调用BurstFireRemaining,后者以Secondary_Mode调用FX_FireBullets、递减剩余发数、排下一发、累加精度惩罚、扣弹;弹匣空则清零连发状态:game/shared/cstrike15/weapon_csbasegun.cpp:246-252、game/shared/cstrike15/weapon_csbasegun.cpp:623-678。m_iBurstShotsRemaining网络化并参与预测game/shared/cstrike15/weapon_csbasegun.cpp:31、game/shared/cstrike15/weapon_csbasegun.cpp:34、game/shared/cstrike15/weapon_csbasegun.cpp:42。 - 按住不放:连发模式下
IsFullAuto()返回 falsegame/shared/cstrike15/weapon_csbasegun.cpp:774-784,因此主攻处理在m_iShotsFired > 0时拒绝再次触发game/shared/cstrike15/weapon_csbase.cpp:987-989。 - 切枪:
Deploy清空未打完的连发game/shared/cstrike15/weapon_csbasegun.cpp:879-881。
观察:水下禁射检查只在
PrimaryAttack中game/shared/cstrike15/weapon_csbasegun.cpp:311-316,BurstFireRemaining没有重复检查;连发途中入水时后两发是否仍会射出,本次未运行验证。
8. 消音器装卸#
- 初始状态:有消音器能力的武器在
Spawn时即为已装、Secondary_Mode:game/shared/cstrike15/weapon_csbase.cpp:3087-3088。m_bSilencerOn是网络变量game/shared/cstrike15/weapon_csbase.cpp:318、game/shared/cstrike15/weapon_csbase.cpp:337。 - 开始装卸:副攻播放
ACT_VM_DETACH_SILENCER或ACT_VM_ATTACH_SILENCER,服务端同步活动事件、第三人称动画与silencer_detach事件;按动画时长锁住主攻并设置m_flDoneSwitchingSilencer:game/shared/cstrike15/weapon_csbasegun.cpp:554-591。 - 完成装卸:状态并不在按键时翻转,而是在服务端动画事件
AE_CL_ATTACH_SILENCER_COMPLETE/AE_CL_DETACH_SILENCER_COMPLETE到达时翻转m_bSilencerOn与m_weaponMode,并触发silencer_on/silencer_off事件:game/shared/cstrike15/weapon_csbase.cpp:2715-2720、game/shared/cstrike15/weapon_csbase.cpp:2885-2905;显示/隐藏消音器的身体组同样由动画事件驱动game/shared/cstrike15/weapon_csbase.cpp:2843-2884。 - 中途取消:装卸未完成时收枪或丢枪,只重置计时、不改变状态(注释称“重新部署本身就是惩罚”):
game/shared/cstrike15/weapon_csbasegun.cpp:129-149、game/shared/cstrike15/weapon_csbasegun.cpp:848-857。 - 效果:已消音时声音类型为
SPECIAL1game/shared/cstrike15/weapon_csbasegun.cpp:745、game/shared/cstrike15/weapon_csbasegun.cpp:648,接收端只接受SINGLE与SPECIAL1两种game/client/cstrike15/c_te_shotgun_shot.cpp:66-77;使用消音开火动画game/shared/cstrike15/weapon_csbasegun.cpp:339-340;不触发枪口闪光game/shared/cstrike15/weapon_csbasegun.cpp:786-795,客户端改用 alt 枪口粒子game/shared/cstrike15/weapon_csbasegun.cpp:911-933;weapon_fire事件带silenced字段game/shared/cstrike15/weapon_csbasegun.cpp:600-607。由于处于Secondary_Mode,散布、移速等也改用 alt 数值(第 1 节)。
9. 开镜:FOV、精度与移速#
切换:m_zoomLevel 在 0 到 GetZoomLevels() 之间循环 game/shared/cstrike15/weapon_csbasegun.cpp:363-364。进入开镜时切到 Secondary_Mode,狙击枪设置该档 FOV 与过渡时间,额外加一次 GetInaccuracyAltSwitch() 精度惩罚并置 m_bIsScoped game/shared/cstrike15/weapon_csbasegun.cpp:366-380;非狙击武器(如带瞄具的步枪)在 IRONSIGHT(于 game/shared/cstrike15/cs_shareddefs.h:47 定义)下通过机瞄控制器设置 FOV game/shared/cstrike15/weapon_csbasegun.cpp:382-412;退镜时恢复站立或下蹲默认 FOV game/shared/cstrike15/weapon_csbasegun.cpp:415-443。开/退镜音效只由服务端播放;狙击枪触发供 Bot 监听的 weapon_zoom,其他武器触发供教学系统使用的 weapon_zoom_rifle game/shared/cstrike15/weapon_csbasegun.cpp:466-533。
对精度与移速的影响:
| 影响 | 机制 | 证据 |
|---|---|---|
| 移速上限 | GetMaxSpeed 按 m_weaponMode 取值;玩家最大速度取武器上限与 260 的较小值,带人质、持盾等情形另有固定值 | game/shared/cstrike15/weapon_csbase.h:183、game/shared/cstrike15/cs_player_shared.cpp:341-366 |
| 散布 | GetSpread 按 m_weaponMode 取值 | game/shared/cstrike15/weapon_csbase.h:337 |
| 基础不精确度 | UpdateAccuracyPenalty 按 m_weaponMode 读取站立/下蹲/跳跃不精确度 | game/shared/cstrike15/weapon_csbase.cpp:3705-3724 |
| 移动惩罚基准 | GetInaccuracy 以当前模式的 GetMaxSpeed 作为速度映射上限 | game/shared/cstrike15/weapon_csbase.cpp:1219-1221 |
| 射速 | 属性钩子 cycletime_when_zoomed | game/shared/cstrike15/weapon_csbasegun.cpp:331-334 |
| 开镜瞬间 | 加 GetInaccuracyAltSwitch() | game/shared/cstrike15/weapon_csbasegun.cpp:377 |
栓动狙击:DoesUnzoomAfterShot() 为真时射击后立即退镜并置 m_bResumeZoom game/shared/cstrike15/weapon_csbasegun.cpp:342-349;下一次可射击且弹匣非空时,ItemPostFrame 以 0.1 秒过渡恢复之前的档位 game/shared/cstrike15/weapon_csbasegun.cpp:181-213。
观察:
ItemBusyFrame在换弹期间退镜,注释称“不改变 zoom level,换弹结束后会恢复”game/shared/cstrike15/weapon_csbasegun.cpp:151-169;但CWeaponCSBaseGun::Reload在换弹开始时已把m_zoomLevel清零game/shared/cstrike15/weapon_csbasegun.cpp:815-825,而自动恢复要求m_zoomLevel > 0game/shared/cstrike15/weapon_csbasegun.cpp:183-184。据此推断换弹后不会自动重新开镜,注释已过时(推断,未运行验证)。
10. 客户端弹道特效:TE_FireBullets 的接收端#
武器与射击 说明了服务端在 FX_FireBullets 中发送临时实体并关闭本端特效。以下是另一端:
证据:服务端发送后把 bDoEffects 置 false game/shared/cstrike15/fx_cs_shared.cpp:308-332;TE_FireBullets 填充单例并以带预测规则的广播过滤器创建 game/server/cstrike15/te_shotgun_shot.cpp:85-126,UsePredictionRules 移除当前“抑制主机” game/server/recipientfilter.cpp:288-303,而服务端在执行一个预测武器的玩家的命令期间正是把该玩家设为抑制主机 game/server/player.cpp:3602-3624;客户端接收并重放 game/client/cstrike15/c_te_shotgun_shot.cpp:56-100、game/client/cstrike15/c_te_shotgun_shot.cpp:103-122;同一武器 20ms 内只追踪一次 game/shared/cstrike15/fx_cs_shared.cpp:201-214;撞击 game/shared/cstrike15/cs_player_shared.cpp:1735-1769;曳光 game/shared/cstrike15/cs_player_shared.cpp:1780-1783;穿墙轨迹 game/shared/cstrike15/cs_player_shared.cpp:1903-1933。因此射手本人收不到这条临时实体,其特效来自本地预测执行的 FX_FireBullets(预测流程见 输入与预测)。
临时实体字段与量化:
| 字段 | 编码 | 证据 |
|---|---|---|
m_iPlayer | 6 位无符号,发送前减 1,接收后加 1 | game/server/cstrike15/te_shotgun_shot.cpp:73、game/server/cstrike15/te_shotgun_shot.cpp:108、game/client/cstrike15/c_te_shotgun_shot.cpp:82 |
m_iWeaponID / m_iMode | 6 位 / 1 位 | game/server/cstrike15/te_shotgun_shot.cpp:70-71 |
m_iSeed | 8 位(NUM_BULLET_SEED_BITS),对应射击方传入的“预测随机种子 & 255” | game/server/cstrike15/te_shotgun_shot.cpp:18、game/server/cstrike15/te_shotgun_shot.cpp:72、game/shared/cstrike15/weapon_csbasegun.cpp:740 |
| 角度 | 俯仰、偏航各 13 位;接收端把滚转清零 | game/server/cstrike15/te_shotgun_shot.cpp:68-69、game/client/cstrike15/c_te_shotgun_shot.cpp:79 |
m_fInaccuracy / m_flRecoilIndex / m_fSpread | 10 位 [0,1] / 10 位 [0,1000] / 8 位 [0,0.1] | game/server/cstrike15/te_shotgun_shot.cpp:74-76 |
m_nItemDefIndex / m_iSoundType | 16 位 / 6 位 | game/server/cstrike15/te_shotgun_shot.cpp:77-78 |
m_fAccuracyFishtail | 仅在 WEAPON_FIRE_BULLETS_ACCURACY_FISHTAIL_FEATURE 下发送;接收端否则传 0 | game/server/cstrike15/te_shotgun_shot.cpp:79-81、game/client/cstrike15/c_te_shotgun_shot.cpp:91-95 |
全仓检索没有任何地方定义 WEAPON_FIRE_BULLETS_ACCURACY_FISHTAIL_FEATURE,于是 CWeaponCSBase 中返回成员变量的版本不会编译,GetAccuracyFishtail() 落到基类返回 0 的实现:game/shared/cstrike15/weapon_csbase.h:333-336、game/shared/basecombatweapon_shared.h:350。由于不精确度、散布被有损量化,其他客户端重建的弹道只是服务端的近似;这只影响表现,伤害仍以服务端为准(推断)。接收端还兼容 2013 年经济更新前录制的 demo:没有声音类型时按 SINGLE 播放 game/client/cstrike15/c_te_shotgun_shot.cpp:60-77。
客户端效果清单:
| 效果 | 产生方式与条件 | 证据 |
|---|---|---|
| 枪声 | FX_WeaponSound 只在客户端有实现(服务端为空函数);不精确度低于阈值时换成 SINGLE_ACCURATE,否则按不精确度微调音高;弹匣剩余 ≤20% 时追加 NEARLYEMPTY;物品定义可替换音效,但仅当索引大于 0 | game/shared/cstrike15/fx_cs_shared.cpp:33-77、game/shared/cstrike15/fx_cs_shared.cpp:128-139、game/shared/cstrike15/fx_cs_shared.cpp:345-367 |
| 枪口火焰 | 新动画状态下调用 ProcessMuzzleFlashEvent;粒子回调 CS_MuzzleFlash / CS_MuzzleFlash_X | game/shared/cstrike15/fx_cs_shared.cpp:300-305、game/client/cstrike15/fx_cs_muzzleflash.cpp:26、game/client/cstrike15/fx_cs_muzzleflash.cpp:95、game/client/cstrike15/fx_cs_muzzleflash.cpp:151 |
| 弹孔与撞击 | 终点不在天空/nodraw/hint/skip 表面时 UTIL_ImpactTrace,转给被击中实体的 ImpactTrace,派发 Impact 效果;客户端回调对休眠实体跳过,贴花、自定义效果后播放撞击声 | game/shared/cstrike15/cs_player_shared.cpp:1758-1768、game/client/cdll_util.cpp:419-441、game/shared/baseentity_shared.cpp:807-834、game/client/cstrike15/fx_cs_impacts.cpp:18-47 |
| 水花 | 终点位于水或泥浆中时,带水体掩码重新追踪,在水面派发 gunshotsplash(此时不做普通弹孔) | game/shared/cstrike15/cs_player_shared.cpp:1737-1757 |
| 玩家身上的血迹 | sv_server_verify_blood_on_player(默认 1)时由服务端对被击中玩家派发 Impact,坐标相对受害者;客户端的 CCSPlayer::ImpactTrace 直接返回,避免“看见血但对方满血” | game/shared/cstrike15/cs_player_shared.cpp:1143、game/shared/cstrike15/cs_player_shared.cpp:1771-1778、game/shared/cstrike15/cs_player_shared.cpp:2290-2342 |
| 出口弹孔 | 穿透成功后对出口追踪结果再做一次 UTIL_ImpactTrace | game/shared/cstrike15/cs_player_shared.cpp:2101-2107 |
| 曳光 | 起点优先取第一人称观战目标或世界模型的 muzzle_flash 附件,否则取视图模型附件;附带动态光;按 tracer frequency 与弹匣已用发数决定画曳光,否则只播呼啸声 | game/shared/cstrike15/cs_player_shared.cpp:2349-2431 |
| 穿墙轨迹 | 记录第一次命中非玩家表面到其后命中点的线段(命中玩家后停止);长度在 (0, 800) 单位内才播放;首次命中时剩余伤害 >20 用 heavy 版本 | game/shared/cstrike15/cs_player_shared.cpp:98、game/shared/cstrike15/cs_player_shared.cpp:1387-1402、game/shared/cstrike15/cs_player_shared.cpp:1903-1933 |
| 弹壳 | EjectBrass_* 回调只打印 “Unhandled” 警告,没有实际效果 | game/client/cstrike15/fx_cs_weaponfx.cpp:20-65 |
| 调试 | sv_showimpacts 1/2/3:客户端红框、服务端蓝框 | game/shared/cstrike15/cs_player_shared.cpp:69-70、game/shared/cstrike15/cs_player_shared.cpp:1601-1604、game/shared/cstrike15/cs_player_shared.cpp:1671-1674 |
11. 射击传入物品定义索引 0 的后果(审计 U2)#
- 所有玩家射击路径都传字面量 0:
CSBaseGunFire、BurstFireRemaining和四个霰弹枪类都以局部变量nItemDefIndex = 0调用FX_FireBulletsgame/shared/cstrike15/weapon_csbasegun.cpp:634-649、game/shared/cstrike15/weapon_csbasegun.cpp:731-746、game/shared/cstrike15/weapon_nova.cpp:118-135、game/shared/cstrike15/weapon_xm1014.cpp:120-124、game/shared/cstrike15/weapon_m3.cpp:117-121、game/shared/cstrike15/weapon_sawedoff.cpp:118-122;临时实体把 0 原样带给客户端game/server/cstrike15/te_shotgun_shot.cpp:109。唯一传真实索引的是地图实体env_gunfire,它还显式拒绝索引 0game/server/effects.cpp:2334-2360。 - 查不到时整次射击被丢弃:
GetItemDefinition(0)先查定义表,查不到时返回 schema 中default块生成的默认定义,再没有则在CSTRIKE_DLL下返回NULLgame/shared/econ/econ_item_schema.cpp:8501-8526、game/shared/econ/econ_item_schema.cpp:6916-6933。FX_FireBullets得到NULL就打印开发信息并返回,此时尚未播放射击动画事件、发送临时实体、播放枪声、开启延迟补偿或调用任何FireBulletgame/shared/cstrike15/fx_cs_shared.cpp:237-242;而调用方在其返回后照常扣弹、累加精度惩罚game/shared/cstrike15/weapon_csbasegun.cpp:748-771。 - 查到时用的是错误的武器脚本:武器数据按
pItemDef->GetItemClass()查找,即 0 号或默认定义的类,而不是正在开火的武器game/shared/cstrike15/fx_cs_shared.cpp:273-286。带物品视图参数的读取(伤害、射程、穿透等)会先查开火武器自身的物品属性game/shared/cstrike15/fx_cs_shared.cpp:336-343、game/shared/cstrike15/cs_weapon_parse.cpp:1065-1071,但不带物品视图的读取(如弹丸数GetBullets())只看这份错误脚本game/shared/cstrike15/fx_cs_shared.cpp:448-453。推断:此时所有枪械共享同一套弹丸数等数据。 - 按索引分支的特例不可达:R8 副攻(64)与 Negev(28)的散布曲线
game/shared/cstrike15/fx_cs_shared.cpp:420-433、game/shared/cstrike15/fx_cs_shared.cpp:455-468,以及物品替换音效(要求索引 > 0)game/shared/cstrike15/fx_cs_shared.cpp:46。 - 不受影响的路径:刀直接派发伤害(第 3 节);投掷物与 C4 不经过
FX_FireBullets(见 投掷物与 C4)。
结论:U2 的后果已能从源码闭环——要么第 2 条(射击不产生伤害、声音与他人可见特效),要么第 3 条(数据取自错误的武器脚本),取决于运行时 schema 的内容。结合 U1(源码中找不到正常启动时填充 schema 的入口,见 物品与经济),若 schema 为空则第 2 条成立(推断,未运行验证)。仓库中没有 items_game.txt,无法判断真实 schema 是否含 0 号或 default 定义(证据不足)。六处调用方都用同一种“局部变量赋 0”写法,这表明它可能是 partner depot 裁剪时替换掉了原本的物品索引查询(推断,无直接证据)。
约束与边界#
- 后坐力
CWeaponCSBase::Recoil函数体已被删空,本页所有Recoil(...)调用都是空操作;客户端武器自定义材质(皮肤)相关的三个函数同样被删空:game/shared/cstrike15/weapon_csbase.cpp:3833-3836、game/shared/cstrike15/weapon_csbase.cpp:3851-3864。 - 射速、开镜 FOV、弹丸数、曳光频率、左轮扳机延迟、连发间隔等具体数值来自武器脚本、物品 schema 或模型动画事件,均不在仓库中;本页只描述代码如何读取它们。
- 条件编译差异:连发切换、电击枪自动丢弃、刀的伤害结算只在服务端编译;
FX_WeaponSound在服务端是空函数;WEAPON_FIRE_BULLETS_ACCURACY_FISHTAIL_FEATURE未定义。 - 未展开:双枪
weapon_elite.cpp的左右交替、沙鹰最后一发的空膛动画替换(game/shared/cstrike15/weapon_deagle.cpp:55-60)、机瞄控制器内部、盾牌相关代码、客户端开镜遮罩 HUD、Bot 的武器使用策略(见 Bot)。 - 本页未运行任何游戏或测试;“约每 0.5 秒装一发”“换弹后不自动开镜”等时序结论均由代码静态推出。