Add all the files

This commit is contained in:
Exzap 2022-08-22 22:21:23 +02:00
parent e3db07a16a
commit d60742f52b
1445 changed files with 430238 additions and 0 deletions

View file

@ -0,0 +1,68 @@
#include "BaseInfo.h"
#include "config/CemuConfig.h"
#include "Cafe/Filesystem/fsc.h"
#include "Cafe/Filesystem/FST/FST.h"
sint32 BaseInfo::GetLanguageIndex(std::string_view language)
{
if (language == "ja")
return (sint32)CafeConsoleLanguage::JA;
else if (language == "en")
return (sint32)CafeConsoleLanguage::EN;
else if (language == "fr")
return (sint32)CafeConsoleLanguage::FR;
else if (language == "de")
return (sint32)CafeConsoleLanguage::DE;
else if (language == "it")
return (sint32)CafeConsoleLanguage::IT;
else if (language == "es")
return (sint32)CafeConsoleLanguage::ES;
else if (language == "zhs")
return (sint32)CafeConsoleLanguage::ZH;
else if (language == "ko")
return (sint32)CafeConsoleLanguage::KO;
else if (language == "nl")
return (sint32)CafeConsoleLanguage::NL;
else if (language == "pt")
return (sint32)CafeConsoleLanguage::PT;
else if (language == "ru")
return (sint32)CafeConsoleLanguage::RU;
else if (language == "zht")
return (sint32)CafeConsoleLanguage::ZH;
return -1;
}
std::unique_ptr<uint8[]> BaseInfo::ReadFSCFile(std::string_view filename, uint32& size) const
{
size = 0;
sint32 fscStatus = 0;
// load and parse meta.xml
FSCVirtualFile* file = fsc_open(const_cast<char*>(std::string(filename).c_str()), FSC_ACCESS_FLAG::OPEN_FILE | FSC_ACCESS_FLAG::READ_PERMISSION, &fscStatus);
if (file)
{
size = fsc_getFileSize(file);
auto buffer = std::make_unique<uint8[]>(size);
fsc_readFile(file, buffer.get(), size);
fsc_close(file);
return buffer;
}
return nullptr;
}
std::unique_ptr<uint8[]> BaseInfo::ReadVirtualFile(FSTVolume* volume, std::string_view filename, uint32& size) const
{
size = 0;
FSTFileHandle fileHandle;
if (!volume->OpenFile(filename, fileHandle, true))
return nullptr;
size = volume->GetFileSize(fileHandle);
auto buffer = std::make_unique<uint8[]>(size);
volume->ReadFile(fileHandle, 0, size, buffer.get());
return buffer;
}

View file

@ -0,0 +1,37 @@
#pragma once
namespace pugi
{
struct xml_parse_result;
class xml_document;
}
class BaseInfo
{
public:
enum class GameType
{
FSC, // using fsc API
Directory, // rpx/meta
Image, // wud/wux
};
virtual ~BaseInfo() = default;
[[nodiscard]] const fs::path& GetPath() const { return m_type_path; }
[[nodiscard]] GameType GetGameType() const { return m_type; }
protected:
GameType m_type;
fs::path m_type_path; // empty / base dir / wud path
virtual void ParseDirectory(const fs::path& filename) = 0;
virtual bool ParseFile(const fs::path& filename) = 0;
[[nodiscard]] std::unique_ptr<uint8[]> ReadFSCFile(std::string_view filename, uint32& size) const;
[[nodiscard]] std::unique_ptr<uint8[]> ReadVirtualFile(class FSTVolume* volume, std::string_view filename, uint32& size) const;
[[nodiscard]] static sint32 GetLanguageIndex(std::string_view language);
};

View file

@ -0,0 +1,121 @@
#pragma once
#include "config/CemuConfig.h"
#include "MetaInfo.h"
#include "TitleInfo.h"
#include "config/ActiveSettings.h"
class GameInfo2
{
public:
~GameInfo2()
{
m_base.UnmountAll();
m_update.UnmountAll();
for (auto& it : m_aoc)
it.UnmountAll();
}
bool IsValid() const
{
return m_base.IsValid(); // at least the base must be valid for this to be a runnable title
}
void SetBase(const TitleInfo& titleInfo)
{
m_base = titleInfo;
}
void SetUpdate(const TitleInfo& titleInfo)
{
if (HasUpdate())
{
if (titleInfo.GetAppTitleVersion() > m_update.GetAppTitleVersion())
m_update = titleInfo;
}
else
m_update = titleInfo;
}
bool HasUpdate() const
{
return m_update.IsValid();
}
void AddAOC(const TitleInfo& titleInfo)
{
TitleId aocTitleId = titleInfo.GetAppTitleId();
uint16 aocVersion = titleInfo.GetAppTitleVersion();
auto it = std::find_if(m_aoc.begin(), m_aoc.end(), [aocTitleId](const TitleInfo& rhs) { return rhs.GetAppTitleId() == aocTitleId; });
if (it != m_aoc.end())
{
if(it->GetAppTitleVersion() >= aocVersion)
return;
m_aoc.erase(it);
}
m_aoc.emplace_back(titleInfo);
}
bool HasAOC() const
{
return !m_aoc.empty();
}
TitleInfo& GetBase()
{
return m_base;
}
TitleInfo& GetUpdate()
{
return m_update;
}
std::span<TitleInfo> GetAOC()
{
return m_aoc;
}
TitleId GetBaseTitleId()
{
cemu_assert_debug(m_base.IsValid());
return m_base.GetAppTitleId();
}
std::string GetTitleName()
{
cemu_assert_debug(m_base.IsValid());
return m_base.GetTitleName(); // long name
}
uint16 GetVersion() const
{
if (m_update.IsValid())
return m_update.GetAppTitleVersion();
return m_base.GetAppTitleVersion();
}
CafeConsoleRegion GetRegion() const
{
if (m_update.IsValid())
return m_update.GetMetaRegion();
return m_base.GetMetaRegion();
}
uint16 GetAOCVersion() const
{
if (m_aoc.empty())
return 0;
return m_aoc.front().GetAppTitleVersion();
}
fs::path GetSaveFolder()
{
return ActiveSettings::GetMlcPath(fmt::format("usr/save/{:08x}/{:08x}", (GetBaseTitleId() >> 32), GetBaseTitleId() & 0xFFFFFFFF));
}
private:
TitleInfo m_base;
TitleInfo m_update;
std::vector<TitleInfo> m_aoc;
};

View file

@ -0,0 +1,180 @@
#include "MetaInfo.h"
#include "Cafe/Filesystem/fsc.h"
#include "pugixml.hpp"
#include "Cafe/Filesystem/FST/FST.h"
MetaInfo::MetaInfo()
{
m_type = GameType::FSC;
uint32 meta_size;
const auto meta_data = ReadFSCFile("vol/meta/meta.xml", meta_size);
if (meta_size == 0 || !meta_data)
throw std::runtime_error("meta.xml missing");
pugi::xml_document meta_doc;
ParseMetaFile(meta_doc, meta_doc.load_buffer_inplace(meta_data.get(), meta_size));
}
MetaInfo::MetaInfo(const fs::path& filename)
{
if (!fs::exists(filename))
throw std::invalid_argument("filename doesn't exist");
if (fs::is_directory(filename))
{
MetaInfo::ParseDirectory(filename);
m_type = GameType::Directory;
m_type_path = filename;
}
else
MetaInfo::ParseFile(filename);
}
std::string MetaInfo::GetName(CafeConsoleLanguage language) const
{
std::string long_name{ GetLongName(language) };
const auto nl = long_name.find(L'\n');
if (nl != std::string::npos)
long_name.replace(nl, 1, " - ");
return long_name;
}
const std::string& MetaInfo::GetLongName(CafeConsoleLanguage language) const
{
return m_long_name[(int)language].empty() ? m_long_name[(int)CafeConsoleLanguage::EN] : m_long_name[(int)language];
}
const std::string& MetaInfo::GetShortName(CafeConsoleLanguage language) const
{
return m_short_name[(int)language].empty() ? m_short_name[(int)CafeConsoleLanguage::EN] : m_short_name[(int)language];
}
const std::string& MetaInfo::GetPublisher(CafeConsoleLanguage language) const
{
return m_publisher[(int)language].empty() ? m_publisher[(int)CafeConsoleLanguage::EN] : m_publisher[(int)language];
}
void MetaInfo::ParseDirectory(const fs::path& filename)
{
const auto meta_dir = fs::path(filename).append(L"meta");
if (!fs::exists(meta_dir) || !fs::is_directory(meta_dir))
throw std::invalid_argument("meta directory missing");
const auto meta_file = meta_dir / L"meta.xml";
if (!fs::exists(meta_file) || !fs::is_regular_file(meta_file))
throw std::invalid_argument("meta.xml missing");
ParseMetaFile(meta_file.wstring());
}
bool MetaInfo::ParseFile(const fs::path& filename)
{
const auto extension = filename.extension();
if (filename.filename() != "meta.xml")
return false;
const auto base_dir = filename.parent_path().parent_path();
ParseMetaFile(filename);
m_type = GameType::Directory;
m_type_path = base_dir;
return true;
}
void MetaInfo::ParseMetaFile(const fs::path& meta_file)
{
pugi::xml_document doc;
const auto result = doc.load_file(meta_file.wstring().c_str());
ParseMetaFile(doc, result);
}
void MetaInfo::ParseMetaFile(const pugi::xml_document& doc, const pugi::xml_parse_result& result)
{
if (!result)
throw std::invalid_argument(fmt::format("error when parsing the meta.xml: {}", result.description()));
const auto root = doc.child("menu");
if (!root)
throw std::invalid_argument("meta.xml invalid");
for (const auto& child : root.children())
{
std::string_view name = child.name();
if (name == "title_version")
m_version = child.text().as_uint();
else if (name == "product_code")
m_product_code = child.text().as_string();
else if (name == "company_code")
m_company_code = child.text().as_string();
else if (name == "content_platform")
m_content_platform = child.text().as_string();
else if (name == "title_id")
m_title_id = std::stoull(child.text().as_string(), nullptr, 16);
else if (name == "region")
m_region = (CafeConsoleRegion)child.text().as_uint();
else if (boost::starts_with(name, "longname_"))
{
const sint32 index = GetLanguageIndex(name.substr(std::size("longname_") - 1));
if (index != -1)
m_long_name[index] = child.text().as_string();
}
else if (boost::starts_with(name, L"shortname_"))
{
const sint32 index = GetLanguageIndex(name.substr(std::size("shortname_") - 1));
if (index != -1)
m_short_name[index] = child.text().as_string();
}
else if (boost::starts_with(name, L"publisher_"))
{
const sint32 index = GetLanguageIndex(name.substr(std::size("publisher_") - 1));
if (index != -1)
m_publisher[index] = child.text().as_string();
}
}
}
std::unique_ptr<uint8[]> MetaInfo::GetIcon(uint32& size) const
{
size = 0;
switch (m_type)
{
case GameType::FSC:
return ReadFSCFile("vol/meta/iconTex.tga", size);
case GameType::Directory:
{
cemu_assert_debug(!m_type_path.empty());
const auto icon = fs::path(m_type_path).append(L"meta").append(L"iconTex.tga");
std::ifstream file(icon, std::ios::binary | std::ios::ate);
if (file.is_open())
{
size = file.tellg();
if (size > 0)
{
file.seekg(0, std::ios::beg);
auto result = std::make_unique<uint8[]>(size);
file.read((char*)result.get(), size);
return result;
}
}
return nullptr;
}
case GameType::Image:
{
cemu_assert_debug(!m_type_path.empty());
FSTVolume* volume = FSTVolume::OpenFromDiscImage(m_type_path);
if (volume)
{
auto result = ReadVirtualFile(volume, "meta/iconTex.tga", size);
delete volume;
return result;
}
return nullptr;
}
default:
__assume(false);
}
return nullptr;
}

