mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-09-03 23:44:43 +00:00
Compare commits
9 Commits
9dc25fd901
...
b826f38b02
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b826f38b02 | ||
![]() |
042362ee2b | ||
![]() |
7347ee2212 | ||
![]() |
01a9b636af | ||
![]() |
6e47d8548c | ||
![]() |
da340f5615 | ||
![]() |
be249f7bdc | ||
![]() |
a89dca3671 | ||
![]() |
98cee8f88f |
@ -24316,6 +24316,631 @@
|
|||||||
"zh_CN": "在执行首条指令前挂起应用程序,这样就可以从最早的点开始调试。",
|
"zh_CN": "在执行首条指令前挂起应用程序,这样就可以从最早的点开始调试。",
|
||||||
"zh_TW": ""
|
"zh_TW": ""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "LdnGameListOpen",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Open LDN Game List",
|
||||||
|
"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": "打开 LDN 游戏列表",
|
||||||
|
"zh_TW": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "LdnGameListTitle",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "LDN Game Browser - {0} games",
|
||||||
|
"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": "LDN 游戏浏览器 - {0} 个游戏",
|
||||||
|
"zh_TW": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "LdnGameListSearchBoxWatermark",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Search {0} LDN games...",
|
||||||
|
"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": "搜索到 {0} 个 LDN 游戏...",
|
||||||
|
"zh_TW": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "LdnGameListInfoButtonToolTip",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "What is LDN?",
|
||||||
|
"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": "什么是 LDN",
|
||||||
|
"zh_TW": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "LdnGameListRefreshToolTip",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Refresh available games from the server at {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": "在 {0} 时从服务器刷新可用游戏",
|
||||||
|
"zh_TW": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "LdnGameListPlayerSortDisable",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Player Count - Disable",
|
||||||
|
"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": "LdnGameListPlayerSortAscending",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Player Count - Ascending",
|
||||||
|
"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": "LdnGameListPlayerSortDescending",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Player Count - Descending",
|
||||||
|
"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": "LdnGameListFiltersHeading",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Filters",
|
||||||
|
"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": "LdnGameListFiltersOnlyShowPublicGames",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Only show public games",
|
||||||
|
"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": "LdnGameListFiltersOnlyShowJoinableGames",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Only show joinable games",
|
||||||
|
"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": "LdnGameListConnectionTypeMasterServerProxy",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Master Server Proxy",
|
||||||
|
"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": "LdnGameListConnectionTypeP2P",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "P2P",
|
||||||
|
"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": null,
|
||||||
|
"zh_TW": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "LdnGameListConnectionTypeMasterServerProxyToolTip",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Connects through the RyuLDN server (slower).",
|
||||||
|
"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": "通过 RyuLDN 服务器进行连接 (较慢)。",
|
||||||
|
"zh_TW": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "LdnGameListConnectionTypeP2PToolTip",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Connects via Peer-to-Peer via UPnP (faster).",
|
||||||
|
"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": "通过 UPnP 进行点对点连接 (较快)。",
|
||||||
|
"zh_TW": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "LdnGameListCreatedAt",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Created: {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": "已创建: {0} ",
|
||||||
|
"zh_TW": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "LdnGameListPlayersAndPlayerCount",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Players ({0} out of {1}):",
|
||||||
|
"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": "玩家 ({0} 之 {1}):",
|
||||||
|
"zh_TW": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "LdnGameListJoinable",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Joinable",
|
||||||
|
"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": "LdnGameListJoinableToolTip",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Game is joinable if it is public or if you know the passphrase.",
|
||||||
|
"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": "LdnGameListNotJoinable",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Not Joinable",
|
||||||
|
"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": "LdnGameListNotJoinableToolTip",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Game is currently in progress.",
|
||||||
|
"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": "LdnGameListPublic",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Public",
|
||||||
|
"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": "LdnGameListPublicToolTip",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Anyone can join this game.",
|
||||||
|
"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": "LdnGameListPrivate",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Private",
|
||||||
|
"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": "LdnGameListPrivateToolTip",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "You can only join this game if you also have the same LDN Passphrase in your settings.",
|
||||||
|
"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": "如果您在设置中有某些 LDN 口令则可加入此游戏。",
|
||||||
|
"zh_TW": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
226
src/Ryujinx.Common/Collections/BitMap.cs
Normal file
226
src/Ryujinx.Common/Collections/BitMap.cs
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
namespace Ryujinx.Common.Collections
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a collection that can store 1 bit values.
|
||||||
|
/// </summary>
|
||||||
|
public struct BitMap
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Size in bits of the integer used internally for the groups of bits.
|
||||||
|
/// </summary>
|
||||||
|
public const int IntSize = 64;
|
||||||
|
|
||||||
|
private const int IntShift = 6;
|
||||||
|
private const int IntMask = IntSize - 1;
|
||||||
|
|
||||||
|
private readonly long[] _masks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the value of a bit.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bit">Bit to access</param>
|
||||||
|
/// <returns>Bit value</returns>
|
||||||
|
public bool this[int bit]
|
||||||
|
{
|
||||||
|
get => IsSet(bit);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value)
|
||||||
|
{
|
||||||
|
Set(bit);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Clear(bit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new bitmap.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="count">Total number of bits</param>
|
||||||
|
public BitMap(int count)
|
||||||
|
{
|
||||||
|
_masks = new long[(count + IntMask) / IntSize];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if any bit is set.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if any bit is set, false otherwise</returns>
|
||||||
|
public bool AnySet()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _masks.Length; i++)
|
||||||
|
{
|
||||||
|
if (_masks[i] != 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a specific bit is set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bit">Bit to be checked</param>
|
||||||
|
/// <returns>True if set, false otherwise</returns>
|
||||||
|
public bool IsSet(int bit)
|
||||||
|
{
|
||||||
|
int wordIndex = bit >> IntShift;
|
||||||
|
int wordBit = bit & IntMask;
|
||||||
|
|
||||||
|
long wordMask = 1L << wordBit;
|
||||||
|
|
||||||
|
return (_masks[wordIndex] & wordMask) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if any bit inside a given range of bits is set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="start">Start bit of the range</param>
|
||||||
|
/// <param name="end">End bit of the range (inclusive)</param>
|
||||||
|
/// <returns>True if any bit is set, false otherwise</returns>
|
||||||
|
public bool IsSet(int start, int end)
|
||||||
|
{
|
||||||
|
if (start == end)
|
||||||
|
{
|
||||||
|
return IsSet(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
int startIndex = start >> IntShift;
|
||||||
|
int startBit = start & IntMask;
|
||||||
|
long startMask = -1L << startBit;
|
||||||
|
|
||||||
|
int endIndex = end >> IntShift;
|
||||||
|
int endBit = end & IntMask;
|
||||||
|
long endMask = (long)(ulong.MaxValue >> (IntMask - endBit));
|
||||||
|
|
||||||
|
if (startIndex == endIndex)
|
||||||
|
{
|
||||||
|
return (_masks[startIndex] & startMask & endMask) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_masks[startIndex] & startMask) != 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = startIndex + 1; i < endIndex; i++)
|
||||||
|
{
|
||||||
|
if (_masks[i] != 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_masks[endIndex] & endMask) != 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the value of a bit to 1.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bit">Bit to be set</param>
|
||||||
|
/// <returns>True if the bit was 0 and then changed to 1, false if it was already 1</returns>
|
||||||
|
public bool Set(int bit)
|
||||||
|
{
|
||||||
|
int wordIndex = bit >> IntShift;
|
||||||
|
int wordBit = bit & IntMask;
|
||||||
|
|
||||||
|
long wordMask = 1L << wordBit;
|
||||||
|
|
||||||
|
if ((_masks[wordIndex] & wordMask) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_masks[wordIndex] |= wordMask;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a given range of bits to 1.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="start">Start bit of the range</param>
|
||||||
|
/// <param name="end">End bit of the range (inclusive)</param>
|
||||||
|
public void SetRange(int start, int end)
|
||||||
|
{
|
||||||
|
if (start == end)
|
||||||
|
{
|
||||||
|
Set(start);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int startIndex = start >> IntShift;
|
||||||
|
int startBit = start & IntMask;
|
||||||
|
long startMask = -1L << startBit;
|
||||||
|
|
||||||
|
int endIndex = end >> IntShift;
|
||||||
|
int endBit = end & IntMask;
|
||||||
|
long endMask = (long)(ulong.MaxValue >> (IntMask - endBit));
|
||||||
|
|
||||||
|
if (startIndex == endIndex)
|
||||||
|
{
|
||||||
|
_masks[startIndex] |= startMask & endMask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_masks[startIndex] |= startMask;
|
||||||
|
|
||||||
|
for (int i = startIndex + 1; i < endIndex; i++)
|
||||||
|
{
|
||||||
|
_masks[i] |= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_masks[endIndex] |= endMask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a given bit to 0.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bit">Bit to be cleared</param>
|
||||||
|
public void Clear(int bit)
|
||||||
|
{
|
||||||
|
int wordIndex = bit >> IntShift;
|
||||||
|
int wordBit = bit & IntMask;
|
||||||
|
|
||||||
|
long wordMask = 1L << wordBit;
|
||||||
|
|
||||||
|
_masks[wordIndex] &= ~wordMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets all bits to 0.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _masks.Length; i++)
|
||||||
|
{
|
||||||
|
_masks[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets one or more groups of bits to 0.
|
||||||
|
/// See <see cref="IntSize"/> for how many bits are inside each group.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="start">Start index of the group</param>
|
||||||
|
/// <param name="end">End index of the group (inclusive)</param>
|
||||||
|
public void ClearInt(int start, int end)
|
||||||
|
{
|
||||||
|
for (int i = start; i <= end; i++)
|
||||||
|
{
|
||||||
|
_masks[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,5 +5,15 @@ namespace Ryujinx.Common
|
|||||||
public const string DefaultLanPlayHost = "ldn.ryujinx.app";
|
public const string DefaultLanPlayHost = "ldn.ryujinx.app";
|
||||||
public const short LanPlayPort = 30456;
|
public const short LanPlayPort = 30456;
|
||||||
public const string DefaultLanPlayWebHost = DefaultLanPlayHost;
|
public const string DefaultLanPlayWebHost = DefaultLanPlayHost;
|
||||||
|
|
||||||
|
public const string AmiiboTagsUrl = "https://raw.githubusercontent.com/Ryubing/Nfc/refs/heads/main/tags.json";
|
||||||
|
|
||||||
|
public const string FaqWikiUrl = "https://git.ryujinx.app/ryubing/ryujinx/-/wikis/FAQ-&-Troubleshooting";
|
||||||
|
|
||||||
|
public const string SetupGuideWikiUrl =
|
||||||
|
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&-Configuration-Guide";
|
||||||
|
|
||||||
|
public const string MultiplayerWikiUrl =
|
||||||
|
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,16 +81,8 @@ namespace Ryujinx.Graphics.Device
|
|||||||
if (index < Size)
|
if (index < Size)
|
||||||
{
|
{
|
||||||
uint alignedOffset = index * RegisterSize;
|
uint alignedOffset = index * RegisterSize;
|
||||||
|
|
||||||
Func<int> readCallback = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_readCallbacks), (nint)index);
|
return _readCallbacks[index]?.Invoke() ?? GetRefUnchecked<int>(alignedOffset);
|
||||||
if (readCallback != null)
|
|
||||||
{
|
|
||||||
return readCallback();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return GetRefUnchecked<int>(alignedOffset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@ -107,7 +99,7 @@ namespace Ryujinx.Graphics.Device
|
|||||||
|
|
||||||
GetRefIntAlignedUncheck(index) = data;
|
GetRefIntAlignedUncheck(index) = data;
|
||||||
|
|
||||||
Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_writeCallbacks), (nint)index)?.Invoke(data);
|
_writeCallbacks[index]?.Invoke(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +116,7 @@ namespace Ryujinx.Graphics.Device
|
|||||||
changed = storage != data;
|
changed = storage != data;
|
||||||
storage = data;
|
storage = data;
|
||||||
|
|
||||||
Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_writeCallbacks), (nint)index)?.Invoke(data);
|
_writeCallbacks[index]?.Invoke(data);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -109,7 +109,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
if (index < BlockSize)
|
if (index < BlockSize)
|
||||||
{
|
{
|
||||||
int groupIndex = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_registerToGroupMapping), (nint)index);
|
int groupIndex = _registerToGroupMapping[index];
|
||||||
if (groupIndex != 0)
|
if (groupIndex != 0)
|
||||||
{
|
{
|
||||||
groupIndex--;
|
groupIndex--;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using Ryujinx.Common.Collections;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
using Ryujinx.Graphics.Gpu.Memory;
|
using Ryujinx.Graphics.Gpu.Memory;
|
||||||
@ -72,6 +73,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readonly GpuChannel _channel;
|
private readonly GpuChannel _channel;
|
||||||
|
private readonly BitMap _invalidMap;
|
||||||
private readonly ConcurrentQueue<DereferenceRequest> _dereferenceQueue = new();
|
private readonly ConcurrentQueue<DereferenceRequest> _dereferenceQueue = new();
|
||||||
private TextureDescriptor _defaultDescriptor;
|
private TextureDescriptor _defaultDescriptor;
|
||||||
|
|
||||||
@ -166,6 +168,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
_channel = channel;
|
_channel = channel;
|
||||||
_aliasLists = new Dictionary<Texture, TextureAliasList>();
|
_aliasLists = new Dictionary<Texture, TextureAliasList>();
|
||||||
|
_invalidMap = new BitMap(maximumId + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -182,6 +185,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (texture == null)
|
if (texture == null)
|
||||||
{
|
{
|
||||||
|
if (_invalidMap.IsSet(id))
|
||||||
|
{
|
||||||
|
return ref descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
texture = PhysicalMemory.TextureCache.FindShortCache(descriptor);
|
texture = PhysicalMemory.TextureCache.FindShortCache(descriptor);
|
||||||
|
|
||||||
if (texture == null)
|
if (texture == null)
|
||||||
@ -198,6 +206,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
// If this happens, then the texture address is invalid, we can't add it to the cache.
|
// If this happens, then the texture address is invalid, we can't add it to the cache.
|
||||||
if (texture == null)
|
if (texture == null)
|
||||||
{
|
{
|
||||||
|
_invalidMap.Set(id);
|
||||||
return ref descriptor;
|
return ref descriptor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -515,6 +524,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
RemoveAliasList(texture);
|
RemoveAliasList(texture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_invalidMap.Clear(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,11 +120,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
public void ExcludeModifiedRegions(ulong address, ulong size, Action<ulong, ulong> action)
|
public void ExcludeModifiedRegions(ulong address, ulong size, Action<ulong, ulong> action)
|
||||||
{
|
{
|
||||||
// Slices a given region using the modified regions in the list. Calls the action for the new slices.
|
// Slices a given region using the modified regions in the list. Calls the action for the new slices.
|
||||||
bool lockOwner = Lock.IsReadLockHeld;
|
Lock.EnterReadLock();
|
||||||
if (!lockOwner)
|
|
||||||
{
|
|
||||||
Lock.EnterReadLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
(RangeItem<BufferModifiedRange> first, RangeItem<BufferModifiedRange> last) = FindOverlaps(address, size);
|
(RangeItem<BufferModifiedRange> first, RangeItem<BufferModifiedRange> last) = FindOverlaps(address, size);
|
||||||
|
|
||||||
@ -145,10 +141,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
current = current.Next;
|
current = current.Next;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lockOwner)
|
Lock.ExitReadLock();
|
||||||
{
|
|
||||||
Lock.ExitReadLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((long)size > 0)
|
if ((long)size > 0)
|
||||||
{
|
{
|
||||||
@ -179,9 +172,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferModifiedRange buffPost = null;
|
|
||||||
bool extendsPost = false;
|
|
||||||
bool extendsPre = false;
|
|
||||||
|
|
||||||
if (first == last)
|
if (first == last)
|
||||||
{
|
{
|
||||||
@ -196,14 +187,12 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
if (first.Address < address)
|
if (first.Address < address)
|
||||||
{
|
{
|
||||||
first.Value.Size = address - first.Address;
|
first.Value.Size = address - first.Address;
|
||||||
|
Update(first);
|
||||||
extendsPre = true;
|
|
||||||
|
|
||||||
if (first.EndAddress > endAddress)
|
if (first.EndAddress > endAddress)
|
||||||
{
|
{
|
||||||
buffPost = new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
|
Add(new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
|
||||||
first.Value.SyncNumber, first.Value.Parent);
|
first.Value.SyncNumber, first.Value.Parent));
|
||||||
extendsPost = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -212,6 +201,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
{
|
{
|
||||||
first.Value.Size = first.EndAddress - endAddress;
|
first.Value.Size = first.EndAddress - endAddress;
|
||||||
first.Value.Address = endAddress;
|
first.Value.Address = endAddress;
|
||||||
|
Update(first);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -219,11 +209,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extendsPre && extendsPost)
|
|
||||||
{
|
|
||||||
Add(buffPost);
|
|
||||||
}
|
|
||||||
|
|
||||||
Add(new BufferModifiedRange(address, size, syncNumber, this));
|
Add(new BufferModifiedRange(address, size, syncNumber, this));
|
||||||
Lock.ExitWriteLock();
|
Lock.ExitWriteLock();
|
||||||
|
|
||||||
@ -231,6 +216,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
}
|
}
|
||||||
|
|
||||||
BufferModifiedRange buffPre = null;
|
BufferModifiedRange buffPre = null;
|
||||||
|
BufferModifiedRange buffPost = null;
|
||||||
|
bool extendsPost = false;
|
||||||
|
bool extendsPre = false;
|
||||||
|
|
||||||
if (first.Address < address)
|
if (first.Address < address)
|
||||||
{
|
{
|
||||||
@ -329,7 +317,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
public bool HasRange(ulong address, ulong size)
|
public bool HasRange(ulong address, ulong size)
|
||||||
{
|
{
|
||||||
Lock.EnterReadLock();
|
Lock.EnterReadLock();
|
||||||
(RangeItem<BufferModifiedRange> first, RangeItem<BufferModifiedRange> _) = FindOverlaps(address, size);
|
RangeItem<BufferModifiedRange> first = FindOverlapFast(address, size);
|
||||||
bool result = first is not null;
|
bool result = first is not null;
|
||||||
Lock.ExitReadLock();
|
Lock.ExitReadLock();
|
||||||
return result;
|
return result;
|
||||||
@ -386,9 +374,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
ulong clampAddress = Math.Max(address, overlap.Address);
|
ulong clampAddress = Math.Max(address, overlap.Address);
|
||||||
ulong clampEnd = Math.Min(endAddress, overlap.EndAddress);
|
ulong clampEnd = Math.Min(endAddress, overlap.EndAddress);
|
||||||
|
|
||||||
Lock.EnterWriteLock();
|
|
||||||
ClearPart(overlap, clampAddress, clampEnd);
|
ClearPart(overlap, clampAddress, clampEnd);
|
||||||
Lock.ExitWriteLock();
|
|
||||||
|
|
||||||
RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, _flushAction);
|
RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, _flushAction);
|
||||||
}
|
}
|
||||||
@ -418,40 +404,33 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
ulong endAddress = address + size;
|
ulong endAddress = address + size;
|
||||||
ulong currentSync = _context.SyncNumber;
|
ulong currentSync = _context.SyncNumber;
|
||||||
|
|
||||||
int rangeCount = 0;
|
|
||||||
|
|
||||||
List<RangeItem<BufferModifiedRange>> overlaps = [];
|
List<RangeItem<BufferModifiedRange>> overlaps = [];
|
||||||
|
|
||||||
// Range list must be consistent for this operation
|
// Range list must be consistent for this operation
|
||||||
Lock.EnterReadLock();
|
|
||||||
if (_migrationTarget != null)
|
if (_migrationTarget != null)
|
||||||
{
|
|
||||||
rangeCount = -1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// We use the non-span method here because the array is partially modified by the code, which would invalidate a span.
|
|
||||||
(RangeItem<BufferModifiedRange> first, RangeItem<BufferModifiedRange> last) = FindOverlaps(address, size);
|
|
||||||
|
|
||||||
RangeItem<BufferModifiedRange> current = first;
|
|
||||||
while (last != null && current != last.Next)
|
|
||||||
{
|
|
||||||
rangeCount++;
|
|
||||||
overlaps.Add(current);
|
|
||||||
current = current.Next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Lock.ExitReadLock();
|
|
||||||
|
|
||||||
if (rangeCount == -1)
|
|
||||||
{
|
{
|
||||||
_migrationTarget!.WaitForAndFlushRanges(address, size);
|
_migrationTarget!.WaitForAndFlushRanges(address, size);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Lock.EnterWriteLock();
|
||||||
|
// We use the non-span method here because the array is partially modified by the code, which would invalidate a span.
|
||||||
|
(RangeItem<BufferModifiedRange> first, RangeItem<BufferModifiedRange> last) = FindOverlaps(address, size);
|
||||||
|
|
||||||
|
RangeItem<BufferModifiedRange> current = first;
|
||||||
|
while (last != null && current != last.Next)
|
||||||
|
{
|
||||||
|
overlaps.Add(current);
|
||||||
|
current = current.Next;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rangeCount = overlaps.Count;
|
||||||
|
|
||||||
if (rangeCount == 0)
|
if (rangeCount == 0)
|
||||||
{
|
{
|
||||||
|
Lock.ExitWriteLock();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -474,6 +453,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
if (highestDiff == long.MinValue)
|
if (highestDiff == long.MinValue)
|
||||||
{
|
{
|
||||||
|
Lock.ExitWriteLock();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -481,6 +462,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
_context.Renderer.WaitSync(currentSync + (ulong)highestDiff);
|
_context.Renderer.WaitSync(currentSync + (ulong)highestDiff);
|
||||||
|
|
||||||
RemoveRangesAndFlush(overlaps.ToArray(), rangeCount, highestDiff, currentSync, address, endAddress);
|
RemoveRangesAndFlush(overlaps.ToArray(), rangeCount, highestDiff, currentSync, address, endAddress);
|
||||||
|
|
||||||
|
Lock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -607,22 +590,17 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferModifiedRange buffPost = null;
|
|
||||||
bool extendsPost = false;
|
|
||||||
bool extendsPre = false;
|
|
||||||
|
|
||||||
if (first == last)
|
if (first == last)
|
||||||
{
|
{
|
||||||
if (first.Address < address)
|
if (first.Address < address)
|
||||||
{
|
{
|
||||||
first.Value.Size = address - first.Address;
|
first.Value.Size = address - first.Address;
|
||||||
extendsPre = true;
|
Update(first);
|
||||||
|
|
||||||
if (first.EndAddress > endAddress)
|
if (first.EndAddress > endAddress)
|
||||||
{
|
{
|
||||||
buffPost = new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
|
Add(new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
|
||||||
first.Value.SyncNumber, first.Value.Parent);
|
first.Value.SyncNumber, first.Value.Parent));
|
||||||
extendsPost = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -631,6 +609,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
{
|
{
|
||||||
first.Value.Size = first.EndAddress - endAddress;
|
first.Value.Size = first.EndAddress - endAddress;
|
||||||
first.Value.Address = endAddress;
|
first.Value.Address = endAddress;
|
||||||
|
Update(first);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -638,16 +617,14 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extendsPre && extendsPost)
|
|
||||||
{
|
|
||||||
Add(buffPost);
|
|
||||||
}
|
|
||||||
|
|
||||||
Lock.ExitWriteLock();
|
Lock.ExitWriteLock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferModifiedRange buffPre = null;
|
BufferModifiedRange buffPre = null;
|
||||||
|
BufferModifiedRange buffPost = null;
|
||||||
|
bool extendsPost = false;
|
||||||
|
bool extendsPre = false;
|
||||||
|
|
||||||
if (first.Address < address)
|
if (first.Address < address)
|
||||||
{
|
{
|
||||||
|
@ -87,6 +87,43 @@ namespace Ryujinx.Memory.Range
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates an item's end address on the list. Address must be the same.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The RangeItem to be updated</param>
|
||||||
|
/// <returns>True if the item was located and updated, false otherwise</returns>
|
||||||
|
protected override bool Update(RangeItem<T> item)
|
||||||
|
{
|
||||||
|
int index = BinarySearch(item.Address);
|
||||||
|
|
||||||
|
RangeItem<T> rangeItem = new(item.Value) { Previous = item.Previous, Next = item.Next };
|
||||||
|
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
Items[index - 1].Next = rangeItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < Count - 1)
|
||||||
|
{
|
||||||
|
Items[index + 1].Previous = rangeItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (ulong addr in item.QuickAccessAddresses)
|
||||||
|
{
|
||||||
|
_quickAccess.Remove(addr);
|
||||||
|
_fastQuickAccess.Remove(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
Items[index] = rangeItem;
|
||||||
|
|
||||||
|
if (item.Address != rangeItem.Address)
|
||||||
|
_quickAccess.Remove(item.Address);
|
||||||
|
|
||||||
|
_quickAccess[rangeItem.Address] = rangeItem;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private void Insert(int index, RangeItem<T> item)
|
private void Insert(int index, RangeItem<T> item)
|
||||||
{
|
{
|
||||||
@ -193,10 +230,9 @@ namespace Ryujinx.Memory.Range
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int startIndex = BinarySearch(startItem.Address);
|
(int startIndex, int endIndex) = BinarySearchEdges(startItem.Address, endItem.EndAddress);
|
||||||
int endIndex = BinarySearch(endItem.Address);
|
|
||||||
|
|
||||||
for (int i = startIndex; i <= endIndex; i++)
|
for (int i = startIndex; i < endIndex; i++)
|
||||||
{
|
{
|
||||||
_quickAccess.Remove(Items[i].Address);
|
_quickAccess.Remove(Items[i].Address);
|
||||||
foreach (ulong addr in Items[i].QuickAccessAddresses)
|
foreach (ulong addr in Items[i].QuickAccessAddresses)
|
||||||
@ -206,23 +242,23 @@ namespace Ryujinx.Memory.Range
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endIndex < Count - 1)
|
if (endIndex < Count)
|
||||||
{
|
{
|
||||||
Items[endIndex + 1].Previous = startIndex > 0 ? Items[startIndex - 1] : null;
|
Items[endIndex].Previous = startIndex > 0 ? Items[startIndex - 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startIndex > 0)
|
if (startIndex > 0)
|
||||||
{
|
{
|
||||||
Items[startIndex - 1].Next = endIndex < Count - 1 ? Items[endIndex + 1] : null;
|
Items[startIndex - 1].Next = endIndex < Count ? Items[endIndex] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (endIndex < Count - 1)
|
if (endIndex < Count)
|
||||||
{
|
{
|
||||||
Array.Copy(Items, endIndex + 1, Items, startIndex, Count - endIndex - 1);
|
Array.Copy(Items, endIndex, Items, startIndex, Count - endIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
Count -= endIndex - startIndex + 1;
|
Count -= endIndex - startIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -81,12 +81,73 @@ namespace Ryujinx.Memory.Range
|
|||||||
{
|
{
|
||||||
if (Items[index].Value.Equals(item))
|
if (Items[index].Value.Equals(item))
|
||||||
{
|
{
|
||||||
|
RangeItem<T> rangeItem = new(item) { Previous = Items[index].Previous, Next = Items[index].Next };
|
||||||
|
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
Items[index - 1].Next = rangeItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < Count - 1)
|
||||||
|
{
|
||||||
|
Items[index + 1].Previous = rangeItem;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (ulong address in Items[index].QuickAccessAddresses)
|
foreach (ulong address in Items[index].QuickAccessAddresses)
|
||||||
{
|
{
|
||||||
_quickAccess.Remove(address);
|
_quickAccess.Remove(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
Items[index] = new RangeItem<T>(item);
|
Items[index] = rangeItem;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Items[index].Address > item.Address)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates an item's end address on the list. Address must be the same.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The RangeItem to be updated</param>
|
||||||
|
/// <returns>True if the item was located and updated, false otherwise</returns>
|
||||||
|
protected override bool Update(RangeItem<T> item)
|
||||||
|
{
|
||||||
|
int index = BinarySearch(item.Address);
|
||||||
|
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
while (index < Count)
|
||||||
|
{
|
||||||
|
if (Items[index].Equals(item))
|
||||||
|
{
|
||||||
|
RangeItem<T> rangeItem = new(item.Value) { Previous = item.Previous, Next = item.Next };
|
||||||
|
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
Items[index - 1].Next = rangeItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < Count - 1)
|
||||||
|
{
|
||||||
|
Items[index + 1].Previous = rangeItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (ulong address in item.QuickAccessAddresses)
|
||||||
|
{
|
||||||
|
_quickAccess.Remove(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
Items[index] = rangeItem;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
return u1 == u2;
|
return u1 == u2;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetHashCode(ulong value) => (int)(value >> 5);
|
public int GetHashCode(ulong value) => (int)(value << 5);
|
||||||
|
|
||||||
public static readonly AddressEqualityComparer Comparer = new();
|
public static readonly AddressEqualityComparer Comparer = new();
|
||||||
}
|
}
|
||||||
@ -63,6 +63,13 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// <returns>True if the item was located and updated, false otherwise</returns>
|
/// <returns>True if the item was located and updated, false otherwise</returns>
|
||||||
protected abstract bool Update(T item);
|
protected abstract bool Update(T item);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates an item's end address on the list. Address must be the same.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The RangeItem to be updated</param>
|
||||||
|
/// <returns>True if the item was located and updated, false otherwise</returns>
|
||||||
|
protected abstract bool Update(RangeItem<T> item);
|
||||||
|
|
||||||
public abstract bool Remove(T item);
|
public abstract bool Remove(T item);
|
||||||
|
|
||||||
public abstract void RemoveRange(RangeItem<T> startItem, RangeItem<T> endItem);
|
public abstract void RemoveRange(RangeItem<T> startItem, RangeItem<T> endItem);
|
||||||
|
@ -182,11 +182,15 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
{
|
{
|
||||||
if (region.Guest)
|
if (region.Guest)
|
||||||
{
|
{
|
||||||
|
_guestVirtualRegions.Lock.EnterWriteLock();
|
||||||
_guestVirtualRegions.Remove(region);
|
_guestVirtualRegions.Remove(region);
|
||||||
|
_guestVirtualRegions.Lock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
_virtualRegions.Lock.EnterWriteLock();
|
||||||
_virtualRegions.Remove(region);
|
_virtualRegions.Remove(region);
|
||||||
|
_virtualRegions.Lock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,8 +47,12 @@ namespace Ryujinx.Ava.Common.Locale
|
|||||||
|
|
||||||
private void Load()
|
private void Load()
|
||||||
{
|
{
|
||||||
string localeLanguageCode = !string.IsNullOrEmpty(ConfigurationState.Instance.UI.LanguageCode.Value) ?
|
string localeLanguageCode = CultureInfo.CurrentCulture.Name.Replace('-', '_');
|
||||||
ConfigurationState.Instance.UI.LanguageCode.Value : CultureInfo.CurrentCulture.Name.Replace('-', '_');
|
if (Program.PreviewerDetached && ConfigurationState.Instance.UI.LanguageCode.Value is { } lang)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(lang))
|
||||||
|
localeLanguageCode = lang;
|
||||||
|
}
|
||||||
|
|
||||||
LoadLanguage(localeLanguageCode);
|
LoadLanguage(localeLanguageCode);
|
||||||
|
|
||||||
@ -63,6 +67,15 @@ namespace Ryujinx.Ava.Common.Locale
|
|||||||
|
|
||||||
public static string GetUnformatted(LocaleKeys key) => Instance.Get(key);
|
public static string GetUnformatted(LocaleKeys key) => Instance.Get(key);
|
||||||
|
|
||||||
|
public static string GetFormatted(LocaleKeys key, params object[] values)
|
||||||
|
=> GetUnformatted(key).Format(values);
|
||||||
|
|
||||||
|
public static string FormatDynamicValue(LocaleKeys key, params object[] values)
|
||||||
|
=> Instance.UpdateAndGetDynamicValue(key, values);
|
||||||
|
|
||||||
|
public static void Associate(LocaleKeys key, params object[] values)
|
||||||
|
=> Instance.SetDynamicValues(key, values);
|
||||||
|
|
||||||
public string Get(LocaleKeys key) =>
|
public string Get(LocaleKeys key) =>
|
||||||
_localeStrings.TryGetValue(key, out string value)
|
_localeStrings.TryGetValue(key, out string value)
|
||||||
? value
|
? value
|
||||||
@ -107,9 +120,6 @@ namespace Ryujinx.Ava.Common.Locale
|
|||||||
_ => false
|
_ => false
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string FormatDynamicValue(LocaleKeys key, params object[] values)
|
|
||||||
=> Instance.UpdateAndGetDynamicValue(key, values);
|
|
||||||
|
|
||||||
public void SetDynamicValues(LocaleKeys key, params object[] values)
|
public void SetDynamicValues(LocaleKeys key, params object[] values)
|
||||||
{
|
{
|
||||||
_dynamicValues[key] = values;
|
_dynamicValues[key] = values;
|
||||||
@ -161,12 +171,14 @@ namespace Ryujinx.Ava.Common.Locale
|
|||||||
{
|
{
|
||||||
if (locale.Translations.Count < _localeData.Value.Languages.Count)
|
if (locale.Translations.Count < _localeData.Value.Languages.Count)
|
||||||
{
|
{
|
||||||
throw new Exception($"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
|
throw new Exception(
|
||||||
|
$"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (locale.Translations.Count > _localeData.Value.Languages.Count)
|
if (locale.Translations.Count > _localeData.Value.Languages.Count)
|
||||||
{
|
{
|
||||||
throw new Exception($"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
|
throw new Exception(
|
||||||
|
$"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Enum.TryParse<LocaleKeys>(locale.ID, out LocaleKeys localeKey))
|
if (!Enum.TryParse<LocaleKeys>(locale.ID, out LocaleKeys localeKey))
|
||||||
@ -178,7 +190,8 @@ namespace Ryujinx.Ava.Common.Locale
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(str))
|
if (string.IsNullOrEmpty(str))
|
||||||
{
|
{
|
||||||
throw new Exception($"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null");
|
throw new Exception(
|
||||||
|
$"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null");
|
||||||
}
|
}
|
||||||
|
|
||||||
localeStrings[localeKey] = str;
|
localeStrings[localeKey] = str;
|
||||||
|
@ -1169,10 +1169,8 @@ namespace Ryujinx.Ava.Systems
|
|||||||
string frameTime = Device.Statistics.GetGameFrameTime().ToString("00.00");
|
string frameTime = Device.Statistics.GetGameFrameTime().ToString("00.00");
|
||||||
|
|
||||||
return Device.TurboMode
|
return Device.TurboMode
|
||||||
? LocaleManager.GetUnformatted(LocaleKeys.FpsTurboStatusBarText)
|
? LocaleManager.GetFormatted(LocaleKeys.FpsTurboStatusBarText, frameRate, frameTime, Device.TickScalar)
|
||||||
.Format(frameRate, frameTime, Device.TickScalar)
|
: LocaleManager.GetFormatted(LocaleKeys.FpsStatusBarText, frameRate, frameTime);
|
||||||
: LocaleManager.GetUnformatted(LocaleKeys.FpsStatusBarText)
|
|
||||||
.Format(frameRate, frameTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ShowExitPrompt()
|
public async Task ShowExitPrompt()
|
||||||
|
@ -14,6 +14,7 @@ using LibHac.Tools.FsSystem.NcaUtils;
|
|||||||
using Ryujinx.Ava.Common.Models;
|
using Ryujinx.Ava.Common.Models;
|
||||||
using Ryujinx.Ava.Systems.Configuration;
|
using Ryujinx.Ava.Systems.Configuration;
|
||||||
using Ryujinx.Ava.Systems.Configuration.System;
|
using Ryujinx.Ava.Systems.Configuration.System;
|
||||||
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.Ava.Utilities;
|
using Ryujinx.Ava.Utilities;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
@ -29,7 +30,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -85,7 +85,6 @@ namespace Ryujinx.Ava.Systems.AppLibrary
|
|||||||
private readonly SourceCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> _downloadableContents = new(it => it.Dlc);
|
private readonly SourceCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> _downloadableContents = new(it => it.Dlc);
|
||||||
|
|
||||||
private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||||
private static readonly LdnGameDataSerializerContext _ldnDataSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
|
||||||
|
|
||||||
public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel)
|
public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel)
|
||||||
{
|
{
|
||||||
@ -865,16 +864,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string ldnWebHost = ConfigurationState.Instance.Multiplayer.LdnServer;
|
LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs(await LdnGameModel.GetAllAsync()));
|
||||||
if (string.IsNullOrEmpty(ldnWebHost))
|
|
||||||
{
|
|
||||||
ldnWebHost = SharedConstants.DefaultLanPlayWebHost;
|
|
||||||
}
|
|
||||||
|
|
||||||
using HttpClient httpClient = new();
|
|
||||||
string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games");
|
|
||||||
LdnGameData[] ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData).ToArray();
|
|
||||||
LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs(ldnGameDataArray));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
using Gommon;
|
|
||||||
using LibHac.Ns;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Systems.AppLibrary
|
|
||||||
{
|
|
||||||
public struct LdnGameData
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public int PlayerCount { get; set; }
|
|
||||||
public int MaxPlayerCount { get; set; }
|
|
||||||
public string GameName { get; set; }
|
|
||||||
public string TitleId { get; set; }
|
|
||||||
public string Mode { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
public IEnumerable<string> Players { get; set; }
|
|
||||||
|
|
||||||
public static Array GetArrayForApp(
|
|
||||||
LdnGameData[] receivedData, ref ApplicationControlProperty acp)
|
|
||||||
{
|
|
||||||
LibHac.Common.FixedArrays.Array8<ulong> communicationId = acp.LocalCommunicationId;
|
|
||||||
|
|
||||||
return new Array(receivedData.Where(game =>
|
|
||||||
communicationId.AsReadOnlySpan().Contains(game.TitleId.ToULong())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Array
|
|
||||||
{
|
|
||||||
private readonly LdnGameData[] _ldnDatas;
|
|
||||||
|
|
||||||
internal Array(IEnumerable<LdnGameData> receivedData)
|
|
||||||
{
|
|
||||||
_ldnDatas = receivedData.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int PlayerCount => _ldnDatas.Sum(it => it.PlayerCount);
|
|
||||||
public int GameCount => _ldnDatas.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LdnGameDataHelper
|
|
||||||
{
|
|
||||||
public static LdnGameData.Array Where(this LdnGameData[] unfilteredDatas, ref ApplicationControlProperty acp)
|
|
||||||
=> LdnGameData.GetArrayForApp(unfilteredDatas, ref acp);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,7 @@
|
|||||||
|
using Ryujinx.Ava.UI.Models;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Systems.AppLibrary
|
namespace Ryujinx.Ava.Systems.AppLibrary
|
||||||
{
|
{
|
||||||
@ -6,12 +9,17 @@ namespace Ryujinx.Ava.Systems.AppLibrary
|
|||||||
{
|
{
|
||||||
public static new readonly LdnGameDataReceivedEventArgs Empty = new(null);
|
public static new readonly LdnGameDataReceivedEventArgs Empty = new(null);
|
||||||
|
|
||||||
public LdnGameDataReceivedEventArgs(LdnGameData[] ldnData)
|
public LdnGameDataReceivedEventArgs(LdnGameModel[] ldnData)
|
||||||
{
|
{
|
||||||
LdnData = ldnData ?? [];
|
LdnData = ldnData ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LdnGameDataReceivedEventArgs(IEnumerable<LdnGameModel> ldnData)
|
||||||
|
{
|
||||||
|
LdnData = ldnData?.ToArray() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public LdnGameData[] LdnData { get; set; }
|
public LdnGameModel[] LdnData { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Systems.AppLibrary
|
|
||||||
{
|
|
||||||
[JsonSerializable(typeof(IEnumerable<LdnGameData>))]
|
|
||||||
internal partial class LdnGameDataSerializerContext : JsonSerializerContext;
|
|
||||||
}
|
|
@ -11,6 +11,7 @@ using Ryujinx.Common.Helper;
|
|||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE;
|
using Ryujinx.HLE;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using RyuLogger = Ryujinx.Common.Logging.Logger;
|
using RyuLogger = Ryujinx.Common.Logging.Logger;
|
||||||
@ -689,6 +690,16 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||||||
: ldnServer;
|
: ldnServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetLdnWebServer()
|
||||||
|
{
|
||||||
|
if (Environment.GetEnvironmentVariable("RYULDN_WEB_HOST") is not { } ldnWebServer)
|
||||||
|
ldnWebServer = LdnServer;
|
||||||
|
|
||||||
|
return string.IsNullOrEmpty(ldnWebServer)
|
||||||
|
? SharedConstants.DefaultLanPlayWebHost
|
||||||
|
: ldnWebServer;
|
||||||
|
}
|
||||||
|
|
||||||
public MultiplayerSection()
|
public MultiplayerSection()
|
||||||
{
|
{
|
||||||
LanInterfaceId = new ReactiveObject<string>();
|
LanInterfaceId = new ReactiveObject<string>();
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Utilities;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Systems
|
namespace Ryujinx.Ava.Systems
|
||||||
|
@ -1,26 +1,5 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Platform.Storage;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using LibHac.Fs;
|
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
|
||||||
using Ryujinx.Ava.Common;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
|
||||||
using Ryujinx.Ava.Common.Models;
|
|
||||||
using Ryujinx.Ava.Systems.AppLibrary;
|
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
|
||||||
using Ryujinx.Ava.UI.Views.Dialog;
|
|
||||||
using Ryujinx.Ava.UI.Windows;
|
|
||||||
using Ryujinx.Ava.Utilities;
|
|
||||||
using Ryujinx.Common.Configuration;
|
|
||||||
using Ryujinx.Common.Helper;
|
|
||||||
using Ryujinx.HLE.HOS;
|
|
||||||
using SkiaSharp;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using Path = System.IO.Path;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Controls
|
namespace Ryujinx.Ava.UI.Controls
|
||||||
{
|
{
|
||||||
|
20
src/Ryujinx/UI/Helpers/Converters/LocaleKeyValueConverter.cs
Normal file
20
src/Ryujinx/UI/Helpers/Converters/LocaleKeyValueConverter.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using CommandLine;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
|
{
|
||||||
|
public class LocaleKeyValueConverter : IValueConverter
|
||||||
|
{
|
||||||
|
private static readonly Lazy<LocaleKeyValueConverter> _shared = new(() => new());
|
||||||
|
public static LocaleKeyValueConverter Shared => _shared.Value;
|
||||||
|
|
||||||
|
public object Convert(object value, Type _, object __, CultureInfo ___)
|
||||||
|
=> LocaleManager.Instance[value.Cast<LocaleKeys>()];
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type _, object __, CultureInfo ___)
|
||||||
|
=> throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.Common.Configuration.Hid;
|
using Ryujinx.Common.Configuration.Hid;
|
||||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||||
using System.Xml.Linq;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Models.Input
|
namespace Ryujinx.Ava.UI.Models.Input
|
||||||
{
|
{
|
||||||
|
180
src/Ryujinx/UI/Models/LdnGameModel.cs
Normal file
180
src/Ryujinx/UI/Models/LdnGameModel.cs
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
using Gommon;
|
||||||
|
using Humanizer;
|
||||||
|
using LibHac.Ns;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Systems.Configuration;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Models
|
||||||
|
{
|
||||||
|
public record LdnGameModel
|
||||||
|
{
|
||||||
|
public string Id { get; private init; }
|
||||||
|
public bool IsPublic { get; private init; }
|
||||||
|
public short PlayerCount { get; private init; }
|
||||||
|
public short MaxPlayerCount { get; private init; }
|
||||||
|
public TitleTuple Title { get; private init; }
|
||||||
|
public ConnectionType ConnectionType { get; private init; }
|
||||||
|
public bool IsJoinable { get; private init; }
|
||||||
|
public ushort SceneId { get; private init; }
|
||||||
|
public string[] Players { get; private init; }
|
||||||
|
|
||||||
|
public string PlayersLabel =>
|
||||||
|
LocaleManager.GetFormatted(LocaleKeys.LdnGameListPlayersAndPlayerCount, PlayerCount, MaxPlayerCount);
|
||||||
|
|
||||||
|
public string FormattedPlayers =>
|
||||||
|
Players.Chunk(4)
|
||||||
|
.Select(x => x.FormatCollection(s => s, prefix: " ", separator: ", "))
|
||||||
|
.JoinToString("\n ");
|
||||||
|
|
||||||
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
|
||||||
|
public string FormattedCreatedAt
|
||||||
|
=> LocaleManager.GetFormatted(LocaleKeys.LdnGameListCreatedAt, CreatedAt.Humanize());
|
||||||
|
|
||||||
|
public string CreatedAtToolTip => CreatedAt.DateTime.ToString(CultureInfo.CurrentUICulture);
|
||||||
|
|
||||||
|
public LocaleKeys ConnectionTypeLocaleKey => ConnectionType switch
|
||||||
|
{
|
||||||
|
ConnectionType.MasterServerProxy => LocaleKeys.LdnGameListConnectionTypeMasterServerProxy,
|
||||||
|
ConnectionType.PeerToPeer => LocaleKeys.LdnGameListConnectionTypeP2P,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(ConnectionType),
|
||||||
|
$"Expected either 'P2P' or 'Master Server Proxy' ConnectionType; got '{ConnectionType}'")
|
||||||
|
};
|
||||||
|
|
||||||
|
public LocaleKeys ConnectionTypeToolTipLocaleKey => ConnectionType switch
|
||||||
|
{
|
||||||
|
ConnectionType.MasterServerProxy => LocaleKeys.LdnGameListConnectionTypeMasterServerProxyToolTip,
|
||||||
|
ConnectionType.PeerToPeer => LocaleKeys.LdnGameListConnectionTypeP2PToolTip,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(ConnectionType),
|
||||||
|
$"Expected either 'P2P' or 'Master Server Proxy' ConnectionType; got '{ConnectionType}'")
|
||||||
|
};
|
||||||
|
|
||||||
|
public record struct TitleTuple
|
||||||
|
{
|
||||||
|
public required string Name { get; init; }
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Version { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Array GetArrayForApp(
|
||||||
|
LdnGameModel[] receivedData,
|
||||||
|
ref ApplicationControlProperty acp,
|
||||||
|
bool onlyJoinable = true,
|
||||||
|
bool onlyPublic = true)
|
||||||
|
{
|
||||||
|
LibHac.Common.FixedArrays.Array8<ulong> communicationId = acp.LocalCommunicationId;
|
||||||
|
|
||||||
|
return new Array(receivedData.Where(game =>
|
||||||
|
communicationId.AsReadOnlySpan().Contains(game.Title.Id.ToULong())
|
||||||
|
), onlyJoinable, onlyPublic);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Array : IEnumerable<LdnGameModel>
|
||||||
|
{
|
||||||
|
private readonly LdnGameModel[] _ldnDatas;
|
||||||
|
|
||||||
|
internal Array(IEnumerable<LdnGameModel> receivedData, bool onlyJoinable = false, bool onlyPublic = false)
|
||||||
|
{
|
||||||
|
if (onlyJoinable)
|
||||||
|
receivedData = receivedData.Where(x => x.IsJoinable);
|
||||||
|
|
||||||
|
if (onlyPublic)
|
||||||
|
receivedData = receivedData.Where(x => x.IsPublic);
|
||||||
|
|
||||||
|
_ldnDatas = receivedData.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int PlayerCount => _ldnDatas.Sum(it => it.PlayerCount);
|
||||||
|
public int GameCount => _ldnDatas.Length;
|
||||||
|
|
||||||
|
public IEnumerator<LdnGameModel> GetEnumerator() => (_ldnDatas as IEnumerable<LdnGameModel>).GetEnumerator();
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => _ldnDatas.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<IEnumerable<LdnGameModel>> GetAllAsync(HttpClient client = null)
|
||||||
|
=> LdnGameJsonModel.ParseArray(await GetAllAsyncRequestImpl(client))
|
||||||
|
.Select(FromJson);
|
||||||
|
|
||||||
|
private static async Task<string> GetAllAsyncRequestImpl(HttpClient client = null)
|
||||||
|
{
|
||||||
|
var ldnWebHost = ConfigurationState.Instance.Multiplayer.GetLdnWebServer();
|
||||||
|
|
||||||
|
LocaleManager.Associate(LocaleKeys.LdnGameListRefreshToolTip, ldnWebHost);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (client != null)
|
||||||
|
return await client.GetStringAsync($"https://{ldnWebHost}/api/public_games");
|
||||||
|
|
||||||
|
using HttpClient httpClient = new();
|
||||||
|
return await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LdnGameModel FromJson(LdnGameJsonModel json) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = json.Id,
|
||||||
|
IsPublic = json.IsPublic,
|
||||||
|
PlayerCount = json.PlayerCount,
|
||||||
|
MaxPlayerCount = json.MaxPlayerCount,
|
||||||
|
Title = new TitleTuple { Name = json.TitleName, Id = json.TitleId, Version = json.TitleVersion },
|
||||||
|
ConnectionType = json.ConnectionType switch
|
||||||
|
{
|
||||||
|
"P2P" => ConnectionType.PeerToPeer,
|
||||||
|
"Master Server Proxy" => ConnectionType.MasterServerProxy,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(json),
|
||||||
|
$"Expected either 'P2P' or 'Master Server Proxy' ConnectionType; got '{json.ConnectionType}'")
|
||||||
|
},
|
||||||
|
IsJoinable = json.Joinability is "Joinable",
|
||||||
|
SceneId = json.SceneId,
|
||||||
|
Players = json.Players,
|
||||||
|
CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(json.CreatedAtUnixTimestamp).ToLocalTime()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LdnGameJsonModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")] public string Id { get; set; }
|
||||||
|
[JsonPropertyName("is_public")] public bool IsPublic { get; set; }
|
||||||
|
[JsonPropertyName("player_count")] public short PlayerCount { get; set; }
|
||||||
|
[JsonPropertyName("max_player_count")] public short MaxPlayerCount { get; set; }
|
||||||
|
[JsonPropertyName("game_name")] public string TitleName { get; set; }
|
||||||
|
[JsonPropertyName("title_id")] public string TitleId { get; set; }
|
||||||
|
[JsonPropertyName("title_version")] public string TitleVersion { get; set; }
|
||||||
|
[JsonPropertyName("mode")] public string ConnectionType { get; set; }
|
||||||
|
[JsonPropertyName("status")] public string Joinability { get; set; }
|
||||||
|
[JsonPropertyName("scene_id")] public ushort SceneId { get; set; }
|
||||||
|
[JsonPropertyName("players")] public string[] Players { get; set; }
|
||||||
|
[JsonPropertyName("created_at")] public long CreatedAtUnixTimestamp { get; set; }
|
||||||
|
|
||||||
|
public static LdnGameJsonModel Parse(string value)
|
||||||
|
=> JsonHelper.Deserialize(value, LdnGameJsonModelSerializerContext.Default.LdnGameJsonModel);
|
||||||
|
|
||||||
|
public static LdnGameJsonModel[] ParseArray(string value)
|
||||||
|
=> JsonHelper.Deserialize(value, LdnGameJsonModelSerializerContext.Default.LdnGameJsonModelArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ConnectionType
|
||||||
|
{
|
||||||
|
PeerToPeer,
|
||||||
|
MasterServerProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(LdnGameJsonModel[]))]
|
||||||
|
partial class LdnGameJsonModelSerializerContext : JsonSerializerContext;
|
||||||
|
}
|
@ -442,7 +442,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://raw.githubusercontent.com/Ryubing/Nfc/refs/heads/main/tags.json"));
|
HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, SharedConstants.AmiiboTagsUrl));
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@ -461,7 +461,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
HttpResponseMessage response = await _httpClient.GetAsync("https://raw.githubusercontent.com/Ryubing/Nfc/refs/heads/main/tags.json");
|
HttpResponseMessage response = await _httpClient.GetAsync(SharedConstants.AmiiboTagsUrl);
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
|
@ -145,8 +145,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OnPropertyChanged();
|
|
||||||
OnPropertyChanged(nameof(CurrentEntries));
|
OnPropertyChanged(nameof(CurrentEntries));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Svg.Skia;
|
using Avalonia.Svg.Skia;
|
||||||
using Avalonia.Threading;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using Gommon;
|
using Gommon;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
207
src/Ryujinx/UI/ViewModels/LdnGamesListViewModel.cs
Normal file
207
src/Ryujinx/UI/ViewModels/LdnGamesListViewModel.cs
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Gommon;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Ryujinx.Ava.Systems.AppLibrary;
|
||||||
|
using Ryujinx.Ava.UI.Models;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
{
|
||||||
|
public partial class LdnGamesListViewModel : BaseModel, IDisposable
|
||||||
|
{
|
||||||
|
public MainWindowViewModel Mwvm { get; }
|
||||||
|
|
||||||
|
private readonly HttpClient _refreshClient;
|
||||||
|
|
||||||
|
private (int PlayerCount, int Name) _sorting;
|
||||||
|
|
||||||
|
private IEnumerable<LdnGameModel> _visibleEntries;
|
||||||
|
|
||||||
|
private string[] _ownedGameTitleIds = [];
|
||||||
|
|
||||||
|
private Func<LdnGameModel, object> _sortKeySelector = x => x.Title.Name; // Default sort by Title name
|
||||||
|
|
||||||
|
public IEnumerable<LdnGameModel> VisibleEntries => ApplyFilters();
|
||||||
|
|
||||||
|
private IEnumerable<LdnGameModel> ApplyFilters()
|
||||||
|
{
|
||||||
|
if (_visibleEntries is null)
|
||||||
|
{
|
||||||
|
_visibleEntries = Mwvm.LdnModels;
|
||||||
|
SortApply();
|
||||||
|
}
|
||||||
|
|
||||||
|
var filtered = _visibleEntries;
|
||||||
|
|
||||||
|
if (OnlyShowForOwnedGames)
|
||||||
|
filtered = filtered.Where(x => _ownedGameTitleIds.ContainsIgnoreCase(x.Title.Id));
|
||||||
|
|
||||||
|
if (OnlyShowPublicGames)
|
||||||
|
filtered = filtered.Where(x => x.IsPublic);
|
||||||
|
|
||||||
|
if (OnlyShowJoinableGames)
|
||||||
|
filtered = filtered.Where(x => x.IsJoinable);
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LdnGamesListViewModel()
|
||||||
|
{
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
Mwvm = RyujinxApp.MainWindow.ViewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppCountUpdated(object _, ApplicationCountUpdatedEventArgs __)
|
||||||
|
=> _ownedGameTitleIds = Mwvm.ApplicationLibrary.Applications.Keys.Select(x => x.ToString("X16")).ToArray();
|
||||||
|
|
||||||
|
public LdnGamesListViewModel(MainWindowViewModel mwvm)
|
||||||
|
{
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
Mwvm = mwvm;
|
||||||
|
_visibleEntries = Mwvm.LdnModels;
|
||||||
|
_refreshClient = new HttpClient();
|
||||||
|
AppCountUpdated(null, null);
|
||||||
|
Mwvm.ApplicationLibrary.ApplicationCountUpdated += AppCountUpdated;
|
||||||
|
Mwvm.PropertyChanged += Mwvm_OnPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IDisposable.Dispose()
|
||||||
|
{
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
_visibleEntries = null;
|
||||||
|
_refreshClient.Dispose();
|
||||||
|
Mwvm.ApplicationLibrary.ApplicationCountUpdated -= AppCountUpdated;
|
||||||
|
Mwvm.PropertyChanged -= Mwvm_OnPropertyChanged;
|
||||||
|
}
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Mwvm_OnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName is nameof(MainWindowViewModel.LdnModels))
|
||||||
|
OnPropertyChanged(nameof(VisibleEntries));
|
||||||
|
}
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isRefreshing;
|
||||||
|
private bool _onlyShowForOwnedGames;
|
||||||
|
private bool _onlyShowPublicGames = true;
|
||||||
|
private bool _onlyShowJoinableGames = true;
|
||||||
|
|
||||||
|
public async Task RefreshAsync()
|
||||||
|
{
|
||||||
|
IsRefreshing = true;
|
||||||
|
|
||||||
|
await Mwvm.ApplicationLibrary.RefreshLdn();
|
||||||
|
|
||||||
|
IsRefreshing = false;
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(VisibleEntries));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnlyShowForOwnedGames
|
||||||
|
{
|
||||||
|
get => _onlyShowForOwnedGames;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
OnPropertyChanging();
|
||||||
|
OnPropertyChanging(nameof(VisibleEntries));
|
||||||
|
_onlyShowForOwnedGames = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(VisibleEntries));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnlyShowPublicGames
|
||||||
|
{
|
||||||
|
get => _onlyShowPublicGames;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
OnPropertyChanging();
|
||||||
|
OnPropertyChanging(nameof(VisibleEntries));
|
||||||
|
_onlyShowPublicGames = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(VisibleEntries));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnlyShowJoinableGames
|
||||||
|
{
|
||||||
|
get => _onlyShowJoinableGames;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
OnPropertyChanging();
|
||||||
|
OnPropertyChanging(nameof(VisibleEntries));
|
||||||
|
_onlyShowJoinableGames = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(VisibleEntries));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void NameSorting(int nameSort = 0)
|
||||||
|
{
|
||||||
|
_sorting.Name = nameSort;
|
||||||
|
SortApply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StatusSorting(int statusSort = 0)
|
||||||
|
{
|
||||||
|
_sorting.PlayerCount = statusSort;
|
||||||
|
SortApply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Search(string searchTerm)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(searchTerm))
|
||||||
|
{
|
||||||
|
SetEntries(Mwvm.LdnModels);
|
||||||
|
SortApply();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetEntries(Mwvm.LdnModels.Where(x =>
|
||||||
|
x.Title.Name.ContainsIgnoreCase(searchTerm)
|
||||||
|
|| x.Title.Id.ContainsIgnoreCase(searchTerm)));
|
||||||
|
|
||||||
|
SortApply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetEntries(IEnumerable<LdnGameModel> entries)
|
||||||
|
{
|
||||||
|
_visibleEntries = entries.ToList();
|
||||||
|
OnPropertyChanged(nameof(VisibleEntries));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SortApply()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_visibleEntries = (_sorting switch
|
||||||
|
{
|
||||||
|
(0, 0) => _visibleEntries.OrderBy(x => _sortKeySelector(x) ?? string.Empty), // A - Z
|
||||||
|
(0, 1) => _visibleEntries.OrderByDescending(x => _sortKeySelector(x) ?? string.Empty), // Z - A
|
||||||
|
(1, 0) => _visibleEntries.OrderBy(x => x.PlayerCount).ThenBy(x => x.Title.Name, StringComparer.OrdinalIgnoreCase), // Player count low - high, then A - Z
|
||||||
|
(1, 1) => _visibleEntries.OrderBy(x => x.PlayerCount).ThenByDescending(x => x.Title.Name, StringComparer.OrdinalIgnoreCase), // Player count high - low, then A - Z
|
||||||
|
(2, 0) => _visibleEntries.OrderByDescending(x => x.PlayerCount).ThenBy(x => x.Title.Name, StringComparer.OrdinalIgnoreCase), // Player count low - high, then Z - A
|
||||||
|
(2, 1) => _visibleEntries.OrderByDescending(x => x.PlayerCount).ThenByDescending(x => x.Title.Name, StringComparer.OrdinalIgnoreCase), // Player count high - low, then Z - A
|
||||||
|
_ => _visibleEntries.OrderBy(x => x.PlayerCount)
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(VisibleEntries));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -32,6 +32,7 @@ using Ryujinx.Ava.UI.Windows;
|
|||||||
using Ryujinx.Ava.Utilities;
|
using Ryujinx.Ava.Utilities;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Common.Configuration.Multiplayer;
|
||||||
using Ryujinx.Common.Helper;
|
using Ryujinx.Common.Helper;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.UI;
|
using Ryujinx.Common.UI;
|
||||||
@ -57,7 +58,6 @@ using Key = Ryujinx.Input.Key;
|
|||||||
using MissingKeyException = LibHac.Common.Keys.MissingKeyException;
|
using MissingKeyException = LibHac.Common.Keys.MissingKeyException;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState;
|
using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState;
|
||||||
using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.ViewModels
|
namespace Ryujinx.Ava.UI.ViewModels
|
||||||
{
|
{
|
||||||
@ -111,6 +111,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
[ObservableProperty] private bool _isSubMenuOpen;
|
[ObservableProperty] private bool _isSubMenuOpen;
|
||||||
[ObservableProperty] private ApplicationContextMenu _listAppContextMenu;
|
[ObservableProperty] private ApplicationContextMenu _listAppContextMenu;
|
||||||
[ObservableProperty] private ApplicationContextMenu _gridAppContextMenu;
|
[ObservableProperty] private ApplicationContextMenu _gridAppContextMenu;
|
||||||
|
[ObservableProperty] private bool _isRyuLdnEnabled;
|
||||||
[ObservableProperty] private bool _updateAvailable;
|
[ObservableProperty] private bool _updateAvailable;
|
||||||
|
|
||||||
public static AsyncRelayCommand UpdateCommand { get; } = Commands.Create(async () =>
|
public static AsyncRelayCommand UpdateCommand { get; } = Commands.Create(async () =>
|
||||||
@ -142,7 +143,25 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
private ApplicationData _gridSelectedApplication;
|
private ApplicationData _gridSelectedApplication;
|
||||||
|
|
||||||
// Key is Title ID
|
// Key is Title ID
|
||||||
public SafeDictionary<string, LdnGameData.Array> LdnData = [];
|
/// <summary>
|
||||||
|
/// At any given time, this dictionary contains the filtered data from <see cref="_ldnModels"/>.
|
||||||
|
/// Filtered in this case meaning installed games only.
|
||||||
|
/// </summary>
|
||||||
|
public SafeDictionary<string, LdnGameModel.Array> UsableLdnData = [];
|
||||||
|
|
||||||
|
private LdnGameModel[] _ldnModels;
|
||||||
|
|
||||||
|
public LdnGameModel[] LdnModels
|
||||||
|
{
|
||||||
|
get => _ldnModels;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_ldnModels = value;
|
||||||
|
LocaleManager.Associate(LocaleKeys.LdnGameListTitle, value.Length);
|
||||||
|
LocaleManager.Associate(LocaleKeys.LdnGameListSearchBoxWatermark, value.Length);
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public MainWindow Window { get; init; }
|
public MainWindow Window { get; init; }
|
||||||
|
|
||||||
@ -165,11 +184,28 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
{
|
{
|
||||||
LoadConfigurableHotKeys();
|
LoadConfigurableHotKeys();
|
||||||
|
|
||||||
|
IsRyuLdnEnabled = ConfigurationState.Instance.Multiplayer.Mode.Value is MultiplayerMode.LdnRyu;
|
||||||
|
ConfigurationState.Instance.Multiplayer.Mode.Event += OnLdnModeChanged;
|
||||||
|
|
||||||
Volume = ConfigurationState.Instance.System.AudioVolume;
|
Volume = ConfigurationState.Instance.System.AudioVolume;
|
||||||
CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value;
|
CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~MainWindowViewModel()
|
||||||
|
{
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
ConfigurationState.Instance.Multiplayer.Mode.Event -= OnLdnModeChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLdnModeChanged(object sender, ReactiveEventArgs<MultiplayerMode> e) =>
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
IsRyuLdnEnabled = e.NewValue is MultiplayerMode.LdnRyu;
|
||||||
|
});
|
||||||
|
|
||||||
public void Initialize(
|
public void Initialize(
|
||||||
ContentManager contentManager,
|
ContentManager contentManager,
|
||||||
IStorageProvider storageProvider,
|
IStorageProvider storageProvider,
|
||||||
@ -313,11 +349,11 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
if (ts.HasValue)
|
if (ts.HasValue)
|
||||||
{
|
{
|
||||||
var formattedPlayTime = ValueFormatUtils.FormatTimeSpan(ts.Value);
|
var formattedPlayTime = ValueFormatUtils.FormatTimeSpan(ts.Value);
|
||||||
LocaleManager.Instance.SetDynamicValues(LocaleKeys.GameListLabelTotalTimePlayed, formattedPlayTime);
|
LocaleManager.Associate(LocaleKeys.GameListLabelTotalTimePlayed, formattedPlayTime);
|
||||||
ShowTotalTimePlayed = formattedPlayTime != string.Empty;
|
ShowTotalTimePlayed = formattedPlayTime != string.Empty;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShowTotalTimePlayed = ts.HasValue;
|
ShowTotalTimePlayed = ts.HasValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ using DynamicData.Binding;
|
|||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.ViewModels
|
namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||||
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||||
|
xmlns:common="clr-namespace:Ryujinx.Common;assembly=Ryujinx.Common"
|
||||||
x:DataType="viewModels:MainWindowViewModel"
|
x:DataType="viewModels:MainWindowViewModel"
|
||||||
x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView">
|
x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView">
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
@ -250,25 +251,30 @@
|
|||||||
Name="CompatibilityListMenuItem"
|
Name="CompatibilityListMenuItem"
|
||||||
Header="{ext:Locale CompatibilityListOpen}"
|
Header="{ext:Locale CompatibilityListOpen}"
|
||||||
Icon="{ext:Icon fa-solid fa-database}"/>
|
Icon="{ext:Icon fa-solid fa-database}"/>
|
||||||
|
<MenuItem
|
||||||
|
Name="LdnGameListMenuItem"
|
||||||
|
Header="{ext:Locale LdnGameListOpen}"
|
||||||
|
Icon="{ext:Icon fa-solid fa-people-group}"
|
||||||
|
IsEnabled="{Binding IsRyuLdnEnabled}"/>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarHelpFaqAndGuides}" Icon="{ext:Icon fa-solid fa-question}" >
|
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarHelpFaqAndGuides}" Icon="{ext:Icon fa-solid fa-question}" >
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Name="FaqMenuItem"
|
Name="FaqMenuItem"
|
||||||
Header="{ext:Locale MenuBarHelpFaq}"
|
Header="{ext:Locale MenuBarHelpFaq}"
|
||||||
Icon="{ext:Icon fa-brands fa-gitlab}"
|
Icon="{ext:Icon fa-brands fa-gitlab}"
|
||||||
CommandParameter="https://git.ryujinx.app/ryubing/ryujinx/-/wikis/FAQ-&-Troubleshooting"
|
CommandParameter="{x:Static common:SharedConstants.FaqWikiUrl}"
|
||||||
ToolTip.Tip="{ext:Locale MenuBarHelpFaqTooltip}" />
|
ToolTip.Tip="{ext:Locale MenuBarHelpFaqTooltip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Name="SetupGuideMenuItem"
|
Name="SetupGuideMenuItem"
|
||||||
Header="{ext:Locale MenuBarHelpSetup}"
|
Header="{ext:Locale MenuBarHelpSetup}"
|
||||||
Icon="{ext:Icon fa-brands fa-gitlab}"
|
Icon="{ext:Icon fa-brands fa-gitlab}"
|
||||||
CommandParameter="https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&-Configuration-Guide"
|
CommandParameter="{x:Static common:SharedConstants.SetupGuideWikiUrl}"
|
||||||
ToolTip.Tip="{ext:Locale MenuBarHelpSetupTooltip}" />
|
ToolTip.Tip="{ext:Locale MenuBarHelpSetupTooltip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Name="LdnGuideMenuItem"
|
Name="LdnGuideMenuItem"
|
||||||
Header="{ext:Locale MenuBarHelpMultiplayer}"
|
Header="{ext:Locale MenuBarHelpMultiplayer}"
|
||||||
Icon="{ext:Icon fa-brands fa-gitlab}"
|
Icon="{ext:Icon fa-brands fa-gitlab}"
|
||||||
CommandParameter="https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide"
|
CommandParameter="{x:Static common:SharedConstants.MultiplayerWikiUrl}"
|
||||||
ToolTip.Tip="{ext:Locale MenuBarHelpMultiplayerTooltip}" />
|
ToolTip.Tip="{ext:Locale MenuBarHelpMultiplayerTooltip}" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -38,7 +38,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
|||||||
ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems();
|
ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems();
|
||||||
|
|
||||||
MiiAppletMenuItem.Command = Commands.Create(OpenMiiApplet);
|
MiiAppletMenuItem.Command = Commands.Create(OpenMiiApplet);
|
||||||
CloseRyujinxMenuItem.Command = Commands.Create(CloseWindow);
|
CloseRyujinxMenuItem.Command = Commands.Create(() => Window?.Close());
|
||||||
OpenSettingsMenuItem.Command = Commands.Create(OpenSettings);
|
OpenSettingsMenuItem.Command = Commands.Create(OpenSettings);
|
||||||
PauseEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Pause());
|
PauseEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Pause());
|
||||||
ResumeEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Resume());
|
ResumeEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Resume());
|
||||||
@ -49,6 +49,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
|||||||
XciTrimmerMenuItem.Command = Commands.Create(XciTrimmerView.Show);
|
XciTrimmerMenuItem.Command = Commands.Create(XciTrimmerView.Show);
|
||||||
AboutWindowMenuItem.Command = Commands.Create(AboutView.Show);
|
AboutWindowMenuItem.Command = Commands.Create(AboutView.Show);
|
||||||
CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityListWindow.Show());
|
CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityListWindow.Show());
|
||||||
|
LdnGameListMenuItem.Command = Commands.Create(() => LdnGamesListWindow.Show());
|
||||||
|
|
||||||
UpdateMenuItem.Command = MainWindowViewModel.UpdateCommand;
|
UpdateMenuItem.Command = MainWindowViewModel.UpdateCommand;
|
||||||
|
|
||||||
@ -235,8 +236,5 @@ namespace Ryujinx.Ava.UI.Views.Main
|
|||||||
Window.Arrange(new Rect(Window.Position.X, Window.Position.Y, windowWidthScaled, windowHeightScaled));
|
Window.Arrange(new Rect(Window.Position.X, Window.Position.Y, windowWidthScaled, windowHeightScaled));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CloseWindow() => Window.Close();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,12 +148,27 @@
|
|||||||
Text="{Binding FileExtension}"
|
Text="{Binding FileExtension}"
|
||||||
TextAlignment="Start"
|
TextAlignment="Start"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
<TextBlock
|
<Button
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalContentAlignment="Left"
|
||||||
|
Click="LdnGames_OnClick"
|
||||||
|
VerticalAlignment="Center"
|
||||||
IsVisible="{Binding HasLdnGames}"
|
IsVisible="{Binding HasLdnGames}"
|
||||||
Text="{Binding Converter={x:Static helpers:MultiplayerInfoConverter.Instance}}"
|
Background="{DynamicResource AppListBackgroundColor}"
|
||||||
TextAlignment="Start"
|
Padding="0">
|
||||||
TextWrapping="Wrap"/>
|
<TextBlock
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Tag="{Binding IdString}"
|
||||||
|
Text="{Binding Converter={x:Static helpers:MultiplayerInfoConverter.Instance}}"
|
||||||
|
TextAlignment="Start"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
<Button.Styles>
|
||||||
|
<Style Selector="Button">
|
||||||
|
<Setter Property="MinWidth"
|
||||||
|
Value="0" />
|
||||||
|
<!-- avoids very wide buttons from the overall project avalonia style -->
|
||||||
|
</Style>
|
||||||
|
</Button.Styles>
|
||||||
|
</Button>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
IsVisible="{Binding HasIndependentConfiguration}"
|
IsVisible="{Binding HasIndependentConfiguration}"
|
||||||
|
@ -38,6 +38,14 @@ namespace Ryujinx.Ava.UI.Views.Misc
|
|||||||
|
|
||||||
await CompatibilityListWindow.Show((string)playabilityLabel.Tag);
|
await CompatibilityListWindow.Show((string)playabilityLabel.Tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void LdnGames_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Button { Content: TextBlock ldnGamesLabel })
|
||||||
|
return;
|
||||||
|
|
||||||
|
await LdnGamesListWindow.Show((string)ldnGamesLabel.Tag);
|
||||||
|
}
|
||||||
|
|
||||||
private async void IdString_OnClick(object sender, RoutedEventArgs e)
|
private async void IdString_OnClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
|
@ -51,7 +51,7 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
if (DataContext is not CompatibilityViewModel cvm)
|
if (DataContext is not CompatibilityViewModel cvm)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
cvm.NameSorting(int.Parse(sortStrategy));
|
cvm.NameSorting(int.Parse(sortStrategy));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +65,5 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
cvm.StatusSorting(int.Parse(sortStrategy));
|
cvm.StatusSorting(int.Parse(sortStrategy));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
349
src/Ryujinx/UI/Windows/LdnGamesListWindow.axaml
Normal file
349
src/Ryujinx/UI/Windows/LdnGamesListWindow.axaml
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
<window:StyleableAppWindow xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||||
|
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||||
|
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
||||||
|
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||||
|
xmlns:facontrols="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
|
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||||
|
CanResize="False"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
MinWidth="800"
|
||||||
|
MinHeight="745"
|
||||||
|
x:Class="Ryujinx.Ava.UI.Windows.LdnGamesListWindow"
|
||||||
|
x:DataType="viewModels:LdnGamesListViewModel">
|
||||||
|
<window:StyleableAppWindow.DataContext>
|
||||||
|
<viewModels:LdnGamesListViewModel />
|
||||||
|
</window:StyleableAppWindow.DataContext>
|
||||||
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
<!-- UI FlushControls -->
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto" Name="FlushControls">
|
||||||
|
<controls:RyujinxLogo
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="15, 0, 7, 0"
|
||||||
|
ToolTip.Tip="{ext:WindowTitle LdnGameListTitle, False}"/>
|
||||||
|
<TextBox Grid.Column="1"
|
||||||
|
Name="SearchBoxFlush"
|
||||||
|
Margin="0, 5, 0, 5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Watermark="{ext:Locale LdnGameListSearchBoxWatermark}"
|
||||||
|
TextChanged="TextBox_OnTextChanged"/>
|
||||||
|
<Button
|
||||||
|
Grid.Column="2"
|
||||||
|
Name="InfoFlush"
|
||||||
|
Margin="10, 5, 0, 5"
|
||||||
|
MinWidth="32"
|
||||||
|
MinHeight="32"
|
||||||
|
ClipToBounds="False"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Content="{ext:Icon fa-solid fa-info}"
|
||||||
|
ToolTip.Tip="{ext:Locale LdnGameListInfoButtonToolTip}"/>
|
||||||
|
<Button
|
||||||
|
Grid.Column="3"
|
||||||
|
Name="RefreshFlush"
|
||||||
|
Margin="10, 5, 0, 5"
|
||||||
|
MinWidth="32"
|
||||||
|
MinHeight="32"
|
||||||
|
ClipToBounds="False"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
IsEnabled="{Binding !IsRefreshing}"
|
||||||
|
ToolTip.Tip="{ext:Locale LdnGameListRefreshToolTip}">
|
||||||
|
<facontrols:SymbolIcon Symbol="Refresh" />
|
||||||
|
</Button>
|
||||||
|
<StackPanel Grid.Column="4" Orientation="Horizontal" Margin="10, 5, 0, 5">
|
||||||
|
<DropDownButton
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="{ext:Locale CommonSort}"
|
||||||
|
DockPanel.Dock="Right">
|
||||||
|
<DropDownButton.Flyout>
|
||||||
|
<Flyout Placement="Bottom">
|
||||||
|
<StackPanel
|
||||||
|
Margin="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Orientation="Vertical">
|
||||||
|
<StackPanel>
|
||||||
|
<RadioButton
|
||||||
|
IsCheckedChanged="Sort_Name_Checked"
|
||||||
|
Content="{ext:Locale GameListSortStatusNameAscending}"
|
||||||
|
GroupName="Sort"
|
||||||
|
IsChecked="True"
|
||||||
|
Tag="0" />
|
||||||
|
<RadioButton
|
||||||
|
IsCheckedChanged="Sort_Name_Checked"
|
||||||
|
Content="{ext:Locale GameListSortStatusNameDescending}"
|
||||||
|
GroupName="Sort"
|
||||||
|
Tag="1" />
|
||||||
|
</StackPanel>
|
||||||
|
<Border
|
||||||
|
Width="60"
|
||||||
|
Height="2"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
BorderBrush="White"
|
||||||
|
BorderThickness="0,1,0,0">
|
||||||
|
<Separator Height="0" HorizontalAlignment="Stretch" />
|
||||||
|
</Border>
|
||||||
|
<RadioButton
|
||||||
|
IsCheckedChanged="Sort_PlayerCount_Checked"
|
||||||
|
Content="{ext:Locale LdnGameListPlayerSortDisable}"
|
||||||
|
GroupName="Order"
|
||||||
|
IsChecked="true"
|
||||||
|
Tag="0" />
|
||||||
|
<RadioButton
|
||||||
|
IsCheckedChanged="Sort_PlayerCount_Checked"
|
||||||
|
Content="{ext:Locale LdnGameListPlayerSortAscending}"
|
||||||
|
GroupName="Order"
|
||||||
|
Tag="1" />
|
||||||
|
<RadioButton
|
||||||
|
IsCheckedChanged="Sort_PlayerCount_Checked"
|
||||||
|
Content="{ext:Locale LdnGameListPlayerSortDescending}"
|
||||||
|
GroupName="Order"
|
||||||
|
Tag="2" />
|
||||||
|
</StackPanel>
|
||||||
|
</Flyout>
|
||||||
|
</DropDownButton.Flyout>
|
||||||
|
</DropDownButton>
|
||||||
|
</StackPanel>
|
||||||
|
<DropDownButton
|
||||||
|
Grid.Column="5"
|
||||||
|
Margin="10, 0, 148, 0"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="{ext:Locale LdnGameListFiltersHeading}"
|
||||||
|
DockPanel.Dock="Right">
|
||||||
|
<DropDownButton.Flyout>
|
||||||
|
<Flyout Placement="Bottom">
|
||||||
|
<StackPanel>
|
||||||
|
<CheckBox IsChecked="{Binding OnlyShowForOwnedGames}">
|
||||||
|
<TextBlock Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
|
||||||
|
</CheckBox>
|
||||||
|
<CheckBox IsChecked="{Binding OnlyShowPublicGames}">
|
||||||
|
<TextBlock Text="{ext:Locale LdnGameListFiltersOnlyShowPublicGames}" />
|
||||||
|
</CheckBox>
|
||||||
|
<CheckBox IsChecked="{Binding OnlyShowJoinableGames}">
|
||||||
|
<TextBlock Text="{ext:Locale LdnGameListFiltersOnlyShowJoinableGames}" />
|
||||||
|
</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Flyout>
|
||||||
|
</DropDownButton.Flyout>
|
||||||
|
</DropDownButton>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- UI NormalControls -->
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Name="NormalControls">
|
||||||
|
<TextBox Name="SearchBoxNormal" Grid.Column="0" Margin="20, 5, 0, 5" HorizontalAlignment="Stretch"
|
||||||
|
Watermark="{ext:Locale LdnGameListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" />
|
||||||
|
<Button
|
||||||
|
Grid.Column="1"
|
||||||
|
Name="InfoNormal"
|
||||||
|
Margin="10, 5, 0, 5"
|
||||||
|
MinWidth="32"
|
||||||
|
MinHeight="32"
|
||||||
|
ClipToBounds="False"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Content="{ext:Icon fa-solid fa-info}"
|
||||||
|
ToolTip.Tip="{ext:Locale LdnGameListInfoButtonToolTip}"/>
|
||||||
|
<Button Grid.Column="2" Name="RefreshNormal" Margin="10, 5, 0, 5" MinWidth="32" MinHeight="32" ClipToBounds="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" IsEnabled="{Binding !IsRefreshing}">
|
||||||
|
<facontrols:SymbolIcon Symbol="Refresh" />
|
||||||
|
</Button>
|
||||||
|
<StackPanel Grid.Column="3" Orientation="Horizontal" Margin="10, 5, 0, 5">
|
||||||
|
<DropDownButton
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="{ext:Locale CommonSort}"
|
||||||
|
DockPanel.Dock="Right">
|
||||||
|
<DropDownButton.Flyout>
|
||||||
|
<Flyout Placement="Bottom">
|
||||||
|
<StackPanel
|
||||||
|
Margin="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Orientation="Vertical">
|
||||||
|
<StackPanel>
|
||||||
|
<RadioButton
|
||||||
|
IsCheckedChanged="Sort_Name_Checked"
|
||||||
|
Content="{ext:Locale GameListSortStatusNameAscending}"
|
||||||
|
GroupName="Sort"
|
||||||
|
IsChecked="True"
|
||||||
|
Tag="0" />
|
||||||
|
<RadioButton
|
||||||
|
IsCheckedChanged="Sort_Name_Checked"
|
||||||
|
Content="{ext:Locale GameListSortStatusNameDescending}"
|
||||||
|
GroupName="Sort"
|
||||||
|
Tag="1" />
|
||||||
|
</StackPanel>
|
||||||
|
<Border
|
||||||
|
Width="60"
|
||||||
|
Height="2"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
BorderBrush="White"
|
||||||
|
BorderThickness="0,1,0,0">
|
||||||
|
<Separator Height="0" HorizontalAlignment="Stretch" />
|
||||||
|
</Border>
|
||||||
|
<RadioButton
|
||||||
|
IsCheckedChanged="Sort_PlayerCount_Checked"
|
||||||
|
Content="{ext:Locale LdnGameListPlayerSortDisable}"
|
||||||
|
GroupName="Order"
|
||||||
|
IsChecked="true"
|
||||||
|
Tag="0" />
|
||||||
|
<RadioButton
|
||||||
|
IsCheckedChanged="Sort_PlayerCount_Checked"
|
||||||
|
Content="{ext:Locale LdnGameListPlayerSortAscending}"
|
||||||
|
GroupName="Order"
|
||||||
|
Tag="1" />
|
||||||
|
<RadioButton
|
||||||
|
IsCheckedChanged="Sort_PlayerCount_Checked"
|
||||||
|
Content="{ext:Locale LdnGameListPlayerSortDescending}"
|
||||||
|
GroupName="Order"
|
||||||
|
Tag="2" />
|
||||||
|
</StackPanel>
|
||||||
|
</Flyout>
|
||||||
|
</DropDownButton.Flyout>
|
||||||
|
</DropDownButton>
|
||||||
|
</StackPanel>
|
||||||
|
<DropDownButton
|
||||||
|
Grid.Column="4"
|
||||||
|
Margin="10, 5, 20, 5"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="{ext:Locale LdnGameListFiltersHeading}"
|
||||||
|
DockPanel.Dock="Right">
|
||||||
|
<DropDownButton.Flyout>
|
||||||
|
<Flyout Placement="Bottom">
|
||||||
|
<StackPanel>
|
||||||
|
<CheckBox IsChecked="{Binding OnlyShowForOwnedGames}">
|
||||||
|
<TextBlock Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
|
||||||
|
</CheckBox>
|
||||||
|
<CheckBox IsChecked="{Binding OnlyShowPublicGames}">
|
||||||
|
<TextBlock Text="{ext:Locale LdnGameListFiltersOnlyShowPublicGames}" />
|
||||||
|
</CheckBox>
|
||||||
|
<CheckBox IsChecked="{Binding OnlyShowJoinableGames}">
|
||||||
|
<TextBlock Text="{ext:Locale LdnGameListFiltersOnlyShowJoinableGames}" />
|
||||||
|
</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Flyout>
|
||||||
|
</DropDownButton.Flyout>
|
||||||
|
</DropDownButton>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- List of open LDN games -->
|
||||||
|
<ScrollViewer Grid.Row="1">
|
||||||
|
<ListBox Margin="12, 0, 13, 0"
|
||||||
|
Background="Transparent"
|
||||||
|
ItemsSource="{Binding VisibleEntries}">
|
||||||
|
<ListBox.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel
|
||||||
|
ItemHeight="125"
|
||||||
|
ItemWidth="450"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Orientation="Horizontal" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListBox.ItemsPanel>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type models:LdnGameModel}">
|
||||||
|
<Border
|
||||||
|
Margin="10"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
CornerRadius="4"
|
||||||
|
Background="Transparent">
|
||||||
|
<Grid ColumnDefinitions="Auto,Auto,*" RowDefinitions="Auto,Auto,Auto,*" Width="420" Height="110" HorizontalAlignment="Center">
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||||
|
Text="{Binding Title.Name}"
|
||||||
|
Margin="7, 0,0, 0"
|
||||||
|
Width="250"
|
||||||
|
ToolTip.Tip="{Binding Title.Id}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
ClipToBounds="False"
|
||||||
|
TextAlignment="Left"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0"
|
||||||
|
Text="{Binding Title.Version}"
|
||||||
|
Margin="7, 0,0, 0"
|
||||||
|
Width="250"
|
||||||
|
ToolTip.Tip="{Binding Title.Id}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
ClipToBounds="False"
|
||||||
|
TextAlignment="Left"/>
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0"
|
||||||
|
Text="{Binding FormattedCreatedAt}"
|
||||||
|
ToolTip.Tip="{Binding CreatedAtToolTip}"
|
||||||
|
Margin="7, 0,0, 0"
|
||||||
|
Width="250"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
ClipToBounds="False"
|
||||||
|
TextAlignment="Left" />
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="2"
|
||||||
|
Margin="0, 0,7, 0"
|
||||||
|
IsVisible="{Binding IsJoinable}"
|
||||||
|
Text="{ext:Locale LdnGameListJoinable}"
|
||||||
|
ToolTip.Tip="{ext:Locale LdnGameListJoinableToolTip}"
|
||||||
|
Foreground="MediumSeaGreen"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
ClipToBounds="False"
|
||||||
|
TextAlignment="Right" />
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="2"
|
||||||
|
Margin="0, 0,7, 0"
|
||||||
|
IsVisible="{Binding !IsJoinable}"
|
||||||
|
Text="{ext:Locale LdnGameListNotJoinable}"
|
||||||
|
ToolTip.Tip="{ext:Locale LdnGameListNotJoinableToolTip}"
|
||||||
|
Foreground="IndianRed"
|
||||||
|
TextDecorations="Underline"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
ClipToBounds="False"
|
||||||
|
TextAlignment="Right" />
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="2"
|
||||||
|
Margin="0, 0,7, 0"
|
||||||
|
IsVisible="{Binding IsPublic}"
|
||||||
|
Text="{ext:Locale LdnGameListPublic}"
|
||||||
|
ToolTip.Tip="{ext:Locale LdnGameListPublicToolTip}"
|
||||||
|
Foreground="LawnGreen"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
ClipToBounds="False"
|
||||||
|
TextAlignment="Right" />
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="2"
|
||||||
|
Margin="0, 0,7, 0"
|
||||||
|
IsVisible="{Binding !IsPublic}"
|
||||||
|
Text="{ext:Locale LdnGameListPrivate}"
|
||||||
|
ToolTip.Tip="{ext:Locale LdnGameListPrivateToolTip}"
|
||||||
|
Foreground="DarkRed"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
ClipToBounds="False"
|
||||||
|
TextAlignment="Right" />
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="2"
|
||||||
|
Margin="0, 0,7, 0"
|
||||||
|
Text="{Binding ConnectionTypeLocaleKey, Converter={x:Static helpers:LocaleKeyValueConverter.Shared}}"
|
||||||
|
ToolTip.Tip="{Binding ConnectionTypeToolTipLocaleKey, Converter={x:Static helpers:LocaleKeyValueConverter.Shared}}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
TextWrapping="NoWrap"
|
||||||
|
ClipToBounds="False"
|
||||||
|
TextAlignment="Right" />
|
||||||
|
<StackPanel Margin="7, 0,0, 0" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding PlayersLabel}" TextAlignment="Left" />
|
||||||
|
<TextBlock Text="{Binding FormattedPlayers}" TextAlignment="Center" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</window:StyleableAppWindow>
|
79
src/Ryujinx/UI/Windows/LdnGamesListWindow.axaml.cs
Normal file
79
src/Ryujinx/UI/Windows/LdnGamesListWindow.axaml.cs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Gommon;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Systems.Configuration;
|
||||||
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
using Ryujinx.Common;
|
||||||
|
using Ryujinx.Common.Helper;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Windows
|
||||||
|
{
|
||||||
|
public partial class LdnGamesListWindow : StyleableAppWindow
|
||||||
|
{
|
||||||
|
public static async Task Show(string searchTerm = null)
|
||||||
|
{
|
||||||
|
using LdnGamesListViewModel ldnGamesListVm = new(RyujinxApp.MainWindow.ViewModel);
|
||||||
|
|
||||||
|
await ShowAsync(new LdnGamesListWindow
|
||||||
|
{
|
||||||
|
DataContext = ldnGamesListVm,
|
||||||
|
SearchBoxFlush = { Text = searchTerm ?? string.Empty },
|
||||||
|
SearchBoxNormal = { Text = searchTerm ?? string.Empty }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public LdnGamesListWindow() : base(useCustomTitleBar: true, 37)
|
||||||
|
{
|
||||||
|
Title = RyujinxApp.FormatTitle(LocaleKeys.LdnGameListTitle);
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
FlushControls.IsVisible = !ConfigurationState.Instance.ShowOldUI;
|
||||||
|
NormalControls.IsVisible = ConfigurationState.Instance.ShowOldUI;
|
||||||
|
|
||||||
|
RefreshFlush.Command = RefreshNormal.Command =
|
||||||
|
Commands.Create(() => (DataContext as LdnGamesListViewModel)?.RefreshAsync().OrCompleted());
|
||||||
|
|
||||||
|
InfoFlush.Command = InfoNormal.Command =
|
||||||
|
Commands.Create(() => OpenHelper.OpenUrl(SharedConstants.MultiplayerWikiUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once UnusedMember.Local
|
||||||
|
// its referenced in the axaml but rider keeps yelling at me that its unused so
|
||||||
|
private void TextBox_OnTextChanged(object sender, TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not LdnGamesListViewModel cvm)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (sender is not TextBox searchBox)
|
||||||
|
return;
|
||||||
|
|
||||||
|
cvm.Search(searchBox.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Sort_Name_Checked(object sender, RoutedEventArgs args)
|
||||||
|
{
|
||||||
|
if (sender is RadioButton { Tag: string sortStrategy })
|
||||||
|
{
|
||||||
|
if (DataContext is not LdnGamesListViewModel cvm)
|
||||||
|
return;
|
||||||
|
|
||||||
|
cvm.NameSorting(int.Parse(sortStrategy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Sort_PlayerCount_Checked(object sender, RoutedEventArgs args)
|
||||||
|
{
|
||||||
|
if (sender is RadioButton { Tag: string sortStrategy })
|
||||||
|
{
|
||||||
|
if (DataContext is not LdnGamesListViewModel cvm)
|
||||||
|
return;
|
||||||
|
|
||||||
|
cvm.StatusSorting(int.Parse(sortStrategy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,6 @@ using Avalonia.Threading;
|
|||||||
using DynamicData;
|
using DynamicData;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Gommon;
|
using Gommon;
|
||||||
using LibHac.Ns;
|
|
||||||
using Ryujinx.Ava.Common;
|
using Ryujinx.Ava.Common;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Input;
|
using Ryujinx.Ava.Input;
|
||||||
@ -18,6 +17,7 @@ using Ryujinx.Ava.Systems.Configuration;
|
|||||||
using Ryujinx.Ava.Systems.Configuration.UI;
|
using Ryujinx.Ava.Systems.Configuration.UI;
|
||||||
using Ryujinx.Ava.UI.Applet;
|
using Ryujinx.Ava.UI.Applet;
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.Ava.Utilities;
|
using Ryujinx.Ava.Utilities;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
@ -185,12 +185,11 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
ViewModel.LdnData.Clear();
|
ViewModel.LdnModels = e.LdnData;
|
||||||
|
ViewModel.UsableLdnData.Clear();
|
||||||
foreach (ApplicationData application in ViewModel.Applications.Where(it => it.HasControlHolder))
|
foreach (ApplicationData application in ViewModel.Applications.Where(it => it.HasControlHolder))
|
||||||
{
|
{
|
||||||
ref ApplicationControlProperty controlHolder = ref application.ControlHolder.Value;
|
ViewModel.UsableLdnData[application.IdString] = LdnGameModel.GetArrayForApp(e.LdnData, ref application.ControlHolder.Value);
|
||||||
|
|
||||||
ViewModel.LdnData[application.IdString] = e.LdnData.Where(ref controlHolder);
|
|
||||||
|
|
||||||
UpdateApplicationWithLdnData(application);
|
UpdateApplicationWithLdnData(application);
|
||||||
}
|
}
|
||||||
@ -201,7 +200,7 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
|
|
||||||
private void UpdateApplicationWithLdnData(ApplicationData application)
|
private void UpdateApplicationWithLdnData(ApplicationData application)
|
||||||
{
|
{
|
||||||
if (application.HasControlHolder && ViewModel.LdnData.TryGetValue(application.IdString, out LdnGameData.Array ldnGameDatas))
|
if (application.HasControlHolder && ViewModel.UsableLdnData.TryGetValue(application.IdString, out LdnGameModel.Array ldnGameDatas))
|
||||||
{
|
{
|
||||||
application.PlayerCount = ldnGameDatas.PlayerCount;
|
application.PlayerCount = ldnGameDatas.PlayerCount;
|
||||||
application.GameCount = ldnGameDatas.GameCount;
|
application.GameCount = ldnGameDatas.GameCount;
|
||||||
|
Loading…
Reference in New Issue
Block a user