文档信息#
- 项目名称: SF6 Gemini Input Control System
- 版本: 2.0
- 最后更新: 2026-02-22
- 作者: 研究团队
- 目的: 记录通过REFramework控制SF6角色输入的完整技术方案
目录#
概述#
本文档记录了通过REFramework Lua脚本控制Street Fighter 6角色输入的完整技术方案。该系统允许:
- 通过Python GUI或Lua脚本控制P1/P2角色
- 注入虚拟输入(方向+按钮)
- 可选的硬件输入合并功能
- 支持输入序列和连招编辑
技术栈#
- 游戏引擎: RE Engine
- Hook框架: REFramework
- 脚本语言: Lua 5.4
- GUI: Python + PyQt5
- 通信方式: 文件IPC
核心发现#
1. cPlayer对象结构#
cPlayer是游戏中管理玩家输入和状态的核心对象。
访问路径:
local training_mgr = sdk.get_managed_singleton("app.training.TrainingManager")
local datas = training_mgr:call("GetPLDatas")
local data0 = datas:call("get_Item", 0) -- P1
local cPlayer = data0:get_field("cPlayer")
关键字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
pl_input_new |
int | 新输入(当前帧) |
pl_input_now |
int | 当前输入 |
pl_input_old |
int | 上一帧输入 |
pl_input_old1 |
int | 上上帧输入 |
pl_sw_new |
int | 新按钮状态 |
pl_sw_now |
int | 当前按钮状态 |
pl_sw_old |
int | 上一帧按钮状态 |
pl_cmd_now |
int | 当前命令 |
pl_cmd_buff |
int | 命令缓冲 |
input_data |
array | 输入数据数组(历史缓冲) |
act_st |
int | 动作状态 |
act_st_old |
int | 上一帧动作状态 |
act_dir |
int | 动作方向 |
move_dir |
int | 移动方向 |
free_to_move |
bool | 是否可以移动 |
auto_pilot |
bool | 是否自动驾驶 |
dummy_work |
bool | 是否为假人 |
2. 输入位掩码#
输入使用位标志(bit flags)编码:
方向位:
INPUT = {
NONE = 0,
UP = 1, -- 0b00000001
DOWN = 2, -- 0b00000010
LEFT = 4, -- 0b00000100
RIGHT = 8, -- 0b00001000
}
按钮位:
BUTTON = {
NONE = 0,
LP = 16, -- 0b00010000 (轻拳)
MP = 32, -- 0b00100000 (中拳)
HP = 64, -- 0b01000000 (重拳)
LK = 128, -- 0b10000000 (轻腿)
MK = 256, -- 0b100000000 (中腿)
HK = 512, -- 0b1000000000 (重腿)
}
组合输入:
-- 向下+轻拳 = 2 + 16 = 18
-- 向右+中拳 = 8 + 32 = 40
-- 向下+向右+重拳 = 2 + 8 + 64 = 74
3. 可Hook的方法#
通过研究发现,cPlayer对象有多个可以hook的方法:
| 方法名 | 调用频率 | 适用性 | 说明 |
|---|---|---|---|
pl_input_sub |
每帧 | ✅ 推荐 | 输入处理子函数,最稳定 |
pl_input_main |
每帧 | ⚠️ 可用 | 主输入处理函数 |
pl_cmd_check |
每帧 | ⚠️ 可用 | 命令检查函数 |
update |
每帧 | ❌ 不推荐 | 太通用,可能影响其他逻辑 |
推荐: 使用pl_input_sub,它在输入处理流程中位置合适,不会干扰其他系统。
系统架构#
整体架构图#
┌─────────────────────────────────────────────────────────────┐
│ Python GUI (ComboTrialGui) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 输入控制器 │ │ 动作编辑器 │ │ 试炼编辑器 │ │
│ └──────┬───────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ │ IPC (文件通信) │
└─────────┼────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ REFramework Lua Scripts │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ GeminiInputBridge.lua (IPC桥接) │ │
│ │ - 读取command.txt │ │
│ │ - 解析命令 │ │
│ │ - 调用Core模块 │ │
│ │ - 写入status.txt │ │
│ └────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ GeminiInputCore.lua (核心逻辑) │ │
│ │ - Hook管理 │ │
│ │ - 输入队列 │ │
│ │ - 字段写入 │ │
│ │ - 硬件输入合并 │ │
│ └────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ GeminiInputUI.lua (可选,REFramework内置UI) │ │
│ │ - 提供游戏内控制面板 │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ SDK Hook
▼
┌─────────────────────────────────────────────────────────────┐
│ Street Fighter 6 Game │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ cPlayer Object │ │
│ │ - pl_input_sub() ← Hook Point │ │
│ │ - pl_input_new/now/old │ │
│ │ - pl_sw_new/now/old │ │
│ │ - input_data[] │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
模块职责#
GeminiInputBridge.lua#
- 职责: IPC通信桥接
- 功能:
- 监听
ipc/command.txt文件 - 解析命令字符串
- 调用Core模块的API
- 更新
ipc/status.txt状态文件
- 监听
- 命令格式:
command:param1:param2:...
GeminiInputCore.lua#
- 职责: 核心输入注入逻辑
- 功能:
- 管理Hook的安装和卸载
- 维护输入队列
- 处理持续按住(hold)
- 写入cPlayer字段
- 可选的硬件输入合并
- 关键变量:
hook_installed: Hook是否已安装active: 输入注入是否激活queue: 输入队列hold_dir/hold_btn: 持续按住的方向/按钮cur_dir/cur_btn: 当前输入
GeminiInputUI.lua#
- 职责: REFramework内置UI
- 功能:
- 提供游戏内控制面板
- 可视化状态显示
- 手动测试接口
关键数据结构#
输入队列#
queue = {
{dir = 8, btn = 16, frames = 3}, -- 向右+轻拳,持续3帧
{dir = 0, btn = 0, frames = 2}, -- 释放,持续2帧
{dir = 2, btn = 64, frames = 5}, -- 向下+重拳,持续5帧
}
状态对象#
state = {
active = true, -- 是否激活
hook = true, -- Hook是否安装
hold_dir = 1, -- 持续按住的方向
hold_btn = 16, -- 持续按住的按钮
cur_dir = 8, -- 当前方向
cur_btn = 32, -- 当前按钮
qlen = 3, -- 队列长度
target = 0, -- 目标玩家索引(0=P1, 1=P2)
method = "pl_input_sub" -- Hook的方法名
}
Hook技术#
Hook时机#
REFramework提供两个Hook时机:
PreHook: 在原函数执行之前
- 用途: 读取原始硬件输入
- 时机: 游戏还未处理输入
PostHook: 在原函数执行之后
- 用途: 写入虚拟输入
- 时机: 游戏已处理完输入
Hook实现#
local function do_hook(method)
sdk.hook(
method,
function(args) -- PreHook
if use_pre then
local this = sdk.to_managed_object(args[2])
inject_now(this, true) -- 读取硬件输入
end
return sdk.PreHookResult.CALL_ORIGINAL
end,
function(retval) -- PostHook
if use_post then
inject_now(nil, false) -- 写入虚拟输入
end
return retval
end
)
end
Hook安装流程#
function install_hook()
-- 1. 初始化目标对象
if not init_target(target_index) then
return false
end
-- 2. 获取类型定义
local td = p_target:get_type_definition()
if not td then return false end
-- 3. 获取方法
local method = td:get_method("pl_input_sub")
if not method then return false end
-- 4. 安装Hook
local ok = do_hook(method)
if ok then
hook_installed = true
return true
end
return false
end
输入注入方法#
方法1: Fields+Buffer (write_strategy=0)#
最稳定的方法,写入所有相关字段。
local combined = dir_bits + btn_bits
-- 写入输入字段
o:set_field("pl_input_new", combined)
o:set_field("pl_input_now", combined)
o:set_field("pl_input_old", dir_bits)
o:set_field("pl_input_old1", dir_bits)
-- 写入按钮字段
o:set_field("pl_sw_new", combined)
o:set_field("pl_sw_now", combined)
o:set_field("pl_sw_old", btn_bits)
-- 写入input_data数组
local arr = o:get_field("input_data")
if arr then
for i = 0, span - 1 do
arr:call("set_Item", i, combined)
end
end
优点:
- 最稳定,兼容性最好
- 适用于所有场景
缺点:
- 完全覆盖硬件输入
- 不支持硬件输入合并
方法2: Buffer Only (write_strategy=1)#
仅写入缓冲区,适合合并模式。
local arr = o:get_field("input_data")
if arr then
for i = 0, span - 1 do
arr:call("set_Item", i, combined)
end
end
优点:
- 对其他字段影响小
- 适合硬件输入合并
缺点:
- 可能不够稳定
- 某些情况下输入可能被忽略
方法3: Now Fields Only (write_strategy=2)#
仅写入当前字段,实验性方法。
o:set_field("pl_input_now", combined)
o:set_field("pl_sw_now", combined)
优点:
- 最小化干扰
缺点:
- 不稳定
- 不推荐使用
硬件输入合并#
当merge_enabled=true时,系统会合并硬件输入和虚拟输入。
合并策略:
- 按轴独立合并: 水平轴和垂直轴分别处理
- 虚拟输入优先: 如果虚拟输入占用了某个轴,则忽略该轴的硬件输入
- 按钮直接合并: 虚拟按钮 OR 硬件按钮
-- 提取硬件输入的各个轴
local hw_horizontal = bit.band(hw_dir, INPUT.LEFT + INPUT.RIGHT)
local hw_vertical = bit.band(hw_dir, INPUT.UP + INPUT.DOWN)
-- 检查虚拟输入是否占用了某个轴
local virt_has_horizontal = bit.band(virt_dir, INPUT.LEFT + INPUT.RIGHT) ~= 0
local virt_has_vertical = bit.band(virt_dir, INPUT.UP + INPUT.DOWN) ~= 0
-- 只合并虚拟输入未占用的轴
if not virt_has_horizontal then
final_dir = bit.bor(final_dir, hw_horizontal)
end
if not virt_has_vertical then
final_dir = bit.bor(final_dir, hw_vertical)
end
-- 按钮直接合并
final_btn = bit.bor(virt_btn, hw_buttons)
示例:
- 虚拟输入: 向上 (1)
- 硬件输入: 向右+轻拳 (8+16=24)
- 合并结果: 向上+向右+轻拳 (1+8+16=25)
已知问题与解决方案#
问题1: 视角Bug#
现象: 使用某些write_strategy时,游戏视角会出现异常。
原因: 过度写入字段,干扰了游戏的其他系统。
解决方案:
- 使用
write_strategy=0(Fields+Buffer) - 禁用merge模式
- 确保Hook时机正确(PreHook + PostHook)
问题2: 输入延迟#
现象: 虚拟输入有明显延迟。
原因:
- Hook时机不对
- 队列处理不及时
- 帧数设置过大
解决方案:
- 使用PostHook写入
- 减少tap的frames参数(推荐1-3帧)
- 优化队列处理逻辑
问题3: 输入被忽略#
现象: 某些输入无效。
原因:
- Hook未安装
- active=false
- 字段写入不完整
解决方案:
- 确保调用
install_hook() - 确保
set_active(true) - 使用
write_strategy=0
问题4: 与硬件输入冲突#
现象: 虚拟输入和硬件输入互相干扰。
原因:
- 完全覆盖模式会屏蔽硬件输入
- 合并模式的时序问题
解决方案:
- 使用合并模式(
merge_enabled=true) - 使用PreHook读取硬件输入
- 使用PostHook写入合并后的输入
问题5: 重复Hook#
现象: 多个脚本同时hook导致冲突。
原因:
- autorun目录有多个测试脚本
- 没有使用全局单例模式
解决方案:
- 使用全局单例(
_G.__GeminiInputCore) - 清理autorun目录的测试脚本
- 只保留必要的脚本
最佳实践#
1. 配置推荐#
稳定配置 (推荐用于生产):
write_strategy = 0 -- Fields+Buffer
merge_enabled = false -- 禁用合并
use_pre = false -- 禁用PreHook
use_post = true -- 启用PostHook
input_data_span = 2 -- 缓冲区跨度
合并配置 (实验性):
write_strategy = 1 -- Buffer Only
merge_enabled = true -- 启用合并
use_pre = true -- 启用PreHook (读取硬件输入)
use_post = true -- 启用PostHook (写入合并输入)
input_data_span = 2 -- 缓冲区跨度
2. 输入序列设计#
原则:
- 每个动作后添加释放帧(0,0,2)
- 使用合理的帧数(1-5帧)
- 避免过长的队列
示例 - 波动拳:
-- 下 (2帧)
tap(INPUT.DOWN, 0, 2)
tap(0, 0, 1) -- 释放
-- 下前 (2帧)
tap(INPUT.DOWN + INPUT.RIGHT, 0, 2)
tap(0, 0, 1) -- 释放
-- 前+拳 (3帧)
tap(INPUT.RIGHT, BUTTON.LP, 3)
tap(0, 0, 2) -- 释放
3. 错误处理#
总是使用pcall:
local ok, result = pcall(function()
return obj:get_field("pl_input_now")
end)
if ok then
-- 处理result
else
-- 处理错误
end
4. 调试输出#
分级日志:
-- 关键事件
print("[INFO] Hook installed successfully")
-- 调试信息
if debug_mode then
print(string.format("[DEBUG] cur_dir=%d cur_btn=%d", cur_dir, cur_btn))
end
-- 错误信息
print("[ERROR] Failed to initialize target")
5. 性能优化#
避免每帧操作:
-- 不好
re.on_frame(function()
print("Frame") -- 每帧都输出
end)
-- 好
local frame_count = 0
re.on_frame(function()
frame_count = frame_count + 1
if frame_count % 60 == 0 then
print("60 frames passed")
end
end)
调试技巧#
1. 查看cPlayer字段#
local function dump_fields(obj)
local td = obj:get_type_definition()
local fields = td:get_fields()
for _, field in ipairs(fields) do
local name = field:get_name()
local value = obj:get_field(name)
print(string.format("%s = %s", name, tostring(value)))
end
end
2. 监控输入变化#
local last_input = 0
re.on_frame(function()
local input = cPlayer:get_field("pl_input_now")
if input ~= last_input then
print(string.format("Input changed: %d -> %d", last_input, input))
last_input = input
end
end)
3. 验证Hook状态#
print(string.format("Hook installed: %s", tostring(hook_installed)))
print(string.format("Active: %s", tostring(active)))
print(string.format("Queue length: %d", #queue))
print(string.format("Current input: dir=%d btn=%d", cur_dir, cur_btn))
4. 使用InputComparison脚本#
这个脚本可以对比真实输入和虚拟输入的差异:
-- 监控所有输入相关字段
local watch_fields = {
"pl_input_new", "pl_input_old", "pl_input_now",
"pl_sw_new", "pl_sw_old", "pl_sw_now",
"pl_cmd_now", "act_st", "act_dir"
}
for _, field in ipairs(watch_fields) do
local value = cPlayer:get_field(field)
print(string.format("%s = %d", field, value))
end
未来改进方向#
1. 更精确的时序控制#
目标: 实现帧级精确的输入控制
方案:
- 研究游戏的帧同步机制
- 实现基于帧计数的输入调度
- 支持负边输入(negative edge)
2. 录制与回放#
目标: 录制玩家输入并回放
方案:
- 监控硬件输入
- 保存输入序列到文件
- 实现回放功能
- 支持循环播放
3. AI集成#
目标: 集成AI模型进行自动操作
方案:
- 读取游戏状态(角色位置、血量等)
- AI决策输入
- 实时注入输入
4. 网络对战支持#
目标: 支持在线对战中的输入控制
挑战:
- 网络延迟
- 反外挂检测
- 同步问题
注意: 在线对战中使用输入控制可能违反游戏条款,仅用于研究目的。
5. 跨平台支持#
目标: 支持其他RE Engine游戏
方案:
- 抽象化cPlayer接口
- 适配不同游戏的字段名
- 统一的配置系统
附录#
A. 完整的输入位掩码表#
| 输入 | 十进制 | 十六进制 | 二进制 |
|---|---|---|---|
| NONE | 0 | 0x0 | 0b0000000000 |
| UP | 1 | 0x1 | 0b0000000001 |
| DOWN | 2 | 0x2 | 0b0000000010 |
| LEFT | 4 | 0x4 | 0b0000000100 |
| RIGHT | 8 | 0x8 | 0b0000001000 |
| LP | 16 | 0x10 | 0b0000010000 |
| MP | 32 | 0x20 | 0b0000100000 |
| HP | 64 | 0x40 | 0b0001000000 |
| LK | 128 | 0x80 | 0b0010000000 |
| MK | 256 | 0x100 | 0b0100000000 |
| HK | 512 | 0x200 | 0b1000000000 |
B. 常用组合输入#
| 组合 | 计算 | 值 |
|---|---|---|
| 下+轻拳 | 2+16 | 18 |
| 下+中拳 | 2+32 | 34 |
| 下+重拳 | 2+64 | 66 |
| 右+轻拳 | 8+16 | 24 |
| 右+中拳 | 8+32 | 40 |
| 下右+轻拳 | 2+8+16 | 26 |
| 下右+重拳 | 2+8+64 | 74 |
C. IPC命令参考#
| 命令 | 格式 | 示例 | 说明 |
|---|---|---|---|
| tap | tap:dir:btn:frames |
tap:8:16:3 |
点按输入 |
| hold_dir | hold_dir:bits |
hold_dir:1 |
持续按住方向 |
| hold_btn | hold_btn:bits |
hold_btn:16 |
持续按住按钮 |
| clear_hold | clear_hold |
clear_hold |
清除持续输入 |
| set_active | set_active:bool |
set_active:true |
设置激活状态 |
| install_hook | install_hook |
install_hook |
安装Hook |
| set_target | set_target:index |
set_target:0 |
设置目标玩家 |
| set_write_strategy | set_write_strategy:n |
set_write_strategy:0 |
设置写入策略 |
| set_merge | set_merge:bool |
set_merge:true |
设置合并模式 |
| set_hook_timing | set_hook_timing:pre:post |
set_hook_timing:true:true |
设置Hook时机 |
D. 参考资料#
- REFramework文档: https://github.com/praydog/REFramework
- RE Engine逆向工程: https://residentevilmodding.boards.net/
- Lua 5.4参考手册: https://www.lua.org/manual/5.4/
版本历史#
v2.0 (2026-02-22)#
- 添加硬件输入合并功能
- 优化Hook时机(PreHook + PostHook)
- 添加write_strategy配置
- 改进调试输出
- 修复视角bug
v1.0 (2026-01-22)#
- 初始版本
- 基本的输入注入功能
- IPC通信
- Python GUI
致谢#
感谢所有参与研究和开发的成员,以及REFramework社区的支持。
文档结束