View file

@ -0,0 +1,61 @@
#pragma once
#include "BaseInfo.h"
#include "config/CemuConfig.h"
class MetaInfo : public BaseInfo
{
public:
MetaInfo();
MetaInfo(const fs::path& filename);
// returns long name with replaces newlines to ' - '
[[nodiscard]] std::string GetName(CafeConsoleLanguage language = CafeConsoleLanguage::EN) const;
[[nodiscard]] uint64 GetTitleId() const { return m_title_id; }
[[nodiscard]] uint32 GetTitleIdHigh() const { return (uint32)(GetTitleId() >> 32); }
[[nodiscard]] uint32 GetTitleIdLow() const { return (uint32)(GetTitleId() & 0xFFFFFFFF); }
[[nodiscard]] uint64 GetBaseTitleId() const { return m_title_id & ~0xF00000000ULL; }
[[nodiscard]] uint32 GetBaseTitleIdHigh() const { return (uint32)(GetBaseTitleId() >> 32); }
[[nodiscard]] uint32 GetBaseTitleIdLow() const { return (uint32)(GetBaseTitleId() & 0xFFFFFFFF); }
[[nodiscard]] uint64 GetUpdateTitleId() const { return GetBaseTitleId() | 0xE00000000ULL; }
[[nodiscard]] uint32 GetUpdateTitleIdHigh() const { return (uint32)(GetUpdateTitleId() >> 32); }
[[nodiscard]] uint32 GetUpdateTitleIdLow() const { return (uint32)(GetUpdateTitleId() & 0xFFFFFFFF); }
[[nodiscard]] uint64 GetDLCTitleId() const { return GetBaseTitleId() | 0xC00000000ULL; }
[[nodiscard]] uint32 GetDLCTitleIdHigh() const { return (uint32)(GetDLCTitleId() >> 32); }
[[nodiscard]] uint32 GetDLCTitleIdLow() const { return (uint32)(GetDLCTitleId() & 0xFFFFFFFF); }
[[nodiscard]] const std::string& GetLongName(CafeConsoleLanguage language) const;
[[nodiscard]] const std::string& GetShortName(CafeConsoleLanguage language) const;
[[nodiscard]] const std::string& GetPublisher(CafeConsoleLanguage language) const;
[[nodiscard]] const std::string& GetProductCode() const { return m_product_code; }
[[nodiscard]] const std::string& GetCompanyCode() const { return m_company_code; }
[[nodiscard]] const std::string& GetContentPlatform() const { return m_content_platform; }
[[nodiscard]] uint32 GetVersion() const { return m_version; }
[[nodiscard]] CafeConsoleRegion GetRegion() const { return m_region; }
[[nodiscard]] std::unique_ptr<uint8[]> GetIcon(uint32& size) const;
protected:
// meta.xml
uint32 m_version;
std::string m_product_code;
std::string m_company_code;
std::string m_content_platform;
uint64 m_title_id;
CafeConsoleRegion m_region;
std::array<std::string, 12> m_long_name;
std::array<std::string, 12> m_short_name;
std::array<std::string, 12> m_publisher;
void ParseDirectory(const fs::path& filename) override;
bool ParseFile(const fs::path& filename) override;
void ParseMetaFile(const fs::path& meta_file);
void ParseMetaFile(const pugi::xml_document& doc, const pugi::xml_parse_result& result);
};

View file

@ -0,0 +1,140 @@
#pragma once
#include <pugixml.hpp>
#include "config/CemuConfig.h"
struct ParsedMetaXml
{
uint32 m_version;
std::string m_product_code;
std::string m_company_code;
std::string m_content_platform;
uint64 m_title_id;
CafeConsoleRegion m_region;
std::array<std::string, 12> m_long_name;
std::array<std::string, 12> m_short_name;
std::array<std::string, 12> m_publisher;
std::string GetShortName(CafeConsoleLanguage languageId) const
{
return m_short_name[(size_t)languageId];
}
std::string GetLongName(CafeConsoleLanguage languageId) const
{
return m_long_name[(size_t)languageId];
}
TitleId GetTitleId() const
{
return m_title_id;
}
uint16 GetTitleVersion() const
{
return (uint16)m_version;
}
CafeConsoleRegion GetRegion() const
{
return m_region;
}
std::string GetProductCode() const
{
return m_product_code;
}
std::string GetCompanyCode() const
{
return m_company_code;
}
static ParsedMetaXml* Parse(uint8* xmlData, size_t xmlSize)
{
if (xmlSize == 0)
return nullptr;
pugi::xml_document meta_doc;
if (!meta_doc.load_buffer_inplace(xmlData, xmlSize))
return nullptr;
const auto root = meta_doc.child("menu");
if (!root)
return nullptr;
ParsedMetaXml* parsedMetaXml = new ParsedMetaXml();
for (const auto& child : root.children())
{
std::string_view name = child.name();
if (name == "title_version")
parsedMetaXml->m_version = child.text().as_uint();
else if (name == "product_code")
parsedMetaXml->m_product_code = child.text().as_string();
else if (name == "company_code")
parsedMetaXml->m_company_code = child.text().as_string();
else if (name == "content_platform")
parsedMetaXml->m_content_platform = child.text().as_string();
else if (name == "title_id")
parsedMetaXml->m_title_id = std::stoull(child.text().as_string(), nullptr, 16);
else if (name == "region")
parsedMetaXml->m_region = (CafeConsoleRegion)child.text().as_uint();
else if (boost::starts_with(name, "longname_"))
{
const sint32 index = GetLanguageIndex(name.substr(std::size("longname_") - 1));
if (index != -1)
parsedMetaXml->m_long_name[index] = child.text().as_string();
}
else if (boost::starts_with(name, L"shortname_"))
{
const sint32 index = GetLanguageIndex(name.substr(std::size("shortname_") - 1));
if (index != -1)
parsedMetaXml->m_short_name[index] = child.text().as_string();
}
else if (boost::starts_with(name, L"publisher_"))
{
const sint32 index = GetLanguageIndex(name.substr(std::size("publisher_") - 1));
if (index != -1)
parsedMetaXml->m_publisher[index] = child.text().as_string();
}
}
if (parsedMetaXml->m_title_id == 0)
{
// not valid
delete parsedMetaXml;
return nullptr;
}
return parsedMetaXml;
}
private:
static sint32 GetLanguageIndex(std::string_view language) // move to NCrypto ?
{
if (language == "ja")
return (sint32)CafeConsoleLanguage::JA;
else if (language == "en")
return (sint32)CafeConsoleLanguage::EN;
else if (language == "fr")
return (sint32)CafeConsoleLanguage::FR;
else if (language == "de")
return (sint32)CafeConsoleLanguage::DE;
else if (language == "it")
return (sint32)CafeConsoleLanguage::IT;
else if (language == "es")
return (sint32)CafeConsoleLanguage::ES;
else if (language == "zhs")
return (sint32)CafeConsoleLanguage::ZH;
else if (language == "ko")
return (sint32)CafeConsoleLanguage::KO;
else if (language == "nl")
return (sint32)CafeConsoleLanguage::NL;
else if (language == "pt")
return (sint32)CafeConsoleLanguage::PT;
else if (language == "ru")
return (sint32)CafeConsoleLanguage::RU;
else if (language == "zht")
return (sint32)CafeConsoleLanguage::ZH;
return -1;
}
};

View file

@ -0,0 +1,34 @@
#include "SaveInfo.h"
#include "config/ActiveSettings.h"
#include "Common/filestream.h"
#include "ParsedMetaXml.h"
SaveInfo::SaveInfo(TitleId titleId) : m_titleId(titleId)
{
m_path = GetSavePath(titleId);
std::error_code ec;
m_isValid = fs::is_directory(m_path, ec);
}
std::string SaveInfo::GetStorageSubpathByTitleId(TitleId titleId)
{
// usr/save/<titleIdHigh>/<titleIdLow>/
return fmt::format("usr/save/{:08x}/{:08x}", ((uint64)titleId) >> 32, (uint64)titleId & 0xFFFFFFFF);
}
fs::path SaveInfo::GetSavePath(TitleId titleId)
{
return ActiveSettings::GetMlcPath(GetStorageSubpathByTitleId(titleId));
}
bool SaveInfo::ParseMetaData()
{
if (m_hasMetaLoaded)
return m_parsedMetaXml != nullptr;
m_hasMetaLoaded = true;
auto xmlData = FileStream::LoadIntoMemory(m_path / "meta/meta.xml");
if (!xmlData)
return false;
m_parsedMetaXml = ParsedMetaXml::Parse(xmlData->data(), xmlData->size());
return m_parsedMetaXml != nullptr;
}

View file

@ -0,0 +1,30 @@
#pragma once
#include "TitleId.h"
#include "ParsedMetaXml.h"
class SaveInfo
{
public:
SaveInfo() {};
SaveInfo(TitleId titleId);
bool IsValid() const { return m_isValid; }
TitleId GetTitleId() const { return m_titleId; }
fs::path GetPath() const { return m_path; }
// meta data
bool ParseMetaData();
ParsedMetaXml* GetMetaInfo() { return m_parsedMetaXml; }
private:
static std::string GetStorageSubpathByTitleId(TitleId titleId);
static fs::path GetSavePath(TitleId titleId);
TitleId m_titleId;
fs::path m_path;
bool m_isValid{false};
bool m_hasMetaLoaded{false};
ParsedMetaXml* m_parsedMetaXml{nullptr};
};

