← Back to Blog

Bongo Cat连点器脚本

基于Windows的连续键盘输入

针对Steam上的Bongo Cat,刷点击次数与成就。

Mac用户只需下载一个连点器即可完成,Windows用户的表情成就也可以使用连点器完成。但由于大部分Windows连点器产生的模拟输入事件无法被游戏接收,也无法计入点击次数,因此使用脚本通过键盘输入累计计数。

最终功能:

  • 使用F13–F24功能键,12个工作线程,并通过全局锁避免多个模拟键同时按下。

  • 按住 Shift、Ctrl、Alt 时自动暂停,松开后延迟恢复。

  • 微信、QQ、Steam 位于前台时自动暂停。

  • 支持设置自动停止时间。

  • 输出运行状态和暂停原因。

适用环境:Windows 10/11,Steam 版 Bongo Cat。

一、准备工作

1. 安装 Python

如果电脑还没有安装 Python,可以前往 Python 官网 下载安装。

安装后打开终端,输入:

python --version

能够显示 Python 版本号即可。

2. 安装 pynput

本教程使用 pynput 模拟键盘输入。

在 PowerShell 或 CMD 中执行:

python -m pip install pynput

二、完整代码

import time
import threading
import ctypes
from ctypes import wintypes
from pynput import keyboard

# ========== 配置 ==========

RATE = 5                 # 每个线程每秒目标发送次数
PRESS_TIME = 0.03        # 单次按键持续时间(秒)
MAX_SECONDS = 1800       # 自动停止时间,0表示不限时
MODIFIER_DELAY = 2.0     # 修饰键操作后的恢复延迟

KEYS = [
    getattr(keyboard.Key, f"f{i}")
    for i in range(13, 25)
]

BLOCKED_PROCESSES = {
    "wechat.exe",
    "weixin.exe",
    "qq.exe",
    "steam.exe",
    "steamwebhelper.exe",
}

# ========== 状态 ==========

running = threading.Event()
shutdown = threading.Event()
allowed = threading.Event()

lock = threading.RLock()
send_lock = threading.Lock()

pressed_hotkeys = set()
held_modifiers = set()

start_time = None
resume_after = 0.0
last_status = None

# ========== Windows API ==========

user32 = ctypes.WinDLL(
    "user32", use_last_error=True
)
kernel32 = ctypes.WinDLL(
    "kernel32", use_last_error=True
)

user32.GetForegroundWindow.restype = wintypes.HWND

user32.GetWindowThreadProcessId.argtypes = [
    wintypes.HWND,
    ctypes.POINTER(wintypes.DWORD)
]

kernel32.OpenProcess.argtypes = [
    wintypes.DWORD,
    wintypes.BOOL,
    wintypes.DWORD
]
kernel32.OpenProcess.restype = wintypes.HANDLE

kernel32.QueryFullProcessImageNameW.argtypes = [
    wintypes.HANDLE,
    wintypes.DWORD,
    wintypes.LPWSTR,
    ctypes.POINTER(wintypes.DWORD)
]
kernel32.QueryFullProcessImageNameW.restype = wintypes.BOOL

kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
kernel32.CloseHandle.restype = wintypes.BOOL

user32.GetAsyncKeyState.argtypes = [ctypes.c_int]
user32.GetAsyncKeyState.restype = ctypes.c_short


def get_foreground_process():
    hwnd = user32.GetForegroundWindow()

    if not hwnd:
        return None

    pid = wintypes.DWORD()

    user32.GetWindowThreadProcessId(
        hwnd, ctypes.byref(pid)
    )

    if not pid.value:
        return None

    handle = kernel32.OpenProcess(
        0x1000, False, pid.value
    )

    if not handle:
        return None

    try:
        buf = ctypes.create_unicode_buffer(1024)
        size = wintypes.DWORD(len(buf))

        ok = kernel32.QueryFullProcessImageNameW(
            handle, 0, buf, ctypes.byref(size)
        )

        if not ok:
            return None

        return buf.value.rsplit(
            "\\", 1
        )[-1].lower()

    finally:
        kernel32.CloseHandle(handle)


# ========== 修饰键检测 ==========

MODIFIER_KEYS = {
    keyboard.Key.shift,
    keyboard.Key.shift_l,
    keyboard.Key.shift_r,
    keyboard.Key.ctrl,
    keyboard.Key.ctrl_l,
    keyboard.Key.ctrl_r,
    keyboard.Key.alt,
    keyboard.Key.alt_l,
    keyboard.Key.alt_r,
    keyboard.Key.alt_gr,
}


def physical_modifiers():
    result = []

    for vk, name in [
        (0x10, "Shift"),
        (0x11, "Ctrl"),
        (0x12, "Alt"),
    ]:
        if user32.GetAsyncKeyState(vk) & 0x8000:
            result.append(name)

    return result


def pause_for_modifier():
    global resume_after

    with lock:
        resume_after = max(
            resume_after,
            time.monotonic() + MODIFIER_DELAY
        )

    allowed.clear()


def modifier_active():
    with lock:
        listener_detected = bool(held_modifiers)

    physical = physical_modifiers()

    if listener_detected or physical:
        pause_for_modifier()
        return True

    return False


# ========== 状态管理 ==========

def get_status():
    if shutdown.is_set():
        return "退出"

    if not running.is_set():
        return "手动暂停"

    if modifier_active():
        return "修饰键按下,自动暂停"

    with lock:
        deadline = resume_after

    if time.monotonic() < deadline:
        return "修饰键冷却中,自动暂停"

    process = get_foreground_process()

    if process is None:
        return "前台窗口无法识别,自动暂停"

    if process in BLOCKED_PROCESSES:
        return f"{process} 在前台,自动暂停"

    return "运行中"


def print_status(status):
    global last_status

    if status == last_status:
        return

    last_status = status
    now = time.strftime("%H:%M:%S")

    if status == "运行中":
        print(f"[{now}] 连点器恢复")
    elif status == "退出":
        print(f"[{now}] 程序退出")
    else:
        print(f"[{now}] {status}")


# ========== 状态监控 ==========

def monitor():
    while not shutdown.is_set():
        with lock:
            start = start_time

        if (
            MAX_SECONDS > 0
            and running.is_set()
            and start is not None
            and time.monotonic() - start >= MAX_SECONDS
        ):
            running.clear()
            allowed.clear()
            print("达到时间上限,自动暂停")

        status = get_status()

        if status == "运行中":
            allowed.set()
        else:
            allowed.clear()

        print_status(status)
        shutdown.wait(0.01)


# ========== 工作线程 ==========

def worker(worker_id):
    kb = keyboard.Controller()
    key = KEYS[worker_id]

    interval = 1.0 / RATE
    offset = worker_id / (RATE * len(KEYS))

    next_press = 0.0
    active = False

    try:
        while not shutdown.is_set():
            if not allowed.is_set():
                active = False
                shutdown.wait(0.01)
                continue

            if not active:
                next_press = (
                    time.monotonic() + offset
                )
                active = True

            now = time.monotonic()

            if now < next_press:
                shutdown.wait(
                    min(next_press - now, 0.01)
                )
                continue

            # 全局发送锁
            # 同一时刻只模拟一个功能键
            with send_lock:
                if shutdown.is_set():
                    break

                if (
                    not allowed.is_set()
                    or modifier_active()
                ):
                    next_press = time.monotonic() + interval
                    continue

                with lock:
                    cooling = (
                        time.monotonic() < resume_after
                    )

                if cooling:
                    next_press = time.monotonic() + interval
                    continue

                try:
                    kb.press(key)

                    deadline = (
                        time.monotonic() + PRESS_TIME
                    )

                    while time.monotonic() < deadline:
                        if (
                            shutdown.is_set()
                            or not allowed.is_set()
                            or modifier_active()
                        ):
                            break

                        remaining = (
                            deadline - time.monotonic()
                        )

                        shutdown.wait(
                            min(max(0, remaining), 0.003)
                        )

                finally:
                    kb.release(key)

            next_press = max(
                next_press + interval,
                time.monotonic()
            )

    except Exception as e:
        print(f"工作线程异常:{e}")
        shutdown.set()

    finally:
        try:
            kb.release(key)
        except Exception:
            pass


# ========== 键盘监听 ==========

