输入与命令:从按键到 CUserCmd 与命令缓冲
本页沿着“物理按键 → 引擎输入事件 → 按键绑定 → 控制台命令 → 客户端按钮状态”这条链解释 CS:GO 的输入管线,并说明所有控制台命令共用的命令缓冲(Cbuf_*)如何按来源做权限检查、客户端发给服务器的字符串命令如何被分派与限流,以及配置文件按什么顺序执行。它是 输入命令与预测 的上游:那一页从 CInput::CreateMove 写好 CUserCmd 开始,本页解释 CUserCmd::buttons 里的位从哪里来。
相关页面:引擎总览 · 主循环与 Tick · 输入命令与预测 · 基础库层(ConVar 注册与标志) · VGUI 与本地化 · 服务器运维面 · 比赛模式(游戏模式 cfg)
相关源码文件#
- 设备采集:
inputsystem/inputsystem.cpp、inputsystem/inputsystem.h - 消息泵与事件分发:
engine/sys_dll2.cpp、engine/sys_mainwind.cpp、engine/keys.cpp - 客户端按钮状态:
game/client/in_main.cpp、game/client/kbutton.h、game/client/in_mouse.cpp - 命令缓冲与执行:
engine/cmd.cpp、engine/cmd.h、tier1/commandbuffer.cpp、public/tier1/convar.h、engine/cvar.cpp - 网络字符串命令:
engine/sv_client.cpp、engine/baseclient.cpp、engine/baseclientstate.cpp、engine/cdll_engine_int.cpp、engine/vengineserver_impl.cpp、game/server/client.cpp - 配置执行:
engine/host.cpp、game/shared/multiplay_gamerules.cpp、game/server/gameinterface.cpp、game/server/game.cpp
1. 全景#
证据:窗口过程把消息转成事件 inputsystem/inputsystem.cpp:1600-1605、inputsystem/inputsystem.cpp:588-599;消息泵在 PeekMessageW 之后调用 PollInputState 与 DispatchAllStoredGameMessages engine/sys_dll2.cpp:982-1025;分发函数把按键类事件交给 Key_Event engine/sys_mainwind.cpp:397-409、engine/sys_mainwind.cpp:495-503;过滤链与引擎绑定处理 engine/keys.cpp:1302-1366;绑定文本以 kCommandSrcUserInput 入缓冲 engine/keys.cpp:1153-1170;+attack 注册为 IN_AttackDown game/client/in_main.cpp:1955-1956、game/client/in_main.cpp:738-741;CreateMove 写按钮位 game/client/in_main.cpp:1442-1443。
2. 设备采集:inputsystem#
CInputSystem 是一个 AppSystem(启动器模块表中加载,见 接口工厂与 AppSystem),负责把操作系统输入转成统一的 InputEvent_t 事件流。
| 环节 | 行为 | 证据 |
|---|---|---|
| 挂接窗口 | AttachToWindow 用 SetWindowLongPtrW 子类化游戏窗口的窗口过程,并注册原始鼠标输入(Raw Input) | inputsystem/inputsystem.cpp:386-419 |
| 窗口消息 | WindowProc 在禁用时直接链回原窗口过程;WM_INPUT 只累加原始鼠标位移到 m_mouseRawAccumX/Y | inputsystem/inputsystem.cpp:1600-1605、inputsystem/inputsystem.cpp:1858-1876 |
| 入队 | PostEvent 写入 m_InputState[m_bIsPolling]:泵消息期间写 INPUT_STATE_QUEUED,轮询期间写 INPUT_STATE_CURRENT | inputsystem/inputsystem.cpp:588-599、inputsystem/inputsystem.h:216-217 |
| 每帧轮询 | PollInputState 先把排队状态(含事件)复制到当前状态,再 SampleDevices,最后把当前状态(不含事件)复制回排队状态以清空队列 | inputsystem/inputsystem.cpp:1047-1086、inputsystem/inputsystem.cpp:666-686 |
| 手柄与控制器 | SampleDevices 在 joystick_force_disabled 为假时轮询 XInput 与通用摇杆,并始终轮询 Steam Controller | inputsystem/inputsystem.cpp:1123-1141 |
| 读取 | GetEventCount/GetEventData 返回当前状态的事件数组 | inputsystem/inputsystem.cpp:1314-1322 |
原始鼠标位移不走事件流:client.dll 的 m_rawinput(默认 1)开启时,CInput 直接读取 GetRawMouseAccumulators,并跳过基于光标回中的 AccumulateMouse:game/client/in_mouse.cpp:120、game/client/in_mouse.cpp:365-378、game/client/in_mouse.cpp:614-629。
3. 引擎侧:消息泵与事件分发#
客户端主循环每轮先 PumpMessages 再 eng->Frame()(见 客户端启动链):engine/sys_dll2.cpp:1126-1155。PumpMessages 的顺序是:
PeekMessageW取出全部窗口消息;启用 Scaleform 时,键盘/鼠标按键与 IME 消息先交给 Scaleform 的 IME 预处理,再TranslateMessage/DispatchMessageW(后者会进入CInputSystem::WindowProc):engine/sys_dll2.cpp:982-1003。PollInputState( GetBaseLocalClient().IsActive() )采样设备并固化本帧事件:engine/sys_dll2.cpp:1010-1011。game->DispatchAllStoredGameMessages()逐个分发事件:engine/sys_dll2.cpp:1020、engine/sys_mainwind.cpp:495-503。
CGame::DispatchInputEvent 按事件类型分流:engine/sys_mainwind.cpp:397-492
| 事件类型 | 去向(先到先得) |
|---|---|
IE_ButtonPressed/DoubleClicked/Released、IE_KeyTyped、IE_KeyCodeTyped | 全部交给 Key_Event(第 4 节) |
IE_AnalogValueChanged(摇杆轴) | Scaleform → VGUI surface → client.dll HandleGameUIEvent |
IE_AnalogValueChanged(鼠标轴/滚轮) | VGUI surface → Scaleform(vgui_panel_active 为真时跳过,避免控制台下方的 Scaleform 抢输入)→ HandleGameUIEvent |
| 其他(窗口激活、关闭、尺寸变化等) | VGUI surface → Scaleform → g_GameMessageHandlers 表中的 CGame::HandleMsg_* |
由于按键事件在 eng->Frame() 之前分发,而绑定只是把文本放入命令缓冲,+attack 之类的命令要等到本帧 _Host_RunFrame 中的 Cbuf_Execute 才真正执行(推断,依据是上述调用顺序与第 7 节的执行时机)。
4. Key_Event:优先级链与“松开归属”#
引擎为每个按键码维护一份 KeyInfo_t:绑定字符串、按下状态,以及“松开事件应交给谁”(m_nKeyUpTarget):engine/keys.cpp:40-58、engine/keys.cpp:79。
Key_Event 的前置处理:DEDICATED 构建直接返回;按键盘/手柄确定分屏玩家槽;重复的按下/松开被丢弃;按键捕获模式(绑定界面用)优先;VGUI 未初始化时不处理;无论谁消费都先同步 VGUI 的按键状态;加载画面期间忽略 ESC:engine/keys.cpp:1243-1300。随后按下表顺序尝试,任一环节返回“已消费”即停止:
| 顺序 | 目标 | 处理函数与条件 | 证据 |
|---|---|---|---|
| 1 | 工具框架 | HandleToolKey:最上层工具的 TrapKey;不接收字符事件 | engine/keys.cpp:963-971、engine/keys.cpp:1303-1304 |
| 2 | VGUI | HandleVGuiKey → EngineVGui()->Key_Event;ESC 和反引号按下不走这一步 | engine/keys.cpp:977-988、engine/keys.cpp:1310-1314 |
| 3 | Scaleform | HandleScaleformKey(INCLUDE_SCALEFORM);ESC/反引号仅在 CS:GO 聊天框或绑定菜单打开时才先给 Scaleform | engine/keys.cpp:1316-1327 |
| 4 | GameUI | HandleGameUIKey → g_ClientDLL->HandleGameUIEvent | engine/keys.cpp:1087-1093、engine/keys.cpp:1331-1333 |
| 5 | 客户端 | HandleClientKey → IN_KeyEvent → CInput::KeyEvent → ClientMode::KeyInput;返回 0 表示吞掉;字符事件不交给客户端 | engine/keys.cpp:1068-1081、game/client/cdll_client_int.cpp:2127-2130、game/client/in_main.cpp:783-799 |
| 6 | ESC 回退 | 仅 ESC:控制台窗口、服务器浏览器打开或正在播放电影时跳过 Scaleform,再给 VGUI | engine/keys.cpp:1341-1363 |
| 7 | 引擎绑定 | HandleEngineKey:查绑定表,生成命令文本 | engine/keys.cpp:1100-1171、engine/keys.cpp:1365-1366 |
松开归属:FilterKey 在某个目标消费了按下事件时,把该键的 m_nKeyUpTarget 记为这个目标;之后该键的松开与字符事件只交给同一目标(未被认领时任何目标都可接收),目标收到属于自己的松开事件即视为消费:engine/keys.cpp:1180-1218、engine/keys.cpp:94-99。这保证“按下进了 UI、松开却落到游戏”或反之的情况不会出现,从而 +attack/-attack 总是成对(推断,依据是该归属逻辑)。
HandleEngineKey 生成的命令:
- 未绑定的手柄键只打印提示;空绑定不处理:
engine/keys.cpp:1109-1127。 - 非
DEDICATED构建下,未连接服务器时只有toggleconsole绑定生效:engine/keys.cpp:1129-1137。 - 松开:只有以
+开头的绑定产生-xxx <键码>;按下:+绑定产生+xxx <键码>,其余绑定原样入队;按住修饰键时吞掉toggleconsole。所有文本都以kCommandSrcUserInput加入当前分屏玩家的缓冲:engine/keys.cpp:1139-1170。
5. 绑定表#
Key_SetBinding:手柄键码先映射到当前分屏槽,替换旧字符串,并通知 Scaleform(UpdateBindingForButton)与 client.dll(OnKeyBindingChanged):engine/keys.cpp:114-155。bind <key> [command]:只给键名时打印当前绑定;绑定 ESC 时强制改为cancelselect:engine/keys.cpp:269-328。Key_WriteBindings把绑定以bind "键" "命令"写入配置缓冲(config.cfg的来源之一):engine/keys.cpp:530-558。- 读取配置后若一个绑定都没有则加载默认绑定,并保证 ESC 为
cancelselect、toggleconsole至少绑定在反引号:engine/host.cpp:2060-2075。
6. +attack 到 CUserCmd::buttons#
client.dll 用 kbutton_t 表示一个“按钮命令”的状态,每个分屏玩家一份:down[2] 记录最多两个按住它的键码,state 的位 1/2/4 分别表示“按住 / 本帧按下脉冲 / 本帧松开脉冲”:game/client/kbutton.h:16-29、game/client/in_main.cpp:803-822。
| 步骤 | 行为 | 证据 |
|---|---|---|
+attack <键码> | KeyDown 记录键码(同一键重复按下被忽略,第三个键被拒绝),未按住时置按住位与按下脉冲(state |= 1+2) | game/client/in_main.cpp:424-453 |
-attack <键码> | KeyUp 移除对应键码;两个键都松开才清除按住位并置松开脉冲;无参数时强制全部松开 | game/client/in_main.cpp:460-490 |
| 每 tick 汇总 | CalcButtonBits:state & 3 非零即置位(因此一帧内“按下又松开”也会被记入);bResetState 时清除按下脉冲,被 ClearInputButton 标记的按钮连按住位一起清除 | game/client/in_main.cpp:1738-1761 |
| 附加位 | GetButtonBits 还合并蹲/走切换键、IN_CANCEL、HUD 武器位,最后去掉被要求重新按下的位 | game/client/in_main.cpp:1771-1858 |
| 写入命令 | CreateMove 用 GetButtonBits(true) 写 cmd->buttons(带重置);帧间的 ExtraMouseSample 用 GetButtonBits(false)(不重置) | game/client/in_main.cpp:1442-1443、game/client/in_main.cpp:1246、game/client/in_main.cpp:1320 |
之后的命令编码、发送与服务端执行见 输入命令与预测。
7. 命令缓冲:Cbuf_*#
缓冲划分:每个分屏玩家一个缓冲,外加一个 CBUF_SERVER:engine/cmd.h:149-156。全部缓冲由一个互斥量保护:engine/cmd.cpp:63-66。
命令来源 cmd_source_t 决定后续权限检查:public/tier1/convar.h:85-122
| 来源 | 典型产生者 | 限制 |
|---|---|---|
kCommandSrcCode | 引擎/游戏代码直接入队 | 基本不限制 |
kCommandSrcClientCmd | client.dll 调用 engine->ClientCmd(且 m_bRestrictClientCommands 为真) | 只能执行带 FCVAR_CLIENTCMD_CAN_EXECUTE 的命令 |
kCommandSrcUserInput | 控制台输入、按键绑定 | 不限制;转发给服务器时客户端限流 16 条/秒 |
kCommandSrcNetClient | 服务器收到的 net_StringCmd | 见第 9 节 |
kCommandSrcNetServer | 客户端收到服务器下发的 net_StringCmd | 只能执行带 FCVAR_SERVER_CAN_EXECUTE 的命令 |
kCommandSrcDemoFile | demo 回放 | 注释明确说明未按标志限制 |
入队与执行:
Cbuf_AddText加锁后追加,溢出时只打印buffer overflow并丢弃;Cbuf_InsertText只允许在缓冲正在处理时调用(别名与exec用它把展开内容插到当前位置):engine/cmd.cpp:185-207。Cbuf_Execute:不在主线程时告警;逐个缓冲执行,正在处理中的缓冲被跳过(防重入);为玩家缓冲设置对应分屏槽;每条命令经Cmd_ExecuteCommand执行,未带FCVAR_DONTRECORD的命令同时写入正在录制的 demo:engine/cmd.cpp:232-246、engine/cmd.cpp:252-304。- 执行时机:
_Host_RunFrame每帧(所有构建)执行一次Cbuf_Execute;非DEDICATED构建在每个 tick 的输入阶段(ClientDLL_ProcessInput之后、CL_Move之前)再执行一次:engine/host.cpp:4112-4146、engine/host.cpp:3256、engine/host.cpp:3276-3292。此外 RCON、ServerExecute等路径会就地调用Cbuf_Execute(例如engine/vengineserver_impl.cpp:962-965)。
8. Cmd_ExecuteCommand:分派与权限检查#
证据:执行标记与别名 engine/cmd.cpp:935-959;服务器下发命令过滤 engine/cmd.cpp:869-895、engine/cmd.cpp:964-968;ClientCmd 来源过滤 engine/cmd.cpp:898-922、engine/cmd.cpp:971-973;FCVAR_GAMEDLL 命令在监听服务器上设置“命令发起者”、在仅作客户端时转发 engine/cmd.cpp:975-1021;作弊、单人、开发命令检查与分派 engine/cmd.cpp:1024-1062;ConVar 分支与未知命令转发 engine/cmd.cpp:1066-1083。
各 FCVAR_* 标志在哪里生效:
| 标志 | ConCommand(Cmd_ExecuteCommand) | ConVar(CCvarUtilities::IsCommand) |
|---|---|---|
FCVAR_CHEAT | 需要 CanCheat()(发行构建即 sv_cheats);服务器以 FCVAR_SERVER_CAN_EXECUTE 下发的除外:engine/cmd.cpp:1024-1044、engine/gl_cvars.h:30-43 | 多人游戏且不能作弊时拒绝;HLTV 客户端与 demo 回放例外:engine/cvar.cpp:437-453 |
FCVAR_SPONLY | 多人游戏拒绝:engine/cmd.cpp:1046-1053 | 已连接到多人服务器时拒绝:engine/cvar.cpp:401-415 |
FCVAR_DEVELOPMENTONLY | 当作未知命令:engine/cmd.cpp:1055-1059 | 不可见:engine/cvar.cpp:390-392 |
FCVAR_NOT_CONNECTED | — | 已连接时拒绝,除非是 client.dll 允许修改的 FCVAR_USERINFO:engine/cvar.cpp:417-435 |
FCVAR_REPLICATED | — | 本机不是服务器但已连接时,客户端控制台不能修改(demo 回放除外):engine/cvar.cpp:455-475 |
转发到服务器:Cmd_ForwardToServer 去掉 cmd 前缀;demo 回放时交给 client.dll;对 kCommandSrcUserInput 来源按每秒 16 条限流(注释说明是为了避免触发服务器端配额踢人),然后 SendStringCmd:engine/cmd.cpp:1105-1155、engine/cmd.cpp:76-85。
9. 客户端 → 服务器:net_StringCmd 的分派与限制#
证据与细节:
- 引擎把
net_StringCmd绑定到CBaseClient::NETMsg_StringCmd,它调用虚函数ExecuteStringCommand;基类只处理demorestart:engine/baseclient.cpp:975、engine/baseclient.cpp:1038-1040、engine/baseclient.cpp:1575-1588。 CGameClient::ExecuteStringCommand:以kCommandSrcNetClient分词;sv_quota_stringcmdspersecond(默认 40)为 0 时静默拒绝全部字符串命令,超出配额则以 DoS 理由断开客户端:engine/sv_client.cpp:1513-1550、engine/sv_client.cpp:67。- 引擎白名单
s_clcommands(status、pause、setpause、unpause、ping、ss_connect等)直接进入Cmd_ExecuteCommand( CBUF_SERVER, … ):engine/sv_client.cpp:88-110、engine/sv_client.cpp:1352-1364、engine/sv_client.cpp:1552-1556。 - 带
FCVAR_GAMEDLL的 ConCommand:多人且不能作弊时拒绝作弊命令;否则除非sv_allow_legacy_cmd_execution_from_client(默认 0)开启、命令带FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS,或发起者是监听服务器的本机玩家,一律忽略;多人拒绝FCVAR_SPONLY;通过后设置命令发起者并Cmd_Dispatch:engine/sv_client.cpp:1558-1601、engine/sv_client.cpp:73。例如say、say_team就是带FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS的 server.dll ConCommand:game/server/client.cpp:914、game/server/client.cpp:934。 - 其余命令(不是 ConCommand 的游戏命令,例如 CS:GO 玩家类识别的
jointeam、buy,以及客户端误发的 ConVar 名)交给服务器插件,插件未返回PLUGIN_STOP时进入CServerGameClients::ClientCommand→::ClientCommand→ 规则对象ClientCommand(CCSGameRules先处理少数规则命令,再交给CCSPlayer::ClientCommand、基类规则与 Bot 管理器),都不认识时回显Unknown command:engine/sv_client.cpp:1602-1607、engine/sv_plugin.cpp:556-570、game/server/gameinterface.cpp:3236-3240、game/server/client.cpp:1775-1781、game/server/client.cpp:1854-1868、game/shared/cstrike15/cs_gamerules.cpp:5106-5115、game/server/cstrike15/cs_player.cpp:8962、game/server/cstrike15/cs_player.cpp:9152。因此远程客户端不能通过字符串命令直接设置服务器 ConVar(推断,依据是该分支结构中没有 ConVar 设置路径)。
10. 服务器 → 客户端:下发命令#
- server.dll 调用
engine->ClientCommand(edict, fmt, …)时,引擎校验实体编号后构造CNETMsg_StringCmd发给该客户端:engine/vengineserver_impl.cpp:977-1002。 - 客户端
InternalProcessStringCmd:未开启限制(m_bRestrictServerCommands,初始为真)或本机正在运行服务器时以kCommandSrcCode入队;否则以kCommandSrcNetServer入队,由第 8 节的FCVAR_SERVER_CAN_EXECUTE检查把关:engine/baseclientstate.cpp:2368-2389、engine/baseclientstate.cpp:494。 - client.dll 自己调用的
ClientCmd在m_bRestrictClientCommands为真时使用kCommandSrcClientCmd;ClientCmd_Unrestricted按是否来自控制台/按键选择kCommandSrcUserInput或kCommandSrcCode:engine/cdll_engine_int.cpp:847-895。
11. 配置文件执行顺序#
exec <file> [pathID] 的规则:路径拼成 //<pathID>/cfg/<file>(默认 path ID 为 *)并补 .cfg 扩展名;拒绝非法路径、非法扩展名与超过 1 MB 的文件;autoexec.cfg、joystick.cfg、game.cfg 缺失时不报错;文件内容逐行立即执行到当前正在处理的缓冲中(而不是追加到末尾);execwithwhitelist 只执行白名单中的命令:engine/cmd.cpp:500-657、engine/cmd.cpp:659-672。
| 时机 | 执行内容 | 条件 | 证据 |
|---|---|---|---|
| 创建窗口之前 | Host_ReadPreStartupConfiguration 只从 config.cfg 中抽取少数影响启动的 ConVar(声音、UI 等) | 先找 usrlocal,再找 mod | engine/sys_dll2.cpp:1341-1346、engine/host.cpp:2191-2240 |
Host_Init 中 | Host_ReadConfiguration:可选从 Steam Cloud 拉取,然后 exec config.cfg usrlocal / exec config.cfg mod / exec config_default.cfg(三选一),立即 Cbuf_Execute,再补默认绑定;使用默认配置时立刻回写 config.cfg | 仅非 DEDICATED 构建,且非专用服务器 | engine/host.cpp:5709-5712、engine/host.cpp:2003-2006、engine/host.cpp:2023-2058、engine/host.cpp:2079-2088 |
Host_Init 末尾 | exec valve.rc;若 //mod/cfg/modsettings.cfg 存在则 exec modsettings.cfg mod | 所有构建;只入队,下一次 Cbuf_Execute 时执行 | engine/host.cpp:5742-5756 |
valve.rc 之后 | autoexec.cfg、命令行 +xxx 参数(stuffcmds) | 证据不足:两者都应由 valve.rc 触发,但仓库不含任何 .rc/.cfg 内容;代码侧只有 stuffcmds 命令实现、对缺失 autoexec.cfg 的静默处理,以及“在命令行与 autoexec.cfg 解析后初始化网络”的注释 | engine/cmd.cpp:357-420、engine/cmd.cpp:570-580、engine/host.cpp:4140-4144 |
每次 GameInit(换图、重开) | exec game.cfg(Xbox/PS3 专用服务器换成对应文件名),随后 ServerExecute | — | game/server/gameinterface.cpp:1066-1081 |
| 规则对象构造 | 专用服务器 exec <servercfgfile>(默认 server.cfg),监听服务器 exec <lservercfgfile>(默认 listenserver.cfg) | — | game/shared/multiplay_gamerules.cpp:275-311、game/server/game.cpp:31-35 |
| 游戏模式切换 | ExecGameTypeCfg 按 game_type/game_mode 执行模式配置 | 见 比赛模式 | engine/sv_main.cpp:2847-2850 |
| 进程退出 | Host_WriteConfiguration( -1, "config.cfg" ) 保存绑定与归档 ConVar | 非 DEDICATED | engine/host.cpp:6437-6440、engine/host.cpp:1546 |
专用服务器没有按键与绑定:控制台文本由 dedicated 的 ProcessConsoleInput 读出,经 CDedicatedServerAPI::AddConsoleText 进入 CBUF_SERVER:dedicated/sys_common.cpp:103-120、engine/sys_dll2.cpp:2898-2901。
约束与边界#
- 单线程约束:
Cbuf_Execute只应在主循环线程调用,否则打印告警并在调试器下中断:engine/cmd.cpp:256-260。 wait命令在本快照中很可能不可用(推断):CCommandBuffer只有在IsWaitEnabled()为真时才把wait当作延迟处理tier1/commandbuffer.cpp:235-241;但m_bWaitEnabled既不在构造函数中初始化,也没有任何调用者执行SetWaitEnabledtier1/commandbuffer.cpp:31-40、public/tier1/commandbuffer.h:78-79;sv_allow_wait_command只被定义和extern声明,没有读取点engine/baseserver.cpp:222、engine/cmd.cpp:48。静态缓冲对象零初始化意味着该开关为假,wait会落入普通命令查找。- demo 命令不受标志限制:
kCommandSrcDemoFile的注释承认 demo 中的命令可能来自不可信来源但尚未加限制:public/tier1/convar.h:113-118。 - 配额两端不一致:客户端只对用户输入限流 16 条/秒,服务端配额默认 40 条/秒且超出即断开;由代码发出的转发命令不受客户端限流:
engine/cmd.cpp:76-85、engine/sv_client.cpp:1529-1550。 - 条件编译:
Key_Event与Cmd_ForwardToServer在DEDICATED中为空实现;Scaleform 相关分支依赖INCLUDE_SCALEFORM;ESC/反引号的 Scaleform 特例依赖CSTRIKE15:engine/keys.cpp:1245-1247、engine/cmd.cpp:1107-1108、engine/keys.cpp:1322-1329。 - 本页未展开:手柄/Steam Controller 的轴映射与死区、IME 事件、
CInputStackSystem的输入上下文栈(inputsystem/inputstacksystem.cpp:30)、分屏输入分配、CInput的鼠标灵敏度与加速、CVerifiedUserCmd。