View file

@ -0,0 +1,177 @@
#include "SaveList.h"
#include <charconv>
std::mutex sSLMutex;
fs::path sSLMLCPath;
std::vector<SaveInfo*> sSLList;
// callback list
struct SaveListCallbackEntry
{
SaveListCallbackEntry(void(*cb)(CafeSaveListCallbackEvent* evt, void* ctx), void* ctx, uint64 uniqueId) :
cb(cb), ctx(ctx), uniqueId(uniqueId) {};
void (*cb)(CafeSaveListCallbackEvent* evt, void* ctx);
void* ctx;
uint64 uniqueId;
};
std::vector<SaveListCallbackEntry> sSLCallbackList;
// worker thread
std::atomic_bool sSLWorkerThreadActive{false};
void CafeSaveList::Initialize()
{
}
void CafeSaveList::SetMLCPath(fs::path mlcPath)
{
std::unique_lock _lock(sSLMutex);
sSLMLCPath = mlcPath;
}
void CafeSaveList::Refresh()
{
std::unique_lock _lock(sSLMutex);
if (sSLWorkerThreadActive)
return;
sSLWorkerThreadActive = true;
std::thread t(RefreshThreadWorker);
t.detach();
}
void CafeSaveList::RefreshThreadWorker()
{
// clear save list
for (auto& itSaveInfo : sSLList)
{
for (auto& it : sSLCallbackList)
{
CafeSaveListCallbackEvent evt;
evt.eventType = CafeSaveListCallbackEvent::TYPE::SAVE_REMOVED;
evt.saveInfo = itSaveInfo;
it.cb(&evt, it.ctx);
}
delete itSaveInfo;
}
sSLList.clear();
sSLMutex.lock();
fs::path mlcPath = sSLMLCPath;
sSLMutex.unlock();
std::error_code ec;
for (auto it_titleHigh : fs::directory_iterator(mlcPath / "usr/save", ec))
{
if(!it_titleHigh.is_directory(ec))
continue;
std::string dirName = _utf8Wrapper(it_titleHigh.path().filename());
if(dirName.empty())
continue;
uint32 titleIdHigh;
std::from_chars_result r = std::from_chars(dirName.data(), dirName.data() + dirName.size(), titleIdHigh, 16);
if (r.ec != std::errc())
continue;
fs::path tmp = it_titleHigh.path();
for (auto it_titleLow : fs::directory_iterator(tmp, ec))
{
if (!it_titleLow.is_directory(ec))
continue;
dirName = _utf8Wrapper(it_titleLow.path().filename());
if (dirName.empty())
continue;
uint32 titleIdLow;
std::from_chars_result r = std::from_chars(dirName.data(), dirName.data() + dirName.size(), titleIdLow, 16);
if (r.ec != std::errc())
continue;
// found save
TitleId titleId = (uint64)titleIdHigh << 32 | (uint64)titleIdLow;
SaveInfo* saveInfo = new SaveInfo(titleId);
if (saveInfo->IsValid())
DiscoveredSave(saveInfo);
else
delete saveInfo;
}
}
sSLMutex.lock();
sSLWorkerThreadActive = false;
sSLMutex.unlock();
// send notification about finished scan
for (auto& it : sSLCallbackList)
{
CafeSaveListCallbackEvent evt;
evt.eventType = CafeSaveListCallbackEvent::TYPE::SCAN_FINISHED;
evt.saveInfo = nullptr;
it.cb(&evt, it.ctx);
}
}
void CafeSaveList::DiscoveredSave(SaveInfo* saveInfo)
{
if (!saveInfo->ParseMetaData())
{
delete saveInfo;
return;
}
std::unique_lock _lock(sSLMutex);
auto it = std::find_if(sSLList.begin(), sSLList.end(), [saveInfo](const SaveInfo* rhs) { return saveInfo->GetTitleId() == rhs->GetTitleId(); });
if (it != sSLList.end())
{
// save already known
delete saveInfo;
return;
}
sSLList.emplace_back(saveInfo);
// send notification
for (auto& it : sSLCallbackList)
{
CafeSaveListCallbackEvent evt;
evt.eventType = CafeSaveListCallbackEvent::TYPE::SAVE_DISCOVERED;
evt.saveInfo = saveInfo;
it.cb(&evt, it.ctx);
}
}
uint64 CafeSaveList::RegisterCallback(void(*cb)(CafeSaveListCallbackEvent* evt, void* ctx), void* ctx)
{
static std::atomic<uint64_t> sCallbackIdGen = 1;
uint64 id = sCallbackIdGen.fetch_add(1);
std::unique_lock _lock(sSLMutex);
sSLCallbackList.emplace_back(cb, ctx, id);
// immediately notify of all known titles
for (auto& it : sSLList)
{
CafeSaveListCallbackEvent evt;
evt.eventType = CafeSaveListCallbackEvent::TYPE::SAVE_DISCOVERED;
evt.saveInfo = it;
cb(&evt, ctx);
}
// if not scanning then send out scan finished notification
if (!sSLWorkerThreadActive)
{
CafeSaveListCallbackEvent evt;
evt.eventType = CafeSaveListCallbackEvent::TYPE::SCAN_FINISHED;
evt.saveInfo = nullptr;
for (auto& it : sSLCallbackList)
it.cb(&evt, it.ctx);
}
return id;
}
void CafeSaveList::UnregisterCallback(uint64 id)
{
std::unique_lock _lock(sSLMutex);
auto it = std::find_if(sSLCallbackList.begin(), sSLCallbackList.end(), [id](auto& e) { return e.uniqueId == id; });
cemu_assert(it != sSLCallbackList.end());
sSLCallbackList.erase(it);
}
SaveInfo CafeSaveList::GetSaveByTitleId(TitleId titleId)
{
std::unique_lock _lock(sSLMutex);
for (auto& it : sSLList)
if (it->GetTitleId() == titleId)
return *it;
return {};
}

View file

@ -0,0 +1,33 @@
#pragma once
#include "SaveInfo.h"
struct CafeSaveListCallbackEvent
{
enum class TYPE
{
SAVE_DISCOVERED,
SAVE_REMOVED,
SCAN_FINISHED,
};
TYPE eventType;
SaveInfo* saveInfo;
};
class CafeSaveList
{
public:
static void Initialize();
static void SetMLCPath(fs::path mlcPath);
static void Refresh();
static SaveInfo GetSaveByTitleId(TitleId titleId);
// callback
static uint64 RegisterCallback(void(*cb)(CafeSaveListCallbackEvent* evt, void* ctx), void* ctx); // on register, the callback will be invoked for every already known save
static void UnregisterCallback(uint64 id);
private:
static void RefreshThreadWorker();
static void DiscoveredSave(SaveInfo* saveInfo);
};

View file

@ -0,0 +1,125 @@
#pragma once
using TitleId = uint64;
static_assert(sizeof(TitleId) == 8);
class TitleIdParser
{
public:
enum class TITLE_TYPE
{
/* XX */ UNKNOWN = 0xFF, // placeholder
/* 00 */ BASE_TITLE = 0x00, // eShop and disc titles
/* 02 */ BASE_TITLE_DEMO = 0x02,
/* 0E */ BASE_TITLE_UPDATE = 0x0E, // update for BASE_TITLE (and maybe BASE_TITLE_DEMO?)
/* 0C */ AOC = 0x0C, // DLC
/* 10 */ SYSTEM_TITLE = 0x10, // eShop etc
/* 1B */ SYSTEM_DATA = 0x1B,
/* 30 */ SYSTEM_OVERLAY_TITLE = 0x30,
};
TitleIdParser(uint64 titleId) : m_titleId(titleId) {};
// controls whether this title installs to /usr/title or /sys/title
bool IsSystemTitle() const
{
return (GetTypeByte() & 0x10) != 0;
};
bool IsBaseTitleUpdate() const
{
return GetType() == TITLE_TYPE::BASE_TITLE_UPDATE;
}
TITLE_TYPE GetType() const
{
uint8 b = GetTypeByte();
switch (b)
{
case 0x00:
return TITLE_TYPE::BASE_TITLE;
case 0x02:
return TITLE_TYPE::BASE_TITLE_DEMO;
case 0x0E:
return TITLE_TYPE::BASE_TITLE_UPDATE;
case 0x0C:
return TITLE_TYPE::AOC;
case 0x10:
return TITLE_TYPE::SYSTEM_TITLE;
case 0x1B:
return TITLE_TYPE::SYSTEM_DATA;
case 0x30:
return TITLE_TYPE::SYSTEM_OVERLAY_TITLE;
}
cemuLog_log(LogType::Force, "Unknown title type ({0:016x})", m_titleId);
return TITLE_TYPE::UNKNOWN;
}
bool IsPlatformCafe() const
{
return GetPlatformWord() == 0x0005;
}
bool CanHaveSeparateUpdateTitleId() const
{
return GetType() == TITLE_TYPE::BASE_TITLE;
}
TitleId GetSeparateUpdateTitleId() const
{
cemu_assert_debug(CanHaveSeparateUpdateTitleId());
return MakeTitleIdWithType(TITLE_TYPE::BASE_TITLE_UPDATE); // e.g. 00050000-11223344 -> 0005000E-11223344
}
static TitleId MakeBaseTitleId(TitleId titleId)
{
TitleIdParser titleIdParser(titleId);
if (titleIdParser.GetType() == TITLE_TYPE::BASE_TITLE_UPDATE)
return titleIdParser.MakeTitleIdWithType(TITLE_TYPE::BASE_TITLE);
return titleId;
}
static bool ParseFromStr(std::string_view strView, TitleId& titleIdOut)
{
if (strView.size() < 16)
return false;
uint64 tmp = 0;
for (size_t i = 0; i < 8*2; i++)
{
tmp <<= 4;
char c = strView[i];
if (c >= 'A' && c <= 'F')
tmp += (uint64)(c - 'A' + 10);
else if (c >= 'a' && c <= 'f')
tmp += (uint64)(c - 'a' + 10);
else if (c >= '0' && c <= '9')
tmp += (uint64)(c - '0');
else
return false;
}
titleIdOut = tmp;
return true;
}
private:
uint8 GetTypeByte() const
{
return (m_titleId >> 32) & 0xFF;
}
TitleId MakeTitleIdWithType(TITLE_TYPE newType) const
{
TitleId t = m_titleId;
t &= ~(0xFFull << 32);
t |= ((uint64)newType << 32);
return t;
}
uint16 GetPlatformWord() const // might not be a whole word?
{
return (m_titleId >> 48) & 0xFFFF;
}
TitleId m_titleId;
};

