mirror of
https://github.com/cemu-project/Cemu.git
synced 2025-07-14 18:58:29 +12:00
Add all the files
This commit is contained in:
parent
e3db07a16a
commit
d60742f52b
1445 changed files with 430238 additions and 0 deletions
68
src/Cafe/TitleList/BaseInfo.cpp
Normal file
68
src/Cafe/TitleList/BaseInfo.cpp
Normal 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;
|
||||
}
|
||||
|
37
src/Cafe/TitleList/BaseInfo.h
Normal file
37
src/Cafe/TitleList/BaseInfo.h
Normal 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);
|
||||
};
|
121
src/Cafe/TitleList/GameInfo.h
Normal file
121
src/Cafe/TitleList/GameInfo.h
Normal 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;
|
||||
};
|
180
src/Cafe/TitleList/MetaInfo.cpp
Normal file
180
src/Cafe/TitleList/MetaInfo.cpp
Normal 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;
|
||||
}
|
61
src/Cafe/TitleList/MetaInfo.h
Normal file
61
src/Cafe/TitleList/MetaInfo.h
Normal 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);
|
||||
};
|
||||
|
140
src/Cafe/TitleList/ParsedMetaXml.h
Normal file
140
src/Cafe/TitleList/ParsedMetaXml.h
Normal 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;
|
||||
}
|
||||
};
|
34
src/Cafe/TitleList/SaveInfo.cpp
Normal file
34
src/Cafe/TitleList/SaveInfo.cpp
Normal 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;
|
||||
}
|
30
src/Cafe/TitleList/SaveInfo.h
Normal file
30
src/Cafe/TitleList/SaveInfo.h
Normal 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};
|
||||
};
|
177
src/Cafe/TitleList/SaveList.cpp
Normal file
177
src/Cafe/TitleList/SaveList.cpp
Normal 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 {};
|
||||
}
|
33
src/Cafe/TitleList/SaveList.h
Normal file
33
src/Cafe/TitleList/SaveList.h
Normal 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);
|
||||
};
|
125
src/Cafe/TitleList/TitleId.h
Normal file
125
src/Cafe/TitleList/TitleId.h
Normal 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;
|
||||
};
|
654
src/Cafe/TitleList/TitleInfo.cpp
Normal file
654
src/Cafe/TitleList/TitleInfo.cpp
Normal 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;
|
||||
}
|
190
src/Cafe/TitleList/TitleInfo.h
Normal file
190
src/Cafe/TitleList/TitleInfo.h
Normal 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};
|
||||
};
|
678
src/Cafe/TitleList/TitleList.cpp
Normal file
678
src/Cafe/TitleList/TitleList.cpp
Normal 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;
|
||||
}
|
57
src/Cafe/TitleList/TitleList.h
Normal file
57
src/Cafe/TitleList/TitleList.h
Normal 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);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue