PCSX2 中的 RetroAchievements 集成指南:RAInterface 从接入到硬核模式实战解析
2026/9/14 3:59:49 网站建设 项目流程

PCSX2 中的 RetroAchievements 集成指南:RAInterface 从接入到硬核模式实战解析

【免费下载链接】pcsx2PCSX2 - The Playstation 2 Emulator项目地址: https://gitcode.com/GitHub_Trending/pc/pcsx2

本指南以 3rdparty/rainterface/README.md 为核心骨架,系统讲解 PCSX2 如何通过 RAInterface 桥接层接入 RetroAchievements 成就服务。你将掌握 RAInterface 的架构定位、RA_Interface.cpp的引导(Bootstrap)加载机制、全套 C 风格 Hook API 的用法,以及 PCSX2 在 pcsx2/Achievements.cpp 中的真实调用链,最终能够在自己的模拟器或客户端中复现这套成就系统集成方案。

RAInterface 是什么:成就服务与模拟器之间的薄适配层

RAInterface 是一段被设计为「以 submodule 形式加载进其他仓库」的胶水代码(其 README.md 开篇即声明"This code is intended to be loaded into another repository as a submodule")。它的核心职责只有一件事:让模拟器通过一套稳定的 C 风格函数指针接口,与 RetroAchievements 官方提供的RA_Integration.dll动态库通信

它的目录结构非常精简,只有六个文件:

  • RA_Interface.h:对外暴露的全部 Hook API 声明,共约 40 个函数,全部以extern "C"包裹以保持 ABI 稳定;
  • RA_Interface.cpp:核心实现,负责 DLL 的查找、下载、升级、加载与所有函数指针转发;
  • RA_Consoles.h:ConsoleID枚举,必须与rcheevos/include/rc_consoles.h保持一致;
  • RA_Emulators.h:EmulatorID枚举,用于老式RA_Init接口的身份识别;
  • CMakeLists.txt:构建目标定义,头文件同时以INTERFACE方式导出;
  • rainterface.vcxproj/rainterface.vcxproj.filters:MSVC 工程文件。

README 给出的接入条件是:RA_Interface.cpp纳入模拟器构建,并链接winhttp.lib。之所以依赖 WinHTTP,是因为 RAInterface 自己实现了基于WinHttpOpen的引导下载逻辑(见 RA_Interface.cpp),用于在首次运行时自动获取RA_Integration.dll

架构分层:模拟器、RAInterface 与 RA_Integration.dll

从源码结构可以清晰推断出三层调用模型:

  1. 模拟器层(Client):PCSX2 的 pcsx2/Achievements.cpp 只调用RA_Interface.h中声明的公开函数,从不直接触碰 DLL。
  2. RAInterface 桥接层:内部维护一组static函数指针(如_RA_InitI_RA_DoAchievementsFrame),每个公开函数都是「指针非空则转发,否则安全返回默认值」的包装(wrapper)。
  3. RA_Integration.dll:真正的成就逻辑实现。RAInterface 通过GetProcAddress逐个解析导出符号(RA_Interface.cpp),将指针安装到桥接层。

这种「运行时动态解析」设计的价值在于:模拟器只需编译一次,DLL 可以独立升级,无需重新编译模拟器即可获得成就系统的功能更新。所有转发函数都做了空指针保护——例如RA_UserName()在 DLL 未加载时返回空字符串(RA_Interface.cpp),RA_IdentifyRom()返回 0,保证模拟器在无成就环境下降级为完全无感。

构建与依赖:把 RAInterface 编进你的项目

以 CMake 方式引入

CMakeLists.txt 定义了静态库目标:

add_library(rainterface RA_Consoles.h RA_Emulators.h RA_Interface.cpp RA_Interface.h ) target_include_directories(rainterface PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") target_include_directories(rainterface INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")

引入后,依赖方只需target_link_libraries(<your-target> rainterface winhttp)即可。INTERFACE头文件目录使下游直接#include "RA_Interface.h"而不必关心路径。

关键编译宏

  • RA_NOPROGRESS:定义后编译产物不再引用IProgressDialogShlObj.h),下载进度对话框功能被禁用(RA_Interface.cpp),适合无 UI 的测试环境;
  • RA_UTEST:定义后跳过GetIntegrationPath等需要宿主进程上下文的实现(RA_Interface.cpp),供单元测试使用;
  • 架构宏_M_X64/__amd64__决定加载RA_Integration-x64.dll还是 32 位的RA_Integration.dll(RA_Interface.cpp),并影响LatestVersionUrlLatestVersionUrlX64的选取(RA_Interface.cpp)。

初始化流程:引导加载、版本协商与离线回退

初始化是整个 RAInterface 最复杂、也最能体现其工程价值的部分,入口是RA_Init/RA_InitClient,二者最终汇聚到RA_InitCommon(RA_Interface.cpp),完整流程如下:

  1. _RA_InstallIntegration()加载本地 DLL:先按架构名(RA_Integration-x64.dll,失败时回退到RA_Integration.dll)在可执行文件同目录查找;找不到则返回版本号"0.0";加载失败(如ERROR_BAD_EXE_FORMAT位宽不匹配)会弹窗并明确提示「Are you trying to load the 32-bit/64-bit version?」(RA_Interface.cpp)。
  2. 探测服务端:通过 WinHTTP 以POST r=latestintegration请求dorequest.php,带 4 次重试(DoBlockingHttpCallWithRetry),并依据IsNetworkError的状态码表(12002 超时、12007 DNS 失败、12030 连接中止等)判定是否值得重试(RA_Interface.cpp)。
  3. 版本协商:从 JSON 响应中解析MinimumVersion(必须满足)、LatestVersion(建议升级)与LatestVersionUrl(X64)(下载地址)。ParseVersionmajor.minor.patch.revision编码为 64 位整数比较(RA_Interface.cpp)。
  4. 交互升级:低于最低版本时弹出「Install/Upgrade RetroAchievements toolset?」警告(MB_YESNO);处于可选升级区间时仅提示可选升级。用户同意后,FetchIntegrationFromWeb通过.download→ 重命名 → 删除.old的原子替换策略更新 DLL,并等待模块句柄释放(最多 1 秒)(RA_Interface.cpp)。
  5. 正式初始化:版本达标后调用 DLL 的_RA_InitI(hMainHWND, nEmulatorID, sClientVersion)_RA_InitClient(hMainHWND, sClientName, sClientVersion),失败则RA_Shutdown()收尾。
  6. 离线回退:若_RA_HostUrl()返回http://OFFLINE,直接走_RA_InitOffline/_RA_InitClientOffline;若网络不可达且有离线入口,弹「Working offline」警告后同样进入离线模式(RA_Interface.cpp)。

两种初始化 API 的区别

API身份参数适用场景
RA_Init(hMainHWND, nEmulatorID, sClientVersion)数值型EmulatorID(见 RA_Emulators.h)接入官方已知模拟器清单的旧式客户端
RA_InitClient(hMainHWND, sClientName, sClientVersion)字符串型客户端名新式客户端;名称会显示在标题栏并作为 API 调用的 User-Agent

PCSX2 采用第二种方式(pcsx2/Achievements.cpp):

RA_InitClient((HWND)main_window_handle, "PCSX2", BuildVersion::GitTag);

身份枚举:ConsoleID 与 EmulatorID

ConsoleID:游戏平台身份

RA_Consoles.h 定义了从UnknownConsoleID = 0FamicomDiskSystem = 81的完整平台枚举,另有Hubs = 100Events = 101Standalone = 102三个特殊用途值。PCSX2 对应的平台是:

PlayStation2 = 21,

该枚举被明确要求与 rcheevos 仓库的rc_consoles.h同步(头文件注释"this list should match the list in rcheevos/include/rc_consoles.h"),因为成就逻辑需要根据 console ID 解释内存布局。多主机模拟器应在每次加载游戏前调用RA_SetConsoleID

EmulatorID:客户端身份

RA_Emulators.h 仅覆盖RA_Gens = 0RA_Oricutron = 11十二个历史模拟器(含RA_Libretro = 7)。注意:PCSX2 并不在此清单中,这正是它使用RA_InitClient(字符串名)而非RA_Init的原因——新式接口不再受限于枚举。

共享回调:模拟器能力向 DLL 的注入

RA_InstallSharedFunctions是反向通道:把模拟器的能力以回调函数指针交给 DLL,让成就系统能够驱动宿主。参数依次为(见 RA_Interface.h):