View file

@ -0,0 +1,654 @@
#include "TitleInfo.h"
#include "Cafe/Filesystem/fscDeviceHostFS.h"
#include "Cafe/Filesystem/FST/FST.h"
#include "pugixml.hpp"
#include "Common/filestream.h"
#include <zarchive/zarchivereader.h>
#include "config/ActiveSettings.h"
// detect format by reading file header/footer
CafeTitleFileType DetermineCafeSystemFileType(fs::path filePath)
{
std::unique_ptr<FileStream> fs(FileStream::openFile2(filePath));
if (!fs)
return CafeTitleFileType::UNKNOWN;
// very small files (<32 bytes) are always considered unknown
uint64 fileSize = fs->GetSize();
if (fileSize < 32)
return CafeTitleFileType::UNKNOWN;
// read header bytes
uint8 headerRaw[32]{};
fs->readData(headerRaw, sizeof(headerRaw));
// check for WUX
uint8 wuxHeaderMagic[8] = { 0x57,0x55,0x58,0x30,0x2E,0xD0,0x99,0x10 };
if (memcmp(headerRaw, wuxHeaderMagic, sizeof(wuxHeaderMagic)) == 0)
return CafeTitleFileType::WUX;
// check for RPX
uint8 rpxHeaderMagic[9] = { 0x7F,0x45,0x4C,0x46,0x01,0x02,0x01,0xCA,0xFE };
if (memcmp(headerRaw, rpxHeaderMagic, sizeof(rpxHeaderMagic)) == 0)
return CafeTitleFileType::RPX;
// check for ELF
uint8 elfHeaderMagic[9] = { 0x7F,0x45,0x4C,0x46,0x01,0x02,0x01,0x00,0x00 };
if (memcmp(headerRaw, elfHeaderMagic, sizeof(elfHeaderMagic)) == 0)
return CafeTitleFileType::ELF;
// check for WUD
uint8 wudMagic1[4] = { 0x57,0x55,0x50,0x2D }; // wud files should always start with "WUP-..."
uint8 wudMagic2[4] = { 0xCC,0x54,0x9E,0xB9 };
if (fileSize >= 0x10000)
{
uint8 magic1[4];
fs->SetPosition(0);
fs->readData(magic1, 4);
if (memcmp(magic1, wudMagic1, 4) == 0)
{
uint8 magic2[4];
fs->SetPosition(0x10000);
fs->readData(magic2, 4);
if (memcmp(magic2, wudMagic2, 4) == 0)
{
return CafeTitleFileType::WUD;
}
}
}
// check for WUA
// todo
return CafeTitleFileType::UNKNOWN;
}
TitleInfo::TitleInfo(const fs::path& path)
{
m_isValid = DetectFormat(path, m_fullPath, m_titleFormat);
if (!m_isValid)
m_titleFormat = TitleDataFormat::INVALID_STRUCTURE;
else
{
m_isValid = ParseXmlInfo();
}
if (m_isValid)
CalcUID();
}
TitleInfo::TitleInfo(const fs::path& path, std::string_view subPath)
{
// path must point to a (wua) file
if (!path.has_filename())
{
m_isValid = false;
return;
}
m_isValid = true;
m_titleFormat = TitleDataFormat::WIIU_ARCHIVE;
m_fullPath = path;
m_subPath = subPath;
m_isValid = ParseXmlInfo();
if (m_isValid)
CalcUID();
}
TitleInfo::TitleInfo(const TitleInfo::CachedInfo& cachedInfo)
{
m_cachedInfo = new CachedInfo(cachedInfo);
m_fullPath = cachedInfo.path;
m_subPath = cachedInfo.subPath;
m_titleFormat = cachedInfo.titleDataFormat;
// verify some parameters
m_isValid = false;
if (cachedInfo.titleDataFormat != TitleDataFormat::HOST_FS &&
cachedInfo.titleDataFormat != TitleDataFormat::WIIU_ARCHIVE &&
cachedInfo.titleDataFormat != TitleDataFormat::WUD &&
cachedInfo.titleDataFormat != TitleDataFormat::INVALID_STRUCTURE)
return;
if (cachedInfo.path.empty())
return;
if (cachedInfo.titleDataFormat == TitleDataFormat::WIIU_ARCHIVE && m_subPath.empty())
return; // for wua files the subpath must never be empty (title must not be stored in root of archive)
m_isValid = true;
CalcUID();
}
TitleInfo::~TitleInfo()
{
cemu_assert(m_mountpoints.empty());
delete m_parsedMetaXml;
delete m_parsedAppXml;
delete m_parsedCosXml;
delete m_cachedInfo;
}
TitleInfo::CachedInfo TitleInfo::MakeCacheEntry()
{
cemu_assert_debug(IsValid());
CachedInfo e;
e.titleDataFormat = m_titleFormat;
e.path = m_fullPath;
e.subPath = m_subPath;
e.titleId = GetAppTitleId();
e.titleVersion = GetAppTitleVersion();
e.titleName = GetTitleName();
e.region = GetMetaRegion();
e.group_id = GetAppGroup();
e.app_type = GetAppType();
return e;
}
// WUA can contain multiple titles. Root directory contains one directory for each title. The name must match: <titleId>_v<version>
bool TitleInfo::ParseWuaTitleFolderName(std::string_view name, TitleId& titleIdOut, uint16& titleVersionOut)
{
std::string_view sv = name;
if (sv.size() < 16 + 2)
return false;
TitleId parsedId;
if (!TitleIdParser::ParseFromStr(sv, parsedId))
return false;
sv.remove_prefix(16);
if (sv[0] != '_' || (sv[1] != 'v' && sv[1] != 'v'))
return false;
sv.remove_prefix(2);
if (sv.empty())
return false;
if (sv[0] == '0' && sv.size() != 1) // leading zero not allowed
return false;
uint32 v = 0;
while (!sv.empty())
{
uint8 c = sv[0];
sv.remove_prefix(1);
v *= 10;
if (c >= '0' && c <= '9')
v += (uint32)(c - '0');
else
{
v = 0xFFFFFFFF;
break;
}
}
if (v > 0xFFFF)
return false;
titleIdOut = parsedId;
titleVersionOut = v;
return true;
}
bool TitleInfo::DetectFormat(const fs::path& path, fs::path& pathOut, TitleDataFormat& formatOut)
{
std::error_code ec;
if (path.has_extension() && fs::is_regular_file(path, ec))
{
std::string filenameStr = _utf8Wrapper(path.filename());
if (boost::iends_with(filenameStr, ".rpx"))
{
// is in code folder?
fs::path parentPath = path.parent_path();
if (boost::iequals(_utf8Wrapper(parentPath.filename()), "code"))
{
parentPath = parentPath.parent_path();
// next to content and meta?
std::error_code ec;
if (fs::exists(parentPath / "content", ec) && fs::exists(parentPath / "meta", ec))
{
formatOut = TitleDataFormat::HOST_FS;
pathOut = parentPath;
return true;
}
}
}
else if (boost::iends_with(filenameStr, ".wud") ||
boost::iends_with(filenameStr, ".wux") ||
boost::iends_with(filenameStr, ".iso"))
{
formatOut = TitleDataFormat::WUD;
pathOut = path;
return true;
}
else if (boost::iends_with(filenameStr, ".wua"))
{
formatOut = TitleDataFormat::WIIU_ARCHIVE;
pathOut = path;
// a Wii U archive file can contain multiple titles but TitleInfo only maps to one
// we use the first base title that we find. This is the most intuitive behavior when someone launches "game.wua"
ZArchiveReader* zar = ZArchiveReader::OpenFromFile(path);
if (!zar)
return false;
ZArchiveNodeHandle rootDir = zar->LookUp("", false, true);
bool foundBase = false;
for (uint32 i = 0; i < zar->GetDirEntryCount(rootDir); i++)
{
ZArchiveReader::DirEntry dirEntry;
if (!zar->GetDirEntry(rootDir, i, dirEntry))
continue;
if (!dirEntry.isDirectory)
continue;
TitleId parsedId;
uint16 parsedVersion;
if (!TitleInfo::ParseWuaTitleFolderName(dirEntry.name, parsedId, parsedVersion))
continue;
TitleIdParser tip(parsedId);
TitleIdParser::TITLE_TYPE tt = tip.GetType();
if (tt == TitleIdParser::TITLE_TYPE::BASE_TITLE || tt == TitleIdParser::TITLE_TYPE::BASE_TITLE_DEMO ||
tt == TitleIdParser::TITLE_TYPE::SYSTEM_TITLE || tt == TitleIdParser::TITLE_TYPE::SYSTEM_OVERLAY_TITLE)
{
m_subPath = dirEntry.name;
foundBase = true;
break;
}
}
delete zar;
return foundBase;
}
// note: Since a Wii U archive file (.wua) contains multiple titles we shouldn't auto-detect them here
// instead TitleInfo has a second constructor which takes a subpath
// unable to determine type by extension, check contents
CafeTitleFileType fileType = DetermineCafeSystemFileType(path);
if (fileType == CafeTitleFileType::WUD ||
fileType == CafeTitleFileType::WUX)
{
formatOut = TitleDataFormat::WUD;
pathOut = path;
return true;
}
}
else
{
// does it point to the root folder of a title?
std::error_code ec;
if (fs::exists(path / "content", ec) && fs::exists(path / "meta", ec) && fs::exists(path / "code", ec))
{
formatOut = TitleDataFormat::HOST_FS;
pathOut = path;
return true;
}
}
return false;
}
bool TitleInfo::IsValid() const
{
return m_isValid;
}
fs::path TitleInfo::GetPath() const
{
if (!m_isValid)
{
cemu_assert_suspicious();
return {};
}
return m_fullPath;
}
void TitleInfo::CalcUID()
{
cemu_assert_debug(m_isValid);
if (!m_isValid)
{
m_uid = 0;
return;
}
std::error_code ec;
// get absolute normalized path
fs::path normalizedPath;
if (m_fullPath.is_relative())
{
normalizedPath = ActiveSettings::GetPath();
normalizedPath /= m_fullPath;
}
else
normalizedPath = m_fullPath;
normalizedPath = normalizedPath.lexically_normal();
uint64 h = fs::hash_value(normalizedPath);
// for WUA files also hash the subpath
if (m_titleFormat == TitleDataFormat::WIIU_ARCHIVE)
{
uint64 subHash = std::hash<std::string_view>{}(m_subPath);
h += subHash;
}
m_uid = h;
}
uint64 TitleInfo::GetUID()
{
cemu_assert_debug(m_isValid);
return m_uid;
}
std::mutex sZArchivePoolMtx;
std::map<fs::path, std::pair<uint32, ZArchiveReader*>> sZArchivePool;
ZArchiveReader* _ZArchivePool_AcquireInstance(const fs::path& path)
{
std::unique_lock _lock(sZArchivePoolMtx);
auto it = sZArchivePool.find(path);
if (it != sZArchivePool.end())
{
it->second.first++; // increment ref count
return it->second.second;
}
_lock.unlock();
// opening wua files can be expensive, so we do it outside of the lock
ZArchiveReader* zar = ZArchiveReader::OpenFromFile(path);
if (!zar)
return nullptr;
_lock.lock();
// check if another instance was allocated in the meantime
it = sZArchivePool.find(path);
if (it != sZArchivePool.end())
{
delete zar;
it->second.first++; // increment ref count
return it->second.second;
}
sZArchivePool.emplace(std::piecewise_construct,
std::forward_as_tuple(path),
std::forward_as_tuple(1, zar)
);
return zar;
}
void _ZArchivePool_ReleaseInstance(const fs::path& path, ZArchiveReader* zar)
{
std::unique_lock _lock(sZArchivePoolMtx);
auto it = sZArchivePool.find(path);
cemu_assert(it != sZArchivePool.end());
cemu_assert(it->second.second == zar);
it->second.first--; // decrement ref count
if (it->second.first == 0)
{
delete it->second.second;
sZArchivePool.erase(it);
}
}
bool TitleInfo::Mount(std::string_view virtualPath, std::string_view subfolder, sint32 mountPriority)
{
cemu_assert_debug(subfolder.empty() || (subfolder.front() != '/' || subfolder.front() != '\\')); // only relative subfolder allowed
cemu_assert(m_isValid);
if (m_titleFormat == TitleDataFormat::HOST_FS)
{
fs::path hostFSPath = m_fullPath;
hostFSPath.append(subfolder);
bool r = FSCDeviceHostFS_Mount(std::string(virtualPath).c_str(), boost::nowide::widen(_utf8Wrapper(hostFSPath)).c_str(), mountPriority);
cemu_assert_debug(r);
if (!r)
{
cemuLog_log(LogType::Force, "Failed to mount {} to {}", virtualPath, subfolder);
return false;
}
}
else if (m_titleFormat == TitleDataFormat::WUD)
{
if (m_mountpoints.empty())
{
cemu_assert_debug(!m_wudVolume);
m_wudVolume = FSTVolume::OpenFromDiscImage(m_fullPath);
}
if (!m_wudVolume)
return false;
bool r = FSCDeviceWUD_Mount(std::string(virtualPath).c_str(), subfolder, m_wudVolume, mountPriority);
cemu_assert_debug(r);
if (!r)
{
cemuLog_log(LogType::Force, "Failed to mount {} to {}", virtualPath, subfolder);
delete m_wudVolume;
return false;
}
}
else if (m_titleFormat == TitleDataFormat::WIIU_ARCHIVE)
{
if (!m_zarchive)
{
m_zarchive = _ZArchivePool_AcquireInstance(m_fullPath);
if (!m_zarchive)
return false;
}
bool r = FSCDeviceWUA_Mount(std::string(virtualPath).c_str(), std::string(m_subPath).append("/").append(subfolder), m_zarchive, mountPriority);
if (!r)
{
cemuLog_log(LogType::Force, "Failed to mount {} to {}", virtualPath, subfolder);
_ZArchivePool_ReleaseInstance(m_fullPath, m_zarchive);
return false;
}
}
else
{
cemu_assert_unimplemented();
}
m_mountpoints.emplace_back(mountPriority, virtualPath);
return true;
}
void TitleInfo::Unmount(std::string_view virtualPath)
{
for (auto& itr : m_mountpoints)
{
if (!boost::equals(itr.second, virtualPath))
continue;
fsc_unmount(itr.second.c_str(), itr.first);
std::erase(m_mountpoints, itr);
// if the last mount point got unmounted, delete any open devices
if (m_mountpoints.empty())
{
if (m_wudVolume)
{
cemu_assert_debug(m_titleFormat == TitleDataFormat::WUD);
delete m_wudVolume;
m_wudVolume = nullptr;
}
}
// wua files use reference counting
if (m_zarchive)
{
_ZArchivePool_ReleaseInstance(m_fullPath, m_zarchive);
if (m_mountpoints.empty())
m_zarchive = nullptr;
}
return;
}
cemu_assert_suspicious(); // unmount on unknown path
}
void TitleInfo::UnmountAll()
{
while (!m_mountpoints.empty())
Unmount(m_mountpoints.front().second);
}
std::atomic_uint64_t sTempMountingPathCounter = 1;
std::string TitleInfo::GetUniqueTempMountingPath()
{
uint64_t v = sTempMountingPathCounter.fetch_add(1);
return fmt::format("/internal/tempMount{:016x}/", v);
}
bool TitleInfo::ParseXmlInfo()
{
cemu_assert(m_isValid);
if (m_hasParsedXmlFiles)
return m_parsedMetaXml && m_parsedAppXml && m_parsedCosXml;
m_hasParsedXmlFiles = true;
std::string mountPath = GetUniqueTempMountingPath();
bool r = Mount(mountPath, "", FSC_PRIORITY_BASE);
if (!r)
return false;
// meta/meta.xml
auto xmlData = fsc_extractFile(fmt::format("{}meta/meta.xml", mountPath).c_str());
if(xmlData)
m_parsedMetaXml = ParsedMetaXml::Parse(xmlData->data(), xmlData->size());
// code/app.xml
xmlData = fsc_extractFile(fmt::format("{}code/app.xml", mountPath).c_str());
if(xmlData)
ParseAppXml(*xmlData);
// code/cos.xml
xmlData = fsc_extractFile(fmt::format("{}code/cos.xml", mountPath).c_str());
if (xmlData)
m_parsedCosXml = ParsedCosXml::Parse(xmlData->data(), xmlData->size());
Unmount(mountPath);
bool hasAnyXml = m_parsedMetaXml || m_parsedAppXml || m_parsedCosXml;
if (!m_parsedMetaXml || !m_parsedAppXml || !m_parsedCosXml)
{
if (hasAnyXml)
cemuLog_log(LogType::Force, "Title has missing meta .xml files. Title path: {}", _utf8Wrapper(m_fullPath));
delete m_parsedMetaXml;
delete m_parsedAppXml;
delete m_parsedCosXml;
m_parsedMetaXml = nullptr;
m_parsedAppXml = nullptr;
m_parsedCosXml = nullptr;
m_isValid = false;
return false;
}
m_isValid = true;
return true;
}
bool TitleInfo::ParseAppXml(std::vector<uint8>& appXmlData)
{
pugi::xml_document app_doc;
if (!app_doc.load_buffer_inplace(appXmlData.data(), appXmlData.size()))
return false;
const auto root = app_doc.child("app");
if (!root)
return false;
m_parsedAppXml = new ParsedAppXml();
for (const auto& child : root.children())
{
std::string_view name = child.name();
if (name == "title_version")
m_parsedAppXml->title_version = (uint16)std::stoull(child.text().as_string(), nullptr, 16);
else if (name == "title_id")
m_parsedAppXml->title_id = std::stoull(child.text().as_string(), nullptr, 16);
else if (name == "app_type")
m_parsedAppXml->app_type = (uint32)std::stoull(child.text().as_string(), nullptr, 16);
else if (name == "group_id")
m_parsedAppXml->group_id = (uint32)std::stoull(child.text().as_string(), nullptr, 16);
}
return true;
}
TitleId TitleInfo::GetAppTitleId() const
{
cemu_assert_debug(m_isValid);
if (m_parsedAppXml)
return m_parsedAppXml->title_id;
if (m_cachedInfo)
return m_cachedInfo->titleId;
cemu_assert_suspicious();
return 0;
}
uint16 TitleInfo::GetAppTitleVersion() const
{
cemu_assert_debug(m_isValid);
if (m_parsedAppXml)
return m_parsedAppXml->title_version;
if (m_cachedInfo)
return m_cachedInfo->titleVersion;
cemu_assert_suspicious();
return 0;
}
uint32 TitleInfo::GetAppGroup() const
{
cemu_assert_debug(m_isValid);
if (m_parsedAppXml)
return m_parsedAppXml->group_id;
if (m_cachedInfo)
return m_cachedInfo->group_id;
cemu_assert_suspicious();
return 0;
}
uint32 TitleInfo::GetAppType() const
{
cemu_assert_debug(m_isValid);
if (m_parsedAppXml)
return m_parsedAppXml->app_type;
if (m_cachedInfo)
return m_cachedInfo->app_type;
cemu_assert_suspicious();
return 0;
}
TitleIdParser::TITLE_TYPE TitleInfo::GetTitleType()
{
TitleIdParser tip(GetAppTitleId());
return tip.GetType();
}
std::string TitleInfo::GetTitleName() const
{
cemu_assert_debug(m_isValid);
if (m_parsedMetaXml)
return m_parsedMetaXml->GetShortName(CafeConsoleLanguage::EN);
if (m_cachedInfo)
return m_cachedInfo->titleName;
cemu_assert_suspicious();
return "";
}
CafeConsoleRegion TitleInfo::GetMetaRegion() const
{
cemu_assert_debug(m_isValid);
if (m_parsedMetaXml)
return m_parsedMetaXml->GetRegion();
if (m_cachedInfo)
return m_cachedInfo->region;
cemu_assert_suspicious();
return CafeConsoleRegion::JPN;
}
std::string TitleInfo::GetArgStr() const
{
cemu_assert_debug(m_parsedCosXml);
if (!m_parsedCosXml)
return "";
return m_parsedCosXml->argstr;
}
std::string TitleInfo::GetPrintPath() const
{
if (!m_isValid)
return "invalid";
std::string tmp;
tmp.append(_utf8Wrapper(m_fullPath));
switch (m_titleFormat)
{
case TitleDataFormat::HOST_FS:
tmp.append(" [Folder]");
break;
case TitleDataFormat::WUD:
tmp.append(" [WUD]");
break;
case TitleDataFormat::WIIU_ARCHIVE:
tmp.append(" [WUA]");
break;
default:
break;
}
if (m_titleFormat == TitleDataFormat::WIIU_ARCHIVE)
tmp.append(fmt::format(" [{}]", m_subPath));
return tmp;
}
std::string TitleInfo::GetInstallPath() const
{
TitleId titleId = GetAppTitleId();
TitleIdParser tip(titleId);
std::string tmp;
if (tip.IsSystemTitle())
tmp = fmt::format("sys\\title\\{:08x}\\{:08x}", GetTitleIdHigh(titleId), GetTitleIdLow(titleId));
else
tmp = fmt::format("usr\\title\\{:08x}\\{:08x}", GetTitleIdHigh(titleId), GetTitleIdLow(titleId));
return tmp;
}

