nn_olv: More work on post API

This commit is contained in:
Exzap 2023-07-27 21:04:42 +02:00
parent 67819a68d9
commit 0d96255bae
12 changed files with 655 additions and 207 deletions

View File

@ -422,6 +422,8 @@ add_library(CemuCafe
OS/libs/nn_olv/nn_olv_UploadFavoriteTypes.h
OS/libs/nn_olv/nn_olv_PostTypes.cpp
OS/libs/nn_olv/nn_olv_PostTypes.h
OS/libs/nn_olv/nn_olv_OfflineDB.cpp
OS/libs/nn_olv/nn_olv_OfflineDB.h
OS/libs/nn_pdm/nn_pdm.cpp
OS/libs/nn_pdm/nn_pdm.h
OS/libs/nn_save/nn_save.cpp

View File

@ -546,6 +546,7 @@ void coreinitExport_UCReadSysConfig(PPCInterpreter_t* hCPU)
{
// get parental online control for online features
// note: This option is account-bound, the p_acct1 prefix indicates that the account in slot 1 is used
// a non-zero value means network access is restricted through parental access. 0 means allowed
// account in slot 1
if (ucParam->resultPtr != _swapEndianU32(MPTR_NULL))
memory_writeU8(_swapEndianU32(ucParam->resultPtr), 0); // data type is guessed
@ -561,7 +562,7 @@ void coreinitExport_UCReadSysConfig(PPCInterpreter_t* hCPU)
{
// miiverse restrictions
if (ucParam->resultPtr != _swapEndianU32(MPTR_NULL))
memory_writeU8(_swapEndianU32(ucParam->resultPtr), 0); // data type is guessed (0 -> no restrictions, 1 -> read only?, 2 -> no access?)
memory_writeU8(_swapEndianU32(ucParam->resultPtr), 0); // data type is guessed (0 -> no restrictions, 1 -> read only, 2 -> no access)
}
else if (_strcmpi(ucParam->settingName, "s_acct01.uuid") == 0)
{

View File

@ -5,6 +5,7 @@
#include "nn_olv_DownloadCommunityTypes.h"
#include "nn_olv_UploadFavoriteTypes.h"
#include "nn_olv_PostTypes.h"
#include "nn_olv_OfflineDB.h"
#include "Cafe/OS/libs/proc_ui/proc_ui.h"
#include "Cafe/OS/libs/coreinit/coreinit_Time.h"
@ -13,179 +14,6 @@ namespace nn
{
namespace olv
{
struct DownloadedPostData_t
{
/* +0x0000 */ uint32be flags;
/* +0x0004 */ uint32be userPrincipalId;
/* +0x0008 */ char postId[0x20]; // size guessed
/* +0x0028 */ uint64 postDate;
/* +0x0030 */ uint8 feeling;
/* +0x0031 */ uint8 padding0031[3];
/* +0x0034 */ uint32be regionId;
/* +0x0038 */ uint8 platformId;
/* +0x0039 */ uint8 languageId;
/* +0x003A */ uint8 countryId;
/* +0x003B */ uint8 padding003B[1];
/* +0x003C */ uint16be bodyText[0x100]; // actual size is unknown
/* +0x023C */ uint32be bodyTextLength;
/* +0x0240 */ uint8 compressedMemoBody[0xA000]; // 40KB
/* +0xA240 */ uint32be compressedMemoBodyRelated; // size of compressed data?
/* +0xA244 */ uint16be topicTag[0x98];
// app data
/* +0xA374 */ uint8 appData[0x400];
/* +0xA774 */ uint32be appDataLength;
// external binary
/* +0xA778 */ uint8 externalBinaryUrl[0x100];
/* +0xA878 */ uint32be externalBinaryDataSize;
// external image
/* +0xA87C */ uint8 externalImageDataUrl[0x100];
/* +0xA97C */ uint32be externalImageDataSize;
// external url ?
/* +0xA980 */ char externalUrl[0x100];
// mii
/* +0xAA80 */ uint8 miiData[0x60];
/* +0xAAE0 */ uint16be miiNickname[0x20];
/* +0xAB20 */ uint8 unusedAB20[0x14E0];
// everything above is part of DownloadedDataBase
// everything below is part of DownloadedPostData
/* +0xC000 */ uint8 uknDataC000[8]; // ??
/* +0xC008 */ uint32be communityId;
/* +0xC00C */ uint32be empathyCount;
/* +0xC010 */ uint32be commentCount;
/* +0xC014 */ uint8 unused[0x1F4];
}; // size: 0xC208
static_assert(sizeof(DownloadedPostData_t) == 0xC208, "");
static_assert(offsetof(DownloadedPostData_t, postDate) == 0x0028, "");
static_assert(offsetof(DownloadedPostData_t, platformId) == 0x0038, "");
static_assert(offsetof(DownloadedPostData_t, bodyText) == 0x003C, "");
static_assert(offsetof(DownloadedPostData_t, compressedMemoBody) == 0x0240, "");
static_assert(offsetof(DownloadedPostData_t, topicTag) == 0xA244, "");
static_assert(offsetof(DownloadedPostData_t, appData) == 0xA374, "");
static_assert(offsetof(DownloadedPostData_t, externalBinaryUrl) == 0xA778, "");
static_assert(offsetof(DownloadedPostData_t, externalImageDataUrl) == 0xA87C, "");
static_assert(offsetof(DownloadedPostData_t, externalUrl) == 0xA980, "");
static_assert(offsetof(DownloadedPostData_t, miiData) == 0xAA80, "");
static_assert(offsetof(DownloadedPostData_t, miiNickname) == 0xAAE0, "");
static_assert(offsetof(DownloadedPostData_t, unusedAB20) == 0xAB20, "");
static_assert(offsetof(DownloadedPostData_t, communityId) == 0xC008, "");
static_assert(offsetof(DownloadedPostData_t, empathyCount) == 0xC00C, "");
static_assert(offsetof(DownloadedPostData_t, commentCount) == 0xC010, "");
const int POST_DATA_FLAG_HAS_BODY_TEXT = (0x0001);
const int POST_DATA_FLAG_HAS_BODY_MEMO = (0x0002);
void export_DownloadPostDataList(PPCInterpreter_t* hCPU)
{
ppcDefineParamTypePtr(downloadedTopicData, void, 0); // DownloadedTopicData
ppcDefineParamTypePtr(downloadedPostData, DownloadedPostData_t, 1); // DownloadedPostData
ppcDefineParamTypePtr(downloadedPostDataSize, uint32be, 2);
ppcDefineParamS32(maxCount, 3);
ppcDefineParamTypePtr(listParam, void, 4); // DownloadPostDataListParam
maxCount = 0; // DISABLED
// just some test
for (sint32 i = 0; i < maxCount; i++)
{
DownloadedPostData_t* postData = downloadedPostData + i;
memset(postData, 0, sizeof(DownloadedPostData_t));
postData->userPrincipalId = 0x1000 + i;
// post id
sprintf(postData->postId, "postid-%04x", i+(GetTickCount()%10000));
postData->bodyTextLength = 12;
postData->bodyText[0] = 'H';
postData->bodyText[1] = 'e';
postData->bodyText[2] = 'l';
postData->bodyText[3] = 'l';
postData->bodyText[4] = 'o';
postData->bodyText[5] = ' ';
postData->bodyText[6] = 'w';
postData->bodyText[7] = 'o';
postData->bodyText[8] = 'r';
postData->bodyText[9] = 'l';
postData->bodyText[10] = 'd';
postData->bodyText[11] = '!';
postData->miiNickname[0] = 'C';
postData->miiNickname[1] = 'e';
postData->miiNickname[2] = 'm';
postData->miiNickname[3] = 'u';
postData->miiNickname[4] = '-';
postData->miiNickname[5] = 'M';
postData->miiNickname[6] = 'i';
postData->miiNickname[7] = 'i';
postData->topicTag[0] = 't';
postData->topicTag[1] = 'o';
postData->topicTag[2] = 'p';
postData->topicTag[3] = 'i';
postData->topicTag[4] = 'c';
postData->flags = POST_DATA_FLAG_HAS_BODY_TEXT;
}
*downloadedPostDataSize = maxCount;
osLib_returnFromFunction(hCPU, 0);
}
void exportDownloadPostData_TestFlags(PPCInterpreter_t* hCPU)
{
ppcDefineParamTypePtr(downloadedPostData, DownloadedPostData_t, 0);
ppcDefineParamU32(testFlags, 1);
if (((uint32)downloadedPostData->flags) & testFlags)
osLib_returnFromFunction(hCPU, 1);
else
osLib_returnFromFunction(hCPU, 0);
}
void exportDownloadPostData_GetPostId(PPCInterpreter_t* hCPU)
{
ppcDefineParamTypePtr(downloadedPostData, DownloadedPostData_t, 0);
osLib_returnFromFunction(hCPU, memory_getVirtualOffsetFromPointer(downloadedPostData->postId));
}
void exportDownloadPostData_GetMiiNickname(PPCInterpreter_t* hCPU)
{
ppcDefineParamTypePtr(downloadedPostData, DownloadedPostData_t, 0);
if(downloadedPostData->miiNickname[0] == 0 )
osLib_returnFromFunction(hCPU, MPTR_NULL);
else
osLib_returnFromFunction(hCPU, memory_getVirtualOffsetFromPointer(downloadedPostData->miiNickname));
}
void exportDownloadPostData_GetTopicTag(PPCInterpreter_t* hCPU)
{
ppcDefineParamTypePtr(downloadedPostData, DownloadedPostData_t, 0);
osLib_returnFromFunction(hCPU, memory_getVirtualOffsetFromPointer(downloadedPostData->topicTag));
}
void exportDownloadPostData_GetBodyText(PPCInterpreter_t* hCPU)
{
ppcDefineParamTypePtr(downloadedPostData, DownloadedPostData_t, 0);
ppcDefineParamWStrBE(strOut, 1);
ppcDefineParamS32(maxLength, 2);
if (((uint32)downloadedPostData->flags&POST_DATA_FLAG_HAS_BODY_TEXT) == 0)
{
osLib_returnFromFunction(hCPU, 0xC1106800);
return;
}
memset(strOut, 0, sizeof(uint16be)*maxLength);
sint32 copyLen = std::min(maxLength - 1, (sint32)downloadedPostData->bodyTextLength);
for (sint32 i = 0; i < copyLen; i++)
{
strOut[i] = downloadedPostData->bodyText[i];
}
strOut[copyLen] = '\0';
osLib_returnFromFunction(hCPU, 0);
}
struct PortalAppParam_t
{
/* +0x1A663B */ char serviceToken[32]; // size is unknown
@ -284,6 +112,10 @@ namespace nn
void load()
{
g_ReportTypes = 0;
g_IsOnlineMode = false;
g_IsInitialized = false;
g_IsOfflineDBMode = false;
loadOliveInitializeTypes();
loadOliveUploadCommunityTypes();
@ -293,13 +125,6 @@ namespace nn
cafeExportRegisterFunc(GetErrorCode, "nn_olv", "GetErrorCode__Q2_2nn3olvFRCQ2_2nn6Result", LogType::None);
osLib_addFunction("nn_olv", "DownloadPostDataList__Q2_2nn3olvFPQ3_2nn3olv19DownloadedTopicDataPQ3_2nn3olv18DownloadedPostDataPUiUiPCQ3_2nn3olv25DownloadPostDataListParam", export_DownloadPostDataList);
// osLib_addFunction("nn_olv", "TestFlags__Q3_2nn3olv18DownloadedDataBaseCFUi", exportDownloadPostData_TestFlags);
// osLib_addFunction("nn_olv", "GetPostId__Q3_2nn3olv18DownloadedDataBaseCFv", exportDownloadPostData_GetPostId);
// osLib_addFunction("nn_olv", "GetMiiNickname__Q3_2nn3olv18DownloadedDataBaseCFv", exportDownloadPostData_GetMiiNickname);
// osLib_addFunction("nn_olv", "GetTopicTag__Q3_2nn3olv18DownloadedDataBaseCFv", exportDownloadPostData_GetTopicTag);
// osLib_addFunction("nn_olv", "GetBodyText__Q3_2nn3olv18DownloadedDataBaseCFPwUi", exportDownloadPostData_GetBodyText);
osLib_addFunction("nn_olv", "GetServiceToken__Q4_2nn3olv6hidden14PortalAppParamCFv", exportPortalAppParam_GetServiceToken);
cafeExportRegisterFunc(StubPostApp, "nn_olv", "UploadPostDataByPostApp__Q2_2nn3olvFPCQ3_2nn3olv28UploadPostDataByPostAppParam", LogType::Force);
@ -314,5 +139,10 @@ namespace nn
cafeExportRegisterFunc(UploadedPostData_GetPostId, "nn_olv", "GetPostId__Q3_2nn3olv16UploadedPostDataCFv", LogType::Force);
}
void unload() // not called yet
{
OfflineDB_Shutdown();
}
}
}

View File

@ -19,5 +19,6 @@ namespace nn
sint32 GetOlvAccessKey(uint32_t* pOutKey);
void load();
void unload();
}
}

View File

@ -69,6 +69,7 @@ namespace nn
extern uint32_t g_ReportTypes;
extern bool g_IsInitialized;
extern bool g_IsOnlineMode;
extern bool g_IsOfflineDBMode; // use offline cache for posts
static void InitializeOliveRequest(CurlRequestHelper& req)
{
@ -175,5 +176,39 @@ namespace nn
bool FormatCommunityCode(char* pOutCode, uint32* outLen, uint32 communityId);
sint32 olv_curlformcode_to_error(CURLFORMcode code);
// convert and copy utf8 string into UC2 big-endian array
template<size_t TLength>
uint32 SetStringUC2(uint16be(&str)[TLength], std::string_view sv, bool unescape = false)
{
if(unescape)
{
// todo
}
std::wstring ws = boost::nowide::widen(sv);
size_t copyLen = std::min<size_t>(TLength-1, ws.size());
for(size_t i=0; i<copyLen; i++)
str[i] = ws[i];
str[copyLen] = '\0';
return copyLen;
}
// safely copy null-terminated UC2 big-endian string into UC2 big-endian array
template<size_t TLength>
uint32 SetStringUC2(uint16be(&str)[TLength], const uint16be* strIn)
{
size_t copyLen = TLength-1;
for(size_t i=0; i<copyLen; i++)
{
if(strIn[i] == 0)
{
str[i] = 0;
return i;
}
str[i] = strIn[i];
}
str[copyLen] = '\0';
return copyLen;
}
}
}

View File

@ -9,10 +9,10 @@ namespace nn
{
namespace olv
{
uint32_t g_ReportTypes = 0;
bool g_IsOnlineMode = false;
bool g_IsInitialized = false;
bool g_IsOfflineDBMode = false;
ParamPackStorage g_ParamPack;
DiscoveryResultStorage g_DiscoveryResults;
@ -241,9 +241,15 @@ namespace nn
g_IsInitialized = true;
if(ActiveSettings::GetNetworkService() == NetworkService::Nintendo)
{
// since the official Miiverse was shut down, use local post archive instead
g_IsOnlineMode = true;
g_IsOfflineDBMode = true;
return OLV_RESULT_SUCCESS;
}
if ((pParam->m_Flags & InitializeParam::FLAG_OFFLINE_MODE) == 0)
{
g_IsOnlineMode = true;
independentServiceToken_t token;

View File

@ -0,0 +1,215 @@
#include "nn_olv_Common.h"
#include "nn_olv_PostTypes.h"
#include "nn_olv_OfflineDB.h"
#include "Cemu/ncrypto/ncrypto.h" // for base64 encoder/decoder
#include "util/helpers/helpers.h"
#include "Config/ActiveSettings.h"
#include "Cafe/CafeSystem.h"
#include <pugixml.hpp>
#include <zlib.h>
#include <zarchive/zarchivereader.h>
namespace nn
{
namespace olv
{
std::mutex g_offlineDBMutex;
bool g_offlineDBInitialized = false;
ZArchiveReader* g_offlineDBArchive{nullptr};
void OfflineDB_LazyInit()
{
std::scoped_lock _l(g_offlineDBMutex);
if(g_offlineDBInitialized)
return;
// open archive
g_offlineDBArchive = ZArchiveReader::OpenFromFile(ActiveSettings::GetUserDataPath("resources/miiverse/OfflineDB.zar"));
if(!g_offlineDBArchive)
cemuLog_log(LogType::Force, "Failed to open resources/miiverse/OfflineDB.zar. Miiverse posts will not be available");
g_offlineDBInitialized = true;
}
void OfflineDB_Shutdown()
{
std::scoped_lock _l(g_offlineDBMutex);
if(!g_offlineDBInitialized)
return;
delete g_offlineDBArchive;
g_offlineDBInitialized = false;
}
bool CheckForOfflineDBFile(const char* filePath, uint32* fileSize)
{
if(!g_offlineDBArchive)
return false;
ZArchiveNodeHandle fileHandle = g_offlineDBArchive->LookUp(filePath);
if (!g_offlineDBArchive->IsFile(fileHandle))
return false;
if(fileSize)
*fileSize = g_offlineDBArchive->GetFileSize(fileHandle);
return true;
}
bool LoadOfflineDBFile(const char* filePath, std::vector<uint8>& fileData)
{
fileData.clear();
if(!g_offlineDBArchive)
return false;
ZArchiveNodeHandle fileHandle = g_offlineDBArchive->LookUp(filePath);
if (!g_offlineDBArchive->IsFile(fileHandle))
return false;
fileData.resize(g_offlineDBArchive->GetFileSize(fileHandle));
g_offlineDBArchive->ReadFromFile(fileHandle, 0, fileData.size(), fileData.data());
return true;
}
void TryLoadCompressedMemoImage(DownloadedPostData& downloadedPostData)
{
const unsigned char tgaHeader_320x120_32BPP[] = {0x0,0x0,0x2,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x40,0x1,0x78,0x0,0x20,0x8};
std::string memoImageFilename = fmt::format("memo/{}", (char*)downloadedPostData.downloadedDataBase.postId);
std::vector<uint8> bitmaskCompressedImg;
if (!LoadOfflineDBFile(memoImageFilename.c_str(), bitmaskCompressedImg))
return;
if (bitmaskCompressedImg.size() != (320*120)/8)
return;
std::vector<uint8> decompressedImage;
decompressedImage.resize(sizeof(tgaHeader_320x120_32BPP) + 320 * 120 * 4);
memcpy(decompressedImage.data(), tgaHeader_320x120_32BPP, sizeof(tgaHeader_320x120_32BPP));
uint8* pOut = decompressedImage.data() + sizeof(tgaHeader_320x120_32BPP);
for(int i=0; i<320*120; i++)
{
bool isWhite = (bitmaskCompressedImg[i/8] & (1 << (i%8))) != 0;
if(isWhite)
{
pOut[0] = pOut[1] = pOut[2] = pOut[3] = 0xFF;
}
else
{
pOut[0] = pOut[1] = pOut[2] = 0;
pOut[3] = 0xFF;
}
pOut += 4;
}
// store compressed image
uLongf compressedDestLen = 40960;
int r = compress((uint8*)downloadedPostData.downloadedDataBase.compressedMemoBody, &compressedDestLen, decompressedImage.data(), decompressedImage.size());
if( r != Z_OK)
return;
downloadedPostData.downloadedDataBase.compressedMemoBodySize = compressedDestLen;
downloadedPostData.downloadedDataBase.SetFlag(DownloadedDataBase::FLAGS::HAS_BODY_MEMO);
}
void CheckForExternalImage(DownloadedPostData& downloadedPostData)
{
std::string externalImageFilename = fmt::format("image/{}.jpg", (char*)downloadedPostData.downloadedDataBase.postId);
uint32 fileSize;
if (!CheckForOfflineDBFile(externalImageFilename.c_str(), &fileSize))
return;
strcpy((char*)downloadedPostData.downloadedDataBase.externalImageDataUrl, externalImageFilename.c_str());
downloadedPostData.downloadedDataBase.SetFlag(DownloadedDataBase::FLAGS::HAS_EXTERNAL_IMAGE);
downloadedPostData.downloadedDataBase.externalImageDataSize = fileSize;
}
nnResult _Async_OfflineDB_DownloadPostDataListParam_DownloadPostDataList(coreinit::OSEvent* event, DownloadedTopicData* downloadedTopicData, DownloadedPostData* downloadedPostData, uint32be* postCountOut, uint32 maxCount, DownloadPostDataListParam* param)
{
scope_exit _se([&](){coreinit::OSSignalEvent(event);});
uint64 titleId = CafeSystem::GetForegroundTitleId();
memset(downloadedTopicData, 0, sizeof(DownloadedTopicData));
memset(downloadedPostData, 0, sizeof(DownloadedPostData) * maxCount);
*postCountOut = 0;
const char* postXmlFilename = nullptr;
if(titleId == 0x0005000010143400 || titleId == 0x0005000010143500 || titleId == 0x0005000010143600)
postXmlFilename = "PostList_WindWakerHD.xml";
if (!postXmlFilename)
return OLV_RESULT_SUCCESS;
// load post XML
std::vector<uint8> xmlData;
if (!LoadOfflineDBFile(postXmlFilename, xmlData))
return OLV_RESULT_SUCCESS;
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_buffer(xmlData.data(), xmlData.size());
if (!result)
return OLV_RESULT_SUCCESS;
// collect list of all post xml nodes
std::vector<pugi::xml_node> postXmlNodes;
for (pugi::xml_node postNode = doc.child("posts").child("post"); postNode; postNode = postNode.next_sibling("post"))
postXmlNodes.push_back(postNode);
// randomly select up to maxCount posts
srand(GetTickCount());
uint32 postCount = 0;
while(!postXmlNodes.empty() && postCount < maxCount)
{
uint32 index = rand() % postXmlNodes.size();
pugi::xml_node& postNode = postXmlNodes[index];
auto& addedPost = downloadedPostData[postCount];
memset(&addedPost, 0, sizeof(DownloadedPostData));
if (!ParseXML_DownloadedPostData(addedPost, postNode) )
continue;
TryLoadCompressedMemoImage(addedPost);
CheckForExternalImage(addedPost);
postCount++;
// remove from post list
postXmlNodes[index] = postXmlNodes.back();
postXmlNodes.pop_back();
}
*postCountOut = postCount;
return OLV_RESULT_SUCCESS;
}
nnResult OfflineDB_DownloadPostDataListParam_DownloadPostDataList(DownloadedTopicData* downloadedTopicData, DownloadedPostData* downloadedPostData, uint32be* postCountOut, uint32 maxCount, DownloadPostDataListParam* param)
{
OfflineDB_LazyInit();
memset(downloadedTopicData, 0, sizeof(DownloadedTopicData));
downloadedTopicData->communityId = param->communityId;
*postCountOut = 0;
if(param->_HasFlag(DownloadPostDataListParam::FLAGS::SELF_ONLY))
return OLV_RESULT_SUCCESS; // the offlineDB doesn't contain any self posts
StackAllocator<coreinit::OSEvent> doneEvent;
coreinit::OSInitEvent(doneEvent, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_MANUAL);
auto asyncTask = std::async(std::launch::async, _Async_OfflineDB_DownloadPostDataListParam_DownloadPostDataList, doneEvent.GetPointer(), downloadedTopicData, downloadedPostData, postCountOut, maxCount, param);
coreinit::OSWaitEvent(doneEvent);
nnResult r = asyncTask.get();
return r;
}
nnResult _Async_OfflineDB_DownloadPostDataListParam_DownloadExternalImageData(coreinit::OSEvent* event, DownloadedDataBase* _this, void* imageDataOut, uint32be* imageSizeOut, uint32 maxSize)
{
scope_exit _se([&](){coreinit::OSSignalEvent(event);});
if (!_this->TestFlags(_this, DownloadedDataBase::FLAGS::HAS_EXTERNAL_IMAGE))
return OLV_RESULT_MISSING_DATA;
// not all games may use JPEG files?
std::string externalImageFilename = fmt::format("image/{}.jpg", (char*)_this->postId);
std::vector<uint8> jpegData;
if (!LoadOfflineDBFile(externalImageFilename.c_str(), jpegData))
return OLV_RESULT_FAILED_REQUEST;
memcpy(imageDataOut, jpegData.data(), jpegData.size());
*imageSizeOut = jpegData.size();
return OLV_RESULT_SUCCESS;
}
nnResult OfflineDB_DownloadPostDataListParam_DownloadExternalImageData(DownloadedDataBase* _this, void* imageDataOut, uint32be* imageSizeOut, uint32 maxSize)
{
StackAllocator<coreinit::OSEvent> doneEvent;
coreinit::OSInitEvent(doneEvent, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_MANUAL);
auto asyncTask = std::async(std::launch::async, _Async_OfflineDB_DownloadPostDataListParam_DownloadExternalImageData, doneEvent.GetPointer(), _this, imageDataOut, imageSizeOut, maxSize);
coreinit::OSWaitEvent(doneEvent);
nnResult r = asyncTask.get();
return r;
}
}
}

View File

@ -0,0 +1,16 @@
#pragma once
#include "Cafe/OS/libs/nn_common.h"
#include "nn_olv_Common.h"
namespace nn
{
namespace olv
{
void OfflineDB_Init();
void OfflineDB_Shutdown();
nnResult OfflineDB_DownloadPostDataListParam_DownloadPostDataList(DownloadedTopicData* downloadedTopicData, DownloadedPostData* downloadedPostData, uint32be* postCountOut, uint32 maxCount, DownloadPostDataListParam* param);
nnResult OfflineDB_DownloadPostDataListParam_DownloadExternalImageData(DownloadedDataBase* _this, void* imageDataOut, uint32be* imageSizeOut, uint32 maxSize);
}
}

View File

@ -1,5 +1,6 @@
#include "Cafe/OS/libs/nn_olv/nn_olv_Common.h"
#include "nn_olv_PostTypes.h"
#include "nn_olv_OfflineDB.h"
#include "Cemu/ncrypto/ncrypto.h" // for base64 decoder
#include "util/helpers/helpers.h"
#include <pugixml.hpp>
@ -9,41 +10,28 @@ namespace nn
{
namespace olv
{
template<size_t TLength>
uint32 SetStringUC2(uint16be(&str)[TLength], std::string_view sv, bool unescape = false)
{
if(unescape)
{
// todo
}
std::wstring ws = boost::nowide::widen(sv);
size_t copyLen = std::min<size_t>(TLength-1, ws.size());
for(size_t i=0; i<copyLen; i++)
str[i] = ws[i];
str[copyLen] = '\0';
return copyLen;
}
bool ParseXml_DownloadedDataBase(DownloadedDataBase& obj, pugi::xml_node& xmlNode)
{
// todo:
// app_data, body, painting, name
// painting with url?
pugi::xml_node tokenNode;
if(tokenNode = xmlNode.child("body"); tokenNode)
{
//cemu_assert_unimplemented();
obj.bodyTextLength = SetStringUC2(obj.bodyText, tokenNode.child_value(), true);
if(obj.bodyTextLength > 0)
obj.SetFlag(DownloadedDataBase::FLAGS::HAS_BODY_TEXT);
}
if(tokenNode = xmlNode.child("topic_tag"); tokenNode)
{
SetStringUC2(obj.topicTag, tokenNode.child_value(), true);
}
if(tokenNode = xmlNode.child("feeling_id"); tokenNode)
{
obj.feeling = ConvertString<sint8>(tokenNode.child_value());
if(obj.feeling < 0 || obj.feeling >= 5)
{
cemuLog_log(LogType::Force, "DownloadedDataBase::ParseXml: feeling_id out of range");
cemuLog_log(LogType::Force, "[Olive-XML] DownloadedDataBase::ParseXml: feeling_id out of range");
return false;
}
}
@ -52,7 +40,7 @@ namespace nn
std::string_view id_sv = tokenNode.child_value();
if(id_sv.size() > 22)
{
cemuLog_log(LogType::Force, "DownloadedDataBase::ParseXml: id too long");
cemuLog_log(LogType::Force, "[Olive-XML] DownloadedDataBase::ParseXml: id too long");
return false;
}
memcpy(obj.postId, id_sv.data(), id_sv.size());
@ -67,7 +55,7 @@ namespace nn
obj.SetFlag(DownloadedDataBase::FLAGS::IS_NOT_AUTOPOST);
else
{
cemuLog_log(LogType::Force, "DownloadedDataBase::ParseXml: is_autopost has invalid value");
cemuLog_log(LogType::Force, "[Olive-XML] DownloadedDataBase::ParseXml: is_autopost has invalid value");
return false;
}
}
@ -116,6 +104,36 @@ namespace nn
{
obj.countryId = ConvertString<uint8>(tokenNode.child_value());
}
if(tokenNode = xmlNode.child("painting"); tokenNode)
{
if(pugi::xml_node subNode = tokenNode.child("content"); subNode)
{
std::vector<uint8> paintingData = NCrypto::base64Decode(subNode.child_value());
if (paintingData.size() > 0xA000)
{
cemuLog_log(LogType::Force, "[Olive-XML] DownloadedDataBase painting content is too large");
return false;
}
memcpy(obj.compressedMemoBody, paintingData.data(), paintingData.size());
obj.SetFlag(DownloadedDataBase::FLAGS::HAS_BODY_MEMO);
}
if(pugi::xml_node subNode = tokenNode.child("size"); subNode)
{
obj.compressedMemoBodySize = ConvertString<uint32>(subNode.child_value());
}
}
if(tokenNode = xmlNode.child("app_data"); tokenNode)
{
std::vector<uint8> appData = NCrypto::base64Decode(tokenNode.child_value());
if (appData.size() > 0x400)
{
cemuLog_log(LogType::Force, "[Olive-XML] DownloadedDataBase AppData is too large");
return false;
}
memcpy(obj.appData, appData.data(), appData.size());
obj.appDataLength = appData.size();
obj.SetFlag(DownloadedDataBase::FLAGS::HAS_APP_DATA);
}
return true;
}
@ -256,6 +274,121 @@ namespace nn
return 0;
}
nnResult DownloadedDataBase::DownloadExternalImageData(DownloadedDataBase* _this, void* imageDataOut, uint32be* imageSizeOut, uint32 maxSize)
{
if(g_IsOfflineDBMode)
return OfflineDB_DownloadPostDataListParam_DownloadExternalImageData(_this, imageDataOut, imageSizeOut, maxSize);
if(!g_IsOnlineMode)
return OLV_RESULT_OFFLINE_MODE_REQUEST;
if (!TestFlags(_this, FLAGS::HAS_EXTERNAL_IMAGE))
return OLV_RESULT_MISSING_DATA;
cemuLog_logDebug(LogType::Force, "DownloadedDataBase::DownloadExternalImageData not implemented");
return OLV_RESULT_FAILED_REQUEST; // placeholder error
}
nnResult DownloadPostDataListParam::GetRawDataUrl(DownloadPostDataListParam* _this, char* urlOut, uint32 urlMaxSize)
{
if(!g_IsOnlineMode)
return OLV_RESULT_OFFLINE_MODE_REQUEST;
//if(_this->communityId == 0)
// cemuLog_log(LogType::Force, "DownloadPostDataListParam::GetRawDataUrl called with invalid communityId");
// get base url
std::string baseUrl;
baseUrl.append(g_DiscoveryResults.apiEndpoint);
//baseUrl.append(fmt::format("/v1/communities/{}/posts", (uint32)_this->communityId));
cemu_assert_debug(_this->communityId == 0);
baseUrl.append(fmt::format("/v1/posts.search", (uint32)_this->communityId));
// "v1/posts.search"
// build parameter string
std::string params;
// this function behaves differently for the Wii U menu? Where it can lookup posts by titleId?
if(_this->titleId != 0)
{
cemu_assert_unimplemented(); // Wii U menu mode
}
// todo: Generic parameters. Which includes: language_id, limit, type=text/memo
// handle postIds
for(size_t i=0; i<_this->MAX_NUM_POST_ID; i++)
{
if(_this->searchPostId[i].str[0] == '\0')
continue;
cemu_assert_unimplemented(); // todo
// todo - postId parameter
// handle filters
if(_this->_HasFlag(DownloadPostDataListParam::FLAGS::WITH_MII))
params.append("&with_mii=1");
if(_this->_HasFlag(DownloadPostDataListParam::FLAGS::WITH_EMPATHY))
params.append("&with_empathy_added=1");
if(_this->bodyTextMaxLength != 0)
params.append(fmt::format("&max_body_length={}", _this->bodyTextMaxLength));
}
if(_this->titleId != 0)
params.append(fmt::format("&title_id={}", (uint64)_this->titleId));
if (_this->_HasFlag(DownloadPostDataListParam::FLAGS::FRIENDS_ONLY))
params.append("&by=friend");
if (_this->_HasFlag(DownloadPostDataListParam::FLAGS::FOLLOWERS_ONLY))
params.append("&by=followings");
if (_this->_HasFlag(DownloadPostDataListParam::FLAGS::SELF_ONLY))
params.append("&by=self");
if(!params.empty())
params[0] = '?'; // replace the leading ampersand
baseUrl.append(params);
if(baseUrl.size()+1 > urlMaxSize)
return OLV_RESULT_NOT_ENOUGH_SIZE;
strncpy(urlOut, baseUrl.c_str(), urlMaxSize);
return OLV_RESULT_SUCCESS;
}
nnResult DownloadPostDataList(DownloadedTopicData* downloadedTopicData, DownloadedPostData* downloadedPostData, uint32be* postCountOut, uint32 maxCount, DownloadPostDataListParam* param)
{
if(g_IsOfflineDBMode)
return OfflineDB_DownloadPostDataListParam_DownloadPostDataList(downloadedTopicData, downloadedPostData, postCountOut, maxCount, param);
memset(downloadedTopicData, 0, sizeof(DownloadedTopicData));
downloadedTopicData->communityId = param->communityId;
*postCountOut = 0;
char urlBuffer[2048];
if (NN_RESULT_IS_FAILURE(DownloadPostDataListParam::GetRawDataUrl(param, urlBuffer, sizeof(urlBuffer))))
return OLV_RESULT_INVALID_PARAMETER;
/*
CurlRequestHelper req;
req.initate(urlBuffer, CurlRequestHelper::SERVER_SSL_CONTEXT::OLIVE);
InitializeOliveRequest(req);
bool reqResult = req.submitRequest();
if (!reqResult)
{
long httpCode = 0;
curl_easy_getinfo(req.getCURL(), CURLINFO_RESPONSE_CODE, &httpCode);
cemuLog_log(LogType::Force, "Failed request: {} ({})", urlBuffer, httpCode);
if (!(httpCode >= 400))
return OLV_RESULT_FAILED_REQUEST;
}
pugi::xml_document doc;
if (!doc.load_buffer(req.getReceivedData().data(), req.getReceivedData().size()))
{
cemuLog_log(LogType::Force, fmt::format("Invalid XML in community download response"));
return OLV_RESULT_INVALID_XML;
}
*/
*postCountOut = 0;
return OLV_RESULT_SUCCESS;
}
void loadOlivePostAndTopicTypes()
{
cafeExportRegisterFunc(GetSystemTopicDataListFromRawData, "nn_olv", "GetSystemTopicDataListFromRawData__Q3_2nn3olv6hiddenFPQ4_2nn3olv6hidden29DownloadedSystemTopicDataListPQ4_2nn3olv6hidden24DownloadedSystemPostDataPUiUiPCUcT4", LogType::None);
@ -279,6 +412,8 @@ namespace nn
cafeExportRegisterFunc(DownloadedDataBase::GetAppDataSize, "nn_olv", "GetAppDataSize__Q3_2nn3olv18DownloadedDataBaseCFv", LogType::None);
cafeExportRegisterFunc(DownloadedDataBase::GetPostId, "nn_olv", "GetPostId__Q3_2nn3olv18DownloadedDataBaseCFv", LogType::None);
cafeExportRegisterFunc(DownloadedDataBase::GetMiiData2, "nn_olv", "GetMiiData__Q3_2nn3olv18DownloadedDataBaseCFv", LogType::None);
cafeExportRegisterFunc(DownloadedDataBase::DownloadExternalImageData, "nn_olv", "DownloadExternalImageData__Q3_2nn3olv18DownloadedDataBaseCFPvPUiUi", LogType::None);
cafeExportRegisterFunc(DownloadedDataBase::GetExternalImageDataSize, "nn_olv", "GetExternalImageDataSize__Q3_2nn3olv18DownloadedDataBaseCFv", LogType::None);
// DownloadedPostData getters
cafeExportRegisterFunc(DownloadedPostData::GetCommunityId, "nn_olv", "GetCommunityId__Q3_2nn3olv18DownloadedPostDataCFv", LogType::None);
@ -305,6 +440,23 @@ namespace nn
cafeExportRegisterFunc(hidden::DownloadedSystemTopicDataList::GetDownloadedSystemTopicData, "nn_olv", "GetDownloadedSystemTopicData__Q4_2nn3olv6hidden29DownloadedSystemTopicDataListCFi", LogType::None);
cafeExportRegisterFunc(hidden::DownloadedSystemTopicDataList::GetDownloadedSystemPostData, "nn_olv", "GetDownloadedSystemPostData__Q4_2nn3olv6hidden29DownloadedSystemTopicDataListCFiT1", LogType::None);
// DownloadPostDataListParam constructor and getters
cafeExportRegisterFunc(DownloadPostDataListParam::Construct, "nn_olv", "__ct__Q3_2nn3olv25DownloadPostDataListParamFv", LogType::None);
cafeExportRegisterFunc(DownloadPostDataListParam::SetFlags, "nn_olv", "SetFlags__Q3_2nn3olv25DownloadPostDataListParamFUi", LogType::None);
cafeExportRegisterFunc(DownloadPostDataListParam::SetLanguageId, "nn_olv", "SetLanguageId__Q3_2nn3olv25DownloadPostDataListParamFUc", LogType::None);
cafeExportRegisterFunc(DownloadPostDataListParam::SetCommunityId, "nn_olv", "SetCommunityId__Q3_2nn3olv25DownloadPostDataListParamFUi", LogType::None);
cafeExportRegisterFunc(DownloadPostDataListParam::SetSearchKey, "nn_olv", "SetSearchKey__Q3_2nn3olv25DownloadPostDataListParamFPCwUc", LogType::None);
cafeExportRegisterFunc(DownloadPostDataListParam::SetSearchKeySingle, "nn_olv", "SetSearchKey__Q3_2nn3olv25DownloadPostDataListParamFPCw", LogType::None);
cafeExportRegisterFunc(DownloadPostDataListParam::SetSearchPid, "nn_olv", "SetSearchPid__Q3_2nn3olv25DownloadPostDataListParamFUi", LogType::None);
cafeExportRegisterFunc(DownloadPostDataListParam::SetPostId, "nn_olv", "SetPostId__Q3_2nn3olv25DownloadPostDataListParamFPCcUi", LogType::None);
cafeExportRegisterFunc(DownloadPostDataListParam::SetPostDate, "nn_olv", "SetPostDate__Q3_2nn3olv25DownloadPostDataListParamFL", LogType::None);
cafeExportRegisterFunc(DownloadPostDataListParam::SetPostDataMaxNum, "nn_olv", "SetPostDataMaxNum__Q3_2nn3olv25DownloadPostDataListParamFUi", LogType::None);
cafeExportRegisterFunc(DownloadPostDataListParam::SetBodyTextMaxLength, "nn_olv", "SetBodyTextMaxLength__Q3_2nn3olv25DownloadPostDataListParamFUi", LogType::None);
// URL and downloading functions
cafeExportRegisterFunc(DownloadPostDataListParam::GetRawDataUrl, "nn_olv", "GetRawDataUrl__Q3_2nn3olv25DownloadPostDataListParamCFPcUi", LogType::None);
cafeExportRegisterFunc(DownloadPostDataList, "nn_olv", "DownloadPostDataList__Q2_2nn3olvFPQ3_2nn3olv19DownloadedTopicDataPQ3_2nn3olv18DownloadedPostDataPUiUiPCQ3_2nn3olv25DownloadPostDataListParam", LogType::None);
}
}

View File

@ -1,5 +1,6 @@
#pragma once
#include <zlib.h>
#include "nn_olv_Common.h"
namespace nn
{
@ -154,8 +155,11 @@ namespace nn
return OLV_RESULT_INVALID_PTR;
if (maxLength == 0)
return OLV_RESULT_NOT_ENOUGH_SIZE;
if (!TestFlags(_this, FLAGS::HAS_BODY_TEXT))
return OLV_RESULT_MISSING_DATA;
memset(bodyTextOut, 0, maxLength * sizeof(uint16));
uint32 outputLength = std::min<uint32>(_this->bodyTextLength, maxLength);
olv_wstrncpy((char16_t*)bodyTextOut, (char16_t*)_this->bodyText, _this->bodyTextLength);
olv_wstrncpy((char16_t*)bodyTextOut, (char16_t*)_this->bodyText, outputLength);
return OLV_RESULT_SUCCESS;
}
@ -213,9 +217,19 @@ namespace nn
return _this->postId;
}
// todo:
// DownloadExternalImageData__Q3_2nn3olv18DownloadedDataBaseCFPvPUiUi
static nnResult DownloadExternalImageData(DownloadedDataBase* _this, void* imageDataOut, uint32be* imageSizeOut, uint32 maxSize);
// GetExternalImageDataSize__Q3_2nn3olv18DownloadedDataBaseCFv
static uint32 GetExternalImageDataSize(DownloadedDataBase* _this)
{
if (!TestFlags(_this, FLAGS::HAS_EXTERNAL_IMAGE))
return 0;
return _this->externalImageDataSize;
}
// todo:
// DownloadExternalImageData__Q3_2nn3olv18DownloadedDataBaseCFPvPUiUi (implement downloading)
// DownloadExternalBinaryData__Q3_2nn3olv18DownloadedDataBaseCFPvPUiUi
// GetExternalBinaryDataSize__Q3_2nn3olv18DownloadedDataBaseCFv
};
@ -425,6 +439,174 @@ namespace nn
static_assert(sizeof(DownloadedSystemTopicDataList) == 0xC1000);
}
struct DownloadPostDataListParam
{
static constexpr size_t MAX_NUM_SEARCH_PID = 12;
static constexpr size_t MAX_NUM_SEARCH_KEY = 5;
static constexpr size_t MAX_NUM_POST_ID = 20;
enum class FLAGS
{
FRIENDS_ONLY = 0x01, // friends only
FOLLOWERS_ONLY = 0x02, // followers only
SELF_ONLY = 0x04, // self only
ONLY_TYPE_TEXT = 0x08,
ONLY_TYPE_MEMO = 0x10,
UKN_20 = 0x20,
WITH_MII = 0x40, // with mii
WITH_EMPATHY = 0x80, // with yeahs added
UKN_100 = 0x100,
UKN_200 = 0x200, // "is_delay" parameter
UKN_400 = 0x400, // "is_hot" parameter
};
struct SearchKey
{
uint16be str[152];
};
struct PostId
{
char str[32];
};
betype<FLAGS> flags;
uint32be communityId;
uint32be searchPid[MAX_NUM_SEARCH_PID];
uint8 languageId;
uint8 hasLanguageId_039;
uint8 padding03A[2];
uint32be postDataMaxNum;
SearchKey searchKeyArray[MAX_NUM_SEARCH_KEY];
PostId searchPostId[MAX_NUM_POST_ID];
uint64be postDate; // OSTime?
uint64be titleId; // only used by System posts?
uint32be bodyTextMaxLength;
uint8 padding8C4[1852];
bool _HasFlag(FLAGS flag)
{
return ((uint32)flags.value() & (uint32)flag) != 0;
}
void _SetFlags(FLAGS flag)
{
flags = (FLAGS)((uint32)flags.value() | (uint32)flag);
}
// constructor and getters
// __ct__Q3_2nn3olv25DownloadPostDataListParamFv
static DownloadPostDataListParam* Construct(DownloadPostDataListParam* _this)
{
memset(_this, 0, sizeof(DownloadPostDataListParam));
return _this;
}
// SetFlags__Q3_2nn3olv25DownloadPostDataListParamFUi
static nnResult SetFlags(DownloadPostDataListParam* _this, FLAGS flags)
{
// todo - verify flag combos
_this->flags = flags;
return OLV_RESULT_SUCCESS;
}
// SetLanguageId__Q3_2nn3olv25DownloadPostDataListParamFUc
static nnResult SetLanguageId(DownloadPostDataListParam* _this, uint8 languageId)
{
_this->languageId = languageId;
_this->hasLanguageId_039 = 1;
return OLV_RESULT_SUCCESS;
}
// SetCommunityId__Q3_2nn3olv25DownloadPostDataListParamFUi
static nnResult SetCommunityId(DownloadPostDataListParam* _this, uint32 communityId)
{
_this->communityId = communityId;
return OLV_RESULT_SUCCESS;
}
// SetSearchKey__Q3_2nn3olv25DownloadPostDataListParamFPCwUc
static nnResult SetSearchKey(DownloadPostDataListParam* _this, const uint16be* searchKey, uint8 searchKeyIndex)
{
if (searchKeyIndex >= MAX_NUM_SEARCH_KEY)
return OLV_RESULT_INVALID_PARAMETER;
memset(&_this->searchKeyArray[searchKeyIndex], 0, sizeof(SearchKey));
if(olv_wstrnlen((const char16_t*)searchKey, 152) > 50)
{
cemuLog_log(LogType::Force, "DownloadPostDataListParam::SetSearchKey: searchKey is too long\n");
return OLV_RESULT_INVALID_PARAMETER;
}
SetStringUC2(_this->searchKeyArray[searchKeyIndex].str, searchKey);
return OLV_RESULT_SUCCESS;
}
// SetSearchKey__Q3_2nn3olv25DownloadPostDataListParamFPCw
static nnResult SetSearchKeySingle(DownloadPostDataListParam* _this, const uint16be* searchKey)
{
return SetSearchKey(_this, searchKey, 0);
}
// SetSearchPid__Q3_2nn3olv25DownloadPostDataListParamFUi
static nnResult SetSearchPid(DownloadPostDataListParam* _this, uint32 searchPid)
{
if(_this->_HasFlag(FLAGS::FRIENDS_ONLY) || _this->_HasFlag(FLAGS::FOLLOWERS_ONLY) || _this->_HasFlag(FLAGS::SELF_ONLY))
return OLV_RESULT_INVALID_PARAMETER;
_this->searchPid[0] = searchPid;
return OLV_RESULT_SUCCESS;
}
// SetPostId__Q3_2nn3olv25DownloadPostDataListParamFPCcUi
static nnResult SetPostId(DownloadPostDataListParam* _this, const char* postId, uint32 postIdIndex)
{
if (postIdIndex >= MAX_NUM_POST_ID)
return OLV_RESULT_INVALID_PARAMETER;
memset(&_this->searchPostId[postIdIndex], 0, sizeof(PostId));
if (strlen(postId) > 22)
{
cemuLog_log(LogType::Force, "DownloadPostDataListParam::SetPostId: postId is too long\n");
return OLV_RESULT_INVALID_PARAMETER;
}
strcpy(_this->searchPostId[postIdIndex].str, postId);
return OLV_RESULT_SUCCESS;
}
// SetPostDate__Q3_2nn3olv25DownloadPostDataListParamFL
static nnResult SetPostDate(DownloadPostDataListParam* _this, uint64 postDate)
{
_this->postDate = postDate;
return OLV_RESULT_SUCCESS;
}
// SetPostDataMaxNum__Q3_2nn3olv25DownloadPostDataListParamFUi
static nnResult SetPostDataMaxNum(DownloadPostDataListParam* _this, uint32 postDataMaxNum)
{
if(postDataMaxNum == 0)
return OLV_RESULT_INVALID_PARAMETER;
_this->postDataMaxNum = postDataMaxNum;
return OLV_RESULT_SUCCESS;
}
// SetBodyTextMaxLength__Q3_2nn3olv25DownloadPostDataListParamFUi
static nnResult SetBodyTextMaxLength(DownloadPostDataListParam* _this, uint32 bodyTextMaxLength)
{
if(bodyTextMaxLength >= 256)
return OLV_RESULT_INVALID_PARAMETER;
_this->bodyTextMaxLength = bodyTextMaxLength;
return OLV_RESULT_SUCCESS;
}
// GetRawDataUrl__Q3_2nn3olv25DownloadPostDataListParamCFPcUi
static nnResult GetRawDataUrl(DownloadPostDataListParam* _this, char* urlOut, uint32 urlMaxSize);
};
static_assert(sizeof(DownloadPostDataListParam) == 0x1000);
// parsing functions
bool ParseXML_DownloadedPostData(DownloadedPostData& obj, pugi::xml_node& xmlNode);
void loadOlivePostAndTopicTypes();
}
}

View File

@ -9,7 +9,7 @@ bool sTLInitialized{ false };
fs::path sTLCacheFilePath;
// lists for tracking known titles
// note: The list may only contain titles with valid meta data. Entries loaded from the cache may not have been parsed yet, but they will use a cached value for titleId and titleVersion
// note: The list may only contain titles with valid meta data (except for certain system titles). Entries loaded from the cache may not have been parsed yet, but they will use a cached value for titleId and titleVersion
std::mutex sTLMutex;
std::vector<TitleInfo*> sTLList;
std::vector<TitleInfo*> sTLListPending;

View File

@ -485,6 +485,14 @@ bool future_is_ready(std::future<T>& f)
#endif
}
// replace with std::scope_exit once available
struct scope_exit
{
std::function<void()> f_;
explicit scope_exit(std::function<void()> f) noexcept : f_(std::move(f)) {}
~scope_exit() { if (f_) f_(); }
};
// helper function to cast raw pointers to std::atomic
// this is technically not legal but works on most platforms as long as alignment restrictions are met and the implementation of atomic doesnt come with additional members