参数作用
fpUnusedIsActive已弃用,传nullptr(实现中确实如此转发,见 RA_Interface.cpp)
fpCauseUnpause/fpCausePause恢复 / 暂停模拟
fpRebuildMenu通知客户端成就弹出菜单已变更(配合RA_CreatePopupMenu
fpEstimateTitle填充 256 字节缓冲区,返回当前游戏的短描述
fpResetEmulator重置模拟器
fpLoadROM当前未使用,未来可指示加载特定游戏

游戏识别与激活:从 ROM 哈希到成就数据

游戏生命周期管理围绕「识别 → 激活 → 每帧处理」展开:

  • RA_IdentifyRom(BYTE*, size):对完整缓冲的 ROM 文件做整文件哈希并查询游戏 ID,返回 0 表示无关联;复杂识别场景可链接 rcheevos/rhash 直接生成哈希后改调RA_IdentifyHash(const char*)
  • RA_ActivateGame(nGameId):拉取指定游戏的成就、排行榜等全部数据。
  • RA_OnLoadNewRom:等价于IdentifyRom+ActivateGame的组合,是「加载新游戏」的推荐入口。
  • RA_ConfirmLoadNewRom(bIsQuitting):卸载前调用,给玩家保存进度/关闭成就弹窗的机会,返回 0 表示中止卸载。
  • 换盘处理RA_IdentifyRom/RA_IdentifyHash可在换盘时再次调用,以确认新盘仍归属当前游戏。

PCSX2 的实际调用顺序(pcsx2/Achievements.cpp):用s_game_hash直接走哈希识别路径RA_IdentifyHash,避免对整盘数据做二次哈希。

内存暴露:让成就逻辑读到模拟器的游戏状态

成就要判定「杀死了 BOSS」「收集了道具」,本质是监测游戏内存中的变量。RAInterface 通过「内存银行(Memory Bank)」机制实现:

  • RA_InstallMemoryBank(nBankID, pReader, pWriter, nBankSize):注册读/写回调。pReader接收银行内偏移量返回 8 位值;pWriter接收偏移量与 8 位值执行写入(RA_Interface.h)。
  • RA_InstallMemoryBankBlockReader:追加块读回调,一次读取一段内存以提升性能(RA_Interface.h)。
  • RA_ClearMemoryBanks:重置所有已注册银行。
  • 银行布局按 console ID 定义,需查阅 rcheevos 仓库的consoleinfo.c

PCSX2 注册了单一银行,将 EE 内存整体暴露(pcsx2/Achievements.cpp):

RA_InstallMemoryBank(0, RACallbackReadMemory, RACallbackWriteMemory, GetExposedEEMemorySize()); RA_InstallMemoryBankBlockReader(0, RACallbackReadBlock);

运行时帧处理与硬核模式(Hardcore)

每帧驱动

RA_DoAchievementsFrame()必须在每帧执行所有成就相关处理(成就进度、排行榜、弹窗逻辑),PCSX2 在帧循环中直接调用(pcsx2/Achievements.cpp)。

配套的辅助函数:

  • RA_SuspendRepaint/RA_ResumeRepaint:临时挂起/恢复工具窗口的强制重绘,常用于快进(fast-forward)时降低开销;
  • RA_UpdateAppTitle(sCustomMessage):向标题栏追加「风味文本」,最终格式为ClientName - Version - Flavor Text - Username
  • RA_HandleHTTPResults:已弃用,空实现(RA_Interface.cpp),异步 HTTP 已完全由 DLL 内部管理。

硬核模式:公平性的守护

硬核模式是 RetroAchievements 的防作弊机制。RA_HardcoreModeIsActive()返回非零时,客户端必须禁用一切可能构成不公平优势的功能。头文件明确列举:加载状态、使用金手指/作弊码、修改 RAM、禁用渲染层、查看解码后的 tilemap等。

接入时必须遵守的三条规则:

  1. 拦截危险操作:在玩家尝试「读取存档」等操作前调用RA_WarnDisableHardcore("load a state")——DLL 会弹「是否禁用硬核模式」对话框;返回非零才允许继续,返回 0 必须中止操作。
  2. 主动降级:客户端自己做提示时,可调用RA_DisableHardcore()静默禁用(实现上等价于向_RA_WarnDisableHardcorenullptr,见 RA_Interface.cpp)。
  3. 兜底防护:DLL 未加载时,RA_WarnDisableHardcore的本地实现会直接弹出警告框并返回 0 阻止操作(RA_Interface.cpp),保证即使无 DLL 也不破环公平性。

PCSX2 在「允许取消的读档确认」流程中使用了RA_ConfirmLoadNewRom等守卫逻辑(pcsx2/Achievements.cpp),并结合上述硬核规则阻止读档/金手指等操作。

存档状态协同:.rap 文件与 RA_CaptureState

成就进度必须与模拟器存档共存亡:

  • RA_OnSaveState(sFilename):在状态文件旁生成.rap文件,内含成就运行时信息;
  • RA_OnLoadState(sFilename):加载存档时同步读取.rap恢复进度;
  • RA_CaptureState(pBuffer, nBufferSize):把成就运行时状态序列化进自定义缓冲;返回所需字节数——若大于传入缓冲则不会填充,需扩容后重调;
  • RA_RestoreState(pBuffer):从缓冲恢复。

后两者用于把成就状态内嵌进模拟器自己的存档格式(如 PCSX2 的存档块),实现零额外文件的深度集成。

覆盖层(Overlay)与菜单集成

RAInterface 同时承担成就 UI 的宿主职责:

  • RA_CreatePopupMenu():创建可附加到主菜单的弹出菜单,未附加时调用方需自行销毁;
  • RA_GetPopupMenuItems(RA_MenuItem*):填充至少 32 个条目的菜单项数组(RA_MenuItem含宽字符标签、nID与选中态);
  • RA_InvokeDialog(nID):菜单项被选中时调用,nID落在IDM_RA_MENUSTART(1700)~IDM_RA_MENUEND(1739)区间;
  • 覆盖层导航RA_IsOverlayFullyVisible()判断覆盖层是否完全可见;可见时通过RA_NavigateOverlay(ControllerInput*)把方向键/确认/取消/退出(通常对应 C/A、B、Start)传入;RA_SetPaused(bool)控制显示与隐藏;
  • RA_UpdateHWnd(hWnd):窗口句柄变更(如窗口化↔全屏切换)后重新锚定覆盖层;
  • RA_SetForceRepaint(1):使用UpdateWindow代替InvalidateRect强制重绘,专门解决 SDL 类模拟器消息队列被刷爆、WM_PAINT永不派发的问题;
  • RA_UpdateRenderOverlay:已弃用,仅转发给RA_NavigateOverlay(RA_Interface.cpp),渲染已内置于 DLL。

用户与会话管理

  • RA_AttemptLogin(bBlocking):触发登录流程;bBlocking = 0时控制权立即返回调用进程,由服务端异步完成;已保存凭据则免提示静默登录。
  • RA_UserName():返回当前登录用户名,未登录返回空串。
  • RA_SetUserAgentDetail(sDetail):向 API 调用的 User-Agent 追加依赖/配置信息(典型用途是标识 libretro 核心版本)。
  • RA_Shutdown():反初始化并卸载 DLL——依次调用_RA_Shutdown()(带异常保护)、清空全部函数指针、FreeLibrary释放模块(RA_Interface.cpp)。

集成要点清单与实战建议

  • 必须最先调用:任何 API 之前先RA_InitClient(或RA_Init),它会完成 DLL 引导;其余函数在未初始化时均安全返回默认值。
  • 先装回调再注册内存RA_InstallSharedFunctions应在初始化后尽快调用,使 DLL 具备驱动模拟器的能力;随后按 console ID 注册内存银行。
  • 帧循环纪律:确保每帧调用RA_DoAchievementsFrame(),并在暂停状态同步RA_SetPaused
  • 硬核模式零容忍:读档、金手指、内存编辑等入口一律先过RA_WarnDisableHardcore检查。
  • 存档联动:创建/加载存档时同步调用RA_OnSaveState/RA_OnLoadState;若使用自定义存档格式,则用RA_CaptureState/RA_RestoreState内嵌状态。
  • 位宽匹配:确保目标进程位宽与加载的RA_Integration(-x64).dll一致,避免ERROR_BAD_EXE_FORMAT
  • 离线体验http://OFFLINE主机与_RA_InitOffline支持无网络环境,测试时可借此绕开网络依赖。

以上所有证据均来自当前仓库:接口契约见 3rdparty/rainterface/RA_Interface.h,引导加载实现见 3rdparty/rainterface/RA_Interface.cpp,PCSX2 侧的真实调用见 pcsx2/Achievements.cpp。如需深入了解成就判定算法本身,可继续阅读仓库内的 3rdparty/rcheevos 相关头文件与实现。

【免费下载链接】pcsx2PCSX2 - The Playstation 2 Emulator项目地址: https://gitcode.com/GitHub_Trending/pc/pcsx2

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询