mirror of
https://github.com/RetroAchievements/RAIntegration.git
synced 2026-10-12 02:09:25 +00:00
974 lines
33 KiB
C++
974 lines
33 KiB
C++
#include "EmulatorContext.hh"
|
|
|
|
#include "Exports.hh"
|
|
#include "RA_BuildVer.h"
|
|
#include "RA_Log.h"
|
|
#include "RA_StringUtils.h"
|
|
|
|
#include "api\LatestClient.hh"
|
|
|
|
#include "data\context\GameContext.hh"
|
|
#include "data\context\UserContext.hh"
|
|
|
|
#include "services\AchievementRuntime.hh"
|
|
#include "services\IClock.hh"
|
|
#include "services\IConfiguration.hh"
|
|
#include "services\IFileSystem.hh"
|
|
#include "services\IHttpRequester.hh"
|
|
|
|
#include "ui\IDesktop.hh"
|
|
|
|
#include "ui\viewmodels\MessageBoxViewModel.hh"
|
|
#include "ui\viewmodels\WindowManager.hh"
|
|
|
|
#include "RAInterface\RA_Consoles.h"
|
|
#include "RAInterface\RA_Emulators.h"
|
|
|
|
namespace ra {
|
|
namespace data {
|
|
namespace context {
|
|
|
|
void EmulatorContext::Initialize(EmulatorID nEmulatorId, const char* sClientName)
|
|
{
|
|
m_nEmulatorId = nEmulatorId;
|
|
|
|
switch (nEmulatorId)
|
|
{
|
|
case RA_Gens:
|
|
m_sClientName = "RAGens_REWiND";
|
|
m_sAcceptButtonText = L"\u24B8"; // encircled C
|
|
_RA_SetConsoleID(ConsoleID::MegaDrive);
|
|
break;
|
|
|
|
case RA_Project64:
|
|
m_sClientName = "RAP64";
|
|
_RA_SetConsoleID(ConsoleID::N64);
|
|
break;
|
|
|
|
case RA_Snes9x:
|
|
m_sClientName = "RASnes9X";
|
|
_RA_SetConsoleID(ConsoleID::SNES);
|
|
break;
|
|
|
|
case RA_VisualboyAdvance:
|
|
m_sClientName = "RAVisualBoyAdvance";
|
|
_RA_SetConsoleID(ConsoleID::GB);
|
|
break;
|
|
|
|
case RA_Nester:
|
|
case RA_FCEUX:
|
|
m_sClientName = "RANes";
|
|
_RA_SetConsoleID(ConsoleID::NES);
|
|
break;
|
|
|
|
case RA_PCE:
|
|
m_sClientName = "RAPCE";
|
|
_RA_SetConsoleID(ConsoleID::PCEngine);
|
|
break;
|
|
|
|
case RA_Libretro:
|
|
m_sClientName = "RALibRetro";
|
|
// don't provide a default console - wait for user to load core
|
|
break;
|
|
|
|
case RA_Meka:
|
|
m_sClientName = "RAMeka";
|
|
_RA_SetConsoleID(ConsoleID::MasterSystem);
|
|
break;
|
|
|
|
case RA_QUASI88:
|
|
m_sClientName = "RAQUASI88";
|
|
m_sAcceptButtonText = L"Enter";
|
|
m_sCancelButtonText = L"Backspace";
|
|
_RA_SetConsoleID(ConsoleID::PC8800);
|
|
break;
|
|
|
|
case RA_AppleWin:
|
|
m_sClientName = "RAppleWin";
|
|
m_sAcceptButtonText = L"Enter";
|
|
m_sCancelButtonText = L"Backspace";
|
|
_RA_SetConsoleID(ConsoleID::AppleII);
|
|
break;
|
|
|
|
case RA_Oricutron:
|
|
m_sClientName = "RAOricutron";
|
|
m_sAcceptButtonText = L"Enter";
|
|
m_sCancelButtonText = L"Backspace";
|
|
_RA_SetConsoleID(ConsoleID::Oric);
|
|
break;
|
|
|
|
default:
|
|
if (sClientName != nullptr && *sClientName)
|
|
{
|
|
m_sClientName = sClientName;
|
|
}
|
|
else
|
|
{
|
|
const auto& pDesktop = ra::services::ServiceLocator::Get<ra::ui::IDesktop>();
|
|
const auto sFileName = pDesktop.GetRunningExecutable();
|
|
const auto& pFileSystem = ra::services::ServiceLocator::Get<ra::services::IFileSystem>();
|
|
m_sClientName = ra::Narrow(pFileSystem.RemoveExtension(pFileSystem.GetFileName(sFileName)));
|
|
}
|
|
m_nEmulatorId = EmulatorID::UnknownEmulator;
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void AppendIntegrationVersion(_Inout_ std::string& sUserAgent)
|
|
{
|
|
sUserAgent.append(ra::StringPrintf("%d.%d.%d.%d", RA_INTEGRATION_VERSION_MAJOR,
|
|
RA_INTEGRATION_VERSION_MINOR, RA_INTEGRATION_VERSION_PATCH,
|
|
RA_INTEGRATION_VERSION_REVISION));
|
|
|
|
// include branch if specified
|
|
if constexpr (_CONSTANT_LOC pos{ std::string_view{ RA_INTEGRATION_VERSION_PRODUCT }.find('-') }; pos != std::string_view::npos)
|
|
{
|
|
constexpr std::string_view sAppend{ RA_INTEGRATION_VERSION_PRODUCT };
|
|
sUserAgent.append(sAppend, pos);
|
|
}
|
|
}
|
|
|
|
void EmulatorContext::UpdateUserAgent()
|
|
{
|
|
std::string sUserAgent;
|
|
sUserAgent.reserve(128U);
|
|
|
|
if (!m_sClientName.empty())
|
|
{
|
|
sUserAgent.append(m_sClientName);
|
|
sUserAgent.push_back('/');
|
|
}
|
|
else
|
|
{
|
|
sUserAgent.append("UnknownClient/");
|
|
}
|
|
|
|
size_t nOffset = 0;
|
|
if (!m_sVersion.empty() && (m_sVersion.at(0) == 'v' || m_sVersion.at(0) == 'V'))
|
|
{
|
|
/* version string starts with a "v". assume it's part of a prefix (i.e. "v1.2", or "version 1.2") and ignore it */
|
|
while (nOffset < m_sVersion.length() && (isalpha(m_sVersion.at(nOffset)) || isspace(m_sVersion.at(nOffset))))
|
|
++nOffset;
|
|
}
|
|
|
|
sUserAgent.append(m_sVersion, nOffset);
|
|
sUserAgent.append(" (");
|
|
|
|
const auto& pDesktop = ra::services::ServiceLocator::Get<ra::ui::IDesktop>();
|
|
sUserAgent.append(pDesktop.GetOSVersionString());
|
|
sUserAgent.append(") Integration/");
|
|
AppendIntegrationVersion(sUserAgent);
|
|
|
|
if (!m_sClientUserAgentDetail.empty())
|
|
{
|
|
sUserAgent.push_back(' ');
|
|
sUserAgent.append(m_sClientUserAgentDetail);
|
|
}
|
|
|
|
RA_LOG_INFO("User-Agent: %s", sUserAgent.c_str());
|
|
|
|
ra::services::ServiceLocator::GetMutable<ra::services::IHttpRequester>().SetUserAgent(sUserAgent);
|
|
}
|
|
|
|
static unsigned long long ParseVersion(const char* sVersion)
|
|
{
|
|
Expects(sVersion != nullptr);
|
|
|
|
while (isalpha(*sVersion) || isspace(*sVersion))
|
|
++sVersion;
|
|
|
|
char* pPart{};
|
|
const auto major = strtoull(sVersion, &pPart, 10);
|
|
Expects(pPart != nullptr);
|
|
if (*pPart == '.')
|
|
++pPart;
|
|
|
|
const auto minor = strtoul(pPart, &pPart, 10);
|
|
if (*pPart == '.')
|
|
++pPart;
|
|
|
|
const auto patch = strtoul(pPart, &pPart, 10);
|
|
if (*pPart == '.')
|
|
++pPart;
|
|
|
|
const auto revision = strtoul(pPart, &pPart, 10);
|
|
// 64-bit max signed value is 9223 37203 68547 75807
|
|
auto version = (major * 100000) + minor;
|
|
version = (version * 100000) + patch;
|
|
version = (version * 100000) + revision;
|
|
return version;
|
|
}
|
|
|
|
bool EmulatorContext::ValidateClientVersion()
|
|
{
|
|
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
|
|
const bool bWasHardcore = pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore);
|
|
bool bHardcore = bWasHardcore;
|
|
if (!ValidateClientVersion(bHardcore))
|
|
return false;
|
|
|
|
// ValidateClientVersion can set bHardcore to false if it was true and the user chose not to upgrade
|
|
if (bWasHardcore && !bHardcore)
|
|
DisableHardcoreMode();
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns <c>true</c> if the player is allowed to play using the current client.
|
|
/// </summary>
|
|
bool EmulatorContext::ValidateClientVersion(bool& bHardcore)
|
|
{
|
|
std::string sMinimumVersion;
|
|
|
|
// client not specified - assume pre-release of new client
|
|
if (m_nEmulatorId == EmulatorID::UnknownEmulator)
|
|
return true;
|
|
|
|
// fetch the latest version
|
|
unsigned long long nMinimumVersion = 0;
|
|
if (m_sLatestVersion.empty())
|
|
{
|
|
ra::api::LatestClient::Request request;
|
|
request.EmulatorId = ra::etoi(m_nEmulatorId);
|
|
auto response = request.Call();
|
|
if (!response.Succeeded())
|
|
{
|
|
if (ra::StringStartsWith(response.ErrorMessage, "Unknown client!"))
|
|
{
|
|
// if m_nEmulatorID is not recognized by the server, let it through regardless of version.
|
|
// assume it's a new emulator that hasn't been released yet.
|
|
m_sLatestVersion = "0.0.0.0";
|
|
ra::ui::viewmodels::MessageBoxViewModel::ShowWarningMessage(L"Could not retrieve latest client version.", ra::Widen(response.ErrorMessage));
|
|
}
|
|
else
|
|
{
|
|
m_sLatestVersion = "Unknown";
|
|
m_sLatestVersionError = response.ErrorMessage;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_sLatestVersion = response.LatestVersion;
|
|
sMinimumVersion = response.MinimumVersion;
|
|
nMinimumVersion = ParseVersion(response.MinimumVersion.c_str());
|
|
|
|
#ifndef RA_UTEST
|
|
const unsigned long long nServerVersion = ParseVersion(m_sLatestVersion.c_str());
|
|
const unsigned long long nLocalVersion = ParseVersion(m_sVersion.c_str());
|
|
RA_LOG_INFO("Client %s date: server %s, current %s",
|
|
(nLocalVersion >= nServerVersion) ? "up to" : "out of",
|
|
m_sLatestVersion,
|
|
m_sVersion);
|
|
#endif
|
|
}
|
|
|
|
if (m_nEmulatorId == EmulatorID::RA_Gens)
|
|
ra::ui::viewmodels::MessageBoxViewModel::ShowWarningMessage(L"RAGens is being retired", L"With the next major release of the toolkit, you will no longer be able to play games using RAGens. Please switch over to RALibretro or RetroArch.");
|
|
}
|
|
|
|
// if we failed to fetch the latest version, abort
|
|
if (m_sLatestVersion == "Unknown")
|
|
{
|
|
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
|
|
|
|
std::wstring sError;
|
|
if (bHardcore)
|
|
{
|
|
sError = L"The latest client is required for hardcore mode.";
|
|
auto& pUserContext = ra::services::ServiceLocator::GetMutable<ra::data::context::UserContext>();
|
|
if (pUserContext.IsLoggedIn())
|
|
{
|
|
sError += L" You will be logged out.";
|
|
pUserContext.Logout();
|
|
}
|
|
else
|
|
{
|
|
if (!pUserContext.IsLoginDisabled() && !pConfiguration.GetApiToken().empty())
|
|
sError += L" Login canceled.";
|
|
}
|
|
|
|
pUserContext.DisableLogin();
|
|
}
|
|
|
|
if (!m_sLatestVersionError.empty())
|
|
{
|
|
if (!sError.empty())
|
|
sError += L"\n";
|
|
sError += ra::Widen(m_sLatestVersionError);
|
|
}
|
|
|
|
ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(L"Could not retrieve latest client version.", sError);
|
|
return false;
|
|
}
|
|
|
|
// determine if current version is valid
|
|
const unsigned long long nServerVersion = ParseVersion(m_sLatestVersion.c_str());
|
|
const unsigned long long nLocalVersion = ParseVersion(m_sVersion.c_str());
|
|
|
|
if (nLocalVersion >= nServerVersion)
|
|
return true;
|
|
|
|
// remove leading prefix and any trailing ".0"s off the client version
|
|
std::string sClientVersion = m_sVersion;
|
|
while (!sClientVersion.empty() && (isalpha(sClientVersion.at(0)) || isspace(sClientVersion.at(0))))
|
|
sClientVersion.erase(0, 1);
|
|
while (ra::StringEndsWith(sClientVersion, ".0"))
|
|
sClientVersion.resize(sClientVersion.length() - 2);
|
|
if (sClientVersion.find('.') == std::string::npos)
|
|
sClientVersion.append(".0");
|
|
|
|
std::string sNewVersion = m_sLatestVersion;
|
|
while (ra::StringEndsWith(sNewVersion, ".0"))
|
|
sNewVersion.resize(sNewVersion.length() - 2);
|
|
if (sNewVersion.find('.') == std::string::npos)
|
|
sNewVersion.append(".0");
|
|
|
|
bool bUpdate = false;
|
|
bool bResult = true;
|
|
|
|
std::wstring sMessage;
|
|
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
|
|
if (nLocalVersion < nMinimumVersion && bHardcore)
|
|
{
|
|
// enforce minimum version required for hardcore
|
|
ra::ui::viewmodels::MessageBoxViewModel vmMessageBox;
|
|
vmMessageBox.SetHeader(L"A newer client is required for hardcore mode.");
|
|
vmMessageBox.SetMessage(ra::StringPrintf(
|
|
L"A new version of %s is available to download at %s.\n\n- Current version: %s\n- New version: %s\n\n"
|
|
L"Press OK to logout and download the new version, or Cancel to disable hardcore mode and proceed.",
|
|
m_sClientName,
|
|
pConfiguration.GetHostName(),
|
|
sClientVersion,
|
|
sNewVersion));
|
|
vmMessageBox.SetIcon(ra::ui::viewmodels::MessageBoxViewModel::Icon::Warning);
|
|
vmMessageBox.SetButtons(ra::ui::viewmodels::MessageBoxViewModel::Buttons::OKCancel);
|
|
|
|
if (vmMessageBox.ShowModal() == ra::ui::DialogResult::Cancel)
|
|
{
|
|
RA_LOG_INFO("Hardcore disabled by version check %s < %s", m_sVersion, sMinimumVersion);
|
|
bHardcore = false;
|
|
}
|
|
else
|
|
{
|
|
bUpdate = true;
|
|
bResult = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// allow any version in non-hardcore, but inform the user a new version is available
|
|
ra::ui::viewmodels::MessageBoxViewModel vmMessageBox;
|
|
vmMessageBox.SetHeader(L"Would you like to update?");
|
|
vmMessageBox.SetMessage(ra::StringPrintf(
|
|
L"A new version of %s is available to download at %s.\n\n- Current version: %s\n- New version: %s",
|
|
m_sClientName,
|
|
pConfiguration.GetHostName(),
|
|
sClientVersion,
|
|
sNewVersion));
|
|
vmMessageBox.SetIcon(ra::ui::viewmodels::MessageBoxViewModel::Icon::Info);
|
|
vmMessageBox.SetButtons(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo);
|
|
|
|
bUpdate = (vmMessageBox.ShowModal() == ra::ui::DialogResult::Yes);
|
|
}
|
|
|
|
if (bUpdate)
|
|
{
|
|
ra::services::ServiceLocator::Get<ra::ui::IDesktop>().OpenUrl(
|
|
ra::StringPrintf("%s/download.php", pConfiguration.GetHostUrl()));
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
bool EmulatorContext::WarnDisableHardcoreMode(const std::string& sActivity)
|
|
{
|
|
// already disabled, just return success
|
|
auto& pConfiguration = ra::services::ServiceLocator::GetMutable<ra::services::IConfiguration>();
|
|
if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore))
|
|
return true;
|
|
|
|
if (!sActivity.empty())
|
|
{
|
|
// prompt. if user doesn't consent, return failure - caller should not continue
|
|
ra::ui::viewmodels::MessageBoxViewModel vmMessageBox;
|
|
vmMessageBox.SetHeader(L"Disable Hardcore mode?");
|
|
vmMessageBox.SetMessage(ra::StringPrintf(L"You cannot %s while Hardcore mode is active.", sActivity));
|
|
vmMessageBox.SetButtons(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo);
|
|
vmMessageBox.SetIcon(ra::ui::viewmodels::MessageBoxViewModel::Icon::Warning);
|
|
if (vmMessageBox.ShowModal() != ra::ui::DialogResult::Yes)
|
|
return false;
|
|
|
|
RA_LOG_INFO("User chose to disable hardcore to %s", sActivity);
|
|
}
|
|
else
|
|
{
|
|
// no activity specified. just proceed to disabling without prompting.
|
|
RA_LOG_INFO("Disabling hardcore. No activity provided to WarnDisableHardcore");
|
|
}
|
|
|
|
// user consented, switch to non-hardcore mode
|
|
DisableHardcoreMode();
|
|
|
|
// return success
|
|
return true;
|
|
}
|
|
|
|
void EmulatorContext::DisableHardcoreMode()
|
|
{
|
|
auto& pConfiguration = ra::services::ServiceLocator::GetMutable<ra::services::IConfiguration>();
|
|
if (pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore))
|
|
{
|
|
pConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false);
|
|
|
|
// updating the enabled-ness of the buttons of the asset list
|
|
auto& pWindowManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>();
|
|
pWindowManager.AssetList.UpdateButtons();
|
|
|
|
RebuildMenu();
|
|
|
|
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
|
|
pGameContext.RefreshUnlocks();
|
|
}
|
|
}
|
|
|
|
bool EmulatorContext::EnableHardcoreMode(bool bShowWarning)
|
|
{
|
|
auto& pConfiguration = ra::services::ServiceLocator::GetMutable<ra::services::IConfiguration>();
|
|
if (pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore))
|
|
return true;
|
|
|
|
// pass true to ValidateClientVersion so it behaves as if hardcore is already enabled
|
|
bool bHardcore = true;
|
|
if (!ValidateClientVersion(bHardcore))
|
|
{
|
|
// The version could not be validated, or the user has chosen to update. Log them out.
|
|
auto& pUserContext = ra::services::ServiceLocator::GetMutable<ra::data::context::UserContext>();
|
|
pUserContext.Logout();
|
|
return false;
|
|
}
|
|
|
|
if (!bHardcore)
|
|
{
|
|
// The user has chosen to continue playing without upgrading their client. Abort the switch to hardcore
|
|
return false;
|
|
}
|
|
|
|
// The user is on the latest version. If a game is loaded, inform them that the emulator must be reset
|
|
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
|
|
if (pGameContext.GameId() != 0 && bShowWarning)
|
|
{
|
|
ra::ui::viewmodels::MessageBoxViewModel vmMessageBox;
|
|
vmMessageBox.SetHeader(L"Enable hardcore mode?");
|
|
vmMessageBox.SetMessage(
|
|
L"Enabling hardcore mode will reset the emulator. You will lose any progress that has not been saved "
|
|
L"through the game.");
|
|
vmMessageBox.SetIcon(ra::ui::viewmodels::MessageBoxViewModel::Icon::Warning);
|
|
vmMessageBox.SetButtons(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo);
|
|
|
|
if (vmMessageBox.ShowModal() == ra::ui::DialogResult::No)
|
|
return false;
|
|
}
|
|
|
|
RA_LOG_INFO("Hardcore enabled");
|
|
|
|
// Disable any modified assets
|
|
for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast<gsl::index>(pGameContext.Assets().Count()); ++nIndex)
|
|
{
|
|
auto* pAsset = pGameContext.Assets().GetItemAt(nIndex);
|
|
if (pAsset != nullptr && pAsset->GetChanges() != ra::data::models::AssetChanges::None)
|
|
{
|
|
if (pAsset->IsActive())
|
|
pAsset->SetState(ra::data::models::AssetState::Inactive);
|
|
}
|
|
}
|
|
|
|
// User has agreed to reset the emulator, or no game is loaded. Enabled hardcore!
|
|
pConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
|
|
|
|
// close any windows not permitted in hardcore
|
|
auto& pWindowManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>();
|
|
auto& pDesktop = ra::services::ServiceLocator::Get<ra::ui::IDesktop>();
|
|
|
|
if (pWindowManager.MemoryInspector.IsVisible())
|
|
pDesktop.CloseWindow(pWindowManager.MemoryInspector);
|
|
|
|
if (pWindowManager.MemoryBookmarks.IsVisible())
|
|
pDesktop.CloseWindow(pWindowManager.MemoryBookmarks);
|
|
|
|
if (pWindowManager.AssetEditor.IsVisible())
|
|
pDesktop.CloseWindow(pWindowManager.AssetEditor);
|
|
|
|
if (pWindowManager.CodeNotes.IsVisible())
|
|
pDesktop.CloseWindow(pWindowManager.CodeNotes);
|
|
|
|
// updating the enabled-ness of the buttons of the asset list
|
|
pWindowManager.AssetList.UpdateButtons();
|
|
|
|
// update the integration menu
|
|
RebuildMenu();
|
|
|
|
// when enabling hardcore mode, force a system reset
|
|
if (m_fResetEmulator)
|
|
m_fResetEmulator();
|
|
|
|
#ifndef RA_UTEST
|
|
// emulator reset code will normally call back into _RA_OnReset, but call it explicitly just in case.
|
|
_RA_OnReset();
|
|
#endif
|
|
|
|
auto& vmBookmarks = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().MemoryBookmarks;
|
|
for (gsl::index nIndex = 0; nIndex < ra::to_signed(vmBookmarks.Bookmarks().Count()); ++nIndex)
|
|
{
|
|
auto* pBookmark = vmBookmarks.Bookmarks().GetItemAt(nIndex);
|
|
if (pBookmark)
|
|
pBookmark->SetBehavior(ra::ui::viewmodels::MemoryBookmarksViewModel::BookmarkBehavior::None);
|
|
}
|
|
|
|
pGameContext.ActivateLeaderboards();
|
|
pGameContext.RefreshUnlocks();
|
|
|
|
return true;
|
|
}
|
|
|
|
std::wstring EmulatorContext::GetAppTitle(const std::string& sMessage) const
|
|
{
|
|
ra::StringBuilder builder;
|
|
builder.Append(m_sClientName);
|
|
builder.Append(" - ");
|
|
|
|
// only copy the first two parts of the version string to the title bar: 0.12.7.1 => 0.12
|
|
std::string sClientVersion = m_sVersion;
|
|
auto nIndex = sClientVersion.find('.');
|
|
if (nIndex != std::string::npos)
|
|
{
|
|
nIndex = sClientVersion.find('.', nIndex + 1);
|
|
if (nIndex != std::string::npos)
|
|
sClientVersion.erase(nIndex);
|
|
}
|
|
builder.Append(sClientVersion);
|
|
|
|
if (!sMessage.empty())
|
|
{
|
|
builder.Append(" - ");
|
|
builder.Append(sMessage);
|
|
}
|
|
|
|
const auto& pUserContext = ra::services::ServiceLocator::Get<ra::data::context::UserContext>();
|
|
if (pUserContext.IsLoggedIn())
|
|
{
|
|
builder.Append(" - ");
|
|
builder.Append(pUserContext.GetDisplayName());
|
|
}
|
|
|
|
const auto& sHostName = ra::services::ServiceLocator::Get<ra::services::IConfiguration>().GetHostName();
|
|
if (sHostName != "retroachievements.org")
|
|
{
|
|
builder.Append(" [");
|
|
builder.Append(sHostName);
|
|
builder.Append("]");
|
|
}
|
|
|
|
return builder.ToWString();
|
|
}
|
|
|
|
std::string EmulatorContext::GetGameTitle() const
|
|
{
|
|
if (!m_fGetGameTitle)
|
|
return std::string();
|
|
|
|
std::array<char, 256> buffer{};
|
|
m_fGetGameTitle(buffer.data());
|
|
return std::string(buffer.data());
|
|
}
|
|
|
|
void EmulatorContext::AddMemoryBlock(gsl::index nIndex, size_t nBytes,
|
|
EmulatorContext::MemoryReadFunction* pReader, EmulatorContext::MemoryWriteFunction* pWriter)
|
|
{
|
|
while (m_vMemoryBlocks.size() <= ra::to_unsigned(nIndex))
|
|
m_vMemoryBlocks.emplace_back();
|
|
|
|
MemoryBlock& pBlock = m_vMemoryBlocks.at(nIndex);
|
|
if (pBlock.size == 0)
|
|
{
|
|
pBlock.size = nBytes;
|
|
pBlock.read = pReader;
|
|
pBlock.write = pWriter;
|
|
pBlock.readBlock = nullptr;
|
|
|
|
m_nTotalMemorySize += nBytes;
|
|
|
|
OnTotalMemorySizeChanged();
|
|
}
|
|
}
|
|
|
|
void EmulatorContext::AddMemoryBlockReader(gsl::index nIndex,
|
|
EmulatorContext::MemoryReadBlockFunction* pReader)
|
|
{
|
|
if (nIndex < gsl::narrow_cast<gsl::index>(m_vMemoryBlocks.size()))
|
|
m_vMemoryBlocks.at(nIndex).readBlock = pReader;
|
|
}
|
|
|
|
void EmulatorContext::OnTotalMemorySizeChanged()
|
|
{
|
|
// create a copy of the list of pointers in case it's modified by one of the callbacks
|
|
NotifyTargetSet vNotifyTargets(m_vNotifyTargets);
|
|
for (NotifyTarget* target : vNotifyTargets)
|
|
{
|
|
Expects(target != nullptr);
|
|
target->OnTotalMemorySizeChanged();
|
|
}
|
|
}
|
|
|
|
bool EmulatorContext::HasInvalidRegions() const noexcept
|
|
{
|
|
for (const auto& pBlock : m_vMemoryBlocks)
|
|
{
|
|
if (!pBlock.read)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool EmulatorContext::IsValidAddress(ra::ByteAddress nAddress) const noexcept
|
|
{
|
|
for (const auto& pBlock : m_vMemoryBlocks)
|
|
{
|
|
if (nAddress < pBlock.size)
|
|
{
|
|
if (pBlock.read)
|
|
return true;
|
|
|
|
break;
|
|
}
|
|
|
|
nAddress -= gsl::narrow_cast<ra::ByteAddress>(pBlock.size);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
uint8_t EmulatorContext::ReadMemoryByte(ra::ByteAddress nAddress) const
|
|
{
|
|
const ra::ByteAddress nOriginalAddress = nAddress;
|
|
for (const auto& pBlock : m_vMemoryBlocks)
|
|
{
|
|
if (nAddress < pBlock.size)
|
|
{
|
|
if (pBlock.read)
|
|
return pBlock.read(nAddress);
|
|
|
|
break;
|
|
}
|
|
|
|
nAddress -= gsl::narrow_cast<ra::ByteAddress>(pBlock.size);
|
|
}
|
|
|
|
if (nOriginalAddress < m_nTotalMemorySize)
|
|
ra::services::ServiceLocator::GetMutable<ra::services::AchievementRuntime>().InvalidateAddress(nOriginalAddress);
|
|
|
|
return 0;
|
|
}
|
|
|
|
_Use_decl_annotations_
|
|
void EmulatorContext::ReadMemory(ra::ByteAddress nAddress, uint8_t pBuffer[], size_t nCount) const
|
|
{
|
|
const ra::ByteAddress nOriginalAddress = nAddress;
|
|
Expects(pBuffer != nullptr);
|
|
|
|
for (const auto& pBlock : m_vMemoryBlocks)
|
|
{
|
|
if (nAddress >= pBlock.size)
|
|
{
|
|
nAddress -= gsl::narrow_cast<ra::ByteAddress>(pBlock.size);
|
|
continue;
|
|
}
|
|
|
|
const size_t nBlockRemaining = pBlock.size - nAddress;
|
|
size_t nToRead = std::min(nCount, nBlockRemaining);
|
|
nCount -= nToRead;
|
|
|
|
if (pBlock.readBlock)
|
|
{
|
|
const size_t nRead = pBlock.readBlock(nAddress, pBuffer, gsl::narrow_cast<uint32_t>(nToRead));
|
|
if (nRead < nToRead)
|
|
memset(pBuffer + nRead, 0, nToRead - nRead);
|
|
|
|
pBuffer += nToRead;
|
|
}
|
|
else if (!pBlock.read)
|
|
{
|
|
ra::services::ServiceLocator::GetMutable<ra::services::AchievementRuntime>().InvalidateAddress(nOriginalAddress);
|
|
memset(pBuffer, 0, nToRead);
|
|
pBuffer += nToRead;
|
|
}
|
|
else
|
|
{
|
|
while (nToRead >= 8) // unrolled loop to read 8-byte chunks
|
|
{
|
|
*pBuffer++ = pBlock.read(nAddress++);
|
|
*pBuffer++ = pBlock.read(nAddress++);
|
|
*pBuffer++ = pBlock.read(nAddress++);
|
|
*pBuffer++ = pBlock.read(nAddress++);
|
|
*pBuffer++ = pBlock.read(nAddress++);
|
|
*pBuffer++ = pBlock.read(nAddress++);
|
|
*pBuffer++ = pBlock.read(nAddress++);
|
|
*pBuffer++ = pBlock.read(nAddress++);
|
|
nToRead -= 8;
|
|
}
|
|
|
|
switch (nToRead) // partial Duff's device to read remaining bytes
|
|
{
|
|
case 7: *pBuffer++ = pBlock.read(nAddress++); _FALLTHROUGH;
|
|
case 6: *pBuffer++ = pBlock.read(nAddress++); _FALLTHROUGH;
|
|
case 5: *pBuffer++ = pBlock.read(nAddress++); _FALLTHROUGH;
|
|
case 4: *pBuffer++ = pBlock.read(nAddress++); _FALLTHROUGH;
|
|
case 3: *pBuffer++ = pBlock.read(nAddress++); _FALLTHROUGH;
|
|
case 2: *pBuffer++ = pBlock.read(nAddress++); _FALLTHROUGH;
|
|
case 1: *pBuffer++ = pBlock.read(nAddress++); _FALLTHROUGH;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
if (nCount == 0)
|
|
return;
|
|
|
|
nAddress = 0;
|
|
}
|
|
|
|
if (nCount > 0)
|
|
memset(pBuffer, 0, nCount);
|
|
}
|
|
|
|
uint32_t EmulatorContext::ReadMemory(ra::ByteAddress nAddress, MemSize nSize) const
|
|
{
|
|
switch (nSize)
|
|
{
|
|
case MemSize::Bit_0:
|
|
return (ReadMemoryByte(nAddress) & 0x01);
|
|
case MemSize::Bit_1:
|
|
return (ReadMemoryByte(nAddress) & 0x02) ? 1 : 0;
|
|
case MemSize::Bit_2:
|
|
return (ReadMemoryByte(nAddress) & 0x04) ? 1 : 0;
|
|
case MemSize::Bit_3:
|
|
return (ReadMemoryByte(nAddress) & 0x08) ? 1 : 0;
|
|
case MemSize::Bit_4:
|
|
return (ReadMemoryByte(nAddress) & 0x10) ? 1 : 0;
|
|
case MemSize::Bit_5:
|
|
return (ReadMemoryByte(nAddress) & 0x20) ? 1 : 0;
|
|
case MemSize::Bit_6:
|
|
return (ReadMemoryByte(nAddress) & 0x40) ? 1 : 0;
|
|
case MemSize::Bit_7:
|
|
return (ReadMemoryByte(nAddress) & 0x80) ? 1 : 0;
|
|
case MemSize::Nibble_Lower:
|
|
return (ReadMemoryByte(nAddress) & 0x0F);
|
|
case MemSize::Nibble_Upper:
|
|
return ((ReadMemoryByte(nAddress) >> 4) & 0x0F);
|
|
case MemSize::EightBit:
|
|
return ReadMemoryByte(nAddress);
|
|
default:
|
|
case MemSize::SixteenBit:
|
|
{
|
|
uint8_t buffer[2];
|
|
ReadMemory(nAddress, buffer, 2);
|
|
return buffer[0] | (buffer[1] << 8);
|
|
}
|
|
case MemSize::TwentyFourBit:
|
|
{
|
|
uint8_t buffer[3];
|
|
ReadMemory(nAddress, buffer, 3);
|
|
return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16);
|
|
}
|
|
case MemSize::Float:
|
|
case MemSize::MBF32:
|
|
case MemSize::ThirtyTwoBit:
|
|
{
|
|
uint8_t buffer[4];
|
|
ReadMemory(nAddress, buffer, 4);
|
|
return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24);
|
|
}
|
|
case MemSize::SixteenBitBigEndian:
|
|
{
|
|
uint8_t buffer[2];
|
|
ReadMemory(nAddress, buffer, 2);
|
|
return buffer[1] | (buffer[0] << 8);
|
|
}
|
|
case MemSize::TwentyFourBitBigEndian:
|
|
{
|
|
uint8_t buffer[3];
|
|
ReadMemory(nAddress, buffer, 3);
|
|
return buffer[2] | (buffer[1] << 8) | (buffer[0] << 16);
|
|
}
|
|
case MemSize::ThirtyTwoBitBigEndian:
|
|
{
|
|
uint8_t buffer[4];
|
|
ReadMemory(nAddress, buffer, 4);
|
|
return buffer[3] | (buffer[2] << 8) | (buffer[1] << 16) | (buffer[0] << 24);
|
|
}
|
|
case MemSize::BitCount:
|
|
{
|
|
const uint8_t nValue = ReadMemoryByte(nAddress);
|
|
static const std::array<uint8_t, 16> nBitsSet = { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 };
|
|
return nBitsSet.at(nValue & 0x0F) + nBitsSet.at((nValue >> 4) & 0x0F);
|
|
}
|
|
}
|
|
}
|
|
|
|
void EmulatorContext::WriteMemoryByte(ra::ByteAddress nAddress, uint8_t nValue) const
|
|
{
|
|
auto nBlockAddress = nAddress;
|
|
for (const auto& pBlock : m_vMemoryBlocks)
|
|
{
|
|
if (nBlockAddress < pBlock.size)
|
|
{
|
|
if (!pBlock.write)
|
|
return;
|
|
|
|
pBlock.write(nBlockAddress, nValue);
|
|
m_bMemoryModified = true;
|
|
|
|
// create a copy of the list of pointers in case it's modified by one of the callbacks
|
|
NotifyTargetSet vNotifyTargets(m_vNotifyTargets);
|
|
for (NotifyTarget* target : vNotifyTargets)
|
|
{
|
|
Expects(target != nullptr);
|
|
target->OnByteWritten(nAddress, nValue);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
nBlockAddress -= gsl::narrow_cast<ra::ByteAddress>(pBlock.size);
|
|
}
|
|
}
|
|
|
|
void EmulatorContext::WriteMemory(ra::ByteAddress nAddress, MemSize nSize, uint32_t nValue) const
|
|
{
|
|
switch (nSize)
|
|
{
|
|
case MemSize::EightBit:
|
|
WriteMemoryByte(nAddress, nValue & 0xFF);
|
|
return;
|
|
case MemSize::SixteenBit:
|
|
WriteMemoryByte(nAddress, nValue & 0xFF);
|
|
nValue >>= 8;
|
|
WriteMemoryByte(++nAddress, nValue & 0xFF);
|
|
return;
|
|
case MemSize::TwentyFourBit:
|
|
WriteMemoryByte(nAddress, nValue & 0xFF);
|
|
nValue >>= 8;
|
|
WriteMemoryByte(++nAddress, nValue & 0xFF);
|
|
nValue >>= 8;
|
|
WriteMemoryByte(++nAddress, nValue & 0xFF);
|
|
return;
|
|
case MemSize::ThirtyTwoBit:
|
|
case MemSize::Float: // assumes the value has already been encoded into a 32-bit value
|
|
case MemSize::MBF32: // assumes the value has already been encoded into a 32-bit value
|
|
WriteMemoryByte(nAddress, nValue & 0xFF);
|
|
nValue >>= 8;
|
|
WriteMemoryByte(++nAddress, nValue & 0xFF);
|
|
nValue >>= 8;
|
|
WriteMemoryByte(++nAddress, nValue & 0xFF);
|
|
nValue >>= 8;
|
|
WriteMemoryByte(++nAddress, nValue & 0xFF);
|
|
return;
|
|
case MemSize::SixteenBitBigEndian:
|
|
WriteMemoryByte(nAddress, (nValue >> 8) & 0xFF);
|
|
WriteMemoryByte(++nAddress, nValue & 0xFF);
|
|
return;
|
|
case MemSize::TwentyFourBitBigEndian:
|
|
WriteMemoryByte(nAddress, (nValue >> 16) & 0xFF);
|
|
WriteMemoryByte(++nAddress, (nValue >> 8) & 0xFF);
|
|
WriteMemoryByte(++nAddress, nValue & 0xFF);
|
|
return;
|
|
case MemSize::ThirtyTwoBitBigEndian:
|
|
WriteMemoryByte(nAddress, (nValue >> 24) & 0xFF);
|
|
WriteMemoryByte(++nAddress, (nValue >> 16) & 0xFF);
|
|
WriteMemoryByte(++nAddress, (nValue >> 8) & 0xFF);
|
|
WriteMemoryByte(++nAddress, nValue & 0xFF);
|
|
return;
|
|
case MemSize::BitCount:
|
|
return;
|
|
}
|
|
|
|
const auto nCurrentValue = ReadMemoryByte(nAddress);
|
|
uint8_t nNewValue = gsl::narrow_cast<uint8_t>(nValue);
|
|
|
|
switch (nSize)
|
|
{
|
|
case MemSize::Bit_0:
|
|
nNewValue = (nCurrentValue & ~0x01) | (nNewValue & 1);
|
|
break;
|
|
case MemSize::Bit_1:
|
|
nNewValue = (nCurrentValue & ~0x02) | ((nNewValue & 1) << 1);
|
|
break;
|
|
case MemSize::Bit_2:
|
|
nNewValue = (nCurrentValue & ~0x04) | ((nNewValue & 1) << 2);
|
|
break;
|
|
case MemSize::Bit_3:
|
|
nNewValue = (nCurrentValue & ~0x08) | ((nNewValue & 1) << 3);
|
|
break;
|
|
case MemSize::Bit_4:
|
|
nNewValue = (nCurrentValue & ~0x10) | ((nNewValue & 1) << 4);
|
|
break;
|
|
case MemSize::Bit_5:
|
|
nNewValue = (nCurrentValue & ~0x20) | ((nNewValue & 1) << 5);
|
|
break;
|
|
case MemSize::Bit_6:
|
|
nNewValue = (nCurrentValue & ~0x40) | ((nNewValue & 1) << 6);
|
|
break;
|
|
case MemSize::Bit_7:
|
|
nNewValue = (nCurrentValue & ~0x80) | ((nNewValue & 1) << 7);
|
|
break;
|
|
case MemSize::Nibble_Lower:
|
|
nNewValue = (nCurrentValue & ~0x0F) | (nNewValue & 0x0F);
|
|
break;
|
|
case MemSize::Nibble_Upper:
|
|
nNewValue = (nCurrentValue & ~0xF0) | ((nNewValue & 0x0F) << 4);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
WriteMemoryByte(nAddress, nNewValue);
|
|
}
|
|
|
|
void EmulatorContext::ResetMemoryModified()
|
|
{
|
|
m_bMemoryModified = false;
|
|
|
|
if (m_bMemoryInsecure)
|
|
{
|
|
// memory was insecure, immediately do a check
|
|
const auto& pDesktop = ra::services::ServiceLocator::Get<ra::ui::IDesktop>();
|
|
m_bMemoryInsecure = pDesktop.IsDebuggerPresent();
|
|
|
|
const auto& pClock = ra::services::ServiceLocator::Get<ra::services::IClock>();
|
|
m_tLastInsecureCheck = pClock.UpTime();
|
|
}
|
|
}
|
|
|
|
bool EmulatorContext::IsMemoryInsecure() const
|
|
{
|
|
if (m_bMemoryInsecure)
|
|
return true;
|
|
|
|
// limit checks to once every 10 seconds
|
|
const auto& pClock = ra::services::ServiceLocator::Get<ra::services::IClock>();
|
|
const auto tNow = pClock.UpTime();
|
|
const auto tElapsed = tNow - m_tLastInsecureCheck;
|
|
if (tElapsed > std::chrono::seconds(10))
|
|
{
|
|
const auto& pDesktop = ra::services::ServiceLocator::Get<ra::ui::IDesktop>();
|
|
m_bMemoryInsecure = pDesktop.IsDebuggerPresent();
|
|
m_tLastInsecureCheck = tNow;
|
|
}
|
|
|
|
return m_bMemoryInsecure;
|
|
}
|
|
|
|
} // namespace context
|
|
} // namespace data
|
|
} // namespace ra
|