获取 SteamVR 的安装目录

/ 0评 / 0

哥啊,为啥你不给个 API 呢?

编写 OpenVR 驱动是一个问题,而安装 OpenVR 驱动到 SteamVR 则是一个更大的问题。许多 OpenVR 驱动要求用户手动将驱动文件夹放到那个神秘目录内,但有些比较高端的程序就不需要用户手动这么做。我们能否实现这个自动安装行为呢?

答案是可以。SteamVR 提供了 $SteamVRRoot/bin/vrpathreg.exe 来注册和反注册驱动,这样即使你的安装程序没有管理员权限,也一样可以用这个工具注册新的驱动到 SteamVR. 但是这只是将问题往下踢了一层:我们怎么知道 $SteamVRRoot 是哪?

如同所有未标准化的操作一样,实现这个功能的方法有各种野路子。下面是一些可以尝试的操作:

扫盘

如果用户没有做什么骚操作的话,SteamVR 的安装路径一定是 %SystemDrive%\Program Files (x86)\Steam\steamapps\common\SteamVR. 没错,SteamVR 其实默认是不允许修改安装目录的。所以我们可以先检查这个路径是否存在,如果存在的话,剩下的问题就简单了。

但是有许多用户会搞骚操作把这个东西挪走——毕竟这玩意太大了,放在系统盘里是不可接受的。我们或许可以通过检查 X:\Program Files (x86)\Steam\steamapps\common\SteamVR 的方式来找,但枚举驱动器的成功率也不是很高,因为 Steam 的游戏库的路径是可以完全自定义的。

提取注册表信息

我们还可以通过查注册表来找到 SteamVR 的安装路径。SteamVR 的安装路径一般写在 HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 250820\InstallLocation, 直接读这个键就能获得 $SteamVRRoot.

但是在实机测试的时候,我发现有些时候这个键是读不到的。直觉上这是因为安装程序是 32 位的,所以尝试访问注册表时,所有请求会被转发到 WOW6432Node 里面去。但是即使重新打包 64 位的安装程序,该读不到还是读不到。似乎 SteamVR 的老用户尤其会遇到这种问题。当然,也有一种可能是我的 NSIS 写得稀烂,注册表操作没有成功完成或者之类的。

我不是很想问用户手动键入 SteamVR 的位置。看来要一劳永逸地解决这个问题,使用一个安装后处理程序是必须的了。

来点灰魔法

用一个安装后处理程序读取注册表当然是可以做到的,但这未免也太无聊了一点。而且我手动排查了一台安装无法进行的电脑,结论是这个注册表键不一定在每台电脑上都存在。毕竟,这个注册表键是 Windows 的安装与卸载信息,即使没有这个信息 SteamVR 的各个组件也能正常工作。一些所谓的注册表清理软件可能会毫无理由地删除这些信息,或者 Steam 在安装这个东西的时候没有写入这些信息也不一定。

事已至此,要开始弄一些灰魔法了。

灰魔法,不是黑魔法,因为这些东西严格意义上来说是有理论支持的,而且里面并不需要太多整活。

vr::VR_Init 究竟干了什么?

因为我们仍然有 OpenVR API 可用,我们或许可以从这个方面着手,看看它有没有提供一个「获取安装路径」的能力。结论自然是「没有」,因为技术上一个 OpenVR Runtime 甚至可以在一个没有文件系统概念的环境中运行。

不过我们知道这个程序一定是在标准 Windows 下运行的。有没有其他好方法呢?

vr::VRInit(vr::EVRInitError *, vr::EVRApplicationType) 可以指定当前 VR 应用的类型。其中,vr::EVRApplicationType::VRApplication_Utility 可以在不唤起 SteamVR 或者其他 OpenVR Runtime 的情况下初始化 OpenVR 客户端程序接口,当然,只有 IVRSettingsIVRApplication 这两个接口能用。

不过这两个接口也没有提供任何有用的信息。调转一下思路:它是怎么知道和哪个 OpenVR Runtime 通信的?拆开头文件,可以看到这样一句话:

namespace vr {
/// <SNIP> ///
    /** Finds the active installation of vrclient.dll and initializes it */
	inline IVRSystem *VR_Init( EVRInitError *peError, EVRApplicationType eApplicationType, const char *pStartupInfo )
	{
/// <SNIP> ///
    }
}

这个 vrclient.dll 似乎是由 OpenVR 运行时提供的。如果我们搜索 vrclient.dll 的话,会发现它就在 $SteamVRRoot/bin/vrclient.dll.

嗯,答案接近了。

获取当前进程加载的 DLL 列表

我们可以初始化 OpenVR 之后,检查当前进程加载的 DLL 列表,其中一定能找到 vrclient.dll. 这之后,我们再拿到这个文件的路径,就可以进一步推断出剩下的路径了。

当然,实际使用的时候会发现在 x64 上加载的是 vrclient_x64.dll. 所以我们需要照顾两种情况:

const TCHAR* targetModuleName = TEXT("vrclient.dll");
const TCHAR* targetModuleName64 = TEXT("vrclient_x64.dll");

static bool findVrClientModule(TCHAR* path, size_t len) {
    auto currentPid = GetCurrentProcessId();
    HMODULE hMods[1024];
    HANDLE process;
    DWORD cbNeeded;

    process = OpenProcess(
        PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, currentPid);
    if (process == nullptr) {
        logger->error("Failed to open process: {}", GetLastError());
        return false;
    }

    if (!EnumProcessModules(process, hMods, sizeof(hMods), &cbNeeded)) {
        return false;
    }

    const TCHAR* foundModuleName = nullptr;
    TCHAR szModName[MAX_PATH];
    for (unsigned int i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {
        if (GetModuleFileNameEx(process,
                                hMods[i],
                                szModName,
                                sizeof(szModName) / sizeof(TCHAR))) {
            logger->trace(TEXT("Module name: {}"), szModName);
            if (StrStr(szModName, targetModuleName) != nullptr) {
                logger->info(TEXT("Found module: {}"), szModName);
                foundModuleName = targetModuleName;
                break;
            }

            if (StrStr(szModName, targetModuleName64) != nullptr) {
                logger->info(TEXT("Found module: {}"), szModName);
                foundModuleName = targetModuleName64;
                break;
            }
        }
    }

    if (foundModuleName == nullptr) {
        return false;
    }

    result = StringCchCopy(path, len, szModName);
    if (FAILED(result)) {
        return false;
    }

    return true;
}

获取 $SteamVRRoot

获得了 SteamVR 客户端 DLL 所在位置之后,只需要擦去 \bin\vrclient(_x64)?.dll 就可以了:

static bool extractSteamVrRoot(TCHAR* path,
                               size_t len,
                               const TCHAR* vrPathRegPath) {
    if (path == nullptr || vrPathRegPath == nullptr) {
        return false;
    }

    size_t vrPathRegPathLen = 0;
    auto result = StringCchLength(vrPathRegPath, MAX_PATH, &vrPathRegPathLen);
    if (FAILED(result)) {
        return false;
    }

    size_t backtickIndex = 0;
    bool firstBackTickFound = false;
    bool secondBackTickFound = false;
    for (size_t i = vrPathRegPathLen - 1; i > 0; i--) {
        if (vrPathRegPath[i] == TEXT('\\')) {
            if (!firstBackTickFound) {
                firstBackTickFound = true;
                continue;
            }

            secondBackTickFound = true;
            backtickIndex = i;
            break;
        }
    }

    if (!secondBackTickFound) {
        return false;
    }

    auto length = vrPathRegPathLen - backtickIndex;
    if (length >= len) {
        return false;
    }

    result = StringCchCopyN(path, len, vrPathRegPath, backtickIndex);
    if (FAILED(result)) {
        return false;
    }

    return true;
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Your comments will be submitted to a human moderator and will only be shown publicly after approval. The moderator reserves the full right to not approve any comment without reason. Please be civil.