229 lines
7.5 KiB
C++
229 lines
7.5 KiB
C++
#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 },
|
|
{ "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 {
|
|
const char* appId = settings.at("SendAchievementsToLE") ? APPID_ENDERAL_LE : APPID_ENDERAL_SE;
|
|
|
|
logModuleState("BEFORE");
|
|
logEnvironment("BEFORE");
|
|
logger::info("[BEFORE] HSteamUser = {}", SteamAPI_GetHSteamUser());
|
|
|
|
// 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 (settings.at("ReloadSteamClient")) {
|
|
unloadSteamClient();
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|
|
|
|
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) {
|
|
try {
|
|
std::string msg = "User id: " + std::to_string(event->m_steamIDUser.ConvertToUint64()) + ", game id: " + std::to_string(event->m_nGameID) + ", success state: " + std::to_string(event->m_eResult);
|
|
logger::info("{}", msg.c_str());
|
|
|
|
uint32 achievementCount = this->stats->GetNumAchievements();
|
|
msg = "There are " + std::to_string(achievementCount) + " achievements";
|
|
logger::info("{}", msg.c_str());
|
|
}
|
|
catch (const std::exception& ex) {
|
|
std::string msg = "Exception during steam callback: onUserStatsReceived. Failed to print data: " + std::string(ex.what());
|
|
logger::info("{}", msg.c_str());
|
|
}
|
|
}
|
|
|
|
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);
|
|
if (!success) {
|
|
logger::error("{}", "Error while unlocking achievement");
|
|
return false;
|
|
}
|
|
success = this->stats->StoreStats();
|
|
if (!success) {
|
|
logger::error("{}", "Error while storing unlocked achievement");
|
|
}
|
|
return success;
|
|
}
|
|
|
|
void AchievementHolder::start()
|
|
{
|
|
if (!this->stats) {
|
|
return;
|
|
}
|
|
|
|
if (!this->stats->RequestCurrentStats()) {
|
|
logger::error("{}", "RequestCurrentStats failed, achievements may not unlock");
|
|
}
|
|
}
|
|
}
|