Add support for games in NUS format (.app)

Requires title.tmd and title.tik in same directory
This commit is contained in:
Exzap 2023-09-27 11:05:40 +02:00
parent f9f6206929
commit 5ad57bb0c9
8 changed files with 54 additions and 24 deletions

View File

@ -78,13 +78,6 @@ struct RPLRegionMappingTable
void RPLLoader_UnloadModule(RPLModule* rpl); void RPLLoader_UnloadModule(RPLModule* rpl);
void RPLLoader_RemoveDependency(const char* name); void RPLLoader_RemoveDependency(const char* name);
char _ansiToLower(char c)
{
if (c >= 'A' && c <= 'Z')
c -= ('A' - 'a');
return c;
}
uint8* RPLLoader_AllocateTrampolineCodeSpace(RPLModule* rplLoaderContext, sint32 size) uint8* RPLLoader_AllocateTrampolineCodeSpace(RPLModule* rplLoaderContext, sint32 size)
{ {
if (rplLoaderContext) if (rplLoaderContext)

View File

@ -99,6 +99,7 @@ TitleInfo::TitleInfo(const TitleInfo::CachedInfo& cachedInfo)
if (cachedInfo.titleDataFormat != TitleDataFormat::HOST_FS && if (cachedInfo.titleDataFormat != TitleDataFormat::HOST_FS &&
cachedInfo.titleDataFormat != TitleDataFormat::WIIU_ARCHIVE && cachedInfo.titleDataFormat != TitleDataFormat::WIIU_ARCHIVE &&
cachedInfo.titleDataFormat != TitleDataFormat::WUD && cachedInfo.titleDataFormat != TitleDataFormat::WUD &&
cachedInfo.titleDataFormat != TitleDataFormat::NUS &&
cachedInfo.titleDataFormat != TitleDataFormat::INVALID_STRUCTURE) cachedInfo.titleDataFormat != TitleDataFormat::INVALID_STRUCTURE)
return; return;
if (cachedInfo.path.empty()) if (cachedInfo.path.empty())
@ -204,6 +205,12 @@ bool TitleInfo::DetectFormat(const fs::path& path, fs::path& pathOut, TitleDataF
pathOut = path; pathOut = path;
return true; return true;
} }
else if (boost::iequals(filenameStr, "title.tmd"))
{
formatOut = TitleDataFormat::NUS;
pathOut = path;
return true;
}
else if (boost::iends_with(filenameStr, ".wua")) else if (boost::iends_with(filenameStr, ".wua"))
{ {
formatOut = TitleDataFormat::WIIU_ARCHIVE; formatOut = TitleDataFormat::WIIU_ARCHIVE;
@ -378,12 +385,15 @@ bool TitleInfo::Mount(std::string_view virtualPath, std::string_view subfolder,
return false; return false;
} }
} }
else if (m_titleFormat == TitleDataFormat::WUD) else if (m_titleFormat == TitleDataFormat::WUD || m_titleFormat == TitleDataFormat::NUS)
{ {
if (m_mountpoints.empty()) if (m_mountpoints.empty())
{ {
cemu_assert_debug(!m_wudVolume); cemu_assert_debug(!m_wudVolume);
m_wudVolume = FSTVolume::OpenFromDiscImage(m_fullPath); if(m_titleFormat == TitleDataFormat::WUD)
m_wudVolume = FSTVolume::OpenFromDiscImage(m_fullPath); // open wud/wux
else
m_wudVolume = FSTVolume::OpenFromContentFolder(m_fullPath.parent_path()); // open from .app files directory, the path points to /title.tmd
} }
if (!m_wudVolume) if (!m_wudVolume)
return false; return false;
@ -433,7 +443,7 @@ void TitleInfo::Unmount(std::string_view virtualPath)
{ {
if (m_wudVolume) if (m_wudVolume)
{ {
cemu_assert_debug(m_titleFormat == TitleDataFormat::WUD); cemu_assert_debug(m_titleFormat == TitleDataFormat::WUD || m_titleFormat == TitleDataFormat::NUS);
delete m_wudVolume; delete m_wudVolume;
m_wudVolume = nullptr; m_wudVolume = nullptr;
} }
@ -664,6 +674,9 @@ std::string TitleInfo::GetPrintPath() const
case TitleDataFormat::WUD: case TitleDataFormat::WUD:
tmp.append(" [WUD]"); tmp.append(" [WUD]");
break; break;
case TitleDataFormat::NUS:
tmp.append(" [NUS]");
break;
case TitleDataFormat::WIIU_ARCHIVE: case TitleDataFormat::WIIU_ARCHIVE:
tmp.append(" [WUA]"); tmp.append(" [WUA]");
break; break;

View File

@ -60,6 +60,7 @@ public:
HOST_FS = 1, // host filesystem directory (fullPath points to root with content/code/meta subfolders) HOST_FS = 1, // host filesystem directory (fullPath points to root with content/code/meta subfolders)
WUD = 2, // WUD or WUX WUD = 2, // WUD or WUX
WIIU_ARCHIVE = 3, // Wii U compressed single-file archive (.wua) WIIU_ARCHIVE = 3, // Wii U compressed single-file archive (.wua)
NUS = 4, // NUS format. Directory with .app files, title.tik and title.tmd
// error // error
INVALID_STRUCTURE = 0, INVALID_STRUCTURE = 0,
}; };

View File

@ -324,17 +324,25 @@ bool CafeTitleList::RefreshWorkerThread()
return true; return true;
} }
bool _IsKnownFileExtension(std::string fileExtension) bool _IsKnownFileNameOrExtension(const fs::path& path)
{ {
std::string fileExtension = _pathToUtf8(path.extension());
for (auto& it : fileExtension) for (auto& it : fileExtension)
if (it >= 'A' && it <= 'Z') it = _ansiToLower(it);
it -= ('A' - 'a'); if(fileExtension == ".tmd")
{
// must be "title.tmd"
std::string fileName = _pathToUtf8(path.filename());
for (auto& it : fileName)
it = _ansiToLower(it);
return fileName == "title.tmd";
}
return return
fileExtension == ".wud" || fileExtension == ".wud" ||
fileExtension == ".wux" || fileExtension == ".wux" ||
fileExtension == ".iso" || fileExtension == ".iso" ||
fileExtension == ".wua"; fileExtension == ".wua";
// note: To detect extracted titles with RPX we use the content/code/meta folder structure // note: To detect extracted titles with RPX we rely on the presence of the content,code,meta directory structure
} }
void CafeTitleList::ScanGamePath(const fs::path& path) void CafeTitleList::ScanGamePath(const fs::path& path)
@ -353,7 +361,6 @@ void CafeTitleList::ScanGamePath(const fs::path& path)
else if (it.is_directory(ec)) else if (it.is_directory(ec))
{ {
dirsInDirectory.emplace_back(it.path()); dirsInDirectory.emplace_back(it.path());
std::string dirName = _pathToUtf8(it.path().filename()); std::string dirName = _pathToUtf8(it.path().filename());
if (boost::iequals(dirName, "content")) if (boost::iequals(dirName, "content"))
hasContentFolder = true; hasContentFolder = true;
@ -366,10 +373,10 @@ void CafeTitleList::ScanGamePath(const fs::path& path)
// always check individual files // always check individual files
for (auto& it : filesInDirectory) for (auto& it : filesInDirectory)
{ {
// since checking files is slow, we only do it for known file extensions // since checking individual files is slow, we limit it to known file names or extensions
if (!it.has_extension()) if (!it.has_extension())
continue; continue;
if (!_IsKnownFileExtension(_pathToUtf8(it.extension()))) if (!_IsKnownFileNameOrExtension(it))
continue; continue;
AddTitleFromPath(it); AddTitleFromPath(it);
} }

View File

@ -468,6 +468,14 @@ inline fs::path _utf8ToPath(std::string_view input)
return fs::path(v); return fs::path(v);
} }
// locale-independent variant of tolower() which also matches Wii U behavior
inline char _ansiToLower(char c)
{
if (c >= 'A' && c <= 'Z')
c -= ('A' - 'a');
return c;
}
class RunAtCemuBoot // -> replaces this with direct function calls. Linkers other than MSVC may optimize way object files entirely if they are not referenced from outside. So a source file self-registering using this would be causing issues class RunAtCemuBoot // -> replaces this with direct function calls. Linkers other than MSVC may optimize way object files entirely if they are not referenced from outside. So a source file self-registering using this would be causing issues
{ {
public: public:

View File

@ -639,13 +639,15 @@ void MainWindow::OnFileMenu(wxCommandEvent& event)
if (menuId == MAINFRAME_MENU_ID_FILE_LOAD) if (menuId == MAINFRAME_MENU_ID_FILE_LOAD)
{ {
const auto wildcard = formatWxString( const auto wildcard = formatWxString(
"{}|*.wud;*.wux;*.wua;*.iso;*.rpx;*.elf" "{}|*.wud;*.wux;*.wua;*.iso;*.rpx;*.elf;title.tmd"
"|{}|*.wud;*.wux;*.iso" "|{}|*.wud;*.wux;*.iso"
"|{}|title.tmd"
"|{}|*.wua" "|{}|*.wua"
"|{}|*.rpx;*.elf" "|{}|*.rpx;*.elf"
"|{}|*", "|{}|*",
_("All Wii U files (*.wud, *.wux, *.wua, *.iso, *.rpx, *.elf)"), _("All Wii U files (*.wud, *.wux, *.wua, *.iso, *.rpx, *.elf)"),
_("Wii U image (*.wud, *.wux, *.iso, *.wad)"), _("Wii U image (*.wud, *.wux, *.iso, *.wad)"),
_("Wii U NUS content"),
_("Wii U archive (*.wua)"), _("Wii U archive (*.wua)"),
_("Wii U executable (*.rpx, *.elf)"), _("Wii U executable (*.rpx, *.elf)"),
_("All files (*.*)") _("All files (*.*)")

View File

@ -941,6 +941,8 @@ wxString wxTitleManagerList::GetTitleEntryText(const TitleEntry& entry, ItemColu
return _("Folder"); return _("Folder");
case wxTitleManagerList::EntryFormat::WUD: case wxTitleManagerList::EntryFormat::WUD:
return _("WUD"); return _("WUD");
case wxTitleManagerList::EntryFormat::NUS:
return _("NUS");
case wxTitleManagerList::EntryFormat::WUA: case wxTitleManagerList::EntryFormat::WUA:
return _("WUA"); return _("WUA");
} }
@ -1010,16 +1012,19 @@ void wxTitleManagerList::HandleTitleListCallback(CafeTitleListCallbackEvent* evt
wxTitleManagerList::EntryFormat entryFormat; wxTitleManagerList::EntryFormat entryFormat;
switch (titleInfo.GetFormat()) switch (titleInfo.GetFormat())
{ {
case TitleInfo::TitleDataFormat::HOST_FS:
default:
entryFormat = EntryFormat::Folder;
break;
case TitleInfo::TitleDataFormat::WUD: case TitleInfo::TitleDataFormat::WUD:
entryFormat = EntryFormat::WUD; entryFormat = EntryFormat::WUD;
break; break;
case TitleInfo::TitleDataFormat::NUS:
entryFormat = EntryFormat::NUS;
break;
case TitleInfo::TitleDataFormat::WIIU_ARCHIVE: case TitleInfo::TitleDataFormat::WIIU_ARCHIVE:
entryFormat = EntryFormat::WUA; entryFormat = EntryFormat::WUA;
break; break;
case TitleInfo::TitleDataFormat::HOST_FS:
default:
entryFormat = EntryFormat::Folder;
break;
} }
if (evt->eventType == CafeTitleListCallbackEvent::TYPE::TITLE_DISCOVERED) if (evt->eventType == CafeTitleListCallbackEvent::TYPE::TITLE_DISCOVERED)

View File

@ -42,6 +42,7 @@ public:
{ {
Folder, Folder,
WUD, WUD,
NUS,
WUA, WUA,
}; };