Updated Steam integration to 1.6.1170 by reloading steamclient64.dll

This commit is contained in:
Eddoursul 2026-07-30 12:45:32 +02:00
parent 9c7b31dd60
commit 9cbe00e296
8 changed files with 190 additions and 44 deletions

View File

@ -1,3 +1,4 @@
ShowWarningOnInitFail = true
SendAchievementsToLE = false
TestMode = true
TestMode = false
ReloadSteamClient = true

BIN
source/Steam DLL/cmake/version.rc.in (Stored with Git LFS)

Binary file not shown.

View File

@ -1,66 +1,182 @@
#include "Achievements.h"
#include "Util.h"
#include "steam/steam_api.h"
#include <cstdlib>
#include <string>
#include <processenv.h>
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()
{
std::map<std::string, bool> settings{
{ "SendAchievementsToLE", false },
{ "TestMode", false },
{ "ShowWarningOnInitFail", true }
{ "ShowWarningOnInitFail", true },
{ "ReloadSteamClient", true }
};
LoadINI(&settings, "Data/SKSE/Plugins/EnderalSteam.ini");
AchievementsEnabled(!settings.at("TestMode"));
bWarnOnInitFail = settings.at("ShowWarningOnInitFail");
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;
}
try {
if (SteamInstance() == nullptr) {
SteamAPI_Shutdown();
const char* appId = settings.at("SendAchievementsToLE") ? APPID_ENDERAL_LE : APPID_ENDERAL_SE;
if (settings.at("SendAchievementsToLE")) {
SetEnvironmentVariable(L"SteamAppID", L"933480");
SetEnvironmentVariable(L"SteamGameId", L"933480");
} else {
SetEnvironmentVariable(L"SteamAppID", L"976620");
SetEnvironmentVariable(L"SteamGameId", L"976620");
}
logModuleState("BEFORE");
logEnvironment("BEFORE");
logger::info("[BEFORE] HSteamUser = {}", SteamAPI_GetHSteamUser());
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) {
logger::info("{}", "Steam api init was successfull");
} 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());
if (settings.at("ReloadSteamClient")) {
unloadSteamClient();
}
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) {
bInitFailed = true;
std::string msg = "Exception while initializing the Steam API, steam achievements will not be available: " + std::string(ex.what());
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)
{
if (!this->stats) {
logger::error("{}", "ISteamUserStats is unavailable, achievements will not be unlocked");
}
}
void AchievementHolder::onUserStatsReceived(UserStatsReceived_t * event) {
@ -80,6 +196,11 @@ namespace Achievements {
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);
logger::info("{}", msg.c_str());
bool success = this->stats->SetAchievement(achievementName);
@ -96,6 +217,12 @@ namespace Achievements {
void AchievementHolder::start()
{
this->stats->RequestCurrentStats();
if (!this->stats) {
return;
}
if (!this->stats->RequestCurrentStats()) {
logger::error("{}", "RequestCurrentStats failed, achievements may not unlock");
}
}
}

View File

@ -1,7 +1,6 @@
#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 <memory>
namespace Achievements {
@ -18,7 +17,11 @@ namespace Achievements {
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();
// Result of startSteam(), so the warning can be shown once a UI exists.
bool steamInitFailed();
bool shouldWarnOnInitFail();
}

View File

@ -20,8 +20,12 @@ auto EventListener::ProcessEvent(
{
if (a_event->opening && a_event->menuName == "Main Menu") {
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;

View File

@ -38,6 +38,11 @@ SKSEPluginLoad(const LoadInterface* 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();
GetPapyrusInterface()->Register(Papyrus::Bind);

View File

@ -8,7 +8,12 @@ namespace Papyrus::PapyrusFunctions
bool CallUnlockAchievement(RE::StaticFunctionTag* tag, RE::BSFixedString achievement)
{
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 {
RE::DebugNotification(std::format("Achievement unlocked: {}", achievement.c_str()).c_str());
logger::info("{}", std::format("Achievement unlocked: {}", achievement.c_str()).c_str());

View File

@ -2,6 +2,7 @@
#include "Achievements.h"
#include <SimpleIni.h>
#include <optional>
inline const SKSE::LoadInterface* GetLoadInterface(const SKSE::LoadInterface* loadInterface = nullptr)
{
@ -21,11 +22,11 @@ inline Achievements::AchievementHolder* SteamInstance(Achievements::AchievementH
return singleton;
}
inline bool AchievementsEnabled(bool bEnabled = NULL)
inline bool AchievementsEnabled(std::optional<bool> bEnabled = std::nullopt)
{
static bool value;
if (bEnabled != NULL) {
value = bEnabled;
static bool value = false;
if (bEnabled.has_value()) {
value = *bEnabled;
}
return value;
}
@ -68,7 +69,7 @@ inline void LoadINI(std::map<std::string, bool>* settings, const char* iniPath)
}
if (bUpdateINI) {
logger::info("New settings detected, adding to ArtifactTracker.ini");
logger::info("New settings detected, updating {}", iniPath);
ini.SaveFile(iniPath);
}