Updated Steam integration to 1.6.1170 by reloading steamclient64.dll
This commit is contained in:
parent
9c7b31dd60
commit
9cbe00e296
@ -1,3 +1,4 @@
|
|||||||
ShowWarningOnInitFail = true
|
ShowWarningOnInitFail = true
|
||||||
SendAchievementsToLE = false
|
SendAchievementsToLE = false
|
||||||
TestMode = true
|
TestMode = false
|
||||||
|
ReloadSteamClient = true
|
||||||
|
|||||||
BIN
source/Steam DLL/cmake/version.rc.in
(Stored with Git LFS)
BIN
source/Steam DLL/cmake/version.rc.in
(Stored with Git LFS)
Binary file not shown.
@ -1,66 +1,182 @@
|
|||||||
#include "Achievements.h"
|
#include "Achievements.h"
|
||||||
#include "Util.h"
|
#include "Util.h"
|
||||||
#include "steam/steam_api.h"
|
#include "steam/steam_api.h"
|
||||||
|
#include <cstdlib>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <processenv.h>
|
#include <processenv.h>
|
||||||
|
|
||||||
namespace Achievements {
|
namespace Achievements {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Enderal's own AppIDs. Skyrim SE is 489830.
|
||||||
|
constexpr const char* APPID_ENDERAL_SE = "976620";
|
||||||
|
constexpr const char* APPID_ENDERAL_LE = "933480";
|
||||||
|
|
||||||
|
bool bInitFailed = false;
|
||||||
|
bool bWarnOnInitFail = true;
|
||||||
|
|
||||||
|
constexpr const char* STEAMCLIENT_MODULE = "steamclient64.dll";
|
||||||
|
|
||||||
|
// Which app a process is registered as is decided by steamclient64.dll, and
|
||||||
|
// these variables are how it is told. It latches them when the module is
|
||||||
|
// *loaded* into the process - not when a pipe is created - so changing them
|
||||||
|
// afterwards does nothing at all. steam_api never reads them back either, it
|
||||||
|
// asks ISteamUtils::GetAppID(). (steam_appid.txt is no help: only
|
||||||
|
// SteamAPI_RestartAppIfNecessary reads it, and Skyrim does not call that.)
|
||||||
|
void setAppIdEnvironment(const char* appId)
|
||||||
|
{
|
||||||
|
SetEnvironmentVariableA("SteamAppId", appId);
|
||||||
|
SetEnvironmentVariableA("SteamGameId", appId);
|
||||||
|
SetEnvironmentVariableA("SteamOverlayGameId", appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void logEnvironment(const char* label)
|
||||||
|
{
|
||||||
|
static const char* const vars[] = { "SteamAppId", "SteamGameId", "SteamOverlayGameId", "SteamClientLaunch" };
|
||||||
|
for (const char* name : vars) {
|
||||||
|
char value[64] = "<unset>";
|
||||||
|
GetEnvironmentVariableA(name, value, sizeof(value));
|
||||||
|
logger::info("[{}] {} = {}", label, name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void logModuleState(const char* label)
|
||||||
|
{
|
||||||
|
logger::info("[{}] steam_api64={}, {}={}, GameOverlayRenderer64={}",
|
||||||
|
label,
|
||||||
|
GetModuleHandleA("steam_api64.dll") ? "loaded" : "absent",
|
||||||
|
STEAMCLIENT_MODULE,
|
||||||
|
GetModuleHandleA(STEAMCLIENT_MODULE) ? "loaded" : "absent",
|
||||||
|
GetModuleHandleA("GameOverlayRenderer64.dll") ? "loaded" : "absent");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop every outstanding reference to steamclient64.dll so that the
|
||||||
|
// SteamAPI_Init() below loads a fresh copy, which then picks up the AppID we
|
||||||
|
// just put in the environment. Something loads it before SKSE gets control
|
||||||
|
// (the SteamStub DRM wrapper is the only candidate that runs that early) and
|
||||||
|
// leaves it resident with Skyrim's AppID latched in.
|
||||||
|
//
|
||||||
|
// SteamAPI_Shutdown() has to run first: it releases steam_api's own reference
|
||||||
|
// and clears its cached module handle, so it will not later FreeLibrary a
|
||||||
|
// handle we already invalidated.
|
||||||
|
bool unloadSteamClient()
|
||||||
|
{
|
||||||
|
constexpr int MAX_REFERENCES = 32;
|
||||||
|
|
||||||
|
int released = 0;
|
||||||
|
while (auto* module = GetModuleHandleA(STEAMCLIENT_MODULE)) {
|
||||||
|
if (released >= MAX_REFERENCES) {
|
||||||
|
logger::error("{} is still loaded after releasing {} references, giving up", STEAMCLIENT_MODULE, released);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FreeLibrary(module);
|
||||||
|
++released;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (released > 0) {
|
||||||
|
logger::info("Unloaded {} after releasing {} reference(s)", STEAMCLIENT_MODULE, released);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool steamInitFailed()
|
||||||
|
{
|
||||||
|
return bInitFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldWarnOnInitFail()
|
||||||
|
{
|
||||||
|
return bWarnOnInitFail;
|
||||||
|
}
|
||||||
|
|
||||||
void startSteam()
|
void startSteam()
|
||||||
{
|
{
|
||||||
std::map<std::string, bool> settings{
|
std::map<std::string, bool> settings{
|
||||||
{ "SendAchievementsToLE", false },
|
{ "SendAchievementsToLE", false },
|
||||||
{ "TestMode", false },
|
{ "TestMode", false },
|
||||||
{ "ShowWarningOnInitFail", true }
|
{ "ShowWarningOnInitFail", true },
|
||||||
|
{ "ReloadSteamClient", true }
|
||||||
};
|
};
|
||||||
LoadINI(&settings, "Data/SKSE/Plugins/EnderalSteam.ini");
|
LoadINI(&settings, "Data/SKSE/Plugins/EnderalSteam.ini");
|
||||||
|
|
||||||
AchievementsEnabled(!settings.at("TestMode"));
|
AchievementsEnabled(!settings.at("TestMode"));
|
||||||
|
bWarnOnInitFail = settings.at("ShowWarningOnInitFail");
|
||||||
|
|
||||||
if (settings.at("TestMode")) {
|
if (settings.at("TestMode")) {
|
||||||
|
logger::info("{}", "TestMode is on, leaving the Steam session alone");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SteamInstance() != nullptr) {
|
||||||
|
logger::info("{}", "Already initialized steam api, skipping it");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (SteamInstance() == nullptr) {
|
const char* appId = settings.at("SendAchievementsToLE") ? APPID_ENDERAL_LE : APPID_ENDERAL_SE;
|
||||||
SteamAPI_Shutdown();
|
|
||||||
|
|
||||||
if (settings.at("SendAchievementsToLE")) {
|
logModuleState("BEFORE");
|
||||||
SetEnvironmentVariable(L"SteamAppID", L"933480");
|
logEnvironment("BEFORE");
|
||||||
SetEnvironmentVariable(L"SteamGameId", L"933480");
|
logger::info("[BEFORE] HSteamUser = {}", SteamAPI_GetHSteamUser());
|
||||||
} else {
|
|
||||||
SetEnvironmentVariable(L"SteamAppID", L"976620");
|
|
||||||
SetEnvironmentVariable(L"SteamGameId", L"976620");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool success = SteamAPI_Init();
|
// This runs from SKSEPluginLoad, i.e. after the CRT global initializers but
|
||||||
|
// before WinMain. Skyrim opens its own Steam session much later, from
|
||||||
|
// BSWin32SystemUtility, so we get here first and its SteamAPI_Init() then
|
||||||
|
// returns early because ours is already up.
|
||||||
|
//
|
||||||
|
// Being first is not enough on its own, though - see unloadSteamClient().
|
||||||
|
SteamAPI_Shutdown();
|
||||||
|
|
||||||
if (success) {
|
if (settings.at("ReloadSteamClient")) {
|
||||||
logger::info("{}", "Steam api init was successfull");
|
unloadSteamClient();
|
||||||
} else {
|
|
||||||
logger::error("{}", "Error while initializing the steam api");
|
|
||||||
if (settings.at("ShowWarningOnInitFail")) {
|
|
||||||
RE::DebugMessageBox("Unable to initialize Steam achievements. Try to restart the game and the Steam client. This warning can be disabled in SKSE\\Plugins\\EnderalSteam.ini.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SteamInstance(new AchievementHolder());
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
logger::info("{}", "Already initialized steam api, skipping it");
|
setAppIdEnvironment(appId);
|
||||||
|
|
||||||
|
if (!SteamAPI_Init()) {
|
||||||
|
bInitFailed = true;
|
||||||
|
logger::error("{}", "Error while initializing the steam api");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger::info("Steam api init was successfull, requested AppID {}", appId);
|
||||||
|
|
||||||
|
// The AppID the Steam client actually gave us. Anything other than the one
|
||||||
|
// requested above means the session still belongs to another app - Enderal's
|
||||||
|
// achievement names would not resolve there - so treat it like a failed init:
|
||||||
|
// leave achievements unwired and let the main-menu warning fire.
|
||||||
|
if (auto* utils = SteamUtils()) {
|
||||||
|
const uint32 sessionAppId = utils->GetAppID();
|
||||||
|
logger::info("Steam session is registered as AppID {}", sessionAppId);
|
||||||
|
if (sessionAppId != static_cast<uint32>(std::strtoul(appId, nullptr, 10))) {
|
||||||
|
bInitFailed = true;
|
||||||
|
logger::error("{}", "Session kept a foreign AppID, not wiring up achievements");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger::warn("{}", "ISteamUtils is unavailable, cannot verify the session AppID");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* holder = new AchievementHolder();
|
||||||
|
SteamInstance(holder);
|
||||||
|
|
||||||
|
// Steam wants the user's stats downloaded before achievements can be set.
|
||||||
|
// Nothing used to call this, so UserStatsReceived_t never fired.
|
||||||
|
holder->start();
|
||||||
}
|
}
|
||||||
catch (const std::exception& ex) {
|
catch (const std::exception& ex) {
|
||||||
|
bInitFailed = true;
|
||||||
std::string msg = "Exception while initializing the Steam API, steam achievements will not be available: " + std::string(ex.what());
|
std::string msg = "Exception while initializing the Steam API, steam achievements will not be available: " + std::string(ex.what());
|
||||||
logger::error("{}", msg.c_str());
|
logger::error("{}", msg.c_str());
|
||||||
if (settings.at("ShowWarningOnInitFail")) {
|
|
||||||
RE::DebugMessageBox("Unable to initialize Steam achievements. Try to restart the game and the Steam client. This warning can be disabled in SKSE\\Plugins\\EnderalSteam.ini.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AchievementHolder::AchievementHolder() : stats(SteamUserStats()), callback(this, &AchievementHolder::onUserStatsReceived)
|
AchievementHolder::AchievementHolder() : stats(SteamUserStats()), callback(this, &AchievementHolder::onUserStatsReceived)
|
||||||
{
|
{
|
||||||
|
if (!this->stats) {
|
||||||
|
logger::error("{}", "ISteamUserStats is unavailable, achievements will not be unlocked");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AchievementHolder::onUserStatsReceived(UserStatsReceived_t * event) {
|
void AchievementHolder::onUserStatsReceived(UserStatsReceived_t * event) {
|
||||||
@ -80,6 +196,11 @@ namespace Achievements {
|
|||||||
|
|
||||||
bool AchievementHolder::setAchievementUnlocked(const char * achievementName)
|
bool AchievementHolder::setAchievementUnlocked(const char * achievementName)
|
||||||
{
|
{
|
||||||
|
if (!this->stats) {
|
||||||
|
logger::error("{}", "Cannot unlock achievement, ISteamUserStats is unavailable");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
std::string msg = "Unlocking achievement: " + std::string(achievementName);
|
std::string msg = "Unlocking achievement: " + std::string(achievementName);
|
||||||
logger::info("{}", msg.c_str());
|
logger::info("{}", msg.c_str());
|
||||||
bool success = this->stats->SetAchievement(achievementName);
|
bool success = this->stats->SetAchievement(achievementName);
|
||||||
@ -96,6 +217,12 @@ namespace Achievements {
|
|||||||
|
|
||||||
void AchievementHolder::start()
|
void AchievementHolder::start()
|
||||||
{
|
{
|
||||||
this->stats->RequestCurrentStats();
|
if (!this->stats) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this->stats->RequestCurrentStats()) {
|
||||||
|
logger::error("{}", "RequestCurrentStats failed, achievements may not unlock");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
//Steam API Version 1.31 matches the Skyrim Steam API version
|
//Steam API version 1.55, matching Skyrim SE 1.6.1170 (pre-1.6.1130 shipped 1.31)
|
||||||
#include "steam/steam_api.h"
|
#include "steam/steam_api.h"
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
namespace Achievements {
|
namespace Achievements {
|
||||||
|
|
||||||
@ -18,7 +17,11 @@ namespace Achievements {
|
|||||||
bool setAchievementUnlocked(const char * achievementName);
|
bool setAchievementUnlocked(const char * achievementName);
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::unique_ptr<AchievementHolder> singleton(nullptr);
|
// Opens the Steam session under Enderal's AppID. Must run before the game
|
||||||
|
// opens its own - see the comment inside.
|
||||||
void startSteam();
|
void startSteam();
|
||||||
|
|
||||||
|
// Result of startSteam(), so the warning can be shown once a UI exists.
|
||||||
|
bool steamInitFailed();
|
||||||
|
bool shouldWarnOnInitFail();
|
||||||
}
|
}
|
||||||
@ -20,8 +20,12 @@ auto EventListener::ProcessEvent(
|
|||||||
{
|
{
|
||||||
if (a_event->opening && a_event->menuName == "Main Menu") {
|
if (a_event->opening && a_event->menuName == "Main Menu") {
|
||||||
RE::UI::GetSingleton()->RemoveEventSink<RE::MenuOpenCloseEvent>(GetSingleton());
|
RE::UI::GetSingleton()->RemoveEventSink<RE::MenuOpenCloseEvent>(GetSingleton());
|
||||||
logger::info("{}", "Main menu opened, trying to init steam API.");
|
|
||||||
Achievements::startSteam();
|
// Steam is started at plugin load, long before there is a UI to complain to,
|
||||||
|
// so the warning waits until the main menu is up.
|
||||||
|
if (Achievements::steamInitFailed() && Achievements::shouldWarnOnInitFail()) {
|
||||||
|
RE::DebugMessageBox("Unable to initialize Steam achievements. Try to restart the game and the Steam client. This warning can be disabled in SKSE\\Plugins\\EnderalSteam.ini.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return RE::BSEventNotifyControl::kContinue;
|
return RE::BSEventNotifyControl::kContinue;
|
||||||
|
|||||||
@ -38,6 +38,11 @@ SKSEPluginLoad(const LoadInterface* skse) {
|
|||||||
|
|
||||||
Init(skse);
|
Init(skse);
|
||||||
|
|
||||||
|
// Has to happen here, not from a game event: the game opens its own Steam session
|
||||||
|
// from BSWin32SystemUtility during WinMain, and whichever session is opened first
|
||||||
|
// decides which AppID the process stays registered as.
|
||||||
|
Achievements::startSteam();
|
||||||
|
|
||||||
EventListener::Install();
|
EventListener::Install();
|
||||||
|
|
||||||
GetPapyrusInterface()->Register(Papyrus::Bind);
|
GetPapyrusInterface()->Register(Papyrus::Bind);
|
||||||
|
|||||||
@ -8,7 +8,12 @@ namespace Papyrus::PapyrusFunctions
|
|||||||
bool CallUnlockAchievement(RE::StaticFunctionTag* tag, RE::BSFixedString achievement)
|
bool CallUnlockAchievement(RE::StaticFunctionTag* tag, RE::BSFixedString achievement)
|
||||||
{
|
{
|
||||||
if (AchievementsEnabled()) {
|
if (AchievementsEnabled()) {
|
||||||
return SteamInstance()->setAchievementUnlocked(achievement.c_str());
|
auto* steam = SteamInstance();
|
||||||
|
if (!steam) {
|
||||||
|
logger::error("Steam is not initialized, cannot unlock achievement: {}", achievement.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return steam->setAchievementUnlocked(achievement.c_str());
|
||||||
} else {
|
} else {
|
||||||
RE::DebugNotification(std::format("Achievement unlocked: {}", achievement.c_str()).c_str());
|
RE::DebugNotification(std::format("Achievement unlocked: {}", achievement.c_str()).c_str());
|
||||||
logger::info("{}", std::format("Achievement unlocked: {}", achievement.c_str()).c_str());
|
logger::info("{}", std::format("Achievement unlocked: {}", achievement.c_str()).c_str());
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "Achievements.h"
|
#include "Achievements.h"
|
||||||
#include <SimpleIni.h>
|
#include <SimpleIni.h>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
inline const SKSE::LoadInterface* GetLoadInterface(const SKSE::LoadInterface* loadInterface = nullptr)
|
inline const SKSE::LoadInterface* GetLoadInterface(const SKSE::LoadInterface* loadInterface = nullptr)
|
||||||
{
|
{
|
||||||
@ -21,11 +22,11 @@ inline Achievements::AchievementHolder* SteamInstance(Achievements::AchievementH
|
|||||||
return singleton;
|
return singleton;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool AchievementsEnabled(bool bEnabled = NULL)
|
inline bool AchievementsEnabled(std::optional<bool> bEnabled = std::nullopt)
|
||||||
{
|
{
|
||||||
static bool value;
|
static bool value = false;
|
||||||
if (bEnabled != NULL) {
|
if (bEnabled.has_value()) {
|
||||||
value = bEnabled;
|
value = *bEnabled;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@ -68,7 +69,7 @@ inline void LoadINI(std::map<std::string, bool>* settings, const char* iniPath)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (bUpdateINI) {
|
if (bUpdateINI) {
|
||||||
logger::info("New settings detected, adding to ArtifactTracker.ini");
|
logger::info("New settings detected, updating {}", iniPath);
|
||||||
ini.SaveFile(iniPath);
|
ini.SaveFile(iniPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user