diff --git a/extensions/cpp/explorer/src/extension.cpp b/extensions/cpp/explorer/src/extension.cpp index 1ebbd6f..2d24f1c 100644 --- a/extensions/cpp/explorer/src/extension.cpp +++ b/extensions/cpp/explorer/src/extension.cpp @@ -5,6 +5,209 @@ using namespace rpcsx::ui; +enum class LanguageCode { + ja, + en, + fr, + es, + de, + it, + nl, + pt, + ru, + ko, + ch, + zh, + fi, + sv, + da, + no, + pl, + br, + gb, + tr, + la, + ar, + ca, + cs, + hu, + el, + ro, + th, + vi, + in, + uk, + + _count +}; + +static std::string languageCodeToString(LanguageCode code) { + switch (code) { + case LanguageCode::ja: + return "ja"; + case LanguageCode::en: + return "en"; + case LanguageCode::fr: + return "fr"; + case LanguageCode::es: + return "es"; + case LanguageCode::de: + return "de"; + case LanguageCode::it: + return "it"; + case LanguageCode::nl: + return "nl"; + case LanguageCode::pt: + return "pt"; + case LanguageCode::ru: + return "ru"; + case LanguageCode::ko: + return "ko"; + case LanguageCode::ch: + return "ch"; + case LanguageCode::zh: + return "zh"; + case LanguageCode::fi: + return "fi"; + case LanguageCode::sv: + return "sv"; + case LanguageCode::da: + return "da"; + case LanguageCode::no: + return "no"; + case LanguageCode::pl: + return "pl"; + case LanguageCode::br: + return "br"; + case LanguageCode::gb: + return "gb"; + case LanguageCode::tr: + return "tr"; + case LanguageCode::la: + return "la"; + case LanguageCode::ar: + return "ar"; + case LanguageCode::ca: + return "ca"; + case LanguageCode::cs: + return "cs"; + case LanguageCode::hu: + return "hu"; + case LanguageCode::el: + return "el"; + case LanguageCode::ro: + return "ro"; + case LanguageCode::th: + return "th"; + case LanguageCode::vi: + return "vi"; + case LanguageCode::in: + return "in"; + case LanguageCode::uk: + return "uk"; + default: + return "en"; + } +} + +static std::vector +fetchLocalizedString(const sfo::registry ®istry, const std::string &key) { + if (!registry.contains(key)) { + return {}; + } + + std::vector result; + result.push_back({.text = registry.at(key).as_string()}); + + for (std::size_t i = 0; i < static_cast(LanguageCode::_count); ++i) { + std::string keyWithSuffix = key + (i < 10 ? "_0" : "_"); + keyWithSuffix += std::to_string(i); + + if (!registry.contains(keyWithSuffix)) { + continue; + } + + result.push_back( + {.text = registry.at(keyWithSuffix).as_string(), + .lang = languageCodeToString(static_cast(i))}); + } + + return result; +} + +static std::vector +fetchLocalizedResourceFile(const std::filesystem::path &path, + const std::string &name, const std::string &ext) { + std::vector result; + + if (std::filesystem::is_regular_file(path / (name + ext))) { + result.push_back(LocalizedResource{ + .uri = "file://" + (path / (name + ext)).string(), + }); + } else { + return {}; + } + + for (std::size_t i = 0; i < static_cast(LanguageCode::_count); ++i) { + std::string suffix = (i < 10 ? "_0" : "_"); + suffix += std::to_string(i); + + if (auto testPath = path / (name + suffix + ext); + std::filesystem::is_regular_file(testPath)) { + result.push_back(LocalizedResource{ + .uri = "file://" + testPath.string(), + .lang = languageCodeToString(static_cast(i)), + }); + } + } + + return result; +} + +static std::vector +fetchLocalizedImageFile(const std::filesystem::path &path, + const std::string &name, const std::string &ext) { + std::vector result; + + if (std::filesystem::is_regular_file(path / (name + ext))) { + result.push_back(LocalizedImage{ + .uri = "file://" + (path / (name + ext)).string(), + .resolution = ImageResolution::Normal, + }); + } + + if (std::filesystem::is_regular_file(path / (name + "_4k" + ext))) { + result.push_back(LocalizedImage{ + .uri = "file://" + (path / (name + "_4k" + ext)).string(), + .resolution = ImageResolution::High, + }); + } + + for (std::size_t i = 0; i < static_cast(LanguageCode::_count); ++i) { + std::string suffix = (i < 10 ? "_0" : "_"); + suffix += std::to_string(i); + + if (auto testPath = path / (name + suffix + ext); + std::filesystem::is_regular_file(testPath)) { + result.push_back(LocalizedImage{ + .uri = "file://" + testPath.string(), + .lang = languageCodeToString(static_cast(i)), + .resolution = ImageResolution::Normal, + }); + } + + if (auto testPath = path / (name + "_4k" + suffix + ext); + std::filesystem::is_regular_file(testPath)) { + result.push_back(LocalizedImage{ + .uri = "file://" + testPath.string(), + .lang = languageCodeToString(static_cast(i)), + .resolution = ImageResolution::High, + }); + } + } + + return result; +} static std::size_t calcDirectorySize(const std::filesystem::path &path) { std::uint64_t result = 0; @@ -104,18 +307,12 @@ tryFetchGame(const std::filesystem::directory_entry &entry) { ExplorerItem info; info.type = "game"; + info.name = fetchLocalizedString(data.sfo, "TITLE"); - auto name = sfo::get_string(data.sfo, "TITLE"); - if (name.empty()) { - name = sfo::get_string(data.sfo, "TITLE_ID"); - - if (name.empty()) { - return {}; - } + if (info.name.empty()) { + return {}; } - info.name = {LocalizedString{.text = std::string(name)}}; - info.titleId = sfo::get_string(data.sfo, "TITLE_ID"); info.version = sfo::get_string(data.sfo, "APP_VER"); @@ -123,21 +320,80 @@ tryFetchGame(const std::filesystem::directory_entry &entry) { info.version = sfo::get_string(data.sfo, "VERSION", "1.0"); } - if (std::filesystem::is_regular_file(sysPath / "icon0.png")) { - info.icon = { - LocalizedIcon{.uri = "file://" + (sysPath / "icon0.png").string()}}; - } + info.icon = fetchLocalizedImageFile(sysPath, "icon0", ".png"); + info.iconSound = fetchLocalizedResourceFile(sysPath, "snd0", ".at9"); + info.background = fetchLocalizedImageFile(sysPath, "pic1", ".png"); + info.overlayImage = fetchLocalizedImageFile(sysPath, "pic2", ".png"); info.size = calcDirectorySize(entry.path()); info.type = "game"; info.launcher = LauncherInfo{ - .type = "fself-ps4-orbis" // FIXME: self/elf? ps3? ps5? + .type = "fself-ps4-orbis" // FIXME: self/elf? ps5? // "fself-ps5-prospero" }; info.location = "file://" + entry.path().string(); return std::move(info); } +static std::optional +tryFetchPs3Game(const std::filesystem::directory_entry &entry) { + auto usrdirPath = entry.path() / "USRDIR"; + auto paramSfoPath = entry.path() / "PARAM.SFO"; + auto ebootPath = usrdirPath / "EBOOT.BIN"; + + if (!std::filesystem::is_regular_file(ebootPath)) { + return {}; + } + + if (!std::filesystem::is_regular_file(paramSfoPath)) { + return {}; + } + + auto data = sfo::load(paramSfoPath.string()); + if (data.errc != sfo::error::ok) { + elog("%s: error %d", entry.path().c_str(), static_cast(data.errc)); + return {}; + } + + auto titleId = sfo::get_string(data.sfo, "TITLE_ID"); + auto bootable = sfo::get_integer(data.sfo, "BOOTABLE", 0); + auto category = sfo::get_string(data.sfo, "CATEGORY"); + + if (!bootable || titleId.empty()) { + return {}; + } + + ExplorerItem info; + info.type = "game"; + info.name = fetchLocalizedString(data.sfo, "TITLE"); + + if (info.name.empty()) { + return {}; + } + + info.version = sfo::get_string(data.sfo, "APP_VER"); + + if (info.version->empty()) { + info.version = sfo::get_string(data.sfo, "VERSION", "1.0"); + } + + info.icon = fetchLocalizedImageFile(entry.path(), "ICON0", ".PNG"); + info.iconSound = fetchLocalizedResourceFile(entry.path(), "SND0", ".AT3"); + info.iconVideo = fetchLocalizedResourceFile(entry.path(), "ICON1", ".PAM"); + info.overlayImageWide = fetchLocalizedImageFile(entry.path(), "PIC0", ".PNG"); + info.background = fetchLocalizedImageFile(entry.path(), "PIC1", ".PNG"); + info.overlayImage = fetchLocalizedImageFile(entry.path(), "PIC2", ".PNG"); + + info.size = calcDirectorySize(entry.path()); + info.type = "game"; + info.launcher = LauncherInfo{ + .type = "self-ps3-cellos" // FIXME: fself/elf? + }; + info.location = "file://" + entry.path().string(); + + return info; +} + struct ExplorerExtension : rpcsx::ui::Extension { std::thread explorerThread; std::vector locations; @@ -205,6 +461,11 @@ struct ExplorerExtension : rpcsx::ui::Extension { submit(std::move(*fw)); continue; } + + if (auto game = tryFetchPs3Game(entry)) { + submit(std::move(*game)); + continue; + } } } diff --git a/rpcsx-ui/src/core/component.json b/rpcsx-ui/src/core/component.json index 14eaa69..6f67a87 100644 --- a/rpcsx-ui/src/core/component.json +++ b/rpcsx-ui/src/core/component.json @@ -37,14 +37,26 @@ } } }, - "icon-resolution": { + "image-resolution": { "type": "enum", "enumerators": { "normal": 0, "high": 1 } }, - "localized-icon": { + "localized-resource": { + "type": "object", + "params": { + "uri": { + "type": "string" + }, + "lang": { + "type": "string", + "optional": true + } + } + }, + "localized-image": { "type": "object", "params": { "uri": { @@ -55,7 +67,7 @@ "optional": true }, "resolution": { - "type": "icon-resolution", + "type": "image-resolution", "optional": true } } @@ -177,7 +189,7 @@ }, "icon": { "type": "array", - "item-type": "localized-icon", + "item-type": "localized-image", "optional": true }, "description": { @@ -218,7 +230,7 @@ }, "icon": { "type": "array", - "item-type": "localized-icon", + "item-type": "localized-image", "optional": true }, "description": { diff --git a/rpcsx-ui/src/core/lib/Localized.ts b/rpcsx-ui/src/core/lib/Localized.ts index 5fdfc52..961d214 100644 --- a/rpcsx-ui/src/core/lib/Localized.ts +++ b/rpcsx-ui/src/core/lib/Localized.ts @@ -13,28 +13,21 @@ export function getLocalizedString(string: LocalizedString[], langs: string[] = return string[0].text; } -export function getLocalizedIcon(icon: LocalizedIcon[], resolution: IconResolution = IconResolution.Normal, langs: string[] = []) { - for (let langIndex = 0; langIndex < langs.length; ++langIndex) { - const lang = langs[langIndex]; - - for (let iconIndex = 0; iconIndex < icon.length; ++iconIndex) { - const localizedIcon = icon[iconIndex]; - if (localizedIcon.lang === lang && localizedIcon.resolution === resolution) { - return localizedIcon.uri; - } - } +export function getLocalizedResource(resources: LocalizedResource[], langs: string[] = []) { + if (resources.length == 0) { + return undefined; } for (let langIndex = 0; langIndex < langs.length; ++langIndex) { const lang = langs[langIndex]; - for (let iconIndex = 0; iconIndex < icon.length; ++iconIndex) { - const localizedIcon = icon[iconIndex]; - if (localizedIcon.lang === lang) { - return localizedIcon.uri; + for (let resourceIndex = 0; resourceIndex < resources.length; ++resourceIndex) { + const localizedResource = resources[resourceIndex]; + if (localizedResource.lang === lang) { + return localizedResource.uri; } } } - return icon[0].uri; + return resources[0].uri; } diff --git a/rpcsx-ui/src/explorer/component.json b/rpcsx-ui/src/explorer/component.json index 2dc5d0b..b73558d 100644 --- a/rpcsx-ui/src/explorer/component.json +++ b/rpcsx-ui/src/explorer/component.json @@ -35,7 +35,32 @@ }, "icon": { "type": "array", - "item-type": "$core/localized-icon", + "item-type": "$core/localized-image", + "optional": true + }, + "icon-video": { + "type": "array", + "item-type": "$core/localized-resource", + "optional": true + }, + "icon-sound": { + "type": "array", + "item-type": "$core/localized-resource", + "optional": true + }, + "background": { + "type": "array", + "item-type": "$core/localized-image", + "optional": true + }, + "overlay-image": { + "type": "array", + "item-type": "$core/localized-image", + "optional": true + }, + "overlay-image-wide": { + "type": "array", + "item-type": "$core/localized-image", "optional": true }, "publisher": { diff --git a/rpcsx-ui/src/explorer/renderer/helpers/ExplorerItemUtils.ts b/rpcsx-ui/src/explorer/renderer/helpers/ExplorerItemUtils.ts index d294e15..12e0052 100644 --- a/rpcsx-ui/src/explorer/renderer/helpers/ExplorerItemUtils.ts +++ b/rpcsx-ui/src/explorer/renderer/helpers/ExplorerItemUtils.ts @@ -1,6 +1,6 @@ import { Region } from '$/Region'; -import { IconResolution } from '$core/enums'; -import { getLocalizedString, getLocalizedIcon } from '$core/Localized'; +import { ImageResolution } from '$core/enums'; +import { getLocalizedString } from '$core/Localized'; export function getRegion(contentId?: string) { if (contentId === undefined || contentId.length != 36) { @@ -23,11 +23,41 @@ export function getName(item: ExplorerItem, langs: string[] = []) { return getLocalizedString(item.name, langs); } -export function getIcon(item: ExplorerItem, resolution: IconResolution = IconResolution.Normal, langs: string[] = []) { +export function getIcon(item: ExplorerItem, resolution: ImageResolution = ImageResolution.Normal, langs: string[] = []) { if (!item.icon) { return undefined; } - return getLocalizedIcon(item.icon, resolution, langs); + return getLocalizedImage(item.icon, resolution, langs); +} + +export function getLocalizedImage(icon: LocalizedImage[], resolution: ImageResolution = ImageResolution.Normal, langs: string[] = []) { + if (icon.length == 0) { + return undefined; + } + + for (let langIndex = 0; langIndex < langs.length; ++langIndex) { + const lang = langs[langIndex]; + + for (let iconIndex = 0; iconIndex < icon.length; ++iconIndex) { + const localizedIcon = icon[iconIndex]; + if (localizedIcon.lang === lang && localizedIcon.resolution === resolution) { + return localizedIcon.uri; + } + } + } + + for (let langIndex = 0; langIndex < langs.length; ++langIndex) { + const lang = langs[langIndex]; + + for (let iconIndex = 0; iconIndex < icon.length; ++iconIndex) { + const localizedIcon = icon[iconIndex]; + if (localizedIcon.lang === lang) { + return localizedIcon.uri; + } + } + } + + return icon[0].uri; } diff --git a/rpcsx-ui/src/explorer/renderer/views/Explorer.tsx b/rpcsx-ui/src/explorer/renderer/views/Explorer.tsx index ee57edc..a0e2955 100644 --- a/rpcsx-ui/src/explorer/renderer/views/Explorer.tsx +++ b/rpcsx-ui/src/explorer/renderer/views/Explorer.tsx @@ -1,9 +1,10 @@ -import { ComponentProps, memo, ReactElement, useEffect, useRef, useState } from 'react'; -import { Image, Pressable, ScrollView, StyleSheet, View, FlatList, Modal, useWindowDimensions } from 'react-native'; +import { ComponentProps, memo, useEffect, useRef, useState } from 'react'; +import * as React from 'react'; +import { Image, Pressable, ScrollView, StyleSheet, View, FlatList, Modal, useWindowDimensions, ImageBackground } from 'react-native'; import { useThemeColor } from '$core/useThemeColor' import ThemedIcon from '$core/ThemedIcon'; import { ThemedText } from '$core/ThemedText'; -import { getIcon, getName } from "$/ExplorerItemUtils" +import { getIcon, getName, getLocalizedImage } from "$/ExplorerItemUtils" import { HapticPressable } from '$core/HapticPressable'; import { DownShowViewSelector, LeftRightViewSelector } from '$core/ViewSelector'; import { getLocalizedString } from '$core/Localized'; @@ -242,7 +243,8 @@ const ExplorerItemHeader = memo(function ({ item, active, ...rest }: { item: Exp justifyContent: 'center', flexWrap: 'nowrap', height: "100%", - minWidth: 100, + minWidth: 250, + minHeight: 100 }, }); @@ -432,7 +434,7 @@ const ExplorerItemBody = memo(function ({ item }: { item: ExplorerItem }) { ) }); -const ExplorerView = function ({ items }: { items: ExplorerItem[] }) { +const ExplorerView = function ({ items, setBackground }: { items: ExplorerItem[], setBackground: (uri?: string) => void }) { const styles = StyleSheet.create({ topContainer: { width: "100%", @@ -444,7 +446,13 @@ const ExplorerView = function ({ items }: { items: ExplorerItem[] }) { } }); - const [selectedItem, selectItem] = useState(0); + const [selectedItem, setSelectedItem] = useState(0); + + const selectItem = (index: number) => { + setSelectedItem(index); + + setBackground(items[index].background ? getLocalizedImage(items[index].background) : undefined); + }; return ( @@ -523,7 +531,6 @@ const ExplorerStyles = StyleSheet.create({ }, }); - export function Explorer(props?: Props) { const insets = useSafeAreaInsets(); const [background, setBackground] = useState(undefined); @@ -548,12 +555,22 @@ export function Explorer(props?: Props) { self.onExplorerItems(event => { games.push(...event.items.filter(item => item.type == 'game')); setGames(games); - setUpdateId(updateId + 1); + if (updateId == 0) { + setUpdateId(updateId + 1); + } + console.log("received items", event.items.length, games.length); }); - self.explorerGet({}); + if (games.length == 0) { + self.explorerGet({}); + } }, []); + const updateActiveTab = (tab: number) => { + setActiveTab(tab); + setBackground(undefined); + }; + const screens = [ { title: "Games", @@ -566,29 +583,31 @@ export function Explorer(props?: Props) { ]; return ( - - - - - { - screens.map((screen, index) => - setActiveTab(index)} /> - ) - } + + + + + + { + screens.map((screen, index) => + updateActiveTab(index)} /> + ) + } + + + + + + settings.pushSettingsView({})}> + + - - - - settings.pushSettingsView({})}> - - - - - - } selectedItem={activeTab} /> - + + } selectedItem={activeTab} /> + + ) };