def on_press(key):
    global start_time

    if key in KEYS:
        return

    if key in MODIFIER_KEYS:
        with lock:
            held_modifiers.add(key)

        pause_for_modifier()
        return

    if key in (keyboard.Key.f8, keyboard.Key.esc):
        with lock:
            if key in pressed_hotkeys:
                return

            pressed_hotkeys.add(key)

    if key == keyboard.Key.f8:
        if running.is_set():
            running.clear()
            allowed.clear()
            print("手动暂停")
        else:
            with lock:
                start_time = time.monotonic()

            running.set()
            print("手动开始")

    elif key == keyboard.Key.esc:
        running.clear()
        allowed.clear()
        shutdown.set()
        return False


def on_release(key):
    if key in MODIFIER_KEYS:
        with lock:
            held_modifiers.discard(key)

        pause_for_modifier()

    if key in (keyboard.Key.f8, keyboard.Key.esc):
        with lock:
            pressed_hotkeys.discard(key)


# ========== 主程序 ==========

print("Bongo Cat F13-F24 连点器")
print(f"工作线程:{len(KEYS)}")
print(f"每线程目标频率:{RATE} 次/秒")
print(f"修饰键恢复延迟:{MODIFIER_DELAY} 秒")
print(f"自动停止:{MAX_SECONDS} 秒")
print("F8 开始/暂停 | ESC 或 Ctrl+C 退出")

threads = []

monitor_thread = threading.Thread(
    target=monitor,
    daemon=True
)
monitor_thread.start()
threads.append(monitor_thread)

for i in range(len(KEYS)):
    t = threading.Thread(
        target=worker,
        args=(i,),
        daemon=True
    )
    t.start()
    threads.append(t)

try:
    with keyboard.Listener(
        on_press=on_press,
        on_release=on_release
    ) as listener:
        while not shutdown.is_set():
            shutdown.wait(0.1)

except KeyboardInterrupt:
    print("Ctrl+C 退出")

finally:
    running.clear()
    allowed.clear()
    shutdown.set()

    for t in threads:
        t.join(timeout=2)

    kb = keyboard.Controller()

    for key in KEYS:
        try:
            kb.release(key)
        except Exception:
            pass

    print("程序已退出")

三、运行方法

打开 Steam 版 Bongo Cat,然后在脚本所在目录打开终端。

输入:

python bongo_cat.py

程序启动后不会立刻开始模拟输入,需要按 F8。

操作功能
F8开始或手动暂停
ESC退出程序
Ctrl+C在运行脚本的终端中退出
按住 Shift、Ctrl、Alt自动暂停
松开修饰键冷却结束后自动恢复
微信、QQ、Steam 位于前台自动暂停
切换到其他软件无其他暂停条件时自动恢复

程序会在控制台输出状态,例如:

Bongo Cat F13-F24 连点器
工作线程:12
每线程目标频率:5 次/秒
修饰键恢复延迟:2.0 秒
自动停止:1800 秒

手动开始
[14:30:01] 连点器恢复
[14:30:15] 修饰键按下,自动暂停
[14:30:17] 修饰键冷却中,自动暂停
[14:30:19] 连点器恢复

运行期间可以正常输入普通字母和数字,但模拟功能键仍可能影响某些软件的快捷键处理。

四、参数调整

主要需要修改代码开头的四个参数。

1. RATE:发送频率

RATE = 5

这个参数表示每个工作线程的目标频率。

RATE12 线程合计目标频率
224 次/秒
336 次/秒
560 次/秒
896 次/秒
10120 次/秒

注意:由于使用了全局发送锁,所有线程不能同时模拟按键。因此上表是目标频率,并非实际能够达到的速度。

在 PRESS_TIME = 0.03 时,单纯按键持续时间就将总吞吐量的理论上限限制在约 33 次/秒,实际通常还会更低。不建议通过无限提高 RATE 来加速,这样不一定增加有效计数,反而可能造成线程竞争。

2. PRESS_TIME:按键持续时间

PRESS_TIME = 0.03

单位为秒,0.03 即 30ms。这个参数影响每次模拟按键从按下到释放的时间。持续时间太短可能无法被 Bongo Cat 正常记录,太长则会限制发送频率。本机测试中,30ms 是能够正常计数的配置。

3. MAX_SECONDS:自动停止时间

MAX_SECONDS = 1800

常用设置:

数值效果
0不自动停止
60010 分钟
180030 分钟
36001 小时
72002 小时

