enderalse/source/Steam DLL/src/Achievements.cpp

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");
}
}
}