View file

@ -0,0 +1,190 @@
#pragma once
#include "Cafe/Filesystem/fsc.h"
#include "config/CemuConfig.h" // for CafeConsoleRegion. Move to NCrypto?
#include "TitleId.h"
#include "ParsedMetaXml.h"
enum class CafeTitleFileType
{
UNKNOWN,
WUD,
WUX,
RPX,
ELF,
};
CafeTitleFileType DetermineCafeSystemFileType(fs::path filePath);
struct ParsedAppXml
{
uint64 title_id;
uint16 title_version;
uint32 app_type;
uint32 group_id;
};
struct ParsedCosXml
{
std::string argstr;
static ParsedCosXml* Parse(uint8* xmlData, size_t xmlLen)
{
pugi::xml_document app_doc;
if (!app_doc.load_buffer_inplace(xmlData, xmlLen))
return nullptr;
const auto root = app_doc.child("app");
if (!root)
return nullptr;
ParsedCosXml* parsedCos = new ParsedCosXml();
for (const auto& child : root.children())
{
std::string_view name = child.name();
if (name == "argstr")
parsedCos->argstr = child.text().as_string();
}
return parsedCos;
}
};
class TitleInfo
{
public:
enum class TitleDataFormat
{
HOST_FS = 1, // host filesystem directory (fullPath points to root with content/code/meta subfolders)
WUD = 2, // WUD or WUX
WIIU_ARCHIVE = 3, // Wii U compressed single-file archive (.wua)
// error
INVALID_STRUCTURE = 0,
};
struct CachedInfo
{
TitleDataFormat titleDataFormat;
fs::path path;
std::string subPath; // for WUA
uint64 titleId;
uint16 titleVersion;
std::string titleName;
CafeConsoleRegion region;
uint32 group_id;
uint32 app_type;
};
TitleInfo() : m_isValid(false) {};
TitleInfo(const fs::path& path);
TitleInfo(const fs::path& path, std::string_view subPath);
TitleInfo(const CachedInfo& cachedInfo);
~TitleInfo();
TitleInfo(const TitleInfo& other)
{
Copy(other);
}
TitleInfo& operator=(TitleInfo other)
{
Copy(other);
return *this;
}
bool IsCached() { return m_cachedInfo; }; // returns true if this TitleInfo was loaded from cache and has not yet been parsed
CachedInfo MakeCacheEntry();
bool IsValid() const;
uint64 GetUID(); // returns a unique identifier derived from the absolute canonical title location which can be used to identify this title by its location. May not persist across sessions, especially when Cemu is used portable
fs::path GetPath() const;
TitleDataFormat GetFormat() const { return m_titleFormat; };
bool Mount(std::string_view virtualPath, std::string_view subfolder, sint32 mountPriority);
void Unmount(std::string_view virtualPath);
void UnmountAll();
bool IsMounted() const { return !m_mountpoints.empty(); }
bool ParseXmlInfo();
bool HasValidXmlInfo() const { return m_parsedMetaXml && m_parsedAppXml && m_parsedCosXml; };
bool IsEqualByLocation(const TitleInfo& rhs) const
{
return m_uid == rhs.m_uid;
}
// API which requires parsed meta data or cached info
TitleId GetAppTitleId() const; // from app.xml
uint16 GetAppTitleVersion() const; // from app.xml
uint32 GetAppGroup() const; // from app.xml
uint32 GetAppType() const; // from app.xml
std::string GetTitleName() const; // from meta.xml
CafeConsoleRegion GetMetaRegion() const; // from meta.xml
// cos.xml
std::string GetArgStr() const;
// meta.xml also contains a version which seems to match the one from app.xml
// the titleId in meta.xml seems to be the title id of the base game for updates specifically. For AOC content it's the AOC's titleId
TitleIdParser::TITLE_TYPE GetTitleType();
ParsedMetaXml* GetMetaInfo()
{
return m_parsedMetaXml;
}
std::string GetPrintPath() const; // formatted path for log writing
std::string GetInstallPath() const; // installation subpath, relative to storage base. E.g. "usr/title/.../..." or "sys/title/.../..."
static std::string GetUniqueTempMountingPath();
static bool ParseWuaTitleFolderName(std::string_view name, TitleId& titleIdOut, uint16& titleVersionOut);
private:
void Copy(const TitleInfo& other)
{
m_isValid = other.m_isValid;
m_titleFormat = other.m_titleFormat;
m_fullPath = other.m_fullPath;
m_subPath = other.m_subPath;
m_hasParsedXmlFiles = other.m_hasParsedXmlFiles;
m_parsedMetaXml = nullptr;
m_parsedAppXml = nullptr;
if (other.m_parsedMetaXml)
m_parsedMetaXml = new ParsedMetaXml(*other.m_parsedMetaXml);
if (other.m_parsedAppXml)
m_parsedAppXml = new ParsedAppXml(*other.m_parsedAppXml);
if (other.m_parsedCosXml)
m_parsedCosXml = new ParsedCosXml(*other.m_parsedCosXml);
if (other.m_cachedInfo)
m_cachedInfo = new CachedInfo(*other.m_cachedInfo);
m_mountpoints.clear();
m_wudVolume = nullptr;
}
bool DetectFormat(const fs::path& path, fs::path& pathOut, TitleDataFormat& formatOut);
void CalcUID();
bool ParseAppXml(std::vector<uint8>& appXmlData);
bool m_isValid{ false };
TitleDataFormat m_titleFormat{ TitleDataFormat::INVALID_STRUCTURE };
fs::path m_fullPath;
std::string m_subPath; // used for formats where fullPath isn't unique on its own (like WUA)
uint64 m_uid{};
// mounting info
std::vector<std::pair<sint32, std::string>> m_mountpoints;
class FSTVolume* m_wudVolume{};
class ZArchiveReader* m_zarchive{};
// xml info
bool m_hasParsedXmlFiles{ false };
ParsedMetaXml* m_parsedMetaXml{};
ParsedAppXml* m_parsedAppXml{};
ParsedCosXml* m_parsedCosXml{};
// cached info if called with cache constructor
CachedInfo* m_cachedInfo{nullptr};
};