达到时间上限后,程序会自动暂停,需要再次按 F8 才会重新开始。计时从每次 F8 启动开始,临时自动暂停的时间也包含在内。

4. MODIFIER_DELAY:修饰键恢复延迟

MODIFIER_DELAY = 2.0

按住 Shift、Ctrl、Alt 时会暂停模拟按键,松开后再等待 2 秒恢复。如果觉得等待太长,可以减小到 1.0;如果键盘组合操作仍受到影响,也可以适当增大。需要说明,延长恢复时间只能减少后续冲突,不能保证第一次 Shift 按下就一定正常生效。

五、为什么使用 F13–F24?

最初参考这一实现,使用的是数字和字母:

KEY_LIST = list(
    "0123456789abcdefghijklmnopqrstuvwxyz"
)

这种方案同时创建 36 个工作线程,能够快速产生大量键盘事件,但也会导致用户的其他输入受到影响。

F13–F24 是 Windows 支持的虚拟功能键,虽然普通键盘通常没有这些物理按键,但可以通过软件模拟。这些按键一般不会直接产生文本字符,因此更适合这个用途。

值得注意的是,不同的输入方式也会影响游戏的识别。测试中,使用 Windows keybd_event 模拟 F24 未能增加次数,而使用 pynput 模拟 F13 则可以成功计数。

六、使用风险与注意事项

1. 高并发可能导致系统异常

最初使用 36 线程高频模拟字母数字时,实际遇到过电脑黑屏死机。因此,不建议直接使用非常高的频率,也不要在无人看管时长时间运行。

2. 其他软件可能受到影响

在一次测试中,使用 F13 连点器期间,正在微信中与好友正常聊天,微信突然要求重新登录,并出现提示:「为了你的账号安全,请重新登录。」

因此,本脚本增加了微信、QQ 和 Steam 的前台检测:这些软件处于前台时会自动暂停模拟输入。如果出现异常登录提示、快捷键失效或其他问题,建议立即停止脚本。

3. Shift、Ctrl、Alt 可能仍然受到影响

程序提供了修饰键检测和冷却机制,但无法完全避免真实输入与模拟输入之间的竞争。

如果需要使用重要快捷键,或者正在编写代码、处理文件,建议通过 F8 手动暂停。

4. 部分前台窗口可能无法识别

Windows 对部分系统窗口或高权限程序的访问存在限制,例如任务管理器可能无法被普通权限的 Python 脚本正确识别。当前版本在无法识别前台窗口时会自动暂停,避免在未知窗口继续发送输入。

七、常见问题

Q:按 F8 后,Bongo Cat 次数没有增加?

首先检查脚本是否显示「连点器恢复」。如果显示自动暂停,需要检查是不是微信、QQ、Steam 位于前台,或存在尚未结束的修饰键冷却时间。不同模拟输入方式的效果也可能不同,并不能保证所有设备都能成功计数。

Q:为什么控制台显示开始了,但次数增长很慢?

当前方案通过全局发送锁串行发送按键,优先减少键盘冲突,因此不能达到极高的速度。游戏实际记录的次数也不一定等于脚本发送的次数。

Q:可以继续提高 RATE 吗?

可以修改,但不建议盲目提高。如果 PRESS_TIME = 0.03 保持不变,发送锁会成为主要的吞吐量限制。更高的 RATE 不一定提升实际有效计数。

Q:为什么 Shift 还是需要按两次?

可能是模拟键盘事件与真实修饰键发生重叠。程序会在检测到修饰键后暂停,但第一次按键可能已经受到影响。可以适当延长冷却时间,但这不是彻底解决方案。如果对正常键盘使用有较高要求,建议手动暂停后再操作。

Q:如何彻底退出?

按 ESC,或者在运行 Python 的终端中按 Ctrl+C。如果整个 Windows 系统已经没有响应,快捷键不一定能够及时生效。

Q:可以增加其他需要自动暂停的软件吗?

可以,在配置区修改:

BLOCKED_PROCESSES = {
    "wechat.exe",
    "weixin.exe",
    "qq.exe",
    "steam.exe",
    "steamwebhelper.exe",
    "discord.exe",
    "telegram.exe",
}

这些名称需要与 Windows 任务管理器中实际显示的进程名相对应。

DISCUSSION

Comments

Sign in with GitHub to join the conversation.