From 911a63fc7bbe935c2e1f8ead5f042a81299c51da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=9C=A8=E4=B8=AD=E5=9B=BD=E7=9A=84=E6=B3=B0=E5=9B=BD?= =?UTF-8?q?=E9=9D=92=E5=B9=B4=5F?= <132539847+dekthaiinchina@users.noreply.github.com> Date: Sat, 1 Feb 2025 19:41:36 +0700 Subject: [PATCH 001/209] Update locales.json --- src/Ryujinx/Assets/locales.json | 634 +++++++++++++++++--------------- 1 file changed, 343 insertions(+), 291 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index ec8a2aaac..4859e62b4 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -40,7 +40,7 @@ "pt_BR": "Português (BR)", "ru_RU": "Русский (RU)", "sv_SE": "Svenska", - "th_TH": "ภาษาไทย", + "th_TH": "ไทย", "tr_TR": "Türkçe", "uk_UA": "Українська", "zh_CN": "简体中文", @@ -65,7 +65,7 @@ "pt_BR": "Abrir Applet", "ru_RU": "Открыть апплет", "sv_SE": "Öppna applet", - "th_TH": "เปิด Applet", + "th_TH": "เปิด แอปเพล็ต", "tr_TR": "Applet'i Aç", "uk_UA": "Відкрити аплет", "zh_CN": "打开小程序", @@ -90,7 +90,11 @@ "pt_BR": "", "ru_RU": "Апплет Mii Editor", "sv_SE": "Redigera Mii-applet", - "th_TH": "", +<<<<<<< HEAD + "th_TH": "แอปเพล็ต แก้ไข Mii", +======= + "th_TH": "เครื่องมือแก้ไข Mii", +>>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Аплет редагування Mii", "zh_CN": "Mii 小程序", @@ -115,7 +119,7 @@ "pt_BR": "Abrir editor Mii em modo avulso", "ru_RU": "Открывает апплет Mii Editor в автономном режиме", "sv_SE": "Öppna Mii Editor Applet i fristående läge", - "th_TH": "เปิดโปรแกรม Mii Editor Applet", + "th_TH": "เปิดเครื่องมือแก้ไข Mii ในโหมดสแตนด์อโลน", "tr_TR": "Mii Editör Applet'ini Bağımsız Mod'da Aç", "uk_UA": "Відкрити аплет Mii Editor в автономному режимі", "zh_CN": "打开独立的 Mii 小程序", @@ -140,7 +144,7 @@ "pt_BR": "Acesso direto ao mouse", "ru_RU": "Прямой ввод мыши", "sv_SE": "Direkt musåtkomst", - "th_TH": "เข้าถึงเมาส์ได้โดยตรง", + "th_TH": "เข้าถึงเมาส์โดยตรง", "tr_TR": "Doğrudan Mouse Erişimi", "uk_UA": "Прямий доступ мишею", "zh_CN": "直通鼠标操作", @@ -240,7 +244,7 @@ "pt_BR": "Hóspede sem verificação (mais rápido, inseguro)", "ru_RU": "Хост не установлен (самый быстрый, небезопасный)", "sv_SE": "Värd inte kontrollerad (snabbaste, osäkert)", - "th_TH": "ไม่ได้ตรวจสอบโฮสต์ (เร็วที่สุด, แต่ไม่ปลอดภัย)", + "th_TH": "โฮสต์ไม่ได้ตรวจสอบ (เร็วที่สุด, ไม่ปลอดภัย)", "tr_TR": "Host Unchecked (en hızlısı, tehlikeli)", "uk_UA": "Неперевірений хост (найшвидший, небезпечний)", "zh_CN": "跳过检查的本机映射 (最快,不安全)", @@ -265,7 +269,7 @@ "pt_BR": "Usar Hipervisor", "ru_RU": "Использовать Hypervisor", "sv_SE": "Använd Hypervisor", - "th_TH": "ใช้งาน Hypervisor", + "th_TH": "ใช้งาน ไฮเปอร์ไวเซอร์", "tr_TR": "Hypervisor Kullan", "uk_UA": "Використовувати гіпервізор", "zh_CN": "使用 Hypervisor 虚拟化", @@ -290,7 +294,7 @@ "pt_BR": "_Arquivo", "ru_RU": "_Файл", "sv_SE": "_Arkiv", - "th_TH": "ไฟล์", + "th_TH": "_ไฟล์", "tr_TR": "_Dosya", "uk_UA": "_Файл", "zh_CN": "文件(_F)", @@ -315,7 +319,7 @@ "pt_BR": "_Abrir ROM do jogo...", "ru_RU": "_Добавить приложение из файла", "sv_SE": "_Läs in applikation från fil", - "th_TH": "โหลดแอปพลิเคชั่นจากไฟล์", + "th_TH": "_โหลดแอปพลิเคชันจากไฟล์", "tr_TR": "_Dosyadan Uygulama Aç", "uk_UA": "_Завантажити програму з файлу", "zh_CN": "加载游戏文件(_L)", @@ -340,7 +344,7 @@ "pt_BR": "Nenhum aplicativo encontrado no arquivo selecionado.", "ru_RU": "Приложения в выбранном файле не найдены", "sv_SE": "Inga applikationer hittades i vald fil.", - "th_TH": "ไม่พบแอปพลิเคชั่นจากไฟล์ที่เลือก", + "th_TH": "ไม่พบแอปพลิเคชันในไฟล์ที่เลือก", "tr_TR": "", "uk_UA": "У вибраному файлі не знайдено жодних додатків.", "zh_CN": "未发现应用", @@ -365,7 +369,7 @@ "pt_BR": "Abrir jogo _extraído...", "ru_RU": "Добавить _распакованную игру", "sv_SE": "Läs in _uppackat spel", - "th_TH": "โหลดเกมที่แตกไฟล์แล้ว", + "th_TH": "โหลด _เกมที่ยังไม่ได้บีบอัด", "tr_TR": "_Sıkıştırılmamış Oyun Aç", "uk_UA": "Завантажити _розпаковану гру", "zh_CN": "加载解包后的游戏(_U)", @@ -415,7 +419,7 @@ "pt_BR": "Carregar Atualizações de Jogo da Pasta", "ru_RU": "Загрузить обновления из папки", "sv_SE": "Läs in titeluppdateringar från mapp", - "th_TH": "โหลดไฟล์อัพเดตจากโฟลเดอร์", + "th_TH": "โหลดอัปเดตเกมจากโฟลเดอร์", "tr_TR": "", "uk_UA": "Завантажити оновлення заголовків з теки", "zh_CN": "从文件夹加载游戏更新", @@ -465,7 +469,7 @@ "pt_BR": "Abrir diretório de _logs...", "ru_RU": "Открыть папку с логами", "sv_SE": "Öppna loggmapp", - "th_TH": "เปิดโฟลเดอร์ Logs", + "th_TH": "เปิดโฟลเดอร์บันทึก", "tr_TR": "Logs Klasörünü aç", "uk_UA": "Відкрити теку журналів змін", "zh_CN": "打开日志目录", @@ -540,7 +544,7 @@ "pt_BR": "_Mudar para tela cheia", "ru_RU": "Включить полноэкранный режим", "sv_SE": "Växla helskärm", - "th_TH": "สลับเป็นโหมดเต็มหน้าจอ", + "th_TH": "สลับเป็นโหมดเต็มจอ", "tr_TR": "Tam Ekran Modunu Aç", "uk_UA": "На весь екран", "zh_CN": "切换全屏", @@ -565,7 +569,7 @@ "pt_BR": "Iniciar jogos em tela cheia", "ru_RU": "Запускать игры в полноэкранном режиме", "sv_SE": "Starta spel i helskärmsläge", - "th_TH": "เริ่มเกมในโหมดเต็มหน้าจอ", + "th_TH": "เริ่มเกมในโหมดเต็มจอ", "tr_TR": "Oyunları Tam Ekran Modunda Başlat", "uk_UA": "Запускати ігри на весь екран", "zh_CN": "全屏模式启动游戏", @@ -590,7 +594,11 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", +<<<<<<< HEAD + "th_TH": "เริ่มเกมด้วย UI ที่ซ่อนอยู่", +======= + "th_TH": "เริ่มเกมโดยซ่อน UI", +>>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "", "zh_CN": "启动游戏时隐藏 UI", @@ -665,7 +673,7 @@ "pt_BR": "_Gerenciar perfis de usuário", "ru_RU": "_Менеджер учетных записей", "sv_SE": "_Hantera användarprofiler", - "th_TH": "_จัดการโปรไฟล์ผู้ใช้งาน", + "th_TH": "_จัดการโปรไฟล์ผู้ใช้", "tr_TR": "_Kullanıcı Profillerini Yönet", "uk_UA": "_Керувати профілями користувачів", "zh_CN": "管理用户账户(_M)", @@ -690,7 +698,7 @@ "pt_BR": "_Ações", "ru_RU": "_Действия", "sv_SE": "Åt_gärder", - "th_TH": "การดำเนินการ", + "th_TH": "_การดำเนินการ", "tr_TR": "_Eylemler", "uk_UA": "_Дії", "zh_CN": "操作(_A)", @@ -765,7 +773,11 @@ "pt_BR": "", "ru_RU": "Сканировать Amiibo (из папки Bin)", "sv_SE": "Skanna en Amiibo (från bin-fil)", - "th_TH": "", +<<<<<<< HEAD + "th_TH": "สแกน อามิโบ (จาก Bin)", +======= + "th_TH": "สแกน Amiibo (จากไฟล์ Bin)", +>>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Сканувати Amiibo (з теки Bin)", "zh_CN": "扫描 Amiibo (从 bin 文件)", @@ -815,7 +827,7 @@ "pt_BR": "Instalar firmware a partir de um arquivo ZIP/XCI", "ru_RU": "Установить прошивку из XCI или ZIP", "sv_SE": "Installera en firmware från XCI eller ZIP", - "th_TH": "ติดตั้งเฟิร์มแวร์จาก ไฟล์ XCI หรือ ไฟล์ ZIP", + "th_TH": "ติดตั้งเฟิร์มแวร์จากไฟล์ XCI หรือ ZIP", "tr_TR": "XCI veya ZIP'ten Yazılım Yükle", "uk_UA": "Встановити прошивку з XCI або ZIP", "zh_CN": "从 XCI 或 ZIP 文件安装系统固件", @@ -840,7 +852,7 @@ "pt_BR": "Instalar firmware a partir de um diretório", "ru_RU": "Установить прошивку из папки", "sv_SE": "Installera en firmware från en katalog", - "th_TH": "ติดตั้งเฟิร์มแวร์จากไดเร็กทอรี", + "th_TH": "ติดตั้งเฟิร์มแวร์จากไดเรกทอรี", "tr_TR": "Bir Dizin Üzerinden Yazılım Yükle", "uk_UA": "Встановити прошивку з теки", "zh_CN": "从文件夹安装系统固件", @@ -865,7 +877,7 @@ "pt_BR": "", "ru_RU": "Установить ключи", "sv_SE": "Installera nycklar", - "th_TH": "", + "th_TH": "ติดตั้งคีย์", "tr_TR": "", "uk_UA": "Встановити ключі", "zh_CN": "安装密匙", @@ -890,7 +902,11 @@ "pt_BR": "", "ru_RU": "Установить ключи из файла KEYS или ZIP", "sv_SE": "Installera nycklar från KEYS eller ZIP", - "th_TH": "", +<<<<<<< HEAD + "th_TH": "ติดตั้งคีย์จาก KEYS หรือ ZIP", +======= + "th_TH": "ติดตั้งคีย์จากไฟล์ KEYS หรือ ZIP", +>>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Встановити ключі з файлу .KEYS або .ZIP", "zh_CN": "从 .KEYS 文件或 .ZIP 压缩包安装密匙", @@ -915,7 +931,11 @@ "pt_BR": "", "ru_RU": "Установить ключи из папки", "sv_SE": "Installera nycklar från en katalog", - "th_TH": "", +<<<<<<< HEAD + "th_TH": "ติดตั้งคีย์จากไดเร็กทอรี", +======= + "th_TH": "ติดตั้งคีย์จากไดเรกทอรี", +>>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Встановити ключі з теки", "zh_CN": "从一个文件夹安装密匙", @@ -1015,7 +1035,11 @@ "pt_BR": "", "ru_RU": "Уменьшить размер XCI файлов", "sv_SE": "Optimera XCI-filer", - "th_TH": "", +<<<<<<< HEAD + "th_TH": "ตัดแต่งไฟล์ XCI", +======= + "th_TH": "ตัดไฟล์ XCI", +>>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Обрізати XCI файли", "zh_CN": "瘦身 XCI 文件", @@ -1040,7 +1064,7 @@ "pt_BR": "", "ru_RU": "_Вид", "sv_SE": "_Visa", - "th_TH": "_มุมมอง", + "th_TH": "_ดู", "tr_TR": "_Görüntüle", "uk_UA": "_Вид", "zh_CN": "视图(_V)", @@ -1090,7 +1114,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "720p", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -1115,7 +1139,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "1080p", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -1140,7 +1164,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "1440p", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -1165,7 +1189,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "2160p", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -1215,7 +1239,7 @@ "pt_BR": "_Verificar se há atualizações", "ru_RU": "Проверить наличие обновлений", "sv_SE": "Leta efter uppdateringar", - "th_TH": "ตรวจสอบอัปเดต", + "th_TH": "ตรวจสอบการอัปเดต", "tr_TR": "Güncellemeleri Denetle", "uk_UA": "Перевірити оновлення", "zh_CN": "检查更新", @@ -1240,7 +1264,11 @@ "pt_BR": "", "ru_RU": "FAQ и Руководства", "sv_SE": "Frågor, svar och guider", - "th_TH": "", +<<<<<<< HEAD + "th_TH": "คำถามที่พบบ่อยและคำแนะนำ", +======= + "th_TH": "คำถามที่พบบ่อย & คู่มือ", +>>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "FAQ та посібники", "zh_CN": "问答与指南", @@ -1265,7 +1293,11 @@ "pt_BR": "", "ru_RU": "FAQ & Устранение неполадок", "sv_SE": "Frågor, svar och felsökningssida", - "th_TH": "", +<<<<<<< HEAD + "th_TH": "หน้าคำถามที่พบบ่อยและการแก้ไขปัญหา", +======= + "th_TH": "คำถามที่พบบ่อย & หน้าการแก้ไขปัญหา", +>>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "FAQ та усунення несправностей (eng)", "zh_CN": "常见问题和问题排除页面", @@ -1290,7 +1322,11 @@ "pt_BR": "", "ru_RU": "Открывает страницы с FAQ и Устранением неполадок на официальной странице вики Ryujinx", "sv_SE": "Öppnar Frågor, svar och felsökningssidan på den officiella Ryujinx-wikin", - "th_TH": "", +<<<<<<< HEAD + "th_TH": "เปิดหน้าคำถามที่พบบ่อยและการแก้ไขปัญหาบนวิกิ Ryujinx อย่างเป็นทางการ", +======= + "th_TH": "เปิดหน้าคำถามที่พบบ่อยและการแก้ไขปัญหาบนวิกิทางการของ Ryujinx", +>>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Відкриває сторінку з Посібником по усуненню помилок та несправностей на офіційній вікі-сторінці Ryujinx (англійською)", "zh_CN": "打开 Ryujinx 官方 Wiki 的常见问题和问题排除页面", @@ -1315,7 +1351,11 @@ "pt_BR": "", "ru_RU": "Руководство по установке и настройке", "sv_SE": "Konfigurationsguide", - "th_TH": "", +<<<<<<< HEAD + "th_TH": "คู่มือการติดตั้งและกำหนดค่า", +======= + "th_TH": "คู่มือการตั้งค่าและการกำหนดค่า", +>>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Посібник зі встановлення та налаштування (eng)", "zh_CN": "安装与配置指南", @@ -1340,7 +1380,11 @@ "pt_BR": "", "ru_RU": "Открывает страницу Руководство по установке и настройке на официальной странице вики Ryujinx", "sv_SE": "Öppnar konfigurationsguiden på den officiella Ryujinx-wikin", - "th_TH": "", +<<<<<<< HEAD + "th_TH": "เปิดคู่มือการติดตั้งและกำหนดค่าบนวิกิ Ryujinx อย่างเป็นทางการ", +======= + "th_TH": "เปิดคู่มือการตั้งค่าและการกำหนดค่าบนวิกิทางการของ Ryujinx", +>>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Відкриває посібник з Налаштування та конфігурації на офіційній вікі-сторінці Ryujinx (англійською)", "zh_CN": "打开 Ryujinx 官方 Wiki 的安装与配置指南", @@ -1365,7 +1409,11 @@ "pt_BR": "", "ru_RU": "Гайд по мультиплееру (LDN/LAN)", "sv_SE": "Flerspelarguide (LDN/LAN)", - "th_TH": "", +<<<<<<< HEAD + "th_TH": "คู่มือการเล่นหลายผู้เล่น (LDN/LAN)", +======= + "th_TH": "คู่มือผู้เล่นหลายคน (LDN/LAN)", +>>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Посібник з мультиплеєру (LDN/LAN) (eng)", "zh_CN": "多人游戏(LDN/LAN)指南", @@ -1390,7 +1438,11 @@ "pt_BR": "", "ru_RU": "Открывает гайд по мультиплееру на официальной странице вики Ryujinx", "sv_SE": "Öppnar flerspelarguiden på den officiella Ryujinx-wikin", - "th_TH": "", +<<<<<<< HEAD + "th_TH": "เปิดคู่มือผู้เล่นหลายคนบนวิกิ Ryujinx อย่างเป็นทางการ", +======= + "th_TH": "เปิดคู่มือผู้เล่นหลายคนบนวิกิทางการของ Ryujinx", +>>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Відкриває посібник з налаштування Мультиплеєру на офіційній вікі-сторінці Ryujinx (англійською)", "zh_CN": "打开 Ryujinx 官方 Wiki 的多人游戏指南", @@ -1440,7 +1492,7 @@ "pt_BR": "Buscar...", "ru_RU": "Поиск...", "sv_SE": "Sök...", - "th_TH": "กำลังค้นหา...", + "th_TH": "ค้นหา...", "tr_TR": "Ara...", "uk_UA": "Пошук...", "zh_CN": "搜索...", @@ -1540,7 +1592,7 @@ "pt_BR": "Desenvolvedor", "ru_RU": "Разработчик", "sv_SE": "Utvecklare", - "th_TH": "ผู้พัฒนา", + "th_TH": "นักพัฒนา", "tr_TR": "Geliştirici", "uk_UA": "Розробник", "zh_CN": "制作商", @@ -1565,7 +1617,7 @@ "pt_BR": "Versão", "ru_RU": "Версия", "sv_SE": "", - "th_TH": "เวอร์ชั่น", + "th_TH": "เวอร์ชัน", "tr_TR": "Sürüm", "uk_UA": "Версія", "zh_CN": "版本", @@ -1590,7 +1642,7 @@ "pt_BR": "Tempo de jogo", "ru_RU": "Время в игре", "sv_SE": "Speltid", - "th_TH": "เล่นไปแล้ว", + "th_TH": "เวลาที่เล่น", "tr_TR": "Oynama Süresi", "uk_UA": "Зіграно часу", "zh_CN": "游玩时长", @@ -1690,7 +1742,7 @@ "pt_BR": "Caminho", "ru_RU": "Путь", "sv_SE": "Sökväg", - "th_TH": "ที่อยู่ไฟล์", + "th_TH": "เส้นทาง", "tr_TR": "Yol", "uk_UA": "Шлях", "zh_CN": "路径", @@ -1715,7 +1767,7 @@ "pt_BR": "Abrir diretório de saves do usuário", "ru_RU": "Открыть папку с сохранениями", "sv_SE": "Öppna användarkatalog för sparningar", - "th_TH": "เปิดไดเร็กทอรี่บันทึกของผู้ใช้", + "th_TH": "เปิดไดเรกทอรีบันทึกของผู้ใช้", "tr_TR": "Kullanıcı Kayıt Dosyası Dizinini Aç", "uk_UA": "Відкрити теку збережень користувача", "zh_CN": "打开用户存档目录", @@ -1740,7 +1792,7 @@ "pt_BR": "Abre o diretório que contém jogos salvos para o usuário atual", "ru_RU": "Открывает папку с пользовательскими сохранениями", "sv_SE": "Öppnar katalogen som innehåller applikationens användarsparade spel", - "th_TH": "เปิดไดเร็กทอรี่ซึ่งมีการบันทึกข้อมูลของผู้ใช้แอปพลิเคชัน", + "th_TH": "เปิดไดเรกทอรีที่มีบันทึกของผู้ใช้แอปพลิเคชัน", "tr_TR": "Uygulamanın Kullanıcı Kaydı'nın bulunduğu dizini açar", "uk_UA": "Відкриває теку, яка містить збереження користувача", "zh_CN": "打开储存游戏用户存档的目录", @@ -1765,7 +1817,7 @@ "pt_BR": "Abrir diretório de saves de dispositivo do usuário", "ru_RU": "Открыть папку сохраненных устройств", "sv_SE": "Öppna enhetens katalog för sparade spel", - "th_TH": "เปิดไดเร็กทอรี่บันทึกของอุปกรณ์", + "th_TH": "เปิดไดเรกทอรีบันทึกของอุปกรณ์", "tr_TR": "Kullanıcı Cihaz Dizinini Aç", "uk_UA": "Відкрити теку пристроїв користувача", "zh_CN": "打开系统数据目录", @@ -1790,7 +1842,7 @@ "pt_BR": "Abre o diretório que contém saves do dispositivo para o usuário atual", "ru_RU": "Открывает папку, содержащую сохраненные устройства", "sv_SE": "Öppnar katalogen som innehåller applikationens sparade spel på enheten", - "th_TH": "เปิดไดเรกทอรี่ซึ่งมีบันทึกข้อมูลของอุปกรณ์ในแอปพลิเคชัน", + "th_TH": "เปิดไดเรกทอรีที่มีบันทึกอุปกรณ์ของแอปพลิเคชัน", "tr_TR": "Uygulamanın Kullanıcı Cihaz Kaydı'nın bulunduğu dizini açar", "uk_UA": "Відкриває теку, яка містить збережені пристрої", "zh_CN": "打开储存游戏系统数据的目录", @@ -1815,7 +1867,7 @@ "pt_BR": "Abrir diretório de saves BCAT do usuário", "ru_RU": "Открыть папку сохраненных BCAT", "sv_SE": "Öppna katalog för BCAT-sparningar", - "th_TH": "เปิดไดเรกทอรี่บันทึกของ BCAT", + "th_TH": "เปิดไดเรกทอรีบันทึก BCAT", "tr_TR": "Kullanıcı BCAT Dizinini Aç", "uk_UA": "Відкрити теку збережень BCAT", "zh_CN": "打开 BCAT 数据目录", @@ -1840,7 +1892,7 @@ "pt_BR": "Abre o diretório que contém saves BCAT para o usuário atual", "ru_RU": "Открывает папку, содержащую сохраненные BCAT", "sv_SE": "Öppnar katalogen som innehåller applikationens BCAT-sparningar", - "th_TH": "เปิดไดเรกทอรี่ซึ่งมีการบันทึกข้อมูลของ BCAT ในแอปพลิเคชัน", + "th_TH": "เปิดไดเรกทอรีที่มีบันทึก BCAT ของแอปพลิเคชัน", "tr_TR": "Uygulamanın Kullanıcı BCAT Kaydı'nın bulunduğu dizini açar", "uk_UA": "Відкриває теку, яка містить BCAT-збереження програми", "zh_CN": "打开储存游戏 BCAT 数据的目录", @@ -1865,7 +1917,7 @@ "pt_BR": "Gerenciar atualizações do jogo", "ru_RU": "Управление обновлениями", "sv_SE": "Hantera speluppdateringar", - "th_TH": "จัดการเวอร์ชั่นอัปเดต", + "th_TH": "จัดการอัปเดตเกม", "tr_TR": "Oyun Güncellemelerini Yönet", "uk_UA": "Керування оновленнями", "zh_CN": "管理游戏更新", @@ -1890,7 +1942,7 @@ "pt_BR": "Abre a janela de gerenciamento de atualizações", "ru_RU": "Открывает окно управления обновлениями приложения", "sv_SE": "Öppnar spelets hanteringsfönster för uppdateringar", - "th_TH": "เปิดหน้าต่างการจัดการเวอร์ชั่นการอัพเดต", + "th_TH": "เปิดหน้าต่างการจัดการอัปเดตเกม", "tr_TR": "Oyun Güncelleme Yönetim Penceresini Açar", "uk_UA": "Відкриває меню керування оновленнями до гри (застосунку)", "zh_CN": "打开游戏更新管理窗口", @@ -1940,7 +1992,7 @@ "pt_BR": "Abre a janela de gerenciamento de DLCs", "ru_RU": "Открывает окно управления DLC", "sv_SE": "Öppnar DLC-hanteringsfönstret", - "th_TH": "เปิดหน้าต่างจัดการ DLC", + "th_TH": "เปิดหน้าต่างการจัดการ DLC", "tr_TR": "DLC yönetim penceresini açar", "uk_UA": "Відкриває меню керування DLC", "zh_CN": "打开 DLC 管理窗口", @@ -1990,7 +2042,7 @@ "pt_BR": "Limpar cache PPTC", "ru_RU": "Перестроить очередь PPTC", "sv_SE": "Kölägg PPTC Rebuild", - "th_TH": "เพิ่มคิวการสร้าง PPTC ใหม่", + "th_TH": "คิวการสร้าง PPTC ใหม่", "tr_TR": "PPTC Yeniden Yapılandırmasını Başlat", "uk_UA": "Додати до черги перекомпіляцію PPTC", "zh_CN": "清除 PPTC 缓存文件", @@ -2015,7 +2067,7 @@ "pt_BR": "Deleta o cache PPTC armazenado em disco do jogo", "ru_RU": "Запускает перестройку PPTC во время следующего запуска игры.", "sv_SE": "Gör så att PPTC bygger om vid uppstart när nästa spel startas", - "th_TH": "ให้ PPTC สร้างใหม่ในเวลาบูตเมื่อเปิดเกมครั้งถัดไป", + "th_TH": "ตั้งให้สร้าง PPTC ใหม่เมื่อเริ่มเกมครั้งถัดไป", "tr_TR": "Oyunun bir sonraki açılışında PPTC'yi yeniden yapılandır", "uk_UA": "Видаляє кеш PPTC застосунку (гри)", "zh_CN": "删除游戏的 PPTC 缓存文件,下次启动游戏时重新编译生成 PPTC 缓存文件", @@ -2040,7 +2092,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ล้างแคช PPTC", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -2065,7 +2117,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ลบไฟล์แคช PPTC ทั้งหมดสำหรับแอปพลิเคชัน", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -2140,7 +2192,7 @@ "pt_BR": "Abrir diretório do cache PPTC", "ru_RU": "Открыть папку PPTC", "sv_SE": "Öppna PPTC-katalog", - "th_TH": "เปิดไดเรกทอรี่ PPTC", + "th_TH": "เปิดไดเรกทอรี PPTC", "tr_TR": "PPTC Dizinini Aç", "uk_UA": "Відкрити теку PPTC", "zh_CN": "打开 PPTC 缓存目录", @@ -2365,7 +2417,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "DLC ของ RomFS", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -2390,7 +2442,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "แยก RomFS จากไฟล์ DLC ที่เลือก", "tr_TR": "", "uk_UA": "", "zh_CN": "从选定的 DLC 文件中解压 RomFS", @@ -2640,7 +2692,7 @@ "pt_BR": "", "ru_RU": "Проверить и обрезать XCI файл", "sv_SE": "Kontrollera och optimera XCI-fil", - "th_TH": "", + "th_TH": "ตรวจสอบและตัดแต่งไฟล์ XCI", "tr_TR": "", "uk_UA": "Перевірка та нарізка XCI Файлу", "zh_CN": "检查并瘦身 XCI 文件", @@ -2665,7 +2717,7 @@ "pt_BR": "", "ru_RU": "Проверить и обрезать XCI файл для уменьшения его размера", "sv_SE": "Kontrollera och optimera XCI-fil för att spara diskutrymme", - "th_TH": "", + "th_TH": "ตรวจสอบและตัดไฟล์ XCI เพื่อประหยัดพื้นที่ดิสก์", "tr_TR": "", "uk_UA": "Перевірити та обрізати XCI Файл задля збереження місця на диску", "zh_CN": "检查并瘦身 XCI 文件以节约磁盘空间", @@ -2690,7 +2742,7 @@ "pt_BR": "{0}/{1} jogos carregados", "ru_RU": "{0}/{1} игр загружено", "sv_SE": "{0}/{1} spel inlästa", - "th_TH": "เกมส์โหลดแล้ว {0}/{1}", + "th_TH": "{0}/{1} เกมโหลดแล้ว", "tr_TR": "{0}/{1} Oyun Yüklendi", "uk_UA": "{0}/{1} ігор завантажено", "zh_CN": "{0}/{1} 游戏加载完成", @@ -2715,7 +2767,7 @@ "pt_BR": "Versão do firmware: {0}", "ru_RU": "Версия прошивки: {0}", "sv_SE": "Firmware-version: {0}", - "th_TH": "", + "th_TH": "เวอร์ชันเฟิร์มแวร์: {0}", "tr_TR": "", "uk_UA": "Версія прошивки: {0}", "zh_CN": "系统固件版本:{0}", @@ -2740,7 +2792,7 @@ "pt_BR": "", "ru_RU": "Обрезается XCI файл '{0}'", "sv_SE": "Optimerar XCI-filen '{0}'", - "th_TH": "", + "th_TH": "การตัดแต่งไฟล์ XCI '{0}'", "tr_TR": "", "uk_UA": "Обрізається XCI Файлів '{0}'", "zh_CN": "正在瘦身 XCI 文件 '{0}'", @@ -3090,7 +3142,7 @@ "pt_BR": "Lembrar tamanho/posição da Janela", "ru_RU": "Запомнить размер/положение окна", "sv_SE": "Kom ihåg fönstrets storlek/position", - "th_TH": "จดจำ ขนาดหน้าต่างแอพพลิเคชั่น/คำแหน่ง", + "th_TH": "จำขนาด/ตำแหน่งของหน้าต่าง", "tr_TR": "", "uk_UA": "Запам'ятати Розмір/Позицію вікна", "zh_CN": "记住窗口大小和位置", @@ -3115,7 +3167,7 @@ "pt_BR": "", "ru_RU": "Показать строку заголовка (требуется перезапуск)", "sv_SE": "Visa titelrad (kräver omstart)", - "th_TH": "", + "th_TH": "แสดงแถบชื่อ (ต้องรีสตาร์ท)", "tr_TR": "", "uk_UA": "Показувати рядок заголовка (Потрібен перезапуск)", "zh_CN": "显示标题栏 (需要重启)", @@ -3290,7 +3342,7 @@ "pt_BR": "DLCs e Atualizações que se referem a arquivos ausentes serão descarregadas automaticamente", "ru_RU": "DLC и обновления, которые ссылаются на отсутствующие файлы, будут выгружаться автоматически", "sv_SE": "DLC och speluppdateringar som refererar till saknade filer kommer inte att läsas in automatiskt", - "th_TH": "", + "th_TH": "DLC และการอัพเดทที่อ้างถึงไฟล์ที่ขาดหายไปจะถูกลบออกโดยอัตโนมัติ", "tr_TR": "", "uk_UA": "DLC та Оновлення, які посилаються на відсутні файли, будуть автоматично вимкнуті.", "zh_CN": "DLC 及 游戏更新 可自动加载和卸载", @@ -4065,7 +4117,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Svenska", - "th_TH": "", + "th_TH": "สวีเดน", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -4090,7 +4142,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Norska", - "th_TH": "", + "th_TH": "นอร์เว", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -4165,7 +4217,7 @@ "pt_BR": "", "ru_RU": "Повторная синхронизация с датой и временем на компьютере", "sv_SE": "Återsynka till datorns datum och tid", - "th_TH": "", + "th_TH": "ซิงค์กับวันที่และเวลาพีซี", "tr_TR": "", "uk_UA": "Синхронізувати з датою та часом ПК", "zh_CN": "与 PC 日期和时间重新同步", @@ -4290,7 +4342,7 @@ "pt_BR": "Nenhuma", "ru_RU": "Без звука", "sv_SE": "", - "th_TH": "", + "th_TH": "ตัวอย่างจำลอง", "tr_TR": "Yapay", "uk_UA": "Вимкнено", "zh_CN": "无", @@ -4340,7 +4392,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ระบบจัดการเสียง อินพุต/เอาต์พุต", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -4465,7 +4517,7 @@ "pt_BR": "", "ru_RU": "4ГиБ", "sv_SE": "", - "th_TH": "", + "th_TH": "4 กิกะไบต์", "tr_TR": "", "uk_UA": "4Гб", "zh_CN": "", @@ -4490,7 +4542,7 @@ "pt_BR": "", "ru_RU": "6ГиБ", "sv_SE": "", - "th_TH": "", + "th_TH": "6 กิกะไบต์", "tr_TR": "", "uk_UA": "6Гб", "zh_CN": "", @@ -4515,7 +4567,7 @@ "pt_BR": "", "ru_RU": "8ГиБ", "sv_SE": "", - "th_TH": "", + "th_TH": "8 กิกะไบต์", "tr_TR": "", "uk_UA": "8Гб", "zh_CN": "", @@ -4540,7 +4592,7 @@ "pt_BR": "", "ru_RU": "12ГиБ", "sv_SE": "", - "th_TH": "", + "th_TH": "12 กิกะไบต์", "tr_TR": "", "uk_UA": "12Гб", "zh_CN": "", @@ -4565,7 +4617,7 @@ "pt_BR": "Ignorar serviços não implementados", "ru_RU": "Игнорировать отсутствующие службы", "sv_SE": "Ignorera saknade tjänster", - "th_TH": "เมินเฉยบริการที่หายไป", + "th_TH": "เมินไม่สนใจบริการที่ขาดหายไป", "tr_TR": "Eksik Servisleri Görmezden Gel", "uk_UA": "Ігнорувати відсутні служби", "zh_CN": "忽略缺失的服务", @@ -4590,7 +4642,7 @@ "pt_BR": "Ignorar applet", "ru_RU": "Игнорировать Апплет", "sv_SE": "Ignorera applet", - "th_TH": "เมินเฉย Applet", + "th_TH": "เพิกเฉยต่อแอปเพล็ต", "tr_TR": "", "uk_UA": "Ігнорувати Аплет", "zh_CN": "忽略小程序", @@ -5690,7 +5742,7 @@ "pt_BR": "Controle", "ru_RU": "Управление", "sv_SE": "Inmatning", - "th_TH": "ป้อนข้อมูล", + "th_TH": "ตั้งค่าปุ่ม", "tr_TR": "Giriş Yöntemi", "uk_UA": "Введення", "zh_CN": "输入", @@ -6515,7 +6567,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม A", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -6540,7 +6592,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม B", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -6565,7 +6617,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม X", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -6590,7 +6642,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Y", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -7190,7 +7242,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ทริกเกอร์ L", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -7215,7 +7267,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ทริกเกอร์ R", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -7240,7 +7292,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ทริกเกอร์ ZL", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -7265,7 +7317,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ทริกเกอร์ ZR", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -7290,7 +7342,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "SL ซ้าย", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -7315,7 +7367,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "SR ซ้าย", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -7340,7 +7392,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "SL ขวา", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -7365,7 +7417,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "SR ขวา", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -7715,7 +7767,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปิดการใช้งาน", "tr_TR": "", "uk_UA": "", "zh_CN": "关闭", @@ -7740,7 +7792,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "สายรุ้ง", "tr_TR": "", "uk_UA": "", "zh_CN": "彩虹", @@ -7765,7 +7817,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ความเร็วของเอฟเฟกต์สายรุ้ง", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -7790,7 +7842,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "สี", "tr_TR": "", "uk_UA": "", "zh_CN": "颜色", @@ -7890,7 +7942,7 @@ "pt_BR": "", "ru_RU": "Левый Shift", "sv_SE": "Skift vänster", - "th_TH": "", + "th_TH": "Shift ซ้าย", "tr_TR": "Sol Shift", "uk_UA": "Shift Лівий", "zh_CN": "左侧Shift", @@ -7915,7 +7967,7 @@ "pt_BR": "", "ru_RU": "Правый Shift", "sv_SE": "Skift höger", - "th_TH": "", + "th_TH": "Shift ขวา", "tr_TR": "Sağ Shift", "uk_UA": "Shift Правий", "zh_CN": "右侧Shift", @@ -7940,7 +7992,7 @@ "pt_BR": "", "ru_RU": "Левый Ctrl", "sv_SE": "Ctrl vänster", - "th_TH": "", + "th_TH": "Ctrl ซ้าย", "tr_TR": "Sol Ctrl", "uk_UA": "Ctrl Лівий", "zh_CN": "左侧Ctrl", @@ -7965,7 +8017,7 @@ "pt_BR": "", "ru_RU": "Левый ⌃", "sv_SE": "^ Vänster", - "th_TH": "", + "th_TH": "⌃ ซ้าย", "tr_TR": "⌃ Sol", "uk_UA": "⌃ Лівий", "zh_CN": "左侧⌃", @@ -7990,7 +8042,7 @@ "pt_BR": "", "ru_RU": "Правый Ctrl", "sv_SE": "Ctrl höger", - "th_TH": "", + "th_TH": "Ctrl ขวา", "tr_TR": "Sağ Control", "uk_UA": "Ctrl Правий", "zh_CN": "右侧Ctrl", @@ -8015,7 +8067,7 @@ "pt_BR": "", "ru_RU": "Правый ⌃", "sv_SE": "^ Höger", - "th_TH": "", + "th_TH": "⌃ ขวา", "tr_TR": "⌃ Sağ", "uk_UA": "⌃ Правий", "zh_CN": "右侧⌃", @@ -8040,7 +8092,7 @@ "pt_BR": "", "ru_RU": "Левый Alt", "sv_SE": "Alt vänster", - "th_TH": "", + "th_TH": "Alt ซ้าย", "tr_TR": "Sol Alt", "uk_UA": "Alt Лівий", "zh_CN": "左侧Alt", @@ -8065,7 +8117,7 @@ "pt_BR": "", "ru_RU": "Левый ⌥", "sv_SE": "⌥ vänster", - "th_TH": "", + "th_TH": "⌥ ซ้าย", "tr_TR": "⌥ Sol", "uk_UA": "⌥ Лівий", "zh_CN": "左侧⌥", @@ -8090,7 +8142,7 @@ "pt_BR": "", "ru_RU": "Правый Alt", "sv_SE": "Alt höger", - "th_TH": "", + "th_TH": "Alt ขวา", "tr_TR": "Sağ Alt", "uk_UA": "Alt Правий", "zh_CN": "右侧Alt", @@ -8115,7 +8167,7 @@ "pt_BR": "", "ru_RU": "Правый ⌥", "sv_SE": "⌥ höger", - "th_TH": "", + "th_TH": "⌥ ขวา", "tr_TR": "⌥ Sağ", "uk_UA": "⌥ Правий", "zh_CN": "右侧⌥", @@ -8140,7 +8192,7 @@ "pt_BR": "", "ru_RU": "Левый ⊞", "sv_SE": "⊞ vänster", - "th_TH": "", + "th_TH": "⊞ ซ้าย", "tr_TR": "⊞ Sol", "uk_UA": "⊞ Лівий", "zh_CN": "左侧⊞", @@ -8165,7 +8217,7 @@ "pt_BR": "", "ru_RU": "Левый ⌘", "sv_SE": "⌘ vänster", - "th_TH": "", + "th_TH": "⌘ ซ้าย", "tr_TR": "⌘ Sol", "uk_UA": "⌘ Лівий", "zh_CN": "左侧⌘", @@ -8190,7 +8242,7 @@ "pt_BR": "", "ru_RU": "Правый ⊞", "sv_SE": "⊞ höger", - "th_TH": "", + "th_TH": "⊞ ขวา", "tr_TR": "⊞ Sağ", "uk_UA": "⊞ Правий", "zh_CN": "右侧⊞", @@ -8215,7 +8267,7 @@ "pt_BR": "", "ru_RU": "Правый ⌘", "sv_SE": "⌘ höger", - "th_TH": "", + "th_TH": "⌘ ขวา", "tr_TR": "⌘ Sağ", "uk_UA": "⌘ Правий", "zh_CN": "右侧⌘", @@ -8240,7 +8292,7 @@ "pt_BR": "", "ru_RU": "Меню", "sv_SE": "Meny", - "th_TH": "", + "th_TH": "เมนู", "tr_TR": "Menü", "uk_UA": "Меню", "zh_CN": "菜单键", @@ -8265,7 +8317,7 @@ "pt_BR": "", "ru_RU": "Вверх", "sv_SE": "Upp", - "th_TH": "", + "th_TH": "ขึ้น", "tr_TR": "Yukarı", "uk_UA": "Вгору ↑", "zh_CN": "上", @@ -8290,7 +8342,7 @@ "pt_BR": "", "ru_RU": "Вниз", "sv_SE": "Ner", - "th_TH": "", + "th_TH": "ลง", "tr_TR": "Aşağı", "uk_UA": "Вниз ↓", "zh_CN": "下", @@ -8315,7 +8367,7 @@ "pt_BR": "", "ru_RU": "Влево", "sv_SE": "Vänster", - "th_TH": "", + "th_TH": "ซ้าย", "tr_TR": "Sol", "uk_UA": "Вліво ←", "zh_CN": "左", @@ -8340,7 +8392,7 @@ "pt_BR": "", "ru_RU": "Вправо", "sv_SE": "Höger", - "th_TH": "", + "th_TH": "ขวา", "tr_TR": "Sağ", "uk_UA": "Вправо →", "zh_CN": "右", @@ -8365,7 +8417,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Enter", "tr_TR": "", "uk_UA": "", "zh_CN": "回车键", @@ -8390,7 +8442,7 @@ "pt_BR": "", "ru_RU": "Esc", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Escape", "tr_TR": "Esc", "uk_UA": "", "zh_CN": "Esc", @@ -8415,7 +8467,7 @@ "pt_BR": "", "ru_RU": "Пробел", "sv_SE": "Blanksteg", - "th_TH": "", + "th_TH": "ปุ่ม Spacebar", "tr_TR": "", "uk_UA": "Пробіл", "zh_CN": "空格键", @@ -8440,7 +8492,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Tab", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -8465,7 +8517,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Backspace", "tr_TR": "Geri tuşu", "uk_UA": "", "zh_CN": "退格键", @@ -8490,7 +8542,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Insert", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -8515,7 +8567,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Delete", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -8540,7 +8592,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Page Up", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -8565,7 +8617,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Page Down", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -8590,7 +8642,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Home", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -8615,7 +8667,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม End", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -8640,7 +8692,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Caps Lock", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -8665,7 +8717,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Scroll Lock", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -8690,7 +8742,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Print Screen", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -8715,7 +8767,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Pause", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -8740,7 +8792,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "นัมล็อค", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -8765,7 +8817,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Töm", - "th_TH": "", + "th_TH": "ล้าง", "tr_TR": "", "uk_UA": "Очистити", "zh_CN": "清除键", @@ -8790,7 +8842,7 @@ "pt_BR": "", "ru_RU": "0 (цифровий блок)", "sv_SE": "", - "th_TH": "", + "th_TH": "แป้นพิมพ์ตัวเลข 0", "tr_TR": "", "uk_UA": "Блок цифр 0", "zh_CN": "小键盘0", @@ -8815,7 +8867,7 @@ "pt_BR": "", "ru_RU": "1 (цифровий блок)", "sv_SE": "", - "th_TH": "", + "th_TH": "แป้นพิมพ์ตัวเลข 1", "tr_TR": "", "uk_UA": "Блок цифр 1", "zh_CN": "小键盘1", @@ -8840,7 +8892,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 2", "sv_SE": "", - "th_TH": "", + "th_TH": "แป้นพิมพ์ตัวเลข 2", "tr_TR": "", "uk_UA": "2 (цифровий блок)", "zh_CN": "小键盘2", @@ -8865,7 +8917,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 3", "sv_SE": "", - "th_TH": "", + "th_TH": "แป้นพิมพ์ตัวเลข 3", "tr_TR": "", "uk_UA": "3 (цифровий блок)", "zh_CN": "小键盘3", @@ -8890,7 +8942,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 4", "sv_SE": "", - "th_TH": "", + "th_TH": "แป้นพิมพ์ตัวเลข 4", "tr_TR": "", "uk_UA": "4 (цифровий блок)", "zh_CN": "小键盘4", @@ -8915,7 +8967,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 5", "sv_SE": "", - "th_TH": "", + "th_TH": "แป้นพิมพ์ตัวเลข 5", "tr_TR": "", "uk_UA": "5 (цифровий блок)", "zh_CN": "小键盘5", @@ -8940,7 +8992,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 6", "sv_SE": "", - "th_TH": "", + "th_TH": "แป้นพิมพ์ตัวเลข 6", "tr_TR": "", "uk_UA": "6 (цифровий блок)", "zh_CN": "小键盘6", @@ -8965,7 +9017,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 7", "sv_SE": "", - "th_TH": "", + "th_TH": "แป้นพิมพ์ตัวเลข 7", "tr_TR": "", "uk_UA": "7 (цифровий блок)", "zh_CN": "小键盘7", @@ -8990,7 +9042,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 8", "sv_SE": "", - "th_TH": "", + "th_TH": "แป้นพิมพ์ตัวเลข 8", "tr_TR": "", "uk_UA": "8 (цифровий блок)", "zh_CN": "小键盘8", @@ -9015,7 +9067,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 9", "sv_SE": "", - "th_TH": "", + "th_TH": "แป้นพิมพ์ตัวเลข 9", "tr_TR": "", "uk_UA": "9 (цифровий блок)", "zh_CN": "小键盘9", @@ -9040,7 +9092,7 @@ "pt_BR": "", "ru_RU": "/ (блок цифр)", "sv_SE": "Keypad /", - "th_TH": "", + "th_TH": "ปุ่มเครื่องหมาย /", "tr_TR": "", "uk_UA": "/ (цифровий блок)", "zh_CN": "小键盘/", @@ -9065,7 +9117,7 @@ "pt_BR": "", "ru_RU": "* (блок цифр)", "sv_SE": "Keypad *", - "th_TH": "", + "th_TH": "ปุ่มเครื่องหมาย *", "tr_TR": "", "uk_UA": "* (цифровий блок)", "zh_CN": "小键盘*", @@ -9090,7 +9142,7 @@ "pt_BR": "", "ru_RU": "- (блок цифр)", "sv_SE": "Keypad -", - "th_TH": "", + "th_TH": "ปุ่มเครื่องหมาย -", "tr_TR": "", "uk_UA": "- (цифровий блок)", "zh_CN": "小键盘-", @@ -9115,7 +9167,7 @@ "pt_BR": "", "ru_RU": "+ (блок цифр)", "sv_SE": "Keypad +", - "th_TH": "", + "th_TH": "เพิ่มปุ่มแป้นพิมพ์", "tr_TR": "", "uk_UA": "+ (цифровий блок)", "zh_CN": "小键盘+", @@ -9140,7 +9192,7 @@ "pt_BR": "", "ru_RU": ". (блок цифр)", "sv_SE": "Keypad ,", - "th_TH": "", + "th_TH": "แป้นพิมพ์ตัวเลข", "tr_TR": "", "uk_UA": ". (цифровий блок)", "zh_CN": "小键盘.", @@ -9165,7 +9217,7 @@ "pt_BR": "", "ru_RU": "Enter (блок цифр)", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่มแป้นพิมพ์ Enter", "tr_TR": "", "uk_UA": "Enter (цифровий блок)", "zh_CN": "小键盘回车键", @@ -9740,7 +9792,7 @@ "pt_BR": "", "ru_RU": "Не привязано", "sv_SE": "Obunden", - "th_TH": "", + "th_TH": "ไม่ถูกจำกัด", "tr_TR": "", "uk_UA": "Відв'язати", "zh_CN": "未分配", @@ -9765,7 +9817,7 @@ "pt_BR": "", "ru_RU": "Кнопка лев. стика", "sv_SE": "L-spakknapp", - "th_TH": "", + "th_TH": "ปุ่มสติ๊ก L", "tr_TR": "", "uk_UA": "L Кнопка Стіку", "zh_CN": "左摇杆按键", @@ -9790,7 +9842,7 @@ "pt_BR": "", "ru_RU": "Кнопка пр. стика", "sv_SE": "R-spakknapp", - "th_TH": "", + "th_TH": "ปุ่มสติ๊ก R", "tr_TR": "", "uk_UA": "R Кнопка Стіку", "zh_CN": "右摇杆按键", @@ -9815,7 +9867,7 @@ "pt_BR": "", "ru_RU": "Левый бампер", "sv_SE": "Vänster kantknapp", - "th_TH": "", + "th_TH": "ไหล่ด้านซ้าย", "tr_TR": "", "uk_UA": "Лівий Бампер", "zh_CN": "左肩键L", @@ -9840,7 +9892,7 @@ "pt_BR": "", "ru_RU": "Правый бампер", "sv_SE": "Höger kantknapp", - "th_TH": "", + "th_TH": "ไหล่ด้านขวา", "tr_TR": "", "uk_UA": "Правий Бампер", "zh_CN": "右肩键R", @@ -9865,7 +9917,7 @@ "pt_BR": "", "ru_RU": "Левый триггер", "sv_SE": "Vänster avtryckare", - "th_TH": "", + "th_TH": "ทริกเกอร์ซ้าย", "tr_TR": "", "uk_UA": "Лівий Тригер", "zh_CN": "左扳机键ZL", @@ -9890,7 +9942,7 @@ "pt_BR": "", "ru_RU": "Правый триггер", "sv_SE": "Höger avtryckare", - "th_TH": "", + "th_TH": "ทริกเกอร์ขวา", "tr_TR": "", "uk_UA": "Правий Тригер", "zh_CN": "右扳机键ZR", @@ -9915,7 +9967,7 @@ "pt_BR": "", "ru_RU": "Вверх", "sv_SE": "Upp", - "th_TH": "", + "th_TH": "ขึ้น", "tr_TR": "", "uk_UA": "Вверх", "zh_CN": "上键", @@ -9940,7 +9992,7 @@ "pt_BR": "", "ru_RU": "Вниз", "sv_SE": "Ner", - "th_TH": "", + "th_TH": "ล่าง", "tr_TR": "", "uk_UA": "Вниз", "zh_CN": "下键", @@ -9965,7 +10017,7 @@ "pt_BR": "", "ru_RU": "Влево", "sv_SE": "Vänster", - "th_TH": "", + "th_TH": "ซ้าย", "tr_TR": "", "uk_UA": "Вліво", "zh_CN": "左键", @@ -9990,7 +10042,7 @@ "pt_BR": "", "ru_RU": "Вправо", "sv_SE": "Höger", - "th_TH": "", + "th_TH": "ขวา", "tr_TR": "Sağ", "uk_UA": "Вправо", "zh_CN": "右键", @@ -10065,7 +10117,7 @@ "pt_BR": "", "ru_RU": "Кнопка меню", "sv_SE": "", - "th_TH": "", + "th_TH": "แนะนำ", "tr_TR": "Rehber", "uk_UA": "", "zh_CN": "主页键", @@ -10090,7 +10142,7 @@ "pt_BR": "", "ru_RU": "Прочее", "sv_SE": "Diverse", - "th_TH": "", + "th_TH": "เบ็ดเตล็ด", "tr_TR": "Diğer", "uk_UA": "", "zh_CN": "截图键", @@ -10115,7 +10167,7 @@ "pt_BR": "", "ru_RU": "Доп.кнопка 1", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่มใบพาย 1", "tr_TR": "Pedal 1", "uk_UA": "Додаткова кнопка 1", "zh_CN": "其他按键1", @@ -10140,7 +10192,7 @@ "pt_BR": "", "ru_RU": "Доп.кнопка 2", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่มใบพาย 2", "tr_TR": "Pedal 2", "uk_UA": "Додаткова кнопка 2", "zh_CN": "其他按键2", @@ -10165,7 +10217,7 @@ "pt_BR": "", "ru_RU": "Доп.кнопка 3", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่มใบพาย 3", "tr_TR": "Pedal 3", "uk_UA": "Додаткова кнопка 3", "zh_CN": "其他按键3", @@ -10190,7 +10242,7 @@ "pt_BR": "", "ru_RU": "Доп.кнопка 4", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่มใบพาย 4", "tr_TR": "Pedal 4", "uk_UA": "Додаткова кнопка 4", "zh_CN": "其他按键4", @@ -10215,7 +10267,7 @@ "pt_BR": "", "ru_RU": "Тачпад", "sv_SE": "", - "th_TH": "", + "th_TH": "ทัชแพด", "tr_TR": "", "uk_UA": "", "zh_CN": "触摸板", @@ -10240,7 +10292,7 @@ "pt_BR": "", "ru_RU": "Левый триггер 0", "sv_SE": "Vänster avtryckare 0", - "th_TH": "", + "th_TH": "ทริกเกอร์ซ้าย 0", "tr_TR": "Sol Tetik 0", "uk_UA": "Лівий Тригер 0", "zh_CN": "左扳机0", @@ -10265,7 +10317,7 @@ "pt_BR": "", "ru_RU": "Правый триггер 0", "sv_SE": "Höger avtryckare 0", - "th_TH": "", + "th_TH": "ทริกเกอร์ขวา 0", "tr_TR": "Sağ Tetik 0", "uk_UA": "Правий Тригер 0", "zh_CN": "右扳机0", @@ -10290,7 +10342,7 @@ "pt_BR": "", "ru_RU": "Левый триггер 1", "sv_SE": "Vänster avtryckare 1", - "th_TH": "", + "th_TH": "ทริกเกอร์ซ้าย 1", "tr_TR": "Sol Tetik 1", "uk_UA": "Лівий Тригер 1", "zh_CN": "左扳机1", @@ -10315,7 +10367,7 @@ "pt_BR": "", "ru_RU": "Правый триггер 1", "sv_SE": "Höger avtryckare 1", - "th_TH": "", + "th_TH": "ทริกเกอร์ขวา 1", "tr_TR": "Sağ Tetik 1", "uk_UA": "Правий Тригер 1", "zh_CN": "右扳机1", @@ -10340,7 +10392,7 @@ "pt_BR": "", "ru_RU": "Левый стик", "sv_SE": "Vänster spak", - "th_TH": "", + "th_TH": "สติ๊กซ้าย", "tr_TR": "Sol Çubuk", "uk_UA": "Лівий Стік", "zh_CN": "左摇杆", @@ -10365,7 +10417,7 @@ "pt_BR": "", "ru_RU": "Правый стик", "sv_SE": "Höger spak", - "th_TH": "", + "th_TH": "สติ๊กขวา", "tr_TR": "Sağ çubuk", "uk_UA": "Правий Стік", "zh_CN": "右摇杆", @@ -10790,7 +10842,7 @@ "pt_BR": "", "ru_RU": "Отмена", "sv_SE": "Avbryter", - "th_TH": "", + "th_TH": "การยกเลิก", "tr_TR": "", "uk_UA": "Скасування", "zh_CN": "正在取消", @@ -10815,7 +10867,7 @@ "pt_BR": "", "ru_RU": "Закрыть", "sv_SE": "Stäng", - "th_TH": "", + "th_TH": "ปิด", "tr_TR": "", "uk_UA": "Закрити", "zh_CN": "关闭", @@ -11015,7 +11067,7 @@ "pt_BR": "", "ru_RU": "Показать профиль", "sv_SE": "Visa profil", - "th_TH": "", + "th_TH": "ดูโปรไฟล์", "tr_TR": "", "uk_UA": "Показати профіль", "zh_CN": "预览配置文件", @@ -12115,7 +12167,7 @@ "pt_BR": "", "ru_RU": "Показать список изменений", "sv_SE": "Visa ändringslogg", - "th_TH": "", + "th_TH": "แสดงบันทึกการเปลี่ยนแปลง", "tr_TR": "", "uk_UA": "Показати список змін", "zh_CN": "显示更新日志", @@ -12590,7 +12642,7 @@ "pt_BR": "", "ru_RU": "Окно триммера XCI", "sv_SE": "XCI-optimerare", - "th_TH": "", + "th_TH": "หน้าต่างตัดแต่ง XCI", "tr_TR": "", "uk_UA": "Вікно XCI Тримера", "zh_CN": "XCI 文件瘦身窗口", @@ -12790,7 +12842,7 @@ "pt_BR": "API Amiibo", "ru_RU": "API Amiibo", "sv_SE": "Amiibo-API", - "th_TH": "", + "th_TH": "API ของ Amiibo", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -13040,7 +13092,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "คุณกำลังจะล้างข้อมูล PPTC ทั้งหมดจาก:\n\n{0}\n\nคุณแน่ใจว่าต้องการดำเนินการต่อหรือไม่", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -13340,7 +13392,7 @@ "pt_BR": "", "ru_RU": "В {0} были найдены некорректные ключи", "sv_SE": "En ogiltig nyckelfil hittades i {0}", - "th_TH": "", + "th_TH": "พบไฟล์คีย์ที่ไม่ถูกต้องใน {0}", "tr_TR": "", "uk_UA": "", "zh_CN": "在 {0} 发现了一个无效的密匙文件", @@ -13365,7 +13417,7 @@ "pt_BR": "", "ru_RU": "Установить ключи", "sv_SE": "Installera nycklar", - "th_TH": "", + "th_TH": "ติดตั้งคีย์", "tr_TR": "", "uk_UA": "Встановлення Ключів", "zh_CN": "安装密匙", @@ -13390,7 +13442,7 @@ "pt_BR": "", "ru_RU": "Будут установлены новые ключи.", "sv_SE": "Ny nyckelfil kommer att installeras.", - "th_TH": "", + "th_TH": "ไฟล์คีย์ใหม่จะถูกติดตั้ง", "tr_TR": "", "uk_UA": "Новий файл Ключів буде встановлено", "zh_CN": "将会安装新密匙文件", @@ -13415,7 +13467,7 @@ "pt_BR": "", "ru_RU": "\n\nЭто действие может перезаписать установленные ключи.", "sv_SE": "\n\nDetta kan ersätta några av de redan installerade nycklarna.", - "th_TH": "", + "th_TH": "\n\nสิ่งนี้อาจแทนที่คีย์บางส่วนที่ติดตั้งในปัจจุบัน", "tr_TR": "", "uk_UA": "\n\nЦе замінить собою поточні файли Ключів.", "zh_CN": "\n\n这也许会替换掉一些当前已安装的密匙", @@ -13440,7 +13492,7 @@ "pt_BR": "", "ru_RU": "\n\nХотите продолжить?", "sv_SE": "\n\nVill du fortsätta?", - "th_TH": "", + "th_TH": "\n\nคุณต้องการจะดำเนินการต่อหรือไม่?", "tr_TR": "", "uk_UA": "\n\nВи хочете продовжити?", "zh_CN": "\n\n你想要继续吗?", @@ -13465,7 +13517,7 @@ "pt_BR": "", "ru_RU": "Установка ключей...", "sv_SE": "Installerar nycklar...", - "th_TH": "", + "th_TH": "กำลังติดตั้งคีย์...", "tr_TR": "", "uk_UA": "Встановлення Ключів...", "zh_CN": "安装密匙中。。。", @@ -13490,7 +13542,7 @@ "pt_BR": "", "ru_RU": "Новые ключи были успешно установлены.", "sv_SE": "Ny nyckelfil installerades.", - "th_TH": "", + "th_TH": "ติดตั้งไฟล์คีย์ใหม่สำเร็จแล้ว", "tr_TR": "", "uk_UA": "Нові ключі встановлено.", "zh_CN": "已成功安装新密匙文件", @@ -14490,7 +14542,7 @@ "pt_BR": "", "ru_RU": "Ryujinx - это эмулятор для Nintendo Switch™.\nПолучайте все последние новости разработки в нашем Discord.\nРазработчики, заинтересованные в участии, могут узнать больше на нашем GitHub или Discord.", "sv_SE": "Ryujinx är en emulator för Nintendo Switch™.\nFå de senaste nyheterna via vår Discord.\nUtvecklare som är intresserade att bidra kan hitta mer info på vår GitHub eller Discord.", - "th_TH": "", + "th_TH": "Ryujinx คือโปรแกรมจำลองสำหรับ Nintendo Switch™\nรับข่าวสารล่าสุดทั้งหมดใน Discord ของเรา\nนักพัฒนาที่สนใจในการมีส่วนร่วมสามารถหาข้อมูลเพิ่มเติมได้ใน GitHub หรือ Discord ของเรา", "tr_TR": "", "uk_UA": "Ryujinx — це емулятор для Nintendo Switch™.\nОстанні новини можна отримати в нашому Discord.\nРозробники, що бажають долучитись до розробки та зробити свій внесок, можуть отримати більше інформації на нашому GitHub або в Discord.", "zh_CN": "Ryujinx 是一个 Nintendo Switch™ 模拟器。\n有兴趣做出贡献的开发者可以在我们的 GitHub 或 Discord 上了解更多信息。\n", @@ -14540,7 +14592,7 @@ "pt_BR": "", "ru_RU": "Поддержка:", "sv_SE": "Underhölls tidigare av:", - "th_TH": "", + "th_TH": "ก่อนหน้านี้ได้รับการดูแลโดย:", "tr_TR": "", "uk_UA": "Минулі розробники:", "zh_CN": "曾经的维护者:", @@ -15565,7 +15617,7 @@ "pt_BR": "", "ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.\n\nЭто не активная настройка, она все еще может рассинхронизироваться; в этом случае просто нажмите эту кнопку еще раз.", "sv_SE": "Återsynkronisera systemtiden för att matcha din dators aktuella datum och tid.\n\nDetta är inte en aktiv inställning och den kan tappa synken och om det händer så kan du klicka på denna knapp igen.", - "th_TH": "", + "th_TH": "ซิงค์เวลาของระบบใหม่เพื่อให้ตรงกับวันที่และเวลาปัจจุบันของพีซีของคุณ\n\nนี่ไม่ใช่การตั้งค่าที่ใช้งานอยู่ และอาจไม่ซิงค์ได้ ในกรณีนี้ เพียงคลิกปุ่มนี้อีกครั้ง", "tr_TR": "", "uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.\n\nЦе не активне налаштування, тому синхронізація може збитися; у такому разі просто натискайте цю кнопку знову.", "zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。\n\n这个操作不会实时同步系统时间与电脑时间,时间仍然可能不同步;在这种情况下,只需再次单击此按钮即可。", @@ -17690,7 +17742,7 @@ "pt_BR": "Abrir o guia de configuração", "ru_RU": "Открыть руководство по установке", "sv_SE": "Öppna konfigurationsguiden", - "th_TH": "เปิดคู่มือการตั้งค่า", + "th_TH": "เปิดคู่มือการติดตั้ง", "tr_TR": "Kurulum Kılavuzunu Aç", "uk_UA": "Відкрити посібник із налаштування", "zh_CN": "打开安装指南", @@ -17740,7 +17792,7 @@ "pt_BR": "Versão {0}", "ru_RU": "Версия {0}", "sv_SE": "", - "th_TH": "เวอร์ชั่น {0}", + "th_TH": "เวอร์ชัน {0}", "tr_TR": "Sürüm {0}", "uk_UA": "Версія {0}", "zh_CN": "游戏更新的版本 {0}", @@ -17765,7 +17817,7 @@ "pt_BR": "Empacotado: Versão {0}", "ru_RU": "Баднл: Версия {0}", "sv_SE": "Bundlad: Version {0}", - "th_TH": "Bundled: เวอร์ชั่น {0}", + "th_TH": "ชุดรวม: เวอร์ชัน {0}", "tr_TR": "", "uk_UA": "Комплектні: Версія {0}", "zh_CN": "捆绑:版本 {0}", @@ -17790,7 +17842,7 @@ "pt_BR": "Empacotado:", "ru_RU": "Бандл:", "sv_SE": "Bundlad:", - "th_TH": "", + "th_TH": "รวมเป็นชุด:", "tr_TR": "", "uk_UA": "Комплектні:", "zh_CN": "捆绑:", @@ -17815,7 +17867,7 @@ "pt_BR": "", "ru_RU": "Частично", "sv_SE": "Delvis", - "th_TH": "", + "th_TH": "บางส่วน", "tr_TR": "", "uk_UA": "Часткові", "zh_CN": "分区", @@ -17840,7 +17892,7 @@ "pt_BR": "", "ru_RU": "Не обрезан", "sv_SE": "Inte optimerad", - "th_TH": "", + "th_TH": "ไม่ได้ตัดแต่ง", "tr_TR": "", "uk_UA": "Необрізані", "zh_CN": "没有瘦身的", @@ -17865,7 +17917,7 @@ "pt_BR": "", "ru_RU": "Обрезан", "sv_SE": "Optimerad", - "th_TH": "", + "th_TH": "ตัดแต่งแล้ว", "tr_TR": "", "uk_UA": "Обрізані", "zh_CN": "经过瘦身的", @@ -17890,7 +17942,7 @@ "pt_BR": "", "ru_RU": "(Ошибка)", "sv_SE": "(misslyckades)", - "th_TH": "", + "th_TH": "(ล้มเหลว)", "tr_TR": "", "uk_UA": "(Невдача)", "zh_CN": "(失败)", @@ -17915,7 +17967,7 @@ "pt_BR": "", "ru_RU": "Сохранить {0:n0} Мб", "sv_SE": "Spara {0:n0} Mb", - "th_TH": "", + "th_TH": "บันทึก {0:n0} เมกะไบต์", "tr_TR": "", "uk_UA": "Зберегти {0:n0} Мб", "zh_CN": "能节约 {0:n0} Mb", @@ -17940,7 +17992,7 @@ "pt_BR": "", "ru_RU": "Сохранено {0:n0} Мб", "sv_SE": "Sparade {0:n0} Mb", - "th_TH": "", + "th_TH": "บันทึกแล้ว {0:n0} เมกะไบต์", "tr_TR": "", "uk_UA": "Збережено {0:n0} Мб", "zh_CN": "节约了 {0:n0} Mb", @@ -18040,7 +18092,7 @@ "pt_BR": "Nunca", "ru_RU": "Никогда", "sv_SE": "Aldrig", - "th_TH": "ไม่ต้อง", + "th_TH": "ไม่เคย", "tr_TR": "Hiçbir Zaman", "uk_UA": "Ніколи", "zh_CN": "从不", @@ -18065,7 +18117,7 @@ "pt_BR": "Deve ter pelo menos {0} caracteres", "ru_RU": "Должно быть не менее {0} символов.", "sv_SE": "Får endast vara minst {0} tecken långt", - "th_TH": "ต้องมีความยาวของตัวอักษรอย่างน้อย {0} ตัว", + "th_TH": "ต้องมีความยาวอย่างน้อย {0} อักขระ", "tr_TR": "En az {0} karakter uzunluğunda olmalı", "uk_UA": "Мінімальна кількість символів: {0}", "zh_CN": "不少于 {0} 个字符", @@ -18090,7 +18142,7 @@ "pt_BR": "Deve ter entre {0}-{1} caracteres", "ru_RU": "Должно быть {0}-{1} символов", "sv_SE": "Får endast vara {0}-{1} tecken långt", - "th_TH": "ต้องมีความยาวของตัวอักษร {0}-{1} ตัว", + "th_TH": "ต้องมีความยาว {0}-{1} ตัวอักษร", "tr_TR": "{0}-{1} karakter uzunluğunda olmalı", "uk_UA": "Має бути {0}-{1} символів", "zh_CN": "必须为 {0}-{1} 个字符", @@ -18115,7 +18167,7 @@ "pt_BR": "", "ru_RU": "Сообщение кабинета", "sv_SE": "Cabinet-dialog", - "th_TH": "", + "th_TH": "กล่องโต้ตอบการเก็บถาวร", "tr_TR": "", "uk_UA": "", "zh_CN": "档案对话框", @@ -18140,7 +18192,7 @@ "pt_BR": "", "ru_RU": "Введите новое имя вашего Amiibo", "sv_SE": "Ange nya namnet för din Amiibo", - "th_TH": "", + "th_TH": "ป้อนชื่อใหม่ของ อามิโบ ของคุณ", "tr_TR": "", "uk_UA": "Вкажіть Ваше нове ім'я Amiibo", "zh_CN": "输入你的 Amiibo 的新名字", @@ -18165,7 +18217,7 @@ "pt_BR": "", "ru_RU": "Пожалуйста, отсканируйте свой Amiibo.", "sv_SE": "Skanna din Amiibo nu.", - "th_TH": "", + "th_TH": "กรุณาสแกน อามิโบ ของคุณตอนนี้", "tr_TR": "", "uk_UA": "Будь ласка, проскануйте Ваш Amiibo.", "zh_CN": "请现在扫描你的 Amiibo", @@ -19040,7 +19092,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "การตั้งค่า LED", "tr_TR": "", "uk_UA": "", "zh_CN": "LED 设置", @@ -19140,7 +19192,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "อามิโบ", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -19315,7 +19367,7 @@ "pt_BR": "", "ru_RU": "Проверить и обрезать XCI файл", "sv_SE": "Kontrollera och optimera XCI-filer", - "th_TH": "", + "th_TH": "ตรวจสอบและตัดแต่งไฟล์ XCI", "tr_TR": "", "uk_UA": "Перевірити та Обрізати XCI файл", "zh_CN": "检查并瘦身 XCI 文件", @@ -19340,7 +19392,7 @@ "pt_BR": "", "ru_RU": "Эта функция сначала проверит наличие пустого пространства, а затем обрежет файл XCI, чтобы сэкономить место на диске.", "sv_SE": "Denna funktion kommer först att kontrollera ledigt utrymme och sedan optimera XCI-filen för att spara diskutrymme.", - "th_TH": "", + "th_TH": "ฟังก์ชันนี้จะตรวจสอบพื้นที่ว่างก่อนแล้วจึงตัดไฟล์ XCI เพื่อประหยัดพื้นที่ดิสก์", "tr_TR": "", "uk_UA": "Ця функція спочатку перевірить наявність порожнього місця, після чого обріже файл XCI для економії місця на диску.", "zh_CN": "这个功能将会先检查 XCI 文件,再对其执行瘦身操作以节约磁盘空间。", @@ -19365,7 +19417,7 @@ "pt_BR": "", "ru_RU": "Размер текущего файла: {0:n} Мб\nРазмер игровых данных: {1:n} MB\nЭкономия дискового пространства: {2:n} Мб", "sv_SE": "Aktuell filstorlek: {0:n} MB\nStorlek för speldata: {1:n} MB\nSparat diskutrymme: {2:n} MB", - "th_TH": "", + "th_TH": "ขนาดไฟล์ปัจจุบัน: {0:n} เมกะไบต์\nขนาดข้อมูลเกม: {1:n} เมกะไบต์\nการประหยัดพื้นที่ดิสก์: {2:n} เมกะไบต์", "tr_TR": "", "uk_UA": "Поточний розмір файла: {0:n} MB\nРозмір файлів гри: {1:n} MB\nЕкономія місця: {2:n} MB", "zh_CN": "当前文件大小: {0:n} MB\n游戏数据大小: {1:n} MB\n节约的磁盘空间: {2:n} MB", @@ -19390,7 +19442,7 @@ "pt_BR": "", "ru_RU": "Файл XCI не нуждается в обрезке. Проверьте логи для получения более подробной информации", "sv_SE": "XCI-filen behöver inte optimeras. Kontrollera loggen för mer information", - "th_TH": "", + "th_TH": "ไม่จำเป็นต้องตัดแต่งไฟล์ XCI ตรวจสอบบันทึกเพื่อดูรายละเอียดเพิ่มเติม", "tr_TR": "", "uk_UA": "XCI файл не потребує обрізання. Перевірте журнали (logs) для отримання додаткової інформації", "zh_CN": "XCI 文件不需要被瘦身。查看日志以获得更多细节。", @@ -19415,7 +19467,7 @@ "pt_BR": "", "ru_RU": "XCI файл не может быть обрезан. Проверьте логи для получения более подробной информации", "sv_SE": "XCI-filen kan inte avoptimeras. Kontrollera loggen för mer information", - "th_TH": "", + "th_TH": "ไม่สามารถแยกไฟล์ XCI ได้ ตรวจสอบบันทึกเพื่อดูรายละเอียดเพิ่มเติม", "tr_TR": "", "uk_UA": "XCI файл не може бути обрізаний. Перевірте журнали (logs) для отримання додаткової інформації", "zh_CN": "XCI 文件不能被瘦身。查看日志以获得更多细节。", @@ -19440,7 +19492,7 @@ "pt_BR": "", "ru_RU": "Файл XCI доступен только для чтения и его невозможно сделать доступным для записи. Проверьте логи для получения более подробной информации", "sv_SE": "XCI-filen är skrivskyddad och kunde inte göras skrivbar. Kontrollera loggen för mer information", - "th_TH": "", + "th_TH": "ไฟล์ XCI เป็นแบบอ่านอย่างเดียวและไม่สามารถเขียนได้ โปรดตรวจสอบบันทึกเพื่อดูรายละเอียดเพิ่มเติม", "tr_TR": "", "uk_UA": "XCI файл Тільки для Читання і не може бути прочитаним. Перевірте журнали (logs) для отримання додаткової інформації", "zh_CN": "XCI 文件是只读的,且不可以被标记为可读取的。查看日志以获得更多细节。", @@ -19465,7 +19517,7 @@ "pt_BR": "", "ru_RU": "Файл XCI изменился в размере после сканирования. Проверьте, не производится ли запись в этот файл, и повторите попытку.", "sv_SE": "XCI-filen har ändrats i storlek sedan den lästes av. Kontrollera att filen inte skrivs till och försök igen.", - "th_TH": "", + "th_TH": "ขนาดไฟล์ XCI เปลี่ยนแปลงไปตั้งแต่สแกน โปรดตรวจสอบว่าไฟล์ไม่ได้ถูกเขียนลงไปแล้วลองอีกครั้ง", "tr_TR": "", "uk_UA": "Розмір файлу XCI змінився з моменту сканування. Перевірте, чи не записується файл, та спробуйте знову", "zh_CN": "XCI 文件在扫描后大小发生了变化。请检查文件是否未被写入,然后重试。", @@ -19490,7 +19542,7 @@ "pt_BR": "", "ru_RU": "XCI файл содержит данные в пустой зоне, обрезать его небезопасно", "sv_SE": "XCI-filen har data i det lediga utrymmet. Den är inte säker att optimera", - "th_TH": "", + "th_TH": "ไฟล์ XCI มีข้อมูลในพื้นที่ว่าง ไม่ปลอดภัยที่จะตัดแต่ง", "tr_TR": "", "uk_UA": "Файл XCI містить дані в зоні вільного простору, тому обрізка небезпечна", "zh_CN": "XCI 文件的空闲区域内有数据,不能安全瘦身。", @@ -19515,7 +19567,7 @@ "pt_BR": "", "ru_RU": "Файл XCI содержит недопустимые данные. Проверьте логи для получения дополнительной информации", "sv_SE": "XCI-filen innehåller ogiltig data. Kontrollera loggen för mer information", - "th_TH": "", + "th_TH": "ไฟล์ XCI มีข้อมูลที่ไม่ถูกต้อง ตรวจสอบบันทึกเพื่อดูรายละเอียดเพิ่มเติม", "tr_TR": "", "uk_UA": "XCI Файл містить недійсні дані. Перевірте журнали (logs) для отримання додаткової інформації", "zh_CN": "XCI 文件含有无效数据。查看日志以获得更多细节。", @@ -19540,7 +19592,7 @@ "pt_BR": "", "ru_RU": "XCI файл не удалось открыть для записи. Проверьте логи для получения дополнительной информации", "sv_SE": "XCI-filen kunde inte öppnas för skrivning. Kontrollera loggen för mer information", - "th_TH": "", + "th_TH": "ไม่สามารถเปิดไฟล์ XCI เพื่อเขียนได้ โปรดตรวจสอบบันทึกเพื่อดูรายละเอียดเพิ่มเติม", "tr_TR": "", "uk_UA": "XCI Файл файл не вдалося відкрити для запису. Перевірте журнали для додаткової інформації", "zh_CN": "XCI 文件不能被读写。查看日志以获得更多细节。", @@ -19565,7 +19617,7 @@ "pt_BR": "", "ru_RU": "Обрезка файла XCI не удалась", "sv_SE": "Optimering av XCI-filen misslyckades", - "th_TH": "", + "th_TH": "การตัดแต่งไฟล์ XCI ล้มเหลว", "tr_TR": "", "uk_UA": "Не вдалося обрізати файл XCI", "zh_CN": "XCI 文件瘦身失败", @@ -19590,7 +19642,7 @@ "pt_BR": "", "ru_RU": "Операция была отменена", "sv_SE": "Åtgärden avbröts", - "th_TH": "", + "th_TH": "การดำเนินการถูกยกเลิก", "tr_TR": "", "uk_UA": "Операція перервана", "zh_CN": "操作已取消", @@ -19615,7 +19667,7 @@ "pt_BR": "", "ru_RU": "Операция не была проведена", "sv_SE": "Ingen åtgärd genomfördes", - "th_TH": "", + "th_TH": "ไม่มีการดำเนินการใดๆ", "tr_TR": "", "uk_UA": "Операція не проводилася", "zh_CN": "未执行操作", @@ -19690,7 +19742,7 @@ "pt_BR": "Gerenciador de DLC", "ru_RU": "Управление DLC для {0} ({1})", "sv_SE": "Hantera hämtningsbart innehåll för {0} ({1})", - "th_TH": "จัดการ DLC ที่ดาวน์โหลดได้สำหรับ {0} ({1})", + "th_TH": "จัดการเนื้อหาที่ดาวน์โหลดได้สำหรับ {0} ({1})", "tr_TR": "Oyun DLC'lerini Yönet", "uk_UA": "Менеджер вмісту для завантаження", "zh_CN": "管理 {0} ({1}) 的 DLC", @@ -19715,7 +19767,7 @@ "pt_BR": "Gerenciar Mods para {0} ({1})", "ru_RU": "Управление модами для {0} ({1})", "sv_SE": "Hantera moddar för {0} ({1})", - "th_TH": "จัดการม็อดที่ดาวน์โหลดได้สำหรับ {0} ({1})", + "th_TH": "จัดการม็อดสำหรับ {0} ({1})", "tr_TR": "", "uk_UA": "Керувати модами для {0} ({1})", "zh_CN": "管理 {0} ({1}) 的 MOD", @@ -19740,7 +19792,7 @@ "pt_BR": "Gerenciador de atualizações", "ru_RU": "Менеджер обновлений игр", "sv_SE": "Hanterare för speluppdateringar", - "th_TH": "จัดการอัปเดตหัวข้อ", + "th_TH": "จัดการอัปเดตชื่อ", "tr_TR": "Oyun Güncellemelerini Yönet", "uk_UA": "Менеджер оновлення назв", "zh_CN": "游戏更新管理器", @@ -19765,7 +19817,7 @@ "pt_BR": "", "ru_RU": "Уменьшение размера XCI файлов", "sv_SE": "Optimera XCI-filer", - "th_TH": "", + "th_TH": "ตัดแต่ง ไฟล์ XCI", "tr_TR": "", "uk_UA": "Обрізка XCI Файлів", "zh_CN": "XCI 文件瘦身器", @@ -19790,7 +19842,7 @@ "pt_BR": "", "ru_RU": "{0} из {1} файла(ов) выбрано", "sv_SE": "{0} av {1} spel markerade", - "th_TH": "", + "th_TH": "{0} จาก {1} ชื่อที่เลือก", "tr_TR": "", "uk_UA": "{0} з {1} тайтл(ів) обрано", "zh_CN": "在 {1} 中选中了 {0} 个游戏 ", @@ -19815,7 +19867,7 @@ "pt_BR": "", "ru_RU": "{0} из {1} файла(ов) выбрано ({2} показано)", "sv_SE": "{0} av {1} spel markerade ({2} visade)", - "th_TH": "", + "th_TH": "{0} จาก {1} ชื่อที่เลือก (แสดง {2} รายการ)", "tr_TR": "", "uk_UA": "{0} з {1} тайтл(ів) обрано ({2} відображається)", "zh_CN": "在 {1} 中选中了 {0} 个游戏 (显示了 {2} 个)", @@ -19840,7 +19892,7 @@ "pt_BR": "", "ru_RU": "Обрезка {0} файла(ов)...", "sv_SE": "Optimerar {0} spel...", - "th_TH": "", + "th_TH": "กำลังตัดแต่ง {0} ชื่อ...", "tr_TR": "", "uk_UA": "Обрізка {0} тайтл(ів)...", "zh_CN": "{0} 个游戏瘦身中。。。", @@ -19865,7 +19917,7 @@ "pt_BR": "", "ru_RU": "Отмена обрезки {0} файла(ов)...", "sv_SE": "Avoptimerar {0} spel...", - "th_TH": "", + "th_TH": "กำลังยกเลิกการตัดแต่ง {0} ชื่อ...", "tr_TR": "", "uk_UA": "Необрізаних {0} тайтл(ів)...", "zh_CN": "正在精简 {0} 个游戏", @@ -19890,7 +19942,7 @@ "pt_BR": "", "ru_RU": "Ошибка", "sv_SE": "Misslyckades", - "th_TH": "", + "th_TH": "ล้มเหลว", "tr_TR": "", "uk_UA": "Невдача", "zh_CN": "失败", @@ -19915,7 +19967,7 @@ "pt_BR": "", "ru_RU": "Потенциально освобождено места", "sv_SE": "Möjlig besparning", - "th_TH": "", + "th_TH": "ศักยภาพในการประหยัดพื้นที่จัดเก็บ", "tr_TR": "", "uk_UA": "Потенційна економія", "zh_CN": "潜在的储存空间节省", @@ -19940,7 +19992,7 @@ "pt_BR": "", "ru_RU": "Реально освобождено места", "sv_SE": "Faktisk besparning", - "th_TH": "", + "th_TH": "ประหยัดพื้นที่จัดเก็บจริง", "tr_TR": "", "uk_UA": "Зекономлено", "zh_CN": "实际的储存空间节省", @@ -19965,7 +20017,7 @@ "pt_BR": "", "ru_RU": "{0:n0} Мб", "sv_SE": "", - "th_TH": "", + "th_TH": "{0:n0} เมกะไบต์", "tr_TR": "", "uk_UA": "{0:n0} Мб", "zh_CN": "", @@ -19990,7 +20042,7 @@ "pt_BR": "", "ru_RU": "Выбрать то что показано", "sv_SE": "Markera visade", - "th_TH": "", + "th_TH": "เลือกแสดง", "tr_TR": "", "uk_UA": "Вибрати показане", "zh_CN": "选定显示的", @@ -20015,7 +20067,7 @@ "pt_BR": "", "ru_RU": "Отменить выбор показанного", "sv_SE": "Avmarkera visade", - "th_TH": "", + "th_TH": "เลิกเลือกสิ่งที่แสดงอยู่", "tr_TR": "", "uk_UA": "Скасувати вибір показаного", "zh_CN": "反选显示的", @@ -20040,7 +20092,7 @@ "pt_BR": "", "ru_RU": "Приложение", "sv_SE": "Titel", - "th_TH": "", + "th_TH": "ชื่อ", "tr_TR": "", "uk_UA": "Назва", "zh_CN": "标题", @@ -20065,7 +20117,7 @@ "pt_BR": "", "ru_RU": "Сохранение места на диске", "sv_SE": "Utrymmesbesparning", - "th_TH": "", + "th_TH": "ประหยัดพื้นที่", "tr_TR": "", "uk_UA": "Економія місця", "zh_CN": "节省空间", @@ -20090,7 +20142,7 @@ "pt_BR": "", "ru_RU": "Обрезать", "sv_SE": "Optimera", - "th_TH": "", + "th_TH": "ตัดแต่ง", "tr_TR": "", "uk_UA": "Обрізка", "zh_CN": "瘦身", @@ -20115,7 +20167,7 @@ "pt_BR": "", "ru_RU": "Отмена обрезки", "sv_SE": "Avoptimera", - "th_TH": "", + "th_TH": "คืนค่าให้ครบถ้วน", "tr_TR": "", "uk_UA": "Зшивання", "zh_CN": "取消精简", @@ -20140,7 +20192,7 @@ "pt_BR": "{0} nova(s) atualização(ões) adicionada(s)", "ru_RU": "Добавлено {0} новых обновлений", "sv_SE": "{0} nya uppdatering(ar) lades till", - "th_TH": "{0} อัพเดตที่เพิ่มมาใหม่", + "th_TH": "เพิ่มการอัปเดตใหม่ {0} รายการ", "tr_TR": "", "uk_UA": "{0} нових оновлень додано", "zh_CN": "{0} 个更新被添加", @@ -20165,7 +20217,7 @@ "pt_BR": "Atualizações incorporadas não podem ser removidas, apenas desativadas.", "ru_RU": "Обновления бандлов не могут быть удалены, только отключены.", "sv_SE": "Bundlade uppdateringar kan inte tas bort, endast inaktiveras.", - "th_TH": "แพ็คที่อัพเดตมาไม่สามารถลบทิ้งได้ สามารถปิดใช้งานได้เท่านั้น", + "th_TH": "การอัปเดตแบบรวมไม่สามารถลบได้ ทำได้เพียงปิดการใช้งานเท่านั้น", "tr_TR": "", "uk_UA": "Вбудовані оновлення не можуть бути видалені, лише вимкнені.", "zh_CN": "游戏整合的更新无法移除,可尝试禁用。", @@ -20215,7 +20267,7 @@ "pt_BR": "ID da Build:", "ru_RU": "ID версии:", "sv_SE": "Bygg-id:", - "th_TH": "รหัสการสร้าง:", + "th_TH": "รหัสบิวด์:", "tr_TR": "", "uk_UA": "ID збірки:", "zh_CN": "游戏版本 ID:", @@ -20240,7 +20292,7 @@ "pt_BR": "DLCs incorporadas não podem ser removidas, apenas desativadas.", "ru_RU": "DLC бандлов не могут быть удалены, только отключены.", "sv_SE": "Bundlade DLC kan inte tas bort, endast inaktiveras.", - "th_TH": "แพ็ค DLC ไม่สามารถลบทิ้งได้ สามารถปิดใช้งานได้เท่านั้น", + "th_TH": "DLC ที่รวมมาไม่สามารถลบออกได้ ทำได้เพียงปิดการใช้งานเท่านั้น", "tr_TR": "", "uk_UA": "Комплектні DLC (бандли) не можуть бути видаленими, лише вимкненими.", "zh_CN": "游戏整合的 DLC 无法移除,可尝试禁用。", @@ -20265,7 +20317,7 @@ "pt_BR": "", "ru_RU": "{0} доступных DLC", "sv_SE": "{0} DLC(er) tillgängliga", - "th_TH": "", + "th_TH": "มี DLC {0} รายการให้เลือก", "tr_TR": "", "uk_UA": "{0} DLC доступно", "zh_CN": "{0} 个 DLC", @@ -20290,7 +20342,7 @@ "pt_BR": "{0} novo(s) conteúdo(s) para download adicionado(s)", "ru_RU": "Добавлено {0} новых DLC", "sv_SE": "{0} nya hämtningsbara innehåll lades till", - "th_TH": "{0} DLC ใหม่ที่เพิ่มเข้ามา", + "th_TH": "เพิ่มเนื้อหาที่ดาวน์โหลดได้ใหม่ {0} รายการ", "tr_TR": "", "uk_UA": "{0} нового завантажувального вмісту додано", "zh_CN": "{0} 个 DLC 被添加", @@ -20315,7 +20367,7 @@ "pt_BR": "{0} novo(s) conteúdo(s) para download adicionado(s)", "ru_RU": "Добавлено {0} новых DLC", "sv_SE": "{0} nya hämtningsbara innehåll lades till", - "th_TH": "{0} ใหม่ที่เพิ่มเข้ามา", + "th_TH": "เพิ่มเนื้อหาที่ดาวน์โหลดได้ใหม่ {0} รายการ", "tr_TR": "", "uk_UA": "{0} нового завантажувального вмісту додано", "zh_CN": "{0} 个 DLC 被添加", @@ -20340,7 +20392,7 @@ "pt_BR": "{0} conteúdo(s) para download ausente(s) removido(s)", "ru_RU": "{0} отсутствующих DLC удалено", "sv_SE": "{0} saknade hämtningsbara innehåll togs bort", - "th_TH": "", + "th_TH": "เนื้อหาที่ดาวน์โหลดได้ {0} รายการถูกลบออก", "tr_TR": "", "uk_UA": "{0} відсутнього завантажувального вмісту видалено", "zh_CN": "{0} 个失效的 DLC 已移除", @@ -20365,7 +20417,7 @@ "pt_BR": "{0} nova(s) atualização(ões) adicionada(s)", "ru_RU": "{0} новых обновлений добавлено", "sv_SE": "{0} nya uppdatering(ar) lades till", - "th_TH": "{0} อัพเดตใหม่ที่เพิ่มเข้ามา", + "th_TH": "เพิ่มการอัปเดตใหม่ {0} รายการ", "tr_TR": "", "uk_UA": "{0} нових оновлень додано", "zh_CN": "{0} 个游戏更新被添加", @@ -20390,7 +20442,7 @@ "pt_BR": "{0} atualização(ões) ausente(s) removida(s)", "ru_RU": "{0} отсутствующих обновлений удалено", "sv_SE": "{0} saknade uppdatering(ar) togs bort", - "th_TH": "", + "th_TH": "มีการลบการอัปเดตที่ขาดหายไป {0} รายการ", "tr_TR": "", "uk_UA": "{0} відсутніх оновлень видалено", "zh_CN": "{0} 个失效的游戏更新已移除", @@ -20465,7 +20517,7 @@ "pt_BR": "", "ru_RU": "Продолжить", "sv_SE": "Fortsätt", - "th_TH": "", + "th_TH": "ดำเนินการต่อ", "tr_TR": "", "uk_UA": "Продовжити", "zh_CN": "继续", @@ -20840,7 +20892,7 @@ "pt_BR": "", "ru_RU": "Авто", "sv_SE": "Automatiskt", - "th_TH": "", + "th_TH": "อัตโนมัติ", "tr_TR": "", "uk_UA": "", "zh_CN": "自动", @@ -20865,7 +20917,7 @@ "pt_BR": "", "ru_RU": "Использует Vulkan.\nНа Mac с ARM процессорами используется Metal, если игра с ним совместима и хорошо работает.", "sv_SE": "Använder Vulkan.\nPå en ARM Mac och vid spel som körs bra på den så används Metal-bakänden.", - "th_TH": "", + "th_TH": "ใช้ Vulkan\nบน ARM Mac และเมื่อเล่นเกมที่ทำงานได้ดีบนระบบปฏิบัติการนั้น จะใช้แบ็กเอนด์ Metal", "tr_TR": "", "uk_UA": "", "zh_CN": "使用 Vulkan。\n在 ARM Mac 上,当玩在其下运行良好的游戏时,使用 Metal 后端。", @@ -21665,7 +21717,7 @@ "pt_BR": "", "ru_RU": "Билинейная", "sv_SE": "Bilinjär", - "th_TH": "", + "th_TH": "ไบลิเนียร์", "tr_TR": "", "uk_UA": "Білінійний", "zh_CN": "Bilinear(双线性过滤)", @@ -21740,7 +21792,7 @@ "pt_BR": "", "ru_RU": "Зональная", "sv_SE": "Yta", - "th_TH": "", + "th_TH": "พื้นที่", "tr_TR": "", "uk_UA": "", "zh_CN": "Area(区域过滤)", @@ -22265,7 +22317,7 @@ "pt_BR": "", "ru_RU": "Отключить хостинг P2P-сетей (может увеличить задержку)", "sv_SE": "Inaktivera P2P-nätverkshosting (kan öka latens)", - "th_TH": "", + "th_TH": "ปิดใช้งานการโฮสต์เครือข่าย P2P (อาจเพิ่มความล่าช้า)", "tr_TR": "", "uk_UA": "Вимкнути хостинг P2P мережі (може збільшити затримку)", "zh_CN": "禁用 P2P 网络连接 (也许会增加延迟)", @@ -22290,7 +22342,7 @@ "pt_BR": "", "ru_RU": "Отключая хостинг P2P-сетей, пользователи будут проксироваться через главный сервер, а не подключаться к вам напрямую.", "sv_SE": "Inaktivera P2P-nätverkshosting, motparter kommer skickas genom masterservern isället för att ansluta direkt till dig.", - "th_TH": "", + "th_TH": "ปิดใช้งานการโฮสติ้งเครือข่าย P2P พีร์จะใช้พร็อกซีผ่านเซิร์ฟเวอร์หลักแทนที่จะเชื่อมต่อกับคุณโดยตรง", "tr_TR": "", "uk_UA": "Вимкнути хостинг P2P мережі, піри будуть підключатися через майстер-сервер замість прямого з'єднання з вами.", "zh_CN": "禁用 P2P 网络连接,对方将通过主服务器进行连接,而不是直接连接到您。", @@ -22315,7 +22367,7 @@ "pt_BR": "", "ru_RU": "Cетевой пароль:", "sv_SE": "Lösenfras för nätverk:", - "th_TH": "", + "th_TH": "รหัสผ่านเครือข่าย:", "tr_TR": "", "uk_UA": "Мережевий пароль:", "zh_CN": "网络密码:", @@ -22340,7 +22392,7 @@ "pt_BR": "", "ru_RU": "Вы сможете видеть только те игры, в которых используется тот же пароль, что и у вас.", "sv_SE": "Du kommer endast kunna se hostade spel med samma lösenfras som du.", - "th_TH": "", + "th_TH": "คุณจะสามารถเห็นเฉพาะเกมโฮสต์ที่ใช้รหัสผ่านเดียวกับคุณเท่านั้น", "tr_TR": "", "uk_UA": "Ви зможете бачити лише ті ігри, які мають такий самий пароль, як і у вас.", "zh_CN": "您只能看到与您使用相同密码的游戏房间。", @@ -22365,7 +22417,7 @@ "pt_BR": "", "ru_RU": "Введите пароль в формате Ryujinx-<8 шестнадцатеричных символов>. Вы сможете видеть только те игры, в которых используется тот же пароль, что и у вас.", "sv_SE": "Ange en lösenfras i formatet Ryujinx-<8 hextecken>. Du kommer endast kunna se hostade spel med samma lösenfras som du.", - "th_TH": "", + "th_TH": "ป้อนรหัสผ่านในรูปแบบ Ryujinx-<8 hex chars> คุณจะสามารถเห็นเกมที่โฮสต์โดยใช้รหัสผ่านเดียวกับคุณได้เท่านั้น", "tr_TR": "", "uk_UA": "Введіть пароль у форматі Ryujinx-<8 символів>. Ви зможете бачити лише ті ігри, які мають такий самий пароль, як і у вас.", "zh_CN": "以 Ryujinx-<8个十六进制字符> 的格式输入密码。您只能看到与您使用相同密码的游戏房间。", @@ -22390,7 +22442,7 @@ "pt_BR": "", "ru_RU": "(публичный)", "sv_SE": "(publik)", - "th_TH": "", + "th_TH": "(สาธารณะ)", "tr_TR": "", "uk_UA": "(публічний)", "zh_CN": "(公开的)", @@ -22415,7 +22467,7 @@ "pt_BR": "", "ru_RU": "Сгенерировать рандомно", "sv_SE": "Generera slumpmässigt", - "th_TH": "", + "th_TH": "สร้างแบบสุ่ม", "tr_TR": "", "uk_UA": "Згенерувати випадкову", "zh_CN": "随机生成", @@ -22440,7 +22492,7 @@ "pt_BR": "", "ru_RU": "Генерирует новый пароль, который можно передать другим игрокам.", "sv_SE": "Genererar en ny lösenfras som kan delas med andra spelare.", - "th_TH": "", + "th_TH": "สร้างรหัสผ่านใหม่ซึ่งสามารถแบ่งปันกับผู้เล่นคนอื่นได้", "tr_TR": "", "uk_UA": "Генерує новий пароль, яким можна поділитися з іншими гравцями.", "zh_CN": "生成一个新的密码,可以与其他玩家共享。", @@ -22465,7 +22517,7 @@ "pt_BR": "", "ru_RU": "Очистить", "sv_SE": "Töm", - "th_TH": "", + "th_TH": "ล้าง", "tr_TR": "", "uk_UA": "Очистити", "zh_CN": "清除", @@ -22490,7 +22542,7 @@ "pt_BR": "", "ru_RU": "Очищает текущий пароль, возвращаясь в публичную сеть.", "sv_SE": "Tömmer aktuell lösenfras och återgår till det publika nätverket.", - "th_TH": "", + "th_TH": "ล้างรหัสผ่านปัจจุบันเพื่อกลับสู่เครือข่ายสาธารณะ", "tr_TR": "", "uk_UA": "Очищає поточну пароль, повертаючись до публічної мережі.", "zh_CN": "清除当前密码,返回公共网络。", @@ -22515,7 +22567,7 @@ "pt_BR": "", "ru_RU": "Неверный пароль! Пароль должен быть в формате \"Ryujinx-<8 шестнадцатеричных символов>\"", "sv_SE": "Ogiltig lösenfras! Måste vara i formatet \"Ryujinx-<8 hextecken>\"", - "th_TH": "", + "th_TH": "รหัสผ่านไม่ถูกต้อง! ต้องอยู่ในรูปแบบ \"Ryujinx-<8 hex chars>\"", "tr_TR": "", "uk_UA": "Невірний пароль! Має бути в форматі \"Ryujinx-<8 символів>\"", "zh_CN": "无效密码!密码的格式必须是\"Ryujinx-<8个十六进制字符>\"", @@ -22540,7 +22592,7 @@ "pt_BR": "", "ru_RU": "Вертикальная синхронизация:", "sv_SE": "", - "th_TH": "", + "th_TH": "วีซิงค์:", "tr_TR": "", "uk_UA": "Вертикальна синхронізація (VSync):", "zh_CN": "垂直同步(VSync)", @@ -22565,7 +22617,7 @@ "pt_BR": "", "ru_RU": "Включить пользовательскую частоту кадров (Экспериментально)", "sv_SE": "Aktivera anpassad uppdateringsfrekvens (experimentell)", - "th_TH": "", + "th_TH": "เปิดใช้งานอัตราการรีเฟรชแบบกำหนดเอง (ทดลอง)", "tr_TR": "", "uk_UA": "Увімкнути користувацьку частоту оновлення (Експериментально)", "zh_CN": "启动自定义刷新率(实验性功能)", @@ -22590,7 +22642,7 @@ "pt_BR": "", "ru_RU": "Консоль", "sv_SE": "", - "th_TH": "", + "th_TH": "สลับ", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -22615,7 +22667,7 @@ "pt_BR": "", "ru_RU": "Без ограничений", "sv_SE": "Obunden", - "th_TH": "", + "th_TH": "ไม่มีข้อจำกัด", "tr_TR": "", "uk_UA": "Безмежна", "zh_CN": "无限制", @@ -22640,7 +22692,7 @@ "pt_BR": "", "ru_RU": "Пользовательская частота кадров", "sv_SE": "Anpassad uppdateringsfrekvens", - "th_TH": "", + "th_TH": "อัตราการรีเฟรชที่กำหนดเอง", "tr_TR": "", "uk_UA": "Користувацька", "zh_CN": "自定义刷新率", @@ -22665,7 +22717,7 @@ "pt_BR": "", "ru_RU": "Эмулированная вертикальная синхронизация. 'Консоль' эмулирует частоту обновления консоли, равную 60 Гц. 'Без ограничений' - неограниченная частота кадров.", "sv_SE": "Emulerad vertikal synk. 'Switch' emulerar Switchens uppdateringsfrekvens på 60Hz. 'Obunden' är en obegränsad uppdateringsfrekvens.", - "th_TH": "", + "th_TH": "จำลองการซิงค์แนวตั้ง 'สวิตช์' จำลองอัตราการรีเฟรชของสวิตช์ที่ 60Hz 'ไม่จำกัด' คืออัตราการรีเฟรชที่ไม่จำกัด", "tr_TR": "", "uk_UA": "Емульована вертикальна синхронізація кадрів. 'Switch' емулює частоту оновлення консолі Nintendo Switch (60 Гц). 'Необмежена' — частота оновлення не матиме обмежень.", "zh_CN": "模拟垂直同步。“Switch”模拟了 Switch 的 60Hz 刷新率。“无限制”没有刷新率限制。", @@ -22690,7 +22742,7 @@ "pt_BR": "", "ru_RU": "Эмулированная вертикальная синхронизация. 'Консоль' эмулирует частоту обновления консоли, равную 60 Гц. 'Без ограничений' - неограниченная частота кадров. 'Пользовательска частота кадров' эмулирует выбранную пользователем частоту кадров.", "sv_SE": "Emulerad vertikal synk. 'Switch' emulerar Switchens uppdateringsfrekvens på 60Hz. 'Obunden' är en obegränsad uppdateringsfrekvens. 'Anpassad uppdateringsfrekvens' emulerar den angivna anpassade uppdateringsfrekvensen.", - "th_TH": "", + "th_TH": "การซิงค์แนวตั้งจำลอง 'สวิตช์' จำลองอัตราการรีเฟรชของสวิตช์ที่ 60Hz 'ไม่จำกัด' คืออัตราการรีเฟรชไม่จำกัด 'อัตราการรีเฟรชที่กำหนดเอง' จำลองอัตราการรีเฟรชที่กำหนดเองที่ระบุ", "tr_TR": "", "uk_UA": "Емульована вертикальна синхронізація кадрів. 'Switch' емулює частоту оновлення консолі Nintendo Switch (60 Гц). 'Необмежена' — частота оновлення не матиме обмежень. 'Користувацька' емулює вказану користувацьку частоту оновлення.", "zh_CN": "模拟垂直同步。“Switch”模拟了 Switch 的 60Hz 刷新率。“无限制”没有刷新率限制。“自定义刷新率”模拟指定的自定义刷新率。", @@ -22715,7 +22767,7 @@ "pt_BR": "", "ru_RU": "Позволяет пользователю указать эмулируемую частоту кадров. В некоторых играх это может ускорить или замедлить скорость логики игрового процесса. В других играх это может позволить ограничить FPS на уровне, кратном частоте обновления, или привести к непредсказуемому поведению. Это экспериментальная функция, и нет никаких гарантий того, как она повлияет на игровой процесс. \n\nОставьте выключенным, если не уверены.", "sv_SE": "Låter användaren ange en emulerad uppdateringsfrekvens. För vissa spel så kan detta snabba upp eller ner frekvensen för spellogiken. I andra spel så kan detta tillåta att bildfrekvensen kapas för delar av uppdateringsfrekvensen eller leda till oväntat beteende. Detta är en experimentell funktion utan några garantier för hur spelet påverkas. \n\nLämna AV om du är osäker.", - "th_TH": "", + "th_TH": "อนุญาตให้ผู้ใช้ระบุอัตราการรีเฟรชจำลอง ในบางเกม อาจทำให้ความเร็วหรือความช้าของตรรกะการเล่นเกมเพิ่มขึ้น ในบางเกม อาจอนุญาตให้จำกัด FPS ไว้ที่หลายเท่าของอัตราการรีเฟรช หรืออาจนำไปสู่พฤติกรรมที่ไม่สามารถคาดเดาได้ คุณลักษณะนี้เป็นคุณลักษณะทดลอง โดยไม่มีการรับประกันว่าการเล่นเกมจะได้รับผลกระทบอย่างไร \n\nปิดไว้หากไม่แน่ใจ", "tr_TR": "", "uk_UA": "Дозволяє користувачу вказати емульовану частоту оновлення. У деяких іграх це може прискорити або сповільнити логіку гри. Натомість в інших іграх ця функція може дозволити обмежити FPS на певні кратні частоти оновлення або призвести до непередбачуваної поведінки. Це експериментальна функція, без гарантій того, як вона вплине на ігровий процес. \n\nЗалиште ВИМКНЕНИМ, якщо не впевнені.", "zh_CN": "允许用户指定模拟刷新率。在某些游戏中,这可能会加快或减慢游戏逻辑的速度。在其他游戏中,它可能允许将 FPS 限制在刷新率的某个倍数,或者导致不可预测的行为。这是一个实验性功能,无法保证游戏会受到怎样的影响。\n\n如果不确定,请关闭。", @@ -22740,7 +22792,7 @@ "pt_BR": "", "ru_RU": "Заданное значение частоты кадров", "sv_SE": "Målvärde för anpassad uppdateringsfrekvens.", - "th_TH": "", + "th_TH": "ค่าเป้าหมายอัตราการรีเฟรชแบบกำหนดเอง", "tr_TR": "", "uk_UA": "Цільове значення користувацької частоти оновлення.", "zh_CN": "目标自定义刷新率值。", @@ -22765,7 +22817,7 @@ "pt_BR": "", "ru_RU": "Пользовательская частота кадров в процентах от обычной частоты обновления на консоли.", "sv_SE": "Anpassad uppdateringsfrekvens, som en procentdel av den normala uppdateringsfrekvensen för Switch.", - "th_TH": "", + "th_TH": "อัตราการรีเฟรชแบบกำหนดเองเป็นเปอร์เซ็นต์ของอัตราการรีเฟรชสวิตช์ปกติ", "tr_TR": "", "uk_UA": "Користувацька частота оновлення, як відсоток від стандартної частоти оновлення Switch.", "zh_CN": "自定义刷新率,占正常SWitch刷新率的百分比值。", @@ -22790,7 +22842,7 @@ "pt_BR": "", "ru_RU": "Пользовательская частота кадров %:", "sv_SE": "Anpassad uppdateringsfrekvens %:", - "th_TH": "", + "th_TH": "อัตราการรีเฟรชที่กำหนดเอง %:", "tr_TR": "", "uk_UA": "Користувацька частота оновлення %:", "zh_CN": "自定义刷新率值 %:", @@ -22815,7 +22867,7 @@ "pt_BR": "", "ru_RU": "Значение пользовательской частоты кадров:", "sv_SE": "Värde för anpassad uppdateringsfrekvens:", - "th_TH": "", + "th_TH": "ค่าอัตราการรีเฟรชที่กำหนดเอง:", "tr_TR": "", "uk_UA": "Значення користувацької частоти оновлення:", "zh_CN": "自定义刷新率值:", @@ -22840,7 +22892,7 @@ "pt_BR": "", "ru_RU": "Интервал", "sv_SE": "Intervall", - "th_TH": "", + "th_TH": "ช่วงเวลา", "tr_TR": "", "uk_UA": "", "zh_CN": "间隔", @@ -22865,7 +22917,7 @@ "pt_BR": "", "ru_RU": "Выбрать режим вертикальной синхронизации:", "sv_SE": "Växla VSync-läge:", - "th_TH": "", + "th_TH": "สลับโหมด VSync:", "tr_TR": "", "uk_UA": "Перемкнути VSync режим:", "zh_CN": "设置 VSync 模式:", @@ -22890,7 +22942,7 @@ "pt_BR": "", "ru_RU": "Повышение пользовательской частоты кадров", "sv_SE": "Höj anpassad uppdateringsfrekvens", - "th_TH": "", + "th_TH": "เพิ่มอัตราการรีเฟรชที่กำหนดเอง", "tr_TR": "", "uk_UA": "Підвищити користувацьку частоту оновлення", "zh_CN": "提高自定义刷新率:", @@ -22915,7 +22967,7 @@ "pt_BR": "", "ru_RU": "Понижение пользовательской частоты кадров", "sv_SE": "Sänk anpassad uppdateringsfrekvens", - "th_TH": "", + "th_TH": "อัตราการรีเฟรชแบบกำหนดเองที่ต่ำกว่า:", "tr_TR": "", "uk_UA": "Понизити користувацьку частоту оновлення", "zh_CN": "降低自定义刷新率:", @@ -22940,7 +22992,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Senast uppdaterad: {0}", - "th_TH": "", + "th_TH": "อัปเดตล่าสุด: {0}", "tr_TR": "", "uk_UA": "", "zh_CN": "最后更新于: {0}", @@ -22965,7 +23017,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Denna kompatibilitetslista kan innehålla utdaterade poster.\nTesta gärna spelen som listas med \"Spelproblem\"-status.", - "th_TH": "", + "th_TH": "รายการความเข้ากันได้นี้ อาจมีรายการที่ล้าสมัยอยู่\nอย่าเพิ่งทดสอบเกมในสถานะ\"อยู่ในเกม\"", "tr_TR": "", "uk_UA": "", "zh_CN": "此兼容性列表可能包含过时的条目。\n不要只测试 \"进入游戏\" 状态的游戏。", @@ -22990,7 +23042,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Sök i kompatibilitetsposter...", - "th_TH": "", + "th_TH": "ค้นหารายการความเข้ากันได้...", "tr_TR": "", "uk_UA": "", "zh_CN": "正在搜索兼容性条目...", @@ -23015,7 +23067,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Öppna kompatibilitetslistan", - "th_TH": "", + "th_TH": "เปิดรายการความเข้ากันได้", "tr_TR": "", "uk_UA": "", "zh_CN": "打开兼容性列表", @@ -23040,7 +23092,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Visa endast ägda spel", - "th_TH": "", + "th_TH": "แสดงเฉพาะเกมที่เป็นเจ้าของเท่านั้น", "tr_TR": "", "uk_UA": "", "zh_CN": "仅显示拥有的游戏", @@ -23065,7 +23117,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Spelbart", - "th_TH": "", + "th_TH": "สามารถเล่นได้", "tr_TR": "", "uk_UA": "", "zh_CN": "可游玩", @@ -23090,7 +23142,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Spelproblem", - "th_TH": "", + "th_TH": "ในเกม", "tr_TR": "", "uk_UA": "", "zh_CN": "进入游戏", @@ -23115,7 +23167,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Menyer", - "th_TH": "", + "th_TH": "เมนู", "tr_TR": "", "uk_UA": "", "zh_CN": "菜单", @@ -23140,7 +23192,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Startar", - "th_TH": "", + "th_TH": "บู๊ทส์", "tr_TR": "", "uk_UA": "", "zh_CN": "启动", @@ -23165,7 +23217,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Ingenting", - "th_TH": "", + "th_TH": "ไม่มี", "tr_TR": "", "uk_UA": "", "zh_CN": "什么都没有", @@ -23190,7 +23242,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "เลือก DLC ที่จะแยก", "tr_TR": "", "uk_UA": "", "zh_CN": "选择一个要解压的 DLC", @@ -23198,4 +23250,4 @@ } } ] -} \ No newline at end of file +} -- 2.47.1 From 4e87e2708bb07366eb3d9df4d90c6110642c0f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=9C=A8=E4=B8=AD=E5=9B=BD=E7=9A=84=E6=B3=B0=E5=9B=BD?= =?UTF-8?q?=E9=9D=92=E5=B9=B4=5F?= <132539847+dekthaiinchina@users.noreply.github.com> Date: Sat, 1 Feb 2025 20:11:48 +0700 Subject: [PATCH 002/209] Update locales.json --- src/Ryujinx/Assets/locales.json | 39 --------------------------------- 1 file changed, 39 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 4859e62b4..ca38e369e 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -90,11 +90,8 @@ "pt_BR": "", "ru_RU": "Апплет Mii Editor", "sv_SE": "Redigera Mii-applet", -<<<<<<< HEAD "th_TH": "แอปเพล็ต แก้ไข Mii", -======= "th_TH": "เครื่องมือแก้ไข Mii", ->>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Аплет редагування Mii", "zh_CN": "Mii 小程序", @@ -594,11 +591,8 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", -<<<<<<< HEAD "th_TH": "เริ่มเกมด้วย UI ที่ซ่อนอยู่", -======= "th_TH": "เริ่มเกมโดยซ่อน UI", ->>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "", "zh_CN": "启动游戏时隐藏 UI", @@ -773,11 +767,8 @@ "pt_BR": "", "ru_RU": "Сканировать Amiibo (из папки Bin)", "sv_SE": "Skanna en Amiibo (från bin-fil)", -<<<<<<< HEAD "th_TH": "สแกน อามิโบ (จาก Bin)", -======= "th_TH": "สแกน Amiibo (จากไฟล์ Bin)", ->>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Сканувати Amiibo (з теки Bin)", "zh_CN": "扫描 Amiibo (从 bin 文件)", @@ -902,11 +893,8 @@ "pt_BR": "", "ru_RU": "Установить ключи из файла KEYS или ZIP", "sv_SE": "Installera nycklar från KEYS eller ZIP", -<<<<<<< HEAD "th_TH": "ติดตั้งคีย์จาก KEYS หรือ ZIP", -======= "th_TH": "ติดตั้งคีย์จากไฟล์ KEYS หรือ ZIP", ->>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Встановити ключі з файлу .KEYS або .ZIP", "zh_CN": "从 .KEYS 文件或 .ZIP 压缩包安装密匙", @@ -931,11 +919,8 @@ "pt_BR": "", "ru_RU": "Установить ключи из папки", "sv_SE": "Installera nycklar från en katalog", -<<<<<<< HEAD "th_TH": "ติดตั้งคีย์จากไดเร็กทอรี", -======= "th_TH": "ติดตั้งคีย์จากไดเรกทอรี", ->>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Встановити ключі з теки", "zh_CN": "从一个文件夹安装密匙", @@ -1035,11 +1020,8 @@ "pt_BR": "", "ru_RU": "Уменьшить размер XCI файлов", "sv_SE": "Optimera XCI-filer", -<<<<<<< HEAD "th_TH": "ตัดแต่งไฟล์ XCI", -======= "th_TH": "ตัดไฟล์ XCI", ->>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Обрізати XCI файли", "zh_CN": "瘦身 XCI 文件", @@ -1264,11 +1246,8 @@ "pt_BR": "", "ru_RU": "FAQ и Руководства", "sv_SE": "Frågor, svar och guider", -<<<<<<< HEAD "th_TH": "คำถามที่พบบ่อยและคำแนะนำ", -======= "th_TH": "คำถามที่พบบ่อย & คู่มือ", ->>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "FAQ та посібники", "zh_CN": "问答与指南", @@ -1293,11 +1272,8 @@ "pt_BR": "", "ru_RU": "FAQ & Устранение неполадок", "sv_SE": "Frågor, svar och felsökningssida", -<<<<<<< HEAD "th_TH": "หน้าคำถามที่พบบ่อยและการแก้ไขปัญหา", -======= "th_TH": "คำถามที่พบบ่อย & หน้าการแก้ไขปัญหา", ->>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "FAQ та усунення несправностей (eng)", "zh_CN": "常见问题和问题排除页面", @@ -1322,11 +1298,8 @@ "pt_BR": "", "ru_RU": "Открывает страницы с FAQ и Устранением неполадок на официальной странице вики Ryujinx", "sv_SE": "Öppnar Frågor, svar och felsökningssidan på den officiella Ryujinx-wikin", -<<<<<<< HEAD "th_TH": "เปิดหน้าคำถามที่พบบ่อยและการแก้ไขปัญหาบนวิกิ Ryujinx อย่างเป็นทางการ", -======= "th_TH": "เปิดหน้าคำถามที่พบบ่อยและการแก้ไขปัญหาบนวิกิทางการของ Ryujinx", ->>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Відкриває сторінку з Посібником по усуненню помилок та несправностей на офіційній вікі-сторінці Ryujinx (англійською)", "zh_CN": "打开 Ryujinx 官方 Wiki 的常见问题和问题排除页面", @@ -1351,11 +1324,8 @@ "pt_BR": "", "ru_RU": "Руководство по установке и настройке", "sv_SE": "Konfigurationsguide", -<<<<<<< HEAD "th_TH": "คู่มือการติดตั้งและกำหนดค่า", -======= "th_TH": "คู่มือการตั้งค่าและการกำหนดค่า", ->>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Посібник зі встановлення та налаштування (eng)", "zh_CN": "安装与配置指南", @@ -1380,11 +1350,8 @@ "pt_BR": "", "ru_RU": "Открывает страницу Руководство по установке и настройке на официальной странице вики Ryujinx", "sv_SE": "Öppnar konfigurationsguiden på den officiella Ryujinx-wikin", -<<<<<<< HEAD "th_TH": "เปิดคู่มือการติดตั้งและกำหนดค่าบนวิกิ Ryujinx อย่างเป็นทางการ", -======= "th_TH": "เปิดคู่มือการตั้งค่าและการกำหนดค่าบนวิกิทางการของ Ryujinx", ->>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Відкриває посібник з Налаштування та конфігурації на офіційній вікі-сторінці Ryujinx (англійською)", "zh_CN": "打开 Ryujinx 官方 Wiki 的安装与配置指南", @@ -1409,11 +1376,8 @@ "pt_BR": "", "ru_RU": "Гайд по мультиплееру (LDN/LAN)", "sv_SE": "Flerspelarguide (LDN/LAN)", -<<<<<<< HEAD "th_TH": "คู่มือการเล่นหลายผู้เล่น (LDN/LAN)", -======= "th_TH": "คู่มือผู้เล่นหลายคน (LDN/LAN)", ->>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Посібник з мультиплеєру (LDN/LAN) (eng)", "zh_CN": "多人游戏(LDN/LAN)指南", @@ -1438,11 +1402,8 @@ "pt_BR": "", "ru_RU": "Открывает гайд по мультиплееру на официальной странице вики Ryujinx", "sv_SE": "Öppnar flerspelarguiden på den officiella Ryujinx-wikin", -<<<<<<< HEAD "th_TH": "เปิดคู่มือผู้เล่นหลายคนบนวิกิ Ryujinx อย่างเป็นทางการ", -======= "th_TH": "เปิดคู่มือผู้เล่นหลายคนบนวิกิทางการของ Ryujinx", ->>>>>>> c7ca03ea8c8c0b6f4cf3c00186eca120f78ff367 "tr_TR": "", "uk_UA": "Відкриває посібник з налаштування Мультиплеєру на офіційній вікі-сторінці Ryujinx (англійською)", "zh_CN": "打开 Ryujinx 官方 Wiki 的多人游戏指南", -- 2.47.1 From 38ef65aae017f0c7bd79d9f055fbcd8d7438124f Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 1 Feb 2025 14:07:32 -0600 Subject: [PATCH 003/209] misc: chore: Move all GeneratedRegex methods into one static class with static instance accessors. --- src/Ryujinx.Common/Helpers/Patterns.cs | 118 ++++++++++++++++++ src/Ryujinx.Graphics.Vulkan/Vendor.cs | 8 +- src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 5 +- .../HOS/Applets/Error/ErrorApplet.cs | 6 +- .../CJKCharacterValidation.cs | 14 +-- .../NumericCharacterValidation.cs | 14 +-- .../Sockets/Sfdnsres/Proxy/DnsBlacklist.cs | 30 +---- .../Loaders/Executables/NsoExecutable.cs | 14 +-- .../UI/ViewModels/SettingsViewModel.cs | 6 +- 9 files changed, 139 insertions(+), 76 deletions(-) create mode 100644 src/Ryujinx.Common/Helpers/Patterns.cs diff --git a/src/Ryujinx.Common/Helpers/Patterns.cs b/src/Ryujinx.Common/Helpers/Patterns.cs new file mode 100644 index 000000000..84cc1353a --- /dev/null +++ b/src/Ryujinx.Common/Helpers/Patterns.cs @@ -0,0 +1,118 @@ +using System.Text.RegularExpressions; + +namespace Ryujinx.Common.Helper +{ + public static partial class Patterns + { + #region Accessors + + public static readonly Regex Numeric = NumericRegex(); + + public static readonly Regex AmdGcn = AmdGcnRegex(); + public static readonly Regex NvidiaConsumerClass = NvidiaConsumerClassRegex(); + + public static readonly Regex DomainLp1Ns = DomainLp1NsRegex(); + public static readonly Regex DomainLp1Lp1Npln = DomainLp1Lp1NplnRegex(); + public static readonly Regex DomainLp1Znc = DomainLp1ZncRegex(); + public static readonly Regex DomainSbApi = DomainSbApiRegex(); + public static readonly Regex DomainSbAccounts = DomainSbAccountsRegex(); + public static readonly Regex DomainAccounts = DomainAccountsRegex(); + + public static readonly Regex Module = ModuleRegex(); + public static readonly Regex FsSdk = FsSdkRegex(); + public static readonly Regex SdkMw = SdkMwRegex(); + + // ReSharper disable once InconsistentNaming + public static readonly Regex CJK = CJKRegex(); + + public static readonly Regex LdnPassphrase = LdnPassphraseRegex(); + + public static readonly Regex CleanText = CleanTextRegex(); + + #endregion + + #region Generated pattern stubs + + #region Numeric validation + + [GeneratedRegex("[0-9]|.")] + internal static partial Regex NumericRegex(); + + #endregion + + #region GPU names + + [GeneratedRegex( + "Radeon (((HD|R(5|7|9|X)) )?((M?[2-6]\\d{2}(\\D|$))|([7-8]\\d{3}(\\D|$))|Fury|Nano))|(Pro Duo)")] + internal static partial Regex AmdGcnRegex(); + + [GeneratedRegex("NVIDIA GeForce (R|G)?TX? (\\d{3}\\d?)M?")] + internal static partial Regex NvidiaConsumerClassRegex(); + + #endregion + + #region DNS blocking + + public static readonly Regex[] BlockedHosts = + [ + DomainLp1Ns, + DomainLp1Lp1Npln, + DomainLp1Znc, + DomainSbApi, + DomainSbAccounts, + DomainAccounts + ]; + + const RegexOptions DnsRegexOpts = + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; + + [GeneratedRegex(@"^(.*)\-lp1\.(n|s)\.n\.srv\.nintendo\.net$", DnsRegexOpts)] + internal static partial Regex DomainLp1NsRegex(); + + [GeneratedRegex(@"^(.*)\-lp1\.lp1\.t\.npln\.srv\.nintendo\.net$", DnsRegexOpts)] + internal static partial Regex DomainLp1Lp1NplnRegex(); + + [GeneratedRegex(@"^(.*)\-lp1\.(znc|p)\.srv\.nintendo\.net$", DnsRegexOpts)] + internal static partial Regex DomainLp1ZncRegex(); + + [GeneratedRegex(@"^(.*)\-sb\-api\.accounts\.nintendo\.com$", DnsRegexOpts)] + internal static partial Regex DomainSbApiRegex(); + + [GeneratedRegex(@"^(.*)\-sb\.accounts\.nintendo\.com$", DnsRegexOpts)] + internal static partial Regex DomainSbAccountsRegex(); + + [GeneratedRegex(@"^accounts\.nintendo\.com$", DnsRegexOpts)] + internal static partial Regex DomainAccountsRegex(); + + #endregion + + #region Executable information + + [GeneratedRegex(@"[a-z]:[\\/][ -~]{5,}\.nss", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + internal static partial Regex ModuleRegex(); + + [GeneratedRegex(@"sdk_version: ([0-9.]*)")] + internal static partial Regex FsSdkRegex(); + + [GeneratedRegex(@"SDK MW[ -~]*")] + internal static partial Regex SdkMwRegex(); + + #endregion + + #region CJK + + [GeneratedRegex( + "\\p{IsHangulJamo}|\\p{IsCJKRadicalsSupplement}|\\p{IsCJKSymbolsandPunctuation}|\\p{IsEnclosedCJKLettersandMonths}|\\p{IsCJKCompatibility}|\\p{IsCJKUnifiedIdeographsExtensionA}|\\p{IsCJKUnifiedIdeographs}|\\p{IsHangulSyllables}|\\p{IsCJKCompatibilityForms}")] + private static partial Regex CJKRegex(); + + #endregion + + [GeneratedRegex("Ryujinx-[0-9a-f]{8}")] + private static partial Regex LdnPassphraseRegex(); + + [GeneratedRegex(@"[^\u0000\u0009\u000A\u000D\u0020-\uFFFF]..")] + private static partial Regex CleanTextRegex(); + + #endregion + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/Vendor.cs b/src/Ryujinx.Graphics.Vulkan/Vendor.cs index 6a2a76a88..87c6407cd 100644 --- a/src/Ryujinx.Graphics.Vulkan/Vendor.cs +++ b/src/Ryujinx.Graphics.Vulkan/Vendor.cs @@ -16,14 +16,8 @@ namespace Ryujinx.Graphics.Vulkan Unknown, } - static partial class VendorUtils + static class VendorUtils { - [GeneratedRegex("Radeon (((HD|R(5|7|9|X)) )?((M?[2-6]\\d{2}(\\D|$))|([7-8]\\d{3}(\\D|$))|Fury|Nano))|(Pro Duo)")] - public static partial Regex AmdGcnRegex(); - - [GeneratedRegex("NVIDIA GeForce (R|G)?TX? (\\d{3}\\d?)M?")] - public static partial Regex NvidiaConsumerClassRegex(); - public static Vendor FromId(uint id) { return id switch diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index 986baf91b..e90606dcf 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -1,5 +1,6 @@ using Gommon; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Shader; @@ -375,11 +376,11 @@ namespace Ryujinx.Graphics.Vulkan GpuVersion = $"Vulkan v{ParseStandardVulkanVersion(properties.ApiVersion)}, Driver v{ParseDriverVersion(ref properties)}"; - IsAmdGcn = !IsMoltenVk && Vendor == Vendor.Amd && VendorUtils.AmdGcnRegex().IsMatch(GpuRenderer); + IsAmdGcn = !IsMoltenVk && Vendor == Vendor.Amd && Patterns.AmdGcn.IsMatch(GpuRenderer); if (Vendor == Vendor.Nvidia) { - Match match = VendorUtils.NvidiaConsumerClassRegex().Match(GpuRenderer); + Match match = Patterns.NvidiaConsumerClass.Match(GpuRenderer); if (match != null && int.TryParse(match.Groups[2].Value, out int gpuNumber)) { diff --git a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs index 54b5721c1..b41bc60b1 100644 --- a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs @@ -5,6 +5,7 @@ using LibHac.FsSystem; using LibHac.Ncm; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Am.AppletAE; using Ryujinx.HLE.HOS.SystemState; @@ -30,9 +31,6 @@ namespace Ryujinx.HLE.HOS.Applets.Error public event EventHandler AppletStateChanged; - [GeneratedRegex(@"[^\u0000\u0009\u000A\u000D\u0020-\uFFFF]..")] - private static partial Regex CleanTextRegex(); - public ErrorApplet(Horizon horizon) { _horizon = horizon; @@ -107,7 +105,7 @@ namespace Ryujinx.HLE.HOS.Applets.Error private static string CleanText(string value) { - return CleanTextRegex().Replace(value, string.Empty).Replace("\0", string.Empty); + return Patterns.CleanText.Replace(value, string.Empty).Replace("\0", string.Empty); } private string GetMessageText(uint module, uint description, string key) diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CJKCharacterValidation.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CJKCharacterValidation.cs index 6134a3cdd..022fa9d5b 100644 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CJKCharacterValidation.cs +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CJKCharacterValidation.cs @@ -1,17 +1,9 @@ -using System.Text.RegularExpressions; +using Ryujinx.Common.Helper; namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { - public static partial class CJKCharacterValidation + public static class CJKCharacterValidation { - public static bool IsCJK(char value) - { - Regex regex = CJKRegex(); - - return regex.IsMatch(value.ToString()); - } - - [GeneratedRegex("\\p{IsHangulJamo}|\\p{IsCJKRadicalsSupplement}|\\p{IsCJKSymbolsandPunctuation}|\\p{IsEnclosedCJKLettersandMonths}|\\p{IsCJKCompatibility}|\\p{IsCJKUnifiedIdeographsExtensionA}|\\p{IsCJKUnifiedIdeographs}|\\p{IsHangulSyllables}|\\p{IsCJKCompatibilityForms}")] - private static partial Regex CJKRegex(); + public static bool IsCJK(char value) => Patterns.CJK.IsMatch(value.ToString()); } } diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/NumericCharacterValidation.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/NumericCharacterValidation.cs index d72b68eae..4f61773c0 100644 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/NumericCharacterValidation.cs +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/NumericCharacterValidation.cs @@ -1,17 +1,9 @@ -using System.Text.RegularExpressions; +using Ryujinx.Common.Helper; namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { - public static partial class NumericCharacterValidation + public static class NumericCharacterValidation { - public static bool IsNumeric(char value) - { - Regex regex = NumericRegex(); - - return regex.IsMatch(value.ToString()); - } - - [GeneratedRegex("[0-9]|.")] - private static partial Regex NumericRegex(); + public static bool IsNumeric(char value) => Patterns.Numeric.IsMatch(value.ToString()); } } diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Proxy/DnsBlacklist.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Proxy/DnsBlacklist.cs index 78c6be164..507e60573 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Proxy/DnsBlacklist.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Proxy/DnsBlacklist.cs @@ -1,37 +1,13 @@ +using Ryujinx.Common.Helper; using System.Text.RegularExpressions; namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Proxy { - static partial class DnsBlacklist + static class DnsBlacklist { - const RegexOptions RegexOpts = RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; - - [GeneratedRegex(@"^(.*)\-lp1\.(n|s)\.n\.srv\.nintendo\.net$", RegexOpts)] - private static partial Regex BlockedHost1(); - [GeneratedRegex(@"^(.*)\-lp1\.lp1\.t\.npln\.srv\.nintendo\.net$", RegexOpts)] - private static partial Regex BlockedHost2(); - [GeneratedRegex(@"^(.*)\-lp1\.(znc|p)\.srv\.nintendo\.net$", RegexOpts)] - private static partial Regex BlockedHost3(); - [GeneratedRegex(@"^(.*)\-sb\-api\.accounts\.nintendo\.com$", RegexOpts)] - private static partial Regex BlockedHost4(); - [GeneratedRegex(@"^(.*)\-sb\.accounts\.nintendo\.com$", RegexOpts)] - private static partial Regex BlockedHost5(); - [GeneratedRegex(@"^accounts\.nintendo\.com$", RegexOpts)] - private static partial Regex BlockedHost6(); - - private static readonly Regex[] _blockedHosts = - [ - BlockedHost1(), - BlockedHost2(), - BlockedHost3(), - BlockedHost4(), - BlockedHost5(), - BlockedHost6() - ]; - public static bool IsHostBlocked(string host) { - foreach (Regex regex in _blockedHosts) + foreach (Regex regex in Patterns.BlockedHosts) { if (regex.IsMatch(host)) { diff --git a/src/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs b/src/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs index 5217612b9..84f229d8e 100644 --- a/src/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs +++ b/src/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs @@ -2,6 +2,7 @@ using LibHac.Common.FixedArrays; using LibHac.Fs; using LibHac.Loader; using LibHac.Tools.FsSystem; +using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using System; using System.Text; @@ -29,13 +30,6 @@ namespace Ryujinx.HLE.Loaders.Executables public string Name; public Array32 BuildId; - [GeneratedRegex(@"[a-z]:[\\/][ -~]{5,}\.nss", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] - private static partial Regex ModuleRegex(); - [GeneratedRegex(@"sdk_version: ([0-9.]*)")] - private static partial Regex FsSdkRegex(); - [GeneratedRegex(@"SDK MW[ -~]*")] - private static partial Regex SdkMwRegex(); - public NsoExecutable(IStorage inStorage, string name = null) { NsoReader reader = new(); @@ -90,7 +84,7 @@ namespace Ryujinx.HLE.Loaders.Executables if (string.IsNullOrEmpty(modulePath)) { - Match moduleMatch = ModuleRegex().Match(rawTextBuffer); + Match moduleMatch = Patterns.Module.Match(rawTextBuffer); if (moduleMatch.Success) { modulePath = moduleMatch.Value; @@ -99,13 +93,13 @@ namespace Ryujinx.HLE.Loaders.Executables stringBuilder.AppendLine($" Module: {modulePath}"); - Match fsSdkMatch = FsSdkRegex().Match(rawTextBuffer); + Match fsSdkMatch = Patterns.FsSdk.Match(rawTextBuffer); if (fsSdkMatch.Success) { stringBuilder.AppendLine($" FS SDK Version: {fsSdkMatch.Value.Replace("sdk_version: ", string.Empty)}"); } - MatchCollection sdkMwMatches = SdkMwRegex().Matches(rawTextBuffer); + MatchCollection sdkMwMatches = Patterns.SdkMw.Matches(rawTextBuffer); if (sdkMwMatches.Count != 0) { string libHeader = " SDK Libraries: "; diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 5a73dd574..488828482 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -16,6 +16,7 @@ using Ryujinx.Ava.Utilities.Configuration.System; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.Common.GraphicsDriver; +using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Vulkan; @@ -330,9 +331,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - [GeneratedRegex("Ryujinx-[0-9a-f]{8}")] - private static partial Regex LdnPassphraseRegex(); - public bool IsInvalidLdnPassphraseVisible { get; set; } public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() @@ -470,7 +468,7 @@ namespace Ryujinx.Ava.UI.ViewModels private bool ValidateLdnPassphrase(string passphrase) { - return string.IsNullOrEmpty(passphrase) || (passphrase.Length == 16 && LdnPassphraseRegex().IsMatch(passphrase)); + return string.IsNullOrEmpty(passphrase) || (passphrase.Length == 16 && Patterns.LdnPassphrase.IsMatch(passphrase)); } public void ValidateAndSetTimeZone(string location) -- 2.47.1 From ad9d6588e8e7a91e6185c11e8824b944a610bdb5 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 1 Feb 2025 14:11:35 -0600 Subject: [PATCH 004/209] misc: chore: Collapse HLE swkbd character validation utils into a single class --- .../Applets/SoftwareKeyboard/CJKCharacterValidation.cs | 9 --------- ...ericCharacterValidation.cs => CharacterValidation.cs} | 3 ++- src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs | 4 ++-- 3 files changed, 4 insertions(+), 12 deletions(-) delete mode 100644 src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CJKCharacterValidation.cs rename src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/{NumericCharacterValidation.cs => CharacterValidation.cs} (59%) diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CJKCharacterValidation.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CJKCharacterValidation.cs deleted file mode 100644 index 022fa9d5b..000000000 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CJKCharacterValidation.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ryujinx.Common.Helper; - -namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard -{ - public static class CJKCharacterValidation - { - public static bool IsCJK(char value) => Patterns.CJK.IsMatch(value.ToString()); - } -} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/NumericCharacterValidation.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CharacterValidation.cs similarity index 59% rename from src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/NumericCharacterValidation.cs rename to src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CharacterValidation.cs index 4f61773c0..5ce8204cb 100644 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/NumericCharacterValidation.cs +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CharacterValidation.cs @@ -2,8 +2,9 @@ using Ryujinx.Common.Helper; namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { - public static class NumericCharacterValidation + public static class CharacterValidation { public static bool IsNumeric(char value) => Patterns.Numeric.IsMatch(value.ToString()); + public static bool IsCJK(char value) => Patterns.CJK.IsMatch(value.ToString()); } } diff --git a/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs b/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs index 75a9b3d41..dd5b7d9f1 100644 --- a/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs +++ b/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs @@ -144,12 +144,12 @@ namespace Ryujinx.Ava.UI.Controls case KeyboardMode.Numeric: localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeNumeric); validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText); - _checkInput = text => text.All(NumericCharacterValidation.IsNumeric); + _checkInput = text => text.All(CharacterValidation.IsNumeric); break; case KeyboardMode.Alphabet: localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeAlphabet); validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText); - _checkInput = text => text.All(value => !CJKCharacterValidation.IsCJK(value)); + _checkInput = text => text.All(value => !CharacterValidation.IsCJK(value)); break; case KeyboardMode.ASCII: localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeASCII); -- 2.47.1 From a46aacf2e2a723e06ac8f493e5c0445b5b6d219e Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 1 Feb 2025 19:20:36 -0600 Subject: [PATCH 005/209] gpu: Switch the 500ms timeout back to 1s It seemed like it was waiting for 1 second no matter what; might as well have the log & syncpoint map match reality. --- .../Synchronization/SynchronizationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs b/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs index 7165af6ad..d51b0ef60 100644 --- a/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs @@ -83,7 +83,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization // TODO: Remove this when GPU channel scheduling will be implemented. if (timeout == Timeout.InfiniteTimeSpan) { - timeout = TimeSpan.FromMilliseconds(500); + timeout = TimeSpan.FromSeconds(1); } using ManualResetEvent waitEvent = new(false); -- 2.47.1 From 50cee3fd19e4503b63eec3ede8e23cc0d5275bf6 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 02:20:14 -0600 Subject: [PATCH 006/209] feature: HorizonStatic PlayReportPrinted event --- src/Ryujinx.Horizon/HorizonStatic.cs | 5 +++++ src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/Ryujinx.Horizon/HorizonStatic.cs b/src/Ryujinx.Horizon/HorizonStatic.cs index 305d54bd1..6de6c4d05 100644 --- a/src/Ryujinx.Horizon/HorizonStatic.cs +++ b/src/Ryujinx.Horizon/HorizonStatic.cs @@ -1,3 +1,4 @@ +using MsgPack; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; @@ -6,6 +7,10 @@ namespace Ryujinx.Horizon { public static class HorizonStatic { + internal static void HandlePlayReport(MessagePackObject report) => PlayReportPrinted.Invoke(report); + + public static event Action PlayReportPrinted; + [ThreadStatic] private static HorizonOptions _options; diff --git a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs index 4ed7dd48e..ab972d85e 100644 --- a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs +++ b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs @@ -230,6 +230,8 @@ namespace Ryujinx.Horizon.Prepo.Ipc builder.AppendLine($" Room: {gameRoom}"); builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}"); + + HorizonStatic.HandlePlayReport(deserializedReport); Logger.Info?.Print(LogClass.ServicePrepo, builder.ToString()); -- 2.47.1 From 37af8c70aa724e7e5096fcb16f821e810cf0942a Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 02:21:33 -0600 Subject: [PATCH 007/209] UI: RPC: Add the ability for the DiscordIntegrationModule to inspect values in Play Reports and dynamically show different gameplay values, depending on a predefined map of values and formatters. Currently only BOTW Master Mode is supported. Open to PRs! --- src/Ryujinx/DiscordIntegrationModule.cs | 62 +++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index 70ebedfa4..392895d3a 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -1,11 +1,18 @@ using DiscordRPC; using Gommon; +using MsgPack; using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.Configuration; using Ryujinx.Common; +using Ryujinx.Common.Logging; using Ryujinx.HLE; using Ryujinx.HLE.Loaders.Processes; +using Ryujinx.Horizon; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using System.Text; namespace Ryujinx.Ava @@ -30,6 +37,7 @@ namespace Ryujinx.Ava private static DiscordRpcClient _discordClient; private static RichPresence _discordPresenceMain; + private static RichPresence _discordPresencePlaying; public static void Initialize() { @@ -47,6 +55,7 @@ namespace Ryujinx.Ava ConfigurationState.Instance.EnableDiscordIntegration.Event += Update; TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue); + HorizonStatic.PlayReportPrinted += HandlePlayReport; } private static void Update(object sender, ReactiveEventArgs evnt) @@ -84,9 +93,8 @@ namespace Ryujinx.Ava SwitchToMainState(); } - private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes) - { - _discordClient?.SetPresence(new RichPresence + private static RichPresence CreatePlayingState(ApplicationMetadata appMeta, ProcessResult procRes) => + new() { Assets = new Assets { @@ -100,10 +108,54 @@ namespace Ryujinx.Ava ? $"Total play time: {ValueFormatUtils.FormatTimeSpan(appMeta.TimePlayed)}" : "Never played", Timestamps = GuestAppStartedAt ??= Timestamps.Now - }); + }; + + private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes) + { + _discordClient?.SetPresence(_discordPresencePlaying ??= CreatePlayingState(appMeta, procRes)); + } + + private static void UpdatePlayingState() + { + _discordClient?.SetPresence(_discordPresencePlaying); } - private static void SwitchToMainState() => _discordClient?.SetPresence(_discordPresenceMain); + private static void SwitchToMainState() + { + _discordClient?.SetPresence(_discordPresenceMain); + _discordPresencePlaying = null; + } + + private static void HandlePlayReport(MessagePackObject playReport) + { + if (!TitleIDs.CurrentApplication.Value.HasValue) return; + if (_discordPresencePlaying is null) return; + if (!playReport.IsDictionary) return; + + _playReportValues + .FindFirst(x => x.Key.EqualsIgnoreCase(TitleIDs.CurrentApplication.Value)) + .Convert(x => x.Value) + .IfPresent(x => + { + if (!playReport.AsDictionary().TryGetValue(x.ReportKey, out MessagePackObject valuePackObject)) + return; + + _discordPresencePlaying.Details = x.Formatter(valuePackObject.ToObject()); + UpdatePlayingState(); + Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report."); + }); + } + + // title ID -> Play Report key & value formatter + private static readonly ReadOnlyDictionary Formatter)> + _playReportValues = new(new Dictionary Formatter)> + { + { + // Breath of the Wild Master Mode display + "01007ef00011e000", + ("IsHardMode", val => val is 1 ? "Playing Master Mode" : "Playing Normal Mode") + } + }); private static string TruncateToByteLength(string input) { -- 2.47.1 From ea2287af036bd2ab41743bac8e3bba7c95a61d7e Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 13:17:31 -0600 Subject: [PATCH 008/209] misc: chore: Rewrite play report checker to use a simple loop instead of Gommon Optionals (I love how a class that's supposed to guard against null values entering your code still allows them thats so cool) --- src/Ryujinx/DiscordIntegrationModule.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index 392895d3a..8d1a55582 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -132,18 +132,18 @@ namespace Ryujinx.Ava if (_discordPresencePlaying is null) return; if (!playReport.IsDictionary) return; - _playReportValues - .FindFirst(x => x.Key.EqualsIgnoreCase(TitleIDs.CurrentApplication.Value)) - .Convert(x => x.Value) - .IfPresent(x => - { - if (!playReport.AsDictionary().TryGetValue(x.ReportKey, out MessagePackObject valuePackObject)) - return; + foreach ((string titleId, (string reportKey, Func formatter)) in _playReportValues) + { + if (!TitleIDs.CurrentApplication.Value.Value.EqualsIgnoreCase(titleId)) + continue; + + if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) + return; - _discordPresencePlaying.Details = x.Formatter(valuePackObject.ToObject()); - UpdatePlayingState(); - Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report."); - }); + _discordPresencePlaying.Details = formatter(valuePackObject.ToObject()); + UpdatePlayingState(); + Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report."); + } } // title ID -> Play Report key & value formatter -- 2.47.1 From 2d7700949c26352f6eba0c3e67f903a064ac9913 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 16:07:30 -0600 Subject: [PATCH 009/209] UI: Play Report Analysis V2 Support for multiple keys per game, and provide an order of resolution via Priority. (Currently) functionally identical to before, as only BOTW Master Mode is supported. --- .../Helpers/PlayReportAnalyzer.cs | 80 +++++++++++++++++++ src/Ryujinx.Horizon/HorizonStatic.cs | 2 +- src/Ryujinx/DiscordIntegrationModule.cs | 56 ++++++------- 3 files changed, 104 insertions(+), 34 deletions(-) create mode 100644 src/Ryujinx.Common/Helpers/PlayReportAnalyzer.cs diff --git a/src/Ryujinx.Common/Helpers/PlayReportAnalyzer.cs b/src/Ryujinx.Common/Helpers/PlayReportAnalyzer.cs new file mode 100644 index 000000000..b69b18f57 --- /dev/null +++ b/src/Ryujinx.Common/Helpers/PlayReportAnalyzer.cs @@ -0,0 +1,80 @@ +using Gommon; +using MsgPack; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Common.Helper +{ + public class PlayReportAnalyzer + { + private readonly List _specs = []; + + public PlayReportAnalyzer AddSpec(string titleId, Func transform) + { + _specs.Add(transform(new PlayReportGameSpec { TitleIdStr = titleId })); + return this; + } + + public PlayReportAnalyzer AddSpec(string titleId, Action transform) + { + _specs.Add(new PlayReportGameSpec { TitleIdStr = titleId }.Apply(transform)); + return this; + } + + public Optional Run(string runningGameId, MessagePackObject playReport) + { + if (!playReport.IsDictionary) + return Optional.None; + + if (!_specs.TryGetFirst(s => s.TitleIdStr.EqualsIgnoreCase(runningGameId), out PlayReportGameSpec spec)) + return Optional.None; + + foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority)) + { + if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) + continue; + + return formatSpec.ValueFormatter(valuePackObject.ToObject()); + } + + return Optional.None; + } + + } + + public class PlayReportGameSpec + { + public required string TitleIdStr { get; init; } + public List Analyses { get; } = []; + + public PlayReportGameSpec AddValueFormatter(string reportKey, Func valueFormatter) + { + Analyses.Add(new PlayReportValueFormatterSpec + { + Priority = Analyses.Count, + ReportKey = reportKey, + ValueFormatter = valueFormatter + }); + return this; + } + + public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, Func valueFormatter) + { + Analyses.Add(new PlayReportValueFormatterSpec + { + Priority = priority, + ReportKey = reportKey, + ValueFormatter = valueFormatter + }); + return this; + } + } + + public struct PlayReportValueFormatterSpec + { + public required int Priority { get; init; } + public required string ReportKey { get; init; } + public required Func ValueFormatter { get; init; } + } +} diff --git a/src/Ryujinx.Horizon/HorizonStatic.cs b/src/Ryujinx.Horizon/HorizonStatic.cs index 6de6c4d05..f08ddb3c0 100644 --- a/src/Ryujinx.Horizon/HorizonStatic.cs +++ b/src/Ryujinx.Horizon/HorizonStatic.cs @@ -7,7 +7,7 @@ namespace Ryujinx.Horizon { public static class HorizonStatic { - internal static void HandlePlayReport(MessagePackObject report) => PlayReportPrinted.Invoke(report); + internal static void HandlePlayReport(MessagePackObject report) => PlayReportPrinted?.Invoke(report); public static event Action PlayReportPrinted; diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index 8d1a55582..add46bda4 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -5,6 +5,7 @@ using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.Configuration; using Ryujinx.Common; +using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using Ryujinx.HLE; using Ryujinx.HLE.Loaders.Processes; @@ -23,12 +24,12 @@ namespace Ryujinx.Ava public static Timestamps GuestAppStartedAt { get; set; } private static string VersionString - => (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}"; + => (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}"; - private static readonly string _description = - ReleaseInformation.IsValid - ? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}" - : "dev build"; + private static readonly string _description = + ReleaseInformation.IsValid + ? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}" + : "dev build"; private const string ApplicationId = "1293250299716173864"; @@ -45,8 +46,7 @@ namespace Ryujinx.Ava { Assets = new Assets { - LargeImageKey = "ryujinx", - LargeImageText = TruncateToByteLength(_description) + LargeImageKey = "ryujinx", LargeImageText = TruncateToByteLength(_description) }, Details = "Main Menu", State = "Idling", @@ -86,10 +86,10 @@ namespace Ryujinx.Ava { if (titleId.TryGet(out string tid)) SwitchToPlayingState( - ApplicationLibrary.LoadAndSaveMetaData(tid), + ApplicationLibrary.LoadAndSaveMetaData(tid), Switch.Shared.Processes.ActiveApplication ); - else + else SwitchToMainState(); } @@ -114,7 +114,7 @@ namespace Ryujinx.Ava { _discordClient?.SetPresence(_discordPresencePlaying ??= CreatePlayingState(appMeta, procRes)); } - + private static void UpdatePlayingState() { _discordClient?.SetPresence(_discordPresencePlaying); @@ -126,37 +126,27 @@ namespace Ryujinx.Ava _discordPresencePlaying = null; } + private static readonly PlayReportAnalyzer _playReportAnalyzer = new PlayReportAnalyzer() + .AddSpec( // Breath of the Wild + "01007ef00011e000", + gameSpec => + gameSpec.AddValueFormatter("IsHardMode", val => val is 1 ? "Playing Master Mode" : "Playing Normal Mode") + ); + private static void HandlePlayReport(MessagePackObject playReport) { if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; - if (!playReport.IsDictionary) return; - foreach ((string titleId, (string reportKey, Func formatter)) in _playReportValues) - { - if (!TitleIDs.CurrentApplication.Value.Value.EqualsIgnoreCase(titleId)) - continue; - - if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) - return; + Optional details = _playReportAnalyzer.Run(TitleIDs.CurrentApplication.Value, playReport); - _discordPresencePlaying.Details = formatter(valuePackObject.ToObject()); - UpdatePlayingState(); - Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report."); - } + if (!details.HasValue) return; + + _discordPresencePlaying.Details = details; + UpdatePlayingState(); + Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report."); } - // title ID -> Play Report key & value formatter - private static readonly ReadOnlyDictionary Formatter)> - _playReportValues = new(new Dictionary Formatter)> - { - { - // Breath of the Wild Master Mode display - "01007ef00011e000", - ("IsHardMode", val => val is 1 ? "Playing Master Mode" : "Playing Normal Mode") - } - }); - private static string TruncateToByteLength(string input) { if (Encoding.UTF8.GetByteCount(input) <= ApplicationByteLimit) -- 2.47.1 From b38b5a1e709c797b3e409c8d34775284f35bff96 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 16:59:06 -0600 Subject: [PATCH 010/209] docs: compat: Saints Row IV: Playable -> Ingame Deadlock label added. Game sometimes just stops loading in loading screens. Game continues like its doing something but you'll be sitting there for minutes wondering why nothing is happening. Considering the game isn't crashing, this might be an emulator-side mutex issue. I've seen that before. --- docs/compatibility.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compatibility.csv b/docs/compatibility.csv index 570c93618..53ad389b6 100644 --- a/docs/compatibility.csv +++ b/docs/compatibility.csv @@ -2483,7 +2483,7 @@ 0100A5200C2E0000,"Safety First!",,playable,2021-01-06 09:05:23 0100A51013530000,"SaGa Frontier Remastered",nvdec,playable,2022-11-03 13:54:56 010003A00D0B4000,"SaGa SCARLET GRACE: AMBITIONS™",,playable,2022-10-06 13:20:31 -01008D100D43E000,"Saints Row IV®: Re-Elected™",ldn-untested;LAN,playable,2023-12-04 18:33:37 +01008D100D43E000,"Saints Row IV®: Re-Elected™",ldn-untested;LAN;deadlock,ingame,2025-02-02 16:57:53 0100DE600BEEE000,"SAINTS ROW®: THE THIRD™ - THE FULL PACKAGE",slow;LAN,playable,2023-08-24 02:40:58 01007F000EB36000,"Sakai and...",nvdec,playable,2022-12-15 13:53:19 0100B1400E8FE000,"Sakuna: Of Rice and Ruin",,playable,2023-07-24 13:47:13 -- 2.47.1 From bf713a80d66c14dc2b045fced8d1cfafbce91875 Mon Sep 17 00:00:00 2001 From: Piplup <100526773+piplup55@users.noreply.github.com> Date: Mon, 3 Feb 2025 02:29:00 +0000 Subject: [PATCH 011/209] PlayReportAnalyzer: Added Games (#614) Added Super Mario Odyssey, Super Mario Odyssey (China), Super Mario 3D World + Bowser's Fury, Mario Kart 8 Deluxe and Mario Kart 8 Deluxe (China) --- src/Ryujinx/DiscordIntegrationModule.cs | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index add46bda4..7f48089b9 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -82,6 +82,36 @@ namespace Ryujinx.Ava } } + public static string MarioKart8(object obj) + { + return obj switch + { + // Single Player + "Single" => "Single Player", + // Multiplayer + "Multi-2players" => "Multiplayer 2 Players", + "Multi-3players" => "Multiplayer 3 Players", + "Multi-4players" => "Multiplayer 4 Players", + // Wireless/LAN Play + "Local-Single" => "Wireless/LAN Play", + "Local-2players" => "Wireless/LAN Play 2 Players", + // CC Classes + "50cc" => "50cc", + "100cc" => "100cc", + "150cc" => "150cc", + "Mirror" => "Mirror (150cc)", + "200cc" => "200cc", + // Modes + "GrandPrix" => "Grand Prix", + "TimeAttack" => "Time Trials", + "VS" => "VS Races", + "Battle" => "Battle Mode", + "RaceStart" => "Selecting a Course", + "Race" => "Racing", + _ => "Playing Mario Kart 8 Deluxe" + }; + } + public static void Use(Optional titleId) { if (titleId.TryGet(out string tid)) @@ -131,6 +161,31 @@ namespace Ryujinx.Ava "01007ef00011e000", gameSpec => gameSpec.AddValueFormatter("IsHardMode", val => val is 1 ? "Playing Master Mode" : "Playing Normal Mode") + ) + .AddSpec( // Super Mario Odyssey + "0100000000010000", + gameSpec => + gameSpec.AddValueFormatter("is_kids_mode", val => val is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode") + ) + .AddSpec( // Super Mario Odyssey (China) + "010075000ECBE000", + gameSpec => + gameSpec.AddValueFormatter("is_kids_mode", val => val is 1 ? "Playing in 帮助模式" : "Playing in 普通模式") + ) + .AddSpec( // Super Mario 3D World + Bowser's Fury + "010028600EBDA000", + gameSpec => + gameSpec.AddValueFormatter("mode", val => val is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury") + ) + .AddSpec( // Mario Kart 8 Deluxe + "0100152000022000", + gameSpec => + gameSpec.AddValueFormatter("To", MarioKart8) + ) + .AddSpec( // Mario Kart 8 Deluxe (China) + "010075100E8EC000", + gameSpec => + gameSpec.AddValueFormatter("To", MarioKart8) ); private static void HandlePlayReport(MessagePackObject playReport) -- 2.47.1 From 8117e160c2e82ffacea312bb4b9c41171b4396d5 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 20:32:01 -0600 Subject: [PATCH 012/209] misc: chore: [ci skip] Move the play report analyzer definition into a PlayReport static class to avoid polluting the Discord integration module --- src/Ryujinx/DiscordIntegrationModule.cs | 64 +-------------------- src/Ryujinx/Utilities/PlayReport.cs | 76 +++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 63 deletions(-) create mode 100644 src/Ryujinx/Utilities/PlayReport.cs diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index 7f48089b9..c9fa1f732 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -82,36 +82,6 @@ namespace Ryujinx.Ava } } - public static string MarioKart8(object obj) - { - return obj switch - { - // Single Player - "Single" => "Single Player", - // Multiplayer - "Multi-2players" => "Multiplayer 2 Players", - "Multi-3players" => "Multiplayer 3 Players", - "Multi-4players" => "Multiplayer 4 Players", - // Wireless/LAN Play - "Local-Single" => "Wireless/LAN Play", - "Local-2players" => "Wireless/LAN Play 2 Players", - // CC Classes - "50cc" => "50cc", - "100cc" => "100cc", - "150cc" => "150cc", - "Mirror" => "Mirror (150cc)", - "200cc" => "200cc", - // Modes - "GrandPrix" => "Grand Prix", - "TimeAttack" => "Time Trials", - "VS" => "VS Races", - "Battle" => "Battle Mode", - "RaceStart" => "Selecting a Course", - "Race" => "Racing", - _ => "Playing Mario Kart 8 Deluxe" - }; - } - public static void Use(Optional titleId) { if (titleId.TryGet(out string tid)) @@ -155,45 +125,13 @@ namespace Ryujinx.Ava _discordClient?.SetPresence(_discordPresenceMain); _discordPresencePlaying = null; } - - private static readonly PlayReportAnalyzer _playReportAnalyzer = new PlayReportAnalyzer() - .AddSpec( // Breath of the Wild - "01007ef00011e000", - gameSpec => - gameSpec.AddValueFormatter("IsHardMode", val => val is 1 ? "Playing Master Mode" : "Playing Normal Mode") - ) - .AddSpec( // Super Mario Odyssey - "0100000000010000", - gameSpec => - gameSpec.AddValueFormatter("is_kids_mode", val => val is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode") - ) - .AddSpec( // Super Mario Odyssey (China) - "010075000ECBE000", - gameSpec => - gameSpec.AddValueFormatter("is_kids_mode", val => val is 1 ? "Playing in 帮助模式" : "Playing in 普通模式") - ) - .AddSpec( // Super Mario 3D World + Bowser's Fury - "010028600EBDA000", - gameSpec => - gameSpec.AddValueFormatter("mode", val => val is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury") - ) - .AddSpec( // Mario Kart 8 Deluxe - "0100152000022000", - gameSpec => - gameSpec.AddValueFormatter("To", MarioKart8) - ) - .AddSpec( // Mario Kart 8 Deluxe (China) - "010075100E8EC000", - gameSpec => - gameSpec.AddValueFormatter("To", MarioKart8) - ); private static void HandlePlayReport(MessagePackObject playReport) { if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; - Optional details = _playReportAnalyzer.Run(TitleIDs.CurrentApplication.Value, playReport); + Optional details = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, playReport); if (!details.HasValue) return; diff --git a/src/Ryujinx/Utilities/PlayReport.cs b/src/Ryujinx/Utilities/PlayReport.cs new file mode 100644 index 000000000..e913ffa13 --- /dev/null +++ b/src/Ryujinx/Utilities/PlayReport.cs @@ -0,0 +1,76 @@ +using Ryujinx.Common.Helper; + +namespace Ryujinx.Ava.Utilities +{ + public static class PlayReport + { + public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer() + .AddSpec( + "01007ef00011e000", + spec => spec.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode) + ) + .AddSpec( // Super Mario Odyssey + "0100000000010000", + spec => + spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode) + ) + .AddSpec( // Super Mario Odyssey (China) + "010075000ECBE000", + spec => + spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode) + ) + .AddSpec( // Super Mario 3D World + Bowser's Fury + "010028600EBDA000", + spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury) + ) + .AddSpec( // Mario Kart 8 Deluxe + "0100152000022000", + spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) + ) + .AddSpec( // Mario Kart 8 Deluxe (China) + "010075100E8EC000", + spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) + ); + + private static string BreathOfTheWild_MasterMode(object val) + => val is 1 ? "Playing Master Mode" : "Playing Normal Mode"; + + private static string SuperMarioOdyssey_AssistMode(object val) + => val is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; + + private static string SuperMarioOdysseyChina_AssistMode(object val) + => val is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; + + private static string SuperMario3DWorldOrBowsersFury(object val) + => val is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; + + private static string MarioKart8Deluxe_Mode(object obj) + => obj switch + { + // Single Player + "Single" => "Single Player", + // Multiplayer + "Multi-2players" => "Multiplayer 2 Players", + "Multi-3players" => "Multiplayer 3 Players", + "Multi-4players" => "Multiplayer 4 Players", + // Wireless/LAN Play + "Local-Single" => "Wireless/LAN Play", + "Local-2players" => "Wireless/LAN Play 2 Players", + // CC Classes + "50cc" => "50cc", + "100cc" => "100cc", + "150cc" => "150cc", + "Mirror" => "Mirror (150cc)", + "200cc" => "200cc", + // Modes + "GrandPrix" => "Grand Prix", + "TimeAttack" => "Time Trials", + "VS" => "VS Races", + "Battle" => "Battle Mode", + "RaceStart" => "Selecting a Course", + "Race" => "Racing", + //TODO: refactor value formatting system to pass in the name from the content archive so this can be localized properly + _ => "Playing Mario Kart 8 Deluxe" + }; + } +} -- 2.47.1 From fe43c32e60008fa39dd6ab6216de8df97d13f98e Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 20:47:42 -0600 Subject: [PATCH 013/209] UI: The argument to Play Report value formatters is now a struct containing the current ApplicationMetadata & the BoxedValue that was the only argument previously. This allows for the title of Mario Kart to be localized when one of the value checkers doesn't match. --- .../Helpers/PlayReportAnalyzer.cs | 80 ------------ src/Ryujinx/DiscordIntegrationModule.cs | 5 +- src/Ryujinx/Utilities/PlayReport.cs | 121 ++++++++++++++++-- 3 files changed, 112 insertions(+), 94 deletions(-) delete mode 100644 src/Ryujinx.Common/Helpers/PlayReportAnalyzer.cs diff --git a/src/Ryujinx.Common/Helpers/PlayReportAnalyzer.cs b/src/Ryujinx.Common/Helpers/PlayReportAnalyzer.cs deleted file mode 100644 index b69b18f57..000000000 --- a/src/Ryujinx.Common/Helpers/PlayReportAnalyzer.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Gommon; -using MsgPack; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Ryujinx.Common.Helper -{ - public class PlayReportAnalyzer - { - private readonly List _specs = []; - - public PlayReportAnalyzer AddSpec(string titleId, Func transform) - { - _specs.Add(transform(new PlayReportGameSpec { TitleIdStr = titleId })); - return this; - } - - public PlayReportAnalyzer AddSpec(string titleId, Action transform) - { - _specs.Add(new PlayReportGameSpec { TitleIdStr = titleId }.Apply(transform)); - return this; - } - - public Optional Run(string runningGameId, MessagePackObject playReport) - { - if (!playReport.IsDictionary) - return Optional.None; - - if (!_specs.TryGetFirst(s => s.TitleIdStr.EqualsIgnoreCase(runningGameId), out PlayReportGameSpec spec)) - return Optional.None; - - foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority)) - { - if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) - continue; - - return formatSpec.ValueFormatter(valuePackObject.ToObject()); - } - - return Optional.None; - } - - } - - public class PlayReportGameSpec - { - public required string TitleIdStr { get; init; } - public List Analyses { get; } = []; - - public PlayReportGameSpec AddValueFormatter(string reportKey, Func valueFormatter) - { - Analyses.Add(new PlayReportValueFormatterSpec - { - Priority = Analyses.Count, - ReportKey = reportKey, - ValueFormatter = valueFormatter - }); - return this; - } - - public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, Func valueFormatter) - { - Analyses.Add(new PlayReportValueFormatterSpec - { - Priority = priority, - ReportKey = reportKey, - ValueFormatter = valueFormatter - }); - return this; - } - } - - public struct PlayReportValueFormatterSpec - { - public required int Priority { get; init; } - public required string ReportKey { get; init; } - public required Func ValueFormatter { get; init; } - } -} diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index c9fa1f732..5561c1562 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -39,6 +39,7 @@ namespace Ryujinx.Ava private static DiscordRpcClient _discordClient; private static RichPresence _discordPresenceMain; private static RichPresence _discordPresencePlaying; + private static ApplicationMetadata _currentApp; public static void Initialize() { @@ -113,6 +114,7 @@ namespace Ryujinx.Ava private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes) { _discordClient?.SetPresence(_discordPresencePlaying ??= CreatePlayingState(appMeta, procRes)); + _currentApp = appMeta; } private static void UpdatePlayingState() @@ -124,6 +126,7 @@ namespace Ryujinx.Ava { _discordClient?.SetPresence(_discordPresenceMain); _discordPresencePlaying = null; + _currentApp = null; } private static void HandlePlayReport(MessagePackObject playReport) @@ -131,7 +134,7 @@ namespace Ryujinx.Ava if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; - Optional details = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, playReport); + Optional details = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport); if (!details.HasValue) return; diff --git a/src/Ryujinx/Utilities/PlayReport.cs b/src/Ryujinx/Utilities/PlayReport.cs index e913ffa13..9665a1628 100644 --- a/src/Ryujinx/Utilities/PlayReport.cs +++ b/src/Ryujinx/Utilities/PlayReport.cs @@ -1,4 +1,10 @@ -using Ryujinx.Common.Helper; +using Gommon; +using MsgPack; +using Ryujinx.Ava.Utilities.AppLibrary; +using Ryujinx.Common.Helper; +using System; +using System.Collections.Generic; +using System.Linq; namespace Ryujinx.Ava.Utilities { @@ -32,20 +38,20 @@ namespace Ryujinx.Ava.Utilities spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) ); - private static string BreathOfTheWild_MasterMode(object val) - => val is 1 ? "Playing Master Mode" : "Playing Normal Mode"; + private static string BreathOfTheWild_MasterMode(ref PlayReportValue value) + => value.BoxedValue is 1 ? "Playing Master Mode" : "Playing Normal Mode"; - private static string SuperMarioOdyssey_AssistMode(object val) - => val is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; + private static string SuperMarioOdyssey_AssistMode(ref PlayReportValue value) + => value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; - private static string SuperMarioOdysseyChina_AssistMode(object val) - => val is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; + private static string SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value) + => value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; - private static string SuperMario3DWorldOrBowsersFury(object val) - => val is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; + private static string SuperMario3DWorldOrBowsersFury(ref PlayReportValue value) + => value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; - private static string MarioKart8Deluxe_Mode(object obj) - => obj switch + private static string MarioKart8Deluxe_Mode(ref PlayReportValue value) + => value.BoxedValue switch { // Single Player "Single" => "Single Player", @@ -69,8 +75,97 @@ namespace Ryujinx.Ava.Utilities "Battle" => "Battle Mode", "RaceStart" => "Selecting a Course", "Race" => "Racing", - //TODO: refactor value formatting system to pass in the name from the content archive so this can be localized properly - _ => "Playing Mario Kart 8 Deluxe" + _ => $"Playing {value.Application.Title}" }; } + + #region Analyzer implementation + + public class PlayReportAnalyzer + { + private readonly List _specs = []; + + public PlayReportAnalyzer AddSpec(string titleId, Func transform) + { + _specs.Add(transform(new PlayReportGameSpec { TitleIdStr = titleId })); + return this; + } + + public PlayReportAnalyzer AddSpec(string titleId, Action transform) + { + _specs.Add(new PlayReportGameSpec { TitleIdStr = titleId }.Apply(transform)); + return this; + } + + public Optional Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport) + { + if (!playReport.IsDictionary) + return Optional.None; + + if (!_specs.TryGetFirst(s => s.TitleIdStr.EqualsIgnoreCase(runningGameId), out PlayReportGameSpec spec)) + return Optional.None; + + foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority)) + { + if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) + continue; + + PlayReportValue value = new() + { + Application = appMeta, + BoxedValue = valuePackObject.ToObject() + }; + + return formatSpec.ValueFormatter(ref value); + } + + return Optional.None; + } + + } + + public class PlayReportGameSpec + { + public required string TitleIdStr { get; init; } + public List Analyses { get; } = []; + + public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter) + { + Analyses.Add(new PlayReportValueFormatterSpec + { + Priority = Analyses.Count, + ReportKey = reportKey, + ValueFormatter = valueFormatter + }); + return this; + } + + public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, PlayReportValueFormatter valueFormatter) + { + Analyses.Add(new PlayReportValueFormatterSpec + { + Priority = priority, + ReportKey = reportKey, + ValueFormatter = valueFormatter + }); + return this; + } + } + + public struct PlayReportValue + { + public ApplicationMetadata Application { get; init; } + public object BoxedValue { get; init; } + } + + public struct PlayReportValueFormatterSpec + { + public required int Priority { get; init; } + public required string ReportKey { get; init; } + public required PlayReportValueFormatter ValueFormatter { get; init; } + } + + public delegate string PlayReportValueFormatter(ref PlayReportValue value); + + #endregion } -- 2.47.1 From b2eecd28cea1d50dfc18e2cbfac0db80f4c11678 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 22:10:49 -0600 Subject: [PATCH 014/209] UI: RPC: Value Formatter V3 - Allows the ability to bind a single PlayReportGameSpec to multiple title IDs, like for MK8D - Allows the ability for the value formatters to tell the caller of the analyzer that they should reset the value, and also added the ability to explicitly not handle a value format. --- src/Ryujinx/DiscordIntegrationModule.cs | 18 ++++-- src/Ryujinx/Utilities/PlayReport.cs | 75 +++++++++++++++++-------- 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index 5561c1562..0e1f91869 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -134,13 +134,21 @@ namespace Ryujinx.Ava if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; - Optional details = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport); + PlayReportFormattedValue value = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport); - if (!details.HasValue) return; - - _discordPresencePlaying.Details = details; + if (!value.Handled) return; + + if (value.Reset) + { + _discordPresencePlaying.Details = $"Playing {_currentApp.Title}"; + Logger.Info?.Print(LogClass.UI, "Reset Discord RPC based on a supported play report value formatter."); + } + else + { + _discordPresencePlaying.Details = value.FormattedString; + Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report."); + } UpdatePlayingState(); - Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report."); } private static string TruncateToByteLength(string input) diff --git a/src/Ryujinx/Utilities/PlayReport.cs b/src/Ryujinx/Utilities/PlayReport.cs index 9665a1628..af56bae12 100644 --- a/src/Ryujinx/Utilities/PlayReport.cs +++ b/src/Ryujinx/Utilities/PlayReport.cs @@ -29,28 +29,24 @@ namespace Ryujinx.Ava.Utilities "010028600EBDA000", spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury) ) - .AddSpec( // Mario Kart 8 Deluxe - "0100152000022000", - spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) - ) - .AddSpec( // Mario Kart 8 Deluxe (China) - "010075100E8EC000", + .AddSpec( // Mario Kart 8 Deluxe, Mario Kart 8 Deluxe (China) + ["0100152000022000", "010075100E8EC000"], spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) ); - private static string BreathOfTheWild_MasterMode(ref PlayReportValue value) - => value.BoxedValue is 1 ? "Playing Master Mode" : "Playing Normal Mode"; + private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref PlayReportValue value) + => value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset; - private static string SuperMarioOdyssey_AssistMode(ref PlayReportValue value) + private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(ref PlayReportValue value) => value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; - private static string SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value) + private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value) => value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; - private static string SuperMario3DWorldOrBowsersFury(ref PlayReportValue value) + private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(ref PlayReportValue value) => value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; - private static string MarioKart8Deluxe_Mode(ref PlayReportValue value) + private static PlayReportFormattedValue MarioKart8Deluxe_Mode(ref PlayReportValue value) => value.BoxedValue switch { // Single Player @@ -75,7 +71,7 @@ namespace Ryujinx.Ava.Utilities "Battle" => "Battle Mode", "RaceStart" => "Selecting a Course", "Race" => "Racing", - _ => $"Playing {value.Application.Title}" + _ => PlayReportFormattedValue.ForceReset }; } @@ -87,23 +83,35 @@ namespace Ryujinx.Ava.Utilities public PlayReportAnalyzer AddSpec(string titleId, Func transform) { - _specs.Add(transform(new PlayReportGameSpec { TitleIdStr = titleId })); + _specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] })); return this; } public PlayReportAnalyzer AddSpec(string titleId, Action transform) { - _specs.Add(new PlayReportGameSpec { TitleIdStr = titleId }.Apply(transform)); + _specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform)); + return this; + } + + public PlayReportAnalyzer AddSpec(IEnumerable titleIds, Func transform) + { + _specs.Add(transform(new PlayReportGameSpec { TitleIds = [..titleIds] })); + return this; + } + + public PlayReportAnalyzer AddSpec(IEnumerable titleIds, Action transform) + { + _specs.Add(new PlayReportGameSpec { TitleIds = [..titleIds] }.Apply(transform)); return this; } - public Optional Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport) + public PlayReportFormattedValue Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport) { if (!playReport.IsDictionary) - return Optional.None; + return PlayReportFormattedValue.Unhandled; - if (!_specs.TryGetFirst(s => s.TitleIdStr.EqualsIgnoreCase(runningGameId), out PlayReportGameSpec spec)) - return Optional.None; + if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec)) + return PlayReportFormattedValue.Unhandled; foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority)) { @@ -119,14 +127,14 @@ namespace Ryujinx.Ava.Utilities return formatSpec.ValueFormatter(ref value); } - return Optional.None; + return PlayReportFormattedValue.Unhandled; } } public class PlayReportGameSpec { - public required string TitleIdStr { get; init; } + public required string[] TitleIds { get; init; } public List Analyses { get; } = []; public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter) @@ -158,14 +166,33 @@ namespace Ryujinx.Ava.Utilities public object BoxedValue { get; init; } } + public struct PlayReportFormattedValue + { + public bool Handled { get; private init; } + + public bool Reset { get; private init; } + + public string FormattedString { get; private init; } + + public static implicit operator PlayReportFormattedValue(string formattedValue) + => new() { Handled = true, FormattedString = formattedValue }; + + public static PlayReportFormattedValue Unhandled => default; + public static PlayReportFormattedValue ForceReset => new() { Handled = true, Reset = true }; + + public static PlayReportValueFormatter AlwaysResets = AlwaysResetsImpl; + + private static PlayReportFormattedValue AlwaysResetsImpl(ref PlayReportValue _) => ForceReset; + } + public struct PlayReportValueFormatterSpec { public required int Priority { get; init; } public required string ReportKey { get; init; } - public required PlayReportValueFormatter ValueFormatter { get; init; } + public PlayReportValueFormatter ValueFormatter { get; init; } } - public delegate string PlayReportValueFormatter(ref PlayReportValue value); - + public delegate PlayReportFormattedValue PlayReportValueFormatter(ref PlayReportValue value); + #endregion } -- 2.47.1 From 55536f5d7815e82aa2032bf63878497d87c7e70c Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 22:14:43 -0600 Subject: [PATCH 015/209] misc: chore: Early exit HandlePlayReport if RPC is not enabled --- src/Ryujinx/DiscordIntegrationModule.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index 0e1f91869..f55eb8d66 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -131,6 +131,7 @@ namespace Ryujinx.Ava private static void HandlePlayReport(MessagePackObject playReport) { + if (_discordClient is null) return; if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; -- 2.47.1 From 774edb7b29bf8ba44b0cea331977b37017ac6499 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 23:46:55 -0600 Subject: [PATCH 016/209] UI: Match System Time is now an active setting which you can toggle on/off. --- src/Ryujinx/AppHost.cs | 4 +- src/Ryujinx/Assets/locales.json | 40 +++++++++---------- .../UI/ViewModels/SettingsViewModel.cs | 24 ++++------- .../UI/Views/Settings/SettingsCPUView.axaml | 3 +- .../Views/Settings/SettingsGraphicsView.axaml | 3 +- .../Views/Settings/SettingsSystemView.axaml | 21 ++++++---- .../Settings/SettingsSystemView.axaml.cs | 2 - src/Ryujinx/UI/Windows/SettingsWindow.axaml | 3 +- .../Configuration/ConfigurationFileFormat.cs | 7 +++- .../ConfigurationState.Migration.cs | 3 +- .../Configuration/ConfigurationState.Model.cs | 7 ++++ 11 files changed, 64 insertions(+), 53 deletions(-) diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index d12795963..25f451858 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -938,7 +938,9 @@ namespace Ryujinx.Ava ConfigurationState.Instance.System.EnableInternetAccess, ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None, ConfigurationState.Instance.System.FsGlobalAccessLogMode, - ConfigurationState.Instance.System.SystemTimeOffset, + ConfigurationState.Instance.System.MatchSystemTime + ? 0 + : ConfigurationState.Instance.System.SystemTimeOffset, ConfigurationState.Instance.System.TimeZone, ConfigurationState.Instance.System.MemoryManagerMode, ConfigurationState.Instance.System.IgnoreMissingServices, diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index ec8a2aaac..3db41b963 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -4153,23 +4153,23 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Resync to PC Date & Time", + "en_US": "Match System Time", "es_ES": "", - "fr_FR": "Resynchronier la Date à celle du PC", + "fr_FR": "", "he_IL": "", - "it_IT": "Sincronizza data e ora con il PC", + "it_IT": "", "ja_JP": "", - "ko_KR": "PC 날짜와 시간에 동기화", - "no_NO": "Resynkroniser til PC-dato og -klokkeslett", + "ko_KR": "", + "no_NO": "", "pl_PL": "", "pt_BR": "", - "ru_RU": "Повторная синхронизация с датой и временем на компьютере", - "sv_SE": "Återsynka till datorns datum och tid", + "ru_RU": "", + "sv_SE": "", "th_TH": "", "tr_TR": "", - "uk_UA": "Синхронізувати з датою та часом ПК", - "zh_CN": "与 PC 日期和时间重新同步", - "zh_TW": "重新同步至 PC 的日期和時間" + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" } }, { @@ -15553,23 +15553,23 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Resync System Time to match your PC's current date & time.\n\nThis is not an active setting, it can still fall out of sync; in which case just click this button again.", + "en_US": "Sync System Time to match your PC's current date & time.", "es_ES": "", - "fr_FR": "Resynchronise la Date du Système pour qu'elle soit la même que celle du PC.\n\nCeci n'est pas un paramètrage automatique, la date peut se désynchroniser; dans ce cas là, rappuyer sur le boutton.", + "fr_FR": "Resynchronise la Date du Système pour qu'elle soit la même que celle du PC.", "he_IL": "", - "it_IT": "Sincronizza data e ora del sistema con quelle del PC.\n\nQuesta non è un'opzione attiva, perciò data e ora potrebbero tornare a non essere sincronizzate: in tal caso basterà cliccare nuovamente questo pulsante.", + "it_IT": "Sincronizza data e ora del sistema con quelle del PC.", "ja_JP": "", - "ko_KR": "시스템 시간을 PC의 현재 날짜 및 시간과 일치하도록 다시 동기화합니다.\n\n이 설정은 활성 설정이 아니므로 여전히 동기화되지 않을 수 있으며, 이 경우 이 버튼을 다시 클릭하면 됩니다.", - "no_NO": "Resynkroniser systemtiden slik at den samsvarer med PC-ens gjeldende dato og klokkeslett. \\Dette er ikke en aktiv innstilling, men den kan likevel komme ut av synkronisering; i så fall er det bare å klikke på denne knappen igjen.", + "ko_KR": "시스템 시간을 PC의 현재 날짜 및 시간과 일치하도록 다시 동기화합니다.", + "no_NO": "Resynkroniser systemtiden slik at den samsvarer med PC-ens gjeldende dato og klokkeslett.", "pl_PL": "", "pt_BR": "", - "ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.\n\nЭто не активная настройка, она все еще может рассинхронизироваться; в этом случае просто нажмите эту кнопку еще раз.", - "sv_SE": "Återsynkronisera systemtiden för att matcha din dators aktuella datum och tid.\n\nDetta är inte en aktiv inställning och den kan tappa synken och om det händer så kan du klicka på denna knapp igen.", + "ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.", + "sv_SE": "Återsynkronisera systemtiden för att matcha din dators aktuella datum och tid.", "th_TH": "", "tr_TR": "", - "uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.\n\nЦе не активне налаштування, тому синхронізація може збитися; у такому разі просто натискайте цю кнопку знову.", - "zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。\n\n这个操作不会实时同步系统时间与电脑时间,时间仍然可能不同步;在这种情况下,只需再次单击此按钮即可。", - "zh_TW": "重新同步系統韌體時間至 PC 目前的日期和時間。\n\n這不是一個主動設定,它仍然可能會失去同步;在這種情況下,只需再次點擊此按鈕。" + "uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.", + "zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。", + "zh_TW": "重新同步系統韌體時間至 PC 目前的日期和時間。" } }, { diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 488828482..d54313e76 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -116,10 +116,6 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsOpenGLAvailable => !OperatingSystem.IsMacOS(); - public bool IsAppleSiliconMac => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64; - - public bool IsMacOS => OperatingSystem.IsMacOS(); - public bool EnableDiscordIntegration { get; set; } public bool CheckUpdatesOnStart { get; set; } public bool ShowConfirmExit { get; set; } @@ -201,7 +197,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool EnableTextureRecompression { get; set; } public bool EnableMacroHLE { get; set; } public bool EnableColorSpacePassthrough { get; set; } - public bool ColorSpacePassthroughAvailable => IsMacOS; + public bool ColorSpacePassthroughAvailable => RunningPlatform.IsMacOS; public bool EnableFileLog { get; set; } public bool EnableStub { get; set; } public bool EnableInfo { get; set; } @@ -297,6 +293,8 @@ namespace Ryujinx.Ava.UI.ViewModels } } + [ObservableProperty] private bool _matchSystemTime; + public DateTimeOffset CurrentDate { get; set; } public TimeSpan CurrentTime { get; set; } @@ -412,17 +410,6 @@ namespace Ryujinx.Ava.UI.ViewModels Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(PreferredGpuIndex))); } - public void MatchSystemTime() - { - (DateTimeOffset dto, TimeSpan timeOfDay) = DateTimeOffset.Now.Extract(); - - CurrentDate = dto; - CurrentTime = timeOfDay; - - OnPropertyChanged(nameof(CurrentDate)); - OnPropertyChanged(nameof(CurrentTime)); - } - public async Task LoadTimeZones() { _timeZoneContentManager = new TimeZoneContentManager(); @@ -524,7 +511,9 @@ namespace Ryujinx.Ava.UI.ViewModels CurrentDate = currentDateTime.Date; CurrentTime = currentDateTime.TimeOfDay; - EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval.Value; + MatchSystemTime = config.System.MatchSystemTime; + + EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval; CustomVSyncInterval = config.Graphics.CustomVSyncInterval; VSyncMode = config.Graphics.VSyncMode; EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; @@ -629,6 +618,7 @@ namespace Ryujinx.Ava.UI.ViewModels config.System.TimeZone.Value = TimeZone; } + config.System.MatchSystemTime.Value = MatchSystemTime; config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds()); config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; config.System.DramSize.Value = DramSize; diff --git a/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml index 83f908a9c..62f087510 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml @@ -6,6 +6,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common" mc:Ignorable="d" x:DataType="viewModels:SettingsViewModel"> @@ -69,7 +70,7 @@ diff --git a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml index a2559f393..42515a4e9 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml @@ -8,6 +8,7 @@ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common" Design.Width="1000" mc:Ignorable="d" x:DataType="viewModels:SettingsViewModel"> @@ -48,7 +49,7 @@ - + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml index d05f3b7bf..aa7144cf1 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml @@ -170,7 +170,8 @@ ToolTip.Tip="{ext:Locale TimeTooltip}" Width="250"/> @@ -181,17 +182,21 @@ - + Text="{ext:Locale SettingsTabSystemSystemTimeMatch}" + ToolTip.Tip="{ext:Locale MatchTimeTooltip}" + Width="250"/> + ViewModel.MatchSystemTime(); } } diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml b/src/Ryujinx/UI/Windows/SettingsWindow.axaml index 59302b6fc..7abf044ee 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml @@ -10,6 +10,7 @@ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:settings="clr-namespace:Ryujinx.Ava.UI.Views.Settings" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common" Width="1100" Height="768" MinWidth="800" @@ -113,7 +114,7 @@ Spacing="10" Orientation="Horizontal" HorizontalAlignment="Right" - ReverseOrder="{Binding IsMacOS}"> + ReverseOrder="{x:Static helper:RunningPlatform.IsMacOS}"> appData = + ApplicationLibrary.Applications.Lookup(SelectedApplication.Id); + + return appData.HasValue && appData.Value.HasPlayabilityInfo; + } + } + public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0; public bool OpenDeviceSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0; diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index f6c43aade..a0bcd1aa2 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -50,7 +50,7 @@ namespace Ryujinx.Ava.UI.Views.Main UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes); XciTrimmerMenuItem.Command = Commands.Create(XCITrimmerWindow.Show); AboutWindowMenuItem.Command = Commands.Create(AboutWindow.Show); - CompatibilityListMenuItem.Command = Commands.Create(CompatibilityList.Show); + CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityList.Show()); UpdateMenuItem.Command = Commands.Create(async () => { diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs index dec265623..ee86a4a33 100644 --- a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs @@ -135,6 +135,14 @@ namespace Ryujinx.Ava.Utilities.AppLibrary return id.ToString("X16"); } + public bool FindApplication(ulong id, out ApplicationData foundData) + { + DynamicData.Kernel.Optional appData = Applications.Lookup(id); + foundData = appData.HasValue ? appData.Value : null; + + return appData.HasValue; + } + /// The configured key set is missing a key. /// The NCA header could not be decrypted. /// The NCA version is not supported. diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs b/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs index d0e251fe0..c3fcf99ca 100644 --- a/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs +++ b/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs @@ -113,20 +113,17 @@ namespace Ryujinx.Ava.Utilities.Compat .Select(FormatLabelName) .JoinToString(", "); - public override string ToString() - { - StringBuilder sb = new("CompatibilityEntry: {"); - sb.Append($"{nameof(GameName)}=\"{GameName}\", "); - sb.Append($"{nameof(TitleId)}={TitleId}, "); - sb.Append($"{nameof(Labels)}={ - Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]") - }, "); - sb.Append($"{nameof(Status)}=\"{Status}\", "); - sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\""); - sb.Append('}'); - - return sb.ToString(); - } + public override string ToString() => + new StringBuilder("CompatibilityEntry: {") + .Append($"{nameof(GameName)}=\"{GameName}\", ") + .Append($"{nameof(TitleId)}={TitleId}, ") + .Append($"{nameof(Labels)}={ + Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]") + }, ") + .Append($"{nameof(Status)}=\"{Status}\", ") + .Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"") + .Append('}') + .ToString(); public static string FormatLabelName(string labelName) => labelName.ToLower() switch { diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml index 73ec84c53..132b10e26 100644 --- a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml +++ b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml @@ -34,7 +34,7 @@ Text="{ext:Locale CompatibilityListWarning}" /> - + diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs index e0d3b0c56..30d2649bc 100644 --- a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs +++ b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs @@ -9,7 +9,7 @@ namespace Ryujinx.Ava.Utilities.Compat { public partial class CompatibilityList : UserControl { - public static async Task Show() + public static async Task Show(string titleId = null) { ContentDialog contentDialog = new() { @@ -18,7 +18,10 @@ namespace Ryujinx.Ava.Utilities.Compat CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose], Content = new CompatibilityList { - DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary) + DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary), + SearchBox = { + Text = titleId ?? "" + } } }; -- 2.47.1 From fafb99c702a83a294838359535100f5883b86822 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 15:57:32 -0600 Subject: [PATCH 034/209] misc: chore: [ci skip] don't even bother looking up the application; the tag present on the control *is* a valid title ID and can't reasonably change in between the tag being set and playability information being requested. Even if it does, worst case scenario the compat list that pops up has no results. --- src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs index 95fc911d0..7c6b0cf15 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs @@ -39,13 +39,7 @@ namespace Ryujinx.Ava.UI.Controls if (sender is not Button { Content: TextBlock playabilityLabel }) return; - if (!ulong.TryParse((string)playabilityLabel.Tag, NumberStyles.HexNumber, null, out ulong titleId)) - return; - - if (!mwvm.ApplicationLibrary.FindApplication(titleId, out ApplicationData appData)) - return; - - await CompatibilityList.Show(appData.IdString); + await CompatibilityList.Show((string)playabilityLabel.Tag); } private async void IdString_OnClick(object sender, RoutedEventArgs e) -- 2.47.1 From e8a7d5b0b74d1d5ad2ba09cc7c07f6ba333972d3 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 17:21:54 -0600 Subject: [PATCH 035/209] UI: Only show DLC RomFS button under Extract Data when DLCs are available. Also convert the constructor of DlcSelectViewModel to expect a normal title id and not one already converted to the base ID. --- .../UI/Controls/ApplicationContextMenu.axaml | 1 + .../Controls/ApplicationContextMenu.axaml.cs | 2 +- .../UI/ViewModels/DlcSelectViewModel.cs | 4 +--- .../UI/ViewModels/MainWindowViewModel.cs | 2 ++ .../Utilities/AppLibrary/ApplicationLibrary.cs | 18 ++++++++++++++++++ 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 797bc27e0..2804485fe 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -117,6 +117,7 @@ Header="{ext:Locale GameListContextMenuExtractDataRomFS}" ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" /> diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs index f29f70432..0d81484ba 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs @@ -334,7 +334,7 @@ namespace Ryujinx.Ava.UI.Controls if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) return; - DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.IdBase, viewModel.ApplicationLibrary); + DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.Id, viewModel.ApplicationLibrary); if (selectedDlc is not null) { diff --git a/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs b/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs index d50d8249a..b486aa766 100644 --- a/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs @@ -14,9 +14,7 @@ namespace Ryujinx.Ava.UI.ViewModels public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary) { - _dlcs = appLibrary.DownloadableContents.Items - .Where(x => x.Dlc.TitleIdBase == titleId) - .Select(x => x.Dlc) + _dlcs = appLibrary.FindDlcsFor(titleId) .OrderBy(it => it.IsBundled ? 0 : 1) .ThenBy(it => it.TitleId) .ToArray(); diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index f0e05d517..632e3b4f0 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -360,6 +360,8 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id); + public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0; public bool OpenDeviceSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0; diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs index ee86a4a33..75737c3e5 100644 --- a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs @@ -142,6 +142,24 @@ namespace Ryujinx.Ava.Utilities.AppLibrary return appData.HasValue; } + + public bool FindUpdate(ulong id, out TitleUpdateModel foundData) + { + Gommon.Optional appData = + TitleUpdates.Keys.FindFirst(x => x.TitleId == id); + foundData = appData.HasValue ? appData.Value : null; + + return appData.HasValue; + } + + public TitleUpdateModel[] FindUpdatesFor(ulong id) + => TitleUpdates.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray(); + + public DownloadableContentModel[] FindDlcsFor(ulong id) + => DownloadableContents.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray(); + + public bool HasDlcs(ulong id) + => DownloadableContents.Keys.Any(x => x.TitleIdBase == (id & ~0x1FFFUL)); /// The configured key set is missing a key. /// The NCA header could not be decrypted. -- 2.47.1 From 820e8f73750b7348f73b69f38b033ccb8d87adff Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 18:10:28 -0600 Subject: [PATCH 036/209] [ci skip] UI: Strip dumped file information out of the DLC name --- .../Utilities/AppLibrary/ApplicationLibrary.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs index 75737c3e5..9571394fe 100644 --- a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs @@ -128,11 +128,16 @@ namespace Ryujinx.Ava.Utilities.AppLibrary DynamicData.Kernel.Optional appData = Applications.Lookup(id); if (appData.HasValue) return appData.Value.Name; - - if (DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData)) - return Path.GetFileNameWithoutExtension(dlcData.FileName); - return id.ToString("X16"); + if (!DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData)) + return id.ToString("X16"); + + string name = Path.GetFileNameWithoutExtension(dlcData.FileName)!; + int idx = name.IndexOf('['); + if (idx != -1) + name = name[..idx]; + + return name; } public bool FindApplication(ulong id, out ApplicationData foundData) -- 2.47.1 From b0fcc5bee1674c075c125ccb65773db8b3c466fe Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 18:21:24 -0600 Subject: [PATCH 037/209] misc: chore: Simplify HasCompatibilityEntry (Totally didn't realize that SelectedApplication is already an ApplicationData) --- src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 632e3b4f0..d7a09a0e3 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -349,16 +349,7 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public bool HasCompatibilityEntry - { - get - { - DynamicData.Kernel.Optional appData = - ApplicationLibrary.Applications.Lookup(SelectedApplication.Id); - - return appData.HasValue && appData.Value.HasPlayabilityInfo; - } - } + public bool HasCompatibilityEntry => SelectedApplication.HasPlayabilityInfo; public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id); -- 2.47.1 From 222ceb818b9f5c49f854762a9b544b349a9a6ea0 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 18:21:49 -0600 Subject: [PATCH 038/209] misc: chore: Use ApplicationLibrary helpers for getting DLCs & Updates for a game --- .../UI/ViewModels/DownloadableContentManagerViewModel.cs | 3 +-- src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs | 3 +-- src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs | 6 ++++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs index 1533b7d5d..a16a06ff5 100644 --- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -69,8 +69,7 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadDownloadableContents() { - IEnumerable<(DownloadableContentModel Dlc, bool IsEnabled)> dlcs = _applicationLibrary.DownloadableContents.Items - .Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase); + (DownloadableContentModel Dlc, bool IsEnabled)[] dlcs = _applicationLibrary.FindDlcConfigurationFor(_applicationData.Id); bool hasBundledContent = false; foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs) diff --git a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs index aaafc3913..2b88aceed 100644 --- a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs @@ -41,8 +41,7 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadUpdates() { - IEnumerable<(TitleUpdateModel TitleUpdate, bool IsSelected)> updates = ApplicationLibrary.TitleUpdates.Items - .Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase); + (TitleUpdateModel TitleUpdate, bool IsSelected)[] updates = ApplicationLibrary.FindUpdateConfigurationFor(ApplicationData.Id); bool hasBundledContent = false; SelectedUpdate = new TitleUpdateViewModelNoUpdate(); diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs index 9571394fe..79cac1a0e 100644 --- a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs @@ -160,9 +160,15 @@ namespace Ryujinx.Ava.Utilities.AppLibrary public TitleUpdateModel[] FindUpdatesFor(ulong id) => TitleUpdates.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray(); + public (TitleUpdateModel TitleUpdate, bool IsSelected)[] FindUpdateConfigurationFor(ulong id) + => TitleUpdates.Items.Where(x => x.TitleUpdate.TitleIdBase == (id & ~0x1FFFUL)).ToArray(); + public DownloadableContentModel[] FindDlcsFor(ulong id) => DownloadableContents.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray(); + public (DownloadableContentModel Dlc, bool IsEnabled)[] FindDlcConfigurationFor(ulong id) + => DownloadableContents.Items.Where(x => x.Dlc.TitleIdBase == (id & ~0x1FFFUL)).ToArray(); + public bool HasDlcs(ulong id) => DownloadableContents.Keys.Any(x => x.TitleIdBase == (id & ~0x1FFFUL)); -- 2.47.1 From 1972a47f39014e8df9bcfcf3a61ca7d27eaaa030 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 19:32:17 -0600 Subject: [PATCH 039/209] UI: Game stats button on right click for Grid view users --- src/Ryujinx/Assets/locales.json | 50 ++++++++ src/Ryujinx/RyujinxApp.axaml.cs | 3 + .../UI/Controls/ApplicationContextMenu.axaml | 6 + .../Controls/ApplicationContextMenu.axaml.cs | 12 +- .../UI/Controls/ApplicationDataView.axaml | 116 ++++++++++++++++++ .../UI/Controls/ApplicationDataView.axaml.cs | 84 +++++++++++++ 6 files changed, 266 insertions(+), 5 deletions(-) create mode 100644 src/Ryujinx/UI/Controls/ApplicationDataView.axaml create mode 100644 src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 3da0b1728..9f9053ae0 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -2572,6 +2572,56 @@ "zh_TW": "" } }, + { + "ID": "GameListContextMenuShowGameData", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Show Game Stats", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameListContextMenuShowGameDataToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Show the other various information about the currently selected game that is missing from the Grid view layout.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "GameListContextMenuOpenModsDirectory", "Translations": { diff --git a/src/Ryujinx/RyujinxApp.axaml.cs b/src/Ryujinx/RyujinxApp.axaml.cs index be24315f6..32318776a 100644 --- a/src/Ryujinx/RyujinxApp.axaml.cs +++ b/src/Ryujinx/RyujinxApp.axaml.cs @@ -32,6 +32,9 @@ namespace Ryujinx.Ava public static MainWindow MainWindow => Current! .ApplicationLifetime.Cast() .MainWindow.Cast(); + + public static IClassicDesktopStyleApplicationLifetime AppLifetime => Current! + .ApplicationLifetime.Cast(); public static bool IsClipboardAvailable(out IClipboard clipboard) { diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 2804485fe..acade1df9 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -25,6 +25,12 @@ Header="{ext:Locale GameListContextMenuShowCompatEntry}" Icon="{ext:Icon mdi-gamepad}" ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs new file mode 100644 index 000000000..0bd22a243 --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs @@ -0,0 +1,84 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input.Platform; +using Avalonia.Interactivity; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Ava.Utilities.AppLibrary; +using Ryujinx.Ava.Utilities.Compat; +using System.Linq; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class ApplicationDataView : UserControl + { + public static async Task Show(ApplicationData appData) + { + ContentDialog contentDialog = new() + { + PrimaryButtonText = string.Empty, + SecondaryButtonText = string.Empty, + CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose], + Content = new ApplicationDataView { DataContext = appData } + }; + + Style closeButton = new(x => x.Name("CloseButton")); + closeButton.Setters.Add(new Setter(WidthProperty, 160d)); + + Style closeButtonParent = new(x => x.Name("CommandSpace")); + closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty, + Avalonia.Layout.HorizontalAlignment.Center)); + + contentDialog.Styles.Add(closeButton); + contentDialog.Styles.Add(closeButtonParent); + + await ContentDialogHelper.ShowAsync(contentDialog); + } + + public ApplicationDataView() + { + InitializeComponent(); + } + + private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e) + { + if (sender is not Button { Content: TextBlock playabilityLabel }) + return; + + if (RyujinxApp.AppLifetime.Windows.TryGetFirst(x => x is ContentDialogOverlayWindow, out Window window)) + window.Close(ContentDialogResult.None); + + await CompatibilityList.Show((string)playabilityLabel.Tag); + } + + private async void IdString_OnClick(object sender, RoutedEventArgs e) + { + if (DataContext is not MainWindowViewModel mwvm) + return; + + if (sender is not Button { Content: TextBlock idText }) + return; + + if (!RyujinxApp.IsClipboardAvailable(out IClipboard clipboard)) + return; + + ApplicationData appData = mwvm.Applications.FirstOrDefault(it => it.IdString == idText.Text); + if (appData is null) + return; + + await clipboard.SetTextAsync(appData.IdString); + + NotificationHelper.ShowInformation( + "Copied Title ID", + $"{appData.Name} ({appData.IdString})"); + } + } +} + -- 2.47.1 From bd08a111a8829a3e958f606890b2ad4b4642fc6b Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 22:47:12 -0600 Subject: [PATCH 040/209] UI: Show what each value is in the Game Info dialog, add game icon --- src/Ryujinx/Assets/locales.json | 6 +- .../UI/Controls/ApplicationContextMenu.axaml | 1 - .../UI/Controls/ApplicationDataView.axaml | 212 +++++++++--------- .../UI/Controls/ApplicationDataView.axaml.cs | 3 +- .../UI/Controls/ApplicationListView.axaml | 1 + .../Converters/MultiplayerInfoConverter.cs | 7 +- .../UI/ViewModels/ApplicationDataViewModel.cs | 34 +++ .../Utilities/AppLibrary/ApplicationData.cs | 3 + 8 files changed, 155 insertions(+), 112 deletions(-) create mode 100644 src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 9f9053ae0..d597e8a4c 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -2578,7 +2578,7 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Show Game Stats", + "en_US": "Show Game Info", "es_ES": "", "fr_FR": "", "he_IL": "", @@ -2603,7 +2603,7 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Show the other various information about the currently selected game that is missing from the Grid view layout.", + "en_US": "Show stats & details about the currently selected game.", "es_ES": "", "fr_FR": "", "he_IL": "", @@ -23298,4 +23298,4 @@ } } ] -} \ No newline at end of file +} diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index acade1df9..3e47a1910 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -26,7 +26,6 @@ Icon="{ext:Icon mdi-gamepad}" ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/> - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs index 0bd22a243..cc8091d4d 100644 --- a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs @@ -26,7 +26,8 @@ namespace Ryujinx.Ava.UI.Controls PrimaryButtonText = string.Empty, SecondaryButtonText = string.Empty, CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose], - Content = new ApplicationDataView { DataContext = appData } + MinWidth = 256, + Content = new ApplicationDataView { DataContext = new ApplicationDataViewModel(appData) } }; Style closeButton = new(x => x.Name("CloseButton")); diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml index 151bf5b32..c01c4e8be 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml @@ -140,6 +140,7 @@ TextWrapping="Wrap" /> diff --git a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs index 47d0b94d0..dc36098a1 100644 --- a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs +++ b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs @@ -12,12 +12,9 @@ namespace Ryujinx.Ava.UI.Helpers public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value is ApplicationData applicationData) + if (value is ApplicationData { HasLdnGames: true } applicationData) { - if (applicationData.PlayerCount != 0 && applicationData.GameCount != 0) - { - return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}"; - } + return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}"; } return ""; diff --git a/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs new file mode 100644 index 000000000..73d555389 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs @@ -0,0 +1,34 @@ +using Gommon; +using Ryujinx.Ava.Utilities.AppLibrary; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class ApplicationDataViewModel : BaseModel + { + private const string FormatVersion = "Current Version: {0}"; + private const string FormatDeveloper = "Developed by {0}"; + + private const string FormatExtension = "Game type: {0}"; + private const string FormatLastPlayed = "Last played: {0}"; + private const string FormatPlayTime = "Play time: {0}"; + private const string FormatSize = "Size: {0}"; + + private const string FormatHostedGames = "Hosted Games: {0}"; + private const string FormatPlayerCount = "Online Players: {0}"; + + public ApplicationData AppData { get; } + + public ApplicationDataViewModel(ApplicationData appData) => AppData = appData; + + public string FormattedVersion => FormatVersion.Format(AppData.Version); + public string FormattedDeveloper => FormatDeveloper.Format(AppData.Developer); + + public string FormattedFileExtension => FormatExtension.Format(AppData.FileExtension); + public string FormattedLastPlayed => FormatLastPlayed.Format(AppData.LastPlayedString); + public string FormattedPlayTime => FormatPlayTime.Format(AppData.TimePlayedString); + public string FormattedFileSize => FormatSize.Format(AppData.FileSizeString); + + public string FormattedLdnInfo => + $"{FormatHostedGames.Format(AppData.GameCount)}\n{FormatPlayerCount.Format(AppData.PlayerCount)}"; + } +} diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs index aef54819e..48e30e663 100644 --- a/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs +++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs @@ -49,6 +49,9 @@ namespace Ryujinx.Ava.Utilities.AppLibrary public int PlayerCount { get; set; } public int GameCount { get; set; } + + public bool HasLdnGames => PlayerCount != 0 && GameCount != 0; + public TimeSpan TimePlayed { get; set; } public DateTime? LastPlayed { get; set; } public string FileExtension { get; set; } -- 2.47.1 From 717851985e352b87934ab74739d601ea30c34758 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 23:28:37 -0600 Subject: [PATCH 041/209] UI: Reorganize Game Info dialog popup + localization --- src/Ryujinx/Assets/locales.json | 336 ++++++++++++------ .../UI/Controls/ApplicationDataView.axaml | 186 +++++----- .../UI/Controls/ApplicationDataView.axaml.cs | 1 + .../UI/ViewModels/ApplicationDataViewModel.cs | 28 +- 4 files changed, 317 insertions(+), 234 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index d597e8a4c..cb222e935 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -1525,151 +1525,151 @@ { "ID": "GameListHeaderDeveloper", "Translations": { - "ar_SA": "المطور", - "de_DE": "Entwickler", - "el_GR": "Προγραμματιστής", - "en_US": "Developer", - "es_ES": "Desarrollador", - "fr_FR": "Développeur", - "he_IL": "מפתח", - "it_IT": "Sviluppatore", - "ja_JP": "開発元", - "ko_KR": "개발자", - "no_NO": "Utvikler", - "pl_PL": "Twórca", - "pt_BR": "Desenvolvedor", - "ru_RU": "Разработчик", - "sv_SE": "Utvecklare", - "th_TH": "ผู้พัฒนา", - "tr_TR": "Geliştirici", - "uk_UA": "Розробник", - "zh_CN": "制作商", - "zh_TW": "開發者" + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Developed by {0}", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" } }, { "ID": "GameListHeaderVersion", "Translations": { - "ar_SA": "الإصدار", + "ar_SA": "", "de_DE": "", - "el_GR": "Έκδοση", - "en_US": "Version", - "es_ES": "Versión", + "el_GR": "Έκδοση: {0}", + "en_US": "Version: {0}", + "es_ES": "Versión: {0}", "fr_FR": "", - "he_IL": "גרסה", - "it_IT": "Versione", - "ja_JP": "バージョン", - "ko_KR": "버전", - "no_NO": "Versjon", - "pl_PL": "Wersja", - "pt_BR": "Versão", - "ru_RU": "Версия", + "he_IL": "", + "it_IT": "Versione: {0}", + "ja_JP": "バージョン: {0}", + "ko_KR": "버전: {0}", + "no_NO": "Versjon: {0}", + "pl_PL": "Wersja: {0}", + "pt_BR": "Versão: {0}", + "ru_RU": "Версия: {0}", "sv_SE": "", - "th_TH": "เวอร์ชั่น", - "tr_TR": "Sürüm", - "uk_UA": "Версія", - "zh_CN": "版本", - "zh_TW": "版本" + "th_TH": "เวอร์ชั่น: {0}", + "tr_TR": "Sürüm: {0}", + "uk_UA": "Версія: {0}", + "zh_CN": "版本: {0}", + "zh_TW": "版本: {0}" } }, { "ID": "GameListHeaderTimePlayed", "Translations": { - "ar_SA": "وقت اللعب", - "de_DE": "Spielzeit", - "el_GR": "Χρόνος", - "en_US": "Play Time", - "es_ES": "Tiempo jugado", - "fr_FR": "Temps de jeu", - "he_IL": "זמן משחק", - "it_IT": "Tempo di gioco", - "ja_JP": "プレイ時間", - "ko_KR": "플레이 타임", - "no_NO": "Spilletid", - "pl_PL": "Czas w grze:", - "pt_BR": "Tempo de jogo", - "ru_RU": "Время в игре", - "sv_SE": "Speltid", - "th_TH": "เล่นไปแล้ว", - "tr_TR": "Oynama Süresi", - "uk_UA": "Зіграно часу", - "zh_CN": "游玩时长", - "zh_TW": "遊玩時數" + "ar_SA": "", + "de_DE": "Spielzeit: {0}", + "el_GR": "Χρόνος: {0}", + "en_US": "Play Time: {0}", + "es_ES": "Tiempo jugado: {0}", + "fr_FR": "Temps de jeu: {0}", + "he_IL": "", + "it_IT": "Tempo di gioco: {0}", + "ja_JP": "プレイ時間: {0}", + "ko_KR": "플레이 타임: {0}", + "no_NO": "Spilletid: {0}", + "pl_PL": "Czas w grze: {0}", + "pt_BR": "Tempo de jogo: {0}", + "ru_RU": "Время в игре: {0}", + "sv_SE": "Speltid: {0}", + "th_TH": "เล่นไปแล้ว: {0}", + "tr_TR": "Oynama Süresi: {0}", + "uk_UA": "Зіграно часу: {0}", + "zh_CN": "游玩时长: {0}", + "zh_TW": "遊玩時數: {0}" } }, { "ID": "GameListHeaderLastPlayed", "Translations": { - "ar_SA": "آخر مرة لُعبت", - "de_DE": "Zuletzt gespielt", - "el_GR": "Παίχτηκε", - "en_US": "Last Played", - "es_ES": "Jugado por última vez", - "fr_FR": "Dernière partie jouée", - "he_IL": "שוחק לאחרונה", - "it_IT": "Ultima partita", - "ja_JP": "最終プレイ日時", - "ko_KR": "마지막 플레이", - "no_NO": "Sist Spilt", - "pl_PL": "Ostatnio grane", - "pt_BR": "Último jogo", - "ru_RU": "Последний запуск", - "sv_SE": "Senast spelad", - "th_TH": "เล่นล่าสุด", - "tr_TR": "Son Oynama Tarihi", - "uk_UA": "Востаннє зіграно", - "zh_CN": "最近游玩", - "zh_TW": "最近遊玩" + "ar_SA": "", + "de_DE": "Zuletzt gespielt: {0}", + "el_GR": "Παίχτηκε: {0}", + "en_US": "Last Played: {0}", + "es_ES": "Jugado por última vez: {0}", + "fr_FR": "Dernière partie jouée: {0}", + "he_IL": "", + "it_IT": "Ultima partita: {0}", + "ja_JP": "最終プレイ日時: {0}", + "ko_KR": "마지막 플레이: {0}", + "no_NO": "Sist Spilt: {0}", + "pl_PL": "Ostatnio grane: {0}", + "pt_BR": "Último jogo: {0}", + "ru_RU": "Последний запуск: {0}", + "sv_SE": "Senast spelad: {0}", + "th_TH": "เล่นล่าสุด: {0}", + "tr_TR": "Son Oynama Tarihi: {0}", + "uk_UA": "Востаннє зіграно: {0}", + "zh_CN": "最近游玩: {0}", + "zh_TW": "最近遊玩: {0}" } }, { "ID": "GameListHeaderFileExtension", "Translations": { - "ar_SA": "صيغة الملف", - "de_DE": "Dateiformat", - "el_GR": "Κατάληξη", - "en_US": "File Ext", - "es_ES": "Extensión", - "fr_FR": "Extension du Fichier", - "he_IL": "סיומת קובץ", - "it_IT": "Estensione", - "ja_JP": "ファイル拡張子", - "ko_KR": "파일 확장자", - "no_NO": "Fil Eks.", - "pl_PL": "Rozszerzenie pliku", - "pt_BR": "Extensão", - "ru_RU": "Расширение файла", - "sv_SE": "Filänd", - "th_TH": "นามสกุลไฟล์", - "tr_TR": "Dosya Uzantısı", - "uk_UA": "Розширення файлу", - "zh_CN": "扩展名", - "zh_TW": "副檔名" + "ar_SA": "", + "de_DE": "Dateiformat: {0}", + "el_GR": "Κατάληξη: {0}", + "en_US": "Extension: {0}", + "es_ES": "Extensión: {0}", + "fr_FR": "Extension du Fichier: {0}", + "he_IL": "", + "it_IT": "Estensione: {0}", + "ja_JP": "ファイル拡張子: {0}", + "ko_KR": "파일 확장자: {0}", + "no_NO": "Fil Eks.: {0}", + "pl_PL": "Rozszerzenie pliku: {0}", + "pt_BR": "Extensão: {0}", + "ru_RU": "Расширение файла: {0}", + "sv_SE": "Filänd: {0}", + "th_TH": "นามสกุลไฟล์: {0}", + "tr_TR": "Dosya Uzantısı: {0}", + "uk_UA": "Розширення файлу: {0}", + "zh_CN": "扩展名: {0}", + "zh_TW": "副檔名: {0}" } }, { "ID": "GameListHeaderFileSize", "Translations": { - "ar_SA": "حجم الملف", - "de_DE": "Dateigröße", - "el_GR": "Μέγεθος Αρχείου", - "en_US": "File Size", - "es_ES": "Tamaño del archivo", - "fr_FR": "Taille du Fichier", - "he_IL": "גודל הקובץ", - "it_IT": "Dimensione file", - "ja_JP": "ファイルサイズ", - "ko_KR": "파일 크기", - "no_NO": "Fil Størrelse", - "pl_PL": "Rozmiar pliku", - "pt_BR": "Tamanho", - "ru_RU": "Размер файла", - "sv_SE": "Filstorlek", - "th_TH": "ขนาดไฟล์", - "tr_TR": "Dosya Boyutu", - "uk_UA": "Розмір файлу", - "zh_CN": "大小", - "zh_TW": "檔案大小" + "ar_SA": "", + "de_DE": "Dateigröße: {0}", + "el_GR": "Μέγεθος Αρχείου: {0}", + "en_US": "File Size: {0}", + "es_ES": "Tamaño del archivo: {0}", + "fr_FR": "Taille du Fichier: {0}", + "he_IL": "", + "it_IT": "Dimensione file: {0}", + "ja_JP": "ファイルサイズ: {0}", + "ko_KR": "파일 크기: {0}", + "no_NO": "Fil Størrelse: {0}", + "pl_PL": "Rozmiar pliku: {0}", + "pt_BR": "Tamanho: {0}", + "ru_RU": "Размер файла: {0}", + "sv_SE": "Filstorlek: {0}", + "th_TH": "ขนาดไฟล์: {0}", + "tr_TR": "Dosya Boyutu: {0}", + "uk_UA": "Розмір файлу: {0}", + "zh_CN": "大小: {0}", + "zh_TW": "檔案大小: {0}" } }, { @@ -1697,6 +1697,106 @@ "zh_TW": "路徑" } }, + { + "ID": "GameListHeaderCompatibilityStatus", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Compatibility:", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameListHeaderTitleId", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Title ID:", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameListHeaderHostedGames", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Hosted Games: {0}", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameListHeaderPlayerCount", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Online Players: {0}", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "GameListContextMenuOpenUserSaveDirectory", "Translations": { @@ -23298,4 +23398,4 @@ } } ] -} +} \ No newline at end of file diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml index dafd75087..a18fec656 100644 --- a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" - xmlns:appLibrary="using:Ryujinx.Ava.Utilities.AppLibrary" + xmlns:ext="using:Ryujinx.Ava.Common.Markup" xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" @@ -13,112 +13,102 @@ MaxWidth="256" MinWidth="256" Source="{Binding AppData.Icon, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" /> - + - - - - - - + + + + + - - - - + - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs index cc8091d4d..e85e1188e 100644 --- a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs @@ -23,6 +23,7 @@ namespace Ryujinx.Ava.UI.Controls { ContentDialog contentDialog = new() { + Title = appData.Name, PrimaryButtonText = string.Empty, SecondaryButtonText = string.Empty, CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose], diff --git a/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs index 73d555389..9e0a3554a 100644 --- a/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs @@ -1,34 +1,26 @@ using Gommon; +using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Utilities.AppLibrary; namespace Ryujinx.Ava.UI.ViewModels { public class ApplicationDataViewModel : BaseModel { - private const string FormatVersion = "Current Version: {0}"; - private const string FormatDeveloper = "Developed by {0}"; - - private const string FormatExtension = "Game type: {0}"; - private const string FormatLastPlayed = "Last played: {0}"; - private const string FormatPlayTime = "Play time: {0}"; - private const string FormatSize = "Size: {0}"; - - private const string FormatHostedGames = "Hosted Games: {0}"; - private const string FormatPlayerCount = "Online Players: {0}"; - public ApplicationData AppData { get; } public ApplicationDataViewModel(ApplicationData appData) => AppData = appData; - public string FormattedVersion => FormatVersion.Format(AppData.Version); - public string FormattedDeveloper => FormatDeveloper.Format(AppData.Developer); + public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version); + public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer); - public string FormattedFileExtension => FormatExtension.Format(AppData.FileExtension); - public string FormattedLastPlayed => FormatLastPlayed.Format(AppData.LastPlayedString); - public string FormattedPlayTime => FormatPlayTime.Format(AppData.TimePlayedString); - public string FormattedFileSize => FormatSize.Format(AppData.FileSizeString); + public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension); + public string FormattedLastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed].Format(AppData.LastPlayedString); + public string FormattedPlayTime => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed].Format(AppData.TimePlayedString); + public string FormattedFileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize].Format(AppData.FileSizeString); public string FormattedLdnInfo => - $"{FormatHostedGames.Format(AppData.GameCount)}\n{FormatPlayerCount.Format(AppData.PlayerCount)}"; + $"{LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames].Format(AppData.GameCount)}" + + $"\n" + + $"{LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount].Format(AppData.PlayerCount)}"; } } -- 2.47.1 From 4ae9f1c0d210d8a0af9b43e8ebaca59292ec5510 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 23:31:31 -0600 Subject: [PATCH 042/209] UI: Use Hosted Games & Player Count localization keys in list view too --- .../Converters/MultiplayerInfoConverter.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs index dc36098a1..f23a2beae 100644 --- a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs +++ b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs @@ -1,8 +1,11 @@ using Avalonia.Data.Converters; using Avalonia.Markup.Xaml; +using Gommon; +using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Utilities.AppLibrary; using System; using System.Globalization; +using System.Text; namespace Ryujinx.Ava.UI.Helpers { @@ -12,13 +15,17 @@ namespace Ryujinx.Ava.UI.Helpers public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value is ApplicationData { HasLdnGames: true } applicationData) - { - return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}"; - } - - return ""; + if (value is not ApplicationData { HasLdnGames: true } applicationData) + return ""; + return new StringBuilder() + .AppendLine( + LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames] + .Format(applicationData.GameCount)) + .Append( + LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount] + .Format(applicationData.PlayerCount)) + .ToString(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) -- 2.47.1 From 4b1d94ccd8e468979eff1c061d51584a1b901747 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 23:36:36 -0600 Subject: [PATCH 043/209] misc: chore: [ci skip] use MultiplayerInfoConverter instance instead of constructing for every use --- src/Ryujinx/UI/Controls/ApplicationListView.axaml | 2 +- src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml index c01c4e8be..ab4b38621 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml @@ -141,7 +141,7 @@ diff --git a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs index f23a2beae..7694e8883 100644 --- a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs +++ b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs @@ -11,7 +11,7 @@ namespace Ryujinx.Ava.UI.Helpers { internal class MultiplayerInfoConverter : MarkupExtension, IValueConverter { - private static readonly MultiplayerInfoConverter _instance = new(); + public static readonly MultiplayerInfoConverter Instance = new(); public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { @@ -35,7 +35,7 @@ namespace Ryujinx.Ava.UI.Helpers public override object ProvideValue(IServiceProvider serviceProvider) { - return _instance; + return Instance; } } } -- 2.47.1 From 3ecc7819cc1b259e06adf1340b5e5ac843921c1d Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 23:47:24 -0600 Subject: [PATCH 044/209] UI: Fix the app list sort types using the newly changed localization keys --- src/Ryujinx/Assets/locales.json | 127 +++++++++++++++++- .../UI/ViewModels/MainWindowViewModel.cs | 14 +- .../UI/Views/Main/MainViewControls.axaml | 12 +- 3 files changed, 139 insertions(+), 14 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index cb222e935..8ec664c81 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -1673,7 +1673,132 @@ } }, { - "ID": "GameListHeaderPath", + "ID": "GameListSortDeveloper", + "Translations": { + "ar_SA": "المطور", + "de_DE": "Entwickler", + "el_GR": "Προγραμματιστής", + "en_US": "Developer", + "es_ES": "Desarrollador", + "fr_FR": "Développeur", + "he_IL": "מפתח", + "it_IT": "Sviluppatore", + "ja_JP": "開発元", + "ko_KR": "개발자", + "no_NO": "Utvikler", + "pl_PL": "Twórca", + "pt_BR": "Desenvolvedor", + "ru_RU": "Разработчик", + "sv_SE": "Utvecklare", + "th_TH": "ผู้พัฒนา", + "tr_TR": "Geliştirici", + "uk_UA": "Розробник", + "zh_CN": "制作商", + "zh_TW": "開發者" + } + }, + { + "ID": "GameListSortTimePlayed", + "Translations": { + "ar_SA": "وقت اللعب", + "de_DE": "Spielzeit", + "el_GR": "Χρόνος", + "en_US": "Play Time", + "es_ES": "Tiempo jugado", + "fr_FR": "Temps de jeu", + "he_IL": "זמן משחק", + "it_IT": "Tempo di gioco", + "ja_JP": "プレイ時間", + "ko_KR": "플레이 타임", + "no_NO": "Spilletid", + "pl_PL": "Czas w grze:", + "pt_BR": "Tempo de jogo", + "ru_RU": "Время в игре", + "sv_SE": "Speltid", + "th_TH": "เล่นไปแล้ว", + "tr_TR": "Oynama Süresi", + "uk_UA": "Зіграно часу", + "zh_CN": "游玩时长", + "zh_TW": "遊玩時數" + } + }, + { + "ID": "GameListSortLastPlayed", + "Translations": { + "ar_SA": "آخر مرة لُعبت", + "de_DE": "Zuletzt gespielt", + "el_GR": "Παίχτηκε", + "en_US": "Last Played", + "es_ES": "Jugado por última vez", + "fr_FR": "Dernière partie jouée", + "he_IL": "שוחק לאחרונה", + "it_IT": "Ultima partita", + "ja_JP": "最終プレイ日時", + "ko_KR": "마지막 플레이", + "no_NO": "Sist Spilt", + "pl_PL": "Ostatnio grane", + "pt_BR": "Último jogo", + "ru_RU": "Последний запуск", + "sv_SE": "Senast spelad", + "th_TH": "เล่นล่าสุด", + "tr_TR": "Son Oynama Tarihi", + "uk_UA": "Востаннє зіграно", + "zh_CN": "最近游玩", + "zh_TW": "最近遊玩" + } + }, + { + "ID": "GameListSortFileExtension", + "Translations": { + "ar_SA": "صيغة الملف", + "de_DE": "Dateiformat", + "el_GR": "Κατάληξη", + "en_US": "File Ext", + "es_ES": "Extensión", + "fr_FR": "Extension du Fichier", + "he_IL": "סיומת קובץ", + "it_IT": "Estensione", + "ja_JP": "ファイル拡張子", + "ko_KR": "파일 확장자", + "no_NO": "Fil Eks.", + "pl_PL": "Rozszerzenie pliku", + "pt_BR": "Extensão", + "ru_RU": "Расширение файла", + "sv_SE": "Filänd", + "th_TH": "นามสกุลไฟล์", + "tr_TR": "Dosya Uzantısı", + "uk_UA": "Розширення файлу", + "zh_CN": "扩展名", + "zh_TW": "副檔名" + } + }, + { + "ID": "GameListSortFileSize", + "Translations": { + "ar_SA": "حجم الملف", + "de_DE": "Dateigröße", + "el_GR": "Μέγεθος Αρχείου", + "en_US": "File Size", + "es_ES": "Tamaño del archivo", + "fr_FR": "Taille du Fichier", + "he_IL": "גודל הקובץ", + "it_IT": "Dimensione file", + "ja_JP": "ファイルサイズ", + "ko_KR": "파일 크기", + "no_NO": "Fil Størrelse", + "pl_PL": "Rozmiar pliku", + "pt_BR": "Tamanho", + "ru_RU": "Размер файла", + "sv_SE": "Filstorlek", + "th_TH": "ขนาดไฟล์", + "tr_TR": "Dosya Boyutu", + "uk_UA": "Розмір файлу", + "zh_CN": "大小", + "zh_TW": "檔案大小" + } + }, + { + "ID": "GameListSortPath", "Translations": { "ar_SA": "المسار", "de_DE": "Pfad", diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index d7a09a0e3..499eedc8d 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -633,15 +633,15 @@ namespace Ryujinx.Ava.UI.ViewModels { return SortMode switch { - ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication], - ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper], - ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed], - ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed], - ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension], - ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize], - ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath], ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite], ApplicationSort.TitleId => LocaleManager.Instance[LocaleKeys.DlcManagerTableHeadingTitleIdLabel], + ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication], + ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListSortDeveloper], + ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListSortLastPlayed], + ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListSortTimePlayed], + ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListSortFileExtension], + ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListSortFileSize], + ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListSortPath], _ => string.Empty, }; } diff --git a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml index cdc66a138..db557b417 100644 --- a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml +++ b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml @@ -113,37 +113,37 @@ Tag="TitleId" /> -- 2.47.1 From 479b38f035b35ceffe2d4d87999d18eafc29573f Mon Sep 17 00:00:00 2001 From: FluffyOMC <45863583+FluffyOMC@users.noreply.github.com> Date: Wed, 5 Feb 2025 01:42:20 -0500 Subject: [PATCH 045/209] Add tooltips to game status (#625) --- src/Ryujinx/Assets/locales.json | 125 ++++++++++++++++++ .../UI/Controls/ApplicationDataView.axaml | 4 +- .../UI/Controls/ApplicationListView.axaml | 3 +- .../Utilities/AppLibrary/ApplicationData.cs | 14 +- .../Utilities/Compat/CompatibilityCsv.cs | 15 ++- .../Utilities/Compat/CompatibilityList.axaml | 2 + 6 files changed, 158 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 8ec664c81..c3044f639 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -23497,6 +23497,131 @@ "zh_TW": "無法啟動" } }, + { + "ID": "CompatibilityListPlayableTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Boots and plays without any crashes or GPU bugs of any kind, and at a speed fast enough to reasonably enjoy on an average PC.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "CompatibilityListIngameTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Boots and goes in-game but suffers from one or more of the following: crashes, deadlocks, GPU bugs, distractingly bad audio, or is simply too slow. Game still might able to be played all the way through, but not as the game is intended to play.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "CompatibilityListMenusTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Boots and goes past the title screen but does not make it into main gameplay.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "CompatibilityListBootsTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Boots but does not make it past the title screen.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "CompatibilityListNothingTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Does not boot or shows no signs of activity.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "ExtractAocListHeader", "Translations": { diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml index a18fec656..45ae75639 100644 --- a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml @@ -41,13 +41,12 @@ HorizontalAlignment="Left" Orientation="Vertical" Spacing="5"> - +