View file

@ -0,0 +1,678 @@
#include "TitleList.h"
#include "Common/filestream.h"
#include "util/helpers/helpers.h"
#include <zarchive/zarchivereader.h>
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
std::mutex sTLMutex;
std::vector<TitleInfo*> sTLList;
std::vector<TitleInfo*> sTLListPending;
std::unordered_multimap<uint64, TitleInfo*> sTLMap;
bool sTLCacheDirty{false};
// paths
fs::path sTLMLCPath;
std::vector<fs::path> sTLScanPaths;
// worker
std::thread sTLRefreshWorker;
bool sTLRefreshWorkerActive{false};
std::atomic_uint32_t sTLRefreshRequests{};
std::atomic_bool sTLIsScanMandatory{ false };
// callback list
struct TitleListCallbackEntry
{
TitleListCallbackEntry(void(*cb)(CafeTitleListCallbackEvent* evt, void* ctx), void* ctx, uint64 uniqueId) :
cb(cb), ctx(ctx), uniqueId(uniqueId) {};
void (*cb)(CafeTitleListCallbackEvent* evt, void* ctx);
void* ctx;
uint64 uniqueId;
};
std::vector<TitleListCallbackEntry> sTLCallbackList;
void CafeTitleList::Initialize(const fs::path cacheXmlFile)
{
std::unique_lock _lock(sTLMutex);
sTLInitialized = true;
sTLCacheFilePath = cacheXmlFile;
LoadCacheFile();
}
void CafeTitleList::LoadCacheFile()
{
sTLIsScanMandatory = true;
cemu_assert_debug(sTLInitialized);
cemu_assert_debug(sTLList.empty());
auto xmlData = FileStream::LoadIntoMemory(sTLCacheFilePath);
if (!xmlData)
return;
pugi::xml_document doc;
if (!doc.load_buffer_inplace(xmlData->data(), xmlData->size(), pugi::parse_default, pugi::xml_encoding::encoding_utf8))
return;
auto titleListNode = doc.child("title_list");
pugi::xml_node itNode = titleListNode.first_child();
for (const auto& titleInfoNode : doc.child("title_list"))
{
TitleId titleId;
if( !TitleIdParser::ParseFromStr(titleInfoNode.attribute("titleId").as_string(), titleId))
continue;
uint16 titleVersion = titleInfoNode.attribute("version").as_uint();
TitleInfo::TitleDataFormat format = (TitleInfo::TitleDataFormat)ConvertString<uint32>(titleInfoNode.child_value("format"));
CafeConsoleRegion region = (CafeConsoleRegion)ConvertString<uint32>(titleInfoNode.child_value("region"));
std::string name = titleInfoNode.child_value("name");
std::string path = titleInfoNode.child_value("path");
std::string sub_path = titleInfoNode.child_value("sub_path");
uint32 group_id = ConvertString<uint32>(titleInfoNode.attribute("group_id").as_string(), 16);
uint32 app_type = ConvertString<uint32>(titleInfoNode.attribute("app_type").as_string(), 16);
TitleInfo::CachedInfo cacheEntry;
cacheEntry.titleId = titleId;
cacheEntry.titleVersion = titleVersion;
cacheEntry.titleDataFormat = format;
cacheEntry.region = region;
cacheEntry.titleName = name;
cacheEntry.path = _asUtf8(path);
cacheEntry.subPath = sub_path;
cacheEntry.group_id = group_id;
cacheEntry.app_type = app_type;
TitleInfo* ti = new TitleInfo(cacheEntry);
if (!ti->IsValid())
{
cemuLog_log(LogType::Force, "Title cache contained invalid title");
delete ti;
continue;
}
AddTitle(ti);
}
sTLIsScanMandatory = false;
}
void CafeTitleList::StoreCacheFile()
{
cemu_assert_debug(sTLInitialized);
if (sTLCacheFilePath.empty())
return;
std::unique_lock _lock(sTLMutex);
pugi::xml_document doc;
auto declarationNode = doc.append_child(pugi::node_declaration);
declarationNode.append_attribute("version") = "1.0";
declarationNode.append_attribute("encoding") = "UTF-8";
auto title_list_node = doc.append_child("title_list");
for (auto& tiIt : sTLList)
{
TitleInfo::CachedInfo info = tiIt->MakeCacheEntry();
auto titleInfoNode = title_list_node.append_child("title");
titleInfoNode.append_attribute("titleId").set_value(fmt::format("{:016x}", info.titleId).c_str());
titleInfoNode.append_attribute("version").set_value(fmt::format("{:}", info.titleVersion).c_str());
titleInfoNode.append_attribute("group_id").set_value(fmt::format("{:08x}", info.group_id).c_str());
titleInfoNode.append_attribute("app_type").set_value(fmt::format("{:08x}", info.app_type).c_str());
titleInfoNode.append_child("region").append_child(pugi::node_pcdata).set_value(fmt::format("{}", (uint32)info.region).c_str());
titleInfoNode.append_child("name").append_child(pugi::node_pcdata).set_value(info.titleName.c_str());
titleInfoNode.append_child("format").append_child(pugi::node_pcdata).set_value(fmt::format("{}", (uint32)info.titleDataFormat).c_str());
titleInfoNode.append_child("path").append_child(pugi::node_pcdata).set_value(_utf8Wrapper(info.path).c_str());
if(!info.subPath.empty())
titleInfoNode.append_child("sub_path").append_child(pugi::node_pcdata).set_value(_utf8Wrapper(info.subPath).c_str());
}
fs::path tmpPath = fs::path(sTLCacheFilePath.parent_path()).append(fmt::format("{}__tmp", _utf8Wrapper(sTLCacheFilePath.filename())));
std::ofstream fileOut(tmpPath, std::ios::out | std::ios::binary | std::ios::trunc);
if (!fileOut.is_open())
{
cemuLog_log(LogType::Force, "Unable to store title list in {}", _utf8Wrapper(tmpPath));
return;
}
doc.save(fileOut, " ", 1, pugi::xml_encoding::encoding_utf8);
fileOut.flush();
fileOut.close();
std::error_code ec;
fs::rename(tmpPath, sTLCacheFilePath, ec);
}
void CafeTitleList::ClearScanPaths()
{
std::unique_lock _lock(sTLMutex);
sTLScanPaths.clear();
}
void CafeTitleList::AddScanPath(fs::path path)
{
std::unique_lock _lock(sTLMutex);
sTLScanPaths.emplace_back(path);
}
void CafeTitleList::SetMLCPath(fs::path path)
{
std::unique_lock _lock(sTLMutex);
std::error_code ec;
if (!fs::is_directory(path, ec))
{
cemuLog_log(LogType::Force, "MLC set to invalid path: {}", _utf8Wrapper(path));
return;
}
sTLMLCPath = path;
}
void CafeTitleList::Refresh()
{
std::unique_lock _lock(sTLMutex);
cemu_assert_debug(sTLInitialized);
sTLRefreshRequests++;
if (!sTLRefreshWorkerActive)
{
if (sTLRefreshWorker.joinable())
sTLRefreshWorker.join();
sTLRefreshWorkerActive = true;
sTLRefreshWorker = std::thread(RefreshWorkerThread);
}
sTLIsScanMandatory = false;
}
bool CafeTitleList::IsScanning()
{
std::unique_lock _lock(sTLMutex);
return sTLRefreshWorkerActive;
}
void CafeTitleList::WaitForMandatoryScan()
{
if (!sTLIsScanMandatory)
return;
while (IsScanning())
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
void _RemoveTitleFromMultimap(TitleInfo* titleInfo)
{
auto mapRange = sTLMap.equal_range(titleInfo->GetAppTitleId());
for (auto mapIt = mapRange.first; mapIt != mapRange.second; ++mapIt)
{
if (mapIt->second == titleInfo)
{
sTLMap.erase(mapIt);
return;
}
}
cemu_assert_suspicious();
}
// check if path is a valid title and if it is, permanently add it to the title list
// in the special case that path points to a WUA file, all contained titles will be added
void CafeTitleList::AddTitleFromPath(fs::path path)
{
if (path.has_extension() && boost::iequals(_utf8Wrapper(path.extension()), ".wua"))
{
ZArchiveReader* zar = ZArchiveReader::OpenFromFile(path);
if (!zar)
{
cemuLog_log(LogType::Force, "Found {} but it is not a valid Wii U archive file", _utf8Wrapper(path));
return;
}
// enumerate all contained titles
ZArchiveNodeHandle rootDir = zar->LookUp("", false, true);
cemu_assert(rootDir != ZARCHIVE_INVALID_NODE);
for (uint32 i = 0; i < zar->GetDirEntryCount(rootDir); i++)
{
ZArchiveReader::DirEntry dirEntry;
if( !zar->GetDirEntry(rootDir, i, dirEntry) )
continue;
if(!dirEntry.isDirectory)
continue;
TitleId parsedId;
uint16 parsedVersion;
if (!TitleInfo::ParseWuaTitleFolderName(dirEntry.name, parsedId, parsedVersion))
{
cemuLog_log(LogType::Force, "Invalid title directory in {}: \"{}\"", _utf8Wrapper(path), dirEntry.name);
continue;
}
// valid subdirectory
TitleInfo* titleInfo = new TitleInfo(path, dirEntry.name);
if (titleInfo->IsValid())
AddDiscoveredTitle(titleInfo);
else
delete titleInfo;
}
delete zar;
return;
}
TitleInfo* titleInfo = new TitleInfo(path);
if (titleInfo->IsValid())
AddDiscoveredTitle(titleInfo);
else
delete titleInfo;
}
bool CafeTitleList::RefreshWorkerThread()
{
while (sTLRefreshRequests.load())
{
sTLRefreshRequests.store(0);
// create copies of all the paths
sTLMutex.lock();
fs::path mlcPath = sTLMLCPath;
std::vector<fs::path> gamePaths = sTLScanPaths;
// remember the current list of known titles
// during the scanning process we will erase matches from the pending list
// at the end of scanning, we can then use this list to identify and remove any titles that are no longer discoverable
sTLListPending = sTLList;
sTLMutex.unlock();
// scan game paths
for (auto& it : gamePaths)
ScanGamePath(it);
// scan MLC
if (!mlcPath.empty())
{
std::error_code ec;
for (auto& it : fs::directory_iterator(mlcPath / "usr/title", ec))
{
if (!it.is_directory(ec))
continue;
ScanMLCPath(it.path());
}
ScanMLCPath(mlcPath / "sys/title/00050010");
ScanMLCPath(mlcPath / "sys/title/00050030");
}
// remove any titles that are still pending
for (auto& itPending : sTLListPending)
{
_RemoveTitleFromMultimap(itPending);
std::erase(sTLList, itPending);
}
// send notifications for removed titles, but only if there exists no other title with the same titleId and version
if (!sTLListPending.empty())
sTLCacheDirty = true;
for (auto& itPending : sTLListPending)
{
CafeTitleListCallbackEvent evt;
evt.eventType = CafeTitleListCallbackEvent::TYPE::TITLE_REMOVED;
evt.titleInfo = itPending;
for (auto& it : sTLCallbackList)
it.cb(&evt, it.ctx);
delete itPending;
}
sTLListPending.clear();
}
sTLMutex.lock();
sTLRefreshWorkerActive = false;
// send notification that scanning finished
CafeTitleListCallbackEvent evt;
evt.eventType = CafeTitleListCallbackEvent::TYPE::SCAN_FINISHED;
evt.titleInfo = nullptr;
for (auto& it : sTLCallbackList)
it.cb(&evt, it.ctx);
sTLMutex.unlock();
if (sTLCacheDirty)
{
StoreCacheFile();
sTLCacheDirty = false;
}
return true;
}
bool _IsKnownFileExtension(std::string fileExtension)
{
for (auto& it : fileExtension)
if (it >= 'A' && it <= 'Z')
it -= ('A' - 'a');
return
fileExtension == ".wud" ||
fileExtension == ".wux" ||
fileExtension == ".iso" ||
fileExtension == ".wua";
// note: To detect extracted titles with RPX we use the content/code/meta folder structure
}
void CafeTitleList::ScanGamePath(const fs::path& path)
{
// scan the whole directory first to determine if this is a title folder
std::vector<fs::path> filesInDirectory;
std::vector<fs::path> dirsInDirectory;
bool hasContentFolder = false, hasCodeFolder = false, hasMetaFolder = false;
std::error_code ec;
for (auto& it : fs::directory_iterator(path, ec))
{
if (it.is_regular_file(ec))
{
filesInDirectory.emplace_back(it.path());
}
else if (it.is_directory(ec))
{
dirsInDirectory.emplace_back(it.path());
std::string dirName = _utf8Wrapper(it.path().filename());
if (boost::iequals(dirName, "content"))
hasContentFolder = true;
else if (boost::iequals(dirName, "code"))
hasCodeFolder = true;
else if (boost::iequals(dirName, "meta"))
hasMetaFolder = true;
}
}
// always check individual files
for (auto& it : filesInDirectory)
{
// since checking files is slow, we only do it for known file extensions
if (!it.has_extension())
continue;
if (!_IsKnownFileExtension(_utf8Wrapper(it.extension())))
continue;
AddTitleFromPath(it);
}
// is the current directory a title folder?
if (hasContentFolder && hasCodeFolder && hasMetaFolder)
{
// verify if this folder is a valid title
TitleInfo* titleInfo = new TitleInfo(path);
if (titleInfo->IsValid())
AddDiscoveredTitle(titleInfo);
else
delete titleInfo;
// if there are other folders besides content/code/meta then traverse those
if (dirsInDirectory.size() > 3)
{
for (auto& it : dirsInDirectory)
{
std::string dirName = _utf8Wrapper(it.filename());
if (!boost::iequals(dirName, "content") &&
!boost::iequals(dirName, "code") &&
!boost::iequals(dirName, "meta"))
ScanGamePath(it);
}
}
}
else
{
// scan subdirectories
for (auto& it : dirsInDirectory)
ScanGamePath(it);
}
}
void CafeTitleList::ScanMLCPath(const fs::path& path)
{
std::error_code ec;
for (auto& it : fs::directory_iterator(path, ec))
{
if (!it.is_directory())
continue;
// only scan directories which match the title id naming scheme
std::string dirName = _utf8Wrapper(it.path().filename());
if(dirName.size() != 8)
continue;
bool containsNoHexCharacter = false;
for (auto& it : dirName)
{
if(it >= 'A' && it <= 'F' ||
it >= 'a' && it <= 'f' ||
it >= '0' && it <= '9')
continue;
containsNoHexCharacter = true;
break;
}
if(containsNoHexCharacter)
continue;
if (fs::is_directory(it.path() / "code", ec) &&
fs::is_directory(it.path() / "content", ec) &&
fs::is_directory(it.path() / "meta", ec))
{
TitleInfo* titleInfo = new TitleInfo(it);
if (titleInfo->IsValid() && titleInfo->ParseXmlInfo())
AddDiscoveredTitle(titleInfo);
else
delete titleInfo;
}
}
}
void CafeTitleList::AddDiscoveredTitle(TitleInfo* titleInfo)
{
cemu_assert_debug(titleInfo->ParseXmlInfo());
std::unique_lock _lock(sTLMutex);
// remove from pending list
auto pendingIt = std::find_if(sTLListPending.begin(), sTLListPending.end(), [titleInfo](const TitleInfo* it) { return it->IsEqualByLocation(*titleInfo); });
if (pendingIt != sTLListPending.end())
sTLListPending.erase(pendingIt);
AddTitle(titleInfo);
}
void CafeTitleList::AddTitle(TitleInfo* titleInfo)
{
// check if title is already known
if (titleInfo->IsCached())
{
bool isKnown = std::any_of(sTLList.cbegin(), sTLList.cend(), [&titleInfo](const TitleInfo* ti) { return titleInfo->IsEqualByLocation(*ti); });
if (isKnown)
{
delete titleInfo;
return;
}
}
else
{
auto it = std::find_if(sTLList.begin(), sTLList.end(), [titleInfo](const TitleInfo* it) { return it->IsEqualByLocation(*titleInfo); });
if (it != sTLList.end())
{
if ((*it)->IsCached())
{
// replace cached entry with newly parsed title
TitleInfo* deletedInfo = *it;
sTLList.erase(it);
_RemoveTitleFromMultimap(deletedInfo);
delete deletedInfo;
}
else
{
// title already known
delete titleInfo;
return;
}
}
}
sTLList.emplace_back(titleInfo);
sTLMap.insert(std::pair(titleInfo->GetAppTitleId(), titleInfo));
// send out notification
CafeTitleListCallbackEvent evt;
evt.eventType = CafeTitleListCallbackEvent::TYPE::TITLE_DISCOVERED;
evt.titleInfo = titleInfo;
for (auto& it : sTLCallbackList)
it.cb(&evt, it.ctx);
sTLCacheDirty = true;
}
uint64 CafeTitleList::RegisterCallback(void(*cb)(CafeTitleListCallbackEvent* evt, void* ctx), void* ctx)
{
static std::atomic<uint64_t> sCallbackIdGen = 1;
uint64 id = sCallbackIdGen.fetch_add(1);
std::unique_lock _lock(sTLMutex);
sTLCallbackList.emplace_back(cb, ctx, id);
// immediately notify of all known titles
for (auto& it : sTLList)
{
CafeTitleListCallbackEvent evt;
evt.eventType = CafeTitleListCallbackEvent::TYPE::TITLE_DISCOVERED;
evt.titleInfo = it;
cb(&evt, ctx);
}
// if not scanning then send out scan finished notification
if (!sTLRefreshWorkerActive)
{
CafeTitleListCallbackEvent evt;
evt.eventType = CafeTitleListCallbackEvent::TYPE::SCAN_FINISHED;
evt.titleInfo = nullptr;
for (auto& it : sTLCallbackList)
it.cb(&evt, it.ctx);
}
return id;
}
void CafeTitleList::UnregisterCallback(uint64 id)
{
std::unique_lock _lock(sTLMutex);
auto it = std::find_if(sTLCallbackList.begin(), sTLCallbackList.end(), [id](auto& e) { return e.uniqueId == id; });
cemu_assert(it != sTLCallbackList.end()); // must be a valid callback
sTLCallbackList.erase(it);
}
bool CafeTitleList::HasTitle(TitleId titleId, uint16& versionOut)
{
// todo - optimize?
bool matchFound = false;
versionOut = 0;
std::unique_lock _lock(sTLMutex);
for (auto& it : sTLList)
{
if (it->GetAppTitleId() == titleId)
{
uint16 titleVersion = it->GetAppTitleVersion();
if (titleVersion > versionOut)
versionOut = titleVersion;
matchFound = true;
}
}
return matchFound;
}
bool CafeTitleList::HasTitleAndVersion(TitleId titleId, uint16 version)
{
std::unique_lock _lock(sTLMutex);
for (auto& it : sTLList)
{
if (it->GetAppTitleId() == titleId && it->GetAppTitleVersion() == version)
return true;
}
return false;
}
std::vector<TitleId> CafeTitleList::GetAllTitleIds()
{
std::unordered_set<TitleId> visitedTitleIds;
std::unique_lock _lock(sTLMutex);
std::vector<TitleId> titleIds;
titleIds.reserve(sTLList.size());
for (auto& it : sTLList)
{
TitleId tid = it->GetAppTitleId();
if (visitedTitleIds.find(tid) != visitedTitleIds.end())
continue;
titleIds.emplace_back(tid);
visitedTitleIds.emplace(tid);
}
return titleIds;
}
std::span<TitleInfo*> CafeTitleList::AcquireInternalList()
{
sTLMutex.lock();
return { sTLList.data(), sTLList.size() };
}
void CafeTitleList::ReleaseInternalList()
{
sTLMutex.unlock();
}
bool CafeTitleList::GetFirstByTitleId(TitleId titleId, TitleInfo& titleInfoOut)
{
std::unique_lock _lock(sTLMutex);
auto it = sTLMap.find(titleId);
if (it != sTLMap.end())
{
cemu_assert_debug(it->first == titleId);
titleInfoOut = *it->second;
return true;
}
return false;
}
// takes update or AOC title id and returns the title id of the associated base title
// this can fail if trying to translate an AOC title id without having the base title meta information
bool CafeTitleList::FindBaseTitleId(TitleId titleId, TitleId& titleIdBaseOut)
{
titleId = TitleIdParser::MakeBaseTitleId(titleId);
// aoc to base
// todo - this requires scanning all base titles and their updates to see if they reference this title id
// for now we assume there is a direct match of ids
if (((titleId >> 32) & 0xFF) == 0x0C)
{
titleId &= ~0xFF00000000;
titleId |= 0x0000000000;
}
titleIdBaseOut = titleId;
return true;
}
GameInfo2 CafeTitleList::GetGameInfo(TitleId titleId)
{
GameInfo2 gameInfo;
// find base title id
uint64 baseTitleId;
if (!FindBaseTitleId(titleId, baseTitleId))
{
cemuLog_logDebug(LogType::Force, "Failed to translate title id in GetGameInfo()");
return gameInfo;
}
// determine if an optional update title id exists
TitleIdParser tip(baseTitleId);
bool hasSeparateUpdateTitleId = tip.CanHaveSeparateUpdateTitleId();
uint64 updateTitleId = 0;
if (hasSeparateUpdateTitleId)
updateTitleId = tip.GetSeparateUpdateTitleId();
// scan the title list for base and update
std::unique_lock _lock(sTLMutex);
for (auto& it : sTLList)
{
TitleId appTitleId = it->GetAppTitleId();
if (appTitleId == baseTitleId)
gameInfo.SetBase(*it);
if (hasSeparateUpdateTitleId && appTitleId == updateTitleId)
{
gameInfo.SetUpdate(*it);
}
}
// if this title can have AOC content then do a second scan
// todo - get a list of all AOC title ids from the base/update meta information
// for now we assume there is a direct match between the base titleId and the aoc titleId
if (tip.CanHaveSeparateUpdateTitleId())
{
uint64 aocTitleId = baseTitleId | 0xC00000000;
for (auto& it : sTLList)
{
TitleId appTitleId = it->GetAppTitleId();
if (appTitleId == aocTitleId)
{
gameInfo.AddAOC(*it); // stores the AOC with the highest title version
}
}
}
return gameInfo;
}
TitleInfo CafeTitleList::GetTitleInfoByUID(uint64 uid)
{
TitleInfo titleInfo;
std::unique_lock _lock(sTLMutex);
for (auto& it : sTLList)
{
if (it->GetUID() == uid)
{
titleInfo = *it;
break;
}
}
return titleInfo;
}

View file

@ -0,0 +1,57 @@
#pragma once
#include "TitleInfo.h"
#include "GameInfo.h"
struct CafeTitleListCallbackEvent
{
enum class TYPE
{
TITLE_DISCOVERED,
TITLE_REMOVED,
SCAN_FINISHED,
};
TYPE eventType;
TitleInfo* titleInfo;
};
class CafeTitleList
{
public:
static void Initialize(const fs::path cacheXmlFile);
static void LoadCacheFile();
static void StoreCacheFile();
static void ClearScanPaths();
static void AddScanPath(fs::path path);
static void SetMLCPath(fs::path path);
static void Refresh(); // scan all paths
static bool IsScanning(); // returns true if async refresh is currently active
static void WaitForMandatoryScan(); // wait for current scan result if no cached info is available
static void AddTitleFromPath(fs::path path);
static uint64 RegisterCallback(void(*cb)(CafeTitleListCallbackEvent* evt, void* ctx), void* ctx); // on register, the callback will be invoked for every already known title
static void UnregisterCallback(uint64 id);
// utility functions
static bool HasTitle(TitleId titleId, uint16& versionOut);
static bool HasTitleAndVersion(TitleId titleId, uint16 version);
static std::vector<TitleId> GetAllTitleIds();
static bool GetFirstByTitleId(TitleId titleId, TitleInfo& titleInfoOut);
static bool FindBaseTitleId(TitleId titleId, TitleId& titleIdBaseOut);
static std::span<TitleInfo*> AcquireInternalList();
static void ReleaseInternalList();
static GameInfo2 GetGameInfo(TitleId titleId);
static TitleInfo GetTitleInfoByUID(uint64 uid);
private:
static bool RefreshWorkerThread();
static void ScanGamePath(const fs::path& path);
static void ScanMLCPath(const fs::path& path);
static void AddDiscoveredTitle(TitleInfo* titleInfo);
static void AddTitle(TitleInfo* titleInfo);
};