mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-15 15:30:29 +02:00
The core and the headless tools no longer use mutexes, condition variables, spinlocks, std::atomic, std::thread or volatile flags. Threads hand off through libretro-common's retro_atomic and rthreads plus a few small pieces on top of them in Common/Thread: Thread, MpscQueue, EventCounter, ParkingLot waits and ReaderGate. Every wait blocks until it's woken; nothing polls on a timer. State that only one thread touches just loses its lock. The PSP filesystem, sceIo and the savedata dialog now belong to the emulation thread: async reads and writes run when they're queued and complete later in emulated time, and the IO and savedata threads are gone. The friend finder only talks to the adhoc server and hands what it gets to the emulation thread. GPU init always runs on the thread that starts the game. The Vulkan render and compile queues, pipeline and render pass caches, delete list and SPIR-V cache, the GL render queue, the software renderer's jit caches, the file loader caches, the web debugger, UPnP, input, savestates, logging and the GE debugger all hand off through queues and atomics. The IR jit's spinning profiler thread goes. A CPU thread with nothing to run now sleeps until work is queued instead of spinning. Headless with --debugger waits for a client while stopped, and ends the run once every client that connected has left. Web server shutdown keeps servicing the CPU queue until the server has finished.
634 lines
18 KiB
C++
634 lines
18 KiB
C++
// Copyright (c) 2012- PPSSPP Project.
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, version 2.0 or later versions.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License 2.0 for more details.
|
|
|
|
// A copy of the GPL 2.0 should have been included with the program.
|
|
// If not, see http://www.gnu.org/licenses/
|
|
|
|
// Official git repository and contact information can be found at
|
|
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
|
|
|
#include "ppsspp_config.h"
|
|
|
|
#include <deque>
|
|
#include <set>
|
|
#include <cstdlib>
|
|
#include <cstdarg>
|
|
|
|
// for crc32
|
|
extern "C" {
|
|
#include <encodings/crc32.h>
|
|
}
|
|
|
|
#include <retro_atomic.h>
|
|
|
|
#include "Core/Reporting.h"
|
|
#include "Common/File/VFS/VFS.h"
|
|
#include "Common/CPUDetect.h"
|
|
#include "Common/File/FileUtil.h"
|
|
#include "Common/Serialize/SerializeFuncs.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/System/OSD.h"
|
|
#include "Common/Data/Text/I18n.h"
|
|
#include "Common/Net/HTTPClient.h"
|
|
#include "Common/Net/Resolve.h"
|
|
#include "Common/Net/URL.h"
|
|
#include "Common/Thread/ThreadUtil.h"
|
|
#include "Core/Core.h"
|
|
#include "Core/CoreTiming.h"
|
|
#include "Core/Config.h"
|
|
#include "Core/CwCheat.h"
|
|
#include "Core/Loaders.h"
|
|
#include "Core/SaveState.h"
|
|
#include "Core/System.h"
|
|
#include "Core/MIPS/MIPS.h"
|
|
#include "Core/ELF/ParamSFO.h"
|
|
#include "Core/FileSystems/BlockDevices.h"
|
|
#include "Core/FileSystems/MetaFileSystem.h"
|
|
#include "Core/HLE/Plugins.h"
|
|
#include "Core/HLE/sceKernelMemory.h"
|
|
#include "Core/HLE/scePower.h"
|
|
#include "Core/HW/Display.h"
|
|
#include "GPU/GPUCommon.h"
|
|
#include "GPU/GPUState.h"
|
|
#include "Common/Thread/ParkingLot.h"
|
|
#include "Common/Thread/Thread.h"
|
|
|
|
namespace Reporting
|
|
{
|
|
const int DEFAULT_PORT = 80;
|
|
const u32 SPAM_LIMIT = 100;
|
|
const int PAYLOAD_BUFFER_SIZE = 200;
|
|
|
|
// Internal limiter on number of requests per instance. Bumped by whichever thread logs.
|
|
static retro_atomic_int_t spamProtectionCount{ 0 };
|
|
// Keeps track of whether a harmful setting was ever used.
|
|
static bool everUnsupported = false;
|
|
// Support is cached here to avoid checking it on every single request.
|
|
static bool currentSupported = false;
|
|
// Whether the most recent server request seemed successful.
|
|
static retro_atomic_int_t serverWorking{ 1 };
|
|
// The latest compatibility result from the server.
|
|
static std::vector<std::string> lastCompatResult;
|
|
|
|
static std::string lastModuleName;
|
|
static int lastModuleVersion;
|
|
static uint32_t lastModuleCrc;
|
|
|
|
enum class RequestType {
|
|
NONE,
|
|
MESSAGE,
|
|
COMPAT,
|
|
};
|
|
|
|
struct Payload {
|
|
RequestType type;
|
|
std::string string1;
|
|
std::string string2;
|
|
int int1;
|
|
int int2;
|
|
int int3;
|
|
std::vector<u8> icon;
|
|
};
|
|
|
|
// The CRC thread computes one CRC at a time. crcFilename and crcResults belong to the thread
|
|
// that calls the functions below (the emulation thread); the CRC thread reads crcFilename,
|
|
// writes crcValue, then clears crcPending.
|
|
static Path crcFilename;
|
|
static std::map<Path, u32> crcResults;
|
|
static u32 crcValue;
|
|
static retro_atomic_int_t crcPending{ 0 };
|
|
static retro_atomic_int_t crcCancel{ 0 };
|
|
static Thread crcThread;
|
|
|
|
static u32 CalculateCRC(BlockDevice *blockDevice, retro_atomic_int_t *cancel) {
|
|
auto ga = GetI18NCategory(I18NCat::GAME);
|
|
|
|
u32 crc = 0;
|
|
|
|
u8 block[2048];
|
|
u32 numBlocks = blockDevice->GetNumBlocks();
|
|
for (u32 i = 0; i < numBlocks; ++i) {
|
|
if (cancel && retro_atomic_load_acquire_int(cancel)) {
|
|
g_OSD.RemoveProgressBar("crc", false, 0.0f);
|
|
return 0;
|
|
}
|
|
if (!blockDevice->ReadBlock(i, block, true)) {
|
|
ERROR_LOG(Log::FileSystem, "Failed to read block for CRC");
|
|
g_OSD.RemoveProgressBar("crc", false, 0.0f);
|
|
return 0;
|
|
}
|
|
crc = encoding_crc32(crc, block, 2048);
|
|
g_OSD.SetProgressBar("crc", std::string(ga->T("Calculate CRC")), 0.0f, (float)numBlocks, (float)i, 0.5f);
|
|
}
|
|
|
|
g_OSD.RemoveProgressBar("crc", true, 0.0f);
|
|
return crc;
|
|
}
|
|
|
|
static void FinishCRCThread(u32 crc) {
|
|
crcValue = crc;
|
|
retro_atomic_store_release_int(&crcPending, 0);
|
|
ParkingLotNotify(&crcPending);
|
|
}
|
|
|
|
static int CalculateCRCThread() {
|
|
SetCurrentThreadName("ReportCRC");
|
|
|
|
IdentifiedFileType type;
|
|
|
|
std::string errorString;
|
|
FileLoader *fileLoader = ResolveFileLoaderTarget(ConstructFileLoader(crcFilename), &type, &errorString);
|
|
if (!fileLoader) {
|
|
ERROR_LOG(Log::Loader, "Failed to construct file loader for CRC: %s", errorString.c_str());
|
|
FinishCRCThread(0);
|
|
return 0;
|
|
}
|
|
|
|
std::unique_ptr<BlockDevice> blockDevice(ConstructBlockDevice(fileLoader, &errorString));
|
|
|
|
u32 crc = 0;
|
|
if (blockDevice) {
|
|
crc = CalculateCRC(blockDevice.get(), &crcCancel);
|
|
} else {
|
|
ERROR_LOG(Log::Loader, "Failed to read from block device for CRC: %s", errorString.c_str());
|
|
}
|
|
|
|
blockDevice.reset();
|
|
delete fileLoader;
|
|
|
|
FinishCRCThread(crc);
|
|
return 0;
|
|
}
|
|
|
|
// Picks up the result of a finished CRC thread, if there is one.
|
|
static void CollectCRC() {
|
|
if (crcThread.joinable() && !retro_atomic_load_acquire_int(&crcPending)) {
|
|
crcThread.join();
|
|
crcResults[crcFilename] = crcValue;
|
|
INFO_LOG(Log::System, "Finished CRC calculation");
|
|
}
|
|
}
|
|
|
|
void QueueCRC(const Path &gamePath) {
|
|
CollectCRC();
|
|
|
|
auto it = crcResults.find(gamePath);
|
|
if (it != crcResults.end()) {
|
|
// Nothing to do, we've already calculated it.
|
|
// Note: we assume it stays static until the app is closed.
|
|
return;
|
|
}
|
|
|
|
if (crcThread.joinable()) {
|
|
// Already in process. This is OK - on the crash screen we call this in a polling fashion.
|
|
return;
|
|
}
|
|
|
|
INFO_LOG(Log::System, "Starting CRC calculation");
|
|
crcFilename = gamePath;
|
|
retro_atomic_store_release_int(&crcCancel, 0);
|
|
retro_atomic_store_release_int(&crcPending, 1);
|
|
crcThread = Thread(CalculateCRCThread);
|
|
}
|
|
|
|
bool HasCRC(const Path &gamePath) {
|
|
CollectCRC();
|
|
return crcResults.find(gamePath) != crcResults.end();
|
|
}
|
|
|
|
uint32_t RetrieveCRC(const Path &gamePath) {
|
|
for (;;) {
|
|
QueueCRC(gamePath);
|
|
auto it = crcResults.find(gamePath);
|
|
if (it != crcResults.end()) {
|
|
return it->second;
|
|
}
|
|
// Running, for this path or (then QueueCRC starts ours next lap) another one.
|
|
ParkingLotWait(&crcPending, [] { return retro_atomic_load_acquire_int(&crcPending) == 0; });
|
|
}
|
|
}
|
|
|
|
static uint32_t RetrieveCRCUnlessPowerSaving(const Path &gamePath) {
|
|
// It's okay to use it if we have it already.
|
|
if (Core_GetPowerSaving() && !HasCRC(gamePath)) {
|
|
return 0;
|
|
}
|
|
|
|
return RetrieveCRC(gamePath);
|
|
}
|
|
|
|
static void PurgeCRC() {
|
|
if (crcThread.joinable()) {
|
|
if (retro_atomic_load_acquire_int(&crcPending)) {
|
|
INFO_LOG(Log::System, "Cancelling CRC calculation");
|
|
retro_atomic_store_release_int(&crcCancel, 1);
|
|
ParkingLotWait(&crcPending, [] { return retro_atomic_load_acquire_int(&crcPending) == 0; });
|
|
}
|
|
CollectCRC();
|
|
} else {
|
|
DEBUG_LOG(Log::System, "No CRC pending");
|
|
}
|
|
}
|
|
|
|
void CancelCRC() {
|
|
PurgeCRC();
|
|
}
|
|
|
|
// Returns the full host
|
|
std::string ServerHost() {
|
|
if (g_Config.sReportHost.compare("default") == 0)
|
|
return "";
|
|
return g_Config.sReportHost;
|
|
}
|
|
|
|
// Returns the length of the hostname part (e.g. before the :80.)
|
|
static size_t ServerHostnameLength()
|
|
{
|
|
if (!IsEnabled())
|
|
return g_Config.sReportHost.npos;
|
|
|
|
// IPv6 literal?
|
|
std::string hostString = ServerHost();
|
|
if (hostString[0] == '[')
|
|
{
|
|
size_t length = hostString.find("]:");
|
|
if (length != hostString.npos)
|
|
++length;
|
|
return length;
|
|
}
|
|
else
|
|
return hostString.find(':');
|
|
}
|
|
|
|
// Returns only the hostname part (e.g. "report.ppsspp.org".)
|
|
static std::string ServerHostname() {
|
|
if (!IsEnabled())
|
|
return "";
|
|
|
|
std::string host = ServerHost();
|
|
size_t length = ServerHostnameLength();
|
|
|
|
// This means there's no port number - it's already the hostname.
|
|
if (length == host.npos)
|
|
return host;
|
|
else
|
|
return host.substr(0, length);
|
|
}
|
|
|
|
// Returns only the port part (e.g. 80) as an int.
|
|
static int ServerPort()
|
|
{
|
|
if (!IsEnabled())
|
|
return 0;
|
|
|
|
std::string host = ServerHost();
|
|
size_t offset = ServerHostnameLength();
|
|
// If there's no port, use the default one.
|
|
if (offset == host.npos)
|
|
return DEFAULT_PORT;
|
|
|
|
// Skip the colon.
|
|
std::string port = host.substr(offset + 1);
|
|
return atoi(port.c_str());
|
|
}
|
|
|
|
// Should only be called once per request.
|
|
bool CheckSpamLimited()
|
|
{
|
|
return (u32)retro_atomic_fetch_add_int(&spamProtectionCount, 1) + 1 >= SPAM_LIMIT;
|
|
}
|
|
|
|
static void SendReportRequest(const char *uri, const std::string &data, const std::string &mimeType, std::function<void(http::Request &)> callback) {
|
|
char url[1024];
|
|
std::string hostname = ServerHostname();
|
|
int port = ServerPort();
|
|
snprintf(url, sizeof(url), "http://%s:%d%s", hostname.c_str(), port, uri);
|
|
g_DownloadManager.AsyncPostWithCallback(url, data, mimeType, http::RequestFlags::Default, callback);
|
|
}
|
|
|
|
std::string StripTrailingNull(const std::string &str) {
|
|
size_t pos = str.find_first_of('\0');
|
|
if (pos != str.npos)
|
|
return str.substr(0, pos);
|
|
return str;
|
|
}
|
|
|
|
std::string GetPlatformIdentifer()
|
|
{
|
|
// TODO: Do we care about OS version?
|
|
#if defined(__ANDROID__)
|
|
return "Android";
|
|
#elif defined(_WIN64) && defined(_M_ARM64)
|
|
return "Windows ARM64";
|
|
#elif defined(_WIN64)
|
|
return "Windows 64";
|
|
#elif defined(_WIN32)
|
|
return "Windows";
|
|
#elif PPSSPP_PLATFORM(IOS)
|
|
return "iOS";
|
|
#elif defined(__APPLE__)
|
|
return "Mac";
|
|
#elif defined(LOONGSON)
|
|
return "Loongson";
|
|
#elif defined(__SWITCH__)
|
|
return "Switch";
|
|
#elif defined(__linux__)
|
|
return "Linux";
|
|
#elif defined(__Bitrig__)
|
|
return "Bitrig";
|
|
#elif defined(__DragonFly__)
|
|
return "DragonFly";
|
|
#elif defined(__FreeBSD__)
|
|
return "FreeBSD";
|
|
#elif defined(__FreeBSD_kernel__) && defined(__GLIBC__)
|
|
return "GNU/kFreeBSD";
|
|
#elif defined(__NetBSD__)
|
|
return "NetBSD";
|
|
#elif defined(__OpenBSD__)
|
|
return "OpenBSD";
|
|
#else
|
|
return "Unknown";
|
|
#endif
|
|
}
|
|
|
|
bool MessageAllowed();
|
|
void SendReportMessage(const char *message, const char *formatted);
|
|
|
|
void Init() {
|
|
// New game, clean slate.
|
|
retro_atomic_store_release_int(&spamProtectionCount, 0);
|
|
ResetCounts();
|
|
everUnsupported = false;
|
|
currentSupported = IsSupported();
|
|
Reporting::SetupCallbacks(&MessageAllowed, &SendReportMessage);
|
|
|
|
lastModuleName.clear();
|
|
lastModuleVersion = 0;
|
|
lastModuleCrc = 0;
|
|
}
|
|
|
|
void Shutdown() {
|
|
PurgeCRC();
|
|
|
|
// Just so it can be enabled in the menu again.
|
|
Init();
|
|
}
|
|
|
|
void DoState(PointerWrap &p) {
|
|
const int LATEST_VERSION = 1;
|
|
auto s = p.Section("Reporting", 0, LATEST_VERSION);
|
|
if (!s || s < LATEST_VERSION) {
|
|
// Don't report from old savestates, they may "entomb" bugs.
|
|
everUnsupported = true;
|
|
return;
|
|
}
|
|
|
|
bool reportingWasUnsupported = everUnsupported;
|
|
Do(p, everUnsupported);
|
|
if (!reportingWasUnsupported && everUnsupported) {
|
|
INFO_LOG(Log::System, "Reporting became unsupported due to loading an old savestate.");
|
|
}
|
|
}
|
|
|
|
void UpdateConfig() {
|
|
currentSupported = IsSupported();
|
|
if (!currentSupported && PSP_GetBootState() == BootState::Complete)
|
|
everUnsupported = true;
|
|
}
|
|
|
|
void NotifyDebugger() {
|
|
currentSupported = false;
|
|
everUnsupported = true;
|
|
}
|
|
|
|
void NotifyExecModule(const char *name, int ver, uint32_t crc) {
|
|
lastModuleName = name;
|
|
lastModuleVersion = ver;
|
|
lastModuleCrc = crc;
|
|
}
|
|
|
|
std::string CurrentGameID()
|
|
{
|
|
// TODO: Maybe ParamSFOData shouldn't include nulls in std::strings? Don't work to break savedata, though...
|
|
const std::string disc_id = StripTrailingNull(g_paramSFO.GetDiscID());
|
|
const std::string disc_version = StripTrailingNull(g_paramSFO.GetValueString("DISC_VERSION"));
|
|
return disc_id + "_" + disc_version;
|
|
}
|
|
|
|
void AddGameInfo(UrlEncoder &postdata)
|
|
{
|
|
postdata.Add("game", CurrentGameID());
|
|
postdata.Add("game_title", StripTrailingNull(g_paramSFO.GetValueString("TITLE")));
|
|
postdata.Add("sdkver", sceKernelGetCompiledSdkVersion());
|
|
postdata.Add("module_name", lastModuleName);
|
|
postdata.Add("module_ver", lastModuleVersion);
|
|
postdata.Add("module_crc", lastModuleCrc);
|
|
}
|
|
|
|
void AddSystemInfo(UrlEncoder &postdata)
|
|
{
|
|
std::string gpuPrimary, gpuFull;
|
|
if (gpu)
|
|
gpu->GetReportingInfo(gpuPrimary, gpuFull);
|
|
|
|
postdata.Add("version", PPSSPP_GIT_VERSION);
|
|
postdata.Add("gpu", gpuPrimary);
|
|
postdata.Add("gpu_full", gpuFull);
|
|
postdata.Add("cpu", cpu_info.Summarize());
|
|
postdata.Add("platform", GetPlatformIdentifer());
|
|
}
|
|
|
|
void AddConfigInfo(UrlEncoder &postdata)
|
|
{
|
|
postdata.Add("pixel_width", PSP_CoreParameter().pixelWidth);
|
|
postdata.Add("pixel_height", PSP_CoreParameter().pixelHeight);
|
|
|
|
g_Config.GetReportingInfo(postdata);
|
|
}
|
|
|
|
void AddGameplayInfo(UrlEncoder &postdata)
|
|
{
|
|
// Just to get an idea of how long they played.
|
|
if (PSP_GetBootState() == BootState::Complete)
|
|
postdata.Add("ticks", (const uint64_t)CoreTiming::GetTicks(currentMIPS));
|
|
|
|
float vps, fps;
|
|
__DisplayGetAveragedFPS(&vps, &fps);
|
|
postdata.Add("vps", vps);
|
|
postdata.Add("fps", fps);
|
|
|
|
postdata.Add("savestate_used", SaveState::HasLoadedState());
|
|
}
|
|
|
|
void AddScreenshotData(MultipartFormDataEncoder &postdata, const Path &filename, const std::vector<u8> &icon)
|
|
{
|
|
std::string data;
|
|
if (!filename.empty() && File::ReadBinaryFileToString(filename, &data)) {
|
|
postdata.Add("screenshot", data, "screenshot.jpg", "image/jpeg");
|
|
}
|
|
|
|
if (!icon.empty()) {
|
|
postdata.Add("icon", icon, "icon.png", "image/png");
|
|
}
|
|
}
|
|
|
|
int Process(const Payload &payload) {
|
|
Buffer output;
|
|
|
|
MultipartFormDataEncoder postdata;
|
|
AddSystemInfo(postdata);
|
|
AddGameInfo(postdata);
|
|
AddConfigInfo(postdata);
|
|
AddGameplayInfo(postdata);
|
|
|
|
switch (payload.type) {
|
|
case RequestType::MESSAGE:
|
|
// TODO: Add CRC?
|
|
postdata.Add("message", payload.string1);
|
|
postdata.Add("value", payload.string2);
|
|
// We tend to get corrupted data, this acts as a very primitive verification check.
|
|
postdata.Add("verify", payload.string1 + payload.string2);
|
|
|
|
postdata.Finish();
|
|
SendReportRequest("/report/message", postdata.ToString(), postdata.GetMimeType(), [=](http::Request &req) {
|
|
retro_atomic_store_release_int(&serverWorking, req.Failed() ? 0 : 1);
|
|
});
|
|
break;
|
|
|
|
case RequestType::COMPAT:
|
|
postdata.Add("compat", payload.string1);
|
|
// We tend to get corrupted data, this acts as a very primitive verification check.
|
|
postdata.Add("verify", payload.string1);
|
|
postdata.Add("graphics", StringFromFormat("%d", payload.int1));
|
|
postdata.Add("speed", StringFromFormat("%d", payload.int2));
|
|
postdata.Add("gameplay", StringFromFormat("%d", payload.int3));
|
|
postdata.Add("crc", StringFromFormat("%08x", RetrieveCRCUnlessPowerSaving(PSP_CoreParameter().fileToStart)));
|
|
postdata.Add("suggestions", payload.string1 != "perfect" && payload.string1 != "playable" ? "1" : "0");
|
|
AddScreenshotData(postdata, Path(payload.string2), payload.icon);
|
|
|
|
postdata.Finish();
|
|
retro_atomic_store_release_int(&serverWorking, 1);
|
|
SendReportRequest("/report/compat", postdata.ToString(), postdata.GetMimeType(), [=](http::Request &req) {
|
|
if (req.Failed()) {
|
|
retro_atomic_store_release_int(&serverWorking, 0);
|
|
return;
|
|
}
|
|
retro_atomic_store_release_int(&serverWorking, 1);
|
|
|
|
std::string result;
|
|
req.buffer().TakeAll(&result);
|
|
lastCompatResult.clear();
|
|
if (result.empty() || result[0] == '0')
|
|
retro_atomic_store_release_int(&serverWorking, 0);
|
|
else if (result[0] != '1')
|
|
SplitString(result, '\n', lastCompatResult);
|
|
});
|
|
break;
|
|
case RequestType::NONE:
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool IsSupported()
|
|
{
|
|
// Disabled when using certain hacks, because they make for poor reports.
|
|
if (CheatsInEffect() || HLEPlugins::HasEnabled())
|
|
return false;
|
|
if (GetLockedCPUSpeedMhz() != 0)
|
|
return false;
|
|
if (g_Config.uJitDisableFlags != 0)
|
|
return false;
|
|
// Don't allow builds without version info from git. They're useless for reporting.
|
|
if (strcmp(PPSSPP_GIT_VERSION, "unknown") == 0)
|
|
return false;
|
|
// Don't report from games without a version ID (i.e. random hashed homebrew IDs.)
|
|
// The problem is, these aren't useful because the hashes end up different for different people.
|
|
// TODO: Should really hash the ELF instead of the path, but then that affects savestates/cheats.
|
|
if (PSP_GetBootState() == BootState::Complete && g_paramSFO.GetValueString("DISC_VERSION").empty())
|
|
return false;
|
|
|
|
return !everUnsupported;
|
|
}
|
|
|
|
bool IsEnabled()
|
|
{
|
|
if (g_Config.sReportHost.empty() || (!currentSupported && PSP_GetBootState() == BootState::Complete))
|
|
return false;
|
|
// Disabled by default for now.
|
|
if (g_Config.sReportHost.compare("default") == 0)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool Enable(bool flag, const std::string &host)
|
|
{
|
|
if (IsSupported() && IsEnabled() != flag)
|
|
{
|
|
// "" means explicitly disabled. Don't ever turn on by default.
|
|
// "default" means it's okay to turn it on by default.
|
|
g_Config.sReportHost = flag ? host : "";
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void EnableDefault()
|
|
{
|
|
g_Config.sReportHost = "default";
|
|
}
|
|
|
|
ReportStatus GetStatus() {
|
|
if (!retro_atomic_load_acquire_int(&serverWorking))
|
|
return ReportStatus::FAILING;
|
|
return ReportStatus::WORKING;
|
|
}
|
|
|
|
bool MessageAllowed() {
|
|
if (!IsEnabled() || CheckSpamLimited())
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
void SendReportMessage(const char *message, const char *formatted) {
|
|
// MessageAllowed is checked first.
|
|
|
|
Payload payload{};
|
|
payload.type = RequestType::MESSAGE;
|
|
payload.string1 = message;
|
|
payload.string2 = formatted;
|
|
|
|
Process(payload);
|
|
}
|
|
|
|
// Emulation thread: pspFileSystem is read here.
|
|
void ReportCompatibility(const char *compat, int graphics, int speed, int gameplay, const std::string &screenshotFilename) {
|
|
if (!IsEnabled())
|
|
return;
|
|
Payload payload{};
|
|
if (pspFileSystem.ReadEntireFile("disc0:/PSP_GAME/ICON0.PNG", payload.icon) < 0) {
|
|
payload.icon.clear();
|
|
}
|
|
payload.type = RequestType::COMPAT;
|
|
payload.string1 = compat;
|
|
payload.string2 = screenshotFilename;
|
|
payload.int1 = graphics;
|
|
payload.int2 = speed;
|
|
payload.int3 = gameplay;
|
|
|
|
Process(payload);
|
|
}
|
|
|
|
std::vector<std::string> CompatibilitySuggestions() {
|
|
return lastCompatResult;
|
|
}
|
|
}
|